feat: add scoped read-only audio preview and isolated Dify transport
This commit is contained in:
@@ -18,6 +18,8 @@ final class FollowupAudioStore
|
||||
return $profile === null ? self::verifiedProfiles() !== [] : FollowupAudioProviderConfig::isVerified($profile);
|
||||
}
|
||||
|
||||
public static function ready(?string $profile = null): bool { return FollowupAudioGate::ready($profile); }
|
||||
|
||||
private static function verifiedProfiles(): array
|
||||
{
|
||||
return array_values(array_filter(['qwen', 'openai'], [FollowupAudioProviderConfig::class, 'isVerified']));
|
||||
@@ -43,7 +45,7 @@ final class FollowupAudioStore
|
||||
|
||||
public static function assertEnabled(?string $profile = null): void
|
||||
{
|
||||
if (!self::enabled() || !self::verified($profile)) { throw new DomainException('FOLLOWUP_AUDIO_DISABLED_OR_UNVERIFIED'); }
|
||||
if (!self::enabled() || !self::ready($profile)) { throw new DomainException('FOLLOWUP_AUDIO_DISABLED_OR_UNVERIFIED'); }
|
||||
}
|
||||
|
||||
public static function create(array $upload, string $recordedAt, string $modelKey, int $actor, array $info): array
|
||||
@@ -89,7 +91,7 @@ final class FollowupAudioStore
|
||||
'lease_token' => '', 'lease_until' => 0, 'upstream_started_at' => 0,
|
||||
'upstream_run_id' => '', 'upstream_file_id' => '', 'upstream_ids_json' => FollowupAudioPolicy::canonical([
|
||||
'provider_fingerprint' => FollowupAudioProviderConfig::resolve($modelKey)['fingerprint'],
|
||||
]),
|
||||
] + (FollowupAudioGate::previewOnly() ? ['preview_only' => true] : [])),
|
||||
'error_code' => '', 'error_message' => '', 'extraction_cipher' => '', 'review_cipher' => '', 'applied_cipher' => '',
|
||||
'created_at' => $now, 'updated_at' => $now, 'expires_at' => $expiresAt, 'applied_at' => 0, 'purged_at' => 0,
|
||||
]);
|
||||
@@ -149,7 +151,9 @@ final class FollowupAudioStore
|
||||
$result['error_code'] = 'FOLLOWUP_AUDIO_EXPIRED';
|
||||
$result['error_message'] = '录音及审阅已到保留期限,不能采用';
|
||||
}
|
||||
$result['can_retry'] = self::enabled() && self::verified((string) $task['model_key']) && $alive && $task['status'] === 'failed'
|
||||
$result['preview_only'] = FollowupAudioGate::previewOnly() || FollowupAudioGate::taskPreview($task);
|
||||
$result['can_retry'] = self::enabled() && self::ready((string) $task['model_key'])
|
||||
&& FollowupAudioGate::scopeAllowed((int) $task['diagnosis_id'], (int) $task['actor_id']) && $alive && $task['status'] === 'failed'
|
||||
&& self::providerMatches($task) && self::safeToResume($task) && (int) $task['attempts'] < 3;
|
||||
$pipeline = $alive ? self::pipelineState($task) : [];
|
||||
$segments = $pipeline ? FollowupAudioPipelineCheckpoint::segments($pipeline) : [];
|
||||
@@ -233,6 +237,7 @@ final class FollowupAudioStore
|
||||
$task = self::lockTask($taskId);
|
||||
self::assertEnabled((string) $task['model_key']);
|
||||
self::assertTaskProvider($task);
|
||||
FollowupAudioGate::assertScope((int) $task['diagnosis_id'], (int) $task['actor_id']);
|
||||
if ($task['status'] !== 'failed' || !self::safeToResume($task) || (int) $task['attempts'] >= 3
|
||||
|| (int) $task['expires_at'] <= time() || (int) $task['purged_at']) {
|
||||
throw new DomainException('FOLLOWUP_AUDIO_RETRY_NOT_SAFE');
|
||||
@@ -272,6 +277,7 @@ final class FollowupAudioStore
|
||||
->where('expires_at', '>', $now)->where('purged_at', 0)->order('id', 'asc')->limit(200)->lock(true)->select()->toArray();
|
||||
$task = null;
|
||||
foreach ($pending as $candidate) {
|
||||
if (!FollowupAudioGate::scopeAllowed((int) $candidate['diagnosis_id'], (int) $candidate['actor_id'])) { continue; }
|
||||
if (!self::safeToResume($candidate)) {
|
||||
Db::name('followup_audio_task')->where('id', $candidate['id'])->update([
|
||||
'status' => 'needs_reconciliation', 'stage' => 'needs_reconciliation',
|
||||
@@ -289,7 +295,7 @@ final class FollowupAudioStore
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
if (self::verified((string) $candidate['model_key'])) { $task = $candidate; break; }
|
||||
if (self::ready((string) $candidate['model_key'])) { $task = $candidate; break; }
|
||||
}
|
||||
if ($task === null) { return null; }
|
||||
$changes = ['status' => 'running', 'stage' => 'preparing', 'lease_token' => bin2hex(random_bytes(32)),
|
||||
@@ -339,6 +345,10 @@ final class FollowupAudioStore
|
||||
if (!is_array($ids)) { throw new DomainException('FOLLOWUP_AUDIO_CHECKPOINT_INVALID'); }
|
||||
$previous = json_decode($task['upstream_ids_json'] ?: '{}', true, 16, JSON_THROW_ON_ERROR);
|
||||
foreach ($ids as $name => $identifier) {
|
||||
if ($name === 'preview_only') {
|
||||
if (($previous[$name] ?? null) !== true || $identifier !== true) { throw new DomainException('FOLLOWUP_AUDIO_CHECKPOINT_INVALID'); }
|
||||
continue;
|
||||
}
|
||||
if ($name === 'provider_fingerprint') {
|
||||
if (!is_string($identifier) || !is_string($previous[$name] ?? null)
|
||||
|| !hash_equals($previous[$name], $identifier)) {
|
||||
@@ -547,7 +557,8 @@ final class FollowupAudioStore
|
||||
|
||||
private static function hasLease(array $task, string $token, bool $processing = true): bool
|
||||
{
|
||||
return (!$processing || self::enabled() && self::verified((string) $task['model_key']) && self::providerMatches($task))
|
||||
return (!$processing || self::enabled() && self::ready((string) $task['model_key']) && self::providerMatches($task)
|
||||
&& FollowupAudioGate::scopeAllowed((int) $task['diagnosis_id'], (int) $task['actor_id']))
|
||||
&& $task['status'] === 'running' && $token !== ''
|
||||
&& hash_equals((string) $task['lease_token'], $token) && (int) $task['lease_until'] > time()
|
||||
&& (int) $task['expires_at'] > time() && !(int) $task['purged_at'];
|
||||
|
||||
Reference in New Issue
Block a user