Metadata-Version: 2.4
Name: modlink-agent
Version: 1.1.0
Summary: 模联 ModLink 的 Agent 工具包——把平台模型封装为 CLI 与 MCP 工具，供 AI Agent 调用
Author: ModLink Platform Team
License-Expression: Apache-2.0
Project-URL: Homepage, https://asr.syncmeet.tech:9443
Project-URL: Documentation, https://asr.syncmeet.tech:9443/docs
Keywords: mcp,agent,asr,tts,llm,modlink,语音识别,语音合成,图像生成
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: requests>=2.28
Provides-Extra: mcp
Requires-Dist: mcp<2,>=1.0; extra == "mcp"

# modlink-agent —— 模联平台的 Agent 工具包

把模联（ModLink）的模型能力封装成 **CLI 命令**与 **MCP 工具**，供 AI Agent 调用。

平台侧接口见 [API 文档](../../docs/api-reference.md)，本包是它的 Agent 友好封装。

---

## ⚠️ 安全约束：不提供对话模型

**本包刻意不封装 LLM 对话模型（`qwen3-coder-next`）**，CLI 也没有 `chat` 子命令。

平台十余个模型里，只有对话模型同时具备两个特性：

1. **接受任意提示词** —— 其余模型都是「输入固定格式数据、输出固定格式结果」
2. **能调用工具** —— 可通过 `tools` / `tool_choice` 组合出调用链

把它做成 Agent 可调用的工具，等于**把 Agent 的控制面交给不可信输入**。提示词注入的后果是任意的：

- 诱导 Agent 读取不该读的文件、执行不该执行的命令
- 通过 `tools` 组合出「看起来像合法业务调用」的越权链
- 借 Agent 的身份绕过人类已经设好的权限边界

**这不是能力缺失，是刻意的取舍。**

- 需要使用对话模型的用户：仍可直接调平台的 `/v1/chat/completions` + 自建 API Key
- Agent 的规划与生成能力：应交给宿主自带的主模型，不要二次调用本平台的对话模型

该约束由单元测试与端到端验证双重钉死（`hasattr` 断言 + 子命令集检查），
防止日后有人「顺手加回来」。

---

## 安装

```bash
pip install -e .              # 只用 CLI
pip install -e ".[mcp]"       # 需要 MCP Server
```

依赖只有 `requests` 一个；MCP SDK 是可选项，只用 CLI 不需要装。

---

## 配置 API Key

API Key 在模联**用户控制台 → API Key** 页创建。
支持四种来源，**优先级从上到下**：

| 优先级 | 来源 | 说明 |
|---|---|---|
| 1 | 环境变量 `MODLINK_API_KEY` | **推荐**。不进 shell history 与进程列表 |
| 2 | 配置文件 `~/.config/modlink-agent/config.json` 的 `apiKey` | 免重复输入 |
| 3 | MCP 客户端配置的 `env` 段 | 见下方「MCP 接入」 |
| 4 | 交互式输入（仅 CLI 且在 TTY 下） | 首次试用 |

> **优先级不可绕过**：环境变量存在时会忽略明文。
> 否则「先设了环境变量、后又改了面板配置」会静默用错一把 Key，
> 表现为「明明配了 A 却按 B 的额度扣费」，极难排查。

### 为什么不用命令行传 Key

`--api-key ml_live_xxx` 会进 shell history，也会被 `ps aux` 看到。
环境变量在同一台机器上通过 `/proc/PID/environ` 也能被同用户读到，
但**不会落进 history 与审计日志**——这是 CLI 工具的通行取舍。

`--key` 参数仍然保留（供程序化调用），但优先级低于环境变量。

### 全部环境变量

| 变量 | 必填 | 默认 | 说明 |
|---|---|---|---|
| `MODLINK_API_KEY` | ✅ | — | API Key |
| `MODLINK_BASE_URL` | | `https://asr.syncmeet.tech:9443` | 网关地址 |
| `MODLINK_WORK_DIR` | | `./modlink-out` | 音频/图片落盘目录 |
| `MODLINK_TIMEOUT` | | `600` | 单请求超时（秒） |
| `MODLINK_ASYNC_THRESHOLD` | | `15` | 超过该秒数视为长任务，转异步轮询 |

---

## 模型白名单自动生效

在用户端创建 Key 时若设了「模型白名单」，本包**无需任何配置**即可正确受限：

```bash
# 用只授权向量的 Key
export MODLINK_API_KEY=ml_live_xxx
modlink models
# 只列出 qwen3-embedding —— 平台侧 /v1/models 已按白名单过滤

modlink translate "你好" --to 英语
# 当前 API Key 无权调用模型 hymt2-translate。该 Key 的模型白名单为：qwen3-embedding。
# 如需调用，请在「API Key」页修改白名单或新建 Key。
```

---

## CLI 用法

```bash
modlink models                      # 列出当前 Key 可调用的模型
modlink health                      # 检查平台可达性

modlink transcribe a.wav --model funasr --diarization
modlink synthesize "欢迎使用模联" --model cosyvoice
modlink translate "你好" --to 英语
modlink embed "模联平台" --dimensions 1024
modlink image photo.jpg --task upscale
modlink generate "一只橘猫" --width 1024 --height 1024
```

> **没有 `chat` 子命令** —— 见上方「安全约束」。

所有子命令支持 `--json` 输出结构化结果，便于脚本与 Agent 解析：

```bash
modlink models --json | jq '.data[].id'
```

### 图像任务对应关系

| `--task` | 默认模型 | 额外必填 |
|---|---|---|
| `remove_background` | `birefnet-matting` | — |
| `erase` | `lama-inpaint` | `--mask` |
| `upscale` | `realesrgan-upscale` | — |
| `face_restore` | `codeformer-restore` | — |
| `face_swap` | `inswapper-face-swap` | `--source-image` |

---

## 作为 Python 库

```python
from modlink_agent import ModLinkClient

client = ModLinkClient()                     # 自动读环境变量

# 文本类直接返回结果
print(client.translate("你好", target_lang="英语"))
print(client.embed(["文档A", "文档B"], dimensions=1024))

# 二进制类返回**文件路径**，不是内容
audio = client.synthesize("欢迎使用模联")
print(audio.path)          # ./modlink-out/tts-20261006-200547-xxx.wav
print(audio.describe())    # 一句话摘要（不含任何二进制内容）
```

> 客户端**没有** `chat` / `chat_text` / `stream_chat` 方法 —— 见上方「安全约束」。

---

## 三个为 Agent 做的关键设计

### ① 二进制不进上下文

一张 1024×1024 图转 base64 约 1.4MB token，会直接撑爆 Agent 上下文。
本包**一律落盘后只返回绝对路径**：

```python
result = client.generate_image("一只猫")
print(result.path, result.size_bytes)   # 路径 + 字节数，不含内容
```

### ② 429 自动退避

图像模型**同卡互斥**（平台文档明写「必须串行调用」），Agent 天生并发，一并发就吃 429。
若把 429 原样抛出，Agent 只会反复重试并再次 429，形成死循环。

本包读 `Retry-After` 头做指数退避，重试耗尽后返回**可执行的提示**：

```
模型互斥或触发限流（429），重试 4 次仍失败。
平台说明：图像生成类模型**同卡互斥，必须串行调用**。
请改为逐个调用（等上一个返回后再发下一个），或稍后再试。
```

### ③ 长任务不超时

- **ASR**：长音频自动走 `/v1/asr/tasks` 异步接口，内部轮询到完成
  （按音频大小启发式判断，调用方无需决策）
- **图像**：平台无异步接口，用长超时 + 返回值带耗时秒数

实测真机耗时（供参考，用于设置 MCP 客户端超时）：

| 操作 | 耗时 |
|---|---|
| 翻译（2 条批量） | 0.16s |
| 向量化（2 条 / 256 维） | 0.09s |
| 图像超分（64×64） | 0.15s |
| 语音合成（1 句 → 86KB WAV） | 1.38s |
| 语音转写（1 句） | 0.29s |

> 图像**生成**（1024×1024，20B 模型）实测需数十秒到数分钟，
> 且同卡互斥（并发会 429）——MCP 客户端应给足超时并串行调用。
> 本包已内置 429 退避，但客户端超时也要相应放宽。

---

## 关于函数调用（工具调用）

平台侧的 `/v1/chat/completions` **完整支持** `tools` / `tool_choice`
与多轮 `tool_calls` 回传（**平台只做透传，不执行工具**——模型只负责
「提出调用请求」，由调用方执行后把结果作为 `role=tool` 消息回传，
漏掉 `tool_call_id` 会让模型重复发起同一次调用）。

但本包**不封装对话模型**（见上方「安全约束」），因此 Agent 侧
不会拿到这个入口——函数调用能力请直接在平台侧使用。

---

## MCP 接入

### 配置

`command` 指向装了 MCP SDK 的那个解释器 —— 若报 `No module named mcp`，
说明该解释器缺依赖，用装了 SDK 的那个（如 `…/venvs/mcp-agent/bin/python`）。

**方式一：Key 走环境变量（推荐）**

```json
{
  "mcpServers": {
    "modlink": {
      "command": "python",
      "args": ["-m", "modlink_agent.mcp_server"],
      "env": {
        "MODLINK_API_KEY": "ml_live_xxxxxxxx"
      }
    }
  }
}
```

`env` 段写死的值就是「配置文件/显式传入」层，优先级低于机器上真实的环境变量
（见上方 Key 表格第 1 行）。也就是说：**面板里填了明文，但进程环境变量里
存在 `MODLINK_API_KEY` 时，后者胜出**。这样 Key 可以从 CI secret 或
`~/.bashrc` 统一注入，不必落到面板配置里。

**方式二：环境变量已在 shell 里**

```json
{
  "mcpServers": {
    "modlink": {
      "command": "python",
      "args": ["-m", "modlink_agent.mcp_server"],
      "env": {}
    }
  }
}
```

### 工具清单（7 个）

按「族」暴露，不按模型拆成一堆工具 —— 一模型一工具会让 Agent 在 20+ 个
名字里纠结选择，且模型下线后工具名就变成死引用。

| 工具 | 作用 | 覆盖模型 |
|---|---|---|
| `modlink_list_models` | 列出当前 Key 可调模型 | 全部（按白名单过滤） |
| `modlink_transcribe` | 语音转写 | funasr / sensevoice / paraformer-bilingual / zipformer-bilingual |
| `modlink_synthesize` | 语音合成 | cosyvoice / indextts |
| `modlink_translate` | 文本翻译 | hymt2-translate |
| `modlink_embed` | 文本向量化 | qwen3-embedding |
| `modlink_image_process` | 图像处理 | 抠图 / 消除 / 超分 / 人脸修复 / 换脸 |
| `modlink_image_generate` | 图像生成 | flux2-generate / qwen-image-2.1 |

**不包含对话工具**（`modlink_chat` 不存在）——与 CLI 保持一致的安全边界。
客户端可用 `modlink_list_models` 自查可调范围，但其中不会出现对话模型。

### Server Instructions

Server 带一段面向 Agent 的说明，覆盖三类「模型自身不会说」的知识：

1. 二进制结果返回的是**文件路径**而非内容，需要 Agent 再用文件工具读取
2. 图像生成/处理**同卡互斥**，并发会 429
3. 图像生成耗时长（1024×1024 数十秒到数分钟），需要给足超时

`modlink_list_models` 的存在是必要的：不调它，Agent 无从知道
「这个 Key 到底能用什么」，只能靠猜或反复试错吃 403。

---

## 错误处理

| 异常 | 触发条件 | 提示要点 |
|---|---|---|
| `AuthenticationError` | 401 | Key 无效/吊销/过期，指向控制台检查 |
| `ForbiddenError` | 403 | 模型白名单未授权，提示去改白名单 |
| `InsufficientCredits` | 402 | 积分不足，指向充值页 |
| `RateLimitError` | 429 | 模型互斥，提示改串行 |
| `ModelNotFound` | 404 / 503 | 模型标识错或未部署 |
| `UpstreamError` | 5xx | 平台或引擎异常 |

错误文案都是**给 Agent 读的**，每条都包含下一步该做什么——
Agent 拿到「HTTP 403」只会反复重试同一个错误调用。

---

## 测试

```bash
python tests/test_client_local.py     # 32 项：起本地假 HTTP 服务，不连生产
python tests/test_mcp_local.py        # 44 项：含真实 stdio 握手
```

`test_mcp_local.py` 会真的起一个 `python -m modlink_agent.mcp_server`
子进程，走完整 JSON-RPC 握手并 `list_tools` —— 因为 **stdio 传输下
stdout 是协议通道，任何一处 `print` 都会破坏 JSON-RPC 帧**，
而这类问题不真起进程就测不出来。

真机验证覆盖 7 个族各调一次。
