Spaces:
Sleeping
Sleeping
GitHub Actions
commited on
Commit
·
6d43d9c
1
Parent(s):
17bd284
Sync from GitHub (excluding README)
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- dashboard/public/app.js +210 -17
- dashboard/public/index.html +41 -0
- dashboard/public/style.css +235 -204
- docs/API.md +28 -1
- docs/openapi.json +930 -0
- docs/openapi.yaml +512 -0
- hf_repo/docs/TEST_REPORT.md +69 -0
- hf_repo/hf_repo/.gitignore +1 -0
- hf_repo/hf_repo/hf_repo/cli/cli.js +20 -2
- hf_repo/hf_repo/hf_repo/dashboard/public/app.js +160 -31
- hf_repo/hf_repo/hf_repo/dashboard/public/index.html +10 -13
- hf_repo/hf_repo/hf_repo/dashboard/public/style.css +550 -324
- hf_repo/hf_repo/hf_repo/docs/UI_UX_DESIGN_2026.md +72 -0
- hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/dashboard/public/index.html +43 -43
- hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/cli/cli.js +9 -9
- hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/dashboard/server.js +3 -3
- hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/docs/protocol.md +38 -40
- hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/docs/protocol.md +125 -0
- hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/dashboard/public/app.js +178 -0
- hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/dashboard/public/index.html +38 -0
- hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/dashboard/public/style.css +85 -0
- hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/docs/AI_API.md +415 -0
- hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/docs/API.md +730 -0
- hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/docs/TECH_STACK.md +149 -0
- 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 +32 -0
- 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 +448 -285
- 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 +78 -41
- 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 +31 -6
- 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 +4 -4
- hf_repo/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 +48 -79
- hf_repo/hf_repo/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 +46 -116
- hf_repo/hf_repo/hf_repo/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 +116 -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/.github/scripts/ai_agent.py +86 -79
- hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/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
- hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/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
- hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/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
- hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/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
- hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/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
- hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/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
- hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/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
- hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/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
- hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/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
- hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/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
- hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/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
- hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/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
- hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/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
- hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/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
- hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/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
- hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/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
- hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/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
dashboard/public/app.js
CHANGED
|
@@ -20,6 +20,12 @@ 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');
|
|
@@ -263,35 +269,19 @@ function renderAdminKeys(keys) {
|
|
| 263 |
|
| 264 |
let html = '';
|
| 265 |
|
| 266 |
-
// Render Admin Keys
|
| 267 |
if (adminKeys.length > 0) {
|
| 268 |
html += '<h3 class="group-title">管理员密钥 (Admin Keys)</h3>';
|
| 269 |
html += adminKeys.map(key => renderKeyCard(key)).join('');
|
| 270 |
}
|
| 271 |
|
| 272 |
-
// Render Regular Keys with nested Server Keys
|
| 273 |
if (regularKeys.length > 0) {
|
| 274 |
html += '<h3 class="group-title">用户密钥 (Regular Keys)</h3>';
|
| 275 |
html += regularKeys.map(regularKey => {
|
| 276 |
const childServerKeys = serverKeysMap[regularKey.id] || [];
|
| 277 |
-
return
|
| 278 |
-
<div class="regular-key-group">
|
| 279 |
-
<div class="regular-key-header" onclick="toggleGroup(this)">
|
| 280 |
-
${renderKeyCard(regularKey)}
|
| 281 |
-
${childServerKeys.length > 0 ? `<span class="toggle-icon">▼</span>` : ''}
|
| 282 |
-
</div>
|
| 283 |
-
${childServerKeys.length > 0 ? `
|
| 284 |
-
<div class="nested-server-keys hidden">
|
| 285 |
-
<h4>关联的服务器密钥 (Linked Server Keys)</h4>
|
| 286 |
-
${childServerKeys.map(serverKey => renderKeyCard(serverKey, true)).join('')}
|
| 287 |
-
</div>
|
| 288 |
-
` : ''}
|
| 289 |
-
</div>
|
| 290 |
-
`;
|
| 291 |
}).join('');
|
| 292 |
}
|
| 293 |
|
| 294 |
-
// Render Orphaned Server Keys (if any)
|
| 295 |
const linkedServerKeyIds = new Set(Object.values(serverKeysMap).flat().map(k => k.id));
|
| 296 |
const orphanServerKeys = serverKeys.filter(k => !linkedServerKeyIds.has(k.id));
|
| 297 |
|
|
@@ -339,12 +329,54 @@ function renderKeyCard(key, isNested = false) {
|
|
| 339 |
? `<button class="btn-danger" onclick="deactivateKey('${key.id}')">停用</button>`
|
| 340 |
: `<button class="btn-success" onclick="activateKey('${key.id}')">启用</button>`
|
| 341 |
}
|
|
|
|
| 342 |
<button class="btn-danger" onclick="deleteKey('${key.id}', '${key.name}')">删除</button>
|
| 343 |
</div>
|
| 344 |
</div>
|
| 345 |
`;
|
| 346 |
}
|
| 347 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 348 |
function renderUserServerKeys(keys) {
|
| 349 |
if (!userKeysList) {
|
| 350 |
return;
|
|
@@ -376,6 +408,7 @@ function renderUserServerKeys(keys) {
|
|
| 376 |
? `<button class="btn-danger" onclick="deactivateKey('${key.id}')">停用</button>`
|
| 377 |
: `<button class="btn-success" onclick="activateKey('${key.id}')">启用</button>`
|
| 378 |
}
|
|
|
|
| 379 |
</div>
|
| 380 |
</div>
|
| 381 |
`).join('');
|
|
@@ -658,6 +691,166 @@ if (closeDetailsBtn) {
|
|
| 658 |
});
|
| 659 |
}
|
| 660 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 661 |
if (createKeyForm) {
|
| 662 |
createKeyForm.addEventListener('submit', async (event) => {
|
| 663 |
event.preventDefault();
|
|
|
|
| 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 createServerKeyModal = document.getElementById('create-server-key-modal');
|
| 24 |
+
const createServerKeyForm = document.getElementById('create-server-key-form');
|
| 25 |
+
const cancelServerKeyBtn = document.getElementById('cancel-server-key-btn');
|
| 26 |
+
const refreshKeyModal = document.getElementById('refresh-key-modal');
|
| 27 |
+
const cancelRefreshBtn = document.getElementById('cancel-refresh-btn');
|
| 28 |
+
const confirmRefreshBtn = document.getElementById('confirm-refresh-btn');
|
| 29 |
const adminKeysList = document.getElementById('admin-keys-list');
|
| 30 |
const userKeysList = document.getElementById('user-keys-list');
|
| 31 |
const commandForm = document.getElementById('command-form');
|
|
|
|
| 269 |
|
| 270 |
let html = '';
|
| 271 |
|
|
|
|
| 272 |
if (adminKeys.length > 0) {
|
| 273 |
html += '<h3 class="group-title">管理员密钥 (Admin Keys)</h3>';
|
| 274 |
html += adminKeys.map(key => renderKeyCard(key)).join('');
|
| 275 |
}
|
| 276 |
|
|
|
|
| 277 |
if (regularKeys.length > 0) {
|
| 278 |
html += '<h3 class="group-title">用户密钥 (Regular Keys)</h3>';
|
| 279 |
html += regularKeys.map(regularKey => {
|
| 280 |
const childServerKeys = serverKeysMap[regularKey.id] || [];
|
| 281 |
+
return renderRegularKeyCard(regularKey, childServerKeys);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 282 |
}).join('');
|
| 283 |
}
|
| 284 |
|
|
|
|
| 285 |
const linkedServerKeyIds = new Set(Object.values(serverKeysMap).flat().map(k => k.id));
|
| 286 |
const orphanServerKeys = serverKeys.filter(k => !linkedServerKeyIds.has(k.id));
|
| 287 |
|
|
|
|
| 329 |
? `<button class="btn-danger" onclick="deactivateKey('${key.id}')">停用</button>`
|
| 330 |
: `<button class="btn-success" onclick="activateKey('${key.id}')">启用</button>`
|
| 331 |
}
|
| 332 |
+
<button class="btn-secondary" onclick="showRefreshKeyModal('${key.id}', '${key.name}')">刷新</button>
|
| 333 |
<button class="btn-danger" onclick="deleteKey('${key.id}', '${key.name}')">删除</button>
|
| 334 |
</div>
|
| 335 |
</div>
|
| 336 |
`;
|
| 337 |
}
|
| 338 |
|
| 339 |
+
function renderRegularKeyCard(regularKey, childServerKeys = []) {
|
| 340 |
+
return `
|
| 341 |
+
<div class="regular-key-group">
|
| 342 |
+
<div class="regular-key-header" onclick="toggleGroup(this)">
|
| 343 |
+
<div class="key-card">
|
| 344 |
+
<div class="key-info">
|
| 345 |
+
<h3>
|
| 346 |
+
<span class="key-badge regular">Regular</span>
|
| 347 |
+
<span class="key-badge ${regularKey.isActive ? 'active' : 'inactive'}">
|
| 348 |
+
${regularKey.isActive ? '已启用' : '已停用'}
|
| 349 |
+
</span>
|
| 350 |
+
${regularKey.name}
|
| 351 |
+
</h3>
|
| 352 |
+
<p>ID: ${regularKey.id}</p>
|
| 353 |
+
<p>Prefix: ${regularKey.keyPrefix}</p>
|
| 354 |
+
${regularKey.serverId ? `<p>Server ID: ${regularKey.serverId}</p>` : ''}
|
| 355 |
+
<p>创建时间: ${new Date(regularKey.createdAt).toLocaleString()}</p>
|
| 356 |
+
<p>最后使用: ${regularKey.lastUsed ? new Date(regularKey.lastUsed).toLocaleString() : '从未'}</p>
|
| 357 |
+
</div>
|
| 358 |
+
<div class="key-actions">
|
| 359 |
+
${regularKey.isActive
|
| 360 |
+
? `<button class="btn-danger" onclick="deactivateKey('${regularKey.id}')">停用</button>`
|
| 361 |
+
: `<button class="btn-success" onclick="activateKey('${regularKey.id}')">启用</button>`
|
| 362 |
+
}
|
| 363 |
+
<button class="btn-secondary" onclick="showRefreshKeyModal('${regularKey.id}', '${regularKey.name}')">刷新</button>
|
| 364 |
+
<button class="btn-danger" onclick="deleteKey('${regularKey.id}', '${regularKey.name}')">删除</button>
|
| 365 |
+
<button class="btn-primary" onclick="showCreateServerKeyModal('${regularKey.id}', '${regularKey.name}')">添加 Server Key</button>
|
| 366 |
+
</div>
|
| 367 |
+
</div>
|
| 368 |
+
${childServerKeys.length > 0 ? `<span class="toggle-icon">▼</span>` : ''}
|
| 369 |
+
</div>
|
| 370 |
+
${childServerKeys.length > 0 ? `
|
| 371 |
+
<div class="nested-server-keys hidden">
|
| 372 |
+
<h4>关联的服务器密钥 (Linked Server Keys)</h4>
|
| 373 |
+
${childServerKeys.map(serverKey => renderKeyCard(serverKey, true)).join('')}
|
| 374 |
+
</div>
|
| 375 |
+
` : ''}
|
| 376 |
+
</div>
|
| 377 |
+
`;
|
| 378 |
+
}
|
| 379 |
+
|
| 380 |
function renderUserServerKeys(keys) {
|
| 381 |
if (!userKeysList) {
|
| 382 |
return;
|
|
|
|
| 408 |
? `<button class="btn-danger" onclick="deactivateKey('${key.id}')">停用</button>`
|
| 409 |
: `<button class="btn-success" onclick="activateKey('${key.id}')">启用</button>`
|
| 410 |
}
|
| 411 |
+
<button class="btn-secondary" onclick="showRefreshKeyModal('${key.id}', '${key.name}')">刷新</button>
|
| 412 |
</div>
|
| 413 |
</div>
|
| 414 |
`).join('');
|
|
|
|
| 691 |
});
|
| 692 |
}
|
| 693 |
|
| 694 |
+
function showCreateServerKeyModal(regularKeyId, regularKeyName) {
|
| 695 |
+
if (!createServerKeyModal) {
|
| 696 |
+
return;
|
| 697 |
+
}
|
| 698 |
+
|
| 699 |
+
const parentNameEl = document.getElementById('create-server-key-parent-name');
|
| 700 |
+
if (parentNameEl) {
|
| 701 |
+
parentNameEl.textContent = `父级 Regular Key: ${regularKeyName}`;
|
| 702 |
+
}
|
| 703 |
+
|
| 704 |
+
const regularIdInput = document.getElementById('server-key-regular-id');
|
| 705 |
+
if (regularIdInput) {
|
| 706 |
+
regularIdInput.value = regularKeyId;
|
| 707 |
+
}
|
| 708 |
+
|
| 709 |
+
createServerKeyModal.classList.remove('hidden');
|
| 710 |
+
}
|
| 711 |
+
|
| 712 |
+
function hideCreateServerKeyModal() {
|
| 713 |
+
if (!createServerKeyModal) {
|
| 714 |
+
return;
|
| 715 |
+
}
|
| 716 |
+
|
| 717 |
+
createServerKeyModal.classList.add('hidden');
|
| 718 |
+
|
| 719 |
+
const nameInput = document.getElementById('server-key-name');
|
| 720 |
+
const descInput = document.getElementById('server-key-description');
|
| 721 |
+
const serverIdInput = document.getElementById('server-key-server-id');
|
| 722 |
+
|
| 723 |
+
if (nameInput) nameInput.value = '';
|
| 724 |
+
if (descInput) descInput.value = '';
|
| 725 |
+
if (serverIdInput) serverIdInput.value = '';
|
| 726 |
+
}
|
| 727 |
+
|
| 728 |
+
async function createServerKey(event) {
|
| 729 |
+
event.preventDefault();
|
| 730 |
+
|
| 731 |
+
const regularKeyId = document.getElementById('server-key-regular-id')?.value;
|
| 732 |
+
const name = document.getElementById('server-key-name')?.value.trim();
|
| 733 |
+
const description = document.getElementById('server-key-description')?.value.trim();
|
| 734 |
+
const serverId = document.getElementById('server-key-server-id')?.value.trim();
|
| 735 |
+
|
| 736 |
+
if (!regularKeyId || !name) {
|
| 737 |
+
alert('Regular Key ID and name are required.');
|
| 738 |
+
return;
|
| 739 |
+
}
|
| 740 |
+
|
| 741 |
+
try {
|
| 742 |
+
const payload = {
|
| 743 |
+
name,
|
| 744 |
+
description,
|
| 745 |
+
regular_key_id: regularKeyId
|
| 746 |
+
};
|
| 747 |
+
|
| 748 |
+
if (serverId) {
|
| 749 |
+
payload.server_id = serverId;
|
| 750 |
+
}
|
| 751 |
+
|
| 752 |
+
const response = await fetch(`${API_URL}/manage/keys/server-keys`, {
|
| 753 |
+
method: 'POST',
|
| 754 |
+
headers: {
|
| 755 |
+
...authHeaders(apiKey),
|
| 756 |
+
'Content-Type': 'application/json'
|
| 757 |
+
},
|
| 758 |
+
body: JSON.stringify(payload)
|
| 759 |
+
});
|
| 760 |
+
|
| 761 |
+
if (!response.ok) {
|
| 762 |
+
const error = await response.json();
|
| 763 |
+
alert(`Failed to create server key: ${error.detail}`);
|
| 764 |
+
return;
|
| 765 |
+
}
|
| 766 |
+
|
| 767 |
+
const result = await response.json();
|
| 768 |
+
hideCreateServerKeyModal();
|
| 769 |
+
showKeyCreatedModal(result);
|
| 770 |
+
loadAdminKeys();
|
| 771 |
+
} catch (error) {
|
| 772 |
+
alert(`Failed to create server key: ${error.message}`);
|
| 773 |
+
}
|
| 774 |
+
}
|
| 775 |
+
|
| 776 |
+
if (cancelServerKeyBtn) {
|
| 777 |
+
cancelServerKeyBtn.addEventListener('click', hideCreateServerKeyModal);
|
| 778 |
+
}
|
| 779 |
+
|
| 780 |
+
if (createServerKeyForm) {
|
| 781 |
+
createServerKeyForm.addEventListener('submit', createServerKey);
|
| 782 |
+
}
|
| 783 |
+
|
| 784 |
+
window.showCreateServerKeyModal = showCreateServerKeyModal;
|
| 785 |
+
window.hideCreateServerKeyModal = hideCreateServerKeyModal;
|
| 786 |
+
window.createServerKey = createServerKey;
|
| 787 |
+
|
| 788 |
+
function showRefreshKeyModal(keyId, keyName) {
|
| 789 |
+
if (!refreshKeyModal) {
|
| 790 |
+
return;
|
| 791 |
+
}
|
| 792 |
+
|
| 793 |
+
const keyNameEl = document.getElementById('refresh-key-name');
|
| 794 |
+
const keyIdInput = document.getElementById('refresh-key-id');
|
| 795 |
+
|
| 796 |
+
if (keyNameEl) keyNameEl.textContent = keyName;
|
| 797 |
+
if (keyIdInput) keyIdInput.value = keyId;
|
| 798 |
+
|
| 799 |
+
refreshKeyModal.classList.remove('hidden');
|
| 800 |
+
}
|
| 801 |
+
|
| 802 |
+
function hideRefreshKeyModal() {
|
| 803 |
+
if (!refreshKeyModal) {
|
| 804 |
+
return;
|
| 805 |
+
}
|
| 806 |
+
refreshKeyModal.classList.add('hidden');
|
| 807 |
+
}
|
| 808 |
+
|
| 809 |
+
async function refreshKey() {
|
| 810 |
+
const keyId = document.getElementById('refresh-key-id')?.value;
|
| 811 |
+
|
| 812 |
+
if (!keyId || !apiKey) {
|
| 813 |
+
return;
|
| 814 |
+
}
|
| 815 |
+
|
| 816 |
+
try {
|
| 817 |
+
const response = await fetch(`${API_URL}/manage/keys/${keyId}/refresh`, {
|
| 818 |
+
method: 'POST',
|
| 819 |
+
headers: authHeaders(apiKey)
|
| 820 |
+
});
|
| 821 |
+
|
| 822 |
+
if (!response.ok) {
|
| 823 |
+
const error = await response.json();
|
| 824 |
+
alert(`刷新失败: ${error.detail}`);
|
| 825 |
+
return;
|
| 826 |
+
}
|
| 827 |
+
|
| 828 |
+
const result = await response.json();
|
| 829 |
+
hideRefreshKeyModal();
|
| 830 |
+
showKeyCreatedModal(result);
|
| 831 |
+
|
| 832 |
+
if (currentRole === 'admin') {
|
| 833 |
+
loadAdminKeys();
|
| 834 |
+
} else if (currentRole === 'regular') {
|
| 835 |
+
loadUserServerKeys();
|
| 836 |
+
}
|
| 837 |
+
} catch (error) {
|
| 838 |
+
alert(`刷新失败: ${error.message}`);
|
| 839 |
+
}
|
| 840 |
+
}
|
| 841 |
+
|
| 842 |
+
if (cancelRefreshBtn) {
|
| 843 |
+
cancelRefreshBtn.addEventListener('click', hideRefreshKeyModal);
|
| 844 |
+
}
|
| 845 |
+
|
| 846 |
+
if (confirmRefreshBtn) {
|
| 847 |
+
confirmRefreshBtn.addEventListener('click', refreshKey);
|
| 848 |
+
}
|
| 849 |
+
|
| 850 |
+
window.showRefreshKeyModal = showRefreshKeyModal;
|
| 851 |
+
window.hideRefreshKeyModal = hideRefreshKeyModal;
|
| 852 |
+
window.refreshKey = refreshKey;
|
| 853 |
+
|
| 854 |
if (createKeyForm) {
|
| 855 |
createKeyForm.addEventListener('submit', async (event) => {
|
| 856 |
event.preventDefault();
|
dashboard/public/index.html
CHANGED
|
@@ -170,6 +170,47 @@
|
|
| 170 |
</div>
|
| 171 |
</div>
|
| 172 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 173 |
<script src="app.js"></script>
|
| 174 |
</body>
|
| 175 |
</html>
|
|
|
|
| 170 |
</div>
|
| 171 |
</div>
|
| 172 |
|
| 173 |
+
<div id="create-server-key-modal" class="modal hidden">
|
| 174 |
+
<div class="modal-content">
|
| 175 |
+
<h2>为普通密钥创建服务器密钥</h2>
|
| 176 |
+
<p id="create-server-key-parent-name" class="modal-parent-info"></p>
|
| 177 |
+
<form id="create-server-key-form">
|
| 178 |
+
<input type="hidden" id="server-key-regular-id">
|
| 179 |
+
|
| 180 |
+
<label>名称 *</label>
|
| 181 |
+
<input type="text" id="server-key-name" required>
|
| 182 |
+
|
| 183 |
+
<label>描述</label>
|
| 184 |
+
<textarea id="server-key-description"></textarea>
|
| 185 |
+
|
| 186 |
+
<label>服务器 ID(可选)</label>
|
| 187 |
+
<input type="text" id="server-key-server-id">
|
| 188 |
+
|
| 189 |
+
<div class="modal-actions">
|
| 190 |
+
<button type="button" id="cancel-server-key-btn" class="btn-secondary">取消</button>
|
| 191 |
+
<button type="submit" class="btn-primary">创建</button>
|
| 192 |
+
</div>
|
| 193 |
+
</form>
|
| 194 |
+
</div>
|
| 195 |
+
</div>
|
| 196 |
+
|
| 197 |
+
<div id="refresh-key-modal" class="modal hidden">
|
| 198 |
+
<div class="modal-content">
|
| 199 |
+
<h2>刷新密钥</h2>
|
| 200 |
+
<div class="modal-parent-info danger-bg">
|
| 201 |
+
<p>⚠️ <strong>警告:此操作不可撤销!</strong></p>
|
| 202 |
+
<p>刷新密钥将立即使旧密钥失效。</p>
|
| 203 |
+
<p>任何使用旧密钥连接的插件或服务都将断开连接,直到您更新为新密钥。</p>
|
| 204 |
+
</div>
|
| 205 |
+
<p>您确定要刷新密钥 <strong id="refresh-key-name"></strong> 吗?</p>
|
| 206 |
+
<input type="hidden" id="refresh-key-id">
|
| 207 |
+
<div class="modal-actions">
|
| 208 |
+
<button id="cancel-refresh-btn" class="btn-secondary">取消</button>
|
| 209 |
+
<button id="confirm-refresh-btn" class="btn-danger">确认刷新</button>
|
| 210 |
+
</div>
|
| 211 |
+
</div>
|
| 212 |
+
</div>
|
| 213 |
+
|
| 214 |
<script src="app.js"></script>
|
| 215 |
</body>
|
| 216 |
</html>
|
dashboard/public/style.css
CHANGED
|
@@ -1,29 +1,31 @@
|
|
| 1 |
:root {
|
| 2 |
-
/* Colors - Light Theme (
|
| 3 |
-
--bg-body: #
|
| 4 |
--bg-card: #ffffff;
|
| 5 |
-
--bg-card-hover: #
|
| 6 |
--bg-input: #ffffff;
|
| 7 |
|
| 8 |
-
--border-color: #
|
| 9 |
-
--border-hover: #
|
| 10 |
|
| 11 |
-
|
| 12 |
-
--primary
|
| 13 |
-
--primary-
|
|
|
|
| 14 |
|
| 15 |
-
|
| 16 |
-
--danger
|
|
|
|
| 17 |
|
| 18 |
-
--success: #
|
| 19 |
-
--success-hover: #
|
| 20 |
|
| 21 |
-
--text-main: #
|
| 22 |
-
--text-muted: #
|
| 23 |
|
| 24 |
/* Spacing & Radius */
|
| 25 |
--radius-sm: 6px;
|
| 26 |
-
--radius-md:
|
| 27 |
--radius-lg: 16px;
|
| 28 |
--radius-xl: 24px;
|
| 29 |
|
|
@@ -35,33 +37,34 @@
|
|
| 35 |
|
| 36 |
/* Effects */
|
| 37 |
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
| 38 |
-
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.
|
| 39 |
-
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.
|
| 40 |
-
--glass-blur: blur(
|
| 41 |
--transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
| 42 |
}
|
| 43 |
|
| 44 |
[data-theme="dark"] {
|
| 45 |
-
/* Colors - Dark Theme */
|
| 46 |
--bg-body: #09090b;
|
| 47 |
-
--bg-card: rgba(
|
| 48 |
-
--bg-card-hover: rgba(
|
| 49 |
-
--bg-input: rgba(
|
| 50 |
|
| 51 |
--border-color: rgba(255, 255, 255, 0.08);
|
| 52 |
-
--border-hover: rgba(255, 255, 255, 0.
|
| 53 |
|
| 54 |
-
|
| 55 |
-
--primary
|
| 56 |
-
--primary-
|
|
|
|
| 57 |
|
| 58 |
-
--danger: #
|
| 59 |
-
--danger-hover: #
|
| 60 |
|
| 61 |
-
--success: #
|
| 62 |
-
--success-hover: #
|
| 63 |
|
| 64 |
-
--text-main: #
|
| 65 |
--text-muted: #a1a1aa;
|
| 66 |
|
| 67 |
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.5);
|
|
@@ -84,26 +87,41 @@ body {
|
|
| 84 |
-webkit-font-smoothing: antialiased;
|
| 85 |
}
|
| 86 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
[data-theme="dark"] body {
|
| 88 |
background-image:
|
| 89 |
-
radial-gradient(circle at
|
| 90 |
-
radial-gradient(circle at
|
| 91 |
}
|
| 92 |
|
| 93 |
/* Scrollbar */
|
| 94 |
::-webkit-scrollbar {
|
| 95 |
-
width:
|
| 96 |
-
height:
|
| 97 |
}
|
| 98 |
::-webkit-scrollbar-track {
|
| 99 |
background: transparent;
|
| 100 |
}
|
| 101 |
::-webkit-scrollbar-thumb {
|
| 102 |
-
background:
|
| 103 |
-
border-radius:
|
| 104 |
}
|
| 105 |
::-webkit-scrollbar-thumb:hover {
|
| 106 |
-
background:
|
| 107 |
}
|
| 108 |
|
| 109 |
/* Utilities */
|
|
@@ -115,11 +133,11 @@ body {
|
|
| 115 |
min-height: 100vh;
|
| 116 |
display: flex;
|
| 117 |
flex-direction: column;
|
| 118 |
-
animation: fadeIn 0.
|
| 119 |
}
|
| 120 |
|
| 121 |
@keyframes fadeIn {
|
| 122 |
-
from { opacity: 0; transform: translateY(
|
| 123 |
to { opacity: 1; transform: translateY(0); }
|
| 124 |
}
|
| 125 |
|
|
@@ -131,18 +149,16 @@ body {
|
|
| 131 |
align-items: center;
|
| 132 |
justify-content: center;
|
| 133 |
padding: var(--space-xl);
|
| 134 |
-
background: radial-gradient(circle at center, rgba(99, 102, 241, 0.1) 0%, transparent 70%);
|
| 135 |
}
|
| 136 |
|
| 137 |
.login-container h1 {
|
| 138 |
font-size: 2.5rem;
|
| 139 |
font-weight: 800;
|
| 140 |
-
letter-spacing: -0.
|
| 141 |
margin-bottom: var(--space-xs);
|
| 142 |
-
background: linear-gradient(135deg,
|
| 143 |
-webkit-background-clip: text;
|
| 144 |
-webkit-text-fill-color: transparent;
|
| 145 |
-
text-shadow: 0 0 30px rgba(99, 102, 241, 0.3);
|
| 146 |
}
|
| 147 |
|
| 148 |
.login-container h2 {
|
|
@@ -155,23 +171,23 @@ body {
|
|
| 155 |
#login-form {
|
| 156 |
background: var(--bg-card);
|
| 157 |
backdrop-filter: var(--glass-blur);
|
| 158 |
-
padding:
|
| 159 |
border-radius: var(--radius-xl);
|
| 160 |
border: 1px solid var(--border-color);
|
| 161 |
box-shadow: var(--shadow-lg);
|
| 162 |
width: 100%;
|
| 163 |
-
max-width:
|
| 164 |
transition: var(--transition);
|
| 165 |
}
|
| 166 |
|
| 167 |
#login-form input {
|
| 168 |
width: 100%;
|
| 169 |
-
padding: 16px;
|
| 170 |
background: var(--bg-input);
|
| 171 |
border: 1px solid var(--border-color);
|
| 172 |
border-radius: var(--radius-md);
|
| 173 |
color: var(--text-main);
|
| 174 |
-
font-size:
|
| 175 |
margin-bottom: var(--space-lg);
|
| 176 |
transition: var(--transition);
|
| 177 |
}
|
|
@@ -179,17 +195,17 @@ body {
|
|
| 179 |
#login-form input:focus {
|
| 180 |
outline: none;
|
| 181 |
border-color: var(--primary);
|
| 182 |
-
box-shadow: 0 0 0
|
| 183 |
}
|
| 184 |
|
| 185 |
#login-form button {
|
| 186 |
width: 100%;
|
| 187 |
-
padding:
|
| 188 |
-
background:
|
| 189 |
color: white;
|
| 190 |
border: none;
|
| 191 |
border-radius: var(--radius-md);
|
| 192 |
-
font-size:
|
| 193 |
font-weight: 600;
|
| 194 |
cursor: pointer;
|
| 195 |
transition: var(--transition);
|
|
@@ -197,15 +213,15 @@ body {
|
|
| 197 |
}
|
| 198 |
|
| 199 |
#login-form button:hover {
|
| 200 |
-
|
| 201 |
-
|
| 202 |
}
|
| 203 |
|
| 204 |
.error-message {
|
| 205 |
color: var(--danger);
|
| 206 |
margin-top: var(--space-md);
|
| 207 |
text-align: center;
|
| 208 |
-
font-size: 0.
|
| 209 |
min-height: 20px;
|
| 210 |
}
|
| 211 |
|
|
@@ -213,17 +229,15 @@ body {
|
|
| 213 |
margin-top: var(--space-xl);
|
| 214 |
text-align: center;
|
| 215 |
color: var(--text-muted);
|
| 216 |
-
font-size: 0.
|
| 217 |
-
background: rgba(255, 255, 255, 0.03);
|
| 218 |
padding: var(--space-md) var(--space-lg);
|
| 219 |
border-radius: var(--radius-lg);
|
| 220 |
-
border: 1px solid var(--border-color);
|
| 221 |
}
|
| 222 |
|
| 223 |
/* Navbar */
|
| 224 |
.navbar {
|
| 225 |
-
background: var(--bg-card);
|
| 226 |
-
backdrop-filter: blur(
|
| 227 |
padding: 16px 32px;
|
| 228 |
display: flex;
|
| 229 |
justify-content: space-between;
|
|
@@ -235,10 +249,23 @@ body {
|
|
| 235 |
}
|
| 236 |
|
| 237 |
.navbar h1 {
|
| 238 |
-
font-size: 1.
|
| 239 |
font-weight: 700;
|
| 240 |
-
letter-spacing: -0.
|
| 241 |
color: var(--text-main);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 242 |
}
|
| 243 |
|
| 244 |
.nav-info {
|
|
@@ -249,7 +276,8 @@ body {
|
|
| 249 |
|
| 250 |
#user-info, #admin-user-info {
|
| 251 |
font-size: 0.85rem;
|
| 252 |
-
|
|
|
|
| 253 |
background: var(--bg-card);
|
| 254 |
padding: 6px 12px;
|
| 255 |
border-radius: 20px;
|
|
@@ -258,61 +286,44 @@ body {
|
|
| 258 |
|
| 259 |
.logout-btn {
|
| 260 |
padding: 8px 16px;
|
| 261 |
-
background:
|
| 262 |
-
color: var(--
|
| 263 |
-
border: 1px solid
|
| 264 |
-
border-radius: var(--radius-
|
| 265 |
cursor: pointer;
|
| 266 |
font-size: 0.85rem;
|
| 267 |
-
font-weight:
|
| 268 |
transition: var(--transition);
|
| 269 |
}
|
| 270 |
|
| 271 |
.logout-btn:hover {
|
| 272 |
-
background:
|
|
|
|
| 273 |
border-color: var(--danger);
|
| 274 |
}
|
| 275 |
|
| 276 |
/* Theme Toggle */
|
| 277 |
.theme-toggle {
|
| 278 |
background: transparent;
|
| 279 |
-
border:
|
| 280 |
color: var(--text-muted);
|
| 281 |
-
padding:
|
| 282 |
-
border-radius:
|
| 283 |
cursor: pointer;
|
| 284 |
-
font-size: 0.85rem;
|
| 285 |
-
font-weight: 600;
|
| 286 |
transition: var(--transition);
|
| 287 |
display: flex;
|
| 288 |
align-items: center;
|
| 289 |
justify-content: center;
|
| 290 |
-
height: auto;
|
| 291 |
-
width: auto;
|
| 292 |
-
white-space: nowrap;
|
| 293 |
}
|
| 294 |
|
| 295 |
.theme-toggle:hover {
|
| 296 |
-
background: var(--bg-card-hover);
|
| 297 |
color: var(--text-main);
|
| 298 |
-
border-color: var(--border-hover);
|
| 299 |
-
}
|
| 300 |
-
|
| 301 |
-
[data-theme="light"] .theme-toggle {
|
| 302 |
-
background: var(--bg-input);
|
| 303 |
-
color: var(--text-main);
|
| 304 |
-
border-color: var(--border-color);
|
| 305 |
-
}
|
| 306 |
-
|
| 307 |
-
[data-theme="light"] .theme-toggle:hover {
|
| 308 |
background: var(--bg-card-hover);
|
| 309 |
-
border-color: var(--primary);
|
| 310 |
-
color: var(--primary);
|
| 311 |
}
|
| 312 |
|
| 313 |
/* Main Container */
|
| 314 |
.container {
|
| 315 |
-
max-width:
|
| 316 |
margin: 0 auto;
|
| 317 |
padding: var(--space-xl);
|
| 318 |
width: 100%;
|
|
@@ -321,7 +332,7 @@ body {
|
|
| 321 |
/* Stats Grid */
|
| 322 |
.stats-grid {
|
| 323 |
display: grid;
|
| 324 |
-
grid-template-columns: repeat(auto-fit, minmax(
|
| 325 |
gap: var(--space-md);
|
| 326 |
margin-bottom: var(--space-xl);
|
| 327 |
}
|
|
@@ -331,32 +342,29 @@ body {
|
|
| 331 |
padding: var(--space-lg);
|
| 332 |
border-radius: var(--radius-lg);
|
| 333 |
border: 1px solid var(--border-color);
|
| 334 |
-
|
| 335 |
-
text-align: center;
|
| 336 |
transition: var(--transition);
|
| 337 |
}
|
| 338 |
|
| 339 |
.stat-card:hover {
|
| 340 |
-
border-color: var(--
|
| 341 |
-
|
| 342 |
-
background: var(--bg-card-hover);
|
| 343 |
}
|
| 344 |
|
| 345 |
.stat-card h3 {
|
| 346 |
color: var(--text-muted);
|
| 347 |
-
font-size: 0.
|
|
|
|
| 348 |
text-transform: uppercase;
|
| 349 |
letter-spacing: 0.05em;
|
| 350 |
-
margin-bottom: var(--space-
|
| 351 |
}
|
| 352 |
|
| 353 |
.stat-value {
|
| 354 |
-
font-size: 2.
|
| 355 |
font-weight: 700;
|
| 356 |
color: var(--text-main);
|
| 357 |
-
|
| 358 |
-
-webkit-background-clip: text;
|
| 359 |
-
-webkit-text-fill-color: transparent;
|
| 360 |
}
|
| 361 |
|
| 362 |
/* Section */
|
|
@@ -366,7 +374,7 @@ body {
|
|
| 366 |
border-radius: var(--radius-lg);
|
| 367 |
border: 1px solid var(--border-color);
|
| 368 |
margin-bottom: var(--space-xl);
|
| 369 |
-
|
| 370 |
}
|
| 371 |
|
| 372 |
.section-header {
|
|
@@ -374,19 +382,19 @@ body {
|
|
| 374 |
justify-content: space-between;
|
| 375 |
align-items: center;
|
| 376 |
margin-bottom: var(--space-lg);
|
| 377 |
-
border-bottom: 1px solid var(--border-color);
|
| 378 |
padding-bottom: var(--space-md);
|
|
|
|
| 379 |
}
|
| 380 |
|
| 381 |
.section h2 {
|
| 382 |
-
font-size: 1.
|
| 383 |
font-weight: 600;
|
| 384 |
color: var(--text-main);
|
| 385 |
}
|
| 386 |
|
| 387 |
/* Buttons */
|
| 388 |
.btn-primary, .btn-secondary, .btn-danger, .btn-success {
|
| 389 |
-
padding: 10px
|
| 390 |
border-radius: var(--radius-md);
|
| 391 |
font-weight: 500;
|
| 392 |
cursor: pointer;
|
|
@@ -396,46 +404,46 @@ body {
|
|
| 396 |
align-items: center;
|
| 397 |
justify-content: center;
|
| 398 |
gap: 8px;
|
|
|
|
| 399 |
}
|
| 400 |
|
| 401 |
.btn-primary {
|
| 402 |
background: var(--primary);
|
| 403 |
color: white;
|
| 404 |
-
|
| 405 |
-
box-shadow: 0 2px 8px rgba(99, 102, 241, 0.3);
|
| 406 |
}
|
| 407 |
.btn-primary:hover {
|
| 408 |
background: var(--primary-hover);
|
| 409 |
-
|
| 410 |
}
|
| 411 |
|
| 412 |
.btn-secondary {
|
| 413 |
-
background:
|
| 414 |
color: var(--text-main);
|
| 415 |
-
border:
|
| 416 |
}
|
| 417 |
.btn-secondary:hover {
|
| 418 |
-
background:
|
| 419 |
border-color: var(--border-hover);
|
| 420 |
}
|
| 421 |
|
| 422 |
.btn-danger {
|
| 423 |
-
background:
|
| 424 |
color: var(--danger);
|
| 425 |
-
border:
|
| 426 |
}
|
| 427 |
.btn-danger:hover {
|
| 428 |
-
background: rgba(239, 68, 68, 0.
|
| 429 |
border-color: var(--danger);
|
| 430 |
}
|
| 431 |
|
| 432 |
.btn-success {
|
| 433 |
-
background:
|
| 434 |
color: var(--success);
|
| 435 |
-
border:
|
| 436 |
}
|
| 437 |
.btn-success:hover {
|
| 438 |
-
background: rgba(16, 185, 129, 0.
|
| 439 |
border-color: var(--success);
|
| 440 |
}
|
| 441 |
|
|
@@ -448,45 +456,41 @@ body {
|
|
| 448 |
|
| 449 |
.group-title {
|
| 450 |
color: var(--text-muted);
|
| 451 |
-
font-size: 0.
|
|
|
|
| 452 |
text-transform: uppercase;
|
| 453 |
letter-spacing: 0.05em;
|
| 454 |
margin: var(--space-lg) 0 var(--space-sm) 0;
|
| 455 |
-
padding-bottom: var(--space-xs);
|
| 456 |
-
border-bottom: 1px solid var(--border-color);
|
| 457 |
}
|
| 458 |
|
| 459 |
/* AI Provider Select */
|
| 460 |
#ai-provider-select {
|
| 461 |
margin-bottom: var(--space-md);
|
| 462 |
-
background: var(--bg-input);
|
| 463 |
-
color: var(--text-main);
|
| 464 |
-
border: 1px solid var(--border-color);
|
| 465 |
}
|
|
|
|
| 466 |
/* Key Cards */
|
| 467 |
.key-card {
|
| 468 |
background: var(--bg-card);
|
| 469 |
border: 1px solid var(--border-color);
|
| 470 |
border-radius: var(--radius-md);
|
| 471 |
-
padding: var(--space-lg);
|
| 472 |
display: flex;
|
| 473 |
align-items: center;
|
| 474 |
transition: var(--transition);
|
| 475 |
}
|
| 476 |
|
| 477 |
-
.key-card
|
| 478 |
-
border-color:
|
| 479 |
-
background:
|
| 480 |
}
|
| 481 |
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
border-color: rgba(99, 102, 241, 0.2);
|
| 485 |
}
|
| 486 |
|
| 487 |
.regular-key-group {
|
| 488 |
border: 1px solid var(--border-color);
|
| 489 |
-
border-radius: var(--radius-
|
| 490 |
overflow: hidden;
|
| 491 |
background: var(--bg-card);
|
| 492 |
}
|
|
@@ -505,26 +509,24 @@ body {
|
|
| 505 |
}
|
| 506 |
|
| 507 |
.nested-server-keys {
|
| 508 |
-
padding: 0 var(--space-lg) var(--space-lg) calc(var(--space-lg) +
|
| 509 |
border-top: 1px solid var(--border-color);
|
| 510 |
-
background:
|
| 511 |
-
}
|
| 512 |
-
|
| 513 |
-
[data-theme="light"] .nested-server-keys {
|
| 514 |
-
background: rgba(0, 0, 0, 0.02);
|
| 515 |
}
|
| 516 |
|
| 517 |
.nested-server-keys h4 {
|
| 518 |
color: var(--text-muted);
|
| 519 |
-
font-size: 0.
|
|
|
|
|
|
|
| 520 |
margin: var(--space-md) 0;
|
| 521 |
-
opacity: 0.
|
| 522 |
}
|
| 523 |
|
| 524 |
.key-card.nested {
|
| 525 |
padding: var(--space-md);
|
| 526 |
background: var(--bg-card);
|
| 527 |
-
border
|
| 528 |
}
|
| 529 |
|
| 530 |
.toggle-icon-container {
|
|
@@ -534,25 +536,15 @@ body {
|
|
| 534 |
align-items: center;
|
| 535 |
justify-content: center;
|
| 536 |
margin-right: var(--space-md);
|
| 537 |
-
background:
|
| 538 |
border-radius: 50%;
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
[data-theme="light"] .toggle-icon-container {
|
| 542 |
-
background: rgba(0, 0, 0, 0.05);
|
| 543 |
}
|
| 544 |
|
| 545 |
.toggle-icon {
|
| 546 |
-
font-size: 0.
|
| 547 |
color: var(--text-muted);
|
| 548 |
-
transition: transform 0.
|
| 549 |
-
/* Fix alignment */
|
| 550 |
-
display: flex;
|
| 551 |
-
align-items: center;
|
| 552 |
-
justify-content: center;
|
| 553 |
-
width: 100%;
|
| 554 |
-
height: 100%;
|
| 555 |
-
margin-top: 2px; /* Visual correction */
|
| 556 |
}
|
| 557 |
|
| 558 |
.key-info {
|
|
@@ -562,33 +554,35 @@ body {
|
|
| 562 |
.key-info h3 {
|
| 563 |
display: flex;
|
| 564 |
align-items: center;
|
| 565 |
-
font-size:
|
| 566 |
-
|
|
|
|
| 567 |
color: var(--text-main);
|
| 568 |
}
|
| 569 |
|
| 570 |
.key-info p {
|
| 571 |
color: var(--text-muted);
|
| 572 |
-
font-size: 0.
|
| 573 |
margin-bottom: 2px;
|
| 574 |
font-family: 'JetBrains Mono', monospace;
|
| 575 |
}
|
| 576 |
|
| 577 |
.key-badge {
|
| 578 |
padding: 2px 8px;
|
| 579 |
-
border-radius:
|
| 580 |
font-size: 0.7rem;
|
| 581 |
font-weight: 600;
|
| 582 |
margin-right: var(--space-sm);
|
| 583 |
text-transform: uppercase;
|
|
|
|
| 584 |
}
|
| 585 |
|
| 586 |
-
.key-badge.admin { background:
|
| 587 |
-
.key-badge.regular { background:
|
| 588 |
-
.key-badge.server { background:
|
| 589 |
|
| 590 |
-
.key-badge.active { background: rgba(16, 185, 129, 0.
|
| 591 |
-
.key-badge.inactive {
|
| 592 |
|
| 593 |
.key-actions {
|
| 594 |
display: flex;
|
|
@@ -603,10 +597,10 @@ body {
|
|
| 603 |
|
| 604 |
label {
|
| 605 |
display: block;
|
| 606 |
-
margin-bottom:
|
| 607 |
font-weight: 500;
|
| 608 |
color: var(--text-main);
|
| 609 |
-
font-size: 0.
|
| 610 |
}
|
| 611 |
|
| 612 |
input[type="text"],
|
|
@@ -614,25 +608,25 @@ input[type="password"],
|
|
| 614 |
textarea,
|
| 615 |
select {
|
| 616 |
width: 100%;
|
| 617 |
-
padding:
|
| 618 |
background: var(--bg-input);
|
| 619 |
border: 1px solid var(--border-color);
|
| 620 |
border-radius: var(--radius-md);
|
| 621 |
color: var(--text-main);
|
| 622 |
font-family: inherit;
|
| 623 |
-
font-size: 0.
|
| 624 |
transition: var(--transition);
|
| 625 |
}
|
| 626 |
|
| 627 |
input:focus, textarea:focus, select:focus {
|
| 628 |
outline: none;
|
| 629 |
border-color: var(--primary);
|
| 630 |
-
box-shadow: 0 0 0 3px
|
| 631 |
}
|
| 632 |
|
| 633 |
.hint {
|
| 634 |
display: block;
|
| 635 |
-
margin-top:
|
| 636 |
font-size: 0.8rem;
|
| 637 |
color: var(--text-muted);
|
| 638 |
}
|
|
@@ -641,39 +635,48 @@ input:focus, textarea:focus, select:focus {
|
|
| 641 |
.events-list {
|
| 642 |
max-height: 400px;
|
| 643 |
overflow-y: auto;
|
| 644 |
-
background:
|
|
|
|
| 645 |
border-radius: var(--radius-md);
|
| 646 |
-
padding:
|
| 647 |
}
|
| 648 |
|
| 649 |
.event-item {
|
| 650 |
-
padding:
|
| 651 |
-
border-
|
| 652 |
-
|
| 653 |
-
|
| 654 |
-
|
| 655 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 656 |
}
|
| 657 |
|
| 658 |
.event-item strong {
|
| 659 |
-
color: var(--
|
|
|
|
| 660 |
}
|
| 661 |
|
| 662 |
.event-item pre {
|
| 663 |
-
background:
|
| 664 |
-
padding:
|
| 665 |
border-radius: var(--radius-sm);
|
| 666 |
-
margin-top:
|
| 667 |
color: var(--text-muted);
|
| 668 |
font-size: 0.8rem;
|
| 669 |
-
|
| 670 |
}
|
| 671 |
|
| 672 |
/* Command Console */
|
| 673 |
.command-history {
|
| 674 |
height: 300px;
|
| 675 |
overflow-y: auto;
|
| 676 |
-
background: #
|
| 677 |
border: 1px solid var(--border-color);
|
| 678 |
border-radius: var(--radius-md);
|
| 679 |
padding: var(--space-md);
|
|
@@ -682,17 +685,17 @@ input:focus, textarea:focus, select:focus {
|
|
| 682 |
}
|
| 683 |
|
| 684 |
.command-item {
|
| 685 |
-
margin-bottom:
|
| 686 |
-
padding-bottom:
|
| 687 |
-
border-bottom: 1px solid rgba(255, 255, 255, 0.
|
| 688 |
color: #e2e8f0;
|
| 689 |
-
font-size: 0.
|
| 690 |
}
|
| 691 |
|
| 692 |
.command-item .timestamp {
|
| 693 |
color: #64748b;
|
| 694 |
font-size: 0.75rem;
|
| 695 |
-
margin-top:
|
| 696 |
}
|
| 697 |
|
| 698 |
.command-form {
|
|
@@ -707,8 +710,8 @@ input:focus, textarea:focus, select:focus {
|
|
| 707 |
left: 0;
|
| 708 |
width: 100%;
|
| 709 |
height: 100%;
|
| 710 |
-
background: rgba(0, 0, 0, 0.
|
| 711 |
-
backdrop-filter: blur(
|
| 712 |
display: flex;
|
| 713 |
justify-content: center;
|
| 714 |
align-items: center;
|
|
@@ -718,29 +721,57 @@ input:focus, textarea:focus, select:focus {
|
|
| 718 |
|
| 719 |
.modal-content {
|
| 720 |
background: var(--bg-card);
|
| 721 |
-
padding:
|
| 722 |
border-radius: var(--radius-xl);
|
| 723 |
border: 1px solid var(--border-color);
|
| 724 |
-
|
| 725 |
-
max-width: 500px;
|
| 726 |
-
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.5);
|
| 727 |
-
color: var(--text-main);
|
| 728 |
}
|
| 729 |
|
| 730 |
.modal-actions {
|
| 731 |
-
|
| 732 |
-
|
| 733 |
-
|
| 734 |
-
margin-top: var(--space-xl);
|
| 735 |
}
|
| 736 |
|
| 737 |
/* AI Status */
|
| 738 |
.ai-status {
|
| 739 |
margin-top: var(--space-md);
|
| 740 |
-
padding:
|
| 741 |
border-radius: var(--radius-md);
|
| 742 |
-
font-size: 0.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 743 |
}
|
| 744 |
.ai-status.success { background: rgba(16, 185, 129, 0.1); color: var(--success); border: 1px solid rgba(16, 185, 129, 0.2); }
|
|
|
|
|
|
|
| 745 |
.ai-status.error { background: rgba(239, 68, 68, 0.1); color: var(--danger); border: 1px solid rgba(239, 68, 68, 0.2); }
|
| 746 |
-
.ai-status.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
:root {
|
| 2 |
+
/* Colors - Light Theme (Modern Minimalist) */
|
| 3 |
+
--bg-body: #fafafa;
|
| 4 |
--bg-card: #ffffff;
|
| 5 |
+
--bg-card-hover: #f4f4f5;
|
| 6 |
--bg-input: #ffffff;
|
| 7 |
|
| 8 |
+
--border-color: #e4e4e7;
|
| 9 |
+
--border-hover: #d4d4d8;
|
| 10 |
|
| 11 |
+
/* Primary: Teal/Emerald (Modern & Elegant) */
|
| 12 |
+
--primary: #0d9488;
|
| 13 |
+
--primary-hover: #0f766e;
|
| 14 |
+
--primary-glow: rgba(13, 148, 136, 0.15);
|
| 15 |
|
| 16 |
+
/* Accents */
|
| 17 |
+
--danger: #ef4444;
|
| 18 |
+
--danger-hover: #dc2626;
|
| 19 |
|
| 20 |
+
--success: #10b981;
|
| 21 |
+
--success-hover: #059669;
|
| 22 |
|
| 23 |
+
--text-main: #18181b;
|
| 24 |
+
--text-muted: #71717a;
|
| 25 |
|
| 26 |
/* Spacing & Radius */
|
| 27 |
--radius-sm: 6px;
|
| 28 |
+
--radius-md: 10px;
|
| 29 |
--radius-lg: 16px;
|
| 30 |
--radius-xl: 24px;
|
| 31 |
|
|
|
|
| 37 |
|
| 38 |
/* Effects */
|
| 39 |
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
| 40 |
+
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -1px rgba(0, 0, 0, 0.03);
|
| 41 |
+
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.05), 0 4px 6px -2px rgba(0, 0, 0, 0.02);
|
| 42 |
+
--glass-blur: blur(16px);
|
| 43 |
--transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
| 44 |
}
|
| 45 |
|
| 46 |
[data-theme="dark"] {
|
| 47 |
+
/* Colors - Dark Theme (Slate/Zinc) */
|
| 48 |
--bg-body: #09090b;
|
| 49 |
+
--bg-card: rgba(24, 24, 27, 0.6);
|
| 50 |
+
--bg-card-hover: rgba(39, 39, 42, 0.6);
|
| 51 |
+
--bg-input: rgba(39, 39, 42, 0.5);
|
| 52 |
|
| 53 |
--border-color: rgba(255, 255, 255, 0.08);
|
| 54 |
+
--border-hover: rgba(255, 255, 255, 0.12);
|
| 55 |
|
| 56 |
+
/* Primary: Lighter Teal for Dark Mode */
|
| 57 |
+
--primary: #14b8a6;
|
| 58 |
+
--primary-hover: #2dd4bf;
|
| 59 |
+
--primary-glow: rgba(20, 184, 166, 0.25);
|
| 60 |
|
| 61 |
+
--danger: #f87171;
|
| 62 |
+
--danger-hover: #ef4444;
|
| 63 |
|
| 64 |
+
--success: #34d399;
|
| 65 |
+
--success-hover: #10b981;
|
| 66 |
|
| 67 |
+
--text-main: #f4f4f5;
|
| 68 |
--text-muted: #a1a1aa;
|
| 69 |
|
| 70 |
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.5);
|
|
|
|
| 87 |
-webkit-font-smoothing: antialiased;
|
| 88 |
}
|
| 89 |
|
| 90 |
+
/* Elegant Background Pattern */
|
| 91 |
+
body::before {
|
| 92 |
+
content: '';
|
| 93 |
+
position: fixed;
|
| 94 |
+
top: 0;
|
| 95 |
+
left: 0;
|
| 96 |
+
width: 100%;
|
| 97 |
+
height: 100%;
|
| 98 |
+
pointer-events: none;
|
| 99 |
+
background-image: radial-gradient(circle at 1px 1px, var(--border-color) 1px, transparent 0);
|
| 100 |
+
background-size: 40px 40px;
|
| 101 |
+
opacity: 0.4;
|
| 102 |
+
z-index: -1;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
[data-theme="dark"] body {
|
| 106 |
background-image:
|
| 107 |
+
radial-gradient(circle at 20% 20%, rgba(20, 184, 166, 0.05), transparent 40%),
|
| 108 |
+
radial-gradient(circle at 80% 80%, rgba(20, 184, 166, 0.05), transparent 40%);
|
| 109 |
}
|
| 110 |
|
| 111 |
/* Scrollbar */
|
| 112 |
::-webkit-scrollbar {
|
| 113 |
+
width: 6px;
|
| 114 |
+
height: 6px;
|
| 115 |
}
|
| 116 |
::-webkit-scrollbar-track {
|
| 117 |
background: transparent;
|
| 118 |
}
|
| 119 |
::-webkit-scrollbar-thumb {
|
| 120 |
+
background: var(--border-color);
|
| 121 |
+
border-radius: 3px;
|
| 122 |
}
|
| 123 |
::-webkit-scrollbar-thumb:hover {
|
| 124 |
+
background: var(--text-muted);
|
| 125 |
}
|
| 126 |
|
| 127 |
/* Utilities */
|
|
|
|
| 133 |
min-height: 100vh;
|
| 134 |
display: flex;
|
| 135 |
flex-direction: column;
|
| 136 |
+
animation: fadeIn 0.4s cubic-bezier(0.16, 1, 0.3, 1);
|
| 137 |
}
|
| 138 |
|
| 139 |
@keyframes fadeIn {
|
| 140 |
+
from { opacity: 0; transform: translateY(8px); }
|
| 141 |
to { opacity: 1; transform: translateY(0); }
|
| 142 |
}
|
| 143 |
|
|
|
|
| 149 |
align-items: center;
|
| 150 |
justify-content: center;
|
| 151 |
padding: var(--space-xl);
|
|
|
|
| 152 |
}
|
| 153 |
|
| 154 |
.login-container h1 {
|
| 155 |
font-size: 2.5rem;
|
| 156 |
font-weight: 800;
|
| 157 |
+
letter-spacing: -0.03em;
|
| 158 |
margin-bottom: var(--space-xs);
|
| 159 |
+
background: linear-gradient(135deg, var(--text-main) 0%, var(--text-muted) 100%);
|
| 160 |
-webkit-background-clip: text;
|
| 161 |
-webkit-text-fill-color: transparent;
|
|
|
|
| 162 |
}
|
| 163 |
|
| 164 |
.login-container h2 {
|
|
|
|
| 171 |
#login-form {
|
| 172 |
background: var(--bg-card);
|
| 173 |
backdrop-filter: var(--glass-blur);
|
| 174 |
+
padding: 48px;
|
| 175 |
border-radius: var(--radius-xl);
|
| 176 |
border: 1px solid var(--border-color);
|
| 177 |
box-shadow: var(--shadow-lg);
|
| 178 |
width: 100%;
|
| 179 |
+
max-width: 400px;
|
| 180 |
transition: var(--transition);
|
| 181 |
}
|
| 182 |
|
| 183 |
#login-form input {
|
| 184 |
width: 100%;
|
| 185 |
+
padding: 14px 16px;
|
| 186 |
background: var(--bg-input);
|
| 187 |
border: 1px solid var(--border-color);
|
| 188 |
border-radius: var(--radius-md);
|
| 189 |
color: var(--text-main);
|
| 190 |
+
font-size: 0.95rem;
|
| 191 |
margin-bottom: var(--space-lg);
|
| 192 |
transition: var(--transition);
|
| 193 |
}
|
|
|
|
| 195 |
#login-form input:focus {
|
| 196 |
outline: none;
|
| 197 |
border-color: var(--primary);
|
| 198 |
+
box-shadow: 0 0 0 3px var(--primary-glow);
|
| 199 |
}
|
| 200 |
|
| 201 |
#login-form button {
|
| 202 |
width: 100%;
|
| 203 |
+
padding: 14px;
|
| 204 |
+
background: var(--primary);
|
| 205 |
color: white;
|
| 206 |
border: none;
|
| 207 |
border-radius: var(--radius-md);
|
| 208 |
+
font-size: 0.95rem;
|
| 209 |
font-weight: 600;
|
| 210 |
cursor: pointer;
|
| 211 |
transition: var(--transition);
|
|
|
|
| 213 |
}
|
| 214 |
|
| 215 |
#login-form button:hover {
|
| 216 |
+
background: var(--primary-hover);
|
| 217 |
+
transform: translateY(-1px);
|
| 218 |
}
|
| 219 |
|
| 220 |
.error-message {
|
| 221 |
color: var(--danger);
|
| 222 |
margin-top: var(--space-md);
|
| 223 |
text-align: center;
|
| 224 |
+
font-size: 0.85rem;
|
| 225 |
min-height: 20px;
|
| 226 |
}
|
| 227 |
|
|
|
|
| 229 |
margin-top: var(--space-xl);
|
| 230 |
text-align: center;
|
| 231 |
color: var(--text-muted);
|
| 232 |
+
font-size: 0.85rem;
|
|
|
|
| 233 |
padding: var(--space-md) var(--space-lg);
|
| 234 |
border-radius: var(--radius-lg);
|
|
|
|
| 235 |
}
|
| 236 |
|
| 237 |
/* Navbar */
|
| 238 |
.navbar {
|
| 239 |
+
background: rgba(var(--bg-card), 0.8);
|
| 240 |
+
backdrop-filter: blur(12px);
|
| 241 |
padding: 16px 32px;
|
| 242 |
display: flex;
|
| 243 |
justify-content: space-between;
|
|
|
|
| 249 |
}
|
| 250 |
|
| 251 |
.navbar h1 {
|
| 252 |
+
font-size: 1.1rem;
|
| 253 |
font-weight: 700;
|
| 254 |
+
letter-spacing: -0.01em;
|
| 255 |
color: var(--text-main);
|
| 256 |
+
display: flex;
|
| 257 |
+
align-items: center;
|
| 258 |
+
gap: 12px;
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
.navbar h1::before {
|
| 262 |
+
content: '';
|
| 263 |
+
display: block;
|
| 264 |
+
width: 10px;
|
| 265 |
+
height: 10px;
|
| 266 |
+
background: var(--primary);
|
| 267 |
+
border-radius: 50%;
|
| 268 |
+
box-shadow: 0 0 10px var(--primary);
|
| 269 |
}
|
| 270 |
|
| 271 |
.nav-info {
|
|
|
|
| 276 |
|
| 277 |
#user-info, #admin-user-info {
|
| 278 |
font-size: 0.85rem;
|
| 279 |
+
font-weight: 500;
|
| 280 |
+
color: var(--text-main);
|
| 281 |
background: var(--bg-card);
|
| 282 |
padding: 6px 12px;
|
| 283 |
border-radius: 20px;
|
|
|
|
| 286 |
|
| 287 |
.logout-btn {
|
| 288 |
padding: 8px 16px;
|
| 289 |
+
background: transparent;
|
| 290 |
+
color: var(--text-muted);
|
| 291 |
+
border: 1px solid var(--border-color);
|
| 292 |
+
border-radius: var(--radius-md);
|
| 293 |
cursor: pointer;
|
| 294 |
font-size: 0.85rem;
|
| 295 |
+
font-weight: 500;
|
| 296 |
transition: var(--transition);
|
| 297 |
}
|
| 298 |
|
| 299 |
.logout-btn:hover {
|
| 300 |
+
background: var(--bg-card-hover);
|
| 301 |
+
color: var(--danger);
|
| 302 |
border-color: var(--danger);
|
| 303 |
}
|
| 304 |
|
| 305 |
/* Theme Toggle */
|
| 306 |
.theme-toggle {
|
| 307 |
background: transparent;
|
| 308 |
+
border: none;
|
| 309 |
color: var(--text-muted);
|
| 310 |
+
padding: 8px;
|
| 311 |
+
border-radius: 50%;
|
| 312 |
cursor: pointer;
|
|
|
|
|
|
|
| 313 |
transition: var(--transition);
|
| 314 |
display: flex;
|
| 315 |
align-items: center;
|
| 316 |
justify-content: center;
|
|
|
|
|
|
|
|
|
|
| 317 |
}
|
| 318 |
|
| 319 |
.theme-toggle:hover {
|
|
|
|
| 320 |
color: var(--text-main);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 321 |
background: var(--bg-card-hover);
|
|
|
|
|
|
|
| 322 |
}
|
| 323 |
|
| 324 |
/* Main Container */
|
| 325 |
.container {
|
| 326 |
+
max-width: 1000px;
|
| 327 |
margin: 0 auto;
|
| 328 |
padding: var(--space-xl);
|
| 329 |
width: 100%;
|
|
|
|
| 332 |
/* Stats Grid */
|
| 333 |
.stats-grid {
|
| 334 |
display: grid;
|
| 335 |
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
| 336 |
gap: var(--space-md);
|
| 337 |
margin-bottom: var(--space-xl);
|
| 338 |
}
|
|
|
|
| 342 |
padding: var(--space-lg);
|
| 343 |
border-radius: var(--radius-lg);
|
| 344 |
border: 1px solid var(--border-color);
|
| 345 |
+
text-align: left;
|
|
|
|
| 346 |
transition: var(--transition);
|
| 347 |
}
|
| 348 |
|
| 349 |
.stat-card:hover {
|
| 350 |
+
border-color: var(--primary);
|
| 351 |
+
box-shadow: var(--shadow-md);
|
|
|
|
| 352 |
}
|
| 353 |
|
| 354 |
.stat-card h3 {
|
| 355 |
color: var(--text-muted);
|
| 356 |
+
font-size: 0.8rem;
|
| 357 |
+
font-weight: 600;
|
| 358 |
text-transform: uppercase;
|
| 359 |
letter-spacing: 0.05em;
|
| 360 |
+
margin-bottom: var(--space-xs);
|
| 361 |
}
|
| 362 |
|
| 363 |
.stat-value {
|
| 364 |
+
font-size: 2.2rem;
|
| 365 |
font-weight: 700;
|
| 366 |
color: var(--text-main);
|
| 367 |
+
letter-spacing: -0.03em;
|
|
|
|
|
|
|
| 368 |
}
|
| 369 |
|
| 370 |
/* Section */
|
|
|
|
| 374 |
border-radius: var(--radius-lg);
|
| 375 |
border: 1px solid var(--border-color);
|
| 376 |
margin-bottom: var(--space-xl);
|
| 377 |
+
box-shadow: var(--shadow-sm);
|
| 378 |
}
|
| 379 |
|
| 380 |
.section-header {
|
|
|
|
| 382 |
justify-content: space-between;
|
| 383 |
align-items: center;
|
| 384 |
margin-bottom: var(--space-lg);
|
|
|
|
| 385 |
padding-bottom: var(--space-md);
|
| 386 |
+
border-bottom: 1px solid var(--border-color);
|
| 387 |
}
|
| 388 |
|
| 389 |
.section h2 {
|
| 390 |
+
font-size: 1.1rem;
|
| 391 |
font-weight: 600;
|
| 392 |
color: var(--text-main);
|
| 393 |
}
|
| 394 |
|
| 395 |
/* Buttons */
|
| 396 |
.btn-primary, .btn-secondary, .btn-danger, .btn-success {
|
| 397 |
+
padding: 10px 18px;
|
| 398 |
border-radius: var(--radius-md);
|
| 399 |
font-weight: 500;
|
| 400 |
cursor: pointer;
|
|
|
|
| 404 |
align-items: center;
|
| 405 |
justify-content: center;
|
| 406 |
gap: 8px;
|
| 407 |
+
border: 1px solid transparent;
|
| 408 |
}
|
| 409 |
|
| 410 |
.btn-primary {
|
| 411 |
background: var(--primary);
|
| 412 |
color: white;
|
| 413 |
+
box-shadow: 0 2px 8px var(--primary-glow);
|
|
|
|
| 414 |
}
|
| 415 |
.btn-primary:hover {
|
| 416 |
background: var(--primary-hover);
|
| 417 |
+
transform: translateY(-1px);
|
| 418 |
}
|
| 419 |
|
| 420 |
.btn-secondary {
|
| 421 |
+
background: var(--bg-input);
|
| 422 |
color: var(--text-main);
|
| 423 |
+
border-color: var(--border-color);
|
| 424 |
}
|
| 425 |
.btn-secondary:hover {
|
| 426 |
+
background: var(--bg-card-hover);
|
| 427 |
border-color: var(--border-hover);
|
| 428 |
}
|
| 429 |
|
| 430 |
.btn-danger {
|
| 431 |
+
background: transparent;
|
| 432 |
color: var(--danger);
|
| 433 |
+
border-color: rgba(239, 68, 68, 0.3);
|
| 434 |
}
|
| 435 |
.btn-danger:hover {
|
| 436 |
+
background: rgba(239, 68, 68, 0.1);
|
| 437 |
border-color: var(--danger);
|
| 438 |
}
|
| 439 |
|
| 440 |
.btn-success {
|
| 441 |
+
background: transparent;
|
| 442 |
color: var(--success);
|
| 443 |
+
border-color: rgba(16, 185, 129, 0.3);
|
| 444 |
}
|
| 445 |
.btn-success:hover {
|
| 446 |
+
background: rgba(16, 185, 129, 0.1);
|
| 447 |
border-color: var(--success);
|
| 448 |
}
|
| 449 |
|
|
|
|
| 456 |
|
| 457 |
.group-title {
|
| 458 |
color: var(--text-muted);
|
| 459 |
+
font-size: 0.8rem;
|
| 460 |
+
font-weight: 600;
|
| 461 |
text-transform: uppercase;
|
| 462 |
letter-spacing: 0.05em;
|
| 463 |
margin: var(--space-lg) 0 var(--space-sm) 0;
|
|
|
|
|
|
|
| 464 |
}
|
| 465 |
|
| 466 |
/* AI Provider Select */
|
| 467 |
#ai-provider-select {
|
| 468 |
margin-bottom: var(--space-md);
|
|
|
|
|
|
|
|
|
|
| 469 |
}
|
| 470 |
+
|
| 471 |
/* Key Cards */
|
| 472 |
.key-card {
|
| 473 |
background: var(--bg-card);
|
| 474 |
border: 1px solid var(--border-color);
|
| 475 |
border-radius: var(--radius-md);
|
| 476 |
+
padding: var(--space-md) var(--space-lg);
|
| 477 |
display: flex;
|
| 478 |
align-items: center;
|
| 479 |
transition: var(--transition);
|
| 480 |
}
|
| 481 |
|
| 482 |
+
.key-card:hover {
|
| 483 |
+
border-color: var(--border-hover);
|
| 484 |
+
background: var(--bg-card-hover);
|
| 485 |
}
|
| 486 |
|
| 487 |
+
.key-card.admin {
|
| 488 |
+
border-left: 3px solid var(--primary);
|
|
|
|
| 489 |
}
|
| 490 |
|
| 491 |
.regular-key-group {
|
| 492 |
border: 1px solid var(--border-color);
|
| 493 |
+
border-radius: var(--radius-md);
|
| 494 |
overflow: hidden;
|
| 495 |
background: var(--bg-card);
|
| 496 |
}
|
|
|
|
| 509 |
}
|
| 510 |
|
| 511 |
.nested-server-keys {
|
| 512 |
+
padding: 0 var(--space-lg) var(--space-lg) calc(var(--space-lg) + 24px);
|
| 513 |
border-top: 1px solid var(--border-color);
|
| 514 |
+
background: var(--bg-card-hover);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 515 |
}
|
| 516 |
|
| 517 |
.nested-server-keys h4 {
|
| 518 |
color: var(--text-muted);
|
| 519 |
+
font-size: 0.75rem;
|
| 520 |
+
font-weight: 600;
|
| 521 |
+
text-transform: uppercase;
|
| 522 |
margin: var(--space-md) 0;
|
| 523 |
+
opacity: 0.8;
|
| 524 |
}
|
| 525 |
|
| 526 |
.key-card.nested {
|
| 527 |
padding: var(--space-md);
|
| 528 |
background: var(--bg-card);
|
| 529 |
+
border: 1px solid var(--border-color);
|
| 530 |
}
|
| 531 |
|
| 532 |
.toggle-icon-container {
|
|
|
|
| 536 |
align-items: center;
|
| 537 |
justify-content: center;
|
| 538 |
margin-right: var(--space-md);
|
| 539 |
+
background: var(--bg-input);
|
| 540 |
border-radius: 50%;
|
| 541 |
+
border: 1px solid var(--border-color);
|
|
|
|
|
|
|
|
|
|
| 542 |
}
|
| 543 |
|
| 544 |
.toggle-icon {
|
| 545 |
+
font-size: 0.7rem;
|
| 546 |
color: var(--text-muted);
|
| 547 |
+
transition: transform 0.2s;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 548 |
}
|
| 549 |
|
| 550 |
.key-info {
|
|
|
|
| 554 |
.key-info h3 {
|
| 555 |
display: flex;
|
| 556 |
align-items: center;
|
| 557 |
+
font-size: 0.95rem;
|
| 558 |
+
font-weight: 600;
|
| 559 |
+
margin-bottom: 4px;
|
| 560 |
color: var(--text-main);
|
| 561 |
}
|
| 562 |
|
| 563 |
.key-info p {
|
| 564 |
color: var(--text-muted);
|
| 565 |
+
font-size: 0.8rem;
|
| 566 |
margin-bottom: 2px;
|
| 567 |
font-family: 'JetBrains Mono', monospace;
|
| 568 |
}
|
| 569 |
|
| 570 |
.key-badge {
|
| 571 |
padding: 2px 8px;
|
| 572 |
+
border-radius: 12px;
|
| 573 |
font-size: 0.7rem;
|
| 574 |
font-weight: 600;
|
| 575 |
margin-right: var(--space-sm);
|
| 576 |
text-transform: uppercase;
|
| 577 |
+
letter-spacing: 0.02em;
|
| 578 |
}
|
| 579 |
|
| 580 |
+
.key-badge.admin { background: var(--primary); color: white; }
|
| 581 |
+
.key-badge.regular { background: var(--bg-input); color: var(--text-main); border: 1px solid var(--border-color); }
|
| 582 |
+
.key-badge.server { background: transparent; color: var(--text-muted); border: 1px solid var(--border-color); }
|
| 583 |
|
| 584 |
+
.key-badge.active { color: var(--success); background: rgba(16, 185, 129, 0.1); }
|
| 585 |
+
.key-badge.inactive { color: var(--text-muted); background: var(--bg-input); }
|
| 586 |
|
| 587 |
.key-actions {
|
| 588 |
display: flex;
|
|
|
|
| 597 |
|
| 598 |
label {
|
| 599 |
display: block;
|
| 600 |
+
margin-bottom: 6px;
|
| 601 |
font-weight: 500;
|
| 602 |
color: var(--text-main);
|
| 603 |
+
font-size: 0.85rem;
|
| 604 |
}
|
| 605 |
|
| 606 |
input[type="text"],
|
|
|
|
| 608 |
textarea,
|
| 609 |
select {
|
| 610 |
width: 100%;
|
| 611 |
+
padding: 10px 14px;
|
| 612 |
background: var(--bg-input);
|
| 613 |
border: 1px solid var(--border-color);
|
| 614 |
border-radius: var(--radius-md);
|
| 615 |
color: var(--text-main);
|
| 616 |
font-family: inherit;
|
| 617 |
+
font-size: 0.9rem;
|
| 618 |
transition: var(--transition);
|
| 619 |
}
|
| 620 |
|
| 621 |
input:focus, textarea:focus, select:focus {
|
| 622 |
outline: none;
|
| 623 |
border-color: var(--primary);
|
| 624 |
+
box-shadow: 0 0 0 3px var(--primary-glow);
|
| 625 |
}
|
| 626 |
|
| 627 |
.hint {
|
| 628 |
display: block;
|
| 629 |
+
margin-top: 6px;
|
| 630 |
font-size: 0.8rem;
|
| 631 |
color: var(--text-muted);
|
| 632 |
}
|
|
|
|
| 635 |
.events-list {
|
| 636 |
max-height: 400px;
|
| 637 |
overflow-y: auto;
|
| 638 |
+
background: var(--bg-input);
|
| 639 |
+
border: 1px solid var(--border-color);
|
| 640 |
border-radius: var(--radius-md);
|
| 641 |
+
padding: 0;
|
| 642 |
}
|
| 643 |
|
| 644 |
.event-item {
|
| 645 |
+
padding: 12px 16px;
|
| 646 |
+
border-bottom: 1px solid var(--border-color);
|
| 647 |
+
border-left: 3px solid transparent;
|
| 648 |
+
transition: var(--transition);
|
| 649 |
+
}
|
| 650 |
+
|
| 651 |
+
.event-item:last-child {
|
| 652 |
+
border-bottom: none;
|
| 653 |
+
}
|
| 654 |
+
|
| 655 |
+
.event-item:hover {
|
| 656 |
+
background: var(--bg-card-hover);
|
| 657 |
+
border-left-color: var(--primary);
|
| 658 |
}
|
| 659 |
|
| 660 |
.event-item strong {
|
| 661 |
+
color: var(--text-main);
|
| 662 |
+
font-weight: 600;
|
| 663 |
}
|
| 664 |
|
| 665 |
.event-item pre {
|
| 666 |
+
background: var(--bg-body);
|
| 667 |
+
padding: 10px;
|
| 668 |
border-radius: var(--radius-sm);
|
| 669 |
+
margin-top: 8px;
|
| 670 |
color: var(--text-muted);
|
| 671 |
font-size: 0.8rem;
|
| 672 |
+
border: 1px solid var(--border-color);
|
| 673 |
}
|
| 674 |
|
| 675 |
/* Command Console */
|
| 676 |
.command-history {
|
| 677 |
height: 300px;
|
| 678 |
overflow-y: auto;
|
| 679 |
+
background: #0f172a; /* Always dark for terminal feel */
|
| 680 |
border: 1px solid var(--border-color);
|
| 681 |
border-radius: var(--radius-md);
|
| 682 |
padding: var(--space-md);
|
|
|
|
| 685 |
}
|
| 686 |
|
| 687 |
.command-item {
|
| 688 |
+
margin-bottom: 8px;
|
| 689 |
+
padding-bottom: 8px;
|
| 690 |
+
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
| 691 |
color: #e2e8f0;
|
| 692 |
+
font-size: 0.85rem;
|
| 693 |
}
|
| 694 |
|
| 695 |
.command-item .timestamp {
|
| 696 |
color: #64748b;
|
| 697 |
font-size: 0.75rem;
|
| 698 |
+
margin-top: 4px;
|
| 699 |
}
|
| 700 |
|
| 701 |
.command-form {
|
|
|
|
| 710 |
left: 0;
|
| 711 |
width: 100%;
|
| 712 |
height: 100%;
|
| 713 |
+
background: rgba(0, 0, 0, 0.4);
|
| 714 |
+
backdrop-filter: blur(4px);
|
| 715 |
display: flex;
|
| 716 |
justify-content: center;
|
| 717 |
align-items: center;
|
|
|
|
| 721 |
|
| 722 |
.modal-content {
|
| 723 |
background: var(--bg-card);
|
| 724 |
+
padding: 32px;
|
| 725 |
border-radius: var(--radius-xl);
|
| 726 |
border: 1px solid var(--border-color);
|
| 727 |
+
box-shadow: var(--shadow-lg);
|
|
|
|
|
|
|
|
|
|
| 728 |
}
|
| 729 |
|
| 730 |
.modal-actions {
|
| 731 |
+
margin-top: 32px;
|
| 732 |
+
padding-top: 24px;
|
| 733 |
+
border-top: 1px solid var(--border-color);
|
|
|
|
| 734 |
}
|
| 735 |
|
| 736 |
/* AI Status */
|
| 737 |
.ai-status {
|
| 738 |
margin-top: var(--space-md);
|
| 739 |
+
padding: 12px 16px;
|
| 740 |
border-radius: var(--radius-md);
|
| 741 |
+
font-size: 0.85rem;
|
| 742 |
+
display: flex;
|
| 743 |
+
align-items: center;
|
| 744 |
+
gap: 8px;
|
| 745 |
+
}
|
| 746 |
+
.ai-status::before {
|
| 747 |
+
content: '';
|
| 748 |
+
display: block;
|
| 749 |
+
width: 8px;
|
| 750 |
+
height: 8px;
|
| 751 |
+
border-radius: 50%;
|
| 752 |
}
|
| 753 |
.ai-status.success { background: rgba(16, 185, 129, 0.1); color: var(--success); border: 1px solid rgba(16, 185, 129, 0.2); }
|
| 754 |
+
.ai-status.success::before { background: var(--success); }
|
| 755 |
+
|
| 756 |
.ai-status.error { background: rgba(239, 68, 68, 0.1); color: var(--danger); border: 1px solid rgba(239, 68, 68, 0.2); }
|
| 757 |
+
.ai-status.error::before { background: var(--danger); }
|
| 758 |
+
|
| 759 |
+
.ai-status.info { background: rgba(13, 148, 136, 0.1); color: var(--primary); border: 1px solid rgba(13, 148, 136, 0.2); }
|
| 760 |
+
.ai-status.info::before { background: var(--primary); }
|
| 761 |
+
|
| 762 |
+
.modal-parent-info {
|
| 763 |
+
color: var(--primary);
|
| 764 |
+
font-size: 0.85rem;
|
| 765 |
+
margin-bottom: var(--space-lg);
|
| 766 |
+
padding: 12px;
|
| 767 |
+
background: var(--primary-glow);
|
| 768 |
+
border: 1px solid transparent;
|
| 769 |
+
border-radius: var(--radius-md);
|
| 770 |
+
font-weight: 500;
|
| 771 |
+
}
|
| 772 |
+
|
| 773 |
+
.modal-parent-info.danger-bg {
|
| 774 |
+
color: var(--danger);
|
| 775 |
+
background: rgba(239, 68, 68, 0.1);
|
| 776 |
+
border: 1px solid rgba(239, 68, 68, 0.2);
|
| 777 |
+
}
|
docs/API.md
CHANGED
|
@@ -290,7 +290,7 @@ Authorization: Bearer <regular_key_or_admin_key>
|
|
| 290 |
|
| 291 |
为指定的 Regular Key 创建新的 Server Key。
|
| 292 |
|
| 293 |
-
**权限**: Admin Key
|
| 294 |
|
| 295 |
**请求**
|
| 296 |
|
|
@@ -336,6 +336,33 @@ Content-Type: application/json
|
|
| 336 |
}
|
| 337 |
```
|
| 338 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 339 |
---
|
| 340 |
|
| 341 |
### 激活 API Key
|
|
|
|
| 290 |
|
| 291 |
为指定的 Regular Key 创建新的 Server Key。
|
| 292 |
|
| 293 |
+
**权限**: Admin Key(仅限管理员,Regular Key 不能为自己创建 Server Key)
|
| 294 |
|
| 295 |
**请求**
|
| 296 |
|
|
|
|
| 336 |
}
|
| 337 |
```
|
| 338 |
|
| 339 |
+
**错误响应** `400 Bad Request`
|
| 340 |
+
|
| 341 |
+
```json
|
| 342 |
+
{
|
| 343 |
+
"detail": "Name is required"
|
| 344 |
+
}
|
| 345 |
+
```
|
| 346 |
+
|
| 347 |
+
```json
|
| 348 |
+
{
|
| 349 |
+
"detail": "regular_key_id is required"
|
| 350 |
+
}
|
| 351 |
+
```
|
| 352 |
+
|
| 353 |
+
**错误响应** `404 Not Found`
|
| 354 |
+
|
| 355 |
+
```json
|
| 356 |
+
{
|
| 357 |
+
"detail": "Invalid Regular Key ID"
|
| 358 |
+
}
|
| 359 |
+
```
|
| 360 |
+
|
| 361 |
+
> ⚠️ **重要**:
|
| 362 |
+
> - 只有 Admin Key 可以为 Regular Key 创建额外的 Server Key
|
| 363 |
+
> - Regular Key 不能为自己创建或删除 Server Key
|
| 364 |
+
> - `key` 字段只在创建时返回一次,请妥善保存!
|
| 365 |
+
|
| 366 |
---
|
| 367 |
|
| 368 |
### 激活 API Key
|
docs/openapi.json
ADDED
|
@@ -0,0 +1,930 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"openapi": "3.0.0",
|
| 3 |
+
"info": {
|
| 4 |
+
"title": "InterConnect-Server API",
|
| 5 |
+
"version": "1.0.0",
|
| 6 |
+
"description": "InterConnect-Server 是一个 Minecraft WebSocket API 服务器,提供 API Key 管理、事件广播、服务器命令等功能。\n\n权限说明:\n- **Admin Key**: `mc_admin_` 前缀,拥有完整管理权限。\n- **Regular Key**: `mc_key_` 前缀,可查看/管理关联的 Server Key,发送服务器命令。\n- **Server Key**: `mc_server_` 前缀,仅用于插件/Mod 配置,用于认证和事件上报。"
|
| 7 |
+
},
|
| 8 |
+
"servers": [
|
| 9 |
+
{
|
| 10 |
+
"url": "http://localhost:8000",
|
| 11 |
+
"description": "本地服务器"
|
| 12 |
+
}
|
| 13 |
+
],
|
| 14 |
+
"components": {
|
| 15 |
+
"securitySchemes": {
|
| 16 |
+
"bearerAuth": {
|
| 17 |
+
"type": "http",
|
| 18 |
+
"scheme": "bearer",
|
| 19 |
+
"bearerFormat": "API Key",
|
| 20 |
+
"description": "使用 API Key 进行认证 (Bearer Token)"
|
| 21 |
+
}
|
| 22 |
+
},
|
| 23 |
+
"schemas": {
|
| 24 |
+
"ApiKey": {
|
| 25 |
+
"type": "object",
|
| 26 |
+
"properties": {
|
| 27 |
+
"id": {
|
| 28 |
+
"type": "string",
|
| 29 |
+
"format": "uuid"
|
| 30 |
+
},
|
| 31 |
+
"name": {
|
| 32 |
+
"type": "string"
|
| 33 |
+
},
|
| 34 |
+
"description": {
|
| 35 |
+
"type": "string"
|
| 36 |
+
},
|
| 37 |
+
"keyPrefix": {
|
| 38 |
+
"type": "string",
|
| 39 |
+
"enum": ["mc_admin_", "mc_key_", "mc_server_"]
|
| 40 |
+
},
|
| 41 |
+
"keyType": {
|
| 42 |
+
"type": "string",
|
| 43 |
+
"enum": ["admin", "regular", "server"]
|
| 44 |
+
},
|
| 45 |
+
"serverId": {
|
| 46 |
+
"type": "string",
|
| 47 |
+
"nullable": true
|
| 48 |
+
},
|
| 49 |
+
"regularKeyId": {
|
| 50 |
+
"type": "string",
|
| 51 |
+
"nullable": true
|
| 52 |
+
},
|
| 53 |
+
"createdAt": {
|
| 54 |
+
"type": "string",
|
| 55 |
+
"format": "date-time"
|
| 56 |
+
},
|
| 57 |
+
"lastUsed": {
|
| 58 |
+
"type": "string",
|
| 59 |
+
"format": "date-time",
|
| 60 |
+
"nullable": true
|
| 61 |
+
},
|
| 62 |
+
"isActive": {
|
| 63 |
+
"type": "boolean"
|
| 64 |
+
}
|
| 65 |
+
}
|
| 66 |
+
},
|
| 67 |
+
"ApiKeyWithSecret": {
|
| 68 |
+
"allOf": [
|
| 69 |
+
{
|
| 70 |
+
"$ref": "#/components/schemas/ApiKey"
|
| 71 |
+
},
|
| 72 |
+
{
|
| 73 |
+
"type": "object",
|
| 74 |
+
"properties": {
|
| 75 |
+
"key": {
|
| 76 |
+
"type": "string",
|
| 77 |
+
"description": "原始 API Key,仅在创建时返回一次"
|
| 78 |
+
}
|
| 79 |
+
}
|
| 80 |
+
}
|
| 81 |
+
]
|
| 82 |
+
},
|
| 83 |
+
"CreateKeyRequest": {
|
| 84 |
+
"type": "object",
|
| 85 |
+
"required": ["name"],
|
| 86 |
+
"properties": {
|
| 87 |
+
"name": {
|
| 88 |
+
"type": "string"
|
| 89 |
+
},
|
| 90 |
+
"description": {
|
| 91 |
+
"type": "string"
|
| 92 |
+
},
|
| 93 |
+
"key_type": {
|
| 94 |
+
"type": "string",
|
| 95 |
+
"enum": ["regular", "admin"],
|
| 96 |
+
"default": "regular"
|
| 97 |
+
},
|
| 98 |
+
"server_id": {
|
| 99 |
+
"type": "string"
|
| 100 |
+
}
|
| 101 |
+
}
|
| 102 |
+
},
|
| 103 |
+
"CreateServerKeyRequest": {
|
| 104 |
+
"type": "object",
|
| 105 |
+
"required": ["name", "regular_key_id"],
|
| 106 |
+
"properties": {
|
| 107 |
+
"name": {
|
| 108 |
+
"type": "string"
|
| 109 |
+
},
|
| 110 |
+
"description": {
|
| 111 |
+
"type": "string"
|
| 112 |
+
},
|
| 113 |
+
"server_id": {
|
| 114 |
+
"type": "string"
|
| 115 |
+
},
|
| 116 |
+
"regular_key_id": {
|
| 117 |
+
"type": "string",
|
| 118 |
+
"description": "关联的 Regular Key ID"
|
| 119 |
+
}
|
| 120 |
+
}
|
| 121 |
+
},
|
| 122 |
+
"Event": {
|
| 123 |
+
"type": "object",
|
| 124 |
+
"required": ["event_type", "server_name", "timestamp", "data"],
|
| 125 |
+
"properties": {
|
| 126 |
+
"event_type": {
|
| 127 |
+
"type": "string",
|
| 128 |
+
"description": "事件类型 (e.g., player_join, player_leave, message, ai_chat)"
|
| 129 |
+
},
|
| 130 |
+
"server_name": {
|
| 131 |
+
"type": "string"
|
| 132 |
+
},
|
| 133 |
+
"timestamp": {
|
| 134 |
+
"type": "string",
|
| 135 |
+
"format": "date-time"
|
| 136 |
+
},
|
| 137 |
+
"data": {
|
| 138 |
+
"type": "object",
|
| 139 |
+
"description": "事件具体数据"
|
| 140 |
+
}
|
| 141 |
+
}
|
| 142 |
+
},
|
| 143 |
+
"CommandRequest": {
|
| 144 |
+
"type": "object",
|
| 145 |
+
"required": ["command"],
|
| 146 |
+
"properties": {
|
| 147 |
+
"command": {
|
| 148 |
+
"type": "string"
|
| 149 |
+
},
|
| 150 |
+
"server_id": {
|
| 151 |
+
"type": "string",
|
| 152 |
+
"description": "仅 Admin Key 需要/可以指定"
|
| 153 |
+
}
|
| 154 |
+
}
|
| 155 |
+
},
|
| 156 |
+
"AiConfig": {
|
| 157 |
+
"type": "object",
|
| 158 |
+
"properties": {
|
| 159 |
+
"apiUrl": {
|
| 160 |
+
"type": "string"
|
| 161 |
+
},
|
| 162 |
+
"modelId": {
|
| 163 |
+
"type": "string"
|
| 164 |
+
},
|
| 165 |
+
"apiKey": {
|
| 166 |
+
"type": "string",
|
| 167 |
+
"description": "部分隐藏的 API Key"
|
| 168 |
+
},
|
| 169 |
+
"enabled": {
|
| 170 |
+
"type": "boolean"
|
| 171 |
+
},
|
| 172 |
+
"systemPrompt": {
|
| 173 |
+
"type": "string"
|
| 174 |
+
},
|
| 175 |
+
"createdAt": {
|
| 176 |
+
"type": "string",
|
| 177 |
+
"format": "date-time"
|
| 178 |
+
},
|
| 179 |
+
"updatedAt": {
|
| 180 |
+
"type": "string",
|
| 181 |
+
"format": "date-time"
|
| 182 |
+
}
|
| 183 |
+
}
|
| 184 |
+
},
|
| 185 |
+
"AiConfigUpdateRequest": {
|
| 186 |
+
"type": "object",
|
| 187 |
+
"properties": {
|
| 188 |
+
"api_url": {
|
| 189 |
+
"type": "string"
|
| 190 |
+
},
|
| 191 |
+
"model_id": {
|
| 192 |
+
"type": "string"
|
| 193 |
+
},
|
| 194 |
+
"api_key": {
|
| 195 |
+
"type": "string"
|
| 196 |
+
},
|
| 197 |
+
"enabled": {
|
| 198 |
+
"type": "boolean"
|
| 199 |
+
},
|
| 200 |
+
"system_prompt": {
|
| 201 |
+
"type": "string"
|
| 202 |
+
}
|
| 203 |
+
}
|
| 204 |
+
},
|
| 205 |
+
"AiChatRequest": {
|
| 206 |
+
"type": "object",
|
| 207 |
+
"required": ["message"],
|
| 208 |
+
"properties": {
|
| 209 |
+
"message": {
|
| 210 |
+
"type": "string"
|
| 211 |
+
},
|
| 212 |
+
"player_name": {
|
| 213 |
+
"type": "string"
|
| 214 |
+
},
|
| 215 |
+
"server_id": {
|
| 216 |
+
"type": "string"
|
| 217 |
+
}
|
| 218 |
+
}
|
| 219 |
+
},
|
| 220 |
+
"ErrorResponse": {
|
| 221 |
+
"type": "object",
|
| 222 |
+
"properties": {
|
| 223 |
+
"detail": {
|
| 224 |
+
"type": "string"
|
| 225 |
+
}
|
| 226 |
+
}
|
| 227 |
+
}
|
| 228 |
+
}
|
| 229 |
+
},
|
| 230 |
+
"paths": {
|
| 231 |
+
"/health": {
|
| 232 |
+
"get": {
|
| 233 |
+
"summary": "健康检查",
|
| 234 |
+
"description": "获取服务器状态和统计信息",
|
| 235 |
+
"security": [],
|
| 236 |
+
"responses": {
|
| 237 |
+
"200": {
|
| 238 |
+
"description": "服务器正常",
|
| 239 |
+
"content": {
|
| 240 |
+
"application/json": {
|
| 241 |
+
"schema": {
|
| 242 |
+
"type": "object",
|
| 243 |
+
"properties": {
|
| 244 |
+
"status": {
|
| 245 |
+
"type": "string"
|
| 246 |
+
},
|
| 247 |
+
"timestamp": {
|
| 248 |
+
"type": "string"
|
| 249 |
+
},
|
| 250 |
+
"active_ws": {
|
| 251 |
+
"type": "integer"
|
| 252 |
+
},
|
| 253 |
+
"keys_total": {
|
| 254 |
+
"type": "integer"
|
| 255 |
+
},
|
| 256 |
+
"admin_active": {
|
| 257 |
+
"type": "integer"
|
| 258 |
+
},
|
| 259 |
+
"server_active": {
|
| 260 |
+
"type": "integer"
|
| 261 |
+
},
|
| 262 |
+
"regular_active": {
|
| 263 |
+
"type": "integer"
|
| 264 |
+
}
|
| 265 |
+
}
|
| 266 |
+
}
|
| 267 |
+
}
|
| 268 |
+
}
|
| 269 |
+
}
|
| 270 |
+
}
|
| 271 |
+
}
|
| 272 |
+
},
|
| 273 |
+
"/manage/keys": {
|
| 274 |
+
"get": {
|
| 275 |
+
"summary": "获取所有 API Key",
|
| 276 |
+
"description": "获取所有 API Key 列表 (仅 Admin)",
|
| 277 |
+
"security": [
|
| 278 |
+
{
|
| 279 |
+
"bearerAuth": []
|
| 280 |
+
}
|
| 281 |
+
],
|
| 282 |
+
"responses": {
|
| 283 |
+
"200": {
|
| 284 |
+
"description": "成功",
|
| 285 |
+
"content": {
|
| 286 |
+
"application/json": {
|
| 287 |
+
"schema": {
|
| 288 |
+
"type": "array",
|
| 289 |
+
"items": {
|
| 290 |
+
"$ref": "#/components/schemas/ApiKey"
|
| 291 |
+
}
|
| 292 |
+
}
|
| 293 |
+
}
|
| 294 |
+
}
|
| 295 |
+
},
|
| 296 |
+
"403": {
|
| 297 |
+
"description": "权限不足",
|
| 298 |
+
"content": {
|
| 299 |
+
"application/json": {
|
| 300 |
+
"schema": {
|
| 301 |
+
"$ref": "#/components/schemas/ErrorResponse"
|
| 302 |
+
}
|
| 303 |
+
}
|
| 304 |
+
}
|
| 305 |
+
}
|
| 306 |
+
}
|
| 307 |
+
},
|
| 308 |
+
"post": {
|
| 309 |
+
"summary": "创建 API Key",
|
| 310 |
+
"description": "创建新的 Admin 或 Regular Key (创建 Regular Key 会自动附带一个 Server Key)",
|
| 311 |
+
"security": [
|
| 312 |
+
{
|
| 313 |
+
"bearerAuth": []
|
| 314 |
+
}
|
| 315 |
+
],
|
| 316 |
+
"requestBody": {
|
| 317 |
+
"required": true,
|
| 318 |
+
"content": {
|
| 319 |
+
"application/json": {
|
| 320 |
+
"schema": {
|
| 321 |
+
"$ref": "#/components/schemas/CreateKeyRequest"
|
| 322 |
+
}
|
| 323 |
+
}
|
| 324 |
+
}
|
| 325 |
+
},
|
| 326 |
+
"responses": {
|
| 327 |
+
"201": {
|
| 328 |
+
"description": "创建成功",
|
| 329 |
+
"content": {
|
| 330 |
+
"application/json": {
|
| 331 |
+
"schema": {
|
| 332 |
+
"oneOf": [
|
| 333 |
+
{
|
| 334 |
+
"$ref": "#/components/schemas/ApiKeyWithSecret"
|
| 335 |
+
},
|
| 336 |
+
{
|
| 337 |
+
"type": "object",
|
| 338 |
+
"properties": {
|
| 339 |
+
"regularKey": {
|
| 340 |
+
"$ref": "#/components/schemas/ApiKeyWithSecret"
|
| 341 |
+
},
|
| 342 |
+
"serverKey": {
|
| 343 |
+
"$ref": "#/components/schemas/ApiKeyWithSecret"
|
| 344 |
+
}
|
| 345 |
+
}
|
| 346 |
+
}
|
| 347 |
+
]
|
| 348 |
+
}
|
| 349 |
+
}
|
| 350 |
+
}
|
| 351 |
+
}
|
| 352 |
+
}
|
| 353 |
+
}
|
| 354 |
+
},
|
| 355 |
+
"/manage/keys/{key_id}": {
|
| 356 |
+
"get": {
|
| 357 |
+
"summary": "获取 API Key 详情",
|
| 358 |
+
"security": [
|
| 359 |
+
{
|
| 360 |
+
"bearerAuth": []
|
| 361 |
+
}
|
| 362 |
+
],
|
| 363 |
+
"parameters": [
|
| 364 |
+
{
|
| 365 |
+
"in": "path",
|
| 366 |
+
"name": "key_id",
|
| 367 |
+
"schema": {
|
| 368 |
+
"type": "string"
|
| 369 |
+
},
|
| 370 |
+
"required": true
|
| 371 |
+
}
|
| 372 |
+
],
|
| 373 |
+
"responses": {
|
| 374 |
+
"200": {
|
| 375 |
+
"description": "成功",
|
| 376 |
+
"content": {
|
| 377 |
+
"application/json": {
|
| 378 |
+
"schema": {
|
| 379 |
+
"$ref": "#/components/schemas/ApiKey"
|
| 380 |
+
}
|
| 381 |
+
}
|
| 382 |
+
}
|
| 383 |
+
},
|
| 384 |
+
"404": {
|
| 385 |
+
"description": "未找到",
|
| 386 |
+
"content": {
|
| 387 |
+
"application/json": {
|
| 388 |
+
"schema": {
|
| 389 |
+
"$ref": "#/components/schemas/ErrorResponse"
|
| 390 |
+
}
|
| 391 |
+
}
|
| 392 |
+
}
|
| 393 |
+
}
|
| 394 |
+
}
|
| 395 |
+
},
|
| 396 |
+
"delete": {
|
| 397 |
+
"summary": "删除 API Key",
|
| 398 |
+
"description": "仅 Admin 可操作,不能删除自己",
|
| 399 |
+
"security": [
|
| 400 |
+
{
|
| 401 |
+
"bearerAuth": []
|
| 402 |
+
}
|
| 403 |
+
],
|
| 404 |
+
"parameters": [
|
| 405 |
+
{
|
| 406 |
+
"in": "path",
|
| 407 |
+
"name": "key_id",
|
| 408 |
+
"schema": {
|
| 409 |
+
"type": "string"
|
| 410 |
+
},
|
| 411 |
+
"required": true
|
| 412 |
+
}
|
| 413 |
+
],
|
| 414 |
+
"responses": {
|
| 415 |
+
"204": {
|
| 416 |
+
"description": "删除成功"
|
| 417 |
+
},
|
| 418 |
+
"400": {
|
| 419 |
+
"description": "无法删除(如试图删除自己)",
|
| 420 |
+
"content": {
|
| 421 |
+
"application/json": {
|
| 422 |
+
"schema": {
|
| 423 |
+
"$ref": "#/components/schemas/ErrorResponse"
|
| 424 |
+
}
|
| 425 |
+
}
|
| 426 |
+
}
|
| 427 |
+
}
|
| 428 |
+
}
|
| 429 |
+
}
|
| 430 |
+
},
|
| 431 |
+
"/manage/keys/{key_id}/activate": {
|
| 432 |
+
"patch": {
|
| 433 |
+
"summary": "激活 API Key",
|
| 434 |
+
"description": "Admin 可激活任意 Key,Regular 仅可激活关联的 Server Key",
|
| 435 |
+
"security": [
|
| 436 |
+
{
|
| 437 |
+
"bearerAuth": []
|
| 438 |
+
}
|
| 439 |
+
],
|
| 440 |
+
"parameters": [
|
| 441 |
+
{
|
| 442 |
+
"in": "path",
|
| 443 |
+
"name": "key_id",
|
| 444 |
+
"schema": {
|
| 445 |
+
"type": "string"
|
| 446 |
+
},
|
| 447 |
+
"required": true
|
| 448 |
+
}
|
| 449 |
+
],
|
| 450 |
+
"responses": {
|
| 451 |
+
"200": {
|
| 452 |
+
"description": "成功",
|
| 453 |
+
"content": {
|
| 454 |
+
"application/json": {
|
| 455 |
+
"schema": {
|
| 456 |
+
"type": "object",
|
| 457 |
+
"properties": {
|
| 458 |
+
"message": {
|
| 459 |
+
"type": "string"
|
| 460 |
+
}
|
| 461 |
+
}
|
| 462 |
+
}
|
| 463 |
+
}
|
| 464 |
+
}
|
| 465 |
+
}
|
| 466 |
+
}
|
| 467 |
+
}
|
| 468 |
+
},
|
| 469 |
+
"/manage/keys/{key_id}/deactivate": {
|
| 470 |
+
"patch": {
|
| 471 |
+
"summary": "停用 API Key",
|
| 472 |
+
"description": "Admin 可停用任意 Key,Regular 仅可停用关联的 Server Key",
|
| 473 |
+
"security": [
|
| 474 |
+
{
|
| 475 |
+
"bearerAuth": []
|
| 476 |
+
}
|
| 477 |
+
],
|
| 478 |
+
"parameters": [
|
| 479 |
+
{
|
| 480 |
+
"in": "path",
|
| 481 |
+
"name": "key_id",
|
| 482 |
+
"schema": {
|
| 483 |
+
"type": "string"
|
| 484 |
+
},
|
| 485 |
+
"required": true
|
| 486 |
+
}
|
| 487 |
+
],
|
| 488 |
+
"responses": {
|
| 489 |
+
"200": {
|
| 490 |
+
"description": "成功",
|
| 491 |
+
"content": {
|
| 492 |
+
"application/json": {
|
| 493 |
+
"schema": {
|
| 494 |
+
"type": "object",
|
| 495 |
+
"properties": {
|
| 496 |
+
"message": {
|
| 497 |
+
"type": "string"
|
| 498 |
+
}
|
| 499 |
+
}
|
| 500 |
+
}
|
| 501 |
+
}
|
| 502 |
+
}
|
| 503 |
+
}
|
| 504 |
+
}
|
| 505 |
+
}
|
| 506 |
+
},
|
| 507 |
+
"/manage/keys/server-keys": {
|
| 508 |
+
"get": {
|
| 509 |
+
"summary": "获取 Server Key 列表",
|
| 510 |
+
"description": "Admin 获取所有 Server Key,Regular 获取关联的 Server Key",
|
| 511 |
+
"security": [
|
| 512 |
+
{
|
| 513 |
+
"bearerAuth": []
|
| 514 |
+
}
|
| 515 |
+
],
|
| 516 |
+
"responses": {
|
| 517 |
+
"200": {
|
| 518 |
+
"description": "成功",
|
| 519 |
+
"content": {
|
| 520 |
+
"application/json": {
|
| 521 |
+
"schema": {
|
| 522 |
+
"type": "array",
|
| 523 |
+
"items": {
|
| 524 |
+
"$ref": "#/components/schemas/ApiKey"
|
| 525 |
+
}
|
| 526 |
+
}
|
| 527 |
+
}
|
| 528 |
+
}
|
| 529 |
+
}
|
| 530 |
+
}
|
| 531 |
+
},
|
| 532 |
+
"post": {
|
| 533 |
+
"summary": "创建 Server Key",
|
| 534 |
+
"description": "为指定的 Regular Key 创建新的 Server Key (仅 Admin,Regular Key 不能为自己创建)",
|
| 535 |
+
"security": [
|
| 536 |
+
{
|
| 537 |
+
"bearerAuth": []
|
| 538 |
+
}
|
| 539 |
+
],
|
| 540 |
+
"requestBody": {
|
| 541 |
+
"required": true,
|
| 542 |
+
"content": {
|
| 543 |
+
"application/json": {
|
| 544 |
+
"schema": {
|
| 545 |
+
"$ref": "#/components/schemas/CreateServerKeyRequest"
|
| 546 |
+
}
|
| 547 |
+
}
|
| 548 |
+
}
|
| 549 |
+
},
|
| 550 |
+
"responses": {
|
| 551 |
+
"201": {
|
| 552 |
+
"description": "创建成功",
|
| 553 |
+
"content": {
|
| 554 |
+
"application/json": {
|
| 555 |
+
"schema": {
|
| 556 |
+
"$ref": "#/components/schemas/ApiKeyWithSecret"
|
| 557 |
+
}
|
| 558 |
+
}
|
| 559 |
+
}
|
| 560 |
+
},
|
| 561 |
+
"400": {
|
| 562 |
+
"description": "参数错误",
|
| 563 |
+
"content": {
|
| 564 |
+
"application/json": {
|
| 565 |
+
"schema": {
|
| 566 |
+
"$ref": "#/components/schemas/ErrorResponse"
|
| 567 |
+
}
|
| 568 |
+
}
|
| 569 |
+
}
|
| 570 |
+
},
|
| 571 |
+
"404": {
|
| 572 |
+
"description": "Regular Key 不��在",
|
| 573 |
+
"content": {
|
| 574 |
+
"application/json": {
|
| 575 |
+
"schema": {
|
| 576 |
+
"$ref": "#/components/schemas/ErrorResponse"
|
| 577 |
+
}
|
| 578 |
+
}
|
| 579 |
+
}
|
| 580 |
+
}
|
| 581 |
+
}
|
| 582 |
+
}
|
| 583 |
+
},
|
| 584 |
+
"/api/events": {
|
| 585 |
+
"post": {
|
| 586 |
+
"summary": "发送事件",
|
| 587 |
+
"description": "发送 Minecraft 事件并广播给所有 WebSocket 连接",
|
| 588 |
+
"security": [
|
| 589 |
+
{
|
| 590 |
+
"bearerAuth": []
|
| 591 |
+
}
|
| 592 |
+
],
|
| 593 |
+
"requestBody": {
|
| 594 |
+
"required": true,
|
| 595 |
+
"content": {
|
| 596 |
+
"application/json": {
|
| 597 |
+
"schema": {
|
| 598 |
+
"$ref": "#/components/schemas/Event"
|
| 599 |
+
}
|
| 600 |
+
}
|
| 601 |
+
}
|
| 602 |
+
},
|
| 603 |
+
"responses": {
|
| 604 |
+
"200": {
|
| 605 |
+
"description": "成功广播",
|
| 606 |
+
"content": {
|
| 607 |
+
"application/json": {
|
| 608 |
+
"schema": {
|
| 609 |
+
"type": "object",
|
| 610 |
+
"properties": {
|
| 611 |
+
"message": {
|
| 612 |
+
"type": "string"
|
| 613 |
+
}
|
| 614 |
+
}
|
| 615 |
+
}
|
| 616 |
+
}
|
| 617 |
+
}
|
| 618 |
+
}
|
| 619 |
+
}
|
| 620 |
+
}
|
| 621 |
+
},
|
| 622 |
+
"/api/server/info": {
|
| 623 |
+
"get": {
|
| 624 |
+
"summary": "获取服务器信息",
|
| 625 |
+
"security": [
|
| 626 |
+
{
|
| 627 |
+
"bearerAuth": []
|
| 628 |
+
}
|
| 629 |
+
],
|
| 630 |
+
"parameters": [
|
| 631 |
+
{
|
| 632 |
+
"in": "query",
|
| 633 |
+
"name": "server_id",
|
| 634 |
+
"schema": {
|
| 635 |
+
"type": "string"
|
| 636 |
+
},
|
| 637 |
+
"description": "仅 Admin 需要"
|
| 638 |
+
}
|
| 639 |
+
],
|
| 640 |
+
"responses": {
|
| 641 |
+
"200": {
|
| 642 |
+
"description": "成功",
|
| 643 |
+
"content": {
|
| 644 |
+
"application/json": {
|
| 645 |
+
"schema": {
|
| 646 |
+
"type": "object",
|
| 647 |
+
"properties": {
|
| 648 |
+
"server_id": {
|
| 649 |
+
"type": "string"
|
| 650 |
+
},
|
| 651 |
+
"status": {
|
| 652 |
+
"type": "string"
|
| 653 |
+
},
|
| 654 |
+
"online_players": {
|
| 655 |
+
"type": "integer"
|
| 656 |
+
}
|
| 657 |
+
}
|
| 658 |
+
}
|
| 659 |
+
}
|
| 660 |
+
}
|
| 661 |
+
}
|
| 662 |
+
}
|
| 663 |
+
}
|
| 664 |
+
},
|
| 665 |
+
"/api/server/command": {
|
| 666 |
+
"post": {
|
| 667 |
+
"summary": "发送服务器命令",
|
| 668 |
+
"security": [
|
| 669 |
+
{
|
| 670 |
+
"bearerAuth": []
|
| 671 |
+
}
|
| 672 |
+
],
|
| 673 |
+
"requestBody": {
|
| 674 |
+
"required": true,
|
| 675 |
+
"content": {
|
| 676 |
+
"application/json": {
|
| 677 |
+
"schema": {
|
| 678 |
+
"$ref": "#/components/schemas/CommandRequest"
|
| 679 |
+
}
|
| 680 |
+
}
|
| 681 |
+
}
|
| 682 |
+
},
|
| 683 |
+
"responses": {
|
| 684 |
+
"200": {
|
| 685 |
+
"description": "命令发送成功",
|
| 686 |
+
"content": {
|
| 687 |
+
"application/json": {
|
| 688 |
+
"schema": {
|
| 689 |
+
"type": "object",
|
| 690 |
+
"properties": {
|
| 691 |
+
"message": {
|
| 692 |
+
"type": "string"
|
| 693 |
+
}
|
| 694 |
+
}
|
| 695 |
+
}
|
| 696 |
+
}
|
| 697 |
+
}
|
| 698 |
+
},
|
| 699 |
+
"403": {
|
| 700 |
+
"description": "禁止的命令",
|
| 701 |
+
"content": {
|
| 702 |
+
"application/json": {
|
| 703 |
+
"schema": {
|
| 704 |
+
"$ref": "#/components/schemas/ErrorResponse"
|
| 705 |
+
}
|
| 706 |
+
}
|
| 707 |
+
}
|
| 708 |
+
}
|
| 709 |
+
}
|
| 710 |
+
}
|
| 711 |
+
},
|
| 712 |
+
"/api/ai/config": {
|
| 713 |
+
"get": {
|
| 714 |
+
"summary": "获取 AI 配置 (Admin)",
|
| 715 |
+
"security": [
|
| 716 |
+
{
|
| 717 |
+
"bearerAuth": []
|
| 718 |
+
}
|
| 719 |
+
],
|
| 720 |
+
"responses": {
|
| 721 |
+
"200": {
|
| 722 |
+
"description": "成功",
|
| 723 |
+
"content": {
|
| 724 |
+
"application/json": {
|
| 725 |
+
"schema": {
|
| 726 |
+
"$ref": "#/components/schemas/AiConfig"
|
| 727 |
+
}
|
| 728 |
+
}
|
| 729 |
+
}
|
| 730 |
+
},
|
| 731 |
+
"404": {
|
| 732 |
+
"description": "配置不存在",
|
| 733 |
+
"content": {
|
| 734 |
+
"application/json": {
|
| 735 |
+
"schema": {
|
| 736 |
+
"$ref": "#/components/schemas/ErrorResponse"
|
| 737 |
+
}
|
| 738 |
+
}
|
| 739 |
+
}
|
| 740 |
+
}
|
| 741 |
+
}
|
| 742 |
+
},
|
| 743 |
+
"post": {
|
| 744 |
+
"summary": "创建 AI 配置 (Admin)",
|
| 745 |
+
"security": [
|
| 746 |
+
{
|
| 747 |
+
"bearerAuth": []
|
| 748 |
+
}
|
| 749 |
+
],
|
| 750 |
+
"requestBody": {
|
| 751 |
+
"content": {
|
| 752 |
+
"application/json": {
|
| 753 |
+
"schema": {
|
| 754 |
+
"$ref": "#/components/schemas/AiConfigUpdateRequest"
|
| 755 |
+
}
|
| 756 |
+
}
|
| 757 |
+
}
|
| 758 |
+
},
|
| 759 |
+
"responses": {
|
| 760 |
+
"201": {
|
| 761 |
+
"description": "创建成功",
|
| 762 |
+
"content": {
|
| 763 |
+
"application/json": {
|
| 764 |
+
"schema": {
|
| 765 |
+
"$ref": "#/components/schemas/AiConfig"
|
| 766 |
+
}
|
| 767 |
+
}
|
| 768 |
+
}
|
| 769 |
+
}
|
| 770 |
+
}
|
| 771 |
+
},
|
| 772 |
+
"patch": {
|
| 773 |
+
"summary": "更新 AI 配置 (Admin)",
|
| 774 |
+
"security": [
|
| 775 |
+
{
|
| 776 |
+
"bearerAuth": []
|
| 777 |
+
}
|
| 778 |
+
],
|
| 779 |
+
"requestBody": {
|
| 780 |
+
"content": {
|
| 781 |
+
"application/json": {
|
| 782 |
+
"schema": {
|
| 783 |
+
"$ref": "#/components/schemas/AiConfigUpdateRequest"
|
| 784 |
+
}
|
| 785 |
+
}
|
| 786 |
+
}
|
| 787 |
+
},
|
| 788 |
+
"responses": {
|
| 789 |
+
"200": {
|
| 790 |
+
"description": "更新成功",
|
| 791 |
+
"content": {
|
| 792 |
+
"application/json": {
|
| 793 |
+
"schema": {
|
| 794 |
+
"$ref": "#/components/schemas/AiConfig"
|
| 795 |
+
}
|
| 796 |
+
}
|
| 797 |
+
}
|
| 798 |
+
}
|
| 799 |
+
}
|
| 800 |
+
},
|
| 801 |
+
"delete": {
|
| 802 |
+
"summary": "删除 AI 配置 (Admin)",
|
| 803 |
+
"security": [
|
| 804 |
+
{
|
| 805 |
+
"bearerAuth": []
|
| 806 |
+
}
|
| 807 |
+
],
|
| 808 |
+
"responses": {
|
| 809 |
+
"204": {
|
| 810 |
+
"description": "删除成功"
|
| 811 |
+
}
|
| 812 |
+
}
|
| 813 |
+
}
|
| 814 |
+
},
|
| 815 |
+
"/api/ai/config/test": {
|
| 816 |
+
"post": {
|
| 817 |
+
"summary": "测试 AI 连接 (Admin)",
|
| 818 |
+
"security": [
|
| 819 |
+
{
|
| 820 |
+
"bearerAuth": []
|
| 821 |
+
}
|
| 822 |
+
],
|
| 823 |
+
"responses": {
|
| 824 |
+
"200": {
|
| 825 |
+
"description": "连接成功",
|
| 826 |
+
"content": {
|
| 827 |
+
"application/json": {
|
| 828 |
+
"schema": {
|
| 829 |
+
"type": "object",
|
| 830 |
+
"properties": {
|
| 831 |
+
"success": {
|
| 832 |
+
"type": "boolean"
|
| 833 |
+
},
|
| 834 |
+
"message": {
|
| 835 |
+
"type": "string"
|
| 836 |
+
},
|
| 837 |
+
"model": {
|
| 838 |
+
"type": "string"
|
| 839 |
+
},
|
| 840 |
+
"response": {
|
| 841 |
+
"type": "string"
|
| 842 |
+
}
|
| 843 |
+
}
|
| 844 |
+
}
|
| 845 |
+
}
|
| 846 |
+
}
|
| 847 |
+
}
|
| 848 |
+
}
|
| 849 |
+
}
|
| 850 |
+
},
|
| 851 |
+
"/api/ai/chat": {
|
| 852 |
+
"post": {
|
| 853 |
+
"summary": "AI 聊天",
|
| 854 |
+
"description": "发送消息给 AI 并获取回复",
|
| 855 |
+
"security": [
|
| 856 |
+
{
|
| 857 |
+
"bearerAuth": []
|
| 858 |
+
}
|
| 859 |
+
],
|
| 860 |
+
"requestBody": {
|
| 861 |
+
"required": true,
|
| 862 |
+
"content": {
|
| 863 |
+
"application/json": {
|
| 864 |
+
"schema": {
|
| 865 |
+
"$ref": "#/components/schemas/AiChatRequest"
|
| 866 |
+
}
|
| 867 |
+
}
|
| 868 |
+
}
|
| 869 |
+
},
|
| 870 |
+
"responses": {
|
| 871 |
+
"200": {
|
| 872 |
+
"description": "成功",
|
| 873 |
+
"content": {
|
| 874 |
+
"application/json": {
|
| 875 |
+
"schema": {
|
| 876 |
+
"type": "object",
|
| 877 |
+
"properties": {
|
| 878 |
+
"success": {
|
| 879 |
+
"type": "boolean"
|
| 880 |
+
},
|
| 881 |
+
"reply": {
|
| 882 |
+
"type": "string"
|
| 883 |
+
},
|
| 884 |
+
"model": {
|
| 885 |
+
"type": "string"
|
| 886 |
+
},
|
| 887 |
+
"usage": {
|
| 888 |
+
"type": "object",
|
| 889 |
+
"properties": {
|
| 890 |
+
"prompt_tokens": {
|
| 891 |
+
"type": "integer"
|
| 892 |
+
},
|
| 893 |
+
"completion_tokens": {
|
| 894 |
+
"type": "integer"
|
| 895 |
+
},
|
| 896 |
+
"total_tokens": {
|
| 897 |
+
"type": "integer"
|
| 898 |
+
}
|
| 899 |
+
}
|
| 900 |
+
}
|
| 901 |
+
}
|
| 902 |
+
}
|
| 903 |
+
}
|
| 904 |
+
}
|
| 905 |
+
},
|
| 906 |
+
"403": {
|
| 907 |
+
"description": "AI 功能已禁用",
|
| 908 |
+
"content": {
|
| 909 |
+
"application/json": {
|
| 910 |
+
"schema": {
|
| 911 |
+
"$ref": "#/components/schemas/ErrorResponse"
|
| 912 |
+
}
|
| 913 |
+
}
|
| 914 |
+
}
|
| 915 |
+
},
|
| 916 |
+
"404": {
|
| 917 |
+
"description": "AI 配置不存在",
|
| 918 |
+
"content": {
|
| 919 |
+
"application/json": {
|
| 920 |
+
"schema": {
|
| 921 |
+
"$ref": "#/components/schemas/ErrorResponse"
|
| 922 |
+
}
|
| 923 |
+
}
|
| 924 |
+
}
|
| 925 |
+
}
|
| 926 |
+
}
|
| 927 |
+
}
|
| 928 |
+
}
|
| 929 |
+
}
|
| 930 |
+
}
|
docs/openapi.yaml
ADDED
|
@@ -0,0 +1,512 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
openapi: 3.0.0
|
| 2 |
+
info:
|
| 3 |
+
title: InterConnect-Server API
|
| 4 |
+
version: 1.0.0
|
| 5 |
+
description: |
|
| 6 |
+
InterConnect-Server 是一个 Minecraft WebSocket API 服务器,提供 API Key 管理、事件广播、服务器命令等功能。
|
| 7 |
+
|
| 8 |
+
权限说明:
|
| 9 |
+
- **Admin Key**: `mc_admin_` 前缀,拥有完整管理权限。
|
| 10 |
+
- **Regular Key**: `mc_key_` 前缀,可查看/管理关联的 Server Key,发送服务器命令。
|
| 11 |
+
- **Server Key**: `mc_server_` 前缀,仅用于插件/Mod 配置,用于认证和事件上报。
|
| 12 |
+
|
| 13 |
+
servers:
|
| 14 |
+
- url: http://localhost:8000
|
| 15 |
+
description: 本地服务器
|
| 16 |
+
|
| 17 |
+
components:
|
| 18 |
+
securitySchemes:
|
| 19 |
+
bearerAuth:
|
| 20 |
+
type: http
|
| 21 |
+
scheme: bearer
|
| 22 |
+
bearerFormat: API Key
|
| 23 |
+
description: 使用 API Key 进行认证 (Bearer Token)
|
| 24 |
+
|
| 25 |
+
schemas:
|
| 26 |
+
ApiKey:
|
| 27 |
+
type: object
|
| 28 |
+
properties:
|
| 29 |
+
id:
|
| 30 |
+
type: string
|
| 31 |
+
format: uuid
|
| 32 |
+
name:
|
| 33 |
+
type: string
|
| 34 |
+
description:
|
| 35 |
+
type: string
|
| 36 |
+
keyPrefix:
|
| 37 |
+
type: string
|
| 38 |
+
enum: [mc_admin_, mc_key_, mc_server_]
|
| 39 |
+
keyType:
|
| 40 |
+
type: string
|
| 41 |
+
enum: [admin, regular, server]
|
| 42 |
+
serverId:
|
| 43 |
+
type: string
|
| 44 |
+
nullable: true
|
| 45 |
+
regularKeyId:
|
| 46 |
+
type: string
|
| 47 |
+
nullable: true
|
| 48 |
+
createdAt:
|
| 49 |
+
type: string
|
| 50 |
+
format: date-time
|
| 51 |
+
lastUsed:
|
| 52 |
+
type: string
|
| 53 |
+
format: date-time
|
| 54 |
+
nullable: true
|
| 55 |
+
isActive:
|
| 56 |
+
type: boolean
|
| 57 |
+
|
| 58 |
+
ApiKeyWithSecret:
|
| 59 |
+
allOf:
|
| 60 |
+
- $ref: '#/components/schemas/ApiKey'
|
| 61 |
+
- type: object
|
| 62 |
+
properties:
|
| 63 |
+
key:
|
| 64 |
+
type: string
|
| 65 |
+
description: 原始 API Key,仅在创建时返回一次
|
| 66 |
+
|
| 67 |
+
CreateKeyRequest:
|
| 68 |
+
type: object
|
| 69 |
+
required: [name]
|
| 70 |
+
properties:
|
| 71 |
+
name:
|
| 72 |
+
type: string
|
| 73 |
+
description:
|
| 74 |
+
type: string
|
| 75 |
+
key_type:
|
| 76 |
+
type: string
|
| 77 |
+
enum: [regular, admin]
|
| 78 |
+
default: regular
|
| 79 |
+
server_id:
|
| 80 |
+
type: string
|
| 81 |
+
|
| 82 |
+
CreateServerKeyRequest:
|
| 83 |
+
type: object
|
| 84 |
+
required: [name, regular_key_id]
|
| 85 |
+
properties:
|
| 86 |
+
name:
|
| 87 |
+
type: string
|
| 88 |
+
description:
|
| 89 |
+
type: string
|
| 90 |
+
server_id:
|
| 91 |
+
type: string
|
| 92 |
+
regular_key_id:
|
| 93 |
+
type: string
|
| 94 |
+
|
| 95 |
+
Event:
|
| 96 |
+
type: object
|
| 97 |
+
required: [event_type, server_name, timestamp, data]
|
| 98 |
+
properties:
|
| 99 |
+
event_type:
|
| 100 |
+
type: string
|
| 101 |
+
description: 事件类型 (e.g., player_join, player_leave, message, ai_chat)
|
| 102 |
+
server_name:
|
| 103 |
+
type: string
|
| 104 |
+
timestamp:
|
| 105 |
+
type: string
|
| 106 |
+
format: date-time
|
| 107 |
+
data:
|
| 108 |
+
type: object
|
| 109 |
+
description: 事件具体数据
|
| 110 |
+
|
| 111 |
+
CommandRequest:
|
| 112 |
+
type: object
|
| 113 |
+
required: [command]
|
| 114 |
+
properties:
|
| 115 |
+
command:
|
| 116 |
+
type: string
|
| 117 |
+
server_id:
|
| 118 |
+
type: string
|
| 119 |
+
description: 仅 Admin Key 需要/可以指定
|
| 120 |
+
|
| 121 |
+
AiConfig:
|
| 122 |
+
type: object
|
| 123 |
+
properties:
|
| 124 |
+
apiUrl:
|
| 125 |
+
type: string
|
| 126 |
+
modelId:
|
| 127 |
+
type: string
|
| 128 |
+
apiKey:
|
| 129 |
+
type: string
|
| 130 |
+
description: 部分隐藏的 API Key
|
| 131 |
+
enabled:
|
| 132 |
+
type: boolean
|
| 133 |
+
systemPrompt:
|
| 134 |
+
type: string
|
| 135 |
+
createdAt:
|
| 136 |
+
type: string
|
| 137 |
+
format: date-time
|
| 138 |
+
updatedAt:
|
| 139 |
+
type: string
|
| 140 |
+
format: date-time
|
| 141 |
+
|
| 142 |
+
AiConfigUpdateRequest:
|
| 143 |
+
type: object
|
| 144 |
+
properties:
|
| 145 |
+
api_url:
|
| 146 |
+
type: string
|
| 147 |
+
model_id:
|
| 148 |
+
type: string
|
| 149 |
+
api_key:
|
| 150 |
+
type: string
|
| 151 |
+
enabled:
|
| 152 |
+
type: boolean
|
| 153 |
+
system_prompt:
|
| 154 |
+
type: string
|
| 155 |
+
|
| 156 |
+
AiChatRequest:
|
| 157 |
+
type: object
|
| 158 |
+
required: [message]
|
| 159 |
+
properties:
|
| 160 |
+
message:
|
| 161 |
+
type: string
|
| 162 |
+
player_name:
|
| 163 |
+
type: string
|
| 164 |
+
server_id:
|
| 165 |
+
type: string
|
| 166 |
+
|
| 167 |
+
paths:
|
| 168 |
+
/health:
|
| 169 |
+
get:
|
| 170 |
+
summary: 健康检查
|
| 171 |
+
description: 获取服务器状态和统计信息
|
| 172 |
+
security: []
|
| 173 |
+
responses:
|
| 174 |
+
'200':
|
| 175 |
+
description: 服务器正常
|
| 176 |
+
content:
|
| 177 |
+
application/json:
|
| 178 |
+
schema:
|
| 179 |
+
type: object
|
| 180 |
+
properties:
|
| 181 |
+
status:
|
| 182 |
+
type: string
|
| 183 |
+
timestamp:
|
| 184 |
+
type: string
|
| 185 |
+
active_ws:
|
| 186 |
+
type: integer
|
| 187 |
+
keys_total:
|
| 188 |
+
type: integer
|
| 189 |
+
admin_active:
|
| 190 |
+
type: integer
|
| 191 |
+
server_active:
|
| 192 |
+
type: integer
|
| 193 |
+
regular_active:
|
| 194 |
+
type: integer
|
| 195 |
+
|
| 196 |
+
/manage/keys:
|
| 197 |
+
get:
|
| 198 |
+
summary: 获取所有 API Key
|
| 199 |
+
description: 获取所有 API Key 列表 (仅 Admin)
|
| 200 |
+
security:
|
| 201 |
+
- bearerAuth: []
|
| 202 |
+
responses:
|
| 203 |
+
'200':
|
| 204 |
+
description: 成功
|
| 205 |
+
content:
|
| 206 |
+
application/json:
|
| 207 |
+
schema:
|
| 208 |
+
type: array
|
| 209 |
+
items:
|
| 210 |
+
$ref: '#/components/schemas/ApiKey'
|
| 211 |
+
'403':
|
| 212 |
+
description: 权限不足
|
| 213 |
+
|
| 214 |
+
post:
|
| 215 |
+
summary: 创建 API Key
|
| 216 |
+
description: 创建新的 Admin 或 Regular Key (创建 Regular Key 会自动附带一个 Server Key)
|
| 217 |
+
security:
|
| 218 |
+
- bearerAuth: []
|
| 219 |
+
requestBody:
|
| 220 |
+
required: true
|
| 221 |
+
content:
|
| 222 |
+
application/json:
|
| 223 |
+
schema:
|
| 224 |
+
$ref: '#/components/schemas/CreateKeyRequest'
|
| 225 |
+
responses:
|
| 226 |
+
'201':
|
| 227 |
+
description: 创建成功
|
| 228 |
+
content:
|
| 229 |
+
application/json:
|
| 230 |
+
schema:
|
| 231 |
+
oneOf:
|
| 232 |
+
- $ref: '#/components/schemas/ApiKeyWithSecret'
|
| 233 |
+
- type: object
|
| 234 |
+
properties:
|
| 235 |
+
regularKey:
|
| 236 |
+
$ref: '#/components/schemas/ApiKeyWithSecret'
|
| 237 |
+
serverKey:
|
| 238 |
+
$ref: '#/components/schemas/ApiKeyWithSecret'
|
| 239 |
+
|
| 240 |
+
/manage/keys/{key_id}:
|
| 241 |
+
get:
|
| 242 |
+
summary: 获取 API Key 详情
|
| 243 |
+
security:
|
| 244 |
+
- bearerAuth: []
|
| 245 |
+
parameters:
|
| 246 |
+
- in: path
|
| 247 |
+
name: key_id
|
| 248 |
+
schema:
|
| 249 |
+
type: string
|
| 250 |
+
required: true
|
| 251 |
+
responses:
|
| 252 |
+
'200':
|
| 253 |
+
description: 成功
|
| 254 |
+
content:
|
| 255 |
+
application/json:
|
| 256 |
+
schema:
|
| 257 |
+
$ref: '#/components/schemas/ApiKey'
|
| 258 |
+
'404':
|
| 259 |
+
description: 未找到
|
| 260 |
+
|
| 261 |
+
delete:
|
| 262 |
+
summary: 删除 API Key
|
| 263 |
+
description: 仅 Admin 可操作,不能删除自己
|
| 264 |
+
security:
|
| 265 |
+
- bearerAuth: []
|
| 266 |
+
parameters:
|
| 267 |
+
- in: path
|
| 268 |
+
name: key_id
|
| 269 |
+
schema:
|
| 270 |
+
type: string
|
| 271 |
+
required: true
|
| 272 |
+
responses:
|
| 273 |
+
'204':
|
| 274 |
+
description: 删除成功
|
| 275 |
+
'400':
|
| 276 |
+
description: 无法删除(如试图删除自己)
|
| 277 |
+
|
| 278 |
+
/manage/keys/{key_id}/activate:
|
| 279 |
+
patch:
|
| 280 |
+
summary: 激活 API Key
|
| 281 |
+
description: Admin 可激活任意 Key,Regular 仅可激活关联的 Server Key
|
| 282 |
+
security:
|
| 283 |
+
- bearerAuth: []
|
| 284 |
+
parameters:
|
| 285 |
+
- in: path
|
| 286 |
+
name: key_id
|
| 287 |
+
schema:
|
| 288 |
+
type: string
|
| 289 |
+
required: true
|
| 290 |
+
responses:
|
| 291 |
+
'200':
|
| 292 |
+
description: 成功
|
| 293 |
+
content:
|
| 294 |
+
application/json:
|
| 295 |
+
schema:
|
| 296 |
+
type: object
|
| 297 |
+
properties:
|
| 298 |
+
message:
|
| 299 |
+
type: string
|
| 300 |
+
|
| 301 |
+
/manage/keys/{key_id}/deactivate:
|
| 302 |
+
patch:
|
| 303 |
+
summary: 停用 API Key
|
| 304 |
+
description: Admin 可停用任意 Key,Regular 仅可停用关联的 Server Key
|
| 305 |
+
security:
|
| 306 |
+
- bearerAuth: []
|
| 307 |
+
parameters:
|
| 308 |
+
- in: path
|
| 309 |
+
name: key_id
|
| 310 |
+
schema:
|
| 311 |
+
type: string
|
| 312 |
+
required: true
|
| 313 |
+
responses:
|
| 314 |
+
'200':
|
| 315 |
+
description: 成功
|
| 316 |
+
content:
|
| 317 |
+
application/json:
|
| 318 |
+
schema:
|
| 319 |
+
type: object
|
| 320 |
+
properties:
|
| 321 |
+
message:
|
| 322 |
+
type: string
|
| 323 |
+
|
| 324 |
+
/manage/keys/server-keys:
|
| 325 |
+
get:
|
| 326 |
+
summary: 获取 Server Key 列表
|
| 327 |
+
description: Admin 获取所有 Server Key,Regular 获取关联的 Server Key
|
| 328 |
+
security:
|
| 329 |
+
- bearerAuth: []
|
| 330 |
+
responses:
|
| 331 |
+
'200':
|
| 332 |
+
description: 成功
|
| 333 |
+
content:
|
| 334 |
+
application/json:
|
| 335 |
+
schema:
|
| 336 |
+
type: array
|
| 337 |
+
items:
|
| 338 |
+
$ref: '#/components/schemas/ApiKey'
|
| 339 |
+
|
| 340 |
+
post:
|
| 341 |
+
summary: 创建 Server Key
|
| 342 |
+
description: 为指定的 Regular Key 创建新的 Server Key (仅 Admin)
|
| 343 |
+
security:
|
| 344 |
+
- bearerAuth: []
|
| 345 |
+
requestBody:
|
| 346 |
+
required: true
|
| 347 |
+
content:
|
| 348 |
+
application/json:
|
| 349 |
+
schema:
|
| 350 |
+
$ref: '#/components/schemas/CreateServerKeyRequest'
|
| 351 |
+
responses:
|
| 352 |
+
'201':
|
| 353 |
+
description: 创建成功
|
| 354 |
+
content:
|
| 355 |
+
application/json:
|
| 356 |
+
schema:
|
| 357 |
+
$ref: '#/components/schemas/ApiKeyWithSecret'
|
| 358 |
+
'400':
|
| 359 |
+
description: 参数错误
|
| 360 |
+
'404':
|
| 361 |
+
description: Regular Key 不存在
|
| 362 |
+
|
| 363 |
+
/api/events:
|
| 364 |
+
post:
|
| 365 |
+
summary: 发送事件
|
| 366 |
+
description: 发送 Minecraft 事件并广播给所有 WebSocket 连接
|
| 367 |
+
security:
|
| 368 |
+
- bearerAuth: []
|
| 369 |
+
requestBody:
|
| 370 |
+
required: true
|
| 371 |
+
content:
|
| 372 |
+
application/json:
|
| 373 |
+
schema:
|
| 374 |
+
$ref: '#/components/schemas/Event'
|
| 375 |
+
responses:
|
| 376 |
+
'200':
|
| 377 |
+
description: 成功广播
|
| 378 |
+
|
| 379 |
+
/api/server/info:
|
| 380 |
+
get:
|
| 381 |
+
summary: 获取服务器信息
|
| 382 |
+
security:
|
| 383 |
+
- bearerAuth: []
|
| 384 |
+
parameters:
|
| 385 |
+
- in: query
|
| 386 |
+
name: server_id
|
| 387 |
+
schema:
|
| 388 |
+
type: string
|
| 389 |
+
description: 仅 Admin 需要
|
| 390 |
+
responses:
|
| 391 |
+
'200':
|
| 392 |
+
description: 成功
|
| 393 |
+
content:
|
| 394 |
+
application/json:
|
| 395 |
+
schema:
|
| 396 |
+
type: object
|
| 397 |
+
properties:
|
| 398 |
+
server_id:
|
| 399 |
+
type: string
|
| 400 |
+
status:
|
| 401 |
+
type: string
|
| 402 |
+
online_players:
|
| 403 |
+
type: integer
|
| 404 |
+
|
| 405 |
+
/api/server/command:
|
| 406 |
+
post:
|
| 407 |
+
summary: 发送服务器命令
|
| 408 |
+
security:
|
| 409 |
+
- bearerAuth: []
|
| 410 |
+
requestBody:
|
| 411 |
+
required: true
|
| 412 |
+
content:
|
| 413 |
+
application/json:
|
| 414 |
+
schema:
|
| 415 |
+
$ref: '#/components/schemas/CommandRequest'
|
| 416 |
+
responses:
|
| 417 |
+
'200':
|
| 418 |
+
description: 命令发送成功
|
| 419 |
+
'403':
|
| 420 |
+
description: 禁止的命令
|
| 421 |
+
|
| 422 |
+
/api/ai/config:
|
| 423 |
+
get:
|
| 424 |
+
summary: 获取 AI 配置 (Admin)
|
| 425 |
+
security:
|
| 426 |
+
- bearerAuth: []
|
| 427 |
+
responses:
|
| 428 |
+
'200':
|
| 429 |
+
description: 成功
|
| 430 |
+
content:
|
| 431 |
+
application/json:
|
| 432 |
+
schema:
|
| 433 |
+
$ref: '#/components/schemas/AiConfig'
|
| 434 |
+
|
| 435 |
+
post:
|
| 436 |
+
summary: 创建 AI 配置 (Admin)
|
| 437 |
+
security:
|
| 438 |
+
- bearerAuth: []
|
| 439 |
+
requestBody:
|
| 440 |
+
content:
|
| 441 |
+
application/json:
|
| 442 |
+
schema:
|
| 443 |
+
$ref: '#/components/schemas/AiConfigUpdateRequest'
|
| 444 |
+
responses:
|
| 445 |
+
'201':
|
| 446 |
+
description: 创建成功
|
| 447 |
+
|
| 448 |
+
patch:
|
| 449 |
+
summary: 更新 AI 配置 (Admin)
|
| 450 |
+
security:
|
| 451 |
+
- bearerAuth: []
|
| 452 |
+
requestBody:
|
| 453 |
+
content:
|
| 454 |
+
application/json:
|
| 455 |
+
schema:
|
| 456 |
+
$ref: '#/components/schemas/AiConfigUpdateRequest'
|
| 457 |
+
responses:
|
| 458 |
+
'200':
|
| 459 |
+
description: 更新成功
|
| 460 |
+
|
| 461 |
+
delete:
|
| 462 |
+
summary: 删除 AI 配置 (Admin)
|
| 463 |
+
security:
|
| 464 |
+
- bearerAuth: []
|
| 465 |
+
responses:
|
| 466 |
+
'204':
|
| 467 |
+
description: 删除成功
|
| 468 |
+
|
| 469 |
+
/api/ai/config/test:
|
| 470 |
+
post:
|
| 471 |
+
summary: 测试 AI 连接 (Admin)
|
| 472 |
+
security:
|
| 473 |
+
- bearerAuth: []
|
| 474 |
+
responses:
|
| 475 |
+
'200':
|
| 476 |
+
description: 连接成功
|
| 477 |
+
content:
|
| 478 |
+
application/json:
|
| 479 |
+
schema:
|
| 480 |
+
type: object
|
| 481 |
+
properties:
|
| 482 |
+
success:
|
| 483 |
+
type: boolean
|
| 484 |
+
message:
|
| 485 |
+
type: string
|
| 486 |
+
|
| 487 |
+
/api/ai/chat:
|
| 488 |
+
post:
|
| 489 |
+
summary: AI 聊天
|
| 490 |
+
description: 发送消息给 AI 并获取回复
|
| 491 |
+
security:
|
| 492 |
+
- bearerAuth: []
|
| 493 |
+
requestBody:
|
| 494 |
+
required: true
|
| 495 |
+
content:
|
| 496 |
+
application/json:
|
| 497 |
+
schema:
|
| 498 |
+
$ref: '#/components/schemas/AiChatRequest'
|
| 499 |
+
responses:
|
| 500 |
+
'200':
|
| 501 |
+
description: 成功
|
| 502 |
+
content:
|
| 503 |
+
application/json:
|
| 504 |
+
schema:
|
| 505 |
+
type: object
|
| 506 |
+
properties:
|
| 507 |
+
success:
|
| 508 |
+
type: boolean
|
| 509 |
+
reply:
|
| 510 |
+
type: string
|
| 511 |
+
model:
|
| 512 |
+
type: string
|
hf_repo/docs/TEST_REPORT.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# InterConnect-Server 测试总结报告
|
| 2 |
+
|
| 3 |
+
## 1. 测试概览
|
| 4 |
+
|
| 5 |
+
本项目已完成全面的自动化测试套件构建,覆盖了核心业务逻辑、数据库操作、认证机制以及 API 接口。
|
| 6 |
+
|
| 7 |
+
- **测试框架**: Jest
|
| 8 |
+
- **API 测试工具**: Supertest
|
| 9 |
+
- **测试结果**: ✅ 全部通过 (30/30 测试用例)
|
| 10 |
+
- **测试时间**: 2026-01-23
|
| 11 |
+
|
| 12 |
+
## 2. 测试覆盖范围
|
| 13 |
+
|
| 14 |
+
测试套件分为单元测试 (Unit Tests) 和集成测试 (Integration Tests) 两部分。
|
| 15 |
+
|
| 16 |
+
### 2.1 单元测试 (Unit Tests)
|
| 17 |
+
|
| 18 |
+
针对各个独立模块进行的功能验证。
|
| 19 |
+
|
| 20 |
+
#### 认证模块 (`src/auth.js`)
|
| 21 |
+
- **测试文件**: `tests/unit/auth.test.js`
|
| 22 |
+
- **覆盖内容**:
|
| 23 |
+
- `verifyApiKey`: 验证 API Key 的提取、校验和错误处理(401 Unauthorized)。
|
| 24 |
+
- `requireAdminKey`: 验证管理员权限控制。
|
| 25 |
+
- `requireRegularOrAdminKey`: 验证普通用户与管理员的权限兼容性。
|
| 26 |
+
- `requireAnyKey`: 验证基础访问权限。
|
| 27 |
+
|
| 28 |
+
#### 数据库模块 (`src/database.js`)
|
| 29 |
+
- **测试文件**: `tests/unit/database.test.js`
|
| 30 |
+
- **覆盖内容**:
|
| 31 |
+
- **初始化**: 验证数据库表结构 (`api_keys`, `event_logs`, `ai_config`) 的自动创建。
|
| 32 |
+
- **密钥管理**: 验证 Admin Key 的初始生成、Regular Key 和 Server Key 的创建与关联。
|
| 33 |
+
- **验证逻辑**: 验证 `verifyApiKey` 的准确性。
|
| 34 |
+
- **事件日志**: 验证 `logEvent` 和 `getRecentEvents` 的读写功能。
|
| 35 |
+
- **AI 配置**: 验证 AI 配置信息的保存、读取和更新。
|
| 36 |
+
|
| 37 |
+
#### WebSocket 管理器 (`src/websocket.js`)
|
| 38 |
+
- **测试文件**: `tests/unit/websocket.test.js`
|
| 39 |
+
- **覆盖内容**:
|
| 40 |
+
- **连接管理**: 验证连接跟踪 (`connect`) 和断开处理 (`disconnect`)。
|
| 41 |
+
- **广播功能**: 验证 `broadcastToAll` 能正确向所有活跃连接发送消息。
|
| 42 |
+
- **异常处理**: 验证在广播过程中自动清理已关闭的连接。
|
| 43 |
+
|
| 44 |
+
### 2.2 集成测试 (Integration Tests)
|
| 45 |
+
|
| 46 |
+
针对 HTTP API 接口的端到端测试。
|
| 47 |
+
|
| 48 |
+
#### API 接口 (`src/routes/*.js`)
|
| 49 |
+
- **测试文件**: `tests/integration/api.test.js`
|
| 50 |
+
- **覆盖内容**:
|
| 51 |
+
- **基础端点**:
|
| 52 |
+
- `GET /`: 验证服务器欢迎信息。
|
| 53 |
+
- `GET /health`: 验证健康检查接口返回 `healthy` 状态。
|
| 54 |
+
- **受保护路由**:
|
| 55 |
+
- `POST /api/events`: 验证无 Key、无效 Key 和有效 Key 的访问控制及业务逻辑。
|
| 56 |
+
- **密钥管理 API**:
|
| 57 |
+
- `GET /manage/keys`: 验证 Admin 只有权访问。
|
| 58 |
+
- `POST /manage/keys`: 验证密钥创建流程。
|
| 59 |
+
|
| 60 |
+
## 3. 测试环境配置
|
| 61 |
+
|
| 62 |
+
为了支持测试,对项目进行了以下配置(现已清理):
|
| 63 |
+
1. **依赖**: 安装了 `jest` 和 `supertest`。
|
| 64 |
+
2. **配置**: 添加了 `jest.config.js` 和 `tests/setup.js`。
|
| 65 |
+
3. **代码调整**: `src/server.js` 采用了条件启动模式,允许测试框架导入 App 实例而不自动监听端口。
|
| 66 |
+
|
| 67 |
+
## 4. 结论
|
| 68 |
+
|
| 69 |
+
当前代码库的核心功能稳定,权限控制逻辑严密,数据库操作符合预期。所有测试用例均已通过,可以放心地进行部署或后续开发。
|
hf_repo/hf_repo/.gitignore
CHANGED
|
@@ -57,3 +57,4 @@ cli/*.log
|
|
| 57 |
*.dockerfile
|
| 58 |
|
| 59 |
|
|
|
|
|
|
| 57 |
*.dockerfile
|
| 58 |
|
| 59 |
|
| 60 |
+
ICS-测试数据.txt
|
hf_repo/hf_repo/hf_repo/cli/cli.js
CHANGED
|
@@ -111,8 +111,10 @@ class MinecraftWSCLIClient {
|
|
| 111 |
return true;
|
| 112 |
}
|
| 113 |
|
| 114 |
-
async
|
| 115 |
-
|
|
|
|
|
|
|
| 116 |
}
|
| 117 |
}
|
| 118 |
|
|
@@ -125,6 +127,22 @@ program
|
|
| 125 |
.option('-s, --server-url <url>', 'API服务器URL', process.env.MC_WS_API_URL || HARDCODED_API_URL)
|
| 126 |
.option('-k, --admin-key <key>', '用于管理操作的Admin Key', process.env.ADMIN_KEY || HARDCODED_ADMIN_KEY);
|
| 127 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
program
|
| 129 |
.command('create-key <name>')
|
| 130 |
.description('创建新的API密钥')
|
|
|
|
| 111 |
return true;
|
| 112 |
}
|
| 113 |
|
| 114 |
+
async resetAIConfig() {
|
| 115 |
+
this.ensureAdminKeyForManagement();
|
| 116 |
+
await this.request('DELETE', '/api/ai/config');
|
| 117 |
+
return true;
|
| 118 |
}
|
| 119 |
}
|
| 120 |
|
|
|
|
| 127 |
.option('-s, --server-url <url>', 'API服务器URL', process.env.MC_WS_API_URL || HARDCODED_API_URL)
|
| 128 |
.option('-k, --admin-key <key>', '用于管理操作的Admin Key', process.env.ADMIN_KEY || HARDCODED_ADMIN_KEY);
|
| 129 |
|
| 130 |
+
program
|
| 131 |
+
.command('reset-ai-config')
|
| 132 |
+
.description('重置/清除 AI 配置 (使用 Admin Key)')
|
| 133 |
+
.action(async () => {
|
| 134 |
+
try {
|
| 135 |
+
const opts = program.opts();
|
| 136 |
+
const client = new MinecraftWSCLIClient(opts.serverUrl, opts.adminKey);
|
| 137 |
+
await client.resetAIConfig();
|
| 138 |
+
console.log(`\x1b[32m✅ AI 配置已成功重置/清除!\x1b[0m`);
|
| 139 |
+
console.log('现在您可以重新在 Dashboard 配置 AI 设置。');
|
| 140 |
+
} catch (error) {
|
| 141 |
+
console.error(`\x1b[31m❌ 错误: ${error.message}\x1b[0m`);
|
| 142 |
+
process.exit(1);
|
| 143 |
+
}
|
| 144 |
+
});
|
| 145 |
+
|
| 146 |
program
|
| 147 |
.command('create-key <name>')
|
| 148 |
.description('创建新的API密钥')
|
hf_repo/hf_repo/hf_repo/dashboard/public/app.js
CHANGED
|
@@ -35,8 +35,83 @@ 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}` };
|
| 42 |
}
|
|
@@ -172,33 +247,102 @@ function renderAdminKeys(keys) {
|
|
| 172 |
return;
|
| 173 |
}
|
| 174 |
|
| 175 |
-
|
| 176 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
<div class="key-info">
|
| 178 |
<h3>
|
| 179 |
<span class="key-badge ${key.keyType}">
|
| 180 |
${key.keyType === 'admin' ? 'Admin' : key.keyType === 'server' ? 'Server' : 'Regular'}
|
| 181 |
</span>
|
| 182 |
<span class="key-badge ${key.isActive ? 'active' : 'inactive'}">
|
| 183 |
-
${key.isActive ? '
|
| 184 |
</span>
|
| 185 |
${key.name}
|
| 186 |
</h3>
|
| 187 |
<p>ID: ${key.id}</p>
|
| 188 |
<p>Prefix: ${key.keyPrefix}</p>
|
| 189 |
${key.serverId ? `<p>Server ID: ${key.serverId}</p>` : ''}
|
| 190 |
-
<p
|
| 191 |
-
<p
|
| 192 |
</div>
|
| 193 |
<div class="key-actions">
|
| 194 |
${key.isActive
|
| 195 |
-
? `<button class="btn-danger" onclick="deactivateKey('${key.id}')"
|
| 196 |
-
: `<button class="btn-success" onclick="activateKey('${key.id}')"
|
| 197 |
}
|
| 198 |
-
<button class="btn-danger" onclick="deleteKey('${key.id}', '${key.name}')"
|
| 199 |
</div>
|
| 200 |
</div>
|
| 201 |
-
|
| 202 |
}
|
| 203 |
|
| 204 |
function renderUserServerKeys(keys) {
|
|
@@ -217,20 +361,20 @@ function renderUserServerKeys(keys) {
|
|
| 217 |
<h3>
|
| 218 |
<span class="key-badge server">Server</span>
|
| 219 |
<span class="key-badge ${key.isActive ? 'active' : 'inactive'}">
|
| 220 |
-
${key.isActive ? '
|
| 221 |
</span>
|
| 222 |
${key.name}
|
| 223 |
</h3>
|
| 224 |
<p>ID: ${key.id}</p>
|
| 225 |
<p>Prefix: ${key.keyPrefix}</p>
|
| 226 |
${key.serverId ? `<p>Server ID: ${key.serverId}</p>` : ''}
|
| 227 |
-
<p
|
| 228 |
-
<p
|
| 229 |
</div>
|
| 230 |
<div class="key-actions">
|
| 231 |
${key.isActive
|
| 232 |
-
? `<button class="btn-danger" onclick="deactivateKey('${key.id}')"
|
| 233 |
-
: `<button class="btn-success" onclick="activateKey('${key.id}')"
|
| 234 |
}
|
| 235 |
</div>
|
| 236 |
</div>
|
|
@@ -293,7 +437,7 @@ async function deleteKey(keyId, keyName) {
|
|
| 293 |
if (currentRole !== 'admin') {
|
| 294 |
return;
|
| 295 |
}
|
| 296 |
-
if (!confirm(
|
| 297 |
return;
|
| 298 |
}
|
| 299 |
try {
|
|
@@ -365,21 +509,6 @@ async function loadStats() {
|
|
| 365 |
connections.textContent = data.active_ws || 0;
|
| 366 |
}
|
| 367 |
|
| 368 |
-
const totalKeys = document.getElementById('user-stat-total-keys');
|
| 369 |
-
if (totalKeys) {
|
| 370 |
-
totalKeys.textContent = data.keys_total || 0;
|
| 371 |
-
}
|
| 372 |
-
|
| 373 |
-
const adminKeys = document.getElementById('user-stat-admin-keys');
|
| 374 |
-
if (adminKeys) {
|
| 375 |
-
adminKeys.textContent = data.admin_active || 0;
|
| 376 |
-
}
|
| 377 |
-
|
| 378 |
-
const regularKeys = document.getElementById('user-stat-regular-keys');
|
| 379 |
-
if (regularKeys) {
|
| 380 |
-
regularKeys.textContent = data.regular_active || 0;
|
| 381 |
-
}
|
| 382 |
-
|
| 383 |
const serverKeys = document.getElementById('user-stat-server-keys');
|
| 384 |
if (serverKeys) {
|
| 385 |
serverKeys.textContent = data.server_active || 0;
|
|
@@ -737,7 +866,7 @@ async function deleteAIConfig() {
|
|
| 737 |
if (!apiKey) {
|
| 738 |
return;
|
| 739 |
}
|
| 740 |
-
if (!confirm('
|
| 741 |
return;
|
| 742 |
}
|
| 743 |
|
|
|
|
| 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 aiProviderSelect = document.getElementById('ai-provider-select');
|
| 39 |
+
const systemLogs = document.getElementById('system-logs');
|
| 40 |
+
const clearLogsBtn = document.getElementById('clear-logs-btn');
|
| 41 |
+
|
| 42 |
+
// AI Providers Configuration
|
| 43 |
+
const AI_PROVIDERS = {
|
| 44 |
+
openai: {
|
| 45 |
+
url: 'https://api.openai.com/v1/chat/completions',
|
| 46 |
+
model: 'gpt-3.5-turbo'
|
| 47 |
+
},
|
| 48 |
+
siliconflow: {
|
| 49 |
+
url: 'https://api.siliconflow.cn/v1/chat/completions',
|
| 50 |
+
model: 'deepseek-ai/DeepSeek-R1'
|
| 51 |
+
},
|
| 52 |
+
gemini: {
|
| 53 |
+
url: 'https://generativelanguage.googleapis.com/v1beta/openai/chat/completions',
|
| 54 |
+
model: 'gemini-2.0-flash-exp'
|
| 55 |
+
},
|
| 56 |
+
deepseek: {
|
| 57 |
+
url: 'https://api.deepseek.com/chat/completions',
|
| 58 |
+
model: 'deepseek-chat'
|
| 59 |
+
},
|
| 60 |
+
moonshot: {
|
| 61 |
+
url: 'https://api.moonshot.cn/v1/chat/completions',
|
| 62 |
+
model: 'moonshot-v1-8k'
|
| 63 |
+
},
|
| 64 |
+
custom: {
|
| 65 |
+
url: '',
|
| 66 |
+
model: ''
|
| 67 |
+
}
|
| 68 |
+
};
|
| 69 |
+
|
| 70 |
+
if (aiProviderSelect) {
|
| 71 |
+
aiProviderSelect.addEventListener('change', (e) => {
|
| 72 |
+
const provider = AI_PROVIDERS[e.target.value];
|
| 73 |
+
if (provider && e.target.value !== 'custom') {
|
| 74 |
+
if (aiApiUrlInput) aiApiUrlInput.value = provider.url;
|
| 75 |
+
if (aiModelIdInput) aiModelIdInput.value = provider.model;
|
| 76 |
+
}
|
| 77 |
+
});
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
const aiStatus = document.getElementById('ai-status');
|
| 81 |
|
| 82 |
+
// Theme Management
|
| 83 |
+
const themeToggleBtns = document.querySelectorAll('.theme-toggle');
|
| 84 |
+
|
| 85 |
+
function initTheme() {
|
| 86 |
+
const savedTheme = localStorage.getItem('theme') || 'dark';
|
| 87 |
+
document.documentElement.setAttribute('data-theme', savedTheme);
|
| 88 |
+
updateThemeIcons(savedTheme);
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
function toggleTheme() {
|
| 92 |
+
const currentTheme = document.documentElement.getAttribute('data-theme');
|
| 93 |
+
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
| 94 |
+
|
| 95 |
+
document.documentElement.setAttribute('data-theme', newTheme);
|
| 96 |
+
localStorage.setItem('theme', newTheme);
|
| 97 |
+
updateThemeIcons(newTheme);
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
function updateThemeIcons(theme) {
|
| 101 |
+
const text = theme === 'dark' ? '浅色模式' : '深色模式';
|
| 102 |
+
|
| 103 |
+
themeToggleBtns.forEach(btn => {
|
| 104 |
+
btn.textContent = text;
|
| 105 |
+
});
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
// Initialize Theme
|
| 109 |
+
initTheme();
|
| 110 |
+
|
| 111 |
+
themeToggleBtns.forEach(btn => {
|
| 112 |
+
btn.addEventListener('click', toggleTheme);
|
| 113 |
+
});
|
| 114 |
+
|
| 115 |
function authHeaders(key) {
|
| 116 |
return { Authorization: `Bearer ${key}` };
|
| 117 |
}
|
|
|
|
| 247 |
return;
|
| 248 |
}
|
| 249 |
|
| 250 |
+
const adminKeys = keys.filter(k => k.keyType === 'admin');
|
| 251 |
+
const regularKeys = keys.filter(k => k.keyType === 'regular');
|
| 252 |
+
const serverKeys = keys.filter(k => k.keyType === 'server');
|
| 253 |
+
const serverKeysMap = {};
|
| 254 |
+
|
| 255 |
+
serverKeys.forEach(key => {
|
| 256 |
+
if (key.regularKeyId) {
|
| 257 |
+
if (!serverKeysMap[key.regularKeyId]) {
|
| 258 |
+
serverKeysMap[key.regularKeyId] = [];
|
| 259 |
+
}
|
| 260 |
+
serverKeysMap[key.regularKeyId].push(key);
|
| 261 |
+
}
|
| 262 |
+
});
|
| 263 |
+
|
| 264 |
+
let html = '';
|
| 265 |
+
|
| 266 |
+
// Render Admin Keys
|
| 267 |
+
if (adminKeys.length > 0) {
|
| 268 |
+
html += '<h3 class="group-title">管理员密钥 (Admin Keys)</h3>';
|
| 269 |
+
html += adminKeys.map(key => renderKeyCard(key)).join('');
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
// Render Regular Keys with nested Server Keys
|
| 273 |
+
if (regularKeys.length > 0) {
|
| 274 |
+
html += '<h3 class="group-title">用户密钥 (Regular Keys)</h3>';
|
| 275 |
+
html += regularKeys.map(regularKey => {
|
| 276 |
+
const childServerKeys = serverKeysMap[regularKey.id] || [];
|
| 277 |
+
return `
|
| 278 |
+
<div class="regular-key-group">
|
| 279 |
+
<div class="regular-key-header" onclick="toggleGroup(this)">
|
| 280 |
+
${renderKeyCard(regularKey)}
|
| 281 |
+
${childServerKeys.length > 0 ? `<span class="toggle-icon">▼</span>` : ''}
|
| 282 |
+
</div>
|
| 283 |
+
${childServerKeys.length > 0 ? `
|
| 284 |
+
<div class="nested-server-keys hidden">
|
| 285 |
+
<h4>关联的服务器密钥 (Linked Server Keys)</h4>
|
| 286 |
+
${childServerKeys.map(serverKey => renderKeyCard(serverKey, true)).join('')}
|
| 287 |
+
</div>
|
| 288 |
+
` : ''}
|
| 289 |
+
</div>
|
| 290 |
+
`;
|
| 291 |
+
}).join('');
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
// Render Orphaned Server Keys (if any)
|
| 295 |
+
const linkedServerKeyIds = new Set(Object.values(serverKeysMap).flat().map(k => k.id));
|
| 296 |
+
const orphanServerKeys = serverKeys.filter(k => !linkedServerKeyIds.has(k.id));
|
| 297 |
+
|
| 298 |
+
if (orphanServerKeys.length > 0) {
|
| 299 |
+
html += '<h3 class="group-title">独立服务器密钥 (Orphan Server Keys)</h3>';
|
| 300 |
+
html += orphanServerKeys.map(key => renderKeyCard(key)).join('');
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
adminKeysList.innerHTML = html;
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
function toggleGroup(headerElement) {
|
| 307 |
+
const nestedContainer = headerElement.nextElementSibling;
|
| 308 |
+
const toggleIcon = headerElement.querySelector('.toggle-icon');
|
| 309 |
+
|
| 310 |
+
if (nestedContainer && nestedContainer.classList.contains('nested-server-keys')) {
|
| 311 |
+
nestedContainer.classList.toggle('hidden');
|
| 312 |
+
if (toggleIcon) {
|
| 313 |
+
toggleIcon.style.transform = nestedContainer.classList.contains('hidden') ? 'rotate(0deg)' : 'rotate(180deg)';
|
| 314 |
+
}
|
| 315 |
+
}
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
function renderKeyCard(key, isNested = false) {
|
| 319 |
+
return `
|
| 320 |
+
<div class="key-card ${key.keyType === 'admin' ? 'admin' : ''} ${isNested ? 'nested' : ''}">
|
| 321 |
<div class="key-info">
|
| 322 |
<h3>
|
| 323 |
<span class="key-badge ${key.keyType}">
|
| 324 |
${key.keyType === 'admin' ? 'Admin' : key.keyType === 'server' ? 'Server' : 'Regular'}
|
| 325 |
</span>
|
| 326 |
<span class="key-badge ${key.isActive ? 'active' : 'inactive'}">
|
| 327 |
+
${key.isActive ? '已启用' : '已停用'}
|
| 328 |
</span>
|
| 329 |
${key.name}
|
| 330 |
</h3>
|
| 331 |
<p>ID: ${key.id}</p>
|
| 332 |
<p>Prefix: ${key.keyPrefix}</p>
|
| 333 |
${key.serverId ? `<p>Server ID: ${key.serverId}</p>` : ''}
|
| 334 |
+
<p>创建时间: ${new Date(key.createdAt).toLocaleString()}</p>
|
| 335 |
+
<p>最后使用: ${key.lastUsed ? new Date(key.lastUsed).toLocaleString() : '从未'}</p>
|
| 336 |
</div>
|
| 337 |
<div class="key-actions">
|
| 338 |
${key.isActive
|
| 339 |
+
? `<button class="btn-danger" onclick="deactivateKey('${key.id}')">停用</button>`
|
| 340 |
+
: `<button class="btn-success" onclick="activateKey('${key.id}')">启用</button>`
|
| 341 |
}
|
| 342 |
+
<button class="btn-danger" onclick="deleteKey('${key.id}', '${key.name}')">删除</button>
|
| 343 |
</div>
|
| 344 |
</div>
|
| 345 |
+
`;
|
| 346 |
}
|
| 347 |
|
| 348 |
function renderUserServerKeys(keys) {
|
|
|
|
| 361 |
<h3>
|
| 362 |
<span class="key-badge server">Server</span>
|
| 363 |
<span class="key-badge ${key.isActive ? 'active' : 'inactive'}">
|
| 364 |
+
${key.isActive ? '已启用' : '已停用'}
|
| 365 |
</span>
|
| 366 |
${key.name}
|
| 367 |
</h3>
|
| 368 |
<p>ID: ${key.id}</p>
|
| 369 |
<p>Prefix: ${key.keyPrefix}</p>
|
| 370 |
${key.serverId ? `<p>Server ID: ${key.serverId}</p>` : ''}
|
| 371 |
+
<p>创建时间: ${new Date(key.createdAt).toLocaleString()}</p>
|
| 372 |
+
<p>最后使用: ${key.lastUsed ? new Date(key.lastUsed).toLocaleString() : '从未'}</p>
|
| 373 |
</div>
|
| 374 |
<div class="key-actions">
|
| 375 |
${key.isActive
|
| 376 |
+
? `<button class="btn-danger" onclick="deactivateKey('${key.id}')">停用</button>`
|
| 377 |
+
: `<button class="btn-success" onclick="activateKey('${key.id}')">启用</button>`
|
| 378 |
}
|
| 379 |
</div>
|
| 380 |
</div>
|
|
|
|
| 437 |
if (currentRole !== 'admin') {
|
| 438 |
return;
|
| 439 |
}
|
| 440 |
+
if (!confirm(`确定要删除密钥 "${keyName}" 吗? 此操作无法撤销。`)) {
|
| 441 |
return;
|
| 442 |
}
|
| 443 |
try {
|
|
|
|
| 509 |
connections.textContent = data.active_ws || 0;
|
| 510 |
}
|
| 511 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 512 |
const serverKeys = document.getElementById('user-stat-server-keys');
|
| 513 |
if (serverKeys) {
|
| 514 |
serverKeys.textContent = data.server_active || 0;
|
|
|
|
| 866 |
if (!apiKey) {
|
| 867 |
return;
|
| 868 |
}
|
| 869 |
+
if (!confirm('确定要删除 AI 配置吗?')) {
|
| 870 |
return;
|
| 871 |
}
|
| 872 |
|
hf_repo/hf_repo/hf_repo/dashboard/public/index.html
CHANGED
|
@@ -3,7 +3,10 @@
|
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
-
<title>
|
|
|
|
|
|
|
|
|
|
| 7 |
<link rel="stylesheet" href="style.css">
|
| 8 |
</head>
|
| 9 |
<body>
|
|
@@ -28,6 +31,9 @@
|
|
| 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>
|
|
@@ -86,6 +92,9 @@
|
|
| 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>
|
|
@@ -97,18 +106,6 @@
|
|
| 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">
|
| 105 |
-
<h3>Admin Keys</h3>
|
| 106 |
-
<p class="stat-value" id="user-stat-admin-keys">0</p>
|
| 107 |
-
</div>
|
| 108 |
-
<div class="stat-card">
|
| 109 |
-
<h3>Regular Keys</h3>
|
| 110 |
-
<p class="stat-value" id="user-stat-regular-keys">0</p>
|
| 111 |
-
</div>
|
| 112 |
<div class="stat-card">
|
| 113 |
<h3>Server Keys</h3>
|
| 114 |
<p class="stat-value" id="user-stat-server-keys">0</p>
|
|
|
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>InterConnect Dashboard</title>
|
| 7 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 8 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 9 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
| 10 |
<link rel="stylesheet" href="style.css">
|
| 11 |
</head>
|
| 12 |
<body>
|
|
|
|
| 31 |
<nav class="navbar">
|
| 32 |
<h1>管理员面板</h1>
|
| 33 |
<div class="nav-info">
|
| 34 |
+
<button id="admin-theme-toggle" class="theme-toggle" title="切换深色/浅色模式">
|
| 35 |
+
<!-- Icon will be injected by JS -->
|
| 36 |
+
</button>
|
| 37 |
<span id="admin-user-info"></span>
|
| 38 |
<button class="logout-btn">退出登录</button>
|
| 39 |
</div>
|
|
|
|
| 92 |
<nav class="navbar">
|
| 93 |
<h1>用户监测</h1>
|
| 94 |
<div class="nav-info">
|
| 95 |
+
<button id="user-theme-toggle" class="theme-toggle" title="切换深色/浅色模式">
|
| 96 |
+
<!-- Icon will be injected by JS -->
|
| 97 |
+
</button>
|
| 98 |
<span id="user-info"></span>
|
| 99 |
<button class="logout-btn">退出登录</button>
|
| 100 |
</div>
|
|
|
|
| 106 |
<h3>活跃连接</h3>
|
| 107 |
<p class="stat-value" id="user-stat-connections">0</p>
|
| 108 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
<div class="stat-card">
|
| 110 |
<h3>Server Keys</h3>
|
| 111 |
<p class="stat-value" id="user-stat-server-keys">0</p>
|
hf_repo/hf_repo/hf_repo/dashboard/public/style.css
CHANGED
|
@@ -1,3 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
* {
|
| 2 |
margin: 0;
|
| 3 |
padding: 0;
|
|
@@ -5,516 +76,671 @@
|
|
| 5 |
}
|
| 6 |
|
| 7 |
body {
|
| 8 |
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
| 9 |
-
background:
|
|
|
|
| 10 |
min-height: 100vh;
|
| 11 |
-
|
|
|
|
| 12 |
}
|
| 13 |
|
| 14 |
-
|
| 15 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 28 |
-
|
| 29 |
}
|
| 30 |
|
| 31 |
.login-container h1 {
|
| 32 |
-
|
| 33 |
-
font-
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
}
|
| 36 |
|
| 37 |
.login-container h2 {
|
| 38 |
-
color:
|
| 39 |
-
font-size: 1.
|
| 40 |
-
|
|
|
|
| 41 |
}
|
| 42 |
|
| 43 |
#login-form {
|
| 44 |
-
background:
|
|
|
|
| 45 |
padding: 40px;
|
| 46 |
-
border-radius:
|
| 47 |
-
|
|
|
|
| 48 |
width: 100%;
|
| 49 |
-
max-width:
|
|
|
|
| 50 |
}
|
| 51 |
|
| 52 |
#login-form input {
|
| 53 |
width: 100%;
|
| 54 |
-
padding:
|
| 55 |
-
|
| 56 |
-
border
|
| 57 |
-
|
| 58 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
}
|
| 60 |
|
| 61 |
#login-form button {
|
| 62 |
width: 100%;
|
| 63 |
-
padding:
|
| 64 |
-
background:
|
| 65 |
color: white;
|
| 66 |
border: none;
|
| 67 |
-
border-radius:
|
| 68 |
-
font-size:
|
| 69 |
-
font-weight:
|
| 70 |
cursor: pointer;
|
| 71 |
-
transition:
|
|
|
|
| 72 |
}
|
| 73 |
|
| 74 |
#login-form button:hover {
|
| 75 |
-
|
|
|
|
| 76 |
}
|
| 77 |
|
| 78 |
.error-message {
|
| 79 |
-
color:
|
| 80 |
-
margin-top:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
text-align: center;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
}
|
| 83 |
|
|
|
|
| 84 |
.navbar {
|
| 85 |
-
background:
|
| 86 |
-
|
|
|
|
| 87 |
display: flex;
|
| 88 |
justify-content: space-between;
|
| 89 |
align-items: center;
|
| 90 |
-
|
|
|
|
|
|
|
|
|
|
| 91 |
}
|
| 92 |
|
| 93 |
.navbar h1 {
|
| 94 |
-
font-size: 1.
|
| 95 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
}
|
| 97 |
|
| 98 |
.logout-btn {
|
| 99 |
-
padding:
|
| 100 |
-
background:
|
| 101 |
-
color:
|
| 102 |
-
border:
|
| 103 |
-
border-radius:
|
| 104 |
cursor: pointer;
|
| 105 |
-
font-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
}
|
| 107 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
.container {
|
| 109 |
-
max-width:
|
| 110 |
margin: 0 auto;
|
| 111 |
-
padding:
|
|
|
|
| 112 |
}
|
| 113 |
|
|
|
|
| 114 |
.stats-grid {
|
| 115 |
display: grid;
|
| 116 |
-
grid-template-columns: repeat(auto-fit, minmax(
|
| 117 |
-
gap:
|
| 118 |
-
margin-bottom:
|
| 119 |
}
|
| 120 |
|
| 121 |
.stat-card {
|
| 122 |
-
background:
|
| 123 |
-
padding:
|
| 124 |
-
border-radius:
|
| 125 |
-
|
|
|
|
| 126 |
text-align: center;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
}
|
| 128 |
|
| 129 |
.stat-card h3 {
|
| 130 |
-
color:
|
| 131 |
-
font-size: 0.
|
| 132 |
-
margin-bottom: 10px;
|
| 133 |
text-transform: uppercase;
|
|
|
|
|
|
|
| 134 |
}
|
| 135 |
|
| 136 |
.stat-value {
|
| 137 |
-
font-size:
|
| 138 |
-
font-weight:
|
| 139 |
-
color:
|
|
|
|
|
|
|
|
|
|
| 140 |
}
|
| 141 |
|
|
|
|
| 142 |
.section {
|
| 143 |
-
background:
|
| 144 |
-
padding:
|
| 145 |
-
border-radius:
|
| 146 |
-
|
| 147 |
-
margin-bottom:
|
|
|
|
| 148 |
}
|
| 149 |
|
| 150 |
.section-header {
|
| 151 |
display: flex;
|
| 152 |
justify-content: space-between;
|
| 153 |
align-items: center;
|
| 154 |
-
margin-bottom:
|
|
|
|
|
|
|
| 155 |
}
|
| 156 |
|
| 157 |
.section h2 {
|
| 158 |
-
|
| 159 |
-
|
|
|
|
| 160 |
}
|
| 161 |
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
border-radius: 5px;
|
| 168 |
cursor: pointer;
|
| 169 |
-
|
| 170 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
}
|
| 172 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 173 |
.btn-primary:hover {
|
| 174 |
-
background:
|
|
|
|
| 175 |
}
|
| 176 |
|
| 177 |
.btn-secondary {
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
}
|
| 186 |
|
| 187 |
.btn-danger {
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
}
|
| 196 |
|
| 197 |
.btn-success {
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
}
|
| 206 |
|
|
|
|
| 207 |
.keys-list {
|
| 208 |
-
display:
|
| 209 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
}
|
| 211 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 212 |
.key-card {
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
border-radius:
|
|
|
|
| 216 |
display: flex;
|
| 217 |
-
justify-content: space-between;
|
| 218 |
align-items: center;
|
|
|
|
| 219 |
}
|
| 220 |
|
| 221 |
.key-card.admin {
|
| 222 |
-
border-color:
|
| 223 |
-
background:
|
| 224 |
}
|
| 225 |
|
| 226 |
-
.key-
|
| 227 |
-
|
|
|
|
| 228 |
}
|
| 229 |
|
| 230 |
-
.key-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
font-weight: bold;
|
| 236 |
-
margin-right: 10px;
|
| 237 |
}
|
| 238 |
|
| 239 |
-
.key-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
}
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
background: #667eea;
|
| 246 |
-
color: white;
|
| 247 |
}
|
| 248 |
|
| 249 |
-
.key-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
}
|
| 253 |
|
| 254 |
-
.
|
| 255 |
-
|
| 256 |
-
|
|
|
|
| 257 |
}
|
| 258 |
|
| 259 |
-
.
|
| 260 |
-
background:
|
| 261 |
-
color: white;
|
| 262 |
}
|
| 263 |
|
| 264 |
-
.
|
| 265 |
-
|
| 266 |
-
|
|
|
|
|
|
|
| 267 |
}
|
| 268 |
|
| 269 |
-
.
|
| 270 |
-
|
| 271 |
-
|
|
|
|
| 272 |
}
|
| 273 |
|
| 274 |
-
.
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
background: #f5f5f5;
|
| 278 |
-
margin-bottom: 10px;
|
| 279 |
-
border-radius: 4px;
|
| 280 |
-
}
|
| 281 |
-
|
| 282 |
-
.event-item strong {
|
| 283 |
-
color: #667eea;
|
| 284 |
-
}
|
| 285 |
-
|
| 286 |
-
.modal {
|
| 287 |
-
position: fixed;
|
| 288 |
-
top: 0;
|
| 289 |
-
left: 0;
|
| 290 |
-
width: 100%;
|
| 291 |
-
height: 100%;
|
| 292 |
-
background: rgba(0,0,0,0.5);
|
| 293 |
display: flex;
|
| 294 |
-
justify-content: center;
|
| 295 |
align-items: center;
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
background: white;
|
| 301 |
-
padding: 40px;
|
| 302 |
-
border-radius: 10px;
|
| 303 |
-
max-width: 500px;
|
| 304 |
-
width: 90%;
|
| 305 |
-
max-height: 90vh;
|
| 306 |
-
overflow-y: auto;
|
| 307 |
-
}
|
| 308 |
-
|
| 309 |
-
.modal-content h2 {
|
| 310 |
-
margin-bottom: 20px;
|
| 311 |
-
}
|
| 312 |
-
|
| 313 |
-
.modal-content label {
|
| 314 |
-
display: block;
|
| 315 |
-
margin-bottom: 5px;
|
| 316 |
-
font-weight: bold;
|
| 317 |
-
color: #666;
|
| 318 |
-
}
|
| 319 |
-
|
| 320 |
-
.modal-content input[type="text"],
|
| 321 |
-
.modal-content textarea {
|
| 322 |
-
width: 100%;
|
| 323 |
-
padding: 10px;
|
| 324 |
-
border: 2px solid #e0e0e0;
|
| 325 |
-
border-radius: 5px;
|
| 326 |
-
margin-bottom: 15px;
|
| 327 |
-
font-size: 14px;
|
| 328 |
}
|
| 329 |
|
| 330 |
-
.
|
| 331 |
-
|
| 332 |
-
resize: vertical;
|
| 333 |
}
|
| 334 |
|
| 335 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 336 |
width: 100%;
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
border-radius: 5px;
|
| 340 |
-
margin-bottom: 15px;
|
| 341 |
-
font-size: 14px;
|
| 342 |
}
|
| 343 |
|
| 344 |
-
.
|
| 345 |
-
|
| 346 |
-
gap: 10px;
|
| 347 |
-
justify-content: flex-end;
|
| 348 |
-
margin-top: 20px;
|
| 349 |
}
|
| 350 |
|
| 351 |
-
.
|
| 352 |
display: flex;
|
| 353 |
align-items: center;
|
| 354 |
-
|
|
|
|
|
|
|
| 355 |
}
|
| 356 |
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
font-size: 0.
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
padding: 8px 12px;
|
| 363 |
-
border-radius: 20px;
|
| 364 |
}
|
| 365 |
|
| 366 |
-
.
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
|
|
|
|
|
|
|
|
|
| 370 |
}
|
| 371 |
|
| 372 |
-
.
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
border-radius: 8px;
|
| 376 |
-
border: 1px solid #e9ecef;
|
| 377 |
-
}
|
| 378 |
|
| 379 |
-
.
|
| 380 |
-
|
| 381 |
-
color: #495057;
|
| 382 |
-
}
|
| 383 |
|
| 384 |
-
.
|
| 385 |
-
max-height: 200px;
|
| 386 |
-
overflow-y: auto;
|
| 387 |
-
background: white;
|
| 388 |
-
border: 1px solid #e9ecef;
|
| 389 |
-
border-radius: 4px;
|
| 390 |
-
padding: 10px;
|
| 391 |
-
}
|
| 392 |
-
|
| 393 |
-
.command-form {
|
| 394 |
display: flex;
|
| 395 |
-
gap:
|
| 396 |
-
margin-
|
| 397 |
}
|
| 398 |
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
border: 2px solid #e0e0e0;
|
| 403 |
-
border-radius: 5px;
|
| 404 |
-
font-size: 14px;
|
| 405 |
}
|
| 406 |
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
font-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
.command-item:last-child {
|
| 415 |
-
border-bottom: none;
|
| 416 |
}
|
| 417 |
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 421 |
}
|
| 422 |
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
padding: 15px;
|
| 428 |
-
border-radius: 8px;
|
| 429 |
}
|
| 430 |
|
| 431 |
-
.
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
|
|
|
| 435 |
}
|
| 436 |
|
| 437 |
-
|
| 438 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 439 |
}
|
| 440 |
|
| 441 |
-
.
|
| 442 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 443 |
}
|
| 444 |
|
| 445 |
-
.
|
| 446 |
-
|
| 447 |
-
margin-bottom: 8px;
|
| 448 |
-
font-weight: bold;
|
| 449 |
-
color: #333;
|
| 450 |
}
|
| 451 |
|
| 452 |
-
.
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
transition: border-color 0.3s;
|
| 461 |
}
|
| 462 |
|
| 463 |
-
|
| 464 |
-
.
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 468 |
}
|
| 469 |
|
| 470 |
-
.
|
| 471 |
-
|
| 472 |
-
|
|
|
|
|
|
|
|
|
|
| 473 |
}
|
| 474 |
|
| 475 |
-
.
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
color: #666;
|
| 480 |
}
|
| 481 |
|
| 482 |
-
.
|
| 483 |
display: flex;
|
| 484 |
-
gap:
|
| 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 |
-
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 503 |
}
|
| 504 |
|
| 505 |
-
.
|
| 506 |
-
background:
|
| 507 |
-
|
| 508 |
-
border:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 509 |
}
|
| 510 |
|
| 511 |
-
.
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
|
|
|
| 515 |
}
|
| 516 |
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
margin-
|
| 520 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
:root {
|
| 2 |
+
/* Colors - Light Theme (Clean & Professional) */
|
| 3 |
+
--bg-body: #f3f4f6;
|
| 4 |
+
--bg-card: #ffffff;
|
| 5 |
+
--bg-card-hover: #f9fafb;
|
| 6 |
+
--bg-input: #ffffff;
|
| 7 |
+
|
| 8 |
+
--border-color: #e5e7eb;
|
| 9 |
+
--border-hover: #d1d5db;
|
| 10 |
+
|
| 11 |
+
--primary: #4f46e5; /* Slightly darker indigo for better contrast on white */
|
| 12 |
+
--primary-hover: #4338ca;
|
| 13 |
+
--primary-glow: rgba(79, 70, 229, 0.15);
|
| 14 |
+
|
| 15 |
+
--danger: #dc2626;
|
| 16 |
+
--danger-hover: #b91c1c;
|
| 17 |
+
|
| 18 |
+
--success: #059669;
|
| 19 |
+
--success-hover: #047857;
|
| 20 |
+
|
| 21 |
+
--text-main: #111827;
|
| 22 |
+
--text-muted: #6b7280;
|
| 23 |
+
|
| 24 |
+
/* Spacing & Radius */
|
| 25 |
+
--radius-sm: 6px;
|
| 26 |
+
--radius-md: 12px;
|
| 27 |
+
--radius-lg: 16px;
|
| 28 |
+
--radius-xl: 24px;
|
| 29 |
+
|
| 30 |
+
--space-xs: 4px;
|
| 31 |
+
--space-sm: 8px;
|
| 32 |
+
--space-md: 16px;
|
| 33 |
+
--space-lg: 24px;
|
| 34 |
+
--space-xl: 32px;
|
| 35 |
+
|
| 36 |
+
/* Effects */
|
| 37 |
+
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
| 38 |
+
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
| 39 |
+
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
| 40 |
+
--glass-blur: blur(12px);
|
| 41 |
+
--transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
[data-theme="dark"] {
|
| 45 |
+
/* Colors - Dark Theme */
|
| 46 |
+
--bg-body: #09090b;
|
| 47 |
+
--bg-card: rgba(39, 39, 42, 0.6);
|
| 48 |
+
--bg-card-hover: rgba(63, 63, 70, 0.6);
|
| 49 |
+
--bg-input: rgba(24, 24, 27, 0.8);
|
| 50 |
+
|
| 51 |
+
--border-color: rgba(255, 255, 255, 0.08);
|
| 52 |
+
--border-hover: rgba(255, 255, 255, 0.15);
|
| 53 |
+
|
| 54 |
+
--primary: #6366f1;
|
| 55 |
+
--primary-hover: #4f46e5;
|
| 56 |
+
--primary-glow: rgba(99, 102, 241, 0.4);
|
| 57 |
+
|
| 58 |
+
--danger: #ef4444;
|
| 59 |
+
--danger-hover: #dc2626;
|
| 60 |
+
|
| 61 |
+
--success: #10b981;
|
| 62 |
+
--success-hover: #059669;
|
| 63 |
+
|
| 64 |
+
--text-main: #fafafa;
|
| 65 |
+
--text-muted: #a1a1aa;
|
| 66 |
+
|
| 67 |
+
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.5);
|
| 68 |
+
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.5);
|
| 69 |
+
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.5);
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
* {
|
| 73 |
margin: 0;
|
| 74 |
padding: 0;
|
|
|
|
| 76 |
}
|
| 77 |
|
| 78 |
body {
|
| 79 |
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
| 80 |
+
background-color: var(--bg-body);
|
| 81 |
+
color: var(--text-main);
|
| 82 |
min-height: 100vh;
|
| 83 |
+
line-height: 1.6;
|
| 84 |
+
-webkit-font-smoothing: antialiased;
|
| 85 |
}
|
| 86 |
|
| 87 |
+
[data-theme="dark"] body {
|
| 88 |
+
background-image:
|
| 89 |
+
radial-gradient(circle at 15% 50%, rgba(99, 102, 241, 0.08), transparent 25%),
|
| 90 |
+
radial-gradient(circle at 85% 30%, rgba(139, 92, 246, 0.08), transparent 25%);
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
/* Scrollbar */
|
| 94 |
+
::-webkit-scrollbar {
|
| 95 |
+
width: 8px;
|
| 96 |
+
height: 8px;
|
| 97 |
+
}
|
| 98 |
+
::-webkit-scrollbar-track {
|
| 99 |
+
background: transparent;
|
| 100 |
+
}
|
| 101 |
+
::-webkit-scrollbar-thumb {
|
| 102 |
+
background: rgba(255, 255, 255, 0.1);
|
| 103 |
+
border-radius: 4px;
|
| 104 |
+
}
|
| 105 |
+
::-webkit-scrollbar-thumb:hover {
|
| 106 |
+
background: rgba(255, 255, 255, 0.2);
|
| 107 |
}
|
| 108 |
|
| 109 |
+
/* Utilities */
|
| 110 |
.hidden {
|
| 111 |
display: none !important;
|
| 112 |
}
|
| 113 |
|
| 114 |
+
.screen {
|
| 115 |
+
min-height: 100vh;
|
| 116 |
+
display: flex;
|
| 117 |
+
flex-direction: column;
|
| 118 |
+
animation: fadeIn 0.5s ease-out;
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
@keyframes fadeIn {
|
| 122 |
+
from { opacity: 0; transform: translateY(10px); }
|
| 123 |
+
to { opacity: 1; transform: translateY(0); }
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
/* Login Screen */
|
| 127 |
.login-container {
|
| 128 |
+
flex: 1;
|
| 129 |
display: flex;
|
| 130 |
flex-direction: column;
|
| 131 |
align-items: center;
|
| 132 |
justify-content: center;
|
| 133 |
+
padding: var(--space-xl);
|
| 134 |
+
background: radial-gradient(circle at center, rgba(99, 102, 241, 0.1) 0%, transparent 70%);
|
| 135 |
}
|
| 136 |
|
| 137 |
.login-container h1 {
|
| 138 |
+
font-size: 2.5rem;
|
| 139 |
+
font-weight: 800;
|
| 140 |
+
letter-spacing: -0.05em;
|
| 141 |
+
margin-bottom: var(--space-xs);
|
| 142 |
+
background: linear-gradient(135deg, #fff 0%, #a1a1aa 100%);
|
| 143 |
+
-webkit-background-clip: text;
|
| 144 |
+
-webkit-text-fill-color: transparent;
|
| 145 |
+
text-shadow: 0 0 30px rgba(99, 102, 241, 0.3);
|
| 146 |
}
|
| 147 |
|
| 148 |
.login-container h2 {
|
| 149 |
+
color: var(--text-muted);
|
| 150 |
+
font-size: 1.1rem;
|
| 151 |
+
font-weight: 400;
|
| 152 |
+
margin-bottom: var(--space-xl);
|
| 153 |
}
|
| 154 |
|
| 155 |
#login-form {
|
| 156 |
+
background: var(--bg-card);
|
| 157 |
+
backdrop-filter: var(--glass-blur);
|
| 158 |
padding: 40px;
|
| 159 |
+
border-radius: var(--radius-xl);
|
| 160 |
+
border: 1px solid var(--border-color);
|
| 161 |
+
box-shadow: var(--shadow-lg);
|
| 162 |
width: 100%;
|
| 163 |
+
max-width: 420px;
|
| 164 |
+
transition: var(--transition);
|
| 165 |
}
|
| 166 |
|
| 167 |
#login-form input {
|
| 168 |
width: 100%;
|
| 169 |
+
padding: 16px;
|
| 170 |
+
background: var(--bg-input);
|
| 171 |
+
border: 1px solid var(--border-color);
|
| 172 |
+
border-radius: var(--radius-md);
|
| 173 |
+
color: var(--text-main);
|
| 174 |
+
font-size: 1rem;
|
| 175 |
+
margin-bottom: var(--space-lg);
|
| 176 |
+
transition: var(--transition);
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
#login-form input:focus {
|
| 180 |
+
outline: none;
|
| 181 |
+
border-color: var(--primary);
|
| 182 |
+
box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.1);
|
| 183 |
}
|
| 184 |
|
| 185 |
#login-form button {
|
| 186 |
width: 100%;
|
| 187 |
+
padding: 16px;
|
| 188 |
+
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-hover) 100%);
|
| 189 |
color: white;
|
| 190 |
border: none;
|
| 191 |
+
border-radius: var(--radius-md);
|
| 192 |
+
font-size: 1rem;
|
| 193 |
+
font-weight: 600;
|
| 194 |
cursor: pointer;
|
| 195 |
+
transition: var(--transition);
|
| 196 |
+
box-shadow: 0 4px 12px var(--primary-glow);
|
| 197 |
}
|
| 198 |
|
| 199 |
#login-form button:hover {
|
| 200 |
+
transform: translateY(-2px);
|
| 201 |
+
box-shadow: 0 8px 20px var(--primary-glow);
|
| 202 |
}
|
| 203 |
|
| 204 |
.error-message {
|
| 205 |
+
color: var(--danger);
|
| 206 |
+
margin-top: var(--space-md);
|
| 207 |
+
text-align: center;
|
| 208 |
+
font-size: 0.9rem;
|
| 209 |
+
min-height: 20px;
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
.login-info {
|
| 213 |
+
margin-top: var(--space-xl);
|
| 214 |
text-align: center;
|
| 215 |
+
color: var(--text-muted);
|
| 216 |
+
font-size: 0.9rem;
|
| 217 |
+
background: rgba(255, 255, 255, 0.03);
|
| 218 |
+
padding: var(--space-md) var(--space-lg);
|
| 219 |
+
border-radius: var(--radius-lg);
|
| 220 |
+
border: 1px solid var(--border-color);
|
| 221 |
}
|
| 222 |
|
| 223 |
+
/* Navbar */
|
| 224 |
.navbar {
|
| 225 |
+
background: var(--bg-card);
|
| 226 |
+
backdrop-filter: blur(20px);
|
| 227 |
+
padding: 16px 32px;
|
| 228 |
display: flex;
|
| 229 |
justify-content: space-between;
|
| 230 |
align-items: center;
|
| 231 |
+
border-bottom: 1px solid var(--border-color);
|
| 232 |
+
position: sticky;
|
| 233 |
+
top: 0;
|
| 234 |
+
z-index: 100;
|
| 235 |
}
|
| 236 |
|
| 237 |
.navbar h1 {
|
| 238 |
+
font-size: 1.25rem;
|
| 239 |
+
font-weight: 700;
|
| 240 |
+
letter-spacing: -0.02em;
|
| 241 |
+
color: var(--text-main);
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
.nav-info {
|
| 245 |
+
display: flex;
|
| 246 |
+
align-items: center;
|
| 247 |
+
gap: var(--space-md);
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
#user-info, #admin-user-info {
|
| 251 |
+
font-size: 0.85rem;
|
| 252 |
+
color: var(--text-muted);
|
| 253 |
+
background: var(--bg-card);
|
| 254 |
+
padding: 6px 12px;
|
| 255 |
+
border-radius: 20px;
|
| 256 |
+
border: 1px solid var(--border-color);
|
| 257 |
}
|
| 258 |
|
| 259 |
.logout-btn {
|
| 260 |
+
padding: 8px 16px;
|
| 261 |
+
background: rgba(239, 68, 68, 0.1);
|
| 262 |
+
color: var(--danger);
|
| 263 |
+
border: 1px solid rgba(239, 68, 68, 0.2);
|
| 264 |
+
border-radius: var(--radius-sm);
|
| 265 |
cursor: pointer;
|
| 266 |
+
font-size: 0.85rem;
|
| 267 |
+
font-weight: 600;
|
| 268 |
+
transition: var(--transition);
|
| 269 |
+
}
|
| 270 |
+
|
| 271 |
+
.logout-btn:hover {
|
| 272 |
+
background: rgba(239, 68, 68, 0.2);
|
| 273 |
+
border-color: var(--danger);
|
| 274 |
}
|
| 275 |
|
| 276 |
+
/* Theme Toggle */
|
| 277 |
+
.theme-toggle {
|
| 278 |
+
background: transparent;
|
| 279 |
+
border: 1px solid var(--border-color);
|
| 280 |
+
color: var(--text-muted);
|
| 281 |
+
padding: 6px 16px;
|
| 282 |
+
border-radius: var(--radius-md);
|
| 283 |
+
cursor: pointer;
|
| 284 |
+
font-size: 0.85rem;
|
| 285 |
+
font-weight: 600;
|
| 286 |
+
transition: var(--transition);
|
| 287 |
+
display: flex;
|
| 288 |
+
align-items: center;
|
| 289 |
+
justify-content: center;
|
| 290 |
+
height: auto;
|
| 291 |
+
width: auto;
|
| 292 |
+
white-space: nowrap;
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
.theme-toggle:hover {
|
| 296 |
+
background: var(--bg-card-hover);
|
| 297 |
+
color: var(--text-main);
|
| 298 |
+
border-color: var(--border-hover);
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
[data-theme="light"] .theme-toggle {
|
| 302 |
+
background: var(--bg-input);
|
| 303 |
+
color: var(--text-main);
|
| 304 |
+
border-color: var(--border-color);
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
[data-theme="light"] .theme-toggle:hover {
|
| 308 |
+
background: var(--bg-card-hover);
|
| 309 |
+
border-color: var(--primary);
|
| 310 |
+
color: var(--primary);
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
/* Main Container */
|
| 314 |
.container {
|
| 315 |
+
max-width: 1200px;
|
| 316 |
margin: 0 auto;
|
| 317 |
+
padding: var(--space-xl);
|
| 318 |
+
width: 100%;
|
| 319 |
}
|
| 320 |
|
| 321 |
+
/* Stats Grid */
|
| 322 |
.stats-grid {
|
| 323 |
display: grid;
|
| 324 |
+
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
| 325 |
+
gap: var(--space-md);
|
| 326 |
+
margin-bottom: var(--space-xl);
|
| 327 |
}
|
| 328 |
|
| 329 |
.stat-card {
|
| 330 |
+
background: var(--bg-card);
|
| 331 |
+
padding: var(--space-lg);
|
| 332 |
+
border-radius: var(--radius-lg);
|
| 333 |
+
border: 1px solid var(--border-color);
|
| 334 |
+
backdrop-filter: var(--glass-blur);
|
| 335 |
text-align: center;
|
| 336 |
+
transition: var(--transition);
|
| 337 |
+
}
|
| 338 |
+
|
| 339 |
+
.stat-card:hover {
|
| 340 |
+
border-color: var(--border-hover);
|
| 341 |
+
transform: translateY(-2px);
|
| 342 |
+
background: var(--bg-card-hover);
|
| 343 |
}
|
| 344 |
|
| 345 |
.stat-card h3 {
|
| 346 |
+
color: var(--text-muted);
|
| 347 |
+
font-size: 0.85rem;
|
|
|
|
| 348 |
text-transform: uppercase;
|
| 349 |
+
letter-spacing: 0.05em;
|
| 350 |
+
margin-bottom: var(--space-sm);
|
| 351 |
}
|
| 352 |
|
| 353 |
.stat-value {
|
| 354 |
+
font-size: 2.5rem;
|
| 355 |
+
font-weight: 700;
|
| 356 |
+
color: var(--text-main);
|
| 357 |
+
background: linear-gradient(135deg, #fff 0%, #cbd5e1 100%);
|
| 358 |
+
-webkit-background-clip: text;
|
| 359 |
+
-webkit-text-fill-color: transparent;
|
| 360 |
}
|
| 361 |
|
| 362 |
+
/* Section */
|
| 363 |
.section {
|
| 364 |
+
background: var(--bg-card);
|
| 365 |
+
padding: var(--space-xl);
|
| 366 |
+
border-radius: var(--radius-lg);
|
| 367 |
+
border: 1px solid var(--border-color);
|
| 368 |
+
margin-bottom: var(--space-xl);
|
| 369 |
+
backdrop-filter: var(--glass-blur);
|
| 370 |
}
|
| 371 |
|
| 372 |
.section-header {
|
| 373 |
display: flex;
|
| 374 |
justify-content: space-between;
|
| 375 |
align-items: center;
|
| 376 |
+
margin-bottom: var(--space-lg);
|
| 377 |
+
border-bottom: 1px solid var(--border-color);
|
| 378 |
+
padding-bottom: var(--space-md);
|
| 379 |
}
|
| 380 |
|
| 381 |
.section h2 {
|
| 382 |
+
font-size: 1.25rem;
|
| 383 |
+
font-weight: 600;
|
| 384 |
+
color: var(--text-main);
|
| 385 |
}
|
| 386 |
|
| 387 |
+
/* Buttons */
|
| 388 |
+
.btn-primary, .btn-secondary, .btn-danger, .btn-success {
|
| 389 |
+
padding: 10px 20px;
|
| 390 |
+
border-radius: var(--radius-md);
|
| 391 |
+
font-weight: 500;
|
|
|
|
| 392 |
cursor: pointer;
|
| 393 |
+
transition: var(--transition);
|
| 394 |
+
font-size: 0.9rem;
|
| 395 |
+
display: inline-flex;
|
| 396 |
+
align-items: center;
|
| 397 |
+
justify-content: center;
|
| 398 |
+
gap: 8px;
|
| 399 |
}
|
| 400 |
|
| 401 |
+
.btn-primary {
|
| 402 |
+
background: var(--primary);
|
| 403 |
+
color: white;
|
| 404 |
+
border: 1px solid transparent;
|
| 405 |
+
box-shadow: 0 2px 8px rgba(99, 102, 241, 0.3);
|
| 406 |
+
}
|
| 407 |
.btn-primary:hover {
|
| 408 |
+
background: var(--primary-hover);
|
| 409 |
+
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.5);
|
| 410 |
}
|
| 411 |
|
| 412 |
.btn-secondary {
|
| 413 |
+
background: transparent;
|
| 414 |
+
color: var(--text-main);
|
| 415 |
+
border: 1px solid var(--border-color);
|
| 416 |
+
}
|
| 417 |
+
.btn-secondary:hover {
|
| 418 |
+
background: rgba(255, 255, 255, 0.05);
|
| 419 |
+
border-color: var(--border-hover);
|
| 420 |
}
|
| 421 |
|
| 422 |
.btn-danger {
|
| 423 |
+
background: rgba(239, 68, 68, 0.1);
|
| 424 |
+
color: var(--danger);
|
| 425 |
+
border: 1px solid rgba(239, 68, 68, 0.2);
|
| 426 |
+
}
|
| 427 |
+
.btn-danger:hover {
|
| 428 |
+
background: rgba(239, 68, 68, 0.2);
|
| 429 |
+
border-color: var(--danger);
|
| 430 |
}
|
| 431 |
|
| 432 |
.btn-success {
|
| 433 |
+
background: rgba(16, 185, 129, 0.1);
|
| 434 |
+
color: var(--success);
|
| 435 |
+
border: 1px solid rgba(16, 185, 129, 0.2);
|
| 436 |
+
}
|
| 437 |
+
.btn-success:hover {
|
| 438 |
+
background: rgba(16, 185, 129, 0.2);
|
| 439 |
+
border-color: var(--success);
|
| 440 |
}
|
| 441 |
|
| 442 |
+
/* Keys List */
|
| 443 |
.keys-list {
|
| 444 |
+
display: flex;
|
| 445 |
+
flex-direction: column;
|
| 446 |
+
gap: var(--space-md);
|
| 447 |
+
}
|
| 448 |
+
|
| 449 |
+
.group-title {
|
| 450 |
+
color: var(--text-muted);
|
| 451 |
+
font-size: 0.9rem;
|
| 452 |
+
text-transform: uppercase;
|
| 453 |
+
letter-spacing: 0.05em;
|
| 454 |
+
margin: var(--space-lg) 0 var(--space-sm) 0;
|
| 455 |
+
padding-bottom: var(--space-xs);
|
| 456 |
+
border-bottom: 1px solid var(--border-color);
|
| 457 |
}
|
| 458 |
|
| 459 |
+
/* AI Provider Select */
|
| 460 |
+
#ai-provider-select {
|
| 461 |
+
margin-bottom: var(--space-md);
|
| 462 |
+
background: var(--bg-input);
|
| 463 |
+
color: var(--text-main);
|
| 464 |
+
border: 1px solid var(--border-color);
|
| 465 |
+
}
|
| 466 |
+
/* Key Cards */
|
| 467 |
.key-card {
|
| 468 |
+
background: var(--bg-card);
|
| 469 |
+
border: 1px solid var(--border-color);
|
| 470 |
+
border-radius: var(--radius-md);
|
| 471 |
+
padding: var(--space-lg);
|
| 472 |
display: flex;
|
|
|
|
| 473 |
align-items: center;
|
| 474 |
+
transition: var(--transition);
|
| 475 |
}
|
| 476 |
|
| 477 |
.key-card.admin {
|
| 478 |
+
border-color: rgba(99, 102, 241, 0.3);
|
| 479 |
+
background: rgba(99, 102, 241, 0.05);
|
| 480 |
}
|
| 481 |
|
| 482 |
+
[data-theme="light"] .key-card.admin {
|
| 483 |
+
background: rgba(99, 102, 241, 0.05);
|
| 484 |
+
border-color: rgba(99, 102, 241, 0.2);
|
| 485 |
}
|
| 486 |
|
| 487 |
+
.regular-key-group {
|
| 488 |
+
border: 1px solid var(--border-color);
|
| 489 |
+
border-radius: var(--radius-lg);
|
| 490 |
+
overflow: hidden;
|
| 491 |
+
background: var(--bg-card);
|
|
|
|
|
|
|
| 492 |
}
|
| 493 |
|
| 494 |
+
.regular-key-header {
|
| 495 |
+
cursor: pointer;
|
| 496 |
+
transition: var(--transition);
|
| 497 |
}
|
| 498 |
+
.regular-key-header:hover {
|
| 499 |
+
background: var(--bg-card-hover);
|
|
|
|
|
|
|
| 500 |
}
|
| 501 |
|
| 502 |
+
.regular-key-header .key-card {
|
| 503 |
+
border: none;
|
| 504 |
+
background: transparent;
|
| 505 |
}
|
| 506 |
|
| 507 |
+
.nested-server-keys {
|
| 508 |
+
padding: 0 var(--space-lg) var(--space-lg) calc(var(--space-lg) + 30px);
|
| 509 |
+
border-top: 1px solid var(--border-color);
|
| 510 |
+
background: rgba(0, 0, 0, 0.1);
|
| 511 |
}
|
| 512 |
|
| 513 |
+
[data-theme="light"] .nested-server-keys {
|
| 514 |
+
background: rgba(0, 0, 0, 0.02);
|
|
|
|
| 515 |
}
|
| 516 |
|
| 517 |
+
.nested-server-keys h4 {
|
| 518 |
+
color: var(--text-muted);
|
| 519 |
+
font-size: 0.8rem;
|
| 520 |
+
margin: var(--space-md) 0;
|
| 521 |
+
opacity: 0.7;
|
| 522 |
}
|
| 523 |
|
| 524 |
+
.key-card.nested {
|
| 525 |
+
padding: var(--space-md);
|
| 526 |
+
background: var(--bg-card);
|
| 527 |
+
border-left: 2px solid var(--primary);
|
| 528 |
}
|
| 529 |
|
| 530 |
+
.toggle-icon-container {
|
| 531 |
+
width: 24px;
|
| 532 |
+
height: 24px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 533 |
display: flex;
|
|
|
|
| 534 |
align-items: center;
|
| 535 |
+
justify-content: center;
|
| 536 |
+
margin-right: var(--space-md);
|
| 537 |
+
background: rgba(255, 255, 255, 0.05);
|
| 538 |
+
border-radius: 50%;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 539 |
}
|
| 540 |
|
| 541 |
+
[data-theme="light"] .toggle-icon-container {
|
| 542 |
+
background: rgba(0, 0, 0, 0.05);
|
|
|
|
| 543 |
}
|
| 544 |
|
| 545 |
+
.toggle-icon {
|
| 546 |
+
font-size: 0.8rem;
|
| 547 |
+
color: var(--text-muted);
|
| 548 |
+
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
| 549 |
+
/* Fix alignment */
|
| 550 |
+
display: flex;
|
| 551 |
+
align-items: center;
|
| 552 |
+
justify-content: center;
|
| 553 |
width: 100%;
|
| 554 |
+
height: 100%;
|
| 555 |
+
margin-top: 2px; /* Visual correction */
|
|
|
|
|
|
|
|
|
|
| 556 |
}
|
| 557 |
|
| 558 |
+
.key-info {
|
| 559 |
+
flex: 1;
|
|
|
|
|
|
|
|
|
|
| 560 |
}
|
| 561 |
|
| 562 |
+
.key-info h3 {
|
| 563 |
display: flex;
|
| 564 |
align-items: center;
|
| 565 |
+
font-size: 1rem;
|
| 566 |
+
margin-bottom: var(--space-xs);
|
| 567 |
+
color: var(--text-main);
|
| 568 |
}
|
| 569 |
|
| 570 |
+
.key-info p {
|
| 571 |
+
color: var(--text-muted);
|
| 572 |
+
font-size: 0.85rem;
|
| 573 |
+
margin-bottom: 2px;
|
| 574 |
+
font-family: 'JetBrains Mono', monospace;
|
|
|
|
|
|
|
| 575 |
}
|
| 576 |
|
| 577 |
+
.key-badge {
|
| 578 |
+
padding: 2px 8px;
|
| 579 |
+
border-radius: 4px;
|
| 580 |
+
font-size: 0.7rem;
|
| 581 |
+
font-weight: 600;
|
| 582 |
+
margin-right: var(--space-sm);
|
| 583 |
+
text-transform: uppercase;
|
| 584 |
}
|
| 585 |
|
| 586 |
+
.key-badge.admin { background: rgba(139, 92, 246, 0.2); color: #c4b5fd; border: 1px solid rgba(139, 92, 246, 0.3); }
|
| 587 |
+
.key-badge.regular { background: rgba(59, 130, 246, 0.2); color: #93c5fd; border: 1px solid rgba(59, 130, 246, 0.3); }
|
| 588 |
+
.key-badge.server { background: rgba(16, 185, 129, 0.2); color: #6ee7b7; border: 1px solid rgba(16, 185, 129, 0.3); }
|
|
|
|
|
|
|
|
|
|
| 589 |
|
| 590 |
+
.key-badge.active { background: rgba(16, 185, 129, 0.2); color: #34d399; }
|
| 591 |
+
.key-badge.inactive { background: rgba(239, 68, 68, 0.2); color: #f87171; }
|
|
|
|
|
|
|
| 592 |
|
| 593 |
+
.key-actions {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 594 |
display: flex;
|
| 595 |
+
gap: var(--space-sm);
|
| 596 |
+
margin-left: var(--space-lg);
|
| 597 |
}
|
| 598 |
|
| 599 |
+
/* Forms & Inputs */
|
| 600 |
+
.form-group {
|
| 601 |
+
margin-bottom: var(--space-lg);
|
|
|
|
|
|
|
|
|
|
| 602 |
}
|
| 603 |
|
| 604 |
+
label {
|
| 605 |
+
display: block;
|
| 606 |
+
margin-bottom: var(--space-xs);
|
| 607 |
+
font-weight: 500;
|
| 608 |
+
color: var(--text-main);
|
| 609 |
+
font-size: 0.9rem;
|
|
|
|
|
|
|
|
|
|
| 610 |
}
|
| 611 |
|
| 612 |
+
input[type="text"],
|
| 613 |
+
input[type="password"],
|
| 614 |
+
textarea,
|
| 615 |
+
select {
|
| 616 |
+
width: 100%;
|
| 617 |
+
padding: 12px;
|
| 618 |
+
background: var(--bg-input);
|
| 619 |
+
border: 1px solid var(--border-color);
|
| 620 |
+
border-radius: var(--radius-md);
|
| 621 |
+
color: var(--text-main);
|
| 622 |
+
font-family: inherit;
|
| 623 |
+
font-size: 0.95rem;
|
| 624 |
+
transition: var(--transition);
|
| 625 |
}
|
| 626 |
|
| 627 |
+
input:focus, textarea:focus, select:focus {
|
| 628 |
+
outline: none;
|
| 629 |
+
border-color: var(--primary);
|
| 630 |
+
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.15);
|
|
|
|
|
|
|
| 631 |
}
|
| 632 |
|
| 633 |
+
.hint {
|
| 634 |
+
display: block;
|
| 635 |
+
margin-top: var(--space-xs);
|
| 636 |
+
font-size: 0.8rem;
|
| 637 |
+
color: var(--text-muted);
|
| 638 |
}
|
| 639 |
|
| 640 |
+
/* Events List */
|
| 641 |
+
.events-list {
|
| 642 |
+
max-height: 400px;
|
| 643 |
+
overflow-y: auto;
|
| 644 |
+
background: rgba(0, 0, 0, 0.2);
|
| 645 |
+
border-radius: var(--radius-md);
|
| 646 |
+
padding: var(--space-sm);
|
| 647 |
}
|
| 648 |
|
| 649 |
+
.event-item {
|
| 650 |
+
padding: var(--space-md);
|
| 651 |
+
border-left: 3px solid var(--primary);
|
| 652 |
+
background: rgba(255, 255, 255, 0.02);
|
| 653 |
+
margin-bottom: var(--space-sm);
|
| 654 |
+
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
|
| 655 |
+
font-size: 0.9rem;
|
| 656 |
}
|
| 657 |
|
| 658 |
+
.event-item strong {
|
| 659 |
+
color: var(--primary);
|
|
|
|
|
|
|
|
|
|
| 660 |
}
|
| 661 |
|
| 662 |
+
.event-item pre {
|
| 663 |
+
background: rgba(0, 0, 0, 0.3);
|
| 664 |
+
padding: var(--space-sm);
|
| 665 |
+
border-radius: var(--radius-sm);
|
| 666 |
+
margin-top: var(--space-xs);
|
| 667 |
+
color: var(--text-muted);
|
| 668 |
+
font-size: 0.8rem;
|
| 669 |
+
overflow-x: auto;
|
|
|
|
| 670 |
}
|
| 671 |
|
| 672 |
+
/* Command Console */
|
| 673 |
+
.command-history {
|
| 674 |
+
height: 300px;
|
| 675 |
+
overflow-y: auto;
|
| 676 |
+
background: #000;
|
| 677 |
+
border: 1px solid var(--border-color);
|
| 678 |
+
border-radius: var(--radius-md);
|
| 679 |
+
padding: var(--space-md);
|
| 680 |
+
font-family: 'JetBrains Mono', monospace;
|
| 681 |
+
margin-top: var(--space-md);
|
| 682 |
}
|
| 683 |
|
| 684 |
+
.command-item {
|
| 685 |
+
margin-bottom: 6px;
|
| 686 |
+
padding-bottom: 6px;
|
| 687 |
+
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
| 688 |
+
color: #e2e8f0;
|
| 689 |
+
font-size: 0.9rem;
|
| 690 |
}
|
| 691 |
|
| 692 |
+
.command-item .timestamp {
|
| 693 |
+
color: #64748b;
|
| 694 |
+
font-size: 0.75rem;
|
| 695 |
+
margin-top: 2px;
|
|
|
|
| 696 |
}
|
| 697 |
|
| 698 |
+
.command-form {
|
| 699 |
display: flex;
|
| 700 |
+
gap: var(--space-sm);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 701 |
}
|
| 702 |
|
| 703 |
+
/* Modal */
|
| 704 |
+
.modal {
|
| 705 |
+
position: fixed;
|
| 706 |
+
top: 0;
|
| 707 |
+
left: 0;
|
| 708 |
+
width: 100%;
|
| 709 |
+
height: 100%;
|
| 710 |
+
background: rgba(0, 0, 0, 0.7);
|
| 711 |
+
backdrop-filter: blur(8px);
|
| 712 |
+
display: flex;
|
| 713 |
+
justify-content: center;
|
| 714 |
+
align-items: center;
|
| 715 |
+
z-index: 1000;
|
| 716 |
+
animation: fadeIn 0.2s ease-out;
|
| 717 |
}
|
| 718 |
|
| 719 |
+
.modal-content {
|
| 720 |
+
background: var(--bg-card);
|
| 721 |
+
padding: var(--space-xl);
|
| 722 |
+
border-radius: var(--radius-xl);
|
| 723 |
+
border: 1px solid var(--border-color);
|
| 724 |
+
width: 90%;
|
| 725 |
+
max-width: 500px;
|
| 726 |
+
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.5);
|
| 727 |
+
color: var(--text-main);
|
| 728 |
}
|
| 729 |
|
| 730 |
+
.modal-actions {
|
| 731 |
+
display: flex;
|
| 732 |
+
justify-content: flex-end;
|
| 733 |
+
gap: var(--space-sm);
|
| 734 |
+
margin-top: var(--space-xl);
|
| 735 |
}
|
| 736 |
|
| 737 |
+
/* AI Status */
|
| 738 |
+
.ai-status {
|
| 739 |
+
margin-top: var(--space-md);
|
| 740 |
+
padding: var(--space-md);
|
| 741 |
+
border-radius: var(--radius-md);
|
| 742 |
+
font-size: 0.9rem;
|
| 743 |
+
}
|
| 744 |
+
.ai-status.success { background: rgba(16, 185, 129, 0.1); color: var(--success); border: 1px solid rgba(16, 185, 129, 0.2); }
|
| 745 |
+
.ai-status.error { background: rgba(239, 68, 68, 0.1); color: var(--danger); border: 1px solid rgba(239, 68, 68, 0.2); }
|
| 746 |
+
.ai-status.info { background: rgba(99, 102, 241, 0.1); color: var(--primary); border: 1px solid rgba(99, 102, 241, 0.2); }
|
hf_repo/hf_repo/hf_repo/docs/UI_UX_DESIGN_2026.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# InterConnect Server UI/UX 设计方案 2026
|
| 2 |
+
|
| 3 |
+
## 1. 设计理念:"星云玻璃 (Nebula Glass)"
|
| 4 |
+
全新的设计语言摒弃了扁平的白色界面,转向以深度为导向的深色主题界面,灵感来源于现代开发工具(如 Vercel, Linear, Raycast)和游戏美学(Minecraft 光影)。
|
| 5 |
+
|
| 6 |
+
### 核心概念
|
| 7 |
+
- **沉浸式深度 (Immersive Depth)**:利用多层级的暗色、阴影和微妙的边框来构建层级关系,而不依赖厚重的投影。
|
| 8 |
+
- **玻璃拟态 (Glassmorphism)**:使用半透明表面和背景模糊 (`backdrop-filter`) 来提供上下文关联和未来感。
|
| 9 |
+
- **动效 (Motion)**:微交互(悬停状态、焦点转换)感觉即时且流畅。
|
| 10 |
+
- **内容优先 (Content-First)**:高对比度的排版和充足的留白(或"留黑")。
|
| 11 |
+
|
| 12 |
+
## 2. 配色方案
|
| 13 |
+
|
| 14 |
+
### 深色模式 (Dark Mode - 默认)
|
| 15 |
+
| 变量 | 颜色 | 用途 |
|
| 16 |
+
| :--- | :--- | :--- |
|
| 17 |
+
| `--bg-body` | `#09090b` (Zinc 950) | 主背景 |
|
| 18 |
+
| `--bg-card` | `rgba(39, 39, 42, 0.6)` | 卡片背景 (玻璃效果) |
|
| 19 |
+
| `--border-color` | `rgba(255, 255, 255, 0.08)` | 边框 |
|
| 20 |
+
| `--primary` | `#6366f1` (Indigo 500) | 主要操作、激活状态 |
|
| 21 |
+
| `--primary-glow` | `rgba(99, 102, 241, 0.4)` | 发光效果 |
|
| 22 |
+
| `--text-main` | `#fafafa` (Zinc 50) | 标题、主要文本 |
|
| 23 |
+
| `--text-muted` | `#a1a1aa` (Zinc 400) | 次要文本、标签 |
|
| 24 |
+
| `--danger` | `#ef4444` (Red 500) | 破坏性操作 |
|
| 25 |
+
| `--success` | `#10b981` (Emerald 500) | 成功状态 |
|
| 26 |
+
|
| 27 |
+
### 浅色模式 (Light Mode - 专业白)
|
| 28 |
+
| 变量 | 颜色 | 用途 |
|
| 29 |
+
| :--- | :--- | :--- |
|
| 30 |
+
| `--bg-body` | `#f3f4f6` (Gray 100) | 主背景 |
|
| 31 |
+
| `--bg-card` | `#ffffff` (White) | 卡片背景 (纯白) |
|
| 32 |
+
| `--border-color` | `#e5e7eb` (Gray 200) | 边框 |
|
| 33 |
+
| `--primary` | `#4f46e5` (Indigo 600) | 主要操作 (加深以提升对比度) |
|
| 34 |
+
| `--text-main` | `#111827` (Gray 900) | 主要文本 |
|
| 35 |
+
| `--text-muted` | `#6b7280` (Gray 500) | 次要文本 |
|
| 36 |
+
|
| 37 |
+
## 3. 组件设计
|
| 38 |
+
|
| 39 |
+
### 3.1 导航栏 (Navigation)
|
| 40 |
+
- **样式**: 顶部固定,半透明磨砂玻璃效果。
|
| 41 |
+
- **功能**: 包含标题、用户信息、主题切换(深/浅色)和退出按钮。
|
| 42 |
+
|
| 43 |
+
### 3.2 卡片 (Cards - Bento Grid 风格)
|
| 44 |
+
- **结构**: 圆角矩形 (`border-radius: 12px/16px`)。
|
| 45 |
+
- **边框**: 1px 实线,微弱的透明度。
|
| 46 |
+
- **交互**: 悬停时轻微上浮并增强边框颜色。
|
| 47 |
+
|
| 48 |
+
### 3.3 按钮与输入框
|
| 49 |
+
- **按钮**:
|
| 50 |
+
- 主要按钮:渐变色填充 + 悬停发光。
|
| 51 |
+
- 次要按钮:透明背景 + 边框,悬停高亮。
|
| 52 |
+
- **输入框**:
|
| 53 |
+
- 深色背景 (`rgba(24, 24, 27, 0.8)`)。
|
| 54 |
+
- 聚焦光环:主色调光晕。
|
| 55 |
+
|
| 56 |
+
## 4. 排版 (Typography)
|
| 57 |
+
- **字体**:
|
| 58 |
+
- 界面: `Inter` (现代无衬线体)
|
| 59 |
+
- 代码/密钥: `JetBrains Mono` (等宽字体)
|
| 60 |
+
- **层级**:
|
| 61 |
+
- H1: 24px, 粗体, 紧凑字间距。
|
| 62 |
+
- H2: 18px, 半粗体, 大写字母间距。
|
| 63 |
+
- 正文: 14px/16px, 常规字重。
|
| 64 |
+
|
| 65 |
+
## 5. 实施计划 (已完成)
|
| 66 |
+
1. **CSS 重构**: 使用 CSS 变量 (`:root` 和 `[data-theme="light"]`) 实现了完整的主题切换系统。
|
| 67 |
+
2. **HTML 结构**: 优化了语义化标签,添加了主题切换按钮。
|
| 68 |
+
3. **交互优化**: 实现了二级折叠菜单、动态箭头动画和响应式布局。
|
| 69 |
+
4. **功能增强**: 集成了系统日志控制台、多 AI 提供商配置和 API 密钥安全管理。
|
| 70 |
+
|
| 71 |
+
---
|
| 72 |
+
*由 Trae AI Pair Programmer 生成与执行*
|
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/dashboard/public/index.html
CHANGED
|
@@ -1,71 +1,71 @@
|
|
| 1 |
<!DOCTYPE html>
|
| 2 |
-
<html lang="
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
-
<title>Minecraft WebSocket API -
|
| 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
|
| 14 |
<form id="login-form">
|
| 15 |
-
<input type="password" id="api-key-input" placeholder="
|
| 16 |
-
<button type="submit"
|
| 17 |
</form>
|
| 18 |
<p class="error-message" id="login-error"></p>
|
| 19 |
<div class="login-info">
|
| 20 |
-
<p><strong
|
| 21 |
-
<p>Admin Key -
|
| 22 |
-
<p>Regular Key -
|
| 23 |
</div>
|
| 24 |
</div>
|
| 25 |
</div>
|
| 26 |
|
| 27 |
<div id="admin-screen" class="screen hidden">
|
| 28 |
<nav class="navbar">
|
| 29 |
-
<h1
|
| 30 |
<div class="nav-info">
|
| 31 |
<span id="admin-user-info"></span>
|
| 32 |
-
<button class="logout-btn"
|
| 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
|
| 40 |
</div>
|
| 41 |
<div class="ai-config-form">
|
| 42 |
<form id="ai-config-form">
|
| 43 |
<div class="form-group">
|
| 44 |
-
<label>API
|
| 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
|
| 49 |
<input type="text" id="ai-model-id" placeholder="gpt-3.5-turbo" required>
|
| 50 |
</div>
|
| 51 |
<div class="form-group">
|
| 52 |
-
<label>API
|
| 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
|
| 58 |
-
<textarea id="ai-system-prompt" placeholder="
|
| 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"
|
| 67 |
-
<button type="button" id="ai-test-btn" class="btn-secondary"
|
| 68 |
-
<button type="button" id="ai-delete-btn" class="btn-danger"
|
| 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
|
| 78 |
-
<button id="create-key-btn" class="btn-primary"
|
| 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
|
| 88 |
<div class="nav-info">
|
| 89 |
<span id="user-info"></span>
|
| 90 |
-
<button class="logout-btn"
|
| 91 |
</div>
|
| 92 |
</nav>
|
| 93 |
|
| 94 |
<div class="container">
|
| 95 |
<div class="stats-grid">
|
| 96 |
<div class="stat-card">
|
| 97 |
-
<h3
|
| 98 |
<p class="stat-value" id="user-stat-connections">0</p>
|
| 99 |
</div>
|
| 100 |
<div class="stat-card">
|
| 101 |
-
<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
|
| 120 |
<div id="user-keys-list" class="keys-list"></div>
|
| 121 |
</div>
|
| 122 |
|
| 123 |
<div class="section">
|
| 124 |
-
<h2
|
| 125 |
<form id="command-form" class="command-form">
|
| 126 |
-
<input type="text" id="command-input" placeholder="
|
| 127 |
-
<button type="submit" class="btn-primary"
|
| 128 |
</form>
|
| 129 |
<div id="command-history" class="command-history"></div>
|
| 130 |
</div>
|
| 131 |
|
| 132 |
<div class="section">
|
| 133 |
-
<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
|
| 142 |
<form id="create-key-form">
|
| 143 |
-
<label
|
| 144 |
<input type="text" id="key-name" required>
|
| 145 |
|
| 146 |
-
<label
|
| 147 |
<textarea id="key-description"></textarea>
|
| 148 |
|
| 149 |
-
<label
|
| 150 |
<select id="key-type">
|
| 151 |
-
<option value="regular"
|
| 152 |
-
<option value="admin"
|
| 153 |
</select>
|
| 154 |
|
| 155 |
-
<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"
|
| 160 |
-
<button type="submit" class="btn-primary"
|
| 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
|
| 169 |
<div id="key-details-content"></div>
|
| 170 |
<div class="modal-actions">
|
| 171 |
-
<button id="close-details-btn" class="btn-secondary"
|
| 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/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/cli/cli.js
CHANGED
|
@@ -389,15 +389,15 @@ program
|
|
| 389 |
|
| 390 |
program
|
| 391 |
.command('reset-admin')
|
| 392 |
-
.description('
|
| 393 |
-
.requiredOption('-r, --recovery-token <token>', '
|
| 394 |
-
.option('--db-path <path>', '
|
| 395 |
-
.option('--keep-existing', '
|
| 396 |
-
.option('--name <name>', '
|
| 397 |
.action(async (options) => {
|
| 398 |
try {
|
| 399 |
if (!fs.existsSync(options.dbPath)) {
|
| 400 |
-
throw new Error(
|
| 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
|
| 412 |
console.log(` ID : ${result.id}`);
|
| 413 |
-
console.log(`
|
| 414 |
console.log(` Key : ${result.key}`);
|
| 415 |
} catch (error) {
|
| 416 |
-
console.error(`\x1b[
|
| 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/hf_repo/hf_repo/hf_repo/hf_repo/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 -
|
| 17 |
console.log('='.repeat(50));
|
| 18 |
-
console.log(
|
| 19 |
-
console.log('
|
| 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/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/docs/protocol.md
CHANGED
|
@@ -1,32 +1,31 @@
|
|
| 1 |
-
#
|
| 2 |
|
| 3 |
-
|
| 4 |
-
InterConnect-Server. All payloads are JSON.
|
| 5 |
|
| 6 |
-
## Authentication
|
| 7 |
|
| 8 |
-
- REST:
|
| 9 |
-
- WebSocket:
|
| 10 |
|
| 11 |
-
## Event Object
|
| 12 |
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
|
| 17 |
-
|
| 18 |
-
- `event_type` (
|
| 19 |
-
- `server_name` (
|
| 20 |
-
- `timestamp` (
|
| 21 |
-
- `data` (
|
| 22 |
|
| 23 |
-
|
| 24 |
- `player_join`
|
| 25 |
- `player_leave`
|
| 26 |
- `message`
|
| 27 |
- `server_command`
|
| 28 |
|
| 29 |
-
|
| 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 |
-
|
| 46 |
|
| 47 |
-
|
| 48 |
-
-
|
| 49 |
- `Content-Type: application/json`
|
| 50 |
-
- Body:
|
| 51 |
|
| 52 |
-
|
| 53 |
```json
|
| 54 |
{
|
| 55 |
"message": "Event received and broadcasted"
|
| 56 |
}
|
| 57 |
```
|
| 58 |
|
| 59 |
-
|
| 60 |
-
- `400`
|
| 61 |
-
- `401`
|
| 62 |
|
| 63 |
### POST /api/server/command
|
| 64 |
|
| 65 |
-
|
| 66 |
|
| 67 |
-
|
| 68 |
-
-
|
| 69 |
- `Content-Type: application/json`
|
| 70 |
- Body:
|
| 71 |
```json
|
|
@@ -75,11 +74,11 @@ Request:
|
|
| 75 |
}
|
| 76 |
```
|
| 77 |
|
| 78 |
-
|
| 79 |
-
-
|
| 80 |
-
-
|
| 81 |
|
| 82 |
-
|
| 83 |
```json
|
| 84 |
{
|
| 85 |
"message": "Command sent successfully",
|
|
@@ -88,23 +87,23 @@ Response:
|
|
| 88 |
}
|
| 89 |
```
|
| 90 |
|
| 91 |
-
## WebSocket Messages
|
| 92 |
|
| 93 |
-
###
|
| 94 |
|
| 95 |
Ping:
|
| 96 |
```json
|
| 97 |
{ "type": "ping" }
|
| 98 |
```
|
| 99 |
|
| 100 |
-
Pong
|
| 101 |
```json
|
| 102 |
{ "type": "pong" }
|
| 103 |
```
|
| 104 |
|
| 105 |
-
###
|
| 106 |
|
| 107 |
-
|
| 108 |
```json
|
| 109 |
{
|
| 110 |
"type": "minecraft_event",
|
|
@@ -121,5 +120,4 @@ Broadcasted event:
|
|
| 121 |
}
|
| 122 |
```
|
| 123 |
|
| 124 |
-
`source_key_id_prefix`
|
| 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/hf_repo/hf_repo/hf_repo/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/hf_repo/hf_repo/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/hf_repo/hf_repo/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/hf_repo/hf_repo/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/hf_repo/hf_repo/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/hf_repo/hf_repo/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/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/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/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
|
|
|
|
| 3 |
let ws = null;
|
|
|
|
|
|
|
| 4 |
|
| 5 |
const loginScreen = document.getElementById('login-screen');
|
| 6 |
-
const
|
|
|
|
| 7 |
const loginForm = document.getElementById('login-form');
|
| 8 |
const loginError = document.getElementById('login-error');
|
| 9 |
-
const
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 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 |
-
|
| 86 |
-
|
| 87 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
});
|
| 93 |
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
}
|
| 97 |
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 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 |
-
|
| 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 |
-
|
| 152 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
currentUserServerId = userServerId;
|
| 157 |
-
|
| 158 |
loginScreen.classList.add('hidden');
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 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 |
-
|
| 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
|
|
|
|
|
|
|
|
|
|
| 212 |
try {
|
| 213 |
const response = await fetch(`${API_URL}/manage/keys`, {
|
| 214 |
-
headers:
|
| 215 |
});
|
| 216 |
-
|
| 217 |
if (response.ok) {
|
| 218 |
const keys = await response.json();
|
| 219 |
-
|
|
|
|
|
|
|
| 220 |
}
|
| 221 |
} catch (error) {
|
| 222 |
-
|
| 223 |
}
|
| 224 |
}
|
| 225 |
|
| 226 |
-
async function
|
|
|
|
|
|
|
|
|
|
| 227 |
try {
|
| 228 |
const response = await fetch(`${API_URL}/manage/keys/server-keys`, {
|
| 229 |
-
headers:
|
| 230 |
});
|
| 231 |
-
|
| 232 |
if (response.ok) {
|
| 233 |
-
const
|
| 234 |
-
|
| 235 |
} else {
|
| 236 |
-
|
| 237 |
}
|
| 238 |
} catch (error) {
|
| 239 |
-
|
| 240 |
-
document.getElementById('keys-list').innerHTML = '<p>无法加载Server Key列表</p>';
|
| 241 |
}
|
| 242 |
}
|
| 243 |
|
| 244 |
-
function
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
if (keys.length === 0) {
|
| 248 |
-
keysList.innerHTML = '<p>暂无关联的Server Key</p>';
|
| 249 |
return;
|
| 250 |
}
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 254 |
<div class="key-info">
|
| 255 |
<h3>
|
| 256 |
-
<span class="key-badge
|
|
|
|
|
|
|
| 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
|
| 264 |
-
${key.serverId ? `<p
|
| 265 |
-
<p
|
| 266 |
-
<p
|
| 267 |
</div>
|
| 268 |
<div class="key-actions">
|
| 269 |
-
${key.isActive
|
| 270 |
-
`<button class="btn-danger" onclick="deactivateKey('${key.id}')"
|
| 271 |
-
`<button class="btn-success" onclick="activateKey('${key.id}')"
|
| 272 |
}
|
| 273 |
-
<button class="btn-danger" onclick="deleteKey('${key.id}', '${key.name}')"
|
| 274 |
</div>
|
| 275 |
</div>
|
| 276 |
`).join('');
|
| 277 |
}
|
| 278 |
|
| 279 |
-
function
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
if (keys.length === 0) {
|
| 283 |
-
keysList.innerHTML = '<p>暂无API密钥</p>';
|
| 284 |
return;
|
| 285 |
}
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 289 |
<div class="key-info">
|
| 290 |
<h3>
|
| 291 |
-
<span class="key-badge
|
| 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
|
| 301 |
-
${key.serverId ? `<p
|
| 302 |
-
<p
|
| 303 |
-
<p
|
| 304 |
</div>
|
| 305 |
<div class="key-actions">
|
| 306 |
-
${key.isActive
|
| 307 |
-
`<button class="btn-danger" onclick="deactivateKey('${key.id}')"
|
| 308 |
-
`<button class="btn-success" onclick="activateKey('${key.id}')"
|
| 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:
|
| 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 332 |
}
|
| 333 |
} catch (error) {
|
| 334 |
-
alert(
|
| 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:
|
| 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 354 |
}
|
| 355 |
} catch (error) {
|
| 356 |
-
alert(
|
| 357 |
}
|
| 358 |
}
|
| 359 |
|
| 360 |
async function deleteKey(keyId, keyName) {
|
| 361 |
-
if (
|
|
|
|
|
|
|
|
|
|
| 362 |
return;
|
| 363 |
}
|
| 364 |
-
|
| 365 |
try {
|
| 366 |
const response = await fetch(`${API_URL}/manage/keys/${keyId}`, {
|
| 367 |
method: 'DELETE',
|
| 368 |
-
headers:
|
| 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(
|
|
|
|
| 380 |
}
|
|
|
|
|
|
|
| 381 |
} catch (error) {
|
| 382 |
-
alert(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 383 |
}
|
| 384 |
}
|
| 385 |
|
| 386 |
function connectWebSocket() {
|
| 387 |
-
if (!
|
| 388 |
-
|
| 389 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
|
|
|
|
|
|
| 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 |
-
|
| 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(
|
| 436 |
<pre>${JSON.stringify(event.data, null, 2)}</pre>
|
| 437 |
`;
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
while (
|
| 442 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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/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="
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
-
<title>Minecraft WebSocket API -
|
| 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
|
| 13 |
-
<h2
|
| 14 |
<form id="login-form">
|
| 15 |
-
<input type="password" id="
|
| 16 |
-
<button type="submit"
|
| 17 |
</form>
|
| 18 |
<p class="error-message" id="login-error"></p>
|
| 19 |
<div class="login-info">
|
| 20 |
-
<p><strong
|
| 21 |
-
<p
|
| 22 |
-
<p
|
| 23 |
</div>
|
| 24 |
</div>
|
| 25 |
</div>
|
| 26 |
|
| 27 |
-
<div id="
|
| 28 |
<nav class="navbar">
|
| 29 |
-
<h1
|
| 30 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
</nav>
|
| 32 |
|
| 33 |
<div class="container">
|
| 34 |
<div class="stats-grid">
|
| 35 |
<div class="stat-card">
|
| 36 |
-
<h3
|
| 37 |
-
<p class="stat-value" id="stat-connections">0</p>
|
| 38 |
</div>
|
| 39 |
<div class="stat-card">
|
| 40 |
-
<h3
|
| 41 |
-
<p class="stat-value" id="stat-total-keys">0</p>
|
| 42 |
</div>
|
| 43 |
<div class="stat-card">
|
| 44 |
-
<h3
|
| 45 |
-
<p class="stat-value" id="stat-
|
| 46 |
</div>
|
| 47 |
<div class="stat-card">
|
| 48 |
-
<h3
|
| 49 |
-
<p class="stat-value" id="stat-regular-keys">0</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
</div>
|
| 51 |
</div>
|
| 52 |
|
| 53 |
<div class="section">
|
| 54 |
-
<
|
| 55 |
-
|
| 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
|
| 63 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
</div>
|
| 65 |
</div>
|
| 66 |
</div>
|
| 67 |
|
| 68 |
<div id="create-key-modal" class="modal hidden">
|
| 69 |
<div class="modal-content">
|
| 70 |
-
<h2
|
| 71 |
<form id="create-key-form">
|
| 72 |
-
<label
|
| 73 |
<input type="text" id="key-name" required>
|
| 74 |
-
|
| 75 |
-
<label
|
| 76 |
<textarea id="key-description"></textarea>
|
| 77 |
-
|
| 78 |
-
<label>
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
<div class="modal-actions">
|
| 84 |
-
<button type="button" id="cancel-create-btn" class="btn-secondary"
|
| 85 |
-
<button type="submit" class="btn-primary"
|
| 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
|
| 94 |
<div id="key-details-content"></div>
|
| 95 |
<div class="modal-actions">
|
| 96 |
-
<button id="close-details-btn" class="btn-secondary"
|
| 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/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 |
-
|
| 99 |
padding: 10px 20px;
|
| 100 |
background: #ff4444;
|
| 101 |
color: white;
|
|
@@ -218,7 +218,7 @@ body {
|
|
| 218 |
align-items: center;
|
| 219 |
}
|
| 220 |
|
| 221 |
-
.key-card.
|
| 222 |
border-color: #764ba2;
|
| 223 |
background: #f9f7fb;
|
| 224 |
}
|
|
@@ -236,7 +236,7 @@ body {
|
|
| 236 |
margin-right: 10px;
|
| 237 |
}
|
| 238 |
|
| 239 |
-
.key-badge.
|
| 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
|
| 331 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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/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('
|
| 17 |
console.log('='.repeat(50));
|
| 18 |
-
console.log(
|
| 19 |
-
console.log('
|
| 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/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
|
| 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,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 |
-
# ---
|
| 17 |
-
|
| 18 |
def list_directory(path=".", **kwargs):
|
| 19 |
-
"""列出指定目录下的文件和文件夹"""
|
| 20 |
try:
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 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 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 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 |
-
|
| 61 |
-
|
| 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 |
-
|
| 72 |
-
|
| 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 |
-
# ---
|
| 82 |
-
|
| 83 |
messages = [
|
| 84 |
-
{"role": "system", "content": f"""
|
| 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 |
-
#
|
| 102 |
-
for
|
| 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 |
-
|
|
|
|
| 111 |
|
| 112 |
if not msg.tool_calls:
|
| 113 |
break
|
| 114 |
|
| 115 |
for tool_call in msg.tool_calls:
|
| 116 |
-
|
| 117 |
-
|
| 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 |
-
#
|
| 131 |
-
|
| 132 |
-
|
|
|
|
| 133 |
|
| 134 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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")
|
| 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/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.github/workflows/ai-review.yaml
CHANGED
|
@@ -1,116 +1,46 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
event_name
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
with
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 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/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.github/workflows/ai-review.yaml
CHANGED
|
@@ -1,46 +1,116 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
with:
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 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}")
|
hf_repo/hf_repo/hf_repo/hf_repo/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
CHANGED
|
@@ -1,106 +1,110 @@
|
|
| 1 |
import os
|
| 2 |
import json
|
| 3 |
-
import
|
| 4 |
from openai import OpenAI
|
| 5 |
-
from github import Github
|
| 6 |
|
| 7 |
-
# 初始化客户端
|
| 8 |
-
|
| 9 |
-
gh = Github(
|
| 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 |
-
# ---
|
| 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]
|
| 29 |
except Exception as e:
|
| 30 |
-
return str(e)
|
| 31 |
|
| 32 |
-
def search_keyword(keyword, path="."):
|
| 33 |
"""在当前目录及其子目录中搜索关键词"""
|
| 34 |
results = []
|
| 35 |
-
|
| 36 |
-
for
|
| 37 |
-
if
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
|
|
|
|
|
|
|
|
|
| 48 |
|
| 49 |
def get_context():
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
return number, f"PR Author
|
| 56 |
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
|
| 62 |
if event_name == "issue_comment":
|
| 63 |
actor = event_data["comment"]["user"]["login"]
|
| 64 |
cmd = event_data["comment"]["body"]
|
| 65 |
-
return number, f"
|
| 66 |
|
| 67 |
-
return number,
|
| 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 |
-
|
| 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 |
-
#
|
| 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 |
-
|
| 113 |
-
|
| 114 |
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
|
|
|
|
|
|
|
|
|
| 118 |
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
if
|
| 136 |
-
issue_obj.add_to_labels(*
|
| 137 |
-
if
|
| 138 |
-
issue_obj.edit(state=
|
| 139 |
-
|
| 140 |
-
issue_obj.create_comment(f"### 🤖 AI Agent
|
|
|
|
| 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/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.github/workflows/ai-review.yaml
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
name: AI Repository
|
| 2 |
|
| 3 |
on:
|
| 4 |
pull_request:
|
|
@@ -9,11 +9,11 @@ on:
|
|
| 9 |
types: [created]
|
| 10 |
|
| 11 |
jobs:
|
| 12 |
-
ai-
|
| 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 |
-
|
| 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 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 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 |
-
|
| 136 |
-
|
| 137 |
-
body: `### 🤖 AI Assistant Action\n\n${commentBody}`
|
| 138 |
-
});
|
| 139 |
|
| 140 |
-
|
| 141 |
-
|
| 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/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/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:
|
|
|
|
| 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/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 |
-
#
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
git fetch origin ${{ github.event.pull_request.base.ref }}
|
| 36 |
-
git diff origin/${{ github.event.pull_request.base.ref }}...HEAD >
|
|
|
|
| 37 |
else
|
| 38 |
-
|
| 39 |
-
|
| 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 |
-
|
| 49 |
-
echo "number=$
|
|
|
|
| 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 |
-
|
| 72 |
-
|
| 73 |
-
你的任务:分析内容,打上标签,并决定是否关闭或保持开启。
|
| 74 |
-
|
| 75 |
-
仓库现有标签:[${labelNames.join(", ")}]。
|
| 76 |
-
|
| 77 |
-
输出要求:
|
| 78 |
-
必须在回复的第一行输出 JSON 格式的操作指令:
|
| 79 |
-
{"labels": ["label1"], "state": "closed" | "open"}
|
| 80 |
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 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 |
-
//
|
| 118 |
if (config.labels && config.labels.length > 0) {
|
| 119 |
await github.rest.issues.addLabels({ ...issueParams, labels: config.labels });
|
| 120 |
}
|
| 121 |
|
| 122 |
-
//
|
| 123 |
if (config.state) {
|
| 124 |
await github.rest.issues.update({ ...issueParams, state: config.state });
|
| 125 |
}
|
| 126 |
|
| 127 |
-
//
|
| 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/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.github/workflows/ai-review.yaml
CHANGED
|
@@ -1,13 +1,20 @@
|
|
| 1 |
-
name: AI
|
| 2 |
|
| 3 |
on:
|
| 4 |
pull_request:
|
| 5 |
-
types: [opened
|
| 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 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
|
|
|
| 29 |
else
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
fi
|
|
|
|
|
|
|
| 34 |
|
| 35 |
-
- name: AI
|
| 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('
|
| 48 |
-
if (!content.trim()) return;
|
| 49 |
|
| 50 |
-
// 1.
|
| 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.
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
|
|
|
| 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 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 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 |
-
|
| 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 |
-
|
| 112 |
-
});
|
| 113 |
|
| 114 |
-
//
|
| 115 |
-
if (
|
| 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/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
|
| 2 |
|
| 3 |
on:
|
| 4 |
pull_request:
|
| 5 |
types: [opened, synchronize]
|
| 6 |
issues:
|
| 7 |
-
types: [opened]
|
| 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
|
|
|
|
| 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 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
const prompt = process.env.EVENT_TYPE === 'PR'
|
| 49 |
-
? `你是一位资深工程师。请审查以下 PR 的代码改动,指出潜在 Bug
|
| 50 |
-
: `你是一位开源项目维护者。请分析以下 Issue
|
| 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.
|
| 63 |
})
|
| 64 |
});
|
| 65 |
|
| 66 |
const data = await response.json();
|
| 67 |
-
const
|
| 68 |
-
const header = process.env.EVENT_TYPE === 'PR' ? "### 🤖 AI Code Review" : "### 🤖 AI Issue Assistant";
|
| 69 |
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
owner: context.repo.owner,
|
| 80 |
repo: context.repo.repo,
|
| 81 |
-
issue_number:
|
| 82 |
-
|
| 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/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/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/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/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/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/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/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/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/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/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/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
|