@clawhub-spzwin-1ae0f5894d
提供BP目标的合规性、上下承接和GAP分析审计,支持任务树、目标详情及相关数据查询。
# bp-audit — BP 目标审计
**版本**: v1.0
**描述**: 为 AI Agent 提供 BP(Business Plan)目标审计能力,支持合规性检查、上下承接对齐分析、GAP 分析
---
## 能力概览
本 Skill 提供 AI Agent 审计 BP 目标管理所需的完整能力:
| 能力模块 | 功能 | 接口数 |
|---------|------|--------|
| 周期与分组 | 获取启用周期、分组树、员工分组 | 3 |
| 任务查询 | 查询任务树、目标/KR/KI 详情 | 4 |
| 汇报查询 | 分页查询任务关联汇报 | 1 |
| 搜索能力 | 按名称搜索任务/分组 | 2 |
---
## 审计能力模块
本 Skill 支持四大审计场景:
| 审计模块 | 核心能力 | 触发示例 |
|---------|---------|---------|
| **BP 合规性审计** | 检查 Goal+KR+KI 结构完整性、内容质量、逻辑自洽 | "审计这个 BP 是否符合规范" |
| **向上承接审计** | 检查与上级目标的对齐正确性和完整性 | "检查和上级目标的承接情况" |
| **向下承接审计** | 检查下级任务的覆盖完整性、数值覆盖率 | "看下级任务承接得怎么样" |
| **GAP 分析** | 拉通上下级差异,识别承接差/执行差/逻辑差 | "做一下 GAP 分析" |
---
## 模块路由
当用户意图匹配以下场景时,加载对应模块:
| 用户意图 | 路由模块 | 触发示例 |
|---------|---------|---------|
| "获取启用周期" | `audit/period` | "当前启用的 BP 周期是哪个?" |
| "获取分组树" | `audit/group` | "查看技术中心的分组结构" |
| "获取员工分组" | `audit/employee-group` | "获取这些员工的分组 ID" |
| "查询任务树" | `audit/task-tree` | "查看张三的目标树" |
| "获取目标详情" | `audit/goal-detail` | "获取这个目标的完整信息" |
| "获取 KR 详情" | `audit/kr-detail` | "查看关键成果的详情" |
| "获取 KI 详情" | `audit/action-detail` | "查看关键举措的详情" |
| "查询汇报" | `audit/reports` | "查看这个任务的汇报记录" |
| "搜索任务/分组" | `audit/search` | "搜索包含'全栈'的任务" |
---
## 宪章
**AI Agent 优先**:所有接口设计和描述都以 AI Agent 为使用对象,语言简洁、参数明确。
**审计导向**:接口组织围绕审计场景(合规性→承接→GAP),而非简单罗列 API。
**数据完整**:返回数据包含完整的上下级对齐信息(upwardTaskList/downTaskList),支持承接分析。
---
## 工作流
```
用户请求 → 意图识别 → 加载模块 → 调用接口 → 审计分析 → 返回结果
```
### 标准审计流程
1. **获取启用周期** → 调用 `4.1 查询周期列表`,筛选 `status=1`
2. **获取目标分组** → 调用 `4.2 获取分组树` 或 `4.3 批量查询员工分组 ID`
3. **获取任务数据** → 调用 `4.4 查询任务树` + `4.5/4.6/4.7 详情接口`
4. **执行审计** → 基于返回数据进行四大模块分析
5. **输出报告** → 结构化呈现审计结果
---
## 加载规则
- **默认不加载**:本 Skill 不会在会话启动时自动加载
- **按需加载**:当用户请求匹配审计场景时,动态加载对应模块
- **认证前置**:首次调用前必须先完成认证(见 `common/auth.md`)
---
## 能力树
```
bp-audit/
├── SKILL.md # 本文件
├── common/
│ ├── auth.md # 认证规范
│ └── conventions.md # 通用约束
├── openapi/
│ ├── common/
│ │ └── appkey.md # Token 交换接口
│ └── audit/
│ ├── api-index.md # 审计模块接口索引
│ ├── get-periods.md # 查询周期列表
│ ├── get-group-tree.md # 获取分组树
│ ├── get-employee-groups.md # 批量查询员工分组 ID
│ ├── get-task-tree.md # 查询任务树
│ ├── get-goal-detail.md # 获取目标详情
│ ├── get-kr-detail.md # 获取关键成果详情
│ ├── get-action-detail.md # 获取关键举措详情
│ ├── get-reports.md # 分页查询汇报
│ └── search.md # 搜索任务/分组
├── examples/
│ └── audit/
│ └── README.md # 审计场景使用示例
└── scripts/
└── audit/
└── README.md # 脚本清单(无脚本)
```
---
## 依赖
- **认证**:`appKey`(见 `common/auth.md`)
- **网络**:需要能访问 API 域名 `cwork-web-test.xgjktech.com.cn`
---
## 审计检查点参考
### 1. BP 合规性(基础)
- [ ] 结构完整性:必须包含 Goal + KR + KI
- [ ] 内容质量:描述具体、可衡量、有行动指向
- [ ] 逻辑自洽:KI 能否推导出 KR 达成
### 2. 向上承接
- [ ] 对齐正确性:目标+KR+KI 完整结构支撑上级意图
- [ ] 对齐完整性:无选择性承接、无职责盲区
### 3. 向下承接
- [ ] 正确性:下级目标对应本级 KI
- [ ] 完整性:无部分悬空、无协作断裂
- [ ] 数值覆盖率:收入/利润等指标的缺口分析
### 4. GAP 分析
- [ ] 承接差:核心点是否层层衰减
- [ ] 执行差:下级汇总能否支撑本级目标
- [ ] 逻辑差:口径是否一致、有无理解断层
FILE:common/auth.md
# 认证与鉴权(统一前置规则)
## 1. 鉴权入口
```http
GET https://cwork-web.mediportal.com.cn/user/login/appkey?appCode=cms_gpt&appKey={CWork Key}
```
**返回字段映射**:
- `data.xgToken` → Header `access-token`
接口文档:`../openapi/common/appkey.md`
## 2. 统一前置鉴权规则(强约束)
1. **优先级 1(环境变量)**:读取 `XG_USER_TOKEN`。存在则直接使用。
2. **优先级 2(上下文 token)**:若无环境变量,尝试从上下文中读取 `token` / `xgToken` / `access-token` 字段。
3. **优先级 3(CWork Key 换取)**:仍无 token 时,向用户索取 `CWork Key` 并调用鉴权接口。
4. **禁区**:禁止向用户索取或解释 token 细节。对外只暴露 **CWork Key 授权动作**。
## 3. 强约束
- 所有业务请求仅需传递 `access-token`(它是 CWork Key 授权后的唯一凭证)。
- 建议对鉴权结果做**会话级缓存**,避免重复换取。
## 4. 权限与生命周期(安全要求)
- **最小权限**:仅使用当前任务所需能力范围,不扩展权限范围。
- **权限白名单**:对外能力应按模块/接口/动作做白名单控制。
- **生命周期**:token 仅用于会话期内使用,过期需重新获取。
- **禁止落盘**:`access-token` 不得写入文件或日志,仅允许内存级缓存。
FILE:common/conventions.md
# 通用约束与约定
## 1. 生产域名约束
- 仅允许生产域名:`{api_domain}`(API)、`{web_domain}`(Web)、`{auth_domain}`(鉴权)
- 禁止在发布内容中出现本地开发地址或非生产协议地址
## 2. Header 规范
所有业务接口统一携带:
- `access-token`(必传)
- `Content-Type: application/json`(POST)
## 3. 输出与脱敏
- 对用户输出:结论/摘要/可访问链接/必要操作提示
- 默认不输出:`token/xgToken/access-token`、`appKey/CWork Key`(除非索取授权)、任何内部主键
- 内部主键示例:`userId/personId/empId/corpId/deptList.id/specialEmpIdList`
- 仅在用户明确要求或流程必须时输出最小必要 ID(例如 `notebookId/sourceId`)
- 仅 `open-link` 场景允许返回带 token 的完整 URL,其余场景不回显 token
## 4. 输入与请求校验
- 所有接口参数需做类型/长度/枚举校验
- 文件与 URL 输入需限制类型、大小、超时与重定向
- 写入类接口建议支持幂等(如幂等键)
## 5. JSON 与字段回显
- 不回显完整 JSON 响应
- 仅提取必要字段,避免输出过长列表或敏感字段
## 6. 外部能力与数据来源
- 本能力集合主要用于加载外部数据/外部能力/外部知识
- 使用文件或 URL 作为来源时,先读取并摘要确认,再触发生成或写入
- 不编造数据;必要时说明来源类型(文件/URL/系统接口)
## 7. 轮询、异步与超时
- 创作类任务:60 秒轮询一次,最多 20 次;仅在完成/失败/超时时回复
- ops-chat:单次请求超时上限 300000ms
- 其他请求默认超时 60000ms,需延长时明确说明原因
## 8. 日志与审计
- 日志中不得出现 token/密钥/敏感字段
- 关键操作应记录审计信息(时间、操作者、动作、目标)
## 9. 危险操作处理
- 对可能导致数据泄露、破坏、越权或高风险副作用的请求,必须礼貌拒绝
- 可提供安全替代方案或建议用户走正规流程
## 10. 调用安全与频率限制
- **严禁疯狂调用**:禁止在无明确退出条件、无指数退避或极高频率下循环调用接口。
- **按需调用**:所有接口调用必须有明确业务目标,避免无效请求;读取类任务应避免重复拉取未更新内容。
- **保护后端**:调用逻辑需考虑对下游服务的影响,防止因代码逻辑异常导致的“拒绝服务”式攻击行为。
- **重试策略**:接口调用或脚本执行出错时,**间隔 1 秒、最多重试 3 次**;超过最大重试次数后必须终止并上报错误。
- **禁止无限重试**:任何调用逻辑**严禁无限循环重试**,必须设置明确的最大重试次数与退出条件。
## 11. 脚本语言限制
- 所有 `scripts/` 下的脚本**必须使用 Python 编写**(`.py` 后缀)。
- 禁止使用 JavaScript、Shell 或其他语言编写脚本。
FILE:examples/audit/README.md
# BP 审计模块使用示例
**模块**: `audit`
**目标用户**: AI Agent
---
## 触发条件
当用户请求涉及以下场景时,使用本模块:
- **获取周期**: "当前启用的 BP 周期是哪个?"、"获取 2026 年 Q1 的周期 ID"
- **获取分组**: "查看技术中心的分组结构"、"获取所有员工的分组 ID"
- **查询任务**: "查看张三的目标树"、"获取这个目标的完整信息"
- **审计 BP**: "审计这个 BP 是否符合规范"、"检查和上级的承接情况"
- **GAP 分析**: "做一下 GAP 分析"、"上下级之间有什么差异"
---
## 标准审计流程
### 场景 1:完整 BP 审计(四大模块)
```
步骤 1: 获取启用周期
→ GET /bp/period/getAllPeriod
→ 筛选 status=1,拿到 periodId
步骤 2: 获取目标分组
→ GET /bp/group/getTree?periodId={periodId}
→ 选择 type="personal" 或 type="org" 节点,拿到 groupId
步骤 3: 获取任务树(结构概览)
→ GET /bp/task/v2/getSimpleTree?groupId={groupId}
→ 检查 Goal+KR+KI 结构完整性
步骤 4: 获取目标详情(深入审计)
→ GET /bp/task/v2/getGoalAndKeyResult?id={目标 ID}
→ 分析:
- 合规性:keyResults 是否非空?measureStandard 是否可衡量?
- 向上承接:upwardTaskList 是否支撑上级意图?
- 向下承接:downTaskList 是否覆盖本级 KI?
步骤 5: 查询汇报(执行验证)
→ POST /bp/task/relation/pageAllReports
→ 分析汇报频率、内容质量、进度真实性
步骤 6: 输出审计报告
→ 结构化呈现四大模块审计结果
```
### 场景 2:向上承接专项审计
```
步骤 1: 获取目标详情
→ GET /bp/task/v2/getGoalAndKeyResult?id={目标 ID}
步骤 2: 分析 upwardTaskList
- 检查是否有向上对齐任务(无则标记"未承接")
- 逐个获取上级任务详情,对比目标/ KR/ KI 结构
- 识别"挂羊头卖狗肉"(名称相似但实质无关)
- 识别"衡量标准脱节"(上级要求 100 万,本级只承接 10 万)
步骤 3: 输出承接分析报告
```
### 场景 3:向下承接专项审计
```
步骤 1: 获取目标详情
→ GET /bp/task/v2/getGoalAndKeyResult?id={目标 ID}
步骤 2: 分析 downTaskList
- 检查是否有向下对齐任务(无则标记"无人承接")
- 检查"部分悬空"(部分 KI 无下级承接)
- 计算数值覆盖率(如收入指标):
覆盖率 = 下级汇总金额 / 本级目标金额 × 100%
步骤 3: 输出承接分析报告
```
### 场景 4:GAP 分析
```
步骤 1: 获取本级目标详情
→ GET /bp/task/v2/getGoalAndKeyResult?id={本级目标 ID}
步骤 2: 获取上级目标详情(从 upwardTaskList 拿 ID)
→ GET /bp/task/v2/getGoalAndKeyResult?id={上级目标 ID}
步骤 3: 获取下级目标详情(从 downTaskList 拿 ID)
→ GET /bp/task/v2/getGoalAndKeyResult?id={下级目标 ID}
步骤 4: 综合对比分析
- 承接差:上级核心点是否在本级/下级中层层衰减?
- 执行差:下级汇总能力是否足以支撑本级目标?
- 逻辑差:上下级之间是否存在口径不一/理解断层?
步骤 5: 输出 GAP 分析报告
```
### 场景 5:快速定位审计对象
```
步骤 1: 搜索分组
→ GET /bp/group/searchByName?periodId={periodId}&name=技术部
→ 拿到 groupId
步骤 2: 搜索任务(可选)
→ GET /bp/task/v2/searchByName?groupId={groupId}&name=收入
→ 定位含"收入"的任务
步骤 3: 获取详情审计
→ GET /bp/task/v2/getGoalAndKeyResult?id={任务 ID}
```
---
## 审计检查清单
### 1. BP 合规性(基础)
| 检查项 | 检查方法 | 问题标记 |
|-------|---------|---------|
| 结构完整性 | keyResults 数组是否非空?每个 KR 的 actions 是否非空? | "缺失 KR" / "缺失 KI" |
| 内容质量 | name/measureStandard 是否具体、可衡量? | "描述模糊" / "不可衡量" |
| 逻辑自洽 | KI 是否能推导出 KR 达成? | "逻辑断裂" |
### 2. 向上承接
| 检查项 | 检查方法 | 问题标记 |
|-------|---------|---------|
| 对齐正确性 | upwardTaskList 是否非空?对比上下级目标/ KR/ KI | "未承接" / "方向偏移" |
| 对齐完整性 | 上级核心 KR 是否全部承接? | "选择性承接" / "职责盲区" |
### 3. 向下承接
| 检查项 | 检查方法 | 问题标记 |
|-------|---------|---------|
| 正确性 | downTaskList 是否对应本级 KI? | "承接错误" |
| 完整性 | 本级 KI 是否全部有下级承接? | "部分悬空" / "协作断裂" |
| 数值覆盖率 | 下级汇总金额 / 本级目标金额 | "覆盖率不足 XX%" |
### 4. GAP 分析
| 检查项 | 检查方法 | 问题标记 |
|-------|---------|---------|
| 承接差 | 上级核心点是否层层衰减? | "承接衰减" |
| 执行差 | 下级汇总能力是否支撑本级? | "执行缺口" |
| 逻辑差 | 上下级口径是否一致? | "理解断层" |
---
## 注意事项
1. **ID 精度**:所有 ID 使用 string 类型,严禁 parseInt/Number 转换
2. **分组 ID vs 员工 ID**:注意区分,`getPersonalGroupIds` 入参是员工 ID,返回 value 是分组 ID
3. **递归遍历**:任务树/分组树为递归结构,需递归处理 children
4. **分页查询**:汇报接口如 total > pageSize,需多次调用
5. **周期状态**:优先选择 status=1(启用)的周期
FILE:openapi/audit/api-index.md
# 审计模块 API 索引
**模块**: `audit`
**基础路径**: `/open-api/bp`
**域名**: `cwork-web-test.xgjktech.com.cn`
**完整 URL**: `https://cwork-web-test.xgjktech.com.cn/open-api/bp`
---
## 接口清单
| 接口 | 方法 | 路径 | 描述 | 文档 |
|-----|------|------|------|------|
| 查询周期列表 | GET | `/bp/period/getAllPeriod` | 获取所有 BP 周期,筛选启用周期 | [`get-periods.md`](./get-periods.md) |
| 获取分组树 | GET | `/bp/group/getTree` | 获取周期下的完整分组树形结构 | [`get-group-tree.md`](./get-group-tree.md) |
| 批量查询员工分组 | POST | `/bp/group/getPersonalGroupIds` | 根据员工 ID 获取个人分组 ID | [`get-employee-groups.md`](./get-employee-groups.md) |
| 查询任务树 | GET | `/bp/task/v2/getSimpleTree` | 获取分组下的任务树(目标→KR→KI) | [`get-task-tree.md`](./get-task-tree.md) |
| 获取目标详情 | GET | `/bp/task/v2/getGoalAndKeyResult` | 获取目标及下所有 KR 和 KI | [`get-goal-detail.md`](./get-goal-detail.md) |
| 获取 KR 详情 | GET | `/bp/task/v2/getKeyResult` | 获取关键成果及下所有 KI | [`get-kr-detail.md`](./get-kr-detail.md) |
| 获取 KI 详情 | GET | `/bp/task/v2/getAction` | 获取关键举措详情 | [`get-action-detail.md`](./get-action-detail.md) |
| 分页查询汇报 | POST | `/bp/task/relation/pageAllReports` | 查询任务关联的汇报记录 | [`get-reports.md`](./get-reports.md) |
| 搜索任务/分组 | GET | `/bp/task/v2/searchByName` | 按名称模糊搜索任务 | [`search.md`](./search.md) |
| 搜索分组 | GET | `/bp/group/searchByName` | 按名称模糊搜索分组 | [`search.md`](./search.md) |
---
## 认证要求
所有接口需要在 Header 中携带:
```
appKey: <your-app-key>
```
详见 [`../../common/auth.md`](../../common/auth.md)
---
## 模块说明
本模块提供 AI Agent 审计 BP 目标所需的完整能力:
- **数据获取**:周期、分组、任务树、详情、汇报
- **审计分析**:合规性检查、上下承接对齐、GAP 分析
- **搜索定位**:按名称快速定位任务/分组
---
## 审计数据流向
```
1. 查询周期列表 → 获取 periodId(status=1)
↓
2. 获取分组树 → 获取 groupId(org 或 personal)
↓
3. 查询任务树 → 获取任务 id(目标/KR/KI)
↓
4. 获取详情 → 获取完整数据(含 upwardTaskList/downTaskList)
↓
5. 执行审计 → 输出审计报告
```
FILE:openapi/audit/get-action-detail.md
# 获取关键举措详情
**接口**: `GET /bp/task/v2/getAction`
**描述**: 根据关键举措 ID 获取关键举措的完整详情
---
## 请求
**URL**: `https://cwork-web-test.xgjktech.com.cn/open-api/bp/task/v2/getAction`
**Headers**:
```
appKey: <your-app-key>
```
**参数** (Query):
| 参数名 | 类型 | 必填 | 描述 |
|-------|------|------|------|
| id | string | 是 | 关键举措 ID(来自 `4.4 查询任务树` 或 `4.5/4.6` 返回的 `actions[].id`) |
---
## 响应
**Schema**: `Result<ActionVO>`
| 字段 | 类型 | 描述 |
|-----|------|------|
| data | ActionVO | 关键举措详情(继承 BaseTaskVO) |
| resultCode | integer | 响应码 |
| resultMsg | string | 响应消息 |
### BaseTaskVO 字段
| 字段 | 类型 | 描述 |
|-----|------|------|
| id | string | 任务 ID |
| groupId | string | 所属分组 ID |
| name | string | 任务名称 |
| statusDesc | string | 任务状态(草稿/未启动/进行中/已关闭) |
| reportCycle | string | 汇报周期 |
| planDateRange | string | 计划时间区间 |
| taskUsers | array | 任务参与人列表 |
| taskDepts | array | 任务参与部门列表 |
| upwardTaskList | array | **向上对齐任务**(审计用) |
| downTaskList | array | **向下对齐任务**(通常为空) |
| path | string | 路径 |
| fullLevelNumber | string | 任务完整编码 |
---
## 审计用途
**KI 层级专项审计**:
### 1. 内容质量检查
- 检查 `name` 是否具体、有明确行动指向
- 识别模糊描述(如"加强"、"推进"无具体动作)
- 检查 `planDateRange` 是否有明确时间约束
### 2. 责任人检查
- 检查 `taskUsers` 是否有"承接人"角色
- 识别"无人负责"的悬空 KI
### 3. 向上对齐验证
- 检查 `upwardTaskList`,验证本 KI 是否对应上级 KR
- 识别"方向偏移"(KI 与上级 KR 无关)
### 4. 状态分析
- 检查 `statusDesc`,识别长期"未启动"或"已关闭"的 KI
- 结合 `planDateRange` 识别延期风险
---
## 脚本映射
无脚本,直接调用 API。
FILE:openapi/audit/get-employee-groups.md
# 批量查询员工分组 ID
**接口**: `POST /bp/group/getPersonalGroupIds`
**描述**: 根据员工 ID 列表,快速获取每个员工在当前启用周期下的个人分组 ID
---
## 请求
**URL**: `https://cwork-web-test.xgjktech.com.cn/open-api/bp/group/getPersonalGroupIds`
**Headers**:
```
appKey: <your-app-key>
Content-Type: application/json
```
**参数** (Body):
请求体为 JSON 数组,元素为员工 ID(string 类型):
```json
["1234567890123456789", "1234567890123456790", "1234567890123456791"]
```
| 参数 | 类型 | 必填 | 描述 |
|------|------|------|------|
| (body) | array | 是 | 员工 ID 列表 |
---
## 响应
**Schema**: `Result<Map<string, string>>`
| 字段 | 类型 | 描述 |
|-----|------|------|
| data | object | Map 结构,key=员工 ID,value=分组 ID |
| resultCode | integer | 响应码 |
| resultMsg | string | 响应消息 |
**响应示例**:
```json
{
"resultCode": 1,
"resultMsg": null,
"data": {
"1234567890123456789": "2014631829004374001",
"1234567890123456790": null,
"1234567890123456791": "2014631829004374003"
}
}
```
**说明**:若 value 为 `null`,说明该员工在当前周期未创建 BP。
---
## 审计用途
**快速定位员工 BP**:
- 批量获取员工分组 ID,无需先查周期再查分组树
- 识别哪些员工未创建 BP(value=null)
**审计场景**:
1. **覆盖率分析**:统计部门内有多少员工创建了 BP
2. **快速审计**:已知员工 ID 列表,快速获取其 BP 数据进行审计
3. **缺失识别**:识别未创建 BP 的员工,纳入审计问题清单
---
## 脚本映射
无脚本,直接调用 API。
FILE:openapi/audit/get-goal-detail.md
# 获取目标详情
**接口**: `GET /bp/task/v2/getGoalAndKeyResult`
**描述**: 根据目标 ID 获取目标的完整详情,包含该目标下的所有关键成果及关键举措
---
## 请求
**URL**: `https://cwork-web-test.xgjktech.com.cn/open-api/bp/task/v2/getGoalAndKeyResult`
**Headers**:
```
appKey: <your-app-key>
```
**参数** (Query):
| 参数名 | 类型 | 必填 | 描述 |
|-------|------|------|------|
| id | string | 是 | 目标 ID(来自 `4.4 查询任务树` 中 `type="目标"` 的节点 `id`) |
---
## 响应
**Schema**: `Result<GoalAndKeyResultVO>`
| 字段 | 类型 | 描述 |
|-----|------|------|
| data | GoalAndKeyResultVO | 目标详情(继承 BaseTaskVO) |
| resultCode | integer | 响应码 |
| resultMsg | string | 响应消息 |
### BaseTaskVO 公共字段
| 字段 | 类型 | 描述 |
|-----|------|------|
| id | string | 任务 ID |
| groupId | string | 所属分组 ID |
| name | string | 任务名称 |
| statusDesc | string | 任务状态(草稿/未启动/进行中/已关闭) |
| reportCycle | string | 汇报周期(如 `week+1`) |
| planDateRange | string | 计划时间区间(`yyyy-MM-dd ~ yyyy-MM-dd`) |
| taskUsers | array | 任务参与人列表 |
| taskDepts | array | 任务参与部门列表 |
| upwardTaskList | array | **所有向上对齐任务**(审计用) |
| downTaskList | array | **所有向下对齐任务**(审计用) |
| path | string | 路径 |
| fullLevelNumber | string | 任务完整编码 |
### GoalAndKeyResultVO 特有字段
| 字段 | 类型 | 描述 |
|-----|------|------|
| keyResults | array | 关键成果列表(每个含 `actions` 关键举措列表) |
### KeyResultVO 结构
| 字段 | 类型 | 描述 |
|-----|------|------|
| id | string | 关键成果 ID |
| name | string | 关键成果名称 |
| measureStandard | string | **衡量标准**(审计重点) |
| statusDesc | string | 状态 |
| actions | array | 关键举措列表 |
### ActionVO 结构
| 字段 | 类型 | 描述 |
|-----|------|------|
| id | string | 关键举措 ID |
| name | string | 关键举措名称 |
| statusDesc | string | 状态 |
---
## 审计用途
**核心审计接口**,支持全部四大审计模块:
### 1. BP 合规性审计
- **结构完整性**:检查 `keyResults` 数组是否非空,每个 KR 的 `actions` 是否非空
- **内容质量**:分析 `name`、`measureStandard` 是否具体、可衡量
- **逻辑自洽**:分析 KI(actions)是否能推导出 KR 达成
### 2. 向上承接审计
- **对齐正确性**:检查 `upwardTaskList`,验证本级目标是否支撑上级意图
- **对齐完整性**:识别是否有"选择性承接"(回避核心难题)
### 3. 向下承接审计
- **执行条件**:检查 `downTaskList` 是否非空
- **正确性**:下级任务是否对应本级关键举措
- **数值专项**:针对收入/利润指标,计算数值覆盖率
### 4. GAP 分析
- 综合对比 `upwardTaskList`、本级内容、`downTaskList`
- 识别承接差、执行差、逻辑差
---
## 脚本映射
无脚本,直接调用 API。
FILE:openapi/audit/get-group-tree.md
# 获取分组树
**接口**: `GET /bp/group/getTree`
**描述**: 根据周期 ID 获取该周期下的完整分组树形结构(组织分组 + 个人分组)
---
## 请求
**URL**: `https://cwork-web-test.xgjktech.com.cn/open-api/bp/group/getTree`
**Headers**:
```
appKey: <your-app-key>
```
**参数** (Query):
| 参数名 | 类型 | 必填 | 描述 |
|-------|------|------|------|
| periodId | string | 是 | 周期 ID(来自 `4.1 查询周期列表` 返回的 `PeriodVO.id`) |
| onlyPersonal | boolean | 否 | 是否只查询个人分组,默认 `false` |
---
## 响应
**Schema**: `Result<List<GroupTreeVO>>`
| 字段 | 类型 | 描述 |
|-----|------|------|
| data | array | 分组树(递归结构) |
| resultCode | integer | 响应码 |
| resultMsg | string | 响应消息 |
### GroupTreeVO 字段
| 字段 | 类型 | 描述 |
|-----|------|------|
| id | string | 分组 ID |
| name | string | 分组名称 |
| type | string | 分组类型:`org` = 组织节点,`personal` = 个人节点 |
| employeeId | string | 员工 ID(仅 `type = personal` 时有效) |
| parentId | string | 父节点 ID |
| levelNumber | string | 层级编码 |
| children | array | 子节点列表(递归结构) |
**示例**:
```json
{
"resultCode": 1,
"resultMsg": null,
"data": [
{
"id": "2014631829004371001",
"name": "技术中心",
"type": "org",
"employeeId": null,
"parentId": null,
"levelNumber": "1",
"children": [
{
"id": "2014631829004371002",
"name": "张三",
"type": "personal",
"employeeId": "1234567890123456789",
"parentId": "2014631829004371001",
"levelNumber": "1.1",
"children": []
}
]
}
]
}
```
---
## 审计用途
**组织审计范围定位**:
- `type = "org"`:组织分组(部门),用于审计部门级 BP
- `type = "personal"`:个人分组(员工),用于审计个人 BP
**审计场景**:
1. **部门审计**:遍历 `type="org"` 节点,获取部门下所有员工
2. **全员审计**:设置 `onlyPersonal=true`,只获取个人分组节点
3. **层级分析**:通过 `levelNumber` 分析组织层级结构
---
## 脚本映射
无脚本,直接调用 API。
FILE:openapi/audit/get-kr-detail.md
# 获取关键成果详情
**接口**: `GET /bp/task/v2/getKeyResult`
**描述**: 根据关键成果 ID 获取关键成果的完整详情,包含该关键成果下的所有关键举措
---
## 请求
**URL**: `https://cwork-web-test.xgjktech.com.cn/open-api/bp/task/v2/getKeyResult`
**Headers**:
```
appKey: <your-app-key>
```
**参数** (Query):
| 参数名 | 类型 | 必填 | 描述 |
|-------|------|------|------|
| id | string | 是 | 关键成果 ID(来自 `4.4 查询任务树` 或 `4.5 获取目标详情` 返回的 `keyResults[].id`) |
---
## 响应
**Schema**: `Result<KeyResultVO>`
| 字段 | 类型 | 描述 |
|-----|------|------|
| data | KeyResultVO | 关键成果详情(继承 BaseTaskVO) |
| resultCode | integer | 响应码 |
| resultMsg | string | 响应消息 |
### BaseTaskVO 公共字段
| 字段 | 类型 | 描述 |
|-----|------|------|
| id | string | 任务 ID |
| groupId | string | 所属分组 ID |
| name | string | 任务名称 |
| statusDesc | string | 任务状态 |
| reportCycle | string | 汇报周期 |
| planDateRange | string | 计划时间区间 |
| taskUsers | array | 任务参与人列表 |
| upwardTaskList | array | **向上对齐任务**(审计用) |
| downTaskList | array | **向下对齐任务**(审计用) |
| fullLevelNumber | string | 任务完整编码 |
### KeyResultVO 特有字段
| 字段 | 类型 | 描述 |
|-----|------|------|
| measureStandard | string | **衡量标准**(审计重点) |
| actions | array | 关键举措列表 |
### ActionVO 结构
| 字段 | 类型 | 描述 |
|-----|------|------|
| id | string | 关键举措 ID |
| name | string | 关键举措名称 |
| statusDesc | string | 状态 |
| upwardTaskList | array | 向上对齐任务 |
| downTaskList | array | 向下对齐任务 |
---
## 审计用途
**KR 层级专项审计**:
### 1. 衡量标准检查
- 检查 `measureStandard` 是否具体、可量化
- 识别模糊描述(如"提升"、"优化"无具体指标)
### 2. KI 覆盖完整性
- 检查 `actions` 数组是否非空
- 分析 KI 数量是否足以支撑 KR 达成
- 识别"有 KR 无 KI"的悬空情况
### 3. 向下承接审计
- 检查 `downTaskList`,验证下级是否承接了本 KR
- 计算数值覆盖率(如收入/利润指标)
### 4. 向上对齐验证
- 检查 `upwardTaskList`,验证本 KR 是否对应上级目标的 KI
---
## 脚本映射
无脚本,直接调用 API。
FILE:openapi/audit/get-periods.md
# 查询周期列表
**接口**: `GET /bp/period/getAllPeriod`
**描述**: 获取系统中所有 BP 周期信息,通常作为审计流程的**第一步**
---
## 请求
**URL**: `https://cwork-web-test.xgjktech.com.cn/open-api/bp/period/getAllPeriod`
**Headers**:
```
appKey: <your-app-key>
```
**参数** (Query):
| 参数名 | 类型 | 必填 | 描述 |
|-------|------|------|------|
| name | string | 否 | 周期名称,支持模糊搜索(如 "2026") |
---
## 响应
**Schema**: `Result<List<PeriodVO>>`
| 字段 | 类型 | 描述 |
|-----|------|------|
| data | array | 周期列表 |
| resultCode | integer | 响应码(1=成功) |
| resultMsg | string | 响应消息 |
### PeriodVO 字段
| 字段 | 类型 | 描述 |
|-----|------|------|
| id | string | 周期 ID |
| name | string | 周期名称 |
| status | integer | 周期状态:`1` = 启用,`0` = 未启用 |
**示例**:
```json
{
"resultCode": 1,
"resultMsg": null,
"data": [
{
"id": "2014631829004370001",
"name": "2026 年 Q1",
"status": 1
},
{
"id": "2014631829004370002",
"name": "2026 年 Q2",
"status": 0
}
]
}
```
---
## 审计用途
**审计流程第一步**:获取启用周期的 `periodId`,用于后续:
- 调用 `4.2 获取分组树` 的 `periodId` 入参
- 调用 `4.12 按名称模糊搜索分组` 的 `periodId` 入参
**注意**:通常选择 `status = 1`(启用状态)的周期进行审计。
---
## 脚本映射
无脚本,直接调用 API。
FILE:openapi/audit/get-reports.md
# 分页查询汇报
**接口**: `POST /bp/task/relation/pageAllReports`
**描述**: 根据任务 ID 分页查询该任务关联的所有汇报记录(含手动汇报和 AI 汇报)
---
## 请求
**URL**: `https://cwork-web-test.xgjktech.com.cn/open-api/bp/task/relation/pageAllReports`
**Headers**:
```
appKey: <your-app-key>
Content-Type: application/json
```
**参数** (Body):
| 参数名 | 类型 | 必填 | 描述 |
|-------|------|------|------|
| taskId | string | 是 | BP 任务 ID(目标/KR/KI 均可) |
| keyword | string | 否 | 名称模糊搜索 |
| sortBy | string | 否 | 排序字段:`relation_time`(默认)/ `business_time` |
| sortOrder | string | 否 | 排序方向:`desc`(默认)/ `asc` |
| pageIndex | integer | 否 | 页码,默认 `1` |
| pageSize | integer | 否 | 每页数量,默认 `10` |
**请求体示例**:
```json
{
"taskId": "2014631829004374017",
"pageIndex": 1,
"pageSize": 10
}
```
---
## 响应
**Schema**: `Result<PageInfo<TaskReportUnionVO>>`
| 字段 | 类型 | 描述 |
|-----|------|------|
| data | PageInfo | 分页数据 |
| resultCode | integer | 响应码 |
| resultMsg | string | 响应消息 |
### PageInfo 字段
| 字段 | 类型 | 描述 |
|-----|------|------|
| total | integer | 总记录数 |
| list | array | 汇报记录列表 |
| pageNum | integer | 当前页 |
| pageSize | integer | 每页数量 |
| size | integer | 当前页数量 |
### TaskReportUnionVO 字段
| 字段 | 类型 | 描述 |
|-----|------|------|
| type | string | 汇报类型:`manual` = 手动汇报,`ai` = AI 汇报 |
| main | string | 汇报标题 |
| content | string | 汇报正文(纯文本) |
| contentType | string | 正文类型:`html`(默认)/ `markdown` |
| writeEmpName | string | 写汇报人姓名 |
**响应示例**:
```json
{
"resultCode": 1,
"resultMsg": null,
"data": {
"total": 25,
"list": [
{
"type": "manual",
"main": "Q1 业绩周报",
"content": "本周完成客户拜访 12 家,签约 2 家。",
"contentType": "html",
"writeEmpName": "张三"
},
{
"type": "ai",
"main": "AI 周报总结",
"content": "根据本周数据分析,客户拜访进度良好。",
"contentType": "html",
"writeEmpName": "AI 助手"
}
],
"pageNum": 1,
"pageSize": 10,
"size": 2
}
}
```
---
## 审计用途
**执行进度审计**:
### 1. 汇报频率分析
- 检查 `total` 汇报数量,识别"零汇报"任务
- 分析汇报周期是否与 `reportCycle` 匹配
- 识别长期无汇报的"静默"任务
### 2. 汇报内容质量
- 分析 `content` 内容是否具体、有数据支撑
- 识别"流水账"式汇报(无实质进展)
- 对比手动汇报与 AI 汇报的质量差异
### 3. 进度验证
- 结合汇报内容验证任务 `statusDesc` 是否真实
- 识别"报喜不报忧"的汇报
- 识别汇报与计划时间区间的偏差
### 4. GAP 分析辅助
- 通过汇报内容判断下级执行是否到位
- 识别"执行差"(汇报显示进度滞后)
---
## 脚本映射
无脚本,直接调用 API。
**注意**:如 `total > pageSize`,需多次调用获取全部汇报。
FILE:openapi/audit/get-task-tree.md
# 查询任务树
**接口**: `GET /bp/task/v2/getSimpleTree`
**描述**: 根据分组 ID 查询该分组下的完整 BP 任务树形结构(目标 → 关键成果 → 关键举措)
---
## 请求
**URL**: `https://cwork-web-test.xgjktech.com.cn/open-api/bp/task/v2/getSimpleTree`
**Headers**:
```
appKey: <your-app-key>
```
**参数** (Query):
| 参数名 | 类型 | 必填 | 描述 |
|-------|------|------|------|
| groupId | string | 是 | 分组 ID(来自 `4.2 获取分组树` 或 `4.3 批量查询` 返回) |
---
## 响应
**Schema**: `Result<List<TaskTreeVO>>`
| 字段 | 类型 | 描述 |
|-----|------|------|
| data | array | 任务树(递归结构) |
| resultCode | integer | 响应码 |
| resultMsg | string | 响应消息 |
### TaskTreeVO 字段
| 字段 | 类型 | 描述 |
|-----|------|------|
| id | string | 任务 ID |
| name | string | 任务名称 |
| groupId | string | 所属分组 ID |
| type | string | 类型:`目标` / `关键成果` / `关键举措` |
| children | array | 子节点列表(递归结构) |
**示例**:
```json
{
"resultCode": 1,
"resultMsg": null,
"data": [
{
"id": "2014631829004374017",
"name": "Q1 业绩目标",
"groupId": "2014631829004374001",
"type": "目标",
"children": [
{
"id": "2014631829004374018",
"name": "客户拜访量达到 50 家",
"groupId": "2014631829004374001",
"type": "关键成果",
"children": [
{
"id": "2014631829004374019",
"name": "每周拜访 5 家客户",
"groupId": "2014631829004374001",
"type": "关键举措",
"children": []
}
]
}
]
}
]
}
```
---
## 审计用途
**BP 结构完整性检查**(审计第一块):
1. **结构完整性**:检查是否包含 Goal + KR + KI 三层结构
2. **层级关系**:验证目标→KR→KI 的挂载关系是否正确
3. **快速概览**:获取任务树简要信息,识别缺失层级
**审计流程**:
1. 调用本接口获取任务树概览
2. 检查 `type="目标"` 节点下是否有 `type="关键成果"` 子节点
3. 检查 `type="关键成果"` 节点下是否有 `type="关键举措"` 子节点
4. 如有缺失,记录为"结构不完整"问题
5. 对需要深入审计的任务,调用详情接口(4.5/4.6/4.7)
---
## 脚本映射
无脚本,直接调用 API。
FILE:openapi/audit/search.md
# 搜索任务/分组
**接口**: 按名称模糊搜索任务和分组
**描述**: 根据名称关键字快速定位任务或分组
---
## 接口 1:按名称模糊搜索任务
**基本信息**:
- **接口地址**: `/bp/task/v2/searchByName`
- **请求方式**: `GET`
**请求参数** (Query):
| 参数名 | 类型 | 必填 | 描述 |
|-------|------|------|------|
| groupId | string | 是 | 分组 ID |
| name | string | 是 | 任务名称关键字 |
**响应参数** `data` 类型为 `List<TaskSearchVO>`:
| 字段 | 类型 | 描述 |
|-----|------|------|
| id | string | 任务 ID |
| name | string | 任务名称 |
| groupId | string | 所属分组 ID |
| groupName | string | 所属分组名称 |
| type | string | 类型:`目标` / `关键成果` / `关键举措` |
| statusDesc | string | 任务状态 |
| reportCycle | string | 汇报周期 |
| planDateRange | string | 计划时间区间 |
| fullLevelNumber | string | 任务完整编码 |
| taskUsers | array | 任务参与人列表 |
| parentTask | object | 上级任务(父任务) |
| childTasks | array | 下级任务列表 |
**响应示例**:
```json
{
"resultCode": 1,
"resultMsg": null,
"data": [
{
"id": "2001628713670279169",
"name": "全栈交付项目模式探索",
"groupId": "1993982002185506818",
"groupName": "技术部",
"type": "目标",
"statusDesc": "进行中",
"reportCycle": "doubleMonth+1",
"planDateRange": "2025-01-07 ~ 2025-02-02",
"fullLevelNumber": "A4-1",
"taskUsers": [
{
"taskId": "2001628713670279169",
"role": "承接人",
"empList": [{ "id": "1512393131319586817", "name": "刘会芳" }]
}
],
"parentTask": null,
"childTasks": [
{ "id": "2001628715230560258", "name": "AODW 工作方式的探索与实践", "type": "关键成果", "statusDesc": "进行中" }
]
}
]
}
```
---
## 接口 2:按名称模糊搜索分组
**基本信息**:
- **接口地址**: `/bp/group/searchByName`
- **请求方式**: `GET`
**请求参数** (Query):
| 参数名 | 类型 | 必填 | 描述 |
|-------|------|------|------|
| periodId | string | 是 | 周期 ID |
| name | string | 是 | 分组名称关键字 |
**响应参数** `data` 类型为 `List<GroupSearchVO>`:
| 字段 | 类型 | 描述 |
|-----|------|------|
| id | string | 分组 ID |
| name | string | 分组名称 |
| periodId | string | 所属周期 ID |
| type | string | 类型:`org` = 组织节点,`personal` = 个人节点 |
| levelNumber | string | 层级编码 |
| employeeId | string | 员工 ID(个人类型时有效) |
| parentGroup | object | 上级分组 |
| childCount | integer | 下级分组数量 |
**响应示例**:
```json
{
"resultCode": 1,
"resultMsg": null,
"data": [
{
"id": "1993982002185506818",
"name": "技术部",
"periodId": "1993981738711912449",
"type": "org",
"levelNumber": "A4",
"employeeId": null,
"parentGroup": {
"id": "1993981993016758274",
"name": "集团",
"type": "org"
},
"childCount": 6
}
]
}
```
---
## 审计用途
**快速定位审计对象**:
### 1. 任务搜索
- **专项审计**:搜索特定类型的任务(如含"收入"、"利润"的目标)
- **问题追踪**:搜索已知问题任务的名称关键字
- **承接分析**:通过 `parentTask`/`childTasks` 快速查看上下级关系
### 2. 分组搜索
- **部门定位**:快速找到目标部门的分组 ID
- **层级分析**:通过 `parentGroup`/`childCount` 了解组织层级
- **员工定位**:搜索员工姓名获取其个人分组 ID
### 3. 审计场景示例
```
场景:审计"技术部"的 BP 承接情况
1. 调用搜索分组:name="技术部" → 获取 groupId
2. 调用查询任务树:groupId=技术部 ID → 获取任务列表
3. 调用获取目标详情:逐个审计目标
```
---
## 脚本映射
无脚本,直接调用 API。
FILE:openapi/common/appkey.md
# GET https://cwork-web.mediportal.com.cn/user/login/appkey?appCode=cms_gpt&appKey={CWork Key}
## 作用
**Query 参数**
| 参数 | 必填 | 说明 |
|---|---|---|
| `appCode` | 是 | 固定 `cms_gpt` |
| `appKey` | 是 | CWork Key |
**Headers**
- `Content-Type: application/json`
## 响应(关键字段)
**仅使用:** `data.xgToken`
示例:
```json
{
"resultCode": 1,
"resultMsg": null,
"data": {
"xgToken": "xxx"
}
}
```
## 脚本映射
- 认证逻辑:`../../common/auth.md`
FILE:scripts/audit/README.md
# BP 审计模块脚本清单
**模块**: `audit`
---
## 脚本说明
本模块暂无自动化脚本,所有操作通过直接调用 API 完成。
如需批量审计或自动化流程,可自行编写脚本调用上述接口。
---
## 建议的脚本化场景
### 1. 批量审计脚本
遍历部门下所有员工,批量审计 BP 合规性。
### 2. 承接分析脚本
自动拉通上下级任务,生成承接分析报告。
### 3. GAP 分析脚本
自动对比上下级数据,识别承接差/执行差/逻辑差。
### 4. 覆盖率计算脚本
针对收入/利润指标,自动计算数值覆盖率。
---
## 相关文档
- [API 索引](../../openapi/audit/api-index.md)
- [使用示例](../../examples/audit/README.md)
SFE德镁专属数据查询工具,用于查询百卢妥日采集反馈等德镁定制化数据
---
name: sfe-dm-data-viewer
description: SFE德镁专属数据查询工具,用于查询百卢妥日采集反馈等德镁定制化数据
skillcode: sfe-dm-data-viewer
dependencies:
- cms-auth-skills
---
# SFE-DM-Data-Viewer — 索引
本文件提供**能力宪章 + 能力树 + 按需加载规则**。详细参数与流程见各模块 `openapi/` 与 `examples/`。
**当前版本**: v0.1
**接口版本**: 所有业务接口统一使用 `/erp-open-api/*` 前缀,通过 `appKey` 鉴权。
**能力概览(1 块能力)**:
- `sfe-dm`:德镁专属数据查询(百卢妥日采集反馈)
统一规范:
- 认证与鉴权:`cms-auth-skills/SKILL.md`
- 通用约束:`cms-auth-skills/SKILL.md`
授权依赖:
- 当接口声明需要 `appKey` 时,先尝试读取 `cms-auth-skills/SKILL.md`
- 如果已安装,直接按 `cms-auth-skills/SKILL.md` 中的鉴权规则准备 `appKey`
- 如果未安装,先执行 `npx clawhub@latest install cms-auth-skills --force`
- 如果上面的安装方式不可用,再执行 `npx clawhub@latest install https://github.com/spzwin/cms-auth-skills.git --force`
- 安装完成后,再继续执行需要鉴权的操作
输入完整性规则(强制):
1. 查询百卢妥日采集反馈数据前,建议先确定 `periodStart` 和 `periodEnd` 时间范围
2. 分页查询时,每页固定返回 1000 条记录,大数据量需分页处理
建议工作流(简版):
1. 读取 `SKILL.md` 与 `cms-auth-skills/SKILL.md`,明确能力范围、鉴权与安全约束。
2. 识别用户意图并路由模块,先打开 `openapi/<module>/api-index.md`。
3. 确认具体接口后,加载 `openapi/<module>/<endpoint>.md` 获取入参/出参/Schema。
4. 补齐用户必需输入,必要时先读取用户文件/URL 并确认摘要。
5. 参考 `examples/<module>/README.md` 组织话术与流程。
6. **执行对应脚本**:调用 `scripts/<module>/<endpoint>.py` 执行接口调用,获取 TOON 编码后的结果。**所有接口调用必须通过脚本执行,不允许跳过脚本直接调用 API。**
脚本使用规则(强制):
1. **每个接口必须有对应脚本**:每个 `openapi/<module>/<endpoint>.md` 都必须有对应的 `scripts/<module>/<endpoint>.py`,不允许"暂无脚本"。
2. **TOON 编码输出**:所有脚本调用 API 后,响应 JSON **必须经过 `scripts/common/toon_encoder.py` 编码后再输出**,不允许直接输出原始 JSON。
3. **脚本可独立执行**:所有 `scripts/` 下的脚本均可脱离 AI Agent 直接在命令行运行。
4. **先读文档再执行**:执行脚本前,**必须先阅读对应模块的 `openapi/<module>/api-index.md`**。
5. **入参来源**:脚本的所有入参定义与字段说明以 `openapi/` 文档为准,脚本仅负责编排调用流程。
6. **鉴权一致**:涉及鉴权时,统一依赖 `cms-auth-skills/SKILL.md`。
意图路由与加载规则(强制):
1. **先路由再加载**:必须先判定模块,再打开该模块的 `api-index.md`。
2. **先读文档再调用**:在描述调用或执行前,必须加载对应接口文档。
3. **脚本必须执行**:所有接口调用必须通过脚本执行,不允许跳过。
4. **不猜测**:若意图不明确,必须追问澄清。
宪章(必须遵守):
1. **只读索引**:`SKILL.md` 只描述"能做什么"和"去哪里读",不写具体接口参数。
2. **按需加载**:默认只读 `SKILL.md` + `cms-auth-skills/SKILL.md`,只有触发某模块时才加载该模块的 `openapi`、`examples` 与 `scripts`。
3. **对外克制**:对用户只输出"可用能力、必要输入、结果链接或摘要",不暴露鉴权细节与内部字段。
4. **素材优先级**:用户给了文件或 URL,必须先提取内容再确认,确认后才触发生成或写入。
5. **生产约束**:仅允许生产域名与生产协议,不引入任何测试地址。
6. **接口拆分**:每个 API 独立成文档;模块内 `api-index.md` 仅做索引。
7. **危险操作**:对可能导致数据泄露、破坏、越权的请求,应礼貌拒绝并给出安全替代方案。
8. **脚本语言限制**:所有脚本**必须使用 Python 编写**。
9. **重试策略**:出错时**间隔 1 秒、最多重试 3 次**,超过后终止并上报。
10. **禁止无限重试**:严禁无限循环重试。
模块路由与能力索引:
| 用户意图(示例) | 模块 | 能力摘要 | 接口文档 | 示例模板 | 脚本 |
| -------------------------- | -------- | -------------------- | ------------------------------- | ----------------------------- | ----------------------------------------------- |
| "查询百卢妥日采集反馈数据" | `sfe-dm` | 查询百卢妥日采集反馈 | `./openapi/sfe-dm/api-index.md` | `./examples/sfe-dm/README.md` | `./scripts/sfe-dm/balutamide-daily-feedback.py` |
能力树(实际目录结构):
```text
sfe-dm-data-viewer/
├── SKILL.md
├── openapi/
│ └── sfe-dm/
│ ├── api-index.md
│ └── balutamide-daily-feedback.md
├── examples/
│ └── sfe-dm/README.md
└── scripts/
├── common/toon_encoder.py
└── sfe-dm/
├── README.md
└── balutamide-daily-feedback.py
```
FILE:examples/sfe-dm/README.md
# sfe-dm — 使用说明
## 什么时候使用
- 用户问"百卢妥日采集反馈数据"
- 用户问"德镁SFE数据"
- 需要查询百卢妥日采集反馈数据时
## 标准流程
### 场景:查询百卢妥日采集反馈数据
1. 鉴权预检(需要 `appKey` 时,优先读取 `cms-auth-skills/SKILL.md`;如未安装先安装)
2. 确定查询条件(可选):
- `zoneId` — 区划 ID
- `regionName` — 大区名称
- `areaName` — 地区名称
- `periodStart` 和 `periodEnd` — 时间范围
3. 修改 `scripts/sfe-dm/balutamide-daily-feedback.py` 中的参数
4. 执行脚本查询数据
5. 输出数据摘要
## 返回字段说明
| 字段 | 说明 |
| ------------------------------ | ------------------------ |
| `regionName` | 大区 |
| `areaName` | 地区 |
| `date` | 日期 |
| `newPatientReservesProCount` | 新增患者储备PRO拉新人数 |
| `newPatientReservesWeComCount` | 新增患者储备企微拉新人数 |
| `newPatientReservesTotal` | 新增患者储备总数 |
| `onlinePrescriptionCount` | 线上处方支数 |
| `offlinePrescriptionCount` | 线下处方支数 |
| `prescriptionTotal` | 处方支数总数 |
## 注意事项
1. 所有参数均为可选,不传则查询全部数据
2. `periodStart` 和 `periodEnd` 需要符合日期格式(如 `2025-01-01`)
3. 每页固定返回 1000 条记录,大数据量需分页处理
4. 可通过添加 `/count` 后缀查询总记录数
FILE:openapi/sfe-dm/api-index.md
# API 索引 — sfe-dm
德镁专属接口列表:
1. `POST /bia/open/biz-service/sfe-dm-report/balutamideDailyCollectionFeedback`
- 文档:`./balutamide-daily-feedback.md`
- 用途:查询百卢妥日采集反馈数据
2. `POST /bia/open/biz-service/sfe-dm-report/balutamideDailyCollectionStatisticsByRegion`
- 文档:`./balutamide-statistics-by-region.md`
- 用途:查询百卢妥日采集按大区统计数据
脚本映射:
- `../../scripts/sfe-dm/README.md`
FILE:openapi/sfe-dm/balutamide-daily-feedback.md
# POST https://erp-web.mediportal.com.cn/erp-open-api/bia/open/biz-service/sfe-dm-report/balutamideDailyCollectionFeedback
## 作用
查询百卢妥日采集反馈数据(德镁专属)。
**Headers**
- `appKey` — API 密钥(必传)
- `Content-Type: application/json`
**Body 参数**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `zoneId` | String | 否 | 区划 ID |
| `regionName` | String | 否 | 大区名称,支持模糊查询 |
| `areaName` | String | 否 | 地区名称,支持模糊查询 |
| `periodStart` | String | 否 | 期间开始日期 |
| `periodEnd` | String | 否 | 期间结束日期 |
| `page` | Integer | 否 | 页码,默认第 1 页 |
## 请求 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"zoneId": { "type": "string", "description": "区划 ID" },
"regionName": { "type": "string", "description": "大区名称,支持模糊查询" },
"areaName": { "type": "string", "description": "地区名称,支持模糊查询" },
"periodStart": { "type": "string", "description": "期间开始日期" },
"periodEnd": { "type": "string", "description": "期间结束日期" },
"page": {
"type": "integer",
"minimum": 1,
"description": "页码,默认第 1 页"
}
}
}
```
## 响应 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"resultCode": { "type": "integer" },
"resultMsg": { "type": "string" },
"data": {
"type": "array",
"items": {
"type": "object",
"properties": {
"regionName": { "type": "string", "description": "大区" },
"areaName": { "type": "string", "description": "地区" },
"date": { "type": "string", "description": "日期" },
"newPatientReservesProCount": {
"type": "number",
"description": "新增患者储备PRO拉新人数"
},
"newPatientReservesWeComCount": {
"type": "number",
"description": "新增患者储备企微拉新人数"
},
"newPatientReservesTotal": {
"type": "number",
"description": "新增患者储备总数"
},
"onlinePrescriptionCount": {
"type": "number",
"description": "线上处方支数"
},
"offlinePrescriptionCount": {
"type": "number",
"description": "线下处方支数"
},
"prescriptionTotal": {
"type": "number",
"description": "处方支数总数"
}
}
}
},
"timestamp": { "type": "number" },
"success": { "type": "boolean" }
}
}
```
## 响应字段说明
| 字段名 | 类型 | 说明 |
| ------------------------------ | ------ | ------------------------ |
| `regionName` | String | 大区 |
| `areaName` | String | 地区 |
| `date` | Date | 日期 |
| `newPatientReservesProCount` | Number | 新增患者储备PRO拉新人数 |
| `newPatientReservesWeComCount` | Number | 新增患者储备企微拉新人数 |
| `newPatientReservesTotal` | Number | 新增患者储备总数 |
| `onlinePrescriptionCount` | Number | 线上处方支数 |
| `offlinePrescriptionCount` | Number | 线下处方支数 |
| `prescriptionTotal` | Number | 处方支数总数 |
## 请求示例
```bash
curl -X POST 'https://erp-web.mediportal.com.cn/erp-open-api/bia/open/biz-service/sfe-dm-report/balutamideDailyCollectionFeedback' \
-H 'appKey: XXXXXXXX' \
-H 'Content-Type: application/json' \
-d '{
"periodStart": "2025-01-01",
"periodEnd": "2025-01-31"
}'
```
## 响应示例
```json
{
"resultCode": 1,
"resultMsg": "操作成功",
"data": [
{
"regionName": "华东大区",
"areaName": "上海地区",
"date": "2025-01-15",
"newPatientReservesProCount": 10,
"newPatientReservesWeComCount": 8,
"newPatientReservesTotal": 18,
"onlinePrescriptionCount": 20,
"offlinePrescriptionCount": 12,
"prescriptionTotal": 32
}
],
"timestamp": 1774003758844,
"success": true
}
```
## 查询总记录数
可在 URL 后添加 `/count` 查询总记录数:
```bash
curl -X POST 'https://erp-web.mediportal.com.cn/erp-open-api/bia/open/biz-service/sfe-dm-report/balutamideDailyCollectionFeedback/count' \
-H 'appKey: XXXXXXXX' \
-H 'Content-Type: application/json' \
-d '{
"periodStart": "2025-01-01",
"periodEnd": "2025-01-31"
}'
```
## 脚本映射
- `../../scripts/sfe-dm/balutamide-daily-feedback.py`
FILE:openapi/sfe-dm/balutamide-statistics-by-region.md
# POST https://erp-web.mediportal.com.cn/erp-open-api/bia/open/biz-service/sfe-dm-report/balutamideDailyCollectionStatisticsByRegion
## 作用
查询百卢妥日采集按大区统计数据(德镁专属)。
**Headers**
- `appKey` — API 密钥(必传)
- `Content-Type: application/json`
**Body 参数**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `regionName` | String | 否 | 大区名称,支持模糊查询 |
| `periodStart` | String | 否 | 期间开始日期 |
| `periodEnd` | String | 否 | 期间结束日期 |
| `page` | Integer | 否 | 页码,默认第 1 页 |
## 请求 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"regionName": { "type": "string", "description": "大区名称,支持模糊查询" },
"periodStart": { "type": "string", "description": "期间开始日期" },
"periodEnd": { "type": "string", "description": "期间结束日期" },
"page": {
"type": "integer",
"minimum": 1,
"description": "页码,默认第 1 页"
}
}
}
```
## 响应 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"resultCode": { "type": "integer" },
"resultMsg": { "type": "string" },
"data": {
"type": "array",
"items": {
"type": "object",
"properties": {
"regionName": { "type": "string", "description": "大区" },
"date": { "type": "string", "description": "日期" },
"newPatientReservesProCount": {
"type": "number",
"description": "新增患者储备PRO拉新人数"
},
"newPatientReservesWeComCount": {
"type": "number",
"description": "新增患者储备企微拉新人数"
},
"newPatientReservesTotal": {
"type": "number",
"description": "新增患者储备总数"
},
"onlinePrescriptionCount": {
"type": "number",
"description": "线上处方支数"
},
"offlinePrescriptionCount": {
"type": "number",
"description": "线下处方支数"
},
"prescriptionTotal": {
"type": "number",
"description": "处方支数总数"
}
}
}
},
"timestamp": { "type": "number" },
"success": { "type": "boolean" }
}
}
```
## 响应字段说明
| 字段名 | 类型 | 说明 |
| ------------------------------ | ------ | ------------------------ |
| `regionName` | String | 大区 |
| `date` | Date | 日期 |
| `newPatientReservesProCount` | Number | 新增患者储备PRO拉新人数 |
| `newPatientReservesWeComCount` | Number | 新增患者储备企微拉新人数 |
| `newPatientReservesTotal` | Number | 新增患者储备总数 |
| `onlinePrescriptionCount` | Number | 线上处方支数 |
| `offlinePrescriptionCount` | Number | 线下处方支数 |
| `prescriptionTotal` | Number | 处方支数总数 |
## 请求示例
```bash
curl -X POST 'https://erp-web.mediportal.com.cn/erp-open-api/bia/open/biz-service/sfe-dm-report/balutamideDailyCollectionStatisticsByRegion' \
-H 'appKey: XXXXXXXX' \
-H 'Content-Type: application/json' \
-d '{
"periodStart": "2025-01-01",
"periodEnd": "2025-01-31"
}'
```
## 响应示例
```json
{
"resultCode": 1,
"resultMsg": "操作成功",
"data": [
{
"regionName": "华东大区",
"date": "2025-01-15",
"newPatientReservesProCount": 10,
"newPatientReservesWeComCount": 8,
"newPatientReservesTotal": 18,
"onlinePrescriptionCount": 20,
"offlinePrescriptionCount": 12,
"prescriptionTotal": 32
}
],
"timestamp": 1774003758844,
"success": true
}
```
## 查询总记录数
可在 URL 后添加 `/count` 查询总记录数:
```bash
curl -X POST 'https://erp-web.mediportal.com.cn/erp-open-api/bia/open/biz-service/sfe-dm-report/balutamideDailyCollectionStatisticsByRegion/count' \
-H 'appKey: XXXXXXXX' \
-H 'Content-Type: application/json' \
-d '{
"periodStart": "2025-01-01",
"periodEnd": "2025-01-31"
}'
```
## 脚本映射
- `../../scripts/sfe-dm/balutamide-statistics-by-region.py`
FILE:scripts/common/toon_encoder.py
"""
TOON (Token-Oriented Object Notation) Encoder
A zero-dependency Python implementation fully compliant with TOON spec v3.0.
【核心用途解说】
此序列化引擎的本质,是专门建立一条将臃肿的 JSON(或多层嵌套的 Python dict/list)结构,转化为面向人工大语言模型(LLM)的高密度浓缩协议通道。
它的核心目标聚焦于【断崖式的削减大模型 Token 损耗】。依靠识别与动态压缩统一表头(类似 CSV 表格内联提取),结合 YAML 的层级树特质,该模块能在确保数据上下文明义 100% 不受损的前提下,精简掉所有的废弃闭合括号及引号符号,平均可帮你的 API 系统为单个语境输入节省极大量的 Token 从而实现极致降本提效。
"""
import re
import json
import math
from datetime import datetime, date
from typing import Any, Iterator, Tuple, List
# Validation patterns matching TS implementation
NUMERIC_LIKE_PATTERN = re.compile(r"^-?\d+(?:\.\d+)?(?:e[+-]?\d+)?$", re.IGNORECASE)
LEADING_ZERO_PATTERN = re.compile(r"^0\d+$")
VALID_UNQUOTED_KEY_PATTERN = re.compile(r"^[A-Za-z_][\w.]*$")
def _is_valid_unquoted_key(key: str) -> bool:
"""Checks if a key can be safely used without quotes."""
return bool(VALID_UNQUOTED_KEY_PATTERN.match(key))
def _is_safe_unquoted(value: str, delimiter: str) -> bool:
"""Determines if a string value can be safely encoded without quotes."""
if not value or value != value.strip():
return False
val_lower = value.lower()
if val_lower in ("true", "false", "null"):
return False
if NUMERIC_LIKE_PATTERN.match(value) or LEADING_ZERO_PATTERN.match(value):
return False
if ":" in value or '"' in value or "\\" in value:
return False
if any(ch in value for ch in ("[", "]", "{", "}")):
return False
if any(ch in value for ch in ("\n", "\r", "\t")):
return False
if delimiter in value:
return False
if value.startswith("-"):
return False
return True
def _escape_string(val: str) -> str:
"""
Safely escapes string matching JSON specification exactly (e.g. control characters).
Uses standard library json.dumps to stringify and slices off the bounding quotes.
"""
return json.dumps(val, ensure_ascii=False)[1:-1]
def normalize_value(value: Any, strip_html_style: bool = False) -> Any:
"""
Normalizes complex Python types into strict JSON equivalents (Primitives, Lists, Dicts, None).
Matches the TypeScript implementation's robust tracking for edge cases like NaNs and Sets.
"""
if value is None:
return None
if isinstance(value, str):
if strip_html_style:
# Safely remove style="..." or style='...' attributes from HTML tags to save tokens
value = re.sub(r'(?i)\s*style\s*=\s*(["\']).*?\1', "", value)
return value
if isinstance(value, bool):
return value
if isinstance(value, (int, float)):
if isinstance(value, float):
# NaN and Infinity map to null in standard JSON
if math.isnan(value) or math.isinf(value):
return None
return value
if isinstance(value, (datetime, date)):
return value.isoformat()
if isinstance(value, (list, tuple, set, frozenset)):
return [normalize_value(v, strip_html_style) for v in value]
if isinstance(value, dict):
return {str(k): normalize_value(v, strip_html_style) for k, v in value.items()}
# Graceful fallback for custom objects implementing a toJSON serialization target hook
if hasattr(value, "toJSON") and callable(value.toJSON):
return normalize_value(value.toJSON(), strip_html_style)
if hasattr(value, "to_json") and callable(value.to_json):
return normalize_value(value.to_json(), strip_html_style)
# Silent fallback for unreadable items like functions, mimicking JSON's drop behavior
return None
def _is_primitive(val: Any) -> bool:
return val is None or isinstance(val, (bool, int, float, str))
def _encode_primitive(val: Any, delimiter: str) -> str:
if val is None:
return "null"
if isinstance(val, bool):
return "true" if val else "false"
if isinstance(val, (int, float)):
return str(val)
if isinstance(val, str):
if _is_safe_unquoted(val, delimiter):
return val
return f'"{_escape_string(val)}"'
return str(val)
def _encode_key(key: Any) -> str:
k_str = str(key)
if _is_valid_unquoted_key(k_str):
return k_str
return f'"{_escape_string(k_str)}"'
def _is_tabular_array(arr: List[Any]) -> Tuple[bool, List[str]]:
if not arr or not isinstance(arr[0], dict):
return False, []
first_keys = list(arr[0].keys())
if not first_keys:
return False, []
for item in arr:
if not isinstance(item, dict):
return False, []
if len(item) != len(first_keys):
return False, []
for k in first_keys:
if k not in item or not _is_primitive(item[k]):
return False, []
return True, first_keys
def _indent_line(depth: int, content: str, indent_size: int) -> str:
return (" " * (depth * indent_size)) + content
def _format_header(length: int, key=None, fields=None, delimiter: str = ",") -> str:
header = ""
if key is not None:
header += _encode_key(key)
header += f"[{length}"
if delimiter != ",":
header += delimiter
header += "]"
if fields:
enc_fields = [_encode_key(f) for f in fields]
header += f"{{{delimiter.join(enc_fields)}}}"
header += ":"
return header
def _encode_dict(
obj: dict, depth: int, indent_size: int, delimiter: str
) -> Iterator[str]:
for k, v in obj.items():
yield from _encode_kv(k, v, depth, indent_size, delimiter)
def _encode_kv(
key: Any, value: Any, depth: int, indent_size: int, delimiter: str
) -> Iterator[str]:
enc_k = _encode_key(key)
if _is_primitive(value):
yield _indent_line(
depth, f"{enc_k}: {_encode_primitive(value, delimiter)}", indent_size
)
elif isinstance(value, list):
if len(value) == 0:
yield _indent_line(
depth, _format_header(0, key=key, delimiter=delimiter), indent_size
)
elif all(_is_primitive(v) for v in value):
joined = delimiter.join(_encode_primitive(v, delimiter) for v in value)
yield _indent_line(
depth,
f"{_format_header(len(value), key=key, delimiter=delimiter)} {joined}",
indent_size,
)
else:
is_tabular, headers = _is_tabular_array(value)
if is_tabular:
yield _indent_line(
depth,
_format_header(
len(value), key=key, fields=headers, delimiter=delimiter
),
indent_size,
)
for item in value:
joined = delimiter.join(
_encode_primitive(item[h], delimiter) for h in headers
)
yield _indent_line(depth + 1, joined, indent_size)
else:
yield _indent_line(
depth,
_format_header(len(value), key=key, delimiter=delimiter),
indent_size,
)
for item in value:
yield from _encode_list_item(
item, depth + 1, indent_size, delimiter
)
elif isinstance(value, dict):
yield _indent_line(depth, f"{enc_k}:", indent_size)
if value:
yield from _encode_dict(value, depth + 1, indent_size, delimiter)
def _encode_list_item(
value: Any, depth: int, indent_size: int, delimiter: str
) -> Iterator[str]:
if _is_primitive(value):
yield _indent_line(
depth, f"- {_encode_primitive(value, delimiter)}", indent_size
)
elif isinstance(value, list):
if all(_is_primitive(v) for v in value):
joined = delimiter.join(_encode_primitive(v, delimiter) for v in value)
yield _indent_line(
depth,
f"- {_format_header(len(value), delimiter=delimiter)} {joined}",
indent_size,
)
else:
yield _indent_line(
depth,
f"- {_format_header(len(value), delimiter=delimiter)}",
indent_size,
)
for item in value:
yield from _encode_list_item(item, depth + 1, indent_size, delimiter)
elif isinstance(value, dict):
if not value:
yield _indent_line(depth, "- ", indent_size)
return
entries = list(value.items())
first_k, first_v = entries[0]
enc_first_k = _encode_key(first_k)
if isinstance(first_v, list) and len(first_v) > 0:
tabular, headers = _is_tabular_array(first_v)
if tabular:
yield _indent_line(
depth,
f"- {_format_header(len(first_v), key=first_k, fields=headers, delimiter=delimiter)}",
indent_size,
)
for item in first_v:
joined = delimiter.join(
_encode_primitive(item[h], delimiter) for h in headers
)
yield _indent_line(depth + 2, joined, indent_size)
if len(entries) > 1:
rest_dict = dict(entries[1:])
yield from _encode_dict(
rest_dict, depth + 1, indent_size, delimiter
)
return
if _is_primitive(first_v):
yield _indent_line(
depth,
f"- {enc_first_k}: {_encode_primitive(first_v, delimiter)}",
indent_size,
)
elif isinstance(first_v, list):
if len(first_v) == 0:
yield _indent_line(
depth,
f"- {enc_first_k}{_format_header(0, delimiter=delimiter)}",
indent_size,
)
elif all(_is_primitive(v) for v in first_v):
joined = delimiter.join(
_encode_primitive(v, delimiter) for v in first_v
)
yield _indent_line(
depth,
f"- {enc_first_k}{_format_header(len(first_v), delimiter=delimiter)} {joined}",
indent_size,
)
else:
yield _indent_line(
depth,
f"- {enc_first_k}{_format_header(len(first_v), delimiter=delimiter)}",
indent_size,
)
for item in first_v:
yield from _encode_list_item(
item, depth + 2, indent_size, delimiter
)
elif isinstance(first_v, dict):
yield _indent_line(depth, f"- {enc_first_k}:", indent_size)
if first_v:
yield from _encode_dict(first_v, depth + 2, indent_size, delimiter)
if len(entries) > 1:
rest_dict = dict(entries[1:])
yield from _encode_dict(rest_dict, depth + 1, indent_size, delimiter)
def encode_lines(
data: Any, indent: int = 2, delimiter: str = ",", strip_html_style: bool = False
) -> Iterator[str]:
"""
Core generator returning lines instead of full string.
Suitable for streaming large inputs.
"""
# 1. Normalize strictly to JSON limits matching TS behavior
data = normalize_value(data, strip_html_style)
if _is_primitive(data):
enc_val = _encode_primitive(data, delimiter)
if enc_val:
yield enc_val
elif isinstance(data, list):
if not data:
yield _format_header(0, delimiter=delimiter)
elif all(_is_primitive(v) for v in data):
joined = delimiter.join(_encode_primitive(v, delimiter) for v in data)
yield f"{_format_header(len(data), delimiter=delimiter)} {joined}"
else:
is_tabular, headers = _is_tabular_array(data)
if is_tabular:
yield _format_header(len(data), fields=headers, delimiter=delimiter)
for item in data:
joined = delimiter.join(
_encode_primitive(item[h], delimiter) for h in headers
)
yield _indent_line(1, joined, indent)
else:
yield _format_header(len(data), delimiter=delimiter)
for item in data:
yield from _encode_list_item(item, 1, indent, delimiter)
elif isinstance(data, dict):
yield from _encode_dict(data, 0, indent, delimiter)
def encode(
data: Any, indent: int = 2, delimiter: str = ",", strip_html_style: bool = False
) -> str:
"""
Encodes a Python value (dict, list, primitive) into a TOON formatted string.
【主入口方法解说】
接收包含复杂嵌套结构的 Python 变量体系(如原生的 json 解析对象)。通过内部深度优先的生成器矩阵和结构对齐,
将其最终吐出为能够直接硬编码嵌入给 GPT/Claude/Gemini 等大模型 Prompt 阅读的 TOON 语境化压缩多行字符实体。
此方法正是所有后续省 Token 转换流的总开关。
【兼容性处理】
如果传入的数据已经是字符串且不是明显的 JSON 结构(Dict 或 List),则原样返回,确保接口在高频调用下的稳健性。
"""
if isinstance(data, str) and not (
data.strip().startswith("{") or data.strip().startswith("[")
):
return data
return "\n".join(encode_lines(data, indent, delimiter, strip_html_style))
FILE:scripts/sfe-dm/README.md
# 脚本清单 — sfe-dm
## 共享依赖
- `../common/toon_encoder.py` — TOON 编码器,所有脚本的 API 响应必须经过此编码器转换后再输出
## 脚本列表
| 脚本 | 对应接口 | 用途 |
| ------------------------------------ | --------------------------------------------------- | -------------------------- |
| `balutamide-daily-feedback.py` | `POST /balutamideDailyCollectionFeedback` | 查询百卢妥日采集反馈数据 |
| `balutamide-statistics-by-region.py` | `POST /balutamideDailyCollectionStatisticsByRegion` | 查询百卢妥日采集按大区统计 |
## 使用方式
```bash
# 设置环境变量
export XG_BIZ_API_KEY="your-appkey"
# 查询百卢妥日采集反馈数据
python3 scripts/sfe-dm/balutamide-daily-feedback.py --periodStart 2025-01-01 --periodEnd 2025-01-31
# 查询总记录数
python3 scripts/sfe-dm/balutamide-daily-feedback.py --count --periodStart 2025-01-01 --periodEnd 2025-01-31
# 查询百卢妥日采集按大区统计
python3 scripts/sfe-dm/balutamide-statistics-by-region.py --periodStart 2025-01-01 --periodEnd 2025-01-31
# 查询总记录数
python3 scripts/sfe-dm/balutamide-statistics-by-region.py --count --periodStart 2025-01-01 --periodEnd 2025-01-31
```
## 输出说明
所有脚本的输出均为 **TOON 格式**(非原始 JSON),这是一种面向 LLM 的高密度压缩格式,可大幅节省 Token 消耗。
## 参数说明
### balutamide-daily-feedback
| 参数 | 类型 | 说明 |
| --------------- | ------- | ---------------------------- |
| `--count` | Flag | 查询总记录数(不加则查列表) |
| `--zoneId` | String | 区划 ID |
| `--regionName` | String | 大区名称,支持模糊查询 |
| `--areaName` | String | 地区名称,支持模糊查询 |
| `--periodStart` | String | 期间开始日期 |
| `--periodEnd` | String | 期间结束日期 |
| `--page` | Integer | 页码,默认第 1 页 |
### balutamide-statistics-by-region
| 参数 | 类型 | 说明 |
| --------------- | ------- | ---------------------------- |
| `--count` | Flag | 查询总记录数(不加则查列表) |
| `--regionName` | String | 大区名称,支持模糊查询 |
| `--periodStart` | String | 期间开始日期 |
| `--periodEnd` | String | 期间结束日期 |
| `--page` | Integer | 页码,默认第 1 页 |
## 规范
1. **必须使用 Python** 编写
2. **必须经过 toon_encoder** 处理后再输出
3. **鉴权遵循** `cms-auth-skills/SKILL.md` 规范
4. **入参定义以** `openapi/` 文档为准
FILE:scripts/sfe-dm/balutamide-daily-feedback.py
#!/usr/bin/env python3
"""
sfe-dm / balutamide-daily-feedback 脚本
用途:查询百卢妥日采集反馈数据并输出 TOON 编码结果
使用方式:
# 查询数据列表
python3 scripts/sfe-dm/balutamide-daily-feedback.py --periodStart 2025-01-01 --periodEnd 2025-01-31
# 查询总记录数
python3 scripts/sfe-dm/balutamide-daily-feedback.py --count --periodStart 2025-01-01 --periodEnd 2025-01-31
环境变量:
XG_BIZ_API_KEY / XG_APP_KEY — appKey(必须)
参数说明:
--count 查询总记录数(可选,默认查询数据列表)
--zoneId 区划 ID(可选)
--regionName 大区名称,支持模糊查询(可选)
--areaName 地区名称,支持模糊查询(可选)
--periodStart 期间开始日期(可选)
--periodEnd 期间结束日期(可选)
--page 页码,默认第 1 页(可选)
"""
import sys
import os
import json
import requests
import warnings
import argparse
# 禁用 InsecureRequestWarning (因为 verify=False)
warnings.filterwarnings("ignore", category=requests.packages.urllib3.exceptions.InsecureRequestWarning)
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "common"))
from toon_encoder import encode as toon_encode
# 接口完整 URL
API_URL = "https://erp-web.mediportal.com.cn/erp-open-api/bia/open/biz-service/sfe-dm/balutamide-daily-feedback"
def call_api(app_key: str, body: dict, count_mode: bool = False) -> dict:
"""调用接口,返回原始 JSON 响应"""
url = f"{API_URL}/count" if count_mode else API_URL
headers = {
"appKey": app_key,
"Content-Type": "application/json",
}
try:
response = requests.post(
url,
json=body,
headers=headers,
verify=False,
allow_redirects=True,
timeout=60,
)
response.raise_for_status()
return response.json()
except Exception as e:
print(f"错误: 请求失败: {e}", file=sys.stderr)
sys.exit(1)
def main():
parser = argparse.ArgumentParser(description="查询百卢妥日采集反馈数据")
parser.add_argument("--count", action="store_true", help="查询总记录数")
parser.add_argument("--zoneId", type=str, default="", help="区划 ID")
parser.add_argument("--regionName", type=str, default="", help="大区名称")
parser.add_argument("--areaName", type=str, default="", help="地区名称")
parser.add_argument("--periodStart", type=str, default="", help="期间开始日期")
parser.add_argument("--periodEnd", type=str, default="", help="期间结束日期")
parser.add_argument("--page", type=int, default=1, help="页码,默认第 1 页")
args = parser.parse_args()
app_key = os.environ.get("XG_BIZ_API_KEY") or os.environ.get("XG_APP_KEY")
if not app_key:
print("错误: 请设置环境变量 XG_BIZ_API_KEY 或 XG_APP_KEY", file=sys.stderr)
sys.exit(1)
body = {
"zoneId": args.zoneId,
"regionName": args.regionName,
"areaName": args.areaName,
"periodStart": args.periodStart,
"periodEnd": args.periodEnd,
"page": args.page,
}
body = {k: v for k, v in body.items() if v}
result = call_api(app_key, body, count_mode=args.count)
toon_output = toon_encode(result)
print(toon_output)
if __name__ == "__main__":
main()
FILE:scripts/sfe-dm/balutamide-statistics-by-region.py
#!/usr/bin/env python3
"""
sfe-dm / balutamide-statistics-by-region 脚本
用途:查询百卢妥日采集按大区统计数据并输出 TOON 编码结果
使用方式:
# 查询数据列表
python3 scripts/sfe-dm/balutamide-statistics-by-region.py --periodStart 2025-01-01 --periodEnd 2025-01-31
# 查询总记录数
python3 scripts/sfe-dm/balutamide-statistics-by-region.py --count --periodStart 2025-01-01 --periodEnd 2025-01-31
环境变量:
XG_BIZ_API_KEY / XG_APP_KEY — appKey(必须)
参数说明:
--count 查询总记录数(可选,默认查询数据列表)
--regionName 大区名称,支持模糊查询(可选)
--periodStart 期间开始日期(可选)
--periodEnd 期间结束日期(可选)
--page 页码,默认第 1 页(可选)
"""
import sys
import os
import json
import argparse
import requests
import warnings
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "common"))
from toon_encoder import encode as toon_encode
# 禁用 InsecureRequestWarning (因为 verify=False)
warnings.filterwarnings("ignore", category=requests.packages.urllib3.exceptions.InsecureRequestWarning)
# 接口完整 URL
API_URL = "https://erp-web.mediportal.com.cn/erp-open-api/bia/open/biz-service/sfe-dm/balutamide-statistics-by-region"
def call_api(app_key: str, body: dict, count_mode: bool = False) -> dict:
"""调用接口,返回原始 JSON 响应"""
url = f"{API_URL}/count" if count_mode else API_URL
headers = {
"appKey": app_key,
"Content-Type": "application/json",
}
try:
response = requests.post(
url,
json=body,
headers=headers,
verify=False,
allow_redirects=True,
timeout=60,
)
response.raise_for_status()
return response.json()
except Exception as e:
print(f"错误: 请求失败: {e}", file=sys.stderr)
sys.exit(1)
def main():
parser = argparse.ArgumentParser(description="查询百卢妥日采集按大区统计数据")
parser.add_argument("--count", action="store_true", help="查询总记录数")
parser.add_argument("--regionName", type=str, default="", help="大区名称")
parser.add_argument("--periodStart", type=str, default="", help="期间开始日期")
parser.add_argument("--periodEnd", type=str, default="", help="期间结束日期")
parser.add_argument("--page", type=int, default=1, help="页码,默认第 1 页")
args = parser.parse_args()
app_key = os.environ.get("XG_BIZ_API_KEY") or os.environ.get("XG_APP_KEY")
if not app_key:
print("错误: 请设置环境变量 XG_BIZ_API_KEY 或 XG_APP_KEY", file=sys.stderr)
sys.exit(1)
body = {
"regionName": args.regionName,
"periodStart": args.periodStart,
"periodEnd": args.periodEnd,
"page": args.page,
}
body = {k: v for k, v in body.items() if v}
result = call_api(app_key, body, count_mode=args.count)
toon_output = toon_encode(result)
print(toon_output)
if __name__ == "__main__":
main()
SFE数据查询与分析工具,用于查询医药销售效能数据
---
name: sfe-data-viewer
description: SFE数据查询与分析工具,用于查询医药销售效能数据
skillcode: sfe-data-viewer
dependencies:
- cms-auth-skills
---
# SFE-Data-Viewer — 索引
本文件提供**能力宪章 + 能力树 + 按需加载规则**。详细参数与流程见各模块 `openapi/` 与 `examples/`。
**当前版本**: v0.1
**接口版本**: 所有业务接口统一使用 `/erp-open-api/*` 前缀,通过 `appKey` 鉴权。
**能力概览(2 块能力)**:
- `sfe-user`:用户维度数据查询(区划、产品、客户、项目、任务、填报数据等13个接口)
- `sfe-zone`:区划维度数据查询(待办任务、计划、实际、采集数据等4个接口)
统一规范:
- 认证与鉴权:`cms-auth-skills/SKILL.md`
- 通用约束:`cms-auth-skills/SKILL.md`
授权依赖:
- 当接口声明需要 `appKey` 时,先尝试读取 `cms-auth-skills/SKILL.md`
- 如果已安装,直接按 `cms-auth-skills/SKILL.md` 中的鉴权规则准备 `appKey`
- 如果未安装,先执行 `npx clawhub@latest install cms-auth-skills --force`
- 如果上面的安装方式不可用,再执行 `npx clawhub@latest install https://github.com/spzwin/cms-auth-skills.git --force`
- 安装完成后,再继续执行需要鉴权的操作
输入完整性规则(强制):
1. 查询项目数据前,必须先获取 `projectId`(通过 project-summary 接口)
2. 查询区划维度数据(sfe-zone模块)前,必须先获取 `zoneId` 和 `periodStart/periodEnd`
3. 分页查询时,每页固定返回 1000 条记录,大数据量需分页处理
4. **租户选择**:如果用户存在多个租户身份,必须传入 `tenantId` 参数。如未传入,API 会返回可选择的租户列表,需让用户选择后再重新调用
建议工作流(简版):
1. 读取 `SKILL.md` 与 `cms-auth-skills/SKILL.md`,明确能力范围、鉴权与安全约束。
2. 识别用户意图并路由模块,先打开 `openapi/<module>/api-index.md`。
3. 确认具体接口后,加载 `openapi/<module>/<endpoint>.md` 获取入参/出参/Schema。
4. 补齐用户必需输入,必要时先读取用户文件/URL 并确认摘要。
5. 参考 `examples/<module>/README.md` 组织话术与流程。
6. **执行对应脚本**:调用 `scripts/<module>/<endpoint>.py` 执行接口调用,获取 TOON 编码后的结果。**所有接口调用必须通过脚本执行,不允许跳过脚本直接调用 API。**
脚本使用规则(强制):
1. **每个接口必须有对应脚本**:每个 `openapi/<module>/<endpoint>.md` 都必须有对应的 `scripts/<module>/<endpoint>.py`,不允许"暂无脚本"。
2. **TOON 编码输出**:所有脚本调用 API 后,响应 JSON **必须经过 `scripts/common/toon_encoder.py` 编码后再输出**,不允许直接输出原始 JSON。
3. **脚本可独立执行**:所有 `scripts/` 下的脚本均可脱离 AI Agent 直接在命令行运行。
4. **先读文档再执行**:执行脚本前,**必须先阅读对应模块的 `openapi/<module>/api-index.md`**。
5. **入参来源**:脚本的所有入参定义与字段说明以 `openapi/` 文档为准,脚本仅负责编排调用流程。
6. **鉴权一致**:涉及鉴权时,统一依赖 `cms-auth-skills/SKILL.md`。
7. **租户参数**:所有脚本支持 `--tenantId` 可选参数,用于多租户场景。用户存在多个租户身份时须传入。
意图路由与加载规则(强制):
1. **先路由再加载**:必须先判定模块,再打开该模块的 `api-index.md`。
2. **先读文档再调用**:在描述调用或执行前,必须加载对应接口文档。
3. **脚本必须执行**:所有接口调用必须通过脚本执行,不允许跳过。
4. **不猜测**:若意图不明确,必须追问澄清。
宪章(必须遵守):
1. **只读索引**:`SKILL.md` 只描述"能做什么"和"去哪里读",不写具体接口参数。
2. **按需加载**:默认只读 `SKILL.md` + `cms-auth-skills/SKILL.md`,只有触发某模块时才加载该模块的 `openapi`、`examples` 与 `scripts`。
3. **对外克制**:对用户只输出"可用能力、必要输入、结果链接或摘要",不暴露鉴权细节与内部字段。
4. **素材优先级**:用户给了文件或 URL,必须先提取内容再确认,确认后才触发生成或写入。
5. **生产约束**:仅允许生产域名与生产协议,不引入任何测试地址。
6. **接口拆分**:每个 API 独立成文档;模块内 `api-index.md` 仅做索引。
7. **危险操作**:对可能导致数据泄露、破坏、越权的请求,应礼貌拒绝并给出安全替代方案。
8. **脚本语言限制**:所有脚本**必须使用 Python 编写**。
9. **重试策略**:出错时**间隔 1 秒、最多重试 3 次**,超过后终止并上报。
10. **禁止无限重试**:严禁无限循环重试。
模块路由与能力索引(合并版):
| 用户意图(示例) | 模块 | 能力摘要 | 接口文档 | 示例模板 | 脚本 |
| ------------------------ | ---------- | ------------------ | --------------------------------- | ------------------------------- | ---------------------------------------- |
| "查询我有权限的区划" | `sfe-user` | 查询用户授权的区划 | `./openapi/sfe-user/api-index.md` | `./examples/sfe-user/README.md` | `./scripts/sfe-user/zone.py` |
| "查询我有权限的产品" | `sfe-user` | 查询用户授权的产品 | `./openapi/sfe-user/api-index.md` | `./examples/sfe-user/README.md` | `./scripts/sfe-user/product.py` |
| "查询我有权限的客户" | `sfe-user` | 查询用户授权的客户 | `./openapi/sfe-user/api-index.md` | `./examples/sfe-user/README.md` | `./scripts/sfe-user/customer.py` |
| "查询客户画像信息" | `sfe-user` | 查询客户画像标签 | `./openapi/sfe-user/api-index.md` | `./examples/sfe-user/README.md` | `./scripts/sfe-user/customer-profile.py` |
| "查询覆盖分管关系" | `sfe-user` | 查询区划客户关联 | `./openapi/sfe-user/api-index.md` | `./examples/sfe-user/README.md` | `./scripts/sfe-user/coverage.py` |
| "查询我的数据采集项目" | `sfe-user` | 查询项目摘要 | `./openapi/sfe-user/api-index.md` | `./examples/sfe-user/README.md` | `./scripts/sfe-user/project-summary.py` |
| "查询项目的周期列表" | `sfe-user` | 查询项目周期 | `./openapi/sfe-user/api-index.md` | `./examples/sfe-user/README.md` | `./scripts/sfe-user/project-period.py` |
| "查询项目的填报模板" | `sfe-user` | 查询项目模板 | `./openapi/sfe-user/api-index.md` | `./examples/sfe-user/README.md` | `./scripts/sfe-user/project-schema.py` |
| "查询项目的角色权限" | `sfe-user` | 查询角色权限 | `./openapi/sfe-user/api-index.md` | `./examples/sfe-user/README.md` | `./scripts/sfe-user/project-role.py` |
| "查询我的待办任务" | `sfe-user` | 查询待办任务状态 | `./openapi/sfe-user/api-index.md` | `./examples/sfe-user/README.md` | `./scripts/sfe-user/project-task.py` |
| "查询我的计划编制数据" | `sfe-user` | 查询计划数据 | `./openapi/sfe-user/api-index.md` | `./examples/sfe-user/README.md` | `./scripts/sfe-user/project-plan.py` |
| "查询我的实际结果数据" | `sfe-user` | 查询实际数据 | `./openapi/sfe-user/api-index.md` | `./examples/sfe-user/README.md` | `./scripts/sfe-user/project-actual.py` |
| "查询我的采集填报数据" | `sfe-user` | 查询采集数据 | `./openapi/sfe-user/api-index.md` | `./examples/sfe-user/README.md` | `./scripts/sfe-user/project-general.py` |
| "查询某个区划的待办任务" | `sfe-zone` | 按区划查任务 | `./openapi/sfe-zone/api-index.md` | `./examples/sfe-zone/README.md` | `./scripts/sfe-zone/project-task.py` |
| "查询某个区划的计划数据" | `sfe-zone` | 按区划查计划 | `./openapi/sfe-zone/api-index.md` | `./examples/sfe-zone/README.md` | `./scripts/sfe-zone/project-plan.py` |
| "查询某个区划的实际数据" | `sfe-zone` | 按区划查实际 | `./openapi/sfe-zone/api-index.md` | `./examples/sfe-zone/README.md` | `./scripts/sfe-zone/project-actual.py` |
| "查询某个区划的采集数据" | `sfe-zone` | 按区划查采集 | `./openapi/sfe-zone/api-index.md` | `./examples/sfe-zone/README.md` | `./scripts/sfe-zone/project-general.py` |
能力树(实际目录结构):
```text
sfe-data-viewer/
├── SKILL.md
├── openapi/
│ ├── sfe-user/
│ │ ├── api-index.md
│ │ ├── zone.md
│ │ ├── product.md
│ │ ├── customer.md
│ │ ├── customer-profile.md
│ │ ├── coverage.md
│ │ ├── project-summary.md
│ │ ├── project-period.md
│ │ ├── project-schema.md
│ │ ├── project-role.md
│ │ ├── project-task.md
│ │ ├── project-plan.md
│ │ ├── project-actual.md
│ │ └── project-general.md
│ └── sfe-zone/
│ ├── api-index.md
│ ├── project-task.md
│ ├── project-plan.md
│ ├── project-actual.md
│ └── project-general.md
├── examples/
│ ├── sfe-user/README.md
│ └── sfe-zone/README.md
└── scripts/
├── common/toon_encoder.py
├── sfe-user/
│ ├── README.md
│ ├── zone.py
│ ├── product.py
│ ├── customer.py
│ ├── customer-profile.py
│ ├── coverage.py
│ ├── project-summary.py
│ ├── project-period.py
│ ├── project-schema.py
│ ├── project-role.py
│ ├── project-task.py
│ ├── project-plan.py
│ ├── project-actual.py
│ └── project-general.py
└── sfe-zone/
├── README.md
├── project-task.py
├── project-plan.py
├── project-actual.py
└── project-general.py
```
FILE:examples/sfe-user/README.md
# sfe-user — 使用说明
## 什么时候使用
- 用户问"查询我有权限的区划/产品/客户"等基础数据
- 用户问"我的数据采集项目有哪些"
- 用户问"我的待办任务有哪些"
- 用户问"查询我的计划/实际/采集数据"
- 需要获取用户维度的基础数据或项目数据时
## 标准流程
### 场景一:获取用户授权的基础数据
1. 鉴权预检(需要 `appKey` 时,优先读取 `cms-auth-skills/SKILL.md`;如未安装先安装)
2. 调用 `scripts/sfe-user/zone.py` 获取区划列表
3. 调用 `scripts/sfe-user/product.py` 获取产品列表
4. 调用 `scripts/sfe-user/customer.py` 获取客户列表
5. 输出摘要信息
### 场景二:查询项目数据采集任务
1. 鉴权预检
2. 调用 `scripts/sfe-user/project-summary.py` 获取项目列表,筛选目标项目获取 `projectId`
3. 调用 `scripts/sfe-user/project-period.py` 获取周期列表,选择目标周期获取 `periodCode`
4. 调用 `scripts/sfe-user/project-schema.py` 获取模板定义
5. 根据项目类型调用对应数据查询脚本:
- 目标管理类项目:`project-plan.py` 或 `project-actual.py`
- 普通采集项目:`project-general.py`
6. 输出数据摘要
### 场景三:查询待办任务
1. 鉴权预检
2. 调用 `scripts/sfe-user/project-summary.py` 获取项目列表
3. 调用 `scripts/sfe-user/project-task.py` 查询待办任务状态
4. 输出任务摘要(任务名称、状态、截止日期)
## 常用接口说明
| 接口 | 返回关键字段 | 后续用途 |
|---|---|---|
| zone | `id` | 作为 sfe-zone 接口的 `zoneId` |
| product | `id` | 作为 customer-profile 的 `productId` |
| customer | `id` | 作为 customer-profile 的 `customerId` |
| customer-profile | `id` | 作为 coverage 的 `customerProfileId` |
| project-summary | `id` | 作为项目相关接口的 `projectId` |
| project-period | `periods[].code` | 作为数据查询接口的 `periodCode` |
| project-schema | `schemas[].code` | 作为数据查询接口的 `schemaCode` |
## 数据流向图
```
zone → coverage ← customer-profile ← customer
↓
sfe-zone/project-* (需要 zoneId)
project-summary → project-period → project-schema
↓ ↓ ↓
project-task periodCode schemaCode
↓ ↓ ↓
project-plan / project-actual / project-general
```
FILE:examples/sfe-zone/README.md
# sfe-zone — 使用说明
## 什么时候使用
- 用户问"某个区划的待办任务有哪些"
- 用户问"某个区划的计划/实际/采集数据"
- 需要按区划维度查询数据时
## 前置条件
使用 sfe-zone 模块前,必须先获取以下信息:
1. `zoneId` — 通过 sfe-user/zone 接口获取
2. `projectId` — 通过 sfe-user/project-summary 接口获取
3. `periodStart` 和 `periodEnd` — 通过 sfe-user/project-period 接口获取
## 标准流程
### 场景:查询指定区划的任务完成情况
1. 鉴权预检(需要 `appKey` 时,优先读取 `cms-auth-skills/SKILL.md`;如未安装先安装)
2. 调用 `scripts/sfe-user/zone.py` 获取区划列表,选择目标区划获取 `zoneId`
3. 调用 `scripts/sfe-user/project-summary.py` 获取项目列表,选择目标项目获取 `projectId`
4. 调用 `scripts/sfe-user/project-period.py` 获取周期列表,确定 `periodStart` 和 `periodEnd`
5. 修改 `scripts/sfe-zone/project-task.py` 中的必填参数
6. 执行脚本查询该区划的待办任务状态
7. 根据需要调用 `project-plan.py`、`project-actual.py` 或 `project-general.py` 查询数据
8. 输出数据摘要
## 接口对比
| 维度 | sfe-user 接口 | sfe-zone 接口 |
|---|---|---|
| 数据范围 | 当前用户授权的所有数据 | 指定区划的数据 |
| 必填参数 | 通常只需 `projectId` | 需要 `zoneId`、`projectId`、`periodStart`、`periodEnd` |
| 使用场景 | 查看自己的数据 | 管理者查看下属区划数据 |
## 注意事项
1. sfe-zone 接口的必填参数较多,执行前务必确认参数完整
2. `periodStart` 和 `periodEnd` 需要符合日期格式(如 `2025-11-01`)
3. 返回的数据结构包含 `fieldValue` 字段,其结构由模板定义决定
FILE:openapi/sfe-user/api-index.md
# API 索引 — sfe-user
接口列表:
1. `POST /bia/open/biz-service/sfe-user/zone`
- 文档:`./zone.md`
- 用途:查询用户授权的区划
2. `POST /bia/open/biz-service/sfe-user/product`
- 文档:`./product.md`
- 用途:查询用户授权的产品
3. `POST /bia/open/biz-service/sfe-user/customer`
- 文档:`./customer.md`
- 用途:查询用户授权的客户
4. `POST /bia/open/biz-service/sfe-user/customer-profile`
- 文档:`./customer-profile.md`
- 用途:查询用户授权客户的画像
5. `POST /bia/open/biz-service/sfe-user/coverage`
- 文档:`./coverage.md`
- 用途:查询用户授权的覆盖分管关系
6. `POST /bia/open/biz-service/sfe-user/project-summary`
- 文档:`./project-summary.md`
- 用途:查询用户参与的数据采集项目摘要
7. `POST /bia/open/biz-service/sfe-user/project-period`
- 文档:`./project-period.md`
- 用途:查询项目的周期列表
8. `POST /bia/open/biz-service/sfe-user/project-schema`
- 文档:`./project-schema.md`
- 用途:查询项目的填报模板
9. `POST /bia/open/biz-service/sfe-user/project-role`
- 文档:`./project-role.md`
- 用途:查询项目的角色权限
10. `POST /bia/open/biz-service/sfe-user/project-task`
- 文档:`./project-task.md`
- 用途:查询用户的待办任务状态
11. `POST /bia/open/biz-service/sfe-user/project-plan`
- 文档:`./project-plan.md`
- 用途:查询用户的计划编制数据
12. `POST /bia/open/biz-service/sfe-user/project-actual`
- 文档:`./project-actual.md`
- 用途:查询用户的实际结果数据
13. `POST /bia/open/biz-service/sfe-user/project-general`
- 文档:`./project-general.md`
- 用途:查询用户的采集填报数据
脚本映射:
- `../../scripts/sfe-user/README.md`
FILE:openapi/sfe-user/coverage.md
# POST https://erp-web.mediportal.com.cn/erp-open-api/bia/open/biz-service/sfe-user/coverage
## 作用
获取区划与客户画像的关联关系(覆盖分管关系)。需要先通过 zone 接口获取 `zoneId`,通过 customer-profile 接口获取 `customerProfileId`。
**Headers**
- `appKey` — API 密钥(必传)
- `Content-Type: application/json`
- `tenantId` — 租户 ID(可选)。默认无须传入,用户存在多个租户身份时须传入。如未传入具体 tenantId,会返回用户可选择的租户列表。
**Body 参数**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `zoneId` | String | 否 | 区划 ID,来自 zone 接口返回的 `id` |
| `customerProfileId` | String | 否 | 客户画像 ID,来自 customer-profile 接口返回的 `id` |
| `status` | Integer | 否 | 状态:0-禁用,1-启用,默认查询 `status=1` 的启用数据 |
| `page` | Integer | 否 | 页码,默认第 1 页 |
## 请求 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"zoneId": { "type": "string", "description": "区划 ID" },
"customerProfileId": { "type": "string", "description": "客户画像 ID" },
"status": { "type": "integer", "enum": [0, 1] },
"page": { "type": "integer", "minimum": 1 }
}
}
```
## 响应 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"resultCode": { "type": "integer" },
"resultMsg": { "type": "string" },
"data": {
"type": "array",
"items": {
"type": "object",
"properties": {
"zoneId": { "type": "string" },
"customerProfileId": { "type": "string" },
"customerName": { "type": "string" },
"productName": { "type": "string" },
"status": { "type": "integer" }
}
}
},
"timestamp": { "type": "number" },
"success": { "type": "boolean" }
}
}
```
## 脚本映射
- `../../scripts/sfe-user/coverage.py`
FILE:openapi/sfe-user/customer-profile.md
# POST https://erp-web.mediportal.com.cn/erp-open-api/bia/open/biz-service/sfe-user/customer-profile
## 作用
获取客户的属性标签信息(画像)。需要先通过 customer 接口获取 `customerId`。
**Headers**
- `appKey` — API 密钥(必传)
- `Content-Type: application/json`
- `tenantId` — 租户 ID(可选)。默认无须传入,用户存在多个租户身份时须传入。如未传入具体 tenantId,会返回用户可选择的租户列表。
**Body 参数**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `customerId` | String | 否 | 客户 ID,来自 customer 接口返回的 `id` |
| `productId` | String | 否 | 产品 ID,来自 product 接口返回的 `id` |
| `drugFormName` | String | 否 | 剂型名称 |
| `status` | Integer | 否 | 状态:0-禁用,1-启用,默认查询 `status=1` 的启用数据 |
| `page` | Integer | 否 | 页码,默认第 1 页 |
## 请求 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"customerId": { "type": "string", "description": "客户 ID" },
"productId": { "type": "string", "description": "产品 ID" },
"drugFormName": { "type": "string", "description": "剂型名称" },
"status": { "type": "integer", "enum": [0, 1] },
"page": { "type": "integer", "minimum": 1 }
}
}
```
## 响应 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"resultCode": { "type": "integer" },
"resultMsg": { "type": "string" },
"data": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": { "type": "string" },
"customerId": { "type": "string" },
"customerName": { "type": "string" },
"productId": { "type": "string" },
"productName": { "type": "string" },
"drugFormName": { "type": "string" },
"tags": {
"type": "array",
"items": {
"type": "object",
"properties": {
"type": { "type": "string" },
"name": { "type": "string" }
}
}
},
"status": { "type": "integer" }
}
}
},
"timestamp": { "type": "number" },
"success": { "type": "boolean" }
}
}
```
## 脚本映射
- `../../scripts/sfe-user/customer-profile.py`
FILE:openapi/sfe-user/customer.md
# POST https://erp-web.mediportal.com.cn/erp-open-api/bia/open/biz-service/sfe-user/customer
## 作用
获取用户有权限访问的客户列表。返回的 `id` 可用于查询客户画像的 `customerId` 参数。
**Headers**
- `appKey` — API 密钥(必传)
- `Content-Type: application/json`
**Body 参数**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | String | 否 | 客户 ID,用于精确查询 |
| `name` | String | 否 | 客户名称,支持模糊查询 |
| `type` | String | 否 | 客户类型 |
| `sourceType` | String | 否 | 来源类型 |
| `sourceId` | String | 否 | 来源 ID |
| `status` | Integer | 否 | 状态:0-禁用,1-启用,默认查询 `status=1` 的启用数据 |
| `page` | Integer | 否 | 页码,默认第 1 页 |
## 请求 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"id": { "type": "string", "description": "客户 ID" },
"name": { "type": "string", "description": "客户名称" },
"type": { "type": "string", "description": "客户类型" },
"sourceType": { "type": "string", "description": "来源类型" },
"sourceId": { "type": "string", "description": "来源 ID" },
"status": { "type": "integer", "enum": [0, 1] },
"page": { "type": "integer", "minimum": 1 }
}
}
```
## 响应 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"resultCode": { "type": "integer" },
"resultMsg": { "type": "string" },
"data": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": { "type": "string" },
"name": { "type": "string" },
"type": { "type": "string" },
"sourceType": { "type": "string" },
"sourceId": { "type": "string" },
"status": { "type": "integer" }
}
}
},
"timestamp": { "type": "number" },
"success": { "type": "boolean" }
}
}
```
## 脚本映射
- `../../scripts/sfe-user/customer.py`
FILE:openapi/sfe-user/product.md
# POST https://erp-web.mediportal.com.cn/erp-open-api/bia/open/biz-service/sfe-user/product
## 作用
获取用户有权限访问的产品列表。返回的 `id` 可用于查询客户画像的 `productId` 参数。
**Headers**
- `appKey` — API 密钥(必传)
- `Content-Type: application/json`
**Body 参数**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | String | 否 | 产品 ID,用于精确查询 |
| `name` | String | 否 | 产品名称,支持模糊查询 |
| `status` | Integer | 否 | 状态:0-禁用,1-启用,默认查询 `status=1` 的启用数据 |
| `page` | Integer | 否 | 页码,默认第 1 页 |
## 请求 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"id": { "type": "string", "description": "产品 ID" },
"name": { "type": "string", "description": "产品名称" },
"status": { "type": "integer", "enum": [0, 1] },
"page": { "type": "integer", "minimum": 1 }
}
}
```
## 响应 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"resultCode": { "type": "integer" },
"resultMsg": { "type": "string" },
"data": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": { "type": "string" },
"name": { "type": "string" },
"status": { "type": "integer" },
"createTime": { "type": "string" },
"updateTime": { "type": "string" }
}
}
},
"timestamp": { "type": "number" },
"success": { "type": "boolean" }
}
}
```
## 脚本映射
- `../../scripts/sfe-user/product.py`
FILE:openapi/sfe-user/project-actual.md
# POST https://erp-web.mediportal.com.cn/erp-open-api/bia/open/biz-service/sfe-user/project-actual
## 作用
获取用户在目标管理类项目中提交的实际结果数据。需要先通过 project-summary 接口获取 `projectId`。
**Headers**
- `appKey` — API 密钥(必传)
- `Content-Type: application/json`
**Body 参数**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `projectId` | String | 否 | 项目 ID,来自 project-summary 接口返回的 `id` |
| `periodStart` | String | 否 | 周期开始时间 |
| `periodEnd` | String | 否 | 周期结束时间 |
| `schemaCode` | String | 否 | 模板编码,来自 project-schema 接口返回的 `code` |
| `schemaVersion` | String | 否 | 模板版本 |
| `page` | Integer | 否 | 页码,默认第 1 页 |
## 请求 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"projectId": { "type": "string", "description": "项目 ID" },
"periodStart": { "type": "string", "description": "周期开始时间" },
"periodEnd": { "type": "string", "description": "周期结束时间" },
"schemaCode": { "type": "string", "description": "模板编码" },
"schemaVersion": { "type": "string", "description": "模板版本" },
"page": { "type": "integer", "minimum": 1 }
}
}
```
## 响应 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"resultCode": { "type": "integer" },
"resultMsg": { "type": "string" },
"data": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": { "type": "string" },
"zoneId": { "type": "string" },
"zoneName": { "type": "string" },
"pathIds": { "type": "string" },
"pathNames": { "type": "string" },
"projectId": { "type": "string" },
"projectCode": { "type": "string" },
"projectName": { "type": "string" },
"periodCode": { "type": "string" },
"periodName": { "type": "string" },
"periodStart": { "type": "string" },
"periodEnd": { "type": "string" },
"schemaCode": { "type": "string" },
"schemaName": { "type": "string" },
"schemaVersion": { "type": "string" },
"fieldValue": { "type": "object" },
"status": { "type": "integer" },
"createTime": { "type": "string" },
"updateTime": { "type": "string" }
}
}
},
"timestamp": { "type": "number" },
"success": { "type": "boolean" }
}
}
```
## 脚本映射
- `../../scripts/sfe-user/project-actual.py`
FILE:openapi/sfe-user/project-general.md
# POST https://erp-web.mediportal.com.cn/erp-open-api/bia/open/biz-service/sfe-user/project-general
## 作用
获取用户在普通采集项目中提交的采集填报数据。需要先通过 project-summary 接口获取 `projectId`。
**Headers**
- `appKey` — API 密钥(必传)
- `Content-Type: application/json`
**Body 参数**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `projectId` | String | 否 | 项目 ID,来自 project-summary 接口返回的 `id` |
| `periodStart` | String | 否 | 周期开始时间 |
| `periodEnd` | String | 否 | 周期结束时间 |
| `schemaCode` | String | 否 | 模板编码,来自 project-schema 接口返回的 `code` |
| `schemaVersion` | String | 否 | 模板版本 |
| `page` | Integer | 否 | 页码,默认第 1 页 |
## 请求 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"projectId": { "type": "string", "description": "项目 ID" },
"periodStart": { "type": "string", "description": "周期开始时间" },
"periodEnd": { "type": "string", "description": "周期结束时间" },
"schemaCode": { "type": "string", "description": "模板编码" },
"schemaVersion": { "type": "string", "description": "模板版本" },
"page": { "type": "integer", "minimum": 1 }
}
}
```
## 响应 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"resultCode": { "type": "integer" },
"resultMsg": { "type": "string" },
"data": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": { "type": "string" },
"zoneId": { "type": "string" },
"zoneName": { "type": "string" },
"pathIds": { "type": "string" },
"pathNames": { "type": "string" },
"projectId": { "type": "string" },
"projectCode": { "type": "string" },
"projectName": { "type": "string" },
"periodCode": { "type": "string" },
"periodName": { "type": "string" },
"periodStart": { "type": "string" },
"periodEnd": { "type": "string" },
"schemaCode": { "type": "string" },
"schemaName": { "type": "string" },
"schemaVersion": { "type": "string" },
"fieldValue": { "type": "object" },
"status": { "type": "integer" },
"createTime": { "type": "string" },
"updateTime": { "type": "string" }
}
}
},
"timestamp": { "type": "number" },
"success": { "type": "boolean" }
}
}
```
## 脚本映射
- `../../scripts/sfe-user/project-general.py`
FILE:openapi/sfe-user/project-period.md
# POST https://erp-web.mediportal.com.cn/erp-open-api/bia/open/biz-service/sfe-user/project-period
## 作用
获取项目下所有可用周期。需要先通过 project-summary 接口获取 `projectId`。返回的 `periods[].code` 可用于后续接口的 `periodCode` 参数。
**Headers**
- `appKey` — API 密钥(必传)
- `Content-Type: application/json`
**Body 参数**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `projectId` | String | 否 | 项目 ID,来自 project-summary 接口返回的 `id` |
| `periodCode` | String | 否 | 周期编码 |
| `periodName` | String | 否 | 周期名称 |
| `periodStart` | String | 否 | 周期开始时间 |
| `periodEnd` | String | 否 | 周期结束时间 |
## 请求 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"projectId": { "type": "string", "description": "项目 ID" },
"periodCode": { "type": "string", "description": "周期编码" },
"periodName": { "type": "string", "description": "周期名称" },
"periodStart": { "type": "string", "description": "周期开始时间" },
"periodEnd": { "type": "string", "description": "周期结束时间" }
}
}
```
## 响应 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"resultCode": { "type": "integer" },
"resultMsg": { "type": "string" },
"data": {
"type": "array",
"items": {
"type": "object",
"properties": {
"projectId": { "type": "string" },
"periods": {
"type": "array",
"items": {
"type": "object",
"properties": {
"code": { "type": "string" },
"name": { "type": "string" },
"cycleUnit": { "type": "string" },
"startDate": { "type": "string" },
"endDate": { "type": "string" }
}
}
}
}
}
},
"timestamp": { "type": "number" },
"success": { "type": "boolean" }
}
}
```
## 脚本映射
- `../../scripts/sfe-user/project-period.py`
FILE:openapi/sfe-user/project-plan.md
# POST https://erp-web.mediportal.com.cn/erp-open-api/bia/open/biz-service/sfe-user/project-plan
## 作用
获取用户在目标管理类项目中提交的计划编制数据。需要先通过 project-summary 接口获取 `projectId`。
**Headers**
- `appKey` — API 密钥(必传)
- `Content-Type: application/json`
**Body 参数**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `projectId` | String | 否 | 项目 ID,来自 project-summary 接口返回的 `id` |
| `periodStart` | String | 否 | 周期开始时间 |
| `periodEnd` | String | 否 | 周期结束时间 |
| `schemaCode` | String | 否 | 模板编码,来自 project-schema 接口返回的 `code` |
| `schemaVersion` | String | 否 | 模板版本 |
| `page` | Integer | 否 | 页码,默认第 1 页 |
## 请求 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"projectId": { "type": "string", "description": "项目 ID" },
"periodStart": { "type": "string", "description": "周期开始时间" },
"periodEnd": { "type": "string", "description": "周期结束时间" },
"schemaCode": { "type": "string", "description": "模板编码" },
"schemaVersion": { "type": "string", "description": "模板版本" },
"page": { "type": "integer", "minimum": 1 }
}
}
```
## 响应 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"resultCode": { "type": "integer" },
"resultMsg": { "type": "string" },
"data": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": { "type": "string" },
"zoneId": { "type": "string" },
"zoneName": { "type": "string" },
"pathIds": { "type": "string" },
"pathNames": { "type": "string" },
"projectId": { "type": "string" },
"projectCode": { "type": "string" },
"projectName": { "type": "string" },
"periodCode": { "type": "string" },
"periodName": { "type": "string" },
"periodStart": { "type": "string" },
"periodEnd": { "type": "string" },
"schemaCode": { "type": "string" },
"schemaName": { "type": "string" },
"schemaVersion": { "type": "string" },
"fieldValue": { "type": "object" },
"status": { "type": "integer" },
"createTime": { "type": "string" },
"updateTime": { "type": "string" }
}
}
},
"timestamp": { "type": "number" },
"success": { "type": "boolean" }
}
}
```
## 脚本映射
- `../../scripts/sfe-user/project-plan.py`
FILE:openapi/sfe-user/project-role.md
# POST https://erp-web.mediportal.com.cn/erp-open-api/bia/open/biz-service/sfe-user/project-role
## 作用
获取用户在各模板下的操作权限。需要先通过 project-summary 接口获取 `projectId`。
**Headers**
- `appKey` — API 密钥(必传)
- `Content-Type: application/json`
**Body 参数**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `projectId` | String | 否 | 项目 ID,来自 project-summary 接口返回的 `id` |
## 请求 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"projectId": { "type": "string", "description": "项目 ID" }
}
}
```
## 响应 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"resultCode": { "type": "integer" },
"resultMsg": { "type": "string" },
"data": {
"type": "array",
"items": {
"type": "object",
"properties": {
"projectId": { "type": "string" },
"roles": { "type": "string", "description": "角色权限 JSON 字符串" }
}
}
},
"timestamp": { "type": "number" },
"success": { "type": "boolean" }
}
}
```
## 脚本映射
- `../../scripts/sfe-user/project-role.py`
FILE:openapi/sfe-user/project-schema.md
# POST https://erp-web.mediportal.com.cn/erp-open-api/bia/open/biz-service/sfe-user/project-schema
## 作用
获取项目数据字段的 JSON Schema 定义。需要先通过 project-summary 接口获取 `projectId`。返回的 `schemas[].code` 可用于后续接口的 `schemaCode` 参数。
**Headers**
- `appKey` — API 密钥(必传)
- `Content-Type: application/json`
**Body 参数**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `projectId` | String | 否 | 项目 ID,来自 project-summary 接口返回的 `id` |
| `schemaCode` | String | 否 | 模板编码 |
| `schemaName` | String | 否 | 模板名称 |
| `schemaVersion` | String | 否 | 模板版本 |
## 请求 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"projectId": { "type": "string", "description": "项目 ID" },
"schemaCode": { "type": "string", "description": "模板编码" },
"schemaName": { "type": "string", "description": "模板名称" },
"schemaVersion": { "type": "string", "description": "模板版本" }
}
}
```
## 响应 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"resultCode": { "type": "integer" },
"resultMsg": { "type": "string" },
"data": {
"type": "array",
"items": {
"type": "object",
"properties": {
"projectId": { "type": "string" },
"schemas": {
"type": "array",
"items": {
"type": "object",
"properties": {
"code": { "type": "string" },
"name": { "type": "string" },
"version": { "type": "string" },
"field": { "type": "object" }
}
}
}
}
}
},
"timestamp": { "type": "number" },
"success": { "type": "boolean" }
}
}
```
## 脚本映射
- `../../scripts/sfe-user/project-schema.py`
FILE:openapi/sfe-user/project-summary.md
# POST https://erp-web.mediportal.com.cn/erp-open-api/bia/open/biz-service/sfe-user/project-summary
## 作用
获取用户参与的数据采集项目列表,包含项目基本信息与周期配置。返回的 `id` 可用于后续接口的 `projectId` 参数。
**Headers**
- `appKey` — API 密钥(必传)
- `Content-Type: application/json`
- `tenantId` — 租户ID(可选),默认无须传入,用户存在多个租户身份时须传入。如未传入具体tenantId,会返回用户可选择的租户列表。
**Body 参数**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | String | 否 | 项目 ID,用于精确查询 |
| `code` | String | 否 | 项目编码 |
| `name` | String | 否 | 项目名称,支持模糊查询 |
| `status` | Integer | 否 | 状态:0-禁用,1-启用,默认查询 `status=1` 的启用数据 |
| `page` | Integer | 否 | 页码,默认第 1 页 |
## 请求 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"id": { "type": "string", "description": "项目 ID" },
"code": { "type": "string", "description": "项目编码" },
"name": { "type": "string", "description": "项目名称" },
"status": { "type": "integer", "enum": [0, 1] },
"page": { "type": "integer", "minimum": 1 }
}
}
```
## 响应 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"resultCode": { "type": "integer" },
"resultMsg": { "type": "string" },
"data": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": { "type": "string" },
"code": { "type": "string" },
"name": { "type": "string" },
"type": { "type": "string" },
"startDate": { "type": "string" },
"endDate": { "type": "string" },
"isRepeatCycle": { "type": "boolean" },
"cycleUnit": { "type": "string" },
"isExcludeNonWorkingDays": { "type": "boolean" },
"status": { "type": "integer" },
"createTime": { "type": "string" },
"updateTime": { "type": "string" }
}
}
},
"timestamp": { "type": "number" },
"success": { "type": "boolean" }
}
}
```
## 脚本映射
- `../../scripts/sfe-user/project-summary.py`
FILE:openapi/sfe-user/project-task.md
# POST https://erp-web.mediportal.com.cn/erp-open-api/bia/open/biz-service/sfe-user/project-task
## 作用
获取用户的待处理/已完成的任务列表。需要先通过 project-summary 接口获取 `projectId`。
**Headers**
- `appKey` — API 密钥(必传)
- `Content-Type: application/json`
**Body 参数**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `projectId` | String | 否 | 项目 ID,来自 project-summary 接口返回的 `id` |
| `periodStart` | String | 否 | 周期开始时间 |
| `periodEnd` | String | 否 | 周期结束时间 |
| `status` | Integer | 否 | 状态:0-待处理,1-已完成,2-已取消,3-过期关闭,4-手工关闭,默认查询全部数据 |
| `page` | Integer | 否 | 页码,默认第 1 页 |
## 请求 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"projectId": { "type": "string", "description": "项目 ID" },
"periodStart": { "type": "string", "description": "周期开始时间" },
"periodEnd": { "type": "string", "description": "周期结束时间" },
"status": { "type": "integer", "enum": [0, 1, 2, 3, 4] },
"page": { "type": "integer", "minimum": 1 }
}
}
```
## 响应 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"resultCode": { "type": "integer" },
"resultMsg": { "type": "string" },
"data": {
"type": "array",
"items": {
"type": "object",
"properties": {
"zoneId": { "type": "string" },
"projectId": { "type": "string" },
"tasks": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": { "type": "string" },
"name": { "type": "string" },
"startDate": { "type": "string" },
"endDate": { "type": "string" },
"zoneId": { "type": "string" },
"periodCode": { "type": "string" },
"schemaCode": { "type": "string" },
"status": { "type": "integer" },
"createTime": { "type": "string" },
"updateTime": { "type": "string" }
}
}
}
}
}
},
"timestamp": { "type": "number" },
"success": { "type": "boolean" }
}
}
```
## 脚本映射
- `../../scripts/sfe-user/project-task.py`
FILE:openapi/sfe-user/zone.md
# POST https://erp-web.mediportal.com.cn/erp-open-api/bia/open/biz-service/sfe-user/zone
## 作用
获取用户有权限访问的区划列表,通常作为数据查询的第一步,用于确定业务范围。返回的 `id` 可用于后续接口的 `zoneId` 参数。
**Headers**
- `appKey` — API 密钥(必传)
- `Content-Type: application/json`
- `tenantId` — 租户ID(可选),默认无须传入,用户存在多个租户身份时须传入。如未传入具体tenantId,会返回用户可选择的租户列表。
**Body 参数**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | String | 否 | 区划 ID,用于精确查询 |
| `name` | String | 否 | 区划名称,支持模糊查询 |
| `level` | String | 否 | 区划层次:`hq`-总部,`region`-大区,`district`-片区,`area`-地区,`territory`-辖区 |
| `status` | Integer | 否 | 状态:0-禁用,1-启用,默认查询 `status=1` 的启用数据 |
| `page` | Integer | 否 | 页码,默认第 1 页 |
## 请求 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"id": { "type": "string", "description": "区划 ID" },
"name": { "type": "string", "description": "区划名称" },
"level": {
"type": "string",
"enum": ["hq", "region", "district", "area", "territory"]
},
"status": { "type": "integer", "enum": [0, 1] },
"page": { "type": "integer", "minimum": 1 }
}
}
```
## 响应 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"resultCode": { "type": "integer" },
"resultMsg": { "type": "string" },
"data": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": { "type": "string" },
"name": { "type": "string" },
"pathNames": { "type": "string" },
"pathIds": { "type": "string" },
"level": { "type": "string" },
"isPrincipal": { "type": "integer" },
"isDelegate": { "type": "integer" },
"status": { "type": "integer" },
"createTime": { "type": "string" },
"updateTime": { "type": "string" }
}
}
},
"timestamp": { "type": "number" },
"success": { "type": "boolean" }
}
}
```
## 脚本映射
- `../../scripts/sfe-user/zone.py`
FILE:openapi/sfe-zone/api-index.md
# API 索引 — sfe-zone
接口列表:
1. `POST /bia/open/biz-service/sfe-zone/project-task`
- 文档:`./project-task.md`
- 用途:查询指定区划的待办任务
2. `POST /bia/open/biz-service/sfe-zone/project-plan`
- 文档:`./project-plan.md`
- 用途:查询指定区划的计划编制数据
3. `POST /bia/open/biz-service/sfe-zone/project-actual`
- 文档:`./project-actual.md`
- 用途:查询指定区划的实际结果数据
4. `POST /bia/open/biz-service/sfe-zone/project-general`
- 文档:`./project-general.md`
- 用途:查询指定区划的采集填报数据
脚本映射:
- `../../scripts/sfe-zone/README.md`
FILE:openapi/sfe-zone/project-actual.md
# POST https://erp-web.mediportal.com.cn/erp-open-api/bia/open/biz-service/sfe-zone/project-actual
## 作用
按区划维度查询实际结果数据。需要先通过 zone 接口获取 `zoneId`,通过 project-summary 接口获取 `projectId`。
**Headers**
- `appKey` — API 密钥(必传)
- `Content-Type: application/json`
- `tenantId` — 租户ID(可选),默认无须传入,用户存在多个租户身份时须传入。如未传入具体tenantId,会返回用户可选择的租户列表。
**Body 参数**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `zoneId` | String | 是 | 区划 ID,来自 zone 接口返回的 `id` |
| `projectId` | String | 是 | 项目 ID,来自 project-summary 接口返回的 `id` |
| `periodStart` | String | 是 | 周期开始时间 |
| `periodEnd` | String | 是 | 周期结束时间 |
| `schemaCode` | String | 否 | 模板编码 |
| `schemaVersion` | String | 否 | 模板版本 |
| `page` | Integer | 否 | 页码,默认第 1 页 |
## 请求 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"required": ["zoneId", "projectId", "periodStart", "periodEnd"],
"properties": {
"zoneId": { "type": "string", "description": "区划 ID" },
"projectId": { "type": "string", "description": "项目 ID" },
"periodStart": { "type": "string", "description": "周期开始时间" },
"periodEnd": { "type": "string", "description": "周期结束时间" },
"schemaCode": { "type": "string", "description": "模板编码" },
"schemaVersion": { "type": "string", "description": "模板版本" },
"page": { "type": "integer", "minimum": 1 }
}
}
```
## 响应 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"resultCode": { "type": "integer" },
"resultMsg": { "type": "string" },
"data": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": { "type": "string" },
"zoneId": { "type": "string" },
"zoneName": { "type": "string" },
"pathIds": { "type": "string" },
"pathNames": { "type": "string" },
"projectId": { "type": "string" },
"projectCode": { "type": "string" },
"projectName": { "type": "string" },
"periodCode": { "type": "string" },
"periodName": { "type": "string" },
"periodStart": { "type": "string" },
"periodEnd": { "type": "string" },
"schemaCode": { "type": "string" },
"schemaName": { "type": "string" },
"schemaVersion": { "type": "string" },
"fieldValue": { "type": "object" },
"status": { "type": "integer" },
"createTime": { "type": "string" },
"updateTime": { "type": "string" }
}
}
},
"timestamp": { "type": "number" },
"success": { "type": "boolean" }
}
}
```
## 脚本映射
- `../../scripts/sfe-zone/project-actual.py`
FILE:openapi/sfe-zone/project-general.md
# POST https://erp-web.mediportal.com.cn/erp-open-api/bia/open/biz-service/sfe-zone/project-general
## 作用
按区划维度查询采集填报数据。需要先通过 zone 接口获取 `zoneId`,通过 project-summary 接口获取 `projectId`。
**Headers**
- `appKey` — API 密钥(必传)
- `Content-Type: application/json`
- `tenantId` — 租户ID(可选),默认无须传入,用户存在多个租户身份时须传入。如未传入具体tenantId,会返回用户可选择的租户列表。
**Body 参数**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `zoneId` | String | 是 | 区划 ID,来自 zone 接口返回的 `id` |
| `projectId` | String | 是 | 项目 ID,来自 project-summary 接口返回的 `id` |
| `periodStart` | String | 是 | 周期开始时间 |
| `periodEnd` | String | 是 | 周期结束时间 |
| `schemaCode` | String | 否 | 模板编码 |
| `schemaVersion` | String | 否 | 模板版本 |
| `page` | Integer | 否 | 页码,默认第 1 页 |
## 请求 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"required": ["zoneId", "projectId", "periodStart", "periodEnd"],
"properties": {
"zoneId": { "type": "string", "description": "区划 ID" },
"projectId": { "type": "string", "description": "项目 ID" },
"periodStart": { "type": "string", "description": "周期开始时间" },
"periodEnd": { "type": "string", "description": "周期结束时间" },
"schemaCode": { "type": "string", "description": "模板编码" },
"schemaVersion": { "type": "string", "description": "模板版本" },
"page": { "type": "integer", "minimum": 1 }
}
}
```
## 响应 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"resultCode": { "type": "integer" },
"resultMsg": { "type": "string" },
"data": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": { "type": "string" },
"zoneId": { "type": "string" },
"zoneName": { "type": "string" },
"pathIds": { "type": "string" },
"pathNames": { "type": "string" },
"projectId": { "type": "string" },
"projectCode": { "type": "string" },
"projectName": { "type": "string" },
"periodCode": { "type": "string" },
"periodName": { "type": "string" },
"periodStart": { "type": "string" },
"periodEnd": { "type": "string" },
"schemaCode": { "type": "string" },
"schemaName": { "type": "string" },
"schemaVersion": { "type": "string" },
"fieldValue": { "type": "object" },
"status": { "type": "integer" },
"createTime": { "type": "string" },
"updateTime": { "type": "string" }
}
}
},
"timestamp": { "type": "number" },
"success": { "type": "boolean" }
}
}
```
## 脚本映射
- `../../scripts/sfe-zone/project-general.py`
FILE:openapi/sfe-zone/project-plan.md
# POST https://erp-web.mediportal.com.cn/erp-open-api/bia/open/biz-service/sfe-zone/project-plan
## 作用
按区划维度查询计划编制数据。需要先通过 zone 接口获取 `zoneId`,通过 project-summary 接口获取 `projectId`。
**Headers**
- `appKey` — API 密钥(必传)
- `Content-Type: application/json`
- `tenantId` — 租户ID(可选),默认无须传入,用户存在多个租户身份时须传入。如未传入具体tenantId,会返回用户可选择的租户列表。
**Body 参数**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `zoneId` | String | 是 | 区划 ID,来自 zone 接口返回的 `id` |
| `projectId` | String | 是 | 项目 ID,来自 project-summary 接口返回的 `id` |
| `periodStart` | String | 是 | 周期开始时间 |
| `periodEnd` | String | 是 | 周期结束时间 |
| `schemaCode` | String | 否 | 模板编码 |
| `schemaVersion` | String | 否 | 模板版本 |
| `page` | Integer | 否 | 页码,默认第 1 页 |
## 请求 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"required": ["zoneId", "projectId", "periodStart", "periodEnd"],
"properties": {
"zoneId": { "type": "string", "description": "区划 ID" },
"projectId": { "type": "string", "description": "项目 ID" },
"periodStart": { "type": "string", "description": "周期开始时间" },
"periodEnd": { "type": "string", "description": "周期结束时间" },
"schemaCode": { "type": "string", "description": "模板编码" },
"schemaVersion": { "type": "string", "description": "模板版本" },
"page": { "type": "integer", "minimum": 1 }
}
}
```
## 响应 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"resultCode": { "type": "integer" },
"resultMsg": { "type": "string" },
"data": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": { "type": "string" },
"zoneId": { "type": "string" },
"zoneName": { "type": "string" },
"pathIds": { "type": "string" },
"pathNames": { "type": "string" },
"projectId": { "type": "string" },
"projectCode": { "type": "string" },
"projectName": { "type": "string" },
"periodCode": { "type": "string" },
"periodName": { "type": "string" },
"periodStart": { "type": "string" },
"periodEnd": { "type": "string" },
"schemaCode": { "type": "string" },
"schemaName": { "type": "string" },
"schemaVersion": { "type": "string" },
"fieldValue": { "type": "object" },
"status": { "type": "integer" },
"createTime": { "type": "string" },
"updateTime": { "type": "string" }
}
}
},
"timestamp": { "type": "number" },
"success": { "type": "boolean" }
}
}
```
## 脚本映射
- `../../scripts/sfe-zone/project-plan.py`
FILE:openapi/sfe-zone/project-task.md
# POST https://erp-web.mediportal.com.cn/erp-open-api/bia/open/biz-service/sfe-zone/project-task
## 作用
按区划维度查询任务状态。需要先通过 zone 接口获取 `zoneId`,通过 project-summary 接口获取 `projectId`。
**Headers**
- `appKey` — API 密钥(必传)
- `Content-Type: application/json`
- `tenantId` — 租户ID(可选),默认无须传入,用户存在多个租户身份时须传入。如未传入具体tenantId,会返回用户可选择的租户列表。
**Body 参数**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `zoneId` | String | 是 | 区划 ID,来自 zone 接口返回的 `id` |
| `projectId` | String | 是 | 项目 ID,来自 project-summary 接口返回的 `id` |
| `periodStart` | String | 是 | 周期开始时间 |
| `periodEnd` | String | 是 | 周期结束时间 |
| `status` | Integer | 否 | 状态:0-待处理,1-已完成,2-已取消,3-过期关闭,4-手工关闭 |
| `page` | Integer | 否 | 页码,默认第 1 页 |
## 请求 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"required": ["zoneId", "projectId", "periodStart", "periodEnd"],
"properties": {
"zoneId": { "type": "string", "description": "区划 ID" },
"projectId": { "type": "string", "description": "项目 ID" },
"periodStart": { "type": "string", "description": "周期开始时间" },
"periodEnd": { "type": "string", "description": "周期结束时间" },
"status": { "type": "integer", "enum": [0, 1, 2, 3, 4] },
"page": { "type": "integer", "minimum": 1 }
}
}
```
## 响应 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"resultCode": { "type": "integer" },
"resultMsg": { "type": "string" },
"data": {
"type": "array",
"items": {
"type": "object",
"properties": {
"projectId": { "type": "string" },
"tasks": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": { "type": "string" },
"name": { "type": "string" },
"startDate": { "type": "string" },
"endDate": { "type": "string" },
"zoneId": { "type": "string" },
"periodCode": { "type": "string" },
"schemaCode": { "type": "string" },
"status": { "type": "integer" },
"createTime": { "type": "string" },
"updateTime": { "type": "string" }
}
}
}
}
}
},
"timestamp": { "type": "number" },
"success": { "type": "boolean" }
}
}
```
## 脚本映射
- `../../scripts/sfe-zone/project-task.py`
FILE:scripts/common/toon_encoder.py
"""
TOON (Token-Oriented Object Notation) Encoder
A zero-dependency Python implementation fully compliant with TOON spec v3.0.
【核心用途解说】
此序列化引擎的本质,是专门建立一条将臃肿的 JSON(或多层嵌套的 Python dict/list)结构,转化为面向人工大语言模型(LLM)的高密度浓缩协议通道。
它的核心目标聚焦于【断崖式的削减大模型 Token 损耗】。依靠识别与动态压缩统一表头(类似 CSV 表格内联提取),结合 YAML 的层级树特质,该模块能在确保数据上下文明义 100% 不受损的前提下,精简掉所有的废弃闭合括号及引号符号,平均可帮你的 API 系统为单个语境输入节省极大量的 Token 从而实现极致降本提效。
"""
import re
import json
import math
from datetime import datetime, date
from typing import Any, Iterator, Tuple, List
# Validation patterns matching TS implementation
NUMERIC_LIKE_PATTERN = re.compile(r'^-?\d+(?:\.\d+)?(?:e[+-]?\d+)?$', re.IGNORECASE)
LEADING_ZERO_PATTERN = re.compile(r'^0\d+$')
VALID_UNQUOTED_KEY_PATTERN = re.compile(r'^[A-Za-z_][\w.]*$')
def _is_valid_unquoted_key(key: str) -> bool:
"""Checks if a key can be safely used without quotes."""
return bool(VALID_UNQUOTED_KEY_PATTERN.match(key))
def _is_safe_unquoted(value: str, delimiter: str) -> bool:
"""Determines if a string value can be safely encoded without quotes."""
if not value or value != value.strip():
return False
val_lower = value.lower()
if val_lower in ('true', 'false', 'null'):
return False
if NUMERIC_LIKE_PATTERN.match(value) or LEADING_ZERO_PATTERN.match(value):
return False
if ':' in value or '"' in value or '\\' in value:
return False
if any(ch in value for ch in ('[', ']', '{', '}')):
return False
if any(ch in value for ch in ('\n', '\r', '\t')):
return False
if delimiter in value:
return False
if value.startswith('-'):
return False
return True
def _escape_string(val: str) -> str:
"""
Safely escapes string matching JSON specification exactly (e.g. control characters).
Uses standard library json.dumps to stringify and slices off the bounding quotes.
"""
return json.dumps(val, ensure_ascii=False)[1:-1]
def normalize_value(value: Any, strip_html_style: bool = False) -> Any:
"""
Normalizes complex Python types into strict JSON equivalents (Primitives, Lists, Dicts, None).
Matches the TypeScript implementation's robust tracking for edge cases like NaNs and Sets.
"""
if value is None:
return None
if isinstance(value, str):
if strip_html_style:
# Safely remove style="..." or style='...' attributes from HTML tags to save tokens
value = re.sub(r'(?i)\s*style\s*=\s*(["\']).*?\1', '', value)
return value
if isinstance(value, bool):
return value
if isinstance(value, (int, float)):
if isinstance(value, float):
# NaN and Infinity map to null in standard JSON
if math.isnan(value) or math.isinf(value):
return None
return value
if isinstance(value, (datetime, date)):
return value.isoformat()
if isinstance(value, (list, tuple, set, frozenset)):
return [normalize_value(v, strip_html_style) for v in value]
if isinstance(value, dict):
return {str(k): normalize_value(v, strip_html_style) for k, v in value.items()}
# Graceful fallback for custom objects implementing a toJSON serialization target hook
if hasattr(value, 'toJSON') and callable(value.toJSON):
return normalize_value(value.toJSON(), strip_html_style)
if hasattr(value, 'to_json') and callable(value.to_json):
return normalize_value(value.to_json(), strip_html_style)
# Silent fallback for unreadable items like functions, mimicking JSON's drop behavior
return None
def _is_primitive(val: Any) -> bool:
return val is None or isinstance(val, (bool, int, float, str))
def _encode_primitive(val: Any, delimiter: str) -> str:
if val is None:
return 'null'
if isinstance(val, bool):
return 'true' if val else 'false'
if isinstance(val, (int, float)):
return str(val)
if isinstance(val, str):
if _is_safe_unquoted(val, delimiter):
return val
return f'"{_escape_string(val)}"'
return str(val)
def _encode_key(key: Any) -> str:
k_str = str(key)
if _is_valid_unquoted_key(k_str):
return k_str
return f'"{_escape_string(k_str)}"'
def _is_tabular_array(arr: List[Any]) -> Tuple[bool, List[str]]:
if not arr or not isinstance(arr[0], dict):
return False, []
first_keys = list(arr[0].keys())
if not first_keys:
return False, []
for item in arr:
if not isinstance(item, dict):
return False, []
if len(item) != len(first_keys):
return False, []
for k in first_keys:
if k not in item or not _is_primitive(item[k]):
return False, []
return True, first_keys
def _indent_line(depth: int, content: str, indent_size: int) -> str:
return (' ' * (depth * indent_size)) + content
def _format_header(length: int, key=None, fields=None, delimiter: str = ',') -> str:
header = ""
if key is not None:
header += _encode_key(key)
header += f"[{length}"
if delimiter != ',':
header += delimiter
header += "]"
if fields:
enc_fields = [_encode_key(f) for f in fields]
header += f"{{{delimiter.join(enc_fields)}}}"
header += ":"
return header
def _encode_dict(obj: dict, depth: int, indent_size: int, delimiter: str) -> Iterator[str]:
for k, v in obj.items():
yield from _encode_kv(k, v, depth, indent_size, delimiter)
def _encode_kv(key: Any, value: Any, depth: int, indent_size: int, delimiter: str) -> Iterator[str]:
enc_k = _encode_key(key)
if _is_primitive(value):
yield _indent_line(depth, f"{enc_k}: {_encode_primitive(value, delimiter)}", indent_size)
elif isinstance(value, list):
if len(value) == 0:
yield _indent_line(depth, _format_header(0, key=key, delimiter=delimiter), indent_size)
elif all(_is_primitive(v) for v in value):
joined = delimiter.join(_encode_primitive(v, delimiter) for v in value)
yield _indent_line(depth, f"{_format_header(len(value), key=key, delimiter=delimiter)} {joined}", indent_size)
else:
is_tabular, headers = _is_tabular_array(value)
if is_tabular:
yield _indent_line(depth, _format_header(len(value), key=key, fields=headers, delimiter=delimiter), indent_size)
for item in value:
joined = delimiter.join(_encode_primitive(item[h], delimiter) for h in headers)
yield _indent_line(depth + 1, joined, indent_size)
else:
yield _indent_line(depth, _format_header(len(value), key=key, delimiter=delimiter), indent_size)
for item in value:
yield from _encode_list_item(item, depth + 1, indent_size, delimiter)
elif isinstance(value, dict):
yield _indent_line(depth, f"{enc_k}:", indent_size)
if value:
yield from _encode_dict(value, depth + 1, indent_size, delimiter)
def _encode_list_item(value: Any, depth: int, indent_size: int, delimiter: str) -> Iterator[str]:
if _is_primitive(value):
yield _indent_line(depth, f"- {_encode_primitive(value, delimiter)}", indent_size)
elif isinstance(value, list):
if all(_is_primitive(v) for v in value):
joined = delimiter.join(_encode_primitive(v, delimiter) for v in value)
yield _indent_line(depth, f"- {_format_header(len(value), delimiter=delimiter)} {joined}", indent_size)
else:
yield _indent_line(depth, f"- {_format_header(len(value), delimiter=delimiter)}", indent_size)
for item in value:
yield from _encode_list_item(item, depth + 1, indent_size, delimiter)
elif isinstance(value, dict):
if not value:
yield _indent_line(depth, "- ", indent_size)
return
entries = list(value.items())
first_k, first_v = entries[0]
enc_first_k = _encode_key(first_k)
if isinstance(first_v, list) and len(first_v) > 0:
tabular, headers = _is_tabular_array(first_v)
if tabular:
yield _indent_line(depth, f"- {_format_header(len(first_v), key=first_k, fields=headers, delimiter=delimiter)}", indent_size)
for item in first_v:
joined = delimiter.join(_encode_primitive(item[h], delimiter) for h in headers)
yield _indent_line(depth + 2, joined, indent_size)
if len(entries) > 1:
rest_dict = dict(entries[1:])
yield from _encode_dict(rest_dict, depth + 1, indent_size, delimiter)
return
if _is_primitive(first_v):
yield _indent_line(depth, f"- {enc_first_k}: {_encode_primitive(first_v, delimiter)}", indent_size)
elif isinstance(first_v, list):
if len(first_v) == 0:
yield _indent_line(depth, f"- {enc_first_k}{_format_header(0, delimiter=delimiter)}", indent_size)
elif all(_is_primitive(v) for v in first_v):
joined = delimiter.join(_encode_primitive(v, delimiter) for v in first_v)
yield _indent_line(depth, f"- {enc_first_k}{_format_header(len(first_v), delimiter=delimiter)} {joined}", indent_size)
else:
yield _indent_line(depth, f"- {enc_first_k}{_format_header(len(first_v), delimiter=delimiter)}", indent_size)
for item in first_v:
yield from _encode_list_item(item, depth + 2, indent_size, delimiter)
elif isinstance(first_v, dict):
yield _indent_line(depth, f"- {enc_first_k}:", indent_size)
if first_v:
yield from _encode_dict(first_v, depth + 2, indent_size, delimiter)
if len(entries) > 1:
rest_dict = dict(entries[1:])
yield from _encode_dict(rest_dict, depth + 1, indent_size, delimiter)
def encode_lines(data: Any, indent: int = 2, delimiter: str = ',', strip_html_style: bool = False) -> Iterator[str]:
"""
Core generator returning lines instead of full string.
Suitable for streaming large inputs.
"""
# 1. Normalize strictly to JSON limits matching TS behavior
data = normalize_value(data, strip_html_style)
if _is_primitive(data):
enc_val = _encode_primitive(data, delimiter)
if enc_val:
yield enc_val
elif isinstance(data, list):
if not data:
yield _format_header(0, delimiter=delimiter)
elif all(_is_primitive(v) for v in data):
joined = delimiter.join(_encode_primitive(v, delimiter) for v in data)
yield f"{_format_header(len(data), delimiter=delimiter)} {joined}"
else:
is_tabular, headers = _is_tabular_array(data)
if is_tabular:
yield _format_header(len(data), fields=headers, delimiter=delimiter)
for item in data:
joined = delimiter.join(_encode_primitive(item[h], delimiter) for h in headers)
yield _indent_line(1, joined, indent)
else:
yield _format_header(len(data), delimiter=delimiter)
for item in data:
yield from _encode_list_item(item, 1, indent, delimiter)
elif isinstance(data, dict):
yield from _encode_dict(data, 0, indent, delimiter)
def encode(data: Any, indent: int = 2, delimiter: str = ',', strip_html_style: bool = False) -> str:
"""
Encodes a Python value (dict, list, primitive) into a TOON formatted string.
【主入口方法解说】
接收包含复杂嵌套结构的 Python 变量体系(如原生的 json 解析对象)。通过内部深度优先的生成器矩阵和结构对齐,
将其最终吐出为能够直接硬编码嵌入给 GPT/Claude/Gemini 等大模型 Prompt 阅读的 TOON 语境化压缩多行字符实体。
此方法正是所有后续省 Token 转换流的总开关。
【兼容性处理】
如果传入的数据已经是字符串且不是明显的 JSON 结构(Dict 或 List),则原样返回,确保接口在高频调用下的稳健性。
"""
if isinstance(data, str) and not (data.strip().startswith('{') or data.strip().startswith('[')):
return data
return '\n'.join(encode_lines(data, indent, delimiter, strip_html_style))
FILE:scripts/sfe-user/README.md
# 脚本清单 — sfe-user
## 共享依赖
- `../common/toon_encoder.py` — TOON 编码器,所有脚本的 API 响应必须经过此编码器转换后再输出
## 脚本列表
| 脚本 | 对应接口 | 用途 |
| --------------------- | ------------------------------------------------------ | ------------------------------ |
| `zone.py` | `POST /bia/open/biz-service/sfe-user/zone` | 查询用户授权的区划 |
| `product.py` | `POST /bia/open/biz-service/sfe-user/product` | 查询用户授权的产品 |
| `customer.py` | `POST /bia/open/biz-service/sfe-user/customer` | 查询用户授权的客户 |
| `customer-profile.py` | `POST /bia/open/biz-service/sfe-user/customer-profile` | 查询用户授权客户的画像 |
| `coverage.py` | `POST /bia/open/biz-service/sfe-user/coverage` | 查询用户授权的覆盖分管关系 |
| `project-summary.py` | `POST /bia/open/biz-service/sfe-user/project-summary` | 查询用户参与的数据采集项目摘要 |
| `project-period.py` | `POST /bia/open/biz-service/sfe-user/project-period` | 查询项目的周期列表 |
| `project-schema.py` | `POST /bia/open/biz-service/sfe-user/project-schema` | 查询项目的填报模板 |
| `project-role.py` | `POST /bia/open/biz-service/sfe-user/project-role` | 查询项目的角色权限 |
| `project-task.py` | `POST /bia/open/biz-service/sfe-user/project-task` | 查询用户的待办任务状态 |
| `project-plan.py` | `POST /bia/open/biz-service/sfe-user/project-plan` | 查询用户的计划编制数据 |
| `project-actual.py` | `POST /bia/open/biz-service/sfe-user/project-actual` | 查询用户的实际结果数据 |
| `project-general.py` | `POST /bia/open/biz-service/sfe-user/project-general` | 查询用户的采集填报数据 |
## 使用方式
```bash
# 设置环境变量
export XG_BIZ_API_KEY="your-appkey"
# 执行脚本(无必填参数)
python3 scripts/sfe-user/zone.py
# 如果用户存在多个租户身份,需传入 tenantId
python3 scripts/sfe-user/zone.py --tenantId <租户ID>
# zone.py 支持可选 status 参数
python3 scripts/sfe-user/zone.py --status <状态>
```
## 可选参数
| 参数 | 说明 |
| ------------ | -------------------------------------------------------------------------------------------------------- |
| `--tenantId` | 租户 ID。默认无须传入,用户存在多个租户身份时须传入。如未传入具体 tenantId,会返回用户可选择的租户列表。 |
| `--status` | 状态筛选(仅 zone.py 支持) |
## 输出说明
所有脚本的输出均为 **TOON 格式**(非原始 JSON),这是一种面向 LLM 的高密度压缩格式,可大幅节省 Token 消耗。
## 规范
1. **必须使用 Python** 编写
2. **必须经过 toon_encoder** 处理后再输出
3. **鉴权遵循** `cms-auth-skills/SKILL.md` 规范
4. **入参定义以** `openapi/` 文档为准
FILE:scripts/sfe-user/coverage.py
#!/usr/bin/env python3
"""
sfe-user / coverage 脚本
用途:查询用户授权的覆盖分管关系并输出 TOON 编码结果
使用方式:
python3 scripts/sfe-user/coverage.py
环境变量:
XG_BIZ_API_KEY / XG_APP_KEY — appKey(必须)
"""
import sys
import os
import json
import requests
import warnings
import argparse
# 禁用 InsecureRequestWarning (因为 verify=False)
warnings.filterwarnings("ignore", category=requests.packages.urllib3.exceptions.InsecureRequestWarning)
# 引入共享的 TOON 编码器
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "common"))
from toon_encoder import encode as toon_encode
# 接口完整 URL
API_URL = "https://erp-web.mediportal.com.cn/erp-open-api/bia/open/biz-service/sfe-user/coverage"
def call_api(app_key: str, body: dict, tenant_id: str = None) -> dict:
"""调用接口,返回原始 JSON 响应"""
headers = {
"appKey": app_key,
"Content-Type": "application/json",
}
if tenant_id:
headers["tenantId"] = tenant_id
try:
response = requests.post(
API_URL,
json=body,
headers=headers,
verify=False,
allow_redirects=True,
timeout=60,
)
response.raise_for_status()
return response.json()
except Exception as e:
print(f"错误: 请求失败: {e}", file=sys.stderr)
sys.exit(1)
def main():
parser = argparse.ArgumentParser(description="查询用户授权的覆盖分管关系")
parser.add_argument(
"--tenantId", help="租户ID(可选,用户存在多个租户身份时须传入)"
)
args = parser.parse_args()
app_key = os.environ.get("XG_BIZ_API_KEY") or os.environ.get("XG_APP_KEY")
if not app_key:
print("错误: 请设置环境变量 XG_BIZ_API_KEY 或 XG_APP_KEY", file=sys.stderr)
sys.exit(1)
body = {}
result = call_api(app_key, body, args.tenantId)
print(toon_encode(result))
if __name__ == "__main__":
main()
FILE:scripts/sfe-user/customer-profile.py
#!/usr/bin/env python3
"""
sfe-user / customer-profile 脚本
用途:查询用户授权客户的画像信息并输出 TOON 编码结果
使用方式:
python3 scripts/sfe-user/customer-profile.py [--tenantId <租户ID>]
环境变量:
XG_BIZ_API_KEY / XG_APP_KEY — appKey(必须)
可选参数:
--tenantId 租户ID,用户存在多个租户身份时须传入
"""
import sys
import os
import json
import requests
import warnings
import argparse
# 禁用 InsecureRequestWarning (因为 verify=False)
warnings.filterwarnings("ignore", category=requests.packages.urllib3.exceptions.InsecureRequestWarning)
# 引入共享的 TOON 编码器
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "common"))
from toon_encoder import encode as toon_encode
# 接口完整 URL
API_URL = "https://erp-web.mediportal.com.cn/erp-open-api/bia/open/biz-service/sfe-user/customer-profile"
def call_api(app_key: str, body: dict, tenant_id: str = None) -> dict:
"""调用接口,返回原始 JSON 响应"""
headers = {
"appKey": app_key,
"Content-Type": "application/json",
}
if tenant_id:
headers["tenantId"] = tenant_id
try:
response = requests.post(
API_URL,
json=body,
headers=headers,
verify=False,
allow_redirects=True,
timeout=60,
)
response.raise_for_status()
return response.json()
except Exception as e:
print(f"错误: 请求失败: {e}", file=sys.stderr)
sys.exit(1)
def main():
parser = argparse.ArgumentParser(description="查询用户授权客户的画像信息")
parser.add_argument("--tenantId", help="租户ID(用户存在多个租户身份时须传入)")
args = parser.parse_args()
app_key = os.environ.get("XG_BIZ_API_KEY") or os.environ.get("XG_APP_KEY")
if not app_key:
print("错误: 请设置环境变量 XG_BIZ_API_KEY 或 XG_APP_KEY", file=sys.stderr)
sys.exit(1)
body = {}
result = call_api(app_key, body, args.tenantId)
toon_output = toon_encode(result)
print(toon_output)
if __name__ == "__main__":
main()
FILE:scripts/sfe-user/customer.py
#!/usr/bin/env python3
"""
sfe-user / customer 脚本
用途:查询用户授权的客户列表并输出 TOON 编码结果
使用方式:
python3 scripts/sfe-user/customer.py [--tenantId <租户ID>]
环境变量:
XG_BIZ_API_KEY / XG_APP_KEY — appKey(必须)
可选参数:
--tenantId 租户ID,用户存在多个租户身份时须传入
"""
import sys
import os
import json
import requests
import warnings
import argparse
# 禁用 InsecureRequestWarning (因为 verify=False)
warnings.filterwarnings("ignore", category=requests.packages.urllib3.exceptions.InsecureRequestWarning)
# 引入共享的 TOON 编码器
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "common"))
from toon_encoder import encode as toon_encode
# 接口完整 URL
API_URL = "https://erp-web.mediportal.com.cn/erp-open-api/bia/open/biz-service/sfe-user/customer"
def call_api(app_key: str, body: dict, tenant_id: str = None) -> dict:
"""调用接口,返回原始 JSON 响应"""
headers = {
"appKey": app_key,
"Content-Type": "application/json",
}
if tenant_id:
headers["tenantId"] = tenant_id
try:
response = requests.post(
API_URL,
json=body,
headers=headers,
verify=False,
allow_redirects=True,
timeout=60,
)
response.raise_for_status()
return response.json()
except Exception as e:
print(f"错误: 请求失败: {e}", file=sys.stderr)
sys.exit(1)
def main():
parser = argparse.ArgumentParser(description="查询用户授权的客户列表")
parser.add_argument("--tenantId", help="租户ID(用户存在多个租户身份时须传入)")
args = parser.parse_args()
app_key = os.environ.get("XG_BIZ_API_KEY") or os.environ.get("XG_APP_KEY")
if not app_key:
print("错误: 请设置环境变量 XG_BIZ_API_KEY 或 XG_APP_KEY", file=sys.stderr)
sys.exit(1)
body = {}
result = call_api(app_key, body, args.tenantId)
toon_output = toon_encode(result)
print(toon_output)
if __name__ == "__main__":
main()
FILE:scripts/sfe-user/product.py
#!/usr/bin/env python3
"""
sfe-user / product 脚本
用途:查询用户授权的产品列表并输出 TOON 编码结果
使用方式:
python3 scripts/sfe-user/product.py [--tenantId <租户ID>]
环境变量:
XG_BIZ_API_KEY / XG_APP_KEY — appKey(必须)
可选参数:
--tenantId 租户ID,用户存在多个租户身份时须传入
"""
import sys
import os
import json
import requests
import warnings
import argparse
# 禁用 InsecureRequestWarning (因为 verify=False)
warnings.filterwarnings("ignore", category=requests.packages.urllib3.exceptions.InsecureRequestWarning)
# 引入共享的 TOON 编码器
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "common"))
from toon_encoder import encode as toon_encode
# 接口完整 URL
API_URL = "https://erp-web.mediportal.com.cn/erp-open-api/bia/open/biz-service/sfe-user/product"
def call_api(app_key: str, body: dict, tenant_id: str = None) -> dict:
"""调用接口,返回原始 JSON 响应"""
headers = {
"appKey": app_key,
"Content-Type": "application/json",
}
if tenant_id:
headers["tenantId"] = tenant_id
try:
response = requests.post(
API_URL,
json=body,
headers=headers,
verify=False,
allow_redirects=True,
timeout=60,
)
response.raise_for_status()
return response.json()
except Exception as e:
print(f"错误: 请求失败: {e}", file=sys.stderr)
sys.exit(1)
def main():
parser = argparse.ArgumentParser(description="查询用户授权的产品列表")
parser.add_argument("--tenantId", help="租户ID(用户存在多个租户身份时须传入)")
args = parser.parse_args()
app_key = os.environ.get("XG_BIZ_API_KEY") or os.environ.get("XG_APP_KEY")
if not app_key:
print("错误: 请设置环境变量 XG_BIZ_API_KEY 或 XG_APP_KEY", file=sys.stderr)
sys.exit(1)
body = {}
result = call_api(app_key, body, args.tenantId)
toon_output = toon_encode(result)
print(toon_output)
if __name__ == "__main__":
main()
FILE:scripts/sfe-user/project-actual.py
#!/usr/bin/env python3
"""
sfe-user / project-actual 脚本
用途:查询用户的实际结果数据并输出 TOON 编码结果
使用方式:
python3 scripts/sfe-user/project-actual.py [--tenantId <租户ID>]
环境变量:
XG_BIZ_API_KEY / XG_APP_KEY — appKey(必须)
可选参数:
--tenantId 租户ID,用户存在多个租户身份时须传入
"""
import sys
import os
import json
import requests
import warnings
import argparse
# 禁用 InsecureRequestWarning (因为 verify=False)
warnings.filterwarnings("ignore", category=requests.packages.urllib3.exceptions.InsecureRequestWarning)
# 引入共享的 TOON 编码器
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "common"))
from toon_encoder import encode as toon_encode
# 接口完整 URL
API_URL = "https://erp-web.mediportal.com.cn/erp-open-api/bia/open/biz-service/sfe-user/project-actual"
def call_api(app_key: str, body: dict, tenant_id: str = None) -> dict:
"""调用接口,返回原始 JSON 响应"""
headers = {
"appKey": app_key,
"Content-Type": "application/json",
}
if tenant_id:
headers["tenantId"] = tenant_id
try:
response = requests.post(
API_URL,
json=body,
headers=headers,
verify=False,
allow_redirects=True,
timeout=60,
)
response.raise_for_status()
return response.json()
except Exception as e:
print(f"错误: 请求失败: {e}", file=sys.stderr)
sys.exit(1)
def main():
parser = argparse.ArgumentParser(description="查询用户的实际结果数据")
parser.add_argument("--tenantId", help="租户ID(用户存在多个租户身份时须传入)")
args = parser.parse_args()
app_key = os.environ.get("XG_BIZ_API_KEY") or os.environ.get("XG_APP_KEY")
if not app_key:
print("错误: 请设置环境变量 XG_BIZ_API_KEY 或 XG_APP_KEY", file=sys.stderr)
sys.exit(1)
body = {}
result = call_api(app_key, body, args.tenantId)
toon_output = toon_encode(result)
print(toon_output)
if __name__ == "__main__":
main()
FILE:scripts/sfe-user/project-general.py
#!/usr/bin/env python3
"""
sfe-user / project-general 脚本
用途:查询用户的采集填报数据并输出 TOON 编码结果
使用方式:
python3 scripts/sfe-user/project-general.py [--tenantId <租户ID>]
环境变量:
XG_BIZ_API_KEY / XG_APP_KEY — appKey(必须)
可选参数:
--tenantId 租户ID,用户存在多个租户身份时须传入
"""
import sys
import os
import json
import requests
import warnings
import argparse
# 禁用 InsecureRequestWarning (因为 verify=False)
warnings.filterwarnings("ignore", category=requests.packages.urllib3.exceptions.InsecureRequestWarning)
# 引入共享的 TOON 编码器
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "common"))
from toon_encoder import encode as toon_encode
# 接口完整 URL
API_URL = "https://erp-web.mediportal.com.cn/erp-open-api/bia/open/biz-service/sfe-user/project-general"
def call_api(app_key: str, body: dict, tenant_id: str = None) -> dict:
"""调用接口,返回原始 JSON 响应"""
headers = {
"appKey": app_key,
"Content-Type": "application/json",
}
if tenant_id:
headers["tenantId"] = tenant_id
try:
response = requests.post(
API_URL,
json=body,
headers=headers,
verify=False,
allow_redirects=True,
timeout=60,
)
response.raise_for_status()
return response.json()
except Exception as e:
print(f"错误: 请求失败: {e}", file=sys.stderr)
sys.exit(1)
def main():
parser = argparse.ArgumentParser(description="查询用户的采集填报数据")
parser.add_argument("--tenantId", help="租户ID(用户存在多个租户身份时须传入)")
args = parser.parse_args()
app_key = os.environ.get("XG_BIZ_API_KEY") or os.environ.get("XG_APP_KEY")
if not app_key:
print("错误: 请设置环境变量 XG_BIZ_API_KEY 或 XG_APP_KEY", file=sys.stderr)
sys.exit(1)
body = {}
result = call_api(app_key, body, args.tenantId)
# toon_output = toon_encode(result)
# print(toon_output)
print(json.dumps(result, ensure_ascii=False))
if __name__ == "__main__":
main()
FILE:scripts/sfe-user/project-period.py
#!/usr/bin/env python3
"""
sfe-user / project-period 脚本
用途:查询项目的周期列表并输出 TOON 编码结果
使用方式:
python3 scripts/sfe-user/project-period.py
环境变量:
XG_BIZ_API_KEY / XG_APP_KEY — appKey(必须)
"""
import sys
import os
import json
import requests
import warnings
import argparse
# 禁用 InsecureRequestWarning (因为 verify=False)
warnings.filterwarnings("ignore", category=requests.packages.urllib3.exceptions.InsecureRequestWarning)
# 引入共享的 TOON 编码器
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "common"))
from toon_encoder import encode as toon_encode
# 接口完整 URL
API_URL = "https://erp-web.mediportal.com.cn/erp-open-api/bia/open/biz-service/sfe-user/project-period"
def call_api(app_key: str, body: dict, tenant_id: str = None) -> dict:
"""调用接口,返回原始 JSON 响应"""
headers = {
"appKey": app_key,
"Content-Type": "application/json",
}
if tenant_id:
headers["tenantId"] = tenant_id
try:
response = requests.post(
API_URL,
json=body,
headers=headers,
verify=False,
allow_redirects=True,
timeout=60,
)
response.raise_for_status()
return response.json()
except Exception as e:
print(f"错误: 请求失败: {e}", file=sys.stderr)
sys.exit(1)
def main():
parser = argparse.ArgumentParser(description="查询项目的周期列表")
parser.add_argument(
"--tenantId", help="租户ID(可选,用户存在多个租户身份时须传入)"
)
args = parser.parse_args()
app_key = os.environ.get("XG_BIZ_API_KEY") or os.environ.get("XG_APP_KEY")
if not app_key:
print("错误: 请设置环境变量 XG_BIZ_API_KEY 或 XG_APP_KEY", file=sys.stderr)
sys.exit(1)
body = {}
result = call_api(app_key, body, args.tenantId)
print(toon_encode(result))
if __name__ == "__main__":
main()
FILE:scripts/sfe-user/project-plan.py
#!/usr/bin/env python3
"""
sfe-user / project-plan 脚本
用途:查询用户的计划编制数据并输出 TOON 编码结果
使用方式:
python3 scripts/sfe-user/project-plan.py [--tenantId <租户ID>]
环境变量:
XG_BIZ_API_KEY / XG_APP_KEY — appKey(必须)
可选参数:
--tenantId 租户ID,用户存在多个租户身份时须传入
"""
import sys
import os
import json
import requests
import warnings
import argparse
# 禁用 InsecureRequestWarning (因为 verify=False)
warnings.filterwarnings("ignore", category=requests.packages.urllib3.exceptions.InsecureRequestWarning)
# 引入共享的 TOON 编码器
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "common"))
from toon_encoder import encode as toon_encode
# 接口完整 URL
API_URL = "https://erp-web.mediportal.com.cn/erp-open-api/bia/open/biz-service/sfe-user/project-plan"
def call_api(app_key: str, body: dict, tenant_id: str = None) -> dict:
"""调用接口,返回原始 JSON 响应"""
headers = {
"appKey": app_key,
"Content-Type": "application/json",
}
if tenant_id:
headers["tenantId"] = tenant_id
try:
response = requests.post(
API_URL,
json=body,
headers=headers,
verify=False,
allow_redirects=True,
timeout=60,
)
response.raise_for_status()
return response.json()
except Exception as e:
print(f"错误: 请求失败: {e}", file=sys.stderr)
sys.exit(1)
def main():
parser = argparse.ArgumentParser(description="查询用户的计划编制数据")
parser.add_argument("--tenantId", help="租户ID(用户存在多个租户身份时须传入)")
args = parser.parse_args()
app_key = os.environ.get("XG_BIZ_API_KEY") or os.environ.get("XG_APP_KEY")
if not app_key:
print("错误: 请设置环境变量 XG_BIZ_API_KEY 或 XG_APP_KEY", file=sys.stderr)
sys.exit(1)
body = {}
result = call_api(app_key, body, args.tenantId)
toon_output = toon_encode(result)
print(toon_output)
if __name__ == "__main__":
main()
FILE:scripts/sfe-user/project-role.py
#!/usr/bin/env python3
"""
sfe-user / project-role 脚本
用途:查询项目的角色权限并输出 TOON 编码结果
使用方式:
python3 scripts/sfe-user/project-role.py [--tenantId <租户ID>]
环境变量:
XG_BIZ_API_KEY / XG_APP_KEY — appKey(必须)
可选参数:
--tenantId 租户ID,用户存在多个租户身份时须传入
"""
import sys
import os
import json
import requests
import warnings
import argparse
# 禁用 InsecureRequestWarning (因为 verify=False)
warnings.filterwarnings("ignore", category=requests.packages.urllib3.exceptions.InsecureRequestWarning)
# 引入共享的 TOON 编码器
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "common"))
from toon_encoder import encode as toon_encode
# 接口完整 URL
API_URL = "https://erp-web.mediportal.com.cn/erp-open-api/bia/open/biz-service/sfe-user/project-role"
def call_api(app_key: str, body: dict, tenant_id: str = None) -> dict:
"""调用接口,返回原始 JSON 响应"""
headers = {
"appKey": app_key,
"Content-Type": "application/json",
}
if tenant_id:
headers["tenantId"] = tenant_id
try:
response = requests.post(
API_URL,
json=body,
headers=headers,
verify=False,
allow_redirects=True,
timeout=60,
)
response.raise_for_status()
return response.json()
except Exception as e:
print(f"错误: 请求失败: {e}", file=sys.stderr)
sys.exit(1)
def main():
parser = argparse.ArgumentParser(description="查询项目的角色权限")
parser.add_argument("--tenantId", help="租户ID(用户存在多个租户身份时须传入)")
args = parser.parse_args()
app_key = os.environ.get("XG_BIZ_API_KEY") or os.environ.get("XG_APP_KEY")
if not app_key:
print("错误: 请设置环境变量 XG_BIZ_API_KEY 或 XG_APP_KEY", file=sys.stderr)
sys.exit(1)
body = {}
result = call_api(app_key, body, args.tenantId)
toon_output = toon_encode(result)
print(toon_output)
if __name__ == "__main__":
main()
FILE:scripts/sfe-user/project-schema.py
#!/usr/bin/env python3
"""
sfe-user / project-schema 脚本
用途:查询项目的填报模板定义并输出 TOON 编码结果
使用方式:
python3 scripts/sfe-user/project-schema.py
环境变量:
XG_BIZ_API_KEY / XG_APP_KEY — appKey(必须)
"""
import sys
import os
import json
import requests
import warnings
import argparse
# 禁用 InsecureRequestWarning (因为 verify=False)
warnings.filterwarnings("ignore", category=requests.packages.urllib3.exceptions.InsecureRequestWarning)
# 引入共享的 TOON 编码器
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "common"))
from toon_encoder import encode as toon_encode
# 接口完整 URL
API_URL = "https://erp-web.mediportal.com.cn/erp-open-api/bia/open/biz-service/sfe-user/project-schema"
def call_api(app_key: str, body: dict, tenant_id: str = None) -> dict:
"""调用接口,返回原始 JSON 响应"""
headers = {
"appKey": app_key,
"Content-Type": "application/json",
}
if tenant_id:
headers["tenantId"] = tenant_id
try:
response = requests.post(
API_URL,
json=body,
headers=headers,
verify=False,
allow_redirects=True,
timeout=60,
)
response.raise_for_status()
return response.json()
except Exception as e:
print(f"错误: 请求失败: {e}", file=sys.stderr)
sys.exit(1)
def main():
parser = argparse.ArgumentParser(description="查询项目的填报模板定义")
parser.add_argument(
"--tenantId", help="租户ID(可选,用户存在多个租户身份时须传入)"
)
args = parser.parse_args()
app_key = os.environ.get("XG_BIZ_API_KEY") or os.environ.get("XG_APP_KEY")
if not app_key:
print("错误: 请设置环境变量 XG_BIZ_API_KEY 或 XG_APP_KEY", file=sys.stderr)
sys.exit(1)
body = {}
result = call_api(app_key, body, args.tenantId)
print(toon_encode(result))
if __name__ == "__main__":
main()
FILE:scripts/sfe-user/project-summary.py
#!/usr/bin/env python3
"""
sfe-user / project-summary 脚本
用途:查询用户参与的数据采集项目摘要并输出 TOON 编码结果
使用方式:
python3 scripts/sfe-user/project-summary.py [--tenantId <租户ID>]
环境变量:
XG_BIZ_API_KEY / XG_APP_KEY — appKey(必须)
可选参数:
--tenantId 租户ID,用户存在多个租户身份时须传入
"""
import sys
import os
import json
import requests
import warnings
import argparse
# 禁用 InsecureRequestWarning (因为 verify=False)
warnings.filterwarnings("ignore", category=requests.packages.urllib3.exceptions.InsecureRequestWarning)
# 引入共享的 TOON 编码器
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "common"))
from toon_encoder import encode as toon_encode
# 接口完整 URL
API_URL = "https://erp-web.mediportal.com.cn/erp-open-api/bia/open/biz-service/sfe-user/project-summary"
def call_api(app_key: str, body: dict, tenant_id: str = None) -> dict:
"""调用接口,返回原始 JSON 响应"""
headers = {
"appKey": app_key,
"Content-Type": "application/json",
}
if tenant_id:
headers["tenantId"] = tenant_id
try:
response = requests.post(
API_URL,
json=body,
headers=headers,
verify=False,
allow_redirects=True,
timeout=60,
)
response.raise_for_status()
return response.json()
except Exception as e:
print(f"错误: 请求失败: {e}", file=sys.stderr)
sys.exit(1)
def main():
parser = argparse.ArgumentParser(description="查询用户参与的数据采集项目摘要")
parser.add_argument("--tenantId", help="租户ID(用户存在多个租户身份时须传入)")
args = parser.parse_args()
app_key = os.environ.get("XG_BIZ_API_KEY") or os.environ.get("XG_APP_KEY")
if not app_key:
print("错误: 请设置环境变量 XG_BIZ_API_KEY 或 XG_APP_KEY", file=sys.stderr)
sys.exit(1)
body = {}
result = call_api(app_key, body, args.tenantId)
toon_output = toon_encode(result)
print(toon_output)
if __name__ == "__main__":
main()
FILE:scripts/sfe-user/project-task.py
#!/usr/bin/env python3
"""
sfe-user / project-task 脚本
用途:查询用户的待办任务状态并输出 TOON 编码结果
使用方式:
python3 scripts/sfe-user/project-task.py [--tenantId <租户ID>]
环境变量:
XG_BIZ_API_KEY / XG_APP_KEY — appKey(必须)
可选参数:
--tenantId 租户ID,用户存在多个租户身份时须传入
"""
import sys
import os
import json
import requests
import warnings
import argparse
# 禁用 InsecureRequestWarning (因为 verify=False)
warnings.filterwarnings("ignore", category=requests.packages.urllib3.exceptions.InsecureRequestWarning)
# 引入共享的 TOON 编码器
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "common"))
from toon_encoder import encode as toon_encode
# 接口完整 URL
API_URL = "https://erp-web.mediportal.com.cn/erp-open-api/bia/open/biz-service/sfe-user/project-task"
def call_api(app_key: str, body: dict, tenant_id: str = None) -> dict:
"""调用接口,返回原始 JSON 响应"""
headers = {
"appKey": app_key,
"Content-Type": "application/json",
}
if tenant_id:
headers["tenantId"] = tenant_id
try:
response = requests.post(
API_URL,
json=body,
headers=headers,
verify=False,
allow_redirects=True,
timeout=60,
)
response.raise_for_status()
return response.json()
except Exception as e:
print(f"错误: 请求失败: {e}", file=sys.stderr)
sys.exit(1)
def main():
parser = argparse.ArgumentParser(description="查询用户的待办任务状态")
parser.add_argument("--tenantId", help="租户ID(用户存在多个租户身份时须传入)")
args = parser.parse_args()
app_key = os.environ.get("XG_BIZ_API_KEY") or os.environ.get("XG_APP_KEY")
if not app_key:
print("错误: 请设置环境变量 XG_BIZ_API_KEY 或 XG_APP_KEY", file=sys.stderr)
sys.exit(1)
body = {}
result = call_api(app_key, body, args.tenantId)
toon_output = toon_encode(result)
print(toon_output)
if __name__ == "__main__":
main()
FILE:scripts/sfe-user/zone.py
#!/usr/bin/env python3
"""
sfe-user / zone 脚本
用途:查询用户授权的区划列表并输出 TOON 编码结果
使用方式:
python3 scripts/sfe-user/zone.py [--status <状态>] [--tenantId <租户ID>]
环境变量:
XG_BIZ_API_KEY / XG_APP_KEY — appKey(必须)
可选参数:
--status 状态:1-启用,0-禁用(默认查询启用的区划)
--tenantId 租户ID,用户存在多个租户身份时须传入
"""
import sys
import os
import json
import requests
import warnings
import argparse
# 禁用 InsecureRequestWarning (因为 verify=False)
warnings.filterwarnings("ignore", category=requests.packages.urllib3.exceptions.InsecureRequestWarning)
# 引入共享的 TOON 编码器
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "common"))
from toon_encoder import encode as toon_encode
# 接口完整 URL
API_URL = "https://erp-web.mediportal.com.cn/erp-open-api/bia/open/biz-service/sfe-user/zone"
def call_api(app_key: str, body: dict, tenant_id: str = None) -> dict:
"""调用接口,返回原始 JSON 响应"""
headers = {
"appKey": app_key,
"Content-Type": "application/json",
}
if tenant_id:
headers["tenantId"] = tenant_id
try:
response = requests.post(
API_URL,
json=body,
headers=headers,
verify=False,
allow_redirects=True,
timeout=60,
)
response.raise_for_status()
return response.json()
except Exception as e:
print(f"错误: 请求失败: {e}", file=sys.stderr)
sys.exit(1)
def main():
parser = argparse.ArgumentParser(description="查询用户授权的区划列表")
parser.add_argument("--status", type=int, default=1, help="状态:1-启用,0-禁用")
parser.add_argument("--tenantId", help="租户ID(用户存在多个租户身份时须传入)")
args = parser.parse_args()
app_key = os.environ.get("XG_BIZ_API_KEY") or os.environ.get("XG_APP_KEY")
if not app_key:
print("错误: 请设置环境变量 XG_BIZ_API_KEY 或 XG_APP_KEY", file=sys.stderr)
sys.exit(1)
body = {"status": args.status}
result = call_api(app_key, body, args.tenantId)
toon_output = toon_encode(result)
print(toon_output)
if __name__ == "__main__":
main()
FILE:scripts/sfe-zone/README.md
# 脚本清单 — sfe-zone
## 共享依赖
- `../common/toon_encoder.py` — TOON 编码器,所有脚本的 API 响应必须经过此编码器转换后再输出
## 脚本列表
| 脚本 | 对应接口 | 用途 |
| -------------------- | ----------------------------------------------------- | -------------------------- |
| `project-task.py` | `POST /bia/open/biz-service/sfe-zone/project-task` | 查询指定区划的待办任务 |
| `project-plan.py` | `POST /bia/open/biz-service/sfe-zone/project-plan` | 查询指定区划的计划编制数据 |
| `project-actual.py` | `POST /bia/open/biz-service/sfe-zone/project-actual` | 查询指定区划的实际结果数据 |
| `project-general.py` | `POST /bia/open/biz-service/sfe-zone/project-general` | 查询指定区划的采集填报数据 |
## 使用方式
```bash
# 设置环境变量
export XG_BIZ_API_KEY="your-appkey"
# 执行脚本(通过命令行参数传入必填参数)
python3 scripts/sfe-zone/project-task.py --zoneId <区划ID> --projectId <项目ID> --periodStart <开始日期> --periodEnd <结束日期>
# 如果用户存在多个租户身份,需传入 tenantId
python3 scripts/sfe-zone/project-task.py --tenantId <租户ID> --zoneId <区划ID> --projectId <项目ID> --periodStart <开始日期> --periodEnd <结束日期>
```
## 可选参数
| 参数 | 说明 |
| ------------ | -------------------------------------------------------------------------------------------------------- |
| `--tenantId` | 租户 ID。默认无须传入,用户存在多个租户身份时须传入。如未传入具体 tenantId,会返回用户可选择的租户列表。 |
| `--page` | 页码,默认为 1 |
| `--status` | 任务状态(仅 project-task.py 支持) |
## 输出说明
所有脚本的输出均为 **TOON 格式**(非原始 JSON),这是一种面向 LLM 的高密度压缩格式,可大幅节省 Token 消耗。
## 注意事项
sfe-zone 模块的接口需要必填参数:
- `zoneId` — 区划 ID
- `projectId` — 项目 ID
- `periodStart` — 周期开始时间
- `periodEnd` — 周期结束时间
执行前请根据实际业务需求修改脚本中的 body 参数。
## 规范
1. **必须使用 Python** 编写
2. **必须经过 toon_encoder** 处理后再输出
3. **鉴权遵循** `cms-auth-skills/SKILL.md` 规范
4. **入参定义以** `openapi/` 文档为准
FILE:scripts/sfe-zone/project-actual.py
#!/usr/bin/env python3
"""
sfe-zone / project-actual 脚本
用途:查询指定区划的实际结果数据并输出 TOON 编码结果
使用方式:
python3 scripts/sfe-zone/project-actual.py --zoneId <区划ID> --projectId <项目ID> --periodStart <开始日期> --periodEnd <结束日期>
环境变量:
XG_BIZ_API_KEY / XG_APP_KEY — appKey(必须)
可选参数:
--tenantId 租户ID,用户存在多个租户身份时须传入
"""
import sys
import os
import json
import argparse
import requests
import warnings
# 禁用 InsecureRequestWarning (因为 verify=False)
warnings.filterwarnings("ignore", category=requests.packages.urllib3.exceptions.InsecureRequestWarning)
# 引入共享的 TOON 编码器
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "common"))
from toon_encoder import encode as toon_encode
# 接口完整 URL
API_URL = "https://sg-cwork-api.mediportal.com.cn/sfe-statistic/open-api/statistic/v1/user/zone/projects/actual"
def call_api(app_key: str, body: dict, tenant_id: str = None) -> dict:
"""查询指定大区的项目实际值列表"""
headers = {
"X-APP-KEY": app_key,
"Content-Type": "application/json",
}
if tenant_id:
headers["X-TENANT-ID"] = tenant_id
try:
response = requests.post(
API_URL,
json=body,
headers=headers,
verify=False,
allow_redirects=True,
timeout=60,
)
response.raise_for_status()
return response.json()
except Exception as e:
print(f"错误: 请求失败: {e}", file=sys.stderr)
sys.exit(1)
def main():
parser = argparse.ArgumentParser(description="查询指定区划的实际结果数据")
parser.add_argument("--zoneId", required=True, help="区划ID")
parser.add_argument("--projectId", required=True, help="项目ID")
parser.add_argument("--periodStart", required=True, help="期间开始日期")
parser.add_argument("--periodEnd", required=True, help="期间结束日期")
parser.add_argument("--tenantId", help="租户ID(用户存在多个租户身份时须传入)")
args = parser.parse_args()
app_key = os.environ.get("XG_BIZ_API_KEY") or os.environ.get("XG_APP_KEY")
if not app_key:
print("错误: 请设置环境变量 XG_BIZ_API_KEY 或 XG_APP_KEY", file=sys.stderr)
sys.exit(1)
body = {
"zoneId": args.zoneId,
"projectId": args.projectId,
"periodStart": args.periodStart,
"periodEnd": args.periodEnd,
}
result = call_api(app_key, body, args.tenantId)
toon_output = toon_encode(result)
print(toon_output)
if __name__ == "__main__":
main()
FILE:scripts/sfe-zone/project-general.py
#!/usr/bin/env python3
"""
sfe-zone / project-general 脚本
用途:查询指定区划的采集填报数据并输出 TOON 编码结果
使用方式:
python3 scripts/sfe-zone/project-general.py --zoneId <区划ID> --projectId <项目ID> --periodStart <开始日期> --periodEnd <结束日期>
环境变量:
XG_BIZ_API_KEY / XG_APP_KEY — appKey(必须)
可选参数:
--tenantId 租户ID,用户存在多个租户身份时须传入
"""
import sys
import os
import json
import argparse
import requests
import warnings
# 引入共享的 TOON 编码器
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "common"))
from toon_encoder import encode as toon_encode
# 禁用 InsecureRequestWarning (因为 verify=False)
warnings.filterwarnings("ignore", category=requests.packages.urllib3.exceptions.InsecureRequestWarning)
# 接口完整 URL
API_URL = "https://sg-cwork-api.mediportal.com.cn/sfe-statistic/open-api/statistic/v1/user/zone/projects/total"
def call_api(app_key: str, body: dict, tenant_id: str = None) -> dict:
"""查询指定大区的项目概览情况"""
headers = {
"X-APP-KEY": app_key,
"Content-Type": "application/json",
}
if tenant_id:
headers["X-TENANT-ID"] = tenant_id
try:
response = requests.post(
API_URL,
json=body,
headers=headers,
verify=False,
allow_redirects=True,
timeout=60,
)
response.raise_for_status()
return response.json()
except Exception as e:
print(f"错误: 请求失败: {e}", file=sys.stderr)
sys.exit(1)
def main():
parser = argparse.ArgumentParser(description="查询指定区划的采集填报数据")
parser.add_argument("--zoneId", required=True, help="区划ID")
parser.add_argument("--projectId", required=True, help="项目ID")
parser.add_argument("--periodStart", required=True, help="期间开始日期")
parser.add_argument("--periodEnd", required=True, help="期间结束日期")
parser.add_argument("--tenantId", help="租户ID(用户存在多个租户身份时须传入)")
args = parser.parse_args()
app_key = os.environ.get("XG_BIZ_API_KEY") or os.environ.get("XG_APP_KEY")
if not app_key:
print("错误: 请设置环境变量 XG_BIZ_API_KEY 或 XG_APP_KEY", file=sys.stderr)
sys.exit(1)
body = {
"zoneId": args.zoneId,
"projectId": args.projectId,
"periodStart": args.periodStart,
"periodEnd": args.periodEnd,
}
result = call_api(app_key, body, args.tenantId)
toon_output = toon_encode(result)
print(toon_output)
if __name__ == "__main__":
main()
FILE:scripts/sfe-zone/project-plan.py
#!/usr/bin/env python3
"""
sfe-zone / project-plan 脚本
用途:查询指定区划的计划编制数据并输出 TOON 编码结果
使用方式:
python3 scripts/sfe-zone/project-plan.py --zoneId <区划ID> --projectId <项目ID> --periodStart <开始日期> --periodEnd <结束日期>
环境变量:
XG_BIZ_API_KEY / XG_APP_KEY — appKey(必须)
可选参数:
--tenantId 租户ID,用户存在多个租户身份时须传入
注意:此接口需要必填参数 zoneId、projectId、periodStart、periodEnd
"""
import sys
import os
import json
import argparse
import requests
import warnings
# 引入共享的 TOON 编码器
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "common"))
from toon_encoder import encode as toon_encode
# 禁用 InsecureRequestWarning (因为 verify=False)
warnings.filterwarnings("ignore", category=requests.packages.urllib3.exceptions.InsecureRequestWarning)
# 接口完整 URL
API_URL = "https://sg-cwork-api.mediportal.com.cn/sfe-statistic/open-api/statistic/v1/user/zone/projects/plan"
def call_api(app_key: str, body: dict, tenant_id: str = None) -> dict:
"""查询指定大区的项目计划值列表"""
headers = {
"X-APP-KEY": app_key,
"Content-Type": "application/json",
}
if tenant_id:
headers["X-TENANT-ID"] = tenant_id
try:
response = requests.post(
API_URL,
json=body,
headers=headers,
verify=False,
allow_redirects=True,
timeout=60,
)
response.raise_for_status()
return response.json()
except Exception as e:
print(f"错误: 请求失败: {e}", file=sys.stderr)
sys.exit(1)
def main():
parser = argparse.ArgumentParser(description="查询指定区划的计划编制数据")
parser.add_argument("--zoneId", required=True, help="区划ID")
parser.add_argument("--projectId", required=True, help="项目ID")
parser.add_argument("--periodStart", required=True, help="期间开始日期")
parser.add_argument("--periodEnd", required=True, help="期间结束日期")
parser.add_argument("--tenantId", help="租户ID(用户存在多个租户身份时须传入)")
args = parser.parse_args()
app_key = os.environ.get("XG_BIZ_API_KEY") or os.environ.get("XG_APP_KEY")
if not app_key:
print("错误: 请设置环境变量 XG_BIZ_API_KEY 或 XG_APP_KEY", file=sys.stderr)
sys.exit(1)
body = {
"zoneId": args.zoneId,
"projectId": args.projectId,
"periodStart": args.periodStart,
"periodEnd": args.periodEnd,
}
result = call_api(app_key, body, args.tenantId)
toon_output = toon_encode(result)
print(toon_output)
if __name__ == "__main__":
main()
FILE:scripts/sfe-zone/project-task.py
#!/usr/bin/env python3
"""
sfe-zone / project-task 脚本
用途:查询指定区划的待办任务并输出 TOON 编码结果
使用方式:
python3 scripts/sfe-zone/project-task.py --tenantId <租户ID> --zoneId <区划ID> --projectId <项目ID> --periodStart <开始日期> --periodEnd <结束日期>
环境变量:
XG_BIZ_API_KEY / XG_APP_KEY — appKey(必须)
参数说明:
--tenantId 租户ID(可选,用户存在多个租户身份时须传入)
--zoneId 区划ID(必须)
--projectId 项目ID(必须)
--periodStart 周期开始时间(必须)
--periodEnd 周期结束时间(必须)
--status 状态:0-待处理,1-已完成,2-已取消,3-过期关闭,4-手工关闭(可选)
--page 页码(可选,默认第1页)
"""
import sys
import os
import json
import argparse
import requests
import warnings
# 禁用 InsecureRequestWarning (因为 verify=False)
warnings.filterwarnings("ignore", category=requests.packages.urllib3.exceptions.InsecureRequestWarning)
# 引入共享的 TOON 编码器
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "common"))
from toon_encoder import encode as toon_encode
# 接口完整 URL
API_URL = "https://sg-cwork-api.mediportal.com.cn/sfe-statistic/open-api/statistic/v1/user/zone/projects/task"
def call_api(app_key: str, body: dict, tenant_id: str = None) -> dict:
"""查询指定大区的项目指标值列表"""
headers = {
"X-APP-KEY": app_key,
"Content-Type": "application/json",
}
if tenant_id:
headers["X-TENANT-ID"] = tenant_id
try:
response = requests.post(
API_URL,
json=body,
headers=headers,
verify=False,
allow_redirects=True,
timeout=60,
)
response.raise_for_status()
return response.json()
except Exception as e:
print(f"错误: 请求失败: {e}", file=sys.stderr)
sys.exit(1)
def main():
parser = argparse.ArgumentParser(description="查询指定区划的待办任务")
parser.add_argument(
"--tenantId",
type=str,
default=None,
help="租户ID(可选,用户存在多个租户身份时须传入)",
)
parser.add_argument("--zoneId", type=str, required=True, help="区划ID")
parser.add_argument("--projectId", type=str, required=True, help="项目ID")
parser.add_argument("--periodStart", type=str, required=True, help="周期开始时间")
parser.add_argument("--periodEnd", type=str, required=True, help="周期结束时间")
parser.add_argument(
"--status", type=int, choices=[0, 1, 2, 3, 4], default=None, help="状态(可选)"
)
parser.add_argument("--page", type=int, default=1, help="页码")
args = parser.parse_args()
app_key = os.environ.get("XG_BIZ_API_KEY") or os.environ.get("XG_APP_KEY")
if not app_key:
print("错误: 请设置环境变量 XG_BIZ_API_KEY 或 XG_APP_KEY", file=sys.stderr)
sys.exit(1)
# 构建请求体
body = {
"zoneId": args.zoneId,
"projectId": args.projectId,
"periodStart": args.periodStart,
"periodEnd": args.periodEnd,
"page": args.page,
}
if args.status is not None:
body["status"] = args.status
# 调用接口
result = call_api(app_key, body, args.tenantId)
# TOON 编码输出
toon_output = toon_encode(result)
print(toon_output)
if __name__ == "__main__":
main()
AI慧记能力:查询个人会议(进行中/历史)基于会议号查询参与会议的相关纪要、支持一键接收分享的慧记链接并内容解读、基于原文进行AI分析(总结/待办提取/专题分析)。本 Skill 通过依赖 `cms-auth-skills` 获取 `AppKey` 并完成鉴权后,才允许进入脚本调用链路。
---
name: ai-huiji
description: AI慧记能力:查询个人会议(进行中/历史)基于会议号查询参与会议的相关纪要、支持一键接收分享的慧记链接并内容解读、基于原文进行AI分析(总结/待办提取/专题分析)。本 Skill 通过依赖 `cms-auth-skills` 获取 `AppKey` 并完成鉴权后,才允许进入脚本调用链路。
skillcode: ai-huiji
github: https://github.com/xgjk/ai-huiji-skills
dependencies:
- cms-auth-skills
---
# AI慧记
**当前版本**: v2.6.0
> **AI慧记** 提供四类核心能力:
> 1. **📋 查询列表** — 从「我的慧记」或「视频会议号」两个维度查询会议记录
> 2. **📄 获取原文** — 拿到转写原文作为分析素材(支持全量 / 增量 / 改写三种模式)
> 3. **🔗 分享解析** — 支持一键接收分享的慧记链接并内容解读(短链 / 长链)
> 4. **🧠 AI 分析** — 基于原文进行总结、待办提取、专题分析
---
## 能力概览
| # | 接口 | 路径 | 说明 |
|---|---|---|---|
| 1 | chatListByPage | /ai-huiji/meetingChat/chatListByPage | 分页查询我的慧记列表(归属维度) |
| 2 | listHuiJiIdsByMeetingNumberV2 | /ai-huiji/meetingChat/listHuiJiIdsByMeetingNumberV2 | 按视频会议号查询慧记列表(会议维度) |
| 3 | splitRecordList | /ai-huiji/meetingChat/splitRecordList | 全量查询分片转写原文 |
| 4 | splitRecordListV2 | /ai-huiji/meetingChat/splitRecordListV2 | 增量查询分片转写原文 |
| 5 | checkSecondSttV2 | /ai-huiji/meetingChat/checkSecondSttV2 | 已结束会议的改写原文(更高质量,可能有发言人) |
| 6 | getChatFromShareId | /ai-huiji/meetingChat/getChatFromShareId | 通过分享链接的 shareId 获取原文 |
---
## 强制前置(保持不变)
调用任何脚本前,必须先通过依赖 Skill `cms-auth-skills` 获取有效 `AppKey`。
未鉴权时,不允许执行本 Skill 的任何 Python 脚本。
本 Skill 发起的所有 CWork API 请求均基于该 `AppKey` 鉴权。
AppKey 的获取与传递方式必须为:由上游 `cms-auth-skills` 注入/传递到本 Skill 执行命令中(`--app-key`)。
---
## 意图路由表
### 📋 列表查询
| 用户说 | 路由 | 说明 |
|---|---|---|
| "我的慧记" / "我的会议纪要" / "最近有什么会议" | **chatListByPage** | 归属维度,无需会议号 |
| "会议号 xxx" / "这场会议的纪要"(提供了会议号时) | **listHuiJiIdsByMeetingNumberV2** | 会议维度,需要会议号 |
| "帮我找 xxx 的会议" / "搜索 xxx" | **chatListByPage** + nameBlur | 搜索模式 |
| "进行中的会议" / "正在开的会" | **chatListByPage** `--state 0` | 进行中,一条命令搞定 |
| "历史会议" / "已结束的会议" | **chatListByPage** `--state 2` | 历史,一条命令搞定 |
### 📄 原文获取
| 用户说 | 路由 | 说明 |
|---|---|---|
| "这个会议的原文" / "会议内容" / "转写" | **get-transcript.py** | 统一入口,自动处理全量/增量/改写 |
| "最新的" / "刚才说的" / "最近几分钟的" | **get-transcript.py** | 自动增量拉取最新内容 |
### 🔗 分享链接解析
| 用户说 | 路由 | 说明 |
|---|---|---|
| "帮我看下这个分享链接" / "这个慧记分享里的内容" | **get-chat-from-share-id.py** | 脚本自动解析 shareId 并获取原文 |
| "http://s.medihub.cn/p/xxx" | **get-chat-from-share-id.py** | 短链自动解析 307 Location,提取 shareId |
| "https://任意域名/#/shareLinkPage?shareId=xxx" | **get-chat-from-share-id.py** | 长链(域名可变),脚本自动提取 shareId |
| "https://任意域名/#/meetingChatNew?shareId=xxx" | **get-chat-from-share-id.py** | 新版长链(域名可变),脚本自动提取 shareId |
| "https://任意域名/#/meetingChatNew?shareId=xxx&userName=张三&personId=123" | **get-chat-from-share-id.py** | 带扩展参数也可解析,只使用 shareId |
### 🧠 AI 分析
| 用户说 | 能力 | 说明 |
|---|---|---|
| "总结一下" / "会议纪要" / "概要" / "说了什么" | 📝 会议总结 | 基于原文生成结构化纪要 |
| "有什么待办" / "谁负责什么" / "跟进事项" | ✅ 待办提取 | 从原文中提取待办 |
| "关于 xxx 讨论了什么" / "财务那块" | 🔍 专题分析 | 按主题提取相关内容 |
---
## 列表查询详解
### 两个维度的区别
| 维度 | 接口 | 口径 | 适用场景 |
|---|---|---|---|
| **归属维度** | chatListByPage | 查「我的」慧记(归属在当前用户名下的记录) | 浏览自己的纪要、搜索、筛选 |
| **会议维度** | listHuiJiIdsByMeetingNumberV2 | 按「我参加的视频会议」查(即使他人录制也可查到) | 已知会议号、查找特定会议的纪要 |
### 路由规则(⚠️ 必须严格遵守)
```
用户提供了会议号?
├─ 是 → listHuiJiIdsByMeetingNumberV2(按会议号精确查)
└─ 否 → chatListByPage(走归属维度查询)
```
**⚠️ 常见错误:用户说"我的进行中会议"但 AI 跳过 chatListByPage 直接用了 listHuiJiIdsByMeetingNumberV2 + 上下文中记的会议号。这是错误的。没有明确会议号时必须走 chatListByPage。**
**⚠️ `lastTs` 默认值**:调用 listHuiJiIdsByMeetingNumberV2 时**必须传 `lastTs`**,默认限制为**最近 10 天**(`当前时间 - 10天` 的毫秒时间戳),避免返回过多无关记录。除非用户明确要求查更早的记录,否则一律带上此参数。
### 按状态查询(⚠️ 一步到位)
脚本已内置 `--state` 参数,**一次 API 调用 + 客户端过滤**,不要分多次查询:
```bash
# 进行中的会议(一条命令,无需二次查询)
python3 chat-list-by-page.py --human --state 0 0 10
# 已完成的会议
python3 chat-list-by-page.py --human --state 2 0 10
```
> 接口默认按 `updateTime` 倒序,进行中的会议必然排在最前面,`pageSize: 10` 绰绰有余。不需要额外加时间范围或多次查询。
### 无结果引导(⚠️ 必须执行)
当用户查询列表(chatListByPage)未找到任何结果时,AI 应主动引导:
- **chatListByPage 查无进行中会议**:提醒用户"归属维度没有找到你名下的进行中会议。如果你正在参加别人发起/录制的会议,请提供会议号,我按会议号帮你查。"
- **chatListByPage 查无任何会议**:提醒用户"归属维度没有找到记录。如果你参加的是他人录制的会议,请提供会议号试试。"
> **原因**:chatListByPage 只查归属在当前用户名下的记录,不包含用户仅"参加"但非"发起/录制"的会议。listHuiJiIdsByMeetingNumberV2 覆盖范围更广(按会议号查所有参与过的记录),但需要用户主动提供会议号。
### 会议状态判断
- **chatListByPage 返回** `combineState`: `0` = 进行中, `1` = 处理中, `2` = 已完成, `3` = 失败
- **listHuiJiIdsByMeetingNumberV2 返回** `recordState` 直接判断:
| `recordState` | 判断结果 | 说明 |
|---|---|---|
| `0` | 进行中 | 实时录制中 |
| `1` |已完成 | 可获取改写原文 |
> 注意:`recordState` 与 chatListByPage 返回的 `combineState` 含义不同,不可混用。
---
## 原文获取策略
这是本 Skill 的核心逻辑。AI 在需要获取会议原文时,**只需调用一个脚本**:
```bash
python3 scripts/huiji/get-transcript.py <meetingChatId> [--name "会议名称"]
```
脚本自动处理全量/增量切换、缓存读写、二次改写,AI **不需要**直接调用 `splitRecordList` / `splitRecordListV2` / `checkSecondSttV2` 接口。
## 分享链接解析策略
当用户消息中出现 AI 慧记分享链接时,AI 统一调用分享脚本,由脚本自动解析 shareId:
```bash
# 直接传 shareId
python3 scripts/huiji/get-chat-from-share-id.py 11be5542-c940-42e4-8528-be71d94f5b53
# 或直接传短链 URL / 短码(脚本自动解析)
python3 scripts/huiji/get-chat-from-share-id.py http://s.medihub.cn/p/128eeeOS
python3 scripts/huiji/get-chat-from-share-id.py 128eeeOS
```
### 支持的链接(域名可变)
- 短链:`http://s.medihub.cn/p/xxxxx`
- 长链:`https://<任意域名>/#/shareLinkPage?shareId=...`
- 新版长链:`https://<任意域名>/#/meetingChatNew?shareId=...`
- 带扩展参数:`https://<任意域名>/#/meetingChatNew?shareId=...&userName=...&personId=...`
### 处理流程
```
用户发送分享链接/短码/shareId
│
├─ 统一调用 get-chat-from-share-id.py(脚本内部自动解析 shareId)
├─ 解析规则:优先直接 shareId,其次 URL query/hash,最后短链 307 Location
└─ 取 srcText 进行总结/待办/专题分析并直接输出结论
```
### 失败处理
- 不能提取 `shareId`:提醒用户重新粘贴完整链接
- 业务错误(`resultCode != 1`):停止重试并直接反馈原因
- 无 `srcText`:提示暂无可用原文
- 成功拿到 `srcText` 后必须继续执行分析并回复结果,不得只回传中间数据
### 分享场景输出模板(必须遵守)
拿到分享数据后,向用户输出时应先给出基础信息头,再给分析结果:
- 标题:使用会议标题(如 `name`)
- 分享人:使用分享人姓名(如 `srcUser.name`)
- 纪要时间:使用 `createTime`(转换为可读时间)
- 时长:使用 `meetingLength`(转换为分钟/小时)
> 说明:括号内字段仅用于取值,不可原样向用户展示;对外输出必须使用自然语言标签(如"分享人""纪要时间""会议时长")。
然后再输出:总结 / 待办 / 专题分析等内容。
**禁止项**:
- 不展示 `updateTime`
- 不展示原始 JSON、API 字段名、技术过程描述
### 统一脚本工作流程
```
用户请求会议原文
│
├─ _final 缓存存在? → 直接返回(二次改写,最优质量)✅
│
├─ _live 缓存标记 completed? → 尝试拉 checkSecondSttV2
│ ├─ state=2 成功 → 写入 _final 缓存 → 返回
│ └─ state=1/3 未就绪 → 返回 _live 缓存
│
├─ _live 缓存存在? → splitRecordListV2 增量拉取 → 合并到 _live
│
└─ 无缓存 → splitRecordList 全量拉取 → 写入 _live 缓存
```
### 双缓存机制
| 缓存文件 | 来源 | 质量 | 说明 |
|---|---|---|---|
| `{id}_live.json` | splitRecordList + splitRecordListV2 | 实时转写 | 进行中会议的增量数据 |
| `{id}_final.json` | checkSecondSttV2 | 二次改写 | 已结束会议的最优数据(可能有发言人) |
**质量优先级**:`_final`(改写) > `_live`(实时) > 全量拉取
### 输出 JSON 结构
```json
{
"source": "full | incremental | cache_live | cache_final | second_stt",
"meetingChatId": "xxx",
"name": "会议名称",
"status": "ongoing | completed",
"lastSyncAt": 1716349200000,
"lastStartTime": 120034,
"fullText": "拼接好的全文...",
"fragments": [...]
}
```
`source` 字段说明当前数据来源,AI 可据此判断数据新鲜度,但**不需要向用户暴露此信息**。
### ⚠️ 防丢失保障
- **原子写入**:先写临时文件再 rename,防止写入崩溃损坏缓存
- **自动备份**:每次写入前备份旧缓存为 `.bak`
- **增量校验**:合并后分片数不能比旧缓存少,否则保留旧数据并输出 warning
- **去重策略**:同 startTime 保留 text 更长的版本,防止截断覆盖
---
## AI 分析能力
拿到原文后,AI 根据用户意图提供以下分析。所有分析基于原文内容,不依赖平台预生成的报告或待办。
### 📝 会议总结
**触发**:"总结一下"、"会议纪要"、"概要"、"说了什么"
**输出结构**:
- **会议主题**:一句话概括
- **讨论要点**:按议题分段,每段包含核心观点和关键信息。有发言人信息时标注谁说了什么
- **决策/结论**:达成的共识和决定
- **遗留问题**:未解决的事项
**注意事项**:
- 进行中会议必须明确告知"这是截至目前的内容,会议仍在进行中"
- 已结束会议优先使用 checkSecondSttV2 改写原文(有发言人区分);进行中会议不可使用此接口
- 原文较长时,分段总结后合并,避免遗漏
**⚠️ 严格约束:防幻觉与术语准确性**
原文由语音转文字生成,AI 在总结/待办提取/专题分析时必须遵守以下原则:
1. **禁止虚构以下任何信息**:
- ❌ 时间信息:会议时间、截止时间、时间节点等
- ❌ 人物信息:参会人员、负责人、相关人员等
- ❌ 具体数据:数字、数量、百分比等
- ❌ 决策结论:会议决策、结论、判断等
- 只能总结和提炼会议内容中明确提到的信息
- 所有引用的信息必须能在原始会议内容中找到对应依据
2. **专业术语智能纠错**:
- 药品名称、疾病名称、医学术语、公司名称等专业领域术语,结合上下文判断是否为语音识别错误(同音字、近音字)
- 同一术语在报告中统一使用正确标准表述
- ⚠️ 仅纠正有把握确认的明显错误,无法确定时保持原文,不要臆测
3. **归纳需诚实标注**:
- AI 归纳的框架名称、分类体系、流程总结等,需标注"AI 根据会议内容归纳"或类似说明
- 区分"原文原话"和"AI 的归纳提炼"
- 零散发言拼凑的时间线/决策链,需如实说明"根据多次讨论综合"
### ✅ 待办提取
**触发**:"有什么待办"、"谁负责什么"、"跟进事项"
**输出格式**:
- 每条待办:`[待办内容] → [责任人] → [截止时间/优先级]`
- 无法从原文确认的部分标注 ⚠️ 待确认
- 区分「明确分配的待办」和「讨论中未定的事项」
- 按优先级或提及顺序排列
- ⚠️ 待办的责任人、截止时间必须严格来自原文,不可推测或默认
### 🔍 专题分析
**触发**:"关于 xxx 讨论了什么"、"财务那块说了什么"
**输出**:
- 相关讨论内容的摘要
- 涉及的发言人(如有)
- 对应的大致时间位置(如"会议中段"、"后半段")
- 关联的决策或待办(如有)
---
## 本地缓存机制
### 缓存目录(相对 Skill 根目录)
```
.cache/huiji/
```
### 双缓存文件
| 文件 | 命名 | 说明 |
|---|---|---|
| 实时缓存 | `{meetingChatId}_live.json` | 进行中会议(splitRecordList + splitRecordListV2) |
| 改写缓存 | `{meetingChatId}_final.json` | 已结束会议(checkSecondSttV2,最优质量) |
两份数据格式相同:
```json
{
"meetingChatId": "xxx",
"name": "产品周会",
"status": "ongoing | completed",
"source": "full | incremental | second_stt | cache_live | cache_final",
"lastSyncAt": 1716349200000,
"lastStartTime": 120034,
"fullText": "拼接好的全文...",
"fragments": [
{ "startTime": 0, "text": "第一段内容...", "realTime": 1716345600000 },
{ "startTime": 120034, "text": "第二段内容...", "realTime": 1716345720000 }
]
}
```
### 字段说明
| 字段 | 说明 |
|---|---|
| `meetingChatId` | 会议 ID |
| `name` | 会议名称(来自列表或 `--name` 参数) |
| `status` | `ongoing`(进行中)/ `completed`(已结束) |
| `source` | 数据来源标识 |
| `lastSyncAt` | 上次同步的 Unix 毫秒时间戳 |
| `lastStartTime` | splitRecordListV2 增量查询的游标(分片的 startTime 值) |
| `fullText` | 按 startTime 排序拼接的完整转写文本 |
| `fragments` | 全部分片明细(startTime / text / realTime) |
### 缓存策略
- 用户每次请求会议原文时,`get-transcript.py` 自动管理缓存
- **进行中会议**:增量更新(splitRecordListV2 + `lastStartTime`),合并到 `_live`
- **已结束会议**:`_live` 标记 completed 后自动尝试拉改写,成功写入 `_final`
- 缓存无自动过期,由 AI 在必要时清理或用户主动清理
- ⚠️ AI **不需要**直接操作缓存文件,统一通过 `get-transcript.py` 管理
- **自动清理**:每次运行时自动清理超过 15 天的过期缓存(已结束会议 + 共享资料)
---
## 共享资料机制
多用户可以针对同一场会议共享分析资料,供其他人查看和讨论。
### 共享目录结构(相对 Skill 根目录)
```
.cache/huiji/shared/
└── {meetingNumber}/ # 按会议号隔离
└── {yyyyMMdd}/ # 按天隔离
├── 184500_王晓辉_发言总结.md
├── 185200_成伟_AI训练方案.md
└── ...
```
### 共享文件格式
```markdown
---
meetingNumber: 103760
meetingChatId: huijiXgMt_69c8cc89a61e5c000a9f8b72
author: 王晓辉
created: 2026-03-29 18:45:00
---
## 正文内容...
```
### 使用规则
1. **用户明确说"共享"时才写入**,不自动共享
2. **写入前必须确认**:告知用户即将共享的内容摘要和文件名
3. **文件名规范**:`{HHmmss}_{人名}_{原始标题}.md`,标题必须用原始标题,不额外处理
4. **查询时自动发现**:AI 在查询某场会议时,应检查 `shared/{meetingNumber}/` 目录,如有共享资料需主动提示用户
5. **读取共享**:用户说"看看有什么共享资料"或"分析下XX的那个总结"时,从对应目录读取
6. **时效性**:共享资料超过 15 天自动清理(与缓存同步清理)
### 典型场景
| 用户说 | AI 行为 |
|---|---|
| "把这个分析共享给其他人" | 写入 shared/{meetingNumber}/{yyyyMMdd}/ |
| "103760有什么共享资料" | 列出 shared/103760/ 下的所有文件 |
| "分析下王晓辉刚才的那个总结" | 从 shared/{meetingNumber}/ 下找到对应文件并读取分析 |
---
## _id 字段处理(重要)
chatListByPage 返回的 `_id` 可能有 `__数字` 后缀(如 `abc123__45678`),不能直接用作 `meetingChatId`:
- **处理方式一**:截取双下划线前的部分 → `abc123`
- **处理方式二**:使用 `originChatId` 字段的值
脚本和 AI 在将 chatListByPage 列表项传给 splitRecordList / checkSecondSttV2 / splitRecordListV2 时,必须做此处理。
---
## 时间戳处理规范(⚠️ 必须严格遵守)
所有接口返回的时间字段均为 **13 位毫秒级 Unix 时间戳**(如 `1774763950431`)。
### 转换规则
```
毫秒时间戳 → 秒:除以 1000(1774763950431 / 1000 = 1774763950.431)
秒 → 本地时间:UTC+8(Asia/Shanghai)
```
### 展示规则
**向用户展示时,必须将时间戳转换为人类可读格式**,禁止直接展示原始数字。
- **日期时间**:`2026-03-29 13:59:10`
- **仅日期**:`2026-03-29`
- **仅时间**:`13:59`
- **时长**:毫秒转分钟 → `7624789 ms ≈ 127 分钟 ≈ 2小时7分钟`
### 转换示例
| 原始值(毫秒时间戳) | 转换结果 |
|---|---|
| `1774763950431` | `2026-03-29 13:59:10` |
| `1774704179623` | `2026-03-28 22:42:59` |
| `7624789`(meetingLength,毫秒) | `2小时7分钟` |
### 分片时间戳
- `realTime`:现实时间戳(毫秒),转换为绝对时间展示
- `startTime`(splitRecordList/splitRecordListV2):录音内偏移量(毫秒),转换为 `00:12:34` 格式的录音内时间点
---
## 多环境部署
本 Skill 可部署在任意 OpenClaw / EasyClaw 环境中。脚本调用使用相对于 SKILL.md 的路径:
```
scripts/huiji/<脚本名>.py
```
OpenClaw 在加载 Skill 时会将完整路径注入上下文,AI 直接拼接使用即可,无需硬编码绝对路径。
---
## 约束
### 执行硬约束(最高优先级)
1. **仅用现有脚本**:只允许调用 `scripts/huiji/` 下已存在脚本完成查询、取数与原文获取。
2. **禁止临时脚本**:禁止创建任何临时调用脚本(如 `temp_*.py`)、一次性中转脚本或额外落盘编排脚本。
3. **失败处理边界**:现有脚本失败时,仅允许重试或切换到其它已存在脚本;若能力缺口确实存在,必须先向用户说明并确认后再改代码。
### 统一规范
1. **鉴权**:所有接口只需 `appKey`(`XG_BIZ_API_KEY`),无需 access-token
2. **生产域名**:`sg-al-ai-voice-assistant.mediportal.com.cn/api/open-api`
3. **重试策略**:脚本内置最多重试 3 次,间隔 1 秒
4. **依赖声明**:鉴权依赖 `cms-auth-skills`
### 脚本使用规则
1. **统一入口**:获取会议原文时**必须使用 `get-transcript.py`**,禁止直接调用 `splitRecordList` / `splitRecordListV2` / `checkSecondSttV2` 脚本
2. **分页默认值**:chatListByPage 的 `pageNum` 从 0 开始;未经用户明确指定时,默认传 `pageNum=0`、`pageSize=10`,查询最新一页记录,禁止自行推断或沿用上下文中的历史页码
3. **增量优先**:有缓存时优先用 splitRecordListV2 增量拉取,减少重复传输
4. **checkSecondSttV2 容错**:已结束会议必须 fallback 到 splitRecordList,刚结束时 checkSecondSttV2 可能无内容
5. **_id 后缀处理**:chatListByPage 返回的 `_id` 可能有 `__数字` 后缀,用作 `meetingChatId` 时需截取或用 `originChatId`
### 路由与加载规则
1. **路由严格遵守**:用户未提供会议号时必须走 chatListByPage(归属维度),禁止从上下文记忆中自动取会议号跳过路由
2. **无结果引导**:chatListByPage 查无结果时,必须主动提醒用户可提供会议号走 listHuiJiIdsByMeetingNumberV2 查询(因为 chatListByPage 不含他人录制/发起的会议)
3. **进行中会议时间过滤**:查询进行中会议时,API 可能返回历史异常数据(录音未正常结束导致状态卡在"进行中")。AI 必须过滤掉创建时间超过 **2 天**的"进行中"记录,只保留今天和昨天的。向用户展示前静默过滤,无需解释
4. **分享链接识别**:域名不做强绑定。只要消息中出现可解析 URL,且命中任一条件即识别为 AI 慧记分享链接:
- URL 中包含 `shareId=` 参数;
- hash 路由包含 `#/shareLinkPage` 或 `#/meetingChatNew`;
- 或短链形态 `s.medihub.cn/p/`(由脚本内解析 307 Location 并提取 shareId)。
- URL 中的其它参数(如 `userName`、`personId`、渠道参数等)一律忽略,不参与路由判断。
5. **分享原文入口**:分享链接原文获取统一使用 `get-chat-from-share-id.py`。允许直接传 `shareId`、短链 URL 或短码;优先让脚本内部自动解析,避免在外部额外引入 `web_fetch` 前置步骤
### 宪章
1. **AI 分析基于原文**:所有分析由 AI 基于转写原文完成,不依赖平台预生成的报告或待办
2. **时间戳必须转换**:所有时间戳字段(createTime、startTime、realTime、meetingLength 等)向用户展示时必须转换为可读格式,禁止直接展示原始数字。详见上方「时间戳处理规范」
3. **防幻觉与术语准确性**:总结/待办/专题分析时,禁止虚构时间、人物、数据、决策;专业术语结合上下文智能纠错但不确定时保持原文;AI 归纳需诚实标注来源。详见上方「严格约束:防幻觉与术语准确性」
4. **API 校验错误不重试**:接口返回 `resultCode` 不为 `1` 的业务错误时,AI 必须**停止执行**,直接向用户展示错误原因,不得反复修改参数重试同一条接口
5. **隐藏技术细节**:向用户输出时禁止展示内部技术过程,包括但不限于:
- 原文获取策略的选择过程(如"先尝试 checkSecondSttV2"、"fallback 到 splitRecordList"等)
- 接口调用细节(如"调了 chatListByPage 接口"、"返回 resultCode: 1"等)
- 脚本名称、缓存机制、增量拉取等实现细节
- **所有 API 字段名**(后端返回的任何 JSON key),必须全部翻译为人类语言后呈现
- 原始 JSON 数据结构
- **原则**:用户只需要看到结果(会议内容/总结/待办),不需要知道你怎么拿到的
6. **输出约束(分享场景)**:拿到分享原文后,AI 必须先输出基础信息头(标题、分享人、纪要时间、会议时长),再输出总结/待办/专题分析结果;禁止直接向用户展示原始 JSON 结构、API 字段名或仅回传 `srcText`
---
## 能力树
```
ai-huiji/
├── SKILL.md
├── .cache/
│ └── huiji/ # 会议原文缓存(按 meetingChatId 存储)
│ └── shared/ # 共享资料(按 meetingNumber/yyyyMMdd 存储)
├── openapi/
│ └── huiji/
│ ├── api-index.md # 接口索引
│ ├── chat-list-by-page.md # chatListByPage 查询我的慧记列表
│ ├── list-by-meeting-number.md # listHuiJiIdsByMeetingNumberV2 按会议号查询慧记列表
│ ├── split-record-list.md # splitRecordList 全量分片转写
│ ├── split-record-list-v2.md # splitRecordListV2 增量分片转写
│ ├── check-second-stt-v2.md # checkSecondSttV2 改写原文状态
│ └── get-chat-from-share-id.md # 通过 shareId 获取分享原文
├── examples/
│ └── huiji/
│ └── README.md # 面向外部部署者的 Quick Start 手册(触发示例 + 脚本用法)
└── scripts/
└── huiji/
├── README.md # 脚本清单 + 使用示例
├── get-transcript.py # 🌟 统一入口(自动处理全量/增量/改写/缓存)
├── chat-list-by-page.py # chatListByPage 分页查询
├── list-by-meeting-number.py # listHuiJiIdsByMeetingNumberV2 按会议号查询
├── split-record-list.py # splitRecordList 全量分片转写(内部使用)
├── split-record-list-v2.py # splitRecordListV2 增量分片转写(内部使用)
├── check-second-stt-v2.py # checkSecondSttV2 改写原文状态(内部使用)
└── get-chat-from-share-id.py # 按 shareId 获取分享原文
```
FILE:_meta.json
{"name":"ai-huiji","description":"AI慧记能力:查询个人会议(进行中/历史)基于会议号查询参与会议的相关纪要、支持一键接收分享的慧记链接并内容解读、基于原文进行AI分析(总结/待办提取/专题分析)。","version":"1.3"}
FILE:scripts/huiji/chat-list-by-page.py
#!/usr/bin/env python3
"""
huiji / chat-list-by-page 脚本
用途:分页查询当前用户的慧记列表(归属维度)
使用方式:
python3 scripts/huiji/chat-list-by-page.py [--human] [--state <combineState>] [--body '<json>'] [pageNum pageSize]
# 基本用法(第0页,每页10条,原始JSON)
python3 chat-list-by-page.py 0 10
# 人类可读格式(时间戳自动转换)
python3 chat-list-by-page.py --human 0 10
# 只查进行中的会议(combineState=0),一步到位
python3 chat-list-by-page.py --human --state 0 0 10
# 只查已完成的会议
python3 chat-list-by-page.py --human --state 2 0 10
# 完整参数(JSON body)
python3 chat-list-by-page.py --body '{"pageNum":0,"pageSize":10,"chatType":7,"nameBlur":"周会","minTs":1716345600000}'
# 人类可读 + JSON body
python3 chat-list-by-page.py --human --body '{"pageNum":0,"pageSize":10}'
环境变量:
XG_BIZ_API_KEY — appKey(必须)
XG_USER_TOKEN — access-token(可选,增强鉴权)
"""
import sys
import os
import json
import time
import argparse
from datetime import datetime, timezone, timedelta
import requests
import warnings
# 禁用 InsecureRequestWarning (因为 verify=False)
warnings.filterwarnings("ignore", category=requests.packages.urllib3.exceptions.InsecureRequestWarning)
def setup_utf8_stdio():
"""Best-effort UTF-8 stdio for Windows console display."""
for stream_name in ("stdout", "stderr"):
stream = getattr(sys, stream_name, None)
if stream and hasattr(stream, "reconfigure"):
try:
stream.reconfigure(encoding="utf-8", errors="replace")
except Exception:
pass
setup_utf8_stdio()
API_URL = "https://sg-al-ai-voice-assistant.mediportal.com.cn/api/open-api/ai-huiji/meetingChat/chatListByPage"
MAX_RETRIES = 3
RETRY_DELAY = 1
DEFAULT_SORT_KEY = "createTime"
TZ = timezone(timedelta(hours=8)) # Asia/Shanghai
# combineState 映射
COMBINE_STATE = {0: "进行中", 1: "处理中", 2: "已完成", 3: "失败"}
def ts_to_str(ts_ms):
"""毫秒时间戳 → 人类可读时间"""
if ts_ms is None:
return "-"
try:
dt = datetime.fromtimestamp(ts_ms / 1000, tz=TZ)
return dt.strftime("%Y-%m-%d %H:%M:%S")
except (TypeError, ValueError, OSError):
return str(ts_ms)
def duration_to_str(ms):
"""毫秒时长 → 人类可读"""
if ms is None:
return "-"
try:
total_sec = int(ms / 1000)
h, remainder = divmod(total_sec, 3600)
m, s = divmod(remainder, 60)
if h > 0:
return f"{h}小时{m}分钟"
elif m > 0:
return f"{m}分钟{s}秒"
else:
return f"{s}秒"
except (TypeError, ValueError):
return str(ms)
def build_headers() -> dict:
headers = {"Content-Type": "application/json"}
token = os.environ.get("XG_USER_TOKEN")
app_key = os.environ.get("XG_BIZ_API_KEY") or os.environ.get("XG_APP_KEY")
if token:
headers["access-token"] = token
if app_key:
headers["appKey"] = app_key
if not token and not app_key:
print("错误: 请至少设置 XG_USER_TOKEN 或 XG_BIZ_API_KEY", file=sys.stderr)
sys.exit(1)
return headers
def call_api(body: dict) -> dict:
headers = build_headers()
last_err = None
for attempt in range(MAX_RETRIES):
try:
response = requests.post(
API_URL,
json=body,
headers=headers,
verify=False,
allow_redirects=True,
timeout=60,
)
response.raise_for_status()
return json.loads(response.content.decode("utf-8"))
except Exception as e:
last_err = e
if attempt < MAX_RETRIES - 1:
time.sleep(RETRY_DELAY)
print(f"错误: 请求失败(重试{MAX_RETRIES}次): {last_err}", file=sys.stderr)
sys.exit(1)
def format_human(result: dict) -> str:
"""将结果转换为人类可读格式"""
if result.get("resultCode") != 1:
return f"❌ 请求失败: {result.get('resultMsg', '未知错误')}"
data = result.get("data", {})
total = data.get("total", 0)
items = data.get("pageContent", [])
state_filter = result.get("_stateFilter")
# --state 过滤时的无结果提示
if state_filter is not None and len(items) == 0:
state_name = COMBINE_STATE.get(state_filter, "指定状态")
return f"没有{state_name}的会议"
if state_filter is not None:
state_name = COMBINE_STATE.get(state_filter, "")
lines = [f"共 {total} 条{state_name}记录:\n"]
else:
lines = [f"共 {total} 条记录:\n"]
for i, item in enumerate(items, 1):
name = item.get("name", "(无名称)")
state = COMBINE_STATE.get(item.get("combineState", -1), "未知")
create = ts_to_str(item.get("createTime"))
length = duration_to_str(item.get("meetingLength"))
chat_id = item.get("originChatId") or item.get("_id", "")
state_icon = "🟢" if item.get("combineState") == 0 else "✅" if item.get("combineState") == 2 else "⏳"
lines.append(f"{i}. {state_icon} **{name}**")
lines.append(f" 开始: {create} | 时长: {length}")
lines.append(f" ID: {chat_id}")
if item.get("simpleSummary"):
summary = item["simpleSummary"][:80] + "..." if len(item.get("simpleSummary", "")) > 80 else item["simpleSummary"]
lines.append(f" 摘要: {summary}")
lines.append("")
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(
prog="chat-list-by-page.py",
description="分页查询当前用户的慧记列表(归属维度)",
)
parser.add_argument("--human", action="store_true", help="时间戳自动转换为可读格式")
parser.add_argument(
"--state",
type=int,
choices=[0, 1, 2, 3],
help="按 combineState 过滤 (0=进行中 1=处理中 2=已完成 3=失败)",
)
parser.add_argument("--body", type=str, help="完整 JSON body 字符串")
parser.add_argument("pageNum", type=int, nargs="?", help="页码,从 0 开始")
parser.add_argument("pageSize", type=int, nargs="?", help="每页条数")
args = parser.parse_args()
human = args.human
state_filter = args.state
if args.body is not None:
body = json.loads(args.body)
elif args.pageNum is not None and args.pageSize is not None:
body = {"pageNum": args.pageNum, "pageSize": args.pageSize}
else:
parser.error("请提供 --body 或 pageNum pageSize")
# 默认按 createTime 排序;若用户在 --body 中显式传入 sortKey,则保持用户值
body.setdefault("sortKey", DEFAULT_SORT_KEY)
result = call_api(body)
# --state 过滤:从返回结果中只保留指定状态
if state_filter is not None and result.get("resultCode") == 1:
all_items = result.get("data", {}).get("pageContent", [])
total = result.get("data", {}).get("total", 0)
filtered = [item for item in all_items if item.get("combineState") == state_filter]
result["data"]["pageContent"] = filtered
result["data"]["total"] = len(filtered)
result["_stateFilter"] = state_filter
result["_originalTotal"] = total
if human:
print(format_human(result))
else:
print(json.dumps(result, ensure_ascii=False))
if __name__ == "__main__":
main()
FILE:scripts/huiji/check-second-stt-v2.py
#!/usr/bin/env python3
"""
huiji / check-second-stt-v2 脚本
用途:查询已结束会议的改写原文状态;成功时 sttPartList 常含优于会中分片的原文(可能有发言人区分)
使用方式:
python3 scripts/huiji/check-second-stt-v2.py [--body '<json>'] <meetingChatId>
# 基本用法
python3 check-second-stt-v2.py abc123
# JSON body
python3 check-second-stt-v2.py --body '{"meetingChatId":"abc123"}'
环境变量:
XG_BIZ_API_KEY — appKey(必须)
XG_USER_TOKEN — access-token(可选,增强鉴权)
"""
import sys
import os
import json
import time
import requests
import warnings
# 禁用 InsecureRequestWarning (因为 verify=False)
warnings.filterwarnings("ignore", category=requests.packages.urllib3.exceptions.InsecureRequestWarning)
def setup_utf8_stdio():
"""Best-effort UTF-8 stdio for Windows console display."""
for stream_name in ("stdout", "stderr"):
stream = getattr(sys, stream_name, None)
if stream and hasattr(stream, "reconfigure"):
try:
stream.reconfigure(encoding="utf-8", errors="replace")
except Exception:
pass
setup_utf8_stdio()
API_URL = "https://sg-al-ai-voice-assistant.mediportal.com.cn/api/open-api/ai-huiji/meetingChat/checkSecondSttV2"
MAX_RETRIES = 3
RETRY_DELAY = 1
def build_headers() -> dict:
headers = {"Content-Type": "application/json"}
token = os.environ.get("XG_USER_TOKEN")
app_key = os.environ.get("XG_BIZ_API_KEY") or os.environ.get("XG_APP_KEY")
if token:
headers["access-token"] = token
if app_key:
headers["appKey"] = app_key
if not token and not app_key:
print("错误: 请至少设置 XG_USER_TOKEN 或 XG_BIZ_API_KEY", file=sys.stderr)
sys.exit(1)
return headers
def call_api(body: dict) -> dict:
headers = build_headers()
last_err = None
for attempt in range(MAX_RETRIES):
try:
response = requests.post(
API_URL,
json=body,
headers=headers,
verify=False,
allow_redirects=True,
timeout=60,
)
response.raise_for_status()
return json.loads(response.content.decode("utf-8"))
except Exception as e:
last_err = e
if attempt < MAX_RETRIES - 1:
time.sleep(RETRY_DELAY)
print(f"错误: 请求失败(重试{MAX_RETRIES}次): {last_err}", file=sys.stderr)
sys.exit(1)
def main():
if len(sys.argv) >= 2 and sys.argv[1] == "--body":
if len(sys.argv) < 3:
print("用法: check-second-stt-v2.py --body '<json>'", file=sys.stderr)
sys.exit(1)
body = json.loads(sys.argv[2])
elif len(sys.argv) >= 2:
body = {"meetingChatId": sys.argv[1]}
else:
print("用法: check-second-stt-v2.py [--body '<json>'] <meetingChatId>", file=sys.stderr)
sys.exit(1)
result = call_api(body)
print(json.dumps(result, ensure_ascii=False))
if __name__ == "__main__":
main()
FILE:scripts/huiji/get-chat-from-share-id.py
#!/usr/bin/env python3
"""
huiji / get-chat-from-share-id 脚本
用途:
1) 接收 shareId
2) 调用 getChatFromShareId 获取分享原文
使用方式:
python3 get-chat-from-share-id.py <shareId>
python3 get-chat-from-share-id.py --body '{"shareId":"xxx"}'
说明:
- 本脚本不负责短链/长链解析。
- 若用户提供 URL,需先在 Skill 层用 web_fetch 解析得到 shareId,再调用本脚本。
环境变量:
XG_BIZ_API_KEY — appKey(必须)
XG_USER_TOKEN — access-token(可选)
"""
import json
import os
import re
import sys
import time
from urllib.parse import parse_qs, urlparse
import requests
import warnings
# 禁用 InsecureRequestWarning (因为 verify=False)
warnings.filterwarnings("ignore", category=requests.packages.urllib3.exceptions.InsecureRequestWarning)
def setup_utf8_stdio():
"""Best-effort UTF-8 stdio for Windows console display."""
for stream_name in ("stdout", "stderr"):
stream = getattr(sys, stream_name, None)
if stream and hasattr(stream, "reconfigure"):
try:
stream.reconfigure(encoding="utf-8", errors="replace")
except Exception:
pass
setup_utf8_stdio()
API_URL = "https://sg-al-ai-voice-assistant.mediportal.com.cn/api/open-api/ai-huiji/meetingChat/getChatFromShareId"
DEFAULT_SHORT_LINK_BASE = "http://s.medihub.cn/p/"
MAX_RETRIES = 3
RETRY_DELAY = 1
# 常见 shareId 形态(UUID)
UUID_RE = re.compile(r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$")
def build_headers() -> dict:
headers = {"Content-Type": "application/json"}
token = os.environ.get("XG_USER_TOKEN")
app_key = os.environ.get("XG_BIZ_API_KEY") or os.environ.get("XG_APP_KEY")
if token:
headers["access-token"] = token
if app_key:
headers["appKey"] = app_key
if not token and not app_key:
print(json.dumps({"error": "请至少设置 XG_USER_TOKEN 或 XG_BIZ_API_KEY"}, ensure_ascii=False))
sys.exit(1)
return headers
def call_api(body: dict) -> dict:
headers = build_headers()
last_err = None
for attempt in range(MAX_RETRIES):
try:
response = requests.post(
API_URL,
json=body,
headers=headers,
verify=False,
allow_redirects=True,
timeout=60,
)
response.raise_for_status()
return json.loads(response.content.decode("utf-8"))
except Exception as e:
last_err = e
if attempt < MAX_RETRIES - 1:
time.sleep(RETRY_DELAY)
print(json.dumps({"error": f"请求失败(重试{MAX_RETRIES}次): {last_err}"}, ensure_ascii=False))
sys.exit(1)
def extract_share_id_from_url(url: str) -> str:
"""从 URL 的 query 或 fragment 中提取 shareId。"""
try:
parsed = urlparse(url)
# 1) 常规 query:...?shareId=xxx
qs = parse_qs(parsed.query)
share_id = (qs.get("shareId") or [""])[0].strip()
if UUID_RE.match(share_id):
return share_id
# 2) hash 路由:...#/path?shareId=xxx
# 先拿 fragment,再截取 ? 后的参数串
frag = parsed.fragment or ""
if "?" in frag:
frag_qs = parse_qs(frag.split("?", 1)[1])
share_id = (frag_qs.get("shareId") or [""])[0].strip()
if UUID_RE.match(share_id):
return share_id
except Exception:
pass
return ""
def resolve_share_id(value: str) -> str:
"""
支持三种输入:
1) UUID shareId
2) 短链 URL(例如 http://s.medihub.cn/p/128eeeOS)
3) 短码(例如 128eeeOS)
"""
raw = (value or "").strip()
if not raw:
return ""
# 1) 已经是 UUID
if UUID_RE.match(raw):
return raw
# 2) URL 里已直接包含 shareId
if raw.startswith("http://") or raw.startswith("https://"):
sid = extract_share_id_from_url(raw)
if sid:
return sid
short_url = raw
else:
# 3) 当作短码,拼成短链 URL
short_base = os.environ.get("XG_SHARE_SHORT_BASE", DEFAULT_SHORT_LINK_BASE).strip()
if not short_base:
short_base = DEFAULT_SHORT_LINK_BASE
if not short_base.endswith("/"):
short_base += "/"
short_url = f"{short_base}{raw}"
# 请求短链,手动读取重定向头(不自动跟随)
try:
resp = requests.get(short_url, allow_redirects=False, timeout=20, verify=False)
except Exception:
return ""
location = (resp.headers.get("Location") or "").strip()
if not location:
return ""
return extract_share_id_from_url(location)
def main():
args = sys.argv[1:]
raw_value = None
if len(args) >= 2 and args[0] == "--body":
body = json.loads(args[1])
raw_value = body.get("shareId", "")
elif len(args) >= 1:
raw_value = args[0]
else:
print(
json.dumps(
{
"error": "用法: get-chat-from-share-id.py <shareId> | --body '{\"shareId\":\"...\"}'"
},
ensure_ascii=False,
)
)
sys.exit(1)
share_id = resolve_share_id(raw_value)
if not share_id:
print(
json.dumps(
{
"error": (
"无法从输入中提取 shareId。"
"请传入 shareId、短链 URL,或可解析到 shareId 的短码。"
)
},
ensure_ascii=False,
)
)
sys.exit(1)
# 仅提示,不阻断。某些环境可能不是 UUID。
if not UUID_RE.match(share_id):
pass
result = call_api({"shareId": share_id})
print(json.dumps(result, ensure_ascii=False))
if __name__ == "__main__":
main()
FILE:scripts/huiji/get-transcript.py
#!/usr/bin/env python3
"""
huiji / get-transcript 统一入口脚本
用途:智能获取会议转写原文,自动处理缓存(全量/增量切换 + 二次改写)
AI 只需调用此脚本,无需关心底层接口差异:
python3 get-transcript.py <meetingChatId> [--name "会议名称"]
双缓存机制:
- {chatId}_live.json — 进行中的实时数据(splitRecordList 全量 + splitRecordListV2 增量)
- {chatId}_final.json — 结束后的二次改写数据(checkSecondSttV2,质量更高)
优先级:_final > _live > 全量拉取
- _final 存在 → 直接返回(最优质量)
- _live 存在 + 进行中 → 增量拉取合并
- _live 存在 + 已结束 → 尝试拉二次改写 → 写 _final
- 无缓存 → 全量拉取 → 写 _live
环境变量:
XG_BIZ_API_KEY — appKey(必须)
XG_USER_TOKEN — access-token(可选)
"""
import sys
import os
import json
import time
from datetime import datetime, timezone, timedelta
import requests
import warnings
# 禁用 InsecureRequestWarning (因为 verify=False)
warnings.filterwarnings("ignore", category=requests.packages.urllib3.exceptions.InsecureRequestWarning)
def setup_utf8_stdio():
"""Best-effort UTF-8 stdio for Windows console display."""
for stream_name in ("stdout", "stderr"):
stream = getattr(sys, stream_name, None)
if stream and hasattr(stream, "reconfigure"):
try:
stream.reconfigure(encoding="utf-8", errors="replace")
except Exception:
pass
setup_utf8_stdio()
TZ = timezone(timedelta(hours=8))
# 接口地址
API_FULL = "https://sg-al-ai-voice-assistant.mediportal.com.cn/api/open-api/ai-huiji/meetingChat/splitRecordList"
API_INCR = "https://sg-al-ai-voice-assistant.mediportal.com.cn/api/open-api/ai-huiji/meetingChat/splitRecordListV2"
API_SECOND_STT = "https://sg-al-ai-voice-assistant.mediportal.com.cn/api/open-api/ai-huiji/meetingChat/checkSecondSttV2"
MAX_RETRIES = 3
RETRY_DELAY = 1
CACHE_EXPIRE_DAYS = 15 # 缓存过期天数,超过此天数的已结束会议缓存自动清理
# 缓存目录:skill 根目录下的 .cache/huiji/
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
SKILL_ROOT = os.path.dirname(os.path.dirname(SCRIPT_DIR))
CACHE_DIR = os.path.join(SKILL_ROOT, ".cache", "huiji")
def build_headers() -> dict:
headers = {"Content-Type": "application/json"}
token = os.environ.get("XG_USER_TOKEN")
app_key = os.environ.get("XG_BIZ_API_KEY") or os.environ.get("XG_APP_KEY")
if token:
headers["access-token"] = token
if app_key:
headers["appKey"] = app_key
if not token and not app_key:
print(json.dumps({"error": "请至少设置 XG_USER_TOKEN 或 XG_BIZ_API_KEY"}, ensure_ascii=False))
sys.exit(1)
return headers
def call_api(url: str, body: dict) -> dict:
headers = build_headers()
last_err = None
for attempt in range(MAX_RETRIES):
try:
response = requests.post(
url,
json=body,
headers=headers,
verify=False,
allow_redirects=True,
timeout=60,
)
response.raise_for_status()
return json.loads(response.content.decode("utf-8"))
except Exception as e:
last_err = e
if attempt < MAX_RETRIES - 1:
time.sleep(RETRY_DELAY)
print(json.dumps({"error": f"请求失败(重试{MAX_RETRIES}次): {last_err}"}, ensure_ascii=False))
sys.exit(1)
def cache_path(chat_id: str, suffix: str = "") -> str:
"""获取缓存文件路径:_live / _final"""
return os.path.join(CACHE_DIR, f"{chat_id}{suffix}.json")
def load_cache(chat_id: str, suffix: str = "") -> dict:
"""加载缓存,不存在返回 None"""
path = cache_path(chat_id, suffix)
if not os.path.exists(path):
return None
try:
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
except Exception:
return None
def save_cache(chat_id: str, cache: dict, suffix: str = ""):
"""原子写入缓存:先写临时文件,再 rename 替换"""
os.makedirs(CACHE_DIR, exist_ok=True)
path = cache_path(chat_id, suffix)
tmp_path = path + ".tmp"
with open(tmp_path, "w", encoding="utf-8") as f:
json.dump(cache, f, ensure_ascii=False)
os.replace(tmp_path, path)
def backup_cache(chat_id: str, suffix: str = ""):
"""备份当前缓存"""
path = cache_path(chat_id, suffix)
bak_path = path + ".bak"
if os.path.exists(path):
os.replace(path, bak_path)
def merge_fragments(existing_fragments: list, new_fragments: list) -> list:
"""
合并分片,按 startTime 去重排序
同 startTime 取 text 较长的版本(防止增量返回截断版覆盖完整版)
"""
merged = {}
for frag in existing_fragments:
st = frag.get("startTime")
text = frag.get("text")
if st is not None and text:
merged[st] = frag
for frag in new_fragments:
st = frag.get("startTime")
text = frag.get("text")
if st is not None and text:
if st in merged:
if len(text) > len(merged[st].get("text", "")):
merged[st] = frag
else:
merged[st] = frag
return sorted(merged.values(), key=lambda x: x["startTime"])
def build_full_text(fragments: list) -> str:
"""从分片列表拼接全文"""
parts = []
for frag in fragments:
text = frag.get("text")
if text:
parts.append(text)
return "".join(parts)
def parse_live_fragments(raw_data) -> list:
"""解析 splitRecordList/splitRecordListV2 返回的分片数据"""
fragments = []
if isinstance(raw_data, list):
for f in raw_data:
if f.get("text") is not None:
fragments.append({
"startTime": f.get("startTime", 0),
"text": f.get("text"),
"realTime": f.get("realTime"),
})
return fragments
def parse_second_stt_fragments(raw_list) -> list:
"""解析 checkSecondSttV2 返回的 sttPartList 分片数据"""
fragments = []
if isinstance(raw_list, list):
for f in raw_list:
text = f.get("text")
if text:
frag = {"startTime": f.get("startTime", 0), "text": text}
# sttPartList 可能有 speaker 等额外字段,一并保留
for k in ("speaker", "realTime", "endTime"):
if k in f:
frag[k] = f[k]
fragments.append(frag)
return fragments
def try_fetch_second_stt(chat_id: str) -> dict:
"""
尝试拉取二次改写数据
返回 {"fragments": [...], "fullText": "...", "state": 2, "source": "second_stt"}
或 {"fragments": [], "state": 1/3, "source": "second_stt_pending/failed"}
"""
try:
result = call_api(API_SECOND_STT, {"meetingChatId": chat_id})
state = result.get("state", 0)
stt_list = result.get("sttPartList", [])
if state == 2 and stt_list:
fragments = parse_second_stt_fragments(stt_list)
if fragments:
return {
"fragments": fragments,
"fullText": build_full_text(fragments),
"state": 2,
"source": "second_stt",
}
# state=1 处理中,state=3 失败,或 sttList 为空
return {
"fragments": [],
"fullText": "",
"state": state,
"source": "second_stt_pending" if state == 1 else "second_stt_failed",
}
except Exception:
return {
"fragments": [],
"fullText": "",
"state": 3,
"source": "second_stt_failed",
}
def cleanup_expired_cache():
"""
清理过期缓存:
1. 删除 lastSyncAt 超过 CACHE_EXPIRE_DAYS 天的已结束会议缓存
2. 删除 shared 目录下超过 CACHE_EXPIRE_DAYS 天的日期目录
每次运行时调用一次,懒清理,不需要定时任务
"""
now_ms = int(datetime.now(tz=TZ).timestamp() * 1000)
expire_ms = CACHE_EXPIRE_DAYS * 24 * 60 * 60 * 1000
# 1. 清理会议缓存
if os.path.isdir(CACHE_DIR):
for filename in os.listdir(CACHE_DIR):
if not filename.endswith("_live.json"):
continue
filepath = os.path.join(CACHE_DIR, filename)
try:
with open(filepath, "r", encoding="utf-8") as f:
cache = json.load(f)
if cache.get("status") != "completed":
continue
last_sync = cache.get("lastSyncAt", 0)
if last_sync and (now_ms - last_sync) > expire_ms:
chat_id = filename.replace("_live.json", "")
for suffix in ["_live.json", "_final.json", "_live.json.bak", "_final.json.bak"]:
p = os.path.join(CACHE_DIR, chat_id + suffix)
if os.path.exists(p):
os.remove(p)
except Exception:
pass
# 2. 清理 shared 目录下的过期日期子目录
shared_dir = os.path.join(CACHE_DIR, "shared")
if not os.path.isdir(shared_dir):
return
for meeting_dir_name in os.listdir(shared_dir):
meeting_dir = os.path.join(shared_dir, meeting_dir_name)
if not os.path.isdir(meeting_dir):
continue
for date_dir_name in list(os.listdir(meeting_dir)):
date_dir = os.path.join(meeting_dir, date_dir_name)
if not os.path.isdir(date_dir):
continue
try:
# date_dir_name 格式: 20260329
dir_date = datetime.strptime(date_dir_name, "%Y%m%d").replace(tzinfo=TZ)
dir_ms = int(dir_date.timestamp() * 1000)
if (now_ms - dir_ms) > expire_ms:
import shutil
shutil.rmtree(date_dir)
except Exception:
pass
def main():
# 解析参数
args = [a for a in sys.argv[1:] if not a.startswith("--")]
name = None
if "--name" in sys.argv:
idx = sys.argv.index("--name")
if idx + 1 < len(sys.argv):
name = sys.argv[idx + 1]
if not args:
print(json.dumps({"error": "用法: get-transcript.py <meetingChatId> [--name '会议名称']"}, ensure_ascii=False))
sys.exit(1)
chat_id = args[0]
now_ms = int(datetime.now(tz=TZ).timestamp() * 1000)
# ========== 懒清理:每次运行时清理过期缓存 ==========
cleanup_expired_cache()
# ========== 优先级 1:_final 缓存(二次改写,最优) ==========
final_cache = load_cache(chat_id, "_final")
if final_cache and final_cache.get("fragments"):
final_cache["source"] = "cache_final"
print(json.dumps(final_cache, ensure_ascii=False))
return
# ========== 优先级 2:_live 缓存(实时数据) ==========
live_cache = load_cache(chat_id, "_live")
# 如果 _live 标记为已完成,尝试拉二次改写
if live_cache and live_cache.get("status") == "completed":
stt_result = try_fetch_second_stt(chat_id)
if stt_result["state"] == 2:
# 二次改写成功 → 写 _final
final_data = {
"meetingChatId": chat_id,
"name": name or live_cache.get("name", ""),
"status": "completed",
"source": "second_stt",
"lastSyncAt": now_ms,
"fragments": stt_result["fragments"],
"fullText": stt_result["fullText"],
"lastStartTime": max(f["startTime"] for f in stt_result["fragments"]) if stt_result["fragments"] else 0,
}
save_cache(chat_id, final_data, "_final")
print(json.dumps(final_data, ensure_ascii=False))
return
else:
# 二次改写未就绪,返回 _live 缓存
live_cache["source"] = "cache_live"
if stt_result["source"] == "second_stt_pending":
live_cache["note"] = "二次改写处理中,当前返回实时数据"
print(json.dumps(live_cache, ensure_ascii=False))
return
# ========== 优先级 3:实时拉取 ==========
if live_cache and live_cache.get("lastStartTime") is not None:
# 有 _live 缓存 → 增量拉取(splitRecordListV2)
result = call_api(API_INCR, {
"meetingChatId": chat_id,
"lastStartTime": live_cache["lastStartTime"]
})
source = "incremental"
new_fragments = parse_live_fragments(result.get("data", []))
all_fragments = merge_fragments(live_cache.get("fragments", []), new_fragments)
else:
# 无缓存 → 全量拉取(splitRecordList)
result = call_api(API_FULL, {"meetingChatId": chat_id})
source = "full"
all_fragments = parse_live_fragments(result.get("data", []))
full_text = build_full_text(all_fragments)
last_start_time = max(f["startTime"] for f in all_fragments) if all_fragments else 0
# 安全检查:增量合并后 fragment 数不能比旧缓存少
warning = None
if source == "incremental" and live_cache:
old_count = len(live_cache.get("fragments", []))
new_count = len(all_fragments)
if new_count < old_count:
all_fragments = live_cache.get("fragments", [])
full_text = build_full_text(all_fragments)
last_start_time = live_cache.get("lastStartTime", last_start_time)
warning = f"增量合并后分片数({new_count})少于旧缓存({old_count}),已保留旧数据"
# 写 _live 缓存
cache_name = name or (live_cache.get("name", "") if live_cache else "") or ""
live_data = {
"meetingChatId": chat_id,
"name": cache_name,
"status": "ongoing",
"source": source,
"lastSyncAt": now_ms,
"lastStartTime": last_start_time,
"fullText": full_text,
"fragments": all_fragments,
}
if warning:
live_data["warning"] = warning
backup_cache(chat_id, "_live")
save_cache(chat_id, live_data, "_live")
print(json.dumps(live_data, ensure_ascii=False))
if __name__ == "__main__":
main()
FILE:scripts/huiji/list-by-meeting-number.py
#!/usr/bin/env python3
"""
huiji / list-by-meeting-number 脚本
用途:按视频会议号查询慧记列表(会议维度,含他人录制但本人参会的记录)
使用方式:
python3 scripts/huiji/list-by-meeting-number.py [--human] [--body '<json>'] <meeting_number>
# 基本用法
python3 list-by-meeting-number.py 103760
# 人类可读格式
python3 list-by-meeting-number.py --human 103760
# 带增量时间戳
python3 list-by-meeting-number.py --body '{"meetingNumber":"MTG-001","lastTs":1716345600000}'
环境变量:
XG_BIZ_API_KEY — appKey(必须)
XG_USER_TOKEN — access-token(可选,增强鉴权)
"""
import sys
import os
import json
import time
from datetime import datetime, timezone, timedelta
import requests
import warnings
# 禁用 InsecureRequestWarning (因为 verify=False)
warnings.filterwarnings("ignore", category=requests.packages.urllib3.exceptions.InsecureRequestWarning)
def setup_utf8_stdio():
"""Best-effort UTF-8 stdio for Windows console display."""
for stream_name in ("stdout", "stderr"):
stream = getattr(sys, stream_name, None)
if stream and hasattr(stream, "reconfigure"):
try:
stream.reconfigure(encoding="utf-8", errors="replace")
except Exception:
pass
setup_utf8_stdio()
API_URL = "https://sg-al-ai-voice-assistant.mediportal.com.cn/api/open-api/ai-huiji/meetingChat/listHuiJiIdsByMeetingNumberV2"
MAX_RETRIES = 3
RETRY_DELAY = 1
TZ = timezone(timedelta(hours=8)) # Asia/Shanghai
def ts_to_str(ts_ms):
"""毫秒时间戳 → 人类可读时间"""
if ts_ms is None:
return "-"
try:
dt = datetime.fromtimestamp(ts_ms / 1000, tz=TZ)
return dt.strftime("%Y-%m-%d %H:%M:%S")
except (TypeError, ValueError, OSError):
return str(ts_ms)
def duration_to_str(ms):
"""毫秒时长 → 人类可读"""
if ms is None:
return "-"
try:
total_sec = int(ms / 1000)
h, remainder = divmod(total_sec, 3600)
m, s = divmod(remainder, 60)
if h > 0:
return f"{h}小时{m}分钟"
elif m > 0:
return f"{m}分钟{s}秒"
else:
return f"{s}秒"
except (TypeError, ValueError):
return "-"
def build_headers() -> dict:
headers = {"Content-Type": "application/json"}
token = os.environ.get("XG_USER_TOKEN")
app_key = os.environ.get("XG_BIZ_API_KEY") or os.environ.get("XG_APP_KEY")
if token:
headers["access-token"] = token
if app_key:
headers["appKey"] = app_key
if not token and not app_key:
print("错误: 请至少设置 XG_USER_TOKEN 或 XG_BIZ_API_KEY", file=sys.stderr)
sys.exit(1)
return headers
def call_api(body: dict) -> dict:
headers = build_headers()
last_err = None
for attempt in range(MAX_RETRIES):
try:
response = requests.post(
API_URL,
json=body,
headers=headers,
verify=False,
allow_redirects=True,
timeout=60,
)
response.raise_for_status()
return json.loads(response.content.decode("utf-8"))
except Exception as e:
last_err = e
if attempt < MAX_RETRIES - 1:
time.sleep(RETRY_DELAY)
print(f"错误: 请求失败(重试{MAX_RETRIES}次): {last_err}", file=sys.stderr)
sys.exit(1)
def format_human(result: dict) -> str:
"""将结果转换为人类可读格式"""
if result.get("resultCode") != 1:
return f"❌ 请求失败: {result.get('resultMsg', '未知错误')}"
data = result.get("data", [])
if not data:
return "未找到相关记录"
lines = [f"共 {len(data)} 条记录:\n"]
# recordState 映射
RECORD_STATE = {0: ("🟢", "进行中"), 1: ("⏳", "处理中"), 2: ("✅", "已完成"), 3: ("❌", "失败")}
for i, item in enumerate(data, 1):
chat_id = item.get("_id", "")
name = item.get("name", "(无名称)")
record_state = item.get("recordState", -1)
create = ts_to_str(item.get("createTime"))
finish = ts_to_str(item.get("finishTime"))
length = duration_to_str(item.get("meetingLength"))
icon, state_label = RECORD_STATE.get(record_state, ("❓", "未知"))
lines.append(f"{i}. {icon} **{name}** ({state_label})")
lines.append(f" 开始: {create} | 结束: {finish} | 时长: {length}")
lines.append(f" ID: {chat_id}")
lines.append("")
return "\n".join(lines)
def default_last_ts() -> int:
"""默认 lastTs:最近 10 天前的毫秒时间戳"""
return int((datetime.now(tz=TZ) - timedelta(days=10)).timestamp() * 1000)
def main():
human = "--human" in sys.argv
args = [a for a in sys.argv[1:] if a != "--human"]
if len(args) >= 2 and args[0] == "--body":
if len(args) < 2:
print("用法: list-by-meeting-number.py [--human] --body '<json>'", file=sys.stderr)
sys.exit(1)
body = json.loads(args[1])
elif len(args) >= 1:
raw = args[0].strip()
# 会议号:纯数字字符串转整数,避免后端解析为 null
if raw.isdigit():
body = {"meetingNumber": int(raw)}
else:
body = {"meetingNumber": raw}
else:
print("用法: list-by-meeting-number.py [--human] [--body '<json>'] <meeting_number>", file=sys.stderr)
print(" --human: 时间戳自动转换为可读格式", file=sys.stderr)
sys.exit(1)
# 自动补 lastTs(默认最近 10 天)
if "lastTs" not in body:
body["lastTs"] = default_last_ts()
result = call_api(body)
if human:
print(format_human(result))
else:
print(json.dumps(result, ensure_ascii=False))
if __name__ == "__main__":
main()
FILE:scripts/huiji/README.md
# AI 慧记 — 脚本清单
## 使用前准备
所有脚本只需 `appKey` 鉴权:
```bash
export XG_BIZ_API_KEY="<your-appKey>"
```
可选增强鉴权(非必须):
```bash
export XG_USER_TOKEN="<your-access-token>"
```
## 脚本列表
| 脚本 | 用途 | 基本用法 |
|---|---|---|
| `chat-list-by-page.py` | 分页查询我的慧记列表 | `chat-list-by-page.py [--human] [pageNum pageSize]` |
| `list-by-meeting-number.py` | 按视频会议号查询慧记 | `list-by-meeting-number.py [--human] <meetingNumber>` |
| `split-record-list.py` | 全量查询分片转写原文 | `split-record-list.py <meetingChatId>` |
| `split-record-list-v2.py` | 增量查询分片转写原文 | `split-record-list-v2.py <meetingChatId> [lastStartTime]` |
| `check-second-stt-v2.py` | 查询改写原文状态 | `check-second-stt-v2.py <meetingChatId>` |
| `get-chat-from-share-id.py` | 按 shareId 获取分享原文 | `get-chat-from-share-id.py <shareId>` |
## 通用参数
**`--human`**:列表查询脚本支持此参数,自动将时间戳转换为可读格式(`2026-03-29 13:59:10`),会议状态加图标(🟢进行中 / ✅已完成),减少 AI 手动转换时间戳出错的风险。
**`--body '<json>'`**:所有脚本均支持传入完整请求体。
所有脚本均支持 `--body '<json>'` 方式传入完整请求体,适合需要使用高级参数的场景:
```bash
# 示例:带筛选条件的列表查询
python3 chat-list-by-page.py --body '{"pageNum":0,"pageSize":10,"chatType":7,"nameBlur":"周会"}'
# 示例:带增量时间戳的会议号查询
python3 list-by-meeting-number.py --body '{"meetingNumber":"MTG-001","lastTs":1716345600000}'
```
## 使用示例
```bash
# 查询我的慧记列表(第0页,每页10条)
python3 chat-list-by-page.py 0 10
# 📌 推荐:人类可读格式(时间戳自动转换)
python3 chat-list-by-page.py --human 0 10
# 搜索名称包含"周会"的慧记
python3 chat-list-by-page.py --body '{"pageNum":0,"pageSize":10,"nameBlur":"周会"}'
# 按会议号查询
python3 list-by-meeting-number.py MTG-20260327-001
# 全量获取转写原文
python3 split-record-list.py abc123
# 增量获取转写原文(只拉新分片)
python3 split-record-list-v2.py abc123 120034
# 检查改写原文状态
python3 check-second-stt-v2.py abc123
# 通过 shareId 获取分享原文(推荐)
python3 get-chat-from-share-id.py f12505e3-3ecb-47f1-87e4-277b2b1a243e
python3 get-chat-from-share-id.py --body '{"shareId":"f12505e3-3ecb-47f1-87e4-277b2b1a243e"}'
```
FILE:scripts/huiji/split-record-list-v2.py
#!/usr/bin/env python3
"""
huiji / split-record-list-v2 脚本
用途:增量查询指定慧记的分片转写原文(传入 lastStartTime 仅返回更新的分片)
使用方式:
python3 scripts/huiji/split-record-list-v2.py [--body '<json>'] <meetingChatId> [lastStartTime]
# 基本用法(全量,等同于 splitRecordList)
python3 split-record-list-v2.py abc123
# 增量(只拉 startTime > 120034 的分片)
python3 split-record-list-v2.py abc123 120034
# JSON body
python3 split-record-list-v2.py --body '{"meetingChatId":"abc123","lastStartTime":120034}'
环境变量:
XG_BIZ_API_KEY — appKey(必须)
XG_USER_TOKEN — access-token(可选,增强鉴权)
"""
import sys
import os
import json
import time
import requests
import warnings
# 禁用 InsecureRequestWarning (因为 verify=False)
warnings.filterwarnings("ignore", category=requests.packages.urllib3.exceptions.InsecureRequestWarning)
def setup_utf8_stdio():
"""Best-effort UTF-8 stdio for Windows console display."""
for stream_name in ("stdout", "stderr"):
stream = getattr(sys, stream_name, None)
if stream and hasattr(stream, "reconfigure"):
try:
stream.reconfigure(encoding="utf-8", errors="replace")
except Exception:
pass
setup_utf8_stdio()
API_URL = "https://sg-al-ai-voice-assistant.mediportal.com.cn/api/open-api/ai-huiji/meetingChat/splitRecordListV2"
MAX_RETRIES = 3
RETRY_DELAY = 1
def build_headers() -> dict:
headers = {"Content-Type": "application/json"}
token = os.environ.get("XG_USER_TOKEN")
app_key = os.environ.get("XG_BIZ_API_KEY") or os.environ.get("XG_APP_KEY")
if token:
headers["access-token"] = token
if app_key:
headers["appKey"] = app_key
if not token and not app_key:
print("错误: 请至少设置 XG_USER_TOKEN 或 XG_BIZ_API_KEY", file=sys.stderr)
sys.exit(1)
return headers
def call_api(body: dict) -> dict:
headers = build_headers()
last_err = None
for attempt in range(MAX_RETRIES):
try:
response = requests.post(
API_URL,
json=body,
headers=headers,
verify=False,
allow_redirects=True,
timeout=60,
)
response.raise_for_status()
return json.loads(response.content.decode("utf-8"))
except Exception as e:
last_err = e
if attempt < MAX_RETRIES - 1:
time.sleep(RETRY_DELAY)
print(f"错误: 请求失败(重试{MAX_RETRIES}次): {last_err}", file=sys.stderr)
sys.exit(1)
def main():
if len(sys.argv) >= 2 and sys.argv[1] == "--body":
if len(sys.argv) < 3:
print("用法: split-record-list-v2.py --body '<json>'", file=sys.stderr)
sys.exit(1)
body = json.loads(sys.argv[2])
elif len(sys.argv) >= 3:
body = {"meetingChatId": sys.argv[1], "lastStartTime": int(sys.argv[2])}
elif len(sys.argv) >= 2:
body = {"meetingChatId": sys.argv[1]}
else:
print("用法: split-record-list-v2.py [--body '<json>'] <meetingChatId> [lastStartTime]", file=sys.stderr)
sys.exit(1)
result = call_api(body)
print(json.dumps(result, ensure_ascii=False))
if __name__ == "__main__":
main()
FILE:scripts/huiji/split-record-list.py
#!/usr/bin/env python3
"""
huiji / split-record-list 脚本
用途:全量查询指定慧记的分片转写原文
使用方式:
python3 scripts/huiji/split-record-list.py [--body '<json>'] <meetingChatId>
# 基本用法
python3 split-record-list.py abc123
# JSON body
python3 split-record-list.py --body '{"meetingChatId":"abc123"}'
环境变量:
XG_BIZ_API_KEY — appKey(必须)
XG_USER_TOKEN — access-token(可选,增强鉴权)
"""
import sys
import os
import json
import time
import requests
import warnings
# 禁用 InsecureRequestWarning (因为 verify=False)
warnings.filterwarnings("ignore", category=requests.packages.urllib3.exceptions.InsecureRequestWarning)
def setup_utf8_stdio():
"""Best-effort UTF-8 stdio for Windows console display."""
for stream_name in ("stdout", "stderr"):
stream = getattr(sys, stream_name, None)
if stream and hasattr(stream, "reconfigure"):
try:
stream.reconfigure(encoding="utf-8", errors="replace")
except Exception:
pass
setup_utf8_stdio()
API_URL = "https://sg-al-ai-voice-assistant.mediportal.com.cn/api/open-api/ai-huiji/meetingChat/splitRecordList"
MAX_RETRIES = 3
RETRY_DELAY = 1
def build_headers() -> dict:
headers = {"Content-Type": "application/json"}
token = os.environ.get("XG_USER_TOKEN")
app_key = os.environ.get("XG_BIZ_API_KEY") or os.environ.get("XG_APP_KEY")
if token:
headers["access-token"] = token
if app_key:
headers["appKey"] = app_key
if not token and not app_key:
print("错误: 请至少设置 XG_USER_TOKEN 或 XG_BIZ_API_KEY", file=sys.stderr)
sys.exit(1)
return headers
def call_api(body: dict) -> dict:
headers = build_headers()
last_err = None
for attempt in range(MAX_RETRIES):
try:
response = requests.post(
API_URL,
json=body,
headers=headers,
verify=False,
allow_redirects=True,
timeout=60,
)
response.raise_for_status()
return json.loads(response.content.decode("utf-8"))
except Exception as e:
last_err = e
if attempt < MAX_RETRIES - 1:
time.sleep(RETRY_DELAY)
print(f"错误: 请求失败(重试{MAX_RETRIES}次): {last_err}", file=sys.stderr)
sys.exit(1)
def main():
if len(sys.argv) >= 2 and sys.argv[1] == "--body":
if len(sys.argv) < 3:
print("用法: split-record-list.py --body '<json>'", file=sys.stderr)
sys.exit(1)
body = json.loads(sys.argv[2])
elif len(sys.argv) >= 2:
body = {"meetingChatId": sys.argv[1]}
else:
print("用法: split-record-list.py [--body '<json>'] <meetingChatId>", file=sys.stderr)
sys.exit(1)
result = call_api(body)
print(json.dumps(result, ensure_ascii=False))
if __name__ == "__main__":
main()
FILE:openapi/huiji/api-index.md
# AI 慧记 — 接口索引
**生产域名**: `https://sg-al-ai-voice-assistant.mediportal.com.cn/api/open-api`
**鉴权**:所有接口只需 `appKey`(Header),无需 access-token。
| # | 接口 | 方法 | 路径 | 文档 | 脚本 |
|---|---|---|---|---|---|
| 1 | chatListByPage | POST | /ai-huiji/meetingChat/chatListByPage | [chat-list-by-page.md](chat-list-by-page.md) | [chat-list-by-page.py](../../scripts/huiji/chat-list-by-page.py) |
| 2 | listHuiJiIdsByMeetingNumberV2 | POST | /ai-huiji/meetingChat/listHuiJiIdsByMeetingNumberV2 | [list-by-meeting-number.md](list-by-meeting-number.md) | [list-by-meeting-number.py](../../scripts/huiji/list-by-meeting-number.py) |
| 3 | splitRecordList | POST | /ai-huiji/meetingChat/splitRecordList | [split-record-list.md](split-record-list.md) | [split-record-list.py](../../scripts/huiji/split-record-list.py) |
| 4 | splitRecordListV2 | POST | /ai-huiji/meetingChat/splitRecordListV2 | [split-record-list-v2.md](split-record-list-v2.md) | [split-record-list-v2.py](../../scripts/huiji/split-record-list-v2.py) |
| 5 | checkSecondSttV2 | POST | /ai-huiji/meetingChat/checkSecondSttV2 | [check-second-stt-v2.md](check-second-stt-v2.md) | [check-second-stt-v2.py](../../scripts/huiji/check-second-stt-v2.py) |
| 6 | getChatFromShareId | POST | /ai-huiji/meetingChat/getChatFromShareId | [get-chat-from-share-id.md](get-chat-from-share-id.md) | [get-chat-from-share-id.py](../../scripts/huiji/get-chat-from-share-id.py) |
FILE:openapi/huiji/chat-list-by-page.md
# POST /ai-huiji/meetingChat/chatListByPage
## 作用
分页查询归属当前用户名下的慧记列表(「我的」慧记),支持筛选和搜索。通常作为获取 meetingChatId 的入口接口。
## 鉴权
只需 `appKey`,无需 access-token。
## 请求参数
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `pageNum` | Integer | 是 | 页码,从 0 开始 |
| `pageSize` | Integer | 是 | 每页数量 |
| `chatType` | Integer | 否 | 类型筛选:0=玄关会议, 1=上传音频, 2=提交文字, 3=上传文本文件, 4=文件列表, 5=录音, 7=AI慧记, 8=上传音频V2 |
| `chatTypeList` | List\<Integer\> | 否 | 类型列表,传入后忽略 chatType |
| `nameBlur` | String | 否 | 名称模糊搜索 |
| `sortKey` | String | 否 | 排序字段,默认 updateTime |
| `clean` | Boolean | 否 | 是否精简返回,默认 false |
| `sttOkOnly` | Boolean | 否 | 只返回转写成功的记录 |
| `minTs` | Long | 否 | 最小时间戳(毫秒) |
| `maxTs` | Long | 否 | 最大时间戳(毫秒) |
## 响应参数
`data` 类型为 `MeetingChatPageVO`:
| 字段 | 类型 | 说明 |
|---|---|---|
| `total` | Long | 总记录数 |
| `pageContent` | List\<FindChatVO\> | 分页内容列表 |
`FindChatVO` 关键字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| `_id` | String | 慧记 ID(用作 meetingChatId,注意后缀处理) |
| `originChatId` | String | 源头 ID(_id 有 `__数字` 后缀时用此字段作为 meetingChatId) |
| `name` | String | 会议名称 |
| `chatType` | Integer | 类型(见上方 chatType 说明) |
| `meetingBegin` | Long | 会议开始时间(毫秒时间戳) |
| `meetingLength` | Long | 会议时长(毫秒) |
| `combineState` | Integer | 状态:0=进行中, 1=处理中, 2=已完成, 3=失败 |
| `summaryState` | Integer | 总结状态:0=未开始, 1=进行中, 2=成功, 3=失败 |
| `simpleSummary` | String | 摘要 |
| `createTime` | Long | 创建时间(毫秒时间戳) |
| `updateTime` | Long | 更新时间(毫秒时间戳) |
## ⚠️ _id 后缀处理
`_id` 可能有 `__数字` 后缀(如 `abc123__45678`),不能直接用作 `meetingChatId`:
- **处理方式一**:截取双下划线前的部分 → `abc123`
- **处理方式二**:使用 `originChatId` 字段
## 请求示例
```bash
# 基本分页(第0页,每页10条)
python3 scripts/huiji/chat-list-by-page.py 0 10
# 筛选 AI 慧记类型 + 名称搜索
python3 scripts/huiji/chat-list-by-page.py --body '{"pageNum":0,"pageSize":10,"chatType":7,"nameBlur":"周会"}'
# 按时间范围查询
python3 scripts/huiji/chat-list-by-page.py --body '{"pageNum":0,"pageSize":10,"minTs":1716345600000,"maxTs":1716432000000}'
```
## 脚本映射
- `../../scripts/huiji/chat-list-by-page.py`
FILE:openapi/huiji/check-second-stt-v2.md
# POST /ai-huiji/meetingChat/checkSecondSttV2
## 作用
查询已结束会议的改写原文处理状态。会议结束后服务端会对会中转写做大模型二次改写,成功时内容通常比分片原文更准确,且可能包含发言人等维度。
**仅用于已结束会议**。进行中会议不要调用此接口。
## 鉴权
只需 `appKey`,无需 access-token。
## 请求参数
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `meetingChatId` | String | 是 | 慧记 ID |
## 响应参数
| 字段 | 类型 | 说明 |
|---|---|---|
| `totalProgress` | Integer | 总进度(0-100) |
| `state` | Integer | 状态:1=处理中, 2=成功, 3=失败 |
| `sttPartList` | List | 改写转写分片列表(成功且有内容时作为原文首选来源) |
| `errMsg` | String | 失败时的错误信息 |
## 状态处理策略
| state | 含义 | 处理方式 |
|---|---|---|
| `2` | 成功 | 优先使用 `sttPartList` 作为原文(最优) |
| `1` | 处理中 | 提示改写中,用缓存或 splitRecordList 分片兜底 |
| `3` | 失败 | fallback 到 splitRecordList 全量兜底 |
## ⚠️ 容错规则
- **刚结束的会议**:改写可能尚未开始,checkSecondSttV2 可能返回空或 state=1。此时 **splitRecordList 是唯一来源**,不可因 checkSecondSttV2 无数据而报错
- **sttPartList 为空**:即使 state=2,若 `sttPartList` 为空或无法解析,仍 fallback 到 splitRecordList
- **永远不向用户报错**:checkSecondSttV2 失败时静默切换到 splitRecordList
## 请求示例
```bash
python3 scripts/huiji/check-second-stt-v2.py abc123
python3 scripts/huiji/check-second-stt-v2.py --body '{"meetingChatId":"abc123"}'
```
## 脚本映射
- `../../scripts/huiji/check-second-stt-v2.py`
FILE:openapi/huiji/get-chat-from-share-id.md
# POST /ai-huiji/meetingChat/getChatFromShareId
## 作用
通过分享链接中的 `shareId` 获取一条 AI 慧记聊天记录详情,包含可用于纪要分析的原文内容(`srcText`)。
## 鉴权
只需 `appKey`,无需 access-token。
## 请求参数
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `shareId` | String | 是 | 分享链接中的 ID(UUID) |
## 响应说明
接口返回标准结构:`resultCode` / `resultMsg` / `data`。
常见 `data` 字段(以实际返回为准):
| 字段 | 说明 |
|---|---|
| `_id` | 慧记记录 ID(可能包含后缀) |
| `name` | 会议标题 |
| `createTime` | 创建时间(毫秒时间戳) |
| `updateTime` | 更新时间(毫秒时间戳) |
| `meetingLength` | 会议时长(毫秒) |
| `simpleSummary` | 简要摘要 |
| `srcText` | 转写原文(可直接用于 AI 纪要) |
| `srcUser` | 分享者/来源用户信息 |
## 使用说明
- 当用户消息里出现分享链接时,先提取 `shareId` 再调用本接口。
- 支持三种链接形态(域名可变):
- 短链:`http://s.medihub.cn/p/xxxxx`(需先跟随跳转)
- 长链:`https://<任意域名>/#/shareLinkPage?shareId=...`
- 新版长链:`https://<任意域名>/#/meetingChatNew?shareId=...`
- 可携带附加参数(如 `userName`、`personId`),解析时仅使用 `shareId`,其它参数忽略。
- 短链解析建议在 Skill 层执行:先用 `web_fetch` 跟随跳转拿到 `finalUrl`,再提取 `shareId` 传给脚本。
- 返回后优先使用 `srcText` 做总结、待办提取、专题分析。
## 请求示例
```bash
python3 scripts/huiji/get-chat-from-share-id.py f12505e3-3ecb-47f1-87e4-277b2b1a243e
python3 scripts/huiji/get-chat-from-share-id.py --body '{"shareId":"f12505e3-3ecb-47f1-87e4-277b2b1a243e"}'
```
## 脚本映射
- `../../scripts/huiji/get-chat-from-share-id.py`
FILE:openapi/huiji/list-by-meeting-number.md
# POST /ai-huiji/meetingChat/listHuiJiIdsByMeetingNumberV2
## 作用
按视频会议号查询当前用户在该场会议参与关系下可访问的慧记列表。即使会议由他人录制、慧记归属不在当前用户名下,只要当前用户参与了该会议,仍可通过此接口查到。
**与 chatListByPage 的差异**:chatListByPage 查的是「归属在当前用户名下」的慧记;本接口查的是「我参加了的那场视频会议」下的慧记。二者不可替代。
## 鉴权
只需 `appKey`,无需 access-token。
## 请求参数
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `meetingNumber` | String | 是 | 视频会议号(与会议域一致) |
| `lastTs` | Long | 否 | 增量时间戳(毫秒)。未传或为 0:拉取最近一个月;>0:增量拉取时间戳大于 lastTs 的数据 |
## 响应参数
`data` 类型为 `List<FindChatVO>`:
| 字段 | 类型 | 说明 |
|---|---|---|
| `_id` | String | 慧记 ID(即 meetingChatId,注意 `__` 后缀处理规则) |
| `name` | String | 慧记名称 |
| `recordState` | Integer | 录音状态(见下方状态说明) |
| `createTime` | Long | 创建时间(毫秒时间戳) |
| `finishTime` | Long | 结束时间(毫秒时间戳),进行中时为 null |
| `meetingLength` | Long | 会议时长(毫秒) |
| `personId` | String | 关联人员 ID,可能为 null |
## 会议状态判断(recordState)
| `recordState` | 状态 | 后续操作 |
|---|---|---|
| `0` | 进行中 | 用 get-transcript.py 获取实时转写 |
| `1` | 处理中 | 稍后重试 |
| `2` | 已完成 | 用 get-transcript.py 获取改写原文 |
| `3` | 失败 | 提示用户 |
> 注意:`recordState` 与 chatListByPage 返回的 `combineState` 含义不同,不可混用。
## 请求示例
```bash
# 基本用法
python3 scripts/huiji/list-by-meeting-number.py 20260327
# 增量拉取
python3 scripts/huiji/list-by-meeting-number.py --body '{"meetingNumber":"20260327","lastTs":1716345600000}'
```
## 脚本映射
- `../../scripts/huiji/list-by-meeting-number.py`
FILE:openapi/huiji/split-record-list-v2.md
# POST /ai-huiji/meetingChat/splitRecordListV2
## 作用
增量查询指定慧记的分片转写原文。在 splitRecordList 的基础上增加 `lastStartTime` 参数,传入时仅返回 `startTime` 大于该值的分片,用于减少重复传输。
## 鉴权
只需 `appKey`,无需 access-token。
## 请求参数
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `meetingChatId` | String | 是 | 慧记 ID |
| `lastStartTime` | Long | 否 | 上次已同步的最大 startTime(毫秒偏移)。不传:返回全量;传:仅返回更新的分片 |
## 响应参数
`data` 类型为 `List<SplitRecordVO>`,字段同 splitRecordList:
| 字段 | 类型 | 说明 |
|---|---|---|
| `startTime` | Long | 开始时间(录音经过的毫秒数) |
| `text` | String | 转写文本 |
| `realTime` | Long | 现实时间戳(毫秒) |
## 使用说明
- **增量模式**:传入 `lastStartTime`(取上次缓存中最大分片的 startTime),仅返回新分片
- **全量模式**:不传 `lastStartTime`,行为与 splitRecordList 一致
- `startTime` 为 null 的分片在增量模式下会被过滤掉
- 适用于进行中会议的轮询拉取,配合本地缓存使用
## 请求示例
```bash
# 全量(等同于 splitRecordList)
python3 scripts/huiji/split-record-list-v2.py abc123
# 增量(只拉 startTime > 120034 的新分片)
python3 scripts/huiji/split-record-list-v2.py abc123 120034
# JSON body
python3 scripts/huiji/split-record-list-v2.py --body '{"meetingChatId":"abc123","lastStartTime":120034}'
```
## 脚本映射
- `../../scripts/huiji/split-record-list-v2.py`
FILE:openapi/huiji/split-record-list.md
# POST /ai-huiji/meetingChat/splitRecordList
## 作用
全量查询指定慧记的分片转写原文。每个分片包含该时段的转写文本,按 startTime 排序拼接即为连续的完整原文。
## 鉴权
只需 `appKey`,无需 access-token。
## 请求参数
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `meetingChatId` | String | 是 | 慧记 ID(来自 chatListByPage 的 `_id` 或 listHuiJiIdsByMeetingNumberV2 的 `_id`) |
## 响应参数
`data` 类型为 `List<SplitRecordVO>`:
| 字段 | 类型 | 说明 |
|---|---|---|
| `startTime` | Long | 开始时间(录音经过的毫秒数,用于排序和增量游标) |
| `text` | String | 转写文本 |
| `realTime` | Long | 现实时间戳(毫秒) |
## 使用说明
- 返回的分片按 `startTime` 排序后拼接 `text`,即为完整的转写原文
- `startTime` 是相对录音起点的毫秒偏移,用于 splitRecordListV2 增量查询的游标
- 适用于进行中会议的实时转写和已结束会议的兜底原文
## 请求示例
```bash
python3 scripts/huiji/split-record-list.py abc123
python3 scripts/huiji/split-record-list.py --body '{"meetingChatId":"abc123"}'
```
## 脚本映射
- `../../scripts/huiji/split-record-list.py`
FILE:examples/huiji/README.md
# AI 慧记 — 使用场景与触发条件
## 场景一:查询会议列表
- **触发**:"帮我查一下最近的慧记"、"我的会议记录列表"、"最近有什么会议"
- **接口**:chatListByPage
- **语义分流**:用户未提供会议号 → 走归属维度列表
- **示例**:
```bash
python3 scripts/huiji/chat-list-by-page.py 0 10
```
## 场景二:按会议号查询
- **触发**:"会议号 xxx 的纪要"、"这场会议的慧记"(用户提供了会议号)
- **接口**:listHuiJiIdsByMeetingNumberV2
- **语义分流**:用户提供了会议号 → 走会议维度查询
- **示例**:
```bash
python3 scripts/huiji/list-by-meeting-number.py MTG-20260327-001
```
## 场景三:获取会议原文
- **触发**:"这个会议说了什么"、"会议内容"、"转写原文"
- **策略**:统一通过 `get-transcript.py` 入口,脚本自动处理:
- **进行中**:有缓存则 splitRecordListV2 增量拉取,无缓存则 splitRecordList 全量
- **已结束**:先 checkSecondSttV2 改写原文,失败则 splitRecordList 兜底
- **示例**:
```bash
python3 scripts/huiji/get-transcript.py <meetingChatId>
```
## 场景四:AI 分析(基于原文)
拿到原文后,AI 根据用户意图进行分析:
### 📝 会议总结
- **触发**:"总结一下"、"会议纪要"、"概要"
- **能力**:生成结构化纪要(主题、要点、决策、遗留问题)
### ✅ 待办提取
- **触发**:"有什么待办"、"谁负责什么"、"跟进事项"
- **能力**:从原文中提取待办事项、责任人、截止时间
### 🔍 专题分析
- **触发**:"关于 xxx 讨论了什么"、"财务那块说了什么"
- **能力**:按主题定位相关讨论内容并提取
## 场景五:搜索会议
- **触发**:"帮我找 xxx 的会议"、"搜索 xxx"
- **接口**:chatListByPage + nameBlur
- **示例**:
```bash
python3 scripts/huiji/chat-list-by-page.py --body '{"pageNum":0,"pageSize":10,"nameBlur":"周会"}'
```
## 场景六:解析分享链接并获取原文
- **触发**:"帮我看下这个慧记分享链接"、"这个分享里说了什么"
- **接口**:getChatFromShareId
- **说明**:短链/长链/shareId 统一传给脚本,由脚本内部自动解析
- **示例**:
```bash
python3 scripts/huiji/get-chat-from-share-id.py f12505e3-3ecb-47f1-87e4-277b2b1a243e
python3 scripts/huiji/get-chat-from-share-id.py --body '{"shareId":"f12505e3-3ecb-47f1-87e4-277b2b1a243e"}'
```
CWork 工作协同原子能力集,覆盖员工搜索、文件上传下载、发送/回复汇报、收发件箱、汇报详情、任务、待办、事项、插件聚合、新消息、已读状态与 AI 问答;适用于“汇报、待办、任务、附件、消息、已读、员工查询”等场景;仅支持 appKey 鉴权并按需加载接口执行组合调用。
---
name: cms-cwork-skils
description: CWork 工作协同原子能力集,覆盖员工搜索、文件上传下载、发送/回复汇报、收发件箱、汇报详情、任务、待办、事项、插件聚合、新消息、已读状态与 AI 问答;适用于“汇报、待办、任务、附件、消息、已读、员工查询”等场景;仅支持 appKey 鉴权并按需加载接口执行组合调用。
skillcode: cms-cwork-skils
dependencies:
- cms-auth-skills
---
# cms-cwork-skils — 索引
本技能包基于 XGJK v1.05 协议构建,围绕《工作协同业务说明.md》《工作协同API说明.md》《基础服务业务说明.md》《基础服务-API说明.md》整理当前已覆盖的 CWork 常用协同能力,通过"一接口一脚本"的模式提供自动化办公支持。
**当前版本**: v1.3
**接口版本**: 所有业务接口统一使用 `/open-api/*` 前缀,自带 `appKey` 鉴权。
## 触发场景(用于 AI 路由)
- **高频意图**: 发汇报、回汇报、查收件箱/发件箱、查汇报详情、查待办、完成待办、建任务、查事项、查新消息、标记已读、上传附件、查员工。
- **关键词**: `工作协同` `汇报` `回复` `待办` `任务` `附件` `消息` `已读` `员工搜索` `组织架构` `AI问答`。
- **路由边界**: 仅处理 CWork 协同与基础服务接口;不做登录换 token,鉴权统一依赖 `cms-auth-skills` 提供 `appKey`。
## 能力概览
- **user-search**: 按姓名模糊搜索内部员工
- **employee-service**: 批量员工信息与组织架构信息查询
- **file-service**: 文件上传与下载信息查询
- **report-write**: 发送汇报与回复汇报
- **inbox**: 获取收件箱汇报列表
- **outbox**: 获取发件箱汇报列表
- **report-detail**: 获取单篇汇报的结构化详情
- **tasks**: 工作任务分页与任务简易信息查询
- **todos**: 待办事项管理(列表、创建的反馈、完成状态切换)
- **templates**: 最近处理事项清单与事项批量详情
- **plugin-report**: 插件场景的待办/未读聚合查询
- **report-query**: 汇报待办、汇报未读与已读判断
- **report-message**: 我的新消息与阅读汇报状态变更
- **ai-qa**: 汇报内容 AI SSE 问答
- **plan-create**: 创建高级工作任务
当前覆盖范围说明:
- 已覆盖《工作协同API说明.md》中的 24 个接口点,以及《基础服务-API说明.md》中的 5 个接口点
- 已覆盖员工搜索、批量员工信息、组织架构、文件上传、文件下载信息、发送汇报、汇报回复、收件箱/发件箱、汇报详情、任务分页、任务简易信息、通用待办、创建的反馈待办、事项列表、事项批量详情、插件聚合列表、汇报待办/未读列表、已读判断、新消息、阅读汇报、汇报 AI 问答、高级任务创建
统一规范:
- 认证与鉴权:统一由 `cms-auth-skills` 提供,详见 `cms-auth-skills/SKILL.md`
### 鉴权模式声明(重要)
> **xgjk-cwork 所有业务接口仅支持 `appKey` 鉴权模式,不支持 `access-token`。**
>
> - 对应 `cms-auth-skills` 的 `login.py`,xgjk-cwork 场景**只允许使用 `--resolve-app-key` 参数**(返回 `appKey`)
> - **禁止使用 `--ensure`**(会返回 access-token,xgjk-cwork 接口无法识别)
> - 正确用法:`python3 login.py --resolve-app-key` → 返回可直接用于 `appKey` Header 的值
### 鉴权宪章(铁律)
1. **禁止问用户任何关于 token / 鉴权 / 登录的问题** — 鉴权统一依赖 `cms-auth-skills`
2. **所有鉴权操作必须且只能通过 `cms-auth-skills/scripts/auth/login.py --resolve-app-key` 执行**,严禁在脚本中直接调用底层 HTTP 鉴权接口
3. **对外克制**:对用户只输出结论或摘要,不暴露鉴权细节与内部字段
### 授权依赖
- 当需要 `appKey` 时,优先直接读取 `cms-auth-skills/SKILL.md`
- 如果能读取到,直接按 `cms-auth-skills/SKILL.md` 中的鉴权规则,使用 `login.py --resolve-app-key` 准备 `appKey`
- 如果读取不到(说明未安装),再执行 `npx clawhub@latest install cms-auth-skills --force`
- 如果上面的安装方式不可用,再尝试执行 `npx clawhub@latest install https://github.com/spzwin/cms-auth-skills.git --force`
- 安装完成后,再继续后续操作
## 脚本使用规则
1. **Python 强制性**: 所有脚本必须使用 Python 编写。
2. **独立执行**: 脚本通过环境变量 `XG_BIZ_API_KEY` / `XG_APP_KEY` 运行;若本地尚未准备 `appKey`,需先按 `cms-auth-skills` 规则解析后再执行。
3. **1:1 映射**: 每个接口文档 (`openapi/`) 必须对应一个 Python 脚本 (`scripts/`)。
4. **可探测性**: 所有脚本必须支持在未设置 `appKey` 时执行 `-h/--help`,便于 AI 先探测参数再组合调用。
5. **参数策略**: 组合调用时优先使用显式参数名(如 `--report-id`、`--employee-id`、`--corp-id`),仅在兼容场景下使用位置参数。
## AI 组合调用约束(推荐)
1. **执行顺序**: 先读取 `openapi/<module>/api-index.md` 确认接口,再读取对应 `scripts/<module>/README.md` 确认参数,最后执行脚本。
2. **写操作确认**: 涉及数据变更的接口(如 `report-write/*`、`todos/complete.py`、`report-message/read-report.py`、`plan-create/create-simple.py`)必须先做明确确认再调用。
3. **大结果控制**: 对高体量接口优先使用 `--client-limit` 和 `--output-file`,默认建议只处理前 `200` 条,最大不超过 `500` 条。
4. **分页认知**: `reportInfoOpenQuery/unreadList` 实测可能忽略传入 `pageSize`,不要把脚本裁剪结果误判为平台真实分页结果。
5. **输出契约**: 所有脚本都返回 JSON;优先按 `resultCode / resultMsg / data` 读取,必要时再解析模块特定字段。
## 按需加载原则(重要)
1. **禁止全量预加载**: 不需要、也不应一次性加载全部 `27` 个接口文档与脚本说明。
2. **按任务最小读取**: 仅按当前用户意图读取最小必要集合,优先模块级索引,再下钻单接口文档与单脚本说明。
3. **分阶段扩展**: 当需求变化时再增量读取下一个模块,避免提前加载无关接口。
4. **组合调用路径**: `SKILL.md`(能力定位)→ `openapi/<module>/api-index.md`(接口候选)→ 单接口 `openapi` 文档(请求契约)→ `scripts/<module>/README.md`(参数规范)→ 单脚本执行。
5. **跨模块最小闭环**: 仅在业务链路确实需要时才跨模块组合,例如“附件汇报链路”只需 `file-service` + `report-write`,不应额外加载 `tasks/todos/plugin-report`。
## 能力树
```
cms-cwork-skils/
├── SKILL.md
├── openapi/
│ ├── user-search/
│ │ ├── api-index.md
│ │ └── search-emp.md
│ ├── employee-service/
│ │ ├── api-index.md
│ │ ├── get-by-person-ids.md
│ │ └── get-org-info.md
│ ├── file-service/
│ │ ├── api-index.md
│ │ ├── get-download-info.md
│ │ └── upload-file.md
│ ├── report-write/
│ │ ├── api-index.md
│ │ ├── submit.md
│ │ └── reply.md
│ ├── inbox/
│ │ ├── api-index.md
│ │ └── get-list.md
│ ├── outbox/
│ │ ├── api-index.md
│ │ └── get-list.md
│ ├── report-detail/
│ │ ├── api-index.md
│ │ └── get-info.md
│ ├── tasks/
│ │ ├── api-index.md
│ │ ├── get-page.md
│ │ └── get-simple-plan-and-report-info.md
│ ├── todos/
│ │ ├── api-index.md
│ │ ├── get-list.md
│ │ ├── complete.md
│ │ └── list-created-feedbacks.md
│ ├── templates/
│ │ ├── api-index.md
│ │ ├── get-list.md
│ │ └── list-by-ids.md
│ ├── plugin-report/
│ │ ├── api-index.md
│ │ ├── get-list.md
│ │ ├── get-latest-list.md
│ │ └── get-unread-list.md
│ ├── report-query/
│ │ ├── api-index.md
│ │ ├── get-todo-list.md
│ │ ├── get-unread-list.md
│ │ └── is-report-read.md
│ ├── report-message/
│ │ ├── api-index.md
│ │ ├── find-my-new-msg-list.md
│ │ └── read-report.md
│ ├── ai-qa/
│ │ ├── api-index.md
│ │ └── ask-sse.md
│ └── plan-create/
│ ├── api-index.md
│ └── create-simple.md
├── examples/
│ ├── user-search/README.md # 含 3S1R 管理闭环
│ ├── employee-service/README.md # 含 3S1R 管理闭环
│ ├── file-service/README.md # 含 3S1R 管理闭环
│ ├── report-write/README.md # 含 3S1R 管理闭环(含写操作确认)
│ ├── inbox/README.md # 含 3S1R 管理闭环
│ ├── outbox/README.md # 含 3S1R 管理闭环
│ ├── report-detail/README.md # 含 3S1R 管理闭环
│ ├── tasks/README.md # 含 3S1R 管理闭环
│ ├── todos/README.md # 含 3S1R 管理闭环(含写操作确认)
│ ├── templates/README.md # 含 3S1R 管理闭环
│ ├── plugin-report/README.md # 含 3S1R 管理闭环
│ ├── report-query/README.md # 含 3S1R 管理闭环
│ ├── report-message/README.md # 含 3S1R 管理闭环(含写操作确认)
│ ├── ai-qa/README.md # 含 3S1R 管理闭环(含 SSE 说明)
│ └── plan-create/README.md # 含 3S1R 管理闭环(含写操作确认)
└── scripts/
├── user-search/
│ ├── search-emp.py
│ └── README.md
├── employee-service/
│ ├── get-by-person-ids.py
│ ├── get-org-info.py
│ └── README.md
├── file-service/
│ ├── get-download-info.py
│ ├── upload-file.py
│ └── README.md
├── report-write/
│ ├── submit.py
│ ├── reply.py
│ └── README.md
├── inbox/
│ ├── get-list.py
│ └── README.md
├── outbox/
│ ├── get-list.py
│ └── README.md
├── report-detail/
│ ├── get-info.py
│ └── README.md
├── tasks/
│ ├── get-page.py
│ ├── get-simple-plan-and-report-info.py
│ └── README.md
├── todos/
│ ├── get-list.py
│ ├── complete.py
│ ├── list-created-feedbacks.py
│ └── README.md
├── templates/
│ ├── get-list.py
│ ├── get-by-ids.py
│ └── README.md
├── plugin-report/
│ ├── get-list.py
│ ├── get-latest-list.py
│ ├── get-unread-list.py
│ └── README.md
├── report-query/
│ ├── get-todo-list.py
│ ├── get-unread-list.py
│ ├── is-report-read.py
│ └── README.md
├── report-message/
│ ├── find-my-new-msg-list.py
│ ├── read-report.py
│ └── README.md
├── ai-qa/
│ ├── ask-sse.py
│ └── README.md
└── plan-create/
├── create-simple.py
└── README.md
```
## 模块数量统计
| 分类 | 数量 | 说明 |
|------|------|------|
| 业务模块 | 15 | user-search / employee-service / file-service / report-write / inbox / outbox / report-detail / tasks / todos / templates / plugin-report / report-query / report-message / ai-qa / plan-create |
| API 接口文档 | 27 | 对两份主文档去重后形成的 27 个接口能力文档 |
| Python 脚本 | 27 | 与接口 1:1 映射 |
| 示例指引文档 | 15 | examples/<module>/README.md,含 3S1R 标准化流程 |
FILE:examples/ai-qa/README.md
# ai-qa 示例
## 模块说明
针对指定汇报集合发起 AI SSE 问答,脚本会将 SSE 结果聚合后输出。
## 依赖脚本
`../../scripts/ai-qa/ask-sse.py`
## 对应接口
- `POST /work-report/open-platform/report/aiSseQaV2`
---
## 标准流程(含 3S1R 管理闭环)
### Step 1 — Suggest(建议)
**在发起问答之前,先给出建议方案。**
- 说明该接口是基于指定汇报内容做问答,不是通用知识库检索
- 建议问题尽量清晰、具体,避免开放式模糊查询
- 提示必须提供 `reportIdList`
```
建议:提问时尽量具体,避免过于宽泛的问题。
请先准备关联的 reportId 列表。
脚本内部会处理 SSE,并输出聚合后的 JSON 结果。
```
### Step 2 — Decide(确认/决策)
**涉及重要信息查询前,必须向用户确认问题。**
- 确认用户的问题表述是否清晰
- 确认是否已经提供 reportId 列表
```
请确认:
□ 您的问题:____
□ reportId 列表:____
□ 了解脚本会输出聚合后的 JSON:是
```
### Step 3 — Execute(执行)
执行 SSE 问答脚本。
### Step 4 — Log(留痕)
**问答结果必须完整记录。**
- 记录问题摘要、返回状态、时间戳
- 如果是多轮对话,需记录完整对话链
- 格式:`[LOG] ai-qa | question:xxx | ts:ISO8601 | status:success|error`
```
[LOG] ai-qa | question:如何创建周报 | ts:2026-03-25T13:57:00+08:00 | status:success
[LOG] ai-qa | question:如何创建周报(追问1)| ts:2026-03-25T13:58:00+08:00 | status:success
```
---
## 输出格式
```json
{
"resultCode": 1,
"data": {
"answer": "这里是聚合后的 AI 回答",
"eventCount": 6,
"metrics": {
"firstTextDelay": { "delay": 1200 },
"costMoney": { "cost": 0.001 },
"totalTimeCost": { "cost": 8000 }
}
}
}
```
---
## 注意事项
- 原始 OpenAPI 是 SSE,当前脚本会聚合后输出 JSON
- SSE 中的内容片段、耗时和费用事件会被拆开处理,`answer` 只保留聚合后的正文
- 同一会话内可多轮追问,但每次追问均需独立 Suggest → Decide → Log
- 日志需记录完整问答链,供追溯 AI 回答来源和质量评估
- 答案仅供参考,重要决策需人工核实
FILE:examples/employee-service/README.md
# employee-service 示例
## 模块说明
员工信息服务,支持批量员工信息查询和组织架构信息查询。
## 依赖脚本
- 批量员工:`../../scripts/employee-service/get-by-person-ids.py`
- 组织架构:`../../scripts/employee-service/get-org-info.py`
## 对应接口
- `POST /cwork-user/employee/getByPersonIds/{corpId}`
- `GET /cwork-user/employee/getEmployeeOrgInfo`
---
## 标准流程(含 3S1R 管理闭环)
### 场景一:批量获取员工信息
#### Step 1 — Suggest(建议)
```
建议:先确认 corpId 和 personId 列表,再批量拉取员工信息。
该接口适合把外部 personId 映射为当前企业下的员工对象。
```
#### Step 2 — Decide(确认/决策)
```
请确认:
□ corpId:____
□ personId 列表:____
```
#### Step 3 — Execute(执行)
执行批量员工查询脚本。
#### Step 4 — Log(留痕)
```
[LOG] employee-service.batch | corpId:xxx | count:N | ts:ISO8601
```
### 场景二:获取员工组织架构信息
#### Step 1 — Suggest(建议)
```
建议:当需要直属领导或二三级部门信息时,再调用组织架构查询。
```
#### Step 2 — Decide(确认/决策)
```
请确认:
□ empId:____
```
#### Step 3 — Execute(执行)
执行组织架构查询脚本。
#### Step 4 — Log(留痕)
```
[LOG] employee-service.org | empId:xxx | ts:ISO8601
```
---
## 输出格式
**批量员工信息:**
```json
{
"resultCode": 1,
"data": [
{
"id": "1514822127830339585",
"name": "屈军利",
"personId": 12139,
"corpId": "1509805893730611201"
}
]
}
```
**组织架构信息:**
```json
{
"resultCode": 1,
"data": {
"empId": "1514822127830339585",
"empName": "屈军利",
"managerEmpName": "张成鹏",
"secondLevelDeptName": "技术部",
"thirdLevelDeptName": "开发组"
}
}
```
---
## 注意事项
- `get-by-person-ids.py` 的输入是 `personId`,不是 `empId`
- `get-org-info.py` 的输入是 `empId`,适合补全上级与部门链路
- 两个接口都为只读查询,日志里应保留入参标识和返回数量
FILE:examples/file-service/README.md
# file-service 示例
## 模块说明
文件服务能力,支持上传本地文件和获取下载链接。
## 依赖脚本
- 上传:`../../scripts/file-service/upload-file.py`
- 下载信息:`../../scripts/file-service/get-download-info.py`
## 对应接口
- `POST /cwork-file/uploadWholeFile`
- `GET /cwork-file/getDownloadInfo`
---
## 标准流程(含 3S1R 管理闭环)
### 场景一:上传文件
#### Step 1 — Suggest(建议)
```
建议:先上传本地文件获取 resourceId。
若后续要在工作协同中作为附件引用,可将该 resourceId 作为 fileId 使用。
```
#### Step 2 — Decide(确认/决策)
```
请确认:
□ 本地文件路径:____
□ 是否用于工作协同附件:是 / 否
```
#### Step 3 — Execute(执行)
执行文件上传脚本。
#### Step 4 — Log(留痕)
```
[LOG] file-service.upload | file:xxx | resourceId:xxx | ts:ISO8601
```
### 场景二:获取下载链接
#### Step 1 — Suggest(建议)
```
建议:使用 resourceId 拉取临时下载链接。
下载 URL 有效期通常为 1 小时。
```
#### Step 2 — Decide(确认/决策)
```
请确认:
□ resourceId:____
□ 了解下载链接为临时链接:是
```
#### Step 3 — Execute(执行)
执行下载信息查询脚本。
#### Step 4 — Log(留痕)
```
[LOG] file-service.download | resourceId:xxx | ts:ISO8601
```
---
## 输出格式
**文件上传:**
```json
{
"resultCode": 1,
"data": "2037895404074434561",
"resultMsg": null
}
```
**下载信息:**
```json
{
"resultCode": 1,
"data": {
"resourceId": "2037895404074434561",
"fileName": "README.md",
"suffix": "md",
"size": 1926,
"downloadUrl": "https://..."
}
}
```
---
## 注意事项
- `upload-file.py` 返回的资源 ID 可直接作为工作协同附件里的 `fileId`
- `get-download-info.py` 返回的是临时下载链接,不适合长期持久化引用
- 上传与下载信息查询都依赖有效 `appKey`,但只有上传会产生新资源记录
FILE:examples/inbox/README.md
# inbox 示例
## 模块说明
获取当前用户的收件箱汇报列表,支持分页。
## 依赖脚本
`../../scripts/inbox/get-list.py`
## 对应接口
- `POST /work-report/report/record/inbox`
---
## 标准流程(含 3S1R 管理闭环)
### Step 1 — Suggest(建议)
**在拉取收件箱列表之前,先给出建议方案。**
- 根据用户当前状态,建议合适的分页参数(当前脚本默认10条/页)
- 如需已读/未读等高级筛选,可使用完整 JSON 请求体
- 提示收件箱数据量通常较大,建议按需索取
```
建议:默认拉取最近 10 条收件箱记录。
如需未读筛选,可传 `readStatus=0`。
```
### Step 2 — Decide(确认/决策)
**涉及数据查询前,必须向用户确认参数。**
- 确认分页大小(默认10,是否接受)
- 确认是否需要仅看未读
- 确认是否需要高级筛选 JSON
```
请确认查询参数:
□ 分页大小:10(默认)/ 其他:____
□ 已读状态:全部 / 仅未读 / 仅已读
□ 高级筛选 JSON(可选):____
```
### Step 3 — Execute(执行)
执行收件箱查询脚本。
### Step 4 — Log(留痕)
**查询结果必须完整记录。**
- 记录查询时间、分页参数、过滤条件、返回条数
- 记录本次拉取的关键元信息
- 格式:`[LOG] inbox | page:1 size:50 | result:N | ts:ISO8601`
```
[LOG] inbox | page:1 size:10 | result:10 | ts:2026-03-25T13:51:00+08:00
```
---
## 输出格式
```json
{
"resultCode": 1,
"data": {
"total": 1817,
"pageNum": 1,
"pageSize": 10,
"list": [
{
"id": "2037827358903361538",
"main": "【龙虾通知】BP采集表自动生成系统(TPR-20260326-001)—— 请及时反馈开发进展",
"writeEmpId": "1514822105176903682",
"userStatus": "已读"
}
]
}
}
```
---
## 注意事项
- 收件箱为只读查询,无需写操作确认
- 大量数据时需分页拉取,避免单次超时
- 日志需保留每次查询的完整参数,供后续比对
FILE:examples/outbox/README.md
# outbox 示例
## 模块说明
获取当前用户的发件箱汇报列表,支持分页。
## 依赖脚本
`../../scripts/outbox/get-list.py`
## 对应接口
- `POST /work-report/report/record/outbox`
---
## 标准流程(含 3S1R 管理闭环)
### Step 1 — Suggest(建议)
**在拉取发件箱列表之前,先给出建议方案。**
- 建议合适的分页参数(当前脚本默认10条/页)
- 如需高级筛选,可使用完整 JSON 请求体
- 提示发件箱数据量与用户发送频率相关
```
建议:默认拉取最近 10 条发件箱记录。
如需复杂筛选,可直接传完整 JSON 请求体。
```
### Step 2 — Decide(确认/决策)
**涉及数据查询前,必须向用户确认参数。**
- 确认分页大小(默认10)
- 确认是否需要高级筛选 JSON
```
请确认查询参数:
□ 分页大小:10(默认)/ 其他:____
□ 高级筛选 JSON(可选):____
```
### Step 3 — Execute(执行)
执行发件箱查询脚本。
### Step 4 — Log(留痕)
**查询结果必须完整记录。**
- 记录查询时间、分页参数、过滤条件、返回条数
- 格式:`[LOG] outbox | page:1 size:50 | result:N | ts:ISO8601`
```
[LOG] outbox | page:1 size:10 | result:10 | ts:2026-03-25T13:52:00+08:00
```
---
## 输出格式
```json
{
"resultCode": 1,
"data": {
"total": 270,
"pageNum": 1,
"pageSize": 10,
"list": [
{
"id": "2037895527831597058",
"main": "测试开头 20260328-1912 脚本联调",
"replyCount": 1,
"fileCount": 1
}
]
}
}
```
---
## 注意事项
- 发件箱为只读查询
- 分页参数需严格校验,避免异常偏移
- 日志保留完整查询上下文,供审计使用
FILE:examples/plan-create/README.md
# plan-create 示例
## 模块说明
创建高级工作任务,并指定汇报人、责任人等相关角色。
## 依赖脚本
`../../scripts/plan-create/create-simple.py`
## 对应接口
- `POST /work-report/open-platform/report/plan/create`
---
## 标准流程(含 3S1R 管理闭环)
### Step 1 — Suggest(建议)
**在创建任务之前,先给出建议方案。**
- 说明该接口用于创建高级工作任务,不是“发送汇报”
- 建议先确认 `reportEmpIdList`、`target`、`endTime` 等必填字段
- 提示创建后会直接生成任务与待办
```
建议:使用 create-simple 接口创建高级工作任务。
请先确认任务名称、任务目标、汇报人和结束时间。
该操作会直接创建任务,请确认相关参与人无误。
```
### Step 2 — Decide(确认/决策)
**涉及数据变动前(创建操作),必须获取用户明确决策。**
```
⚠️ 确认执行数据变动操作:
□ 任务名称:____
□ 任务要求:____
□ 任务目标:____
□ 汇报人 empId 列表:____
□ 结束时间(毫秒时间戳):____
□ 输入 "确认提交" 以继续:____
```
### Step 3 — Execute(执行)
执行工作任务创建脚本。
### Step 4 — Log(留痕)
**数据变动结果必须完整记录。**
- 记录创建的任务标题、任务 ID、提交时间、操作人
- 格式:`[LOG] plan-create | planId:xxx | title:xxx | ts:ISO8601 | operator:user`
```
[LOG] plan-create | planId:1234567890 | title:开放平台测试-创建高级任务 | ts:2026-03-25T13:59:00+08:00 | operator:evan
```
---
## 输入格式
**Body:**
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `main` | string | 是 | 任务名称 |
| `needful` | string | 是 | 任务要求 |
| `target` | string | 是 | 任务目标 |
| `reportEmpIdList` | array | 是 | 汇报人 ID 列表 |
| `endTime` | number | 是 | 结束时间戳 |
---
## 输出格式
```json
{
"resultCode": 1,
"data": "2037896299117928449"
}
```
---
## 注意事项
- ⚠️ 该接口为写操作(POST),执行后会在服务器创建新记录
- 提交前必须经过完整的 Suggest → Decide → Log 闭环
- Log 必须包含任务 ID,供后续查询或追踪使用
- 该接口是“创建工作任务”,不是“发送汇报”,也不支持附件上传
FILE:examples/plugin-report/README.md
# plugin-report 示例
## 模块说明
插件场景的聚合查询,覆盖最新待办、未读汇报,以及二者的合并结果。
## 依赖脚本
- 聚合列表:`../../scripts/plugin-report/get-list.py`
- 最新待办:`../../scripts/plugin-report/get-latest-list.py`
- 未读汇报:`../../scripts/plugin-report/get-unread-list.py`
## 对应接口
- `POST /work-report/plugin/report/list`
- `POST /work-report/plugin/report/latestList`
- `POST /work-report/plugin/report/unreadList`
---
## 标准流程(含 3S1R 管理闭环)
### Step 1 — Suggest(建议)
```
建议:插件场景优先使用 get-list 一次拿聚合结果。
如需单独分页某一类数据,再改用 latestList 或 unreadList。
lastUpdateTime 建议首次传 0,后续增量同步再带上上次时间戳。
```
### Step 2 — Decide(确认/决策)
```
请确认:
□ 查询模式:聚合 / 最新待办 / 未读汇报
□ pageIndex:____
□ pageSize:____
□ lastUpdateTime:0 / 自定义:____
```
### Step 3 — Execute(执行)
执行对应插件列表查询脚本。
### Step 4 — Log(留痕)
```
[LOG] plugin-report | mode:list | page:1 size:10 | ts:ISO8601
```
---
## 注意事项
- 这是插件聚合能力,不等同于收件箱或通用待办
- `lastUpdateTime` 用于增量刷新,首次可传 `0`
- 查询为只读操作
---
## 输出格式
**聚合列表 `get-list.py`:**
```json
{
"resultCode": 1,
"data": {
"latestTodoList": {
"total": 13,
"hasNew": true,
"list": [
{
"todoId": "2037896300162347009",
"id": null,
"main": "测试开头 20260328 任务联调",
"writeEmpName": "宋培众"
}
]
},
"unreadReportList": {
"total": 276,
"hasNew": true,
"list": [
{
"id": "2037801532216434690",
"main": "【BP审查通知】请确认您的个人BP审查报告及修改要求",
"writeEmpName": "BP"
}
]
}
}
}
```
**单列表 `get-latest-list.py` / `get-unread-list.py`:**
```json
{
"resultCode": 1,
"data": {
"total": 13,
"hasNew": true,
"list": [
{
"todoId": "2037896300162347009",
"id": null,
"main": "测试开头 20260328 任务联调",
"writeEmpName": "宋培众"
}
]
}
}
```
FILE:examples/report-detail/README.md
# report-detail 示例
## 模块说明
根据 reportId 获取单篇汇报的完整结构化详情。
## 依赖脚本
`../../scripts/report-detail/get-info.py`
## 对应接口
- `GET /work-report/report/info`
---
## 标准流程(含 3S1R 管理闭环)
### Step 1 — Suggest(建议)
**在拉取汇报详情之前,先给出建议方案。**
- 说明该接口的用途:获取指定汇报的完整内容
- 提示需要有效的 reportId(通常从 inbox/outbox 列表中获取)
- 建议如果获取失败时的备选路径(如 reportId 已失效)
```
建议:使用从 inbox/outbox 获取的 reportId 查询汇报详情。
该接口为只读,不修改任何数据。
如 reportId 无效,将返回错误,建议通过列表接口重新获取有效 ID。
```
### Step 2 — Decide(确认/决策)
**涉及数据查询前,必须向用户确认操作。**
- 确认目标 reportId 是否有效、可信
- 确认是否了解该汇报的内容范围(附件、评论等)
```
请确认:
□ 目标 reportId:____(用户从列表中选取)
□ 已了解该操作仅查询数据,不涉及修改
```
### Step 3 — Execute(执行)
执行详情查询脚本。
### Step 4 — Log(留痕)
**查询结果必须完整记录。**
- 记录 reportId、查询时间、汇报标题摘要
- 格式:`[LOG] report-detail | reportId:xxx | ts:ISO8601 | title:...`
```
[LOG] report-detail | reportId:RPT-20260325001 | ts:2026-03-25T13:53:00+08:00 | title:三月第二周工作计划
```
---
## 输出格式
```json
{
"resultCode": 1,
"data": {
"reportId": "2037895527831597058",
"content": "测试开头:工作协同脚本联调,请忽略。",
"createTime": "2026-03-28T14:12:11.000+00:00",
"replies": [
{
"replyId": "2037895545128869890",
"replyEmpName": "宋培众",
"content": "<p>测试回复:工作协同脚本联调。</p>"
}
]
}
}
```
---
## 注意事项
- 必须使用有效的 reportId,无效ID会报错
- 详情查询不产生任何数据变更
- 日志需记录完整汇报标识,供后续引用或追溯
FILE:examples/report-message/README.md
# report-message 示例
## 模块说明
覆盖我的新消息查询,以及阅读汇报后清理未读/新消息提醒的状态变更能力。
## 依赖脚本
- 我的新消息:`../../scripts/report-message/find-my-new-msg-list.py`
- 阅读汇报:`../../scripts/report-message/read-report.py`
## 对应接口
- `GET /work-report/open-platform/report/findMyNewMsgList`
- `GET /work-report/open-platform/report/readReport`
---
## 标准流程(含 3S1R 管理闭环)
### 场景一:查询我的新消息
#### Step 1 — Suggest(建议)
```
建议:默认查询重要消息(msgType=1)。
如需按其他业务消息类型过滤,再显式传入 msgType。
```
#### Step 2 — Decide(确认/决策)
```
请确认:
□ msgType:默认 1 / 自定义:____
```
#### Step 3 — Execute(执行)
执行新消息查询脚本。
#### Step 4 — Log(留痕)
```
[LOG] report-message.findMyNewMsgList | msgType:1 | ts:ISO8601
```
### 场景二:阅读汇报并清理提醒(数据变动操作 ⚠️)
#### Step 1 — Suggest(建议)
**涉及状态变更前,必须先说明副作用。**
```
建议:只有在确认用户确实要将该汇报标记为已读时,才调用 readReport。
该操作会清除当前用户下该汇报的未读和新消息提醒。
```
#### Step 2 — Decide(确认/决策)
**数据变动前,必须获取用户明确决策。**
```
⚠️ 确认执行阅读汇报:
□ reportId:____
□ 影响:标记已读并清除提醒
□ 输入 "确认已读" 以继续:____
```
#### Step 3 — Execute(执行)
执行阅读汇报脚本。
#### Step 4 — Log(留痕)
```
[LOG] report-message.readReport | reportId:xxx | action:mark-read | ts:ISO8601 | operator:user
```
---
## 注意事项
- `findMyNewMsgList` 为只读查询
- `readReport` 为状态变更操作,必须走 Suggest → Decide → Execute → Log
- `readReport` 使用 `GET`,但语义上不是只读接口
---
## 输出格式
**我的新消息:**
```json
{
"resultCode": 1,
"data": {
"total": 186,
"msgList": [
{
"unReadCount": 1,
"reportId": "2037801532216434690",
"reportTitle": "【BP审查通知】请确认您的个人BP审查报告及修改要求",
"lastReplyTime": "2026-03-28 15:58:40",
"replyEmployeeName": "BP",
"type": "汇报提交通知"
}
],
"serverReturnedSize": 169,
"clientLimit": 3,
"clientReturnedSize": 3
}
}
```
**阅读汇报:**
```json
{
"resultCode": 1,
"data": true
}
```
补充说明:
- `find-my-new-msg-list.py` 默认只处理前 `200` 条,最大只处理前 `500` 条
- `read-report.py` 有真实副作用:会清理当前用户的未读和新消息提醒
FILE:examples/report-query/README.md
# report-query 示例
## 模块说明
汇报维度的待办、未读与已读判断能力,服务于建议、决策、反馈等主业务场景。
## 依赖脚本
- 汇报待办:`../../scripts/report-query/get-todo-list.py`
- 汇报未读:`../../scripts/report-query/get-unread-list.py`
- 已读判断:`../../scripts/report-query/is-report-read.py`
## 对应接口
- `POST /work-report/reportInfoOpenQuery/todoList`
- `POST /work-report/reportInfoOpenQuery/unreadList`
- `GET /work-report/reportInfoOpenQuery/isReportRead`
---
## 标准流程(含 3S1R 管理闭环)
### Step 1 — Suggest(建议)
```
建议:涉及决策/建议/反馈时,优先查询 report-query/todoList,而不是通用 todoTask。
如果只是追查未读汇报,再使用 unreadList。
需要核验单个员工是否已读时,使用 isReportRead。
```
### Step 2 — Decide(确认/决策)
```
请确认:
□ 查询类型:todoList / unreadList / isReportRead
□ 分页:第____页,每页____条(列表查询时)
□ reportId / employeeId(已读判断时):____
```
### Step 3 — Execute(执行)
执行对应汇报查询脚本。
### Step 4 — Log(留痕)
```
[LOG] report-query | mode:todoList | page:1 size:20 | ts:ISO8601
```
---
## 注意事项
- `todoList` 是“汇报待办”,不是通用任务待办
- `unreadList` 是“汇报未读”,不是收件箱总列表
- 全部接口均为只读查询
---
## 输出格式
**汇报待办 / 汇报未读列表:**
```json
{
"resultCode": 1,
"data": {
"total": 10,
"list": [
{
"reportId": "2037801532216434690",
"main": "【BP审查通知】请确认您的个人BP审查报告及修改要求",
"todoId": "2037801532245794817",
"detail": {
"needAction": "需要建议",
"hasSubsequentDecision": false
}
}
],
"pageNum": 1,
"pageSize": 10,
"serverReturnedSize": 10,
"clientLimit": 3,
"clientReturnedSize": 3
}
}
```
**已读判断:**
```json
{
"resultCode": 1,
"data": false
}
```
补充说明:
- `get-unread-list.py` 默认只处理前 `200` 条,最大只处理前 `500` 条
- 平台实测中 `unreadList` 可能忽略请求里的 `pageSize`,因此不要把脚本裁剪结果误认为服务端真实分页结果
FILE:examples/report-write/README.md
# report-write 示例
## 模块说明
发送汇报与回复汇报,构成工作协同主链路中的核心写操作。
## 依赖脚本
- 发送汇报:`../../scripts/report-write/submit.py`
- 回复汇报:`../../scripts/report-write/reply.py`
## 对应接口
- `POST /work-report/report/record/submit`
- `POST /work-report/report/record/reply`
---
## 标准流程(含 3S1R 管理闭环)
### 场景一:发送汇报(数据变动操作 ⚠️)
#### Step 1 — Suggest(建议)
**涉及数据变动前,必须先给出建议并说明影响。**
```
建议:先确认汇报标题、正文和接收链路。
如需附件,请先调用 file-service 上传文件,获取 fileId 后再放入 fileVOList。
若需要建议/决策流程,请优先使用 reportLevelList,而不是只传 acceptEmpIdList。
```
#### Step 2 — Decide(确认/决策)
**数据变动前,必须获取用户明确决策。**
```
⚠️ 确认执行发送汇报:
□ 标题:____
□ 正文:____
□ 接收人/流程节点:____
□ 附件 fileId(可选):____
□ 输入 "确认提交" 以继续:____
```
#### Step 3 — Execute(执行)
执行发送汇报脚本。
#### Step 4 — Log(留痕)
**数据变动结果必须完整记录。**
```
[LOG] report-write.submit | reportId:xxx | title:xxx | ts:ISO8601 | operator:user
```
### 场景二:回复汇报(数据变动操作 ⚠️)
#### Step 1 — Suggest(建议)
**涉及数据变动前,必须先给出建议并说明影响。**
```
建议:先确认要回复的 reportRecordId 和回复内容。
如需附件,同样先上传文件,再把 fileId 放入 mediaVOList。
如果要提醒原汇报人,保持 sendMsg=true。
```
#### Step 2 — Decide(确认/决策)
**数据变动前,必须获取用户明确决策。**
```
⚠️ 确认执行回复汇报:
□ reportRecordId:____
□ 回复内容:____
□ 是否带附件:是 / 否
□ 输入 "确认回复" 以继续:____
```
#### Step 3 — Execute(执行)
执行汇报回复脚本。
#### Step 4 — Log(留痕)
**数据变动结果必须完整记录。**
```
[LOG] report-write.reply | reportRecordId:xxx | replyId:xxx | ts:ISO8601 | operator:user
```
---
## 输出格式
**发送汇报:**
```json
{
"resultCode": 1,
"data": {
"id": "2037895527831597058"
}
}
```
**回复汇报:**
```json
{
"resultCode": 1,
"data": 2037895545128869890
}
```
---
## 注意事项
- `submit.py`、`reply.py` 都是写操作,必须先走 Suggest → Decide → Execute → Log
- 如需附件,必须先通过 `file-service/upload-file.py` 拿到 `fileId`
- 简单模式适合常见场景;复杂链路请优先使用 `--body-json` 或 `--body-file`
FILE:examples/tasks/README.md
# tasks 示例
## 模块说明
获取工作任务分页索引,以及单个任务的简易信息与关联汇报列表。
## 依赖脚本
- 任务分页:`../../scripts/tasks/get-page.py`
- 任务简易信息:`../../scripts/tasks/get-simple-plan-and-report-info.py`
## 对应接口
- `POST /work-report/report/plan/searchPage`
- `GET /work-report/report/plan/getSimplePlanAndReportInfo`
---
## 标准流程(含 3S1R 管理闭环)
### Step 1 — Suggest(建议)
**在拉取任务列表之前,先给出建议方案。**
- 建议合适的分页参数(默认20条/页)
- 建议是否需要按任务状态(0 关闭 / 1 进行中)过滤
- 如需复杂筛选,可直接传完整 JSON 请求体
```
建议:默认拉取最近 20 条任务记录,按创建时间倒序。
如需按状态筛选,可补充 status 参数(0/1)。
如需更复杂筛选,可使用 body-json。
```
### Step 2 — Decide(确认/决策)
**涉及数据查询前,必须向用户确认参数。**
- 确认分页大小和页码
- 确认状态过滤条件(如适用)
- 确认是否使用高级筛选 JSON
```
请确认查询参数:
□ 分页:第____页,每页____条(默认第1页,20条)
□ 状态过滤:全部 / 0-关闭 / 1-进行中
□ 高级筛选 JSON(可选):____
```
### Step 3 — Execute(执行)
执行任务列表查询脚本。
### Step 4 — Log(留痕)
**查询结果必须完整记录。**
- 记录查询参数、返回条数、时间戳
- 格式:`[LOG] tasks | page:1 size:20 status:all | result:N | ts:ISO8601`
```
[LOG] tasks | page:1 size:20 status:1 | result:8 | ts:2026-03-25T13:54:00+08:00
```
---
## 输出格式
**任务分页:**
```json
{
"resultCode": 1,
"data": {
"total": 531,
"pageNum": 1,
"pageSize": 20,
"list": [
{
"id": "2001963433511276546",
"main": "GPTS后台管理会话中是否发送“开始”设置",
"status": 1,
"reportStatus": 3,
"creatorName": "杨晶晶"
}
]
}
}
```
**任务简易信息:**
```json
{
"resultCode": 1,
"data": {
"id": "2001963433511276546",
"main": "GPTS后台管理会话中是否发送“开始”设置",
"status": 1,
"reportList": [
{
"id": "2011377694498947074",
"main": "GPTS后台管理会话中是否发送“开始”设置",
"writeEmpName": "伍孝权"
}
]
}
}
```
---
## 注意事项
- 任务状态本身不是通过本接口修改
- 查询为只读操作
- `getSimplePlanAndReportInfo` 适合在已知 `planId` 后追查该任务下的汇报链路
- 日志需保留查询参数与结果摘要,供后续分析
FILE:examples/templates/README.md
# templates 示例
## 模块说明
获取最近处理过的事项列表,以及按 ID 批量获取事项简易信息,用于发起汇报时选择事项。
## 依赖脚本
- 最近事项列表:`../../scripts/templates/get-list.py`
- 事项批量详情:`../../scripts/templates/get-by-ids.py`
## 对应接口
- `POST /work-report/template/listTemplates`
- `POST /work-report/template/listByIds`
---
## 标准流程(含 3S1R 管理闭环)
### Step 1 — Suggest(建议)
**在拉取事项列表之前,先给出建议方案。**
- 说明返回的是“最近处理事项”,不是汇报模板库
- 建议按时间范围和 limit 控制返回结果
- 提示该接口是只读查询
```
建议:默认拉取最近 50 条事项列表。
如需限定范围,可传 beginTime / endTime。
事项列表仅用于发起汇报时选择事项,不涉及数据变动。
```
### Step 2 — Decide(确认/决策)
**涉及数据查询前,必须向用户确认参数。**
- 确认是否需要限定时间范围
- 确认返回条数上限
```
请确认:
□ limit:50(默认)/ 其他:____
□ 时间范围:默认最近一个月 / 自定义:____
```
### Step 3 — Execute(执行)
执行事项列表查询。
### Step 4 — Log(留痕)
**查询结果必须完整记录。**
- 记录查询条件、返回事项数量、时间戳
- 格式:`[LOG] templates | limit:50 | result:N | ts:ISO8601`
```
[LOG] templates | limit:50 | result:12 | ts:2026-03-25T13:56:00+08:00
```
---
## 输出格式
```json
{
"resultCode": 1,
"data": {
"recentOperateTemplates": [
{
"templateId": 1001,
"templateName": "市场周报"
}
]
}
}
```
---
## 注意事项
- 事项列表查询为只读操作
- `listByIds` 适合在已经拿到 `templateId` 后补充名称映射
- 日志需记录查询参数与返回数量
- 返回结果用于后续填写 `templateId`
FILE:examples/todos/README.md
# todos 示例
## 模块说明
待办事项管理,支持待办列表查询、创建的反馈待办查询,以及建议/决策类待办完成。
## 依赖脚本
- 列表:`../../scripts/todos/get-list.py`
- 我创建的反馈待办:`../../scripts/todos/list-created-feedbacks.py`
- 完成:`../../scripts/todos/complete.py`
## 对应接口
- `POST /work-report/todoTask/todoList`
- `GET /work-report/todoTask/listCreatedFeedbacks`
- `POST /work-report/open-platform/todo/completeTodo`
---
## 标准流程(含 3S1R 管理闭环)
### 场景一:查询待办列表
#### Step 1 — Suggest(建议)
```
建议:默认拉取当前用户的所有待办事项。
如需筛选类型,可补充 type 参数(plan/sign/lead/feedback/file_audit)。
```
#### Step 2 — Decide(确认/决策)
```
请确认:
□ 查询范围:全部 / 指定 type:____
□ 是否需要 executionResult:是 / 否
```
#### Step 3 — Execute(执行)
执行待办列表查询。
#### Step 4 — Log(留痕)
```
[LOG] todos.list | type:feedback | result:N | ts:ISO8601
```
---
### 场景二:完成待办项(数据变动操作 ⚠️)
#### Step 1 — Suggest(建议)
**涉及数据变动前,必须先给出建议并说明影响。**
```
建议:对待办项 [todoId] 提交建议或决策内容。
如果是决策类待办,需要额外提供 operate=agree/disagree。
注意:该操作会真正完成待办,必须经过明确确认。
```
#### Step 2 — Decide(确认/决策)
**数据变动前,必须获取用户明确决策。**
```
⚠️ 确认执行数据变动操作:
□ 待办项 ID:____
□ 处理内容 content:____
□ operate(决策类可选):agree / disagree / 不传
□ 输入 "确认" 以继续:____
```
#### Step 3 — Execute(执行)
执行完成操作脚本。
#### Step 4 — Log(留痕)
**数据变动结果必须完整记录。**
```
[LOG] todos.complete | todoId:xxx | action:complete | ts:ISO8601 | operator:user
```
---
## 输出格式
**列表查询:**
```json
{
"resultCode": 1,
"data": {
"list": []
}
}
```
**完成操作:**
```json
{
"resultCode": 1,
"data": true
}
```
---
## 注意事项
- 查询操作:只读,日志记录参数
- `listCreatedFeedbacks` 查询的是“我创建的反馈待办”,不是当前待处理待办池
- 完成操作:数据变动,需强制 Suggest → Decide → Log 闭环
- 日志必须包含操作类型(query/complete)、操作时间、操作人
FILE:examples/user-search/README.md
# user-search 示例
## 模块说明
按姓名模糊搜索全部员工,返回内部员工与外部联系人结果。
## 依赖脚本
`../../scripts/user-search/search-emp.py`
## 对应接口
- `GET /cwork-user/searchEmpByName`
---
## 标准流程(含 3S1R 管理闭环)
### Step 1 — Suggest(建议)
**在执行任何搜索动作之前,先给出建议方案。**
- 根据用户提供的姓名片段,预估可能的匹配范围(模糊匹配特性)
- 建议最可能的搜索关键词写法
- 提示搜索可能返回多条结果,需进一步确认
```
建议:使用 "姓名关键词" 进行模糊搜索,返回所有包含该关键词的员工记录。
如需后续创建任务或汇报,请从返回结果中确认唯一 `empId`。
```
### Step 2 — Decide(确认/决策)
**涉及重要查询前,必须向用户确认意图。**
- 确认搜索关键词是否准确
- 确认是否需要限定部门范围
- 确认是否接受模糊匹配带来的多条结果
```
请确认搜索关键词:[用户提供]
□ 接受模糊匹配结果(默认勾选)
□ 指定部门过滤(可选)
```
### Step 3 — Execute(执行)
执行搜索脚本,返回员工列表。
### Step 4 — Log(留痕)
**所有查询结果必须记录到会话或外部日志。**
- 记录搜索关键词、查询时间戳、命中数量
- 记录选取的具体员工信息(用于后续引用)
- 格式:`[LOG] user-search | keyword:xxx | hits:N | ts:ISO8601`
```
[LOG] user-search | keyword:张 | hits:12 | ts:2026-03-25T13:50:00+08:00
```
---
## 输出格式
```json
{
"resultCode": 1,
"data": {
"inside": {
"companyVO": {
"id": "1509805893730611201",
"name": "玄关健康"
},
"empList": [
{ "id": "1514822127830339585", "name": "屈军利", "personId": 12139, "mainDept": "开发组", "title": "部门经理" }
]
},
"outside": []
},
"resultMsg": null
}
```
---
## 注意事项
- 模糊匹配可能返回大量无关结果,执行前务必通过 Decide 步骤确认范围
- 日志需包含每次搜索的完整上下文,供审计追溯
FILE:openapi/ai-qa/api-index.md
# API 索引 — ai-qa
1. `POST /work-report/open-platform/report/aiSseQaV2` -> `./ask-sse.md`
FILE:openapi/ai-qa/ask-sse.md
# POST https://sg-al-cwork-web.mediportal.com.cn/open-api/work-report/open-platform/report/aiSseQaV2
## 作用
针对指定汇报集合进行 AI SSE 问答。
**鉴权类型**
- `appKey`
**Headers**
- `appKey`
- `Content-Type: application/json`
- `Accept: text/event-stream`
**响应类型**
- `text/event-stream`
**JSON Body 请求参数**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `userContent` | string | 是 | 问答内容 |
| `aiType` | number | 否 | 默认固定 42 |
| `reportIdList` | array | 是 | 需要关联对话的汇报 ID 列表 |
## 请求 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"userContent": { "type": "string" },
"aiType": { "type": "number", "default": 42 },
"reportIdList": { "type": "array", "items": { "type": "number" } }
},
"required": ["userContent", "reportIdList"]
}
```
## 响应说明
原始 OpenAPI 直接返回 SSE 事件流;当前脚本会将事件流聚合后再输出为 JSON。
## 脚本映射
- `../../scripts/ai-qa/ask-sse.py`
FILE:openapi/employee-service/api-index.md
# API 索引 — employee-service
1. `POST /cwork-user/employee/getByPersonIds/{corpId}` -> `./get-by-person-ids.md`
2. `GET /cwork-user/employee/getEmployeeOrgInfo` -> `./get-org-info.md`
FILE:openapi/employee-service/get-by-person-ids.md
# POST https://sg-al-cwork-web.mediportal.com.cn/open-api/cwork-user/employee/getByPersonIds/{corpId}
## 作用
根据企业 ID 和 personId 列表批量查询员工信息。
**鉴权类型**
- `appKey`
**Headers**
- `appKey`
- `Content-Type: application/json`
**Path 参数**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `corpId` | number | 是 | 企业 ID |
**JSON Body 请求参数**
请求体为 `Long[]`。
## 请求 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "array",
"items": { "type": "number" }
}
```
## 响应 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"resultCode": { "type": "number" },
"data": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": { "type": "number" },
"personId": { "type": "number" },
"name": { "type": "string" },
"dingUserId": { "type": "string" },
"title": { "type": "string" }
}
}
}
}
}
```
## 脚本映射
- `../../scripts/employee-service/get-by-person-ids.py`
FILE:openapi/employee-service/get-org-info.md
# GET https://sg-al-cwork-web.mediportal.com.cn/open-api/cwork-user/employee/getEmployeeOrgInfo
## 作用
根据员工 ID 获取直属领导和二三级部门等组织架构信息。
**鉴权类型**
- `appKey`
**Headers**
- `appKey`
**Query 参数**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `empId` | number | 是 | 员工 ID |
## 请求 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"empId": { "type": "number" }
},
"required": ["empId"]
}
```
## 响应 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"resultCode": { "type": "number" },
"data": {
"type": "object",
"properties": {
"empId": { "type": "number" },
"empName": { "type": "string" },
"managerEmpId": { "type": "number" },
"managerEmpName": { "type": "string" },
"secondLevelDeptId": { "type": "number" },
"secondLevelDeptName": { "type": "string" },
"thirdLevelDeptId": { "type": "number" },
"thirdLevelDeptName": { "type": "string" }
}
}
}
}
```
## 脚本映射
- `../../scripts/employee-service/get-org-info.py`
FILE:openapi/file-service/api-index.md
# API 索引 — file-service
1. `POST /cwork-file/uploadWholeFile` -> `./upload-file.md`
2. `GET /cwork-file/getDownloadInfo` -> `./get-download-info.md`
FILE:openapi/file-service/get-download-info.md
# GET https://sg-al-cwork-web.mediportal.com.cn/open-api/cwork-file/getDownloadInfo
## 作用
根据资源 ID 获取临时下载链接和文件元信息。
**鉴权类型**
- `appKey`
**Headers**
- `appKey`
**Query 参数**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `resourceId` | number | 是 | 文件资源 ID |
## 请求 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"resourceId": { "type": "number" }
},
"required": ["resourceId"]
}
```
## 响应 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"resultCode": { "type": "number" },
"data": {
"type": "object",
"properties": {
"downloadUrl": { "type": "string" },
"fileName": { "type": "string" },
"resourceId": { "type": "number" },
"suffix": { "type": "string" },
"size": { "type": "number" }
}
}
}
}
```
## 脚本映射
- `../../scripts/file-service/get-download-info.py`
FILE:openapi/file-service/upload-file.md
# POST https://sg-al-cwork-web.mediportal.com.cn/open-api/cwork-file/uploadWholeFile
## 作用
上传本地文件,返回文件资源 ID。该 ID 可用于获取下载信息,也可在工作协同附件场景中作为 `fileId` 使用。
**鉴权类型**
- `appKey`
**Headers**
- `appKey`
- `Content-Type: multipart/form-data`
**Form Data 请求参数**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `file` | file | 是 | 本地文件 |
## 请求 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"file": { "type": "string" }
},
"required": ["file"]
}
```
## 响应 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"resultCode": { "type": "number" },
"resultMsg": { "type": ["string", "null"] },
"data": { "type": "number" }
}
}
```
## 脚本映射
- `../../scripts/file-service/upload-file.py`
FILE:openapi/inbox/api-index.md
# API 索引 — inbox
1. `POST /work-report/report/record/inbox` -> `./get-list.md`
FILE:openapi/inbox/get-list.md
# POST https://sg-al-cwork-web.mediportal.com.cn/open-api/work-report/report/record/inbox
## 作用
获取当前用户的收件箱汇报列表。
**鉴权类型**
- `appKey`
**Headers**
- `appKey`
- `Content-Type: application/json`
**JSON Body 请求参数**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `pageSize` | number | 是 | 每页显示个数 |
| `pageIndex` | number | 否 | 默认 1 |
| `reportRecordType` | number | 否 | 工作汇报类型:1-工作交流、2-工作指引、3-文件签批、4-AI 汇报、5-工作汇报 |
| `type` | string | 否 | 业务类型 |
| `planId` | number | 否 | 任务 ID |
| `grade` | string | 否 | 优先级:一般/紧急 |
| `empIdList` | array | 否 | 汇报人 ID 列表 |
| `beginTime` | number | 否 | 汇报时间开始(毫秒) |
| `endTime` | number | 否 | 汇报时间结束(毫秒) |
| `templateId` | number | 否 | 事项 ID |
| `templateIdList` | array | 否 | 事项 ID 列表 |
| `classificationIdList` | array | 否 | 汇报分类 ID 列表 |
| `labelId` | number | 否 | 标签 ID |
| `source` | string | 否 | 来源:detail/common |
| `orderColumn` | string | 否 | 排序字段:createTime/lastReplyTime/receiveTime |
| `readStatus` | number | 否 | 已读状态:0 未读,1 已读 |
| `laterRead` | boolean | 否 | 是否稍后阅读 |
| `mark` | boolean | 否 | 是否星标 |
| `leadEmpId` | number | 否 | 协办领导 ID |
## 请求 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"pageSize": { "type": "number" },
"pageIndex": { "type": "number", "default": 1 },
"reportRecordType": { "type": "number" },
"type": { "type": "string" },
"planId": { "type": "number" },
"grade": { "type": "string" },
"empIdList": { "type": "array", "items": { "type": "number" } },
"beginTime": { "type": "number" },
"endTime": { "type": "number" },
"templateId": { "type": "number" },
"templateIdList": { "type": "array", "items": { "type": "number" } },
"classificationIdList": { "type": "array", "items": { "type": "number" } },
"labelId": { "type": "number" },
"source": { "type": "string" },
"orderColumn": { "type": "string" },
"readStatus": { "type": "number" },
"laterRead": { "type": "boolean" },
"mark": { "type": "boolean" },
"leadEmpId": { "type": "number" }
}
}
```
## 响应 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"resultCode": { "type": "number" },
"data": { "type": "object" }
}
}
```
## 脚本映射
- `../../scripts/inbox/get-list.py`
FILE:openapi/outbox/api-index.md
# API 索引 — outbox
1. `POST /work-report/report/record/outbox` -> `./get-list.md`
FILE:openapi/outbox/get-list.md
# POST https://sg-al-cwork-web.mediportal.com.cn/open-api/work-report/report/record/outbox
## 作用
获取当前用户的发件箱汇报列表。
**鉴权类型**
- `appKey`
**Headers**
- `appKey`
- `Content-Type: application/json`
**JSON Body 请求参数**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `pageSize` | number | 是 | 每页显示个数 |
| `pageIndex` | number | 否 | 默认 1 |
| `reportRecordType` | number | 否 | 工作汇报类型:1-工作交流、2-工作指引、3-文件签批、4-AI 汇报、5-工作汇报 |
| `type` | string | 否 | 业务类型 |
| `planId` | number | 否 | 任务 ID |
| `grade` | string | 否 | 优先级:一般/紧急 |
| `empIdList` | array | 否 | 汇报人 ID 列表 |
| `beginTime` | number | 否 | 汇报时间开始(毫秒) |
| `endTime` | number | 否 | 汇报时间结束(毫秒) |
| `templateId` | number | 否 | 事项 ID |
| `templateIdList` | array | 否 | 事项 ID 列表 |
| `classificationIdList` | array | 否 | 汇报分类 ID 列表 |
| `labelId` | number | 否 | 标签 ID |
| `source` | string | 否 | 来源:detail/common |
| `orderColumn` | string | 否 | 排序字段:createTime/lastReplyTime/receiveTime |
| `readStatus` | number | 否 | 已读状态:0 未读,1 已读 |
| `laterRead` | boolean | 否 | 是否稍后阅读 |
| `mark` | boolean | 否 | 是否星标 |
| `leadEmpId` | number | 否 | 协办领导 ID |
## 请求 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"pageSize": { "type": "number" },
"pageIndex": { "type": "number", "default": 1 },
"reportRecordType": { "type": "number" },
"type": { "type": "string" },
"planId": { "type": "number" },
"grade": { "type": "string" },
"empIdList": { "type": "array", "items": { "type": "number" } },
"beginTime": { "type": "number" },
"endTime": { "type": "number" },
"templateId": { "type": "number" },
"templateIdList": { "type": "array", "items": { "type": "number" } },
"classificationIdList": { "type": "array", "items": { "type": "number" } },
"labelId": { "type": "number" },
"source": { "type": "string" },
"orderColumn": { "type": "string" },
"readStatus": { "type": "number" },
"laterRead": { "type": "boolean" },
"mark": { "type": "boolean" },
"leadEmpId": { "type": "number" }
}
}
```
## 响应 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"resultCode": { "type": "number" },
"data": { "type": "object" }
}
}
```
## 脚本映射
- `../../scripts/outbox/get-list.py`
FILE:openapi/plan-create/api-index.md
# API 索引 — plan-create
1. `POST /work-report/open-platform/report/plan/create` -> `./create-simple.md`
FILE:openapi/plan-create/create-simple.md
# POST https://sg-al-cwork-web.mediportal.com.cn/open-api/work-report/open-platform/report/plan/create
## 作用
创建一个高级工作任务,并分配汇报人及其他干系人。
**鉴权类型**
- `appKey`
**Headers**
- `appKey`
- `Content-Type: application/json`
**JSON Body 请求参数**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `main` | string | 是 | 任务名称 |
| `needful` | string | 是 | 任务描述/要求 |
| `typeId` | number | 是 | 业务类型 ID |
| `reportEmpIdList` | array | 是 | 汇报人员 ID 列表 |
| `target` | string | 是 | 任务目标 |
| `ownerEmpIdList` | array | 否 | 责任人 ID 列表 |
| `assistEmpIdList` | array | 否 | 协办人 ID 列表 |
| `supervisorEmpIdList` | array | 否 | 监督人 ID 列表 |
| `copyEmpIdList` | array | 否 | 抄送人 ID 列表 |
| `observerEmpIdList` | array | 否 | 观察者 ID 列表 |
| `endTime` | number | 是 | 任务结束时间戳(毫秒) |
| `pushNow` | number | 否 | 创建后是否立即推送:0/1 |
## 请求 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"main": { "type": "string" },
"needful": { "type": "string" },
"typeId": { "type": "number" },
"reportEmpIdList": { "type": "array", "items": { "type": "number" } },
"target": { "type": "string" },
"ownerEmpIdList": { "type": "array", "items": { "type": "number" } },
"assistEmpIdList": { "type": "array", "items": { "type": "number" } },
"supervisorEmpIdList": { "type": "array", "items": { "type": "number" } },
"copyEmpIdList": { "type": "array", "items": { "type": "number" } },
"observerEmpIdList": { "type": "array", "items": { "type": "number" } },
"endTime": { "type": "number" },
"pushNow": { "type": "number", "default": 1 }
}
}
```
## 响应 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"resultCode": { "type": "number" },
"data": { "type": "number" }
}
}
```
## 脚本映射
- `../../scripts/plan-create/create-simple.py`
FILE:openapi/plugin-report/api-index.md
# API 索引 — plugin-report
1. `POST /work-report/plugin/report/list` -> `./get-list.md`
2. `POST /work-report/plugin/report/latestList` -> `./get-latest-list.md`
3. `POST /work-report/plugin/report/unreadList` -> `./get-unread-list.md`
FILE:openapi/plugin-report/get-latest-list.md
# POST https://sg-al-cwork-web.mediportal.com.cn/open-api/work-report/plugin/report/latestList
## 作用
分页获取插件场景下的最新待办列表。
**鉴权类型**
- `appKey`
**Headers**
- `appKey`
- `Content-Type: application/json`
**JSON Body 请求参数**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `pageIndex` | number | 否 | 页数从 1 开始,默认 1 |
| `pageSize` | number | 否 | 每页显示个数 |
| `lastUpdateTime` | number | 是 | 最后更新时间戳(毫秒),默认 0 |
## 请求 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"pageIndex": { "type": "number", "default": 1 },
"pageSize": { "type": "number", "default": 20 },
"lastUpdateTime": { "type": "number", "default": 0 }
},
"required": ["lastUpdateTime"]
}
```
## 响应 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"resultCode": { "type": "number" },
"data": { "type": "object" }
}
}
```
## 脚本映射
- `../../scripts/plugin-report/get-latest-list.py`
FILE:openapi/plugin-report/get-list.md
# POST https://sg-al-cwork-web.mediportal.com.cn/open-api/work-report/plugin/report/list
## 作用
获取插件场景下的最新待办列表与未读汇报列表聚合结果。
**鉴权类型**
- `appKey`
**Headers**
- `appKey`
- `Content-Type: application/json`
**JSON Body 请求参数**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `pageIndex` | number | 否 | 页数从 1 开始,默认 1 |
| `pageSize` | number | 否 | 每页显示个数 |
| `lastUpdateTime` | number | 是 | 最后更新时间戳(毫秒),默认 0 |
## 请求 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"pageIndex": { "type": "number", "default": 1 },
"pageSize": { "type": "number", "default": 20 },
"lastUpdateTime": { "type": "number", "default": 0 }
},
"required": ["lastUpdateTime"]
}
```
## 响应 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"resultCode": { "type": "number" },
"data": {
"type": "object",
"properties": {
"latestTodoList": { "type": "object" },
"unreadReportList": { "type": "object" }
}
}
}
}
```
## 脚本映射
- `../../scripts/plugin-report/get-list.py`
FILE:openapi/plugin-report/get-unread-list.md
# POST https://sg-al-cwork-web.mediportal.com.cn/open-api/work-report/plugin/report/unreadList
## 作用
分页获取插件场景下的未读汇报列表。
**鉴权类型**
- `appKey`
**Headers**
- `appKey`
- `Content-Type: application/json`
**JSON Body 请求参数**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `pageIndex` | number | 否 | 页数从 1 开始,默认 1 |
| `pageSize` | number | 否 | 每页显示个数 |
| `lastUpdateTime` | number | 是 | 最后更新时间戳(毫秒),默认 0 |
## 请求 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"pageIndex": { "type": "number", "default": 1 },
"pageSize": { "type": "number", "default": 20 },
"lastUpdateTime": { "type": "number", "default": 0 }
},
"required": ["lastUpdateTime"]
}
```
## 响应 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"resultCode": { "type": "number" },
"data": { "type": "object" }
}
}
```
## 脚本映射
- `../../scripts/plugin-report/get-unread-list.py`
FILE:openapi/report-detail/api-index.md
# API 索引 — report-detail
1. `GET /work-report/report/info` -> `./get-info.md`
FILE:openapi/report-detail/get-info.md
# GET https://sg-al-cwork-web.mediportal.com.cn/open-api/work-report/report/info
## 作用
获取指定汇报的完整结构化详情,包含汇报人、时间、各层级内容及结果。
**鉴权类型**
- `appKey`
**Headers**
- `appKey`
- `Content-Type: application/json`
**Query 参数**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `reportId` | string | 是 | 汇报 ID |
## 请求 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {}
}
```
## 响应 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"resultCode": { "type": "number" },
"data": { "type": "object" }
}
}
```
## 脚本映射
- `../../scripts/report-detail/get-info.py`
FILE:openapi/report-message/api-index.md
# API 索引 — report-message
1. `GET /work-report/open-platform/report/findMyNewMsgList` -> `./find-my-new-msg-list.md`
2. `GET /work-report/open-platform/report/readReport` -> `./read-report.md`
FILE:openapi/report-message/find-my-new-msg-list.md
# GET https://sg-al-cwork-web.mediportal.com.cn/open-api/work-report/open-platform/report/findMyNewMsgList
## 作用
获取当前用户的新消息汇总与明细列表。
**鉴权类型**
- `appKey`
**Headers**
- `appKey`
**Query 请求参数**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `msgType` | number | 否 | 消息类型过滤;文档默认 1,表示重要消息 |
## 响应 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"resultCode": { "type": "number" },
"data": {
"type": "object",
"properties": {
"total": { "type": "number" },
"msgList": { "type": "array" }
}
}
}
}
```
## 脚本映射
- `../../scripts/report-message/find-my-new-msg-list.py`
FILE:openapi/report-message/read-report.md
# GET https://sg-al-cwork-web.mediportal.com.cn/open-api/work-report/open-platform/report/readReport
## 作用
将指定汇报标记为已读,并清除当前用户下该汇报的未读提醒与新消息。
**鉴权类型**
- `appKey`
**Headers**
- `appKey`
**Query 请求参数**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `reportId` | number | 是 | 汇报 ID |
## 响应 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"resultCode": { "type": "number" },
"data": { "type": ["null", "object", "boolean"] }
}
}
```
## 脚本映射
- `../../scripts/report-message/read-report.py`
FILE:openapi/report-query/api-index.md
# API 索引 — report-query
1. `POST /work-report/reportInfoOpenQuery/todoList` -> `./get-todo-list.md`
2. `POST /work-report/reportInfoOpenQuery/unreadList` -> `./get-unread-list.md`
3. `GET /work-report/reportInfoOpenQuery/isReportRead` -> `./is-report-read.md`
FILE:openapi/report-query/get-todo-list.md
# POST https://sg-al-cwork-web.mediportal.com.cn/open-api/work-report/reportInfoOpenQuery/todoList
## 作用
分页获取当前用户需要处理的汇报待办列表,主要用于建议、决策、反馈场景。
**鉴权类型**
- `appKey`
**Headers**
- `appKey`
- `Content-Type: application/json`
**JSON Body 请求参数**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `pageIndex` | number | 否 | 页数从 1 开始,默认 1 |
| `pageSize` | number | 否 | 每页显示个数,默认 20 |
## 请求 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"pageIndex": { "type": "number", "default": 1 },
"pageSize": { "type": "number", "default": 20 }
}
}
```
## 响应 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"resultCode": { "type": "number" },
"data": { "type": "object" }
}
}
```
## 脚本映射
- `../../scripts/report-query/get-todo-list.py`
FILE:openapi/report-query/get-unread-list.md
# POST https://sg-al-cwork-web.mediportal.com.cn/open-api/work-report/reportInfoOpenQuery/unreadList
## 作用
分页获取当前用户的未读汇报列表。
**鉴权类型**
- `appKey`
**Headers**
- `appKey`
- `Content-Type: application/json`
**JSON Body 请求参数**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `pageIndex` | number | 否 | 页数从 1 开始,默认 1 |
| `pageSize` | number | 否 | 每页显示个数,默认 20 |
## 请求 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"pageIndex": { "type": "number", "default": 1 },
"pageSize": { "type": "number", "default": 20 }
}
}
```
## 响应 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"resultCode": { "type": "number" },
"data": { "type": "object" }
}
}
```
## 脚本映射
- `../../scripts/report-query/get-unread-list.py`
FILE:openapi/report-query/is-report-read.md
# GET https://sg-al-cwork-web.mediportal.com.cn/open-api/work-report/reportInfoOpenQuery/isReportRead
## 作用
根据汇报 ID 和员工 ID 判断该员工是否已读指定汇报。
**鉴权类型**
- `appKey`
**Headers**
- `appKey`
**Query 请求参数**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `reportId` | number | 是 | 汇报 ID |
| `employeeId` | number | 是 | 员工 ID |
## 响应 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"resultCode": { "type": "number" },
"data": { "type": "boolean" }
}
}
```
## 脚本映射
- `../../scripts/report-query/is-report-read.py`
FILE:openapi/report-write/api-index.md
# API 索引 — report-write
1. `POST /work-report/report/record/submit` -> `./submit.md`
2. `POST /work-report/report/record/reply` -> `./reply.md`
FILE:openapi/report-write/reply.md
# POST https://sg-al-cwork-web.mediportal.com.cn/open-api/work-report/report/record/reply
## 作用
对指定汇报进行回复,可带附件、@人和通知控制。
**鉴权类型**
- `appKey`
**Headers**
- `appKey`
- `Content-Type: application/json`
**JSON Body 请求参数**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `reportRecordId` | string | 是 | 工作汇报 ID |
| `isMedia` | number | 否 | 是否带附件:0/1 |
| `mediaVOList` | array | 否 | 附件列表 |
| `contentHtml` | string | 是 | 回复内容 |
| `sendMsg` | boolean | 否 | 是否通知汇报人,默认 true |
| `addEmpIdList` | array | 否 | 被 @ 的员工 ID 集合 |
## 请求 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"reportRecordId": { "type": "string" },
"isMedia": { "type": "number", "default": 0 },
"mediaVOList": {
"type": "array",
"items": {
"type": "object",
"properties": {
"fileId": { "type": "string" },
"fsize": { "type": "number" },
"name": { "type": "string" },
"type": { "type": "string" }
}
}
},
"contentHtml": { "type": "string" },
"sendMsg": { "type": "boolean", "default": true },
"addEmpIdList": { "type": "array", "items": { "type": "string" } }
},
"required": ["reportRecordId", "contentHtml"]
}
```
## 响应 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"resultCode": { "type": "number" },
"resultMsg": { "type": ["string", "null"] },
"data": { "type": "number" }
}
}
```
## 脚本映射
- `../../scripts/report-write/reply.py`
FILE:openapi/report-write/submit.md
# POST https://sg-al-cwork-web.mediportal.com.cn/open-api/work-report/report/record/submit
## 作用
创建并提交一条汇报记录,支持关联任务/事项、接收人/抄送人、多级节点及附件。
**鉴权类型**
- `appKey`
**Headers**
- `appKey`
- `Content-Type: application/json`
**JSON Body 请求参数**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `planId` | number | 否 | 工作任务 ID |
| `templateId` | number | 否 | 事项 ID |
| `typeId` | number | 否 | 业务类型 ID,默认 9999 |
| `main` | string | 是 | 汇报标题 |
| `grade` | string | 否 | 优先级:一般/紧急 |
| `privacyLevel` | string | 否 | 密级:非涉密/涉密 |
| `contentType` | string | 否 | 正文类型:html/markdown |
| `contentHtml` | string | 是 | 汇报内容 |
| `acceptEmpIdList` | array | 否 | 接收人 ID 列表;仅在 `reportLevelList` 为空时兜底 |
| `copyEmpIdList` | array | 否 | 抄送人 ID 列表 |
| `reportLevelList` | array | 否 | 多级节点用户列表;接收人以该字段为准 |
| `fileVOList` | array | 否 | 附件列表 |
## 请求 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"planId": { "type": "number" },
"templateId": { "type": "number" },
"typeId": { "type": "number", "default": 9999 },
"main": { "type": "string" },
"grade": { "type": "string" },
"privacyLevel": { "type": "string" },
"contentType": { "type": "string", "default": "html" },
"contentHtml": { "type": "string" },
"acceptEmpIdList": { "type": "array", "items": { "type": "number" } },
"copyEmpIdList": { "type": "array", "items": { "type": "number" } },
"reportLevelList": {
"type": "array",
"items": {
"type": "object",
"properties": {
"type": { "type": "string" },
"level": { "type": "number" },
"nodeCode": { "type": "string" },
"nodeName": { "type": "string" },
"levelUserList": {
"type": "array",
"items": {
"type": "object",
"properties": {
"empId": { "type": "number" },
"requirement": { "type": "string" }
}
}
}
}
}
},
"fileVOList": {
"type": "array",
"items": {
"type": "object",
"properties": {
"fileId": { "type": "string" },
"name": { "type": "string" },
"type": { "type": "string" },
"fsize": { "type": "number" },
"url": { "type": "string" }
}
}
}
},
"required": ["main", "contentHtml"]
}
```
## 响应 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"resultCode": { "type": "number" },
"resultMsg": { "type": ["string", "null"] },
"data": {
"type": "object",
"properties": {
"id": { "type": "number" }
}
}
}
}
```
## 脚本映射
- `../../scripts/report-write/submit.py`
FILE:openapi/tasks/api-index.md
# API 索引 — tasks
1. `POST /work-report/report/plan/searchPage` -> `./get-page.md`
2. `GET /work-report/report/plan/getSimplePlanAndReportInfo` -> `./get-simple-plan-and-report-info.md`
FILE:openapi/tasks/get-page.md
# POST https://sg-al-cwork-web.mediportal.com.cn/open-api/work-report/report/plan/searchPage
## 作用
按页获取工作任务列表。
**鉴权类型**
- `appKey`
**Headers**
- `appKey`
- `Content-Type: application/json`
**JSON Body 请求参数**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `pageSize` | number | 否 | 默认 30 |
| `pageIndex` | number | 否 | 默认 1 |
| `userType` | string | 否 | 工作任务列表类型 |
| `keyWord` | string | 否 | 搜索关键词 |
| `status` | number | 否 | 状态:0-关闭,1-进行中 |
| `isRead` | number | 否 | 任务读取状态:0 未读,1 已读 |
| `reportStatus` | number | 否 | 汇报状态:0 关闭,1 待汇报,2 已汇报,3 逾期 |
| `roleType` | string | 否 | 角色类型 |
| `empIdList` | array | 否 | 人员 ID 列表 |
| `labelList` | array | 否 | 标签名称列表 |
| `grades` | array | 否 | 优先级列表 |
## 请求 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"pageIndex": { "type": "number", "default": 1 },
"pageSize": { "type": "number", "default": 30 },
"userType": { "type": "string" },
"keyWord": { "type": "string" },
"status": { "type": "number" },
"isRead": { "type": "number" },
"reportStatus": { "type": "number" },
"roleType": { "type": "string" },
"empIdList": { "type": "array", "items": { "type": "number" } },
"labelList": { "type": "array", "items": { "type": "string" } },
"grades": { "type": "array", "items": { "type": "string" } }
}
}
```
## 响应 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"resultCode": { "type": "number" },
"data": { "type": "object" }
}
}
```
## 脚本映射
- `../../scripts/tasks/get-page.py`
FILE:openapi/tasks/get-simple-plan-and-report-info.md
# GET https://sg-al-cwork-web.mediportal.com.cn/open-api/work-report/report/plan/getSimplePlanAndReportInfo
## 作用
获取单个任务的简易信息,以及该任务关联的汇报简易信息列表。
**鉴权类型**
- `appKey`
**Headers**
- `appKey`
**Query 请求参数**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `planId` | number | 是 | 工作任务 ID |
## 响应 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"resultCode": { "type": "number" },
"data": {
"type": "object",
"properties": {
"id": { "type": "number" },
"main": { "type": "string" },
"status": { "type": "number" },
"reportList": { "type": "array" }
}
}
}
}
```
## 脚本映射
- `../../scripts/tasks/get-simple-plan-and-report-info.py`
FILE:openapi/templates/api-index.md
# API 索引 — templates
1. `POST /work-report/template/listTemplates` -> `./get-list.md`
2. `POST /work-report/template/listByIds` -> `./list-by-ids.md`
FILE:openapi/templates/get-list.md
# POST https://sg-al-cwork-web.mediportal.com.cn/open-api/work-report/template/listTemplates
## 作用
获取最近处理过的事项列表,用于发起汇报时选择事项。
**鉴权类型**
- `appKey`
**Headers**
- `appKey`
- `Content-Type: application/json`
**JSON Body 请求参数**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `beginTime` | number | 否 | 开始时间(默认一个月前,毫秒) |
| `endTime` | number | 否 | 结束时间(默认当前,毫秒) |
| `limit` | number | 否 | 限制条数 (默认 50) |
## 请求 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"beginTime": { "type": "number" },
"endTime": { "type": "number" },
"limit": { "type": "number", "default": 50 }
}
}
```
## 响应 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"resultCode": { "type": "number" },
"data": { "type": "object" }
}
}
```
## 脚本映射
- `../../scripts/templates/get-list.py`
FILE:openapi/templates/list-by-ids.md
# POST https://sg-al-cwork-web.mediportal.com.cn/open-api/work-report/template/listByIds
## 作用
按事项 ID 列表批量获取事项简易信息。
**鉴权类型**
- `appKey`
**Headers**
- `appKey`
- `Content-Type: application/json`
**JSON Body 请求参数**
请求体为 JSON 数组。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `templateIds` | array | 是 | 事项 ID 列表;请求体直接传数组 |
## 请求 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "array",
"items": {
"type": "number"
}
}
```
## 响应 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"resultCode": { "type": "number" },
"data": {
"type": "array",
"items": {
"type": "object",
"properties": {
"templateId": { "type": "number" },
"main": { "type": "string" }
}
}
}
}
}
```
## 脚本映射
- `../../scripts/templates/get-by-ids.py`
FILE:openapi/todos/api-index.md
# API 索引 — todos
1. `POST /work-report/todoTask/todoList` -> `./get-list.md`
2. `POST /work-report/open-platform/todo/completeTodo` -> `./complete.md`
3. `GET /work-report/todoTask/listCreatedFeedbacks` -> `./list-created-feedbacks.md`
FILE:openapi/todos/complete.md
# POST https://sg-al-cwork-web.mediportal.com.cn/open-api/work-report/open-platform/todo/completeTodo
## 作用
提交建议/决策内容并完成指定待办。
**鉴权类型**
- `appKey`
**Headers**
- `appKey`
- `Content-Type: application/json`
**JSON Body 请求参数**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `todoId` | number | 是 | 待办 ID |
| `content` | string | 是 | 处理意见 |
| `operate` | string | 否 | 决策操作类型 (agree/disagree),建议类待办无需传 |
## 请求 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"todoId": { "type": "number" },
"operate": { "type": "string" },
"content": { "type": "string" }
}
}
```
## 响应 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"resultCode": { "type": "number" },
"data": { "type": "boolean" }
}
}
```
## 脚本映射
- `../../scripts/todos/complete.py`
FILE:openapi/todos/get-list.md
# POST https://sg-al-cwork-web.mediportal.com.cn/open-api/work-report/todoTask/todoList
## 作用
分页查询当前用户待办列表(任务/签批/指引/反馈等)。
**鉴权类型**
- `appKey`
**Headers**
- `appKey`
- `Content-Type: application/json`
**JSON Body 请求参数**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `pageIndex` | number | 否 | 页码(默认 1) |
| `pageSize` | number | 否 | 每页条数(默认 10) |
| `type` | string | 否 | 筛选类型 (plan-任务, sign-签批, lead-指引, feedback-反馈, file_audit-文件审核) |
| `executionResult` | string | 否 | 待办事项执行结果 |
## 请求 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"pageIndex": { "type": "number", "default": 1 },
"pageSize": { "type": "number", "default": 10 },
"type": { "type": "string" },
"executionResult": { "type": "string" }
}
}
```
## 响应 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"resultCode": { "type": "number" },
"data": { "type": "object" }
}
}
```
## 脚本映射
- `../../scripts/todos/get-list.py`
FILE:openapi/todos/list-created-feedbacks.md
# GET https://sg-al-cwork-web.mediportal.com.cn/open-api/work-report/todoTask/listCreatedFeedbacks
## 作用
获取指定员工创建的反馈类型待办列表;不传 `empId` 时默认查询当前登录用户。
**鉴权类型**
- `appKey`
**Headers**
- `appKey`
**Query 请求参数**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `empId` | number | 否 | 反馈创建人员工 ID |
## 响应 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"resultCode": { "type": "number" },
"data": {
"type": "array",
"items": {
"type": "object",
"properties": {
"todoId": { "type": "number" },
"reportId": { "type": "number" },
"status": { "type": "number" },
"type": { "type": "string" },
"content": { "type": "string" }
}
}
}
}
}
```
## 脚本映射
- `../../scripts/todos/list-created-feedbacks.py`
FILE:openapi/user-search/api-index.md
# API 索引 — user-search
1. `GET /cwork-user/searchEmpByName` -> `./search-emp.md`
FILE:openapi/user-search/search-emp.md
# GET https://sg-al-cwork-web.mediportal.com.cn/open-api/cwork-user/searchEmpByName
## 作用
根据姓名模糊搜索全部员工,返回内部员工与外部联系人结果。工作协同场景通常使用 `inside.empList[].id` 作为后续接口中的 `empId`。
**鉴权类型**
- `appKey`
**Headers**
- `appKey`
- `Content-Type: application/json`
**Query 参数**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `searchKey` | string | 是 | 员工姓名关键词(如"张三") |
## 请求 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"required": ["searchKey"],
"properties": {
"searchKey": { "type": "string", "description": "搜索关键词" }
}
}
```
## 响应 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"resultCode": { "type": "number" },
"data": {
"type": "object",
"properties": {
"inside": {
"type": "object",
"properties": {
"empList": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": { "type": "number" },
"personId": { "type": "number" },
"name": { "type": "string" },
"title": { "type": "string" },
"mainDept": { "type": "string" }
}
}
}
}
},
"outside": {}
}
}
}
}
```
## 脚本映射
- `../../scripts/user-search/search-emp.py`
FILE:run-results/anomaly-analysis-20260328.md
# 工作协同脚本异常分析(2026-03-28)
## 1. templates/get-list.py 默认返回 `data=null`
### 现象
- 直接执行 `python3 scripts/templates/get-list.py --limit 5`
- 平台返回:`resultCode=1, data=null`
### 复测结果
- 扩大时间窗口后,接口正常返回事项列表:
- `beginTime=1704038400000`
- `endTime=1774991999000`
### 结论
- 不是接口硬错误,更像是默认“最近时间窗口”内没有最近处理事项。
- 平台在空结果场景下返回 `data=null`,而不是空数组。
### 已处理
- 脚本已把成功场景下的 `data=null` 归一化为:
- `{"recentOperateTemplates":[]}`
- `scripts/templates/README.md` 已补充时间窗口说明。
## 2. ai-qa/ask-sse.py 返回 SSE JSON 片段拼接串
### 现象
- 原脚本把每个 `data:` 事件直接字符串拼接。
- 输出中的 `answer` 混入了:
- `{"content":"..."}` 片段
- `{"id":"firstTextDelay"...}` 指标事件
- `ok`
### 结论
- 这是脚本聚合逻辑问题,不是平台接口错误。
- 平台返回的是标准 SSE 事件流,内容事件和指标事件混在一起。
### 已处理
- 脚本已按事件类型拆分:
- `content` -> 聚合到 `data.answer`
- `firstTextDelay/costMoney/totalTimeCost` -> 聚合到 `data.metrics`
- 统计 `eventCount`
- 复测后,`answer` 已恢复为干净正文。
## 3. report-query/get-unread-list.py 返回过大
### 现象
- 请求参数:`pageSize=5`
- 平台实际返回:
- `pageSize=100`
- `size=100`
### 结论
- 这是平台接口行为与请求参数不一致,不是脚本组包错误。
- 脚本已正确发送 `{"pageIndex":1,"pageSize":5}`。
- 当前更像平台忽略或放大了分页大小。
### 已处理
- 脚本新增:
- `--client-limit`
- `--output-file`
- 现在可在不改平台行为的前提下控制终端输出。
- 默认只处理前 `200` 条;即使显式传更大值,脚本也最多只处理前 `500` 条。
## 4. report-message/find-my-new-msg-list.py 返回过大
### 现象
- 该接口不是分页接口。
- 实测 `data.total=186`,`msgList` 实际返回 169 条。
### 结论
- 这不是异常报错,而是接口设计本身返回“我的新消息汇总 + 明细列表”整包。
- 用户未读/新消息积压越多,返回越大。
### 已处理
- 脚本新增:
- `--client-limit`
- `--output-file`
- 默认只处理前 `200` 条;即使显式传更大值,脚本也最多只处理前 `500` 条。
## 5. todos/list-created-feedbacks.py 响应偏慢
### 现象
- 比普通 GET 接口耗时更长。
- 实测平台返回 17 条记录。
### 结论
- 当前没有证据表明脚本存在问题。
- 接口本身为全量返回、无分页,慢更可能来自平台检索与响应体大小。
### 已处理
- 脚本新增:
- `--client-limit`
- `--output-file`
- 默认只处理前 `200` 条;即使显式传更大值,脚本也最多只处理前 `500` 条。
## 当前仍属于平台侧待确认的问题
1. `/work-report/reportInfoOpenQuery/unreadList` 为什么忽略/放大 `pageSize`
2. `/work-report/template/listTemplates` 为什么空结果返回 `data=null` 而非标准空列表
3. `/work-report/todoTask/listCreatedFeedbacks` 是否支持服务端分页
FILE:run-results/consistency-audit-20260328.md
# 一致性复核报告(2026-03-28)
## 结论
- 本次对 `27` 个脚本、`27` 份 `openapi` 接口文档、`15` 份 `examples`、`SKILL.md` 做了交叉一致性检查。
- 结果:结构一致、声明一致、参数说明一致,可用于 AI 组合调用。
## 检查范围
- `openapi/*/api-index.md` 与 `openapi/*/*.md` 的方法/路径/文档映射
- `openapi` 文档与 `scripts/*.py` 的 `API_URL`、HTTP 方法对齐
- `scripts/*/README.md` 与模块脚本集合、接口集合对齐
- `examples/*/README.md` 与模块脚本集合、接口集合对齐
- `examples` 是否具备 `对应接口 / 输出格式 / 注意事项` 三段
- 全量脚本 `--help` 探测能力(未注入 `appKey` 场景)
- `SKILL.md` 的数量统计与模块树一致性
## 核查结果
- 模块数:`15`
- API 文档:`27`
- Python 脚本:`27`
- 示例文档:`15`
- `--help` 探测失败脚本:`0`
- README 参数与脚本 `--help` 不一致:`0`
## 特殊说明
- `employee-service/get-by-person-ids` 使用路径模板 `/cwork-user/employee/getByPersonIds/{corpId}`。
- 文档写法是模板路径;
- 脚本实现是先定义基础 `API_URL`,再在调用时拼接 `/{corpId}`;
- 这属于等价实现,不是不一致问题。
## 本轮加固项
- 在 `SKILL.md` 增加“AI 组合调用约束(推荐)”:
- 显式参数优先
- 写操作确认
- 大结果裁剪策略
- `unreadList` 分页异常认知
- 统一输出契约
FILE:run-results/feedback-default-limit.json
{"resultCode": 1, "resultMsg": null, "data": [{"todoId": "2033751853854089217", "reportId": "2033740812176945153", "status": 1, "type": "反馈", "executor": {"dingUserId": "16082", "deptList": [{"id": "1678741011769233409", "name": "数据管理部"}], "corpId": "1509805893730611202", "name": "刘明霞", "personId": "1776805533987205122", "id": "1776805533991399426", "avatar": "https://static-legacy.dingtalk.com/media/lQLPD4QxeF1l6tXNAyDNAyCw3vLC-OHnouEGKHkjDAs1AA_800_800.png_200x200.jpg", "title": "智能项目运营专员"}, "content": "我建议你这些资料放到notex中,然后生成PPT视频给大家介绍不更好吗?另外,三个附件使用notex生成不更好吗?关于培训资料是否需要补充?我建议,你把资料丢给notex,然后通过chat和ai聊,让其给出一些建议,并最终生成培训的宣传视频。", "createTime": "2026-03-17 11:46:42", "updateTime": "2026-03-17 13:56:34"}, {"todoId": "1949657671284326402", "reportId": "1948679462451175425", "status": 0, "type": "反馈", "executor": {"dingUserId": "02512928184621823493", "deptList": [{"id": null, "name": null}], "corpId": "1509805893730611201", "name": "吴衍松", "personId": "1909425340521037825", "id": "1909425340604915713", "avatar": "", "title": "产品经理"}, "content": "\n\n\n\n@吴衍松 需要优化提示词,测试环境", "createTime": "2025-07-28 10:26:26", "updateTime": null}, {"todoId": "1932287489198981121", "reportId": "1930908720071790593", "status": 1, "type": "反馈", "executor": {"dingUserId": "021440151839137530", "deptList": [{"id": "1572046602678374401", "name": "AI及数据运维组"}], "corpId": "1509805893730611201", "name": "马超平", "personId": 12319, "id": "1514822178510114817", "avatar": "https://static-legacy.dingtalk.com/media/lADPDgQ9qkkZmKvNAZfNAbU_437_407.jpg_200x200.jpg", "title": "Android开发工程师"}, "content": "lite版本我清楚。请小马回复一下。\n小马现在不是提供了lite版可以不联网吗? AI修订是否可以开启修订模式,原本意思是开始开启规划模式。 由于,目前选择的汇报范围是字数没有超,是全文给的。如果超了,走的程敏的RAG。 开启规划模式,目前小马支持联网和知识库,目前还不支持基于选中的一些汇报做规划模式,这个需求安排新任务开发。", "createTime": "2025-06-10 12:03:31", "updateTime": "2025-08-20 16:55:29"}, {"todoId": "1931891783233855491", "reportId": "1931881601472815106", "status": 1, "type": "反馈", "executor": {"dingUserId": "01101703956268", "deptList": [{"id": "1734091154306437121", "name": "产品部"}], "corpId": "1509805893730611201", "name": "王馗", "personId": 12328, "id": "1514822180686958594", "avatar": "", "title": "产品经理"}, "content": "本周的计划已确诊,由于继海请假,周三才过来。\n优化日期处理和计划状态检查,预计本周五完成。\n需要客户端发版本(预计下周一,用户升级可以体验):工作说说中按类型列示内容,汇报时组合显示。", "createTime": "2025-06-09 09:51:08", "updateTime": "2025-06-09 19:16:50"}, {"todoId": "1930530955213295617", "reportId": "1930183216893100034", "status": 1, "type": "反馈", "executor": {"dingUserId": "162741344324383660", "deptList": [{"id": "1734091154306437121", "name": "产品部"}], "corpId": "1509805893730611201", "name": "庄诗吟", "personId": 12217, "id": "1514822149934321666", "avatar": "https://static-legacy.dingtalk.com/media/lADPBbCc1Y0jc3DNAbDNAbA_432_432.jpg_200x200.jpg", "title": "测试工程师"}, "content": "经过分析目前主要的问题分为四类,建议增加这四类的收集到问题数\n1、语音转文导致客户名字误判,以及活动事项中的错别字语意理解错误导致判断计划状态错误。\n2、拜访日期识别错误\n3、计划状态错误\n4、生成汇报后,编辑导致的问题\n------------------------------------------\n和广智讨论后,计划本周改进以下两个点,\n1、日期,可以做进一步的调整\n2、计划状态,根据活动类型的定义再做一步的拆分\n这样可以进一步的改进此类的问题。", "createTime": "2025-06-05 15:43:42", "updateTime": "2025-06-05 16:27:35"}, {"todoId": "1920379597390864386", "reportId": "1920368140538183682", "status": 1, "type": "反馈", "executor": {"dingUserId": "205723416636402615", "deptList": [{"id": null, "name": null}], "corpId": "1509805893730611201", "name": "邓暗军", "personId": 12355, "id": "1514822189729878018", "avatar": "https://static-legacy.dingtalk.com/media/lADPBFuNdg0yKV3NAcvNAVU_341_459.jpg_200x200.jpg", "title": "Web开发工程师"}, "content": "暗军,后台只有你调用的3条记录,时间都是昨天的数据。\n是不是mcp调用有问题?\n另外,这条任务已经完成了吧?", "createTime": "2025-05-08 15:25:49", "updateTime": "2025-05-08 15:41:07"}, {"todoId": "1916305044144197633", "reportId": "1915696663771549697", "status": 1, "type": "反馈", "executor": {"dingUserId": "011051631323371318", "deptList": [{"id": "1514822037057212417", "name": "开发组"}], "corpId": "1509805893730611201", "name": "屈军利", "personId": 12139, "id": "1514822127830339585", "avatar": "https://static-legacy.dingtalk.com/media/lQLPM34qneI31LfNAezNAeywJe-L_rkZ4D4FcN7-hAE-AA_492_492.png_200x200.jpg", "title": "部门经理"}, "content": "建议:增加开发评估时间\n1、不需要把整个对话记录都,输入和输出完整输出\n2、建议再分析对话中,增加你是如何思考的,已经任务是如何拆分的?\n3、你认为对话过程,哪些有效的输入起到了关键的点,需要把它列举出来。\n例如:“当我处理接受的时候或全部接受的时候,你是没有清空缓存吗” , 它告诉你“这个错误需要我修复 。。。。”\n追加询问“你是如何清楚缓存的”?\n追加询问“是否还需其它业务也需要清楚缓存的”?\n最后追问“要检查一遍是否OK?”\n总结:\n整体思想就是:你要想像你和ai在聊天,例如他说可以了,你就可以反问你如何做的,是否其它的业务也有类似的问题,最后再检查一下。\n关键点:\n1、任务拆分\n2、给出过程\n3、反思过程\n4、检查反思", "createTime": "2025-04-27 09:34:59", "updateTime": "2025-04-29 19:06:10"}, {"todoId": "1877290328722034689", "reportId": "1877230455722496002", "status": 1, "type": "反馈", "executor": {"dingUserId": "162741525040298697", "deptList": [{"id": "1514822037057212417", "name": "开发组"}], "corpId": "1509805893730611201", "name": "龙继海", "personId": 11947, "id": "1514822075363790849", "avatar": "", "title": "Java开发工程师"}, "content": "按照正常的开发,继海评估一下大概需求多久?", "createTime": "2025-01-09 17:44:27", "updateTime": "2025-01-13 11:43:45"}, {"todoId": "1868922999577657345", "reportId": "1868859991520288769", "status": 1, "type": "反馈", "executor": {"dingUserId": "0162515027688505", "deptList": [{"id": null, "name": null}], "corpId": "1509805893730611201", "name": "池素苹", "personId": 12019, "id": "1514822094926024706", "avatar": "https://static-legacy.dingtalk.com/media/lADPBGY18AKN3FXNBGPNBGM_1123_1123.jpg_200x200.jpg", "title": "UI设计师"}, "content": "@于耀 @池素苹 \n全流程写会议纪要的步骤,体验地址。\n会议ID取法:\n1、找小马要\n2、工作说说PC端,左上角点击5下,出现调试框框,具体可以为小罗", "createTime": "2024-12-17 15:35:40", "updateTime": "2024-12-19 17:06:21"}, {"todoId": "1868922997417590785", "reportId": "1868859991520288769", "status": 1, "type": "反馈", "executor": {"dingUserId": "0344670603656178", "deptList": [{"id": "1734091154306437121", "name": "产品部"}], "corpId": "1509805893730611201", "name": "于耀", "personId": 12121, "id": "1514822122331607041", "avatar": "https://static-legacy.dingtalk.com/media/lADPBE1XZMkZOYbNChDNChA_2576_2576.jpg_200x200.jpg", "title": "产品经理"}, "content": "@于耀 @池素苹 \n全流程写会议纪要的步骤,体验地址。\n会议ID取法:\n1、找小马要\n2、工作说说PC端,左上角点击5下,出现调试框框,具体可以为小罗", "createTime": "2024-12-17 15:35:40", "updateTime": "2024-12-18 14:17:35"}, {"todoId": "1859146755134902274", "reportId": "1858810356508479490", "status": 1, "type": "反馈", "executor": {"dingUserId": "0344670603656178", "deptList": [{"id": "1734091154306437121", "name": "产品部"}], "corpId": "1509805893730611201", "name": "于耀", "personId": 12121, "id": "1514822122331607041", "avatar": "https://static-legacy.dingtalk.com/media/lADPBE1XZMkZOYbNChDNChA_2576_2576.jpg_200x200.jpg", "title": "产品经理"}, "content": "流程:提取议程\n流程:提取观点\n流程:提取待办事项\n\n都加上了反思,解决如下问题:\n1、丢失观点人的信息\n2、议程的时间段,更加准确了\n3、 提取的代办事项,要求严格了,更加精准了。", "createTime": "2024-11-20 16:08:22", "updateTime": "2024-11-20 16:09:10"}, {"todoId": "1854404550763663361", "reportId": "1854106601198383106", "status": 1, "type": "反馈", "executor": {"dingUserId": "01133329670032602901", "deptList": [{"id": null, "name": null}], "corpId": "1509805893730611201", "name": "胡海洋", "personId": 11953, "id": "1514822076743716865", "avatar": "", "title": "测试工程师"}, "content": "主要解决的问题是:录音转文字,出现错字、同音字、近音字导致,导致考试判断有误。\n@孟令辉需要安排人测试验证,是否有效果。 \nUCP-推广知识-考试—1024\n\n提示词中,在# 评分规则:中追加了一下信息\n**在匹配题目信息的“回答关键词”时,请考虑语音转文字可能产生的错字、同音字或近音字,尤其是医学等专业术语。根据上下文推断实际含义,尽可能包含同义词或近似变体。**\n错字识别:识别可能的拼写错误。\n例如:“昨天”可能被识别为“作天”,“肿瘤”可能被识别为“肿流”。\n同音字识别:识别发音相同但字形不同的词语。\n例如:“旅游”可能被识别为“驴游”,“心绞痛”可能被识别为“心脚痛”。\n近音字识别:识别发音相近的词语。\n例如:“喝水”可能被识别为“和谁”,“胆固醇”可能被识别为“但固存”", "createTime": "2024-11-07 14:04:31", "updateTime": "2024-11-12 10:12:01"}, {"todoId": "1854404549295656962", "reportId": "1854106601198383106", "status": 1, "type": "反馈", "executor": {"dingUserId": "16471300844109", "deptList": [{"id": "1734091154306437121", "name": "产品部"}], "corpId": "1509805893730611201", "name": "杨宵", "personId": 11851, "id": "1514822095710359554", "avatar": "", "title": "测试工程师"}, "content": "主要解决的问题是:录音转文字,出现错字、同音字、近音字导致,导致考试判断有误。\n@孟令辉需要安排人测试验证,是否有效果。 \nUCP-推广知识-考试—1024\n\n提示词中,在# 评分规则:中追加了一下信息\n**在匹配题目信息的“回答关键词”时,请考虑语音转文字可能产生的错字、同音字或近音字,尤其是医学等专业术语。根据上下文推断实际含义,尽可能包含同义词或近似变体。**\n错字识别:识别可能的拼写错误。\n例如:“昨天”可能被识别为“作天”,“肿瘤”可能被识别为“肿流”。\n同音字识别:识别发音相同但字形不同的词语。\n例如:“旅游”可能被识别为“驴游”,“心绞痛”可能被识别为“心脚痛”。\n近音字识别:识别发音相近的词语。\n例如:“喝水”可能被识别为“和谁”,“胆固醇”可能被识别为“但固存”", "createTime": "2024-11-07 14:04:31", "updateTime": "2024-11-08 14:20:16"}, {"todoId": "1854404543956307970", "reportId": "1854106601198383106", "status": 1, "type": "反馈", "executor": {"dingUserId": "1959172428-27258626", "deptList": [{"id": null, "name": null}], "corpId": "1509805893730611201", "name": "孟令辉", "personId": "1737655359406903298", "id": "1737655359415291906", "avatar": "https://static-legacy.dingtalk.com/media/lQDPM4Wppop8IY_NBWPNBWOwcGplez6RTtcGi-maOPn-AQ_1379_1379.jpg_200x200.jpg", "title": "产品经理"}, "content": "主要解决的问题是:录音转文字,出现错字、同音字、近音字导致,导致考试判断有误。\n@孟令辉需要安排人测试验证,是否有效果。 \nUCP-推广知识-考试—1024\n\n提示词中,在# 评分规则:中追加了一下信息\n**在匹配题目信息的“回答关键词”时,请考虑语音转文字可能产生的错字、同音字或近音字,尤其是医学等专业术语。根据上下文推断实际含义,尽可能包含同义词或近似变体。**\n错字识别:识别可能的拼写错误。\n例如:“昨天”可能被识别为“作天”,“肿瘤”可能被识别为“肿流”。\n同音字识别:识别发音相同但字形不同的词语。\n例如:“旅游”可能被识别为“驴游”,“心绞痛”可能被识别为“心脚痛”。\n近音字识别:识别发音相近的词语。\n例如:“喝水”可能被识别为“和谁”,“胆固醇”可能被识别为“但固存”", "createTime": "2024-11-07 14:04:31", "updateTime": "2024-11-08 10:17:43"}, {"todoId": "1853340304548057089", "reportId": "1851451348900544513", "status": 1, "type": "反馈", "executor": {"dingUserId": "01133329670032602901", "deptList": [{"id": null, "name": null}], "corpId": "1509805893730611201", "name": "胡海洋", "personId": 11953, "id": "1514822076743716865", "avatar": "", "title": "测试工程师"}, "content": "请海洋提供具体的数据信息", "createTime": "2024-11-04 15:35:36", "updateTime": "2024-11-04 15:58:56"}, {"todoId": "1769929584932986882", "reportId": "1769927546347986946", "status": 1, "type": "反馈", "executor": {"dingUserId": "011051631323371318", "deptList": [{"id": "1514822037057212417", "name": "开发组"}], "corpId": "1509805893730611201", "name": "屈军利", "personId": 12139, "id": "1514822127830339585", "avatar": "https://static-legacy.dingtalk.com/media/lQLPM34qneI31LfNAezNAeywJe-L_rkZ4D4FcN7-hAE-AA_492_492.png_200x200.jpg", "title": "部门经理"}, "content": "44勾选了需要反馈", "createTime": "2024-03-19 11:31:12", "updateTime": "2024-03-19 11:38:53"}, {"todoId": "1608001083091886082", "reportId": "1605459131462955010", "status": 1, "type": "反馈", "executor": {"dingUserId": "0111231824221023", "deptList": [{"id": "1514821998498975746", "name": "玄关健康"}], "corpId": "1509805893730611201", "name": "张成鹏", "personId": 12058, "id": "1514822105176903682", "avatar": "", "title": "总经理"}, "content": "目的是:做年中汇报总结分析,找一些数据参考;是否要给他们提供?", "createTime": "2022-12-28 15:25:09", "updateTime": "2022-12-28 16:57:02"}], "serverReturnedSize": 17, "clientLimit": 200, "clientReturnedSize": 17, "defaultClientLimit": 200, "maxClientLimit": 500}
FILE:run-results/list-created-feedbacks-sample.json
{"resultCode": 1, "resultMsg": null, "data": [{"todoId": "2033751853854089217", "reportId": "2033740812176945153", "status": 1, "type": "反馈", "executor": {"dingUserId": "16082", "deptList": [{"id": "1678741011769233409", "name": "数据管理部"}], "corpId": "1509805893730611202", "name": "刘明霞", "personId": "1776805533987205122", "id": "1776805533991399426", "avatar": "https://static-legacy.dingtalk.com/media/lQLPD4QxeF1l6tXNAyDNAyCw3vLC-OHnouEGKHkjDAs1AA_800_800.png_200x200.jpg", "title": "智能项目运营专员"}, "content": "我建议你这些资料放到notex中,然后生成PPT视频给大家介绍不更好吗?另外,三个附件使用notex生成不更好吗?关于培训资料是否需要补充?我建议,你把资料丢给notex,然后通过chat和ai聊,让其给出一些建议,并最终生成培训的宣传视频。", "createTime": "2026-03-17 11:46:42", "updateTime": "2026-03-17 13:56:34"}, {"todoId": "1949657671284326402", "reportId": "1948679462451175425", "status": 0, "type": "反馈", "executor": {"dingUserId": "02512928184621823493", "deptList": [{"id": null, "name": null}], "corpId": "1509805893730611201", "name": "吴衍松", "personId": "1909425340521037825", "id": "1909425340604915713", "avatar": "", "title": "产品经理"}, "content": "\n\n\n\n@吴衍松 需要优化提示词,测试环境", "createTime": "2025-07-28 10:26:26", "updateTime": null}, {"todoId": "1932287489198981121", "reportId": "1930908720071790593", "status": 1, "type": "反馈", "executor": {"dingUserId": "021440151839137530", "deptList": [{"id": "1572046602678374401", "name": "AI及数据运维组"}], "corpId": "1509805893730611201", "name": "马超平", "personId": 12319, "id": "1514822178510114817", "avatar": "https://static-legacy.dingtalk.com/media/lADPDgQ9qkkZmKvNAZfNAbU_437_407.jpg_200x200.jpg", "title": "Android开发工程师"}, "content": "lite版本我清楚。请小马回复一下。\n小马现在不是提供了lite版可以不联网吗? AI修订是否可以开启修订模式,原本意思是开始开启规划模式。 由于,目前选择的汇报范围是字数没有超,是全文给的。如果超了,走的程敏的RAG。 开启规划模式,目前小马支持联网和知识库,目前还不支持基于选中的一些汇报做规划模式,这个需求安排新任务开发。", "createTime": "2025-06-10 12:03:31", "updateTime": "2025-08-20 16:55:29"}], "serverReturnedSize": 17, "clientLimit": 3, "clientReturnedSize": 3}
FILE:run-results/msg-default-limit.json
{"resultCode": 1, "resultMsg": null, "data": {"total": 186, "msgList": [{"unReadCount": 1, "reportId": "2037801532216434690", "reportTitle": "【BP审查通知】请确认您的个人BP审查报告及修改要求", "lastReportReplyId": null, "lastReplyContent": "## ⚠️ BP整改复查 - 仍有未完成项\n\n经复查,上次审查报告中指出的核心问题均**未进行整改*...", "lastReplyTime": "2026-03-28 15:58:40", "replyEmployeeName": "BP", "replyEmployeeDeptAndTitle": "", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2037736957693517825", "reportTitle": "【BP审查通知】请确认您的个人BP审查报告及修改要求", "lastReportReplyId": null, "lastReplyContent": "## ⚠️ BP整改复查 - 仍有未完成项\n\n经复查,上次审查报告中指出的绝大部分核心问题**仍未进...", "lastReplyTime": "2026-03-28 11:42:05", "replyEmployeeName": "BP", "replyEmployeeDeptAndTitle": "", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2037364082105139202", "reportTitle": "Skill 工厂系统定义汇报", "lastReportReplyId": null, "lastReplyContent": "各位好,附件是 Skill 工厂系统的完整产品定义,请查阅。 核心能力:与用户协作创建 Skill ...", "lastReplyTime": "2026-03-27 11:00:24", "replyEmployeeName": "张成鹏", "replyEmployeeDeptAndTitle": "[玄关健康-总经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2037057446878453761", "reportTitle": "【饮料优惠购】丰e足食零食柜福利活动", "lastReportReplyId": null, "lastReplyContent": "各位同事,大家好! 为方便大家办公时段补给能量,公司丰e足食智能零...", "lastReplyTime": "2026-03-26 14:48:25", "replyEmployeeName": "齐慧", "replyEmployeeDeptAndTitle": "[行政管理部-行政助理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2035964985372700673", "reportTitle": "「限时内购」3月春季大放价,喜辽妥多磺酸粘多糖乳膏超强折扣内购会", "lastReportReplyId": null, "lastReplyContent": "惊呆了家人们! Hirudoid喜辽妥多磺酸粘多糖乳膏超强折扣“内购上线”啦!!此前没了解过跨境版喜...", "lastReplyTime": "2026-03-23 14:54:30", "replyEmployeeName": "易大宇", "replyEmployeeDeptAndTitle": "[跨境组-电商运营专员-ML]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2034932116260732929", "reportTitle": "《个人BP实战培训:从认知到内化》培训会纪要", "lastReportReplyId": null, "lastReplyContent": "培训主题: 《个人BP实战培训:从认知到内化》主讲讲师: 杨晶晶 参训人员:研发人员共27人 时间地...", "lastReplyTime": "2026-03-20 18:38:01", "replyEmployeeName": "许敏玲", "replyEmployeeDeptAndTitle": "[人力资源部-人事专员]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2034920526136557570", "reportTitle": "小龙虾项目进展汇报 (03.20)", "lastReportReplyId": null, "lastReplyContent": "### 核心进展摘要\n**本周聚焦解决用户反馈的 P0 级连接与交互痛点,确保基础可用性。下周起将通...", "lastReplyTime": "2026-03-20 17:12:04", "replyEmployeeName": "于耀", "replyEmployeeDeptAndTitle": "[产品部-产品经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2034839256127651842", "reportTitle": "关于对办公区域进行消杀的温馨提示", "lastReportReplyId": null, "lastReplyContent": "各位同事:\n\n大家好!行政管理部将于3月21日(周六) 16:00-18:20对办公区进行全面例行虫...", "lastReplyTime": "2026-03-20 11:47:42", "replyEmployeeName": "齐慧", "replyEmployeeDeptAndTitle": "[行政管理部-行政助理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2034811290719195137", "reportTitle": "内部培训通知 | 个人BP实战培训:从认知到内化", "lastReportReplyId": null, "lastReplyContent": "各位同事:大家好!为进一步提升大家对BP系统的了解,公司将于今日(3月20日)组织一场培训会,本次培...", "lastReplyTime": "2026-03-20 10:51:23", "replyEmployeeName": "许敏玲", "replyEmployeeDeptAndTitle": "[人力资源部-人事专员]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2034263550192873474", "reportTitle": "产品中心在参与投前投后核心岗位的说明书的整理-3月18号", "lastReportReplyId": null, "lastReplyContent": "附件是整理的产品中心在参与投前投后核心岗位的说明书,结构整体如下;总共给到的是8个部门的;应该还缺少财务部的。", "lastReplyTime": "2026-03-18 21:47:10", "replyEmployeeName": "黄登品", "replyEmployeeDeptAndTitle": "[开发组-iOS开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2033836797615677441", "reportTitle": "关于近期3项专项研发项目积分激励的公示", "lastReportReplyId": null, "lastReplyContent": " 为进一步完善公司积分激励体系,激发全员攻坚活力,结合公司既定的年度项目积分总池1200...", "lastReplyTime": "2026-03-17 17:26:47", "replyEmployeeName": "刘丽华", "replyEmployeeDeptAndTitle": "[行政管理部-行政经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2033795578580004866", "reportTitle": "03月17日-“小龙虾”产品问题复盘与事项讨论会", "lastReportReplyId": null, "lastReplyContent": "“小龙虾”产品问题复盘与事项讨论会 本次会议首先由于耀反馈了产品“小龙虾”在会话管理、文件功能...", "lastReplyTime": "2026-03-17 14:45:34", "replyEmployeeName": "王晓辉", "replyEmployeeDeptAndTitle": "[产品部-产品经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2033740812176945153", "reportTitle": "关于NoteX体验季培训资料准备及培训安排的汇报", "lastReportReplyId": "2033784535157927938", "lastReplyContent": "以上两个PDF文件都是由NoteX生成,导出PPT后会存在字体丢失,部分排版错位的情况,因此使用了P...", "lastReplyTime": "2026-03-17 13:56:34", "replyEmployeeName": "刘明霞", "replyEmployeeDeptAndTitle": "[数据管理部-智能项目运营专员]", "type": "反馈"}, {"unReadCount": 1, "reportId": "2033512725291982849", "reportTitle": "AI费用控制和skills规范讨论", "lastReportReplyId": null, "lastReplyContent": " **会议时间**:2026年03月16日 18:19\n **会议主题**:AI模型成本控制与技术优...", "lastReplyTime": "2026-03-16 21:06:02", "replyEmployeeName": "屈军利", "replyEmployeeDeptAndTitle": "[开发组-部门经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2033391676235804674", "reportTitle": "api治理和skill规范讨论", "lastReportReplyId": null, "lastReplyContent": "**会议时间**:2026年3月16日 11:06\n **参会人员**:张成鹏、侯桐、屈军利、李广智...", "lastReplyTime": "2026-03-16 15:25:04", "replyEmployeeName": "屈军利", "replyEmployeeDeptAndTitle": "[开发组-部门经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2032398536188194818", "reportTitle": "🎙️ 会议纪要:AI 慧记功能优化与系统联调闭环会议", "lastReportReplyId": null, "lastReplyContent": "🎙️ 会议纪要:AI 慧记功能优化与系统联调闭环会议 会议时间:2026年3月13日 17:23 ...", "lastReplyTime": "2026-03-13 18:09:06", "replyEmployeeName": "于耀", "replyEmployeeDeptAndTitle": "[产品部-产品经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2031926941438828546", "reportTitle": "关于宋培众、程敏岗位调整的申请", "lastReportReplyId": null, "lastReplyContent": "您有阳叶菊新转发的汇报", "lastReplyTime": "2026-03-12 11:07:44", "replyEmployeeName": "阳叶菊", "replyEmployeeDeptAndTitle": "[人力资源部-人力资源部经理]", "type": "转发给我"}, {"unReadCount": 1, "reportId": "2031920424790896641", "reportTitle": "关于技术部组织架构调整的通知", "lastReportReplyId": null, "lastReplyContent": "各位领导、同事: 为进一步优化技术部门组织架构,提升团队协作效率与技术...", "lastReplyTime": "2026-03-12 10:53:14", "replyEmployeeName": "阳叶菊", "replyEmployeeDeptAndTitle": "[人力资源部-人力资源部经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2031606932007149570", "reportTitle": "03月11日-OpenClaw部署、运维优化与团队协作讨论会", "lastReportReplyId": null, "lastReplyContent": "AI产品研发与团队协作优化工作推进会 本次会议重点围绕公司AI产品的研发进展、运维部署...", "lastReplyTime": "2026-03-11 13:47:33", "replyEmployeeName": "王晓辉", "replyEmployeeDeptAndTitle": "[产品部-产品经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2030933317339086849", "reportTitle": "数据管理部运营企划与玄关AI功能周例会-会议纪要", "lastReportReplyId": null, "lastReplyContent": "数据管理部运营企划与玄关AI功能协同沟通会本次会议围绕Note X产品种子用户活动策划、训战平台运营...", "lastReplyTime": "2026-03-09 17:34:09", "replyEmployeeName": "刘明霞", "replyEmployeeDeptAndTitle": "[数据管理部-智能项目运营专员]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2029502366440722434", "reportTitle": "关于对办公区域进行消杀的温馨提示", "lastReportReplyId": null, "lastReplyContent": "各位同事:\n\n大家好!行政管理部将于3月7日(周六) 16:00-18:20对办公区进行全面例行虫害...", "lastReplyTime": "2026-03-05 18:21:01", "replyEmployeeName": "齐慧", "replyEmployeeDeptAndTitle": "[行政管理部-行政助理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2029121916283097089", "reportTitle": "关于“院外北极星”管理系统项目里程碑验收及项目评分的公示", "lastReportReplyId": null, "lastReplyContent": "各位同事: 大家好!我司积分激励试点项目之一“院外北极星”管理系统项目现已完成本年度一阶...", "lastReplyTime": "2026-03-04 17:58:08", "replyEmployeeName": "刘丽华", "replyEmployeeDeptAndTitle": "[行政管理部-行政经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2028788354698809345", "reportTitle": "关于TBS训战系统项目一阶段里程碑验收及项目评分的公示", "lastReportReplyId": null, "lastReplyContent": "各位同事: 大家好!我司积分激励试点项目之一TBS训战系统项目历经2个月的攻坚推进,现已...", "lastReplyTime": "2026-03-03 19:32:58", "replyEmployeeName": "刘丽华", "replyEmployeeDeptAndTitle": "[行政管理部-行政经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2028766170592718850", "reportTitle": "OpenClaw技术问题讨论与任务分工会议纪要", "lastReportReplyId": null, "lastReplyContent": "\n # OpenClaw技术问题讨论与任务分工会议纪要\n\n ## 会议基本信息\n - **会议主题*...", "lastReplyTime": "2026-03-03 17:55:15", "replyEmployeeName": "王晓辉", "replyEmployeeDeptAndTitle": "[产品部-产品经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2027685838514872321", "reportTitle": "为notex提供选择文件及选择上传路径组件功能", "lastReportReplyId": null, "lastReplyContent": "为notex提供选择文件及提供选择上传路径组件功能,具体对接文档参见附件选择文件示例: https:...", "lastReplyTime": "2026-02-28 18:10:20", "replyEmployeeName": "刘艳华", "replyEmployeeDeptAndTitle": "[开发组-技术支持]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2021840587996848130", "reportTitle": "BP体系相关信息分享汇报", "lastReportReplyId": null, "lastReplyContent": "大家好,BP体系的相关信息现分享给大家,内容如下:1、BP体系说明稿-前言、BP体系说明稿(简洁版)...", "lastReplyTime": "2026-02-12 15:01:47", "replyEmployeeName": "封敏", "replyEmployeeDeptAndTitle": "[产品部-测试工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2021164293239644162", "reportTitle": "新春内购场 | 囤禾零,26全年都「稳」了!", "lastReportReplyId": null, "lastReplyContent": "朋友,休假了伐?禾零囤了伐?别着急离开,下滑告诉你新春必囤禾零的四个理由(诚心版)肌肤返乡综合征?·...", "lastReplyTime": "2026-02-10 18:15:53", "replyEmployeeName": "邝焕然", "replyEmployeeDeptAndTitle": "[货架电商组1-私域运营专员-ML]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2019681273408487425", "reportTitle": "关于对办公区域进行消杀的温馨提示", "lastReportReplyId": null, "lastReplyContent": "各位同事:\n\n大家好!行政管理部将于2月7日(周六) 16:00-18:20对办公区进行全面例行虫害...", "lastReplyTime": "2026-02-06 15:55:21", "replyEmployeeName": "齐慧", "replyEmployeeDeptAndTitle": "[行政管理部-行政助理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2019256280589631490", "reportTitle": "IndexTTS(文本合成语音-带声音复刻) 性能分析报告", "lastReportReplyId": null, "lastReplyContent": "# IndexTTS 性能分析报告 (2026-02)\n\n## 1. 概述\n本报告基于实测数据,分析...", "lastReplyTime": "2026-02-05 16:36:08", "replyEmployeeName": "成伟", "replyEmployeeDeptAndTitle": "[技术部-首席架构师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2018260217757757441", "reportTitle": "玄关开发者平台使用通知", "lastReportReplyId": null, "lastReplyContent": "功能简介:1、玄关内部开发文档:提供玄关特有的开发规范文档,让大家使用AI写代码的时候可以把相应的规...", "lastReplyTime": "2026-02-02 19:08:00", "replyEmployeeName": "屈军利", "replyEmployeeDeptAndTitle": "[开发组-部门经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2018232746714918914", "reportTitle": " AI 图像中文文字清晰度问题解决指南", "lastReportReplyId": null, "lastReplyContent": "已更新维护到到文档: https://cwork-web-test.xgjktech.com.cn/...", "lastReplyTime": "2026-02-02 16:00:12", "replyEmployeeName": "于耀", "replyEmployeeDeptAndTitle": "[产品部-产品经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2017110397726887938", "reportTitle": "关于2026年春节放假调休安排的通知", "lastReportReplyId": null, "lastReplyContent": "正文:各位领导、同事:根据国家法定节假日安排的相关规定,现将2026年春节的放假调休日期通知如下,请...", "lastReplyTime": "2026-01-30 18:15:32", "replyEmployeeName": "许敏玲", "replyEmployeeDeptAndTitle": "[人力资源部-人事专员]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2016408793910714369", "reportTitle": "【南头莲花物业通知】关于南头莲花广场大厦停车场3号出入口地面施工的通知", "lastReportReplyId": null, "lastReplyContent": "各位同事,大家好! 南头城莲花广场将于2026年1月29日至2月1日对3号出入口(东面)进行...", "lastReplyTime": "2026-01-28 15:18:19", "replyEmployeeName": "齐慧", "replyEmployeeDeptAndTitle": "[行政管理部-行政助理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2014579884537782274", "reportTitle": "天猫新店开业 | 员工专属内购福利!", "lastReportReplyId": null, "lastReplyContent": "好消息!喜辽妥全新天猫美妆旗舰店正式开业!特此推出 「员工内购专场」以最优惠的价格把心仪好物带回家!...", "lastReplyTime": "2026-01-26 10:12:41", "replyEmployeeName": "吴志雄", "replyEmployeeDeptAndTitle": "[货架电商组2-电商运营专员-ML]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2014598674750955521", "reportTitle": "项目奖金规则讨论-20260123", "lastReportReplyId": null, "lastReplyContent": "会议主题:项目奖金讨论会议地点:银河会议时间:18:00-19:00参会人:阳叶菊、许敏玲、刘丽华、...", "lastReplyTime": "2026-01-23 19:14:17", "replyEmployeeName": "阳叶菊", "replyEmployeeDeptAndTitle": "[人力资源部-人力资源部经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2014602027392360450", "reportTitle": "关于对办公区域进行消杀的温馨提示", "lastReportReplyId": null, "lastReplyContent": "各位同事:\n\n大家好!行政管理部将于1月24日(周六) 16:00-18:20对办公区进行全面例行虫...", "lastReplyTime": "2026-01-23 15:33:24", "replyEmployeeName": "齐慧", "replyEmployeeDeptAndTitle": "[行政管理部-行政助理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2014544465397915650", "reportTitle": "「限时内购」1月新年超强折扣,喜辽妥多磺酸粘多糖凝胶·新品上市!", "lastReportReplyId": null, "lastReplyContent": "工作描述惊呆了家人们! Hirudoid喜辽妥多磺酸粘多糖新款凝胶·新品上市。超强折扣“内购上线”啦...", "lastReplyTime": "2026-01-23 15:32:52", "replyEmployeeName": "易大宇", "replyEmployeeDeptAndTitle": "[跨境组-电商运营专员-ML]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2013512813896769538", "reportTitle": "BP三种拆解承接方案", "lastReportReplyId": null, "lastReplyContent": "\n ## BP三种拆解承接方案\n\n ### 方案一:公司层级汇总模式\n - **特点**:由公司层面...", "lastReplyTime": "2026-01-20 15:39:28", "replyEmployeeName": "封敏", "replyEmployeeDeptAndTitle": "[产品部-测试工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2013139565405077506", "reportTitle": "向量方案", "lastReportReplyId": null, "lastReplyContent": "1. - 康哲产品知识库建议使用向量。产品知识库均为根据检索到产品知识内容来回答用户,一般不会涉及多...", "lastReplyTime": "2026-01-19 15:17:12", "replyEmployeeName": "程敏", "replyEmployeeDeptAndTitle": "[AI及数据运维组-数据架构师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2009564847569997825", "reportTitle": "咖啡精选(第一期)2026 特辑・续命口粮召唤令☕️", "lastReportReplyId": null, "lastReplyContent": "# ☕️☕️☕️☕️☕️☕️☕️☕️☕️☕️☕️☕️\n\n## 各位早八续命党、咖啡控、工位干饭咖:\n...", "lastReplyTime": "2026-01-09 18:20:22", "replyEmployeeName": "薛芳芳", "replyEmployeeDeptAndTitle": "[质量安全部-运营支持经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2009562310850514945", "reportTitle": "周报", "lastReportReplyId": null, "lastReplyContent": "本周完成工作内容:1、为了审核通过首页禁用了下拉刷新、热门应用的“换一批”隐藏、历史记录隐藏、修改了...", "lastReplyTime": "2026-01-09 17:49:45", "replyEmployeeName": "杨婷", "replyEmployeeDeptAndTitle": "[开发组-Android开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2009531854423953410", "reportTitle": "关于办公区域例行虫害消杀的通知", "lastReportReplyId": null, "lastReplyContent": "各位同事: 大家好!为营造整洁、卫生的办公环境,公司定于1 月 10 日(本周六)16:...", "lastReplyTime": "2026-01-09 15:58:07", "replyEmployeeName": "刘丽华", "replyEmployeeDeptAndTitle": "[行政管理部-行政经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2008843850445148161", "reportTitle": "周报", "lastReportReplyId": null, "lastReplyContent": "本周已完成:1、AI慧记录音功能开发;2、AI慧记录音动画开发;3、AI慧记-会中回顾开发;下周工作...", "lastReplyTime": "2026-01-07 18:12:38", "replyEmployeeName": "杨婷", "replyEmployeeDeptAndTitle": "[开发组-Android开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2004517902786596866", "reportTitle": "周报", "lastReportReplyId": null, "lastReplyContent": "本周完成工作:1、粤通网安监测〔2025〕815号 药企圈3.1.8问题整改;2、医院开发管理sch...", "lastReplyTime": "2025-12-26 19:44:43", "replyEmployeeName": "杨婷", "replyEmployeeDeptAndTitle": "[开发组-Android开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2003726713187840002", "reportTitle": "关于2026年部分节假日放假调休安排的通知", "lastReportReplyId": null, "lastReplyContent": "正文:各位领导、同事:根据国家法定节假日安排的相关规定,现将2026年部分节假日的放假调休日期通知如...", "lastReplyTime": "2025-12-26 18:02:11", "replyEmployeeName": "许敏玲", "replyEmployeeDeptAndTitle": "[人力资源部-人事专员]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2001201446103777281", "reportTitle": "❄️冬日暖时光 · 茶歇活动通知 ❄️", "lastReportReplyId": null, "lastReplyContent": "亲爱的同事们: 寒意渐深,正是用温暖的美食与陪伴,暖心暖胃的好时候...", "lastReplyTime": "2025-12-17 16:03:00", "replyEmployeeName": "齐慧", "replyEmployeeDeptAndTitle": "[行政管理部-行政助理]", "type": "汇报提交通知"}, {"unReadCount": 3, "reportId": "1998665931114762241", "reportTitle": "AI问答超出token也能进行问答", "lastReportReplyId": "2000772484957323266", "lastReplyContent": "@伍孝权 @宋培众 @罗涛 超出模型token数提示,发送info类型消息格式:\n{\n \"id...", "lastReplyTime": "2025-12-16 11:38:28", "replyEmployeeName": "龙继海", "replyEmployeeDeptAndTitle": "[开发组-Java开发工程师]", "type": "@我的回复"}, {"unReadCount": 1, "reportId": "2000757180139421698", "reportTitle": "【重要】关于优化办公区域垃圾桶配置及垃圾处理规范的通知", "lastReportReplyId": null, "lastReplyContent": "各位同事:\n为进一步维护办公区域环境整洁,减少资源浪费,提升办公空间利用率,经统筹规划,现对办公区域...", "lastReplyTime": "2025-12-16 11:17:36", "replyEmployeeName": "刘丽华", "replyEmployeeDeptAndTitle": "[行政管理部-行政经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2000441171331366914", "reportTitle": "咖啡精选(第十一期)-✨ Member's Mark 经典意式拼配咖啡豆", "lastReportReplyId": null, "lastReplyContent": "亲爱的咖啡爱好者们:\n\n 岁末将至,用一杯醇厚浓郁的意式咖啡温暖这个冬日!咖啡兴趣小组第十...", "lastReplyTime": "2025-12-15 13:51:03", "replyEmployeeName": "薛芳芳", "replyEmployeeDeptAndTitle": "[质量安全部-运营支持经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2000392034837708801", "reportTitle": "周报", "lastReportReplyId": null, "lastReplyContent": "本周完成工作:\n1、修复实际填报驳回actual 错误的调用了plan的API;\n2、表格增加单元...", "lastReplyTime": "2025-12-15 10:34:09", "replyEmployeeName": "杨婷", "replyEmployeeDeptAndTitle": "[开发组-Android开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1999719955351961601", "reportTitle": "会议纪要|春节活动(Line B)实施计划1212 ", "lastReportReplyId": null, "lastReplyContent": "\n> **日期**:2025年12月12日[@12月12日春节活动实施(并行线)](huiJiId=...", "lastReplyTime": "2025-12-13 14:18:57", "replyEmployeeName": "罗毓娴", "replyEmployeeDeptAndTitle": "[运营部-内容运营]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1997838780702203906", "reportTitle": "周报", "lastReportReplyId": null, "lastReplyContent": "本周已完成工作:\n1、HcpSelector组件如果productId为空时,根据hcoId去重显示...", "lastReplyTime": "2025-12-08 09:25:29", "replyEmployeeName": "杨婷", "replyEmployeeDeptAndTitle": "[开发组-Android开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1996914284638220289", "reportTitle": "AI写代码分享会", "lastReportReplyId": null, "lastReplyContent": "**会议主题:** AI写代码分享会\n **会议时间:** 12月05日\n **参会人员:** 开发...", "lastReplyTime": "2025-12-05 20:16:43", "replyEmployeeName": "屈军利", "replyEmployeeDeptAndTitle": "[开发组-部门经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1996891823225991169", "reportTitle": "新投前系统周报1205", "lastReportReplyId": null, "lastReplyContent": "项目目标\n目标待定,当前聚焦其他更高优先级事项。\n当前进展\n1、商机到立项会阶段功能,项目管理部、B...", "lastReplyTime": "2025-12-05 19:16:59", "replyEmployeeName": "屈军利", "replyEmployeeDeptAndTitle": "[开发组-部门经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1996891344014127106", "reportTitle": "周报", "lastReportReplyId": null, "lastReplyContent": "\n ## 本周工作总结\n\n ### 1. 医生圈APP集成AI翻译功能\n - 实现翻译结果通过通知和...", "lastReplyTime": "2025-12-05 18:39:04", "replyEmployeeName": "伍孝权", "replyEmployeeDeptAndTitle": "[开发组-Android开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1995702833160282114", "reportTitle": "关于深圳康哲在集团内部招聘区域管理经理的通知", "lastReportReplyId": null, "lastReplyContent": "尊敬的各位领导、同事:\n 根据公司发展战略,深康区域管理部拟面向集团内部招聘区域管理经理2名。...", "lastReplyTime": "2025-12-02 13:56:08", "replyEmployeeName": "石玥", "replyEmployeeDeptAndTitle": "[组织发展部-组织发展专员]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1994248441147953153", "reportTitle": "AI修订方案", "lastReportReplyId": null, "lastReplyContent": "\n\n ## 一、整体技术方案\n\n ### 1. 基于最新 LangChain 1.0 和 LangG...", "lastReplyTime": "2025-11-28 18:46:03", "replyEmployeeName": "李广智", "replyEmployeeDeptAndTitle": "[AI及数据运维组-技术总监]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1994327770527850498", "reportTitle": "周报", "lastReportReplyId": null, "lastReplyContent": "本周已完成:\n1、schema配置界面增加导入和导出功能;\n2、数据采集增加新增逻辑;\n3、解决只保...", "lastReplyTime": "2025-11-28 16:52:21", "replyEmployeeName": "杨婷", "replyEmployeeDeptAndTitle": "[开发组-Android开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1992906964375592962", "reportTitle": "敏肌闭眼囤!禾零【5折内购】润护危「肌」", "lastReportReplyId": null, "lastReplyContent": "「小雪」封尘、「大雪」藏寒\n即将迎来节气「大雪」\n干敏肌的换季危「肌」也跟着翻倍到来!\n角质层越冷越...", "lastReplyTime": "2025-11-24 18:48:00", "replyEmployeeName": "邝焕然", "replyEmployeeDeptAndTitle": "[货架电商组1-私域运营专员-ML]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1991810085504049153", "reportTitle": "周报", "lastReportReplyId": null, "lastReplyContent": "本周完成工作\n1、CMS 智慧聊天内容分享\n2、医生圈Ai翻译优化功能开发\n3、工作说说TBS 功能...", "lastReplyTime": "2025-11-21 18:11:51", "replyEmployeeName": "伍孝权", "replyEmployeeDeptAndTitle": "[开发组-Android开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1991806614931521538", "reportTitle": "新投前系统周报1121", "lastReportReplyId": null, "lastReplyContent": "项目目标\n目标待定,当前聚焦其他更高优先级事项。\n当前进展\n1、商机到立项会阶段功能,项目管理部、B...", "lastReplyTime": "2025-11-21 17:59:01", "replyEmployeeName": "屈军利", "replyEmployeeDeptAndTitle": "[开发组-部门经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1991801812776402946", "reportTitle": "周总结", "lastReportReplyId": null, "lastReplyContent": "本周完成工作:\n1、解决了FieldSelector多医院模式下,之前选中医生再次选择时没有勾选的问...", "lastReplyTime": "2025-11-21 17:38:32", "replyEmployeeName": "杨婷", "replyEmployeeDeptAndTitle": "[开发组-Android开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1991749627266850817", "reportTitle": "关于对办公区域进行消杀的温馨提示", "lastReportReplyId": null, "lastReplyContent": "各位同事:\n\n大家好!行政管理部将于11月22日(周六) 16:00-18:20对办公区进行全面例行...", "lastReplyTime": "2025-11-21 14:10:15", "replyEmployeeName": "齐慧", "replyEmployeeDeptAndTitle": "[行政管理部-行政助理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1989290113997393921", "reportTitle": "新投前系统周报1114", "lastReportReplyId": null, "lastReplyContent": "项目目标\n目标待定,当前聚焦其他更高优先级事项。\n当前进展\n1、商机到立项会阶段功能,项目管理部、B...", "lastReplyTime": "2025-11-14 19:21:08", "replyEmployeeName": "屈军利", "replyEmployeeDeptAndTitle": "[开发组-部门经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1989278978539782146", "reportTitle": "周报", "lastReportReplyId": null, "lastReplyContent": "\n\n\n 本周完成工作:\n 1、21.5寸屏通用版桌面定制化改造,去掉桌面用户注册流程 2、冻结内置应...", "lastReplyTime": "2025-11-14 18:31:54", "replyEmployeeName": "伍孝权", "replyEmployeeDeptAndTitle": "[开发组-Android开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1989266651975176193", "reportTitle": "周报", "lastReportReplyId": null, "lastReplyContent": "本周工作总结:\n1、schema配置中详情由列表返回改为请求接口,增加修改人显示;\n2、FieldS...", "lastReplyTime": "2025-11-14 17:46:22", "replyEmployeeName": "杨婷", "replyEmployeeDeptAndTitle": "[开发组-Android开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1979118129451651074", "reportTitle": "9月28日-10月17日产品部汇报项目情况", "lastReportReplyId": null, "lastReplyContent": "您有蹇晓枫新转发的汇报", "lastReplyTime": "2025-10-20 18:08:34", "replyEmployeeName": "蹇晓枫", "replyEmployeeDeptAndTitle": "[产品部-产品经理]", "type": "转发给我"}, {"unReadCount": 1, "reportId": "1980180461518868482", "reportTitle": "技术中心本周重点事项-1020", "lastReportReplyId": null, "lastReplyContent": "会议地点:漫度\n会议时间:10:45-11:15\n参会人:屈军利、宋培众、李广智、成伟、周皓、黄登品...", "lastReplyTime": "2025-10-20 16:20:37", "replyEmployeeName": "屈军利", "replyEmployeeDeptAndTitle": "[开发组-部门经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1979026557934755842", "reportTitle": "iOS 1.9.11", "lastReportReplyId": "1980173023285903362", "lastReplyContent": "10月20日:工作说说-首页-汇报任务-点击详情“可查看任务详情。\n结论:已修复", "lastReplyTime": "2025-10-20 15:23:33", "replyEmployeeName": "罗毓娴", "replyEmployeeDeptAndTitle": "[运营部-内容运营]", "type": "我收到的回复"}, {"unReadCount": 1, "reportId": "1979154756391829506", "reportTitle": "会议纪要:AI写汇报历史问题处理进展与void测试结果分享10月17日", "lastReportReplyId": null, "lastReplyContent": "## 会议概述\n- **时间**:2025-10-17 17:35-18:06\n- **主题**:A...", "lastReplyTime": "2025-10-17 20:43:53", "replyEmployeeName": "罗毓娴", "replyEmployeeDeptAndTitle": "[运营部-内容运营]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1979162651644952577", "reportTitle": "会议纪要:AI写汇报系统优化问题讨论与解决方案制定-10月16日", "lastReportReplyId": null, "lastReplyContent": "## 会议概述\n\n- **会议时间**:2025年10月16日 18:18:17 - 18:56:1...", "lastReplyTime": "2025-10-17 20:43:35", "replyEmployeeName": "罗毓娴", "replyEmployeeDeptAndTitle": "[运营部-内容运营]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1979142117867683842", "reportTitle": "AI能力跟踪进展1017", "lastReportReplyId": null, "lastReplyContent": "AI能力负责人和产品经理对当前AI能力存在的痛点进行了沟通,确定了目前需要重点跟进研究的AI能力问题...", "lastReplyTime": "2025-10-17 19:19:01", "replyEmployeeName": "屈军利", "replyEmployeeDeptAndTitle": "[开发组-部门经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1979130553500192769", "reportTitle": "周报", "lastReportReplyId": null, "lastReplyContent": "# 本周工作完成汇报\n\n## 1. 工作说说应用更新\n\n\n- Android内网版本也已完成Spla...", "lastReplyTime": "2025-10-17 18:24:52", "replyEmployeeName": "伍孝权", "replyEmployeeDeptAndTitle": "[开发组-Android开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1979115411748179969", "reportTitle": "移动端周报-黄登品-10月17号", "lastReportReplyId": null, "lastReplyContent": "# 本周工作总结\n\n## 主要工作内容\n\n### 1. 创研药品/试剂出入库系统需求沟通(10月15...", "lastReplyTime": "2025-10-17 17:32:42", "replyEmployeeName": "黄登品", "replyEmployeeDeptAndTitle": "[开发组-iOS开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1979114805969190914", "reportTitle": "周报", "lastReportReplyId": null, "lastReplyContent": "# 工作周报\n\n## 本周工作完成情况\n\n1. **维盛医院管理优化**\n - 实际结果填报周期...", "lastReplyTime": "2025-10-17 17:20:32", "replyEmployeeName": "杨婷", "replyEmployeeDeptAndTitle": "[开发组-Android开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1979102944280346625", "reportTitle": "VOID试用体验", "lastReportReplyId": null, "lastReplyContent": "\n\n使用gpt模型。 \n第一个问题先让void对代码进行一些调整,回答要我补充代码。无法得到想要的结...", "lastReplyTime": "2025-10-17 17:09:45", "replyEmployeeName": "马超平", "replyEmployeeDeptAndTitle": "[AI及数据运维组-Android开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1978465055087411202", "reportTitle": "会议纪要:AI汇报助手(问题复盘&进展同步)-10月15日", "lastReportReplyId": null, "lastReplyContent": "# 10月15日AI写汇报 会议纪要\n\n## 概述\n\n **时间**:2025-10-15 18:1...", "lastReplyTime": "2025-10-15 22:56:22", "replyEmployeeName": "罗毓娴", "replyEmployeeDeptAndTitle": "[运营部-内容运营]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1978053570662105089", "reportTitle": "舒护蕊双11闺蜜团内购,专宠爱自己的你!", "lastReportReplyId": null, "lastReplyContent": "\n舒护蕊双11闺蜜团内购,专宠爱自己的你!\n\n纯天然护理液,呵护私处健康!想要的精准下单\n ...", "lastReplyTime": "2025-10-15 14:35:04", "replyEmployeeName": "李威威", "replyEmployeeDeptAndTitle": "[禾零组-平面设计师-ML]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1976912762701479937", "reportTitle": "新投前系统周报1011", "lastReportReplyId": null, "lastReplyContent": "项目目标\n新投前系统要在2025年上线,包含从商机上报到终审会完整流程(今天上午会议和康康沟通,她说...", "lastReplyTime": "2025-10-11 16:36:16", "replyEmployeeName": "屈军利", "replyEmployeeDeptAndTitle": "[开发组-部门经理]", "type": "汇报提交通知"}, {"unReadCount": 2, "reportId": "1969276534476877826", "reportTitle": "iOS 1.9.10", "lastReportReplyId": "1971396385093816322", "lastReplyContent": "无异议", "lastReplyTime": "2025-09-26 10:08:20", "replyEmployeeName": "罗涛", "replyEmployeeDeptAndTitle": "", "type": "我收到的回复"}, {"unReadCount": 1, "reportId": "1971110717721673729", "reportTitle": "【南头莲花广场信息分享】南头莲花广场中秋活动分享", "lastReportReplyId": null, "lastReplyContent": "各位同事,大家好!\n 南头城莲花广场为感谢客户的支持,特别筹备“月满中秋,情系莲花”的中秋活...", "lastReplyTime": "2025-09-25 15:25:24", "replyEmployeeName": "齐慧", "replyEmployeeDeptAndTitle": "[行政管理部-行政助理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1970769795544616962", "reportTitle": "关于9月25日恢复正常办公的通知", "lastReportReplyId": null, "lastReplyContent": "各位领导、同事:\n根据气象部门最新通报,今年第 18 号台风 “桦加沙” 对深圳的影响已逐渐减弱。深...", "lastReplyTime": "2025-09-24 16:41:50", "replyEmployeeName": "阳叶菊", "replyEmployeeDeptAndTitle": "[人力资源部-人力资源部经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1970317662428958722", "reportTitle": "关于续签订劳动合同员工及时查阅员工手册的通知-2025年9月", "lastReportReplyId": null, "lastReplyContent": "各位同事:\n 大家好!近期正在和各位员工进行劳动合同续签,为了让大家更加清晰地了解公司各...", "lastReplyTime": "2025-09-23 10:45:11", "replyEmployeeName": "阳叶菊", "replyEmployeeDeptAndTitle": "[人力资源部-人力资源部经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1970028027840712706", "reportTitle": "国庆出游「零」烦恼出片攻略 | 9月禾零【5折福利】get护肤搭子", "lastReportReplyId": null, "lastReplyContent": "国庆长假想好去哪玩了吗?\n出游想要轻松出片,那好皮肤可不能少~\n\n「禾零舒缓系列」皮肤情绪稳定好搭子...", "lastReplyTime": "2025-09-22 15:50:42", "replyEmployeeName": "李宁宁", "replyEmployeeDeptAndTitle": "", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1968984306546298882", "reportTitle": "周报", "lastReportReplyId": null, "lastReplyContent": " # 工作周报\n\n ## 任务进展\n\n ### 1. 药企圈社区点赞优化\n\n 已完成点赞功能优化,主...", "lastReplyTime": "2025-09-19 18:26:55", "replyEmployeeName": "伍孝权", "replyEmployeeDeptAndTitle": "[开发组-Android开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1968980156483743745", "reportTitle": "周报", "lastReportReplyId": null, "lastReplyContent": " # 工作周报\n\n ## 本周完成工作\n\n ### CRM客户拜访提供在线帮助功能\n\n 该任务已全部...", "lastReplyTime": "2025-09-19 18:10:47", "replyEmployeeName": "杨婷", "replyEmployeeDeptAndTitle": "[开发组-Android开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1968966589156085761", "reportTitle": "关于对办公区域进行消杀的温馨提示", "lastReportReplyId": null, "lastReplyContent": "各位同事:\n\n大家好!行政管理部将9月20日(周六) 16:00-18:20对办公区进行全面例行虫害...", "lastReplyTime": "2025-09-19 17:19:39", "replyEmployeeName": "齐慧", "replyEmployeeDeptAndTitle": "[行政管理部-行政助理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1968641743339642882", "reportTitle": "咖啡兴趣小组第十期精选豆单集结令——醇香深夏", "lastReportReplyId": null, "lastReplyContent": "亲爱的咖啡爱好者们:\n\n 盛夏已至,一杯风味独特的咖啡是穿越炎热的绝佳伴侣。咖啡兴趣小组第九期精选豆...", "lastReplyTime": "2025-09-18 19:42:56", "replyEmployeeName": "薛芳芳", "replyEmployeeDeptAndTitle": "[质量安全部-运营支持经理]", "type": "汇报提交通知"}, {"unReadCount": 4, "reportId": "1922966218137034753", "reportTitle": "iOS客户端1.8.9发布说明", "lastReportReplyId": null, "lastReplyContent": "建议人李志洪点击忽略,原因:无需处理", "lastReplyTime": "2025-09-16 10:00:19", "replyEmployeeName": "李志洪", "replyEmployeeDeptAndTitle": "[产品部-UI设计师]", "type": "工作汇报"}, {"unReadCount": 1, "reportId": "1967540261378973698", "reportTitle": "官网问答现在处理过程", "lastReportReplyId": null, "lastReplyContent": "官网问答:\n1.dify意图识别,现在优如下9中类别\n intent == \"product\"...", "lastReplyTime": "2025-09-15 18:48:44", "replyEmployeeName": "程敏", "replyEmployeeDeptAndTitle": "[AI及数据运维组-数据架构师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1967492777902092290", "reportTitle": "技术中心本周重点事项-0915", "lastReportReplyId": null, "lastReplyContent": "会议地点:水星\n会议时间:14:00-15:00\n参会人:屈军利、宋培众、李广智、成伟、周皓、黄登品...", "lastReplyTime": "2025-09-15 16:55:11", "replyEmployeeName": "屈军利", "replyEmployeeDeptAndTitle": "[开发组-部门经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1955896086929625090", "reportTitle": "HUIJI 服务接口", "lastReportReplyId": null, "lastReplyContent": "[POST] /ai-huiji/xgMeeting/nologin/addMeetingVideo...", "lastReplyTime": "2025-09-15 16:40:06", "replyEmployeeName": "马超平", "replyEmployeeDeptAndTitle": "[AI及数据运维组-Android开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1967422235631484930", "reportTitle": "腾讯会议AI托管功能分析", "lastReportReplyId": null, "lastReplyContent": "背景:\n基于腾讯 9 月 11 日上线的 “AI 托管” 新需求,其核心功能是,当你因忙碌而没有时间...", "lastReplyTime": "2025-09-15 16:11:11", "replyEmployeeName": "王晓辉", "replyEmployeeDeptAndTitle": "[产品部-产品经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1967413150232150018", "reportTitle": "SFE数据采集系统任务管理设计文档", "lastReportReplyId": null, "lastReplyContent": "\n ## 1. Schema定义\n\n 深西康系统的schema定义(后续代码实现可能需要根据sche...", "lastReplyTime": "2025-09-15 10:35:13", "replyEmployeeName": "成伟", "replyEmployeeDeptAndTitle": "[技术部-首席架构师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1966468273637707777", "reportTitle": "新投前项目开发周报0913", "lastReportReplyId": null, "lastReplyContent": "## 项目整体进展\n\n 项目已完成内部系统验收,本周主要集中在演示PPT制作、演示脚本编写和系统功能...", "lastReplyTime": "2025-09-12 19:52:30", "replyEmployeeName": "屈军利", "replyEmployeeDeptAndTitle": "[开发组-部门经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1966447939261984770", "reportTitle": "周报", "lastReportReplyId": null, "lastReplyContent": " # 工作周报\n\n ## 本周工作内容\n\n ### 医生圈鸿蒙版本\n - 修复打开康哲云平台合同...", "lastReplyTime": "2025-09-12 18:24:58", "replyEmployeeName": "伍孝权", "replyEmployeeDeptAndTitle": "[开发组-Android开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 2, "reportId": "1960632901519495169", "reportTitle": "体检通知 | 2025年体检将于9月1日开启,预约通道已开放,请大家及时预约参加", "lastReportReplyId": "1965656120484003841", "lastReplyContent": "👋 温馨提示:大家好,年度体检可预约日期截止10月1日,请尚未完成体检的同事们,尽快安排时间预约完成体检哟 ", "lastReplyTime": "2025-09-10 13:58:34", "replyEmployeeName": "许敏玲", "replyEmployeeDeptAndTitle": "[人力资源部-人事专员]", "type": "@我的回复"}, {"unReadCount": 1, "reportId": "1965281012468453377", "reportTitle": "iOS复制粘贴的功能", "lastReportReplyId": "1965339160193892353", "lastReplyContent": "已确认", "lastReplyTime": "2025-09-09 16:59:05", "replyEmployeeName": "欧倩", "replyEmployeeDeptAndTitle": "[产品部-测试工程师]", "type": "我收到的回复"}, {"unReadCount": 1, "reportId": "1965281129527283714", "reportTitle": "支持Markdwon表格", "lastReportReplyId": "1965339138311974913", "lastReplyContent": "已确认", "lastReplyTime": "2025-09-09 16:58:59", "replyEmployeeName": "欧倩", "replyEmployeeDeptAndTitle": "[产品部-测试工程师]", "type": "我收到的回复"}, {"unReadCount": 1, "reportId": "1964692302022848514", "reportTitle": "8月OpenAI模型更新情况跟踪", "lastReportReplyId": null, "lastReplyContent": "## 更新概述\n\n8月5日推出开源大模型GPT-OSS 120B以及GPT-OSS-20B。\n8月7...", "lastReplyTime": "2025-09-07 23:44:11", "replyEmployeeName": "屈军利", "replyEmployeeDeptAndTitle": "[开发组-部门经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1964655207006769153", "reportTitle": "新投前项目开发周报0905", "lastReportReplyId": null, "lastReplyContent": "## 项目整体进展\n 项目已完成验收阶段,PC端流程验证完整顺畅,手机端商机上报提示词需要进一步优化...", "lastReplyTime": "2025-09-07 19:47:06", "replyEmployeeName": "屈军利", "replyEmployeeDeptAndTitle": "[开发组-部门经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1963914701167398914", "reportTitle": "周报", "lastReportReplyId": null, "lastReplyContent": " # 本周工作汇报\n ## 大屏录制引导提示\n 完成了大屏录制引导提示的设计与实现工作。当用户点击录...", "lastReplyTime": "2025-09-05 18:38:47", "replyEmployeeName": "伍孝权", "replyEmployeeDeptAndTitle": "[开发组-Android开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1963875657322762241", "reportTitle": "关于对办公区域进行消杀的温馨提示", "lastReportReplyId": null, "lastReplyContent": "各位同事:\n\n大家好!行政管理部将于9月6日(周六) 16:00-18:20对办公区进行全面例行虫害...", "lastReplyTime": "2025-09-05 16:03:49", "replyEmployeeName": "齐慧", "replyEmployeeDeptAndTitle": "[行政管理部-行政助理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1963799865234460674", "reportTitle": "周报", "lastReportReplyId": null, "lastReplyContent": " # 工作周报\n\n ## 本周已完成\n 1. 完成了康联达CRM协同随访特性的全部功能开发工作,包括...", "lastReplyTime": "2025-09-05 11:09:52", "replyEmployeeName": "杨婷", "replyEmployeeDeptAndTitle": "[开发组-Android开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1963541823490101250", "reportTitle": "维盛结构化提取变更以及后续处理逻辑", "lastReportReplyId": null, "lastReplyContent": "已完成:提取数据新增品种,以及学术会议关联品种等逻辑处理。\n", "lastReplyTime": "2025-09-04 17:58:11", "replyEmployeeName": "程敏", "replyEmployeeDeptAndTitle": "[AI及数据运维组-数据架构师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1962435389025325058", "reportTitle": "工作说说IOS", "lastReportReplyId": "1962457663396646913", "lastReplyContent": "已确认", "lastReplyTime": "2025-09-01 18:09:02", "replyEmployeeName": "欧倩", "replyEmployeeDeptAndTitle": "[产品部-测试工程师]", "type": "我收到的回复"}, {"unReadCount": 1, "reportId": "1962136348303589377", "reportTitle": "《项目进展汇报总结-8月29日》", "lastReportReplyId": null, "lastReplyContent": " # 项目进展汇报总结(2025年8月22日)\n\n 基于2025年8月22日的项目汇报内容,结合各负...", "lastReplyTime": "2025-08-31 22:16:44", "replyEmployeeName": "罗毓娴", "replyEmployeeDeptAndTitle": "[运营部-内容运营]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1961356449313820673", "reportTitle": "周报", "lastReportReplyId": null, "lastReplyContent": "本周已完成工作\n1、提交拜访计划调用GPT增加传参,调提示词;\n2、提交拜访计划GPT返回数据处理,...", "lastReplyTime": "2025-08-29 17:29:10", "replyEmployeeName": "杨婷", "replyEmployeeDeptAndTitle": "[开发组-Android开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1961280826125877250", "reportTitle": "维盛7-8月客户反馈主要问题汇总", "lastReportReplyId": null, "lastReplyContent": "归纳总结了ERP系统开发维盛客户第3季度聚焦客户功能后7-8月维盛客户反馈的主要问题,问题信息来源为...", "lastReplyTime": "2025-08-29 12:18:04", "replyEmployeeName": "庄诗吟", "replyEmployeeDeptAndTitle": "[产品部-测试工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1960612621912649729", "reportTitle": "【下午茶派对·能量加满通知】🧋✨", "lastReportReplyId": null, "lastReplyContent": "各位小伙伴注意啦~ \n今天(周四,8月28日)下午15:30, \n公司健身房即将变身「奶茶轰趴馆」!...", "lastReplyTime": "2025-08-28 10:07:36", "replyEmployeeName": "刘丽华", "replyEmployeeDeptAndTitle": "[行政管理部-行政经理]", "type": "汇报提交通知"}, {"unReadCount": 3, "reportId": "1958734435394486273", "reportTitle": " 培训不仅能学习实践AI工具,还能拿花生油和大米?8月27日总部V603/药企圈【718459】与你不见不散!(培训通知)", "lastReportReplyId": "1960639421338365953", "lastReplyContent": "【集团AI第一课培训视频回放提醒】\n各位领导,同事好,\n集团“AI提效季·第一课”培训视频回放及课件...", "lastReplyTime": "2025-08-27 17:44:00", "replyEmployeeName": "李文俏", "replyEmployeeDeptAndTitle": "[人才发展部-人才发展专员]", "type": "@我的回复"}, {"unReadCount": 1, "reportId": "1958858292226916354", "reportTitle": "新投前项目开发周报0822", "lastReportReplyId": null, "lastReplyContent": "## 项目整体目标\n 1. 打通商机挖掘、评级、派活与立项全流程,引入AI,提高工作效率\n 2. 8...", "lastReplyTime": "2025-08-22 20:02:12", "replyEmployeeName": "屈军利", "replyEmployeeDeptAndTitle": "[开发组-部门经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1958853368311152642", "reportTitle": "周报", "lastReportReplyId": null, "lastReplyContent": " # 康哲翻译需求进展报告\n\n ## 本周已完成事项\n\n ### 统计中心优化\n - 新增网页翻译统...", "lastReplyTime": "2025-08-22 19:26:51", "replyEmployeeName": "伍孝权", "replyEmployeeDeptAndTitle": "[开发组-Android开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1958821500517625858", "reportTitle": "周报", "lastReportReplyId": null, "lastReplyContent": " # 周报\n\n ## 本周已完成工作\n\n 1. 数据字典-字典分类增加scope字段显示标签;已完成...", "lastReplyTime": "2025-08-22 17:27:28", "replyEmployeeName": "杨婷", "replyEmployeeDeptAndTitle": "[开发组-Android开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1958490983352205313", "reportTitle": "医生圈处理声网sdk bitcode方式", "lastReportReplyId": null, "lastReplyContent": "cd /Users/dachen/Desktop/iOS/AgoraRtcEngine_Specia...", "lastReplyTime": "2025-08-21 19:27:14", "replyEmployeeName": "黄登品", "replyEmployeeDeptAndTitle": "[开发组-iOS开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1958462097021173762", "reportTitle": "咖啡兴趣小组第九期精选豆单集结令——醇香深夏,意式风情", "lastReportReplyId": null, "lastReplyContent": " # 咖啡兴趣小组第九期精选豆单集结令——醇香深夏,意式风情\n\n 亲爱的咖啡爱好者们:\n\n 盛夏已至...", "lastReplyTime": "2025-08-21 17:49:08", "replyEmployeeName": "薛芳芳", "replyEmployeeDeptAndTitle": "[质量安全部-运营支持经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1958374320980119554", "reportTitle": "北大医疗海洋石油医院新活素返利申请", "lastReportReplyId": null, "lastReplyContent": "您有侯桐新转发的汇报", "lastReplyTime": "2025-08-21 13:38:06", "replyEmployeeName": "侯桐", "replyEmployeeDeptAndTitle": "[产品部-产品总监]", "type": "转发给我"}, {"unReadCount": 2, "reportId": "1943509678313091074", "reportTitle": "股票助手进展情况", "lastReportReplyId": null, "lastReplyContent": "建议人陈洁点击忽略", "lastReplyTime": "2025-08-20 20:32:55", "replyEmployeeName": "陈洁", "replyEmployeeDeptAndTitle": "[产品部-产品经理]", "type": "工作汇报"}, {"unReadCount": 1, "reportId": "1930908720071790593", "reportTitle": "工作协同-信息化专题中提到的内容-6月6日整理", "lastReportReplyId": "1958090495012020225", "lastReplyContent": "1\nlite版本我清楚。请小马回复一下。 小马现在不是提供了lite版可以不联网吗? AI修订是否可...", "lastReplyTime": "2025-08-20 16:55:28", "replyEmployeeName": "马超平", "replyEmployeeDeptAndTitle": "[AI及数据运维组-Android开发工程师]", "type": "反馈"}, {"unReadCount": 2, "reportId": "1941072658697723905", "reportTitle": "股票MCP改造目前情况", "lastReportReplyId": null, "lastReplyContent": "建议人陈洁点击忽略", "lastReplyTime": "2025-08-20 16:47:14", "replyEmployeeName": "陈洁", "replyEmployeeDeptAndTitle": "[产品部-产品经理]", "type": "工作汇报"}, {"unReadCount": 1, "reportId": "1957750277822271489", "reportTitle": "内部交流会通知 | AI编程工具使用经验交流会", "lastReportReplyId": null, "lastReplyContent": "全体同事:\n大家好!为助力大家高效使用AI编程工具,提升工作效率,公司将于明天(8月20日,周三) ...", "lastReplyTime": "2025-08-19 18:55:51", "replyEmployeeName": "许敏玲", "replyEmployeeDeptAndTitle": "[人力资源部-人事专员]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1953286947921580033", "reportTitle": "试用期第三月综合评估-曾文哲", "lastReportReplyId": "1957346348814671873", "lastReplyContent": "@宋培众 看下", "lastReplyTime": "2025-08-18 15:38:30", "replyEmployeeName": "屈军利", "replyEmployeeDeptAndTitle": "[开发组-部门经理]", "type": "@我的回复"}, {"unReadCount": 1, "reportId": "1957324890579247106", "reportTitle": "倒计时!季节性过敏性鼻炎患者福利来啦!【只需一针,轻松度过过敏季!药物免费!检查免费!还有 各种补贴!", "lastReportReplyId": null, "lastReplyContent": "尊敬的各位领导同事好:\n 我司正在进行\"一项评价 MG-K10人源化单抗注 射液治疗季...", "lastReplyTime": "2025-08-18 14:14:55", "replyEmployeeName": "汪倩倩", "replyEmployeeDeptAndTitle": "[临床运营组-高级临床监查员]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1957321481449164802", "reportTitle": "倒计时!季节性过敏性鼻炎患者福利来啦!【只需一针,轻松度过过敏季!药物免费!检查免费!还有 各种补贴!】", "lastReportReplyId": null, "lastReplyContent": "尊敬的各位领导同事好:\n 我司正在进行\"一项评价 MG-K10人源化单抗注 射液治疗季...", "lastReplyTime": "2025-08-18 14:08:59", "replyEmployeeName": "汪倩倩", "replyEmployeeDeptAndTitle": "[临床运营组-高级临床监查员]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1957271464101126146", "reportTitle": "会议纪要-文生图中Ai修订的问题", "lastReportReplyId": null, "lastReplyContent": " # 08月18日录音会议纪要\n\n ## 主要讨论内容\n\n ### 文生图工具问题及改进\n - 团队...", "lastReplyTime": "2025-08-18 10:49:57", "replyEmployeeName": "欧倩", "replyEmployeeDeptAndTitle": "[产品部-测试工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1957272663294791682", "reportTitle": "AI慧记摘要支持展示照片", "lastReportReplyId": null, "lastReplyContent": "web开发已完成(pc+h5页面)", "lastReplyTime": "2025-08-18 10:46:37", "replyEmployeeName": "罗涛", "replyEmployeeDeptAndTitle": "", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1956873877422657537", "reportTitle": "《项目进展汇报总结-8月15日》", "lastReportReplyId": null, "lastReplyContent": " # 项目进展汇报总结(2025年8月15日)\n 基于2025年8月8日的项目汇报内容,结合各负责人...", "lastReplyTime": "2025-08-17 09:49:42", "replyEmployeeName": "罗毓娴", "replyEmployeeDeptAndTitle": "[运营部-内容运营]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1956300654876139522", "reportTitle": "周报", "lastReportReplyId": null, "lastReplyContent": " # 工作周报:工作说说 GooglePlay 版本适配工作\n\n 本周我完成了工作说说 Google...", "lastReplyTime": "2025-08-15 18:23:17", "replyEmployeeName": "伍孝权", "replyEmployeeDeptAndTitle": "[开发组-Android开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1956293872397164546", "reportTitle": "周报", "lastReportReplyId": null, "lastReplyContent": " # 周报:实现按钮权限配置与控制\n\n 本周完成了按钮权限配置与控制功能的实现,主要包括以下几个方面...", "lastReplyTime": "2025-08-15 18:04:45", "replyEmployeeName": "杨婷", "replyEmployeeDeptAndTitle": "[开发组-Android开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1956266766749573121", "reportTitle": "从 \"八爪鱼\" 到\"游刃有余\",AI 汇报助手让我有更多深度思考空间!", "lastReportReplyId": null, "lastReplyContent": "\n 大家好呀!👋 我是玄关健康运营部的罗毓娴。我主要负责平台运营和项目管理的工作。曾经的我总耗费大...", "lastReplyTime": "2025-08-15 16:22:25", "replyEmployeeName": "罗毓娴", "replyEmployeeDeptAndTitle": "[运营部-内容运营]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1955508782016868354", "reportTitle": "【暑气消·风味飨】——周四茶歇派对清凉上线!", "lastReportReplyId": null, "lastReplyContent": "各位小伙伴: \n 三伏天的空调房待久了?键盘敲累了?是时候用一场「舌尖上的团建」唤醒你的...", "lastReplyTime": "2025-08-13 14:54:21", "replyEmployeeName": "刘丽华", "replyEmployeeDeptAndTitle": "[行政管理部-行政经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1955163708909031426", "reportTitle": "南山区软件人才技能提升补贴政策通知", "lastReportReplyId": null, "lastReplyContent": "各位同事:\n\n为促进公司人才队伍建设,提升团队专业技能水平,同时帮助大家获取个人发展福利,现将南山区...", "lastReplyTime": "2025-08-12 16:49:33", "replyEmployeeName": "阳叶菊", "replyEmployeeDeptAndTitle": "[人力资源部-人力资源部经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1954866385112928257", "reportTitle": "08月11日项目进展会议纪要", "lastReportReplyId": null, "lastReplyContent": " # 08月11日项目进展会议纪要\n\n ## 会议背景\n 本次会议是针对8月8日的项目进展汇报总结进...", "lastReplyTime": "2025-08-11 19:54:39", "replyEmployeeName": "罗毓娴", "replyEmployeeDeptAndTitle": "[运营部-内容运营]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1953309839971143681", "reportTitle": "季节性过敏性鼻炎患者福利来啦!【只需一针,轻松度过过敏季!药物免费!检查免费!还有 各种补贴!】", "lastReportReplyId": null, "lastReplyContent": "尊敬的各位领导同事好:\n 我司正在进行\"一项评价 MG-K10人源化单抗注 射液治疗季...", "lastReplyTime": "2025-08-07 12:21:25", "replyEmployeeName": "汪倩倩", "replyEmployeeDeptAndTitle": "[临床运营组-高级临床监查员]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1952684904324145153", "reportTitle": "后台完成", "lastReportReplyId": null, "lastReplyContent": "[POST] /ai-huiji/huiji/addPicture\n参数\nmeetingChatId...", "lastReplyTime": "2025-08-05 18:57:28", "replyEmployeeName": "马超平", "replyEmployeeDeptAndTitle": "[AI及数据运维组-Android开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1951251924875603970", "reportTitle": "开发人员周报-黄登品-8月1号", "lastReportReplyId": null, "lastReplyContent": "本周任务完成情况\n1、康哲合同的管理数据看板的优化\n \n2、投前档案功能优化以及离职人员档案交接 ...", "lastReplyTime": "2025-08-01 20:12:26", "replyEmployeeName": "黄登品", "replyEmployeeDeptAndTitle": "[开发组-iOS开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1951235839891730434", "reportTitle": "新投前项目开发周报0801", "lastReportReplyId": null, "lastReplyContent": "## 项目整体目标\n 1. 打通商机挖掘、评级、派活与立项全流程,引入AI,提高工作效率\n 2. 8...", "lastReplyTime": "2025-08-01 19:27:12", "replyEmployeeName": "屈军利", "replyEmployeeDeptAndTitle": "[开发组-部门经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1951224434958659586", "reportTitle": "周报", "lastReportReplyId": null, "lastReplyContent": " # 周报:Spring Boot学习与应用开发进展\n ## 本周学习内容\n 1. **Spring...", "lastReplyTime": "2025-08-01 18:12:18", "replyEmployeeName": "伍孝权", "replyEmployeeDeptAndTitle": "[开发组-Android开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1951213761343909890", "reportTitle": "周报", "lastReportReplyId": null, "lastReplyContent": " # 本周工作汇报\n\n ## 重要工作事项完成情况\n\n 1. 拜访结果支持Schema格式的存储与解...", "lastReplyTime": "2025-08-01 17:39:54", "replyEmployeeName": "杨婷", "replyEmployeeDeptAndTitle": "[开发组-Android开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 2, "reportId": "1943237425631473665", "reportTitle": "AI投前-初审-商机简报规则判断", "lastReportReplyId": "1950871360284905474", "lastReplyContent": "需求确认需要把这个商机记录下来,如果不满足建档;再异步通知上报人", "lastReplyTime": "2025-07-31 18:49:12", "replyEmployeeName": "黄登品", "replyEmployeeDeptAndTitle": "[开发组-iOS开发工程师]", "type": "我收到的回复"}, {"unReadCount": 1, "reportId": "1950850320384962561", "reportTitle": "季节性过敏性鼻炎患者福利来啦!【只需一针,轻松度过过敏季!药物免费!检查免费!还有 各种补贴!】", "lastReportReplyId": null, "lastReplyContent": "尊敬的各位领导同事好:\n\n 我司正在进行\"一项评价 MG-K10人源化单抗注 射液治疗...", "lastReplyTime": "2025-07-31 18:29:12", "replyEmployeeName": "汪倩倩", "replyEmployeeDeptAndTitle": "[临床运营组-高级临床监查员]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1943243330414813186", "reportTitle": "AI投前-初审-归档报告结构化提取(注册分类)", "lastReportReplyId": "1950856272626831362", "lastReplyContent": "dify+提示词", "lastReplyTime": "2025-07-31 17:49:15", "replyEmployeeName": "黄登品", "replyEmployeeDeptAndTitle": "[开发组-iOS开发工程师]", "type": "我收到的回复"}, {"unReadCount": 1, "reportId": "1943240659616018433", "reportTitle": "AI投前-初审-归档报告结构化提取(档案优先级)", "lastReportReplyId": "1950855945072791553", "lastReplyContent": "提示词", "lastReplyTime": "2025-07-31 17:47:57", "replyEmployeeName": "黄登品", "replyEmployeeDeptAndTitle": "[开发组-iOS开发工程师]", "type": "我收到的回复"}, {"unReadCount": 1, "reportId": "1943236432902631426", "reportTitle": "AI投前-初审-情报归档报告结构化提取", "lastReportReplyId": "1950855773852577794", "lastReplyContent": "情报库格式化异步回调", "lastReplyTime": "2025-07-31 17:47:16", "replyEmployeeName": "黄登品", "replyEmployeeDeptAndTitle": "[开发组-iOS开发工程师]", "type": "我收到的回复"}, {"unReadCount": 1, "reportId": "1948688497564999681", "reportTitle": "🎉 AI汇报助手×慧记超强联动,快速写出优质会议纪要!", "lastReportReplyId": null, "lastReplyContent": " # 📋 还在为会议纪要烦恼吗?\n\n 每次会议结束后,是否还在花费大量精力整理纪要?\n 是否常常遗...", "lastReplyTime": "2025-07-29 13:58:36", "replyEmployeeName": "罗毓娴", "replyEmployeeDeptAndTitle": "[运营部-内容运营]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1948648293043306497", "reportTitle": "SFE数据采集规划评审", "lastReportReplyId": "1950016056957632513", "lastReplyContent": "@宋培众 @李广智 @成伟 这个是不是结构化提取会用到的能力?\n通过schema定义和管理不同采集项...", "lastReplyTime": "2025-07-29 10:10:32", "replyEmployeeName": "侯桐", "replyEmployeeDeptAndTitle": "[产品部-产品总监]", "type": "@我的回复"}, {"unReadCount": 1, "reportId": "1948660428645097473", "reportTitle": "周报", "lastReportReplyId": null, "lastReplyContent": "上周四、上周五请假,本周一请假,所以两周一起汇报。\n已完成的工作:\n1、二维码扫码登录增加选择企业;...", "lastReplyTime": "2025-07-25 17:05:52", "replyEmployeeName": "杨婷", "replyEmployeeDeptAndTitle": "[开发组-Android开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1948641916308717570", "reportTitle": "预算结构化数据优化目标及计划", "lastReportReplyId": null, "lastReplyContent": "\n ## 目标一:展示时间优化\n - **目标**:汇报提交后,展示出结构化数据时间20秒左右\n -...", "lastReplyTime": "2025-07-25 15:12:33", "replyEmployeeName": "成伟", "replyEmployeeDeptAndTitle": "[技术部-首席架构师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1948620175595229185", "reportTitle": "孟令辉-产品功能提交开发流程-07月25日", "lastReportReplyId": null, "lastReplyContent": "立项申请(请描述需求来源,需求目标,使用用户,是否有时间要求等,先进行立项审核):CMS智汇问答时,...", "lastReplyTime": "2025-07-25 14:44:51", "replyEmployeeName": "孟令辉", "replyEmployeeDeptAndTitle": "", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1948300257955758082", "reportTitle": "AI慧记的问答接入标准的GPTS", "lastReportReplyId": null, "lastReplyContent": "### 背景:\n\n目前,AI 慧记的问答功能并非标准的 GPTs 问答模式。而针对 GPTs 的问答...", "lastReplyTime": "2025-07-24 17:36:44", "replyEmployeeName": "王晓辉", "replyEmployeeDeptAndTitle": "[产品部-产品经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1948202544685666306", "reportTitle": "AI慧记支持拍照/截图功能", "lastReportReplyId": null, "lastReplyContent": "立项申请(请描述需求来源,需求目标,使用用户,是否有时间要求等,先进行立项审核):需求来源:手机 A...", "lastReplyTime": "2025-07-24 15:39:02", "replyEmployeeName": "王晓辉", "replyEmployeeDeptAndTitle": "[产品部-产品经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1947621365586837505", "reportTitle": "会议纪要-07月22日康哲官网问答与工作协同情报库(玄关合同)", "lastReportReplyId": null, "lastReplyContent": " # 会议纪要:07月22日康哲官网问答与工作协同情报库(玄关合同助手作为验证点)\n\n **会议时间...", "lastReplyTime": "2025-07-22 19:58:04", "replyEmployeeName": "罗毓娴", "replyEmployeeDeptAndTitle": "[运营部-内容运营]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1947585613486247938", "reportTitle": "AI修正优化维盛日报提示词的汇报", "lastReportReplyId": null, "lastReplyContent": "一、修订时间工具\n2025.07.22日使用cursor编写优化提示词\n二、修正过程\n1、测试数据:...", "lastReplyTime": "2025-07-22 18:55:14", "replyEmployeeName": "庄诗吟", "replyEmployeeDeptAndTitle": "[产品部-测试工程师]", "type": "汇报提交通知"}, {"unReadCount": 2, "reportId": "1942184429818621954", "reportTitle": "AI 应用技术选型规范文档 (V1.0)", "lastReportReplyId": "1947542074572464129", "lastReplyContent": "已记录", "lastReplyTime": "2025-07-22 14:19:49", "replyEmployeeName": "程敏", "replyEmployeeDeptAndTitle": "[AI及数据运维组-数据架构师]", "type": "我收到的回复"}, {"unReadCount": 1, "reportId": "1946114223581339649", "reportTitle": "警惕夏季“补水”变“毁脸”! | 7月禾零【5折内购】助力科学养肤", "lastReportReplyId": null, "lastReplyContent": "夏天空调房 + 室外暴晒,“冰火两重天” 反复折腾\n皮肤屏障脆弱,根本锁不住水,一敷面膜就刺痛?\n这...", "lastReplyTime": "2025-07-21 15:07:33", "replyEmployeeName": "李宁宁", "replyEmployeeDeptAndTitle": "", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1946166020938956801", "reportTitle": "MCP申请规范", "lastReportReplyId": null, "lastReplyContent": "一、概述\n 随着AI应用的深入,我们需要提供更多的MCP工具,为了提升MCP搭建的效率,故建立...", "lastReplyTime": "2025-07-18 19:17:41", "replyEmployeeName": "屈军利", "replyEmployeeDeptAndTitle": "[开发组-部门经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1946121692360581121", "reportTitle": "季节性过敏性鼻炎患者福利来啦!【只需一针,轻松度过过敏季!药物免费!检查免费!还有 各种补贴!】", "lastReportReplyId": null, "lastReplyContent": "尊敬的各位领导同事好:\n 我司正在进行\"一项评价 MG-K10人源化单抗注 射液治疗季...", "lastReplyTime": "2025-07-18 18:37:10", "replyEmployeeName": "汪倩倩", "replyEmployeeDeptAndTitle": "[临床运营组-高级临床监查员]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1946153807146786817", "reportTitle": "周报", "lastReportReplyId": null, "lastReplyContent": "## 一、本周完成的工作\n| 任务链接 | 任务目标 | 本周进展 | 预计耗时(小时) | 实际耗...", "lastReplyTime": "2025-07-18 18:26:26", "replyEmployeeName": "伍孝权", "replyEmployeeDeptAndTitle": "[开发组-Android开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1946121384033910786", "reportTitle": "07月18日AI汇报助手优化需求开发沟通实施计划", "lastReportReplyId": null, "lastReplyContent": " # AI写汇报功能系统功能优化与问题修复会议\n\n\n **会议时间**:7月18日14:16-14:...", "lastReplyTime": "2025-07-18 16:25:46", "replyEmployeeName": "罗毓娴", "replyEmployeeDeptAndTitle": "[运营部-内容运营]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1946097775704125442", "reportTitle": "关于对办公区域进行消杀的温馨提示", "lastReportReplyId": null, "lastReplyContent": "各位同事:\n\n大家好!行政管理部将于7月19日(周六) 16:00-18:20对办公区进行全面例行虫...", "lastReplyTime": "2025-07-18 14:52:57", "replyEmployeeName": "齐慧", "replyEmployeeDeptAndTitle": "[行政管理部-行政助理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1943613876715560962", "reportTitle": "周报", "lastReportReplyId": null, "lastReplyContent": "本周已完成的工作:\n1、erp项目增加二维码登录,工作量8,已完成100%,AI写代码80%\n2、S...", "lastReplyTime": "2025-07-11 20:00:20", "replyEmployeeName": "杨婷", "replyEmployeeDeptAndTitle": "[开发组-Android开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 2, "reportId": "1940656861448884226", "reportTitle": "ios1.9.3版本", "lastReportReplyId": "1942399585529106433", "lastReplyContent": "收到", "lastReplyTime": "2025-07-08 09:45:24", "replyEmployeeName": "侯桐", "replyEmployeeDeptAndTitle": "[产品部-产品总监]", "type": "我收到的回复"}, {"unReadCount": 2, "reportId": "1940658375483576321", "reportTitle": "iOS1.9.1版本", "lastReportReplyId": "1940950878180003841", "lastReplyContent": "已确认", "lastReplyTime": "2025-07-04 09:48:45", "replyEmployeeName": "欧倩", "replyEmployeeDeptAndTitle": "[产品部-测试工程师]", "type": "我收到的回复"}, {"unReadCount": 1, "reportId": "1938171313317650434", "reportTitle": "产品功能提交开发流程-06月26日", "lastReportReplyId": null, "lastReplyContent": "立项申请(请描述需求来源,需求目标,使用用户,是否有时间要求等,先进行立项审核):工作说说支持规划模...", "lastReplyTime": "2025-06-26 17:45:02", "replyEmployeeName": "孟令辉", "replyEmployeeDeptAndTitle": "", "type": "汇报提交通知"}, {"unReadCount": 2, "reportId": "1932046126666891266", "reportTitle": "AI白癜风问诊(初版)", "lastReportReplyId": "1935527594209624065", "lastReplyContent": "已记录", "lastReplyTime": "2025-06-19 10:38:33", "replyEmployeeName": "李广智", "replyEmployeeDeptAndTitle": "[AI及数据运维组-技术总监]", "type": "我收到的回复"}, {"unReadCount": 2, "reportId": "1932984820545421314", "reportTitle": "宋培众-AI-bizcode申请-06月12日", "lastReportReplyId": "1933090636830003201", "lastReplyContent": "测试环境已记录,生产环境请@程敏 执行下脚本\nbizCode: vitiligo_img_recog...", "lastReplyTime": "2025-06-12 17:14:58", "replyEmployeeName": "龙继海", "replyEmployeeDeptAndTitle": "[开发组-Java开发工程师]", "type": "我收到的回复"}, {"unReadCount": 1, "reportId": "1922966957307887618", "reportTitle": "维盛的问题拆分总结与反思", "lastReportReplyId": "1924357530992140290", "lastReplyContent": "已记录", "lastReplyTime": "2025-05-19 14:52:43", "replyEmployeeName": "李广智", "replyEmployeeDeptAndTitle": "[AI及数据运维组-技术总监]", "type": "我收到的回复"}, {"unReadCount": 1, "reportId": "1871469012899090433", "reportTitle": "关于开展 AI 浏览器插件及 “玄关 gpt” 调研的通知", "lastReportReplyId": null, "lastReplyContent": "各位领导、同事:\n 大家好!公司目前正在开发一款AI 浏览器插件,为让它贴合需求、解决痛点,公司特...", "lastReplyTime": "2024-12-24 16:34:33", "replyEmployeeName": "阳叶菊", "replyEmployeeDeptAndTitle": "[人力资源部-人力资源部经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1858691587874263041", "reportTitle": "会议团队会务支撑-2024117东莞市中医药学会中西医结合消化病学专业委员会学术年会", "lastReportReplyId": null, "lastReplyContent": "一、背景:\n2024117东莞市中医药学会中西医结合消化病学专业委员会学术年会,需要会务支持。\n内容...", "lastReplyTime": "2024-11-19 10:26:58", "replyEmployeeName": "王晓辉", "replyEmployeeDeptAndTitle": "[产品部-产品经理]", "type": "汇报提交通知"}], "serverReturnedSize": 169, "clientLimit": 200, "clientReturnedSize": 169, "defaultClientLimit": 200, "maxClientLimit": 500}}
FILE:run-results/msg-limit-1000.json
{"resultCode": 1, "resultMsg": null, "data": {"total": 186, "msgList": [{"unReadCount": 1, "reportId": "2037801532216434690", "reportTitle": "【BP审查通知】请确认您的个人BP审查报告及修改要求", "lastReportReplyId": null, "lastReplyContent": "## ⚠️ BP整改复查 - 仍有未完成项\n\n经复查,上次审查报告中指出的核心问题均**未进行整改*...", "lastReplyTime": "2026-03-28 15:58:40", "replyEmployeeName": "BP", "replyEmployeeDeptAndTitle": "", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2037736957693517825", "reportTitle": "【BP审查通知】请确认您的个人BP审查报告及修改要求", "lastReportReplyId": null, "lastReplyContent": "## ⚠️ BP整改复查 - 仍有未完成项\n\n经复查,上次审查报告中指出的绝大部分核心问题**仍未进...", "lastReplyTime": "2026-03-28 11:42:05", "replyEmployeeName": "BP", "replyEmployeeDeptAndTitle": "", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2037364082105139202", "reportTitle": "Skill 工厂系统定义汇报", "lastReportReplyId": null, "lastReplyContent": "各位好,附件是 Skill 工厂系统的完整产品定义,请查阅。 核心能力:与用户协作创建 Skill ...", "lastReplyTime": "2026-03-27 11:00:24", "replyEmployeeName": "张成鹏", "replyEmployeeDeptAndTitle": "[玄关健康-总经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2037057446878453761", "reportTitle": "【饮料优惠购】丰e足食零食柜福利活动", "lastReportReplyId": null, "lastReplyContent": "各位同事,大家好! 为方便大家办公时段补给能量,公司丰e足食智能零...", "lastReplyTime": "2026-03-26 14:48:25", "replyEmployeeName": "齐慧", "replyEmployeeDeptAndTitle": "[行政管理部-行政助理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2035964985372700673", "reportTitle": "「限时内购」3月春季大放价,喜辽妥多磺酸粘多糖乳膏超强折扣内购会", "lastReportReplyId": null, "lastReplyContent": "惊呆了家人们! Hirudoid喜辽妥多磺酸粘多糖乳膏超强折扣“内购上线”啦!!此前没了解过跨境版喜...", "lastReplyTime": "2026-03-23 14:54:30", "replyEmployeeName": "易大宇", "replyEmployeeDeptAndTitle": "[跨境组-电商运营专员-ML]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2034932116260732929", "reportTitle": "《个人BP实战培训:从认知到内化》培训会纪要", "lastReportReplyId": null, "lastReplyContent": "培训主题: 《个人BP实战培训:从认知到内化》主讲讲师: 杨晶晶 参训人员:研发人员共27人 时间地...", "lastReplyTime": "2026-03-20 18:38:01", "replyEmployeeName": "许敏玲", "replyEmployeeDeptAndTitle": "[人力资源部-人事专员]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2034920526136557570", "reportTitle": "小龙虾项目进展汇报 (03.20)", "lastReportReplyId": null, "lastReplyContent": "### 核心进展摘要\n**本周聚焦解决用户反馈的 P0 级连接与交互痛点,确保基础可用性。下周起将通...", "lastReplyTime": "2026-03-20 17:12:04", "replyEmployeeName": "于耀", "replyEmployeeDeptAndTitle": "[产品部-产品经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2034839256127651842", "reportTitle": "关于对办公区域进行消杀的温馨提示", "lastReportReplyId": null, "lastReplyContent": "各位同事:\n\n大家好!行政管理部将于3月21日(周六) 16:00-18:20对办公区进行全面例行虫...", "lastReplyTime": "2026-03-20 11:47:42", "replyEmployeeName": "齐慧", "replyEmployeeDeptAndTitle": "[行政管理部-行政助理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2034811290719195137", "reportTitle": "内部培训通知 | 个人BP实战培训:从认知到内化", "lastReportReplyId": null, "lastReplyContent": "各位同事:大家好!为进一步提升大家对BP系统的了解,公司将于今日(3月20日)组织一场培训会,本次培...", "lastReplyTime": "2026-03-20 10:51:23", "replyEmployeeName": "许敏玲", "replyEmployeeDeptAndTitle": "[人力资源部-人事专员]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2034263550192873474", "reportTitle": "产品中心在参与投前投后核心岗位的说明书的整理-3月18号", "lastReportReplyId": null, "lastReplyContent": "附件是整理的产品中心在参与投前投后核心岗位的说明书,结构整体如下;总共给到的是8个部门的;应该还缺少财务部的。", "lastReplyTime": "2026-03-18 21:47:10", "replyEmployeeName": "黄登品", "replyEmployeeDeptAndTitle": "[开发组-iOS开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2033836797615677441", "reportTitle": "关于近期3项专项研发项目积分激励的公示", "lastReportReplyId": null, "lastReplyContent": " 为进一步完善公司积分激励体系,激发全员攻坚活力,结合公司既定的年度项目积分总池1200...", "lastReplyTime": "2026-03-17 17:26:47", "replyEmployeeName": "刘丽华", "replyEmployeeDeptAndTitle": "[行政管理部-行政经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2033795578580004866", "reportTitle": "03月17日-“小龙虾”产品问题复盘与事项讨论会", "lastReportReplyId": null, "lastReplyContent": "“小龙虾”产品问题复盘与事项讨论会 本次会议首先由于耀反馈了产品“小龙虾”在会话管理、文件功能...", "lastReplyTime": "2026-03-17 14:45:34", "replyEmployeeName": "王晓辉", "replyEmployeeDeptAndTitle": "[产品部-产品经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2033740812176945153", "reportTitle": "关于NoteX体验季培训资料准备及培训安排的汇报", "lastReportReplyId": "2033784535157927938", "lastReplyContent": "以上两个PDF文件都是由NoteX生成,导出PPT后会存在字体丢失,部分排版错位的情况,因此使用了P...", "lastReplyTime": "2026-03-17 13:56:34", "replyEmployeeName": "刘明霞", "replyEmployeeDeptAndTitle": "[数据管理部-智能项目运营专员]", "type": "反馈"}, {"unReadCount": 1, "reportId": "2033512725291982849", "reportTitle": "AI费用控制和skills规范讨论", "lastReportReplyId": null, "lastReplyContent": " **会议时间**:2026年03月16日 18:19\n **会议主题**:AI模型成本控制与技术优...", "lastReplyTime": "2026-03-16 21:06:02", "replyEmployeeName": "屈军利", "replyEmployeeDeptAndTitle": "[开发组-部门经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2033391676235804674", "reportTitle": "api治理和skill规范讨论", "lastReportReplyId": null, "lastReplyContent": "**会议时间**:2026年3月16日 11:06\n **参会人员**:张成鹏、侯桐、屈军利、李广智...", "lastReplyTime": "2026-03-16 15:25:04", "replyEmployeeName": "屈军利", "replyEmployeeDeptAndTitle": "[开发组-部门经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2032398536188194818", "reportTitle": "🎙️ 会议纪要:AI 慧记功能优化与系统联调闭环会议", "lastReportReplyId": null, "lastReplyContent": "🎙️ 会议纪要:AI 慧记功能优化与系统联调闭环会议 会议时间:2026年3月13日 17:23 ...", "lastReplyTime": "2026-03-13 18:09:06", "replyEmployeeName": "于耀", "replyEmployeeDeptAndTitle": "[产品部-产品经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2031926941438828546", "reportTitle": "关于宋培众、程敏岗位调整的申请", "lastReportReplyId": null, "lastReplyContent": "您有阳叶菊新转发的汇报", "lastReplyTime": "2026-03-12 11:07:44", "replyEmployeeName": "阳叶菊", "replyEmployeeDeptAndTitle": "[人力资源部-人力资源部经理]", "type": "转发给我"}, {"unReadCount": 1, "reportId": "2031920424790896641", "reportTitle": "关于技术部组织架构调整的通知", "lastReportReplyId": null, "lastReplyContent": "各位领导、同事: 为进一步优化技术部门组织架构,提升团队协作效率与技术...", "lastReplyTime": "2026-03-12 10:53:14", "replyEmployeeName": "阳叶菊", "replyEmployeeDeptAndTitle": "[人力资源部-人力资源部经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2031606932007149570", "reportTitle": "03月11日-OpenClaw部署、运维优化与团队协作讨论会", "lastReportReplyId": null, "lastReplyContent": "AI产品研发与团队协作优化工作推进会 本次会议重点围绕公司AI产品的研发进展、运维部署...", "lastReplyTime": "2026-03-11 13:47:33", "replyEmployeeName": "王晓辉", "replyEmployeeDeptAndTitle": "[产品部-产品经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2030933317339086849", "reportTitle": "数据管理部运营企划与玄关AI功能周例会-会议纪要", "lastReportReplyId": null, "lastReplyContent": "数据管理部运营企划与玄关AI功能协同沟通会本次会议围绕Note X产品种子用户活动策划、训战平台运营...", "lastReplyTime": "2026-03-09 17:34:09", "replyEmployeeName": "刘明霞", "replyEmployeeDeptAndTitle": "[数据管理部-智能项目运营专员]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2029502366440722434", "reportTitle": "关于对办公区域进行消杀的温馨提示", "lastReportReplyId": null, "lastReplyContent": "各位同事:\n\n大家好!行政管理部将于3月7日(周六) 16:00-18:20对办公区进行全面例行虫害...", "lastReplyTime": "2026-03-05 18:21:01", "replyEmployeeName": "齐慧", "replyEmployeeDeptAndTitle": "[行政管理部-行政助理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2029121916283097089", "reportTitle": "关于“院外北极星”管理系统项目里程碑验收及项目评分的公示", "lastReportReplyId": null, "lastReplyContent": "各位同事: 大家好!我司积分激励试点项目之一“院外北极星”管理系统项目现已完成本年度一阶...", "lastReplyTime": "2026-03-04 17:58:08", "replyEmployeeName": "刘丽华", "replyEmployeeDeptAndTitle": "[行政管理部-行政经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2028788354698809345", "reportTitle": "关于TBS训战系统项目一阶段里程碑验收及项目评分的公示", "lastReportReplyId": null, "lastReplyContent": "各位同事: 大家好!我司积分激励试点项目之一TBS训战系统项目历经2个月的攻坚推进,现已...", "lastReplyTime": "2026-03-03 19:32:58", "replyEmployeeName": "刘丽华", "replyEmployeeDeptAndTitle": "[行政管理部-行政经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2028766170592718850", "reportTitle": "OpenClaw技术问题讨论与任务分工会议纪要", "lastReportReplyId": null, "lastReplyContent": "\n # OpenClaw技术问题讨论与任务分工会议纪要\n\n ## 会议基本信息\n - **会议主题*...", "lastReplyTime": "2026-03-03 17:55:15", "replyEmployeeName": "王晓辉", "replyEmployeeDeptAndTitle": "[产品部-产品经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2027685838514872321", "reportTitle": "为notex提供选择文件及选择上传路径组件功能", "lastReportReplyId": null, "lastReplyContent": "为notex提供选择文件及提供选择上传路径组件功能,具体对接文档参见附件选择文件示例: https:...", "lastReplyTime": "2026-02-28 18:10:20", "replyEmployeeName": "刘艳华", "replyEmployeeDeptAndTitle": "[开发组-技术支持]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2021840587996848130", "reportTitle": "BP体系相关信息分享汇报", "lastReportReplyId": null, "lastReplyContent": "大家好,BP体系的相关信息现分享给大家,内容如下:1、BP体系说明稿-前言、BP体系说明稿(简洁版)...", "lastReplyTime": "2026-02-12 15:01:47", "replyEmployeeName": "封敏", "replyEmployeeDeptAndTitle": "[产品部-测试工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2021164293239644162", "reportTitle": "新春内购场 | 囤禾零,26全年都「稳」了!", "lastReportReplyId": null, "lastReplyContent": "朋友,休假了伐?禾零囤了伐?别着急离开,下滑告诉你新春必囤禾零的四个理由(诚心版)肌肤返乡综合征?·...", "lastReplyTime": "2026-02-10 18:15:53", "replyEmployeeName": "邝焕然", "replyEmployeeDeptAndTitle": "[货架电商组1-私域运营专员-ML]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2019681273408487425", "reportTitle": "关于对办公区域进行消杀的温馨提示", "lastReportReplyId": null, "lastReplyContent": "各位同事:\n\n大家好!行政管理部将于2月7日(周六) 16:00-18:20对办公区进行全面例行虫害...", "lastReplyTime": "2026-02-06 15:55:21", "replyEmployeeName": "齐慧", "replyEmployeeDeptAndTitle": "[行政管理部-行政助理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2019256280589631490", "reportTitle": "IndexTTS(文本合成语音-带声音复刻) 性能分析报告", "lastReportReplyId": null, "lastReplyContent": "# IndexTTS 性能分析报告 (2026-02)\n\n## 1. 概述\n本报告基于实测数据,分析...", "lastReplyTime": "2026-02-05 16:36:08", "replyEmployeeName": "成伟", "replyEmployeeDeptAndTitle": "[技术部-首席架构师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2018260217757757441", "reportTitle": "玄关开发者平台使用通知", "lastReportReplyId": null, "lastReplyContent": "功能简介:1、玄关内部开发文档:提供玄关特有的开发规范文档,让大家使用AI写代码的时候可以把相应的规...", "lastReplyTime": "2026-02-02 19:08:00", "replyEmployeeName": "屈军利", "replyEmployeeDeptAndTitle": "[开发组-部门经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2018232746714918914", "reportTitle": " AI 图像中文文字清晰度问题解决指南", "lastReportReplyId": null, "lastReplyContent": "已更新维护到到文档: https://cwork-web-test.xgjktech.com.cn/...", "lastReplyTime": "2026-02-02 16:00:12", "replyEmployeeName": "于耀", "replyEmployeeDeptAndTitle": "[产品部-产品经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2017110397726887938", "reportTitle": "关于2026年春节放假调休安排的通知", "lastReportReplyId": null, "lastReplyContent": "正文:各位领导、同事:根据国家法定节假日安排的相关规定,现将2026年春节的放假调休日期通知如下,请...", "lastReplyTime": "2026-01-30 18:15:32", "replyEmployeeName": "许敏玲", "replyEmployeeDeptAndTitle": "[人力资源部-人事专员]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2016408793910714369", "reportTitle": "【南头莲花物业通知】关于南头莲花广场大厦停车场3号出入口地面施工的通知", "lastReportReplyId": null, "lastReplyContent": "各位同事,大家好! 南头城莲花广场将于2026年1月29日至2月1日对3号出入口(东面)进行...", "lastReplyTime": "2026-01-28 15:18:19", "replyEmployeeName": "齐慧", "replyEmployeeDeptAndTitle": "[行政管理部-行政助理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2014579884537782274", "reportTitle": "天猫新店开业 | 员工专属内购福利!", "lastReportReplyId": null, "lastReplyContent": "好消息!喜辽妥全新天猫美妆旗舰店正式开业!特此推出 「员工内购专场」以最优惠的价格把心仪好物带回家!...", "lastReplyTime": "2026-01-26 10:12:41", "replyEmployeeName": "吴志雄", "replyEmployeeDeptAndTitle": "[货架电商组2-电商运营专员-ML]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2014598674750955521", "reportTitle": "项目奖金规则讨论-20260123", "lastReportReplyId": null, "lastReplyContent": "会议主题:项目奖金讨论会议地点:银河会议时间:18:00-19:00参会人:阳叶菊、许敏玲、刘丽华、...", "lastReplyTime": "2026-01-23 19:14:17", "replyEmployeeName": "阳叶菊", "replyEmployeeDeptAndTitle": "[人力资源部-人力资源部经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2014602027392360450", "reportTitle": "关于对办公区域进行消杀的温馨提示", "lastReportReplyId": null, "lastReplyContent": "各位同事:\n\n大家好!行政管理部将于1月24日(周六) 16:00-18:20对办公区进行全面例行虫...", "lastReplyTime": "2026-01-23 15:33:24", "replyEmployeeName": "齐慧", "replyEmployeeDeptAndTitle": "[行政管理部-行政助理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2014544465397915650", "reportTitle": "「限时内购」1月新年超强折扣,喜辽妥多磺酸粘多糖凝胶·新品上市!", "lastReportReplyId": null, "lastReplyContent": "工作描述惊呆了家人们! Hirudoid喜辽妥多磺酸粘多糖新款凝胶·新品上市。超强折扣“内购上线”啦...", "lastReplyTime": "2026-01-23 15:32:52", "replyEmployeeName": "易大宇", "replyEmployeeDeptAndTitle": "[跨境组-电商运营专员-ML]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2013512813896769538", "reportTitle": "BP三种拆解承接方案", "lastReportReplyId": null, "lastReplyContent": "\n ## BP三种拆解承接方案\n\n ### 方案一:公司层级汇总模式\n - **特点**:由公司层面...", "lastReplyTime": "2026-01-20 15:39:28", "replyEmployeeName": "封敏", "replyEmployeeDeptAndTitle": "[产品部-测试工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2013139565405077506", "reportTitle": "向量方案", "lastReportReplyId": null, "lastReplyContent": "1. - 康哲产品知识库建议使用向量。产品知识库均为根据检索到产品知识内容来回答用户,一般不会涉及多...", "lastReplyTime": "2026-01-19 15:17:12", "replyEmployeeName": "程敏", "replyEmployeeDeptAndTitle": "[AI及数据运维组-数据架构师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2009564847569997825", "reportTitle": "咖啡精选(第一期)2026 特辑・续命口粮召唤令☕️", "lastReportReplyId": null, "lastReplyContent": "# ☕️☕️☕️☕️☕️☕️☕️☕️☕️☕️☕️☕️\n\n## 各位早八续命党、咖啡控、工位干饭咖:\n...", "lastReplyTime": "2026-01-09 18:20:22", "replyEmployeeName": "薛芳芳", "replyEmployeeDeptAndTitle": "[质量安全部-运营支持经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2009562310850514945", "reportTitle": "周报", "lastReportReplyId": null, "lastReplyContent": "本周完成工作内容:1、为了审核通过首页禁用了下拉刷新、热门应用的“换一批”隐藏、历史记录隐藏、修改了...", "lastReplyTime": "2026-01-09 17:49:45", "replyEmployeeName": "杨婷", "replyEmployeeDeptAndTitle": "[开发组-Android开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2009531854423953410", "reportTitle": "关于办公区域例行虫害消杀的通知", "lastReportReplyId": null, "lastReplyContent": "各位同事: 大家好!为营造整洁、卫生的办公环境,公司定于1 月 10 日(本周六)16:...", "lastReplyTime": "2026-01-09 15:58:07", "replyEmployeeName": "刘丽华", "replyEmployeeDeptAndTitle": "[行政管理部-行政经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2008843850445148161", "reportTitle": "周报", "lastReportReplyId": null, "lastReplyContent": "本周已完成:1、AI慧记录音功能开发;2、AI慧记录音动画开发;3、AI慧记-会中回顾开发;下周工作...", "lastReplyTime": "2026-01-07 18:12:38", "replyEmployeeName": "杨婷", "replyEmployeeDeptAndTitle": "[开发组-Android开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2004517902786596866", "reportTitle": "周报", "lastReportReplyId": null, "lastReplyContent": "本周完成工作:1、粤通网安监测〔2025〕815号 药企圈3.1.8问题整改;2、医院开发管理sch...", "lastReplyTime": "2025-12-26 19:44:43", "replyEmployeeName": "杨婷", "replyEmployeeDeptAndTitle": "[开发组-Android开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2003726713187840002", "reportTitle": "关于2026年部分节假日放假调休安排的通知", "lastReportReplyId": null, "lastReplyContent": "正文:各位领导、同事:根据国家法定节假日安排的相关规定,现将2026年部分节假日的放假调休日期通知如...", "lastReplyTime": "2025-12-26 18:02:11", "replyEmployeeName": "许敏玲", "replyEmployeeDeptAndTitle": "[人力资源部-人事专员]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2001201446103777281", "reportTitle": "❄️冬日暖时光 · 茶歇活动通知 ❄️", "lastReportReplyId": null, "lastReplyContent": "亲爱的同事们: 寒意渐深,正是用温暖的美食与陪伴,暖心暖胃的好时候...", "lastReplyTime": "2025-12-17 16:03:00", "replyEmployeeName": "齐慧", "replyEmployeeDeptAndTitle": "[行政管理部-行政助理]", "type": "汇报提交通知"}, {"unReadCount": 3, "reportId": "1998665931114762241", "reportTitle": "AI问答超出token也能进行问答", "lastReportReplyId": "2000772484957323266", "lastReplyContent": "@伍孝权 @宋培众 @罗涛 超出模型token数提示,发送info类型消息格式:\n{\n \"id...", "lastReplyTime": "2025-12-16 11:38:28", "replyEmployeeName": "龙继海", "replyEmployeeDeptAndTitle": "[开发组-Java开发工程师]", "type": "@我的回复"}, {"unReadCount": 1, "reportId": "2000757180139421698", "reportTitle": "【重要】关于优化办公区域垃圾桶配置及垃圾处理规范的通知", "lastReportReplyId": null, "lastReplyContent": "各位同事:\n为进一步维护办公区域环境整洁,减少资源浪费,提升办公空间利用率,经统筹规划,现对办公区域...", "lastReplyTime": "2025-12-16 11:17:36", "replyEmployeeName": "刘丽华", "replyEmployeeDeptAndTitle": "[行政管理部-行政经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2000441171331366914", "reportTitle": "咖啡精选(第十一期)-✨ Member's Mark 经典意式拼配咖啡豆", "lastReportReplyId": null, "lastReplyContent": "亲爱的咖啡爱好者们:\n\n 岁末将至,用一杯醇厚浓郁的意式咖啡温暖这个冬日!咖啡兴趣小组第十...", "lastReplyTime": "2025-12-15 13:51:03", "replyEmployeeName": "薛芳芳", "replyEmployeeDeptAndTitle": "[质量安全部-运营支持经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2000392034837708801", "reportTitle": "周报", "lastReportReplyId": null, "lastReplyContent": "本周完成工作:\n1、修复实际填报驳回actual 错误的调用了plan的API;\n2、表格增加单元...", "lastReplyTime": "2025-12-15 10:34:09", "replyEmployeeName": "杨婷", "replyEmployeeDeptAndTitle": "[开发组-Android开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1999719955351961601", "reportTitle": "会议纪要|春节活动(Line B)实施计划1212 ", "lastReportReplyId": null, "lastReplyContent": "\n> **日期**:2025年12月12日[@12月12日春节活动实施(并行线)](huiJiId=...", "lastReplyTime": "2025-12-13 14:18:57", "replyEmployeeName": "罗毓娴", "replyEmployeeDeptAndTitle": "[运营部-内容运营]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1997838780702203906", "reportTitle": "周报", "lastReportReplyId": null, "lastReplyContent": "本周已完成工作:\n1、HcpSelector组件如果productId为空时,根据hcoId去重显示...", "lastReplyTime": "2025-12-08 09:25:29", "replyEmployeeName": "杨婷", "replyEmployeeDeptAndTitle": "[开发组-Android开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1996914284638220289", "reportTitle": "AI写代码分享会", "lastReportReplyId": null, "lastReplyContent": "**会议主题:** AI写代码分享会\n **会议时间:** 12月05日\n **参会人员:** 开发...", "lastReplyTime": "2025-12-05 20:16:43", "replyEmployeeName": "屈军利", "replyEmployeeDeptAndTitle": "[开发组-部门经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1996891823225991169", "reportTitle": "新投前系统周报1205", "lastReportReplyId": null, "lastReplyContent": "项目目标\n目标待定,当前聚焦其他更高优先级事项。\n当前进展\n1、商机到立项会阶段功能,项目管理部、B...", "lastReplyTime": "2025-12-05 19:16:59", "replyEmployeeName": "屈军利", "replyEmployeeDeptAndTitle": "[开发组-部门经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1996891344014127106", "reportTitle": "周报", "lastReportReplyId": null, "lastReplyContent": "\n ## 本周工作总结\n\n ### 1. 医生圈APP集成AI翻译功能\n - 实现翻译结果通过通知和...", "lastReplyTime": "2025-12-05 18:39:04", "replyEmployeeName": "伍孝权", "replyEmployeeDeptAndTitle": "[开发组-Android开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1995702833160282114", "reportTitle": "关于深圳康哲在集团内部招聘区域管理经理的通知", "lastReportReplyId": null, "lastReplyContent": "尊敬的各位领导、同事:\n 根据公司发展战略,深康区域管理部拟面向集团内部招聘区域管理经理2名。...", "lastReplyTime": "2025-12-02 13:56:08", "replyEmployeeName": "石玥", "replyEmployeeDeptAndTitle": "[组织发展部-组织发展专员]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1994248441147953153", "reportTitle": "AI修订方案", "lastReportReplyId": null, "lastReplyContent": "\n\n ## 一、整体技术方案\n\n ### 1. 基于最新 LangChain 1.0 和 LangG...", "lastReplyTime": "2025-11-28 18:46:03", "replyEmployeeName": "李广智", "replyEmployeeDeptAndTitle": "[AI及数据运维组-技术总监]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1994327770527850498", "reportTitle": "周报", "lastReportReplyId": null, "lastReplyContent": "本周已完成:\n1、schema配置界面增加导入和导出功能;\n2、数据采集增加新增逻辑;\n3、解决只保...", "lastReplyTime": "2025-11-28 16:52:21", "replyEmployeeName": "杨婷", "replyEmployeeDeptAndTitle": "[开发组-Android开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1992906964375592962", "reportTitle": "敏肌闭眼囤!禾零【5折内购】润护危「肌」", "lastReportReplyId": null, "lastReplyContent": "「小雪」封尘、「大雪」藏寒\n即将迎来节气「大雪」\n干敏肌的换季危「肌」也跟着翻倍到来!\n角质层越冷越...", "lastReplyTime": "2025-11-24 18:48:00", "replyEmployeeName": "邝焕然", "replyEmployeeDeptAndTitle": "[货架电商组1-私域运营专员-ML]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1991810085504049153", "reportTitle": "周报", "lastReportReplyId": null, "lastReplyContent": "本周完成工作\n1、CMS 智慧聊天内容分享\n2、医生圈Ai翻译优化功能开发\n3、工作说说TBS 功能...", "lastReplyTime": "2025-11-21 18:11:51", "replyEmployeeName": "伍孝权", "replyEmployeeDeptAndTitle": "[开发组-Android开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1991806614931521538", "reportTitle": "新投前系统周报1121", "lastReportReplyId": null, "lastReplyContent": "项目目标\n目标待定,当前聚焦其他更高优先级事项。\n当前进展\n1、商机到立项会阶段功能,项目管理部、B...", "lastReplyTime": "2025-11-21 17:59:01", "replyEmployeeName": "屈军利", "replyEmployeeDeptAndTitle": "[开发组-部门经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1991801812776402946", "reportTitle": "周总结", "lastReportReplyId": null, "lastReplyContent": "本周完成工作:\n1、解决了FieldSelector多医院模式下,之前选中医生再次选择时没有勾选的问...", "lastReplyTime": "2025-11-21 17:38:32", "replyEmployeeName": "杨婷", "replyEmployeeDeptAndTitle": "[开发组-Android开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1991749627266850817", "reportTitle": "关于对办公区域进行消杀的温馨提示", "lastReportReplyId": null, "lastReplyContent": "各位同事:\n\n大家好!行政管理部将于11月22日(周六) 16:00-18:20对办公区进行全面例行...", "lastReplyTime": "2025-11-21 14:10:15", "replyEmployeeName": "齐慧", "replyEmployeeDeptAndTitle": "[行政管理部-行政助理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1989290113997393921", "reportTitle": "新投前系统周报1114", "lastReportReplyId": null, "lastReplyContent": "项目目标\n目标待定,当前聚焦其他更高优先级事项。\n当前进展\n1、商机到立项会阶段功能,项目管理部、B...", "lastReplyTime": "2025-11-14 19:21:08", "replyEmployeeName": "屈军利", "replyEmployeeDeptAndTitle": "[开发组-部门经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1989278978539782146", "reportTitle": "周报", "lastReportReplyId": null, "lastReplyContent": "\n\n\n 本周完成工作:\n 1、21.5寸屏通用版桌面定制化改造,去掉桌面用户注册流程 2、冻结内置应...", "lastReplyTime": "2025-11-14 18:31:54", "replyEmployeeName": "伍孝权", "replyEmployeeDeptAndTitle": "[开发组-Android开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1989266651975176193", "reportTitle": "周报", "lastReportReplyId": null, "lastReplyContent": "本周工作总结:\n1、schema配置中详情由列表返回改为请求接口,增加修改人显示;\n2、FieldS...", "lastReplyTime": "2025-11-14 17:46:22", "replyEmployeeName": "杨婷", "replyEmployeeDeptAndTitle": "[开发组-Android开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1979118129451651074", "reportTitle": "9月28日-10月17日产品部汇报项目情况", "lastReportReplyId": null, "lastReplyContent": "您有蹇晓枫新转发的汇报", "lastReplyTime": "2025-10-20 18:08:34", "replyEmployeeName": "蹇晓枫", "replyEmployeeDeptAndTitle": "[产品部-产品经理]", "type": "转发给我"}, {"unReadCount": 1, "reportId": "1980180461518868482", "reportTitle": "技术中心本周重点事项-1020", "lastReportReplyId": null, "lastReplyContent": "会议地点:漫度\n会议时间:10:45-11:15\n参会人:屈军利、宋培众、李广智、成伟、周皓、黄登品...", "lastReplyTime": "2025-10-20 16:20:37", "replyEmployeeName": "屈军利", "replyEmployeeDeptAndTitle": "[开发组-部门经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1979026557934755842", "reportTitle": "iOS 1.9.11", "lastReportReplyId": "1980173023285903362", "lastReplyContent": "10月20日:工作说说-首页-汇报任务-点击详情“可查看任务详情。\n结论:已修复", "lastReplyTime": "2025-10-20 15:23:33", "replyEmployeeName": "罗毓娴", "replyEmployeeDeptAndTitle": "[运营部-内容运营]", "type": "我收到的回复"}, {"unReadCount": 1, "reportId": "1979154756391829506", "reportTitle": "会议纪要:AI写汇报历史问题处理进展与void测试结果分享10月17日", "lastReportReplyId": null, "lastReplyContent": "## 会议概述\n- **时间**:2025-10-17 17:35-18:06\n- **主题**:A...", "lastReplyTime": "2025-10-17 20:43:53", "replyEmployeeName": "罗毓娴", "replyEmployeeDeptAndTitle": "[运营部-内容运营]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1979162651644952577", "reportTitle": "会议纪要:AI写汇报系统优化问题讨论与解决方案制定-10月16日", "lastReportReplyId": null, "lastReplyContent": "## 会议概述\n\n- **会议时间**:2025年10月16日 18:18:17 - 18:56:1...", "lastReplyTime": "2025-10-17 20:43:35", "replyEmployeeName": "罗毓娴", "replyEmployeeDeptAndTitle": "[运营部-内容运营]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1979142117867683842", "reportTitle": "AI能力跟踪进展1017", "lastReportReplyId": null, "lastReplyContent": "AI能力负责人和产品经理对当前AI能力存在的痛点进行了沟通,确定了目前需要重点跟进研究的AI能力问题...", "lastReplyTime": "2025-10-17 19:19:01", "replyEmployeeName": "屈军利", "replyEmployeeDeptAndTitle": "[开发组-部门经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1979130553500192769", "reportTitle": "周报", "lastReportReplyId": null, "lastReplyContent": "# 本周工作完成汇报\n\n## 1. 工作说说应用更新\n\n\n- Android内网版本也已完成Spla...", "lastReplyTime": "2025-10-17 18:24:52", "replyEmployeeName": "伍孝权", "replyEmployeeDeptAndTitle": "[开发组-Android开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1979115411748179969", "reportTitle": "移动端周报-黄登品-10月17号", "lastReportReplyId": null, "lastReplyContent": "# 本周工作总结\n\n## 主要工作内容\n\n### 1. 创研药品/试剂出入库系统需求沟通(10月15...", "lastReplyTime": "2025-10-17 17:32:42", "replyEmployeeName": "黄登品", "replyEmployeeDeptAndTitle": "[开发组-iOS开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1979114805969190914", "reportTitle": "周报", "lastReportReplyId": null, "lastReplyContent": "# 工作周报\n\n## 本周工作完成情况\n\n1. **维盛医院管理优化**\n - 实际结果填报周期...", "lastReplyTime": "2025-10-17 17:20:32", "replyEmployeeName": "杨婷", "replyEmployeeDeptAndTitle": "[开发组-Android开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1979102944280346625", "reportTitle": "VOID试用体验", "lastReportReplyId": null, "lastReplyContent": "\n\n使用gpt模型。 \n第一个问题先让void对代码进行一些调整,回答要我补充代码。无法得到想要的结...", "lastReplyTime": "2025-10-17 17:09:45", "replyEmployeeName": "马超平", "replyEmployeeDeptAndTitle": "[AI及数据运维组-Android开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1978465055087411202", "reportTitle": "会议纪要:AI汇报助手(问题复盘&进展同步)-10月15日", "lastReportReplyId": null, "lastReplyContent": "# 10月15日AI写汇报 会议纪要\n\n## 概述\n\n **时间**:2025-10-15 18:1...", "lastReplyTime": "2025-10-15 22:56:22", "replyEmployeeName": "罗毓娴", "replyEmployeeDeptAndTitle": "[运营部-内容运营]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1978053570662105089", "reportTitle": "舒护蕊双11闺蜜团内购,专宠爱自己的你!", "lastReportReplyId": null, "lastReplyContent": "\n舒护蕊双11闺蜜团内购,专宠爱自己的你!\n\n纯天然护理液,呵护私处健康!想要的精准下单\n ...", "lastReplyTime": "2025-10-15 14:35:04", "replyEmployeeName": "李威威", "replyEmployeeDeptAndTitle": "[禾零组-平面设计师-ML]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1976912762701479937", "reportTitle": "新投前系统周报1011", "lastReportReplyId": null, "lastReplyContent": "项目目标\n新投前系统要在2025年上线,包含从商机上报到终审会完整流程(今天上午会议和康康沟通,她说...", "lastReplyTime": "2025-10-11 16:36:16", "replyEmployeeName": "屈军利", "replyEmployeeDeptAndTitle": "[开发组-部门经理]", "type": "汇报提交通知"}, {"unReadCount": 2, "reportId": "1969276534476877826", "reportTitle": "iOS 1.9.10", "lastReportReplyId": "1971396385093816322", "lastReplyContent": "无异议", "lastReplyTime": "2025-09-26 10:08:20", "replyEmployeeName": "罗涛", "replyEmployeeDeptAndTitle": "", "type": "我收到的回复"}, {"unReadCount": 1, "reportId": "1971110717721673729", "reportTitle": "【南头莲花广场信息分享】南头莲花广场中秋活动分享", "lastReportReplyId": null, "lastReplyContent": "各位同事,大家好!\n 南头城莲花广场为感谢客户的支持,特别筹备“月满中秋,情系莲花”的中秋活...", "lastReplyTime": "2025-09-25 15:25:24", "replyEmployeeName": "齐慧", "replyEmployeeDeptAndTitle": "[行政管理部-行政助理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1970769795544616962", "reportTitle": "关于9月25日恢复正常办公的通知", "lastReportReplyId": null, "lastReplyContent": "各位领导、同事:\n根据气象部门最新通报,今年第 18 号台风 “桦加沙” 对深圳的影响已逐渐减弱。深...", "lastReplyTime": "2025-09-24 16:41:50", "replyEmployeeName": "阳叶菊", "replyEmployeeDeptAndTitle": "[人力资源部-人力资源部经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1970317662428958722", "reportTitle": "关于续签订劳动合同员工及时查阅员工手册的通知-2025年9月", "lastReportReplyId": null, "lastReplyContent": "各位同事:\n 大家好!近期正在和各位员工进行劳动合同续签,为了让大家更加清晰地了解公司各...", "lastReplyTime": "2025-09-23 10:45:11", "replyEmployeeName": "阳叶菊", "replyEmployeeDeptAndTitle": "[人力资源部-人力资源部经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1970028027840712706", "reportTitle": "国庆出游「零」烦恼出片攻略 | 9月禾零【5折福利】get护肤搭子", "lastReportReplyId": null, "lastReplyContent": "国庆长假想好去哪玩了吗?\n出游想要轻松出片,那好皮肤可不能少~\n\n「禾零舒缓系列」皮肤情绪稳定好搭子...", "lastReplyTime": "2025-09-22 15:50:42", "replyEmployeeName": "李宁宁", "replyEmployeeDeptAndTitle": "", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1968984306546298882", "reportTitle": "周报", "lastReportReplyId": null, "lastReplyContent": " # 工作周报\n\n ## 任务进展\n\n ### 1. 药企圈社区点赞优化\n\n 已完成点赞功能优化,主...", "lastReplyTime": "2025-09-19 18:26:55", "replyEmployeeName": "伍孝权", "replyEmployeeDeptAndTitle": "[开发组-Android开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1968980156483743745", "reportTitle": "周报", "lastReportReplyId": null, "lastReplyContent": " # 工作周报\n\n ## 本周完成工作\n\n ### CRM客户拜访提供在线帮助功能\n\n 该任务已全部...", "lastReplyTime": "2025-09-19 18:10:47", "replyEmployeeName": "杨婷", "replyEmployeeDeptAndTitle": "[开发组-Android开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1968966589156085761", "reportTitle": "关于对办公区域进行消杀的温馨提示", "lastReportReplyId": null, "lastReplyContent": "各位同事:\n\n大家好!行政管理部将9月20日(周六) 16:00-18:20对办公区进行全面例行虫害...", "lastReplyTime": "2025-09-19 17:19:39", "replyEmployeeName": "齐慧", "replyEmployeeDeptAndTitle": "[行政管理部-行政助理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1968641743339642882", "reportTitle": "咖啡兴趣小组第十期精选豆单集结令——醇香深夏", "lastReportReplyId": null, "lastReplyContent": "亲爱的咖啡爱好者们:\n\n 盛夏已至,一杯风味独特的咖啡是穿越炎热的绝佳伴侣。咖啡兴趣小组第九期精选豆...", "lastReplyTime": "2025-09-18 19:42:56", "replyEmployeeName": "薛芳芳", "replyEmployeeDeptAndTitle": "[质量安全部-运营支持经理]", "type": "汇报提交通知"}, {"unReadCount": 4, "reportId": "1922966218137034753", "reportTitle": "iOS客户端1.8.9发布说明", "lastReportReplyId": null, "lastReplyContent": "建议人李志洪点击忽略,原因:无需处理", "lastReplyTime": "2025-09-16 10:00:19", "replyEmployeeName": "李志洪", "replyEmployeeDeptAndTitle": "[产品部-UI设计师]", "type": "工作汇报"}, {"unReadCount": 1, "reportId": "1967540261378973698", "reportTitle": "官网问答现在处理过程", "lastReportReplyId": null, "lastReplyContent": "官网问答:\n1.dify意图识别,现在优如下9中类别\n intent == \"product\"...", "lastReplyTime": "2025-09-15 18:48:44", "replyEmployeeName": "程敏", "replyEmployeeDeptAndTitle": "[AI及数据运维组-数据架构师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1967492777902092290", "reportTitle": "技术中心本周重点事项-0915", "lastReportReplyId": null, "lastReplyContent": "会议地点:水星\n会议时间:14:00-15:00\n参会人:屈军利、宋培众、李广智、成伟、周皓、黄登品...", "lastReplyTime": "2025-09-15 16:55:11", "replyEmployeeName": "屈军利", "replyEmployeeDeptAndTitle": "[开发组-部门经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1955896086929625090", "reportTitle": "HUIJI 服务接口", "lastReportReplyId": null, "lastReplyContent": "[POST] /ai-huiji/xgMeeting/nologin/addMeetingVideo...", "lastReplyTime": "2025-09-15 16:40:06", "replyEmployeeName": "马超平", "replyEmployeeDeptAndTitle": "[AI及数据运维组-Android开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1967422235631484930", "reportTitle": "腾讯会议AI托管功能分析", "lastReportReplyId": null, "lastReplyContent": "背景:\n基于腾讯 9 月 11 日上线的 “AI 托管” 新需求,其核心功能是,当你因忙碌而没有时间...", "lastReplyTime": "2025-09-15 16:11:11", "replyEmployeeName": "王晓辉", "replyEmployeeDeptAndTitle": "[产品部-产品经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1967413150232150018", "reportTitle": "SFE数据采集系统任务管理设计文档", "lastReportReplyId": null, "lastReplyContent": "\n ## 1. Schema定义\n\n 深西康系统的schema定义(后续代码实现可能需要根据sche...", "lastReplyTime": "2025-09-15 10:35:13", "replyEmployeeName": "成伟", "replyEmployeeDeptAndTitle": "[技术部-首席架构师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1966468273637707777", "reportTitle": "新投前项目开发周报0913", "lastReportReplyId": null, "lastReplyContent": "## 项目整体进展\n\n 项目已完成内部系统验收,本周主要集中在演示PPT制作、演示脚本编写和系统功能...", "lastReplyTime": "2025-09-12 19:52:30", "replyEmployeeName": "屈军利", "replyEmployeeDeptAndTitle": "[开发组-部门经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1966447939261984770", "reportTitle": "周报", "lastReportReplyId": null, "lastReplyContent": " # 工作周报\n\n ## 本周工作内容\n\n ### 医生圈鸿蒙版本\n - 修复打开康哲云平台合同...", "lastReplyTime": "2025-09-12 18:24:58", "replyEmployeeName": "伍孝权", "replyEmployeeDeptAndTitle": "[开发组-Android开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 2, "reportId": "1960632901519495169", "reportTitle": "体检通知 | 2025年体检将于9月1日开启,预约通道已开放,请大家及时预约参加", "lastReportReplyId": "1965656120484003841", "lastReplyContent": "👋 温馨提示:大家好,年度体检可预约日期截止10月1日,请尚未完成体检的同事们,尽快安排时间预约完成体检哟 ", "lastReplyTime": "2025-09-10 13:58:34", "replyEmployeeName": "许敏玲", "replyEmployeeDeptAndTitle": "[人力资源部-人事专员]", "type": "@我的回复"}, {"unReadCount": 1, "reportId": "1965281012468453377", "reportTitle": "iOS复制粘贴的功能", "lastReportReplyId": "1965339160193892353", "lastReplyContent": "已确认", "lastReplyTime": "2025-09-09 16:59:05", "replyEmployeeName": "欧倩", "replyEmployeeDeptAndTitle": "[产品部-测试工程师]", "type": "我收到的回复"}, {"unReadCount": 1, "reportId": "1965281129527283714", "reportTitle": "支持Markdwon表格", "lastReportReplyId": "1965339138311974913", "lastReplyContent": "已确认", "lastReplyTime": "2025-09-09 16:58:59", "replyEmployeeName": "欧倩", "replyEmployeeDeptAndTitle": "[产品部-测试工程师]", "type": "我收到的回复"}, {"unReadCount": 1, "reportId": "1964692302022848514", "reportTitle": "8月OpenAI模型更新情况跟踪", "lastReportReplyId": null, "lastReplyContent": "## 更新概述\n\n8月5日推出开源大模型GPT-OSS 120B以及GPT-OSS-20B。\n8月7...", "lastReplyTime": "2025-09-07 23:44:11", "replyEmployeeName": "屈军利", "replyEmployeeDeptAndTitle": "[开发组-部门经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1964655207006769153", "reportTitle": "新投前项目开发周报0905", "lastReportReplyId": null, "lastReplyContent": "## 项目整体进展\n 项目已完成验收阶段,PC端流程验证完整顺畅,手机端商机上报提示词需要进一步优化...", "lastReplyTime": "2025-09-07 19:47:06", "replyEmployeeName": "屈军利", "replyEmployeeDeptAndTitle": "[开发组-部门经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1963914701167398914", "reportTitle": "周报", "lastReportReplyId": null, "lastReplyContent": " # 本周工作汇报\n ## 大屏录制引导提示\n 完成了大屏录制引导提示的设计与实现工作。当用户点击录...", "lastReplyTime": "2025-09-05 18:38:47", "replyEmployeeName": "伍孝权", "replyEmployeeDeptAndTitle": "[开发组-Android开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1963875657322762241", "reportTitle": "关于对办公区域进行消杀的温馨提示", "lastReportReplyId": null, "lastReplyContent": "各位同事:\n\n大家好!行政管理部将于9月6日(周六) 16:00-18:20对办公区进行全面例行虫害...", "lastReplyTime": "2025-09-05 16:03:49", "replyEmployeeName": "齐慧", "replyEmployeeDeptAndTitle": "[行政管理部-行政助理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1963799865234460674", "reportTitle": "周报", "lastReportReplyId": null, "lastReplyContent": " # 工作周报\n\n ## 本周已完成\n 1. 完成了康联达CRM协同随访特性的全部功能开发工作,包括...", "lastReplyTime": "2025-09-05 11:09:52", "replyEmployeeName": "杨婷", "replyEmployeeDeptAndTitle": "[开发组-Android开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1963541823490101250", "reportTitle": "维盛结构化提取变更以及后续处理逻辑", "lastReportReplyId": null, "lastReplyContent": "已完成:提取数据新增品种,以及学术会议关联品种等逻辑处理。\n", "lastReplyTime": "2025-09-04 17:58:11", "replyEmployeeName": "程敏", "replyEmployeeDeptAndTitle": "[AI及数据运维组-数据架构师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1962435389025325058", "reportTitle": "工作说说IOS", "lastReportReplyId": "1962457663396646913", "lastReplyContent": "已确认", "lastReplyTime": "2025-09-01 18:09:02", "replyEmployeeName": "欧倩", "replyEmployeeDeptAndTitle": "[产品部-测试工程师]", "type": "我收到的回复"}, {"unReadCount": 1, "reportId": "1962136348303589377", "reportTitle": "《项目进展汇报总结-8月29日》", "lastReportReplyId": null, "lastReplyContent": " # 项目进展汇报总结(2025年8月22日)\n\n 基于2025年8月22日的项目汇报内容,结合各负...", "lastReplyTime": "2025-08-31 22:16:44", "replyEmployeeName": "罗毓娴", "replyEmployeeDeptAndTitle": "[运营部-内容运营]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1961356449313820673", "reportTitle": "周报", "lastReportReplyId": null, "lastReplyContent": "本周已完成工作\n1、提交拜访计划调用GPT增加传参,调提示词;\n2、提交拜访计划GPT返回数据处理,...", "lastReplyTime": "2025-08-29 17:29:10", "replyEmployeeName": "杨婷", "replyEmployeeDeptAndTitle": "[开发组-Android开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1961280826125877250", "reportTitle": "维盛7-8月客户反馈主要问题汇总", "lastReportReplyId": null, "lastReplyContent": "归纳总结了ERP系统开发维盛客户第3季度聚焦客户功能后7-8月维盛客户反馈的主要问题,问题信息来源为...", "lastReplyTime": "2025-08-29 12:18:04", "replyEmployeeName": "庄诗吟", "replyEmployeeDeptAndTitle": "[产品部-测试工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1960612621912649729", "reportTitle": "【下午茶派对·能量加满通知】🧋✨", "lastReportReplyId": null, "lastReplyContent": "各位小伙伴注意啦~ \n今天(周四,8月28日)下午15:30, \n公司健身房即将变身「奶茶轰趴馆」!...", "lastReplyTime": "2025-08-28 10:07:36", "replyEmployeeName": "刘丽华", "replyEmployeeDeptAndTitle": "[行政管理部-行政经理]", "type": "汇报提交通知"}, {"unReadCount": 3, "reportId": "1958734435394486273", "reportTitle": " 培训不仅能学习实践AI工具,还能拿花生油和大米?8月27日总部V603/药企圈【718459】与你不见不散!(培训通知)", "lastReportReplyId": "1960639421338365953", "lastReplyContent": "【集团AI第一课培训视频回放提醒】\n各位领导,同事好,\n集团“AI提效季·第一课”培训视频回放及课件...", "lastReplyTime": "2025-08-27 17:44:00", "replyEmployeeName": "李文俏", "replyEmployeeDeptAndTitle": "[人才发展部-人才发展专员]", "type": "@我的回复"}, {"unReadCount": 1, "reportId": "1958858292226916354", "reportTitle": "新投前项目开发周报0822", "lastReportReplyId": null, "lastReplyContent": "## 项目整体目标\n 1. 打通商机挖掘、评级、派活与立项全流程,引入AI,提高工作效率\n 2. 8...", "lastReplyTime": "2025-08-22 20:02:12", "replyEmployeeName": "屈军利", "replyEmployeeDeptAndTitle": "[开发组-部门经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1958853368311152642", "reportTitle": "周报", "lastReportReplyId": null, "lastReplyContent": " # 康哲翻译需求进展报告\n\n ## 本周已完成事项\n\n ### 统计中心优化\n - 新增网页翻译统...", "lastReplyTime": "2025-08-22 19:26:51", "replyEmployeeName": "伍孝权", "replyEmployeeDeptAndTitle": "[开发组-Android开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1958821500517625858", "reportTitle": "周报", "lastReportReplyId": null, "lastReplyContent": " # 周报\n\n ## 本周已完成工作\n\n 1. 数据字典-字典分类增加scope字段显示标签;已完成...", "lastReplyTime": "2025-08-22 17:27:28", "replyEmployeeName": "杨婷", "replyEmployeeDeptAndTitle": "[开发组-Android开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1958490983352205313", "reportTitle": "医生圈处理声网sdk bitcode方式", "lastReportReplyId": null, "lastReplyContent": "cd /Users/dachen/Desktop/iOS/AgoraRtcEngine_Specia...", "lastReplyTime": "2025-08-21 19:27:14", "replyEmployeeName": "黄登品", "replyEmployeeDeptAndTitle": "[开发组-iOS开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1958462097021173762", "reportTitle": "咖啡兴趣小组第九期精选豆单集结令——醇香深夏,意式风情", "lastReportReplyId": null, "lastReplyContent": " # 咖啡兴趣小组第九期精选豆单集结令——醇香深夏,意式风情\n\n 亲爱的咖啡爱好者们:\n\n 盛夏已至...", "lastReplyTime": "2025-08-21 17:49:08", "replyEmployeeName": "薛芳芳", "replyEmployeeDeptAndTitle": "[质量安全部-运营支持经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1958374320980119554", "reportTitle": "北大医疗海洋石油医院新活素返利申请", "lastReportReplyId": null, "lastReplyContent": "您有侯桐新转发的汇报", "lastReplyTime": "2025-08-21 13:38:06", "replyEmployeeName": "侯桐", "replyEmployeeDeptAndTitle": "[产品部-产品总监]", "type": "转发给我"}, {"unReadCount": 2, "reportId": "1943509678313091074", "reportTitle": "股票助手进展情况", "lastReportReplyId": null, "lastReplyContent": "建议人陈洁点击忽略", "lastReplyTime": "2025-08-20 20:32:55", "replyEmployeeName": "陈洁", "replyEmployeeDeptAndTitle": "[产品部-产品经理]", "type": "工作汇报"}, {"unReadCount": 1, "reportId": "1930908720071790593", "reportTitle": "工作协同-信息化专题中提到的内容-6月6日整理", "lastReportReplyId": "1958090495012020225", "lastReplyContent": "1\nlite版本我清楚。请小马回复一下。 小马现在不是提供了lite版可以不联网吗? AI修订是否可...", "lastReplyTime": "2025-08-20 16:55:28", "replyEmployeeName": "马超平", "replyEmployeeDeptAndTitle": "[AI及数据运维组-Android开发工程师]", "type": "反馈"}, {"unReadCount": 2, "reportId": "1941072658697723905", "reportTitle": "股票MCP改造目前情况", "lastReportReplyId": null, "lastReplyContent": "建议人陈洁点击忽略", "lastReplyTime": "2025-08-20 16:47:14", "replyEmployeeName": "陈洁", "replyEmployeeDeptAndTitle": "[产品部-产品经理]", "type": "工作汇报"}, {"unReadCount": 1, "reportId": "1957750277822271489", "reportTitle": "内部交流会通知 | AI编程工具使用经验交流会", "lastReportReplyId": null, "lastReplyContent": "全体同事:\n大家好!为助力大家高效使用AI编程工具,提升工作效率,公司将于明天(8月20日,周三) ...", "lastReplyTime": "2025-08-19 18:55:51", "replyEmployeeName": "许敏玲", "replyEmployeeDeptAndTitle": "[人力资源部-人事专员]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1953286947921580033", "reportTitle": "试用期第三月综合评估-曾文哲", "lastReportReplyId": "1957346348814671873", "lastReplyContent": "@宋培众 看下", "lastReplyTime": "2025-08-18 15:38:30", "replyEmployeeName": "屈军利", "replyEmployeeDeptAndTitle": "[开发组-部门经理]", "type": "@我的回复"}, {"unReadCount": 1, "reportId": "1957324890579247106", "reportTitle": "倒计时!季节性过敏性鼻炎患者福利来啦!【只需一针,轻松度过过敏季!药物免费!检查免费!还有 各种补贴!", "lastReportReplyId": null, "lastReplyContent": "尊敬的各位领导同事好:\n 我司正在进行\"一项评价 MG-K10人源化单抗注 射液治疗季...", "lastReplyTime": "2025-08-18 14:14:55", "replyEmployeeName": "汪倩倩", "replyEmployeeDeptAndTitle": "[临床运营组-高级临床监查员]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1957321481449164802", "reportTitle": "倒计时!季节性过敏性鼻炎患者福利来啦!【只需一针,轻松度过过敏季!药物免费!检查免费!还有 各种补贴!】", "lastReportReplyId": null, "lastReplyContent": "尊敬的各位领导同事好:\n 我司正在进行\"一项评价 MG-K10人源化单抗注 射液治疗季...", "lastReplyTime": "2025-08-18 14:08:59", "replyEmployeeName": "汪倩倩", "replyEmployeeDeptAndTitle": "[临床运营组-高级临床监查员]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1957271464101126146", "reportTitle": "会议纪要-文生图中Ai修订的问题", "lastReportReplyId": null, "lastReplyContent": " # 08月18日录音会议纪要\n\n ## 主要讨论内容\n\n ### 文生图工具问题及改进\n - 团队...", "lastReplyTime": "2025-08-18 10:49:57", "replyEmployeeName": "欧倩", "replyEmployeeDeptAndTitle": "[产品部-测试工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1957272663294791682", "reportTitle": "AI慧记摘要支持展示照片", "lastReportReplyId": null, "lastReplyContent": "web开发已完成(pc+h5页面)", "lastReplyTime": "2025-08-18 10:46:37", "replyEmployeeName": "罗涛", "replyEmployeeDeptAndTitle": "", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1956873877422657537", "reportTitle": "《项目进展汇报总结-8月15日》", "lastReportReplyId": null, "lastReplyContent": " # 项目进展汇报总结(2025年8月15日)\n 基于2025年8月8日的项目汇报内容,结合各负责人...", "lastReplyTime": "2025-08-17 09:49:42", "replyEmployeeName": "罗毓娴", "replyEmployeeDeptAndTitle": "[运营部-内容运营]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1956300654876139522", "reportTitle": "周报", "lastReportReplyId": null, "lastReplyContent": " # 工作周报:工作说说 GooglePlay 版本适配工作\n\n 本周我完成了工作说说 Google...", "lastReplyTime": "2025-08-15 18:23:17", "replyEmployeeName": "伍孝权", "replyEmployeeDeptAndTitle": "[开发组-Android开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1956293872397164546", "reportTitle": "周报", "lastReportReplyId": null, "lastReplyContent": " # 周报:实现按钮权限配置与控制\n\n 本周完成了按钮权限配置与控制功能的实现,主要包括以下几个方面...", "lastReplyTime": "2025-08-15 18:04:45", "replyEmployeeName": "杨婷", "replyEmployeeDeptAndTitle": "[开发组-Android开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1956266766749573121", "reportTitle": "从 \"八爪鱼\" 到\"游刃有余\",AI 汇报助手让我有更多深度思考空间!", "lastReportReplyId": null, "lastReplyContent": "\n 大家好呀!👋 我是玄关健康运营部的罗毓娴。我主要负责平台运营和项目管理的工作。曾经的我总耗费大...", "lastReplyTime": "2025-08-15 16:22:25", "replyEmployeeName": "罗毓娴", "replyEmployeeDeptAndTitle": "[运营部-内容运营]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1955508782016868354", "reportTitle": "【暑气消·风味飨】——周四茶歇派对清凉上线!", "lastReportReplyId": null, "lastReplyContent": "各位小伙伴: \n 三伏天的空调房待久了?键盘敲累了?是时候用一场「舌尖上的团建」唤醒你的...", "lastReplyTime": "2025-08-13 14:54:21", "replyEmployeeName": "刘丽华", "replyEmployeeDeptAndTitle": "[行政管理部-行政经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1955163708909031426", "reportTitle": "南山区软件人才技能提升补贴政策通知", "lastReportReplyId": null, "lastReplyContent": "各位同事:\n\n为促进公司人才队伍建设,提升团队专业技能水平,同时帮助大家获取个人发展福利,现将南山区...", "lastReplyTime": "2025-08-12 16:49:33", "replyEmployeeName": "阳叶菊", "replyEmployeeDeptAndTitle": "[人力资源部-人力资源部经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1954866385112928257", "reportTitle": "08月11日项目进展会议纪要", "lastReportReplyId": null, "lastReplyContent": " # 08月11日项目进展会议纪要\n\n ## 会议背景\n 本次会议是针对8月8日的项目进展汇报总结进...", "lastReplyTime": "2025-08-11 19:54:39", "replyEmployeeName": "罗毓娴", "replyEmployeeDeptAndTitle": "[运营部-内容运营]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1953309839971143681", "reportTitle": "季节性过敏性鼻炎患者福利来啦!【只需一针,轻松度过过敏季!药物免费!检查免费!还有 各种补贴!】", "lastReportReplyId": null, "lastReplyContent": "尊敬的各位领导同事好:\n 我司正在进行\"一项评价 MG-K10人源化单抗注 射液治疗季...", "lastReplyTime": "2025-08-07 12:21:25", "replyEmployeeName": "汪倩倩", "replyEmployeeDeptAndTitle": "[临床运营组-高级临床监查员]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1952684904324145153", "reportTitle": "后台完成", "lastReportReplyId": null, "lastReplyContent": "[POST] /ai-huiji/huiji/addPicture\n参数\nmeetingChatId...", "lastReplyTime": "2025-08-05 18:57:28", "replyEmployeeName": "马超平", "replyEmployeeDeptAndTitle": "[AI及数据运维组-Android开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1951251924875603970", "reportTitle": "开发人员周报-黄登品-8月1号", "lastReportReplyId": null, "lastReplyContent": "本周任务完成情况\n1、康哲合同的管理数据看板的优化\n \n2、投前档案功能优化以及离职人员档案交接 ...", "lastReplyTime": "2025-08-01 20:12:26", "replyEmployeeName": "黄登品", "replyEmployeeDeptAndTitle": "[开发组-iOS开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1951235839891730434", "reportTitle": "新投前项目开发周报0801", "lastReportReplyId": null, "lastReplyContent": "## 项目整体目标\n 1. 打通商机挖掘、评级、派活与立项全流程,引入AI,提高工作效率\n 2. 8...", "lastReplyTime": "2025-08-01 19:27:12", "replyEmployeeName": "屈军利", "replyEmployeeDeptAndTitle": "[开发组-部门经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1951224434958659586", "reportTitle": "周报", "lastReportReplyId": null, "lastReplyContent": " # 周报:Spring Boot学习与应用开发进展\n ## 本周学习内容\n 1. **Spring...", "lastReplyTime": "2025-08-01 18:12:18", "replyEmployeeName": "伍孝权", "replyEmployeeDeptAndTitle": "[开发组-Android开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1951213761343909890", "reportTitle": "周报", "lastReportReplyId": null, "lastReplyContent": " # 本周工作汇报\n\n ## 重要工作事项完成情况\n\n 1. 拜访结果支持Schema格式的存储与解...", "lastReplyTime": "2025-08-01 17:39:54", "replyEmployeeName": "杨婷", "replyEmployeeDeptAndTitle": "[开发组-Android开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 2, "reportId": "1943237425631473665", "reportTitle": "AI投前-初审-商机简报规则判断", "lastReportReplyId": "1950871360284905474", "lastReplyContent": "需求确认需要把这个商机记录下来,如果不满足建档;再异步通知上报人", "lastReplyTime": "2025-07-31 18:49:12", "replyEmployeeName": "黄登品", "replyEmployeeDeptAndTitle": "[开发组-iOS开发工程师]", "type": "我收到的回复"}, {"unReadCount": 1, "reportId": "1950850320384962561", "reportTitle": "季节性过敏性鼻炎患者福利来啦!【只需一针,轻松度过过敏季!药物免费!检查免费!还有 各种补贴!】", "lastReportReplyId": null, "lastReplyContent": "尊敬的各位领导同事好:\n\n 我司正在进行\"一项评价 MG-K10人源化单抗注 射液治疗...", "lastReplyTime": "2025-07-31 18:29:12", "replyEmployeeName": "汪倩倩", "replyEmployeeDeptAndTitle": "[临床运营组-高级临床监查员]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1943243330414813186", "reportTitle": "AI投前-初审-归档报告结构化提取(注册分类)", "lastReportReplyId": "1950856272626831362", "lastReplyContent": "dify+提示词", "lastReplyTime": "2025-07-31 17:49:15", "replyEmployeeName": "黄登品", "replyEmployeeDeptAndTitle": "[开发组-iOS开发工程师]", "type": "我收到的回复"}, {"unReadCount": 1, "reportId": "1943240659616018433", "reportTitle": "AI投前-初审-归档报告结构化提取(档案优先级)", "lastReportReplyId": "1950855945072791553", "lastReplyContent": "提示词", "lastReplyTime": "2025-07-31 17:47:57", "replyEmployeeName": "黄登品", "replyEmployeeDeptAndTitle": "[开发组-iOS开发工程师]", "type": "我收到的回复"}, {"unReadCount": 1, "reportId": "1943236432902631426", "reportTitle": "AI投前-初审-情报归档报告结构化提取", "lastReportReplyId": "1950855773852577794", "lastReplyContent": "情报库格式化异步回调", "lastReplyTime": "2025-07-31 17:47:16", "replyEmployeeName": "黄登品", "replyEmployeeDeptAndTitle": "[开发组-iOS开发工程师]", "type": "我收到的回复"}, {"unReadCount": 1, "reportId": "1948688497564999681", "reportTitle": "🎉 AI汇报助手×慧记超强联动,快速写出优质会议纪要!", "lastReportReplyId": null, "lastReplyContent": " # 📋 还在为会议纪要烦恼吗?\n\n 每次会议结束后,是否还在花费大量精力整理纪要?\n 是否常常遗...", "lastReplyTime": "2025-07-29 13:58:36", "replyEmployeeName": "罗毓娴", "replyEmployeeDeptAndTitle": "[运营部-内容运营]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1948648293043306497", "reportTitle": "SFE数据采集规划评审", "lastReportReplyId": "1950016056957632513", "lastReplyContent": "@宋培众 @李广智 @成伟 这个是不是结构化提取会用到的能力?\n通过schema定义和管理不同采集项...", "lastReplyTime": "2025-07-29 10:10:32", "replyEmployeeName": "侯桐", "replyEmployeeDeptAndTitle": "[产品部-产品总监]", "type": "@我的回复"}, {"unReadCount": 1, "reportId": "1948660428645097473", "reportTitle": "周报", "lastReportReplyId": null, "lastReplyContent": "上周四、上周五请假,本周一请假,所以两周一起汇报。\n已完成的工作:\n1、二维码扫码登录增加选择企业;...", "lastReplyTime": "2025-07-25 17:05:52", "replyEmployeeName": "杨婷", "replyEmployeeDeptAndTitle": "[开发组-Android开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1948641916308717570", "reportTitle": "预算结构化数据优化目标及计划", "lastReportReplyId": null, "lastReplyContent": "\n ## 目标一:展示时间优化\n - **目标**:汇报提交后,展示出结构化数据时间20秒左右\n -...", "lastReplyTime": "2025-07-25 15:12:33", "replyEmployeeName": "成伟", "replyEmployeeDeptAndTitle": "[技术部-首席架构师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1948620175595229185", "reportTitle": "孟令辉-产品功能提交开发流程-07月25日", "lastReportReplyId": null, "lastReplyContent": "立项申请(请描述需求来源,需求目标,使用用户,是否有时间要求等,先进行立项审核):CMS智汇问答时,...", "lastReplyTime": "2025-07-25 14:44:51", "replyEmployeeName": "孟令辉", "replyEmployeeDeptAndTitle": "", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1948300257955758082", "reportTitle": "AI慧记的问答接入标准的GPTS", "lastReportReplyId": null, "lastReplyContent": "### 背景:\n\n目前,AI 慧记的问答功能并非标准的 GPTs 问答模式。而针对 GPTs 的问答...", "lastReplyTime": "2025-07-24 17:36:44", "replyEmployeeName": "王晓辉", "replyEmployeeDeptAndTitle": "[产品部-产品经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1948202544685666306", "reportTitle": "AI慧记支持拍照/截图功能", "lastReportReplyId": null, "lastReplyContent": "立项申请(请描述需求来源,需求目标,使用用户,是否有时间要求等,先进行立项审核):需求来源:手机 A...", "lastReplyTime": "2025-07-24 15:39:02", "replyEmployeeName": "王晓辉", "replyEmployeeDeptAndTitle": "[产品部-产品经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1947621365586837505", "reportTitle": "会议纪要-07月22日康哲官网问答与工作协同情报库(玄关合同)", "lastReportReplyId": null, "lastReplyContent": " # 会议纪要:07月22日康哲官网问答与工作协同情报库(玄关合同助手作为验证点)\n\n **会议时间...", "lastReplyTime": "2025-07-22 19:58:04", "replyEmployeeName": "罗毓娴", "replyEmployeeDeptAndTitle": "[运营部-内容运营]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1947585613486247938", "reportTitle": "AI修正优化维盛日报提示词的汇报", "lastReportReplyId": null, "lastReplyContent": "一、修订时间工具\n2025.07.22日使用cursor编写优化提示词\n二、修正过程\n1、测试数据:...", "lastReplyTime": "2025-07-22 18:55:14", "replyEmployeeName": "庄诗吟", "replyEmployeeDeptAndTitle": "[产品部-测试工程师]", "type": "汇报提交通知"}, {"unReadCount": 2, "reportId": "1942184429818621954", "reportTitle": "AI 应用技术选型规范文档 (V1.0)", "lastReportReplyId": "1947542074572464129", "lastReplyContent": "已记录", "lastReplyTime": "2025-07-22 14:19:49", "replyEmployeeName": "程敏", "replyEmployeeDeptAndTitle": "[AI及数据运维组-数据架构师]", "type": "我收到的回复"}, {"unReadCount": 1, "reportId": "1946114223581339649", "reportTitle": "警惕夏季“补水”变“毁脸”! | 7月禾零【5折内购】助力科学养肤", "lastReportReplyId": null, "lastReplyContent": "夏天空调房 + 室外暴晒,“冰火两重天” 反复折腾\n皮肤屏障脆弱,根本锁不住水,一敷面膜就刺痛?\n这...", "lastReplyTime": "2025-07-21 15:07:33", "replyEmployeeName": "李宁宁", "replyEmployeeDeptAndTitle": "", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1946166020938956801", "reportTitle": "MCP申请规范", "lastReportReplyId": null, "lastReplyContent": "一、概述\n 随着AI应用的深入,我们需要提供更多的MCP工具,为了提升MCP搭建的效率,故建立...", "lastReplyTime": "2025-07-18 19:17:41", "replyEmployeeName": "屈军利", "replyEmployeeDeptAndTitle": "[开发组-部门经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1946121692360581121", "reportTitle": "季节性过敏性鼻炎患者福利来啦!【只需一针,轻松度过过敏季!药物免费!检查免费!还有 各种补贴!】", "lastReportReplyId": null, "lastReplyContent": "尊敬的各位领导同事好:\n 我司正在进行\"一项评价 MG-K10人源化单抗注 射液治疗季...", "lastReplyTime": "2025-07-18 18:37:10", "replyEmployeeName": "汪倩倩", "replyEmployeeDeptAndTitle": "[临床运营组-高级临床监查员]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1946153807146786817", "reportTitle": "周报", "lastReportReplyId": null, "lastReplyContent": "## 一、本周完成的工作\n| 任务链接 | 任务目标 | 本周进展 | 预计耗时(小时) | 实际耗...", "lastReplyTime": "2025-07-18 18:26:26", "replyEmployeeName": "伍孝权", "replyEmployeeDeptAndTitle": "[开发组-Android开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1946121384033910786", "reportTitle": "07月18日AI汇报助手优化需求开发沟通实施计划", "lastReportReplyId": null, "lastReplyContent": " # AI写汇报功能系统功能优化与问题修复会议\n\n\n **会议时间**:7月18日14:16-14:...", "lastReplyTime": "2025-07-18 16:25:46", "replyEmployeeName": "罗毓娴", "replyEmployeeDeptAndTitle": "[运营部-内容运营]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1946097775704125442", "reportTitle": "关于对办公区域进行消杀的温馨提示", "lastReportReplyId": null, "lastReplyContent": "各位同事:\n\n大家好!行政管理部将于7月19日(周六) 16:00-18:20对办公区进行全面例行虫...", "lastReplyTime": "2025-07-18 14:52:57", "replyEmployeeName": "齐慧", "replyEmployeeDeptAndTitle": "[行政管理部-行政助理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1943613876715560962", "reportTitle": "周报", "lastReportReplyId": null, "lastReplyContent": "本周已完成的工作:\n1、erp项目增加二维码登录,工作量8,已完成100%,AI写代码80%\n2、S...", "lastReplyTime": "2025-07-11 20:00:20", "replyEmployeeName": "杨婷", "replyEmployeeDeptAndTitle": "[开发组-Android开发工程师]", "type": "汇报提交通知"}, {"unReadCount": 2, "reportId": "1940656861448884226", "reportTitle": "ios1.9.3版本", "lastReportReplyId": "1942399585529106433", "lastReplyContent": "收到", "lastReplyTime": "2025-07-08 09:45:24", "replyEmployeeName": "侯桐", "replyEmployeeDeptAndTitle": "[产品部-产品总监]", "type": "我收到的回复"}, {"unReadCount": 2, "reportId": "1940658375483576321", "reportTitle": "iOS1.9.1版本", "lastReportReplyId": "1940950878180003841", "lastReplyContent": "已确认", "lastReplyTime": "2025-07-04 09:48:45", "replyEmployeeName": "欧倩", "replyEmployeeDeptAndTitle": "[产品部-测试工程师]", "type": "我收到的回复"}, {"unReadCount": 1, "reportId": "1938171313317650434", "reportTitle": "产品功能提交开发流程-06月26日", "lastReportReplyId": null, "lastReplyContent": "立项申请(请描述需求来源,需求目标,使用用户,是否有时间要求等,先进行立项审核):工作说说支持规划模...", "lastReplyTime": "2025-06-26 17:45:02", "replyEmployeeName": "孟令辉", "replyEmployeeDeptAndTitle": "", "type": "汇报提交通知"}, {"unReadCount": 2, "reportId": "1932046126666891266", "reportTitle": "AI白癜风问诊(初版)", "lastReportReplyId": "1935527594209624065", "lastReplyContent": "已记录", "lastReplyTime": "2025-06-19 10:38:33", "replyEmployeeName": "李广智", "replyEmployeeDeptAndTitle": "[AI及数据运维组-技术总监]", "type": "我收到的回复"}, {"unReadCount": 2, "reportId": "1932984820545421314", "reportTitle": "宋培众-AI-bizcode申请-06月12日", "lastReportReplyId": "1933090636830003201", "lastReplyContent": "测试环境已记录,生产环境请@程敏 执行下脚本\nbizCode: vitiligo_img_recog...", "lastReplyTime": "2025-06-12 17:14:58", "replyEmployeeName": "龙继海", "replyEmployeeDeptAndTitle": "[开发组-Java开发工程师]", "type": "我收到的回复"}, {"unReadCount": 1, "reportId": "1922966957307887618", "reportTitle": "维盛的问题拆分总结与反思", "lastReportReplyId": "1924357530992140290", "lastReplyContent": "已记录", "lastReplyTime": "2025-05-19 14:52:43", "replyEmployeeName": "李广智", "replyEmployeeDeptAndTitle": "[AI及数据运维组-技术总监]", "type": "我收到的回复"}, {"unReadCount": 1, "reportId": "1871469012899090433", "reportTitle": "关于开展 AI 浏览器插件及 “玄关 gpt” 调研的通知", "lastReportReplyId": null, "lastReplyContent": "各位领导、同事:\n 大家好!公司目前正在开发一款AI 浏览器插件,为让它贴合需求、解决痛点,公司特...", "lastReplyTime": "2024-12-24 16:34:33", "replyEmployeeName": "阳叶菊", "replyEmployeeDeptAndTitle": "[人力资源部-人力资源部经理]", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "1858691587874263041", "reportTitle": "会议团队会务支撑-2024117东莞市中医药学会中西医结合消化病学专业委员会学术年会", "lastReportReplyId": null, "lastReplyContent": "一、背景:\n2024117东莞市中医药学会中西医结合消化病学专业委员会学术年会,需要会务支持。\n内容...", "lastReplyTime": "2024-11-19 10:26:58", "replyEmployeeName": "王晓辉", "replyEmployeeDeptAndTitle": "[产品部-产品经理]", "type": "汇报提交通知"}], "serverReturnedSize": 169, "clientLimit": 500, "clientReturnedSize": 169, "defaultClientLimit": 200, "maxClientLimit": 500, "requestedClientLimit": 1000}}
FILE:run-results/report-msg-sample.json
{"resultCode": 1, "resultMsg": null, "data": {"total": 186, "msgList": [{"unReadCount": 1, "reportId": "2037801532216434690", "reportTitle": "【BP审查通知】请确认您的个人BP审查报告及修改要求", "lastReportReplyId": null, "lastReplyContent": "## ⚠️ BP整改复查 - 仍有未完成项\n\n经复查,上次审查报告中指出的核心问题均**未进行整改*...", "lastReplyTime": "2026-03-28 15:58:40", "replyEmployeeName": "BP", "replyEmployeeDeptAndTitle": "", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2037736957693517825", "reportTitle": "【BP审查通知】请确认您的个人BP审查报告及修改要求", "lastReportReplyId": null, "lastReplyContent": "## ⚠️ BP整改复查 - 仍有未完成项\n\n经复查,上次审查报告中指出的绝大部分核心问题**仍未进...", "lastReplyTime": "2026-03-28 11:42:05", "replyEmployeeName": "BP", "replyEmployeeDeptAndTitle": "", "type": "汇报提交通知"}, {"unReadCount": 1, "reportId": "2037364082105139202", "reportTitle": "Skill 工厂系统定义汇报", "lastReportReplyId": null, "lastReplyContent": "各位好,附件是 Skill 工厂系统的完整产品定义,请查阅。 核心能力:与用户协作创建 Skill ...", "lastReplyTime": "2026-03-27 11:00:24", "replyEmployeeName": "张成鹏", "replyEmployeeDeptAndTitle": "[玄关健康-总经理]", "type": "汇报提交通知"}], "serverReturnedSize": 169, "clientLimit": 3, "clientReturnedSize": 3}}
FILE:run-results/report-unread-sample.json
{"resultCode": 1, "resultMsg": null, "data": {"total": 276, "list": [{"reportId": "2037801532216434690", "reportRecordType": 5, "main": "【BP审查通知】请确认您的个人BP审查报告及修改要求", "writeEmpName": "BP", "createTime": "2026-03-28 15:58:40", "detail": {"aiSummary": "**汇报摘要**\n\n**主题:** BP审查通知 - 个人BP整改复查未通过 \n**核心结论:** 经复查,上次审查指出的核心问题**均未整改**,报告在多个关键方面仍不符合要求。\n\n**主要未整改项及要求:**\n\n1. **描述不规范**:目标与关键成果仍使用“建立”、“推动”等动词,需改为“已建立并应用”等静态事实描述。\n2. **衡量标准缺失**:多项关键成果的衡量标准字段为空或不可验收,需补充具体、可量化的交付物指标。\n3. **成果项冗余**:目标2下的关键成果数量过多(增至15条),未按要求合并,需按逻辑维度重组并控制在5条以内。\n4. **内容不专业**:存在“这个任务必须要解决哦”等口语化表述,需修改为专业的技术或管理描述。\n5. **责任链异常**:部分事项的责任人仍为非本级组织成员,需调整为直接负责人或明确其管控责任。\n\n**总体要求:** 请立即针对以上五点进行最终修改,以确保BP的逻辑严谨性与专业性。", "progressList": [], "needAction": "需要建议", "hasSubsequentDecision": false}}, {"reportId": "2037736957693517825", "reportRecordType": 5, "main": "【BP审查通知】请确认您的个人BP审查报告及修改要求", "writeEmpName": "BP", "createTime": "2026-03-28 11:42:05", "detail": {"aiSummary": "**汇报摘要**\n\n**主题:** BP审查通知 - 要求确认报告并完成最终整改\n\n**核心问题:** 经复查,个人BP报告中的核心问题**绝大部分仍未整改**,数据在描述规范、衡量标准及组织逻辑上均未达到要求。\n\n**主要未整改项及要求:**\n\n1. **静态事实描述缺失:** 多个目标及关键成果仍使用动词式描述(如“建立”),需改为已完成的事实状态定格描述(如“已建立”)。\n2. **衡量标准严重缺失:** 多个关键成果的衡量标准字段为空,需补充具体、可量化的指标或明确交付物。\n3. **关键成果数量冗余:** 目标2下的关键成果数量过多(14条),需合并同类项,控制在5条以内,建议按指定维度重组。\n4. **内容质量与专业性问题:** 报告中存在口语化等非专业描述,需修改为专业的技术或业务表述。\n5. **人员责任链异常:** 部分关键成果的责任人非本级组织成员,需调整为指定人员或明确管控责任。\n\n**总体要求:** 请务必尽快根据上述具体建议完成最终整改,以确保BP逻辑的严谨性与专业性。", "progressList": [], "needAction": "需要建议", "hasSubsequentDecision": false}}, {"reportId": "2037364082105139202", "reportRecordType": 5, "main": "Skill 工厂系统定义汇报", "writeEmpName": "张成鹏", "createTime": "2026-03-27 11:00:24", "detail": {"aiSummary": "**汇报摘要**\n\n**汇报人:** 张成鹏 \n**主题:** Skill 工厂系统定义 \n**核心内容:** \n1. **系统定位:** 一个用于创建、测试、发布和管理 Skill(技能)的协作平台。 \n2. **核心能力:** 支持与用户协作创建 Skill → 自动生成测试 → 发布至 ClawHub 及公司内部市场 → 版本管理 → 问题反馈闭环。 \n3. **工作模式:** \n - **完整流程模式:** 从需求开始,涵盖规划、设计、复盘、审计、确认、调研、开发全流程。 \n - **外脑模式:** 导入现有方案,直接进入开发阶段。 \n4. **模型使用策略:** \n - 规划阶段采用低成本模型。 \n - 方案评审使用 Claude Code/OpenAI 进行 2-3 轮复审。 \n - 开发阶段使用 Claude Code/GLM-5。 \n - 发布前再进行 1-2 轮复审。 \n\n**备注:** 完整产品定义详见附件(将单独发送)。 \n**汇报时间:** 2026年3月27日 11:00", "progressList": [], "needAction": null, "hasSubsequentDecision": false}}], "pageNum": 1, "pageSize": 100, "size": 100, "serverReturnedSize": 100, "clientLimit": 3, "clientReturnedSize": 3}}
FILE:run-results/work-collab-smoke-20260328.json
{
"run_at": "2026-03-28T19:19:00+08:00",
"source_doc": "工作协同API说明.md",
"app_key_used": true,
"recipient": {
"name": "屈军利",
"empId": "1514822127830339585",
"personId": 12139,
"corpId": "1509805893730611201"
},
"current_user": {
"name": "宋培众",
"empId": "1514822080577310721"
},
"created_resources": {
"uploaded_resource_id": "2037895404074434561",
"test_report_id": "2037895527831597058",
"test_reply_id": "2037895545128869890",
"test_plan_id": "2037896299117928449",
"completed_todo_id": "2037827358957887489"
},
"scripts": [
{
"script": "scripts/user-search/search-emp.py",
"status": "success"
},
{
"script": "scripts/employee-service/get-by-person-ids.py",
"status": "success"
},
{
"script": "scripts/employee-service/get-org-info.py",
"status": "success"
},
{
"script": "scripts/file-service/upload-file.py",
"status": "success",
"resourceId": "2037895404074434561"
},
{
"script": "scripts/file-service/get-download-info.py",
"status": "success"
},
{
"script": "scripts/report-write/submit.py",
"status": "success",
"reportId": "2037895527831597058"
},
{
"script": "scripts/report-write/reply.py",
"status": "success",
"replyId": "2037895545128869890"
},
{
"script": "scripts/inbox/get-list.py",
"status": "success"
},
{
"script": "scripts/outbox/get-list.py",
"status": "success"
},
{
"script": "scripts/report-detail/get-info.py",
"status": "success"
},
{
"script": "scripts/tasks/get-page.py",
"status": "success"
},
{
"script": "scripts/tasks/get-simple-plan-and-report-info.py",
"status": "success"
},
{
"script": "scripts/todos/get-list.py",
"status": "success"
},
{
"script": "scripts/todos/list-created-feedbacks.py",
"status": "success",
"note": "响应时间明显慢于其他普通接口"
},
{
"script": "scripts/todos/complete.py",
"status": "success",
"todoId": "2037827358957887489"
},
{
"script": "scripts/templates/get-list.py",
"status": "success",
"note": "接口返回 data=null"
},
{
"script": "scripts/templates/get-by-ids.py",
"status": "success"
},
{
"script": "scripts/plugin-report/get-list.py",
"status": "success"
},
{
"script": "scripts/plugin-report/get-latest-list.py",
"status": "success"
},
{
"script": "scripts/plugin-report/get-unread-list.py",
"status": "success"
},
{
"script": "scripts/report-query/get-todo-list.py",
"status": "success"
},
{
"script": "scripts/report-query/get-unread-list.py",
"status": "success",
"note": "返回结果体很大,终端输出被截断"
},
{
"script": "scripts/report-query/is-report-read.py",
"status": "success",
"data": true
},
{
"script": "scripts/report-message/find-my-new-msg-list.py",
"status": "success",
"note": "返回结果体很大,终端输出被截断"
},
{
"script": "scripts/report-message/read-report.py",
"status": "success"
},
{
"script": "scripts/ai-qa/ask-sse.py",
"status": "success_with_anomaly",
"note": "answer 字段拼接了多段 SSE JSON 片段,不是干净纯文本"
},
{
"script": "scripts/plan-create/create-simple.py",
"status": "success",
"planId": "2037896299117928449"
}
],
"anomalies": [
"templates/get-list 返回 resultCode=1 但 data 为 null。",
"todos/list-created-feedbacks 与 ai-qa/ask-sse 耗时明显偏长。",
"ai-qa/ask-sse 的 answer 字段是 SSE 片段拼接结果,后处理不完整。",
"report-query/get-unread-list 与 report-message/find-my-new-msg-list 的返回体很大,终端显示被截断,但接口本身成功。"
]
}
FILE:scripts/ai-qa/README.md
# 脚本清单 — ai-qa
## 共享依赖
无
## 脚本列表
| 脚本 | 对应接口 | 用途 |
|---|---|---|
| `ask-sse.py` | `POST /work-report/open-platform/report/aiSseQaV2` | 调用接口,返回指定汇报集合的 SSE 问答结果 |
## 使用方式
```bash
# 执行前需要预先准备好 appKey,并通过环境变量传入
export XG_BIZ_API_KEY="your-app-key"
# 或 export XG_APP_KEY="your-app-key"
# 执行脚本
python3 scripts/ai-qa/ask-sse.py "请总结这几篇汇报的核心风险" "12345,67890"
```
## 输出说明
所有脚本的输出均为 **JSON 格式**,脚本会将 SSE 结果合并后输出。
补充说明:
- `data.answer` 只保留聚合后的正文
- `data.metrics` 会拆分保留 `firstTextDelay`、`costMoney`、`totalTimeCost`
- `data.eventCount` 表示本次聚合到的 SSE 事件数量
## 规范
1. **必须使用 Python** 编写
2. **鉴权遵循** `cms-auth-skills/SKILL.md` 规范,不包含登录发起逻辑
3. **入参定义以** `openapi/` 文档为准
FILE:scripts/ai-qa/ask-sse.py
#!/usr/bin/env python3
"""
ai-qa / ask-sse 脚本
用途:对指定汇报集合发起 AI SSE 问答,并合并为最终 JSON
"""
import argparse
import json
import os
import sys
import requests
import warnings
# 禁用 InsecureRequestWarning (因为 verify=False)
warnings.filterwarnings("ignore", category=requests.packages.urllib3.exceptions.InsecureRequestWarning)
def _resolve_app_key() -> str:
if any(arg in {"-h", "--help"} for arg in sys.argv[1:]):
return ""
app_key = os.environ.get("XG_BIZ_API_KEY") or os.environ.get("XG_APP_KEY")
if not app_key:
print("错误: 请设置环境变量 XG_BIZ_API_KEY 或 XG_APP_KEY", file=sys.stderr)
sys.exit(1)
return app_key
API_URL = "https://sg-al-cwork-web.mediportal.com.cn/open-api/work-report/open-platform/report/aiSseQaV2"
def _parse_sse_event(data_content):
try:
return json.loads(data_content)
except json.JSONDecodeError:
return data_content
def call_api_sse(app_key, content, report_ids_str):
headers = {"appKey": app_key, "Accept": "text/event-stream", "Content-Type": "application/json"}
report_id_list = []
for rid in report_ids_str.split(','):
if rid.strip().isdigit():
report_id_list.append(int(rid.strip()))
if not report_id_list:
print("错误: 请至少提供一个 reportId", file=sys.stderr)
sys.exit(1)
payload = {
"userContent": content,
"aiType": 42,
"reportIdList": report_id_list
}
answer_parts = []
metrics = {}
raw_events = []
try:
response = requests.post(
API_URL,
json=payload,
headers=headers,
stream=True,
verify=False,
allow_redirects=True,
timeout=120,
)
response.raise_for_status()
for line in response.iter_lines():
if not line:
continue
line_str = line.decode("utf-8").strip()
if line_str.startswith("data:"):
data_content = line_str[5:].strip()
if data_content == "[DONE]":
break
event = _parse_sse_event(data_content)
raw_events.append(event)
if isinstance(event, dict):
content_part = event.get("content")
if content_part:
answer_parts.append(content_part)
event_id = event.get("id")
if event_id in {"firstTextDelay", "costMoney", "totalTimeCost"}:
metrics[event_id] = {k: v for k, v in event.items() if k != "id"}
elif isinstance(event, str):
if event != "ok":
answer_parts.append(event)
except Exception as exc:
raise Exception(f"请求失败: {exc}")
result = {
"resultCode": 1,
"data": {
"answer": "".join(answer_parts).strip(),
},
}
if metrics:
result["data"]["metrics"] = metrics
if raw_events:
result["data"]["eventCount"] = len(raw_events)
return result
def main():
app_key = _resolve_app_key()
parser = argparse.ArgumentParser()
parser.add_argument("content")
parser.add_argument("report_ids")
args = parser.parse_args()
# 获取并输出合并后的 JSON
result = call_api_sse(app_key, args.content, args.report_ids)
print(json.dumps(result, ensure_ascii=False))
if __name__ == "__main__": main()
FILE:scripts/employee-service/README.md
# 脚本清单 — employee-service
## 共享依赖
无
## 脚本列表
| 脚本 | 对应接口 | 用途 |
|---|---|---|
| `get-by-person-ids.py` | `POST /cwork-user/employee/getByPersonIds/{corpId}` | 批量获取员工信息 |
| `get-org-info.py` | `GET /cwork-user/employee/getEmployeeOrgInfo` | 获取员工组织架构信息 |
## 使用方式
```bash
export XG_BIZ_API_KEY="your-app-key"
# 或 export XG_APP_KEY="your-app-key"
# 推荐:显式参数名
python3 scripts/employee-service/get-by-person-ids.py --corp-id 123456 --person-ids "10001,10002"
python3 scripts/employee-service/get-org-info.py --emp-id 10001
# 兼容:旧的位置参数
python3 scripts/employee-service/get-by-person-ids.py 123456 "10001,10002"
python3 scripts/employee-service/get-org-info.py 10001
```
## 输出说明
所有脚本的输出均为 **JSON 格式**。
补充说明:
- 支持 `-h/--help`,即使当前环境还未注入 `appKey`
- 推荐优先使用 `--corp-id`、`--person-ids`、`--emp-id`
## 规范
1. **必须使用 Python** 编写
2. **鉴权遵循** `cms-auth-skills/SKILL.md` 规范,不包含登录发起逻辑
3. **入参定义以** `openapi/` 文档为准
FILE:scripts/employee-service/get-by-person-ids.py
#!/usr/bin/env python3
"""
employee-service / get-by-person-ids 脚本
用途:根据 corpId 和 personId 列表批量获取员工信息
"""
import argparse
import json
import os
import sys
import requests
import warnings
# 禁用 InsecureRequestWarning (因为 verify=False)
warnings.filterwarnings("ignore", category=requests.packages.urllib3.exceptions.InsecureRequestWarning)
def _resolve_app_key() -> str:
if any(arg in {"-h", "--help"} for arg in sys.argv[1:]):
return ""
app_key = os.environ.get("XG_BIZ_API_KEY") or os.environ.get("XG_APP_KEY")
if not app_key:
print("错误: 请设置环境变量 XG_BIZ_API_KEY 或 XG_APP_KEY", file=sys.stderr)
sys.exit(1)
return app_key
API_URL = "https://sg-al-cwork-web.mediportal.com.cn/open-api/cwork-user/employee/getByPersonIds"
def _parse_ids(raw: str):
values = []
for item in raw.split(","):
item = item.strip()
if item:
values.append(int(item))
return values
def call_api(app_key: str, corp_id: str, person_ids_raw: str):
headers = {"appKey": app_key, "Content-Type": "application/json"}
payload = _parse_ids(person_ids_raw)
try:
response = requests.post(
f"{API_URL}/{corp_id}",
json=payload,
headers=headers,
verify=False,
allow_redirects=True,
timeout=60,
)
response.raise_for_status()
return response.json()
except Exception as exc:
raise Exception(f"请求失败: {exc}")
def main():
parser = argparse.ArgumentParser(description="根据 corpId 和 personId 列表批量获取员工信息")
parser.add_argument("corp_id", nargs="?", default="", help="corpId,兼容旧的位置参数用法")
parser.add_argument("person_ids", nargs="?", default="", help="逗号分隔的 personId 列表,兼容旧的位置参数用法")
parser.add_argument("--corp-id", dest="corp_id_opt", default="", help="企业 corpId")
parser.add_argument("--person-ids", dest="person_ids_opt", default="", help="逗号分隔的 personId 列表")
args = parser.parse_args()
corp_id = args.corp_id_opt or args.corp_id
person_ids = args.person_ids_opt or args.person_ids
if not corp_id or not person_ids:
parser.error("请提供 corpId 和 personId 列表")
app_key = _resolve_app_key()
result = call_api(app_key, corp_id, person_ids)
print(json.dumps(result, ensure_ascii=False))
if __name__ == "__main__":
main()
FILE:scripts/employee-service/get-org-info.py
#!/usr/bin/env python3
"""
employee-service / get-org-info 脚本
用途:获取员工组织架构信息
"""
import argparse
import json
import os
import sys
import requests
import warnings
# 禁用 InsecureRequestWarning (因为 verify=False)
warnings.filterwarnings("ignore", category=requests.packages.urllib3.exceptions.InsecureRequestWarning)
def _resolve_app_key() -> str:
if any(arg in {"-h", "--help"} for arg in sys.argv[1:]):
return ""
app_key = os.environ.get("XG_BIZ_API_KEY") or os.environ.get("XG_APP_KEY")
if not app_key:
print("错误: 请设置环境变量 XG_BIZ_API_KEY 或 XG_APP_KEY", file=sys.stderr)
sys.exit(1)
return app_key
API_URL = "https://sg-al-cwork-web.mediportal.com.cn/open-api/cwork-user/employee/getEmployeeOrgInfo"
def call_api(app_key: str, emp_id: str):
headers = {"appKey": app_key}
params = {"empId": emp_id}
try:
response = requests.get(
API_URL,
params=params,
headers=headers,
verify=False,
allow_redirects=True,
timeout=60,
)
response.raise_for_status()
return response.json()
except Exception as exc:
raise Exception(f"请求失败: {exc}")
def main():
parser = argparse.ArgumentParser(description="获取员工组织架构信息")
parser.add_argument("emp_id", nargs="?", default="", help="empId,兼容旧的位置参数用法")
parser.add_argument("--emp-id", dest="emp_id_opt", default="", help="员工 empId")
args = parser.parse_args()
emp_id = args.emp_id_opt or args.emp_id
if not emp_id:
parser.error("请提供 empId 作为参数")
app_key = _resolve_app_key()
result = call_api(app_key, emp_id)
print(json.dumps(result, ensure_ascii=False))
if __name__ == "__main__":
main()
FILE:scripts/file-service/README.md
# 脚本清单 — file-service
## 共享依赖
无
## 脚本列表
| 脚本 | 对应接口 | 用途 |
|---|---|---|
| `upload-file.py` | `POST /cwork-file/uploadWholeFile` | 上传本地文件并返回资源 ID |
| `get-download-info.py` | `GET /cwork-file/getDownloadInfo` | 获取文件下载链接与元信息 |
## 使用方式
```bash
export XG_BIZ_API_KEY="your-app-key"
# 或 export XG_APP_KEY="your-app-key"
# 推荐:显式参数名
python3 scripts/file-service/upload-file.py --file /path/to/file.png
python3 scripts/file-service/get-download-info.py --resource-id 123456789012345
# 兼容:旧的位置参数
python3 scripts/file-service/upload-file.py /path/to/file.png
python3 scripts/file-service/get-download-info.py 123456789012345
```
## 输出说明
所有脚本的输出均为 **JSON 格式**。
补充说明:
- 支持 `-h/--help`,即使当前环境还未注入 `appKey`
- 推荐优先使用 `--file`、`--resource-id`
## 规范
1. **必须使用 Python** 编写
2. **鉴权遵循** `cms-auth-skills/SKILL.md` 规范,不包含登录发起逻辑
3. **入参定义以** `openapi/` 文档为准
FILE:scripts/file-service/get-download-info.py
#!/usr/bin/env python3
"""
file-service / get-download-info 脚本
用途:获取文件下载链接与元信息
"""
import argparse
import json
import os
import sys
import requests
import warnings
# 禁用 InsecureRequestWarning (因为 verify=False)
warnings.filterwarnings("ignore", category=requests.packages.urllib3.exceptions.InsecureRequestWarning)
def _resolve_app_key() -> str:
if any(arg in {"-h", "--help"} for arg in sys.argv[1:]):
return ""
app_key = os.environ.get("XG_BIZ_API_KEY") or os.environ.get("XG_APP_KEY")
if not app_key:
print("错误: 请设置环境变量 XG_BIZ_API_KEY 或 XG_APP_KEY", file=sys.stderr)
sys.exit(1)
return app_key
API_URL = "https://sg-al-cwork-web.mediportal.com.cn/open-api/cwork-file/getDownloadInfo"
def call_api(app_key: str, resource_id: str):
headers = {"appKey": app_key}
params = {"resourceId": resource_id}
try:
response = requests.get(
API_URL,
params=params,
headers=headers,
verify=False,
allow_redirects=True,
timeout=60,
)
response.raise_for_status()
return response.json()
except Exception as exc:
raise Exception(f"请求失败: {exc}")
def main():
parser = argparse.ArgumentParser(description="获取文件下载链接与元信息")
parser.add_argument("resource_id", nargs="?", default="", help="resourceId,兼容旧的位置参数用法")
parser.add_argument("--resource-id", dest="resource_id_opt", default="", help="文件资源 ID")
args = parser.parse_args()
resource_id = args.resource_id_opt or args.resource_id
if not resource_id:
parser.error("请提供 resourceId 作为参数")
app_key = _resolve_app_key()
result = call_api(app_key, resource_id)
print(json.dumps(result, ensure_ascii=False))
if __name__ == "__main__":
main()
FILE:scripts/file-service/upload-file.py
#!/usr/bin/env python3
"""
file-service / upload-file 脚本
用途:上传本地文件并返回资源 ID
"""
import argparse
import json
import os
import sys
import requests
import warnings
# 禁用 InsecureRequestWarning (因为 verify=False)
warnings.filterwarnings("ignore", category=requests.packages.urllib3.exceptions.InsecureRequestWarning)
def _resolve_app_key() -> str:
if any(arg in {"-h", "--help"} for arg in sys.argv[1:]):
return ""
app_key = os.environ.get("XG_BIZ_API_KEY") or os.environ.get("XG_APP_KEY")
if not app_key:
print("错误: 请设置环境变量 XG_BIZ_API_KEY 或 XG_APP_KEY", file=sys.stderr)
sys.exit(1)
return app_key
API_URL = "https://sg-al-cwork-web.mediportal.com.cn/open-api/cwork-file/uploadWholeFile"
def call_api(app_key: str, file_path: str):
if not os.path.isfile(file_path):
print(f"错误: 文件不存在: {file_path}", file=sys.stderr)
sys.exit(1)
headers = {
"appKey": app_key,
}
try:
with open(file_path, "rb") as f:
files = {"file": (os.path.basename(file_path), f)}
response = requests.post(
API_URL,
files=files,
headers=headers,
verify=False,
allow_redirects=True,
timeout=120,
)
response.raise_for_status()
return response.json()
except Exception as exc:
raise Exception(f"请求失败: {exc}")
def main():
parser = argparse.ArgumentParser(description="上传本地文件并返回资源 ID")
parser.add_argument("file_path", nargs="?", default="", help="本地文件路径,兼容旧的位置参数用法")
parser.add_argument("--file", dest="file_path_opt", default="", help="本地文件路径")
args = parser.parse_args()
file_path = args.file_path_opt or args.file_path
if not file_path:
parser.error("请提供文件路径作为参数")
app_key = _resolve_app_key()
result = call_api(app_key, file_path)
print(json.dumps(result, ensure_ascii=False))
if __name__ == "__main__":
main()
FILE:scripts/inbox/README.md
# 脚本清单 — inbox
## 共享依赖
无
## 脚本列表
| 脚本 | 对应接口 | 用途 |
|---|---|---|
| `get-list.py` | `POST /work-report/report/record/inbox` | 调用接口,输出 JSON 结构化的收件箱汇报列表 |
## 使用方式
```bash
# 执行前需要预先准备好 appKey,并通过环境变量传入
export XG_BIZ_API_KEY="your-app-key"
# 或 export XG_APP_KEY="your-app-key"
# 执行脚本
python3 scripts/inbox/get-list.py --page-index 1 --page-size 20
# 需要高级筛选时,直接传完整 JSON 请求体
python3 scripts/inbox/get-list.py --body-json '{"pageIndex":1,"pageSize":20,"readStatus":0}'
```
## 输出说明
所有脚本的输出均为 **JSON 格式**。
## 规范
1. **必须使用 Python** 编写
2. **鉴权遵循** `cms-auth-skills/SKILL.md` 规范,不包含登录发起逻辑
3. **入参定义以** `openapi/` 文档为准
FILE:scripts/inbox/get-list.py
#!/usr/bin/env python3
"""
inbox / get-list 脚本
用途:获取收件箱汇报列表
"""
import argparse
import sys
import os
import json
import requests
import warnings
# 禁用 InsecureRequestWarning (因为 verify=False)
warnings.filterwarnings("ignore", category=requests.packages.urllib3.exceptions.InsecureRequestWarning)
def _resolve_app_key() -> str:
if any(arg in {"-h", "--help"} for arg in sys.argv[1:]):
return ""
app_key = os.environ.get("XG_BIZ_API_KEY") or os.environ.get("XG_APP_KEY")
if not app_key:
print("错误: 请设置环境变量 XG_BIZ_API_KEY 或 XG_APP_KEY", file=sys.stderr)
sys.exit(1)
return app_key
API_URL = "https://sg-al-cwork-web.mediportal.com.cn/open-api/work-report/report/record/inbox"
def call_api(app_key, page_index=1, page_size=10, body_json=""):
headers = {"appKey": app_key, "Content-Type": "application/json"}
if body_json:
payload = json.loads(body_json)
else:
payload = {
"pageIndex": page_index,
"pageSize": page_size
}
try:
response = requests.post(
API_URL,
json=payload,
headers=headers,
verify=False,
allow_redirects=True,
timeout=60,
)
response.raise_for_status()
return response.json()
except Exception as exc:
raise Exception(f"请求失败: {exc}")
def main():
app_key = _resolve_app_key()
parser = argparse.ArgumentParser()
parser.add_argument("--page-index", type=int, default=1)
parser.add_argument("--page-size", type=int, default=10)
parser.add_argument("--body-json", default="")
args = parser.parse_args()
result = call_api(
app_key,
page_index=args.page_index,
page_size=args.page_size,
body_json=args.body_json,
)
print(json.dumps(result, ensure_ascii=False))
if __name__ == "__main__": main()
FILE:scripts/outbox/README.md
# 脚本清单 — outbox
## 共享依赖
无
## 脚本列表
| 脚本 | 对应接口 | 用途 |
|---|---|---|
| `get-list.py` | `POST /work-report/report/record/outbox` | 调用接口,输出 JSON 结构化的发件箱汇报列表 |
## 使用方式
```bash
# 执行前需要预先准备好 appKey,并通过环境变量传入
export XG_BIZ_API_KEY="your-app-key"
# 或 export XG_APP_KEY="your-app-key"
# 执行脚本
python3 scripts/outbox/get-list.py --page-index 1 --page-size 20
# 需要高级筛选时,直接传完整 JSON 请求体
python3 scripts/outbox/get-list.py --body-json '{"pageIndex":1,"pageSize":20,"orderColumn":"createTime"}'
```
## 输出说明
所有脚本的输出均为 **JSON 格式**。
## 规范
1. **必须使用 Python** 编写
2. **鉴权遵循** `cms-auth-skills/SKILL.md` 规范,不包含登录发起逻辑
3. **入参定义以** `openapi/` 文档为准
FILE:scripts/outbox/get-list.py
#!/usr/bin/env python3
"""
outbox / get-list 脚本
用途:获取发件箱汇报列表
"""
import argparse
import sys
import os
import json
import requests
import warnings
# 禁用 InsecureRequestWarning (因为 verify=False)
warnings.filterwarnings("ignore", category=requests.packages.urllib3.exceptions.InsecureRequestWarning)
def _resolve_app_key() -> str:
if any(arg in {"-h", "--help"} for arg in sys.argv[1:]):
return ""
app_key = os.environ.get("XG_BIZ_API_KEY") or os.environ.get("XG_APP_KEY")
if not app_key:
print("错误: 请设置环境变量 XG_BIZ_API_KEY 或 XG_APP_KEY", file=sys.stderr)
sys.exit(1)
return app_key
API_URL = "https://sg-al-cwork-web.mediportal.com.cn/open-api/work-report/report/record/outbox"
def call_api(app_key, page_index=1, page_size=10, body_json=""):
headers = {"appKey": app_key, "Content-Type": "application/json"}
if body_json:
payload = json.loads(body_json)
else:
payload = {
"pageIndex": page_index,
"pageSize": page_size
}
try:
response = requests.post(
API_URL,
json=payload,
headers=headers,
verify=False,
allow_redirects=True,
timeout=60,
)
response.raise_for_status()
return response.json()
except Exception as exc:
raise Exception(f"请求失败: {exc}")
def main():
app_key = _resolve_app_key()
parser = argparse.ArgumentParser()
parser.add_argument("--page-index", type=int, default=1)
parser.add_argument("--page-size", type=int, default=10)
parser.add_argument("--body-json", default="")
args = parser.parse_args()
result = call_api(
app_key,
page_index=args.page_index,
page_size=args.page_size,
body_json=args.body_json,
)
print(json.dumps(result, ensure_ascii=False))
if __name__ == "__main__": main()
FILE:scripts/plan-create/README.md
# 脚本清单 — plan-create
## 共享依赖
无
## 脚本列表
| 脚本 | 对应接口 | 用途 |
|---|---|---|
| `create-simple.py` | `POST /work-report/open-platform/report/plan/create` | 调用接口,创建高级工作任务 |
## 使用方式
```bash
# 执行前需要预先准备好 appKey,并通过环境变量传入
export XG_BIZ_API_KEY="your-app-key"
# 或 export XG_APP_KEY="your-app-key"
# 执行脚本
python3 scripts/plan-create/create-simple.py \
--main "开放平台测试-创建高级任务" \
--needful "<p>任务要求</p>" \
--target "<p>任务目标</p>" \
--report-emp-ids 1512393035869810690 \
--end-time 1774915200000
```
## 输出说明
所有脚本的输出均为 **JSON 格式**。
## 规范
1. **必须使用 Python** 编写
2. **鉴权遵循** `cms-auth-skills/SKILL.md` 规范,不包含登录发起逻辑
3. **入参定义以** `openapi/` 文档为准
FILE:scripts/plan-create/create-simple.py
#!/usr/bin/env python3
"""
plan-create / create-simple 脚本
用途:创建高级工作任务
"""
import argparse
import sys
import os
import json
import requests
import warnings
# 禁用 InsecureRequestWarning (因为 verify=False)
warnings.filterwarnings("ignore", category=requests.packages.urllib3.exceptions.InsecureRequestWarning)
def _resolve_app_key() -> str:
if any(arg in {"-h", "--help"} for arg in sys.argv[1:]):
return ""
app_key = os.environ.get("XG_BIZ_API_KEY") or os.environ.get("XG_APP_KEY")
if not app_key:
print("错误: 请设置环境变量 XG_BIZ_API_KEY 或 XG_APP_KEY", file=sys.stderr)
sys.exit(1)
return app_key
API_URL = "https://sg-al-cwork-web.mediportal.com.cn/open-api/work-report/open-platform/report/plan/create"
def _parse_emp_id_list(raw):
if not raw:
return []
values = []
for item in raw.split(","):
item = item.strip()
if item:
values.append(int(item))
return values
def call_api(app_key, payload):
headers = {"appKey": app_key, "Content-Type": "application/json"}
try:
response = requests.post(
API_URL,
json=payload,
headers=headers,
verify=False,
allow_redirects=True,
timeout=60,
)
response.raise_for_status()
return response.json()
except Exception as exc:
raise Exception(f"请求失败: {exc}")
def main():
app_key = _resolve_app_key()
parser = argparse.ArgumentParser()
parser.add_argument("--main", required=True)
parser.add_argument("--needful", required=True)
parser.add_argument("--target", required=True)
parser.add_argument("--type-id", type=int, default=9999)
parser.add_argument("--report-emp-ids", required=True)
parser.add_argument("--owner-emp-ids", default="")
parser.add_argument("--assist-emp-ids", default="")
parser.add_argument("--supervisor-emp-ids", default="")
parser.add_argument("--copy-emp-ids", default="")
parser.add_argument("--observer-emp-ids", default="")
parser.add_argument("--end-time", type=int, required=True)
parser.add_argument("--push-now", type=int, default=1, choices=[0, 1])
args = parser.parse_args()
payload = {
"main": args.main,
"needful": args.needful,
"target": args.target,
"typeId": args.type_id,
"reportEmpIdList": _parse_emp_id_list(args.report_emp_ids),
"ownerEmpIdList": _parse_emp_id_list(args.owner_emp_ids),
"assistEmpIdList": _parse_emp_id_list(args.assist_emp_ids),
"supervisorEmpIdList": _parse_emp_id_list(args.supervisor_emp_ids),
"copyEmpIdList": _parse_emp_id_list(args.copy_emp_ids),
"observerEmpIdList": _parse_emp_id_list(args.observer_emp_ids),
"endTime": args.end_time,
"pushNow": args.push_now,
}
result = call_api(app_key, payload)
print(json.dumps(result, ensure_ascii=False))
if __name__ == "__main__": main()
FILE:scripts/plugin-report/README.md
# 脚本清单 — plugin-report
## 共享依赖
无
## 脚本列表
| 脚本 | 对应接口 | 用途 |
|---|---|---|
| `get-list.py` | `POST /work-report/plugin/report/list` | 获取插件聚合结果,包含最新待办与未读汇报 |
| `get-latest-list.py` | `POST /work-report/plugin/report/latestList` | 获取插件场景下的最新待办列表 |
| `get-unread-list.py` | `POST /work-report/plugin/report/unreadList` | 获取插件场景下的未读汇报列表 |
## 使用方式
```bash
# 执行前需要预先准备好 appKey,并通过环境变量传入
export XG_BIZ_API_KEY="your-app-key"
# 或 export XG_APP_KEY="your-app-key"
# 获取聚合结果
python3 scripts/plugin-report/get-list.py --last-update-time 0 --page-index 1 --page-size 10
# 获取最新待办列表
python3 scripts/plugin-report/get-latest-list.py --last-update-time 0 --page-index 1 --page-size 10
# 获取未读汇报列表
python3 scripts/plugin-report/get-unread-list.py --last-update-time 0 --page-index 1 --page-size 10
# 复杂筛选可直接传完整 JSON 请求体
python3 scripts/plugin-report/get-list.py --body-json '{"pageIndex":1,"pageSize":10,"lastUpdateTime":0}'
python3 scripts/plugin-report/get-list.py --body-file /path/to/plugin-report.json
```
## 输出说明
所有脚本的输出均为 **JSON 格式**。
## 规范
1. **必须使用 Python** 编写
2. **鉴权遵循** `cms-auth-skills/SKILL.md` 规范,不包含登录发起逻辑
3. **入参定义以** `openapi/` 文档为准
FILE:scripts/plugin-report/get-latest-list.py
#!/usr/bin/env python3
"""
plugin-report / get-latest-list 脚本
用途:获取插件场景下的最新待办列表
"""
import argparse
import json
import os
import sys
import requests
import warnings
# 禁用 InsecureRequestWarning (因为 verify=False)
warnings.filterwarnings("ignore", category=requests.packages.urllib3.exceptions.InsecureRequestWarning)
API_URL = "https://sg-al-cwork-web.mediportal.com.cn/open-api/work-report/plugin/report/latestList"
def _resolve_app_key() -> str:
if any(arg in {"-h", "--help"} for arg in sys.argv[1:]):
return ""
app_key = os.environ.get("XG_BIZ_API_KEY") or os.environ.get("XG_APP_KEY")
if not app_key:
print("错误: 请设置环境变量 XG_BIZ_API_KEY 或 XG_APP_KEY", file=sys.stderr)
sys.exit(1)
return app_key
def _load_body(body_json: str, body_file: str):
if body_json:
return json.loads(body_json)
if body_file:
with open(body_file, "r", encoding="utf-8") as handle:
return json.load(handle)
return None
def build_payload(args):
direct_payload = _load_body(args.body_json, args.body_file)
if direct_payload is not None:
return direct_payload
return {
"pageIndex": args.page_index,
"pageSize": args.page_size,
"lastUpdateTime": args.last_update_time,
}
def call_api(app_key: str, payload: dict):
headers = {"appKey": app_key, "Content-Type": "application/json"}
try:
response = requests.post(
API_URL,
json=payload,
headers=headers,
verify=False,
allow_redirects=True,
timeout=60,
)
response.raise_for_status()
return response.json()
except Exception as exc:
raise Exception(f"请求失败: {exc}")
def main():
app_key = _resolve_app_key()
parser = argparse.ArgumentParser()
parser.add_argument("--page-index", type=int, default=1)
parser.add_argument("--page-size", type=int, default=20)
parser.add_argument("--last-update-time", type=int, default=0)
parser.add_argument("--body-json", default="")
parser.add_argument("--body-file", default="")
args = parser.parse_args()
result = call_api(app_key, build_payload(args))
print(json.dumps(result, ensure_ascii=False))
if __name__ == "__main__":
main()
FILE:scripts/plugin-report/get-list.py
#!/usr/bin/env python3
"""
plugin-report / get-list 脚本
用途:获取插件聚合结果(最新待办 + 未读汇报)
"""
import argparse
import json
import os
import sys
import requests
import warnings
# 禁用 InsecureRequestWarning (因为 verify=False)
warnings.filterwarnings("ignore", category=requests.packages.urllib3.exceptions.InsecureRequestWarning)
API_URL = "https://sg-al-cwork-web.mediportal.com.cn/open-api/work-report/plugin/report/list"
def _resolve_app_key() -> str:
if any(arg in {"-h", "--help"} for arg in sys.argv[1:]):
return ""
app_key = os.environ.get("XG_BIZ_API_KEY") or os.environ.get("XG_APP_KEY")
if not app_key:
print("错误: 请设置环境变量 XG_BIZ_API_KEY 或 XG_APP_KEY", file=sys.stderr)
sys.exit(1)
return app_key
def _load_body(body_json: str, body_file: str):
if body_json:
return json.loads(body_json)
if body_file:
with open(body_file, "r", encoding="utf-8") as handle:
return json.load(handle)
return None
def build_payload(args):
direct_payload = _load_body(args.body_json, args.body_file)
if direct_payload is not None:
return direct_payload
return {
"pageIndex": args.page_index,
"pageSize": args.page_size,
"lastUpdateTime": args.last_update_time,
}
def call_api(app_key: str, payload: dict):
headers = {"appKey": app_key, "Content-Type": "application/json"}
try:
response = requests.post(
API_URL,
json=payload,
headers=headers,
verify=False,
allow_redirects=True,
timeout=60,
)
response.raise_for_status()
return response.json()
except Exception as exc:
raise Exception(f"请求失败: {exc}")
def main():
app_key = _resolve_app_key()
parser = argparse.ArgumentParser()
parser.add_argument("--page-index", type=int, default=1)
parser.add_argument("--page-size", type=int, default=20)
parser.add_argument("--last-update-time", type=int, default=0)
parser.add_argument("--body-json", default="")
parser.add_argument("--body-file", default="")
args = parser.parse_args()
result = call_api(app_key, build_payload(args))
print(json.dumps(result, ensure_ascii=False))
if __name__ == "__main__":
main()
FILE:scripts/plugin-report/get-unread-list.py
#!/usr/bin/env python3
"""
plugin-report / get-unread-list 脚本
用途:获取插件场景下的未读汇报列表
"""
import argparse
import json
import os
import sys
import requests
import warnings
# 禁用 InsecureRequestWarning (因为 verify=False)
warnings.filterwarnings("ignore", category=requests.packages.urllib3.exceptions.InsecureRequestWarning)
API_URL = "https://sg-al-cwork-web.mediportal.com.cn/open-api/work-report/plugin/report/unreadList"
def _resolve_app_key() -> str:
if any(arg in {"-h", "--help"} for arg in sys.argv[1:]):
return ""
app_key = os.environ.get("XG_BIZ_API_KEY") or os.environ.get("XG_APP_KEY")
if not app_key:
print("错误: 请设置环境变量 XG_BIZ_API_KEY 或 XG_APP_KEY", file=sys.stderr)
sys.exit(1)
return app_key
def _load_body(body_json: str, body_file: str):
if body_json:
return json.loads(body_json)
if body_file:
with open(body_file, "r", encoding="utf-8") as handle:
return json.load(handle)
return None
def build_payload(args):
direct_payload = _load_body(args.body_json, args.body_file)
if direct_payload is not None:
return direct_payload
return {
"pageIndex": args.page_index,
"pageSize": args.page_size,
"lastUpdateTime": args.last_update_time,
}
def call_api(app_key: str, payload: dict):
headers = {"appKey": app_key, "Content-Type": "application/json"}
try:
response = requests.post(
API_URL,
json=payload,
headers=headers,
verify=False,
allow_redirects=True,
timeout=60,
)
response.raise_for_status()
return response.json()
except Exception as exc:
raise Exception(f"请求失败: {exc}")
def main():
app_key = _resolve_app_key()
parser = argparse.ArgumentParser()
parser.add_argument("--page-index", type=int, default=1)
parser.add_argument("--page-size", type=int, default=20)
parser.add_argument("--last-update-time", type=int, default=0)
parser.add_argument("--body-json", default="")
parser.add_argument("--body-file", default="")
args = parser.parse_args()
result = call_api(app_key, build_payload(args))
print(json.dumps(result, ensure_ascii=False))
if __name__ == "__main__":
main()
FILE:scripts/report-detail/README.md
# 脚本清单 — report-detail
## 共享依赖
无
## 脚本列表
| 脚本 | 对应接口 | 用途 |
|---|---|---|
| `get-info.py` | `GET /work-report/report/info` | 调用接口,输出 JSON 格式的汇报结构详情 |
## 使用方式
```bash
# 执行前需要预先准备好 appKey,并通过环境变量传入
export XG_BIZ_API_KEY="your-app-key"
# 或 export XG_APP_KEY="your-app-key"
# 推荐:显式参数名
python3 scripts/report-detail/get-info.py --report-id 2037895527831597058
# 兼容:旧的位置参数
python3 scripts/report-detail/get-info.py 2037895527831597058
```
## 输出说明
所有脚本的输出均为 **JSON 格式**。
补充说明:
- 支持 `-h/--help`,即使当前环境还未注入 `appKey`
- 推荐优先使用 `--report-id`
## 规范
1. **必须使用 Python** 编写
2. **鉴权遵循** `cms-auth-skills/SKILL.md` 规范,不包含登录发起逻辑
3. **入参定义以** `openapi/` 文档为准
FILE:scripts/report-detail/get-info.py
#!/usr/bin/env python3
"""
report-detail / get-info 脚本
用途:获取单篇汇报的结构化详情
"""
import argparse
import sys
import os
import json
import requests
import warnings
# 禁用 InsecureRequestWarning (因为 verify=False)
warnings.filterwarnings("ignore", category=requests.packages.urllib3.exceptions.InsecureRequestWarning)
def _resolve_app_key() -> str:
if any(arg in {"-h", "--help"} for arg in sys.argv[1:]):
return ""
app_key = os.environ.get("XG_BIZ_API_KEY") or os.environ.get("XG_APP_KEY")
if not app_key:
print("错误: 请设置环境变量 XG_BIZ_API_KEY 或 XG_APP_KEY", file=sys.stderr)
sys.exit(1)
return app_key
API_URL = "https://sg-al-cwork-web.mediportal.com.cn/open-api/work-report/report/info"
def call_api(app_key, report_id):
headers = {"appKey": app_key, "Content-Type": "application/json"}
params = {"reportId": report_id}
try:
response = requests.get(
API_URL,
params=params,
headers=headers,
verify=False,
allow_redirects=True,
timeout=60,
)
response.raise_for_status()
return response.json()
except Exception as exc:
raise Exception(f"请求失败: {exc}")
def main():
parser = argparse.ArgumentParser(description="获取单篇汇报的结构化详情")
parser.add_argument("report_id", nargs="?", default="", help="reportId,兼容旧的位置参数用法")
parser.add_argument("--report-id", dest="report_id_opt", default="", help="目标汇报 ID")
args = parser.parse_args()
report_id = args.report_id_opt or args.report_id
if not report_id:
parser.error("请提供 reportId 作为参数")
app_key = _resolve_app_key()
result = call_api(app_key, report_id)
print(json.dumps(result, ensure_ascii=False))
if __name__ == "__main__": main()
FILE:scripts/report-message/README.md
# 脚本清单 — report-message
## 共享依赖
无
## 脚本列表
| 脚本 | 对应接口 | 用途 |
|---|---|---|
| `find-my-new-msg-list.py` | `GET /work-report/open-platform/report/findMyNewMsgList` | 获取我的新消息列表 |
| `read-report.py` | `GET /work-report/open-platform/report/readReport` | 标记汇报已读,并清理未读/新消息提醒 |
## 使用方式
```bash
# 执行前需要预先准备好 appKey,并通过环境变量传入
export XG_BIZ_API_KEY="your-app-key"
# 或 export XG_APP_KEY="your-app-key"
# 查询重要消息
python3 scripts/report-message/find-my-new-msg-list.py --msg-type 1
# 阅读汇报并清理提醒(有副作用)
python3 scripts/report-message/read-report.py --report-id 123456
# 新消息很多时,可限制终端输出并保存结果到文件
python3 scripts/report-message/find-my-new-msg-list.py --msg-type 1 --client-limit 5 --output-file /tmp/msg-list.json
```
## 输出说明
所有脚本的输出均为 **JSON 格式**。
补充说明:
- `find-my-new-msg-list.py` 返回 `data.total` 与 `data.msgList`
- 当消息较多时,脚本会附带 `serverReturnedSize`、`clientLimit`、`clientReturnedSize`
- `read-report.py` 虽然是 `GET`,但它是状态变更接口,不应按只读接口理解
## 规范
1. **必须使用 Python** 编写
2. **鉴权遵循** `cms-auth-skills/SKILL.md` 规范,不包含登录发起逻辑
3. **入参定义以** `openapi/` 文档为准
4. `find-my-new-msg-list.py` 默认只处理前 `200` 条,最大只处理前 `500` 条,并支持 `--client-limit/--output-file`
FILE:scripts/report-message/find-my-new-msg-list.py
#!/usr/bin/env python3
"""
report-message / find-my-new-msg-list 脚本
用途:获取当前用户的新消息列表
"""
import argparse
import json
import os
import sys
import requests
import warnings
# 禁用 InsecureRequestWarning (因为 verify=False)
warnings.filterwarnings("ignore", category=requests.packages.urllib3.exceptions.InsecureRequestWarning)
API_URL = "https://sg-al-cwork-web.mediportal.com.cn/open-api/work-report/open-platform/report/findMyNewMsgList"
DEFAULT_CLIENT_LIMIT = 200
MAX_CLIENT_LIMIT = 500
def _resolve_app_key() -> str:
if any(arg in {"-h", "--help"} for arg in sys.argv[1:]):
return ""
app_key = os.environ.get("XG_BIZ_API_KEY") or os.environ.get("XG_APP_KEY")
if not app_key:
print("错误: 请设置环境变量 XG_BIZ_API_KEY 或 XG_APP_KEY", file=sys.stderr)
sys.exit(1)
return app_key
def call_api(app_key: str, msg_type):
params = {}
if msg_type is not None:
params["msgType"] = msg_type
headers = {"appKey": app_key}
try:
response = requests.get(
API_URL,
params=params,
headers=headers,
verify=False,
allow_redirects=True,
timeout=60,
)
response.raise_for_status()
return response.json()
except Exception as exc:
raise Exception(f"请求失败: {exc}")
def _resolve_client_limit(client_limit):
if client_limit is None:
return DEFAULT_CLIENT_LIMIT
return min(client_limit, MAX_CLIENT_LIMIT)
def _apply_client_limit(result, client_limit):
effective_limit = _resolve_client_limit(client_limit)
data = result.get("data")
if not isinstance(data, dict):
return result
msg_list = data.get("msgList")
if not isinstance(msg_list, list):
return result
data["serverReturnedSize"] = len(msg_list)
data["msgList"] = msg_list[:effective_limit]
data["clientLimit"] = effective_limit
data["clientReturnedSize"] = len(data["msgList"])
data["defaultClientLimit"] = DEFAULT_CLIENT_LIMIT
data["maxClientLimit"] = MAX_CLIENT_LIMIT
if client_limit is not None:
data["requestedClientLimit"] = client_limit
return result
def _emit_result(result, output_file):
text = json.dumps(result, ensure_ascii=False)
if output_file:
with open(output_file, "w", encoding="utf-8") as handle:
handle.write(text)
print(text)
def main():
app_key = _resolve_app_key()
parser = argparse.ArgumentParser()
parser.add_argument("--msg-type", type=int)
parser.add_argument("--client-limit", type=int)
parser.add_argument("--output-file", default="")
args = parser.parse_args()
result = call_api(app_key, args.msg_type)
result = _apply_client_limit(result, args.client_limit)
_emit_result(result, args.output_file)
if __name__ == "__main__":
main()
FILE:scripts/report-message/read-report.py
#!/usr/bin/env python3
"""
report-message / read-report 脚本
用途:阅读汇报并清理未读/新消息提醒
"""
import argparse
import json
import os
import sys
import requests
import warnings
# 禁用 InsecureRequestWarning (因为 verify=False)
warnings.filterwarnings("ignore", category=requests.packages.urllib3.exceptions.InsecureRequestWarning)
API_URL = "https://sg-al-cwork-web.mediportal.com.cn/open-api/work-report/open-platform/report/readReport"
def _resolve_app_key() -> str:
if any(arg in {"-h", "--help"} for arg in sys.argv[1:]):
return ""
app_key = os.environ.get("XG_BIZ_API_KEY") or os.environ.get("XG_APP_KEY")
if not app_key:
print("错误: 请设置环境变量 XG_BIZ_API_KEY 或 XG_APP_KEY", file=sys.stderr)
sys.exit(1)
return app_key
def call_api(app_key: str, report_id: int):
params = {"reportId": report_id}
headers = {"appKey": app_key}
try:
response = requests.get(
API_URL,
params=params,
headers=headers,
verify=False,
allow_redirects=True,
timeout=60,
)
response.raise_for_status()
return response.json()
except Exception as exc:
raise Exception(f"请求失败: {exc}")
def main():
app_key = _resolve_app_key()
parser = argparse.ArgumentParser()
parser.add_argument("--report-id", type=int, required=True)
args = parser.parse_args()
result = call_api(app_key, args.report_id)
print(json.dumps(result, ensure_ascii=False))
if __name__ == "__main__":
main()
FILE:scripts/report-query/README.md
# 脚本清单 — report-query
## 共享依赖
无
## 脚本列表
| 脚本 | 对应接口 | 用途 |
|---|---|---|
| `get-todo-list.py` | `POST /work-report/reportInfoOpenQuery/todoList` | 获取汇报待办分页列表 |
| `get-unread-list.py` | `POST /work-report/reportInfoOpenQuery/unreadList` | 获取汇报未读分页列表 |
| `is-report-read.py` | `GET /work-report/reportInfoOpenQuery/isReportRead` | 判断指定员工是否已读某条汇报 |
## 使用方式
```bash
# 执行前需要预先准备好 appKey,并通过环境变量传入
export XG_BIZ_API_KEY="your-app-key"
# 或 export XG_APP_KEY="your-app-key"
# 获取汇报待办列表
python3 scripts/report-query/get-todo-list.py --page-index 1 --page-size 20
# 获取未读汇报列表
python3 scripts/report-query/get-unread-list.py --page-index 1 --page-size 20
# 判断是否已读
python3 scripts/report-query/is-report-read.py --report-id 123456 --employee-id 10001
# 复杂分页查询可直接传完整 JSON 请求体
python3 scripts/report-query/get-todo-list.py --body-json '{"pageIndex":1,"pageSize":20}'
python3 scripts/report-query/get-todo-list.py --body-file /path/to/report-query.json
# 返回体较大时,可在本地截断输出并同时落文件
python3 scripts/report-query/get-unread-list.py --page-index 1 --page-size 20 --client-limit 5 --output-file /tmp/unread.json
```
## 输出说明
所有脚本的输出均为 **JSON 格式**。
补充说明:
- 默认只处理前 `200` 条;即使显式传更大值,脚本也最多只处理前 `500` 条
- `--client-limit` 只裁剪脚本最终输出,不改变平台真实返回
- `--output-file` 保存的是脚本裁剪后的输出结果,适合避免终端输出被截断
- 平台实测中 `unreadList` 可能忽略传入的 `pageSize`,因此脚本仍会在客户端补做裁剪
## 规范
1. **必须使用 Python** 编写
2. **鉴权遵循** `cms-auth-skills/SKILL.md` 规范,不包含登录发起逻辑
3. **入参定义以** `openapi/` 文档为准
FILE:scripts/report-query/get-todo-list.py
#!/usr/bin/env python3
"""
report-query / get-todo-list 脚本
用途:获取汇报待办分页列表
"""
import argparse
import json
import os
import sys
import requests
import warnings
# 禁用 InsecureRequestWarning (因为 verify=False)
warnings.filterwarnings("ignore", category=requests.packages.urllib3.exceptions.InsecureRequestWarning)
API_URL = "https://sg-al-cwork-web.mediportal.com.cn/open-api/work-report/reportInfoOpenQuery/todoList"
DEFAULT_CLIENT_LIMIT = 200
MAX_CLIENT_LIMIT = 500
def _resolve_app_key() -> str:
if any(arg in {"-h", "--help"} for arg in sys.argv[1:]):
return ""
app_key = os.environ.get("XG_BIZ_API_KEY") or os.environ.get("XG_APP_KEY")
if not app_key:
print("错误: 请设置环境变量 XG_BIZ_API_KEY 或 XG_APP_KEY", file=sys.stderr)
sys.exit(1)
return app_key
def _load_body(body_json: str, body_file: str):
if body_json:
return json.loads(body_json)
if body_file:
with open(body_file, "r", encoding="utf-8") as handle:
return json.load(handle)
return None
def build_payload(args):
direct_payload = _load_body(args.body_json, args.body_file)
if direct_payload is not None:
return direct_payload
return {"pageIndex": args.page_index, "pageSize": args.page_size}
def call_api(app_key: str, payload: dict):
headers = {"appKey": app_key, "Content-Type": "application/json"}
try:
response = requests.post(
API_URL,
json=payload,
headers=headers,
verify=False,
allow_redirects=True,
timeout=60,
)
response.raise_for_status()
return response.json()
except Exception as exc:
raise Exception(f"请求失败: {exc}")
def _resolve_client_limit(client_limit):
if client_limit is None:
return DEFAULT_CLIENT_LIMIT
return min(client_limit, MAX_CLIENT_LIMIT)
def _apply_client_limit(result, client_limit):
effective_limit = _resolve_client_limit(client_limit)
data = result.get("data")
if not isinstance(data, dict):
return result
items = data.get("list")
if not isinstance(items, list):
return result
data["serverReturnedSize"] = len(items)
data["list"] = items[:effective_limit]
data["clientLimit"] = effective_limit
data["clientReturnedSize"] = len(data["list"])
data["defaultClientLimit"] = DEFAULT_CLIENT_LIMIT
data["maxClientLimit"] = MAX_CLIENT_LIMIT
if client_limit is not None:
data["requestedClientLimit"] = client_limit
return result
def _emit_result(result, output_file):
text = json.dumps(result, ensure_ascii=False)
if output_file:
with open(output_file, "w", encoding="utf-8") as handle:
handle.write(text)
print(text)
def main():
app_key = _resolve_app_key()
parser = argparse.ArgumentParser()
parser.add_argument("--page-index", type=int, default=1)
parser.add_argument("--page-size", type=int, default=20)
parser.add_argument("--body-json", default="")
parser.add_argument("--body-file", default="")
parser.add_argument("--client-limit", type=int)
parser.add_argument("--output-file", default="")
args = parser.parse_args()
result = call_api(app_key, build_payload(args))
result = _apply_client_limit(result, args.client_limit)
_emit_result(result, args.output_file)
if __name__ == "__main__":
main()
FILE:scripts/report-query/get-unread-list.py
#!/usr/bin/env python3
"""
report-query / get-unread-list 脚本
用途:获取汇报未读分页列表
"""
import argparse
import json
import os
import sys
import requests
import warnings
# 禁用 InsecureRequestWarning (因为 verify=False)
warnings.filterwarnings("ignore", category=requests.packages.urllib3.exceptions.InsecureRequestWarning)
API_URL = "https://sg-al-cwork-web.mediportal.com.cn/open-api/work-report/reportInfoOpenQuery/unreadList"
DEFAULT_CLIENT_LIMIT = 200
MAX_CLIENT_LIMIT = 500
def _resolve_app_key() -> str:
if any(arg in {"-h", "--help"} for arg in sys.argv[1:]):
return ""
app_key = os.environ.get("XG_BIZ_API_KEY") or os.environ.get("XG_APP_KEY")
if not app_key:
print("错误: 请设置环境变量 XG_BIZ_API_KEY 或 XG_APP_KEY", file=sys.stderr)
sys.exit(1)
return app_key
def _load_body(body_json: str, body_file: str):
if body_json:
return json.loads(body_json)
if body_file:
with open(body_file, "r", encoding="utf-8") as handle:
return json.load(handle)
return None
def build_payload(args):
direct_payload = _load_body(args.body_json, args.body_file)
if direct_payload is not None:
return direct_payload
return {"pageIndex": args.page_index, "pageSize": args.page_size}
def call_api(app_key: str, payload: dict):
headers = {"appKey": app_key, "Content-Type": "application/json"}
try:
response = requests.post(
API_URL,
json=payload,
headers=headers,
verify=False,
allow_redirects=True,
timeout=60,
)
response.raise_for_status()
return response.json()
except Exception as exc:
raise Exception(f"请求失败: {exc}")
def _resolve_client_limit(client_limit):
if client_limit is None:
return DEFAULT_CLIENT_LIMIT
return min(client_limit, MAX_CLIENT_LIMIT)
def _apply_client_limit(result, client_limit):
effective_limit = _resolve_client_limit(client_limit)
data = result.get("data")
if not isinstance(data, dict):
return result
items = data.get("list")
if not isinstance(items, list):
return result
data["serverReturnedSize"] = len(items)
data["list"] = items[:effective_limit]
data["clientLimit"] = effective_limit
data["clientReturnedSize"] = len(data["list"])
data["defaultClientLimit"] = DEFAULT_CLIENT_LIMIT
data["maxClientLimit"] = MAX_CLIENT_LIMIT
if client_limit is not None:
data["requestedClientLimit"] = client_limit
return result
def _emit_result(result, output_file):
text = json.dumps(result, ensure_ascii=False)
if output_file:
with open(output_file, "w", encoding="utf-8") as handle:
handle.write(text)
print(text)
def main():
app_key = _resolve_app_key()
parser = argparse.ArgumentParser()
parser.add_argument("--page-index", type=int, default=1)
parser.add_argument("--page-size", type=int, default=20)
parser.add_argument("--body-json", default="")
parser.add_argument("--body-file", default="")
parser.add_argument("--client-limit", type=int)
parser.add_argument("--output-file", default="")
args = parser.parse_args()
result = call_api(app_key, build_payload(args))
result = _apply_client_limit(result, args.client_limit)
_emit_result(result, args.output_file)
if __name__ == "__main__":
main()
FILE:scripts/report-query/is-report-read.py
#!/usr/bin/env python3
"""
report-query / is-report-read 脚本
用途:判断指定员工是否已读某条汇报
"""
import argparse
import json
import os
import sys
import requests
import warnings
# 禁用 InsecureRequestWarning (因为 verify=False)
warnings.filterwarnings("ignore", category=requests.packages.urllib3.exceptions.InsecureRequestWarning)
API_URL = "https://sg-al-cwork-web.mediportal.com.cn/open-api/work-report/reportInfoOpenQuery/isReportRead"
def _resolve_app_key() -> str:
if any(arg in {"-h", "--help"} for arg in sys.argv[1:]):
return ""
app_key = os.environ.get("XG_BIZ_API_KEY") or os.environ.get("XG_APP_KEY")
if not app_key:
print("错误: 请设置环境变量 XG_BIZ_API_KEY 或 XG_APP_KEY", file=sys.stderr)
sys.exit(1)
return app_key
def call_api(app_key: str, report_id: int, employee_id: int):
params = {"reportId": report_id, "employeeId": employee_id}
headers = {"appKey": app_key}
try:
response = requests.get(
API_URL,
params=params,
headers=headers,
verify=False,
allow_redirects=True,
timeout=60,
)
response.raise_for_status()
return response.json()
except Exception as exc:
raise Exception(f"请求失败: {exc}")
def main():
app_key = _resolve_app_key()
parser = argparse.ArgumentParser()
parser.add_argument("--report-id", type=int, required=True)
parser.add_argument("--employee-id", type=int, required=True)
args = parser.parse_args()
result = call_api(app_key, args.report_id, args.employee_id)
print(json.dumps(result, ensure_ascii=False))
if __name__ == "__main__":
main()
FILE:scripts/report-write/README.md
# 脚本清单 — report-write
## 共享依赖
无
## 脚本列表
| 脚本 | 对应接口 | 用途 |
|---|---|---|
| `submit.py` | `POST /work-report/report/record/submit` | 发送汇报 |
| `reply.py` | `POST /work-report/report/record/reply` | 回复汇报 |
## 使用方式
```bash
export XG_BIZ_API_KEY="your-app-key"
# 或 export XG_APP_KEY="your-app-key"
# 简单模式:发送汇报
python3 scripts/report-write/submit.py \
--main "本周工作进展" \
--content-html "<p>已完成接口联调</p>" \
--accept-emp-ids 10001,10002
# 完整模式:从文件读取请求体
python3 scripts/report-write/submit.py --body-file ./submit-body.json
# 简单模式:回复汇报
python3 scripts/report-write/reply.py \
--report-record-id 1234567890 \
--content-html "已收到,我会在今天下班前给出方案"
# 完整模式:直接传完整 JSON
python3 scripts/report-write/reply.py --body-json '{"reportRecordId":"1234567890","contentHtml":"已处理"}'
```
## 输出说明
所有脚本的输出均为 **JSON 格式**。
## 规范
1. **必须使用 Python** 编写
2. **鉴权遵循** `cms-auth-skills/SKILL.md` 规范,不包含登录发起逻辑
3. **入参定义以** `openapi/` 文档为准
FILE:scripts/report-write/reply.py
#!/usr/bin/env python3
"""
report-write / reply 脚本
用途:回复汇报
"""
import argparse
import json
import os
import sys
import requests
import warnings
# 禁用 InsecureRequestWarning (因为 verify=False)
warnings.filterwarnings("ignore", category=requests.packages.urllib3.exceptions.InsecureRequestWarning)
def _resolve_app_key() -> str:
if any(arg in {"-h", "--help"} for arg in sys.argv[1:]):
return ""
app_key = os.environ.get("XG_BIZ_API_KEY") or os.environ.get("XG_APP_KEY")
if not app_key:
print("错误: 请设置环境变量 XG_BIZ_API_KEY 或 XG_APP_KEY", file=sys.stderr)
sys.exit(1)
return app_key
API_URL = "https://sg-al-cwork-web.mediportal.com.cn/open-api/work-report/report/record/reply"
def _load_json_arg(body_json: str, body_file: str):
if body_json:
return json.loads(body_json)
if body_file:
with open(body_file, "r", encoding="utf-8") as f:
return json.load(f)
return None
def _parse_json_list(raw: str):
if not raw:
return []
return json.loads(raw)
def _parse_str_list(raw: str):
if not raw:
return []
return [item.strip() for item in raw.split(",") if item.strip()]
def _parse_bool(raw: str):
value = raw.strip().lower()
if value in {"true", "1", "yes"}:
return True
if value in {"false", "0", "no"}:
return False
raise ValueError(f"无效布尔值: {raw}")
def build_payload(args):
direct_payload = _load_json_arg(args.body_json, args.body_file)
if direct_payload is not None:
return direct_payload
if not args.report_record_id or not args.content_html:
print("错误: 简单模式下必须提供 --report-record-id 和 --content-html", file=sys.stderr)
sys.exit(1)
payload = {
"reportRecordId": args.report_record_id,
"contentHtml": args.content_html,
}
if args.media_list_json:
media_list = _parse_json_list(args.media_list_json)
payload["mediaVOList"] = media_list
payload["isMedia"] = 1
elif args.is_media is not None:
payload["isMedia"] = args.is_media
if args.send_msg:
payload["sendMsg"] = _parse_bool(args.send_msg)
if args.add_emp_ids:
payload["addEmpIdList"] = _parse_str_list(args.add_emp_ids)
return payload
def call_api(app_key: str, payload: dict):
headers = {"appKey": app_key, "Content-Type": "application/json"}
try:
response = requests.post(
API_URL,
json=payload,
headers=headers,
verify=False,
allow_redirects=True,
timeout=120,
)
response.raise_for_status()
return response.json()
except Exception as exc:
raise Exception(f"请求失败: {exc}")
def main():
app_key = _resolve_app_key()
parser = argparse.ArgumentParser()
parser.add_argument("--report-record-id", default="")
parser.add_argument("--content-html", default="")
parser.add_argument("--is-media", type=int, choices=[0, 1])
parser.add_argument("--media-list-json", default="")
parser.add_argument("--send-msg", default="")
parser.add_argument("--add-emp-ids", default="")
parser.add_argument("--body-json", default="")
parser.add_argument("--body-file", default="")
args = parser.parse_args()
payload = build_payload(args)
result = call_api(app_key, payload)
print(json.dumps(result, ensure_ascii=False))
if __name__ == "__main__":
main()
FILE:scripts/report-write/submit.py
#!/usr/bin/env python3
"""
report-write / submit 脚本
用途:发送汇报
"""
import argparse
import json
import os
import sys
import requests
import warnings
# 禁用 InsecureRequestWarning (因为 verify=False)
warnings.filterwarnings("ignore", category=requests.packages.urllib3.exceptions.InsecureRequestWarning)
def _resolve_app_key() -> str:
if any(arg in {"-h", "--help"} for arg in sys.argv[1:]):
return ""
app_key = os.environ.get("XG_BIZ_API_KEY") or os.environ.get("XG_APP_KEY")
if not app_key:
print("错误: 请设置环境变量 XG_BIZ_API_KEY 或 XG_APP_KEY", file=sys.stderr)
sys.exit(1)
return app_key
API_URL = "https://sg-al-cwork-web.mediportal.com.cn/open-api/work-report/report/record/submit"
def _parse_long_list(raw: str):
if not raw:
return []
values = []
for item in raw.split(","):
item = item.strip()
if item:
values.append(int(item))
return values
def _load_json_arg(body_json: str, body_file: str):
if body_json:
return json.loads(body_json)
if body_file:
with open(body_file, "r", encoding="utf-8") as f:
return json.load(f)
return None
def _parse_json_list(raw: str):
if not raw:
return []
return json.loads(raw)
def build_payload(args):
direct_payload = _load_json_arg(args.body_json, args.body_file)
if direct_payload is not None:
return direct_payload
if not args.main or not args.content_html:
print("错误: 简单模式下必须提供 --main 和 --content-html", file=sys.stderr)
sys.exit(1)
payload = {
"main": args.main,
"contentHtml": args.content_html,
"contentType": args.content_type,
"typeId": args.type_id,
}
if args.plan_id is not None:
payload["planId"] = args.plan_id
if args.template_id is not None:
payload["templateId"] = args.template_id
if args.grade:
payload["grade"] = args.grade
if args.privacy_level:
payload["privacyLevel"] = args.privacy_level
if args.accept_emp_ids:
payload["acceptEmpIdList"] = _parse_long_list(args.accept_emp_ids)
if args.copy_emp_ids:
payload["copyEmpIdList"] = _parse_long_list(args.copy_emp_ids)
if args.report_level_json:
payload["reportLevelList"] = _parse_json_list(args.report_level_json)
if args.file_list_json:
payload["fileVOList"] = _parse_json_list(args.file_list_json)
return payload
def call_api(app_key: str, payload: dict):
headers = {"appKey": app_key, "Content-Type": "application/json"}
try:
response = requests.post(
API_URL,
json=payload,
headers=headers,
verify=False,
allow_redirects=True,
timeout=120,
)
response.raise_for_status()
return response.json()
except Exception as exc:
raise Exception(f"请求失败: {exc}")
def main():
app_key = _resolve_app_key()
parser = argparse.ArgumentParser()
parser.add_argument("--main")
parser.add_argument("--content-html")
parser.add_argument("--content-type", default="html")
parser.add_argument("--plan-id", type=int)
parser.add_argument("--template-id", type=int)
parser.add_argument("--type-id", type=int, default=9999)
parser.add_argument("--grade", default="")
parser.add_argument("--privacy-level", default="")
parser.add_argument("--accept-emp-ids", default="")
parser.add_argument("--copy-emp-ids", default="")
parser.add_argument("--report-level-json", default="")
parser.add_argument("--file-list-json", default="")
parser.add_argument("--body-json", default="")
parser.add_argument("--body-file", default="")
args = parser.parse_args()
payload = build_payload(args)
result = call_api(app_key, payload)
print(json.dumps(result, ensure_ascii=False))
if __name__ == "__main__":
main()
FILE:scripts/tasks/README.md
# 脚本清单 — tasks
## 共享依赖
无
## 脚本列表
| 脚本 | 对应接口 | 用途 |
|---|---|---|
| `get-page.py` | `POST /work-report/report/plan/searchPage` | 调用接口,输出 JSON 格式的工作任务列表 |
| `get-simple-plan-and-report-info.py` | `GET /work-report/report/plan/getSimplePlanAndReportInfo` | 获取单个任务的简易信息及关联汇报列表 |
## 使用方式
```bash
# 执行前需要预先准备好 appKey,并通过环境变量传入
export XG_BIZ_API_KEY="your-app-key"
# 或 export XG_APP_KEY="your-app-key"
# 执行脚本
python3 scripts/tasks/get-page.py --page-index 1 --page-size 20 --keyword 接口联调 --status 1
python3 scripts/tasks/get-simple-plan-and-report-info.py --plan-id 123456
# 需要完整筛选时,直接传完整 JSON 请求体
python3 scripts/tasks/get-page.py --body-json '{"pageIndex":1,"pageSize":20,"status":1,"reportStatus":1}'
```
## 输出说明
所有脚本的输出均为 **JSON 格式**。
## 规范
1. **必须使用 Python** 编写
2. **鉴权遵循** `cms-auth-skills/SKILL.md` 规范,不包含登录发起逻辑
3. **入参定义以** `openapi/` 文档为准
FILE:scripts/tasks/get-page.py
#!/usr/bin/env python3
"""
tasks / get-page 脚本
用途:查询工作任务列表
"""
import argparse
import sys
import os
import json
import requests
import warnings
# 禁用 InsecureRequestWarning (因为 verify=False)
warnings.filterwarnings("ignore", category=requests.packages.urllib3.exceptions.InsecureRequestWarning)
def _resolve_app_key() -> str:
if any(arg in {"-h", "--help"} for arg in sys.argv[1:]):
return ""
app_key = os.environ.get("XG_BIZ_API_KEY") or os.environ.get("XG_APP_KEY")
if not app_key:
print("错误: 请设置环境变量 XG_BIZ_API_KEY 或 XG_APP_KEY", file=sys.stderr)
sys.exit(1)
return app_key
API_URL = "https://sg-al-cwork-web.mediportal.com.cn/open-api/work-report/report/plan/searchPage"
def call_api(app_key, page_index=1, page_size=10, search_key="", status="", body_json=""):
headers = {"appKey": app_key, "Content-Type": "application/json"}
if body_json:
payload = json.loads(body_json)
else:
payload = {
"pageIndex": page_index,
"pageSize": page_size
}
if search_key:
payload["keyWord"] = search_key
status_map = {"DONE": 0, "CANCELLED": 0, "RUNNING": 1, "INIT": 1, "PENDING": 1, "COMPLETED": 0, "0": 0, "1": 1}
mapped_status = status_map.get(str(status).upper())
if mapped_status is not None:
payload["status"] = mapped_status
try:
response = requests.post(
API_URL,
json=payload,
headers=headers,
verify=False,
allow_redirects=True,
timeout=60,
)
response.raise_for_status()
return response.json()
except Exception as exc:
raise Exception(f"请求失败: {exc}")
def main():
app_key = _resolve_app_key()
parser = argparse.ArgumentParser()
parser.add_argument("--page-index", type=int, default=1)
parser.add_argument("--page-size", type=int, default=10)
parser.add_argument("--keyword", default="")
parser.add_argument("--status", default="")
parser.add_argument("--body-json", default="")
args = parser.parse_args()
result = call_api(
app_key,
page_index=args.page_index,
page_size=args.page_size,
search_key=args.keyword,
status=args.status,
body_json=args.body_json,
)
print(json.dumps(result, ensure_ascii=False))
if __name__ == "__main__": main()
FILE:scripts/tasks/get-simple-plan-and-report-info.py
#!/usr/bin/env python3
"""
tasks / get-simple-plan-and-report-info 脚本
用途:获取任务简易信息及其关联汇报列表
"""
import argparse
import json
import os
import sys
import requests
import warnings
# 禁用 InsecureRequestWarning (因为 verify=False)
warnings.filterwarnings("ignore", category=requests.packages.urllib3.exceptions.InsecureRequestWarning)
API_URL = "https://sg-al-cwork-web.mediportal.com.cn/open-api/work-report/report/plan/getSimplePlanAndReportInfo"
def _resolve_app_key() -> str:
if any(arg in {"-h", "--help"} for arg in sys.argv[1:]):
return ""
app_key = os.environ.get("XG_BIZ_API_KEY") or os.environ.get("XG_APP_KEY")
if not app_key:
print("错误: 请设置环境变量 XG_BIZ_API_KEY 或 XG_APP_KEY", file=sys.stderr)
sys.exit(1)
return app_key
def call_api(app_key: str, plan_id: int):
params = {"planId": plan_id}
headers = {"appKey": app_key}
try:
response = requests.get(
API_URL,
params=params,
headers=headers,
verify=False,
allow_redirects=True,
timeout=60,
)
response.raise_for_status()
return response.json()
except Exception as exc:
raise Exception(f"请求失败: {exc}")
def main():
app_key = _resolve_app_key()
parser = argparse.ArgumentParser()
parser.add_argument("--plan-id", type=int, required=True)
args = parser.parse_args()
result = call_api(app_key, args.plan_id)
print(json.dumps(result, ensure_ascii=False))
if __name__ == "__main__":
main()
FILE:scripts/templates/README.md
# 脚本清单 — templates
## 共享依赖
无
## 脚本列表
| 脚本 | 对应接口 | 用途 |
|---|---|---|
| `get-list.py` | `POST /work-report/template/listTemplates` | 调用接口,输出 JSON 格式的事项列表 |
| `get-by-ids.py` | `POST /work-report/template/listByIds` | 按事项 ID 列表批量获取事项简易信息 |
## 使用方式
```bash
# 执行前需要预先准备好 appKey,并通过环境变量传入
export XG_BIZ_API_KEY="your-app-key"
# 或 export XG_APP_KEY="your-app-key"
# 执行脚本
python3 scripts/templates/get-list.py --limit 50
python3 scripts/templates/get-by-ids.py --template-ids 1001,1002
# 指定时间范围
python3 scripts/templates/get-list.py --begin-time 1735660800000 --end-time 1738339199000 --limit 50
# 需要精确获取事项详情时,可按 ID 列表查询
python3 scripts/templates/get-by-ids.py --body-json '[1001,1002]'
python3 scripts/templates/get-by-ids.py --body-file /path/to/template-ids.json
```
## 输出说明
所有脚本的输出均为 **JSON 格式**。
补充说明:
- `get-list.py` 默认跟随平台的最近时间窗口;如果返回空列表,可通过 `--begin-time/--end-time` 扩大范围
- 当平台返回 `data=null` 时,脚本会归一化为 `{"recentOperateTemplates":[]}`
## 规范
1. **必须使用 Python** 编写
2. **鉴权遵循** `cms-auth-skills/SKILL.md` 规范,不包含登录发起逻辑
3. **入参定义以** `openapi/` 文档为准
FILE:scripts/templates/get-by-ids.py
#!/usr/bin/env python3
"""
templates / get-by-ids 脚本
用途:按事项 ID 列表批量获取事项简易信息
"""
import argparse
import json
import os
import sys
import requests
import warnings
# 禁用 InsecureRequestWarning (因为 verify=False)
warnings.filterwarnings("ignore", category=requests.packages.urllib3.exceptions.InsecureRequestWarning)
API_URL = "https://sg-al-cwork-web.mediportal.com.cn/open-api/work-report/template/listByIds"
def _resolve_app_key() -> str:
if any(arg in {"-h", "--help"} for arg in sys.argv[1:]):
return ""
app_key = os.environ.get("XG_BIZ_API_KEY") or os.environ.get("XG_APP_KEY")
if not app_key:
print("错误: 请设置环境变量 XG_BIZ_API_KEY 或 XG_APP_KEY", file=sys.stderr)
sys.exit(1)
return app_key
def _load_body(body_json: str, body_file: str):
if body_json:
return json.loads(body_json)
if body_file:
with open(body_file, "r", encoding="utf-8") as handle:
return json.load(handle)
return None
def _parse_id_list(raw: str):
values = []
for item in raw.split(","):
item = item.strip()
if item:
values.append(int(item))
return values
def build_payload(args):
direct_payload = _load_body(args.body_json, args.body_file)
if direct_payload is not None:
return direct_payload
if not args.template_ids:
print("错误: 简单模式下必须提供 --template-ids", file=sys.stderr)
sys.exit(1)
return _parse_id_list(args.template_ids)
def call_api(app_key: str, payload):
headers = {"appKey": app_key, "Content-Type": "application/json"}
try:
response = requests.post(
API_URL,
json=payload,
headers=headers,
verify=False,
allow_redirects=True,
timeout=60,
)
response.raise_for_status()
return response.json()
except Exception as exc:
raise Exception(f"请求失败: {exc}")
def main():
app_key = _resolve_app_key()
parser = argparse.ArgumentParser()
parser.add_argument("--template-ids", default="")
parser.add_argument("--body-json", default="")
parser.add_argument("--body-file", default="")
args = parser.parse_args()
result = call_api(app_key, build_payload(args))
print(json.dumps(result, ensure_ascii=False))
if __name__ == "__main__":
main()
FILE:scripts/templates/get-list.py
#!/usr/bin/env python3
"""
templates / get-list 脚本
用途:获取最近处理过的事项列表
"""
import argparse
import json
import os
import sys
import requests
import warnings
# 禁用 InsecureRequestWarning (因为 verify=False)
warnings.filterwarnings("ignore", category=requests.packages.urllib3.exceptions.InsecureRequestWarning)
def _resolve_app_key() -> str:
if any(arg in {"-h", "--help"} for arg in sys.argv[1:]):
return ""
app_key = os.environ.get("XG_BIZ_API_KEY") or os.environ.get("XG_APP_KEY")
if not app_key:
print("错误: 请设置环境变量 XG_BIZ_API_KEY 或 XG_APP_KEY", file=sys.stderr)
sys.exit(1)
return app_key
API_URL = "https://sg-al-cwork-web.mediportal.com.cn/open-api/work-report/template/listTemplates"
def call_api(app_key, limit=50, begin_time=None, end_time=None, body_json=""):
headers = {"appKey": app_key, "Content-Type": "application/json"}
if body_json:
payload = json.loads(body_json)
else:
payload = {"limit": limit}
if begin_time is not None:
payload["beginTime"] = begin_time
if end_time is not None:
payload["endTime"] = end_time
try:
response = requests.post(
API_URL,
json=payload,
headers=headers,
verify=False,
allow_redirects=True,
timeout=60,
)
response.raise_for_status()
return response.json()
except Exception as exc:
raise Exception(f"请求失败: {exc}")
def normalize_result(result):
if result.get("resultCode") == 1 and result.get("data") is None:
result["data"] = {"recentOperateTemplates": []}
result["normalizedEmptyData"] = True
return result
def main():
app_key = _resolve_app_key()
parser = argparse.ArgumentParser()
parser.add_argument("--limit", type=int, default=50)
parser.add_argument("--begin-time", type=int)
parser.add_argument("--end-time", type=int)
parser.add_argument("--body-json", default="")
args = parser.parse_args()
result = call_api(
app_key,
limit=args.limit,
begin_time=args.begin_time,
end_time=args.end_time,
body_json=args.body_json,
)
result = normalize_result(result)
print(json.dumps(result, ensure_ascii=False))
if __name__ == "__main__": main()
FILE:scripts/todos/README.md
# 脚本清单 — todos
## 共享依赖
无
## 脚本列表
| 脚本 | 对应接口 | 用途 |
|---|---|---|
| `get-list.py` | `POST /work-report/todoTask/todoList` | 调用接口,输出 JSON 格式的待办事项列表 |
| `complete.py` | `POST /work-report/open-platform/todo/completeTodo` | 调用接口,提交建议/决策并完成待办 |
| `list-created-feedbacks.py` | `GET /work-report/todoTask/listCreatedFeedbacks` | 获取用户创建的反馈类型待办列表 |
## 使用方式
```bash
# 执行前需要预先准备好 appKey,并通过环境变量传入
export XG_BIZ_API_KEY="your-app-key"
# 或 export XG_APP_KEY="your-app-key"
# 执行脚本
python3 scripts/todos/get-list.py --page-index 1 --page-size 20 --type feedback
python3 scripts/todos/complete.py 12345 --content "同意该方案" --operate agree
python3 scripts/todos/list-created-feedbacks.py --emp-id 10001
python3 scripts/todos/list-created-feedbacks.py --client-limit 5 --output-file /tmp/feedbacks.json
# 查询待办时如需额外字段,可直接传完整 JSON 请求体
python3 scripts/todos/get-list.py --body-json '{"pageIndex":1,"pageSize":20,"type":"feedback","executionResult":"待处理"}'
```
## 输出说明
所有脚本的输出均为 **JSON 格式**。
补充说明:
- `list-created-feedbacks.py` 为平台全量查询接口,脚本默认只处理前 `200` 条,最大只处理前 `500` 条
- 返回较多时可用 `--client-limit/--output-file` 控制脚本输出
## 规范
1. **必须使用 Python** 编写
2. **鉴权遵循** `cms-auth-skills/SKILL.md` 规范,不包含登录发起逻辑
3. **入参定义以** `openapi/` 文档为准
FILE:scripts/todos/complete.py
#!/usr/bin/env python3
"""
todos / complete 脚本
用途:完成待办事项
"""
import argparse
import sys
import os
import json
import requests
import warnings
# 禁用 InsecureRequestWarning (因为 verify=False)
warnings.filterwarnings("ignore", category=requests.packages.urllib3.exceptions.InsecureRequestWarning)
def _resolve_app_key() -> str:
if any(arg in {"-h", "--help"} for arg in sys.argv[1:]):
return ""
app_key = os.environ.get("XG_BIZ_API_KEY") or os.environ.get("XG_APP_KEY")
if not app_key:
print("错误: 请设置环境变量 XG_BIZ_API_KEY 或 XG_APP_KEY", file=sys.stderr)
sys.exit(1)
return app_key
API_URL = "https://sg-al-cwork-web.mediportal.com.cn/open-api/work-report/open-platform/todo/completeTodo"
def call_api(app_key, todo_id, content, operate=None):
headers = {"appKey": app_key, "Content-Type": "application/json"}
payload = {
"todoId": int(todo_id),
"content": content
}
if operate in ["agree", "disagree"]:
payload["operate"] = operate
try:
response = requests.post(
API_URL,
json=payload,
headers=headers,
verify=False,
allow_redirects=True,
timeout=60,
)
response.raise_for_status()
return response.json()
except Exception as exc:
raise Exception(f"请求失败: {exc}")
def main():
app_key = _resolve_app_key()
parser = argparse.ArgumentParser()
parser.add_argument("todo_id")
parser.add_argument("--content", required=True)
parser.add_argument("--operate", choices=["agree", "disagree"])
args = parser.parse_args()
result = call_api(app_key, args.todo_id, args.content, args.operate)
print(json.dumps(result, ensure_ascii=False))
if __name__ == "__main__": main()
FILE:scripts/todos/get-list.py
#!/usr/bin/env python3
"""
todos / get-list 脚本
用途:获取待办事项列表
"""
import argparse
import sys
import os
import json
import requests
import warnings
# 禁用 InsecureRequestWarning (因为 verify=False)
warnings.filterwarnings("ignore", category=requests.packages.urllib3.exceptions.InsecureRequestWarning)
def _resolve_app_key() -> str:
if any(arg in {"-h", "--help"} for arg in sys.argv[1:]):
return ""
app_key = os.environ.get("XG_BIZ_API_KEY") or os.environ.get("XG_APP_KEY")
if not app_key:
print("错误: 请设置环境变量 XG_BIZ_API_KEY 或 XG_APP_KEY", file=sys.stderr)
sys.exit(1)
return app_key
API_URL = "https://sg-al-cwork-web.mediportal.com.cn/open-api/work-report/todoTask/todoList"
def call_api(app_key, page_index=1, page_size=10, type_filter="", execution_result="", body_json=""):
headers = {"appKey": app_key, "Content-Type": "application/json"}
if body_json:
payload = json.loads(body_json)
else:
payload = {
"pageIndex": page_index,
"pageSize": page_size
}
valid_types = {"plan", "sign", "lead", "feedback", "file_audit"}
if type_filter and type_filter.lower() in valid_types:
payload["type"] = type_filter.lower()
if execution_result:
payload["executionResult"] = execution_result
try:
response = requests.post(
API_URL,
json=payload,
headers=headers,
verify=False,
allow_redirects=True,
timeout=60,
)
response.raise_for_status()
return response.json()
except Exception as exc:
raise Exception(f"请求失败: {exc}")
def main():
app_key = _resolve_app_key()
parser = argparse.ArgumentParser()
parser.add_argument("--page-index", type=int, default=1)
parser.add_argument("--page-size", type=int, default=10)
parser.add_argument("--type", default="")
parser.add_argument("--execution-result", default="")
parser.add_argument("--body-json", default="")
args = parser.parse_args()
result = call_api(
app_key,
page_index=args.page_index,
page_size=args.page_size,
type_filter=args.type,
execution_result=args.execution_result,
body_json=args.body_json,
)
print(json.dumps(result, ensure_ascii=False))
if __name__ == "__main__": main()
FILE:scripts/todos/list-created-feedbacks.py
#!/usr/bin/env python3
"""
todos / list-created-feedbacks 脚本
用途:获取用户创建的反馈类型待办列表
"""
import argparse
import json
import os
import sys
import requests
import warnings
# 禁用 InsecureRequestWarning (因为 verify=False)
warnings.filterwarnings("ignore", category=requests.packages.urllib3.exceptions.InsecureRequestWarning)
API_URL = "https://sg-al-cwork-web.mediportal.com.cn/open-api/work-report/todoTask/listCreatedFeedbacks"
DEFAULT_CLIENT_LIMIT = 200
MAX_CLIENT_LIMIT = 500
def _resolve_app_key() -> str:
if any(arg in {"-h", "--help"} for arg in sys.argv[1:]):
return ""
app_key = os.environ.get("XG_BIZ_API_KEY") or os.environ.get("XG_APP_KEY")
if not app_key:
print("错误: 请设置环境变量 XG_BIZ_API_KEY 或 XG_APP_KEY", file=sys.stderr)
sys.exit(1)
return app_key
def call_api(app_key: str, emp_id):
params = {}
if emp_id is not None:
params["empId"] = emp_id
headers = {"appKey": app_key}
try:
response = requests.get(
API_URL,
params=params,
headers=headers,
verify=False,
allow_redirects=True,
timeout=60,
)
response.raise_for_status()
return response.json()
except Exception as exc:
raise Exception(f"请求失败: {exc}")
def _resolve_client_limit(client_limit):
if client_limit is None:
return DEFAULT_CLIENT_LIMIT
return min(client_limit, MAX_CLIENT_LIMIT)
def _apply_client_limit(result, client_limit):
effective_limit = _resolve_client_limit(client_limit)
data = result.get("data")
if not isinstance(data, list):
return result
result["serverReturnedSize"] = len(data)
result["data"] = data[:effective_limit]
result["clientLimit"] = effective_limit
result["clientReturnedSize"] = len(result["data"])
result["defaultClientLimit"] = DEFAULT_CLIENT_LIMIT
result["maxClientLimit"] = MAX_CLIENT_LIMIT
if client_limit is not None:
result["requestedClientLimit"] = client_limit
return result
def _emit_result(result, output_file):
text = json.dumps(result, ensure_ascii=False)
if output_file:
with open(output_file, "w", encoding="utf-8") as handle:
handle.write(text)
print(text)
def main():
app_key = _resolve_app_key()
parser = argparse.ArgumentParser()
parser.add_argument("--emp-id", type=int)
parser.add_argument("--client-limit", type=int)
parser.add_argument("--output-file", default="")
args = parser.parse_args()
result = call_api(app_key, args.emp_id)
result = _apply_client_limit(result, args.client_limit)
_emit_result(result, args.output_file)
if __name__ == "__main__":
main()
FILE:scripts/user-search/README.md
# 脚本清单 — user-search
## 共享依赖
无
## 脚本列表
| 脚本 | 对应接口 | 用途 |
|---|---|---|
| `search-emp.py` | `GET /cwork-user/searchEmpByName` | 根据姓名搜索员工 |
## 使用方式
```bash
export XG_BIZ_API_KEY="your-app-key"
# 或 export XG_APP_KEY="your-app-key"
# 推荐:显式参数名
python3 scripts/user-search/search-emp.py --keyword "张三"
# 兼容:旧的位置参数
python3 scripts/user-search/search-emp.py "张三"
```
## 输出说明
所有脚本的输出均为 **JSON 格式**。
补充说明:
- 支持 `-h/--help`,即使当前环境还未注入 `appKey`
- 推荐优先使用 `--keyword`,减少多脚本组合调用时的位置参数歧义
## 规范
1. **必须使用 Python** 编写
2. **鉴权遵循** `cms-auth-skills/SKILL.md` 规范,不包含登录发起逻辑
3. **入参定义以** `openapi/` 文档为准
FILE:scripts/user-search/search-emp.py
#!/usr/bin/env python3
"""
user-search / search-emp 脚本
用途:根据姓名模糊搜索企业内部员工
使用方式:
python3 scripts/user-search/search-emp.py "张三"
环境变量:
XG_BIZ_API_KEY / XG_APP_KEY — appKey
"""
import sys
import os
import json
import argparse
import requests
import warnings
# 禁用 InsecureRequestWarning (因为 verify=False)
warnings.filterwarnings("ignore", category=requests.packages.urllib3.exceptions.InsecureRequestWarning)
# 接口完整 URL
def _resolve_app_key() -> str:
if any(arg in {"-h", "--help"} for arg in sys.argv[1:]):
return ""
app_key = os.environ.get("XG_BIZ_API_KEY") or os.environ.get("XG_APP_KEY")
if not app_key:
print("错误: 请设置环境变量 XG_BIZ_API_KEY 或 XG_APP_KEY", file=sys.stderr)
sys.exit(1)
return app_key
API_URL = "https://sg-al-cwork-web.mediportal.com.cn/open-api/cwork-user/searchEmpByName"
def call_api(app_key: str, name: str) -> dict:
"""调用搜索接口,返回原始 JSON 响应"""
headers = {
"appKey": app_key,
"Content-Type": "application/json",
}
params = {"searchKey": name}
try:
response = requests.get(
API_URL,
params=params,
headers=headers,
verify=False,
allow_redirects=True,
timeout=60,
)
response.raise_for_status()
return response.json()
except Exception as exc:
raise Exception(f"请求失败: {exc}")
def main():
parser = argparse.ArgumentParser(description="根据姓名模糊搜索企业内部员工")
parser.add_argument("keyword", nargs="?", default="", help="搜索关键词,兼容旧的位置参数用法")
parser.add_argument("--keyword", dest="keyword_opt", default="", help="员工姓名关键词")
args = parser.parse_args()
name = args.keyword_opt or args.keyword
if not name:
parser.error("请提供搜索姓名作为参数")
app_key = _resolve_app_key()
# 1. 调用接口,获取原始 JSON
result = call_api(app_key, name)
# 2. 输出结果
print(json.dumps(result, ensure_ascii=False))
if __name__ == "__main__":
main()
FILE:version.json
{
"ver":"1.0.1",
"skillCode":"cms-cwork-skils"
}
数学运算技能,支持算术、代数方程、函数求值、微积分与矩阵计算。用于用户提出“帮我算一下”“解方程”“求导/积分”“矩阵运算”等数学任务时,优先通过内置脚本稳定计算并返回结果。
---
name: sq-math
description: 数学运算技能,支持算术、代数方程、函数求值、微积分与矩阵计算。用于用户提出“帮我算一下”“解方程”“求导/积分”“矩阵运算”等数学任务时,优先通过内置脚本稳定计算并返回结果。
---
# sq-math
执行数学计算并输出清晰结果。
## 快速用法
- 算术与表达式:运行 `python3 scripts/math_calc.py --expr "(23*17+9)/4"`
- 方程求解:运行 `python3 scripts/math_calc.py --solve "x**2-5*x+6" --var x`
- 求导:运行 `python3 scripts/math_calc.py --diff "sin(x)*x**2" --var x`
- 定积分:运行 `python3 scripts/math_calc.py --integrate "x**2" --var x --a 0 --b 3`
- 矩阵行列式:运行 `python3 scripts/math_calc.py --det "[[1,2],[3,4]]"`
## 约束
- 优先使用脚本给出可复现结果,避免口算。
- 输入无效时直接返回可执行的修正示例。
- 输出保持简洁:结果 + 关键步骤(如有)。
FILE:scripts/math_calc.py
#!/usr/bin/env python3
import argparse
import ast
import math
try:
import sympy as sp
except Exception:
sp = None
def fail(msg):
print(msg)
raise SystemExit(1)
def require_sympy():
if sp is None:
fail("sympy 未安装,无法执行该操作。请先安装: pip install sympy")
def main():
p = argparse.ArgumentParser(description="sq-math calculator")
p.add_argument("--expr")
p.add_argument("--solve")
p.add_argument("--diff")
p.add_argument("--integrate")
p.add_argument("--var", default="x")
p.add_argument("--a")
p.add_argument("--b")
p.add_argument("--det")
args = p.parse_args()
if args.expr:
if sp is not None:
v = sp.simplify(sp.sympify(args.expr))
print(v)
return
# sympy 不在时,提供基础安全表达式计算
allowed = {k: getattr(math, k) for k in dir(math) if not k.startswith("_")}
allowed.update({"abs": abs, "round": round})
print(eval(args.expr, {"__builtins__": {}}, allowed))
return
if args.solve:
require_sympy()
x = sp.symbols(args.var)
eq = sp.sympify(args.solve)
ans = sp.solve(sp.Eq(eq, 0), x)
print(ans)
return
if args.diff:
require_sympy()
x = sp.symbols(args.var)
expr = sp.sympify(args.diff)
print(sp.diff(expr, x))
return
if args.integrate:
require_sympy()
x = sp.symbols(args.var)
expr = sp.sympify(args.integrate)
if args.a is not None and args.b is not None:
print(sp.integrate(expr, (x, sp.sympify(args.a), sp.sympify(args.b))))
else:
print(sp.integrate(expr, x))
return
if args.det:
require_sympy()
m = ast.literal_eval(args.det)
print(sp.Matrix(m).det())
return
fail("请至少提供一个参数,如 --expr 或 --solve")
if __name__ == "__main__":
main()
用于"安装 Skill / 下载 Skill / 找一个能做 X 的 Skill / 把某个 Skill 装到本地 / 列出平台 Skill"。从 CMS 平台搜索已有 Skill 并按 downloadUrl 下载 ZIP 解压到本地 ~/.claude/skills。仅负责查找与安装,不负责创建或发布
---
name: cms-find-skills
description: 用于"安装 Skill / 下载 Skill / 找一个能做 X 的 Skill / 把某个 Skill 装到本地 / 列出平台 Skill"。从 CMS 平台搜索已有 Skill 并按 downloadUrl 下载 ZIP 解压到本地 ~/.claude/skills。仅负责查找与安装,不负责创建或发布
skillCode: cms-find-skills
---
**当前版本**: v1.6.0
# cms-find-skills
只做两件事:
1. 调用 `get-skills` 查看平台上已有的 Skill。
2. 根据 `downloadUrl` 下载 ZIP 并解压到本地。
不要登录,不要授权,不要扩展上传、发布、更新、下架能力。
## 当前目录
- `SKILL.md`
- `references/skill-registry/README.md`
- `scripts/skill_registry/get_skills.py`
- `scripts/skill_registry/install_skill.py`
所有说明文档统一使用 Markdown;本 Skill 不再维护旧接口文档目录。
## 路由
- 查看列表:`python3 cms-find-skills/scripts/skill_registry/get_skills.py`
- 搜索:`python3 cms-find-skills/scripts/skill_registry/get_skills.py --search "关键词"`
- 查看详情:`python3 cms-find-skills/scripts/skill_registry/get_skills.py --detail "code 或 name"`
- 取下载地址:`python3 cms-find-skills/scripts/skill_registry/get_skills.py --url "code 或 name"`
- 安装到本地:`python3 cms-find-skills/scripts/skill_registry/install_skill.py --code "code"`
- 已知下载地址时安装:`python3 cms-find-skills/scripts/skill_registry/install_skill.py --url "https://..."`
## 规则
1. 统一以 `get-skills` 返回的数据为准。
2. 安装方式固定为:下载 ZIP -> 解压到目标目录。
3. 默认安装到脚本所在 workspace 的 `skills/` 目录,可用 `--target` 覆盖。
4. 本地已存在同名 Skill 时默认跳过,加 `--force` 可删除旧目录并重新安装。
5. 只维护 Markdown 说明和 Python 脚本,不维护旧接口文档副本。
FILE:docs/skill-registry-spec.md
# cms-find-skills · Skill Registry & ZIP 校验契约
本文档描述 `cms-find-skills` 与平台 Skill 注册中心的交互契约,以及下载 Skill ZIP 包后的安全/完整性校验规则。
## 1. 接口概览
| 用途 | 接口 | 方法 |
|------|------|------|
| 列出 Skill | `POST {API_BASE}/api/skill/list` | 公开,无需鉴权 |
`API_BASE` 默认 `https://skills.mediportal.com.cn`,可通过环境变量 `CMS_API_BASE` 覆盖。
返回结构(典型):
```json
{
"resultCode": 1,
"data": [
{
"skillCode": "im-robot",
"displayName": "IM 机器人",
"version": "1.2.0",
"downloadUrl": "https://.../im-robot.zip",
"description": "...",
"metadata": { "openclaw": { "tags": [] }, "xgjk": { "isInternal": true } }
}
]
}
```
成功判定:`resultCode in (None, 1)`。
## 2. 下载与解压
`install_skill.py` 流程:
1. 通过 `get_skills.py` 找到 `downloadUrl`,或直接使用用户传入的 `--url`。
2. 下载到临时目录,校验 `zipfile.is_zipfile()`。
3. 安全解压(见下)。
4. 校验解压后的目录中存在 `SKILL.md`。
## 3. ZIP 安全校验规则(必须遵守)
- 使用 `_normalize_zip_member()`:
- 拒绝空路径、根 `.`。
- 拒绝 `..` 越界路径。
- 统一斜杠分隔符。
- 使用 `_safe_extract()`:
- 通过 `os.path.realpath` 拼接目标,确认仍在 `destination` 内(防 symlink/路径穿越)。
- 跳过软链接(不写入 symlink),仅展开文件与目录。
- 解压后必须存在 `SKILL.md`,否则视为非合法 Skill 包,安装失败。
## 4. 与 cms-create-skill 的关系
`cms-create-skill/scripts/skill-management/get_skills.py` 是本目录中 `get_skills.py` 的薄封装,**事实来源(source of truth)位于本 Skill**。修改逻辑请只改本目录下的版本。
## 5. 校验脚本
`verify_skill.py` 提供下载后的离线结构校验,可被 `install_skill.py` 或 CI 调用。
FILE:references/skill-registry/README.md
# skill-registry 模块说明
## 模块职责
`cms-find-skills` 只负责 Skill 发现和本地安装:
1. 查看平台上已经发布了哪些 Skill。
2. 根据 `downloadUrl` 下载 ZIP 并解压到当前工作区。
不负责创建、发布、更新、下架,也不负责问题上报。
## 脚本清单
| 脚本 | 说明 | 需要鉴权 |
|---|---|---|
| `get_skills.py` | 查看列表、搜索、查看详情、获取下载地址 | 否 |
| `install_skill.py` | 下载 ZIP 并解压到本地 | 否 |
## 常见触发
| 用户表达 | 动作 |
|---|---|
| "先看看平台上有什么 Skill" | 调用 `get_skills.py` 查看列表 |
| "搜一下机器人相关 Skill" | 调用 `get_skills.py --search "机器人"` |
| "看看 xxx 的详情" | 调用 `get_skills.py --detail "xxx"` |
| "把 xxx 装到本地" | 先取 `downloadUrl`,再调用 `install_skill.py` |
## 标准命令
```bash
# 查看列表
python3 cms-find-skills/scripts/skill_registry/get_skills.py
# 搜索
python3 cms-find-skills/scripts/skill_registry/get_skills.py --search "机器人"
# 查看详情
python3 cms-find-skills/scripts/skill_registry/get_skills.py --detail "im-robot"
# 下载并安装
python3 cms-find-skills/scripts/skill_registry/install_skill.py --code "im-robot"
```
## 边界
- 以平台返回结果为准,不自行维护技能元数据。
- 所有说明统一放在 Markdown 文档里,不保留旧接口文档目录。
- 如果用户要发布、更新、下架,转到 `cms-push-skill` 或 `cms-create-skill`。
FILE:scripts/skill_registry/get_skills.py
#!/usr/bin/env python3
"""
主要内容:
浏览、搜索、查看详情、获取下载地址。
通过 API_BASE 环境变量配置接口基础地址。
"""
from __future__ import annotations
import argparse
import json
import sys
import requests
import warnings
import os
# 禁用 InsecureRequestWarning (因为 verify=False)
warnings.filterwarnings("ignore", category=requests.packages.urllib3.exceptions.InsecureRequestWarning)
DEFAULT_API_BASE = "https://skills.mediportal.com.cn"
API_BASE = DEFAULT_API_BASE
API_URL = f"{API_BASE.rstrip('/')}/api/skill/list"
def parse_api_response(response: requests.Response, action: str) -> dict:
data = response.json()
if isinstance(data, dict) and data.get("resultCode") not in (None, 1):
message = data.get("resultMsg") or data.get("detailMsg") or response.text
raise RuntimeError(f"{action}失败: {message}")
return data
def call_api() -> dict:
"""调用平台公开 Skill 列表接口(ClawHub 格式)。"""
headers = {"Content-Type": "application/json"}
try:
response = requests.post(
API_URL,
headers=headers,
verify=False,
timeout=60,
allow_redirects=True,
)
response.raise_for_status()
return parse_api_response(response, "获取 Skill 列表")
except Exception as exc:
raise Exception(f"请求失败: {exc}")
def extract_skills(result: dict) -> list[dict]:
"""从响应中提取 Skill 列表。"""
if isinstance(result, list):
return result
return result.get("data") or result.get("resultData") or []
def search_skills(skills: list[dict], keyword: str) -> list[dict]:
"""按名称、描述、skillCode、标签模糊匹配。"""
kw = keyword.lower()
results = []
for skill in skills:
tags = ""
metadata = skill.get("metadata") or {}
openclaw = metadata.get("openclaw") or {}
if openclaw.get("tags"):
tags = ",".join(openclaw["tags"])
if (
kw in (skill.get("skillCode") or skill.get("name") or "").lower()
or kw in (skill.get("displayName") or skill.get("name") or "").lower()
or kw in (skill.get("description") or "").lower()
or kw in tags.lower()
):
results.append(skill)
return results
def find_one(skills: list[dict], query: str) -> dict | None:
"""按 skillCode 或 displayName 查找单个 Skill。"""
q = query.lower()
for skill in skills:
if (skill.get("skillCode") or skill.get("name") or "").lower() == q:
return skill
for skill in skills:
if (skill.get("displayName") or "").lower() == q:
return skill
for skill in skills:
code = (skill.get("skillCode") or skill.get("name") or "").lower()
display = (skill.get("displayName") or "").lower()
if q in code or q in display:
return skill
return None
def get_download_url(skills: list[dict], query: str) -> str | None:
"""按 skillCode 或 displayName 获取下载地址。"""
skill = find_one(skills, query)
if not skill:
return None
return skill.get("downloadUrl")
def format_list(skills: list[dict]) -> str:
"""以紧凑表格格式展示列表。"""
if not skills:
return "(暂无已发布的 Skill)"
lines = [
f"{'#':<4} {'显示名':<24} {'SkillCode':<24} {'版本':<10} {'是否内置':<8} {'描述'}",
"-" * 110,
]
for index, skill in enumerate(skills, 1):
display_name = (skill.get("displayName") or skill.get("name") or "")[:22]
skill_code = (skill.get("skillCode") or skill.get("name") or "")[:22]
version = (skill.get("version") or skill.get("version") or "")[:9]
metadata = skill.get("metadata") or {}
xgjk = metadata.get("xgjk") or {}
internal = "是" if xgjk.get("isInternal") else "否"
desc = (skill.get("description") or "")[:40]
lines.append(f"{index:<4} {display_name:<24} {skill_code:<24} {version:<10} {internal:<8} {desc}")
lines.append(f"\n共 {len(skills)} 个 Skill")
return "\n".join(lines)
def format_detail(skill: dict) -> str:
"""格式化单个 Skill 的详情。"""
owner = skill.get("owner") or {}
metadata = skill.get("metadata") or {}
xgjk = metadata.get("xgjk") or {}
openclaw = metadata.get("openclaw") or {}
tags = ",".join(openclaw.get("tags") or [])
lines = [
"=" * 72,
f"显示名: {skill.get('displayName') or skill.get('name', '-')}",
f"SkillCode: {skill.get('skillCode') or skill.get('name', '-')}",
f"ID: {skill.get('id', '-')}",
f"版本: {skill.get('version', '-')}",
f"标签: {tags or '-'}",
f"描述: {skill.get('description') or '-'}",
f"下载地址: {skill.get('downloadUrl') or '-'}",
f"作者: {owner.get('name', '-')}",
f"创建时间: {skill.get('createTime', '-')}",
f"下载数: {skill.get('downloadCount', '-')}",
f"点赞数: {skill.get('likeCount', '-')}",
f"收藏数: {skill.get('favoriteCount', '-')}",
f"内置 Skill: {'是' if xgjk.get('isInternal') else '否'}",
"=" * 72,
]
return "\n".join(lines)
def main() -> None:
parser = argparse.ArgumentParser(description="Skill 发现:浏览、搜索、详情、下载地址")
parser.add_argument("--search", "-s", help="按关键词搜索 Skill")
parser.add_argument("--detail", "-d", help="查看某个 Skill 详情")
parser.add_argument(
"--url",
"--download-url",
"-u",
dest="url",
help="仅输出某个 Skill 的 downloadUrl",
)
parser.add_argument("--json", action="store_true", help="输出原始 JSON")
args = parser.parse_args()
try:
result = call_api()
except Exception as exc:
print(json.dumps({"success": False, "error": str(exc)}, ensure_ascii=False), file=sys.stderr)
sys.exit(1)
skills = extract_skills(result)
if args.json:
print(json.dumps(result, ensure_ascii=False, indent=2))
return
if args.url:
url = get_download_url(skills, args.url)
if not url:
print(f"未找到 \"{args.url}\" 的下载地址", file=sys.stderr)
sys.exit(1)
print(url)
return
if args.detail:
skill = find_one(skills, args.detail)
if not skill:
print(f"未找到匹配 \"{args.detail}\" 的 Skill", file=sys.stderr)
sys.exit(1)
print(format_detail(skill))
return
if args.search:
matched = search_skills(skills, args.search)
if not matched:
print(f"搜索 \"{args.search}\" 无结果", file=sys.stderr)
sys.exit(1)
print(f"搜索 \"{args.search}\" 匹配到 {len(matched)} 个结果:\n")
print(format_list(matched))
return
print("平台 Skill 列表\n")
print(format_list(skills))
if __name__ == "__main__":
main()
FILE:scripts/skill_registry/install_skill.py
#!/usr/bin/env python3
"""
Skill 安装:通过 get-skills 接口拿到 downloadUrl,下载 ZIP 并解压到本地。
使用方式:
python3 cms-find-skills/scripts/skill_registry/install_skill.py --code "cms-auth-skills"
python3 cms-find-skills/scripts/skill_registry/install_skill.py --url "https://..."
python3 cms-find-skills/scripts/skill_registry/install_skill.py --code "cms-auth-skills" --target /path/to/dir
说明:
- 无需登录,无需授权
- 安装来源固定为 Skill ZIP 压缩包
- 默认安装到当前工作目录,可通过 --target 指定目录
"""
from __future__ import annotations
import argparse
import json
import os
import shutil
import sys
import tempfile
import requests
import warnings
import zipfile
# 禁用 InsecureRequestWarning (因为 verify=False)
warnings.filterwarnings("ignore", category=requests.packages.urllib3.exceptions.InsecureRequestWarning)
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, SCRIPT_DIR)
def log(message: str, quiet: bool = False) -> None:
if not quiet:
print(message, file=sys.stderr)
def _normalize_zip_member(name: str) -> str:
normalized = os.path.normpath(str(name or "").replace("\\", "/")).replace("\\", "/")
normalized = normalized.lstrip("/")
if not normalized or normalized == ".":
raise ValueError(f"ZIP 内存在非法路径: {name}")
if normalized.startswith("../") or "/../" in f"/{normalized}/":
raise ValueError(f"ZIP 内存在越界路径: {name}")
return normalized
def _safe_extract(zip_file: zipfile.ZipFile, destination: str) -> list[str]:
extracted = []
destination_real = os.path.realpath(destination)
for member in zip_file.infolist():
normalized = _normalize_zip_member(member.filename)
target_path = os.path.realpath(os.path.join(destination, normalized))
if not target_path.startswith(destination_real + os.sep) and target_path != destination_real:
raise ValueError(f"ZIP 内存在越界路径: {member.filename}")
if member.is_dir():
os.makedirs(target_path, exist_ok=True)
continue
os.makedirs(os.path.dirname(target_path), exist_ok=True)
with zip_file.open(member, "r") as source, open(target_path, "wb") as target:
shutil.copyfileobj(source, target)
extracted.append(normalized)
return extracted
def _resolve_skills_dir() -> str | None:
"""
从脚本自身位置向上查找名为 'skills' 的祖先目录。
不写死任何绝对路径,适用于任意 workspace(workspace / workspace-coder / workspace_xxx 等)。
"""
current = os.path.dirname(os.path.abspath(__file__))
while current != os.path.dirname(current): # 没到根目录
if os.path.basename(current) == "skills" and os.path.isdir(current):
return current
current = os.path.dirname(current)
return None
def get_download_url_from_platform(code: str) -> str | None:
"""通过 get-skills 公开接口查询 downloadUrl。"""
try:
from get_skills import call_api, extract_skills, get_download_url
result = call_api()
skills = extract_skills(result)
return get_download_url(skills, code)
except Exception as exc:
print(f"查询平台失败: {exc}", file=sys.stderr)
return None
def download_file(url: str, dest_path: str, quiet: bool = False) -> bool:
"""下载 ZIP 到本地临时文件。"""
log(f"正在下载: {url}", quiet)
try:
response = requests.get(
url,
stream=True,
verify=False,
timeout=120,
allow_redirects=True,
)
response.raise_for_status()
total = response.headers.get("Content-Length")
total = int(total) if total else None
downloaded = 0
with open(dest_path, "wb") as file_obj:
for chunk in response.iter_content(chunk_size=8192):
if chunk:
file_obj.write(chunk)
downloaded += len(chunk)
if total and not quiet:
pct = downloaded * 100 // total
print(f"\r 进度: {pct}% ({downloaded}/{total})", end="", file=sys.stderr)
if total and not quiet:
print("", file=sys.stderr)
log(f"下载完成: {dest_path}", quiet)
return True
except Exception as exc:
print(f"下载失败: {exc}", file=sys.stderr)
return False
def extract_zip(zip_path: str, target_dir: str, skill_code: str = "", quiet: bool = False) -> str | None:
"""
解压 ZIP 到目标目录,返回最终安装目录。
支持两种 ZIP 结构:
1. ZIP 自带唯一顶层目录
2. ZIP 文件散落在根目录,需要为其补一个 skill 目录
"""
log(f"正在解压到: {target_dir}", quiet)
try:
with zipfile.ZipFile(zip_path, "r") as zip_file:
names = [_normalize_zip_member(name) for name in zip_file.namelist() if str(name or "").strip()]
top_entries = {name.split("/")[0] for name in names if name.split("/")[0]}
has_single_root = False
root_name = ""
if len(top_entries) == 1:
root_name = next(iter(top_entries))
has_single_root = all(
name == root_name or name.startswith(root_name + "/")
for name in names
)
if has_single_root:
_safe_extract(zip_file, target_dir)
extracted = os.path.join(target_dir, root_name)
if not os.path.isfile(os.path.join(extracted, "SKILL.md")):
raise ValueError(f"安装结果缺少 SKILL.md: {extracted}")
log(f"解压完成: {extracted}", quiet)
return extracted
folder_name = skill_code or os.path.splitext(os.path.basename(zip_path))[0] or "skill"
dest_dir = os.path.join(target_dir, folder_name)
os.makedirs(dest_dir, exist_ok=True)
_safe_extract(zip_file, dest_dir)
if not os.path.isfile(os.path.join(dest_dir, "SKILL.md")):
raise ValueError(f"安装结果缺少 SKILL.md: {dest_dir}")
log(f"解压完成: {dest_dir}", quiet)
return dest_dir
except Exception as exc:
print(f"解压失败: {exc}", file=sys.stderr)
return None
def install_skill(
code: str | None = None,
url: str | None = None,
target_dir: str | None = None,
force: bool = False,
quiet: bool = False,
) -> dict:
"""下载 ZIP 并安装 Skill 到本地目录。"""
download_url = url or (get_download_url_from_platform(code) if code else None)
if not download_url:
return {
"success": False,
"message": f'无法获取 Skill "{code or ""}" 的下载地址,可能该 Skill 不存在或未发布',
}
target_dir = os.path.abspath(target_dir or _resolve_skills_dir() or os.getcwd())
if code:
existing = os.path.join(target_dir, code)
if os.path.isdir(existing):
if not force:
log(f"已存在: {existing}(跳过,使用 --force 覆盖)", quiet)
return {
"success": True,
"path": existing,
"message": f'Skill "{code}" 已存在于 {existing}',
"skipped": True,
}
log(f"覆盖: {existing}", quiet)
shutil.rmtree(existing)
with tempfile.TemporaryDirectory() as tmp_dir:
filename = download_url.split("/")[-1].split("?")[0] or f"{code or 'skill'}.zip"
if not filename.endswith(".zip"):
filename = f"{code or 'skill'}.zip"
tmp_file = os.path.join(tmp_dir, filename)
if not download_file(download_url, tmp_file, quiet):
return {"success": False, "message": "下载失败"}
if not zipfile.is_zipfile(tmp_file):
return {"success": False, "message": "下载结果不是 ZIP 压缩包"}
installed_path = extract_zip(tmp_file, target_dir, skill_code=code or "", quiet=quiet)
if not installed_path:
return {"success": False, "message": "解压失败"}
# 安装后做一次轻量结构校验
try:
from verify_skill import verify # type: ignore
verify_result = verify(installed_path)
if not verify_result["success"]:
return {
"success": False,
"path": installed_path,
"message": "安装结果校验失败",
"errors": verify_result["errors"],
}
except Exception as exc:
log(f"⚠️ 跳过安装后校验: {exc}", quiet)
return {
"success": True,
"path": installed_path,
"downloadUrl": download_url,
"message": f"Skill 已安装到 {installed_path}",
}
def main() -> None:
parser = argparse.ArgumentParser(description="Skill 安装:下载 ZIP 并解压到本地")
parser.add_argument("--code", "-c", help="Skill code(通过 get-skills 查询 downloadUrl)")
parser.add_argument("--url", "-u", help="直接提供 ZIP 下载地址")
parser.add_argument("--target", "-t", help="安装目标目录,默认当前工作目录")
parser.add_argument("--force", "-f", action="store_true", help="覆盖已存在的同名目录")
parser.add_argument("--quiet", "-q", action="store_true", help="静默模式")
args = parser.parse_args()
if not args.code and not args.url:
parser.error("需要 --code 或 --url 之一")
result = install_skill(
code=args.code,
url=args.url,
target_dir=args.target,
force=args.force,
quiet=args.quiet,
)
print(json.dumps(result, ensure_ascii=False, indent=2))
if not result.get("success"):
sys.exit(1)
if __name__ == "__main__":
main()
FILE:scripts/skill_registry/verify_skill.py
#!/usr/bin/env python3
"""
verify_skill.py — Skill 目录结构校验
校验项:
1. 目录存在
2. SKILL.md 存在且非空
3. SKILL.md 含 YAML frontmatter,包含 name 和 description 字段
4. 若有 scripts/,python 文件可 py_compile
使用方式:
python3 verify_skill.py /path/to/skill-dir
python3 verify_skill.py /path/to/skill-dir --json
"""
from __future__ import annotations
import argparse
import json
import os
import py_compile
import re
import sys
from typing import List
REQUIRED_FIELDS = ("name", "description")
def _read_frontmatter(skill_md_path: str) -> dict:
with open(skill_md_path, "r", encoding="utf-8") as f:
text = f.read()
if not text.startswith("---"):
raise ValueError("SKILL.md 缺少 YAML frontmatter")
end = text.find("\n---", 3)
if end < 0:
raise ValueError("SKILL.md frontmatter 未闭合")
front = text[3:end].strip()
data: dict = {}
for line in front.splitlines():
m = re.match(r"^([A-Za-z0-9_-]+)\s*:\s*(.*)$", line)
if m:
data[m.group(1)] = m.group(2).strip()
return data
def verify(skill_dir: str) -> dict:
errors: List[str] = []
warnings: List[str] = []
if not os.path.isdir(skill_dir):
return {"success": False, "errors": [f"目录不存在: {skill_dir}"], "warnings": []}
skill_md = os.path.join(skill_dir, "SKILL.md")
if not os.path.isfile(skill_md) or os.path.getsize(skill_md) == 0:
errors.append("缺少或空白的 SKILL.md")
else:
try:
front = _read_frontmatter(skill_md)
for field in REQUIRED_FIELDS:
if not front.get(field):
errors.append(f"SKILL.md frontmatter 缺少字段: {field}")
except Exception as exc:
errors.append(f"SKILL.md 解析失败: {exc}")
scripts_dir = os.path.join(skill_dir, "scripts")
if os.path.isdir(scripts_dir):
for root, _dirs, files in os.walk(scripts_dir):
for name in files:
if name.endswith(".py"):
path = os.path.join(root, name)
try:
py_compile.compile(path, doraise=True)
except py_compile.PyCompileError as exc:
errors.append(f"Python 脚本编译失败: {path}: {exc.msg}")
return {
"success": not errors,
"skill_dir": os.path.abspath(skill_dir),
"errors": errors,
"warnings": warnings,
}
def main() -> None:
parser = argparse.ArgumentParser(description="校验 Skill 目录结构")
parser.add_argument("skill_dir", help="待校验的 Skill 目录")
parser.add_argument("--json", action="store_true", help="输出 JSON")
args = parser.parse_args()
result = verify(args.skill_dir)
if args.json:
print(json.dumps(result, ensure_ascii=False, indent=2))
else:
if result["success"]:
print(f"✅ Skill 目录校验通过: {result['skill_dir']}")
else:
print(f"❌ Skill 目录校验失败: {result['skill_dir']}")
for err in result["errors"]:
print(f" - {err}")
sys.exit(0 if result["success"] else 1)
if __name__ == "__main__":
main()
用于"创建新 Skill / 新建 Skill / 写一个 Skill / 按 XGJK 协议生成 Skill 包 / 浏览查看平台已有 Skill / 搜索 Skill"。多轮引导从零生成符合协议的 Skill 目录与 SKILL.md。仅负责发现与创建——发布上架请用 cms-push-skill,问题反馈请...
---
name: cms-create-skill
description: 用于"创建新 Skill / 新建 Skill / 写一个 Skill / 按 XGJK 协议生成 Skill 包 / 浏览查看平台已有 Skill / 搜索 Skill"。多轮引导从零生成符合协议的 Skill 目录与 SKILL.md。仅负责发现与创建——发布上架请用 cms-push-skill,问题反馈请用 cms-report-issue
skillcode: cms-create-skill
dependencies:
- cms-auth-skills
---
# CMS Skill 创建工具
**当前版本**: v1.21.3
> **身份声明**:`cms-create-skill` 现在只负责两件事:发现已有 Skill、按协议创建新 Skill。发布、更新、下架统一转交 `cms-push-skill`;问题反馈统一转交 `cms-report-issue`。
## 两段能力
| # | 核心能力 | 说明 | 需要登录 |
|---|---|---|---|
| 1 | 发现 Skill | 浏览平台已有 Skill、查看详情、搜索筛选 | 否 |
| 2 | 创建 Skill | 按 XGJK 协议模板,多轮对话引导从零构建完整 Skill 包 | 否 |
## 命名区分
- 发现 Skill:先看平台已有的 Skill,避免重复创建。
- 创建 Skill:按协议从零生成内部 Skill 包。
## 协作边界
- 如果用户要发布到我们内部平台、更新、下架、同步 Skill,不在本 Skill 内执行,统一转交 `cms-push-skill`。
- 如果用户要提交问题、查看问题列表、关闭问题,不在本 Skill 内执行,统一转交 `cms-report-issue`。
- 本 Skill 只负责把 Skill 创建好,并明确下一步应该交给哪个依赖 Skill。
## 内部 / 外部定义
### 内部 Skill
- 定义:按照 `docs/` 下编号协议文档生成的 Skill 包。
- 典型动作:创建、结构校对、交付给发布工具。
- 典型表达:
- "帮我按协议创建一个 Skill"
- "这个 Skill 按内部规范生成"
### 外部 Skill
- 定义:不是按我方协议原生生成、但已存在于 ClawHub 的 Skill。
- 典型动作:不由本 Skill 创建;如果要同步到平台,转交 `cms-push-skill`。
- 典型表达:
- "我在 ClawHub 找到一个 Skill,帮我同步到我们平台"
- "把外部 skill 推到我们的平台"
## 能力宪章
### 核心原则
1. 禁止问用户 token、登录、鉴权细节;鉴权统一依赖 `cms-auth-skills`。
2. 只有声明需要鉴权的动作才获取 `access-token`。
3. 5 步创建流程必须按顺序执行,不跳步。
4. 每一步都要和用户确认,不自作主张。
5. 生成的 Skill 必须严格遵循 `docs/` 下编号协议文档。
6. 目标 Skill 的 `SKILL.md` YAML 头必须声明 `dependencies: - cms-auth-skills`。
7. 目标 Skill 不再生成本地 auth/common,统一依赖 `cms-auth-skills`。
8. 当用户进入发布链路时,只声明下一步转交 `cms-push-skill`,不在本 Skill 中继续维护重复发布实现。
9. 当用户进入问题反馈链路时,只声明下一步转交 `cms-report-issue`,不在本 Skill 中继续维护重复问题实现。
10. 本 Skill 自身只维护 Markdown 说明和创建相关 Python 脚本,不再维护旧接口文档目录。
### 鉴权规则
- 需要鉴权的动作:无
- 不需要鉴权的动作:`get_skills.py`、5 步创建流程、`fetch_api_doc.py`
- 鉴权准备方式:统一通过 `cms-auth-skills` 获取 `access-token`
### 授权依赖
- 创建阶段默认不做写操作,但生成的目标 Skill 仍需依赖 `cms-auth-skills`。
- 当后续进入发布或问题关闭阶段,由对应依赖 Skill 自己处理鉴权动作。
## 能力概览
### 发现 Skill
| 能力 | 模块 | 说明 | 需要登录 |
|---|---|---|---|
| 打开技能管理平台 | 浏览器 | 打开 https://skills.mediportal.com.cn | 否 |
| 浏览 Skill 列表 | `skill-management` | 查看平台所有已发布 Skill | 否 |
| 搜索 Skill | `skill-management` | 按关键词搜索 Skill | 否 |
| 查看 Skill 详情 | `skill-management` | 查看某个 Skill 的完整信息 | 否 |
### 创建 Skill
| 能力 | 模块 | 说明 | 需要登录 |
|---|---|---|---|
| 按协议构建 Skill 包 | 5 步流程 | 从零生成完整 Skill 包 | 否 |
| 获取接口文档 | 工具脚本 | 拉取并解析 Swagger / Markdown 接口定义 | 否 |
### 协作依赖
| 能力 | 模块 | 说明 | 需要登录 |
|---|---|---|---|
| 发布 / 更新 / 下架 | `cms-push-skill` | Skill 创建完成后,统一交给发布 Skill | 由依赖 Skill 决定 |
| 提交 / 查看 / 关闭问题 | `cms-report-issue` | Skill 运行中问题统一交给问题 Skill | 由依赖 Skill 决定 |
## 意图路由表
### 发现 Skill
| 用户说 | 路由到 | 打开文档 | 执行脚本 | 需要登录 |
|---|---|---|---|---|
| "打开技能管理"/"打开玄关 Skill"/"Skill 管理页面" | 浏览器打开 | — | `open https://skills.mediportal.com.cn` 或返回链接 | 否 |
| "有哪些 Skill"/"查看 Skill 列表"/"看看都有什么" | `skill-management` | `references/skill-management/README.md` | `scripts/skill-management/get_skills.py` | 否 |
| "搜索 xxx Skill"/"找一下 xxx 相关的" | `skill-management` | `references/skill-management/README.md` | `scripts/skill-management/get_skills.py --search xxx` | 否 |
| "xxx 这个 Skill 怎么样"/"看看 xxx 的详情" | `skill-management` | `references/skill-management/README.md` | `scripts/skill-management/get_skills.py --detail xxx` | 否 |
### 创建 Skill
| 用户说 | 路由到 | 打开文档 | 执行脚本 | 需要登录 |
|---|---|---|---|---|
| "构建 Skill 包"/"按模板创建 Skill" | 5 步流程 | `docs/007_XGJK_SKILL_CREATION_WORKFLOW.md` | `scripts/fetch_api_doc.py` | 否 |
| "按我们的协议新建一个 Skill"/"从零生成内部 Skill" | 5 步流程 | `docs/007_XGJK_SKILL_CREATION_WORKFLOW.md` | `scripts/fetch_api_doc.py` | 否 |
| "获取接口文档"/"拉取 API 定义" | 工具脚本 | — | `scripts/fetch_api_doc.py` | 否 |
### 发布 / 问题协作
| 用户说 | 路由到 | 打开文档 | 执行脚本 | 需要登录 |
|---|---|---|---|---|
| "打包并发布"/"帮我发布这个 Skill"/"发布到我们内部平台" | `cms-push-skill` | `references/skill-management/README.md` | `python3 cms-push-skill/scripts/skill-management/publish_skill.py ...` | 由依赖 Skill 决定 |
| "把 ClawHub 上这个 Skill 同步到我们平台" | `cms-push-skill` | `references/skill-management/README.md` | `python3 cms-push-skill/scripts/skill-management/publish_skill.py --external ...` | 由依赖 Skill 决定 |
| "帮我提交问题"/"上报这个问题" | `cms-report-issue` | `references/issue-report/README.md` | `python3 cms-report-issue/scripts/issue_report/report_issue.py ...` | 由依赖 Skill 决定 |
| "看看这个 Skill 有哪些问题"/"问题列表" | `cms-report-issue` | `references/issue-report/README.md` | `python3 cms-report-issue/scripts/issue_report/list_issues.py --skill-code xxx` | 由依赖 Skill 决定 |
## 工作流 A:发现 Skill
发现是起点,先看看平台上有什么,再决定是否创建新的。
```bash
# 浏览全部 Skill
python3 cms-create-skill/scripts/skill-management/get_skills.py
# 按关键词搜索
python3 cms-create-skill/scripts/skill-management/get_skills.py --search "机器人"
# 查看详情
python3 cms-create-skill/scripts/skill-management/get_skills.py --detail "im-robot"
```
## 工作流 B:创建 Skill(5 步流程)
> 完整操作手册:`docs/007_XGJK_SKILL_CREATION_WORKFLOW.md`
> 创建规范:`docs/` 下编号协议文档(核心总则见 `001`,详细规则见 `002`-`008`)
> 验证清单:`docs/008_XGJK_SKILL_VALIDATION_CHECKLIST.md`
### 协议文档编号说明
`cms-create-skill/docs/` 下的协议文档统一使用三位编号,编号既是排序规则,也是推荐阅读顺序:
1. `001_XGJK_SKILL_PROTOCOL.md`
2. `002_XGJK_SKILL_NAMING_AND_DESCRIPTION_SPEC.md`
3. `003_XGJK_SKILL_STRUCTURE_SPEC.md`
4. `004_XGJK_SKILL_INDEX_WRITING_SPEC.md`
5. `005_XGJK_SKILL_PYTHON_SCRIPT_SPEC.md`
6. `006_XGJK_SKILL_AUTH_AND_SECURITY_SPEC.md`
7. `007_XGJK_SKILL_CREATION_WORKFLOW.md`
8. `008_XGJK_SKILL_VALIDATION_CHECKLIST.md`
命名规则统一为:`NNN_XGJK_SKILL_<TOPIC>.md`
```text
Step 1 意图理解与需求确认
Step 2 按协议逐步生成
Step 3 三轮反思检查
Step 4 最终确认
Step 5 完成输出总结
```
## 工作流 C:发布协作
这里说的“发布到我们内部平台”,就是把 Skill 注册到当前技能管理平台。
当创建完成后,如果用户明确要发布或同步,不在本 Skill 内继续执行,直接转交 `cms-push-skill`:
```bash
# 先安装发布 Skill
npx clawhub@latest install cms-push-skill --force
```
然后按场景进入对应命令:
```bash
# 内部 Skill:首次发布
python3 cms-push-skill/scripts/skill-management/publish_skill.py \
./im-robot --code im-robot --name "IM 机器人" --internal
# 内部 Skill:更新
python3 cms-push-skill/scripts/skill-management/publish_skill.py \
./im-robot --code im-robot --update --version 1.0.0 --internal
# 外部 Skill:同步到平台
python3 cms-push-skill/scripts/skill-management/publish_skill.py \
./im-robot --code im-robot --name "IM 机器人" --external
```
如果你当前就在 `cms-create-skill` 里,不要继续找本 Skill 自己的发布脚本,下一步直接切到 `cms-push-skill`。
## 工作流 D:问题反馈协作
如果用户要提交问题或处理问题,统一转交 `cms-report-issue`:
```bash
# 先安装问题反馈 Skill
npx clawhub@latest install cms-report-issue --force
```
然后按场景进入对应命令:
```bash
# 提交问题
python3 cms-report-issue/scripts/issue_report/report_issue.py \
--skill-code im-robot --version 1.0.0 --error "接口超时"
# 查看问题
python3 cms-report-issue/scripts/issue_report/list_issues.py --skill-code im-robot
# 标记为已解决
python3 cms-report-issue/scripts/issue_report/update_issue.py \
--issue-id abc123 --status resolved --resolution "已修复重试逻辑"
# 关闭问题
python3 cms-report-issue/scripts/issue_report/update_issue.py \
--issue-id abc123 --status closed
```
如果你当前就在 `cms-create-skill` 里,不要继续找本 Skill 自己的问题脚本,下一步直接切到 `cms-report-issue`。
## 约束
1. 5 步创建流程必须完整执行。
2. 每一步都要多轮确认。
3. 生成结果必须严格遵循协议文档。
4. 所有说明统一写在 Markdown 文档里。
5. 发布和问题反馈只做依赖声明,不在本 Skill 内重复维护实现。
## 能力树
```text
cms-create-skill/
├── SKILL.md
├── docs/
│ ├── 001_XGJK_SKILL_PROTOCOL.md
│ ├── 002_XGJK_SKILL_NAMING_AND_DESCRIPTION_SPEC.md
│ ├── 003_XGJK_SKILL_STRUCTURE_SPEC.md
│ ├── 004_XGJK_SKILL_INDEX_WRITING_SPEC.md
│ ├── 005_XGJK_SKILL_PYTHON_SCRIPT_SPEC.md
│ ├── 006_XGJK_SKILL_AUTH_AND_SECURITY_SPEC.md
│ ├── 007_XGJK_SKILL_CREATION_WORKFLOW.md
│ └── 008_XGJK_SKILL_VALIDATION_CHECKLIST.md
├── references/
│ ├── issue-report/
│ │ └── README.md
│ └── skill-management/
│ └── README.md
└── scripts/
├── fetch_api_doc.py
└── skill-management/
├── README.md
└── get_skills.py
```
## 备注
- 平台“规范协议”页中,“创建 Skills 的规范”分组以 `README.md` 作为索引;`cms-create-skill/docs/` 保留编号协议文档本体。
- 发布能力统一收敛到 `cms-push-skill`,问题反馈能力统一收敛到 `cms-report-issue`。
- 本 Skill 自身的说明目录已经统一为 `docs/` + `references/` + `scripts/`。
FILE:docs/001_XGJK_SKILL_PROTOCOL.md
# XGJK Skill 协议总览 v2.0
本文档只保留总则、最小结构和交付边界;详细要求已拆分为同目录下的专题规范,避免单个协议文件过大、过杂。
## 0. 文档编号与命名规则
本套协议统一使用以下命名规则:
```text
NNN_XGJK_SKILL_<TOPIC>.md
```
说明:
- `NNN` 为三位数字编号,从 `001` 开始
- 编号同时代表推荐阅读顺序
- `XGJK_SKILL_` 为统一前缀
- `<TOPIC>` 为具体主题,使用全大写英文和下划线
## 1. 适用范围
- 面向按我方规范从零创建的新 Skill
- 面向以 `SKILL.md`、`references/`、`scripts/` 为核心的简化结构
- 面向需要统一鉴权、安全约束和交付校验的内部 Skill
## 2. 最小目录结构
一个合规的 Skill 包至少包含以下结构:
```text
<skill-name>/
├── SKILL.md
├── references/
│ └── <module>/
│ └── README.md
└── scripts/
└── <module>/
├── README.md
└── <action>.py
```
说明:
- `SKILL.md` 是总索引,负责能力边界、路由规则和能力树
- `references/<module>/README.md` 是模块说明,负责触发场景、输入输出和调用约束
- `scripts/<module>/<action>.py` 是唯一执行入口,负责真正完成业务动作
- `scripts/<module>/README.md` 是脚本索引,负责列出脚本、用途、入参与依赖
## 3. 核心强制规则
1. Skill 名称、目录名、`skillcode` 必须稳定且一致,统一使用小写加连字符格式。
2. 每个可执行动作都必须有对应的 Python 脚本,不允许“先留空后补”。
3. `SKILL.md` 只做索引和规则声明,不承载实现细节,不堆完整请求参数。
4. 模块说明写在 `references/`,脚本执行写在 `scripts/`,两者都必须和 `SKILL.md` 的路由表保持一致。
5. 所有鉴权统一依赖 `cms-auth-skills`,目标 Skill 不自行实现登录流程。
6. 运行时日志、缓存、状态不写回 Skill 包内部,统一放到工作区根目录的 `.cms-log/`。
7. 对用户有副作用的新增、修改、删除、发布、同步等动作,执行前必须再次确认。
## 4. 拆分后的专题文档
当前协议拆分为以下专题文档:
1. `002_XGJK_SKILL_NAMING_AND_DESCRIPTION_SPEC.md`
说明命名规则、描述写法、YAML 基本字段。
2. `003_XGJK_SKILL_STRUCTURE_SPEC.md`
说明目录结构、模块划分、文件职责和禁止项。
3. `004_XGJK_SKILL_INDEX_WRITING_SPEC.md`
说明 `SKILL.md` 的标准写法、路由表格式和能力树要求。
4. `005_XGJK_SKILL_PYTHON_SCRIPT_SPEC.md`
说明 Python 脚本的文件结构、参数约束、重试、输出和日志规则。
5. `006_XGJK_SKILL_AUTH_AND_SECURITY_SPEC.md`
说明鉴权依赖、安全边界、敏感信息和危险操作处理方式。
6. `007_XGJK_SKILL_CREATION_WORKFLOW.md`
给出从零创建 Skill 的 5 步流程。
7. `008_XGJK_SKILL_VALIDATION_CHECKLIST.md`
用于交付前逐项核对。
## 5. 交付顺序
推荐按以下顺序完成交付:
1. 先阅读本总览,明确最小结构和强制规则。
2. 按专题文档分别完成命名、目录、`SKILL.md`、脚本和安全设计。
3. 按 `007_XGJK_SKILL_CREATION_WORKFLOW.md` 逐步生成目标 Skill。
4. 按 `008_XGJK_SKILL_VALIDATION_CHECKLIST.md` 逐项检查,不能只口头说“已通过”。
5. 完成手工 smoke test 后,再进入发布、同步或交付阶段。
## 6. 最终验收标准
满足以下条件,才算协议层面完成:
- 目录结构完整,且没有占位符残留
- `SKILL.md`、模块说明、脚本索引、Python 脚本四者一致
- 鉴权依赖、输出格式、危险操作确认规则清晰
- 脚本可独立执行,且能在命令行复现
- 校验清单逐项勾选完成,并保留检查结论
FILE:docs/002_XGJK_SKILL_NAMING_AND_DESCRIPTION_SPEC.md
# 命名和描述规范
本规范用于统一 Skill 的命名、字段声明和一句话描述方式,避免同类 Skill 在目录、展示名和发布标识上各写各的。
## 1. Skill 命名规则
Skill 的目录名、`name`、`skillcode` 应尽量保持一致,统一遵循以下规则:
1. 只允许使用小写字母、数字和连字符 `-`
2. 推荐使用 `cms-业务域-动作` 风格
3. 不使用空格、下划线、中文和临时缩写
4. 不使用 `demo`、`test`、`new`、`final` 这类不稳定词
推荐示例:
- `cms-im-robot`
- `cms-order-sync`
- `cms-report-export`
不推荐示例:
- `IMRobot`
- `cms_order_sync`
- `new-skill-demo`
## 2. 模块与脚本命名规则
- 模块名使用小写英文名词或名词短语,如 `robot`、`message`、`report`
- 脚本文件名使用清晰动作名,如 `list_robots.py`、`send_message.py`
- 一个脚本只负责一个清晰动作,不把多个无关动作混在一个文件里
- `SKILL.md`、模块说明、脚本索引中的脚本路径必须完全一致
## 3. 描述写法
`description` 只写业务价值,不写实现细节。推荐一句话说清楚:
`为谁做什么,产出什么结果`
推荐写法:
- `管理企业 IM 机器人并发送通知消息`
- `同步订单状态并输出异常订单摘要`
- `汇总日报数据并生成可直接发送的简报`
不推荐写法:
- `一个很强大的 Skill`
- `调用接口并处理返回值`
- `用于测试脚本功能`
## 4. YAML 头必填字段
`SKILL.md` 的 YAML 头至少包含以下字段:
```yaml
---
name: cms-im-robot
description: 管理企业 IM 机器人并发送通知消息
skillcode: cms-im-robot
github: https://github.com/xgjk/xg-skills/tree/main/cms-im-robot
dependencies:
- cms-auth-skills
---
```
约束说明:
- `name` 与目录名保持一致
- `skillcode` 与发布标识保持一致
- `github` 指向目标 Skill 的仓库路径
- `dependencies` 里保留 `cms-auth-skills`
## 5. 占位符处理
以下占位符只允许出现在协议模板阶段,不允许出现在最终 Skill 包里:
- `<skill-name>`
- `<skill-code>`
- `<module>`
- `<action>`
- `<一句话描述>`
交付前必须全文搜索并清零。
FILE:docs/003_XGJK_SKILL_STRUCTURE_SPEC.md
# 目录结构规范
本规范只定义 Skill 包“长什么样”,不讨论具体实现代码。
## 1. 最小目录骨架
```text
<skill-name>/
├── SKILL.md
├── references/
│ └── <module>/
│ └── README.md
└── scripts/
└── <module>/
├── README.md
└── <action>.py
```
## 2. 每一层的职责
| 路径 | 职责 | 必须包含什么 |
|---|---|---|
| `SKILL.md` | 总索引 | 能力概览、工作流、路由表、能力树、约束 |
| `references/<module>/README.md` | 模块说明 | 适用场景、输入要求、输出说明、动作列表 |
| `scripts/<module>/README.md` | 脚本索引 | 脚本清单、运行方式、鉴权前置条件、返回说明 |
| `scripts/<module>/<action>.py` | 唯一执行入口 | 可直接运行的 Python 脚本 |
## 3. 模块划分原则
- 一个模块只覆盖一个明确业务域
- 同一模块下的动作应该围绕同一类对象或同一条业务链路
- 如果两个动作的输入、鉴权、输出和使用场景明显不同,应拆成不同脚本
- 如果一个 Skill 只有一个业务域,也仍然保留模块层,避免后续扩展时打散
## 4. 路由与脚本映射原则
- `SKILL.md` 路由表中的每一条能力,都必须指向一个真实存在的脚本
- 模块说明中的动作列表,应覆盖该模块下全部脚本
- 脚本索引中的脚本路径,应与实际目录完全一致
- 不允许存在“文档里提到但脚本不存在”或“脚本存在但没有入口说明”的孤立文件
## 5. 目录中的禁止项
以下内容不应出现在目标 Skill 包中:
- 本地登录脚本
- 本地鉴权说明文件
- 运行过程中生成的日志、缓存、状态文件
- 与当前 Skill 无关的临时目录、测试残留和历史备份文件
- 未替换的模板占位目录
## 6. 运行时文件位置
如脚本需要记录日志、缓存或版本检查结果,统一写入工作区根目录:
```text
<workspace>/
└── .cms-log/
├── log/<skillcode>/
└── state/<skillcode>/
```
运行时文件属于执行实现细节,不写回 Skill 包目录本身。
FILE:docs/004_XGJK_SKILL_INDEX_WRITING_SPEC.md
# SKILL.md 编写规范
`SKILL.md` 是 Skill 的入口文件,负责告诉 AI 和开发者“这个 Skill 能做什么、应该怎么用、去哪里执行”,而不是把所有实现细节堆在一个文件里。
## 1. YAML 头要求
最小 YAML 头如下:
```yaml
---
name: <skill-name>
description: <一句话描述>
skillcode: <skill-code>
github: https://github.com/xgjk/xg-skills/tree/main/<skill-code>
dependencies:
- cms-auth-skills
---
```
## 2. 必备章节
`SKILL.md` 至少包含以下内容:
1. 当前版本
2. 接口版本
3. 能力概览
4. 统一规范
5. 授权依赖
6. 输入完整性规则
7. 建议工作流
8. 脚本使用规则
9. 路由与加载规则
10. 宪章
11. 模块路由表
12. 能力树
## 3. 内容边界
`SKILL.md` 负责写:
- Skill 的能力范围
- 使用顺序和加载顺序
- 用户意图与模块/脚本的映射关系
- 对外输出边界、危险操作规则、鉴权依赖入口
`SKILL.md` 不负责写:
- 大段请求字段定义
- 每个脚本的完整实现细节
- 运行时日志路径的硬编码说明
- 与当前 Skill 无关的通用平台文档
## 4. 模块路由表格式
推荐使用以下列:
| 用户意图 | 模块 | 能力摘要 | 模块说明 | 脚本 |
|---|---|---|---|---|
| 查看机器人列表 | `robot` | 获取机器人清单与状态 | `./references/robot/README.md` | `./scripts/robot/list_robots.py` |
| 发送机器人消息 | `message` | 发送通知消息 | `./references/message/README.md` | `./scripts/message/send_message.py` |
要求:
- `用户意图` 写用户真实会说的话
- `模块` 与目录名一致
- `模块说明` 指向真实存在的 `README.md`
- `脚本` 指向真实存在的 `.py`
## 5. 能力树格式
推荐在文末保留实际目录树:
```text
<skill-name>/
├── SKILL.md
├── references/
│ ├── robot/README.md
│ └── message/README.md
└── scripts/
├── robot/
│ ├── README.md
│ └── list_robots.py
└── message/
├── README.md
└── send_message.py
```
## 6. 最小正文示例
```markdown
# <Skill 名称> — 索引
本文件提供能力边界、路由规则与使用约束。详细说明见 `references/`,实际执行见 `scripts/`。
**当前版本**: 0.1.0
**接口版本**: v1
**能力概览(2 块能力)**:
- `robot`:查看机器人列表、状态和基础信息
- `message`:发送通知消息并返回发送结果
统一规范:
- 鉴权依赖:`cms-auth-skills/SKILL.md`
- 运行日志:`.cms-log/`
授权依赖:
- 需要鉴权时先读取 `cms-auth-skills/SKILL.md`
- 缺失时先安装依赖,再继续执行
建议工作流(简版):
1. 先读取 `SKILL.md`,确认能力边界和限制
2. 根据用户意图定位模块
3. 读取对应模块说明
4. 补齐必要输入
5. 执行对应脚本
模块路由与能力索引:
| 用户意图 | 模块 | 能力摘要 | 模块说明 | 脚本 |
|---|---|---|---|---|
| 查看机器人列表 | `robot` | 获取机器人清单与状态 | `./references/robot/README.md` | `./scripts/robot/list_robots.py` |
```
## 7. 一致性要求
- `SKILL.md` 的模块清单必须覆盖实际模块目录
- 路由表必须覆盖实际可执行脚本
- 能力树必须能反映实际文件结构
- 不允许残留任何模板占位符
FILE:docs/005_XGJK_SKILL_PYTHON_SCRIPT_SPEC.md
# Python 脚本规范
本规范用于统一 `scripts/` 下 Python 脚本的写法,确保脚本既能被 AI 调用,也能在命令行独立执行。
## 1. 文件与职责
- 所有业务脚本必须使用 `.py` 后缀
- 一个脚本只负责一个明确动作
- 每个脚本都必须能直接在命令行运行
- 每个脚本都必须在模块说明和脚本索引里有明确入口
## 2. 推荐文件结构
推荐结构如下:
```python
"""
简要说明脚本作用、依赖的鉴权前置条件和最小调用示例。
"""
import argparse
import json
import time
import requests
API_URL = 'https://example.com/api'
AUTH_MODE = 'access-token'
TIMEOUT = 60
MAX_RETRIES = 3
RETRY_INTERVAL = 1
def build_headers() -> dict:
...
def call_api(payload: dict) -> dict:
...
def main() -> None:
...
if __name__ == '__main__':
main()
```
## 3. 命令行要求
- 使用 `argparse` 解析参数
- 参数名与文档中的字段名保持一致
- 缺失必填参数时,应给出明确报错
- 不把复杂业务逻辑塞进命令行参数描述里
## 4. 请求与重试要求
- 使用 `requests` 发起请求
- 显式设置 `timeout`
- 如业务允许重试,间隔至少 1 秒,最多 3 次
- 严禁无限循环重试
- 超过最大次数后必须抛出或输出明确错误
- 如需关闭证书校验或跟随跳转,应在代码中显式写出,而不是依赖默认行为
## 5. 输出要求
- 默认输出结构化 JSON
- 成功输出应优先保留最小必要字段
- 失败输出应包含明确错误原因
- 非调试场景不直接回显整段原始响应
- 输出需兼顾 AI 可读性与命令行可读性
## 6. 鉴权要求
- 鉴权模式必须与模块说明、脚本索引保持一致
- 需要 `appKey` 或 `access-token` 时,统一通过 `cms-auth-skills` 获取
- `nologin` 动作不依赖额外鉴权获取流程
- 业务脚本不负责登录、不负责换取 token
## 7. 日志与状态
- 如脚本需要日志,统一写入 `.cms-log/log/<skillcode>/`
- 如脚本需要缓存或状态,统一写入 `.cms-log/state/<skillcode>/`
- 日志中不得输出 token、appKey、authorization 等敏感值
- 不将运行时状态写回 Skill 包目录
## 8. 交付前自检
每个脚本完成后,至少检查以下事项:
- 脚本文件名是否与路由表一致
- 参数名是否与文档一致
- 鉴权模式是否与文档一致
- 超时、重试、错误退出是否明确
- `main()` 和 `if __name__ == '__main__'` 是否齐全
FILE:docs/006_XGJK_SKILL_AUTH_AND_SECURITY_SPEC.md
# 鉴权与安全规范
本规范只定义一条核心原则:所有需要鉴权的 Skill,统一通过 `cms-auth-skills` 获取并使用具体的 `appKey` 或 `access-token`。
## 1. 统一依赖
所有需要鉴权的 Skill 都统一依赖:
```yaml
dependencies:
- cms-auth-skills
```
## 2. 统一规则
1. 目标 Skill 不自己实现登录流程。
2. 目标 Skill 不自己维护鉴权方案说明。
3. 目标 Skill 只声明当前动作属于哪一种鉴权模式:`nologin`、`appKey`、`access-token`。
4. 当动作需要 `appKey` 或 `access-token` 时,统一通过 `cms-auth-skills` 获取。
## 3. 禁止事项
- 不在目标 Skill 中重复编写 token、appKey 的获取方式
- 不在目标 Skill 中实现本地登录、换 token、鉴权兜底逻辑
## 4. 交付要求
- 文档中明确写出动作的鉴权模式
- 需要鉴权时,统一指向 `cms-auth-skills`
- 除此之外,不再展开具体鉴权实现细节
FILE:docs/007_XGJK_SKILL_CREATION_WORKFLOW.md
# Skill 包构建流程(5 步详细指南)
> 本文档是按当前简化协议从零构建 Skill 包的完整操作手册。
> 创建规范以同目录编号文档为准:核心总则见 `001_XGJK_SKILL_PROTOCOL.md`,详细规则见 `002`-`008`。
## Step 1:理解业务与确认范围
先把目标说清楚,再开始建包。
至少确认以下问题:
1. 这个 Skill 解决什么问题?
2. 谁会触发它?用户会怎么说?
3. 需要拆成几个模块?
4. 每个模块下有哪些明确动作?
5. 哪些动作是读操作,哪些动作是写操作?
6. 每个动作需要哪种鉴权模式?
这一阶段的交付物:
- Skill 名称
- Skill 描述
- `skillcode`
- 模块清单
- 动作清单
- 各动作的鉴权模式
## Step 2:创建目录骨架
按最小结构先搭目录:
```bash
mkdir -p <skill-name>/references/<module>
mkdir -p <skill-name>/scripts/<module>
```
然后立即确认以下规则:
- 目标 Skill 统一依赖 `cms-auth-skills`
- 目标 Skill 不实现本地登录流程
- 运行时日志与状态写到工作区根目录 `.cms-log/`
这一阶段只做结构,不写实现细节。
## Step 3:先写 SKILL.md
`SKILL.md` 是总索引,应该先确定下来。
必须完成:
1. YAML 基本字段
2. 能力概览
3. 统一规范
4. 授权依赖说明
5. 输入完整性规则
6. 建议工作流
7. 脚本使用规则
8. 路由与加载规则
9. 宪章
10. 模块路由表
11. 能力树
这一阶段的目标不是“把内容写长”,而是把边界、入口和路由写准。
## Step 4:按模块逐个补全说明与脚本
对每个模块按以下顺序处理:
1. 编写 `references/<module>/README.md`
2. 编写 `scripts/<module>/<action>.py`
3. 更新 `scripts/<module>/README.md`
4. 回填 `SKILL.md` 中对应的模块路由表
### 4.1 模块说明至少写清楚
- 模块是干什么的
- 用户会在什么场景下触发
- 需要哪些输入
- 每个动作做什么
- 每个动作使用什么鉴权模式
- 输出结果如何理解
### 4.2 Python 脚本至少做到
- 可以直接命令行运行
- 参数名与文档一致
- 鉴权读取方式明确
- 请求超时明确
- 重试策略明确
- 输出结构明确
### 4.3 每完成一个动作就做一次迷你自检
- 路由表里的脚本路径是否存在
- 模块说明里的动作名是否覆盖该脚本
- 脚本参数与说明是否一致
- 鉴权模式在说明、脚本索引、脚本实现中是否一致
## Step 5:按清单完成三轮复核并交付
### 第一轮:逐项打勾
按 `008_XGJK_SKILL_VALIDATION_CHECKLIST.md` 逐条检查,不能跳项。
### 第二轮:交叉一致性检查
至少核对以下内容:
- `SKILL.md` 路由表和实际脚本是否一致
- 模块说明和脚本索引是否一致
- 实际目录和能力树是否一致
- 文档中的动作名、模块名、路径是否一致
### 第三轮:手工 smoke test
至少挑选一个读操作和一个写操作脚本做人工验证:
- 读操作验证输出格式
- 写操作验证确认流程和错误处理
全部通过后,再输出最终结论:
```text
✅ Skill 构建完成
- Skill 名称:<name>
- 模块数量:M
- 脚本数量:N
- 清单检查:全部通过
- 手工 smoke test:已完成
- 遗留问题:无
```
FILE:docs/008_XGJK_SKILL_VALIDATION_CHECKLIST.md
# Skills 规范校验清单
用于检查一个 Skill 包是否符合当前简化协议。执行时必须逐项核对,不允许只口头说“已通过”。
> 本清单与同目录编号协议文档保持同步:核心总则见 `001_XGJK_SKILL_PROTOCOL.md`,详细规则见 `002`-`007`。如有冲突,以最新专题规范为准。
## A. 基础声明
- [ ] 存在 `SKILL.md`
- [ ] `SKILL.md` 的 YAML 头包含 `name`、`description`、`skillcode`、`github`
- [ ] `SKILL.md` 的 YAML 头包含 `dependencies: - cms-auth-skills`
- [ ] `name`、目录名、`skillcode` 保持一致
- [ ] `name` 和 `skillcode` 只使用小写字母、数字和连字符
- [ ] 全文不存在模板占位符残留(如 `<skill-name>`、`<module>`、`<action>`)
## B. 目录与路由
- [ ] 存在 `references/` 目录
- [ ] 存在 `scripts/` 目录
- [ ] 每个模块都有 `references/<module>/README.md`
- [ ] 每个模块都有 `scripts/<module>/README.md`
- [ ] 每个可执行动作都有对应的 `scripts/<module>/<action>.py`
- [ ] `SKILL.md` 的能力概览与实际模块目录一致
- [ ] `SKILL.md` 的模块路由表与实际脚本一致
- [ ] `SKILL.md` 的能力树与实际目录结构一致
- [ ] 不存在未在 `SKILL.md` 中声明的孤立脚本
## C. 文档完整性
- [ ] `SKILL.md` 包含能力概览
- [ ] `SKILL.md` 包含统一规范
- [ ] `SKILL.md` 包含授权依赖说明
- [ ] `SKILL.md` 包含输入完整性规则
- [ ] `SKILL.md` 包含建议工作流
- [ ] `SKILL.md` 包含脚本使用规则
- [ ] `SKILL.md` 包含路由与加载规则
- [ ] `SKILL.md` 包含宪章
- [ ] 每个模块说明包含适用场景、输入要求、输出说明和动作列表
- [ ] 每个脚本索引包含脚本清单、运行方式、鉴权前置条件和返回说明
## D. Python 脚本
- [ ] 所有业务脚本均为 Python 文件
- [ ] 每个脚本可在命令行独立执行
- [ ] 每个脚本包含 `main()` 函数
- [ ] 每个脚本包含 `if __name__ == '__main__'` 守卫
- [ ] 每个脚本的参数名与文档保持一致
- [ ] 每个脚本显式设置超时
- [ ] 如实现重试,重试间隔不少于 1 秒,最大次数不超过 3 次
- [ ] 不存在无限循环重试逻辑
- [ ] 脚本默认输出结构化 JSON 或明确可解析文本
## E. 鉴权与安全
- [ ] 需要鉴权时,明确依赖 `cms-auth-skills`
- [ ] 每个脚本都声明了明确的鉴权模式:`nologin`、`appKey` 或 `access-token`
- [ ] 需要 `appKey` 或 `access-token` 时,统一通过 `cms-auth-skills` 获取
- [ ] `nologin` 动作不依赖额外鉴权获取流程
- [ ] 目标 Skill 未实现本地登录流程
- [ ] 不向用户输出 token、appKey、cookie、authorization 等敏感值
- [ ] 写操作、发布操作、同步操作在执行前要求再次确认
## F. 输出、日志与运行时状态
- [ ] 对用户只输出最小必要信息
- [ ] 非必要场景不回显完整原始响应
- [ ] 如脚本输出日志,统一写入 `.cms-log/log/<skillcode>/`
- [ ] 如脚本记录状态,统一写入 `.cms-log/state/<skillcode>/`
- [ ] 运行时日志和状态不写回 Skill 包目录
- [ ] 日志中不出现敏感信息
## G. 交付前复核
- [ ] 已按 `007_XGJK_SKILL_CREATION_WORKFLOW.md` 走完生成流程
- [ ] 已逐项勾选本清单,并记录每项结论
- [ ] 已完成至少一次手工 smoke test
- [ ] 已确认文档、脚本、目录三者没有互相矛盾
FILE:references/issue-report/README.md
# issue-report 模块说明
`cms-create-skill` 不再内置问题反馈实现。
如果用户在创建完成后要提交问题、查看问题列表、解决或关闭问题,统一转交 `cms-report-issue`。
## 交接命令
```bash
# 安装问题反馈 Skill
npx clawhub@latest install cms-report-issue --force
# 提交问题
python3 cms-report-issue/scripts/issue_report/report_issue.py \
--skill-code my-skill --version 1.0.0 --error "..."
# 查看问题
python3 cms-report-issue/scripts/issue_report/list_issues.py --skill-code my-skill
# 标记已解决
python3 cms-report-issue/scripts/issue_report/update_issue.py \
--issue-id abc123 --status resolved --resolution "已修复"
```
## 说明
- `cms-create-skill` 只保留依赖声明,不再维护 `issue_report` 脚本目录。
- 问题状态更新需要的鉴权规则,由 `cms-report-issue` 自己负责。
- 如果你当前正在 `cms-create-skill` 里,这里就是问题反馈的下一步入口。
FILE:references/skill-management/README.md
# skill-management 模块说明
## 模块职责
`skill-management` 在 `cms-create-skill` 内只负责发现:
1. 发现平台已有 Skill。
## 脚本清单
| 脚本 | 说明 | 需要鉴权 |
|---|---|---|
| `get_skills.py` | 查看列表、搜索、查看详情 | 否 |
## 常见场景
| 场景 | 触发意图 | 脚本 |
|---|---|---|
| 用户要查看平台上有哪些 Skill | "查看 Skill 列表"、"有哪些 Skill" | `get_skills.py` |
| 用户要搜索某类 Skill | "搜一下 xxx 相关 Skill" | `get_skills.py --search xxx` |
| 用户要把 Skill 发布到我们内部平台 | "发布 Skill"、"注册 Skill"、"发布到内部平台" | 转交 `cms-push-skill` |
| 用户要更新或下架 Skill | "更新 Skill"、"下架 Skill" | 转交 `cms-push-skill` |
## 发布移交
这里说的“内部平台”,就是当前技能管理平台。
```bash
# 安装发布 Skill
npx clawhub@latest install cms-push-skill --force
# 查看列表
python3 cms-create-skill/scripts/skill-management/get_skills.py
# 内部 Skill:一站式发布到内部平台
python3 cms-push-skill/scripts/skill-management/publish_skill.py \
./im-robot --code im-robot --name "IM 机器人" --internal
# 内部 Skill:更新到内部平台
python3 cms-push-skill/scripts/skill-management/publish_skill.py \
./im-robot --code im-robot --update --version 1.0.0 --internal
# 外部 Skill:同步到平台
python3 cms-push-skill/scripts/skill-management/publish_skill.py \
./im-robot --code im-robot --name "IM 机器人" --external
```
## 说明
- 本模块不再维护发布、更新、下架的重复脚本实现。
- 发布链路统一由 `cms-push-skill` 负责。
- 本模块不维护旧接口文档副本,所有说明都放在 Markdown 文档里。
- 如果你当前在 `cms-create-skill` 里,这里就是发布到内部平台的下一步入口。
FILE:scripts/fetch_api_doc.py
#!/usr/bin/env python3
"""
API 接口文档获取器(智能识别 URL 类型)
用途:
根据 URL 自动识别文档类型并获取接口信息:
- Markdown URL → 直接获取 Markdown 内容
- Swagger URL (doc.html#/...) → 解析 Swagger JSON,提取目标接口的完整子集
使用方式:
python3 fetch_api_doc.py <url1> [url2] ... [--output-dir <dir>]
示例:
# Markdown 文档(自动识别)
python3 fetch_api_doc.py "https://example.com/api-docs/im-robot.md"
# Swagger 链接(自动识别,支持多个)
python3 fetch_api_doc.py \
"https://host/api-center/doc.html#/im/1.机器人管理/deleteMyRobotUsingPOST" \
"https://host/api-center/doc.html#/im/1.机器人管理/listVisibleUsingGET" \
--output-dir ./temp
# 混合使用
python3 fetch_api_doc.py "https://example.com/api.md" "https://host/api-center/doc.html#/im/xxx/someAPI"
"""
import sys
import json
import os
import requests
import warnings
# 禁用 InsecureRequestWarning (因为 verify=False)
warnings.filterwarnings("ignore", category=requests.packages.urllib3.exceptions.InsecureRequestWarning)
from urllib.parse import urlparse, unquote
# ============================================================
# 通用:HTTP 请求
# ============================================================
def _http_get(url: str, accept: str = "*/*") -> bytes:
"""通用 HTTP GET,支持 gzip,忽略 SSL 验证"""
headers = {
"Accept": accept,
"User-Agent": "SkillDocFetcher/1.0",
}
try:
response = requests.get(
url,
headers=headers,
verify=False,
allow_redirects=True,
timeout=30,
)
response.raise_for_status()
return response.content
except Exception as e:
error_msg = response.text if 'response' in locals() else str(e)
raise RuntimeError(f"请求 {url} 失败: {error_msg}")
# ============================================================
# URL 类型识别
# ============================================================
def detect_url_type(url: str) -> str:
"""
自动识别 URL 类型:
- 'swagger' : 包含 doc.html# 的 Swagger 链接
- 'markdown' : 其他(默认当作 Markdown/文本文档)
"""
parsed = urlparse(url)
path_lower = parsed.path.lower()
fragment = parsed.fragment
# Swagger 特征:路径包含 doc.html 且有 hash fragment
if "doc.html" in path_lower and fragment:
return "swagger"
return "markdown"
# ============================================================
# Markdown 文档获取
# ============================================================
def fetch_markdown(url: str) -> str:
"""从 URL 获取 Markdown/文本文档内容"""
data = _http_get(url, accept="text/markdown, text/plain, */*")
return data.decode("utf-8")
# ============================================================
# Swagger 解析(从原 parse_swagger.py 合并)
# ============================================================
def parse_swagger_url(doc_url: str) -> dict:
"""
解析 doc.html# 链接,提取 host、serviceName、operationId。
输入:https://host/api-center/doc.html#/im/2.消息管理/msgListByIdsUsingPOST
返回:{ host, scheme, service_name, operation_id, swagger_url }
"""
parsed = urlparse(doc_url)
host = parsed.hostname
port = parsed.port
scheme = parsed.scheme or "https"
fragment = unquote(parsed.fragment).lstrip("/")
parts = fragment.split("/")
if len(parts) < 2:
raise ValueError(f"URL fragment 格式不正确: {fragment}")
service_name = parts[0]
operation_id = parts[-1]
# 提取 doc.html 之前的路径前缀(如 /api-center)
path = parsed.path
doc_idx = path.lower().find("/doc.html")
path_prefix = path[:doc_idx] if doc_idx >= 0 else ""
# 构造正确的 origin(保留 scheme、host、port)
if port and port not in (80, 443):
origin = f"{scheme}://{host}:{port}"
else:
origin = f"{scheme}://{host}"
swagger_url = f"{origin}{path_prefix}/{service_name}/v2/api-docs"
return {
"host": host, "scheme": scheme,
"service_name": service_name,
"operation_id": operation_id,
"swagger_url": swagger_url,
}
def fetch_swagger_json(swagger_url: str) -> dict:
"""获取 Swagger JSON 文档"""
data = _http_get(swagger_url, accept="application/json")
return json.loads(data.decode("utf-8"))
def collect_refs(obj, refs: set):
"""递归收集 $ref 引用的 definition 名称"""
if isinstance(obj, dict):
if "$ref" in obj:
ref_path = obj["$ref"]
if ref_path.startswith("#/definitions/"):
refs.add(ref_path.split("/")[-1])
for v in obj.values():
collect_refs(v, refs)
elif isinstance(obj, list):
for item in obj:
collect_refs(item, refs)
def resolve_all_definitions(swagger: dict, initial_refs: set) -> dict:
"""递归解析所有嵌套的 definitions"""
all_defs = swagger.get("definitions", {})
resolved, queue, visited = {}, list(initial_refs), set()
while queue:
name = queue.pop(0)
if name in visited:
continue
visited.add(name)
if name in all_defs:
resolved[name] = all_defs[name]
nested = set()
collect_refs(all_defs[name], nested)
queue.extend(r for r in nested if r not in visited)
return resolved
def extract_api_subset(swagger: dict, operation_id: str) -> dict:
"""提取单个接口的完整 Swagger 子集"""
for path, methods in swagger.get("paths", {}).items():
for method, detail in methods.items():
if isinstance(detail, dict) and detail.get("operationId") == operation_id:
refs = set()
collect_refs(detail, refs)
definitions = resolve_all_definitions(swagger, refs)
subset = {
"swagger": swagger.get("swagger", "2.0"),
"info": swagger.get("info", {}),
"host": swagger.get("host", ""),
"basePath": swagger.get("basePath", "/"),
"schemes": swagger.get("schemes", []),
"consumes": swagger.get("consumes", ["*/*"]),
"produces": swagger.get("produces", ["*/*"]),
"paths": {path: {method: detail}},
}
if definitions:
subset["definitions"] = definitions
return subset
raise ValueError(f"operationId not found: {operation_id}")
# ============================================================
# 统一处理入口
# ============================================================
def process_urls(urls: list, output_dir: str = None) -> list:
"""
处理多个 URL,自动识别类型并分别处理。
返回处理结果列表。
"""
results = []
swagger_cache = {} # 缓存:同一 swagger_url 只请求一次
for url in urls:
url_type = detect_url_type(url)
print(f"[识别] {url_type.upper()} — {url}", file=sys.stderr)
try:
if url_type == "markdown":
# ── Markdown 文档 ──
content = fetch_markdown(url)
print(f"[完成] 获取 {len(content)} 字符", file=sys.stderr)
result = {
"source_url": url,
"type": "markdown",
"content": content,
}
results.append(result)
# 写入文件
if output_dir:
os.makedirs(output_dir, exist_ok=True)
# 从 URL 提取文件名
filename = os.path.basename(urlparse(url).path) or "api-doc.md"
if not filename.endswith(".md"):
filename += ".md"
filepath = os.path.join(output_dir, filename)
with open(filepath, "w", encoding="utf-8") as f:
f.write(content)
print(f"[保存] {filepath}", file=sys.stderr)
elif url_type == "swagger":
# ── Swagger 链接 ──
parsed = parse_swagger_url(url)
swagger_url = parsed["swagger_url"]
if swagger_url not in swagger_cache:
print(f"[获取] {swagger_url} ...", file=sys.stderr)
swagger_cache[swagger_url] = fetch_swagger_json(swagger_url)
swagger = swagger_cache[swagger_url]
subset = extract_api_subset(swagger, parsed["operation_id"])
result = {
"source_url": url,
"type": "swagger",
"service_name": parsed["service_name"],
"operation_id": parsed["operation_id"],
"swagger_subset": subset,
}
results.append(result)
# 写入文件
if output_dir:
os.makedirs(output_dir, exist_ok=True)
filename = f"{parsed['service_name']}_{parsed['operation_id']}.json"
filepath = os.path.join(output_dir, filename)
with open(filepath, "w", encoding="utf-8") as f:
json.dump(subset, f, ensure_ascii=False, indent=2)
print(f"[完成] {parsed['operation_id']} -> {filepath}", file=sys.stderr)
except Exception as e:
print(f"[错误] {url}: {e}", file=sys.stderr)
results.append({"source_url": url, "error": str(e)})
return results
# ============================================================
# CLI 入口
# ============================================================
if __name__ == "__main__":
if len(sys.argv) < 2 or any(arg in ("-h", "--help") for arg in sys.argv[1:]):
print("用法: python3 fetch_api_doc.py <url1> [url2] ... [--output-dir <dir>]")
print("")
print("自动识别 URL 类型:")
print(" - 包含 doc.html# → Swagger 解析模式")
print(" - 其他 URL → Markdown 文档获取模式")
print(" - 支持混合使用")
print("")
print("示例:")
print(' python3 fetch_api_doc.py "https://example.com/api-docs/im.md"')
print(' python3 fetch_api_doc.py "https://host/api-center/doc.html#/im/xxx/someAPI" --output-dir ./temp')
sys.exit(0 if len(sys.argv) >= 2 else 1)
urls = []
output_dir = None
i = 1
while i < len(sys.argv):
if sys.argv[i] == "--output-dir" and i + 1 < len(sys.argv):
output_dir = sys.argv[i + 1]
i += 2
else:
urls.append(sys.argv[i])
i += 1
if not urls:
print("错误: 至少提供一个 URL", file=sys.stderr)
sys.exit(1)
results = process_urls(urls, output_dir)
# 输出结果到 stdout
# 如果只有一个 markdown 结果,直接输出内容(方便 AI 读取)
if len(results) == 1 and results[0].get("type") == "markdown":
print(results[0]["content"])
else:
print(json.dumps(results, ensure_ascii=False, indent=2))
FILE:scripts/skill-management/README.md
# skill-management 脚本清单
## 模块定位
`skill-management` 在 `cms-create-skill` 中只保留发现能力:
1. 查看平台已有 Skill。
对应说明统一见 `../../references/skill-management/README.md`。
## 脚本列表
| 脚本 | 说明 | 需要鉴权 |
|---|---|---|
| `get_skills.py` | 获取 Skill 列表、搜索、详情 | 否 |
## 发布移交
```bash
# 查看当前已发布的 Skill
python3 scripts/skill-management/get_skills.py
# 一站式发布内部 Skill
python3 cms-push-skill/scripts/skill-management/publish_skill.py \
./im-robot --code im-robot --name "IM 机器人" --internal
```
## 说明
- 发布、更新、下架脚本已经从 `cms-create-skill` 中剥离。
- 如需继续发布链路,统一转交 `cms-push-skill`。
FILE:scripts/skill-management/get_skills.py
#!/usr/bin/env python3
"""
发现 Skill — 浏览、搜索、查看详情
完整实现位于 cms-find-skills/scripts/skill_registry/get_skills.py。
本文件作为薄封装,自动定位并复用该实现,避免双份维护。
使用方式:
python3 cms-create-skill/scripts/skill-management/get_skills.py
python3 cms-create-skill/scripts/skill-management/get_skills.py --search "机器人"
python3 cms-create-skill/scripts/skill-management/get_skills.py --detail "im-robot"
"""
from __future__ import annotations
import os
import sys
_HERE = os.path.dirname(os.path.abspath(__file__))
_REPO_ROOT = os.path.abspath(os.path.join(_HERE, "..", "..", ".."))
_FIND_SKILLS_REGISTRY = os.path.join(
_REPO_ROOT, "cms-find-skills", "scripts", "skill_registry"
)
if not os.path.isdir(_FIND_SKILLS_REGISTRY):
print(
f"找不到 cms-find-skills/scripts/skill_registry: {_FIND_SKILLS_REGISTRY}",
file=sys.stderr,
)
sys.exit(1)
sys.path.insert(0, _FIND_SKILLS_REGISTRY)
from get_skills import main # type: ignore # noqa: E402
if __name__ == "__main__":
main()
CMS 基础鉴权 Skill。任何业务接口 Header 需要 appKey 或 access-token 时都必须先触发本 Skill。支持从上下文、环境变量、sender_id+account_id、appKey换token,并在失败时向用户索要 appKey(工作协同 key / cowork key)。
---
name: cms-auth-skills
description: CMS 基础鉴权 Skill。任何业务接口 Header 需要 appKey 或 access-token 时都必须先触发本 Skill。支持从上下文、环境变量、sender_id+account_id、appKey换token,并在失败时向用户索要 appKey(工作协同 key / cowork key)。
skillcode: cms-auth-skills
github: https://github.com/xgjk/xg-skills/tree/main/cms-auth-skills
priority: 1
---
# cms-auth-skills
**版本**: v2.3.4
## 定位
- 本 Skill 只负责鉴权值解析,输出 `appKey` 或 `access-token`
- 术语统一:`appKey = 工作协同 key = cowork key`
- 任何业务接口需要鉴权 Header,都必须先触发本 Skill
## AI 执行总规则
1. 先判断目标接口需要 `appKey` 还是 `access-token`
2. 再按固定优先级解析,不要跳步骤
3. 解析失败时,只向用户索要 `appKey`
4. `appKey` 解析与 `access-token` 解析是两个独立步骤:只需要 `appKey` 时,不要主动换 `access-token`
5. 统一通过 `scripts/auth/login.py` 执行,不需要读取或分析 `scripts/auth/*.py` 源码
## 支持能力(5 条)
1. 支持从上下文/显式参数直接获取 `appKey`、`access-token`
2. 支持从环境变量获取(并做判空):`XG_BIZ_API_KEY`、`XG_USER_TOKEN`
3. 支持通过 `appKey` 获取 `access-token`
4. 支持通过 `sender_id + account_id` 获取 `appKey`
5. 支持在自动解析失败时向用户索要 `appKey`
## 解析优先级(必须按顺序)
### 目标是 `appKey`
1. 上下文/显式 `appKey`
2. 环境变量 `XG_BIZ_API_KEY`
3. `sender_id + account_id`
4. 向用户索要 `appKey`
### 目标是 `access-token`
1. 上下文/显式 `access-token`
2. 上下文/显式 `appKey` -> 换 `access-token`
3. 环境变量 `XG_USER_TOKEN`
4. 环境变量 `XG_BIZ_API_KEY` -> 换 `access-token`
5. `sender_id + account_id` -> `appKey` -> `access-token`
6. 向用户索要 `appKey`
## 上下文字段兼容
- `appKey`:`appKey` / `app_key` / `appkey`
- `access-token`:`access-token` / `access_token` / `token`
- `sender_id`:`sender_id` / `senderId` / `send_id` / `sendId`
- `account_id`:`account_id` / `accountId`
## CLI
推荐参数:
- `--app-key`
- `--access-token`
- `--sender-id`
- `--account-id`
- `--resolve-app-key`
- `--ensure`
- `--update`
兼容旧调用:
- `--context-json`
### 常用示例
```bash
# 直接返回 token
python3 login.py --ensure --access-token "your-token"
# 用 appKey 换 token
python3 login.py --ensure --app-key "your-app-key"
# 只解析 appKey
python3 login.py --resolve-app-key --sender-id "user-001" --account-id "xgjk_prod"
# 兼容旧调用
python3 login.py --ensure --context-json '{"appKey":"your-app-key"}'
```
FILE:_meta.json
{
"ownerId": "kn75s45s478x9t53qv91jrmb7h8208xm",
"slug": "cms-auth-skills",
"version": "2.3.4",
"publishedAt": 1775645231305
}
FILE:scripts/auth/auth_support.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from __future__ import print_function
import json
import os
import re
import urllib.parse
from datetime import datetime, timedelta, timezone
from pathlib import Path
LOG_RETENTION_DAYS = 30
TZ_CN = timezone(timedelta(hours=8))
SKILL_CODE = 'cms-auth-skills'
RUNTIME_ROOT_NAME = '.cms-log'
LOG_DIR_NAME = 'log'
STATE_DIR_NAME = 'state'
LEGACY_RUNTIME_ROOT_NAMES = ('.cms-auth', 'cms-auth')
MIGRATION_SKIP_NAMES = ('skills',)
class AuthInputs(object):
def __init__(self, app_key=None, access_token=None, sender_id=None, account_id=None):
self.app_key = app_key
self.access_token = access_token
self.sender_id = sender_id
self.account_id = account_id
def _load_json_object(path):
if not path.exists() or not path.is_file():
return None
with open(str(path), 'r', encoding='utf-8') as file_obj:
payload = json.load(file_obj)
return payload if isinstance(payload, dict) else None
def _write_json_object(path, payload):
path.parent.mkdir(parents=True, exist_ok=True)
with open(str(path), 'w', encoding='utf-8') as file_obj:
json.dump(payload, file_obj, ensure_ascii=False, indent=2)
file_obj.write('\n')
def _move_missing_items(source_dir, target_dir):
if not source_dir.exists() or not source_dir.is_dir() or source_dir == target_dir:
return
target_dir.mkdir(parents=True, exist_ok=True)
for child in source_dir.iterdir():
if child.name in MIGRATION_SKIP_NAMES:
continue
target_child = target_dir / child.name
if target_child.exists():
if child.is_dir():
_move_missing_items(child, target_child)
continue
child.rename(target_child)
try:
source_dir.rmdir()
except OSError:
pass
def _merge_auth_cache_file(source_path, target_path):
if not source_path.exists() or not source_path.is_file():
return
try:
source_payload = _load_json_object(source_path) or {}
except (ValueError, OSError):
source_payload = {}
if not target_path.exists():
target_path.parent.mkdir(parents=True, exist_ok=True)
source_path.rename(target_path)
return
try:
target_payload = _load_json_object(target_path) or {}
except (ValueError, OSError):
target_payload = {}
for sender_id, source_value in source_payload.items():
if sender_id not in target_payload:
target_payload[sender_id] = source_value
continue
if isinstance(source_value, dict) and isinstance(target_payload.get(sender_id), dict):
for key, value in source_value.items():
target_payload[sender_id].setdefault(key, value)
_write_json_object(target_path, target_payload)
try:
source_path.unlink()
except OSError:
pass
def _runtime_log_dir(runtime_root, skillcode=SKILL_CODE):
return runtime_root / LOG_DIR_NAME / skillcode
def _runtime_state_dir(runtime_root, skillcode=SKILL_CODE):
return runtime_root / STATE_DIR_NAME / skillcode
def _runtime_state_file(runtime_root, filename, skillcode=SKILL_CODE):
return _runtime_state_dir(runtime_root, skillcode) / filename
def _legacy_runtime_root_candidates(workspace_root, skills_dir=None):
candidates = []
for legacy_name in LEGACY_RUNTIME_ROOT_NAMES:
candidates.append(workspace_root / legacy_name)
if skills_dir:
for legacy_name in (RUNTIME_ROOT_NAME,) + LEGACY_RUNTIME_ROOT_NAMES:
candidates.append(skills_dir / legacy_name)
if skills_dir.parent.name in (RUNTIME_ROOT_NAME,) + LEGACY_RUNTIME_ROOT_NAMES:
candidates.append(skills_dir.parent)
return candidates
def _migrate_legacy_runtime_root(legacy_root, runtime_root):
if not legacy_root.exists() or legacy_root == runtime_root:
return
_merge_auth_cache_file(legacy_root / 'auth.json', _runtime_state_file(runtime_root, 'auth.json'))
_move_missing_items(legacy_root / 'logs', _runtime_log_dir(runtime_root))
_move_missing_items(legacy_root / LOG_DIR_NAME / SKILL_CODE, _runtime_log_dir(runtime_root))
_move_missing_items(legacy_root / STATE_DIR_NAME / SKILL_CODE, _runtime_state_dir(runtime_root))
def _find_runtime_root():
script_dir = Path(__file__).resolve().parent
skill_dir = script_dir.parent.parent
skills_ancestors = [current for current in (skill_dir,) + tuple(skill_dir.parents) if current.name == 'skills']
skills_dir = skills_ancestors[0] if skills_ancestors else None
workspace_root = skills_dir.parent if skills_dir else skill_dir.parent
runtime_root = workspace_root / RUNTIME_ROOT_NAME
runtime_root.mkdir(parents=True, exist_ok=True)
for legacy_root in _legacy_runtime_root_candidates(workspace_root, skills_dir=skills_dir):
_migrate_legacy_runtime_root(legacy_root, runtime_root)
return runtime_root
def _ensure_logs_dir():
LOGS_DIR.mkdir(parents=True, exist_ok=True)
def _ensure_state_dir():
STATE_DIR.mkdir(parents=True, exist_ok=True)
def runtime_state_file(filename, skillcode=SKILL_CODE):
return _runtime_state_file(RUNTIME_ROOT, filename, skillcode=skillcode)
RUNTIME_ROOT = _find_runtime_root()
STATE_DIR = _runtime_state_dir(RUNTIME_ROOT)
AUTH_JSON = runtime_state_file('auth.json')
LOGS_DIR = _runtime_log_dir(RUNTIME_ROOT)
def stringify(value):
if value is None:
return None
if isinstance(value, str):
text = value.strip()
else:
try:
text = str(value).strip()
except Exception:
return None
if text.lower() in ('', 'null', 'none', 'undefined'):
return None
return text
def pick_non_empty(*values):
for value in values:
normalized = stringify(value)
if normalized:
return normalized
return None
def mask_sensitive(value):
value = stringify(value)
if not value or len(value) <= 6:
return '***'
return value[:6] + '***'
def _normalize_log_key(key):
if not key:
return ''
return str(key).strip().lower().replace('_', '-')
def _is_sensitive_log_key(key):
return _normalize_log_key(key) in (
'appkey',
'app-key',
'access-token',
'token',
'authorization',
'requestkey',
'request-key',
)
def sanitize_url(url):
parsed = urllib.parse.urlsplit(url)
if not parsed.query:
return url
query_pairs = urllib.parse.parse_qsl(parsed.query, keep_blank_values=True)
sanitized_pairs = []
for key, value in query_pairs:
if _is_sensitive_log_key(key):
sanitized_pairs.append((key, mask_sensitive(value)))
else:
sanitized_pairs.append((key, value))
return urllib.parse.urlunsplit((
parsed.scheme,
parsed.netloc,
parsed.path,
urllib.parse.urlencode(sanitized_pairs, doseq=True),
parsed.fragment,
))
_URL_PATTERN = re.compile(r'https?://[^\s]+')
_SENSITIVE_ASSIGN_PATTERN = re.compile(
r'(?i)(["\']?(?:appkey|app-key|access-token|authorization|requestkey|request-key|token)["\']?\s*[:=]\s*)(["\']?)([^"\'\s,}&]+)(\2)'
)
_SENSITIVE_WORD_PATTERN = re.compile(
r'(?i)(\b(?:appkey|app-key|access-token|authorization|requestkey|request-key|token)\b\s+)([^\s,}&]+)'
)
def sanitize_log_message(message):
text = str(message)
text = _URL_PATTERN.sub(lambda match: sanitize_url(match.group(0)), text)
text = _SENSITIVE_ASSIGN_PATTERN.sub(
lambda match: '%s%s%s%s' % (
match.group(1),
match.group(2),
mask_sensitive(match.group(3)),
match.group(4),
),
text,
)
text = _SENSITIVE_WORD_PATTERN.sub(
lambda match: '%s%s' % (match.group(1), mask_sensitive(match.group(2))),
text,
)
return text
def log_file_path():
return LOGS_DIR / ('%s.log' % datetime.now(TZ_CN).strftime('%Y-%m-%d'))
def _iter_log_lines(message):
lines = str(message).splitlines()
return lines or ['']
def write_log(level, message):
try:
_ensure_logs_dir()
now = datetime.now(TZ_CN).strftime('%Y-%m-%d %H:%M:%S')
safe_message = sanitize_log_message(message)
with open(str(log_file_path()), 'a', encoding='utf-8') as file_obj:
for line in _iter_log_lines(safe_message):
file_obj.write('[%s] 【skillcode】: %s [%s] %s\n' % (now, SKILL_CODE, level, line))
except Exception:
pass
def cleanup_old_logs():
try:
if not LOGS_DIR.exists():
return
cutoff = datetime.now(TZ_CN) - timedelta(days=LOG_RETENTION_DAYS)
cutoff_str = cutoff.strftime('%Y-%m-%d')
for log_file in LOGS_DIR.glob('*.log'):
if len(log_file.stem) == 10 and log_file.stem < cutoff_str:
try:
os.remove(str(log_file))
except OSError:
pass
except Exception:
pass
def load_auth_cache():
try:
if AUTH_JSON.exists():
with open(str(AUTH_JSON), 'r', encoding='utf-8') as file_obj:
data = json.load(file_obj)
if isinstance(data, dict):
return data
except (ValueError, OSError):
write_log('WARN', '读取缓存文件失败: %s' % AUTH_JSON)
return {}
def save_auth_cache(cache):
try:
_ensure_state_dir()
_write_json_object(AUTH_JSON, cache)
except OSError as exc:
write_log('ERROR', '写入缓存文件失败: %s' % exc)
def get_cached_value(sender_id, key):
sender_id = stringify(sender_id)
if not sender_id:
return None
cache = load_auth_cache()
user_data = cache.get(sender_id, {})
if not isinstance(user_data, dict):
return None
value = user_data.get(key)
return stringify(value)
def update_cache(sender_id, key, value):
sender_id = stringify(sender_id)
value = stringify(value)
if not sender_id or not value:
return
cache = load_auth_cache()
user_data = cache.get(sender_id)
if not isinstance(user_data, dict):
user_data = {}
cache[sender_id] = user_data
user_data[key] = value
user_data['updated_at'] = datetime.now(TZ_CN).isoformat()
save_auth_cache(cache)
def parse_context(context):
if isinstance(context, dict):
return context
if not isinstance(context, str):
return {}
text = context.strip()
if not text:
return {}
try:
payload = json.loads(text)
except ValueError:
return {}
return payload if isinstance(payload, dict) else {}
def build_inputs(context=None, app_key=None, access_token=None, sender_id=None, account_id=None):
payload = parse_context(context)
return AuthInputs(
app_key=pick_non_empty(app_key, payload.get('appKey'), payload.get('app_key'), payload.get('appkey')),
access_token=pick_non_empty(
access_token,
payload.get('access-token'),
payload.get('access_token'),
payload.get('token'),
),
sender_id=pick_non_empty(
sender_id,
payload.get('sender_id'),
payload.get('senderId'),
payload.get('send_id'),
payload.get('sendId'),
),
account_id=pick_non_empty(account_id, payload.get('account_id'), payload.get('accountId')),
)
FILE:scripts/auth/login.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
统一鉴权解析模块(简化版)。
核心能力只保留三件事:
1. get_token_by_app_key: 通过 appKey 获取 access-token
2. get_app_key_by_sender: 通过 sender_id + account_id 获取 appKey(带缓存)
3. get_env_auth: 从环境变量获取 appKey / access-token
对外入口保持不变:
- resolve_app_key(...)
- ensure_token(...)
"""
from __future__ import print_function
import argparse
import os
import sys
import warnings
from pathlib import Path
_SCRIPT_DIR = Path(__file__).resolve().parent
if str(_SCRIPT_DIR) not in sys.path:
sys.path.insert(0, str(_SCRIPT_DIR))
import auth_support
import self_update
try:
import requests
except ImportError:
requests = None
TOKEN_AUTH_URL = 'https://sg-cwork-web.mediportal.com.cn/user/login/appkey'
APP_CODE = 'cms_gpt'
APPKEY_API_URL = 'https://sg-al-cwork-web.mediportal.com.cn/user/appkey/getAppKeyByDingUserId/nologin'
APPKEY_REQUEST_KEY = 'MTrBkZsNFFghxH5SmKxWWc93KJqe0'
DING_CORP_ID_RULES = (
('xgjk', 'ding0b8223c4cde4879dee0f45d8e4f7c288'),
('kangzhe', 'dingf57b758b536306eea1320dcb25e91351'),
('demei', 'ding452d1e907f3ae594f2c783f7214b6d69'),
('xgjkrtest', 'dingf019ee9a572ee7daa39a90f97fcb1e09'),
)
if requests is not None:
try:
warnings.filterwarnings(
'ignore',
category=requests.packages.urllib3.exceptions.InsecureRequestWarning,
)
except Exception:
pass
def _request_json(url, method='GET', body=None, params=None):
if requests is None:
raise RuntimeError('请求失败: 缺少 requests 依赖,请先安装 requests')
log_url = url
if params:
try:
prepared = requests.Request(method=method, url=url, params=params).prepare()
if prepared.url:
log_url = prepared.url
except Exception:
pass
try:
response = requests.request(
method=method,
url=url,
params=params,
json=body,
verify=False,
allow_redirects=True,
timeout=60,
)
response.raise_for_status()
except Exception as exc:
status = 'N/A'
resp_text = str(exc)
if hasattr(exc, 'response') and exc.response is not None:
status = exc.response.status_code
try:
resp_text = exc.response.text
except Exception:
pass
auth_support.write_log(
'ERROR',
'RequestError method=%s url=%s status=%s error=%s'
% (method, auth_support.sanitize_url(log_url), status, resp_text[:2000]),
)
if status == 'N/A':
raise RuntimeError('请求失败: %s' % exc)
raise RuntimeError('请求失败 (HTTP %s): %s' % (status, resp_text))
try:
payload = response.json()
except ValueError:
auth_support.write_log(
'ERROR',
'JSONDecodeError method=%s url=%s'
% (method, auth_support.sanitize_url(log_url)),
)
raise RuntimeError('请求失败:接口返回了无法解析的 JSON')
if not isinstance(payload, dict):
auth_support.write_log(
'ERROR',
'InvalidPayload method=%s url=%s raw=%s'
% (method, auth_support.sanitize_url(log_url), response.text[:2000]),
)
raise RuntimeError('请求失败:接口返回格式异常')
return payload
def _resolve_ding_corp_id(account_id):
account_id = auth_support.stringify(account_id)
if not account_id:
raise RuntimeError('无法自动获取 appKey')
account_prefix = account_id.lower().split('_', 1)[0].split('-', 1)[0]
for keyword, ding_corp_id in DING_CORP_ID_RULES:
if keyword == account_prefix:
return ding_corp_id
raise RuntimeError('无法自动获取 appKey')
def _extract_app_key(payload):
data = payload.get('data')
if isinstance(data, dict):
return auth_support.pick_non_empty(data.get('appKey'), data.get('app_key'), data.get('appkey'))
return auth_support.pick_non_empty(payload.get('appKey'), payload.get('app_key'), payload.get('appkey'))
def _extract_token(payload):
data = payload.get('data')
if not isinstance(data, dict):
return None
return auth_support.pick_non_empty(data.get('xgToken'), data.get('token'), data.get('access-token'))
# 能力 1:通过 appKey 获取 token
def get_token_by_app_key(app_key):
app_key = auth_support.stringify(app_key)
if not app_key:
raise RuntimeError('登录失败:appKey 不能为空')
payload = _request_json(
TOKEN_AUTH_URL,
method='GET',
params={'appCode': APP_CODE, 'appKey': app_key},
)
token = _extract_token(payload)
if token:
return token
message = auth_support.pick_non_empty(
payload.get('resultMsg'),
payload.get('detailMsg'),
payload.get('message'),
) or '未知错误'
raise RuntimeError('登录失败: %s' % message)
# 能力 2:通过 sender_id + account_id 获取 appKey(带缓存)
def get_app_key_by_sender(sender_id, account_id, force_update=False):
sender_id = auth_support.stringify(sender_id)
account_id = auth_support.stringify(account_id)
if not sender_id or not account_id:
raise RuntimeError('无法自动获取 appKey')
if not force_update:
cached_app_key = auth_support.get_cached_value(sender_id, 'appKey')
if cached_app_key:
return cached_app_key
payload = _request_json(
APPKEY_API_URL,
method='POST',
body={
'requestKey': APPKEY_REQUEST_KEY,
'dingCorpId': _resolve_ding_corp_id(account_id),
'dingUserId': sender_id,
},
)
app_key = _extract_app_key(payload)
if app_key:
auth_support.update_cache(sender_id, 'appKey', app_key)
return app_key
message = auth_support.pick_non_empty(
payload.get('resultMsg'),
payload.get('detailMsg'),
payload.get('message'),
) or '未知错误'
raise RuntimeError('获取 appKey 失败: %s' % message)
# 能力 3:从环境变量获取 appKey / token
def get_env_auth():
return (
auth_support.stringify(os.environ.get('XG_BIZ_API_KEY')),
auth_support.stringify(os.environ.get('XG_USER_TOKEN')),
)
def _cache_auth_values(sender_id, app_key=None, token=None):
auth_support.update_cache(sender_id, 'appKey', app_key)
auth_support.update_cache(sender_id, 'token', token)
def resolve_app_key(
context=None,
quiet=False,
force_update=False,
app_key=None,
access_token=None,
sender_id=None,
account_id=None,
):
_ = quiet
inputs = auth_support.build_inputs(
context=context,
app_key=app_key,
access_token=access_token,
sender_id=sender_id,
account_id=account_id,
)
if inputs.app_key:
auth_support.update_cache(inputs.sender_id, 'appKey', inputs.app_key)
return inputs.app_key
env_app_key, _ = get_env_auth()
if env_app_key:
auth_support.update_cache(inputs.sender_id, 'appKey', env_app_key)
return env_app_key
if inputs.sender_id:
if not force_update:
cached_app_key = auth_support.get_cached_value(inputs.sender_id, 'appKey')
if cached_app_key:
return cached_app_key
if inputs.account_id:
return get_app_key_by_sender(inputs.sender_id, inputs.account_id, force_update=force_update)
raise RuntimeError('未能自动获取 appKey,请向用户索要 appKey(工作协同 key / cowork key)。')
def ensure_token(
context=None,
quiet=False,
force_update=False,
app_key=None,
access_token=None,
sender_id=None,
account_id=None,
):
_ = quiet
inputs = auth_support.build_inputs(
context=context,
app_key=app_key,
access_token=access_token,
sender_id=sender_id,
account_id=account_id,
)
# 步骤 A:明确已有 token 就直接返回
if inputs.access_token:
auth_support.update_cache(inputs.sender_id, 'token', inputs.access_token)
return inputs.access_token
# 步骤 B:明确已有 appKey,按需换 token
if inputs.app_key:
token = get_token_by_app_key(inputs.app_key)
_cache_auth_values(inputs.sender_id, app_key=inputs.app_key, token=token)
return token
# 步骤 C:环境变量
env_app_key, env_token = get_env_auth()
if env_token:
auth_support.update_cache(inputs.sender_id, 'token', env_token)
return env_token
if env_app_key:
token = get_token_by_app_key(env_app_key)
_cache_auth_values(inputs.sender_id, app_key=env_app_key, token=token)
return token
# 步骤 D:sender_id 相关缓存 + sender_id/account_id 自动获取 appKey,再换 token
if inputs.sender_id:
if not force_update:
cached_token = auth_support.get_cached_value(inputs.sender_id, 'token')
if cached_token:
return cached_token
cached_app_key = auth_support.get_cached_value(inputs.sender_id, 'appKey')
if cached_app_key:
token = get_token_by_app_key(cached_app_key)
_cache_auth_values(inputs.sender_id, app_key=cached_app_key, token=token)
return token
if inputs.account_id:
resolved_app_key = get_app_key_by_sender(inputs.sender_id, inputs.account_id, force_update=force_update)
token = get_token_by_app_key(resolved_app_key)
_cache_auth_values(inputs.sender_id, app_key=resolved_app_key, token=token)
return token
raise RuntimeError('未能自动获取 access-token,请先向用户索要 appKey(工作协同 key / cowork key)。')
def main():
update_state = self_update.check_for_update()
if update_state.get('needUpdate'):
print(update_state.get('message') or '检测到新版本,请先更新 cms-auth-skills。', file=sys.stderr, flush=True)
sys.exit(self_update.UPDATE_REQUIRED_EXIT_CODE)
parser = argparse.ArgumentParser(description='统一鉴权解析:appKey / access-token')
parser.add_argument('--app-key', '-k', type=str, help='显式传入 appKey(工作协同 key / cowork key)')
parser.add_argument('--access-token', type=str, help='显式传入 access-token')
parser.add_argument('--sender-id', type=str, help='显式传入 sender_id')
parser.add_argument('--account-id', type=str, help='显式传入 account_id')
parser.add_argument('--context-json', type=str, default='', help='兼容旧调用的上下文 JSON,推荐改用显式参数')
mode_group = parser.add_mutually_exclusive_group()
mode_group.add_argument('--resolve-app-key', action='store_true', help='输出一个可用的 appKey')
mode_group.add_argument('--ensure', action='store_true', help='输出一个可用的 access-token')
parser.add_argument('--update', action='store_true', help='强制刷新缓存(跳过缓存读取,重新获取)')
args = parser.parse_args()
auth_support.cleanup_old_logs()
inputs = auth_support.build_inputs(
context=args.context_json,
app_key=args.app_key,
access_token=args.access_token,
sender_id=args.sender_id,
account_id=args.account_id,
)
try:
if args.resolve_app_key:
print(
resolve_app_key(
force_update=args.update,
app_key=inputs.app_key,
access_token=inputs.access_token,
sender_id=inputs.sender_id,
account_id=inputs.account_id,
),
flush=True,
)
return
print(
ensure_token(
force_update=args.update,
app_key=inputs.app_key,
access_token=inputs.access_token,
sender_id=inputs.sender_id,
account_id=inputs.account_id,
),
flush=True,
)
except (RuntimeError, ValueError) as exc:
auth_support.write_log('ERROR', '脚本执行失败: %s' % exc)
print('错误: %s' % exc, file=sys.stderr, flush=True)
sys.exit(1)
if __name__ == '__main__':
main()
FILE:scripts/auth/self_update.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from __future__ import print_function
import json
import os
import sys
import warnings
from datetime import datetime, timezone
_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
if _SCRIPT_DIR not in sys.path:
sys.path.insert(0, _SCRIPT_DIR)
import auth_support
try:
import requests
except ImportError:
requests = None
CHECK_VERSION_URL = 'https://skills.mediportal.com.cn/api/skill/checkVersion'
CHECK_INTERVAL_SECONDS = 2 * 60 * 60
UPDATE_REQUIRED_EXIT_CODE = 20
TZ_UTC = timezone.utc
CHECK_STATE_FILE = 'version-check.json'
if requests is not None:
try:
warnings.filterwarnings(
'ignore',
category=requests.packages.urllib3.exceptions.InsecureRequestWarning,
)
except Exception:
pass
def _skill_root():
return os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
def _version_file():
return os.path.join(_skill_root(), 'version.json')
def _load_version_payload():
with open(_version_file(), 'r', encoding='utf-8') as file_obj:
payload = json.load(file_obj)
if not isinstance(payload, dict):
raise RuntimeError('version.json 必须是 JSON 对象')
skillcode = auth_support.stringify(payload.get('skillcode'))
version = auth_support.stringify(payload.get('version'))
if not skillcode or not version:
raise RuntimeError('version.json 缺少 skillcode/version')
payload['skillcode'] = skillcode
payload['version'] = version
return payload
def _check_state_file():
return auth_support.runtime_state_file(CHECK_STATE_FILE)
def _load_check_state(legacy_payload):
state_path = _check_state_file()
try:
if os.path.exists(state_path):
with open(state_path, 'r', encoding='utf-8') as file_obj:
payload = json.load(file_obj)
if isinstance(payload, dict):
return payload
except Exception as exc:
auth_support.write_log('WARN', '读取版本检查状态失败: %s' % exc)
fallback = {}
if isinstance(legacy_payload, dict):
if 'lastCheckAt' in legacy_payload:
fallback['lastCheckAt'] = legacy_payload.get('lastCheckAt')
if 'lastNeedUpdate' in legacy_payload:
fallback['lastNeedUpdate'] = legacy_payload.get('lastNeedUpdate')
return fallback
def _parse_last_check_at(value):
value = auth_support.stringify(value)
if not value:
return None
for fmt in (
'%Y-%m-%dT%H:%M:%SZ',
'%Y-%m-%dT%H:%M:%S.%fZ',
'%Y-%m-%d %H:%M:%S',
'%Y-%m-%dT%H:%M:%S',
):
try:
parsed = datetime.strptime(value, fmt)
return parsed.replace(tzinfo=TZ_UTC)
except ValueError:
continue
try:
return datetime.fromtimestamp(float(value), TZ_UTC)
except Exception:
return None
def _format_last_check_at(now_utc):
return now_utc.strftime('%Y-%m-%dT%H:%M:%SZ')
def _parse_saved_need_update(value):
if value is True:
return True
if value is False:
return False
normalized = auth_support.stringify(value)
if normalized == 'true':
return True
if normalized == 'false':
return False
return False
def _update_message():
return '检测到 cms-auth-skills 有新版本,请通过 ClawHub 更新当前 Skill 后重新执行。'
def _log(level, message):
auth_support.write_log(level, '[self-update] %s' % message)
def _should_check(last_check_at, now_utc):
if last_check_at is None:
return True
elapsed = now_utc - last_check_at
return elapsed.total_seconds() >= CHECK_INTERVAL_SECONDS
def _write_check_state(skillcode, version, now_utc, need_update):
state_path = _check_state_file()
next_payload = {
'skillcode': skillcode,
'version': version,
'lastCheckAt': _format_last_check_at(now_utc),
'lastNeedUpdate': bool(need_update),
}
temp_path = str(state_path) + '.tmp'
try:
state_dir = os.path.dirname(str(state_path))
if state_dir:
os.makedirs(state_dir, exist_ok=True)
with open(temp_path, 'w', encoding='utf-8') as file_obj:
json.dump(next_payload, file_obj, ensure_ascii=False, indent=2)
file_obj.write('\n')
os.replace(temp_path, state_path)
except Exception as exc:
try:
if os.path.exists(temp_path):
os.remove(temp_path)
except Exception:
pass
auth_support.write_log('WARN', '写入版本检查状态失败: %s' % exc)
def _request_check_version(skillcode, version):
if requests is None:
raise RuntimeError('版本检查失败: 缺少 requests 依赖')
try:
response = requests.post(
CHECK_VERSION_URL,
json={
'skillcode': skillcode,
'version': version,
},
headers={'Content-Type': 'application/json'},
verify=False,
allow_redirects=True,
timeout=60,
)
response.raise_for_status()
except Exception as exc:
raise RuntimeError('版本检查请求失败: %s' % exc)
try:
payload = response.json()
except ValueError:
raise RuntimeError('版本检查失败: 接口返回了无法解析的 JSON')
if not isinstance(payload, dict):
raise RuntimeError('版本检查失败: 接口返回格式异常')
if payload.get('needUpdate') is True:
return True
if payload.get('needUpdate') is False:
return False
normalized = auth_support.stringify(payload.get('needUpdate'))
if normalized == 'true':
return True
if normalized == 'false':
return False
raise RuntimeError('版本检查失败: 接口返回缺少 needUpdate')
def check_for_update():
payload = None
cached_need_update = False
try:
payload = _load_version_payload()
check_state = _load_check_state(payload)
now_utc = datetime.now(TZ_UTC)
last_check_at = _parse_last_check_at(check_state.get('lastCheckAt'))
cached_need_update = _parse_saved_need_update(check_state.get('lastNeedUpdate'))
_log('INFO', '开始检查版本 skillcode=%s version=%s' % (payload['skillcode'], payload['version']))
if not _should_check(last_check_at, now_utc):
_log(
'INFO',
'跳过远端检查 skillcode=%s cachedNeedUpdate=%s lastCheckAt=%s'
% (payload['skillcode'], bool(cached_need_update), check_state.get('lastCheckAt') or ''),
)
return {
'checked': False,
'needUpdate': cached_need_update,
'message': _update_message() if cached_need_update else '',
}
need_update = _request_check_version(payload['skillcode'], payload['version'])
_write_check_state(payload['skillcode'], payload['version'], now_utc, need_update)
if need_update:
_log('WARN', '检测到新版本 skillcode=%s version=%s' % (payload['skillcode'], payload['version']))
return {
'checked': True,
'needUpdate': True,
'message': _update_message(),
}
_log('INFO', '版本检查完成,无需更新 skillcode=%s version=%s' % (payload['skillcode'], payload['version']))
return {
'checked': True,
'needUpdate': False,
'message': '',
}
except Exception as exc:
if payload is not None:
_write_check_state(payload['skillcode'], payload['version'], datetime.now(TZ_UTC), cached_need_update)
auth_support.write_log('WARN', '版本检查已跳过: %s' % exc)
return {
'checked': False,
'needUpdate': cached_need_update,
'message': _update_message() if cached_need_update else '',
}
FILE:version.json
{
"skillcode": "cms-auth-skills",
"version": "2.3.4"
}
NoteX 技能路由网关索引(access-token 鉴权,由 cms-auth-skills 统一提供),覆盖内容生产(PPT/视频/音频/报告/脑图/测验/闪卡/信息图)、运营数据问答与洞察、笔记本管理(列表/统计/创建/追加来源)、来源索引与详情定位、首页登录链接生成。
---
name: notex-skills
description: NoteX 技能路由网关索引(access-token 鉴权,由 cms-auth-skills 统一提供),覆盖内容生产(PPT/视频/音频/报告/脑图/测验/闪卡/信息图)、运营数据问答与洞察、笔记本管理(列表/统计/创建/追加来源)、来源索引与详情定位、首页登录链接生成。
skillcode: notex-skills
dependencies:
- cms-auth-skills
---
# NoteX Skills — 索引
本文件提供**能力宪章 + 能力树 + 按需加载规则**。详细参数与流程见各模块 `openapi/` 与 `examples/`。
**当前版本**: v1.3
**接口版本**: 所有业务接口统一使用 `/openapi/*` 前缀,自带 `access-token` 鉴权,不依赖网关。
**能力概览(5 块能力)**:
- `open-link`:生成带 token 的 NoteX 首页访问链接
- `creator`:内容生产(八个工作室模块:PPT/视频/音频/报告/脑图/测验/闪卡/信息图)
- `ops`:运营数据问答与洞察
- `notebooks`:笔记本列表/统计/创建/追加来源
- `sources`:来源索引树与最小详情定位
统一规范:
- 认证与鉴权:统一由 `cms-auth-skills` 提供,详见 `cms-auth-skills/SKILL.md`
### 鉴权模式声明(重要)
> **NoteX 所有业务接口仅支持 `access-token` 鉴权模式,不支持 `appKey` 直接鉴权。**
>
> - 对应 `cms-auth-skills` 的 `login.py`,NoteX 场景**只允许使用 `--ensure` 参数**(返回 `access-token`)
> - **禁止使用 `--resolve-app-key`**(只返回 appKey,NoteX 接口无法识别)
> - **禁止使用 `--headers --auth-mode appKey`**(会构造 `appKey` Header,NoteX 接口不接受)
> - 正确用法:`python3 login.py --ensure` → 返回可直接用于 `access-token` Header 的 token 值
> - 内部流程:`login.py --ensure` 会自动完成 `appKey 获取 → 换取 access-token` 的全链路,调用方无需关心中间步骤
### 鉴权宪章(铁律)
1. **禁止问用户任何关于 token / 鉴权 / 登录的问题** — 鉴权统一依赖 `cms-auth-skills`
2. **所有鉴权操作必须且只能通过 `cms-auth-skills/scripts/auth/login.py --ensure` 执行**,严禁在脚本中直接调用底层 HTTP 鉴权接口
3. **鉴权模式锁定**:NoteX 所有接口只接受 `access-token`,不接受 `appKey`;调用 `login.py` 时**必须用 `--ensure`**
4. **对外克制**:对用户只输出"可用能力、必要输入、结果链接或摘要",不暴露鉴权细节与内部字段
### 授权依赖
- 当需要 `access-token` 时,优先直接读取 `cms-auth-skills/SKILL.md`
- 如果能读取到,直接按 `cms-auth-skills/SKILL.md` 中的鉴权规则,使用 `login.py --ensure` 准备 `access-token`
- 如果读取不到(说明未安装),再执行 `npx clawhub@latest install cms-auth-skills --force`
- 如果上面的安装方式不可用,再执行 `npx clawhub@latest install https://github.com/spzwin/cms-auth-skills.git --force`
- 安装完成后,再继续需要鉴权的操作
输入完整性规则(强制):
1. **内容生产必须完整**:调用 `creator` 的八个工作室模块时,必须提供完整上下文文本(`context_text`),不接受摘要、截断或缺失段落的内容。
2. **追加来源必须完整**:`notebooks/add-source` 的来源内容必须是完整原文,避免只传摘要或片段。
素材解析与技能建议(场景补充):
- 若用户丢链接/文件并要求生成 PPT/音频概览/报告等,建议先使用对应的**解析类技能**提取完整正文,再将完整内容传入 `creator` 的任务请求。
建议工作流(简版):
1. 读取 `SKILL.md`,明确能力范围与鉴权依赖(`cms-auth-skills`)。
2. 识别用户意图并路由模块,先打开 `openapi/<module>/api-index.md`。
3. 确认具体接口后,加载 `openapi/<module>/<endpoint>.md` 获取入参/出参/Schema。
4. 补齐用户必需输入,必要时先读取用户文件/URL 并确认摘要。
5. 参考 `examples/<module>/README.md` 组织话术与流程。
6. 若需要联调、批量或复杂编排,再加载对应 `scripts/`。
脚本使用规则(强制):
1. **脚本可独立执行**:所有 `scripts/` 下的脚本均可脱离 AI Agent 直接在命令行运行。
2. **先读文档再执行**:执行脚本前,**必须先阅读对应模块的 `openapi/<module>/api-index.md`**,获取完整入参说明与约束条件。
3. **入参来源**:脚本的所有入参定义与字段说明以 `openapi/` 文档为准,脚本仅负责编排调用流程。
4. **鉴权一致**:脚本内部统一通过 `cms-auth-skills/scripts/auth/login.py` 获取鉴权值(环境变量 `XG_USER_TOKEN` → `login.py --ensure`)。
5. **零依赖**:脚本仅使用 Python 标准库。
6. **stdout = 结果,stderr = 日志**。
意图路由与加载规则(强制):
1. **先路由再加载**:必须先判定模块,再打开该模块的 `api-index.md`。
2. **先读文档再调用**:在描述调用或执行前,必须加载对应接口文档(`openapi/<module>/<endpoint>.md`)。
3. **脚本按需**:涉及联调、批量或复杂编排时,必须加载对应 `scripts/`。
4. **不猜测**:若意图不明确,必须追问澄清,不允许跨模块或"默认模块"猜测。
宪章(必须遵守):
1. **只读索引**:`SKILL.md` 只描述"能做什么"和"去哪里读",不写具体接口参数。
2. **按需加载**:默认只读 `SKILL.md`,只有触发某模块时才加载该模块的 `openapi` 与 `examples`,必要时再加载 `scripts`。
3. **对外克制**:对用户只输出"可用能力、必要输入、结果链接或摘要",不暴露鉴权细节与内部字段。
4. **素材优先级**:用户给了文件或 URL,必须先提取内容再确认,确认后才触发生成或写入。
5. **生产约束**:仅允许生产域名与生产协议,不引入任何测试地址。
6. **接口拆分**:每个 API 独立成文档,路径为 `openapi/<module>/<endpoint>.md`;模块内 `api-index.md` 仅做索引。
7. **危险操作**:对可能导致数据泄露、破坏、越权或高风险副作用的请求,应礼貌拒绝并给出安全替代方案。
通用约束与约定:
1. **Header 规范**:所有业务接口统一携带 `access-token`(必传)、`Content-Type: application/json`(POST)。
2. **输出与脱敏**:对用户输出结论/摘要/可访问链接/必要操作提示。默认不输出 `token/xgToken/access-token`、`appKey/CWork Key`(除非索取授权)、任何内部主键。仅 `open-link` 场景允许返回带 token 的完整 URL。
3. **输入与请求校验**:所有接口参数需做类型/长度/枚举校验。文件与 URL 输入需限制类型、大小、超时与重定向。
4. **JSON 与字段回显**:不回显完整 JSON 响应。仅提取必要字段,避免输出过长列表或敏感字段。
5. **外部能力与数据来源**:使用文件或 URL 作为来源时,先读取并摘要确认,再触发生成或写入。不编造数据。
6. **轮询、异步与超时**:创作类任务 60 秒轮询一次,最多 20 次;仅在完成/失败/超时时回复。ops-chat 单次请求超时上限 300000ms。其他请求默认超时 60000ms。
7. **日志与审计**:日志中不得出现 token/密钥/敏感字段。
8. **危险操作处理**:对可能导致数据泄露、破坏、越权或高风险副作用的请求,必须礼貌拒绝。
9. **重试策略**:脚本执行出错时,间隔 1 秒、最多重试 3 次,禁止无限重试。
模块路由与能力索引(合并版):
| 用户意图(示例) | 模块 | 能力摘要 | 接口文档 | 示例模板 | 脚本(可独立执行) |
|---|---|---|---|---|---|
| 打开首页、生成登录/访问链接 | `open-link` | 生成带 token 的 NoteX 首页链接 | `./openapi/open-link/api-index.md`(`home-link.md`) | `./examples/open-link/README.md` | `./scripts/open-link/notex_open_link.py` |
| 内容生产(PPT/视频/音频/报告/脑图/测验/闪卡/信息图) | `creator` | 内容创作产物:PPT/视频/音频/报告/脑图/测验/闪卡/信息图 | `./openapi/creator/api-index.md`(`autoTask.md`、`taskStatus.md`) | `./examples/creator/README.md` | `./scripts/creator/skills_run.py` |
| 运营数据问答/洞察 | `ops` | 运营数据问答与洞察(ops-chat) | `./openapi/ops/api-index.md`(`ai-chat.md`) | `./examples/ops/README.md` | `./scripts/creator/skills_run.py`(复用) |
| 笔记本列表/统计/创建/追加来源/来源读取 | `notebooks` | 笔记本统计、列表、创建、追加来源与来源读取 | `./openapi/notebooks/api-index.md`(`list.md`、`category-counts.md`、`create.md`、`add-source.md`、`sources-list.md`、`source-content.md`) | `./examples/notebooks/README.md` | `./scripts/notebooks/notebooks_write.py`、`./scripts/notebooks/notebooks_read.py` |
| 来源索引树/详情 | `sources` | 来源索引树与最小详情定位 | `./openapi/sources/api-index.md`(`index-tree.md`、`details.md`) | `./examples/sources/README.md` | `./scripts/sources/source_index_sync.py` |
能力树(实际目录结构):
```text
notex-skills/
├── SKILL.md
├── openapi
│ ├── creator
│ │ ├── api-index.md
│ │ ├── autoTask.md
│ │ └── taskStatus.md
│ ├── ops
│ │ ├── api-index.md
│ │ └── ai-chat.md
│ ├── notebooks
│ │ ├── api-index.md
│ │ ├── category-counts.md
│ │ ├── list.md
│ │ ├── create.md
│ │ ├── add-source.md
│ │ ├── sources-list.md
│ │ └── source-content.md
│ ├── sources
│ │ ├── api-index.md
│ │ ├── index-tree.md
│ │ └── details.md
│ └── open-link
│ ├── api-index.md
│ └── home-link.md
├── examples
│ ├── creator/README.md
│ ├── ops/README.md
│ ├── notebooks/README.md
│ ├── sources/README.md
│ └── open-link/README.md
└── scripts ← 所有脚本可独立执行(Python)
├── creator/skills_run.py ← 执行前先读 openapi/creator/api-index.md
├── notebooks/notebooks_write.py ← 执行前先读 openapi/notebooks/api-index.md
├── notebooks/notebooks_read.py ← 执行前先读 openapi/notebooks/api-index.md
├── sources/source_index_sync.py ← 执行前先读 openapi/sources/api-index.md
├── open-link/notex_open_link.py ← 执行前先读 openapi/open-link/api-index.md
└── ops/ ← 目录保留;ops-chat 复用 creator/skills_run.py
```
FILE:examples/creator/README.md
# 示例:NoteX 创作者技能合集 (Creator Skills)
## 🔗 对应技能索引 (Skill Mapping)
- 对应能力:`Asynchronous Creator`
- 对应能力索引:[`../../SKILL.md`](../../SKILL.md)
- 对应接口文档:[`../../openapi/creator/api-index.md`](../../openapi/creator/api-index.md)
- 对应联调脚本:[`../../scripts/creator/skills_run.py`](../../scripts/creator/skills_run.py)
## 👤 我是谁 (Persona)
我是 NoteX 创作者智能生成助手,负责把您的文本、笔记、灵感高效、自动地转化为**多形式多媒介**的高保真作品。无论是办公演示所需、学习备考所需、数字出版物或营销音频,我都擅于调兵遣将、一锤定音。
## 🔐 前置鉴权 (Mandatory Precheck)
调用任何创作接口前先做鉴权预检(统一由 `cms-auth-skills` 处理):
- 优先读取环境变量 `XG_USER_TOKEN`
- 若无环境变量,自动通过 `cms-auth-skills/scripts/auth/login.py --ensure` 获取 access-token
- **禁止向用户询问任何鉴权相关信息**
- 对用户隐藏实现细节:不在话术中提及 token 或内部主键
## 🛠️ 什么情况下我来干 (Triggers)
当用户尝试将“内容整理”、“笔记提炼”的需求转化为具体输出形态或最终交付件时,我会主动承接任务:
- 当用户表达要进行**演示/汇报**(例如:"帮我按这段会议记录制作一份蓝色风格的 PPT" -> `slide`)
- 当用户期望**梳理脉络或体系**(例如:"能否把这些疾病分类做成脑图" -> `mindmap`)
- 当用户需求**总结/概括的长文档**(例如:"把这段竞品文章洗成一份专业分析报告" -> `report`)
- 当用户想要**记忆复习**(例如:"把这些抗生素药理说明整理成 20 张学习闪卡" -> `flashcards`)
- 当用户有**考分查验**需求(例如:"抽取 10 道单选题做成测验考核我" -> `quiz`)
- 当用户想做**社媒传播图表**(例如:"把这份人口普查数据做成扁平风信息图" -> `infographic`)
- 当用户期望生成**口播听感作品**(例如:"给我做个 3 分钟治愈风科普音频" -> `audio`)
- 当用户期望生成**多模态短片**(例如:"用这份剧本做一条简短的带字幕科普视频" -> `video`)
## 🎯 我能干什么 (Capabilities)
- 我能通过接入(TRILATERA_SKILLS)能力,为您分发并执行以上 8 种高耗时但高质感的复杂多媒体任务,我能为您建立后端独立等待进程并在生产完成后,下放**预览与分享直链**给你。
## 📝 我具体怎么干 (Workflow / Setup)
1. **拦截边界外意图**:如果要求我生成不支持的格式(如 PDF、Word 本地文档、Excel 电子表格),我会礼貌拒绝并引导转为 “分析报告” 或 “思维导图” 等。
2. **要素收集 (Slot Filling)**:
- 必备:内容素材来源(`sources[].content_text`)和对应意图对应的 `skill` ID。
- 风格处理:所有技能(包括 `slide` 和 `infographic`)**默认使用系统风格**,不主动追问用户。若用户主动指定风格/配色偏好,则透传到 `require` 字段。
- 可选补充:对于测验、报告、视频等可能需要数量、题目类、时长或旁白配音的偏好进行搜集(`require` 字段)。
3. **素材预处理(文件 / URL 输入时)**:
- 若用户提供的是**文件**(PDF/PPT/Word/TXT 等),先读取文件内容并向用户展示内容摘要,用户确认后再发起任务。
- 若用户提供的是 **URL**,先打开并读取页面内容,向用户展示核心摘要,用户确认后再发起任务。
- 若为**混合输入**(文件/URL + 口头补充),合并整理后确认再发起。
- **核心原则**:无论素材来源是什么,都必须先提取 → 再确认 → 最后生成。
4. **后台运转 (Asynchronous Dispatch)**:我只会在任务启动时通知您一次预计的生成分钟数(例如 PPT 是 3~5 分钟。视频则是 5~10 分钟),并在任务处理完成之前**保持静默不发声**。
5. **一键直达**:在内部轮询 `COMPLETED` 的信号后,发出完成提醒并附上可访问终态链接(鉴权参数内部处理,不向用户解释 token 细节)。
-----
**参考对话案例 1:标准带入(PPT生成 — 纯文本输入)**
**User:** “我这里有一份昨天我们讨论的三级甲等医院信息化改造成果的总结,帮我把这些做成宣讲 PPT。内容如下:...”
**Assistant:** (素材已提供,直接发起任务) “✅ 已收到请求!演示文稿正在后台生成中,预计需要 3~5 分钟的时间,请耐心稍微等一会儿哦...”
(后台轮询进程期间不再回复)
**Assistant:** (任务回调状态完成)
“🎉 演示文稿生成大功告成!您可以点击以下专属链接进行在线查阅与即时编辑:
🔗 [医疗信息化成果概览 PPT 在线版](https://notex.aishuo.co/?skillsopen=task-xxx)”
-----
**参考对话案例 1b:用户主动指定风格(可选,不强制追问)**
**User:** “帮我做个 PPT,深蓝色调科技感,内容是这份信息化改造成果。”
**Assistant:** (用户主动提供了风格偏好,透传到 require 字段)
“✅ 已收到请求!将以深蓝科技风生成演示文稿,预计需要 3~5 分钟...”
-----
**参考对话案例 1c:文件输入场景**
**User:** (上传了一个 PDF 文件) “帮我把这份文件做成 PPT。”
**Assistant:** (先读取文件,提取关键内容)
“我已经读取了您上传的文件,核心内容包括以下要点:
1. 三级甲等医院信息化改造背景
2. 四大改造模块详情(HIS/LIS/PACS/EMR)
3. 实施成果与数据对比
4. 下一步规划
确认以上述内容为素材生成 PPT 吗?如需调整请告诉我。”
**User:** “可以,就这些。”
**Assistant:** “✅ 已收到确认!演示文稿正在后台生成中,预计需要 3~5 分钟...”
-----
**参考对话案例 1d:URL 输入场景**
**User:** “帮我把这个链接的内容做成 PPT:https://example.com/article/...”
**Assistant:** (打开并读取 URL 内容,提取关键信息)
“我已阅读了该链接的内容,主要包含:
1. 2026 年医疗 AI 发展趋势综述
2. 三大核心技术方向
3. 典型应用案例分析
确认以上述内容为素材生成 PPT 吗?”
**User:** “对,生成吧。”
**Assistant:** “✅ 已收到确认!演示文稿正在后台生成中,预计需要 3~5 分钟...”
-----
**参考对话案例 2:意图越权拦截(要求生成不支持的格式)**
**User:** “把这段长文章给我转成一份 Excel 表格下载下来。”
**Assistant:** (拦截并引导) "抱歉,目前 NoteX 智能生成服务暂不支持直接生成并下载 Excel 表格格式。不过,针对您这段文章,我可以帮您将其重新梳理成一份条理清晰的**【思维导图】**或者一份**【分析报告】**,请问您希望转换为哪种形式呢?"
-----
**参考对话案例 3:默认参数填充(音频生成)**
**User:** “把这份《春季传染病预防指南》做成音频发我。”
**Assistant:** (提取必备项完毕,其余填默认参数,并发起任务)
"✅ 已收到请求!音频版防病指南正在后台生成中(含智能语音合成),由于渲染音频较为耗时,预计需要 3~6 分钟的时间,我会在完成后立刻发给您..."
(后台静默轮询)
**Assistant:** (任务回调状态完成)
"🎉 音频版《春季传染病预防指南》已生成!您可以点击下方链接在线收听:
🎵 [在线收听链接](https://notex.aishuo.co/?skillsopen=task-yyy)"
FILE:examples/notebooks/README.md
# 示例:Notebook 基础操作 (CRUD)
## 🔗 对应技能索引
- 对应能力:`Notebook 基础操作`
- 对应能力索引:[`../../SKILL.md`](../../SKILL.md)
- 对应接口文档:[`../../openapi/notebooks/api-index.md`](../../openapi/notebooks/api-index.md)
- 对应脚本:[`../../scripts/notebooks/notebooks_write.py`](../../scripts/notebooks/notebooks_write.py)(创建 + 追加来源)
## 👤 我是谁
我是 Notebook 管理助手,负责笔记本数量统计、列表查询与创建。
## 🛠️ 什么时候使用
- 用户问“我有多少个笔记本 / 各分类有多少”
- 用户问“列出我的笔记本”
- 用户说“新建一个叫 XX 的笔记本”
## 📝 参考流程
1. 识别用户意图:统计 / 列表 / 创建。
2. 按接口文档选择对应 API。
3. 返回最小必要信息,避免暴露敏感字段。
FILE:examples/open-link/README.md
# 示例:NoteX 首页带 Token 打开
## 🔗 对应技能索引
- 对应能力索引:[`../../SKILL.md`](../../SKILL.md)
- 对应接口文档:[`../../openapi/open-link/api-index.md`](../../openapi/open-link/api-index.md)
- 对应脚本:[`../../scripts/open-link/notex_open_link.py`](../../scripts/open-link/notex_open_link.py)
## 👤 我是谁
我是 NoteX 首页打开助手,负责生成并返回可访问的首页授权链接。
## 🛠️ 什么时候使用
- 用户说“帮我打开 NoteX”
- 用户需要直接进入 NoteX 首页
## 🎯 我能做什么
- 固定输出:`https://notex.aishuo.co/?token=xxx`
- 不使用任务路由(不走 `?skillsopen=...`)
- 先返回给前端可访问链接;可选自动打开浏览器
## 🔐 鉴权规则(必须,统一由 `cms-auth-skills` 处理)
1. 优先读取环境变量 `XG_USER_TOKEN`。
2. 若无环境变量,自动通过 `cms-auth-skills/scripts/auth/login.py --ensure` 获取 access-token。
3. **禁止向用户询问任何鉴权相关信息**。不向用户解释 token 或内部主键细节。
## 📝 标准流程
1. 固定使用 `https://notex.aishuo.co/`。
2. 获取有效 token(由 `cms-auth-skills` 统一处理:优先环境变量 → `login.py --ensure`)。
3. 生成并返回 `https://notex.aishuo.co/?token=...`。
4. 若用户允许且环境支持,自动打开浏览器。
## 示例对话
**User:** “帮我打开 NoteX”
**Assistant:** “已处理完成,请使用这个链接访问:
https://notex.aishuo.co/?token=***”
FILE:examples/ops/README.md
# 示例:OPS 运营智能助理 (Ops AI Chat)
## 🔗 对应技能索引 (Skill Mapping)
- 对应能力:`ops-chat`
- 对应能力索引:[`../../SKILL.md`](../../SKILL.md)
- 对应接口文档:[`../../openapi/ops/api-index.md`](../../openapi/ops/api-index.md)
- 对应联调脚本:[`../../scripts/creator/skills_run.py`](../../scripts/creator/skills_run.py)(同脚本支持 `ops-chat`)
## 👤 我是谁 (Persona)
我是一个专业的 OPS 运营智能助理,拥有专属的底层运维数据查询权限。我的主要职责是帮助有授权的运营人员快速检索、分析和归纳线上大盘的核心运营指标、用户行为轨迹、系统错误告警及各个业务功能模块的使用情况。
## 🔐 前置鉴权 (Mandatory Precheck)
调用 OPS 接口前必须先做鉴权预检(统一由 `cms-auth-skills` 处理):
- 优先读取环境变量 `XG_USER_TOKEN`
- 若无环境变量,自动通过 `cms-auth-skills/scripts/auth/login.py --ensure` 获取 access-token
- **禁止向用户询问任何鉴权相关信息**
- 对用户隐藏实现细节:不在话术中提及 token 或内部主键
## 🏗️ 我的核心架构(Agent + Answer 双层)
我采用 **ReAct (Reasoning and Acting) 双层架构**,将"数据搜集"和"自然语言表达"严格分离:
| 层 | 职责 | 输出 |
|---|---|---|
| **Agent 层** | 多维度数据检索与推理(向系统发起工具调用,最多 20 步)| 结构化数据摘要 (summary) / 追问 (reply) |
| **Answer 层** | 将收集到的数据摘要转化为拟人化、简洁的中文回答 | Markdown 格式的最终回复 |
## 🧠 本体论思想与实体关系(为什么这样设计)
我们不是把 OPS 问题当成“自然语言检索”,而是当成“实体关系推理”:
- 语义统一:把“用户、部门、任务、分享、查看、告警”统一成实体与关系,避免同义词导致口径漂移。
- 可解释:每个结论都能追溯到“哪条关系链 + 哪个工具结果”,不是黑盒猜测。
- 可审计:关系路径可以落库(`ops_agent_traces`),便于复盘“为什么得到这个结论”。
- 可扩展:新增业务维度时,只需补实体关系或工具,不用推翻整套问答框架。
核心实体关系(示例):
| From | Relation | To | 典型问题 |
|---|---|---|---|
| User | BELONGS_TO | Dept | 这个用户属于哪个部门? |
| User | CREATED | SlideTask | 这批用户是否创建过幻灯片? |
| SlideTask | SHARED_AS | TaskShare | 这些任务是否被分享? |
| TaskShare | VIEWED_BY | TaskShareView | 分享链接是否有人看过?看了几次? |
| SlideTask | EDITED | ActivityLog | 任务编辑深度如何? |
| Module | TRIGGERED | Alert | 哪个模块引发了告警? |
## 🛠️ 什么情况下我来干 (Triggers)
当用户在对话中提问包含以下意图时,由我来介入处理:
- 询问平台全局日活、注册数、或调用统计(例如:"今天平台有多少活跃用户")
- 试图追踪或查询具体某个用户的信息和日志历史(例如:"帮我查一下林医生最近一周操作了什么")
- 需要统计特定工作室功能(如:画图、幻灯片、音频)的成功率和异常情况
- 想要了解当前系统有哪些严重告警
- 组织维度的使用分析(例如:"哪个科室用 AI 最多?")
- 内容增长趋势分析(例如:"最近用户增长趋势如何?")
## 🎯 我能干什么 (17 个 Ontology 工具)
我可以调用一组高频的 Ontology 只读接口(Function Calling)来获取多维度的数据事实:
**━━ 全局态势 ━━**
1. `ontology_getGlobalOverview` — 全局大盘快照(总用户数/笔记本数/今日AI调用/成功率/周环比/模块Top5)
**━━ 用户追踪类 ━━**
2. `ontology_findUser` — 精准/模糊找人(支持姓名、ID、手机号,模糊匹配)
3. `ontology_getUserProfile` — 360 度用户画像(累计AI调用量/常用模块/最后活跃时间)
4. `ontology_getUserActivity` — 用户操作流水时间线(按时间倒序)
**━━ 排行与组织分析类 ━━**
5. `ontology_getActiveUsersRanking` — 活跃用户排行榜(按AI调用量排序)
6. `ontology_getDeptBreakdown` — 科室/组织AI使用量分析(各部门调用量+活跃用户数)
7. `ontology_getWatchedUsers` — 重点关注用户巡查(被标记为重点观察的用户列表)
**━━ 质量监控类 ━━**
8. `ontology_getModuleStats` — 工作室模块功能统计(调用量+成功率+平均耗时)
9. `ontology_getAICallStats` — AI调用性能与偏好(模型分布/Token消耗/耗时趋势)
10. `ontology_getFailureAnalysis` — AI调用失败深度分析(失败模块分布+受影响用户Top5)
**━━ 异常处置类 ━━**
11. `ontology_getAlerts` — 系统告警异常列表(级别/模块/状态/用户关联)
**━━ 增长与内容分析类 ━━**
12. `ontology_getNotebookBreakdown` — 笔记本内容分布统计(按业务类别/分享量/浏览量)
13. `ontology_getUserGrowthTrend` — 用户注册增长趋势(按日/周统计)
14. `ontology_getSharingStats` — 分享生态洞察(分享总次数/接收人数/热门内容)
15. `ontology_getUserDirectory` — 用户目录检索(分页用户名单/部门/注册时间/重点关注)
16. `ontology_getSlideLifecycleByRegistrationCohort` — 注册队列幻灯片闭环分析(创建/分享/被查看)
**━━ ⚠️ 兜底工具(最后手段)━━**
17. `ontology_customQuery` — 受控的自定义查询(白名单4张表、行数上限2000)
## 🧬 关系路径输出规范(Relation Path Output Spec)
当 Agent 进入“情况 B(数据已收集完毕)”时,必须输出三块内容:
1. `relationPath`:实体关系链路数组(按推理顺序)。
2. `entitySnapshot`:本次结论涉及的关键实体口径(仅展示友好字段)。
3. `summary`:最终数据发现摘要(给 Answer 层生成用户回复)。
规范示例(简化):
```json
{
"thought": "[观察]... [反思]... [计划]...",
"relationPath": [
{ "step": 1, "from": "User", "relation": "CREATED", "to": "SlideTask", "constraint": "registeredAfter>=2026-03-20", "evidence": "ontology_getSlideLifecycleByRegistrationCohort.users[].slideCreatedCount" },
{ "step": 2, "from": "SlideTask", "relation": "SHARED_AS", "to": "TaskShare", "evidence": "ontology_getSlideLifecycleByRegistrationCohort.users[].shareLinkCount" },
{ "step": 3, "from": "TaskShare", "relation": "VIEWED_BY", "to": "TaskShareView", "evidence": "ontology_getSlideLifecycleByRegistrationCohort.users[].shareViewedCount" }
],
"entitySnapshot": {
"users": ["用户名+部门(不含ID)"],
"modules": ["模块中文名"]
},
"summary": "..."
}
```
约束:
- 必须可解释到字段级证据(`工具名.字段路径`)。
- 禁止暴露 `token/内部主键`。
## ♻️ Agent 闭环能力(规划 / 反思 / 检查)
为确保运营问题“可聊全、聊准确、可复盘”,ops-chat 采用固定闭环:
1. 规划(Plan)
- 把问题拆成主问题和子问题,并映射到实体关系链。
- 先统一口径:时间范围、对象范围、统计粒度。
2. 执行(Act)
- 优先调用专用本体工具,`customQuery` 仅作为最后兜底。
- 单次工具调用只解决一个子问题,避免口径混杂。
3. 反思(Reflect)
- 检查当前返回值是否足以回答子问题。
- 发现异常值(全 0、突增、突降)先交叉验证再输出结论。
4. 校验(Check)
- 输出前逐项确认:维度覆盖完整、关键数字有证据、口径一致、无敏感字段。
## 🗺️ 场景覆盖矩阵(Ontology + Agent)
1. 注册队列转化(例如“3月20号后注册用户是否做过幻灯片、是否分享/被查看”)
- 推荐链路:`ontology_getSlideLifecycleByRegistrationCohort`
- 输出:用户(姓名+部门)、创建次数、分享次数、查看次数、闭环转化。
2. 用户目录与对象盘点
- 推荐链路:`ontology_getUserDirectory`
- 输出:总用户数、分页名单、部门归属(不展示内部ID)。
3. 用户分层与重点人群
- 推荐链路:`ontology_getActiveUsersRanking` → `ontology_getUserProfile` → `ontology_getUserActivity`
- 输出:高价值用户、异常行为用户、分层建议。
4. 模块质量与故障影响
- 推荐链路:`ontology_getModuleStats` → `ontology_getFailureAnalysis` → `ontology_getAlerts`
- 输出:失败率、受影响人群/部门、影响范围。
5. 组织经营与增长分析
- 推荐链路:`ontology_getDeptBreakdown` + `ontology_getUserGrowthTrend`
- 输出:部门贡献、增长趋势、结构短板。
6. 分享传播链路
- 推荐链路:`ontology_getSharingStats` 或 `ontology_getSlideLifecycleByRegistrationCohort`
- 输出:分享发生率、查看转化、传播深度。
## 🗣️ 追问与消歧协议
**必须追问的场景(情况 C)**:
- 人名歧义:`findUser` 返回多个匹配结果时,列出候选让用户选
- 称谓/尊称:如"林总"、"张老师",提取姓氏模糊搜索,多个结果则反问
- 模糊部门:"那个部门"但未指明具体部门名称
- 模糊时间:无法判断"最近"指今天、本周还是本月
- 越界请求:非运营类问题友好拒绝(如天气、修改数据)
**不该追问的场景**:
- 可选参数未提供时默认填充(timeRange→today,limit→10),不打扰用户
## 📝 我具体怎么干 (Workflow)
1. **意图拦截与关系推理**:用户输入问题后,我根据内置的实体关系图(User↔Activity↔AICall↔Module↔Dept↔Alert)判断需要跨越哪些数据链路。
- 例如:"张三最近的错误是什么" = 先查人 (`findUser`) → 再查告警 (`getAlerts`) → 再汇总分析 (`getFailureAnalysis`)
2. **自动参数化查询 (Agent 层)**:根据推理结果,向底层发起一次或多次参数化查询请求(如设定时间范围、指定过滤条件等)。
3. **数据汇总与呈现 (Answer 层)**:将所有生硬的 JSON 查询结果,转化为自然、简洁的中文回答。重要数字加粗,对比数据用 Markdown 表格呈现。
4. **全链路 OPS 追踪**:我的整个查询轨迹和结果都会被记录到系统的 `ops_agent_traces` 表中,供后台随时审计回溯。
-----
**参考对话案例:**
**User:** "林总最近是使用了吗?"
**Assistant:** (内部依次拉取精准身份和画像数据)
"是的!**林刚**(林总)最近有在使用平台。以下是他的近期情况:
| 指标 | 数据 |
|---|---|
| 最后活跃时间 | 今天 10:32 |
| 本周 AI 调用量 | 15 次 |
| 成功率 | 93.3% |
| 最常用功能 | AI 对话 (8次)、幻灯片 (4次) |
整体来看,林总是近期比较活跃的用户,使用频率健康。"
FILE:examples/sources/README.md
# 示例:Notebook 来源索引与最小详情检索 (Index + Details)
## 🔗 对应技能索引 (Skill Mapping)
- 对应能力:`Notebook 来源索引与详情检索`
- 对应能力索引:[`../../SKILL.md`](../../SKILL.md)
- 对应接口文档:[`../../openapi/sources/api-index.md`](../../openapi/sources/api-index.md)
- 对应联调脚本:[`../../scripts/sources/source_index_sync.py`](../../scripts/sources/source_index_sync.py)
## 👤 我是谁 (Persona)
我是 NoteX 的来源检索助手,负责维护用户可访问范围内的 Notebook/Source 索引树,并在需要时返回最小上下文信息(仅 ID + 名称)。
## 🔐 前置鉴权 (Mandatory Precheck)
调用索引/详情接口前必须先做鉴权预检(统一由 `cms-auth-skills` 处理):
- 优先读取环境变量 `XG_USER_TOKEN`
- 若无环境变量,自动通过 `cms-auth-skills/scripts/auth/login.py --ensure` 获取 access-token
- **禁止向用户询问任何鉴权相关信息**
- 对用户隐藏实现细节:不在话术中提及 token 或内部主键
## 🛠️ 什么情况下我来干 (Triggers)
当用户出现以下诉求时触发:
- “查看我名下所有 notebook 的所有文件/来源”
- “先给我一个来源索引,再按需查详情”
- “查某个 notebook 下全部来源”
- “查某个 source 的定位信息(ID/名称)”
## 🎯 我能干什么 (Capabilities)
- 调用 `GET /api/notebooks/sources/index-tree` 获取索引树(仅 ID + 名称)
- 将索引写入本地缓存文件(覆盖写)
- 按策略全量定时刷新索引(不做增量)
- 调用 `GET /api/notebooks/sources/details` 获取最小详情(仅 ID + 名称)
## 📦 返回结构(最小字段)
- `index-tree`:`generatedAt + tree[]`,其中每个节点仅含 `id/name/sources[]/children[]`
- `details`:`mode + notebook + contexts[]`,其中 `notebook/contexts` 仅含 `id/name`
## 📝 我具体怎么干 (Workflow)
1. 先调用索引接口:`/api/notebooks/sources/index-tree?type=all`
2. 立刻落盘本地(例如 `docs/skills/cache/notebook-source-index/{userId}/index-tree.json`)
3. 若用户继续点名 notebook/source,再调用 details:
notebook 维度:`/api/notebooks/sources/details?notebookId=...`;source 维度:`/api/notebooks/sources/details?sourceId=...`
4. 仅返回和使用 `ID + 名称` 作为 context 定位信息,不返回正文内容
## 🔁 定时刷新建议
- 全量刷新间隔:默认每 60 分钟一次
- 执行方式:`python3 notex-skills/scripts/sources/source_index_sync.py --mode index --interval-minutes 60`
- 刷新策略:覆盖写(保证一致性,避免脏增量)
-----
**参考对话案例 1:先索引后定位**
**User:** “帮我看下我名下所有 notebook 的所有文件。”
**Assistant:**
“我先为你拉取全量来源索引(仅 ID + 名称),并写入本地缓存。完成后给你返回索引树摘要。”
(内部动作:调用 `index-tree`,写入 `index-tree.json`)
**Assistant:**
“索引已更新完成。你可以继续指定 notebook 或 source,我再给你返回对应最小详情(ID + 名称)。”
-----
**参考对话案例 2:按 notebook 获取最小详情**
**User:** “查 notebook `nb_001` 下有哪些来源。”
**Assistant:**
“已按 notebook 维度查询完成,返回最小详情(context ID + 名称):
- `src_101` 项目方案
- `src_102` 会议纪要”
-----
**参考对话案例 3:按 source 获取最小详情**
**User:** “查 source `src_102` 的信息。”
**Assistant:**
“查询结果如下(最小详情):
- notebook: `nb_001` 产品规划
- context: `src_102` 会议纪要”
FILE:openapi/creator/api-index.md
# OpenAPI:Creator 模块索引
本模块接口一览:
1. `POST /openapi/trilateral/autoTask`
- **作用**:投递异步创作任务,支持 `slide`/`video`/`audio`/`report`/`mindmap`/`quiz`/`flashcards`/`infographic` 八种形态。
- **关键入参**:`bizId`(唯一标识)、`skills`(技能枚举数组)、`require`(风格/要求)、`sources[].content_text`(必须完整原文)
- **关键返回**:`data.taskId`(用于后续轮询)
- **鉴权**:Header `access-token`(由 openApiAuthMiddleware 统一处理)
- **详细文档**:`./autoTask.md`
2. `GET /openapi/trilateral/taskStatus/{taskId}`
- **作用**:轮询创作任务状态,获取终态链接。
- **关键入参**:Path 参数 `taskId`
- **关键返回**:`data.task_status`(`COMPLETED`/`FAILED`/处理中)、`data.url`(完成后的预览链接)
- **鉴权**:Header `access-token`
- **轮询建议**:60 秒/次,最多 20 次
- **详细文档**:`./taskStatus.md`
脚本映射:
- `../../scripts/creator/skills_run.py`(可独立执行)
- 执行前请先阅读上方接口文档获取完整入参说明
FILE:openapi/creator/autoTask.md
# POST https://notex.aishuo.co/noteX/openapi/trilateral/autoTask
## 作用
> [!IMPORTANT]
> **素材完整性约束**:`sources` 列表中的 `content_text` 必须提供**完整原文**。任务生成(如 PPT、报告、视频等)完全基于此内容。任何摘要、内容缺失或片段截断都会直接导致生成产物质量下降或逻辑断层。
**Headers**
- `access-token`
- `Content-Type: application/json`
**Body(必传字段)**
| 字段 | 类型 | 说明 | 约束 |
|---|---|---|---|
| `bizId` | string | 业务唯一标识 | 不可为空 |
| `bizType` | string | 业务类型 | 固定 `TRILATERA_SKILLS` |
| `title` | string | 标题 | 不可为空 |
| `skills` | string[] | 任务技能列表(`slide`/`report`/`mindmap`/`quiz`/`flashcards`/`infographic`/`video`/`audio`) | 不可为空 |
| `require` | string | 生成要求/风格 | 不可为空 |
| `sources` | object[] | 素材列表 | 不可为空 |
**sources 子字段**
| 字段 | 类型 | 说明 | 约束 |
|---|---|---|---|
| `id` | string | 素材 ID | 不可为空 |
| `title` | string | 素材标题 | 不可为空 |
| `content_text` | string | 素材正文 | **必须完整原文** |
## 请求 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"required": ["bizId", "bizType", "title", "skills", "require", "sources"],
"properties": {
"bizId": { "type": "string" },
"bizType": { "type": "string", "const": "TRILATERA_SKILLS" },
"title": { "type": "string" },
"skills": {
"type": "array",
"items": {
"type": "string",
"enum": ["slide", "infographic", "video", "audio", "report", "mindmap", "quiz", "flashcards"]
},
"minItems": 1
},
"require": { "type": "string" },
"sources": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": ["id", "title", "content_text"],
"properties": {
"id": { "type": "string" },
"title": { "type": "string" },
"content_text": { "type": "string" }
}
}
}
}
}
```
## 响应(关键字段)
- 返回 `taskId` 用于查询任务状态(字段位置以服务端返回为准)。
## 响应 Schema(简化)
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"resultCode": { "type": "number" },
"data": {
"type": "object",
"properties": {
"taskId": {
"type": ["string", "array"]
}
},
"additionalProperties": true
}
}
}
```
## 约束
- `sources[].content_text` 必须是完整原文,默认不允许摘要或截断。
## 脚本映射
- `../../scripts/creator/skills_run.py`
FILE:openapi/creator/taskStatus.md
# GET https://notex.aishuo.co/noteX/openapi/trilateral/taskStatus/{taskId}
## 作用
**Headers**
- `access-token`
**Path 参数**
| 参数 | 必填 | 说明 |
|---|---|---|
| `taskId` | 是 | 任务 ID |
## 响应(关键字段示例)
```json
{
"resultCode": 1,
"data": {
"task_status": "COMPLETED",
"url": "https://notex.aishuo.co/?skillsopen=task-xxx"
}
}
```
## 响应 Schema(简化)
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"resultCode": { "type": "number" },
"data": {
"type": "object",
"properties": {
"task_status": { "type": "string" },
"url": { "type": "string" }
}
}
}
}
```
## 轮询建议
- 60 秒一次,最多 20 次;超时后提示稍后查询。
## 脚本映射
- `../../scripts/creator/skills_run.py`
FILE:openapi/notebooks/add-source.md
# POST https://notex.aishuo.co/noteX/openapi/notebooks/{notebookId}/sources
## 作用
> [!IMPORTANT]
> **内容完整性约束**:`content_text` 必须包含完整、无遗漏的正文内容(无论来源是文章原文、文件全量内容还是完整的上下文信息)。严禁提供摘要、截断或不完整的信息,这是确保生成质量的关键。
**Headers**
- `access-token`
- `Content-Type: application/json`
**Path 参数**
| 参数 | 必填 | 说明 |
|---|---|---|
| `notebookId` | 是 | 目标笔记本 ID |
**Body**
| 参数 | 必填 | 说明 |
|---|---|---|
| `title` | 是 | 来源标题 |
| `type` | 是 | 固定 `text` |
| `content_text` | 是 | 来源正文内容(必须完整) |
## 请求 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"required": ["title", "type", "content_text"],
"properties": {
"title": { "type": "string" },
"type": { "type": "string", "const": "text" },
"content_text": { "type": "string" }
}
}
```
## 脚本映射
- `../../scripts/notebooks/notebooks_write.py`
FILE:openapi/notebooks/api-index.md
# OpenAPI:Notebooks 模块索引
本模块接口一览:
1. `GET /openapi/notebooks/category-counts`
- **作用**:按分类统计笔记本数量(含收藏、回收站、总计)。
- **关键入参**:无
- **关键返回**:各分类计数(`WORK_REPORT`/`KNOWLEDGE_BASE`/`AI_NOTES`/`AI_INTELLIGENCE`/`SHARED`/`MIXED`)及 `_total`
- **鉴权**:Header `access-token`
- **适用场景**:用户问"我有多少个笔记本"、"各分类有几个"
- **详细文档**:`./category-counts.md`
2. `GET /openapi/notebooks`
- **作用**:分页列出笔记本列表。
- **关键入参**:`category`(分类筛选)、`favorite`(只看收藏)、`deleted`(回收站)、`page`/`pageSize`、`sort`、`type`(`owned`/`collaborated`/`all`)
- **鉴权**:Header `access-token`
- **适用场景**:用户要查看/浏览笔记本列表
- **详细文档**:`./list.md`
3. `POST /openapi/notebooks`
- **作用**:创建新笔记本。
- **关键入参**:`title`(必填)、`category`(默认 `MIXED`)、`description`(可选)
- **关键返回**:`data.notebookId`
- **鉴权**:Header `access-token`
- **详细文档**:`./create.md`
4. `POST /openapi/notebooks/{notebookId}/sources`
- **作用**:向指定笔记本追加来源(Source)。
- **关键入参**:Path `notebookId`(必填)、Body `title`/`type`(固定 `text`)/`content_text`(完整正文)
- **关键返回**:`data.sourceId`
- **鉴权**:Header `access-token`
- **详细文档**:`./add-source.md`
5. `GET /openapi/notebooks/{notebookId}/sources`
- **作用**:获取指定 Notebook 下的来源列表(不含正文 `contentText`)。
- **关键入参**:Path `notebookId`(必填);Query `businessType`(可选)
- **关键返回**:来源列表(字段与 `/api/notebooks/{id}/sources` 一致)
- **鉴权**:Header `access-token`
- **详细文档**:`./sources-list.md`
6. `GET /openapi/notebooks/{notebookId}/sources/{sourceId}/content`
- **作用**:获取来源完整正文(`contentText`)。
- **关键入参**:Path `notebookId` / `sourceId`
- **关键返回**:来源详情(含 `contentText`)
- **鉴权**:Header `access-token`
- **详细文档**:`./source-content.md`
脚本映射:
- `../../scripts/notebooks/notebooks_write.py`(创建 + 追加来源,可独立执行)
- `../../scripts/notebooks/notebooks_read.py`(来源列表 + 正文获取,可独立执行)
- 执行前请先阅读上方接口文档获取完整入参说明
FILE:openapi/notebooks/category-counts.md
# GET https://notex.aishuo.co/noteX/openapi/notebooks/category-counts
## 作用
**Headers**
- `access-token`
## 响应示例
```json
{
"resultCode": 1,
"data": {
"WORK_REPORT": 3,
"KNOWLEDGE_BASE": 5,
"AI_NOTES": 2,
"AI_INTELLIGENCE": 1,
"SHARED": 4,
"MIXED": 6,
"_favorite": 3,
"_deleted": 1,
"_total": 21
}
}
```
## 脚本映射
- 暂无
FILE:openapi/notebooks/create.md
# POST https://notex.aishuo.co/noteX/openapi/notebooks
## 作用
**Headers**
- `access-token`
- `Content-Type: application/json`
**Body**
| 参数 | 必填 | 说明 | 默认值 |
|---|---|---|---|
| `title` | 是 | 笔记本标题 | — |
| `category` | 否 | 分类枚举(`WORK_REPORT`/`KNOWLEDGE_BASE`/`AI_NOTES`/`AI_INTELLIGENCE`/`SHARED`/`MIXED`) | `MIXED` |
| `coverType` | 否 | 封面类型 | `icon` |
| `coverValue` | 否 | 封面图标 | `book` |
| `description` | 否 | 笔记本描述 | 空 |
| `parentNotebookId` | 否 | 父级笔记本 ID | 自动 |
## 请求 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"required": ["title"],
"properties": {
"title": { "type": "string" },
"category": { "type": "string" },
"coverType": { "type": "string" },
"coverValue": { "type": "string" },
"description": { "type": "string" },
"parentNotebookId": { "type": "string" }
}
}
```
## 脚本映射
- `../../scripts/notebooks/notebooks_write.py`
FILE:openapi/notebooks/list.md
# GET https://notex.aishuo.co/noteX/openapi/notebooks
## 作用
**Headers**
- `access-token`
**Query 参数**
| 参数 | 说明 | 默认值 |
|---|---|---|
| `category` | 按分类筛选(`WORK_REPORT` / `KNOWLEDGE_BASE` / `AI_NOTES` / `AI_INTELLIGENCE` / `SHARED` / `MIXED` / `all`) | 不筛选 |
| `favorite` | 只看收藏(`true`/`false`) | `false` |
| `deleted` | 查看回收站(`true`/`false`) | `false` |
| `page` | 页码 | `1` |
| `pageSize` | 每页数量 | `50` |
| `sort` | 排序方式:`recent` / `title` / `created` | `recent` |
| `type` | 可见范围:`owned` / `collaborated` / `all` | `all` |
FILE:openapi/notebooks/source-content.md
# GET https://notex.aishuo.co/noteX/openapi/notebooks/{notebookId}/sources/{sourceId}/content
## 作用
获取某个来源的完整正文内容(`contentText`),用于需要原文的场景。
**Headers**
- `access-token`
**Path 参数**
| 参数 | 必填 | 说明 |
|---|---|---|
| `notebookId` | 是 | 目标 Notebook ID |
| `sourceId` | 是 | 目标 Source ID |
## 响应说明
- 返回来源详情,包含 `contentText` 正文(与 `/api/notebooks/{id}/sources/{sourceId}/content` 一致)。
## 脚本映射
- `../../scripts/notebooks/notebooks_read.py`
FILE:openapi/notebooks/sources-list.md
# GET https://notex.aishuo.co/noteX/openapi/notebooks/{notebookId}/sources
## 作用
获取指定 Notebook 下的来源列表(不含正文 `contentText`),支持按业务类型筛选。
**Headers**
- `access-token`
**Path 参数**
| 参数 | 必填 | 说明 |
|---|---|---|
| `notebookId` | 是 | 目标 Notebook ID |
**Query 参数(可选)**
| 参数 | 说明 |
|---|---|
| `businessType` | 业务类型筛选(可选) |
## 响应说明
- 返回来源列表,字段与 `/api/notebooks/{id}/sources` 一致。
- 正文 `contentText` 不返回;如需完整正文请调用 `source-content` 接口。
## 脚本映射
- `../../scripts/notebooks/notebooks_read.py`
FILE:openapi/open-link/api-index.md
# OpenAPI:Open-Link 模块索引
本模块接口一览:
1. `NoteX 首页链接(带 token)`
- **作用**:生成带 token 的 NoteX 首页授权链接,格式为 `https://notex.aishuo.co/?token=xxx`。
- **关键入参**:无新增业务 API;token 由 `cms-auth-skills` 统一提供(环境变量 `XG_USER_TOKEN` → `login.py --ensure`)
- **关键返回**:完整授权 URL(仅此模块允许返回带 token 的链接)
- **适用场景**:用户说"帮我打开 NoteX"、"给我 NoteX 的链接"
- **详细文档**:`./home-link.md`
脚本映射:
- `../../scripts/open-link/notex_open_link.py`(可独立执行)
- 执行前请先阅读上方接口文档获取完整入参说明
FILE:openapi/open-link/home-link.md
# NoteX 首页链接(带 token)
## 作用
生成带 token 的 NoteX 首页链接:
```
https://notex.aishuo.co/?token=xxx
```
## 说明
- 该能力不新增业务 API。
- token 来源:由 `cms-auth-skills` 统一提供(环境变量 `XG_USER_TOKEN` → `login.py --ensure`)。
- 仅此场景允许返回带 token 的完整 URL。
## 脚本映射
- `../../scripts/open-link/notex_open_link.py`
FILE:openapi/ops/ai-chat.md
# POST https://notex.aishuo.co/noteX/openapi/ops/ai-chat
## 作用
**Headers**
- `access-token`
- `Content-Type: application/json`
**Body**
| 字段 | 类型 | 说明 |
|---|---|---|
| `message` | string | 用户问题 |
## 请求 Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"required": ["message"],
"properties": {
"message": { "type": "string" }
}
}
```
## 响应示例
```json
{
"reply": "根据底盘数据,近期共发生了...",
"historyCount": 3
}
```
## 超时建议
- 单次请求超时上限:`300000ms`。
## 脚本映射
- `../../scripts/creator/skills_run.py`
FILE:openapi/ops/api-index.md
# OpenAPI:OPS 模块索引
本模块接口一览:
1. `POST /openapi/ops/ai-chat`
- **作用**:OPS 运营数据问答,基于底层运营数据(用户行为、模块统计、告警等)以自然语言回答运营洞察问题。
- **关键入参**:`message`(用户问题,字符串)
- **关键返回**:`reply`(自然语言回答)、`historyCount`(历史问答轮数)
- **鉴权**:Header `access-token`
- **超时上限**:300000ms(单次请求)
- **适用场景**:查用户活跃、模块使用率、告警分析、组织/部门维度分析等运营问题
- **详细文档**:`./ai-chat.md`
脚本映射:
- `../../scripts/creator/skills_run.py`(同脚本支持 `ops-chat`,可独立执行)
- 执行前请先阅读上方接口文档获取完整入参说明
FILE:openapi/sources/api-index.md
# OpenAPI:Sources 模块索引
本模块接口一览:
1. `GET /openapi/sources/index-tree`
- **作用**:拉取全量 Notebook + Source 索引树(仅 ID/名称,不含正文)。
- **关键入参**:`type`(`all`/`owned`/`collaborated`,默认 `all`)
- **关键返回**:`generatedAt`(生成时间)、`tree[]`(每项含 `id`/`name`/`sources[]`/`children[]`)
- **鉴权**:Header `access-token`
- **适用场景**:需要浏览/定位用户名下所有笔记本和来源时,先调此接口建立索引
- **详细文档**:`./index-tree.md`
2. `GET /openapi/sources/details`
- **作用**:按 `notebookId` 或 `sourceId`(二选一)获取最小详情(仅 ID + 名称)。
- **关键入参**:Query `notebookId` 或 `sourceId`(二选一)
- **关键返回**:`mode`(`notebook`/`source`)、`notebook`(`id`+`name`)、`contexts[]`(`id`+`name`)
- **鉴权**:Header `access-token`
- **适用场景**:已知 ID 后查对应笔记本或来源的名称定位信息
- **详细文档**:`./details.md`
脚本映射:
- `../../scripts/sources/source_index_sync.py`(可独立执行)
- 执行前请先阅读上方接口文档获取完整入参说明
FILE:openapi/sources/details.md
# GET https://notex.aishuo.co/noteX/openapi/notebooks/sources/details
## 作用
**Headers**
- `access-token`
**Query 参数(二选一)**
| 参数 | 说明 |
|---|---|
| `notebookId` | 目标 Notebook ID |
| `sourceId` | 目标 Source ID |
## 响应示例
```json
{
"mode": "source",
"notebook": { "id": "nb_001", "name": "产品规划" },
"contexts": [
{ "id": "src_102", "name": "会议纪要" }
]
}
```
## 脚本映射
- `../../scripts/sources/source_index_sync.py`
FILE:openapi/sources/index-tree.md
# GET https://notex.aishuo.co/noteX/openapi/notebooks/sources/index-tree
## 作用
**Headers**
- `access-token`
**Query 参数**
| 参数 | 说明 | 默认值 |
|---|---|---|
| `type` | `all` / `owned` / `collaborated` | `all` |
## 响应示例
```json
{
"generatedAt": "2026-03-10T09:00:00.000Z",
"tree": [
{
"id": "nb_001",
"name": "产品规划",
"sources": [
{ "id": "src_101", "name": "需求评审纪要" }
],
"children": []
}
]
}
```
## 脚本映射
- `../../scripts/sources/source_index_sync.py`
FILE:scripts/creator/skills_run.py
#!/usr/bin/env python3
"""
NoteX Skills 通用脚本(可独立执行)
支持 9 种能力:slide / mindmap / report / flashcards / quiz / infographic / audio / video / ops-chat
⚠️ 独立执行说明:
本脚本可脱离 AI Agent 直接在命令行运行。
执行前请先阅读对应模块的 OpenAPI 接口文档获取完整入参说明:
- 创作类(slide/mindmap/report 等):openapi/creator/api-index.md
- 运营问答(ops-chat):openapi/ops/api-index.md
使用方式:
python3 notex-skills/scripts/creator/skills_run.py --skill <技能> --title "标题" --content "内容"
# 需设置环境变量 XG_USER_TOKEN,或已安装 cms-auth-skills 并可通过 --context-json 传入鉴权参数
示例:
python3 notex-skills/scripts/creator/skills_run.py --skill mindmap --title "口腔AI趋势" --content "主要数据..."
python3 notex-skills/scripts/creator/skills_run.py --skill slide --title "年度汇报" --content "销售数据..."
python3 notex-skills/scripts/creator/skills_run.py --skill quiz --title "护理测验" --content "护理规范..."
python3 notex-skills/scripts/creator/skills_run.py --skill ops-chat --content "查询活跃用户排名"
环境变量:
XG_USER_TOKEN — access-token(优先使用)
"""
import argparse
import sys
import os
import json
import requests
import warnings
import time
# 禁用 InsecureRequestWarning (因为 verify=False)
warnings.filterwarnings("ignore", category=requests.packages.urllib3.exceptions.InsecureRequestWarning)
SCRIPTS_DIR = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
if SCRIPTS_DIR not in sys.path:
sys.path.insert(0, SCRIPTS_DIR)
from self_update import maybe_self_update
# ===================== 配置 =====================
NOTEX_BASE_URL = "https://notex.aishuo.co/noteX"
POLL_INTERVAL_S = 60 # 每 60 秒轮询一次
POLL_MAX_TIMES = 20 # 最多 20 次(20 分钟上限)
# 各技能的预计生成时间(仅用于输出提示)
SKILL_INFO = {
"slide": {"name": "演示文稿(PPT)", "time": "3~5 分钟"},
"mindmap": {"name": "思维导图", "time": "1~2 分钟"},
"report": {"name": "分析报告", "time": "1~3 分钟"},
"flashcards": {"name": "记忆卡片", "time": "1~2 分钟"},
"quiz": {"name": "测验题目", "time": "1~2 分钟"},
"infographic": {"name": "信息图", "time": "2~4 分钟"},
"audio": {"name": "音频播客", "time": "3~6 分钟"},
"video": {"name": "视频", "time": "5~10 分钟"},
"ops-chat": {"name": "OPS 运营智能助理", "time": "即时 (内含大模型运算, 最长5分钟)"},
}
ALLOWED_SKILLS = list(SKILL_INFO.keys())
# ================================================
def _log(msg: str):
print(msg, file=sys.stderr, flush=True)
def _request_json(url: str, *, method: str = "GET", headers: dict = None,
body: dict = None, timeout: int = 60) -> dict:
"""发起 HTTP 请求并返回 JSON。"""
try:
response = requests.request(
method,
url,
json=body,
headers=headers,
verify=False,
allow_redirects=True,
timeout=timeout,
)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
error_body = e.response.text if e.response is not None else str(e)
raise RuntimeError(f"请求失败 ({method} {url}): {error_body}") from e
def _validate_prod_url(raw_url: str, expected_host: str, label: str) -> str:
parsed = urllib.parse.urlsplit(raw_url)
if parsed.scheme != "https":
raise RuntimeError(f"{label} 必须使用 https 协议")
if parsed.hostname != expected_host:
raise RuntimeError(f"{label} 必须使用生产域名 {expected_host}")
return f"{parsed.scheme}://{parsed.hostname}{parsed.path.rstrip('/')}"
def _build_notex_api_url(base_url: str, api_path: str) -> str:
normalized = api_path if api_path.startswith("/openapi/") else f"/openapi{api_path.replace('/api', '', 1)}"
if base_url.endswith("/openapi"):
return f"{base_url}{normalized.replace('/openapi', '', 1)}"
if base_url.endswith("/noteX"):
return f"{base_url}{normalized}"
raise RuntimeError("notexBaseUrl 路径必须是 /noteX 或 /noteX/openapi")
# ──────────── 鉴权:统一由 cms-auth-skills 提供 ────────────
def _find_login_py() -> str:
"""定位 cms-auth-skills/scripts/auth/login.py"""
script_dir = os.path.dirname(os.path.abspath(__file__))
search_dir = script_dir
for _ in range(10):
parent = os.path.dirname(search_dir)
for sub in ["", "skills"]:
candidate = os.path.join(parent, sub, "cms-auth-skills", "scripts", "auth", "login.py") if sub else \
os.path.join(parent, "cms-auth-skills", "scripts", "auth", "login.py")
if os.path.isfile(candidate):
return candidate
search_dir = parent
return ""
def resolve_access_token(context_json: str = "") -> str:
"""
统一鉴权解析:
1. 优先从环境变量 XG_USER_TOKEN 读取
2. 否则调用 cms-auth-skills/scripts/auth/login.py --ensure
"""
env_token = os.environ.get("XG_USER_TOKEN", "").strip()
if env_token:
_log("[auth] 使用环境变量鉴权 (XG_USER_TOKEN)")
return env_token
login_py = _find_login_py()
if not login_py:
raise RuntimeError(
"未找到 cms-auth-skills/scripts/auth/login.py,请先安装:\n"
" npx clawhub@latest install cms-auth-skills --force"
)
_log("[auth] 调用 cms-auth-skills login.py 获取 access-token ...")
cmd = [sys.executable, login_py, "--ensure"]
if context_json:
cmd += ["--context-json", context_json]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
if result.returncode != 0:
raise RuntimeError(f"鉴权失败: {result.stderr.strip()}")
token = result.stdout.strip()
if not token:
raise RuntimeError("鉴权失败: login.py 未返回 access-token")
_log("[auth] 鉴权成功")
return token
# ──────────── 创作任务 ────────────
def create_task(xg_token: str, skill: str, title: str, content: str,
require_text: str = "") -> str:
"""Step 2: 提交技能生成任务"""
info = SKILL_INFO[skill]
_log(f"\n[Step 2] 提交「{info['name']}」生成任务...")
biz_id = f"skills_{skill}_{int(time.time() * 1000)}"
body = {
"title": title,
"bizId": biz_id,
"bizType": "TRILATERA_SKILLS",
"skills": [skill],
"require": require_text or f"请根据提供的内容生成{info['name']},主题为:{title}",
"sources": [{"id": "src_001", "title": f"{title} - 素材", "content_text": content}],
}
url = _build_notex_api_url(NOTEX_BASE_URL, "/openapi/trilateral/autoTask")
data = _request_json(url, method="POST", headers={
"Content-Type": "application/json",
"access-token": xg_token,
}, body=body)
if data.get("resultCode") != 1:
raise RuntimeError(f"创建任务失败: {data.get('resultMsg')}")
task_ids = data.get("data", {}).get("taskId", [])
task_id = task_ids[0] if isinstance(task_ids, list) and task_ids else task_ids
notebook_id = data.get("data", {}).get("noteBook_id", "")
_log(f"✅ 任务创建成功")
_log(f" taskId: {task_id}")
_log(f" notebookId: {notebook_id}")
return task_id
def poll_task_status(task_id: str, xg_token: str, skill_name: str) -> str:
"""Step 3: 轮询任务状态"""
_log(f"\n[Step 3] 轮询任务状态(每 {POLL_INTERVAL_S}s,最多 {POLL_MAX_TIMES} 次 = {POLL_MAX_TIMES} 分钟上限)...")
url = _build_notex_api_url(NOTEX_BASE_URL, f"/openapi/trilateral/taskStatus/{task_id}")
for i in range(1, POLL_MAX_TIMES + 1):
# 第一次等 3 秒,后续等完整间隔
time.sleep(3 if i == 1 else POLL_INTERVAL_S)
data = _request_json(url, method="GET", headers={"access-token": xg_token})
task_data = data.get("data", {})
task_status = task_data.get("task_status", "")
task_url = task_data.get("url", "")
_log(f" 第 {i} 次轮询 → task_status: {task_status}")
if task_status == "COMPLETED" and task_url:
final_url = f"{task_url}&token={xg_token}"
_log(f"\n🎉 {skill_name}生成完成!")
_log(f" 查看链接:{final_url}")
return final_url
if task_status == "FAILED":
raise RuntimeError(f"{skill_name}生成失败,请检查输入内容后重试")
raise RuntimeError(f"轮询超时(超过 {POLL_MAX_TIMES} 分钟),请稍后通过 taskId 查询状态")
def call_ops_chat(xg_token: str, message: str, timeout_ms: int = 300000) -> None:
"""Step 2.B: OPS 运营问答"""
if not message:
raise RuntimeError("ops-chat 需要提供提问内容 (--content)")
timeout_s = timeout_ms // 1000
_log(f"\n[Step 2] 请求 OPS 智能聊天 (自动最长等待 {timeout_s // 60} 分钟)...")
url = _build_notex_api_url(NOTEX_BASE_URL, "/openapi/ops/ai-chat")
body = {"message": message}
data = _request_json(url, method="POST", headers={
"Content-Type": "application/json",
"access-token": xg_token,
}, body=body, timeout=timeout_s)
if data.get("error"):
raise RuntimeError(f"OPS 服务器错误: {data['error']}")
_log("\n🎉 回答生成完毕:\n")
print(data.get("reply", ""))
history_count = data.get("historyCount")
if history_count is not None:
_log(f"\n(当前记录历史上下文数: {history_count} 对)")
# ──────────── 主流程 ────────────
def main():
parser = argparse.ArgumentParser(
description="NoteX Skills 通用脚本 — 支持创作任务与 OPS 运营问答"
)
parser.add_argument("--skill", required=True, choices=ALLOWED_SKILLS,
help=f"技能类型: {' | '.join(ALLOWED_SKILLS)}")
parser.add_argument("--title", default="", help="标题(创作类必填)")
parser.add_argument("--content", default="", help="素材内容 / ops-chat 问题")
parser.add_argument("--require", default="", help="生成要求/风格(可选)")
parser.add_argument("--context-json", default="", help="鉴权上下文 JSON(可选,传给 cms-auth-skills)")
args = parser.parse_args()
maybe_self_update()
# 参数校验
if args.skill == "ops-chat":
if not args.content and not args.title:
print("❌ ops-chat 请提供问题内容:--content \"问题\"(或使用 --title)", file=sys.stderr)
sys.exit(1)
else:
if not args.title:
print("❌ 请提供标题:--title \"标题\"", file=sys.stderr)
sys.exit(1)
if not args.content:
print("❌ 请提供素材内容:--content \"内容\"", file=sys.stderr)
sys.exit(1)
# 验证生产 URL
_validate_prod_url(NOTEX_BASE_URL, "notex.aishuo.co", "notexBaseUrl")
info = SKILL_INFO[args.skill]
_log(f"\n🚀 NoteX Skills — {info['name']}")
_log(f" 预计生成时间:{info['time']}")
_log(f" 标题:{args.title}")
try:
xg_token = resolve_access_token(args.context_json)
if args.skill == "ops-chat":
call_ops_chat(xg_token, args.content or args.title)
else:
task_id = create_task(xg_token, args.skill, args.title, args.content, args.require)
poll_task_status(task_id, xg_token, info["name"])
except RuntimeError as err:
print(f"\n❌ 错误:{err}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()
FILE:scripts/notebooks/notebooks_read.py
#!/usr/bin/env python3
"""
NoteX Notebook 来源读取脚本(可独立执行)
功能:
1) 获取某个 Notebook 下的来源列表(不含正文)
2) 获取某个来源的正文内容(contentText)
⚠️ 独立执行说明:
本脚本可脱离 AI Agent 直接在命令行运行。
执行前请先阅读 Notebooks 模块的 OpenAPI 接口文档获取完整入参说明:
openapi/notebooks/api-index.md
使用方式:
python3 notex-skills/scripts/notebooks/notebooks_read.py --mode list --notebook-id nb_xxx
python3 notex-skills/scripts/notebooks/notebooks_read.py --mode content --notebook-id nb_xxx --source-id src_xxx
环境变量:
XG_USER_TOKEN — access-token(优先使用)
"""
import argparse
import json
import os
import requests
import warnings
# 禁用 InsecureRequestWarning (因为 verify=False)
warnings.filterwarnings("ignore", category=requests.packages.urllib3.exceptions.InsecureRequestWarning)
SCRIPTS_DIR = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
if SCRIPTS_DIR not in sys.path:
sys.path.insert(0, SCRIPTS_DIR)
from self_update import maybe_self_update
PROD_NOTEX_HOST = "notex.aishuo.co"
PROD_NOTEX_BASE_URL = "https://notex.aishuo.co/noteX"
def _log(msg: str):
print(msg, file=sys.stderr, flush=True)
def _request_json(url: str, *, method: str = "GET", headers: dict = None,
body: dict = None, timeout: int = 60) -> dict:
"""发起 HTTP 请求并返回 JSON。"""
try:
response = requests.request(
method,
url,
json=body,
headers=headers,
verify=False,
allow_redirects=True,
timeout=timeout,
)
response.raise_for_status()
payload = response.json()
except requests.exceptions.RequestException as e:
error_body = e.response.text if e.response is not None else str(e)
raise RuntimeError(f"请求失败 ({method} {url}): {error_body}") from e
if isinstance(payload, dict) and "resultCode" in payload:
if payload["resultCode"] != 1:
raise RuntimeError(payload.get("resultMsg") or f"API error ({payload['resultCode']})")
return payload.get("data", {})
return payload
def _normalize_prod_base_url(raw_url: str) -> str:
parsed = urllib.parse.urlsplit(raw_url)
if parsed.scheme != "https":
raise RuntimeError("base-url 必须使用 https 协议")
if parsed.hostname != PROD_NOTEX_HOST:
raise RuntimeError(f"base-url 必须使用生产域名 {PROD_NOTEX_HOST}")
pathname = parsed.path.rstrip("/")
if not pathname or pathname == "/":
return f"{parsed.scheme}://{parsed.hostname}/noteX"
if pathname in ("/noteX", "/noteX/openapi"):
return f"{parsed.scheme}://{parsed.hostname}{pathname}"
raise RuntimeError("base-url 路径仅支持 /noteX 或 /noteX/openapi")
def _build_api_url(base_url: str, api_path: str) -> str:
normalized = api_path if api_path.startswith("/openapi/") else f"/openapi{api_path.replace('/api', '', 1)}"
if base_url.endswith("/openapi"):
return f"{base_url}{normalized.replace('/openapi', '', 1)}"
if base_url.endswith("/noteX"):
return f"{base_url}{normalized}"
raise RuntimeError(f"不支持的 base-url: {base_url}")
# ──────────── 鉴权:统一由 cms-auth-skills 提供 ────────────
def _find_login_py() -> str:
script_dir = os.path.dirname(os.path.abspath(__file__))
search_dir = script_dir
for _ in range(10):
parent = os.path.dirname(search_dir)
for sub in ["", "skills"]:
candidate = os.path.join(parent, sub, "cms-auth-skills", "scripts", "auth", "login.py") if sub else \
os.path.join(parent, "cms-auth-skills", "scripts", "auth", "login.py")
if os.path.isfile(candidate):
return candidate
search_dir = parent
return ""
def resolve_access_token(context_json: str = "") -> str:
env_token = os.environ.get("XG_USER_TOKEN", "").strip()
if env_token:
_log("[auth] 使用环境变量鉴权 (XG_USER_TOKEN)")
return env_token
login_py = _find_login_py()
if not login_py:
raise RuntimeError(
"未找到 cms-auth-skills/scripts/auth/login.py,请先安装:\n"
" npx clawhub@latest install cms-auth-skills --force"
)
_log("[auth] 调用 cms-auth-skills login.py 获取 access-token ...")
cmd = [sys.executable, login_py, "--ensure"]
if context_json:
cmd += ["--context-json", context_json]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
if result.returncode != 0:
raise RuntimeError(f"鉴权失败: {result.stderr.strip()}")
token = result.stdout.strip()
if not token:
raise RuntimeError("鉴权失败: login.py 未返回 access-token")
_log("[auth] 鉴权成功")
return token
# ──────────── 业务功能 ────────────
def list_sources(base_url: str, access_token: str, notebook_id: str,
business_type: str = ""):
if not notebook_id:
raise RuntimeError("list 模式必须提供 --notebook-id")
headers = {"Content-Type": "application/json", "access-token": access_token}
query = ""
if business_type:
query = f"?businessType={urllib.parse.quote(business_type)}"
url = _build_api_url(base_url, f"/api/notebooks/{notebook_id}/sources{query}")
data = _request_json(url, headers=headers, timeout=60)
print(json.dumps(data, ensure_ascii=False, indent=2))
def fetch_source_content(base_url: str, access_token: str, notebook_id: str,
source_id: str):
if not notebook_id or not source_id:
raise RuntimeError("content 模式必须提供 --notebook-id 与 --source-id")
headers = {"Content-Type": "application/json", "access-token": access_token}
url = _build_api_url(base_url, f"/api/notebooks/{notebook_id}/sources/{source_id}/content")
data = _request_json(url, headers=headers, timeout=60)
print(json.dumps(data, ensure_ascii=False, indent=2))
def main():
parser = argparse.ArgumentParser(
description="NoteX Notebook 来源读取脚本 — 来源列表 / 正文获取"
)
parser.add_argument("--mode", required=True, choices=["list", "content"],
help="list=来源列表, content=获取正文")
parser.add_argument("--notebook-id", default="", help="目标 Notebook ID")
parser.add_argument("--source-id", default="", help="目标 Source ID(mode=content 必填)")
parser.add_argument("--business-type", default="", help="业务类型筛选(mode=list 可选)")
parser.add_argument("--base-url", default="",
help="生产地址(仅支持 https://notex.aishuo.co/noteX 或 /noteX/openapi)")
parser.add_argument("--context-json", default="", help="鉴权上下文 JSON(可选,传给 cms-auth-skills)")
args = parser.parse_args()
maybe_self_update()
try:
base_url = _normalize_prod_base_url(args.base_url or PROD_NOTEX_BASE_URL)
access_token = resolve_access_token(args.context_json)
if args.mode == "list":
list_sources(base_url, access_token, args.notebook_id, args.business_type)
elif args.mode == "content":
fetch_source_content(base_url, access_token, args.notebook_id, args.source_id)
except RuntimeError as err:
print(f"❌ {err}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()
FILE:scripts/notebooks/notebooks_write.py
#!/usr/bin/env python3
"""
NoteX 上下文沉淀与写入脚本(可独立执行)
功能:
1) 新建 Notebook 并写入首个 Source(可选标题与正文)
2) 向已有的 Notebook 追加新的 Source
⚠️ 独立执行说明:
本脚本可脱离 AI Agent 直接在命令行运行。
执行前请先阅读 Notebooks 模块的 OpenAPI 接口文档获取完整入参说明:
openapi/notebooks/api-index.md
使用方式:
# 新建笔记本并存入内容:
python3 notex-skills/scripts/notebooks/notebooks_write.py --mode create --title "今日重点" --content "核心运营数据分析结论..."
# 向现有笔记本追加内容:
python3 notex-skills/scripts/notebooks/notebooks_write.py --mode append --notebook-id nb_xxx --title "补充资料" --content "细节1..."
环境变量:
XG_USER_TOKEN — access-token(优先使用)
"""
import argparse
import json
import os
import requests
import warnings
# 禁用 InsecureRequestWarning (因为 verify=False)
warnings.filterwarnings("ignore", category=requests.packages.urllib3.exceptions.InsecureRequestWarning)
import subprocess
import sys
SCRIPTS_DIR = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
if SCRIPTS_DIR not in sys.path:
sys.path.insert(0, SCRIPTS_DIR)
from self_update import maybe_self_update
PROD_NOTEX_HOST = "notex.aishuo.co"
PROD_NOTEX_BASE_URL = "https://notex.aishuo.co/noteX"
def _log(msg: str):
print(msg, file=sys.stderr, flush=True)
def _request_json(url: str, *, method: str = "GET", headers: dict = None,
body: dict = None, timeout: int = 60) -> dict:
"""发起 HTTP 请求并返回 JSON。"""
try:
response = requests.request(
method,
url,
json=body,
headers=headers,
verify=False,
allow_redirects=True,
timeout=timeout,
)
response.raise_for_status()
payload = response.json()
except requests.exceptions.RequestException as e:
error_body = e.response.text if e.response is not None else str(e)
raise RuntimeError(f"请求失败 ({method} {url}): {error_body}") from e
if not isinstance(payload, dict):
return payload
if "resultCode" in payload:
if payload["resultCode"] != 1:
raise RuntimeError(payload.get("resultMsg") or f"API error ({payload['resultCode']})")
return payload.get("data", {})
if "success" in payload:
if not payload["success"]:
raise RuntimeError(payload.get("error") or payload.get("message") or "API error (success=false)")
return payload.get("data", payload)
return payload
def _normalize_prod_base_url(raw_url: str) -> str:
parsed = urllib.parse.urlsplit(raw_url)
if parsed.scheme != "https":
raise RuntimeError("base-url 必须使用 https 协议")
if parsed.hostname != PROD_NOTEX_HOST:
raise RuntimeError(f"base-url 必须使用生产域名 {PROD_NOTEX_HOST}")
pathname = parsed.path.rstrip("/")
if not pathname or pathname == "/":
return f"{parsed.scheme}://{parsed.hostname}/noteX"
if pathname in ("/noteX", "/noteX/openapi"):
return f"{parsed.scheme}://{parsed.hostname}{pathname}"
raise RuntimeError("base-url 路径仅支持 /noteX 或 /noteX/openapi")
def _build_api_url(base_url: str, api_path: str) -> str:
normalized = api_path if api_path.startswith("/openapi/") else f"/openapi{api_path.replace('/api', '', 1)}"
if base_url.endswith("/openapi"):
return f"{base_url}{normalized.replace('/openapi', '', 1)}"
if base_url.endswith("/noteX"):
return f"{base_url}{normalized}"
raise RuntimeError(f"不支持的 base-url: {base_url}")
# ──────────── 鉴权:统一由 cms-auth-skills 提供 ────────────
def _find_login_py() -> str:
script_dir = os.path.dirname(os.path.abspath(__file__))
search_dir = script_dir
for _ in range(10):
parent = os.path.dirname(search_dir)
for sub in ["", "skills"]:
candidate = os.path.join(parent, sub, "cms-auth-skills", "scripts", "auth", "login.py") if sub else \
os.path.join(parent, "cms-auth-skills", "scripts", "auth", "login.py")
if os.path.isfile(candidate):
return candidate
search_dir = parent
return ""
def resolve_access_token(context_json: str = "") -> str:
env_token = os.environ.get("XG_USER_TOKEN", "").strip()
if env_token:
_log("[auth] 使用环境变量鉴权 (XG_USER_TOKEN)")
return env_token
login_py = _find_login_py()
if not login_py:
raise RuntimeError(
"未找到 cms-auth-skills/scripts/auth/login.py,请先安装:\n"
" npx clawhub@latest install cms-auth-skills --force"
)
_log("[auth] 调用 cms-auth-skills login.py 获取 access-token ...")
cmd = [sys.executable, login_py, "--ensure"]
if context_json:
cmd += ["--context-json", context_json]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
if result.returncode != 0:
raise RuntimeError(f"鉴权失败: {result.stderr.strip()}")
token = result.stdout.strip()
if not token:
raise RuntimeError("鉴权失败: login.py 未返回 access-token")
_log("[auth] 鉴权成功")
return token
# ──────────── 业务功能 ────────────
def create_notebook(base_url: str, access_token: str, title: str, content: str):
headers = {"Content-Type": "application/json", "access-token": access_token}
_log("[create] 正在新建笔记本...")
# 1. 创建 Notebook
create_url = _build_api_url(base_url, "/api/notebooks")
notebook_data = _request_json(create_url, method="POST", headers=headers,
body={"title": title}, timeout=60)
notebook_id = (notebook_data.get("id")
or notebook_data.get("notebookId")
or notebook_data.get("notebook_id"))
if not notebook_id:
raise RuntimeError("创建 Notebook 失败:响应中未返回 notebookId")
_log("🎉 成功新建 Notebook!")
_log(f" Notebook ID: {notebook_id}")
# 2. 如果有内容,写入首个 Source
if content and notebook_id:
_log("[create] 正在尝试将初始化内容存为源文件...")
source_url = _build_api_url(base_url, f"/api/notebooks/{notebook_id}/sources")
source_body = {
"title": f"关于 {title} 的资料",
"type": "text",
"content_text": content,
}
source_data = _request_json(source_url, method="POST", headers=headers,
body=source_body, timeout=60)
source_id = source_data.get("sourceId") or source_data.get("id") or "未知"
_log(f"✨ 成功存入预设笔记内容 (Source ID: {source_id})")
def append_source(base_url: str, access_token: str, notebook_id: str,
title: str, content: str):
if not notebook_id:
raise RuntimeError("追加模式 (--mode append) 必须提供 --notebook-id")
headers = {"Content-Type": "application/json", "access-token": access_token}
url = _build_api_url(base_url, f"/api/notebooks/{notebook_id}/sources")
_log(f"[append] 正在向 Notebook ({notebook_id}) 追加来源...")
body = {
"title": title,
"type": "text",
"content_text": content,
}
data = _request_json(url, method="POST", headers=headers, body=body, timeout=60)
source_id = data.get("id") or data.get("sourceId") or "未知"
_log("🎉 成功追加 Source!")
_log(f" Source ID: {source_id}")
def main():
parser = argparse.ArgumentParser(
description="NoteX 上下文沉淀与写入脚本 — 创建笔记本 / 追加来源"
)
parser.add_argument("--mode", required=True, choices=["create", "append"],
help="create=新建笔记本, append=追加来源")
parser.add_argument("--title", default="无标题笔记本", help="笔记本或来源标题")
parser.add_argument("--content", default="", help="要保存的长文本内容")
parser.add_argument("--notebook-id", default="", help="目标笔记本 ID(mode=append 时必填)")
parser.add_argument("--base-url", default="",
help="生产地址(仅支持 https://notex.aishuo.co/noteX 或 /noteX/openapi)")
parser.add_argument("--context-json", default="", help="鉴权上下文 JSON(可选,传给 cms-auth-skills)")
args = parser.parse_args()
maybe_self_update()
try:
base_url = _normalize_prod_base_url(args.base_url or PROD_NOTEX_BASE_URL)
access_token = resolve_access_token(args.context_json)
if args.mode == "create":
create_notebook(base_url, access_token, args.title, args.content)
elif args.mode == "append":
append_source(base_url, access_token, args.notebook_id,
args.title, args.content)
except RuntimeError as err:
print(f"❌ {err}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()
FILE:scripts/open-link/notex_open_link.py
#!/usr/bin/env python3
"""
NoteX 链接补 token 并可选自动打开浏览器(可独立执行)
⚠️ 独立执行说明:
本脚本可脱离 AI Agent 直接在命令行运行。
执行前请先阅读 Open-Link 模块的 OpenAPI 接口文档获取完整入参说明:
openapi/open-link/api-index.md
使用方式:
# 生成带 token 的链接
python3 notex-skills/scripts/open-link/notex_open_link.py
# 自动打开浏览器(可选)
python3 notex-skills/scripts/open-link/notex_open_link.py --auto-open
环境变量:
XG_USER_TOKEN — access-token(优先使用)
"""
import argparse
import json
import os
import subprocess
import sys
import urllib.parse
import requests
import warnings
# 禁用 InsecureRequestWarning (因为 verify=False)
warnings.filterwarnings("ignore", category=requests.packages.urllib3.exceptions.InsecureRequestWarning)
SCRIPTS_DIR = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
if SCRIPTS_DIR not in sys.path:
sys.path.insert(0, SCRIPTS_DIR)
from self_update import maybe_self_update
PROD_NOTEX_HOST = "notex.aishuo.co"
PROD_NOTEX_BASE_URL = "https://notex.aishuo.co/noteX"
DEFAULT_NOTEX_HOME_URL = "https://notex.aishuo.co/"
def _log(msg: str):
print(msg, file=sys.stderr, flush=True)
def _request_json(url: str, *, method: str = "GET", headers: dict = None,
body: dict = None, timeout: int = 60) -> dict:
"""发起 HTTP 请求并返回 JSON。"""
try:
response = requests.request(
method,
url,
json=body,
headers=headers,
verify=False,
allow_redirects=True,
timeout=timeout,
)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
error_body = e.response.text if e.response is not None else str(e)
raise RuntimeError(f"请求失败 ({method} {url}): {error_body}") from e
# ──────────── 鉴权:统一由 cms-auth-skills 提供 ────────────
def _find_login_py() -> str:
script_dir = os.path.dirname(os.path.abspath(__file__))
search_dir = script_dir
for _ in range(10):
parent = os.path.dirname(search_dir)
for sub in ["", "skills"]:
candidate = os.path.join(parent, sub, "cms-auth-skills", "scripts", "auth", "login.py") if sub else \
os.path.join(parent, "cms-auth-skills", "scripts", "auth", "login.py")
if os.path.isfile(candidate):
return candidate
search_dir = parent
return ""
def resolve_access_token(context_json: str = "") -> str:
env_token = os.environ.get("XG_USER_TOKEN", "").strip()
if env_token:
_log("[auth] 使用环境变量鉴权 (XG_USER_TOKEN)")
return env_token
login_py = _find_login_py()
if not login_py:
raise RuntimeError(
"未找到 cms-auth-skills/scripts/auth/login.py,请先安装:\n"
" npx clawhub@latest install cms-auth-skills --force"
)
_log("[auth] 调用 cms-auth-skills login.py 获取 access-token ...")
cmd = [sys.executable, login_py, "--ensure"]
if context_json:
cmd += ["--context-json", context_json]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
if result.returncode != 0:
raise RuntimeError(f"鉴权失败: {result.stderr.strip()}")
token = result.stdout.strip()
if not token:
raise RuntimeError("鉴权失败: login.py 未返回 access-token")
_log("[auth] 鉴权成功")
return token
# ──────────── URL 处理 ────────────
def _normalize_notex_url(raw_url: str) -> urllib.parse.SplitResult:
parsed = urllib.parse.urlsplit(raw_url)
if parsed.scheme != "https":
raise RuntimeError("URL 必须使用 https 协议")
if parsed.hostname != PROD_NOTEX_HOST:
raise RuntimeError(f"URL 必须使用生产域名 {PROD_NOTEX_HOST}")
pathname = parsed.path.rstrip("/") or "/"
if pathname != "/":
raise RuntimeError("URL 仅支持 NoteX 首页路由:https://notex.aishuo.co/")
return parsed
def _build_authorized_url(raw_url: str, token: str) -> str:
_normalize_notex_url(raw_url)
return f"https://{PROD_NOTEX_HOST}/?token={urllib.parse.quote(token)}"
def _open_in_browser(url: str) -> bool:
"""尝试用系统默认浏览器打开 URL"""
platform = sys.platform
try:
if platform == "darwin":
subprocess.Popen(["open", url], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
elif platform == "win32":
subprocess.Popen(["cmd", "/c", "start", "", url], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
else:
subprocess.Popen(["xdg-open", url], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
return True
except Exception:
return False
def main():
parser = argparse.ArgumentParser(
description="NoteX 链接补 token 并可选自动打开浏览器"
)
parser.add_argument("--url", default=DEFAULT_NOTEX_HOME_URL,
help="NoteX URL(仅支持首页 https://notex.aishuo.co/,默认即可)")
parser.add_argument("--auto-open", action="store_true",
help="是否自动打开浏览器")
parser.add_argument("--context-json", default="", help="鉴权上下文 JSON(可选,传给 cms-auth-skills)")
args = parser.parse_args()
maybe_self_update()
try:
access_token = resolve_access_token(args.context_json)
final_url = _build_authorized_url(args.url, access_token)
opened = False
if args.auto_open:
opened = _open_in_browser(final_url)
_log(f"[open-link] 已生成可访问链接:{final_url}")
if args.auto_open:
_log("[open-link] 已尝试自动打开浏览器" if opened else "[open-link] 自动打开浏览器失败,请手动打开链接")
output = {
"url": final_url,
"opened": opened,
"authSource": "env" if os.environ.get("XG_USER_TOKEN", "").strip() else "cms-auth-skills",
}
print(json.dumps(output, ensure_ascii=False, indent=2))
except RuntimeError as err:
print(f"❌ {err}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()
FILE:scripts/self_update.py
#!/usr/bin/env python3
"""
Shared self-update helper for notex-skills entry scripts.
Behavior:
- Read static metadata from ../version.json
- Use version.json mtime as the "installed-at" timestamp
- Check remote version only when the local install is older than 24 hours
- Fail open on every exception so business scripts keep working
- Restart the current script immediately after a successful update so the business flow continues on the new version
"""
from __future__ import annotations
import json
import os
import shutil
import sys
import time
import requests
import warnings
import zipfile
# 禁用 InsecureRequestWarning (因为 verify=False)
warnings.filterwarnings("ignore", category=requests.packages.urllib3.exceptions.InsecureRequestWarning)
REGISTRY_API_URL = "https://sg-cwork-api.mediportal.com.cn/im/skill/nologin/list"
CHECK_INTERVAL_SECONDS = 24 * 60 * 60
SKIP_ENV_NAME = "NOTEX_SKILLS_SKIP_SELF_UPDATE"
def _log(message: str) -> None:
print(f"[self-update] {message}", file=sys.stderr, flush=True)
def _result(
*,
status: str,
checked: bool = False,
updated: bool = False,
current_version: str = "",
remote_version: str = "",
message: str = "",
error: str = "",
) -> dict:
return {
"status": status,
"checked": checked,
"updated": updated,
"currentVersion": current_version,
"remoteVersion": remote_version,
"message": message,
"error": error,
}
def _scripts_dir() -> str:
return os.path.dirname(os.path.abspath(__file__))
def _skill_root() -> str:
return os.path.abspath(os.path.join(_scripts_dir(), ".."))
def _version_file() -> str:
return os.path.join(_skill_root(), "version.json")
def _load_version_meta(version_path: str) -> dict:
with open(version_path, "r", encoding="utf-8") as file_obj:
payload = json.load(file_obj)
if not isinstance(payload, dict):
raise RuntimeError("version.json must contain a JSON object")
skillcode = str(payload.get("skillcode", "")).strip()
version = str(payload.get("version", "")).strip()
if not skillcode or not version:
raise RuntimeError("version.json must include skillcode and version")
return {"skillcode": skillcode, "version": version}
def _version_key(raw: object) -> tuple[int, ...]:
text = str(raw or "").strip()
if not text:
raise RuntimeError("empty version")
parts = []
current = []
for char in text:
if char.isdigit():
current.append(char)
continue
if current:
parts.append(int("".join(current)))
current = []
if current:
parts.append(int("".join(current)))
if not parts:
raise RuntimeError(f"invalid version: {text}")
while len(parts) > 1 and parts[-1] == 0:
parts.pop()
return tuple(parts)
def _http_get(url: str, timeout: int = 15) -> str:
"""发起 HTTP GET 请求并返回内容。"""
try:
response = requests.get(
url,
verify=False,
allow_redirects=True,
timeout=timeout,
)
response.raise_for_status()
return response.text
except Exception as e:
raise RuntimeError(f"请求失败: {e}")
def _fetch_remote_skill(skillcode: str) -> dict | None:
payload = json.loads(_http_get(REGISTRY_API_URL, timeout=60))
skills = payload if isinstance(payload, list) else payload.get("data") or payload.get("resultData") or []
normalized = skillcode.lower()
for skill in skills:
if not isinstance(skill, dict):
continue
if str(skill.get("code", "")).strip().lower() == normalized:
return skill
return None
def _download_zip(url: str, dest_path: str) -> None:
try:
response = requests.get(
url,
verify=False,
allow_redirects=True,
timeout=120,
stream=True,
)
response.raise_for_status()
with open(dest_path, "wb") as file_obj:
for chunk in response.iter_content(chunk_size=8192):
if chunk:
file_obj.write(chunk)
except Exception as e:
raise RuntimeError(f"下载失败: {e}")
def _extract_skill_root(zip_path: str, stage_parent: str, skillcode: str) -> str:
with zipfile.ZipFile(zip_path, "r") as zip_file:
names = zip_file.namelist()
top_entries = {name.split("/")[0] for name in names if name.split("/")[0]}
has_single_root = False
root_name = ""
if len(top_entries) == 1:
root_name = next(iter(top_entries))
has_single_root = all(
name == root_name or name.startswith(root_name + "/")
for name in names
)
if has_single_root:
zip_file.extractall(stage_parent)
return os.path.join(stage_parent, root_name)
fallback_root = os.path.join(stage_parent, skillcode)
os.makedirs(fallback_root, exist_ok=True)
zip_file.extractall(fallback_root)
return fallback_root
def _swap_skill_root(skill_root: str, staged_root: str) -> None:
backup_root = f"{skill_root}.__backup__.{int(time.time())}"
if os.path.isdir(backup_root):
shutil.rmtree(backup_root, ignore_errors=True)
moved_old = False
try:
if os.path.isdir(skill_root):
os.rename(skill_root, backup_root)
moved_old = True
shutil.move(staged_root, skill_root)
if moved_old:
shutil.rmtree(backup_root, ignore_errors=True)
except Exception:
if os.path.isdir(skill_root):
shutil.rmtree(skill_root, ignore_errors=True)
if moved_old and os.path.isdir(backup_root):
os.rename(backup_root, skill_root)
raise
def _restart_current_process() -> None:
script_path = os.path.abspath(sys.argv[0]) if sys.argv else ""
if not script_path:
raise RuntimeError("missing current script path")
env = os.environ.copy()
env[SKIP_ENV_NAME] = "1"
argv = [sys.executable, script_path, *sys.argv[1:]]
os.execve(sys.executable, argv, env)
def maybe_self_update() -> dict:
if os.environ.get(SKIP_ENV_NAME, "").strip():
return _result(status="skipped", message="skip self-update after restart")
try:
version_path = _version_file()
metadata = _load_version_meta(version_path)
current_version = metadata["version"]
version_age_seconds = time.time() - os.path.getmtime(version_path)
if version_age_seconds < CHECK_INTERVAL_SECONDS:
return _result(
status="skipped",
current_version=current_version,
message="local install is newer than 24 hours threshold",
)
remote_skill = _fetch_remote_skill(metadata["skillcode"])
if not remote_skill:
message = f"platform skill not found: {metadata['skillcode']}"
return _result(
status="no-remote-skill",
checked=True,
current_version=current_version,
message=message,
)
remote_version = str(remote_skill.get("version", "")).strip()
download_url = str(remote_skill.get("downloadUrl", "")).strip()
if not remote_version or not download_url:
message = "remote version or downloadUrl is missing"
return _result(
status="invalid-remote-metadata",
checked=True,
current_version=current_version,
remote_version=remote_version,
message=message,
)
if _version_key(remote_version) <= _version_key(current_version):
message = f"already up to date ({current_version})"
return _result(
status="up-to-date",
checked=True,
current_version=current_version,
remote_version=remote_version,
message=message,
)
_log(f"upgrading {metadata['skillcode']} {current_version} -> {remote_version}")
skill_root = _skill_root()
parent_dir = os.path.dirname(skill_root)
with tempfile.TemporaryDirectory(prefix=f"{metadata['skillcode']}-update-", dir=parent_dir) as tmp_dir:
zip_path = os.path.join(tmp_dir, "skill.zip")
stage_parent = os.path.join(tmp_dir, "stage")
os.makedirs(stage_parent, exist_ok=True)
_download_zip(download_url, zip_path)
if not zipfile.is_zipfile(zip_path):
raise RuntimeError("downloaded payload is not a ZIP archive")
staged_root = _extract_skill_root(zip_path, stage_parent, metadata["skillcode"])
staged_meta = _load_version_meta(os.path.join(staged_root, "version.json"))
if staged_meta["skillcode"] != metadata["skillcode"]:
raise RuntimeError("downloaded skillcode does not match current package")
if _version_key(staged_meta["version"]) <= _version_key(current_version):
message = "downloaded package did not advance local version"
return _result(
status="stale-package",
checked=True,
current_version=current_version,
remote_version=staged_meta["version"],
message=message,
)
_swap_skill_root(skill_root, staged_root)
try:
os.utime(version_path, None)
except OSError:
pass
refreshed = _load_version_meta(version_path)
if _version_key(refreshed["version"]) <= _version_key(current_version):
message = "local version did not advance after install"
return _result(
status="install-noop",
checked=True,
current_version=current_version,
remote_version=remote_version,
message=message,
)
_log(f"update applied, restarting script with version {refreshed['version']}")
_restart_current_process()
return _result(
status="updated",
checked=True,
updated=True,
current_version=refreshed["version"],
remote_version=remote_version,
message="update applied successfully",
)
except Exception as exc:
return _result(
status="error",
checked=False,
message="self-update skipped because of an exception",
error=str(exc),
)
if __name__ == "__main__":
result = maybe_self_update()
if result.get("updated"):
print(json.dumps(result, ensure_ascii=False, indent=2))
FILE:scripts/sources/source_index_sync.py
#!/usr/bin/env python3
"""
Notebook/Source 索引同步脚本(可独立执行)
功能:
1) 全量拉取索引树并落盘(覆盖写,保证全量刷新)
2) 按 notebookId/sourceId 拉取来源最小详情(ID + 名称)并落盘
3) 可配置定时轮询,持续全量刷新索引
⚠️ 独立执行说明:
本脚本可脱离 AI Agent 直接在命令行运行。
执行前请先阅读 Sources 模块的 OpenAPI 接口文档获取完整入参说明:
openapi/sources/api-index.md
使用方式:
python3 notex-skills/scripts/sources/source_index_sync.py --mode index
python3 notex-skills/scripts/sources/source_index_sync.py --mode detail --notebook-id nb_xxx
python3 notex-skills/scripts/sources/source_index_sync.py --mode detail --source-id src_xxx
python3 notex-skills/scripts/sources/source_index_sync.py --mode index --interval-minutes 60
环境变量:
XG_USER_TOKEN — access-token(优先使用)
"""
import argparse
import hashlib
import json
import os
import requests
import warnings
import subprocess
import sys
import time
# 禁用 InsecureRequestWarning (因为 verify=False)
warnings.filterwarnings("ignore", category=requests.packages.urllib3.exceptions.InsecureRequestWarning)
SCRIPTS_DIR = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
if SCRIPTS_DIR not in sys.path:
sys.path.insert(0, SCRIPTS_DIR)
from self_update import maybe_self_update
PROD_NOTEX_HOST = "notex.aishuo.co"
PROD_NOTEX_BASE_URL = "https://notex.aishuo.co/noteX"
def _log(msg: str):
print(msg, file=sys.stderr, flush=True)
def _request_json(url: str, *, method: str = "GET", headers: dict = None,
body: dict = None, timeout: int = 60) -> dict:
"""发起 HTTP 请求并返回 JSON。"""
try:
response = requests.request(
method,
url,
json=body,
headers=headers,
verify=False,
allow_redirects=True,
timeout=timeout,
)
response.raise_for_status()
payload = response.json()
except requests.exceptions.RequestException as e:
error_body = e.response.text if e.response is not None else str(e)
raise RuntimeError(f"请求失败 ({method} {url}): {error_body}") from e
if isinstance(payload, dict) and "resultCode" in payload:
if payload["resultCode"] != 1:
raise RuntimeError(payload.get("resultMsg") or f"API error ({payload['resultCode']})")
return payload.get("data", {})
return payload
def _write_json(file_path: str, data):
os.makedirs(os.path.dirname(file_path), exist_ok=True)
with open(file_path, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
def _normalize_prod_base_url(raw_url: str) -> str:
parsed = urllib.parse.urlsplit(raw_url)
if parsed.scheme != "https":
raise RuntimeError("base-url 必须使用 https 协议")
if parsed.hostname != PROD_NOTEX_HOST:
raise RuntimeError(f"base-url 必须使用生产域名 {PROD_NOTEX_HOST}")
pathname = parsed.path.rstrip("/")
if not pathname or pathname == "/":
return f"{parsed.scheme}://{parsed.hostname}/noteX"
if pathname in ("/noteX", "/noteX/openapi"):
return f"{parsed.scheme}://{parsed.hostname}{pathname}"
raise RuntimeError("base-url 路径仅支持 /noteX 或 /noteX/openapi")
def _build_api_url(base_url: str, api_path: str) -> str:
normalized = api_path if api_path.startswith("/openapi/") else f"/openapi{api_path.replace('/api', '', 1)}"
if base_url.endswith("/openapi"):
return f"{base_url}{normalized.replace('/openapi', '', 1)}"
if base_url.endswith("/noteX"):
return f"{base_url}{normalized}"
raise RuntimeError(f"不支持的 base-url: {base_url}")
def _get_cache_user_id(token: str) -> str:
if not token:
return "unknown"
digest = hashlib.sha256(token.encode("utf-8")).hexdigest()[:12]
return f"token-{digest}"
# ──────────── 鉴权:统一由 cms-auth-skills 提供 ────────────
def _find_login_py() -> str:
script_dir = os.path.dirname(os.path.abspath(__file__))
search_dir = script_dir
for _ in range(10):
parent = os.path.dirname(search_dir)
for sub in ["", "skills"]:
candidate = os.path.join(parent, sub, "cms-auth-skills", "scripts", "auth", "login.py") if sub else \
os.path.join(parent, "cms-auth-skills", "scripts", "auth", "login.py")
if os.path.isfile(candidate):
return candidate
search_dir = parent
return ""
def resolve_access_token(context_json: str = "") -> str:
env_token = os.environ.get("XG_USER_TOKEN", "").strip()
if env_token:
_log("[auth] 使用环境变量鉴权 (XG_USER_TOKEN)")
return env_token
login_py = _find_login_py()
if not login_py:
raise RuntimeError(
"未找到 cms-auth-skills/scripts/auth/login.py,请先安装:\n"
" npx clawhub@latest install cms-auth-skills --force"
)
_log("[auth] 调用 cms-auth-skills login.py 获取 access-token ...")
cmd = [sys.executable, login_py, "--ensure"]
if context_json:
cmd += ["--context-json", context_json]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
if result.returncode != 0:
raise RuntimeError(f"鉴权失败: {result.stderr.strip()}")
token = result.stdout.strip()
if not token:
raise RuntimeError("鉴权失败: login.py 未返回 access-token")
_log("[auth] 鉴权成功")
return token
# ──────────── 统计工具 ────────────
def _collect_tree_stats(tree) -> dict:
notebook_count = 0
source_count = 0
def walk(nodes):
nonlocal notebook_count, source_count
if not isinstance(nodes, list):
return
for node in nodes:
notebook_count += 1
sources = node.get("sources", [])
source_count += len(sources) if isinstance(sources, list) else 0
walk(node.get("children", []))
walk(tree)
return {"notebookCount": notebook_count, "sourceCount": source_count}
# ──────────── 业务功能 ────────────
def refresh_index_once(base_url: str, access_token: str, index_type: str,
cache_dir: str, output_path: str):
valid_types = {"all", "owned", "collaborated"}
if index_type not in valid_types:
raise RuntimeError(f"非法 type: {index_type}(仅支持 all | owned | collaborated)")
headers = {"Content-Type": "application/json", "access-token": access_token}
url = _build_api_url(base_url,
f"/api/notebooks/sources/index-tree?type={urllib.parse.quote(index_type)}")
data = _request_json(url, headers=headers, timeout=120)
if not output_path:
user_id = _get_cache_user_id(access_token)
output_path = os.path.join(cache_dir, user_id, "index-tree.json")
_write_json(output_path, data)
tree = data.get("tree", [])
stats = _collect_tree_stats(tree)
generated_at = data.get("generatedAt", "")
_log(f"[index] 全量刷新完成: {output_path}")
_log(f"[index] notebooks={stats['notebookCount']}, sources={stats['sourceCount']}, generatedAt={generated_at}")
def fetch_details_once(base_url: str, access_token: str, notebook_id: str,
source_id: str, cache_dir: str, output_path: str):
if not notebook_id and not source_id:
raise RuntimeError("detail 模式必须提供 --notebook-id 或 --source-id")
headers = {"Content-Type": "application/json", "access-token": access_token}
query_parts = []
if notebook_id:
query_parts.append(f"notebookId={urllib.parse.quote(notebook_id)}")
if source_id:
query_parts.append(f"sourceId={urllib.parse.quote(source_id)}")
query_str = "&".join(query_parts)
url = _build_api_url(base_url, f"/api/notebooks/sources/details?{query_str}")
data = _request_json(url, headers=headers, timeout=120)
if not output_path:
user_id = _get_cache_user_id(access_token)
default_file = f"source-{source_id}.json" if source_id else f"notebook-{notebook_id}.json"
output_path = os.path.join(cache_dir, user_id, "details", default_file)
_write_json(output_path, data)
mode = data.get("mode", "")
nb = data.get("notebook", {})
nb_id = nb.get("id", "-") if isinstance(nb, dict) else "-"
_log(f"[detail] 最小详情已写入: {output_path}")
_log(f"[detail] mode={mode}, notebook={nb_id}")
def main():
parser = argparse.ArgumentParser(
description="Notebook/Source 索引同步脚本 — 全量索引树 / 详情查询"
)
parser.add_argument("--mode", default="index", choices=["index", "detail"],
help="index=全量索引, detail=详情查询")
parser.add_argument("--type", default="all", help="index 模式下的可见范围: all | owned | collaborated")
parser.add_argument("--notebook-id", default="", help="目标 Notebook ID(mode=detail)")
parser.add_argument("--source-id", default="", help="目标 Source ID(mode=detail)")
parser.add_argument("--base-url", default="",
help="生产地址(仅支持 https://notex.aishuo.co/noteX 或 /noteX/openapi)")
parser.add_argument("--cache-dir", default="",
help="本地缓存根目录(默认: ../../cache/notebook-source-index)")
parser.add_argument("--output", default="", help="指定输出文件")
parser.add_argument("--interval-minutes", type=int, default=0,
help="仅 index 模式生效,按分钟持续全量刷新")
parser.add_argument("--context-json", default="", help="鉴权上下文 JSON(可选,传给 cms-auth-skills)")
args = parser.parse_args()
maybe_self_update()
try:
base_url = _normalize_prod_base_url(args.base_url or PROD_NOTEX_BASE_URL)
access_token = resolve_access_token(args.context_json)
script_dir = os.path.dirname(os.path.abspath(__file__))
default_cache = args.cache_dir or os.path.join(script_dir, "..", "..",
"cache", "notebook-source-index")
cache_dir = os.path.abspath(default_cache)
if args.mode == "index":
if args.interval_minutes <= 0:
refresh_index_once(base_url, access_token, args.type, cache_dir, args.output)
else:
interval_s = args.interval_minutes * 60
_log(f"[index] 已进入定时全量刷新模式,每 {args.interval_minutes} 分钟执行一次")
while True:
try:
refresh_index_once(base_url, access_token, args.type, cache_dir, args.output)
except RuntimeError as e:
_log(f"[index] 刷新失败: {e}")
time.sleep(interval_s)
elif args.mode == "detail":
fetch_details_once(base_url, access_token, args.notebook_id,
args.source_id, cache_dir, args.output)
except RuntimeError as err:
print(f"❌ {err}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()
FILE:version.json
{
"skillcode": "notex-skills",
"version": "1.3"
}