colurwindows / app.py
WACATW's picture
Upload 8 files
1db5bc8 verified
Raw History Blame Contribute Delete
25.3 kB
import os
import io
import base64
import time
import threading
import shutil
from flask import Flask, render_template, request, jsonify, send_file
from playwright.sync_api import sync_playwright, TimeoutError as PlaywrightTimeoutError
app = Flask(__name__)
# ========== 浏览器实例管理 ==========
browser_instance = None
context_instance = None
playwright_instance = None
# 页面管理(多标签页)
pages = {}
active_page_id = None
page_counter = 0
# 线程锁
browser_lock = threading.Lock()
# ========== 文件系统配置 ==========
# 用户文件存储目录
USER_FILES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'user_files')
os.makedirs(USER_FILES_DIR, exist_ok=True)
# 确保初始有一些示例文件
def init_sample_files():
"""初始化示例文件"""
docs_dir = os.path.join(USER_FILES_DIR, '文档')
os.makedirs(docs_dir, exist_ok=True)
readme_file = os.path.join(docs_dir, '欢迎使用云电脑.txt')
if not os.path.exists(readme_file):
with open(readme_file, 'w', encoding='utf-8') as f:
f.write("""欢迎使用云电脑!
这是一个基于云端计算的云电脑系统。
功能特性:
- 云端浏览器:所有网页在服务器端渲染
- 文件管理:可以新建、编辑、删除文件
- 多标签页:支持打开多个浏览器标签
- 全屏模式:沉浸式体验
提示:
- 双击桌面图标打开应用
- 点击浏览器画面进行操作
- 按 ESC 退出全屏模式
祝您使用愉快!
""")
notes_dir = os.path.join(USER_FILES_DIR, '笔记')
os.makedirs(notes_dir, exist_ok=True)
init_sample_files()
# ========== 浏览器配置 ==========
VIEWPORT_WIDTH = int(os.environ.get('VIEWPORT_WIDTH', 1280))
VIEWPORT_HEIGHT = int(os.environ.get('VIEWPORT_HEIGHT', 800))
DEFAULT_URL = os.environ.get('DEFAULT_URL', 'https://www.google.com')
# 截图质量(越低越快,50% 是速度和质量的平衡点)
SCREENSHOT_QUALITY = 50
def get_browser():
"""获取或创建浏览器实例"""
global browser_instance, context_instance, playwright_instance, pages, active_page_id, page_counter
with browser_lock:
if browser_instance is None:
playwright_instance = sync_playwright().start()
browser_instance = playwright_instance.chromium.launch(
headless=True,
args=[
'--no-sandbox',
'--disable-dev-shm-usage',
'--disable-gpu',
'--disable-extensions',
'--disable-web-security',
'--disable-features=IsolateOrigins,site-per-process',
'--disable-background-networking',
'--disable-default-apps',
'--disable-extensions',
'--disable-sync',
'--disable-translate',
'--hide-scrollbars',
'--mute-audio',
'--no-first-run',
]
)
context_instance = browser_instance.new_context(
viewport={'width': VIEWPORT_WIDTH, 'height': VIEWPORT_HEIGHT},
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'
)
# 创建第一个页面
page_counter += 1
page_id = f'page_{page_counter}'
page = context_instance.new_page()
page.goto(DEFAULT_URL, timeout=30000, wait_until='domcontentloaded')
pages[page_id] = page
active_page_id = page_id
return pages[active_page_id]
def get_page(page_id=None):
"""获取指定页面或当前活动页面"""
global active_page_id
if page_id is None:
page_id = active_page_id
if page_id and page_id in pages:
return pages[page_id]
return get_browser()
def take_screenshot(page_id=None):
"""截取当前页面截图 - 优化版(低延迟)"""
page = get_page(page_id)
try:
screenshot = page.screenshot(
type='jpeg',
quality=SCREENSHOT_QUALITY, # 降低质量以减小体积
full_page=False,
animations='disabled' # 禁用动画,加快截图
)
return base64.b64encode(screenshot).decode('utf-8')
except Exception as e:
print(f"Screenshot error: {e}")
return None
def create_new_page(url=None):
"""创建新标签页"""
global page_counter, active_page_id, pages
with browser_lock:
get_browser()
page_counter += 1
page_id = f'page_{page_counter}'
page = context_instance.new_page()
if url:
if not url.startswith('http://') and not url.startswith('https://'):
url = 'https://' + url
page.goto(url, timeout=30000, wait_until='domcontentloaded')
else:
page.goto(DEFAULT_URL, timeout=30000, wait_until='domcontentloaded')
pages[page_id] = page
active_page_id = page_id
return page_id, page
def close_page(page_id):
"""关闭标签页"""
global active_page_id, pages
with browser_lock:
if page_id in pages and len(pages) > 1:
pages[page_id].close()
del pages[page_id]
if active_page_id == page_id:
active_page_id = list(pages.keys())[0]
return True
return False
def switch_page(page_id):
"""切换到指定页面"""
global active_page_id
with browser_lock:
if page_id in pages:
active_page_id = page_id
return True
return False
def get_all_pages():
"""获取所有页面信息"""
result = []
for page_id, page in pages.items():
try:
result.append({
'id': page_id,
'title': page.title(),
'url': page.url,
'active': page_id == active_page_id
})
except:
result.append({
'id': page_id,
'title': '加载中...',
'url': '',
'active': page_id == active_page_id
})
return result
# ========== 文件系统 API ==========
def get_relative_path(path):
"""获取相对于用户目录的安全路径"""
# 规范化路径,防止路径遍历攻击
if not path:
path = '/'
# 去掉开头的 /
if path.startswith('/'):
path = path[1:]
# 拼接完整路径
full_path = os.path.join(USER_FILES_DIR, path)
# 规范化路径
full_path = os.path.normpath(full_path)
# 确保路径在用户目录内
if not full_path.startswith(os.path.normpath(USER_FILES_DIR)):
return None, None
# 计算相对路径
rel_path = os.path.relpath(full_path, USER_FILES_DIR)
if rel_path == '.':
rel_path = ''
return full_path, rel_path
@app.route('/api/files/list')
def list_files():
"""列出文件和文件夹"""
path = request.args.get('path', '')
full_path, rel_path = get_relative_path(path)
if full_path is None:
return jsonify({'success': False, 'error': '无效路径'})
if not os.path.exists(full_path):
return jsonify({'success': False, 'error': '路径不存在'})
if not os.path.isdir(full_path):
return jsonify({'success': False, 'error': '不是文件夹'})
items = []
try:
for item in os.listdir(full_path):
item_path = os.path.join(full_path, item)
is_dir = os.path.isdir(item_path)
size = os.path.getsize(item_path) if not is_dir else 0
# 获取修改时间
mtime = os.path.getmtime(item_path)
items.append({
'name': item,
'isDir': is_dir,
'size': size,
'modified': mtime,
'path': os.path.join(rel_path, item) if rel_path else item
})
# 排序:文件夹在前,按名称排序
items.sort(key=lambda x: (not x['isDir'], x['name'].lower()))
return jsonify({
'success': True,
'path': rel_path,
'items': items
})
except Exception as e:
return jsonify({'success': False, 'error': str(e)})
@app.route('/api/files/read')
def read_file():
"""读取文件内容"""
path = request.args.get('path', '')
full_path, rel_path = get_relative_path(path)
if full_path is None:
return jsonify({'success': False, 'error': '无效路径'})
if not os.path.exists(full_path):
return jsonify({'success': False, 'error': '文件不存在'})
if os.path.isdir(full_path):
return jsonify({'success': False, 'error': '不能读取文件夹'})
try:
# 检查文件大小,太大的文件不读取
size = os.path.getsize(full_path)
if size > 10 * 1024 * 1024: # 10MB
return jsonify({'success': False, 'error': '文件太大,无法读取'})
# 尝试以文本方式读取
try:
with open(full_path, 'r', encoding='utf-8') as f:
content = f.read()
return jsonify({
'success': True,
'path': rel_path,
'name': os.path.basename(full_path),
'content': content,
'size': size,
'isText': True
})
except UnicodeDecodeError:
# 二进制文件,返回 base64
with open(full_path, 'rb') as f:
content = base64.b64encode(f.read()).decode('utf-8')
return jsonify({
'success': True,
'path': rel_path,
'name': os.path.basename(full_path),
'content': content,
'size': size,
'isText': False
})
except Exception as e:
return jsonify({'success': False, 'error': str(e)})
@app.route('/api/files/write', methods=['POST'])
def write_file():
"""写入文件内容"""
data = request.get_json()
path = data.get('path', '')
content = data.get('content', '')
full_path, rel_path = get_relative_path(path)
if full_path is None:
return jsonify({'success': False, 'error': '无效路径'})
try:
# 确保父目录存在
parent_dir = os.path.dirname(full_path)
os.makedirs(parent_dir, exist_ok=True)
with open(full_path, 'w', encoding='utf-8') as f:
f.write(content)
return jsonify({
'success': True,
'path': rel_path,
'size': len(content.encode('utf-8'))
})
except Exception as e:
return jsonify({'success': False, 'error': str(e)})
@app.route('/api/files/create', methods=['POST'])
def create_file():
"""新建文件或文件夹"""
data = request.get_json()
path = data.get('path', '')
is_dir = data.get('isDir', False)
full_path, rel_path = get_relative_path(path)
if full_path is None:
return jsonify({'success': False, 'error': '无效路径'})
if os.path.exists(full_path):
return jsonify({'success': False, 'error': '已存在同名文件'})
try:
if is_dir:
os.makedirs(full_path, exist_ok=True)
else:
# 确保父目录存在
parent_dir = os.path.dirname(full_path)
os.makedirs(parent_dir, exist_ok=True)
# 创建空文件
with open(full_path, 'w', encoding='utf-8') as f:
pass
return jsonify({
'success': True,
'path': rel_path,
'isDir': is_dir
})
except Exception as e:
return jsonify({'success': False, 'error': str(e)})
@app.route('/api/files/delete', methods=['POST'])
def delete_file():
"""删除文件或文件夹"""
data = request.get_json()
path = data.get('path', '')
full_path, rel_path = get_relative_path(path)
if full_path is None:
return jsonify({'success': False, 'error': '无效路径'})
if not os.path.exists(full_path):
return jsonify({'success': False, 'error': '文件不存在'})
try:
if os.path.isdir(full_path):
shutil.rmtree(full_path)
else:
os.remove(full_path)
return jsonify({'success': True})
except Exception as e:
return jsonify({'success': False, 'error': str(e)})
@app.route('/api/files/rename', methods=['POST'])
def rename_file():
"""重命名文件"""
data = request.get_json()
path = data.get('path', '')
new_name = data.get('newName', '')
full_path, rel_path = get_relative_path(path)
if full_path is None:
return jsonify({'success': False, 'error': '无效路径'})
if not os.path.exists(full_path):
return jsonify({'success': False, 'error': '文件不存在'})
if not new_name or '/' in new_name or '\\' in new_name:
return jsonify({'success': False, 'error': '无效的文件名'})
try:
parent_dir = os.path.dirname(full_path)
new_path = os.path.join(parent_dir, new_name)
if os.path.exists(new_path):
return jsonify({'success': False, 'error': '已存在同名文件'})
os.rename(full_path, new_path)
new_rel_path = os.path.join(os.path.dirname(rel_path), new_name) if os.path.dirname(rel_path) else new_name
return jsonify({
'success': True,
'oldPath': rel_path,
'newPath': new_rel_path
})
except Exception as e:
return jsonify({'success': False, 'error': str(e)})
# ========== 浏览器 API ==========
@app.route('/')
def index():
"""主页 - 桌面环境"""
return render_template('desktop.html',
viewport_width=VIEWPORT_WIDTH,
viewport_height=VIEWPORT_HEIGHT)
@app.route('/api/navigate', methods=['POST'])
def navigate():
"""导航到指定网址"""
data = request.get_json()
url = data.get('url', '')
page_id = data.get('pageId')
if not url:
return jsonify({'success': False, 'error': '请输入网址'})
if not url.startswith('http://') and not url.startswith('https://'):
url = 'https://' + url
try:
with browser_lock:
page = get_page(page_id)
page.goto(url, timeout=30000, wait_until='domcontentloaded')
time.sleep(0.3) # 减少等待时间
screenshot = take_screenshot(page_id)
current_url = page.url
title = page.title()
pages_list = get_all_pages()
return jsonify({
'success': True,
'screenshot': screenshot,
'url': current_url,
'title': title,
'pages': pages_list,
'activePageId': active_page_id
})
except PlaywrightTimeoutError:
return jsonify({'success': False, 'error': '页面加载超时'})
except Exception as e:
return jsonify({'success': False, 'error': str(e)})
@app.route('/api/screenshot')
def screenshot():
"""获取当前页面截图 - 快速版"""
page_id = request.args.get('pageId')
try:
with browser_lock:
page = get_page(page_id)
screenshot = take_screenshot(page_id)
current_url = page.url
title = page.title()
pages_list = get_all_pages()
return jsonify({
'success': True,
'screenshot': screenshot,
'url': current_url,
'title': title,
'pages': pages_list,
'activePageId': active_page_id
})
except Exception as e:
return jsonify({'success': False, 'error': str(e)})
@app.route('/api/click', methods=['POST'])
def click():
"""处理点击事件 - 快速响应"""
data = request.get_json()
x = data.get('x', 0)
y = data.get('y', 0)
page_id = data.get('pageId')
try:
with browser_lock:
page = get_page(page_id)
page.mouse.click(x, y)
time.sleep(0.15) # 大幅减少等待时间
screenshot = take_screenshot(page_id)
current_url = page.url
title = page.title()
pages_list = get_all_pages()
return jsonify({
'success': True,
'screenshot': screenshot,
'url': current_url,
'title': title,
'pages': pages_list,
'activePageId': active_page_id
})
except Exception as e:
return jsonify({'success': False, 'error': str(e)})
@app.route('/api/type', methods=['POST'])
def type_text():
"""处理键盘输入 - 快速响应"""
data = request.get_json()
text = data.get('text', '')
page_id = data.get('pageId')
try:
with browser_lock:
page = get_page(page_id)
page.keyboard.type(text)
time.sleep(0.1) # 减少等待时间
screenshot = take_screenshot(page_id)
pages_list = get_all_pages()
return jsonify({
'success': True,
'screenshot': screenshot,
'pages': pages_list,
'activePageId': active_page_id
})
except Exception as e:
return jsonify({'success': False, 'error': str(e)})
@app.route('/api/keypress', methods=['POST'])
def keypress():
"""处理特殊按键 - 快速响应"""
data = request.get_json()
key = data.get('key', '')
page_id = data.get('pageId')
try:
with browser_lock:
page = get_page(page_id)
key_map = {
'Enter': 'Enter',
'Backspace': 'Backspace',
'Tab': 'Tab',
'Escape': 'Escape',
'ArrowUp': 'ArrowUp',
'ArrowDown': 'ArrowDown',
'ArrowLeft': 'ArrowLeft',
'ArrowRight': 'ArrowRight',
' ': 'Space',
}
if key in key_map:
page.keyboard.press(key_map[key])
elif len(key) == 1:
page.keyboard.press(key)
time.sleep(0.1) # 减少等待时间
screenshot = take_screenshot(page_id)
pages_list = get_all_pages()
return jsonify({
'success': True,
'screenshot': screenshot,
'pages': pages_list,
'activePageId': active_page_id
})
except Exception as e:
return jsonify({'success': False, 'error': str(e)})
@app.route('/api/scroll', methods=['POST'])
def scroll():
"""处理滚动 - 快速响应"""
data = request.get_json()
delta_x = data.get('deltaX', 0)
delta_y = data.get('deltaY', 0)
page_id = data.get('pageId')
try:
with browser_lock:
page = get_page(page_id)
page.mouse.wheel(delta_x, delta_y)
time.sleep(0.08) # 大幅减少等待时间
screenshot = take_screenshot(page_id)
pages_list = get_all_pages()
return jsonify({
'success': True,
'screenshot': screenshot,
'pages': pages_list,
'activePageId': active_page_id
})
except Exception as e:
return jsonify({'success': False, 'error': str(e)})
@app.route('/api/go-back', methods=['POST'])
def go_back():
"""后退"""
data = request.get_json()
page_id = data.get('pageId')
try:
with browser_lock:
page = get_page(page_id)
page.go_back()
time.sleep(0.3)
screenshot = take_screenshot(page_id)
current_url = page.url
title = page.title()
pages_list = get_all_pages()
return jsonify({
'success': True,
'screenshot': screenshot,
'url': current_url,
'title': title,
'pages': pages_list,
'activePageId': active_page_id
})
except Exception as e:
return jsonify({'success': False, 'error': str(e)})
@app.route('/api/go-forward', methods=['POST'])
def go_forward():
"""前进"""
data = request.get_json()
page_id = data.get('pageId')
try:
with browser_lock:
page = get_page(page_id)
page.go_forward()
time.sleep(0.3)
screenshot = take_screenshot(page_id)
current_url = page.url
title = page.title()
pages_list = get_all_pages()
return jsonify({
'success': True,
'screenshot': screenshot,
'url': current_url,
'title': title,
'pages': pages_list,
'activePageId': active_page_id
})
except Exception as e:
return jsonify({'success': False, 'error': str(e)})
@app.route('/api/reload', methods=['POST'])
def reload():
"""刷新"""
data = request.get_json()
page_id = data.get('pageId')
try:
with browser_lock:
page = get_page(page_id)
page.reload()
time.sleep(0.3)
screenshot = take_screenshot(page_id)
current_url = page.url
title = page.title()
pages_list = get_all_pages()
return jsonify({
'success': True,
'screenshot': screenshot,
'url': current_url,
'title': title,
'pages': pages_list,
'activePageId': active_page_id
})
except Exception as e:
return jsonify({'success': False, 'error': str(e)})
@app.route('/api/new-tab', methods=['POST'])
def new_tab():
"""新建标签页"""
data = request.get_json() or {}
url = data.get('url')
try:
with browser_lock:
page_id, page = create_new_page(url)
time.sleep(0.2)
screenshot = take_screenshot(page_id)
current_url = page.url
title = page.title()
pages_list = get_all_pages()
return jsonify({
'success': True,
'pageId': page_id,
'screenshot': screenshot,
'url': current_url,
'title': title,
'pages': pages_list,
'activePageId': active_page_id
})
except Exception as e:
return jsonify({'success': False, 'error': str(e)})
@app.route('/api/close-tab', methods=['POST'])
def close_tab():
"""关闭标签页"""
data = request.get_json()
page_id = data.get('pageId')
try:
with browser_lock:
success = close_page(page_id)
page = get_page()
screenshot = take_screenshot()
pages_list = get_all_pages()
return jsonify({
'success': success,
'screenshot': screenshot,
'pages': pages_list,
'activePageId': active_page_id
})
except Exception as e:
return jsonify({'success': False, 'error': str(e)})
@app.route('/api/switch-tab', methods=['POST'])
def switch_tab():
"""切换标签页"""
data = request.get_json()
page_id = data.get('pageId')
try:
with browser_lock:
success = switch_page(page_id)
page = get_page()
screenshot = take_screenshot()
current_url = page.url
title = page.title()
pages_list = get_all_pages()
return jsonify({
'success': success,
'screenshot': screenshot,
'url': current_url,
'title': title,
'pages': pages_list,
'activePageId': active_page_id
})
except Exception as e:
return jsonify({'success': False, 'error': str(e)})
@app.route('/api/status')
def status():
"""获取浏览器状态"""
try:
with browser_lock:
page = get_page()
pages_list = get_all_pages()
return jsonify({
'success': True,
'ready': True,
'url': page.url,
'title': page.title(),
'pages': pages_list,
'activePageId': active_page_id
})
except Exception as e:
return jsonify({
'success': True,
'ready': False,
'error': str(e)
})
if __name__ == '__main__':
port = int(os.environ.get('PORT', 7860))
# 使用单线程模式,避免 Playwright 跨线程问题
app.run(host='0.0.0.0', port=port, debug=False, threaded=False)