Files
ulthon_information/app/common/tools/ai/provider/OpenAiCompatibleAdapter.php
augushong 83a2bd48a2 feat(post): 新增手机图片排版与AI智能排版功能
- 新增手机图片排版功能,支持小红书/抖音尺寸输出
- 新增AI智能排版顾问,支持内容分析与优化推荐
- 新增AI供应商管理,支持多渠道配置与同步
- 新增文章输出管理页面,支持图片预览与批量下载
- 新增字体文件与排版样式配置
2026-05-01 12:23:17 +08:00

229 lines
6.2 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace app\common\tools\ai\provider;
use app\common\tools\ai\AiProviderInterface;
/**
* OpenAI 兼容适配器.
*
* 适用于所有兼容 OpenAI API 格式的供应商:
* OpenAI / Zhipu / DeepSeek / Moonshot / Qwen / etc.
*/
class OpenAiCompatibleAdapter implements AiProviderInterface
{
/**
* @var string 供应商标识(如 zhipu, deepseek)
*/
protected $providerId;
/**
* @var string 供应商显示名称
*/
protected $providerName;
/**
* @param string $providerId 供应商标识
* @param string $providerName 显示名称(可选)
*/
public function __construct(string $providerId, string $providerName = '')
{
$this->providerId = $providerId;
$this->providerName = $providerName ?: ucfirst($providerId);
}
/**
* {@inheritdoc}
*/
public function chat(string $systemPrompt, string $userPrompt, array $options = []): string
{
$apiKey = get_system_config("ai_provider_{$this->providerId}_key", '');
$baseUrl = get_system_config("ai_provider_{$this->providerId}_base_url", 'https://api.openai.com/v1');
$model = $options['model'] ?? get_system_config('ai_default_model', 'gpt-3.5-turbo');
$url = rtrim($baseUrl, '/') . '/chat/completions';
$data = [
'model' => $model,
'messages' => [
['role' => 'system', 'content' => $systemPrompt],
['role' => 'user', 'content' => $userPrompt],
],
];
if (isset($options['temperature'])) {
$data['temperature'] = (float) $options['temperature'];
}
if (isset($options['max_tokens'])) {
$data['max_tokens'] = (int) $options['max_tokens'];
}
$response = $this->httpPost($url, $data, [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
]);
if ($response === false) {
return '';
}
$result = json_decode($response, true);
if (!$result || isset($result['error'])) {
return '';
}
return $result['choices'][0]['message']['content'] ?? '';
}
/**
* {@inheritdoc}
*/
public function chatJson(string $systemPrompt, string $userPrompt, array $options = []): array
{
$jsonInstruction = "\n\n请严格按照JSON格式返回结果不要包含任何其他文字说明。";
$content = $this->chat($systemPrompt . $jsonInstruction, $userPrompt, $options);
if (empty($content)) {
return [];
}
// 尝试提取JSON内容(可能包裹在markdown代码块中)
if (preg_match('/```(?:json)?\s*([\s\S]*?)```/', $content, $matches)) {
$content = $matches[1];
}
$decoded = json_decode(trim($content), true);
return is_array($decoded) ? $decoded : [];
}
/**
* {@inheritdoc}
*/
public function getProviderName(): string
{
return $this->providerName;
}
/**
* {@inheritdoc}
*/
public function getModels(): array
{
$apiKey = get_system_config("ai_provider_{$this->providerId}_key", '');
$baseUrl = get_system_config("ai_provider_{$this->providerId}_base_url", 'https://api.openai.com/v1');
if (empty($apiKey)) {
return [];
}
$url = rtrim($baseUrl, '/') . '/models';
$response = $this->httpGet($url, [
'Authorization: Bearer ' . $apiKey,
]);
if ($response === false) {
return [];
}
$result = json_decode($response, true);
if (!$result || !isset($result['data'])) {
return [];
}
$models = [];
foreach ($result['data'] as $model) {
$models[] = [
'id' => $model['id'] ?? '',
'name' => $model['id'] ?? '',
'owned_by' => $model['owned_by'] ?? '',
];
}
return $models;
}
/**
* {@inheritdoc}
*/
public function testConnection(): bool
{
$apiKey = get_system_config("ai_provider_{$this->providerId}_key", '');
if (empty($apiKey)) {
return false;
}
try {
$result = $this->chat(
'You are a test assistant.',
'Reply with exactly: OK',
['max_tokens' => 5]
);
return !empty($result);
} catch (\Exception $e) {
return false;
}
}
/**
* HTTP POST 请求.
*
* @param string $url 请求地址
* @param array $data 请求数据
* @param array $headers 请求头
*
* @return string|false
*/
protected function httpPost(string $url, array $data, array $headers = [])
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_TIMEOUT, 60);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode >= 400) {
return false;
}
return $response;
}
/**
* HTTP GET 请求.
*
* @param string $url 请求地址
* @param array $headers 请求头
*
* @return string|false
*/
protected function httpGet(string $url, array $headers = [])
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode >= 400) {
return false;
}
return $response;
}
}