fix: 代码审查修复 — 安全、性能和正确性

- triggersApi: 添加 assertSubscriptionBaseUrl 防止 OAuth token 泄露
- claude.ts: 修复流式响应 O(n^2) 字符串拼接,改用数组累积
- claude.ts: 移除未使用的 import,动态 import 改为静态 import
- StatusLine: BuiltinStatusLine 仅在 statusLineEnabled 时显示,修复双行问题
- local-vault: 修复 --reveal 标志位置解析 bug
- share: 修复 sk-proj-* OpenAI 密钥未脱敏问题
- store.ts: 临时文件改用同目录创建,避免跨文件系统 rename 失败
- store.ts: 添加空字符串 key 校验
- permissionValidation: 端口正则限制为有效 TCP 范围 0-65535
- 测试 mock 补全: schedule/vault/skill-store 测试文件
- 移除过期的 biome-ignore 注释

Co-Authored-By: glm-5-turbo <zai-org@claude-code-best.win>
This commit is contained in:
claude-code-best
2026-05-10 09:39:34 +08:00
parent 4f493c83fc
commit 82be5ff05b
12 changed files with 78 additions and 32 deletions

View File

@@ -20,6 +20,7 @@ import type {
import type { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'
import type { Stream } from '@anthropic-ai/sdk/streaming.mjs'
import { randomUUID } from 'crypto'
import { existsSync, unlinkSync } from 'node:fs'
import {
getAPIProvider,
isFirstPartyAnthropicBaseUrl,
@@ -124,7 +125,6 @@ import {
getAfkModeHeaderLatched,
getCacheEditingHeaderLatched,
getFastModeHeaderLatched,
getLastApiCompletionTimestamp,
getPromptCache1hAllowlist,
getPromptCache1hEligible,
getSessionId,
@@ -254,7 +254,6 @@ import {
type NonNullableUsage,
} from './logging.js'
import {
CACHE_TTL_1HOUR_MS,
checkResponseForCacheBreak,
recordPromptState,
} from './promptCacheBreakDetection.js'
@@ -1431,8 +1430,6 @@ async function* queryModel(
// unique ephemeral nonce comment to the system prompt so the prefix-cache
// hash changes for this request, forcing a cache miss.
{
const { existsSync, unlinkSync } = await import('node:fs')
const { randomUUID } = await import('node:crypto')
const onceMarker = getBreakCacheMarkerPath()
const alwaysFlag = getBreakCacheAlwaysPath()
const shouldBreak = existsSync(onceMarker) || existsSync(alwaysFlag)
@@ -1842,6 +1839,7 @@ async function* queryModel(
let ttftMs = 0
let partialMessage: BetaMessage | undefined
const contentBlocks: (BetaContentBlock | ConnectorTextBlock)[] = []
const textDeltas = new Map<number, string[]>()
let usage: NonNullableUsage = EMPTY_USAGE
let costUSD = 0
let stopReason: BetaStopReason | null = null
@@ -1940,6 +1938,7 @@ async function* queryModel(
ttftMs = 0
partialMessage = undefined
contentBlocks.length = 0
textDeltas.clear()
usage = EMPTY_USAGE
stopReason = null
isAdvisorInProgress = false
@@ -2096,6 +2095,7 @@ async function* queryModel(
}
break
case 'text':
textDeltas.set(part.index, [])
contentBlocks[part.index] = {
...part.content_block,
// awkwardly, the sdk sometimes returns text as part of a
@@ -2202,7 +2202,7 @@ async function* queryModel(
})
throw new Error('Content block is not a text block')
}
;(contentBlock as { text: string }).text += delta.text
textDeltas.get(part.index)?.push(delta.text!)
break
case 'signature_delta':
if (
@@ -2270,6 +2270,12 @@ async function* queryModel(
})
throw new Error('Message not found')
}
// Merge accumulated text deltas into the content block (O(n) join instead of O(n^2) +=)
const deltas = textDeltas.get(part.index)
if (deltas) {
;(contentBlock as { text: string }).text = deltas.join('')
textDeltas.delete(part.index)
}
const m: AssistantMessage = {
message: {
...partialMessage,

View File

@@ -36,7 +36,7 @@ import {
rmSync,
} from 'node:fs'
import { readFile, writeFile } from 'node:fs/promises'
import { homedir, tmpdir } from 'node:os'
import { homedir } from 'node:os'
import { join } from 'node:path'
import { logError } from '../../utils/log.js'
import { KeychainUnavailableError, tryKeychain } from './keychain.js'
@@ -304,8 +304,9 @@ async function writeVaultFile(data: VaultFile): Promise<void> {
}
const filePath = getVaultFilePath()
// C1: atomic write — tmp file + rename (POSIX rename(2) is atomic)
const vaultDir = join(filePath, '..')
const tmpPath = join(
tmpdir(),
vaultDir,
`.local-vault-${randomBytes(8).toString('hex')}.tmp`,
)
try {
@@ -340,6 +341,8 @@ async function getOrCreateSalt(vaultData: VaultFile): Promise<Buffer> {
// ── Public API ────────────────────────────────────────────────────────────────
export async function setSecret(key: string, value: string): Promise<void> {
if (!key) throw new Error('key must not be empty')
// D1: Guard against unbounded value sizes
const byteLength = Buffer.byteLength(value, 'utf8')
if (byteLength > MAX_SECRET_BYTES) {