Metadata-Version: 2.4
Name: deckflow-extract
Version: 0.3.0
Summary: Deckflow C1 extractor: unpack heterogeneous inputs (PDF/PPTX/Keynote/XLSX/URL/Markdown/DOCX/text/images) into a self-contained Parse Bundle for deckflow-lite.
License: MIT
Requires-Python: >=3.10
Requires-Dist: html2text>=2024
Requires-Dist: mammoth>=1.6
Requires-Dist: olefile>=0.47
Requires-Dist: openpyxl>=3.1
Requires-Dist: pypdf<7,>=5
Requires-Dist: xlrd>=2.0
Provides-Extra: all
Requires-Dist: lxml>=5; extra == 'all'
Requires-Dist: pymupdf4llm<1,>=0.0.17; extra == 'all'
Requires-Dist: python-pptx>=1.0; extra == 'all'
Requires-Dist: readability-lxml>=0.8; extra == 'all'
Provides-Extra: local
Requires-Dist: lxml>=5; extra == 'local'
Requires-Dist: pymupdf4llm<1,>=0.0.17; extra == 'local'
Requires-Dist: python-pptx>=1.0; extra == 'local'
Requires-Dist: readability-lxml>=0.8; extra == 'local'
Provides-Extra: ocr
Requires-Dist: rapidocr-onnxruntime>=1.3; extra == 'ocr'
Provides-Extra: pdf
Requires-Dist: pymupdf4llm<1,>=0.0.17; extra == 'pdf'
Provides-Extra: pptx
Requires-Dist: python-pptx>=1.0; extra == 'pptx'
Provides-Extra: web
Requires-Dist: lxml>=5; extra == 'web'
Requires-Dist: readability-lxml>=0.8; extra == 'web'
Description-Content-Type: text/markdown

# deckflow-extract

deckflow-lite 的 C1「拆包器」：把异构输入拆成统一的 **Parse Bundle**——`parse-manifest.json`（统一 JSON 信封）+ `document.md`（五类元素的 markdown 形态）+ `assets/`（哈希命名的二进制资产）。只拆包，不改写、不裁剪、不解读。

本文档即完整的使用说明。合约与分层设计的内部文档不随仓库发布。

## 安装

```bash
pipx install deckflow-extract
```

**约 4MB，全部是纯 Python wheel，装完即可解析全部受支持格式。** 没有二进制依赖，因此跨平台体积恒定、可离线安装、Python 新版本发布当天就能用。

重引擎按需获取，不必在安装时决定：

```bash
deckflow-extract install --list      # 可安装能力、体积、当前状态
deckflow-extract install pdf         # 56MB：PDF 表格识别与完整图片提取
deckflow-extract install pptx        # 37MB：PPTX shape/bbox 级定位
deckflow-extract install web         # 21MB：网页正文抽取质量
deckflow-extract install ocr         # 107MB：本地 OCR（必须显式指定）
deckflow-extract install cloud       # 3MB：云端解析所需的 deckops SDK
deckflow-extract uninstall pdf       # 卸载即删目录
```

引擎装进独立的 sidecar 目录（默认 `~/.deckflow/parse/engines/<能力>/<python 标签>`），不污染主环境。传统装法完全保留，Docker / CI / 内网离线可以一次装齐：

```bash
pipx install "deckflow-extract[local]"   # = [pdf,pptx,web]
```

**本工具永不隐式安装任何东西**：默认只给建议，只有显式 `deckflow-extract install` 或 `--upgrade auto` 才会真的下载。

仓库内直接运行（开发模式）：

```bash
PYTHONPATH=src python3 -m deckflow_extract probe
```

## 分层

层 = **用户为这个引擎付出的代价**，与产出质量无关（质量单独记在 manifest 的 `fidelity` 里）：

| 层 | 代价 | 例子 |
| --- | --- | --- |
| **L0 内置** | 0 | pypdf、mammoth、openpyxl、xlrd、stdlib OOXML/ODF/EPUB/iWork 读取器 |
| **L1 宿主** | 0，但不保证存在 | Keynote.app（`osascript` 导出）、LibreOffice、textutil、sips、tesseract |
| **L2 按需** | 磁盘 + 一次下载 | pymupdf4llm、python-pptx、readability-lxml |
| **L3 云端** | SDK（3MB）+ API key + 上传 + 配额 | deckops SDK 驱动的任务 API |

L0 是地板：任何机器、任何受支持格式都能产出一个合规的 Parse Bundle，或一条可执行的补救建议。**没有 L1/L2/L3 也能跑完整条 deckflow-lite 流程。**

路由不是「层越低越优先」：可用的引擎意味着代价已付清，所以在可用引擎里选保真度最高的；云端因为按次计费，默认永远排最后。

## 支持格式

```bash
deckflow-extract formats               # 完整能力表（每格式的引擎阶梯与当前状态）
deckflow-extract formats --extensions  # 扁平扩展名列表，供 glob 使用
deckflow-extract formats --available   # 只列当前机器上可用的
```

文档 `pdf docx doc rtf odt epub pages md txt`、演示 `pptx ppt key odp`、表格 `xlsx xls csv ods numbers`、网页 `url html`、图片 `image svg heic`、结构化文本 `json yaml`。

音视频与压缩包**明确不支持**，但会被识别并返回 `unsupported-format`，不会退化成 `unknown-input-type`。

## 使用

```bash
# 环境探测：格式能力表 + 宿主工具 + 已安装引擎 + 云端配置
deckflow-extract probe

# 解析一个输入 → Parse Bundle 目录（stdout 恰好一行 JSON）
deckflow-extract parse ./report.pdf --out ./report.parse
deckflow-extract parse ./book.xlsx --out ./book.parse
deckflow-extract parse ./deck.key --out ./keynote.parse
deckflow-extract parse https://example.com/post --out ./post.parse
deckflow-extract parse ./notes.md --out ./notes.parse --archive zip

# 云端模式（上传并计入配额）
deckflow-extract install cloud                 # 一次性：装 3MB 的 deckops SDK
deckflow-extract auth login                    # 浏览器登录；凭据与 deckhtml 共用
deckflow-extract parse ./deck.pptx --out ./deck.parse --mode cloud
```

云端覆盖 `pdf` / `pptx` / `docx` / `key` / `url` 五类输入——这是服务端解析器实际支持的集合；其余格式没有云端路径，`--mode cloud` 会返回 `no-cloud-path` 并提示改走本地。

## 鉴权：与 DeckHTML 共用一份凭据

两个工具连的是同一个 Deckflow API，用的是同一套凭据，所以读写**同一个文件**：`~/.deckflow/credentials`。

**装了 deckhtml 并登录过，这里不需要再配置一次**；反过来也一样，在这里登录，`deckhtml` 立刻可用。

```bash
deckflow-extract auth login                     # 浏览器登录，存下 token
deckflow-extract auth status                    # 当前用的是哪个凭据、来自哪里
deckflow-extract auth logout                    # 清掉存下的 token / api key

printf '%s\n' "$DECKFLOW_API_KEY" | \
  deckflow-extract config set api-key --stdin   # 推荐：secret 不进入进程参数
deckflow-extract config set api-key <key>       # 兼容写法
deckflow-extract config set space-id <id>       # 可选；不设则首次调用时向服务端查询
deckflow-extract config show                    # 已脱敏
```

命令名与键名刻意和 `deckhtml auth login` / `deckhtml config set api-key` 一致——同一个文件，不该有两套词汇。

### 两种凭据形态都认

| 形态 | 来自 | 请求头 | 会过期 |
| --- | --- | --- | --- |
| `token` | 浏览器登录（`auth login`） | `X-Auth-Token` | 会 |
| `apiKey` | 空间的 worker secret（`config set api-key`） | `Authorization: Bearer` | 不会 |

任意一种存在即视为已配置。**只认 `apiKey` 是 0.2.x 的 bug**：浏览器登录过的用户明明有可用凭据，却被告知未配置。

### token 过期时

在**终端里**（stdin 与 stderr 都是 TTY）用 token 跑云端解析，遇到 401 会像 deckhtml 一样重新打开登录页，拿到新 token 后自动重试。

**在非交互环境（agent 子进程、CI）不会**——直接返回 `unauthorized`，hint 里写明跑 `deckflow-extract auth login`。中途弹浏览器再阻塞五分钟等一个没人会完成的回调，比一个干净的错误糟得多。这和 `--upgrade ask` 在无 TTY 时退化成 `never` 是同一条规则。

配了 `apiKey` 时也不会自动重登：worker secret 不能靠登录续期，而在用户显式配置的 key 旁边悄悄存一个 token，会让下一次解析换一个身份跑——那不是刷新，是替换。

### 解析顺序

1. 环境变量：`DECKFLOW_API_KEY` / `DECKFLOW_TOKEN` / `DECKFLOW_SPACE_ID` / `DECKFLOW_API_BASE`（`DECKOPS_*` 同义）
2. 共用文件 `~/.deckflow/credentials`
3. 都没有 → 未配置，云端引擎不进路由

**环境变量永远压过文件。** 想要一次「保证不出网」的运行，光把变量摘掉已经不够了——还要设 `DECKFLOW_NO_STORED_CREDENTIALS=1`，否则存下的凭据会把刚摘掉的东西又补回来。`deckflow-core` 的 `parse` 已经这么做。

文件位置可以用 `DECKFLOW_CONFIG_DIR`（或 deckhtml 的 `DECKHTML_CONFIG_DIR`）改写，两个工具都认。

### 写文件时的约束

DeckHTML 的 `sanitizeConfig` 只保留它自己那 6 个键，遇到别的会重写文件。所以这里**只写它认识的键**（`apiKey` / `token` / `spaceId` / `apiBase`），并且每次写之前重新读一遍再合并——`webhook` 和 `retentionHours` 是它的，不能被我们的一次 `config set` 抹掉。本工具自己的状态放在 `DECKFLOW_EXTRACT_HOME` 下，不进这个文件。

常用参数：`--mode auto|local|cloud`、`--upgrade never|ask|auto`（默认 `ask`，环境变量 `DECKFLOW_UPGRADE` 等价）、`--prefer local|cloud`、`--max-pages`、`--max-table-rows`、`--ocr auto`、`--strict`。

## 给调用方 agent 的接口

每次 `parse` 的 stdout 与 `parse-manifest.json` 都带同一份评估结果：

| 字段 | 含义 |
| --- | --- |
| `tier` / `fidelity` | 用了哪一层、拿到的质量向量（text / structure / tables / images / notes / provenance） |
| `gaps[]` | 源文件里**有**但这次**没提出来**的东西，含 `detected`/`extracted` 与 `confidence`（`exact` 来自容器枚举，`heuristic` 来自文本推断） |
| `decision` | `usable` + `recommended`（`accept` / `install` / `host` / `cloud` / `input`）+ `reason` |
| `recommendations[]` | 每个选项的收益、代价、可直接执行的 `commands[]`；不可用的选项也会列出并给出 `blocked_by` |

两条不变量：**`usable` 为真时一定有一条 `accept`**（用户永远可以「就用现在的结果」），**没有任何一层能修复的缺口不会产生建议**（比如图表内的数据系列，只写 diagnostic）。

skill 侧的读法：`status == "parsed"` 且 `recommended == "accept"` 就静默继续；`recommended` 是其他值时把 `priority: high` 的选项连同 `accept` 一起呈现给用户；`needs-input` 才必须问。

状态机与 deckflow-lite 一致：`parsed` / `repairable` / `needs-input` / `blocked`，默认退出码恒为 0（读 JSON 判断），`--strict` 时非 `parsed` 即非零。缺依赖不再是阻塞态——除非 L0 确实产不出内容，否则永远先出 Bundle 再给建议。

## 测试

```bash
cd deckflow-extract/tests && python3 -m unittest discover
```

包含体积闸（base 依赖闭包必须是纯 Python 且 ≤10MB）、L0 地板测试、探测精度测试（标为 `exact` 的计数必须与真值完全相等）与决策矩阵测试。未安装的重引擎测试会跳过。
