feat: initial public release
CI / Client installer (macos-latest) (push) Canceled after 0s
CI / Client installer (ubuntu-latest) (push) Canceled after 0s
CI / Client installer (windows-latest) (push) Canceled after 0s
CI / test (22) (push) Canceled after 0s
CI / test (24) (push) Canceled after 0s
CI / release-smoke (push) Canceled after 0s
CI / Client installer (macos-latest) (push) Canceled after 0s
CI / Client installer (ubuntu-latest) (push) Canceled after 0s
CI / Client installer (windows-latest) (push) Canceled after 0s
CI / test (22) (push) Canceled after 0s
CI / test (24) (push) Canceled after 0s
CI / release-smoke (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
# 架构
|
||||
|
||||
## 数据路径
|
||||
|
||||
```text
|
||||
用户 ─ 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`,而不是误报异常。
|
||||
@@ -0,0 +1,150 @@
|
||||
# 部署与分发
|
||||
|
||||
## 服务器形态
|
||||
|
||||
统一网关是一个小型 Node.js 服务,监听 `127.0.0.1:3010`,由 Nginx 提供 HTTPS。它只调用钉钉与薪福通 API,不运行浏览器,通常 1 核 CPU、512 MB–1 GB 内存即可。
|
||||
|
||||
需要:
|
||||
|
||||
- Node.js 22.13+;
|
||||
- Nginx、Certbot 和固定域名;
|
||||
- 一个专用钉钉企业内部应用;
|
||||
- 薪福通正式 OpenAPI 应用。
|
||||
|
||||
钉钉应用 OAuth 回调:
|
||||
|
||||
```text
|
||||
https://approval.justconn.top/oauth/dingtalk/callback
|
||||
```
|
||||
|
||||
钉钉应用需要用户登录/身份、通讯录个人信息读取(用于实时取得当前用户所属部门)、OA 审批待办与已处理工作记录、本人发起实例 ID、实例详情、模板 Schema、审批处理和发起实例所需权限。
|
||||
|
||||
薪福通除已有的用户映射、本人待办、单据详情、审批历史和 pass/reject/back 权限外,发起审批还需要一次性开通:
|
||||
|
||||
- `xft-member/openapi/xft-member/member/get/by-id`(查询企业成员详情,用于由当前登录身份安全取得本人花名册员工号);
|
||||
- `itrip-gateway/v1/form/query-event-list`;
|
||||
- `itrip-gateway/v1/form/customFields/eventSeqId/{eventSeqId}`;
|
||||
- `itrip-gateway/v1/approve/recall`(单据撤回);
|
||||
- `itrip-gateway/v1/bills/apply/travel/create`;
|
||||
- `itrip-gateway/v1/bills/apply/expense/create`;
|
||||
- `itrip-gateway/v1/bills/apply/entertainment/create`;
|
||||
- `itrip-gateway/v1/bills/apply/common/create`;
|
||||
- `itrip-gateway/v1/bills/apply/use-car/create`;
|
||||
- `itrip-gateway/v1/reimburse/bill/create`;
|
||||
- `itrip-gateway/v1/reimburse/bill/travel/create`。
|
||||
|
||||
统一历史查询还会使用已开通的 `itrip-gateway/v1/bills/queryBillByCondition`(本人发起单据)和 `itrip-gateway/v1/bills/bill-list`(精简列表信息)。两者必须保留权限校验;`empId` 只能由网关从当前登录用户的成员档案派生。
|
||||
|
||||
创建接口必须保持权限校验开启。网关统一提交 `APPRV_PEND`,并在写后实时读取本人单据状态:进入待审批时提供撤回引用;按企业流程规则即时通过时返回 `completed`,不提供撤回引用且禁止重复提交。
|
||||
|
||||
`1.1.0-rc.8` 中薪福通撤回仍标记为实验能力:权限和调用契约已接通,但当前企业真实请求返回 `XFTSYS9999`,尚未取得成功样本。网关会写后复核并禁止自动重试。面向试用用户时必须保留这一说明;若业务要求撤回必须稳定可用,应等待薪福通定位后再升级为正式能力。
|
||||
|
||||
## 首次凭证配置
|
||||
|
||||
把钉钉 AppKey/AppSecret 保存为权限 `0600` 的 `.secrets/dingtalk.json`,把薪福通 AuthoritySecret 保存为权限 `0600` 的密钥文件。然后运行一次:
|
||||
|
||||
```bash
|
||||
npm ci
|
||||
npm run build
|
||||
|
||||
node dist/src/admin.js service configure \
|
||||
--public-base-url https://approval.justconn.top \
|
||||
--corp-id YOUR_CORP_ID \
|
||||
--credentials-file .secrets/dingtalk.json \
|
||||
--xft-open-api-app-id YOUR_XFT_APP_ID \
|
||||
--xft-open-api-secret-file .secrets/xft-open-api.secret \
|
||||
--xft-open-api-enterprise-id YOUR_XFT_ENTERPRISE_ID \
|
||||
--port 3010
|
||||
```
|
||||
|
||||
不要把这些企业凭证发给终端用户。
|
||||
|
||||
`MCP_SESSION_SECRET_FILE` 与 `MCP_APPROVAL_REF_SECRET_FILE` 由管理命令生成,内容至少 32 字节且权限必须为 `0600`(或更严格)。手工配置环境变量时同样强制至少 32 字节;缺失时网关拒绝启动,不再临时生成会导致重启后引用失效的随机密钥。`MCP_PUBLIC_BASE_URL` 必须是无内嵌凭证、无额外路径/query/hash 的 HTTPS origin,端口范围为 1–65535。
|
||||
|
||||
## 一键安装和更新
|
||||
|
||||
首次发布还需要生成一对 Ed25519 发布密钥:
|
||||
|
||||
```bash
|
||||
npm run release:keygen
|
||||
```
|
||||
|
||||
私钥保存在 `.secrets/release-signing-private.pem`(`0600`),必须离线备份且禁止提交、复制到 Web 目录或提供给网关进程;公钥位于 `distribution/release-signing-public.pem` 并随客户端发布。密钥已经存在时脚本会拒绝覆盖,轮换必须另行设计交叉签名迁移,不能直接替换。
|
||||
|
||||
首次配置完成后,每次安装、升级或 Bug 修复统一执行:
|
||||
|
||||
```bash
|
||||
cd /path/to/dingMCP
|
||||
./deploy/install.sh
|
||||
```
|
||||
|
||||
脚本自动测试、构建、用 Ed25519 私钥签署 schema v3 清单、重启服务、原子发布静态文件、安装 Nginx 配置并验证公网健康检查、OAuth 元数据和安装链接。首次签证书时,临时 HTTP 配置只开放 ACME challenge,其余请求返回 403;不会在明文 HTTP 上代理 OAuth、Token 或 MCP。证书通过 Certbot webroot 模式签发,正式配置启用 HTTPS 跳转与不带 `includeSubDomains` 的 HSTS。已有证书会自动复用;签名私钥缺失、权限不是 `0600` 或与固定公钥不匹配时发行会失败。
|
||||
|
||||
发行物先复制到独立 staging 目录,再通过符号链接原子切换;客户端不会看到新清单配旧 artifact 的中间状态。服务器保留当前版本和最近两个旧版本用于排障,较老版本自动清理。`install.md`、清单、签名、安装器、Skill、校验文件和测试包均以 `no-cache` 提供;内容寻址的客户端 tgz 才使用 immutable 缓存。签名清单的 `createdAt` 超过 180 天时客户端拒绝安装,避免旧签名版本冻结攻击。
|
||||
|
||||
卸载:
|
||||
|
||||
```bash
|
||||
./deploy/uninstall.sh
|
||||
```
|
||||
|
||||
这是服务器卸载命令,不是终端用户插件卸载。它默认保留凭证、OAuth 数据库和 Let's Encrypt 证书,重新安装无需重新录入企业密钥。自动化环境可使用 `./deploy/uninstall.sh --yes`。
|
||||
|
||||
## 用户分发
|
||||
|
||||
部署脚本会生成:
|
||||
|
||||
```text
|
||||
release/
|
||||
├── install.md
|
||||
├── manifest.json
|
||||
├── manifest.sig
|
||||
├── SHA256SUMS
|
||||
├── dingtalk-approval-installer.mjs
|
||||
├── release-signing-public.pem
|
||||
├── dingtalk-approval-client-*.tgz
|
||||
├── dingtalk-approval-SKILL.md
|
||||
├── dingmcp-test-bundle.tar.gz
|
||||
└── agent-test-install.md
|
||||
```
|
||||
|
||||
终端用户只需要把下面链接发给自己的 Agent:
|
||||
|
||||
```text
|
||||
https://approval.justconn.top/downloads/dingmcp/install.md
|
||||
```
|
||||
|
||||
Agent 只需校验安装器和公钥 SHA-256,再调用原生安装器。安装器验证 `manifest.sig` 和全部 artifact 哈希,将内置运行依赖的客户端以 npm 离线模式安装到稳定用户目录并返回标准 MCP 配置;终端安装过程不会从 npm registry 下载额外代码。Bridge 的稳定绝对路径不会包含 NVM 的具体 Node 版本,配置也不再携带 `--url`;网关地址由受签名保护的安装状态提供。
|
||||
|
||||
安装器对每个下载设置 30 秒超时,并在流式读取时执行 25 MB 硬上限,不会先把无界响应完整缓冲到内存。Windows 必须能定位由当前 Node 安全启动的 `npm-cli.js`;不会退回到受 Node 安全加固影响的 `npm.cmd` 隐式执行。
|
||||
|
||||
安装器不会识别 Agent 品牌或直接修改其配置。所有 Agent 都使用返回的 `skillPath` 和标准 `mcpConfig`,按自身官方方式安装 Skill、注册并信任名为 `dingtalk-approval` 的本地 stdio MCP;不能热加载连接器时再新建会话或重启。
|
||||
|
||||
安装器在返回前通过已安装 Bridge 主动准备登录,并在 JSON 的 `postInstall` 中返回 `authenticated`、`authorization_required` 或 `unavailable`。Agent 必须立即显示其中的短期 `directUrl`,不得等待首次审批请求;网页授权后调用 `approval_login`,再按 `verifyTool` 用 `source=all` 做双来源只读验证。首次认证成功会返回一次性 onboarding 示例,普通文本宿主也可见。登录准备失败不回滚安装,且整个流程不提交 consent、不读取审批、不执行写操作。
|
||||
|
||||
安装后可用稳定 CLI 自检或提取配置:`print-path bridge` 默认只输出路径,`print-config --format mcp-json` 输出可直接消费的标准 MCP JSON,`doctor --json` 同时检查 Node、npm、运行时、Skill、公钥和网关配置。后续更新直接执行安装结果中 `cliPath` 对应的 `update --json`,它会复用已固定的签名清单地址和本地公钥,并保留 OAuth 会话。更新完成后应重新连接该 MCP;Agent 不能单独重启连接器时,再新建会话或重启。
|
||||
|
||||
客户端卸载使用安装结果返回的 `cliPath uninstall --yes --json`,默认删除 OAuth 会话,并安全清理由旧版手工删除留下的四个 npm 入口;只会删除名称精确匹配且目标属于 `dingtalk-approval-mcp` 的链接或 Windows shim。
|
||||
|
||||
## 运维检查
|
||||
|
||||
```bash
|
||||
systemctl --user status dingtalk-approval-mcp.service
|
||||
curl --fail http://127.0.0.1:3010/healthz
|
||||
curl --fail https://approval.justconn.top/healthz
|
||||
curl --fail https://approval.justconn.top/downloads/dingmcp/manifest.json
|
||||
curl --fail https://approval.justconn.top/downloads/dingmcp/manifest.sig
|
||||
```
|
||||
|
||||
备份时必须把 `.runtime/auth.sqlite`、同路径派生的 `.runtime/auth.sqlite.invoice-dedup.sqlite` 与 `.secrets/oauth-session.key` 一并迁移。发票查重库只含不可逆 HMAC 和最小状态元数据;审批正文不落库,不需要备份。
|
||||
|
||||
每个候选版和正式版发布前执行 [发布检查清单](release-checklist.md)。
|
||||
|
||||
只读 E2E 脚本没有生产默认地址,也不会自动挑选第一张审批:
|
||||
|
||||
```bash
|
||||
DINGTALK_APPROVAL_MCP_URL=https://staging.example.com/mcp \
|
||||
node scripts/e2e-approval-mcp.mjs dingtalk
|
||||
```
|
||||
|
||||
读取指定单据时加 `--business-no EXACT`。只有已取得真实写入授权时,才可再加 `--execute-real-write`;脚本要求精确业务编号,并在写后重新列待办复核。任何 `outcome_uncertain` 均不得重试。
|
||||
@@ -0,0 +1,25 @@
|
||||
# 发布检查清单
|
||||
|
||||
## 每个候选版必须完成
|
||||
|
||||
- 当前提交位于 `main`,工作区除明确忽略的本地缓存外干净;版本号、插件版本和 Git 标签一致。
|
||||
- `npm ci`、`npm test`、`npm audit --omit=dev --audit-level=high` 全部通过。
|
||||
- `release:build`、`test-bundle:build` 和 `release:verify` 成功,`SHA256SUMS` 校验通过,发行物不包含 `.secrets`、`.runtime`、数据库、私钥或机器私有路径。
|
||||
- Nginx bootstrap 只开放 ACME challenge;正式站点启用 HSTS,mutable 发行文件返回 `no-cache`,客户端 tgz 才返回 immutable。连续两次部署时公开目录始终对应一个完整签名清单,旧版本保留数符合策略。
|
||||
- 从新生成的公开 `install.md` 在干净的用户级目录安装,不使用 `sudo`;安装结果主动返回 `postInstall`,可发现全部 13 个 MCP 工具。
|
||||
- 首次安装不等待业务查询即产生一条可传递链接;挑战阶段 onboarding 仍为 pending,授权后的首次成功响应在普通文本中展示一次示例,此后不重复。
|
||||
- 授权后钉钉 OA 与薪福通均为 `ok`,网关重启后登录状态仍可恢复;登录准备暂时失败不会回滚安装或删除原 OAuth 会话。
|
||||
- 只读验证待办、本人已处理历史、本人发起历史、历史默认最近 30 天及显式时间窗口、搜索、详情、费用结构化字段、动作、模板 Schema、部门/城市选项和越权拒绝;历史引用必须被动作查询和批处理拒绝。
|
||||
- 发票查重验证钉钉/XFT 跨来源精确匹配、弱匹配、用户隔离、状态更新、来源不可用、历史截断、详情失败、并发确认、35 天过期清理和网关重启恢复;数据库不得出现用户 ID、发票原号、审批正文或附件。重复风险和不完整覆盖未获用户确认时不得签发可提交 `confirmationId`。
|
||||
- 真实写操作只执行用户明确授权的样本;任何 `outcome_uncertain` 都停止,不自动重试。
|
||||
- Mock 契约确认网关和 bridge 的批量 Schema 均固定为 20;第 6 个截断引用及任一无效退回目标都会返回 `preflight_failed`、`writesStarted=false`,上游写调用数为 0。
|
||||
- 代表性 v4 引用长度满足钉钉 OA 不超过 128 字符、薪福通不超过 80 字符,并通过篡改、过期、跨用户、跨密钥、同密钥重启及 v2/v3 兼容测试。
|
||||
- 公网 `manifest.json` 的版本、生成时间和 SHA-256 与当前候选版一致。
|
||||
- E2E 脚本在未设置网关地址、未指定业务编号或未加真实写入开关时不会执行审批;只读运行不会自动选取待办。
|
||||
|
||||
## 正式版额外条件
|
||||
|
||||
- 钉钉和薪福通的同意至少各有一个近期真实成功样本,并完成写后复核。
|
||||
- 对外开放的拒绝、退回和发起模板具备对应的真实成功样本或明确的客户端/实验性降级说明。
|
||||
- 薪福通撤回取得真实成功样本;在此之前必须保留实验性标记,不得作为正式稳定能力承诺。
|
||||
- GitHub CI、外部健康检查、OAuth 数据库与密钥成对备份、升级回滚流程均已验证。
|
||||
@@ -0,0 +1,24 @@
|
||||
# 提单模板能力
|
||||
|
||||
以下结论来自 2026-08-12 通过本项目 MCP 实时读取的 10 个模板 Schema,以及对应正式接口文档。模板每次发起前仍会重新读取,本文不是可复用的静态 Schema。
|
||||
|
||||
| 来源 | 模板 | API 能力 | 关键字段或限制 |
|
||||
|---|---|---|---|
|
||||
| 薪福通 | 差旅报销单 | 支持,有约束 | 报销总额、币种、费用明细、差旅行程;费用和行程逐项递归校验,企业模板还要求承担部门 |
|
||||
| 薪福通 | 费用报销单 | 支持,有约束 | 报销总额、币种、费用明细;当前模板要求承担部门和费用说明 |
|
||||
| 薪福通 | 招待费用申请单 | 支持,有约束 | 企业模板类型为 `EXPENSE_APPLY`,使用费用申请创建接口的 `applyExpenses`(费用类型编码、金额等),而不是按模板名称误用招待申请专用 DTO;写后实时核验状态 |
|
||||
| 薪福通 | 出差费用申请单 | 支持,有约束 | 行程、城市和预计费用递归校验;实测仅有发起节点时流程即时完成并返回 `completed` |
|
||||
| 钉钉 OA | 业务招待申请_北极大 | 仅客户端 | 普通字段和明细表可读取,但真实测试中新版企业创建接口返回系统异常、旧版企业接口拒绝组件格式 |
|
||||
| 钉钉 OA | 出差申请单_北极大 | 仅客户端 | `DDBizSuite` 出差套件包含日程、天数、同行人及考勤联动,企业 OpenAPI 没有可靠构造协议 |
|
||||
| 钉钉 OA | 外出_北极大 | 仅客户端 | `DDBizSuite` 外出套件包含类型、起止时间、时长和考勤联动 |
|
||||
| 钉钉 OA | 业务招待申请_捷康 | 仅客户端 | 钉钉费控申请套件与普通 OA 明细并存,含企业主体、归属、成本中心等套件字段 |
|
||||
| 钉钉 OA | 差旅费用报销_捷康 | 仅客户端 | 钉钉费控报销套件含行程、报销明细、发票、收款账户和企业主体;另有关联出差申请和项目字段 |
|
||||
| 钉钉 OA | 业务招待费用报销_捷康 | 仅客户端 | 钉钉费控报销套件含报销明细、发票、收款账户和付款主体 |
|
||||
|
||||
薪福通自定义字段统一使用 `customFieldValues[].fieldNumber/fieldValue`。日期字段使用 `yyyy-MM-dd HH:mm`,人员字段值是花名册编号 JSON 数组,部门字段值是组织编码 JSON 数组,表格字段值是包含 `sequence`、`columnValues`、`titleCode` 的 JSON 数组字符串,附件字段值是先通过薪福通附件能力取得的附件编号。`approval_get_submission_schema` 会返回实时 `valueFormat`;Agent 不应按字段名称猜编码或结构。
|
||||
|
||||
薪福通模板要求承担部门或城市编码时,使用同一 `templateId` 调用 `approval_get_submission_options`。`kind=department` 只会返回当前登录用户本人档案中的可选组织;`kind=city` 可用 `query` 按城市名称搜索。城市全量接口较慢,网关给予独立的 45 秒超时并在进程内缓存字典 24 小时,不写入磁盘。响应仅包含可填写的 `code` 和 `label`,不会暴露员工号或原始成员档案。
|
||||
|
||||
薪福通的自定义字段接口可能同时列出 `billCompanyCode`、`billDepartmentCode` 等标准单据抬头字段,也可能用动态字段号返回名为“承担部门/单据部门”的 `ADV_DEPARTMENT` 控件,但创建接口禁止把它们放进 `customFieldValues`。MCP 会剔除这些服务器管理控件;公司和部门都省略时,薪福通根据当前登录申请人的档案默认填写。若通过标准抬头字段显式指定,则公司与部门必须成对填写。
|
||||
|
||||
任一来源模板的 `submissionCapability.status=client_only` 时,`approval_prepare_submission` 会再次拒绝,不能通过修改本地 Skill 或桥绕过。薪福通白名单模板使用正式创建接口,并在写后实时核验:`billSts=1` 为待审批,`billSts=2` 为企业流程规则导致的即时完成。当前列出的 6 个钉钉模板均为 `client_only`;对于以后可由 API 构造的钉钉模板,网关会先调用企业接口 `POST /v1.0/workflow/processes/forecast`,预测成功并补齐自选处理人后才生成提交确认。
|
||||
Reference in New Issue
Block a user