# Reckoner 引擎手册 [English](engine.md) Reckoner 插件的全部计算都在一台确定性的**引擎**内完成。 | 引擎的全部职责 | 顺序 | |---|---| | 解析 | 一条公式 | | 推导 | 每个中间值的 SI 向量 | | 求值 | 算术 | | 记录 | 本次调用 | | 它持有 | 它不持有 | |---|---| | SI 名称表及其仿射映射(§6.1) | 领域知识 | | 未封闭记录的槽表 | 求解器 | | 每次调用的轨迹 | 具名公式表 | 它唯一执行的换算是自身 SI 表(§6.1)中名称所带的仿射映射。每个宿主进程运行一台引擎:任何会话的标记都作用于它,且任何时刻至多有一条记录未封闭。同样的内容以面向模型的形态由 `reckoner-interface` 与 `reckoner-template` 两个技能承载,二者由插件注册。 ## 目录 - [1. 引擎是什么](#1-引擎是什么) - [2. 插件工具面](#2-插件工具面) - [3. 值](#3-值) - [4. 公式](#4-公式) - [5. 记号](#5-记号) - [6. 量纲](#6-量纲) - [7. 记录与轨迹](#7-记录与轨迹) - [8. 错误](#8-错误) - [9. 存储与日志](#9-存储与日志) - [10. 文章生成](#10-文章生成) - [11. 面板](#11-面板) - [12. 外部检索](#12-外部检索) ## 1. 引擎是什么 | 引擎负责 | 调用方负责 | |---|---| | 把一条公式解析成表达式树 | 写出公式 | | 推导每个中间值以及结果的 SI 向量 | 为每个值写出它自己的 `dim` | | 对数值与向量做算术 | 选定所使用的关系 | | 拒绝无法按原样存下的值 | 修改公式并再次调用 `eval` | | 把结果写入 target 槽位 | 用 `get` 读回它 | | 每次调用向未封闭记录追加一行轨迹 | —— | - **没有领域知识。** 引擎不懂物理、不懂电子学,也不认识任何具名公式;数学只存在于交给它的那条公式里。 - **没有求解器。** 无法向引擎询问未知量、解方程或反解某个关系;每一步都是调用方写出的表达式。 - **除名称表之外不做单位换算。** 该表只收 SI 名称;以任何其他单位给出的量,都由调用方在 `set` 之前换算,再由 `dim` 指明换算所得的量(§3.3)。 - **算术只在 `eval` 内发生。** `set` 只是转录它收到的内容,在这个边界上不做任何计算。 - **数字来自槽位,或来自公式自身的字面量。** 除槽位表之外,引擎在两次调用之间不保留任何记忆,因此推导中的每个量要么是存下的值,要么是写进公式的常数。 - **确定性。** 同一张槽位表与同一条公式得到同一个值:没有随机,算术中不含时钟,也不访问网络。 ## 2. 插件工具面 ### 2.1 六个操作 - `set` 接受 `name`(槽位名)与 `value`(一个标签值,或用 `null` 删除该槽位);成功收据为 `{ ok:true, name, rev, value }`,删除为 `{ ok:true, name, rev:null, value:null }`。 - `get` 接受 `name`,以及可选的 `form`、`digits`、`dim`;成功收据为 `{ ok:true, name, value }`。 - `eval` 接受 `formula`(一条表达式)与 `target`(槽位名);成功收据为 `{ ok:true, target, rev }`。 - `record_start` 接受 `title`(非空字符串);成功收据为 `{ ok:true }`。 - `record_message` 接受 `text`(非空字符串)与可选的 `hide`(布尔值);成功收据为 `{ ok:true }`。 - `record_end` 接受可选的 `text`(字符串);成功收据为 `{ ok:true }`。 - 没有记录未封闭时,`set`、`get` 与 `eval` 都会被拒(§7.1)。 - `name` 与 `target` 都是裸槽位名,绝不带 `@`:`@` 形式只存在于公式内(§4.3)。 - `set` 存下一个值并回显实际存下的内容;`get` 是唯一返回值的操作。 - `eval` 不返回它的值。收据只给出被写入的槽位及其新版本号;值要用 `get` 读回,或在后续公式里以 `@name` 引用。 ### 2.2 收据 每次调用都返回一张 JSON 收据,不存在第二条失败通道: ``` success: set -> { ok:true, name, rev, value } delete: { ok:true, name, rev:null, value:null } get -> { ok:true, name, value } eval -> { ok:true, target, rev } markers -> { ok:true } failure: -> { ok:false, code, error } ``` `error` 是写给读者的一句话:失败的具体取值、边界或期望,以及修正方式。`code` 是同一个失败中稳定的机器可读部分(§8)。失败调用不改动任何槽位、也不动任何版本号;但有记录未封闭时,该次失败仍会以 `ok: false` 的一行追加进该记录(§7.2)。 ### 2.3 槽位表及其规则 - **名字**:标识符,以字母或下划线开头,其后为字母、数字或下划线。同一条规则覆盖槽位名、`eval` 的 target、对象的字段名与绑定变量名。 - **写入**:向不存在的名字写入即创建该槽位,版本号为 1。 - **覆盖**:整体替换其值并把版本号加一。旧值不被继承,也不钉死任何向量:槽位可以被任何向量的值覆盖。 - **删除**:以 `value: null` 执行 `set`,且是幂等的:删除不存在的槽位同样是 `ok`,之后重新创建时版本号从 1 开始。 - **失败**:失败的操作什么都不写。 - **生命周期**:槽位表的存在期恰好等于记录未封闭的期间;`record_end` 会清空它,因此表非空就意味着有记录未封闭。 ## 3. 值 ### 3.1 四种值类型 | 类型 | 内容 | 向量 | |---|---|---| | `number` | 一个 JSON 数字(`num`) | 一个 SI 向量 | | `complex` | `re` 与 `im`,以直角形式存储 | 一个 SI 向量 | | `array` | `items`,每个元素是数字、复数或嵌套数组 | 一个 SI 向量,由全部元素共享 | | `object` | 具名字段 `fields`,每个字段都是一个完整的值 | 每个字段各一个向量;对象自身不带向量 | - 一个值的身份就是它的 SI 向量:按 ISO 80000-1 顺序(m, kg, s, A, K, mol, cd)的 7 个整数指数。向量相同的两个值就是同一个量,无论写法如何;§6.1 的 kind 标签只是给人看的名字,它从不被存储,也从不决定任何事。 - 数值位置只接受 JSON 数字,即有限双精度浮点数。字符串、布尔值或 `null` 出现在数值位置会以 `ENGINE_INVALID_ARGS` 被拒。 - 只有整数向量才能进入槽位:带有分数指数的向量会在任何写入之前被拒(§6.4)。 ### 3.2 `set` 的标签结构 `set` 的 `value` 是一个恰好携带一个标签的 JSON 对象,另可带 `dim`: ``` {"num": 4.7e3} a real {"re": 3, "im": 4} a complex, rectangular {"mag": 5, "ang": 0.927295218} a complex, polar (radians) {"array": [1, 2, 3]} an array {"object": {"v": {"num": 12}}} an object ``` - **恰好一个标签。** 标签为 `num`、`re` 配 `im`、`mag` 配 `ang`、`array` 与 `object`;一个标签都没有、或有两个以上,都是 `ENGINE_INVALID_ARGS`。未知的键会连同标签词表一起被拒;`dim` 与 `object` 同时出现也会被拒,因为对象的量纲按字段给出。 - **成对的两半必须齐备。** 只有 `re` 而没有 `im`、只有 `mag` 而没有 `ang`,都会被拒,并各自指明它需要的那一对。 - **极坐标在入口处换算。** `mag` 与 `ang` 换算为 `re = mag*cos(ang)` 与 `im = mag*sin(ang)`;存下的值永远是直角形式。 - **数组元素是裸的。** 元素是数字、`{re,im}`、`{mag,ang}` 或嵌套数组,自身不带标签、也不带 `dim`,因为整个数组共享一个向量。对象永远不能作为数组的元素。 - **对象按字段携带 `dim`。** `object` 把字段名映射到完整的值,这些值按同一条规则解析,因此字段本身也可以是对象;字段名必须满足 §2.3 的名字规则。 ### 3.3 `dim` `dim` 是值的向量的书写形式: - 一个**表名**(§6.1):该行的向量,连同该行的仿射映射。 - **7 个整数**,顺序为 m,kg,s,A,K,mol,cd:该向量,不带仿射映射。 - 省略或为 `null`:零向量。 - 其他任何取值都是 `ENGINE_INVALID_DIMENSION`:不指名任何行的字符串(信息会列出全部名称)、长度不对的数组,或含非整数分量的数组。 - 对 `num`、`re`/`im` 与 `mag`/`ang`,仿射映射在存下之前施加:`SI = x*factor + offset`,其中偏移只作用于实部,而虚部仅乘以 factor。数组的裸元素按原样、按该数组的向量存下。`degC` 是表中唯一映射不是恒等的名称(§6.1)。 ### 3.4 `get` `get {name, form?, digits?, dim?}` 读取一个槽位。读取从不改变存下的内容。 | 选项 | 含义 | |---|---| | `form: "rect"` | 每个标量叶子渲染为 `{re, im}` | | `form: "polar"` | 每个标量叶子渲染为 `{mag, ang}`,`ang` 以弧度计 | | 省略 `form` | 保持存下的形式:实数仍为 `{num}`,复数仍为 `{re, im}` | | `digits` | 正整数:每个叶子数字都被舍入到至多这么多**有效**数字,绝不补零 | | `dim` 为表名 | 各叶子经该行的仿射映射从 SI 换算回该写法,且收据的 `dim` 就是该名称 | | `dim` 为 7 个整数 | 用它们校验存下的向量,不做任何换算;收据的 `dim` 就是这 7 个整数 | | 省略 `dim` | 收据的 `dim` 是该向量的首个表名;该向量没有对应行时,就是那 7 个整数 | - `form`、`digits` 与 `dim` 同样作用于每一个标量叶子:对象的字段与数组的元素都包括在内。 - 与存下的向量不符的 `dim` 会以 `ENGINE_INCOMPATIBLE_DIMENSION` 被拒,并写出两个向量;对象会逐字段校验,且不做任何换算。 - `polar` 下的负实数是 `{mag: -x, ang: pi}`;`rect` 下的实数是 `{re: x, im: 0}`。 - `form` 只能是 `"rect"` 或 `"polar"`,`digits` 只能是正整数;其他取值都是 `ENGINE_INVALID_ARGS`。 - 收据形状:实数为 `{num, dim}`,复数为 `{re, im, dim}` 或 `{mag, ang, dim}`,数组为 `{array: [裸元素], dim}`(元素为裸数字、`{re,im}`/`{mag,ang}` 标量或嵌套数组),对象为 `{object: {字段: <收据>}}`,自身不带 `dim`。 - 收据就是 `set` 接受的标签结构,因此可以原样喂回 `set`。带 `digits` 的收据中的数字已经被舍入过,喂回时存下的就是被舍入的数字。 ## 4. 公式 ### 4.1 字符集与字面量 公式是 ASCII。扫描器接受数字;名字(以字母或下划线开头,其后为字母、数字或下划线);`$name`;`@name`;空白字符空格、制表符与换行;以及标点 `+ - * / ^ ( ) [ ] { } , . _ =` 与双字符记号 `->`。其他任何字符都会以 `ENGINE_INVALID_FORMULA` 被拒,其信息给出该字符、它的码位以及整个字符集。 | 字面量 | 读作 | |---|---| | `12`、`4.7` | 实数;小数点后必须有数字 | | `1e5`、`2.5e-3` | 科学计数法实数:小写 `e`、可选符号、至少一位指数数字 | | `2j`、`4i` | 虚数字面量(`{re: 0, im: 2}`);`i` 与 `j` 是虚数后缀 | | `2.5j`、`1e3i` | 同一后缀用于小数或指数形式 | - 标量字面量永远是无量纲的:它携带零向量。 - `e` 之后没有指数数字是 `ENGINE_INVALID_NUMBER`。 - 数字后紧跟字母是 `ENGINE_INVALID_IDENTIFIER`:字母只能作为虚数后缀 `i` 或 `j` 跟在数字之后。`1E5` 与 `2x` 都会被拒;对后者,信息给出改写 `2*x`。 - 没有布尔字面量、没有字符串字面量、没有单位字面量,也没有数组或对象字面量:公式里只有数字、槽位与记号。 ### 4.2 语法 ``` formula := additive EOF additive := multiplicative (('+' | '-') multiplicative)* multiplicative := unary (('*' | '/') unary)* unary := ('-' | '+') unary | power power := postfix ('^' unary)? postfix := primary ('[' additive ']' | '.' NAME)* primary := NUMBER | NAME | SLOT | SYMBOL | '(' additive ')' SLOT := '@' NAME SYMBOL := '$' NAME | '$' NAME '(' args ')' | '$' NAME '_' '{' subscript '}' '^' '{' additive '}' '(' args ')' | '$' NAME '_' '{' subscript '}' '(' args ')' subscript := NAME '=' additive | NAME '->' additive | additive args := [ additive (',' additive)* ] ``` - `SYMBOL` 的四种形状分别是常量、函数、带两个位置的绑定记号,以及只带下标的形状(`$diff` 与不限界的 `$integral` 用第二种形状书写)。一个记号接受哪些形状、接受几个参数,由记号表规定(§5)。 - 一条公式恰好是**一条**表达式:完整表达式之后又出现记号是 `ENGINE_INVALID_FORMULA`,而不是第二条语句。 - 优先级依次为 additive、multiplicative、unary、power、postfix、primary:`*` 与 `/` 比 `+` 与 `-` 结合得更紧,一元符号在左侧比 `^` 结合得更松,而 `^` 右结合。`-2^2` 是 `-4`,`2^3^2` 是 `2^(3^2)`。 - 前置 `+` 被丢弃;前置 `-` 表示取负。 - 乘法必须写出 `*`。`2@R`、`2$pi` 与 `2(3)` 都会被拒。 - 位置就是紧跟在记号名之后的那个花括号,因此其他所有 `^` 都是幂运算符:`$e^(2)` 与 `$pi^2` 都是幂,而 `^` 之后紧跟 `{` 会被拒。 - 圆括号用于分组;未被闭合的 `(` 或 `[` 会被拒,并指名打开它的那个字符。 ### 4.3 数据访问 - `@name` 读取一个槽位的值。 - `@name[index]` 读取数组的一个元素;下标是一条 additive 表达式。 - `@name.field` 读取对象的一个字段;字段名是字面名,不是表达式。 - 链式书写:`@net.ports[0].z` 在同一条路径上读取下标与字段。 - `@name` 只读取;一次调用所写入的槽位由 `eval` 的 `target` 参数给出(§2.1)。任何地方都不存储引用,引用也永不进入值或收据。 - 不存在的槽位是 `ENGINE_SLOT_NOT_FOUND`。 - 下标必须求值为带零向量的整数:一个数字,或 `im` 为 0 的复数。其他任何取值,以及落在 `0..len-1` 之外的下标,都是 `ENGINE_INVALID_INDEX`;后者会给出数组长度。 - 对不是数组的值使用 `[ ]`、对不是对象的值使用 `.`,都是 `ENGINE_UNSUPPORTED_INDEX`。对象没有该字段是 `ENGINE_FIELD_NOT_FOUND`,信息会列出它实际拥有的字段。 - 裸名字是绑定变量(§5.4);其他任何裸名字都是 `ENGINE_NAME_NOT_BOUND`,其信息给出改写 `@name`。 ### 4.4 数组 - 数组的元素共享一个向量,嵌套数组也共享它,因此整个结构只携带一个向量。 - 运算符逐元素作用。两个数组必须长度相同,否则 `ENGINE_INVALID_ARGS` 会给出两个长度;一侧是标量时,它广播到另一侧的每个元素。 - 一元函数逐元素映射并重新收集,向量保持不变。`$len` 改为数出元素个数,`$transpose` 则重排一个矩形的二维数组(§5.3)。 - 对象上不定义任何运算符与函数:先读取字段(§4.3)。这类拒绝是 `ENGINE_UNSUPPORTED_OPERATION`,同一个错误码也拒绝把对象作为数组元素构造。 - 没有矩阵代数:数组由 `$seq` 构造,由 `[i]` 索引,二维数组的转置由 `$transpose` 完成。 ### 4.5 幂 - 指数必须携带零向量,否则是 `ENGINE_INCOMPATIBLE_DIMENSION`。 - 实指数把底的向量按该指数缩放,因此 `(4volt)^2` 量纲为 `volt^2`(§6.2)。 - 复指数要求底是无量纲的,因为 `a^z` 即 `exp(z*Log a)`:结果无量纲,而底为零是 `ENGINE_UNDEFINED_RESULT`。 - `0^0` 是 1,零的负数次幂是 `ENGINE_UNDEFINED_RESULT`,负实数底配分数指数也是 `ENGINE_UNDEFINED_RESULT`,因为它没有实数值。 - 任一侧为复数、或其虚部不为零时,结果是复数;否则结果是实数。 ### 4.6 `eval` 的 target - `target` 是必填项,且是裸槽位名。 - 结果必须具有整数向量:分数向量会在任何写入之前被拒(§6.4)。 - target 无条件被写入:该次写入替换槽位原有的内容并把版本号加一(§2.3)。不会拿向量与该槽位原有内容做任何比较。 - 一次调用产生一行轨迹,因此每个求值过的步骤都在记录里留下自己的公式与结果(§7.2)。 ### 4.7 这门语言没有的东西 没有赋值、没有比较、没有逻辑、没有条件、也没有语句序列:没有 `if`,没有 `==`,没有 `&&`,没有 `x = ...`,也无法在一次调用里写两条表达式。`=` 只出现在下标位里(`_{k=a}`),在那里表示绑定。没有自定义函数,没有注释语法,也没有单位或量纲的字面量。 ## 5. 记号 ### 5.1 命名空间与位置 - 每个记号名都以 `$` 开头。该命名空间属于引擎自身:槽位名(`sum`、`abs`、`ohm`)永不与记号冲突,也没有任何保留字。 - 不在表内的 `$name` 是 `ENGINE_INVALID_NOTATION`,其信息列出全部 35 个名称。 - 记号可以带下标位 `_{...}`,并在它定义了上界时带上标位 `^{...}`,两者都写成紧跟在记号名之后的那个花括号:`$sum_{k=a}^{b}(body)`。下标位要么放该记号自己的绑定形态 `名字 = 表达式` 或 `名字 -> 表达式`,要么放一条普通表达式;位置里的裸名字仍然是绑定变量。 - 常量不带圆括号、也不带任何位置;函数只在圆括号里接受参数;绑定记号在下标位与上标位里给出界,在圆括号里给出 body。 - 参数个数按记号表校验(§5.2、§5.3、§5.4),个数不对是 `ENGINE_INVALID_ARITY`。 ### 5.2 常量(5 个) | 记号 | 书写形式 | 含义 | |---|---|---| | `$pi` | 裸写 | 圆周长与直径之比 | | `$e` | 裸写 | 自然对数的底 | | `$inf` | 裸写 | 正无穷大 | | `$i` | 裸写 | 虚数单位 | | `$j` | 裸写 | 虚数单位,工程写法 | 常量不接受参数、也不接受位置:`$pi()` 与 `$pi_` 都会被拒;幂用 `^` 书写(`$e^(2)`、`$pi^2`)。 ### 5.3 函数(20 个一元,4 个二元) | 记号 | 书写形式 | 含义 | |---|---|---| | `$abs` | `$abs(x)` | 绝对值(复数的模) | | `$sqrt` | `$sqrt(x)` | 平方根;复数参数取主复根 | | `$exp` | `$exp(x)` | e 的参数次幂 | | `$ln` | `$ln(x)` | 自然对数(复数参数取主值) | | `$log` | `$log(x)` | 以 10 为底的对数 | | `$sin` | `$sin(x)` | 正弦 | | `$cos` | `$cos(x)` | 余弦 | | `$tan` | `$tan(x)` | 正切 | | `$asin` | `$asin(x)` | 反正弦,以弧度计 | | `$acos` | `$acos(x)` | 反余弦,以弧度计 | | `$atan` | `$atan(x)` | 反正切,以弧度计 | | `$floor` | `$floor(x)` | 不大于该参数的最大整数 | | `$ceil` | `$ceil(x)` | 不小于该参数的最小整数 | | `$sign` | `$sign(x)` | 参数的符号:-1、0 或 1 | | `$re` | `$re(x)` | 实部 | | `$im` | `$im(x)` | 虚部 | | `$arg` | `$arg(x)` | 辐角(相位),以弧度计 | | `$conj` | `$conj(x)` | 共轭复数 | | `$len` | `$len(a)` | 数组的元素个数 | | `$transpose` | `$transpose(M)` | 矩形二维数组的转置 | | `$atan2` | `$atan2(x, y)` | 点 `(x, y)` 的辐角,以弧度计 | | `$min` | `$min(a, b)` | 两个参数中较小者 | | `$max` | `$max(a, b)` | 两个参数中较大者 | | `$mod` | `$mod(a, b)` | `a` 除以 `b` 的余数 | 函数一律用圆括号书写;给函数加下标位会被拒,两种误用中的任何一种都会重复本表给出的书写形式。 ### 5.4 绑定记号(6 个) | 记号 | 书写形式 | 参数个数 | 可求值 | 含义 | |---|---|---|---|---| | `$sum` | `$sum_{k=a}^{b}(body)` | 1 | 是 | 让变量从下界走到上界,把 body 逐个用 `+` 累加 | | `$prod` | `$prod_{k=a}^{b}(body)` | 1 | 是 | 在同一区间上把 body 逐个用 `*` 累乘 | | `$seq` | `$seq_{k=a}^{b}(body)` | 1 | 是 | 在同一区间上把 body 收成一个数组 | | `$integral` | `$integral_{a}^{b}(body, x)` 或 `$integral(body, x)` | 2 | 否 | 定积分;第二个参数给出积分变量 | | `$limit` | `$limit_{x->a}(body)` | 1 | 否 | 极限,变量用 `->` 绑定 | | `$diff` | `$diff(body, x)` 或 `$diff(body, x, n)` | 2 或 3 | 否 | 导数,给出 `n` 时为 `n` 阶导数 | - `$sum`、`$prod`、`$seq` 与带界的 `$integral` 两个界都必填;`$limit` 只带下标位,`$diff` 两个位置都不带,`$integral` 则两者皆可(都带或都不带)。 - 界在 body 之前只求值一次,且必须是带零向量的整数,否则是 `ENGINE_INVALID_INDEX`。区间是闭区间;下界高于上界也是 `ENGINE_INVALID_INDEX`。 - 每一步都把变量绑定为一个无量纲实数。绑定是裸名字获得取值的唯一途径,且该绑定只在该记号自己的 body 内可见。 - `$seq` 产出数组;`$sum` 与 `$prod` 用 `+` 与 `*` 折叠收集到的值,因此它们的结果遵循这两个运算符的向量运算。 - `$integral`、`$limit` 与 `$diff` 可以被写出但不能被求值。对它们求值是 `ENGINE_UNSUPPORTED_SYMBOL`,其信息重复书写形式。它们存在的意义是让公式仍能**说出**自己的意思;实际要算的必须是闭式。 ## 6. 量纲 ### 6.1 名称表 引擎唯一的量纲词表就是下面这张 24 行的表。名称映射到向量,向量映射回它的行,引擎里再没有别处可以拼写量纲。 | 向量 | kind | 名称 | |---|---|---| | `[0,0,0,0,0,0,0]` | `dim-less` | `dim-less`、`radian`、`steradian` | | `[0,0,1,0,0,0,0]` | `time` | `second` | | `[1,0,0,0,0,0,0]` | `length` | `metre` | | `[0,1,0,0,0,0,0]` | `mass` | `kilogram` | | `[0,0,0,1,0,0,0]` | `current` | `ampere` | | `[0,0,0,0,1,0,0]` | `temperature` | `kelvin`、`degC` | | `[0,0,0,0,0,1,0]` | `amount-of-substance` | `mole` | | `[0,0,0,0,0,0,1]` | `luminous-intensity` | `candela`、`lumen` | | `[0,0,-1,0,0,0,0]` | `frequency` | `hertz`、`becquerel` | | `[1,1,-2,0,0,0,0]` | `force` | `newton` | | `[-1,1,-2,0,0,0,0]` | `pressure` | `pascal` | | `[2,1,-2,0,0,0,0]` | `energy` | `joule` | | `[2,1,-3,0,0,0,0]` | `power` | `watt` | | `[0,0,1,1,0,0,0]` | `charge` | `coulomb` | | `[2,1,-3,-1,0,0,0]` | `voltage` | `volt` | | `[-2,-1,4,2,0,0,0]` | `capacitance` | `farad` | | `[2,1,-3,-2,0,0,0]` | `resistance` | `ohm` | | `[-2,-1,3,2,0,0,0]` | `conductance` | `siemens` | | `[2,1,-2,-2,0,0,0]` | `inductance` | `henry` | | `[2,1,-2,-1,0,0,0]` | `magnetic-flux` | `weber` | | `[0,1,-2,-1,0,0,0]` | `flux-density` | `tesla` | | `[-2,0,0,0,0,0,1]` | `illuminance` | `lux` | | `[2,0,-2,0,0,0,0]` | `absorbed-dose` | `gray`、`sievert` | | `[0,0,-1,0,0,1,0]` | `catalytic-activity` | `katal` | - 分量的读取顺序是 m、kg、s、A、K、mol、cd。 - 没有对应行的向量没有 kind:引擎称它为 `unnamed`,并以自己的 7 个整数被提及。 - 一行的首个名称是引擎提及它时所用的名称——出现在信息里,也出现在未给 `dim` 的 `get` 收据里;其余名称是同一向量的其他可接受写法。 - 每个名称都带一个仿射映射 `SI = x*factor + offset`。`degC` 是表中唯一映射不是恒等的名称:它的 factor 为 1,offset 为 273.15。其余名称的 factor 都是 1、offset 都是 0。 - 该表只收 SI 名称。其他任何单位都由调用方在 `set` 之前换算(§1)。 ### 6.2 向量运算 | 运算 | 结果的向量 | |---|---| | `a + b`、`a - b` | 二者共享的那个向量;两个向量不同则是 `ENGINE_INCOMPATIBLE_DIMENSION` | | `a * b` | 两个向量逐分量相加 | | `a / b` | 两个向量逐分量相减 | | `a ^ p`,`p` 为实指数 | `a` 的每个分量乘以 `p` | - 标量字面量与一切零向量值都是无量纲的。无量纲因子相乘不改变另一侧的向量(`2*@R` 是电阻),而把一个无量纲值加到有量纲的值上会被拒,因为裸计数并没有说清它数的是什么。 - `$min`、`$max`、`$mod` 与 `$atan2` 要求两个参数携带同一个向量(§6.3)。 ### 6.3 各类记号的量纲规则 | 记号 | 参数的向量 | 结果的向量 | |---|---|---| | `$pi`、`$e`、`$inf`、`$i`、`$j` | 无参数 | 零向量 | | `$abs`、`$re`、`$im`、`$conj` | 任意 | 参数的向量 | | `$arg` | 任意 | 零向量(弧度) | | `$sqrt` | 任意 | 参数向量的一半 | | `$exp`、`$ln`、`$log`、`$sin`、`$cos`、`$tan`、`$asin`、`$acos`、`$atan`、`$floor`、`$ceil`、`$sign` | 零向量;其他任何向量都是 `ENGINE_INCOMPATIBLE_DIMENSION` | 零向量 | | `$len` | 任意向量,作用于数组 | 零向量 | | `$transpose` | 任意向量,作用于矩形二维数组 | 同一个向量 | | `$atan2` | 两者同一个向量 | 零向量(弧度) | | `$min`、`$max`、`$mod` | 两者同一个向量;不同则是 `ENGINE_INCOMPATIBLE_DIMENSION` | 该向量 | | `$sum`、`$prod` | 绑定变量是无量纲实数 | body 折叠所得的任何向量 | | `$seq` | 绑定变量是无量纲实数 | body 的向量,由数组元素共享 | 除向量规则之外,某些参数在取值上也有限制: - `$ln`、`$log`:实参数必须大于 0;`$ln(0)` 与 `$ln(-1)` 是 `ENGINE_UNDEFINED_RESULT`。 - `$sqrt` 作用于负实数是 `ENGINE_UNDEFINED_RESULT`;复数参数给出主根。 - `$asin` 与 `$acos` 作用于 -1 到 1 之外的实数是 `ENGINE_UNDEFINED_RESULT`。 - `$floor`、`$ceil`、`$sign` 与四个二元函数要求实参数:复数参数是 `ENGINE_UNDEFINED_RESULT`。 - 除以零与对零取 `$mod` 都是 `ENGINE_UNDEFINED_RESULT`。 ### 6.4 只接受整数向量 - 中间量可以携带分数向量:`$sqrt` 把向量减半,幂按指数缩放它。 - 只有写入槽位的值必须具有整数向量。分数向量会在任何写入之前以 `ENGINE_INCOMPATIBLE_DIMENSION` 被拒,因此对电阻取平方根这样的公式会被拒,而不会被存下。 - 不指名任何行的中间向量完全合法:`(@V)^2/@R` 就在通往功率的路上先把电压平方,而 `volt^2` 从不需要真的成为某一行。 ### 6.5 `dim` 是对槽位内容的断言 给 `get` 的 `dim` 是对该槽位内容的断言:7 个整数必须与存下的向量完全相等,表名必须是一个向量相同的行,随后各叶子才被换算成该写法。不符时会被拒,且不做任何换算(§3.4)。 ## 7. 记录与轨迹 ### 7.1 三个标记 | 标记 | 参数 | 作用 | |---|---|---| | `record_start` | `title`,非空字符串 | 开启一条记录:分配它的标识符,写下头部行与起始行。已有记录未封闭时以 `ENGINE_OPEN_RECORD_FOUND` 失败,因此一条记录只携带一个标题 | | `record_message` | `text`,非空字符串,以及可选的 `hide`(布尔值,默认 false) | 向未封闭记录追加一段说明 | | `record_end` | 可选的 `text` | 追加封闭行,把未封闭文件改名进已封闭层(§7.3),并清空槽位表。没有记录未封闭时以 `ENGINE_OPEN_RECORD_NOT_FOUND` 失败 | - `set`、`get` 与 `eval` 都要求有记录未封闭,`record_message` 也是;否则该次调用以 `ENGINE_OPEN_RECORD_NOT_FOUND` 失败,且任何地方都不写入内容。 - 记录标识符是毫秒时钟读数转换成的字符串;该名字被占用时依次追加 `-2`、`-3`。 - `hide: true` 的消息被标记为面向文章写作者的注记,而不是记录视图的内容(§10)。 - 空字符串或全为空白的封闭文本等同于没有给出。 - 每个标记只回答 `{ ok: true }`。 ### 7.2 轨迹行 每次调用至多向未封闭记录追加一行,输入与输出都在其中。没有记录未封闭时,调用不追加任何东西。 ``` { "seq": 4, "at": 1700000000004, "tool": "eval", "ok": true, "content": { ... } } ``` | 字段 | 含义 | |---|---| | `seq` | 该行在记录中的位置,从 1 起,每追加一行加一 | | `at` | 该次调用的时钟读数,单位为毫秒 | | `tool` | `set`、`get`、`eval`、`record_start`、`record_message`、`record_end` 或 `search` | | `ok` | `true`;被拒的调用为 `false` | | `content` | 该工具所存的内容 | `content` 按工具: - `set`:`{ name, value }`,存下的值,其 `dim` 为 7 个整数;删除是 `{ name, value: null }`。 - `get`:`{ name, value }`,按收据渲染出的值。请求的 `form`、`digits` 与 `dim` 不存。 - `eval`:`{ formula, target, rev, vars, result }`,写下的原文、写入的槽位、它的新版本号、公式读到的每个槽位及其当时的存储值,以及结果。 - `record_start`:`{ title, record }`。 - `record_message`:`{ text }`,或 `{ text, hide: true }`。 - `record_end`:`{ record }`,给出封闭文本时为 `{ text, record }`。 - `search`:`{ question, tier, outcome, candidates, used, policy, synthesis?, error?, durationMs }`(§12.5)。 | `search` | `{ question, tier, outcome, candidates, used, policy, synthesis?, error?, durationMs }`:该次检索自身的事实(12.5 节) | | 被拒的调用 | `{ code, error }` | - `vars` 恰好是公式读到的那些槽位,按首次使用顺序排列,每个都带该槽位当时的取值。绑定变量永不进入轨迹。 - 记录保存的是事实,不是排版好的字符串:`get` 行保存值,而不保存产生其渲染结果的选项。 - `search` 行由执行检索的工具写入,模型从不写它:该行是记录对这次取数的交代,而模型只收到答案(12 节)。它与 `get` 行一样是事实,因此恢复过程不会把它重算一遍。 - 写不进去的轨迹不会把一次成功调用变成失败:追加失败被丢弃。 ### 7.3 记录文件 - 每个记录文件的第一行都是它的头部:`{"seq": 0, "version": 1}`。`seq: 0` 是把它与轨迹行区分开的唯一标记,`version` 是本构建写出并接受的格式版本。 - **两层。** 唯一未封闭的记录存放在 `/open-record.jsonl`。封闭时该文件被一步原子改名为 `/records/.jsonl`,因此 `records/` 下的文件永远是一条完整的记录。 - **版本闸门。** 第一行不是头部、或版本低于当前版本的文件,会被每一个读取方拒绝:启动时未封闭文件被丢弃;已封闭记录不会被列为一行,但它的标识符会出现在无法读取的标识符之中。 - 无法解析的行被丢弃,因此追加过程中崩溃留下的半行只损失那一行:记录保留所有解析成功的行。 ### 7.4 按重放恢复 启动时引擎先清空槽位表,然后读取未封闭文件。未通过版本闸门、或起始行既无标题也无标识符的文件会被丢弃。否则该记录从它的起始行恢复,序号取最后一行的 `seq`,并按顺序重放它的各行: - `set` 行且 ok:写入存下的值;其值为 `null` 时删除该槽位。 - `eval` 行且 ok:把存下的结果写入它的 target,不重新求值。 - `search` 行、`get` 行与其他任何行:跳过。 - 存下的结果被当作事实使用:不重算、不取数、不重新检索、不含随机。 - 不再能解析的行被跳过,因此恢复过程永远不会妨碍插件挂载。 - 随后轨迹在同一文件中继续,下一行的序号接在最后一行之后。 ### 7.5 记录列表 - 列表给出已封闭记录(标识符、版本、标题、开启时间、结束时间)、唯一未封闭的记录(标识符、标题、开启时间),以及无法读取的标识符。 - 一条记录的标题与时间跨度来自它的各行:首个被接受的 `record_start` 行的标题,最后一个被接受的 `record_end` 行的时间作为结束时间;没有该行时取最后一行的时间。 - 已封闭列表带缓存,仅在 `records/` 目录的修改时间变化时重建,因此并非每次读取都扫描目录。 ## 8. 错误 ### 8.1 失败的形状 每次失败都是一张收据——`{ ok: false, code, error }`——并且什么都不写: - **`error` 是写给读者的一句话。** 当失败与文本有关时,它给出文本中的位置;它总是给出失败的具体取值、边界或期望,以及修正方式。 - **`code` 是给机器看的。** 轨迹、测试与面板用它匹配;它从不取代那句话。这些错误码是同一个枚举的 19 个取值(§8.2)。 - **失败的调用没有副作用。** 不建槽位、不改值、不动版本号。失败唯一留下的,是它自己的那一行轨迹(§7.2)。 - 意外的内部故障以 `ENGINE_UNKNOWN_ERROR` 报出,消息为 `internal error: ...`,因此没有任何失败能逃出收据之外。 ### 8.2 19 个错误码 | 错误码 | 触发条件 | |---|---| | `ENGINE_INVALID_FORMULA` | 文本不是语法所接受的一条表达式:出现字符集之外的字符、`$` 或 `@` 之后没有名字、完整表达式之后还有记号、`(`、`[` 或 `{` 未闭合、`^` 之后紧跟 `{`、`.` 之后不是字段名、参数表没有以 `,` 或 `)` 收尾,或 `$integral`/`$diff` 的第二个参数不是裸名字 | | `ENGINE_INVALID_NUMBER` | 数字的科学计数法没有指数数字,如 `2e` | | `ENGINE_INVALID_IDENTIFIER` | 数字后面直接跟字母(`1E5`、`2x`);需要标识符处给出的名字(`set` 的 name、`get` 的 name、`eval` 的 target、对象字段名)不是字符串,或不满足名字规则 | | `ENGINE_INVALID_DIMENSION` | `dim` 不是表名、不是恰好 7 个整数,或含有非整数分量 | | `ENGINE_INVALID_NOTATION` | `$name` 不在记号表内;常量被加了 `(` 或 `_`;函数没有写 `(`、或写了 `_`;绑定记号缺少它必需的界 | | `ENGINE_INVALID_ARITY` | 参数个数不是表中所列的个数;需要绑定变量的下标位里缺少绑定变量;`$limit` 用 `=` 而不是 `->` 书写;缺少上界;`$integral` 的下标位里给了绑定变量 | | `ENGINE_SLOT_NOT_FOUND` | `@name` 读取了不存在的槽位,或 `get` 指名的槽位不存在 | | `ENGINE_NAME_NOT_BOUND` | 裸名字没有被任何外层记号绑定 | | `ENGINE_INCOMPATIBLE_DIMENSION` | `+` 或 `-` 两侧向量不同;指数不是无量纲;复指数用于有量纲的底;要求无量纲参数却给了有量纲的;`$min`、`$max`、`$mod` 或 `$atan2` 的两个参数向量不同;数组元素不共享同一个向量;分数向量被写入槽位;`get` 的 `dim` 与存下的值不符 | | `ENGINE_UNSUPPORTED_OPERATION` | 对对象施加运算符或函数,或把对象放进数组 | | `ENGINE_INVALID_INDEX` | 下标不是带零向量的整数;下标落在数组之外(两者都给出合法范围);界不是带零向量的整数;下界高于上界 | | `ENGINE_UNDEFINED_RESULT` | 引擎没有为其定义取值的结果:除以零、对零取 `$mod`、对不大于 0 的实数取 `$ln` 或 `$log`、对负实数取 `$sqrt`、`$asin` 或 `$acos` 落在 -1 到 1 之外、负实数底配分数指数、零的负数次幂或复数次幂,或给只接受实数的函数传入复数 | | `ENGINE_UNSUPPORTED_INDEX` | 对不是对象的值使用 `.`,或对不是数组的值使用 `[ ]` | | `ENGINE_FIELD_NOT_FOUND` | 对象没有该名字的字段;信息会列出它实际拥有的字段 | | `ENGINE_UNSUPPORTED_SYMBOL` | 对 `$integral`、`$limit` 或 `$diff` 求值 | | `ENGINE_INVALID_ARGS` | 工具参数的形状不对:`set` 的值不是对象、没有标签或有多个标签、含未知键、`dim` 与 `object` 并列、成对的一半缺失、`array` 不是数组、`object` 不是对象,或元素不属于四种裸形式之一;两个数组合并时长度不同;`$len` 或 `$transpose` 收到了错误的值;`form` 或 `digits` 不合要求;`eval` 的 `formula` 不是字符串,或缺少 `target`;标记的文本为空或不是字符串,或其 `hide` 不是布尔值 | | `ENGINE_OPEN_RECORD_NOT_FOUND` | 在没有记录未封闭时调用 `set`、`get`、`eval` 或 `record_message`,或调用 `record_end` | | `ENGINE_OPEN_RECORD_FOUND` | 在已有记录未封闭时调用 `record_start` | | `ENGINE_UNKNOWN_ERROR` | 意外的内部故障:任何不是引擎失败的抛出,报为 `internal error: ...` | ## 9. 存储与日志 ### 9.1 主目录 插件主目录是 `~/.dsh-reckoner`,`DSH_RECKONER_HOME` 可将其改到别处。 ``` ~/.dsh-reckoner/ open-record.jsonl 唯一未封闭的记录:它的头部行与轨迹行 records/.jsonl 已封闭记录,一条一个文件,以记录标识符命名 state.json 插件记忆的设置 logs/ 每次宿主运行一个文件 ``` | 文件 | 存放 | |---|---| | `open-record.jsonl` | 未封闭的记录;在 `record_start` 时连同头部行重写,在 `record_end` 时改名 | | `records/.jsonl` | 一条已封闭记录:头部行与每次调用一行轨迹 | | `state.json` | 记忆的设置,即下文的树:每个会记忆设置的模块一个子树,外加扁平的 `restartRequired` 标记。它以原子方式整体替换;文件缺失、损坏或不是对象时读成 `{}` | | `logs/` | 下文的运行日志 | `state.json` 是一棵树。缺失即「用默认值」,因此只存有人真正选过的值;写入方只动自己的子树,其余每个键——包括更新的构建写下的键——都原样保留。 ```jsonc { "schema": 1, "generation": { "markdown": { "directory": "C:/out", "language": "auto" }, "latex": { "directory": "D:/tex", "language": "zh-CN", "compile": false } }, "search": { "maxResults": 20, "enrich": { "pages": 2 } }, "panel": { "showAll": true }, "restartRequired": true } ``` - `generation` 按文章格式各有一个子树,因为两种格式记忆的东西不同:两者都有 `directory` 与 `language`,`compile` 只属于 LaTeX(10 节)。 - `search` 是用户对检索策略的覆盖:它逐字段叠加在预设行所提供的值之上,而绝不是一份完整策略(12.4 节)。 - `panel` 保存面板自身的偏好;`showAll` 是记录详情「显示全部」的默认值(11.2 节)。 - `restartRequired` 保持在顶层,且不是设置:它是关于本次宿主运行的一个事实,由任何改动了「运行中的宿主只在下一次挂载时才读取的东西」的一方写入。没有待处理事项时它不存在,因为不存在与 `false` 在这里是同一个事实。 - `schema` 是文件的结构版本;本构建写入 `1`。没有该数字、或数字更旧的文件会在下一次写入时被盖上该值;更新的构建写下的文件保留它更高的数字,两种情况里本构建不认识的每个键都原样保留——读取方永不降级它没有写过的内容。 - 更早的构建写下的文件会在挂载时被迁移一次:扁平的 `generateDir` 与 `generateLanguage` 被复制进两种格式(因为没有任何东西说明它们指哪一种),`generateCompile` 只进入 LaTeX,`generateFormat` 被丢弃,因为它被写过却从未被读过。更早的纯文本 `generate-dir.txt` 仍作为目录被沿用,直到第一次写入将它删除。 ### 9.2 日志 - 每次插件挂载、也就是每次宿主运行一个文件:`/logs/.log`,独占创建并保持打开。写入是在所持描述符上的同步写入。 - 每行形如 ` [ k=v ...]`,同时写入文件与 stdout。字段值是 JSON 标量;嵌套对象或数组算一个 token;Error 渲染为它的消息,并追加带 ` | ` 前缀、承载堆栈的续行。 - 级别来自 `DSH_RECKONER_LOG_LEVEL`:`debug`、`info`、`warn`、`error` 或 `off`;其他任何取值都保持默认的 `info`。 - 保留策略:最新的 20 个运行文件,总量至多 50 MB。 - 日志承载插件自身的诊断(挂载与卸载、端点失败、生成任务),而记录只承载引擎调用。 ## 10. 文章生成 已封闭的记录可以被写成一篇独立的解题文章。宿主把记录的各行归约为朴素的事实,在独立上下文中交给模型,再把文章写入磁盘。仍未封闭的记录不能被生成。 | 事实 | 记录中的来源 | |---|---| | 标题 | 首个被接受的 `record_start` 行的 `title` | | 条件 | 被接受的 `set` 行,按槽位名累加;删除会移除该名字 | | 消息 | 被接受的 `record_message` 行,各自带 `seq` 与 `hide` 标记 | | 步骤 | 带公式的被接受的 `eval` 行:`seq`、`formula`、它读到的槽位与它的结果 | | 检索 | 给出了答案的 `search` 行:`seq`、问题、答案,以及它所依据的出处 | | 封闭文本 | 最后一个被接受的 `record_end` 行的 `text` | - 被拒的行与 `get` 行没有任何可写内容,因此被跳过:文章建立在成功的那条推导链及其产出的数字之上。 - 消息与步骤按 `seq` 交错排列,因此一段说明紧挨着它所覆盖的那些步骤。 - `hide: true` 的消息在事实中成为作者注记:写作者被告知这是不得照抄、不得引用、也不得让它改动任何已记录数字的指引。没有 `hide` 的消息成为记录自身的说明。 - 事实中的每个数字都在到达模型之前被截取:非零且小于 `1e-3`、或不小于 `1e6` 的数字写成保留四位小数的指数形式,其余数字四舍五入到四位小数。名为 `dim` 的字段保持它的 7 个整数不变。截取只作用于提示词;记录保留存下的数字。 - 一次检索在时间线中作为独立条目交出:模型所问的问题、记录下来的答案,以及每个出处一行的清单。答案按原样保留,因为它携带的数字是照出处写法存下的,所以四位小数的截取不作用于它(12.7 节)。开放层级的答案会标注为取自开放网络且未经核实,写作者也会被告知:检索得来的值必须保持被引用时的数字与单位,并在使用处注明出处。 - **格式。** Markdown(`.md`)与 LaTeX(`.tex`)。LaTeX 只由模型写正文:宿主拒绝含文档结构改写命令、花括号不平衡或 `$` 个数为奇数的正文,并把其余部分包进 XeLaTeX 文档外壳——zh-CN 用 `ctexart`,en 用 `article` 加 `fontspec`,并加载 amsmath、siunitx 与 unicode-math,标题与作者由宿主固定给出。 - **语言。** `auto`、`zh-CN` 或 `en`。外壳语言在生成之前就已确定,因此 auto 任务会探测记录自身的文字(标题、消息与封闭文本)中是否出现 CJK 表意文字。 - **任务阶段。** prepare、generate、write、compile。LaTeX 文章写入一个以文件名为名的文件夹,PDF 与编译产物也落在其中;Markdown 文章平铺写入,且从不编译。 - PDF 编译只对 LaTeX 且可选。它运行一个驱动,优先 `latexmk`,回退 `texify`,由驱动再去运行 `xelatex`;无论编译结果如何,文章都已写入磁盘,编译失败会被报出而不会丢弃文章。 - 文件名会被强制为对应格式的扩展名;未给出时默认为 `reckoner-<记录标识符的前 8 个字符>`。 - 文章以作者自己的解题口吻写成:提示词禁止提及 Reckoner、宿主、公式、推导步骤、记录或生成过程,也禁止臆造或重算数字。 - **记忆的设置。** 每种格式各自记住自己的输出 `directory`、`language`,以及只有 LaTeX 才有的是否 `compile`,都存在状态文件中属于自己的子树里(9.1 节)。设置对话框以该格式记住的值打开,并通过设置端点写回,因此一种格式记住的东西不会波及另一种。 - 设置端点提供并写入这份视图:`GET /api/dsh-reckoner/settings` 给出两种格式的生成默认值(已套用默认),`PUT` 接收 `{generation: {format, directory?, language?, compile?}}`,并以整份视图作答(11.3 节)。 ## 11. 面板 记录面板是侧栏中的 **Reckoner** 入口,覆盖在会话列之上打开。它的主体是一条标签条,含两个标签页:**记录**——记录列表与单条记录详情——以及 **设置**,后者保存插件记忆的设置。 ### 11.1 记录标签页 - 列表读取 `GET /api/dsh-reckoner/records-index`,每 5 秒轮询一次;从不读取轨迹本体。 - 未封闭的记录带「未完成」标记,被钉在列表之上。已封闭记录紧随其后,最新在前,每行显示标题(标题为空时显示标识符)以及它的开启与结束时间。 - **选择** 模式把各行变成选择项,提供 **全选** 与 **删除所选**,并由确认对话框把关。删除会移除已封闭记录的文件;未封闭的记录永远不会进入选择,因为端点会拒绝它。 - 另有一行报告任何构建都无法读取的标识符,并提供删除它们的入口,因为除此之外没有别的途径能触及它们。 - 打开一条记录会把列表替换为该记录的时间线:它从 `GET /api/dsh-reckoner/records/` 取得,每 5 秒轮询一次,因此正在进行的求解会实时显现。 - 一张头部卡:记录标识符、可见行数、其中失败行的数量,以及结束时间或「未完成」标记。 - 关闭 **显示全部** 会把被拒的行与标记为 `hide: true` 的消息挡在时间线之外:前者是引擎对过程的交代,后者是写作者的注记,都不属于解答。因此在该开关打开之前,失败行数一直是零。该开关以面板偏好为初值,并在切换时写回(11.2 节)。 - 各行被归类成叙事:连续的、被接受的 `set` 行折叠为一张 **写入(n)** 卡(每个槽位一行,并带其版本号);连续的、被接受的 `get` 行折叠为 **读取(n)** 卡;连续失败折叠为一张红色 **失败尝试(n)** 卡,显示序号、工具、公式(若有)、`code` 与 `error` 文本。 - 每个被接受的 `eval` 都有自己的卡:公式、写入的槽位及其版本号、公式读到的每个槽位及其当时的取值、可跳到该槽位 `set` 行的标签,以及以树形展示的结果。 - 标记行渲染为带强调色边的卡片:记录开启、一条消息、记录封闭、被拒的 `record_start` 与被拒的 `record_end`。 - 一次检索有自己的卡片:问题、收到的答案,以及折叠在 **出处(n)** 标题之后的、记录所保存的出处清单。被拒或没有答案的检索则改用警告或错误强调色显示它那一句话。 - 右列是两颗文章按钮——**生成 Markdown** 与 **生成 LaTeX**——它们打开生成设置对话框。 ### 11.2 设置标签页 - **生成**:每种文章格式一块,含 `directory`、`language`,以及只有 LaTeX 才有的 `compile` 开关。每块各自保存,因此写入一种格式绝不会碰到另一种。 - **检索策略**:一次检索所解析的三个层级(12.4 节),每个字段的占位符显示的是实际生效的值。留空的字段会写成 `null` 并清掉该覆盖,让下一层重新生效;**重置全部覆盖** 会清空整个子树。 - **面板**:记录详情是否默认显示隐藏行,也就是它的 **显示全部** 开关同样写入的那个偏好。 - 有待处理的改动需要重启宿主时显示一行,只要扁平的 `restartRequired` 标记被设置就显示(9.1 节)。 - 这里的一切都经由一对端点读写,即 `GET` 与 `PUT /api/dsh-reckoner/settings`(11.3 节);读取或保存失败只留下一行文字,而不会留下一个空标签页。 ### 11.3 宿主端点 | 端点 | 用途 | |---|---| | `GET /api/dsh-reckoner/records-index` | 已封闭记录、未封闭记录,以及无法读取的标识符 | | `GET /api/dsh-reckoner/records/` | 一条记录的身份与轨迹行;未封闭的记录也在其中,其 `endedAt` 为 `null` | | `DELETE /api/dsh-reckoner/records/` | 移除已封闭记录的文件;它是未封闭记录时以 409 被拒,不存在时 404 | | `POST /api/dsh-reckoner/generate` | 启动文章任务(`recordId`、`format`、`directory`、`fileName`、`language`、`compile`) | | `GET /api/dsh-reckoner/generate-progress` | 轮询一个任务的状态、百分比、阶段、路径与错误 | | `POST /api/dsh-reckoner/generate-cancel` | 取消正在运行的任务 | | `GET /api/dsh-reckoner/generate-capability` | 生成设置对话框背后的 LaTeX 工具链与文档外壳探测 | | `GET /api/dsh-reckoner/list-roots`、`GET /api/dsh-reckoner/list-dirs` | 输出目录浏览器 | | `GET /api/dsh-reckoner/directory-tree.css` | 面板注入的随包样式表 | | `GET` / `PUT /api/dsh-reckoner/settings` | 整份设置视图;`PUT` 只写入请求体点名的分区,并以 `{saved, settings}` 作答,请求体不可用时回 `400 {error}`,其他方法回 `405` | | `POST /api/dsh-reckoner/reveal` | 在宿主文件管理器中打开生成的文件或其目录 | ## 12. 外部检索 纯粹的 `reckoner` 预设不接触外界:它有六个工具,没有 shell、没有文件系统、也没有网络。一次一个外部事实经由 `reckoner-with-search` 预设进来,而该预设只比前者多一行。这一行就是 `dsh-reckoner/search`:它为会话注册 `search` 工具,并把每次调用通过 `reckonerSearch` 服务转交给插件的宿主半边;这一行本身不再注册任何东西,因此没有挂载它的会话根本没有 `search` 工具。通用的 `web_search` 与 `web_fetch` 工具两个预设都不挂载,所以模型永远无法自行检索或抓取。引擎不参与取数过程:它只校验事实并追加该行,与 `set` 完全一样。 ### 12.1 工具契约 `question` 是调用方需要的那个事实,用一句话作为问题提出。它必填,并原样转发。 ``` success: { ok: true, answer, origin } failure: { ok: false, code, error } ``` - `answer` 是模型收到的抽取式答案,已按策略的 `answerMaxChars` 截断;`origin` 给出答案来自哪一类出处:每个出处都是 GitHub 主机时为 `github`,严格层级作答时为 `allowlist`,开放层级作答时为 `web`。`origin` 供调用方自行校准,不写进事实。 - 失败码是 12.6 节的四个,`error` 是与引擎自身失败同一口吻的一句话。 - 答案是输入,绝不是结果:人设要求模型用 `set` 存下它,照写法复制数字及其单位,并让引擎去换算。关于一次检索的任何东西都不由引擎计算,答案也不会自行流入公式。 ### 12.2 模型看到与看不到的东西 模型收到答案,以及它来自允许列表、开放网络还是 GitHub。记录保存问题、层级、结局、提供方返回的每一个候选出处、策略允许的出处、抽取步骤的路由与提示词版本,以及答案。 - 提供方自身的检索对我们完全不可见。我们原样送出问题,从不自己规划查询词;提供方那一侧的模型检索了什么、读了哪些页面,是它自己的一轮调用,不会回报给我们。因此记录保存的是问题与提供方引用的出处,而不是查询词。 - 策略在取数之后才运行:它决定抽取步骤可以读什么,而不是已经读过了什么。事实之所以同时保留候选集合,原因就在这里。 - 模型永远不会收到网址、查询词或摘要片段。出处通过记录与面板到达读者,通过事实到达文章写作者。 ### 12.3 一次检索的流水线 1. 问题被原样送交宿主 `web` 服务(`ctx.web.search`),由它解析出部署所用的检索提供方。随包提供方是 `deepseek-official`:它执行一轮辅助模型调用,携带服务端的 `web_search_20250305` 工具,并返回那一轮所引用的出处,最多到策略的 `maxResults`。 2. 策略按层级保留允许的出处(12.4 节)。 3. `enrich.pages` 大于零时,前若干个被允许的页面会被抓取以取正文,每个页面截到 `enrich.charsPerPage`。取不回来的页面就是不参与富化;该次调用继续使用摘要片段。 4. 抽取步骤是一次不带工具、也没有会话记忆的裸模型调用,跑在 `synthesis.provider` 与 `synthesis.model` 上;未设置时跑在部署的默认模型上。它的提示词版本为 `search-extract/1`,只允许使用给出的材料,禁止外部知识,禁止换算单位或计算派生值,并要求回答一个 JSON 对象:答案、所依据的材料条目,以及材料是否不足。 5. 结局与整个事实被写入未封闭记录,而模型只收到答案。 - 严格层级不会缩小提供方的检索范围:无论哪一层级,提供方都在开放网络上检索,而允许列表决定抽取步骤可以读什么。 - 一次检索至少花费两次模型调用:提供方那一轮服务端调用加上抽取调用。`enrich` 每抓取一个页面再加一次。 - 不是约定 JSON 对象的回复属于技术失败,绝不是答案:其原始文本留在记录里,永不到达模型,因为读不通的回复恰是貌似合理的一句话可能夹带未经核对数字的地方。 ### 12.4 该行的策略 每个键都可选,且每个键都有默认值,因此预设只写它想改的部分。不可用的值会被报告并替换,而不会让这一行挂载失败:一个写错的数字不该让一次会话失去它的计算器。 策略按每次调用解析,共三层:下表的代码默认值、预设行自身的 `config`,以及状态文件 `search` 子树里的用户覆盖(9.1 节)。覆盖没有点名的字段回落到预设行,预设行没有点名的字段回落到默认值;设置标签页把三层与实际生效的值并列显示(11.2 节)。解析发生在每次调用时,而不是挂载时,因此一次设置写入会作用于紧接着的下一次检索——既不需要重启宿主,也不需要新会话。 | 键 | 代码默认值 | 含义 | |---|---|---| | `tier` | `strict` | `strict` 只保留允许列表上的主机;`open` 保留每一个出处 | | `allowedHosts` | 12 个参考资料主机 | 严格检索接受的主机后缀,按模式本身或子域匹配,仅字母后缀相同不算 | | `maxResults` | `12` | 一次提供方调用返回的出处数量上限 | | `maxSearchesPerRecord` | `4` | 一条记录最多可以带几次检索 | | `answerMaxChars` | `1200` | 交给模型的答案长度上限 | | `enrich.pages` | `0` | 抓取多少个被允许页面以取正文;`0` 只用引用摘要片段 | | `enrich.charsPerPage` | `4000` | 每个被抓取页面的多少正文进入抽取步骤 | | `synthesis.provider`、`synthesis.model` | 未设置 | 抽取步骤所跑的路由;未设置表示部署默认模型 | | `synthesis.maxTokens` | `800` | 抽取调用的 token 上限 | 显式给空 `allowedHosts` 是一个决定:此时严格检索什么也答不出。被截断的只有模型收到的答案与合成中留存的那一份。 默认允许列表是计算可以引用的参考资料:`wikipedia.org`、`github.com`、`githubusercontent.com`、`stackoverflow.com`、`stackexchange.com`、`developer.mozilla.org`、`docs.python.org`、`nist.gov`、`iso.org`、`ietf.org`、`rfc-editor.org` 与 `arxiv.org`。 ### 12.5 记录下来的事实 无论答出了什么,每次调用都恰好写入一行,该行的工具名是 `search`。 - **`question`**:给出的问题原文。 - **`tier`**:`strict` 或 `open`,本次调用所处的层级。 - **`outcome`**:`answered`、`insufficient`、`refused` 或 `failed`。 - **`candidates`**:提供方返回的每一个出处,保持其顺序,各带 `url`,以及提供方给出时的 `title`、`snippet` 与 `publishedAt`。 - **`used`**:策略允许的出处,`candidates` 中保持原顺序的一个子集;什么都没允许时为空列表。 - **`policy`**:本次调用所处的 `tier`、`allowedHosts`、`maxResults`、`maxSearchesPerRecord`、`answerMaxChars`、`enrichPages` 与 `enrichCharsPerPage`。 - **`synthesis`**:调用在任何合成之前就结束时缺失;否则给出提供方、模型、提示词版本、答案,以及答案所依据的材料条目。 - **`error`**:一句自足的话,在调用没有答出任何东西时给出。 - **`durationMs`**:该次调用耗时。 - 该行由工具写入,模型从不写它;它与 `get` 行一样是事实:恢复过程不会把它重算一遍(7.4 节)。 - `synthesis.used` 存的是该行自身 `used` 列表的下标——那是交给抽取步骤的材料——因此一个答案可以追溯到它所依据的出处。 - 三种没有答出东西的结局也会被记录:记录已用完预算时为 `refused`,没有任何被允许的出处持有该事实时为 `insufficient`,提供方、网络或抽取步骤无法运行时为 `failed`。记录是对所发生之事的交代,因此一次没有产出任何东西的检索也是它的一部分。 - 保留 `candidates` 是因为策略在取数之后才运行:记录既展示提供方找到了什么,也展示答案被允许使用其中的哪一部分。 - 策略按次解析,且可以在宿主运行期间被重新调整,因此该行携带的是本次调用实际所处的策略:读者仅凭该行就能重新推出这次调用被允许做什么,无论现在的设置是什么。更早的构建写下的行没有这个字段,读者那时只有 `tier`。 ### 12.6 调用方看到的失败码 | 错误码 | 触发条件 | |---|---| | `SEARCH_INSUFFICIENT` | 策略允许的任何出处都不持有该事实,或抽取步骤回答了 `insufficient` | | `SEARCH_BUDGET_EXCEEDED` | 记录已用完它的 `maxSearchesPerRecord` 次检索。该次调用根本不会到达提供方 | | `SEARCH_UNAVAILABLE` | 提供方、网络、抽取模型或某个必需的服务失败,或没有配置默认模型 | | `ENGINE_OPEN_RECORD_NOT_FOUND` | 没有记录处于开启状态。检索会被记录,因此它要求有记录处于开启状态 | ### 12.7 调用方这一侧 - 检索预设的人设规则说答案是输入:用 `set` 存下它,照写法复制数字及其单位,并让引擎去换算。工具自身的描述说的是同一件事,因为模型先读到工具,后读人设。 - 永远不要让它计算、换算、取整或推导任何东西,也不要问它记录里已经有的东西。 - `SEARCH_INSUFFICIENT` 意味着该量缺失:说出哪条关系因此无法求值并停下,而不是去估这个值。 - `SEARCH_BUDGET_EXCEEDED` 意味着记录已经没有检索额度:用记录已有的东西继续,或者说明缺什么。 - 记录中的 `search` 行会进入文章:事实渲染出问题、逐字保留的答案——不做四位小数截取,因为它携带的数字是照出处写法存下的——以及出处清单,好让文章注明它们;开放层级的检索会被标注为取自网络且未经核实(10 节)。 - 面板把一次检索画成它自己的卡片:问题、答案,以及可折叠的出处清单;被拒或没有答案的检索改用警告或错误强调色显示它那一句话(11.1 节)。 ### 12.8 实测到的限度 - 提供方那一侧的模型检索了什么不可见。记录保存问题与答案所引用的出处,产生它们的查询词并不在其中。 - 允许列表是在取数发生之后才过滤的。它保证的是模型被展示了什么,而绝不是已经被读取了什么。 - 直接抓取只能到达本机网络能够到达的主机,而那并不是每一个被允许的主机:在实测这套功能的机器上,`wikipedia.org` 与 `stackoverflow.com` 抓不到,而 `github.com`、`raw.githubusercontent.com` 与 `example.com` 可以。因此 `enrich` 只对可达主机会有帮助;其余被允许的出处都以提供方的引用摘要片段形式到达。 - 一次检索花费两次模型调用:提供方那一轮服务端调用,以及抽取调用。