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.
- package/package.json +30 -0
- package/pnpm-workspace.yaml +3 -0
- package/src/agent-flow/adapter/backend.ts +99 -0
- package/src/agent-flow/components/ChatIframePanel.vue +773 -0
- package/src/agent-flow/components/DatasetItemCard.vue +313 -0
- package/src/agent-flow/components/MemoryWindow.vue +104 -0
- package/src/agent-flow/components/PromptEditor.vue +804 -0
- package/src/agent-flow/components/PromptEditorTagPanel.vue +460 -0
- package/src/agent-flow/components/SvgIcon.vue +32 -0
- package/src/agent-flow/components/ToolItemCard.vue +169 -0
- package/src/agent-flow/components/VariableCard.vue +161 -0
- package/src/agent-flow/components/VariableSelector.vue +162 -0
- package/src/agent-flow/components/chat-iframe-types.ts +35 -0
- package/src/agent-flow/components/stubs/DatasetChooser.vue +70 -0
- package/src/agent-flow/components/stubs/Embedding.vue +22 -0
- package/src/agent-flow/components/stubs/ModelSelect.vue +24 -0
- package/src/agent-flow/components/stubs/RestfulSelectCard.vue +24 -0
- package/src/agent-flow/components/stubs/ToolChooser.vue +96 -0
- package/src/agent-flow/components/stubs/TriggersItemCard.vue +126 -0
- package/src/agent-flow/components/stubs/VariableModal.vue +115 -0
- package/src/agent-flow/index.ts +73 -0
- package/src/agent-flow/stores/workflow.ts +72 -0
- package/src/agent-flow/utils/langUtils.ts +173 -0
- package/src/agent-flow/workflow/Ability.vue +65 -0
- package/src/agent-flow/workflow/AuthorizationItem.vue +101 -0
- package/src/agent-flow/workflow/ConversationVar.vue +42 -0
- package/src/agent-flow/workflow/CustomEdge.vue +94 -0
- package/src/agent-flow/workflow/EnvironmentVar.vue +40 -0
- package/src/agent-flow/workflow/FlowDesigner.vue +2802 -0
- package/src/agent-flow/workflow/Icon.vue +179 -0
- package/src/agent-flow/workflow/NodeConfig.vue +509 -0
- package/src/agent-flow/workflow/NodeConfigCard.vue +460 -0
- package/src/agent-flow/workflow/NodeHandle.vue +147 -0
- package/src/agent-flow/workflow/NodeHeaderToolbar.vue +77 -0
- package/src/agent-flow/workflow/NodeHoverToolbar.vue +176 -0
- package/src/agent-flow/workflow/OutputItemCard.vue +668 -0
- package/src/agent-flow/workflow/ParamTableItem.vue +200 -0
- package/src/agent-flow/workflow/StructTable.vue +202 -0
- package/src/agent-flow/workflow/VarInsertField.vue +517 -0
- package/src/agent-flow/workflow/VarSelectField.vue +223 -0
- package/src/agent-flow/workflow/VariableSelectorItem.vue +48 -0
- package/src/agent-flow/workflow/WfField.vue +206 -0
- package/src/agent-flow/workflow/_config-base.css +470 -0
- package/src/agent-flow/workflow/composables/useNode.js +55 -0
- package/src/agent-flow/workflow/index.vue +53 -0
- package/src/agent-flow/workflow/nodeCard/AgentNodeCard.vue +180 -0
- package/src/agent-flow/workflow/nodeCard/AnswerNodeCard.vue +65 -0
- package/src/agent-flow/workflow/nodeCard/ClassifierCard.vue +152 -0
- package/src/agent-flow/workflow/nodeCard/CodeNodeCard.vue +139 -0
- package/src/agent-flow/workflow/nodeCard/ConditionNodeCard.vue +323 -0
- package/src/agent-flow/workflow/nodeCard/DocumentExtractorCard.vue +83 -0
- package/src/agent-flow/workflow/nodeCard/EndNodeCard.vue +123 -0
- package/src/agent-flow/workflow/nodeCard/HttpNodeCard.vue +451 -0
- package/src/agent-flow/workflow/nodeCard/HumanInputCard.vue +152 -0
- package/src/agent-flow/workflow/nodeCard/IterationCard.vue +66 -0
- package/src/agent-flow/workflow/nodeCard/KnowledgeRetrievalCard.vue +48 -0
- package/src/agent-flow/workflow/nodeCard/LLMNodeCard.vue +206 -0
- package/src/agent-flow/workflow/nodeCard/ListOperatorCard.vue +188 -0
- package/src/agent-flow/workflow/nodeCard/ParameterExtractorCard.vue +352 -0
- package/src/agent-flow/workflow/nodeCard/ServiceApiNodeCard.vue +188 -0
- package/src/agent-flow/workflow/nodeCard/StartNodeCard.vue +216 -0
- package/src/agent-flow/workflow/nodeCard/VariableAssignerCard.vue +125 -0
- package/src/agent-flow/workflow/nodeCard/VariableNodeCard.vue +121 -0
- package/src/agent-flow/workflow/nodes/AgentNode.vue +122 -0
- package/src/agent-flow/workflow/nodes/AnswerNode.vue +99 -0
- package/src/agent-flow/workflow/nodes/ClassifierNode.vue +199 -0
- package/src/agent-flow/workflow/nodes/CodeNode.vue +152 -0
- package/src/agent-flow/workflow/nodes/ConditionNode.vue +232 -0
- package/src/agent-flow/workflow/nodes/DocumentExtractorNode.vue +96 -0
- package/src/agent-flow/workflow/nodes/EndNode.vue +119 -0
- package/src/agent-flow/workflow/nodes/FileUploadNode.vue +97 -0
- package/src/agent-flow/workflow/nodes/HttpNode.vue +204 -0
- package/src/agent-flow/workflow/nodes/HumanInputNode.vue +110 -0
- package/src/agent-flow/workflow/nodes/IterationNode.vue +98 -0
- package/src/agent-flow/workflow/nodes/KnowledgeRetrievalNode.vue +253 -0
- package/src/agent-flow/workflow/nodes/LLMNode.vue +134 -0
- package/src/agent-flow/workflow/nodes/ListOperatorNode.vue +111 -0
- package/src/agent-flow/workflow/nodes/LoopNode.vue +94 -0
- package/src/agent-flow/workflow/nodes/ParameterExtractorNode.vue +147 -0
- package/src/agent-flow/workflow/nodes/ServiceApiNode.vue +161 -0
- package/src/agent-flow/workflow/nodes/StartNode.vue +199 -0
- package/src/agent-flow/workflow/nodes/TemplateNode.vue +103 -0
- package/src/agent-flow/workflow/nodes/UserInputNode.vue +92 -0
- package/src/agent-flow/workflow/nodes/VariableAssignerNode.vue +145 -0
- package/src/agent-flow/workflow/nodes/VariableNode.vue +130 -0
- package/src/agent-flow/workflow/nodes/_node-base.css +338 -0
- package/src/agent-flow/workflow/utils/node_card_form.ts +335 -0
- package/src/agent-flow/workflow/utils/node_config.ts +63 -0
- package/src/agent-flow/workflow/utils/workflow_utils.ts +151 -0
- package/src/agent-studio/adapters/springAgentStart.ts +390 -0
- package/src/agent-studio/api/index.ts +1 -0
- package/src/agent-studio/api/types.ts +173 -0
- package/src/agent-studio/components/AgentApiDocs.vue +500 -0
- package/src/agent-studio/components/AgentAppsPage.vue +1601 -0
- package/src/agent-studio/components/AgentCardGrid.vue +259 -0
- package/src/agent-studio/components/AgentChatPage.vue +553 -0
- package/src/agent-studio/components/AgentDebugPanel.vue +350 -0
- package/src/agent-studio/components/AgentLogsPanel.vue +391 -0
- package/src/agent-studio/components/AgentMonitorPanel.vue +204 -0
- package/src/agent-studio/components/AgentOrchestrate.vue +505 -0
- package/src/agent-studio/components/AgentPromptEditor.vue +237 -0
- package/src/agent-studio/components/AgentStudioShell.vue +227 -0
- package/src/agent-studio/components/AgentToolsPanel.vue +394 -0
- package/src/agent-studio/components/AgentVariablesPanel.vue +205 -0
- package/src/agent-studio/components/ApiKeyManager.vue +251 -0
- package/src/agent-studio/components/AppDesignDrawer.vue +3391 -0
- package/src/agent-studio/components/CreateAppModal.vue +737 -0
- package/src/agent-studio/components/SparkChart.vue +200 -0
- package/src/agent-studio/components/VariableEditorModal.vue +499 -0
- package/src/agent-studio/composables/useAgentStudio.ts +347 -0
- package/src/agent-studio/index.ts +60 -0
- package/src/agent-studio/panels/DrawerFlowDesigner.vue +203 -0
- package/src/agent-studio/panels/LogAnnotationPanel.vue +490 -0
- package/src/agent-studio/panels/MonitorPanel.vue +535 -0
- package/src/agent-studio/types.ts +242 -0
- package/src/index.ts +155 -0
- package/src/knowledge-hub/adapters/springAgentStart.ts +472 -0
- package/src/knowledge-hub/components/AddDocumentsWizard.vue +1452 -0
- package/src/knowledge-hub/components/CreateDatasetWizard.vue +1723 -0
- package/src/knowledge-hub/components/DatasetCardGrid.vue +467 -0
- package/src/knowledge-hub/components/DatasetDetailDrawer.vue +888 -0
- package/src/knowledge-hub/components/DatasetPicker.vue +65 -0
- package/src/knowledge-hub/components/DatasetPickerModal.vue +526 -0
- package/src/knowledge-hub/components/DatasetSettingsPanel.vue +908 -0
- package/src/knowledge-hub/components/DatasetSidebar.vue +781 -0
- package/src/knowledge-hub/components/DocumentChunksView.vue +802 -0
- package/src/knowledge-hub/components/DocumentTable.vue +552 -0
- package/src/knowledge-hub/components/HitTestingPanel.vue +74 -0
- package/src/knowledge-hub/components/KnowledgeApp.vue +66 -0
- package/src/knowledge-hub/components/KnowledgeHubApp.vue +504 -0
- package/src/knowledge-hub/components/RecallTestingPanelV2.vue +515 -0
- package/src/knowledge-hub/components/RetrievalConfigPopover.vue +270 -0
- package/src/knowledge-hub/components/RetrievalMethodPicker.vue +547 -0
- package/src/knowledge-hub/components/RetrievedList.vue +24 -0
- package/src/knowledge-hub/components/internal/KhDialog.vue +195 -0
- package/src/knowledge-hub/composables/useKnowledge.ts +89 -0
- package/src/knowledge-hub/i18n/index.ts +2 -0
- package/src/knowledge-hub/i18n/messages.ts +422 -0
- package/src/knowledge-hub/i18n/useKhI18n.ts +107 -0
- package/src/knowledge-hub/index.ts +46 -0
- package/src/knowledge-hub/styles/index.css +4 -0
- package/src/knowledge-hub/styles/tokens.css +138 -0
- package/src/knowledge-hub/types/api.ts +213 -0
- package/src/knowledge-hub/types/dataset.ts +88 -0
- package/src/knowledge-hub/types/document.ts +45 -0
- package/src/knowledge-hub/types/index.ts +9 -0
- package/src/knowledge-hub/types/retrieval.ts +46 -0
- package/src/provider-hub/components/CredentialForm.vue +103 -0
- package/src/provider-hub/components/DefaultModelsPanel.vue +270 -0
- package/src/provider-hub/components/GroupedModelSelect.vue +445 -0
- package/src/provider-hub/components/ModelCardGrid.vue +332 -0
- package/src/provider-hub/components/ModelParameterDrawer.vue +739 -0
- package/src/provider-hub/components/ModelPickerPopover.vue +1282 -0
- package/src/provider-hub/components/ProviderApp.vue +159 -0
- package/src/provider-hub/components/ProviderCredentialModal.vue +321 -0
- package/src/provider-hub/components/ProviderGallery.vue +131 -0
- package/src/provider-hub/components/ProviderHubShell.vue +1992 -0
- package/src/provider-hub/components/ProviderIcon.vue +130 -0
- package/src/provider-hub/composables/useProviderHub.ts +358 -0
- package/src/provider-hub/index.ts +43 -0
- package/src/provider-hub/svg/MiniMax.svg +1 -0
- package/src/provider-hub/svg/Volcengine.svg +1 -0
- package/src/provider-hub/svg/Zhipu.svg +1 -0
- package/src/provider-hub/svg/deepseek.svg +1 -0
- package/src/provider-hub/svg/moonshot.svg +1 -0
- package/src/provider-hub/svg/openai.svg +1 -0
- package/src/provider-hub/svg/qwen.svg +1 -0
- package/src/provider-hub/svg/siliconflow.svg +1 -0
- package/src/provider-hub/types.ts +256 -0
|
@@ -0,0 +1,1601 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* AgentAppsPage — 一站式「应用列表」页面组件。
|
|
4
|
+
*
|
|
5
|
+
* 目标:宿主只需要 `<AgentAppsPage />` 就能获得完整的 Dify 风格应用管理页 ——
|
|
6
|
+
* 列表 / 过滤 / 创建(类型选择器)/ 编辑(设计抽屉)/ 分享嵌入 / 发布 / 删除,
|
|
7
|
+
* 全部内置。api-base 默认 `/api`(宿主代理前缀),/agent-start 命名空间由组件
|
|
8
|
+
* 内部自动拼上。带鉴权的宿主用 :headers 注入 Authorization。
|
|
9
|
+
*
|
|
10
|
+
* 设计约束:
|
|
11
|
+
* - 组件只依赖 vue / vue-router / ant-design-vue / @ant-design/icons-vue,
|
|
12
|
+
* 以及本模块内的 AppDesignDrawer / CreateAppModal,绝不依赖 Vben 生态
|
|
13
|
+
* (@vben/*);
|
|
14
|
+
* - 所有后端调用都是通过 props.apiBase 拼出的 fetch,共用一个 {code, data}
|
|
15
|
+
* 信封的解包器 —— 与 useAgentStudio / useKnowledge / useProviderHub 一致;
|
|
16
|
+
* - 导航(独立对话、去配置模型)通过 useRouter() 内部完成,捕获异常防止
|
|
17
|
+
* 宿主没配对应路由时报错。
|
|
18
|
+
*/
|
|
19
|
+
import { computed, onDeactivated, onMounted, reactive, ref } from 'vue';
|
|
20
|
+
import { useRouter } from 'vue-router';
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
Button,
|
|
24
|
+
Card,
|
|
25
|
+
Dropdown,
|
|
26
|
+
Empty,
|
|
27
|
+
Form,
|
|
28
|
+
FormItem,
|
|
29
|
+
Input,
|
|
30
|
+
Menu,
|
|
31
|
+
MenuItem,
|
|
32
|
+
message,
|
|
33
|
+
Modal,
|
|
34
|
+
Select,
|
|
35
|
+
Skeleton,
|
|
36
|
+
Spin,
|
|
37
|
+
Tag,
|
|
38
|
+
Textarea,
|
|
39
|
+
} from 'ant-design-vue';
|
|
40
|
+
|
|
41
|
+
import type { ChatIframeConfig } from '../../agent-flow/components/chat-iframe-types';
|
|
42
|
+
|
|
43
|
+
import type { AppStudioApi } from '../api';
|
|
44
|
+
import type {
|
|
45
|
+
AgentEntity,
|
|
46
|
+
AgentStrategy,
|
|
47
|
+
AppType,
|
|
48
|
+
} from '../types';
|
|
49
|
+
|
|
50
|
+
import AppDesignDrawer from './AppDesignDrawer.vue';
|
|
51
|
+
import CreateAppModal from './CreateAppModal.vue';
|
|
52
|
+
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// Props
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
/**
|
|
57
|
+
* Header 值可以是静态对象,也可以是同步/异步函数 —— 每次请求前都会重新
|
|
58
|
+
* 求值,方便宿主接入自家 access-token store(token 轮换时无须重挂组件)。
|
|
59
|
+
*/
|
|
60
|
+
type HeadersLike =
|
|
61
|
+
| Record<string, string>
|
|
62
|
+
| (() => Promise<Record<string, string>> | Record<string, string>);
|
|
63
|
+
|
|
64
|
+
interface Props {
|
|
65
|
+
/**
|
|
66
|
+
* 宿主的代理前缀。默认 `/api`:与宿主 vite `/api` 代理约定一致,代理
|
|
67
|
+
* 剥掉 `/api` 后剩下的路径就是后端。组件内部会自动拼上 spring-agent-web
|
|
68
|
+
* 给控制器加的固定 `/agent-start` 命名空间 —— 那是本组件与后端的约定,
|
|
69
|
+
* 宿主不用关心。所以最终请求 URL 是 `${apiBase}/agent-start/xxx`。
|
|
70
|
+
*
|
|
71
|
+
* 若宿主没走 `/api` 代理而是同源直连后端,传 `''`(空字符串),最终
|
|
72
|
+
* 就是 `/agent-start/xxx`。
|
|
73
|
+
*/
|
|
74
|
+
apiBase?: string;
|
|
75
|
+
/**
|
|
76
|
+
* 追加到每个请求上的 header。宿主用 vben 等业务框架时,把
|
|
77
|
+
* `Authorization: Bearer xxx` 通过这里注入即可,不用改组件内部代码。
|
|
78
|
+
*
|
|
79
|
+
* 传函数就每次请求前重新求值 —— 推荐用于依赖 pinia store 的 token
|
|
80
|
+
* (登录/退出时不用重挂组件):
|
|
81
|
+
* :headers="() => ({ Authorization: `Bearer ${accessStore.accessToken}` })"
|
|
82
|
+
*
|
|
83
|
+
* 传对象就是静态 header —— 适合固定 API-key:
|
|
84
|
+
* :headers="{ 'X-Api-Key': 'xxx' }"
|
|
85
|
+
*/
|
|
86
|
+
headers?: HeadersLike;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
90
|
+
apiBase: '/api',
|
|
91
|
+
headers: () => ({}),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// Backend client — small fetch wrapper that unwraps {code, message, data}.
|
|
96
|
+
// Kept local (not exported to useAgentStudio) so this component stays a true
|
|
97
|
+
// drop-in with zero shared state.
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
const AGENT_START_NAMESPACE = '/agent-start';
|
|
100
|
+
|
|
101
|
+
interface Envelope<T> {
|
|
102
|
+
code: string;
|
|
103
|
+
message?: string;
|
|
104
|
+
data: T;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function apiUrl(path: string): string {
|
|
108
|
+
const base = props.apiBase.replace(/\/+$/, '');
|
|
109
|
+
return `${base}${AGENT_START_NAMESPACE}${path.startsWith('/') ? '' : '/'}${path}`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function resolveHeaders(): Promise<Record<string, string>> {
|
|
113
|
+
const h = props.headers;
|
|
114
|
+
const out = typeof h === 'function' ? await h() : h;
|
|
115
|
+
return out ?? {};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function call<T>(path: string, init: RequestInit = {}): Promise<T> {
|
|
119
|
+
const injected = await resolveHeaders();
|
|
120
|
+
const res = await fetch(apiUrl(path), {
|
|
121
|
+
headers: {
|
|
122
|
+
'Content-Type': 'application/json',
|
|
123
|
+
...injected,
|
|
124
|
+
...(init.headers ?? {}),
|
|
125
|
+
},
|
|
126
|
+
...init,
|
|
127
|
+
});
|
|
128
|
+
if (!res.ok) {
|
|
129
|
+
throw new Error(`${res.status} ${res.statusText}`);
|
|
130
|
+
}
|
|
131
|
+
const env = (await res.json()) as Envelope<T>;
|
|
132
|
+
if (env.code !== 'ok') {
|
|
133
|
+
throw new Error(env.message ?? env.code);
|
|
134
|
+
}
|
|
135
|
+
return env.data;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ---- CreateAgent / UpdateAgent request shape (mirrors #/api/agent) ----
|
|
139
|
+
interface CreateAgentRequestFull {
|
|
140
|
+
tenantId?: string;
|
|
141
|
+
name: string;
|
|
142
|
+
description?: string;
|
|
143
|
+
icon?: string;
|
|
144
|
+
iconBackground?: string;
|
|
145
|
+
mode?: string;
|
|
146
|
+
instructions?: string;
|
|
147
|
+
openingStatement?: string;
|
|
148
|
+
suggestedQuestions?: string[];
|
|
149
|
+
datasetIds?: string[];
|
|
150
|
+
retrievalConfig?: Record<string, unknown>;
|
|
151
|
+
modelName?: string;
|
|
152
|
+
modelProvider?: string;
|
|
153
|
+
strategy?: AgentStrategy;
|
|
154
|
+
toolNames?: string[];
|
|
155
|
+
approvalRequiredTools?: string[];
|
|
156
|
+
delegateAgentIds?: string[];
|
|
157
|
+
maxIterations?: number;
|
|
158
|
+
memoryEnabled?: boolean;
|
|
159
|
+
memoryWindow?: number;
|
|
160
|
+
published?: boolean;
|
|
161
|
+
modelSettings?: Record<string, unknown>;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ---- Agent CRUD ----
|
|
165
|
+
const listAgents = () => call<AgentEntity[]>('/agents');
|
|
166
|
+
const createAgentReq = (req: CreateAgentRequestFull) =>
|
|
167
|
+
call<AgentEntity>('/agents', { method: 'POST', body: JSON.stringify(req) });
|
|
168
|
+
const updateAgentReq = (id: string, req: CreateAgentRequestFull) =>
|
|
169
|
+
call<AgentEntity>(`/agents/${id}`, { method: 'PUT', body: JSON.stringify(req) });
|
|
170
|
+
const deleteAgentReq = (id: string) =>
|
|
171
|
+
call<void>(`/agents/${id}`, { method: 'DELETE' });
|
|
172
|
+
|
|
173
|
+
// ---- Chat stream (raw fetch — SSE) ----
|
|
174
|
+
async function chatStreamReq(id: string, body: { query: string }) {
|
|
175
|
+
const injected = await resolveHeaders();
|
|
176
|
+
return fetch(apiUrl(`/agents/${id}/chat/stream`), {
|
|
177
|
+
method: 'POST',
|
|
178
|
+
headers: {
|
|
179
|
+
'Content-Type': 'application/json',
|
|
180
|
+
Accept: 'text/event-stream',
|
|
181
|
+
...injected,
|
|
182
|
+
},
|
|
183
|
+
body: JSON.stringify(body),
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ---- Conversations + history (called by AppStudioApi bag) ----
|
|
188
|
+
interface ConversationSummary {
|
|
189
|
+
conversationId: string;
|
|
190
|
+
name?: string;
|
|
191
|
+
userId?: null | string;
|
|
192
|
+
firstMessage?: string;
|
|
193
|
+
updatedAt?: string;
|
|
194
|
+
pinned?: boolean;
|
|
195
|
+
}
|
|
196
|
+
interface HistoryMessage {
|
|
197
|
+
role: 'ASSISTANT' | 'SYSTEM' | 'USER';
|
|
198
|
+
content: string;
|
|
199
|
+
}
|
|
200
|
+
const listConversationsReq = (appId: string, limit = 100) =>
|
|
201
|
+
call<ConversationSummary[]>(`/chat/conversations/${appId}`, {
|
|
202
|
+
method: 'POST',
|
|
203
|
+
body: JSON.stringify({ limit }),
|
|
204
|
+
});
|
|
205
|
+
const fetchHistoryReq = (appId: string, conversationId: string, limit = 500) =>
|
|
206
|
+
call<HistoryMessage[]>(
|
|
207
|
+
`/chat/conversations/${appId}/${conversationId}/messages`,
|
|
208
|
+
{ method: 'POST', body: JSON.stringify({ limit }) },
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
// ---- Annotations ----
|
|
212
|
+
interface AppAnnotation {
|
|
213
|
+
id: string;
|
|
214
|
+
appId: string;
|
|
215
|
+
question?: string;
|
|
216
|
+
content?: string;
|
|
217
|
+
enabled?: boolean;
|
|
218
|
+
hitCount?: number;
|
|
219
|
+
createdAt?: string;
|
|
220
|
+
updatedAt?: string;
|
|
221
|
+
}
|
|
222
|
+
interface AppAnnotationRequest {
|
|
223
|
+
question: string;
|
|
224
|
+
content: string;
|
|
225
|
+
enabled?: boolean;
|
|
226
|
+
}
|
|
227
|
+
const listAnnotationsReq = (appId: string) =>
|
|
228
|
+
call<AppAnnotation[]>(`/apps/${appId}/annotations`);
|
|
229
|
+
const createAnnotationReq = (appId: string, req: AppAnnotationRequest) =>
|
|
230
|
+
call<AppAnnotation>(`/apps/${appId}/annotations`, {
|
|
231
|
+
method: 'POST',
|
|
232
|
+
body: JSON.stringify(req),
|
|
233
|
+
});
|
|
234
|
+
const updateAnnotationReq = (
|
|
235
|
+
appId: string,
|
|
236
|
+
id: string,
|
|
237
|
+
req: AppAnnotationRequest,
|
|
238
|
+
) =>
|
|
239
|
+
call<AppAnnotation>(`/apps/${appId}/annotations/${id}`, {
|
|
240
|
+
method: 'PUT',
|
|
241
|
+
body: JSON.stringify(req),
|
|
242
|
+
});
|
|
243
|
+
const deleteAnnotationReq = (appId: string, id: string) =>
|
|
244
|
+
call<void>(`/apps/${appId}/annotations/${id}`, { method: 'DELETE' });
|
|
245
|
+
|
|
246
|
+
// ---- API keys ----
|
|
247
|
+
interface AppApiKey {
|
|
248
|
+
id: string;
|
|
249
|
+
appId: string;
|
|
250
|
+
name?: string;
|
|
251
|
+
type?: string;
|
|
252
|
+
token: string;
|
|
253
|
+
createdAt?: string;
|
|
254
|
+
lastUsedAt?: string;
|
|
255
|
+
}
|
|
256
|
+
const listApiKeysReq = (appId: string) =>
|
|
257
|
+
call<AppApiKey[]>(`/apps/${appId}/api-tokens`);
|
|
258
|
+
const createApiKeyReq = (appId: string, name?: string) =>
|
|
259
|
+
call<AppApiKey>(`/apps/${appId}/api-tokens`, {
|
|
260
|
+
method: 'POST',
|
|
261
|
+
body: JSON.stringify({ name }),
|
|
262
|
+
});
|
|
263
|
+
const renameApiKeyReq = (appId: string, id: string, name: string) =>
|
|
264
|
+
call<AppApiKey>(`/apps/${appId}/api-tokens/${id}/rename`, {
|
|
265
|
+
method: 'POST',
|
|
266
|
+
body: JSON.stringify({ name }),
|
|
267
|
+
});
|
|
268
|
+
const deleteApiKeyReq = (appId: string, id: string) =>
|
|
269
|
+
call<void>(`/apps/${appId}/api-tokens/${id}`, { method: 'DELETE' });
|
|
270
|
+
|
|
271
|
+
// ---- Metrics / LLM Ops ----
|
|
272
|
+
interface AppMetrics {
|
|
273
|
+
appId: string;
|
|
274
|
+
totalConversations: number;
|
|
275
|
+
totalMessages: number;
|
|
276
|
+
userMessages: number;
|
|
277
|
+
assistantMessages: number;
|
|
278
|
+
avgInteractionsPerConversation: number;
|
|
279
|
+
lastActivityAt?: string;
|
|
280
|
+
}
|
|
281
|
+
interface LlmUsageStats {
|
|
282
|
+
model?: string;
|
|
283
|
+
calls: number;
|
|
284
|
+
errors: number;
|
|
285
|
+
promptTokens: number;
|
|
286
|
+
completionTokens: number;
|
|
287
|
+
totalTokens: number;
|
|
288
|
+
costMicros: number;
|
|
289
|
+
avgLatencyMs: number;
|
|
290
|
+
}
|
|
291
|
+
interface LlmCallRecord {
|
|
292
|
+
id: string;
|
|
293
|
+
provider: string;
|
|
294
|
+
model: string;
|
|
295
|
+
promptTokens?: number;
|
|
296
|
+
completionTokens?: number;
|
|
297
|
+
totalTokens?: number;
|
|
298
|
+
costMicros?: number;
|
|
299
|
+
latencyMs?: number;
|
|
300
|
+
success: boolean;
|
|
301
|
+
errorType?: string;
|
|
302
|
+
createdAt?: string;
|
|
303
|
+
}
|
|
304
|
+
const fetchAppMetricsReq = (appId: string) =>
|
|
305
|
+
call<AppMetrics>(`/apps/${appId}/metrics`);
|
|
306
|
+
const fetchTotalReq = () => call<LlmUsageStats>('/llmops/total');
|
|
307
|
+
const fetchRecentReq = (limit = 50) =>
|
|
308
|
+
call<LlmCallRecord[]>(`/llmops/recent?limit=${limit}`);
|
|
309
|
+
|
|
310
|
+
// ---- Models / Tools / Datasets ----
|
|
311
|
+
interface ModelEntity {
|
|
312
|
+
id: string;
|
|
313
|
+
tenantId?: string;
|
|
314
|
+
providerName: string;
|
|
315
|
+
modelName: string;
|
|
316
|
+
modelType: string;
|
|
317
|
+
enabled: boolean;
|
|
318
|
+
isDefault: boolean;
|
|
319
|
+
}
|
|
320
|
+
interface ToolView {
|
|
321
|
+
name: string;
|
|
322
|
+
description: string;
|
|
323
|
+
inputSchema?: string;
|
|
324
|
+
}
|
|
325
|
+
interface DatasetEntity {
|
|
326
|
+
id: string;
|
|
327
|
+
name: string;
|
|
328
|
+
description?: string;
|
|
329
|
+
}
|
|
330
|
+
const listModelsReq = (type?: string) =>
|
|
331
|
+
call<ModelEntity[]>(`/models${type ? `?type=${encodeURIComponent(type)}` : ''}`);
|
|
332
|
+
const listToolsReq = () => call<ToolView[]>('/tools');
|
|
333
|
+
const listDatasetsReq = () => call<DatasetEntity[]>('/datasets');
|
|
334
|
+
|
|
335
|
+
// ---- Workflow draft ----
|
|
336
|
+
interface WorkflowEntity {
|
|
337
|
+
id: string;
|
|
338
|
+
appId?: string;
|
|
339
|
+
graph?: unknown;
|
|
340
|
+
version?: string;
|
|
341
|
+
published?: boolean;
|
|
342
|
+
}
|
|
343
|
+
const getWorkflowDraftReq = (appId: string) =>
|
|
344
|
+
call<WorkflowEntity>(`/apps/${appId}/workflow/draft`);
|
|
345
|
+
const saveWorkflowDraftReq = (appId: string, graph: unknown) =>
|
|
346
|
+
call<WorkflowEntity>(`/apps/${appId}/workflow/draft`, {
|
|
347
|
+
method: 'PUT',
|
|
348
|
+
body: JSON.stringify({ graph }),
|
|
349
|
+
});
|
|
350
|
+
const publishWorkflowDraftReq = (appId: string) =>
|
|
351
|
+
call<WorkflowEntity>(`/apps/${appId}/workflow/publish`, {
|
|
352
|
+
method: 'POST',
|
|
353
|
+
body: JSON.stringify({}),
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
// ---------------------------------------------------------------------------
|
|
357
|
+
// AppStudioApi callback bag — the drawer's built-in panels call these.
|
|
358
|
+
// ---------------------------------------------------------------------------
|
|
359
|
+
const studioApi: AppStudioApi = {
|
|
360
|
+
listConversations: (appId: string, limit?: number) =>
|
|
361
|
+
listConversationsReq(appId, limit),
|
|
362
|
+
fetchHistory: (appId: string, conversationId: string, limit?: number) =>
|
|
363
|
+
fetchHistoryReq(appId, conversationId, limit),
|
|
364
|
+
listAnnotations: (appId: string) => listAnnotationsReq(appId),
|
|
365
|
+
createAnnotation: (appId: string, req: any) =>
|
|
366
|
+
createAnnotationReq(appId, req),
|
|
367
|
+
updateAnnotation: (appId: string, id: string, req: any) =>
|
|
368
|
+
updateAnnotationReq(appId, id, req),
|
|
369
|
+
deleteAnnotation: (appId: string, id: string) =>
|
|
370
|
+
deleteAnnotationReq(appId, id),
|
|
371
|
+
fetchAppMetrics: (appId: string) => fetchAppMetricsReq(appId),
|
|
372
|
+
fetchLlmUsage: () => fetchTotalReq() as any,
|
|
373
|
+
fetchRecentLlmCalls: (limit?: number) => fetchRecentReq(limit) as any,
|
|
374
|
+
listApiKeys: (appId: string) => listApiKeysReq(appId) as any,
|
|
375
|
+
createApiKey: (appId: string, name?: string) =>
|
|
376
|
+
createApiKeyReq(appId, name) as any,
|
|
377
|
+
renameApiKey: (appId: string, id: string, name: string) =>
|
|
378
|
+
renameApiKeyReq(appId, id, name) as any,
|
|
379
|
+
deleteApiKey: (appId: string, id: string) => deleteApiKeyReq(appId, id),
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
// ---------------------------------------------------------------------------
|
|
383
|
+
// Navigation helpers — routed via useRouter(); wrapped in try/catch so hosts
|
|
384
|
+
// that lack the expected routes (AgentChat, ModelList) don't blow up.
|
|
385
|
+
// ---------------------------------------------------------------------------
|
|
386
|
+
const router = useRouter();
|
|
387
|
+
|
|
388
|
+
function goToModel() {
|
|
389
|
+
try {
|
|
390
|
+
router?.push({ name: 'ModelList' });
|
|
391
|
+
} catch {
|
|
392
|
+
// Host doesn't have a ModelList route — silently no-op.
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
function openChat(a: AgentEntity) {
|
|
396
|
+
try {
|
|
397
|
+
router?.push({ name: 'AgentChat', params: { id: a.id } });
|
|
398
|
+
} catch {
|
|
399
|
+
// Host doesn't have an AgentChat route — silently no-op.
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// ---------------------------------------------------------------------------
|
|
404
|
+
// Chat iframe config — minimal port of #/adapter/chat-iframe-config.ts, sans
|
|
405
|
+
// any @vben/preferences / @vben/stores dependency. `context.user` stays null
|
|
406
|
+
// (hosts can wrap the iframe in their own theme provider if needed).
|
|
407
|
+
// ---------------------------------------------------------------------------
|
|
408
|
+
interface AgentLike {
|
|
409
|
+
id?: string;
|
|
410
|
+
name?: string;
|
|
411
|
+
mode?: string;
|
|
412
|
+
workflowId?: string;
|
|
413
|
+
difyApiKey?: string;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function resolveIframeBase(): string {
|
|
417
|
+
const raw = (import.meta.env.VITE_CHAT_IFRAME_BASE as string | undefined)?.trim();
|
|
418
|
+
const base = raw && raw.length > 0 ? raw : '/chat/';
|
|
419
|
+
return base.endsWith('/') ? base : `${base}/`;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function resolveApiKey(agent?: AgentLike | null): string | undefined {
|
|
423
|
+
const fromAgent = agent?.difyApiKey?.trim();
|
|
424
|
+
if (fromAgent) return fromAgent;
|
|
425
|
+
const fromEnv = (
|
|
426
|
+
import.meta.env.VITE_DIFY_APP_KEY as string | undefined
|
|
427
|
+
)?.trim();
|
|
428
|
+
if (fromEnv) return fromEnv;
|
|
429
|
+
return agent?.id?.trim() || undefined;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function buildChatIframeConfig(
|
|
433
|
+
agent: AgentLike | null,
|
|
434
|
+
options: { debug?: boolean } = {},
|
|
435
|
+
): ChatIframeConfig {
|
|
436
|
+
const src = resolveIframeBase();
|
|
437
|
+
const difyApiKey = resolveApiKey(agent);
|
|
438
|
+
const label = agent?.name || agent?.id || 'spring-agent';
|
|
439
|
+
const workflowId = agent?.workflowId;
|
|
440
|
+
return {
|
|
441
|
+
src,
|
|
442
|
+
title: agent?.name ? `调试:${agent.name}` : '调试与预览',
|
|
443
|
+
sessionKey: `${agent?.mode ?? 'agent'}-${agent?.id ?? 'draft'}`,
|
|
444
|
+
params: {
|
|
445
|
+
mode: 'Copilot',
|
|
446
|
+
difyApiKey,
|
|
447
|
+
label,
|
|
448
|
+
appId: agent?.id,
|
|
449
|
+
workflowId,
|
|
450
|
+
debug: options.debug ? 'true' : undefined,
|
|
451
|
+
},
|
|
452
|
+
context: {
|
|
453
|
+
appId: agent?.id,
|
|
454
|
+
appMode: agent?.mode,
|
|
455
|
+
appName: agent?.name,
|
|
456
|
+
// Vben-specific user store lookup removed — hosts that want to plumb
|
|
457
|
+
// theme/user info through can post-message it themselves after mount.
|
|
458
|
+
user: null,
|
|
459
|
+
theme: 'light',
|
|
460
|
+
},
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// ---------------------------------------------------------------------------
|
|
465
|
+
// Reactive state — direct port of the original list.vue.
|
|
466
|
+
// ---------------------------------------------------------------------------
|
|
467
|
+
const agents = ref<AgentEntity[]>([]);
|
|
468
|
+
const llmModels = ref<ModelEntity[]>([]);
|
|
469
|
+
const tools = ref<ToolView[]>([]);
|
|
470
|
+
const datasets = ref<DatasetEntity[]>([]);
|
|
471
|
+
const loading = ref(false);
|
|
472
|
+
const llmLoaded = ref(false);
|
|
473
|
+
|
|
474
|
+
const keyword = ref('');
|
|
475
|
+
const filterStrategy = ref<'ALL' | AgentStrategy>('ALL');
|
|
476
|
+
/** Draft vs published filter — Dify's "只看已发布 / 只看草稿" toggle. */
|
|
477
|
+
const filterPublished = ref<'all' | 'draft' | 'published'>('all');
|
|
478
|
+
|
|
479
|
+
const showCreate = ref(false);
|
|
480
|
+
const submitting = ref(false);
|
|
481
|
+
/**
|
|
482
|
+
* Dify-style two-step create: first pick an "app type" from CreateAppModal,
|
|
483
|
+
* then land in the config form with defaults seeded from that choice.
|
|
484
|
+
*/
|
|
485
|
+
const showTypePicker = ref(false);
|
|
486
|
+
const editingId = ref<null | string>(null);
|
|
487
|
+
/**
|
|
488
|
+
* Basic-info edit form — the "编辑基本信息" surface intentionally only carries
|
|
489
|
+
* the four presentation fields (name / icon / iconBackground / description).
|
|
490
|
+
*/
|
|
491
|
+
const form = reactive({
|
|
492
|
+
name: '',
|
|
493
|
+
description: '',
|
|
494
|
+
icon: '',
|
|
495
|
+
iconBackground: '',
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
// Dify's app-card mode icons — colored square with an emoji
|
|
499
|
+
const ICONS = ['🤖', '💬', '🧠', '🎯', '🛠', '📊', '💡', '⚡', '🔎', '📎'];
|
|
500
|
+
const BGS = [
|
|
501
|
+
'#FEF3F2',
|
|
502
|
+
'#EEF4FF',
|
|
503
|
+
'#EFFDF4',
|
|
504
|
+
'#FFF4ED',
|
|
505
|
+
'#F0F9FF',
|
|
506
|
+
'#FEF6EE',
|
|
507
|
+
'#FDF2FA',
|
|
508
|
+
'#F0FDF9',
|
|
509
|
+
];
|
|
510
|
+
function hashCode(s: string): number {
|
|
511
|
+
let h = 0;
|
|
512
|
+
for (let i = 0; i < s.length; i++) {
|
|
513
|
+
h = Math.trunc((h << 5) - h + s.charCodeAt(i));
|
|
514
|
+
}
|
|
515
|
+
return Math.abs(h);
|
|
516
|
+
}
|
|
517
|
+
function iconOf(a: AgentEntity) {
|
|
518
|
+
return a.icon || ICONS[hashCode(a.id || a.name) % ICONS.length];
|
|
519
|
+
}
|
|
520
|
+
function bgOf(a: AgentEntity) {
|
|
521
|
+
return (
|
|
522
|
+
a.iconBackground || BGS[hashCode((a.id || a.name) + '.bg') % BGS.length]
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
function modeLabel(m?: string): string {
|
|
526
|
+
if (m === 'chat') return 'Chat';
|
|
527
|
+
if (m === 'chatflow') return 'Chatflow';
|
|
528
|
+
if (m === 'workflow') return 'Workflow';
|
|
529
|
+
if (m === 'completion') return 'Completion';
|
|
530
|
+
return 'Agent';
|
|
531
|
+
}
|
|
532
|
+
function modeColor(m?: string): string {
|
|
533
|
+
if (m === 'chat') return 'blue';
|
|
534
|
+
if (m === 'chatflow') return 'cyan';
|
|
535
|
+
if (m === 'workflow') return 'purple';
|
|
536
|
+
if (m === 'completion') return 'orange';
|
|
537
|
+
return 'geekblue';
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function fromNow(iso?: string): string {
|
|
541
|
+
if (!iso) return '';
|
|
542
|
+
const t = new Date(iso).getTime();
|
|
543
|
+
if (Number.isNaN(t)) return '';
|
|
544
|
+
const diff = (Date.now() - t) / 1000;
|
|
545
|
+
if (diff < 60) return '刚刚';
|
|
546
|
+
if (diff < 3600) return `${Math.floor(diff / 60)} 分钟前`;
|
|
547
|
+
if (diff < 86_400) return `${Math.floor(diff / 3600)} 小时前`;
|
|
548
|
+
if (diff < 86_400 * 30) return `${Math.floor(diff / 86_400)} 天前`;
|
|
549
|
+
return new Date(iso).toLocaleDateString();
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function strategyLabel(s?: string): string {
|
|
553
|
+
if (s === 'REACT') return 'ReAct';
|
|
554
|
+
if (s === 'FUNCTION_CALLING') return 'Function Calling';
|
|
555
|
+
if (s === 'PLAN_EXECUTE') return 'Plan & Execute';
|
|
556
|
+
return s ?? '';
|
|
557
|
+
}
|
|
558
|
+
function strategyColor(s?: string): string {
|
|
559
|
+
if (s === 'FUNCTION_CALLING') return 'green';
|
|
560
|
+
if (s === 'PLAN_EXECUTE') return 'purple';
|
|
561
|
+
return 'blue';
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function modelLabel(a: AgentEntity): string {
|
|
565
|
+
if (a.modelProvider && a.modelName) {
|
|
566
|
+
return `${a.modelProvider} · ${a.modelName}`;
|
|
567
|
+
}
|
|
568
|
+
if (a.modelName) return a.modelName;
|
|
569
|
+
return '未选择模型';
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const filtered = computed(() => {
|
|
573
|
+
const q = keyword.value.trim().toLowerCase();
|
|
574
|
+
return agents.value.filter((a) => {
|
|
575
|
+
if (
|
|
576
|
+
filterStrategy.value !== 'ALL' &&
|
|
577
|
+
a.strategy !== filterStrategy.value
|
|
578
|
+
) {
|
|
579
|
+
return false;
|
|
580
|
+
}
|
|
581
|
+
if (filterPublished.value === 'published' && a.published === false)
|
|
582
|
+
return false;
|
|
583
|
+
if (filterPublished.value === 'draft' && a.published !== false)
|
|
584
|
+
return false;
|
|
585
|
+
if (!q) return true;
|
|
586
|
+
return (
|
|
587
|
+
a.name.toLowerCase().includes(q) ||
|
|
588
|
+
(a.description ?? '').toLowerCase().includes(q) ||
|
|
589
|
+
(a.instructions ?? '').toLowerCase().includes(q)
|
|
590
|
+
);
|
|
591
|
+
});
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
/** Convenience count helpers for the filter tabs. */
|
|
595
|
+
const publishedCount = computed(
|
|
596
|
+
() => agents.value.filter((a) => a.published !== false).length,
|
|
597
|
+
);
|
|
598
|
+
const draftCount = computed(
|
|
599
|
+
() => agents.value.filter((a) => a.published === false).length,
|
|
600
|
+
);
|
|
601
|
+
|
|
602
|
+
async function refresh() {
|
|
603
|
+
loading.value = true;
|
|
604
|
+
try {
|
|
605
|
+
agents.value = await listAgents();
|
|
606
|
+
} catch (e: any) {
|
|
607
|
+
// 后端 401/404/网络断开等都在这里兜住 —— 不能让异常冒到
|
|
608
|
+
// onMounted 里变成 unhandledrejection,那会触发 vite 错误覆盖层,
|
|
609
|
+
// 视觉上像 <KeepAlive> 里的路由切换全部白屏。
|
|
610
|
+
agents.value = [];
|
|
611
|
+
message.error(`加载应用列表失败:${e?.message ?? e}`);
|
|
612
|
+
} finally {
|
|
613
|
+
loading.value = false;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
async function loadDeps() {
|
|
618
|
+
try {
|
|
619
|
+
llmModels.value = await listModelsReq('LLM');
|
|
620
|
+
} catch {
|
|
621
|
+
llmModels.value = [];
|
|
622
|
+
} finally {
|
|
623
|
+
llmLoaded.value = true;
|
|
624
|
+
}
|
|
625
|
+
try {
|
|
626
|
+
tools.value = await listToolsReq();
|
|
627
|
+
} catch {
|
|
628
|
+
tools.value = [];
|
|
629
|
+
}
|
|
630
|
+
try {
|
|
631
|
+
datasets.value = await listDatasetsReq();
|
|
632
|
+
} catch {
|
|
633
|
+
datasets.value = [];
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function openTypePicker() {
|
|
638
|
+
showTypePicker.value = true;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Dify-parity create flow — always creates an `apps` row and refreshes the list
|
|
643
|
+
* in place. Creating an app must never be gated on model availability.
|
|
644
|
+
*
|
|
645
|
+
* The 4 app types map to Dify's `mode` field on the `apps` row so a single
|
|
646
|
+
* table backs the whole family:
|
|
647
|
+
* chatbot → mode='chat' · REACT
|
|
648
|
+
* agent → mode='agent' · FUNCTION_CALLING
|
|
649
|
+
* workflow → mode='workflow' · FUNCTION_CALLING + empty graph seed
|
|
650
|
+
* text-generator → mode='completion' · PLAN_EXECUTE, memory off
|
|
651
|
+
*/
|
|
652
|
+
async function onCreateApp(payload: {
|
|
653
|
+
appType: AppType;
|
|
654
|
+
name: string;
|
|
655
|
+
description: string;
|
|
656
|
+
icon: { emoji: string; background: string };
|
|
657
|
+
}) {
|
|
658
|
+
if (!llmLoaded.value) {
|
|
659
|
+
await loadDeps();
|
|
660
|
+
}
|
|
661
|
+
const defaultModel =
|
|
662
|
+
llmModels.value.find((m) => m.isDefault) ?? llmModels.value[0];
|
|
663
|
+
|
|
664
|
+
const modeByType: Record<string, string> = {
|
|
665
|
+
agent: 'agent',
|
|
666
|
+
chatbot: 'chat',
|
|
667
|
+
chatflow: 'chatflow',
|
|
668
|
+
'text-generator': 'completion',
|
|
669
|
+
workflow: 'workflow',
|
|
670
|
+
};
|
|
671
|
+
const strategyByType: Record<string, AgentStrategy> = {
|
|
672
|
+
agent: 'FUNCTION_CALLING',
|
|
673
|
+
chatbot: 'REACT',
|
|
674
|
+
chatflow: 'REACT',
|
|
675
|
+
'text-generator': 'PLAN_EXECUTE',
|
|
676
|
+
workflow: 'FUNCTION_CALLING',
|
|
677
|
+
};
|
|
678
|
+
const mode = modeByType[payload.appType] ?? 'agent';
|
|
679
|
+
const strategy = strategyByType[payload.appType] ?? 'REACT';
|
|
680
|
+
const maxIterations = payload.appType === 'text-generator' ? 3 : 6;
|
|
681
|
+
const memoryWindow = payload.appType === 'text-generator' ? 0 : 20;
|
|
682
|
+
|
|
683
|
+
submitting.value = true;
|
|
684
|
+
try {
|
|
685
|
+
await createAgentReq({
|
|
686
|
+
name: payload.name,
|
|
687
|
+
description: payload.description || undefined,
|
|
688
|
+
icon: payload.icon.emoji,
|
|
689
|
+
iconBackground: payload.icon.background,
|
|
690
|
+
mode,
|
|
691
|
+
instructions: payload.description || undefined,
|
|
692
|
+
modelName: defaultModel?.modelName ?? undefined,
|
|
693
|
+
modelProvider: defaultModel?.providerName ?? undefined,
|
|
694
|
+
strategy,
|
|
695
|
+
toolNames: [],
|
|
696
|
+
maxIterations,
|
|
697
|
+
memoryEnabled: memoryWindow > 0,
|
|
698
|
+
memoryWindow,
|
|
699
|
+
});
|
|
700
|
+
message.success(`已创建「${payload.name}」`);
|
|
701
|
+
await refresh();
|
|
702
|
+
} catch (e: any) {
|
|
703
|
+
message.error(e?.message ?? '创建失败');
|
|
704
|
+
} finally {
|
|
705
|
+
submitting.value = false;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// ---------------------------------------------------------------------------
|
|
710
|
+
// Design drawer — click a card to open a Dify-style drawer where the user can
|
|
711
|
+
// design the app. Workflow-mode apps see the DAG canvas; others see the
|
|
712
|
+
// orchestrate/preview two-pane. Persisted via updateAgent().
|
|
713
|
+
// ---------------------------------------------------------------------------
|
|
714
|
+
const drawerOpen = ref(false);
|
|
715
|
+
const drawerApp = ref<AgentEntity | null>(null);
|
|
716
|
+
/**
|
|
717
|
+
* Draft-workflow graph JSON prefetched for the currently open drawer. Only
|
|
718
|
+
* populated for workflow / chatflow apps.
|
|
719
|
+
*/
|
|
720
|
+
const drawerGraphJson = ref<string>('');
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* 抽屉里挂 ChatIframePanel / FlowDesigner 的调试 iframe 配置。每次打开抽屉时
|
|
724
|
+
* 按当前 app 重新构造 —— session key 走 mode+id,切换 app 不会串会话。
|
|
725
|
+
*/
|
|
726
|
+
const drawerChatConfig = computed<ChatIframeConfig | null>(() =>
|
|
727
|
+
drawerApp.value
|
|
728
|
+
? buildChatIframeConfig(drawerApp.value as AgentLike, { debug: true })
|
|
729
|
+
: null,
|
|
730
|
+
);
|
|
731
|
+
|
|
732
|
+
async function openDesigner(a: AgentEntity) {
|
|
733
|
+
drawerApp.value = a;
|
|
734
|
+
drawerGraphJson.value = '';
|
|
735
|
+
// Prefetch the draft BEFORE opening the drawer for flow-mode apps so
|
|
736
|
+
// FlowDesigner mounts with the real payload already on `props` (avoids a
|
|
737
|
+
// race with initGraph()'s setTimeout(10) seed).
|
|
738
|
+
if (a.mode === 'workflow' || a.mode === 'chatflow') {
|
|
739
|
+
try {
|
|
740
|
+
const draft = await getWorkflowDraftReq(a.id);
|
|
741
|
+
if (draft?.graph) drawerGraphJson.value = JSON.stringify(draft.graph);
|
|
742
|
+
} catch {
|
|
743
|
+
drawerGraphJson.value = '';
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
drawerOpen.value = true;
|
|
747
|
+
if (!llmLoaded.value) loadDeps();
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
/**
|
|
751
|
+
* Safely parse the drawer's serialised retrieval config so we can hand the
|
|
752
|
+
* backend a plain object. Empty / malformed input returns undefined.
|
|
753
|
+
*/
|
|
754
|
+
function parseRetrievalConfig(
|
|
755
|
+
json?: string,
|
|
756
|
+
): Record<string, unknown> | undefined {
|
|
757
|
+
if (!json) return undefined;
|
|
758
|
+
try {
|
|
759
|
+
const v = JSON.parse(json);
|
|
760
|
+
return v && typeof v === 'object' && !Array.isArray(v) ? v : undefined;
|
|
761
|
+
} catch {
|
|
762
|
+
return undefined;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
async function onDrawerSave(payload: {
|
|
767
|
+
appId: string;
|
|
768
|
+
mode: string;
|
|
769
|
+
graphJson?: string;
|
|
770
|
+
name?: string;
|
|
771
|
+
instructions?: string;
|
|
772
|
+
openingStatement?: string;
|
|
773
|
+
modelName?: string;
|
|
774
|
+
modelProvider?: string;
|
|
775
|
+
modelSettings?: Record<string, unknown>;
|
|
776
|
+
toolNames?: string[];
|
|
777
|
+
datasetIds?: string[];
|
|
778
|
+
retrievalConfigJson?: string;
|
|
779
|
+
}) {
|
|
780
|
+
const existing = agents.value.find((x) => x.id === payload.appId);
|
|
781
|
+
submitting.value = true;
|
|
782
|
+
try {
|
|
783
|
+
await updateAgentReq(payload.appId, {
|
|
784
|
+
name: payload.name ?? existing?.name ?? '',
|
|
785
|
+
mode: payload.mode,
|
|
786
|
+
instructions: payload.instructions,
|
|
787
|
+
openingStatement: payload.openingStatement,
|
|
788
|
+
modelName: payload.modelName ?? existing?.modelName,
|
|
789
|
+
modelProvider: payload.modelProvider ?? existing?.modelProvider,
|
|
790
|
+
modelSettings: payload.modelSettings,
|
|
791
|
+
strategy: (existing?.strategy as AgentStrategy) ?? 'REACT',
|
|
792
|
+
toolNames: payload.toolNames ?? [],
|
|
793
|
+
datasetIds: payload.datasetIds ?? [],
|
|
794
|
+
retrievalConfig: parseRetrievalConfig(payload.retrievalConfigJson),
|
|
795
|
+
maxIterations: existing?.maxIterations,
|
|
796
|
+
memoryEnabled: existing?.memoryEnabled,
|
|
797
|
+
memoryWindow: existing?.memoryWindow,
|
|
798
|
+
published: existing?.published,
|
|
799
|
+
});
|
|
800
|
+
if (
|
|
801
|
+
(payload.mode === 'workflow' || payload.mode === 'chatflow') &&
|
|
802
|
+
payload.graphJson
|
|
803
|
+
) {
|
|
804
|
+
try {
|
|
805
|
+
const parsed = JSON.parse(payload.graphJson);
|
|
806
|
+
await saveWorkflowDraftReq(payload.appId, parsed);
|
|
807
|
+
} catch {
|
|
808
|
+
message.warning('画布数据无法解析,草稿未保存');
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
message.success('已保存');
|
|
812
|
+
await refresh();
|
|
813
|
+
const fresh = agents.value.find((x) => x.id === payload.appId);
|
|
814
|
+
if (fresh) drawerApp.value = fresh;
|
|
815
|
+
} catch (e: any) {
|
|
816
|
+
message.error(e?.message ?? '保存失败');
|
|
817
|
+
} finally {
|
|
818
|
+
submitting.value = false;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* Brand popover → 编辑信息 shortcut. Reuses the same basic-info modal the
|
|
824
|
+
* card ⋯ menu opens; the drawer stays open so the user can drop back into
|
|
825
|
+
* design after saving.
|
|
826
|
+
*/
|
|
827
|
+
function onDrawerEditInfo(payload: { appId: string }) {
|
|
828
|
+
const a = agents.value.find((x) => x.id === payload.appId);
|
|
829
|
+
if (a) openEdit(a);
|
|
830
|
+
}
|
|
831
|
+
function onDrawerExportDsl(_payload: { appId: string }) {
|
|
832
|
+
message.info('导出 DSL 功能开发中');
|
|
833
|
+
}
|
|
834
|
+
function onDrawerDuplicate(_payload: { appId: string }) {
|
|
835
|
+
message.info('复制应用功能开发中');
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
async function onDrawerPublish(payload: { appId: string }) {
|
|
839
|
+
try {
|
|
840
|
+
await publishWorkflowDraftReq(payload.appId);
|
|
841
|
+
message.success('已发布新版本');
|
|
842
|
+
await refresh();
|
|
843
|
+
} catch (e: any) {
|
|
844
|
+
message.error(e?.message ?? '发布失败');
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* Handle the drawer's `preview` event — pipe the SSE chat stream into the
|
|
850
|
+
* preview pane. Only used for non-workflow modes.
|
|
851
|
+
*/
|
|
852
|
+
async function onDrawerPreview(payload: {
|
|
853
|
+
appId: string;
|
|
854
|
+
query: string;
|
|
855
|
+
onChunk: (t: string) => void;
|
|
856
|
+
onDone: () => void;
|
|
857
|
+
onError: (m: string) => void;
|
|
858
|
+
}) {
|
|
859
|
+
try {
|
|
860
|
+
const res = await chatStreamReq(payload.appId, { query: payload.query });
|
|
861
|
+
if (!res.ok || !res.body) {
|
|
862
|
+
payload.onError(`预览失败:${res.status}`);
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
const reader = res.body.getReader();
|
|
866
|
+
const decoder = new TextDecoder('utf-8');
|
|
867
|
+
let buffer = '';
|
|
868
|
+
while (true) {
|
|
869
|
+
const { value, done } = await reader.read();
|
|
870
|
+
if (done) break;
|
|
871
|
+
buffer += decoder.decode(value, { stream: true });
|
|
872
|
+
let idx = buffer.indexOf('\n\n');
|
|
873
|
+
while (idx >= 0) {
|
|
874
|
+
const raw = buffer.slice(0, idx);
|
|
875
|
+
buffer = buffer.slice(idx + 2);
|
|
876
|
+
let eventName = 'message';
|
|
877
|
+
let dataStr = '';
|
|
878
|
+
for (const line of raw.split('\n')) {
|
|
879
|
+
if (line.startsWith('event:')) eventName = line.slice(6).trim();
|
|
880
|
+
else if (line.startsWith('data:')) dataStr += line.slice(5).trim();
|
|
881
|
+
}
|
|
882
|
+
if (eventName === 'result' && dataStr) {
|
|
883
|
+
try {
|
|
884
|
+
const r = JSON.parse(dataStr);
|
|
885
|
+
if (r.text) payload.onChunk(String(r.text));
|
|
886
|
+
} catch {
|
|
887
|
+
// ignore malformed frame
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
idx = buffer.indexOf('\n\n');
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
payload.onDone();
|
|
894
|
+
} catch (e: any) {
|
|
895
|
+
payload.onError(String(e?.message ?? e));
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function openEdit(a: AgentEntity) {
|
|
900
|
+
editingId.value = a.id;
|
|
901
|
+
form.name = a.name;
|
|
902
|
+
form.description = a.description ?? '';
|
|
903
|
+
form.icon = a.icon ?? '';
|
|
904
|
+
form.iconBackground = a.iconBackground ?? '';
|
|
905
|
+
showCreate.value = true;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
/**
|
|
909
|
+
* Save the trimmed basic-info form. Carries over every other field from the
|
|
910
|
+
* existing entity so the PUT doesn't clobber model / strategy / tools /
|
|
911
|
+
* instructions / memory / published state — those live in the design drawer.
|
|
912
|
+
*/
|
|
913
|
+
async function submitCreate() {
|
|
914
|
+
if (!form.name) {
|
|
915
|
+
message.warning('请填写名称');
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
if (!editingId.value) return;
|
|
919
|
+
const existing = agents.value.find((x) => x.id === editingId.value);
|
|
920
|
+
if (!existing) {
|
|
921
|
+
message.error('未找到应用');
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
submitting.value = true;
|
|
925
|
+
try {
|
|
926
|
+
const body: CreateAgentRequestFull = {
|
|
927
|
+
name: form.name,
|
|
928
|
+
description: form.description || undefined,
|
|
929
|
+
icon: form.icon || undefined,
|
|
930
|
+
iconBackground: form.iconBackground || undefined,
|
|
931
|
+
mode: existing.mode,
|
|
932
|
+
instructions: existing.instructions,
|
|
933
|
+
openingStatement: existing.openingStatement,
|
|
934
|
+
modelName: existing.modelName,
|
|
935
|
+
modelProvider: existing.modelProvider,
|
|
936
|
+
strategy: (existing.strategy as AgentStrategy) ?? 'REACT',
|
|
937
|
+
toolNames: existing.toolNamesJson
|
|
938
|
+
? safeParseArray(existing.toolNamesJson)
|
|
939
|
+
: [],
|
|
940
|
+
datasetIds: existing.datasetIdsJson
|
|
941
|
+
? safeParseArray(existing.datasetIdsJson)
|
|
942
|
+
: [],
|
|
943
|
+
maxIterations: existing.maxIterations,
|
|
944
|
+
memoryEnabled: existing.memoryEnabled,
|
|
945
|
+
memoryWindow: existing.memoryWindow,
|
|
946
|
+
published: existing.published,
|
|
947
|
+
};
|
|
948
|
+
await updateAgentReq(editingId.value, body);
|
|
949
|
+
message.success('已更新');
|
|
950
|
+
showCreate.value = false;
|
|
951
|
+
editingId.value = null;
|
|
952
|
+
await refresh();
|
|
953
|
+
} finally {
|
|
954
|
+
submitting.value = false;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
function safeParseArray(json?: string): string[] {
|
|
959
|
+
if (!json) return [];
|
|
960
|
+
try {
|
|
961
|
+
const v = JSON.parse(json);
|
|
962
|
+
return Array.isArray(v) ? v.map(String) : [];
|
|
963
|
+
} catch {
|
|
964
|
+
return [];
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
function remove(a: AgentEntity) {
|
|
969
|
+
Modal.confirm({
|
|
970
|
+
title: '删除智能体',
|
|
971
|
+
content: `确认删除「${a.name}」?相关对话历史仍会保留在数据库中。`,
|
|
972
|
+
okText: '删除',
|
|
973
|
+
okType: 'danger',
|
|
974
|
+
onOk: async () => {
|
|
975
|
+
await deleteAgentReq(a.id);
|
|
976
|
+
message.success('已删除');
|
|
977
|
+
await refresh();
|
|
978
|
+
},
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// ---- share embed modal ----
|
|
983
|
+
const showShare = ref(false);
|
|
984
|
+
const shareAgent = ref<AgentEntity | null>(null);
|
|
985
|
+
|
|
986
|
+
function openShare(a: AgentEntity) {
|
|
987
|
+
shareAgent.value = a;
|
|
988
|
+
showShare.value = true;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
const shareUrl = computed(() => {
|
|
992
|
+
if (!shareAgent.value) return '';
|
|
993
|
+
return `${window.location.origin}/embed/agent/${shareAgent.value.id}`;
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
const iframeSnippet = computed(() =>
|
|
997
|
+
shareUrl.value
|
|
998
|
+
? `<iframe src="${shareUrl.value}" width="420" height="620" style="border:1px solid #eee;border-radius:8px" allow="microphone"></iframe>`
|
|
999
|
+
: '',
|
|
1000
|
+
);
|
|
1001
|
+
|
|
1002
|
+
async function copyText(txt: string) {
|
|
1003
|
+
try {
|
|
1004
|
+
await navigator.clipboard.writeText(txt);
|
|
1005
|
+
message.success('已复制');
|
|
1006
|
+
} catch {
|
|
1007
|
+
message.error('复制失败,请手动选中');
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
onMounted(() => {
|
|
1012
|
+
// 两个都是 async fn,但不 await —— 显式 .catch 兜住 unhandledrejection,
|
|
1013
|
+
// 避免任何一次网络失败把 <KeepAlive> 缓存里的组件挂坏。
|
|
1014
|
+
refresh().catch(() => {});
|
|
1015
|
+
loadDeps().catch(() => {});
|
|
1016
|
+
});
|
|
1017
|
+
|
|
1018
|
+
// 路由切走时(<KeepAlive> 挂起本组件),强制把抽屉状态关掉。抽屉用
|
|
1019
|
+
// <Teleport to="body"> 挂全屏白色遮罩,若切走时 drawerOpen 还是 true,
|
|
1020
|
+
// 遮罩会漏在 body 上盖住后续路由,表现为「切页面就空白,只能 F5 」。
|
|
1021
|
+
// 配合 AppDesignDrawer 里的 :disabled="!open" 是双保险。
|
|
1022
|
+
onDeactivated(() => {
|
|
1023
|
+
drawerOpen.value = false;
|
|
1024
|
+
});
|
|
1025
|
+
</script>
|
|
1026
|
+
|
|
1027
|
+
<template>
|
|
1028
|
+
<div class="agent-apps-page">
|
|
1029
|
+
<div class="agent-apps-header">
|
|
1030
|
+
<div class="agent-apps-title">应用</div>
|
|
1031
|
+
<div class="agent-apps-subtitle">
|
|
1032
|
+
应用列表:对话 · 智能体 · 工作流 · 文本生成
|
|
1033
|
+
</div>
|
|
1034
|
+
</div>
|
|
1035
|
+
|
|
1036
|
+
<!-- Soft nudge only — creating an app never requires a model configured
|
|
1037
|
+
upfront. Real gating happens when the user tries to *run* the app. -->
|
|
1038
|
+
<Card
|
|
1039
|
+
v-if="llmLoaded && llmModels.length === 0"
|
|
1040
|
+
class="mb-4"
|
|
1041
|
+
:body-style="{ padding: '12px 16px' }"
|
|
1042
|
+
>
|
|
1043
|
+
<div class="flex items-center gap-3">
|
|
1044
|
+
<div class="text-xl">💡</div>
|
|
1045
|
+
<div class="flex-1 text-sm text-gray-600">
|
|
1046
|
+
还没有配置 LLM 模型。你可以先创建应用占位,等到想跑起来的时候再去
|
|
1047
|
+
<a class="text-indigo-600 cursor-pointer" @click="goToModel">
|
|
1048
|
+
配置模型
|
|
1049
|
+
</a>
|
|
1050
|
+
即可。
|
|
1051
|
+
</div>
|
|
1052
|
+
</div>
|
|
1053
|
+
</Card>
|
|
1054
|
+
|
|
1055
|
+
<!-- 顶部搜索 + 过滤 -->
|
|
1056
|
+
<div
|
|
1057
|
+
class="mb-4 flex flex-wrap items-center justify-between gap-2 rounded-lg bg-white p-3 shadow-sm dark:bg-neutral-900"
|
|
1058
|
+
>
|
|
1059
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
1060
|
+
<Input
|
|
1061
|
+
v-model:value="keyword"
|
|
1062
|
+
placeholder="搜索智能体..."
|
|
1063
|
+
allow-clear
|
|
1064
|
+
style="width: 240px"
|
|
1065
|
+
/>
|
|
1066
|
+
<Select
|
|
1067
|
+
v-model:value="filterStrategy"
|
|
1068
|
+
:options="[
|
|
1069
|
+
{ label: '全部策略', value: 'ALL' },
|
|
1070
|
+
{ label: 'ReAct', value: 'REACT' },
|
|
1071
|
+
{ label: 'Function Calling', value: 'FUNCTION_CALLING' },
|
|
1072
|
+
{ label: 'Plan & Execute', value: 'PLAN_EXECUTE' },
|
|
1073
|
+
]"
|
|
1074
|
+
style="width: 180px"
|
|
1075
|
+
/>
|
|
1076
|
+
<div class="dify-pub-tabs">
|
|
1077
|
+
<button
|
|
1078
|
+
class="dify-pub-tab"
|
|
1079
|
+
:class="{ 'dify-pub-tab--on': filterPublished === 'all' }"
|
|
1080
|
+
@click="filterPublished = 'all'"
|
|
1081
|
+
>
|
|
1082
|
+
全部 <span class="dify-pub-count">{{ agents.length }}</span>
|
|
1083
|
+
</button>
|
|
1084
|
+
<button
|
|
1085
|
+
class="dify-pub-tab"
|
|
1086
|
+
:class="{ 'dify-pub-tab--on': filterPublished === 'published' }"
|
|
1087
|
+
@click="filterPublished = 'published'"
|
|
1088
|
+
>
|
|
1089
|
+
已发布
|
|
1090
|
+
<span class="dify-pub-count">{{ publishedCount }}</span>
|
|
1091
|
+
</button>
|
|
1092
|
+
<button
|
|
1093
|
+
class="dify-pub-tab"
|
|
1094
|
+
:class="{ 'dify-pub-tab--on': filterPublished === 'draft' }"
|
|
1095
|
+
@click="filterPublished = 'draft'"
|
|
1096
|
+
>
|
|
1097
|
+
草稿 <span class="dify-pub-count">{{ draftCount }}</span>
|
|
1098
|
+
</button>
|
|
1099
|
+
</div>
|
|
1100
|
+
</div>
|
|
1101
|
+
<div class="text-xs text-gray-500">
|
|
1102
|
+
共 {{ filtered.length }} / {{ agents.length }} 个
|
|
1103
|
+
</div>
|
|
1104
|
+
</div>
|
|
1105
|
+
|
|
1106
|
+
<div v-if="loading && agents.length === 0" class="dify-grid">
|
|
1107
|
+
<div v-for="n in 6" :key="n" class="dify-card">
|
|
1108
|
+
<Skeleton :active="true" :paragraph="{ rows: 3 }" />
|
|
1109
|
+
</div>
|
|
1110
|
+
</div>
|
|
1111
|
+
|
|
1112
|
+
<Spin :spinning="loading && agents.length > 0">
|
|
1113
|
+
<div v-if="!loading || agents.length > 0" class="dify-grid">
|
|
1114
|
+
<!-- "新建" 占位卡 —— 点开走 Dify 风格的 CreateAppModal 选类型。
|
|
1115
|
+
Never disabled: apps can be created without a model configured. -->
|
|
1116
|
+
<div class="dify-card dify-card-new" @click="openTypePicker">
|
|
1117
|
+
<div class="dify-new-inner">
|
|
1118
|
+
<div class="dify-new-plus">+</div>
|
|
1119
|
+
<div class="dify-new-text">新建应用</div>
|
|
1120
|
+
<div class="dify-new-sub">
|
|
1121
|
+
对话 / Agent / 工作流 / Chatflow / 文本生成
|
|
1122
|
+
</div>
|
|
1123
|
+
</div>
|
|
1124
|
+
</div>
|
|
1125
|
+
|
|
1126
|
+
<!-- 智能体卡 -->
|
|
1127
|
+
<div
|
|
1128
|
+
v-for="a in filtered"
|
|
1129
|
+
:key="a.id"
|
|
1130
|
+
class="dify-card"
|
|
1131
|
+
@click="openDesigner(a)"
|
|
1132
|
+
>
|
|
1133
|
+
<div class="dify-header">
|
|
1134
|
+
<div class="dify-icon" :style="{ background: bgOf(a) }">
|
|
1135
|
+
{{ iconOf(a) }}
|
|
1136
|
+
</div>
|
|
1137
|
+
<div class="dify-title-wrap">
|
|
1138
|
+
<div class="dify-title" :title="a.name">
|
|
1139
|
+
{{ a.name }}
|
|
1140
|
+
<Tag
|
|
1141
|
+
v-if="a.published === false"
|
|
1142
|
+
color="orange"
|
|
1143
|
+
size="small"
|
|
1144
|
+
style="margin-left: 4px; font-size: 10px"
|
|
1145
|
+
>
|
|
1146
|
+
草稿
|
|
1147
|
+
</Tag>
|
|
1148
|
+
</div>
|
|
1149
|
+
<div class="dify-meta">
|
|
1150
|
+
<Tag :color="modeColor(a.mode)" style="margin-right: 4px">
|
|
1151
|
+
{{ modeLabel(a.mode) }}
|
|
1152
|
+
</Tag>
|
|
1153
|
+
<Tag :color="strategyColor(a.strategy)" style="margin-right: 4px">
|
|
1154
|
+
{{ strategyLabel(a.strategy) }}
|
|
1155
|
+
</Tag>
|
|
1156
|
+
<span>· {{ fromNow(a.updatedAt) || '刚刚' }}</span>
|
|
1157
|
+
</div>
|
|
1158
|
+
</div>
|
|
1159
|
+
</div>
|
|
1160
|
+
|
|
1161
|
+
<div
|
|
1162
|
+
class="dify-desc"
|
|
1163
|
+
:title="a.description || a.instructions || ''"
|
|
1164
|
+
>
|
|
1165
|
+
{{ a.description || a.instructions || '暂无描述' }}
|
|
1166
|
+
</div>
|
|
1167
|
+
|
|
1168
|
+
<div class="dify-spacer" />
|
|
1169
|
+
|
|
1170
|
+
<div class="dify-footer">
|
|
1171
|
+
<span class="dify-footer-item" :title="modelLabel(a)">
|
|
1172
|
+
🧠 {{ modelLabel(a) }}
|
|
1173
|
+
</span>
|
|
1174
|
+
<span
|
|
1175
|
+
v-if="a.maxIterations"
|
|
1176
|
+
class="dify-footer-item"
|
|
1177
|
+
:title="`最多 ${a.maxIterations} 轮`"
|
|
1178
|
+
>
|
|
1179
|
+
🔁 {{ a.maxIterations }}
|
|
1180
|
+
</span>
|
|
1181
|
+
</div>
|
|
1182
|
+
|
|
1183
|
+
<div class="dify-more" @click.stop>
|
|
1184
|
+
<Dropdown :trigger="['click']" placement="bottomRight">
|
|
1185
|
+
<Button type="text" size="small" class="dify-more-btn">
|
|
1186
|
+
⋯
|
|
1187
|
+
</Button>
|
|
1188
|
+
<template #overlay>
|
|
1189
|
+
<Menu>
|
|
1190
|
+
<MenuItem key="design" @click="openDesigner(a)">
|
|
1191
|
+
设计
|
|
1192
|
+
</MenuItem>
|
|
1193
|
+
<MenuItem key="chat" @click="openChat(a)">
|
|
1194
|
+
独立对话
|
|
1195
|
+
</MenuItem>
|
|
1196
|
+
<MenuItem key="edit" @click="openEdit(a)">
|
|
1197
|
+
编辑基本信息
|
|
1198
|
+
</MenuItem>
|
|
1199
|
+
<MenuItem key="share" @click="openShare(a)">
|
|
1200
|
+
分享嵌入
|
|
1201
|
+
</MenuItem>
|
|
1202
|
+
<MenuItem key="delete" danger @click="remove(a)">
|
|
1203
|
+
删除
|
|
1204
|
+
</MenuItem>
|
|
1205
|
+
</Menu>
|
|
1206
|
+
</template>
|
|
1207
|
+
</Dropdown>
|
|
1208
|
+
</div>
|
|
1209
|
+
</div>
|
|
1210
|
+
</div>
|
|
1211
|
+
|
|
1212
|
+
<Empty
|
|
1213
|
+
v-if="!loading && filtered.length === 0"
|
|
1214
|
+
class="mt-8"
|
|
1215
|
+
:description="
|
|
1216
|
+
keyword
|
|
1217
|
+
? '没有找到匹配的智能体'
|
|
1218
|
+
: '还没有智能体,点上面的“新建智能体”开始吧'
|
|
1219
|
+
"
|
|
1220
|
+
/>
|
|
1221
|
+
</Spin>
|
|
1222
|
+
|
|
1223
|
+
<Modal
|
|
1224
|
+
v-model:open="showCreate"
|
|
1225
|
+
title="编辑基本信息"
|
|
1226
|
+
width="520px"
|
|
1227
|
+
:confirm-loading="submitting"
|
|
1228
|
+
ok-text="保存"
|
|
1229
|
+
@ok="submitCreate"
|
|
1230
|
+
>
|
|
1231
|
+
<Form :model="form" layout="vertical">
|
|
1232
|
+
<FormItem label="图标">
|
|
1233
|
+
<div class="flex flex-wrap gap-1">
|
|
1234
|
+
<button
|
|
1235
|
+
v-for="ic in ICONS"
|
|
1236
|
+
:key="ic"
|
|
1237
|
+
type="button"
|
|
1238
|
+
class="dify-emoji-btn"
|
|
1239
|
+
:class="{ 'dify-emoji-btn--on': form.icon === ic }"
|
|
1240
|
+
:style="{ background: form.iconBackground || '#F3F4F6' }"
|
|
1241
|
+
@click="form.icon = ic"
|
|
1242
|
+
>
|
|
1243
|
+
{{ ic }}
|
|
1244
|
+
</button>
|
|
1245
|
+
</div>
|
|
1246
|
+
</FormItem>
|
|
1247
|
+
<FormItem label="背景颜色">
|
|
1248
|
+
<div class="flex flex-wrap gap-1">
|
|
1249
|
+
<button
|
|
1250
|
+
v-for="bg in BGS"
|
|
1251
|
+
:key="bg"
|
|
1252
|
+
type="button"
|
|
1253
|
+
class="dify-bg-btn"
|
|
1254
|
+
:class="{ 'dify-bg-btn--on': form.iconBackground === bg }"
|
|
1255
|
+
:style="{ background: bg }"
|
|
1256
|
+
@click="form.iconBackground = bg"
|
|
1257
|
+
/>
|
|
1258
|
+
</div>
|
|
1259
|
+
</FormItem>
|
|
1260
|
+
<FormItem label="名称" required>
|
|
1261
|
+
<Input v-model:value="form.name" placeholder="例如 report-writer" />
|
|
1262
|
+
</FormItem>
|
|
1263
|
+
<FormItem label="描述">
|
|
1264
|
+
<Textarea
|
|
1265
|
+
v-model:value="form.description"
|
|
1266
|
+
:rows="3"
|
|
1267
|
+
placeholder="一句话说明这个智能体在做什么 —— 显示在卡片上"
|
|
1268
|
+
/>
|
|
1269
|
+
</FormItem>
|
|
1270
|
+
</Form>
|
|
1271
|
+
</Modal>
|
|
1272
|
+
|
|
1273
|
+
<Modal
|
|
1274
|
+
v-model:open="showShare"
|
|
1275
|
+
:title="shareAgent ? `分享 · ${shareAgent.name}` : '分享'"
|
|
1276
|
+
width="640px"
|
|
1277
|
+
:footer="null"
|
|
1278
|
+
>
|
|
1279
|
+
<div class="mb-2 text-xs text-gray-500">
|
|
1280
|
+
无登录嵌入。iframe 到你的站点即可用,会话会自动保存到浏览器 localStorage。
|
|
1281
|
+
</div>
|
|
1282
|
+
<div class="mb-1 text-xs font-medium">直链</div>
|
|
1283
|
+
<div class="mb-3 flex gap-2">
|
|
1284
|
+
<Input :value="shareUrl" readonly class="flex-1" />
|
|
1285
|
+
<Button @click="copyText(shareUrl)">复制</Button>
|
|
1286
|
+
</div>
|
|
1287
|
+
<div class="mb-1 text-xs font-medium">iframe 嵌入代码</div>
|
|
1288
|
+
<div class="flex gap-2">
|
|
1289
|
+
<Textarea
|
|
1290
|
+
:value="iframeSnippet"
|
|
1291
|
+
:rows="4"
|
|
1292
|
+
readonly
|
|
1293
|
+
class="flex-1"
|
|
1294
|
+
style="font-family: monospace; font-size: 12px"
|
|
1295
|
+
/>
|
|
1296
|
+
<Button @click="copyText(iframeSnippet)">复制</Button>
|
|
1297
|
+
</div>
|
|
1298
|
+
<div class="mt-3 text-xs text-gray-400">
|
|
1299
|
+
提示:URL 支持 <code>?title=xxx</code> 覆盖标题、
|
|
1300
|
+
<code>?conversation=xxx</code> 恢复到指定会话。
|
|
1301
|
+
</div>
|
|
1302
|
+
</Modal>
|
|
1303
|
+
|
|
1304
|
+
<!-- Dify-style app-type picker. Single-page: type tiles + form on the
|
|
1305
|
+
left, big preview on the right. Chatflow is a first-class primary
|
|
1306
|
+
tile alongside workflow — same DAG canvas, but the runtime keeps
|
|
1307
|
+
per-conversation memory + streams via ANSWER nodes. -->
|
|
1308
|
+
<CreateAppModal
|
|
1309
|
+
v-model:open="showTypePicker"
|
|
1310
|
+
:allow="['chatbot', 'agent', 'workflow', 'chatflow', 'text-generator']"
|
|
1311
|
+
@create="onCreateApp"
|
|
1312
|
+
/>
|
|
1313
|
+
|
|
1314
|
+
<!-- Click-a-card design drawer. Drawer's built-in DrawerFlowDesigner /
|
|
1315
|
+
LogAnnotationPanel / MonitorPanel handle everything internally now —
|
|
1316
|
+
we just wire them via :api to the local fetch bag. -->
|
|
1317
|
+
<AppDesignDrawer
|
|
1318
|
+
v-model:open="drawerOpen"
|
|
1319
|
+
:app="drawerApp"
|
|
1320
|
+
:initial-graph-json="drawerGraphJson"
|
|
1321
|
+
:api="studioApi"
|
|
1322
|
+
:api-base="apiBase"
|
|
1323
|
+
:chat-config="drawerChatConfig"
|
|
1324
|
+
:models="
|
|
1325
|
+
llmModels.map((m) => ({
|
|
1326
|
+
id: m.id,
|
|
1327
|
+
label: `${m.providerName} · ${m.modelName}`,
|
|
1328
|
+
provider: m.providerName,
|
|
1329
|
+
}))
|
|
1330
|
+
"
|
|
1331
|
+
:tools="tools.map((t) => ({ name: t.name, description: t.description }))"
|
|
1332
|
+
:knowledge-bases="datasets.map((d) => ({ id: d.id, name: d.name }))"
|
|
1333
|
+
@save="onDrawerSave"
|
|
1334
|
+
@preview="onDrawerPreview"
|
|
1335
|
+
@publish="onDrawerPublish"
|
|
1336
|
+
@edit-info="onDrawerEditInfo"
|
|
1337
|
+
@export-dsl="onDrawerExportDsl"
|
|
1338
|
+
@duplicate="onDrawerDuplicate"
|
|
1339
|
+
/>
|
|
1340
|
+
</div>
|
|
1341
|
+
</template>
|
|
1342
|
+
|
|
1343
|
+
<style scoped>
|
|
1344
|
+
.agent-apps-page {
|
|
1345
|
+
padding: 16px;
|
|
1346
|
+
}
|
|
1347
|
+
.agent-apps-header {
|
|
1348
|
+
margin-bottom: 16px;
|
|
1349
|
+
}
|
|
1350
|
+
.agent-apps-title {
|
|
1351
|
+
font-size: 18px;
|
|
1352
|
+
font-weight: 600;
|
|
1353
|
+
color: #111827;
|
|
1354
|
+
}
|
|
1355
|
+
.agent-apps-subtitle {
|
|
1356
|
+
margin-top: 4px;
|
|
1357
|
+
font-size: 12px;
|
|
1358
|
+
color: #6b7280;
|
|
1359
|
+
}
|
|
1360
|
+
:global(.dark) .agent-apps-title {
|
|
1361
|
+
color: #f3f4f6;
|
|
1362
|
+
}
|
|
1363
|
+
:global(.dark) .agent-apps-subtitle {
|
|
1364
|
+
color: #9ca3af;
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
.dify-grid {
|
|
1368
|
+
display: grid;
|
|
1369
|
+
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
1370
|
+
gap: 16px;
|
|
1371
|
+
}
|
|
1372
|
+
.dify-card {
|
|
1373
|
+
position: relative;
|
|
1374
|
+
height: 200px;
|
|
1375
|
+
padding: 14px 16px 12px;
|
|
1376
|
+
background: #fff;
|
|
1377
|
+
border: 1px solid #e5e7eb;
|
|
1378
|
+
border-radius: 12px;
|
|
1379
|
+
cursor: pointer;
|
|
1380
|
+
transition:
|
|
1381
|
+
box-shadow 0.15s ease,
|
|
1382
|
+
transform 0.15s ease,
|
|
1383
|
+
background 0.15s ease;
|
|
1384
|
+
display: flex;
|
|
1385
|
+
flex-direction: column;
|
|
1386
|
+
overflow: hidden;
|
|
1387
|
+
}
|
|
1388
|
+
:global(.dark) .dify-card {
|
|
1389
|
+
background: #1f1f1f;
|
|
1390
|
+
border-color: #2d2d2d;
|
|
1391
|
+
}
|
|
1392
|
+
.dify-card:hover {
|
|
1393
|
+
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.08);
|
|
1394
|
+
transform: translateY(-1px);
|
|
1395
|
+
}
|
|
1396
|
+
.dify-card-new {
|
|
1397
|
+
border: 1.5px dashed #c7d2fe;
|
|
1398
|
+
background: linear-gradient(135deg, #f5f9ff 0%, #f0f5ff 100%);
|
|
1399
|
+
}
|
|
1400
|
+
.dify-card-new:hover {
|
|
1401
|
+
border-color: #6366f1;
|
|
1402
|
+
background: linear-gradient(135deg, #eef2ff 0%, #e0e7ff 100%);
|
|
1403
|
+
}
|
|
1404
|
+
.dify-card-disabled {
|
|
1405
|
+
opacity: 0.6;
|
|
1406
|
+
cursor: not-allowed;
|
|
1407
|
+
}
|
|
1408
|
+
.dify-new-inner {
|
|
1409
|
+
margin: auto 0;
|
|
1410
|
+
text-align: center;
|
|
1411
|
+
color: #6366f1;
|
|
1412
|
+
}
|
|
1413
|
+
.dify-new-plus {
|
|
1414
|
+
font-size: 42px;
|
|
1415
|
+
line-height: 1;
|
|
1416
|
+
font-weight: 200;
|
|
1417
|
+
}
|
|
1418
|
+
.dify-new-text {
|
|
1419
|
+
margin-top: 6px;
|
|
1420
|
+
font-size: 15px;
|
|
1421
|
+
font-weight: 500;
|
|
1422
|
+
}
|
|
1423
|
+
.dify-new-sub {
|
|
1424
|
+
margin-top: 4px;
|
|
1425
|
+
font-size: 12px;
|
|
1426
|
+
color: #94a3b8;
|
|
1427
|
+
}
|
|
1428
|
+
.dify-header {
|
|
1429
|
+
display: flex;
|
|
1430
|
+
gap: 12px;
|
|
1431
|
+
align-items: flex-start;
|
|
1432
|
+
}
|
|
1433
|
+
.dify-icon {
|
|
1434
|
+
width: 40px;
|
|
1435
|
+
height: 40px;
|
|
1436
|
+
flex-shrink: 0;
|
|
1437
|
+
display: flex;
|
|
1438
|
+
align-items: center;
|
|
1439
|
+
justify-content: center;
|
|
1440
|
+
border-radius: 8px;
|
|
1441
|
+
font-size: 22px;
|
|
1442
|
+
}
|
|
1443
|
+
.dify-title-wrap {
|
|
1444
|
+
min-width: 0;
|
|
1445
|
+
flex: 1;
|
|
1446
|
+
}
|
|
1447
|
+
.dify-title {
|
|
1448
|
+
font-size: 15px;
|
|
1449
|
+
font-weight: 600;
|
|
1450
|
+
color: #111827;
|
|
1451
|
+
white-space: nowrap;
|
|
1452
|
+
overflow: hidden;
|
|
1453
|
+
text-overflow: ellipsis;
|
|
1454
|
+
}
|
|
1455
|
+
:global(.dark) .dify-title {
|
|
1456
|
+
color: #f3f4f6;
|
|
1457
|
+
}
|
|
1458
|
+
.dify-meta {
|
|
1459
|
+
margin-top: 4px;
|
|
1460
|
+
font-size: 12px;
|
|
1461
|
+
color: #6b7280;
|
|
1462
|
+
display: flex;
|
|
1463
|
+
align-items: center;
|
|
1464
|
+
gap: 4px;
|
|
1465
|
+
}
|
|
1466
|
+
.dify-desc {
|
|
1467
|
+
margin-top: 10px;
|
|
1468
|
+
font-size: 13px;
|
|
1469
|
+
color: #6b7280;
|
|
1470
|
+
line-height: 1.5;
|
|
1471
|
+
display: -webkit-box;
|
|
1472
|
+
-webkit-line-clamp: 2;
|
|
1473
|
+
-webkit-box-orient: vertical;
|
|
1474
|
+
overflow: hidden;
|
|
1475
|
+
}
|
|
1476
|
+
:global(.dark) .dify-desc {
|
|
1477
|
+
color: #9ca3af;
|
|
1478
|
+
}
|
|
1479
|
+
.dify-spacer {
|
|
1480
|
+
flex: 1;
|
|
1481
|
+
}
|
|
1482
|
+
.dify-footer {
|
|
1483
|
+
display: flex;
|
|
1484
|
+
align-items: center;
|
|
1485
|
+
gap: 12px;
|
|
1486
|
+
font-size: 12px;
|
|
1487
|
+
color: #6b7280;
|
|
1488
|
+
border-top: 1px solid #f3f4f6;
|
|
1489
|
+
padding-top: 8px;
|
|
1490
|
+
overflow: hidden;
|
|
1491
|
+
white-space: nowrap;
|
|
1492
|
+
}
|
|
1493
|
+
:global(.dark) .dify-footer {
|
|
1494
|
+
border-top-color: #2d2d2d;
|
|
1495
|
+
}
|
|
1496
|
+
.dify-footer-item {
|
|
1497
|
+
display: inline-flex;
|
|
1498
|
+
align-items: center;
|
|
1499
|
+
gap: 4px;
|
|
1500
|
+
overflow: hidden;
|
|
1501
|
+
text-overflow: ellipsis;
|
|
1502
|
+
max-width: 200px;
|
|
1503
|
+
}
|
|
1504
|
+
.dify-more {
|
|
1505
|
+
position: absolute;
|
|
1506
|
+
top: 8px;
|
|
1507
|
+
right: 8px;
|
|
1508
|
+
opacity: 0;
|
|
1509
|
+
transition: opacity 0.15s ease;
|
|
1510
|
+
}
|
|
1511
|
+
.dify-card:hover .dify-more {
|
|
1512
|
+
opacity: 1;
|
|
1513
|
+
}
|
|
1514
|
+
.dify-more-btn {
|
|
1515
|
+
font-size: 18px;
|
|
1516
|
+
line-height: 1;
|
|
1517
|
+
padding: 0 6px;
|
|
1518
|
+
height: 26px;
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
/* Emoji picker for the agent icon */
|
|
1522
|
+
.dify-emoji-btn {
|
|
1523
|
+
width: 32px;
|
|
1524
|
+
height: 32px;
|
|
1525
|
+
font-size: 18px;
|
|
1526
|
+
border: 1px solid transparent;
|
|
1527
|
+
border-radius: 8px;
|
|
1528
|
+
cursor: pointer;
|
|
1529
|
+
display: flex;
|
|
1530
|
+
align-items: center;
|
|
1531
|
+
justify-content: center;
|
|
1532
|
+
transition:
|
|
1533
|
+
border-color 0.15s,
|
|
1534
|
+
transform 0.1s;
|
|
1535
|
+
}
|
|
1536
|
+
.dify-emoji-btn:hover {
|
|
1537
|
+
border-color: #a5b4fc;
|
|
1538
|
+
transform: scale(1.05);
|
|
1539
|
+
}
|
|
1540
|
+
.dify-emoji-btn--on {
|
|
1541
|
+
border-color: #6366f1;
|
|
1542
|
+
box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.15);
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
/* Published filter tabs */
|
|
1546
|
+
.dify-pub-tabs {
|
|
1547
|
+
display: inline-flex;
|
|
1548
|
+
gap: 2px;
|
|
1549
|
+
padding: 2px;
|
|
1550
|
+
background: #f3f4f6;
|
|
1551
|
+
border-radius: 6px;
|
|
1552
|
+
}
|
|
1553
|
+
.dify-pub-tab {
|
|
1554
|
+
background: transparent;
|
|
1555
|
+
border: none;
|
|
1556
|
+
border-radius: 4px;
|
|
1557
|
+
padding: 4px 10px;
|
|
1558
|
+
font-size: 12px;
|
|
1559
|
+
color: #6b7280;
|
|
1560
|
+
cursor: pointer;
|
|
1561
|
+
display: flex;
|
|
1562
|
+
align-items: center;
|
|
1563
|
+
gap: 4px;
|
|
1564
|
+
}
|
|
1565
|
+
.dify-pub-tab:hover {
|
|
1566
|
+
color: #111827;
|
|
1567
|
+
}
|
|
1568
|
+
.dify-pub-tab--on {
|
|
1569
|
+
background: #fff;
|
|
1570
|
+
color: #111827;
|
|
1571
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
|
1572
|
+
}
|
|
1573
|
+
.dify-pub-count {
|
|
1574
|
+
padding: 0 4px;
|
|
1575
|
+
font-size: 10px;
|
|
1576
|
+
border-radius: 999px;
|
|
1577
|
+
background: rgba(0, 0, 0, 0.05);
|
|
1578
|
+
min-width: 16px;
|
|
1579
|
+
text-align: center;
|
|
1580
|
+
}
|
|
1581
|
+
.dify-pub-tab--on .dify-pub-count {
|
|
1582
|
+
background: #eef2ff;
|
|
1583
|
+
color: #4338ca;
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
/* Background color picker */
|
|
1587
|
+
.dify-bg-btn {
|
|
1588
|
+
width: 28px;
|
|
1589
|
+
height: 28px;
|
|
1590
|
+
border: 2px solid transparent;
|
|
1591
|
+
border-radius: 6px;
|
|
1592
|
+
cursor: pointer;
|
|
1593
|
+
transition: border-color 0.15s;
|
|
1594
|
+
}
|
|
1595
|
+
.dify-bg-btn:hover {
|
|
1596
|
+
border-color: #9ca3af;
|
|
1597
|
+
}
|
|
1598
|
+
.dify-bg-btn--on {
|
|
1599
|
+
border-color: #6366f1;
|
|
1600
|
+
}
|
|
1601
|
+
</style>
|