FastGPT SkillEdit 复用标准 Chat 会话:sourceType/sourceId 双资源模型设计与实践
2026/9/10 4:25:38 网站建设 项目流程

FastGPT SkillEdit 复用标准 Chat 会话:sourceType/sourceId 双资源模型设计与实践

【免费下载链接】FastGPTFastGPT is a knowledge-based platform built on the LLMs, offers a comprehensive suite of out-of-the-box capabilities such as data processing, RAG retrieval, and visual AI workflow orchestration, letting you easily develop and deploy complex question-answering systems without the need for extensive setup or configuration.项目地址: https://gitcode.com/GitHub_Trending/fa/FastGPT

本篇技术指南围绕 FastGPT 中"Skill Edit(Skill 调试/预览)复用标准 Chat 会话"这一核心改造展开,讲解在保留历史appId物理字段的前提下,通过业务语义层新增sourceType + sourceId双资源模型,让 App 会话与 Skill Edit 会话共用chatschatitemschat_item_responses三张标准会话表。读完本文,你将掌握该模型的枚举定义、统一查询/写入 helper、OpenAPI 双层 schema、前端 Chat Target 分层、sandbox/S3/stop-resume 的资源隔离规则,以及索引迁移与历史数据兼容策略,可直接用于理解或复刻类似的"多资源类型复用单套会话存储"改造。

一、背景:为什么不能直接改字段

FastGPT 的标准 Chat 体系由三张表构成:chats(会话)、chatitems(消息)、chat_item_responses(节点响应)。历史上这三张表的物理字段叫appId,所有 App 会话都通过它归属到具体应用。

当 Skill Edit 调试会话需要复用这套标准 Chat 能力(标准 chat service、标准 chat API)时,面临两个约束:

  • 历史数据量巨大:历史 App 会话数据量很大,不适合为了接入 Skill Edit 做大规模字段重命名或全量回填;
  • 物理字段语义单一appId字段名承载不了"资源类型"的语义,直接把 SkillId 塞进appId会导致 Skill 会话与 App 会话在同一字段下混存、无法区分。

因此最终方案是在业务语义层新增sourceType + sourceId

  • App 会话:sourceType=appsourceId=appId
  • Skill Edit 会话:sourceType=skillEditsourceId=skillId

Mongo 第一阶段继续保留物理字段appId,但把它视为历史字段名,业务含义统一为sourceId。这样既不动存量数据,又能在语义层完成资源隔离。

二、目标与非目标

目标

  • App 和 Skill Edit 共用chatschatitemschat_item_responses
  • 标准 chat API 对外继续使用业务字段appIdskillId,不暴露内部sourceType/sourceId
  • API route 使用parseApiInput和 runtime schema 把appId/skillId转换为sourceType/sourceId
  • API handler 之后的业务层统一接收sourceType/sourceId,禁止继续传 API 原始字段appId/skillId
  • App 历史数据在缺失sourceType的情况下仍可读取、更新和删除;
  • Skill Edit 的 usage 写入usage.skillId,不污染usage.appId
  • Skill Edit 不写入 App 最近使用、App 统计日志和 App 看板;
  • stop、resume、nodeResponse、S3、sandbox 都按sourceType/sourceId隔离。

非目标(第一阶段的明确边界)

  • 不做 Mongo 字段appId -> sourceId的物理重命名;
  • 不强制回填几亿历史 App chat 数据;
  • 不长期兼容旧 Skill Debug chat,上线初始化阶段清理掉旧数据;
  • 不让ChatSourceEnum承担资源类型语义——它继续表示入口来源testapionlineshare等);
  • 不把 Skill Edit 接入 App 最近使用和 App chat logs。

三、核心模型:ChatSourceTypeEnum 与统一 helper

枚举定义

在 packages/global/core/chat/constants.ts 中定义了资源类型枚举,与表示"入口来源"的ChatSourceEnum明确区分:

export enum ChatSourceTypeEnum { app = 'app', skillEdit = 'skillEdit', chatAgentHelper = 'chatAgentHelper' }

说明:源码中的枚举比设计初稿多了一个chatAgentHelper成员(HelperBot 独立命名空间),注释也明确写道:ChatSourceEnum表示对话入口来源(如 test/api/online),ChatSourceTypeEnum表示会话归属资源类型,用于在同一套 chat 表中隔离 App 和 Skill Edit。这正对应设计中"不让 ChatSourceEnum 承担资源类型语义"的约束。

sourceId是所属资源的真实 ObjectId:

  • sourceType=app时,sourceId是 AppId;
  • sourceType=skillEdit时,sourceId是 SkillId。

统一查询/写入 helper

所有新代码必须通过统一 helper 构造查询和写入字段,实现在 packages/service/core/chat/source.ts:

export function buildChatSourceWriteFields({ sourceType, sourceId }: ChatSourceParams) { return { sourceType, appId: sourceId }; } export function buildChatSourceQuery({ sourceType, sourceId }: ChatSourceParams) { if (sourceType === ChatSourceTypeEnum.app) { return { appId: sourceId, $or: [{ sourceType: ChatSourceTypeEnum.app }, { sourceType: { $exists: false } }] }; } if ( sourceType === ChatSourceTypeEnum.skillEdit || sourceType === ChatSourceTypeEnum.chatAgentHelper ) { return { appId: sourceId, sourceType }; } const exhaustiveCheck: never = sourceType; throw new Error(`Unsupported chat source type: ${exhaustiveCheck}`); }

核心语义:

  • App 查询默认兼容历史数据$or同时匹配sourceType=appsourceType字段缺失({ $exists: false })的记录,保证存量 App 会话长期可读;
  • Skill Edit 查询必须精确匹配sourceType=skillEdit,避免 Skill 与 App 复用同一物理appId字段时串记录;
  • 非法sourceTypenever穷尽检查并抛错,从编译期和运行期双重防呆。

源码中还额外提供了buildChatSourceAggregateMatch,用于聚合管线场景——因为 Mongoose 不会自动 cast aggregate$match,它会把合法的 ObjectId 字符串显式转换成Types.ObjectId,避免聚合查询命中不了历史appId物理字段(见 source.ts)。

四、API 设计:对外 appId/skillId,对内 sourceType/sourceId

入参互斥规则

标准 chat API 对外只接受appIdskillId二者其一:

{ "appId": "68ad85a7463006c963799a05", "chatId": "chat_xxx" }
{ "skillId": "68ad85a7463006c963799a06", "chatId": "chat_xxx" }

规则:

  • appIdskillId必须且只能传一个;
  • 只传appId:转换为sourceType=appsourceId=appId
  • 只传skillId:转换为sourceType=skillEditsourceId=skillId
  • Zod transform 后的业务层不再保留顶层appId/skillId
  • 不使用字段名type表示资源类型,避免和已有业务枚举(如ChatSourceEnum)冲突。

OpenAPI schema 分层

withChatTarget一类 runtime schema 带 transform,不能直接用于 OpenAPI 文档生成。因此每个标准 chat API 使用两层 schema:

  • Raw schema:不带 transform,对外描述appId/skillId,用于 OpenAPI path 和前端请求类型;
  • Runtime schema:基于 raw schema transform,API route 的parseApiInput使用,输出sourceType/sourceId

OpenAPI 只注册 raw schema。互斥约束由 raw schema 的superRefine与 API route 的parseApiInput在运行时共同保证。

这一分层在 packages/global/openapi/core/chat/api.ts 中有完整实现,四个 schema 构造器覆盖了必填/可选与外链鉴权两种维度:

helper语义
createChatTargetInputSchema(shape)必填 target,appId/skillId互斥,用于 OpenAPI path
createOptionalChatTargetInputSchema(shape)可选 target,仅用于可从外链鉴权上下文反推 App 的接口
createOutLinkChatTargetInputSchema(shape)必填 target + 外链鉴权字段
createOptionalOutLinkChatTargetInputSchema(shape)可选 target + 外链鉴权字段

对应的 runtime 转换 helper 为withChatTargetwithOptionalChatTargetwithOutLinkChatTargetwithOptionalOutLinkChatTarget,它们内部调用transformChatTargetInput/transformChatAuthTargetInput等转换函数。源码注释特别强调:不能用OutLinkChatAuthSchema.extend(createChatTargetInputSchema(...).shape)拼接,否则会丢失 chat target 互斥校验。

互斥校验本身由refineChatAuthTargetInput完成(api.ts),它覆盖了这些规则:

  • appIdskillId不能同时提供;
  • appId/skillId/share auth三选一;
  • sourceType=chatAgentHelper必须带appId、不能带skillId
  • share 模式下shareIdoutLinkUid必须成对出现;
  • skillId不能与 share auth 混用;
  • 必填模式下三者必须提供其一。

对应的运行时转换transformChatTargetInput(api.ts)逻辑如下:sourceId = appId || skillId;若传了skillIdsourceType=skillEdit,否则按chatAgentHelper/app判断。

标准接口覆盖范围

以下接口统一支持appId/skillIdraw input,并在 route 中解析为sourceType/sourceId

  • 会话初始化与续跑:/api/core/chat/init/api/core/chat/resume
  • 停止:/api/v2/chat/stop
  • 记录:/api/core/chat/record/getRecords_v2getPaginationRecordsgetResDatadeletegetQuotegetCollectionQuote
  • 历史:/api/core/chat/history/getHistoriesgetHistoryStatusmarkReadupdateHistorydelHistoryclearHistoriesbatchDelete
  • 反馈:/api/core/chat/feedback/updateUserFeedbackupdateFeedbackReadStatusadminUpdatecloseCustomgetFeedbackRecordIds
  • 文件:/api/core/chat/file/presignChatFilePostUrlpresignChatFileGetUrl
  • 语音:/api/v1/audio/transcriptions

历史 wrapper route 如果保留,必须复用同一个 source-aware handler 和 schema。

App-only 接口(不接入 Skill Edit)

以下接口保持 App-only:outLink init、team init、inputGuide、recentlyUsed、公开 OpenAPI chat completions、/api/core/chat/chatTest、helperBot、App chat logs。

App-only 接口内部如调用标准 chat service,必须显式传sourceType=appsourceId=appId

特别说明/api/core/chat/chatTest:它的入参是 App workflow test 协议,依赖nodes/edges/chatConfig/appNameauthApp;Skill Edit 调试使用 Skill 专属协议构造 runtime nodes 和编辑沙盒上下文,不应仅通过给chatTest增加skillId来混用两套请求结构。若后续要彻底移除 Skill debug 专属接口,需要单独设计生成入口转换层,而不是把skillId直接塞进现有ChatTestPropsSchema

五、前端 Chat Target 设计

双层 target

前端分为两层 target(实现见 projects/app/src/web/core/chat/utils.ts):

type ChatSourceTarget = { sourceType: 'app' | 'skillEdit'; sourceId: string; }; type ChatApiTarget = { appId: string } | { skillId: string };

规则:

  • ChatSourceTarget是前端标准 chat 组件内部 target,ChatBoxWorkflowRuntimeContext和标准 chat 请求都以它为准;
  • ChatApiTarget是 OpenAPI/API 边界 raw target,只在请求发出前由toChatApiTarget(sourceTarget)派生;
  • App 页面传sourceTarget={{ sourceType: 'app', sourceId: appId }};Skill Preview 传sourceTarget={{ sourceType: 'skillEdit', sourceId: skillId }}
  • ChatBox runtime 状态 key 不暴露成 prop,统一用getChatSourceKey(sourceTarget)生成,形如${sourceType}:${sourceId}
  • Skill Preview 下 input guide、TTS、语音识别入口和 ChatBox 内的 App 沙盒入口不展示、不调用;
  • 所有标准 chat API 调用只能从sourceTarget派生ChatApiTarget,不能从真实 App-onlyappId或 Skill ID 推导;
  • 前端组件不要自行拼 API raw target,统一走toChatApiTarget(sourceTarget)

核心转换实现:

export const toChatApiTarget = (target: ChatSourceTarget): ChatTargetInputType => { if (target.sourceType === ChatSourceTypeEnum.skillEdit) { return { skillId: target.sourceId }; } if (target.sourceType === ChatSourceTypeEnum.chatAgentHelper) { return { appId: target.sourceId, sourceType: ChatSourceTypeEnum.chatAgentHelper }; } return { appId: target.sourceId }; };

源码中还提供了useChatApiTarget(target)(useMemo 包装的派生 hook)和getChatSourceKey,前者供标准请求统一取 API raw target,后者生成${sourceType}:${sourceId}运行时 key。此外utils.ts还提供了toChatSourceTarget(raw target → 内部 source target,供 SandboxEditor 等 API 边界场景反向使用)和toChatAuthApiTarget(source target → 带 outLinkAuthData 的 raw target,share 模式只传outLinkAuthData)。

ChatBox 最终前端方案

ChatBox不再感知appId/skillId,只接收标准内部 target:

<ChatBox sourceTarget={{ sourceType, sourceId }} features={features} onStartChat={onStartChat} onChatGenerateStatusChange={onChatGenerateStatusChange} />

边界划分:

  • sourceTarget:用于 record/history/feedback/file/quote/resume/stop/delete 等标准 chat 能力;
  • features:只控制功能展示和能力开关,如 feedback、mark、voice、tts、inputGuide、sandbox、workorder、autoResume、markRead、quickReplies、footer actions;
  • onStartChat:保留外部注入,因为 App/Home/Share/ChatTest/Skill Preview 的生成编排不同,暂时不能统一;
  • onChatGenerateStatusChange:只作为事件通知外部页面,ChatBox 不直接读写侧栏 history、最近使用、路由状态等外部模型;
  • onStopChat:移除外部 override,统一走 source-aware/api/v2/chat/stop
  • onDeleteChatItem:移除外部 override,统一走 source-aware chat item delete 接口;
  • ChatBox 目录内禁止直接依赖ChatContextuseChatStore、最近使用等外部页面状态;需要影响外部时通过 props 回调由页面层承接;
  • App/Home/Share 侧栏历史同步放在页面层 hook 中消费onChatGenerateStatusChange;Skill Preview 不传该回调。

迁移顺序

  1. 新增ChatSourceTargetgetChatSourceKeytoChatApiTarget
  2. WorkflowRuntimeContext改为暴露sourceTarget/sourceKey/appId/chatId,其中appId只表示真实 App-only 能力所需的 AppId;
  3. ChatBoxprops 改为sourceTarget + features + onStartChat,不保留feedbackType/showMarkIcon/showVoiceIcon/...等旧 feature props;
  4. 标准 chat 请求统一改用toChatApiTarget(sourceTarget)
  5. 删除onStopChat/onDeleteChatItem两个 props,Skill Preview 改走通用 stop/delete;
  6. App-only 功能全部从appId判断改为features控制;
  7. 外部 history/recently used/router 等状态同步迁到页面层 props 回调,ChatBox 内只保留自身 UI 状态;
  8. 最后扫ChatBox目录内appId/skillId/chatTarget/chatTargetId,确保只剩入口页面或 App-only 能力使用。

最大注意点:onStartChat不是 feature,也不是标准 CRUD,先保留

六、权限设计:source-aware 鉴权入口

新增标准 chat target 鉴权入口(测试见 projects/app/test/service/support/permission/auth/chat.test.ts):

type AuthChatTargetParams = { sourceType: ChatSourceTypeEnum; sourceId: string; chatId?: string; };

规则:

  • sourceType=app:复用现有authChatCrud/authApp
  • sourceType=skillEdit:走authSkill,并在传入chatId时用source-aware 查询校验 chat 属于当前 skill 和团队(团队不匹配必须拒绝,对应测试用例);
  • outLink、share、team domain 等 App 专属入口保持sourceType=app

所有 chat 存在性校验必须使用 source-aware 查询,禁止裸查{ appId, chatId },否则 Skill 会话会被误判为 App 会话,或在新索引启用前命中错误记录。

七、数据模型:三表字段演进

chats

新增sourceType字段,第一阶段不设置required: true,也不设置 schema default。原因有三:

  • 历史 App 数据缺失sourceType
  • 新写入必须通过buildChatSourceWriteFields显式带sourceType,不能让漏传在 Mongoose 层静默默认成 App;
  • 待可选回填完成后,再评估是否收紧 schema 校验。

在 packages/global/core/chat/type.ts 的ChatSchema中,appId保留为物理字段名,但 meta 注释明确说明其业务语义是sourceId(可能是 appId 或 skillId);sourceType的 meta 说明"旧数据可能缺失,业务查询层按 app 兼容"。注意这里的 z.default 仅作用于类型层空值归一,Mongo schema 层不设 default,配合source.ts测试断言"sourceType可缺失但无默认值,防止新写入漏传时被静默归为 App"。

chatitems

新增同样的sourceType字段。Human/AI 占位写入、AI 消息更新、软删除、反馈、记录读取都必须带 source-aware 条件。ChatItemDBSchema中同样保留物理appId字段(见 type.ts)。

chat_item_responses

新增同样的sourceType字段。createWorkflowEntryNodeResponseWriter写入和读取都接收sourceType/sourceId,避免 App 与 Skill Edit 在极端 ID 碰撞时串数据(实现见 packages/service/core/chat/nodeResponseStorage.ts)。ChatItemResponseSchema位于 type.ts。

usages

新增skillId字段。写入规则:

  • App chat:写usage.appId,不写usage.skillId
  • Skill Edit chat:写usage.skillId,不写usage.appId

usagesusage_items是计费审计数据,不能随 chat 删除

app_chat_logs

不新增sourceType。该表语义是 App 统计日志,Skill Edit 不写入。app_chat_logs不纳入统一 chat 资源删除函数,由 App 删除流程自行处理。

八、Workflow Runtime:runningAppInfo 收敛

runningAppInfo不保留 deprecated 的idsandboxId字段,最终结构为:

type RunningAppInfo = { sourceType: ChatSourceTypeEnum; sourceId: string; teamId: string; tmbId: string; name: string; isChildApp?: boolean; };

使用规则:

  • chat 持久化:使用sourceType/sourceId
  • 计费:App 写usage.appId=sourceId,Skill Edit 写usage.skillId=sourceId
  • nodeResponse:使用sourceType/sourceId写入和读取;
  • stop/resume:使用sourceType/sourceId/chatId作为 Redis namespace;
  • App 专属逻辑只能在sourceType=app时把sourceId当 AppId 使用;
  • Skill Edit 专属逻辑只能在sourceType=skillEdit时把sourceId当 SkillId 使用。

streamAgentSandboxInitStatus不再接收appIdsandboxId,只接收sourceType/sourceId/userId/chatId,内部调用getRunningSandboxId计算实际 sandboxId 后推送状态。相关测试覆盖可见 packages/service/test/core/workflow/workflowStatus.test.ts。

九、Sandbox:id 统一计算

Sandbox id 统一由getRunningSandboxId计算:

function getRunningSandboxId({ sourceType, sourceId, userId, chatId }) { if (sourceType === ChatSourceTypeEnum.app) { return generateSandboxId(sourceId, userId, chatId); } if (sourceType === ChatSourceTypeEnum.skillEdit) { return getEditDebugSandboxId(sourceId); } const exhaustiveCheck: never = sourceType; throw new Error(`Unsupported chat source type: ${exhaustiveCheck}`); }

规则:

  • App chat:generateSandboxId(appId, userId, chatId)
  • Skill Edit:固定getEditDebugSandboxId(skillId)(编辑态沙盒与 Skill 生命周期绑定,不随 chat 变化);
  • ensureAgentSandboxRuntime内部计算 sandboxId,不从runningAppInfo读取;
  • 底层 sandbox schema 如仍叫appId,调用层必须集中封装,避免业务代码把它误认为真实 AppId。

在 packages/global/core/ai/sandbox/constants.ts 中,generateSandboxId的 v2 实现为:${sourceType.toLowerCase()}-${hashStr(${sourceId}-${userId}).slice(0, 16)},即带 sourceType 前缀的稳定物理资源 ID。skillEdit 编辑态实例(SandboxTypeEnum.editDebug)则通过 Skill 删除链路处理。

十、systemVar 与 S3 文件隔离

systemVar

Skill Edit 场景不伪造appId

  • App 场景继续注入appId=sourceId
  • Skill Edit 场景不注入appId
  • 内部运行态可以携带sourceType/sourceId;是否暴露到变量面板另行评估。

S3 文件 key

新上传统一使用 source-aware key:

chat/${sourceType}/${sourceId}/${uid}/${chatId}/${filename}

兼容规则:

  • 旧 App 文件 keychat/${appId}/${uid}/${chatId}/${filename}继续可读;
  • 新 App 文件使用chat/app/${appId}/...
  • 新 Skill Edit 文件使用chat/skillEdit/${skillId}/...
  • 历史 chat item 中保存的旧 key 不重写;
  • legacy key 只允许在sourceType=app的鉴权上下文中通过;
  • Skill Edit 不默认读取 legacy App key。

在文件预览接口/api/core/chat/file/presignChatFileGetUrl上,鉴权时同时校验sourceType/sourceId/uid/chatId与 S3 key 归属,错误chatId预览返回unAuthChat(见 projects/app/test/pages/api/core/chat/file/presignChatFileGetUrl.test.ts)。S3 key 构造与解析测试位于 packages/service/test/common/s3/key.test.ts。

十一、stop/resume 的 Redis key 隔离

stop key:

agent_runtime_stopping:${sourceType}:${sourceId}:${chatId}

stream resume key:

stream:resume:data:${teamId}:${sourceType}:${sourceId}:${chatId} stream:resume:unavailable:${teamId}:${sourceType}:${sourceId}:${chatId} stream:resume:active:${teamId}:${sourceType}:${sourceId}:${chatId}

stop、resume、catchUp、runtime status 的 key 构造必须集中到 helper,禁止各处手写。对应的 key 格式测试在 packages/service/test/core/workflow/workflowStatus.test.ts 中,专门防止 stop key 回退为裸sourceId/chatId

十二、删除与清理:统一 source-aware 删除函数

标准 chat 会话资源统一由deleteChatResourcesBySource处理(实现见 packages/service/core/chat/delete.ts):

  • chats
  • chatitems
  • chat_item_responses
  • chat S3 文件
  • chat 绑定的 sandbox 实例和资源

不纳入统一函数:

  • app_chat_logs:App 日志域,由 App 删除流程处理;
  • usages/usage_items:计费审计域,不能删除;
  • chat_input_guides:App 配置域;
  • HelperBot chat:独立命名空间。

App 删除调用

await deleteSandboxesByAppId(appId); deleteChatResourcesBySource({ sourceType: ChatSourceTypeEnum.app, sourceId: appId, includeLegacyApp: true, deleteSandboxResources: false });

App 删除流程先按 App 维度删除 sandbox,再调用统一 chat 资源删除函数;统一函数此时不再重复删 chat 绑定 sandbox。App 日志仍由 App 删除流程单独删除。

App 日志批量硬删 chat 调用

deleteChatResourcesBySource({ sourceType: ChatSourceTypeEnum.app, sourceId: appId, chatIds, includeLegacyApp: true });

该场景会删除指定 chat 绑定的 App sandbox(对应测试 packages/service/test/core/chat/delete.test.ts 与 projects/app/test/api/core/chat/history/batchDelete.test.ts 确认批量删除 Skill Edit chat 不会误删 App chat sandbox)。

Skill 删除调用

deleteChatResourcesBySource({ sourceType: ChatSourceTypeEnum.skillEdit, sourceId: skillId });

旧 Skill Debug 初始化清理调用

deleteChatResourcesBySource({ sourceType: ChatSourceTypeEnum.skillEdit, sourceId: skillId, legacySkillDebug: true });

legacySkillDebug=true只匹配:

{ appId: skillId, source: ChatSourceEnum.test, sourceType: { $exists: false } }

十三、旧 Skill Debug 数据清理策略

旧 Skill Debug chat不做迁移,初始化阶段一次性硬删。原因:

  • 旧数据缺少sourceType,会和历史 App 兼容逻辑冲突;
  • 旧唯一索引{ appId: 1, chatId: 1 }存在时,旧 Skill row 会挡住新 Skill Edit row;
  • Skill Preview 可能从 localStorage 复用旧chatId,不清理会触发 duplicate key。

清理识别规则:

  1. 扫描agentSkills._id(Skill 数量预计不超过 1000);
  2. 用 skillId 集合匹配 legacy chats:
{ appId: { $in: skillIds }, source: 'test', sourceType: { $exists: false } }
  1. apps._id做审计比对,输出重复 ID 报告;第一阶段不把碰撞作为自动剔除条件;
  2. 按 skillId 和 chat 游标分批硬删 chats/items/responses/S3;Skill Edit 编辑沙盒由 Skill 删除链路处理,不由 legacy chat 清理函数处理;
  3. 脚本支持dry-run、断点续跑和幂等重试

上线后保留一次 duplicate 兜底:Skill Edit 创建 chat 遇到 duplicate 时,如果确认是 legacy Skill Debug row,则清理该单 chat 后重试一次。清理逻辑测试见 packages/service/test/core/chat/legacySkillDebugCleanup.test.ts。

十四、索引与迁移

上线前先创建新索引。

chats新唯一索引:

db.chats.createIndex( { sourceType: 1, appId: 1, chatId: 1 }, { unique: true, name: 'sourceType_1_appId_1_chatId_1' } );

创建前先做重复审计(本地审计通过DUPLICATE_SOURCE_ROWS=0DUPLICATE_LEGACY_APP_ROWS=0)。旧{ appId: 1, chatId: 1 }唯一索引稳定后再删除。

关键风险:如果不回填历史 App 数据,删除旧唯一索引后 DB 不会阻止以下逻辑重复:

{ appId, chatId, sourceType: { $exists: false } } { appId, chatId, sourceType: 'app' }

因此App 写入路径必须先用 source-aware query 命中 legacy row,不能盲插新 App row。这也是为什么buildChatSourceQuery对 App 必须带$or兼容条件——先查询命中已有 legacy row 再做 upsert,避免重复。

chatitemschat_item_responses需要补 source-aware非唯一复合索引,用于记录读取、分页、删除和 nodeResponse 查询。旧索引先保留,用于 legacy App 查询、rollback 和 explain 对比。schema 索引声明测试见 packages/service/test/core/chat/schema.test.ts,它断言 chat 三表sourceType可缺失但无默认值,并覆盖sourceType_1_appId_1_chatId_1唯一索引声明。

十五、风险清单

历史 App 数据漏查

App 查询必须长期兼容sourceType缺失,直到可选回填完成且确认不再需要 legacy 查询。

旧唯一索引删除后的重复写入

删除旧唯一索引前必须确认 App 写入路径不会在同一appId/chatId下创建 legacy row 和sourceType=approw 两份逻辑重复。

usage 归因错误

Skill Edit 只能写usage.skillId,不能写usage.appId。浏览器集成复验中已确认:Skill Preview 会话的chats/chatitems.sourceType=skillEdit、物理appId=skillId,usage 只写skillIdappId为空;App 会话则相反,usage 写appIdskillId=null

runningAppInfo 字段误用

不保留runningAppInfo.idrunningAppInfo.sandboxId。所有运行态调用只使用sourceType/sourceId,sandboxId 统一计算。

S3 或删除串源

新文件 key、预览授权和删除前缀都必须按 source-aware 规则处理。App 删除清理 legacy + new App 前缀,Skill 删除只清理chat/skillEdit/${skillId}

十六、验证体系与测试路径

整个改造有完整的测试与集成验证支撑,主要测试路径如下,可供排查问题时直接复用:

  • OpenAPI target schema:packages/global/test/openapi/core/chat/targetSchema.test.ts——覆盖 App/Skill target transform、必填 target 缺失、appId+skillId同传拒绝、可选 target 缺省/歧义,以及/v1/audio/transcriptionsraw form schema 与 runtime transform 分层;
  • chat 三表 schema 与删除:packages/service/test/core/chat/schema.test.ts、delete.test.ts、legacySkillDebugCleanup.test.ts;
  • chat 主链路:packages/service/test/core/chat/nodeResponseStorage.test.ts、saveChat.test.ts、controller.test.ts、title.test.ts;
  • 文件上传/预览与鉴权:projects/app/test/pages/api/core/chat/file/presignChatFilePostUrl.test.ts、presignChatFileGetUrl.test.ts、projects/app/test/service/support/permission/auth/chat.test.ts;
  • Skill Debug 回归:projects/app/test/api/core/ai/skill/debugChat.test.ts 与debugSession/*系列(list/records/delete/stop/chatItemDelete.permission);
  • sandbox 与 workflow:packages/service/test/core/ai/sandbox/runtime/index.test.ts、packages/service/test/core/workflow/workflowStatus.test.ts。

全量测试结果(文档记录):@fastgpt/global81 个文件、1688 个测试;@fastgpt/app138 个文件、1018 个测试;@fastgpt/service217 个文件、2903 个测试(跳过 2 个外部集成文件、35 个测试),并完成pnpm --filter @fastgpt/app typecheckgit diff --check复核。

浏览器集成复验的关键结论包括:App Chat 标准请求体为 OpenAPI raw{ appId, chatId },Skill Preview 请求体为{ skillId, chatId },均未向前端暴露内部sourceType/sourceId;Skill Preview 下不出现语音、TTS、input guide、ChatBox 内 App 沙盒入口;编辑沙盒 ticket 请求体为{ skillId, chatId: "edit-debug" },keepalive 显示sourceType=skillEdit/sourceId=.../appId=None/chatId=edit-debug;本地 MongoDB 中chats/chatitems/chat_item_responses均写入正确的sourceType,usage 归因正确。

小结

这套sourceType/sourceId双资源模型的核心价值在于:用最小的存储迁移成本(不重命名字段、不回填历史数据),完成了最大的语义隔离收益——App 与 Skill Edit 共享整套标准 Chat 能力,却互不污染 usage、日志、sandbox、S3 与 stop/resume 运行时状态。其设计精髓可归纳为四条原则:对外 API 只暴露appId/skillId业务字段、对内业务层统一sourceType/sourceId、App 查询长期兼容缺失sourceType的 legacy 数据、所有新代码禁止绕过统一 helper 手写查询或 key 构造。对于需要在单套会话存储上承载多种资源类型的类似系统,这套方案具备很高的直接参考价值。

【免费下载链接】FastGPTFastGPT is a knowledge-based platform built on the LLMs, offers a comprehensive suite of out-of-the-box capabilities such as data processing, RAG retrieval, and visual AI workflow orchestration, letting you easily develop and deploy complex question-answering systems without the need for extensive setup or configuration.项目地址: https://gitcode.com/GitHub_Trending/fa/FastGPT

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询