# Law Firm Worklog

> 律所工时月报生成（通用版，多数据源）。从用户已配置的任务管理工具（滴答清单 / Notion / Microsoft To Do / 飞书任务模块，任选其一）拉取指定日期范围内已完成的"业务工作"任务，按用户首次配置的工时模板（列名、固定值、姓名）和项目案号注册表（项目名、案号、关键词）自动归类并生成律所工时系统可导入的 Excel（默认 .xlsx）或 CSV。首次运行会引导用户完成一次性配置（数据源选择 + 工作单元白名单、姓名/律所、列模板、项目案号清单），后续可随时增删项目。当用户提到"工时""工作日志""月度工时""填工时""导出工时""律所工时""X月工时"等时触发，不论是否提及具体律所或工具。

- Skill: `cslawyer1985/law-firm-worklog` (Agent Skill, multi-file: 8 files)
- Install (CLI): `npx skillmds@latest add cslawyer1985/law-firm-worklog`
- Raw SKILL.md: https://api.skillmd.com/api/skills/cslawyer1985/law-firm-worklog/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Data & Analytics
- Author: cslawyer1985 (https://skillmd.com/u/cslawyer1985)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/cslawyer1985/law-firm-worklog

---


# 律所工时月报生成 Skill（通用版，多数据源）

把"已完成任务"转成"律所工时系统 Excel / CSV"的工作流，适用于任何律所、使用以下任一任务工具的律师：

| 数据源 | 适配器文件 |
|--------|----------|
| 滴答清单 / TickTick | `references/sources/dida365.md` |
| Notion | `references/sources/notion.md` |
| Microsoft To Do | `references/sources/todo.md` |
| 飞书 / Lark | `references/sources/feishu.md` |

主流程（本文件）数据源无关：所有源在 B3 出口都归一化成 `{date, title, project_key, is_all_day}` 四元组，下游 B4/B5 统一处理。源专属细节（MCP 工具名、字段映射、时区处理、首次配置追问脚本）都封装在对应 `references/sources/<source>.md` 中。

## 总览

本 skill 把工作流拆成两类调用：

- **首次配置**（user-config 目录为空）：引导用户选数据源、录入律所工时表头、姓名、固定值、工作单元白名单、项目案号注册表。
- **生成工时**（已配置过）：读取配置，按 source 字段加载适配器，拉任务，归类，生成 Excel（默认）或 CSV。

另外随时支持：
- **新增/删减项目** — 用户说"新签了 X 项目，案号 …"或"删掉某项目"时，原地编辑 `user-config/projects.csv`。
- **修改个人信息 / 工作单元白名单** — 用户说"换律所了""改姓名""新增工作 list/database/project"时，编辑 `user-config/profile.md`，源专属字段格式见对应 `references/sources/<source>.md`。

## 起手判断

进入 skill 后按以下决策树走，不跳步：

```
user-config/profile.md 存在且完整？
├─ 否 → 流程 A（首次配置）；其中第二组确定 source。
└─ 是 → 读取 profile.md 的 `source` 字段（缺省 = dida365，向后兼容老配置）
         → 加载 references/sources/<source>.md
         → 按其"MCP 工具集"小节验证工具可见性
            ├─ 工具不可见 → 输出该 adapter 的"工具不可见时的提示文案"，停
            ├─ 工具可见但鉴权失败（401/403）→ 输出该 adapter 的鉴权失败提示，停
            └─ 工具就绪 → 流程 B（生成工时）
```

绝不在此阶段假设默认姓名、默认列名、默认 source——没有用户认可的配置就不能生成任何输出文件。

---

## 流程 A：首次配置

详细脚本见 `references/setup.md`。要点：

1. 一次性向用户收齐以下信息（一组一组问，不要一次甩一长串）。**能用规则推断的字段不要主动问用户，只在用户 review 时纠正**——这条仅适用于工时模板的列分类启发式，**不适用于数据源 schema**（见下方约束）。

   - **第一组：工时模板** — 列名 + 启发式分类（保留不变；启发式分类详见 `references/setup.md` 的步骤 1-B 表格）。
   - **第二组：数据源 + 工作单元白名单** — 先问用户用什么任务工具（`dida365` / `notion` / `todo` / `feishu`），然后**加载 `references/sources/<source>.md` 中的"首次配置脚本"小节**走源专属问答。每个源的"工作单元"概念不同：dida 是 project，notion 是 database，todo 是 task list，feishu 是任务容器（任务列表或全部）。
   - **第三组：项目案号注册表** — 项目名 + 案号（关键词默认 = 项目名本身），数据源无关。

2. 把收集到的信息写入 `user-config/` 下三个文件：
   - `profile.md` — 包含 `source:` 字段、工作单元白名单（按 source 不同格式不同）、列分类表（启发式分类结果 + A 类各列固定值，含律师姓名）
   - `template.csv` 或 `template.xlsx` — 用户律所工时系统的表头（用户给什么格式就存什么格式）
   - `projects.csv` — 项目案号注册表，列固定为：`项目名,案号,客户,关键词`

3. 写完后输出"配置已保存到 …，下次直接说'生成 X 月工时'即可"。

**关键约束**：
- 工时模板列名启发式分类**允许**（用户 review 时纠正）。
- 数据源 schema **不允许**启发式（Notion / 飞书的 database / 多维表格 schema 用户自定义，必须显式问清"哪个字段是状态、哪个是完成时间、状态等于什么值算完成"，绝不靠列名猜）。
- 项目案号绝不虚构。关键词允许默认（项目名本身），不允许凭空编造其他词。

---

## 流程 B：生成工时

### B1. 确认日期范围

向用户确认起止日（如 `2026-05-01` 到 `2026-05-24`）。如果用户只说"5月工时"，按当月 1 日到今天（或当月末，看上下文）处理，并复述给用户确认。

### B1.5 检查输出文件是否已存在

生成前检查默认输出路径（`YYYY-MM-工作日志.xlsx`）：

- **文件不存在** → 继续 B2，正常生成
- **文件已存在** → 提示用户："已存在 `YYYY-MM-工作日志.xlsx`（上次生成于 <文件修改时间>）。是否覆盖？还是生成带时间戳的新文件（如 `YYYY-MM-工作日志-v2.xlsx`）？"

不要静默覆盖——用户可能已经在旧文件里手填了小时数。

### B2. 拉指定日期范围内已完成任务

按 `profile.md` 的 `source` 字段加载 `references/sources/<source>.md`，执行其中的 **"B2 数据拉取"** 小节。

跨源共同约束（每个 adapter 都必须遵守）：

- **绝不全量拉取再筛选**。必须在 API 层用工作单元 ID（projectIds / database_id / list_id / table_id）过滤。理由：用户的"个人/学习/还款"任务混在同一账户里，全量拉会把婚礼、还款日、课程都拖进工时；大库还会触发分页/限速，慢且容易漏。
- **分页**：返回结果如带 `hasMore` / `has_more` / `nextCursor` / `next_cursor` / `@odata.nextLink` 等标记，必须补拉至全部，再进入 B3。不要静默丢弃超出首页的任务。

如果用户说"我新建了一个工作单元（项目/list/database/table）"或拉回来的数据明显少了，先按 adapter 的"流程 C"小节复核白名单，必要时让用户确认后更新 `profile.md`。

### B3. 字段归一化

按 `profile.md` 的 `source` 字段加载 `references/sources/<source>.md`，执行其中的 **"B3 字段归一化"** 小节。

每条任务必须归一化成以下统一四元组（下游 B4/B5 只读这层，不再触及源专属字段）：

```json
{
  "date":        "YYYY-MM-DD",   // 北京时间（UTC+8）日期
  "title":       "任务标题",
  "project_key": "<source 内的工作单元 ID 或子分类值>",
  "is_all_day":  true | false
}
```

各源对时区、全天事件的处理差异详见对应 adapter 文件，本文件不重复。**注意滴答清单的全天任务 UTC 日期 +1 天的坑由 `dida365.md` 处理**——主流程不需要知道。

> **设计原理**：完整的架构决策说明见末尾"关于为什么这样设计"段落。

### B4. 按项目案号归类

读取 `user-config/projects.csv`，按以下顺序为每条任务分配项目和案号：

1. **关键词匹配** — 任务标题/描述中包含某项目的任一关键词 → 命中该项目。多个项目同时命中时按以下优先级链裁决：

   1. **完全匹配优先**：关键词完整出现在标题中（如标题含"新能源项目"，关键词也是"新能源项目"）> 仅部分匹配（如关键词是"新能源"，仅命中标题中的子串）
   2. **匹配到的关键词字符数多者优先**（减少单字/短词误命中）
   3. **仍冲突 → 问用户**"以下任务同时命中 [项目A] 和 [项目B] 的关键词，请确认归属"

   例：任务"新能源项目 — 审阅EPC合同"同时命中项目 A（关键词"新能源项目"，5字）和项目 B（关键词"新能源"，3字）→ 项目 A 胜出。
2. **匹配不上** — 不要瞎猜，列出来让用户裁定（"以下 N 条无法自动归类，请告知归属项目"）。

   **兜底处理**：
   - 如果用户在合理时间内未回应：将该任务标记为"待确认"，列在输出摘要末尾，并在文件名中加 `(含待确认)` 后缀。
   - 如果用户明确说"随便归到 X"：按用户说的填，但在输出摘要中注明"[已手动指定]"。
   - 如果用户说"这条不算"：跳过该任务，不出现在输出文件中。

### B5. 生成输出文件

> **设计原理**：完整的架构决策说明见末尾"关于为什么这样设计"段落。

**默认生成 Excel（`.xlsx`）**，用户也可以说"导出 CSV"来要 CSV。

#### 输出路径

文件名默认 `YYYY-MM-工作日志.xlsx`（或 `.csv`），保存到当前工作目录。

#### 输出格式选择

- 用户说"生成 X 月工时"不加后缀 → 输出 `.xlsx`
- 用户说"导出 CSV"或"生成 CSV" → 输出 `.csv`
- `user-config/` 下只有 `template.csv`（没有 `.xlsx`）且用户没指定格式 → 输出 `.csv`（尊重历史配置）

**表头来源不受模板格式限制**：无论 `user-config/` 存的是 `template.xlsx` 还是 `template.csv`，都能从中提取列名数组。所以用户给 Excel 模板、要求导出 CSV（或反过来）完全没问题——列名从模板读，输出格式按用户指令选。

#### Excel 生成（`.xlsx`）

调用 skill 自带的 `scripts/generate_worklog.py`，不要手写 openpyxl：

```bash
python <skill-dir>/scripts/generate_worklog.py \
  --input /tmp/worklog_data.json \
  --output "YYYY-MM-工作日志.xlsx" \
  --template "<user-config/template.xlsx 的路径（如有）>"
```

其中 `/tmp/worklog_data.json` 的结构为：

```json
{
  "headers": ["列1", "列2", ...],
  "rows": [["值1", "值2", ...], ...],
  "column_widths": {"日期": 12, "描述": 50}
}
```

- `headers`：按模板列顺序的列名数组
- `rows`：按通用取值规则填充的数据行（二维数组）
- `column_widths`：可选，指定列宽（key 为列名，value 为宽度数值）；不传则脚本用内置默认值（日期 ~12、描述 ~50、其他 ~15）

脚本会自动处理：加粗灰底表头、细线边框、冻结首行、自动筛选。如果提供了 `--template`，列宽优先从模板继承。依赖 `openpyxl`，首次运行前提示用户 `pip install openpyxl`。

#### CSV 生成（`.csv`）

按 `user-config/template.csv` 的表头顺序写每行。编码 **UTF-8 with BOM**，Excel/WPS 双击不乱码。

#### 通用取值规则（Excel 和 CSV 共用）

按首次配置时的列分类取值：
- **A 类（固定值列）**：直接填 profile.md 里配置好的固定值
- **B 类（任务衍生列）**：
  - 日期列 → B3 归一化后的 `date`（北京时间，格式 `YYYY-MM-DD`）
  - 描述/标题列 → B3 归一化后的 `title`
- **C 类（项目映射衍生列）**：用 B4 归类结果（项目名 / 案号 / 客户名）填入
- **D 类（留空待手填列）**：保持空白。理由：用户最了解每条任务实际花了多少时间或其他主观字段，自动估计反而误导；让用户在 Excel 里手填更准。

### B6. 输出摘要

写完后给用户：
- 文件绝对路径及格式（`.xlsx` / `.csv`）
- 总行数（不含表头）
- 项目分布表（项目 / 案号 / 条数）
- 需用户确认的边界条目清单：未匹配上的任务、关键词命中多个项目的任务

---

## 流程 C：增删项目 / 修改个人信息 / 增删工作单元

用户说"新签 X 项目，案号 …，关键词 …" → 追加一行到 `user-config/projects.csv`，让用户确认后保存。
用户说"删掉 X 项目" → 删行后保存。
用户说"改姓名/换律所" → 修改 `profile.md` 的列分类表中对应 A 类列的固定值。
用户说"新增/删除一个工作单元（项目 / list / database / table）" → 加载 `references/sources/<source>.md` 的"流程 C"小节，按源专属格式修改 `profile.md` 的工作单元白名单。

每次修改后回显"已更新，当前共 N 个项目（或 N 个工作单元）"。

---

## 重要约束

1. **绝不全量拉再筛选** —— 所有数据源都必须在 API 层用工作单元 ID 过滤。理由跨源通用：用户的"个人/学习/还款"任务混在同一账户/workspace 里，全量拉会污染工时。
2. **绝不虚构案号** —— 不在 `user-config/projects.csv` 中的项目，先问用户要新案号再写入注册表，再生成输出文件。
3. **绝不在首次配置完成前生成输出文件** —— 没有用户认可的列模板/姓名/案号/source，任何 Excel/CSV 都不能直接用。
4. **绝不对数据源 schema 启发式推断** —— Notion 的 status property、飞书多维表格的状态列、MS To Do 的 list 含义都必须由用户在首次配置时显式给出。工时模板的列名启发式分类是允许的（行业惯例统一），但数据源 schema 完全自定义，不能猜。
5. 不强行预估"自报小时"（若列模板中有此列），保留空白让用户手填。

## 维护

- 用户新签项目/收到新案号 → 追加到 `user-config/projects.csv`
- 用户新增工作单元（任意 source）→ 按对应 adapter 的"流程 C"小节修改 `user-config/profile.md`

## 参考文件

- `references/setup.md` — 首次配置详细脚本（一问一答模板、示例）
- `references/sources/<source>.md` — 各数据源的专属配置脚本与数据拉取细节
- `scripts/generate_worklog.py` — Excel 生成脚本（`openpyxl`），已打包，B5 直接调用，不要手写 openpyxl

## 常见问题排查

| 症状 | 可能原因 | 处理 |
|------|---------|------|
| 任务拉不到 / 数量明显偏少 | 工作单元白名单过时（新建了 project/list/database/table 但 profile.md 没更新） | 按当前 source 的 adapter 列出可选工作单元，让用户确认后更新白名单 |
| 拉不到 / 报鉴权错 | MCP token / OAuth 过期 | 按当前 source 的 adapter 输出鉴权失败提示文案 |
| 多条任务无法自动归类 | projects.csv 缺少该项目或关键词覆盖不足 | 展示未匹配任务列表，问用户归属；确认后追加到 projects.csv |
| Excel 打开乱码 | CSV 忘了加 BOM | 确认写入时用了 `utf-8-sig` 编码（或直接改用 .xlsx 输出，无编码问题） |
| openpyxl 报 `ModuleNotFoundError` | 环境没装 | 提示用户 `pip install openpyxl` |
| 任务日期不对（差一天） | 全天任务时区没正确处理 | 看当前 source 的 adapter 中 B3 节是怎么算 all-day 的（典型如 `dida365.md` 的 UTC+1 天规则） |

## 关于"为什么这样设计"

- **配置和工作流分离**：列模板、姓名、案号都是律所/律师强相关的"私有信息"，写在 user-config 里而非 SKILL.md 里，是为了让一个 skill 文件能服务任何律所，配置由用户自己管理。
- **数据源适配器分离**：每个 source 的 MCP 工具名、字段语义、时区/全天事件处理差异巨大，但工作流的骨架（列分类、关键词匹配、Excel 生成）完全数据源无关。把源耦合点抽到 `references/sources/<source>.md`，主流程保持薄一层，新增源 = 加一个 reference 文件。
- **不预设默认值**：律所工时表头差异极大（有的 5 列、有的 8 列、有的有"客户"列没有"项目"列），任何"默认表头"都会让用户拿到的文件导不进系统。宁可第一次多问几个问题，也不要默认值导致后续返工。同理：用户的 Notion database / 飞书多维表格 schema 也千差万别，绝不替用户猜状态/完成时间字段。
- **首跑严格走完配置**：相比"边用边补"，一次性把配置走完，后续每月只需一句"生成 5 月工时"就能完成，体验更稳。
