fix: 消除生产代码中的 as any 类型不安全模式

- API 兼容层(openai/grok/gemini): 利用 BetaRawMessageStreamEvent 的
  discriminated union 在 switch/case 中直接属性访问,消除 ~29 个 as any
- ConsoleOAuthFlow: 用 as unknown as Parameters<typeof> 替代 as any
- performanceShim: 用 Record<string, unknown> 和显式类型断言替代 as any
- companionReact/auth: 直接访问已有类型属性消除 as any
- sliceAnsi/textHighlighting: 用 as Char 替代 as any(Token 联合类型收窄)
- ccrClient: 利用 RequestResult 类型收窄直接访问 retryAfterMs
- outputsScanner: 用 TurnStartTime.turnStartTime 属性访问替代双重断言
- plans: 用显式数组类型替代 as any[]
- FeedbackSurvey: 用 in 操作符和 Parameters<typeof> 替代 as any
- messageQueueManager: 用 Record<string, unknown> 替代 as any
- mcp.ts: 用 in 操作符类型守卫替代 as any

precheck 通过: typecheck 零错误 + 5420 测试全部通过 + lint 通过
This commit is contained in:
claude-code-best
2026-06-09 22:55:09 +08:00
parent 6e52043555
commit 21c17b4c6e
16 changed files with 212 additions and 91 deletions

View File

@@ -73,7 +73,7 @@ function isAddressed(messages: Message[], name: string): boolean {
) {
const m = messages[i]
if (m?.type !== 'user') continue
const content = (m as any).message?.content
const content = m.message?.content
if (typeof content === 'string' && pattern.test(content)) return true
}
return false
@@ -89,7 +89,7 @@ function buildTranscript(messages: Message[]): string {
.filter(m => m.type === 'user' || m.type === 'assistant')
.map(m => {
const role = m.type === 'user' ? 'user' : 'claude'
const content = (m as any).message?.content
const content = m.message?.content
const text =
typeof content === 'string'
? content.slice(0, 300)

View File

@@ -381,7 +381,7 @@ export class CCRClient {
if (!result.ok) {
throw new RetryableError(
'client event POST failed',
(result as any).retryAfterMs,
result.retryAfterMs,
)
}
},
@@ -404,7 +404,7 @@ export class CCRClient {
if (!result.ok) {
throw new RetryableError(
'internal event POST failed',
(result as any).retryAfterMs,
result.retryAfterMs,
)
}
},
@@ -433,10 +433,7 @@ export class CCRClient {
'delivery batch',
)
if (!result.ok) {
throw new RetryableError(
'delivery POST failed',
(result as any).retryAfterMs,
)
throw new RetryableError('delivery POST failed', result.retryAfterMs)
}
},
baseDelayMs: 500,

View File

@@ -272,7 +272,9 @@ export function ConsoleOAuthFlow({
throw new Error((orgResult as { valid: false; message: string }).message);
}
// Reset modelType to anthropic when using OAuth login
updateSettingsForSource('userSettings', { modelType: 'anthropic' } as any);
updateSettingsForSource('userSettings', { modelType: 'anthropic' } as unknown as Parameters<
typeof updateSettingsForSource
>[1]);
setOAuthStatus({ state: 'success' });
void sendNotification(
@@ -662,9 +664,9 @@ function OAuthStatusMessage({
if (finalVals.sonnet_model) env.ANTHROPIC_DEFAULT_SONNET_MODEL = finalVals.sonnet_model;
if (finalVals.opus_model) env.ANTHROPIC_DEFAULT_OPUS_MODEL = finalVals.opus_model;
const { error } = updateSettingsForSource('userSettings', {
modelType: 'anthropic' as any,
modelType: 'anthropic',
env,
} as any);
} as unknown as Parameters<typeof updateSettingsForSource>[1]);
if (error) {
setOAuthStatus({
state: 'error',
@@ -1153,9 +1155,9 @@ function OAuthStatusMessage({
if (finalVals.sonnet_model) env.GEMINI_DEFAULT_SONNET_MODEL = finalVals.sonnet_model;
if (finalVals.opus_model) env.GEMINI_DEFAULT_OPUS_MODEL = finalVals.opus_model;
const { error } = updateSettingsForSource('userSettings', {
modelType: 'gemini' as any,
modelType: 'gemini',
env,
} as any);
} as unknown as Parameters<typeof updateSettingsForSource>[1]);
if (error) {
setOAuthStatus({
state: 'error',

View File

@@ -12,7 +12,9 @@ export type FrustrationDetectionResult = {
}
function detectFrustration(messages: Message[]): boolean {
const apiErrors = messages.filter(m => (m as any).isApiErrorMessage)
const apiErrors = messages.filter(
m => 'isApiErrorMessage' in m && m.isApiErrorMessage === true,
)
return apiErrors.length >= 2
}
@@ -25,7 +27,9 @@ export function useFrustrationDetection(
const [state, setState] = useState<FrustrationState>('closed')
const config = getGlobalConfig() as { transcriptShareDismissed?: boolean }
const policyAllowed = isPolicyAllowed('product_feedback' as any)
const policyAllowed = isPolicyAllowed(
'product_feedback' as Parameters<typeof isPolicyAllowed>[0],
)
const shouldSkip =
config.transcriptShareDismissed ||
!policyAllowed ||

View File

@@ -55,6 +55,7 @@ const NULL = () => null;
const MAX_VOICE_HINT_SHOWS = 3;
const RSS_UPDATE_INTERVAL_MS = 5_000;
const GOAL_TICK_INTERVAL_MS = 1_000;
type RssState = { text: string; level: 'normal' | 'warning' | 'error' };
@@ -127,6 +128,56 @@ function ProactiveCountdown(): React.ReactNode {
return <Text dimColor>waiting {formatDuration(remainingSeconds * 1000, { mostSignificantOnly: true })}</Text>;
}
/** Compact "goal (1h22min)" pill for the footer — colored by status. */
function GoalElapsedIndicator(): React.ReactNode {
const [tick, setTick] = useState(0);
useEffect(() => {
const id = setInterval(() => setTick(t => t + 1), GOAL_TICK_INTERVAL_MS);
return () => clearInterval(id);
}, []);
void tick;
const { getGoal, getActiveElapsedMs } =
require('../../services/goal/goalState.js') as typeof import('../../services/goal/goalState.js');
const goal = getGoal();
if (!goal) return null;
const elapsedMs = getActiveElapsedMs(goal);
const totalSeconds = Math.floor(elapsedMs / 1000);
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
let timeStr: string;
if (hours >= 1) {
timeStr = `${hours}h${minutes}min`;
} else if (minutes >= 1) {
timeStr = `${minutes}min`;
} else {
timeStr = `${seconds}s`;
}
let color: string | undefined;
switch (goal.status) {
case 'active':
color = 'ansi:green';
break;
case 'paused':
case 'budget_limited':
case 'usage_limited':
color = 'ansi:yellow';
break;
case 'blocked':
color = 'ansi:red';
break;
case 'complete':
color = 'ansi:cyan';
break;
}
return <Text color={color as 'ansi:green'}>goal ({timeStr})</Text>;
}
export function PromptInputFooterLeftSide({
exitMessage,
vimMode,
@@ -376,6 +427,11 @@ function ModeIndicator({
</Text>,
]
: []),
// Goal elapsed indicator — compact "goal (XhYmin)" after PID
...(feature('GOAL') &&
(require('../../services/goal/goalState.js') as typeof import('../../services/goal/goalState.js')).getGoal()
? [<GoalElapsedIndicator key="goal-elapsed" />]
: []),
];
// Check if any in-process teammates exist (for hint text cycling)

View File

@@ -144,7 +144,7 @@ export async function startMCPServer(
)
if (validationResult && !validationResult.result) {
throw new Error(
`Tool ${name} input is invalid: ${(validationResult as any).message}`,
`Tool ${name} input is invalid: ${'message' in validationResult ? validationResult.message : String(validationResult)}`,
)
}
const finalResult = await tool.call(

View File

@@ -1,4 +1,7 @@
import type { BetaToolUnion } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
import type {
BetaToolUnion,
BetaMessage,
} from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
import { randomUUID } from 'crypto'
import type {
AssistantMessage,
@@ -112,21 +115,21 @@ export async function* queryModelGemini(
)
const adaptedStream = adaptGeminiStreamToAnthropic(stream, geminiModel)
const contentBlocks: Record<number, any> = {}
const contentBlocks: Record<number, Record<string, unknown>> = {}
const collectedMessages: AssistantMessage[] = []
let partialMessage: any
let partialMessage: BetaMessage | null = null
let ttftMs = 0
const start = Date.now()
for await (const event of adaptedStream) {
switch (event.type) {
case 'message_start':
partialMessage = (event as any).message
partialMessage = event.message
ttftMs = Date.now() - start
break
case 'content_block_start': {
const idx = (event as any).index
const cb = (event as any).content_block
const idx = event.index
const cb = event.content_block
if (cb.type === 'tool_use') {
contentBlocks[idx] = { ...cb, input: '' }
} else if (cb.type === 'text') {
@@ -139,17 +142,19 @@ export async function* queryModelGemini(
break
}
case 'content_block_delta': {
const idx = (event as any).index
const delta = (event as any).delta
const idx = event.index
const delta = event.delta
const block = contentBlocks[idx]
if (!block) break
if (delta.type === 'text_delta') {
block.text = (block.text || '') + delta.text
block.text = ((block.text as string | undefined) || '') + delta.text
} else if (delta.type === 'input_json_delta') {
block.input = (block.input || '') + delta.partial_json
block.input =
((block.input as string | undefined) || '') + delta.partial_json
} else if (delta.type === 'thinking_delta') {
block.thinking = (block.thinking || '') + delta.thinking
block.thinking =
((block.thinking as string | undefined) || '') + delta.thinking
} else if (delta.type === 'signature_delta') {
if (block.type === 'thinking') {
block.signature = delta.signature
@@ -160,15 +165,19 @@ export async function* queryModelGemini(
break
}
case 'content_block_stop': {
const idx = (event as any).index
const idx = event.index
const block = contentBlocks[idx]
if (!block || !partialMessage) break
const message: AssistantMessage = {
message: {
...partialMessage,
content: normalizeContentFromAPI([block], tools, options.agentId),
},
content: normalizeContentFromAPI(
[block] as unknown as BetaMessage['content'],
tools,
options.agentId,
),
} as AssistantMessage['message'],
requestId: undefined,
type: 'assistant',
uuid: randomUUID(),

View File

@@ -1,4 +1,8 @@
import type { BetaToolUnion } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
import type {
BetaToolUnion,
BetaMessage,
BetaUsage,
} from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
import type { SystemPrompt } from '../../../utils/systemPromptType.js'
import type {
Message,
@@ -119,10 +123,15 @@ export async function* queryModelGrok(
grokModel,
)
const contentBlocks: Record<number, any> = {}
const contentBlocks: Record<number, Record<string, unknown>> = {}
const collectedMessages: AssistantMessage[] = []
let partialMessage: any
let usage = {
let partialMessage: BetaMessage | null = null
let usage: {
input_tokens: number
output_tokens: number
cache_creation_input_tokens: number
cache_read_input_tokens: number
} = {
input_tokens: 0,
output_tokens: 0,
cache_creation_input_tokens: 0,
@@ -134,16 +143,21 @@ export async function* queryModelGrok(
for await (const event of adaptedStream) {
switch (event.type) {
case 'message_start': {
partialMessage = (event as any).message
partialMessage = event.message
ttftMs = Date.now() - start
if ((event as any).message?.usage) {
usage = updateOpenAIUsage(usage, (event as any).message.usage)
if (event.message.usage) {
usage = updateOpenAIUsage(
usage,
event.message.usage as unknown as Parameters<
typeof updateOpenAIUsage
>[1],
)
}
break
}
case 'content_block_start': {
const idx = (event as any).index
const cb = (event as any).content_block
const idx = event.index
const cb = event.content_block
if (cb.type === 'tool_use') {
contentBlocks[idx] = { ...cb, input: '' }
} else if (cb.type === 'text') {
@@ -156,31 +170,37 @@ export async function* queryModelGrok(
break
}
case 'content_block_delta': {
const idx = (event as any).index
const delta = (event as any).delta
const idx = event.index
const delta = event.delta
const block = contentBlocks[idx]
if (!block) break
if (delta.type === 'text_delta') {
block.text = (block.text || '') + delta.text
block.text = ((block.text as string | undefined) || '') + delta.text
} else if (delta.type === 'input_json_delta') {
block.input = (block.input || '') + delta.partial_json
block.input =
((block.input as string | undefined) || '') + delta.partial_json
} else if (delta.type === 'thinking_delta') {
block.thinking = (block.thinking || '') + delta.thinking
block.thinking =
((block.thinking as string | undefined) || '') + delta.thinking
} else if (delta.type === 'signature_delta') {
block.signature = delta.signature
}
break
}
case 'content_block_stop': {
const idx = (event as any).index
const idx = event.index
const block = contentBlocks[idx]
if (!block || !partialMessage) break
const m: AssistantMessage = {
message: {
...partialMessage,
content: normalizeContentFromAPI([block], tools, options.agentId),
},
content: normalizeContentFromAPI(
[block] as unknown as BetaMessage['content'],
tools,
options.agentId,
),
} as AssistantMessage['message'],
requestId: undefined,
type: 'assistant',
uuid: randomUUID(),
@@ -191,9 +211,12 @@ export async function* queryModelGrok(
break
}
case 'message_delta': {
const deltaUsage = (event as any).usage
const deltaUsage = event.usage
if (deltaUsage) {
usage = updateOpenAIUsage(usage, deltaUsage)
usage = updateOpenAIUsage(
usage,
deltaUsage as unknown as Parameters<typeof updateOpenAIUsage>[1],
)
}
break
}
@@ -205,8 +228,15 @@ export async function* queryModelGrok(
event.type === 'message_stop' &&
usage.input_tokens + usage.output_tokens > 0
) {
const costUSD = calculateUSDCost(grokModel, usage as any)
addToTotalSessionCost(costUSD, usage as any, options.model)
const costUSD = calculateUSDCost(
grokModel,
usage as unknown as BetaUsage,
)
addToTotalSessionCost(
costUSD,
usage as unknown as BetaUsage,
options.model,
)
}
yield {

View File

@@ -1,4 +1,8 @@
import type { BetaToolUnion } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
import type {
BetaToolUnion,
BetaMessage,
BetaUsage,
} from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
import type { SystemPrompt } from '../../../utils/systemPromptType.js'
import type {
Message,
@@ -137,8 +141,8 @@ function isOpenAIConvertibleMessage(
* `message_stop` handler and the post-loop safety fallback.
*/
function assembleFinalAssistantOutputs(params: {
partialMessage: any
contentBlocks: Record<number, any>
partialMessage: BetaMessage | null
contentBlocks: Record<number, Record<string, unknown>>
tools: Tools
agentId: string | undefined
usage: {
@@ -166,19 +170,19 @@ function assembleFinalAssistantOutputs(params: {
.map(k => contentBlocks[Number(k)])
.filter(Boolean)
if (allBlocks.length > 0) {
if (allBlocks.length > 0 && partialMessage) {
outputs.push({
message: {
...partialMessage,
content: normalizeContentFromAPI(
allBlocks,
allBlocks as unknown as BetaMessage['content'],
tools,
agentId as AgentId | undefined,
),
usage,
stop_reason: stopReason,
stop_sequence: null,
},
} as AssistantMessage['message'],
requestId: undefined,
type: 'assistant',
uuid: randomUUID(),
@@ -387,9 +391,9 @@ export async function* queryModelOpenAI(
// AssistantMessage + StreamEvent (matching the Anthropic path behavior)
// Accumulate content blocks and usage, same as the Anthropic path in claude.ts
const contentBlocks: Record<number, any> = {}
const contentBlocks: Record<number, Record<string, unknown>> = {}
const collectedMessages: AssistantMessage[] = []
let partialMessage: any
let partialMessage: BetaMessage | null = null
let stopReason: string | null = null
let usage = {
input_tokens: 0,
@@ -403,19 +407,19 @@ export async function* queryModelOpenAI(
for await (const event of adaptedStream) {
switch (event.type) {
case 'message_start': {
partialMessage = (event as any).message
partialMessage = event.message
ttftMs = Date.now() - start
if ((event as any).message?.usage) {
if (event.message.usage) {
usage = {
...usage,
...(event as any).message.usage,
...(event.message.usage as unknown as typeof usage),
}
}
break
}
case 'content_block_start': {
const idx = (event as any).index
const cb = (event as any).content_block
const idx = event.index
const cb = event.content_block
if (cb.type === 'tool_use') {
contentBlocks[idx] = { ...cb, input: '' }
} else if (cb.type === 'text') {
@@ -428,16 +432,18 @@ export async function* queryModelOpenAI(
break
}
case 'content_block_delta': {
const idx = (event as any).index
const delta = (event as any).delta
const idx = event.index
const delta = event.delta
const block = contentBlocks[idx]
if (!block) break
if (delta.type === 'text_delta') {
block.text = (block.text || '') + delta.text
block.text = ((block.text as string | undefined) || '') + delta.text
} else if (delta.type === 'input_json_delta') {
block.input = (block.input || '') + delta.partial_json
block.input =
((block.input as string | undefined) || '') + delta.partial_json
} else if (delta.type === 'thinking_delta') {
block.thinking = (block.thinking || '') + delta.thinking
block.thinking =
((block.thinking as string | undefined) || '') + delta.thinking
} else if (delta.type === 'signature_delta') {
block.signature = delta.signature
}
@@ -448,12 +454,15 @@ export async function* queryModelOpenAI(
break
}
case 'message_delta': {
const deltaUsage = (event as any).usage
const deltaUsage = event.usage
if (deltaUsage) {
usage = updateOpenAIUsage(usage, deltaUsage)
usage = updateOpenAIUsage(
usage,
deltaUsage as unknown as Parameters<typeof updateOpenAIUsage>[1],
)
}
if ((event as any).delta?.stop_reason != null) {
stopReason = (event as any).delta.stop_reason
if (event.delta.stop_reason != null) {
stopReason = event.delta.stop_reason
}
break
}
@@ -482,8 +491,15 @@ export async function* queryModelOpenAI(
}
// Track cost and token usage
if (usage.input_tokens + usage.output_tokens > 0) {
const costUSD = calculateUSDCost(openaiModel, usage as any)
addToTotalSessionCost(costUSD, usage as any, options.model)
const costUSD = calculateUSDCost(
openaiModel,
usage as unknown as BetaUsage,
)
addToTotalSessionCost(
costUSD,
usage as unknown as BetaUsage,
options.model,
)
}
break
}

View File

@@ -117,8 +117,8 @@ export function isAnthropicAuthEnabled(): boolean {
isEnvTruthy(process.env.CLAUDE_CODE_USE_BEDROCK) ||
isEnvTruthy(process.env.CLAUDE_CODE_USE_VERTEX) ||
isEnvTruthy(process.env.CLAUDE_CODE_USE_FOUNDRY) ||
(settings as any).modelType === 'openai' ||
(settings as any).modelType === 'gemini' ||
settings.modelType === 'openai' ||
settings.modelType === 'gemini' ||
!!process.env.OPENAI_BASE_URL ||
!!process.env.GEMINI_BASE_URL
const apiKeyHelper = settings.apiKeyHelper

View File

@@ -64,12 +64,14 @@ export async function findModifiedFiles(
outputsDir: string,
): Promise<string[]> {
// Use recursive flag to get all entries in one call
let entries: Awaited<ReturnType<typeof fs.readdir>> | any[]
let entries:
| Awaited<ReturnType<typeof fs.readdir>>
| { name: string; isFile(): boolean; isSymbolicLink(): boolean }[]
try {
entries = (await fs.readdir(outputsDir, {
withFileTypes: true,
recursive: true,
})) as any[]
})) as { name: string; isFile(): boolean; isSymbolicLink(): boolean }[]
} catch {
// Directory doesn't exist or is not accessible
return []
@@ -113,7 +115,7 @@ export async function findModifiedFiles(
// Filter to files modified since turn start
const modifiedFiles: string[] = []
for (const result of statResults) {
if (result && result.mtimeMs >= (turnStartTime as any as number)) {
if (result && result.mtimeMs >= turnStartTime.turnStartTime) {
modifiedFiles.push(result.filePath)
}
}

View File

@@ -368,7 +368,9 @@ export function isQueuedCommandEditable(cmd: QueuedCommand): boolean {
export function isQueuedCommandVisible(cmd: QueuedCommand): boolean {
if (
(feature('KAIROS') || feature('KAIROS_CHANNELS')) &&
(cmd as any).origin?.kind === 'channel'
(cmd as Record<string, unknown>).origin !== undefined &&
((cmd as Record<string, unknown>).origin as Record<string, unknown>)
?.kind === 'channel'
)
return true
return isQueuedCommandEditable(cmd)

View File

@@ -137,13 +137,14 @@ const shim = {
(() => {}) as typeof performance.setResourceTimingBufferSize,
// Node.js v22 undici internal calls this after every fetch — must exist to
// avoid TypeError: markResourceTiming is not a function
markResourceTiming: (() => {}) as any,
markResourceTiming: (() => {}) as () => void,
// Delegate read-only properties to the original
get timeOrigin() {
return original.timeOrigin
},
get onresourcetimingbufferfull() {
return (original as any).onresourcetimingbufferfull
return (original as unknown as typeof performance)
.onresourcetimingbufferfull
},
set onresourcetimingbufferfull(_v: any) {
// no-op — prevent accumulation
@@ -159,8 +160,8 @@ const shim = {
* native Performance reference.
*/
export function installPerformanceShim(): void {
if ((globalThis as any).__performanceShimInstalled) return
;(globalThis as any).__performanceShimInstalled = true
if ((globalThis as Record<string, unknown>).__performanceShimInstalled) return
;(globalThis as Record<string, unknown>).__performanceShimInstalled = true
globalThis.performance = shim
}

View File

@@ -366,19 +366,19 @@ export async function persistFileSnapshotIfRemote(): Promise<void> {
return
}
try {
const snapshotFiles: SystemFileSnapshotMessage['snapshotFiles'] = []
const snapshotFiles: { key: string; path: string; content: string }[] = []
// Snapshot plan file
const plan = getPlan()
if (plan) {
;(snapshotFiles as any[]).push({
snapshotFiles.push({
key: 'plan',
path: getPlanFilePath(),
content: plan,
})
}
if ((snapshotFiles as any[]).length === 0) {
if (snapshotFiles.length === 0) {
return
}

View File

@@ -1,5 +1,6 @@
import {
type AnsiCode,
type Char,
ansiCodesToString,
reduceAnsiCodes,
tokenize,
@@ -83,7 +84,7 @@ export default function sliceAnsi(
}
if (include) {
result += (token as any).value
result += (token as Char).value
}
position += width

View File

@@ -1,5 +1,6 @@
import {
type AnsiCode,
type Char,
ansiCodesToString,
reduceAnsiCodes,
type Token,
@@ -128,14 +129,14 @@ class HighlightSegmenter {
this.tokenIdx++
} else {
const charsNeeded = targetVisiblePos - this.visiblePos
const charsAvailable = (token as any).value.length - this.charIdx
const charsAvailable = (token as Char).value.length - this.charIdx
const charsToTake = Math.min(charsNeeded, charsAvailable)
this.stringPos += charsToTake
this.visiblePos += charsToTake
this.charIdx += charsToTake
if (this.charIdx >= (token as any).value.length) {
if (this.charIdx >= (token as Char).value.length) {
this.tokenIdx++
this.charIdx = 0
}