vue-agent-start 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/package.json +30 -0
  2. package/pnpm-workspace.yaml +3 -0
  3. package/src/agent-flow/adapter/backend.ts +99 -0
  4. package/src/agent-flow/components/ChatIframePanel.vue +773 -0
  5. package/src/agent-flow/components/DatasetItemCard.vue +313 -0
  6. package/src/agent-flow/components/MemoryWindow.vue +104 -0
  7. package/src/agent-flow/components/PromptEditor.vue +804 -0
  8. package/src/agent-flow/components/PromptEditorTagPanel.vue +460 -0
  9. package/src/agent-flow/components/SvgIcon.vue +32 -0
  10. package/src/agent-flow/components/ToolItemCard.vue +169 -0
  11. package/src/agent-flow/components/VariableCard.vue +161 -0
  12. package/src/agent-flow/components/VariableSelector.vue +162 -0
  13. package/src/agent-flow/components/chat-iframe-types.ts +35 -0
  14. package/src/agent-flow/components/stubs/DatasetChooser.vue +70 -0
  15. package/src/agent-flow/components/stubs/Embedding.vue +22 -0
  16. package/src/agent-flow/components/stubs/ModelSelect.vue +24 -0
  17. package/src/agent-flow/components/stubs/RestfulSelectCard.vue +24 -0
  18. package/src/agent-flow/components/stubs/ToolChooser.vue +96 -0
  19. package/src/agent-flow/components/stubs/TriggersItemCard.vue +126 -0
  20. package/src/agent-flow/components/stubs/VariableModal.vue +115 -0
  21. package/src/agent-flow/index.ts +73 -0
  22. package/src/agent-flow/stores/workflow.ts +72 -0
  23. package/src/agent-flow/utils/langUtils.ts +173 -0
  24. package/src/agent-flow/workflow/Ability.vue +65 -0
  25. package/src/agent-flow/workflow/AuthorizationItem.vue +101 -0
  26. package/src/agent-flow/workflow/ConversationVar.vue +42 -0
  27. package/src/agent-flow/workflow/CustomEdge.vue +94 -0
  28. package/src/agent-flow/workflow/EnvironmentVar.vue +40 -0
  29. package/src/agent-flow/workflow/FlowDesigner.vue +2802 -0
  30. package/src/agent-flow/workflow/Icon.vue +179 -0
  31. package/src/agent-flow/workflow/NodeConfig.vue +509 -0
  32. package/src/agent-flow/workflow/NodeConfigCard.vue +460 -0
  33. package/src/agent-flow/workflow/NodeHandle.vue +147 -0
  34. package/src/agent-flow/workflow/NodeHeaderToolbar.vue +77 -0
  35. package/src/agent-flow/workflow/NodeHoverToolbar.vue +176 -0
  36. package/src/agent-flow/workflow/OutputItemCard.vue +668 -0
  37. package/src/agent-flow/workflow/ParamTableItem.vue +200 -0
  38. package/src/agent-flow/workflow/StructTable.vue +202 -0
  39. package/src/agent-flow/workflow/VarInsertField.vue +517 -0
  40. package/src/agent-flow/workflow/VarSelectField.vue +223 -0
  41. package/src/agent-flow/workflow/VariableSelectorItem.vue +48 -0
  42. package/src/agent-flow/workflow/WfField.vue +206 -0
  43. package/src/agent-flow/workflow/_config-base.css +470 -0
  44. package/src/agent-flow/workflow/composables/useNode.js +55 -0
  45. package/src/agent-flow/workflow/index.vue +53 -0
  46. package/src/agent-flow/workflow/nodeCard/AgentNodeCard.vue +180 -0
  47. package/src/agent-flow/workflow/nodeCard/AnswerNodeCard.vue +65 -0
  48. package/src/agent-flow/workflow/nodeCard/ClassifierCard.vue +152 -0
  49. package/src/agent-flow/workflow/nodeCard/CodeNodeCard.vue +139 -0
  50. package/src/agent-flow/workflow/nodeCard/ConditionNodeCard.vue +323 -0
  51. package/src/agent-flow/workflow/nodeCard/DocumentExtractorCard.vue +83 -0
  52. package/src/agent-flow/workflow/nodeCard/EndNodeCard.vue +123 -0
  53. package/src/agent-flow/workflow/nodeCard/HttpNodeCard.vue +451 -0
  54. package/src/agent-flow/workflow/nodeCard/HumanInputCard.vue +152 -0
  55. package/src/agent-flow/workflow/nodeCard/IterationCard.vue +66 -0
  56. package/src/agent-flow/workflow/nodeCard/KnowledgeRetrievalCard.vue +48 -0
  57. package/src/agent-flow/workflow/nodeCard/LLMNodeCard.vue +206 -0
  58. package/src/agent-flow/workflow/nodeCard/ListOperatorCard.vue +188 -0
  59. package/src/agent-flow/workflow/nodeCard/ParameterExtractorCard.vue +352 -0
  60. package/src/agent-flow/workflow/nodeCard/ServiceApiNodeCard.vue +188 -0
  61. package/src/agent-flow/workflow/nodeCard/StartNodeCard.vue +216 -0
  62. package/src/agent-flow/workflow/nodeCard/VariableAssignerCard.vue +125 -0
  63. package/src/agent-flow/workflow/nodeCard/VariableNodeCard.vue +121 -0
  64. package/src/agent-flow/workflow/nodes/AgentNode.vue +122 -0
  65. package/src/agent-flow/workflow/nodes/AnswerNode.vue +99 -0
  66. package/src/agent-flow/workflow/nodes/ClassifierNode.vue +199 -0
  67. package/src/agent-flow/workflow/nodes/CodeNode.vue +152 -0
  68. package/src/agent-flow/workflow/nodes/ConditionNode.vue +232 -0
  69. package/src/agent-flow/workflow/nodes/DocumentExtractorNode.vue +96 -0
  70. package/src/agent-flow/workflow/nodes/EndNode.vue +119 -0
  71. package/src/agent-flow/workflow/nodes/FileUploadNode.vue +97 -0
  72. package/src/agent-flow/workflow/nodes/HttpNode.vue +204 -0
  73. package/src/agent-flow/workflow/nodes/HumanInputNode.vue +110 -0
  74. package/src/agent-flow/workflow/nodes/IterationNode.vue +98 -0
  75. package/src/agent-flow/workflow/nodes/KnowledgeRetrievalNode.vue +253 -0
  76. package/src/agent-flow/workflow/nodes/LLMNode.vue +134 -0
  77. package/src/agent-flow/workflow/nodes/ListOperatorNode.vue +111 -0
  78. package/src/agent-flow/workflow/nodes/LoopNode.vue +94 -0
  79. package/src/agent-flow/workflow/nodes/ParameterExtractorNode.vue +147 -0
  80. package/src/agent-flow/workflow/nodes/ServiceApiNode.vue +161 -0
  81. package/src/agent-flow/workflow/nodes/StartNode.vue +199 -0
  82. package/src/agent-flow/workflow/nodes/TemplateNode.vue +103 -0
  83. package/src/agent-flow/workflow/nodes/UserInputNode.vue +92 -0
  84. package/src/agent-flow/workflow/nodes/VariableAssignerNode.vue +145 -0
  85. package/src/agent-flow/workflow/nodes/VariableNode.vue +130 -0
  86. package/src/agent-flow/workflow/nodes/_node-base.css +338 -0
  87. package/src/agent-flow/workflow/utils/node_card_form.ts +335 -0
  88. package/src/agent-flow/workflow/utils/node_config.ts +63 -0
  89. package/src/agent-flow/workflow/utils/workflow_utils.ts +151 -0
  90. package/src/agent-studio/adapters/springAgentStart.ts +390 -0
  91. package/src/agent-studio/api/index.ts +1 -0
  92. package/src/agent-studio/api/types.ts +173 -0
  93. package/src/agent-studio/components/AgentApiDocs.vue +500 -0
  94. package/src/agent-studio/components/AgentAppsPage.vue +1601 -0
  95. package/src/agent-studio/components/AgentCardGrid.vue +259 -0
  96. package/src/agent-studio/components/AgentChatPage.vue +553 -0
  97. package/src/agent-studio/components/AgentDebugPanel.vue +350 -0
  98. package/src/agent-studio/components/AgentLogsPanel.vue +391 -0
  99. package/src/agent-studio/components/AgentMonitorPanel.vue +204 -0
  100. package/src/agent-studio/components/AgentOrchestrate.vue +505 -0
  101. package/src/agent-studio/components/AgentPromptEditor.vue +237 -0
  102. package/src/agent-studio/components/AgentStudioShell.vue +227 -0
  103. package/src/agent-studio/components/AgentToolsPanel.vue +394 -0
  104. package/src/agent-studio/components/AgentVariablesPanel.vue +205 -0
  105. package/src/agent-studio/components/ApiKeyManager.vue +251 -0
  106. package/src/agent-studio/components/AppDesignDrawer.vue +3391 -0
  107. package/src/agent-studio/components/CreateAppModal.vue +737 -0
  108. package/src/agent-studio/components/SparkChart.vue +200 -0
  109. package/src/agent-studio/components/VariableEditorModal.vue +499 -0
  110. package/src/agent-studio/composables/useAgentStudio.ts +347 -0
  111. package/src/agent-studio/index.ts +60 -0
  112. package/src/agent-studio/panels/DrawerFlowDesigner.vue +203 -0
  113. package/src/agent-studio/panels/LogAnnotationPanel.vue +490 -0
  114. package/src/agent-studio/panels/MonitorPanel.vue +535 -0
  115. package/src/agent-studio/types.ts +242 -0
  116. package/src/index.ts +155 -0
  117. package/src/knowledge-hub/adapters/springAgentStart.ts +472 -0
  118. package/src/knowledge-hub/components/AddDocumentsWizard.vue +1452 -0
  119. package/src/knowledge-hub/components/CreateDatasetWizard.vue +1723 -0
  120. package/src/knowledge-hub/components/DatasetCardGrid.vue +467 -0
  121. package/src/knowledge-hub/components/DatasetDetailDrawer.vue +888 -0
  122. package/src/knowledge-hub/components/DatasetPicker.vue +65 -0
  123. package/src/knowledge-hub/components/DatasetPickerModal.vue +526 -0
  124. package/src/knowledge-hub/components/DatasetSettingsPanel.vue +908 -0
  125. package/src/knowledge-hub/components/DatasetSidebar.vue +781 -0
  126. package/src/knowledge-hub/components/DocumentChunksView.vue +802 -0
  127. package/src/knowledge-hub/components/DocumentTable.vue +552 -0
  128. package/src/knowledge-hub/components/HitTestingPanel.vue +74 -0
  129. package/src/knowledge-hub/components/KnowledgeApp.vue +66 -0
  130. package/src/knowledge-hub/components/KnowledgeHubApp.vue +504 -0
  131. package/src/knowledge-hub/components/RecallTestingPanelV2.vue +515 -0
  132. package/src/knowledge-hub/components/RetrievalConfigPopover.vue +270 -0
  133. package/src/knowledge-hub/components/RetrievalMethodPicker.vue +547 -0
  134. package/src/knowledge-hub/components/RetrievedList.vue +24 -0
  135. package/src/knowledge-hub/components/internal/KhDialog.vue +195 -0
  136. package/src/knowledge-hub/composables/useKnowledge.ts +89 -0
  137. package/src/knowledge-hub/i18n/index.ts +2 -0
  138. package/src/knowledge-hub/i18n/messages.ts +422 -0
  139. package/src/knowledge-hub/i18n/useKhI18n.ts +107 -0
  140. package/src/knowledge-hub/index.ts +46 -0
  141. package/src/knowledge-hub/styles/index.css +4 -0
  142. package/src/knowledge-hub/styles/tokens.css +138 -0
  143. package/src/knowledge-hub/types/api.ts +213 -0
  144. package/src/knowledge-hub/types/dataset.ts +88 -0
  145. package/src/knowledge-hub/types/document.ts +45 -0
  146. package/src/knowledge-hub/types/index.ts +9 -0
  147. package/src/knowledge-hub/types/retrieval.ts +46 -0
  148. package/src/provider-hub/components/CredentialForm.vue +103 -0
  149. package/src/provider-hub/components/DefaultModelsPanel.vue +270 -0
  150. package/src/provider-hub/components/GroupedModelSelect.vue +445 -0
  151. package/src/provider-hub/components/ModelCardGrid.vue +332 -0
  152. package/src/provider-hub/components/ModelParameterDrawer.vue +739 -0
  153. package/src/provider-hub/components/ModelPickerPopover.vue +1282 -0
  154. package/src/provider-hub/components/ProviderApp.vue +159 -0
  155. package/src/provider-hub/components/ProviderCredentialModal.vue +321 -0
  156. package/src/provider-hub/components/ProviderGallery.vue +131 -0
  157. package/src/provider-hub/components/ProviderHubShell.vue +1992 -0
  158. package/src/provider-hub/components/ProviderIcon.vue +130 -0
  159. package/src/provider-hub/composables/useProviderHub.ts +358 -0
  160. package/src/provider-hub/index.ts +43 -0
  161. package/src/provider-hub/svg/MiniMax.svg +1 -0
  162. package/src/provider-hub/svg/Volcengine.svg +1 -0
  163. package/src/provider-hub/svg/Zhipu.svg +1 -0
  164. package/src/provider-hub/svg/deepseek.svg +1 -0
  165. package/src/provider-hub/svg/moonshot.svg +1 -0
  166. package/src/provider-hub/svg/openai.svg +1 -0
  167. package/src/provider-hub/svg/qwen.svg +1 -0
  168. package/src/provider-hub/svg/siliconflow.svg +1 -0
  169. package/src/provider-hub/types.ts +256 -0
@@ -0,0 +1,1282 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * ModelPickerPopover — 通用「模型设置」面板。
4
+ *
5
+ * 一个组件同时服务三处:
6
+ * 1. 应用设计右上角的模型选择(agent-studio)
7
+ * 2. 工作流 LLM / Agent / QuestionClassifier / ParameterExtractor 节点的
8
+ * 模型选择(agent-flow)
9
+ * 3. 任何需要「模型 + 参数微调」组合的地方
10
+ *
11
+ * 交互(对齐 Dify 「模型设置」样式):
12
+ *
13
+ * 触发块 —— 一个"普通的块"(非下拉框样式),单行显示 provider icon +
14
+ * 模型名 + provider 标签;点击弹出 popover。调用方可通过 #trigger 插槽
15
+ * 完全自定义外观。
16
+ *
17
+ * Popover 内容(外层"模型设置"面板):
18
+ * ┌─────────────────────────────────────────┐
19
+ * │ 模型设置 × │
20
+ * │ ┌ 🔷 qwen3.7-plus [CHAT] ⚙︎ ▼┐ │ ← 模型卡片,点击展开二级下拉
21
+ * │ 参数 │
22
+ * │ ○ 温度 ⓘ ━━━●━━━ [0.3 ] │
23
+ * │ ○ 最大标记 ⓘ ━━━━━━● [8192] │
24
+ * │ ... (更多参数) │
25
+ * └─────────────────────────────────────────┘
26
+ *
27
+ * 二级下拉(点击模型卡片时展开)—— 搜索框 + 分组模型列表;选中一条模型后
28
+ * 只关闭二级下拉,"模型设置"外层面板保持打开,方便继续调参。
29
+ *
30
+ * 数据源默认走 useProviderHub().listModelsGroupedByType(),即 provider-hub
31
+ * 已经暴露的 /models/grouped-by-type — 和"系统默认模型"面板同一份数据,保证
32
+ * 全站分组、启用状态一致。宿主项目也可以通过 `groups` prop 直接注入。
33
+ *
34
+ * v-model 载荷至少 { providerName, modelName, modelType, modelProvider };同
35
+ * 时兼容 workflow 历史 { provider, modelName, mode, completionParams } 形状:
36
+ * 组件读取时两套字段都识别,回填时保留所有原有键 —— 调用方无需做任何形状转
37
+ * 换。历史上把 provider::model::type 合成到 `modelId` 的做法已废弃 —— 组件不
38
+ * 再写入该字段,遇到旧的合成值会顺手清掉,避免节点/应用配置里再出现无意义
39
+ * 的合成 id。
40
+ */
41
+ import { computed, onMounted, reactive, ref, watch } from 'vue';
42
+
43
+ import { Popover, Segmented, Spin } from 'ant-design-vue';
44
+ import {
45
+ CheckOutlined,
46
+ CloseOutlined,
47
+ DownOutlined,
48
+ QuestionCircleOutlined,
49
+ RightOutlined,
50
+ RobotOutlined,
51
+ SearchOutlined,
52
+ SettingOutlined,
53
+ } from '@ant-design/icons-vue';
54
+
55
+ import {
56
+ modelTypeColor,
57
+ modelTypeLabel,
58
+ useProviderHub,
59
+ } from '../composables/useProviderHub';
60
+ import type {
61
+ GroupedModelView,
62
+ GroupedProviderView,
63
+ ModelEntity,
64
+ ModelType,
65
+ SelectedModel,
66
+ } from '../types';
67
+ import ProviderIcon from './ProviderIcon.vue';
68
+
69
+ interface Props {
70
+ modelValue?: SelectedModel;
71
+ /** 模型类型过滤,默认 LLM。传其它类型(TEXT_EMBEDDING / RERANK …)时组件通用。 */
72
+ modelType?: ModelType;
73
+ /** 直接注入分组数据;提供时组件不再自行请求。 */
74
+ groups?: GroupedProviderView[];
75
+ /** 后端多租户场景下的 tenantId,仅在自行加载数据时透传。 */
76
+ tenantId?: string;
77
+ /** 触发按钮的占位文案。 */
78
+ placeholder?: string;
79
+ /** 气泡卡片宽度。 */
80
+ width?: number | string;
81
+ /** 二级"模型下拉"高度上限。 */
82
+ bodyMaxHeight?: number | string;
83
+ /** ant-design-vue Popover placement 透传。 */
84
+ placement?:
85
+ | 'bottom'
86
+ | 'bottomLeft'
87
+ | 'bottomRight'
88
+ | 'top'
89
+ | 'topLeft'
90
+ | 'topRight';
91
+ /** 禁用触发。 */
92
+ disabled?: boolean;
93
+ /** 是否展示"参数"区域,默认展示。TEXT_EMBEDDING 等场景可传 false 隐藏。 */
94
+ showParams?: boolean;
95
+ /** 面板标题(顶部大字),默认「模型设置」。 */
96
+ title?: string;
97
+ /**
98
+ * 未选中时是否自动加载并回填「系统默认模型」(按 `modelType` 匹配租户默认)。
99
+ * 默认开启 —— 打开一个新建应用/新节点时,触发块直接展示默认 LLM,用户仍可再点开切换。
100
+ */
101
+ autoLoadDefault?: boolean;
102
+ }
103
+
104
+ const props = withDefaults(defineProps<Props>(), {
105
+ modelType: 'LLM',
106
+ placement: 'bottomLeft',
107
+ width: 460,
108
+ bodyMaxHeight: 340,
109
+ placeholder: '点击选择模型',
110
+ showParams: true,
111
+ title: '模型设置',
112
+ autoLoadDefault: true,
113
+ });
114
+
115
+ const emit = defineEmits<{
116
+ (e: 'update:modelValue', v: SelectedModel): void;
117
+ (
118
+ e: 'change',
119
+ v: SelectedModel,
120
+ provider: GroupedProviderView,
121
+ model: GroupedModelView,
122
+ ): void;
123
+ /** 底部「多个模型进行调试」的点击回调 —— 触发后调用方自行处理。 */
124
+ (e: 'multiModelDebug'): void;
125
+ /** 二级下拉底部「模型供应商设置」入口,方便直接跳到 provider hub 设置页。 */
126
+ (e: 'openProviderSettings'): void;
127
+ }>();
128
+
129
+ const { listDefaults, listModelsGroupedByType } = useProviderHub();
130
+
131
+ const open = ref(false);
132
+ const modelDropdownOpen = ref(false);
133
+ const loading = ref(false);
134
+ const searchQuery = ref('');
135
+ const collapsed = ref<Set<string>>(new Set());
136
+ const internalGroups = ref<GroupedProviderView[]>([]);
137
+
138
+ const groups = computed<GroupedProviderView[]>(
139
+ () => props.groups ?? internalGroups.value,
140
+ );
141
+
142
+ /** 兼容读取:新 shape 用 providerName,workflow 老 shape 用 provider。 */
143
+ const current = computed<SelectedModel>(() => props.modelValue ?? {});
144
+ const currentProvider = computed(
145
+ () => current.value.providerName ?? current.value.provider ?? '',
146
+ );
147
+ const currentModelName = computed(() => current.value.modelName ?? '');
148
+
149
+ /** 命中当前选中项所在的 provider + model,用于触发块 & 模型卡片展示。 */
150
+ const currentMeta = computed<null | {
151
+ provider: GroupedProviderView;
152
+ model: GroupedModelView;
153
+ }>(() => {
154
+ const pn = currentProvider.value;
155
+ const mn = currentModelName.value;
156
+ if (!pn || !mn) return null;
157
+ for (const p of groups.value) {
158
+ const m = p.modelList.find(
159
+ (x) => x.providerName === pn && x.modelName === mn,
160
+ );
161
+ if (m) return { provider: p, model: m };
162
+ }
163
+ return null;
164
+ });
165
+
166
+ /** 搜索命中模型 / provider / description。空关键字直接返回全部。 */
167
+ const filteredGroups = computed(() => {
168
+ const q = searchQuery.value.trim().toLowerCase();
169
+ if (!q) return groups.value;
170
+ return groups.value
171
+ .map((p) => {
172
+ const hits = p.modelList.filter((m) => {
173
+ return (
174
+ m.modelName.toLowerCase().includes(q) ||
175
+ (p.label ?? p.provider).toLowerCase().includes(q) ||
176
+ (p.description ?? '').toLowerCase().includes(q)
177
+ );
178
+ });
179
+ return { ...p, modelList: hits };
180
+ })
181
+ .filter((p) => p.modelList.length > 0);
182
+ });
183
+
184
+ const hasResult = computed(() =>
185
+ filteredGroups.value.some((g) => g.modelList.length > 0),
186
+ );
187
+
188
+ function isCollapsed(p: GroupedProviderView): boolean {
189
+ return collapsed.value.has(p.provider);
190
+ }
191
+
192
+ function toggleCollapse(p: GroupedProviderView) {
193
+ const next = new Set(collapsed.value);
194
+ if (next.has(p.provider)) next.delete(p.provider);
195
+ else next.add(p.provider);
196
+ collapsed.value = next;
197
+ }
198
+
199
+ /** ------- 参数 ------- */
200
+ type ParamType = 'boolean' | 'float' | 'int' | 'json' | 'text';
201
+
202
+ interface ParamCfg {
203
+ key: string;
204
+ label: string;
205
+ hint: string;
206
+ type: ParamType;
207
+ min?: number;
208
+ max?: number;
209
+ step?: number;
210
+ slider?: boolean;
211
+ placeholder?: string;
212
+ value: boolean | number | string;
213
+ enable: boolean;
214
+ }
215
+
216
+ /**
217
+ * 参数定义 —— 对齐 Dify「模型设置」参数集合。是否 slider 由 `slider` 决定;
218
+ * 布尔用 True / False segmented;text / json 用 textarea。
219
+ */
220
+ function makeParamConfig(): Record<string, ParamCfg> {
221
+ return {
222
+ temperature: {
223
+ key: 'temperature',
224
+ label: '温度',
225
+ hint: '越高越发散,0-2',
226
+ type: 'float',
227
+ min: 0,
228
+ max: 2,
229
+ step: 0.1,
230
+ slider: true,
231
+ value: 0.3,
232
+ enable: false,
233
+ },
234
+ max_tokens: {
235
+ key: 'max_tokens',
236
+ label: '最大标记',
237
+ hint: '单次生成的 token 上限',
238
+ type: 'int',
239
+ min: 1,
240
+ max: 32_000,
241
+ step: 1,
242
+ slider: true,
243
+ value: 8192,
244
+ enable: false,
245
+ },
246
+ top_p: {
247
+ key: 'top_p',
248
+ label: 'Top P',
249
+ hint: '核采样阈值 0-1',
250
+ type: 'float',
251
+ min: 0,
252
+ max: 1,
253
+ step: 0.05,
254
+ slider: true,
255
+ value: 0.8,
256
+ enable: false,
257
+ },
258
+ top_k: {
259
+ key: 'top_k',
260
+ label: '取样数量',
261
+ hint: '每一步只在概率最高的前 K 个候选里取样',
262
+ type: 'int',
263
+ min: 0,
264
+ max: 100,
265
+ step: 1,
266
+ slider: true,
267
+ value: 0,
268
+ enable: false,
269
+ },
270
+ seed: {
271
+ key: 'seed',
272
+ label: '随机种子',
273
+ hint: '固定随机种子以复现结果',
274
+ type: 'int',
275
+ min: 0,
276
+ max: 9_999_999_999,
277
+ step: 1,
278
+ slider: false,
279
+ value: 1234,
280
+ enable: false,
281
+ },
282
+ repetition_penalty: {
283
+ key: 'repetition_penalty',
284
+ label: '重复惩罚',
285
+ hint: '>1 惩罚已出现的 token,降低重复',
286
+ type: 'float',
287
+ min: 0,
288
+ max: 2,
289
+ step: 0.05,
290
+ slider: false,
291
+ value: 1.1,
292
+ enable: false,
293
+ },
294
+ enable_web_search: {
295
+ key: 'enable_web_search',
296
+ label: '联网搜索',
297
+ hint: '允许模型在生成前发起联网检索',
298
+ type: 'boolean',
299
+ value: false,
300
+ enable: false,
301
+ },
302
+ enable_thinking: {
303
+ key: 'enable_thinking',
304
+ label: '思考模式',
305
+ hint: '开启后模型会先输出思考过程',
306
+ type: 'boolean',
307
+ value: false,
308
+ enable: false,
309
+ },
310
+ thinking_budget: {
311
+ key: 'thinking_budget',
312
+ label: '思考长度限制',
313
+ hint: '思考过程最多消耗的 token',
314
+ type: 'int',
315
+ min: 0,
316
+ max: 32_000,
317
+ step: 1,
318
+ slider: false,
319
+ value: 2048,
320
+ enable: false,
321
+ },
322
+ response_format: {
323
+ key: 'response_format',
324
+ label: '回复格式',
325
+ hint: '例如 text / json_object / json_schema,或直接写 schema 描述',
326
+ type: 'text',
327
+ placeholder: '例如 text 或 json_object',
328
+ value: '',
329
+ enable: false,
330
+ },
331
+ extra_headers: {
332
+ key: 'extra_headers',
333
+ label: '额外请求头',
334
+ hint: 'JSON 字符串,会追加到底层 HTTP 请求头',
335
+ type: 'json',
336
+ placeholder: '{"X-Trace-Id": "abc"}',
337
+ value: '',
338
+ enable: false,
339
+ },
340
+ };
341
+ }
342
+
343
+ const paramConfig = reactive<Record<string, ParamCfg>>(makeParamConfig());
344
+
345
+ function syncParamsFromValue() {
346
+ const cp = current.value.completionParams || {};
347
+ for (const key of Object.keys(paramConfig)) {
348
+ const cfg = paramConfig[key];
349
+ if (!cfg) continue;
350
+ if (Object.prototype.hasOwnProperty.call(cp, key)) {
351
+ cfg.enable = true;
352
+ const raw = cp[key];
353
+ if (raw !== undefined) cfg.value = raw as ParamCfg['value'];
354
+ } else {
355
+ cfg.enable = false;
356
+ }
357
+ }
358
+ }
359
+
360
+ function collectParams(): Record<string, boolean | number | string> {
361
+ const out: Record<string, boolean | number | string> = {};
362
+ for (const cfg of Object.values(paramConfig)) {
363
+ if (cfg.enable) out[cfg.key] = cfg.value;
364
+ }
365
+ return out;
366
+ }
367
+
368
+ /**
369
+ * 组装对外抛出的 v-model 载荷 —— 保留原来 value 上所有键,覆盖新的
370
+ * providerName / modelName / modelType / modelProvider 以及 completionParams。
371
+ * 同时同步老 shape 的 provider / mode 让 workflow 侧序列化仍旧能吃到熟悉的
372
+ * 键;不再写入历史的 modelId 合成字段 —— 应用与工作流节点保存的都是
373
+ * (modelProvider, modelName) 两列。
374
+ */
375
+ function buildPayload(
376
+ next: {
377
+ modelName: string;
378
+ modelType: ModelType;
379
+ mode?: string;
380
+ providerName: string;
381
+ } | null,
382
+ ): SelectedModel {
383
+ const params = collectParams();
384
+ const base: SelectedModel = { ...(props.modelValue ?? {}) };
385
+ // Legacy composite id like "provider::model::type" is meaningless now — drop
386
+ // it so re-saving a node/app cleans up historical values.
387
+ if (typeof base.modelId === 'string' && base.modelId.includes('::')) {
388
+ delete base.modelId;
389
+ }
390
+ if (next) {
391
+ base.providerName = next.providerName;
392
+ base.modelProvider = next.providerName;
393
+ base.modelName = next.modelName;
394
+ base.modelType = next.modelType;
395
+ // 老字段同步,保证 formState.model 序列化后仍是 workflow 熟悉的形状
396
+ base.provider = next.providerName;
397
+ if (next.mode !== undefined) base.mode = next.mode;
398
+ }
399
+ if (Object.keys(params).length > 0) base.completionParams = params;
400
+ else delete base.completionParams;
401
+ return base;
402
+ }
403
+
404
+ function selectModel(p: GroupedProviderView, m: GroupedModelView) {
405
+ const payload = buildPayload({
406
+ providerName: m.providerName,
407
+ modelName: m.modelName,
408
+ modelType: m.modelType,
409
+ mode: (m as any).mode,
410
+ });
411
+ emit('update:modelValue', payload);
412
+ emit('change', payload, p, m);
413
+ // 只关掉二级下拉,外层"模型设置"面板保持打开
414
+ modelDropdownOpen.value = false;
415
+ }
416
+
417
+ function onParamsChange() {
418
+ emit('update:modelValue', buildPayload(null));
419
+ }
420
+
421
+ async function loadGroups() {
422
+ if (props.groups) return;
423
+ loading.value = true;
424
+ try {
425
+ const all = await listModelsGroupedByType(props.tenantId);
426
+ internalGroups.value = all[props.modelType] ?? [];
427
+ } catch {
428
+ internalGroups.value = [];
429
+ } finally {
430
+ loading.value = false;
431
+ }
432
+ }
433
+
434
+ /** 当前 v-model 是否已经指向一个具体模型。 */
435
+ function hasSelection(sel: SelectedModel | undefined): boolean {
436
+ if (!sel) return false;
437
+ const pn = sel.providerName ?? sel.provider;
438
+ return !!(pn && sel.modelName);
439
+ }
440
+
441
+ /**
442
+ * 没选中模型时,读取租户「系统默认模型」并回填当前 modelType 对应的那一条。
443
+ * 命中即向上抛 update:modelValue —— 调用方(AppDesignDrawer / 各类节点)
444
+ * 会把它写进 form 里,页面再次打开时就不会又出现"请选择模型"占位。
445
+ */
446
+ async function loadDefaultIfEmpty() {
447
+ if (!props.autoLoadDefault) return;
448
+ if (hasSelection(props.modelValue)) return;
449
+ try {
450
+ const defaults = await listDefaults(props.tenantId);
451
+ // 用户可能在等待期间已经手动选过 —— 再判一次避免覆盖。
452
+ if (hasSelection(props.modelValue)) return;
453
+ const def = defaults[props.modelType] as ModelEntity | undefined;
454
+ if (!def?.providerName || !def.modelName) return;
455
+ emit(
456
+ 'update:modelValue',
457
+ buildPayload({
458
+ providerName: def.providerName,
459
+ modelName: def.modelName,
460
+ modelType: def.modelType ?? props.modelType,
461
+ }),
462
+ );
463
+ } catch {
464
+ // 默认模型接口失败不影响手动选择,静默即可。
465
+ }
466
+ }
467
+
468
+ onMounted(() => {
469
+ syncParamsFromValue();
470
+ if (!props.groups) loadGroups();
471
+ loadDefaultIfEmpty();
472
+ });
473
+
474
+ watch(
475
+ () => [props.tenantId, props.modelType],
476
+ () => {
477
+ if (!props.groups) loadGroups();
478
+ loadDefaultIfEmpty();
479
+ },
480
+ );
481
+
482
+ watch(
483
+ () => props.modelValue,
484
+ () => syncParamsFromValue(),
485
+ { deep: true },
486
+ );
487
+
488
+ watch(open, (v) => {
489
+ if (!v) {
490
+ // 关闭外层时同时把二级下拉收起来,避免下次打开还残留
491
+ modelDropdownOpen.value = false;
492
+ return;
493
+ }
494
+ if (!props.groups) loadGroups();
495
+ });
496
+
497
+ watch(modelDropdownOpen, (v) => {
498
+ if (!v) return;
499
+ if (!props.groups) loadGroups();
500
+ // 打开二级下拉时展开当前选中的分组
501
+ if (currentProvider.value) {
502
+ const next = new Set(collapsed.value);
503
+ next.delete(currentProvider.value);
504
+ collapsed.value = next;
505
+ }
506
+ });
507
+
508
+ function toggleOpen() {
509
+ if (props.disabled) return;
510
+ open.value = !open.value;
511
+ }
512
+
513
+ function closePanel() {
514
+ open.value = false;
515
+ }
516
+
517
+ defineExpose({
518
+ open: () => {
519
+ open.value = true;
520
+ },
521
+ close: closePanel,
522
+ refresh: loadGroups,
523
+ });
524
+ </script>
525
+
526
+ <template>
527
+ <Popover
528
+ v-model:open="open"
529
+ :placement="placement"
530
+ trigger="click"
531
+ overlay-class-name="ph-model-picker-popover"
532
+ :get-popup-container="(t) => t.parentElement || document.body"
533
+ >
534
+ <template #content>
535
+ <div
536
+ class="ph-mp"
537
+ :style="{
538
+ width: typeof width === 'number' ? `${width}px` : width,
539
+ }"
540
+ >
541
+ <!-- 顶部标题栏 -->
542
+ <div class="ph-mp-header">
543
+ <span class="ph-mp-title">{{ title }}</span>
544
+ <button
545
+ type="button"
546
+ class="ph-mp-close"
547
+ aria-label="关闭"
548
+ @click="closePanel"
549
+ >
550
+ <CloseOutlined />
551
+ </button>
552
+ </div>
553
+
554
+ <div class="ph-mp-scroll">
555
+ <!-- 模型卡片 —— 点击展开二级下拉 -->
556
+ <Popover
557
+ v-model:open="modelDropdownOpen"
558
+ trigger="click"
559
+ placement="bottomLeft"
560
+ overlay-class-name="ph-model-picker-popover"
561
+ :get-popup-container="(t) => t.parentElement || document.body"
562
+ >
563
+ <template #content>
564
+ <div
565
+ class="ph-mp-dropdown"
566
+ :style="{
567
+ width: typeof width === 'number' ? `${width - 40}px` : width,
568
+ }"
569
+ >
570
+ <div class="ph-mp-search">
571
+ <SearchOutlined class="ph-mp-search-icon" />
572
+ <input
573
+ v-model="searchQuery"
574
+ class="ph-mp-search-input"
575
+ placeholder="搜索模型"
576
+ @keydown.stop
577
+ />
578
+ </div>
579
+
580
+ <div
581
+ class="ph-mp-body"
582
+ :style="{
583
+ maxHeight:
584
+ typeof bodyMaxHeight === 'number'
585
+ ? `${bodyMaxHeight}px`
586
+ : bodyMaxHeight,
587
+ }"
588
+ >
589
+ <Spin :spinning="loading">
590
+ <div
591
+ v-if="!loading && groups.length === 0"
592
+ class="ph-mp-empty"
593
+ >
594
+ <RobotOutlined class="ph-mp-empty-icon" />
595
+ <div>暂无可用模型</div>
596
+ <div class="ph-mp-empty-hint">
597
+ 请先在"模型供应商"里配置并启用
598
+ </div>
599
+ </div>
600
+
601
+ <div v-else-if="!hasResult" class="ph-mp-empty">
602
+ <SearchOutlined class="ph-mp-empty-icon" />
603
+ <div>没有匹配的模型</div>
604
+ </div>
605
+
606
+ <div v-else class="ph-mp-groups">
607
+ <div
608
+ v-for="p in filteredGroups"
609
+ :key="p.id"
610
+ class="ph-mp-group"
611
+ >
612
+ <div
613
+ class="ph-mp-group-header"
614
+ @click="toggleCollapse(p)"
615
+ >
616
+ <RightOutlined
617
+ v-if="isCollapsed(p)"
618
+ class="ph-mp-fold"
619
+ />
620
+ <DownOutlined v-else class="ph-mp-fold" />
621
+ <ProviderIcon :name="p.provider" :size="18" />
622
+ <span class="ph-mp-group-name">
623
+ {{ p.label || p.provider }}
624
+ </span>
625
+ <span
626
+ class="ph-mp-item-tag"
627
+ :data-color="modelTypeColor(modelType)"
628
+ >
629
+ {{ modelTypeLabel(modelType) }}
630
+ </span>
631
+ </div>
632
+
633
+ <div v-if="!isCollapsed(p)" class="ph-mp-group-body">
634
+ <div
635
+ v-for="m in p.modelList"
636
+ :key="m.id"
637
+ class="ph-mp-item"
638
+ :class="{
639
+ 'is-active':
640
+ currentProvider === m.providerName &&
641
+ currentModelName === m.modelName,
642
+ }"
643
+ @click="selectModel(p, m)"
644
+ >
645
+ <ProviderIcon :name="m.providerName" :size="16" />
646
+ <span class="ph-mp-item-name" :title="m.modelName">
647
+ {{ m.modelName }}
648
+ </span>
649
+ <CheckOutlined
650
+ v-if="
651
+ currentProvider === m.providerName &&
652
+ currentModelName === m.modelName
653
+ "
654
+ class="ph-mp-item-check"
655
+ />
656
+ </div>
657
+ </div>
658
+ </div>
659
+ </div>
660
+ </Spin>
661
+ </div>
662
+
663
+ <div
664
+ class="ph-mp-dropdown-footer"
665
+ @click="emit('openProviderSettings')"
666
+ >
667
+ <SettingOutlined />
668
+ <span>模型供应商设置</span>
669
+ </div>
670
+ </div>
671
+ </template>
672
+
673
+ <div
674
+ class="ph-mp-model-card"
675
+ :class="{
676
+ 'is-active': modelDropdownOpen,
677
+ 'has-value': !!currentModelName,
678
+ }"
679
+ >
680
+ <div class="ph-mp-model-card-icon">
681
+ <ProviderIcon
682
+ v-if="currentProvider"
683
+ :name="currentProvider"
684
+ :size="20"
685
+ />
686
+ <RobotOutlined v-else />
687
+ </div>
688
+ <span
689
+ v-if="currentModelName"
690
+ class="ph-mp-model-card-name"
691
+ :title="currentModelName"
692
+ >
693
+ {{ currentModelName }}
694
+ </span>
695
+ <span v-else class="ph-mp-model-card-placeholder">
696
+ 请选择模型
697
+ </span>
698
+ <span
699
+ v-if="currentModelName"
700
+ class="ph-mp-item-tag"
701
+ :data-color="modelTypeColor(current.modelType || modelType)"
702
+ >
703
+ {{ modelTypeLabel(current.modelType || modelType) }}
704
+ </span>
705
+ <SettingOutlined class="ph-mp-model-card-cog" />
706
+ <DownOutlined class="ph-mp-model-card-arrow" />
707
+ </div>
708
+ </Popover>
709
+
710
+ <!-- 参数区 -->
711
+ <div v-if="showParams" class="ph-mp-params-block">
712
+ <div class="ph-mp-params-title">参数</div>
713
+ <div class="ph-mp-params">
714
+ <div
715
+ v-for="cfg in paramConfig"
716
+ :key="cfg.key"
717
+ class="ph-mp-param"
718
+ :class="{ 'is-block': cfg.type === 'text' || cfg.type === 'json' }"
719
+ >
720
+ <div class="ph-mp-param-row">
721
+ <a-switch
722
+ v-model:checked="cfg.enable"
723
+ size="small"
724
+ class="ph-mp-param-switch"
725
+ @change="onParamsChange"
726
+ />
727
+ <span class="ph-mp-param-name">{{ cfg.label }}</span>
728
+ <a-tooltip :title="cfg.hint" placement="top">
729
+ <QuestionCircleOutlined class="ph-mp-param-hint" />
730
+ </a-tooltip>
731
+
732
+ <template v-if="cfg.type === 'float' || cfg.type === 'int'">
733
+ <a-slider
734
+ v-if="cfg.slider"
735
+ v-model:value="cfg.value as number"
736
+ :min="cfg.min"
737
+ :max="cfg.max"
738
+ :step="cfg.step"
739
+ :disabled="!cfg.enable"
740
+ class="ph-mp-param-slider"
741
+ @afterChange="onParamsChange"
742
+ />
743
+ <a-input-number
744
+ v-model:value="cfg.value as number"
745
+ :min="cfg.min"
746
+ :max="cfg.max"
747
+ :step="cfg.step"
748
+ :disabled="!cfg.enable"
749
+ size="small"
750
+ class="ph-mp-param-num"
751
+ @change="onParamsChange"
752
+ />
753
+ </template>
754
+
755
+ <a-segmented
756
+ v-else-if="cfg.type === 'boolean'"
757
+ v-model:value="cfg.value as boolean"
758
+ :options="[
759
+ { label: 'True', value: true },
760
+ { label: 'False', value: false },
761
+ ]"
762
+ :disabled="!cfg.enable"
763
+ class="ph-mp-param-seg"
764
+ @change="onParamsChange"
765
+ />
766
+ </div>
767
+
768
+ <a-textarea
769
+ v-if="cfg.type === 'text' || cfg.type === 'json'"
770
+ v-model:value="cfg.value as string"
771
+ :disabled="!cfg.enable"
772
+ :placeholder="cfg.placeholder"
773
+ :auto-size="{ minRows: 2, maxRows: 6 }"
774
+ class="ph-mp-param-textarea"
775
+ @change="onParamsChange"
776
+ />
777
+ </div>
778
+ </div>
779
+ </div>
780
+ </div>
781
+
782
+ <!-- 底部:多模型调试入口 -->
783
+ <div
784
+ class="ph-mp-footer"
785
+ @click="emit('multiModelDebug')"
786
+ >
787
+ <span>多个模型进行调试</span>
788
+ <RightOutlined />
789
+ </div>
790
+ </div>
791
+ </template>
792
+
793
+ <slot
794
+ name="trigger"
795
+ :open="open"
796
+ :toggle="toggleOpen"
797
+ :current="current"
798
+ :meta="currentMeta"
799
+ :disabled="disabled"
800
+ >
801
+ <div
802
+ class="ph-mp-trigger"
803
+ :class="{
804
+ 'is-active': open,
805
+ 'has-value': !!currentModelName,
806
+ 'is-disabled': disabled,
807
+ }"
808
+ @click="toggleOpen"
809
+ >
810
+ <div class="ph-mp-trigger-icon">
811
+ <ProviderIcon
812
+ v-if="currentProvider"
813
+ :name="currentProvider"
814
+ :size="22"
815
+ />
816
+ <RobotOutlined v-else />
817
+ </div>
818
+ <span
819
+ v-if="currentModelName"
820
+ class="ph-mp-trigger-name"
821
+ :title="currentModelName"
822
+ >
823
+ {{ currentModelName }}
824
+ </span>
825
+ <span
826
+ v-if="currentModelName"
827
+ class="ph-mp-trigger-sub"
828
+ :title="currentMeta?.provider.label || currentProvider"
829
+ >
830
+ {{ currentMeta?.provider.label || currentProvider }}
831
+ </span>
832
+ <span v-if="!currentModelName" class="ph-mp-trigger-placeholder">
833
+ {{ placeholder }}
834
+ </span>
835
+ <DownOutlined class="ph-mp-trigger-arrow" />
836
+ </div>
837
+ </slot>
838
+ </Popover>
839
+ </template>
840
+
841
+ <style>
842
+ /* Popover 内容会 Teleport 到 body,不能用 scoped。 */
843
+ .ph-model-picker-popover .ant-popover-inner,
844
+ .ph-model-picker-popover .ant-popover-inner-content {
845
+ padding: 0;
846
+ border-radius: 12px;
847
+ overflow: hidden;
848
+ }
849
+
850
+ .ph-mp {
851
+ display: flex;
852
+ flex-direction: column;
853
+ max-width: 92vw;
854
+ max-height: 84vh;
855
+ }
856
+
857
+ /* 顶部标题栏 */
858
+ .ph-mp-header {
859
+ display: flex;
860
+ align-items: center;
861
+ justify-content: space-between;
862
+ padding: 14px 18px 10px;
863
+ }
864
+ .ph-mp-title {
865
+ font-size: 15px;
866
+ font-weight: 700;
867
+ color: #0f172a;
868
+ }
869
+ .ph-mp-close {
870
+ padding: 0;
871
+ border: none;
872
+ background: transparent;
873
+ color: #9ca3af;
874
+ font-size: 14px;
875
+ cursor: pointer;
876
+ border-radius: 6px;
877
+ width: 24px;
878
+ height: 24px;
879
+ }
880
+ .ph-mp-close:hover {
881
+ background: #f3f4f6;
882
+ color: #4b5563;
883
+ }
884
+
885
+ /* 模型卡片(点击展开二级下拉) */
886
+ .ph-mp-scroll {
887
+ flex: 1;
888
+ min-height: 0;
889
+ padding: 0 18px 4px;
890
+ overflow-y: auto;
891
+ }
892
+ .ph-mp-scroll::-webkit-scrollbar {
893
+ width: 6px;
894
+ }
895
+ .ph-mp-scroll::-webkit-scrollbar-thumb {
896
+ background: #d1d5db;
897
+ border-radius: 3px;
898
+ }
899
+ .ph-mp-model-card {
900
+ display: flex;
901
+ align-items: center;
902
+ gap: 10px;
903
+ padding: 10px 12px;
904
+ margin-bottom: 14px;
905
+ background: #ffffff;
906
+ border: 1px solid #e5e7eb;
907
+ border-radius: 10px;
908
+ cursor: pointer;
909
+ transition: border-color 0.15s, box-shadow 0.15s, background 0.15s;
910
+ }
911
+ .ph-mp-model-card:hover {
912
+ border-color: #a5b4fc;
913
+ }
914
+ .ph-mp-model-card.is-active {
915
+ border-color: #6366f1;
916
+ box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.12);
917
+ }
918
+ .ph-mp-model-card.has-value {
919
+ background: #fafbff;
920
+ }
921
+ .ph-mp-model-card-icon {
922
+ flex: none;
923
+ width: 22px;
924
+ height: 22px;
925
+ display: flex;
926
+ align-items: center;
927
+ justify-content: center;
928
+ color: #6366f1;
929
+ }
930
+ .ph-mp-model-card-name {
931
+ flex: 1;
932
+ min-width: 0;
933
+ font-size: 13px;
934
+ font-weight: 600;
935
+ color: #1f2937;
936
+ overflow: hidden;
937
+ text-overflow: ellipsis;
938
+ white-space: nowrap;
939
+ }
940
+ .ph-mp-model-card-placeholder {
941
+ flex: 1;
942
+ font-size: 13px;
943
+ color: #9ca3af;
944
+ }
945
+ .ph-mp-model-card-cog {
946
+ color: #94a3b8;
947
+ font-size: 12px;
948
+ }
949
+ .ph-mp-model-card-arrow {
950
+ color: #94a3b8;
951
+ font-size: 10px;
952
+ transition: transform 0.15s;
953
+ }
954
+ .ph-mp-model-card.is-active .ph-mp-model-card-arrow {
955
+ transform: rotate(180deg);
956
+ color: #6366f1;
957
+ }
958
+
959
+ /* 二级下拉 */
960
+ .ph-mp-dropdown {
961
+ display: flex;
962
+ flex-direction: column;
963
+ max-width: 92vw;
964
+ }
965
+ .ph-mp-search {
966
+ display: flex;
967
+ align-items: center;
968
+ gap: 8px;
969
+ padding: 8px 12px;
970
+ border-bottom: 1px solid #f0f0f0;
971
+ background: #f9fafb;
972
+ }
973
+ .ph-mp-search-icon {
974
+ color: #9ca3af;
975
+ }
976
+ .ph-mp-search-input {
977
+ flex: 1;
978
+ border: 0;
979
+ outline: 0;
980
+ background: transparent;
981
+ font-size: 13px;
982
+ color: #1f2937;
983
+ }
984
+ .ph-mp-search-input::placeholder {
985
+ color: #9ca3af;
986
+ }
987
+ .ph-mp-body {
988
+ overflow-y: auto;
989
+ padding: 6px 4px;
990
+ }
991
+ .ph-mp-body::-webkit-scrollbar {
992
+ width: 6px;
993
+ }
994
+ .ph-mp-body::-webkit-scrollbar-thumb {
995
+ background: #d1d5db;
996
+ border-radius: 3px;
997
+ }
998
+ .ph-mp-empty {
999
+ display: flex;
1000
+ flex-direction: column;
1001
+ align-items: center;
1002
+ gap: 6px;
1003
+ padding: 28px 16px;
1004
+ color: #9ca3af;
1005
+ font-size: 12px;
1006
+ text-align: center;
1007
+ }
1008
+ .ph-mp-empty-icon {
1009
+ font-size: 28px;
1010
+ opacity: 0.35;
1011
+ }
1012
+ .ph-mp-empty-hint {
1013
+ font-size: 11px;
1014
+ color: #cbd5e1;
1015
+ }
1016
+ .ph-mp-groups {
1017
+ display: flex;
1018
+ flex-direction: column;
1019
+ gap: 2px;
1020
+ }
1021
+ .ph-mp-group + .ph-mp-group {
1022
+ margin-top: 2px;
1023
+ padding-top: 4px;
1024
+ border-top: 1px solid #f3f4f6;
1025
+ }
1026
+ .ph-mp-group-header {
1027
+ display: flex;
1028
+ align-items: center;
1029
+ gap: 8px;
1030
+ padding: 6px 10px;
1031
+ cursor: pointer;
1032
+ border-radius: 6px;
1033
+ user-select: none;
1034
+ transition: background 0.12s;
1035
+ }
1036
+ .ph-mp-group-header:hover {
1037
+ background: #f9fafb;
1038
+ }
1039
+ .ph-mp-fold {
1040
+ font-size: 10px;
1041
+ color: #9ca3af;
1042
+ }
1043
+ .ph-mp-group-name {
1044
+ flex: 1;
1045
+ min-width: 0;
1046
+ font-size: 12px;
1047
+ font-weight: 600;
1048
+ color: #1f2937;
1049
+ overflow: hidden;
1050
+ text-overflow: ellipsis;
1051
+ white-space: nowrap;
1052
+ }
1053
+ .ph-mp-group-body {
1054
+ display: flex;
1055
+ flex-direction: column;
1056
+ gap: 2px;
1057
+ padding: 2px 0 6px 22px;
1058
+ }
1059
+ .ph-mp-item {
1060
+ display: flex;
1061
+ align-items: center;
1062
+ gap: 8px;
1063
+ padding: 6px 10px;
1064
+ cursor: pointer;
1065
+ border-radius: 6px;
1066
+ transition: background 0.12s;
1067
+ }
1068
+ .ph-mp-item:hover {
1069
+ background: #f3f4f6;
1070
+ }
1071
+ .ph-mp-item.is-active {
1072
+ background: #eef2ff;
1073
+ }
1074
+ .ph-mp-item-name {
1075
+ flex: 1;
1076
+ min-width: 0;
1077
+ font-size: 13px;
1078
+ color: #1f2937;
1079
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas,
1080
+ 'Liberation Mono', monospace;
1081
+ overflow: hidden;
1082
+ text-overflow: ellipsis;
1083
+ white-space: nowrap;
1084
+ }
1085
+ .ph-mp-item.is-active .ph-mp-item-name {
1086
+ color: #4338ca;
1087
+ font-weight: 500;
1088
+ }
1089
+ .ph-mp-item-tag {
1090
+ flex: none;
1091
+ padding: 1px 6px;
1092
+ font-size: 10px;
1093
+ border-radius: 4px;
1094
+ background: #eef2ff;
1095
+ color: #4338ca;
1096
+ letter-spacing: 0.3px;
1097
+ }
1098
+ .ph-mp-item-tag[data-color='green'] {
1099
+ background: #ecfdf5;
1100
+ color: #059669;
1101
+ }
1102
+ .ph-mp-item-tag[data-color='orange'] {
1103
+ background: #fff7ed;
1104
+ color: #c2410c;
1105
+ }
1106
+ .ph-mp-item-tag[data-color='purple'] {
1107
+ background: #faf5ff;
1108
+ color: #7c3aed;
1109
+ }
1110
+ .ph-mp-item-check {
1111
+ color: #4338ca;
1112
+ font-size: 12px;
1113
+ }
1114
+ .ph-mp-dropdown-footer {
1115
+ display: flex;
1116
+ align-items: center;
1117
+ gap: 6px;
1118
+ padding: 10px 14px;
1119
+ border-top: 1px solid #f0f0f0;
1120
+ color: #4338ca;
1121
+ font-size: 12px;
1122
+ cursor: pointer;
1123
+ background: #fafbff;
1124
+ }
1125
+ .ph-mp-dropdown-footer:hover {
1126
+ background: #eef2ff;
1127
+ }
1128
+
1129
+ /* 参数区 */
1130
+ .ph-mp-params-block {
1131
+ padding-top: 4px;
1132
+ }
1133
+ .ph-mp-params-title {
1134
+ padding: 4px 0 10px;
1135
+ font-size: 14px;
1136
+ font-weight: 700;
1137
+ color: #0f172a;
1138
+ }
1139
+ .ph-mp-params {
1140
+ display: flex;
1141
+ flex-direction: column;
1142
+ gap: 12px;
1143
+ }
1144
+ .ph-mp-param {
1145
+ display: flex;
1146
+ flex-direction: column;
1147
+ gap: 6px;
1148
+ }
1149
+ .ph-mp-param-row {
1150
+ display: flex;
1151
+ align-items: center;
1152
+ gap: 8px;
1153
+ min-height: 24px;
1154
+ }
1155
+ .ph-mp-param-switch {
1156
+ flex: none;
1157
+ }
1158
+ .ph-mp-param-name {
1159
+ flex: none;
1160
+ width: 88px;
1161
+ font-size: 13px;
1162
+ font-weight: 500;
1163
+ color: #374151;
1164
+ overflow: hidden;
1165
+ text-overflow: ellipsis;
1166
+ white-space: nowrap;
1167
+ }
1168
+ .ph-mp-param-hint {
1169
+ color: #94a3b8;
1170
+ font-size: 12px;
1171
+ cursor: help;
1172
+ }
1173
+ .ph-mp-param-slider {
1174
+ flex: 1;
1175
+ min-width: 0;
1176
+ margin: 0 !important;
1177
+ }
1178
+ .ph-mp-param-num {
1179
+ flex: none;
1180
+ width: 88px;
1181
+ }
1182
+ .ph-mp-param-seg {
1183
+ margin-left: auto;
1184
+ }
1185
+ .ph-mp-param.is-block .ph-mp-param-row {
1186
+ min-height: unset;
1187
+ }
1188
+ .ph-mp-param-textarea {
1189
+ margin-left: 32px;
1190
+ font-size: 12px !important;
1191
+ }
1192
+
1193
+ /* 底部 */
1194
+ .ph-mp-footer {
1195
+ display: flex;
1196
+ align-items: center;
1197
+ justify-content: space-between;
1198
+ padding: 12px 18px;
1199
+ border-top: 1px solid #f0f0f0;
1200
+ color: #4338ca;
1201
+ font-weight: 500;
1202
+ font-size: 13px;
1203
+ cursor: pointer;
1204
+ background: #fafbff;
1205
+ }
1206
+ .ph-mp-footer:hover {
1207
+ background: #eef2ff;
1208
+ }
1209
+ </style>
1210
+
1211
+ <style scoped>
1212
+ .ph-mp-trigger {
1213
+ display: flex;
1214
+ align-items: center;
1215
+ gap: 8px;
1216
+ padding: 6px 10px;
1217
+ min-height: 36px;
1218
+ background: #fff;
1219
+ border: 1px solid #e5e7eb;
1220
+ border-radius: 8px;
1221
+ cursor: pointer;
1222
+ transition: border-color 0.15s, box-shadow 0.15s, background 0.15s;
1223
+ }
1224
+ .ph-mp-trigger:hover {
1225
+ border-color: #a5b4fc;
1226
+ background: #fafbff;
1227
+ }
1228
+ .ph-mp-trigger.is-active {
1229
+ border-color: #6366f1;
1230
+ box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.15);
1231
+ }
1232
+ .ph-mp-trigger.has-value {
1233
+ background: #fafbff;
1234
+ }
1235
+ .ph-mp-trigger.is-disabled {
1236
+ cursor: not-allowed;
1237
+ opacity: 0.55;
1238
+ }
1239
+ .ph-mp-trigger-icon {
1240
+ flex: none;
1241
+ width: 22px;
1242
+ height: 22px;
1243
+ display: flex;
1244
+ align-items: center;
1245
+ justify-content: center;
1246
+ color: #6366f1;
1247
+ }
1248
+ .ph-mp-trigger-name {
1249
+ flex: none;
1250
+ max-width: 55%;
1251
+ font-size: 13px;
1252
+ font-weight: 600;
1253
+ color: #1f2937;
1254
+ overflow: hidden;
1255
+ text-overflow: ellipsis;
1256
+ white-space: nowrap;
1257
+ }
1258
+ .ph-mp-trigger-sub {
1259
+ flex: 1;
1260
+ min-width: 0;
1261
+ font-size: 11px;
1262
+ color: #6b7280;
1263
+ overflow: hidden;
1264
+ text-overflow: ellipsis;
1265
+ white-space: nowrap;
1266
+ }
1267
+ .ph-mp-trigger-placeholder {
1268
+ flex: 1;
1269
+ font-size: 13px;
1270
+ color: #9ca3af;
1271
+ }
1272
+ .ph-mp-trigger-arrow {
1273
+ flex: none;
1274
+ font-size: 10px;
1275
+ color: #9ca3af;
1276
+ transition: transform 0.15s;
1277
+ }
1278
+ .ph-mp-trigger.is-active .ph-mp-trigger-arrow {
1279
+ transform: rotate(180deg);
1280
+ color: #6366f1;
1281
+ }
1282
+ </style>