lyh-917 commited on
Commit
5a2cf9c
·
verified ·
1 Parent(s): 9a8061f

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +386 -0
  2. requirements.txt +8 -0
app.py ADDED
@@ -0,0 +1,386 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import json
3
+ import os
4
+ import uuid
5
+ from datetime import datetime
6
+ from json import dumps
7
+
8
+ from fastapi import Body, FastAPI, HTTPException, Request
9
+ from fastapi.middleware.cors import CORSMiddleware
10
+ from fastapi.responses import (FileResponse, HTMLResponse, JSONResponse, StreamingResponse)
11
+ from fastapi.staticfiles import StaticFiles
12
+ from fastapi.templating import Jinja2Templates
13
+ from pydantic import BaseModel
14
+ from loguru import logger
15
+ import uvicorn
16
+ import aiohttp
17
+
18
+ app = FastAPI()
19
+
20
+ OPENMANUS_ENDPOINT_URL = os.getenv("OPENMANUS_ENDPOINT_URL")
21
+ if not OPENMANUS_ENDPOINT_URL:
22
+ raise EnvironmentError("OPENMANUS_ENDPOINT_URL environment variable must be set")
23
+
24
+ app.mount("/static", StaticFiles(directory="static"), name="static")
25
+ templates = Jinja2Templates(directory="templates")
26
+
27
+ app.add_middleware(
28
+ CORSMiddleware,
29
+ allow_origins=["*"],
30
+ allow_credentials=True,
31
+ allow_methods=["*"],
32
+ allow_headers=["*"],
33
+ )
34
+
35
+
36
+ class Task(BaseModel):
37
+ id: str
38
+ prompt: str
39
+ created_at: datetime
40
+ status: str
41
+ steps: list = []
42
+
43
+ def model_dump(self, *args, **kwargs):
44
+ data = super().model_dump(*args, **kwargs)
45
+ data["created_at"] = self.created_at.isoformat()
46
+ return data
47
+
48
+
49
+ class TaskManager:
50
+ def __init__(self):
51
+ self.tasks = {}
52
+ self.queues = {}
53
+
54
+ def create_task(self, prompt: str) -> Task:
55
+ task_id = str(uuid.uuid4())
56
+ task = Task(
57
+ id=task_id, prompt=prompt, created_at=datetime.now(), status="pending"
58
+ )
59
+ self.tasks[task_id] = task
60
+ self.queues[task_id] = asyncio.Queue()
61
+ return task
62
+
63
+ async def update_task_step(
64
+ self, task_id: str, step: int, result: str, step_type: str = "step"
65
+ ):
66
+ if task_id in self.tasks:
67
+ task = self.tasks[task_id]
68
+ task.steps.append({"step": step, "result": result, "type": step_type})
69
+ await self.queues[task_id].put(
70
+ {"type": step_type, "step": step, "result": result}
71
+ )
72
+ await self.queues[task_id].put(
73
+ {"type": "status", "status": task.status, "steps": task.steps}
74
+ )
75
+
76
+ async def complete_task(self, task_id: str):
77
+ if task_id in self.tasks:
78
+ task = self.tasks[task_id]
79
+ task.status = "completed"
80
+ await self.queues[task_id].put(
81
+ {"type": "status", "status": task.status, "steps": task.steps}
82
+ )
83
+ await self.queues[task_id].put({"type": "complete"})
84
+
85
+ async def fail_task(self, task_id: str, error: str):
86
+ if task_id in self.tasks:
87
+ self.tasks[task_id].status = f"failed: {error}"
88
+ await self.queues[task_id].put({"type": "error", "message": error})
89
+
90
+
91
+ task_manager = TaskManager()
92
+
93
+
94
+ def get_available_themes():
95
+ """扫描themes目录获取所有可用主题"""
96
+ themes_dir = "static/themes"
97
+ if not os.path.exists(themes_dir):
98
+ return [{"id": "openmanus", "name": "Manus", "description": "默认主题"}]
99
+
100
+ themes = []
101
+ for item in os.listdir(themes_dir):
102
+ theme_path = os.path.join(themes_dir, item)
103
+ if os.path.isdir(theme_path):
104
+ # 验证主题文件夹是否包含必要的文件
105
+ templates_dir = os.path.join(theme_path, "templates")
106
+ static_dir = os.path.join(theme_path, "static")
107
+ config_file = os.path.join(theme_path, "theme.json")
108
+
109
+ if os.path.exists(templates_dir) and os.path.exists(static_dir):
110
+ if os.path.exists(os.path.join(templates_dir, "chat.html")):
111
+ theme_info = {"id": item, "name": item, "description": ""}
112
+
113
+ # 如果有配置文件,读取主题名称和描述
114
+ if os.path.exists(config_file):
115
+ try:
116
+ with open(config_file, "r", encoding="utf-8") as f:
117
+ config = json.load(f)
118
+ theme_info["name"] = config.get("name", item)
119
+ theme_info["description"] = config.get(
120
+ "description", ""
121
+ )
122
+ except Exception as e:
123
+ print(f"读取主题配置文件出错: {str(e)}")
124
+
125
+ themes.append(theme_info)
126
+
127
+ # 确保Normal主题始终存在
128
+ normal_exists = any(theme["id"] == "openmanus" for theme in themes)
129
+ if not normal_exists:
130
+ themes.append({"id": "openmanus", "name": "Manus", "description": "默认主题"})
131
+
132
+ return themes
133
+
134
+
135
+ @app.get("/", response_class=HTMLResponse)
136
+ async def index(request: Request):
137
+ # 获取可用主题列表
138
+ themes = get_available_themes()
139
+
140
+ # 对主题进行排序:Normal在前,cyberpunk在后,其他主题按原顺序
141
+ sorted_themes = []
142
+ normal_theme = None
143
+ cyberpunk_theme = None
144
+ other_themes = []
145
+
146
+ for theme in themes:
147
+ if theme["id"] == "openmanus":
148
+ normal_theme = theme
149
+ elif theme["id"] == "cyberpunk":
150
+ cyberpunk_theme = theme
151
+ else:
152
+ other_themes.append(theme)
153
+
154
+ # 按照指定顺序组合主题
155
+ if normal_theme:
156
+ sorted_themes.append(normal_theme)
157
+ if cyberpunk_theme:
158
+ sorted_themes.append(cyberpunk_theme)
159
+ sorted_themes.extend(other_themes)
160
+
161
+ return templates.TemplateResponse(
162
+ "index.html", {"request": request, "themes": sorted_themes}
163
+ )
164
+
165
+
166
+ @app.get("/chat", response_class=HTMLResponse)
167
+ async def chat(request: Request):
168
+ theme = request.query_params.get("theme", "openmanus")
169
+ # 尝试从主题文件夹加载chat.html
170
+ theme_chat_path = f"static/themes/{theme}/templates/chat.html"
171
+ if os.path.exists(theme_chat_path):
172
+ with open(theme_chat_path, "r", encoding="utf-8") as f:
173
+ content = f.read()
174
+
175
+ # 读取主题配置文件
176
+ theme_config_path = f"static/themes/{theme}/theme.json"
177
+ theme_name = theme
178
+ if os.path.exists(theme_config_path):
179
+ try:
180
+ with open(theme_config_path, "r", encoding="utf-8") as f:
181
+ config = json.load(f)
182
+ theme_name = config.get("name", theme)
183
+ except Exception:
184
+ pass
185
+
186
+ # 将主题名称添加到HTML标题中
187
+ content = content.replace(
188
+ "<title>Manus</title>", f"<title>Manus - {theme_name}</title>"
189
+ )
190
+ return HTMLResponse(content=content)
191
+ else:
192
+ # 默认使用templates中的chat.html
193
+ return templates.TemplateResponse("chat.html", {"request": request})
194
+
195
+
196
+ @app.get("/download")
197
+ async def download_file(file_path: str):
198
+ if not os.path.exists(file_path):
199
+ raise HTTPException(status_code=404, detail="File not found")
200
+
201
+ return FileResponse(file_path, filename=os.path.basename(file_path))
202
+
203
+
204
+ @app.post("/tasks")
205
+ async def create_task(prompt: str = Body(..., embed=True)):
206
+ task = task_manager.create_task(prompt)
207
+ asyncio.create_task(run_task(task.id, prompt))
208
+ return {"task_id": task.id}
209
+
210
+
211
+
212
+ async def run_task(task_id: str, prompt: str):
213
+ try:
214
+ task_manager.tasks[task_id].status = "running"
215
+
216
+ async def on_think(thought):
217
+ await task_manager.update_task_step(task_id, 0, thought, "think")
218
+
219
+ async def on_tool_execute(tool, input):
220
+ await task_manager.update_task_step(
221
+ task_id, 0, f"Executing tool: {tool}\nInput: {input}", "tool"
222
+ )
223
+
224
+ async def on_action(action):
225
+ await task_manager.update_task_step(
226
+ task_id, 0, f"Executing action: {action}", "act"
227
+ )
228
+
229
+ async def on_run(step, result):
230
+ await task_manager.update_task_step(task_id, step, result, "run")
231
+
232
+ class SSELogHandler:
233
+ def __init__(self, task_id):
234
+ self.task_id = task_id
235
+
236
+ async def __call__(self, message):
237
+ import re
238
+
239
+ # Extract - Subsequent Content
240
+ cleaned_message = re.sub(r"^.*? - ", "", message)
241
+ cleaned_message = re.sub(r"^.*? - ", "", cleaned_message)
242
+
243
+ event_type = "log"
244
+ if "✨ Manus's thoughts:" in cleaned_message:
245
+ event_type = "think"
246
+ elif "🛠️ Manus selected" in cleaned_message:
247
+ event_type = "tool"
248
+ elif "🎯 Tool" in cleaned_message:
249
+ event_type = "act"
250
+ elif "📝 Oops!" in cleaned_message:
251
+ event_type = "error"
252
+ elif "🏁 Special tool" in cleaned_message:
253
+ event_type = "complete"
254
+ elif "🎉 Manus result:" in cleaned_message:
255
+ event_type = "result"
256
+ cleaned_message = cleaned_message.replace("🎉 Manus result:", "")
257
+
258
+ await task_manager.update_task_step(
259
+ self.task_id, 1, cleaned_message, event_type
260
+ )
261
+ return
262
+
263
+ await task_manager.update_task_step(
264
+ self.task_id, 0, cleaned_message, event_type
265
+ )
266
+
267
+ sse_handler = SSELogHandler(task_id)
268
+ logger.add(sse_handler)
269
+
270
+ import re
271
+ def has_log_prefix(message):
272
+ # 检查字符串是否包含两个 "|" 且以 " - " 分割前缀和内容
273
+ return re.match(r"^.*?\|.*?\|.*? - ", message) is not None
274
+
275
+ async def call_manus(url: str, prompt: str):
276
+ generate_kwargs = {
277
+ "prompt": prompt,
278
+ }
279
+ async with aiohttp.ClientSession() as session:
280
+ async with session.post(
281
+ url=url,
282
+ json=generate_kwargs,
283
+ timeout=aiohttp.ClientTimeout(total=3600)
284
+ ) as response:
285
+ buffer = ""
286
+ async for line in response.content:
287
+ decode_line = line.decode('utf-8')
288
+
289
+ if has_log_prefix(decode_line) and len(buffer)>0:
290
+ logger.info(buffer)
291
+ buffer = ""
292
+ else:
293
+ buffer += decode_line
294
+
295
+ if buffer:
296
+ logger.info(buffer)
297
+
298
+ await call_manus(OPENMANUS_ENDPOINT_URL, prompt)
299
+
300
+ await task_manager.update_task_step(task_id, 1, "", "result")
301
+ await task_manager.complete_task(task_id)
302
+ except Exception as e:
303
+ await task_manager.fail_task(task_id, str(e))
304
+
305
+
306
+ @app.get("/tasks/{task_id}/events")
307
+ async def task_events(task_id: str):
308
+ async def event_generator():
309
+ if task_id not in task_manager.queues:
310
+ yield f"event: error\ndata: {dumps({'message': 'Task not found'})}\n\n"
311
+ return
312
+
313
+ queue = task_manager.queues[task_id]
314
+
315
+ task = task_manager.tasks.get(task_id)
316
+ if task:
317
+ yield f"event: status\ndata: {dumps({'type': 'status', 'status': task.status, 'steps': task.steps})}\n\n"
318
+
319
+ while True:
320
+ try:
321
+ event = await queue.get()
322
+ formatted_event = dumps(event)
323
+
324
+ yield ": heartbeat\n\n"
325
+
326
+ if event["type"] == "complete":
327
+ yield f"event: complete\ndata: {formatted_event}\n\n"
328
+ break
329
+ elif event["type"] == "error":
330
+ yield f"event: error\ndata: {formatted_event}\n\n"
331
+ break
332
+ elif event["type"] == "step":
333
+ task = task_manager.tasks.get(task_id)
334
+ if task:
335
+ yield f"event: status\ndata: {dumps({'type': 'status', 'status': task.status, 'steps': task.steps})}\n\n"
336
+ yield f"event: {event['type']}\ndata: {formatted_event}\n\n"
337
+ elif event["type"] in ["think", "tool", "act", "run"]:
338
+ yield f"event: {event['type']}\ndata: {formatted_event}\n\n"
339
+ else:
340
+ yield f"event: {event['type']}\ndata: {formatted_event}\n\n"
341
+
342
+ except asyncio.CancelledError:
343
+ print(f"Client disconnected for task {task_id}")
344
+ break
345
+ except Exception as e:
346
+ print(f"Error in event stream: {str(e)}")
347
+ yield f"event: error\ndata: {dumps({'message': str(e)})}\n\n"
348
+ break
349
+
350
+ return StreamingResponse(
351
+ event_generator(),
352
+ media_type="text/event-stream",
353
+ headers={
354
+ "Cache-Control": "no-cache",
355
+ "Connection": "keep-alive",
356
+ "X-Accel-Buffering": "no",
357
+ },
358
+ )
359
+
360
+
361
+ @app.get("/tasks")
362
+ async def get_tasks():
363
+ sorted_tasks = sorted(
364
+ task_manager.tasks.values(), key=lambda task: task.created_at, reverse=True
365
+ )
366
+ return JSONResponse(
367
+ content=[task.model_dump() for task in sorted_tasks],
368
+ headers={"Content-Type": "application/json"},
369
+ )
370
+
371
+
372
+ @app.get("/tasks/{task_id}")
373
+ async def get_task(task_id: str):
374
+ if task_id not in task_manager.tasks:
375
+ raise HTTPException(status_code=404, detail="Task not found")
376
+ return task_manager.tasks[task_id]
377
+
378
+
379
+ @app.exception_handler(Exception)
380
+ async def generic_exception_handler(request: Request, exc: Exception):
381
+ return JSONResponse(
382
+ status_code=500, content={"message": f"Server error: {str(exc)}"}
383
+ )
384
+
385
+ if __name__ == "__main__":
386
+ uvicorn.run(app, host="0.0.0.0", port=7860)
requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ pydantic~=2.10.6
2
+ loguru~=0.7.3
3
+ fastapi~=0.115.11
4
+ uvicorn~=0.34.0
5
+ pydantic_core~=2.27.2
6
+ aiofile
7
+ aiohttp
8
+ Jinja2