开源 企业版 高校版 私有云 模力方舟 AI 队友
代码拉取完成,页面将自动刷新
加入 Gitee
与超过 1400万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
已有帐号? 立即登录
文件
main
分支 (1)
main
该仓库未声明开源许可证文件(LICENSE),使用请关注具体项目描述及其代码上游依赖。
项目仓库所选许可证以仓库主分支所使用许可证为准
main
分支 (1)
main
克隆/下载
克隆/下载
提示
下载代码请复制以下命令到终端执行
为确保你提交的代码身份被 Gitee 正确识别,请执行以下命令完成配置
初次使用 SSH 协议进行代码克隆、推送等操作时,需按下述提示完成 SSH 配置
1 生成 RSA 密钥
2 获取 RSA 公钥内容,并配置到 SSH公钥
在 Gitee 上使用 SVN,请访问 使用指南
使用 HTTPS 协议时,命令行会出现如下账号密码验证步骤。基于安全考虑,Gitee 建议 配置并使用私人令牌 替代登录密码进行克隆、推送等操作
Username for 'https://gitee.com': userName
Password for 'https://userName@gitee.com': # 私人令牌
main
分支 (1)
main
FileReadTool.ts 38.16 KB
一键复制 编辑 原始数据 按行查看 历史
sanbucat 提交于 2026年03月31日 17:08 +08:00 . v2.1.88 反编译源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183
import type { Base64ImageSource } from '@anthropic-ai/sdk/resources/index.mjs'
import { readdir, readFile as readFileAsync } from 'fs/promises'
import * as path from 'path'
import { posix, win32 } from 'path'
import { z } from 'zod/v4'
import {
PDF_AT_MENTION_INLINE_THRESHOLD,
PDF_EXTRACT_SIZE_THRESHOLD,
PDF_MAX_PAGES_PER_READ,
} from '../../constants/apiLimits.js'
import { hasBinaryExtension } from '../../constants/files.js'
import { memoryFreshnessNote } from '../../memdir/memoryAge.js'
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js'
import { logEvent } from '../../services/analytics/index.js'
import {
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
getFileExtensionForAnalytics,
} from '../../services/analytics/metadata.js'
import {
countTokensWithAPI,
roughTokenCountEstimationForFileType,
} from '../../services/tokenEstimation.js'
import {
activateConditionalSkillsForPaths,
addSkillDirectories,
discoverSkillDirsForPaths,
} from '../../skills/loadSkillsDir.js'
import type { ToolUseContext } from '../../Tool.js'
import { buildTool, type ToolDef } from '../../Tool.js'
import { getCwd } from '../../utils/cwd.js'
import { getClaudeConfigHomeDir, isEnvTruthy } from '../../utils/envUtils.js'
import { getErrnoCode, isENOENT } from '../../utils/errors.js'
import {
addLineNumbers,
FILE_NOT_FOUND_CWD_NOTE,
findSimilarFile,
getFileModificationTimeAsync,
suggestPathUnderCwd,
} from '../../utils/file.js'
import { logFileOperation } from '../../utils/fileOperationAnalytics.js'
import { formatFileSize } from '../../utils/format.js'
import { getFsImplementation } from '../../utils/fsOperations.js'
import {
compressImageBufferWithTokenLimit,
createImageMetadataText,
detectImageFormatFromBuffer,
type ImageDimensions,
ImageResizeError,
maybeResizeAndDownsampleImageBuffer,
} from '../../utils/imageResizer.js'
import { lazySchema } from '../../utils/lazySchema.js'
import { logError } from '../../utils/log.js'
import { isAutoMemFile } from '../../utils/memoryFileDetection.js'
import { createUserMessage } from '../../utils/messages.js'
import { getCanonicalName, getMainLoopModel } from '../../utils/model/model.js'
import {
mapNotebookCellsToToolResult,
readNotebook,
} from '../../utils/notebook.js'
import { expandPath } from '../../utils/path.js'
import { extractPDFPages, getPDFPageCount, readPDF } from '../../utils/pdf.js'
import {
isPDFExtension,
isPDFSupported,
parsePDFPageRange,
} from '../../utils/pdfUtils.js'
import {
checkReadPermissionForTool,
matchingRuleForInput,
} from '../../utils/permissions/filesystem.js'
import type { PermissionDecision } from '../../utils/permissions/PermissionResult.js'
import { matchWildcardPattern } from '../../utils/permissions/shellRuleMatching.js'
import { readFileInRange } from '../../utils/readFileInRange.js'
import { semanticNumber } from '../../utils/semanticNumber.js'
import { jsonStringify } from '../../utils/slowOperations.js'
import { BASH_TOOL_NAME } from '../BashTool/toolName.js'
import { getDefaultFileReadingLimits } from './limits.js'
import {
DESCRIPTION,
FILE_READ_TOOL_NAME,
FILE_UNCHANGED_STUB,
LINE_FORMAT_INSTRUCTION,
OFFSET_INSTRUCTION_DEFAULT,
OFFSET_INSTRUCTION_TARGETED,
renderPromptTemplate,
} from './prompt.js'
import {
getToolUseSummary,
renderToolResultMessage,
renderToolUseErrorMessage,
renderToolUseMessage,
renderToolUseTag,
userFacingName,
} from './UI.js'
// Device files that would hang the process: infinite output or blocking input.
// Checked by path only (no I/O). Safe devices like /dev/null are intentionally omitted.
const BLOCKED_DEVICE_PATHS = new Set([
// Infinite output — never reach EOF
'/dev/zero',
'/dev/random',
'/dev/urandom',
'/dev/full',
// Blocks waiting for input
'/dev/stdin',
'/dev/tty',
'/dev/console',
// Nonsensical to read
'/dev/stdout',
'/dev/stderr',
// fd aliases for stdin/stdout/stderr
'/dev/fd/0',
'/dev/fd/1',
'/dev/fd/2',
])
function isBlockedDevicePath(filePath: string): boolean {
if (BLOCKED_DEVICE_PATHS.has(filePath)) return true
// /proc/self/fd/0-2 and /proc/<pid>/fd/0-2 are Linux aliases for stdio
if (
filePath.startsWith('/proc/') &&
(filePath.endsWith('/fd/0') ||
filePath.endsWith('/fd/1') ||
filePath.endsWith('/fd/2'))
)
return true
return false
}
// Narrow no-break space (U+202F) used by some macOS versions in screenshot filenames
const THIN_SPACE = String.fromCharCode(8239)
/**
* Resolves macOS screenshot paths that may have different space characters.
* macOS uses either regular space or thin space (U+202F) before AM/PM in screenshot
* filenames depending on the macOS version. This function tries the alternate space
* character if the file doesn't exist with the given path.
*
* @param filePath - The normalized file path to resolve
* @returns The path to the actual file on disk (may differ in space character)
*/
/**
* For macOS screenshot paths with AM/PM, the space before AM/PM may be a
* regular space or a thin space depending on the macOS version. Returns
* the alternate path to try if the original doesn't exist, or undefined.
*/
function getAlternateScreenshotPath(filePath: string): string | undefined {
const filename = path.basename(filePath)
const amPmPattern = /^(.+)([\u202F])(AM|PM)(\.png)$/
const match = filename.match(amPmPattern)
if (!match) return undefined
const currentSpace = match[2]
const alternateSpace = currentSpace === '' ? THIN_SPACE : ''
return filePath.replace(
`${currentSpace}${match[3]}${match[4]}`,
`${alternateSpace}${match[3]}${match[4]}`,
)
}
// File read listeners - allows other services to be notified when files are read
type FileReadListener = (filePath: string, content: string) => void
const fileReadListeners: FileReadListener[] = []
export function registerFileReadListener(
listener: FileReadListener,
): () => void {
fileReadListeners.push(listener)
return () => {
const i = fileReadListeners.indexOf(listener)
if (i >= 0) fileReadListeners.splice(i, 1)
}
}
export class MaxFileReadTokenExceededError extends Error {
constructor(
public tokenCount: number,
public maxTokens: number,
) {
super(
`File content (${tokenCount} tokens) exceeds maximum allowed tokens (${maxTokens}). Use offset and limit parameters to read specific portions of the file, or search for specific content instead of reading the whole file.`,
)
this.name = 'MaxFileReadTokenExceededError'
}
}
// Common image extensions
const IMAGE_EXTENSIONS = new Set(['png', 'jpg', 'jpeg', 'gif', 'webp'])
/**
* Detects if a file path is a session-related file for analytics logging.
* Only matches files within the Claude config directory (e.g., ~/.claude).
* Returns the type of session file or null if not a session file.
*/
function detectSessionFileType(
filePath: string,
): 'session_memory' | 'session_transcript' | null {
const configDir = getClaudeConfigHomeDir()
// Only match files within the Claude config directory
if (!filePath.startsWith(configDir)) {
return null
}
// Normalize path to use forward slashes for consistent matching across platforms
const normalizedPath = filePath.split(win32.sep).join(posix.sep)
// Session memory files: ~/.claude/session-memory/*.md (including summary.md)
if (
normalizedPath.includes('/session-memory/') &&
normalizedPath.endsWith('.md')
) {
return 'session_memory'
}
// Session JSONL transcript files: ~/.claude/projects/*/*.jsonl
if (
normalizedPath.includes('/projects/') &&
normalizedPath.endsWith('.jsonl')
) {
return 'session_transcript'
}
return null
}
const inputSchema = lazySchema(() =>
z.strictObject({
file_path: z.string().describe('The absolute path to the file to read'),
offset: semanticNumber(z.number().int().nonnegative().optional()).describe(
'The line number to start reading from. Only provide if the file is too large to read at once',
),
limit: semanticNumber(z.number().int().positive().optional()).describe(
'The number of lines to read. Only provide if the file is too large to read at once.',
),
pages: z
.string()
.optional()
.describe(
`Page range for PDF files (e.g., "1-5", "3", "10-20"). Only applicable to PDF files. Maximum ${PDF_MAX_PAGES_PER_READ} pages per request.`,
),
}),
)
type InputSchema = ReturnType<typeof inputSchema>
export type Input = z.infer<InputSchema>
const outputSchema = lazySchema(() => {
// Define the media types supported for images
const imageMediaTypes = z.enum([
'image/jpeg',
'image/png',
'image/gif',
'image/webp',
])
return z.discriminatedUnion('type', [
z.object({
type: z.literal('text'),
file: z.object({
filePath: z.string().describe('The path to the file that was read'),
content: z.string().describe('The content of the file'),
numLines: z
.number()
.describe('Number of lines in the returned content'),
startLine: z.number().describe('The starting line number'),
totalLines: z.number().describe('Total number of lines in the file'),
}),
}),
z.object({
type: z.literal('image'),
file: z.object({
base64: z.string().describe('Base64-encoded image data'),
type: imageMediaTypes.describe('The MIME type of the image'),
originalSize: z.number().describe('Original file size in bytes'),
dimensions: z
.object({
originalWidth: z
.number()
.optional()
.describe('Original image width in pixels'),
originalHeight: z
.number()
.optional()
.describe('Original image height in pixels'),
displayWidth: z
.number()
.optional()
.describe('Displayed image width in pixels (after resizing)'),
displayHeight: z
.number()
.optional()
.describe('Displayed image height in pixels (after resizing)'),
})
.optional()
.describe('Image dimension info for coordinate mapping'),
}),
}),
z.object({
type: z.literal('notebook'),
file: z.object({
filePath: z.string().describe('The path to the notebook file'),
cells: z.array(z.any()).describe('Array of notebook cells'),
}),
}),
z.object({
type: z.literal('pdf'),
file: z.object({
filePath: z.string().describe('The path to the PDF file'),
base64: z.string().describe('Base64-encoded PDF data'),
originalSize: z.number().describe('Original file size in bytes'),
}),
}),
z.object({
type: z.literal('parts'),
file: z.object({
filePath: z.string().describe('The path to the PDF file'),
originalSize: z.number().describe('Original file size in bytes'),
count: z.number().describe('Number of pages extracted'),
outputDir: z
.string()
.describe('Directory containing extracted page images'),
}),
}),
z.object({
type: z.literal('file_unchanged'),
file: z.object({
filePath: z.string().describe('The path to the file'),
}),
}),
])
})
type OutputSchema = ReturnType<typeof outputSchema>
export type Output = z.infer<OutputSchema>
export const FileReadTool = buildTool({
name: FILE_READ_TOOL_NAME,
searchHint: 'read files, images, PDFs, notebooks',
// Output is bounded by maxTokens (validateContentTokens). Persisting to a
// file the model reads back with Read is circular — never persist.
maxResultSizeChars: Infinity,
strict: true,
async description() {
return DESCRIPTION
},
async prompt() {
const limits = getDefaultFileReadingLimits()
const maxSizeInstruction = limits.includeMaxSizeInPrompt
? `. Files larger than ${formatFileSize(limits.maxSizeBytes)} will return an error; use offset and limit for larger files`
: ''
const offsetInstruction = limits.targetedRangeNudge
? OFFSET_INSTRUCTION_TARGETED
: OFFSET_INSTRUCTION_DEFAULT
return renderPromptTemplate(
pickLineFormatInstruction(),
maxSizeInstruction,
offsetInstruction,
)
},
get inputSchema(): InputSchema {
return inputSchema()
},
get outputSchema(): OutputSchema {
return outputSchema()
},
userFacingName,
getToolUseSummary,
getActivityDescription(input) {
const summary = getToolUseSummary(input)
return summary ? `Reading ${summary}` : 'Reading file'
},
isConcurrencySafe() {
return true
},
isReadOnly() {
return true
},
toAutoClassifierInput(input) {
return input.file_path
},
isSearchOrReadCommand() {
return { isSearch: false, isRead: true }
},
getPath({ file_path }): string {
return file_path || getCwd()
},
backfillObservableInput(input) {
// hooks.mdx documents file_path as absolute; expand so hook allowlists
// can't be bypassed via ~ or relative paths.
if (typeof input.file_path === 'string') {
input.file_path = expandPath(input.file_path)
}
},
async preparePermissionMatcher({ file_path }) {
return pattern => matchWildcardPattern(pattern, file_path)
},
async checkPermissions(input, context): Promise<PermissionDecision> {
const appState = context.getAppState()
return checkReadPermissionForTool(
FileReadTool,
input,
appState.toolPermissionContext,
)
},
renderToolUseMessage,
renderToolUseTag,
renderToolResultMessage,
// UI.tsx:140 — ALL types render summary chrome only: "Read N lines",
// "Read image (42KB)". Never the content itself. The model-facing
// serialization (below) sends content + CYBER_RISK_MITIGATION_REMINDER
// + line prefixes; UI shows none of it. Nothing to index. Caught by
// the render-fidelity test when this initially claimed file.content.
extractSearchText() {
return ''
},
renderToolUseErrorMessage,
async validateInput({ file_path, pages }, toolUseContext: ToolUseContext) {
// Validate pages parameter (pure string parsing, no I/O)
if (pages !== undefined) {
const parsed = parsePDFPageRange(pages)
if (!parsed) {
return {
result: false,
message: `Invalid pages parameter: "${pages}". Use formats like "1-5", "3", or "10-20". Pages are 1-indexed.`,
errorCode: 7,
}
}
const rangeSize =
parsed.lastPage === Infinity
? PDF_MAX_PAGES_PER_READ + 1
: parsed.lastPage - parsed.firstPage + 1
if (rangeSize > PDF_MAX_PAGES_PER_READ) {
return {
result: false,
message: `Page range "${pages}" exceeds maximum of ${PDF_MAX_PAGES_PER_READ} pages per request. Please use a smaller range.`,
errorCode: 8,
}
}
}
// Path expansion + deny rule check (no I/O)
const fullFilePath = expandPath(file_path)
const appState = toolUseContext.getAppState()
const denyRule = matchingRuleForInput(
fullFilePath,
appState.toolPermissionContext,
'read',
'deny',
)
if (denyRule !== null) {
return {
result: false,
message:
'File is in a directory that is denied by your permission settings.',
errorCode: 1,
}
}
// SECURITY: UNC path check (no I/O) — defer filesystem operations
// until after user grants permission to prevent NTLM credential leaks
const isUncPath =
fullFilePath.startsWith('\\\\') || fullFilePath.startsWith('//')
if (isUncPath) {
return { result: true }
}
// Binary extension check (string check on extension only, no I/O).
// PDF, images, and SVG are excluded - this tool renders them natively.
const ext = path.extname(fullFilePath).toLowerCase()
if (
hasBinaryExtension(fullFilePath) &&
!isPDFExtension(ext) &&
!IMAGE_EXTENSIONS.has(ext.slice(1))
) {
return {
result: false,
message: `This tool cannot read binary files. The file appears to be a binary ${ext} file. Please use appropriate tools for binary file analysis.`,
errorCode: 4,
}
}
// Block specific device files that would hang (infinite output or blocking input).
// This is a path-based check with no I/O — safe special files like /dev/null are allowed.
if (isBlockedDevicePath(fullFilePath)) {
return {
result: false,
message: `Cannot read '${file_path}': this device file would block or produce infinite output.`,
errorCode: 9,
}
}
return { result: true }
},
async call(
{ file_path, offset = 1, limit = undefined, pages },
context,
_canUseTool?,
parentMessage?,
) {
const { readFileState, fileReadingLimits } = context
const defaults = getDefaultFileReadingLimits()
const maxSizeBytes =
fileReadingLimits?.maxSizeBytes ?? defaults.maxSizeBytes
const maxTokens = fileReadingLimits?.maxTokens ?? defaults.maxTokens
// Telemetry: track when callers override default read limits.
// Only fires on override (low volume) — event count = override frequency.
if (fileReadingLimits !== undefined) {
logEvent('tengu_file_read_limits_override', {
hasMaxTokens: fileReadingLimits.maxTokens !== undefined,
hasMaxSizeBytes: fileReadingLimits.maxSizeBytes !== undefined,
})
}
const ext = path.extname(file_path).toLowerCase().slice(1)
// Use expandPath for consistent path normalization with FileEditTool/FileWriteTool
// (especially handles whitespace trimming and Windows path separators)
const fullFilePath = expandPath(file_path)
// Dedup: if we've already read this exact range and the file hasn't
// changed on disk, return a stub instead of re-sending the full content.
// The earlier Read tool_result is still in context — two full copies
// waste cache_creation tokens on every subsequent turn. BQ proxy shows
// ~18% of Read calls are same-file collisions (up to 2.64% of fleet
// cache_creation). Only applies to text/notebook reads — images/PDFs
// aren't cached in readFileState so won't match here.
//
// Ant soak: 1,734 dedup hits in 2h, no Read error regression.
// Killswitch pattern: GB can disable if the stub message confuses
// the model externally.
// 3P default: killswitch off = dedup enabled. Client-side only — no
// server support needed, safe for Bedrock/Vertex/Foundry.
const dedupKillswitch = getFeatureValue_CACHED_MAY_BE_STALE(
'tengu_read_dedup_killswitch',
false,
)
const existingState = dedupKillswitch
? undefined
: readFileState.get(fullFilePath)
// Only dedup entries that came from a prior Read (offset is always set
// by Read). Edit/Write store offset=undefined — their readFileState
// entry reflects post-edit mtime, so deduping against it would wrongly
// point the model at the pre-edit Read content.
if (
existingState &&
!existingState.isPartialView &&
existingState.offset !== undefined
) {
const rangeMatch =
existingState.offset === offset && existingState.limit === limit
if (rangeMatch) {
try {
const mtimeMs = await getFileModificationTimeAsync(fullFilePath)
if (mtimeMs === existingState.timestamp) {
const analyticsExt = getFileExtensionForAnalytics(fullFilePath)
logEvent('tengu_file_read_dedup', {
...(analyticsExt !== undefined && { ext: analyticsExt }),
})
return {
data: {
type: 'file_unchanged' as const,
file: { filePath: file_path },
},
}
}
} catch {
// stat failed — fall through to full read
}
}
}
// Discover skills from this file's path (fire-and-forget, non-blocking)
// Skip in simple mode - no skills available
const cwd = getCwd()
if (!isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE)) {
const newSkillDirs = await discoverSkillDirsForPaths([fullFilePath], cwd)
if (newSkillDirs.length > 0) {
// Store discovered dirs for attachment display
for (const dir of newSkillDirs) {
context.dynamicSkillDirTriggers?.add(dir)
}
// Don't await - let skill loading happen in the background
addSkillDirectories(newSkillDirs).catch(() => {})
}
// Activate conditional skills whose path patterns match this file
activateConditionalSkillsForPaths([fullFilePath], cwd)
}
try {
return await callInner(
file_path,
fullFilePath,
fullFilePath,
ext,
offset,
limit,
pages,
maxSizeBytes,
maxTokens,
readFileState,
context,
parentMessage?.message.id,
)
} catch (error) {
// Handle file-not-found: suggest similar files
const code = getErrnoCode(error)
if (code === 'ENOENT') {
// macOS screenshots may use a thin space or regular space before
// AM/PM — try the alternate before giving up.
const altPath = getAlternateScreenshotPath(fullFilePath)
if (altPath) {
try {
return await callInner(
file_path,
fullFilePath,
altPath,
ext,
offset,
limit,
pages,
maxSizeBytes,
maxTokens,
readFileState,
context,
parentMessage?.message.id,
)
} catch (altError) {
if (!isENOENT(altError)) {
throw altError
}
// Alt path also missing — fall through to friendly error
}
}
const similarFilename = findSimilarFile(fullFilePath)
const cwdSuggestion = await suggestPathUnderCwd(fullFilePath)
let message = `File does not exist. ${FILE_NOT_FOUND_CWD_NOTE}${getCwd()}.`
if (cwdSuggestion) {
message += ` Did you mean ${cwdSuggestion}?`
} else if (similarFilename) {
message += ` Did you mean ${similarFilename}?`
}
throw new Error(message)
}
throw error
}
},
mapToolResultToToolResultBlockParam(data, toolUseID) {
switch (data.type) {
case 'image': {
return {
tool_use_id: toolUseID,
type: 'tool_result',
content: [
{
type: 'image',
source: {
type: 'base64',
data: data.file.base64,
media_type: data.file.type,
},
},
],
}
}
case 'notebook':
return mapNotebookCellsToToolResult(data.file.cells, toolUseID)
case 'pdf':
// Return PDF metadata only - the actual content is sent as a supplemental DocumentBlockParam
return {
tool_use_id: toolUseID,
type: 'tool_result',
content: `PDF file read: ${data.file.filePath} (${formatFileSize(data.file.originalSize)})`,
}
case 'parts':
// Extracted page images are read and sent as image blocks in mapToolResultToAPIMessage
return {
tool_use_id: toolUseID,
type: 'tool_result',
content: `PDF pages extracted: ${data.file.count} page(s) from ${data.file.filePath} (${formatFileSize(data.file.originalSize)})`,
}
case 'file_unchanged':
return {
tool_use_id: toolUseID,
type: 'tool_result',
content: FILE_UNCHANGED_STUB,
}
case 'text': {
let content: string
if (data.file.content) {
content =
memoryFileFreshnessPrefix(data) +
formatFileLines(data.file) +
(shouldIncludeFileReadMitigation()
? CYBER_RISK_MITIGATION_REMINDER
: '')
} else {
// Determine the appropriate warning message
content =
data.file.totalLines === 0
? '<system-reminder>Warning: the file exists but the contents are empty.</system-reminder>'
: `<system-reminder>Warning: the file exists but is shorter than the provided offset (${data.file.startLine}). The file has ${data.file.totalLines} lines.</system-reminder>`
}
return {
tool_use_id: toolUseID,
type: 'tool_result',
content,
}
}
}
},
} satisfies ToolDef<InputSchema, Output>)
function pickLineFormatInstruction(): string {
return LINE_FORMAT_INSTRUCTION
}
/** Format file content with line numbers. */
function formatFileLines(file: { content: string; startLine: number }): string {
return addLineNumbers(file)
}
export const CYBER_RISK_MITIGATION_REMINDER =
'\n\n<system-reminder>\nWhenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.\n</system-reminder>\n'
// Models where cyber risk mitigation should be skipped
const MITIGATION_EXEMPT_MODELS = new Set(['claude-opus-4-6'])
function shouldIncludeFileReadMitigation(): boolean {
const shortName = getCanonicalName(getMainLoopModel())
return !MITIGATION_EXEMPT_MODELS.has(shortName)
}
/**
* Side-channel from call() to mapToolResultToToolResultBlockParam: mtime
* of auto-memory files, keyed by the `data` object identity. Avoids
* adding a presentation-only field to the output schema (which flows
* into SDK types) and avoids sync fs in the mapper. WeakMap auto-GCs
* when the data object becomes unreachable after rendering.
*/
const memoryFileMtimes = new WeakMap<object, number>()
function memoryFileFreshnessPrefix(data: object): string {
const mtimeMs = memoryFileMtimes.get(data)
if (mtimeMs === undefined) return ''
return memoryFreshnessNote(mtimeMs)
}
async function validateContentTokens(
content: string,
ext: string,
maxTokens?: number,
): Promise<void> {
const effectiveMaxTokens =
maxTokens ?? getDefaultFileReadingLimits().maxTokens
const tokenEstimate = roughTokenCountEstimationForFileType(content, ext)
if (!tokenEstimate || tokenEstimate <= effectiveMaxTokens / 4) return
const tokenCount = await countTokensWithAPI(content)
const effectiveCount = tokenCount ?? tokenEstimate
if (effectiveCount > effectiveMaxTokens) {
throw new MaxFileReadTokenExceededError(effectiveCount, effectiveMaxTokens)
}
}
type ImageResult = {
type: 'image'
file: {
base64: string
type: Base64ImageSource['media_type']
originalSize: number
dimensions?: ImageDimensions
}
}
function createImageResponse(
buffer: Buffer,
mediaType: string,
originalSize: number,
dimensions?: ImageDimensions,
): ImageResult {
return {
type: 'image',
file: {
base64: buffer.toString('base64'),
type: `image/${mediaType}` as Base64ImageSource['media_type'],
originalSize,
dimensions,
},
}
}
/**
* Inner implementation of call, separated to allow ENOENT handling in the outer call.
*/
async function callInner(
file_path: string,
fullFilePath: string,
resolvedFilePath: string,
ext: string,
offset: number,
limit: number | undefined,
pages: string | undefined,
maxSizeBytes: number,
maxTokens: number,
readFileState: ToolUseContext['readFileState'],
context: ToolUseContext,
messageId: string | undefined,
): Promise<{
data: Output
newMessages?: ReturnType<typeof createUserMessage>[]
}> {
// --- Notebook ---
if (ext === 'ipynb') {
const cells = await readNotebook(resolvedFilePath)
const cellsJson = jsonStringify(cells)
const cellsJsonBytes = Buffer.byteLength(cellsJson)
if (cellsJsonBytes > maxSizeBytes) {
throw new Error(
`Notebook content (${formatFileSize(cellsJsonBytes)}) exceeds maximum allowed size (${formatFileSize(maxSizeBytes)}). ` +
`Use ${BASH_TOOL_NAME} with jq to read specific portions:\n` +
` cat "${file_path}" | jq '.cells[:20]' # First 20 cells\n` +
` cat "${file_path}" | jq '.cells[100:120]' # Cells 100-120\n` +
` cat "${file_path}" | jq '.cells | length' # Count total cells\n` +
` cat "${file_path}" | jq '.cells[] | select(.cell_type=="code") | .source' # All code sources`,
)
}
await validateContentTokens(cellsJson, ext, maxTokens)
// Get mtime via async stat (single call, no prior existence check)
const stats = await getFsImplementation().stat(resolvedFilePath)
readFileState.set(fullFilePath, {
content: cellsJson,
timestamp: Math.floor(stats.mtimeMs),
offset,
limit,
})
context.nestedMemoryAttachmentTriggers?.add(fullFilePath)
const data = {
type: 'notebook' as const,
file: { filePath: file_path, cells },
}
logFileOperation({
operation: 'read',
tool: 'FileReadTool',
filePath: fullFilePath,
content: cellsJson,
})
return { data }
}
// --- Image (single read, no double-read) ---
if (IMAGE_EXTENSIONS.has(ext)) {
// Images have their own size limits (token budget + compression) —
// don't apply the text maxSizeBytes cap.
const data = await readImageWithTokenBudget(resolvedFilePath, maxTokens)
context.nestedMemoryAttachmentTriggers?.add(fullFilePath)
logFileOperation({
operation: 'read',
tool: 'FileReadTool',
filePath: fullFilePath,
content: data.file.base64,
})
const metadataText = data.file.dimensions
? createImageMetadataText(data.file.dimensions)
: null
return {
data,
...(metadataText && {
newMessages: [
createUserMessage({ content: metadataText, isMeta: true }),
],
}),
}
}
// --- PDF ---
if (isPDFExtension(ext)) {
if (pages) {
const parsedRange = parsePDFPageRange(pages)
const extractResult = await extractPDFPages(
resolvedFilePath,
parsedRange ?? undefined,
)
if (!extractResult.success) {
throw new Error(extractResult.error.message)
}
logEvent('tengu_pdf_page_extraction', {
success: true,
pageCount: extractResult.data.file.count,
fileSize: extractResult.data.file.originalSize,
hasPageRange: true,
})
logFileOperation({
operation: 'read',
tool: 'FileReadTool',
filePath: fullFilePath,
content: `PDF pages ${pages}`,
})
const entries = await readdir(extractResult.data.file.outputDir)
const imageFiles = entries.filter(f => f.endsWith('.jpg')).sort()
const imageBlocks = await Promise.all(
imageFiles.map(async f => {
const imgPath = path.join(extractResult.data.file.outputDir, f)
const imgBuffer = await readFileAsync(imgPath)
const resized = await maybeResizeAndDownsampleImageBuffer(
imgBuffer,
imgBuffer.length,
'jpeg',
)
return {
type: 'image' as const,
source: {
type: 'base64' as const,
media_type:
`image/${resized.mediaType}` as Base64ImageSource['media_type'],
data: resized.buffer.toString('base64'),
},
}
}),
)
return {
data: extractResult.data,
...(imageBlocks.length > 0 && {
newMessages: [
createUserMessage({ content: imageBlocks, isMeta: true }),
],
}),
}
}
const pageCount = await getPDFPageCount(resolvedFilePath)
if (pageCount !== null && pageCount > PDF_AT_MENTION_INLINE_THRESHOLD) {
throw new Error(
`This PDF has ${pageCount} pages, which is too many to read at once. ` +
`Use the pages parameter to read specific page ranges (e.g., pages: "1-5"). ` +
`Maximum ${PDF_MAX_PAGES_PER_READ} pages per request.`,
)
}
const fs = getFsImplementation()
const stats = await fs.stat(resolvedFilePath)
const shouldExtractPages =
!isPDFSupported() || stats.size > PDF_EXTRACT_SIZE_THRESHOLD
if (shouldExtractPages) {
const extractResult = await extractPDFPages(resolvedFilePath)
if (extractResult.success) {
logEvent('tengu_pdf_page_extraction', {
success: true,
pageCount: extractResult.data.file.count,
fileSize: extractResult.data.file.originalSize,
})
} else {
logEvent('tengu_pdf_page_extraction', {
success: false,
available: extractResult.error.reason !== 'unavailable',
fileSize: stats.size,
})
}
}
if (!isPDFSupported()) {
throw new Error(
'Reading full PDFs is not supported with this model. Use a newer model (Sonnet 3.5 v2 or later), ' +
`or use the pages parameter to read specific page ranges (e.g., pages: "1-5", maximum ${PDF_MAX_PAGES_PER_READ} pages per request). ` +
'Page extraction requires poppler-utils: install with `brew install poppler` on macOS or `apt-get install poppler-utils` on Debian/Ubuntu.',
)
}
const readResult = await readPDF(resolvedFilePath)
if (!readResult.success) {
throw new Error(readResult.error.message)
}
const pdfData = readResult.data
logFileOperation({
operation: 'read',
tool: 'FileReadTool',
filePath: fullFilePath,
content: pdfData.file.base64,
})
return {
data: pdfData,
newMessages: [
createUserMessage({
content: [
{
type: 'document',
source: {
type: 'base64',
media_type: 'application/pdf',
data: pdfData.file.base64,
},
},
],
isMeta: true,
}),
],
}
}
// --- Text file (single async read via readFileInRange) ---
const lineOffset = offset === 0 ? 0 : offset - 1
const { content, lineCount, totalLines, totalBytes, readBytes, mtimeMs } =
await readFileInRange(
resolvedFilePath,
lineOffset,
limit,
limit === undefined ? maxSizeBytes : undefined,
context.abortController.signal,
)
await validateContentTokens(content, ext, maxTokens)
readFileState.set(fullFilePath, {
content,
timestamp: Math.floor(mtimeMs),
offset,
limit,
})
context.nestedMemoryAttachmentTriggers?.add(fullFilePath)
// Snapshot before iterating — a listener that unsubscribes mid-callback
// would splice the live array and skip the next listener.
for (const listener of fileReadListeners.slice()) {
listener(resolvedFilePath, content)
}
const data = {
type: 'text' as const,
file: {
filePath: file_path,
content,
numLines: lineCount,
startLine: offset,
totalLines,
},
}
if (isAutoMemFile(fullFilePath)) {
memoryFileMtimes.set(data, mtimeMs)
}
logFileOperation({
operation: 'read',
tool: 'FileReadTool',
filePath: fullFilePath,
content,
})
const sessionFileType = detectSessionFileType(fullFilePath)
const analyticsExt = getFileExtensionForAnalytics(fullFilePath)
logEvent('tengu_session_file_read', {
totalLines,
readLines: lineCount,
totalBytes,
readBytes,
offset,
...(limit !== undefined && { limit }),
...(analyticsExt !== undefined && { ext: analyticsExt }),
...(messageId !== undefined && {
messageID:
messageId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
}),
is_session_memory: sessionFileType === 'session_memory',
is_session_transcript: sessionFileType === 'session_transcript',
})
return { data }
}
/**
* Reads an image file and applies token-based compression if needed.
* Reads the file ONCE, then applies standard resize. If the result exceeds
* the token limit, applies aggressive compression from the same buffer.
*
* @param filePath - Path to the image file
* @param maxTokens - Maximum token budget for the image
* @returns Image data with appropriate compression applied
*/
export async function readImageWithTokenBudget(
filePath: string,
maxTokens: number = getDefaultFileReadingLimits().maxTokens,
maxBytes?: number,
): Promise<ImageResult> {
// Read file ONCE — capped to maxBytes to avoid OOM on huge files
const imageBuffer = await getFsImplementation().readFileBytes(
filePath,
maxBytes,
)
const originalSize = imageBuffer.length
if (originalSize === 0) {
throw new Error(`Image file is empty: ${filePath}`)
}
const detectedMediaType = detectImageFormatFromBuffer(imageBuffer)
const detectedFormat = detectedMediaType.split('/')[1] || 'png'
// Try standard resize
let result: ImageResult
try {
const resized = await maybeResizeAndDownsampleImageBuffer(
imageBuffer,
originalSize,
detectedFormat,
)
result = createImageResponse(
resized.buffer,
resized.mediaType,
originalSize,
resized.dimensions,
)
} catch (e) {
if (e instanceof ImageResizeError) throw e
logError(e)
result = createImageResponse(imageBuffer, detectedFormat, originalSize)
}
// Check if it fits in token budget
const estimatedTokens = Math.ceil(result.file.base64.length * 0.125)
if (estimatedTokens > maxTokens) {
// Aggressive compression from the SAME buffer (no re-read)
try {
const compressed = await compressImageBufferWithTokenLimit(
imageBuffer,
maxTokens,
detectedMediaType,
)
return {
type: 'image',
file: {
base64: compressed.base64,
type: compressed.mediaType,
originalSize,
},
}
} catch (e) {
logError(e)
// Fallback: heavily compressed version from the SAME buffer
try {
const sharpModule = await import('sharp')
const sharp =
(
sharpModule as {
default?: typeof sharpModule
} & typeof sharpModule
).default || sharpModule
const fallbackBuffer = await sharp(imageBuffer)
.resize(400, 400, {
fit: 'inside',
withoutEnlargement: true,
})
.jpeg({ quality: 20 })
.toBuffer()
return createImageResponse(fallbackBuffer, 'jpeg', originalSize)
} catch (error) {
logError(error)
return createImageResponse(imageBuffer, detectedFormat, originalSize)
}
}
}
return result
}
Loading...
举报
举报成功
我们将于2个工作日内通过站内信反馈结果给你!
请认真填写举报原因,尽可能描述详细。
请选择举报类型
取消
发送
误判申诉

此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。

如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。

取消
提交

简介

claude code v2.1.88反编译源码
取消

发行版

暂无发行版

贡献者

全部

近期动态

不能加载更多了
编辑仓库简介
简介内容
主页
马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化
1
https://gitee.com/xddcode_admin/claude-code-source-code.git
git@gitee.com:xddcode_admin/claude-code-source-code.git
xddcode_admin
claude-code-source-code
claude-code-source-code
main
点此查找更多帮助

搜索帮助

评论
仓库举报
回到顶部
登录提示
该操作需登录 Gitee 帐号,请先登录后再操作。
立即登录
没有帐号,去注册

AltStyle によって変換されたページ (->オリジナル) /