feat(node-engine): 实现异步图像生成任务处理 - 添加异步任务提交功能,支持 task_id 查询机制 - 实现轮询机制监控任务状态(processing/succeeded/failed) - 增加超时控制和错误处理机制 - 兼容同步模式回退处理 feat(app): 集成AI聊天助手功能 - 添加 requests 依赖用于API调用 - 实现流式响应处理Gemini API - 添加代理URL配置支持 - 集成Marked.js进行Markdown渲染 refactor(ui): 重构AI助手模态框为全屏布局 - 调整样式为固定右侧全高度布局 - 移除边框阴影,增加背景模糊效果 - 优化滚动行为和输入区域样式 feat(board): 实现节点级联执行逻辑 - 添加工作流自动触发机制 - 实现节点间数据传递和连接处理 - 添加加载状态反馈和执行监控 feat(chat): 优化聊天交互体验 - 替换HTMX表单提交为原生JavaScript处理 - 实现打字指示器和消息滚动 - 添加错误处理和内容解析功能 ```
152 lines
5.8 KiB
Python
152 lines
5.8 KiB
Python
import os
|
|
import json
|
|
import requests
|
|
from flask import Flask, render_template, request, jsonify, send_from_directory
|
|
from node_engine import NodeEngine
|
|
from database import init_db
|
|
from config import Config
|
|
|
|
app = Flask(__name__)
|
|
app.config.from_object(Config)
|
|
|
|
# Ensure directories exist
|
|
os.makedirs('configs/nodes', exist_ok=True)
|
|
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
|
|
os.makedirs('static/css', exist_ok=True)
|
|
os.makedirs('static/js', exist_ok=True)
|
|
|
|
# Initialize engine and DB
|
|
engine = NodeEngine()
|
|
init_db()
|
|
|
|
@app.route('/')
|
|
def index():
|
|
# Reload engine to pick up any manual JSON changes
|
|
nodes_config = engine.get_all_node_configs()
|
|
return render_template('board.html', node_configs=nodes_config)
|
|
|
|
@app.route('/api/nodes/configs')
|
|
def get_node_configs():
|
|
return jsonify(engine.get_all_node_configs())
|
|
|
|
@app.route('/api/create_node', methods=['POST'])
|
|
def create_node():
|
|
node_type_id = request.args.get('type')
|
|
x = int(request.args.get('x', 100))
|
|
y = int(request.args.get('y', 100))
|
|
|
|
# Get config
|
|
configs = engine.get_all_node_configs()
|
|
node_data = next((c for c in configs if c['id'] == node_type_id), None)
|
|
|
|
if not node_data:
|
|
return "Node type not found", 404
|
|
|
|
instance_id = f"inst_{node_type_id}_{os.urandom(2).hex()}"
|
|
return render_template('partials/node.html',
|
|
node_data=node_data,
|
|
node_instance_id=instance_id,
|
|
x=x, y=y)
|
|
|
|
@app.route('/api/upload_asset', methods=['POST'])
|
|
def upload_asset():
|
|
if 'file' not in request.files:
|
|
return "No file", 400
|
|
file = request.files['file']
|
|
if file.filename == '':
|
|
return "No selected file", 400
|
|
|
|
filename = f"upload_{os.urandom(4).hex()}_{file.filename}"
|
|
upload_dir = os.path.join(app.root_path, 'static', 'uploads')
|
|
os.makedirs(upload_dir, exist_ok=True)
|
|
filepath = os.path.join(upload_dir, filename)
|
|
file.save(filepath)
|
|
|
|
print(f"File saved to: {filepath}")
|
|
|
|
# URL for access via browser
|
|
image_url = f"/static/uploads/{filename}"
|
|
|
|
return f'''
|
|
<div class="result-container" data-output-value="{image_url}" data-output-type="image">
|
|
<img src="{image_url}" class="generated-art animated fadeIn">
|
|
<div class="meta-info">✅ 上传成功: {file.filename}</div>
|
|
<input type="hidden" name="uploaded_url" value="{image_url}">
|
|
</div>
|
|
'''
|
|
|
|
@app.route('/api/run_node/<node_id>', methods=['POST'])
|
|
def run_node(node_id):
|
|
data = request.form.to_dict()
|
|
result = engine.execute_node(node_id, data)
|
|
|
|
if result.get('type') == 'image':
|
|
return f'''
|
|
<div class="result-container" data-output-value="{result["url"]}" data-output-type="image">
|
|
<img src="{result["url"]}" class="generated-art animated fadeIn">
|
|
<div class="meta-info">处理完成 | 耗时: {result["time"]}s</div>
|
|
</div>
|
|
'''
|
|
elif result.get('type') == 'text':
|
|
return f'''
|
|
<div class="result-container" data-output-value='{result["content"]}' data-output-type="text">
|
|
<pre class="text-output animated fadeIn">{result["content"]}</pre>
|
|
<div class="meta-info">处理完成 | 耗时: {result["time"]}s</div>
|
|
</div>
|
|
'''
|
|
else:
|
|
error_msg = result.get('error', '未知错误')
|
|
return f'<div class="error-badge" data-error="{error_msg}">❌ 错误: {error_msg}</div>'
|
|
|
|
@app.route('/api/chat', methods=['POST'])
|
|
@app.route('/api/chat', methods=['POST'])
|
|
def chat():
|
|
user_message = request.form.get('message', '')
|
|
|
|
def generate():
|
|
# Do not yield user message here; frontend handles it.
|
|
|
|
# Use proxy + baseurl + endpoint concatenation
|
|
target_url = f"{Config.BASE_URL.rstrip('/')}/v1/chat/completions"
|
|
url = f"{Config.PROXY}{target_url}"
|
|
|
|
headers = {
|
|
"Authorization": f"Bearer {Config.API_KEY}",
|
|
"Content-Type": "application/json"
|
|
}
|
|
payload = {
|
|
"model": "gemini-3-flash-preview",
|
|
"messages": [
|
|
{"role": "system", "content": "You are a professional Design Assistant (设计助理) for NoirFlow. Your goal is to help users maintain a consistent, premium, and aesthetic design language in their workflows. You provide advice on color theory, layout, and visual harmony, while also assisting with the technical node configuration when asked. Keep your tone professional, artistic, and concise."},
|
|
{"role": "user", "content": user_message}
|
|
],
|
|
"stream": True
|
|
}
|
|
|
|
try:
|
|
with requests.post(url, headers=headers, json=payload, stream=True, timeout=60) as r:
|
|
r.raise_for_status()
|
|
for line in r.iter_lines():
|
|
if line:
|
|
decoded_line = line.decode('utf-8')
|
|
if decoded_line.startswith('data: '):
|
|
json_str = decoded_line[6:]
|
|
if json_str == '[DONE]':
|
|
break
|
|
try:
|
|
data = json.loads(json_str)
|
|
content = data['choices'][0]['delta'].get('content', '')
|
|
if content:
|
|
# Basic formatting
|
|
formatted_content = content.replace('\n', '<br>')
|
|
yield formatted_content
|
|
except:
|
|
pass
|
|
except Exception as e:
|
|
yield f"[Error: {str(e)}]"
|
|
|
|
return app.response_class(generate(), mimetype='text/html')
|
|
|
|
if __name__ == '__main__':
|
|
app.run(debug=True, port=5000)
|