---
name: tadado-activity-import
version: "2.2.0"
description: |
  把 py 版（v0.x）「活动分析」导出的 md 清单（标签 → 任务 → 活动三层），转换成 Tadado2
  能**直接导入**的 md —— md 进、md 出，不碰数据库、不调 CLI。逐项对账、认不出就停、
  不替用户拍板。触发词：活动分析导出、活动清单转换、旧数据迁移、数据迁入、md 转换。
---

> 📦 **本 skill 是自包含的**：转换器就在 [`scripts/migrate-activity.mjs`](scripts/migrate-activity.mjs)，
> 样例在 [`sample-input.md`](sample-input.md)。把整个 `tadado-activity-import/` 目录拷走就能用 ——
> **不依赖任何仓库**（脚本零依赖，只要机器上有 Node）。
>
> 仓库里这份是**权威源**；要让它在本机生效，把整个目录拷到工作区的 `.claude/skills/` 下即可。
> **不要**在那边改内容：两份必然分叉，改这里。

# py 活动清单 → Tadado2 可直接导入的 md

## 这个 skill 做什么

| | |
|---|---|
| **输入** | 一份从 py 版「活动分析」导出的 `.md` 清单（三层结构：标签 → 任务 → 活动） |
| **输出** | 一份能被 Tadado2 的「**数据迁入**」直接导入的 `.md`，任务与活动一条不少 |

它**只做文本转换**：不读数据库、不碰应用数据、不调任何 CLI、不改任何既有文件（除非显式要写）。
**导入与导出都由用户手动**在两边应用里做 —— 你只负责把中间那份 md 转出来。

## 转换器在哪

**就在本 skill 里**：`scripts/migrate-activity.mjs`（相对本文件的位置）。它是**零依赖纯 ESM**，
`node` / `deno` / `bun` 任何一个都能跑，**不需要 `npm install`**。

先定位本 skill 自己的目录（即本 SKILL.md 所在目录），再按相对路径调它 —— **不要去找别的副本**，
也不要在用户的项目里另找一份。**拿不准目录就问用户**，别乱猜路径。

**为什么自带而不是外链**：skill 要能被**单独分享** —— 别人拿到这个目录就得能跑。
（仓库内那条端到端验收跑的就是**这一份**：`desktop/e2e/smoke.mjs` 调
`resources/skill/tadado-activity-import/scripts/migrate-activity.mjs` 产出真文件、喂进应用的
「数据迁入」、断言「共 3 条 · 0 行认不出」。一份实现，两处验。）

## 铁律

1. **先预演，再写出。** 默认只解析、只对账、不写文件（加 `--apply` 才写）。
2. **认不出就停下问。** 任何不符合已知行型的行都要报出来 —— 不许「看着差不多就跳过」，
   也不许自己猜一种解释。
3. **数字必须平。** `源活动行数 = 输出 + 系统记录过滤 + 重复剔除`（续行并入上一条）。
   平不了就不许写出，先把对不上的查清楚。
4. **不替用户拍板。** 见「必须停下来问用户」一节，每一条都要问过再继续。
5. **跑不了脚本时，不许自己硬转。** 见下一节 —— 这是本 skill 最容易犯的错。

## 用法

在**本 skill 目录**下执行（下面的 `scripts/` 是相对路径，按实际位置给）：

```bash
# 1) 预演（默认不写任何文件）—— 先读这份对账报告
node scripts/migrate-activity.mjs <清单.md>

# 2) 想留档一份报告
node scripts/migrate-activity.mjs <清单.md> --report 报告.json

# 3) 对账全过、待确认项都问过之后，才生成
node scripts/migrate-activity.mjs <清单.md> --apply
#    默认写到清单旁边：<清单>-可导入.md
#    这个名字**会显示在导入对话框里**，也是用户切分区时的对照物，别随手改

# 4) 用户手改过那份输出、确实要按当前源重写
node scripts/migrate-activity.mjs <清单.md> --apply --force
```

想先看看它长什么样，拿自带样例跑一遍（**不写任何文件**）：

```bash
node scripts/migrate-activity.mjs sample-input.md
```

退出码：`0` 干净 · `1` 有待确认项 · `2` 对账失败 / 有认不出的行 / 输出已存在
（后两种都不许往下走）。

**默认不覆盖已有输出**：用户很可能**手改过**那份文件 —— 比如工具报了「标签超过 3 个」，
而它按原则不替用户删，用户就得自己去删到 3 个。重跑 `--apply` 会把那些手改**静默抹掉**，
所以只有确实要按当前源重写时才加 `--force`。

生成成功后脚本会打印**下一步**（切目标分区 → 任务管理 → 数据迁入 → 选那个文件），
把它转述给用户。

## 环境里没有 JS 运行时时怎么办（重要）

**不许自己逐行硬转。** 这种机械转换由 LLM 手工做会**静默丢行** —— 一份真实清单里八成
以上的行是活动行，丢掉是看不出来的，用户只会看到「导进去了」。

两条路，**都必须带对账**：

1. **让用户装一个**（Windows：`winget install OpenJS.NodeJS`），再照上面的用法跑；
2. **走慢路径**：逐行转、逐行报，把「**源 N 行 → 输出 M 行**」的账**摊开给用户看**，
   任何一行没去处就停下问。宁可慢，不许静默。

## 认识的四种行

脚本对两种导出写法都兼容（同一套层次，符号略有差别）：

| 行 | 写法 | 说明 |
|---|---|---|
| 标签 | `#标签名` | 分组标题。清单就是按它分块的 |
| 任务 | `1. 标题` 或 `1. 标题 [待办→逾期, 0%→0%]:` | 序号与摘要可能没有，标题是必须的 |
| 活动 | `    01-05 09:10 内容` 或 `   - 01-05 09:10 内容` | **缩进** + `MM-DD HH:MM` 开头。缩进量与 `- ` 前缀都可选 |
| 续行 | 紧跟在活动后面的非空行 | 活动文本里带换行时会被导成独立行，并回上一条活动，并在报告里逐行列出 |

其余一律算「认不出的行」→ 停下。

## 输出的 md 长什么样

```
- [~] 标题 #标签1 #标签2 ⏰03-20 :: 60%
   - 01-07 10:30 把接口签名对齐了
   - 02-10 16:50 读完第 3 章
```

每一段读成什么 —— 这是**应用自己的写法**（读入口是 `desktop/src/data/markdown.ts` 的
`parseTasks`；批量框用 `parseTasksDetailed`，就是它加一份「认不出的行」清单）：

| 写法 | 含义 |
|---|---|
| `-` 或 `*` 开头 + `[~]` / `[x]`（`[ ]` 也认） | 一条任务；方括号里是状态 |
| `#标签` | 标签，一条任务最多 3 个 |
| `⏰09-21` 或 `⏰09-21 14:30` | 截止（年份按离今天最近的一年推算）；不写就是「无截止」 |
| `:: 40%` | 进度 |
| 缩进 ≥2 格、以 `MM-DD HH:MM` 开头的一行 | **活动**，挂到上一条任务的时间线上 |

- 状态用方括号：`[~]` 进行中 · `[x]` 已完成 · `[ ]` —— **工具产出的是它**，应用读作
  「进行中」（「待办」那一档 2026-09-21 删了）。清单里的「逾期」也落成 `[ ]` ——
  应用会按截止日期自己把它标回逾期
- 活动行**缩进 3 个空格**，紧跟在它的任务下面；时刻保持 `MM-DD HH:MM` 原样
- **只留人写的进展**：旧版自动写下的系统记录被滤掉，见下节
- 活动文本**一个字符都不改**（`[xx]` 这类前缀是原作者的语义，不翻译）

## 滤掉系统记录（默认行为）

旧版会**自动**记下这些（不是人写的），**默认全部滤掉**：

```
[批量创建] 创建任务 1/3        创建
创建任务                       创建
延后处理: 截止时间 A → B        延后
状态变更为 已完成 / 状态 X → Y   状态
进度 0% → 30%                 进度
截止时间 2026-01-31            设截止
```

理由：它们的**结果已经写在任务行上**了（方括号 = 状态、`::` = 进度、`⏰` = 截止），
本身不携带新信息；而且这套写法里的活动行导入后一律是「**人手写的一条进展**」，
把系统记录带进去等于给它挂个人名，还会落进「可改可删」那一类。

⚠️ 截止与状态正是**从这些行里挖出来**的，所以工具是**先挖字段、再滤行** ——
字段不会丢。滤掉多少会报在两处：对账那一行，与报告末尾的清单。

用户想全留着 → 加 `--keep-system`（时间线会很长，但「什么时候完成的」也留着了）。

## 字段从哪来

| 目标字段 | 来源 | 清单里没有时 |
|---|---|---|
| 标题 | 任务行（剥掉序号与状态摘要） | ——（没有标题的行会停下问） |
| 标签 | 分组标题；一条任务出现在多个组时**合并** | 空 |
| 状态 | 任务行摘要的右侧值；没有摘要就翻活动文本里的「状态变更」 | 落 `[ ]`，**应用读作「进行中」**，并在报告里列出 |
| 进度 | 摘要里 `0%→M%` 的 M | 已完成 = 100，其余 = 0 |
| 截止 | 活动文本里 `截止时间 A -> B` 取**最后一次**的 B（带箭头的写法优先于「直接设」的） | 无截止，并在报告里列出 |
| 活动 | 缩进活动行里**人写的那些**，时刻原样；系统记录滤掉 | 应用会给它补一条「导入任务」 |
| **优先级** | **清单里没有这个信息** → 全部落 P2（关注） | —— |
| 起止区间 | `start` = **最早一条活动的日期**（没有活动行 = 导入日）、`end` = 截止 | —— |
| 创建日 | **最早一条活动的日期**（没有活动行 = 导入日） | —— |
| 归档 / 关联 | 清单里没有 → 未归档 / 无关联 | —— |

**创建日 / 起止是应用在导入时算的，不是工具写进 md 的** —— 这套写法里没有能装创建日的字段，
所以**不要去发明一个**：应用会取这条任务**最早一条活动**的日期。为什么不用导入日：迁进来的
任务历史全在活动行里 —— 一律落导入日会让整批任务的「创建于」挤在同一天，而**创建日是这次
迁移里唯一会被整个抹平的时间维度**（截止 / 进度 / 状态都能从活动文本里挖到）。
**精度要对用户说清**：它给的是「**第一次动手**」那天，不是真正的创建时刻（旧版的「创建任务」
记录按上一条的规矩不转移），对「建了长期搁着」的任务会偏晚。`start` 与创建日取**同一个日期**，
否则会出现「创建于 01-05、甘特色条却从 09-20 才开始」这种矛盾。

## 必须停下来问用户

1. **有认不出的行** —— 把行号与原文贴给用户，问这几行是什么。不要自己归类。
2. **有任务挖不到截止** —— 告诉用户这些会以「无截止」导入（不计入「今日到期」），
   并问：这个来源里确实没有它们的截止，还是需要另找来源？
3. **状态无法确定** —— 列出标题，说明默认落 `[ ]`、**应用读作「进行中」**。
4. **标签超过 3 个** —— 应用上限是 3，让用户决定保留哪几个
   （工具按原则**不替他删**，所以要他去改那份输出文件；改完重跑记得加 `--force`）。
5. **同名任务被合并** —— 清单里区分不出「两条同名任务」和「一条任务出现在两个标签组」，
   把合并清单给用户确认。
6. **优先级** —— 说明这一列在清单里不存在，导入后统一是 P2。
7. **标题里带保留字符**（`#`、`⏰`+日期、`::`+数字）—— 这套写法**没有转义机制**，
   那一段在导入时会被应用的解析器挖走：标题「发布 v1.0 #发布日」会变成标题「发布 v1.0」
   + 标签 `#发布日`，**而且不报错**。让用户先改标题，别带着它往下走。

## 导入（**用户手动做**）

1. **先在 rail 底部切到目标分区** —— 导入落在**当前分区**。旧版是按分区导出的，
   一个清单文件通常对应一个分区，报错也让用户拿文件名对一下。
2. 任务管理页 → **「数据迁入」** → 选那份 `<清单>-可导入.md`。
3. 看它显示「**共 N 条**」：**N 必须等于报告里的任务数**，不等就先查。
4. 看它下面**有没有列出「没能识别的行」** —— 必须是空的；有就说明有行没被读懂，停下。
5. 若列出「N 条在当前分区里已有同名任务」—— 那说明这个分区里已经有同一批了，
   让用户自己决定「跳过重复」还是「全部导入」。

## 导入之后复核

1. **任务数**：任务页 / 管理页的「共 N 条」= 报告里的任务数。
2. **活动数**：活动分析页选「全年」，看「共 N 条活动」是否等于脚本最后打印的
   「**活动 M 条**」（注意是**输出**的条数，不是源清单的 —— 系统记录已被滤掉）。
3. **抽查两三条**：打开抽屉，看时间线的条数与内容是否与输出文件里那几条一致。

任何一项对不上，回到预演报告重新查 —— 不要在应用里手工补。

## 谁负责验证（两层，别混）

| 层 | 查什么 | 在哪 |
|---|---|---|
| **工具** | 源侧：每一行活动都有去向、数字要平、要人拍板的事集中列出来 | 那份终端报告 |
| **应用** | 解析侧：哪几行认不出、哪几条与库里同名 | 「数据迁入」那一屏，**导入之前**列出来 |

别把「工具没报」当成「没问题」—— 工具看不到你的库。
