inventory-mcp
库存 MCP
把方案包里那几十张飞书源表(当前 31 张)读出来、筛选去重、归一、聚合,做成十三个工具给 agent 调;每周导进飞书台账。
零依赖:MCP 走 stdio 上的 JSON-RPC,没有 node_modules,装到别的机器上只要有 node。
装
WorkBuddy 里点「MCP 列表 → 编辑配置」,或者直接改 ~/.workbuddy/mcp.json:
{
"mcpServers": {
"inventory": {
"command": "node",
"args": ["/path/to/inventory-mcp/server.mjs"]
}
}
}Claude Desktop 或别的 MCP 客户端同理,配置形态一样。
Related MCP server: dingjie-erp-mcp
十三个工具
工具 | 参数(全可选) | 回什么 |
| 资产类型 / 型号 或 槽位 / 型号们 / 品牌 / 库房 / 要几根 / 明细几行 / 要可用量 / 一定要文件 | 匹配的物料键、各自根数、按库房小计(带梯队)、合计、收尾那一行(给了「要几根」才算),不够时自动放宽一项并把候选一起带回来;回显「这次查的」(解析后的槽位,给下一轮照抄) |
| 资产类型 / 前几个 | 「型号 × 库房」矩阵 + 每个库房合计 |
| 型号 / 资产类型 / 要几根 / 库房 / 每档几个 | 按「机器能证明多少」分四档:精确 / 规则 / 存疑 / 相近,另给「凑法」;人拍过的候选带一句「人拍过」 |
| 型号 / 品牌 / 库房 / 资产类型 / 物料键 / 最多列几条 / 一定要文件 | 一根根的序列号。超过 50 个就写 CSV、只回路径,见下 |
| 和哪天比 / 资产类型 / 库房 / 前几个 | 和周基线比进出了什么。先报按 SN 算的真进出,键级变化降级,见下 |
| 资产类型 / 库房 / 字段 / 要不要每张的明细 | 这些数从哪张表哪一列来的。只读方案包、不打网络,见下 |
| 资产类型(必填) / 带槽位 / 带根数 | 这一类在库里的全部标准写法 + 有哪些品牌各多少货(封闭清单)。客户写法不标准时从里面挑,见下 |
| 资产类型 / 只看变了的 | 每条筛选规则认识哪些取值、各多少行,以及和上次比变了什么。这是原材料不是结论,见下 |
| 工单号 / 项目 / 配件需求 | 需求满足后把配件登记进占用台账(闭环最后一步)。传 cmdb「查工单」出的配件行(只 内存/硬盘/光模块/网卡)。每条按可用量分:满足→占用/审批中(关联真键)、缺口→采购在途(建/复用同型号虚拟待采购键)。默认干跑回计划+汇报,真写传 |
| 工单号 / 项目 / 配件需求 | 匹配完把通知拼好、按收件人分段返回(不真发):满足→给「资管(台账管理者)」一段、缺货/待定→给「采购(采购对接人)」一段。你复制粘到对应飞书对话框自己发——发件人是你、绕开机器人可用范围/租户策略,"复制→发送"就是审核那一步。缺货=匹配上库存但不够;待定=型号没对上库存(请核对写法)单独列。只读台账、不发消息、不要发消息权限。为什么不 bot 直发:实测用户身份发被租户策略挡(230027)、bot 私信到别人要那人进 app 可用范围(230013)、bot 发群要 bot 先在群里(230002)——返回文本你自发全绕开 |
| 确认票 / 用户选择 | 每天刷新台账+注册表+在途,出「该变没变」红检查自检回执(不是光盖时间戳)。两步确认(真写是批量写台账/注册表):① 不带参调→干跑出回执(台账 新增/更新/置0、注册表 新发几键、在途还剩几条)+ 确认票,不写;② 你必须先调 AskUserQuestion 把回执给用户看、问真写吗,带票+用户选择重调才写,真写后跑完整红检查。走飞书不要公司网;工单同步要公司网、跳过并在回执标。逻辑在 |
| 确认票 / 用户选择 / 认下筛选 | 每周四跑(日更的超集):重读源表 → 和上周四基线比(周环比:出/入库、哪些键真归零→占用悬空、在途到货没)→ 品牌待补 → 存本周基线 → 写台账。两步确认同日更(干跑出回执+票 → AskUserQuestion → 真写)。硬规矩:有源表读不到就不存基线、不写台账。逻辑在 |
| 全必填:资产类型 / 要的 / 顶上的 / 结论 / 依据 / 谁拍的 | 把人当场拍板的替代决定记下来,下次不用再问。唯一往磁盘写「判断」的工具,见下 |
参数一律扁平,数组只装字符串,没有嵌套对象 —— 便宜的模型对嵌套结构容错差。
跨类一次问完写成 型号们: ["光模块:SR4","硬盘:960G"],仍是扁平字符串数组。
参数改过名就在协议层顶回去,不静默忽略。 找替代 的「数量」2026-08-14 并成了
「要几根」(和 查库存 同一个名字 —— 同一件事两个名字,模型迟早发错那个)。
静默忽略的表现是:模型拿到一份看着正常的返回,只是「够不够」那一块整个没了,
一个不报错的缺块。所以老名字直接回 -32602 让它换个名字重发。
型号怎么匹配:走槽位,没有字符串这条路
「型号」不是子串匹配。 实测过一次真事故:问 OSFP112-800G-2*DR4-SM1310 子串匹配命中 0,
而库里有 18,000 根 OSFP112-RHS-800G-2*DR4-SM1310 —— 中间多一段 RHS 整串就对不上,
模型据此报「库里没这个货」。现在两边都解析成 form/rate/std/media/wave 再逐项比,
连裸关键词都吃得下(SR4 解析成 {std:SR4, media:MM, wave:850},精确命中 35 个键)。
链路是死的:
模型(对着 instructions 里两张常驻表:槽位词表 351 token + 品牌名单 93 token)
定资产类型(必填,机器不猜)→ 把客户的乱写法翻成槽位 → 挑品牌标准名
↓
查库存 → 校验槽位值在词表里(不在当场报错并列出合法值)
→ 逐槽位相等才算命中 → 品牌精确匹配(不是包含)
↓
命中 0,或者命中了但不够「要几根」
→ 自动放宽收益最大的那一项,把明细直接带回来(不让模型再问一轮 ≈ 8,000 token)
→ 「没查到」+ 你的槽位是什么 + 差得最少的 5 个,每条带**逐槽位对照**
(对上的和没对上的都列 —— 只列差异的话,人分不清「其余几项真的相同」
还是「其余几项压根没比」)放宽不是只在零命中时做。 命中 300 根、客户要 2,800 根时,放宽之后的 8,932 根才是答案,
而只在 命中 === 0 时算的话,这种情况一个字都不给。判据是「本档够不够 要几根」,
不是「有没有命中」——所以不填 要几根 就不会触发,工具只答「有多少」。
放宽是读操作:摆出候选不等于断言它能用。返回里写清放宽了哪一项、其余槽位逐项相同,
「能不能插」留给人和模型判(QSFP 和 QSFP28 是同一个笼子的两种写法,
SR4 多模 和 LR4 单模 就是不通)。放宽封装之后冒出好几种封装时挂一条 ❓ 这里该问人。
分工是死的:模型做翻译和选择,机器做判断。 翻译的输出受词表约束、当场可验; 「两组槽位是不是同一款」一行都不交给模型 —— 交了的话同一对型号今天判同款明天判不同。 判据是**「答案在不在一个可枚举的集合里」**:在(资产类型 4 个、某类型号 80 个、 品牌 32 个、槽位值 24 个)→ 给模型挑,挑错了当场可验;不在(A 能不能替代 B)→ 机器算。
不猜资产类型。 原来是拿四类解析器都试、谁填的槽位多算谁 —— 189 个真型号里猜不出 61 个,
另有 5 个被多类同时认(128GB 2Rx4 PC5-5600B 光模块和内存都认),靠一票之差分胜负。
现在库里没这个写法又没给资产类型就报错让人补。
但只有一处属性真的定不了类,其余都能(2026-08-15 把四类真实写法全解析了一遍):
101 个 槽位=值 里 96 个只被一类用过,共用的只有 rate 的 5 个值 ——
10G / 25G / 100G / 200G / 400G,光模块和网卡都有。内存的 cap(16/64/96/128G)和硬盘的
cap(480G 起)一个都不撞。所以 instructions 里只写了这一条提示(约 122 token,
一次会话发一遍),没有建「容量→资产类型」的索引 —— 索引强的地方(QSFP28→光模块、
3.84TB→硬盘)模型本来就对,索引弱的地方(只给一个 400G)它也一样卡住,
建表是在模型已经对的地方加确定性、在它不行的地方帮不上,还多一套要防过期的东西。
槽位这条路的两个洞(2026-08-23 量出来的)
问一个具体型号,回来的可能是整个资产类型的库存,而回包里写着「按槽位匹配,库里逐项相等的才算命中」。
两个洞的根子是同一个:比一个() 只遍历核心槽位,而且「需求没给的槽位 → continue」(不约束)。
一路 continue 到底,就是「每一行都逐项相等」。
洞一:需求里一个核心槽位都没有。 两条路都能走到:
怎么发生的 | 实测 |
型号解析出空槽位集。库里 189 个真型号里有 23 个是这样(硬盘 12 / 网卡 9 / 内存 2),全是厂商料号: | 问 |
显式给的槽位全落在 core 之外。光模块的 |
|
方向是最坏的那个 —— 多报:少报人会追问,多报人直接许出去了。
修法:按槽位配 先算「有几个槽位真会被拿去比」(= core ∩ 需求)。为 0 时不再返回匹配,改成两支——
这个写法的字面指纹在库里 → 只回它自己那几条,标
只按字面,同时说清「库里可能还有同款的别的写法,这次没找」。字面也不在 → 回「没查成」+ 怎么办(照词表翻成核心槽位重查,或调
看有哪些型号拿清单挑)。
字面比的是 ledger.字面指纹(大写 + 去分隔符,和 占用写.归一型号、normalize 分组同一个口径),
不是严格相等 —— 严格相等量过一次,客户把料号写成小写就全找不回来了(-21 条)。
不做前缀/包含匹配:那条路是这个仓库特意删掉的,会漏掉「中间多一段」的同款货(实测漏 18,000 根)。
洞二:只解出一部分核心槽位,却报成精确匹配。 这个比洞一普遍得多 —— 189 个真型号里 86 个是部分覆盖。 没解出的那几项不参与比较,语义上说得通(没约束的不筛),但那句「逐项相等才算命中」一个字都没变, 于是「真精确」和「只约束了五分之一」在回包里长得一模一样。实测放大倍数:
解出几项核心槽位 | 平均认回几种货 |
光模块 5/5 | 1.3 |
光模块 4/5 | 2.4 |
光模块 3/5 | 4.0 |
光模块 1/5 | 38(80 种里的 38。那个型号是 |
硬盘 4/4 | 1.0 |
硬盘 1/4 | 6.7 |
修法不是拒答(客户说 3.84T 时回 11 种是对的),是把覆盖率说出来:
按槽位配 返回 解析覆盖 {参与比较, 没参与, 全覆盖},怎么筛的 在没全覆盖时明说
「这不是精确匹配 —— 4 项核心只约束了 cap,bus、form、gen 没参与比较,
所以下面 11 条里混着 bus/form/gen 各不相同的货。报给人时别说成「就是这个型号」」。
顺带发现召回率那个 100% 有一部分是假的。 那 23 个料号原来命中整类,真值自然在里面 → 被算成召回。
修完 带项目尾巴 那行掉到 88%,逐条归因确认掉的 23 条全是这批料号、一条真回归都没有,基线才降的
(见 tests/召回.test.mjs 里那段基线注释)。其余几行靠字面指纹回到了原水位:
全小写/连字符换空格 回到 100%,去掉所有分隔符 回到 96%(剩的 8 条是修之前就没中的真缺口)。
三道闸都从源码里拿掉验过必红:洞一那道 → 查库存.test.mjs 红;字面改回严格相等 → 召回.test.mjs 红;
洞二那段 → 查库存.test.mjs 红。
三个「给模型看的清单」,边界是死的
看源表 这个数从哪张表、哪一列来的 —— 答来源
看有哪些型号 这一类有哪些标准写法和品牌 —— 答清单,给模型挑
看筛选 每条规则认识哪些取值、变了什么 —— 答原材料,给模型判看筛选:交出去的是原材料,不是结论
45 条规则(表 × 规则字段)各认识哪些取值、各多少行,加上和上次基线的 diff。
全量约 1,500 token,只看变了的: true 约 170。
上一版这里是机器判的:「命中率掉超过 20 个百分点 = ⚠ 明显下滑」。那个阈值是拍的、 标着未验证,而同一个变化在三种情境下意思完全不同 —— 人主动改了筛选规则 / 源表的列被改了 / 真的进了一批新状态的货,机器分不出是哪种。实测还有三张表常年 7% / 15% / 20%(全量资产台账, 大部分行是「在线」的在用设备),任何绝对阈值都会把它们误报成异常。
现在的分法:
谁 | 干什么 |
机器 | 收集 45 条规则的取值分布;和上次做 diff(纯 diff,无判断);「读到有效行却一条都没命中」这个零误报的绝对判据留着 |
模型 | 这些变化是哪种情境;某个取值被排除掉了要不要紧( |
人 | 滚不滚基线 —— |
看有哪些型号:封闭清单,让模型挑
某一类在库里的全部标准写法。光模块 80 个 = 1,128 token、硬盘 62 个 = 512、内存 17 个 = 285。
客户的写法不标准、模型拿不准对应库里哪个时调它 —— 从清单里挑一个确切的再去查。
带槽位: true 每条附上解析结果(光模块涨到 4,025 token)。
一并给这一类有哪些品牌、各多少货(按根数倒序,只列有货的)—— instructions 里那份品牌名单
只有「标准名 ← 别名」,模型挑品牌时看不见分布,可能挑一个库里根本没有的,
然后拿到 0 却分不清是「没这个牌子」还是「自己翻错了」。
那两个枚举从方案包现抠,不在 server.mjs 里写死(现抠枚举(),lib/ledger.mjs)——
和「槽位解析从油猴现抠」同一条规矩。原来它们各被复制了 9 遍(五个工具的 inputSchema),
方案包里加一类资产或一个库房,这边毫无反应:模型筛不到它、那批货就查不出来,而且不报错。
抠不到方案包时不给 enum,而不是给一个空 enum——空 enum 等于「什么都不许填」,
是个静默的全禁;这时参数退化成自由字符串,第一次真调用会带着真正的原因失败。
tests/direct.test.mjs 判据 ㊳ 直接 grep server.mjs,写死一个就红。
在库 ≠ 可用量:在库来自方案包里那些源表(实时),占用中来自台账的占用记录汇总,
可用量 = 在库 − 占用中。对外承诺以可用量为准。读台账实测 4.4 秒,所以按需读:
调用 | 耗时(热态) | 答什么 |
| 2.5 秒 | 在库,带一句「这是在库不是可用量」 |
| 5.1 秒 | 在库 / 占用中 / 可用量三个数 |
| 2.5 秒 | 不读占用 |
| 4.6 秒 | 「够不够」必须扣掉被占的 |
| 2.5 秒 | 不读占用(要的是清单,不是能不能拿) |
| 2.5 秒 | 不读占用 |
| 0 秒 | 只读 |
三个工具的默认值故意不一样:查库存 问的是「有多少」,找替代 问的是「能不能顶上」——
后者隐含「能不能拿到」,说够了但实际被占了,人会白跑一趟采购。所以找替代不给关闭选项。
找替代 的每条候选同时给「在库」和「可用量」,够不够 和 小计 都按可用量算,
排序也按可用量(在库多但被占光的不许排前面)。两个数的差本身就是人要知道的。
相近档里只差一个槽位、其余逐项相同的单独标一句 只差这一项(QSFP28-100G-SR4
vs QSFP-100G-SR4 只差 form)。人拍过:不当同一款,但相关类要做推荐 ——
所以它们仍然只进相近档,绝不进精确/规则;标的是「差在哪一项、其余哪几项相同」这个
算出来的事实,不说「所以能替代」,那是光模块知识、不是槽位算得出来的。
相近档默认给「分层」,不给「前 N 条」:按差几项分组,每层报个数、根数、前 2 个代表
(带完整差异和 只差这一项 整句)。实测一次查询相近档全量 192 条 ——
差 1 项 18 条/6,640 根、差 2 项 16 条/21,664 根 … 差 5 项 57 条/24,434 根。
只给排序后的前 5 条时,被截掉的 187 条只剩一句「另有 187 个规格」,人看不出它们差在哪个量级上;
分层之后「差 1 项 18 条」和「差 5 项 57 条」是两个完全不同的信号。
同样「差几项」还要按代价再拆一层(2026-08-15):差一个 wave(1310 vs 1300,能放)
和差一个 std(DR4 vs FR4,多模换单模、根本不通)都是「差 1 项」,混在一层里人得整层翻完
才知道哪几个值得看。分层键是「差几项 + 这几项里最重的那个放宽代价」,每层还带一句
怎么读(能放 → 先看这一层;不能放 → 除非人有别的依据否则别推)。
取最重不取平均:差两项里只要有一项不能放,这个候选就是不能放,另一项多好都不改结论。
排序先按差几项、同差几项按代价从轻到重(判据 ㊳㊴㊵ + 消融 10)。
平铺的「相近候选」和分层必须同序(判据 ㊶㊷ + 消融 11)。加分层的时候差点埋一个坑:
平铺列表还按老规矩排(差几项 → 比需求低 → 根数降序),三个候选都「差 1 项」时一路落到
按根数降序,而根数最多的恰好可能是最不该推的 —— 实测差 std(不能放,33 根)排第一、
差 wave(能放,11 根)排最后,而分层里正好倒过来。同一批货两种视图顺序相反,
看分层的人第一眼看到最该看的,要平铺列表的人第一眼看到最不该看的。
现在「最重代价」进了排序键,排在根数前面:一个插不上的货有再多根也没用。
排序后的那份全量列表没删(截断行为、层内排序、每条差异数这些只有它验得了,砍过一次
8 条判据当场没了依据),要它就显式传 每档几个。
占用读不到时不退回在库冒充:候选里干脆不给「可用量」这个字段(给一个等于在库的
「可用量」比不给危险得多,它看着是个已经扣过的数),小计.按什么算的 和 够不够
两处都自报是按在库判的、并带 ⚠。tests/substitute.test.mjs 消融 4 拿掉这一层就必须变红。
台账读不到时不让整个查询失败,改报 ⚠ 可用量算不出来——「没人占」和「算不出来」是两回事。
每个返回值都挂同一个「口径」块(数据来源、读取时间和身份、在库总根数、品牌怎么补的、
坏件排除、SN 对比、筛选命中率)。不做成独立的「查数据新鲜度」工具——那样模型不会
主动去查,人就看不到。以 ⚠ 开头的字段工具描述里要求模型必须原样转达。
口径块里只留会改变这次回答的字段。各段耗时、读取秒、筛选去重链路、结构缓存、
归一改动、判据来源、台账原始行数这几项是给写代码的人调试用的,模型每次都要读完,
几轮下来是纯噪音——实测占口径块一半、占整个返回体约两成。默认不发,INVENTORY_VERBOSE=1
才发。不是删掉:出问题时那些数字是唯一的定位线索。tests/scope.test.mjs 守住
「该发的一条都没被瘦掉」——少发一个耗时数字没人受伤,少发一条「品牌是补的」
就是把「这个牌子是我们猜的」藏了起来,而藏起来不会报错。
返回体第一个字段是「怎么答」
便宜的模型会把结果写成一大段流水账。格式指令放在返回体的第一个字段,因为它是 从上往下读的,指令排在几千 token 数据后面基本不生效;只写在工具描述里也不行—— 描述在会话开头读一次,几轮之后就被挤远了,而返回体是它每次组织答案时都要重看的。
怎么答: 先出一张表:品牌 | 型号 | 库房 | 在库 | 占用中 | 可用量。
表下面用短句补这几条,一条一行:⚠ 开头的每一条原样带上、
同一型号在多个库房时按库房逐行列,不许加总成一个数、数据读取时间。
别写查询过程、别复述字段名、别加收尾总结段。梯队不进表头。 它是拿来判「这批货要不要跨库房协调」的,不是给人看的列 —— 人要的是「哪个牌子、在哪个库房、有多少」。塞进去会让每张表多一列没人看的数字。
代价约 130 token/次,换掉的是一整段「我调用了查库存工具,查询到以下结果……」。
它只管形状——哪些字段必须转达仍归各自的 ⚠ 和工具描述。
参数回显:把这次实际用的条件写回去
多轮追问是模型最容易错的地方,而且错了不报错:人问完「闵行有多少 400G DR4」接着说 「那临港呢」,模型要靠回忆自己上一轮传了什么 —— 记漏一个槽位查出来多一大截、 多带一个查出来少一大截,两种都拿到一个看着正常的数,人也看不出那不是他问的东西。
查库存 现在把这次实际用的条件回显出来,排在数据前面(排在几千 token 数据后面模型读不到):
这次查的: { 资产类型:'光模块', 槽位:'rate=400G,std=DR4,media=SM,wave=1310', 库房:'闵行' }
换条件时: 照抄「这次查的」改一项,别凭印象重写 —— 少一个槽位会多查出一大截、
多一个会少一大截,两种都不报错。回显的是解析后的槽位,不是原样回参数。 上面那个例子里模型传的是 型号:"400G DR4",
而 DR4 这个标准自动推出了单模和 1310 波长 —— 它实际问的是四个约束,自己不知道。
回显之后它看得见,想放宽就能精确删掉 wave=1310 那一段,而不是整条重写。
必须序列化成 槽位 参数本来收的字符串形式(k=v,k=v),不能回一个对象 ——
回对象看着更结构化,但模型贴不回去,往返就断了。
判据是往返,不是「回显里有没有这个字段」(tests/protocol.test.mjs ㉜㉝㉞):
拿回显原样重查,合计必须一模一样。只验形状的话,「回显一个对象」这种写法会绿而功能是坏的。
不上「查询 ID」那套:那要工具端存状态,回显不用。
收尾那一行:工具算,模型抄(lib/凑单.mjs)
一批型号查完,人要的是每款一行「够不够、从哪儿调」。这一行是人拿去下单的依据 —— 它说「山西 302 + 临港 283」,人就按这个数去两个库房调货。 让模型自己从明细里加,加错了不会有任何东西报错,货到了才发现少几百根。所以机器算:
一处就够 QSFPDD-400G-DR4:光迅·山西 满足
要凑好几处 QSFPDD-400G-DR4:光迅·山西 302 + 海光芯创·临港9号楼 283 = 585 满足
凑不够 QSFPDD-400G-DR4:全部 8 处合计 1073,缺 1727
没给「要几根」 QSFPDD-400G-DR4:光迅·山西 (附「这只是货最多的那一处,不代表够」)三条规矩:
凑出来的每一处各自带数量。 写成
光迅+海光芯创·山西+临港9号楼 满足语法上没错、 读着也顺,但人不知道该去山西调多少、去临港调多少 —— 而这个错不会让任何东西红。凑不够时不逐处铺开,只报合计和缺口。那 N 处的数在「按库房」里本来就有, 摆进这一行只会让人以为「这些加起来就是答案」。
没给「要几根」就不许出现「满足」(返回
够: null)—— 工具没算过够不够, 这时候写「满足」是模型在替人下结论。
「用几处是最少的」靠贪心,而贪心在这个问题上就是最优解:取最大的 k 个能让 k 项和最大, 所以第一次够的那个 k 就是最小 k。前提是「每处能全取」 —— 哪天要加「某库房最多调 200 根」 这种上限,这条就不成立了,得换算法。
换机器 / 出问题:先跑 ./自检.mjs
./自检.mjs 全查一遍(会真读几张源表,约 8 秒)
./自检.mjs --快 跳过真读那步,不打网络这套东西有三样不在这个仓库里,光看代码看不出来:
缺什么 | 表现 | 怎么办 |
| 起不来 | 它跟 WorkBuddy 走,不是单独装的 —— 装 WorkBuddy 并打开一次。换了位置设 |
| 起不来 | 按顺序找: |
飞书那边的读权限 | 「读不到表」 | 最容易卡、也最看不出来的一环 —— 它和路径错、网络断长得一模一样。找表的维护方开权限 |
登录态不用配:lark-cli 用的就是 WorkBuddy 那套(identitySource: auto_detect),
装的人登录自己的飞书账号就行,不用给任何密钥。
发通知 现在不真发(只拼文本返回,你自己粘发),所以不需要任何发消息权限。若以后要 bot 自动发,实测出来的门槛:用户身份发被租户策略挡(230027);bot 私信到别人要那人进 app cli_aae5ee90f8f85cc5 的可用范围(230013)、发群要 bot 先在群里(230002);bot 发消息要 --as bot + im:message scope(lark-cli auth login --recommend)。唯一零门槛路径是「bot 发它在的群 + <at user_id> @人」。
每条红的后面都跟一句「怎么办」,而且一项坏了不挡后面的 ——
换机器时人想一次看全,不想修一个跑一次。这两条都有判据守着(tests/自检.test.mjs)。
三件静态检查(都在 15 秒那档里)
语言级的错交给该语言的工具,不自己写正则。三件都是全局装的(shellcheck / eslint 用 brew 和 npm -g),
仓库本身零 JS 依赖 —— ESLint 用全局二进制 + 仓库里一个 eslint.config.mjs,不要 node_modules。
工具 | 抓什么 | 今天它第一次跑就挑出的东西 |
| shell | 我刚写的 |
| JS 的 | 三处死代码;以及回放验证时精确指到 |
| 按语法树改名(不是检查,是改代码时用) | — |
只开 no-undef 和 no-unused-vars,风格类一条不开。 这个仓库的取舍(中文标识符、长注释、
内联三元)是有意的,让 linter 管风格只会制造一堆要豁免的噪音,而噪音会让人连真错也一起忽略。
接这类工具时防三件事,缺一件它就会「失效时是绿的」:
没装不许静默跳过 —— 那样它永远「通过」。
看它扫了几个文件 —— 报 0 个文件和报 0 个问题长得一样,而前者是没检查。 shellcheck 还要额外看
SC1088:它遇到不认识的语法会停止解析、照样退非 0, 260 行只扫 28 行而看起来在干活(这就是run-tests.sh里函数名叫sec/run_one/teeth/chain的原因)。只认退出码,不认输出文本 —— 第一版拿
grep ' error '匹配 eslint 输出, 而它印的是[Error/no-undef](大写、没空格),一条都匹配不上, 于是 eslint 报了错而这条检查印 ✓。一个专治「失效时是绿的」的检查,自己失效时是绿的。
批量改名一律用 ast-grep,不用 sed / 字符串替换。 实测对比(同一段代码里「窄读」有三种身份):
盲替换 注释、字符串、词义不同的地方全被换 —— 4 处里 3 处是错的
ast-grep 只换标识符那 2 处,注释和字符串一个字没动sed 的 \b 对中文不起作用(s/\b中文\b/x/ 一个字都不会改,而你以为改了)。
还有一个 shellcheck 也不报的形态:$var 后面紧跟中文标点时,bash 会把那几个字节
当成变量名的一部分($es_code) 印出乱码)。变量后面接非 ASCII 一律写 ${var}。
第三个形态,是上面那次改名自己制造的。 e9d5aa8(08-17 23:28,就是"函数名改 ASCII"那次)
把 牙 改成 teeth 时漏了并行链里的一处调用(run-tests.sh:172)。bash 只在运行时报一句
牙: command not found,退出码不受影响、汇总照印 ✓ —— 于是打网络那四条链的消融
(分段读 4 个、增量 2 个、只读要用的表 2 个、写路径 2 个)整整 15 小时一次都没跑过,
而 ./run-tests.sh 全 每次都是全绿。
bash -n通过 —— 命令名是运行时才解析的shellcheck -S warning退出码 0 —— 它不检查函数有没有定义发现它靠的不是任何一层检查,是人扫输出时看见那四行
command not found, 外加"全档 118 秒、比记录里的 195 秒短了一截"这个对不上的数
修完重跑:118 → 218 秒,那 10 个从没跑过的消融全部变红(它们本身是好的,只是从没被执行过)。 这一处至今没有会红的机制,只有一个要人去比的秒数 —— 补法是全档末尾数一下"断言有牙"的行数 够不够链数,还没做。
分发那条路:让最便宜的一档也能抓到它
tests/分发.test.mjs,0 秒、8 条判据、不打网络(走 看源表,它只读方案包)。
为什么单独有它:2026-08-18 加问答日志时,我在分发处写了 工具: name ——
而 name 在那个作用域根本不存在。后果不是报错,是 记这次 抛 ReferenceError、
日志一行不写,而查询照常返回正确结果,人和模型都看不出异常。
抓到它的是 tests/protocol.test.mjs(打网络、几分钟)。而在那之前,
17 秒那档里没有任何测试执行过 tools/call 这条分发路径:
资源/scope 不起子进程,通知 起了但只调 tools/list。
于是分发层的错只能等最慢的那档来抓。
回放验过:把 工具: name 放回去 → 这个测试当场红(tools/call 超时 20 秒),还原后回绿。
它守的是分发这一层,不是任何工具的业务判据:tools/call 通不通、返回体没被改坏、
分发处的钩子真的产生了副作用(只验返回值的话,钩子静默抛异常看不出来)、
改过名的老参数当场顶回去、没有的工具名报错并列出有哪些、ping 回空 result。
ping 单说一句:它是探活,不能掉进兜底的 -32601 —— 客户端拿它判断连接死活,
「不支持这个方法」和「进程已经没了」在客户端那儿是同一种表现,而 server 其实好好的。
一般化的教训写进全局规则了:每条便宜的验证路径必须真覆盖到那条代码 —— 一条只有最贵那档才执行到的路径,等于它的错要等最久才看得见。
资源列表只列最近几个(lib/资源.mjs)
resources/list 原来列 30 个,其中 28 个是历史导出快照。客户端拉这个列表是想知道
「这儿有什么可读的」,答案不该是二十行时间戳。现在每一类最多列 3 个
(INVENTORY_RESOURCE_LIST_MAX 可调),实测 30 → 9:三个静态资源 + 最近 3 份导出 + 最近 3 份周报。
不是怕列表失控 —— 清旧导出 每个目录本来就只留 20 份,磁盘那头有人管。是信噪比。
截断成立的前提是「截掉的还够得着」,两条路缺一不可:完整清单在 inventory://导出
(全部文件名、大小、路径),单个文件走 inventory://导出/{文件名} 模板、名字可补全。
resources/read 从来不受列表限制,任何一个 uri 都照读。哪天把这两条撤了,截断就从
「合理的降噪」变成「列表里没有 = 没有这个文件」—— tests/资源.test.mjs 的 ⑯ 和 ⑭ 是一对,守的就是这个。
问答日志:把评测集从「我造的」换成「人问的」
lib/问答日志.mjs + ./问了什么.mjs。每次工具调用记一行 JSONL 到
~/.cache/inventory-mcp/问答日志/2026-08.jsonl,按月切、只留最近三个月。
为什么要它:2026-08-18 之前一条查询日志都没有 —— 连「命中 0 发生过几次」都不知道。
而 tests/召回.test.mjs 量的是 189 个真型号 × 9 种机械扰动,那 9 种是我造的,不是人问的:
96% 说明解析器不怕大小写和连字符,说明不了「人问的东西查得到」。这份日志是把评测集换掉的原料。
./问了什么.mjs 这个月:按工具/资产类型/档位分布 + 耗时和返回体分位数
./问了什么.mjs 2026-07 指定月份
./问了什么.mjs --没答好 只列命中 0 和出错的 —— 这些才是拿去改召回的样本三条硬规矩,都在 tests/问答日志.test.mjs 里有判据:
参数原样记,不补默认值 ——「他没填」和「他填了默认值」是两件事,补了就分不出模型是不是漏填。
失败也记 —— 命中 0 和「压根跑不通」是两类问题,混在一起就分不出「这个货真没有」和「这条链坏了」。
不许拖慢查询 —— 追加写不 await、异常整个吞掉;日志坏了不该让人查不到货。
记的收口在 server.mjs 的工具分发处,一处覆盖十三个工具 ——
分散到各个工具里记,迟早有一个新工具忘了记,而「少记了一个工具」没有任何地方会报。
顺带补了 tests/不膨胀.test.mjs 的一个洞:它原来只扫五个写死的文件名,
所以新加一个带 mkdirSync 的模块它根本看不见——目录悄悄长大,而这条守着
「会长大的东西必须有清理」的判据照样绿。改成扫全部源文件之后,加这个日志时它当场拦了我一次
(日志目录 不在「该有的清理」名单里),登记 清旧日志 才放行。
一个防漏配的检查自己有个漏配的名单,是这套东西里最讽刺的一种失效。
返回体里什么占地方(2026-08-18 实测)
一次 查库存 返回 8840 字符,拆开之后大头不在我以为的地方:
5487 字符 62% 明细(10 行) ← 其中 物料键 一个字段就占四成
1394 字符 16% 口径 ← 源表链接 645(答案覆盖 6 个库房)
652 字符 7% 怎么答
319 字符 4% 按库房
其余 13 个字段加起来 不到 11%改了三处,降到 7480 字符(省 15%):
明细按梯队排。 改之前主明细一次都没排过序(直接切前 N 行),第一眼看到的可能是
二梯队的大库存 —— 而二梯队「需协调(有项目占着)」,一梯队才是「自由调用」。
第一行就是人当成答案的那一行,它必须是最容易真拿到的那批。
和 按库房 用同一套次序(byTier),两处不许各排各的;同梯队同量时按型号定死次序,两次跑输出能 diff。
对话里的明细不带 物料键。 它就是 资产类型|品牌|型号|库房 拼起来的,而那四列本来就在 ——
在对话里等于把每行最长的字段重复一遍。落文件那份还带:那是给人填占用记录用的,
四个字段自己拼一次就会拼错(分隔符、空格、大小写都得一字不差)。
源表链接的标签去重(闵行/闵行: → 闵行:)。链接本身不动 ——
它已经只给「你这个答案覆盖的库房」,不是「这次读过的所有表」。
放宽代价表:按代价挑,不按捞得多挑(lib/substitute.mjs)
查不到货时工具会「放宽一项」再查一遍。原来挑哪一项是按捞得最多挑的,
而捞得最多的恰好是代价最大的那一项 —— 客户要 100G LR4 单模,放开 std
立刻报出 8,932 根 SR4 多模,规格逐项「相同」、数字很好看,插上不亮。
按收益挑等于优先推荐最危险的那一项。
方向 表说的是「候选比需求高算不算能用」;这张表说的是「查不到时把这一项整个不约束,
风险有多大」—— 两件事,两张表:
能放 | 慎放 | 不能放 | |
光模块 |
|
|
|
内存 |
|
|
|
硬盘 |
|
|
|
网卡 | — |
|
|
wave 判「能放」是跑出来的,不是照着道理拍的。 拿全部光模块按 std 分组数波长:
23 个 std 里只有 2 个对应不止一个波长,而那两个都是同一个波长的两种写法 ——
FR4 是 1300(597 根)/1310(64 根),SR4 是 850(15,872 根)/840(8 根)。
没有一个 std 跨到真正不同的光学波长上。所以放开 wave 捞回的是写法差异,不是另一种货。
(单模↔多模那条线由 media 管,那一格是「慎放」。)
行为:只有「能放」的会被自动放宽;「慎放」的摆出来、点名要人确认;
「不能放」的每条带一句「别拿它当答案」,排在最后。
没定过代价的槽位一律按「不能放」算(放宽代价是() 的兜底)——
少定一格不该变成「默认可以放」,tests/substitute.test.mjs ㉝ 拿解析器的 core 逐项对。
拒答:这套工具唯一能说「没有」的地方
在这张表之前,系统说不出「这个货真的没有」:零命中一律被解释成「匹配没配上」,
提示词里还写着「不许说没有这个货」。于是要 100G LR4 单模、库里只有 SR4 多模 时,
工具报「有 8,932 根」。能说「没有」和敢说「有」是同一件事的两面 ——
一个永远说有的系统,说有的时候也没人信。
判据只有一条:捞到货靠的是放开哪一档的槽位。三档全捞不到 → 还是「匹配没配上」;
只有「不能放」的能捞到 → 「没查到」那句里明写 这一次可以直说,怎么答 里那条
「不许说没有」同时给出唯一的例外口子。实测:
槽位 rate=800G,std=SR8,media=SM,wave=1310
→ 「能捞到货的那几项全是不能放的(std)… 这一次可以直说「这个规格库里没有」」
(放开 std 有 18,377 根,但那是另一种货)记下决定:人拍过的,下次不用再问(lib/决定.mjs)
这是这套东西唯一会随使用变准的部分。在它之前:客户拍了「QSFP28 就用 QSFP 顶」, 下一轮从零开始再问一遍 —— 同一个问题每周问一次,每次答案还可能不一样。
为什么不写进方案包的 modelAliases。 那张表管的是「这两个写法是同一款货」,
写进去会把两边的库存合成一个物料键。而「A 能顶 B」不等于「A 就是 B」:
QSFP-100G-SR4-MM850 能顶 QSFP28-100G-SR4 用,但它们是两款货、两个键、两笔库存。
混进去的后果是库存数当场变形,而且不报错。所以另起一份(决定.json,跟着代码走,
INVENTORY_DECISIONS 可改),只影响推荐,不影响「有多少」。
四条护栏:
依据和「谁拍的」不许空 —— 这条决定将来要有人认账。和「品牌补充」同一条规矩。
日期由调用方给,模块自己不取 —— 取了就没法跑两遍验幂等。
同一对型号只留一条,判重方向不敏感(人拍的是「这两个能不能互顶」)、 写法归一之后比(
QSFP-100G和qsfp 100g是同一对)。改口时把上一版留在改过里: 「上周说能顶、这周说不能顶」本身就是要给人看的。拍「不能顶」的候选不删掉,只标 —— 删了人看不出「这个我们判过」,下次还会有人再问。
读不出来(文件坏了)时 读决定 直接抛,不当空表 —— 当空表等于把人拍过的决定悄悄清零,
而下一次查询只表现为「又来问一遍」。但 挂决定 那条路吞掉异常:决定表坏了不该让查询整个失败。
查SN:大了就给文件,不往对话里倒
源表每条记录就是一根货、带 SN,聚合成物料键时那一列被压掉了 —— 查SN(lib/sn.mjs)
把它还原回来。三件事是这个工具的全部内容:
① 快照里的写法是归一前的,得折回去。 SN 明细来自 收SN() 那份 SN\t资产类型|品牌|型号|库房,
后三段是源表原样(闵行 和 闵行库房 是两个值,SAMSUNG 和 Samsung 也是)。
所以先用 norm.rows 的 原始(记的正是 品牌0|型号0)加 PLACE 表建一张反查索引。
少了这步,查「闵行的 128G 内存」会漏掉源表写「闵行库房」的那 5,838 根,而漏掉的部分不报错,只是数偏小。
一个原始写法对上两个物料键就抛——那说明归一不是个函数了,这时按哪个算都是猜。
② SN 明细跟着 load() 的返回值走,不去读磁盘上那份快照。 快照是 fire-and-forget 写的,
冷读刚返回时盘上还是上一份;热态命中压根不重写。从结果里带出来(约 10MB),
SN 和 norm 就一定是同一次读的,在库 = 有SN + 缺SN 才对得上账。
③ 「在库根数」和「有 SN 的根数」永远分开报。 源表 SN 列有空着的,两个数不相等;
合成一个的话,人会拿 SN 条数当库存数用。对不上时返回里是 ⚠ 有货没有 SN。
另有 认不出的(全库口径):对不回任何物料键的 SN,正常是 0,非 0 说明归一和收快照两边
挑走的行不一致——这个数不能吞,吞了那批货就此从所有 SN 查询里消失。
超过 最多列几条(默认 50,INVENTORY_SN_INLINE 可调)就不在对话里列了,改写一个
带 BOM 的 CSV,返回里只给路径和按物料键的分组计数。去处按「谁要这个文件」分两个:
人明确要(一定要文件: true)→ 桌面;工具因为太大塞不进对话自己落地 → ~/.cache/inventory-mcp/导出/。
后者是工具为了省 token 的内部动作,人没要过,不该占他桌面 —— 混成一件事的后果实测过:
一下午测试跑出 20 个 CSV 糊在桌面上。两个目录各留最近 20 份,只删自己按格式生成的名字。
50 是按 token 定的:一个 SN 约 5 token,50 个约 250,整个返回体和一次普通 查库存
一个量级(约 1,400 token);再往上就开始挤后面几轮的余量,而人要一千个 SN 时,
他要的本来就是那个文件,不是在对话里滚屏。文件只在超上限或显式 一定要文件 时才写 ——
平时不写,因为写了就得有人清,而没人会去清一个每次查询都长出一个文件的目录。
零命中单独说一句「这不等于库里没这个货」:条件比的是归一后的写法,
干返一个 0 会被模型报成「没有这个货」。
看变动:真进出按 SN 算,键级变化是噪音
「有什么变动」这个问题,口径块里的 SN对比 答不了 —— 它比的是「上一次任何人跑过这个
MCP」,而任何一次查询都会把基线覆盖掉(包括 agent 自己随手查的那次)。所以它几乎
永远显示「一致」。真正的基线是每周四 ./周更.mjs 存的 周基线/YYYY-MM-DD.tsv,
看变动 读的是那个,只读不写,不会动基线。
键级变化会骗人,这是这个工具全部的设计压力。 实测 08-06 → 08-13:
口径 | 数字 |
键级:消失 30 个键 / 新增 48 个键,移动45号楼一口气少 3,573 根 | 看着像出了大事 |
按 SN:真出库 32 根 / 真入库 57 根 | 实际动的 |
只是改了写法 | 11,336 根 |
差额全是同一批货把空品牌补成了 CLT / 光迅 / H3C —— SN 一根没动。只筛移动45号楼时更
干净:75 个键变了,真进出 0 / 0。所以返回体的顺序是死的:真进出打头,
⚠ 别把改写法当出入库 跟上,键级的「真消失/真新增」是摘掉改写法之后剩的那些
(解释改名(),lib/weekly.mjs)。判据是 SN:一根 SN 两边都有就是没动,不管键怎么写。
一个键少 5 根、其中 3 根只是改写法 → 报「真少了 2 根」,不是 5 也不是 0。
tests/weekly.test.mjs 消融 3 拿掉这一层就必须变红。
要一份不存在的基线会报错并列出现存的,不返回一个「没变动」—— 「没有这个基线」和「这段时间没动过」混在一起,人会以为账是平的。
看源表:这批数从哪张表、哪一列来的
这个工具是被一次真实的答错逼出来的:有人问「128G 内存的所有 SN」,我去翻了台账那三张表 (存量明细/线下台账/占用记录)、看到没有 SN 列,就答「不存在 SN 数据」—— 而 MCP 根本不读台账,它读方案包里那些源表,那里每条记录就是一根 SN。 光看得见结果、看不见来源,就会拿错的地方当证据。
张数以方案包为准,别在文档和判据里写死:2026-08-14 删掉 CPU 方案后从 34 变 31,
tests/protocol.test.mjs 里那两条写死 34 的判据当场变红 —— 现在改成从方案包现算。
只读方案包(plans.json),一次网络都不打:要的是「配置说这张表怎么读」,
不是「这张表现在有多少行」——后者是 查库存 的事。
map 里那些「源表列名 ≠ 我们的字段名」的映射必须显式列出来。 实测有 4 张的 SN 不叫 SN:
SN: { 有: "31/31 张",
列名不一样的: [ "网卡·山西/山西广灵:叫「外部SN」",
"硬盘·山西/山西台账:叫「外部SN(必填)」",
"内存·临港9号楼/B-2项目-9号楼资产表:叫「CMDBSN」", … ] }只报「有 SN」会让人去源表里找一个不存在的列名。同理「货位」只有一部分表有, 而这 11 张里叫「货架-区块」「储位」「箱号」的都有。
默认只给按字段的汇总,逐表明细要显式要(实测汇总 2,336 token、逐表 8,580)。
两个名单都截——不截的话汇总本身 4,227 token,比它想省掉的明细还离谱;
截了必须报还剩几张(tests/源表.test.mjs 判据 ⑬)。
数据从哪来:两条路,INVENTORY_SOURCE 切
direct(默认) ledger(INVENTORY_SOURCE=ledger)
31 张源表(28 张 + 临港移动7号楼 3 张) 飞书 线下台账 (340 行)
每张 1~2 次并发调用:列宽有缓存就直接只读一类 人每周从油猴导出件粘贴
冷启动约 11 秒 / 热态约 2.4 秒 2.5 秒
约 16 万根 / 360+ 个物料键 12.9 万根 / 325 个物料键
└──────────────┬──────────────┘
库房名统一 → 品牌归一 → 型号归一 → 按物料键聚合
物料键 = 资产类型 | 品牌 | 通用型号 | 位置(粗到库房)两条路共用同一套归一(lib/ledger.mjs 的 normalize()),差别只在数据从哪来。
直读那条多三步:按方案跑 build_stock 的筛选规则 → 按 SN 去重(lib/dedup.py)→ 一条 SN 记一根聚合成台账形态。
默认走 direct 的判据是它又快又全:热态约 2 秒和读台账的 2.5 秒相当,多 3 万多根,
而且差额能解释 —— 临港移动7号楼那本线下台账没收,其余是一周的滞后加人工环节漏的。
ledger 留着当退路,不删。
这里不写死具体根数:源表一天能被改好几次(2026-08-13 当天 09:32 / 09:48 / 13:16 各一次), 写死的数字第二天就是错的,而过期的精确数字比没有更误导。要当下的数就跑一次,口径块里有。
热态那 2 秒是并发查 9 本文档的 revision;全没变就用进程内缓存,数据始终在本地,
变了才重拉。用 revision 不用 latest_modify_time —— 后者实测有 2~5 秒延迟,刚改完查会说没变。
方案包也算「版本」的一部分(方案包指纹()):改了筛选规则、加减一张表、改了列映射,
飞书那 9 本文档的 revision 一个都不会变,缓存却该失效。实测过:砍掉一个方案(少 9 张源表)
之后再查,热态照样答全部源表的数(当时 34 张 / 162,514 根,现在方案包里是 31 张)。而人看到 ⚠ 筛选列冒出新取值 之后要做的第一件事,
正是改方案包 —— 不算进去的话,改完还得等某张不相干的表被人动一下才生效。
指纹按内容算不按 mtime:拷贝/同步/恢复都会改 mtime,按 mtime 判会白重读 6~7 秒;
读不到时给固定值而不是随机数,不然每次查询都判成「变了」。
每次重读还会做一次逐单对比:把 16 万条在库记录压成 SN → 资产类型|品牌|型号|库房 存一份
(~/.cache/inventory-mcp/sn-snapshot.tsv,约 11MB,每次覆盖),和上一份按 SN 求差,结果进口径块:
少了几根(出库)、多了几根(入库),以及**「SN 没动但型号写法变了」几根**。最后这类单独报是因为
它按 SN 求差集查不出来(两边都有),但聚合后长得和一进一出一模一样 —— 这是「两周库存型号对不上」
最常见的成因。SN 明细落在 sn-diff.json,口径块里只放数字和前几个物料键(省 token)。
读旧快照和网络请求并行发起、写新快照不等待,所以不占关键路径;热态压根不碰磁盘。
冷启动还有一层磁盘上的结构缓存(~/.cache/inventory-mcp/schema.json):存 wiki→文档 token
和每张表用到的列排到第几列,这两样几乎不变,命中时每张表省掉「先读表头」那一趟。
缓存里还记着「这张表要分段、段长收敛到几行、上次多少行」。缓存过期不会读错 —— 正文里自带表头行,
每次都拿它现验一遍缺列,对不上就丢缓存走慢路(验表头(),tests/direct.test.mjs 判据 ⑮~⑱ + 消融 4 守着)。
缓存里的段长只许比当前策略小、不许比它大(夹段长()):不夹的话,每段格子 调小之后
已缓存的表照用旧的大段长,新常数对它永远不生效,而且读得对、守恒过、检查全绿,只是慢一倍。
增量重读:只重读 revision 变了的那本
原来任何一本文档变了都全量重读 31 张表。现在按文档级 revision 决定重读哪几本,没变的直接用
盘上存着的行(~/.cache/inventory-mcp/rows.json,实测 36 MB)。判据和热路那条是同一个
(只认内容变没变),只是粒度从「全都没变才用」细到「这一本没变就用这一本」。
全部都变了(=全量) 14.5 秒 复用 0 张
变了最大那本(6 张) 12.4 秒 复用 25 张
变了最小那本(1 张) 7.5 秒 复用 30 张
一本都没变(热态) 2.8 秒 复用 30 张收益完全取决于变的是哪本 —— 最大那本含闵行光模块那张 8 万行的,只省 2 秒。那 7.5 秒里真正
读表的只有一张,其余花在查一轮 revision(约 2 秒)+ 从盘上反序列化几十 MB(23 秒,
原先估成 0.30.5 秒,估错近一个数量级)+ 筛选去重 1.4 秒。
落磁盘不留内存:33 万行留内存实测占堆 168 MB,而这个 MCP 是 WorkBuddy 拉起的单例长驻进程, 留内存就是一直占着。落盘换的是内存不涨 —— 而且多覆盖一个内存版覆盖不了的场景: WorkBuddy 退出再打开时进程是新的,原来必然全量冷读,现在拿盘上的 revision 问一轮就能增量。
三种情况整份作废退回全量:方案包指纹变了(那 9 本 revision 一个都不会变,但每张表怎么读全变了,
没有「哪本变了」可言)、force、盘上没有或指纹对不上。盘上没记 tok 或 revision 的一律重读 ——
「不知道它有没有变」和「它没变」是两回事。
变了的那本重读失败时拿不到旧行(复用 对变了的文档返回 null),照常走 缺表 那条显式降级:
悄悄用旧行的话,人拿到的是「看起来完整但已过期」的数,比缺表严重得多。
行缓存只存读成功的表 —— 存一份空的等于把「这次读不到」固化成「这本就是没货」。
判据只有一条,但它是这个功能的全部安全性:增量结果必须和全量逐个物料键一模一样
(tests/增量.test.mjs)。不能拿「守恒过了」当判据 —— 复用了一本其实变了的文档,
总数少一截而每步守恒照样绿。造场景靠 INVENTORY_FAKE_CHANGED=<token片段>,
每次调用现读环境变量(用法就是「同一个进程里先全量跑一遍、再假装某本变了跑第二遍」,
读死的话第二遍改不动,测试只能起子进程、每遍都得全量打一次网络)。
只读问到的那类资产的表 + 在途去重(2026-08-17)
问一句「光模块够不够」,原来会把 31 张源表全读一遍。光模块只占 7 张,另外 24 张
(网卡 9 + 硬盘 9 + 内存 6)一行都用不上。更糟的是模型问「这两款各够不够」时会
同时发两个 查库存,而进程内缓存只在跑完那一刻才写,所以两个各读一整遍。
实测(改之前):
两个 load() 并发 55.0 秒,各自读回 332,763 行 ← 各读各的,双倍 API 调用
等第一个跑完再来第三个 2.0 秒 ← 这才走缓存改完之后:
只问光模块(冷) 42.6 秒 读 7 张 / 237,109 行 / 119,090 根
再问网卡 8.7 秒 读 9 张,行缓存累计 16 张
两个硬盘并发 8.6 秒 只读一遍,两边拿到同一个结果对象
接着全量 9.8 秒 31 张里 25 张直接复用 —— 前面几次只读一类顺手把缓存捂热了
全量之后再问光模块 2.0 秒 走「全部」那份缓存,不重读哪些工具只读一类,判据是「这个工具的答案会不会用到别类的行」:
查库存 / 看分布 / 找替代 / 看有哪些型号 只读一类;
查SN(SN 可能属于任何一类)、看变动(逐单对比是全仓口径)、看筛选(要全仓的取值分布)必须全读。
三条规矩,每条都是为了挡住一种静默错:
只读一类一个基线都不许滚。 拿只读了 7 张表的 SN 快照覆盖全仓那份,等于把「这次没读网卡」
固化成「网卡的货全没了」—— 下次全量一比,二十几万根全算「多了」,而每一步守恒照样绿。
所以只读一类只回答问题、不承担监控职责:不写 SN 快照、不滚命中基线、不写差异文件,
也不拿旧基线做对比(半份数据 vs 全份基线,归零 会对每一张没读的表报一次警)。
口径必须自报。 少了那句 ⚠ 这次只读了部分源表,在库总根数 就从「全仓」悄悄变成
「这一类」,而数字长得一模一样,模型拿它答「一共多少根」就会答小一个数量级。
行缓存是叠加不是覆盖。 只读一类只带回 7 张表的行,整份覆盖会擦掉另外 24 张, 下次问网卡还得全量冷读 —— 一个本来省时间的改动反而让别的查询变慢。
「全部」那份缓存能答任何窄问题,反过来不行(超集,答案层照样按类型筛)。 没有这条不对称,查过全量之后再问光模块会白读一遍,优化在最常见的场景里反而变慢。
两个逃生开关,同时也是消融的注入点(一个东西关不掉,就等于没法证明它在起作用):
INVENTORY_NO_NARROW=1 退回全读、INVENTORY_NO_INFLIGHT=1 关掉在途去重。
跨方案剔重(同一个 SN 既算网卡又算硬盘)是跨资产类型的,只读一类时看不见跨类的撞号。
实测当前数据是 0 条,所以今天不影响任何数字;哪天它非零了,全量那条路照样会报
⚠ 筛选规则有重叠。
你实际等的是几秒(2026-08-17/18 实测)
先说一个我自己搞错过的区分:所有「冷读 41 秒」的数字都是在空的临时目录里量的(为了不碰真缓存), 那是新机器第一次跑的场景。你的场景是 WorkBuddy 重启——进程是新的,但盘上行缓存还在:
新进程 + 真缓存,问光模块 3.8 秒 ← 7 张全复用,读表 0 秒
同进程再问一次 1.9 秒
空目录冷读 7 张(新机器才遇到) 41.1 秒那 3.8 秒拆开之后,88% 集中在一处:
2.00 秒 问 9 本文档的 revision(一趟网络,已经全部并发了)
0.14 秒 起 python 去重 33 万条
0.08 秒 JSON.parse 那 37.6 MB 行缓存
0.03 秒 从盘上读那 37.6 MB
0.01 秒 import那 2 秒压不下去,量过了:lark-cli 起一次子进程 + 一次网络往返本身就是 12 秒的地板 ——
9 个 metainfo 并发和 1 次 metas/batch_query 一样快(各 12 秒,和单发一个一样)。
所以「合并成批量调用」这条路是死的,batch_query 还只有 latest_modify_time(实测有 2~5 秒延迟)没有 revision。
所以把它挪出用户的等待路径:信任期
MCP 启动 1.5 秒后后台热一遍,之后每小时后台核一次;查询直接用上一次核过的那份,一趟网络都不打。
第一次(真核) 3.7 秒
信任期内 0.0 秒
后台定期核(绕过信任期) 2.1 秒
force(导台账 / 周更) 42.0 秒 ← 不受影响,永远真核真读代价是数据最多陈旧一小时(人拍的 2026-08-17)。所以:
陈旧必须看得见:口径块里带
数据核对于: 2026-08-18 00:00:06(3 分钟前核对的,之后源表有没有被改过这次没查)。 悄悄用一小时前的数是这套东西最不能出的错 —— 每步守恒照样绿、总数照样对,只有真去核源表才看得出来。后台那趟不吃自己的信任期(
_绕过信任),否则它每次命中缓存、永远不去真核,信任期就成了一个永不过期的谎。INVENTORY_TRUST_MS=0退回「每次查都核」。
再往下要快,只剩绕开 lark-cli 子进程直接发 HTTP(省掉那 1~2 秒地板),代价是自己接管飞书的鉴权和 token 刷新 —— 现在这些全是 lark-cli 在管。
读表为什么是现在这个速度(2026-08-15 实测)
读表这一步已经并发到头了,再想快只能少读。 三组交替实测:
① 单张表切几段 闵行光模块 84,952 行 × 9 列,每档三轮取中位
5 段 7.7 秒 · 17 段 3.7 秒 · 34 段 3.2 秒 · 68 段 4.2 秒(掉头)
② 元数据怎么发 串行(拿行数→再发段) 20.1 秒 · 段并发 7.8 秒 · 元数据与段同批 5.0 秒
③ 全局并发几路 31 张 33 万行:4 路 28.5 秒 · 8 路 13.0 秒 · 32 路 14.2 秒并发能重叠的是「等」,重叠不了「传」:请求的时间 = 等(往返延迟)+ 传(占管道)。
并发把多个「等」叠起来,但字节数不会因为同时发就变少。所以曲线是先降后平再微微上翘 ——
③ 里 8 路就填满了,32 路反而慢一点(几十个 lark-cli 进程抢 CPU);① 里 68 段同理。
INVENTORY_CONCURRENCY 默认取 8,每段格子 取 45,000(17 段),都是这么定下来的。
段长不取最快的 34 段,是拿 0.5 秒换频控余量:调用次数随段数走(5 段约 36 次 / 17 段约 48 次 / 34 段约 65 次),而最窄一档 100 次/分,撞线的代价是十几秒。
每次读都跑的五道检查
数字对不对,靠的不是「算得仔细」,是每一类错都有一条会红的检查。五道按严重度排:
检查 | 挡什么 | 不挡的后果 |
筛选命中率 | 某张表的状态列写法被改了(「在库」→「在库中」) | 那张表一条都命中不了、几千根货一次性消失,而每一步守恒照样绿(进出都少了同样多) |
源表读失败 | 读不到其中几张 | 那几个库房的货凭空消失,人以为「那儿没有」而不是「那儿不知道」 |
坏件 / 坏字 | 型号带「坏」的、规格列是 | 坏件被归一合进好件算成可用库存;坏字污染物料键 |
SN 逐单对比 | 说不清「少了 106 根」是出库、改写法,还是读错了 | 每周对账靠猜 |
型号写法不像这一类(只报不拦) | 整批货被归错资产类型 | 错得离谱但所有检查全绿 —— 926 根光模块被判成硬盘那次,总根数对、每步守恒、SN 对比正常、命中率正常,因为货确实都在、只是挂错了类。唯一能发现的方式是人扫一眼物料键列表,觉得「硬盘怎么叫 |
「归零」留在机器里,「变了多少」交给模型。 这条线是 2026-08-14 挪的:原来机器还判
「命中率掉超过 20 个百分点 = 明显下滑」,那个阈值是拍的、标着未验证,而同一个变化在三种情境下
意思完全不同(人改了规则 / 源表的列被改了 / 真进了新状态的货),机器分不出是哪种。
现在机器只报 {表, 命中率: "99% → 12%", 差几个点},算不算异常由读它的模型判(看筛选)。
命中率归零是零误报的硬信号:有效行还在却一条都没命中,不可能是正常业务。 所以它是绝对判据,不需要基线 —— 曾经写成「上次命中 > 0 才报」,于是一张 从来就命中不了的表永远静默:第一次没基线不报,第二次基线里记的也是 0,条件永远不成立。 那正是这个探测器要防的那件事。报法:
⚠ 有源表的筛选一条都没命中:
网卡(导入) · 闵行/闵行网卡在库清单信息:读到 4575 行有效数据,但筛选一条都没命中(上次命中 2841 行)
这几乎一定是那张表的状态列写法改了,不是货清空了。去核对源表的筛选字段,别按下面的数字下结论。
**这条会一直报到修好为止** —— 有异常就不滚命中基线,不然警报会把自己吞掉。警报不许把自己吞掉。 命中率基线(table-hit.json)原来每次读都覆盖,于是:
某张表被改坏 → 报一次 → 基线滚成「命中 0」→ 从此再也不报。现在
该滚基线() 只在这次干净时才让它滚;有归零或下滑就保持不动,异常一直报到修好。
唯一的解除方式是人显式认下:./周更.mjs --认下筛选(--dry 时不认)。
不给自动认领的口子——能被自己清掉的警报等于没有警报。
同一个病 SN 快照也有:它每次读都覆盖,所以口径块里的 SN对比 比的是「上次任何人跑过
这个 MCP」,几乎永远显示「一致」。那半边的解法是 看变动 读周基线(只读不写)。
绝对命中率不是判据。 实测三张表常年低命中,规则都是对的:
表 | 规则 | 这一列的实际取值 |
B-2临港9号楼 · 光模块(7%) |
| 在线 52,341 · 库房 4,280 · RMA出库 1,330 · 「在线,无对应关系」853 |
JYJY临港9号楼 · 光模块(15%) | 同上 | 在线 48,777 · 库房 10,575 · 调拨出库 7,910 · 待定 50 |
移动7号楼 · 内存(20%) |
| 出库 23,689 · 在库 6,031 · 故障 32 |
那几张是全量资产台账,大部分行是已上机在用的设备。定任何绝对阈值都会把它们误报成异常。
第五道:型号写法不像这一类(类不对的键,lib/ledger.mjs)
判据是「别的类解析出的核心槽位比自己多几个」,阈值 2,是量出来的不是拍的 (2026-08-16,374 个真实键 + 那批历史错键):
真实键 差 ≤ 0 的 372 个 · 差 1 的 2 个 · 差 ≥ 2 的 0 个
历史错键 硬盘|QSFP112-400G-DR4-SM1310 自己 1 槽位 vs 光模块 5 → 差 4
正常硬盘 硬盘|7.68T NVMe U.2 Gen4 自己 4 槽位 vs 光模块 0 → 差 -4中间空了两档,所以 2 有真实余量。判据 ㉕㉖㉗㉘ + 消融 9,其中 ㉘ 专门守「阈值不许放松」。
它抓的不只是一种成因:跨方案剔重把货判给先出现的方案、方案包的 配件类型 筛错、
某张表的 map 列映射错、源表里有人把货填到错误的分类下 —— 四种现象一样。
第一版判据「自己解析出 0 个槽位」不成立:硬盘的解析器把 400G 当成容量、解析出 1 个,
历史错键一个都抓不到。这正是「四类之间只有 rate 那 5 个值撞车」在这里的表现 ——
那个结论按写法种数算够用,用在「跨类比槽位数」上就不够,撞一个就把条件破坏了。
只报不拦。 它是启发式的,和「筛选命中率归零」那种零误报的硬信号不是一档。
哪些维度错了查不出来
五道检查覆盖的是「有第二来源可以对照」的那部分,不是全部。 一个维度错了之后 总量还是守恒的,能不能发现取决于源表里有没有第二个角度去看它:
维度 | 错了会怎样 | 有没有第二来源 | 现状 |
资产类型 | 整批货归错类,总数守恒 | 有 —— 型号写法能反推 | 第五道检查 |
库房 | 整批货挪库房,总数守恒 | 没有 —— 物料键的库房段来自方案包配置( | 只能靠配置对 |
品牌 | 整批货换品牌,总数守恒 | 没有 —— 源表只有一份 | 只防「两个方案的对照表打架」( |
在库状态 | 筛多了 → 货凭空变多 | 没有 | 命中率只抓「筛没了」(归零);筛多了命中率反而涨,看着更健康 |
最后一行是这张表里最该记的:「筛多了」比「筛少了」隐蔽。人对数字变大的警惕本来就比 变小低,而机器唯一的探测器正好在往反方向看。
表名当第二来源试过,不行:31 张表里误报 3 张(B-2项目9号楼 这种表名在 PLACE
别名表里登记的是 B-2临港9号楼,差两个字),10% 的误报率做成每次跑的警报只会变噪音。
这套接口不会告诉你发生了什么
飞书多维表格的写接口有一个一贯风格:说成功了,但你没法从返回值知道发生了什么。 2026-08-15~16 两天里踩到五次,放在一起看比分散在各处的注释有用:
命令 | 它不告诉你什么 |
| 回 |
| 不检查 record ID 存不存在。拿一个不存在的 ID 更新,照样回 |
| 返回体里没有 table_id,只回 |
| 只回显请求体、不校验形状。错的和对的都回 |
写公式字段 | 被 |
所以「写完必须回读、不认返回值」在这个项目里不是保守,是唯一可行的做法。
2026-08-16 那次事故正反两面都验到了:回读挡下了一次(./导台账.mjs 补跑时报「值对不上 0 条」),
而崩在回读之前那次它没挡住 —— 所以现在写失败也要走完读回(见 lib/ledger-write.mjs)。
请求体的形状只有一份:创建请求体() / 更新请求体() 在 lib/ledger-write.mjs,
契约测试用的就是这两个函数。原来测试里手写一份 {update_records:{id:{数量:100}}},
于是它抓得住「lark-cli 又改契约了」,却抓不住「我们的代码拼错了」——
而 8-16 真炸的那次正是后者的相邻情况(契约变了、我们没跟上)。两份字面量各自成立,
谁改了另一份都不知道。守这条的是 写路径 那两个消融:把生产那两个函数换成错形状,
判据必须红(实测 ABLATE=1 报的就是当天那个 800010701 Request validation failed)——
要是哪天有人在测试里又手写一遍,消融就不再红,run-tests.sh 当场报「一条判据都没让它红」。
撞频控:一个显式的错误,被压成了要靠猜的
飞书按 API × 应用 × 租户算每分钟速率,最窄一档 100 次/分,撞了回 HTTP 429 + code 99991400。
这是个显式错误,可它一路传上来只剩「这次失败了」,和「表读不到」「登录态过期」长得一模一样。
代价是实打实的:2026-08-16 这一天为此多跑了四轮回归(每轮好几分钟), 每次判断「是限流还是真坏了」靠的都是间接推理——两次红的位置不同(#74 / #55), 位置随机才像环境问题,代码问题会稳定停在同一处。
现在三层都认它:
在哪 | 干什么 |
| 撞了就退避重试(3 秒、8 秒),等的时候往 stderr 出声;退完还撞就打 |
| 分开报:撞频控说「代码没问题,等一分钟再跑」,真读不到才说「查登录态和网络」 |
| 挂的每一条都是频控 → 退出码 3, |
三处要点,每一处都是踩出来的:
退避只退两次、最多 11 秒,不是越多越好。 一开始写的 [3,8,20],然后意识到它会和调用方的
120 秒超时打架——31 张表每张退三次能把整轮读表拖到几分钟,于是「明确的限流」又变回
「莫名其妙的超时」,正是这次要修的那个问题的翻版。
「没验成」不许印成绿的。 只有「挂的每一条都是频控」才退 3,混一条真失败就退 1——
否则频控成了挡箭牌,把真的红盖成黄的。最要紧的是消融那条循环:牙() 只看「非 0 就算变红」,
一个根本没被执行到的消融照样被数成一颗牙、印「✓ N/N 消融都变红」。
是频控失败() 故意写窄。 曾想把「超时」也算进来(历史上撞频控就表现成 tools/call 卡满
120 秒),但那会把「服务器真的卡死了」也悄悄降级成「这轮没验成」。代价是:限流若表现成纯超时、
一个字都没留,这里认不出来,照旧报红。宁可多红。
INVENTORY_FAKE_RATELIMIT=N 造这个场景(前 N 次调用一律返回限流),INVENTORY_BACKOFF_MS
把等待压到毫秒。注入点挡在 退避重试() 那一层而不是 call() 里面——挡在里面的话
写台账那条路注入不进去,而那条一年只跑五十次,撞上了没人有第二次机会看现场。
我一开始说这条「要等真撞一次才验得了」。那是错的——同一个仓库里
INVENTORY_FAKE_FAIL、INVENTORY_FAKE_CHANGED已经用过两次这个模式,理由还是我自己写进注释的。 手上有现成的解法却没想起来用,比不知道更该记一笔。
筛选列冒出新取值(收筛选取值 / 比取值)
命中率抓不住的是慢性的那一类:有人把「在库」改成「在库中」,老记录还是老写法, 命中数只会一周一周地掉 —— 归零不触发,20 个点的阈值也够不着,等发现已经过了几周。 所以再加一条离散信号:走筛选之前的原始行,统计每张表每个筛选列的取值集合; 冒出一个基线里没有的取值就报。几乎零误报,代价是一次全量读多 0.3 秒、基线文件 7.8 KB。
⚠ 筛选列冒出了没见过的取值:
光模块 · 临港9号楼/B-2临港9号楼 · 资产状态:冒出新取值「在线,无对应关系」853 行
这批行现在没被算进任何一边。如果它其实是「在库」的另一种写法,那批货正在静默丢失;
如果是新的业务状态(借出、待检…),把它加进方案包的筛选规则再跑。只报新增不报消失:某个状态这周没人用了是正常的。基线里没有这张表这一列时也不报,
不然第一次跑会把每种取值都当新的。同样受 该滚基线() 管——报了就不滚基线,一直报到处理为止。
读表失败不再一刀切地抛,只有全部源表都读不到才抛(那是登录态或网络问题,给半份没意义)。
只崩几张的话继续走,但完全读不到的库房必须显式占一行、根数写 null:
{ "库房": "七宝", "根数": null, "读不到": "网卡、硬盘、内存、光模块的表都没读到 —— 这里不是 0 根,是不知道" }让它从数组里消失,人会读成「那儿没货」——这两件事差一个数量级。有表失败时不写进程内缓存, 否则下次热态会拿这份残缺数据当好的用,而那几本文档 revision 一个都没变,它会一直用下去。
造这个场景用 INVENTORY_FAKE_FAIL='方案/库房/表名的子串'——不给注入点,这条降级路径永远没法验证。
型号写法:两层归一
第一层字面(字面指纹 + 挑标准写法):只吃掉大小写、空格、连字符、点号的差异。
实测合掉 11 组,CX7-400G ⟷ Cx7 400G、128G ⟷ 128g、SFP-25G-SR-LC ⟷ SFP.25G-SR.LC 这种。
槽位解析救不了它们——解析器认的是规格语义,不认打字方式。
这一层选标准写法的判据不能沾根数:大写多 > 连字符多 > 长度长 > 字典序,四条全是型号字符串 自身的属性。原来的规则里有「根数多者胜」,而根数每周都在变——标准写法是物料键的第三段, 它一变,台账下拉框里选过的值就悬空了。
第二层槽位:解析出封装/速率/标准/介质/波长这些槽位,核心槽位全等才合。这层的判据 运行时从油猴脚本现抠,不复制。
直读这条路踩过的四个坑(都在代码里挡住了)
坑 | 挡法 |
闵行光模块那张整表读报 | 所有表都只读 |
那张表长到 84,952 行 × 9 列,只读一类也超 10MB —— 全库最大的一块(8 万多根)整张读不到 | 按行分段读再拼,段太大就当场砍半重来( |
表头是富文本:山西光模块的「品牌(必填)」是白色「品牌」+ 红色「(必填)」两段,返回结构体 |
|
临港联通那张被网卡/硬盘/光模块三个方案共用, | 按方案包顺序剔重,剔除数量报进口径块 —— 它涨了就说明筛选规则该修 |
默认读回来的是公式文本(台账「物料键」列 340 行全是 | 一律带 |
lark-cli 报错有两条路,原来只认了一条(2026-08-15 查清): |
|
位置粗到库房,货位不管
源表里的 403库房 / CK2-TEMP / 二楼小仓库 是货位,74 种,一律不进物料键 ——
位置取 source 的 region(闵行 / 临港9号楼 / 移动7号楼 / 移动45号楼 / 七宝 / 山西 / 临港联通)。货位是仓管的活。
走原生 values 接口,不用封装的 +csv-get:后者超 10MB 会悄悄只给你一部分(ok:true、数据也是好数据,只有一个 has_more:true),我因此在一个 9.4% 的样本上算过百分比还以为覆盖了全表。原生接口超限直接报 90221,静默失败变显式失败。
台账本身是人每周从油猴导出件粘贴进来的,不是实时的。口径块里的「读取时间」是这份数字的时刻;飞书的版本号留在 meta.revision 里,代码用它判断变没变,不往口径块塞——对人是一串没信息量的数字。
采购在途:已经许出去、但货还没到(2026-08-17)
占用表里出现了第四种闭环状态「采购在途」。做法是:人在物料表手工加一条, 品牌和库房留空(那两段要到货才定),占用挂在它上面。实测那条:
光模块||QSFPDD-400G-DR4 | 在库 0 · 占用 640 · 预计到货 2026-08-31
备注 智设合【2026】年325-004 · 工单 29640危险的不是账面上那个 −640,是到货那一刻。 货落在源表产出的四段全的真键上
(光模块|海光芯创|QSFPDD-400G-DR4-SM1310|闵行),那个键的占用是 0,
于是同一批 640 根在两个地方各算了一次:空键说「许出去了」,真键说「可用」。
人拿真键那个数承诺就是超发。台账里 400G DR4 现成的真键有 11 个、合计 607 根,
和这笔采购一个量级。
挡它的是一条反方向的检查
挂可用量 原来只往一个方向查:源表有、台账没有(该导没导 / 不进台账的)。
反方向「台账有占用、源表却没有这个键」完全看不见 —— 那正是在途落进去的地方。
现在补上 没挂上的占用,出现在三处:
在哪 | 为什么 |
| 排在数据前面,不进口径块也不进调试档 —— 它不是「这个数怎么算的」,是「照这个数发货会超发」 |
| 这里更致命:结论会直接变成「不用采购了」 |
| 每周该看的就是「在途到了没有」,节奏本来就是周级的 |
不按资产类型或型号过滤,全都报。 这类键极少(实测全仓 1 个),而漏报的代价是超发; 过滤逻辑本身会变成一个失效时是绿的点 —— 过滤错了就静默不报。几行噪音换零漏报。 和「筛选一条都没命中」同脾气:一直报到有人处理为止。
日期跟着每一条走。 人拿到「有 640 根在途」之后的动作完全由日期决定 ——
等它到,还是另想办法。没填就明说「没填预计到货,问一下什么时候能到」,
不编一个出来:编了的后果是有人照着一个不存在的到货日排工期。
预计到货 在 读占用 里是软要求(缺列只是少个日期),而 物料键 / 占用中
缺了就算不出可用量,是硬的 —— 两者不共用一个 throw。
人要做的只有一个动作
到货后,把占用记录那行的「关联物料」改指到真键。 改完空行变成 在库 0 / 占用 0, 警报自动消失,那 640 根在真键上被正确扣掉。
闭环状态 那个下拉框 MCP 一个字都不看 —— 它在占用记录表上,而 MCP 读的是物料表。
改它是给飞书 占用时长 那一列用的(🟠等货中 / 🔴等货超期),为人自己看。
两处没有判据、要知道:
挪错了抓不出来。 只检查「有没有挪」,不检查「挪得对不对」。挪到
QSFP112而不是QSFPDD、或者挪到错的库房,一声不吭 —— 因为挪完之后那个键在源表里确实有货。分批或分散到货得拆行。 一条占用记录只能指一个物料键。
讨论过、没做的三条
为什么没做 | |
拿 | 要多读一张表 + 约 50 行;人拍了先不做 |
在途单开一张表 | 承诺一旦分两张表存,就没有一个地方能回答「这张工单一共答应了什么」 |
扩占用记录表(加型号/单号/ETA 三列, | 形式上最规范,但 |
周更跳过「备注或预计到货非空」的行 | 量过了:399 行里符合的 1 行,其中在库非 0 的 0 行 —— 这条规则今天是空的。它要成立必须靠人工填在库,而那会给 |
台账:每周导一次,只增不减
台账是「配件占用管理表」的物料表(多维表格)。MCP 读源表算在库,台账那侧算占用,
两边按物料键对上,可用量 = 实时在库 − 台账占用中。
台账只收走领用流程的配件(台账范围,定义在 lib/ledger.mjs):光模块 / 硬盘 / 网卡 / 内存。
随整机走、不按物料键单独领的东西不收 —— 进了台账只会在下拉框里多几个永远没人选的键。
导入时先过滤再校验,日志会打「挡掉 N 个不在台账范围内的物料键」。
这个集合和方案包(plans.json)现在恰好一样宽,但两者管的是两件事:方案包管
「MCP 读哪些源表」,台账范围 管「哪些资产该进占用台账」。方案包加一类资产时要单独
判断它进不进台账,别默认跟着走 —— 否则新加的那类会静默进台账,多出一批没人选的键。
「台账里没这个键」因此有两种,挂可用量 分开计数、口径块分开报。合成一个数的话,
只要存在任何一类「本来就不该进」的资产,它就永远大于 0,人看几周就习惯性忽略,
真有新货没导时也看不出来,还会照着「跑一次导台账」这条修不好的建议白跑:
信号 | 含义 | 怎么办 |
| 范围内、台账里却没有 | 跑 |
| 范围外、随整机走的 | 什么都不用做,它的「可用量」就是在库数 |
./导台账.mjs --dry # 先看一遍:新增几条、更新几条、置 0 几条
./导台账.mjs # 真写
./周更.mjs # 每周四:重读 → 和上周基线比 → 出报告 → 品牌待补清单 → 存基线 → 写台账--dry 里最该看的是「置 0 的」,而且要逐条问「这批货是没了,还是换了个键」。
--dry 只报「原在库 → 0」,不告诉你那一格现在还有没有别的货 —— 后者才分得出
「真出库」和「改名/重分类」。判据是拿键的「资产类型|品牌|库房」去这周的实时数据里查:
那一格还有别的型号 = 大概率改名,那一格空了 = 这一格真的清了。
2026-08-14 那次实测:24 条置 0 里 19 条落在「硬盘 · 临港联通」,型号却是
QSFP112-400G-DR4-SM1310 这种光模块写法 —— 加起来正好 926 根,
和那天修的「临港联通光模块被读成硬盘」是同一批。这种情况置 0 是对的,
它正是这次修复要清掉的错键。
写入规则一条不改,来自台账那侧的设计说明 §3.3:按物料键 upsert,把「这次没出现在
导入里的物料」的在库数量置 0,而不是删记录。置 0 之后记录还在,占用记录的键仍然匹配
得上,级别自动变「待采购」——库里没货还欠着人,正是想要的语义。代码里永远不调
record-delete:删了占用记录就悬空,而物料那一侧是完全静默的(可用量会悄悄涨回去、
级别还写着「充足」、数据检查是空的)。
四道闸,任何一道不过就整批不写(不是「跳过坏的写好的」):
闸 | 挡什么 |
有源表没读到 | 那个库房会被当成「清零」全部置 0,而货都在 |
键不合法(四段有空/含竖线) | 空键会「吸走所有空键的占用」,且完全静默 |
键归属对不上多个老键 | 这周把两个老键的写法合成了一组,机器不猜留哪个 |
写完读回核对 | 「API 返回 ok 但值是空的」是这张表最贵的错误 |
键一旦发放就锁死(lib/keyreg.mjs)
占用记录存的是键的字符串,而键的第三段是归一「选」出来的标准写法 —— 实测 26 个合并组
里有 9 组是靠根数决出来的(CX6-25G双口(287) vs CX6-25G*2(158),一次出库就翻盘)。
翻盘后老键归零、新键冒出来,占用记录还挂在老键上,那笔占用永远结不清。
锁法不靠槽位序列化(网卡根本解析不出槽位),靠 normalize 现成的 原始 字段:
新算出的键只要和某个已发放的键共享任何一个原始写法,就判定是同一款货,沿用老键。
判据是「共享写法」不是「键字符串相同」,标准写法怎么翻都不影响身份。沿用老键这件事会
报出来(台账里显示的还是老写法,人会觉得不对)。注册表在
~/.cache/inventory-mcp/键注册表.json,写完台账才更新 —— 台账写失败的话键不该算发出去了。
踩过的坑(都实测过):+record-list 默认分页 100、最大 200,不翻页的话台账超过 100 条
之后那些会被当成「不存在」重复新增,而 API 一路返回 ok;+record-delete 的 --record-id
是可重复参数,拿空格拼一串传会报「Record id must start with rec」而 ID 其实合法;
返回是列式的(data.data 二维数组 + data.fields 列名),不是每条一个 fields 对象。
品牌怎么补
源表品牌列空着的有三种补法,可信度不同,所以分开放、分开报:
来源 | 粒度 | 放在哪 | 口径块里报成 |
| 写法映射(Samsung→三星) |
| 归一改动 |
CMDB 导出 | 逐根 SN 查证 |
| 按SN补的品牌 |
人工整批判断 | 库房 + 资产类型 |
| 品牌是补的 |
补出来的值还要再过一遍 brandAliases —— CMDB 写 CLT、库存写 海光芯创,
不过一遍的话台账里就是同一家两个名字、两套键。归一完还有一条会抛的检查:
结果里但凡还留着「对照表里有标准写法」的品牌,直接报错不返回。
补不上的进「品牌待补」,每周四出一份 SN 清单(~/.cache/inventory-mcp/周报/品牌待补-*.csv)
拿去 CMDB 查。给 SN 而不是型号,因为 CMDB 里按 SN 才查得到一根货的品牌。
查的时候品牌传错了:工具知道答案,就别只回一个 0
查库存 的品牌是精确匹配(kw(r.brand) === kw(条件.品牌))—— 数据侧读表时已经用
brandAliases 归一过,所以查询侧要求传标准名。但客户嘴里的牌子是英文、是简称、是打了一半,
模型没翻的时候拿到的是一个不带任何线索的 0,和「这批货真的没有」长得一模一样,
而后者会被原样报给人。零命中那套补救(放宽哪一项、差得最少的几个)只在给了型号或槽位时才算,
{品牌:'Samsung'} 这种查询压根进不去。
现在命中 0 且给了品牌时,返回体里多一条「品牌这一项」,四种情形分开答
(品牌怎么救,lib/ledger.mjs):
传进来的 | 回什么 |
| 不是牌子的问题,是其余条件的问题 —— 这条最容易被误读成「这个牌子没货」 |
| 标准名是「三星」(全库 N 根),换成它重查一次 |
| 名单里没有,但有「海光芯创」—— 候选来自库里真有的牌子,不是拼出来的 |
| 名单里没有,库里有货的是这几个。0 根的不列 —— 列了等于把模型往一个空结果上引 |
四种情形的顺序不能反:brandAliases 允许自映射,标准名会出现在自己的别名列表里,
先判别名的话,传标准名会被答成「你填的是别名」,把人往错方向带。tests/ledger.test.mjs ㉙ 守这条。
判据只有一份,家在 lib/slots.mjs
判「两个写法是不是同一款货」的逻辑在 lib/slots.mjs,lib/ledger.mjs 直接 import。
2026-08-16 之前它不在这儿 —— 住在 userscripts/feishu-warehouse-composer/slots.mjs,
MCP 运行时按 export function 建槽位( 这个字符串标记切一段出来 new Function 跑。
那一整套(环境变量覆盖路径、切片标记、切不出来就抛)都是为了「判据只有一份、家在油猴那边」
这个约束才存在的。
约束没了,所以搬家:那个油猴脚本(飞书仓库组合器)不再更新 —— 飞书那侧 2026-08-11 起
改走 lark-cli 官方接口,逆向那条路退休了。依赖一个没人维护的路径,比多一份拷贝更危险:
拷贝会漂,但漂了有判据能抓;而路径哪天不在了,报出来的是「切不出建槽位」,
没人看得懂那是什么意思。油猴那边那份留着给它自己用,两边不再有同步关系 ——
它不更新了,也就不会漂。
判同款漂了不会报错,只表现为「库存翻倍」或「同一款货查不到」。实测过:
QSFP28-100G-SR4(多模)和 QSFP28-100G-LR4(单模),缺了「标准推光纤类型和波长」
那一步就判成同款,发过去插上不亮。所以它值得有一份自己的家,而不是寄人篱下。
run-tests.sh 那条检查跟着反了方向 —— 原来查「必须去读油猴那份」,现在查三件事:
判据文件在且是本仓库的、lib/ledger.mjs import 的是它、lib/slots.mjs 之外没有第二份
槽位词表字面量。查的是依赖机制不是「提到」:第一版写成 grep 路径字符串,
把解释这段历史的注释也报红了 —— 判据写太宽和写太窄一样坏。
品牌对照表还在外面,家是 plans.json 的 brandAliases(油猴「品牌对照表」界面改的
那份,INVENTORY_PLANS 可覆盖)。这边只读、不留副本,读不到就抛;同一个写法在两个方案里
映射到不同品牌也直接抛,不静默取一个。它和槽位判据的区别是:那份还有人在用界面改,
所以它该留在被改的地方,而不是搬进来变成第二份。
跑测试
./run-tests.sh # 改的过程中跑:23 秒,305 条判据 + 61 个消融,不打网络
./run-tests.sh 全 # 收尾 / 提交前跑一次:89~190 秒(缓存热时 89),打网络的五条链并行
./新旧.sh # 跑着的那个 MCP 是不是最新代码 —— 它是 WorkBuddy 启动时拉起的单例,新开对话不换进程
./自检.mjs # 这套东西能不能跑起来:lark-cli / 登录态 / 方案包 / 真读源表 / 台账 / 三件静态检查
./自检.mjs --快 # 同上,跳过真读源表那步(不打网络)分两档是量出来的:打网络的 5 个测试加它们的消融占全套 635 秒里的 630 秒, 而 15 个不打网络的测试加 14 组消融一共 5 秒。改一行代码等十分钟才知道有没有改坏, 这个循环长到没人会在改的过程中跑它 —— 于是它只在收尾跑一次,而那正是最晚发现问题的时刻。
全 那档五条链并行(2026-08-17 实测 624 秒 → 195 秒,判据和消融一条不少)。
五个正跑单独测过:串行 275 秒、并行 92 秒,一个都没被频控挂 —— 当天做的退避重试兜住了突发。
写路径 那条链必须串着,而且不许和自己并行:它往生产 base 建同名前缀的表、
finally 按前缀清理,两个进程同时跑会互删对方的表,而失败方式是「表突然不见了」,
看起来像接口出错。它整条链(正跑 + 消融)在一个子进程里顺序跑,跟别的链并行。
输出按固定顺序印、不按完成顺序 —— 两次跑的输出要能直接 diff。 每条链的秒数也收回来(并行之后它们从秒表里消失过一次,而「量不到就没法再优化」 正是这一档存在的理由:624→195 就是照着秒表找出来的)。
默认那档末尾会明说没验哪几项、它们各守什么。退出码照旧 0(它确实通过了它跑的那些), 但只跑了一半不许印得像全跑了:「判不了」和「通过」合并成一个无差别的绿, 是这套东西最不能出的那种错。
shell 脚本交给 shellcheck,不自己写正则(brew install shellcheck,没装就报红——静默跳过等于永远通过)。它认得出这个仓库真栽过的那种:计时="" → SC2276 This is interpreted as a command name containing '=',bash 会把它当命令执行、报 command not found 然后接着往下跑,变量恒空而脚本照常绿着跑完(bash -n 查不出来,语法合法)。2026-08-17 一天栽了五次。
接的时候防住第二件事:解析中途停了不许算数。 中文函数名(秒() {)bash 认、shellcheck 不认,它在那一行报 SC1088 停止解析、照样退非 0——看起来在干活,实际 260 行只扫了 28 行,而前面那几条 appears unused 全是假的(它没看到用的地方)。所以 run-tests.sh 里的函数名是 sec / run_one / teeth / chain 而不是中文:中文函数名合法,但它把这个仓库唯一的 shell linter 挡在门外。
判据编号是跑出来的(ok #37 …),不是手编的圈字符 —— ㊱–㊿ 在 90 条判据那儿早用完了,手编到后来必然重号,FAIL ㊹ 分不清是哪一条。加判据不用维护编号。
消融个数是数出来的,不是写在 run-tests.sh 里的(tests/消融.mjs)。每个测试文件用
认消融(N) 声明自己有几个,套件从 1 一直试到测试回退出码 2 为止。
退出码四档,tests/消融.mjs 的文件头是唯一一份定义,缺哪一档就分不出对应的两种情况:
码 | 什么意思 | 缺了它会把什么误当成什么 |
0 | 全绿(消融时 = 这个消融是恒绿的,必须报) | — |
1 | 有判据挂了 | — |
2 | 没有这个消融号,停 | 「压根没这个消融」当成「消融有效」 |
3 | 挂了,但有一条是撞飞书频控 → 这轮没验成 | 「网络那分钟太忙」当成「代码坏了」;消融那圈里更糟,一个根本没被执行到的消融被数成一颗牙 |
3 不是通过。 它让整套退非 0、要求重跑一遍 —— 所以就算某轮里混了真失败被一起标成 ⏸, 干净的那一遍照样会把它印红,最坏是晚一个来回看到,不是看不到。
这条是踩出来的:分段读 加了第 3 个消融、run-tests.sh 里还写着 for m in 1 2,
那个消融从来没跑过,而汇总栏照印「✓ 2/2 消融都变红」 —— 一条新判据加进去了、
从没被验证过会红,而整套测试全绿。顺带堵上另一个:决定.test.mjs 原来对认不出的消融号
什么都不做(if…else if 没有 else),ABLATE=3 既没跑正常路径也没跑消融路径,
四条判据一起挂,从退出码看和「消融 3 起作用了」一模一样。
测试 | 打不打网络 | 守什么 |
| 不打 | 归一 + 坏件排除 + 字面规范化 + 缺表降级 + 品牌补充 + 型号写法不像这一类(第五道检查)+ 品牌传错时说一句。33 条判据 |
| 不打 | 直读那条路。60 条判据:URL 解析、方案包认不出 URL 要抛、位置取「地区」不取「库房」、一条 SN 一根、跨方案剔重、聚合不守恒要抛、结构缓存校验、缓存段长只许比当前策略小、lark-cli 两条报错路归一、SN 逐单对比、筛选命中率、撞频控从认出来到套件退出码整条链(含「不是限流的失败一次都不重试」和「 |
| 不打 | 找替代 + 放宽代价。59 条判据:死路上摆「你是不是想找」(㊺㊻㊼,2026-08-24 加)—— 客户在厂商料号后面缀项目名( |
| 不打 | 周更:键校验、周差异三类、待补清单和快照对齐、改名解释(键级噪音摘不掉就变红)、「键归零」和「台账会悬空」分开(拿原始写法的消失去断言归一后的键会悬空,每周误报一次)。20 条判据 |
| 不打 | 看源表。13 条判据:列名不一样的必须点名、缺字段的表要点名、空映射当「没有」不当「叫空」、筛选规则和表头行原样给、名单截了要说还剩几张 |
| 不打 | 口径块瘦身。7 条判据:调试字段默认不发 / |
| 不打 | 查SN。19 条判据:多种原始写法收回同一个键、 |
| 不打 | 资源 / 提示模板 / 参数补全,外加 |
| 不打 |
|
| 不打 | 召回率回归。189 个真型号 × 9 种客户写法扰动,跑的是 |
| 不打 | 人拍过的决定。17 条判据:依据/谁拍的不许空、跑两遍状态一样、方向和写法都归一之后判重、改口留上一版、拍「不能顶」的不许消失、坏文件要抛但不许拖垮查询 |
| 不打 | 收尾那一行。10 条判据:一处不写数量、多处各自带数量、用的处数是最少的、凑不够不写「满足」也不逐处铺开、没给「要几根」不许下结论、按可用量凑、次序确定 |
| 不打 | 匹配完拼好通知文本、按收件人分段返回(不真发)。9 条判据:分满足/缺货/待定(命中键区分「匹配上但不够」和「没对上库存」、带机房)、资管段+采购段(缺货/待定两段、只有待定时不出缺货段)、归一型号和 normalize 字面指纹同口径(分隔符变体不漏配) |
| 不打 | 日更 MCP 工具的两步闸。3 条判据:第②步不写不执行、重放已消费票/伪造票都票据无效(①和真写要读写飞书台账,离线测不了,靠 日更.test.mjs 的算红 + 真站手验) |
| 不打 | 周更 MCP 工具的两步闸 + SN 快照够不够新。10 条判据:第②步不写不执行、重放/伪造票都票据无效;以及 |
| 不打 | CSV 转义 / BOM / 桌面和缓存两个去处分开 / 旧文件按时间戳淘汰(按文件名排会让上周的 |
| 不打 | 静态扫每个 |
各自的消融 | 三个打(分段读 / 增量 / 写路径) | 每个拿掉一处承重逻辑,都必须变红,否则断言是恒绿的。个数这里不写 —— 写死过一次就漂过一次,跑一遍看汇总栏那行 |
| 打 | 超 10 MB 的表分段读 + 撞了 |
| 打 | 增量重读。6 条判据,只有一条要紧:增量结果必须和全量逐个物料键一模一样(不能拿「守恒过了」当判据 —— 复用了一本其实变了的文档,总数少一截而每步守恒照样绿)。造场景靠 |
| 不打 |
|
| 不打 | README 里写死的数字有没有漂。3 条判据:每个不打网络的测试表里都有一行、写的判据数和跑出来的一致、仓库根每个入口脚本文档里都提到了。加它是因为一次审计发现两个新测试压根没进表格 —— 这类漂移不报错,只会让文档变成一份看着像真的、对不上的东西 |
| 不打 |
|
| 不打 | 记下模型实际问了什么。9 条判据:参数原样记不补默认值、失败也记(命中 0 和「压根跑不通」是两类问题)、不许拖慢查询、按月切只留最近几个月、关得掉 |
| 不打 | 键锁死 + 台账范围 + 占用的两个方向 + 源表整批改写法。27 条判据:标准写法翻盘要沿用老键、跨库房不沿用、合成一组要报冲突;「该导没导」和「不走台账领用」分开计数(源表有、台账没有);「台账有占用、源表没这个键」也要报(在途落进去的地方,漏了就是超发),日期要跟着每一条走、没填不许编; |
| 不打 | 「按 SN 校准型号」那份缓存旧了要能看出来( |
| 不打 | 主路( |
| 不打 | 展示层( |
| 不打 | 给模型看的那份契约(名字/说明/参数表/annotations)的快照 + 不变量。回归里唯一真起一次服务器的地方 —— 2026-08-22 把工具表搬进 |
| 不打 | 依赖方向交给 linter 强制( |
| 不打 | 死代码棘轮, |
| 不打 | 找文件不许从「代码放在哪」推( |
| 不打 | 握手时那一句协议版本谁说了算。12 条判据:客户端报的版本我方支持就原样回,不支持就回我方最新、不许回声(2026-08-22 修的真 bug:原来无条件把对面报的版本回过去,于是对面的兼容性检查「你回的和我要的一样吗」永远成立、永远发现不了不兼容 —— 这是典型的失效时是绿的);乱字符串、未来版本、空串、干脆不报,四种都归到我方最新;最后一条验真起到了服务器并拿到了回复(拿到 null 的话上面十一条全不作数) |
| 不打 | 「离上次写台账新进来了什么」+ SN 证据( |
| 不打 | 判改名那道闸,日更周更共用一份( |
| 不打 |
|
| 打 | 只读问到的那类资产的表 + 在途去重。11 条判据,最要紧的一条是只读一类时一个基线都不许滚(半份数据覆盖全仓 SN 快照,下次全量会把没读的二十几万根全算成「多了」,而每步守恒照样绿)。另外守着:口径要自报「只读了部分源表」、行缓存叠加不覆盖、「全部」那份缓存能答窄问题、全量要滚快照(和前一条互为反证)。消融靠两个逃生开关注入( |
| 打,而且真写 | 写多维表格那两条命令的契约。真建一张一次性的表( |
| 打 | initialize / tools/list / tools/call 这条链能不能通,外加进度通知(带没带 progressToken 的两种行为互为反证)、查SN 走文件和走内联两条路、老参数名要当场报错、记下决定走真的写盘(指到临时文件)。99 条判据 |
消融不是摆设,抓到过三次:brandMap 原来在模块加载时建成常量,测试改 BRAND 影响不到它,消融 1 是恒绿的;直读那边第一版消融把对应判据自己豁免了(ABLATE === '1' ? true : ...),三个消融里两个不变红;字面规范化那条消融第一版样本选错——拿 CX7-400G单口 vs Cx7 400G 单口 当样本,槽位解析器自己就能合,字面归一对它不承重,换成 128g(小写 g 解析不出容量)和 SFP.25G-SR.LC(点号解析不出规格)才真正只走字面这条路。
「消融不红」有两种原因,第二种更隐蔽:判据豁免了自己,或者样本根本不经过那条路径。
演一遍撞频控
INVENTORY_FAKE_RATELIMIT=99999 INVENTORY_BACKOFF_MS=1,1 ./run-tests.sh # 该印 ⏸,退 3
./run-tests.sh # 该全绿,退 0反着跑这一遍不是走过场,2026-08-16 一次就抓出四个真问题,三个是同一天刚写的那套频控机制自己的:
收尾()原来要求「挂的每一条都是频控」才算没验成 —— 而分段读挂 5 条只有 1 条带码, 其余是拿不到数据之后的连锁反应,于是专门为频控做的机制在真频控面前不生效。direct.test.mjs里验退出码的子进程直接{...process.env}传下去, 把注入用的变量也带了进去 —— 一个不打网络的测试因为别处的注入而变红,测试的沙盒漏了。protocol.test.mjs十四处裸JSON.parse(…content[0].text),工具回人话时抛的是Unexpected token '查', "查不了:wiki 换"…—— 原文只剩前十个字,而code=99991400在第 40 个字上。写路径.test.mjs自己那份cli()是裸pexec,lark-cli 非 0 退出时错误体在 stdout 里、 而那个对象根本没被读,报错只剩一句Command failed:。它要真写生产 base, 恰恰是最容易撞上限额的那条。
四个的共同点:都是"失效时是绿的/黄的",而且都只有把场景真造出来才看得见。
还没做的
登记占用没做(写「占用记录」表
tbl8GkzD4stgYcay)。物料表的写入已经做了(./导台账.mjs), 源表一律只读。三条硬规矩先落地:agent 不能填「核验人」、写带请求 ID 且写前查重、 写前重读发现已被占就中止。这张表 2026-08-15 探过一遍,四件事和原来记的不一样:① 占用和出库是同一张表的两种「类型」,不是两张表。靠
带符号数量加减(占用+数量、 出库-数量),剩余占用是公式算的净额。出库那行还要在关联工单里指回它冲销的那条占用行 (实测:29124 占用 +2 → 29154 出库 -2 指着它 → 剩余占用 0、状态 ⚪已结清)。② 物料已经是 link 字段(
关联物料),不是 text。本行物料键是从它算出来的公式, 所以人是从下拉里选物料、不是手打 40 个字符 —— 原来记的「现在是 text」已经过期。③「link 走 API 会静默丢数据」不成立(2026-08-15 实测写了一条再删掉):
+record-batch-create传关联物料: [{"id":"rec..."}]写得进去,回读拿到的 link 值一字不差,而且公式本行物料键真的算出了光模块|海光芯创|QSFP112-400G-DR4-SM1310|闵行—— 关联是活的,不是存了个空壳。 形状写错时接口是显式报错(800010701 Cell value does not match any supported shape), 还在 hint 里给出正确形状,不是悄悄吞掉。④ 但返回体不给 record_id:
+record-batch-create回ok:true而records是空数组, 从返回值看不出写没写、写成了什么。所以「写完必须回读校验、不认返回值」那条规矩在这张表上 不是保险是必需。删记录要--yes。写之前的自检有现成的:这张表有个
数据检查公式字段,把规则全写在里面了 —— 没填类型 / 没选物料(「这笔占用谁都扣不到」)/ 数量要大于 0 / 同一工单同一物料写了多行 (「剩余占用会偏小」)/ 冲销超过占用量 / 缺日期 / 缺成本归属 / 出库没选关联工单 / 出库未填闭环状态。写完回读这个字段就知道对不对,别在代码里把规则再实现一遍。物料键的第三段还是型号字符串,只做了字面层归一。纯打字差异(大小写/分隔符)已经挡住了, 但语义相同、字面不同的仍会各成一个键。根治是把键换成槽位序列化、型号字符串降级成显示名。 实测 26 个合并组里有 9 组的标准写法是靠根数决出来的(
CX6-25G双口(287)vsCX6-25G*2(158), 差 129 根,一次出库就能翻盘),翻了之后周对比会报出假的「消失 + 新增」。网卡不做规则替代(2026-08-15 定,不是待办)。
方向.网卡三项全是null,所以它的 规则档必然是空的 —— 能不能拿 CX6 顶 CX5、双口能不能顶单口是硬件常识,槽位表算不出来, 拍这张表的成本高于收益。空数组必须带解释:不做规则替代那张表命中时返回值里明说 「这类不做规则替代 + 为什么」,和「⚠ 这类的替代方向还没定」互斥 —— 后者读起来是个待办, 会让人一直等一个不会来的东西。网卡照常给精确匹配和相近分层。要开做就删掉那一项、 把方向填上,两处一起动(lib/substitute.mjs,判据 ㉟㊱㊲ + 消融 9)。配额不是障碍,已查清。飞书没有「月度额度」这回事 —— 官方
frequency-control页给的是按 API × 应用 × 租户的每分钟/每秒速率,最窄一档 100 次/分,基础版和商业版的表几乎一样。触发了回 HTTP 429 +code 99991400,响应头x-ogw-ratelimit-reset直接告诉你等几秒。但直读这条路够得着:31 张源表,一次冷启动约 48 次(30 张各一趟 + 闵行光模块那张 1 次元数据 + 17 段),空缓存再多一趟表头。一分钟内连开两次冷启动就撞线,撞上后从 8 秒掉到 1921 秒 —— 这条不是理论,2026-08-14 排查读表慢的时候踩到过,当时误以为是数据量。 热态只有 9 次(查 9 本文档的 revision),台账那条路 12 次。三个人各装一份、各自零星查够不着, 但别在一分钟内连跑两次全量。段数越细调用次数越多,改每段格子之前先算这笔账。
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- FlicenseNot gradedqualityNot gradedmaintenanceConnects to Odoo ERP to provide comprehensive inventory analysis, including demand forecasting, ABC/XYZ classification, and stock level monitoring. It enables users to identify slow-moving items and generate turnover or aging reports through natural language queries.
- FlicenseNot gradedqualityCmaintenanceEnables AI assistants to query and operate Dingjie ERP system via natural language, covering procurement, sales, and material management.1
- FlicenseNot gradedqualityDmaintenanceEnables Claude Desktop to manage office inventory through natural language, including adding items, issuing stock, checking availability, and low-stock alerts.
- FlicenseNot gradedqualityDmaintenanceEnables querying and managing Alegra inventory and products through natural language, including stock checks and product searches.
Related MCP Connectors
Manage your Savanto store from your AI: catalog, content, prompts, and analytics, by chat.
Agent 知识共享市场 — 让 AI Agent 搜索/购买/上传经验记忆。34 个 Tools,支持记忆搜索、购买、上传、评价、团队协作。
Agent-native product catalog for AI shopping agents. 296M+ products, 28 countries.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/320432893-cell/inventory-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server