Files
ulthon_information/app/common/tools/ai/ModelsDevSync.php
augushong 34fe255829 feat(phone-image): 增加翻页预览与无封面图排版样式
- 为手机截图生成器添加翻页功能,支持在生成前预览各页内容
- 增加无封面图时的排版样式,使用装饰线条和居中布局
- 改进图片处理逻辑,清除内联样式并展平嵌套包装元素
- 修复 models.dev 同步接口,支持 GET 请求获取缓存数据
- 优化网络请求,添加直连失败后的本地代理重试机制
2026-05-01 16:31:26 +08:00

278 lines
7.1 KiB
PHP

<?php
namespace app\common\tools\ai;
/**
* models.dev 数据同步与缓存.
*
* 从 https://models.dev/api.json 获取AI模型供应商和模型目录数据,
* 缓存到 runtime/ai/models_dev_cache.json, 有效期24小时.
*/
class ModelsDevSync
{
/**
* @var string models.dev API地址
*/
protected $apiUrl = 'https://models.dev/api.json';
/**
* @var string 缓存文件路径
*/
protected $cachePath;
/**
* @var int 缓存有效期(秒), 默认24小时
*/
protected $cacheTtl = 86400;
public function __construct()
{
$this->cachePath = app()->getRuntimePath() . 'ai' . DIRECTORY_SEPARATOR . 'models_dev_cache.json';
}
/**
* 同步供应商数据到本地缓存.
*
* @return array 同步结果统计
*
* @throws \Exception
*/
public function syncProviders(): array
{
$data = $this->fetchApi();
if ($data === false) {
throw new \Exception('无法连接 models.dev API');
}
$parsed = $this->parseApiData($data);
$this->writeCache($parsed);
return $parsed;
}
/**
* 获取所有供应商列表.
*
* @return array
*/
public function getProviders(): array
{
$cache = $this->readCache();
if ($cache === null) {
return [];
}
return $cache['providers'] ?? [];
}
/**
* 获取指定供应商的模型列表.
*
* @param string $providerId 供应商标识
*
* @return array
*/
public function getModelsByProvider(string $providerId): array
{
$cache = $this->readCache();
if ($cache === null) {
return [];
}
$models = $cache['models'][$providerId] ?? [];
return $models;
}
/**
* 获取缓存状态信息.
*
* @return array
*/
public function getCacheStatus(): array
{
$exists = file_exists($this->cachePath);
$mtime = $exists ? filemtime($this->cachePath) : 0;
$expired = ($mtime + $this->cacheTtl) < time();
return [
'exists' => $exists,
'last_sync' => $mtime > 0 ? date('Y-m-d H:i:s', $mtime) : '从未同步',
'expired' => $expired,
'provider_count' => count($this->getProviders()),
];
}
/**
* 从API获取原始数据.
*
* @return string|false
*/
protected function fetchApi()
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $this->apiUrl);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Accept: application/json',
]);
// 尝试直连,失败后使用本地代理
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_errno($ch);
curl_close($ch);
if ($curlError !== 0 || $httpCode !== 200) {
// 直连失败,尝试通过本地代理
$response = $this->fetchViaProxy();
if ($response !== false) {
return $response;
}
return false;
}
return $response;
}
/**
* 通过本地代理获取数据.
*
* @return string|false
*/
protected function fetchViaProxy()
{
$proxyUrl = 'http://127.0.0.1:7005';
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $this->apiUrl);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
curl_setopt($ch, CURLOPT_PROXY, $proxyUrl);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Accept: application/json',
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200 || $response === false) {
return false;
}
return $response;
}
/**
* 解析API数据为统一格式.
*
* @param string $raw JSON原始数据
*
* @return array
*/
protected function parseApiData(string $raw): array
{
$data = json_decode($raw, true);
if (!is_array($data)) {
return ['providers' => [], 'models' => []];
}
$providers = [];
$models = [];
foreach ($data as $providerId => $providerData) {
if (!is_array($providerData)) {
continue;
}
$provider = [
'id' => $providerId,
'name' => $providerData['name'] ?? ucfirst($providerId),
'url' => $providerData['url'] ?? '',
'docs_url' => $providerData['docs_url'] ?? '',
];
// 提取API基础地址(如果有)
if (isset($providerData['api_base_url'])) {
$provider['api_base_url'] = $providerData['api_base_url'];
}
$providers[$providerId] = $provider;
// 提取模型列表
$providerModels = [];
if (isset($providerData['models']) && is_array($providerData['models'])) {
foreach ($providerData['models'] as $modelId => $modelData) {
$model = [
'id' => $modelId,
'name' => $modelData['name'] ?? $modelId,
'description' => $modelData['description'] ?? '',
];
if (isset($modelData['context_length'])) {
$model['context_length'] = (int) $modelData['context_length'];
}
if (isset($modelData['pricing'])) {
$model['pricing'] = $modelData['pricing'];
}
$providerModels[] = $model;
}
}
$models[$providerId] = $providerModels;
}
return [
'providers' => $providers,
'models' => $models,
];
}
/**
* 写入缓存文件.
*
* @param array $data 缓存数据
*/
protected function writeCache(array $data): void
{
$dir = dirname($this->cachePath);
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
file_put_contents($this->cachePath, json_encode($data, JSON_UNESCAPED_UNICODE));
}
/**
* 读取缓存文件(支持过期降级).
*
* @return array|null
*/
protected function readCache(): ?array
{
if (!file_exists($this->cachePath)) {
return null;
}
$content = file_get_contents($this->cachePath);
$data = json_decode($content, true);
if (!is_array($data)) {
return null;
}
return $data;
}
}