Skill: smallkeyboy/impl-project-manager (version 1.0.0)

--- name: impl-project-manager description: 实施项目经理工具箱 — IT/软件交付项目的全流程管理，覆盖项目管理（立项、里程碑、风险、变更）、主合同签订回款跟踪、采购分包付款跟踪三大模块。数据存储在飞书在线表格，PM 在飞书录入，Skills 读取分析后通过对话确认并自动更新飞书表格。触发词：项目管理、项目立项、里程碑、回款跟踪、分包管理、付款跟踪、进度报告、合同跟踪、项目计划书、结项复盘。 --- # 实施项目经理工具箱 IT/软件交付项目的全流程管理 Skills，以里程碑为核心驱动，飞书在线表格为数据源和交付物。 ## 核心模式 **PM 在飞书表格录入 → Skills 读取分析 → 对话确认 → 自动更新飞书表格** 飞书表格是唯一数据源，不维护本地文档副本。 ## 核心模块 1. 项目管理（立项、里程碑、风险、变更） 2. 主合同签订与回款跟踪 3. 采购分包与付款跟踪 --- ## 飞书对接 ### 配置 应用凭证见 [references/feishu-config.md](references/feishu-config.md) ### 脚本 | 脚本 | 用途 | |------|------| | `scripts/feishu_api.py` | 飞书 API 基础（token 获取、通用请求） | | `scripts/feishu_sheet.py` | 飞书表格读写操作 | | `scripts/calc_engine.py` | 项目管理计算引擎（里程碑推算、金额计算、风险识别、预警判断） | ### 使用方式 1. PM 在飞书表格中录入/修改数据（立项信息、里程碑日期、回款比例等） 2. 调用 `feishu_sheet.py read` 读取表格数据 3. 调用 `calc_engine.py` 自动计算（金额、状态、风险、预警） 4. 通过对话呈现分析结果，PM 确认后 5. 调用 `feishu_sheet.py write` 更新飞书表格中的计算字段 --- ## 工作流程 ### 新项目启动 1. PM 在飞书表格中填写立项信息（项目名称、客户、预算、干系人、范围） 2. PM 填写需求确认日期，回款比例 3. Skills 读取表格 → 自动推算里程碑日期 → 计算回款金额 → 写回表格 **里程碑推算规则：** - M1 需求确认：PM 提供 - M2 试运行完成：M1 + 3个月 - M3 验收通过：M2 + 半个月 - M4 质保完成：M3 + 1年 **干系人角色：** 客户方对接人 + 我方交付负责人 + 我方实施经理 ### 日常更新 - PM 在飞书更新里程碑实际完成日期、回款/付款实际数据 - Skills 读取 → 自动计算状态、预警、风险 → 写回表格 - 变更记录：PM 提供变更描述，Skills 评估三维度影响后写入表格 - 分包录入：PM 提供分包商信息 + 各里程碑付款比例，Skills 计算付款金额并写入 ### 文档生成 按需从飞书表格数据生成中文 markdown 文档： | 文档 | 触发时机 | |------|----------| | 项目计划书 | 项目启动 | | 进度周报 | 定期/按需 | | 合同回款跟踪表 | 合同/回款变更 | | 分包付款跟踪表 | 分包/付款变更 | | 项目状态快照 | 随时 | | 结项复盘报告 | 项目关闭 | --- ## 自动规则 ### 风险自动识别 | 规则 | 触发条件 | 等级 | |------|----------|------| | 里程碑延期 | 计划日期已过且未完成 | 🔴高 | | 里程碑即将到期 | ≤7天且未完成 | 🟡中 | | 需求变更频发 | 单里程碑变更≥3条 | 🟡中 | | 预算超支风险 | 已发生成本>预算×80% | 🔴高 | ### 回款/付款预警 | 级别 | 触发条件 | 标记 | |------|----------|------| | 即将到期 | 距计划日期≤7天且未完成 | 🟡 | | 已逾期 | 超过计划日期且未完成 | 🔴 | ### 里程碑状态自动流转 - 前置完成 → 后继自动进入"进行中" - 填写实际完成日期 → 标记"已完成" - 计划日期已过无实际日期 → 标记"已延期" --- ## 变更管理 每条变更必须评估三维度影响： 1. **成本**：预算变化及金额 2. **工期**：工期变化及天数 3. **范围**：交付范围变化及描述 --- ## 参考文档 - [需求说明](references/requirements.md) — 完整需求规格 - [飞书配置](references/feishu-config.md) — 应用凭证与权限 - [里程碑模板](references/milestone-template.md) — 默认里程碑与状态流转 - [风险识别规则](references/risk-rules.md) — 自动风险规则与记录模板 - [变更管理模板](references/change-management.md) — 变更字段与流程 - [合同回款模板](references/contract-payment-template.md) — 合同生命周期与回款预警 - [分包类型参考](references/subcontract-types.md) — 分包类型与付款节点 --- ## 多项目管理 采用每个项目独立表格模式，通过项目总目录统一管理。 ### 项目总目录 - **总目录表格：** https://my.feishu.cn/sheets/PkIasY146hifjptNC69cdk6knEb - 记录所有项目的名称、客户、预算、状态、表格链接 - 新项目自动在此登记 ### 新项目流程 1. PM 说「新建项目」→ 逐项提问收集立项信息 2. 确认需求确认日期 → 自动推算里程碑 3. 确认回款比例 → 自动计算金额 4. AI 创建新飞书表格（5个Sheet：项目总览、合同回款、分包付款、变更记录、风险清单） 5. **自动设置编辑权限**（组织内可编辑） 6. 写入项目数据 → 在总目录登记 7. 告诉 PM 新表格链接 ### 日常操作 - PM 说「看XX项目」→ AI 从总目录定位表格 → 读取数据 - PM 说「更新XX项目」→ AI 定位表格 → 读取分析 → 对话确认 → 更新飞书表格 ## 数据边界 **不包含：** 签约方银行账户、个人隐私信息、分包商绩效评价。 **数据源：** 飞书在线表格（唯一数据源，不维护本地副本）。 **默认权限：** 所有新建表格自动设置为组织内可编辑（`tenant_editable`）。

path: references/change-management.md
content: # 变更管理模板

## 变更记录字段

| 字段 | 必填 | 说明 |
|------|------|------|
| 变更编号 | ✅ | CHG + 三位数字，自动递增（CHG001, CHG002...） |
| 变更描述 | ✅ | 变更内容概述 |
| 提出方 | ✅ | 客户 / 我方 |
| 提出日期 | ✅ | — |
| 影响评估-成本 | ✅ | 对项目预算的影响（增加/不变/减少 + 金额） |
| 影响评估-工期 | ✅ | 对项目工期的影响（延长/不变/缩短 + 天数） |
| 影响评估-范围 | ✅ | 对项目范围的影响（扩大/不变/缩减 + 描述） |
| 处理状态 | ✅ | 待评估 / 已批准 / 已拒绝 / 已实施 |
| 完成日期 | ❌ | 实施完成日期 |

## 变更状态流转

```
待评估 → 已批准 → 已实施
待评估 → 已拒绝
```

## 影响评估要求

每条变更必须评估三维度影响：

1. **成本影响**：是否导致预算变化？变化金额？
2. **工期影响**：是否导致工期变化？变化天数？
3. **范围影响**：是否改变交付范围？具体变化？

## 变更与风险联动

- 单个里程碑内变更 ≥3 条 → 自动触发风险 R003（需求变更频发）
- 变更导致工期延长 → 检查是否触发风险 R001（里程碑延期）

path: references/contract-payment-template.md
content: # 合同与回款模板

## 合同生命周期

```
NEGOTIATING (谈判中) → SIGNED (已签约) → EFFECTIVE (生效)
→ FULFILLING (履约中) → ACCEPTED (验收) → SETTLED (结算) → CLOSED (关闭)
```

## 回款节点默认模板

基于 IT/软件交付里程碑：

| 里程碑 | 默认回款比例 | 说明 |
|--------|-------------|------|
| 需求确认 | 30% | 需求基线确认后 |
| 试运行完成 | 40% | 系统试运行通过 |
| 验收通过 | 25% | 正式验收签字 |
| 质保完成 | 5% | 质保期满 |

**计算规则：** 计划回款金额 = 合同金额 × 回款比例

## 预警规则

| 预警级别 | 触发条件 | 标记 | 建议动作 |
|----------|----------|------|----------|
| 即将到期 | 距计划回款日期 ≤7 天且未回款 | 🟡 | 提前准备回款材料，联系客户方 |
| 已逾期 | 超过计划回款日期且未回款 | 🔴 | 立即跟进，升级处理 |

## 回款状态

| 状态 | 条件 |
|------|------|
| 未回款 | 实际回款金额 = 0 |
| 部分回款 | 0 < 实际回款金额 < 计划回款金额 |
| 已回款 | 实际回款金额 ≥ 计划回款金额 |

path: references/feishu-config.md
content: # 飞书应用配置

## 应用信息

- **应用名称：** 项目管理助手
- **App ID：** cli_aa9d0e7045f81cc8
- **App Secret：** oZ6exMRCZ9imHRkF28agUhxHFArbltk8

## 需要开通的权限

在飞书开放平台 → 应用详情 → 权限管理 中开通以下权限：

| 权限 | 权限标识 | 用途 |
|------|----------|------|
| 查看文档 | `docx:document:read` | 读取项目文档 |
| 编辑文档 | `docx:document:write` | 更新项目文档 |
| 查看表格 | `sheet:spreadsheet:read` | 读取项目数据表格 |
| 编辑表格 | `sheet:spreadsheet:write` | 更新计算字段和状态 |
| 表格全部权限 | `sheets:spreadsheet` | 创建表格、读写表格 |
| 云空间权限 | `drive:drive` | 设置表格分享权限 |

## 默认权限规则

所有新建表格自动设置为**组织内可编辑**（`link_share_entity: tenant_editable`），确保 PM 可直接在飞书客户端编辑表格数据。

设置方式：调用 `drive/v1/permissions/{token}/public?type=sheet` API，PATCH `link_share_entity` 为 `tenant_editable`。

## 操作步骤

1. 登录 https://open.feishu.cn/app
2. 进入刚创建的应用
3. 左侧菜单 → 权限管理 → 搜索以上权限并开通
4. 左侧菜单 → 版本管理 → 创建版本 → 申请发布
5. 等待管理员审批通过（个人账号自己是管理员，直接通过）

## Token 说明

- 使用 tenant_access_token（企业自建应用凭证）
- Token 有效期 2 小时，过期自动刷新
- 不使用 user_access_token，避免用户授权流程


path: references/milestone-template.md
content: # 里程碑模板

## IT/软件交付默认里程碑

```
需求确认 → 试运行完成 → 验收通过 → 质保完成
```

| 序号 | 里程碑名称 | 说明 | 默认占比 |
|------|-----------|------|----------|
| M1 | 需求确认 | 需求文档基线化，客户签字确认 | 约30%工期 |
| M2 | 试运行完成 | 系统部署上线，试运行期满 | 约40%工期 |
| M3 | 验收通过 | 正式验收报告签署 | 约25%工期 |
| M4 | 质保完成 | 质保期结束，项目正式关闭 | 约5%工期 |

## 里程碑状态

| 状态 | 标记 | 说明 |
|------|------|------|
| NOT_STARTED | ⬜ | 前置里程碑尚未完成 |
| IN_PROGRESS | 🟦 | 当前正在推进 |
| COMPLETED | ✅ | 已完成，有实际完成日期 |
| DELAYED | 🔴 | 计划日期已过且未完成 |

## 状态流转规则

- 前置里程碑完成后，后继里程碑从 NOT_STARTED → IN_PROGRESS
- 填写实际完成日期后，状态 → COMPLETED
- 计划完成日期已过且无实际完成日期 → DELAYED

## 自定义里程碑

PM 可在默认模板基础上：
- 增加里程碑（如：设计确认、联调通过、UAT通过）
- 删除里程碑（质保期短的项目可合并验收+质保）
- 修改里程碑名称和说明

## 里程碑推算规则

IT/软件交付里程碑仅需提供"需求确认"日期，后续自动推算：

- **M1 需求确认**：PM 提供
- **M2 试运行完成**：M1 + 3个月
- **M3 验收通过**：M2 + 半个月
- **M4 质保完成**：M3 + 1年

## 里程碑字段

| 字段 | 必填 | 说明 |
|------|------|------|
| 里程碑名称 | ✅ | — |
| 计划完成日期 | ✅ | — |
| 实际完成日期 | ❌ | 完成后填写 |
| 状态 | 自动 | 由系统根据日期和规则自动判定 |
| 备注 | ❌ | 补充说明 |

path: references/requirements.md
content: # 实施项目经理 Skills — 需求说明

## 1. 概述

**Skills 名称：** `impl-project-manager`
**适用角色：** IT/软件交付项目的唯一实施项目经理（PM）
**核心目标：** 将实施项目管理的经验固化为可复用的工作流，通过交互式引导生成结构化 markdown 文档，覆盖项目管理、主合同回款、采购分包付款三大领域。

## 2. 设计原则

| 原则 | 说明 |
|------|------|
| 敏感数据隔离 | 不存储/不生成签约方账户、银行信息、个人隐私等敏感数据 |
| 里程碑驱动 | 以里程碑为核心跟踪单元，所有进度、回款、付款均绑定里程碑 |
| 交互式引导 | 通过提问方式逐步填充项目信息，而非要求用户一次性提供 |
| Markdown 输出 | 所有文档输出为中文 markdown，方便在微信、文档工具中分享 |
| 独立运行 | 不依赖外部系统，可链接本地微信/文档工具进行分发 |

## 3. 模块设计

### 3.1 模块一：项目管理

#### 3.1.1 项目立项

项目启动时通过交互式提问收集以下信息：

| 字段 | 必填 | 说明 |
|------|------|------|
| 项目名称 | ✅ | 项目全称 |
| 客户 | ✅ | 客户方名称 |
| 预算 | ✅ | 项目总预算（万元） |
| 关键干系人 | ✅ | 客户方对接人 + 我方交付负责人 + 我方实施经理 |
| 项目范围 | ✅ | 交付范围概述 |
| 计划工期 | ❌ | 预计起止日期 |

#### 3.1.2 里程碑管理

**默认里程碑模板（IT/软件交付）：**

```
需求确认 → 试运行完成 → 验收通过 → 质保完成
```

- 每个里程碑包含：名称、计划完成日期、实际完成日期、状态（未开始/进行中/已完成/已延期）
- 支持在默认模板基础上增删里程碑
- 里程碑状态自动流转：前置里程碑完成后，后继里程碑自动变为"未开始→进行中"

#### 3.1.3 风险管理

**自动风险识别规则：**

| 规则 | 触发条件 | 风险等级 |
|------|----------|----------|
| 里程碑延期 | 里程碑计划日期已过且未完成 | 高 |
| 里程碑即将到期 | 距计划完成日期 ≤7 天且未完成 | 中 |
| 需求变更频发 | 单个里程碑内变更记录 ≥3 条 | 中 |
| 预算超支风险 | 已发生成本 > 预算 × 80% | 高 |

- 风险自动标记后，PM 可补充风险描述和应对措施
- 支持手动添加风险条目

#### 3.1.4 变更管理

每次变更记录：

| 字段 | 说明 |
|------|------|
| 变更编号 | 自动递增 |
| 变更描述 | 变更内容概述 |
| 提出方 | 客户/我方 |
| 提出日期 | — |
| 影响评估 | 对成本、工期、范围的影响分析 |
| 处理状态 | 待评估/已批准/已拒绝/已实施 |
| 完成日期 | 实施完成日期（可选） |

#### 3.1.5 项目结项

项目关闭时生成结项复盘文档，包含：
- 里程碑完成情况汇总
- 风险与变更统计
- 经验教训总结（PM 手动填写）

---

### 3.2 模块二：主合同签订与回款

#### 3.2.1 合同状态跟踪

**合同生命周期：**

```
谈判中 → 已签约 → 生效 → 履约中 → 验收 → 结算 → 关闭
```

- 一个项目通常一份主合同，补充协议场景较少
- 跟踪字段：合同编号、合同名称、合同金额、签约日期、当前状态

#### 3.2.2 回款计划模板

回款按里程碑绑定，每个里程碑对应回款比例和金额：

| 里程碑 | 回款比例 | 计划回款金额 | 计划回款日期 | 实际回款金额 | 实际回款日期 | 状态 |
|--------|----------|--------------|--------------|--------------|--------------|------|
| 需求确认 | 30% | — | — | — | — | 未回款 |
| 试运行完成 | 40% | — | — | — | — | 未回款 |
| 验收通过 | 25% | — | — | — | — | 未回款 |
| 质保完成 | 5% | — | — | — | — | 未回款 |

- 比例以合同实际约定为准，上表为默认参考
- 计划回款金额 = 合同金额 × 回款比例（自动计算）
- 状态自动流转：实际回款金额 ≥ 计划回款金额 → 已回款

#### 3.2.3 回款预警

**两层预警机制：**

| 预警级别 | 触发条件 | 标记 |
|----------|----------|------|
| 即将到期 | 距计划回款日期 ≤7 天且未回款 | 🟡 |
| 已逾期 | 超过计划回款日期且未回款 | 🔴 |

---

### 3.3 模块三：采购分包与付款

#### 3.3.1 分包类型

支持以下分包类型（不排除其他）：

| 类型 | 说明 |
|------|------|
| 人力外包 | 驻场开发/测试等纯人力投入 |
| 专业分包 | 某模块/子系统整体外包 |
| 设备采购 | 硬件设备采购 |
| 软件采购 | 软件许可/订阅采购 |

#### 3.3.2 分包流程

```
询价/招标 → 评标 → 签约 → 实施 → 验收 → 结算
```

#### 3.3.3 分包商跟踪

| 字段 | 说明 |
|------|------|
| 分包商名称 | — |
| 分包类型 | 人力外包/专业分包/设备采购/软件采购/其他 |
| 分包范围 | 承担的工作范围 |
| 合同金额 | — |
| 交付状态 | 实施中/已交付/已验收/已结算 |
| 付款进度 | 见 3.3.4 |

#### 3.3.4 付款进度

付款与分包商交付节点绑定：

| 交付节点 | 付款比例 | 计划付款金额 | 计划付款日期 | 实际付款金额 | 实际付款日期 | 状态 |
|----------|----------|--------------|--------------|--------------|--------------|------|
| 签约 | 30% | — | — | — | — | 未付款 |
| 实施完成 | 40% | — | — | — | — | 未付款 |
| 验收通过 | 25% | — | — | — | — | 未付款 |
| 结算完成 | 5% | — | — | — | — | 未付款 |

- 比例以分包合同实际约定为准
- 计划付款金额 = 分包合同金额 × 付款比例（自动计算）

#### 3.3.5 付款预警

与回款预警机制一致：即将到期（🟡）+ 已逾期（🔴）

---

## 4. 文档输出

### 4.1 生成时机

按里程碑推进时逐步生成/更新文档：
- 项目启动 → 生成项目计划书
- 里程碑完成 → 更新进度报告
- 合同状态变更 → 更新合同跟踪表
- 随时可生成当前状态快照

### 4.2 输出文档清单

| 文档 | 触发时机 | 内容 |
|------|----------|------|
| 项目计划书 | 项目启动 | 立项信息 + 里程碑计划 + 风险清单 |
| 进度周报 | 定期/按需 | 里程碑状态 + 风险 + 变更 |
| 合同回款跟踪表 | 合同/回款变更 | 合同状态 + 回款计划与实际 + 预警 |
| 分包付款跟踪表 | 分包/付款变更 | 分包商状态 + 付款计划与实际 + 预警 |
| 项目状态快照 | 随时 | 以上全部的当前状态汇总 |
| 结项复盘报告 | 项目关闭 | 完成情况 + 风险变更统计 + 经验教训 |

### 4.3 输出格式

- 中文 markdown
- 表格使用 markdown table
- 预警使用 emoji 标记（🟡🔴）

## 5. 交互流程

### 5.1 新项目启动

```
1. 提问：项目名称？
2. 提问：客户名称？
3. 提问：项目预算（万元）？
4. 提问：客户方对接人？
5. 提问：我方交付负责人？
6. 提问：我方实施经理？
7. 提问：项目范围概述？
8. 提问：计划起止日期？
9. 确认里程碑模板（默认/自定义）
10. 生成项目计划书
```

### 5.2 合同回款录入

```
1. 提问：合同编号？
2. 提问：合同名称？
3. 提问：合同金额？
4. 提问：签约日期？
5. 提问：各里程碑回款比例（逐个确认，默认按模板）
6. 生成合同回款跟踪表
```

### 5.3 分包录入

```
1. 提问：分包商名称？
2. 提问：分包类型？
3. 提问：分包范围？
4. 提问：合同金额？
5. 提问：各交付节点付款比例（逐个确认，默认按模板）
6. 生成分包付款跟踪表
```

## 6. 不包含的内容

- ❌ 签约方银行账户信息
- ❌ 个人身份证号/联系方式等隐私
- ❌ 分包商绩效评价
- ❌ 与外部 ERP/OA 系统的集成接口


path: references/risk-rules.md
content: # 风险识别规则

## 自动风险识别

| 规则ID | 规则名称 | 触发条件 | 风险等级 | 建议应对 |
|--------|----------|----------|----------|----------|
| R001 | 里程碑延期 | 里程碑计划完成日期已过且状态≠已完成 | 🔴 高 | 评估延期影响，调整后续计划，通知干系人 |
| R002 | 里程碑即将到期 | 距计划完成日期 ≤7 天且状态≠已完成 | 🟡 中 | 加速推进，确认能否按期完成 |
| R003 | 需求变更频发 | 单个里程碑内变更记录 ≥3 条 | 🟡 中 | 评估变更管理流程，考虑变更冻结 |
| R004 | 预算超支风险 | 已发生成本 > 预算 × 80% | 🔴 高 | 梳理剩余工作成本，申请预算追加或缩减范围 |

## 风险等级定义

| 等级 | 标记 | 含义 |
|------|------|------|
| 高 | 🔴 | 需立即处理，可能影响项目目标达成 |
| 中 | 🟡 | 需关注，可能影响进度或质量 |
| 低 | 🟢 | 已知风险，影响可控 |

## 风险记录模板

每次识别或手动添加的风险记录：

| 字段 | 说明 |
|------|------|
| 风险编号 | R + 三位数字，自动递增 |
| 风险描述 | 风险内容概述 |
| 风险等级 | 高/中/低 |
| 触发规则 | 自动识别时填写规则ID，手动添加为"手动" |
| 关联里程碑 | 相关里程碑名称 |
| 应对措施 | PM 填写的应对计划 |
| 状态 | 待处理/处理中/已关闭 |
| 发现日期 | 自动/手动 |
| 关闭日期 | 风险关闭时填写 |

path: references/subcontract-types.md
content: # 分包类型参考

## 标准分包类型

| 类型代码 | 类型名称 | 说明 | 典型场景 |
|----------|----------|------|----------|
| LABOR | 人力外包 | 驻场开发/测试等纯人力投入 | 需要额外开发人员补充团队 |
| MODULE | 专业分包 | 某模块/子系统整体外包 | 专业领域交由专业团队 |
| HW | 设备采购 | 硬件设备采购 | 服务器、网络设备、终端等 |
| SW | 软件采购 | 软件许可/订阅采购 | 数据库许可、中间件、云服务订阅 |
| OTHER | 其他 | 不属于以上分类的分包 | 视项目实际情况 |

## 分包付款模式

**分包付款按项目里程碑进度绑定，与主合同回款节点对齐：**

| 里程碑 | 说明 | 付款日期 |
|--------|------|----------|
| M1 需求确认 | 按分包商约定比例付款 | 与 M1 计划完成日期一致 |
| M2 试运行完成 | 按分包商约定比例付款 | 与 M2 计划完成日期一致 |
| M3 验收通过 | 按分包商约定比例付款 | 与 M3 计划完成日期一致 |
| M4 质保完成 | 按分包商约定比例付款 | 与 M4 计划完成日期一致 |

**计算规则：** 计划付款金额 = 分包合同金额 × 该里程碑付款比例

**每个分包商需填写4个里程碑的付款比例（各分包商比例可能不同）**

## 分包流程状态

```
INQUIRY  (询价/招标) → EVALUATION (评标) → SIGNED (签约)
→ IMPLEMENTING (实施) → ACCEPTED (验收) → SETTLED (结算)
```

## 预警规则

与主合同回款预警机制一致：
- 🟡 即将到期：距计划付款日期 ≤7天且未付款
- 🔴 已逾期：超过计划付款日期且未付款

path: scripts/calc_engine.py
content: #!/usr/bin/env python3
"""项目管理计算引擎 — 根据飞书表格数据自动计算"""

import json
import sys
import os
from datetime import datetime, timedelta
from dateutil.relativedelta import relativedelta

# 里程碑推算规则
MILESTONE_RULES = {
    "M1": {"name": "需求确认", "offset": None},        # 由 PM 提供
    "M2": {"name": "试运行完成", "offset": relativedelta(months=3)},  # M1 + 3个月
    "M3": {"name": "验收通过", "offset": timedelta(days=15)},        # M2 + 半个月
    "M4": {"name": "质保完成", "offset": relativedelta(years=1)},    # M3 + 1年
}


def calc_milestone_dates(m1_date_str):
    """
    根据需求确认日期自动推算所有里程碑日期

    Args:
        m1_date_str: 需求确认日期（格式: YYYY-MM-DD 或 YYYY年M月D日）

    Returns:
        dict: 各里程碑的计划完成日期
    """
    # 解析日期
    for fmt in ["%Y-%m-%d", "%Y年%m月%d日", "%Y.%m.%d", "%Y/%m/%d"]:
        try:
            m1_date = datetime.strptime(m1_date_str.strip(), fmt)
            break
        except ValueError:
            continue
    else:
        raise ValueError(f"无法解析日期: {m1_date_str}")

    dates = {}
    prev_date = m1_date

    for key, rule in MILESTONE_RULES.items():
        if rule["offset"] is None:
            dates[key] = {"name": rule["name"], "date": m1_date}
        else:
            calc_date = prev_date + rule["offset"]
            dates[key] = {"name": rule["name"], "date": calc_date}
            prev_date = calc_date

    return dates


def calc_payment_amounts(contract_amount, ratios):
    """
    根据合同金额和回款比例自动计算回款金额

    Args:
        contract_amount: 合同金额（万元）
        ratios: 各里程碑回款比例列表 [M1比例, M2比例, M3比例, M4比例]

    Returns:
        list: 各里程碑回款金额（万元）
    """
    amounts = []
    for r in ratios:
        amount = round(contract_amount * r / 100, 4)
        amounts.append(amount)
    return amounts


def calc_payment_status(planned, actual):
    """
    根据计划金额和实际金额判断回款/付款状态

    Args:
        planned: 计划金额
        actual: 实际金额

    Returns:
        str: 状态文本
    """
    if actual <= 0:
        return "未回款"
    elif actual < planned:
        return "部分回款"
    else:
        return "已回款"


def calc_payment_warning(planned_date_str, actual_amount, planned_amount):
    """
    根据计划回款日期和实际金额判断预警级别

    Args:
        planned_date_str: 计划回款日期
        actual_amount: 实际回款金额
        planned_amount: 计划回款金额

    Returns:
        str: 预警标记
    """
    if actual_amount >= planned_amount:
        return "✅"

    now = datetime.now()

    for fmt in ["%Y-%m-%d", "%Y年%m月%d日", "%Y.%m.%d", "%Y/%m/%d"]:
        try:
            planned_date = datetime.strptime(planned_date_str.strip(), fmt)
            break
        except ValueError:
            continue
    else:
        return "—"

    days_diff = (planned_date - now).days

    if days_diff < 0:
        return "🔴 已逾期"
    elif days_diff <= 7:
        return "🟡 即将到期"
    else:
        return "—"


def calc_milestone_status(planned_date_str, actual_date_str=None):
    """
    根据计划完成日期和实际完成日期判断里程碑状态

    Args:
        planned_date_str: 计划完成日期
        actual_date_str: 实际完成日期（可选）

    Returns:
        str: 状态标记
    """
    if actual_date_str and actual_date_str.strip():
        return "✅ 已完成"

    now = datetime.now()

    for fmt in ["%Y-%m-%d", "%Y年%m月%d日", "%Y.%m.%d", "%Y/%m/%d"]:
        try:
            planned_date = datetime.strptime(planned_date_str.strip(), fmt)
            break
        except ValueError:
            continue
    else:
        return "⬜ 未开始"

    days_diff = (planned_date - now).days

    if days_diff < 0:
        return "🔴 已延期"
    elif days_diff <= 7:
        return "🟦 即将到期"
    else:
        return "🟦 进行中"


def calc_risks(milestones, changes_count_by_milestone, budget_used, budget_total):
    """
    自动识别风险

    Args:
        milestones: 里程碑数据列表
        changes_count_by_milestone: 各里程碑变更数
        budget_used: 已用预算
        budget_total: 总预算

    Returns:
        list: 识别到的风险列表
    """
    risks = []

    for m in milestones:
        # R001: 里程碑延期
        if m.get("status") in ["🔴 已延期"]:
            risks.append({
                "rule": "R001",
                "desc": f"{m['name']}已延期",
                "level": "🔴高",
                "milestone": m["name"]
            })

        # R002: 里程碑即将到期
        if m.get("status") in ["🟦 即将到期"]:
            risks.append({
                "rule": "R002",
                "desc": f"{m['name']}即将到期",
                "level": "🟡中",
                "milestone": m["name"]
            })

        # R003: 需求变更频发
        m_name = m.get("name", "")
        change_count = changes_count_by_milestone.get(m_name, 0)
        if change_count >= 3:
            risks.append({
                "rule": "R003",
                "desc": f"{m_name}变更频发（{change_count}次）",
                "level": "🟡中",
                "milestone": m_name
            })

    # R004: 预算超支风险
    if budget_total > 0 and budget_used > budget_total * 0.8:
        risks.append({
            "rule": "R004",
            "desc": f"预算超支风险（已用{budget_used}/{budget_total}，超过80%）",
            "level": "🔴高",
            "milestone": "全局"
        })

    return risks


if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("用法:")
        print("  python calc_engine.py milestones <M1日期>")
        print("  python calc_engine.py payments <合同金额> <比例1> <比例2> <比例3> <比例4>")
        print("  python calc_engine.py status <计划日期> [实际日期]")
        print("  python calc_engine.py warning <计划日期> <实际金额> <计划金额>")
        sys.exit(1)

    action = sys.argv[1]

    if action == "milestones":
        result = calc_milestone_dates(sys.argv[2])
        for k, v in result.items():
            print(f"{k} {v['name']}: {v['date'].strftime('%Y-%m-%d')}")

    elif action == "payments":
        amount = float(sys.argv[2])
        ratios = [float(x) for x in sys.argv[3:7]]
        amounts = calc_payment_amounts(amount, ratios)
        for i, a in enumerate(amounts):
            print(f"M{i+1}: {ratios[i]}% → {a} 万元")

    elif action == "status":
        planned = sys.argv[2]
        actual = sys.argv[3] if len(sys.argv) > 3 else None
        print(calc_milestone_status(planned, actual))

    elif action == "warning":
        planned = sys.argv[2]
        actual = float(sys.argv[3])
        planned_amt = float(sys.argv[4])
        print(calc_payment_warning(planned, actual, planned_amt))

    else:
        print(f"未知动作: {action}", file=sys.stderr)
        sys.exit(1)

path: scripts/feishu_api.py
content: #!/usr/bin/env python3
"""飞书 API 基础工具 — 获取 tenant_access_token"""

import json
import os
import sys
import time
import urllib.request
import urllib.error

# 飞书应用配置
APP_ID = os.environ.get("FEISHU_APP_ID", "cli_aa9d0e7045f81cc8")
APP_SECRET = os.environ.get("FEISHU_APP_SECRET", [REDACTED_SECRET])
TOKEN_URL = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal"

# Token 缓存文件
TOKEN_CACHE = os.path.join(os.path.dirname(__file__), ".feishu_token_cache")


def get_token(force=False):
    """获取 tenant_access_token，带缓存和自动刷新"""
    # 读缓存
    if not force and os.path.exists(TOKEN_CACHE):
        with open(TOKEN_CACHE, "r") as f:
            cache = json.load(f)
        if cache.get("expire_time", 0) > time.time() + 300:  # 提前5分钟刷新
            return cache["token"]

    # 请求新 token
    data = json.dumps({
        "app_id": APP_ID,
        "app_secret": APP_SECRET
    }).encode("utf-8")

    req = urllib.request.Request(
        TOKEN_URL,
        data=data,
        headers={"Content-Type": "application/json; charset=utf-8"},
        method="POST"
    )

    try:
        with urllib.request.urlopen(req, timeout=10) as resp:
            result = json.loads(resp.read().decode("utf-8"))
    except urllib.error.HTTPError as e:
        print(f"❌ 获取 token 失败: HTTP {e.code}", file=sys.stderr)
        print(e.read().decode("utf-8"), file=sys.stderr)
        sys.exit(1)

    if result.get("code") != 0:
        print(f"❌ 获取 token 失败: {result}", file=sys.stderr)
        sys.exit(1)

    [REDACTED_SECRET]"tenant_access_token"]
    expire_time = time.time() + result.get("expire", 7200)

    # 写缓存
    with open(TOKEN_CACHE, "w") as f:
        json.dump({"token": token, "expire_time": expire_time}, f)

    return token


def api_request(url, method="GET", body=None):
    """通用飞书 API 请求"""
    [REDACTED_SECRET])
    headers = {
        "Authorization": f"Bearer {token}",
        "Content-Type": "application/json; charset=utf-8"
    }

    data = json.dumps(body).encode("utf-8") if body else None
    req = urllib.request.Request(url, data=data, headers=headers, method=method)

    try:
        with urllib.request.urlopen(req, timeout=15) as resp:
            return json.loads(resp.read().decode("utf-8"))
    except urllib.error.HTTPError as e:
        error_body = e.read().decode("utf-8")
        print(f"❌ API 请求失败: {method} {url}", file=sys.stderr)
        print(f"   HTTP {e.code}: {error_body}", file=sys.stderr)
        sys.exit(1)


if __name__ == "__main__":
    action = sys.argv[1] if len(sys.argv) > 1 else "token"

    if action == "token":
        print(get_token())
    elif action == "test":
        # 测试 API 连通性
        [REDACTED_SECRET])
        print(f"✅ Token 获取成功: {token[:20]}...")
    else:
        print(f"未知动作: {action}", file=sys.stderr)
        sys.exit(1)

path: scripts/feishu_sheet.py
content: #!/usr/bin/env python3
"""飞书表格操作 — 读取和更新项目管理在线表格"""

import json
import os
import sys
import urllib.parse

# 导入基础 API
sys.path.insert(0, os.path.dirname(__file__))
from feishu_api import api_request, get_token

# 飞书表格 API
SHEET_API_BASE = "https://open.feishu.cn/open-apis/sheets/v3/spreadsheets"


def read_sheet(spreadsheet_token, sheet_id=None, range_str=None):
    """
    读取飞书在线表格数据

    Args:
        spreadsheet_token: 表格 token（从表格 URL 获取）
        sheet_id: 工作表 ID（可选，默认第一个）
        range_str: 读取范围（如 "A1:F10"），可选
    """
    url = f"{SHEET_API_BASE}/{spreadsheet_token}/values"

    params = {}
    if sheet_id:
        params["sheetId"] = sheet_id
    if range_str:
        if sheet_id:
            params["range"] = f"{sheet_id}!{range_str}"
        else:
            params["range"] = range_str

    if params:
        url += "?" + urllib.parse.urlencode(params)

    result = api_request(url)
    return result


def write_sheet(spreadsheet_token, sheet_id, range_str, values):
    """
    写入飞书在线表格数据

    Args:
        spreadsheet_token: 表格 token
        sheet_id: 工作表 ID
        range_str: 写入范围（如 "A1:F10"）
        values: 二维数组数据
    """
    url = f"{SHEET_API_BASE}/{spreadsheet_token}/values"

    body = {
        "valueRange": {
            "range": f"{sheet_id}!{range_str}" if sheet_id else range_str,
            "values": values
        }
    }

    result = api_request(url, method="PUT", body=body)
    return result


def batch_update(spreadsheet_token, requests):
    """
    批量更新表格（设置格式、公式等）

    Args:
        spreadsheet_token: 表格 token
        requests: 批量请求列表
    """
    url = f"{SHEET_API_BASE}/{spreadsheet_token}/batchUpdate"

    body = {"requests": requests}
    result = api_request(url, method="POST", body=body)
    return result


def create_spreadsheet(title):
    """
    创建新飞书表格，并自动设置编辑权限

    Args:
        title: 表格标题

    Returns:
        dict: 包含 spreadsheet_token, url, folder_token 等
    """
    # 创建表格
    result = api_request(SHEET_API_BASE, method="POST", body={"title": title})
    if result.get("code") != 0:
        return result

    spreadsheet = result.get("data", {}).get("spreadsheet", {})
    sp_token = spreadsheet.get("spreadsheet_token")

    # 自动设置编辑权限：组织内可编辑
    set_edit_permission(sp_token)

    return result


def set_edit_permission(spreadsheet_token):
    """
    设置表格为组织内可编辑权限

    Args:
        spreadsheet_token: 表格 token

    Returns:
        bool: 是否设置成功
    """
    import requests

    [REDACTED_SECRET])
    url = f"https://open.feishu.cn/open-apis/drive/v1/permissions/{spreadsheet_token}/public?type=sheet"
    resp = requests.patch(
        url,
        headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
        json={"link_share_entity": "tenant_editable"},
        timeout=10,
    )
    result = resp.json()
    success = result.get("code") == 0
    if not success:
        print(f"⚠️ 设置编辑权限失败: {result.get('msg')}", file=sys.stderr)
    return success


def create_project_sheets(spreadsheet_token):
    """
    为项目表格创建标准5个Sheet（项目总览、合同回款、分包付款、变更记录、风险清单）
    并自动设置编辑权限

    Args:
        spreadsheet_token: 表格 token

    Returns:
        dict: {sheet_name: sheet_id}
    """
    sheet_names = ["合同回款", "分包付款", "变更记录", "风险清单"]
    sheet_ids = {}

    for name in sheet_names:
        result = api_request(
            f"{SHEET_API_BASE}/{spreadsheet_token}/sheets",
            method="POST",
            body={"title": name},
        )
        if result.get("code") == 0:
            sheet_ids[name] = result["data"]["sheet"]["sheet_id"]

    return sheet_ids


def list_sheets(spreadsheet_token):
    """获取表格中的所有工作表信息"""
    url = f"{SHEET_API_BASE}/{spreadsheet_token}/sheets"
    result = api_request(url)
    return result


if __name__ == "__main__":
    if len(sys.argv) < 3:
        print("用法:")
        print("  python feishu_sheet.py read <spreadsheet_token> [range]")
        print("  python feishu_sheet.py list <spreadsheet_token>")
        print("  python feishu_sheet.py write <spreadsheet_token> <sheet_id> <range> <json_data>")
        sys.exit(1)

    action = sys.argv[1]
    [REDACTED_SECRET]

    if action == "read":
        range_str = sys.argv[3] if len(sys.argv) > 3 else None
        result = read_sheet(token, range_str=range_str)
        print(json.dumps(result, ensure_ascii=False, indent=2))

    elif action == "list":
        result = list_sheets(token)
        print(json.dumps(result, ensure_ascii=False, indent=2))

    elif action == "write":
        sheet_id = sys.argv[3]
        range_str = sys.argv[4]
        data = json.loads(sys.argv[5])
        result = write_sheet(token, sheet_id, range_str, data)
        print(json.dumps(result, ensure_ascii=False, indent=2))

    else:
        print(f"未知动作: {action}", file=sys.stderr)
        sys.exit(1)

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