15 KiB
架构
数据路径
用户 ─ Messenger ─ Agent ─ stdio 轻量桥 ─ HTTPS/OAuth ─ 统一审批网关
├─ 钉钉 OA
└─ 薪福通
本地桥只完成 MCP stdio 与远程 Streamable HTTP 的转换,保存用户自己的短期 access/refresh token,不包含企业密钥、不读取审批上游,也不监听端口。
客户端安装边界
终端不再使用全局 npm prefix。无第三方依赖的首次安装器通过自有 HTTPS 到达用户机器,随后使用固定 Ed25519 公钥验证 schema v3 清单签名,并校验每个 artifact 的 SHA-256。客户端运行依赖被内置在签名 artifact 中,终端使用 npm 离线模式展开,不在安装时引入清单以外的代码。首次安装以 HTTPS 和安装说明中固定的安装器/公钥哈希建立信任;本地固定公钥保护后续更新链路。发布私钥不进入 Git、网关进程或公开目录。
运行时位于稳定的用户数据目录,启动器与 Node/NVM 具体版本解耦。安装器只有一条通用路径,输出稳定 bridgePath、通用 skillPath、标准 mcpConfig 和机器可读 postInstall,不识别 Agent 品牌,也不写任何 Agent 的私有配置。postInstall 通过已安装的 Bridge 和正式网关主动检查登录;它只返回认证状态、短期授权链接与下一步工具提示,不返回 Token 或 device secret。网关不可用只会把状态标为 unavailable,不会回滚已验证的客户端安装。
连接器信任和重启属于 Agent 自身的安全边界,安装器不会绕过。卸载默认删除运行时、Skill、安装记录和 OAuth 会话;Agent 拥有的连接器条目由其官方入口移除。旧 npm 入口只在名称和目标包都精确匹配时清理。
Agent 领域接口
Agent 面向统一审批语义,不直接理解钉钉 processInstanceId/taskId、薪福通员工号、上游响应或完整 Schema。网关在内部完成来源路由、字段抽取、动作调用和写后复核,只返回审批判断所需的精简字段与统一状态。
所有工具以普通 content.text 中的精简 JSON 作为跨宿主可见结果,同时保留相同的精简 structuredContent 供支持结构化结果的宿主使用;两处都不携带诊断字段或上游原始响应。raw 仍是受过滤、分页和硬限额保护的按需诊断视图,不是默认数据通道。
单张或部分操作使用 v4 紧凑无状态 approvalId。载荷只包含来源、上游标识、用途和 30 分钟有效期,不包含标题、申请人、金额、详情或用户 ID;网关以当前 OAuth 用户派生专属 AES-GCM 密钥,因此同一服务器密钥下其他用户也无法解开。常见薪福通引用约 60 字符,钉钉 OA 引用约 100 字符;长度头可以把宿主截断与普通篡改区分开。待办引用允许进入实时写前校验;approval_list_history 签发的 handled/submitted 引用只能读取详情,动作查询和批处理会在首次上游写入前拒绝。网关继续接受尚未过期的 v2/v3 待办引用,升级或进程重启不会让同一密钥签发的引用失效。
历史列表不与待办混合,也不签发 selectionId。未指定时间时默认查询最近 30 天;startDate/endDate 接受按 Asia/Shanghai 解释的 YYYY-MM-DD 或带时区的 ISO 8601 时间,结束日期包含全天,单次跨度最多 120 天。钉钉 handled 从本人已完成工作记录取得,submitted 在该时间窗口内按当前用户可见模板查询本人发起实例;薪福通 handled 从本人的已处理任务取得,submitted 使用服务器派生的本人员工号查询单据,再由网关统一按时间窗口过滤。读取历史详情时再次验证当前用户确实处理过该任务或确实是发起人;审批正文、费用明细和附件仍只实时读取、不落库。
发起费用审批时,approval_prepare_submission 在网关内部完成跨来源发票查重。它从待提交表单和本人近 30 天 submitted 历史详情中有界提取发票代码/号码、数电票唯一编号、票据号码和附件哈希;金额、日期、商户三项组合仅作为弱匹配。首次索引只读取近 30 天尚未索引的本人历史详情,后续准备仍实时刷新两端精简历史列表和状态,但只为新增业务单补读详情,不会重复逐张下载已索引发票。任一来源异常、历史被上游截断或详情读取失败都会把覆盖标记为不完整,不会静默返回“无重复”。
发票指纹以服务器密钥进行 HMAC,另以用户 HMAC 分区。SQLite 只保存用户 HMAC、发票 HMAC、匹配类型、来源、上游业务单号、规范化状态和时间;不保存发票原号、审批正文、费用详情、商户、附件或上游 Schema。指纹、最小审批索引和非活跃扫描水位最多保留 35 天,网关启动及每次同步时自动清理,删除审批索引会级联删除全部关联指纹。活跃确认还为每个发票 HMAC 建立 10 分钟唯一预留,到期自动删除,因此两个 Agent 会话不能同时为同一用户、同一发票取得可提交确认。历史重复或覆盖不完整只能在用户明确确认后继续,该决定和预留均绑定到一次性 confirmationId。
每次 approval_process_batch 最多接收 20 条。整组同意或拒绝可使用列表返回的 selectionId:它绑定当前用户、查询条件、返回数量和审批集合指纹,执行前网关会重新实时查询;列表有任何变化、任一来源异常或超过上限都会拒绝写入。超过 20 条时不签发 batchPlan,Agent 可在用户授权范围内从同一次原始列表中按 approvalId 分组,不能重新查询后混入新待办。退回因每张目标节点不同,仍必须逐张取得 targetId。
显式 items 写入采用两阶段安全边界:先解码并校验整批 approvalId,同时校验所有退回 targetId 的签名、有效期、用户、来源和任务匹配;任一引用失败即返回 preflight_failed、writesStarted=false,整批零写入。全部引用通过后才逐张做实时本人待办校验和上游写入。开始写入后的业务结果仍是非事务逐项结果,不会自动补偿或重试;引用只在批次开始时检查一次,避免一个已通过预检的批次在执行中途因引用到期而产生人为部分失败。
同意和拒绝不要求 Agent 对每张待办重复调用动作查询。列表已经标明候选动作,网关会在写操作前逐张验证本人待办;allowed=true, guaranteed=false 表示任务状态预检通过,但钉钉签字等最终业务规则仍可能在执行时拒绝。bridge 最多允许 3 个远程工具调用同时在途,避免宿主瞬间并发压垮上游。
一次登录
安装器在安装完成后立即调用 Bridge 的登录准备模式,不等待首次业务请求。未登录时,postInstall.authChallenge.directUrl 直接携带一个钉钉授权链接;用户授权后,Agent 调用 approval_login 确认状态。授权成功后,网关从钉钉身份得到当前企业 userId:
- 钉钉 OA 直接按该
userId查询本人审批任务; - 薪福通先把该
userId映射为企业成员,再从本人企业成员档案读取花名册员工号。
两条数据路径共用同一个网关 Token,因此不会出现本地 DWS 与网关分别登录的情况。
Bridge 的受保护会话中只记录 pending/delivered 一次性 onboarding 状态,不保存任何审批正文。登录挑战不会消费该状态;首次认证成功响应,或 Agent 跳过登录工具后的首个成功业务响应,会在普通 content.text 和 structuredContent.onboarding 中同时返回简短用法示例并标记已展示。错误响应、登录过期和后续升级都不会重复展示或自动执行示例。
远程 https://approval.justconn.top/mcp 已按 MCP 授权规范提供 HTTP 401 challenge、OAuth Protected Resource Metadata、Authorization Server Metadata、动态客户端注册、PKCE、Token 刷新和撤销;原生支持远程 MCP OAuth 的宿主可以直接接管浏览器授权。面向只支持 stdio 或不能承接 OAuth 回调的宿主,轻量桥使用 device-code 兼容流程。由于部分宿主(例如只展示 content.text 的客户端)会丢弃 structuredContent,桥会同时在普通文本、标准 resource_link 和 authChallenge.directUrl 中返回同一个短期登录链接。桥不会调用不存在的消息通道自动推送链接;Agent 必须把 URL 原样写入当前回复,不得只声称已生成或已推送,也不得读取本地会话文件提取它。
开放动态客户端注册不等于静默授权。授权码和设备码流程都会先进入网关 consent 页,展示客户端自报名称、回调 origin、权限范围和设备授权码;只有用户显式确认后才生成钉钉登录跳转,回调也会再次检查 consent 状态。OAuth 客户端总量限制为 10,000,180 天未使用且没有活跃授权工件的客户端会被清理;过期登录请求、授权码和 Token 每小时清理。钉钉用户 token 刷新按用户 single-flight,避免并发消费同一个 refresh token;撤销 refresh token 会级联撤销同客户端、同用户的授权。
每个 MCP 会话绑定 OAuth 用户,未知或过期会话返回 404,跨用户会话返回 403。会话空闲上限 2 小时、绝对上限 24 小时,每用户最多 8 个、网关总计最多 1,000 个,周期清理后才接受新会话。Nginx 的流读取超时略长于会话绝对上限,不会先于网关任意切断健康会话。
网关职责
网关只承担:
- OAuth 登录、Token 刷新与客户端会话;
- 钉钉 OA 待办、详情和审批 OpenAPI 调用;
- 钉钉 OA 模板、字段能力检查与审批发起;
- 薪福通用户映射、本人员工号查询、请求签名、待办、详情、处理与审批发起;
- 每次写入前后的本人待办校验;
- 签发绑定用户、来源和有效期的不透明引用;
- 实时同步本人提单历史并维护不可逆的最小发票查重索引;
- 记录不含审批正文的最小审计事件。
网关不运行浏览器,不保存审批正文、费用明细或附件。SQLite 仅保存 OAuth 客户端、哈希 Token、必要的钉钉刷新凭证、最小审计数据,以及上述不可逆发票/用户 HMAC 和最小业务状态元数据。
越权防护
工具没有 userId 或薪福通员工号参数。待办详情和写操作只接受待办列表签发的短期 approvalId,并在执行前重新查询上游实时待办;历史详情只接受历史列表签发的只读引用,并重新验证本人已处理或本人发起关系。修改本地 Skill 或桥只能改变请求内容,不能伪造服务器认证身份、把历史引用升级为可写引用或绕过归属校验。
发起审批使用绑定当前用户、模板指纹、完整表单哈希、发票重复决定和并发预留的短期一次性 confirmationId。正式提交会重新读取模板,并拒绝内容变化、过期确认、重复提交、失效的发票预留以及 Agent 传入的身份、员工号或审批状态。钉钉的申请人和部门控件由网关通过当前登录用户的通讯录档案填写,显式部门必须属于本人;薪福通成员映射、企业成员档案和员工号任一不一致或缺失时都会安全拒绝。
钉钉提单成功后,网关签发绑定当前用户和新实例的短期 revokeId。撤销前重新读取实例,只有发起人仍是当前登录用户且实例仍在运行中才调用官方 terminate 接口;写后再次读取状态。已接受但状态尚未变化时返回 outcome_uncertain,禁止自动重试。
薪福通创建接口返回单号后,网关实时读取本人单据状态。billSts=1(待审批) 返回 submitted 并签发 revokeId;billSts=2(审批通过) 表示流程已按企业配置即时完成,返回 completed,不签发撤回引用,也禁止重复提交。其他与请求不一致的终态返回 upstream_state_mismatch。撤回时网关重新完成钉钉用户到薪福通成员的唯一映射,从本人成员档案读取员工号,再调用 itrip-gateway/v1/approve/recall;Agent 不能提供或替换 empId、单据号。
撤回接口若返回 XFTSYS* 服务异常或网络级异常,网关会立即只读复核本人单据状态:已撤回则返回 revoked,仍为待审批则返回带 postWriteVerified 的 failed,无法明确归类才返回 outcome_uncertain。任何分支都不会自动重试写操作。
截至 1.1.0-rc.8,薪福通撤回仍是实验能力:当前企业真实调用 approve/recall 会返回 XFTSYS9999,写后复核确认单据仍为待审批,尚无成功样本。该限制不影响薪福通查询、审批处理或发起审批;正式版不得把“接口已接通”表述为“撤回已验证成功”。
薪福通创建或撤回需要复核时,approval_get_submission_status 先用服务器派生的本人员工号和单据详情双重确认目标属于当前用户的已发起单据,再返回状态名称和精简流程历史。只有实时状态仍为 1(待审批) 才签发新的短期、用户绑定 revokeId;已通过、草稿、拒绝或已撤回单据都不会获得撤回引用。该工具不返回审批正文,也不触发写操作。
网关只签发薪福通已支持模板,以及 6 个钉钉模板的提单引用:出差申请单_北极大、外出_北极大、业务招待申请_北极大、差旅费用报销_捷康、业务招待费用报销_捷康、业务招待申请_捷康。分类和模板名必须同时匹配。获取 Schema、准备和正式提交都会重新校验范围,因此修改本地 Skill、请求同分类的其他模板或复用旧引用都不能越过白名单。
能力边界
普通钉钉 OA 的读取、同意和拒绝通过官方 OpenAPI 完成。要求手写签名或其他客户端交互的模板不会伪造操作,而是返回钉钉客户端链接。退回能力仅在上游能返回可靠目标节点时开放,否则明确标记为不可用。
钉钉企业 OpenAPI 与 DWS 的个人接口不是同一能力面,但两者都支持无副作用的流程预测。网关在准备提单时调用企业接口 POST /v1.0/workflow/processes/forecast,发起人身份仍由服务器从已验证 OAuth 用户派生;只有预测成功并补齐全部自选处理人后才签发提交确认。预测请求不会创建审批实例,也不会持久化表单正文。包含 DDBizSuite 的考勤/费控模板不会把套件子字段伪装成普通字段提交。业务招待申请_北极大虽由普通字段和明细表组成,但真实测试中新版企业创建接口返回系统异常、旧版企业接口拒绝组件格式。当前 6 个白名单钉钉模板因此全部标记为 client_only,必须在客户端发起。
薪福通 Schema 会组合正式创建接口字段与企业实时自定义字段。准备阶段递归校验报销金额、费用明细、表格自定义字段、企业必填字段、当前用户成员映射与员工号。人员字段使用花名册编号数组,部门字段使用组织编码数组,表格字段使用带 sequence、columnValues 和 titleCode 的 JSON 行结构。薪福通没有无副作用的流程预测接口,因此所有开放的创建接口都在写后实时核验结果;申请类流程若按企业配置即时完成,会明确返回 completed,而不是误报异常。