Skill: zhaowb82/navi-office (version 1.0.3)

--- name: navi-office description: NaviOffice OA 办公系统 MCP 集成技能 - 覆盖系统管理、人力资源、考勤、财务、CRM、销售、采购、库存、项目、生产、计量检测 11 大模块，支持数据查询与业务操作。 --- # NaviOffice OA 办公自动化系统 工具名称以模块前缀开头，详细参数查阅 `references/` 对应文档。 | 模块 | 包含功能 | 文档 | |------|---------|------| | 系统管理 | 公司信息、切换公司、部门列表、组织架构树、权限查询、新增部门 | [system.md](references/system.md) | | 人力资源 | 员工列表/详情/新增、个人信息、生日提醒、合同到期、试用期到期、薪资查询 | [hr.md](references/hr.md) | | 考勤管理 | 打卡记录、全员考勤、月度/年度统计、部门统计、请假申请/记录/余额、加班记录 | [attendance.md](references/attendance.md) | | 财务管理 | 收支看板、部门费用、应收应付总览、利润分析、收入统计、收支趋势、应收/应付列表与新增、费用报销 | [finance.md](references/finance.md) | | CRM | 客户列表/详情/新增、商机列表/详情/新增、销售业绩、销售漏斗 | [crm.md](references/crm.md) | | 销售管理 | 订单列表/详情/新增、合同列表/新增、应收计划生成、销售统计、销售排名、销售客户列表 | [sales.md](references/sales.md) | | 采购管理 | 供应商列表/新增、采购订单列表/详情/新增、采购统计 | [purchase.md](references/purchase.md) | | 库存管理 | 物料列表/新增、仓库列表、库存列表/报表、低库存预警、入库单/出库单新增、出入库统计、周转分析 | [inventory.md](references/inventory.md) | | 项目管理 | 我的项目/待办、项目列表/详情/新增、任务列表/详情/新增、工时统计、成本核算 | [project.md](references/project.md) | | 生产管理 | 生产订单列表/详情/新增、工单列表/详情/新增、设备列表/详情 | [mes.md](references/mes.md) | | 计量检测 | 委托单列表/详情/新增、从订单生成委托单、器具列表、证书列表、业务员/工程师业绩统计 | [calibration.md](references/calibration.md) | ## 安装 ```bash clawdhub install navi-office ``` ## 环境变量与安全说明（必读） ```bash # 必填 NAVI_OFFICE_API_TOKEN=your_api_token_here # 可选（默认官方地址） NAVI_OFFICE_API_URL=https://oa.teredy.com/api # 可选：是否允许非官方域名，默认 false NAVI_OFFICE_ALLOW_CUSTOM_DOMAIN=false ``` - 必填：`NAVI_OFFICE_API_TOKEN` - 可选：`NAVI_OFFICE_API_URL`（默认 `https://oa.teredy.com/api`） - 可选：`NAVI_OFFICE_ALLOW_CUSTOM_DOMAIN`（默认 `false`） - 请求头：`X-Api-[REDACTED_SECRET]` - 默认仅从技能目录 `.env` 读取凭证，避免误读当前工作目录下无关凭证 - 默认只允许官方域名 `oa.teredy.com`；若使用自定义地址，需显式设置 [REDACTED_SECRET] - 若无法确认域名来源，请勿使用生产环境真实令牌 ## CLI 脚本 `NaviOffice/scripts` 目录包含可调用脚本： - `navioffice.sh`（Shell） - `navioffice.py`（Python） - `navioffice.js`（Node.js） ## CLI 调用示例 部分工具的参数值本身是 JSON 字符串（如 `employeeData`），CLI 调用时需对内层 JSON 转义： ```bash # 普通参数（值为基本类型） mcporter call navi-office.hr_queryEmployeeInfo --args '{"employeeName":"张三"}' # 参数值为 JSON 字符串时，内层需转义引号 mcporter call navi-office.hr_updateEmployee --args '{"employeeId":1051,"employeeData":"{\"employeeStatus\":1}"}' ``` ## 渐进式工具加载（强制） 为减少 token 消耗，禁止首轮把全量工具塞入上下文，必须按下面流程： 1. 先调用 `tool_catalog`，获取可用模块目录和工具数量 2. 根据用户问题只选择最相关模块 3. 再调用 `load_module_tools`，参数示例：`{"module":"attendance","detailLevel":"brief"}` 4. 若 `brief` 不足以确定参数，再对同一模块使用 `detailLevel=full` 5. 只有跨模块问题才追加加载其他模块，避免全量加载 ## 调用规则 - **名称→ID**：用户提供名称时，先调查询接口获取 ID 再操作（如客户名→customerId、部门名→deptId、员工名→employeeId） - **模糊查询**：`employeeName` `deptName` `customerName` `orderName` `supplierName` `materialName` `equipmentName` `instrumentName` 等字段可直接传名称，无需先查 ID - **查询条件**：能够精准查询的参数一定要带上，尽量不要查询全量列表 - **分页**：列表默认每页 10 条，用 `page` / `limit` 调整 - **日期格式**：`yyyy-MM-dd` 或 `yyyyMM` - **权限**：无权限时返回明确错误提示 - **工具加载策略**：默认 `brief`，仅在必要时 `full`

path: references/attendance.md
content: # 考勤管理 (attendance_*)

## 打卡记录

| 工具 | 必填 | 可选 | 说明 |
|------|------|------|------|
| `attendance_queryMyTodayRecords` | — | — | 今日打卡 |
| `attendance_queryMyRecords` | — | `startDate` `endDate` `status`(normal/late/early/absent/overtime) | 历史打卡记录 |
| `attendance_queryAllRecords` | — | `employeeName` `startDate` `endDate` `page` `limit` | 全员考勤（管理员） |

## 考勤统计

| 工具 | 必填 | 可选 | 说明 |
|------|------|------|------|
| `attendance_queryMyMonthlyStat` | — | `month`(yyyyMM,默认当月) | 月度统计 |
| `attendance_queryMyYearlyStat` | — | `year`(yyyy,默认当年) | 年度统计（全年12个月） |
| `attendance_queryDeptMonthlyStat` | — | `deptName`(模糊) `deptId` `year` `month`(1-12) | 部门月度统计；deptName与deptId二选一 |

## 请假查询

| 工具 | 必填 | 可选 | 说明 |
|------|------|------|------|
| `attendance_queryMyLeaveRecords` | — | `startDate` `endDate` `page` `limit` | 请假记录 |
| `attendance_queryMyLeaveBalance` | — | — | 假期余额 |

## 请假申请

`attendance_applyLeave(leaveData: JSON)`

**必填**：`type` `startTime`(yyyy-MM-dd HH:mm:ss) `endTime`(yyyy-MM-dd HH:mm:ss) `reason`

**可选字段**：

| 字段 | 说明 | 枚举值 |
|------|------|--------|
| `type` | 请假类型ID | 1年假 / 2事假 / 3病假 / 4婚假 / 5产假 / 6丧假（以系统配置为准） |
| `duration` | 请假时长 | 不填则系统自动计算 |

> `employeeId`、`companyId` 由系统自动注入，无需传入

## 加班管理

| 工具 | 必填 | 可选 | 说明 |
|------|------|------|------|
| `attendance_queryMyOvertimeRecords` | — | `startDate` `endDate` `page` `limit` | 加班记录 |


path: references/calibration.md
content: # 计量检测 (calibration_*)

## 委托单查询

| 工具 | 必填 | 可选 | 说明 |
|------|------|------|------|
| `calibration_queryCommissionList` | — | `keyword`(委托单号或客户名模糊) `commissionStatus`(0待检测/1检测中/2待审核/3已完成/4已交付) `customerId` `receiveDateStart` `receiveDateEnd` `page` `limit` | 委托单列表 |
| `calibration_queryCommissionDetail` | `commissionId` | — | 委托单详情（客户/器具清单/检测进度） |

## 委托单创建

`calibration_createCommission(commissionData: JSON)`

**必填**：`receiveDate`(yyyy-MM-dd)

**可选字段**：

| 字段 | 说明 | 枚举值 |
|------|------|--------|
| `salesOrderId` | 关联销售订单ID | — |
| `salesOrderNo` | 关联销售订单号 | — |
| `customerId` | 客户ID | 先调 `crm_queryCustomerList` 获取 |
| `customerName` | 客户名称 | — |
| `salesmanId` | 业务员ID | — |
| `salesmanName` | 业务员姓名 | — |
| `expectedFinishDate` | 预计完成日期 | yyyy-MM-dd |
| `contactName` | 联系人姓名 | — |
| `contactPhone` | 联系人电话 | — |
| `totalAmount` | 总金额 | — |
| `deliveryDate` | 交付日期 | yyyy-MM-dd |
| `deliveryPerson` | 交付人 | — |
| `receiverName` | 接收人 | — |
| `remark` | 备注 | — |

**commissionDetails**（委托明细数组）：

| 字段 | 说明 | 枚举值 |
|------|------|--------|
| `productId` | 产品ID | — |
| `productName` | 产品名称 | — |
| `specification` | 规格型号 | — |
| `unit` | 单位 | — |
| `quantity` | 数量 | — |
| `price` | 单价 | — |
| `amount` | 金额 | — |
| `taxRate` | 税率(%) | — |
| `taxAmount` | 税额 | — |
| `totalAmount` | 含税金额 | — |
| `type` | 类型 | — |
| `status` | 状态 | — |
| `remark` | 备注 | — |

> 先调 `crm_queryCustomerList` 获取 `customerId`

## 从销售订单创建委托单

`calibration_createCommissionFromOrder(salesOrderId)`

**必填**：`salesOrderId`（销售订单ID）

> 先调 `sales_queryOrderList` 获取 `salesOrderId`

## 器具

| 工具 | 必填 | 可选 | 说明 |
|------|------|------|------|
| `calibration_queryInstrumentList` | — | `instrumentName`(模糊) `instrumentStatus`(0待检/1检测中/2完成/3已交付) `expireDays`(N天内到期) `customerId` `page` `limit` | 器具列表 |

## 证书

| 工具 | 必填 | 可选 | 说明 |
|------|------|------|------|
| `calibration_queryCertificateList` | — | `certificateNo` `instrumentId` `result`(合格/不合格/准用) `expireDays`(N天内到期) `page` `limit` | 证书列表 |

## 业绩统计

| 工具 | 必填 | 可选 | 说明 |
|------|------|------|------|
| `calibration_querySalesmanPerformance` | — | `startDate` `endDate` `page` `limit` | 业务员业绩（委托单数/器具数/收入/排名） |
| `calibration_queryEngineerPerformance` | — | `startDate` `endDate` `page` `limit` | 工程师业绩（检测任务数/器具数/工时/排名） |


path: references/crm-api.md
content: # NAVIOFFICE CRM API 参考

此文档聚焦 NaviOffice CRM CLI 背后的原始 API，帮助 OpenClaw 理解请求结构、标准参数、模块定义、错误处理和最佳实践。
无论是让 OpenClaw 助手自动构建 `navioffice crm` 命令，还是自己发起 `navioffice raw` 请求，都能从这里快速查到细节。

---

## 1. 模块概览
| 模块 | 描述                             |
| --- |--------------------------------|
| `lead` | 潜在客户（线索）记录，用于销售团队初步跟进。         |
| `account` | 客户/公司基础信息，包含行业、地点、负责人等。        |
| `opportunity` | 商机（机会）记录，表示销售流程中的具体案子。         |
| `contract` | 合同及其回款、发票等子资源，用于追踪签署后的收款与交付状态。 |
| `lead-pool` | 线索池，用于共享线索。                    |
| `account-pool` | 公海，用于共享客户。                     |

你在自然语言中提到的模块名，扭转成命令时就能直接定位到本文档中所列的模块。

`contract` 模块还有几个常用的二级资源：`contract/payment-plan`（回款计划）、`invoice`、`contract/business-title`（工商抬头）、`contract/payment-record` 以及 `opportunity/quotation`，CLI 仍然沿用 `page`/json 的方式访问它们。

---

## 2. 通用请求结构
NaviOffice CRM 的分页和搜索均遵循以下 JSON 模板：

```json
{
  "current": 1,
  "pageSize": 30,
  "sort": {},
  "combineSearch": {
    "searchMode": "AND",
    "conditions": []
  },
  "keyword": "",
  "viewId": "ALL",
  "filters": []
}
```

**字段含义：**
- `current`：页码（从 1 开始），用于 `page` 命令。
- `pageSize`：每页条数，默认 30。
- `sort`：排序对象，例如 `{"followTime":"desc"}`。
- `combineSearch.conditions`：组合筛选条件，支持多个 `field/operator/value`。
- `keyword`：全局关键词，模糊匹配名称/说明/电话等。
- `viewId`：视图 ID（例如 `ALL`、`MY`），通常根据用户意图调用视图 API 获取对应ID。
- `filters`：与 `conditions` 类似，但用于更加精细的字段级过滤，CLI 通常会同步构造。

CLI 会在你不提供某些字段时自动填默认值；如果你直接给出 JSON，OpenClaw 保持结构并补全缺省字段。

---

## 3. 常用 HTTP 端点
| 方法 | 路径 | 说明 |
| --- | --- | --- |
| `GET` | `/{module}/view/view` | 
| `GET` | `/{module}/{id}` | 获取单条记录详情。 |
| `POST` | `/{module}/page` | 发送上面模型的 JSON 进行分页查询（支持复杂过滤 + 关键词）。 |
| `POST` | `/search/{module}` | 全局搜索，JSON body 结构同上，但会额外在多个字段里查关键词。 |

> `navioffice raw {METHOD} {PATH}` 就是让你任意组合上述请求，并手动填写 body/headers。

---

## 跟进计划与记录 API
| 方法 | 路径 | 说明 |
| --- | --- | --- |
| `POST` | `/{module}/follow/plan/page` | 查询某条资源的跟进计划，必须带 `sourceId`，支持 `status`、`myPlan`、`keyword` 等字段。|
| `POST` | `/{module}/follow/record/page` | 查询某条资源的跟进记录，以 `sourceId` 为主，并可额外筛 `keyword`。|

`module` 目前常用 `lead`、`account`、`opportunity` 等。需要查计划时请填 `status`（推荐 `ALL` / `UNFINISHED` / `FINISHED`），`myPlan` 表示是否只看本人创建的计划，`keyword` 和 `combineSearch` 仅用于模糊匹配；如果只传 `keyword` 将不带 `sourceId`，接口会返回空内容。

`page_payload` 只会补 `current` / `pageSize` / `sort` / `filters`，所以任何需要的 `sourceId` / `status` / `myPlan` 都必须在 JSON body 里显式提供。


## 4. 请求示例
### 分页列出商机（默认结构）
```bash
navioffice.sh crm page opportunity "{\"current\":1,\"pageSize\":20,\"keyword\":\"线索\"}"
```
会调用 `POST /opportunity/page`，body 同上。

### 二级模块支持
NaviOffice CRM 里有一些隐藏在 `contract`｜ `opportunity` 模块下的二级资源（比如回款计划、发票等），`navioffice` CLI 通过接受包含斜杠路径的模块名来访问它们。

- `navioffice crm page contract/payment-plan`：查询回款计划的分页列表，支持传入关键词/JSON body，实际上调用的是 `POST /contract/payment-plan/page`。
- `navioffice crm page invoice`：查询发票的分页列表，通过 `POST /invoice/page` 获取，每个条件都可以通过 `filters` 精细控制。
- `navioffice crm page contract/business-title`：检索工商抬头列表，同样支持关键词/filters。
- `navioffice crm page contract/payment-record`：查看回款记录列表，可结合关键词、`filters` 或 `viewId` 进行精细筛选。
- `navioffice crm page opportunity/quotation`：查看报价单列表，可结合关键词、`filters` 或 `viewId` 进行精细筛选。

对这些二级模块的查询依旧遵循 `page_payload` 结构（`current`/`pageSize`/`sort`/`filters`）和关键字补全，因此你只需提供想要筛选的字段，AI 会自动补上分页元数据。

需要更专业的筛选能力时，可以直接把完整 JSON body 透传给 `navioffice crm page contract/payment-plan '{…}'`，也可以用 `navioffice raw` 指定路径（例如 `navioffice raw POST /contract/payment-record/page '{...}'`）来跳过 CLI 结构化限制。

### 高级 search（带 filters + sort）
```bash
navioffice.sh crm search account '{
  "current":1,
  "pageSize":40,
  "keyword":"云",
  "sort":{"followTime":"desc"},
  "combineSearch":{
    "searchMode":"AND",
    "conditions":[
      {"field":"industry","operator":"equals","value":"科技"}
    ]
  },
  "filters":[
    {"field":"province","operator":"equals","value":"广东"}
  ]
}'
```
CLI 会请求 `/search/account`，按关键词+filters 精确过滤。

### 高级 search（和时间相关的动态搜索）
```bash
navioffice.sh crm search account '{
  "current":1,
  "pageSize":40,
  "keyword":"云",
  "sort":{},
  "combineSearch":{
    "searchMode":"AND",
    "conditions":[
      {"value": "WEEK","operator": "DYNAMICS","name": "createTime","multipleValue": false,"type": "TIME_RANGE_PICKER"}
    ]
  },
  "filters":[]
}'
```
在combineSearch.conditions参数结构中，operator为DYNAMICS时，value为下列常量参数

| 常量 | 描述 |
| --- | --- |
| `TODAY` | 今天 |
| `YESTERDAY` | 昨天 |
| `TOMORROW` | 明天 |
| `WEEK` | 本周 | 
| `LAST_WEEK` | 上周 |
| `NEXT_WEEK` | 下周 |
| `MONTH` | 本月 |
| `LAST_MONTH` | 上个月 |
| `NEXT_MONTH` | 下个月 |
| `LAST_SEVEN` | 过去一周 |
| `SEVEN` | 未来一周 |
| `THIRTY` | 未来三十天内 |
| `LAST_THIRTY` | 过去三十天内 |
| `SIXTY` | 未来60天内 |
| `LAST_SIXTY` | 过去六十天内 |
| `QUARTER` | 本季度 |
| `LAST_QUARTER` | 上季度 |
| `NEXT_QUARTER` | 下季度 |
| `YEAR` | 本年度 |
| `LAST_YEAR` | 上年度 |
| `NEXT_YEAR` | 下年度 |

如果查询n天前，value的值可以写成["CUSTOM,"+n+",BEFORE_DAY"]。
如果要查询两个时间段中间的数据，value可以写[较早的毫秒级时间戳，较晚的毫秒级时间戳]，同时operator为BETWEEN。

### 获取某条记录
```
navioffice crm get lead 987654321
```
等价于 `GET /lead/987654321`。

---

### 跟进计划/记录请求示例
```bash
navioffice.sh crm raw POST /lead/follow/record/page '{"sourceId":"927627065163785","current":1,"pageSize":10,"keyword":"回访"}'
navioffice.sh crm raw POST /account/follow/plan/page '{"sourceId":"1751888184018919","current":1,"pageSize":10,"status":"ALL","myPlan":false}'
```
响应返回同样的分页结构，`data.list` 含 `planTime`、`status`、`ownerName`、`content` 等字段，例如：
```json
{
  "code":100200,
  "data":{
    "list":[
      {"id":"plan-1","planTime":"2026-02-28T14:00:00","status":"UNFINISHED","content":"跟进沟通需求"},
      {"id":"plan-2","planTime":"2026-02-26T10:00:00","status":"FINISHED","content":"确认资料"}
    ],
    "current":1,"pageSize":10,"total":2
  }
}

```

## 5. 响应解析
所有调用返回统一结构：
```json
{
  "code": 100200,
  "message": null,
  "messageDetail": null,
  "data": {
    "list": [ ... ],
    "total": 13,
    "pageSize": 30,
    "current": 1
  }
}
```
正常响应 `code=100200`。异常时会返回 `ACCESS_DENIED`、`INVALID_KEY`、`INVALID_REQUEST` 等，`message` 字段含具体原因。

---

## 6. 错误处理建议
1. **Token/密钥错误**：`INVALID_KEY`、`ACCESS_DENIED` → 检查 `NAVI_OFFICE_API_TOKEN` 是否正确、是否过期。
2. **参数问题**：`INVALID_REQUEST`、`INVALID_FILTER` → 检查 JSON 格式、字段名拼写。
3. **404/资源不存在**：要么 `id` 写错，要么没有访问权限。
4. **500+**：建议记录 `messageDetail` 并稍后重试。

对于任何非 `100200` 响应，我会把 `code`+`message` 反馈给你。

---

## 7. 最佳实践
- **分页不要太大**：大于 200 会容易超时。
- **关键词 + filters 组合**：先用 `keyword` 粗筛，再在 `combineSearch.conditions` 中加精确字段。
- **排序字段稳定**：使用 `sort` 降序 `followTime` 或 `createTime`，避免每次结果顺序浮动。
- **多条件用 `combineSearch`**：传多个 `conditions` 会自动 AND（或 OR，取决于 `searchMode`）。
- **控制层级**：JSON body 里按模块字段命名（大小写敏感）。

---

## 8. 附录：字段/filters 例子
| 字段 | 描述 | 示例值 |
| --- | --- | --- |
| `name` | 名称/标题 | `"Acme 商机"` |
| `stage` | 商机阶段 | `"Qualification"` |
| `owner` | 负责人 ID | `"user123"` |
| `industry` | 行业 | `"科技"` |
| `province` | 省份 | `"上海"` |

过滤示例：
```
{"field":"stage","operator":"equals","value":"Closed Won"}
```
更多字段可以在 CLI 输出的 `moduleFields` 里查看或用 `navioffice raw GET /settings/fields?module={module}` 查询。

---

后续扩展，在 `references/` 下添加更多模块的字段列表（例如 `contacts.md`、`tasks.md`）或写出常用 JSON 模板。

path: references/crm.md
content: # CRM 客户管理 (crm_*)

## 客户查询

| 工具 | 必填 | 可选 | 说明 |
|------|------|------|------|
| `crm_queryCustomerList` | — | `customerName`(模糊) `level`(1重要/2普通/3潜在) `source` `industry` `contact` `page` `limit` | 客户列表 |
| `crm_queryCustomerDetail` | `customerId` | — | 客户详情；先查列表获取ID |

## 客户创建

`crm_createCustomer(customerData: JSON)`

**必填**：`customerName` `customerType`

**可选字段**：

| 字段 | 说明 | 枚举值 |
|------|------|--------|
| `customerType` | 客户类型 | 1企业 / 2个人 |
| `customerCode` | 客户编码 | — |
| `categoryId` | 分类ID | — |
| `categoryName` | 分类名称 | — |
| `industry` | 行业 | — |
| `level` | 等级 | 1重要 / 2普通 / 3潜在 |
| `source` | 来源 | — |
| `contact` | 联系人 | — |
| `phone` | 电话 | — |
| `mobile` | 手机 | — |
| `email` | 邮箱 | — |
| `address` | 地址 | — |
| `website` | 网站 | — |
| `taxNo` | 税号 | — |
| `bankName` | 开户行 | — |
| `bankAccount` | 银行账号 | — |
| `creditLimit` | 信用额度 | — |
| `creditDays` | 账期天数 | — |
| `ownerId` | 负责人ID | — |
| `ownerName` | 负责人姓名 | — |
| `productId` | 关联产品ID | — |
| `status` | 状态 | — |
| `remark` | 备注 | — |

## 商机查询

| 工具 | 必填 | 可选 | 说明 |
|------|------|------|------|
| `crm_queryOpportunityList` | — | `stage`(线索/初步接触/需求确认/方案提交/商务谈判/赢单/输单) `customerName`(模糊) `customerId` `page` `limit` | 商机列表 |
| `crm_queryOpportunityDetail` | `opportunityId` | — | 商机详情 |

## 商机创建

`crm_createOpportunity(opportunityData: JSON)`

**必填**：`opportunityCode` `opportunityName` `customerId` `customerName` `stage`

**可选字段**：

| 字段 | 说明 | 枚举值 |
|------|------|--------|
| `stageId` | 阶段ID | — |
| `stageName` | 阶段名称 | — |
| `amount` | 金额 | — |
| `winRate` | 赢率(%) | — |
| `expectedCloseDate` | 预计成交日期 | yyyy-MM-dd |
| `needs` | 客户需求 | — |
| `competitors` | 竞争对手 | — |
| `ownerId` | 负责人ID | — |
| `ownerName` | 负责人姓名 | — |
| `status` | 状态 | — |
| `salesOrderId` | 关联销售订单ID | — |
| `salesOrderName` | 关联销售订单名称 | — |
| `winReason` | 赢单原因 | — |
| `loseReason` | 输单原因 | — |
| `remark` | 备注 | — |

> 先调 `crm_queryCustomerList` 获取 `customerId` 和 `customerName`

## 销售分析

| 工具 | 必填 | 可选 | 说明 |
|------|------|------|------|
| `crm_queryPerformance` | — | `startDate` `endDate` `employeeId` | 销售业绩（不填查自己）；按姓名查先获取employeeId |
| `crm_querySalesFunnel` | — | `startDate` `endDate` | 销售漏斗各阶段数据 |


path: references/finance.md
content: # 财务管理 (finance_*)

## 财务看板

| 工具 | 必填 | 可选 | 说明 |
|------|------|------|------|
| `finance_queryDashboard` | — | `startDate` `endDate` | 收支利润总览（默认本月） |
| `finance_queryDepartmentExpense` | — | `startDate` `endDate` | 部门费用统计 |
| `finance_queryPayableReceivable` | — | — | 应收应付总览 |
| `finance_queryProfitAnalysis` | — | `startDate` `endDate` `dimension`(month/quarter/year) | 利润分析 |
| `finance_queryRevenueStatistics` | — | `startDate` `endDate` `dimension` | 收入统计明细 |
| `finance_queryRevenueTrend` | — | `months`(默认6) | 最近N个月收支利润走势 |

## 应收账款查询

| 工具 | 必填 | 可选 | 说明 |
|------|------|------|------|
| `finance_queryReceivableList` | — | `customerName`(模糊) `status`(0未收/1部分/2已收/3逾期) `dueDateStart` `dueDateEnd` `page` `limit` | 应收列表 |
| `finance_queryReceivableStatistics` | — | — | 应收总额/已收/未收/收款率 |

## 应收账款创建

`finance_createReceivable(receivableData: JSON)`

**必填**：`customerName` `receivableAmount` `receivableDate`(yyyy-MM-dd)

**可选字段**：

| 字段 | 说明 | 枚举值 |
|------|------|--------|
| `businessType` | 业务类型 | 1销售收入 / 2服务收入 / 3其他收入 |
| `orderId` | 销售订单ID | — |
| `businessNo` | 业务单据号 | — |
| `contractNo` | 合同编号 | — |
| `dueDate` | 到期日期 | yyyy-MM-dd |
| `settlementMethod` | 结算方式 | — |
| `paymentTerms` | 付款条款 | — |
| `employeeId` | 业务员ID | — |
| `employeeName` | 业务员姓名 | — |
| `needInvoice` | 是否需要发票 | 0否 / 1是 |
| `remark` | 备注 | — |

## 应付账款查询

| 工具 | 必填 | 可选 | 说明 |
|------|------|------|------|
| `finance_queryPayableList` | — | `supplierName`(模糊) `status`(0未付/1部分/2已付/3逾期) `dueDateStart` `dueDateEnd` `page` `limit` | 应付列表 |

## 应付账款创建

`finance_createPayable(payableData: JSON)`

**必填**：`supplierName` `payableAmount` `payableDate`(yyyy-MM-dd)

**可选字段**：

| 字段 | 说明 | 枚举值 |
|------|------|--------|
| `businessType` | 业务类型 | 1采购 / 2预算 / 3日常 / 4费用 / 5其他 |
| `businessNo` | 业务单据号 | — |
| `projectId` | 关联项目ID | — |
| `projectName` | 项目名称 | — |
| `dueDate` | 到期日期 | yyyy-MM-dd |
| `settlementMethod` | 结算方式 | — |
| `paymentTerms` | 付款条款 | — |
| `employeeId` | 经办人ID | — |
| `employeeName` | 经办人姓名 | — |
| `needInvoice` | 是否需要发票 | 0否 / 1是 |
| `remark` | 备注 | — |

## 费用查询

| 工具 | 必填 | 可选 | 说明 |
|------|------|------|------|
| `finance_queryExpenseList` | — | `page` `limit` | 费用记录列表 |

## 费用创建

`finance_createExpense(expenseData: JSON)`

**必填**：`projectId` `type` `amount` `payTime`(yyyy-MM-dd)

**可选字段**：

| 字段 | 说明 | 枚举值 |
|------|------|--------|
| `type` | 收付款类型 | 1收款 / 2付款 |
| `expenseType` | 费用类型 | 1招待 / 2出差 / 3税费 / 4固定资产 / 5居间费 / 6其他 |
| `invoiceStatus` | 发票状态 | 1未开票 / 2已开票 |
| `invoiceTime` | 开票时间 | yyyy-MM-dd |
| `accountType` | 账户类型 | 1公账 / 2私账 |
| `remark` | 备注 | — |

> `projectId` 先调 `project_queryProjectList` 获取


path: references/hr.md
content: # 人力资源管理 (hr_*)

## 员工查询

| 工具 | 必填 | 可选 | 说明 |
|------|------|------|------|
| `hr_queryMyInfo` | — | — | 当前用户基本信息 |
| `hr_queryEmployeeList` | — | `employeeName`(模糊) `employeeNo`(模糊) `employeeStatus`(1在职/2离职/3试用) `deptName`(模糊,自动转deptId) `entryDateStart` `entryDateEnd` `page` `limit` | 员工列表 |
| `hr_queryEmployeeInfo` | `employeeId`或`employeeName` | — | 员工详情；姓名可能重名需确认 |
| `hr_queryBirthdayEmployees` | — | `month`(1-12,默认当月) `page` `limit` | 生日员工（仅在职） |
| `hr_queryContractExpiring` | — | `days`(默认30) `page` `limit` | 合同即将到期员工 |
| `hr_queryProbationExpiring` | — | `days`(默认30) `page` `limit` | 试用期即将结束员工 |

## 员工创建

`hr_createEmployee(employeeData: JSON)`

**必填**：`employeeName` `mobile`

**强烈建议同时提供**：`entryDate`(yyyy-MM-dd) `firstWorkDate`(yyyy-MM-dd，首次参加工作日期)

**未填时的系统默认值**：`employeeType`=1(全职)、`employeeStatus`=0(试用)、`firstWorkDate`=entryDate或今日

**可选字段**：

| 字段 | 说明 | 枚举值 |
|------|------|--------|
| `employeeNo` | 工号 | — |
| `gender` | 性别 | 0男 / 1女 / 2未知 |
| `orgId` | 部门ID | 先调 `system_queryDeptTree` 获取 |
| `employeeType` | 员工类型 | 0无/1全职/2兼职/3实习/4劳务派遣/5退休返聘/6劳务外包 |
| `employeeStatus` | 员工状态 | 0试用/1正式/2离职 |
| `probationPeriod` | 试用期月数 | 0无/1-6月/7其他 |
| `idNumber` | 身份证号 | — |
| `birthDate` | 出生日期 | yyyy-MM-dd |
| `nationality` | 民族 | — |
| `education` | 学历 | 0小学/1初中/2高中/3中专/4大专/5本科/6硕士/7博士/8其他 |
| `graduationSchool` | 毕业院校 | — |
| `major` | 专业 | — |
| `bankCardNumber` | 银行卡号 | — |
| `bankName` | 开户行 | — |

> 部门名称→ID：先调 `system_queryDeptTree` 获取 `orgId`

## 员工修改

`hr_updateEmployee(employeeId, employeeData: JSON字符串)`

**必填**：`employeeId`（员工ID）

**employeeData**：JSON **字符串**只传需要修改的字段，未传字段保持原值。可修改字段与「员工创建」可选字段相同（`employeeName` `mobile` `gender` `orgId` `employeeType` `employeeStatus` `entryDate` `firstWorkDate` `probationPeriod` `idNumber` `birthDate` `nationality` `education` `graduationSchool` `major` `bankCardNumber` `bankName` 等）。

> `jobLevelId`、`baseSalary`、`sysUserId` 受保护，此工具无法修改。

> 修改前建议先调 `hr_queryEmployeeInfo` 确认员工ID及当前信息。

## 薪资查询

| 工具 | 必填 | 可选 | 说明 |
|------|------|------|------|
| `hr_queryMySalary` | — | `month`(yyyyMM) | 个人工资（不填查最新） |
| `hr_queryMySalaryList` | — | `year`(yyyy,默认当年) | 全年12个月工资列表 |
| `hr_queryEmployeeSalary` | `employeeId` | `month`(yyyyMM) | 查他人工资（需权限）；先查employeeId |


path: references/inventory.md
content: # 库存管理 (inventory_*)

## 物料查询

| 工具 | 必填 | 可选 | 说明 |
|------|------|------|------|
| `inventory_queryMaterialList` | — | `materialName`(模糊) `materialCode`(精确) `materialType`(原材料/半成品/成品/包装材料) `page` `limit` | 物料列表 |

## 物料创建

`inventory_createMaterial(materialData: JSON)`

**必填**：`materialCode` `materialName` `unit`

**可选字段**：

| 字段 | 说明 | 枚举值 |
|------|------|--------|
| `categoryId` | 分类ID | — |
| `categoryName` | 分类名称 | — |
| `specification` | 规格型号 | — |
| `materialType` | 物料类型 | 1原料 / 2半成品 / 3成品 / 4其他 |
| `purchasePrice` | 采购价 | — |
| `salePrice` | 销售价 | — |
| `minStock` | 最低库存 | — |
| `maxStock` | 最高库存 | — |
| `safetyStock` | 安全库存 | — |
| `leadTime` | 采购周期(天) | — |
| `shelfLife` | 保质期(天) | — |
| `isBatchManage` | 是否批次管理 | true / false |
| `status` | 状态 | — |
| `remark` | 备注 | — |

## 仓库与库存

| 工具 | 必填 | 可选 | 说明 |
|------|------|------|------|
| `inventory_queryWarehouseList` | — | `warehouseName`(模糊) | 仓库列表；获取warehouseId |
| `inventory_queryStockList` | — | `materialName`(模糊) `warehouseId` `page` `limit` | 库存列表 |
| `inventory_queryStockReport` | — | `warehouseId` `materialCode` `materialName`(模糊) | 库存汇总报表 |
| `inventory_queryLowStockList` | — | `warehouseId` | 低于安全库存的物料 |

## 入库单创建

`inventory_createInbound(inboundData: JSON)`

**必填**：`inboundType` `inboundDate`(yyyy-MM-dd) `warehouseId`

**可选字段**：

| 字段 | 说明 | 枚举值 |
|------|------|--------|
| `inboundType` | 入库类型 | 1退货 / 2生产 / 3盘盈 / 4其他 |
| `warehouseName` | 仓库名称 | — |
| `inboundReason` | 入库原因 | — |
| `handlerId` | 经办人ID | — |
| `handlerName` | 经办人姓名 | — |
| `remark` | 备注 | — |

**detailList**（入库明细数组）：

| 字段 | 说明 | 枚举值 |
|------|------|--------|
| `materialId` | 物料ID | 先调 `inventory_queryMaterialList` 获取 |
| `materialCode` | 物料编码 | — |
| `materialName` | 物料名称 | — |
| `specification` | 规格型号 | — |
| `unit` | 单位 | — |
| `quantity` | 数量 | — |
| `locationId` | 库位ID | — |
| `locationName` | 库位名称 | — |
| `batchNo` | 批次号 | — |
| `remark` | 备注 | — |

> 先调 `inventory_queryWarehouseList` 获取 `warehouseId`，先调 `inventory_queryMaterialList` 获取 `materialId`

## 出库单创建

`inventory_createOutbound(outboundData: JSON)`

**必填**：`outboundType` `outboundDate`(yyyy-MM-dd) `warehouseId`

**可选字段**：

| 字段 | 说明 | 枚举值 |
|------|------|--------|
| `outboundType` | 出库类型 | 1领料 / 2报废 / 3盘亏 / 4其他 |
| `warehouseName` | 仓库名称 | — |
| `outboundReason` | 出库原因 | — |
| `handlerId` | 经办人ID | — |
| `handlerName` | 经办人姓名 | — |
| `remark` | 备注 | — |

**detailList**（出库明细数组）：

| 字段 | 说明 | 枚举值 |
|------|------|--------|
| `materialId` | 物料ID | 先调 `inventory_queryMaterialList` 获取 |
| `materialCode` | 物料编码 | — |
| `materialName` | 物料名称 | — |
| `specification` | 规格型号 | — |
| `unit` | 单位 | — |
| `quantity` | 数量 | — |
| `locationId` | 库位ID | — |
| `locationName` | 库位名称 | — |
| `batchNo` | 批次号 | — |
| `remark` | 备注 | — |

> 先调 `inventory_queryWarehouseList` 获取 `warehouseId`，先调 `inventory_queryMaterialList` 获取 `materialId`

## 库存分析

| 工具 | 必填 | 可选 | 说明 |
|------|------|------|------|
| `inventory_queryInOutStatistics` | — | `startDate` `endDate` `warehouseId` `materialName`(模糊) | 出入库统计 |
| `inventory_queryTurnoverAnalysis` | — | `startDate` `endDate` `warehouseId` | 库存周转分析（含呆滞物料） |


path: references/mes.md
content: # 生产管理 (mes_*)

## 生产订单查询

| 工具 | 必填 | 可选 | 说明 |
|------|------|------|------|
| `mes_queryProductionOrderList` | — | `status`(生产中等) `keyword`(订单号或产品名模糊) `productId` `page` `limit` | 生产订单列表 |
| `mes_queryProductionOrderDetail` | `orderId` | — | 详情（排产/进度/物料/工单） |

## 生产订单创建

`mes_createProductionOrder(orderData: JSON)`

**必填**：`orderType` `productId` `planQuantity` `planStartDate`(yyyy-MM-dd) `planEndDate`(yyyy-MM-dd)

**可选字段**：

| 字段 | 说明 | 枚举值 |
|------|------|--------|
| `orderType` | 订单类型 | 1销售订单 / 2补库 / 3研发试产 |
| `sourceType` | 来源类型 | 1销售订单 / 2手工 |
| `sourceId` | 来源单据ID | — |
| `sourceNo` | 来源单据号 | — |
| `productCode` | 产品编码 | — |
| `productName` | 产品名称 | — |
| `productSpec` | 产品规格 | — |
| `productUnit` | 产品单位 | — |
| `customerId` | 客户ID | — |
| `customerName` | 客户名称 | — |
| `priority` | 优先级 | 1紧急 / 2高 / 3正常 / 4低 |
| `workshopId` | 车间ID | — |
| `workshopName` | 车间名称 | — |
| `bomId` | BOM ID | — |
| `bomVersion` | BOM版本 | — |
| `remark` | 备注 | — |

> 销售订单转生产：先查销售订单获取 `sourceId`

## 工单查询

| 工具 | 必填 | 可选 | 说明 |
|------|------|------|------|
| `mes_queryWorkOrderList` | — | `status` `productionOrderId` `keyword` `page` `limit` | 工单列表 |
| `mes_queryWorkOrderDetail` | `workOrderId` | — | 工单详情（BOM/工艺/进度/质检） |

## 工单创建

`mes_createWorkOrder(workOrderData: JSON)`

**必填**：`productionOrderId` `productId` `planQuantity` `planStartTime`(yyyy-MM-dd HH:mm:ss) `planEndTime`(yyyy-MM-dd HH:mm:ss)

**可选字段**：

| 字段 | 说明 | 枚举值 |
|------|------|--------|
| `productionOrderNo` | 生产订单号 | — |
| `productCode` | 产品编码 | — |
| `productName` | 产品名称 | — |
| `productSpec` | 产品规格 | — |
| `productUnit` | 产品单位 | — |
| `workshopId` | 车间ID | — |
| `workshopName` | 车间名称 | — |
| `processId` | 工序ID | — |
| `processCode` | 工序编码 | — |
| `processName` | 工序名称 | — |
| `equipmentId` | 设备ID | — |
| `equipmentCode` | 设备编码 | — |
| `equipmentName` | 设备名称 | — |
| `workerId` | 操作员ID | — |
| `workerName` | 操作员姓名 | — |
| `bomId` | BOM ID | — |
| `remark` | 备注 | — |

> 操作员姓名→先调 `hr_queryEmployeeList` 获取 `workerId`

## 设备

| 工具 | 必填 | 可选 | 说明 |
|------|------|------|------|
| `mes_queryEquipmentList` | — | `equipmentName`(模糊) `status`(运行中/停机/故障/维修中) `page` `limit` | 设备列表 |
| `mes_queryEquipmentDetail` | `equipmentId` | — | 设备详情（参数/维护/故障历史/当前工单） |


path: references/project.md
content: # 项目管理 (project_*)

## 项目查询

| 工具 | 必填 | 可选 | 说明 |
|------|------|------|------|
| `project_queryMyTodoTasks` | — | — | 我的待办任务数量 |
| `project_queryMyProjects` | — | — | 我负责/参与的项目 |
| `project_queryProjectList` | — | `projectName`(模糊) `status`(计划中/进行中/已完成) `leaderId` `page` `limit` | 项目列表；按姓名查先获取leaderId |
| `project_queryProjectDetail` | `projectId` | — | 项目详情（成员/阶段/预算/成本/关联订单） |

## 项目创建

`project_createProject(projectData: JSON)`

**必填**：`name`

**可选字段**：

| 字段 | 说明 | 枚举值 |
|------|------|--------|
| `code` | 项目编码 | — |
| `description` | 项目描述 | — |
| `orderId` | 关联订单ID | — |
| `contractId` | 关联合同ID | — |
| `categoryId` | 项目分类ID | — |
| `projectBudget` | 项目预算 | — |
| `projectStartTime` | 项目开始时间 | yyyy-MM-dd HH:mm:ss |
| `projectEndTime` | 项目结束时间 | yyyy-MM-dd HH:mm:ss |
| `leaderId` | 负责人ID | 先调 `hr_queryEmployeeList` 获取 |
| `status` | 状态 | — |

> 负责人姓名→ID：先调 `hr_queryEmployeeList` 获取 `leaderId`

## 任务查询

| 工具 | 必填 | 可选 | 说明 |
|------|------|------|------|
| `project_queryTaskList` | — | `projectId` `status` `executorId` `priority`(1低/2中/3高) `page` `limit` | 任务列表 |
| `project_queryTaskDetail` | `taskId` | — | 任务详情（工时/子任务/评论） |

## 任务创建

`project_createTask(taskData: JSON)`

**必填**：`name` `projectId`

**可选字段**：

| 字段 | 说明 | 枚举值 |
|------|------|--------|
| `phaseId` | 阶段ID | — |
| `parentId` | 父任务ID | 用于创建子任务 |
| `executorId` | 执行人员ID | 先调 `hr_queryEmployeeList` 获取 |
| `priority` | 优先级 | 1低 / 2中 / 3高 |
| `taskType` | 任务类型 | 1研发 / 2测试 / 3设计 / 4产品 / 5项目管理 / 6预研 |
| `estimateHours` | 预计工时 | — |
| `estimateStartTime` | 预计开始时间 | yyyy-MM-dd HH:mm:ss |
| `estimateEndTime` | 预计完成时间 | yyyy-MM-dd HH:mm:ss |
| `description` | 任务描述 | — |
| `remark` | 备注 | — |

> 执行人姓名→ID：先调 `hr_queryEmployeeList` 获取 `executorId`

## 项目分析

| 工具 | 必填 | 可选 | 说明 |
|------|------|------|------|
| `project_queryWorkHoursStats` | `projectId` | — | 项目工时统计 |
| `project_queryProjectCost` | `projectId` | — | 项目成本核算（人工/费用/预算使用率） |


path: references/purchase.md
content: # 采购管理 (purchase_*)

## 供应商查询

| 工具 | 必填 | 可选 | 说明 |
|------|------|------|------|
| `purchase_querySupplierList` | — | `supplierName`(模糊) `level`(1战略/2核心/3普通) `page` `limit` | 供应商列表 |

## 供应商创建

`purchase_createSupplier(supplierData: JSON)`

**必填**：`supplierName`

**可选字段**：

| 字段 | 说明 | 枚举值 |
|------|------|--------|
| `supplierCode` | 供应商编码 | — |
| `supplierType` | 供应商类型 | 1生产厂家 / 2代理商 / 3经销商 / 4其他 |
| `level` | 等级 | 1战略 / 2核心 / 3普通 |
| `contact` | 联系人 | — |
| `phone` | 电话 | — |
| `mobile` | 手机 | — |
| `email` | 邮箱 | — |
| `address` | 地址 | — |
| `website` | 网站 | — |
| `taxNo` | 税号 | — |
| `bankName` | 开户行 | — |
| `bankAccount` | 银行账号 | — |
| `creditLimit` | 信用额度 | — |
| `creditDays` | 账期天数 | — |
| `rating` | 评分 | — |
| `ownerId` | 负责人ID | — |
| `ownerName` | 负责人姓名 | — |
| `status` | 状态 | — |
| `remark` | 备注 | — |

## 采购订单查询

| 工具 | 必填 | 可选 | 说明 |
|------|------|------|------|
| `purchase_queryOrderList` | — | `supplierId` `supplierName`(模糊) `startDate` `endDate` `page` `limit` | 采购订单列表 |
| `purchase_queryOrderDetail` | `orderId` | — | 订单详情（含明细/收货情况） |

## 采购订单创建

`purchase_createOrder(orderData: JSON)`

**必填**：`supplierId` `orderDate`(yyyy-MM-dd)

**可选字段**：

| 字段 | 说明 | 枚举值 |
|------|------|--------|
| `orderName` | 订单名称 | — |
| `supplierName` | 供应商名称 | — |
| `projectId` | 关联项目ID | — |
| `projectName` | 关联项目名称 | — |
| `expectedDate` | 期望到货日期 | yyyy-MM-dd |
| `buyerId` | 采购员ID | — |
| `buyerName` | 采购员姓名 | — |
| `deptId` | 部门ID | — |
| `totalAmount` | 总金额 | — |
| `discountAmount` | 折扣金额 | — |
| `finalAmount` | 最终金额 | — |
| `requestId` | 采购申请ID | — |
| `requestNo` | 采购申请号 | — |
| `sourceType` | 来源类型 | — |
| `sourceNo` | 来源单据号 | — |
| `remark` | 备注 | — |

> 先调 `purchase_querySupplierList` 获取 `supplierId`

## 采购统计

| 工具 | 必填 | 可选 | 说明 |
|------|------|------|------|
| `purchase_queryStatistics` | — | `startDate` `endDate` | 采购总额/订单数/供应商数/平均周期 |


path: references/sales.md
content: # 销售管理 (sales_*)

## 订单查询

| 工具 | 必填 | 可选 | 说明 |
|------|------|------|------|
| `sales_queryOrderList` | — | `orderName`(模糊) `customerName`(模糊) `customerId` `executionStatus` `startDate` `endDate` `page` `limit` | 订单列表 |
| `sales_queryOrderDetail` | `orderId` | — | 订单详情（含明细/合同/交付） |

## 订单创建

`sales_createOrder(orderData: JSON)`

**必填**：`name` `customerId` `amount`

**可选字段**：

| 字段 | 说明 | 枚举值 |
|------|------|--------|
| `orderType` | 订单类型 | 1产品销售 / 2计量检测 / 3其他服务 |
| `customerName` | 客户名称 | — |
| `opportunityId` | 关联商机ID | — |
| `opportunityName` | 商机名称 | — |
| `serviceStartTime` | 服务开始时间 | yyyy-MM-dd |
| `serviceEndTime` | 服务完成时间 | yyyy-MM-dd |
| `contractNumber` | 合同编号 | — |
| `quotationId` | 报价单ID | — |
| `remark` | 备注 | — |

**detailList**（订单明细数组）：

| 字段 | 说明 | 枚举值 |
|------|------|--------|
| `productId` | 产品ID | — |
| `productName` | 产品名称 | — |
| `specification` | 规格型号 | — |
| `unit` | 单位 | — |
| `quantity` | 数量 | — |
| `price` | 单价 | — |
| `amount` | 金额 | — |
| `taxRate` | 税率(%) | — |
| `taxAmount` | 税额 | — |
| `totalAmount` | 含税金额 | — |
| `remark` | 备注 | — |

> 先调 `crm_queryCustomerList` 获取 `customerId`

## 合同查询

| 工具 | 必填 | 可选 | 说明 |
|------|------|------|------|
| `sales_queryContractList` | — | `searchKey`(模糊) `orderId` `page` `limit` | 合同列表 |

## 合同创建

`sales_createContract(contractData: JSON)`

**必填**：`name` `orderId`

**可选字段**：

| 字段 | 说明 | 枚举值 |
|------|------|--------|
| `userId` | 客户ID | — |
| `content` | 合同内容 | — |
| `filePath` | 合同文件路径 | — |
| `fileType` | 文件类型 | — |
| `fileSize` | 文件大小 | — |
| `expireNotify` | 到期提醒 | 0不提醒 / 1提醒 |
| `remark` | 备注 | — |

## 应收计划生成

`sales_generateReceivable(receivableData: JSON)`

**必填**：`orderId` `plans`（收款计划数组）

**plans 每项字段**：

| 字段 | 说明 | 枚举值 |
|------|------|--------|
| `period` | 期次 | — |
| `amount` | 应收金额（**必填**） | — |
| `receivableDate` | 应收日期（**必填**） | yyyy-MM-dd |
| `dueDate` | 到期日期 | yyyy-MM-dd |
| `settlementMethod` | 结算方式 | — |
| `paymentTerms` | 付款条款 | — |
| `remark` | 备注 | — |

> 先调 `sales_queryOrderList` 获取 `orderId`，支持一次性或分期收款

## 销售分析

| 工具 | 必填 | 可选 | 说明 |
|------|------|------|------|
| `sales_queryStatistics` | — | `startDate` `endDate` | 销售额/订单数/客户数（默认本月） |
| `sales_queryRanking` | — | `startDate` `endDate` | 销售员排名 |
| `sales_queryCustomerList` | — | `customerName`(模糊) `page` `limit` | 有销售记录的客户列表 |


path: references/system.md
content: # 系统管理 (system_*)

## 查询工具

| 工具 | 必填 | 可选 | 说明 |
|------|------|------|------|
| `system_queryCurrentCompany` | — | — | 当前公司信息 |
| `system_queryMyCompanies` | — | — | 我的公司列表（含当前标注） |
| `system_queryDeptList` | — | `deptName`(模糊) `page` `limit` | 部门列表；获取deptId用于hr_* |
| `system_queryDeptTree` | — | — | 完整树形组织架构 |
| `system_queryMyPermissions` | — | — | 当前用户权限标识列表 |

## 切换公司

`system_switchCompany(companyId)`

**必填**：`companyId`（目标公司ID）

> 先调 `system_queryMyCompanies` 获取可切换的公司列表及 `companyId`

## 新增组织节点（新增部门）

`system_createDept(name, type, pid?, sort?, leaderId?)`

**必填**：`name`（名称）、`type`（类型）

**`type` 枚举**：`1` = 公司（并不是用户可切换的公司，这里只是一个分类的作用）。`2` = 部门（type=2 时 pid 必传，填写 **type=1 节点的 `id`**）

**可选字段**：

| 字段 | 说明 | 枚举值 |
|------|------|--------|
| `pid` | 上级组织节点ID(sys_org.id) | type=2时必填 |
| `sort` | 排序值 | 默认0 |
| `leaderId` | 负责人的用户ID | — |


path: scripts/navioffice.js
content: #!/usr/bin/env node

//# NAVIOFFICE CLI 工具
//# 使用 X-Api-Token 进行鉴权

const http = require('http');
const https = require('https');
const fs = require('fs');
const path = require('path');
const url = require('url');
const { promisify } = require('util');

// ── 常量定义 ───────────────────────────────────────────────────────

const ENV_FILE = '.env';
const DEFAULT_DOMAIN = 'https://oa.teredy.com/api';

// ── 全局变量 ───────────────────────────────────────────────────────

let config = {
  domain: DEFAULT_DOMAIN,
  apiToken: ''
};

// ── 辅助函数 ───────────────────────────────────────────────────────────

function die(message) {
  console.error(`错误: ${message}`);
  process.exit(1);
}

function info(message) {
  console.error(`:: ${message}`);
}

// 简单的 .env 文件加载
function loadEnvFile(envPath) {
  if (!fs.existsSync(envPath)) return;

  const content = fs.readFileSync(envPath, 'utf-8');
  const lines = content.split('\n');

  for (const line of lines) {
    const trimmed = line.trim();
    if (!trimmed || trimmed.startsWith('#')) continue;

    const idx = trimmed.indexOf('=');
    if (idx === -1) continue;

    const key = trimmed.substring(0, idx).trim();
    let value = trimmed.substring(idx + 1).trim();
    // 去除引号
    if ((value.startsWith('"') && value.endsWith('"')) ||
        (value.startsWith("'") && value.endsWith("'"))) {
      value = value.slice(1, -1);
    }
    process.env[key] = value;
  }
}

function loadEnv() {
  // 仅加载技能根目录 .env，避免误读当前工作目录中的无关凭证
  const skillDir = path.resolve(__dirname, '..');
  const envPath = path.join(skillDir, ENV_FILE);
  if (fs.existsSync(envPath)) {
    loadEnvFile(envPath);
  }
}

function validateDomain(domain) {
  const allowCustom = String(process.env.NAVI_OFFICE_ALLOW_CUSTOM_DOMAIN || '').toLowerCase() === 'true';
  const hostname = new URL(domain).hostname;
  if (!allowCustom && hostname !== 'https://oa.teredy.com/api') {
    die('NAVI_OFFICE_API_URL 非官方域名。若确认可信，请设置 NAVI_OFFICE_ALLOW_CUSTOM_DOMAIN=true');
  }
}

function initEnv() {
  loadEnv();

  config.domain = process.env.NAVI_OFFICE_API_URL || DEFAULT_DOMAIN;
  config.apiToken = process.env.NAVI_OFFICE_API_TOKEN || '';

  validateDomain(config.domain);

  if (!config.apiToken) die('未设置 NAVI_OFFICE_API_TOKEN');
}

function pagePayload(keyword = '') {
  return {
    current: 1,
    pageSize: 30,
    sort: {},
    combineSearch: { searchMode: 'AND', conditions: [] },
    keyword: keyword,
    viewId: 'ALL',
    filters: []
  };
}

// ── API 请求 ─────────────────────────────────────────────────────────

class APIError extends Error {
  constructor(statusCode, message) {
    super(message);
    this.statusCode = statusCode;
    this.name = 'APIError';
  }
}

function apiRequest(method, rawUrl, contentType, params, body) {
  return new Promise((resolve, reject) => {
    if (!config.apiToken) {
      die('未设置 NAVI_OFFICE_API_TOKEN');
    }

    // 构建完整 URL
    let fullUrl = rawUrl;
    if (!rawUrl.startsWith('http://') && !rawUrl.startsWith('https://')) {
      fullUrl = config.domain + rawUrl;
    }

    const parsedUrl = new URL(fullUrl);
    if (params) {
      Object.keys(params).forEach(key => parsedUrl.searchParams.set(key, params[key]));
    }

    const options = {
      hostname: parsedUrl.hostname,
      port: parsedUrl.port || (parsedUrl.protocol === 'https:' ? 443 : 80),
      path: parsedUrl.pathname + parsedUrl.search,
      method: method.toUpperCase(),
      headers: {
        'X-Api-Token': config.apiToken,
        'Content-Type': contentType
      }
    };

    const client = parsedUrl.protocol === 'https:' ? https : http;

    const req = client.request(options, (res) => {
      let data = '';
      res.on('data', chunk => data += chunk);
      res.on('end', () => {
        if (res.statusCode >= 400) {
          reject(new APIError(res.statusCode, data));
        } else {
          resolve(data);
        }
      });
    });

    req.on('error', reject);

    if (body) {
      const bodyStr = typeof body === 'string' ? body : JSON.stringify(body);
      req.write(bodyStr);
    }

    req.end();
  });
}

function api(method, rawUrl, params, body) {
  let bodyData = body;
  if (body && typeof body === 'object') {
    bodyData = JSON.stringify(body);
  }
  return apiRequest(method, rawUrl, 'application/json', params, bodyData);
}

// ── CRM 功能函数 ─────────────────────────────────────────────────────

async function crmList(module, opts = '') {
  const params = opts ? { opts } : {};
  return api('GET', `/${module}/view/list`, params, null);
}

async function crmGet(module, id) {
  return api('GET', `/${module}/${id}`, null, null);
}

async function crmContact(module, id) {
  return api('GET', `/${module}/contact/list/${id}`, null, null);
}

async function crmPage(module, payloadOrKeyword) {
  let body;
  if (payloadOrKeyword && payloadOrKeyword.startsWith('{')) {
    body = payloadOrKeyword;
  } else {
    body = pagePayload(payloadOrKeyword);
  }
  return api('POST', `/${module}/page`, null, body);
}

async function crmSearch(module, jsonData) {
  const body = (jsonData && jsonData.startsWith('{')) ? jsonData : pagePayload('');
  return api('POST', `/global/search/${module}`, null, body);
}

async function crmFollowPage(kind, module, payload) {
  if (kind !== 'plan' && kind !== 'record') {
    die('follow 只支持 plan/record');
  }
  const body = (payload && payload.startsWith('{')) ? payload : pagePayload('');
  return api('POST', `/${module}/follow/${kind}/page`, null, body);
}

async function crmProduct(keyword) {
  let body;
  if (keyword && keyword.startsWith('{')) {
    body = keyword;
  } else if (keyword) {
    body = pagePayload(keyword);
  } else {
    body = pagePayload('');
  }
  return api('POST', '/field/source/product', null, body);
}

async function crmOrg() {
  return api('GET', '/department/tree', null, null);
}

async function crmMembers(jsonData) {
  const body = (jsonData && jsonData.startsWith('{'))
    ? JSON.parse(jsonData)
    : {
        current: 1,
        pageSize: 30,
        combineSearch: { searchMode: 'AND', conditions: [] },
        keyword: '',
        departmentIds: [],
        filters: []
      };
  return api('POST', '/user/list', null, body);
}

async function rawAPI(method, path, extraArgs = []) {
  const params = {};
  for (let i = 0; i < extraArgs.length; i++) {
    const arg = extraArgs[i];
    if (arg.startsWith('-')) {
      if (i + 1 < extraArgs.length && !extraArgs[i + 1].startsWith('-')) {
        const key = arg.replace(/^-+/, '');
        params[key === 'X' ? 'X-Custom' : key] = extraArgs[i + 1];
        i++;
      }
    } else if (arg.includes('=')) {
      const [key, value] = arg.split('=');
      params[key] = value;
    }
  }
  return api(method.toUpperCase(), path, params, null);
}

// ── 命令处理 ─────────────────────────────────────────────────────────

async function handleCrmCommand(args) {
  if (args.length < 1) die('crm 需要子命令');

  const subCmd = args[0];
  const restArgs = args.slice(1);

  let result;
  let err;

  switch (subCmd) {
    case 'view':
      if (restArgs.length < 1) die('view 需要指定模块');
      const opts = restArgs[1] || '';
      result = await crmList(restArgs[0], opts);
      break;

    case 'get':
      if (restArgs.length < 2) die('get 需要 <模块> <ID>');
      result = await crmGet(restArgs[0], restArgs[1]);
      break;

    case 'search':
      if (restArgs.length < 1) die('search 需要指定模块');
      const searchJson = restArgs[1] || '';
      result = await crmSearch(restArgs[0], searchJson);
      break;

    case 'page':
      if (restArgs.length < 1) die('page 需要指定模块');
      const payload = restArgs[1] || '';
      result = await crmPage(restArgs[0], payload);
      break;

    case 'org':
      result = await crmOrg();
      break;

    case 'product':
      const keyword = restArgs[0] || '';
      result = await crmProduct(keyword);
      break;

    case 'members':
      if (restArgs.length < 1) die('members 需要部门ID JSON');
      result = await crmMembers(restArgs[0]);
      break;

    case 'contact':
      if (restArgs.length < 2) die('contact 需要 <模块> <ID>');
      result = await crmContact(restArgs[0], restArgs[1]);
      break;

    case 'follow':
      if (restArgs.length < 2) die('follow 需要 <plan|record> <模块>');
      const kind = restArgs[0];
      const mod = restArgs[1];
      const followPayload = restArgs[2] || '';
      result = await crmFollowPage(kind, mod, followPayload);
      break;

    default:
      die(`未知的 crm 子命令: ${subCmd}`);
  }

  console.log(result);
}

async function handleRawCommand(args) {
  if (args.length < 2) die('raw 需要 <方法> <路径>');
  const result = await rawAPI(args[0], args[1], args.slice(2));
  console.log(result);
}

function printUsage() {
  const usage = `NAVIOFFICE CLI 工具
使用 X-Api-Token 进行鉴权

用法:
  navioffice crm <子命令> [参数...]
  navioffice raw <方法> <路径> [curl参数...]

子命令:
  crm view <模块> [opts]         列出视图记录
  crm get <模块> <ID>            获取单条记录详情
  crm page <模块> [关键词|JSON]   列表分页记录
  crm search <模块> [JSON]        全局搜索记录
  crm org                        获取组织架构
  crm members <部门JSON>         获取部门成员列表
  crm follow <plan|record> <模块> [关键词|JSON]  查询跟进计划或记录
  crm product [关键词|JSON]       查询产品列表
  crm contact <模块> <ID>        获取联系人列表

支持的 CRM 一级模块:
  lead（线索）, opportunity（商机）, account（客户）, contact（联系人）, contract（合同）

示例:
  navioffice crm view lead
  navioffice crm page lead "测试"
  navioffice crm page lead '{"current":1,"pageSize":30,"keyword":"","viewId":"ALL","filters":[]}'
  navioffice crm search account '{"keyword":"测试"}'
  navioffice crm org
  navioffice crm members '{"departmentIds":["xxx"]}'
  navioffice crm follow plan lead '{"sourceId":"xxx"}'
  navioffice crm product "测试"
  navioffice crm contact account 'xxx'

支持的 CRM 二级模块:
  contract/payment-plan, invoice, contract/business-title, contract/payment-record, opportunity/quotation

原始 API:
  navioffice raw GET /settings/fields?module=account

环境变量要求:
  NAVI_OFFICE_API_TOKEN
`;
  console.log(usage);
}

// ── 主函数 ─────────────────────────────────────────────────────────

async function main() {
  const args = process.argv.slice(2);

  if (args.length === 0) {
    printUsage();
    process.exit(0);
  }

  initEnv();

  const command = args[0];
  const commandArgs = args.slice(1);

  try {
    switch (command) {
      case 'crm':
        await handleCrmCommand(commandArgs);
        break;
      case 'raw':
        await handleRawCommand(commandArgs);
        break;
      default:
        die(`未知的命令: ${command}`);
    }
  } catch (e) {
    if (e instanceof APIError) {
      die(`请求失败: HTTP ${e.statusCode} ${e.message}`);
    } else {
      die(e.message);
    }
  }
}

main();


path: scripts/navioffice.py
content: #!/usr/bin/env python3
"""
NAVIOFFICE CLI 工具
使用 X-Api-Token 进行鉴权
"""

import os
import sys
import json
import argparse
from pathlib import Path
from typing import Optional, Dict, Any
from urllib import parse, request
from urllib.error import HTTPError, URLError

try:
    from dotenv import load_dotenv
except ImportError:
    # 如果没有 python-dotenv，提供简单的 .env 加载实现
    def load_dotenv(dotenv_path=None):
        if dotenv_path and os.path.exists(dotenv_path):
            with open(dotenv_path) as f:
                for line in f:
                    line = line.strip()
                    if line and not line.startswith('#') and '=' in line:
                        key, value = line.split('=', 1)
                        os.environ[key.strip()] = value.strip().strip(
                            '"').strip("'")


# ── 路径配置 ──────────────────────────────────────────────────────
SCRIPT_DIR = Path(__file__).resolve().parent
SKILL_DIR = SCRIPT_DIR.parent
ENV_FILE = SKILL_DIR / ".env"

# 加载环境变量
if ENV_FILE.exists():
    load_dotenv(ENV_FILE)

NAVI_OFFICE_API_TOKEN = os.environ.get("NAVI_OFFICE_API_TOKEN", "")
NAVI_OFFICE_API_URL = os.environ.get("NAVI_OFFICE_API_URL", "https://oa.teredy.com/api")
NAVI_OFFICE_ALLOW_CUSTOM_DOMAIN = os.environ.get("NAVI_OFFICE_ALLOW_CUSTOM_DOMAIN", "false")

# ── 辅助函数 ───────────────────────────────────────────────────────────
def die(message: str) -> None:
    """打印错误信息并退出"""
    print(f"错误: {message}", file=sys.stderr)
    sys.exit(1)


def info(message: str) -> None:
    """打印信息"""
    print(f":: {message}", file=sys.stderr)


def validate_domain() -> None:
    """校验 API 域名，默认只允许官方域名"""
    try:
        hostname = parse.urlparse(NAVI_OFFICE_API_URL).hostname or ""
    except Exception:
        die("NAVI_OFFICE_API_URL 格式不正确")

    allow_custom = str(NAVI_OFFICE_ALLOW_CUSTOM_DOMAIN).lower() == "true"
    if not allow_custom and hostname != "https://oa.teredy.com/api":
        die("NAVI_OFFICE_API_URL 非官方域名。若确认可信，请设置 NAVI_OFFICE_ALLOW_CUSTOM_DOMAIN=true")


def check_keys() -> None:
    """检查必需的 API TOKEN"""
    if not NAVI_OFFICE_API_TOKEN:
        die("未设置 NAVI_OFFICE_API_TOKEN")
    validate_domain()


def page_payload(keyword: str = "") -> Dict[str, Any]:
    """生成分页请求的标准 payload"""
    return {
        "current": 1,
        "pageSize": 30,
        "sort": {},
        "combineSearch": {
            "searchMode": "AND",
            "conditions": []
        },
        "keyword": keyword,
        "viewId": "ALL",
        "filters": []
    }


# ── API 封装（Header Key 鉴权）────────────────────────────────────────
def api_request(method: str, url: str, content_type: str, **kwargs) -> str:
    """执行 API 请求"""
    check_keys()

    headers = {
        "X-Api-Token": NAVI_OFFICE_API_TOKEN,
        "Content-Type": content_type
    }

    # 合并用户提供的 headers（如果有）
    if 'headers' in kwargs:
        headers.update(kwargs.pop('headers'))

    params = kwargs.pop("params", None)
    data = kwargs.pop("data", None)

    if params:
        if isinstance(params, dict):
            query = parse.urlencode(params)
        else:
            query = str(params).lstrip("?")
        separator = "&" if "?" in url else "?"
        url = f"{url}{separator}{query}"

    data_bytes = None
    if data is not None:
        if isinstance(data, bytes):
            data_bytes = data
        elif isinstance(data, dict):
            data_bytes = parse.urlencode(data).encode("utf-8")
        else:
            data_bytes = str(data).encode("utf-8")

    try:
        req = request.Request(
            url=url,
            data=data_bytes,
            headers=headers,
            method=method.upper()
        )
        with request.urlopen(req) as response:
            charset = response.headers.get_content_charset() or "utf-8"
            return response.read().decode(charset, errors="replace")
    except HTTPError as e:
        try:
            detail = e.read().decode("utf-8", errors="replace")
        except Exception:
            detail = str(e)
        die(f"请求失败: HTTP {e.code} {detail}")
    except URLError as e:
        die(f"请求失败: {e}")


def api(method: str, url: str, **kwargs) -> str:
    """执行 JSON API 请求"""
    return api_request(method, url, "application/json", **kwargs)


def api_form(method: str, url: str, **kwargs) -> str:
    """执行表单 API 请求"""
    return api_request(
        method, url, "application/x-www-form-urlencoded", **kwargs)


# ── CRM 辅助函数 ──────────────────────────────────────────────────────
def crm_list(module: str, opts: str = "") -> str:
    """列出视图记录"""
    params = opts if opts else None
    return api("GET", f"{NAVI_OFFICE_API_URL}/{module}/view/list", params=params)


def crm_get(module: str, id: str) -> str:
    """获取单条记录详情"""
    return api("GET", f"{NAVI_OFFICE_API_URL}/{module}/{id}")


def crm_contact(module: str, id: str) -> str:
    """获取联系人列表"""
    return api("GET", f"{NAVI_OFFICE_API_URL}/{module}/contact/list/{id}")


def crm_page(module: str, payload_or_keyword: str = "") -> str:
    """列表分页记录"""
    # 判断是否为 JSON
    if payload_or_keyword.startswith("{"):
        body = payload_or_keyword
    else:
        body = json.dumps(page_payload(payload_or_keyword), ensure_ascii=False)

    path = f"{module}/page"
    return api("POST", f"{NAVI_OFFICE_API_URL}/{path}", data=body)


def crm_search(module: str, json_data: str = "") -> str:
    """全局搜索记录"""
    path = f"global/search/{module}"
    return api("POST", f"{NAVI_OFFICE_API_URL}/{path}", data=json_data)


def crm_follow_page(kind: str, module: str, payload: str = "") -> str:
    """查询跟进计划或跟进记录"""
    if kind not in ["plan", "record"]:
        die("follow 子命令只支持 plan/record")
    if not module:
        die(f"follow {kind} 需要指定模块（lead/account 等）")

    # 判断是否为 JSON
    if payload.startswith("{"):
        body = payload
    else:
        body = json.dumps(page_payload(payload), ensure_ascii=False)

    return api("POST", f"{NAVI_OFFICE_API_URL}/{module}/follow/{kind}/page", data=body)


def crm_product(keyword: str = "") -> str:
    """查询产品"""
    if keyword.startswith("{"):
        body = keyword
    else:
        body = json.dumps(page_payload(keyword), ensure_ascii=False)

    return api("POST", f"{NAVI_OFFICE_API_URL}/field/source/product", data=body)


def crm_org() -> str:
    """获取组织架构"""
    return api("GET", f"{NAVI_OFFICE_API_URL}/department/tree")


def crm_members(json_data: str) -> str:
    """根据部门ID获取成员"""
    return api("POST", f"{NAVI_OFFICE_API_URL}/user/list", data=json_data)


# ── 原始 API 调用 ─────────────────────────────────────────────────────
def raw_api(method: str, path: str, *args) -> str:
    """执行原始 API 调用"""
    if path.startswith("http"):
        url = path
    else:
        url = f"{NAVI_OFFICE_API_URL}{path}"

    # 这里简化处理，如果需要更多参数可以扩展
    return api(method, url)


# ── CLI 处理 ──────────────────────────────────────────────────────────
def print_usage():
    """打印使用帮助"""
    usage_text = """
navioffice — NAVIOFFICE CRM CLI 工具（X-Api-Token 模式）

使用方法:
  navioffice <命令> [参数...]

CRM 操作:
  crm view <模块> [参数]             列出视图记录（例：account/lead/opportunity）
  crm get <模块> <ID>               获取单条记录详情
  crm search <模块> [关键词|JSON]    全局搜索记录
  crm page <模块> [关键词|JSON]      列表分页记录 /<module>/page （例：account/lead/opportunity）
  crm org                          获取组织架构树
  crm members <部门IDs>             获取部门成员列表
  crm follow <plan|record> <模块> [关键词|JSON]  查询跟进计划或跟进记录
  crm product [关键词|JSON]         查询产品列表
  crm contact <模块> <ID>           获取联系人列表

支持的 CRM 一级模块:
 [lead（线索）, opportunity（商机）, account（客户）,contact（联系人）,contract（合同）]

列表查询示例:
  navioffice crm view lead
  navioffice crm page lead
  navioffice crm page lead "测试"
  navioffice crm page lead '{"current":1,"pageSize":30,"sort":{},"combineSearch":{"searchMode":"AND","conditions":[]},"keyword":"","viewId":"ALL","filters":[]}'
  navioffice crm page contract/payment-plan '{"current":1,"pageSize":30,"sort":{},"combineSearch":{"searchMode":"AND","conditions":[]},"keyword":"","viewId":"ALL","filters":[]}'
  navioffice crm search account '{"current":1,"pageSize":30,"combineSearch":{"searchMode":"AND","conditions":[]},"keyword":"xyz","viewId":"ALL","filters":[]}'
  navioffice crm org
  navioffice crm members '{"current":1,"pageSize":30,"combineSearch":{"searchMode":"AND","conditions":[]},"keyword":"","departmentIds":["deptId1","deptId2"],"filters":[]}'
  navioffice crm follow plan lead '{"sourceId":"927627065163785","current":1,"pageSize":10,"keyword":"","status":"ALL","myPlan":false}'
  navioffice crm follow record account '{"sourceId":"1751888184018919","current":1,"pageSize":10,"keyword":"","myPlan":false}'
  navioffice crm product "测试"
  navioffice crm contact account '927627065163785'

支持的 CRM 二级模块 :
  [contract/payment-plan(回款计划), invoice（发票）,contract/business-title(工商抬头）,contract/payment-record(回款记录), opportunity/quotation(报价单)]

列表查询示例：
  navioffice crm page contract/payment-plan
  navioffice crm page contract/business-title

原始 API:
  raw <方法> <路径> [curl参数...]
  navioffice raw GET /settings/fields?module=account

环境变量要求:
  NAVI_OFFICE_API_TOKEN
  NAVI_OFFICE_API_URL（可选，默认 https://oa.teredy.com/api）

"""
    print(usage_text)


def handle_crm_command(args: list) -> None:
    """处理 CRM 命令"""
    if not args:
        die("crm 需要子命令")

    sub_cmd = args[0]
    rest_args = args[1:]

    if sub_cmd == "view":
        if not rest_args:
            die("view 需要指定模块")
        module = rest_args[0]
        opts = rest_args[1] if len(rest_args) > 1 else ""
        print(crm_list(module, opts))

    elif sub_cmd == "get":
        if len(rest_args) < 2:
            die("get 需要 <模块> <ID>")
        print(crm_get(rest_args[0], rest_args[1]))

    elif sub_cmd == "search":
        if not rest_args:
            die("search 需要指定模块")
        module = rest_args[0]
        json_data = rest_args[1] if len(rest_args) > 1 else ""
        print(crm_search(module, json_data))

    elif sub_cmd == "page":
        if not rest_args:
            die("page 需要指定模块")
        module = rest_args[0]
        payload = rest_args[1] if len(rest_args) > 1 else ""
        print(crm_page(module, payload))

    elif sub_cmd == "org":
        print(crm_org())

    elif sub_cmd == "product":
        keyword = rest_args[0] if rest_args else ""
        print(crm_product(keyword))

    elif sub_cmd == "members":
        if not rest_args:
            die("members 需要部门ID JSON")
        print(crm_members(rest_args[0]))

    elif sub_cmd == "contact":
        if len(rest_args) < 2:
            die("contact 需要 <模块> <ID>")
        print(crm_contact(rest_args[0], rest_args[1]))

    elif sub_cmd == "follow":
        if not rest_args:
            die("follow 需要 plan 或 record")
        kind = rest_args[0]
        if kind not in ["plan", "record"]:
            die("follow 只支持 plan 或 record")
        if len(rest_args) < 2:
            die(f"follow {kind} 需要指定模块")
        module = rest_args[1]
        payload = rest_args[2] if len(rest_args) > 2 else ""
        print(crm_follow_page(kind, module, payload))

    else:
        die(f"未知的 crm 子命令: {sub_cmd}")


def handle_raw_command(args: list) -> None:
    """处理原始 API 命令"""
    if len(args) < 2:
        die("raw 需要 HTTP 方法和路径")

    method = args[0]
    path = args[1]
    print(raw_api(method, path))


def main():
    """主函数"""
    if len(sys.argv) < 2:
        print_usage()
        sys.exit(1)

    cmd = sys.argv[1]
    args = sys.argv[2:]

    if cmd == "crm":
        handle_crm_command(args)
    elif cmd == "raw":
        handle_raw_command(args)
    elif cmd in ["help", "-h", "--help"]:
        print_usage()
    else:
        die(f"未知命令: {cmd}（尝试 navioffice.py help）")


if __name__ == "__main__":
    main()


path: scripts/navioffice.sh
content: #!/usr/bin/env bash
# NAVIOFFICE CLI 工具
# 使用 X-Api-Token 进行鉴权
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)"
SKILL_DIR="$(dirname "$SCRIPT_DIR")"
ENV_FILE="${SKILL_DIR}/.env"

# ── 加载环境变量 ──────────────────────────────────────────────────────
if [[ -f "$ENV_FILE" ]]; then
  set -a
  source "$ENV_FILE"
  set +a
fi

NAVI_OFFICE_API_URL="${NAVI_OFFICE_API_URL:-https://oa.teredy.com/api}"
NAVI_OFFICE_ALLOW_CUSTOM_DOMAIN="${NAVI_OFFICE_ALLOW_CUSTOM_DOMAIN:-false}"

# ── 辅助函数 ───────────────────────────────────────────────────────────
die()  { echo "错误: $*" >&2; exit 1; }
info() { echo ":: $*" >&2; }

validate_domain() {
  local host
  host="$(python3 - <<'PY'
from urllib.parse import urlparse
import os
print((urlparse(os.environ.get('NAVI_OFFICE_API_URL','')).hostname or ''))
PY
)"
  if [[ "${NAVI_OFFICE_ALLOW_CUSTOM_DOMAIN,,}" != "true" && "$host" != "https://oa.teredy.com/api" ]]; then
    die "NAVI_OFFICE_API_URL 非官方域名。若确认可信，请设置 NAVI_OFFICE_ALLOW_CUSTOM_DOMAIN=true"
  fi
}

check_keys() {
  [[ -n "${NAVI_OFFICE_API_TOKEN:-}" ]] || die "未设置 NAVI_OFFICE_API_TOKEN"
  validate_domain
}

page_payload() {
  local keyword="${1:-}"
  python3 - "$keyword" <<PY
import json, sys
keyword = sys.argv[1] if len(sys.argv) > 1 else ""
payload = {
  "current": 1,
  "pageSize": 30,
  "sort": {},
  "combineSearch": {"searchMode": "AND", "conditions": []},
  "keyword": keyword,
  "viewId": "ALL",
  "filters": []
}
print(json.dumps(payload, ensure_ascii=False))
PY
}

# ── API 封装（Header Key 鉴权）────────────────────────────────────────
api_request() {
  local method="$1" url="$2" content_type="$3"
  shift 3

  check_keys

  # 通过 `"$@"` 保证额外的参数能够传递给 curl
  curl -s -X "$method" "$url" \
    -H "X-Api-[REDACTED_SECRET]" \
    -H "Content-Type: $content_type" \
    "$@"  # 传递剩余的所有参数
}

api() {
  api_request "$1" "$2" "application/json" "${@:3}"  # 传递 method, url 和其余的参数
}

api_form() {
  api_request "$1" "$2" "application/x-www-form-urlencoded" "${@:3}"  # 同样地，传递其余的参数
}

# ── CRM 辅助函数 ──────────────────────────────────────────────────────
crm_base="${NAVI_OFFICE_API_URL}"

crm_list() {
  local module="$1" opts="${2:-}"
  api GET "${NAVI_OFFICE_API_URL}/${module}/view/list" $opts
}

crm_get() {
  local module="$1" id="$2"
  api GET "${NAVI_OFFICE_API_URL}/${module}/${id}"
}

crm_contact() {
  local module="$1" id="$2"
  api GET "${NAVI_OFFICE_API_URL}/${module}/contact/list/${id}"
}

crm_page() {
  local module="$1"
  shift
  local first="${1:-}"
  local body
  if [[ "$first" == \{* ]]; then
    body="$first"
  else
    body=$(page_payload "${first:-}")
  fi
  local path="${module}/page"
  api POST "${NAVI_OFFICE_API_URL}/${path}" -d "$body"
}

crm_search() {
  local module="$1" json="${2:-}"
  local path="global/search/${module}"
  api POST "${NAVI_OFFICE_API_URL}/${path}" -d "$json"
}

crm_follow_page() {
  local kind="$1" module="$2" payload="${3:-}"

  [[ "${kind}" == "plan" || "${kind}" == "record" ]] || die "follow 子命令只支持 plan/record"
  [[ -n "${module}" ]] || die "follow ${kind} 需要指定模块（lead/account 等）"

  local body
  if [[ "${payload}" == \{* ]]; then
    body="${payload}"
  else
    body=$(page_payload "${payload}")
  fi

  api POST "${NAVI_OFFICE_API_URL}/${module}/follow/${kind}/page" -d "$body"
}

# 查询产品
crm_product() {
  local keyword="${1:-}"
  local body
  if [[ "$keyword" == \{* ]]; then
    body="$keyword"
  else
    body=$(page_payload "${keyword}")
  fi
  api POST "${NAVI_OFFICE_API_URL}/field/source/product" -d "$body"
}

# 获取组织架构
crm_org() {
  api GET "${NAVI_OFFICE_API_URL}/department/tree"
}

# 根据部门ID获取成员
crm_members() {
  api POST "${NAVI_OFFICE_API_URL}/user/list" -d "$1"
}

# ── 原始 API 调用 ─────────────────────────────────────────────────────
raw_api() {
  local method="$1" path="$2"
  shift 2

  if [[ "$path" == http* ]]; then
    api "$method" "$path" "$@"
  else
    api "$method" "${NAVI_OFFICE_API_URL}${path}" "$@"
  fi
}

# ── CLI 分发 ──────────────────────────────────────────────────────────
usage() {
  cat <<'EOF'
navioffice — NAVIOFFICE CRM CLI 工具（X-Api-Token 模式）

使用方法:
  navioffice <命令> [参数...]

CRM 操作:
  crm view <模块> [参数]             列出视图记录（例：account/lead/opportunity）
  crm get <模块> <ID>               获取单条记录详情
  crm search <模块> [关键词|JSON]    全局搜索记录
  crm page <模块> [关键词|JSON]      列表分页记录 /<module>/page （例：account/lead/opportunity）
  crm org                          获取组织架构树
  crm members <部门IDs>             获取部门成员列表
  crm follow <plan|record> <模块> [关键词|JSON]  查询跟进计划或跟进记录
  crm product [关键词|JSON]      查询产品列表
  crm contact <模块> <ID>             获取联系人列表

支持的 CRM 一级模块:
 [lead（线索）, opportunity（商机）, account（客户）,contact（联系人）,contract（合同）]

列表查询示例:
  navioffice crm view lead
  navioffice crm page lead
  navioffice crm page lead "测试"
  navioffice crm page lead '{"current":1,"pageSize":30,"sort":{},"combineSearch":{"searchMode":"AND","conditions":[]},"keyword":"","viewId":"ALL","filters":[]}'
  navioffice crm page contract/payment-plan '{"current":1,"pageSize":30,"sort":{},"combineSearch":{"searchMode":"AND","conditions":[]},"keyword":"","viewId":"ALL","filters":[]}'
  navioffice crm search account '{"current":1,"pageSize":30,"combineSearch":{"searchMode":"AND","conditions":[]},"keyword":"xyz","viewId":"ALL","filters":[]}'
  navioffice crm org
  navioffice crm members '{"current":1,"pageSize":30,"combineSearch":{"searchMode":"AND","conditions":[]},"keyword":"","departmentIds":["deptId1","deptId2"],"filters":[]}'
  navioffice crm follow plan lead '{"sourceId":"927627065163785","current":1,"pageSize":10,"keyword":"","status":"ALL","myPlan":false}'
  navioffice crm follow record account '{"sourceId":"1751888184018919","current":1,"pageSize":10,"keyword":"","myPlan":false}'
  navioffice crm product "测试"
  navioffice crm contact account '927627065163785'

支持的 CRM 二级模块 :
  [contract/payment-plan(回款计划), invoice（发票）,contract/business-title(工商抬头）,contract/payment-record(回款记录), opportunity/quotation(报价单)]

列表查询示例：
  navioffice crm page contract/payment-plan
  navioffice crm page contract/business-title

原始 API:
  raw <方法> <路径> [curl参数...]
  navioffice raw GET /settings/fields?module=account

环境变量要求:
  NAVI_OFFICE_API_TOKEN

EOF
}

cmd="${1:-}"
shift || true

case "$cmd" in
  crm)
    sub="${1:-}"; shift || die "crm 需要子命令"
    case "$sub" in
      view)    crm_list "$@" ;;
      get)     crm_get "$@" ;;
      search)  crm_search "$@" ;;
      page)    crm_page "$@" ;;
      org)     crm_org ;;
      product)  crm_product "$@" ;;
      members) crm_members "$@" ;;
      contact) crm_contact "$@" ;;
      follow)
        kind="${1:-}"; shift || die "follow 需要 plan 或 record"
        case "${kind}" in
          plan|record) crm_follow_page "${kind}" "$@" ;;
          *) die "follow 只支持 plan 或 record" ;;
        esac
        ;;
      *)       die "未知的 crm 子命令: $sub" ;;
    esac
    ;;
  raw)
    method="${1:-}"; shift || die "raw 需要 HTTP 方法"
    path="${1:-}"; shift || die "raw 需要路径"
    raw_api "$method" "$path" "$@"
    ;;
  help|-h|--help)
    usage
    ;;
  *)
    die "未知命令: $cmd（尝试 navioffice help）"
    ;;
esac

Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)