# 技术实现方案:23-计算字段判定型扩展 > 依据:R-028(已定稿)+ R-026 既有实现(迭代 22)| 日期:2026-09-10 | 状态:**已实施** ## 1. 公式引擎(src/formula/evaluator.js) - **词法**:新增多字符运算符识别(`>= <= == != && ||` 优先于单字符);全角/符号归一扩展 `≥→>= ≤→<= ≠→!= >→> <→< =→=`; - **语法优先级**(低 → 高):`or` → `and` → 比较(`> < >= <= == !=`) → 加减 → 乘除 → 一元负 → 主; `and`/`or` 以标识符(大小写不敏感)识别,`&&`/`||` 等价; - **求值**:比较返回布尔;逻辑返回布尔(`!!l && !!r`);两者**缺值短路**(任一操作数为 null → null); 数字运算维持原语义(除零 → null、最终结果浮点归一);`normalizeResult` 对布尔直通; - **新增 `inferResultKind(ast)`**:顶层为比较/逻辑 → `'boolean'`(`neg` 递归下钻),否则 `'number'`; - 校验(`validateFormula`)逻辑不变(变量存在性 / 函数白名单 / arity)。 ## 2. 存储与校验 - **src/settings.js**:`fieldSchema` 增 `resultKind: z.union([z.const('number'), z.const('boolean')]).default('number')`; `normalizeFields` 对 formula 字段归一 `resultKind`(非 `'boolean'` 一律 `'number'`),`decimals` 维持夹取 0-4; - **src/api/strategies.js#validateConfigSchema**:公式校验通过后增加**结果类型一致性**校验 —— `declared = resultKind`(默认 number)必须等于 `inferResultKind(compileFormula(formula).ast)`, 不一致抛 `field-validation`: - 声明「判定」但公式无比较 → 「结果类型选了「判定」,但公式没有比较运算(如 涨停价 > 基准值 + 1)」; - 公式是判定但声明「数字」 → 「公式结果是判定,请把「结果类型」改为「判定」」。 ## 3. 现算(src/formula/FormulaService.js) `computed[key]` 布尔直通: ```js computed[c.key] = (typeof v === 'boolean') ? v : ((typeof v === 'number' && Number.isFinite(v)) ? v : null); ``` 其余不变(无 formula 字段零开销、只读 QuoteHub 内存缓存、不落库、历史行不计算)。 ## 4. 前端 **StrategyFieldsEditor.jsx** - 表单草稿增 `resultKind`(默认 `number`,编辑时回填); - 「类型」行新增「结果」下拉(数字 / 判定); - 公式框 placeholder 按结果类型切换(判定示例:`涨停价 > 基准值 + 网格大小`); - 公式框下新增运算符提示行:`运算:+ - × ÷ 括号;判定:> < >= <= == != ,组合:and / or`; - `resultKind === 'boolean'` 时**隐藏小数位**输入; - 列表「类型」列显示 `计算(判定)`; - **判定型试算**:`value` 为布尔时显示 `试算:<名称> <代码> → ✓ 满足 / — 不满足`。 **StrategyTab.jsx** - 计算列渲染:**先判布尔**(`typeof raw === 'boolean'`)—— `true` → `✓ <字段展示名>`(`state-success-primary`), `false` / 缺值 → `—`(tertiary);否则走原数字格式化(`toFixed(decimals)` + 单位); 顺序不可颠倒(`Number(false) === 0` 会被误格式化成 `0.00`); - 表头 `ƒ` 前缀、悬停公式原文、历史行 `—` 维持不变。 ## 4.5 附:公式引擎选型论证(2026-09-10 老师提问「有没有现成的库」) **结论:维持自写引擎**(`src/formula/evaluator.js`,约 250 行 / 零依赖 / 71 条回归断言)。 | 候选 | 中文变量名 | 能力覆盖 | 体积/依赖 | 风险与维护 | |---|---|---|---|---| | **expr-eval** | ✗ ASCII 标识符 | 四则/比较/and·or/**三元 ?:**/属性访问/数组索引 | 小 | **CVE-2025-12735**:构造 variables 对象可致**任意代码执行**(GHSA-jc85-fpwf-qm7x);上游维护停滞,社区有 fork | | **mathjs** | ✗ 明确枚举可用字符(拉丁/希腊/字母类/数学字母数字),**不含 CJK** | 极全(矩阵/单位/复数/函数库) | 大(数百 KB 级) | 重;unicode 标识符诉求自 2015 年 issue #265 起未覆盖 CJK | | **Jexl** | ✗ ASCII 标识符(支持 `a.b` / `a[b]` 取值) | 比较/逻辑/三元/自定义运算符 | 中 | 取值语义与「缺值短路」需自行包装 | | **CEL(cel-js 等)** | ✗ ASCII 标识符 | 受限表达式语言(工业标准、可静态检查) | 中 | 语义偏严格,接入成本不低 | | **解析器工具包**(chevrotain / ohm-js) | ✓(自定义词法) | 自己写语法 + 求值 | 中 | **等于仍自己实现**,只是换成语法文件 + 依赖 | | **自写引擎(现状)** | ✓ 原生支持 | 四则/括号/比较/and·or/round·abs·min·max | 0 | 无 eval/Function、无属性访问、无数组索引、变量来自白名单目录 → **结构上无任意代码执行面** | **为什么不换**: 1. **中文变量名是产品决策**(R-026 Q2 老师拍板)。主流库标识符均限 ASCII → 引库就得加「中文名 → 占位符」映射层 (按长度降序替换防前缀冲突 + 错误位置与提示回译),**复杂度不降反升**,报错体验还变差; 2. 所需能力集很小,自写约 250 行即覆盖,且**语义完全可控**(缺值短路 → `—`、除零 → `—`、最终值浮点归一); 3. **攻击面**:表达式与变量都来自用户输入,expr-eval 恰恰因变量对象注入出过 RCE;自写引擎不做属性访问/动态函数调用(仅 4 个纯数学函数白名单),无此类面; 4. 项目为 DSH 外部插件,依赖越少越稳(现依赖仅 schemastery + MCP SDK)。 **什么时候值得换成库(判据)**: - 需要**条件分支/三元**(如三态信号「空/多/观望」)→ 自写加 `?:` 约 30 行即可,仍不必引库; - 需要**字符串/日期/正则** → 能力超出「数值公式」,届时评估 CEL / Jexl(并重做安全评估); - 需要**Excel 级函数库**(SUM/IF/VLOOKUP…)→ 考虑 hyperformula / Formula.js,但那是产品形态变化(表格公式),应先讨论需求。 > 若后续决定改用第三方库,需同步修订**技术约束-022**(现规定「自写零依赖、禁止 eval/Function」)。 - `scripts/test-r026-formula-fields.mjs` 新增两节(第 7 节判定型 15 条 + 第 8 节端到端 8 条): 比较/and/or/全角符号/短路、`inferResultKind` 三例、结果类型一致性双向拒绝、布尔直通、 `strategy-positions` 附判定 `computed`、判定试算; - **端到端夹具 = 真实数据**:以 2026-09-10 实盘涨跌停(积成电子 8.22/6.72 等)+ 真实持仓基准值构造三行, 断言 `可下空单 / 可下多单` 判定与实算一致; - `npx tsc --noEmit` 0 错;`npm run build` 通过;全量 **17 个回归脚本全绿**(r026 由 48 → 71 条断言)。