diff --git a/cli/cli.js b/cli/cli.js index 64fe6f2f1151520231979eed56e9a6dea43f327b..cb79ee0985ac9181dee33ed26142fda018b0ea76 100644 --- a/cli/cli.js +++ b/cli/cli.js @@ -389,15 +389,15 @@ program program .command('reset-admin') - .description('Reset Admin Key using recovery token (offline)') - .requiredOption('-r, --recovery-token ', 'Admin recovery token') - .option('--db-path ', 'Database path', process.env.DATABASE_PATH || 'minecraft_ws.db') - .option('--keep-existing', 'Keep existing Admin Keys active') - .option('--name ', 'New Admin Key name', 'Recovered Admin Key') + .description('使用恢复令牌重置 Admin Key (离线)') + .requiredOption('-r, --recovery-token ', '管理员恢复令牌') + .option('--db-path ', '数据库路径', process.env.DATABASE_PATH || 'minecraft_ws.db') + .option('--keep-existing', '保持现有 Admin Key 处于活跃状态') + .option('--name ', '新 Admin Key 名称', 'Recovered Admin Key') .action(async (options) => { try { if (!fs.existsSync(options.dbPath)) { - throw new Error(`Database not found at ${options.dbPath}`); + throw new Error(`在 ${options.dbPath} 未找到数据库`); } const db = new Database(options.dbPath); @@ -408,12 +408,12 @@ program name: options.name }); - console.log('Admin Key reset successful.'); + console.log('\x1b[32m✅ Admin Key 重置成功。\x1b[0m'); console.log(` ID : ${result.id}`); - console.log(` Name : ${result.name}`); + console.log(` 名称 : ${result.name}`); console.log(` Key : ${result.key}`); } catch (error) { - console.error(`\x1b[31mReset failed: ${error.message}\x1b[0m`); + console.error(`\x1b[31m❌ 重置失败: ${error.message}\x1b[0m`); process.exit(1); } }); diff --git a/dashboard/server.js b/dashboard/server.js index b06b0e565690459d364e51982b58a5fa8ae3b642..4d063e2bd261eda7c3434eb18759a7326ada9f67 100644 --- a/dashboard/server.js +++ b/dashboard/server.js @@ -13,9 +13,9 @@ app.get('/', (req, res) => { app.listen(PORT, () => { console.log('='.repeat(50)); - console.log('Minecraft WebSocket API - Dashboard'); + console.log('Minecraft WebSocket API - 控制台'); console.log('='.repeat(50)); - console.log(`Dashboard URL: http://localhost:${PORT}`); - console.log('Use Admin or Regular Key to log in.'); + console.log(`控制台地址: http://localhost:${PORT}`); + console.log('请使用 Admin Key 或 Regular Key 登录。'); console.log('='.repeat(50)); }); diff --git a/docs/protocol.md b/docs/protocol.md index 1c77fe1fe0cfc9edb3f34d64503443e939d18a47..7e03e6569ab7b31370a5b5db91734572aeb370a0 100644 --- a/docs/protocol.md +++ b/docs/protocol.md @@ -1,32 +1,31 @@ -# Protocol Specification +# 协议规范 -This document defines the REST and WebSocket protocol formats used by -InterConnect-Server. All payloads are JSON. +本文档定义了 InterConnect-Server 使用的 REST 和 WebSocket 协议格式。所有负载均为 JSON 格式。 -## Authentication +## 认证 (Authentication) -- REST: Send `Authorization: Bearer ` in the request headers. -- WebSocket: Connect to `ws:///ws?api_key=` (use `wss` over HTTPS). +- REST: 在请求头中发送 `Authorization: Bearer `。 +- WebSocket: 连接到 `ws:///ws?api_key=`(HTTPS 下使用 `wss`)。 -## Event Object +## 事件对象 (Event Object) -Events are sent to the server via REST and broadcast to WebSocket clients. -The server validates required fields and the `event_type` value only. -The `data` payload is otherwise passed through as-is. +事件通过 REST 发送到服务器,并广播给 WebSocket 客户端。 +服务器仅验证必填字段和 `event_type` 值。 +`data` 负载会原样传递。 -Required fields: -- `event_type` (string) -- `server_name` (string) -- `timestamp` (string, ISO 8601) -- `data` (object) +必填字段: +- `event_type` (字符串) +- `server_name` (字符串) +- `timestamp` (字符串, ISO 8601) +- `data` (对象) -Valid `event_type` values: +有效的 `event_type` 值: - `player_join` - `player_leave` - `message` - `server_command` -Example event: +事件示例: ```json { "event_type": "player_join", @@ -38,34 +37,34 @@ Example event: } ``` -## REST Endpoints +## REST 端点 (REST Endpoints) ### POST /api/events -Submit an event to be stored and broadcast. +提交一个事件以进行存储和广播。 -Request: -- Auth: any valid key +请求: +- 认证:任意有效密钥 - `Content-Type: application/json` -- Body: Event Object +- Body: 事件对象 -Response: +响应: ```json { "message": "Event received and broadcasted" } ``` -Errors: -- `400` if required fields are missing or `event_type` is invalid -- `401` if the API key is missing/invalid +错误: +- `400` 如果缺少必填字段或 `event_type` 无效 +- `401` 如果 API 密钥丢失/无效 ### POST /api/server/command -Send a server command. This also broadcasts a `server_command` event. +发送服务器命令。这也将广播一个 `server_command` 事件。 -Request: -- Auth: Regular or Admin key +请求: +- 认证:Regular 或 Admin 密钥 - `Content-Type: application/json` - Body: ```json @@ -75,11 +74,11 @@ Request: } ``` -Notes: -- `server_id` is required for Admin keys when not bound to a server. -- The server enforces an allow/block list for Admin commands. +注意: +- 当 Admin 密钥未绑定到服务器时,必须提供 `server_id`。 +- 服务器对 Admin 命令强制执行允许/阻止列表。 -Response: +响应: ```json { "message": "Command sent successfully", @@ -88,23 +87,23 @@ Response: } ``` -## WebSocket Messages +## WebSocket 消息 (WebSocket Messages) -### Client to Server +### 客户端到服务器 Ping: ```json { "type": "ping" } ``` -Pong response: +Pong 响应: ```json { "type": "pong" } ``` -### Server to Client +### 服务器到客户端 -Broadcasted event: +广播事件: ```json { "type": "minecraft_event", @@ -121,5 +120,4 @@ Broadcasted event: } ``` -`source_key_id_prefix` is the first 8 characters of the API key ID that -submitted the event or command. It is not the API key itself. +`source_key_id_prefix` 是提交事件或命令的 API 密钥 ID 的前 8 个字符。它不是 API 密钥本身。 diff --git a/hf_repo/docs/protocol.md b/hf_repo/docs/protocol.md new file mode 100644 index 0000000000000000000000000000000000000000..1c77fe1fe0cfc9edb3f34d64503443e939d18a47 --- /dev/null +++ b/hf_repo/docs/protocol.md @@ -0,0 +1,125 @@ +# Protocol Specification + +This document defines the REST and WebSocket protocol formats used by +InterConnect-Server. All payloads are JSON. + +## Authentication + +- REST: Send `Authorization: Bearer ` in the request headers. +- WebSocket: Connect to `ws:///ws?api_key=` (use `wss` over HTTPS). + +## Event Object + +Events are sent to the server via REST and broadcast to WebSocket clients. +The server validates required fields and the `event_type` value only. +The `data` payload is otherwise passed through as-is. + +Required fields: +- `event_type` (string) +- `server_name` (string) +- `timestamp` (string, ISO 8601) +- `data` (object) + +Valid `event_type` values: +- `player_join` +- `player_leave` +- `message` +- `server_command` + +Example event: +```json +{ + "event_type": "player_join", + "server_name": "survival-01", + "timestamp": "2024-01-21T12:34:56.789Z", + "data": { + "player": "Steve" + } +} +``` + +## REST Endpoints + +### POST /api/events + +Submit an event to be stored and broadcast. + +Request: +- Auth: any valid key +- `Content-Type: application/json` +- Body: Event Object + +Response: +```json +{ + "message": "Event received and broadcasted" +} +``` + +Errors: +- `400` if required fields are missing or `event_type` is invalid +- `401` if the API key is missing/invalid + +### POST /api/server/command + +Send a server command. This also broadcasts a `server_command` event. + +Request: +- Auth: Regular or Admin key +- `Content-Type: application/json` +- Body: +```json +{ + "command": "say Hello from API", + "server_id": "survival-01" +} +``` + +Notes: +- `server_id` is required for Admin keys when not bound to a server. +- The server enforces an allow/block list for Admin commands. + +Response: +```json +{ + "message": "Command sent successfully", + "command": "say Hello from API", + "server_id": "survival-01" +} +``` + +## WebSocket Messages + +### Client to Server + +Ping: +```json +{ "type": "ping" } +``` + +Pong response: +```json +{ "type": "pong" } +``` + +### Server to Client + +Broadcasted event: +```json +{ + "type": "minecraft_event", + "event": { + "event_type": "message", + "server_name": "survival-01", + "timestamp": "2024-01-21T12:34:56.789Z", + "data": { + "player": "Alex", + "message": "Hello world" + } + }, + "source_key_id_prefix": "a1b2c3d4" +} +``` + +`source_key_id_prefix` is the first 8 characters of the API key ID that +submitted the event or command. It is not the API key itself. diff --git a/hf_repo/hf_repo/dashboard/public/app.js b/hf_repo/hf_repo/dashboard/public/app.js index 7deb119f5cb32b9b66fa0b3a8e26db7225937ab4..4948be20bae648f050142e5d62f18c1cd66e8a37 100644 --- a/hf_repo/hf_repo/dashboard/public/app.js +++ b/hf_repo/hf_repo/dashboard/public/app.js @@ -26,6 +26,16 @@ const commandForm = document.getElementById('command-form'); const commandInput = document.getElementById('command-input'); const commandHistory = document.getElementById('command-history'); const userEventsList = document.getElementById('user-events-list'); +const aiConfigForm = document.getElementById('ai-config-form'); +const aiApiUrlInput = document.getElementById('ai-api-url'); +const aiModelIdInput = document.getElementById('ai-model-id'); +const aiApiKeyInput = document.getElementById('ai-api-key'); +const aiApiKeyHint = document.getElementById('ai-api-key-hint'); +const aiSystemPromptInput = document.getElementById('ai-system-prompt'); +const aiEnabledCheckbox = document.getElementById('ai-enabled'); +const aiTestBtn = document.getElementById('ai-test-btn'); +const aiDeleteBtn = document.getElementById('ai-delete-btn'); +const aiStatus = document.getElementById('ai-status'); function authHeaders(key) { return { Authorization: `Bearer ${key}` }; @@ -92,6 +102,7 @@ function showAdminPanel() { } loadAdminKeys(); + loadAIConfig(); } function showUserPanel() { @@ -605,3 +616,170 @@ if (commandForm) { window.activateKey = activateKey; window.deactivateKey = deactivateKey; window.deleteKey = deleteKey; + +async function loadAIConfig() { + if (!apiKey) { + return; + } + try { + const response = await fetch(`${API_URL}/api/ai/config`, { + headers: authHeaders(apiKey) + }); + + if (response.ok) { + const config = await response.json(); + if (aiApiUrlInput) aiApiUrlInput.value = config.apiUrl || ''; + if (aiModelIdInput) aiModelIdInput.value = config.modelId || ''; + if (aiApiKeyInput) aiApiKeyInput.value = ''; + if (aiApiKeyHint) aiApiKeyHint.textContent = config.apiKey ? `Current: ${config.apiKey}` : ''; + if (aiSystemPromptInput) aiSystemPromptInput.value = config.systemPrompt || ''; + if (aiEnabledCheckbox) aiEnabledCheckbox.checked = config.enabled; + showAIStatus('Configuration loaded', 'success'); + } else if (response.status === 404) { + if (aiApiUrlInput) aiApiUrlInput.value = ''; + if (aiModelIdInput) aiModelIdInput.value = ''; + if (aiApiKeyInput) aiApiKeyInput.value = ''; + if (aiApiKeyHint) aiApiKeyHint.textContent = ''; + if (aiSystemPromptInput) aiSystemPromptInput.value = ''; + if (aiEnabledCheckbox) aiEnabledCheckbox.checked = true; + showAIStatus('No configuration found. Please set up AI configuration.', 'info'); + } + } catch (error) { + showAIStatus(`Failed to load AI config: ${error.message}`, 'error'); + } +} + +async function saveAIConfig(event) { + event.preventDefault(); + if (!apiKey) { + return; + } + + const apiUrl = aiApiUrlInput?.value?.trim(); + const modelId = aiModelIdInput?.value?.trim(); + const apiKeyValue = aiApiKeyInput?.value?.trim(); + const systemPrompt = aiSystemPromptInput?.value?.trim() || null; + const enabled = aiEnabledCheckbox?.checked ?? true; + + if (!apiUrl || !modelId) { + showAIStatus('API URL and Model ID are required', 'error'); + return; + } + + try { + const existingResponse = await fetch(`${API_URL}/api/ai/config`, { + headers: authHeaders(apiKey) + }); + const isUpdate = existingResponse.ok; + + const payload = { + api_url: apiUrl, + model_id: modelId, + enabled: enabled, + system_prompt: systemPrompt + }; + + if (apiKeyValue) { + payload.api_key = apiKeyValue; + } else if (!isUpdate) { + showAIStatus('API Key is required for new configuration', 'error'); + return; + } + + const method = isUpdate ? 'PATCH' : 'POST'; + const response = await fetch(`${API_URL}/api/ai/config`, { + method: method, + headers: { + ...authHeaders(apiKey), + 'Content-Type': 'application/json' + }, + body: JSON.stringify(payload) + }); + + if (response.ok) { + const config = await response.json(); + if (aiApiKeyInput) aiApiKeyInput.value = ''; + if (aiApiKeyHint) aiApiKeyHint.textContent = config.apiKey ? `Current: ${config.apiKey}` : ''; + showAIStatus('Configuration saved successfully', 'success'); + } else { + const error = await response.json(); + showAIStatus(`Failed to save: ${error.detail}`, 'error'); + } + } catch (error) { + showAIStatus(`Failed to save AI config: ${error.message}`, 'error'); + } +} + +async function testAIConnection() { + if (!apiKey) { + return; + } + showAIStatus('Testing connection...', 'info'); + + try { + const response = await fetch(`${API_URL}/api/ai/config/test`, { + method: 'POST', + headers: authHeaders(apiKey) + }); + + const result = await response.json(); + if (result.success) { + showAIStatus(`Connection successful! Model: ${result.model}, Response: ${result.response}`, 'success'); + } else { + showAIStatus(`Connection failed: ${result.error}`, 'error'); + } + } catch (error) { + showAIStatus(`Test failed: ${error.message}`, 'error'); + } +} + +async function deleteAIConfig() { + if (!apiKey) { + return; + } + if (!confirm('Are you sure you want to delete the AI configuration?')) { + return; + } + + try { + const response = await fetch(`${API_URL}/api/ai/config`, { + method: 'DELETE', + headers: authHeaders(apiKey) + }); + + if (response.ok || response.status === 204) { + if (aiApiUrlInput) aiApiUrlInput.value = ''; + if (aiModelIdInput) aiModelIdInput.value = ''; + if (aiApiKeyInput) aiApiKeyInput.value = ''; + if (aiApiKeyHint) aiApiKeyHint.textContent = ''; + if (aiSystemPromptInput) aiSystemPromptInput.value = ''; + if (aiEnabledCheckbox) aiEnabledCheckbox.checked = true; + showAIStatus('Configuration deleted', 'success'); + } else { + const error = await response.json(); + showAIStatus(`Failed to delete: ${error.detail}`, 'error'); + } + } catch (error) { + showAIStatus(`Failed to delete AI config: ${error.message}`, 'error'); + } +} + +function showAIStatus(message, type) { + if (!aiStatus) { + return; + } + aiStatus.textContent = message; + aiStatus.className = `ai-status ${type}`; +} + +if (aiConfigForm) { + aiConfigForm.addEventListener('submit', saveAIConfig); +} + +if (aiTestBtn) { + aiTestBtn.addEventListener('click', testAIConnection); +} + +if (aiDeleteBtn) { + aiDeleteBtn.addEventListener('click', deleteAIConfig); +} diff --git a/hf_repo/hf_repo/dashboard/public/index.html b/hf_repo/hf_repo/dashboard/public/index.html index 0eb58abe126ca99298672aaa6b5b170ee582a56d..8ccaa5d0657a4dcdddfbe0935349f1534d22256a 100644 --- a/hf_repo/hf_repo/dashboard/public/index.html +++ b/hf_repo/hf_repo/dashboard/public/index.html @@ -34,6 +34,44 @@
+
+
+

AI Configuration

+
+
+
+
+ + +
+
+ + +
+
+ + + +
+
+ + +
+
+ +
+
+ + + +
+
+
+
+
+

API Key Management

diff --git a/hf_repo/hf_repo/dashboard/public/style.css b/hf_repo/hf_repo/dashboard/public/style.css index 1f962cd33cf818a32d97407b201ad262b0331e25..5774dab5eeb4ea2d60d42c52f8800e11c9ffe31f 100644 --- a/hf_repo/hf_repo/dashboard/public/style.css +++ b/hf_repo/hf_repo/dashboard/public/style.css @@ -433,3 +433,88 @@ body { font-size: 0.9em; color: white; } + +.ai-config-form { + max-width: 600px; +} + +.ai-config-form .form-group { + margin-bottom: 20px; +} + +.ai-config-form label { + display: block; + margin-bottom: 8px; + font-weight: bold; + color: #333; +} + +.ai-config-form input[type="text"], +.ai-config-form input[type="password"], +.ai-config-form textarea { + width: 100%; + padding: 12px; + border: 2px solid #e0e0e0; + border-radius: 5px; + font-size: 14px; + transition: border-color 0.3s; +} + +.ai-config-form input[type="text"]:focus, +.ai-config-form input[type="password"]:focus, +.ai-config-form textarea:focus { + border-color: #667eea; + outline: none; +} + +.ai-config-form textarea { + min-height: 100px; + resize: vertical; +} + +.ai-config-form .hint { + display: block; + margin-top: 5px; + font-size: 0.85em; + color: #666; +} + +.ai-config-form .form-actions { + display: flex; + gap: 10px; + margin-top: 25px; +} + +.ai-status { + margin-top: 20px; + padding: 12px 16px; + border-radius: 5px; + font-size: 0.9em; +} + +.ai-status:empty { + display: none; +} + +.ai-status.success { + background: #d4edda; + color: #155724; + border: 1px solid #c3e6cb; +} + +.ai-status.error { + background: #f8d7da; + color: #721c24; + border: 1px solid #f5c6cb; +} + +.ai-status.info { + background: #d1ecf1; + color: #0c5460; + border: 1px solid #bee5eb; +} + +.ai-config-form input[type="checkbox"] { + width: auto; + margin-right: 8px; +} diff --git a/hf_repo/hf_repo/docs/AI_API.md b/hf_repo/hf_repo/docs/AI_API.md new file mode 100644 index 0000000000000000000000000000000000000000..e082f93a4bd1f345119b4845ba8eb19753e3ed9b --- /dev/null +++ b/hf_repo/hf_repo/docs/AI_API.md @@ -0,0 +1,415 @@ +# InterConnect-Server AI API Documentation + +本文档描述了 InterConnect-Server 的 AI 聊天功能相关 API 接口。 + +## 概述 + +AI 功能允许 Minecraft 服务器通过 InterConnect-Client 插件/模组调用 OpenAI 格式的 API,实现游戏内 AI 聊天功能。 + +**基础路径**: `/api/ai` + +--- + +## 认证 + +所有 API 请求需要在 Header 中携带 API Key: + +``` +Authorization: Bearer +``` + +### 权限要求 + +| 接口 | 权限要求 | +|------|----------| +| `GET /api/ai/config` | Admin Key | +| `POST /api/ai/config` | Admin Key | +| `PATCH /api/ai/config` | Admin Key | +| `DELETE /api/ai/config` | Admin Key | +| `POST /api/ai/config/test` | Admin Key | +| `POST /api/ai/chat` | Server Key 或 Admin Key | + +--- + +## AI 配置接口 + +### 获取 AI 配置 + +获取当前的 AI 配置信息(API Key 会被部分隐藏)。 + +**请求** + +```http +GET /api/ai/config +Authorization: Bearer +``` + +**成功响应** `200 OK` + +```json +{ + "apiUrl": "https://api.openai.com/v1/chat/completions", + "modelId": "gpt-3.5-turbo", + "apiKey": "sk-x****xxxx", + "enabled": true, + "systemPrompt": "You are a helpful assistant for Minecraft players.", + "createdAt": "2024-01-15T10:30:00.000Z", + "updatedAt": "2024-01-15T12:00:00.000Z" +} +``` + +**错误响应** `404 Not Found` + +```json +{ + "detail": "AI configuration not found" +} +``` + +--- + +### 创建 AI 配置 + +创建新的 AI 配置。 + +**请求** + +```http +POST /api/ai/config +Authorization: Bearer +Content-Type: application/json +``` + +**请求体** + +```json +{ + "api_url": "https://api.openai.com/v1/chat/completions", + "model_id": "gpt-3.5-turbo", + "api_key": "sk-xxxxxxxxxxxxxxxxxxxxxxxx", + "enabled": true, + "system_prompt": "You are a helpful assistant for Minecraft players." +} +``` + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `api_url` | string | ✅ | OpenAI 格式的 API URL | +| `model_id` | string | ✅ | 模型 ID (如 gpt-3.5-turbo, gpt-4) | +| `api_key` | string | ✅ | API Key | +| `enabled` | boolean | ❌ | 是否启用,默认 `true` | +| `system_prompt` | string | ❌ | 系统提示词,可选 | + +**成功响应** `201 Created` + +```json +{ + "apiUrl": "https://api.openai.com/v1/chat/completions", + "modelId": "gpt-3.5-turbo", + "apiKey": "sk-x****xxxx", + "enabled": true, + "systemPrompt": "You are a helpful assistant for Minecraft players.", + "createdAt": "2024-01-15T10:30:00.000Z", + "updatedAt": "2024-01-15T10:30:00.000Z" +} +``` + +**错误响应** `400 Bad Request` + +```json +{ + "detail": "api_url, model_id, and api_key are required" +} +``` + +--- + +### 更新 AI 配置 + +更新现有的 AI 配置,只需提供要更新的字段。 + +**请求** + +```http +PATCH /api/ai/config +Authorization: Bearer +Content-Type: application/json +``` + +**请求体** + +```json +{ + "model_id": "gpt-4", + "enabled": false +} +``` + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `api_url` | string | ❌ | OpenAI 格式的 API URL | +| `model_id` | string | ❌ | 模型 ID | +| `api_key` | string | ❌ | API Key(不提供则保持原值) | +| `enabled` | boolean | ❌ | 是否启用 | +| `system_prompt` | string | ❌ | 系统提示词 | + +**成功响应** `200 OK` + +```json +{ + "apiUrl": "https://api.openai.com/v1/chat/completions", + "modelId": "gpt-4", + "apiKey": "sk-x****xxxx", + "enabled": false, + "systemPrompt": "You are a helpful assistant for Minecraft players.", + "createdAt": "2024-01-15T10:30:00.000Z", + "updatedAt": "2024-01-15T14:00:00.000Z" +} +``` + +--- + +### 删除 AI 配置 + +删除 AI 配置。 + +**请求** + +```http +DELETE /api/ai/config +Authorization: Bearer +``` + +**成功响应** `204 No Content` + +无响应体 + +**错误响应** `404 Not Found` + +```json +{ + "detail": "AI configuration not found" +} +``` + +--- + +### 测试 AI 连接 + +测试当前 AI 配置是否能正常连接。 + +**请求** + +```http +POST /api/ai/config/test +Authorization: Bearer +``` + +**成功响应** `200 OK` + +```json +{ + "success": true, + "message": "Connection successful", + "model": "gpt-3.5-turbo", + "response": "OK" +} +``` + +**错误响应** `400 Bad Request` + +```json +{ + "success": false, + "message": "Connection failed", + "error": "Invalid API key" +} +``` + +--- + +## AI 聊天接口 + +### 发送聊天消息 + +发送消息到 AI 并获取回复。此接口供 Minecraft 服务器的 InterConnect-Client 插件/模组调用。 + +**请求** + +```http +POST /api/ai/chat +Authorization: Bearer +Content-Type: application/json +``` + +**请求体** + +```json +{ + "message": "你好,请介绍一下 Minecraft 的红石系统", + "player_name": "Steve", + "server_id": "my-server-1" +} +``` + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `message` | string | ✅ | 用户发送的消息内容 | +| `player_name` | string | ❌ | 发送消息的玩家名称 | +| `server_id` | string | ❌ | 服务器 ID(Admin Key 可指定,Server Key 使用绑定的 server_id) | + +**成功响应** `200 OK` + +```json +{ + "success": true, + "reply": "红石是 Minecraft 中的一种特殊材料,可以用来创建各种机械装置...", + "model": "gpt-3.5-turbo", + "usage": { + "prompt_tokens": 25, + "completion_tokens": 150, + "total_tokens": 175 + } +} +``` + +**错误响应** + +`404 Not Found` - AI 配置不存在 + +```json +{ + "detail": "AI configuration not found" +} +``` + +`403 Forbidden` - AI 功能已禁用 + +```json +{ + "detail": "AI feature is disabled" +} +``` + +`400 Bad Request` - 缺少必填字段 + +```json +{ + "detail": "message is required" +} +``` + +`500 Internal Server Error` - AI 请求失败 + +```json +{ + "success": false, + "detail": "AI request failed", + "error": "Rate limit exceeded" +} +``` + +--- + +## 事件类型 + +当 AI 聊天发生时,会产生 `ai_chat` 类型的事件,通过 WebSocket 广播给所有连接的客户端。 + +**事件格式** + +```json +{ + "type": "minecraft_event", + "event": { + "event_type": "ai_chat", + "server_name": "my-server-1", + "timestamp": "2024-01-15T10:30:00.000Z", + "data": { + "player_name": "Steve", + "message": "你好", + "reply": "你好!有什么我可以帮助你的吗?", + "model": "gpt-3.5-turbo" + } + }, + "source_key_id_prefix": "abc12345" +} +``` + +--- + +## Minecraft 插件/模组集成示例 + +### 命令格式 + +在 Minecraft 服务器中,玩家可以使用以下命令调用 AI: + +``` +/ic chat <消息内容> +``` + +### 插件实现流程 + +1. 玩家执行 `/ic chat 你好` 命令 +2. 插件捕获命令,提取消息内容 +3. 插件向 InterConnect-Server 发送 POST 请求: + +```http +POST /api/ai/chat +Authorization: Bearer +Content-Type: application/json + +{ + "message": "你好", + "player_name": "Steve", + "server_id": "my-server-1" +} +``` + +4. 收到响应后,将 AI 回复发送给玩家 + +### 响应处理示例(伪代码) + +```java +// 发送请求 +Response response = httpClient.post("/api/ai/chat", { + "message": playerMessage, + "player_name": player.getName(), + "server_id": serverId +}); + +// 处理响应 +if (response.success) { + player.sendMessage("[AI] " + response.reply); +} else { + player.sendMessage("[AI] 请求失败: " + response.error); +} +``` + +--- + +## 支持的 API 提供商 + +本系统支持所有兼容 OpenAI Chat Completions API 格式的服务: + +| 提供商 | API URL 示例 | +|--------|-------------| +| OpenAI | `https://api.openai.com/v1/chat/completions` | +| Azure OpenAI | `https://{resource}.openai.azure.com/openai/deployments/{deployment}/chat/completions?api-version=2024-02-01` | +| Anthropic (via proxy) | 需要兼容层 | +| 本地模型 (Ollama) | `http://localhost:11434/v1/chat/completions` | +| 其他兼容服务 | 根据服务商文档配置 | + +--- + +## 错误码参考 + +| HTTP 状态码 | 说明 | +|-------------|------| +| 200 | 请求成功 | +| 201 | 创建成功 | +| 204 | 删除成功(无内容) | +| 400 | 请求参数错误 | +| 401 | 未认证或 API Key 无效 | +| 403 | 权限不足或功能已禁用 | +| 404 | 资源不存在 | +| 500 | 服务器内部错误 | diff --git a/hf_repo/hf_repo/docs/API.md b/hf_repo/hf_repo/docs/API.md new file mode 100644 index 0000000000000000000000000000000000000000..a1d2f98f189da79fe2d88cfadfb40b9d59498b8f --- /dev/null +++ b/hf_repo/hf_repo/docs/API.md @@ -0,0 +1,730 @@ +# InterConnect-Server API Documentation + +本文档描述了 InterConnect-Server 的所有 API 接口(AI 相关接口请参考 [AI_API.md](./AI_API.md))。 + +## 概述 + +InterConnect-Server 是一个 Minecraft WebSocket API 服务器,提供 API Key 管理、事件广播、服务器命令等功能。 + +**默认端口**: `8000` + +--- + +## 认证 + +除健康检查接口外,所有 API 请求需要在 Header 中携带 API Key: + +``` +Authorization: Bearer +``` + +### API Key 类型 + +| 类型 | 前缀 | 说明 | +|------|------|------| +| Admin Key | `mc_admin_` | 管理员密钥,拥有所有权限 | +| Regular Key | `mc_key_` | 普通密钥,可管理关联的 Server Key | +| Server Key | `mc_server_` | 服务器密钥,供 Minecraft 服务器使用 | + +--- + +## 健康检查 + +### 获取服务器状态 + +获取服务器健康状态和统计信息,无需认证。 + +**请求** + +```http +GET /health +``` + +**成功响应** `200 OK` + +```json +{ + "status": "healthy", + "timestamp": "2024-01-15T10:30:00.000Z", + "active_ws": 5, + "keys_total": 10, + "admin_active": 2, + "server_active": 4, + "regular_active": 4 +} +``` + +| 字段 | 类型 | 说明 | +|------|------|------| +| `status` | string | 服务器状态 (`healthy` / `unhealthy`) | +| `timestamp` | string | 当前时间戳 | +| `active_ws` | number | 活跃的 WebSocket 连接数 | +| `keys_total` | number | API Key 总数 | +| `admin_active` | number | 活跃的 Admin Key 数量 | +| `server_active` | number | 活跃的 Server Key 数量 | +| `regular_active` | number | 活跃的 Regular Key 数量 | + +**错误响应** `500 Internal Server Error` + +```json +{ + "status": "unhealthy", + "error": "Database connection failed" +} +``` + +--- + +## API Key 管理 + +**基础路径**: `/manage/keys` + +### 获取所有 API Key + +获取所有 API Key 的信息列表。 + +**权限**: Admin Key + +**请求** + +```http +GET /manage/keys +Authorization: Bearer +``` + +**成功响应** `200 OK` + +```json +[ + { + "id": "550e8400-e29b-41d4-a716-446655440000", + "name": "My Server Key", + "description": "Production server", + "keyPrefix": "mc_key_", + "keyType": "regular", + "serverId": "server-1", + "regularKeyId": null, + "createdAt": "2024-01-15T10:30:00.000Z", + "lastUsed": "2024-01-15T12:00:00.000Z", + "isActive": true + } +] +``` + +--- + +### 创建 API Key + +创建新的 API Key。创建 Regular Key 时会自动生成关联的 Server Key。 + +**权限**: Admin Key + +**请求** + +```http +POST /manage/keys +Authorization: Bearer +Content-Type: application/json +``` + +**请求体 - 创建 Regular Key(推荐)** + +```json +{ + "name": "My Server", + "description": "Production Minecraft server", + "key_type": "regular", + "server_id": "server-1" +} +``` + +**请求体 - 创建 Admin Key** + +```json +{ + "name": "New Admin", + "description": "Backup admin key", + "key_type": "admin" +} +``` + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `name` | string | ✅ | Key 名称 | +| `description` | string | ❌ | 描述信息 | +| `key_type` | string | ❌ | Key 类型:`regular`(默认)、`admin` | +| `server_id` | string | ❌ | 服务器 ID | + +**成功响应 - Regular Key** `201 Created` + +```json +{ + "regularKey": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "name": "My Server", + "description": "Production Minecraft server", + "keyPrefix": "mc_key_", + "keyType": "regular", + "serverId": "server-1", + "regularKeyId": null, + "createdAt": "2024-01-15T10:30:00.000Z", + "lastUsed": null, + "isActive": true, + "key": "mc_key_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6" + }, + "serverKey": { + "id": "660e8400-e29b-41d4-a716-446655440001", + "name": "My Server - Server Key", + "description": "Server Key for My Server", + "keyPrefix": "mc_server_", + "keyType": "server", + "serverId": "server-1", + "regularKeyId": "550e8400-e29b-41d4-a716-446655440000", + "createdAt": "2024-01-15T10:30:00.000Z", + "lastUsed": null, + "isActive": true, + "key": "mc_server_x1y2z3a4b5c6d7e8f9g0h1i2j3k4l5m6" + } +} +``` + +**成功响应 - Admin Key** `201 Created` + +```json +{ + "id": "770e8400-e29b-41d4-a716-446655440002", + "name": "New Admin", + "description": "Backup admin key", + "keyPrefix": "mc_admin_", + "keyType": "admin", + "serverId": null, + "regularKeyId": null, + "createdAt": "2024-01-15T10:30:00.000Z", + "lastUsed": null, + "isActive": true, + "key": "mc_admin_p1q2r3s4t5u6v7w8x9y0z1a2b3c4d5e6" +} +``` + +> ⚠️ **重要**: `key` 字段只在创建时返回一次,请妥善保存! + +--- + +### 获取 API Key 详情 + +获取指定 API Key 的详细信息。 + +**权限**: Admin Key + +**请求** + +```http +GET /manage/keys/:key_id +Authorization: Bearer +``` + +**成功响应** `200 OK` + +```json +{ + "id": "550e8400-e29b-41d4-a716-446655440000", + "name": "My Server Key", + "description": "Production server", + "keyPrefix": "mc_key_", + "keyType": "regular", + "serverId": "server-1", + "regularKeyId": null, + "createdAt": "2024-01-15T10:30:00.000Z", + "lastUsed": "2024-01-15T12:00:00.000Z", + "isActive": true +} +``` + +**错误响应** `404 Not Found` + +```json +{ + "detail": "API Key not found" +} +``` + +--- + +### 获取 Server Key 列表 + +获取当前用户可访问的 Server Key 列表。 + +**权限**: Regular Key 或 Admin Key + +**请求** + +```http +GET /manage/keys/server-keys +Authorization: Bearer +``` + +**成功响应** `200 OK` + +```json +[ + { + "id": "660e8400-e29b-41d4-a716-446655440001", + "name": "My Server - Server Key", + "description": "Server Key for My Server", + "keyPrefix": "mc_server_", + "keyType": "server", + "serverId": "server-1", + "createdAt": "2024-01-15T10:30:00.000Z", + "lastUsed": "2024-01-15T12:00:00.000Z", + "isActive": true + } +] +``` + +- **Admin Key**: 返回所有 Server Key +- **Regular Key**: 只返回关联的 Server Key + +--- + +### 创建 Server Key + +为指定的 Regular Key 创建新的 Server Key。 + +**权限**: Admin Key + +**请求** + +```http +POST /manage/keys/server-keys +Authorization: Bearer +Content-Type: application/json +``` + +**请求体** + +```json +{ + "name": "Backup Server Key", + "description": "Backup key for server-1", + "server_id": "server-1", + "regular_key_id": "550e8400-e29b-41d4-a716-446655440000" +} +``` + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `name` | string | ✅ | Key 名称 | +| `description` | string | ❌ | 描述信息 | +| `server_id` | string | ❌ | 服务器 ID | +| `regular_key_id` | string | ✅ | 关联的 Regular Key ID | + +**成功响应** `201 Created` + +```json +{ + "id": "880e8400-e29b-41d4-a716-446655440003", + "name": "Backup Server Key", + "description": "Backup key for server-1", + "keyPrefix": "mc_server_", + "keyType": "server", + "serverId": "server-1", + "regularKeyId": "550e8400-e29b-41d4-a716-446655440000", + "createdAt": "2024-01-15T10:30:00.000Z", + "lastUsed": null, + "isActive": true, + "key": "mc_server_n1o2p3q4r5s6t7u8v9w0x1y2z3a4b5c6" +} +``` + +--- + +### 激活 API Key + +激活指定的 API Key。 + +**权限**: +- Admin Key: 可激活任意 Key +- Regular Key: 只能激活关联的 Server Key + +**请求** + +```http +PATCH /manage/keys/:key_id/activate +Authorization: Bearer +``` + +**成功响应** `200 OK` + +```json +{ + "message": "Key '550e8400-e29b-41d4-a716-446655440000' activated." +} +``` + +--- + +### 停用 API Key + +停用指定的 API Key。 + +**权限**: +- Admin Key: 可停用任意 Key(不能停用最后一个活跃的 Admin Key) +- Regular Key: 只能停用关联的 Server Key + +**请求** + +```http +PATCH /manage/keys/:key_id/deactivate +Authorization: Bearer +``` + +**成功响应** `200 OK` + +```json +{ + "message": "Key '550e8400-e29b-41d4-a716-446655440000' deactivated." +} +``` + +**错误响应** `400 Bad Request` + +```json +{ + "detail": "Cannot deactivate your own key." +} +``` + +```json +{ + "detail": "Cannot deactivate last active Admin Key." +} +``` + +--- + +### 删除 API Key + +永久删除指定的 API Key。 + +**权限**: Admin Key + +**请求** + +```http +DELETE /manage/keys/:key_id +Authorization: Bearer +``` + +**成功响应** `204 No Content` + +无响应体 + +**错误响应** `400 Bad Request` + +```json +{ + "detail": "Cannot delete your own key." +} +``` + +```json +{ + "detail": "Cannot delete the last Admin Key" +} +``` + +--- + +## 事件接口 + +**基础路径**: `/api/events` + +### 发送事件 + +发送 Minecraft 事件并广播给所有 WebSocket 连接。 + +**权限**: 任意有效 API Key + +**请求** + +```http +POST /api/events +Authorization: Bearer +Content-Type: application/json +``` + +**请求体** + +```json +{ + "event_type": "player_join", + "server_name": "server-1", + "timestamp": "2024-01-15T10:30:00.000Z", + "data": { + "player_name": "Steve", + "player_uuid": "550e8400-e29b-41d4-a716-446655440000" + } +} +``` + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `event_type` | string | ✅ | 事件类型 | +| `server_name` | string | ✅ | 服务器名称 | +| `timestamp` | string | ✅ | 事件时间戳 (ISO 8601) | +| `data` | object | ✅ | 事件数据 | + +**支持的事件类型** + +| 事件类型 | 说明 | +|----------|------| +| `player_join` | 玩家加入服务器 | +| `player_leave` | 玩家离开服务器 | +| `message` | 聊天消息 | +| `server_command` | 服务器命令 | +| `ai_chat` | AI 聊天 | + +**成功响应** `200 OK` + +```json +{ + "message": "Event received and broadcasted" +} +``` + +**错误响应** `400 Bad Request` + +```json +{ + "detail": "Missing required event fields" +} +``` + +```json +{ + "detail": "Invalid event_type: unknown_event" +} +``` + +--- + +## 服务器接口 + +**基础路径**: `/api/server` + +### 获取服务器信息 + +获取 Minecraft 服务器信息。 + +**权限**: Regular Key 或 Admin Key + +**请求** + +```http +GET /api/server/info +Authorization: Bearer +``` + +**查询参数**(仅 Admin Key) + +| 参数 | 类型 | 说明 | +|------|------|------| +| `server_id` | string | 指定服务器 ID | + +**成功响应** `200 OK` + +```json +{ + "server_id": "server-1", + "status": "running", + "online_players": 5, + "max_players": 20, + "version": "1.20.1", + "uptime": "2h 30m", + "tps": 19.8 +} +``` + +**错误响应** `400 Bad Request` + +```json +{ + "detail": "server_id is required for this key" +} +``` + +--- + +### 发送服务器命令 + +向 Minecraft 服务器发送命令。 + +**权限**: Regular Key 或 Admin Key + +**请求** + +```http +POST /api/server/command +Authorization: Bearer +Content-Type: application/json +``` + +**请求体** + +```json +{ + "command": "say Hello World", + "server_id": "server-1" +} +``` + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `command` | string | ✅ | 要执行的命令 | +| `server_id` | string | ❌ | 服务器 ID(Admin Key 可指定) | + +**成功响应** `200 OK` + +```json +{ + "message": "Command sent successfully", + "command": "say Hello World", + "server_id": "server-1" +} +``` + +**错误响应** `400 Bad Request` + +```json +{ + "detail": "Command is required" +} +``` + +**错误响应** `403 Forbidden` + +```json +{ + "detail": "Command is not allowed for Admin Key" +} +``` + +**Admin Key 默认禁止的命令** + +以下命令默认禁止 Admin Key 执行(可通过环境变量配置): + +- `stop`, `restart`, `reload` +- `op`, `deop` +- `ban`, `ban-ip`, `banlist`, `pardon`, `pardon-ip` +- `whitelist`, `kick` +- `save-all`, `save-on`, `save-off` + +--- + +## WebSocket 接口 + +### 连接 WebSocket + +建立 WebSocket 连接以接收实时事件。 + +**端点** + +``` +ws://:/ws?api_key= +``` + +**示例** + +``` +ws://localhost:8000/ws?api_key=mc_server_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6 +``` + +**连接成功后** + +服务器会推送 Minecraft 事件消息: + +```json +{ + "type": "minecraft_event", + "event": { + "event_type": "player_join", + "server_name": "server-1", + "timestamp": "2024-01-15T10:30:00.000Z", + "data": { + "player_name": "Steve" + } + }, + "source_key_id_prefix": "550e8400" +} +``` + +### 心跳检测 + +客户端可以发送 ping 消息保持连接: + +**发送** + +```json +{ + "type": "ping" +} +``` + +**响应** + +```json +{ + "type": "pong" +} +``` + +--- + +## 根路径 + +### 获取服务器信息 + +**请求** + +```http +GET / +``` + +**成功响应** `200 OK` + +```json +{ + "message": "Minecraft WebSocket API Server (Node.js)", + "dashboard": "/dashboard", + "websocket": "ws://localhost:8000/ws", + "version": "1.0.0" +} +``` + +--- + +## 错误码参考 + +| HTTP 状态码 | 说明 | +|-------------|------| +| 200 | 请求成功 | +| 201 | 创建成功 | +| 204 | 删除成功(无内容) | +| 400 | 请求参数错误 | +| 401 | 未认证或 API Key 无效 | +| 403 | 权限不足 | +| 404 | 资源不存在 | +| 500 | 服务器内部错误 | + +--- + +## 环境变量配置 + +| 变量名 | 默认值 | 说明 | +|--------|--------|------| +| `SERVER_HOST` | `0.0.0.0` | 服务器监听地址 | +| `SERVER_PORT` | `8000` | 服务器端口 | +| `DATABASE_PATH` | `minecraft_ws.db` | 数据库文件路径 | +| `DASHBOARD_PORT` | `3000` | Dashboard 独立端口(可选) | +| `ADMIN_ALLOWED_COMMANDS` | - | Admin Key 允许的命令(逗号分隔) | +| `ADMIN_BLOCKED_COMMANDS` | - | Admin Key 禁止的命令(逗号分隔) | diff --git a/hf_repo/hf_repo/hf_repo/hf_repo/docs/TECH_STACK.md b/hf_repo/hf_repo/hf_repo/hf_repo/docs/TECH_STACK.md new file mode 100644 index 0000000000000000000000000000000000000000..26bba4e2664b0baaf66c43dc2d1360140540a43c --- /dev/null +++ b/hf_repo/hf_repo/hf_repo/hf_repo/docs/TECH_STACK.md @@ -0,0 +1,149 @@ +# 项目技术栈文档 + +本文件详细说明 InterConnect-Server 的技术组成、模块职责与关键依赖版本。 + +## 1. 运行环境 +- **Node.js**: 建议 v18+(Docker 镜像使用 `node:18-alpine`) +- **包管理**: npm +- **平台**: Windows / Linux / macOS + +## 2. 核心依赖与用途 + +### 2.1 后端服务 +- **express@4.18.x** + - REST API 服务框架 + - 路由结构:`src/routes/*.js` + - 入口:`src/server.js` + +- **ws@8.14.x** + - WebSocket 服务端实现 + - 协议:`/ws?api_key=...` + - 连接管理:`src/websocket.js` + +- **sql.js@1.10.x** + - SQLite 在内存中运行并导出文件 + - 本地持久化文件:`minecraft_ws.db` + - 初始化与表结构:`src/database.js` + +- **bcryptjs@2.4.x** + - Key 哈希存储 + - 认证校验:`db.verifyApiKey()` + - 恢复 Token 哈希:`admin_recovery` 表 + +- **uuid@9.0.x** + - 生成密钥 ID 与原始 Key + +- **dotenv@16.3.x** + - 读取环境变量 + - 用于命令过滤与数据库路径等配置 + +### 2.2 CLI 工具 +- **commander@11.1.x** + - CLI 命令定义 + - 命令入口:`cli/cli.js` + - 支持管理 Key / health / reset-admin + +- **http / https (Node 内置)** + - CLI 直接调用 API + +### 2.3 前端 Dashboard +纯静态页面,无前端框架。 +- `dashboard/public/index.html` +- `dashboard/public/app.js` +- `dashboard/public/style.css` + +## 3. 关键模块职责 + +### 3.1 `src/server.js` +- 服务入口 +- REST API 路由挂载 +- WebSocket 升级与连接管理 +- 初始化数据库与 Admin Key / Recovery Token + +### 3.2 `src/database.js` +- SQLite 初始化与持久化 +- Key 创建 / 激活 / 停用 / 删除 +- Admin Recovery Token 管理 +- 事件日志写入 + +### 3.3 `src/auth.js` +- Bearer Token 认证中间件 +- Admin / Regular 权限判断 + +### 3.4 `src/routes/keys.js` +- Key CRUD +- `/manage/keys/server-keys` 列表与激活/停用逻辑 + +### 3.5 `src/routes/server.js` +- 服务器信息与命令入口 +- Admin 命令过滤(可配置 allowlist / blocklist) + +### 3.6 `src/websocket.js` +- WebSocket 连接管理 +- 广播消息 + +## 4. 数据结构 + +### 4.1 `api_keys` +| 字段 | 类型 | 说明 | +|---|---|---| +| id | TEXT | 主键 | +| name | TEXT | Key 名称 | +| description | TEXT | 描述 | +| key_hash | TEXT | bcrypt 哈希 | +| key_prefix | TEXT | mc_admin_ / mc_key_ / mc_server_ | +| key_type | TEXT | admin / regular / server | +| server_id | TEXT | 关联服务器 | +| regular_key_id | TEXT | 关联 Regular Key | +| created_at | TEXT | 创建时间 | +| last_used | TEXT | 最近使用 | +| is_active | INTEGER | 是否启用 | + +### 4.2 `event_logs` +事件上报记录。 + +### 4.3 `admin_recovery` +| 字段 | 类型 | 说明 | +|---|---|---| +| id | INTEGER | 固定为 1 | +| token_hash | TEXT | 恢复 Token 哈希 | +| created_at | TEXT | 创建时间 | +| last_used | TEXT | 最近使用 | + +## 5. 权限与安全策略 +- Admin Key:完整管理权限 +- Regular Key:只允许操作自身 Server Key +- Server Key:仅用于插件/Mod,不允许登录 Dashboard +- Admin 命令过滤: + - `ADMIN_ALLOWED_COMMANDS` 非空时为 allowlist + - 否则使用 `ADMIN_BLOCKED_COMMANDS` 作为默认拦截表 + +## 6. 部署与运行 + +### 6.1 直接运行 +```bash +npm install +npm start +``` + +### 6.2 Docker +```bash +docker-compose up -d +``` + +### 6.3 数据持久化 +必须保留 `minecraft_ws.db`,否则 Admin Key 将重新生成。 + +## 7. 关键配置项 +```env +SERVER_HOST=0.0.0.0 +SERVER_PORT=8000 +DATABASE_PATH=minecraft_ws.db +ADMIN_ALLOWED_COMMANDS=list,tps,version +ADMIN_BLOCKED_COMMANDS=stop,deop,op,ban +``` + +## 8. 兼容与约束 +- WebSocket 与 REST 使用同一端口 +- Dashboard 只通过 `/dashboard` 提供 +- Admin Key 恢复必须使用 recovery token,禁止删除数据库 diff --git a/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/cli/cli.js b/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/cli/cli.js index d7414372fc6543db747504a7767b3686506d1a29..64fe6f2f1151520231979eed56e9a6dea43f327b 100644 --- a/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/cli/cli.js +++ b/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/cli/cli.js @@ -4,6 +4,7 @@ const http = require('http'); const https = require('https'); const fs = require('fs'); require('dotenv').config(); +const Database = require('../src/database'); const HARDCODED_API_URL = 'http://localhost:8000'; const HARDCODED_ADMIN_KEY = process.env.ADMIN_KEY || null; @@ -386,4 +387,35 @@ program } }); +program + .command('reset-admin') + .description('Reset Admin Key using recovery token (offline)') + .requiredOption('-r, --recovery-token ', 'Admin recovery token') + .option('--db-path ', 'Database path', process.env.DATABASE_PATH || 'minecraft_ws.db') + .option('--keep-existing', 'Keep existing Admin Keys active') + .option('--name ', 'New Admin Key name', 'Recovered Admin Key') + .action(async (options) => { + try { + if (!fs.existsSync(options.dbPath)) { + throw new Error(`Database not found at ${options.dbPath}`); + } + + const db = new Database(options.dbPath); + await db.init(); + + const result = await db.resetAdminKeyWithRecovery(options.recoveryToken, { + deactivateExisting: !options.keepExisting, + name: options.name + }); + + console.log('Admin Key reset successful.'); + console.log(` ID : ${result.id}`); + console.log(` Name : ${result.name}`); + console.log(` Key : ${result.key}`); + } catch (error) { + console.error(`\x1b[31mReset failed: ${error.message}\x1b[0m`); + process.exit(1); + } + }); + program.parse(); diff --git a/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/dashboard/public/app.js b/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/dashboard/public/app.js index e0e3691de8b476519e08bf58d2b97a137ffc488d..7deb119f5cb32b9b66fa0b3a8e26db7225937ab4 100644 --- a/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/dashboard/public/app.js +++ b/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/dashboard/public/app.js @@ -1,402 +1,399 @@ const API_URL = window.location.origin; -let superKey = null; +let apiKey = null; +let currentRole = null; let ws = null; +let statsIntervalId = null; +let wsPingIntervalId = null; const loginScreen = document.getElementById('login-screen'); -const dashboardScreen = document.getElementById('dashboard-screen'); +const adminScreen = document.getElementById('admin-screen'); +const userScreen = document.getElementById('user-screen'); const loginForm = document.getElementById('login-form'); const loginError = document.getElementById('login-error'); -const logoutBtn = document.getElementById('logout-btn'); +const apiKeyInput = document.getElementById('api-key-input'); +const logoutButtons = document.querySelectorAll('.logout-btn'); +const adminUserInfo = document.getElementById('admin-user-info'); +const userInfo = document.getElementById('user-info'); const createKeyBtn = document.getElementById('create-key-btn'); const createKeyModal = document.getElementById('create-key-modal'); const createKeyForm = document.getElementById('create-key-form'); const cancelCreateBtn = document.getElementById('cancel-create-btn'); const keyDetailsModal = document.getElementById('key-details-modal'); const closeDetailsBtn = document.getElementById('close-details-btn'); +const adminKeysList = document.getElementById('admin-keys-list'); +const userKeysList = document.getElementById('user-keys-list'); +const commandForm = document.getElementById('command-form'); +const commandInput = document.getElementById('command-input'); +const commandHistory = document.getElementById('command-history'); +const userEventsList = document.getElementById('user-events-list'); -loginForm.addEventListener('submit', async (e) => { - e.preventDefault(); - const key = document.getElementById('super-key-input').value; - - try { - // 验证密钥是否有效 - const response = await fetch(`${API_URL}/health`, { - headers: { 'Authorization': `Bearer ${key}` } - }); - - if (response.ok) { - superKey = key; - - // 验证密钥类型 - let userKeyType = null; - let userServerId = null; - - // 尝试Admin Key验证 - const adminResponse = await fetch(`${API_URL}/manage/keys`, { - headers: { 'Authorization': `Bearer ${key}` } - }); - - if (adminResponse.ok) { - userKeyType = 'admin'; - } else { - // 尝试Server Key验证 - const serverResponse = await fetch(`${API_URL}/api/server/info`, { - headers: { 'Authorization': `Bearer ${key}` } - }); - - if (serverResponse.ok) { - userKeyType = 'server'; - const serverData = await serverResponse.json(); - userServerId = serverData.server_id; - } else { - // 尝试Regular Key验证(获取自己的Server Key列表) - const regularResponse = await fetch(`${API_URL}/manage/keys/server-keys`, { - headers: { 'Authorization': `Bearer ${key}` } - }); - - if (regularResponse.ok) { - userKeyType = 'regular'; - } else { - throw new Error('无效的密钥或权限不足'); - } - } - } - - loginError.textContent = ''; - showDashboard(userKeyType, userServerId); - } else { - loginError.textContent = '无效的密钥'; - } - } catch (error) { - loginError.textContent = error.message || '无法连接到服务器'; +function authHeaders(key) { + return { Authorization: `Bearer ${key}` }; +} + +function resetSession() { + apiKey = null; + currentRole = null; + + if (statsIntervalId) { + clearInterval(statsIntervalId); + statsIntervalId = null; + } + + if (wsPingIntervalId) { + clearInterval(wsPingIntervalId); + wsPingIntervalId = null; } -}); -logoutBtn.addEventListener('click', () => { - superKey = null; if (ws) { ws.close(); ws = null; } - loginScreen.classList.remove('hidden'); - dashboardScreen.classList.add('hidden'); -}); -createKeyBtn.addEventListener('click', () => { - createKeyModal.classList.remove('hidden'); -}); + if (loginScreen) { + loginScreen.classList.remove('hidden'); + } + if (adminScreen) { + adminScreen.classList.add('hidden'); + } + if (userScreen) { + userScreen.classList.add('hidden'); + } +} -cancelCreateBtn.addEventListener('click', () => { - createKeyModal.classList.add('hidden'); - createKeyForm.reset(); -}); +async function detectRole(key) { + const adminResponse = await fetch(`${API_URL}/manage/keys`, { + headers: authHeaders(key) + }); -closeDetailsBtn.addEventListener('click', () => { - keyDetailsModal.classList.add('hidden'); -}); + if (adminResponse.ok) { + return 'admin'; + } -createKeyForm.addEventListener('submit', async (e) => { - e.preventDefault(); - - const name = document.getElementById('key-name').value; - const description = document.getElementById('key-description').value; - const isSuper = document.getElementById('key-is-super').checked; - - try { - const response = await fetch(`${API_URL}/manage/keys`, { - method: 'POST', - headers: { - 'Authorization': `Bearer ${superKey}`, - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - name, - description, - is_super_key: isSuper - }) - }); - - if (response.ok) { - const result = await response.json(); - createKeyModal.classList.add('hidden'); - createKeyForm.reset(); - - showKeyCreatedModal(result); - loadKeys(); - } else { - const error = await response.json(); - alert('创建失败: ' + error.detail); - } - } catch (error) { - alert('创建失败: ' + error.message); + const regularResponse = await fetch(`${API_URL}/manage/keys/server-keys`, { + headers: authHeaders(key) + }); + + if (regularResponse.ok) { + return 'regular'; } -}); -function showKeyCreatedModal(keyData) { - const content = ` -

密钥创建成功!

-

名称: ${keyData.name}

-

类型: ${keyData.isSuperKey ? 'SuperKey' : '普通密钥'}

-
- ⚠️ 请立即复制并保存此密钥(仅显示一次):
- ${keyData.key} -
- `; - - document.getElementById('key-details-content').innerHTML = content; - keyDetailsModal.classList.remove('hidden'); + return null; } -// 全局变量 -let currentUserKeyType = null; -let currentUserServerId = null; +function showAdminPanel() { + currentRole = 'admin'; + loginScreen.classList.add('hidden'); + userScreen.classList.add('hidden'); + adminScreen.classList.remove('hidden'); + + if (adminUserInfo) { + adminUserInfo.textContent = 'Admin Key'; + } + + loadAdminKeys(); +} -async function showDashboard(userKeyType, userServerId) { - currentUserKeyType = userKeyType; - currentUserServerId = userServerId; - +function showUserPanel() { + currentRole = 'regular'; loginScreen.classList.add('hidden'); - dashboardScreen.classList.remove('hidden'); - - // 显示用户信息 - const userInfo = document.getElementById('user-info'); - const keyTypeIcons = { admin: '👑', server: '🖥️', regular: '🔑' }; - const keyTypeNames = { admin: 'Admin', server: 'Server', regular: 'Regular' }; - userInfo.textContent = `${keyTypeIcons[userKeyType]} ${keyTypeNames[userKeyType]} 用户`; - - // 根据权限显示/隐藏功能 - const adminSection = document.getElementById('admin-section'); - const serverSection = document.getElementById('server-section'); - - if (userKeyType === 'admin') { - adminSection.style.display = 'block'; - serverSection.style.display = 'block'; - loadKeys(); - } else if (userKeyType === 'server') { - adminSection.style.display = 'none'; - serverSection.style.display = 'block'; - loadServerInfo(); - } else if (userKeyType === 'regular') { - adminSection.style.display = 'none'; - serverSection.style.display = 'block'; - loadRegularServerKeys(); - } - - // 根据用户类型控制创建密钥按钮的显示 - const createKeyBtn = document.getElementById('create-key-btn'); - if (createKeyBtn) { - createKeyBtn.style.display = userKeyType === 'admin' ? 'block' : 'none'; - } - + adminScreen.classList.add('hidden'); + userScreen.classList.remove('hidden'); + + if (userInfo) { + userInfo.textContent = 'Regular Key'; + } + + loadUserServerKeys(); loadStats(); connectWebSocket(); - - setInterval(loadStats, 5000); -} -async function loadStats() { - try { - const response = await fetch(`${API_URL}/health`); - const data = await response.json(); - - document.getElementById('stat-connections').textContent = data.active_ws || 0; - document.getElementById('stat-total-keys').textContent = data.keys_total || 0; - document.getElementById('stat-super-keys').textContent = data.super_active || 0; - document.getElementById('stat-regular-keys').textContent = data.regular_active || 0; - } catch (error) { - console.error('Failed to load stats:', error); - } + statsIntervalId = setInterval(loadStats, 5000); } -async function loadKeys() { +async function loadAdminKeys() { + if (!adminKeysList || !apiKey) { + return; + } try { const response = await fetch(`${API_URL}/manage/keys`, { - headers: { 'Authorization': `Bearer ${superKey}` } + headers: authHeaders(apiKey) }); - + if (response.ok) { const keys = await response.json(); - renderKeys(keys); + renderAdminKeys(keys); + } else { + adminKeysList.innerHTML = '

Failed to load keys.

'; } } catch (error) { - console.error('Failed to load keys:', error); + adminKeysList.innerHTML = `

Failed to load keys: ${error.message}

`; } } -async function loadRegularServerKeys() { +async function loadUserServerKeys() { + if (!userKeysList || !apiKey) { + return; + } try { const response = await fetch(`${API_URL}/manage/keys/server-keys`, { - headers: { 'Authorization': `Bearer ${superKey}` } + headers: authHeaders(apiKey) }); - + if (response.ok) { - const serverKeys = await response.json(); - renderServerKeys(serverKeys); + const keys = await response.json(); + renderUserServerKeys(keys); } else { - document.getElementById('keys-list').innerHTML = '

无法加载Server Key列表

'; + userKeysList.innerHTML = '

Failed to load server keys.

'; } } catch (error) { - console.error('Failed to load server keys:', error); - document.getElementById('keys-list').innerHTML = '

无法加载Server Key列表

'; + userKeysList.innerHTML = `

Failed to load server keys: ${error.message}

`; } } -function renderServerKeys(keys) { - const keysList = document.getElementById('keys-list'); - - if (keys.length === 0) { - keysList.innerHTML = '

暂无关联的Server Key

'; +function renderAdminKeys(keys) { + if (!adminKeysList) { return; } - - keysList.innerHTML = keys.map(key => ` -
+ + if (!keys.length) { + adminKeysList.innerHTML = '

No API keys found.

'; + return; + } + + adminKeysList.innerHTML = keys.map((key) => ` +

- 🖥️ Server + + ${key.keyType === 'admin' ? 'Admin' : key.keyType === 'server' ? 'Server' : 'Regular'} + - ${key.isActive ? '活跃' : '已停用'} + ${key.isActive ? 'Active' : 'Inactive'} ${key.name}

ID: ${key.id}

-

前缀: ${key.keyPrefix}

- ${key.serverId ? `

服务器ID: ${key.serverId}

` : ''} -

创建时间: ${new Date(key.createdAt).toLocaleString('zh-CN')}

-

最后使用: ${key.lastUsed ? new Date(key.lastUsed).toLocaleString('zh-CN') : '从未使用'}

+

Prefix: ${key.keyPrefix}

+ ${key.serverId ? `

Server ID: ${key.serverId}

` : ''} +

Created: ${new Date(key.createdAt).toLocaleString()}

+

Last Used: ${key.lastUsed ? new Date(key.lastUsed).toLocaleString() : 'Never'}

- ${key.isActive ? - `` : - `` + ${key.isActive + ? `` + : `` } - +
`).join(''); } -function renderKeys(keys) { - const keysList = document.getElementById('keys-list'); - - if (keys.length === 0) { - keysList.innerHTML = '

暂无API密钥

'; +function renderUserServerKeys(keys) { + if (!userKeysList) { return; } - - keysList.innerHTML = keys.map(key => ` -
+ + if (!keys.length) { + userKeysList.innerHTML = '

No server keys available.

'; + return; + } + + userKeysList.innerHTML = keys.map((key) => ` +

- - ${key.keyType === 'admin' ? '👑 Admin' : key.keyType === 'server' ? '🖥️ Server' : '🔑 Regular'} - + Server - ${key.isActive ? '活跃' : '已停用'} + ${key.isActive ? 'Active' : 'Inactive'} ${key.name}

ID: ${key.id}

-

前缀: ${key.keyPrefix}

- ${key.serverId ? `

服务器ID: ${key.serverId}

` : ''} -

创建时间: ${new Date(key.createdAt).toLocaleString('zh-CN')}

-

最后使用: ${key.lastUsed ? new Date(key.lastUsed).toLocaleString('zh-CN') : '从未使用'}

+

Prefix: ${key.keyPrefix}

+ ${key.serverId ? `

Server ID: ${key.serverId}

` : ''} +

Created: ${new Date(key.createdAt).toLocaleString()}

+

Last Used: ${key.lastUsed ? new Date(key.lastUsed).toLocaleString() : 'Never'}

- ${key.isActive ? - `` : - `` + ${key.isActive + ? `` + : `` } -
`).join(''); } async function activateKey(keyId) { + if (!apiKey) { + return; + } try { const response = await fetch(`${API_URL}/manage/keys/${keyId}/activate`, { method: 'PATCH', - headers: { 'Authorization': `Bearer ${superKey}` } + headers: authHeaders(apiKey) }); - - if (response.ok) { - if (currentUserKeyType === 'admin') { - loadKeys(); - } else if (currentUserKeyType === 'regular') { - loadRegularServerKeys(); - } - } else { + + if (!response.ok) { const error = await response.json(); - alert('激活失败: ' + error.detail); + alert(`Failed to activate key: ${error.detail}`); + return; + } + + if (currentRole === 'admin') { + loadAdminKeys(); + } else if (currentRole === 'regular') { + loadUserServerKeys(); } } catch (error) { - alert('激活失败: ' + error.message); + alert(`Failed to activate key: ${error.message}`); } } async function deactivateKey(keyId) { + if (!apiKey) { + return; + } try { const response = await fetch(`${API_URL}/manage/keys/${keyId}/deactivate`, { method: 'PATCH', - headers: { 'Authorization': `Bearer ${superKey}` } + headers: authHeaders(apiKey) }); - - if (response.ok) { - if (currentUserKeyType === 'admin') { - loadKeys(); - } else if (currentUserKeyType === 'regular') { - loadRegularServerKeys(); - } - } else { + + if (!response.ok) { const error = await response.json(); - alert('停用失败: ' + error.detail); + alert(`Failed to deactivate key: ${error.detail}`); + return; + } + + if (currentRole === 'admin') { + loadAdminKeys(); + } else if (currentRole === 'regular') { + loadUserServerKeys(); } } catch (error) { - alert('停用失败: ' + error.message); + alert(`Failed to deactivate key: ${error.message}`); } } async function deleteKey(keyId, keyName) { - if (!confirm(`确定要删除密钥 "${keyName}" 吗?此操作无法撤销。`)) { + if (currentRole !== 'admin') { + return; + } + if (!confirm(`Delete key "${keyName}"? This action cannot be undone.`)) { return; } - try { const response = await fetch(`${API_URL}/manage/keys/${keyId}`, { method: 'DELETE', - headers: { 'Authorization': `Bearer ${superKey}` } + headers: authHeaders(apiKey) }); - - if (response.ok) { - if (currentUserKeyType === 'admin') { - loadKeys(); - } else if (currentUserKeyType === 'regular') { - loadRegularServerKeys(); - } - } else { + + if (!response.ok) { const error = await response.json(); - alert('删除失败: ' + error.detail); + alert(`Failed to delete key: ${error.detail}`); + return; } + + loadAdminKeys(); } catch (error) { - alert('删除失败: ' + error.message); + alert(`Failed to delete key: ${error.message}`); + } +} + +function showKeyCreatedModal(payload) { + if (!keyDetailsModal) { + return; + } + + let content = ''; + if (payload.regularKey && payload.serverKey) { + content = ` +

Regular Key

+

Name: ${payload.regularKey.name}

+

Type: ${payload.regularKey.keyType}

+

ID: ${payload.regularKey.id}

+

Key: ${payload.regularKey.key}

+
+

Server Key

+

Name: ${payload.serverKey.name}

+

Type: ${payload.serverKey.keyType}

+

ID: ${payload.serverKey.id}

+

Key: ${payload.serverKey.key}

+ `; + } else { + content = ` +

Key Created

+

Name: ${payload.name}

+

Type: ${payload.keyType}

+

ID: ${payload.id}

+

Key: ${payload.key}

+ `; + } + + const detailsContent = document.getElementById('key-details-content'); + if (detailsContent) { + detailsContent.innerHTML = content; + } + + keyDetailsModal.classList.remove('hidden'); +} + +async function loadStats() { + try { + const response = await fetch(`${API_URL}/health`); + if (!response.ok) { + return; + } + const data = await response.json(); + + const connections = document.getElementById('user-stat-connections'); + if (connections) { + connections.textContent = data.active_ws || 0; + } + + const totalKeys = document.getElementById('user-stat-total-keys'); + if (totalKeys) { + totalKeys.textContent = data.keys_total || 0; + } + + const adminKeys = document.getElementById('user-stat-admin-keys'); + if (adminKeys) { + adminKeys.textContent = data.admin_active || 0; + } + + const regularKeys = document.getElementById('user-stat-regular-keys'); + if (regularKeys) { + regularKeys.textContent = data.regular_active || 0; + } + + const serverKeys = document.getElementById('user-stat-server-keys'); + if (serverKeys) { + serverKeys.textContent = data.server_active || 0; + } + } catch (error) { + console.error('Failed to load stats:', error); } } function connectWebSocket() { - if (!superKey) return; - - const wsUrl = `ws://localhost:8000/ws?api_key=${superKey}`; + if (!apiKey) { + return; + } + + const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws'; + const wsUrl = `${protocol}://${window.location.host}/ws?api_key=${apiKey}`; ws = new WebSocket(wsUrl); - + ws.onopen = () => { console.log('WebSocket connected'); }; - + ws.onmessage = (event) => { try { const message = JSON.parse(event.data); - if (message.type === 'minecraft_event') { addEventToList(message.event); } @@ -404,21 +401,23 @@ function connectWebSocket() { console.error('Failed to parse WebSocket message:', error); } }; - + ws.onerror = (error) => { console.error('WebSocket error:', error); }; - + ws.onclose = () => { console.log('WebSocket disconnected'); - setTimeout(() => { - if (superKey) { - connectWebSocket(); - } - }, 5000); + if (wsPingIntervalId) { + clearInterval(wsPingIntervalId); + wsPingIntervalId = null; + } + if (apiKey && currentRole === 'regular') { + setTimeout(() => connectWebSocket(), 5000); + } }; - - setInterval(() => { + + wsPingIntervalId = setInterval(() => { if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'ping' })); } @@ -426,19 +425,183 @@ function connectWebSocket() { } function addEventToList(event) { - const eventsList = document.getElementById('events-list'); - + if (!userEventsList) { + return; + } + const eventItem = document.createElement('div'); eventItem.className = 'event-item'; eventItem.innerHTML = ` ${event.event_type} - ${event.server_name}
- ${new Date(event.timestamp).toLocaleString('zh-CN')}
+ ${new Date(event.timestamp).toLocaleString()}
${JSON.stringify(event.data, null, 2)}
`; - - eventsList.insertBefore(eventItem, eventsList.firstChild); - - while (eventsList.children.length > 50) { - eventsList.removeChild(eventsList.lastChild); + + userEventsList.insertBefore(eventItem, userEventsList.firstChild); + + while (userEventsList.children.length > 50) { + userEventsList.removeChild(userEventsList.lastChild); + } +} + +function appendCommandHistory(command, status, detail) { + if (!commandHistory) { + return; } + + const entry = document.createElement('div'); + entry.className = 'command-item'; + entry.innerHTML = ` +
${status}: ${command}
+ ${detail ? `
${detail}
` : ''} + `; + commandHistory.insertBefore(entry, commandHistory.firstChild); + + while (commandHistory.children.length > 20) { + commandHistory.removeChild(commandHistory.lastChild); + } +} + +if (loginForm) { + loginForm.addEventListener('submit', async (event) => { + event.preventDefault(); + const key = apiKeyInput.value.trim(); + loginError.textContent = ''; + + if (!key) { + loginError.textContent = 'API key is required.'; + return; + } + + try { + const role = await detectRole(key); + if (!role) { + loginError.textContent = 'Invalid key or insufficient permissions.'; + return; + } + + apiKey = key; + + if (role === 'admin') { + showAdminPanel(); + } else if (role === 'regular') { + showUserPanel(); + } + } catch (error) { + loginError.textContent = error.message || 'Unable to connect to server.'; + } + }); +} + +logoutButtons.forEach((button) => { + button.addEventListener('click', () => { + resetSession(); + }); +}); + +if (createKeyBtn) { + createKeyBtn.addEventListener('click', () => { + createKeyModal.classList.remove('hidden'); + }); } + +if (cancelCreateBtn) { + cancelCreateBtn.addEventListener('click', () => { + createKeyModal.classList.add('hidden'); + createKeyForm.reset(); + }); +} + +if (closeDetailsBtn) { + closeDetailsBtn.addEventListener('click', () => { + keyDetailsModal.classList.add('hidden'); + }); +} + +if (createKeyForm) { + createKeyForm.addEventListener('submit', async (event) => { + event.preventDefault(); + + const name = document.getElementById('key-name').value.trim(); + const description = document.getElementById('key-description').value.trim(); + const keyType = document.getElementById('key-type').value; + const serverId = document.getElementById('key-server-id').value.trim(); + + if (!name) { + alert('Name is required.'); + return; + } + + try { + const payload = { + name, + description, + key_type: keyType + }; + + if (serverId) { + payload.server_id = serverId; + } + + const response = await fetch(`${API_URL}/manage/keys`, { + method: 'POST', + headers: { + ...authHeaders(apiKey), + 'Content-Type': 'application/json' + }, + body: JSON.stringify(payload) + }); + + if (!response.ok) { + const error = await response.json(); + alert(`Failed to create key: ${error.detail}`); + return; + } + + const result = await response.json(); + createKeyModal.classList.add('hidden'); + createKeyForm.reset(); + showKeyCreatedModal(result); + loadAdminKeys(); + } catch (error) { + alert(`Failed to create key: ${error.message}`); + } + }); +} + +if (commandForm) { + commandForm.addEventListener('submit', async (event) => { + event.preventDefault(); + + const command = commandInput.value.trim(); + if (!command) { + return; + } + + try { + const response = await fetch(`${API_URL}/api/server/command`, { + method: 'POST', + headers: { + ...authHeaders(apiKey), + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ command }) + }); + + if (!response.ok) { + const error = await response.json(); + appendCommandHistory(command, 'Rejected', error.detail); + return; + } + + appendCommandHistory(command, 'Sent', new Date().toLocaleString()); + commandInput.value = ''; + } catch (error) { + appendCommandHistory(command, 'Error', error.message); + } + }); +} + +window.activateKey = activateKey; +window.deactivateKey = deactivateKey; +window.deleteKey = deleteKey; diff --git a/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/dashboard/public/index.html b/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/dashboard/public/index.html index 2ed9581a917c042f3952a4aed8317e60d0cc5bfb..0eb58abe126ca99298672aaa6b5b170ee582a56d 100644 --- a/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/dashboard/public/index.html +++ b/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/dashboard/public/index.html @@ -1,88 +1,125 @@ - + - Minecraft WebSocket API - 控制面板 + Minecraft WebSocket API - Dashboard
-