Files
zyt/server/app/common/service/followupaudio/FollowupAudioProviderConfig.php
T

87 lines
4.9 KiB
PHP

<?php
declare(strict_types=1);
namespace app\common\service\followupaudio;
/** Server-only provider identity. Changing any request identity invalidates the audio gate. */
final class FollowupAudioProviderConfig
{
public static function resolve(string $profile, ?array $settings = null, ?array $legacy = null): array
{
if (!in_array($profile, ['qwen', 'openai'], true)) { throw new FollowupAudioException('INVALID_PROFILE'); }
$settings = $settings ?? (array) config('followup_audio', []);
$legacy = $legacy ?? (array) config('prescription_ai', []);
$provider = (array) ($settings['providers'][$profile] ?? []);
$driver = (string) ($provider['driver'] ?? 'dify');
if (!in_array($driver, ['dify', 'openai_audio'], true)) { throw new FollowupAudioException('CONFIG_INVALID'); }
$base = (string) ($provider['base_url'] ?? '');
$key = (string) ($provider['api_key'] ?? '');
if ($driver === 'dify') {
if ($base === '') { $base = (string) ($legacy['base_url'] ?? ''); }
if ($key === '') { $key = (string) ($legacy['models'][$profile]['api_key'] ?? ''); }
}
$model = (string) ($provider['model'] ?? '');
$label = trim((string) ($provider['label'] ?? ''));
if ($label === '') { $label = $model !== '' ? $model : $profile; }
if ($base === '' || $key === '' || ($driver === 'openai_audio' && $model === '')) { throw new FollowupAudioException('CONFIG_MISSING'); }
$parts = parse_url($base);
if (!is_array($parts) || !in_array($parts['scheme'] ?? '', ['http', 'https'], true)
|| empty($parts['host']) || isset($parts['user']) || isset($parts['pass']) || isset($parts['query']) || isset($parts['fragment'])
|| preg_match('/[\x00-\x20\x7f\\\\]/', $base) || preg_match('/[\x00-\x20\x7f]/', $key)
|| strlen($model) > 200 || preg_match('/[\x00-\x20\x7f]/', $model)
|| trim($label) === '' || strlen($label) > 200 || preg_match('/[\x00-\x1f\x7f]/', $label)) {
throw new FollowupAudioException('CONFIG_INVALID');
}
$base = rtrim($base, '/');
$path = rtrim((string) ($parts['path'] ?? ''), '/');
if ($driver === 'dify' && str_ends_with($path, '/chat/completions')) {
throw new FollowupAudioException('DIFY_APPLICATION_REQUIRED');
}
if ($driver === 'openai_audio' && str_ends_with($path, '/chat-messages')) {
throw new FollowupAudioException('CONFIG_INVALID');
}
$endpoint = $driver === 'dify' ? '/chat-messages' : '/chat/completions';
if (str_ends_with($base, $endpoint)) { $base = substr($base, 0, -strlen($endpoint)); }
elseif (!str_ends_with($base, '/v1')) { $base .= '/v1'; }
$fingerprint = hash('sha256', json_encode([$driver, $base, $model, $key], JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR));
return ['driver' => $driver, 'base_url' => $base, 'api_key' => $key, 'model' => $model, 'label' => $label, 'fingerprint' => $fingerprint];
}
/** No endpoint, model credential or plaintext provider data in capability responses. */
public static function status(string $profile, ?array $settings = null, ?array $legacy = null): array
{
try {
$provider = self::resolve($profile, $settings, $legacy);
return ['configured' => true, 'profile' => $profile, 'code' => 'OK', 'application_fingerprint' => $provider['fingerprint']];
} catch (FollowupAudioException $error) {
return ['configured' => false, 'profile' => in_array($profile, ['qwen', 'openai'], true) ? $profile : 'invalid', 'code' => $error->errorCode];
}
}
public static function isVerified(string $profile): bool
{
return self::verified($profile, (array) config('followup_audio', []), (array) config('prescription_ai', []));
}
/** Shared with injected-config transport tests; enabled remains an independent operational switch. */
public static function verified(string $profile, array $settings, array $legacy): bool
{
if (empty($settings['audio_verified']) || !in_array($profile, (array) ($settings['verified_profiles'] ?? []), true)) { return false; }
try { $provider = self::resolve($profile, $settings, $legacy); }
catch (FollowupAudioException $error) { return false; }
$verified = (string) ($settings['providers'][$profile]['verified_fingerprint'] ?? '');
return str_starts_with($provider['base_url'], 'https://') && preg_match('/^[a-f0-9]{64}$/D', $verified)
&& hash_equals($provider['fingerprint'], $verified);
}
public static function publicModels(): array
{
$models = [];
foreach (['qwen', 'openai'] as $profile) {
if (self::isVerified($profile)) { $models[] = ['value' => $profile, 'label' => self::resolve($profile)['label']]; }
}
return $models;
}
}