refactor: reduce cyclomatic complexity of settings, tool data and layout sanitizers
Add characterization tests first, then split the hot spots into small helpers: - STT/TTS settings updates move to extensions/piagentui-speech-settings.ts (rule tables) - defaultExtractData is split into diff/diagnostic/output extractors - getToolSummaryCategory becomes an ordered rule table (toolSummary.ts) - sanitizePersistedTerminalLayoutMap is split per level - duplicated diff stats helpers are merged into utils/diffStats.ts
This commit is contained in:
1 parent
da09846708
commit
2d2cdf3b19
13 files changed
+962
-404
No files matched your search
@@ -0,0 +1,126 @@
|
||||
import { validateSttBaseUrl } from './piagentui-server-core.js'
|
||||
import type { StoredSttSettings, StoredTtsSettings } from './piagentui-settings-store.js'
|
||||
|
||||
export type SettingsUpdateResult<T> = { ok: true; next: T } | { ok: false; error: string }
|
||||
|
||||
type Fields = Record<string, unknown>
|
||||
/** Assigns a validated value to `next`; returns false when the value is invalid. */
|
||||
type FieldRule = (value: unknown, next: Fields) => boolean
|
||||
|
||||
const TTS_RESPONSE_FORMATS = ['mp3', 'opus', 'aac', 'flac', 'wav', 'pcm'] as const
|
||||
|
||||
export function isTtsResponseFormat(value: unknown): value is NonNullable<StoredTtsSettings['responseFormat']> {
|
||||
return typeof value === 'string' && (TTS_RESPONSE_FORMATS as readonly string[]).includes(value)
|
||||
}
|
||||
|
||||
const booleanField =
|
||||
(key: string): FieldRule =>
|
||||
(value, next) => {
|
||||
next[key] = value === true
|
||||
return true
|
||||
}
|
||||
|
||||
const oneOfField =
|
||||
(key: string, allowed: readonly string[]): FieldRule =>
|
||||
(value, next) => {
|
||||
if (typeof value !== 'string' || !allowed.includes(value)) return false
|
||||
next[key] = value
|
||||
return true
|
||||
}
|
||||
|
||||
const endpointField =
|
||||
(key: string): FieldRule =>
|
||||
(value, next) => {
|
||||
if (typeof value !== 'string' || !value.startsWith('/')) return false
|
||||
next[key] = value
|
||||
return true
|
||||
}
|
||||
|
||||
const trimmedField =
|
||||
(key: string): FieldRule =>
|
||||
(value, next) => {
|
||||
if (typeof value !== 'string' || !value.trim()) return false
|
||||
next[key] = value.trim()
|
||||
return true
|
||||
}
|
||||
|
||||
const baseUrlField: FieldRule = (value, next) => {
|
||||
if (typeof value !== 'string' || !validateSttBaseUrl(value)) return false
|
||||
next.baseUrl = value.replace(/\/+$/, '')
|
||||
return true
|
||||
}
|
||||
|
||||
const languageField: FieldRule = (value, next) => {
|
||||
if (value === null || value === '') delete next.language
|
||||
else if (typeof value === 'string') next.language = value.trim()
|
||||
else return false
|
||||
return true
|
||||
}
|
||||
|
||||
const speedField: FieldRule = (value, next) => {
|
||||
if (typeof value !== 'number' || value < 0.25 || value > 4) return false
|
||||
next.speed = value
|
||||
return true
|
||||
}
|
||||
|
||||
const PROVIDER_KINDS = ['openai', 'openai-compatible'] as const
|
||||
|
||||
const STT_RULES: Array<[string, FieldRule]> = [
|
||||
['enabled', booleanField('enabled')],
|
||||
['providerKind', oneOfField('providerKind', PROVIDER_KINDS)],
|
||||
['mode', oneOfField('mode', ['file', 'realtime'])],
|
||||
['baseUrl', baseUrlField],
|
||||
['apiKeyRequired', booleanField('apiKeyRequired')],
|
||||
['transcriptionEndpoint', endpointField('transcriptionEndpoint')],
|
||||
['transcriptionModel', trimmedField('transcriptionModel')],
|
||||
['language', languageField],
|
||||
['insertMode', oneOfField('insertMode', ['append', 'replace'])],
|
||||
]
|
||||
|
||||
const TTS_RULES: Array<[string, FieldRule]> = [
|
||||
['enabled', booleanField('enabled')],
|
||||
['providerKind', oneOfField('providerKind', PROVIDER_KINDS)],
|
||||
['baseUrl', baseUrlField],
|
||||
['apiKeyRequired', booleanField('apiKeyRequired')],
|
||||
['speechEndpoint', endpointField('speechEndpoint')],
|
||||
['model', trimmedField('model')],
|
||||
['voice', trimmedField('voice')],
|
||||
['responseFormat', oneOfField('responseFormat', TTS_RESPONSE_FORMATS)],
|
||||
['speed', speedField],
|
||||
]
|
||||
|
||||
function applyUpdate<T extends object>(
|
||||
existing: T,
|
||||
update: Record<string, unknown>,
|
||||
rules: Array<[string, FieldRule]>,
|
||||
): SettingsUpdateResult<T> {
|
||||
const next: Fields = { ...(existing as Fields) }
|
||||
|
||||
for (const [field, rule] of rules) {
|
||||
if (update[field] === undefined) continue
|
||||
if (!rule(update[field], next)) return { ok: false, error: `Invalid ${field}` }
|
||||
}
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(update, 'apiKey')) {
|
||||
if (update.apiKey === null) delete next.apiKey
|
||||
else if (typeof update.apiKey === 'string' && update.apiKey.trim()) next.apiKey = update.apiKey.trim()
|
||||
}
|
||||
|
||||
return { ok: true, next: next as T }
|
||||
}
|
||||
|
||||
/** Validates a PATCH-style STT update against the stored settings. Pure: does not persist anything. */
|
||||
export function applySttSettingsUpdate(
|
||||
existing: StoredSttSettings,
|
||||
update: Record<string, unknown>,
|
||||
): SettingsUpdateResult<StoredSttSettings> {
|
||||
return applyUpdate(existing, update, STT_RULES)
|
||||
}
|
||||
|
||||
/** Validates a PATCH-style TTS update against the stored settings. Pure: does not persist anything. */
|
||||
export function applyTtsSettingsUpdate(
|
||||
existing: StoredTtsSettings,
|
||||
update: Record<string, unknown>,
|
||||
): SettingsUpdateResult<StoredTtsSettings> {
|
||||
return applyUpdate(existing, update, TTS_RULES)
|
||||
}
|
||||
Reference in new issue
Block a user