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,804 @@
1
+ <script setup lang="ts">
2
+ import { computed, nextTick, onMounted, onUnmounted, reactive, ref, watch } from 'vue';
3
+
4
+ import {
5
+ CopyOutlined,
6
+ FullscreenExitOutlined,
7
+ FullscreenOutlined,
8
+ PlusOutlined,
9
+ } from '@ant-design/icons-vue';
10
+ import { message } from 'ant-design-vue';
11
+
12
+ import { useWorkflowStore } from '@/stores/workflow';
13
+ import workflow_utils from '@/workflow/utils/workflow_utils';
14
+
15
+ import PromptEditorTagPanel from './PromptEditorTagPanel.vue';
16
+
17
+ /**
18
+ * PromptEditor —— Dify 风格的提示词编辑器
19
+ *
20
+ * 特性
21
+ * - 变量以彩色 pill 显示,可点删除
22
+ * - `/` 或 `{{` 触发上游变量选择
23
+ * - 键盘:↑↓ 选择、Enter 插入、Esc 关闭
24
+ * - Backspace 前是 pill 时直接删 pill
25
+ * - 复制按钮、字符计数、全屏(Esc 退出)
26
+ * - v-model 用 `{{#nodeId.field#}}` 模板串(与后端对齐)
27
+ *
28
+ * 公开 API(defineExpose)
29
+ * - focus() / blur()
30
+ * - insertVariable(selector: string[]) 从外部插入一个变量
31
+ * - getContent() / setContent(text)
32
+ */
33
+
34
+ interface Props {
35
+ modelValue?: string;
36
+ title?: string;
37
+ nodeId?: string;
38
+ placeholder?: string;
39
+ minHeight?: string;
40
+ maxHeight?: string;
41
+ readonly?: boolean;
42
+ showToolbar?: boolean;
43
+ }
44
+
45
+ const props = withDefaults(defineProps<Props>(), {
46
+ modelValue: '',
47
+ title: '提示词',
48
+ nodeId: '',
49
+ placeholder: '输入 / 或 {{ 引用上游变量',
50
+ minHeight: '120px',
51
+ maxHeight: '360px',
52
+ readonly: false,
53
+ showToolbar: true,
54
+ });
55
+
56
+ const emit = defineEmits<{
57
+ (e: 'update:modelValue', v: string): void;
58
+ }>();
59
+
60
+ const workflowStore = useWorkflowStore();
61
+
62
+ /** ---------- refs ---------- */
63
+ const editorRef = ref<HTMLDivElement | null>(null);
64
+ const insertBtnRef = ref<HTMLElement | null>(null);
65
+ const isFullscreen = ref(false);
66
+
67
+ /** 弹层状态:mode='insert' → 点击插入按钮打开;mode='trigger' → / 或 {{ 触发 */
68
+ const panel = reactive<{
69
+ open: boolean;
70
+ mode: 'insert' | 'trigger';
71
+ targetElement: HTMLElement | null;
72
+ search: string;
73
+ selectedIndex: number;
74
+ triggerRange: {
75
+ node: Text | null;
76
+ start: number;
77
+ end: number;
78
+ } | null;
79
+ }>({
80
+ open: false,
81
+ mode: 'insert',
82
+ targetElement: null,
83
+ search: '',
84
+ selectedIndex: 0,
85
+ triggerRange: null,
86
+ });
87
+
88
+ const savedRange = ref<null | Range>(null);
89
+
90
+ /** ---------- content <-> DOM 同步 ---------- */
91
+
92
+ /** 序列化编辑器 DOM 为 {{#nodeId.field#}} 模板字符串 */
93
+ function serializeToText(): string {
94
+ const root = editorRef.value;
95
+ if (!root) return '';
96
+
97
+ let text = '';
98
+ const walk = (node: Node) => {
99
+ if (node.nodeType === Node.TEXT_NODE) {
100
+ text += node.textContent ?? '';
101
+ return;
102
+ }
103
+ if (node.nodeType !== Node.ELEMENT_NODE) return;
104
+ const el = node as HTMLElement;
105
+ if (el.classList?.contains('wf-pe-pill')) {
106
+ const key = el.dataset.key ?? '';
107
+ text += `{{#${key}#}}`;
108
+ return;
109
+ }
110
+ // <br> 或块级换行
111
+ if (el.tagName === 'BR') {
112
+ text += '\n';
113
+ return;
114
+ }
115
+ for (const child of Array.from(el.childNodes)) walk(child);
116
+ // 块级元素后补换行(div/p)
117
+ if (
118
+ (el.tagName === 'DIV' || el.tagName === 'P') &&
119
+ el !== root &&
120
+ !text.endsWith('\n')
121
+ ) {
122
+ text += '\n';
123
+ }
124
+ };
125
+ for (const child of Array.from(root.childNodes)) walk(child);
126
+ return text.replace(/\n+$/g, '');
127
+ }
128
+
129
+ /** 把模板字符串反序列化到 DOM */
130
+ function deserializeFromText(text: string) {
131
+ const root = editorRef.value;
132
+ if (!root) return;
133
+ root.innerHTML = '';
134
+ if (!text) return;
135
+
136
+ const frag = document.createDocumentFragment();
137
+ const re = /\{\{#([^}]+)#\}\}/g;
138
+ let lastIndex = 0;
139
+ let match: RegExpExecArray | null;
140
+ while ((match = re.exec(text)) !== null) {
141
+ if (match.index > lastIndex) {
142
+ appendTextWithNewlines(frag, text.slice(lastIndex, match.index));
143
+ }
144
+ frag.appendChild(createPill(match[1]));
145
+ lastIndex = match.index + match[0].length;
146
+ }
147
+ if (lastIndex < text.length) {
148
+ appendTextWithNewlines(frag, text.slice(lastIndex));
149
+ }
150
+ root.appendChild(frag);
151
+ }
152
+
153
+ /** 把纯文本按 \n 拆开插入,每个 \n 变一个 <br> */
154
+ function appendTextWithNewlines(parent: Node, str: string) {
155
+ const parts = str.split('\n');
156
+ parts.forEach((p, i) => {
157
+ if (p) parent.appendChild(document.createTextNode(p));
158
+ if (i < parts.length - 1) parent.appendChild(document.createElement('br'));
159
+ });
160
+ }
161
+
162
+ /** 上游变量 selector -> 显示 label */
163
+ function getPillLabel(selector: string[]): string {
164
+ return workflow_utils.getVariableLabel(selector) || selector.join('.');
165
+ }
166
+
167
+ /** 上游变量 selector -> 类型(用于 pill 上色) */
168
+ function getPillType(selector: string[]): string {
169
+ if (!selector || selector.length < 2) return '';
170
+ const [nodeId, ...rest] = selector;
171
+ const node = workflowStore.getNodeById(nodeId);
172
+ if (!node?.data) return '';
173
+ const outputs = workflow_utils.getOutputList(node.data) || [];
174
+ let cur: any = outputs.find((o: any) => o.name === rest[0]);
175
+ for (let i = 1; i < rest.length; i++) {
176
+ if (!cur?.children) return cur?.type ?? '';
177
+ cur = cur.children.find((c: any) => c.name === rest[i]);
178
+ }
179
+ return cur?.type ?? '';
180
+ }
181
+
182
+ /** 构造一个 pill 元素 */
183
+ function createPill(key: string): HTMLElement {
184
+ const selector = key.split('.');
185
+ const label = getPillLabel(selector);
186
+ const type = getPillType(selector);
187
+
188
+ const pill = document.createElement('span');
189
+ pill.className = 'wf-pe-pill';
190
+ pill.contentEditable = 'false';
191
+ pill.dataset.key = key;
192
+ pill.dataset.type = type;
193
+ pill.title = key;
194
+
195
+ const icon = document.createElement('span');
196
+ icon.className = 'wf-pe-pill-icon';
197
+ icon.textContent = (type?.[0] ?? '·').toUpperCase();
198
+
199
+ const labelEl = document.createElement('span');
200
+ labelEl.className = 'wf-pe-pill-label';
201
+ labelEl.textContent = label;
202
+
203
+ const close = document.createElement('span');
204
+ close.className = 'wf-pe-pill-close';
205
+ close.textContent = '×';
206
+ close.title = '删除';
207
+
208
+ pill.append(icon, labelEl, close);
209
+ return pill;
210
+ }
211
+
212
+ /** ---------- 输入 / 事件 ---------- */
213
+
214
+ let inputDebounce: ReturnType<typeof setTimeout> | null = null;
215
+ function scheduleEmit() {
216
+ if (inputDebounce) clearTimeout(inputDebounce);
217
+ inputDebounce = setTimeout(() => {
218
+ const val = serializeToText();
219
+ if (val !== props.modelValue) emit('update:modelValue', val);
220
+ }, 50);
221
+ }
222
+
223
+ function onInput() {
224
+ scheduleEmit();
225
+ detectTrigger();
226
+ }
227
+
228
+ function onKeyDown(e: KeyboardEvent) {
229
+ // 面板打开时的键盘导航
230
+ if (panel.open) {
231
+ if (e.key === 'ArrowDown') {
232
+ e.preventDefault();
233
+ panel.selectedIndex++;
234
+ return;
235
+ }
236
+ if (e.key === 'ArrowUp') {
237
+ e.preventDefault();
238
+ panel.selectedIndex = Math.max(0, panel.selectedIndex - 1);
239
+ return;
240
+ }
241
+ if (e.key === 'Escape') {
242
+ e.preventDefault();
243
+ closePanel();
244
+ return;
245
+ }
246
+ // Enter 由 TagPanel 内部 emit('select') 处理
247
+ }
248
+
249
+ // Backspace 删除前面的 pill
250
+ if (e.key === 'Backspace' && !e.ctrlKey && !e.metaKey) {
251
+ const sel = window.getSelection();
252
+ if (sel && sel.rangeCount > 0 && sel.isCollapsed) {
253
+ const range = sel.getRangeAt(0);
254
+ const { startContainer, startOffset } = range;
255
+ if (startContainer.nodeType === Node.TEXT_NODE && startOffset === 0) {
256
+ const prev = (startContainer as Text).previousSibling;
257
+ if (
258
+ prev instanceof HTMLElement &&
259
+ prev.classList.contains('wf-pe-pill')
260
+ ) {
261
+ e.preventDefault();
262
+ prev.remove();
263
+ scheduleEmit();
264
+ return;
265
+ }
266
+ }
267
+ if (startContainer === editorRef.value && startOffset > 0) {
268
+ const child = editorRef.value.childNodes[startOffset - 1];
269
+ if (
270
+ child instanceof HTMLElement &&
271
+ child.classList.contains('wf-pe-pill')
272
+ ) {
273
+ e.preventDefault();
274
+ child.remove();
275
+ scheduleEmit();
276
+ return;
277
+ }
278
+ }
279
+ }
280
+ }
281
+
282
+ // Fullscreen Esc
283
+ if (e.key === 'Escape' && isFullscreen.value && !panel.open) {
284
+ e.preventDefault();
285
+ isFullscreen.value = false;
286
+ }
287
+ }
288
+
289
+ function onEditorClick(e: MouseEvent) {
290
+ const target = e.target as HTMLElement;
291
+ if (target.classList?.contains('wf-pe-pill-close')) {
292
+ const pill = target.closest('.wf-pe-pill');
293
+ if (pill) {
294
+ pill.remove();
295
+ scheduleEmit();
296
+ }
297
+ e.preventDefault();
298
+ }
299
+ }
300
+
301
+ function onPaste(e: ClipboardEvent) {
302
+ // 只粘贴纯文本,去除格式
303
+ e.preventDefault();
304
+ const text = e.clipboardData?.getData('text/plain') ?? '';
305
+ document.execCommand('insertText', false, text);
306
+ }
307
+
308
+ /** ---------- 触发检测:/ 或 {{ ---------- */
309
+ function detectTrigger() {
310
+ const sel = window.getSelection();
311
+ if (!sel || sel.rangeCount === 0 || !sel.isCollapsed) return;
312
+ const range = sel.getRangeAt(0);
313
+ const node = range.startContainer;
314
+ if (node.nodeType !== Node.TEXT_NODE) return;
315
+ const text = node.textContent ?? '';
316
+ const offset = range.startOffset;
317
+
318
+ // 检测 `/` 后的搜索文本(到当前光标)
319
+ const beforeCursor = text.slice(0, offset);
320
+ const slashIdx = beforeCursor.lastIndexOf('/');
321
+ const braceIdx = beforeCursor.lastIndexOf('{{');
322
+
323
+ // 优先取更靠近光标的触发字符
324
+ let triggerIdx = -1;
325
+ let triggerLen = 0;
326
+ if (slashIdx > braceIdx) {
327
+ triggerIdx = slashIdx;
328
+ triggerLen = 1;
329
+ } else if (braceIdx > -1) {
330
+ triggerIdx = braceIdx;
331
+ triggerLen = 2;
332
+ }
333
+ if (triggerIdx === -1) {
334
+ if (panel.open && panel.mode === 'trigger') closePanel();
335
+ return;
336
+ }
337
+
338
+ // 触发字符后到光标之间必须都是 word 字符(否则视为无效)
339
+ const searchRaw = beforeCursor.slice(triggerIdx + triggerLen);
340
+ if (/\s/.test(searchRaw)) {
341
+ if (panel.open && panel.mode === 'trigger') closePanel();
342
+ return;
343
+ }
344
+
345
+ // 触发字符前必须是空白/句首(避免 URL 中的斜杠误触发)
346
+ const charBefore = triggerIdx > 0 ? beforeCursor[triggerIdx - 1] : '';
347
+ if (charBefore && !/[\s\n]/.test(charBefore)) {
348
+ if (panel.open && panel.mode === 'trigger') closePanel();
349
+ return;
350
+ }
351
+
352
+ panel.triggerRange = {
353
+ node: node as Text,
354
+ start: triggerIdx,
355
+ end: offset,
356
+ };
357
+ panel.search = searchRaw;
358
+ panel.selectedIndex = 0;
359
+ panel.mode = 'trigger';
360
+ panel.targetElement = null;
361
+ panel.open = true;
362
+ updateAnchor(range);
363
+ }
364
+
365
+ /** 用光标位置作为面板锚点:造一个 0×0 的临时元素 */
366
+ function updateAnchor(range: Range) {
367
+ const rect = range.getBoundingClientRect();
368
+ panel.targetElement = {
369
+ getBoundingClientRect: () =>
370
+ new DOMRect(rect.left, rect.top, 0, rect.height || 18),
371
+ } as unknown as HTMLElement;
372
+ }
373
+
374
+ /** ---------- 变量插入 ---------- */
375
+
376
+ function insertPillAtSelection(pill: HTMLElement) {
377
+ const sel = window.getSelection();
378
+ if (!sel || sel.rangeCount === 0) {
379
+ editorRef.value?.appendChild(pill);
380
+ return;
381
+ }
382
+ const range = sel.getRangeAt(0);
383
+ if (!editorRef.value?.contains(range.startContainer)) {
384
+ editorRef.value?.appendChild(pill);
385
+ return;
386
+ }
387
+ range.deleteContents();
388
+ range.insertNode(pill);
389
+ // 光标移到 pill 之后
390
+ const after = document.createTextNode('​'); // zero-width space 避免光标塌陷
391
+ pill.parentNode?.insertBefore(after, pill.nextSibling);
392
+ const newRange = document.createRange();
393
+ newRange.setStart(after, 1);
394
+ newRange.setEnd(after, 1);
395
+ sel.removeAllRanges();
396
+ sel.addRange(newRange);
397
+ }
398
+
399
+ /** TagPanel emit('select', selector, tag, node) 的处理 */
400
+ function onPanelSelect(variableSelector: string[]) {
401
+ // trigger 模式:删除触发符 + 已经输入的搜索文本
402
+ if (panel.mode === 'trigger' && panel.triggerRange?.node) {
403
+ const { node, start, end } = panel.triggerRange;
404
+ const text = node.textContent ?? '';
405
+ node.textContent = text.slice(0, start) + text.slice(end);
406
+ const range = document.createRange();
407
+ range.setStart(node, start);
408
+ range.setEnd(node, start);
409
+ const sel = window.getSelection();
410
+ sel?.removeAllRanges();
411
+ sel?.addRange(range);
412
+ } else {
413
+ // insert 模式:恢复之前保存的 selection
414
+ restoreSelection();
415
+ }
416
+ const pill = createPill(variableSelector.join('.'));
417
+ insertPillAtSelection(pill);
418
+ closePanel();
419
+ scheduleEmit();
420
+ editorRef.value?.focus();
421
+ }
422
+
423
+ /** ---------- 面板打开/关闭 ---------- */
424
+
425
+ function saveSelection() {
426
+ const sel = window.getSelection();
427
+ if (
428
+ sel &&
429
+ sel.rangeCount > 0 &&
430
+ editorRef.value?.contains(sel.getRangeAt(0).startContainer)
431
+ ) {
432
+ savedRange.value = sel.getRangeAt(0).cloneRange();
433
+ }
434
+ }
435
+
436
+ function restoreSelection() {
437
+ if (!savedRange.value || !editorRef.value) return;
438
+ const sel = window.getSelection();
439
+ sel?.removeAllRanges();
440
+ sel?.addRange(savedRange.value);
441
+ editorRef.value.focus();
442
+ }
443
+
444
+ function toggleInsertPanel() {
445
+ if (panel.open && panel.mode === 'insert') {
446
+ closePanel();
447
+ return;
448
+ }
449
+ saveSelection();
450
+ panel.mode = 'insert';
451
+ panel.search = '';
452
+ panel.selectedIndex = 0;
453
+ panel.triggerRange = null;
454
+ panel.targetElement = insertBtnRef.value;
455
+ panel.open = true;
456
+ }
457
+
458
+ function closePanel() {
459
+ panel.open = false;
460
+ panel.search = '';
461
+ panel.selectedIndex = 0;
462
+ panel.triggerRange = null;
463
+ panel.targetElement = null;
464
+ }
465
+
466
+ /** ---------- 公开工具 ---------- */
467
+
468
+ const charCount = computed(() => (props.modelValue || '').length);
469
+
470
+ async function copyToClipboard() {
471
+ try {
472
+ await navigator.clipboard.writeText(props.modelValue || '');
473
+ message.success('已复制');
474
+ } catch {
475
+ message.error('复制失败');
476
+ }
477
+ }
478
+
479
+ function toggleFullscreen() {
480
+ isFullscreen.value = !isFullscreen.value;
481
+ }
482
+
483
+ function insertVariable(selector: string[]) {
484
+ editorRef.value?.focus();
485
+ const pill = createPill(selector.join('.'));
486
+ insertPillAtSelection(pill);
487
+ scheduleEmit();
488
+ }
489
+
490
+ function focus() {
491
+ editorRef.value?.focus();
492
+ }
493
+
494
+ function blur() {
495
+ editorRef.value?.blur();
496
+ }
497
+
498
+ function getContent() {
499
+ return serializeToText();
500
+ }
501
+
502
+ function setContent(str: string) {
503
+ deserializeFromText(str);
504
+ scheduleEmit();
505
+ }
506
+
507
+ defineExpose({ focus, blur, insertVariable, getContent, setContent });
508
+
509
+ /** ---------- 生命周期 & watch ---------- */
510
+
511
+ // 外部 modelValue 变化 → 只在真正不同时才 patch DOM
512
+ watch(
513
+ () => props.modelValue,
514
+ (val) => {
515
+ if (!editorRef.value) return;
516
+ if (val === serializeToText()) return;
517
+ // 尽量保留光标:粗暴做法是把光标放到末尾(外部灌值时用户一般不在编辑)
518
+ deserializeFromText(val ?? '');
519
+ },
520
+ );
521
+
522
+ function onGlobalMouseDown(e: MouseEvent) {
523
+ if (!panel.open) return;
524
+ const target = e.target as Node;
525
+ if (editorRef.value?.contains(target)) return;
526
+ if (insertBtnRef.value?.contains(target)) return;
527
+ // TagPanel 内部点击不关闭(它自己会阻止事件冒泡到 document 之外的关闭逻辑)
528
+ const panelEl = document.querySelector('.wf-tag-panel');
529
+ if (panelEl?.contains(target)) return;
530
+ closePanel();
531
+ }
532
+
533
+ onMounted(() => {
534
+ deserializeFromText(props.modelValue);
535
+ document.addEventListener('mousedown', onGlobalMouseDown);
536
+ });
537
+
538
+ onUnmounted(() => {
539
+ document.removeEventListener('mousedown', onGlobalMouseDown);
540
+ if (inputDebounce) clearTimeout(inputDebounce);
541
+ });
542
+ </script>
543
+
544
+ <template>
545
+ <div class="wf-pe" :class="{ 'wf-pe-fullscreen': isFullscreen }">
546
+ <div v-if="showToolbar" class="wf-pe-toolbar">
547
+ <div class="wf-pe-title">{{ title }}</div>
548
+ <div class="wf-pe-actions">
549
+ <a
550
+ ref="insertBtnRef"
551
+ class="wf-pe-btn"
552
+ title="插入变量"
553
+ @mousedown.prevent
554
+ @click.prevent="toggleInsertPanel"
555
+ >
556
+ <PlusOutlined />
557
+ <span>变量</span>
558
+ </a>
559
+ <a
560
+ class="wf-pe-btn"
561
+ title="复制"
562
+ @mousedown.prevent
563
+ @click.prevent="copyToClipboard"
564
+ >
565
+ <CopyOutlined />
566
+ </a>
567
+ <a
568
+ class="wf-pe-btn"
569
+ :title="isFullscreen ? '退出全屏' : '全屏(Esc 退出)'"
570
+ @mousedown.prevent
571
+ @click.prevent="toggleFullscreen"
572
+ >
573
+ <FullscreenExitOutlined v-if="isFullscreen" />
574
+ <FullscreenOutlined v-else />
575
+ </a>
576
+ </div>
577
+ </div>
578
+
579
+ <div
580
+ ref="editorRef"
581
+ class="wf-pe-input"
582
+ :contenteditable="!readonly ? 'true' : 'false'"
583
+ :data-placeholder="placeholder"
584
+ :style="{
585
+ minHeight: isFullscreen ? undefined : minHeight,
586
+ maxHeight: isFullscreen ? undefined : maxHeight,
587
+ }"
588
+ @input="onInput"
589
+ @keydown="onKeyDown"
590
+ @click="onEditorClick"
591
+ @paste="onPaste"
592
+ @mouseup="saveSelection"
593
+ @keyup="saveSelection"
594
+ @blur="saveSelection"
595
+ ></div>
596
+
597
+ <div v-if="showToolbar" class="wf-pe-footer">
598
+ <span class="wf-pe-count">{{ charCount }} 字符</span>
599
+ </div>
600
+
601
+ <PromptEditorTagPanel
602
+ :show="panel.open"
603
+ :node-id="nodeId"
604
+ :target-element="panel.targetElement"
605
+ :is-slash-mode="panel.mode === 'trigger'"
606
+ :selected-index="panel.selectedIndex"
607
+ :search="panel.search"
608
+ @close="closePanel"
609
+ @select="onPanelSelect"
610
+ />
611
+ </div>
612
+ </template>
613
+
614
+ <style scoped>
615
+ .wf-pe {
616
+ display: flex;
617
+ flex-direction: column;
618
+ border: 1px solid #e5e7eb;
619
+ border-radius: 8px;
620
+ background: #ffffff;
621
+ transition: border-color 0.15s, box-shadow 0.15s;
622
+ }
623
+
624
+ .wf-pe:focus-within {
625
+ border-color: #6366f1;
626
+ box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.12);
627
+ }
628
+
629
+ .wf-pe-fullscreen {
630
+ position: fixed;
631
+ inset: 32px;
632
+ z-index: 1500;
633
+ box-shadow: 0 20px 60px rgba(15, 23, 42, 0.25);
634
+ }
635
+
636
+ .wf-pe-toolbar {
637
+ display: flex;
638
+ align-items: center;
639
+ justify-content: space-between;
640
+ padding: 6px 10px;
641
+ background: #f9fafb;
642
+ border-bottom: 1px solid #f0f0f0;
643
+ border-top-left-radius: 8px;
644
+ border-top-right-radius: 8px;
645
+ }
646
+
647
+ .wf-pe-title {
648
+ font-size: 12px;
649
+ font-weight: 600;
650
+ color: #4b5563;
651
+ text-transform: uppercase;
652
+ letter-spacing: 0.4px;
653
+ }
654
+
655
+ .wf-pe-actions {
656
+ display: flex;
657
+ align-items: center;
658
+ gap: 4px;
659
+ }
660
+
661
+ .wf-pe-btn {
662
+ display: inline-flex;
663
+ align-items: center;
664
+ gap: 4px;
665
+ padding: 4px 8px;
666
+ font-size: 12px;
667
+ color: #4b5563;
668
+ background: transparent;
669
+ border: 1px solid transparent;
670
+ border-radius: 4px;
671
+ cursor: pointer;
672
+ transition: background 0.15s, color 0.15s;
673
+ }
674
+
675
+ .wf-pe-btn:hover {
676
+ background: #eef2ff;
677
+ color: #4338ca;
678
+ }
679
+
680
+ .wf-pe-input {
681
+ padding: 10px 12px;
682
+ overflow-y: auto;
683
+ overflow-x: hidden;
684
+ font-size: 13px;
685
+ line-height: 1.6;
686
+ color: #1f2937;
687
+ white-space: pre-wrap;
688
+ word-break: break-word;
689
+ outline: none;
690
+ }
691
+
692
+ .wf-pe-input:empty::before {
693
+ content: attr(data-placeholder);
694
+ color: #9ca3af;
695
+ pointer-events: none;
696
+ }
697
+
698
+ .wf-pe-fullscreen .wf-pe-input {
699
+ flex: 1;
700
+ min-height: 0;
701
+ font-size: 15px;
702
+ }
703
+
704
+ .wf-pe-footer {
705
+ display: flex;
706
+ justify-content: flex-end;
707
+ padding: 4px 12px;
708
+ background: #f9fafb;
709
+ border-top: 1px solid #f0f0f0;
710
+ border-bottom-left-radius: 8px;
711
+ border-bottom-right-radius: 8px;
712
+ }
713
+
714
+ .wf-pe-count {
715
+ font-size: 11px;
716
+ color: #9ca3af;
717
+ }
718
+
719
+ /* Pill 样式(非 scoped:pill 是通过 innerHTML 插入的) */
720
+ </style>
721
+
722
+ <style>
723
+ /* pill 全局样式:因为是通过 createElement 挂进 contenteditable 内部,
724
+ * <style scoped> 的 data-v-xxx 不会自动加到运行时创建的元素上 */
725
+ .wf-pe-pill {
726
+ display: inline-flex;
727
+ align-items: center;
728
+ gap: 4px;
729
+ padding: 1px 4px 1px 3px;
730
+ margin: 0 2px;
731
+ font-size: 12px;
732
+ line-height: 1.4;
733
+ color: #1f2937;
734
+ background: #eef2ff;
735
+ border: 1px solid #c7d2fe;
736
+ border-radius: 4px;
737
+ cursor: default;
738
+ user-select: none;
739
+ vertical-align: baseline;
740
+ transition: background 0.15s, border-color 0.15s;
741
+ }
742
+
743
+ .wf-pe-pill:hover {
744
+ background: #e0e7ff;
745
+ border-color: #a5b4fc;
746
+ }
747
+
748
+ .wf-pe-pill-icon {
749
+ display: inline-flex;
750
+ align-items: center;
751
+ justify-content: center;
752
+ width: 14px;
753
+ height: 14px;
754
+ font-size: 9px;
755
+ font-weight: 700;
756
+ color: #ffffff;
757
+ background: #6366f1;
758
+ border-radius: 3px;
759
+ text-transform: uppercase;
760
+ }
761
+
762
+ .wf-pe-pill[data-type='string'] .wf-pe-pill-icon {
763
+ background: #3b82f6;
764
+ }
765
+ .wf-pe-pill[data-type='number'] .wf-pe-pill-icon {
766
+ background: #f59e0b;
767
+ }
768
+ .wf-pe-pill[data-type='boolean'] .wf-pe-pill-icon {
769
+ background: #10b981;
770
+ }
771
+ .wf-pe-pill[data-type='array'] .wf-pe-pill-icon {
772
+ background: #a855f7;
773
+ }
774
+ .wf-pe-pill[data-type='object'] .wf-pe-pill-icon {
775
+ background: #6b7280;
776
+ }
777
+ .wf-pe-pill[data-type='file'] .wf-pe-pill-icon {
778
+ background: #ef4444;
779
+ }
780
+
781
+ .wf-pe-pill-label {
782
+ color: #4338ca;
783
+ font-weight: 500;
784
+ }
785
+
786
+ .wf-pe-pill-close {
787
+ display: inline-flex;
788
+ align-items: center;
789
+ justify-content: center;
790
+ width: 14px;
791
+ height: 14px;
792
+ font-size: 12px;
793
+ line-height: 1;
794
+ color: #6366f1;
795
+ border-radius: 50%;
796
+ cursor: pointer;
797
+ transition: background 0.15s;
798
+ }
799
+
800
+ .wf-pe-pill-close:hover {
801
+ background: #c7d2fe;
802
+ color: #1e1b4b;
803
+ }
804
+ </style>