Spaces:
Sleeping
Sleeping
Create backend/routes_terminal.py
Browse files- backend/routes_terminal.py +109 -0
backend/routes_terminal.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
import re
|
| 4 |
+
import asyncio
|
| 5 |
+
import subprocess
|
| 6 |
+
import traceback
|
| 7 |
+
import urllib.request
|
| 8 |
+
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
|
| 9 |
+
from backend.database import get_user_dir, USERS_DIR
|
| 10 |
+
from backend.routes_ai import ask_openrouter
|
| 11 |
+
|
| 12 |
+
router = APIRouter()
|
| 13 |
+
|
| 14 |
+
SYSTEM_PROMPT = """You are DEVPORTAL AI, an advanced terminal assistant.
|
| 15 |
+
You are operating within a user's private Linux directory.
|
| 16 |
+
To execute a bash command, output it wrapped EXACTLY in <EXEC> and </EXEC> tags.
|
| 17 |
+
Wait for the system to provide the output before taking further action. Keep responses concise."""
|
| 18 |
+
|
| 19 |
+
@router.websocket("/ws/{token}")
|
| 20 |
+
async def websocket_endpoint(websocket: WebSocket, token: str):
|
| 21 |
+
await websocket.accept()
|
| 22 |
+
user_dir = get_user_dir(token)
|
| 23 |
+
if not user_dir:
|
| 24 |
+
await websocket.send_text(json.dumps({"type": "error", "content": "Authentication failed."}))
|
| 25 |
+
await websocket.close()
|
| 26 |
+
return
|
| 27 |
+
|
| 28 |
+
current_dir = user_dir
|
| 29 |
+
session_history = [{"role": "system", "content": SYSTEM_PROMPT}]
|
| 30 |
+
|
| 31 |
+
await websocket.send_text(json.dumps({"type": "system", "content": f"Secure DEVPORTAL environment established. Type 'debug net' to run network diagnostics."}))
|
| 32 |
+
|
| 33 |
+
def run_cmd(cmd):
|
| 34 |
+
nonlocal current_dir
|
| 35 |
+
if cmd.startswith("cd "):
|
| 36 |
+
target = cmd[3:].strip()
|
| 37 |
+
new_dir = os.path.abspath(os.path.join(current_dir, target))
|
| 38 |
+
if not new_dir.startswith(os.path.abspath(USERS_DIR)): return "Permission denied."
|
| 39 |
+
current_dir = new_dir
|
| 40 |
+
return f"Directory changed to {current_dir}"
|
| 41 |
+
else:
|
| 42 |
+
try:
|
| 43 |
+
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, cwd=current_dir, timeout=25)
|
| 44 |
+
out = result.stdout + result.stderr
|
| 45 |
+
return out if out else "[Executed successfully with no output]"
|
| 46 |
+
except Exception as e: return f"[Execution Error: {str(e)}]"
|
| 47 |
+
|
| 48 |
+
try:
|
| 49 |
+
while True:
|
| 50 |
+
data = await websocket.receive_text()
|
| 51 |
+
payload = json.loads(data)
|
| 52 |
+
command = payload.get("command", "").strip()
|
| 53 |
+
if not command: continue
|
| 54 |
+
|
| 55 |
+
if command.lower() == "debug net":
|
| 56 |
+
debug_info = "=== NETWORK DIAGNOSTICS ===\n"
|
| 57 |
+
for k, v in os.environ.items():
|
| 58 |
+
if 'proxy' in k.lower(): debug_info += f" {k} = {v}\n"
|
| 59 |
+
debug_info += f"\n2. Python internal getproxies():\n {urllib.request.getproxies()}\n"
|
| 60 |
+
await websocket.send_text(json.dumps({"type": "output", "content": debug_info}))
|
| 61 |
+
continue
|
| 62 |
+
|
| 63 |
+
if command.lower().startswith("ai "):
|
| 64 |
+
prompt = command[3:].strip()
|
| 65 |
+
await websocket.send_text(json.dumps({"type": "ai_status", "status": "thinking"}))
|
| 66 |
+
session_history.append({"role": "user", "content": prompt})
|
| 67 |
+
if len(session_history) > 21: session_history = [session_history[0]] + session_history[-20:]
|
| 68 |
+
|
| 69 |
+
for _ in range(5):
|
| 70 |
+
ai_response = await ask_openrouter(session_history)
|
| 71 |
+
if "ERROR:" in ai_response:
|
| 72 |
+
await websocket.send_text(json.dumps({"type": "error", "content": ai_response}))
|
| 73 |
+
session_history.pop()
|
| 74 |
+
break
|
| 75 |
+
|
| 76 |
+
match = re.search(r'<EXEC>(.*?)</EXEC>', ai_response, re.DOTALL)
|
| 77 |
+
if match:
|
| 78 |
+
exec_cmd = match.group(1).strip()
|
| 79 |
+
text_before = ai_response[:match.start()].strip()
|
| 80 |
+
|
| 81 |
+
full_ai_message = ""
|
| 82 |
+
if text_before:
|
| 83 |
+
await websocket.send_text(json.dumps({"type": "ai", "content": text_before}))
|
| 84 |
+
full_ai_message += text_before + "\n"
|
| 85 |
+
|
| 86 |
+
await websocket.send_text(json.dumps({"type": "system", "content": f"⚙️ AI is running: {exec_cmd}"}))
|
| 87 |
+
await websocket.send_text(json.dumps({"type": "ai_status", "status": f"executing..."}))
|
| 88 |
+
|
| 89 |
+
full_ai_message += f"<EXEC>{exec_cmd}</EXEC>"
|
| 90 |
+
session_history.append({"role": "assistant", "content": full_ai_message})
|
| 91 |
+
|
| 92 |
+
output = run_cmd(exec_cmd)
|
| 93 |
+
await websocket.send_text(json.dumps({"type": "output", "content": output}))
|
| 94 |
+
session_history.append({"role": "user", "content": f"Command Output:\n{output}"})
|
| 95 |
+
await asyncio.sleep(1)
|
| 96 |
+
else:
|
| 97 |
+
await websocket.send_text(json.dumps({"type": "ai", "content": ai_response}))
|
| 98 |
+
session_history.append({"role": "assistant", "content": ai_response})
|
| 99 |
+
break
|
| 100 |
+
await websocket.send_text(json.dumps({"type": "ai_status", "status": "idle"}))
|
| 101 |
+
else:
|
| 102 |
+
if command.lower() == "clear":
|
| 103 |
+
await websocket.send_text(json.dumps({"type": "clear", "content": ""}))
|
| 104 |
+
else:
|
| 105 |
+
output = run_cmd(command)
|
| 106 |
+
await websocket.send_text(json.dumps({"type": "output", "content": output}))
|
| 107 |
+
except WebSocketDisconnect:
|
| 108 |
+
pass
|
| 109 |
+
|