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:
vin authored and GitHub committed 2026-04-12 11:41:56 +08:00
1 parent 0223e8d807
commit bc35b96b5d
33 files changed
+303 -208

No files matched your search

+3 -3
View File
@@ -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.';
+17 -15
View File
@@ -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;
}
+4 -3
View File
@@ -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];
+3 -2
View File
@@ -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;
+3 -2
View File
@@ -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));