refactor: standardize runtime guard patterns with lodash-es (#154)
* refactor(ui): replace runtime type guards with lodash-es predicates * refactor(runtime): migrate typeof checks to lodash-es in core modules * refactor(ipc): standardize runtime guards with lodash-es * test(refactor): align runtime guard checks with lodash-es * docs(agents): add lodash-es runtime guard skill summary
This commit is contained in:
33 files changed
+303
-208
No files matched your search
@@ -1,5 +1,5 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { isPlainObject, isString } from 'lodash-es';
|
||||
import { isEmpty, isPlainObject, isString } from 'lodash-es';
|
||||
import { mapClaudeModelToGemini, normalizeGeminiModelAlias } from './ModelMapping';
|
||||
import { getMaxOutputTokens, getThinkingBudget } from './ModelSpecs';
|
||||
import { cleanJsonSchema, normalizeObjectJsonSchema } from './JsonSchemaUtils';
|
||||
@@ -518,7 +518,7 @@ function buildContents(
|
||||
|
||||
for (const block of contentBlocks) {
|
||||
if (block.type === 'text') {
|
||||
if (block.text && block.text !== '(no content)' && block.text.trim() !== '')
|
||||
if (block.text && block.text !== '(no content)' && !isEmpty(block.text.trim()))
|
||||
parts.push({ text: block.text.trim() });
|
||||
} else if (block.type === 'thinking') {
|
||||
const part: any = { text: block.thinking, thought: true };
|
||||
@@ -553,7 +553,7 @@ function buildContents(
|
||||
.filter((b: any) => b.type === 'text')
|
||||
.map((b: any) => b.text)
|
||||
.join('\n');
|
||||
if (mergedContent.trim().length === 0)
|
||||
if (isEmpty(mergedContent.trim()))
|
||||
mergedContent = block.is_error
|
||||
? 'Tool execution failed with no output.'
|
||||
: 'Command executed successfully.';
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { isArray, isBoolean, isNumber, isObjectLike, isString } from 'lodash-es';
|
||||
|
||||
/**
|
||||
* Recursively cleans JSON Schema to meet Gemini interface requirements
|
||||
*
|
||||
@@ -9,7 +11,7 @@
|
||||
*/
|
||||
export function cleanJsonSchema(value: any) {
|
||||
// 0. Preprocessing: Expand $ref (Schema Flattening)
|
||||
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
||||
if (isObjectLike(value) && !isArray(value)) {
|
||||
const defs: Record<string, any> = {};
|
||||
|
||||
// Extract $defs or definitions
|
||||
@@ -34,21 +36,21 @@ export function cleanJsonSchema(value: any) {
|
||||
|
||||
export function normalizeObjectJsonSchema(schema: unknown): Record<string, unknown> {
|
||||
const fallbackSchema: Record<string, unknown> = { type: 'object', properties: {} };
|
||||
if (!schema || typeof schema !== 'object' || Array.isArray(schema)) {
|
||||
if (!isObjectLike(schema) || isArray(schema)) {
|
||||
return fallbackSchema;
|
||||
}
|
||||
|
||||
const normalizedSchema = JSON.parse(JSON.stringify(schema)) as Record<string, unknown>;
|
||||
cleanJsonSchema(normalizedSchema);
|
||||
|
||||
if (typeof normalizedSchema.type !== 'string') {
|
||||
if (!isString(normalizedSchema.type)) {
|
||||
normalizedSchema.type = 'object';
|
||||
}
|
||||
if (
|
||||
normalizedSchema.type === 'object' &&
|
||||
(!normalizedSchema.properties ||
|
||||
typeof normalizedSchema.properties !== 'object' ||
|
||||
Array.isArray(normalizedSchema.properties))
|
||||
!isObjectLike(normalizedSchema.properties) ||
|
||||
isArray(normalizedSchema.properties))
|
||||
) {
|
||||
normalizedSchema.properties = {};
|
||||
}
|
||||
@@ -60,10 +62,10 @@ export function normalizeObjectJsonSchema(schema: unknown): Record<string, unkno
|
||||
* Recursively expand $ref
|
||||
*/
|
||||
function flattenRefs(map: any, defs: Record<string, any>) {
|
||||
if (!map || typeof map !== 'object') return;
|
||||
if (!isObjectLike(map)) return;
|
||||
|
||||
// Check and replace $ref
|
||||
if (typeof map['$ref'] === 'string') {
|
||||
if (isString(map['$ref'])) {
|
||||
const refPath = map['$ref'];
|
||||
// Parse reference name (e.g. #/$defs/MyType -> MyType)
|
||||
const parts = refPath.split('/');
|
||||
@@ -74,7 +76,7 @@ function flattenRefs(map: any, defs: Record<string, any>) {
|
||||
// $ref nodes should not have other properties, remove $ref directly
|
||||
delete map['$ref'];
|
||||
|
||||
if (defSchema && typeof defSchema === 'object') {
|
||||
if (isObjectLike(defSchema)) {
|
||||
for (const [k, v] of Object.entries(defSchema)) {
|
||||
// Only insert if the key does not exist in current map (avoid overwrite)
|
||||
if (map[k] === undefined) {
|
||||
@@ -93,7 +95,7 @@ function flattenRefs(map: any, defs: Record<string, any>) {
|
||||
for (const k in map) {
|
||||
if (Object.prototype.hasOwnProperty.call(map, k)) {
|
||||
const v = map[k];
|
||||
if (typeof v === 'object' && v !== null) {
|
||||
if (isObjectLike(v)) {
|
||||
flattenRefs(v, defs);
|
||||
}
|
||||
}
|
||||
@@ -101,11 +103,11 @@ function flattenRefs(map: any, defs: Record<string, any>) {
|
||||
}
|
||||
|
||||
function cleanJsonSchemaRecursive(value: any) {
|
||||
if (!value || typeof value !== 'object') {
|
||||
if (!isObjectLike(value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
if (isArray(value)) {
|
||||
// Array: Recursively process each element
|
||||
for (const v of value) {
|
||||
cleanJsonSchemaRecursive(v);
|
||||
@@ -142,7 +144,7 @@ function cleanJsonSchemaRecursive(value: any) {
|
||||
if (map[field] !== undefined) {
|
||||
const val = map[field];
|
||||
// Only migrate if value is primitive type
|
||||
if (typeof val === 'string' || typeof val === 'number' || typeof val === 'boolean') {
|
||||
if (isString(val) || isNumber(val) || isBoolean(val)) {
|
||||
constraints.push(`${label}: ${val}`);
|
||||
delete map[field];
|
||||
} else {
|
||||
@@ -188,13 +190,13 @@ function cleanJsonSchemaRecursive(value: any) {
|
||||
// 5. Handle type field (Gemini requires single lowercase string)
|
||||
if (map['type']) {
|
||||
const typeVal = map['type'];
|
||||
if (typeof typeVal === 'string') {
|
||||
if (isString(typeVal)) {
|
||||
map['type'] = typeVal.toLowerCase();
|
||||
} else if (Array.isArray(typeVal)) {
|
||||
} else if (isArray(typeVal)) {
|
||||
// Union type downgrade: take the first non-null type
|
||||
let selectedType = 'string';
|
||||
for (const item of typeVal) {
|
||||
if (typeof item === 'string' && item !== 'null') {
|
||||
if (isString(item) && item !== 'null') {
|
||||
selectedType = item.toLowerCase();
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isEmpty, isString } from 'lodash-es';
|
||||
import { logger } from '../../utils/logger';
|
||||
|
||||
const PUBLIC_SUPPORTED_MODELS = [
|
||||
@@ -94,7 +95,7 @@ export function getSupportedModels(): string[] {
|
||||
}
|
||||
|
||||
export function updateDynamicForwardingRules(oldModel: string, newModel: string): void {
|
||||
if (typeof oldModel !== 'string' || typeof newModel !== 'string') {
|
||||
if (!isString(oldModel) || !isString(newModel)) {
|
||||
return;
|
||||
}
|
||||
const normalizedOld = oldModel.trim();
|
||||
@@ -140,7 +141,7 @@ export function getAllDynamicModels(
|
||||
|
||||
if (dynamicModelIds) {
|
||||
for (const dynamicModelId of dynamicModelIds) {
|
||||
if (typeof dynamicModelId === 'string' && dynamicModelId.trim() !== '') {
|
||||
if (isString(dynamicModelId) && !isEmpty(dynamicModelId.trim())) {
|
||||
modelIds.add(dynamicModelId.trim());
|
||||
}
|
||||
}
|
||||
@@ -150,7 +151,7 @@ export function getAllDynamicModels(
|
||||
}
|
||||
|
||||
export function mapClaudeModelToGemini(input: string): string {
|
||||
if (!input || typeof input !== 'string') {
|
||||
if (!isString(input) || isEmpty(input)) {
|
||||
return '';
|
||||
}
|
||||
const mappedModel = CLAUDE_TO_GEMINI[input];
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import modelSpecsJson from './model-specs';
|
||||
import { isNumber } from 'lodash-es';
|
||||
|
||||
type ModelSpec = {
|
||||
max_output_tokens?: number;
|
||||
@@ -24,7 +25,7 @@ export function resolveModelAlias(modelId: string): string {
|
||||
export function getMaxOutputTokens(modelId: string): number {
|
||||
const resolved = resolveModelAlias(modelId);
|
||||
const fromSpec = SPECS.models[resolved]?.max_output_tokens;
|
||||
if (typeof fromSpec === 'number' && Number.isFinite(fromSpec) && fromSpec > 0) {
|
||||
if (isNumber(fromSpec) && Number.isFinite(fromSpec) && fromSpec > 0) {
|
||||
return Math.floor(fromSpec);
|
||||
}
|
||||
return DEFAULT_MAX_OUTPUT_TOKENS;
|
||||
@@ -33,7 +34,7 @@ export function getMaxOutputTokens(modelId: string): number {
|
||||
export function getThinkingBudget(modelId: string): number {
|
||||
const resolved = resolveModelAlias(modelId);
|
||||
const fromSpec = SPECS.models[resolved]?.thinking_budget;
|
||||
if (typeof fromSpec === 'number' && Number.isFinite(fromSpec) && fromSpec >= 0) {
|
||||
if (isNumber(fromSpec) && Number.isFinite(fromSpec) && fromSpec >= 0) {
|
||||
return Math.floor(fromSpec);
|
||||
}
|
||||
return DEFAULT_THINKING_BUDGET;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
import { isNumber } from 'lodash-es';
|
||||
import { QuotaData, LoadProjectResponse, QuotaApiResponse } from './types';
|
||||
import { logger } from '../../utils/logger';
|
||||
|
||||
@@ -162,7 +163,7 @@ export class QuotaService {
|
||||
};
|
||||
}
|
||||
|
||||
if (hasNextEndpoint && (status === 429 || (typeof status === 'number' && status >= 500))) {
|
||||
if (hasNextEndpoint && (status === 429 || (isNumber(status) && status >= 500))) {
|
||||
logger.warn(
|
||||
`Quota API ${endpoint} returned ${status}, falling back to next endpoint`,
|
||||
);
|
||||
@@ -173,7 +174,7 @@ export class QuotaService {
|
||||
|
||||
logger.warn(`API Error: ${status} - ${text}`);
|
||||
lastError = new Error(`HTTP ${status} - ${text}`);
|
||||
shouldFallback = typeof status !== 'number';
|
||||
shouldFallback = !isNumber(status);
|
||||
} else {
|
||||
logger.warn(`Request Failed at ${endpoint}: ${error.message}`);
|
||||
lastError = error instanceof Error ? error : new Error(String(error));
|
||||
|
||||
Reference in new issue
Block a user