Spaces:
Paused
Paused
Delete cloud-browser
Browse files- cloud-browser/.env.example +0 -7
- cloud-browser/Dockerfile +0 -26
- cloud-browser/README.md +0 -133
- cloud-browser/app.py +0 -317
- cloud-browser/requirements.txt +0 -3
- cloud-browser/static/css/style.css +0 -427
- cloud-browser/static/js/app.js +0 -438
- cloud-browser/templates/index.html +0 -128
cloud-browser/.env.example
DELETED
|
@@ -1,7 +0,0 @@
|
|
| 1 |
-
# 浏览器配置
|
| 2 |
-
VIEWPORT_WIDTH=1280
|
| 3 |
-
VIEWPORT_HEIGHT=800
|
| 4 |
-
DEFAULT_URL=https://www.google.com
|
| 5 |
-
|
| 6 |
-
# 服务配置
|
| 7 |
-
PORT=7860
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
cloud-browser/Dockerfile
DELETED
|
@@ -1,26 +0,0 @@
|
|
| 1 |
-
FROM mcr.microsoft.com/playwright/python:v1.40.0-jammy
|
| 2 |
-
|
| 3 |
-
WORKDIR /app
|
| 4 |
-
|
| 5 |
-
# Install dependencies
|
| 6 |
-
COPY requirements.txt .
|
| 7 |
-
RUN pip install --no-cache-dir -r requirements.txt
|
| 8 |
-
|
| 9 |
-
# Install browsers
|
| 10 |
-
RUN playwright install chromium
|
| 11 |
-
RUN playwright install-deps chromium
|
| 12 |
-
|
| 13 |
-
# Copy application
|
| 14 |
-
COPY . .
|
| 15 |
-
|
| 16 |
-
# Environment variables
|
| 17 |
-
ENV PORT=7860
|
| 18 |
-
ENV VIEWPORT_WIDTH=1280
|
| 19 |
-
ENV VIEWPORT_HEIGHT=800
|
| 20 |
-
ENV DEFAULT_URL=https://www.google.com
|
| 21 |
-
|
| 22 |
-
# Expose port
|
| 23 |
-
EXPOSE 7860
|
| 24 |
-
|
| 25 |
-
# Run the application
|
| 26 |
-
CMD ["python", "app.py"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
cloud-browser/README.md
DELETED
|
@@ -1,133 +0,0 @@
|
|
| 1 |
-
# 云浏览器 - Cloud Browser
|
| 2 |
-
|
| 3 |
-
一个基于 Hugging Face Spaces 的云浏览器应用,在服务器端运行浏览器,用户本地只需要看画面和操作,就像云电脑一样。
|
| 4 |
-
|
| 5 |
-
## ✨ 功能特性
|
| 6 |
-
|
| 7 |
-
- 🌐 **云端浏览器**:所有网页都在服务器端加载和渲染
|
| 8 |
-
- 🖱️ **点击操作**:直接在画面上点击,就像操作真实浏览器
|
| 9 |
-
- ⌨️ **键盘输入**:点击画面后直接打字,文字输入到云端浏览器
|
| 10 |
-
- 📜 **滚动页面**:使用鼠标滚轮在画面上滚动
|
| 11 |
-
- 🎮 **玩网页游戏**:输入游戏网址,在云端运行,本地不占资源
|
| 12 |
-
- 🎨 **科技感界面**:深色主题,渐变设计,现代化 UI
|
| 13 |
-
- 📊 **实时状态**:显示当前网址、页面标题、延迟等信息
|
| 14 |
-
|
| 15 |
-
## 🚀 部署到 Hugging Face Spaces
|
| 16 |
-
|
| 17 |
-
### 方法:Docker 部署
|
| 18 |
-
|
| 19 |
-
1. 在 Hugging Face 创建一个新的 Space,选择 **Docker** 作为 SDK
|
| 20 |
-
2. 将本项目的所有文件上传到 Space 仓库
|
| 21 |
-
3. 等待部署完成(首次部署会比较慢,需要下载浏览器)
|
| 22 |
-
4. 访问你的 Space 即可使用
|
| 23 |
-
|
| 24 |
-
### 注意事项
|
| 25 |
-
|
| 26 |
-
- 首次启动需要下载 Chromium 浏览器,可能需要 1-2 分钟
|
| 27 |
-
- 建议使用至少 2GB 内存的 Space 配置
|
| 28 |
-
- 浏览器是单实例的,所有用户共享同一个浏览器会话
|
| 29 |
-
|
| 30 |
-
## 💻 本地运行
|
| 31 |
-
|
| 32 |
-
```bash
|
| 33 |
-
# 安装依赖
|
| 34 |
-
pip install -r requirements.txt
|
| 35 |
-
|
| 36 |
-
# 安装浏览器
|
| 37 |
-
playwright install chromium
|
| 38 |
-
playwright install-deps chromium
|
| 39 |
-
|
| 40 |
-
# 运行
|
| 41 |
-
python app.py
|
| 42 |
-
```
|
| 43 |
-
|
| 44 |
-
访问 http://localhost:7860 即可使用。
|
| 45 |
-
|
| 46 |
-
## ⚙️ 环境变量配置
|
| 47 |
-
|
| 48 |
-
| 变量名 | 说明 | 默认值 |
|
| 49 |
-
|--------|------|--------|
|
| 50 |
-
| `PORT` | 服务端口 | 7860 |
|
| 51 |
-
| `VIEWPORT_WIDTH` | 浏览器视口宽度 | 1280 |
|
| 52 |
-
| `VIEWPORT_HEIGHT` | 浏览器视口高度 | 800 |
|
| 53 |
-
| `DEFAULT_URL` | 默认打开的网址 | https://www.google.com |
|
| 54 |
-
|
| 55 |
-
## 🎯 使用说明
|
| 56 |
-
|
| 57 |
-
### 访问网页
|
| 58 |
-
1. 在地址栏输入网址(例如 google.com)
|
| 59 |
-
2. 按回车或点击前往按钮
|
| 60 |
-
3. 等待页面加载完成
|
| 61 |
-
|
| 62 |
-
### 点击操作
|
| 63 |
-
- 直接在浏览器画面上点击
|
| 64 |
-
- 点击位置会精确映射到云端浏览器
|
| 65 |
-
|
| 66 |
-
### 键盘输入
|
| 67 |
-
1. 点击浏览器画面获得焦点
|
| 68 |
-
2. 直接打字,文字会输入到云端浏览器
|
| 69 |
-
3. 支持 Enter、Backspace、方向键等特殊按键
|
| 70 |
-
|
| 71 |
-
### 滚动页面
|
| 72 |
-
- 使用鼠标滚轮在画面上滚动
|
| 73 |
-
- 滚动会同步到云端浏览器
|
| 74 |
-
|
| 75 |
-
### 浏览器控制
|
| 76 |
-
- 后退按钮:返回上一页
|
| 77 |
-
- 前进按钮:前进到下一页
|
| 78 |
-
- 刷新按钮:重新加载当前页面
|
| 79 |
-
|
| 80 |
-
## 🏗️ 技术栈
|
| 81 |
-
|
| 82 |
-
- **后端**:Flask + Playwright (Python)
|
| 83 |
-
- **前端**:HTML + CSS + JavaScript
|
| 84 |
-
- **浏览器引擎**:Chromium (Playwright 驱动)
|
| 85 |
-
- **部署**:Docker + Hugging Face Spaces
|
| 86 |
-
|
| 87 |
-
## ⚠️ 注意事项
|
| 88 |
-
|
| 89 |
-
### 延迟问题
|
| 90 |
-
- 每次操作都需要发送请求到服务器并等待截图返回
|
| 91 |
-
- 会有一定的延迟(通常 100-500ms)
|
| 92 |
-
- 不适合需要快速反应的游戏
|
| 93 |
-
|
| 94 |
-
### 资源消耗
|
| 95 |
-
- 浏览器运行会占用较多内存
|
| 96 |
-
- Hugging Face Spaces 免费版可能会有限制
|
| 97 |
-
|
| 98 |
-
### 安全性
|
| 99 |
-
- 用户可以访问任意网站
|
| 100 |
-
- 所有用户共享同一个浏览器实例
|
| 101 |
-
- 不建议在公共 Space 中登录重要账号
|
| 102 |
-
|
| 103 |
-
### 音频问题
|
| 104 |
-
- 当前版本不支持音频传输
|
| 105 |
-
- 网页视频和游戏没有声音
|
| 106 |
-
|
| 107 |
-
## 🔧 工作原理
|
| 108 |
-
|
| 109 |
-
```
|
| 110 |
-
用户操作 → 发送到服务器 → Playwright 执行 → 截图 → 返回给用户显示
|
| 111 |
-
```
|
| 112 |
-
|
| 113 |
-
1. 服务器端运行无头 Chromium 浏览器
|
| 114 |
-
2. 用户在前端点击/输入/滚动
|
| 115 |
-
3. 操作通过 HTTP 请求发送到后端
|
| 116 |
-
4. Playwright 在浏览器中模拟执行操作
|
| 117 |
-
5. 截取当前页面截图
|
| 118 |
-
6. 将截图返回给前端显示
|
| 119 |
-
|
| 120 |
-
## 📝 改进方向
|
| 121 |
-
|
| 122 |
-
- [ ] 使用 WebSocket 替代 HTTP,降低延迟
|
| 123 |
-
- [ ] 支持多用户隔离(每个用户独立浏览器)
|
| 124 |
-
- [ ] 添加音频传输支持
|
| 125 |
-
- [ ] 优化截图传输(增量更新、视频流等)
|
| 126 |
-
- [ ] 添加书签/历史记录功能
|
| 127 |
-
- [ ] 支持调整视口大小
|
| 128 |
-
- [ ] 添加密码保护
|
| 129 |
-
- [ ] 支持文件上传下载
|
| 130 |
-
|
| 131 |
-
## 📄 License
|
| 132 |
-
|
| 133 |
-
MIT License
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
cloud-browser/app.py
DELETED
|
@@ -1,317 +0,0 @@
|
|
| 1 |
-
import os
|
| 2 |
-
import io
|
| 3 |
-
import base64
|
| 4 |
-
import time
|
| 5 |
-
from flask import Flask, render_template, request, jsonify, send_file
|
| 6 |
-
from playwright.sync_api import sync_playwright, TimeoutError as PlaywrightTimeoutError
|
| 7 |
-
|
| 8 |
-
app = Flask(__name__)
|
| 9 |
-
|
| 10 |
-
# 浏览器实例管理
|
| 11 |
-
browser_instance = None
|
| 12 |
-
page_instance = None
|
| 13 |
-
playwright_instance = None
|
| 14 |
-
|
| 15 |
-
# 配置
|
| 16 |
-
VIEWPORT_WIDTH = int(os.environ.get('VIEWPORT_WIDTH', 1280))
|
| 17 |
-
VIEWPORT_HEIGHT = int(os.environ.get('VIEWPORT_HEIGHT', 800))
|
| 18 |
-
DEFAULT_URL = os.environ.get('DEFAULT_URL', 'https://www.google.com')
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
def get_browser():
|
| 22 |
-
"""获取或创建浏览器实例"""
|
| 23 |
-
global browser_instance, page_instance, playwright_instance
|
| 24 |
-
|
| 25 |
-
if browser_instance is None:
|
| 26 |
-
playwright_instance = sync_playwright().start()
|
| 27 |
-
browser_instance = playwright_instance.chromium.launch(
|
| 28 |
-
headless=True,
|
| 29 |
-
args=[
|
| 30 |
-
'--no-sandbox',
|
| 31 |
-
'--disable-dev-shm-usage',
|
| 32 |
-
'--disable-gpu',
|
| 33 |
-
'--disable-extensions',
|
| 34 |
-
'--disable-web-security',
|
| 35 |
-
'--disable-features=IsolateOrigins,site-per-process',
|
| 36 |
-
]
|
| 37 |
-
)
|
| 38 |
-
context = browser_instance.new_context(
|
| 39 |
-
viewport={'width': VIEWPORT_WIDTH, 'height': VIEWPORT_HEIGHT},
|
| 40 |
-
user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
| 41 |
-
)
|
| 42 |
-
page_instance = context.new_page()
|
| 43 |
-
page_instance.goto(DEFAULT_URL, timeout=30000, wait_until='domcontentloaded')
|
| 44 |
-
|
| 45 |
-
return page_instance
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
def take_screenshot():
|
| 49 |
-
"""截取当前页面截图"""
|
| 50 |
-
page = get_browser()
|
| 51 |
-
try:
|
| 52 |
-
screenshot = page.screenshot(
|
| 53 |
-
type='jpeg',
|
| 54 |
-
quality=80,
|
| 55 |
-
full_page=False
|
| 56 |
-
)
|
| 57 |
-
return base64.b64encode(screenshot).decode('utf-8')
|
| 58 |
-
except Exception as e:
|
| 59 |
-
print(f"Screenshot error: {e}")
|
| 60 |
-
return None
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
@app.route('/')
|
| 64 |
-
def index():
|
| 65 |
-
"""主页"""
|
| 66 |
-
return render_template('index.html',
|
| 67 |
-
viewport_width=VIEWPORT_WIDTH,
|
| 68 |
-
viewport_height=VIEWPORT_HEIGHT)
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
@app.route('/api/navigate', methods=['POST'])
|
| 72 |
-
def navigate():
|
| 73 |
-
"""导航到指定网址"""
|
| 74 |
-
data = request.get_json()
|
| 75 |
-
url = data.get('url', '')
|
| 76 |
-
|
| 77 |
-
if not url:
|
| 78 |
-
return jsonify({'success': False, 'error': '请输入网址'})
|
| 79 |
-
|
| 80 |
-
# 自动添加 https://
|
| 81 |
-
if not url.startswith('http://') and not url.startswith('https://'):
|
| 82 |
-
url = 'https://' + url
|
| 83 |
-
|
| 84 |
-
try:
|
| 85 |
-
page = get_browser()
|
| 86 |
-
page.goto(url, timeout=30000, wait_until='domcontentloaded')
|
| 87 |
-
time.sleep(0.5) # 等待页面渲染
|
| 88 |
-
|
| 89 |
-
screenshot = take_screenshot()
|
| 90 |
-
current_url = page.url
|
| 91 |
-
title = page.title()
|
| 92 |
-
|
| 93 |
-
return jsonify({
|
| 94 |
-
'success': True,
|
| 95 |
-
'screenshot': screenshot,
|
| 96 |
-
'url': current_url,
|
| 97 |
-
'title': title
|
| 98 |
-
})
|
| 99 |
-
except PlaywrightTimeoutError:
|
| 100 |
-
return jsonify({'success': False, 'error': '页面加载超时'})
|
| 101 |
-
except Exception as e:
|
| 102 |
-
return jsonify({'success': False, 'error': str(e)})
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
@app.route('/api/screenshot')
|
| 106 |
-
def screenshot():
|
| 107 |
-
"""获取当前页面截图"""
|
| 108 |
-
try:
|
| 109 |
-
page = get_browser()
|
| 110 |
-
screenshot = take_screenshot()
|
| 111 |
-
current_url = page.url
|
| 112 |
-
title = page.title()
|
| 113 |
-
|
| 114 |
-
return jsonify({
|
| 115 |
-
'success': True,
|
| 116 |
-
'screenshot': screenshot,
|
| 117 |
-
'url': current_url,
|
| 118 |
-
'title': title
|
| 119 |
-
})
|
| 120 |
-
except Exception as e:
|
| 121 |
-
return jsonify({'success': False, 'error': str(e)})
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
@app.route('/api/click', methods=['POST'])
|
| 125 |
-
def click():
|
| 126 |
-
"""处理点击事件"""
|
| 127 |
-
data = request.get_json()
|
| 128 |
-
x = data.get('x', 0)
|
| 129 |
-
y = data.get('y', 0)
|
| 130 |
-
|
| 131 |
-
try:
|
| 132 |
-
page = get_browser()
|
| 133 |
-
# 点击指定坐标
|
| 134 |
-
page.mouse.click(x, y)
|
| 135 |
-
time.sleep(0.3) # 等待页面响应
|
| 136 |
-
|
| 137 |
-
screenshot = take_screenshot()
|
| 138 |
-
current_url = page.url
|
| 139 |
-
title = page.title()
|
| 140 |
-
|
| 141 |
-
return jsonify({
|
| 142 |
-
'success': True,
|
| 143 |
-
'screenshot': screenshot,
|
| 144 |
-
'url': current_url,
|
| 145 |
-
'title': title
|
| 146 |
-
})
|
| 147 |
-
except Exception as e:
|
| 148 |
-
return jsonify({'success': False, 'error': str(e)})
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
@app.route('/api/type', methods=['POST'])
|
| 152 |
-
def type_text():
|
| 153 |
-
"""处理键盘输入"""
|
| 154 |
-
data = request.get_json()
|
| 155 |
-
text = data.get('text', '')
|
| 156 |
-
|
| 157 |
-
try:
|
| 158 |
-
page = get_browser()
|
| 159 |
-
page.keyboard.type(text)
|
| 160 |
-
time.sleep(0.2)
|
| 161 |
-
|
| 162 |
-
screenshot = take_screenshot()
|
| 163 |
-
return jsonify({
|
| 164 |
-
'success': True,
|
| 165 |
-
'screenshot': screenshot
|
| 166 |
-
})
|
| 167 |
-
except Exception as e:
|
| 168 |
-
return jsonify({'success': False, 'error': str(e)})
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
@app.route('/api/keypress', methods=['POST'])
|
| 172 |
-
def keypress():
|
| 173 |
-
"""处理特殊按键"""
|
| 174 |
-
data = request.get_json()
|
| 175 |
-
key = data.get('key', '')
|
| 176 |
-
|
| 177 |
-
try:
|
| 178 |
-
page = get_browser()
|
| 179 |
-
|
| 180 |
-
# 处理特殊按键
|
| 181 |
-
key_map = {
|
| 182 |
-
'Enter': 'Enter',
|
| 183 |
-
'Backspace': 'Backspace',
|
| 184 |
-
'Tab': 'Tab',
|
| 185 |
-
'Escape': 'Escape',
|
| 186 |
-
'ArrowUp': 'ArrowUp',
|
| 187 |
-
'ArrowDown': 'ArrowDown',
|
| 188 |
-
'ArrowLeft': 'ArrowLeft',
|
| 189 |
-
'ArrowRight': 'ArrowRight',
|
| 190 |
-
' ': 'Space',
|
| 191 |
-
}
|
| 192 |
-
|
| 193 |
-
if key in key_map:
|
| 194 |
-
page.keyboard.press(key_map[key])
|
| 195 |
-
elif len(key) == 1:
|
| 196 |
-
page.keyboard.press(key)
|
| 197 |
-
|
| 198 |
-
time.sleep(0.2)
|
| 199 |
-
screenshot = take_screenshot()
|
| 200 |
-
|
| 201 |
-
return jsonify({
|
| 202 |
-
'success': True,
|
| 203 |
-
'screenshot': screenshot
|
| 204 |
-
})
|
| 205 |
-
except Exception as e:
|
| 206 |
-
return jsonify({'success': False, 'error': str(e)})
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
@app.route('/api/scroll', methods=['POST'])
|
| 210 |
-
def scroll():
|
| 211 |
-
"""处理滚动"""
|
| 212 |
-
data = request.get_json()
|
| 213 |
-
delta_x = data.get('deltaX', 0)
|
| 214 |
-
delta_y = data.get('deltaY', 0)
|
| 215 |
-
|
| 216 |
-
try:
|
| 217 |
-
page = get_browser()
|
| 218 |
-
page.mouse.wheel(delta_x, delta_y)
|
| 219 |
-
time.sleep(0.2)
|
| 220 |
-
|
| 221 |
-
screenshot = take_screenshot()
|
| 222 |
-
return jsonify({
|
| 223 |
-
'success': True,
|
| 224 |
-
'screenshot': screenshot
|
| 225 |
-
})
|
| 226 |
-
except Exception as e:
|
| 227 |
-
return jsonify({'success': False, 'error': str(e)})
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
@app.route('/api/go-back', methods=['POST'])
|
| 231 |
-
def go_back():
|
| 232 |
-
"""后退"""
|
| 233 |
-
try:
|
| 234 |
-
page = get_browser()
|
| 235 |
-
page.go_back()
|
| 236 |
-
time.sleep(0.5)
|
| 237 |
-
|
| 238 |
-
screenshot = take_screenshot()
|
| 239 |
-
current_url = page.url
|
| 240 |
-
title = page.title()
|
| 241 |
-
|
| 242 |
-
return jsonify({
|
| 243 |
-
'success': True,
|
| 244 |
-
'screenshot': screenshot,
|
| 245 |
-
'url': current_url,
|
| 246 |
-
'title': title
|
| 247 |
-
})
|
| 248 |
-
except Exception as e:
|
| 249 |
-
return jsonify({'success': False, 'error': str(e)})
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
@app.route('/api/go-forward', methods=['POST'])
|
| 253 |
-
def go_forward():
|
| 254 |
-
"""前进"""
|
| 255 |
-
try:
|
| 256 |
-
page = get_browser()
|
| 257 |
-
page.go_forward()
|
| 258 |
-
time.sleep(0.5)
|
| 259 |
-
|
| 260 |
-
screenshot = take_screenshot()
|
| 261 |
-
current_url = page.url
|
| 262 |
-
title = page.title()
|
| 263 |
-
|
| 264 |
-
return jsonify({
|
| 265 |
-
'success': True,
|
| 266 |
-
'screenshot': screenshot,
|
| 267 |
-
'url': current_url,
|
| 268 |
-
'title': title
|
| 269 |
-
})
|
| 270 |
-
except Exception as e:
|
| 271 |
-
return jsonify({'success': False, 'error': str(e)})
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
@app.route('/api/reload', methods=['POST'])
|
| 275 |
-
def reload():
|
| 276 |
-
"""刷新"""
|
| 277 |
-
try:
|
| 278 |
-
page = get_browser()
|
| 279 |
-
page.reload()
|
| 280 |
-
time.sleep(0.5)
|
| 281 |
-
|
| 282 |
-
screenshot = take_screenshot()
|
| 283 |
-
current_url = page.url
|
| 284 |
-
title = page.title()
|
| 285 |
-
|
| 286 |
-
return jsonify({
|
| 287 |
-
'success': True,
|
| 288 |
-
'screenshot': screenshot,
|
| 289 |
-
'url': current_url,
|
| 290 |
-
'title': title
|
| 291 |
-
})
|
| 292 |
-
except Exception as e:
|
| 293 |
-
return jsonify({'success': False, 'error': str(e)})
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
@app.route('/api/status')
|
| 297 |
-
def status():
|
| 298 |
-
"""获取浏览器状态"""
|
| 299 |
-
try:
|
| 300 |
-
page = get_browser()
|
| 301 |
-
return jsonify({
|
| 302 |
-
'success': True,
|
| 303 |
-
'ready': True,
|
| 304 |
-
'url': page.url,
|
| 305 |
-
'title': page.title()
|
| 306 |
-
})
|
| 307 |
-
except Exception as e:
|
| 308 |
-
return jsonify({
|
| 309 |
-
'success': True,
|
| 310 |
-
'ready': False,
|
| 311 |
-
'error': str(e)
|
| 312 |
-
})
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
if __name__ == '__main__':
|
| 316 |
-
port = int(os.environ.get('PORT', 7860))
|
| 317 |
-
app.run(host='0.0.0.0', port=port, debug=False, threaded=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
cloud-browser/requirements.txt
DELETED
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
Flask==3.0.0
|
| 2 |
-
playwright==1.40.0
|
| 3 |
-
Werkzeug==3.0.1
|
|
|
|
|
|
|
|
|
|
|
|
cloud-browser/static/css/style.css
DELETED
|
@@ -1,427 +0,0 @@
|
|
| 1 |
-
* {
|
| 2 |
-
margin: 0;
|
| 3 |
-
padding: 0;
|
| 4 |
-
box-sizing: border-box;
|
| 5 |
-
}
|
| 6 |
-
|
| 7 |
-
body {
|
| 8 |
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
| 9 |
-
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
|
| 10 |
-
min-height: 100vh;
|
| 11 |
-
color: #fff;
|
| 12 |
-
}
|
| 13 |
-
|
| 14 |
-
.container {
|
| 15 |
-
max-width: 1400px;
|
| 16 |
-
margin: 0 auto;
|
| 17 |
-
padding: 20px;
|
| 18 |
-
}
|
| 19 |
-
|
| 20 |
-
/* Header */
|
| 21 |
-
.header {
|
| 22 |
-
background: rgba(255, 255, 255, 0.05);
|
| 23 |
-
backdrop-filter: blur(10px);
|
| 24 |
-
border-radius: 16px;
|
| 25 |
-
padding: 20px 24px;
|
| 26 |
-
display: flex;
|
| 27 |
-
justify-content: space-between;
|
| 28 |
-
align-items: center;
|
| 29 |
-
border: 1px solid rgba(255, 255, 255, 0.1);
|
| 30 |
-
margin-bottom: 16px;
|
| 31 |
-
}
|
| 32 |
-
|
| 33 |
-
.header-left {
|
| 34 |
-
display: flex;
|
| 35 |
-
align-items: center;
|
| 36 |
-
gap: 12px;
|
| 37 |
-
}
|
| 38 |
-
|
| 39 |
-
.header-icon {
|
| 40 |
-
font-size: 32px;
|
| 41 |
-
background: linear-gradient(135deg, #00d4ff 0%, #7b2ff7 100%);
|
| 42 |
-
-webkit-background-clip: text;
|
| 43 |
-
-webkit-text-fill-color: transparent;
|
| 44 |
-
background-clip: text;
|
| 45 |
-
}
|
| 46 |
-
|
| 47 |
-
.header-title {
|
| 48 |
-
font-size: 24px;
|
| 49 |
-
font-weight: 700;
|
| 50 |
-
background: linear-gradient(135deg, #00d4ff 0%, #7b2ff7 100%);
|
| 51 |
-
-webkit-background-clip: text;
|
| 52 |
-
-webkit-text-fill-color: transparent;
|
| 53 |
-
background-clip: text;
|
| 54 |
-
}
|
| 55 |
-
|
| 56 |
-
.badge {
|
| 57 |
-
background: linear-gradient(135deg, #00d4ff 0%, #7b2ff7 100%);
|
| 58 |
-
color: white;
|
| 59 |
-
padding: 6px 12px;
|
| 60 |
-
border-radius: 20px;
|
| 61 |
-
font-size: 12px;
|
| 62 |
-
display: flex;
|
| 63 |
-
align-items: center;
|
| 64 |
-
gap: 6px;
|
| 65 |
-
font-weight: 500;
|
| 66 |
-
}
|
| 67 |
-
|
| 68 |
-
/* Status Indicator */
|
| 69 |
-
.status-indicator {
|
| 70 |
-
display: flex;
|
| 71 |
-
align-items: center;
|
| 72 |
-
gap: 8px;
|
| 73 |
-
padding: 8px 16px;
|
| 74 |
-
background: rgba(255, 255, 255, 0.05);
|
| 75 |
-
border-radius: 20px;
|
| 76 |
-
font-size: 14px;
|
| 77 |
-
}
|
| 78 |
-
|
| 79 |
-
.status-dot {
|
| 80 |
-
width: 8px;
|
| 81 |
-
height: 8px;
|
| 82 |
-
border-radius: 50%;
|
| 83 |
-
background: #ffc107;
|
| 84 |
-
animation: pulse 2s infinite;
|
| 85 |
-
}
|
| 86 |
-
|
| 87 |
-
.status-dot.connected {
|
| 88 |
-
background: #4caf50;
|
| 89 |
-
}
|
| 90 |
-
|
| 91 |
-
.status-dot.error {
|
| 92 |
-
background: #f44336;
|
| 93 |
-
}
|
| 94 |
-
|
| 95 |
-
@keyframes pulse {
|
| 96 |
-
0%, 100% { opacity: 1; }
|
| 97 |
-
50% { opacity: 0.5; }
|
| 98 |
-
}
|
| 99 |
-
|
| 100 |
-
/* Browser Toolbar */
|
| 101 |
-
.browser-toolbar {
|
| 102 |
-
background: rgba(255, 255, 255, 0.05);
|
| 103 |
-
backdrop-filter: blur(10px);
|
| 104 |
-
border-radius: 12px;
|
| 105 |
-
padding: 12px 16px;
|
| 106 |
-
display: flex;
|
| 107 |
-
align-items: center;
|
| 108 |
-
gap: 12px;
|
| 109 |
-
border: 1px solid rgba(255, 255, 255, 0.1);
|
| 110 |
-
margin-bottom: 16px;
|
| 111 |
-
}
|
| 112 |
-
|
| 113 |
-
.nav-buttons {
|
| 114 |
-
display: flex;
|
| 115 |
-
gap: 8px;
|
| 116 |
-
}
|
| 117 |
-
|
| 118 |
-
.nav-btn {
|
| 119 |
-
width: 36px;
|
| 120 |
-
height: 36px;
|
| 121 |
-
border: none;
|
| 122 |
-
border-radius: 8px;
|
| 123 |
-
background: rgba(255, 255, 255, 0.1);
|
| 124 |
-
color: #fff;
|
| 125 |
-
cursor: pointer;
|
| 126 |
-
transition: all 0.3s ease;
|
| 127 |
-
display: flex;
|
| 128 |
-
align-items: center;
|
| 129 |
-
justify-content: center;
|
| 130 |
-
}
|
| 131 |
-
|
| 132 |
-
.nav-btn:hover {
|
| 133 |
-
background: rgba(255, 255, 255, 0.2);
|
| 134 |
-
transform: scale(1.05);
|
| 135 |
-
}
|
| 136 |
-
|
| 137 |
-
.nav-btn:active {
|
| 138 |
-
transform: scale(0.95);
|
| 139 |
-
}
|
| 140 |
-
|
| 141 |
-
/* URL Bar */
|
| 142 |
-
.url-bar {
|
| 143 |
-
flex: 1;
|
| 144 |
-
display: flex;
|
| 145 |
-
align-items: center;
|
| 146 |
-
background: rgba(255, 255, 255, 0.1);
|
| 147 |
-
border-radius: 10px;
|
| 148 |
-
padding: 4px 4px 4px 12px;
|
| 149 |
-
border: 1px solid rgba(255, 255, 255, 0.1);
|
| 150 |
-
transition: all 0.3s ease;
|
| 151 |
-
}
|
| 152 |
-
|
| 153 |
-
.url-bar:focus-within {
|
| 154 |
-
border-color: rgba(0, 212, 255, 0.5);
|
| 155 |
-
box-shadow: 0 0 0 3px rgba(0, 212, 255, 0.1);
|
| 156 |
-
}
|
| 157 |
-
|
| 158 |
-
.url-icon {
|
| 159 |
-
color: #4caf50;
|
| 160 |
-
margin-right: 10px;
|
| 161 |
-
font-size: 14px;
|
| 162 |
-
}
|
| 163 |
-
|
| 164 |
-
.url-bar input {
|
| 165 |
-
flex: 1;
|
| 166 |
-
background: transparent;
|
| 167 |
-
border: none;
|
| 168 |
-
color: #fff;
|
| 169 |
-
font-size: 14px;
|
| 170 |
-
outline: none;
|
| 171 |
-
padding: 8px 0;
|
| 172 |
-
}
|
| 173 |
-
|
| 174 |
-
.url-bar input::placeholder {
|
| 175 |
-
color: rgba(255, 255, 255, 0.4);
|
| 176 |
-
}
|
| 177 |
-
|
| 178 |
-
.go-btn {
|
| 179 |
-
width: 36px;
|
| 180 |
-
height: 36px;
|
| 181 |
-
border: none;
|
| 182 |
-
border-radius: 8px;
|
| 183 |
-
background: linear-gradient(135deg, #00d4ff 0%, #7b2ff7 100%);
|
| 184 |
-
color: white;
|
| 185 |
-
cursor: pointer;
|
| 186 |
-
transition: all 0.3s ease;
|
| 187 |
-
display: flex;
|
| 188 |
-
align-items: center;
|
| 189 |
-
justify-content: center;
|
| 190 |
-
}
|
| 191 |
-
|
| 192 |
-
.go-btn:hover {
|
| 193 |
-
transform: scale(1.05);
|
| 194 |
-
box-shadow: 0 4px 15px rgba(0, 212, 255, 0.3);
|
| 195 |
-
}
|
| 196 |
-
|
| 197 |
-
/* Browser Viewport */
|
| 198 |
-
.browser-viewport {
|
| 199 |
-
position: relative;
|
| 200 |
-
background: #000;
|
| 201 |
-
border-radius: 12px;
|
| 202 |
-
overflow: hidden;
|
| 203 |
-
border: 2px solid rgba(255, 255, 255, 0.1);
|
| 204 |
-
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
|
| 205 |
-
aspect-ratio: 1280 / 800;
|
| 206 |
-
max-height: 70vh;
|
| 207 |
-
cursor: crosshair;
|
| 208 |
-
}
|
| 209 |
-
|
| 210 |
-
.screenshot-container {
|
| 211 |
-
width: 100%;
|
| 212 |
-
height: 100%;
|
| 213 |
-
display: flex;
|
| 214 |
-
align-items: center;
|
| 215 |
-
justify-content: center;
|
| 216 |
-
}
|
| 217 |
-
|
| 218 |
-
.screenshot-img {
|
| 219 |
-
width: 100%;
|
| 220 |
-
height: 100%;
|
| 221 |
-
object-fit: contain;
|
| 222 |
-
display: block;
|
| 223 |
-
}
|
| 224 |
-
|
| 225 |
-
/* Loading Overlay */
|
| 226 |
-
.loading-overlay {
|
| 227 |
-
position: absolute;
|
| 228 |
-
top: 0;
|
| 229 |
-
left: 0;
|
| 230 |
-
width: 100%;
|
| 231 |
-
height: 100%;
|
| 232 |
-
background: rgba(0, 0, 0, 0.8);
|
| 233 |
-
display: flex;
|
| 234 |
-
align-items: center;
|
| 235 |
-
justify-content: center;
|
| 236 |
-
z-index: 10;
|
| 237 |
-
transition: opacity 0.3s ease;
|
| 238 |
-
}
|
| 239 |
-
|
| 240 |
-
.loading-overlay.hidden {
|
| 241 |
-
opacity: 0;
|
| 242 |
-
pointer-events: none;
|
| 243 |
-
}
|
| 244 |
-
|
| 245 |
-
.loading-content {
|
| 246 |
-
text-align: center;
|
| 247 |
-
}
|
| 248 |
-
|
| 249 |
-
.loading-icon {
|
| 250 |
-
font-size: 48px;
|
| 251 |
-
color: #00d4ff;
|
| 252 |
-
margin-bottom: 16px;
|
| 253 |
-
}
|
| 254 |
-
|
| 255 |
-
.loading-text {
|
| 256 |
-
font-size: 16px;
|
| 257 |
-
color: rgba(255, 255, 255, 0.8);
|
| 258 |
-
}
|
| 259 |
-
|
| 260 |
-
/* Keyboard Capture (hidden) */
|
| 261 |
-
.keyboard-capture {
|
| 262 |
-
position: absolute;
|
| 263 |
-
opacity: 0;
|
| 264 |
-
pointer-events: none;
|
| 265 |
-
}
|
| 266 |
-
|
| 267 |
-
/* Info Bar */
|
| 268 |
-
.info-bar {
|
| 269 |
-
background: rgba(255, 255, 255, 0.05);
|
| 270 |
-
backdrop-filter: blur(10px);
|
| 271 |
-
border-radius: 12px;
|
| 272 |
-
padding: 12px 20px;
|
| 273 |
-
display: flex;
|
| 274 |
-
gap: 24px;
|
| 275 |
-
border: 1px solid rgba(255, 255, 255, 0.1);
|
| 276 |
-
margin-top: 16px;
|
| 277 |
-
flex-wrap: wrap;
|
| 278 |
-
}
|
| 279 |
-
|
| 280 |
-
.info-item {
|
| 281 |
-
display: flex;
|
| 282 |
-
align-items: center;
|
| 283 |
-
gap: 8px;
|
| 284 |
-
font-size: 13px;
|
| 285 |
-
color: rgba(255, 255, 255, 0.7);
|
| 286 |
-
}
|
| 287 |
-
|
| 288 |
-
.info-item i {
|
| 289 |
-
color: #00d4ff;
|
| 290 |
-
}
|
| 291 |
-
|
| 292 |
-
.info-item span {
|
| 293 |
-
max-width: 300px;
|
| 294 |
-
overflow: hidden;
|
| 295 |
-
text-overflow: ellipsis;
|
| 296 |
-
white-space: nowrap;
|
| 297 |
-
}
|
| 298 |
-
|
| 299 |
-
/* Tips Section */
|
| 300 |
-
.tips-section {
|
| 301 |
-
margin-top: 24px;
|
| 302 |
-
}
|
| 303 |
-
|
| 304 |
-
.tips-section h3 {
|
| 305 |
-
font-size: 18px;
|
| 306 |
-
margin-bottom: 16px;
|
| 307 |
-
display: flex;
|
| 308 |
-
align-items: center;
|
| 309 |
-
gap: 10px;
|
| 310 |
-
color: rgba(255, 255, 255, 0.9);
|
| 311 |
-
}
|
| 312 |
-
|
| 313 |
-
.tips-section h3 i {
|
| 314 |
-
color: #ffc107;
|
| 315 |
-
}
|
| 316 |
-
|
| 317 |
-
.tips-grid {
|
| 318 |
-
display: grid;
|
| 319 |
-
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
| 320 |
-
gap: 16px;
|
| 321 |
-
}
|
| 322 |
-
|
| 323 |
-
.tip-card {
|
| 324 |
-
background: rgba(255, 255, 255, 0.05);
|
| 325 |
-
backdrop-filter: blur(10px);
|
| 326 |
-
border-radius: 12px;
|
| 327 |
-
padding: 20px;
|
| 328 |
-
border: 1px solid rgba(255, 255, 255, 0.1);
|
| 329 |
-
transition: all 0.3s ease;
|
| 330 |
-
}
|
| 331 |
-
|
| 332 |
-
.tip-card:hover {
|
| 333 |
-
background: rgba(255, 255, 255, 0.08);
|
| 334 |
-
transform: translateY(-2px);
|
| 335 |
-
border-color: rgba(0, 212, 255, 0.3);
|
| 336 |
-
}
|
| 337 |
-
|
| 338 |
-
.tip-icon {
|
| 339 |
-
font-size: 28px;
|
| 340 |
-
margin-bottom: 12px;
|
| 341 |
-
background: linear-gradient(135deg, #00d4ff 0%, #7b2ff7 100%);
|
| 342 |
-
-webkit-background-clip: text;
|
| 343 |
-
-webkit-text-fill-color: transparent;
|
| 344 |
-
background-clip: text;
|
| 345 |
-
}
|
| 346 |
-
|
| 347 |
-
.tip-card h4 {
|
| 348 |
-
font-size: 16px;
|
| 349 |
-
margin-bottom: 8px;
|
| 350 |
-
color: #fff;
|
| 351 |
-
}
|
| 352 |
-
|
| 353 |
-
.tip-card p {
|
| 354 |
-
font-size: 13px;
|
| 355 |
-
color: rgba(255, 255, 255, 0.6);
|
| 356 |
-
line-height: 1.5;
|
| 357 |
-
}
|
| 358 |
-
|
| 359 |
-
/* Toast */
|
| 360 |
-
.toast {
|
| 361 |
-
position: fixed;
|
| 362 |
-
top: 20px;
|
| 363 |
-
right: 20px;
|
| 364 |
-
z-index: 2000;
|
| 365 |
-
transform: translateX(120%);
|
| 366 |
-
transition: transform 0.3s ease;
|
| 367 |
-
}
|
| 368 |
-
|
| 369 |
-
.toast.show {
|
| 370 |
-
transform: translateX(0);
|
| 371 |
-
}
|
| 372 |
-
|
| 373 |
-
.toast-content {
|
| 374 |
-
background: rgba(0, 0, 0, 0.9);
|
| 375 |
-
backdrop-filter: blur(10px);
|
| 376 |
-
padding: 14px 20px;
|
| 377 |
-
border-radius: 12px;
|
| 378 |
-
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.4);
|
| 379 |
-
display: flex;
|
| 380 |
-
align-items: center;
|
| 381 |
-
gap: 10px;
|
| 382 |
-
font-size: 14px;
|
| 383 |
-
font-weight: 500;
|
| 384 |
-
border: 1px solid rgba(255, 255, 255, 0.1);
|
| 385 |
-
}
|
| 386 |
-
|
| 387 |
-
.toast-icon.success {
|
| 388 |
-
color: #4caf50;
|
| 389 |
-
}
|
| 390 |
-
|
| 391 |
-
.toast-icon.error {
|
| 392 |
-
color: #f44336;
|
| 393 |
-
}
|
| 394 |
-
|
| 395 |
-
.toast-icon.info {
|
| 396 |
-
color: #00d4ff;
|
| 397 |
-
}
|
| 398 |
-
|
| 399 |
-
/* Responsive */
|
| 400 |
-
@media (max-width: 768px) {
|
| 401 |
-
.container {
|
| 402 |
-
padding: 10px;
|
| 403 |
-
}
|
| 404 |
-
|
| 405 |
-
.header {
|
| 406 |
-
flex-direction: column;
|
| 407 |
-
gap: 12px;
|
| 408 |
-
padding: 16px;
|
| 409 |
-
}
|
| 410 |
-
|
| 411 |
-
.browser-toolbar {
|
| 412 |
-
flex-wrap: wrap;
|
| 413 |
-
}
|
| 414 |
-
|
| 415 |
-
.info-bar {
|
| 416 |
-
flex-direction: column;
|
| 417 |
-
gap: 8px;
|
| 418 |
-
}
|
| 419 |
-
|
| 420 |
-
.info-item span {
|
| 421 |
-
max-width: 100%;
|
| 422 |
-
}
|
| 423 |
-
|
| 424 |
-
.tips-grid {
|
| 425 |
-
grid-template-columns: 1fr;
|
| 426 |
-
}
|
| 427 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
cloud-browser/static/js/app.js
DELETED
|
@@ -1,438 +0,0 @@
|
|
| 1 |
-
// DOM Elements
|
| 2 |
-
const screenshotImg = document.getElementById('screenshotImg');
|
| 3 |
-
const browserViewport = document.getElementById('browserViewport');
|
| 4 |
-
const loadingOverlay = document.getElementById('loadingOverlay');
|
| 5 |
-
const loadingText = document.getElementById('loadingText');
|
| 6 |
-
const urlInput = document.getElementById('urlInput');
|
| 7 |
-
const goBtn = document.getElementById('goBtn');
|
| 8 |
-
const backBtn = document.getElementById('backBtn');
|
| 9 |
-
const forwardBtn = document.getElementById('forwardBtn');
|
| 10 |
-
const reloadBtn = document.getElementById('reloadBtn');
|
| 11 |
-
const keyboardCapture = document.getElementById('keyboardCapture');
|
| 12 |
-
const statusDot = document.querySelector('.status-dot');
|
| 13 |
-
const statusText = document.getElementById('statusText');
|
| 14 |
-
const currentUrlEl = document.getElementById('currentUrl');
|
| 15 |
-
const pageTitleEl = document.getElementById('pageTitle');
|
| 16 |
-
const latencyEl = document.getElementById('latency');
|
| 17 |
-
|
| 18 |
-
// Toast
|
| 19 |
-
const toast = document.getElementById('toast');
|
| 20 |
-
const toastIcon = document.getElementById('toastIcon');
|
| 21 |
-
const toastMessage = document.getElementById('toastMessage');
|
| 22 |
-
|
| 23 |
-
// State
|
| 24 |
-
let isLoading = false;
|
| 25 |
-
let lastRequestTime = 0;
|
| 26 |
-
let isBrowserReady = false;
|
| 27 |
-
let autoRefreshInterval = null;
|
| 28 |
-
|
| 29 |
-
// Toast function
|
| 30 |
-
function showToast(message, type = 'success') {
|
| 31 |
-
toastMessage.textContent = message;
|
| 32 |
-
toastIcon.className = 'toast-icon fas ';
|
| 33 |
-
|
| 34 |
-
if (type === 'success') {
|
| 35 |
-
toastIcon.classList.add('fa-check-circle', 'success');
|
| 36 |
-
} else if (type === 'error') {
|
| 37 |
-
toastIcon.classList.add('fa-exclamation-circle', 'error');
|
| 38 |
-
} else {
|
| 39 |
-
toastIcon.classList.add('fa-info-circle', 'info');
|
| 40 |
-
}
|
| 41 |
-
|
| 42 |
-
toast.classList.add('show');
|
| 43 |
-
|
| 44 |
-
setTimeout(() => {
|
| 45 |
-
toast.classList.remove('show');
|
| 46 |
-
}, 3000);
|
| 47 |
-
}
|
| 48 |
-
|
| 49 |
-
// Update status
|
| 50 |
-
function updateStatus(status, text) {
|
| 51 |
-
statusDot.className = 'status-dot';
|
| 52 |
-
if (status === 'connected') {
|
| 53 |
-
statusDot.classList.add('connected');
|
| 54 |
-
} else if (status === 'error') {
|
| 55 |
-
statusDot.classList.add('error');
|
| 56 |
-
}
|
| 57 |
-
statusText.textContent = text;
|
| 58 |
-
}
|
| 59 |
-
|
| 60 |
-
// Show loading
|
| 61 |
-
function showLoading(text = '加载中...') {
|
| 62 |
-
loadingText.textContent = text;
|
| 63 |
-
loadingOverlay.classList.remove('hidden');
|
| 64 |
-
isLoading = true;
|
| 65 |
-
}
|
| 66 |
-
|
| 67 |
-
// Hide loading
|
| 68 |
-
function hideLoading() {
|
| 69 |
-
loadingOverlay.classList.add('hidden');
|
| 70 |
-
isLoading = false;
|
| 71 |
-
}
|
| 72 |
-
|
| 73 |
-
// Update screenshot
|
| 74 |
-
function updateScreenshot(data) {
|
| 75 |
-
if (data.screenshot) {
|
| 76 |
-
screenshotImg.src = 'data:image/jpeg;base64,' + data.screenshot;
|
| 77 |
-
}
|
| 78 |
-
if (data.url) {
|
| 79 |
-
currentUrlEl.textContent = data.url;
|
| 80 |
-
urlInput.value = data.url;
|
| 81 |
-
}
|
| 82 |
-
if (data.title) {
|
| 83 |
-
pageTitleEl.textContent = data.title;
|
| 84 |
-
}
|
| 85 |
-
|
| 86 |
-
// Calculate latency
|
| 87 |
-
if (lastRequestTime > 0) {
|
| 88 |
-
const latency = Date.now() - lastRequestTime;
|
| 89 |
-
latencyEl.textContent = latency + 'ms';
|
| 90 |
-
}
|
| 91 |
-
}
|
| 92 |
-
|
| 93 |
-
// Navigate to URL
|
| 94 |
-
async function navigateTo(url) {
|
| 95 |
-
if (isLoading) return;
|
| 96 |
-
|
| 97 |
-
showLoading('正在加载页面...');
|
| 98 |
-
lastRequestTime = Date.now();
|
| 99 |
-
|
| 100 |
-
try {
|
| 101 |
-
const response = await fetch('/api/navigate', {
|
| 102 |
-
method: 'POST',
|
| 103 |
-
headers: {
|
| 104 |
-
'Content-Type': 'application/json'
|
| 105 |
-
},
|
| 106 |
-
body: JSON.stringify({ url: url })
|
| 107 |
-
});
|
| 108 |
-
|
| 109 |
-
const data = await response.json();
|
| 110 |
-
|
| 111 |
-
if (data.success) {
|
| 112 |
-
updateScreenshot(data);
|
| 113 |
-
hideLoading();
|
| 114 |
-
showToast('页面加载成功', 'success');
|
| 115 |
-
} else {
|
| 116 |
-
hideLoading();
|
| 117 |
-
showToast(data.error || '加载失败', 'error');
|
| 118 |
-
}
|
| 119 |
-
} catch (error) {
|
| 120 |
-
hideLoading();
|
| 121 |
-
showToast('网络错误: ' + error.message, 'error');
|
| 122 |
-
console.error('Navigate error:', error);
|
| 123 |
-
}
|
| 124 |
-
}
|
| 125 |
-
|
| 126 |
-
// Get screenshot
|
| 127 |
-
async function refreshScreenshot() {
|
| 128 |
-
if (isLoading) return;
|
| 129 |
-
|
| 130 |
-
lastRequestTime = Date.now();
|
| 131 |
-
|
| 132 |
-
try {
|
| 133 |
-
const response = await fetch('/api/screenshot');
|
| 134 |
-
const data = await response.json();
|
| 135 |
-
|
| 136 |
-
if (data.success) {
|
| 137 |
-
updateScreenshot(data);
|
| 138 |
-
return true;
|
| 139 |
-
}
|
| 140 |
-
return false;
|
| 141 |
-
} catch (error) {
|
| 142 |
-
console.error('Screenshot error:', error);
|
| 143 |
-
return false;
|
| 144 |
-
}
|
| 145 |
-
}
|
| 146 |
-
|
| 147 |
-
// Handle click
|
| 148 |
-
async function handleClick(x, y) {
|
| 149 |
-
if (isLoading || !isBrowserReady) return;
|
| 150 |
-
|
| 151 |
-
lastRequestTime = Date.now();
|
| 152 |
-
|
| 153 |
-
try {
|
| 154 |
-
const response = await fetch('/api/click', {
|
| 155 |
-
method: 'POST',
|
| 156 |
-
headers: {
|
| 157 |
-
'Content-Type': 'application/json'
|
| 158 |
-
},
|
| 159 |
-
body: JSON.stringify({ x: x, y: y })
|
| 160 |
-
});
|
| 161 |
-
|
| 162 |
-
const data = await response.json();
|
| 163 |
-
|
| 164 |
-
if (data.success) {
|
| 165 |
-
updateScreenshot(data);
|
| 166 |
-
}
|
| 167 |
-
} catch (error) {
|
| 168 |
-
console.error('Click error:', error);
|
| 169 |
-
}
|
| 170 |
-
}
|
| 171 |
-
|
| 172 |
-
// Handle type
|
| 173 |
-
async function handleType(text) {
|
| 174 |
-
if (isLoading || !isBrowserReady) return;
|
| 175 |
-
|
| 176 |
-
lastRequestTime = Date.now();
|
| 177 |
-
|
| 178 |
-
try {
|
| 179 |
-
const response = await fetch('/api/type', {
|
| 180 |
-
method: 'POST',
|
| 181 |
-
headers: {
|
| 182 |
-
'Content-Type': 'application/json'
|
| 183 |
-
},
|
| 184 |
-
body: JSON.stringify({ text: text })
|
| 185 |
-
});
|
| 186 |
-
|
| 187 |
-
const data = await response.json();
|
| 188 |
-
|
| 189 |
-
if (data.success) {
|
| 190 |
-
updateScreenshot(data);
|
| 191 |
-
}
|
| 192 |
-
} catch (error) {
|
| 193 |
-
console.error('Type error:', error);
|
| 194 |
-
}
|
| 195 |
-
}
|
| 196 |
-
|
| 197 |
-
// Handle keypress
|
| 198 |
-
async function handleKeypress(key) {
|
| 199 |
-
if (isLoading || !isBrowserReady) return;
|
| 200 |
-
|
| 201 |
-
lastRequestTime = Date.now();
|
| 202 |
-
|
| 203 |
-
try {
|
| 204 |
-
const response = await fetch('/api/keypress', {
|
| 205 |
-
method: 'POST',
|
| 206 |
-
headers: {
|
| 207 |
-
'Content-Type': 'application/json'
|
| 208 |
-
},
|
| 209 |
-
body: JSON.stringify({ key: key })
|
| 210 |
-
});
|
| 211 |
-
|
| 212 |
-
const data = await response.json();
|
| 213 |
-
|
| 214 |
-
if (data.success) {
|
| 215 |
-
updateScreenshot(data);
|
| 216 |
-
}
|
| 217 |
-
} catch (error) {
|
| 218 |
-
console.error('Keypress error:', error);
|
| 219 |
-
}
|
| 220 |
-
}
|
| 221 |
-
|
| 222 |
-
// Handle scroll
|
| 223 |
-
async function handleScroll(deltaX, deltaY) {
|
| 224 |
-
if (isLoading || !isBrowserReady) return;
|
| 225 |
-
|
| 226 |
-
lastRequestTime = Date.now();
|
| 227 |
-
|
| 228 |
-
try {
|
| 229 |
-
const response = await fetch('/api/scroll', {
|
| 230 |
-
method: 'POST',
|
| 231 |
-
headers: {
|
| 232 |
-
'Content-Type': 'application/json'
|
| 233 |
-
},
|
| 234 |
-
body: JSON.stringify({ deltaX: deltaX, deltaY: deltaY })
|
| 235 |
-
});
|
| 236 |
-
|
| 237 |
-
const data = await response.json();
|
| 238 |
-
|
| 239 |
-
if (data.success) {
|
| 240 |
-
updateScreenshot(data);
|
| 241 |
-
}
|
| 242 |
-
} catch (error) {
|
| 243 |
-
console.error('Scroll error:', error);
|
| 244 |
-
}
|
| 245 |
-
}
|
| 246 |
-
|
| 247 |
-
// Navigation functions
|
| 248 |
-
async function goBack() {
|
| 249 |
-
if (isLoading) return;
|
| 250 |
-
|
| 251 |
-
showLoading('后退中...');
|
| 252 |
-
lastRequestTime = Date.now();
|
| 253 |
-
|
| 254 |
-
try {
|
| 255 |
-
const response = await fetch('/api/go-back', { method: 'POST' });
|
| 256 |
-
const data = await response.json();
|
| 257 |
-
|
| 258 |
-
if (data.success) {
|
| 259 |
-
updateScreenshot(data);
|
| 260 |
-
hideLoading();
|
| 261 |
-
} else {
|
| 262 |
-
hideLoading();
|
| 263 |
-
showToast(data.error || '操作失败', 'error');
|
| 264 |
-
}
|
| 265 |
-
} catch (error) {
|
| 266 |
-
hideLoading();
|
| 267 |
-
showToast('网络错误', 'error');
|
| 268 |
-
}
|
| 269 |
-
}
|
| 270 |
-
|
| 271 |
-
async function goForward() {
|
| 272 |
-
if (isLoading) return;
|
| 273 |
-
|
| 274 |
-
showLoading('前进中...');
|
| 275 |
-
lastRequestTime = Date.now();
|
| 276 |
-
|
| 277 |
-
try {
|
| 278 |
-
const response = await fetch('/api/go-forward', { method: 'POST' });
|
| 279 |
-
const data = await response.json();
|
| 280 |
-
|
| 281 |
-
if (data.success) {
|
| 282 |
-
updateScreenshot(data);
|
| 283 |
-
hideLoading();
|
| 284 |
-
} else {
|
| 285 |
-
hideLoading();
|
| 286 |
-
showToast(data.error || '操作失败', 'error');
|
| 287 |
-
}
|
| 288 |
-
} catch (error) {
|
| 289 |
-
hideLoading();
|
| 290 |
-
showToast('网络错误', 'error');
|
| 291 |
-
}
|
| 292 |
-
}
|
| 293 |
-
|
| 294 |
-
async function reload() {
|
| 295 |
-
if (isLoading) return;
|
| 296 |
-
|
| 297 |
-
showLoading('刷新中...');
|
| 298 |
-
lastRequestTime = Date.now();
|
| 299 |
-
|
| 300 |
-
try {
|
| 301 |
-
const response = await fetch('/api/reload', { method: 'POST' });
|
| 302 |
-
const data = await response.json();
|
| 303 |
-
|
| 304 |
-
if (data.success) {
|
| 305 |
-
updateScreenshot(data);
|
| 306 |
-
hideLoading();
|
| 307 |
-
} else {
|
| 308 |
-
hideLoading();
|
| 309 |
-
showToast(data.error || '刷新失败', 'error');
|
| 310 |
-
}
|
| 311 |
-
} catch (error) {
|
| 312 |
-
hideLoading();
|
| 313 |
-
showToast('网络错误', 'error');
|
| 314 |
-
}
|
| 315 |
-
}
|
| 316 |
-
|
| 317 |
-
// Calculate click coordinates
|
| 318 |
-
function getClickCoordinates(event) {
|
| 319 |
-
const rect = screenshotImg.getBoundingClientRect();
|
| 320 |
-
const x = event.clientX - rect.left;
|
| 321 |
-
const y = event.clientY - rect.top;
|
| 322 |
-
|
| 323 |
-
// Calculate scale
|
| 324 |
-
const scaleX = viewportWidth / rect.width;
|
| 325 |
-
const scaleY = viewportHeight / rect.height;
|
| 326 |
-
|
| 327 |
-
// Use the smaller scale to maintain aspect ratio
|
| 328 |
-
const scale = Math.min(scaleX, scaleY);
|
| 329 |
-
|
| 330 |
-
// Calculate actual coordinates in the viewport
|
| 331 |
-
const actualX = Math.round(x * scale);
|
| 332 |
-
const actualY = Math.round(y * scale);
|
| 333 |
-
|
| 334 |
-
return { x: actualX, y: actualY };
|
| 335 |
-
}
|
| 336 |
-
|
| 337 |
-
// Event Listeners
|
| 338 |
-
|
| 339 |
-
// URL input
|
| 340 |
-
goBtn.addEventListener('click', () => {
|
| 341 |
-
const url = urlInput.value.trim();
|
| 342 |
-
if (url) {
|
| 343 |
-
navigateTo(url);
|
| 344 |
-
}
|
| 345 |
-
});
|
| 346 |
-
|
| 347 |
-
urlInput.addEventListener('keypress', (e) => {
|
| 348 |
-
if (e.key === 'Enter') {
|
| 349 |
-
const url = urlInput.value.trim();
|
| 350 |
-
if (url) {
|
| 351 |
-
navigateTo(url);
|
| 352 |
-
}
|
| 353 |
-
}
|
| 354 |
-
});
|
| 355 |
-
|
| 356 |
-
// Navigation buttons
|
| 357 |
-
backBtn.addEventListener('click', goBack);
|
| 358 |
-
forwardBtn.addEventListener('click', goForward);
|
| 359 |
-
reloadBtn.addEventListener('click', reload);
|
| 360 |
-
|
| 361 |
-
// Viewport click
|
| 362 |
-
browserViewport.addEventListener('click', (e) => {
|
| 363 |
-
if (!isBrowserReady) return;
|
| 364 |
-
|
| 365 |
-
// Focus the hidden input to capture keyboard
|
| 366 |
-
keyboardCapture.focus();
|
| 367 |
-
|
| 368 |
-
const coords = getClickCoordinates(e);
|
| 369 |
-
handleClick(coords.x, coords.y);
|
| 370 |
-
});
|
| 371 |
-
|
| 372 |
-
// Keyboard input
|
| 373 |
-
keyboardCapture.addEventListener('input', (e) => {
|
| 374 |
-
const text = e.target.value;
|
| 375 |
-
if (text) {
|
| 376 |
-
handleType(text);
|
| 377 |
-
e.target.value = '';
|
| 378 |
-
}
|
| 379 |
-
});
|
| 380 |
-
|
| 381 |
-
keyboardCapture.addEventListener('keydown', (e) => {
|
| 382 |
-
// Handle special keys
|
| 383 |
-
const specialKeys = ['Enter', 'Backspace', 'Tab', 'Escape', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'];
|
| 384 |
-
|
| 385 |
-
if (specialKeys.includes(e.key)) {
|
| 386 |
-
e.preventDefault();
|
| 387 |
-
handleKeypress(e.key);
|
| 388 |
-
}
|
| 389 |
-
});
|
| 390 |
-
|
| 391 |
-
// Scroll
|
| 392 |
-
browserViewport.addEventListener('wheel', (e) => {
|
| 393 |
-
e.preventDefault();
|
| 394 |
-
handleScroll(e.deltaX, e.deltaY);
|
| 395 |
-
}, { passive: false });
|
| 396 |
-
|
| 397 |
-
// Initialize
|
| 398 |
-
async function init() {
|
| 399 |
-
updateStatus('connecting', '连接中...');
|
| 400 |
-
showLoading('正在启动云浏览器...');
|
| 401 |
-
|
| 402 |
-
// Check browser status
|
| 403 |
-
try {
|
| 404 |
-
const response = await fetch('/api/status');
|
| 405 |
-
const data = await response.json();
|
| 406 |
-
|
| 407 |
-
if (data.ready) {
|
| 408 |
-
isBrowserReady = true;
|
| 409 |
-
updateStatus('connected', '已连接');
|
| 410 |
-
currentUrlEl.textContent = data.url || '-';
|
| 411 |
-
pageTitleEl.textContent = data.title || '-';
|
| 412 |
-
|
| 413 |
-
// Get initial screenshot
|
| 414 |
-
await refreshScreenshot();
|
| 415 |
-
hideLoading();
|
| 416 |
-
} else {
|
| 417 |
-
// Try to initialize by getting screenshot
|
| 418 |
-
showLoading('正在初始化浏览器...');
|
| 419 |
-
const success = await refreshScreenshot();
|
| 420 |
-
|
| 421 |
-
if (success) {
|
| 422 |
-
isBrowserReady = true;
|
| 423 |
-
updateStatus('connected', '已连接');
|
| 424 |
-
hideLoading();
|
| 425 |
-
} else {
|
| 426 |
-
updateStatus('error', '连接失败');
|
| 427 |
-
loadingText.textContent = '浏览器启动失败,请刷新页面重试';
|
| 428 |
-
}
|
| 429 |
-
}
|
| 430 |
-
} catch (error) {
|
| 431 |
-
updateStatus('error', '连接失败');
|
| 432 |
-
loadingText.textContent = '无法连接到服务器';
|
| 433 |
-
console.error('Init error:', error);
|
| 434 |
-
}
|
| 435 |
-
}
|
| 436 |
-
|
| 437 |
-
// Start
|
| 438 |
-
document.addEventListener('DOMContentLoaded', init);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
cloud-browser/templates/index.html
DELETED
|
@@ -1,128 +0,0 @@
|
|
| 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>云浏览器 - Cloud Browser</title>
|
| 7 |
-
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
| 8 |
-
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
| 9 |
-
</head>
|
| 10 |
-
<body>
|
| 11 |
-
<div class="container">
|
| 12 |
-
<!-- Header -->
|
| 13 |
-
<header class="header">
|
| 14 |
-
<div class="header-left">
|
| 15 |
-
<i class="fas fa-cloud header-icon"></i>
|
| 16 |
-
<h1 class="header-title">云浏览器</h1>
|
| 17 |
-
<span class="badge">
|
| 18 |
-
<i class="fas fa-server"></i>
|
| 19 |
-
云端运行
|
| 20 |
-
</span>
|
| 21 |
-
</div>
|
| 22 |
-
<div class="header-right">
|
| 23 |
-
<span class="status-indicator" id="statusIndicator">
|
| 24 |
-
<span class="status-dot"></span>
|
| 25 |
-
<span class="status-text" id="statusText">连接中...</span>
|
| 26 |
-
</span>
|
| 27 |
-
</div>
|
| 28 |
-
</header>
|
| 29 |
-
|
| 30 |
-
<!-- Browser Toolbar -->
|
| 31 |
-
<div class="browser-toolbar">
|
| 32 |
-
<div class="nav-buttons">
|
| 33 |
-
<button class="nav-btn" id="backBtn" title="后退">
|
| 34 |
-
<i class="fas fa-arrow-left"></i>
|
| 35 |
-
</button>
|
| 36 |
-
<button class="nav-btn" id="forwardBtn" title="前进">
|
| 37 |
-
<i class="fas fa-arrow-right"></i>
|
| 38 |
-
</button>
|
| 39 |
-
<button class="nav-btn" id="reloadBtn" title="刷新">
|
| 40 |
-
<i class="fas fa-redo"></i>
|
| 41 |
-
</button>
|
| 42 |
-
</div>
|
| 43 |
-
|
| 44 |
-
<div class="url-bar">
|
| 45 |
-
<i class="fas fa-lock url-icon"></i>
|
| 46 |
-
<input type="text" id="urlInput" placeholder="输入网址,例如 google.com" value="">
|
| 47 |
-
<button class="go-btn" id="goBtn">
|
| 48 |
-
<i class="fas fa-arrow-right"></i>
|
| 49 |
-
</button>
|
| 50 |
-
</div>
|
| 51 |
-
</div>
|
| 52 |
-
|
| 53 |
-
<!-- Browser Viewport -->
|
| 54 |
-
<div class="browser-viewport" id="browserViewport">
|
| 55 |
-
<div class="loading-overlay" id="loadingOverlay">
|
| 56 |
-
<div class="loading-content">
|
| 57 |
-
<i class="fas fa-spinner fa-spin loading-icon"></i>
|
| 58 |
-
<p class="loading-text" id="loadingText">正在启动云浏览器...</p>
|
| 59 |
-
</div>
|
| 60 |
-
</div>
|
| 61 |
-
|
| 62 |
-
<div class="screenshot-container" id="screenshotContainer">
|
| 63 |
-
<img id="screenshotImg" class="screenshot-img" alt="Browser screenshot">
|
| 64 |
-
</div>
|
| 65 |
-
|
| 66 |
-
<!-- Hidden input for keyboard capture -->
|
| 67 |
-
<input type="text" id="keyboardCapture" class="keyboard-capture" autocomplete="off">
|
| 68 |
-
</div>
|
| 69 |
-
|
| 70 |
-
<!-- Info Bar -->
|
| 71 |
-
<div class="info-bar">
|
| 72 |
-
<div class="info-item">
|
| 73 |
-
<i class="fas fa-globe"></i>
|
| 74 |
-
<span id="currentUrl">等待加载...</span>
|
| 75 |
-
</div>
|
| 76 |
-
<div class="info-item">
|
| 77 |
-
<i class="fas fa-file-alt"></i>
|
| 78 |
-
<span id="pageTitle">-</span>
|
| 79 |
-
</div>
|
| 80 |
-
<div class="info-item">
|
| 81 |
-
<i class="fas fa-tachometer-alt"></i>
|
| 82 |
-
<span id="latency">-</span>
|
| 83 |
-
</div>
|
| 84 |
-
</div>
|
| 85 |
-
|
| 86 |
-
<!-- Tips -->
|
| 87 |
-
<div class="tips-section">
|
| 88 |
-
<h3><i class="fas fa-lightbulb"></i> 使用提示</h3>
|
| 89 |
-
<div class="tips-grid">
|
| 90 |
-
<div class="tip-card">
|
| 91 |
-
<i class="fas fa-mouse-pointer tip-icon"></i>
|
| 92 |
-
<h4>点击操作</h4>
|
| 93 |
-
<p>直接在画面上点击,就像操作真实浏览器一样</p>
|
| 94 |
-
</div>
|
| 95 |
-
<div class="tip-card">
|
| 96 |
-
<i class="fas fa-keyboard tip-icon"></i>
|
| 97 |
-
<h4>键盘输入</h4>
|
| 98 |
-
<p>点击画面后直接打字,文字会输入到云端浏览器</p>
|
| 99 |
-
</div>
|
| 100 |
-
<div class="tip-card">
|
| 101 |
-
<i class="fas fa-mouse tip-icon"></i>
|
| 102 |
-
<h4>滚动页面</h4>
|
| 103 |
-
<p>使用鼠标滚轮在画面上滚动</p>
|
| 104 |
-
</div>
|
| 105 |
-
<div class="tip-card">
|
| 106 |
-
<i class="fas fa-gamepad tip-icon"></i>
|
| 107 |
-
<h4>玩网页游戏</h4>
|
| 108 |
-
<p>输入游戏网址,在云端运行,本地不占资源</p>
|
| 109 |
-
</div>
|
| 110 |
-
</div>
|
| 111 |
-
</div>
|
| 112 |
-
</div>
|
| 113 |
-
|
| 114 |
-
<!-- Toast -->
|
| 115 |
-
<div class="toast" id="toast">
|
| 116 |
-
<div class="toast-content">
|
| 117 |
-
<i class="toast-icon" id="toastIcon"></i>
|
| 118 |
-
<span id="toastMessage"></span>
|
| 119 |
-
</div>
|
| 120 |
-
</div>
|
| 121 |
-
|
| 122 |
-
<script>
|
| 123 |
-
const viewportWidth = {{ viewport_width }};
|
| 124 |
-
const viewportHeight = {{ viewport_height }};
|
| 125 |
-
</script>
|
| 126 |
-
<script src="{{ url_for('static', filename='js/app.js') }}"></script>
|
| 127 |
-
</body>
|
| 128 |
-
</html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|