Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ui/src/api/API_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ Workspace、System 资源管理和 System 共享资源均把 `postPromptGenerate
路径为 `/workspace/<workspaceId>/model/<modelId>/prompt_generate` 与 `/system/model/<modelId>/prompt_generate`,
System 接口不依赖工作空间 ID。
均返回原始 SSE Response,由公共生成组件读取。画布的 `PromptGenerate` 通过
`useWorkflowStore(apiType).postPromptGenerate` 调用;三个范围的 Store 适配器分别引用对应的
`useWorkflowStore(resourceScope).postPromptGenerate` 调用;三个范围的 Store 适配器分别引用对应的
Model API,直接转发生成请求,不使用查询缓存或请求去重。

### 知识库维护
Expand Down
6 changes: 3 additions & 3 deletions ui/src/workflow-canvas/WORKFLOW_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,15 +132,15 @@ LogicFlow 的节点拖拽;仅拦截 `mousedown` 无法隔离当前版本的 Po

### 资源查询(`store/`)

`useWorkflowStore(apiType)` 是画布资源接口适配器,不是 Pinia Store。它自动收集 `store/api/*/index.ts`,
`useWorkflowStore(resourceScope)` 是画布资源接口适配器,不是 Pinia Store。它自动收集 `store/api/*/index.ts`,
查询方法按范围、方法与查询参数缓存结果并复用在途请求;`store.force.xxx()` 跳过已完成缓存,仍复用同键在途请求。
`postPromptGenerate` 是流式生成方法,始终直接转发到对应范围的 Model API,不缓存或去重;
通过普通 Store 或 `force` 调用都会发起新请求,返回独立的 Response 流。
模型选项统一调用 `store.getModelListWithShared(query)`,由对应范围 API 查询;Workspace 和
System 资源管理合并普通与共享模型,System 共享范围仅返回标记 `source: 'shared'` 的共享模型。
强制刷新使用 `store.force.getModelListWithShared(query)`。动态表单配置器通过
`getSelectModelList` 注入使用该能力,循环体转发同名注入。
主画布从 `resourceScope` 注入读取范围(默认 `workspace`),通过节点上下文传递为 `apiType`。
主画布从 `resourceScope` 注入读取范围(默认 `workspace`),通过节点上下文传递为 `resourceScope`。

当前 `workspace` 适配模型、供应商、模型参数、MCP 工具、共享工具选项、工具详情和标签查询;
`system-resource` 与 `system-shared` 独立提供系统模型、供应商、参数表单、工具选项与详情、MCP 和标签查询,
Expand Down Expand Up @@ -329,7 +329,7 @@ AI 对话节点的提示词、历史记录、视觉理解和输出思考表单
自定义模型使用节点配置的模型 ID,引用变量无法在编辑时确定模型,生成入口禁用。
图片理解和视频理解使用 IMAGE 模型,AI 对话使用 LLM 模型,现有提示词接口支持这两种类型。
共享组件维护提示词模板和请求上下文,不依赖某个节点,也不接收未使用的模型或供应商选项。
组件将画布注入的 `apiType` 传给 `useWorkflowStore`,通过 `store.postPromptGenerate` 发起生成;
组件将画布注入的 `resourceScope` 传给 `useWorkflowStore`,通过 `store.postPromptGenerate` 发起生成;
Store 适配器统一选择 Workspace、System 资源管理或 System 共享资源的 Model API。
循环画布已转发同一上下文,组件不再单独判断资源范围。生成方法不使用 Store 的查询缓存或请求去重。
生成接口不再依赖应用,改用节点自身已保存的模型参数(`model_params_setting`)并随请求体提交,
Expand Down
6 changes: 3 additions & 3 deletions ui/src/workflow-canvas/component/PromptGenerate.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ const props = defineProps<{
}>()
const emit = defineEmits<{ replace: [prompt: string] }>()

// 资源范围沿用画布传递的 apiType,循环内节点也使用同一上下文。
const apiType = inject<string>('apiType', 'workspace')
const store = useWorkflowStore(apiType)
// 资源范围沿用画布传递的 resourceScope,循环内节点也使用同一上下文。
const resourceScope = inject<string>('resourceScope', 'workspace')
const store = useWorkflowStore(resourceScope)

// 提示词模板:约束生成结果的角色结构与内容范围。
const PROMPT_TEMPLATE = `请根据用户描述生成一个完整的AI角色人设模板:
Expand Down
4 changes: 2 additions & 2 deletions ui/src/workflow-canvas/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import NodeControl from './component/NodeControl.vue'
defineOptions({ name: 'WorkflowCanvas' })

type CanvasWorkflowNodeModel = WorkflowNodeModel & { set_loop_body?: () => void }
const apiType = inject('resourceScope', 'workspace')
const resourceScope = inject('resourceScope', 'workspace')
const props = withDefaults(
defineProps<{
data?: LogicFlow.GraphConfigData | null
Expand Down Expand Up @@ -83,7 +83,7 @@ function renderGraphData(data: LogicFlow.GraphConfigData = props.data ?? {}) {
lf.value.graphModel.get_provide = (model: LogicFlow.NodeData | null, graph: GraphModel | null) => ({
getModel: () => model,
getGraph: () => graph,
apiType: apiType,
resourceScope: resourceScope,
workflowMode: props.workflowMode,
loopWorkflowMode: props.loopWorkflowMode,
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import type { McpSetting, McpSource } from '../../types'
defineOptions({ name: 'AiChatNodeMcpSettingDialog' })

const emit = defineEmits<{ submit: [setting: McpSetting]; loaded: [tools: ToolItem[]] }>()
const apiType = inject<string>('apiType', 'workspace')
const store = useWorkflowStore(apiType)
const resourceScope = inject<string>('resourceScope', 'workspace')
const store = useWorkflowStore(resourceScope)

const MCP_SERVER_EXAMPLE = `{
"math": {
Expand Down
4 changes: 2 additions & 2 deletions ui/src/workflow-canvas/nodes/ai-chat-node/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ import { fileTooltip } from '@/workflow-canvas/config/constants'
defineOptions({ name: 'WorkflowAiChatNode' })

const getModel = inject('getModel') as () => WorkflowNodeModel
const apiType = (inject('apiType') as string) || 'workspace'
const resourceScope = (inject('resourceScope') as string) || 'workspace'
const workflowMode = inject<WorkflowMode>('workflowMode', WorkflowMode.Application)
const model = getModel()
const store = useWorkflowStore(apiType)
const store = useWorkflowStore(resourceScope)

const formRef = useTemplateRef<FormInstance>('formRef')

Expand Down
4 changes: 2 additions & 2 deletions ui/src/workflow-canvas/nodes/base-node/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ import {
defineOptions({ name: 'WorkflowBaseNode' })

const getModel = inject('getModel') as () => WorkflowNodeModel
const apiType = (inject('apiType') as string) || 'workspace'
const resourceScope = (inject('resourceScope') as string) || 'workspace'

const model = getModel()
const store = useWorkflowStore(apiType)
const store = useWorkflowStore(resourceScope)

// 为画布节点中的 ModelSelect 提供参数表单接口。
provide('getModelParamsForm', store.force.getModelParamsForm)
Expand Down
4 changes: 2 additions & 2 deletions ui/src/workflow-canvas/nodes/form-node/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import { handleNodeWheel } from '@/workflow-canvas/core/utils'
import FormSettingTable from './component/form-setting/FormSettingTable.vue'
import { useWorkflowStore } from '@/workflow-canvas/store'
defineOptions({ name: 'WorkflowFormNode' })
const apiType = (inject('apiType') as string) || 'workspace'
const store = useWorkflowStore(apiType)
const resourceScope = (inject('resourceScope') as string) || 'workspace'
const store = useWorkflowStore(resourceScope)
const getModel = inject<() => WorkflowNodeModel>('getModel')!
const model = getModel()
// 为画布节点中的 ModelSelect 提供参数表单接口。
Expand Down
4 changes: 2 additions & 2 deletions ui/src/workflow-canvas/nodes/image-generate-node/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { WorkflowMode } from '@/workflow-canvas/types'

defineOptions({ name: 'WorkflowImageGenerateNode' })
const getModel = inject('getModel') as () => WorkflowNodeModel
const apiType = (inject('apiType') as string) || 'workspace'
const resourceScope = (inject('resourceScope') as string) || 'workspace'
const workflowMode = inject<WorkflowMode>('workflowMode', WorkflowMode.Application)
const model = getModel()

Expand All @@ -29,7 +29,7 @@ interface ImageGenerateNodeForm {

const formRef = useTemplateRef<FormInstance>('formRef')

const store = useWorkflowStore(apiType)
const store = useWorkflowStore(resourceScope)
const modelList = ref<Array<ModelItem>>([])
const providerOptions = ref<Array<ModelProviderItem>>([])

Expand Down
4 changes: 2 additions & 2 deletions ui/src/workflow-canvas/nodes/image-to-video-node/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { WorkflowMode } from '@/workflow-canvas/types'

defineOptions({ name: 'WorkflowImageToVideoNode' })
const getModel = inject('getModel') as () => WorkflowNodeModel
const apiType = (inject('apiType') as string) || 'workspace'
const resourceScope = (inject('resourceScope') as string) || 'workspace'
const workflowMode = inject<WorkflowMode>('workflowMode', WorkflowMode.Application)
const model = getModel()

Expand All @@ -33,7 +33,7 @@ interface ImageToVideoNodeForm {

const formRef = useTemplateRef<FormInstance>('formRef')

const store = useWorkflowStore(apiType)
const store = useWorkflowStore(resourceScope)
const modelList = ref<Array<ModelItem>>([])
const providerOptions = ref<Array<ModelProviderItem>>([])

Expand Down
4 changes: 2 additions & 2 deletions ui/src/workflow-canvas/nodes/image-understand-node/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { WorkflowMode, type ReasoningSettingData } from '@/workflow-canvas/types

defineOptions({ name: 'WorkflowImageUnderstandNode' })
const getModel = inject('getModel') as () => WorkflowNodeModel
const apiType = (inject('apiType') as string) || 'workspace'
const resourceScope = (inject('resourceScope') as string) || 'workspace'
const workflowMode = inject<WorkflowMode>('workflowMode', WorkflowMode.Application)
const model = getModel()

Expand All @@ -36,7 +36,7 @@ interface ImageUnderstandNodeForm {

const formRef = useTemplateRef<FormInstance>('formRef')

const store = useWorkflowStore(apiType)
const store = useWorkflowStore(resourceScope)
const modelList = ref<Array<ModelItem>>([])
const providerOptions = ref<Array<ModelProviderItem>>([])

Expand Down
4 changes: 2 additions & 2 deletions ui/src/workflow-canvas/nodes/intent-node/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { randomId } from '@/utils/common'

defineOptions({ name: 'WorkflowIntentNode' })
const getModel = inject('getModel') as () => WorkflowNodeModel
const apiType = (inject('apiType') as string) || 'workspace'
const resourceScope = (inject('resourceScope') as string) || 'workspace'
const model = getModel()
const flowModel = model as unknown as { refreshBranch?: () => void }

Expand All @@ -35,7 +35,7 @@ type IntentModelSetting = Pick<IntentNodeForm, 'model_id' | 'model_id_reference'

const formRef = useTemplateRef<FormInstance>('formRef')

const store = useWorkflowStore(apiType)
const store = useWorkflowStore(resourceScope)
const modelList = ref<Array<ModelItem>>([])
const providerOptions = ref<Array<ModelProviderItem>>([])

Expand Down
4 changes: 2 additions & 2 deletions ui/src/workflow-canvas/nodes/loop-body-node/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const getModel = inject('getModel') as () => BaseNodeModel
const model = getModel()

const parentLoopWorkflowMode = inject<WorkflowMode>('loopWorkflowMode') || WorkflowMode.ApplicationLoop
const apiType = (inject('apiType') as string) || 'workspace'
const resourceScope = (inject('resourceScope') as string) || 'workspace'
const currentResource = inject<unknown>('currentResource')
const getSelectModelList = inject<(params: { model_type: string }) => Promise<ModelItem[]>>('getSelectModelList')
const modelParamsForm = inject<(modelId: string) => unknown>('getModelParamsForm')
Expand Down Expand Up @@ -133,7 +133,7 @@ const renderGraphData = (data?: LogicFlow.GraphConfigData) => {
workflowMode: parentLoopWorkflowMode,
loopWorkflowMode: parentLoopWorkflowMode,
currentResource,
apiType,
resourceScope,
getSelectModelList,
getModelParamsForm: modelParamsForm,
startDragNode: nestedStartDrag,
Expand Down
4 changes: 2 additions & 2 deletions ui/src/workflow-canvas/nodes/mcp-node/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { MsgError, MsgSuccess } from '@/utils/message'

defineOptions({ name: 'WorkflowMcpNode' })
const getModel = inject('getModel') as () => BaseNodeModel
const apiType = (inject('apiType') as string) || 'workspace'
const resourceScope = (inject('resourceScope') as string) || 'workspace'
const workflowMode = inject<WorkflowMode>('workflowMode', WorkflowMode.Application)
const model = getModel() as WorkflowNodeModel
const route = useRoute()
Expand All @@ -37,7 +37,7 @@ const mcpResource = MCP_RESOURCE[workflowMode] ?? MCP_RESOURCE[WorkflowMode.Appl
const mcpResourceType = mcpResource.type
const mcpResourceId = mcpResource.id() || ''

const store = useWorkflowStore(apiType)
const store = useWorkflowStore(resourceScope)

interface SchemaProperty {
description?: string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import type { ParameterField } from './component/parameters-field/types'

defineOptions({ name: 'WorkflowParameterExtractionNode' })
const getModel = inject('getModel') as () => BaseNodeModel
const apiType = (inject('apiType') as string) || 'workspace'
const resourceScope = (inject('resourceScope') as string) || 'workspace'
const model = getModel() as WorkflowNodeModel

interface ParameterExtractionForm {
Expand All @@ -28,7 +28,7 @@ interface ParameterExtractionForm {

const parameterExtractionFormRef = useTemplateRef<FormInstance>('parameterExtractionFormRef')

const store = useWorkflowStore(apiType)
const store = useWorkflowStore(resourceScope)
const modelList = ref<Array<ModelItem>>([])
const providerOptions = ref<Array<ModelProviderItem>>([])

Expand Down
4 changes: 2 additions & 2 deletions ui/src/workflow-canvas/nodes/question-node/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { WorkflowMode } from '@/workflow-canvas/types'

defineOptions({ name: 'WorkflowQuestionNode' })
const getModel = inject('getModel') as () => WorkflowNodeModel
const apiType = (inject('apiType') as string) || 'workspace'
const resourceScope = (inject('resourceScope') as string) || 'workspace'
const workflowMode = inject<WorkflowMode>('workflowMode', WorkflowMode.Application)
const model = getModel()

Expand All @@ -30,7 +30,7 @@ interface QuestionNodeForm {

const formRef = useTemplateRef<FormInstance>('formRef')

const store = useWorkflowStore(apiType)
const store = useWorkflowStore(resourceScope)
const modelList = ref<Array<ModelItem>>([])
const providerOptions = ref<Array<ModelProviderItem>>([])

Expand Down
4 changes: 2 additions & 2 deletions ui/src/workflow-canvas/nodes/reranker-node/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ import type { RerankerSetting } from './types'

defineOptions({ name: 'WorkflowRerankerNode' })
const getModel = inject('getModel') as () => WorkflowNodeModel
const apiType = inject<string>('apiType', 'workspace')
const resourceScope = inject<string>('resourceScope', 'workspace')
const model = getModel()
const store = useWorkflowStore(apiType)
const store = useWorkflowStore(resourceScope)
const formRef = useTemplateRef<FormInstance>('formRef')

interface RerankerForm {
Expand Down
4 changes: 2 additions & 2 deletions ui/src/workflow-canvas/nodes/search-document-node/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ import type { SearchDocumentForm } from './types'

defineOptions({ name: 'WorkflowSearchDocumentNode' })
const getModel = inject('getModel') as () => WorkflowNodeModel
const apiType = (inject('apiType') as string) || 'workspace'
const store = useWorkflowStore(apiType)
const resourceScope = (inject('resourceScope') as string) || 'workspace'
const store = useWorkflowStore(resourceScope)
const model = getModel()
const formRef = useTemplateRef<FormInstance>('formRef')
const questionCascaderRef = useTemplateRef<InstanceType<typeof NodeCascader>>('questionCascaderRef')
Expand Down
4 changes: 2 additions & 2 deletions ui/src/workflow-canvas/nodes/speech-to-text-node/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import type { ModelItem, ModelProviderItem } from '@/api/types'

defineOptions({ name: 'WorkflowSpeechToTextNode' })
const getModel = inject('getModel') as () => WorkflowNodeModel
const apiType = (inject('apiType') as string) || 'workspace'
const resourceScope = (inject('resourceScope') as string) || 'workspace'
const workflowMode = inject<WorkflowMode>('workflowMode', WorkflowMode.Application)
const model = getModel()

Expand All @@ -28,7 +28,7 @@ interface SpeechToTextNodeForm {

const formRef = useTemplateRef<FormInstance>('formRef')

const store = useWorkflowStore(apiType)
const store = useWorkflowStore(resourceScope)
const modelList = ref<Array<ModelItem>>([])
const providerOptions = ref<Array<ModelProviderItem>>([])

Expand Down
4 changes: 2 additions & 2 deletions ui/src/workflow-canvas/nodes/text-to-speech-node/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import type { ModelItem, ModelProviderItem } from '@/api/types'

defineOptions({ name: 'WorkflowTextToSpeechNode' })
const getModel = inject('getModel') as () => WorkflowNodeModel
const apiType = (inject('apiType') as string) || 'workspace'
const resourceScope = (inject('resourceScope') as string) || 'workspace'
const workflowMode = inject<WorkflowMode>('workflowMode', WorkflowMode.Application)
const model = getModel()

Expand All @@ -27,7 +27,7 @@ interface TextToSpeechNodeForm {

const formRef = useTemplateRef<FormInstance>('formRef')

const store = useWorkflowStore(apiType)
const store = useWorkflowStore(resourceScope)
const modelList = ref<Array<ModelItem>>([])
const providerOptions = ref<Array<ModelProviderItem>>([])

Expand Down
4 changes: 2 additions & 2 deletions ui/src/workflow-canvas/nodes/text-to-video-node/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { WorkflowMode } from '@/workflow-canvas/types'

defineOptions({ name: 'WorkflowTextToVideoNode' })
const getModel = inject('getModel') as () => WorkflowNodeModel
const apiType = (inject('apiType') as string) || 'workspace'
const resourceScope = (inject('resourceScope') as string) || 'workspace'
const workflowMode = inject<WorkflowMode>('workflowMode', WorkflowMode.Application)
const model = getModel()

Expand All @@ -29,7 +29,7 @@ interface TextToVideoNodeForm {

const formRef = useTemplateRef<FormInstance>('formRef')

const store = useWorkflowStore(apiType)
const store = useWorkflowStore(resourceScope)
const modelList = ref<Array<ModelItem>>([])
const providerOptions = ref<Array<ModelProviderItem>>([])

Expand Down
2 changes: 1 addition & 1 deletion ui/src/workflow-canvas/nodes/tool-lib-node/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ interface ToolLibNodeForm {
const getModel = inject('getModel') as () => WorkflowNodeModel
const workflowMode = inject<WorkflowMode>('workflowMode', WorkflowMode.Application)
const model = getModel()
const store = useWorkflowStore(inject<string>('apiType', 'workspace'))
const store = useWorkflowStore(inject<string>('resourceScope', 'workspace'))

const formRef = useTemplateRef<FormInstance>('formRef')

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ interface ToolBaseProperties {

const getModel = inject<() => WorkflowNodeModel>('getModel')!
const workflowMode = inject<WorkflowMode>('workflowMode', WorkflowMode.Application)
const store = useWorkflowStore(inject<string>('apiType', 'workspace'))
const store = useWorkflowStore(inject<string>('resourceScope', 'workspace'))
const model = getModel()
const formRef = useTemplateRef<FormInstance>('formRef')
const anchorGuard = createAnchorGuard(model)
Expand Down
4 changes: 2 additions & 2 deletions ui/src/workflow-canvas/nodes/video-understand-node/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { WorkflowMode, type ReasoningSettingData } from '@/workflow-canvas/types

defineOptions({ name: 'WorkflowVideoUnderstandNode' })
const getModel = inject('getModel') as () => WorkflowNodeModel
const apiType = (inject('apiType') as string) || 'workspace'
const resourceScope = (inject('resourceScope') as string) || 'workspace'
const workflowMode = inject<WorkflowMode>('workflowMode', WorkflowMode.Application)
const model = getModel()

Expand All @@ -36,7 +36,7 @@ interface VideoUnderstandNodeForm {

const formRef = useTemplateRef<FormInstance>('formRef')

const store = useWorkflowStore(apiType)
const store = useWorkflowStore(resourceScope)
const modelList = ref<Array<ModelItem>>([])
const providerOptions = ref<Array<ModelProviderItem>>([])

Expand Down
14 changes: 7 additions & 7 deletions ui/src/workflow-canvas/store/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,18 +47,18 @@ const apiMap = Object.fromEntries(
const cacheMap = new Map<string, Map<string, unknown>>()
const pendingMap = new Map<string, Map<string, Promise<unknown>>>()

export function useWorkflowStore(apiType: string): WorkflowStore {
const api: ApiModule | undefined = apiMap[apiType]
export function useWorkflowStore(resourceScope: string): WorkflowStore {
const api: ApiModule | undefined = apiMap[resourceScope]
if (!api) {
throw new Error(`[useWorkflowStore] unknown apiType: "${apiType}"`)
throw new Error(`[useWorkflowStore] unknown resourceScope: "${resourceScope}"`)
}
const requestApi: ApiModule = api

const cache = cacheMap.get(apiType) ?? new Map<string, unknown>()
cacheMap.set(apiType, cache)
const cache = cacheMap.get(resourceScope) ?? new Map<string, unknown>()
cacheMap.set(resourceScope, cache)

const pending = pendingMap.get(apiType) ?? new Map<string, Promise<unknown>>()
pendingMap.set(apiType, pending)
const pending = pendingMap.get(resourceScope) ?? new Map<string, Promise<unknown>>()
pendingMap.set(resourceScope, pending)

// 统一的缓存 + 请求去重逻辑。force 时跳过缓存读取并覆盖缓存,但仍复用在途请求以避免重复触发。
function withCache<T>(key: string, fetcher: () => Promise<T>, force = false): Promise<T> {
Expand Down
Loading