Trae Assistant commited on
Commit
2d0e2ae
·
0 Parent(s):

Initial commit: LogicStream Agent

Browse files
Files changed (7) hide show
  1. .gitattributes +2 -0
  2. .gitignore +5 -0
  3. Dockerfile +28 -0
  4. README.md +57 -0
  5. app.py +214 -0
  6. requirements.txt +5 -0
  7. templates/index.html +400 -0
.gitattributes ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ *.db filter=lfs diff=lfs merge=lfs -text
2
+ *.sqlite filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.pyc
3
+ .env
4
+ instance/
5
+ .DS_Store
Dockerfile ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.9-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Install system dependencies if needed (e.g. for sqlite)
6
+ # RUN apt-get update && apt-get install -y ...
7
+
8
+ COPY requirements.txt .
9
+ RUN pip install --no-cache-dir -r requirements.txt
10
+
11
+ COPY . .
12
+
13
+ # Ensure instance directory exists and is writable
14
+ RUN mkdir -p instance && chmod 777 instance
15
+
16
+ # Create a non-root user (recommended for Hugging Face Spaces)
17
+ RUN useradd -m -u 1000 user
18
+ # Switch to the new user
19
+ USER user
20
+ # Set home to the user's home directory
21
+ ENV HOME=/home/user \
22
+ PATH=/home/user/.local/bin:$PATH
23
+
24
+ # Expose the port
25
+ EXPOSE 7860
26
+
27
+ # Run the application
28
+ CMD ["python", "app.py"]
README.md ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Logic Stream Agent
3
+ emoji: 🧠
4
+ colorFrom: blue
5
+ colorTo: indigo
6
+ sdk: docker
7
+ app_port: 7860
8
+ short_description: 智能业务决策编排引擎 (Intelligent Business Decision Orchestration Engine)
9
+ ---
10
+
11
+ # LogicStream - 智能业务决策编排引擎
12
+
13
+ LogicStream 是一个可视化的业务逻辑编排 Agent,旨在帮助企业和个人构建、测试和部署基于 LLM 的复杂业务决策流。
14
+
15
+ ## 核心功能
16
+
17
+ 1. **可视化工作流设计**:通过简单的步骤添加和配置,构建包含输入、AI 推理、逻辑判断的业务流。
18
+ 2. **SiliconFlow 强力驱动**:集成 SiliconFlow API (Qwen/Qwen2.5-7B-Instruct),提供强大的逻辑推理能力。
19
+ 3. **资产积累**:将成功的业务逻辑保存为“资产”,可重复使用和迭代。
20
+ 4. **实时运行监控**:详细的运行日志和结果预览,确保逻辑的可靠性。
21
+ 5. **商业化潜力**:适用于市场分析、自动财报解读、法律文书生成、客户服务自动化等场景。
22
+
23
+ ## 快速开始
24
+
25
+ ### 本地运行
26
+
27
+ 1. 克隆仓库
28
+ 2. 安装依赖:
29
+ ```bash
30
+ pip install -r requirements.txt
31
+ ```
32
+ 3. 运行应用:
33
+ ```bash
34
+ python app.py
35
+ ```
36
+ 4. 打开浏览器访问:`http://localhost:7860`
37
+
38
+ ### Docker 运行
39
+
40
+ ```bash
41
+ docker build -t logic-stream .
42
+ docker run -p 7860:7860 logic-stream
43
+ ```
44
+
45
+ ## 技术栈
46
+
47
+ - **Backend**: Python Flask, SQLite
48
+ - **Frontend**: Vue.js 3, Tailwind CSS, ECharts
49
+ - **AI**: SiliconFlow API
50
+
51
+ ## 开发者说明
52
+
53
+ 本项目专为商业化 SaaS 场景设计,强调“生产力”和“资产沉淀”。代码结构清晰,易于扩展。
54
+
55
+ ## 许可证
56
+
57
+ MIT
app.py ADDED
@@ -0,0 +1,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import sqlite3
4
+ import requests
5
+ import datetime
6
+ import uuid
7
+ from flask import Flask, render_template, request, jsonify, send_from_directory
8
+ from flask_cors import CORS
9
+
10
+ app = Flask(__name__, static_folder='static', template_folder='templates')
11
+ CORS(app)
12
+
13
+ # Configuration
14
+ SILICONFLOW_API_KEY = os.environ.get("SILICONFLOW_API_KEY", "sk-vimuseiptfbomzegyuvmebjzooncsqbyjtlddrfodzcdskgi")
15
+ SILICONFLOW_API_URL = "https://api.siliconflow.cn/v1/chat/completions"
16
+ DB_PATH = os.path.join(app.instance_path, "logic_stream.db")
17
+
18
+ # Ensure instance folder exists
19
+ os.makedirs(app.instance_path, exist_ok=True)
20
+
21
+ # Database Initialization
22
+ def init_db():
23
+ conn = sqlite3.connect(DB_PATH)
24
+ c = conn.cursor()
25
+ c.execute('''CREATE TABLE IF NOT EXISTS workflows
26
+ (id TEXT PRIMARY KEY, name TEXT, description TEXT, steps TEXT, created_at TEXT, updated_at TEXT)''')
27
+ c.execute('''CREATE TABLE IF NOT EXISTS runs
28
+ (id TEXT PRIMARY KEY, workflow_id TEXT, status TEXT, result TEXT, logs TEXT, created_at TEXT)''')
29
+
30
+ # Check if empty, add default data
31
+ c.execute("SELECT count(*) FROM workflows")
32
+ if c.fetchone()[0] == 0:
33
+ default_workflow = {
34
+ "id": str(uuid.uuid4()),
35
+ "name": "市场趋势分析 (Market Trend Analysis)",
36
+ "description": "自动分析市场新闻并生成简报 (Auto-analyze market news and generate brief)",
37
+ "steps": json.dumps([
38
+ {"id": "step_1", "type": "input", "name": "输入主题", "content": "AI Agent 2025年发展趋势"},
39
+ {"id": "step_2", "type": "llm", "name": "深度分析", "prompt": "请分析以下主题的市场趋势和商业机会:{{step_1.output}}。输出 JSON 格式。"},
40
+ {"id": "step_3", "type": "llm", "name": "生成简报", "prompt": "根据以下分析生成一份简洁的投资简报:{{step_2.output}}"}
41
+ ]),
42
+ "created_at": datetime.datetime.now().isoformat(),
43
+ "updated_at": datetime.datetime.now().isoformat()
44
+ }
45
+ c.execute("INSERT INTO workflows VALUES (?, ?, ?, ?, ?, ?)",
46
+ (default_workflow["id"], default_workflow["name"], default_workflow["description"],
47
+ default_workflow["steps"], default_workflow["created_at"], default_workflow["updated_at"]))
48
+ conn.commit()
49
+ print("Default data initialized.")
50
+
51
+ conn.commit()
52
+ conn.close()
53
+
54
+ init_db()
55
+
56
+ def get_db_connection():
57
+ conn = sqlite3.connect(DB_PATH)
58
+ conn.row_factory = sqlite3.Row
59
+ return conn
60
+
61
+ # Routes
62
+ @app.route('/')
63
+ def index():
64
+ return render_template('index.html')
65
+
66
+ @app.route('/api/workflows', methods=['GET'])
67
+ def get_workflows():
68
+ conn = get_db_connection()
69
+ workflows = conn.execute('SELECT * FROM workflows ORDER BY updated_at DESC').fetchall()
70
+ conn.close()
71
+ return jsonify([dict(w) for w in workflows])
72
+
73
+ @app.route('/api/workflows', methods=['POST'])
74
+ def create_workflow():
75
+ data = request.json
76
+ workflow_id = str(uuid.uuid4())
77
+ now = datetime.datetime.now().isoformat()
78
+ conn = get_db_connection()
79
+ conn.execute('INSERT INTO workflows (id, name, description, steps, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)',
80
+ (workflow_id, data['name'], data.get('description', ''), json.dumps(data['steps']), now, now))
81
+ conn.commit()
82
+ conn.close()
83
+ return jsonify({"id": workflow_id, "status": "created"})
84
+
85
+ @app.route('/api/workflows/<id>', methods=['PUT'])
86
+ def update_workflow(id):
87
+ data = request.json
88
+ now = datetime.datetime.now().isoformat()
89
+ conn = get_db_connection()
90
+ conn.execute('UPDATE workflows SET name = ?, description = ?, steps = ?, updated_at = ? WHERE id = ?',
91
+ (data['name'], data.get('description', ''), json.dumps(data['steps']), now, id))
92
+ conn.commit()
93
+ conn.close()
94
+ return jsonify({"status": "updated"})
95
+
96
+ @app.route('/api/workflows/<id>', methods=['DELETE'])
97
+ def delete_workflow(id):
98
+ conn = get_db_connection()
99
+ conn.execute('DELETE FROM workflows WHERE id = ?', (id,))
100
+ conn.commit()
101
+ conn.close()
102
+ return jsonify({"status": "deleted"})
103
+
104
+ @app.route('/api/run_workflow', methods=['POST'])
105
+ def run_workflow():
106
+ data = request.json
107
+ workflow_id = data.get('workflow_id')
108
+ steps = data.get('steps', [])
109
+
110
+ # If workflow_id is provided, fetch steps from DB (optional, but here we assume frontend sends current steps)
111
+
112
+ run_id = str(uuid.uuid4())
113
+ logs = []
114
+ context = {}
115
+
116
+ try:
117
+ for step in steps:
118
+ step_id = step['id']
119
+ step_type = step['type']
120
+ step_name = step['name']
121
+
122
+ logs.append({"timestamp": datetime.datetime.now().isoformat(), "level": "INFO", "message": f"Starting step: {step_name} ({step_type})"})
123
+
124
+ output = ""
125
+
126
+ if step_type == 'input':
127
+ output = step.get('content', '')
128
+ logs.append({"timestamp": datetime.datetime.now().isoformat(), "level": "SUCCESS", "message": f"Input received: {output[:50]}..."})
129
+
130
+ elif step_type == 'llm':
131
+ prompt_template = step.get('prompt', '')
132
+ # Simple variable substitution {{step_id.output}}
133
+ prompt = prompt_template
134
+ for prev_step_id, prev_output in context.items():
135
+ prompt = prompt.replace(f"{{{{{prev_step_id}.output}}}}", str(prev_output))
136
+
137
+ logs.append({"timestamp": datetime.datetime.now().isoformat(), "level": "INFO", "message": f"Calling LLM with prompt: {prompt[:50]}..."})
138
+
139
+ # Call SiliconFlow API
140
+ try:
141
+ headers = {
142
+ "Authorization": f"Bearer {SILICONFLOW_API_KEY}",
143
+ "Content-Type": "application/json"
144
+ }
145
+ payload = {
146
+ "model": "Qwen/Qwen2.5-7B-Instruct", # Reliable model
147
+ "messages": [
148
+ {"role": "system", "content": "You are a helpful business logic assistant. Output clean, structured responses."},
149
+ {"role": "user", "content": prompt}
150
+ ],
151
+ "stream": False,
152
+ "temperature": 0.7
153
+ }
154
+ response = requests.post(SILICONFLOW_API_URL, headers=headers, json=payload, timeout=30)
155
+ response.raise_for_status()
156
+ result = response.json()
157
+ output = result['choices'][0]['message']['content']
158
+ logs.append({"timestamp": datetime.datetime.now().isoformat(), "level": "SUCCESS", "message": "LLM response received."})
159
+ except Exception as e:
160
+ logs.append({"timestamp": datetime.datetime.now().isoformat(), "level": "ERROR", "message": f"LLM Error: {str(e)}"})
161
+ # Mock fallback for reliability
162
+ output = f"[Mock Output] Failed to call API. Mock result for prompt: {prompt[:20]}..."
163
+
164
+ # Store output in context
165
+ context[step_id] = output
166
+ # Also update the step object to return to frontend
167
+ step['output'] = output
168
+
169
+ final_status = "success"
170
+
171
+ except Exception as e:
172
+ final_status = "error"
173
+ logs.append({"timestamp": datetime.datetime.now().isoformat(), "level": "CRITICAL", "message": f"Workflow failed: {str(e)}"})
174
+
175
+ # Save run
176
+ conn = get_db_connection()
177
+ conn.execute('INSERT INTO runs (id, workflow_id, status, result, logs, created_at) VALUES (?, ?, ?, ?, ?, ?)',
178
+ (run_id, workflow_id, final_status, json.dumps(context), json.dumps(logs), datetime.datetime.now().isoformat()))
179
+ conn.commit()
180
+ conn.close()
181
+
182
+ return jsonify({
183
+ "run_id": run_id,
184
+ "status": final_status,
185
+ "logs": logs,
186
+ "results": context
187
+ })
188
+
189
+ @app.route('/api/chat', methods=['POST'])
190
+ def chat():
191
+ # Direct chat endpoint for "Assistant" feature
192
+ data = request.json
193
+ message = data.get('message', '')
194
+
195
+ try:
196
+ headers = {
197
+ "Authorization": f"Bearer {SILICONFLOW_API_KEY}",
198
+ "Content-Type": "application/json"
199
+ }
200
+ payload = {
201
+ "model": "Qwen/Qwen2.5-7B-Instruct",
202
+ "messages": [
203
+ {"role": "system", "content": "You are LogicStream AI, an intelligent assistant for building business workflows."},
204
+ {"role": "user", "content": message}
205
+ ],
206
+ "stream": False
207
+ }
208
+ response = requests.post(SILICONFLOW_API_URL, headers=headers, json=payload, timeout=30)
209
+ return jsonify(response.json())
210
+ except Exception as e:
211
+ return jsonify({"error": str(e)}), 500
212
+
213
+ if __name__ == '__main__':
214
+ app.run(host='0.0.0.0', port=7860, debug=True)
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ Flask
2
+ flask-cors
3
+ requests
4
+ python-dotenv
5
+ gunicorn
templates/index.html ADDED
@@ -0,0 +1,400 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>LogicStream - 智能业务决策编排引擎</title>
7
+ <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ <script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
10
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
11
+ <style>
12
+ body { font-family: 'Inter', system-ui, -apple-system, sans-serif; background-color: #f3f4f6; color: #1f2937; }
13
+ .fade-enter-active, .fade-leave-active { transition: opacity 0.3s ease; }
14
+ .fade-enter-from, .fade-leave-to { opacity: 0; }
15
+ /* Custom scrollbar */
16
+ ::-webkit-scrollbar { width: 8px; height: 8px; }
17
+ ::-webkit-scrollbar-track { background: #f1f1f1; }
18
+ ::-webkit-scrollbar-thumb { background: #c1c1c1; border-radius: 4px; }
19
+ ::-webkit-scrollbar-thumb:hover { background: #a8a8a8; }
20
+ </style>
21
+ </head>
22
+ <body>
23
+ <div id="app" class="h-screen flex flex-col overflow-hidden">
24
+ <!-- Header -->
25
+ <header class="bg-white border-b border-gray-200 h-16 flex items-center justify-between px-6 shadow-sm z-10">
26
+ <div class="flex items-center space-x-3">
27
+ <div class="w-8 h-8 bg-blue-600 rounded-lg flex items-center justify-center text-white font-bold">L</div>
28
+ <h1 class="text-xl font-bold text-gray-800">LogicStream <span class="text-xs font-normal text-gray-500 ml-2">智能业务编排</span></h1>
29
+ </div>
30
+ <div class="flex items-center space-x-4">
31
+ <button @click="currentView = 'dashboard'" :class="{'text-blue-600 font-semibold': currentView === 'dashboard', 'text-gray-500': currentView !== 'dashboard'}" class="hover:text-blue-600 transition">概览</button>
32
+ <button @click="currentView = 'designer'" :class="{'text-blue-600 font-semibold': currentView === 'designer', 'text-gray-500': currentView !== 'designer'}" class="hover:text-blue-600 transition">工作流设计</button>
33
+ <div class="h-4 w-px bg-gray-300"></div>
34
+ <button class="text-gray-500 hover:text-blue-600"><i class="fas fa-user-circle text-xl"></i></button>
35
+ </div>
36
+ </header>
37
+
38
+ <!-- Main Content -->
39
+ <div class="flex-1 flex overflow-hidden">
40
+ <!-- Sidebar (Workflow List) -->
41
+ <aside class="w-64 bg-white border-r border-gray-200 flex flex-col" v-if="currentView === 'designer'">
42
+ <div class="p-4 border-b border-gray-200 flex justify-between items-center">
43
+ <h2 class="font-semibold text-gray-700">我的工作流</h2>
44
+ <button @click="createNewWorkflow" class="text-blue-600 hover:bg-blue-50 p-1 rounded"><i class="fas fa-plus"></i></button>
45
+ </div>
46
+ <div class="flex-1 overflow-y-auto p-2 space-y-2">
47
+ <div v-for="wf in workflows" :key="wf.id"
48
+ @click="selectWorkflow(wf)"
49
+ :class="{'bg-blue-50 border-blue-200': currentWorkflow && currentWorkflow.id === wf.id, 'hover:bg-gray-50 border-transparent': !currentWorkflow || currentWorkflow.id !== wf.id}"
50
+ class="p-3 rounded-lg border cursor-pointer transition group relative">
51
+ <div class="font-medium text-sm text-gray-800 truncate">${ wf.name }</div>
52
+ <div class="text-xs text-gray-500 truncate mt-1">${ wf.description || '无描述' }</div>
53
+ <button @click.stop="deleteWorkflow(wf.id)" class="absolute right-2 top-3 text-gray-400 hover:text-red-500 opacity-0 group-hover:opacity-100 transition"><i class="fas fa-trash-alt"></i></button>
54
+ </div>
55
+ </div>
56
+ </aside>
57
+
58
+ <!-- Workspace -->
59
+ <main class="flex-1 bg-gray-50 relative flex flex-col overflow-hidden">
60
+
61
+ <!-- Dashboard View -->
62
+ <div v-if="currentView === 'dashboard'" class="p-8 overflow-y-auto h-full">
63
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
64
+ <div class="bg-white p-6 rounded-xl shadow-sm border border-gray-100">
65
+ <div class="text-gray-500 text-sm mb-1">总工作流数</div>
66
+ <div class="text-3xl font-bold text-gray-800">${ workflows.length }</div>
67
+ </div>
68
+ <div class="bg-white p-6 rounded-xl shadow-sm border border-gray-100">
69
+ <div class="text-gray-500 text-sm mb-1">今日运行次数</div>
70
+ <div class="text-3xl font-bold text-blue-600">12</div>
71
+ </div>
72
+ <div class="bg-white p-6 rounded-xl shadow-sm border border-gray-100">
73
+ <div class="text-gray-500 text-sm mb-1">平均成功率</div>
74
+ <div class="text-3xl font-bold text-green-600">98.5%</div>
75
+ </div>
76
+ </div>
77
+
78
+ <div class="bg-white p-6 rounded-xl shadow-sm border border-gray-100 mb-8 h-80">
79
+ <h3 class="text-lg font-semibold mb-4 text-gray-700">调用趋势 (Mock Data)</h3>
80
+ <div id="chart-container" class="w-full h-full"></div>
81
+ </div>
82
+ </div>
83
+
84
+ <!-- Designer View -->
85
+ <div v-else-if="currentView === 'designer' && currentWorkflow" class="flex flex-1 overflow-hidden">
86
+ <!-- Canvas Area -->
87
+ <div class="flex-1 flex flex-col h-full">
88
+ <!-- Toolbar -->
89
+ <div class="bg-white border-b border-gray-200 p-3 flex justify-between items-center">
90
+ <div class="flex items-center space-x-2">
91
+ <input v-model="currentWorkflow.name" class="font-bold text-lg bg-transparent border-b border-transparent hover:border-gray-300 focus:border-blue-500 focus:outline-none px-1" />
92
+ <span class="text-xs text-gray-400 px-2 py-1 bg-gray-100 rounded">上次保存: ${ formatTime(currentWorkflow.updated_at) }</span>
93
+ </div>
94
+ <div class="flex space-x-2">
95
+ <button @click="saveWorkflow" class="px-3 py-1.5 bg-white border border-gray-300 text-gray-700 rounded hover:bg-gray-50 text-sm"><i class="fas fa-save mr-1"></i> 保存</button>
96
+ <button @click="runWorkflow" class="px-3 py-1.5 bg-blue-600 text-white rounded hover:bg-blue-700 text-sm shadow-sm flex items-center">
97
+ <i class="fas fa-play mr-1" :class="{'fa-spin': isRunning}"></i> ${ isRunning ? '运行中...' : '运行' }
98
+ </button>
99
+ </div>
100
+ </div>
101
+
102
+ <!-- Workflow Steps Editor -->
103
+ <div class="flex-1 overflow-y-auto p-6 space-y-4">
104
+ <div v-if="!currentWorkflow.steps || currentWorkflow.steps.length === 0" class="text-center py-20 text-gray-400 border-2 border-dashed border-gray-300 rounded-xl">
105
+ <p>暂无步骤,点击下方按钮添加</p>
106
+ </div>
107
+
108
+ <div v-for="(step, index) in currentWorkflow.steps" :key="step.id" class="bg-white rounded-lg shadow-sm border border-gray-200 p-4 relative transition hover:shadow-md">
109
+ <div class="absolute -left-3 top-1/2 transform -translate-y-1/2 w-6 h-6 bg-blue-100 text-blue-600 rounded-full flex items-center justify-center text-xs font-bold border border-blue-200 z-10">
110
+ ${ index + 1 }
111
+ </div>
112
+
113
+ <div class="flex justify-between items-start mb-3">
114
+ <div class="flex items-center space-x-2">
115
+ <span class="px-2 py-0.5 rounded text-xs font-medium uppercase tracking-wide"
116
+ :class="{
117
+ 'bg-green-100 text-green-700': step.type === 'input',
118
+ 'bg-purple-100 text-purple-700': step.type === 'llm'
119
+ }">
120
+ ${ step.type }
121
+ </span>
122
+ <input v-model="step.name" class="font-semibold text-gray-700 bg-transparent focus:outline-none border-b border-transparent focus:border-blue-300" placeholder="步骤名称" />
123
+ <span class="text-xs text-gray-400 font-mono">ID: ${ step.id }</span>
124
+ </div>
125
+ <button @click="removeStep(index)" class="text-gray-400 hover:text-red-500"><i class="fas fa-times"></i></button>
126
+ </div>
127
+
128
+ <!-- Step Content -->
129
+ <div v-if="step.type === 'input'" class="space-y-2">
130
+ <label class="block text-xs font-medium text-gray-500">输入内容 (作为初始上下文)</label>
131
+ <textarea v-model="step.content" rows="2" class="w-full text-sm p-2 border border-gray-300 rounded focus:ring-1 focus:ring-blue-500 focus:border-blue-500 outline-none resize-none"></textarea>
132
+ </div>
133
+
134
+ <div v-if="step.type === 'llm'" class="space-y-2">
135
+ <label class="block text-xs font-medium text-gray-500">提示词 (Prompt)</label>
136
+ <div class="relative">
137
+ <textarea v-model="step.prompt" rows="3" class="w-full text-sm p-2 border border-gray-300 rounded focus:ring-1 focus:ring-blue-500 focus:border-blue-500 outline-none resize-none font-mono text-gray-600"></textarea>
138
+ <div class="text-xs text-gray-400 mt-1">可用变量: <span v-for="(s, i) in currentWorkflow.steps" :key="s.id" v-show="i < index" class="mr-1 bg-gray-100 px-1 rounded cursor-pointer hover:bg-gray-200" @click="insertVar(step, s.id)">${ '${' + s.id + '.output}' }</span></div>
139
+ </div>
140
+ </div>
141
+
142
+ <!-- Result Preview -->
143
+ <div v-if="runResults && runResults[step.id]" class="mt-3 pt-3 border-t border-gray-100 bg-gray-50 -mx-4 -mb-4 px-4 py-3 rounded-b-lg">
144
+ <div class="text-xs font-bold text-gray-500 mb-1 flex justify-between">
145
+ <span>运行结果</span>
146
+ <span class="text-green-600"><i class="fas fa-check-circle"></i> 完成</span>
147
+ </div>
148
+ <pre class="text-xs text-gray-700 whitespace-pre-wrap overflow-x-auto max-h-40 font-mono bg-white p-2 border border-gray-200 rounded">${ runResults[step.id] }</pre>
149
+ </div>
150
+ </div>
151
+
152
+ <!-- Add Step Buttons -->
153
+ <div class="flex justify-center space-x-3 pt-4 pb-10">
154
+ <button @click="addStep('input')" class="flex items-center space-x-1 px-4 py-2 bg-white border border-gray-300 shadow-sm rounded-full text-sm text-gray-600 hover:bg-gray-50 hover:text-blue-600 transition">
155
+ <i class="fas fa-keyboard"></i> <span>添加输入</span>
156
+ </button>
157
+ <button @click="addStep('llm')" class="flex items-center space-x-1 px-4 py-2 bg-white border border-gray-300 shadow-sm rounded-full text-sm text-gray-600 hover:bg-gray-50 hover:text-purple-600 transition">
158
+ <i class="fas fa-robot"></i> <span>添加 AI 分析</span>
159
+ </button>
160
+ </div>
161
+ </div>
162
+ </div>
163
+
164
+ <!-- Right Panel: Logs -->
165
+ <div class="w-80 bg-white border-l border-gray-200 flex flex-col h-full shadow-lg z-20" v-show="showLogs">
166
+ <div class="p-3 border-b border-gray-200 bg-gray-50 flex justify-between items-center">
167
+ <h3 class="font-semibold text-gray-700">运行日志</h3>
168
+ <button @click="logs = []" class="text-xs text-gray-500 hover:text-gray-800">清空</button>
169
+ </div>
170
+ <div class="flex-1 overflow-y-auto p-3 space-y-2 font-mono text-xs">
171
+ <div v-if="logs.length === 0" class="text-gray-400 text-center py-10">等待运行...</div>
172
+ <div v-for="(log, idx) in logs" :key="idx" class="p-2 rounded bg-gray-50 border-l-2"
173
+ :class="{'border-blue-500': log.level === 'INFO', 'border-green-500': log.level === 'SUCCESS', 'border-red-500': log.level === 'ERROR'}">
174
+ <div class="text-gray-400 text-[10px] mb-0.5">${ log.timestamp.split('T')[1].split('.')[0] }</div>
175
+ <div class="text-gray-800 break-words">${ log.message }</div>
176
+ </div>
177
+ </div>
178
+ </div>
179
+ </div>
180
+
181
+ <div v-else class="flex-1 flex items-center justify-center text-gray-400">
182
+ <div class="text-center">
183
+ <i class="fas fa-project-diagram text-6xl mb-4 text-gray-200"></i>
184
+ <p>请选择或创建一个工作流</p>
185
+ </div>
186
+ </div>
187
+
188
+ </main>
189
+ </div>
190
+ </div>
191
+
192
+ <script>
193
+ {% raw %}
194
+ const { createApp, ref, onMounted, nextTick } = Vue;
195
+
196
+ createApp({
197
+ delimiters: ['${', '}'],
198
+ setup() {
199
+ const currentView = ref('dashboard');
200
+ const workflows = ref([]);
201
+ const currentWorkflow = ref(null);
202
+ const isRunning = ref(false);
203
+ const logs = ref([]);
204
+ const showLogs = ref(true);
205
+ const runResults = ref({});
206
+
207
+ const fetchWorkflows = async () => {
208
+ try {
209
+ const res = await fetch('/api/workflows');
210
+ workflows.value = await res.json();
211
+ } catch (e) {
212
+ console.error("Failed to fetch workflows", e);
213
+ }
214
+ };
215
+
216
+ const createNewWorkflow = async () => {
217
+ const newWf = {
218
+ name: "未命名工作流 " + (workflows.value.length + 1),
219
+ description: "新建的业务逻辑流",
220
+ steps: []
221
+ };
222
+ try {
223
+ const res = await fetch('/api/workflows', {
224
+ method: 'POST',
225
+ headers: {'Content-Type': 'application/json'},
226
+ body: JSON.stringify(newWf)
227
+ });
228
+ const data = await res.json();
229
+ await fetchWorkflows();
230
+ // Select the new one
231
+ const created = workflows.value.find(w => w.id === data.id);
232
+ if (created) selectWorkflow(created);
233
+ } catch (e) {
234
+ alert("创建失败: " + e.message);
235
+ }
236
+ };
237
+
238
+ const selectWorkflow = (wf) => {
239
+ // Deep copy to avoid direct mutation issues before save
240
+ currentWorkflow.value = JSON.parse(JSON.stringify(wf));
241
+ // Ensure steps is array
242
+ if (typeof currentWorkflow.value.steps === 'string') {
243
+ try {
244
+ currentWorkflow.value.steps = JSON.parse(currentWorkflow.value.steps);
245
+ } catch(e) {
246
+ currentWorkflow.value.steps = [];
247
+ }
248
+ }
249
+ if (!currentWorkflow.value.steps) currentWorkflow.value.steps = [];
250
+ currentView.value = 'designer';
251
+ runResults.value = {};
252
+ logs.value = [];
253
+ };
254
+
255
+ const saveWorkflow = async () => {
256
+ if (!currentWorkflow.value) return;
257
+ try {
258
+ await fetch(`/api/workflows/${currentWorkflow.value.id}`, {
259
+ method: 'PUT',
260
+ headers: {'Content-Type': 'application/json'},
261
+ body: JSON.stringify(currentWorkflow.value)
262
+ });
263
+ await fetchWorkflows(); // Refresh list
264
+ alert("保存成功");
265
+ } catch (e) {
266
+ alert("保存失败: " + e.message);
267
+ }
268
+ };
269
+
270
+ const deleteWorkflow = async (id) => {
271
+ if (!confirm("确定要删除吗?")) return;
272
+ try {
273
+ await fetch(`/api/workflows/${id}`, { method: 'DELETE' });
274
+ await fetchWorkflows();
275
+ if (currentWorkflow.value && currentWorkflow.value.id === id) {
276
+ currentWorkflow.value = null;
277
+ }
278
+ } catch (e) {
279
+ alert("删除失败");
280
+ }
281
+ };
282
+
283
+ const addStep = (type) => {
284
+ if (!currentWorkflow.value) return;
285
+ const id = `step_${currentWorkflow.value.steps.length + 1}`;
286
+ currentWorkflow.value.steps.push({
287
+ id: id,
288
+ type: type,
289
+ name: type === 'input' ? '输入数据' : 'AI 处理',
290
+ content: '',
291
+ prompt: '',
292
+ output: ''
293
+ });
294
+ };
295
+
296
+ const removeStep = (index) => {
297
+ currentWorkflow.value.steps.splice(index, 1);
298
+ };
299
+
300
+ const runWorkflow = async () => {
301
+ if (!currentWorkflow.value) return;
302
+ isRunning.value = true;
303
+ logs.value = [];
304
+ runResults.value = {};
305
+
306
+ // Optimistic UI: save first
307
+ await saveWorkflow();
308
+
309
+ try {
310
+ const res = await fetch('/api/run_workflow', {
311
+ method: 'POST',
312
+ headers: {'Content-Type': 'application/json'},
313
+ body: JSON.stringify({
314
+ workflow_id: currentWorkflow.value.id,
315
+ steps: currentWorkflow.value.steps
316
+ })
317
+ });
318
+ const data = await res.json();
319
+ logs.value = data.logs;
320
+ runResults.value = data.results;
321
+
322
+ if (data.status === 'success') {
323
+ // Update local steps with output
324
+ // Not strictly necessary as results are in runResults, but good for persistence logic later
325
+ } else {
326
+ logs.value.push({timestamp: new Date().toISOString(), level: 'ERROR', message: "Run returned error status"});
327
+ }
328
+ } catch (e) {
329
+ logs.value.push({timestamp: new Date().toISOString(), level: 'CRITICAL', message: "Network error: " + e.message});
330
+ } finally {
331
+ isRunning.value = false;
332
+ }
333
+ };
334
+
335
+ const insertVar = (step, varName) => {
336
+ step.prompt += `{{${varName}.output}}`;
337
+ };
338
+
339
+ const formatTime = (t) => {
340
+ if (!t) return '';
341
+ return new Date(t).toLocaleString('zh-CN');
342
+ };
343
+
344
+ // Chart Init
345
+ const initChart = () => {
346
+ const chartDom = document.getElementById('chart-container');
347
+ if (!chartDom) return;
348
+ const myChart = echarts.init(chartDom);
349
+ const option = {
350
+ tooltip: { trigger: 'axis' },
351
+ grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
352
+ xAxis: { type: 'category', boundaryGap: false, data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] },
353
+ yAxis: { type: 'value' },
354
+ series: [
355
+ {
356
+ name: 'Executions',
357
+ type: 'line',
358
+ stack: 'Total',
359
+ smooth: true,
360
+ areaStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{offset: 0, color: '#3B82F6'}, {offset: 1, color: '#EFF6FF'}]) },
361
+ itemStyle: { color: '#3B82F6' },
362
+ data: [120, 132, 101, 134, 90, 230, 210]
363
+ }
364
+ ]
365
+ };
366
+ myChart.setOption(option);
367
+ window.addEventListener('resize', () => myChart.resize());
368
+ };
369
+
370
+ onMounted(() => {
371
+ fetchWorkflows();
372
+ // Delay chart init slightly to ensure DOM is ready if starting on dashboard
373
+ setTimeout(initChart, 500);
374
+ });
375
+
376
+ return {
377
+ currentView,
378
+ workflows,
379
+ currentWorkflow,
380
+ isRunning,
381
+ logs,
382
+ showLogs,
383
+ runResults,
384
+ fetchWorkflows,
385
+ createNewWorkflow,
386
+ selectWorkflow,
387
+ saveWorkflow,
388
+ deleteWorkflow,
389
+ addStep,
390
+ removeStep,
391
+ runWorkflow,
392
+ insertVar,
393
+ formatTime
394
+ };
395
+ }
396
+ }).mount('#app');
397
+ {% endraw %}
398
+ </script>
399
+ </body>
400
+ </html>