Appearance
功能规格说明:Artifact 工具
创建日期:2026-08-12
对齐 Claude Code 的内建 Artifact 工具:把本地
.html/.md文件发布为默认私有的可分享网页(claude.ai 风格),并通过 WebFetch 拦截读取已发布的 artifact 页面。 服务端契约已落地(codechat 自托管同源实现):POST /api/frame/deploy/direct(发布)、GET /api/frame/{slug}?via=model_read(元数据)、GET /api/frame/{slug}/content?v={version}(正文,同源 + Bearer 鉴权,无独立域名/assetToken 流程)。 已拍板的简化决定:零新增配置(API 端点复用 Server URL origin:options.serverUrl > WAVE_SERVER_URL > 默认值,不新增 baseUrl 配置项);客户端只实现 inline 直传一条路径(无 signed URL / DIRECT_UPLOAD);无 AUTO_OPEN / FRAME_TIMING / OWNERSHIP_FRAME 遥测;启用开关enableArtifact(未设置时跟随代码默认值常量,当前默认禁用——后端未上线先不发功能,内测/灰度通过enableArtifact: true显式打开;后端上线后翻转默认值常量为启用)。disableArtifactopt-out 开关等 GA 后再对齐 CC,本期不实现。 触发方式定案(双通道并存,2026-08-13):模型经自然语言自动调用Artifact工具(description 覆盖"发布/分享/做成网页/给链接"语义,中文提示词同样触发)+ 内置技能/artifact人工斜杠触发(builtin SKILL.md,disable-model-invocation: true仅人工、模型不可经 Skill 工具调用该技能)。用户在输入框输入/即可在技能列表看到该命令并一键触发,无需知道怎么写提示词。技能本身不含任何发布逻辑——其内容仅指示模型调用Artifact工具(参数经$ARGUMENTS/$1透传),发布/校验/权限确认/会话映射全部由工具完成,技能不绕过也不复制这些逻辑。 范围:wave-agent 客户端侧工具 + WebFetch 拦截。分享管理(POST /api/frame/{slug}/share、pinned_version)由服务端/网页外壳承担,客户端仅发布私有页面并探测分享状态。 对齐 CC 的 Artifact 工具形态(2026-09-11 增补):工具入口统一为带action参数的单一工具——action: "publish"(省略时的默认值,即现有发布行为)与action: "read"(新增读取动作)。read的返回形态对齐 CC:读取当前用户拥有的 artifact 返回原文 HTML(含内联 CSS/JS);读取他人分享的 artifact 返回隔离摘要(可选prompt指明关注点),不把他人页面全文放进上下文。 本期只补read:CC 的list/watch/status/upload_asset/list_assets/read_asset/delete_asset/list_types等动作依赖平台提供枚举、订阅、资源库、模板等能力,codechat 平台暂无对应接口,本期不做;将来平台补齐后再逐个对齐。 读取实现单一化(2026-09-11):artifact 正文的取用(元数据探测 + Bearer 鉴权 + 正文拉取 + 大内容落盘)收敛为唯一实现,Artifact工具的read动作与 WebFetch 的 artifact URL 拦截共用,不再各写一套。 发布标题与短名对齐 CC(2026-09-18 修正,取代 2026-09-11 的「label当标题兜底」口径):标题与短名是两件独立的事,各由一个参数承载。
title(可选,仅.html生效,≤1000 字符):artifact 的标题(浏览器标签 / 画廊显示名);超过 1000 字符客户端先报错不发请求(服务端TITLE_MAX同值校验)。服务端解析阶梯 = 页面前 8KB 内的<title>>title参数 > 默认名——标签永不被参数覆盖(客户端不自行判优先级,也不做冲突提示);客户端按 CC 的末级兜底补文件名 basename,因此.html发布总会发一个非空title,标题链才闭合。label(可选,≤60 字符):本次发布的短名(如 "Draft to legal"),只用于版本列表/版本选择器,不参与标题解析——工具不再拿它当标题兜底。- Markdown 保持文件名身份:客户端渲染时把文件名注入
<title>(不是label),title参数对.md不生效,.md页面因此以文件名作标题。
用户场景与测试 (必填)
用户故事:发布 HTML/Markdown 为 artifact 网页(优先级:P1)
作为用户,我希望把本地已写好的 .html/.md 文件发布为一个默认私有的可分享网页并拿到 URL,以便把工作成果分享给团队成员。
为什么是这个优先级:这是 Artifact 功能的核心价值。
独立测试:可以调用 Artifact 工具(参数 file_path + favicon)并 mock 服务端 201 响应,验证工具返回 { url, path, title, version }。
验收场景:
- 假设 model 调用
Artifact工具且参数为file_path: "a.html"、favicon: "📄",文件已存在于磁盘,当 发布请求返回 201 时,则 工具返回{ url, path, title, version },其中url形如{host}/code/artifact/{slug}。 - 假设
file_path指向磁盘上不存在的文件,当 工具执行时,则 返回success: false与明确的错误消息(提示先 Write/Edit 落盘,不接受内联 content)。 - 假设
file_path扩展名不是.html或.md,当 工具执行时,则 返回success: false与扩展名受限的错误。 - 假设
file_path是.md文件,当 工具执行时,则 客户端先将其渲染为完整 HTML 再上传(服务端只接收完整 HTML)。 - 假设
favicon包含非 emoji 字符(如文字、URL、HTML markup),当 工具执行时,则 返回success: false与错误提示。 - 假设 发布内容超过 16MB(服务端返回 413),当 工具执行时,则 返回
success: false与大小超限的错误。 - 假设 客户端未登录(无有效 token),当 工具执行时,则 返回鉴权错误并提示先登录。
- 假设 model 调用
Artifact工具时省略action(或显式传action: "publish"),当 工具执行时,则 按发布处理,上述校验/确认/冲突防护全部生效(缺省动作即发布,与既有行为一致)。 - 假设 model 调用
Artifact工具发布一个没有<title>的.html文件且未提供title,当 工具执行时,则 请求体带title= 文件名(不含扩展名)(CC 标题链的末级兜底),服务端按「页面<title>>title参数 > 默认名」解析后标题不会落到Untitled artifact。 - 假设 model 调用
Artifact工具发布.html且显式传了title,当 工具执行时,则 该值随请求发送;若页面自带<title>,仍由服务端判定标签优先(客户端不自行判优先级、不做冲突提示)。 - 假设 model 调用
Artifact工具发布.md文件,当 工具执行时,则 按 CC 保持文件名身份:客户端渲染时把文件名注入<title>,且不发送title参数(title仅对.html生效);显式给出的label只作为本次发布的短名随请求发送。 - 假设 model 显式提供
label,当 工具执行时,则label作为「本次发布的短名」原样发送(超过 60 字符返回success: false与错误提示),且不参与标题解析——无论.html还是.md,标题都不再取决于label。
用户故事:内置技能 /artifact 人工触发(优先级:P1)
作为用户,我希望在不知道如何用自然语言描述发布需求时,通过在输入框输入 / 看到并选择 artifact 命令来触发发布,以便一键使用而不依赖提示词技巧。
为什么是这个优先级:技能是"不会写提示词"用户的入口,与模型自动调用工具构成双通道,均为发布核心路径。技能只是给用户的快捷入口:其内容仅指示模型调用 Artifact 工具,不含任何发布逻辑——发布、校验、权限确认、会话映射全部由工具完成。
独立测试:enableArtifact 开启时断言 getSlashCommands() 包含 artifact 技能命令(描述带 Skill: 前缀、归入 popup 技能分组);模型经 Skill 工具调用 artifact 技能被拒绝(disable-model-invocation)。
验收场景:
- 假设
enableArtifact已开启,当 用户在输入框输入/时,则 popup 技能列表显示artifact命令(描述形如"发布本地 HTML/Markdown 为可分享网页"),用户选择即可触发。 - 假设 用户输入
/artifact(无参数),当 命令执行时,则 技能内容注入主 agent(内容仅指示调用Artifact工具),agent 从会话上下文推断要发布的文件(不明确时先询问用户),随后调用Artifact工具发布并返回 URL。 - 假设 用户输入
/artifact <file_path>(带参数),当 命令执行时,则 文件路径作为参数($ARGUMENTS/$1语义)透传给技能内容,agent 直接以该路径为file_path调用Artifact工具,不再询问。 - 假设 模型试图通过
Skill工具调用 artifact 技能,当 调用时,则 返回 "not available for model invocation"(disable-model-invocation: true);模型的发布入口只有Artifact工具,两通道不重叠。 - 假设 用户经
/artifact触发发布,当Artifact工具执行时,则 与自然语言路径走完全相同的工具调用:文件存在性/扩展名/大小校验、权限确认(首次)与同会话自动允许(重复发布)、409 冲突与 stale_version_guard 全部生效——技能不含任何绕过或复制这些逻辑的实现。 - 假设
enableArtifact未开启(默认禁用),当 会话初始化时,则 artifact 技能不注册,popup 不显示/artifact(与工具注册同 gate)。 - 假设 运行中的会话中
enableArtifact由禁用热重载为启用,当 配置重载时,则/artifact技能命令即时注册、popup 可见;反向关闭时即时注销(与工具注册同 gate)。
用户故事:WebFetch 读取 artifact 页面(优先级:P1)
作为用户,我希望 WebFetch 能识别 artifact URL 并走专用通道读取发布内容,以便 AI 可以基于 artifact 内容回答、排查或继续迭代。
为什么是这个优先级:发布与读取构成完整闭环,也是冲突防护(stale_version_guard)的基础。
验收场景:
- 假设 WebFetch 的
url形如{host}/code/artifact/{slug}(匹配 artifact URL 格式),当 工具执行时,则 走专用读取通道:先GET /api/frame/{slug}?via=model_read取元数据,再拉取正文,返回页面内容;该取用逻辑与Artifact工具read动作共用同一份实现(元数据探测、Bearer 鉴权、正文拉取、大内容落盘不重复实现)。 - 假设 artifact 读取成功,当 WebFetch 返回结果时,则 输出 schema 附带可选
artifactRead: { slug, ver }元数据(ver为当前版本号),且会话内记录的版本号同步更新。 - 假设 artifact HTML 内容较大(超过 ~2KB),当 WebFetch 执行时,则 完整内容落盘到临时文件,返回文件路径 + head 截断预览,避免工具结果过大。
- 假设 artifact 不存在或已删除(服务端 404),当 WebFetch 执行时,则 返回
success: false与对应的错误消息。 - 假设 读取接口返回的
contentUrl需要鉴权,当 WebFetch 拉取正文时,则 携带当前登录 token(Bearer)请求。 - 假设 WebFetch 读取的是他人分享的 artifact,当 返回结果时,则 仍是围绕
prompt的小模型答案(小模型看到内容、主模型只看到答案),他人页面全文不进入主对话上下文。
用户故事:读取已发布 artifact 的原文(优先级:P1)
作为用户,我希望让 AI 直接读取某个已发布 artifact 的原文 HTML(而不是被转成 markdown 的二手文本,也不是被小模型概括过的摘要),以便在真实的 HTML/CSS/JS 上继续修改页面、排查样式或渲染问题。
为什么是这个优先级:读取与发布构成完整闭环;当前唯一的读取通道是 WebFetch,它会把 HTML 转成 markdown 后交给小模型,标签结构、class 与元素的对应关系全部丢失,无法支撑"改页面/查样式"这类需求。
独立测试:mock GET /api/frame/{slug}?via=model_read 返回自有 artifact 元数据与 content 端点返回的 HTML,调用 Artifact 工具(action: "read" + url)并断言返回原文 HTML;另一个用例 mock 他人分享的 artifact 并断言走摘要路径。
验收场景:
- 假设 model 调用
Artifact工具且action: "read"、url形如{host}/code/artifact/{slug}且当前用户是该 artifact 的拥有者,当 读取成功时,则 返回该版本的原始 HTML(含内联 CSS/JS),并给出 artifact 的版本信息。 - 假设 读取到的原文超过落盘阈值(约 2KB),当 工具返回时,则 完整原文写入本地文件,工具结果给出文件路径与开头预览(提示用 Read 查看全文),避免工具结果膨胀。
- 假设
url指向的是他人分享给当前用户的 artifact,当 读取时,则 内容以隔离摘要形式返回(调用方给了prompt时摘要围绕该关注点组织),他人页面的全文不进入对话上下文。 - 假设
url不是 artifact URL(slug 无法解析),当 工具执行时,则 返回success: false与"不是可读取的 artifact URL"错误。 - 假设 artifact 不存在或已删除(服务端 404),当 工具执行时,则 返回
success: false与"artifact 不存在"错误。 - 假设 当前用户无权读取该 artifact(服务端 403),当 工具执行时,则 返回
success: false与"无权限"错误(与"不存在"区分开)。 - 假设 客户端未登录(无有效 token),当 工具执行时,则 返回鉴权错误并提示先登录。
- 假设 工具结果包含 artifact 版本号,当 读取成功后同会话再发布同一 artifact 时,则 会话内记录的版本号已更新为读取到的最新版本,stale_version_guard 不误报冲突。
- 假设 首次读取他人分享的 artifact,当 调用工具时,则 触发权限确认(该页面内容将进入对话上下文);用户同意后,同一 artifact 在本会话内的后续读取不再重复确认。
- 假设 读取当前用户自己拥有的 artifact,当 调用工具时,则 免确认(只读动作,内容本来就在用户的控制范围内)。
- 假设
enableArtifact未开启,当 model 调用Artifact工具时,则 工具不注册、不可调用(与发布同一 gate),WebFetch 的 artifact URL 拦截同样失效。
用户故事:重新部署与并发冲突防护(优先级:P2)
作为系统,我希望同一 artifact 的并发发布受版本保护,以便多会话协作时不发生静默覆盖。
为什么是这个优先级:多会话同时发布同一 slug 是真实协作场景,409 + stale_version_guard 是 CC 对齐的关键行为。
验收场景:
- 假设 model 调用
Artifact工具且带url参数(已有 artifact 的 URL),当 发布时,则 使用该 slug 重新部署,返回包含新version的结果。 - 假设 重部署时服务端返回 409(
{ conflict: true, live: "<最新版本号>" },他人已发布新版),当 工具执行时,则 返回success: false,错误信息包含live版本号并提示先 WebFetch 最新内容、和解后重新发布。 - 假设 冲突时 model 带
force: true重发,当 工具执行时,则 跳过冲突检查直接覆盖发布。 - 假设 同会话内 model 未先 WebFetch 最新版本就重发同一 artifact(stale_version_guard:本地记录的版本落后于服务端),当 工具执行时,则 返回
success: false阻止发布,除非带force: true。 - 假设 服务端 409 响应携带
live版本号,当 冲突错误返回后,则 客户端用live作为下一次发布的baseVersion重试(供服务端做并发检测)。
用户故事:默认私有与分享状态探测(优先级:P2)
作为用户,我希望发布出的页面默认只有我能看到,并且读取时能感知页面的分享状态,以便安全地决定是否传播 URL。
为什么是这个优先级:默认私有是 CC 的默认行为,分享状态探测决定发布确认文案与重发布行为。
验收场景:
- 假设 model 首次发布新 artifact 且未指定分享方式,当 发布完成时,则 页面默认为私有(服务端
share_mode=owner)。 - 假设 WebFetch 读取 artifact 元数据,当 返回结果时,则 元数据包含
perm: { mode, role }(mode: owner/users/org;role: owner/reader),用于探测当前分享状态。 - 假设 已分享为 shared-live(
shared字段为空,读者实时看到更新)的 artifact 被重发布,当 工具执行时,则 发布确认中提示影响读者可见版本,需用户确认(对齐 CC 行为)。
用户故事:发布确认与同会话自动允许(优先级:P2)
作为用户,我希望发布动作默认经过确认、但同会话内的重复发布不再打扰,以便既不误发又保持流畅。
为什么是这个优先级:发布是外发动作需确认;同会话重发是常见迭代循环,频繁确认会打断工作流。
验收场景:
- 假设 model 首次发布某文件,当 调用
Artifact工具时,则 触发权限确认(文案形如 "publish "<file>" to a private page"),用户拒绝则取消发布。 - 假设 同会话内 model 再次发布本会话已发布过的文件(
url省略,靠会话内 file_path → artifact URL 映射),当 调用Artifact工具时,则 自动允许,不再弹确认。 - 假设 会话内映射不存在(本会话未发布过该文件)且用户未配置自动允许,当 调用
Artifact工具时,则 仍弹确认。
用户故事:启用开关与默认禁用(优先级:P2)
作为管理员或内测用户,我希望 Artifact 功能默认不可用、但可显式开启,以便在后端上线前不暴露无效工具,同时支持内测/灰度先行体验。
为什么是这个优先级:当前 frame 后端尚未上线,功能需默认禁用(不注册工具、不拦截读取);内测/灰度通过 enableArtifact: true 显式打开(无需改代码);后端上线后把代码默认值常量翻转为启用(未设置 = 启用,对齐 CC enableArtifact 的"未设置跟随功能可用性"语义)。开关支持热更新:运行中的会话修改 settings.json 后,配置重载会即时重评估工具注册(无需重启会话)。
验收场景:
- 假设 未配置
enableArtifact(默认状态,后端上线前),当 会话初始化时,则Artifact工具不注册、不可调用,/artifact技能命令同样不注册、popup 不显示。 - 假设 settings.json 配置
enableArtifact: true,当 会话初始化时,则Artifact工具注册、可调用,/artifact技能命令同步注册、popup 可见(内测/灰度入口)。 - 假设 后端已上线、代码默认值常量已翻转为启用,当 会话初始化时,则 未配置
enableArtifact也默认启用(工具与技能命令均注册)。 - 假设 Artifact 被禁用(默认或显式),当 WebFetch 收到 artifact URL 时,则 不进入专用读取通道(按普通 URL 处理或报错),不执行
via=model_read调用。 - 假设 运行中的会话未配置
enableArtifact(工具未注册),当 用户在 settings.json 中改为enableArtifact: true触发配置热重载时,则Artifact工具即时注册、可调用,/artifact技能命令同步注册、popup 可见,无需重启会话。 - 假设 运行中的会话已启用
enableArtifact,当 用户改为false触发配置热重载时,则Artifact工具即时注销、不可调用,/artifact技能命令同步注销、popup 隐藏,同时 WebFetch 的 artifact URL 拦截(逐调用检查isArtifactEnabled)同步失效。 - 假设 服务端
GET /api/wave/settings下发了enableArtifact: true(remote settings),当 会话初始化或轮询(60min + 304 checksum)检测到变更时,则 remote 值优先于本地 settings.json 与代码默认值,Artifact工具按 remote 值注册,/artifact技能命令按同 gate 注册,WebFetch 拦截同样生效(管理员远程灰度/回滚入口)。 - 假设 服务端下发的 remote
enableArtifact与本地 settings.json 冲突,当 合并配置时,则 remote 胜出(last-write-wins,与model、permissions.defaultMode等 managed 字段语义一致),工具与技能命令均按 remote 值注册/注销;未下发时回退本地/默认值。
非功能需求
- 零新增配置:API 端点相对 Server URL origin 硬编码(
options.serverUrl > WAVE_SERVER_URL > 默认值,经 authService.getServerUrl() 获取),不新增 baseUrl/artifactUrl 配置项。 - 双通道不重叠:
Artifact工具保留模型自动调用(自然语言触发);内置技能/artifact(builtin SKILL.md,disable-model-invocation: true)仅人工斜杠触发。技能仅指示模型调用Artifact工具,不含任何发布逻辑,与自然语言路径走完全相同的工具调用;技能注册与工具注册同 gate(isArtifactEnabled),禁用时两者都不暴露。 - 鉴权:发布(deploy/direct)与读取(model_read、contentUrl)请求均携带当前登录 token(Bearer);未登录返回明确错误。
- 大小上限:发布内容上限 16MB(413 透传为友好错误)。
- 文件大小策略:读取时 >~2KB 的 HTML 落盘到临时文件(返回路径 + head 预览),避免工具结果膨胀。
- 归属判定:以服务端元数据判定当前用户对该 artifact 的角色(拥有者 / 读者)。拥有者返回原文 HTML;读者(他人分享)与无法确认归属的情况一律走摘要,不返回全文。
- 摘要实现:读者视角的摘要复用 WebFetch 已有的小模型处理路径(同一份 prompt→答案机制),不新增模型调用通道。
- 读取实现单一化:artifact 的元数据探测、Bearer 鉴权、正文拉取、大内容落盘只有一份实现,
Artifact工具的read动作与 WebFetch 的 artifact URL 拦截共同调用;不得出现两套并行逻辑。 - 工具描述:
Artifact工具的 description 需同时覆盖发布与读取两类意图("发布/分享/做成网页/给链接" 与 "读取/查看/看下这个链接里的内容"),并说明缺省动作是发布。 - 只读性:WebFetch 侧读取行为保持只读,不修改 artifact 内容。
- 会话映射:会话内维护 file_path → artifact URL 映射,用于同会话重发免
url参数与 stale_version_guard。 - 测试:SDK 层 mock 服务端(201/409/404/413)覆盖发布、重部署、冲突、读取、禁用开关场景。
边界情况
- md → HTML 渲染失败怎么办? 渲染失败时返回
success: false与渲染错误信息,不发起上传。 - 未登录时发布/读取怎么办? 返回鉴权错误并提示先登录(
/login)。 - artifact URL 的主机与 Server URL 不一致? 自托管场景下发布返回的 URL 即当前 Server URL origin 下的
/code/artifact/{slug};WebFetch 按 URL 路径格式{host}/code/artifact/{slug}识别,读取请求发往同一 origin。 - 并发发布同一 slug(跨会话)怎么办? 服务端 409 +
live版本号;客户端透传错误并提示先 WebFetch 最新内容,或带force: true覆盖。 - 大文件读取的临时文件何时清理? 沿用现有工具临时文件生命周期管理,不引入独立清理机制。
- 与 disallowedTools 的关系?
enableArtifact是独立功能开关(未设置跟随默认值常量);disallowedTools 对 Artifact 工具的显式禁用仍生效(两者取并集)。 - 读到的内容比会话内记录的版本新怎么办? 以读取到的版本号覆盖会话内记录(读取即"已看到最新版本"),随后同会话重发布不再因 stale_version_guard 被拦。
- 读他人 artifact 与 plan 模式? 读他人 artifact 需用户确认(内容进入上下文、且是第三方内容);plan 模式下没有可交互的确认面时不自动放行,保持规划状态并提示用户。
- enableArtifact 关闭时读动作? 与发布同 gate:工具整体不注册;WebFetch 的 artifact URL 拦截同步失效(退化为普通 URL 处理)。
- Artifact 工具是受限工具吗? 是——发布是外发网络动作,需加入 RESTRICTED_TOOLS 以触发默认模式的确认流程。