feat: configure audio providers and add local sample acceptance

This commit is contained in:
2026-10-07 18:15:32 +08:00
parent 70be2fc70f
commit fea33285e8
21 changed files with 1103 additions and 131 deletions
+38 -5
View File
@@ -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'