Metadata-Version: 2.4
Name: modlink-agent
Version: 1.1.1
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` 断言 + 子命令集检查），
防止日后有人「顺手加回来」。

---

## 安装

已发布到 PyPI，**MCP 客户端推荐用 `uvx`**——它会自动准备隔离环境，无需预先安装任何东西：

```json
{
  "mcpServers": {
    "modlink": {
      "command": "uvx",
      "args": ["--from", "modlink-agent", "--with", "modlink-agent[mcp]", "modlink-mcp"],
      "env": { "MODLINK_API_KEY": "ml_live_xxxxxxxx" }
    }
  }
}
```

四个参数缺一不可，各有原因（**以下均为实测结论，不是推演**）：

| 参数 | 作用 | 不写会怎样 |
|---|---|---|
| `--from modlink-agent` | `uvx` 第一个参数是**包名**，不是命令名 | `uvx modlink-mcp` 会去 PyPI 找一个叫 `modlink-mcp` 的**包**，报 `was not found in the package registry` |
| `--with modlink-agent[mcp]` | uvx 默认**不装 optional-dependencies** | 包装成功但启动即报「缺少 MCP SDK」，且没有任何工具可用 |
| `modlink-mcp` | 命令名 | 执行的是这个入口，而不是 CLI 的 `modlink` |

### 为什么配置里没有 URL

**因为本 Server 跑在用户自己的机器上**，不部署在服务器上：

```
Agent ──stdin/stdout──> 本机 modlink-mcp 进程 ──HTTPS──> 模联网关
                          ↑ 网关地址在这一层，客户端不需要知道
```

你可能见过配置里带 `"url": "http://…"` 的 MCP，那是 **HTTP 传输**
（server 在远端，客户端主动去连）。本包用的是 **stdio 传输**
（客户端在本机起一个子进程，用标准输入输出通信），
所以**配置里本来就不该有 URL 字段** —— 加了反而会让人误以为 server 在远端。

网关地址有内置默认值 `https://asr.syncmeet.tech:9443`，
需要改时在 `env` 里加 `MODLINK_BASE_URL` 覆盖（见「换域名时该做什么」）。

`uvx` 是 `uv` 自带的工具，需要先装一次：

| 平台 | 命令 |
|---|---|
| Windows | `powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"` |
| macOS / Linux | `curl -LsSf https://astral.sh/uv/install.sh \| sh` |

> **包名 `modlink-agent` 与命令名 `modlink-mcp` 不同名**，这是有意的：
> 命令名若复用 `modlink`，`uvx modlink-agent` 会去找名为 `modlink-agent` 的命令——
> 而我们注册的是 `modlink`，找不到。代价是用户必须写 `--from`。

不用 uvx 也能走 pip，写法更短：

```bash
pip install "modlink-agent[mcp]"     # 含 MCP Server
pip install modlink-agent            # 仅 CLI
```

依赖只有 `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 接入

### 配置

**方式一：`uvx`（推荐）**

```json
{
  "mcpServers": {
    "modlink": {
      "command": "uvx",
      "args": ["--from", "modlink-agent", "--with", "modlink-agent[mcp]", "modlink-mcp"],
      "env": {
        "MODLINK_API_KEY": "ml_live_xxxxxxxx"
      }
    }
  }
}
```

`args` 四个值缺一不可，原因见上方「安装」节的表格。

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

> **Claude Desktop 需填 uvx 的完整路径**（它的 PATH 常常不含用户级可执行目录）。
> 先在终端跑 `where uvx`（Windows）或 `which uvx`（macOS/Linux），
> 把结果填进 `command`。其它 IDE 插件（Cursor / VS Code 等）用 `uvx` 即可。

> **首次启动会下载约 34 个包**（含 MCP SDK 及其依赖），耗时十几秒到一分钟，
> 取决于网速。某些客户端在首次启动时会因超时而报「连接失败」，
> 属正常现象——等它下载完重连一次即可，后续启动是秒开。

**方式二：已用 pip 装到本地**

```json
{
  "mcpServers": {
    "modlink": {
      "command": "modlink-mcp",
      "env": {}
    }
  }
}
```

Windows 上若提示找不到命令，改用完整路径
（`C:\Users\<你>\AppData\Roaming\Python\Python312\Scripts\modlink-mcp.exe`），
或改用 `python -m modlink_agent.mcp_server`。

**方式三：从源码目录跑**

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

前两种是用户场景，第三种供平台自身开发与排障。

### 工具清单（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 | 模型标识错或未部署 |
| `NetworkError` | 连不上网关 | **点名 `MODLINK_BASE_URL`，并明确不要重试** |
| `UpstreamError` | 5xx | 平台或引擎异常 |

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

`NetworkError` 尤其关键：**域名是会变的**（平台换接入点、或用户填错地址）。
裸 `requests` 异常里只有英文堆栈，Agent 读不出「这是配置问题」，
于是反复重试同一个必然失败的地址。工具包把它翻译成
「请检查 `MODLINK_BASE_URL`……这是配置问题，重试无用」。

---

## 换域名时该做什么

网关地址有四级取值，优先级从高到低：

1. 显式传入（`--base-url` / `ModLinkClient(base_url=...)`）
2. 环境变量 `MODLINK_BASE_URL`
3. 配置文件 `~/.config/modlink-agent/config.json` 的 `baseUrl` 字段
4. 内置默认值 `config.DEFAULT_BASE_URL`

平台更换域名时，改代码里的默认值**只对升级到新版的人生效**——
已经装在用户机器上的旧版本仍写着旧地址。完整处置：

1. 改 `config.py` 的 `DEFAULT_BASE_URL` 与 `pyproject.toml` 的 `project.urls`，发新版；
2. **旧域名保留一段转发**，避免存量用户直接断线；
3. 公告新的 `MODLINK_BASE_URL` 取值。

---

## 已知风险

**`uvx` 每次启动都重新解析依赖。** pip 装一次的用户不受上游升级影响，
但 `uvx` 用户每次启动都在解析 —— 上游一次破坏性发布就能让所有用户的
MCP **同时崩溃**。

因此 `mcp` 依赖硬 pin `>=1.0,<2`（mcp 2.x 已移除 `mcp.server.fastmcp`）。
先例：`mcp-search-console` 0.3.3 就是为此发过补丁版本。

若将来 `mcp` 2.x 的适配完成，应**同时**验证 `uvx` 与 `pip` 两条路径，
再放宽上限。

**首次启动会下载约 34 个包**（MCP SDK 及其依赖）。
部分 MCP 客户端在首次启动时会因超时而报「连接失败」——
这是下载没完成，不是配置错误，等它下完重连即可。

**包名与命令名不同名。** `uvx` 的第一个位置参数是包名，
所以必须写 `--from modlink-agent`，不能简写成 `uvx modlink-mcp`。
这是为避免「包名与命令名相同」带来的歧义所付的代价。

---

## 测试

```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 个族各调一次。
