feat: configure audio providers and add local sample acceptance
This commit is contained in:
@@ -6,6 +6,7 @@ namespace app\command;
|
||||
|
||||
use app\common\service\followupaudio\FollowupAudioDify;
|
||||
use app\common\service\followupaudio\FollowupAudioException;
|
||||
use app\common\service\followupaudio\FollowupAudioProviderConfig;
|
||||
use think\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\input\Option;
|
||||
@@ -19,7 +20,7 @@ final class FollowupAudioProbe extends Command
|
||||
$this->setName('followup-audio:probe')->setDescription('只对脚本生成的合成短/15分钟/1小时音频做能力验证;不会自动开启功能')
|
||||
->addOption('synthetic', null, Option::VALUE_NONE, '明确确认只使用合成数据')
|
||||
->addOption('manifest', null, Option::VALUE_REQUIRED, 'generate_followup_audio_fixtures.py 输出的 manifest.json')
|
||||
->addOption('profile', null, Option::VALUE_REQUIRED, '现有 prescription_ai 配置 qwen/openai', 'qwen')
|
||||
->addOption('profile', null, Option::VALUE_REQUIRED, '服务端 followup_audio 配置的 qwen/openai 逻辑槽位', 'qwen')
|
||||
->addOption('case', null, Option::VALUE_REQUIRED, 'all/short/medium/long', 'all');
|
||||
}
|
||||
|
||||
@@ -39,20 +40,43 @@ final class FollowupAudioProbe extends Command
|
||||
}
|
||||
$manifest = json_decode((string) file_get_contents($path), true, 32, JSON_THROW_ON_ERROR);
|
||||
$fixtures = self::validateManifest($manifest, dirname($path));
|
||||
$reportPath = dirname($path) . '/probe-' . $profile . '.json';
|
||||
$dify = new FollowupAudioDify();
|
||||
$configuration = $dify->configurationStatus($profile);
|
||||
$fingerprint = (string) ($configuration['application_fingerprint'] ?? '');
|
||||
if ($fingerprint !== '' && !preg_match('/^[a-f0-9]{64}$/D', $fingerprint)) {
|
||||
throw new FollowupAudioException('PROBE_IDENTITY_CHANGED');
|
||||
}
|
||||
// Identity-specific evidence never overwrites a previous app or protocol's probe report.
|
||||
$reportPath = dirname($path) . '/probe-' . $profile . '-' . ($fingerprint !== '' ? $fingerprint : 'unconfigured') . '.json';
|
||||
$legacyPath = dirname($path) . '/probe-' . $profile . '.json';
|
||||
if (is_file($legacyPath)) {
|
||||
$legacy = json_decode((string) file_get_contents($legacyPath), true, 32, JSON_THROW_ON_ERROR);
|
||||
$legacyFingerprint = (string) ($legacy['configuration']['application_fingerprint'] ?? '');
|
||||
if (self::sameLegacyDifyApplication($profile, $legacyFingerprint) && array_filter((array) ($legacy['cases'] ?? []),
|
||||
static fn (array $row): bool => !empty($row['upstream_started_at']) && ($row['status'] ?? '') !== 'passed')) {
|
||||
// Upgrading the identity algorithm must not turn an old billable unknown into a new request.
|
||||
$output->writeln('FOLLOWUP_AUDIO_PROBE ' . json_encode(['status' => 'needs_reconciliation',
|
||||
'code' => 'NO_RESUBMISSION', 'report' => $legacyPath]));
|
||||
return 2;
|
||||
}
|
||||
if (!is_file($reportPath) && $fingerprint !== '' && hash_equals($fingerprint, $legacyFingerprint)) {
|
||||
$reportPath = $legacyPath; // Same-identity unknown/passed entries remain authoritative.
|
||||
} elseif ($legacyFingerprint === '' && array_filter((array) ($legacy['cases'] ?? []),
|
||||
static fn (array $row): bool => !empty($row['upstream_started_at']))) {
|
||||
throw new FollowupAudioException('PROBE_IDENTITY_UNBOUND');
|
||||
}
|
||||
}
|
||||
$lock = fopen($reportPath . '.lock', 'c+b');
|
||||
if (!$lock || !flock($lock, LOCK_EX | LOCK_NB)) { throw new FollowupAudioException('PROBE_ALREADY_RUNNING'); }
|
||||
@chmod($reportPath . '.lock', 0600);
|
||||
$hash = hash_file('sha256', $path);
|
||||
$report = is_file($reportPath) ? json_decode((string) file_get_contents($reportPath), true, 32, JSON_THROW_ON_ERROR) : [
|
||||
'schema_version' => 'followup-audio-probe-v1', 'synthetic' => true,
|
||||
'schema_version' => 'followup-audio-probe-v2', 'synthetic' => true,
|
||||
'manifest_sha256' => $hash, 'profile' => $profile, 'cases' => [],
|
||||
];
|
||||
if (!is_array($report) || ($report['manifest_sha256'] ?? '') !== $hash || ($report['profile'] ?? '') !== $profile) {
|
||||
throw new FollowupAudioException('PROBE_IDENTITY_CHANGED');
|
||||
}
|
||||
$dify = new FollowupAudioDify();
|
||||
$configuration = $dify->configurationStatus($profile);
|
||||
if (!empty($report['configuration']['application_fingerprint'])
|
||||
&& ($report['configuration']['application_fingerprint'] !== ($configuration['application_fingerprint'] ?? ''))) {
|
||||
throw new FollowupAudioException('PROBE_APPLICATION_CHANGED');
|
||||
@@ -111,6 +135,15 @@ final class FollowupAudioProbe extends Command
|
||||
}
|
||||
}
|
||||
|
||||
private static function sameLegacyDifyApplication(string $profile, string $fingerprint): bool
|
||||
{
|
||||
if (!preg_match('/^[a-f0-9]{64}$/D', $fingerprint)) { return false; }
|
||||
try { $provider = FollowupAudioProviderConfig::resolve($profile); }
|
||||
catch (FollowupAudioException $error) { return false; }
|
||||
return $provider['driver'] === 'dify'
|
||||
&& hash_equals(hash('sha256', $provider['base_url'] . "\0" . $provider['api_key']), $fingerprint);
|
||||
}
|
||||
|
||||
private static function validateManifest($manifest, string $directory): array
|
||||
{
|
||||
if (!is_array($manifest) || ($manifest['generator'] ?? '') !== 'followup-audio-synthetic-v1'
|
||||
|
||||
Reference in New Issue
Block a user