docs/design/daemon-session-artifacts/session-artifacts-daemon-api-implementation-design.md
输入资料:session artifacts daemon API 初版草案与 artifact design v1 草案。
源码基线:当前 qwen-code 代码。
目标:基于现有 Daemon / ACP / SSE / SDK / hooks / extension 能力,设计一套可实施、可验证、边界清楚的 session artifacts API。
建议把 artifact 定义为:
Session 中被显式登记的、用户可复用/点击/预览/下载/分享的结构化产物引用。普通源码变更不是 artifact;源码变更属于 file change / diff / patch history。
这个定义覆盖文件,也覆盖非文件 URL。关键不在于它是不是物理文件,而在于它是不是被系统明确声明为“产物”。Artifacts 面板应该展示 session outputs,而不是所有 agent 动过的东西。
V1 完整能力建议包含:
session_artifactsGET /session/:id/artifactsartifact_changedToolResult.artifacts?: ToolArtifact[]ArtifactTool structured artifact metadataSessionArtifactStoreDaemonClient.listSessionArtifacts()、DaemonSessionClient.artifacts()record_artifacthookSpecificOutput.artifactsPOST /session/:id/artifactsDELETE /session/:id/artifacts/:artifactIdDaemonSessionClient.addArtifact()DaemonSessionClient.removeArtifact()为了保持 V1 可控,不建议 V1 做:
WRITE_FILE / EDIT / NOTEBOOK_EDIT 自动进入 artifacts算,但必须是“声明式 link artifact”。
例如这些应该算 artifact:
这些不应该默认算 artifact:
核心标准:
| 类型 | 是否进入 artifacts | 原因 |
|---|---|---|
| 普通源码编辑 | 否 | 属于 file change / diff,不是可复用产物 |
| 明确登记的生成型 workspace 文件 | 是 | report / HTML / PDF / image 等可复用输出 |
| ArtifactTool 发布的 HTML URL | 是 | 工具明确发布 |
| skill 按规则拼出的业务详情 URL | 是,但必须显式登记 | 用户需要右侧长期可点 |
| assistant 回答里的普通参考链接 | 否 | 噪音大、容易误报 |
| shell stdout 中出现的 URL | 否 | 语义不可靠 |
| web_fetch 请求过的 URL | 否 | 这是输入/来源,不是产物 |
Link artifact 不是“网页内容”,而是“资源入口”。它应该在右侧产物区表现为可点击条目:
用户画像资源详情internal data platform / prodlinkplatform.example.comToolResult.artifacts / ArtifactTool / record_artifact / hook / clientClient 点击时打开 URL;Daemon 不读取、不验证、不预渲染该 URL。
相关源码:
packages/cli/src/serve/server.tspackages/cli/src/serve/capabilities.tsdocs/developers/qwen-serve-protocol.md现状:
/capabilities 返回 features,Client 必须基于 feature gate UI。GET /session/:id/statusGET /session/:id/contextGET /session/:id/tasksGET /session/:id/eventsSERVE_CAPABILITY_REGISTRY。设计:
session_artifactsGET /session/:id/artifactsPOST /session/:id/artifacts相关源码:
packages/acp-bridge/src/eventBus.tspackages/acp-bridge/src/bridge.tspackages/acp-bridge/src/bridgeClient.tspackages/sdk-typescript/src/daemon/events.ts现状:
EventBus。Last-Event-ID、backpressure。设计:
/session/:id/events。artifact_changed相关源码:
packages/core/src/tools/tools.tspackages/core/src/tools/tool-names.tspackages/core/src/tools/artifact/artifact-tool.tspackages/cli/src/acp-integration/session/Session.tspackages/cli/src/acp-integration/session/emitters/ToolCallEmitter.ts现状:
ToolResult 当前包含 llmContent、returnDisplay、resultFilePaths?、error?。ArtifactTool 已能发布 HTML 并返回 URL,但没有结构化 artifact metadata。ToolCallEmitter.emitResult() 的 _meta 已有扩展位。设计:
ToolResult.artifacts?: ToolArtifact[]。ArtifactTool 成功时填充 artifacts。ToolCallEmitter.emitResult() 把 artifacts 放入 _meta.artifacts。_meta.artifacts,写入 session artifact store。相关源码:
packages/core/src/hooks/types.tspackages/core/src/core/toolHookTriggers.tspackages/core/src/hooks/hookRunner.tspackages/core/src/hooks/sessionHooksManager.tspackages/core/src/hooks/registerSkillHooks.tspackages/core/src/extension/extensionManager.tsdocs/developers/channel-plugins.md当前已有能力:
PreToolUse、PostToolUse、PostToolBatch、SessionStart、Stop、SubagentStart、SubagentStop 等。HookOutput。HookOutput。SessionHooksManager 运行时注册。当前缺口:
additionalContext、decision、stopReason 等通用字段。hookSpecificOutput.artifacts。GET /workspace/hooks 和 GET /session/:id/hooks 状态接口,没有“hook 主动注入 artifact”的 route。结论:
新增:
"session_artifacts"
Client 只有看到该 feature 才展示 artifacts 面板和调用相关 API。
GET /session/:id/artifacts
响应:
{
"v": 1,
"sessionId": "session-123",
"artifacts": [
{
"id": "a1b2c3d4e5f6",
"kind": "link",
"storage": "external_url",
"title": "用户画像资源详情",
"description": "内部数据平台资源详情页",
"url": "https://platform.example.com/resources/user-profile",
"mimeType": "text/html",
"status": "available",
"source": "tool",
"toolCallId": "call_abc",
"toolName": "artifact",
"createdAt": "2026-06-26T10:00:00.000Z",
"updatedAt": "2026-06-26T10:00:00.000Z",
"metadata": {
"resourceType": "data_platform_resource",
"env": "prod"
}
}
]
}
通过现有:
GET /session/:id/events
新增 event:
{
"v": 1,
"type": "artifact_changed",
"data": {
"sessionId": "session-123",
"change": {
"action": "created",
"artifactId": "a1b2c3d4e5f6",
"artifact": {
"id": "a1b2c3d4e5f6",
"kind": "link",
"storage": "external_url",
"title": "用户画像资源详情",
"description": "内部数据平台资源详情页",
"url": "https://platform.example.com/resources/user-profile",
"mimeType": "text/html",
"status": "available",
"source": "tool",
"toolCallId": "call_abc",
"toolName": "artifact",
"createdAt": "2026-06-26T10:00:00.000Z",
"updatedAt": "2026-06-26T10:00:00.000Z",
"metadata": {
"resourceType": "data_platform_resource",
"env": "prod"
}
}
}
}
}
change.action:
createdupdatedremovedV1 主要产生 created / updated;eviction 或显式删除场景产生 removed。
artifact_changed.data.change.artifact 在 created / updated / removed 时携带完整 DaemonSessionArtifact,shape 与 GET /session/:id/artifacts 中的单项一致;removed event 携带被删除前的最后完整 artifact。removed 必须携带 reason,V1 取值为 eviction 或 explicit。这样实时 UI 可以直接应用 event,不需要每条 event 后再 GET。Client 断线、丢 event 或收到未知 event type 时,再用 GET /session/:id/artifacts 做 snapshot sync。
作为 V1 的 client 显式登记入口:
POST /session/:id/artifacts
用途:
请求:
{
"kind": "link",
"storage": "external_url",
"title": "任务详情",
"description": "调度任务 task_123 的详情页",
"url": "https://ops.example.com/tasks/task_123",
"mimeType": "text/html",
"metadata": {
"resourceType": "scheduler_task"
}
}
响应:
{
"v": 1,
"sessionId": "session-123",
"changes": [
{
"action": "created",
"artifactId": "a1b2c3d4e5f6",
"artifact": {
"id": "a1b2c3d4e5f6",
"kind": "link",
"storage": "external_url",
"title": "任务详情",
"description": "调度任务 task_123 的详情页",
"url": "https://ops.example.com/tasks/task_123",
"mimeType": "text/html",
"status": "available",
"source": "client",
"createdAt": "2026-06-26T10:00:00.000Z",
"updatedAt": "2026-06-26T10:00:00.000Z",
"metadata": {
"resourceType": "scheduler_task"
}
}
}
]
}
changes 中的每一项都必须同步发布为一条 artifact_changed SSE event。这样即使一次 POST 触发 upsert 和 eviction,client 也能收到 created/updated 以及 removed 的完整增量。同一次 mutation 内如果多个输入归一到同一个 identity,只能在 changes 中产生一条最终 change。事件发布顺序是协议约束:先按 changes[] 中的顺序发布 created / updated,再发布 removed,避免 client 的本地镜像短暂进入服务端从未存在过的状态。
错误响应:
{
"v": 1,
"error": {
"code": "VALIDATION_FAILED",
"message": "url must use http or https",
"field": "url"
}
}
状态码:
400 VALIDATION_FAILED:字段校验失败,例如多 primary locator、不支持 URL scheme、metadata 超限。401 UNAUTHORIZED / 403 FORBIDDEN:mutation gate 或 bearer token 校验失败。404 SESSION_NOT_FOUND:session 不存在。作为 V1 的显式移除入口:
DELETE /session/:id/artifacts/:artifactId
语义:
DaemonSessionArtifactMutationResult,其中包含一条 action: 'removed'、reason: 'explicit' 的 change。200 和空 changes: [],不发布 SSE event。artifact_changed SSE event。错误响应复用 Section 4.4 envelope;session 不存在仍返回 404 SESSION_NOT_FOUND。
安全:
V1 合并后应作为一项完整的 session artifact 管理能力发布试用,而不是只发布一个半成品接口。完整能力的最小闭环是:
session_artifacts capability 探测功能。GET /session/:id/artifacts snapshot。artifact_changed 增量。ArtifactTool / ToolResult.artifacts、record_artifact、hook artifacts、client POST 四类入口都进入同一个 store。artifact_changed event。建议以 experimental/capability-gated 形式先发布试用。这里的 experimental 表示实现和 UI 可以继续打磨,不表示协议可以随意破坏:已经暴露给 client 的字段和事件语义必须按下列兼容性规则演进。
非 breaking 的后续扩展:
kind / status / source / storage 字面量,但 typed SDK 必须把这些字段声明成 open union,client 必须容忍未知值:未知 kind 按 other,未知 status 显示为 unknown 状态且不阻断列表展示,未知 source 按未分组来源,未知 storage 仅按可用 url / workspacePath 做保守展示。GET /session/:id/artifacts/:artifactId、preview route、pin route。artifact_changed 语义不变。session_artifacts_preview、session_artifacts_persistence。需要新 capability 或新版本的 breaking 变更:
artifact_changed.data.change.action 的 created / updated / removed 语义。GET /session/:id/artifacts 的 envelope shape。type OpenStringUnion<T extends string> = T | (string & {});
export type DaemonSessionArtifactKind = OpenStringUnion<
| 'file'
| 'link'
| 'image'
| 'video'
| 'audio'
| 'html'
| 'pdf'
| 'notebook'
| 'other'
>;
export type DaemonSessionArtifactStatus = OpenStringUnion<
'available' | 'missing'
>;
export type DaemonSessionArtifactSource = OpenStringUnion<
'tool' | 'hook' | 'client'
>;
export type DaemonSessionArtifactStorage = OpenStringUnion<
'workspace' | 'managed' | 'external_url' | 'published'
>;
export interface DaemonSessionArtifact {
id: string;
kind: DaemonSessionArtifactKind;
storage: DaemonSessionArtifactStorage;
title: string;
description?: string;
status: DaemonSessionArtifactStatus;
source: DaemonSessionArtifactSource;
createdAt: string;
updatedAt: string;
workspacePath?: string;
managedId?: string;
url?: string;
mimeType?: string;
sizeBytes?: number;
toolCallId?: string;
toolName?: string;
hookName?: string;
extensionId?: string;
clientId?: string;
metadata?: Record<string, string | number | boolean | null>;
}
export interface DaemonSessionArtifactsEnvelope {
v: 1;
sessionId: string;
artifacts: DaemonSessionArtifact[];
}
export interface DaemonArtifactChangedData {
sessionId: string;
change: DaemonSessionArtifactChange;
}
export interface DaemonSessionArtifactChange {
action: 'created' | 'updated' | 'removed';
artifactId: string;
artifact?: DaemonSessionArtifact;
reason?: 'eviction' | 'explicit';
}
export interface DaemonSessionArtifactMutationResult {
v: 1;
sessionId: string;
changes: DaemonSessionArtifactChange[];
}
export type ToolArtifactKind =
| 'file'
| 'link'
| 'image'
| 'video'
| 'audio'
| 'html'
| 'pdf'
| 'notebook'
| 'other';
export type ToolArtifactStorage =
| 'workspace'
| 'managed'
| 'external_url'
| 'published';
export interface ToolArtifact {
kind?: ToolArtifactKind;
storage?: ToolArtifactStorage;
title: string;
description?: string;
workspacePath?: string;
managedId?: string;
url?: string;
mimeType?: string;
metadata?: Record<string, string | number | boolean | null>;
}
ToolArtifactKind / ToolArtifactStorage 的已知字面量集合必须只有一个实现来源,避免 core、acp-bridge、SDK 三处手工漂移。推荐做法:
TOOL_ARTIFACT_KINDS / TOOL_ARTIFACT_STORAGES const tuple,并导出 ToolArtifactKind / ToolArtifactStorage。.d.ts re-export 已知字面量,再在 response-facing 类型上包一层 open union,以便容忍未来 daemon 返回的新值。并扩展:
export interface ToolResult {
llmContent: unknown;
returnDisplay: unknown;
resultFilePaths?: string[];
artifacts?: ToolArtifact[];
error?: unknown;
}
ToolArtifact 是工具返回的输入形态,SessionArtifactInput 是所有入口进入 store 前的统一内部输入形态,DaemonSessionArtifact 是对外返回形态。所有入口都必须先转换为 SessionArtifactInput,再由 SessionArtifactStore 补全公共字段。
export interface SessionArtifactInput extends ToolArtifact {
source: 'tool' | 'hook' | 'client';
toolCallId?: string;
toolName?: string;
hookName?: string;
extensionId?: string;
clientId?: string;
trustedPublisher?: true;
receivedSeq?: number;
}
trustedPublisher 是 bridge/store 内部输入标志,不是 public schema 或 client/hook 可设置字段。V1 的 daemon/ACP 部署里,qwen --acp 子进程由 daemon 启动并以同一用户运行;因此当前实现把 completed ArtifactTool session update(tool_call_update、status: 'completed'、_meta.toolName: 'artifact')视作唯一 trusted publisher 信号。这个信号不从 artifact payload 本身读取,也不对 client POST、hook notification、record_artifact 或其它 tool result 开放。
如果未来支持远端 sandbox、多方 ACP participant 或非同信任域 agent,应新增不可伪造的 transport / in-process publisher identity,再替换该 V1 信任信号;在那之前不要把 payload 内的 trustedPublisher / source / storage 当作授权依据。
来源转换规则:
ArtifactTool / daemon publisher:BridgeClient 只在 completed ArtifactTool session update 上补 source: 'tool'、toolCallId、toolName,并以内部 option 设置 trustedPublisher: true。ToolResult.artifacts:复制 ToolArtifact 字段,补 source: 'tool'、toolCallId、toolName,但不设置 trustedPublisher。record_artifact:作为 tool source 进入,同样补 source: 'tool'、toolCallId、toolName: 'record_artifact',但不允许 storage: 'published',也不能设置 trustedPublisher。source: 'hook'、hookName、extensionId;如 hook 能拿到触发 tool context,也可补 toolCallId / toolName。Bridge 必须从 transport context 派生 source: 'hook',不能信任 payload 里的 source 字段。source: 'client'、clientId,不允许 storage: 'published',也不能设置 trustedPublisher。receivedSeq:由 bridge/store 在接收输入时分配单调递增值,用于同一批内 deterministic ordering;外部输入不能指定该字段。source、storage、managedId、url、trustedPublisher 或其它 _meta.artifacts[*] 字段推断 trustedPublisher。V1 唯一例外是上述 completed ArtifactTool session update 信号。补全规则:
id:由 Section 7 的 identity hash 生成。source:由入口上下文决定,tool result / ArtifactTool 为 tool,hook 为 hook,client POST 为 client。toolCallId / toolName:由 tool call 上下文补入;hook/client 入口没有则不填。hookName / extensionId / clientId:有上下文时补入,用于审计和 UI 分组。createdAt:首次 upsert 时写入。updatedAt:每次 upsert 时刷新。status:workspace artifact upsert 时做 best-effort stat,存在且 containment check 通过则为 available,不存在或 symlink escape 则为 missing;managed / URL artifact 在 V1 不做本机 stat,始终为 available。storage 默认值:
workspacePath 时为 workspace。storage: 'published' 时必须来自 trustedPublisher,否则校验失败。managedId 且没有 url 时为 managed。url 时为 external_url。ArtifactTool 发布结果显式使用 published。kind 默认值:
storage: 'published' 且没有显式 kind 时为 html。url 且没有 workspacePath 时为 link。workspacePath 时按扩展名推断:.html -> html,图片扩展名 -> image,视频扩展名 -> video,音频扩展名 -> audio,.pdf -> pdf,.ipynb -> notebook,否则 file。other。workspacePath 只对 workspace 内文件对外展示,且必须是 workspace-relative path。managedId 是 daemon/qwen-home 托管产物引用,不能是本机绝对路径。url 只接受明确登记的 URL 或 ArtifactTool 发布 URL。workspacePath、managedId、url 必须且只能存在一个 primary locator;V1 拒绝普通输入同时携带多个 primary locator,避免同一逻辑资源按不同字段生成多个 identity。storage: 'published':url 是 primary locator,managedId 可作为可选 managed reference 一起返回,用于未来下载/预览;此时 identity 只按 url 计算,managedId 不参与 identity。该例外只接受 trustedPublisher: true 的内部输入。~/.qwen、/tmp 或其他本机绝对路径作为 workspacePath 返回。title 必填,trim 后长度 1-200 字符,不允许 ASCII 控制字符;它是 plain text,不承载 HTML 或 markdown 语义。description 是 UI 辅助 plain text,不进入模型上下文。description trim 后最多 1000 字符,不允许 ASCII 控制字符,不承载 HTML 或 markdown 语义。metadata 必须是小对象,只允许 primitive value。metadata 不放 secret、token、cookie、签名私钥。sizeBytes 是 best-effort。DaemonSessionArtifactsEnvelope 不返回宿主机绝对 workspaceCwd;client 只依赖 workspacePath 这类相对路径和 storage 字段展示。V1 不从普通文件编辑工具自动派生 artifact。
不自动派生:
ToolNames.WRITE_FILEToolNames.EDITToolNames.NOTEBOOK_EDITread_filegrep_searchgloblist_directoryweb_fetchrun_shell_command原因:
文件可以进入 artifact store 的条件:
ToolResult.artifacts。ArtifactTool 发布输出。record_artifact / hook / client POST 显式登记。WRITE_FILE / EDIT 默认推断。生成型输出示例:
.html、.pdf、.md.png、.jpg、.mp4、.mp3.xlsx、.docx、.pptx、.csv.ipynb即使是 notebook,也要区分“编辑已有 notebook 源文件”和“生成给用户查看/下载的 notebook artifact”。
ArtifactTool 成功发布后返回:
artifacts: [
{
kind: 'html',
storage: 'published',
title,
url,
managedId,
mimeType: 'text/html',
},
];
保留现有 llmContent、returnDisplay、resultFilePaths,保证兼容。
ArtifactTool 当前 local publisher 可能把内容写入 qwen home 下的托管目录,并返回 file:// 或远端 URL。Daemon artifact API 不应把 qwen home 本机绝对路径作为 workspacePath 暴露;应使用:
storage: 'published'url: 已发布的可打开 URL,也是 published artifact 的 primary locatormanagedId: 可选的内部托管引用,不参与 identityArtifactTool session update 上通过内部 option 设置 trustedPublisher: true。Bridge 不得从模型参数、hook payload、client POST body 或普通 _meta.artifacts[*] 字段推断该标志。如果未来要让 daemon client 下载或预览托管内容,应新增专门的 managed artifact route,而不是把本机绝对路径塞进 public artifact。
作为 V1 的模型/skill 显式登记入口,新增轻量内置工具:
ToolNames.RECORD_ARTIFACT = 'record_artifact';
用途:
参数:
interface RecordArtifactParams {
title: string;
description?: string;
kind?: ToolArtifactKind;
storage?: Exclude<ToolArtifactStorage, 'published'>;
workspacePath?: string;
managedId?: string;
url?: string;
mimeType?: string;
metadata?: Record<string, string | number | boolean | null>;
}
示例:
{
"title": "用户画像资源详情",
"description": "内部数据平台生产环境资源详情页",
"kind": "link",
"storage": "external_url",
"url": "https://platform.example.com/resources/user-profile?env=prod",
"mimeType": "text/html",
"metadata": {
"resourceType": "data_platform_resource",
"env": "prod"
}
}
返回:
return {
llmContent: {
recorded: true,
title: params.title,
location: params.workspacePath ?? params.managedId ?? params.url,
note: 'The daemon will expose the assigned artifact id through artifact_changed and list APIs.',
},
returnDisplay: 'Recorded artifact: 用户画像资源详情',
artifacts: [params],
};
record_artifact 在返回前做参数级 validation;失败时返回工具错误,不产生 ToolResult.artifacts。因为单次调用只有一个 artifact,V1 不需要定义批量 partial success。server-assigned id 由 daemon store 生成,并通过 artifact_changed / GET /session/:id/artifacts 暴露给 client。
record_artifact 不接受 storage: 'published',也不接受 url + managedId 的 published 例外。模型/skill 只能登记 workspace、managed 或 external URL artifact;发布型 artifact 必须来自 ArtifactTool / daemon publisher。
权限建议:
allow,因为它只修改 session UI metadata。file://,必须只允许 workspace 内文件;V1 不建议 record_artifact 接受 file:// URL。作为 V1 的 hook/extension 显式登记入口扩展。当前 hooks 已支持 command/HTTP/function/prompt,并且 command/HTTP hook 可以返回 JSON HookOutput。建议扩展 hookSpecificOutput:
{
"continue": true,
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"artifacts": [
{
"kind": "link",
"storage": "external_url",
"title": "调度任务详情",
"url": "https://ops.example.com/task/task_123",
"mimeType": "text/html",
"metadata": {
"resourceType": "scheduler_task"
}
}
]
}
}
适合场景:
需要代码改动:
HookOutput.hookSpecificOutput.artifacts?: ToolArtifact[]。packages/core/src/hooks/hookAggregator.ts 的 mergeWithOrLogic() 必须为 artifacts 增加 concat 逻辑,不走现有 hookSpecificOutput last-writer-wins。packages/core/src/core/toolHookTriggers.ts 的 PostToolUseHookResult / PostToolBatchHookResult 增加 artifacts?: ToolArtifact[]。firePostToolUseHook() 返回 artifacts?: ToolArtifact[]。firePostToolBatchHook() 返回 artifacts?: ToolArtifact[]。packages/core/src/core/coreToolScheduler.ts 必须纳入实现计划,因为它是 firePostToolBatchHook() 的调用点,也有独立的 firePostToolUseHook() 路径。collectHookArtifacts() 或等价 helper,供 coreToolScheduler.ts 与 ACP Session.ts 两条 PostToolUse 路径复用同一 extraction / validation 前置逻辑,避免两处行为漂移。Session.runTool() 收集 tool result artifacts 与 hook artifacts,但二者使用不同传输:tool result artifacts 只来自成功返回的 tool result;hook artifacts 不依赖工具成功,失败路径也可以进入 store。Session.runTool() 中,成功工具结果携带的 artifacts 继续附着到 tool_call_update._meta.artifacts;PostToolUse / PostToolUseFailure hook 返回的 artifacts 统一通过 client.extNotification('qwen/notify/session/artifact-event', payload) 单独发送。该 notification 必须在 hook artifacts 收集完成后同步 await;发送失败只记录 warning,不改变原工具失败/成功结果;这批 hook artifacts 不进入 daemon store,V1 不做持久重试。record_artifact / client POST 走同一套 validation:URL scheme、workspace path containment、metadata size/type。extNotification 的情况下,才可使用 qwen/notify/session/artifact-event。qwen/notify/session/artifact-event payload:
{
"artifacts": [
{
"kind": "link",
"storage": "external_url",
"title": "批处理任务详情",
"url": "https://ops.example.com/task/batch_123",
"mimeType": "text/html"
}
],
"source": "hook",
"hookEventName": "PostToolBatch",
"hookName": "task-artifacts",
"extensionId": "example-extension"
}
Transport 约定:
qwen/notify/session/artifact-event 是 ACP extNotification,不是 SSE event,也不是 client-facing HTTP route。qwen/notify/session/* 通知约定;例如 bridge 已有的 session notification demux 模式。extNotification 的运行时或 extension bridge。ACP Session.ts 可以发送该通知;coreToolScheduler.ts 本身不能直接向 daemon 主会话发送该通知。BridgeClient 在现有 extNotification 处理分支按 notification name demux:命中 qwen/notify/session/artifact-event 后读取 payload,转换为 SessionArtifactInput[],再进入统一 ingest pipeline。source: 'hook',payload 中的 source 只能作为兼容性提示;如果 payload source 与 transport context 不一致,bridge 覆盖为 hook 并记录 debug/warning。Notification payload 不能设置 trustedPublisher;如果携带 storage: 'published',按普通 untrusted input 校验失败处理。注意:qwen/notify/session/artifact-event 只是 explicit artifacts 的传输 envelope,不应形成第二套 store/validation/dedupe 管道。BridgeClient 必须把 _meta.artifacts、hook artifacts 与 artifact-event.artifacts 都转换为同一个 SessionArtifactInput[],调用同一个 ingestArtifacts() / SessionArtifactStore.upsertMany(),复用同一套 validation、normalization、enrichment、eviction 和 artifact_changed 发布逻辑。ACP 主会话当前没有 PostToolBatch callsite,不能把 coreToolScheduler.ts 的 batch hook 当成 daemon artifacts 面板的默认来源;若后续要支持 daemon 主会话 batch artifacts,必须先增加真实调用点和测试。非 ACP 运行时如果没有 artifact notification sink,不能声明 daemon hook artifacts 支持。
对不想让模型调用工具的场景,提供:
POST /session/:id/artifacts
适合:
与 hook 输出的区别:
artifact identity:
sessionId + ':workspace:' + normalizedWorkspacePathsessionId + ':managed:' + normalizedManagedIdsessionId + ':url:' + identityUrlidentity 只描述资源位置,不包含 source。tool、hook、client 对同一 URL 或路径的登记合并成一条 artifact,避免右侧面板重复展示同一资源。V1 不维护 provenance[]、信任级别或 retention class;首次成功登记者拥有该 artifact 的展示字段和来源审计字段,后续同 identity 登记只表达“同一个资源再次被观察到”。
输入必须且只能携带一个定位字段:
workspacePathmanagedIdurl如果输入同时携带多个 primary locator,V1 直接拒绝,而不是尝试按优先级猜测 identity。这样可以避免一个 artifact 先按 workspacePath 去重、后续又按 url 去重而产生重复。
storage: 'published' 是唯一例外:它必须携带 url 作为 primary locator,可以额外携带 managedId 作为 managed reference。published identity 仍按 url 计算;managedId 只用于未来下载/预览,不参与去重。该例外只接受带内部 trustedPublisher: true 的输入;hook、client POST、record_artifact 或普通工具返回 storage: 'published' 时按校验失败处理。
对外 id:
normalizedWorkspacePath:
path.resolve(workspaceCwd, input) 得到绝对路径。path.relative(workspaceCwd, resolved) 不能以 .. 开头,且不能是绝对路径。fs.realpath 检查 symlink 最终目标仍在 workspace 内;symlink 指向 workspace 外则拒绝。status 必须是 missing;不能因为 realpath 失败就跳过 symlink containment。后续 GET TTL refresh 时必须重新执行同一 containment + realpath 检查。status 变为 missing,并清除 best-effort sizeBytes;V1 绝不把该路径报告为 available。./。normalizedManagedId:
/、\、..,不允许表达路径层级或本机绝对路径语义。managedId 返回 normalized 后的值。identityUrl 与 url:
new URL(input) 解析,禁止字符串 startsWith('http') 这类宽松判断。ArtifactTool trusted published URL 外,普通 link artifact 只允许 http: / https:。url 字段保存清理后的可点击 URL,供 client 打开;不要把 identity 用 URL 反写成可点击 URL。identityUrl 计算,不作为 public 字段返回。https:443 / http:80 不保留。username / password,不把 URL userinfo 存入 artifact store。去重行为:
createdupdatedcreatedAt 保持不变。updatedAt 更新,但不参与 eviction 排序。upsertMany() 内先按 identity 合并输入;同 identity 的 owner 由 receivedSeq 最小的输入决定,若没有 receivedSeq 则使用输入数组顺序。BridgeClient 不应把不同 transport event 的 artifacts 无序合并;如果必须合并,必须先分配 receivedSeq 再排序。每个最终 identity 只在 changes[] 里产生一条 change。若该 identity 在本批之前不存在则为 created,否则为 updated。title、description、source、toolCallId、toolName、hookName、extensionId、clientId 采用 first-writer-wins,不被后续同 identity 输入覆盖。external_url 升级到 published 时,可以更新 storage、补充 managedId、更新 kind / mimeType / sizeBytes,并允许 publisher 覆盖 title / description,避免占位 link 标题永久遮蔽真实发布物。该升级只接受带内部 trustedPublisher: true 的 storage: 'published' 输入。managedId 从空补齐为 published managed reference 是允许的;已有 managedId 不被后续普通输入覆盖。status 和 sizeBytes 是 daemon 的 best-effort 派生字段,可以随 workspace stat 或 published artifact enrichment 刷新。metadata 保存首次登记时通过校验的小对象;后续同 identity 只有 source: 'tool' 或 source: 'client' 的输入可以做受控富化:只添加不存在的 key,不覆盖已有 key,合并后重新校验 primitive-only 与 4KB 总大小。hook 对已存在 artifact 的 metadata 富化默认忽略。若合并后超限,只丢弃本次 metadata 富化并记录 warning,artifact 的其它安全升级仍可继续。retentionSource;它只把内部 clientRetained 置为 true,用于表达用户手动保留意图。SessionArtifactStore.upsertMany() 内同步处理,避免异步读改写竞态。内部 store 字段:
retentionSource:首次成功登记者的 source,创建时赋值,之后不随 client POST 或重复 upsert 改变。clientRetained:布尔值,初始为 source === 'client';任意通过 mutation gate 的 client POST 命中同 identity 时置为 true。clientRetained 不改变展示字段,也不迁移 retentionSource bucket。insertSeq:store 内单调递增序号,创建 artifact 时赋值一次,永不刷新。receivedSeq:输入接收顺序,只用于同批 deterministic coalescing,不作为 public 字段返回。配额与保留策略:
retentionSource 归属:
tool: 100client: 50hook: 50upsertMany() 新创建的 artifact 默认不进入候选池;eviction 先只在本批开始前已经存在的 artifacts 中选择候选。这样一个本批新登记的 missing artifact 可能在满 store 中挤掉仍然 live 的旧 artifact,这是 V1 为保证当前显式产物可见性作出的选择。
status: 'missing' 且 clientRetained === false 的 artifact。retentionSource 数量超过 reservation 的来源中裁剪 clientRetained === false 的 artifact。clientRetained === false 的最旧 artifact。clientRetained === true,裁剪最旧的 client-retained artifact。missing 优先级前,必须对即将作为候选的 workspace artifacts 做 best-effort status refresh / containment check;如果刷新后为 available,不能继续把它当 missing 优先裁剪。刷新失败时保留原 cached 状态。clientRetained 是最后裁剪偏好,不是无限 pin,也不突破 200 全局上限或 soft reservation。所有 artifact 都是 client-retained 时,仍按最旧 client-retained artifact 裁剪。changes[] 前按 receivedSeq / 输入顺序保留前 N 个新 identity,丢弃超出的本批输入并记录 warning/diagnostics。被丢弃的新输入不进入 store,不产生 created 或 removed change,因此同一次 mutation 内同一 identity 不会出现 created 后又 removed。(createdAt, insertSeq),insertSeq 是 store 内部单调递增序号,用来稳定同毫秒或同批输入的 tiebreaker。updatedAt,但 eviction 不看 updatedAt;因此其它来源不能通过高频重复登记把一个旧 artifact 固定在保留集合里。createdAt 升序。artifact_changed / removed。V1 不提供其它裁剪事件。retentionSource、clientRetained 与 insertSeq 是 V1 实现细节,不是 wire protocol 字段;后续可在不改变 API shape 的前提下调整默认值,或增加更细的 per-producer quota。V1 的 store 是 live bridge session 内存索引:
GET /session/:id/artifacts 做 snapshot sync。artifacts_reset event;如果后续支持 session 继续存在但 artifact store 被清空的运行模式,再增加 artifacts_reset 或等价 snapshot-invalidated event。以下 Phase 是同一 V1 完整能力的工程实施顺序,不代表对外拆成多个版本。实现 PR 可以按 Phase 拆小,但合并后的设计基准是一项完整 session artifacts 能力。
改动:
packages/core/src/tools/tools.ts
ToolArtifactKind、ToolArtifactStorage、ToolArtifact。ToolResult.artifacts?。packages/core/src/tools/artifact/artifact-tool.ts
artifacts。storage: 'published',不把 qwen home 本机路径作为 workspacePath 暴露。Phase A 先接入 ToolResult.artifacts 和 ArtifactTool;record_artifact 在 Phase D 接入,但仍属于同一个 V1 完整能力。
改动:
packages/cli/src/acp-integration/session/types.ts
ToolCallResultParams.artifacts?packages/cli/src/acp-integration/session/emitters/ToolCallEmitter.ts
_meta.artifacts = params.artifactspackages/cli/src/acp-integration/session/Session.ts
toolResult.artifacts。WRITE_FILE / EDIT / NOTEBOOK_EDIT 自动派生 artifacts。emitResult()。新增:
packages/acp-bridge/src/sessionArtifacts.ts
SessionArtifactStoreBridge session entry 增加:
artifacts: SessionArtifactStore;
Bridge interface 增加:
getSessionArtifacts(sessionId: string): SessionArtifactsEnvelope;
addSessionArtifacts(
sessionId: string,
artifacts: SessionArtifactInput[],
): DaemonSessionArtifactMutationResult;
removeSessionArtifact(
sessionId: string,
artifactId: string,
): DaemonSessionArtifactMutationResult;
BridgeClient:
session_update/tool_call_update._meta.artifacts 提取 artifacts。qwen/notify/session/artifact-event 提取 explicit notification artifacts。SessionArtifactInput[]。source、receivedSeq。trustedPublisher 只由 completed ArtifactTool session update 的 bridge-side ingest option 分配;BridgeClient 不得根据 artifact payload 字段或普通 _meta.artifacts 内容推断。ingestArtifacts() / SessionArtifactStore.upsertMany(),不要为 notification artifacts 建第二套 validation 或 dedupe。upsertMany() 返回 DaemonSessionArtifactMutationResult,包含 created/updated 以及 eviction 产生的 removed changes。artifact_changed,先发布 created/updated,再发布 removed。removeSessionArtifact() 从 store 删除 artifact,返回 reason: 'explicit' 的 removed change,并发布 artifact_changed。改动:
packages/cli/src/serve/capabilities.ts
session_artifacts。packages/cli/src/serve/server.ts
GET /session/:id/artifacts。DELETE /session/:id/artifacts/:artifactId。GET 行为:
lastStatAt、lastKnownSizeBytes、lastKnownStatus。lastStatAt 过期时按 TTL 刷新,例如 5-30 秒,并限制并发 stat 数量。刷新时必须重新执行 Section 7.1 的 workspace containment 与 realpath symlink check。status: 'missing',不删除 artifact。missing,GET 返回 status: 'available'。status: 'missing',不返回新的 sizeBytes。artifact_changed;V1 status 对 SSE 客户端是最终一致的。artifact_changed / updated,不要放在 GET 热读路径。status: 'available'。改动:
packages/sdk-typescript/src/daemon/types.ts
packages/sdk-typescript/src/daemon/events.ts
artifact_changed。packages/sdk-typescript/src/daemon/DaemonClient.ts
listSessionArtifacts(sessionId, opts?, clientId?)addSessionArtifact(sessionId, artifact, clientId?)removeSessionArtifact(sessionId, artifactId, clientId?)packages/sdk-typescript/src/daemon/DaemonSessionClient.ts
artifacts(opts?)addArtifact(artifact)removeArtifact(artifactId)packages/sdk-typescript/src/index.ts
SDK singular add 映射到 bridge plural mutation:addSessionArtifact(a) 包装为 addSessionArtifacts(sessionId, [a]),返回完整 DaemonSessionArtifactMutationResult,不丢弃 eviction 产生的 removed changes。
改动:
packages/core/src/tools/tool-names.ts
RECORD_ARTIFACT: 'record_artifact'。packages/core/src/tools/record-artifact.ts
RecordArtifactTool。workspacePath / managedId / url,不接受任意本机绝对路径。storage: 'published' 或 url + managedId published 例外。ToolResult.artifacts,复用 V1 store/event/list 链路。Config.createToolRegistry
改动:
packages/core/src/hooks/types.ts
HookOutput.hookSpecificOutput.artifacts?: ToolArtifact[]。packages/core/src/hooks/hookAggregator.ts
mergeWithOrLogic() 对 artifacts 多 hook concat,不走 last-writer-wins。packages/core/src/core/toolHookTriggers.ts
PostToolUseHookResult / PostToolBatchHookResult 增加 artifacts?: ToolArtifact[]。packages/core/src/core/coreToolScheduler.ts
packages/cli/src/acp-integration/session/Session.ts
coreToolScheduler.ts 的非 daemon 主会话路径。qwen/notify/session/artifact-event 发给 bridge。qwen/notify/session/artifact-event 提取 batch-level artifacts,走同一套 validation 和 upsert。改动:
packages/cli/src/serve/server.ts
POST /session/:id/artifacts,走 mutate({ strict: true })。DELETE /session/:id/artifacts/:artifactId,走 mutate({ strict: true })。client。SessionArtifactInput[],调用 bridge 的 addSessionArtifacts()。storage: 'published' 或 trustedPublisher。removeSessionArtifact();artifact 已不存在时返回空 changes[],不发布 SSE。artifact_changed,先发布 created/updated,再发布 removed。artifact add 不新增单数 bridge mutation;所有新增入口都走 addSessionArtifacts() / upsertMany(),避免 validation、coalescing、eviction 行为漂移。artifact remove 使用单独的 removeSessionArtifact(),因为它按 server-assigned artifact id 删除,不参与 input validation / identity coalescing。
SDK 增加:
DaemonClient.addSessionArtifact(sessionId, artifact, clientId?)DaemonSessionClient.addArtifact(artifact)DaemonClient.removeSessionArtifact(sessionId, artifactId, clientId?)DaemonSessionClient.removeArtifact(artifactId)http: / https:。new URL(input) 解析并检查 parsed.protocol,禁止基于字符串前缀判断。parsed.username / parsed.password,避免 URL credential 泄漏。record_artifact / hook / client POST 不允许 file://。ArtifactTool 返回的 file:// published URL 保持例外,因为它来自已授权 publish;remote daemon 场景应优先使用远端 publisher 的 https: URL。kind: 'image' | 'video' | 'audio' | 'html' 就自动把 external URL 填入 ``、<video>、<audio>、iframe 或类似会发起网络请求的预览元素。V1 对 external URL 只展示图标、标题、host 和点击入口;远程预览必须等用户显式点击,或后续通过单独 preview capability 与 sandbox 策略启用。workspacePath,它必须是 workspace-relative path。record_artifact / hook / client POST 如果传 workspacePath,必须在 workspace 内。path.resolve + path.relative containment check,目标存在时再做 fs.realpath symlink escape check;目标不存在时 artifact 可以进入 store,但必须标记为 missing,后续 GET/status refresh 继续重跑同一校验。.. escape、绝对路径 escape、symlink 指向 workspace 外、~/.qwen、/tmp 等本机外部路径。managedId 只能引用 daemon-managed storage;trim 后不能为空,拒绝路径分隔符、..、控制字符和本机绝对路径语义。visibility、sensitivity、expiresAt、sourceId 等无消费者字段;artifact visibility 固定为当前 session-local 语义。source / toolCallId / toolName / hookName / extensionId / clientId、createdAt、updatedAt 承载。external_url -> published upgrade,此时 publisher 可以覆盖 title / description。metadata 只允许 Section 7 定义的受控富化,避免跨来源 metadata 注入。title / description 是 plain text,不是 HTML,也不是 markdown。title、description、metadata string value、toolName、hookName、extensionId、clientId,client 都必须作为 untrusted text 渲染或 HTML escape,禁止通过 innerHTML 直接插入。tool: 100、client: 50、hook: 50,未使用额度可被其它来源借用。record_artifact 每次 tool call 只登记 1 个 artifact。POST /session/:id/artifacts 走现有 rate limit / mutation gate。artifact_changed / removed event。record_artifact 参数校验失败时返回工具错误,不产生 artifact。POST /session/:id/artifacts body 校验失败时返回 400。_meta.artifacts、hook artifacts 或 artifact-event 中的单条 malformed artifact 不应破坏原始 tool/session event;bridge 应跳过该 artifact,并记录 warning-level log。右侧 artifacts 面板只展示声明式 artifacts;聊天正文仍可显示普通链接。
不做自动抽取的原因:
如果业务强烈需要从文本中提取 URL,应作为 Client 可选 UX:
artifact_changed。V1 提供 record_artifact 后,skill 或 agent.md 可以写:
当你根据工具结果构造出可供用户查看的业务资源 URL 时,调用 record_artifact 工具登记它。
登记规则:
- title 使用资源的人类可读名称。
- kind 使用 link。
- storage 使用 external_url。
- url 使用最终可点击 URL。
- metadata.resourceType 填资源类型,例如 data_platform_resource、scheduler_task。
- 不要把普通参考文档链接登记为 artifact。
模型执行后:
record_artifact。这个方案不要求 skill 编写 hook,也不要求 extension/plugin 代码,最适合多数业务规则。
V1 提供 hook artifacts 后,extension 可在 qwen-extension.json 或 hooks/hooks.json 中提供 PostToolUse hook:
{
"hooks": {
"PostToolUse": [
{
"matcher": "mcp__data_platform__get_resource",
"hooks": [
{
"type": "command",
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/table-artifact.js"
}
]
}
]
}
}
当前 qwen-code extension/hook 变量替换仍支持 ${CLAUDE_PLUGIN_ROOT};如果后续引入新的 qwen-specific root 变量,示例可随实现同步迁移。
脚本 stdout:
{
"continue": true,
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"artifacts": [
{
"kind": "link",
"storage": "external_url",
"title": "用户画像资源详情",
"url": "https://platform.example.com/resources/user-profile",
"mimeType": "text/html",
"metadata": {
"resourceType": "data_platform_resource"
}
}
]
}
}
这适合企业插件:把“如何从工具结果拼业务 URL”的逻辑固化在 extension 中,而不是写进每个 prompt。
覆盖:
ToolResult.artifacts 类型编译。ArtifactTool 成功返回 storage: 'published' 的 html artifact。ArtifactTool 不把 qwen home 本机绝对路径作为 workspacePath 暴露。ToolArtifact.kind / storage 默认推断规则有单测覆盖。命令:
cd packages/core && npx vitest run src/tools/artifact/artifact-tool.test.ts
覆盖:
ToolCallEmitter.emitResult() 输出 _meta.artifacts。toolResult.artifacts 被传给 emitResult()。ArtifactTool session update 会通过 bridge-side ingest option 设置内部 trustedPublisher: true;record_artifact、其它 tool result、hook payload、client POST 不会设置,BridgeClient 也不能通过 artifact payload 字段推断。write_file/edit/notebook_edit 普通源码修改不自动派生 artifact。read_file/grep/glob/shell 不派生 artifact。_meta.artifacts。命令:
cd packages/cli && npx vitest run src/acp-integration/session/emitters/ToolCallEmitter.test.ts
cd packages/cli && npx vitest run src/acp-integration/session/Session.test.ts
覆盖:
SessionArtifactStore created/updated/removed。ToolArtifact 到 DaemonSessionArtifact 的 enrichment。SessionArtifactInput 是所有入口统一的内部输入类型。kind / storage 推断,覆盖 published->html、html/image/video/audio/pdf/notebook/file。trustedPublisher: true 且 storage: 'published' 允许 url + managedId,identity 只按 url。record_artifact 或普通 tool result 伪造 storage: 'published' 会被拒绝或跳过并记录 warning。.. 拒绝、控制字符拒绝、大小写不折叠。url 保存清理后的可点击 URL,identity 用内部 identityUrl,两者不混用。../../etc/passwd、workspace 外绝对路径、symlink escape 均被拒绝;不存在路径进入 store 时为 missing,GET TTL refresh 重新做 containment / realpath check。external_url -> published 资源本体升级,补齐 managedId / kind / mimeType,并允许 publisher 覆盖占位 title / description。receivedSeq / 输入数组顺序确定 owner,并在 changes[] 中只产生一条最终 change。retentionSource 创建时赋值且不刷新;clientRetained 与 retentionSource 分离;insertSeq 创建时赋值且不刷新。createdAt + insertSeq 稳定排序,且逐条发送 reason: 'eviction' 的 removed event。created + removed。clientRetained 不突破全局 200 上限;全量 client-retained 时仍裁剪最旧项。_meta.artifacts 被写入 store。artifact_changed 发布。upsertMany() / addSessionArtifacts() 返回包含 eviction changes 的 DaemonSessionArtifactMutationResult。removeSessionArtifact() 返回 reason: 'explicit' 的 removed change。命令:
cd packages/acp-bridge && npx vitest run src/sessionArtifacts.test.ts
cd packages/acp-bridge && npx vitest run src/bridgeClient.test.ts
覆盖:
/capabilities 包含 session_artifacts。GET /session/:id/artifacts 返回空列表。workspaceCwd。status: 'missing',文件恢复后返回 status: 'available'。missing。artifact_changed;managed / URL artifact 不做本机 stat。命令:
cd packages/cli && npx vitest run src/serve/server.test.ts
覆盖:
listSessionArtifacts() route 正确。artifact_changed known event narrowing,event artifact 是完整 DaemonSessionArtifact。命令:
cd packages/sdk-typescript && npx vitest run src/daemon/DaemonClient.test.ts
cd packages/sdk-typescript && npx vitest run src/daemon/events.test.ts
record_artifact:
workspacePath + managedId + url,也不允许普通输入同时传多个 primary locator。storage: 'published'。ToolResult.artifacts。llmContent 返回结构化登记结果;每次 tool call 只登记一个 artifact。hook artifacts:
HookOutput.hookSpecificOutput.artifacts 通过 createHookOutput()、toolHookTriggers.ts 进入 PostToolUseHookResult / PostToolBatchHookResult。hookAggregator.ts 的 mergeWithOrLogic() 多 hook artifacts concat。coreToolScheduler.ts 和 ACP Session.ts 两条路径都能传播 PostToolUse artifacts。qwen/notify/session/artifact-event extNotification 单独进入 bridge,不依赖成功 tool result _meta.artifacts。qwen/notify/session/artifact-event 被写入 store。source 由 bridge 按 transport context 派生,不能伪造 tool source 或 trusted publisher。client POST / SDK add:
POST /session/:id/artifacts 成功 upsert。POST 返回 DaemonSessionArtifactMutationResult,包含 created/updated 以及 eviction removed changes。POST 触发 upsert + eviction 时,验证每个 changes[] 项都同步发布为 artifact_changed SSE event,且 created/updated 先于 removed。POST 在未授权/无 mutation token 时被拒绝。POST 对 workspace 外 path、path traversal、symlink escape 返回 400。POST 对 storage: 'published'、多 primary locator、metadata 超限返回结构化错误 envelope。POST 通过 bridge addSessionArtifacts() 单一路径写入。DaemonClient.addSessionArtifact() body 正确。DELETE /session/:id/artifacts/:artifactId 命中时返回 reason: 'explicit' 的 removed change,并发布对应 SSE event,不删除底层文件或 URL。DELETE /session/:id/artifacts/:artifactId 未命中时幂等返回空 changes[],不发布 SSE event。覆盖完整链路:
ToolResult.artifacts。ToolCallEmitter 写入 _meta.artifacts。BridgeClient 从 event 中提取 artifacts。SessionArtifactStore validate / normalize / upsert。artifact_changed。GET /session/:id/artifacts 返回同一个 artifact。reason: 'eviction' 的 removed event,随后 GET 只返回裁剪后的状态。场景 A:文件产物
lineage.html。GET /session/:id/artifacts 返回 storage: 'published' 的 html artifact。artifact_changed。场景 B:普通源码编辑不进入产物区
场景 C:显式业务链接产物
record_artifact。场景 D:hook 产物
场景 E:普通链接不进入产物区
V1 完整能力实现后至少满足:
session_artifacts feature 存在。GET /session/:id/artifacts 可用。artifact_changed event 可用。ArtifactTool 生成 published html artifact。ToolResult.artifacts 能进入 daemon artifact store。record_artifact 能登记 link / workspace artifact,且 feature-gated 或 opt-in 注册。hookSpecificOutput.artifacts 注入 artifact,多个 hook artifacts concat。POST /session/:id/artifacts 注入 artifact。DELETE /session/:id/artifacts/:artifactId 显式移除误登记 artifact。WRITE_FILE / EDIT / NOTEBOOK_EDIT 不自动进入 artifact list。artifact_changed。npm run build && npm run typecheck 通过。V1 内部建议按以下顺序实现;这是工程排期,不是能力拆分:
ToolArtifact + ToolResult.artifacts?ArtifactTool structured artifactsToolCallEmitter._meta.artifactsSession.runTool() 只收集 toolResult.artifactsSessionArtifactStore validation / normalize / enrichment / upsert_meta.artifactsGET /session/:id/artifactsRecordArtifactToolqwen/notify/session/artifact-eventPOST /session/:id/artifactsPhase 2:历史恢复
session/load 后 artifact list 可恢复。Phase 3:详情与预览
GET /session/:id/artifacts/:artifactIdPhase 4:安全动态预览
Phase 5:长期存储
Link 可以是 artifact,但必须显式登记。右侧产物区不应该自动收集所有文本链接。
V1 对外是一项完整能力,内部由统一 store 和四类入口组成:
ToolResult.artifacts / ArtifactTool 产生结构化 artifact metadata。record_artifact 工具。hookSpecificOutput.artifacts。POST /session/:id/artifacts。这些入口最终都进入同一个 SessionArtifactStore,通过同一个 GET /session/:id/artifacts 查询,通过同一个 artifact_changed SSE 事件更新 UI。这样能覆盖业务 link、文件、HTML、图片、视频等产物,同时保持协议简单、来源清晰、边界可控。最重要的边界是:Artifacts 是被声明的 session outputs,不是所有普通文件编辑或普通链接的集合。