GitHub Actions commited on
Commit
7afb7eb
·
1 Parent(s): c9b1033

Sync from GitHub (excluding README)

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. dashboard/public/index.html +43 -43
  2. hf_repo/cli/cli.js +9 -9
  3. hf_repo/dashboard/server.js +3 -3
  4. hf_repo/docs/protocol.md +38 -40
  5. hf_repo/hf_repo/docs/protocol.md +125 -0
  6. hf_repo/hf_repo/hf_repo/dashboard/public/app.js +178 -0
  7. hf_repo/hf_repo/hf_repo/dashboard/public/index.html +38 -0
  8. hf_repo/hf_repo/hf_repo/dashboard/public/style.css +85 -0
  9. hf_repo/hf_repo/hf_repo/docs/AI_API.md +415 -0
  10. hf_repo/hf_repo/hf_repo/docs/API.md +730 -0
  11. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/docs/TECH_STACK.md +149 -0
  12. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/cli/cli.js +32 -0
  13. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/dashboard/public/app.js +448 -285
  14. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/dashboard/public/index.html +78 -41
  15. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/dashboard/public/style.css +31 -6
  16. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/dashboard/server.js +4 -4
  17. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.github/scripts/ai_agent.py +48 -79
  18. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.github/workflows/ai-review.yaml +46 -116
  19. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.github/workflows/ai-review.yaml +116 -46
  20. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.github/scripts/ai_agent.py +86 -79
  21. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.github/workflows/ai-review.yaml +20 -116
  22. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.github/scripts/ai_agent.py +140 -0
  23. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.github/workflows/ai-review.yaml +1 -1
  24. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.github/workflows/ai-review.yaml +48 -42
  25. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.github/workflows/ai-review.yaml +71 -60
  26. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.github/workflows/ai-review.yaml +62 -24
  27. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.github/workflows/ai-review.yaml +87 -0
  28. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.github/workflows/push_hf.yaml +47 -0
  29. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.gitignore +59 -0
  30. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/Dockerfile +16 -0
  31. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/LICENSE +21 -0
  32. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/cli/cli.js +389 -0
  33. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/dashboard/public/app.js +444 -0
  34. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/dashboard/public/index.html +103 -0
  35. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/dashboard/public/style.css +410 -0
  36. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/dashboard/server.js +21 -0
  37. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/docker-compose.yml +14 -0
  38. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.gitattributes +2 -0
  39. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/package-lock.json +675 -0
  40. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/package.json +31 -0
  41. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/src/auth.js +83 -0
  42. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/src/database.js +367 -0
  43. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/src/routes/events.js +32 -0
  44. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/src/routes/health.js +27 -0
  45. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/src/routes/keys.js +195 -0
  46. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/src/routes/server.js +70 -0
  47. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/src/server.js +134 -0
  48. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/src/websocket.js +63 -0
  49. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/start-server.bat +3 -0
  50. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/start.sh +11 -0
dashboard/public/index.html CHANGED
@@ -1,71 +1,71 @@
1
  <!DOCTYPE html>
2
- <html lang="en">
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Minecraft WebSocket API - Dashboard</title>
7
  <link rel="stylesheet" href="style.css">
8
  </head>
9
  <body>
10
  <div id="login-screen" class="screen">
11
  <div class="login-container">
12
  <h1>Minecraft WebSocket API</h1>
13
- <h2>Dashboard Login</h2>
14
  <form id="login-form">
15
- <input type="password" id="api-key-input" placeholder="Enter Admin or Regular Key" required>
16
- <button type="submit">Login</button>
17
  </form>
18
  <p class="error-message" id="login-error"></p>
19
  <div class="login-info">
20
- <p><strong>Key Types</strong></p>
21
- <p>Admin Key - full management permissions</p>
22
- <p>Regular Key - server monitoring and command access</p>
23
  </div>
24
  </div>
25
  </div>
26
 
27
  <div id="admin-screen" class="screen hidden">
28
  <nav class="navbar">
29
- <h1>Admin Panel</h1>
30
  <div class="nav-info">
31
  <span id="admin-user-info"></span>
32
- <button class="logout-btn">Logout</button>
33
  </div>
34
  </nav>
35
 
36
  <div class="container">
37
  <div class="section" id="admin-ai-section">
38
  <div class="section-header">
39
- <h2>AI Configuration</h2>
40
  </div>
41
  <div class="ai-config-form">
42
  <form id="ai-config-form">
43
  <div class="form-group">
44
- <label>API URL *</label>
45
  <input type="text" id="ai-api-url" placeholder="https://api.openai.com/v1/chat/completions" required>
46
  </div>
47
  <div class="form-group">
48
- <label>Model ID *</label>
49
  <input type="text" id="ai-model-id" placeholder="gpt-3.5-turbo" required>
50
  </div>
51
  <div class="form-group">
52
- <label>API Key *</label>
53
  <input type="password" id="ai-api-key" placeholder="sk-xxx">
54
  <small id="ai-api-key-hint" class="hint"></small>
55
  </div>
56
  <div class="form-group">
57
- <label>System Prompt (Optional)</label>
58
- <textarea id="ai-system-prompt" placeholder="You are a helpful assistant for Minecraft players."></textarea>
59
  </div>
60
  <div class="form-group">
61
  <label>
62
- <input type="checkbox" id="ai-enabled" checked> Enabled
63
  </label>
64
  </div>
65
  <div class="form-actions">
66
- <button type="submit" class="btn-primary">Save Configuration</button>
67
- <button type="button" id="ai-test-btn" class="btn-secondary">Test Connection</button>
68
- <button type="button" id="ai-delete-btn" class="btn-danger">Delete Configuration</button>
69
  </div>
70
  </form>
71
  <div id="ai-status" class="ai-status"></div>
@@ -74,8 +74,8 @@
74
 
75
  <div class="section" id="admin-key-section">
76
  <div class="section-header">
77
- <h2>API Key Management</h2>
78
- <button id="create-key-btn" class="btn-primary">Create Key</button>
79
  </div>
80
  <div id="admin-keys-list" class="keys-list"></div>
81
  </div>
@@ -84,21 +84,21 @@
84
 
85
  <div id="user-screen" class="screen hidden">
86
  <nav class="navbar">
87
- <h1>User Monitoring</h1>
88
  <div class="nav-info">
89
  <span id="user-info"></span>
90
- <button class="logout-btn">Logout</button>
91
  </div>
92
  </nav>
93
 
94
  <div class="container">
95
  <div class="stats-grid">
96
  <div class="stat-card">
97
- <h3>Active Connections</h3>
98
  <p class="stat-value" id="user-stat-connections">0</p>
99
  </div>
100
  <div class="stat-card">
101
- <h3>Total Keys</h3>
102
  <p class="stat-value" id="user-stat-total-keys">0</p>
103
  </div>
104
  <div class="stat-card">
@@ -116,21 +116,21 @@
116
  </div>
117
 
118
  <div class="section">
119
- <h2>Server Keys</h2>
120
  <div id="user-keys-list" class="keys-list"></div>
121
  </div>
122
 
123
  <div class="section">
124
- <h2>Command Console</h2>
125
  <form id="command-form" class="command-form">
126
- <input type="text" id="command-input" placeholder="Enter command" required>
127
- <button type="submit" class="btn-primary">Send</button>
128
  </form>
129
  <div id="command-history" class="command-history"></div>
130
  </div>
131
 
132
  <div class="section">
133
- <h2>Live Events</h2>
134
  <div id="user-events-list" class="events-list"></div>
135
  </div>
136
  </div>
@@ -138,26 +138,26 @@
138
 
139
  <div id="create-key-modal" class="modal hidden">
140
  <div class="modal-content">
141
- <h2>Create API Key</h2>
142
  <form id="create-key-form">
143
- <label>Name *</label>
144
  <input type="text" id="key-name" required>
145
 
146
- <label>Description</label>
147
  <textarea id="key-description"></textarea>
148
 
149
- <label>Key Type</label>
150
  <select id="key-type">
151
- <option value="regular">Regular</option>
152
- <option value="admin">Admin</option>
153
  </select>
154
 
155
- <label>Server ID (optional)</label>
156
  <input type="text" id="key-server-id">
157
 
158
  <div class="modal-actions">
159
- <button type="button" id="cancel-create-btn" class="btn-secondary">Cancel</button>
160
- <button type="submit" class="btn-primary">Create</button>
161
  </div>
162
  </form>
163
  </div>
@@ -165,14 +165,14 @@
165
 
166
  <div id="key-details-modal" class="modal hidden">
167
  <div class="modal-content">
168
- <h2>Key Details</h2>
169
  <div id="key-details-content"></div>
170
  <div class="modal-actions">
171
- <button id="close-details-btn" class="btn-secondary">Close</button>
172
  </div>
173
  </div>
174
  </div>
175
 
176
  <script src="app.js"></script>
177
  </body>
178
- </html>
 
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>Minecraft WebSocket API - 控制台</title>
7
  <link rel="stylesheet" href="style.css">
8
  </head>
9
  <body>
10
  <div id="login-screen" class="screen">
11
  <div class="login-container">
12
  <h1>Minecraft WebSocket API</h1>
13
+ <h2>控制台登录</h2>
14
  <form id="login-form">
15
+ <input type="password" id="api-key-input" placeholder="请输入 Admin Regular Key" required>
16
+ <button type="submit">登录</button>
17
  </form>
18
  <p class="error-message" id="login-error"></p>
19
  <div class="login-info">
20
+ <p><strong>密钥类型</strong></p>
21
+ <p>Admin Key - 拥有完整管理权限</p>
22
+ <p>Regular Key - 服务器监测与命令访问</p>
23
  </div>
24
  </div>
25
  </div>
26
 
27
  <div id="admin-screen" class="screen hidden">
28
  <nav class="navbar">
29
+ <h1>管理员面板</h1>
30
  <div class="nav-info">
31
  <span id="admin-user-info"></span>
32
+ <button class="logout-btn">退出登录</button>
33
  </div>
34
  </nav>
35
 
36
  <div class="container">
37
  <div class="section" id="admin-ai-section">
38
  <div class="section-header">
39
+ <h2>AI 配置</h2>
40
  </div>
41
  <div class="ai-config-form">
42
  <form id="ai-config-form">
43
  <div class="form-group">
44
+ <label>API 地址 *</label>
45
  <input type="text" id="ai-api-url" placeholder="https://api.openai.com/v1/chat/completions" required>
46
  </div>
47
  <div class="form-group">
48
+ <label>模型 ID *</label>
49
  <input type="text" id="ai-model-id" placeholder="gpt-3.5-turbo" required>
50
  </div>
51
  <div class="form-group">
52
+ <label>API 密钥 *</label>
53
  <input type="password" id="ai-api-key" placeholder="sk-xxx">
54
  <small id="ai-api-key-hint" class="hint"></small>
55
  </div>
56
  <div class="form-group">
57
+ <label>系统提示词(可选)</label>
58
+ <textarea id="ai-system-prompt" placeholder="你是 Minecraft 玩家助手。"></textarea>
59
  </div>
60
  <div class="form-group">
61
  <label>
62
+ <input type="checkbox" id="ai-enabled" checked> 启用
63
  </label>
64
  </div>
65
  <div class="form-actions">
66
+ <button type="submit" class="btn-primary">保存配置</button>
67
+ <button type="button" id="ai-test-btn" class="btn-secondary">测试连接</button>
68
+ <button type="button" id="ai-delete-btn" class="btn-danger">删除配置</button>
69
  </div>
70
  </form>
71
  <div id="ai-status" class="ai-status"></div>
 
74
 
75
  <div class="section" id="admin-key-section">
76
  <div class="section-header">
77
+ <h2>API 密钥管理</h2>
78
+ <button id="create-key-btn" class="btn-primary">创建密钥</button>
79
  </div>
80
  <div id="admin-keys-list" class="keys-list"></div>
81
  </div>
 
84
 
85
  <div id="user-screen" class="screen hidden">
86
  <nav class="navbar">
87
+ <h1>用户监测</h1>
88
  <div class="nav-info">
89
  <span id="user-info"></span>
90
+ <button class="logout-btn">退出登录</button>
91
  </div>
92
  </nav>
93
 
94
  <div class="container">
95
  <div class="stats-grid">
96
  <div class="stat-card">
97
+ <h3>活跃连接</h3>
98
  <p class="stat-value" id="user-stat-connections">0</p>
99
  </div>
100
  <div class="stat-card">
101
+ <h3>密钥总数</h3>
102
  <p class="stat-value" id="user-stat-total-keys">0</p>
103
  </div>
104
  <div class="stat-card">
 
116
  </div>
117
 
118
  <div class="section">
119
+ <h2>服务器密钥</h2>
120
  <div id="user-keys-list" class="keys-list"></div>
121
  </div>
122
 
123
  <div class="section">
124
+ <h2>命令控制台</h2>
125
  <form id="command-form" class="command-form">
126
+ <input type="text" id="command-input" placeholder="输入命令" required>
127
+ <button type="submit" class="btn-primary">发送</button>
128
  </form>
129
  <div id="command-history" class="command-history"></div>
130
  </div>
131
 
132
  <div class="section">
133
+ <h2>实时事件</h2>
134
  <div id="user-events-list" class="events-list"></div>
135
  </div>
136
  </div>
 
138
 
139
  <div id="create-key-modal" class="modal hidden">
140
  <div class="modal-content">
141
+ <h2>创建 API 密钥</h2>
142
  <form id="create-key-form">
143
+ <label>名称 *</label>
144
  <input type="text" id="key-name" required>
145
 
146
+ <label>描述</label>
147
  <textarea id="key-description"></textarea>
148
 
149
+ <label>密钥类型</label>
150
  <select id="key-type">
151
+ <option value="regular">普通</option>
152
+ <option value="admin">管理员</option>
153
  </select>
154
 
155
+ <label>服务器 ID(可选)</label>
156
  <input type="text" id="key-server-id">
157
 
158
  <div class="modal-actions">
159
+ <button type="button" id="cancel-create-btn" class="btn-secondary">取消</button>
160
+ <button type="submit" class="btn-primary">创建</button>
161
  </div>
162
  </form>
163
  </div>
 
165
 
166
  <div id="key-details-modal" class="modal hidden">
167
  <div class="modal-content">
168
+ <h2>密钥详情</h2>
169
  <div id="key-details-content"></div>
170
  <div class="modal-actions">
171
+ <button id="close-details-btn" class="btn-secondary">关闭</button>
172
  </div>
173
  </div>
174
  </div>
175
 
176
  <script src="app.js"></script>
177
  </body>
178
+ </html>
hf_repo/cli/cli.js CHANGED
@@ -389,15 +389,15 @@ program
389
 
390
  program
391
  .command('reset-admin')
392
- .description('Reset Admin Key using recovery token (offline)')
393
- .requiredOption('-r, --recovery-token <token>', 'Admin recovery token')
394
- .option('--db-path <path>', 'Database path', process.env.DATABASE_PATH || 'minecraft_ws.db')
395
- .option('--keep-existing', 'Keep existing Admin Keys active')
396
- .option('--name <name>', 'New Admin Key name', 'Recovered Admin Key')
397
  .action(async (options) => {
398
  try {
399
  if (!fs.existsSync(options.dbPath)) {
400
- throw new Error(`Database not found at ${options.dbPath}`);
401
  }
402
 
403
  const db = new Database(options.dbPath);
@@ -408,12 +408,12 @@ program
408
  name: options.name
409
  });
410
 
411
- console.log('Admin Key reset successful.');
412
  console.log(` ID : ${result.id}`);
413
- console.log(` Name : ${result.name}`);
414
  console.log(` Key : ${result.key}`);
415
  } catch (error) {
416
- console.error(`\x1b[31mReset failed: ${error.message}\x1b[0m`);
417
  process.exit(1);
418
  }
419
  });
 
389
 
390
  program
391
  .command('reset-admin')
392
+ .description('使用恢复令牌重置 Admin Key (离线)')
393
+ .requiredOption('-r, --recovery-token <token>', '管理员恢复令牌')
394
+ .option('--db-path <path>', '数据库路径', process.env.DATABASE_PATH || 'minecraft_ws.db')
395
+ .option('--keep-existing', '保持现有 Admin Key 处于活跃状态')
396
+ .option('--name <name>', ' Admin Key 名称', 'Recovered Admin Key')
397
  .action(async (options) => {
398
  try {
399
  if (!fs.existsSync(options.dbPath)) {
400
+ throw new Error(`在 ${options.dbPath} 未找到数据库`);
401
  }
402
 
403
  const db = new Database(options.dbPath);
 
408
  name: options.name
409
  });
410
 
411
+ console.log('\x1b[32m✅ Admin Key 重置成功。\x1b[0m');
412
  console.log(` ID : ${result.id}`);
413
+ console.log(` 名称 : ${result.name}`);
414
  console.log(` Key : ${result.key}`);
415
  } catch (error) {
416
+ console.error(`\x1b[31m❌ 重置失败: ${error.message}\x1b[0m`);
417
  process.exit(1);
418
  }
419
  });
hf_repo/dashboard/server.js CHANGED
@@ -13,9 +13,9 @@ app.get('/', (req, res) => {
13
 
14
  app.listen(PORT, () => {
15
  console.log('='.repeat(50));
16
- console.log('Minecraft WebSocket API - Dashboard');
17
  console.log('='.repeat(50));
18
- console.log(`Dashboard URL: http://localhost:${PORT}`);
19
- console.log('Use Admin or Regular Key to log in.');
20
  console.log('='.repeat(50));
21
  });
 
13
 
14
  app.listen(PORT, () => {
15
  console.log('='.repeat(50));
16
+ console.log('Minecraft WebSocket API - 控制台');
17
  console.log('='.repeat(50));
18
+ console.log(`控制台地址: http://localhost:${PORT}`);
19
+ console.log('请使用 Admin Key Regular Key 登录。');
20
  console.log('='.repeat(50));
21
  });
hf_repo/docs/protocol.md CHANGED
@@ -1,32 +1,31 @@
1
- # Protocol Specification
2
 
3
- This document defines the REST and WebSocket protocol formats used by
4
- InterConnect-Server. All payloads are JSON.
5
 
6
- ## Authentication
7
 
8
- - REST: Send `Authorization: Bearer <API_KEY>` in the request headers.
9
- - WebSocket: Connect to `ws://<host>/ws?api_key=<API_KEY>` (use `wss` over HTTPS).
10
 
11
- ## Event Object
12
 
13
- Events are sent to the server via REST and broadcast to WebSocket clients.
14
- The server validates required fields and the `event_type` value only.
15
- The `data` payload is otherwise passed through as-is.
16
 
17
- Required fields:
18
- - `event_type` (string)
19
- - `server_name` (string)
20
- - `timestamp` (string, ISO 8601)
21
- - `data` (object)
22
 
23
- Valid `event_type` values:
24
  - `player_join`
25
  - `player_leave`
26
  - `message`
27
  - `server_command`
28
 
29
- Example event:
30
  ```json
31
  {
32
  "event_type": "player_join",
@@ -38,34 +37,34 @@ Example event:
38
  }
39
  ```
40
 
41
- ## REST Endpoints
42
 
43
  ### POST /api/events
44
 
45
- Submit an event to be stored and broadcast.
46
 
47
- Request:
48
- - Auth: any valid key
49
  - `Content-Type: application/json`
50
- - Body: Event Object
51
 
52
- Response:
53
  ```json
54
  {
55
  "message": "Event received and broadcasted"
56
  }
57
  ```
58
 
59
- Errors:
60
- - `400` if required fields are missing or `event_type` is invalid
61
- - `401` if the API key is missing/invalid
62
 
63
  ### POST /api/server/command
64
 
65
- Send a server command. This also broadcasts a `server_command` event.
66
 
67
- Request:
68
- - Auth: Regular or Admin key
69
  - `Content-Type: application/json`
70
  - Body:
71
  ```json
@@ -75,11 +74,11 @@ Request:
75
  }
76
  ```
77
 
78
- Notes:
79
- - `server_id` is required for Admin keys when not bound to a server.
80
- - The server enforces an allow/block list for Admin commands.
81
 
82
- Response:
83
  ```json
84
  {
85
  "message": "Command sent successfully",
@@ -88,23 +87,23 @@ Response:
88
  }
89
  ```
90
 
91
- ## WebSocket Messages
92
 
93
- ### Client to Server
94
 
95
  Ping:
96
  ```json
97
  { "type": "ping" }
98
  ```
99
 
100
- Pong response:
101
  ```json
102
  { "type": "pong" }
103
  ```
104
 
105
- ### Server to Client
106
 
107
- Broadcasted event:
108
  ```json
109
  {
110
  "type": "minecraft_event",
@@ -121,5 +120,4 @@ Broadcasted event:
121
  }
122
  ```
123
 
124
- `source_key_id_prefix` is the first 8 characters of the API key ID that
125
- submitted the event or command. It is not the API key itself.
 
1
+ # 协议规范
2
 
3
+ 本文档定义了 InterConnect-Server 使用的 REST WebSocket 协议格式。所有负载均为 JSON 格式。
 
4
 
5
+ ## 认证 (Authentication)
6
 
7
+ - REST: 在请求头中发送 `Authorization: Bearer <API_KEY>`。
8
+ - WebSocket: 连接到 `ws://<host>/ws?api_key=<API_KEY>`(HTTPS 下使用 `wss`)。
9
 
10
+ ## 事件对象 (Event Object)
11
 
12
+ 事件通过 REST 发送到服务器,并广播给 WebSocket 客户端。
13
+ 服务器仅验证必填字段和 `event_type` 值。
14
+ `data` 负载会原样传递。
15
 
16
+ 必填字段:
17
+ - `event_type` (字符串)
18
+ - `server_name` (字符串)
19
+ - `timestamp` (字符串, ISO 8601)
20
+ - `data` (对象)
21
 
22
+ 有效的 `event_type` 值:
23
  - `player_join`
24
  - `player_leave`
25
  - `message`
26
  - `server_command`
27
 
28
+ 事件示例:
29
  ```json
30
  {
31
  "event_type": "player_join",
 
37
  }
38
  ```
39
 
40
+ ## REST 端点 (REST Endpoints)
41
 
42
  ### POST /api/events
43
 
44
+ 提交一个事件以进行存储和广播。
45
 
46
+ 请求:
47
+ - 认证:任意有效密钥
48
  - `Content-Type: application/json`
49
+ - Body: 事件对象
50
 
51
+ 响应:
52
  ```json
53
  {
54
  "message": "Event received and broadcasted"
55
  }
56
  ```
57
 
58
+ 错误:
59
+ - `400` 如果缺少必填字段或 `event_type` 无效
60
+ - `401` 如果 API 密钥丢失/无效
61
 
62
  ### POST /api/server/command
63
 
64
+ 发送服务器命令。这也将广播一个 `server_command` 事件。
65
 
66
+ 请求:
67
+ - 认证:Regular Admin 密钥
68
  - `Content-Type: application/json`
69
  - Body:
70
  ```json
 
74
  }
75
  ```
76
 
77
+ 注意:
78
+ - Admin 密钥未绑定到服务器时,必须提供 `server_id`。
79
+ - 服务器对 Admin 命令强制执行允许/阻止列表。
80
 
81
+ 响应:
82
  ```json
83
  {
84
  "message": "Command sent successfully",
 
87
  }
88
  ```
89
 
90
+ ## WebSocket 消息 (WebSocket Messages)
91
 
92
+ ### 客户端到服务器
93
 
94
  Ping:
95
  ```json
96
  { "type": "ping" }
97
  ```
98
 
99
+ Pong 响应:
100
  ```json
101
  { "type": "pong" }
102
  ```
103
 
104
+ ### 服务器到客户端
105
 
106
+ 广播事件:
107
  ```json
108
  {
109
  "type": "minecraft_event",
 
120
  }
121
  ```
122
 
123
+ `source_key_id_prefix` 是提交事件或命令的 API 密钥 ID 的前 8 个字符。它不是 API 密钥本身。
 
hf_repo/hf_repo/docs/protocol.md ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Protocol Specification
2
+
3
+ This document defines the REST and WebSocket protocol formats used by
4
+ InterConnect-Server. All payloads are JSON.
5
+
6
+ ## Authentication
7
+
8
+ - REST: Send `Authorization: Bearer <API_KEY>` in the request headers.
9
+ - WebSocket: Connect to `ws://<host>/ws?api_key=<API_KEY>` (use `wss` over HTTPS).
10
+
11
+ ## Event Object
12
+
13
+ Events are sent to the server via REST and broadcast to WebSocket clients.
14
+ The server validates required fields and the `event_type` value only.
15
+ The `data` payload is otherwise passed through as-is.
16
+
17
+ Required fields:
18
+ - `event_type` (string)
19
+ - `server_name` (string)
20
+ - `timestamp` (string, ISO 8601)
21
+ - `data` (object)
22
+
23
+ Valid `event_type` values:
24
+ - `player_join`
25
+ - `player_leave`
26
+ - `message`
27
+ - `server_command`
28
+
29
+ Example event:
30
+ ```json
31
+ {
32
+ "event_type": "player_join",
33
+ "server_name": "survival-01",
34
+ "timestamp": "2024-01-21T12:34:56.789Z",
35
+ "data": {
36
+ "player": "Steve"
37
+ }
38
+ }
39
+ ```
40
+
41
+ ## REST Endpoints
42
+
43
+ ### POST /api/events
44
+
45
+ Submit an event to be stored and broadcast.
46
+
47
+ Request:
48
+ - Auth: any valid key
49
+ - `Content-Type: application/json`
50
+ - Body: Event Object
51
+
52
+ Response:
53
+ ```json
54
+ {
55
+ "message": "Event received and broadcasted"
56
+ }
57
+ ```
58
+
59
+ Errors:
60
+ - `400` if required fields are missing or `event_type` is invalid
61
+ - `401` if the API key is missing/invalid
62
+
63
+ ### POST /api/server/command
64
+
65
+ Send a server command. This also broadcasts a `server_command` event.
66
+
67
+ Request:
68
+ - Auth: Regular or Admin key
69
+ - `Content-Type: application/json`
70
+ - Body:
71
+ ```json
72
+ {
73
+ "command": "say Hello from API",
74
+ "server_id": "survival-01"
75
+ }
76
+ ```
77
+
78
+ Notes:
79
+ - `server_id` is required for Admin keys when not bound to a server.
80
+ - The server enforces an allow/block list for Admin commands.
81
+
82
+ Response:
83
+ ```json
84
+ {
85
+ "message": "Command sent successfully",
86
+ "command": "say Hello from API",
87
+ "server_id": "survival-01"
88
+ }
89
+ ```
90
+
91
+ ## WebSocket Messages
92
+
93
+ ### Client to Server
94
+
95
+ Ping:
96
+ ```json
97
+ { "type": "ping" }
98
+ ```
99
+
100
+ Pong response:
101
+ ```json
102
+ { "type": "pong" }
103
+ ```
104
+
105
+ ### Server to Client
106
+
107
+ Broadcasted event:
108
+ ```json
109
+ {
110
+ "type": "minecraft_event",
111
+ "event": {
112
+ "event_type": "message",
113
+ "server_name": "survival-01",
114
+ "timestamp": "2024-01-21T12:34:56.789Z",
115
+ "data": {
116
+ "player": "Alex",
117
+ "message": "Hello world"
118
+ }
119
+ },
120
+ "source_key_id_prefix": "a1b2c3d4"
121
+ }
122
+ ```
123
+
124
+ `source_key_id_prefix` is the first 8 characters of the API key ID that
125
+ submitted the event or command. It is not the API key itself.
hf_repo/hf_repo/hf_repo/dashboard/public/app.js CHANGED
@@ -26,6 +26,16 @@ const commandForm = document.getElementById('command-form');
26
  const commandInput = document.getElementById('command-input');
27
  const commandHistory = document.getElementById('command-history');
28
  const userEventsList = document.getElementById('user-events-list');
 
 
 
 
 
 
 
 
 
 
29
 
30
  function authHeaders(key) {
31
  return { Authorization: `Bearer ${key}` };
@@ -92,6 +102,7 @@ function showAdminPanel() {
92
  }
93
 
94
  loadAdminKeys();
 
95
  }
96
 
97
  function showUserPanel() {
@@ -605,3 +616,170 @@ if (commandForm) {
605
  window.activateKey = activateKey;
606
  window.deactivateKey = deactivateKey;
607
  window.deleteKey = deleteKey;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  const commandInput = document.getElementById('command-input');
27
  const commandHistory = document.getElementById('command-history');
28
  const userEventsList = document.getElementById('user-events-list');
29
+ const aiConfigForm = document.getElementById('ai-config-form');
30
+ const aiApiUrlInput = document.getElementById('ai-api-url');
31
+ const aiModelIdInput = document.getElementById('ai-model-id');
32
+ const aiApiKeyInput = document.getElementById('ai-api-key');
33
+ const aiApiKeyHint = document.getElementById('ai-api-key-hint');
34
+ const aiSystemPromptInput = document.getElementById('ai-system-prompt');
35
+ const aiEnabledCheckbox = document.getElementById('ai-enabled');
36
+ const aiTestBtn = document.getElementById('ai-test-btn');
37
+ const aiDeleteBtn = document.getElementById('ai-delete-btn');
38
+ const aiStatus = document.getElementById('ai-status');
39
 
40
  function authHeaders(key) {
41
  return { Authorization: `Bearer ${key}` };
 
102
  }
103
 
104
  loadAdminKeys();
105
+ loadAIConfig();
106
  }
107
 
108
  function showUserPanel() {
 
616
  window.activateKey = activateKey;
617
  window.deactivateKey = deactivateKey;
618
  window.deleteKey = deleteKey;
619
+
620
+ async function loadAIConfig() {
621
+ if (!apiKey) {
622
+ return;
623
+ }
624
+ try {
625
+ const response = await fetch(`${API_URL}/api/ai/config`, {
626
+ headers: authHeaders(apiKey)
627
+ });
628
+
629
+ if (response.ok) {
630
+ const config = await response.json();
631
+ if (aiApiUrlInput) aiApiUrlInput.value = config.apiUrl || '';
632
+ if (aiModelIdInput) aiModelIdInput.value = config.modelId || '';
633
+ if (aiApiKeyInput) aiApiKeyInput.value = '';
634
+ if (aiApiKeyHint) aiApiKeyHint.textContent = config.apiKey ? `Current: ${config.apiKey}` : '';
635
+ if (aiSystemPromptInput) aiSystemPromptInput.value = config.systemPrompt || '';
636
+ if (aiEnabledCheckbox) aiEnabledCheckbox.checked = config.enabled;
637
+ showAIStatus('Configuration loaded', 'success');
638
+ } else if (response.status === 404) {
639
+ if (aiApiUrlInput) aiApiUrlInput.value = '';
640
+ if (aiModelIdInput) aiModelIdInput.value = '';
641
+ if (aiApiKeyInput) aiApiKeyInput.value = '';
642
+ if (aiApiKeyHint) aiApiKeyHint.textContent = '';
643
+ if (aiSystemPromptInput) aiSystemPromptInput.value = '';
644
+ if (aiEnabledCheckbox) aiEnabledCheckbox.checked = true;
645
+ showAIStatus('No configuration found. Please set up AI configuration.', 'info');
646
+ }
647
+ } catch (error) {
648
+ showAIStatus(`Failed to load AI config: ${error.message}`, 'error');
649
+ }
650
+ }
651
+
652
+ async function saveAIConfig(event) {
653
+ event.preventDefault();
654
+ if (!apiKey) {
655
+ return;
656
+ }
657
+
658
+ const apiUrl = aiApiUrlInput?.value?.trim();
659
+ const modelId = aiModelIdInput?.value?.trim();
660
+ const apiKeyValue = aiApiKeyInput?.value?.trim();
661
+ const systemPrompt = aiSystemPromptInput?.value?.trim() || null;
662
+ const enabled = aiEnabledCheckbox?.checked ?? true;
663
+
664
+ if (!apiUrl || !modelId) {
665
+ showAIStatus('API URL and Model ID are required', 'error');
666
+ return;
667
+ }
668
+
669
+ try {
670
+ const existingResponse = await fetch(`${API_URL}/api/ai/config`, {
671
+ headers: authHeaders(apiKey)
672
+ });
673
+ const isUpdate = existingResponse.ok;
674
+
675
+ const payload = {
676
+ api_url: apiUrl,
677
+ model_id: modelId,
678
+ enabled: enabled,
679
+ system_prompt: systemPrompt
680
+ };
681
+
682
+ if (apiKeyValue) {
683
+ payload.api_key = apiKeyValue;
684
+ } else if (!isUpdate) {
685
+ showAIStatus('API Key is required for new configuration', 'error');
686
+ return;
687
+ }
688
+
689
+ const method = isUpdate ? 'PATCH' : 'POST';
690
+ const response = await fetch(`${API_URL}/api/ai/config`, {
691
+ method: method,
692
+ headers: {
693
+ ...authHeaders(apiKey),
694
+ 'Content-Type': 'application/json'
695
+ },
696
+ body: JSON.stringify(payload)
697
+ });
698
+
699
+ if (response.ok) {
700
+ const config = await response.json();
701
+ if (aiApiKeyInput) aiApiKeyInput.value = '';
702
+ if (aiApiKeyHint) aiApiKeyHint.textContent = config.apiKey ? `Current: ${config.apiKey}` : '';
703
+ showAIStatus('Configuration saved successfully', 'success');
704
+ } else {
705
+ const error = await response.json();
706
+ showAIStatus(`Failed to save: ${error.detail}`, 'error');
707
+ }
708
+ } catch (error) {
709
+ showAIStatus(`Failed to save AI config: ${error.message}`, 'error');
710
+ }
711
+ }
712
+
713
+ async function testAIConnection() {
714
+ if (!apiKey) {
715
+ return;
716
+ }
717
+ showAIStatus('Testing connection...', 'info');
718
+
719
+ try {
720
+ const response = await fetch(`${API_URL}/api/ai/config/test`, {
721
+ method: 'POST',
722
+ headers: authHeaders(apiKey)
723
+ });
724
+
725
+ const result = await response.json();
726
+ if (result.success) {
727
+ showAIStatus(`Connection successful! Model: ${result.model}, Response: ${result.response}`, 'success');
728
+ } else {
729
+ showAIStatus(`Connection failed: ${result.error}`, 'error');
730
+ }
731
+ } catch (error) {
732
+ showAIStatus(`Test failed: ${error.message}`, 'error');
733
+ }
734
+ }
735
+
736
+ async function deleteAIConfig() {
737
+ if (!apiKey) {
738
+ return;
739
+ }
740
+ if (!confirm('Are you sure you want to delete the AI configuration?')) {
741
+ return;
742
+ }
743
+
744
+ try {
745
+ const response = await fetch(`${API_URL}/api/ai/config`, {
746
+ method: 'DELETE',
747
+ headers: authHeaders(apiKey)
748
+ });
749
+
750
+ if (response.ok || response.status === 204) {
751
+ if (aiApiUrlInput) aiApiUrlInput.value = '';
752
+ if (aiModelIdInput) aiModelIdInput.value = '';
753
+ if (aiApiKeyInput) aiApiKeyInput.value = '';
754
+ if (aiApiKeyHint) aiApiKeyHint.textContent = '';
755
+ if (aiSystemPromptInput) aiSystemPromptInput.value = '';
756
+ if (aiEnabledCheckbox) aiEnabledCheckbox.checked = true;
757
+ showAIStatus('Configuration deleted', 'success');
758
+ } else {
759
+ const error = await response.json();
760
+ showAIStatus(`Failed to delete: ${error.detail}`, 'error');
761
+ }
762
+ } catch (error) {
763
+ showAIStatus(`Failed to delete AI config: ${error.message}`, 'error');
764
+ }
765
+ }
766
+
767
+ function showAIStatus(message, type) {
768
+ if (!aiStatus) {
769
+ return;
770
+ }
771
+ aiStatus.textContent = message;
772
+ aiStatus.className = `ai-status ${type}`;
773
+ }
774
+
775
+ if (aiConfigForm) {
776
+ aiConfigForm.addEventListener('submit', saveAIConfig);
777
+ }
778
+
779
+ if (aiTestBtn) {
780
+ aiTestBtn.addEventListener('click', testAIConnection);
781
+ }
782
+
783
+ if (aiDeleteBtn) {
784
+ aiDeleteBtn.addEventListener('click', deleteAIConfig);
785
+ }
hf_repo/hf_repo/hf_repo/dashboard/public/index.html CHANGED
@@ -34,6 +34,44 @@
34
  </nav>
35
 
36
  <div class="container">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  <div class="section" id="admin-key-section">
38
  <div class="section-header">
39
  <h2>API Key Management</h2>
 
34
  </nav>
35
 
36
  <div class="container">
37
+ <div class="section" id="admin-ai-section">
38
+ <div class="section-header">
39
+ <h2>AI Configuration</h2>
40
+ </div>
41
+ <div class="ai-config-form">
42
+ <form id="ai-config-form">
43
+ <div class="form-group">
44
+ <label>API URL *</label>
45
+ <input type="text" id="ai-api-url" placeholder="https://api.openai.com/v1/chat/completions" required>
46
+ </div>
47
+ <div class="form-group">
48
+ <label>Model ID *</label>
49
+ <input type="text" id="ai-model-id" placeholder="gpt-3.5-turbo" required>
50
+ </div>
51
+ <div class="form-group">
52
+ <label>API Key *</label>
53
+ <input type="password" id="ai-api-key" placeholder="sk-xxx">
54
+ <small id="ai-api-key-hint" class="hint"></small>
55
+ </div>
56
+ <div class="form-group">
57
+ <label>System Prompt (Optional)</label>
58
+ <textarea id="ai-system-prompt" placeholder="You are a helpful assistant for Minecraft players."></textarea>
59
+ </div>
60
+ <div class="form-group">
61
+ <label>
62
+ <input type="checkbox" id="ai-enabled" checked> Enabled
63
+ </label>
64
+ </div>
65
+ <div class="form-actions">
66
+ <button type="submit" class="btn-primary">Save Configuration</button>
67
+ <button type="button" id="ai-test-btn" class="btn-secondary">Test Connection</button>
68
+ <button type="button" id="ai-delete-btn" class="btn-danger">Delete Configuration</button>
69
+ </div>
70
+ </form>
71
+ <div id="ai-status" class="ai-status"></div>
72
+ </div>
73
+ </div>
74
+
75
  <div class="section" id="admin-key-section">
76
  <div class="section-header">
77
  <h2>API Key Management</h2>
hf_repo/hf_repo/hf_repo/dashboard/public/style.css CHANGED
@@ -433,3 +433,88 @@ body {
433
  font-size: 0.9em;
434
  color: white;
435
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
433
  font-size: 0.9em;
434
  color: white;
435
  }
436
+
437
+ .ai-config-form {
438
+ max-width: 600px;
439
+ }
440
+
441
+ .ai-config-form .form-group {
442
+ margin-bottom: 20px;
443
+ }
444
+
445
+ .ai-config-form label {
446
+ display: block;
447
+ margin-bottom: 8px;
448
+ font-weight: bold;
449
+ color: #333;
450
+ }
451
+
452
+ .ai-config-form input[type="text"],
453
+ .ai-config-form input[type="password"],
454
+ .ai-config-form textarea {
455
+ width: 100%;
456
+ padding: 12px;
457
+ border: 2px solid #e0e0e0;
458
+ border-radius: 5px;
459
+ font-size: 14px;
460
+ transition: border-color 0.3s;
461
+ }
462
+
463
+ .ai-config-form input[type="text"]:focus,
464
+ .ai-config-form input[type="password"]:focus,
465
+ .ai-config-form textarea:focus {
466
+ border-color: #667eea;
467
+ outline: none;
468
+ }
469
+
470
+ .ai-config-form textarea {
471
+ min-height: 100px;
472
+ resize: vertical;
473
+ }
474
+
475
+ .ai-config-form .hint {
476
+ display: block;
477
+ margin-top: 5px;
478
+ font-size: 0.85em;
479
+ color: #666;
480
+ }
481
+
482
+ .ai-config-form .form-actions {
483
+ display: flex;
484
+ gap: 10px;
485
+ margin-top: 25px;
486
+ }
487
+
488
+ .ai-status {
489
+ margin-top: 20px;
490
+ padding: 12px 16px;
491
+ border-radius: 5px;
492
+ font-size: 0.9em;
493
+ }
494
+
495
+ .ai-status:empty {
496
+ display: none;
497
+ }
498
+
499
+ .ai-status.success {
500
+ background: #d4edda;
501
+ color: #155724;
502
+ border: 1px solid #c3e6cb;
503
+ }
504
+
505
+ .ai-status.error {
506
+ background: #f8d7da;
507
+ color: #721c24;
508
+ border: 1px solid #f5c6cb;
509
+ }
510
+
511
+ .ai-status.info {
512
+ background: #d1ecf1;
513
+ color: #0c5460;
514
+ border: 1px solid #bee5eb;
515
+ }
516
+
517
+ .ai-config-form input[type="checkbox"] {
518
+ width: auto;
519
+ margin-right: 8px;
520
+ }
hf_repo/hf_repo/hf_repo/docs/AI_API.md ADDED
@@ -0,0 +1,415 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # InterConnect-Server AI API Documentation
2
+
3
+ 本文档描述了 InterConnect-Server 的 AI 聊天功能相关 API 接口。
4
+
5
+ ## 概述
6
+
7
+ AI 功能允许 Minecraft 服务器通过 InterConnect-Client 插件/模组调用 OpenAI 格式的 API,实现游戏内 AI 聊天功能。
8
+
9
+ **基础路径**: `/api/ai`
10
+
11
+ ---
12
+
13
+ ## 认证
14
+
15
+ 所有 API 请求需要在 Header 中携带 API Key:
16
+
17
+ ```
18
+ Authorization: Bearer <your_api_key>
19
+ ```
20
+
21
+ ### 权限要求
22
+
23
+ | 接口 | 权限要求 |
24
+ |------|----------|
25
+ | `GET /api/ai/config` | Admin Key |
26
+ | `POST /api/ai/config` | Admin Key |
27
+ | `PATCH /api/ai/config` | Admin Key |
28
+ | `DELETE /api/ai/config` | Admin Key |
29
+ | `POST /api/ai/config/test` | Admin Key |
30
+ | `POST /api/ai/chat` | Server Key 或 Admin Key |
31
+
32
+ ---
33
+
34
+ ## AI 配置接口
35
+
36
+ ### 获取 AI 配置
37
+
38
+ 获取当前的 AI 配置信息(API Key 会被部分隐藏)。
39
+
40
+ **请求**
41
+
42
+ ```http
43
+ GET /api/ai/config
44
+ Authorization: Bearer <admin_key>
45
+ ```
46
+
47
+ **成功响应** `200 OK`
48
+
49
+ ```json
50
+ {
51
+ "apiUrl": "https://api.openai.com/v1/chat/completions",
52
+ "modelId": "gpt-3.5-turbo",
53
+ "apiKey": "sk-x****xxxx",
54
+ "enabled": true,
55
+ "systemPrompt": "You are a helpful assistant for Minecraft players.",
56
+ "createdAt": "2024-01-15T10:30:00.000Z",
57
+ "updatedAt": "2024-01-15T12:00:00.000Z"
58
+ }
59
+ ```
60
+
61
+ **错误响应** `404 Not Found`
62
+
63
+ ```json
64
+ {
65
+ "detail": "AI configuration not found"
66
+ }
67
+ ```
68
+
69
+ ---
70
+
71
+ ### 创建 AI 配置
72
+
73
+ 创建新的 AI 配置。
74
+
75
+ **请求**
76
+
77
+ ```http
78
+ POST /api/ai/config
79
+ Authorization: Bearer <admin_key>
80
+ Content-Type: application/json
81
+ ```
82
+
83
+ **请求体**
84
+
85
+ ```json
86
+ {
87
+ "api_url": "https://api.openai.com/v1/chat/completions",
88
+ "model_id": "gpt-3.5-turbo",
89
+ "api_key": "sk-xxxxxxxxxxxxxxxxxxxxxxxx",
90
+ "enabled": true,
91
+ "system_prompt": "You are a helpful assistant for Minecraft players."
92
+ }
93
+ ```
94
+
95
+ | 字段 | 类型 | 必填 | 说明 |
96
+ |------|------|------|------|
97
+ | `api_url` | string | ✅ | OpenAI 格式的 API URL |
98
+ | `model_id` | string | ✅ | 模型 ID (如 gpt-3.5-turbo, gpt-4) |
99
+ | `api_key` | string | ✅ | API Key |
100
+ | `enabled` | boolean | ❌ | 是否启用,默认 `true` |
101
+ | `system_prompt` | string | ❌ | 系统提示词,可选 |
102
+
103
+ **成功响应** `201 Created`
104
+
105
+ ```json
106
+ {
107
+ "apiUrl": "https://api.openai.com/v1/chat/completions",
108
+ "modelId": "gpt-3.5-turbo",
109
+ "apiKey": "sk-x****xxxx",
110
+ "enabled": true,
111
+ "systemPrompt": "You are a helpful assistant for Minecraft players.",
112
+ "createdAt": "2024-01-15T10:30:00.000Z",
113
+ "updatedAt": "2024-01-15T10:30:00.000Z"
114
+ }
115
+ ```
116
+
117
+ **错误响应** `400 Bad Request`
118
+
119
+ ```json
120
+ {
121
+ "detail": "api_url, model_id, and api_key are required"
122
+ }
123
+ ```
124
+
125
+ ---
126
+
127
+ ### 更新 AI 配置
128
+
129
+ 更新现有的 AI 配置,只需提供要更新的字段。
130
+
131
+ **请求**
132
+
133
+ ```http
134
+ PATCH /api/ai/config
135
+ Authorization: Bearer <admin_key>
136
+ Content-Type: application/json
137
+ ```
138
+
139
+ **请求体**
140
+
141
+ ```json
142
+ {
143
+ "model_id": "gpt-4",
144
+ "enabled": false
145
+ }
146
+ ```
147
+
148
+ | 字段 | 类型 | 必填 | 说明 |
149
+ |------|------|------|------|
150
+ | `api_url` | string | ❌ | OpenAI 格式的 API URL |
151
+ | `model_id` | string | ❌ | 模型 ID |
152
+ | `api_key` | string | ❌ | API Key(不提供则保持原值) |
153
+ | `enabled` | boolean | ❌ | 是否启用 |
154
+ | `system_prompt` | string | ❌ | 系统提示词 |
155
+
156
+ **成功响应** `200 OK`
157
+
158
+ ```json
159
+ {
160
+ "apiUrl": "https://api.openai.com/v1/chat/completions",
161
+ "modelId": "gpt-4",
162
+ "apiKey": "sk-x****xxxx",
163
+ "enabled": false,
164
+ "systemPrompt": "You are a helpful assistant for Minecraft players.",
165
+ "createdAt": "2024-01-15T10:30:00.000Z",
166
+ "updatedAt": "2024-01-15T14:00:00.000Z"
167
+ }
168
+ ```
169
+
170
+ ---
171
+
172
+ ### 删除 AI 配置
173
+
174
+ 删除 AI 配置。
175
+
176
+ **请求**
177
+
178
+ ```http
179
+ DELETE /api/ai/config
180
+ Authorization: Bearer <admin_key>
181
+ ```
182
+
183
+ **成功响应** `204 No Content`
184
+
185
+ 无响应体
186
+
187
+ **错误响应** `404 Not Found`
188
+
189
+ ```json
190
+ {
191
+ "detail": "AI configuration not found"
192
+ }
193
+ ```
194
+
195
+ ---
196
+
197
+ ### 测试 AI 连接
198
+
199
+ 测试当前 AI 配置是否能正常连接。
200
+
201
+ **请求**
202
+
203
+ ```http
204
+ POST /api/ai/config/test
205
+ Authorization: Bearer <admin_key>
206
+ ```
207
+
208
+ **成功响应** `200 OK`
209
+
210
+ ```json
211
+ {
212
+ "success": true,
213
+ "message": "Connection successful",
214
+ "model": "gpt-3.5-turbo",
215
+ "response": "OK"
216
+ }
217
+ ```
218
+
219
+ **错误响应** `400 Bad Request`
220
+
221
+ ```json
222
+ {
223
+ "success": false,
224
+ "message": "Connection failed",
225
+ "error": "Invalid API key"
226
+ }
227
+ ```
228
+
229
+ ---
230
+
231
+ ## AI 聊天接口
232
+
233
+ ### 发送聊天消息
234
+
235
+ 发送消息到 AI 并获取回复。此接口供 Minecraft 服务器的 InterConnect-Client 插件/模组调用。
236
+
237
+ **请求**
238
+
239
+ ```http
240
+ POST /api/ai/chat
241
+ Authorization: Bearer <server_key>
242
+ Content-Type: application/json
243
+ ```
244
+
245
+ **请求体**
246
+
247
+ ```json
248
+ {
249
+ "message": "你好,请介绍一下 Minecraft 的红石系统",
250
+ "player_name": "Steve",
251
+ "server_id": "my-server-1"
252
+ }
253
+ ```
254
+
255
+ | 字段 | 类型 | 必填 | 说明 |
256
+ |------|------|------|------|
257
+ | `message` | string | ✅ | 用户发送的消息内容 |
258
+ | `player_name` | string | ❌ | 发送消息的玩家名称 |
259
+ | `server_id` | string | ❌ | 服务器 ID(Admin Key 可指定,Server Key 使用绑定的 server_id) |
260
+
261
+ **成功响应** `200 OK`
262
+
263
+ ```json
264
+ {
265
+ "success": true,
266
+ "reply": "红石是 Minecraft 中的一种特殊材料,可以用来创建各种机械装置...",
267
+ "model": "gpt-3.5-turbo",
268
+ "usage": {
269
+ "prompt_tokens": 25,
270
+ "completion_tokens": 150,
271
+ "total_tokens": 175
272
+ }
273
+ }
274
+ ```
275
+
276
+ **错误响应**
277
+
278
+ `404 Not Found` - AI 配置不存在
279
+
280
+ ```json
281
+ {
282
+ "detail": "AI configuration not found"
283
+ }
284
+ ```
285
+
286
+ `403 Forbidden` - AI 功能已禁用
287
+
288
+ ```json
289
+ {
290
+ "detail": "AI feature is disabled"
291
+ }
292
+ ```
293
+
294
+ `400 Bad Request` - 缺少必填字段
295
+
296
+ ```json
297
+ {
298
+ "detail": "message is required"
299
+ }
300
+ ```
301
+
302
+ `500 Internal Server Error` - AI 请求失败
303
+
304
+ ```json
305
+ {
306
+ "success": false,
307
+ "detail": "AI request failed",
308
+ "error": "Rate limit exceeded"
309
+ }
310
+ ```
311
+
312
+ ---
313
+
314
+ ## 事件类型
315
+
316
+ 当 AI 聊天发生时,会产生 `ai_chat` 类型的事件,通过 WebSocket 广播给所有连接的客户端。
317
+
318
+ **事件格式**
319
+
320
+ ```json
321
+ {
322
+ "type": "minecraft_event",
323
+ "event": {
324
+ "event_type": "ai_chat",
325
+ "server_name": "my-server-1",
326
+ "timestamp": "2024-01-15T10:30:00.000Z",
327
+ "data": {
328
+ "player_name": "Steve",
329
+ "message": "你好",
330
+ "reply": "你好!有什么我可以帮助你的吗?",
331
+ "model": "gpt-3.5-turbo"
332
+ }
333
+ },
334
+ "source_key_id_prefix": "abc12345"
335
+ }
336
+ ```
337
+
338
+ ---
339
+
340
+ ## Minecraft 插件/模组集成示例
341
+
342
+ ### 命令格式
343
+
344
+ 在 Minecraft 服务器中,玩家可以使用以下命令调用 AI:
345
+
346
+ ```
347
+ /ic chat <消息内容>
348
+ ```
349
+
350
+ ### 插件实现流程
351
+
352
+ 1. 玩家执行 `/ic chat 你好` 命令
353
+ 2. 插件捕获命令,提取消息内容
354
+ 3. 插件向 InterConnect-Server 发送 POST 请求:
355
+
356
+ ```http
357
+ POST /api/ai/chat
358
+ Authorization: Bearer <server_key>
359
+ Content-Type: application/json
360
+
361
+ {
362
+ "message": "你好",
363
+ "player_name": "Steve",
364
+ "server_id": "my-server-1"
365
+ }
366
+ ```
367
+
368
+ 4. 收到响应后,将 AI 回复发送给玩家
369
+
370
+ ### 响应处理示例(伪代码)
371
+
372
+ ```java
373
+ // 发送请求
374
+ Response response = httpClient.post("/api/ai/chat", {
375
+ "message": playerMessage,
376
+ "player_name": player.getName(),
377
+ "server_id": serverId
378
+ });
379
+
380
+ // 处理响应
381
+ if (response.success) {
382
+ player.sendMessage("[AI] " + response.reply);
383
+ } else {
384
+ player.sendMessage("[AI] 请求失败: " + response.error);
385
+ }
386
+ ```
387
+
388
+ ---
389
+
390
+ ## 支持的 API 提供商
391
+
392
+ 本系统支持所有兼容 OpenAI Chat Completions API 格式的服务:
393
+
394
+ | 提供商 | API URL 示例 |
395
+ |--------|-------------|
396
+ | OpenAI | `https://api.openai.com/v1/chat/completions` |
397
+ | Azure OpenAI | `https://{resource}.openai.azure.com/openai/deployments/{deployment}/chat/completions?api-version=2024-02-01` |
398
+ | Anthropic (via proxy) | 需要兼容层 |
399
+ | 本地模型 (Ollama) | `http://localhost:11434/v1/chat/completions` |
400
+ | 其他兼容服务 | 根据服务商文档配置 |
401
+
402
+ ---
403
+
404
+ ## 错误码参考
405
+
406
+ | HTTP 状态码 | 说明 |
407
+ |-------------|------|
408
+ | 200 | 请求成功 |
409
+ | 201 | 创建成功 |
410
+ | 204 | 删除成功(无内容) |
411
+ | 400 | 请求参数错误 |
412
+ | 401 | 未认证或 API Key 无效 |
413
+ | 403 | 权限不足或功能已禁用 |
414
+ | 404 | 资源不存在 |
415
+ | 500 | 服务器内部错误 |
hf_repo/hf_repo/hf_repo/docs/API.md ADDED
@@ -0,0 +1,730 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # InterConnect-Server API Documentation
2
+
3
+ 本文档描述了 InterConnect-Server 的所有 API 接口(AI 相关接口请参考 [AI_API.md](./AI_API.md))。
4
+
5
+ ## 概述
6
+
7
+ InterConnect-Server 是一个 Minecraft WebSocket API 服务器,提供 API Key 管理、事件广播、服务器命令等功能。
8
+
9
+ **默认端口**: `8000`
10
+
11
+ ---
12
+
13
+ ## 认证
14
+
15
+ 除健康检查接口外,所有 API 请求需要在 Header 中携带 API Key:
16
+
17
+ ```
18
+ Authorization: Bearer <your_api_key>
19
+ ```
20
+
21
+ ### API Key 类型
22
+
23
+ | 类型 | 前缀 | 说明 |
24
+ |------|------|------|
25
+ | Admin Key | `mc_admin_` | 管理员密钥,拥有所有权限 |
26
+ | Regular Key | `mc_key_` | 普通密钥,可管理关联的 Server Key |
27
+ | Server Key | `mc_server_` | 服务器密钥,供 Minecraft 服务器使用 |
28
+
29
+ ---
30
+
31
+ ## 健康检查
32
+
33
+ ### 获取服务器状态
34
+
35
+ 获取服务器健康状态和统计信息,无需认证。
36
+
37
+ **请求**
38
+
39
+ ```http
40
+ GET /health
41
+ ```
42
+
43
+ **成功响应** `200 OK`
44
+
45
+ ```json
46
+ {
47
+ "status": "healthy",
48
+ "timestamp": "2024-01-15T10:30:00.000Z",
49
+ "active_ws": 5,
50
+ "keys_total": 10,
51
+ "admin_active": 2,
52
+ "server_active": 4,
53
+ "regular_active": 4
54
+ }
55
+ ```
56
+
57
+ | 字段 | 类型 | 说明 |
58
+ |------|------|------|
59
+ | `status` | string | 服务器状态 (`healthy` / `unhealthy`) |
60
+ | `timestamp` | string | 当前时间戳 |
61
+ | `active_ws` | number | 活跃的 WebSocket 连接数 |
62
+ | `keys_total` | number | API Key 总数 |
63
+ | `admin_active` | number | 活跃的 Admin Key 数量 |
64
+ | `server_active` | number | 活跃的 Server Key 数量 |
65
+ | `regular_active` | number | 活跃的 Regular Key 数量 |
66
+
67
+ **错误响应** `500 Internal Server Error`
68
+
69
+ ```json
70
+ {
71
+ "status": "unhealthy",
72
+ "error": "Database connection failed"
73
+ }
74
+ ```
75
+
76
+ ---
77
+
78
+ ## API Key 管理
79
+
80
+ **基础路径**: `/manage/keys`
81
+
82
+ ### 获取所有 API Key
83
+
84
+ 获取所有 API Key 的信息列表。
85
+
86
+ **权限**: Admin Key
87
+
88
+ **请求**
89
+
90
+ ```http
91
+ GET /manage/keys
92
+ Authorization: Bearer <admin_key>
93
+ ```
94
+
95
+ **成功响应** `200 OK`
96
+
97
+ ```json
98
+ [
99
+ {
100
+ "id": "550e8400-e29b-41d4-a716-446655440000",
101
+ "name": "My Server Key",
102
+ "description": "Production server",
103
+ "keyPrefix": "mc_key_",
104
+ "keyType": "regular",
105
+ "serverId": "server-1",
106
+ "regularKeyId": null,
107
+ "createdAt": "2024-01-15T10:30:00.000Z",
108
+ "lastUsed": "2024-01-15T12:00:00.000Z",
109
+ "isActive": true
110
+ }
111
+ ]
112
+ ```
113
+
114
+ ---
115
+
116
+ ### 创建 API Key
117
+
118
+ 创建新的 API Key。创建 Regular Key 时会自动生成关联的 Server Key。
119
+
120
+ **权限**: Admin Key
121
+
122
+ **请求**
123
+
124
+ ```http
125
+ POST /manage/keys
126
+ Authorization: Bearer <admin_key>
127
+ Content-Type: application/json
128
+ ```
129
+
130
+ **请求体 - 创建 Regular Key(推荐)**
131
+
132
+ ```json
133
+ {
134
+ "name": "My Server",
135
+ "description": "Production Minecraft server",
136
+ "key_type": "regular",
137
+ "server_id": "server-1"
138
+ }
139
+ ```
140
+
141
+ **请求体 - 创建 Admin Key**
142
+
143
+ ```json
144
+ {
145
+ "name": "New Admin",
146
+ "description": "Backup admin key",
147
+ "key_type": "admin"
148
+ }
149
+ ```
150
+
151
+ | 字段 | 类型 | 必填 | 说明 |
152
+ |------|------|------|------|
153
+ | `name` | string | ✅ | Key 名称 |
154
+ | `description` | string | ❌ | 描述信息 |
155
+ | `key_type` | string | ❌ | Key 类型:`regular`(默认)、`admin` |
156
+ | `server_id` | string | ❌ | 服务器 ID |
157
+
158
+ **成功响应 - Regular Key** `201 Created`
159
+
160
+ ```json
161
+ {
162
+ "regularKey": {
163
+ "id": "550e8400-e29b-41d4-a716-446655440000",
164
+ "name": "My Server",
165
+ "description": "Production Minecraft server",
166
+ "keyPrefix": "mc_key_",
167
+ "keyType": "regular",
168
+ "serverId": "server-1",
169
+ "regularKeyId": null,
170
+ "createdAt": "2024-01-15T10:30:00.000Z",
171
+ "lastUsed": null,
172
+ "isActive": true,
173
+ "key": "mc_key_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"
174
+ },
175
+ "serverKey": {
176
+ "id": "660e8400-e29b-41d4-a716-446655440001",
177
+ "name": "My Server - Server Key",
178
+ "description": "Server Key for My Server",
179
+ "keyPrefix": "mc_server_",
180
+ "keyType": "server",
181
+ "serverId": "server-1",
182
+ "regularKeyId": "550e8400-e29b-41d4-a716-446655440000",
183
+ "createdAt": "2024-01-15T10:30:00.000Z",
184
+ "lastUsed": null,
185
+ "isActive": true,
186
+ "key": "mc_server_x1y2z3a4b5c6d7e8f9g0h1i2j3k4l5m6"
187
+ }
188
+ }
189
+ ```
190
+
191
+ **成功响应 - Admin Key** `201 Created`
192
+
193
+ ```json
194
+ {
195
+ "id": "770e8400-e29b-41d4-a716-446655440002",
196
+ "name": "New Admin",
197
+ "description": "Backup admin key",
198
+ "keyPrefix": "mc_admin_",
199
+ "keyType": "admin",
200
+ "serverId": null,
201
+ "regularKeyId": null,
202
+ "createdAt": "2024-01-15T10:30:00.000Z",
203
+ "lastUsed": null,
204
+ "isActive": true,
205
+ "key": "mc_admin_p1q2r3s4t5u6v7w8x9y0z1a2b3c4d5e6"
206
+ }
207
+ ```
208
+
209
+ > ⚠️ **重要**: `key` 字段只在创建时返回一次,请妥善保存!
210
+
211
+ ---
212
+
213
+ ### 获取 API Key 详情
214
+
215
+ 获取指定 API Key 的详细信息。
216
+
217
+ **权限**: Admin Key
218
+
219
+ **请求**
220
+
221
+ ```http
222
+ GET /manage/keys/:key_id
223
+ Authorization: Bearer <admin_key>
224
+ ```
225
+
226
+ **成功响应** `200 OK`
227
+
228
+ ```json
229
+ {
230
+ "id": "550e8400-e29b-41d4-a716-446655440000",
231
+ "name": "My Server Key",
232
+ "description": "Production server",
233
+ "keyPrefix": "mc_key_",
234
+ "keyType": "regular",
235
+ "serverId": "server-1",
236
+ "regularKeyId": null,
237
+ "createdAt": "2024-01-15T10:30:00.000Z",
238
+ "lastUsed": "2024-01-15T12:00:00.000Z",
239
+ "isActive": true
240
+ }
241
+ ```
242
+
243
+ **错误响应** `404 Not Found`
244
+
245
+ ```json
246
+ {
247
+ "detail": "API Key not found"
248
+ }
249
+ ```
250
+
251
+ ---
252
+
253
+ ### 获取 Server Key 列表
254
+
255
+ 获取当前用户可访问的 Server Key 列表。
256
+
257
+ **权限**: Regular Key 或 Admin Key
258
+
259
+ **请求**
260
+
261
+ ```http
262
+ GET /manage/keys/server-keys
263
+ Authorization: Bearer <regular_key_or_admin_key>
264
+ ```
265
+
266
+ **成功响应** `200 OK`
267
+
268
+ ```json
269
+ [
270
+ {
271
+ "id": "660e8400-e29b-41d4-a716-446655440001",
272
+ "name": "My Server - Server Key",
273
+ "description": "Server Key for My Server",
274
+ "keyPrefix": "mc_server_",
275
+ "keyType": "server",
276
+ "serverId": "server-1",
277
+ "createdAt": "2024-01-15T10:30:00.000Z",
278
+ "lastUsed": "2024-01-15T12:00:00.000Z",
279
+ "isActive": true
280
+ }
281
+ ]
282
+ ```
283
+
284
+ - **Admin Key**: 返回所有 Server Key
285
+ - **Regular Key**: 只返回关联的 Server Key
286
+
287
+ ---
288
+
289
+ ### 创建 Server Key
290
+
291
+ 为指定的 Regular Key 创建新的 Server Key。
292
+
293
+ **权限**: Admin Key
294
+
295
+ **请求**
296
+
297
+ ```http
298
+ POST /manage/keys/server-keys
299
+ Authorization: Bearer <admin_key>
300
+ Content-Type: application/json
301
+ ```
302
+
303
+ **请求体**
304
+
305
+ ```json
306
+ {
307
+ "name": "Backup Server Key",
308
+ "description": "Backup key for server-1",
309
+ "server_id": "server-1",
310
+ "regular_key_id": "550e8400-e29b-41d4-a716-446655440000"
311
+ }
312
+ ```
313
+
314
+ | 字段 | 类型 | 必填 | 说明 |
315
+ |------|------|------|------|
316
+ | `name` | string | ✅ | Key 名称 |
317
+ | `description` | string | ❌ | 描述信息 |
318
+ | `server_id` | string | ❌ | 服务器 ID |
319
+ | `regular_key_id` | string | ✅ | 关联的 Regular Key ID |
320
+
321
+ **成功响应** `201 Created`
322
+
323
+ ```json
324
+ {
325
+ "id": "880e8400-e29b-41d4-a716-446655440003",
326
+ "name": "Backup Server Key",
327
+ "description": "Backup key for server-1",
328
+ "keyPrefix": "mc_server_",
329
+ "keyType": "server",
330
+ "serverId": "server-1",
331
+ "regularKeyId": "550e8400-e29b-41d4-a716-446655440000",
332
+ "createdAt": "2024-01-15T10:30:00.000Z",
333
+ "lastUsed": null,
334
+ "isActive": true,
335
+ "key": "mc_server_n1o2p3q4r5s6t7u8v9w0x1y2z3a4b5c6"
336
+ }
337
+ ```
338
+
339
+ ---
340
+
341
+ ### 激活 API Key
342
+
343
+ 激活指定的 API Key。
344
+
345
+ **权限**:
346
+ - Admin Key: 可激活任意 Key
347
+ - Regular Key: 只能激活关联的 Server Key
348
+
349
+ **请求**
350
+
351
+ ```http
352
+ PATCH /manage/keys/:key_id/activate
353
+ Authorization: Bearer <api_key>
354
+ ```
355
+
356
+ **成功响应** `200 OK`
357
+
358
+ ```json
359
+ {
360
+ "message": "Key '550e8400-e29b-41d4-a716-446655440000' activated."
361
+ }
362
+ ```
363
+
364
+ ---
365
+
366
+ ### 停用 API Key
367
+
368
+ 停用指定的 API Key。
369
+
370
+ **权限**:
371
+ - Admin Key: 可停用任意 Key(不能停用最后一个活跃的 Admin Key)
372
+ - Regular Key: 只能停用关联的 Server Key
373
+
374
+ **请求**
375
+
376
+ ```http
377
+ PATCH /manage/keys/:key_id/deactivate
378
+ Authorization: Bearer <api_key>
379
+ ```
380
+
381
+ **成功响应** `200 OK`
382
+
383
+ ```json
384
+ {
385
+ "message": "Key '550e8400-e29b-41d4-a716-446655440000' deactivated."
386
+ }
387
+ ```
388
+
389
+ **错误响应** `400 Bad Request`
390
+
391
+ ```json
392
+ {
393
+ "detail": "Cannot deactivate your own key."
394
+ }
395
+ ```
396
+
397
+ ```json
398
+ {
399
+ "detail": "Cannot deactivate last active Admin Key."
400
+ }
401
+ ```
402
+
403
+ ---
404
+
405
+ ### 删除 API Key
406
+
407
+ 永久删除指定的 API Key。
408
+
409
+ **权限**: Admin Key
410
+
411
+ **请求**
412
+
413
+ ```http
414
+ DELETE /manage/keys/:key_id
415
+ Authorization: Bearer <admin_key>
416
+ ```
417
+
418
+ **成功响应** `204 No Content`
419
+
420
+ 无响应体
421
+
422
+ **错误响应** `400 Bad Request`
423
+
424
+ ```json
425
+ {
426
+ "detail": "Cannot delete your own key."
427
+ }
428
+ ```
429
+
430
+ ```json
431
+ {
432
+ "detail": "Cannot delete the last Admin Key"
433
+ }
434
+ ```
435
+
436
+ ---
437
+
438
+ ## 事件接口
439
+
440
+ **基础路径**: `/api/events`
441
+
442
+ ### 发送事件
443
+
444
+ 发送 Minecraft 事件并广播给所有 WebSocket 连接。
445
+
446
+ **权限**: 任意有效 API Key
447
+
448
+ **请求**
449
+
450
+ ```http
451
+ POST /api/events
452
+ Authorization: Bearer <api_key>
453
+ Content-Type: application/json
454
+ ```
455
+
456
+ **请求体**
457
+
458
+ ```json
459
+ {
460
+ "event_type": "player_join",
461
+ "server_name": "server-1",
462
+ "timestamp": "2024-01-15T10:30:00.000Z",
463
+ "data": {
464
+ "player_name": "Steve",
465
+ "player_uuid": "550e8400-e29b-41d4-a716-446655440000"
466
+ }
467
+ }
468
+ ```
469
+
470
+ | 字段 | 类型 | 必填 | 说明 |
471
+ |------|------|------|------|
472
+ | `event_type` | string | ✅ | 事件类型 |
473
+ | `server_name` | string | ✅ | 服务器名称 |
474
+ | `timestamp` | string | ✅ | 事件时间戳 (ISO 8601) |
475
+ | `data` | object | ✅ | 事件数据 |
476
+
477
+ **支持的事件类型**
478
+
479
+ | 事件类型 | 说明 |
480
+ |----------|------|
481
+ | `player_join` | 玩家加入服务器 |
482
+ | `player_leave` | 玩家离开服务器 |
483
+ | `message` | 聊天消息 |
484
+ | `server_command` | 服务器命令 |
485
+ | `ai_chat` | AI 聊天 |
486
+
487
+ **成功响应** `200 OK`
488
+
489
+ ```json
490
+ {
491
+ "message": "Event received and broadcasted"
492
+ }
493
+ ```
494
+
495
+ **错误响应** `400 Bad Request`
496
+
497
+ ```json
498
+ {
499
+ "detail": "Missing required event fields"
500
+ }
501
+ ```
502
+
503
+ ```json
504
+ {
505
+ "detail": "Invalid event_type: unknown_event"
506
+ }
507
+ ```
508
+
509
+ ---
510
+
511
+ ## 服务器接口
512
+
513
+ **基础路径**: `/api/server`
514
+
515
+ ### 获取服务器信息
516
+
517
+ 获取 Minecraft 服务器信息���
518
+
519
+ **权限**: Regular Key 或 Admin Key
520
+
521
+ **请求**
522
+
523
+ ```http
524
+ GET /api/server/info
525
+ Authorization: Bearer <api_key>
526
+ ```
527
+
528
+ **查询参数**(仅 Admin Key)
529
+
530
+ | 参数 | 类型 | 说明 |
531
+ |------|------|------|
532
+ | `server_id` | string | 指定服务器 ID |
533
+
534
+ **成功响应** `200 OK`
535
+
536
+ ```json
537
+ {
538
+ "server_id": "server-1",
539
+ "status": "running",
540
+ "online_players": 5,
541
+ "max_players": 20,
542
+ "version": "1.20.1",
543
+ "uptime": "2h 30m",
544
+ "tps": 19.8
545
+ }
546
+ ```
547
+
548
+ **错误响应** `400 Bad Request`
549
+
550
+ ```json
551
+ {
552
+ "detail": "server_id is required for this key"
553
+ }
554
+ ```
555
+
556
+ ---
557
+
558
+ ### 发送服务器命令
559
+
560
+ 向 Minecraft 服务器发送命令。
561
+
562
+ **权限**: Regular Key 或 Admin Key
563
+
564
+ **请求**
565
+
566
+ ```http
567
+ POST /api/server/command
568
+ Authorization: Bearer <api_key>
569
+ Content-Type: application/json
570
+ ```
571
+
572
+ **请求体**
573
+
574
+ ```json
575
+ {
576
+ "command": "say Hello World",
577
+ "server_id": "server-1"
578
+ }
579
+ ```
580
+
581
+ | 字段 | 类型 | 必填 | 说明 |
582
+ |------|------|------|------|
583
+ | `command` | string | ✅ | 要执行的命令 |
584
+ | `server_id` | string | ❌ | 服务器 ID(Admin Key 可指定) |
585
+
586
+ **成功响应** `200 OK`
587
+
588
+ ```json
589
+ {
590
+ "message": "Command sent successfully",
591
+ "command": "say Hello World",
592
+ "server_id": "server-1"
593
+ }
594
+ ```
595
+
596
+ **错误响应** `400 Bad Request`
597
+
598
+ ```json
599
+ {
600
+ "detail": "Command is required"
601
+ }
602
+ ```
603
+
604
+ **错误响应** `403 Forbidden`
605
+
606
+ ```json
607
+ {
608
+ "detail": "Command is not allowed for Admin Key"
609
+ }
610
+ ```
611
+
612
+ **Admin Key 默认禁止的命令**
613
+
614
+ 以下命令默认禁止 Admin Key 执行(可通过环境变量配置):
615
+
616
+ - `stop`, `restart`, `reload`
617
+ - `op`, `deop`
618
+ - `ban`, `ban-ip`, `banlist`, `pardon`, `pardon-ip`
619
+ - `whitelist`, `kick`
620
+ - `save-all`, `save-on`, `save-off`
621
+
622
+ ---
623
+
624
+ ## WebSocket 接口
625
+
626
+ ### 连接 WebSocket
627
+
628
+ 建立 WebSocket 连接以接收实时事件。
629
+
630
+ **端点**
631
+
632
+ ```
633
+ ws://<host>:<port>/ws?api_key=<your_api_key>
634
+ ```
635
+
636
+ **示例**
637
+
638
+ ```
639
+ ws://localhost:8000/ws?api_key=mc_server_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
640
+ ```
641
+
642
+ **连接成功后**
643
+
644
+ 服务器会推送 Minecraft 事件消息:
645
+
646
+ ```json
647
+ {
648
+ "type": "minecraft_event",
649
+ "event": {
650
+ "event_type": "player_join",
651
+ "server_name": "server-1",
652
+ "timestamp": "2024-01-15T10:30:00.000Z",
653
+ "data": {
654
+ "player_name": "Steve"
655
+ }
656
+ },
657
+ "source_key_id_prefix": "550e8400"
658
+ }
659
+ ```
660
+
661
+ ### 心跳检测
662
+
663
+ 客户端可以发送 ping 消息保持连接:
664
+
665
+ **发送**
666
+
667
+ ```json
668
+ {
669
+ "type": "ping"
670
+ }
671
+ ```
672
+
673
+ **响应**
674
+
675
+ ```json
676
+ {
677
+ "type": "pong"
678
+ }
679
+ ```
680
+
681
+ ---
682
+
683
+ ## 根路径
684
+
685
+ ### 获取服务器信息
686
+
687
+ **请求**
688
+
689
+ ```http
690
+ GET /
691
+ ```
692
+
693
+ **成功响应** `200 OK`
694
+
695
+ ```json
696
+ {
697
+ "message": "Minecraft WebSocket API Server (Node.js)",
698
+ "dashboard": "/dashboard",
699
+ "websocket": "ws://localhost:8000/ws",
700
+ "version": "1.0.0"
701
+ }
702
+ ```
703
+
704
+ ---
705
+
706
+ ## 错误码参考
707
+
708
+ | HTTP 状态码 | 说明 |
709
+ |-------------|------|
710
+ | 200 | 请求成功 |
711
+ | 201 | 创建成功 |
712
+ | 204 | 删除成功(无内容) |
713
+ | 400 | 请求参数错误 |
714
+ | 401 | 未认证或 API Key 无效 |
715
+ | 403 | 权限不足 |
716
+ | 404 | 资源不存在 |
717
+ | 500 | 服务器内部错误 |
718
+
719
+ ---
720
+
721
+ ## 环境变量配置
722
+
723
+ | 变量名 | 默认值 | 说明 |
724
+ |--------|--------|------|
725
+ | `SERVER_HOST` | `0.0.0.0` | 服务器监听地址 |
726
+ | `SERVER_PORT` | `8000` | 服务器端口 |
727
+ | `DATABASE_PATH` | `minecraft_ws.db` | 数据库文件路径 |
728
+ | `DASHBOARD_PORT` | `3000` | Dashboard 独立端口(可选) |
729
+ | `ADMIN_ALLOWED_COMMANDS` | - | Admin Key 允许的命令(逗号分隔) |
730
+ | `ADMIN_BLOCKED_COMMANDS` | - | Admin Key 禁止的命令(逗号分隔) |
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/docs/TECH_STACK.md ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 项目技术栈文档
2
+
3
+ 本文件详细说明 InterConnect-Server 的技术组成、模块职责与关键依赖版本。
4
+
5
+ ## 1. 运行环境
6
+ - **Node.js**: 建议 v18+(Docker 镜像使用 `node:18-alpine`)
7
+ - **包管理**: npm
8
+ - **平台**: Windows / Linux / macOS
9
+
10
+ ## 2. 核心依赖与用途
11
+
12
+ ### 2.1 后端服务
13
+ - **express@4.18.x**
14
+ - REST API 服务框架
15
+ - 路由结构:`src/routes/*.js`
16
+ - 入口:`src/server.js`
17
+
18
+ - **ws@8.14.x**
19
+ - WebSocket 服务端实现
20
+ - 协议:`/ws?api_key=...`
21
+ - 连接管理:`src/websocket.js`
22
+
23
+ - **sql.js@1.10.x**
24
+ - SQLite 在内存中运行并导出文件
25
+ - 本地持久化文件:`minecraft_ws.db`
26
+ - 初始化与表结构:`src/database.js`
27
+
28
+ - **bcryptjs@2.4.x**
29
+ - Key 哈希存储
30
+ - 认证校验:`db.verifyApiKey()`
31
+ - 恢复 Token 哈希:`admin_recovery` 表
32
+
33
+ - **uuid@9.0.x**
34
+ - 生成密钥 ID 与原始 Key
35
+
36
+ - **dotenv@16.3.x**
37
+ - 读取环境变量
38
+ - 用于命令过滤与数据库路径等配置
39
+
40
+ ### 2.2 CLI 工具
41
+ - **commander@11.1.x**
42
+ - CLI 命令定义
43
+ - 命令入口:`cli/cli.js`
44
+ - 支持管理 Key / health / reset-admin
45
+
46
+ - **http / https (Node 内置)**
47
+ - CLI 直接调用 API
48
+
49
+ ### 2.3 前端 Dashboard
50
+ 纯静态页面,无前端框架。
51
+ - `dashboard/public/index.html`
52
+ - `dashboard/public/app.js`
53
+ - `dashboard/public/style.css`
54
+
55
+ ## 3. 关键模块职责
56
+
57
+ ### 3.1 `src/server.js`
58
+ - 服务入口
59
+ - REST API 路由挂载
60
+ - WebSocket 升级与连接管理
61
+ - 初始化数据库与 Admin Key / Recovery Token
62
+
63
+ ### 3.2 `src/database.js`
64
+ - SQLite 初始化与持久化
65
+ - Key 创建 / 激活 / 停用 / 删除
66
+ - Admin Recovery Token 管理
67
+ - 事件日志写入
68
+
69
+ ### 3.3 `src/auth.js`
70
+ - Bearer Token 认证中间件
71
+ - Admin / Regular 权限判断
72
+
73
+ ### 3.4 `src/routes/keys.js`
74
+ - Key CRUD
75
+ - `/manage/keys/server-keys` 列表与激活/停用逻辑
76
+
77
+ ### 3.5 `src/routes/server.js`
78
+ - 服务器信息与命令入口
79
+ - Admin 命令过滤(可配置 allowlist / blocklist)
80
+
81
+ ### 3.6 `src/websocket.js`
82
+ - WebSocket 连接管理
83
+ - 广播消息
84
+
85
+ ## 4. 数据结构
86
+
87
+ ### 4.1 `api_keys`
88
+ | 字段 | 类型 | 说明 |
89
+ |---|---|---|
90
+ | id | TEXT | 主键 |
91
+ | name | TEXT | Key 名称 |
92
+ | description | TEXT | 描述 |
93
+ | key_hash | TEXT | bcrypt 哈希 |
94
+ | key_prefix | TEXT | mc_admin_ / mc_key_ / mc_server_ |
95
+ | key_type | TEXT | admin / regular / server |
96
+ | server_id | TEXT | 关联服务器 |
97
+ | regular_key_id | TEXT | 关联 Regular Key |
98
+ | created_at | TEXT | 创建时间 |
99
+ | last_used | TEXT | 最近使用 |
100
+ | is_active | INTEGER | 是否启用 |
101
+
102
+ ### 4.2 `event_logs`
103
+ 事件上报记录。
104
+
105
+ ### 4.3 `admin_recovery`
106
+ | 字段 | 类型 | 说明 |
107
+ |---|---|---|
108
+ | id | INTEGER | 固定为 1 |
109
+ | token_hash | TEXT | 恢复 Token 哈希 |
110
+ | created_at | TEXT | 创建时间 |
111
+ | last_used | TEXT | 最近使用 |
112
+
113
+ ## 5. 权限与安全策略
114
+ - Admin Key:完整管理权限
115
+ - Regular Key:只允许操作自身 Server Key
116
+ - Server Key:仅用于插件/Mod,不允许登录 Dashboard
117
+ - Admin 命令过滤:
118
+ - `ADMIN_ALLOWED_COMMANDS` 非空时为 allowlist
119
+ - 否则使用 `ADMIN_BLOCKED_COMMANDS` 作为默认拦截表
120
+
121
+ ## 6. 部署与运行
122
+
123
+ ### 6.1 直接运行
124
+ ```bash
125
+ npm install
126
+ npm start
127
+ ```
128
+
129
+ ### 6.2 Docker
130
+ ```bash
131
+ docker-compose up -d
132
+ ```
133
+
134
+ ### 6.3 数据持久化
135
+ 必须保留 `minecraft_ws.db`,否则 Admin Key 将重新生成。
136
+
137
+ ## 7. 关键配置项
138
+ ```env
139
+ SERVER_HOST=0.0.0.0
140
+ SERVER_PORT=8000
141
+ DATABASE_PATH=minecraft_ws.db
142
+ ADMIN_ALLOWED_COMMANDS=list,tps,version
143
+ ADMIN_BLOCKED_COMMANDS=stop,deop,op,ban
144
+ ```
145
+
146
+ ## 8. 兼容与约束
147
+ - WebSocket 与 REST 使用同一端口
148
+ - Dashboard 只通过 `/dashboard` 提供
149
+ - Admin Key 恢复必须使用 recovery token,禁止删除数据库
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/cli/cli.js CHANGED
@@ -4,6 +4,7 @@ const http = require('http');
4
  const https = require('https');
5
  const fs = require('fs');
6
  require('dotenv').config();
 
7
 
8
  const HARDCODED_API_URL = 'http://localhost:8000';
9
  const HARDCODED_ADMIN_KEY = process.env.ADMIN_KEY || null;
@@ -386,4 +387,35 @@ program
386
  }
387
  });
388
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
389
  program.parse();
 
4
  const https = require('https');
5
  const fs = require('fs');
6
  require('dotenv').config();
7
+ const Database = require('../src/database');
8
 
9
  const HARDCODED_API_URL = 'http://localhost:8000';
10
  const HARDCODED_ADMIN_KEY = process.env.ADMIN_KEY || null;
 
387
  }
388
  });
389
 
390
+ program
391
+ .command('reset-admin')
392
+ .description('Reset Admin Key using recovery token (offline)')
393
+ .requiredOption('-r, --recovery-token <token>', 'Admin recovery token')
394
+ .option('--db-path <path>', 'Database path', process.env.DATABASE_PATH || 'minecraft_ws.db')
395
+ .option('--keep-existing', 'Keep existing Admin Keys active')
396
+ .option('--name <name>', 'New Admin Key name', 'Recovered Admin Key')
397
+ .action(async (options) => {
398
+ try {
399
+ if (!fs.existsSync(options.dbPath)) {
400
+ throw new Error(`Database not found at ${options.dbPath}`);
401
+ }
402
+
403
+ const db = new Database(options.dbPath);
404
+ await db.init();
405
+
406
+ const result = await db.resetAdminKeyWithRecovery(options.recoveryToken, {
407
+ deactivateExisting: !options.keepExisting,
408
+ name: options.name
409
+ });
410
+
411
+ console.log('Admin Key reset successful.');
412
+ console.log(` ID : ${result.id}`);
413
+ console.log(` Name : ${result.name}`);
414
+ console.log(` Key : ${result.key}`);
415
+ } catch (error) {
416
+ console.error(`\x1b[31mReset failed: ${error.message}\x1b[0m`);
417
+ process.exit(1);
418
+ }
419
+ });
420
+
421
  program.parse();
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/dashboard/public/app.js CHANGED
@@ -1,402 +1,399 @@
1
  const API_URL = window.location.origin;
2
- let superKey = null;
 
3
  let ws = null;
 
 
4
 
5
  const loginScreen = document.getElementById('login-screen');
6
- const dashboardScreen = document.getElementById('dashboard-screen');
 
7
  const loginForm = document.getElementById('login-form');
8
  const loginError = document.getElementById('login-error');
9
- const logoutBtn = document.getElementById('logout-btn');
 
 
 
10
  const createKeyBtn = document.getElementById('create-key-btn');
11
  const createKeyModal = document.getElementById('create-key-modal');
12
  const createKeyForm = document.getElementById('create-key-form');
13
  const cancelCreateBtn = document.getElementById('cancel-create-btn');
14
  const keyDetailsModal = document.getElementById('key-details-modal');
15
  const closeDetailsBtn = document.getElementById('close-details-btn');
 
 
 
 
 
 
16
 
17
- loginForm.addEventListener('submit', async (e) => {
18
- e.preventDefault();
19
- const key = document.getElementById('super-key-input').value;
20
-
21
- try {
22
- // 验证密钥是否有效
23
- const response = await fetch(`${API_URL}/health`, {
24
- headers: { 'Authorization': `Bearer ${key}` }
25
- });
26
-
27
- if (response.ok) {
28
- superKey = key;
29
-
30
- // 验证密钥类型
31
- let userKeyType = null;
32
- let userServerId = null;
33
-
34
- // 尝试Admin Key验证
35
- const adminResponse = await fetch(`${API_URL}/manage/keys`, {
36
- headers: { 'Authorization': `Bearer ${key}` }
37
- });
38
-
39
- if (adminResponse.ok) {
40
- userKeyType = 'admin';
41
- } else {
42
- // 尝试Server Key验证
43
- const serverResponse = await fetch(`${API_URL}/api/server/info`, {
44
- headers: { 'Authorization': `Bearer ${key}` }
45
- });
46
-
47
- if (serverResponse.ok) {
48
- userKeyType = 'server';
49
- const serverData = await serverResponse.json();
50
- userServerId = serverData.server_id;
51
- } else {
52
- // 尝试Regular Key验证(获取自己的Server Key列表)
53
- const regularResponse = await fetch(`${API_URL}/manage/keys/server-keys`, {
54
- headers: { 'Authorization': `Bearer ${key}` }
55
- });
56
-
57
- if (regularResponse.ok) {
58
- userKeyType = 'regular';
59
- } else {
60
- throw new Error('无效的密钥或权限不足');
61
- }
62
- }
63
- }
64
-
65
- loginError.textContent = '';
66
- showDashboard(userKeyType, userServerId);
67
- } else {
68
- loginError.textContent = '无效的密钥';
69
- }
70
- } catch (error) {
71
- loginError.textContent = error.message || '无法连接到服务器';
72
  }
73
- });
74
 
75
- logoutBtn.addEventListener('click', () => {
76
- superKey = null;
77
  if (ws) {
78
  ws.close();
79
  ws = null;
80
  }
81
- loginScreen.classList.remove('hidden');
82
- dashboardScreen.classList.add('hidden');
83
- });
84
 
85
- createKeyBtn.addEventListener('click', () => {
86
- createKeyModal.classList.remove('hidden');
87
- });
 
 
 
 
 
 
 
88
 
89
- cancelCreateBtn.addEventListener('click', () => {
90
- createKeyModal.classList.add('hidden');
91
- createKeyForm.reset();
92
- });
93
 
94
- closeDetailsBtn.addEventListener('click', () => {
95
- keyDetailsModal.classList.add('hidden');
96
- });
97
 
98
- createKeyForm.addEventListener('submit', async (e) => {
99
- e.preventDefault();
100
-
101
- const name = document.getElementById('key-name').value;
102
- const description = document.getElementById('key-description').value;
103
- const isSuper = document.getElementById('key-is-super').checked;
104
-
105
- try {
106
- const response = await fetch(`${API_URL}/manage/keys`, {
107
- method: 'POST',
108
- headers: {
109
- 'Authorization': `Bearer ${superKey}`,
110
- 'Content-Type': 'application/json'
111
- },
112
- body: JSON.stringify({
113
- name,
114
- description,
115
- is_super_key: isSuper
116
- })
117
- });
118
-
119
- if (response.ok) {
120
- const result = await response.json();
121
- createKeyModal.classList.add('hidden');
122
- createKeyForm.reset();
123
-
124
- showKeyCreatedModal(result);
125
- loadKeys();
126
- } else {
127
- const error = await response.json();
128
- alert('创建失败: ' + error.detail);
129
- }
130
- } catch (error) {
131
- alert('创建失败: ' + error.message);
132
  }
133
- });
134
 
135
- function showKeyCreatedModal(keyData) {
136
- const content = `
137
- <p><strong>密钥创建成功!</strong></p>
138
- <p>名称: ${keyData.name}</p>
139
- <p>类型: ${keyData.isSuperKey ? 'SuperKey' : '普通密钥'}</p>
140
- <div class="key-display">
141
- <strong>⚠️ 请立即复制并保存此密钥(仅显示一次):</strong><br>
142
- ${keyData.key}
143
- </div>
144
- `;
145
-
146
- document.getElementById('key-details-content').innerHTML = content;
147
- keyDetailsModal.classList.remove('hidden');
148
  }
149
 
150
- // 全局变量
151
- let currentUserKeyType = null;
152
- let currentUserServerId = null;
 
 
 
 
 
 
 
 
 
153
 
154
- async function showDashboard(userKeyType, userServerId) {
155
- currentUserKeyType = userKeyType;
156
- currentUserServerId = userServerId;
157
-
158
  loginScreen.classList.add('hidden');
159
- dashboardScreen.classList.remove('hidden');
160
-
161
- // 显示用户信息
162
- const userInfo = document.getElementById('user-info');
163
- const keyTypeIcons = { admin: '👑', server: '🖥️', regular: '🔑' };
164
- const keyTypeNames = { admin: 'Admin', server: 'Server', regular: 'Regular' };
165
- userInfo.textContent = `${keyTypeIcons[userKeyType]} ${keyTypeNames[userKeyType]} 用户`;
166
-
167
- // 根据权限显示/隐藏功能
168
- const adminSection = document.getElementById('admin-section');
169
- const serverSection = document.getElementById('server-section');
170
-
171
- if (userKeyType === 'admin') {
172
- adminSection.style.display = 'block';
173
- serverSection.style.display = 'block';
174
- loadKeys();
175
- } else if (userKeyType === 'server') {
176
- adminSection.style.display = 'none';
177
- serverSection.style.display = 'block';
178
- loadServerInfo();
179
- } else if (userKeyType === 'regular') {
180
- adminSection.style.display = 'none';
181
- serverSection.style.display = 'block';
182
- loadRegularServerKeys();
183
- }
184
-
185
- // 根据用户类型控制创建密钥按钮的显示
186
- const createKeyBtn = document.getElementById('create-key-btn');
187
- if (createKeyBtn) {
188
- createKeyBtn.style.display = userKeyType === 'admin' ? 'block' : 'none';
189
- }
190
-
191
  loadStats();
192
  connectWebSocket();
193
-
194
- setInterval(loadStats, 5000);
195
- }
196
 
197
- async function loadStats() {
198
- try {
199
- const response = await fetch(`${API_URL}/health`);
200
- const data = await response.json();
201
-
202
- document.getElementById('stat-connections').textContent = data.active_ws || 0;
203
- document.getElementById('stat-total-keys').textContent = data.keys_total || 0;
204
- document.getElementById('stat-super-keys').textContent = data.super_active || 0;
205
- document.getElementById('stat-regular-keys').textContent = data.regular_active || 0;
206
- } catch (error) {
207
- console.error('Failed to load stats:', error);
208
- }
209
  }
210
 
211
- async function loadKeys() {
 
 
 
212
  try {
213
  const response = await fetch(`${API_URL}/manage/keys`, {
214
- headers: { 'Authorization': `Bearer ${superKey}` }
215
  });
216
-
217
  if (response.ok) {
218
  const keys = await response.json();
219
- renderKeys(keys);
 
 
220
  }
221
  } catch (error) {
222
- console.error('Failed to load keys:', error);
223
  }
224
  }
225
 
226
- async function loadRegularServerKeys() {
 
 
 
227
  try {
228
  const response = await fetch(`${API_URL}/manage/keys/server-keys`, {
229
- headers: { 'Authorization': `Bearer ${superKey}` }
230
  });
231
-
232
  if (response.ok) {
233
- const serverKeys = await response.json();
234
- renderServerKeys(serverKeys);
235
  } else {
236
- document.getElementById('keys-list').innerHTML = '<p>无法加载Server Key列表</p>';
237
  }
238
  } catch (error) {
239
- console.error('Failed to load server keys:', error);
240
- document.getElementById('keys-list').innerHTML = '<p>无法加载Server Key列表</p>';
241
  }
242
  }
243
 
244
- function renderServerKeys(keys) {
245
- const keysList = document.getElementById('keys-list');
246
-
247
- if (keys.length === 0) {
248
- keysList.innerHTML = '<p>暂无关联的Server Key</p>';
249
  return;
250
  }
251
-
252
- keysList.innerHTML = keys.map(key => `
253
- <div class="key-card">
 
 
 
 
 
254
  <div class="key-info">
255
  <h3>
256
- <span class="key-badge server">🖥️ Server</span>
 
 
257
  <span class="key-badge ${key.isActive ? 'active' : 'inactive'}">
258
- ${key.isActive ? '活跃' : '已停用'}
259
  </span>
260
  ${key.name}
261
  </h3>
262
  <p>ID: ${key.id}</p>
263
- <p>前缀: ${key.keyPrefix}</p>
264
- ${key.serverId ? `<p>服务器ID: ${key.serverId}</p>` : ''}
265
- <p>创建时间: ${new Date(key.createdAt).toLocaleString('zh-CN')}</p>
266
- <p>最后使用: ${key.lastUsed ? new Date(key.lastUsed).toLocaleString('zh-CN') : '从未使用'}</p>
267
  </div>
268
  <div class="key-actions">
269
- ${key.isActive ?
270
- `<button class="btn-danger" onclick="deactivateKey('${key.id}')">停用</button>` :
271
- `<button class="btn-success" onclick="activateKey('${key.id}')">激活</button>`
272
  }
273
- <button class="btn-danger" onclick="deleteKey('${key.id}', '${key.name}')">删除</button>
274
  </div>
275
  </div>
276
  `).join('');
277
  }
278
 
279
- function renderKeys(keys) {
280
- const keysList = document.getElementById('keys-list');
281
-
282
- if (keys.length === 0) {
283
- keysList.innerHTML = '<p>暂无API密钥</p>';
284
  return;
285
  }
286
-
287
- keysList.innerHTML = keys.map(key => `
288
- <div class="key-card ${key.keyType === 'admin' ? 'super' : ''}">
 
 
 
 
 
289
  <div class="key-info">
290
  <h3>
291
- <span class="key-badge ${key.keyType}">
292
- ${key.keyType === 'admin' ? '👑 Admin' : key.keyType === 'server' ? '🖥️ Server' : '🔑 Regular'}
293
- </span>
294
  <span class="key-badge ${key.isActive ? 'active' : 'inactive'}">
295
- ${key.isActive ? '活跃' : '已停用'}
296
  </span>
297
  ${key.name}
298
  </h3>
299
  <p>ID: ${key.id}</p>
300
- <p>前缀: ${key.keyPrefix}</p>
301
- ${key.serverId ? `<p>服务器ID: ${key.serverId}</p>` : ''}
302
- <p>创建时间: ${new Date(key.createdAt).toLocaleString('zh-CN')}</p>
303
- <p>最后使用: ${key.lastUsed ? new Date(key.lastUsed).toLocaleString('zh-CN') : '从未使用'}</p>
304
  </div>
305
  <div class="key-actions">
306
- ${key.isActive ?
307
- `<button class="btn-danger" onclick="deactivateKey('${key.id}')">停用</button>` :
308
- `<button class="btn-success" onclick="activateKey('${key.id}')">激活</button>`
309
  }
310
- <button class="btn-danger" onclick="deleteKey('${key.id}', '${key.name}')">删除</button>
311
  </div>
312
  </div>
313
  `).join('');
314
  }
315
 
316
  async function activateKey(keyId) {
 
 
 
317
  try {
318
  const response = await fetch(`${API_URL}/manage/keys/${keyId}/activate`, {
319
  method: 'PATCH',
320
- headers: { 'Authorization': `Bearer ${superKey}` }
321
  });
322
-
323
- if (response.ok) {
324
- if (currentUserKeyType === 'admin') {
325
- loadKeys();
326
- } else if (currentUserKeyType === 'regular') {
327
- loadRegularServerKeys();
328
- }
329
- } else {
330
  const error = await response.json();
331
- alert('激活失败: ' + error.detail);
 
 
 
 
 
 
 
332
  }
333
  } catch (error) {
334
- alert('激活失败: ' + error.message);
335
  }
336
  }
337
 
338
  async function deactivateKey(keyId) {
 
 
 
339
  try {
340
  const response = await fetch(`${API_URL}/manage/keys/${keyId}/deactivate`, {
341
  method: 'PATCH',
342
- headers: { 'Authorization': `Bearer ${superKey}` }
343
  });
344
-
345
- if (response.ok) {
346
- if (currentUserKeyType === 'admin') {
347
- loadKeys();
348
- } else if (currentUserKeyType === 'regular') {
349
- loadRegularServerKeys();
350
- }
351
- } else {
352
  const error = await response.json();
353
- alert('停用失败: ' + error.detail);
 
 
 
 
 
 
 
354
  }
355
  } catch (error) {
356
- alert('停用失败: ' + error.message);
357
  }
358
  }
359
 
360
  async function deleteKey(keyId, keyName) {
361
- if (!confirm(`确定要删除密钥 "${keyName}" 吗?此操作无法撤销。`)) {
 
 
 
362
  return;
363
  }
364
-
365
  try {
366
  const response = await fetch(`${API_URL}/manage/keys/${keyId}`, {
367
  method: 'DELETE',
368
- headers: { 'Authorization': `Bearer ${superKey}` }
369
  });
370
-
371
- if (response.ok) {
372
- if (currentUserKeyType === 'admin') {
373
- loadKeys();
374
- } else if (currentUserKeyType === 'regular') {
375
- loadRegularServerKeys();
376
- }
377
- } else {
378
  const error = await response.json();
379
- alert('删除失败: ' + error.detail);
 
380
  }
 
 
381
  } catch (error) {
382
- alert('删除失败: ' + error.message);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
383
  }
384
  }
385
 
386
  function connectWebSocket() {
387
- if (!superKey) return;
388
-
389
- const wsUrl = `ws://localhost:8000/ws?api_key=${superKey}`;
 
 
 
390
  ws = new WebSocket(wsUrl);
391
-
392
  ws.onopen = () => {
393
  console.log('WebSocket connected');
394
  };
395
-
396
  ws.onmessage = (event) => {
397
  try {
398
  const message = JSON.parse(event.data);
399
-
400
  if (message.type === 'minecraft_event') {
401
  addEventToList(message.event);
402
  }
@@ -404,21 +401,23 @@ function connectWebSocket() {
404
  console.error('Failed to parse WebSocket message:', error);
405
  }
406
  };
407
-
408
  ws.onerror = (error) => {
409
  console.error('WebSocket error:', error);
410
  };
411
-
412
  ws.onclose = () => {
413
  console.log('WebSocket disconnected');
414
- setTimeout(() => {
415
- if (superKey) {
416
- connectWebSocket();
417
- }
418
- }, 5000);
 
 
419
  };
420
-
421
- setInterval(() => {
422
  if (ws && ws.readyState === WebSocket.OPEN) {
423
  ws.send(JSON.stringify({ type: 'ping' }));
424
  }
@@ -426,19 +425,183 @@ function connectWebSocket() {
426
  }
427
 
428
  function addEventToList(event) {
429
- const eventsList = document.getElementById('events-list');
430
-
 
 
431
  const eventItem = document.createElement('div');
432
  eventItem.className = 'event-item';
433
  eventItem.innerHTML = `
434
  <strong>${event.event_type}</strong> - ${event.server_name}<br>
435
- <small>${new Date(event.timestamp).toLocaleString('zh-CN')}</small><br>
436
  <pre>${JSON.stringify(event.data, null, 2)}</pre>
437
  `;
438
-
439
- eventsList.insertBefore(eventItem, eventsList.firstChild);
440
-
441
- while (eventsList.children.length > 50) {
442
- eventsList.removeChild(eventsList.lastChild);
 
 
 
 
 
 
443
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
444
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  const API_URL = window.location.origin;
2
+ let apiKey = null;
3
+ let currentRole = null;
4
  let ws = null;
5
+ let statsIntervalId = null;
6
+ let wsPingIntervalId = null;
7
 
8
  const loginScreen = document.getElementById('login-screen');
9
+ const adminScreen = document.getElementById('admin-screen');
10
+ const userScreen = document.getElementById('user-screen');
11
  const loginForm = document.getElementById('login-form');
12
  const loginError = document.getElementById('login-error');
13
+ const apiKeyInput = document.getElementById('api-key-input');
14
+ const logoutButtons = document.querySelectorAll('.logout-btn');
15
+ const adminUserInfo = document.getElementById('admin-user-info');
16
+ const userInfo = document.getElementById('user-info');
17
  const createKeyBtn = document.getElementById('create-key-btn');
18
  const createKeyModal = document.getElementById('create-key-modal');
19
  const createKeyForm = document.getElementById('create-key-form');
20
  const cancelCreateBtn = document.getElementById('cancel-create-btn');
21
  const keyDetailsModal = document.getElementById('key-details-modal');
22
  const closeDetailsBtn = document.getElementById('close-details-btn');
23
+ const adminKeysList = document.getElementById('admin-keys-list');
24
+ const userKeysList = document.getElementById('user-keys-list');
25
+ const commandForm = document.getElementById('command-form');
26
+ const commandInput = document.getElementById('command-input');
27
+ const commandHistory = document.getElementById('command-history');
28
+ const userEventsList = document.getElementById('user-events-list');
29
 
30
+ function authHeaders(key) {
31
+ return { Authorization: `Bearer ${key}` };
32
+ }
33
+
34
+ function resetSession() {
35
+ apiKey = null;
36
+ currentRole = null;
37
+
38
+ if (statsIntervalId) {
39
+ clearInterval(statsIntervalId);
40
+ statsIntervalId = null;
41
+ }
42
+
43
+ if (wsPingIntervalId) {
44
+ clearInterval(wsPingIntervalId);
45
+ wsPingIntervalId = null;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  }
 
47
 
 
 
48
  if (ws) {
49
  ws.close();
50
  ws = null;
51
  }
 
 
 
52
 
53
+ if (loginScreen) {
54
+ loginScreen.classList.remove('hidden');
55
+ }
56
+ if (adminScreen) {
57
+ adminScreen.classList.add('hidden');
58
+ }
59
+ if (userScreen) {
60
+ userScreen.classList.add('hidden');
61
+ }
62
+ }
63
 
64
+ async function detectRole(key) {
65
+ const adminResponse = await fetch(`${API_URL}/manage/keys`, {
66
+ headers: authHeaders(key)
67
+ });
68
 
69
+ if (adminResponse.ok) {
70
+ return 'admin';
71
+ }
72
 
73
+ const regularResponse = await fetch(`${API_URL}/manage/keys/server-keys`, {
74
+ headers: authHeaders(key)
75
+ });
76
+
77
+ if (regularResponse.ok) {
78
+ return 'regular';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  }
 
80
 
81
+ return null;
 
 
 
 
 
 
 
 
 
 
 
 
82
  }
83
 
84
+ function showAdminPanel() {
85
+ currentRole = 'admin';
86
+ loginScreen.classList.add('hidden');
87
+ userScreen.classList.add('hidden');
88
+ adminScreen.classList.remove('hidden');
89
+
90
+ if (adminUserInfo) {
91
+ adminUserInfo.textContent = 'Admin Key';
92
+ }
93
+
94
+ loadAdminKeys();
95
+ }
96
 
97
+ function showUserPanel() {
98
+ currentRole = 'regular';
 
 
99
  loginScreen.classList.add('hidden');
100
+ adminScreen.classList.add('hidden');
101
+ userScreen.classList.remove('hidden');
102
+
103
+ if (userInfo) {
104
+ userInfo.textContent = 'Regular Key';
105
+ }
106
+
107
+ loadUserServerKeys();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  loadStats();
109
  connectWebSocket();
 
 
 
110
 
111
+ statsIntervalId = setInterval(loadStats, 5000);
 
 
 
 
 
 
 
 
 
 
 
112
  }
113
 
114
+ async function loadAdminKeys() {
115
+ if (!adminKeysList || !apiKey) {
116
+ return;
117
+ }
118
  try {
119
  const response = await fetch(`${API_URL}/manage/keys`, {
120
+ headers: authHeaders(apiKey)
121
  });
122
+
123
  if (response.ok) {
124
  const keys = await response.json();
125
+ renderAdminKeys(keys);
126
+ } else {
127
+ adminKeysList.innerHTML = '<p>Failed to load keys.</p>';
128
  }
129
  } catch (error) {
130
+ adminKeysList.innerHTML = `<p>Failed to load keys: ${error.message}</p>`;
131
  }
132
  }
133
 
134
+ async function loadUserServerKeys() {
135
+ if (!userKeysList || !apiKey) {
136
+ return;
137
+ }
138
  try {
139
  const response = await fetch(`${API_URL}/manage/keys/server-keys`, {
140
+ headers: authHeaders(apiKey)
141
  });
142
+
143
  if (response.ok) {
144
+ const keys = await response.json();
145
+ renderUserServerKeys(keys);
146
  } else {
147
+ userKeysList.innerHTML = '<p>Failed to load server keys.</p>';
148
  }
149
  } catch (error) {
150
+ userKeysList.innerHTML = `<p>Failed to load server keys: ${error.message}</p>`;
 
151
  }
152
  }
153
 
154
+ function renderAdminKeys(keys) {
155
+ if (!adminKeysList) {
 
 
 
156
  return;
157
  }
158
+
159
+ if (!keys.length) {
160
+ adminKeysList.innerHTML = '<p>No API keys found.</p>';
161
+ return;
162
+ }
163
+
164
+ adminKeysList.innerHTML = keys.map((key) => `
165
+ <div class="key-card ${key.keyType === 'admin' ? 'admin' : ''}">
166
  <div class="key-info">
167
  <h3>
168
+ <span class="key-badge ${key.keyType}">
169
+ ${key.keyType === 'admin' ? 'Admin' : key.keyType === 'server' ? 'Server' : 'Regular'}
170
+ </span>
171
  <span class="key-badge ${key.isActive ? 'active' : 'inactive'}">
172
+ ${key.isActive ? 'Active' : 'Inactive'}
173
  </span>
174
  ${key.name}
175
  </h3>
176
  <p>ID: ${key.id}</p>
177
+ <p>Prefix: ${key.keyPrefix}</p>
178
+ ${key.serverId ? `<p>Server ID: ${key.serverId}</p>` : ''}
179
+ <p>Created: ${new Date(key.createdAt).toLocaleString()}</p>
180
+ <p>Last Used: ${key.lastUsed ? new Date(key.lastUsed).toLocaleString() : 'Never'}</p>
181
  </div>
182
  <div class="key-actions">
183
+ ${key.isActive
184
+ ? `<button class="btn-danger" onclick="deactivateKey('${key.id}')">Deactivate</button>`
185
+ : `<button class="btn-success" onclick="activateKey('${key.id}')">Activate</button>`
186
  }
187
+ <button class="btn-danger" onclick="deleteKey('${key.id}', '${key.name}')">Delete</button>
188
  </div>
189
  </div>
190
  `).join('');
191
  }
192
 
193
+ function renderUserServerKeys(keys) {
194
+ if (!userKeysList) {
 
 
 
195
  return;
196
  }
197
+
198
+ if (!keys.length) {
199
+ userKeysList.innerHTML = '<p>No server keys available.</p>';
200
+ return;
201
+ }
202
+
203
+ userKeysList.innerHTML = keys.map((key) => `
204
+ <div class="key-card">
205
  <div class="key-info">
206
  <h3>
207
+ <span class="key-badge server">Server</span>
 
 
208
  <span class="key-badge ${key.isActive ? 'active' : 'inactive'}">
209
+ ${key.isActive ? 'Active' : 'Inactive'}
210
  </span>
211
  ${key.name}
212
  </h3>
213
  <p>ID: ${key.id}</p>
214
+ <p>Prefix: ${key.keyPrefix}</p>
215
+ ${key.serverId ? `<p>Server ID: ${key.serverId}</p>` : ''}
216
+ <p>Created: ${new Date(key.createdAt).toLocaleString()}</p>
217
+ <p>Last Used: ${key.lastUsed ? new Date(key.lastUsed).toLocaleString() : 'Never'}</p>
218
  </div>
219
  <div class="key-actions">
220
+ ${key.isActive
221
+ ? `<button class="btn-danger" onclick="deactivateKey('${key.id}')">Deactivate</button>`
222
+ : `<button class="btn-success" onclick="activateKey('${key.id}')">Activate</button>`
223
  }
 
224
  </div>
225
  </div>
226
  `).join('');
227
  }
228
 
229
  async function activateKey(keyId) {
230
+ if (!apiKey) {
231
+ return;
232
+ }
233
  try {
234
  const response = await fetch(`${API_URL}/manage/keys/${keyId}/activate`, {
235
  method: 'PATCH',
236
+ headers: authHeaders(apiKey)
237
  });
238
+
239
+ if (!response.ok) {
 
 
 
 
 
 
240
  const error = await response.json();
241
+ alert(`Failed to activate key: ${error.detail}`);
242
+ return;
243
+ }
244
+
245
+ if (currentRole === 'admin') {
246
+ loadAdminKeys();
247
+ } else if (currentRole === 'regular') {
248
+ loadUserServerKeys();
249
  }
250
  } catch (error) {
251
+ alert(`Failed to activate key: ${error.message}`);
252
  }
253
  }
254
 
255
  async function deactivateKey(keyId) {
256
+ if (!apiKey) {
257
+ return;
258
+ }
259
  try {
260
  const response = await fetch(`${API_URL}/manage/keys/${keyId}/deactivate`, {
261
  method: 'PATCH',
262
+ headers: authHeaders(apiKey)
263
  });
264
+
265
+ if (!response.ok) {
 
 
 
 
 
 
266
  const error = await response.json();
267
+ alert(`Failed to deactivate key: ${error.detail}`);
268
+ return;
269
+ }
270
+
271
+ if (currentRole === 'admin') {
272
+ loadAdminKeys();
273
+ } else if (currentRole === 'regular') {
274
+ loadUserServerKeys();
275
  }
276
  } catch (error) {
277
+ alert(`Failed to deactivate key: ${error.message}`);
278
  }
279
  }
280
 
281
  async function deleteKey(keyId, keyName) {
282
+ if (currentRole !== 'admin') {
283
+ return;
284
+ }
285
+ if (!confirm(`Delete key "${keyName}"? This action cannot be undone.`)) {
286
  return;
287
  }
 
288
  try {
289
  const response = await fetch(`${API_URL}/manage/keys/${keyId}`, {
290
  method: 'DELETE',
291
+ headers: authHeaders(apiKey)
292
  });
293
+
294
+ if (!response.ok) {
 
 
 
 
 
 
295
  const error = await response.json();
296
+ alert(`Failed to delete key: ${error.detail}`);
297
+ return;
298
  }
299
+
300
+ loadAdminKeys();
301
  } catch (error) {
302
+ alert(`Failed to delete key: ${error.message}`);
303
+ }
304
+ }
305
+
306
+ function showKeyCreatedModal(payload) {
307
+ if (!keyDetailsModal) {
308
+ return;
309
+ }
310
+
311
+ let content = '';
312
+ if (payload.regularKey && payload.serverKey) {
313
+ content = `
314
+ <p><strong>Regular Key</strong></p>
315
+ <p>Name: ${payload.regularKey.name}</p>
316
+ <p>Type: ${payload.regularKey.keyType}</p>
317
+ <p>ID: ${payload.regularKey.id}</p>
318
+ <p>Key: ${payload.regularKey.key}</p>
319
+ <hr>
320
+ <p><strong>Server Key</strong></p>
321
+ <p>Name: ${payload.serverKey.name}</p>
322
+ <p>Type: ${payload.serverKey.keyType}</p>
323
+ <p>ID: ${payload.serverKey.id}</p>
324
+ <p>Key: ${payload.serverKey.key}</p>
325
+ `;
326
+ } else {
327
+ content = `
328
+ <p><strong>Key Created</strong></p>
329
+ <p>Name: ${payload.name}</p>
330
+ <p>Type: ${payload.keyType}</p>
331
+ <p>ID: ${payload.id}</p>
332
+ <p>Key: ${payload.key}</p>
333
+ `;
334
+ }
335
+
336
+ const detailsContent = document.getElementById('key-details-content');
337
+ if (detailsContent) {
338
+ detailsContent.innerHTML = content;
339
+ }
340
+
341
+ keyDetailsModal.classList.remove('hidden');
342
+ }
343
+
344
+ async function loadStats() {
345
+ try {
346
+ const response = await fetch(`${API_URL}/health`);
347
+ if (!response.ok) {
348
+ return;
349
+ }
350
+ const data = await response.json();
351
+
352
+ const connections = document.getElementById('user-stat-connections');
353
+ if (connections) {
354
+ connections.textContent = data.active_ws || 0;
355
+ }
356
+
357
+ const totalKeys = document.getElementById('user-stat-total-keys');
358
+ if (totalKeys) {
359
+ totalKeys.textContent = data.keys_total || 0;
360
+ }
361
+
362
+ const adminKeys = document.getElementById('user-stat-admin-keys');
363
+ if (adminKeys) {
364
+ adminKeys.textContent = data.admin_active || 0;
365
+ }
366
+
367
+ const regularKeys = document.getElementById('user-stat-regular-keys');
368
+ if (regularKeys) {
369
+ regularKeys.textContent = data.regular_active || 0;
370
+ }
371
+
372
+ const serverKeys = document.getElementById('user-stat-server-keys');
373
+ if (serverKeys) {
374
+ serverKeys.textContent = data.server_active || 0;
375
+ }
376
+ } catch (error) {
377
+ console.error('Failed to load stats:', error);
378
  }
379
  }
380
 
381
  function connectWebSocket() {
382
+ if (!apiKey) {
383
+ return;
384
+ }
385
+
386
+ const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
387
+ const wsUrl = `${protocol}://${window.location.host}/ws?api_key=${apiKey}`;
388
  ws = new WebSocket(wsUrl);
389
+
390
  ws.onopen = () => {
391
  console.log('WebSocket connected');
392
  };
393
+
394
  ws.onmessage = (event) => {
395
  try {
396
  const message = JSON.parse(event.data);
 
397
  if (message.type === 'minecraft_event') {
398
  addEventToList(message.event);
399
  }
 
401
  console.error('Failed to parse WebSocket message:', error);
402
  }
403
  };
404
+
405
  ws.onerror = (error) => {
406
  console.error('WebSocket error:', error);
407
  };
408
+
409
  ws.onclose = () => {
410
  console.log('WebSocket disconnected');
411
+ if (wsPingIntervalId) {
412
+ clearInterval(wsPingIntervalId);
413
+ wsPingIntervalId = null;
414
+ }
415
+ if (apiKey && currentRole === 'regular') {
416
+ setTimeout(() => connectWebSocket(), 5000);
417
+ }
418
  };
419
+
420
+ wsPingIntervalId = setInterval(() => {
421
  if (ws && ws.readyState === WebSocket.OPEN) {
422
  ws.send(JSON.stringify({ type: 'ping' }));
423
  }
 
425
  }
426
 
427
  function addEventToList(event) {
428
+ if (!userEventsList) {
429
+ return;
430
+ }
431
+
432
  const eventItem = document.createElement('div');
433
  eventItem.className = 'event-item';
434
  eventItem.innerHTML = `
435
  <strong>${event.event_type}</strong> - ${event.server_name}<br>
436
+ <small>${new Date(event.timestamp).toLocaleString()}</small><br>
437
  <pre>${JSON.stringify(event.data, null, 2)}</pre>
438
  `;
439
+
440
+ userEventsList.insertBefore(eventItem, userEventsList.firstChild);
441
+
442
+ while (userEventsList.children.length > 50) {
443
+ userEventsList.removeChild(userEventsList.lastChild);
444
+ }
445
+ }
446
+
447
+ function appendCommandHistory(command, status, detail) {
448
+ if (!commandHistory) {
449
+ return;
450
  }
451
+
452
+ const entry = document.createElement('div');
453
+ entry.className = 'command-item';
454
+ entry.innerHTML = `
455
+ <div>${status}: ${command}</div>
456
+ ${detail ? `<div class="timestamp">${detail}</div>` : ''}
457
+ `;
458
+ commandHistory.insertBefore(entry, commandHistory.firstChild);
459
+
460
+ while (commandHistory.children.length > 20) {
461
+ commandHistory.removeChild(commandHistory.lastChild);
462
+ }
463
+ }
464
+
465
+ if (loginForm) {
466
+ loginForm.addEventListener('submit', async (event) => {
467
+ event.preventDefault();
468
+ const key = apiKeyInput.value.trim();
469
+ loginError.textContent = '';
470
+
471
+ if (!key) {
472
+ loginError.textContent = 'API key is required.';
473
+ return;
474
+ }
475
+
476
+ try {
477
+ const role = await detectRole(key);
478
+ if (!role) {
479
+ loginError.textContent = 'Invalid key or insufficient permissions.';
480
+ return;
481
+ }
482
+
483
+ apiKey = key;
484
+
485
+ if (role === 'admin') {
486
+ showAdminPanel();
487
+ } else if (role === 'regular') {
488
+ showUserPanel();
489
+ }
490
+ } catch (error) {
491
+ loginError.textContent = error.message || 'Unable to connect to server.';
492
+ }
493
+ });
494
+ }
495
+
496
+ logoutButtons.forEach((button) => {
497
+ button.addEventListener('click', () => {
498
+ resetSession();
499
+ });
500
+ });
501
+
502
+ if (createKeyBtn) {
503
+ createKeyBtn.addEventListener('click', () => {
504
+ createKeyModal.classList.remove('hidden');
505
+ });
506
  }
507
+
508
+ if (cancelCreateBtn) {
509
+ cancelCreateBtn.addEventListener('click', () => {
510
+ createKeyModal.classList.add('hidden');
511
+ createKeyForm.reset();
512
+ });
513
+ }
514
+
515
+ if (closeDetailsBtn) {
516
+ closeDetailsBtn.addEventListener('click', () => {
517
+ keyDetailsModal.classList.add('hidden');
518
+ });
519
+ }
520
+
521
+ if (createKeyForm) {
522
+ createKeyForm.addEventListener('submit', async (event) => {
523
+ event.preventDefault();
524
+
525
+ const name = document.getElementById('key-name').value.trim();
526
+ const description = document.getElementById('key-description').value.trim();
527
+ const keyType = document.getElementById('key-type').value;
528
+ const serverId = document.getElementById('key-server-id').value.trim();
529
+
530
+ if (!name) {
531
+ alert('Name is required.');
532
+ return;
533
+ }
534
+
535
+ try {
536
+ const payload = {
537
+ name,
538
+ description,
539
+ key_type: keyType
540
+ };
541
+
542
+ if (serverId) {
543
+ payload.server_id = serverId;
544
+ }
545
+
546
+ const response = await fetch(`${API_URL}/manage/keys`, {
547
+ method: 'POST',
548
+ headers: {
549
+ ...authHeaders(apiKey),
550
+ 'Content-Type': 'application/json'
551
+ },
552
+ body: JSON.stringify(payload)
553
+ });
554
+
555
+ if (!response.ok) {
556
+ const error = await response.json();
557
+ alert(`Failed to create key: ${error.detail}`);
558
+ return;
559
+ }
560
+
561
+ const result = await response.json();
562
+ createKeyModal.classList.add('hidden');
563
+ createKeyForm.reset();
564
+ showKeyCreatedModal(result);
565
+ loadAdminKeys();
566
+ } catch (error) {
567
+ alert(`Failed to create key: ${error.message}`);
568
+ }
569
+ });
570
+ }
571
+
572
+ if (commandForm) {
573
+ commandForm.addEventListener('submit', async (event) => {
574
+ event.preventDefault();
575
+
576
+ const command = commandInput.value.trim();
577
+ if (!command) {
578
+ return;
579
+ }
580
+
581
+ try {
582
+ const response = await fetch(`${API_URL}/api/server/command`, {
583
+ method: 'POST',
584
+ headers: {
585
+ ...authHeaders(apiKey),
586
+ 'Content-Type': 'application/json'
587
+ },
588
+ body: JSON.stringify({ command })
589
+ });
590
+
591
+ if (!response.ok) {
592
+ const error = await response.json();
593
+ appendCommandHistory(command, 'Rejected', error.detail);
594
+ return;
595
+ }
596
+
597
+ appendCommandHistory(command, 'Sent', new Date().toLocaleString());
598
+ commandInput.value = '';
599
+ } catch (error) {
600
+ appendCommandHistory(command, 'Error', error.message);
601
+ }
602
+ });
603
+ }
604
+
605
+ window.activateKey = activateKey;
606
+ window.deactivateKey = deactivateKey;
607
+ window.deleteKey = deleteKey;
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/dashboard/public/index.html CHANGED
@@ -1,88 +1,125 @@
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>Minecraft WebSocket API - 控制面板</title>
7
  <link rel="stylesheet" href="style.css">
8
  </head>
9
  <body>
10
  <div id="login-screen" class="screen">
11
  <div class="login-container">
12
- <h1>🎮 Minecraft WebSocket API</h1>
13
- <h2>控制面板登录</h2>
14
  <form id="login-form">
15
- <input type="password" id="super-key-input" placeholder="输入Admin Key或Server Key" required>
16
- <button type="submit">登录</button>
17
  </form>
18
  <p class="error-message" id="login-error"></p>
19
  <div class="login-info">
20
- <p><strong>密钥类型说明:</strong></p>
21
- <p>👑 <strong>Admin Key</strong> - 完全管理权限</p>
22
- <p>🖥️ <strong>Server Key</strong> - 服务器管理权限</p>
23
  </div>
24
  </div>
25
  </div>
26
 
27
- <div id="dashboard-screen" class="screen hidden">
28
  <nav class="navbar">
29
- <h1>🎮 Minecraft WebSocket API</h1>
30
- <button id="logout-btn">退出登录</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  </nav>
32
 
33
  <div class="container">
34
  <div class="stats-grid">
35
  <div class="stat-card">
36
- <h3>活跃连接</h3>
37
- <p class="stat-value" id="stat-connections">0</p>
38
  </div>
39
  <div class="stat-card">
40
- <h3>总密钥数</h3>
41
- <p class="stat-value" id="stat-total-keys">0</p>
42
  </div>
43
  <div class="stat-card">
44
- <h3>活跃SuperKeys</h3>
45
- <p class="stat-value" id="stat-super-keys">0</p>
46
  </div>
47
  <div class="stat-card">
48
- <h3>活跃普通Keys</h3>
49
- <p class="stat-value" id="stat-regular-keys">0</p>
 
 
 
 
50
  </div>
51
  </div>
52
 
53
  <div class="section">
54
- <div class="section-header">
55
- <h2>API密钥管理</h2>
56
- <button id="create-key-btn" class="btn-primary">创建新密钥</button>
57
- </div>
58
- <div id="keys-list" class="keys-list"></div>
59
  </div>
60
 
61
  <div class="section">
62
- <h2>实时事件监控</h2>
63
- <div id="events-list" class="events-list"></div>
 
 
 
 
 
 
 
 
 
64
  </div>
65
  </div>
66
  </div>
67
 
68
  <div id="create-key-modal" class="modal hidden">
69
  <div class="modal-content">
70
- <h2>创建新API密钥</h2>
71
  <form id="create-key-form">
72
- <label>名称 *</label>
73
  <input type="text" id="key-name" required>
74
-
75
- <label>描述</label>
76
  <textarea id="key-description"></textarea>
77
-
78
- <label>
79
- <input type="checkbox" id="key-is-super">
80
- 创建为SuperKey
81
- </label>
82
-
 
 
 
 
83
  <div class="modal-actions">
84
- <button type="button" id="cancel-create-btn" class="btn-secondary">取消</button>
85
- <button type="submit" class="btn-primary">创建</button>
86
  </div>
87
  </form>
88
  </div>
@@ -90,10 +127,10 @@
90
 
91
  <div id="key-details-modal" class="modal hidden">
92
  <div class="modal-content">
93
- <h2>密钥详情</h2>
94
  <div id="key-details-content"></div>
95
  <div class="modal-actions">
96
- <button id="close-details-btn" class="btn-secondary">关闭</button>
97
  </div>
98
  </div>
99
  </div>
 
1
  <!DOCTYPE html>
2
+ <html lang="en">
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Minecraft WebSocket API - Dashboard</title>
7
  <link rel="stylesheet" href="style.css">
8
  </head>
9
  <body>
10
  <div id="login-screen" class="screen">
11
  <div class="login-container">
12
+ <h1>Minecraft WebSocket API</h1>
13
+ <h2>Dashboard Login</h2>
14
  <form id="login-form">
15
+ <input type="password" id="api-key-input" placeholder="Enter Admin or Regular Key" required>
16
+ <button type="submit">Login</button>
17
  </form>
18
  <p class="error-message" id="login-error"></p>
19
  <div class="login-info">
20
+ <p><strong>Key Types</strong></p>
21
+ <p>Admin Key - full management permissions</p>
22
+ <p>Regular Key - server monitoring and command access</p>
23
  </div>
24
  </div>
25
  </div>
26
 
27
+ <div id="admin-screen" class="screen hidden">
28
  <nav class="navbar">
29
+ <h1>Admin Panel</h1>
30
+ <div class="nav-info">
31
+ <span id="admin-user-info"></span>
32
+ <button class="logout-btn">Logout</button>
33
+ </div>
34
+ </nav>
35
+
36
+ <div class="container">
37
+ <div class="section" id="admin-key-section">
38
+ <div class="section-header">
39
+ <h2>API Key Management</h2>
40
+ <button id="create-key-btn" class="btn-primary">Create Key</button>
41
+ </div>
42
+ <div id="admin-keys-list" class="keys-list"></div>
43
+ </div>
44
+ </div>
45
+ </div>
46
+
47
+ <div id="user-screen" class="screen hidden">
48
+ <nav class="navbar">
49
+ <h1>User Monitoring</h1>
50
+ <div class="nav-info">
51
+ <span id="user-info"></span>
52
+ <button class="logout-btn">Logout</button>
53
+ </div>
54
  </nav>
55
 
56
  <div class="container">
57
  <div class="stats-grid">
58
  <div class="stat-card">
59
+ <h3>Active Connections</h3>
60
+ <p class="stat-value" id="user-stat-connections">0</p>
61
  </div>
62
  <div class="stat-card">
63
+ <h3>Total Keys</h3>
64
+ <p class="stat-value" id="user-stat-total-keys">0</p>
65
  </div>
66
  <div class="stat-card">
67
+ <h3>Admin Keys</h3>
68
+ <p class="stat-value" id="user-stat-admin-keys">0</p>
69
  </div>
70
  <div class="stat-card">
71
+ <h3>Regular Keys</h3>
72
+ <p class="stat-value" id="user-stat-regular-keys">0</p>
73
+ </div>
74
+ <div class="stat-card">
75
+ <h3>Server Keys</h3>
76
+ <p class="stat-value" id="user-stat-server-keys">0</p>
77
  </div>
78
  </div>
79
 
80
  <div class="section">
81
+ <h2>Server Keys</h2>
82
+ <div id="user-keys-list" class="keys-list"></div>
 
 
 
83
  </div>
84
 
85
  <div class="section">
86
+ <h2>Command Console</h2>
87
+ <form id="command-form" class="command-form">
88
+ <input type="text" id="command-input" placeholder="Enter command" required>
89
+ <button type="submit" class="btn-primary">Send</button>
90
+ </form>
91
+ <div id="command-history" class="command-history"></div>
92
+ </div>
93
+
94
+ <div class="section">
95
+ <h2>Live Events</h2>
96
+ <div id="user-events-list" class="events-list"></div>
97
  </div>
98
  </div>
99
  </div>
100
 
101
  <div id="create-key-modal" class="modal hidden">
102
  <div class="modal-content">
103
+ <h2>Create API Key</h2>
104
  <form id="create-key-form">
105
+ <label>Name *</label>
106
  <input type="text" id="key-name" required>
107
+
108
+ <label>Description</label>
109
  <textarea id="key-description"></textarea>
110
+
111
+ <label>Key Type</label>
112
+ <select id="key-type">
113
+ <option value="regular">Regular</option>
114
+ <option value="admin">Admin</option>
115
+ </select>
116
+
117
+ <label>Server ID (optional)</label>
118
+ <input type="text" id="key-server-id">
119
+
120
  <div class="modal-actions">
121
+ <button type="button" id="cancel-create-btn" class="btn-secondary">Cancel</button>
122
+ <button type="submit" class="btn-primary">Create</button>
123
  </div>
124
  </form>
125
  </div>
 
127
 
128
  <div id="key-details-modal" class="modal hidden">
129
  <div class="modal-content">
130
+ <h2>Key Details</h2>
131
  <div id="key-details-content"></div>
132
  <div class="modal-actions">
133
+ <button id="close-details-btn" class="btn-secondary">Close</button>
134
  </div>
135
  </div>
136
  </div>
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/dashboard/public/style.css CHANGED
@@ -95,7 +95,7 @@ body {
95
  color: #667eea;
96
  }
97
 
98
- #logout-btn {
99
  padding: 10px 20px;
100
  background: #ff4444;
101
  color: white;
@@ -218,7 +218,7 @@ body {
218
  align-items: center;
219
  }
220
 
221
- .key-card.super {
222
  border-color: #764ba2;
223
  background: #f9f7fb;
224
  }
@@ -236,7 +236,7 @@ body {
236
  margin-right: 10px;
237
  }
238
 
239
- .key-badge.super {
240
  background: #764ba2;
241
  color: white;
242
  }
@@ -246,6 +246,11 @@ body {
246
  color: white;
247
  }
248
 
 
 
 
 
 
249
  .key-badge.active {
250
  background: #4caf50;
251
  color: white;
@@ -327,8 +332,13 @@ body {
327
  resize: vertical;
328
  }
329
 
330
- .modal-content input[type="checkbox"] {
331
- margin-right: 8px;
 
 
 
 
 
332
  }
333
 
334
  .modal-actions {
@@ -344,7 +354,8 @@ body {
344
  gap: 15px;
345
  }
346
 
347
- #user-info {
 
348
  font-size: 0.9em;
349
  color: #667eea;
350
  background: rgba(255,255,255,0.1);
@@ -379,6 +390,20 @@ body {
379
  padding: 10px;
380
  }
381
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
382
  .command-item {
383
  padding: 8px 0;
384
  border-bottom: 1px solid #f8f9fa;
 
95
  color: #667eea;
96
  }
97
 
98
+ .logout-btn {
99
  padding: 10px 20px;
100
  background: #ff4444;
101
  color: white;
 
218
  align-items: center;
219
  }
220
 
221
+ .key-card.admin {
222
  border-color: #764ba2;
223
  background: #f9f7fb;
224
  }
 
236
  margin-right: 10px;
237
  }
238
 
239
+ .key-badge.admin {
240
  background: #764ba2;
241
  color: white;
242
  }
 
246
  color: white;
247
  }
248
 
249
+ .key-badge.server {
250
+ background: #4f8ef7;
251
+ color: white;
252
+ }
253
+
254
  .key-badge.active {
255
  background: #4caf50;
256
  color: white;
 
332
  resize: vertical;
333
  }
334
 
335
+ .modal-content select {
336
+ width: 100%;
337
+ padding: 10px;
338
+ border: 2px solid #e0e0e0;
339
+ border-radius: 5px;
340
+ margin-bottom: 15px;
341
+ font-size: 14px;
342
  }
343
 
344
  .modal-actions {
 
354
  gap: 15px;
355
  }
356
 
357
+ #user-info,
358
+ #admin-user-info {
359
  font-size: 0.9em;
360
  color: #667eea;
361
  background: rgba(255,255,255,0.1);
 
390
  padding: 10px;
391
  }
392
 
393
+ .command-form {
394
+ display: flex;
395
+ gap: 10px;
396
+ margin-bottom: 15px;
397
+ }
398
+
399
+ .command-form input {
400
+ flex: 1;
401
+ padding: 12px;
402
+ border: 2px solid #e0e0e0;
403
+ border-radius: 5px;
404
+ font-size: 14px;
405
+ }
406
+
407
  .command-item {
408
  padding: 8px 0;
409
  border-bottom: 1px solid #f8f9fa;
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/dashboard/server.js CHANGED
@@ -3,7 +3,7 @@ const express = require('express');
3
  const path = require('path');
4
 
5
  const app = express();
6
- const PORT = parseInt(process.env.DASHBOARD_PORT || '3000');
7
 
8
  app.use(express.static(path.join(__dirname, 'public')));
9
 
@@ -13,9 +13,9 @@ app.get('/', (req, res) => {
13
 
14
  app.listen(PORT, () => {
15
  console.log('='.repeat(50));
16
- console.log('🎮 Minecraft WebSocket API - 控制面板');
17
  console.log('='.repeat(50));
18
- console.log(`控制面板地址: http://localhost:${PORT}`);
19
- console.log('请使用SuperKey登录');
20
  console.log('='.repeat(50));
21
  });
 
3
  const path = require('path');
4
 
5
  const app = express();
6
+ const PORT = parseInt(process.env.DASHBOARD_PORT || '3000', 10);
7
 
8
  app.use(express.static(path.join(__dirname, 'public')));
9
 
 
13
 
14
  app.listen(PORT, () => {
15
  console.log('='.repeat(50));
16
+ console.log('Minecraft WebSocket API - Dashboard');
17
  console.log('='.repeat(50));
18
+ console.log(`Dashboard URL: http://localhost:${PORT}`);
19
+ console.log('Use Admin or Regular Key to log in.');
20
  console.log('='.repeat(50));
21
  });
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.github/scripts/ai_agent.py CHANGED
@@ -2,9 +2,9 @@ import os
2
  import json
3
  import re
4
  from openai import OpenAI
5
- from github import Github, Auth # 导入 Auth 以修复弃用警告
6
 
7
- # --- 1. 初始化客户端 (修复 DeprecationWarning) ---
8
  auth = Auth.Token(os.getenv("GITHUB_TOKEN"))
9
  gh = Github(auth=auth)
10
  repo = gh.get_repo(os.getenv("GITHUB_REPOSITORY"))
@@ -13,82 +13,51 @@ client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"), base_url=os.getenv("OPENAI_
13
  event_data = json.loads(os.getenv("EVENT_CONTEXT"))
14
  event_name = os.getenv("EVENT_NAME")
15
 
16
- # --- 2. 定义工具 (增加 **kwargs 以忽略多余参数) ---
17
-
18
  def list_directory(path=".", **kwargs):
19
- """列出指定目录下的文件和文件夹"""
20
  try:
21
- # 基础路径安全检查
22
- if ".." in path: return "Error: Cannot access parent directory."
23
- items = os.listdir(path)
24
- return "\n".join(items)
25
- except Exception as e:
26
- return f"Error listing directory: {str(e)}"
27
 
28
  def read_file(path, **kwargs):
29
- """读取特定文件的完整内容"""
30
  try:
31
  if ".." in path: return "Error: Access denied."
32
  with open(path, 'r', encoding='utf-8') as f:
33
- return f.read()[:5000]
34
- except Exception as e:
35
- return f"Error reading file: {str(e)}"
36
 
37
  def search_keyword(keyword, path=".", **kwargs):
38
- """在当前目录及其子目录中搜索关键词"""
39
  results = []
40
- try:
41
- for root, dirs, files in os.walk(path):
42
- if ".git" in root: continue # 过滤 Git 目录
43
- for file in files:
44
- if file.endswith(('.py', '.js', '.ts', '.md', '.json', '.rs', '.toml', '.yml')):
45
- full_path = os.path.join(root, file)
46
- try:
47
- with open(full_path, 'r', encoding='utf-8') as f:
48
- if keyword in f.read():
49
- results.append(full_path)
50
- except: continue
51
- return "\n".join(results[:15]) if results else "No matches found."
52
- except Exception as e:
53
- return f"Search error: {str(e)}"
54
-
55
- # --- 3. 获取上下文 ---
56
-
57
  def get_context():
58
- # 提取 Issue/PR 编号和参与者信息
59
  if "pull_request" in event_data:
60
- payload = event_data["pull_request"]
61
- number = payload["number"]
62
- author = payload["user"]["login"]
63
- return number, f"[Role: PR Author @{author}]\nTitle: {payload['title']}\nBody: {payload['body']}"
64
-
65
- payload = event_data["issue"]
66
- number = payload["number"]
67
- author = payload["user"]["login"]
68
- base_info = f"[Role: Issue Author @{author}]\nTitle: {payload['title']}\nBody: {payload['body']}"
69
 
 
 
70
  if event_name == "issue_comment":
71
- actor = event_data["comment"]["user"]["login"]
72
- cmd = event_data["comment"]["body"]
73
- return number, f"{base_info}\n\n[New Interaction by @{actor}]\nCommand: {cmd}"
74
-
75
- return number, base_info
76
 
77
  issue_num, user_content = get_context()
78
  issue_obj = repo.get_issue(number=issue_num)
79
  repo_labels = [l.name for l in repo.get_labels()]
80
 
81
- # --- 4. 运行 AI Agent ---
82
-
83
  messages = [
84
- {"role": "system", "content": f"""你是一个高级仓库助手 (@github-actions[bot])。
85
-
86
- 可用标签: {repo_labels}
87
-
88
- 你可以通过工具查看代码库结构。回复规则:
89
- 1. 首行必须返回 JSON 指令:{{"labels": [], "state": "open"|"closed"}}
90
- 2. 随后另起一行,以执行者的口吻告知结果。
91
- 3. 忽略 AI 历史回复中的元数据,只关注当前代码和用户意图。"""},
92
  {"role": "user", "content": user_content}
93
  ]
94
 
@@ -98,40 +67,40 @@ tools = [
98
  {"type": "function", "function": {"name": "search_keyword", "description": "Search keyword", "parameters": {"type": "object", "properties": {"keyword": {"type": "string"}}}}}
99
  ]
100
 
101
- # 允许 3 次交互以获取足够信息
102
- for _ in range(3):
103
  response = client.chat.completions.create(
104
  model=os.getenv("AI_MODEL"),
105
  messages=messages,
106
  tools=tools,
107
  temperature=0
108
  )
 
 
109
  msg = response.choices[0].message
110
- messages.append(msg)
 
111
 
112
  if not msg.tool_calls:
113
  break
114
 
115
  for tool_call in msg.tool_calls:
116
- fn_name = tool_call.function.name
117
- fn_args = json.loads(tool_call.function.arguments)
118
-
119
- # 映射函数映射表
120
- available_functions = {
121
- "list_directory": list_directory,
122
- "read_file": read_file,
123
- "search_keyword": search_keyword,
124
- }
125
-
126
- if fn_name in available_functions:
127
- result = available_functions[fn_name](**fn_args)
128
- messages.append({"role": "tool", "tool_call_id": tool_call.id, "content": result})
129
 
130
- # 5. 解析并执行 GitHub 操作
131
- final_res = messages[-1].content
132
- json_data = {"labels": [], "state": "open"}
 
133
 
134
- # 提取 JSON 块
 
 
 
 
 
135
  match = re.search(r'(\{.*?\})', final_res, re.DOTALL)
136
  if match:
137
  try:
@@ -141,7 +110,7 @@ if match:
141
 
142
  if json_data.get("labels"):
143
  issue_obj.add_to_labels(*json_data["labels"])
144
- if json_data.get("state") and json_data["state"] in ["open", "closed"]:
145
  issue_obj.edit(state=json_data["state"])
146
 
147
  issue_obj.create_comment(f"### 🤖 AI Agent Execution\n\n{final_res}")
 
2
  import json
3
  import re
4
  from openai import OpenAI
5
+ from github import Github, Auth
6
 
7
+ # 初始化客户端
8
  auth = Auth.Token(os.getenv("GITHUB_TOKEN"))
9
  gh = Github(auth=auth)
10
  repo = gh.get_repo(os.getenv("GITHUB_REPOSITORY"))
 
13
  event_data = json.loads(os.getenv("EVENT_CONTEXT"))
14
  event_name = os.getenv("EVENT_NAME")
15
 
16
+ # --- 工具定义 (保持不变,增加 **kwargs 鲁棒性) ---
 
17
  def list_directory(path=".", **kwargs):
 
18
  try:
19
+ if ".." in path: return "Error: Access denied."
20
+ return "\n".join(os.listdir(path))
21
+ except Exception as e: return str(e)
 
 
 
22
 
23
  def read_file(path, **kwargs):
 
24
  try:
25
  if ".." in path: return "Error: Access denied."
26
  with open(path, 'r', encoding='utf-8') as f:
27
+ return f.read()[:5000]
28
+ except Exception as e: return str(e)
 
29
 
30
  def search_keyword(keyword, path=".", **kwargs):
 
31
  results = []
32
+ for root, _, files in os.walk(path):
33
+ if ".git" in root: continue
34
+ for file in files:
35
+ if file.endswith(('.py', '.js', '.ts', '.md', '.json', '.rs', '.yml')):
36
+ p = os.path.join(root, file)
37
+ try:
38
+ if keyword in open(p, 'r').read(): results.append(p)
39
+ except: continue
40
+ return "\n".join(results[:15]) if results else "No matches."
41
+
42
+ # --- 上下文准备 ---
 
 
 
 
 
 
43
  def get_context():
 
44
  if "pull_request" in event_data:
45
+ p = event_data["pull_request"]
46
+ return p["number"], f"PR @{p['user']['login']}\nTitle: {p['title']}\n{p['body']}"
 
 
 
 
 
 
 
47
 
48
+ i = event_data["issue"]
49
+ ctx = f"Issue @{i['user']['login']}\nTitle: {i['title']}\n{i['body']}"
50
  if event_name == "issue_comment":
51
+ ctx += f"\n\nNew Comment by @{event_data['comment']['user']['login']}: {event_data['comment']['body']}"
52
+ return i["number"], ctx
 
 
 
53
 
54
  issue_num, user_content = get_context()
55
  issue_obj = repo.get_issue(number=issue_num)
56
  repo_labels = [l.name for l in repo.get_labels()]
57
 
58
+ # --- AI 执行逻辑 ---
 
59
  messages = [
60
+ {"role": "system", "content": f"你是一个高级仓库助手。可用标签: {repo_labels}。必须首行返回JSON: {{\"labels\":[], \"state\":\"open\"}},然后解释逻辑。"},
 
 
 
 
 
 
 
61
  {"role": "user", "content": user_content}
62
  ]
63
 
 
67
  {"type": "function", "function": {"name": "search_keyword", "description": "Search keyword", "parameters": {"type": "object", "properties": {"keyword": {"type": "string"}}}}}
68
  ]
69
 
70
+ # 允许最多 5 轮工具交互
71
+ for i in range(5):
72
  response = client.chat.completions.create(
73
  model=os.getenv("AI_MODEL"),
74
  messages=messages,
75
  tools=tools,
76
  temperature=0
77
  )
78
+
79
+ # 核心修复:统一将模型返回的消息转为可序列化的字典格式
80
  msg = response.choices[0].message
81
+ msg_dict = msg.model_dump()
82
+ messages.append(msg_dict)
83
 
84
  if not msg.tool_calls:
85
  break
86
 
87
  for tool_call in msg.tool_calls:
88
+ args = json.loads(tool_call.function.arguments)
89
+ func = {"list_directory": list_directory, "read_file": read_file, "search_keyword": search_keyword}.get(tool_call.function.name)
90
+ result = func(**args) if func else "Unknown function"
91
+ messages.append({"role": "tool", "tool_call_id": tool_call.id, "content": str(result)})
 
 
 
 
 
 
 
 
 
92
 
93
+ # 确保最后一条消息是文本回复
94
+ if messages[-1].get("role") == "tool" or (messages[-1].get("tool_calls") and not messages[-1].get("content")):
95
+ final_check = client.chat.completions.create(model=os.getenv("AI_MODEL"), messages=messages)
96
+ messages.append(final_check.choices[0].message.model_dump())
97
 
98
+ # 提取最终文本内容
99
+ final_msg = messages[-1]
100
+ final_res = final_msg.get("content") or ""
101
+
102
+ # --- 结果解析与执行 ---
103
+ json_data = {"labels": [], "state": "open"}
104
  match = re.search(r'(\{.*?\})', final_res, re.DOTALL)
105
  if match:
106
  try:
 
110
 
111
  if json_data.get("labels"):
112
  issue_obj.add_to_labels(*json_data["labels"])
113
+ if json_data.get("state") in ["open", "closed"]:
114
  issue_obj.edit(state=json_data["state"])
115
 
116
  issue_obj.create_comment(f"### 🤖 AI Agent Execution\n\n{final_res}")
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.github/workflows/ai-review.yaml CHANGED
@@ -1,116 +1,46 @@
1
- import os
2
- import json
3
- import re
4
- from openai import OpenAI
5
- from github import Github, Auth
6
-
7
- # 初始化客户端
8
- auth = Auth.Token(os.getenv("GITHUB_TOKEN"))
9
- gh = Github(auth=auth)
10
- repo = gh.get_repo(os.getenv("GITHUB_REPOSITORY"))
11
- client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"), base_url=os.getenv("OPENAI_BASE_URL"))
12
-
13
- event_data = json.loads(os.getenv("EVENT_CONTEXT"))
14
- event_name = os.getenv("EVENT_NAME")
15
-
16
- # --- 工具定义 (保持不变,增加 **kwargs 鲁棒性) ---
17
- def list_directory(path=".", **kwargs):
18
- try:
19
- if ".." in path: return "Error: Access denied."
20
- return "\n".join(os.listdir(path))
21
- except Exception as e: return str(e)
22
-
23
- def read_file(path, **kwargs):
24
- try:
25
- if ".." in path: return "Error: Access denied."
26
- with open(path, 'r', encoding='utf-8') as f:
27
- return f.read()[:5000]
28
- except Exception as e: return str(e)
29
-
30
- def search_keyword(keyword, path=".", **kwargs):
31
- results = []
32
- for root, _, files in os.walk(path):
33
- if ".git" in root: continue
34
- for file in files:
35
- if file.endswith(('.py', '.js', '.ts', '.md', '.json', '.rs', '.yml')):
36
- p = os.path.join(root, file)
37
- try:
38
- if keyword in open(p, 'r').read(): results.append(p)
39
- except: continue
40
- return "\n".join(results[:15]) if results else "No matches."
41
-
42
- # --- 上下文准备 ---
43
- def get_context():
44
- if "pull_request" in event_data:
45
- p = event_data["pull_request"]
46
- return p["number"], f"PR @{p['user']['login']}\nTitle: {p['title']}\n{p['body']}"
47
-
48
- i = event_data["issue"]
49
- ctx = f"Issue @{i['user']['login']}\nTitle: {i['title']}\n{i['body']}"
50
- if event_name == "issue_comment":
51
- ctx += f"\n\nNew Comment by @{event_data['comment']['user']['login']}: {event_data['comment']['body']}"
52
- return i["number"], ctx
53
-
54
- issue_num, user_content = get_context()
55
- issue_obj = repo.get_issue(number=issue_num)
56
- repo_labels = [l.name for l in repo.get_labels()]
57
-
58
- # --- AI 执行逻辑 ---
59
- messages = [
60
- {"role": "system", "content": f"你是一个高级仓库助手。可用标签: {repo_labels}。必须首行返回JSON: {{\"labels\":[], \"state\":\"open\"}},然后解释逻辑。"},
61
- {"role": "user", "content": user_content}
62
- ]
63
-
64
- tools = [
65
- {"type": "function", "function": {"name": "list_directory", "description": "List files", "parameters": {"type": "object", "properties": {"path": {"type": "string"}}}}},
66
- {"type": "function", "function": {"name": "read_file", "description": "Read content", "parameters": {"type": "object", "properties": {"path": {"type": "string"}}}}},
67
- {"type": "function", "function": {"name": "search_keyword", "description": "Search keyword", "parameters": {"type": "object", "properties": {"keyword": {"type": "string"}}}}}
68
- ]
69
-
70
- # 允许最多 5 轮工具交互
71
- for i in range(5):
72
- response = client.chat.completions.create(
73
- model=os.getenv("AI_MODEL"),
74
- messages=messages,
75
- tools=tools,
76
- temperature=0
77
- )
78
-
79
- # 核心修复:统一将模型返回的消息转为可序列化的字典格式
80
- msg = response.choices[0].message
81
- msg_dict = msg.model_dump()
82
- messages.append(msg_dict)
83
-
84
- if not msg.tool_calls:
85
- break
86
-
87
- for tool_call in msg.tool_calls:
88
- args = json.loads(tool_call.function.arguments)
89
- func = {"list_directory": list_directory, "read_file": read_file, "search_keyword": search_keyword}.get(tool_call.function.name)
90
- result = func(**args) if func else "Unknown function"
91
- messages.append({"role": "tool", "tool_call_id": tool_call.id, "content": str(result)})
92
-
93
- # 确保最后一条消息是文本回复
94
- if messages[-1].get("role") == "tool" or (messages[-1].get("tool_calls") and not messages[-1].get("content")):
95
- final_check = client.chat.completions.create(model=os.getenv("AI_MODEL"), messages=messages)
96
- messages.append(final_check.choices[0].message.model_dump())
97
-
98
- # 提取最终文本内容
99
- final_msg = messages[-1]
100
- final_res = final_msg.get("content") or ""
101
-
102
- # --- 结果解析与执行 ---
103
- json_data = {"labels": [], "state": "open"}
104
- match = re.search(r'(\{.*?\})', final_res, re.DOTALL)
105
- if match:
106
- try:
107
- json_data = json.loads(match.group(1))
108
- final_res = final_res.replace(match.group(1), "").strip()
109
- except: pass
110
-
111
- if json_data.get("labels"):
112
- issue_obj.add_to_labels(*json_data["labels"])
113
- if json_data.get("state") in ["open", "closed"]:
114
- issue_obj.edit(state=json_data["state"])
115
-
116
- issue_obj.create_comment(f"### 🤖 AI Agent Execution\n\n{final_res}")
 
1
+ name: AI Repository Agent (Python Tools)
2
+
3
+ on:
4
+ pull_request:
5
+ types: [opened]
6
+ issues:
7
+ types: [opened]
8
+ issue_comment:
9
+ types: [created]
10
+
11
+ jobs:
12
+ ai-agent:
13
+ if: |
14
+ github.event_name == 'pull_request' ||
15
+ github.event_name == 'issues' ||
16
+ (github.event_name == 'issue_comment' && startsWith(github.event.comment.body, '[use-ai]'))
17
+ runs-on: ubuntu-latest
18
+ permissions:
19
+ contents: read
20
+ pull-requests: write
21
+ issues: write
22
+
23
+ steps:
24
+ - name: Checkout Code
25
+ uses: actions/checkout@v4
26
+ with:
27
+ fetch-depth: 0
28
+
29
+ - name: Set up Python
30
+ uses: actions/setup-python@v4
31
+ with:
32
+ python-version: '3.10'
33
+
34
+ - name: Install Dependencies
35
+ run: |
36
+ pip install openai PyGithub
37
+
38
+ - name: Run AI Agent
39
+ env:
40
+ OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
41
+ OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
42
+ AI_MODEL: ${{ secrets.AI_MODEL }}
43
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
44
+ EVENT_CONTEXT: ${{ toJson(github.event) }}
45
+ EVENT_NAME: ${{ github.event_name }}
46
+ run: python .github/scripts/ai_agent.py
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.github/workflows/ai-review.yaml CHANGED
@@ -1,46 +1,116 @@
1
- name: AI Repository Agent (Python Tools)
2
-
3
- on:
4
- pull_request:
5
- types: [opened]
6
- issues:
7
- types: [opened]
8
- issue_comment:
9
- types: [created]
10
-
11
- jobs:
12
- ai-agent:
13
- if: |
14
- github.event_name == 'pull_request' ||
15
- github.event_name == 'issues' ||
16
- (github.event_name == 'issue_comment' && startsWith(github.event.comment.body, '[use-ai]'))
17
- runs-on: ubuntu-latest
18
- permissions:
19
- contents: read
20
- pull-requests: write
21
- issues: write
22
-
23
- steps:
24
- - name: Checkout Code
25
- uses: actions/checkout@v4
26
- with:
27
- fetch-depth: 0
28
-
29
- - name: Set up Python
30
- uses: actions/setup-python@v4
31
- with:
32
- python-version: '3.10'
33
-
34
- - name: Install Dependencies
35
- run: |
36
- pip install openai PyGithub
37
-
38
- - name: Run AI Agent
39
- env:
40
- OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
41
- OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
42
- AI_MODEL: ${{ secrets.AI_MODEL }}
43
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
44
- EVENT_CONTEXT: ${{ toJson(github.event) }}
45
- EVENT_NAME: ${{ github.event_name }}
46
- run: python .github/scripts/ai_agent.py
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import re
4
+ from openai import OpenAI
5
+ from github import Github, Auth
6
+
7
+ # 初始化客户端
8
+ auth = Auth.Token(os.getenv("GITHUB_TOKEN"))
9
+ gh = Github(auth=auth)
10
+ repo = gh.get_repo(os.getenv("GITHUB_REPOSITORY"))
11
+ client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"), base_url=os.getenv("OPENAI_BASE_URL"))
12
+
13
+ event_data = json.loads(os.getenv("EVENT_CONTEXT"))
14
+ event_name = os.getenv("EVENT_NAME")
15
+
16
+ # --- 工具定义 (保持不变,增加 **kwargs 鲁棒性) ---
17
+ def list_directory(path=".", **kwargs):
18
+ try:
19
+ if ".." in path: return "Error: Access denied."
20
+ return "\n".join(os.listdir(path))
21
+ except Exception as e: return str(e)
22
+
23
+ def read_file(path, **kwargs):
24
+ try:
25
+ if ".." in path: return "Error: Access denied."
26
+ with open(path, 'r', encoding='utf-8') as f:
27
+ return f.read()[:5000]
28
+ except Exception as e: return str(e)
29
+
30
+ def search_keyword(keyword, path=".", **kwargs):
31
+ results = []
32
+ for root, _, files in os.walk(path):
33
+ if ".git" in root: continue
34
+ for file in files:
35
+ if file.endswith(('.py', '.js', '.ts', '.md', '.json', '.rs', '.yml')):
36
+ p = os.path.join(root, file)
37
+ try:
38
+ if keyword in open(p, 'r').read(): results.append(p)
39
+ except: continue
40
+ return "\n".join(results[:15]) if results else "No matches."
41
+
42
+ # --- 上下文准备 ---
43
+ def get_context():
44
+ if "pull_request" in event_data:
45
+ p = event_data["pull_request"]
46
+ return p["number"], f"PR @{p['user']['login']}\nTitle: {p['title']}\n{p['body']}"
47
+
48
+ i = event_data["issue"]
49
+ ctx = f"Issue @{i['user']['login']}\nTitle: {i['title']}\n{i['body']}"
50
+ if event_name == "issue_comment":
51
+ ctx += f"\n\nNew Comment by @{event_data['comment']['user']['login']}: {event_data['comment']['body']}"
52
+ return i["number"], ctx
53
+
54
+ issue_num, user_content = get_context()
55
+ issue_obj = repo.get_issue(number=issue_num)
56
+ repo_labels = [l.name for l in repo.get_labels()]
57
+
58
+ # --- AI 执行逻辑 ---
59
+ messages = [
60
+ {"role": "system", "content": f"你是一个高级仓库助手。可用标签: {repo_labels}。必须首行返回JSON: {{\"labels\":[], \"state\":\"open\"}},然后解释逻辑。"},
61
+ {"role": "user", "content": user_content}
62
+ ]
63
+
64
+ tools = [
65
+ {"type": "function", "function": {"name": "list_directory", "description": "List files", "parameters": {"type": "object", "properties": {"path": {"type": "string"}}}}},
66
+ {"type": "function", "function": {"name": "read_file", "description": "Read content", "parameters": {"type": "object", "properties": {"path": {"type": "string"}}}}},
67
+ {"type": "function", "function": {"name": "search_keyword", "description": "Search keyword", "parameters": {"type": "object", "properties": {"keyword": {"type": "string"}}}}}
68
+ ]
69
+
70
+ # 允许最多 5 轮工具交互
71
+ for i in range(5):
72
+ response = client.chat.completions.create(
73
+ model=os.getenv("AI_MODEL"),
74
+ messages=messages,
75
+ tools=tools,
76
+ temperature=0
77
+ )
78
+
79
+ # 核心修复:统一将模型返回的消息转为可序列化的字典格式
80
+ msg = response.choices[0].message
81
+ msg_dict = msg.model_dump()
82
+ messages.append(msg_dict)
83
+
84
+ if not msg.tool_calls:
85
+ break
86
+
87
+ for tool_call in msg.tool_calls:
88
+ args = json.loads(tool_call.function.arguments)
89
+ func = {"list_directory": list_directory, "read_file": read_file, "search_keyword": search_keyword}.get(tool_call.function.name)
90
+ result = func(**args) if func else "Unknown function"
91
+ messages.append({"role": "tool", "tool_call_id": tool_call.id, "content": str(result)})
92
+
93
+ # 确保最后一条消息是文本回复
94
+ if messages[-1].get("role") == "tool" or (messages[-1].get("tool_calls") and not messages[-1].get("content")):
95
+ final_check = client.chat.completions.create(model=os.getenv("AI_MODEL"), messages=messages)
96
+ messages.append(final_check.choices[0].message.model_dump())
97
+
98
+ # 提取最终文本内容
99
+ final_msg = messages[-1]
100
+ final_res = final_msg.get("content") or ""
101
+
102
+ # --- 结果解析与执行 ---
103
+ json_data = {"labels": [], "state": "open"}
104
+ match = re.search(r'(\{.*?\})', final_res, re.DOTALL)
105
+ if match:
106
+ try:
107
+ json_data = json.loads(match.group(1))
108
+ final_res = final_res.replace(match.group(1), "").strip()
109
+ except: pass
110
+
111
+ if json_data.get("labels"):
112
+ issue_obj.add_to_labels(*json_data["labels"])
113
+ if json_data.get("state") in ["open", "closed"]:
114
+ issue_obj.edit(state=json_data["state"])
115
+
116
+ issue_obj.create_comment(f"### 🤖 AI Agent Execution\n\n{final_res}")
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.github/scripts/ai_agent.py CHANGED
@@ -1,106 +1,110 @@
1
  import os
2
  import json
3
- import base64
4
  from openai import OpenAI
5
- from github import Github
6
 
7
- # 初始化客户端
8
- client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"), base_url=os.getenv("OPENAI_BASE_URL"))
9
- gh = Github(os.getenv("GITHUB_TOKEN"))
10
  repo = gh.get_repo(os.getenv("GITHUB_REPOSITORY"))
 
 
11
  event_data = json.loads(os.getenv("EVENT_CONTEXT"))
12
  event_name = os.getenv("EVENT_NAME")
13
 
14
- # --- 定义 AI 可调用的工具 ---
15
 
16
- def list_directory(path="."):
17
  """列出指定目录下的文件和文件夹"""
18
  try:
 
 
19
  items = os.listdir(path)
20
  return "\n".join(items)
21
  except Exception as e:
22
- return str(e)
23
 
24
- def read_file(path):
25
  """读取特定文件的完整内容"""
26
  try:
 
27
  with open(path, 'r', encoding='utf-8') as f:
28
- return f.read()[:5000] # 限制长度防止 Over-token
29
  except Exception as e:
30
- return str(e)
31
 
32
- def search_keyword(keyword, path="."):
33
  """在当前目录及其子目录中搜索关键词"""
34
  results = []
35
- for root, dirs, files in os.walk(path):
36
- for file in files:
37
- if file.endswith(('.py', '.js', '.ts', '.md', '.json', '.rs')):
38
- full_path = os.path.join(root, file)
39
- try:
40
- with open(full_path, 'r', encoding='utf-8') as f:
41
- if keyword in f.read():
42
- results.append(full_path)
43
- except:
44
- continue
45
- return "\n".join(results[:20])
46
-
47
- # --- 准备上下文与角色 ---
 
 
 
48
 
49
  def get_context():
50
- if event_name == "pull_request":
51
- number = event_data["pull_request"]["number"]
52
- author = event_data["pull_request"]["user"]["login"]
53
- title = event_data["pull_request"]["title"]
54
- body = event_data["pull_request"]["body"]
55
- return number, f"PR Author: @{author}\nTitle: {title}\nBody: {body}\n(This is a Pull Request)"
56
 
57
- number = event_data["issue"]["number"]
58
- author = event_data["issue"]["user"]["login"]
59
- title = event_data["issue"]["title"]
60
- body = event_data["issue"]["body"]
61
 
62
  if event_name == "issue_comment":
63
  actor = event_data["comment"]["user"]["login"]
64
  cmd = event_data["comment"]["body"]
65
- return number, f"Issue Author: @{author}\nTriggered by: @{actor}\nCommand: {cmd}\nContext: {title}\n{body}"
66
 
67
- return number, f"Issue Author: @{author}\nTitle: {title}\nBody: {body}"
68
 
69
  issue_num, user_content = get_context()
70
  issue_obj = repo.get_issue(number=issue_num)
71
  repo_labels = [l.name for l in repo.get_labels()]
72
 
73
- # --- 主逻辑 ---
74
 
75
  messages = [
76
  {"role": "system", "content": f"""你是一个高级仓库助手 (@github-actions[bot])。
77
- 你可以通过工具阅读代码、搜索文件并管理 Issue/PR 状态。
78
 
79
  可用标签: {repo_labels}
80
- 你的目标:
81
- 1. 理解用户意图。
82
- 2. 如果需要,使用工具查看项目结构或具体文件。
83
- 3. 给出处理方案,并直接执行(打标签、关闭等)。
84
 
85
- 输出规范:
86
- 回复开头必须是 JSON 指令: {{"labels": [], "state": "open"|"closed"}}
87
- 然后是你的执行报告。"""},
 
88
  {"role": "user", "content": user_content}
89
  ]
90
 
91
- # 工具定义
92
  tools = [
93
- {"type": "function", "function": {"name": "list_directory", "parameters": {"type": "object", "properties": {"path": {"type": "string"}}}}},
94
- {"type": "function", "function": {"name": "read_file", "parameters": {"type": "object", "properties": {"path": {"type": "string"}}}}},
95
- {"type": "function", "function": {"name": "search_keyword", "parameters": {"type": "object", "properties": {"keyword": {"type": "string"}}}}}
96
  ]
97
 
98
- # AI 决策循环 (允许最多 3 次工具调用)
99
  for _ in range(3):
100
  response = client.chat.completions.create(
101
  model=os.getenv("AI_MODEL"),
102
  messages=messages,
103
- tools=tools
 
104
  )
105
  msg = response.choices[0].message
106
  messages.append(msg)
@@ -109,32 +113,35 @@ for _ in range(3):
109
  break
110
 
111
  for tool_call in msg.tool_calls:
112
- func_name = tool_call.function.name
113
- args = json.loads(tool_call.function.arguments)
114
 
115
- if func_name == "list_directory": result = list_directory(**args)
116
- elif func_name == "read_file": result = read_file(**args)
117
- elif func_name == "search_keyword": result = search_keyword(**args)
 
 
 
118
 
119
- messages.append({"role": "tool", "tool_call_id": tool_call.id, "content": result})
120
-
121
- # 解析结果并操作 GitHub
122
- final_text = messages[-1].content
123
- json_part = {}
124
- try:
125
- if final_text.startswith("{"):
126
- import re
127
- match = re.search(r'(\{.*?\})', final_text, re.DOTALL)
128
- if match:
129
- json_part = json.loads(match.group(1))
130
- final_text = final_text.replace(match.group(1), "").strip()
131
- except:
132
- pass
133
-
134
- # 执行 GitHub 动作
135
- if json_part.get("labels"):
136
- issue_obj.add_to_labels(*json_part["labels"])
137
- if json_part.get("state"):
138
- issue_obj.edit(state=json_part["state"])
139
-
140
- issue_obj.create_comment(f"### 🤖 AI Agent Action\n\n{final_text}")
 
1
  import os
2
  import json
3
+ import re
4
  from openai import OpenAI
5
+ from github import Github, Auth # 导入 Auth 以修复弃用警告
6
 
7
+ # --- 1. 初始化客户端 (修复 DeprecationWarning) ---
8
+ auth = Auth.Token(os.getenv("GITHUB_TOKEN"))
9
+ gh = Github(auth=auth)
10
  repo = gh.get_repo(os.getenv("GITHUB_REPOSITORY"))
11
+ client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"), base_url=os.getenv("OPENAI_BASE_URL"))
12
+
13
  event_data = json.loads(os.getenv("EVENT_CONTEXT"))
14
  event_name = os.getenv("EVENT_NAME")
15
 
16
+ # --- 2. 定义工具 (增加 **kwargs 以忽略多余参数) ---
17
 
18
+ def list_directory(path=".", **kwargs):
19
  """列出指定目录下的文件和文件夹"""
20
  try:
21
+ # 基础路径安全检查
22
+ if ".." in path: return "Error: Cannot access parent directory."
23
  items = os.listdir(path)
24
  return "\n".join(items)
25
  except Exception as e:
26
+ return f"Error listing directory: {str(e)}"
27
 
28
+ def read_file(path, **kwargs):
29
  """读取特定文件的完整内容"""
30
  try:
31
+ if ".." in path: return "Error: Access denied."
32
  with open(path, 'r', encoding='utf-8') as f:
33
+ return f.read()[:5000]
34
  except Exception as e:
35
+ return f"Error reading file: {str(e)}"
36
 
37
+ def search_keyword(keyword, path=".", **kwargs):
38
  """在当前目录及其子目录中搜索关键词"""
39
  results = []
40
+ try:
41
+ for root, dirs, files in os.walk(path):
42
+ if ".git" in root: continue # 过滤 Git 目录
43
+ for file in files:
44
+ if file.endswith(('.py', '.js', '.ts', '.md', '.json', '.rs', '.toml', '.yml')):
45
+ full_path = os.path.join(root, file)
46
+ try:
47
+ with open(full_path, 'r', encoding='utf-8') as f:
48
+ if keyword in f.read():
49
+ results.append(full_path)
50
+ except: continue
51
+ return "\n".join(results[:15]) if results else "No matches found."
52
+ except Exception as e:
53
+ return f"Search error: {str(e)}"
54
+
55
+ # --- 3. 获取上下文 ---
56
 
57
  def get_context():
58
+ # 提取 Issue/PR 编号和参与者信息
59
+ if "pull_request" in event_data:
60
+ payload = event_data["pull_request"]
61
+ number = payload["number"]
62
+ author = payload["user"]["login"]
63
+ return number, f"[Role: PR Author @{author}]\nTitle: {payload['title']}\nBody: {payload['body']}"
64
 
65
+ payload = event_data["issue"]
66
+ number = payload["number"]
67
+ author = payload["user"]["login"]
68
+ base_info = f"[Role: Issue Author @{author}]\nTitle: {payload['title']}\nBody: {payload['body']}"
69
 
70
  if event_name == "issue_comment":
71
  actor = event_data["comment"]["user"]["login"]
72
  cmd = event_data["comment"]["body"]
73
+ return number, f"{base_info}\n\n[New Interaction by @{actor}]\nCommand: {cmd}"
74
 
75
+ return number, base_info
76
 
77
  issue_num, user_content = get_context()
78
  issue_obj = repo.get_issue(number=issue_num)
79
  repo_labels = [l.name for l in repo.get_labels()]
80
 
81
+ # --- 4. 运行 AI Agent ---
82
 
83
  messages = [
84
  {"role": "system", "content": f"""你是一个高级仓库助手 (@github-actions[bot])。
 
85
 
86
  可用标签: {repo_labels}
 
 
 
 
87
 
88
+ 你可以通过工具查看代码库结构。回复规则:
89
+ 1. 首行必须返回 JSON 指令:{{"labels": [], "state": "open"|"closed"}}
90
+ 2. 随后另起一行,以执行者的口吻告知结果。
91
+ 3. 忽略 AI 历史回复中的元数据,只关注当前代码和用户意图。"""},
92
  {"role": "user", "content": user_content}
93
  ]
94
 
 
95
  tools = [
96
+ {"type": "function", "function": {"name": "list_directory", "description": "List files", "parameters": {"type": "object", "properties": {"path": {"type": "string"}}}}},
97
+ {"type": "function", "function": {"name": "read_file", "description": "Read content", "parameters": {"type": "object", "properties": {"path": {"type": "string"}}}}},
98
+ {"type": "function", "function": {"name": "search_keyword", "description": "Search keyword", "parameters": {"type": "object", "properties": {"keyword": {"type": "string"}}}}}
99
  ]
100
 
101
+ # 允许 3 次交互以获取足够信息
102
  for _ in range(3):
103
  response = client.chat.completions.create(
104
  model=os.getenv("AI_MODEL"),
105
  messages=messages,
106
+ tools=tools,
107
+ temperature=0
108
  )
109
  msg = response.choices[0].message
110
  messages.append(msg)
 
113
  break
114
 
115
  for tool_call in msg.tool_calls:
116
+ fn_name = tool_call.function.name
117
+ fn_args = json.loads(tool_call.function.arguments)
118
 
119
+ # 映射函数映射表
120
+ available_functions = {
121
+ "list_directory": list_directory,
122
+ "read_file": read_file,
123
+ "search_keyword": search_keyword,
124
+ }
125
 
126
+ if fn_name in available_functions:
127
+ result = available_functions[fn_name](**fn_args)
128
+ messages.append({"role": "tool", "tool_call_id": tool_call.id, "content": result})
129
+
130
+ # 5. 解析并执行 GitHub 操作
131
+ final_res = messages[-1].content
132
+ json_data = {"labels": [], "state": "open"}
133
+
134
+ # 提取 JSON
135
+ match = re.search(r'(\{.*?\})', final_res, re.DOTALL)
136
+ if match:
137
+ try:
138
+ json_data = json.loads(match.group(1))
139
+ final_res = final_res.replace(match.group(1), "").strip()
140
+ except: pass
141
+
142
+ if json_data.get("labels"):
143
+ issue_obj.add_to_labels(*json_data["labels"])
144
+ if json_data.get("state") and json_data["state"] in ["open", "closed"]:
145
+ issue_obj.edit(state=json_data["state"])
146
+
147
+ issue_obj.create_comment(f"### 🤖 AI Agent Execution\n\n{final_res}")
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.github/workflows/ai-review.yaml CHANGED
@@ -1,4 +1,4 @@
1
- name: AI Repository Assistant
2
 
3
  on:
4
  pull_request:
@@ -9,11 +9,11 @@ on:
9
  types: [created]
10
 
11
  jobs:
12
- ai-assistant:
13
  if: |
14
  github.event_name == 'pull_request' ||
15
  github.event_name == 'issues' ||
16
- startsWith(github.event.comment.body, '[use-ai]')
17
  runs-on: ubuntu-latest
18
  permissions:
19
  contents: read
@@ -23,120 +23,24 @@ jobs:
23
  steps:
24
  - name: Checkout Code
25
  uses: actions/checkout@v4
26
-
27
- - name: Prepare Context
28
- id: prep
29
- run: |
30
- # 提取基本信息
31
- ISSUE_AUTHOR="${{ github.event.issue.user.login || github.event.pull_request.user.login }}"
32
- CURRENT_ACTOR="${{ github.actor }}"
33
-
34
- echo "### METADATA ###" > content.txt
35
- echo "Issue/PR Author: @$ISSUE_AUTHOR" >> content.txt
36
- echo "Instruction triggered by: @$CURRENT_ACTOR" >> content.txt
37
- echo "Assistant Identity: @github-actions[bot]" >> content.txt
38
- echo -e "------------------\n" >> content.txt
39
-
40
- if [ "${{ github.event_name }}" == "issue_comment" ]; then
41
- echo "### NEW COMMAND FROM @$CURRENT_ACTOR ###" >> content.txt
42
- echo "Comment: ${{ github.event.comment.body }}" >> content.txt
43
- echo -e "\n### ORIGINAL CONTEXT ###" >> content.txt
44
- echo "Title: ${{ github.event.issue.title }}" >> content.txt
45
- echo "Description: ${{ github.event.issue.body }}" >> content.txt
46
- elif [ "${{ github.event_name }}" == "pull_request" ]; then
47
- echo "### NEW PULL REQUEST FROM @$ISSUE_AUTHOR ###" >> content.txt
48
- git fetch origin ${{ github.event.pull_request.base.ref }}
49
- git diff origin/${{ github.event.pull_request.base.ref }}...HEAD > pr_diff.txt
50
- cat pr_diff.txt >> content.txt
51
- else
52
- echo "### NEW ISSUE FROM @$ISSUE_AUTHOR ###" >> content.txt
53
- echo "Title: ${{ github.event.issue.title }}" >> content.txt
54
- echo "Body: ${{ github.event.issue.body }}" >> content.txt
55
- fi
56
-
57
- echo "number=${{ github.event.issue.number || github.event.pull_request.number }}" >> $GITHUB_OUTPUT
58
- echo "type=${{ github.event_name }}" >> $GITHUB_OUTPUT
59
-
60
- - name: AI Action Execution
61
- uses: actions/github-script@v7
62
- env:
63
- API_KEY: ${{ secrets.OPENAI_API_KEY }}
64
- BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
65
- MODEL: ${{ secrets.AI_MODEL }}
66
- ISSUE_NUMBER: ${{ steps.prep.outputs.number }}
67
  with:
68
- script: |
69
- const fs = require('fs');
70
- const content = fs.readFileSync('content.txt', 'utf8');
71
-
72
- const { data: repoLabels } = await github.rest.issues.listLabelsForRepo({
73
- owner: context.repo.owner,
74
- repo: context.repo.repo,
75
- });
76
- const labelNames = repoLabels.map(l => l.name);
77
-
78
- const prompt = `你是一个拥有自主权的仓库助手 (@github-actions[bot])。
79
- 你需要根据提供的【METADATA】区分对话中的不同角色。
80
-
81
- 角色指南:
82
- 1. Issue/PR Author: 任务的发起者。
83
- 2. Instruction triggered by: 当前向你下达指令的人。如果是 Author 且内容包含 "[use-ai]",说明他在请求你介入。
84
- 3. Assistant Identity: 这是你自己。请不要审视或评价你自己的历史回复。
85
-
86
- 你的权力:
87
- - 根据 Issue 内容的质量自动打标签(可选:${labelNames.join(", ")})。
88
- - 如果内容是无意义的测试、违反规范或已解决,请直接关闭它。
89
- - 你的语气应该是果断的执行者,而不是卑微的助理。
90
-
91
- 输出要求(严格 JSON 第一行):
92
- {"labels": ["label_name"], "state": "closed" | "open"}
93
- 然后另起一行说明你的处理逻辑。`;
94
 
95
- try {
96
- const response = await fetch(`${process.env.BASE_URL}/chat/completions`, {
97
- method: 'POST',
98
- headers: { 'Authorization': `Bearer ${process.env.API_KEY}`, 'Content-Type': 'application/json' },
99
- body: JSON.stringify({
100
- model: process.env.MODEL,
101
- messages: [{ role: "user", content: prompt + "\n\n内容如下:\n" + content }],
102
- temperature: 0.1 // 降低随机性,增强逻辑判断
103
- })
104
- });
105
-
106
- const data = await response.json();
107
- const fullText = data.choices[0].message.content;
108
-
109
- const jsonMatch = fullText.match(/^\{.*?\}/);
110
- let config = { labels: [], state: "open" };
111
- let commentBody = fullText;
112
-
113
- if (jsonMatch) {
114
- config = JSON.parse(jsonMatch[0]);
115
- commentBody = fullText.replace(jsonMatch[0], "").trim();
116
- }
117
-
118
- const issueParams = {
119
- owner: context.repo.owner,
120
- repo: context.repo.repo,
121
- issue_number: parseInt(process.env.ISSUE_NUMBER)
122
- };
123
-
124
- // 执行打标签
125
- if (config.labels && config.labels.length > 0) {
126
- await github.rest.issues.addLabels({ ...issueParams, labels: config.labels });
127
- }
128
-
129
- // 执行状态变更
130
- if (config.state) {
131
- await github.rest.issues.update({ ...issueParams, state: config.state });
132
- }
133
 
134
- // 发表回复
135
- await github.rest.issues.createComment({
136
- ...issueParams,
137
- body: `### 🤖 AI Assistant Action\n\n${commentBody}`
138
- });
139
 
140
- } catch (err) {
141
- core.setFailed(err.message);
142
- }
 
 
 
 
 
 
 
1
+ name: AI Repository Agent (Python Tools)
2
 
3
  on:
4
  pull_request:
 
9
  types: [created]
10
 
11
  jobs:
12
+ ai-agent:
13
  if: |
14
  github.event_name == 'pull_request' ||
15
  github.event_name == 'issues' ||
16
+ (github.event_name == 'issue_comment' && startsWith(github.event.comment.body, '[use-ai]'))
17
  runs-on: ubuntu-latest
18
  permissions:
19
  contents: read
 
23
  steps:
24
  - name: Checkout Code
25
  uses: actions/checkout@v4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  with:
27
+ fetch-depth: 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
 
29
+ - name: Set up Python
30
+ uses: actions/setup-python@v4
31
+ with:
32
+ python-version: '3.10'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
+ - name: Install Dependencies
35
+ run: |
36
+ pip install openai PyGithub
 
 
37
 
38
+ - name: Run AI Agent
39
+ env:
40
+ OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
41
+ OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
42
+ AI_MODEL: ${{ secrets.AI_MODEL }}
43
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
44
+ EVENT_CONTEXT: ${{ toJson(github.event) }}
45
+ EVENT_NAME: ${{ github.event_name }}
46
+ run: python .github/scripts/ai_agent.py
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.github/scripts/ai_agent.py ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import base64
4
+ from openai import OpenAI
5
+ from github import Github
6
+
7
+ # 初始化客户端
8
+ client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"), base_url=os.getenv("OPENAI_BASE_URL"))
9
+ gh = Github(os.getenv("GITHUB_TOKEN"))
10
+ repo = gh.get_repo(os.getenv("GITHUB_REPOSITORY"))
11
+ event_data = json.loads(os.getenv("EVENT_CONTEXT"))
12
+ event_name = os.getenv("EVENT_NAME")
13
+
14
+ # --- 定义 AI 可调用的工具 ---
15
+
16
+ def list_directory(path="."):
17
+ """列出指定目录下的文件和文件夹"""
18
+ try:
19
+ items = os.listdir(path)
20
+ return "\n".join(items)
21
+ except Exception as e:
22
+ return str(e)
23
+
24
+ def read_file(path):
25
+ """读取特定文件的完整内容"""
26
+ try:
27
+ with open(path, 'r', encoding='utf-8') as f:
28
+ return f.read()[:5000] # 限制长度防止 Over-token
29
+ except Exception as e:
30
+ return str(e)
31
+
32
+ def search_keyword(keyword, path="."):
33
+ """在当前目录及其子目录中搜索关键词"""
34
+ results = []
35
+ for root, dirs, files in os.walk(path):
36
+ for file in files:
37
+ if file.endswith(('.py', '.js', '.ts', '.md', '.json', '.rs')):
38
+ full_path = os.path.join(root, file)
39
+ try:
40
+ with open(full_path, 'r', encoding='utf-8') as f:
41
+ if keyword in f.read():
42
+ results.append(full_path)
43
+ except:
44
+ continue
45
+ return "\n".join(results[:20])
46
+
47
+ # --- 准备上下文与角色 ---
48
+
49
+ def get_context():
50
+ if event_name == "pull_request":
51
+ number = event_data["pull_request"]["number"]
52
+ author = event_data["pull_request"]["user"]["login"]
53
+ title = event_data["pull_request"]["title"]
54
+ body = event_data["pull_request"]["body"]
55
+ return number, f"PR Author: @{author}\nTitle: {title}\nBody: {body}\n(This is a Pull Request)"
56
+
57
+ number = event_data["issue"]["number"]
58
+ author = event_data["issue"]["user"]["login"]
59
+ title = event_data["issue"]["title"]
60
+ body = event_data["issue"]["body"]
61
+
62
+ if event_name == "issue_comment":
63
+ actor = event_data["comment"]["user"]["login"]
64
+ cmd = event_data["comment"]["body"]
65
+ return number, f"Issue Author: @{author}\nTriggered by: @{actor}\nCommand: {cmd}\nContext: {title}\n{body}"
66
+
67
+ return number, f"Issue Author: @{author}\nTitle: {title}\nBody: {body}"
68
+
69
+ issue_num, user_content = get_context()
70
+ issue_obj = repo.get_issue(number=issue_num)
71
+ repo_labels = [l.name for l in repo.get_labels()]
72
+
73
+ # --- 主逻辑 ---
74
+
75
+ messages = [
76
+ {"role": "system", "content": f"""你是一个高级仓库助手 (@github-actions[bot])。
77
+ 你可以通过工具阅读代码、搜索文件并管理 Issue/PR 状态。
78
+
79
+ 可用标签: {repo_labels}
80
+ 你的目标:
81
+ 1. 理解用户意图。
82
+ 2. 如果需要,使用工具查看项目结构或具体文件。
83
+ 3. 给出处理方案,并直接执行(打标签、关闭等)。
84
+
85
+ 输出规范:
86
+ 回复开头必须是 JSON 指令: {{"labels": [], "state": "open"|"closed"}}
87
+ 然后是你的执行报告。"""},
88
+ {"role": "user", "content": user_content}
89
+ ]
90
+
91
+ # 工具定义
92
+ tools = [
93
+ {"type": "function", "function": {"name": "list_directory", "parameters": {"type": "object", "properties": {"path": {"type": "string"}}}}},
94
+ {"type": "function", "function": {"name": "read_file", "parameters": {"type": "object", "properties": {"path": {"type": "string"}}}}},
95
+ {"type": "function", "function": {"name": "search_keyword", "parameters": {"type": "object", "properties": {"keyword": {"type": "string"}}}}}
96
+ ]
97
+
98
+ # AI 决策循环 (允许最多 3 次工具调用)
99
+ for _ in range(3):
100
+ response = client.chat.completions.create(
101
+ model=os.getenv("AI_MODEL"),
102
+ messages=messages,
103
+ tools=tools
104
+ )
105
+ msg = response.choices[0].message
106
+ messages.append(msg)
107
+
108
+ if not msg.tool_calls:
109
+ break
110
+
111
+ for tool_call in msg.tool_calls:
112
+ func_name = tool_call.function.name
113
+ args = json.loads(tool_call.function.arguments)
114
+
115
+ if func_name == "list_directory": result = list_directory(**args)
116
+ elif func_name == "read_file": result = read_file(**args)
117
+ elif func_name == "search_keyword": result = search_keyword(**args)
118
+
119
+ messages.append({"role": "tool", "tool_call_id": tool_call.id, "content": result})
120
+
121
+ # 解析结果并操作 GitHub
122
+ final_text = messages[-1].content
123
+ json_part = {}
124
+ try:
125
+ if final_text.startswith("{"):
126
+ import re
127
+ match = re.search(r'(\{.*?\})', final_text, re.DOTALL)
128
+ if match:
129
+ json_part = json.loads(match.group(1))
130
+ final_text = final_text.replace(match.group(1), "").strip()
131
+ except:
132
+ pass
133
+
134
+ # 执行 GitHub 动作
135
+ if json_part.get("labels"):
136
+ issue_obj.add_to_labels(*json_part["labels"])
137
+ if json_part.get("state"):
138
+ issue_obj.edit(state=json_part["state"])
139
+
140
+ issue_obj.create_comment(f"### 🤖 AI Agent Action\n\n{final_text}")
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.github/workflows/ai-review.yaml CHANGED
@@ -1,4 +1,4 @@
1
- name: AI Repository Assistant (Role-Aware)
2
 
3
  on:
4
  pull_request:
 
1
+ name: AI Repository Assistant
2
 
3
  on:
4
  pull_request:
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.github/workflows/ai-review.yaml CHANGED
@@ -1,4 +1,4 @@
1
- name: AI Repository Assistant
2
 
3
  on:
4
  pull_request:
@@ -6,11 +6,10 @@ on:
6
  issues:
7
  types: [opened]
8
  issue_comment:
9
- types: [created] # 支持通过评论触发
10
 
11
  jobs:
12
  ai-assistant:
13
- # 仅在:1. 新开 PR/Issue 2. 评论以 [use-ai] 开头时运行
14
  if: |
15
  github.event_name == 'pull_request' ||
16
  github.event_name == 'issues' ||
@@ -28,25 +27,35 @@ jobs:
28
  - name: Prepare Context
29
  id: prep
30
  run: |
31
- # 判断是 PR 还是 Issue
32
- if [ "${{ github.event.pull_request }}" != "" ]; then
33
- EVENT_TYPE="PR"
34
- ISSUE_NUMBER="${{ github.event.pull_request.number }}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  git fetch origin ${{ github.event.pull_request.base.ref }}
36
- git diff origin/${{ github.event.pull_request.base.ref }}...HEAD > content.txt
 
37
  else
38
- EVENT_TYPE="Issue"
39
- ISSUE_NUMBER="${{ github.event.issue.number }}"
40
- # 如果是评论触发,获取评论内容作为补充指令
41
- if [ "${{ github.event_name }}" == "issue_comment" ]; then
42
- echo "Command: ${{ github.event.comment.body }}" > content.txt
43
- echo -e "\nOriginal Content:\n${{ github.event.issue.title }}\n${{ github.event.issue.body }}" >> content.txt
44
- else
45
- echo -e "Title: ${{ github.event.issue.title }}\n\n${{ github.event.issue.body }}" > content.txt
46
- fi
47
  fi
48
- echo "type=$EVENT_TYPE" >> $GITHUB_OUTPUT
49
- echo "number=$ISSUE_NUMBER" >> $GITHUB_OUTPUT
 
50
 
51
  - name: AI Action Execution
52
  uses: actions/github-script@v7
@@ -54,35 +63,34 @@ jobs:
54
  API_KEY: ${{ secrets.OPENAI_API_KEY }}
55
  BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
56
  MODEL: ${{ secrets.AI_MODEL }}
57
- EVENT_TYPE: ${{ steps.prep.outputs.type }}
58
  ISSUE_NUMBER: ${{ steps.prep.outputs.number }}
59
  with:
60
  script: |
61
  const fs = require('fs');
62
  const content = fs.readFileSync('content.txt', 'utf8');
63
 
64
- // 1. 获取现有 Labels
65
  const { data: repoLabels } = await github.rest.issues.listLabelsForRepo({
66
  owner: context.repo.owner,
67
  repo: context.repo.repo,
68
  });
69
  const labelNames = repoLabels.map(l => l.name);
70
 
71
- // 2. 角色设定:从“建议者”转变为“执行者”
72
- const prompt = `你是一个自动化的仓库助手。你被授权管理该仓库的 Issues 和 PR。
73
- 你的任务:分析内容,打上标签,并决定是否关闭或保持开启。
74
-
75
- 仓库现有标签:[${labelNames.join(", ")}]。
76
-
77
- 输出要求:
78
- 必须在回复的第一行输出 JSON 格式的操作指令:
79
- {"labels": ["label1"], "state": "closed" | "open"}
80
 
81
- 随后另起一行,以助手的身份简洁地说明你执行的操作和理由。不要说“我建议”,要说“我已执行”。
82
-
83
- 当前环境:${process.env.EVENT_TYPE}
84
- 内容详情:
85
- ${content}`;
 
 
 
 
 
 
 
 
86
 
87
  try {
88
  const response = await fetch(`${process.env.BASE_URL}/chat/completions`, {
@@ -90,15 +98,14 @@ jobs:
90
  headers: { 'Authorization': `Bearer ${process.env.API_KEY}`, 'Content-Type': 'application/json' },
91
  body: JSON.stringify({
92
  model: process.env.MODEL,
93
- messages: [{ role: "user", content: prompt }],
94
- temperature: 0.2
95
  })
96
  });
97
 
98
  const data = await response.json();
99
  const fullText = data.choices[0].message.content;
100
 
101
- // 3. 解析指令
102
  const jsonMatch = fullText.match(/^\{.*?\}/);
103
  let config = { labels: [], state: "open" };
104
  let commentBody = fullText;
@@ -114,23 +121,22 @@ jobs:
114
  issue_number: parseInt(process.env.ISSUE_NUMBER)
115
  };
116
 
117
- // 4. 执行操作:打标签
118
  if (config.labels && config.labels.length > 0) {
119
  await github.rest.issues.addLabels({ ...issueParams, labels: config.labels });
120
  }
121
 
122
- // 5. 执行操作:修改状态 (Close/Reopen)
123
  if (config.state) {
124
  await github.rest.issues.update({ ...issueParams, state: config.state });
125
  }
126
 
127
- // 6. 发布执行报告
128
  await github.rest.issues.createComment({
129
  ...issueParams,
130
  body: `### 🤖 AI Assistant Action\n\n${commentBody}`
131
  });
132
 
133
  } catch (err) {
134
- console.error(err);
135
  core.setFailed(err.message);
136
  }
 
1
+ name: AI Repository Assistant (Role-Aware)
2
 
3
  on:
4
  pull_request:
 
6
  issues:
7
  types: [opened]
8
  issue_comment:
9
+ types: [created]
10
 
11
  jobs:
12
  ai-assistant:
 
13
  if: |
14
  github.event_name == 'pull_request' ||
15
  github.event_name == 'issues' ||
 
27
  - name: Prepare Context
28
  id: prep
29
  run: |
30
+ # 提取基本信息
31
+ ISSUE_AUTHOR="${{ github.event.issue.user.login || github.event.pull_request.user.login }}"
32
+ CURRENT_ACTOR="${{ github.actor }}"
33
+
34
+ echo "### METADATA ###" > content.txt
35
+ echo "Issue/PR Author: @$ISSUE_AUTHOR" >> content.txt
36
+ echo "Instruction triggered by: @$CURRENT_ACTOR" >> content.txt
37
+ echo "Assistant Identity: @github-actions[bot]" >> content.txt
38
+ echo -e "------------------\n" >> content.txt
39
+
40
+ if [ "${{ github.event_name }}" == "issue_comment" ]; then
41
+ echo "### NEW COMMAND FROM @$CURRENT_ACTOR ###" >> content.txt
42
+ echo "Comment: ${{ github.event.comment.body }}" >> content.txt
43
+ echo -e "\n### ORIGINAL CONTEXT ###" >> content.txt
44
+ echo "Title: ${{ github.event.issue.title }}" >> content.txt
45
+ echo "Description: ${{ github.event.issue.body }}" >> content.txt
46
+ elif [ "${{ github.event_name }}" == "pull_request" ]; then
47
+ echo "### NEW PULL REQUEST FROM @$ISSUE_AUTHOR ###" >> content.txt
48
  git fetch origin ${{ github.event.pull_request.base.ref }}
49
+ git diff origin/${{ github.event.pull_request.base.ref }}...HEAD > pr_diff.txt
50
+ cat pr_diff.txt >> content.txt
51
  else
52
+ echo "### NEW ISSUE FROM @$ISSUE_AUTHOR ###" >> content.txt
53
+ echo "Title: ${{ github.event.issue.title }}" >> content.txt
54
+ echo "Body: ${{ github.event.issue.body }}" >> content.txt
 
 
 
 
 
 
55
  fi
56
+
57
+ echo "number=${{ github.event.issue.number || github.event.pull_request.number }}" >> $GITHUB_OUTPUT
58
+ echo "type=${{ github.event_name }}" >> $GITHUB_OUTPUT
59
 
60
  - name: AI Action Execution
61
  uses: actions/github-script@v7
 
63
  API_KEY: ${{ secrets.OPENAI_API_KEY }}
64
  BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
65
  MODEL: ${{ secrets.AI_MODEL }}
 
66
  ISSUE_NUMBER: ${{ steps.prep.outputs.number }}
67
  with:
68
  script: |
69
  const fs = require('fs');
70
  const content = fs.readFileSync('content.txt', 'utf8');
71
 
 
72
  const { data: repoLabels } = await github.rest.issues.listLabelsForRepo({
73
  owner: context.repo.owner,
74
  repo: context.repo.repo,
75
  });
76
  const labelNames = repoLabels.map(l => l.name);
77
 
78
+ const prompt = `你是一个拥有自主权的仓库助手 (@github-actions[bot])。
79
+ 你需要根据提供的【METADATA】区分对话中的不同角色。
 
 
 
 
 
 
 
80
 
81
+ 角���指南:
82
+ 1. Issue/PR Author: 任务的发起者。
83
+ 2. Instruction triggered by: 当前向你下达指令的人。如果是 Author 且内容包含 "[use-ai]",说明他在请求你介入。
84
+ 3. Assistant Identity: 这是你自己。请不要审视或评价你自己的历史回复。
85
+
86
+ 你的权力:
87
+ - 根据 Issue 内容的质量自动打标签(可选:${labelNames.join(", ")})。
88
+ - 如果内容是无意义的测试、违反规范或已解决,请直接关闭它。
89
+ - 你的语气应该是果断的执行者,而不是卑微的助理。
90
+
91
+ 输出要求(严格 JSON 第一行):
92
+ {"labels": ["label_name"], "state": "closed" | "open"}
93
+ 然后另起一行说明你的处理逻辑。`;
94
 
95
  try {
96
  const response = await fetch(`${process.env.BASE_URL}/chat/completions`, {
 
98
  headers: { 'Authorization': `Bearer ${process.env.API_KEY}`, 'Content-Type': 'application/json' },
99
  body: JSON.stringify({
100
  model: process.env.MODEL,
101
+ messages: [{ role: "user", content: prompt + "\n\n内容如下:\n" + content }],
102
+ temperature: 0.1 // 降低随机性,增强逻辑判断
103
  })
104
  });
105
 
106
  const data = await response.json();
107
  const fullText = data.choices[0].message.content;
108
 
 
109
  const jsonMatch = fullText.match(/^\{.*?\}/);
110
  let config = { labels: [], state: "open" };
111
  let commentBody = fullText;
 
121
  issue_number: parseInt(process.env.ISSUE_NUMBER)
122
  };
123
 
124
+ // 执行打标签
125
  if (config.labels && config.labels.length > 0) {
126
  await github.rest.issues.addLabels({ ...issueParams, labels: config.labels });
127
  }
128
 
129
+ // 执行状态变更
130
  if (config.state) {
131
  await github.rest.issues.update({ ...issueParams, state: config.state });
132
  }
133
 
134
+ // 发表回复
135
  await github.rest.issues.createComment({
136
  ...issueParams,
137
  body: `### 🤖 AI Assistant Action\n\n${commentBody}`
138
  });
139
 
140
  } catch (err) {
 
141
  core.setFailed(err.message);
142
  }
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.github/workflows/ai-review.yaml CHANGED
@@ -1,13 +1,20 @@
1
- name: AI PR & Issue Assistant with Auto-Label
2
 
3
  on:
4
  pull_request:
5
- types: [opened, synchronize]
6
  issues:
7
  types: [opened]
 
 
8
 
9
  jobs:
10
  ai-assistant:
 
 
 
 
 
11
  runs-on: ubuntu-latest
12
  permissions:
13
  contents: read
@@ -21,18 +28,27 @@ jobs:
21
  - name: Prepare Context
22
  id: prep
23
  run: |
24
- if [ "${{ github.event_name }}" == "pull_request" ]; then
25
- git fetch origin ${{ github.base_ref }}
26
- git diff origin/${{ github.base_ref }}...HEAD > input_content.txt
27
- echo "type=PR" >> $GITHUB_OUTPUT
28
- echo "number=${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT
 
29
  else
30
- echo -e "Title: ${{ github.event.issue.title }}\n\n${{ github.event.issue.body }}" > input_content.txt
31
- echo "type=Issue" >> $GITHUB_OUTPUT
32
- echo "number=${{ github.event.issue.number }}" >> $GITHUB_OUTPUT
 
 
 
 
 
 
33
  fi
 
 
34
 
35
- - name: AI Processing & Labeling
36
  uses: actions/github-script@v7
37
  env:
38
  API_KEY: ${{ secrets.OPENAI_API_KEY }}
@@ -40,42 +56,38 @@ jobs:
40
  MODEL: ${{ secrets.AI_MODEL }}
41
  EVENT_TYPE: ${{ steps.prep.outputs.type }}
42
  ISSUE_NUMBER: ${{ steps.prep.outputs.number }}
43
- ENABLE_AUTO_LABEL: "true" # 设置为 "false" 则彻底关闭自动打标签功能
44
  with:
45
  script: |
46
  const fs = require('fs');
47
- const content = fs.readFileSync('input_content.txt', 'utf8');
48
- if (!content.trim()) return;
49
 
50
- // 1. 获取仓库现有的所有 Labels
51
  const { data: repoLabels } = await github.rest.issues.listLabelsForRepo({
52
  owner: context.repo.owner,
53
  repo: context.repo.repo,
54
  });
55
  const labelNames = repoLabels.map(l => l.name);
56
 
57
- // 2. 构建针对标签的 Prompt
58
- let labelInstruction = "";
59
- if (process.env.ENABLE_AUTO_LABEL === "true") {
60
- labelInstruction = `
61
- 请从以下现有的标签列表中选择最合适的标签(可多选),仅从列表中选择:[${labelNames.join(", ")}]。
62
- 如果你认为不需要打标签,请返回空数组。
63
- 请在回复的最开头以 JSON 格式输出你的选择,格式如下:
64
- {"suggested_labels": ["label1", "label2"]}
65
- 然后另起一行开始你的评论分析。`;
66
- }
67
-
68
- const prompt = process.env.EVENT_TYPE === 'PR'
69
- ? `你是一位资深工程师。请审查以下 PR 的代码改动,指出潜在 Bug 和改进点。${labelInstruction}\n\nDiff内容:\n${content}`
70
- : `你是一位开源项目维护者。请分析以下 Issue 并给出建议。${labelInstruction}\n\nIssue内容:\n${content}`;
 
71
 
72
  try {
73
  const response = await fetch(`${process.env.BASE_URL}/chat/completions`, {
74
  method: 'POST',
75
- headers: {
76
- 'Authorization': `Bearer ${process.env.API_KEY}`,
77
- 'Content-Type': 'application/json'
78
- },
79
  body: JSON.stringify({
80
  model: process.env.MODEL,
81
  messages: [{ role: "user", content: prompt }],
@@ -86,40 +98,39 @@ jobs:
86
  const data = await response.json();
87
  const fullText = data.choices[0].message.content;
88
 
89
- // 3. 解析标签和正文
 
 
90
  let commentBody = fullText;
91
- let labelsToAdd = [];
92
-
93
- if (process.env.ENABLE_AUTO_LABEL === "true") {
94
- const jsonMatch = fullText.match(/^\{.*?\}/); // 匹配开头的 JSON
95
- if (jsonMatch) {
96
- try {
97
- const labelData = JSON.parse(jsonMatch[0]);
98
- labelsToAdd = labelData.suggested_labels || [];
99
- commentBody = fullText.replace(jsonMatch[0], "").trim();
100
- } catch (e) {
101
- console.log("Failed to parse labels JSON");
102
- }
103
- }
104
  }
105
 
106
- // 4. 发布评论
107
- await github.rest.issues.createComment({
108
  owner: context.repo.owner,
109
  repo: context.repo.repo,
110
- issue_number: parseInt(process.env.ISSUE_NUMBER),
111
- body: `### 🤖 AI ${process.env.EVENT_TYPE === 'PR' ? 'Review' : 'Assistant'}\n\n${commentBody}`
112
- });
113
 
114
- // 5. 自动打标签(如果有建议的标签)
115
- if (labelsToAdd.length > 0) {
116
- await github.rest.issues.addLabels({
117
- owner: context.repo.owner,
118
- repo: context.repo.repo,
119
- issue_number: parseInt(process.env.ISSUE_NUMBER),
120
- labels: labelsToAdd
121
- });
122
  }
 
 
 
 
 
 
 
 
 
 
 
 
123
  } catch (err) {
 
124
  core.setFailed(err.message);
125
  }
 
1
+ name: AI Repository Assistant
2
 
3
  on:
4
  pull_request:
5
+ types: [opened]
6
  issues:
7
  types: [opened]
8
+ issue_comment:
9
+ types: [created] # 支持通过评论触发
10
 
11
  jobs:
12
  ai-assistant:
13
+ # 仅在:1. 新开 PR/Issue 2. 评论以 [use-ai] 开头时运行
14
+ if: |
15
+ github.event_name == 'pull_request' ||
16
+ github.event_name == 'issues' ||
17
+ startsWith(github.event.comment.body, '[use-ai]')
18
  runs-on: ubuntu-latest
19
  permissions:
20
  contents: read
 
28
  - name: Prepare Context
29
  id: prep
30
  run: |
31
+ # 判断是 PR 还是 Issue
32
+ if [ "${{ github.event.pull_request }}" != "" ]; then
33
+ EVENT_TYPE="PR"
34
+ ISSUE_NUMBER="${{ github.event.pull_request.number }}"
35
+ git fetch origin ${{ github.event.pull_request.base.ref }}
36
+ git diff origin/${{ github.event.pull_request.base.ref }}...HEAD > content.txt
37
  else
38
+ EVENT_TYPE="Issue"
39
+ ISSUE_NUMBER="${{ github.event.issue.number }}"
40
+ # 如果是评论触发,获取评论内容作为补充指令
41
+ if [ "${{ github.event_name }}" == "issue_comment" ]; then
42
+ echo "Command: ${{ github.event.comment.body }}" > content.txt
43
+ echo -e "\nOriginal Content:\n${{ github.event.issue.title }}\n${{ github.event.issue.body }}" >> content.txt
44
+ else
45
+ echo -e "Title: ${{ github.event.issue.title }}\n\n${{ github.event.issue.body }}" > content.txt
46
+ fi
47
  fi
48
+ echo "type=$EVENT_TYPE" >> $GITHUB_OUTPUT
49
+ echo "number=$ISSUE_NUMBER" >> $GITHUB_OUTPUT
50
 
51
+ - name: AI Action Execution
52
  uses: actions/github-script@v7
53
  env:
54
  API_KEY: ${{ secrets.OPENAI_API_KEY }}
 
56
  MODEL: ${{ secrets.AI_MODEL }}
57
  EVENT_TYPE: ${{ steps.prep.outputs.type }}
58
  ISSUE_NUMBER: ${{ steps.prep.outputs.number }}
 
59
  with:
60
  script: |
61
  const fs = require('fs');
62
+ const content = fs.readFileSync('content.txt', 'utf8');
 
63
 
64
+ // 1. 获取现有 Labels
65
  const { data: repoLabels } = await github.rest.issues.listLabelsForRepo({
66
  owner: context.repo.owner,
67
  repo: context.repo.repo,
68
  });
69
  const labelNames = repoLabels.map(l => l.name);
70
 
71
+ // 2. 角色设定:从“建议者”转变为“执行者”
72
+ const prompt = `你是一个自动化的仓库助手。你被授权管理该仓库的 Issues 和 PR。
73
+ 你的任务:分析内容,打上标签,并决定是否关闭或保持开启。
74
+
75
+ 仓库现有标签:[${labelNames.join(", ")}]。
76
+
77
+ 输出要求:
78
+ 必须在回复的第一行输出 JSON 格式的操作指令:
79
+ {"labels": ["label1"], "state": "closed" | "open"}
80
+
81
+ 随后另起一行,以助手的身份简洁地说明你执行的操作和理由。不要说“我建议”,要说“我已执行”。
82
+
83
+ 当前环境:${process.env.EVENT_TYPE}
84
+ 内容详情:
85
+ ${content}`;
86
 
87
  try {
88
  const response = await fetch(`${process.env.BASE_URL}/chat/completions`, {
89
  method: 'POST',
90
+ headers: { 'Authorization': `Bearer ${process.env.API_KEY}`, 'Content-Type': 'application/json' },
 
 
 
91
  body: JSON.stringify({
92
  model: process.env.MODEL,
93
  messages: [{ role: "user", content: prompt }],
 
98
  const data = await response.json();
99
  const fullText = data.choices[0].message.content;
100
 
101
+ // 3. 解析指令
102
+ const jsonMatch = fullText.match(/^\{.*?\}/);
103
+ let config = { labels: [], state: "open" };
104
  let commentBody = fullText;
105
+
106
+ if (jsonMatch) {
107
+ config = JSON.parse(jsonMatch[0]);
108
+ commentBody = fullText.replace(jsonMatch[0], "").trim();
 
 
 
 
 
 
 
 
 
109
  }
110
 
111
+ const issueParams = {
 
112
  owner: context.repo.owner,
113
  repo: context.repo.repo,
114
+ issue_number: parseInt(process.env.ISSUE_NUMBER)
115
+ };
 
116
 
117
+ // 4. 执行操作:打标签
118
+ if (config.labels && config.labels.length > 0) {
119
+ await github.rest.issues.addLabels({ ...issueParams, labels: config.labels });
 
 
 
 
 
120
  }
121
+
122
+ // 5. 执行操作:修改状态 (Close/Reopen)
123
+ if (config.state) {
124
+ await github.rest.issues.update({ ...issueParams, state: config.state });
125
+ }
126
+
127
+ // 6. 发布执行报告
128
+ await github.rest.issues.createComment({
129
+ ...issueParams,
130
+ body: `### 🤖 AI Assistant Action\n\n${commentBody}`
131
+ });
132
+
133
  } catch (err) {
134
+ console.error(err);
135
  core.setFailed(err.message);
136
  }
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.github/workflows/ai-review.yaml CHANGED
@@ -1,10 +1,10 @@
1
- name: AI PR & Issue Helper
2
 
3
  on:
4
  pull_request:
5
  types: [opened, synchronize]
6
  issues:
7
- types: [opened] # 当新 Issue 创建时触发
8
 
9
  jobs:
10
  ai-assistant:
@@ -17,37 +17,57 @@ jobs:
17
  steps:
18
  - name: Checkout Code
19
  uses: actions/checkout@v4
20
- with:
21
- fetch-depth: 0
22
 
23
  - name: Prepare Context
24
  id: prep
25
  run: |
26
  if [ "${{ github.event_name }}" == "pull_request" ]; then
27
- git diff origin/${{ github.base_ref }}...origin/${{ github.head_ref }} > input_content.txt
 
28
  echo "type=PR" >> $GITHUB_OUTPUT
 
29
  else
30
- echo "${{ github.event.issue.title }}\n\n${{ github.event.issue.body }}" > input_content.txt
31
  echo "type=Issue" >> $GITHUB_OUTPUT
 
32
  fi
33
 
34
- - name: AI Processing
35
  uses: actions/github-script@v7
36
  env:
37
  API_KEY: ${{ secrets.OPENAI_API_KEY }}
38
  BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
39
  MODEL: ${{ secrets.AI_MODEL }}
40
  EVENT_TYPE: ${{ steps.prep.outputs.type }}
 
 
41
  with:
42
  script: |
43
  const fs = require('fs');
44
  const content = fs.readFileSync('input_content.txt', 'utf8');
45
  if (!content.trim()) return;
46
 
47
- // 根据类型定制 Prompt
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  const prompt = process.env.EVENT_TYPE === 'PR'
49
- ? `你是一位资深工程师。请审查以下 PR 的代码改动,指出潜在 Bug 和改进点:\n\n${content}`
50
- : `你是一位开源项目维护者。请分析以下 Issue 并给出建议或解决方案:\n\n${content}`;
51
 
52
  try {
53
  const response = await fetch(`${process.env.BASE_URL}/chat/completions`, {
@@ -59,27 +79,45 @@ jobs:
59
  body: JSON.stringify({
60
  model: process.env.MODEL,
61
  messages: [{ role: "user", content: prompt }],
62
- temperature: 0.3
63
  })
64
  });
65
 
66
  const data = await response.json();
67
- const result = data.choices[0].message.content;
68
- const header = process.env.EVENT_TYPE === 'PR' ? "### 🤖 AI Code Review" : "### 🤖 AI Issue Assistant";
69
 
70
- if (process.env.EVENT_TYPE === 'PR') {
71
- await github.rest.issues.createComment({
72
- owner: context.repo.owner,
73
- repo: context.repo.repo,
74
- issue_number: context.payload.pull_request.number,
75
- body: `${header}\n\n${result}`
76
- });
77
- } else {
78
- await github.rest.issues.createComment({
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  owner: context.repo.owner,
80
  repo: context.repo.repo,
81
- issue_number: context.payload.issue.number,
82
- body: `${header}\n\n${result}`
83
  });
84
  }
85
  } catch (err) {
 
1
+ name: AI PR & Issue Assistant with Auto-Label
2
 
3
  on:
4
  pull_request:
5
  types: [opened, synchronize]
6
  issues:
7
+ types: [opened]
8
 
9
  jobs:
10
  ai-assistant:
 
17
  steps:
18
  - name: Checkout Code
19
  uses: actions/checkout@v4
 
 
20
 
21
  - name: Prepare Context
22
  id: prep
23
  run: |
24
  if [ "${{ github.event_name }}" == "pull_request" ]; then
25
+ git fetch origin ${{ github.base_ref }}
26
+ git diff origin/${{ github.base_ref }}...HEAD > input_content.txt
27
  echo "type=PR" >> $GITHUB_OUTPUT
28
+ echo "number=${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT
29
  else
30
+ echo -e "Title: ${{ github.event.issue.title }}\n\n${{ github.event.issue.body }}" > input_content.txt
31
  echo "type=Issue" >> $GITHUB_OUTPUT
32
+ echo "number=${{ github.event.issue.number }}" >> $GITHUB_OUTPUT
33
  fi
34
 
35
+ - name: AI Processing & Labeling
36
  uses: actions/github-script@v7
37
  env:
38
  API_KEY: ${{ secrets.OPENAI_API_KEY }}
39
  BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
40
  MODEL: ${{ secrets.AI_MODEL }}
41
  EVENT_TYPE: ${{ steps.prep.outputs.type }}
42
+ ISSUE_NUMBER: ${{ steps.prep.outputs.number }}
43
+ ENABLE_AUTO_LABEL: "true" # 设置为 "false" 则彻底关闭自动打标签功能
44
  with:
45
  script: |
46
  const fs = require('fs');
47
  const content = fs.readFileSync('input_content.txt', 'utf8');
48
  if (!content.trim()) return;
49
 
50
+ // 1. 获取仓库现有的所有 Labels
51
+ const { data: repoLabels } = await github.rest.issues.listLabelsForRepo({
52
+ owner: context.repo.owner,
53
+ repo: context.repo.repo,
54
+ });
55
+ const labelNames = repoLabels.map(l => l.name);
56
+
57
+ // 2. 构建针对标签的 Prompt
58
+ let labelInstruction = "";
59
+ if (process.env.ENABLE_AUTO_LABEL === "true") {
60
+ labelInstruction = `
61
+ 请从以下现有的标签列表中选择最合适的标签(可多选),仅从列表中选择:[${labelNames.join(", ")}]。
62
+ 如果你认为不需要打标签,请返回空数组。
63
+ 请在回复的最开头以 JSON 格式输出你的选择,格式如下:
64
+ {"suggested_labels": ["label1", "label2"]}
65
+ 然后另起一行开始你的评论分析。`;
66
+ }
67
+
68
  const prompt = process.env.EVENT_TYPE === 'PR'
69
+ ? `你是一位资深工程师。请审查以下 PR 的代码改动,指出潜在 Bug 和改进点。${labelInstruction}\n\nDiff内容:\n${content}`
70
+ : `你是一位开源项目维护者。请分析以下 Issue 并给出建议。${labelInstruction}\n\nIssue内容:\n${content}`;
71
 
72
  try {
73
  const response = await fetch(`${process.env.BASE_URL}/chat/completions`, {
 
79
  body: JSON.stringify({
80
  model: process.env.MODEL,
81
  messages: [{ role: "user", content: prompt }],
82
+ temperature: 0.2
83
  })
84
  });
85
 
86
  const data = await response.json();
87
+ const fullText = data.choices[0].message.content;
 
88
 
89
+ // 3. 解析标签和正文
90
+ let commentBody = fullText;
91
+ let labelsToAdd = [];
92
+
93
+ if (process.env.ENABLE_AUTO_LABEL === "true") {
94
+ const jsonMatch = fullText.match(/^\{.*?\}/); // 匹配开头的 JSON
95
+ if (jsonMatch) {
96
+ try {
97
+ const labelData = JSON.parse(jsonMatch[0]);
98
+ labelsToAdd = labelData.suggested_labels || [];
99
+ commentBody = fullText.replace(jsonMatch[0], "").trim();
100
+ } catch (e) {
101
+ console.log("Failed to parse labels JSON");
102
+ }
103
+ }
104
+ }
105
+
106
+ // 4. 发布评论
107
+ await github.rest.issues.createComment({
108
+ owner: context.repo.owner,
109
+ repo: context.repo.repo,
110
+ issue_number: parseInt(process.env.ISSUE_NUMBER),
111
+ body: `### 🤖 AI ${process.env.EVENT_TYPE === 'PR' ? 'Review' : 'Assistant'}\n\n${commentBody}`
112
+ });
113
+
114
+ // 5. 自动打标签(如果有建议的标签)
115
+ if (labelsToAdd.length > 0) {
116
+ await github.rest.issues.addLabels({
117
  owner: context.repo.owner,
118
  repo: context.repo.repo,
119
+ issue_number: parseInt(process.env.ISSUE_NUMBER),
120
+ labels: labelsToAdd
121
  });
122
  }
123
  } catch (err) {
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.github/workflows/ai-review.yaml ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: AI PR & Issue Helper
2
+
3
+ on:
4
+ pull_request:
5
+ types: [opened, synchronize]
6
+ issues:
7
+ types: [opened] # 当新 Issue 创建时触发
8
+
9
+ jobs:
10
+ ai-assistant:
11
+ runs-on: ubuntu-latest
12
+ permissions:
13
+ contents: read
14
+ pull-requests: write
15
+ issues: write
16
+
17
+ steps:
18
+ - name: Checkout Code
19
+ uses: actions/checkout@v4
20
+ with:
21
+ fetch-depth: 0
22
+
23
+ - name: Prepare Context
24
+ id: prep
25
+ run: |
26
+ if [ "${{ github.event_name }}" == "pull_request" ]; then
27
+ git diff origin/${{ github.base_ref }}...origin/${{ github.head_ref }} > input_content.txt
28
+ echo "type=PR" >> $GITHUB_OUTPUT
29
+ else
30
+ echo "${{ github.event.issue.title }}\n\n${{ github.event.issue.body }}" > input_content.txt
31
+ echo "type=Issue" >> $GITHUB_OUTPUT
32
+ fi
33
+
34
+ - name: AI Processing
35
+ uses: actions/github-script@v7
36
+ env:
37
+ API_KEY: ${{ secrets.OPENAI_API_KEY }}
38
+ BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
39
+ MODEL: ${{ secrets.AI_MODEL }}
40
+ EVENT_TYPE: ${{ steps.prep.outputs.type }}
41
+ with:
42
+ script: |
43
+ const fs = require('fs');
44
+ const content = fs.readFileSync('input_content.txt', 'utf8');
45
+ if (!content.trim()) return;
46
+
47
+ // 根据类型定制 Prompt
48
+ const prompt = process.env.EVENT_TYPE === 'PR'
49
+ ? `你是一位资深工程师。请审查以下 PR 的代码改动,指出潜在 Bug 和改进点:\n\n${content}`
50
+ : `你是一位开源项目维护者。请分析以下 Issue 并给出建议或解决方案:\n\n${content}`;
51
+
52
+ try {
53
+ const response = await fetch(`${process.env.BASE_URL}/chat/completions`, {
54
+ method: 'POST',
55
+ headers: {
56
+ 'Authorization': `Bearer ${process.env.API_KEY}`,
57
+ 'Content-Type': 'application/json'
58
+ },
59
+ body: JSON.stringify({
60
+ model: process.env.MODEL,
61
+ messages: [{ role: "user", content: prompt }],
62
+ temperature: 0.3
63
+ })
64
+ });
65
+
66
+ const data = await response.json();
67
+ const result = data.choices[0].message.content;
68
+ const header = process.env.EVENT_TYPE === 'PR' ? "### 🤖 AI Code Review" : "### 🤖 AI Issue Assistant";
69
+
70
+ if (process.env.EVENT_TYPE === 'PR') {
71
+ await github.rest.issues.createComment({
72
+ owner: context.repo.owner,
73
+ repo: context.repo.repo,
74
+ issue_number: context.payload.pull_request.number,
75
+ body: `${header}\n\n${result}`
76
+ });
77
+ } else {
78
+ await github.rest.issues.createComment({
79
+ owner: context.repo.owner,
80
+ repo: context.repo.repo,
81
+ issue_number: context.payload.issue.number,
82
+ body: `${header}\n\n${result}`
83
+ });
84
+ }
85
+ } catch (err) {
86
+ core.setFailed(err.message);
87
+ }
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.github/workflows/push_hf.yaml ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Sync to Hugging Face (Exclude README)
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ workflow_dispatch:
7
+
8
+ jobs:
9
+ sync-to-hub:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - name: Checkout GitHub Repository
13
+ uses: actions/checkout@v4
14
+ with:
15
+ fetch-depth: 0
16
+ lfs: true
17
+
18
+ - name: Sync and Push to HF
19
+ env:
20
+ HF_TOKEN: ${{ secrets.HF_TOKEN }}
21
+ run: |
22
+ # 1. 配置 Git 用户信息
23
+ git config --global user.email "actions@github.com"
24
+ git config --global user.name "GitHub Actions"
25
+
26
+ # 2. 克隆 Hugging Face 仓库到临时目录 'hf_repo'
27
+ # 注意:请将 <USERNAME>/<REPO_NAME> 替换为你的路径
28
+ git clone https://x-access-token:$HF_TOKEN@huggingface.co/spaces/StarrySkyWorld/InterConnectServer hf_repo
29
+
30
+ # 3. 使用 rsync 同步文件
31
+ # --exclude='.git/' : 不同步 git 历史
32
+ # --exclude='README.md' : 不同步 README 文件
33
+ # -av : 归档模式并显示详细过程
34
+ # --delete : 如果 GitHub 删除了某文件,HF 端也相应删除(README 除外)
35
+ rsync -av --exclude='.git/' --exclude='README.md' ./ hf_repo/
36
+
37
+ # 4. 进入临时目录提交并推送
38
+ cd hf_repo
39
+ git add .
40
+
41
+ # 检查是否有内容变化,防止空提交导致报错
42
+ if [ -n "$(git status --porcelain)" ]; then
43
+ git commit -m "Sync from GitHub (excluding README)"
44
+ git push origin main
45
+ else
46
+ echo "No changes to sync."
47
+ fi
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.gitignore ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dependencies
2
+ node_modules/
3
+
4
+ # Environment variables
5
+ .env
6
+ .env.local
7
+ .env.*.local
8
+
9
+ # Database files
10
+ *.db
11
+ *.sqlite
12
+ *.sqlite3
13
+
14
+ # Logs
15
+ npm-debug.log*
16
+ yarn-debug.log*
17
+ yarn-error.log*
18
+ pnpm-debug.log*
19
+ lerna-debug.log*
20
+
21
+ # Build files
22
+ dist/
23
+ build/
24
+
25
+ # IDE
26
+ .idea/
27
+ .vscode/
28
+ *.swp
29
+ *.swo
30
+ *~
31
+ .trae/
32
+
33
+ # OS
34
+ .DS_Store
35
+ Thumbs.db
36
+
37
+ # Testing
38
+ coverage/
39
+ .nyc_output/
40
+
41
+ # Misc
42
+ *.tgz
43
+ .cache/
44
+ .parcel-cache/
45
+ .next/
46
+ .nuxt/
47
+ .vuepress/dist/
48
+ .serverless/
49
+ .fusebox/
50
+ .dynamodb/
51
+
52
+ # CLI
53
+ cli/*.log
54
+
55
+ # Docker
56
+ .dockerignore
57
+ *.dockerfile
58
+
59
+
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/Dockerfile ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:18-alpine
2
+
3
+ WORKDIR /app
4
+
5
+ COPY package*.json ./
6
+
7
+ RUN npm install --production
8
+
9
+ COPY . .
10
+
11
+ EXPOSE 8000
12
+
13
+ ENV SERVER_HOST=0.0.0.0
14
+ ENV SERVER_PORT=8000
15
+
16
+ CMD ["node", "src/server.js"]
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2026 BeiChen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/cli/cli.js ADDED
@@ -0,0 +1,389 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env node
2
+ const { Command } = require('commander');
3
+ const http = require('http');
4
+ const https = require('https');
5
+ const fs = require('fs');
6
+ require('dotenv').config();
7
+
8
+ const HARDCODED_API_URL = 'http://localhost:8000';
9
+ const HARDCODED_ADMIN_KEY = process.env.ADMIN_KEY || null;
10
+
11
+ class MinecraftWSCLIClient {
12
+ constructor(serverUrl, adminKey) {
13
+ this.serverUrl = serverUrl.replace(/\/$/, '');
14
+ this.effectiveAdminKey = adminKey || HARDCODED_ADMIN_KEY;
15
+ }
16
+
17
+ async request(method, endpoint, data = null) {
18
+ return new Promise((resolve, reject) => {
19
+ const url = new URL(endpoint, this.serverUrl);
20
+ const isHttps = url.protocol === 'https:';
21
+ const lib = isHttps ? https : http;
22
+
23
+ const options = {
24
+ hostname: url.hostname,
25
+ port: url.port || (isHttps ? 443 : 80),
26
+ path: url.pathname + url.search,
27
+ method: method,
28
+ headers: {
29
+ 'Content-Type': 'application/json'
30
+ }
31
+ };
32
+
33
+ if (this.effectiveAdminKey) {
34
+ options.headers['Authorization'] = `Bearer ${this.effectiveAdminKey}`;
35
+ }
36
+
37
+ const req = lib.request(options, (res) => {
38
+ let body = '';
39
+ res.on('data', chunk => body += chunk);
40
+ res.on('end', () => {
41
+ if (res.statusCode >= 200 && res.statusCode < 300) {
42
+ if (res.statusCode === 204 || !body) {
43
+ resolve(null);
44
+ } else {
45
+ try {
46
+ resolve(JSON.parse(body));
47
+ } catch {
48
+ resolve(body);
49
+ }
50
+ }
51
+ } else {
52
+ let detail = body;
53
+ try {
54
+ const json = JSON.parse(body);
55
+ detail = json.detail || body;
56
+ } catch {}
57
+ reject(new Error(`API请求失败 (${res.statusCode}): ${detail}`));
58
+ }
59
+ });
60
+ });
61
+
62
+ req.on('error', (e) => reject(new Error(`网络请求失败: ${e.message}`)));
63
+
64
+ if (data) {
65
+ req.write(JSON.stringify(data));
66
+ }
67
+ req.end();
68
+ });
69
+ }
70
+
71
+ ensureAdminKeyForManagement() {
72
+ if (!this.effectiveAdminKey) {
73
+ throw new Error('此管理操作需要Admin Key。请通过--admin-key选项或ADMIN_KEY环境变量提供。');
74
+ }
75
+ }
76
+
77
+ async createApiKey(name, description = '', keyType = 'regular', serverId = null) {
78
+ this.ensureAdminKeyForManagement();
79
+ return await this.request('POST', '/manage/keys', {
80
+ name,
81
+ description,
82
+ key_type: keyType,
83
+ server_id: serverId
84
+ });
85
+ }
86
+
87
+ async listApiKeys() {
88
+ this.ensureAdminKeyForManagement();
89
+ return await this.request('GET', '/manage/keys');
90
+ }
91
+
92
+ async getApiKeyDetails(keyId) {
93
+ this.ensureAdminKeyForManagement();
94
+ return await this.request('GET', `/manage/keys/${keyId}`);
95
+ }
96
+
97
+ async activateApiKey(keyId) {
98
+ this.ensureAdminKeyForManagement();
99
+ return await this.request('PATCH', `/manage/keys/${keyId}/activate`);
100
+ }
101
+
102
+ async deactivateApiKey(keyId) {
103
+ this.ensureAdminKeyForManagement();
104
+ return await this.request('PATCH', `/manage/keys/${keyId}/deactivate`);
105
+ }
106
+
107
+ async deleteApiKey(keyId) {
108
+ this.ensureAdminKeyForManagement();
109
+ await this.request('DELETE', `/manage/keys/${keyId}`);
110
+ return true;
111
+ }
112
+
113
+ async healthCheck() {
114
+ return await this.request('GET', '/health');
115
+ }
116
+ }
117
+
118
+ const program = new Command();
119
+
120
+ program
121
+ .name('minecraft-ws-cli')
122
+ .description('Minecraft WebSocket API CLI - 管理API密钥和服务器')
123
+ .version('1.0.0')
124
+ .option('-s, --server-url <url>', 'API服务器URL', process.env.MC_WS_API_URL || HARDCODED_API_URL)
125
+ .option('-k, --admin-key <key>', '用于管理操作的Admin Key', process.env.ADMIN_KEY || HARDCODED_ADMIN_KEY);
126
+
127
+ program
128
+ .command('create-key <name>')
129
+ .description('创建新的API密钥')
130
+ .option('-d, --description <desc>', 'API密钥描述', '')
131
+ .option('-t, --type <type>', '密钥类型: admin, server, regular', 'regular')
132
+ .option('--server-id <id>', '关联的服务器ID(仅server类型需要)')
133
+ .action(async (name, options) => {
134
+ try {
135
+ const opts = program.opts();
136
+ const client = new MinecraftWSCLIClient(opts.serverUrl, opts.adminKey);
137
+ const result = await client.createApiKey(name, options.description, options.type, options.serverId);
138
+
139
+ const keyTypeNames = { admin: 'Admin Key', server: 'Server Key', regular: '普通Key' };
140
+
141
+ if (result.regularKey && result.serverKey) {
142
+ // 返回了Regular Key和关联的Server Key
143
+ console.log(`\x1b[32m✅ 成功创建Regular Key和关联的Server Key!\x1b[0m`);
144
+ console.log('='.repeat(60));
145
+
146
+ // 显示Regular Key
147
+ console.log(`\x1b[34m🔑 Regular Key:${keyTypeNames[result.regularKey.keyType]}\x1b[0m`);
148
+ console.log(` ID: ${result.regularKey.id}`);
149
+ console.log(` 名称: ${result.regularKey.name}`);
150
+ console.log(` 描述: ${result.regularKey.description || '无'}`);
151
+ console.log(` 前缀: ${result.regularKey.keyPrefix}`);
152
+ if (result.regularKey.serverId) {
153
+ console.log(` 服务器ID: ${result.regularKey.serverId}`);
154
+ }
155
+ console.log(` 创建时间: ${result.regularKey.createdAt}`);
156
+ console.log(`\x1b[33m🔑 原始密钥 (请妥善保存,仅显示一次):\x1b[0m`);
157
+ console.log(`\x1b[31m ${result.regularKey.key}\x1b[0m`);
158
+ console.log();
159
+
160
+ // 显示关联的Server Key
161
+ console.log(`\x1b[34m🖥️ Server Key:${keyTypeNames[result.serverKey.keyType]}\x1b[0m`);
162
+ console.log(` ID: ${result.serverKey.id}`);
163
+ console.log(` 名称: ${result.serverKey.name}`);
164
+ console.log(` 描述: ${result.serverKey.description || '无'}`);
165
+ console.log(` 前缀: ${result.serverKey.keyPrefix}`);
166
+ if (result.serverKey.serverId) {
167
+ console.log(` 服务器ID: ${result.serverKey.serverId}`);
168
+ }
169
+ console.log(` 创建时间: ${result.serverKey.createdAt}`);
170
+ console.log(`\x1b[33m🔑 原始密钥 (请妥善保存,仅显示一次):\x1b[0m`);
171
+ console.log(`\x1b[31m ${result.serverKey.key}\x1b[0m`);
172
+ console.log('='.repeat(60));
173
+ console.log();
174
+ console.log('使用示例:');
175
+ console.log(` Regular Key登录控制面板: http://localhost:8000/dashboard`);
176
+ console.log(` Server Key用于插件配置: 放入Minecraft插件的配置文件中`);
177
+ } else {
178
+ // 返回了单个密钥
179
+ const keyType = keyTypeNames[result.keyType] || result.keyType;
180
+
181
+ console.log(`\x1b[32m✅ ${keyType}创建成功!\x1b[0m`);
182
+ console.log(` ID: ${result.id}`);
183
+ console.log(` 名称: ${result.name}`);
184
+ console.log(` 类型: ${keyType}`);
185
+ console.log(` 前缀: ${result.keyPrefix}`);
186
+ if (result.serverId) {
187
+ console.log(` 服务器ID: ${result.serverId}`);
188
+ }
189
+ console.log(` 创建时间: ${result.createdAt}`);
190
+ console.log(`\x1b[33m🔑 原始密钥 (请妥善保存,仅显示一次):\x1b[0m`);
191
+ console.log(`\x1b[31m ${result.key}\x1b[0m`);
192
+ console.log();
193
+ console.log('使用示例:');
194
+ console.log(` WebSocket连接: ws://localhost:8000/ws?api_key=${result.key}`);
195
+ console.log(` HTTP请求头: Authorization: Bearer ${result.key}`);
196
+ }
197
+ } catch (error) {
198
+ console.error(`\x1b[31m❌ 错误: ${error.message}\x1b[0m`);
199
+ process.exit(1);
200
+ }
201
+ });
202
+
203
+ program
204
+ .command('list-keys')
205
+ .description('列出所有API密钥')
206
+ .action(async () => {
207
+ try {
208
+ const opts = program.opts();
209
+ const client = new MinecraftWSCLIClient(opts.serverUrl, opts.adminKey);
210
+ const keys = await client.listApiKeys();
211
+
212
+ if (keys.length === 0) {
213
+ console.log('📭 没有找到API密钥.');
214
+ return;
215
+ }
216
+
217
+ console.log(`\x1b[34m📋 API密钥列表 (共 ${keys.length} 个):\x1b[0m`);
218
+
219
+ const keyTypeIcons = { admin: '👑', server: '🖥️', regular: '🔑' };
220
+ const keyTypeNames = { admin: 'Admin', server: 'Server', regular: 'Regular' };
221
+
222
+ for (const key of keys) {
223
+ const status = key.isActive ? '\x1b[32m🟢 活跃\x1b[0m' : '\x1b[31m🔴 已停用\x1b[0m';
224
+ const icon = keyTypeIcons[key.keyType] || '🔑';
225
+ const typeName = keyTypeNames[key.keyType] || key.keyType;
226
+ const lastUsed = key.lastUsed || '从未使用';
227
+
228
+ console.log('-'.repeat(50));
229
+ console.log(` ID : ${key.id}`);
230
+ console.log(` 名称 : ${key.name}`);
231
+ console.log(` 类型 : ${icon} ${typeName}`);
232
+ console.log(` 状态 : ${status}`);
233
+ console.log(` 前缀 : ${key.keyPrefix}`);
234
+ if (key.serverId) {
235
+ console.log(` 服务器ID : ${key.serverId}`);
236
+ }
237
+ console.log(` 创建时间 : ${key.createdAt}`);
238
+ console.log(` 最后使用 : ${lastUsed}`);
239
+ }
240
+ console.log('-'.repeat(50));
241
+ } catch (error) {
242
+ console.error(`\x1b[31m❌ 错误: ${error.message}\x1b[0m`);
243
+ process.exit(1);
244
+ }
245
+ });
246
+
247
+ program
248
+ .command('get-key <key_id>')
249
+ .description('获取特定API密钥的详细信息')
250
+ .action(async (keyId) => {
251
+ try {
252
+ const opts = program.opts();
253
+ const client = new MinecraftWSCLIClient(opts.serverUrl, opts.adminKey);
254
+ const key = await client.getApiKeyDetails(keyId);
255
+
256
+ const keyTypeIcons = { admin: '👑', server: '🖥️', regular: '🔑' };
257
+ const keyTypeNames = { admin: 'Admin', server: 'Server', regular: 'Regular' };
258
+
259
+ const status = key.isActive ? '\x1b[32m🟢 活跃\x1b[0m' : '\x1b[31m🔴 已停用\x1b[0m';
260
+ const icon = keyTypeIcons[key.keyType] || '🔑';
261
+ const typeName = keyTypeNames[key.keyType] || key.keyType;
262
+ const lastUsed = key.lastUsed || '从未使用';
263
+
264
+ console.log(`\x1b[34m📄 密钥详情 (ID: ${key.id}):\x1b[0m`);
265
+ console.log(` 名称 : ${key.name}`);
266
+ console.log(` 描述 : ${key.description || '无'}`);
267
+ console.log(` 类型 : ${icon} ${typeName}`);
268
+ console.log(` 状态 : ${status}`);
269
+ console.log(` 前缀 : ${key.keyPrefix}`);
270
+ if (key.serverId) {
271
+ console.log(` 服务器ID : ${key.serverId}`);
272
+ }
273
+ console.log(` 创建时间 : ${key.createdAt}`);
274
+ console.log(` 最后使用 : ${lastUsed}`);
275
+ } catch (error) {
276
+ console.error(`\x1b[31m❌ 错误: ${error.message}\x1b[0m`);
277
+ process.exit(1);
278
+ }
279
+ });
280
+
281
+ program
282
+ .command('activate-key <key_id>')
283
+ .description('激活指定的API密钥')
284
+ .action(async (keyId) => {
285
+ try {
286
+ const opts = program.opts();
287
+ const client = new MinecraftWSCLIClient(opts.serverUrl, opts.adminKey);
288
+ const result = await client.activateApiKey(keyId);
289
+ console.log(`\x1b[32m✅ ${result.message}\x1b[0m`);
290
+ } catch (error) {
291
+ console.error(`\x1b[31m❌ 错误: ${error.message}\x1b[0m`);
292
+ process.exit(1);
293
+ }
294
+ });
295
+
296
+ program
297
+ .command('deactivate-key <key_id>')
298
+ .description('停用指定的API密钥')
299
+ .action(async (keyId) => {
300
+ try {
301
+ const opts = program.opts();
302
+ const client = new MinecraftWSCLIClient(opts.serverUrl, opts.adminKey);
303
+ const result = await client.deactivateApiKey(keyId);
304
+ console.log(`\x1b[32m✅ ${result.message}\x1b[0m`);
305
+ } catch (error) {
306
+ console.error(`\x1b[31m❌ 错误: ${error.message}\x1b[0m`);
307
+ process.exit(1);
308
+ }
309
+ });
310
+
311
+ program
312
+ .command('delete-key <key_id>')
313
+ .description('永久删除指定的API密钥')
314
+ .action(async (keyId) => {
315
+ try {
316
+ const opts = program.opts();
317
+ const client = new MinecraftWSCLIClient(opts.serverUrl, opts.adminKey);
318
+ await client.deleteApiKey(keyId);
319
+ console.log(`\x1b[32m✅ API密钥 ${keyId} 已成功删除!\x1b[0m`);
320
+ } catch (error) {
321
+ console.error(`\x1b[31m❌ 错误: ${error.message}\x1b[0m`);
322
+ process.exit(1);
323
+ }
324
+ });
325
+
326
+ program
327
+ .command('health')
328
+ .description('检查服务器健康状态')
329
+ .action(async () => {
330
+ try {
331
+ const opts = program.opts();
332
+ const client = new MinecraftWSCLIClient(opts.serverUrl, opts.adminKey);
333
+ const result = await client.healthCheck();
334
+
335
+ console.log('\x1b[34m🏥 服务器健康状态:\x1b[0m');
336
+ const statusIcon = result.status === 'healthy' ? '🟢' : '🔴';
337
+ console.log(` 状态 : ${statusIcon} ${result.status}`);
338
+ console.log(` 时间戳 : ${result.timestamp}`);
339
+ console.log(` 活跃WS连接数 : ${result.active_ws}`);
340
+ console.log(` 总密钥数 : ${result.keys_total}`);
341
+ console.log(` 活跃Admin Keys: ${result.admin_active}`);
342
+ console.log(` 活跃Server Keys: ${result.server_active}`);
343
+ console.log(` 活跃Regular Keys: ${result.regular_active}`);
344
+
345
+ if (result.status === 'healthy') {
346
+ console.log('\x1b[32m✅ 服务器运行正常\x1b[0m');
347
+ } else {
348
+ console.log('\x1b[33m⚠️ 服务器状态异常\x1b[0m');
349
+ }
350
+ } catch (error) {
351
+ console.error(`\x1b[31m❌ 服务器连接或检查失败: ${error.message}\x1b[0m`);
352
+ process.exit(1);
353
+ }
354
+ });
355
+
356
+ program
357
+ .command('generate-config [filename]')
358
+ .description('为Minecraft插件生成配置文件模板')
359
+ .action(async (filename = 'mc_ws_plugin_config.json') => {
360
+ try {
361
+ const opts = program.opts();
362
+ const wsUrl = opts.serverUrl.replace('http://', 'ws://').replace('https://', 'wss://');
363
+
364
+ const configTemplate = {
365
+ websocket_settings: {
366
+ server_address: `${wsUrl}/ws`,
367
+ reconnect_delay_seconds: 10,
368
+ ping_interval_seconds: 30
369
+ },
370
+ api_key: 'PASTE_YOUR_GENERATED_API_KEY_HERE',
371
+ server_identifier: 'MyMinecraftServer_1',
372
+ log_level: 'INFO',
373
+ enabled_events: {
374
+ player_join: true,
375
+ player_quit: true,
376
+ player_chat: false,
377
+ player_death: true
378
+ }
379
+ };
380
+
381
+ fs.writeFileSync(filename, JSON.stringify(configTemplate, null, 4));
382
+ console.log(`\x1b[32m✅ 插件配置文件模板已生成: ${filename}\x1b[0m`);
383
+ } catch (error) {
384
+ console.error(`\x1b[31m❌ 生成配置文件失败: ${error.message}\x1b[0m`);
385
+ process.exit(1);
386
+ }
387
+ });
388
+
389
+ program.parse();
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/dashboard/public/app.js ADDED
@@ -0,0 +1,444 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const API_URL = window.location.origin;
2
+ let superKey = null;
3
+ let ws = null;
4
+
5
+ const loginScreen = document.getElementById('login-screen');
6
+ const dashboardScreen = document.getElementById('dashboard-screen');
7
+ const loginForm = document.getElementById('login-form');
8
+ const loginError = document.getElementById('login-error');
9
+ const logoutBtn = document.getElementById('logout-btn');
10
+ const createKeyBtn = document.getElementById('create-key-btn');
11
+ const createKeyModal = document.getElementById('create-key-modal');
12
+ const createKeyForm = document.getElementById('create-key-form');
13
+ const cancelCreateBtn = document.getElementById('cancel-create-btn');
14
+ const keyDetailsModal = document.getElementById('key-details-modal');
15
+ const closeDetailsBtn = document.getElementById('close-details-btn');
16
+
17
+ loginForm.addEventListener('submit', async (e) => {
18
+ e.preventDefault();
19
+ const key = document.getElementById('super-key-input').value;
20
+
21
+ try {
22
+ // 验证密钥是否有效
23
+ const response = await fetch(`${API_URL}/health`, {
24
+ headers: { 'Authorization': `Bearer ${key}` }
25
+ });
26
+
27
+ if (response.ok) {
28
+ superKey = key;
29
+
30
+ // 验证密钥类型
31
+ let userKeyType = null;
32
+ let userServerId = null;
33
+
34
+ // 尝试Admin Key验证
35
+ const adminResponse = await fetch(`${API_URL}/manage/keys`, {
36
+ headers: { 'Authorization': `Bearer ${key}` }
37
+ });
38
+
39
+ if (adminResponse.ok) {
40
+ userKeyType = 'admin';
41
+ } else {
42
+ // 尝试Server Key验证
43
+ const serverResponse = await fetch(`${API_URL}/api/server/info`, {
44
+ headers: { 'Authorization': `Bearer ${key}` }
45
+ });
46
+
47
+ if (serverResponse.ok) {
48
+ userKeyType = 'server';
49
+ const serverData = await serverResponse.json();
50
+ userServerId = serverData.server_id;
51
+ } else {
52
+ // 尝试Regular Key验证(获取自己的Server Key列表)
53
+ const regularResponse = await fetch(`${API_URL}/manage/keys/server-keys`, {
54
+ headers: { 'Authorization': `Bearer ${key}` }
55
+ });
56
+
57
+ if (regularResponse.ok) {
58
+ userKeyType = 'regular';
59
+ } else {
60
+ throw new Error('无效的密钥或权限不足');
61
+ }
62
+ }
63
+ }
64
+
65
+ loginError.textContent = '';
66
+ showDashboard(userKeyType, userServerId);
67
+ } else {
68
+ loginError.textContent = '无效的密钥';
69
+ }
70
+ } catch (error) {
71
+ loginError.textContent = error.message || '无法连接到服务器';
72
+ }
73
+ });
74
+
75
+ logoutBtn.addEventListener('click', () => {
76
+ superKey = null;
77
+ if (ws) {
78
+ ws.close();
79
+ ws = null;
80
+ }
81
+ loginScreen.classList.remove('hidden');
82
+ dashboardScreen.classList.add('hidden');
83
+ });
84
+
85
+ createKeyBtn.addEventListener('click', () => {
86
+ createKeyModal.classList.remove('hidden');
87
+ });
88
+
89
+ cancelCreateBtn.addEventListener('click', () => {
90
+ createKeyModal.classList.add('hidden');
91
+ createKeyForm.reset();
92
+ });
93
+
94
+ closeDetailsBtn.addEventListener('click', () => {
95
+ keyDetailsModal.classList.add('hidden');
96
+ });
97
+
98
+ createKeyForm.addEventListener('submit', async (e) => {
99
+ e.preventDefault();
100
+
101
+ const name = document.getElementById('key-name').value;
102
+ const description = document.getElementById('key-description').value;
103
+ const isSuper = document.getElementById('key-is-super').checked;
104
+
105
+ try {
106
+ const response = await fetch(`${API_URL}/manage/keys`, {
107
+ method: 'POST',
108
+ headers: {
109
+ 'Authorization': `Bearer ${superKey}`,
110
+ 'Content-Type': 'application/json'
111
+ },
112
+ body: JSON.stringify({
113
+ name,
114
+ description,
115
+ is_super_key: isSuper
116
+ })
117
+ });
118
+
119
+ if (response.ok) {
120
+ const result = await response.json();
121
+ createKeyModal.classList.add('hidden');
122
+ createKeyForm.reset();
123
+
124
+ showKeyCreatedModal(result);
125
+ loadKeys();
126
+ } else {
127
+ const error = await response.json();
128
+ alert('创建失败: ' + error.detail);
129
+ }
130
+ } catch (error) {
131
+ alert('创建失败: ' + error.message);
132
+ }
133
+ });
134
+
135
+ function showKeyCreatedModal(keyData) {
136
+ const content = `
137
+ <p><strong>密钥创建成功!</strong></p>
138
+ <p>名称: ${keyData.name}</p>
139
+ <p>类型: ${keyData.isSuperKey ? 'SuperKey' : '普通密钥'}</p>
140
+ <div class="key-display">
141
+ <strong>⚠️ 请立即复制并保存此密钥(仅显示一次):</strong><br>
142
+ ${keyData.key}
143
+ </div>
144
+ `;
145
+
146
+ document.getElementById('key-details-content').innerHTML = content;
147
+ keyDetailsModal.classList.remove('hidden');
148
+ }
149
+
150
+ // 全局变量
151
+ let currentUserKeyType = null;
152
+ let currentUserServerId = null;
153
+
154
+ async function showDashboard(userKeyType, userServerId) {
155
+ currentUserKeyType = userKeyType;
156
+ currentUserServerId = userServerId;
157
+
158
+ loginScreen.classList.add('hidden');
159
+ dashboardScreen.classList.remove('hidden');
160
+
161
+ // 显示用户信息
162
+ const userInfo = document.getElementById('user-info');
163
+ const keyTypeIcons = { admin: '👑', server: '🖥️', regular: '🔑' };
164
+ const keyTypeNames = { admin: 'Admin', server: 'Server', regular: 'Regular' };
165
+ userInfo.textContent = `${keyTypeIcons[userKeyType]} ${keyTypeNames[userKeyType]} 用户`;
166
+
167
+ // 根据权限显示/隐藏功能
168
+ const adminSection = document.getElementById('admin-section');
169
+ const serverSection = document.getElementById('server-section');
170
+
171
+ if (userKeyType === 'admin') {
172
+ adminSection.style.display = 'block';
173
+ serverSection.style.display = 'block';
174
+ loadKeys();
175
+ } else if (userKeyType === 'server') {
176
+ adminSection.style.display = 'none';
177
+ serverSection.style.display = 'block';
178
+ loadServerInfo();
179
+ } else if (userKeyType === 'regular') {
180
+ adminSection.style.display = 'none';
181
+ serverSection.style.display = 'block';
182
+ loadRegularServerKeys();
183
+ }
184
+
185
+ // 根据用户类型控制创建密钥按钮的显示
186
+ const createKeyBtn = document.getElementById('create-key-btn');
187
+ if (createKeyBtn) {
188
+ createKeyBtn.style.display = userKeyType === 'admin' ? 'block' : 'none';
189
+ }
190
+
191
+ loadStats();
192
+ connectWebSocket();
193
+
194
+ setInterval(loadStats, 5000);
195
+ }
196
+
197
+ async function loadStats() {
198
+ try {
199
+ const response = await fetch(`${API_URL}/health`);
200
+ const data = await response.json();
201
+
202
+ document.getElementById('stat-connections').textContent = data.active_ws || 0;
203
+ document.getElementById('stat-total-keys').textContent = data.keys_total || 0;
204
+ document.getElementById('stat-super-keys').textContent = data.super_active || 0;
205
+ document.getElementById('stat-regular-keys').textContent = data.regular_active || 0;
206
+ } catch (error) {
207
+ console.error('Failed to load stats:', error);
208
+ }
209
+ }
210
+
211
+ async function loadKeys() {
212
+ try {
213
+ const response = await fetch(`${API_URL}/manage/keys`, {
214
+ headers: { 'Authorization': `Bearer ${superKey}` }
215
+ });
216
+
217
+ if (response.ok) {
218
+ const keys = await response.json();
219
+ renderKeys(keys);
220
+ }
221
+ } catch (error) {
222
+ console.error('Failed to load keys:', error);
223
+ }
224
+ }
225
+
226
+ async function loadRegularServerKeys() {
227
+ try {
228
+ const response = await fetch(`${API_URL}/manage/keys/server-keys`, {
229
+ headers: { 'Authorization': `Bearer ${superKey}` }
230
+ });
231
+
232
+ if (response.ok) {
233
+ const serverKeys = await response.json();
234
+ renderServerKeys(serverKeys);
235
+ } else {
236
+ document.getElementById('keys-list').innerHTML = '<p>无法加载Server Key列表</p>';
237
+ }
238
+ } catch (error) {
239
+ console.error('Failed to load server keys:', error);
240
+ document.getElementById('keys-list').innerHTML = '<p>无法加载Server Key列表</p>';
241
+ }
242
+ }
243
+
244
+ function renderServerKeys(keys) {
245
+ const keysList = document.getElementById('keys-list');
246
+
247
+ if (keys.length === 0) {
248
+ keysList.innerHTML = '<p>暂无关联的Server Key</p>';
249
+ return;
250
+ }
251
+
252
+ keysList.innerHTML = keys.map(key => `
253
+ <div class="key-card">
254
+ <div class="key-info">
255
+ <h3>
256
+ <span class="key-badge server">🖥️ Server</span>
257
+ <span class="key-badge ${key.isActive ? 'active' : 'inactive'}">
258
+ ${key.isActive ? '活跃' : '已停用'}
259
+ </span>
260
+ ${key.name}
261
+ </h3>
262
+ <p>ID: ${key.id}</p>
263
+ <p>前缀: ${key.keyPrefix}</p>
264
+ ${key.serverId ? `<p>服务器ID: ${key.serverId}</p>` : ''}
265
+ <p>创建时间: ${new Date(key.createdAt).toLocaleString('zh-CN')}</p>
266
+ <p>最后使用: ${key.lastUsed ? new Date(key.lastUsed).toLocaleString('zh-CN') : '从未使用'}</p>
267
+ </div>
268
+ <div class="key-actions">
269
+ ${key.isActive ?
270
+ `<button class="btn-danger" onclick="deactivateKey('${key.id}')">停用</button>` :
271
+ `<button class="btn-success" onclick="activateKey('${key.id}')">激活</button>`
272
+ }
273
+ <button class="btn-danger" onclick="deleteKey('${key.id}', '${key.name}')">删除</button>
274
+ </div>
275
+ </div>
276
+ `).join('');
277
+ }
278
+
279
+ function renderKeys(keys) {
280
+ const keysList = document.getElementById('keys-list');
281
+
282
+ if (keys.length === 0) {
283
+ keysList.innerHTML = '<p>暂无API密钥</p>';
284
+ return;
285
+ }
286
+
287
+ keysList.innerHTML = keys.map(key => `
288
+ <div class="key-card ${key.keyType === 'admin' ? 'super' : ''}">
289
+ <div class="key-info">
290
+ <h3>
291
+ <span class="key-badge ${key.keyType}">
292
+ ${key.keyType === 'admin' ? '👑 Admin' : key.keyType === 'server' ? '🖥️ Server' : '🔑 Regular'}
293
+ </span>
294
+ <span class="key-badge ${key.isActive ? 'active' : 'inactive'}">
295
+ ${key.isActive ? '活跃' : '已停用'}
296
+ </span>
297
+ ${key.name}
298
+ </h3>
299
+ <p>ID: ${key.id}</p>
300
+ <p>前缀: ${key.keyPrefix}</p>
301
+ ${key.serverId ? `<p>服务器ID: ${key.serverId}</p>` : ''}
302
+ <p>创建时间: ${new Date(key.createdAt).toLocaleString('zh-CN')}</p>
303
+ <p>最后使用: ${key.lastUsed ? new Date(key.lastUsed).toLocaleString('zh-CN') : '从未使用'}</p>
304
+ </div>
305
+ <div class="key-actions">
306
+ ${key.isActive ?
307
+ `<button class="btn-danger" onclick="deactivateKey('${key.id}')">停用</button>` :
308
+ `<button class="btn-success" onclick="activateKey('${key.id}')">激活</button>`
309
+ }
310
+ <button class="btn-danger" onclick="deleteKey('${key.id}', '${key.name}')">删除</button>
311
+ </div>
312
+ </div>
313
+ `).join('');
314
+ }
315
+
316
+ async function activateKey(keyId) {
317
+ try {
318
+ const response = await fetch(`${API_URL}/manage/keys/${keyId}/activate`, {
319
+ method: 'PATCH',
320
+ headers: { 'Authorization': `Bearer ${superKey}` }
321
+ });
322
+
323
+ if (response.ok) {
324
+ if (currentUserKeyType === 'admin') {
325
+ loadKeys();
326
+ } else if (currentUserKeyType === 'regular') {
327
+ loadRegularServerKeys();
328
+ }
329
+ } else {
330
+ const error = await response.json();
331
+ alert('激活失败: ' + error.detail);
332
+ }
333
+ } catch (error) {
334
+ alert('激活失败: ' + error.message);
335
+ }
336
+ }
337
+
338
+ async function deactivateKey(keyId) {
339
+ try {
340
+ const response = await fetch(`${API_URL}/manage/keys/${keyId}/deactivate`, {
341
+ method: 'PATCH',
342
+ headers: { 'Authorization': `Bearer ${superKey}` }
343
+ });
344
+
345
+ if (response.ok) {
346
+ if (currentUserKeyType === 'admin') {
347
+ loadKeys();
348
+ } else if (currentUserKeyType === 'regular') {
349
+ loadRegularServerKeys();
350
+ }
351
+ } else {
352
+ const error = await response.json();
353
+ alert('停用失败: ' + error.detail);
354
+ }
355
+ } catch (error) {
356
+ alert('停用失败: ' + error.message);
357
+ }
358
+ }
359
+
360
+ async function deleteKey(keyId, keyName) {
361
+ if (!confirm(`确定要删除密钥 "${keyName}" 吗?此操作无法撤销。`)) {
362
+ return;
363
+ }
364
+
365
+ try {
366
+ const response = await fetch(`${API_URL}/manage/keys/${keyId}`, {
367
+ method: 'DELETE',
368
+ headers: { 'Authorization': `Bearer ${superKey}` }
369
+ });
370
+
371
+ if (response.ok) {
372
+ if (currentUserKeyType === 'admin') {
373
+ loadKeys();
374
+ } else if (currentUserKeyType === 'regular') {
375
+ loadRegularServerKeys();
376
+ }
377
+ } else {
378
+ const error = await response.json();
379
+ alert('删除失败: ' + error.detail);
380
+ }
381
+ } catch (error) {
382
+ alert('删除失败: ' + error.message);
383
+ }
384
+ }
385
+
386
+ function connectWebSocket() {
387
+ if (!superKey) return;
388
+
389
+ const wsUrl = `ws://localhost:8000/ws?api_key=${superKey}`;
390
+ ws = new WebSocket(wsUrl);
391
+
392
+ ws.onopen = () => {
393
+ console.log('WebSocket connected');
394
+ };
395
+
396
+ ws.onmessage = (event) => {
397
+ try {
398
+ const message = JSON.parse(event.data);
399
+
400
+ if (message.type === 'minecraft_event') {
401
+ addEventToList(message.event);
402
+ }
403
+ } catch (error) {
404
+ console.error('Failed to parse WebSocket message:', error);
405
+ }
406
+ };
407
+
408
+ ws.onerror = (error) => {
409
+ console.error('WebSocket error:', error);
410
+ };
411
+
412
+ ws.onclose = () => {
413
+ console.log('WebSocket disconnected');
414
+ setTimeout(() => {
415
+ if (superKey) {
416
+ connectWebSocket();
417
+ }
418
+ }, 5000);
419
+ };
420
+
421
+ setInterval(() => {
422
+ if (ws && ws.readyState === WebSocket.OPEN) {
423
+ ws.send(JSON.stringify({ type: 'ping' }));
424
+ }
425
+ }, 30000);
426
+ }
427
+
428
+ function addEventToList(event) {
429
+ const eventsList = document.getElementById('events-list');
430
+
431
+ const eventItem = document.createElement('div');
432
+ eventItem.className = 'event-item';
433
+ eventItem.innerHTML = `
434
+ <strong>${event.event_type}</strong> - ${event.server_name}<br>
435
+ <small>${new Date(event.timestamp).toLocaleString('zh-CN')}</small><br>
436
+ <pre>${JSON.stringify(event.data, null, 2)}</pre>
437
+ `;
438
+
439
+ eventsList.insertBefore(eventItem, eventsList.firstChild);
440
+
441
+ while (eventsList.children.length > 50) {
442
+ eventsList.removeChild(eventsList.lastChild);
443
+ }
444
+ }
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/dashboard/public/index.html ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>Minecraft WebSocket API - 控制面板</title>
7
+ <link rel="stylesheet" href="style.css">
8
+ </head>
9
+ <body>
10
+ <div id="login-screen" class="screen">
11
+ <div class="login-container">
12
+ <h1>🎮 Minecraft WebSocket API</h1>
13
+ <h2>控制面板登录</h2>
14
+ <form id="login-form">
15
+ <input type="password" id="super-key-input" placeholder="输入Admin Key或Server Key" required>
16
+ <button type="submit">登录</button>
17
+ </form>
18
+ <p class="error-message" id="login-error"></p>
19
+ <div class="login-info">
20
+ <p><strong>密钥类型说明:</strong></p>
21
+ <p>👑 <strong>Admin Key</strong> - 完全管理权限</p>
22
+ <p>🖥️ <strong>Server Key</strong> - 服务器管理权限</p>
23
+ </div>
24
+ </div>
25
+ </div>
26
+
27
+ <div id="dashboard-screen" class="screen hidden">
28
+ <nav class="navbar">
29
+ <h1>🎮 Minecraft WebSocket API</h1>
30
+ <button id="logout-btn">退出登录</button>
31
+ </nav>
32
+
33
+ <div class="container">
34
+ <div class="stats-grid">
35
+ <div class="stat-card">
36
+ <h3>活跃连接</h3>
37
+ <p class="stat-value" id="stat-connections">0</p>
38
+ </div>
39
+ <div class="stat-card">
40
+ <h3>总密钥数</h3>
41
+ <p class="stat-value" id="stat-total-keys">0</p>
42
+ </div>
43
+ <div class="stat-card">
44
+ <h3>活跃SuperKeys</h3>
45
+ <p class="stat-value" id="stat-super-keys">0</p>
46
+ </div>
47
+ <div class="stat-card">
48
+ <h3>活跃普通Keys</h3>
49
+ <p class="stat-value" id="stat-regular-keys">0</p>
50
+ </div>
51
+ </div>
52
+
53
+ <div class="section">
54
+ <div class="section-header">
55
+ <h2>API密钥管理</h2>
56
+ <button id="create-key-btn" class="btn-primary">创建新密钥</button>
57
+ </div>
58
+ <div id="keys-list" class="keys-list"></div>
59
+ </div>
60
+
61
+ <div class="section">
62
+ <h2>实时事件监控</h2>
63
+ <div id="events-list" class="events-list"></div>
64
+ </div>
65
+ </div>
66
+ </div>
67
+
68
+ <div id="create-key-modal" class="modal hidden">
69
+ <div class="modal-content">
70
+ <h2>创建新API密钥</h2>
71
+ <form id="create-key-form">
72
+ <label>名称 *</label>
73
+ <input type="text" id="key-name" required>
74
+
75
+ <label>描述</label>
76
+ <textarea id="key-description"></textarea>
77
+
78
+ <label>
79
+ <input type="checkbox" id="key-is-super">
80
+ 创建为SuperKey
81
+ </label>
82
+
83
+ <div class="modal-actions">
84
+ <button type="button" id="cancel-create-btn" class="btn-secondary">取消</button>
85
+ <button type="submit" class="btn-primary">创建</button>
86
+ </div>
87
+ </form>
88
+ </div>
89
+ </div>
90
+
91
+ <div id="key-details-modal" class="modal hidden">
92
+ <div class="modal-content">
93
+ <h2>密钥详情</h2>
94
+ <div id="key-details-content"></div>
95
+ <div class="modal-actions">
96
+ <button id="close-details-btn" class="btn-secondary">关闭</button>
97
+ </div>
98
+ </div>
99
+ </div>
100
+
101
+ <script src="app.js"></script>
102
+ </body>
103
+ </html>
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/dashboard/public/style.css ADDED
@@ -0,0 +1,410 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ * {
2
+ margin: 0;
3
+ padding: 0;
4
+ box-sizing: border-box;
5
+ }
6
+
7
+ body {
8
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
9
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
10
+ min-height: 100vh;
11
+ color: #333;
12
+ }
13
+
14
+ .screen {
15
+ min-height: 100vh;
16
+ }
17
+
18
+ .hidden {
19
+ display: none !important;
20
+ }
21
+
22
+ .login-container {
23
+ display: flex;
24
+ flex-direction: column;
25
+ align-items: center;
26
+ justify-content: center;
27
+ min-height: 100vh;
28
+ padding: 20px;
29
+ }
30
+
31
+ .login-container h1 {
32
+ color: white;
33
+ font-size: 3em;
34
+ margin-bottom: 10px;
35
+ }
36
+
37
+ .login-container h2 {
38
+ color: white;
39
+ font-size: 1.5em;
40
+ margin-bottom: 30px;
41
+ }
42
+
43
+ #login-form {
44
+ background: white;
45
+ padding: 40px;
46
+ border-radius: 10px;
47
+ box-shadow: 0 10px 40px rgba(0,0,0,0.2);
48
+ width: 100%;
49
+ max-width: 400px;
50
+ }
51
+
52
+ #login-form input {
53
+ width: 100%;
54
+ padding: 15px;
55
+ border: 2px solid #e0e0e0;
56
+ border-radius: 5px;
57
+ font-size: 16px;
58
+ margin-bottom: 20px;
59
+ }
60
+
61
+ #login-form button {
62
+ width: 100%;
63
+ padding: 15px;
64
+ background: #667eea;
65
+ color: white;
66
+ border: none;
67
+ border-radius: 5px;
68
+ font-size: 16px;
69
+ font-weight: bold;
70
+ cursor: pointer;
71
+ transition: background 0.3s;
72
+ }
73
+
74
+ #login-form button:hover {
75
+ background: #5568d3;
76
+ }
77
+
78
+ .error-message {
79
+ color: #ff4444;
80
+ margin-top: 10px;
81
+ text-align: center;
82
+ }
83
+
84
+ .navbar {
85
+ background: white;
86
+ padding: 20px 40px;
87
+ display: flex;
88
+ justify-content: space-between;
89
+ align-items: center;
90
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
91
+ }
92
+
93
+ .navbar h1 {
94
+ font-size: 1.5em;
95
+ color: #667eea;
96
+ }
97
+
98
+ #logout-btn {
99
+ padding: 10px 20px;
100
+ background: #ff4444;
101
+ color: white;
102
+ border: none;
103
+ border-radius: 5px;
104
+ cursor: pointer;
105
+ font-weight: bold;
106
+ }
107
+
108
+ .container {
109
+ max-width: 1400px;
110
+ margin: 0 auto;
111
+ padding: 40px 20px;
112
+ }
113
+
114
+ .stats-grid {
115
+ display: grid;
116
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
117
+ gap: 20px;
118
+ margin-bottom: 40px;
119
+ }
120
+
121
+ .stat-card {
122
+ background: white;
123
+ padding: 30px;
124
+ border-radius: 10px;
125
+ box-shadow: 0 4px 15px rgba(0,0,0,0.1);
126
+ text-align: center;
127
+ }
128
+
129
+ .stat-card h3 {
130
+ color: #666;
131
+ font-size: 0.9em;
132
+ margin-bottom: 10px;
133
+ text-transform: uppercase;
134
+ }
135
+
136
+ .stat-value {
137
+ font-size: 3em;
138
+ font-weight: bold;
139
+ color: #667eea;
140
+ }
141
+
142
+ .section {
143
+ background: white;
144
+ padding: 30px;
145
+ border-radius: 10px;
146
+ box-shadow: 0 4px 15px rgba(0,0,0,0.1);
147
+ margin-bottom: 30px;
148
+ }
149
+
150
+ .section-header {
151
+ display: flex;
152
+ justify-content: space-between;
153
+ align-items: center;
154
+ margin-bottom: 20px;
155
+ }
156
+
157
+ .section h2 {
158
+ color: #333;
159
+ margin-bottom: 20px;
160
+ }
161
+
162
+ .btn-primary {
163
+ padding: 12px 24px;
164
+ background: #667eea;
165
+ color: white;
166
+ border: none;
167
+ border-radius: 5px;
168
+ cursor: pointer;
169
+ font-weight: bold;
170
+ transition: background 0.3s;
171
+ }
172
+
173
+ .btn-primary:hover {
174
+ background: #5568d3;
175
+ }
176
+
177
+ .btn-secondary {
178
+ padding: 12px 24px;
179
+ background: #e0e0e0;
180
+ color: #333;
181
+ border: none;
182
+ border-radius: 5px;
183
+ cursor: pointer;
184
+ font-weight: bold;
185
+ }
186
+
187
+ .btn-danger {
188
+ padding: 8px 16px;
189
+ background: #ff4444;
190
+ color: white;
191
+ border: none;
192
+ border-radius: 5px;
193
+ cursor: pointer;
194
+ font-size: 0.9em;
195
+ }
196
+
197
+ .btn-success {
198
+ padding: 8px 16px;
199
+ background: #4caf50;
200
+ color: white;
201
+ border: none;
202
+ border-radius: 5px;
203
+ cursor: pointer;
204
+ font-size: 0.9em;
205
+ }
206
+
207
+ .keys-list {
208
+ display: grid;
209
+ gap: 15px;
210
+ }
211
+
212
+ .key-card {
213
+ border: 2px solid #e0e0e0;
214
+ padding: 20px;
215
+ border-radius: 8px;
216
+ display: flex;
217
+ justify-content: space-between;
218
+ align-items: center;
219
+ }
220
+
221
+ .key-card.super {
222
+ border-color: #764ba2;
223
+ background: #f9f7fb;
224
+ }
225
+
226
+ .key-info h3 {
227
+ margin-bottom: 5px;
228
+ }
229
+
230
+ .key-badge {
231
+ display: inline-block;
232
+ padding: 4px 8px;
233
+ border-radius: 4px;
234
+ font-size: 0.8em;
235
+ font-weight: bold;
236
+ margin-right: 10px;
237
+ }
238
+
239
+ .key-badge.super {
240
+ background: #764ba2;
241
+ color: white;
242
+ }
243
+
244
+ .key-badge.regular {
245
+ background: #667eea;
246
+ color: white;
247
+ }
248
+
249
+ .key-badge.active {
250
+ background: #4caf50;
251
+ color: white;
252
+ }
253
+
254
+ .key-badge.inactive {
255
+ background: #ff4444;
256
+ color: white;
257
+ }
258
+
259
+ .key-actions {
260
+ display: flex;
261
+ gap: 10px;
262
+ }
263
+
264
+ .events-list {
265
+ max-height: 400px;
266
+ overflow-y: auto;
267
+ }
268
+
269
+ .event-item {
270
+ padding: 15px;
271
+ border-left: 4px solid #667eea;
272
+ background: #f5f5f5;
273
+ margin-bottom: 10px;
274
+ border-radius: 4px;
275
+ }
276
+
277
+ .event-item strong {
278
+ color: #667eea;
279
+ }
280
+
281
+ .modal {
282
+ position: fixed;
283
+ top: 0;
284
+ left: 0;
285
+ width: 100%;
286
+ height: 100%;
287
+ background: rgba(0,0,0,0.5);
288
+ display: flex;
289
+ justify-content: center;
290
+ align-items: center;
291
+ z-index: 1000;
292
+ }
293
+
294
+ .modal-content {
295
+ background: white;
296
+ padding: 40px;
297
+ border-radius: 10px;
298
+ max-width: 500px;
299
+ width: 90%;
300
+ max-height: 90vh;
301
+ overflow-y: auto;
302
+ }
303
+
304
+ .modal-content h2 {
305
+ margin-bottom: 20px;
306
+ }
307
+
308
+ .modal-content label {
309
+ display: block;
310
+ margin-bottom: 5px;
311
+ font-weight: bold;
312
+ color: #666;
313
+ }
314
+
315
+ .modal-content input[type="text"],
316
+ .modal-content textarea {
317
+ width: 100%;
318
+ padding: 10px;
319
+ border: 2px solid #e0e0e0;
320
+ border-radius: 5px;
321
+ margin-bottom: 15px;
322
+ font-size: 14px;
323
+ }
324
+
325
+ .modal-content textarea {
326
+ min-height: 80px;
327
+ resize: vertical;
328
+ }
329
+
330
+ .modal-content input[type="checkbox"] {
331
+ margin-right: 8px;
332
+ }
333
+
334
+ .modal-actions {
335
+ display: flex;
336
+ gap: 10px;
337
+ justify-content: flex-end;
338
+ margin-top: 20px;
339
+ }
340
+
341
+ .nav-info {
342
+ display: flex;
343
+ align-items: center;
344
+ gap: 15px;
345
+ }
346
+
347
+ #user-info {
348
+ font-size: 0.9em;
349
+ color: #667eea;
350
+ background: rgba(255,255,255,0.1);
351
+ padding: 8px 12px;
352
+ border-radius: 20px;
353
+ }
354
+
355
+ .server-info {
356
+ display: grid;
357
+ grid-template-columns: 1fr 1fr;
358
+ gap: 20px;
359
+ }
360
+
361
+ .info-card {
362
+ background: #f8f9fa;
363
+ padding: 20px;
364
+ border-radius: 8px;
365
+ border: 1px solid #e9ecef;
366
+ }
367
+
368
+ .info-card h3 {
369
+ margin-bottom: 15px;
370
+ color: #495057;
371
+ }
372
+
373
+ .command-history {
374
+ max-height: 200px;
375
+ overflow-y: auto;
376
+ background: white;
377
+ border: 1px solid #e9ecef;
378
+ border-radius: 4px;
379
+ padding: 10px;
380
+ }
381
+
382
+ .command-item {
383
+ padding: 8px 0;
384
+ border-bottom: 1px solid #f8f9fa;
385
+ font-family: monospace;
386
+ font-size: 0.9em;
387
+ }
388
+
389
+ .command-item:last-child {
390
+ border-bottom: none;
391
+ }
392
+
393
+ .command-item .timestamp {
394
+ color: #6c757d;
395
+ font-size: 0.8em;
396
+ }
397
+
398
+ .login-info {
399
+ margin-top: 20px;
400
+ text-align: left;
401
+ background: rgba(255,255,255,0.1);
402
+ padding: 15px;
403
+ border-radius: 8px;
404
+ }
405
+
406
+ .login-info p {
407
+ margin: 5px 0;
408
+ font-size: 0.9em;
409
+ color: white;
410
+ }
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/dashboard/server.js ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ require('dotenv').config();
2
+ const express = require('express');
3
+ const path = require('path');
4
+
5
+ const app = express();
6
+ const PORT = parseInt(process.env.DASHBOARD_PORT || '3000');
7
+
8
+ app.use(express.static(path.join(__dirname, 'public')));
9
+
10
+ app.get('/', (req, res) => {
11
+ res.sendFile(path.join(__dirname, 'public', 'index.html'));
12
+ });
13
+
14
+ app.listen(PORT, () => {
15
+ console.log('='.repeat(50));
16
+ console.log('🎮 Minecraft WebSocket API - 控制面板');
17
+ console.log('='.repeat(50));
18
+ console.log(`控制面板地址: http://localhost:${PORT}`);
19
+ console.log('请使用SuperKey登录');
20
+ console.log('='.repeat(50));
21
+ });
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/docker-compose.yml ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3.8'
2
+
3
+ services:
4
+ interconnect-server:
5
+ build: .
6
+ ports:
7
+ - "8000:8000"
8
+ environment:
9
+ - SERVER_HOST=0.0.0.0
10
+ - SERVER_PORT=8000
11
+ - DATABASE_PATH=/data/minecraft_ws.db
12
+ volumes:
13
+ - ./data:/data
14
+ restart: unless-stopped
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.gitattributes ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ # Auto detect text files and perform LF normalization
2
+ * text=auto
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/package-lock.json ADDED
@@ -0,0 +1,675 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "interconnect-server-node",
3
+ "version": "1.0.0",
4
+ "lockfileVersion": 1,
5
+ "requires": true,
6
+ "dependencies": {
7
+ "accepts": {
8
+ "version": "1.3.8",
9
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
10
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
11
+ "requires": {
12
+ "mime-types": "~2.1.34",
13
+ "negotiator": "0.6.3"
14
+ }
15
+ },
16
+ "array-flatten": {
17
+ "version": "1.1.1",
18
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
19
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
20
+ },
21
+ "asynckit": {
22
+ "version": "0.4.0",
23
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
24
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
25
+ },
26
+ "axios": {
27
+ "version": "1.13.2",
28
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
29
+ "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
30
+ "requires": {
31
+ "follow-redirects": "^1.15.6",
32
+ "form-data": "^4.0.4",
33
+ "proxy-from-env": "^1.1.0"
34
+ }
35
+ },
36
+ "bcryptjs": {
37
+ "version": "2.4.3",
38
+ "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
39
+ "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ=="
40
+ },
41
+ "body-parser": {
42
+ "version": "1.20.4",
43
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
44
+ "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==",
45
+ "requires": {
46
+ "bytes": "~3.1.2",
47
+ "content-type": "~1.0.5",
48
+ "debug": "2.6.9",
49
+ "depd": "2.0.0",
50
+ "destroy": "~1.2.0",
51
+ "http-errors": "~2.0.1",
52
+ "iconv-lite": "~0.4.24",
53
+ "on-finished": "~2.4.1",
54
+ "qs": "~6.14.0",
55
+ "raw-body": "~2.5.3",
56
+ "type-is": "~1.6.18",
57
+ "unpipe": "~1.0.0"
58
+ },
59
+ "dependencies": {
60
+ "debug": {
61
+ "version": "2.6.9",
62
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
63
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
64
+ "requires": {
65
+ "ms": "2.0.0"
66
+ }
67
+ },
68
+ "ms": {
69
+ "version": "2.0.0",
70
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
71
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
72
+ }
73
+ }
74
+ },
75
+ "bytes": {
76
+ "version": "3.1.2",
77
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
78
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="
79
+ },
80
+ "call-bind-apply-helpers": {
81
+ "version": "1.0.2",
82
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
83
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
84
+ "requires": {
85
+ "es-errors": "^1.3.0",
86
+ "function-bind": "^1.1.2"
87
+ }
88
+ },
89
+ "call-bound": {
90
+ "version": "1.0.4",
91
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
92
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
93
+ "requires": {
94
+ "call-bind-apply-helpers": "^1.0.2",
95
+ "get-intrinsic": "^1.3.0"
96
+ }
97
+ },
98
+ "combined-stream": {
99
+ "version": "1.0.8",
100
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
101
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
102
+ "requires": {
103
+ "delayed-stream": "~1.0.0"
104
+ }
105
+ },
106
+ "commander": {
107
+ "version": "11.1.0",
108
+ "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz",
109
+ "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="
110
+ },
111
+ "content-disposition": {
112
+ "version": "0.5.4",
113
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
114
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
115
+ "requires": {
116
+ "safe-buffer": "5.2.1"
117
+ }
118
+ },
119
+ "content-type": {
120
+ "version": "1.0.5",
121
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
122
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="
123
+ },
124
+ "cookie": {
125
+ "version": "0.7.2",
126
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
127
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="
128
+ },
129
+ "cookie-signature": {
130
+ "version": "1.0.7",
131
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
132
+ "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="
133
+ },
134
+ "delayed-stream": {
135
+ "version": "1.0.0",
136
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
137
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
138
+ },
139
+ "depd": {
140
+ "version": "2.0.0",
141
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
142
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="
143
+ },
144
+ "destroy": {
145
+ "version": "1.2.0",
146
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
147
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="
148
+ },
149
+ "dotenv": {
150
+ "version": "16.6.1",
151
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
152
+ "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="
153
+ },
154
+ "dunder-proto": {
155
+ "version": "1.0.1",
156
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
157
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
158
+ "requires": {
159
+ "call-bind-apply-helpers": "^1.0.1",
160
+ "es-errors": "^1.3.0",
161
+ "gopd": "^1.2.0"
162
+ }
163
+ },
164
+ "ee-first": {
165
+ "version": "1.1.1",
166
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
167
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
168
+ },
169
+ "encodeurl": {
170
+ "version": "2.0.0",
171
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
172
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="
173
+ },
174
+ "es-define-property": {
175
+ "version": "1.0.1",
176
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
177
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="
178
+ },
179
+ "es-errors": {
180
+ "version": "1.3.0",
181
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
182
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="
183
+ },
184
+ "es-object-atoms": {
185
+ "version": "1.1.1",
186
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
187
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
188
+ "requires": {
189
+ "es-errors": "^1.3.0"
190
+ }
191
+ },
192
+ "es-set-tostringtag": {
193
+ "version": "2.1.0",
194
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
195
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
196
+ "requires": {
197
+ "es-errors": "^1.3.0",
198
+ "get-intrinsic": "^1.2.6",
199
+ "has-tostringtag": "^1.0.2",
200
+ "hasown": "^2.0.2"
201
+ }
202
+ },
203
+ "escape-html": {
204
+ "version": "1.0.3",
205
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
206
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
207
+ },
208
+ "etag": {
209
+ "version": "1.8.1",
210
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
211
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="
212
+ },
213
+ "express": {
214
+ "version": "4.22.1",
215
+ "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
216
+ "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
217
+ "requires": {
218
+ "accepts": "~1.3.8",
219
+ "array-flatten": "1.1.1",
220
+ "body-parser": "~1.20.3",
221
+ "content-disposition": "~0.5.4",
222
+ "content-type": "~1.0.4",
223
+ "cookie": "~0.7.1",
224
+ "cookie-signature": "~1.0.6",
225
+ "debug": "2.6.9",
226
+ "depd": "2.0.0",
227
+ "encodeurl": "~2.0.0",
228
+ "escape-html": "~1.0.3",
229
+ "etag": "~1.8.1",
230
+ "finalhandler": "~1.3.1",
231
+ "fresh": "~0.5.2",
232
+ "http-errors": "~2.0.0",
233
+ "merge-descriptors": "1.0.3",
234
+ "methods": "~1.1.2",
235
+ "on-finished": "~2.4.1",
236
+ "parseurl": "~1.3.3",
237
+ "path-to-regexp": "~0.1.12",
238
+ "proxy-addr": "~2.0.7",
239
+ "qs": "~6.14.0",
240
+ "range-parser": "~1.2.1",
241
+ "safe-buffer": "5.2.1",
242
+ "send": "~0.19.0",
243
+ "serve-static": "~1.16.2",
244
+ "setprototypeof": "1.2.0",
245
+ "statuses": "~2.0.1",
246
+ "type-is": "~1.6.18",
247
+ "utils-merge": "1.0.1",
248
+ "vary": "~1.1.2"
249
+ },
250
+ "dependencies": {
251
+ "debug": {
252
+ "version": "2.6.9",
253
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
254
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
255
+ "requires": {
256
+ "ms": "2.0.0"
257
+ }
258
+ },
259
+ "ms": {
260
+ "version": "2.0.0",
261
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
262
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
263
+ }
264
+ }
265
+ },
266
+ "finalhandler": {
267
+ "version": "1.3.2",
268
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
269
+ "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
270
+ "requires": {
271
+ "debug": "2.6.9",
272
+ "encodeurl": "~2.0.0",
273
+ "escape-html": "~1.0.3",
274
+ "on-finished": "~2.4.1",
275
+ "parseurl": "~1.3.3",
276
+ "statuses": "~2.0.2",
277
+ "unpipe": "~1.0.0"
278
+ },
279
+ "dependencies": {
280
+ "debug": {
281
+ "version": "2.6.9",
282
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
283
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
284
+ "requires": {
285
+ "ms": "2.0.0"
286
+ }
287
+ },
288
+ "ms": {
289
+ "version": "2.0.0",
290
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
291
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
292
+ }
293
+ }
294
+ },
295
+ "follow-redirects": {
296
+ "version": "1.15.11",
297
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
298
+ "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="
299
+ },
300
+ "form-data": {
301
+ "version": "4.0.5",
302
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
303
+ "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
304
+ "requires": {
305
+ "asynckit": "^0.4.0",
306
+ "combined-stream": "^1.0.8",
307
+ "es-set-tostringtag": "^2.1.0",
308
+ "hasown": "^2.0.2",
309
+ "mime-types": "^2.1.12"
310
+ }
311
+ },
312
+ "forwarded": {
313
+ "version": "0.2.0",
314
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
315
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="
316
+ },
317
+ "fresh": {
318
+ "version": "0.5.2",
319
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
320
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="
321
+ },
322
+ "function-bind": {
323
+ "version": "1.1.2",
324
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
325
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="
326
+ },
327
+ "get-intrinsic": {
328
+ "version": "1.3.0",
329
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
330
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
331
+ "requires": {
332
+ "call-bind-apply-helpers": "^1.0.2",
333
+ "es-define-property": "^1.0.1",
334
+ "es-errors": "^1.3.0",
335
+ "es-object-atoms": "^1.1.1",
336
+ "function-bind": "^1.1.2",
337
+ "get-proto": "^1.0.1",
338
+ "gopd": "^1.2.0",
339
+ "has-symbols": "^1.1.0",
340
+ "hasown": "^2.0.2",
341
+ "math-intrinsics": "^1.1.0"
342
+ }
343
+ },
344
+ "get-proto": {
345
+ "version": "1.0.1",
346
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
347
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
348
+ "requires": {
349
+ "dunder-proto": "^1.0.1",
350
+ "es-object-atoms": "^1.0.0"
351
+ }
352
+ },
353
+ "gopd": {
354
+ "version": "1.2.0",
355
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
356
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="
357
+ },
358
+ "has-symbols": {
359
+ "version": "1.1.0",
360
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
361
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="
362
+ },
363
+ "has-tostringtag": {
364
+ "version": "1.0.2",
365
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
366
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
367
+ "requires": {
368
+ "has-symbols": "^1.0.3"
369
+ }
370
+ },
371
+ "hasown": {
372
+ "version": "2.0.2",
373
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
374
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
375
+ "requires": {
376
+ "function-bind": "^1.1.2"
377
+ }
378
+ },
379
+ "http-errors": {
380
+ "version": "2.0.1",
381
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
382
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
383
+ "requires": {
384
+ "depd": "~2.0.0",
385
+ "inherits": "~2.0.4",
386
+ "setprototypeof": "~1.2.0",
387
+ "statuses": "~2.0.2",
388
+ "toidentifier": "~1.0.1"
389
+ }
390
+ },
391
+ "iconv-lite": {
392
+ "version": "0.4.24",
393
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
394
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
395
+ "requires": {
396
+ "safer-buffer": ">= 2.1.2 < 3"
397
+ }
398
+ },
399
+ "inherits": {
400
+ "version": "2.0.4",
401
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
402
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
403
+ },
404
+ "ipaddr.js": {
405
+ "version": "1.9.1",
406
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
407
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="
408
+ },
409
+ "math-intrinsics": {
410
+ "version": "1.1.0",
411
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
412
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="
413
+ },
414
+ "media-typer": {
415
+ "version": "0.3.0",
416
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
417
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="
418
+ },
419
+ "merge-descriptors": {
420
+ "version": "1.0.3",
421
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
422
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="
423
+ },
424
+ "methods": {
425
+ "version": "1.1.2",
426
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
427
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="
428
+ },
429
+ "mime": {
430
+ "version": "1.6.0",
431
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
432
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
433
+ },
434
+ "mime-db": {
435
+ "version": "1.52.0",
436
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
437
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
438
+ },
439
+ "mime-types": {
440
+ "version": "2.1.35",
441
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
442
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
443
+ "requires": {
444
+ "mime-db": "1.52.0"
445
+ }
446
+ },
447
+ "ms": {
448
+ "version": "2.1.3",
449
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
450
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
451
+ },
452
+ "negotiator": {
453
+ "version": "0.6.3",
454
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
455
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="
456
+ },
457
+ "object-inspect": {
458
+ "version": "1.13.4",
459
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
460
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="
461
+ },
462
+ "on-finished": {
463
+ "version": "2.4.1",
464
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
465
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
466
+ "requires": {
467
+ "ee-first": "1.1.1"
468
+ }
469
+ },
470
+ "parseurl": {
471
+ "version": "1.3.3",
472
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
473
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
474
+ },
475
+ "path-to-regexp": {
476
+ "version": "0.1.12",
477
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
478
+ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="
479
+ },
480
+ "proxy-addr": {
481
+ "version": "2.0.7",
482
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
483
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
484
+ "requires": {
485
+ "forwarded": "0.2.0",
486
+ "ipaddr.js": "1.9.1"
487
+ }
488
+ },
489
+ "proxy-from-env": {
490
+ "version": "1.1.0",
491
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
492
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
493
+ },
494
+ "qs": {
495
+ "version": "6.14.1",
496
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
497
+ "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
498
+ "requires": {
499
+ "side-channel": "^1.1.0"
500
+ }
501
+ },
502
+ "range-parser": {
503
+ "version": "1.2.1",
504
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
505
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
506
+ },
507
+ "raw-body": {
508
+ "version": "2.5.3",
509
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
510
+ "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
511
+ "requires": {
512
+ "bytes": "~3.1.2",
513
+ "http-errors": "~2.0.1",
514
+ "iconv-lite": "~0.4.24",
515
+ "unpipe": "~1.0.0"
516
+ }
517
+ },
518
+ "safe-buffer": {
519
+ "version": "5.2.1",
520
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
521
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
522
+ },
523
+ "safer-buffer": {
524
+ "version": "2.1.2",
525
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
526
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
527
+ },
528
+ "send": {
529
+ "version": "0.19.2",
530
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
531
+ "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
532
+ "requires": {
533
+ "debug": "2.6.9",
534
+ "depd": "2.0.0",
535
+ "destroy": "1.2.0",
536
+ "encodeurl": "~2.0.0",
537
+ "escape-html": "~1.0.3",
538
+ "etag": "~1.8.1",
539
+ "fresh": "~0.5.2",
540
+ "http-errors": "~2.0.1",
541
+ "mime": "1.6.0",
542
+ "ms": "2.1.3",
543
+ "on-finished": "~2.4.1",
544
+ "range-parser": "~1.2.1",
545
+ "statuses": "~2.0.2"
546
+ },
547
+ "dependencies": {
548
+ "debug": {
549
+ "version": "2.6.9",
550
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
551
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
552
+ "requires": {
553
+ "ms": "2.0.0"
554
+ },
555
+ "dependencies": {
556
+ "ms": {
557
+ "version": "2.0.0",
558
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
559
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
560
+ }
561
+ }
562
+ }
563
+ }
564
+ },
565
+ "serve-static": {
566
+ "version": "1.16.3",
567
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
568
+ "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
569
+ "requires": {
570
+ "encodeurl": "~2.0.0",
571
+ "escape-html": "~1.0.3",
572
+ "parseurl": "~1.3.3",
573
+ "send": "~0.19.1"
574
+ }
575
+ },
576
+ "setprototypeof": {
577
+ "version": "1.2.0",
578
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
579
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
580
+ },
581
+ "side-channel": {
582
+ "version": "1.1.0",
583
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
584
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
585
+ "requires": {
586
+ "es-errors": "^1.3.0",
587
+ "object-inspect": "^1.13.3",
588
+ "side-channel-list": "^1.0.0",
589
+ "side-channel-map": "^1.0.1",
590
+ "side-channel-weakmap": "^1.0.2"
591
+ }
592
+ },
593
+ "side-channel-list": {
594
+ "version": "1.0.0",
595
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
596
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
597
+ "requires": {
598
+ "es-errors": "^1.3.0",
599
+ "object-inspect": "^1.13.3"
600
+ }
601
+ },
602
+ "side-channel-map": {
603
+ "version": "1.0.1",
604
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
605
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
606
+ "requires": {
607
+ "call-bound": "^1.0.2",
608
+ "es-errors": "^1.3.0",
609
+ "get-intrinsic": "^1.2.5",
610
+ "object-inspect": "^1.13.3"
611
+ }
612
+ },
613
+ "side-channel-weakmap": {
614
+ "version": "1.0.2",
615
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
616
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
617
+ "requires": {
618
+ "call-bound": "^1.0.2",
619
+ "es-errors": "^1.3.0",
620
+ "get-intrinsic": "^1.2.5",
621
+ "object-inspect": "^1.13.3",
622
+ "side-channel-map": "^1.0.1"
623
+ }
624
+ },
625
+ "sql.js": {
626
+ "version": "1.13.0",
627
+ "resolved": "https://registry.npmjs.org/sql.js/-/sql.js-1.13.0.tgz",
628
+ "integrity": "sha512-RJbVP1HRDlUUXahJ7VMTcu9Rm1Nzw+EBpoPr94vnbD4LwR715F3CcxE2G2k45PewcaZ57pjetYa+LoSJLAASgA=="
629
+ },
630
+ "statuses": {
631
+ "version": "2.0.2",
632
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
633
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="
634
+ },
635
+ "toidentifier": {
636
+ "version": "1.0.1",
637
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
638
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="
639
+ },
640
+ "type-is": {
641
+ "version": "1.6.18",
642
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
643
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
644
+ "requires": {
645
+ "media-typer": "0.3.0",
646
+ "mime-types": "~2.1.24"
647
+ }
648
+ },
649
+ "unpipe": {
650
+ "version": "1.0.0",
651
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
652
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="
653
+ },
654
+ "utils-merge": {
655
+ "version": "1.0.1",
656
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
657
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="
658
+ },
659
+ "uuid": {
660
+ "version": "9.0.1",
661
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
662
+ "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="
663
+ },
664
+ "vary": {
665
+ "version": "1.1.2",
666
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
667
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="
668
+ },
669
+ "ws": {
670
+ "version": "8.19.0",
671
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
672
+ "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="
673
+ }
674
+ }
675
+ }
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/package.json ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "interconnect-server",
3
+ "version": "1.0.0",
4
+ "description": "InterConnect-Server - Minecraft WebSocket API Server",
5
+ "author": "",
6
+ "license": "MIT",
7
+ "main": "src/server.js",
8
+ "scripts": {
9
+ "start": "node src/server.js",
10
+ "dev": "node --watch src/server.js",
11
+ "cli": "node cli/cli.js",
12
+ "dashboard": "node dashboard/server.js"
13
+ },
14
+ "keywords": [
15
+ "minecraft",
16
+ "websocket",
17
+ "api"
18
+ ],
19
+ "author": "",
20
+ "license": "MIT",
21
+ "dependencies": {
22
+ "axios": "^1.13.2",
23
+ "bcryptjs": "^2.4.3",
24
+ "commander": "^11.1.0",
25
+ "dotenv": "^16.3.1",
26
+ "express": "^4.18.2",
27
+ "sql.js": "^1.10.3",
28
+ "uuid": "^9.0.1",
29
+ "ws": "^8.14.2"
30
+ }
31
+ }
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/src/auth.js ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const { KEY_TYPE_ADMIN, KEY_TYPE_SERVER, KEY_TYPE_REGULAR } = require('./database');
2
+
3
+ function verifyApiKey(db) {
4
+ return async (req, res, next) => {
5
+ const authHeader = req.headers.authorization;
6
+
7
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
8
+ return res.status(401).json({ detail: 'Invalid API Key' });
9
+ }
10
+
11
+ const token = authHeader.substring(7);
12
+ const result = await db.verifyApiKey(token);
13
+
14
+ if (!result) {
15
+ return res.status(401).json({ detail: 'Invalid API Key' });
16
+ }
17
+
18
+ req.apiKey = result;
19
+ next();
20
+ };
21
+ }
22
+
23
+ function requireAdminKey(req, res, next) {
24
+ if (!req.apiKey || req.apiKey.keyType !== KEY_TYPE_ADMIN) {
25
+ return res.status(403).json({ detail: 'Operation requires an Admin Key' });
26
+ }
27
+ next();
28
+ }
29
+
30
+ function requireRegularOrAdminKey(req, res, next) {
31
+ if (!req.apiKey || (req.apiKey.keyType !== KEY_TYPE_ADMIN && req.apiKey.keyType !== KEY_TYPE_REGULAR)) {
32
+ return res.status(403).json({ detail: 'Operation requires a Regular Key or Admin Key' });
33
+ }
34
+ next();
35
+ }
36
+
37
+ function requireRegularKeyForOwnServerKeys(db) {
38
+ return async (req, res, next) => {
39
+ requireRegularOrAdminKey(req, res, async () => {
40
+ const keyId = req.params.key_id;
41
+ const keyInfo = db.getApiKeyDetailsById(keyId);
42
+
43
+ if (!keyInfo) {
44
+ return res.status(404).json({ detail: 'Key not found' });
45
+ }
46
+
47
+ // Admin可以管理所有密钥
48
+ if (req.apiKey.keyType === KEY_TYPE_ADMIN) {
49
+ return next();
50
+ }
51
+
52
+ // Regular Key只能管理自己关联的Server Key
53
+ if (req.apiKey.keyType === KEY_TYPE_REGULAR && keyInfo.regularKeyId === req.apiKey.id) {
54
+ return next();
55
+ }
56
+
57
+ return res.status(403).json({ detail: 'Operation not permitted for this key' });
58
+ });
59
+ };
60
+ }
61
+
62
+ function requireServerKey(req, res, next) {
63
+ if (!req.apiKey || (req.apiKey.keyType !== KEY_TYPE_ADMIN && req.apiKey.keyType !== KEY_TYPE_SERVER)) {
64
+ return res.status(403).json({ detail: 'Operation requires a Server Key or Admin Key' });
65
+ }
66
+ next();
67
+ }
68
+
69
+ function requireAnyKey(req, res, next) {
70
+ if (!req.apiKey) {
71
+ return res.status(401).json({ detail: 'Authentication required' });
72
+ }
73
+ next();
74
+ }
75
+
76
+ module.exports = {
77
+ verifyApiKey,
78
+ requireAdminKey,
79
+ requireServerKey,
80
+ requireRegularOrAdminKey,
81
+ requireRegularKeyForOwnServerKeys,
82
+ requireAnyKey
83
+ };
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/src/database.js ADDED
@@ -0,0 +1,367 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const initSqlJs = require('sql.js');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const bcrypt = require('bcryptjs');
5
+ const { v4: uuidv4 } = require('uuid');
6
+
7
+ const KEY_PREFIX_ADMIN = 'mc_admin_';
8
+ const KEY_PREFIX_SERVER = 'mc_server_';
9
+ const KEY_PREFIX_REGULAR = 'mc_key_';
10
+
11
+ const KEY_TYPE_ADMIN = 'admin';
12
+ const KEY_TYPE_SERVER = 'server';
13
+ const KEY_TYPE_REGULAR = 'regular';
14
+
15
+ class Database {
16
+ constructor(dbPath = 'minecraft_ws.db') {
17
+ this.dbPath = dbPath;
18
+ this.db = null;
19
+ }
20
+
21
+ async init() {
22
+ const SQL = await initSqlJs();
23
+
24
+ if (fs.existsSync(this.dbPath)) {
25
+ const buffer = fs.readFileSync(this.dbPath);
26
+ this.db = new SQL.Database(buffer);
27
+ } else {
28
+ this.db = new SQL.Database();
29
+ }
30
+
31
+ this.initDbStructure();
32
+ this.save();
33
+ }
34
+
35
+ initDbStructure() {
36
+ this.db.run(`
37
+ CREATE TABLE IF NOT EXISTS api_keys (
38
+ id TEXT PRIMARY KEY,
39
+ name TEXT NOT NULL,
40
+ description TEXT,
41
+ key_hash TEXT NOT NULL UNIQUE,
42
+ key_prefix TEXT NOT NULL,
43
+ key_type TEXT NOT NULL,
44
+ server_id TEXT,
45
+ regular_key_id TEXT,
46
+ created_at TEXT NOT NULL,
47
+ last_used TEXT,
48
+ is_active INTEGER DEFAULT 1
49
+ )
50
+ `);
51
+
52
+ this.db.run(`
53
+ CREATE TABLE IF NOT EXISTS event_logs (
54
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
55
+ event_type TEXT NOT NULL,
56
+ server_name TEXT NOT NULL,
57
+ timestamp TEXT NOT NULL,
58
+ data TEXT NOT NULL,
59
+ api_key_id TEXT,
60
+ FOREIGN KEY (api_key_id) REFERENCES api_keys (id)
61
+ )
62
+ `);
63
+ }
64
+
65
+ save() {
66
+ const data = this.db.export();
67
+ const buffer = Buffer.from(data);
68
+ fs.writeFileSync(this.dbPath, buffer);
69
+ }
70
+
71
+ generateRandomRawKeyString(prefix) {
72
+ return `${prefix}${uuidv4().replace(/-/g, '')}`;
73
+ }
74
+
75
+ async ensureInitialAdminKey() {
76
+ const stmt = this.db.prepare('SELECT 1 FROM api_keys WHERE key_type = ? LIMIT 1');
77
+ stmt.bind([KEY_TYPE_ADMIN]);
78
+ const adminKeyExists = stmt.step();
79
+ stmt.free();
80
+
81
+ if (!adminKeyExists) {
82
+ const generatedRawAdminKey = this.generateRandomRawKeyString(KEY_PREFIX_ADMIN);
83
+ const adminKeyName = 'Auto-Generated Admin Key';
84
+ const adminKeyDescription = `Auto-generated on ${new Date().toISOString()}`;
85
+
86
+ const keyId = uuidv4();
87
+ const keyHash = await bcrypt.hash(generatedRawAdminKey, 10);
88
+ const createdAt = new Date().toISOString();
89
+
90
+ try {
91
+ this.db.run(
92
+ 'INSERT INTO api_keys (id, name, description, key_hash, key_prefix, key_type, created_at, is_active) VALUES (?, ?, ?, ?, ?, ?, ?, 1)',
93
+ [keyId, adminKeyName, adminKeyDescription, keyHash, KEY_PREFIX_ADMIN, KEY_TYPE_ADMIN, createdAt]
94
+ );
95
+ this.save();
96
+ return { name: adminKeyName, key: generatedRawAdminKey };
97
+ } catch (error) {
98
+ console.error('Error creating initial Admin Key:', error);
99
+ return null;
100
+ }
101
+ }
102
+ return null;
103
+ }
104
+
105
+ async createRegularKeyWithServerKey(name, description = '', serverId = null) {
106
+ const regularKeyId = uuidv4();
107
+ const serverKeyId = uuidv4();
108
+
109
+ // 创建Regular Key
110
+ const regularRawKey = this.generateRandomRawKeyString(KEY_PREFIX_REGULAR);
111
+ const regularKeyHash = await bcrypt.hash(regularRawKey, 10);
112
+
113
+ // 创建Server Key
114
+ const serverRawKey = this.generateRandomRawKeyString(KEY_PREFIX_SERVER);
115
+ const serverKeyHash = await bcrypt.hash(serverRawKey, 10);
116
+
117
+ const createdAt = new Date().toISOString();
118
+
119
+ try {
120
+ // 开始事务
121
+ this.db.exec('BEGIN TRANSACTION');
122
+
123
+ // 插入Regular Key
124
+ this.db.run(
125
+ 'INSERT INTO api_keys (id, name, description, key_hash, key_prefix, key_type, server_id, created_at, is_active) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1)',
126
+ [regularKeyId, name, description, regularKeyHash, KEY_PREFIX_REGULAR, KEY_TYPE_REGULAR, serverId, createdAt]
127
+ );
128
+
129
+ // 插入关联的Server Key
130
+ this.db.run(
131
+ 'INSERT INTO api_keys (id, name, description, key_hash, key_prefix, key_type, server_id, regular_key_id, created_at, is_active) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1)',
132
+ [serverKeyId, `${name} - Server Key`, `Server Key for ${name}`, serverKeyHash, KEY_PREFIX_SERVER, KEY_TYPE_SERVER, serverId, regularKeyId, createdAt]
133
+ );
134
+
135
+ // 提交事务
136
+ this.db.exec('COMMIT');
137
+ this.save();
138
+
139
+ return {
140
+ regularKey: {
141
+ id: regularKeyId,
142
+ rawKey: regularRawKey,
143
+ keyPrefix: KEY_PREFIX_REGULAR,
144
+ keyType: KEY_TYPE_REGULAR,
145
+ serverId
146
+ },
147
+ serverKey: {
148
+ id: serverKeyId,
149
+ rawKey: serverRawKey,
150
+ keyPrefix: KEY_PREFIX_SERVER,
151
+ keyType: KEY_TYPE_SERVER,
152
+ serverId,
153
+ regularKeyId
154
+ }
155
+ };
156
+ } catch (error) {
157
+ this.db.exec('ROLLBACK');
158
+ throw new Error('Could not generate unique API key hash');
159
+ }
160
+ }
161
+
162
+ async createApiKey(name, description = '', keyType = KEY_TYPE_REGULAR, serverId = null, regularKeyId = null) {
163
+ const keyId = uuidv4();
164
+ let prefix;
165
+
166
+ switch(keyType) {
167
+ case KEY_TYPE_ADMIN:
168
+ prefix = KEY_PREFIX_ADMIN;
169
+ break;
170
+ case KEY_TYPE_SERVER:
171
+ prefix = KEY_PREFIX_SERVER;
172
+ break;
173
+ default:
174
+ prefix = KEY_PREFIX_REGULAR;
175
+ }
176
+
177
+ const rawKey = this.generateRandomRawKeyString(prefix);
178
+ const keyHash = await bcrypt.hash(rawKey, 10);
179
+ const createdAt = new Date().toISOString();
180
+
181
+ try {
182
+ this.db.run(
183
+ 'INSERT INTO api_keys (id, name, description, key_hash, key_prefix, key_type, server_id, regular_key_id, created_at, is_active) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1)',
184
+ [keyId, name, description, keyHash, prefix, keyType, serverId, regularKeyId, createdAt]
185
+ );
186
+ this.save();
187
+ return { keyId, rawKey, keyPrefix: prefix, keyType, serverId, regularKeyId };
188
+ } catch (error) {
189
+ throw new Error('Could not generate unique API key hash');
190
+ }
191
+ }
192
+
193
+ async verifyApiKey(keyToCheck) {
194
+ const stmt = this.db.prepare('SELECT id, key_hash, key_type, server_id, regular_key_id FROM api_keys WHERE is_active = 1');
195
+
196
+ while (stmt.step()) {
197
+ const row = stmt.getAsObject();
198
+ const isValid = await bcrypt.compare(keyToCheck, row.key_hash);
199
+
200
+ if (isValid) {
201
+ stmt.free();
202
+
203
+ this.db.run(
204
+ 'UPDATE api_keys SET last_used = ? WHERE id = ?',
205
+ [new Date().toISOString(), row.id]
206
+ );
207
+ this.save();
208
+
209
+ return {
210
+ id: row.id,
211
+ keyType: row.key_type,
212
+ serverId: row.server_id,
213
+ regularKeyId: row.regular_key_id
214
+ };
215
+ }
216
+ }
217
+
218
+ stmt.free();
219
+ return null;
220
+ }
221
+
222
+ getApiKeyDetailsById(keyId) {
223
+ const stmt = this.db.prepare(
224
+ 'SELECT id, name, description, key_prefix, key_type, server_id, regular_key_id, created_at, last_used, is_active FROM api_keys WHERE id = ?'
225
+ );
226
+ stmt.bind([keyId]);
227
+
228
+ if (stmt.step()) {
229
+ const row = stmt.getAsObject();
230
+ stmt.free();
231
+ return {
232
+ id: row.id,
233
+ name: row.name,
234
+ description: row.description,
235
+ keyPrefix: row.key_prefix,
236
+ keyType: row.key_type,
237
+ serverId: row.server_id,
238
+ regularKeyId: row.regular_key_id,
239
+ createdAt: row.created_at,
240
+ lastUsed: row.last_used,
241
+ isActive: Boolean(row.is_active)
242
+ };
243
+ }
244
+
245
+ stmt.free();
246
+ return null;
247
+ }
248
+
249
+ getAllApiKeysInfo() {
250
+ const stmt = this.db.prepare(
251
+ 'SELECT id, name, description, key_prefix, key_type, server_id, regular_key_id, created_at, last_used, is_active FROM api_keys ORDER BY created_at DESC'
252
+ );
253
+
254
+ const keys = [];
255
+ while (stmt.step()) {
256
+ const row = stmt.getAsObject();
257
+ keys.push({
258
+ id: row.id,
259
+ name: row.name,
260
+ description: row.description,
261
+ keyPrefix: row.key_prefix,
262
+ keyType: row.key_type,
263
+ serverId: row.server_id,
264
+ regularKeyId: row.regular_key_id,
265
+ createdAt: row.created_at,
266
+ lastUsed: row.last_used,
267
+ isActive: Boolean(row.is_active)
268
+ });
269
+ }
270
+
271
+ stmt.free();
272
+ return keys;
273
+ }
274
+
275
+ getServerKeysByRegularKeyId(regularKeyId) {
276
+ const stmt = this.db.prepare(
277
+ 'SELECT id, name, description, key_prefix, key_type, server_id, created_at, last_used, is_active FROM api_keys WHERE regular_key_id = ? AND key_type = ? ORDER BY created_at DESC'
278
+ );
279
+ stmt.bind([regularKeyId, KEY_TYPE_SERVER]);
280
+
281
+ const keys = [];
282
+ while (stmt.step()) {
283
+ const row = stmt.getAsObject();
284
+ keys.push({
285
+ id: row.id,
286
+ name: row.name,
287
+ description: row.description,
288
+ keyPrefix: row.key_prefix,
289
+ keyType: row.key_type,
290
+ serverId: row.server_id,
291
+ createdAt: row.created_at,
292
+ lastUsed: row.last_used,
293
+ isActive: Boolean(row.is_active)
294
+ });
295
+ }
296
+
297
+ stmt.free();
298
+ return keys;
299
+ }
300
+
301
+ toggleApiKeyActivation(keyId, activate) {
302
+ const result = this.db.run(
303
+ 'UPDATE api_keys SET is_active = ? WHERE id = ?',
304
+ [activate ? 1 : 0, keyId]
305
+ );
306
+ this.save();
307
+ return result.changes > 0;
308
+ }
309
+
310
+ deleteApiKeyById(keyId) {
311
+ const keyInfo = this.getApiKeyDetailsById(keyId);
312
+
313
+ if (keyInfo && keyInfo.keyType === KEY_TYPE_ADMIN) {
314
+ const stmt = this.db.prepare(
315
+ 'SELECT COUNT(*) as count FROM api_keys WHERE key_type = ? AND id != ?'
316
+ );
317
+ stmt.bind([KEY_TYPE_ADMIN, keyId]);
318
+ stmt.step();
319
+ const row = stmt.getAsObject();
320
+ stmt.free();
321
+
322
+ if (row.count === 0) {
323
+ throw new Error('Cannot delete the last Admin Key');
324
+ }
325
+ }
326
+
327
+ const result = this.db.run('DELETE FROM api_keys WHERE id = ?', [keyId]);
328
+ this.save();
329
+ return result.changes > 0;
330
+ }
331
+
332
+ logEvent(event, apiKeyId) {
333
+ this.db.run(
334
+ 'INSERT INTO event_logs (event_type, server_name, timestamp, data, api_key_id) VALUES (?, ?, ?, ?, ?)',
335
+ [event.event_type, event.server_name, event.timestamp, JSON.stringify(event.data), apiKeyId]
336
+ );
337
+ this.save();
338
+ }
339
+
340
+ getRecentEvents(limit = 100) {
341
+ const stmt = this.db.prepare(
342
+ 'SELECT * FROM event_logs ORDER BY timestamp DESC LIMIT ?'
343
+ );
344
+ stmt.bind([limit]);
345
+
346
+ const events = [];
347
+ while (stmt.step()) {
348
+ const row = stmt.getAsObject();
349
+ events.push({
350
+ id: row.id,
351
+ eventType: row.event_type,
352
+ serverName: row.server_name,
353
+ timestamp: row.timestamp,
354
+ data: JSON.parse(row.data),
355
+ apiKeyId: row.api_key_id
356
+ });
357
+ }
358
+
359
+ stmt.free();
360
+ return events;
361
+ }
362
+ }
363
+
364
+ module.exports = Database;
365
+ module.exports.KEY_TYPE_ADMIN = KEY_TYPE_ADMIN;
366
+ module.exports.KEY_TYPE_SERVER = KEY_TYPE_SERVER;
367
+ module.exports.KEY_TYPE_REGULAR = KEY_TYPE_REGULAR;
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/src/routes/events.js ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const express = require('express');
2
+ const router = express.Router();
3
+
4
+ function createEventsRouter(db, manager, requireAnyKey) {
5
+ router.post('/', requireAnyKey, async (req, res) => {
6
+ try {
7
+ const event = req.body;
8
+
9
+ if (!event.event_type || !event.server_name || !event.timestamp || !event.data) {
10
+ return res.status(400).json({ detail: 'Missing required event fields' });
11
+ }
12
+
13
+ db.logEvent(event, req.apiKey.id);
14
+
15
+ const message = {
16
+ type: 'minecraft_event',
17
+ event: event,
18
+ source_key_id_prefix: req.apiKey.id.substring(0, 8)
19
+ };
20
+
21
+ await manager.broadcastToAll(message);
22
+
23
+ res.json({ message: 'Event received and broadcasted' });
24
+ } catch (error) {
25
+ res.status(500).json({ detail: error.message });
26
+ }
27
+ });
28
+
29
+ return router;
30
+ }
31
+
32
+ module.exports = createEventsRouter;
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/src/routes/health.js ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const express = require('express');
2
+ const router = express.Router();
3
+ const { KEY_TYPE_ADMIN, KEY_TYPE_SERVER, KEY_TYPE_REGULAR } = require('../database');
4
+
5
+ function createHealthRouter(db, manager) {
6
+ router.get('/', (req, res) => {
7
+ try {
8
+ const allKeys = db.getAllApiKeysInfo();
9
+
10
+ res.json({
11
+ status: 'healthy',
12
+ timestamp: new Date().toISOString(),
13
+ active_ws: manager.getActiveConnectionsCount(),
14
+ keys_total: allKeys.length,
15
+ admin_active: allKeys.filter(k => k.keyType === KEY_TYPE_ADMIN && k.isActive).length,
16
+ server_active: allKeys.filter(k => k.keyType === KEY_TYPE_SERVER && k.isActive).length,
17
+ regular_active: allKeys.filter(k => k.keyType === KEY_TYPE_REGULAR && k.isActive).length
18
+ });
19
+ } catch (error) {
20
+ res.status(500).json({ status: 'unhealthy', error: error.message });
21
+ }
22
+ });
23
+
24
+ return router;
25
+ }
26
+
27
+ module.exports = createHealthRouter;
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/src/routes/keys.js ADDED
@@ -0,0 +1,195 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const express = require('express');
2
+ const router = express.Router();
3
+ const { KEY_TYPE_ADMIN, KEY_TYPE_SERVER, KEY_TYPE_REGULAR } = require('../database');
4
+ const { requireRegularKeyForOwnServerKeys } = require('../auth');
5
+
6
+ function createKeysRouter(db, requireAdminKey) {
7
+ const verifyApiKey = require('../auth').verifyApiKey(db);
8
+ router.post('/', requireAdminKey, async (req, res) => {
9
+ try {
10
+ const { name, description = '', key_type = KEY_TYPE_REGULAR, server_id = null } = req.body;
11
+
12
+ if (!name) {
13
+ return res.status(400).json({ detail: 'Name is required' });
14
+ }
15
+
16
+ if (key_type === KEY_TYPE_REGULAR) {
17
+ // 创建Regular Key并自动生成关联的Server Key
18
+ const result = await db.createRegularKeyWithServerKey(name, description, server_id);
19
+
20
+ res.status(201).json({
21
+ regularKey: {
22
+ ...db.getApiKeyDetailsById(result.regularKey.id),
23
+ key: result.regularKey.rawKey
24
+ },
25
+ serverKey: {
26
+ ...db.getApiKeyDetailsById(result.serverKey.id),
27
+ key: result.serverKey.rawKey
28
+ }
29
+ });
30
+ } else {
31
+ // 创建单个Admin或Server Key
32
+ const result = await db.createApiKey(name, description, key_type, server_id);
33
+ const details = db.getApiKeyDetailsById(result.keyId);
34
+
35
+ res.status(201).json({
36
+ ...details,
37
+ key: result.rawKey
38
+ });
39
+ }
40
+ } catch (error) {
41
+ res.status(500).json({ detail: error.message });
42
+ }
43
+ });
44
+
45
+ router.get('/', requireAdminKey, (req, res) => {
46
+ try {
47
+ const keys = db.getAllApiKeysInfo();
48
+ res.json(keys);
49
+ } catch (error) {
50
+ res.status(500).json({ detail: error.message });
51
+ }
52
+ });
53
+
54
+ // Regular Key获取自己的Server Key列表
55
+ router.get('/server-keys', verifyApiKey(db), requireRegularOrAdminKey, (req, res) => {
56
+ try {
57
+ if (req.apiKey.keyType === KEY_TYPE_ADMIN) {
58
+ // Admin获取所有Server Key
59
+ const allKeys = db.getAllApiKeysInfo();
60
+ const serverKeys = allKeys.filter(key => key.keyType === KEY_TYPE_SERVER);
61
+ res.json(serverKeys);
62
+ } else if (req.apiKey.keyType === KEY_TYPE_REGULAR) {
63
+ // Regular Key获取自己关联的Server Key
64
+ const serverKeys = db.getServerKeysByRegularKeyId(req.apiKey.id);
65
+ res.json(serverKeys);
66
+ }
67
+ } catch (error) {
68
+ res.status(500).json({ detail: error.message });
69
+ }
70
+ });
71
+
72
+ // Regular Key为自己创建新的Server Key
73
+ // 只有Admin Key可以为Regular Key创建Server Key
74
+ router.post('/server-keys', verifyApiKey(db), requireAdminKey, async (req, res) => {
75
+ try {
76
+ const { name, description = '', server_id = null, regular_key_id } = req.body;
77
+
78
+ if (!name) {
79
+ return res.status(400).json({ detail: 'Name is required' });
80
+ }
81
+
82
+ if (!regular_key_id) {
83
+ return res.status(400).json({ detail: 'regular_key_id is required' });
84
+ }
85
+
86
+ const regularKeyInfo = db.getApiKeyDetailsById(regular_key_id);
87
+ if (!regularKeyInfo || regularKeyInfo.keyType !== KEY_TYPE_REGULAR) {
88
+ return res.status(404).json({ detail: 'Invalid Regular Key ID' });
89
+ }
90
+
91
+ const result = await db.createApiKey(name, description, KEY_TYPE_SERVER, server_id, regular_key_id);
92
+ const details = db.getApiKeyDetailsById(result.keyId);
93
+
94
+ res.status(201).json({
95
+ ...details,
96
+ key: result.rawKey
97
+ });
98
+ } catch (error) {
99
+ res.status(500).json({ detail: error.message });
100
+ }
101
+ });
102
+
103
+ router.get('/:key_id', verifyApiKey, requireRegularKeyForOwnServerKeys(db), (req, res) => {
104
+ try {
105
+ const details = db.getApiKeyDetailsById(req.params.key_id);
106
+
107
+ if (!details) {
108
+ return res.status(404).json({ detail: 'API Key not found' });
109
+ }
110
+
111
+ res.json(details);
112
+ } catch (error) {
113
+ res.status(500).json({ detail: error.message });
114
+ }
115
+ });
116
+
117
+ router.patch('/:key_id/activate', verifyApiKey, requireRegularKeyForOwnServerKeys(db), (req, res) => {
118
+ try {
119
+ const keyId = req.params.key_id;
120
+
121
+ if (!db.getApiKeyDetailsById(keyId)) {
122
+ return res.status(404).json({ detail: 'Key not found' });
123
+ }
124
+
125
+ if (db.toggleApiKeyActivation(keyId, true)) {
126
+ return res.json({ message: `Key '${keyId}' activated.` });
127
+ }
128
+
129
+ res.status(500).json({ detail: 'Failed to activate key.' });
130
+ } catch (error) {
131
+ res.status(500).json({ detail: error.message });
132
+ }
133
+ });
134
+
135
+ router.patch('/:key_id/deactivate', verifyApiKey, requireRegularKeyForOwnServerKeys(db), (req, res) => {
136
+ try {
137
+ const keyId = req.params.key_id;
138
+ const keyInfo = db.getApiKeyDetailsById(keyId);
139
+
140
+ if (!keyInfo) {
141
+ return res.status(404).json({ detail: 'Key not found' });
142
+ }
143
+
144
+ // 不能删除自己
145
+ if (keyId === req.apiKey.id) {
146
+ return res.status(400).json({ detail: 'Cannot deactivate your own key.' });
147
+ }
148
+
149
+ // 管理员不能删除最后一个Admin Key
150
+ if (req.apiKey.keyType === KEY_TYPE_ADMIN && keyInfo.keyType === KEY_TYPE_ADMIN) {
151
+ const allKeys = db.getAllApiKeysInfo();
152
+ const activeAdmin = allKeys.filter(k => k.keyType === KEY_TYPE_ADMIN && k.isActive && k.id !== keyId);
153
+
154
+ if (activeAdmin.length === 0) {
155
+ return res.status(400).json({ detail: 'Cannot deactivate last active Admin Key.' });
156
+ }
157
+ }
158
+
159
+ if (db.toggleApiKeyActivation(keyId, false)) {
160
+ return res.json({ message: `Key '${keyId}' deactivated.` });
161
+ }
162
+
163
+ res.status(500).json({ detail: 'Failed to deactivate key.' });
164
+ } catch (error) {
165
+ res.status(500).json({ detail: error.message });
166
+ }
167
+ });
168
+
169
+ router.delete('/:key_id', verifyApiKey, requireRegularKeyForOwnServerKeys(db), (req, res) => {
170
+ try {
171
+ const keyId = req.params.key_id;
172
+
173
+ if (!db.getApiKeyDetailsById(keyId)) {
174
+ return res.status(404).json({ detail: 'Key not found' });
175
+ }
176
+
177
+ // 不能删除自己
178
+ if (keyId === req.apiKey.id) {
179
+ return res.status(400).json({ detail: 'Cannot delete your own key.' });
180
+ }
181
+
182
+ db.deleteApiKeyById(keyId);
183
+ res.status(204).send();
184
+ } catch (error) {
185
+ if (error.message.includes('last Admin Key')) {
186
+ return res.status(400).json({ detail: error.message });
187
+ }
188
+ res.status(500).json({ detail: error.message });
189
+ }
190
+ });
191
+
192
+ return router;
193
+ }
194
+
195
+ module.exports = createKeysRouter;
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/src/routes/server.js ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const express = require('express');
2
+ const router = express.Router();
3
+
4
+ function createServerRouter(db, manager, requireServerKey) {
5
+ // 获取服务器信息
6
+ router.get('/info', requireServerKey, (req, res) => {
7
+ try {
8
+ // 这里应该从实际的Minecraft服务器获取信息
9
+ // 目前返回模拟数据
10
+ const serverInfo = {
11
+ server_id: req.apiKey.serverId || 'server-001',
12
+ status: 'running',
13
+ online_players: 5,
14
+ max_players: 20,
15
+ version: '1.20.1',
16
+ uptime: '2h 30m',
17
+ tps: 19.8
18
+ };
19
+
20
+ res.json(serverInfo);
21
+ } catch (error) {
22
+ res.status(500).json({ detail: error.message });
23
+ }
24
+ });
25
+
26
+ // 发送服务器命令
27
+ router.post('/command', requireServerKey, (req, res) => {
28
+ try {
29
+ const { command } = req.body;
30
+
31
+ if (!command) {
32
+ return res.status(400).json({ detail: 'Command is required' });
33
+ }
34
+
35
+ // 这里应该向实际的Minecraft服务器发送命令
36
+ // 目前只是记录命令
37
+ console.log(`[Server Command] ${req.apiKey.serverId}: ${command}`);
38
+
39
+ // 广播命令执行事件
40
+ const commandEvent = {
41
+ event_type: 'server_command',
42
+ server_name: req.apiKey.serverId || 'unknown',
43
+ timestamp: new Date().toISOString(),
44
+ data: {
45
+ command: command,
46
+ executed_by: req.apiKey.id,
47
+ server_id: req.apiKey.serverId
48
+ }
49
+ };
50
+
51
+ manager.broadcastToAll({
52
+ type: 'minecraft_event',
53
+ event: commandEvent,
54
+ source_key_id_prefix: req.apiKey.id.substring(0, 8)
55
+ });
56
+
57
+ res.json({
58
+ message: 'Command sent successfully',
59
+ command: command,
60
+ server_id: req.apiKey.serverId
61
+ });
62
+ } catch (error) {
63
+ res.status(500).json({ detail: error.message });
64
+ }
65
+ });
66
+
67
+ return router;
68
+ }
69
+
70
+ module.exports = createServerRouter;
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/src/server.js ADDED
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ require('dotenv').config();
2
+ const express = require('express');
3
+ const path = require('path');
4
+ const { WebSocketServer } = require('ws');
5
+ const http = require('http');
6
+ const Database = require('./database');
7
+ const ConnectionManager = require('./websocket');
8
+ const { verifyApiKey, requireAdminKey, requireServerKey, requireAnyKey } = require('./auth');
9
+ const createKeysRouter = require('./routes/keys');
10
+ const createEventsRouter = require('./routes/events');
11
+ const createHealthRouter = require('./routes/health');
12
+ const createServerRouter = require('./routes/server');
13
+
14
+ const HOST = process.env.SERVER_HOST || '0.0.0.0';
15
+ const PORT = parseInt(process.env.SERVER_PORT || '8000');
16
+
17
+ const app = express();
18
+ const server = http.createServer(app);
19
+ const wss = new WebSocketServer({ noServer: true });
20
+
21
+ const db = new Database(process.env.DATABASE_PATH || 'minecraft_ws.db');
22
+ const manager = new ConnectionManager();
23
+
24
+ app.use(express.json());
25
+
26
+ app.use('/dashboard', express.static(path.join(__dirname, '../dashboard/public')));
27
+
28
+ app.get('/', (req, res) => {
29
+ res.json({
30
+ message: 'Minecraft WebSocket API Server (Node.js)',
31
+ dashboard: '/dashboard',
32
+ websocket: 'ws://' + req.get('host') + '/ws',
33
+ version: '1.0.0'
34
+ });
35
+ });
36
+
37
+ app.use('/manage/keys', verifyApiKey(db), requireAdminKey, createKeysRouter(db, requireAdminKey));
38
+ app.use('/api/events', verifyApiKey(db), requireAnyKey, createEventsRouter(db, manager, requireAnyKey));
39
+ app.use('/health', createHealthRouter(db, manager));
40
+
41
+ server.on('upgrade', async (request, socket, head) => {
42
+ const url = new URL(request.url, `http://${request.headers.host}`);
43
+
44
+ if (url.pathname === '/ws') {
45
+ const apiKey = url.searchParams.get('api_key');
46
+
47
+ if (!apiKey) {
48
+ socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
49
+ socket.destroy();
50
+ return;
51
+ }
52
+
53
+ const result = await db.verifyApiKey(apiKey);
54
+
55
+ if (!result) {
56
+ socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
57
+ socket.destroy();
58
+ return;
59
+ }
60
+
61
+ wss.handleUpgrade(request, socket, head, (ws) => {
62
+ wss.emit('connection', ws, request, result);
63
+ });
64
+ } else {
65
+ socket.destroy();
66
+ }
67
+ });
68
+
69
+ wss.on('connection', (ws, request, apiKeyInfo) => {
70
+ manager.connect(ws, apiKeyInfo.id);
71
+
72
+ ws.on('message', (data) => {
73
+ try {
74
+ const msg = JSON.parse(data.toString());
75
+
76
+ if (msg.type === 'ping') {
77
+ ws.send(JSON.stringify({ type: 'pong' }));
78
+ }
79
+ } catch (error) {
80
+ }
81
+ });
82
+
83
+ ws.on('close', () => {
84
+ manager.disconnect(ws);
85
+ });
86
+
87
+ ws.on('error', () => {
88
+ manager.disconnect(ws);
89
+ });
90
+ });
91
+
92
+ async function startServer() {
93
+ console.log('🚀 启动Minecraft WebSocket API服务器...');
94
+ console.log('='.repeat(50));
95
+
96
+ await db.init();
97
+
98
+ const adminKeyInfo = await db.ensureInitialAdminKey();
99
+
100
+ if (adminKeyInfo) {
101
+ console.log('='.repeat(60));
102
+ console.log('重要: 已生成新的Admin Key!');
103
+ console.log(` 名称: ${adminKeyInfo.name}`);
104
+ console.log(` 密钥: ${adminKeyInfo.key}`);
105
+ console.log('请复制并安全保存此密钥。');
106
+ console.log('您需要使用它来管理API密钥。');
107
+ console.log('如果丢失此密钥且没有其他Admin Key,您可能失去管理员访问权限。');
108
+ console.log('='.repeat(60));
109
+ } else {
110
+ console.log('信息: 已找到现有Admin Key或Admin Key检查已执行。');
111
+ }
112
+
113
+ server.listen(PORT, HOST, () => {
114
+ console.log(`服务器地址: http://${HOST}:${PORT}`);
115
+ console.log(`WebSocket端点: ws://${HOST}:${PORT}/ws`);
116
+ console.log(`健康检查: http://${HOST}:${PORT}/health`);
117
+ console.log('='.repeat(50));
118
+ console.log();
119
+ console.log('💡 首次使用提示:');
120
+ console.log('1. 使用CLI工具创建API密钥:');
121
+ console.log(' node cli/cli.js create-key "MyServer"');
122
+ console.log();
123
+ console.log('2. 生成Minecraft插件配置文件:');
124
+ console.log(' node cli/cli.js generate-config');
125
+ console.log();
126
+ console.log('3. 将API密钥配置到Minecraft插件中');
127
+ console.log('='.repeat(50));
128
+ });
129
+ }
130
+
131
+ startServer().catch(error => {
132
+ console.error('❌ 启动失败:', error);
133
+ process.exit(1);
134
+ });
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/src/websocket.js ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class ConnectionManager {
2
+ constructor() {
3
+ this.activeConnections = new Map();
4
+ this.connectionKeys = new Map();
5
+ }
6
+
7
+ connect(websocket, apiKeyId) {
8
+ if (!this.activeConnections.has(apiKeyId)) {
9
+ this.activeConnections.set(apiKeyId, new Set());
10
+ }
11
+ this.activeConnections.get(apiKeyId).add(websocket);
12
+ this.connectionKeys.set(websocket, apiKeyId);
13
+ }
14
+
15
+ disconnect(websocket) {
16
+ const apiKeyId = this.connectionKeys.get(websocket);
17
+
18
+ if (apiKeyId && this.activeConnections.has(apiKeyId)) {
19
+ this.activeConnections.get(apiKeyId).delete(websocket);
20
+
21
+ if (this.activeConnections.get(apiKeyId).size === 0) {
22
+ this.activeConnections.delete(apiKeyId);
23
+ }
24
+ }
25
+
26
+ this.connectionKeys.delete(websocket);
27
+ }
28
+
29
+ async broadcastToAll(message) {
30
+ if (this.activeConnections.size === 0) return;
31
+
32
+ const messageStr = JSON.stringify(message);
33
+ const disconnectedWebsockets = [];
34
+
35
+ for (const connectionsSet of this.activeConnections.values()) {
36
+ for (const connection of connectionsSet) {
37
+ try {
38
+ if (connection.readyState === 1) {
39
+ connection.send(messageStr);
40
+ } else {
41
+ disconnectedWebsockets.push(connection);
42
+ }
43
+ } catch (error) {
44
+ disconnectedWebsockets.push(connection);
45
+ }
46
+ }
47
+ }
48
+
49
+ for (const ws of disconnectedWebsockets) {
50
+ this.disconnect(ws);
51
+ }
52
+ }
53
+
54
+ getActiveConnectionsCount() {
55
+ let count = 0;
56
+ for (const connectionsSet of this.activeConnections.values()) {
57
+ count += connectionsSet.size;
58
+ }
59
+ return count;
60
+ }
61
+ }
62
+
63
+ module.exports = ConnectionManager;
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/start-server.bat ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ @echo off
2
+ echo Starting Minecraft WebSocket API Server...
3
+ node src/server.js
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/start.sh ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ echo "🚀 启动Minecraft WebSocket API服务器..."
4
+ echo "=================================================="
5
+
6
+ if [ ! -d "node_modules" ]; then
7
+ echo "📦 检测到缺少依赖,正在安装..."
8
+ npm install
9
+ fi
10
+
11
+ node src/server.js