1. 从手动下载到自建工具:这个场景到底解决什么问题
C站(Civitai)上的模型动辄几个 GB,手动点下载、等进度条、断线重连,一套流程下来人已经麻了。更麻烦的是,当你需要批量拉一批模型、打包发给同事,或者想把模型元信息整理成自己的资料库时,纯手工操作基本不可行。我试过市面上一些脚本和浏览器插件,要么功能只做了一半,要么跑着跑着就崩,稳定性堪忧。
所以这次干脆自己撸一个:前端用 React 做任务提交和列表展示,后端用 PHP 7.4+ 写接口层,MySQL 8.0 存模型元信息和任务状态。整个链路拆成「提交下载任务 → 后端拉取模型信息 → 下载文件 → 分卷打包 → 上传网盘 → 回写状态」几个独立环节,用定时任务串起来。五天时间,从原型到跑通,核心不是写多复杂的代码,而是把每个环节的边界划清楚。
这里有个容易被忽略的点:当你同时用多个 AI 工具(比如 Trae 写前端、Dify 跑工作流、Claude Code 做代码审查)时,每个工具都要单独配 Key、单独管额度,切换成本很高。我的做法是把 TaoToken 的统一 Key 塞进settings.json,让所有工具走同一个鉴权入口,省掉反复登录和配置的麻烦。下面把整个搭建过程和配置骨架拆开讲。
2. TaoToken 前置:统一 Key 与 settings.json 骨架
在开始写下载器之前,先把 AI 工具的鉴权问题解决掉。个人开发者手里往往有好几个 AI 编码工具,每个都要单独申请 Key、单独记额度,时间一长就乱了。TaoToken 的思路是提供一个统一的 API 入口,你只需要一个 Key,就能在多个工具之间复用。
官网地址:https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=
API 入口:https://taotoken.net/api
具体操作上,先去控制台创建一个 API Key,然后把它写进项目的settings.json。这个文件可以放在项目根目录,也可以放在用户配置目录,取决于你用的工具链。下面是一个可复制的配置骨架,字段名按你实际使用的工具调整:
{ "ai": { "provider": "taotoken", "base_url": "https://taotoken.net/api", "api_key": "sk-你的TaoTokenKey", "models": { "coding": "claude-sonnet", "chat": "gpt-4o", "workflow": "grok-3" }, "timeout": 120, "max_retries": 3 }, "project": { "name": "civitai-downloader", "frontend": "react", "backend": "php74", "database": "mysql8" } }注意:
api_key不要提交到 Git 仓库,建议用环境变量注入,或者在.gitignore里排除settings.json。
配置好之后,你的 React 前端、PHP 后端、Dify 工作流都可以读同一个settings.json,不用每个工具单独配一遍。这一步做完,后面写代码时调用 AI 接口就统一走 TaoToken 的入口,省心不少。
3. 可复制配置:React + PHP + MySQL 三层落地
3.1 React 前端:任务提交与列表展示
前端核心就两个页面:一个提交表单,一个任务列表。表单里填 C 站模型页面 URL、选择任务类型(下载/打包/上传),提交后调 PHP 接口写入 MySQL。列表页展示任务状态、文件大小、下载进度,支持按状态筛选。
用 Trae 生成前端代码时,把原型截图和腾讯云验证码的接入文档一起丢进去,Claude-3.7-Sonnet 基本能一次跑通。关键是把组件拆清楚:TaskForm负责提交,TaskList负责展示,TaskItem负责单条状态渲染。状态管理用 React 自带的useState+useEffect就够了,不需要上 Redux。
// TaskForm.jsx 核心逻辑 const submitTask = async (url, taskType) => { const res = await fetch('/api/task/create', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ page: url, task_type: taskType }) }); const data = await res.json(); if (data.code === 0) { setTasks(prev => [data.task, ...prev]); } };3.2 PHP 接口层:拆成独立任务,定时触发
后端不搞复杂的状态机,就是把每个环节拆成独立任务:下载任务只管下载,上传任务只管上传,完成就改状态。因为服务器资源有限,同一时间只允许一个下载任务跑,所以不用考虑分布式锁。
数据库访问用php-crud-api封装,直接通过 URL 操作数据,省掉手写 CRUD 的功夫。PHP 层只负责调 API、写状态、记日志。
// download.php 核心逻辑 $task = getTaskById($taskId); if ($task['status'] !== 'pending') { exit('task already running'); } updateTaskStatus($taskId, 'downloading'); $file = downloadModel($task['download_url'], $task['folder']); if ($file) { updateTaskStatus($taskId, 'downloaded'); updateTaskField($taskId, 'file', $file); } else { updateTaskStatus($taskId, 'failed'); }3.3 MySQL 表结构:task 与 volume
任务表存模型元信息和状态,分卷表存打包后的文件信息。字段设计上,status用字符串而不是枚举,方便后续扩展状态;page存原始 URL,方便回溯。
CREATE TABLE `task` ( `Id` INT PRIMARY KEY AUTO_INCREMENT, `created` DATETIME, `updated` DATETIME, `name` VARCHAR(100), `file_size` VARCHAR(20), `base_model` VARCHAR(20), `type` VARCHAR(50), `page` VARCHAR(500), `status` VARCHAR(50), `task_type` VARCHAR(50), `download_url` VARCHAR(500), `md5` VARCHAR(50), `folder` VARCHAR(50), `file` VARCHAR(50), `pan` VARCHAR(255), `code` VARCHAR(10), `cover` TEXT, `description` TEXT ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `volume` ( `Id` INT PRIMARY KEY AUTO_INCREMENT, `model` VARCHAR(50), `folder` VARCHAR(50), `file` VARCHAR(100), `status` VARCHAR(20), `created` VARCHAR(20) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;3.4 Dify 工作流:模型描述生成
C 站采集回来的模型描述里经常有违禁词,直接发布风险高。用 Dify 跑一个工作流,把原始描述丢给 Grok3 重写,输出 800 字以内的干净介绍。提示词里明确要求「不出现描述性、裸体、色情等不健康用语」,输出纯文本,不要 Markdown。
工作流节点很简单:输入模型页面 URL → 抓取原始描述 → Grok3 重写 → 输出到 Typecho。发布时直接往 API 提交 C 站页面地址就行。
4. 验证请求:一次下载任务从请求到落库
配置写完之后,跑一次完整链路验证。用 curl 模拟前端提交任务:
curl -X POST https://your-domain.com/api/task/create \ -H "Content-Type: application/json" \ -d '{"page":"https://civitai.com/models/xxxxx","task_type":"download"}'预期返回:
{ "code": 0, "task": { "Id": 101, "name": "example-model", "status": "pending", "task_type": "download", "page": "https://civitai.com/models/xxxxx" } }然后查数据库确认落库:
SELECT Id, name, status, task_type, created FROM task WHERE Id = 101;如果status是pending,说明任务已写入,定时任务会把它捞起来执行。等几分钟后再查,status应该变成downloading或downloaded,file字段有值。这一步跑通,说明前端 → PHP → MySQL → 定时任务 → 下载器整条链路是通的。
再验证一下 TaoToken 的 Key 是否生效:在 PHP 里调一次模型对话接口,确认返回正常。
curl https://taotoken.net/api/v1/chat/completions \ -H "Authorization: Bearer sk-你的TaoTokenKey" \ -H "Content-Type: application/json" \ -d '{"model":"gpt-4o","messages":[{"role":"user","content":"ping"}]}'返回里有choices字段就说明 Key 配置正确。
5. 本篇常见错排查
问题一:PHP 调 API 返回 401
先检查settings.json里的api_key有没有多余空格,再确认base_url是不是https://taotoken.net/api。如果用的是环境变量,打印一下getenv('TAOTOKEN_KEY')看是否为空。
问题二:MySQL 写入中文乱码
建表时确认CHARSET=utf8mb4,PHP 连接时设置SET NAMES utf8mb4。如果用的是php-crud-api,在配置文件里指定charset参数。
问题三:下载任务卡在 downloading 不动
检查定时任务是否在跑,看 Cronicle 的任务日志。常见原因是下载 URL 过期,C 站的下载链接有时效性,需要重新拉取。另外确认服务器磁盘空间够不够,几个 GB 的模型很容易把盘写满。
问题四:React 前端提交后列表不刷新
检查fetch的返回状态码,如果是 200 但code不是 0,说明后端逻辑有问题。可以在 PHP 里加一行error_log把请求参数记下来,方便排查。
问题五:TaoToken Key 在多个工具间冲突
如果你同时在 Trae、Dify、Claude Code 里用同一个 Key,注意并发限制。建议在settings.json里给每个工具单独配max_retries,避免同时重试把额度打满。
6. 统一鉴权与后续扩展
把 TaoToken 的 Key 塞进settings.json之后,最大的好处是切换工具时不用重新配鉴权。你可以在项目根目录放一份settings.json,所有 AI 工具读同一个文件,Key 变了只改一处。
如果你主要做长期编码和 Agent 任务,可以看看 Coding Plan 的额度方案,比按次调用更划算。需要管理多个 Key 或者查看用量,去控制台就行。接入文档里有完整的 API 说明和示例代码,照着改就能用。
后续扩展方向:把分卷打包和网盘上传也接进定时任务,任务表加一个pan字段记录网盘路径,code字段存分享码。模型资讯站那边用 Typecho 的 Markdown 接口自动发布,Dify 工作流跑完直接写库。整个链路跑顺之后,你只需要在 React 页面提交一个 C 站 URL,剩下的下载、打包、上传、发布全自动完成。