From 547d8b7b74738c95ef7fe784313cbcf5f87c3647 Mon Sep 17 00:00:00 2001 From: liumingyang <1950307156@qq.com> Date: Mon, 29 Jun 2026 12:48:17 +0800 Subject: [PATCH 1/3] feat: add auth and parse usage examples to CLI README. --- cli/README.md | 159 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 155 insertions(+), 4 deletions(-) diff --git a/cli/README.md b/cli/README.md index 99fd3a9..e0f7dc4 100644 --- a/cli/README.md +++ b/cli/README.md @@ -47,6 +47,7 @@ uniparser parse /path/to/report.pdf ~/Uni-Parser-Skill/report/ ├── report.md # 解析后的 Markdown ├── pages_tree.json # 文档版面结构(可选查阅) +├── formatted_meta.json # 格式化元数据 └── trigger_meta.json # 任务信息(含 token,便于中断后恢复) ``` @@ -91,6 +92,69 @@ uniparser auth --show # 查看 Key 来源与脱敏后的 Key uniparser auth --verify # 检查是否已配置(不联网校验) ``` +### 使用实例 + +#### 首次配置 + +```bash +uniparser auth +``` + +终端交互示例(输入 Key 后): + +```text +UniParser API Key Setup +Get your API key from: https://uniparser.dp.tech/ + +Enter your API key: ******** +API key saved successfully +``` + +写入 `~/.uniparser/config.yaml`: + +```yaml +api_key: your-api-key-here +``` + +#### 保留已有 Key + +若已配置过,直接回车即可保留: + +```text +Current API key source: config +Enter new API key (or press Enter to keep current): +Keeping existing API key. +``` + +#### 查看当前配置(`--show`) + +```bash +uniparser auth --show +``` + +```text +API key source: config +API key: abcd...mnop +``` + +未配置时: + +```text +No API key configured. +Run 'uniparser auth' to set up your API key. +``` + +#### 检查是否已配置(`--verify`) + +```bash +uniparser auth --verify +``` + +```text +API key is configured. + Source: config +``` + --- ## 解析文档:`parse` @@ -151,13 +215,99 @@ uniparser parse paper.pdf -o ./results/ --overwrite --async `trigger_meta.json` 会记录本次实际提交的 `trigger_kwargs`,便于复现配置。 -### 输出目录示例 +### 使用实例 + +#### 解析本地 PDF(默认输出目录) + +```bash +uniparser parse paper.pdf +``` + +解析过程中 **stderr** 显示进度: + +```text +Parsing... paper.pdf +``` + +完成后 **stdout** 打印结果路径: + +```text +Token: a1b2c3d4e5f6789012345678901234ab +Markdown: /Users/you/Uni-Parser-Skill/paper/paper.md +Pages tree: /Users/you/Uni-Parser-Skill/paper/pages_tree.json +Output directory: /Users/you/Uni-Parser-Skill/paper +``` + +**stderr** 还会提示 meta 文件路径: + +```text +Trigger meta: /Users/you/Uni-Parser-Skill/paper/trigger_meta.json +``` + +默认输出目录结构: + +```text +~/Uni-Parser-Skill/paper/ +├── paper.md # 解析后的 Markdown(主要结果) +├── pages_tree.json # 版面结构树 +├── formatted_meta.json # 格式化元数据(不含正文) +└── trigger_meta.json # 任务 token 与 trigger_kwargs +``` + + +#### 指定输出目录与解析参数 + +```bash +uniparser parse paper.pdf -o ./out/paper --overwrite --textual digital --molecule disable +``` + +`trigger_meta.json` 中 `trigger_kwargs` 会反映覆盖项,例如: + +```json +"trigger_kwargs": { + "textual": "digital", + "equation": "ocr-hq", + "table": "ocr-hq", + "chart": "base64", + "figure": "base64", + "expression": "base64", + "molecule": "disable", + "sync": true +} +``` + +#### 三种输入类型 + +| 输入 | 命令 | 进度提示(stderr) | +|------|------|-------------------| +| 本地 PDF | `uniparser parse report.pdf` | `Parsing... report.pdf` | +| 本地图片 | `uniparser parse figure.png` | `Parsing... figure.png` | +| 公网 PDF 链接 | `uniparser parse https://example.com/paper.pdf` | `Parsing... paper.pdf` | + +#### 常见错误 + +文件不存在时,**stderr** 输出 JSON 错误(exit code 1): ```bash -uniparser parse paper.pdf -o ./results/ -uniparser parse paper.pdf -o ./results/ --overwrite +uniparser parse /no/such/file.pdf ``` +```json +{"ok": false, "error": {"code": "INPUT_ERROR", "message": "File not found: /no/such/file.pdf"}} +``` + +输出目录已存在且未加 `--overwrite` 时: + +```bash +uniparser parse paper.pdf -o ./out/paper +``` + +```json +{"ok": false, "error": {"code": "DIR_EXISTS", "message": "Output directory already exists: ...", "output_dir": "..."}} +``` + +> 说明:成功信息在 stdout;进度 `Parsing...` 与部分路径提示在 stderr;错误统一为 stderr 单行 JSON。 + ### 输出说明 解析成功后,输出目录中通常包含: @@ -166,9 +316,10 @@ uniparser parse paper.pdf -o ./results/ --overwrite |------|------| | `<文件名>.md` | 完整 Markdown 正文(主要结果) | | `pages_tree.json` | 版面结构树,便于按章节/块定位 | +| `formatted_meta.json` | 格式化元数据(不含 `content` 正文) | | `trigger_meta.json` | 任务 token、输入信息及 `trigger_kwargs`,供 `fetch` 恢复使用 | -解析过程中终端会显示一行 `Parsing... <文件名>`。大文档可能需要等待数分钟。 +大文档可能需要等待数分钟;详见上文「使用实例」中的终端输出示例。 --- From 0615db832616220852833e283d6e8c1705e80fb2 Mon Sep 17 00:00:00 2001 From: liumingyang <1950307156@qq.com> Date: Mon, 29 Jun 2026 13:05:26 +0800 Subject: [PATCH 2/3] feat: move CLI package under uniparser_tools.cli. --- README.md | 8 +-- cli/__main__.py | 5 -- pyproject.toml | 4 +- tests/unit/test_cli.py | 52 +++++++++---------- tests/unit/test_parse_options.py | 2 +- {cli => uniparser_tools/cli}/README.md | 0 {cli => uniparser_tools/cli}/__init__.py | 0 uniparser_tools/cli/__main__.py | 5 ++ .../cli}/commands/__init__.py | 0 {cli => uniparser_tools/cli}/commands/auth.py | 2 +- .../cli}/commands/fetch.py | 8 +-- .../cli}/commands/health.py | 2 +- .../cli}/commands/parse.py | 12 ++--- .../cli}/commands/version.py | 4 +- {cli => uniparser_tools/cli}/core/__init__.py | 0 {cli => uniparser_tools/cli}/core/config.py | 6 +-- .../cli}/core/credentials.py | 0 {cli => uniparser_tools/cli}/core/defaults.py | 0 {cli => uniparser_tools/cli}/core/errors.py | 0 {cli => uniparser_tools/cli}/core/input.py | 2 +- {cli => uniparser_tools/cli}/core/output.py | 2 +- .../cli}/core/parse_options.py | 0 {cli => uniparser_tools/cli}/core/pipeline.py | 10 ++-- {cli => uniparser_tools/cli}/main.py | 10 ++-- 24 files changed, 67 insertions(+), 67 deletions(-) delete mode 100644 cli/__main__.py rename {cli => uniparser_tools/cli}/README.md (100%) rename {cli => uniparser_tools/cli}/__init__.py (100%) create mode 100644 uniparser_tools/cli/__main__.py rename {cli => uniparser_tools/cli}/commands/__init__.py (100%) rename {cli => uniparser_tools/cli}/commands/auth.py (97%) rename {cli => uniparser_tools/cli}/commands/fetch.py (83%) rename {cli => uniparser_tools/cli}/commands/health.py (89%) rename {cli => uniparser_tools/cli}/commands/parse.py (86%) rename {cli => uniparser_tools/cli}/commands/version.py (92%) rename {cli => uniparser_tools/cli}/core/__init__.py (100%) rename {cli => uniparser_tools/cli}/core/config.py (79%) rename {cli => uniparser_tools/cli}/core/credentials.py (100%) rename {cli => uniparser_tools/cli}/core/defaults.py (100%) rename {cli => uniparser_tools/cli}/core/errors.py (100%) rename {cli => uniparser_tools/cli}/core/input.py (96%) rename {cli => uniparser_tools/cli}/core/output.py (98%) rename {cli => uniparser_tools/cli}/core/parse_options.py (100%) rename {cli => uniparser_tools/cli}/core/pipeline.py (90%) rename {cli => uniparser_tools/cli}/main.py (74%) diff --git a/README.md b/README.md index afe3612..e9381b0 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ pip install -e . uniparser --help ``` -CLI 完整说明见 [`cli/README.md`](./cli/README.md)。 +CLI 完整说明见 [`uniparser_tools/cli/README.md`](./uniparser_tools/cli/README.md)。 ## CLI 命令行 @@ -82,7 +82,7 @@ CLI 完整说明见 [`cli/README.md`](./cli/README.md)。 **`--json` 须写在子命令之前:** `uniparser --json parse paper.pdf` -参数、输出文件、错误码等详见 [`cli/README.md`](./cli/README.md)。 +参数、输出文件、错误码等详见 [`uniparser_tools/cli/README.md`](./uniparser_tools/cli/README.md)。 首次使用: @@ -489,7 +489,7 @@ uv run python -m uniparser_mcp # 启动 MCP 服务(stdio 模式) ## 项目结构 ``` -cli/ # uniparser 命令行工具(见 cli/README.md) +uniparser_tools/cli/ # uniparser 命令行工具(见 uniparser_tools/cli/README.md) ├── commands/ # auth, parse, fetch, health, version └── core/ # 配置、凭证、pipeline、输出 @@ -518,7 +518,7 @@ playground/ 项目提供了丰富的示例和教程,位于 `playground/` 目录下: -- **CLI 命令行**:[`cli/README.md`](./cli/README.md) - `uniparser` 安装、子命令与参数说明 +- **CLI 命令行**:[`uniparser_tools/cli/README.md`](./uniparser_tools/cli/README.md) - `uniparser` 安装、子命令与参数说明 - **快速开始**:`playground/01.quick_start.ipynb` - 基础用法教程,包括 PDF 和图片解析、多种格式输出 - **高级用法**:`playground/02.advance.ipynb` - 高级功能教程,包括图片+图题+图注、表格+表题+表注、分子+分子索引、公式+公式索引的提取 - **异步回调**:`playground/04.use_callbacks.py` - 异步回调演示,用于在异步解析任务完成后自动接收通知和结果 diff --git a/cli/__main__.py b/cli/__main__.py deleted file mode 100644 index 8a070d4..0000000 --- a/cli/__main__.py +++ /dev/null @@ -1,5 +0,0 @@ -from cli.main import cli - - -if __name__ == "__main__": - cli() diff --git a/pyproject.toml b/pyproject.toml index 5bb1d3c..471157d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ requires-python = ">=3.8" keywords = ["ocr", "document-parser", "uniparser"] [project.scripts] -uniparser = "cli.main:cli" +uniparser = "uniparser_tools.cli.main:cli" [build-system] requires = ["setuptools>=61.0"] @@ -36,7 +36,7 @@ select = ["C", "E", "F", "I", "W"] [tool.ruff.lint.isort] lines-after-imports = 2 -known-first-party = [] +known-first-party = ["uniparser_tools"] known-third-party = [] [tool.ruff.format] diff --git a/tests/unit/test_cli.py b/tests/unit/test_cli.py index 27045bc..e8406b4 100644 --- a/tests/unit/test_cli.py +++ b/tests/unit/test_cli.py @@ -10,9 +10,9 @@ import pytest from click.testing import CliRunner -from cli.core.credentials import ApiKeySource -from cli.core.input import InputKind, resolve_input -from cli.main import cli +from uniparser_tools.cli.core.credentials import ApiKeySource +from uniparser_tools.cli.core.input import InputKind, resolve_input +from uniparser_tools.cli.main import cli from uniparser_tools.common.constant import ParseMode, ParseModeTextual @@ -29,7 +29,7 @@ def env_without_api_key(monkeypatch: pytest.MonkeyPatch) -> None: @pytest.fixture def no_config_file(monkeypatch: pytest.MonkeyPatch) -> None: config_path = Path("/nonexistent/uniparser-cli-test-config/config.yaml") - monkeypatch.setattr("cli.core.credentials.CONFIG_PATH", config_path) + monkeypatch.setattr("uniparser_tools.cli.core.credentials.CONFIG_PATH", config_path) class TestCliConfig: @@ -60,12 +60,12 @@ def test_parse_with_config_file_api_key( config_dir.mkdir() config_path = config_dir / "config.yaml" config_path.write_text("api_key: file-key\n", encoding="utf-8") - monkeypatch.setattr("cli.core.credentials.CONFIG_DIR", config_dir) - monkeypatch.setattr("cli.core.credentials.CONFIG_PATH", config_path) + monkeypatch.setattr("uniparser_tools.cli.core.credentials.CONFIG_DIR", config_dir) + monkeypatch.setattr("uniparser_tools.cli.core.credentials.CONFIG_PATH", config_path) mock_client = MagicMock() mock_client.trigger_file.return_value = {"status": "error", "description": "stop early"} - monkeypatch.setattr("cli.commands.parse.make_client", lambda ctx: (mock_client, None)) + monkeypatch.setattr("uniparser_tools.cli.commands.parse.make_client", lambda ctx: (mock_client, None)) pdf = tmp_path / "paper.pdf" pdf.write_bytes(b"%PDF-1.4") @@ -147,7 +147,7 @@ def test_parse_success_writes_trigger_meta_and_json_token( ] mock_client.get_formatted.return_value = {"status": "success", "content": "# Hi"} - monkeypatch.setattr("cli.commands.parse.make_client", lambda ctx: (mock_client, None)) + monkeypatch.setattr("uniparser_tools.cli.commands.parse.make_client", lambda ctx: (mock_client, None)) out = tmp_path / "out" result = runner.invoke( @@ -179,7 +179,7 @@ def test_parse_default_trigger_kwargs( mock_client = MagicMock() mock_client.trigger_file.return_value = {"status": "error", "description": "stop"} - monkeypatch.setattr("cli.commands.parse.make_client", lambda ctx: (mock_client, None)) + monkeypatch.setattr("uniparser_tools.cli.commands.parse.make_client", lambda ctx: (mock_client, None)) out = tmp_path / "out" runner.invoke( @@ -210,7 +210,7 @@ def test_parse_molecule_disable_override( mock_client = MagicMock() mock_client.trigger_file.return_value = {"status": "error", "description": "stop"} - monkeypatch.setattr("cli.commands.parse.make_client", lambda ctx: (mock_client, None)) + monkeypatch.setattr("uniparser_tools.cli.commands.parse.make_client", lambda ctx: (mock_client, None)) out = tmp_path / "out" runner.invoke( @@ -258,7 +258,7 @@ def test_fetch_success( ] mock_client.get_formatted.return_value = {"status": "success", "content": "body"} - monkeypatch.setattr("cli.commands.fetch.make_client", lambda ctx: (mock_client, None)) + monkeypatch.setattr("uniparser_tools.cli.commands.fetch.make_client", lambda ctx: (mock_client, None)) out = tmp_path / "fetch-out" result = runner.invoke( @@ -279,7 +279,7 @@ def test_health_json(self, runner: CliRunner, monkeypatch: pytest.MonkeyPatch) - monkeypatch.setenv("UNIPARSER_API_KEY", "test-key") mock_client = MagicMock() mock_client.health.return_value = {"status": "success"} - monkeypatch.setattr("cli.commands.health.make_client", lambda ctx: (mock_client, None)) + monkeypatch.setattr("uniparser_tools.cli.commands.health.make_client", lambda ctx: (mock_client, None)) result = runner.invoke(cli, ["--json", "health"], env={**os.environ, "UNIPARSER_API_KEY": "test-key"}) assert result.exit_code == 0 @@ -289,7 +289,7 @@ def test_version_human(self, runner: CliRunner, monkeypatch: pytest.MonkeyPatch) monkeypatch.setenv("UNIPARSER_API_KEY", "test-key") mock_client = MagicMock() mock_client.version.return_value = {"status": "success", "version": "1.0"} - monkeypatch.setattr("cli.commands.version.make_client", lambda ctx: (mock_client, None)) + monkeypatch.setattr("uniparser_tools.cli.commands.version.make_client", lambda ctx: (mock_client, None)) result = runner.invoke(cli, ["version"], env={**os.environ, "UNIPARSER_API_KEY": "test-key"}) assert result.exit_code == 0 @@ -299,7 +299,7 @@ def test_version_without_api_key( self, runner: CliRunner, monkeypatch: pytest.MonkeyPatch, no_config_file: None ) -> None: monkeypatch.setattr( - "cli.commands.version.resolve_api_key_source", + "uniparser_tools.cli.commands.version.resolve_api_key_source", lambda ctx: ApiKeySource(api_key=None, source=""), ) env = {k: v for k, v in os.environ.items() if k != "UNIPARSER_API_KEY"} @@ -312,7 +312,7 @@ def test_version_json_without_api_key( self, runner: CliRunner, monkeypatch: pytest.MonkeyPatch, no_config_file: None ) -> None: monkeypatch.setattr( - "cli.commands.version.resolve_api_key_source", + "uniparser_tools.cli.commands.version.resolve_api_key_source", lambda ctx: ApiKeySource(api_key=None, source=""), ) env = {k: v for k, v in os.environ.items() if k != "UNIPARSER_API_KEY"} @@ -350,8 +350,8 @@ def test_auth_saves_api_key( monkeypatch.delenv("UNIPARSER_API_KEY", raising=False) config_dir = tmp_path / ".uniparser" config_path = config_dir / "config.yaml" - monkeypatch.setattr("cli.core.credentials.CONFIG_DIR", config_dir) - monkeypatch.setattr("cli.core.credentials.CONFIG_PATH", config_path) + monkeypatch.setattr("uniparser_tools.cli.core.credentials.CONFIG_DIR", config_dir) + monkeypatch.setattr("uniparser_tools.cli.core.credentials.CONFIG_PATH", config_path) result = runner.invoke(cli, ["auth"], input="my-secret-key\n") assert result.exit_code == 0, result.stderr @@ -371,8 +371,8 @@ def test_auth_keeps_existing_on_enter( config_dir.mkdir() config_path = config_dir / "config.yaml" config_path.write_text("api_key: existing-key\n", encoding="utf-8") - monkeypatch.setattr("cli.core.credentials.CONFIG_DIR", config_dir) - monkeypatch.setattr("cli.core.credentials.CONFIG_PATH", config_path) + monkeypatch.setattr("uniparser_tools.cli.core.credentials.CONFIG_DIR", config_dir) + monkeypatch.setattr("uniparser_tools.cli.core.credentials.CONFIG_PATH", config_path) result = runner.invoke(cli, ["auth"], input="\n") assert result.exit_code == 0, result.stderr @@ -387,8 +387,8 @@ def test_auth_interactive_shows_env_source( tmp_path: Path, ) -> None: config_dir = tmp_path / ".uniparser" - monkeypatch.setattr("cli.core.credentials.CONFIG_DIR", config_dir) - monkeypatch.setattr("cli.core.credentials.CONFIG_PATH", config_dir / "config.yaml") + monkeypatch.setattr("uniparser_tools.cli.core.credentials.CONFIG_DIR", config_dir) + monkeypatch.setattr("uniparser_tools.cli.core.credentials.CONFIG_PATH", config_dir / "config.yaml") env = {k: v for k, v in os.environ.items() if k != "UNIPARSER_API_KEY"} env["UNIPARSER_API_KEY"] = "env-key" @@ -408,8 +408,8 @@ def test_auth_show_masked( config_dir.mkdir() config_path = config_dir / "config.yaml" config_path.write_text("api_key: abcdefghijklmnop\n", encoding="utf-8") - monkeypatch.setattr("cli.core.credentials.CONFIG_DIR", config_dir) - monkeypatch.setattr("cli.core.credentials.CONFIG_PATH", config_path) + monkeypatch.setattr("uniparser_tools.cli.core.credentials.CONFIG_DIR", config_dir) + monkeypatch.setattr("uniparser_tools.cli.core.credentials.CONFIG_PATH", config_path) result = runner.invoke(cli, ["auth", "--show"]) assert result.exit_code == 0 @@ -426,7 +426,7 @@ def test_auth_show_env_source(self, runner: CliRunner, monkeypatch: pytest.Monke def test_auth_show_without_config(self, runner: CliRunner, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.delenv("UNIPARSER_API_KEY", raising=False) - monkeypatch.setattr("cli.core.credentials.read_api_key_from_config", lambda: None) + monkeypatch.setattr("uniparser_tools.cli.core.credentials.read_api_key_from_config", lambda: None) env = {k: v for k, v in os.environ.items() if k != "UNIPARSER_API_KEY"} result = runner.invoke(cli, ["auth", "--show"], env=env) @@ -445,8 +445,8 @@ def test_auth_verify_success( config_dir.mkdir() config_path = config_dir / "config.yaml" config_path.write_text("api_key: configured\n", encoding="utf-8") - monkeypatch.setattr("cli.core.credentials.CONFIG_DIR", config_dir) - monkeypatch.setattr("cli.core.credentials.CONFIG_PATH", config_path) + monkeypatch.setattr("uniparser_tools.cli.core.credentials.CONFIG_DIR", config_dir) + monkeypatch.setattr("uniparser_tools.cli.core.credentials.CONFIG_PATH", config_path) result = runner.invoke(cli, ["auth", "--verify"]) assert result.exit_code == 0 diff --git a/tests/unit/test_parse_options.py b/tests/unit/test_parse_options.py index 5b308a5..bb8d25e 100644 --- a/tests/unit/test_parse_options.py +++ b/tests/unit/test_parse_options.py @@ -2,7 +2,7 @@ from __future__ import annotations -from cli.core.parse_options import ( +from uniparser_tools.cli.core.parse_options import ( PARSE_MODE_ALIASES, SCIENTIFIC_PAPER_DEFAULTS, TEXTUAL_ALIASES, diff --git a/cli/README.md b/uniparser_tools/cli/README.md similarity index 100% rename from cli/README.md rename to uniparser_tools/cli/README.md diff --git a/cli/__init__.py b/uniparser_tools/cli/__init__.py similarity index 100% rename from cli/__init__.py rename to uniparser_tools/cli/__init__.py diff --git a/uniparser_tools/cli/__main__.py b/uniparser_tools/cli/__main__.py new file mode 100644 index 0000000..8c75e18 --- /dev/null +++ b/uniparser_tools/cli/__main__.py @@ -0,0 +1,5 @@ +from uniparser_tools.cli.main import cli + + +if __name__ == "__main__": + cli() diff --git a/cli/commands/__init__.py b/uniparser_tools/cli/commands/__init__.py similarity index 100% rename from cli/commands/__init__.py rename to uniparser_tools/cli/commands/__init__.py diff --git a/cli/commands/auth.py b/uniparser_tools/cli/commands/auth.py similarity index 97% rename from cli/commands/auth.py rename to uniparser_tools/cli/commands/auth.py index cd6cc2d..5605f09 100644 --- a/cli/commands/auth.py +++ b/uniparser_tools/cli/commands/auth.py @@ -2,7 +2,7 @@ import click -from cli.core.credentials import ( +from uniparser_tools.cli.core.credentials import ( API_KEY_URL, mask_api_key, resolve_api_key_source, diff --git a/cli/commands/fetch.py b/uniparser_tools/cli/commands/fetch.py similarity index 83% rename from cli/commands/fetch.py rename to uniparser_tools/cli/commands/fetch.py index 4c3ca31..7219ecc 100644 --- a/cli/commands/fetch.py +++ b/uniparser_tools/cli/commands/fetch.py @@ -4,10 +4,10 @@ import click -from cli.core.config import ctx_flag, make_client -from cli.core.errors import missing_token_error -from cli.core.output import emit_success, fetch_source_stem, resolve_fetch_output_dir -from cli.core.pipeline import complete_fetch +from uniparser_tools.cli.core.config import ctx_flag, make_client +from uniparser_tools.cli.core.errors import missing_token_error +from uniparser_tools.cli.core.output import emit_success, fetch_source_stem, resolve_fetch_output_dir +from uniparser_tools.cli.core.pipeline import complete_fetch @click.command("fetch") diff --git a/cli/commands/health.py b/uniparser_tools/cli/commands/health.py similarity index 89% rename from cli/commands/health.py rename to uniparser_tools/cli/commands/health.py index 8f7fd99..87f2991 100644 --- a/cli/commands/health.py +++ b/uniparser_tools/cli/commands/health.py @@ -4,7 +4,7 @@ import click -from cli.core.config import ctx_flag, make_client +from uniparser_tools.cli.core.config import ctx_flag, make_client @click.command("health") diff --git a/cli/commands/parse.py b/uniparser_tools/cli/commands/parse.py similarity index 86% rename from cli/commands/parse.py rename to uniparser_tools/cli/commands/parse.py index 48ffd3c..cf1b4a2 100644 --- a/cli/commands/parse.py +++ b/uniparser_tools/cli/commands/parse.py @@ -2,12 +2,12 @@ import click -from cli.core.config import ctx_flag, make_client -from cli.core.errors import input_error -from cli.core.input import resolve_input -from cli.core.output import emit_success, resolve_output_dir -from cli.core.parse_options import PARSE_MODE_CHOICES, TEXTUAL_CHOICES, resolve_trigger_kwargs -from cli.core.pipeline import run_parse +from uniparser_tools.cli.core.config import ctx_flag, make_client +from uniparser_tools.cli.core.errors import input_error +from uniparser_tools.cli.core.input import resolve_input +from uniparser_tools.cli.core.output import emit_success, resolve_output_dir +from uniparser_tools.cli.core.parse_options import PARSE_MODE_CHOICES, TEXTUAL_CHOICES, resolve_trigger_kwargs +from uniparser_tools.cli.core.pipeline import run_parse def _parse_mode_option(name: str, help_text: str): diff --git a/cli/commands/version.py b/uniparser_tools/cli/commands/version.py similarity index 92% rename from cli/commands/version.py rename to uniparser_tools/cli/commands/version.py index a235038..58c2a38 100644 --- a/cli/commands/version.py +++ b/uniparser_tools/cli/commands/version.py @@ -4,8 +4,8 @@ import click -from cli.core.config import ctx_flag, make_client -from cli.core.credentials import resolve_api_key_source +from uniparser_tools.cli.core.config import ctx_flag, make_client +from uniparser_tools.cli.core.credentials import resolve_api_key_source def format_remote_version(remote: object) -> str: diff --git a/cli/core/__init__.py b/uniparser_tools/cli/core/__init__.py similarity index 100% rename from cli/core/__init__.py rename to uniparser_tools/cli/core/__init__.py diff --git a/cli/core/config.py b/uniparser_tools/cli/core/config.py similarity index 79% rename from cli/core/config.py rename to uniparser_tools/cli/core/config.py index d6a820b..1914191 100644 --- a/cli/core/config.py +++ b/uniparser_tools/cli/core/config.py @@ -2,9 +2,9 @@ import click -from cli.core.credentials import resolve_api_key_source -from cli.core.defaults import UNIPARSER_BASE_URL -from cli.core.errors import config_error +from uniparser_tools.cli.core.credentials import resolve_api_key_source +from uniparser_tools.cli.core.defaults import UNIPARSER_BASE_URL +from uniparser_tools.cli.core.errors import config_error def resolve_host() -> str: diff --git a/cli/core/credentials.py b/uniparser_tools/cli/core/credentials.py similarity index 100% rename from cli/core/credentials.py rename to uniparser_tools/cli/core/credentials.py diff --git a/cli/core/defaults.py b/uniparser_tools/cli/core/defaults.py similarity index 100% rename from cli/core/defaults.py rename to uniparser_tools/cli/core/defaults.py diff --git a/cli/core/errors.py b/uniparser_tools/cli/core/errors.py similarity index 100% rename from cli/core/errors.py rename to uniparser_tools/cli/core/errors.py diff --git a/cli/core/input.py b/uniparser_tools/cli/core/input.py similarity index 96% rename from cli/core/input.py rename to uniparser_tools/cli/core/input.py index aa301ee..f5416d8 100644 --- a/cli/core/input.py +++ b/uniparser_tools/cli/core/input.py @@ -5,7 +5,7 @@ from pathlib import Path from urllib.parse import urlparse -from cli.core.defaults import IMAGE_SUFFIXES +from uniparser_tools.cli.core.defaults import IMAGE_SUFFIXES class InputKind(str, Enum): diff --git a/cli/core/output.py b/uniparser_tools/cli/core/output.py similarity index 98% rename from cli/core/output.py rename to uniparser_tools/cli/core/output.py index 91c21f4..13e7467 100644 --- a/cli/core/output.py +++ b/uniparser_tools/cli/core/output.py @@ -7,7 +7,7 @@ from pathlib import Path from typing import Any -from cli.core.errors import dir_exists_error +from uniparser_tools.cli.core.errors import dir_exists_error def default_output_dir(source_stem: str) -> Path: diff --git a/cli/core/parse_options.py b/uniparser_tools/cli/core/parse_options.py similarity index 100% rename from cli/core/parse_options.py rename to uniparser_tools/cli/core/parse_options.py diff --git a/cli/core/pipeline.py b/uniparser_tools/cli/core/pipeline.py similarity index 90% rename from cli/core/pipeline.py rename to uniparser_tools/cli/core/pipeline.py index 094c2b1..efa1e30 100644 --- a/cli/core/pipeline.py +++ b/uniparser_tools/cli/core/pipeline.py @@ -5,11 +5,11 @@ from pathlib import Path from typing import Any -from cli.core.defaults import PENDING_STATUSES, POLL_INTERVAL_SEC, POLL_TIMEOUT_SEC -from cli.core.errors import parse_error -from cli.core.input import InputKind, ResolvedInput, display_label_for_input -from cli.core.output import print_parsing_status, save_parse_results, write_trigger_meta -from cli.core.parse_options import resolve_trigger_kwargs, serialize_trigger_kwargs +from uniparser_tools.cli.core.defaults import PENDING_STATUSES, POLL_INTERVAL_SEC, POLL_TIMEOUT_SEC +from uniparser_tools.cli.core.errors import parse_error +from uniparser_tools.cli.core.input import InputKind, ResolvedInput, display_label_for_input +from uniparser_tools.cli.core.output import print_parsing_status, save_parse_results, write_trigger_meta +from uniparser_tools.cli.core.parse_options import resolve_trigger_kwargs, serialize_trigger_kwargs def scientific_paper_trigger_kwargs(*, sync: bool = True) -> dict: diff --git a/cli/main.py b/uniparser_tools/cli/main.py similarity index 74% rename from cli/main.py rename to uniparser_tools/cli/main.py index e85c633..79ca162 100644 --- a/cli/main.py +++ b/uniparser_tools/cli/main.py @@ -2,11 +2,11 @@ import click -from cli.commands.auth import auth_cmd -from cli.commands.fetch import fetch_cmd -from cli.commands.health import health_cmd -from cli.commands.parse import parse_cmd -from cli.commands.version import version_cmd +from uniparser_tools.cli.commands.auth import auth_cmd +from uniparser_tools.cli.commands.fetch import fetch_cmd +from uniparser_tools.cli.commands.health import health_cmd +from uniparser_tools.cli.commands.parse import parse_cmd +from uniparser_tools.cli.commands.version import version_cmd @click.group(context_settings={"help_option_names": ["-h", "--help"]}) From fa49235a03edfc70d0ea3abd460f32a27feeb16b Mon Sep 17 00:00:00 2001 From: liumingyang <1950307156@qq.com> Date: Mon, 29 Jun 2026 13:27:02 +0800 Subject: [PATCH 3/3] feat: show early parse progress and document JSON parse output. --- uniparser_tools/cli/README.md | 48 ++++++++++++++++++++++++++- uniparser_tools/cli/commands/fetch.py | 9 +++-- uniparser_tools/cli/core/output.py | 2 +- uniparser_tools/cli/core/pipeline.py | 4 +-- 4 files changed, 56 insertions(+), 7 deletions(-) diff --git a/uniparser_tools/cli/README.md b/uniparser_tools/cli/README.md index e0f7dc4..dc7d1de 100644 --- a/uniparser_tools/cli/README.md +++ b/uniparser_tools/cli/README.md @@ -366,7 +366,7 @@ uniparser version ## 脚本与自动化:`--json` -在命令**最前面**加上 `--json`,成功时会在标准输出打印 JSON,便于脚本解析: +在命令**最前面**加上 `--json`,成功时会在**标准输出(stdout)**打印 JSON,便于脚本解析: ```bash uniparser --json parse paper.pdf @@ -380,6 +380,52 @@ uniparser --json parse paper.pdf # 正确 uniparser parse paper.pdf --json # 错误 ``` +### `parse` 成功时的输出示例 + +```bash +uniparser --json parse /path/to/report.pdf +``` + +**stderr**(进度,与是否 `--json` 无关): + +```text +Parsing... report.pdf +``` + +**stdout**(成功时唯一的 JSON,exit code 0): + +```json +{ + "ok": true, + "output_dir": "/Users/you/Uni-Parser-Skill/report", + "pages_tree_path": "/Users/you/Uni-Parser-Skill/report/pages_tree.json", + "markdown_path": "/Users/you/Uni-Parser-Skill/report/report.md", + "content_chars": 12345, + "token": "a1b2c3d4e5f6789012345678901234ab", + "input_type": "file", + "trigger_meta_path": "/Users/you/Uni-Parser-Skill/report/trigger_meta.json" +} +``` + +| 字段 | 含义 | +|------|------| +| `ok` | 是否成功(`true`) | +| `output_dir` | 结果目录绝对路径 | +| `markdown_path` | 主 Markdown 文件路径 | +| `pages_tree_path` | 版面结构树 JSON 路径 | +| `content_chars` | Markdown 正文字符数(随文档而异) | +| `token` | 任务 token,可用于 `uniparser fetch --token` | +| `input_type` | 输入类型:`file` / `image` / `url` | +| `trigger_meta_path` | `trigger_meta.json` 路径(含 `trigger_kwargs`) | + +脚本中可只解析 stdout,例如: + +```bash +uniparser --json parse paper.pdf 2>/dev/null | python3 -c "import sys,json; print(json.load(sys.stdin)['markdown_path'])" +``` + +失败时仍为 **stderr 单行 JSON**(`ok: false`),见上文 parse「常见错误」;exit code 为 1。 + --- ## 常见问题 diff --git a/uniparser_tools/cli/commands/fetch.py b/uniparser_tools/cli/commands/fetch.py index 7219ecc..bc50402 100644 --- a/uniparser_tools/cli/commands/fetch.py +++ b/uniparser_tools/cli/commands/fetch.py @@ -6,7 +6,12 @@ from uniparser_tools.cli.core.config import ctx_flag, make_client from uniparser_tools.cli.core.errors import missing_token_error -from uniparser_tools.cli.core.output import emit_success, fetch_source_stem, resolve_fetch_output_dir +from uniparser_tools.cli.core.output import ( + emit_success, + fetch_source_stem, + print_parsing_status, + resolve_fetch_output_dir, +) from uniparser_tools.cli.core.pipeline import complete_fetch @@ -39,12 +44,12 @@ def fetch_cmd( if dir_code is not None: raise SystemExit(dir_code) + print_parsing_status(source_stem) summary = complete_fetch( client, resolved_token, out_dir=Path(out_dir), source_stem=source_stem, - parsing_label=source_stem, ) if isinstance(summary, int): raise SystemExit(summary) diff --git a/uniparser_tools/cli/core/output.py b/uniparser_tools/cli/core/output.py index 13e7467..35d2f51 100644 --- a/uniparser_tools/cli/core/output.py +++ b/uniparser_tools/cli/core/output.py @@ -121,4 +121,4 @@ def emit_success(summary: dict, *, json_output: bool) -> None: def print_parsing_status(label: str) -> None: - print(f"Parsing... {label}", file=sys.stderr) + print(f"Parsing... {label}", file=sys.stderr, flush=True) diff --git a/uniparser_tools/cli/core/pipeline.py b/uniparser_tools/cli/core/pipeline.py index efa1e30..91d95a9 100644 --- a/uniparser_tools/cli/core/pipeline.py +++ b/uniparser_tools/cli/core/pipeline.py @@ -88,9 +88,7 @@ def complete_fetch( *, out_dir: Path, source_stem: str, - parsing_label: str, ) -> dict[str, Any] | int: - print_parsing_status(parsing_label) poll_result = poll_until_success(client, token) if isinstance(poll_result, int): return poll_result @@ -122,6 +120,7 @@ def run_parse( out_dir: Path, trigger_kwargs: dict, ) -> dict[str, Any] | int: + print_parsing_status(display_label_for_input(resolved)) trigger, stage = trigger_input(client, resolved, trigger_kwargs=trigger_kwargs) if trigger.get("status") != "success": save_stage_error(out_dir, "trigger_error.json", trigger) @@ -144,7 +143,6 @@ def run_parse( token, out_dir=out_dir, source_stem=resolved.source_stem, - parsing_label=display_label_for_input(resolved), ) if isinstance(summary, int): return summary