Metadata-Version: 2.4
Name: ruijian-openapi-sdk
Version: 1.0.0
Summary: Python SDK for Ruijian OpenAPI V2
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Requires-Python: >=3.9
Description-Content-Type: text/markdown
Requires-Dist: httpx[http2]<1.0.0,>=0.27.0
Provides-Extra: release
Requires-Dist: build<2.0,>=1.2; extra == "release"
Requires-Dist: twine<7.0,>=6.0; extra == "release"
Provides-Extra: test
Requires-Dist: pytest<9.0,>=8.0; extra == "test"
Requires-Dist: ruff<1.0.0,>=0.9.0; extra == "test"

# 睿鉴 OpenAPI Python SDK

睿鉴 OpenAPI V2 的 Python 客户端，支持图片、视频、音频、文本和文档检测能力。SDK 自动完成：

- `RJ-HMAC-SHA256` 请求签名
- Body SHA-256、毫秒时间戳和防重放 Nonce
- HTTP/2、连接池与 TLS 连接复用
- `fileUrl` 和 Base64 文件输入
- 同步调用、异步任务提交、查询和轮询
- 同步 Python 客户端和原生 `asyncio` 客户端
- 统一响应、业务异常、传输异常和响应格式异常

调用方不需要自行实现签名，也不需要把 `AccessSecret` 发送给服务端。

## 1. 运行要求

- Python 3.9 或更高版本
- 通过 HTTPS 访问 OpenAPI
- 推荐使用虚拟环境

## 2. 安装 SDK

### 2.1 从 PyPI 安装

```bash
python -m venv .venv
source .venv/bin/activate
python -m pip install --index-url https://pypi.org/simple/ ruijian-openapi-sdk==1.0.0
```

安装时会自动安装 HTTPX 和 HTTP/2 所需依赖。也可以安装下载的 Wheel：

```bash
python -m pip install ruijian_openapi_sdk-1.0.0-py3-none-any.whl
```

### 2.2 从源码安装

```bash
python -m venv .venv
source .venv/bin/activate
python -m pip install .
```

开发和运行测试时：

```bash
python -m pip install -e '.[test]'
```

### 2.3 构建发布包

```bash
python -m pip install '.[release]'
python -m build
python -m twine check --strict dist/*
```

构建结果位于 `dist/`：

```text
ruijian_openapi_sdk-1.0.0-py3-none-any.whl
ruijian_openapi_sdk-1.0.0.tar.gz
```

## 3. 凭据与安全

| 配置 | 说明 | 是否发送到服务端 |
| --- | --- | --- |
| `AccessKey` | 调用方身份标识，通常以 `sk_` 开头 | 是，放入签名请求头 |
| `AccessSecret` | HMAC 签名密钥；平台页面也可能显示为 `AccessToken` | 否，仅在本地签名 |
| `Endpoint` | OpenAPI 服务根地址 | 作为 HTTPS 请求地址 |

不要把 `AccessSecret`/`AccessToken` 放入请求头、请求体、URL、浏览器代码、移动端代码或日志。
推荐通过环境变量或密钥管理系统注入：

```bash
export RUIJIAN_OPENAPI_ENDPOINT="https://openapi.ruijianai.com"
export RUIJIAN_ACCESS_KEY="your-access-key"
export RUIJIAN_ACCESS_SECRET="your-access-secret"
```

## 4. 五分钟快速接入

```python
import os
import uuid

from ruijian_openapi import (
    Actions,
    AsyncMode,
    InvokeRequest,
    RuijianOpenApiClient,
)


request = InvokeRequest(
    idempotency_key=str(uuid.uuid4()),
    parameters={
        "text": "这是一段待检测的文本。",
        "threshold": 0.5,
    },
)

with RuijianOpenApiClient(
    endpoint=os.environ.get(
        "RUIJIAN_OPENAPI_ENDPOINT",
        "https://openapi.ruijianai.com",
    ),
    access_key=os.environ["RUIJIAN_ACCESS_KEY"],
    access_secret=os.environ["RUIJIAN_ACCESS_SECRET"],
) as client:
    response = client.invoke(
        Actions.TEXT_AIGC_CHECK,
        request,
        AsyncMode.SYNC,
    )
    response.require_success()
    print("request_id=", response.request_id)
    print("result=", response.data)
```

`require_success()` 会同时检查 HTTP 状态和响应业务码。业务失败时抛出
`OpenApiServiceError`。

## 5. 客户端生命周期

客户端内部维护 DNS、TCP、TLS 和 HTTP/2 连接池。Web 服务中应创建一个进程级客户端并复用，
不要每次请求都创建客户端。

```python
from ruijian_openapi import RuijianOpenApiClient

client = RuijianOpenApiClient(
    endpoint="https://openapi.ruijianai.com",
    access_key=access_key,
    access_secret=access_secret,
)

# 应用停止时执行
client.close()
```

同步客户端可跨线程共享。异步客户端应在同一个事件循环中复用，并在应用停止时执行
`await client.aclose()`。

默认连接配置：

| 项目 | 默认值 |
| --- | --- |
| 最大连接数 | 100 |
| 最大空闲连接数 | 20 |
| 空闲连接保留时间 | 300 秒 |
| 连接超时 | 10 秒 |
| 读取/写入超时 | 180 秒 |
| 连接池等待超时 | 10 秒 |
| HTTP 协议 | 优先 HTTP/2，自动回退 HTTP/1.1 |

HTTP/2 不需要手工添加 `Connection: keep-alive`。

## 6. 统一响应

```python
response = client.invoke(action, request, AsyncMode.SYNC)

print(response.request_id)
print(response.code)
print(response.message)
print(response.data)
print(response.http_status)
print(response.http_version)
```

| 字段 | 说明 |
| --- | --- |
| `request_id` | 服务端请求标识，排查问题时必须提供 |
| `code` | 业务码，字符串 `"200"` 表示业务成功 |
| `message` | 响应说明 |
| `data` | Action 对应的字典、列表或异步任务数据 |
| `http_status` | HTTP 状态码 |
| `http_version` | 本次请求使用的 HTTP 协议 |
| `is_success` | HTTP 和业务码是否同时成功 |

不能只判断 HTTP 200。推荐调用 `response.require_success()`。

## 7. 同步与异步模式

| Python 枚举 | Header 值 | 说明 |
| --- | --- | --- |
| `AsyncMode.AUTO` | `auto` | 由服务端根据 Action 和请求体大小决定 |
| `AsyncMode.ASYNC` | `true` | 强制异步受理并返回 `taskId` |
| `AsyncMode.SYNC` | `false` | 强制同步；强制异步 Action 会返回参数错误 |

当前默认同步阈值是完整 HTTP JSON Body 的 5 MiB。`AUTO` 模式下 Body 大于 5 MiB 时异步
受理，刚好等于 5 MiB 时仍同步。强制异步 Action 不使用该阈值。

Base64 编码后的字符串属于 Body 大小；`fileUrl` 指向的远程文件大小不属于请求 Body 大小。

## 8. 文件输入

### 8.1 fileUrl（推荐大文件使用）

```python
from ruijian_openapi import Actions, AsyncMode, InvokeRequest, OpenApiFile

file = OpenApiFile.from_url(
    "https://cdn.example.com/input/image.jpg",
    filename="image.jpg",
    content_type="image/jpeg",
)
request = InvokeRequest(
    idempotency_key="image-order-001",
    file=file,
    parameters={"heatmap": False},
)

response = client.invoke(
    Actions.IMAGE_DEEPFAKE_CHECK,
    request,
    AsyncMode.SYNC,
)
response.require_success()
```

URL 必须能被 OpenAPI 服务直接访问，不能依赖浏览器 Cookie 或调用方内网。带有效期的签名 URL
应覆盖排队、下载和检测时间。

### 8.2 本地文件转 Base64

```python
from pathlib import Path

file = OpenApiFile.from_path(
    Path("/data/input/image.jpg"),
    content_type="image/jpeg",
)
request = InvokeRequest(
    idempotency_key="image-base64-001",
    file=file,
    parameters={},
)

client.invoke(
    Actions.IMAGE_DEEPFAKE_CHECK,
    request,
    AsyncMode.SYNC,
).require_success()
```

SDK 默认限制 Base64 解码前的原文件不超过 20 MiB，并发送纯 Base64，不发送废弃的
`file.encoding`。Base64 通常增加约三分之一传输体积，大文件应使用 `fileUrl`。

## 9. 服务端异步任务

### 9.1 提交任务

```python
submitted = client.submit(Actions.VIDEO_ASYNC_DETECT, request)
submitted.require_success()

task_id = submitted.data.task_id
print(task_id, submitted.data.status)
```

### 9.2 查询任务

查询时必须使用提交任务时的同一个 Action 和 AccessKey：

```python
queried = client.query_task(Actions.VIDEO_ASYNC_DETECT, task_id)
queried.require_success()

print(queried.data.status)
print(queried.data.result)
```

任务状态包括 `QUEUED`、`RUNNING`、`SUCCEEDED` 和 `FAILED`。

### 9.3 自动轮询

```python
completed = client.wait_for_task(
    Actions.VIDEO_ASYNC_DETECT,
    task_id,
    interval_seconds=2,
    timeout_seconds=20 * 60,
)
completed.require_success()

if completed.data.status == "SUCCEEDED":
    print(completed.data.result)
```

也可以一次完成提交和等待：

```python
completed = client.submit_and_wait(
    Actions.VIDEO_ASYNC_DETECT,
    request,
    interval_seconds=2,
    timeout_seconds=20 * 60,
)
```

## 10. 异步回调

`callbackId` 和 `callbackUrl` 必须同时填写：

```python
request = InvokeRequest(
    idempotency_key="doc-audit-001",
    file=OpenApiFile.from_url(
        "https://cdn.example.com/input/document.pdf",
        filename="document.pdf",
        content_type="application/pdf",
    ),
).with_callback(
    "customer-order-001",
    "https://client.example.com/openapi/callback/doc-audit",
)

client.submit(Actions.DOC_AUDIT_CHECK, request).require_success()
```

回调接收端成功时必须在 5 秒内返回：

```http
HTTP/1.1 200 OK
Content-Type: application/json

{"code":"200","msg":"success"}
```

失败后服务端每 5 分钟扫描重试一次，最多重试 10 次，不包含首次投递；最大投递次数为 11 次。
回调属于至少一次投递，接收端必须根据 `taskId` 做幂等。当前回调不携带 OpenAPI HMAC 签名，
重要结果应通过 `query_task` 再次核验。

## 11. 原生 asyncio 客户端

```python
import asyncio
import os

from ruijian_openapi import (
    Actions,
    AsyncMode,
    AsyncRuijianOpenApiClient,
    InvokeRequest,
)


async def main() -> None:
    async with AsyncRuijianOpenApiClient(
        endpoint=os.environ.get(
            "RUIJIAN_OPENAPI_ENDPOINT",
            "https://openapi.ruijianai.com",
        ),
        access_key=os.environ["RUIJIAN_ACCESS_KEY"],
        access_secret=os.environ["RUIJIAN_ACCESS_SECRET"],
    ) as client:
        response = await client.invoke(
            Actions.TEXT_AIGC_CHECK,
            InvokeRequest(parameters={"text": "待检测文本"}),
            AsyncMode.SYNC,
        )
        response.require_success()
        print(response.data)


asyncio.run(main())
```

`AsyncRuijianOpenApiClient` 同样提供 `submit`、`query_task`、`wait_for_task` 和
`submit_and_wait`，这些方法都需要使用 `await`。

## 12. 错误处理

```python
from ruijian_openapi import (
    OpenApiError,
    OpenApiResponseError,
    OpenApiServiceError,
    OpenApiTransportError,
)

try:
    response = client.invoke(action, request, AsyncMode.SYNC)
    response.require_success()
except OpenApiServiceError as exc:
    # 服务端返回了结构化业务错误。
    print(exc.response.request_id)
    print(exc.response.code)
    print(exc.response.message)
except OpenApiTransportError as exc:
    # DNS、连接、TLS、超时等网络异常。
    print(exc)
except OpenApiResponseError as exc:
    # 服务端返回空响应或不符合协议的 JSON。
    print(exc)
except OpenApiError as exc:
    print(exc)
```

SDK 不自动重试调用接口，因为网络异常发生时，请求可能已经到达服务端并产生计费。业务方重试时：

1. 为业务请求生成稳定的 `idempotency_key`。
2. 同一业务请求重试时保持该值不变。
3. 使用有限次数指数退避。
4. 不重试明确的参数、权限或签名错误。
5. 每次 HTTP 重试由 SDK 自动生成新的 Nonce 和签名。

## 13. Action 列表

| Action 常量 | Action 值 | 能力 | 推荐模式 |
| --- | --- | --- | --- |
| `IMAGE_DEEPFAKE_CHECK` | `ImageDeepfakeCheck` | 图片深度伪造检测 | `SYNC/AUTO` |
| `VIDEO_DEEPFAKE_CHECK` | `VideoDeepfakeCheck` | 视频深度伪造检测 | `ASYNC` |
| `PHOTO_IDENTITY_DETECT` | `PhotoIdentityDetect` | 人像身份核验 | `SYNC` |
| `TEXT_AIGC_CHECK` | `TextAigcCheck` | 文本 AIGC 检测 | `SYNC` |
| `IMAGE_AIGC_CHECK` | `ImageAigcCheck` | 图片 AIGC 检测 | `SYNC` |
| `IMAGE_ALL_DETECT` | `ImageAllDetect` | 图片综合检测 | `SYNC` |
| `AUDIO_DEEPFAKE_CHECK` | `AudioDeepfakeCheck` | 音频深度伪造检测 | `SYNC` |
| `VIDEO_ASYNC_DETECT` | `VideoAsyncDetect` | AI 生成合成视频检测 | `ASYNC` |
| `IMAGE_PS_DETECT` | `ImagePsDetect` | 图片 PS 检测 | `SYNC` |
| `DOC_PS_DETECT` | `DocPsDetect` | 文档 PS 检测 | `SYNC` |
| `TEXT_PROMPT_CHECK` | `TextPromptCheck` | 文本指令安全检测 | `SYNC` |
| `IMAGE_COMPLIANCE_CHECK` | `ImageComplianceCheck` | 图片合规检测 | `SYNC` |
| `TEXT_SAFETY_CHECK_WITH_ANSWER` | `TextSafetyCheckWithAnswer` | 文本安全检测与代答 | `SYNC` |
| `TEXT_CONTENT_CHECK` | `TextContentCheck` | 文本内容安全检测 | `SYNC` |
| `TEXT_STREAM_SAFETY_CHECK` | `TextStreamSafetyCheck` | 流式文本安全检测 | `SYNC` |
| `IMAGE_AIGC_COMPLIANCE_CHECK` | `ImageAigcComplianceCheck` | 图片 AIGC 合规检测 | `SYNC` |
| `AUDIO_AIGC_COMPLIANCE_CHECK` | `AudioAigcComplianceCheck` | 音频 AIGC 合规检测 | `SYNC` |
| `VIDEO_AIGC_COMPLIANCE_CHECK` | `VideoAigcComplianceCheck` | 视频 AIGC 合规检测 | `ASYNC` |
| `TEXT_MULTI_TURN_SAFETY_CHECK` | `TextMultiTurnSafetyCheck` | 多轮对话安全检测 | `SYNC` |
| `DOC_AUDIT_CHECK` | `DocAuditCheck` | 文档内容审核 | `ASYNC` |
| `TEXT_AIGC_PARAGRAPH_CHECK` | `TextAigcParagraphCheck` | 文本 AIGC 段落检测 | `SYNC` |

新增 Action 时，即使 SDK 尚未新增常量，也可以直接传 Action 字符串：

```python
client.invoke("NewCapabilityAction", request, AsyncMode.AUTO)
```

21 个 Action 的完整请求参数示例位于 `tests/action_examples.py`，对应协议回归位于
`tests/test_all_actions.py`。

## 14. 签名一致性

SDK 对请求字典只执行一次 JSON 序列化，并使用同一个 `bytes` 对象完成：

1. `X-RJ-Content-SHA256` 计算。
2. CanonicalRequest 和 HMAC-SHA256 签名。
3. HTTP Body 发送。

因此 Python 字典顺序不会导致签名与服务端不一致。真正需要保证的是参与摘要的 Body 字节与
实际发送字节完全相同。`Host` 和 `X-RJ-Async` 不参与签名，版本默认发送 `V1.0`。

## 15. 测试

### 15.1 本地测试

```bash
python -m pytest
```

测试覆盖：

- 固定签名向量
- 最终 Body、摘要和实际发送字节一致性
- Base64 文件输入
- 默认版本和同步/异步 Header
- 任务查询使用原 Action
- 同步与原生异步客户端
- 21 个 Action 的完整请求构造
- 业务错误保留

本地测试使用 HTTPX MockTransport，不访问远程服务，也不会产生调用费用。

### 15.2 真实环境回归

真实环境测试默认跳过。执行后会调用全部服务并可能产生费用，必须显式配置：

```bash
export RUIJIAN_RUN_LIVE_TESTS="true"
export RUIJIAN_OPENAPI_ENDPOINT="https://openapi-beta.ruijianai.com"
export RUIJIAN_ACCESS_KEY="your-access-key"
export RUIJIAN_ACCESS_SECRET="your-access-secret"

export RUIJIAN_TEST_IMAGE_URL="https://cdn.example.com/test/image.jpg"
export RUIJIAN_TEST_VIDEO_URL="https://cdn.example.com/test/video.mp4"
export RUIJIAN_TEST_AUDIO_URL="https://cdn.example.com/test/audio.mp3"
export RUIJIAN_TEST_DOCUMENT_URL="https://cdn.example.com/test/document.pdf"
export RUIJIAN_TEST_USER_ID_INFO="your-sm4-base64-user-id-info"
export RUIJIAN_TEST_CALLBACK_URL="https://client.example.com/openapi/callback"

python -m pytest -m live -s
```

真实回归对强制异步接口只验证任务成功受理并返回 `taskId`，不会等待所有算法任务执行完成。
