109 lines
3.7 KiB
PHP
109 lines
3.7 KiB
PHP
<?php
|
||
|
||
namespace app\common;
|
||
|
||
/**
|
||
* 使用本地 chat/completions 接口,从 affiliation 文本推断国家。
|
||
*
|
||
* 输出约定:返回数组 ['code' => 'US', 'en_name' => 'United States'],任一可为空。
|
||
*/
|
||
class CountryResolverService
|
||
{
|
||
private $chatUrl = '';
|
||
private $chatModel = '';
|
||
private $apiKey = '';
|
||
private $timeout = 60;
|
||
|
||
public function __construct(array $config = [])
|
||
{
|
||
if (isset($config['chat_url'])) $this->chatUrl = (string)$config['chat_url'];
|
||
if (isset($config['chat_model'])) $this->chatModel = (string)$config['chat_model'];
|
||
if (isset($config['api_key'])) $this->apiKey = (string)$config['api_key'];
|
||
if (isset($config['timeout'])) $this->timeout = max(5, intval($config['timeout']));
|
||
}
|
||
|
||
public function resolve(string $affiliation): array
|
||
{
|
||
$affiliation = trim($affiliation);
|
||
if ($affiliation === '' || $this->chatUrl === '' || $this->chatModel === '') {
|
||
return [];
|
||
}
|
||
|
||
$messages = [
|
||
[
|
||
'role' => 'system',
|
||
'content' => 'You extract the country from an academic affiliation string. Reply ONLY with minified JSON.',
|
||
],
|
||
[
|
||
'role' => 'user',
|
||
'content' => "Affiliation:\n" . $affiliation . "\n\nReturn JSON with keys:\n- code: ISO 3166-1 alpha-3 (preferred)\n- en_name: English country name (optional)\nIf uncertain, return {\"code\":\"\",\"en_name\":\"\"}.",
|
||
],
|
||
];
|
||
|
||
$payload = [
|
||
'model' => $this->chatModel,
|
||
'temperature' => 0,
|
||
'messages' => $messages,
|
||
];
|
||
|
||
$ch = curl_init();
|
||
curl_setopt($ch, CURLOPT_URL, $this->chatUrl);
|
||
curl_setopt($ch, CURLOPT_POST, true);
|
||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload, JSON_UNESCAPED_UNICODE));
|
||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, min(10, $this->timeout));
|
||
curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout);
|
||
|
||
$headers = [
|
||
'Content-Type: application/json',
|
||
];
|
||
if ($this->apiKey !== '') {
|
||
$headers[] = 'Authorization: Bearer ' . $this->apiKey;
|
||
}
|
||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||
|
||
$raw = curl_exec($ch);
|
||
if ($raw === false) {
|
||
curl_close($ch);
|
||
return [];
|
||
}
|
||
$httpCode = intval(curl_getinfo($ch, CURLINFO_HTTP_CODE));
|
||
curl_close($ch);
|
||
if ($httpCode < 200 || $httpCode >= 300) {
|
||
return [];
|
||
}
|
||
|
||
$data = json_decode($raw, true);
|
||
if (!is_array($data)) return [];
|
||
|
||
// 兼容 OpenAI chat/completions 结构:choices[0].message.content
|
||
$content = '';
|
||
if (isset($data['choices'][0]['message']['content'])) {
|
||
$content = (string)$data['choices'][0]['message']['content'];
|
||
} elseif (isset($data['content'])) {
|
||
$content = (string)$data['content'];
|
||
}
|
||
|
||
$content = trim($content);
|
||
if ($content === '') return [];
|
||
|
||
// 尝试提取 JSON(允许模型包裹 ```json)
|
||
if (preg_match('/\{.*\}/s', $content, $m)) {
|
||
$content = $m[0];
|
||
}
|
||
|
||
$obj = json_decode($content, true);
|
||
if (!is_array($obj)) return [];
|
||
|
||
$code = isset($obj['code']) ? strtoupper(trim((string)$obj['code'])) : '';
|
||
$enName = isset($obj['en_name']) ? trim((string)$obj['en_name']) : '';
|
||
|
||
// 防止模型乱回长段文本
|
||
if (strlen($code) > 8) $code = '';
|
||
if (strlen($enName) > 128) $enName = mb_substr($enName, 0, 128);
|
||
|
||
return ['code' => $code, 'en_name' => $enName];
|
||
}
|
||
}
|
||
|