08-文件工具族

📅 2026/7/17 1:33:37
08-文件工具族
8. 文件工具族所属分组工具系统概述文件操作是 Claude Code 与代码库交互最频繁的场景因此文件工具族被设计得最为细致FileEditTool 负责精确字符串替换FileWriteTool 负责整文件创建/覆盖FileReadTool 负责读取文本、图片、PDF、notebook 等多形态BriefTool 则承担向用户发送富文本消息并附带附件的职能。这四个工具共享一套diff 生成 拒绝回退 路径展示的 UI 哲学但各自的实现细节折射出不同的关注点。本文以四个工具目录下的 UI 与 prompt/upload 文件为切入点剖析它们如何在统一的Tool接口下演化出各自的渲染策略与执行机制。重点关注 diff 生成的懒加载、计划文件的特殊处理、Brief 附件上传的 graceful degradation 等设计。源码位置[tools/FileEditTool/UI.tsx](file:///e:/2026plan/AI_Lab/claude-code-sourcemap-main/restored-src/src/tools/FileEditTool/UI.tsx)[tools/FileWriteTool/UI.tsx](file:///e:/2026plan/AI_Lab/claude-code-sourcemap-main/restored-src/src/tools/FileWriteTool/UI.tsx)[tools/FileReadTool/UI.tsx](file:///e:/2026plan/AI_Lab/claude-code-sourcemap-main/restored-src/src/tools/FileReadTool/UI.tsx)[tools/BriefTool/prompt.ts](file:///e:/2026plan/AI_Lab/claude-code-sourcemap-main/restored-src/src/tools/BriefTool/prompt.ts)[tools/BriefTool/upload.ts](file:///e:/2026plan/AI_Lab/claude-code-sourcemap-main/restored-src/src/tools/BriefTool/upload.ts)核心实现分析1. FileEditTool从输入推断意图的 userFacingNameFileEditTool 的userFacingName()不是简单返回固定字符串而是根据输入推断模型意图exportfunctionuserFacingName(input):string{if(!input)returnUpdateif(input.file_path?.startsWith(getPlansDirectory()))returnUpdated planif(input.edits!null)returnUpdate// Hashline 多点编辑if(input.old_string)returnCreate// 空字符串新建文件returnUpdate}old_string 被解释为新建文件UI 显示Create而非Update——这是工具用输入字段语义反推操作类型的范例。edits字段则代表 hashline 多点编辑模式基于行号引用的批量编辑。2. FileEditTool 的拒绝 diff 懒加载renderToolUseRejectedMessage是用户拒绝编辑后看到的 diff 视图。它使用 React Suspense useState 懒加载 difffunctionEditRejectionDiff({filePath,oldString,newString,replaceAll,style,verbose}){const[dataPromise]useState(()loadRejectionDiff(filePath,oldString,newString,replaceAll))// ...returnSuspense fallback{t2}{t3}/Suspense}loadRejectionDiff调用readEditContext做分块读取——只读old_string第一次出现位置周围的窗口CONTEXT_LINES 行而不是整个文件asyncfunctionloadRejectionDiff(filePath,oldString,newString,replaceAll){// Chunked read — context window around the first occurrence. replaceAll// still shows matches *within* the window via getPatchForEdit; we accept// losing the all-occurrences view to keep the read bounded.constctxawaitreadEditContext(filePath,oldString,CONTEXT_LINES)if(ctxnull||ctx.truncated||ctx.content){// ENOENT / not found / truncated — diff just the tool inputs.const{patch}getPatchForEdit({filePath,fileContents:oldString,oldString,newString})return{patch,firstLine:null,fileContent:undefined}}constactualOldfindActualString(ctx.content,oldString)||oldStringconstactualNewpreserveQuoteStyle(oldString,actualOld,newString)// ...return{patch:adjustHunkLineNumbers(patch,ctx.lineOffset-1),/* ... */}}注释里accept losing the all-occurrences view to keep the read bounded揭示了取舍当replace_all: true时理论上应该展示所有匹配位置但为了保证读取有界只展示第一个匹配窗口内的所有匹配——这样既不 OOM又保留了大部分信息。preserveQuoteStyle处理old_string中的引号风格被模型改写的情况如直引号变弯引号让 diff 显示用户文件里的真实旧字符串。3. FileEditTool 的错误降级renderToolUseErrorMessage把底层的tool_use_error标签转换为更友好的提示if(errorMessage?.includes(File has not been read yet)){returnMessageResponseText dimColorFile must be read first/Text/MessageResponse}if(errorMessage?.includes(FILE_NOT_FOUND_CWD_NOTE)){returnMessageResponseText colorerrorFile not found/Text/MessageResponse}returnMessageResponseText colorerrorError editing file/Text/MessageResponseFile must be read first用 dimColor 表示这是预期行为而非错误模型忘了先 ReadFile not found才用 error 红色。这种颜色编码错误严重性的细节让 UI 更准确传达状态。4. FileWriteToolcountLines 与 EOL 处理FileWriteTool 中有个独立的countLines函数constEOL\nexportfunctioncountLines(content:string):number{constpartscontent.split(EOL)returncontent.endsWith(EOL)?parts.length-1:parts.length}注释说明原因“Model output uses \n regardless of platform, so always split on \n. os.EOL is \r\n on Windows, which would give numLines1 for all files.” 这是跨平台一致性的硬性要求——文件内容来自模型永远是 \n 分隔不能用os.EOL。5. FileWriteTool 的 isResultTruncated 早退出isResultTruncated()决定是否显示点击展开提示。它做了一个性能优化——只数前 MAX_LINES_TO_RENDER1 行就退出不切分整个内容exportfunctionisResultTruncated({type,content}:Output):boolean{if(type!create)returnfalseletpos0for(leti0;iMAX_LINES_TO_RENDER;i){poscontent.indexOf(EOL,pos)if(pos-1)returnfalsepos}returnposcontent.length}注释提到这是Called per visible message on hover/scroll, so early-exit after finding the (MAX1)th line instead of splitting the whole (possibly huge) content. 在大文件创建时这条路径会被频繁调用早退出避免了无意义的字符串拷贝。6. FileWriteTool 的 create vs update 双形态renderToolResultMessage根据type字段分两路渲染switch(type){casecreate:// Plan files 特殊处理常规模式只显示 /plan hintcondensed 模式显示全内容if(isPlanFile!verbose){if(style!condensed){returnMessageResponseText dimColor/plan to preview/Text/MessageResponse}}elseif(stylecondensed!verbose){returnTextWroteText bold{numLines}/Textlines to{ }Text bold{relative(getCwd(),filePath)}/Text/Text}returnFileWriteToolCreatedMessage filePath{filePath}content{content}verbose{verbose}/caseupdate:returnFileEditToolUpdatedMessage filePath{filePath}structuredPatch{structuredPatch}/* ... *//}create渲染文件预览前 MAX_LINES_TO_RENDER10 行 “N lines” 提示update渲染 diff。Plan files 又是一种特例——它把 condensed/verbose 行为反转了常规模式只显示提示因为 plan 内容较长用户可以用/plan命令展开condensed 模式subagent 视图反而显示全内容。这种上下文反向的设计反映了 plan 文件在不同视图下的语义差异。7. FileWriteTool 的 RejectionDiff 三态loadRejectionDiff返回三种状态typeRejectionDiffData|{type:create}// 文件原本不存在回退到 create 视图|{type:update;patch;oldContent}// 正常 update diff|{type:error}// 异常用户可能手动改了文件openForScan返回 null 表示文件不存在 →type: createreadCapped返回 null 表示文件超过 MAX_SCAN_BYTES → 同样回退到 create 视图“rather than OOMing on a diff of a multi-GB file”。error 分支显示(No changes)“——这是注释里说的User may have manually applied the change while the diff was shown”。8. FileReadTool多类型输出渲染renderToolResultMessage根据output.type分支switch(output.type){caseimage:returnTextReadimage({formattedSize})/Textcasenotebook:returnTextReadText bold{cells.length}/Textcells/Textcasepdf:returnTextReadPDF({formattedSize})/Textcaseparts:returnTextReadText bold{output.file.count}/Text{ }{...?page:pages}({formatFileSize(...)})/Textcasetext:returnTextReadText bold{numLines}/Text{ }{numLines1?line:lines}/Textcasefile_unchanged:returnText dimColorUnchanged since last read/Text}注意file_unchanged用 dimColor 表示未变化——这是 FileStateCache 的应用模型重复读同一文件时如果文件未被外部修改UI 上提示Unchanged since last read让用户知道模型没在浪费 token。getAgentOutputTaskId又是一个特殊路径——当读取{projectTempDir}/tasks/{taskId}.output时UI 隐藏括号内容task ID 通过renderToolUseTag单独显示。9. BriefTool 的 prompt把消息通道讲清楚BriefTool工具名SendUserMessage旧名Brief的 prompt 短小但语义密集exportconstBRIEF_TOOL_PROMPTSend a message the user will read. Text outside this tool is visible in the detail view, but most wont open it — the answer lives here. \message\ supports markdown. \attachments\ takes file paths (absolute or cwd-relative) for images, diffs, logs. \status\ labels intent: normal when replying to what they just asked; proactive when youre initiating — a scheduled task finished, a blocker surfaced during background work, you need input on something they havent asked about. Set it honestly; downstream routing uses it.它讲清了三件事① 工具外文本用户多半看不到重要内容必须走 BriefTool② attachments 接受文件路径图片、diff、日志③status字段会被下游路由使用必须如实标注。BRIEF_PROACTIVE_SECTION进一步强调every time the user says something, the reply they actually read comes through BriefTool. Even for ‘hi’. Even for ‘thanks’.——这是为主动消息场景训练模型习惯的强约束。10. BriefTool upload.tsgraceful degradation 的范本uploadBriefAttachment是附件上传的 best-effort 实现文件头注释把它说得很清楚/** * Upload BriefTool attachments to private_api so web viewers can preview them. * ... * Best-effort: any failure (no token, bridge off, network error, 4xx) logs * debug and returns undefined. The attachment still carries {path, size, * isImage}, so local-terminal and same-machine-desktop render unaffected. */每一个早返回都是有意为之的降级if(feature(BRIDGE_MODE)){if(!ctx.replBridgeEnabled)returnundefined// bridge 未启用if(sizeMAX_UPLOAD_BYTES){/* skip */}// 超过 30MBconsttokengetBridgeAccessToken()if(!token){debug(skip: no oauth token);returnundefined}// 无 tokenletcontent:Buffertry{contentawaitreadFile(fullPath)}catch(e){debug(read failed);returnundefined}// 读文件失败// ...axios.post...if(response.status!201){/* upload failed */returnundefined}// ...}returnundefined注释提到一个微妙的部署细节“Subprocess hosts (cowork) pass ANTHROPIC_BASE_URL alongside CLAUDE_CODE_OAUTH_TOKEN — prefer that since getOauthConfig() only returns staging when USE_STAGING_OAUTH is set, which such hosts don’t set. Without this a staging token hits api.anthropic.com → 401 → silent skip → web viewer sees inert cards with no file_uuid.” 这是真实部署踩过的坑——staging token 打到生产域名导致 401所有附件静默失败。11. MIME 类型白名单的取舍MIME_BY_EXT只列出四种图片格式constMIME_BY_EXT:Recordstring,string{.png:image/png,.jpg:image/jpeg,.jpeg:image/jpeg,.gif:image/gif,.webp:image/webp,}注释解释了为什么 svg/bmp/ico/pdf 都不在内“svg/bmp/ico risk a 400, and pdf routes to upload_pdf_file_wrapped which also skips ORIGINAL. Dispatch viewers use /preview for images and /contents for everything else, so images go image/* and the rest go octet-stream.” 这是对后端 dispatch 行为的精确适配——非白名单格式统一用application/octet-stream由后端走 generic file 路径。关键设计要点意图反推FileEditTool 用old_string 推断CreateBriefTool 用status字段区分normal/proactiveUI 标签随意图变化避免一刀切的 Update/Write 表述。diff 懒加载与有界读取拒绝 diff 用 Suspense readEditContext只读窗口上下文超大文件回退到 create 视图保证 UI 永不 OOM。跨平台 EOL 一致性FileWriteTool 强制\n分割注释明确反对os.EOL保证模型输出在 Windows 上也能正确数行。Plan 文件上下文反向渲染常规模式只显示/plan提示condensed 模式subagent 视图反而展示全内容反映 plan 在不同视图下的语义差异。BriefTool 全链路降级附件上传从 bridge 状态、token 存在性、文件大小、读文件、网络请求五层做 graceful degradation本地体验永不因云端失败而损坏。与其他模块的关系utils/diff.tsgetPatchForEdit、getPatchForDisplay、adjustHunkLineNumbers、CONTEXT_LINES是 FileEdit/FileWrite 共用的 diff 生成基础设施。utils/readEditContext.tsreadEditContext、openForScan、readCapped提供有界读取能力是拒绝 diff 不 OOM 的根本保障。utils/fileStateCache.tsFileReadTool 的file_unchanged状态依赖此缓存避免重复读未变更文件。bridge 模块getBridgeAccessToken、getBridgeBaseUrlOverride给 BriefTool upload 提供 OAuth token 与 base URL与bridgeConfig.ts、bridgeEnabled.ts协作。constants/oauth.tsgetOauthConfig()提供生产/ staging 切换BriefTool upload 在 base URL 选择上有特殊优先级ANTHROPIC_BASE_URLgetOauthConfig().BASE_API_URL。components/FileEditToolUpdatedMessage、FileEditToolUseRejectedMessageFileEdit/FileWrite 共用同一套 diff 渲染组件保持视觉一致。utils/plans.tsgetPlansDirectory()让所有文件工具都能识别 plan 文件并触发特殊渲染路径。小结文件工具族展示了 Claude Code 工具系统在统一接口下的多样性FileEditTool 专注精确字符串替换并处理 hashline 多点编辑FileWriteTool 区分 create/update 双形态并对 plan 文件做反向渲染FileReadTool 同时处理文本/图片/PDF/notebook/agent 输出五类形态BriefTool 是消息通道而非文件工具但其附件上传机制与文件系统紧密耦合。它们的共同点是用输入字段语义反推操作意图、用 Suspense 懒加载避免 OOM、用 dimColor/error 颜色编码状态严重性。这些模式构成了文件类工具的设计范式也为后续 MCPTool、AgentTool 等复杂工具的 UI 实现提供了参考。