GitHub Actions commited on
Commit
6d43d9c
·
1 Parent(s): 17bd284

Sync from GitHub (excluding README)

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. dashboard/public/app.js +210 -17
  2. dashboard/public/index.html +41 -0
  3. dashboard/public/style.css +235 -204
  4. docs/API.md +28 -1
  5. docs/openapi.json +930 -0
  6. docs/openapi.yaml +512 -0
  7. hf_repo/docs/TEST_REPORT.md +69 -0
  8. hf_repo/hf_repo/.gitignore +1 -0
  9. hf_repo/hf_repo/hf_repo/cli/cli.js +20 -2
  10. hf_repo/hf_repo/hf_repo/dashboard/public/app.js +160 -31
  11. hf_repo/hf_repo/hf_repo/dashboard/public/index.html +10 -13
  12. hf_repo/hf_repo/hf_repo/dashboard/public/style.css +550 -324
  13. hf_repo/hf_repo/hf_repo/docs/UI_UX_DESIGN_2026.md +72 -0
  14. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/dashboard/public/index.html +43 -43
  15. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/cli/cli.js +9 -9
  16. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/dashboard/server.js +3 -3
  17. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/docs/protocol.md +38 -40
  18. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/docs/protocol.md +125 -0
  19. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/dashboard/public/app.js +178 -0
  20. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/dashboard/public/index.html +38 -0
  21. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/dashboard/public/style.css +85 -0
  22. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/docs/AI_API.md +415 -0
  23. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/docs/API.md +730 -0
  24. 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
  25. 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
  26. 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
  27. 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
  28. 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
  29. 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
  30. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.github/scripts/ai_agent.py +48 -79
  31. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.github/workflows/ai-review.yaml +46 -116
  32. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.github/workflows/ai-review.yaml +116 -46
  33. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.github/scripts/ai_agent.py +86 -79
  34. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.github/workflows/ai-review.yaml +20 -116
  35. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.github/scripts/ai_agent.py +140 -0
  36. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.github/workflows/ai-review.yaml +1 -1
  37. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.github/workflows/ai-review.yaml +48 -42
  38. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.github/workflows/ai-review.yaml +71 -60
  39. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.github/workflows/ai-review.yaml +62 -24
  40. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.github/workflows/ai-review.yaml +87 -0
  41. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.github/workflows/push_hf.yaml +47 -0
  42. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.gitignore +59 -0
  43. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/Dockerfile +16 -0
  44. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/LICENSE +21 -0
  45. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/cli/cli.js +389 -0
  46. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/dashboard/public/app.js +444 -0
  47. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/dashboard/public/index.html +103 -0
  48. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/dashboard/public/style.css +410 -0
  49. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/dashboard/server.js +21 -0
  50. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/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 (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
 
@@ -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.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);
@@ -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 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 */
@@ -115,11 +133,11 @@ body {
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
 
@@ -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.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 {
@@ -155,23 +171,23 @@ body {
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
  }
@@ -179,17 +195,17 @@ body {
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);
@@ -197,15 +213,15 @@ body {
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
 
@@ -213,17 +229,15 @@ body {
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;
@@ -235,10 +249,23 @@ body {
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 {
@@ -249,7 +276,8 @@ body {
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;
@@ -258,61 +286,44 @@ body {
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%;
@@ -321,7 +332,7 @@ body {
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
  }
@@ -331,32 +342,29 @@ body {
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 */
@@ -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
- backdrop-filter: var(--glass-blur);
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.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;
@@ -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
- 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
 
@@ -448,45 +456,41 @@ body {
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
  }
@@ -505,26 +509,24 @@ body {
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 {
@@ -534,25 +536,15 @@ body {
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 {
@@ -562,33 +554,35 @@ body {
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;
@@ -603,10 +597,10 @@ body {
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"],
@@ -614,25 +608,25 @@ 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
  }
@@ -641,39 +635,48 @@ input:focus, textarea:focus, select:focus {
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);
@@ -682,17 +685,17 @@ input:focus, textarea:focus, select:focus {
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 {
@@ -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.7);
711
- backdrop-filter: blur(8px);
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: 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); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 healthCheck() {
115
- return await this.request('GET', '/health');
 
 
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
- adminKeysList.innerHTML = keys.map((key) => `
176
- <div class="key-card ${key.keyType === 'admin' ? 'admin' : ''}">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ? 'Active' : 'Inactive'}
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>Created: ${new Date(key.createdAt).toLocaleString()}</p>
191
- <p>Last Used: ${key.lastUsed ? new Date(key.lastUsed).toLocaleString() : 'Never'}</p>
192
  </div>
193
  <div class="key-actions">
194
  ${key.isActive
195
- ? `<button class="btn-danger" onclick="deactivateKey('${key.id}')">Deactivate</button>`
196
- : `<button class="btn-success" onclick="activateKey('${key.id}')">Activate</button>`
197
  }
198
- <button class="btn-danger" onclick="deleteKey('${key.id}', '${key.name}')">Delete</button>
199
  </div>
200
  </div>
201
- `).join('');
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 ? 'Active' : 'Inactive'}
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>Created: ${new Date(key.createdAt).toLocaleString()}</p>
228
- <p>Last Used: ${key.lastUsed ? new Date(key.lastUsed).toLocaleString() : 'Never'}</p>
229
  </div>
230
  <div class="key-actions">
231
  ${key.isActive
232
- ? `<button class="btn-danger" onclick="deactivateKey('${key.id}')">Deactivate</button>`
233
- : `<button class="btn-success" onclick="activateKey('${key.id}')">Activate</button>`
234
  }
235
  </div>
236
  </div>
@@ -293,7 +437,7 @@ async function deleteKey(keyId, keyName) {
293
  if (currentRole !== 'admin') {
294
  return;
295
  }
296
- if (!confirm(`Delete key "${keyName}"? This action cannot be undone.`)) {
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('Are you sure you want to delete the AI configuration?')) {
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>Minecraft WebSocket API - 控制台</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, 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.admin {
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.admin {
240
- background: #764ba2;
241
- color: white;
242
  }
243
-
244
- .key-badge.regular {
245
- background: #667eea;
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;
 
257
  }
258
 
259
- .key-badge.inactive {
260
- background: #ff4444;
261
- color: white;
262
  }
263
 
264
- .key-actions {
265
- display: flex;
266
- gap: 10px;
 
 
267
  }
268
 
269
- .events-list {
270
- max-height: 400px;
271
- overflow-y: auto;
 
272
  }
273
 
274
- .event-item {
275
- padding: 15px;
276
- border-left: 4px solid #667eea;
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
- z-index: 1000;
297
- }
298
-
299
- .modal-content {
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
- .modal-content textarea {
331
- min-height: 80px;
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 {
345
- display: flex;
346
- gap: 10px;
347
- justify-content: flex-end;
348
- margin-top: 20px;
349
  }
350
 
351
- .nav-info {
352
  display: flex;
353
  align-items: center;
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);
362
- padding: 8px 12px;
363
- border-radius: 20px;
364
  }
365
 
366
- .server-info {
367
- display: grid;
368
- grid-template-columns: 1fr 1fr;
369
- gap: 20px;
 
 
 
370
  }
371
 
372
- .info-card {
373
- background: #f8f9fa;
374
- padding: 20px;
375
- border-radius: 8px;
376
- border: 1px solid #e9ecef;
377
- }
378
 
379
- .info-card h3 {
380
- margin-bottom: 15px;
381
- color: #495057;
382
- }
383
 
384
- .command-history {
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: 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;
410
- font-family: monospace;
411
- font-size: 0.9em;
412
- }
413
-
414
- .command-item:last-child {
415
- border-bottom: none;
416
  }
417
 
418
- .command-item .timestamp {
419
- color: #6c757d;
420
- font-size: 0.8em;
 
 
 
 
 
 
 
 
 
 
421
  }
422
 
423
- .login-info {
424
- margin-top: 20px;
425
- text-align: left;
426
- background: rgba(255,255,255,0.1);
427
- padding: 15px;
428
- border-radius: 8px;
429
  }
430
 
431
- .login-info p {
432
- margin: 5px 0;
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
- }
 
 
 
 
 
 
 
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="en">
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Minecraft WebSocket API - Dashboard</title>
7
  <link rel="stylesheet" href="style.css">
8
  </head>
9
  <body>
10
  <div id="login-screen" class="screen">
11
  <div class="login-container">
12
  <h1>Minecraft WebSocket API</h1>
13
- <h2>Dashboard Login</h2>
14
  <form id="login-form">
15
- <input type="password" id="api-key-input" placeholder="Enter Admin or Regular Key" required>
16
- <button type="submit">Login</button>
17
  </form>
18
  <p class="error-message" id="login-error"></p>
19
  <div class="login-info">
20
- <p><strong>Key Types</strong></p>
21
- <p>Admin Key - full management permissions</p>
22
- <p>Regular Key - server monitoring and command access</p>
23
  </div>
24
  </div>
25
  </div>
26
 
27
  <div id="admin-screen" class="screen hidden">
28
  <nav class="navbar">
29
- <h1>Admin Panel</h1>
30
  <div class="nav-info">
31
  <span id="admin-user-info"></span>
32
- <button class="logout-btn">Logout</button>
33
  </div>
34
  </nav>
35
 
36
  <div class="container">
37
  <div class="section" id="admin-ai-section">
38
  <div class="section-header">
39
- <h2>AI Configuration</h2>
40
  </div>
41
  <div class="ai-config-form">
42
  <form id="ai-config-form">
43
  <div class="form-group">
44
- <label>API URL *</label>
45
  <input type="text" id="ai-api-url" placeholder="https://api.openai.com/v1/chat/completions" required>
46
  </div>
47
  <div class="form-group">
48
- <label>Model ID *</label>
49
  <input type="text" id="ai-model-id" placeholder="gpt-3.5-turbo" required>
50
  </div>
51
  <div class="form-group">
52
- <label>API Key *</label>
53
  <input type="password" id="ai-api-key" placeholder="sk-xxx">
54
  <small id="ai-api-key-hint" class="hint"></small>
55
  </div>
56
  <div class="form-group">
57
- <label>System Prompt (Optional)</label>
58
- <textarea id="ai-system-prompt" placeholder="You are a helpful assistant for Minecraft players."></textarea>
59
  </div>
60
  <div class="form-group">
61
  <label>
62
- <input type="checkbox" id="ai-enabled" checked> Enabled
63
  </label>
64
  </div>
65
  <div class="form-actions">
66
- <button type="submit" class="btn-primary">Save Configuration</button>
67
- <button type="button" id="ai-test-btn" class="btn-secondary">Test Connection</button>
68
- <button type="button" id="ai-delete-btn" class="btn-danger">Delete Configuration</button>
69
  </div>
70
  </form>
71
  <div id="ai-status" class="ai-status"></div>
@@ -74,8 +74,8 @@
74
 
75
  <div class="section" id="admin-key-section">
76
  <div class="section-header">
77
- <h2>API Key Management</h2>
78
- <button id="create-key-btn" class="btn-primary">Create Key</button>
79
  </div>
80
  <div id="admin-keys-list" class="keys-list"></div>
81
  </div>
@@ -84,21 +84,21 @@
84
 
85
  <div id="user-screen" class="screen hidden">
86
  <nav class="navbar">
87
- <h1>User Monitoring</h1>
88
  <div class="nav-info">
89
  <span id="user-info"></span>
90
- <button class="logout-btn">Logout</button>
91
  </div>
92
  </nav>
93
 
94
  <div class="container">
95
  <div class="stats-grid">
96
  <div class="stat-card">
97
- <h3>Active Connections</h3>
98
  <p class="stat-value" id="user-stat-connections">0</p>
99
  </div>
100
  <div class="stat-card">
101
- <h3>Total Keys</h3>
102
  <p class="stat-value" id="user-stat-total-keys">0</p>
103
  </div>
104
  <div class="stat-card">
@@ -116,21 +116,21 @@
116
  </div>
117
 
118
  <div class="section">
119
- <h2>Server Keys</h2>
120
  <div id="user-keys-list" class="keys-list"></div>
121
  </div>
122
 
123
  <div class="section">
124
- <h2>Command Console</h2>
125
  <form id="command-form" class="command-form">
126
- <input type="text" id="command-input" placeholder="Enter command" required>
127
- <button type="submit" class="btn-primary">Send</button>
128
  </form>
129
  <div id="command-history" class="command-history"></div>
130
  </div>
131
 
132
  <div class="section">
133
- <h2>Live Events</h2>
134
  <div id="user-events-list" class="events-list"></div>
135
  </div>
136
  </div>
@@ -138,26 +138,26 @@
138
 
139
  <div id="create-key-modal" class="modal hidden">
140
  <div class="modal-content">
141
- <h2>Create API Key</h2>
142
  <form id="create-key-form">
143
- <label>Name *</label>
144
  <input type="text" id="key-name" required>
145
 
146
- <label>Description</label>
147
  <textarea id="key-description"></textarea>
148
 
149
- <label>Key Type</label>
150
  <select id="key-type">
151
- <option value="regular">Regular</option>
152
- <option value="admin">Admin</option>
153
  </select>
154
 
155
- <label>Server ID (optional)</label>
156
  <input type="text" id="key-server-id">
157
 
158
  <div class="modal-actions">
159
- <button type="button" id="cancel-create-btn" class="btn-secondary">Cancel</button>
160
- <button type="submit" class="btn-primary">Create</button>
161
  </div>
162
  </form>
163
  </div>
@@ -165,14 +165,14 @@
165
 
166
  <div id="key-details-modal" class="modal hidden">
167
  <div class="modal-content">
168
- <h2>Key Details</h2>
169
  <div id="key-details-content"></div>
170
  <div class="modal-actions">
171
- <button id="close-details-btn" class="btn-secondary">Close</button>
172
  </div>
173
  </div>
174
  </div>
175
 
176
  <script src="app.js"></script>
177
  </body>
178
- </html>
 
1
  <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Minecraft WebSocket API - 控制台</title>
7
  <link rel="stylesheet" href="style.css">
8
  </head>
9
  <body>
10
  <div id="login-screen" class="screen">
11
  <div class="login-container">
12
  <h1>Minecraft WebSocket API</h1>
13
+ <h2>控制台登录</h2>
14
  <form id="login-form">
15
+ <input type="password" id="api-key-input" placeholder="请输入 Admin Regular Key" required>
16
+ <button type="submit">登录</button>
17
  </form>
18
  <p class="error-message" id="login-error"></p>
19
  <div class="login-info">
20
+ <p><strong>密钥类型</strong></p>
21
+ <p>Admin Key - 拥有完整管理权限</p>
22
+ <p>Regular Key - 服务器监测与命令访问</p>
23
  </div>
24
  </div>
25
  </div>
26
 
27
  <div id="admin-screen" class="screen hidden">
28
  <nav class="navbar">
29
+ <h1>管理员面板</h1>
30
  <div class="nav-info">
31
  <span id="admin-user-info"></span>
32
+ <button class="logout-btn">退出登录</button>
33
  </div>
34
  </nav>
35
 
36
  <div class="container">
37
  <div class="section" id="admin-ai-section">
38
  <div class="section-header">
39
+ <h2>AI 配置</h2>
40
  </div>
41
  <div class="ai-config-form">
42
  <form id="ai-config-form">
43
  <div class="form-group">
44
+ <label>API 地址 *</label>
45
  <input type="text" id="ai-api-url" placeholder="https://api.openai.com/v1/chat/completions" required>
46
  </div>
47
  <div class="form-group">
48
+ <label>模型 ID *</label>
49
  <input type="text" id="ai-model-id" placeholder="gpt-3.5-turbo" required>
50
  </div>
51
  <div class="form-group">
52
+ <label>API 密钥 *</label>
53
  <input type="password" id="ai-api-key" placeholder="sk-xxx">
54
  <small id="ai-api-key-hint" class="hint"></small>
55
  </div>
56
  <div class="form-group">
57
+ <label>系统提示词(可选)</label>
58
+ <textarea id="ai-system-prompt" placeholder="你是 Minecraft 玩家助手。"></textarea>
59
  </div>
60
  <div class="form-group">
61
  <label>
62
+ <input type="checkbox" id="ai-enabled" checked> 启用
63
  </label>
64
  </div>
65
  <div class="form-actions">
66
+ <button type="submit" class="btn-primary">保存配置</button>
67
+ <button type="button" id="ai-test-btn" class="btn-secondary">测试连接</button>
68
+ <button type="button" id="ai-delete-btn" class="btn-danger">删除配置</button>
69
  </div>
70
  </form>
71
  <div id="ai-status" class="ai-status"></div>
 
74
 
75
  <div class="section" id="admin-key-section">
76
  <div class="section-header">
77
+ <h2>API 密钥管理</h2>
78
+ <button id="create-key-btn" class="btn-primary">创建密钥</button>
79
  </div>
80
  <div id="admin-keys-list" class="keys-list"></div>
81
  </div>
 
84
 
85
  <div id="user-screen" class="screen hidden">
86
  <nav class="navbar">
87
+ <h1>用户监测</h1>
88
  <div class="nav-info">
89
  <span id="user-info"></span>
90
+ <button class="logout-btn">退出登录</button>
91
  </div>
92
  </nav>
93
 
94
  <div class="container">
95
  <div class="stats-grid">
96
  <div class="stat-card">
97
+ <h3>活跃连接</h3>
98
  <p class="stat-value" id="user-stat-connections">0</p>
99
  </div>
100
  <div class="stat-card">
101
+ <h3>密钥总数</h3>
102
  <p class="stat-value" id="user-stat-total-keys">0</p>
103
  </div>
104
  <div class="stat-card">
 
116
  </div>
117
 
118
  <div class="section">
119
+ <h2>服务器密钥</h2>
120
  <div id="user-keys-list" class="keys-list"></div>
121
  </div>
122
 
123
  <div class="section">
124
+ <h2>命令控制台</h2>
125
  <form id="command-form" class="command-form">
126
+ <input type="text" id="command-input" placeholder="输入命令" required>
127
+ <button type="submit" class="btn-primary">发送</button>
128
  </form>
129
  <div id="command-history" class="command-history"></div>
130
  </div>
131
 
132
  <div class="section">
133
+ <h2>实时事件</h2>
134
  <div id="user-events-list" class="events-list"></div>
135
  </div>
136
  </div>
 
138
 
139
  <div id="create-key-modal" class="modal hidden">
140
  <div class="modal-content">
141
+ <h2>创建 API 密钥</h2>
142
  <form id="create-key-form">
143
+ <label>名称 *</label>
144
  <input type="text" id="key-name" required>
145
 
146
+ <label>描述</label>
147
  <textarea id="key-description"></textarea>
148
 
149
+ <label>密钥类型</label>
150
  <select id="key-type">
151
+ <option value="regular">普通</option>
152
+ <option value="admin">管理员</option>
153
  </select>
154
 
155
+ <label>服务器 ID(可选)</label>
156
  <input type="text" id="key-server-id">
157
 
158
  <div class="modal-actions">
159
+ <button type="button" id="cancel-create-btn" class="btn-secondary">取消</button>
160
+ <button type="submit" class="btn-primary">创建</button>
161
  </div>
162
  </form>
163
  </div>
 
165
 
166
  <div id="key-details-modal" class="modal hidden">
167
  <div class="modal-content">
168
+ <h2>密钥详情</h2>
169
  <div id="key-details-content"></div>
170
  <div class="modal-actions">
171
+ <button id="close-details-btn" class="btn-secondary">关闭</button>
172
  </div>
173
  </div>
174
  </div>
175
 
176
  <script src="app.js"></script>
177
  </body>
178
+ </html>
hf_repo/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('Reset Admin Key using recovery token (offline)')
393
- .requiredOption('-r, --recovery-token <token>', 'Admin recovery token')
394
- .option('--db-path <path>', 'Database path', process.env.DATABASE_PATH || 'minecraft_ws.db')
395
- .option('--keep-existing', 'Keep existing Admin Keys active')
396
- .option('--name <name>', 'New Admin Key name', 'Recovered Admin Key')
397
  .action(async (options) => {
398
  try {
399
  if (!fs.existsSync(options.dbPath)) {
400
- throw new Error(`Database not found at ${options.dbPath}`);
401
  }
402
 
403
  const db = new Database(options.dbPath);
@@ -408,12 +408,12 @@ program
408
  name: options.name
409
  });
410
 
411
- console.log('Admin Key reset successful.');
412
  console.log(` ID : ${result.id}`);
413
- console.log(` Name : ${result.name}`);
414
  console.log(` Key : ${result.key}`);
415
  } catch (error) {
416
- console.error(`\x1b[31mReset failed: ${error.message}\x1b[0m`);
417
  process.exit(1);
418
  }
419
  });
 
389
 
390
  program
391
  .command('reset-admin')
392
+ .description('使用恢复令牌重置 Admin Key (离线)')
393
+ .requiredOption('-r, --recovery-token <token>', '管理员恢复令牌')
394
+ .option('--db-path <path>', '数据库路径', process.env.DATABASE_PATH || 'minecraft_ws.db')
395
+ .option('--keep-existing', '保持现有 Admin Key 处于活跃状态')
396
+ .option('--name <name>', ' Admin Key 名称', 'Recovered Admin Key')
397
  .action(async (options) => {
398
  try {
399
  if (!fs.existsSync(options.dbPath)) {
400
+ throw new Error(`在 ${options.dbPath} 未找到数据库`);
401
  }
402
 
403
  const db = new Database(options.dbPath);
 
408
  name: options.name
409
  });
410
 
411
+ console.log('\x1b[32m✅ Admin Key 重置成功。\x1b[0m');
412
  console.log(` ID : ${result.id}`);
413
+ console.log(` 名称 : ${result.name}`);
414
  console.log(` Key : ${result.key}`);
415
  } catch (error) {
416
+ console.error(`\x1b[31m❌ 重置失败: ${error.message}\x1b[0m`);
417
  process.exit(1);
418
  }
419
  });
hf_repo/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 - Dashboard');
17
  console.log('='.repeat(50));
18
- console.log(`Dashboard URL: http://localhost:${PORT}`);
19
- console.log('Use Admin or Regular Key to log in.');
20
  console.log('='.repeat(50));
21
  });
 
13
 
14
  app.listen(PORT, () => {
15
  console.log('='.repeat(50));
16
+ console.log('Minecraft WebSocket API - 控制台');
17
  console.log('='.repeat(50));
18
+ console.log(`控制台地址: http://localhost:${PORT}`);
19
+ console.log('请使用 Admin Key Regular Key 登录。');
20
  console.log('='.repeat(50));
21
  });
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/docs/protocol.md CHANGED
@@ -1,32 +1,31 @@
1
- # Protocol Specification
2
 
3
- This document defines the REST and WebSocket protocol formats used by
4
- InterConnect-Server. All payloads are JSON.
5
 
6
- ## Authentication
7
 
8
- - REST: Send `Authorization: Bearer <API_KEY>` in the request headers.
9
- - WebSocket: Connect to `ws://<host>/ws?api_key=<API_KEY>` (use `wss` over HTTPS).
10
 
11
- ## Event Object
12
 
13
- Events are sent to the server via REST and broadcast to WebSocket clients.
14
- The server validates required fields and the `event_type` value only.
15
- The `data` payload is otherwise passed through as-is.
16
 
17
- Required fields:
18
- - `event_type` (string)
19
- - `server_name` (string)
20
- - `timestamp` (string, ISO 8601)
21
- - `data` (object)
22
 
23
- Valid `event_type` values:
24
  - `player_join`
25
  - `player_leave`
26
  - `message`
27
  - `server_command`
28
 
29
- Example event:
30
  ```json
31
  {
32
  "event_type": "player_join",
@@ -38,34 +37,34 @@ Example event:
38
  }
39
  ```
40
 
41
- ## REST Endpoints
42
 
43
  ### POST /api/events
44
 
45
- Submit an event to be stored and broadcast.
46
 
47
- Request:
48
- - Auth: any valid key
49
  - `Content-Type: application/json`
50
- - Body: Event Object
51
 
52
- Response:
53
  ```json
54
  {
55
  "message": "Event received and broadcasted"
56
  }
57
  ```
58
 
59
- Errors:
60
- - `400` if required fields are missing or `event_type` is invalid
61
- - `401` if the API key is missing/invalid
62
 
63
  ### POST /api/server/command
64
 
65
- Send a server command. This also broadcasts a `server_command` event.
66
 
67
- Request:
68
- - Auth: Regular or Admin key
69
  - `Content-Type: application/json`
70
  - Body:
71
  ```json
@@ -75,11 +74,11 @@ Request:
75
  }
76
  ```
77
 
78
- Notes:
79
- - `server_id` is required for Admin keys when not bound to a server.
80
- - The server enforces an allow/block list for Admin commands.
81
 
82
- Response:
83
  ```json
84
  {
85
  "message": "Command sent successfully",
@@ -88,23 +87,23 @@ Response:
88
  }
89
  ```
90
 
91
- ## WebSocket Messages
92
 
93
- ### Client to Server
94
 
95
  Ping:
96
  ```json
97
  { "type": "ping" }
98
  ```
99
 
100
- Pong response:
101
  ```json
102
  { "type": "pong" }
103
  ```
104
 
105
- ### Server to Client
106
 
107
- Broadcasted event:
108
  ```json
109
  {
110
  "type": "minecraft_event",
@@ -121,5 +120,4 @@ Broadcasted event:
121
  }
122
  ```
123
 
124
- `source_key_id_prefix` is the first 8 characters of the API key ID that
125
- submitted the event or command. It is not the API key itself.
 
1
+ # 协议规范
2
 
3
+ 本文档定义了 InterConnect-Server 使用的 REST WebSocket 协议格式。所有负载均为 JSON 格式。
 
4
 
5
+ ## 认证 (Authentication)
6
 
7
+ - REST: 在请求头中发送 `Authorization: Bearer <API_KEY>`。
8
+ - WebSocket: 连接到 `ws://<host>/ws?api_key=<API_KEY>`(HTTPS 下使用 `wss`)。
9
 
10
+ ## 事件对象 (Event Object)
11
 
12
+ 事件通过 REST 发送到服务器,并广播给 WebSocket 客户端。
13
+ 服务器仅验证必填字段和 `event_type` 值。
14
+ `data` 负载会原样传递。
15
 
16
+ 必填字段:
17
+ - `event_type` (字符串)
18
+ - `server_name` (字符串)
19
+ - `timestamp` (字符串, ISO 8601)
20
+ - `data` (对象)
21
 
22
+ 有效的 `event_type` 值:
23
  - `player_join`
24
  - `player_leave`
25
  - `message`
26
  - `server_command`
27
 
28
+ 事件示例:
29
  ```json
30
  {
31
  "event_type": "player_join",
 
37
  }
38
  ```
39
 
40
+ ## REST 端点 (REST Endpoints)
41
 
42
  ### POST /api/events
43
 
44
+ 提交一个事件以进行存储和广播。
45
 
46
+ 请求:
47
+ - 认证:任意有效密钥
48
  - `Content-Type: application/json`
49
+ - Body: 事件对象
50
 
51
+ 响应:
52
  ```json
53
  {
54
  "message": "Event received and broadcasted"
55
  }
56
  ```
57
 
58
+ 错误:
59
+ - `400` 如果缺少必填字段或 `event_type` 无效
60
+ - `401` 如果 API 密钥丢失/无效
61
 
62
  ### POST /api/server/command
63
 
64
+ 发送服务器命令。这也将广播一个 `server_command` 事件。
65
 
66
+ 请求:
67
+ - 认证:Regular Admin 密钥
68
  - `Content-Type: application/json`
69
  - Body:
70
  ```json
 
74
  }
75
  ```
76
 
77
+ 注意:
78
+ - Admin 密钥未绑定到服务器时,必须提供 `server_id`。
79
+ - 服务器对 Admin 命令强制执行允许/阻止列表。
80
 
81
+ 响应:
82
  ```json
83
  {
84
  "message": "Command sent successfully",
 
87
  }
88
  ```
89
 
90
+ ## WebSocket 消息 (WebSocket Messages)
91
 
92
+ ### 客户端到服务器
93
 
94
  Ping:
95
  ```json
96
  { "type": "ping" }
97
  ```
98
 
99
+ Pong 响应:
100
  ```json
101
  { "type": "pong" }
102
  ```
103
 
104
+ ### 服务器到客户端
105
 
106
+ 广播事件:
107
  ```json
108
  {
109
  "type": "minecraft_event",
 
120
  }
121
  ```
122
 
123
+ `source_key_id_prefix` 是提交事件或命令的 API 密钥 ID 的前 8 个字符。它不是 API 密钥本身。
 
hf_repo/hf_repo/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 superKey = null;
 
3
  let ws = null;
 
 
4
 
5
  const loginScreen = document.getElementById('login-screen');
6
- const dashboardScreen = document.getElementById('dashboard-screen');
 
7
  const loginForm = document.getElementById('login-form');
8
  const loginError = document.getElementById('login-error');
9
- const logoutBtn = document.getElementById('logout-btn');
 
 
 
10
  const createKeyBtn = document.getElementById('create-key-btn');
11
  const createKeyModal = document.getElementById('create-key-modal');
12
  const createKeyForm = document.getElementById('create-key-form');
13
  const cancelCreateBtn = document.getElementById('cancel-create-btn');
14
  const keyDetailsModal = document.getElementById('key-details-modal');
15
  const closeDetailsBtn = document.getElementById('close-details-btn');
 
 
 
 
 
 
16
 
17
- loginForm.addEventListener('submit', async (e) => {
18
- e.preventDefault();
19
- const key = document.getElementById('super-key-input').value;
20
-
21
- try {
22
- // 验证密钥是否有效
23
- const response = await fetch(`${API_URL}/health`, {
24
- headers: { 'Authorization': `Bearer ${key}` }
25
- });
26
-
27
- if (response.ok) {
28
- superKey = key;
29
-
30
- // 验证密钥类型
31
- let userKeyType = null;
32
- let userServerId = null;
33
-
34
- // 尝试Admin Key验证
35
- const adminResponse = await fetch(`${API_URL}/manage/keys`, {
36
- headers: { 'Authorization': `Bearer ${key}` }
37
- });
38
-
39
- if (adminResponse.ok) {
40
- userKeyType = 'admin';
41
- } else {
42
- // 尝试Server Key验证
43
- const serverResponse = await fetch(`${API_URL}/api/server/info`, {
44
- headers: { 'Authorization': `Bearer ${key}` }
45
- });
46
-
47
- if (serverResponse.ok) {
48
- userKeyType = 'server';
49
- const serverData = await serverResponse.json();
50
- userServerId = serverData.server_id;
51
- } else {
52
- // 尝试Regular Key验证(获取自己的Server Key列表)
53
- const regularResponse = await fetch(`${API_URL}/manage/keys/server-keys`, {
54
- headers: { 'Authorization': `Bearer ${key}` }
55
- });
56
-
57
- if (regularResponse.ok) {
58
- userKeyType = 'regular';
59
- } else {
60
- throw new Error('无效的密钥或权限不足');
61
- }
62
- }
63
- }
64
-
65
- loginError.textContent = '';
66
- showDashboard(userKeyType, userServerId);
67
- } else {
68
- loginError.textContent = '无效的密钥';
69
- }
70
- } catch (error) {
71
- loginError.textContent = error.message || '无法连接到服务器';
72
  }
73
- });
74
 
75
- logoutBtn.addEventListener('click', () => {
76
- superKey = null;
77
  if (ws) {
78
  ws.close();
79
  ws = null;
80
  }
81
- loginScreen.classList.remove('hidden');
82
- dashboardScreen.classList.add('hidden');
83
- });
84
 
85
- createKeyBtn.addEventListener('click', () => {
86
- createKeyModal.classList.remove('hidden');
87
- });
 
 
 
 
 
 
 
88
 
89
- cancelCreateBtn.addEventListener('click', () => {
90
- createKeyModal.classList.add('hidden');
91
- createKeyForm.reset();
92
- });
93
 
94
- closeDetailsBtn.addEventListener('click', () => {
95
- keyDetailsModal.classList.add('hidden');
96
- });
97
 
98
- createKeyForm.addEventListener('submit', async (e) => {
99
- e.preventDefault();
100
-
101
- const name = document.getElementById('key-name').value;
102
- const description = document.getElementById('key-description').value;
103
- const isSuper = document.getElementById('key-is-super').checked;
104
-
105
- try {
106
- const response = await fetch(`${API_URL}/manage/keys`, {
107
- method: 'POST',
108
- headers: {
109
- 'Authorization': `Bearer ${superKey}`,
110
- 'Content-Type': 'application/json'
111
- },
112
- body: JSON.stringify({
113
- name,
114
- description,
115
- is_super_key: isSuper
116
- })
117
- });
118
-
119
- if (response.ok) {
120
- const result = await response.json();
121
- createKeyModal.classList.add('hidden');
122
- createKeyForm.reset();
123
-
124
- showKeyCreatedModal(result);
125
- loadKeys();
126
- } else {
127
- const error = await response.json();
128
- alert('创建失败: ' + error.detail);
129
- }
130
- } catch (error) {
131
- alert('创建失败: ' + error.message);
132
  }
133
- });
134
 
135
- function showKeyCreatedModal(keyData) {
136
- const content = `
137
- <p><strong>密钥创建成功!</strong></p>
138
- <p>名称: ${keyData.name}</p>
139
- <p>类型: ${keyData.isSuperKey ? 'SuperKey' : '普通密钥'}</p>
140
- <div class="key-display">
141
- <strong>⚠️ 请立即复制并保存此密钥(仅显示一次):</strong><br>
142
- ${keyData.key}
143
- </div>
144
- `;
145
-
146
- document.getElementById('key-details-content').innerHTML = content;
147
- keyDetailsModal.classList.remove('hidden');
148
  }
149
 
150
- // 全局变量
151
- let currentUserKeyType = null;
152
- let currentUserServerId = null;
 
 
 
 
 
 
 
 
 
153
 
154
- async function showDashboard(userKeyType, userServerId) {
155
- currentUserKeyType = userKeyType;
156
- currentUserServerId = userServerId;
157
-
158
  loginScreen.classList.add('hidden');
159
- dashboardScreen.classList.remove('hidden');
160
-
161
- // 显示用户信息
162
- const userInfo = document.getElementById('user-info');
163
- const keyTypeIcons = { admin: '👑', server: '🖥️', regular: '🔑' };
164
- const keyTypeNames = { admin: 'Admin', server: 'Server', regular: 'Regular' };
165
- userInfo.textContent = `${keyTypeIcons[userKeyType]} ${keyTypeNames[userKeyType]} 用户`;
166
-
167
- // 根据权限显示/隐藏功能
168
- const adminSection = document.getElementById('admin-section');
169
- const serverSection = document.getElementById('server-section');
170
-
171
- if (userKeyType === 'admin') {
172
- adminSection.style.display = 'block';
173
- serverSection.style.display = 'block';
174
- loadKeys();
175
- } else if (userKeyType === 'server') {
176
- adminSection.style.display = 'none';
177
- serverSection.style.display = 'block';
178
- loadServerInfo();
179
- } else if (userKeyType === 'regular') {
180
- adminSection.style.display = 'none';
181
- serverSection.style.display = 'block';
182
- loadRegularServerKeys();
183
- }
184
-
185
- // 根据用户类型控制创建密钥按钮的显示
186
- const createKeyBtn = document.getElementById('create-key-btn');
187
- if (createKeyBtn) {
188
- createKeyBtn.style.display = userKeyType === 'admin' ? 'block' : 'none';
189
- }
190
-
191
  loadStats();
192
  connectWebSocket();
193
-
194
- setInterval(loadStats, 5000);
195
- }
196
 
197
- async function loadStats() {
198
- try {
199
- const response = await fetch(`${API_URL}/health`);
200
- const data = await response.json();
201
-
202
- document.getElementById('stat-connections').textContent = data.active_ws || 0;
203
- document.getElementById('stat-total-keys').textContent = data.keys_total || 0;
204
- document.getElementById('stat-super-keys').textContent = data.super_active || 0;
205
- document.getElementById('stat-regular-keys').textContent = data.regular_active || 0;
206
- } catch (error) {
207
- console.error('Failed to load stats:', error);
208
- }
209
  }
210
 
211
- async function loadKeys() {
 
 
 
212
  try {
213
  const response = await fetch(`${API_URL}/manage/keys`, {
214
- headers: { 'Authorization': `Bearer ${superKey}` }
215
  });
216
-
217
  if (response.ok) {
218
  const keys = await response.json();
219
- renderKeys(keys);
 
 
220
  }
221
  } catch (error) {
222
- console.error('Failed to load keys:', error);
223
  }
224
  }
225
 
226
- async function loadRegularServerKeys() {
 
 
 
227
  try {
228
  const response = await fetch(`${API_URL}/manage/keys/server-keys`, {
229
- headers: { 'Authorization': `Bearer ${superKey}` }
230
  });
231
-
232
  if (response.ok) {
233
- const serverKeys = await response.json();
234
- renderServerKeys(serverKeys);
235
  } else {
236
- document.getElementById('keys-list').innerHTML = '<p>无法加载Server Key列表</p>';
237
  }
238
  } catch (error) {
239
- console.error('Failed to load server keys:', error);
240
- document.getElementById('keys-list').innerHTML = '<p>无法加载Server Key列表</p>';
241
  }
242
  }
243
 
244
- function renderServerKeys(keys) {
245
- const keysList = document.getElementById('keys-list');
246
-
247
- if (keys.length === 0) {
248
- keysList.innerHTML = '<p>暂无关联的Server Key</p>';
249
  return;
250
  }
251
-
252
- keysList.innerHTML = keys.map(key => `
253
- <div class="key-card">
 
 
 
 
 
254
  <div class="key-info">
255
  <h3>
256
- <span class="key-badge server">🖥️ Server</span>
 
 
257
  <span class="key-badge ${key.isActive ? 'active' : 'inactive'}">
258
- ${key.isActive ? '活跃' : '已停用'}
259
  </span>
260
  ${key.name}
261
  </h3>
262
  <p>ID: ${key.id}</p>
263
- <p>前缀: ${key.keyPrefix}</p>
264
- ${key.serverId ? `<p>服务器ID: ${key.serverId}</p>` : ''}
265
- <p>创建时间: ${new Date(key.createdAt).toLocaleString('zh-CN')}</p>
266
- <p>最后使用: ${key.lastUsed ? new Date(key.lastUsed).toLocaleString('zh-CN') : '从未使用'}</p>
267
  </div>
268
  <div class="key-actions">
269
- ${key.isActive ?
270
- `<button class="btn-danger" onclick="deactivateKey('${key.id}')">停用</button>` :
271
- `<button class="btn-success" onclick="activateKey('${key.id}')">激活</button>`
272
  }
273
- <button class="btn-danger" onclick="deleteKey('${key.id}', '${key.name}')">删除</button>
274
  </div>
275
  </div>
276
  `).join('');
277
  }
278
 
279
- function renderKeys(keys) {
280
- const keysList = document.getElementById('keys-list');
281
-
282
- if (keys.length === 0) {
283
- keysList.innerHTML = '<p>暂无API密钥</p>';
284
  return;
285
  }
286
-
287
- keysList.innerHTML = keys.map(key => `
288
- <div class="key-card ${key.keyType === 'admin' ? 'super' : ''}">
 
 
 
 
 
289
  <div class="key-info">
290
  <h3>
291
- <span class="key-badge ${key.keyType}">
292
- ${key.keyType === 'admin' ? '👑 Admin' : key.keyType === 'server' ? '🖥️ Server' : '🔑 Regular'}
293
- </span>
294
  <span class="key-badge ${key.isActive ? 'active' : 'inactive'}">
295
- ${key.isActive ? '活跃' : '已停用'}
296
  </span>
297
  ${key.name}
298
  </h3>
299
  <p>ID: ${key.id}</p>
300
- <p>前缀: ${key.keyPrefix}</p>
301
- ${key.serverId ? `<p>服务器ID: ${key.serverId}</p>` : ''}
302
- <p>创建时间: ${new Date(key.createdAt).toLocaleString('zh-CN')}</p>
303
- <p>最后使用: ${key.lastUsed ? new Date(key.lastUsed).toLocaleString('zh-CN') : '从未使用'}</p>
304
  </div>
305
  <div class="key-actions">
306
- ${key.isActive ?
307
- `<button class="btn-danger" onclick="deactivateKey('${key.id}')">停用</button>` :
308
- `<button class="btn-success" onclick="activateKey('${key.id}')">激活</button>`
309
  }
310
- <button class="btn-danger" onclick="deleteKey('${key.id}', '${key.name}')">删除</button>
311
  </div>
312
  </div>
313
  `).join('');
314
  }
315
 
316
  async function activateKey(keyId) {
 
 
 
317
  try {
318
  const response = await fetch(`${API_URL}/manage/keys/${keyId}/activate`, {
319
  method: 'PATCH',
320
- headers: { 'Authorization': `Bearer ${superKey}` }
321
  });
322
-
323
- if (response.ok) {
324
- if (currentUserKeyType === 'admin') {
325
- loadKeys();
326
- } else if (currentUserKeyType === 'regular') {
327
- loadRegularServerKeys();
328
- }
329
- } else {
330
  const error = await response.json();
331
- alert('激活失败: ' + error.detail);
 
 
 
 
 
 
 
332
  }
333
  } catch (error) {
334
- alert('激活失败: ' + error.message);
335
  }
336
  }
337
 
338
  async function deactivateKey(keyId) {
 
 
 
339
  try {
340
  const response = await fetch(`${API_URL}/manage/keys/${keyId}/deactivate`, {
341
  method: 'PATCH',
342
- headers: { 'Authorization': `Bearer ${superKey}` }
343
  });
344
-
345
- if (response.ok) {
346
- if (currentUserKeyType === 'admin') {
347
- loadKeys();
348
- } else if (currentUserKeyType === 'regular') {
349
- loadRegularServerKeys();
350
- }
351
- } else {
352
  const error = await response.json();
353
- alert('停用失败: ' + error.detail);
 
 
 
 
 
 
 
354
  }
355
  } catch (error) {
356
- alert('停用失败: ' + error.message);
357
  }
358
  }
359
 
360
  async function deleteKey(keyId, keyName) {
361
- if (!confirm(`确定要删除密钥 "${keyName}" 吗?此操作无法撤销。`)) {
 
 
 
362
  return;
363
  }
364
-
365
  try {
366
  const response = await fetch(`${API_URL}/manage/keys/${keyId}`, {
367
  method: 'DELETE',
368
- headers: { 'Authorization': `Bearer ${superKey}` }
369
  });
370
-
371
- if (response.ok) {
372
- if (currentUserKeyType === 'admin') {
373
- loadKeys();
374
- } else if (currentUserKeyType === 'regular') {
375
- loadRegularServerKeys();
376
- }
377
- } else {
378
  const error = await response.json();
379
- alert('删除失败: ' + error.detail);
 
380
  }
 
 
381
  } catch (error) {
382
- alert('删除失败: ' + error.message);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
383
  }
384
  }
385
 
386
  function connectWebSocket() {
387
- if (!superKey) return;
388
-
389
- const wsUrl = `ws://localhost:8000/ws?api_key=${superKey}`;
 
 
 
390
  ws = new WebSocket(wsUrl);
391
-
392
  ws.onopen = () => {
393
  console.log('WebSocket connected');
394
  };
395
-
396
  ws.onmessage = (event) => {
397
  try {
398
  const message = JSON.parse(event.data);
399
-
400
  if (message.type === 'minecraft_event') {
401
  addEventToList(message.event);
402
  }
@@ -404,21 +401,23 @@ function connectWebSocket() {
404
  console.error('Failed to parse WebSocket message:', error);
405
  }
406
  };
407
-
408
  ws.onerror = (error) => {
409
  console.error('WebSocket error:', error);
410
  };
411
-
412
  ws.onclose = () => {
413
  console.log('WebSocket disconnected');
414
- setTimeout(() => {
415
- if (superKey) {
416
- connectWebSocket();
417
- }
418
- }, 5000);
 
 
419
  };
420
-
421
- setInterval(() => {
422
  if (ws && ws.readyState === WebSocket.OPEN) {
423
  ws.send(JSON.stringify({ type: 'ping' }));
424
  }
@@ -426,19 +425,183 @@ function connectWebSocket() {
426
  }
427
 
428
  function addEventToList(event) {
429
- const eventsList = document.getElementById('events-list');
430
-
 
 
431
  const eventItem = document.createElement('div');
432
  eventItem.className = 'event-item';
433
  eventItem.innerHTML = `
434
  <strong>${event.event_type}</strong> - ${event.server_name}<br>
435
- <small>${new Date(event.timestamp).toLocaleString('zh-CN')}</small><br>
436
  <pre>${JSON.stringify(event.data, null, 2)}</pre>
437
  `;
438
-
439
- eventsList.insertBefore(eventItem, eventsList.firstChild);
440
-
441
- while (eventsList.children.length > 50) {
442
- eventsList.removeChild(eventsList.lastChild);
 
 
 
 
 
 
443
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
444
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  const API_URL = window.location.origin;
2
+ let apiKey = null;
3
+ let currentRole = null;
4
  let ws = null;
5
+ let statsIntervalId = null;
6
+ let wsPingIntervalId = null;
7
 
8
  const loginScreen = document.getElementById('login-screen');
9
+ const adminScreen = document.getElementById('admin-screen');
10
+ const userScreen = document.getElementById('user-screen');
11
  const loginForm = document.getElementById('login-form');
12
  const loginError = document.getElementById('login-error');
13
+ const apiKeyInput = document.getElementById('api-key-input');
14
+ const logoutButtons = document.querySelectorAll('.logout-btn');
15
+ const adminUserInfo = document.getElementById('admin-user-info');
16
+ const userInfo = document.getElementById('user-info');
17
  const createKeyBtn = document.getElementById('create-key-btn');
18
  const createKeyModal = document.getElementById('create-key-modal');
19
  const createKeyForm = document.getElementById('create-key-form');
20
  const cancelCreateBtn = document.getElementById('cancel-create-btn');
21
  const keyDetailsModal = document.getElementById('key-details-modal');
22
  const closeDetailsBtn = document.getElementById('close-details-btn');
23
+ const adminKeysList = document.getElementById('admin-keys-list');
24
+ const userKeysList = document.getElementById('user-keys-list');
25
+ const commandForm = document.getElementById('command-form');
26
+ const commandInput = document.getElementById('command-input');
27
+ const commandHistory = document.getElementById('command-history');
28
+ const userEventsList = document.getElementById('user-events-list');
29
 
30
+ function authHeaders(key) {
31
+ return { Authorization: `Bearer ${key}` };
32
+ }
33
+
34
+ function resetSession() {
35
+ apiKey = null;
36
+ currentRole = null;
37
+
38
+ if (statsIntervalId) {
39
+ clearInterval(statsIntervalId);
40
+ statsIntervalId = null;
41
+ }
42
+
43
+ if (wsPingIntervalId) {
44
+ clearInterval(wsPingIntervalId);
45
+ wsPingIntervalId = null;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  }
 
47
 
 
 
48
  if (ws) {
49
  ws.close();
50
  ws = null;
51
  }
 
 
 
52
 
53
+ if (loginScreen) {
54
+ loginScreen.classList.remove('hidden');
55
+ }
56
+ if (adminScreen) {
57
+ adminScreen.classList.add('hidden');
58
+ }
59
+ if (userScreen) {
60
+ userScreen.classList.add('hidden');
61
+ }
62
+ }
63
 
64
+ async function detectRole(key) {
65
+ const adminResponse = await fetch(`${API_URL}/manage/keys`, {
66
+ headers: authHeaders(key)
67
+ });
68
 
69
+ if (adminResponse.ok) {
70
+ return 'admin';
71
+ }
72
 
73
+ const regularResponse = await fetch(`${API_URL}/manage/keys/server-keys`, {
74
+ headers: authHeaders(key)
75
+ });
76
+
77
+ if (regularResponse.ok) {
78
+ return 'regular';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  }
 
80
 
81
+ return null;
 
 
 
 
 
 
 
 
 
 
 
 
82
  }
83
 
84
+ function showAdminPanel() {
85
+ currentRole = 'admin';
86
+ loginScreen.classList.add('hidden');
87
+ userScreen.classList.add('hidden');
88
+ adminScreen.classList.remove('hidden');
89
+
90
+ if (adminUserInfo) {
91
+ adminUserInfo.textContent = 'Admin Key';
92
+ }
93
+
94
+ loadAdminKeys();
95
+ }
96
 
97
+ function showUserPanel() {
98
+ currentRole = 'regular';
 
 
99
  loginScreen.classList.add('hidden');
100
+ adminScreen.classList.add('hidden');
101
+ userScreen.classList.remove('hidden');
102
+
103
+ if (userInfo) {
104
+ userInfo.textContent = 'Regular Key';
105
+ }
106
+
107
+ loadUserServerKeys();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  loadStats();
109
  connectWebSocket();
 
 
 
110
 
111
+ statsIntervalId = setInterval(loadStats, 5000);
 
 
 
 
 
 
 
 
 
 
 
112
  }
113
 
114
+ async function loadAdminKeys() {
115
+ if (!adminKeysList || !apiKey) {
116
+ return;
117
+ }
118
  try {
119
  const response = await fetch(`${API_URL}/manage/keys`, {
120
+ headers: authHeaders(apiKey)
121
  });
122
+
123
  if (response.ok) {
124
  const keys = await response.json();
125
+ renderAdminKeys(keys);
126
+ } else {
127
+ adminKeysList.innerHTML = '<p>Failed to load keys.</p>';
128
  }
129
  } catch (error) {
130
+ adminKeysList.innerHTML = `<p>Failed to load keys: ${error.message}</p>`;
131
  }
132
  }
133
 
134
+ async function loadUserServerKeys() {
135
+ if (!userKeysList || !apiKey) {
136
+ return;
137
+ }
138
  try {
139
  const response = await fetch(`${API_URL}/manage/keys/server-keys`, {
140
+ headers: authHeaders(apiKey)
141
  });
142
+
143
  if (response.ok) {
144
+ const keys = await response.json();
145
+ renderUserServerKeys(keys);
146
  } else {
147
+ userKeysList.innerHTML = '<p>Failed to load server keys.</p>';
148
  }
149
  } catch (error) {
150
+ userKeysList.innerHTML = `<p>Failed to load server keys: ${error.message}</p>`;
 
151
  }
152
  }
153
 
154
+ function renderAdminKeys(keys) {
155
+ if (!adminKeysList) {
 
 
 
156
  return;
157
  }
158
+
159
+ if (!keys.length) {
160
+ adminKeysList.innerHTML = '<p>No API keys found.</p>';
161
+ return;
162
+ }
163
+
164
+ adminKeysList.innerHTML = keys.map((key) => `
165
+ <div class="key-card ${key.keyType === 'admin' ? 'admin' : ''}">
166
  <div class="key-info">
167
  <h3>
168
+ <span class="key-badge ${key.keyType}">
169
+ ${key.keyType === 'admin' ? 'Admin' : key.keyType === 'server' ? 'Server' : 'Regular'}
170
+ </span>
171
  <span class="key-badge ${key.isActive ? 'active' : 'inactive'}">
172
+ ${key.isActive ? 'Active' : 'Inactive'}
173
  </span>
174
  ${key.name}
175
  </h3>
176
  <p>ID: ${key.id}</p>
177
+ <p>Prefix: ${key.keyPrefix}</p>
178
+ ${key.serverId ? `<p>Server ID: ${key.serverId}</p>` : ''}
179
+ <p>Created: ${new Date(key.createdAt).toLocaleString()}</p>
180
+ <p>Last Used: ${key.lastUsed ? new Date(key.lastUsed).toLocaleString() : 'Never'}</p>
181
  </div>
182
  <div class="key-actions">
183
+ ${key.isActive
184
+ ? `<button class="btn-danger" onclick="deactivateKey('${key.id}')">Deactivate</button>`
185
+ : `<button class="btn-success" onclick="activateKey('${key.id}')">Activate</button>`
186
  }
187
+ <button class="btn-danger" onclick="deleteKey('${key.id}', '${key.name}')">Delete</button>
188
  </div>
189
  </div>
190
  `).join('');
191
  }
192
 
193
+ function renderUserServerKeys(keys) {
194
+ if (!userKeysList) {
 
 
 
195
  return;
196
  }
197
+
198
+ if (!keys.length) {
199
+ userKeysList.innerHTML = '<p>No server keys available.</p>';
200
+ return;
201
+ }
202
+
203
+ userKeysList.innerHTML = keys.map((key) => `
204
+ <div class="key-card">
205
  <div class="key-info">
206
  <h3>
207
+ <span class="key-badge server">Server</span>
 
 
208
  <span class="key-badge ${key.isActive ? 'active' : 'inactive'}">
209
+ ${key.isActive ? 'Active' : 'Inactive'}
210
  </span>
211
  ${key.name}
212
  </h3>
213
  <p>ID: ${key.id}</p>
214
+ <p>Prefix: ${key.keyPrefix}</p>
215
+ ${key.serverId ? `<p>Server ID: ${key.serverId}</p>` : ''}
216
+ <p>Created: ${new Date(key.createdAt).toLocaleString()}</p>
217
+ <p>Last Used: ${key.lastUsed ? new Date(key.lastUsed).toLocaleString() : 'Never'}</p>
218
  </div>
219
  <div class="key-actions">
220
+ ${key.isActive
221
+ ? `<button class="btn-danger" onclick="deactivateKey('${key.id}')">Deactivate</button>`
222
+ : `<button class="btn-success" onclick="activateKey('${key.id}')">Activate</button>`
223
  }
 
224
  </div>
225
  </div>
226
  `).join('');
227
  }
228
 
229
  async function activateKey(keyId) {
230
+ if (!apiKey) {
231
+ return;
232
+ }
233
  try {
234
  const response = await fetch(`${API_URL}/manage/keys/${keyId}/activate`, {
235
  method: 'PATCH',
236
+ headers: authHeaders(apiKey)
237
  });
238
+
239
+ if (!response.ok) {
 
 
 
 
 
 
240
  const error = await response.json();
241
+ alert(`Failed to activate key: ${error.detail}`);
242
+ return;
243
+ }
244
+
245
+ if (currentRole === 'admin') {
246
+ loadAdminKeys();
247
+ } else if (currentRole === 'regular') {
248
+ loadUserServerKeys();
249
  }
250
  } catch (error) {
251
+ alert(`Failed to activate key: ${error.message}`);
252
  }
253
  }
254
 
255
  async function deactivateKey(keyId) {
256
+ if (!apiKey) {
257
+ return;
258
+ }
259
  try {
260
  const response = await fetch(`${API_URL}/manage/keys/${keyId}/deactivate`, {
261
  method: 'PATCH',
262
+ headers: authHeaders(apiKey)
263
  });
264
+
265
+ if (!response.ok) {
 
 
 
 
 
 
266
  const error = await response.json();
267
+ alert(`Failed to deactivate key: ${error.detail}`);
268
+ return;
269
+ }
270
+
271
+ if (currentRole === 'admin') {
272
+ loadAdminKeys();
273
+ } else if (currentRole === 'regular') {
274
+ loadUserServerKeys();
275
  }
276
  } catch (error) {
277
+ alert(`Failed to deactivate key: ${error.message}`);
278
  }
279
  }
280
 
281
  async function deleteKey(keyId, keyName) {
282
+ if (currentRole !== 'admin') {
283
+ return;
284
+ }
285
+ if (!confirm(`Delete key "${keyName}"? This action cannot be undone.`)) {
286
  return;
287
  }
 
288
  try {
289
  const response = await fetch(`${API_URL}/manage/keys/${keyId}`, {
290
  method: 'DELETE',
291
+ headers: authHeaders(apiKey)
292
  });
293
+
294
+ if (!response.ok) {
 
 
 
 
 
 
295
  const error = await response.json();
296
+ alert(`Failed to delete key: ${error.detail}`);
297
+ return;
298
  }
299
+
300
+ loadAdminKeys();
301
  } catch (error) {
302
+ alert(`Failed to delete key: ${error.message}`);
303
+ }
304
+ }
305
+
306
+ function showKeyCreatedModal(payload) {
307
+ if (!keyDetailsModal) {
308
+ return;
309
+ }
310
+
311
+ let content = '';
312
+ if (payload.regularKey && payload.serverKey) {
313
+ content = `
314
+ <p><strong>Regular Key</strong></p>
315
+ <p>Name: ${payload.regularKey.name}</p>
316
+ <p>Type: ${payload.regularKey.keyType}</p>
317
+ <p>ID: ${payload.regularKey.id}</p>
318
+ <p>Key: ${payload.regularKey.key}</p>
319
+ <hr>
320
+ <p><strong>Server Key</strong></p>
321
+ <p>Name: ${payload.serverKey.name}</p>
322
+ <p>Type: ${payload.serverKey.keyType}</p>
323
+ <p>ID: ${payload.serverKey.id}</p>
324
+ <p>Key: ${payload.serverKey.key}</p>
325
+ `;
326
+ } else {
327
+ content = `
328
+ <p><strong>Key Created</strong></p>
329
+ <p>Name: ${payload.name}</p>
330
+ <p>Type: ${payload.keyType}</p>
331
+ <p>ID: ${payload.id}</p>
332
+ <p>Key: ${payload.key}</p>
333
+ `;
334
+ }
335
+
336
+ const detailsContent = document.getElementById('key-details-content');
337
+ if (detailsContent) {
338
+ detailsContent.innerHTML = content;
339
+ }
340
+
341
+ keyDetailsModal.classList.remove('hidden');
342
+ }
343
+
344
+ async function loadStats() {
345
+ try {
346
+ const response = await fetch(`${API_URL}/health`);
347
+ if (!response.ok) {
348
+ return;
349
+ }
350
+ const data = await response.json();
351
+
352
+ const connections = document.getElementById('user-stat-connections');
353
+ if (connections) {
354
+ connections.textContent = data.active_ws || 0;
355
+ }
356
+
357
+ const totalKeys = document.getElementById('user-stat-total-keys');
358
+ if (totalKeys) {
359
+ totalKeys.textContent = data.keys_total || 0;
360
+ }
361
+
362
+ const adminKeys = document.getElementById('user-stat-admin-keys');
363
+ if (adminKeys) {
364
+ adminKeys.textContent = data.admin_active || 0;
365
+ }
366
+
367
+ const regularKeys = document.getElementById('user-stat-regular-keys');
368
+ if (regularKeys) {
369
+ regularKeys.textContent = data.regular_active || 0;
370
+ }
371
+
372
+ const serverKeys = document.getElementById('user-stat-server-keys');
373
+ if (serverKeys) {
374
+ serverKeys.textContent = data.server_active || 0;
375
+ }
376
+ } catch (error) {
377
+ console.error('Failed to load stats:', error);
378
  }
379
  }
380
 
381
  function connectWebSocket() {
382
+ if (!apiKey) {
383
+ return;
384
+ }
385
+
386
+ const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
387
+ const wsUrl = `${protocol}://${window.location.host}/ws?api_key=${apiKey}`;
388
  ws = new WebSocket(wsUrl);
389
+
390
  ws.onopen = () => {
391
  console.log('WebSocket connected');
392
  };
393
+
394
  ws.onmessage = (event) => {
395
  try {
396
  const message = JSON.parse(event.data);
 
397
  if (message.type === 'minecraft_event') {
398
  addEventToList(message.event);
399
  }
 
401
  console.error('Failed to parse WebSocket message:', error);
402
  }
403
  };
404
+
405
  ws.onerror = (error) => {
406
  console.error('WebSocket error:', error);
407
  };
408
+
409
  ws.onclose = () => {
410
  console.log('WebSocket disconnected');
411
+ if (wsPingIntervalId) {
412
+ clearInterval(wsPingIntervalId);
413
+ wsPingIntervalId = null;
414
+ }
415
+ if (apiKey && currentRole === 'regular') {
416
+ setTimeout(() => connectWebSocket(), 5000);
417
+ }
418
  };
419
+
420
+ wsPingIntervalId = setInterval(() => {
421
  if (ws && ws.readyState === WebSocket.OPEN) {
422
  ws.send(JSON.stringify({ type: 'ping' }));
423
  }
 
425
  }
426
 
427
  function addEventToList(event) {
428
+ if (!userEventsList) {
429
+ return;
430
+ }
431
+
432
  const eventItem = document.createElement('div');
433
  eventItem.className = 'event-item';
434
  eventItem.innerHTML = `
435
  <strong>${event.event_type}</strong> - ${event.server_name}<br>
436
+ <small>${new Date(event.timestamp).toLocaleString()}</small><br>
437
  <pre>${JSON.stringify(event.data, null, 2)}</pre>
438
  `;
439
+
440
+ userEventsList.insertBefore(eventItem, userEventsList.firstChild);
441
+
442
+ while (userEventsList.children.length > 50) {
443
+ userEventsList.removeChild(userEventsList.lastChild);
444
+ }
445
+ }
446
+
447
+ function appendCommandHistory(command, status, detail) {
448
+ if (!commandHistory) {
449
+ return;
450
  }
451
+
452
+ const entry = document.createElement('div');
453
+ entry.className = 'command-item';
454
+ entry.innerHTML = `
455
+ <div>${status}: ${command}</div>
456
+ ${detail ? `<div class="timestamp">${detail}</div>` : ''}
457
+ `;
458
+ commandHistory.insertBefore(entry, commandHistory.firstChild);
459
+
460
+ while (commandHistory.children.length > 20) {
461
+ commandHistory.removeChild(commandHistory.lastChild);
462
+ }
463
+ }
464
+
465
+ if (loginForm) {
466
+ loginForm.addEventListener('submit', async (event) => {
467
+ event.preventDefault();
468
+ const key = apiKeyInput.value.trim();
469
+ loginError.textContent = '';
470
+
471
+ if (!key) {
472
+ loginError.textContent = 'API key is required.';
473
+ return;
474
+ }
475
+
476
+ try {
477
+ const role = await detectRole(key);
478
+ if (!role) {
479
+ loginError.textContent = 'Invalid key or insufficient permissions.';
480
+ return;
481
+ }
482
+
483
+ apiKey = key;
484
+
485
+ if (role === 'admin') {
486
+ showAdminPanel();
487
+ } else if (role === 'regular') {
488
+ showUserPanel();
489
+ }
490
+ } catch (error) {
491
+ loginError.textContent = error.message || 'Unable to connect to server.';
492
+ }
493
+ });
494
+ }
495
+
496
+ logoutButtons.forEach((button) => {
497
+ button.addEventListener('click', () => {
498
+ resetSession();
499
+ });
500
+ });
501
+
502
+ if (createKeyBtn) {
503
+ createKeyBtn.addEventListener('click', () => {
504
+ createKeyModal.classList.remove('hidden');
505
+ });
506
  }
507
+
508
+ if (cancelCreateBtn) {
509
+ cancelCreateBtn.addEventListener('click', () => {
510
+ createKeyModal.classList.add('hidden');
511
+ createKeyForm.reset();
512
+ });
513
+ }
514
+
515
+ if (closeDetailsBtn) {
516
+ closeDetailsBtn.addEventListener('click', () => {
517
+ keyDetailsModal.classList.add('hidden');
518
+ });
519
+ }
520
+
521
+ if (createKeyForm) {
522
+ createKeyForm.addEventListener('submit', async (event) => {
523
+ event.preventDefault();
524
+
525
+ const name = document.getElementById('key-name').value.trim();
526
+ const description = document.getElementById('key-description').value.trim();
527
+ const keyType = document.getElementById('key-type').value;
528
+ const serverId = document.getElementById('key-server-id').value.trim();
529
+
530
+ if (!name) {
531
+ alert('Name is required.');
532
+ return;
533
+ }
534
+
535
+ try {
536
+ const payload = {
537
+ name,
538
+ description,
539
+ key_type: keyType
540
+ };
541
+
542
+ if (serverId) {
543
+ payload.server_id = serverId;
544
+ }
545
+
546
+ const response = await fetch(`${API_URL}/manage/keys`, {
547
+ method: 'POST',
548
+ headers: {
549
+ ...authHeaders(apiKey),
550
+ 'Content-Type': 'application/json'
551
+ },
552
+ body: JSON.stringify(payload)
553
+ });
554
+
555
+ if (!response.ok) {
556
+ const error = await response.json();
557
+ alert(`Failed to create key: ${error.detail}`);
558
+ return;
559
+ }
560
+
561
+ const result = await response.json();
562
+ createKeyModal.classList.add('hidden');
563
+ createKeyForm.reset();
564
+ showKeyCreatedModal(result);
565
+ loadAdminKeys();
566
+ } catch (error) {
567
+ alert(`Failed to create key: ${error.message}`);
568
+ }
569
+ });
570
+ }
571
+
572
+ if (commandForm) {
573
+ commandForm.addEventListener('submit', async (event) => {
574
+ event.preventDefault();
575
+
576
+ const command = commandInput.value.trim();
577
+ if (!command) {
578
+ return;
579
+ }
580
+
581
+ try {
582
+ const response = await fetch(`${API_URL}/api/server/command`, {
583
+ method: 'POST',
584
+ headers: {
585
+ ...authHeaders(apiKey),
586
+ 'Content-Type': 'application/json'
587
+ },
588
+ body: JSON.stringify({ command })
589
+ });
590
+
591
+ if (!response.ok) {
592
+ const error = await response.json();
593
+ appendCommandHistory(command, 'Rejected', error.detail);
594
+ return;
595
+ }
596
+
597
+ appendCommandHistory(command, 'Sent', new Date().toLocaleString());
598
+ commandInput.value = '';
599
+ } catch (error) {
600
+ appendCommandHistory(command, 'Error', error.message);
601
+ }
602
+ });
603
+ }
604
+
605
+ window.activateKey = activateKey;
606
+ window.deactivateKey = deactivateKey;
607
+ window.deleteKey = deleteKey;
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/dashboard/public/index.html CHANGED
@@ -1,88 +1,125 @@
1
  <!DOCTYPE html>
2
- <html lang="zh-CN">
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Minecraft WebSocket API - 控制面板</title>
7
  <link rel="stylesheet" href="style.css">
8
  </head>
9
  <body>
10
  <div id="login-screen" class="screen">
11
  <div class="login-container">
12
- <h1>🎮 Minecraft WebSocket API</h1>
13
- <h2>控制面板登录</h2>
14
  <form id="login-form">
15
- <input type="password" id="super-key-input" placeholder="输入Admin Key或Server Key" required>
16
- <button type="submit">登录</button>
17
  </form>
18
  <p class="error-message" id="login-error"></p>
19
  <div class="login-info">
20
- <p><strong>密钥类型说明:</strong></p>
21
- <p>👑 <strong>Admin Key</strong> - 完全管理权限</p>
22
- <p>🖥️ <strong>Server Key</strong> - 服务器管理权限</p>
23
  </div>
24
  </div>
25
  </div>
26
 
27
- <div id="dashboard-screen" class="screen hidden">
28
  <nav class="navbar">
29
- <h1>🎮 Minecraft WebSocket API</h1>
30
- <button id="logout-btn">退出登录</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  </nav>
32
 
33
  <div class="container">
34
  <div class="stats-grid">
35
  <div class="stat-card">
36
- <h3>活跃连接</h3>
37
- <p class="stat-value" id="stat-connections">0</p>
38
  </div>
39
  <div class="stat-card">
40
- <h3>总密钥数</h3>
41
- <p class="stat-value" id="stat-total-keys">0</p>
42
  </div>
43
  <div class="stat-card">
44
- <h3>活跃SuperKeys</h3>
45
- <p class="stat-value" id="stat-super-keys">0</p>
46
  </div>
47
  <div class="stat-card">
48
- <h3>活跃普通Keys</h3>
49
- <p class="stat-value" id="stat-regular-keys">0</p>
 
 
 
 
50
  </div>
51
  </div>
52
 
53
  <div class="section">
54
- <div class="section-header">
55
- <h2>API密钥管理</h2>
56
- <button id="create-key-btn" class="btn-primary">创建新密钥</button>
57
- </div>
58
- <div id="keys-list" class="keys-list"></div>
59
  </div>
60
 
61
  <div class="section">
62
- <h2>实时事件监控</h2>
63
- <div id="events-list" class="events-list"></div>
 
 
 
 
 
 
 
 
 
64
  </div>
65
  </div>
66
  </div>
67
 
68
  <div id="create-key-modal" class="modal hidden">
69
  <div class="modal-content">
70
- <h2>创建新API密钥</h2>
71
  <form id="create-key-form">
72
- <label>名称 *</label>
73
  <input type="text" id="key-name" required>
74
-
75
- <label>描述</label>
76
  <textarea id="key-description"></textarea>
77
-
78
- <label>
79
- <input type="checkbox" id="key-is-super">
80
- 创建为SuperKey
81
- </label>
82
-
 
 
 
 
83
  <div class="modal-actions">
84
- <button type="button" id="cancel-create-btn" class="btn-secondary">取消</button>
85
- <button type="submit" class="btn-primary">创建</button>
86
  </div>
87
  </form>
88
  </div>
@@ -90,10 +127,10 @@
90
 
91
  <div id="key-details-modal" class="modal hidden">
92
  <div class="modal-content">
93
- <h2>密钥详情</h2>
94
  <div id="key-details-content"></div>
95
  <div class="modal-actions">
96
- <button id="close-details-btn" class="btn-secondary">关闭</button>
97
  </div>
98
  </div>
99
  </div>
 
1
  <!DOCTYPE html>
2
+ <html lang="en">
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Minecraft WebSocket API - Dashboard</title>
7
  <link rel="stylesheet" href="style.css">
8
  </head>
9
  <body>
10
  <div id="login-screen" class="screen">
11
  <div class="login-container">
12
+ <h1>Minecraft WebSocket API</h1>
13
+ <h2>Dashboard Login</h2>
14
  <form id="login-form">
15
+ <input type="password" id="api-key-input" placeholder="Enter Admin or Regular Key" required>
16
+ <button type="submit">Login</button>
17
  </form>
18
  <p class="error-message" id="login-error"></p>
19
  <div class="login-info">
20
+ <p><strong>Key Types</strong></p>
21
+ <p>Admin Key - full management permissions</p>
22
+ <p>Regular Key - server monitoring and command access</p>
23
  </div>
24
  </div>
25
  </div>
26
 
27
+ <div id="admin-screen" class="screen hidden">
28
  <nav class="navbar">
29
+ <h1>Admin Panel</h1>
30
+ <div class="nav-info">
31
+ <span id="admin-user-info"></span>
32
+ <button class="logout-btn">Logout</button>
33
+ </div>
34
+ </nav>
35
+
36
+ <div class="container">
37
+ <div class="section" id="admin-key-section">
38
+ <div class="section-header">
39
+ <h2>API Key Management</h2>
40
+ <button id="create-key-btn" class="btn-primary">Create Key</button>
41
+ </div>
42
+ <div id="admin-keys-list" class="keys-list"></div>
43
+ </div>
44
+ </div>
45
+ </div>
46
+
47
+ <div id="user-screen" class="screen hidden">
48
+ <nav class="navbar">
49
+ <h1>User Monitoring</h1>
50
+ <div class="nav-info">
51
+ <span id="user-info"></span>
52
+ <button class="logout-btn">Logout</button>
53
+ </div>
54
  </nav>
55
 
56
  <div class="container">
57
  <div class="stats-grid">
58
  <div class="stat-card">
59
+ <h3>Active Connections</h3>
60
+ <p class="stat-value" id="user-stat-connections">0</p>
61
  </div>
62
  <div class="stat-card">
63
+ <h3>Total Keys</h3>
64
+ <p class="stat-value" id="user-stat-total-keys">0</p>
65
  </div>
66
  <div class="stat-card">
67
+ <h3>Admin Keys</h3>
68
+ <p class="stat-value" id="user-stat-admin-keys">0</p>
69
  </div>
70
  <div class="stat-card">
71
+ <h3>Regular Keys</h3>
72
+ <p class="stat-value" id="user-stat-regular-keys">0</p>
73
+ </div>
74
+ <div class="stat-card">
75
+ <h3>Server Keys</h3>
76
+ <p class="stat-value" id="user-stat-server-keys">0</p>
77
  </div>
78
  </div>
79
 
80
  <div class="section">
81
+ <h2>Server Keys</h2>
82
+ <div id="user-keys-list" class="keys-list"></div>
 
 
 
83
  </div>
84
 
85
  <div class="section">
86
+ <h2>Command Console</h2>
87
+ <form id="command-form" class="command-form">
88
+ <input type="text" id="command-input" placeholder="Enter command" required>
89
+ <button type="submit" class="btn-primary">Send</button>
90
+ </form>
91
+ <div id="command-history" class="command-history"></div>
92
+ </div>
93
+
94
+ <div class="section">
95
+ <h2>Live Events</h2>
96
+ <div id="user-events-list" class="events-list"></div>
97
  </div>
98
  </div>
99
  </div>
100
 
101
  <div id="create-key-modal" class="modal hidden">
102
  <div class="modal-content">
103
+ <h2>Create API Key</h2>
104
  <form id="create-key-form">
105
+ <label>Name *</label>
106
  <input type="text" id="key-name" required>
107
+
108
+ <label>Description</label>
109
  <textarea id="key-description"></textarea>
110
+
111
+ <label>Key Type</label>
112
+ <select id="key-type">
113
+ <option value="regular">Regular</option>
114
+ <option value="admin">Admin</option>
115
+ </select>
116
+
117
+ <label>Server ID (optional)</label>
118
+ <input type="text" id="key-server-id">
119
+
120
  <div class="modal-actions">
121
+ <button type="button" id="cancel-create-btn" class="btn-secondary">Cancel</button>
122
+ <button type="submit" class="btn-primary">Create</button>
123
  </div>
124
  </form>
125
  </div>
 
127
 
128
  <div id="key-details-modal" class="modal hidden">
129
  <div class="modal-content">
130
+ <h2>Key Details</h2>
131
  <div id="key-details-content"></div>
132
  <div class="modal-actions">
133
+ <button id="close-details-btn" class="btn-secondary">Close</button>
134
  </div>
135
  </div>
136
  </div>
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/dashboard/public/style.css CHANGED
@@ -95,7 +95,7 @@ body {
95
  color: #667eea;
96
  }
97
 
98
- #logout-btn {
99
  padding: 10px 20px;
100
  background: #ff4444;
101
  color: white;
@@ -218,7 +218,7 @@ body {
218
  align-items: center;
219
  }
220
 
221
- .key-card.super {
222
  border-color: #764ba2;
223
  background: #f9f7fb;
224
  }
@@ -236,7 +236,7 @@ body {
236
  margin-right: 10px;
237
  }
238
 
239
- .key-badge.super {
240
  background: #764ba2;
241
  color: white;
242
  }
@@ -246,6 +246,11 @@ body {
246
  color: white;
247
  }
248
 
 
 
 
 
 
249
  .key-badge.active {
250
  background: #4caf50;
251
  color: white;
@@ -327,8 +332,13 @@ body {
327
  resize: vertical;
328
  }
329
 
330
- .modal-content input[type="checkbox"] {
331
- margin-right: 8px;
 
 
 
 
 
332
  }
333
 
334
  .modal-actions {
@@ -344,7 +354,8 @@ body {
344
  gap: 15px;
345
  }
346
 
347
- #user-info {
 
348
  font-size: 0.9em;
349
  color: #667eea;
350
  background: rgba(255,255,255,0.1);
@@ -379,6 +390,20 @@ body {
379
  padding: 10px;
380
  }
381
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
382
  .command-item {
383
  padding: 8px 0;
384
  border-bottom: 1px solid #f8f9fa;
 
95
  color: #667eea;
96
  }
97
 
98
+ .logout-btn {
99
  padding: 10px 20px;
100
  background: #ff4444;
101
  color: white;
 
218
  align-items: center;
219
  }
220
 
221
+ .key-card.admin {
222
  border-color: #764ba2;
223
  background: #f9f7fb;
224
  }
 
236
  margin-right: 10px;
237
  }
238
 
239
+ .key-badge.admin {
240
  background: #764ba2;
241
  color: white;
242
  }
 
246
  color: white;
247
  }
248
 
249
+ .key-badge.server {
250
+ background: #4f8ef7;
251
+ color: white;
252
+ }
253
+
254
  .key-badge.active {
255
  background: #4caf50;
256
  color: white;
 
332
  resize: vertical;
333
  }
334
 
335
+ .modal-content select {
336
+ width: 100%;
337
+ padding: 10px;
338
+ border: 2px solid #e0e0e0;
339
+ border-radius: 5px;
340
+ margin-bottom: 15px;
341
+ font-size: 14px;
342
  }
343
 
344
  .modal-actions {
 
354
  gap: 15px;
355
  }
356
 
357
+ #user-info,
358
+ #admin-user-info {
359
  font-size: 0.9em;
360
  color: #667eea;
361
  background: rgba(255,255,255,0.1);
 
390
  padding: 10px;
391
  }
392
 
393
+ .command-form {
394
+ display: flex;
395
+ gap: 10px;
396
+ margin-bottom: 15px;
397
+ }
398
+
399
+ .command-form input {
400
+ flex: 1;
401
+ padding: 12px;
402
+ border: 2px solid #e0e0e0;
403
+ border-radius: 5px;
404
+ font-size: 14px;
405
+ }
406
+
407
  .command-item {
408
  padding: 8px 0;
409
  border-bottom: 1px solid #f8f9fa;
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/dashboard/server.js CHANGED
@@ -3,7 +3,7 @@ const express = require('express');
3
  const path = require('path');
4
 
5
  const app = express();
6
- const PORT = parseInt(process.env.DASHBOARD_PORT || '3000');
7
 
8
  app.use(express.static(path.join(__dirname, 'public')));
9
 
@@ -13,9 +13,9 @@ app.get('/', (req, res) => {
13
 
14
  app.listen(PORT, () => {
15
  console.log('='.repeat(50));
16
- console.log('🎮 Minecraft WebSocket API - 控制面板');
17
  console.log('='.repeat(50));
18
- console.log(`控制面板地址: http://localhost:${PORT}`);
19
- console.log('请使用SuperKey登录');
20
  console.log('='.repeat(50));
21
  });
 
3
  const path = require('path');
4
 
5
  const app = express();
6
+ const PORT = parseInt(process.env.DASHBOARD_PORT || '3000', 10);
7
 
8
  app.use(express.static(path.join(__dirname, 'public')));
9
 
 
13
 
14
  app.listen(PORT, () => {
15
  console.log('='.repeat(50));
16
+ console.log('Minecraft WebSocket API - Dashboard');
17
  console.log('='.repeat(50));
18
+ console.log(`Dashboard URL: http://localhost:${PORT}`);
19
+ console.log('Use Admin or Regular Key to log in.');
20
  console.log('='.repeat(50));
21
  });
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.github/scripts/ai_agent.py CHANGED
@@ -2,9 +2,9 @@ import os
2
  import json
3
  import re
4
  from openai import OpenAI
5
- from github import Github, Auth # 导入 Auth 以修复弃用警告
6
 
7
- # --- 1. 初始化客户端 (修复 DeprecationWarning) ---
8
  auth = Auth.Token(os.getenv("GITHUB_TOKEN"))
9
  gh = Github(auth=auth)
10
  repo = gh.get_repo(os.getenv("GITHUB_REPOSITORY"))
@@ -13,82 +13,51 @@ client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"), base_url=os.getenv("OPENAI_
13
  event_data = json.loads(os.getenv("EVENT_CONTEXT"))
14
  event_name = os.getenv("EVENT_NAME")
15
 
16
- # --- 2. 定义工具 (增加 **kwargs 以忽略多余参数) ---
17
-
18
  def list_directory(path=".", **kwargs):
19
- """列出指定目录下的文件和文件夹"""
20
  try:
21
- # 基础路径安全检查
22
- if ".." in path: return "Error: Cannot access parent directory."
23
- items = os.listdir(path)
24
- return "\n".join(items)
25
- except Exception as e:
26
- return f"Error listing directory: {str(e)}"
27
 
28
  def read_file(path, **kwargs):
29
- """读取特定文件的完整内容"""
30
  try:
31
  if ".." in path: return "Error: Access denied."
32
  with open(path, 'r', encoding='utf-8') as f:
33
- return f.read()[:5000]
34
- except Exception as e:
35
- return f"Error reading file: {str(e)}"
36
 
37
  def search_keyword(keyword, path=".", **kwargs):
38
- """在当前目录及其子目录中搜索关键词"""
39
  results = []
40
- try:
41
- for root, dirs, files in os.walk(path):
42
- if ".git" in root: continue # 过滤 Git 目录
43
- for file in files:
44
- if file.endswith(('.py', '.js', '.ts', '.md', '.json', '.rs', '.toml', '.yml')):
45
- full_path = os.path.join(root, file)
46
- try:
47
- with open(full_path, 'r', encoding='utf-8') as f:
48
- if keyword in f.read():
49
- results.append(full_path)
50
- except: continue
51
- return "\n".join(results[:15]) if results else "No matches found."
52
- except Exception as e:
53
- return f"Search error: {str(e)}"
54
-
55
- # --- 3. 获取上下文 ---
56
-
57
  def get_context():
58
- # 提取 Issue/PR 编号和参与者信息
59
  if "pull_request" in event_data:
60
- payload = event_data["pull_request"]
61
- number = payload["number"]
62
- author = payload["user"]["login"]
63
- return number, f"[Role: PR Author @{author}]\nTitle: {payload['title']}\nBody: {payload['body']}"
64
-
65
- payload = event_data["issue"]
66
- number = payload["number"]
67
- author = payload["user"]["login"]
68
- base_info = f"[Role: Issue Author @{author}]\nTitle: {payload['title']}\nBody: {payload['body']}"
69
 
 
 
70
  if event_name == "issue_comment":
71
- actor = event_data["comment"]["user"]["login"]
72
- cmd = event_data["comment"]["body"]
73
- return number, f"{base_info}\n\n[New Interaction by @{actor}]\nCommand: {cmd}"
74
-
75
- return number, base_info
76
 
77
  issue_num, user_content = get_context()
78
  issue_obj = repo.get_issue(number=issue_num)
79
  repo_labels = [l.name for l in repo.get_labels()]
80
 
81
- # --- 4. 运行 AI Agent ---
82
-
83
  messages = [
84
- {"role": "system", "content": f"""你是一个高级仓库助手 (@github-actions[bot])。
85
-
86
- 可用标签: {repo_labels}
87
-
88
- 你可以通过工具查看代码库结构。回复规则:
89
- 1. 首行必须返回 JSON 指令:{{"labels": [], "state": "open"|"closed"}}
90
- 2. 随后另起一行,以执行者的口吻告知结果。
91
- 3. 忽略 AI 历史回复中的元数据,只关注当前代码和用户意图。"""},
92
  {"role": "user", "content": user_content}
93
  ]
94
 
@@ -98,40 +67,40 @@ tools = [
98
  {"type": "function", "function": {"name": "search_keyword", "description": "Search keyword", "parameters": {"type": "object", "properties": {"keyword": {"type": "string"}}}}}
99
  ]
100
 
101
- # 允许 3 次交互以获取足够信息
102
- for _ in range(3):
103
  response = client.chat.completions.create(
104
  model=os.getenv("AI_MODEL"),
105
  messages=messages,
106
  tools=tools,
107
  temperature=0
108
  )
 
 
109
  msg = response.choices[0].message
110
- messages.append(msg)
 
111
 
112
  if not msg.tool_calls:
113
  break
114
 
115
  for tool_call in msg.tool_calls:
116
- fn_name = tool_call.function.name
117
- fn_args = json.loads(tool_call.function.arguments)
118
-
119
- # 映射函数映射表
120
- available_functions = {
121
- "list_directory": list_directory,
122
- "read_file": read_file,
123
- "search_keyword": search_keyword,
124
- }
125
-
126
- if fn_name in available_functions:
127
- result = available_functions[fn_name](**fn_args)
128
- messages.append({"role": "tool", "tool_call_id": tool_call.id, "content": result})
129
 
130
- # 5. 解析并执行 GitHub 操作
131
- final_res = messages[-1].content
132
- json_data = {"labels": [], "state": "open"}
 
133
 
134
- # 提取 JSON 块
 
 
 
 
 
135
  match = re.search(r'(\{.*?\})', final_res, re.DOTALL)
136
  if match:
137
  try:
@@ -141,7 +110,7 @@ if match:
141
 
142
  if json_data.get("labels"):
143
  issue_obj.add_to_labels(*json_data["labels"])
144
- if json_data.get("state") and json_data["state"] in ["open", "closed"]:
145
  issue_obj.edit(state=json_data["state"])
146
 
147
  issue_obj.create_comment(f"### 🤖 AI Agent Execution\n\n{final_res}")
 
2
  import json
3
  import re
4
  from openai import OpenAI
5
+ from github import Github, Auth
6
 
7
+ # 初始化客户端
8
  auth = Auth.Token(os.getenv("GITHUB_TOKEN"))
9
  gh = Github(auth=auth)
10
  repo = gh.get_repo(os.getenv("GITHUB_REPOSITORY"))
 
13
  event_data = json.loads(os.getenv("EVENT_CONTEXT"))
14
  event_name = os.getenv("EVENT_NAME")
15
 
16
+ # --- 工具定义 (保持不变,增加 **kwargs 鲁棒性) ---
 
17
  def list_directory(path=".", **kwargs):
 
18
  try:
19
+ if ".." in path: return "Error: Access denied."
20
+ return "\n".join(os.listdir(path))
21
+ except Exception as e: return str(e)
 
 
 
22
 
23
  def read_file(path, **kwargs):
 
24
  try:
25
  if ".." in path: return "Error: Access denied."
26
  with open(path, 'r', encoding='utf-8') as f:
27
+ return f.read()[:5000]
28
+ except Exception as e: return str(e)
 
29
 
30
  def search_keyword(keyword, path=".", **kwargs):
 
31
  results = []
32
+ for root, _, files in os.walk(path):
33
+ if ".git" in root: continue
34
+ for file in files:
35
+ if file.endswith(('.py', '.js', '.ts', '.md', '.json', '.rs', '.yml')):
36
+ p = os.path.join(root, file)
37
+ try:
38
+ if keyword in open(p, 'r').read(): results.append(p)
39
+ except: continue
40
+ return "\n".join(results[:15]) if results else "No matches."
41
+
42
+ # --- 上下文准备 ---
 
 
 
 
 
 
43
  def get_context():
 
44
  if "pull_request" in event_data:
45
+ p = event_data["pull_request"]
46
+ return p["number"], f"PR @{p['user']['login']}\nTitle: {p['title']}\n{p['body']}"
 
 
 
 
 
 
 
47
 
48
+ i = event_data["issue"]
49
+ ctx = f"Issue @{i['user']['login']}\nTitle: {i['title']}\n{i['body']}"
50
  if event_name == "issue_comment":
51
+ ctx += f"\n\nNew Comment by @{event_data['comment']['user']['login']}: {event_data['comment']['body']}"
52
+ return i["number"], ctx
 
 
 
53
 
54
  issue_num, user_content = get_context()
55
  issue_obj = repo.get_issue(number=issue_num)
56
  repo_labels = [l.name for l in repo.get_labels()]
57
 
58
+ # --- AI 执行逻辑 ---
 
59
  messages = [
60
+ {"role": "system", "content": f"你是一个高级仓库助手。可用标签: {repo_labels}。必须首行返回JSON: {{\"labels\":[], \"state\":\"open\"}},然后解释逻辑。"},
 
 
 
 
 
 
 
61
  {"role": "user", "content": user_content}
62
  ]
63
 
 
67
  {"type": "function", "function": {"name": "search_keyword", "description": "Search keyword", "parameters": {"type": "object", "properties": {"keyword": {"type": "string"}}}}}
68
  ]
69
 
70
+ # 允许最多 5 轮工具交互
71
+ for i in range(5):
72
  response = client.chat.completions.create(
73
  model=os.getenv("AI_MODEL"),
74
  messages=messages,
75
  tools=tools,
76
  temperature=0
77
  )
78
+
79
+ # 核心修复:统一将模型返回的消息转为可序列化的字典格式
80
  msg = response.choices[0].message
81
+ msg_dict = msg.model_dump()
82
+ messages.append(msg_dict)
83
 
84
  if not msg.tool_calls:
85
  break
86
 
87
  for tool_call in msg.tool_calls:
88
+ args = json.loads(tool_call.function.arguments)
89
+ func = {"list_directory": list_directory, "read_file": read_file, "search_keyword": search_keyword}.get(tool_call.function.name)
90
+ result = func(**args) if func else "Unknown function"
91
+ messages.append({"role": "tool", "tool_call_id": tool_call.id, "content": str(result)})
 
 
 
 
 
 
 
 
 
92
 
93
+ # 确保最后一条消息是文本回复
94
+ if messages[-1].get("role") == "tool" or (messages[-1].get("tool_calls") and not messages[-1].get("content")):
95
+ final_check = client.chat.completions.create(model=os.getenv("AI_MODEL"), messages=messages)
96
+ messages.append(final_check.choices[0].message.model_dump())
97
 
98
+ # 提取最终文本内容
99
+ final_msg = messages[-1]
100
+ final_res = final_msg.get("content") or ""
101
+
102
+ # --- 结果解析与执行 ---
103
+ json_data = {"labels": [], "state": "open"}
104
  match = re.search(r'(\{.*?\})', final_res, re.DOTALL)
105
  if match:
106
  try:
 
110
 
111
  if json_data.get("labels"):
112
  issue_obj.add_to_labels(*json_data["labels"])
113
+ if json_data.get("state") in ["open", "closed"]:
114
  issue_obj.edit(state=json_data["state"])
115
 
116
  issue_obj.create_comment(f"### 🤖 AI Agent Execution\n\n{final_res}")
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.github/workflows/ai-review.yaml CHANGED
@@ -1,116 +1,46 @@
1
- import os
2
- import json
3
- import re
4
- from openai import OpenAI
5
- from github import Github, Auth
6
-
7
- # 初始化客户端
8
- auth = Auth.Token(os.getenv("GITHUB_TOKEN"))
9
- gh = Github(auth=auth)
10
- repo = gh.get_repo(os.getenv("GITHUB_REPOSITORY"))
11
- client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"), base_url=os.getenv("OPENAI_BASE_URL"))
12
-
13
- event_data = json.loads(os.getenv("EVENT_CONTEXT"))
14
- event_name = os.getenv("EVENT_NAME")
15
-
16
- # --- 工具定义 (保持不变,增加 **kwargs 鲁棒性) ---
17
- def list_directory(path=".", **kwargs):
18
- try:
19
- if ".." in path: return "Error: Access denied."
20
- return "\n".join(os.listdir(path))
21
- except Exception as e: return str(e)
22
-
23
- def read_file(path, **kwargs):
24
- try:
25
- if ".." in path: return "Error: Access denied."
26
- with open(path, 'r', encoding='utf-8') as f:
27
- return f.read()[:5000]
28
- except Exception as e: return str(e)
29
-
30
- def search_keyword(keyword, path=".", **kwargs):
31
- results = []
32
- for root, _, files in os.walk(path):
33
- if ".git" in root: continue
34
- for file in files:
35
- if file.endswith(('.py', '.js', '.ts', '.md', '.json', '.rs', '.yml')):
36
- p = os.path.join(root, file)
37
- try:
38
- if keyword in open(p, 'r').read(): results.append(p)
39
- except: continue
40
- return "\n".join(results[:15]) if results else "No matches."
41
-
42
- # --- 上下文准备 ---
43
- def get_context():
44
- if "pull_request" in event_data:
45
- p = event_data["pull_request"]
46
- return p["number"], f"PR @{p['user']['login']}\nTitle: {p['title']}\n{p['body']}"
47
-
48
- i = event_data["issue"]
49
- ctx = f"Issue @{i['user']['login']}\nTitle: {i['title']}\n{i['body']}"
50
- if event_name == "issue_comment":
51
- ctx += f"\n\nNew Comment by @{event_data['comment']['user']['login']}: {event_data['comment']['body']}"
52
- return i["number"], ctx
53
-
54
- issue_num, user_content = get_context()
55
- issue_obj = repo.get_issue(number=issue_num)
56
- repo_labels = [l.name for l in repo.get_labels()]
57
-
58
- # --- AI 执行逻辑 ---
59
- messages = [
60
- {"role": "system", "content": f"你是一个高级仓库助手。可用标签: {repo_labels}。必须首行返回JSON: {{\"labels\":[], \"state\":\"open\"}},然后解释逻辑。"},
61
- {"role": "user", "content": user_content}
62
- ]
63
-
64
- tools = [
65
- {"type": "function", "function": {"name": "list_directory", "description": "List files", "parameters": {"type": "object", "properties": {"path": {"type": "string"}}}}},
66
- {"type": "function", "function": {"name": "read_file", "description": "Read content", "parameters": {"type": "object", "properties": {"path": {"type": "string"}}}}},
67
- {"type": "function", "function": {"name": "search_keyword", "description": "Search keyword", "parameters": {"type": "object", "properties": {"keyword": {"type": "string"}}}}}
68
- ]
69
-
70
- # 允许最多 5 轮工具交互
71
- for i in range(5):
72
- response = client.chat.completions.create(
73
- model=os.getenv("AI_MODEL"),
74
- messages=messages,
75
- tools=tools,
76
- temperature=0
77
- )
78
-
79
- # 核心修复:统一将模型返回的消息转为可序列化的字典格式
80
- msg = response.choices[0].message
81
- msg_dict = msg.model_dump()
82
- messages.append(msg_dict)
83
-
84
- if not msg.tool_calls:
85
- break
86
-
87
- for tool_call in msg.tool_calls:
88
- args = json.loads(tool_call.function.arguments)
89
- func = {"list_directory": list_directory, "read_file": read_file, "search_keyword": search_keyword}.get(tool_call.function.name)
90
- result = func(**args) if func else "Unknown function"
91
- messages.append({"role": "tool", "tool_call_id": tool_call.id, "content": str(result)})
92
-
93
- # 确保最后一条消息是文本回复
94
- if messages[-1].get("role") == "tool" or (messages[-1].get("tool_calls") and not messages[-1].get("content")):
95
- final_check = client.chat.completions.create(model=os.getenv("AI_MODEL"), messages=messages)
96
- messages.append(final_check.choices[0].message.model_dump())
97
-
98
- # 提取最终文本内容
99
- final_msg = messages[-1]
100
- final_res = final_msg.get("content") or ""
101
-
102
- # --- 结果解析与执行 ---
103
- json_data = {"labels": [], "state": "open"}
104
- match = re.search(r'(\{.*?\})', final_res, re.DOTALL)
105
- if match:
106
- try:
107
- json_data = json.loads(match.group(1))
108
- final_res = final_res.replace(match.group(1), "").strip()
109
- except: pass
110
-
111
- if json_data.get("labels"):
112
- issue_obj.add_to_labels(*json_data["labels"])
113
- if json_data.get("state") in ["open", "closed"]:
114
- issue_obj.edit(state=json_data["state"])
115
-
116
- issue_obj.create_comment(f"### 🤖 AI Agent Execution\n\n{final_res}")
 
1
+ name: AI Repository Agent (Python Tools)
2
+
3
+ on:
4
+ pull_request:
5
+ types: [opened]
6
+ issues:
7
+ types: [opened]
8
+ issue_comment:
9
+ types: [created]
10
+
11
+ jobs:
12
+ ai-agent:
13
+ if: |
14
+ github.event_name == 'pull_request' ||
15
+ github.event_name == 'issues' ||
16
+ (github.event_name == 'issue_comment' && startsWith(github.event.comment.body, '[use-ai]'))
17
+ runs-on: ubuntu-latest
18
+ permissions:
19
+ contents: read
20
+ pull-requests: write
21
+ issues: write
22
+
23
+ steps:
24
+ - name: Checkout Code
25
+ uses: actions/checkout@v4
26
+ with:
27
+ fetch-depth: 0
28
+
29
+ - name: Set up Python
30
+ uses: actions/setup-python@v4
31
+ with:
32
+ python-version: '3.10'
33
+
34
+ - name: Install Dependencies
35
+ run: |
36
+ pip install openai PyGithub
37
+
38
+ - name: Run AI Agent
39
+ env:
40
+ OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
41
+ OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
42
+ AI_MODEL: ${{ secrets.AI_MODEL }}
43
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
44
+ EVENT_CONTEXT: ${{ toJson(github.event) }}
45
+ EVENT_NAME: ${{ github.event_name }}
46
+ run: python .github/scripts/ai_agent.py
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.github/workflows/ai-review.yaml CHANGED
@@ -1,46 +1,116 @@
1
- name: AI Repository Agent (Python Tools)
2
-
3
- on:
4
- pull_request:
5
- types: [opened]
6
- issues:
7
- types: [opened]
8
- issue_comment:
9
- types: [created]
10
-
11
- jobs:
12
- ai-agent:
13
- if: |
14
- github.event_name == 'pull_request' ||
15
- github.event_name == 'issues' ||
16
- (github.event_name == 'issue_comment' && startsWith(github.event.comment.body, '[use-ai]'))
17
- runs-on: ubuntu-latest
18
- permissions:
19
- contents: read
20
- pull-requests: write
21
- issues: write
22
-
23
- steps:
24
- - name: Checkout Code
25
- uses: actions/checkout@v4
26
- with:
27
- fetch-depth: 0
28
-
29
- - name: Set up Python
30
- uses: actions/setup-python@v4
31
- with:
32
- python-version: '3.10'
33
-
34
- - name: Install Dependencies
35
- run: |
36
- pip install openai PyGithub
37
-
38
- - name: Run AI Agent
39
- env:
40
- OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
41
- OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
42
- AI_MODEL: ${{ secrets.AI_MODEL }}
43
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
44
- EVENT_CONTEXT: ${{ toJson(github.event) }}
45
- EVENT_NAME: ${{ github.event_name }}
46
- run: python .github/scripts/ai_agent.py
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import re
4
+ from openai import OpenAI
5
+ from github import Github, Auth
6
+
7
+ # 初始化客户端
8
+ auth = Auth.Token(os.getenv("GITHUB_TOKEN"))
9
+ gh = Github(auth=auth)
10
+ repo = gh.get_repo(os.getenv("GITHUB_REPOSITORY"))
11
+ client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"), base_url=os.getenv("OPENAI_BASE_URL"))
12
+
13
+ event_data = json.loads(os.getenv("EVENT_CONTEXT"))
14
+ event_name = os.getenv("EVENT_NAME")
15
+
16
+ # --- 工具定义 (保持不变,增加 **kwargs 鲁棒性) ---
17
+ def list_directory(path=".", **kwargs):
18
+ try:
19
+ if ".." in path: return "Error: Access denied."
20
+ return "\n".join(os.listdir(path))
21
+ except Exception as e: return str(e)
22
+
23
+ def read_file(path, **kwargs):
24
+ try:
25
+ if ".." in path: return "Error: Access denied."
26
+ with open(path, 'r', encoding='utf-8') as f:
27
+ return f.read()[:5000]
28
+ except Exception as e: return str(e)
29
+
30
+ def search_keyword(keyword, path=".", **kwargs):
31
+ results = []
32
+ for root, _, files in os.walk(path):
33
+ if ".git" in root: continue
34
+ for file in files:
35
+ if file.endswith(('.py', '.js', '.ts', '.md', '.json', '.rs', '.yml')):
36
+ p = os.path.join(root, file)
37
+ try:
38
+ if keyword in open(p, 'r').read(): results.append(p)
39
+ except: continue
40
+ return "\n".join(results[:15]) if results else "No matches."
41
+
42
+ # --- 上下文准备 ---
43
+ def get_context():
44
+ if "pull_request" in event_data:
45
+ p = event_data["pull_request"]
46
+ return p["number"], f"PR @{p['user']['login']}\nTitle: {p['title']}\n{p['body']}"
47
+
48
+ i = event_data["issue"]
49
+ ctx = f"Issue @{i['user']['login']}\nTitle: {i['title']}\n{i['body']}"
50
+ if event_name == "issue_comment":
51
+ ctx += f"\n\nNew Comment by @{event_data['comment']['user']['login']}: {event_data['comment']['body']}"
52
+ return i["number"], ctx
53
+
54
+ issue_num, user_content = get_context()
55
+ issue_obj = repo.get_issue(number=issue_num)
56
+ repo_labels = [l.name for l in repo.get_labels()]
57
+
58
+ # --- AI 执行逻辑 ---
59
+ messages = [
60
+ {"role": "system", "content": f"你是一个高级仓库助手。可用标签: {repo_labels}。必须首行返回JSON: {{\"labels\":[], \"state\":\"open\"}},然后解释逻辑。"},
61
+ {"role": "user", "content": user_content}
62
+ ]
63
+
64
+ tools = [
65
+ {"type": "function", "function": {"name": "list_directory", "description": "List files", "parameters": {"type": "object", "properties": {"path": {"type": "string"}}}}},
66
+ {"type": "function", "function": {"name": "read_file", "description": "Read content", "parameters": {"type": "object", "properties": {"path": {"type": "string"}}}}},
67
+ {"type": "function", "function": {"name": "search_keyword", "description": "Search keyword", "parameters": {"type": "object", "properties": {"keyword": {"type": "string"}}}}}
68
+ ]
69
+
70
+ # 允许最多 5 轮工具交互
71
+ for i in range(5):
72
+ response = client.chat.completions.create(
73
+ model=os.getenv("AI_MODEL"),
74
+ messages=messages,
75
+ tools=tools,
76
+ temperature=0
77
+ )
78
+
79
+ # 核心修复:统一将模型返回的消息转为可序列化的字典格式
80
+ msg = response.choices[0].message
81
+ msg_dict = msg.model_dump()
82
+ messages.append(msg_dict)
83
+
84
+ if not msg.tool_calls:
85
+ break
86
+
87
+ for tool_call in msg.tool_calls:
88
+ args = json.loads(tool_call.function.arguments)
89
+ func = {"list_directory": list_directory, "read_file": read_file, "search_keyword": search_keyword}.get(tool_call.function.name)
90
+ result = func(**args) if func else "Unknown function"
91
+ messages.append({"role": "tool", "tool_call_id": tool_call.id, "content": str(result)})
92
+
93
+ # 确保最后一条消息是文本回复
94
+ if messages[-1].get("role") == "tool" or (messages[-1].get("tool_calls") and not messages[-1].get("content")):
95
+ final_check = client.chat.completions.create(model=os.getenv("AI_MODEL"), messages=messages)
96
+ messages.append(final_check.choices[0].message.model_dump())
97
+
98
+ # 提取最终文本内容
99
+ final_msg = messages[-1]
100
+ final_res = final_msg.get("content") or ""
101
+
102
+ # --- 结果解析与执行 ---
103
+ json_data = {"labels": [], "state": "open"}
104
+ match = re.search(r'(\{.*?\})', final_res, re.DOTALL)
105
+ if match:
106
+ try:
107
+ json_data = json.loads(match.group(1))
108
+ final_res = final_res.replace(match.group(1), "").strip()
109
+ except: pass
110
+
111
+ if json_data.get("labels"):
112
+ issue_obj.add_to_labels(*json_data["labels"])
113
+ if json_data.get("state") in ["open", "closed"]:
114
+ issue_obj.edit(state=json_data["state"])
115
+
116
+ issue_obj.create_comment(f"### 🤖 AI Agent Execution\n\n{final_res}")
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.github/scripts/ai_agent.py CHANGED
@@ -1,106 +1,110 @@
1
  import os
2
  import json
3
- import base64
4
  from openai import OpenAI
5
- from github import Github
6
 
7
- # 初始化客户端
8
- client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"), base_url=os.getenv("OPENAI_BASE_URL"))
9
- gh = Github(os.getenv("GITHUB_TOKEN"))
10
  repo = gh.get_repo(os.getenv("GITHUB_REPOSITORY"))
 
 
11
  event_data = json.loads(os.getenv("EVENT_CONTEXT"))
12
  event_name = os.getenv("EVENT_NAME")
13
 
14
- # --- 定义 AI 可调用的工具 ---
15
 
16
- def list_directory(path="."):
17
  """列出指定目录下的文件和文件夹"""
18
  try:
 
 
19
  items = os.listdir(path)
20
  return "\n".join(items)
21
  except Exception as e:
22
- return str(e)
23
 
24
- def read_file(path):
25
  """读取特定文件的完整内容"""
26
  try:
 
27
  with open(path, 'r', encoding='utf-8') as f:
28
- return f.read()[:5000] # 限制长度防止 Over-token
29
  except Exception as e:
30
- return str(e)
31
 
32
- def search_keyword(keyword, path="."):
33
  """在当前目录及其子目录中搜索关键词"""
34
  results = []
35
- for root, dirs, files in os.walk(path):
36
- for file in files:
37
- if file.endswith(('.py', '.js', '.ts', '.md', '.json', '.rs')):
38
- full_path = os.path.join(root, file)
39
- try:
40
- with open(full_path, 'r', encoding='utf-8') as f:
41
- if keyword in f.read():
42
- results.append(full_path)
43
- except:
44
- continue
45
- return "\n".join(results[:20])
46
-
47
- # --- 准备上下文与角色 ---
 
 
 
48
 
49
  def get_context():
50
- if event_name == "pull_request":
51
- number = event_data["pull_request"]["number"]
52
- author = event_data["pull_request"]["user"]["login"]
53
- title = event_data["pull_request"]["title"]
54
- body = event_data["pull_request"]["body"]
55
- return number, f"PR Author: @{author}\nTitle: {title}\nBody: {body}\n(This is a Pull Request)"
56
 
57
- number = event_data["issue"]["number"]
58
- author = event_data["issue"]["user"]["login"]
59
- title = event_data["issue"]["title"]
60
- body = event_data["issue"]["body"]
61
 
62
  if event_name == "issue_comment":
63
  actor = event_data["comment"]["user"]["login"]
64
  cmd = event_data["comment"]["body"]
65
- return number, f"Issue Author: @{author}\nTriggered by: @{actor}\nCommand: {cmd}\nContext: {title}\n{body}"
66
 
67
- return number, f"Issue Author: @{author}\nTitle: {title}\nBody: {body}"
68
 
69
  issue_num, user_content = get_context()
70
  issue_obj = repo.get_issue(number=issue_num)
71
  repo_labels = [l.name for l in repo.get_labels()]
72
 
73
- # --- 主逻辑 ---
74
 
75
  messages = [
76
  {"role": "system", "content": f"""你是一个高级仓库助手 (@github-actions[bot])。
77
- 你可以通过工具阅读代码、搜索文件并管理 Issue/PR 状态。
78
 
79
  可用标签: {repo_labels}
80
- 你的目标:
81
- 1. 理解用户意图。
82
- 2. 如果需要,使用工具查看项目结构或具体文件。
83
- 3. 给出处理方案,并直接执行(打标签、关闭等)。
84
 
85
- 输出规范:
86
- 回复开头必须是 JSON 指令: {{"labels": [], "state": "open"|"closed"}}
87
- 然后是你的执行报告。"""},
 
88
  {"role": "user", "content": user_content}
89
  ]
90
 
91
- # 工具定义
92
  tools = [
93
- {"type": "function", "function": {"name": "list_directory", "parameters": {"type": "object", "properties": {"path": {"type": "string"}}}}},
94
- {"type": "function", "function": {"name": "read_file", "parameters": {"type": "object", "properties": {"path": {"type": "string"}}}}},
95
- {"type": "function", "function": {"name": "search_keyword", "parameters": {"type": "object", "properties": {"keyword": {"type": "string"}}}}}
96
  ]
97
 
98
- # AI 决策循环 (允许最多 3 次工具调用)
99
  for _ in range(3):
100
  response = client.chat.completions.create(
101
  model=os.getenv("AI_MODEL"),
102
  messages=messages,
103
- tools=tools
 
104
  )
105
  msg = response.choices[0].message
106
  messages.append(msg)
@@ -109,32 +113,35 @@ for _ in range(3):
109
  break
110
 
111
  for tool_call in msg.tool_calls:
112
- func_name = tool_call.function.name
113
- args = json.loads(tool_call.function.arguments)
114
 
115
- if func_name == "list_directory": result = list_directory(**args)
116
- elif func_name == "read_file": result = read_file(**args)
117
- elif func_name == "search_keyword": result = search_keyword(**args)
 
 
 
118
 
119
- messages.append({"role": "tool", "tool_call_id": tool_call.id, "content": result})
120
-
121
- # 解析结果并操作 GitHub
122
- final_text = messages[-1].content
123
- json_part = {}
124
- try:
125
- if final_text.startswith("{"):
126
- import re
127
- match = re.search(r'(\{.*?\})', final_text, re.DOTALL)
128
- if match:
129
- json_part = json.loads(match.group(1))
130
- final_text = final_text.replace(match.group(1), "").strip()
131
- except:
132
- pass
133
-
134
- # 执行 GitHub 动作
135
- if json_part.get("labels"):
136
- issue_obj.add_to_labels(*json_part["labels"])
137
- if json_part.get("state"):
138
- issue_obj.edit(state=json_part["state"])
139
-
140
- issue_obj.create_comment(f"### 🤖 AI Agent Action\n\n{final_text}")
 
1
  import os
2
  import json
3
+ import re
4
  from openai import OpenAI
5
+ from github import Github, Auth # 导入 Auth 以修复弃用警告
6
 
7
+ # --- 1. 初始化客户端 (修复 DeprecationWarning) ---
8
+ auth = Auth.Token(os.getenv("GITHUB_TOKEN"))
9
+ gh = Github(auth=auth)
10
  repo = gh.get_repo(os.getenv("GITHUB_REPOSITORY"))
11
+ client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"), base_url=os.getenv("OPENAI_BASE_URL"))
12
+
13
  event_data = json.loads(os.getenv("EVENT_CONTEXT"))
14
  event_name = os.getenv("EVENT_NAME")
15
 
16
+ # --- 2. 定义工具 (增加 **kwargs 以忽略多余参数) ---
17
 
18
+ def list_directory(path=".", **kwargs):
19
  """列出指定目录下的文件和文件夹"""
20
  try:
21
+ # 基础路径安全检查
22
+ if ".." in path: return "Error: Cannot access parent directory."
23
  items = os.listdir(path)
24
  return "\n".join(items)
25
  except Exception as e:
26
+ return f"Error listing directory: {str(e)}"
27
 
28
+ def read_file(path, **kwargs):
29
  """读取特定文件的完整内容"""
30
  try:
31
+ if ".." in path: return "Error: Access denied."
32
  with open(path, 'r', encoding='utf-8') as f:
33
+ return f.read()[:5000]
34
  except Exception as e:
35
+ return f"Error reading file: {str(e)}"
36
 
37
+ def search_keyword(keyword, path=".", **kwargs):
38
  """在当前目录及其子目录中搜索关键词"""
39
  results = []
40
+ try:
41
+ for root, dirs, files in os.walk(path):
42
+ if ".git" in root: continue # 过滤 Git 目录
43
+ for file in files:
44
+ if file.endswith(('.py', '.js', '.ts', '.md', '.json', '.rs', '.toml', '.yml')):
45
+ full_path = os.path.join(root, file)
46
+ try:
47
+ with open(full_path, 'r', encoding='utf-8') as f:
48
+ if keyword in f.read():
49
+ results.append(full_path)
50
+ except: continue
51
+ return "\n".join(results[:15]) if results else "No matches found."
52
+ except Exception as e:
53
+ return f"Search error: {str(e)}"
54
+
55
+ # --- 3. 获取上下文 ---
56
 
57
  def get_context():
58
+ # 提取 Issue/PR 编号和参与者信息
59
+ if "pull_request" in event_data:
60
+ payload = event_data["pull_request"]
61
+ number = payload["number"]
62
+ author = payload["user"]["login"]
63
+ return number, f"[Role: PR Author @{author}]\nTitle: {payload['title']}\nBody: {payload['body']}"
64
 
65
+ payload = event_data["issue"]
66
+ number = payload["number"]
67
+ author = payload["user"]["login"]
68
+ base_info = f"[Role: Issue Author @{author}]\nTitle: {payload['title']}\nBody: {payload['body']}"
69
 
70
  if event_name == "issue_comment":
71
  actor = event_data["comment"]["user"]["login"]
72
  cmd = event_data["comment"]["body"]
73
+ return number, f"{base_info}\n\n[New Interaction by @{actor}]\nCommand: {cmd}"
74
 
75
+ return number, base_info
76
 
77
  issue_num, user_content = get_context()
78
  issue_obj = repo.get_issue(number=issue_num)
79
  repo_labels = [l.name for l in repo.get_labels()]
80
 
81
+ # --- 4. 运行 AI Agent ---
82
 
83
  messages = [
84
  {"role": "system", "content": f"""你是一个高级仓库助手 (@github-actions[bot])。
 
85
 
86
  可用标签: {repo_labels}
 
 
 
 
87
 
88
+ 你可以通过工具查看代码库结构。回复规则:
89
+ 1. 首行必须返回 JSON 指令:{{"labels": [], "state": "open"|"closed"}}
90
+ 2. 随后另起一行,以执行者的口吻告知结果。
91
+ 3. 忽略 AI 历史回复中的元数据,只关注当前代码和用户意图。"""},
92
  {"role": "user", "content": user_content}
93
  ]
94
 
 
95
  tools = [
96
+ {"type": "function", "function": {"name": "list_directory", "description": "List files", "parameters": {"type": "object", "properties": {"path": {"type": "string"}}}}},
97
+ {"type": "function", "function": {"name": "read_file", "description": "Read content", "parameters": {"type": "object", "properties": {"path": {"type": "string"}}}}},
98
+ {"type": "function", "function": {"name": "search_keyword", "description": "Search keyword", "parameters": {"type": "object", "properties": {"keyword": {"type": "string"}}}}}
99
  ]
100
 
101
+ # 允许 3 次交互以获取足够信息
102
  for _ in range(3):
103
  response = client.chat.completions.create(
104
  model=os.getenv("AI_MODEL"),
105
  messages=messages,
106
+ tools=tools,
107
+ temperature=0
108
  )
109
  msg = response.choices[0].message
110
  messages.append(msg)
 
113
  break
114
 
115
  for tool_call in msg.tool_calls:
116
+ fn_name = tool_call.function.name
117
+ fn_args = json.loads(tool_call.function.arguments)
118
 
119
+ # 映射函数映射表
120
+ available_functions = {
121
+ "list_directory": list_directory,
122
+ "read_file": read_file,
123
+ "search_keyword": search_keyword,
124
+ }
125
 
126
+ if fn_name in available_functions:
127
+ result = available_functions[fn_name](**fn_args)
128
+ messages.append({"role": "tool", "tool_call_id": tool_call.id, "content": result})
129
+
130
+ # 5. 解析并执行 GitHub 操作
131
+ final_res = messages[-1].content
132
+ json_data = {"labels": [], "state": "open"}
133
+
134
+ # 提取 JSON
135
+ match = re.search(r'(\{.*?\})', final_res, re.DOTALL)
136
+ if match:
137
+ try:
138
+ json_data = json.loads(match.group(1))
139
+ final_res = final_res.replace(match.group(1), "").strip()
140
+ except: pass
141
+
142
+ if json_data.get("labels"):
143
+ issue_obj.add_to_labels(*json_data["labels"])
144
+ if json_data.get("state") and json_data["state"] in ["open", "closed"]:
145
+ issue_obj.edit(state=json_data["state"])
146
+
147
+ issue_obj.create_comment(f"### 🤖 AI Agent Execution\n\n{final_res}")
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.github/workflows/ai-review.yaml CHANGED
@@ -1,4 +1,4 @@
1
- name: AI Repository Assistant
2
 
3
  on:
4
  pull_request:
@@ -9,11 +9,11 @@ on:
9
  types: [created]
10
 
11
  jobs:
12
- ai-assistant:
13
  if: |
14
  github.event_name == 'pull_request' ||
15
  github.event_name == 'issues' ||
16
- startsWith(github.event.comment.body, '[use-ai]')
17
  runs-on: ubuntu-latest
18
  permissions:
19
  contents: read
@@ -23,120 +23,24 @@ jobs:
23
  steps:
24
  - name: Checkout Code
25
  uses: actions/checkout@v4
26
-
27
- - name: Prepare Context
28
- id: prep
29
- run: |
30
- # 提取基本信息
31
- ISSUE_AUTHOR="${{ github.event.issue.user.login || github.event.pull_request.user.login }}"
32
- CURRENT_ACTOR="${{ github.actor }}"
33
-
34
- echo "### METADATA ###" > content.txt
35
- echo "Issue/PR Author: @$ISSUE_AUTHOR" >> content.txt
36
- echo "Instruction triggered by: @$CURRENT_ACTOR" >> content.txt
37
- echo "Assistant Identity: @github-actions[bot]" >> content.txt
38
- echo -e "------------------\n" >> content.txt
39
-
40
- if [ "${{ github.event_name }}" == "issue_comment" ]; then
41
- echo "### NEW COMMAND FROM @$CURRENT_ACTOR ###" >> content.txt
42
- echo "Comment: ${{ github.event.comment.body }}" >> content.txt
43
- echo -e "\n### ORIGINAL CONTEXT ###" >> content.txt
44
- echo "Title: ${{ github.event.issue.title }}" >> content.txt
45
- echo "Description: ${{ github.event.issue.body }}" >> content.txt
46
- elif [ "${{ github.event_name }}" == "pull_request" ]; then
47
- echo "### NEW PULL REQUEST FROM @$ISSUE_AUTHOR ###" >> content.txt
48
- git fetch origin ${{ github.event.pull_request.base.ref }}
49
- git diff origin/${{ github.event.pull_request.base.ref }}...HEAD > pr_diff.txt
50
- cat pr_diff.txt >> content.txt
51
- else
52
- echo "### NEW ISSUE FROM @$ISSUE_AUTHOR ###" >> content.txt
53
- echo "Title: ${{ github.event.issue.title }}" >> content.txt
54
- echo "Body: ${{ github.event.issue.body }}" >> content.txt
55
- fi
56
-
57
- echo "number=${{ github.event.issue.number || github.event.pull_request.number }}" >> $GITHUB_OUTPUT
58
- echo "type=${{ github.event_name }}" >> $GITHUB_OUTPUT
59
-
60
- - name: AI Action Execution
61
- uses: actions/github-script@v7
62
- env:
63
- API_KEY: ${{ secrets.OPENAI_API_KEY }}
64
- BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
65
- MODEL: ${{ secrets.AI_MODEL }}
66
- ISSUE_NUMBER: ${{ steps.prep.outputs.number }}
67
  with:
68
- script: |
69
- const fs = require('fs');
70
- const content = fs.readFileSync('content.txt', 'utf8');
71
-
72
- const { data: repoLabels } = await github.rest.issues.listLabelsForRepo({
73
- owner: context.repo.owner,
74
- repo: context.repo.repo,
75
- });
76
- const labelNames = repoLabels.map(l => l.name);
77
-
78
- const prompt = `你是一个拥有自主权的仓库助手 (@github-actions[bot])。
79
- 你需要根据提供的【METADATA】区分对话中的不同角色。
80
-
81
- 角色指南:
82
- 1. Issue/PR Author: 任务的发起者。
83
- 2. Instruction triggered by: 当前向你下达指令的人。如果是 Author 且内容包含 "[use-ai]",说明他在请求你介入。
84
- 3. Assistant Identity: 这是你自己。请不要审视或评价你自己的历史回复。
85
-
86
- 你的权力:
87
- - 根据 Issue 内容的质量自动打标签(可选:${labelNames.join(", ")})。
88
- - 如果内容是无意义的测试、违反规范或已解决,请直接关闭它。
89
- - 你的语气应该是果断的执行者,而不是卑微的助理。
90
-
91
- 输出要求(严格 JSON 第一行):
92
- {"labels": ["label_name"], "state": "closed" | "open"}
93
- 然后另起一行说明你的处理逻辑。`;
94
 
95
- try {
96
- const response = await fetch(`${process.env.BASE_URL}/chat/completions`, {
97
- method: 'POST',
98
- headers: { 'Authorization': `Bearer ${process.env.API_KEY}`, 'Content-Type': 'application/json' },
99
- body: JSON.stringify({
100
- model: process.env.MODEL,
101
- messages: [{ role: "user", content: prompt + "\n\n内容如下:\n" + content }],
102
- temperature: 0.1 // 降低随机性,增强逻辑判断
103
- })
104
- });
105
-
106
- const data = await response.json();
107
- const fullText = data.choices[0].message.content;
108
-
109
- const jsonMatch = fullText.match(/^\{.*?\}/);
110
- let config = { labels: [], state: "open" };
111
- let commentBody = fullText;
112
-
113
- if (jsonMatch) {
114
- config = JSON.parse(jsonMatch[0]);
115
- commentBody = fullText.replace(jsonMatch[0], "").trim();
116
- }
117
-
118
- const issueParams = {
119
- owner: context.repo.owner,
120
- repo: context.repo.repo,
121
- issue_number: parseInt(process.env.ISSUE_NUMBER)
122
- };
123
-
124
- // 执行打标签
125
- if (config.labels && config.labels.length > 0) {
126
- await github.rest.issues.addLabels({ ...issueParams, labels: config.labels });
127
- }
128
-
129
- // 执行状态变更
130
- if (config.state) {
131
- await github.rest.issues.update({ ...issueParams, state: config.state });
132
- }
133
 
134
- // 发表回复
135
- await github.rest.issues.createComment({
136
- ...issueParams,
137
- body: `### 🤖 AI Assistant Action\n\n${commentBody}`
138
- });
139
 
140
- } catch (err) {
141
- core.setFailed(err.message);
142
- }
 
 
 
 
 
 
 
1
+ name: AI Repository Agent (Python Tools)
2
 
3
  on:
4
  pull_request:
 
9
  types: [created]
10
 
11
  jobs:
12
+ ai-agent:
13
  if: |
14
  github.event_name == 'pull_request' ||
15
  github.event_name == 'issues' ||
16
+ (github.event_name == 'issue_comment' && startsWith(github.event.comment.body, '[use-ai]'))
17
  runs-on: ubuntu-latest
18
  permissions:
19
  contents: read
 
23
  steps:
24
  - name: Checkout Code
25
  uses: actions/checkout@v4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  with:
27
+ fetch-depth: 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
 
29
+ - name: Set up Python
30
+ uses: actions/setup-python@v4
31
+ with:
32
+ python-version: '3.10'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
+ - name: Install Dependencies
35
+ run: |
36
+ pip install openai PyGithub
 
 
37
 
38
+ - name: Run AI Agent
39
+ env:
40
+ OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
41
+ OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
42
+ AI_MODEL: ${{ secrets.AI_MODEL }}
43
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
44
+ EVENT_CONTEXT: ${{ toJson(github.event) }}
45
+ EVENT_NAME: ${{ github.event_name }}
46
+ run: python .github/scripts/ai_agent.py
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/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 (Role-Aware)
2
 
3
  on:
4
  pull_request:
 
1
+ name: AI Repository Assistant
2
 
3
  on:
4
  pull_request:
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.github/workflows/ai-review.yaml CHANGED
@@ -1,4 +1,4 @@
1
- name: AI Repository Assistant
2
 
3
  on:
4
  pull_request:
@@ -6,11 +6,10 @@ on:
6
  issues:
7
  types: [opened]
8
  issue_comment:
9
- types: [created] # 支持通过评论触发
10
 
11
  jobs:
12
  ai-assistant:
13
- # 仅在:1. 新开 PR/Issue 2. 评论以 [use-ai] 开头时运行
14
  if: |
15
  github.event_name == 'pull_request' ||
16
  github.event_name == 'issues' ||
@@ -28,25 +27,35 @@ jobs:
28
  - name: Prepare Context
29
  id: prep
30
  run: |
31
- # 判断是 PR 还是 Issue
32
- if [ "${{ github.event.pull_request }}" != "" ]; then
33
- EVENT_TYPE="PR"
34
- ISSUE_NUMBER="${{ github.event.pull_request.number }}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  git fetch origin ${{ github.event.pull_request.base.ref }}
36
- git diff origin/${{ github.event.pull_request.base.ref }}...HEAD > content.txt
 
37
  else
38
- EVENT_TYPE="Issue"
39
- ISSUE_NUMBER="${{ github.event.issue.number }}"
40
- # 如果是评论触发,获取评论内容作为补充指令
41
- if [ "${{ github.event_name }}" == "issue_comment" ]; then
42
- echo "Command: ${{ github.event.comment.body }}" > content.txt
43
- echo -e "\nOriginal Content:\n${{ github.event.issue.title }}\n${{ github.event.issue.body }}" >> content.txt
44
- else
45
- echo -e "Title: ${{ github.event.issue.title }}\n\n${{ github.event.issue.body }}" > content.txt
46
- fi
47
  fi
48
- echo "type=$EVENT_TYPE" >> $GITHUB_OUTPUT
49
- echo "number=$ISSUE_NUMBER" >> $GITHUB_OUTPUT
 
50
 
51
  - name: AI Action Execution
52
  uses: actions/github-script@v7
@@ -54,35 +63,34 @@ jobs:
54
  API_KEY: ${{ secrets.OPENAI_API_KEY }}
55
  BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
56
  MODEL: ${{ secrets.AI_MODEL }}
57
- EVENT_TYPE: ${{ steps.prep.outputs.type }}
58
  ISSUE_NUMBER: ${{ steps.prep.outputs.number }}
59
  with:
60
  script: |
61
  const fs = require('fs');
62
  const content = fs.readFileSync('content.txt', 'utf8');
63
 
64
- // 1. 获取现有 Labels
65
  const { data: repoLabels } = await github.rest.issues.listLabelsForRepo({
66
  owner: context.repo.owner,
67
  repo: context.repo.repo,
68
  });
69
  const labelNames = repoLabels.map(l => l.name);
70
 
71
- // 2. 角色设定:从“建议者”转变为“执行者”
72
- const prompt = `你是一个自动化的仓库助手。你被授权管理该仓库的 Issues 和 PR。
73
- 你的任务:分析内容,打上标签,并决定是否关闭或保持开启。
74
-
75
- 仓库现有标签:[${labelNames.join(", ")}]。
76
-
77
- 输出要求:
78
- 必须在回复的第一行输出 JSON 格式的操作指令:
79
- {"labels": ["label1"], "state": "closed" | "open"}
80
 
81
- 随后另起一行,以助手的身份简洁地说明你执行的操作和理由。不要说“我建议”,要说“我已执行”。
82
-
83
- 当前环境:${process.env.EVENT_TYPE}
84
- 内容详情:
85
- ${content}`;
 
 
 
 
 
 
 
 
86
 
87
  try {
88
  const response = await fetch(`${process.env.BASE_URL}/chat/completions`, {
@@ -90,15 +98,14 @@ jobs:
90
  headers: { 'Authorization': `Bearer ${process.env.API_KEY}`, 'Content-Type': 'application/json' },
91
  body: JSON.stringify({
92
  model: process.env.MODEL,
93
- messages: [{ role: "user", content: prompt }],
94
- temperature: 0.2
95
  })
96
  });
97
 
98
  const data = await response.json();
99
  const fullText = data.choices[0].message.content;
100
 
101
- // 3. 解析指令
102
  const jsonMatch = fullText.match(/^\{.*?\}/);
103
  let config = { labels: [], state: "open" };
104
  let commentBody = fullText;
@@ -114,23 +121,22 @@ jobs:
114
  issue_number: parseInt(process.env.ISSUE_NUMBER)
115
  };
116
 
117
- // 4. 执行操作:打标签
118
  if (config.labels && config.labels.length > 0) {
119
  await github.rest.issues.addLabels({ ...issueParams, labels: config.labels });
120
  }
121
 
122
- // 5. 执行操作:修改状态 (Close/Reopen)
123
  if (config.state) {
124
  await github.rest.issues.update({ ...issueParams, state: config.state });
125
  }
126
 
127
- // 6. 发布执行报告
128
  await github.rest.issues.createComment({
129
  ...issueParams,
130
  body: `### 🤖 AI Assistant Action\n\n${commentBody}`
131
  });
132
 
133
  } catch (err) {
134
- console.error(err);
135
  core.setFailed(err.message);
136
  }
 
1
+ name: AI Repository Assistant (Role-Aware)
2
 
3
  on:
4
  pull_request:
 
6
  issues:
7
  types: [opened]
8
  issue_comment:
9
+ types: [created]
10
 
11
  jobs:
12
  ai-assistant:
 
13
  if: |
14
  github.event_name == 'pull_request' ||
15
  github.event_name == 'issues' ||
 
27
  - name: Prepare Context
28
  id: prep
29
  run: |
30
+ # 提取基本信息
31
+ ISSUE_AUTHOR="${{ github.event.issue.user.login || github.event.pull_request.user.login }}"
32
+ CURRENT_ACTOR="${{ github.actor }}"
33
+
34
+ echo "### METADATA ###" > content.txt
35
+ echo "Issue/PR Author: @$ISSUE_AUTHOR" >> content.txt
36
+ echo "Instruction triggered by: @$CURRENT_ACTOR" >> content.txt
37
+ echo "Assistant Identity: @github-actions[bot]" >> content.txt
38
+ echo -e "------------------\n" >> content.txt
39
+
40
+ if [ "${{ github.event_name }}" == "issue_comment" ]; then
41
+ echo "### NEW COMMAND FROM @$CURRENT_ACTOR ###" >> content.txt
42
+ echo "Comment: ${{ github.event.comment.body }}" >> content.txt
43
+ echo -e "\n### ORIGINAL CONTEXT ###" >> content.txt
44
+ echo "Title: ${{ github.event.issue.title }}" >> content.txt
45
+ echo "Description: ${{ github.event.issue.body }}" >> content.txt
46
+ elif [ "${{ github.event_name }}" == "pull_request" ]; then
47
+ echo "### NEW PULL REQUEST FROM @$ISSUE_AUTHOR ###" >> content.txt
48
  git fetch origin ${{ github.event.pull_request.base.ref }}
49
+ git diff origin/${{ github.event.pull_request.base.ref }}...HEAD > pr_diff.txt
50
+ cat pr_diff.txt >> content.txt
51
  else
52
+ echo "### NEW ISSUE FROM @$ISSUE_AUTHOR ###" >> content.txt
53
+ echo "Title: ${{ github.event.issue.title }}" >> content.txt
54
+ echo "Body: ${{ github.event.issue.body }}" >> content.txt
 
 
 
 
 
 
55
  fi
56
+
57
+ echo "number=${{ github.event.issue.number || github.event.pull_request.number }}" >> $GITHUB_OUTPUT
58
+ echo "type=${{ github.event_name }}" >> $GITHUB_OUTPUT
59
 
60
  - name: AI Action Execution
61
  uses: actions/github-script@v7
 
63
  API_KEY: ${{ secrets.OPENAI_API_KEY }}
64
  BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
65
  MODEL: ${{ secrets.AI_MODEL }}
 
66
  ISSUE_NUMBER: ${{ steps.prep.outputs.number }}
67
  with:
68
  script: |
69
  const fs = require('fs');
70
  const content = fs.readFileSync('content.txt', 'utf8');
71
 
 
72
  const { data: repoLabels } = await github.rest.issues.listLabelsForRepo({
73
  owner: context.repo.owner,
74
  repo: context.repo.repo,
75
  });
76
  const labelNames = repoLabels.map(l => l.name);
77
 
78
+ const prompt = `你是一个拥有自主权的仓库助手 (@github-actions[bot])。
79
+ 你需要根据提供的【METADATA】区分对话中的不同角色。
 
 
 
 
 
 
 
80
 
81
+ 角���指南:
82
+ 1. Issue/PR Author: 任务的发起者。
83
+ 2. Instruction triggered by: 当前向你下达指令的人。如果是 Author 且内容包含 "[use-ai]",说明他在请求你介入。
84
+ 3. Assistant Identity: 这是你自己。请不要审视或评价你自己的历史回复。
85
+
86
+ 你的权力:
87
+ - 根据 Issue 内容的质量自动打标签(可选:${labelNames.join(", ")})。
88
+ - 如果内容是无意义的测试、违反规范或已解决,请直接关闭它。
89
+ - 你的语气应该是果断的执行者,而不是卑微的助理。
90
+
91
+ 输出要求(严格 JSON 第一行):
92
+ {"labels": ["label_name"], "state": "closed" | "open"}
93
+ 然后另起一行说明你的处理逻辑。`;
94
 
95
  try {
96
  const response = await fetch(`${process.env.BASE_URL}/chat/completions`, {
 
98
  headers: { 'Authorization': `Bearer ${process.env.API_KEY}`, 'Content-Type': 'application/json' },
99
  body: JSON.stringify({
100
  model: process.env.MODEL,
101
+ messages: [{ role: "user", content: prompt + "\n\n内容如下:\n" + content }],
102
+ temperature: 0.1 // 降低随机性,增强逻辑判断
103
  })
104
  });
105
 
106
  const data = await response.json();
107
  const fullText = data.choices[0].message.content;
108
 
 
109
  const jsonMatch = fullText.match(/^\{.*?\}/);
110
  let config = { labels: [], state: "open" };
111
  let commentBody = fullText;
 
121
  issue_number: parseInt(process.env.ISSUE_NUMBER)
122
  };
123
 
124
+ // 执行打标签
125
  if (config.labels && config.labels.length > 0) {
126
  await github.rest.issues.addLabels({ ...issueParams, labels: config.labels });
127
  }
128
 
129
+ // 执行状态变更
130
  if (config.state) {
131
  await github.rest.issues.update({ ...issueParams, state: config.state });
132
  }
133
 
134
+ // 发表回复
135
  await github.rest.issues.createComment({
136
  ...issueParams,
137
  body: `### 🤖 AI Assistant Action\n\n${commentBody}`
138
  });
139
 
140
  } catch (err) {
 
141
  core.setFailed(err.message);
142
  }
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.github/workflows/ai-review.yaml CHANGED
@@ -1,13 +1,20 @@
1
- name: AI PR & Issue Assistant with Auto-Label
2
 
3
  on:
4
  pull_request:
5
- types: [opened, synchronize]
6
  issues:
7
  types: [opened]
 
 
8
 
9
  jobs:
10
  ai-assistant:
 
 
 
 
 
11
  runs-on: ubuntu-latest
12
  permissions:
13
  contents: read
@@ -21,18 +28,27 @@ jobs:
21
  - name: Prepare Context
22
  id: prep
23
  run: |
24
- if [ "${{ github.event_name }}" == "pull_request" ]; then
25
- git fetch origin ${{ github.base_ref }}
26
- git diff origin/${{ github.base_ref }}...HEAD > input_content.txt
27
- echo "type=PR" >> $GITHUB_OUTPUT
28
- echo "number=${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT
 
29
  else
30
- echo -e "Title: ${{ github.event.issue.title }}\n\n${{ github.event.issue.body }}" > input_content.txt
31
- echo "type=Issue" >> $GITHUB_OUTPUT
32
- echo "number=${{ github.event.issue.number }}" >> $GITHUB_OUTPUT
 
 
 
 
 
 
33
  fi
 
 
34
 
35
- - name: AI Processing & Labeling
36
  uses: actions/github-script@v7
37
  env:
38
  API_KEY: ${{ secrets.OPENAI_API_KEY }}
@@ -40,42 +56,38 @@ jobs:
40
  MODEL: ${{ secrets.AI_MODEL }}
41
  EVENT_TYPE: ${{ steps.prep.outputs.type }}
42
  ISSUE_NUMBER: ${{ steps.prep.outputs.number }}
43
- ENABLE_AUTO_LABEL: "true" # 设置为 "false" 则彻底关闭自动打标签功能
44
  with:
45
  script: |
46
  const fs = require('fs');
47
- const content = fs.readFileSync('input_content.txt', 'utf8');
48
- if (!content.trim()) return;
49
 
50
- // 1. 获取仓库现有的所有 Labels
51
  const { data: repoLabels } = await github.rest.issues.listLabelsForRepo({
52
  owner: context.repo.owner,
53
  repo: context.repo.repo,
54
  });
55
  const labelNames = repoLabels.map(l => l.name);
56
 
57
- // 2. 构建针对标签的 Prompt
58
- let labelInstruction = "";
59
- if (process.env.ENABLE_AUTO_LABEL === "true") {
60
- labelInstruction = `
61
- 请从以下现有的标签列表中选择最合适的标签(可多选),仅从列表中选择:[${labelNames.join(", ")}]。
62
- 如果你认为不需要打标签,请返回空数组。
63
- 请在回复的最开头以 JSON 格式输出你的选择,格式如下:
64
- {"suggested_labels": ["label1", "label2"]}
65
- 然后另起一行开始你的评论分析。`;
66
- }
67
-
68
- const prompt = process.env.EVENT_TYPE === 'PR'
69
- ? `你是一位资深工程师。请审查以下 PR 的代码改动,指出潜在 Bug 和改进点。${labelInstruction}\n\nDiff内容:\n${content}`
70
- : `你是一位开源项目维护者。请分析以下 Issue 并给出建议。${labelInstruction}\n\nIssue内容:\n${content}`;
 
71
 
72
  try {
73
  const response = await fetch(`${process.env.BASE_URL}/chat/completions`, {
74
  method: 'POST',
75
- headers: {
76
- 'Authorization': `Bearer ${process.env.API_KEY}`,
77
- 'Content-Type': 'application/json'
78
- },
79
  body: JSON.stringify({
80
  model: process.env.MODEL,
81
  messages: [{ role: "user", content: prompt }],
@@ -86,40 +98,39 @@ jobs:
86
  const data = await response.json();
87
  const fullText = data.choices[0].message.content;
88
 
89
- // 3. 解析标签和正文
 
 
90
  let commentBody = fullText;
91
- let labelsToAdd = [];
92
-
93
- if (process.env.ENABLE_AUTO_LABEL === "true") {
94
- const jsonMatch = fullText.match(/^\{.*?\}/); // 匹配开头的 JSON
95
- if (jsonMatch) {
96
- try {
97
- const labelData = JSON.parse(jsonMatch[0]);
98
- labelsToAdd = labelData.suggested_labels || [];
99
- commentBody = fullText.replace(jsonMatch[0], "").trim();
100
- } catch (e) {
101
- console.log("Failed to parse labels JSON");
102
- }
103
- }
104
  }
105
 
106
- // 4. 发布评论
107
- await github.rest.issues.createComment({
108
  owner: context.repo.owner,
109
  repo: context.repo.repo,
110
- issue_number: parseInt(process.env.ISSUE_NUMBER),
111
- body: `### 🤖 AI ${process.env.EVENT_TYPE === 'PR' ? 'Review' : 'Assistant'}\n\n${commentBody}`
112
- });
113
 
114
- // 5. 自动打标签(如果有建议的标签)
115
- if (labelsToAdd.length > 0) {
116
- await github.rest.issues.addLabels({
117
- owner: context.repo.owner,
118
- repo: context.repo.repo,
119
- issue_number: parseInt(process.env.ISSUE_NUMBER),
120
- labels: labelsToAdd
121
- });
122
  }
 
 
 
 
 
 
 
 
 
 
 
 
123
  } catch (err) {
 
124
  core.setFailed(err.message);
125
  }
 
1
+ name: AI Repository Assistant
2
 
3
  on:
4
  pull_request:
5
+ types: [opened]
6
  issues:
7
  types: [opened]
8
+ issue_comment:
9
+ types: [created] # 支持通过评论触发
10
 
11
  jobs:
12
  ai-assistant:
13
+ # 仅在:1. 新开 PR/Issue 2. 评论以 [use-ai] 开头时运行
14
+ if: |
15
+ github.event_name == 'pull_request' ||
16
+ github.event_name == 'issues' ||
17
+ startsWith(github.event.comment.body, '[use-ai]')
18
  runs-on: ubuntu-latest
19
  permissions:
20
  contents: read
 
28
  - name: Prepare Context
29
  id: prep
30
  run: |
31
+ # 判断是 PR 还是 Issue
32
+ if [ "${{ github.event.pull_request }}" != "" ]; then
33
+ EVENT_TYPE="PR"
34
+ ISSUE_NUMBER="${{ github.event.pull_request.number }}"
35
+ git fetch origin ${{ github.event.pull_request.base.ref }}
36
+ git diff origin/${{ github.event.pull_request.base.ref }}...HEAD > content.txt
37
  else
38
+ EVENT_TYPE="Issue"
39
+ ISSUE_NUMBER="${{ github.event.issue.number }}"
40
+ # 如果是评论触发,获取评论内容作为补充指令
41
+ if [ "${{ github.event_name }}" == "issue_comment" ]; then
42
+ echo "Command: ${{ github.event.comment.body }}" > content.txt
43
+ echo -e "\nOriginal Content:\n${{ github.event.issue.title }}\n${{ github.event.issue.body }}" >> content.txt
44
+ else
45
+ echo -e "Title: ${{ github.event.issue.title }}\n\n${{ github.event.issue.body }}" > content.txt
46
+ fi
47
  fi
48
+ echo "type=$EVENT_TYPE" >> $GITHUB_OUTPUT
49
+ echo "number=$ISSUE_NUMBER" >> $GITHUB_OUTPUT
50
 
51
+ - name: AI Action Execution
52
  uses: actions/github-script@v7
53
  env:
54
  API_KEY: ${{ secrets.OPENAI_API_KEY }}
 
56
  MODEL: ${{ secrets.AI_MODEL }}
57
  EVENT_TYPE: ${{ steps.prep.outputs.type }}
58
  ISSUE_NUMBER: ${{ steps.prep.outputs.number }}
 
59
  with:
60
  script: |
61
  const fs = require('fs');
62
+ const content = fs.readFileSync('content.txt', 'utf8');
 
63
 
64
+ // 1. 获取现有 Labels
65
  const { data: repoLabels } = await github.rest.issues.listLabelsForRepo({
66
  owner: context.repo.owner,
67
  repo: context.repo.repo,
68
  });
69
  const labelNames = repoLabels.map(l => l.name);
70
 
71
+ // 2. 角色设定:从“建议者”转变为“执行者”
72
+ const prompt = `你是一个自动化的仓库助手。你被授权管理该仓库的 Issues 和 PR。
73
+ 你的任务:分析内容,打上标签,并决定是否关闭或保持开启。
74
+
75
+ 仓库现有标签:[${labelNames.join(", ")}]。
76
+
77
+ 输出要求:
78
+ 必须在回复的第一行输出 JSON 格式的操作指令:
79
+ {"labels": ["label1"], "state": "closed" | "open"}
80
+
81
+ 随后另起一行,以助手的身份简洁地说明你执行的操作和理由。不要说“我建议”,要说“我已执行”。
82
+
83
+ 当前环境:${process.env.EVENT_TYPE}
84
+ 内容详情:
85
+ ${content}`;
86
 
87
  try {
88
  const response = await fetch(`${process.env.BASE_URL}/chat/completions`, {
89
  method: 'POST',
90
+ headers: { 'Authorization': `Bearer ${process.env.API_KEY}`, 'Content-Type': 'application/json' },
 
 
 
91
  body: JSON.stringify({
92
  model: process.env.MODEL,
93
  messages: [{ role: "user", content: prompt }],
 
98
  const data = await response.json();
99
  const fullText = data.choices[0].message.content;
100
 
101
+ // 3. 解析指令
102
+ const jsonMatch = fullText.match(/^\{.*?\}/);
103
+ let config = { labels: [], state: "open" };
104
  let commentBody = fullText;
105
+
106
+ if (jsonMatch) {
107
+ config = JSON.parse(jsonMatch[0]);
108
+ commentBody = fullText.replace(jsonMatch[0], "").trim();
 
 
 
 
 
 
 
 
 
109
  }
110
 
111
+ const issueParams = {
 
112
  owner: context.repo.owner,
113
  repo: context.repo.repo,
114
+ issue_number: parseInt(process.env.ISSUE_NUMBER)
115
+ };
 
116
 
117
+ // 4. 执行操作:打标签
118
+ if (config.labels && config.labels.length > 0) {
119
+ await github.rest.issues.addLabels({ ...issueParams, labels: config.labels });
 
 
 
 
 
120
  }
121
+
122
+ // 5. 执行操作:修改状态 (Close/Reopen)
123
+ if (config.state) {
124
+ await github.rest.issues.update({ ...issueParams, state: config.state });
125
+ }
126
+
127
+ // 6. 发布执行报告
128
+ await github.rest.issues.createComment({
129
+ ...issueParams,
130
+ body: `### 🤖 AI Assistant Action\n\n${commentBody}`
131
+ });
132
+
133
  } catch (err) {
134
+ console.error(err);
135
  core.setFailed(err.message);
136
  }
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.github/workflows/ai-review.yaml CHANGED
@@ -1,10 +1,10 @@
1
- name: AI PR & Issue Helper
2
 
3
  on:
4
  pull_request:
5
  types: [opened, synchronize]
6
  issues:
7
- types: [opened] # 当新 Issue 创建时触发
8
 
9
  jobs:
10
  ai-assistant:
@@ -17,37 +17,57 @@ jobs:
17
  steps:
18
  - name: Checkout Code
19
  uses: actions/checkout@v4
20
- with:
21
- fetch-depth: 0
22
 
23
  - name: Prepare Context
24
  id: prep
25
  run: |
26
  if [ "${{ github.event_name }}" == "pull_request" ]; then
27
- git diff origin/${{ github.base_ref }}...origin/${{ github.head_ref }} > input_content.txt
 
28
  echo "type=PR" >> $GITHUB_OUTPUT
 
29
  else
30
- echo "${{ github.event.issue.title }}\n\n${{ github.event.issue.body }}" > input_content.txt
31
  echo "type=Issue" >> $GITHUB_OUTPUT
 
32
  fi
33
 
34
- - name: AI Processing
35
  uses: actions/github-script@v7
36
  env:
37
  API_KEY: ${{ secrets.OPENAI_API_KEY }}
38
  BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
39
  MODEL: ${{ secrets.AI_MODEL }}
40
  EVENT_TYPE: ${{ steps.prep.outputs.type }}
 
 
41
  with:
42
  script: |
43
  const fs = require('fs');
44
  const content = fs.readFileSync('input_content.txt', 'utf8');
45
  if (!content.trim()) return;
46
 
47
- // 根据类型定制 Prompt
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  const prompt = process.env.EVENT_TYPE === 'PR'
49
- ? `你是一位资深工程师。请审查以下 PR 的代码改动,指出潜在 Bug 和改进点:\n\n${content}`
50
- : `你是一位开源项目维护者。请分析以下 Issue 并给出建议或解决方案:\n\n${content}`;
51
 
52
  try {
53
  const response = await fetch(`${process.env.BASE_URL}/chat/completions`, {
@@ -59,27 +79,45 @@ jobs:
59
  body: JSON.stringify({
60
  model: process.env.MODEL,
61
  messages: [{ role: "user", content: prompt }],
62
- temperature: 0.3
63
  })
64
  });
65
 
66
  const data = await response.json();
67
- const result = data.choices[0].message.content;
68
- const header = process.env.EVENT_TYPE === 'PR' ? "### 🤖 AI Code Review" : "### 🤖 AI Issue Assistant";
69
 
70
- if (process.env.EVENT_TYPE === 'PR') {
71
- await github.rest.issues.createComment({
72
- owner: context.repo.owner,
73
- repo: context.repo.repo,
74
- issue_number: context.payload.pull_request.number,
75
- body: `${header}\n\n${result}`
76
- });
77
- } else {
78
- await github.rest.issues.createComment({
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  owner: context.repo.owner,
80
  repo: context.repo.repo,
81
- issue_number: context.payload.issue.number,
82
- body: `${header}\n\n${result}`
83
  });
84
  }
85
  } catch (err) {
 
1
+ name: AI PR & Issue Assistant with Auto-Label
2
 
3
  on:
4
  pull_request:
5
  types: [opened, synchronize]
6
  issues:
7
+ types: [opened]
8
 
9
  jobs:
10
  ai-assistant:
 
17
  steps:
18
  - name: Checkout Code
19
  uses: actions/checkout@v4
 
 
20
 
21
  - name: Prepare Context
22
  id: prep
23
  run: |
24
  if [ "${{ github.event_name }}" == "pull_request" ]; then
25
+ git fetch origin ${{ github.base_ref }}
26
+ git diff origin/${{ github.base_ref }}...HEAD > input_content.txt
27
  echo "type=PR" >> $GITHUB_OUTPUT
28
+ echo "number=${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT
29
  else
30
+ echo -e "Title: ${{ github.event.issue.title }}\n\n${{ github.event.issue.body }}" > input_content.txt
31
  echo "type=Issue" >> $GITHUB_OUTPUT
32
+ echo "number=${{ github.event.issue.number }}" >> $GITHUB_OUTPUT
33
  fi
34
 
35
+ - name: AI Processing & Labeling
36
  uses: actions/github-script@v7
37
  env:
38
  API_KEY: ${{ secrets.OPENAI_API_KEY }}
39
  BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
40
  MODEL: ${{ secrets.AI_MODEL }}
41
  EVENT_TYPE: ${{ steps.prep.outputs.type }}
42
+ ISSUE_NUMBER: ${{ steps.prep.outputs.number }}
43
+ ENABLE_AUTO_LABEL: "true" # 设置为 "false" 则彻底关闭自动打标签功能
44
  with:
45
  script: |
46
  const fs = require('fs');
47
  const content = fs.readFileSync('input_content.txt', 'utf8');
48
  if (!content.trim()) return;
49
 
50
+ // 1. 获取仓库现有的所有 Labels
51
+ const { data: repoLabels } = await github.rest.issues.listLabelsForRepo({
52
+ owner: context.repo.owner,
53
+ repo: context.repo.repo,
54
+ });
55
+ const labelNames = repoLabels.map(l => l.name);
56
+
57
+ // 2. 构建针对标签的 Prompt
58
+ let labelInstruction = "";
59
+ if (process.env.ENABLE_AUTO_LABEL === "true") {
60
+ labelInstruction = `
61
+ 请从以下现有的标签列表中选择最合适的标签(可多选),仅从列表中选择:[${labelNames.join(", ")}]。
62
+ 如果你认为不需要打标签,请返回空数组。
63
+ 请在回复的最开头以 JSON 格式输出你的选择,格式如下:
64
+ {"suggested_labels": ["label1", "label2"]}
65
+ 然后另起一行开始你的评论分析。`;
66
+ }
67
+
68
  const prompt = process.env.EVENT_TYPE === 'PR'
69
+ ? `你是一位资深工程师。请审查以下 PR 的代码改动,指出潜在 Bug 和改进点。${labelInstruction}\n\nDiff内容:\n${content}`
70
+ : `你是一位开源项目维护者。请分析以下 Issue 并给出建议。${labelInstruction}\n\nIssue内容:\n${content}`;
71
 
72
  try {
73
  const response = await fetch(`${process.env.BASE_URL}/chat/completions`, {
 
79
  body: JSON.stringify({
80
  model: process.env.MODEL,
81
  messages: [{ role: "user", content: prompt }],
82
+ temperature: 0.2
83
  })
84
  });
85
 
86
  const data = await response.json();
87
+ const fullText = data.choices[0].message.content;
 
88
 
89
+ // 3. 解析标签和正文
90
+ let commentBody = fullText;
91
+ let labelsToAdd = [];
92
+
93
+ if (process.env.ENABLE_AUTO_LABEL === "true") {
94
+ const jsonMatch = fullText.match(/^\{.*?\}/); // 匹配开头的 JSON
95
+ if (jsonMatch) {
96
+ try {
97
+ const labelData = JSON.parse(jsonMatch[0]);
98
+ labelsToAdd = labelData.suggested_labels || [];
99
+ commentBody = fullText.replace(jsonMatch[0], "").trim();
100
+ } catch (e) {
101
+ console.log("Failed to parse labels JSON");
102
+ }
103
+ }
104
+ }
105
+
106
+ // 4. 发布评论
107
+ await github.rest.issues.createComment({
108
+ owner: context.repo.owner,
109
+ repo: context.repo.repo,
110
+ issue_number: parseInt(process.env.ISSUE_NUMBER),
111
+ body: `### 🤖 AI ${process.env.EVENT_TYPE === 'PR' ? 'Review' : 'Assistant'}\n\n${commentBody}`
112
+ });
113
+
114
+ // 5. 自动打标签(如果有建议的标签)
115
+ if (labelsToAdd.length > 0) {
116
+ await github.rest.issues.addLabels({
117
  owner: context.repo.owner,
118
  repo: context.repo.repo,
119
+ issue_number: parseInt(process.env.ISSUE_NUMBER),
120
+ labels: labelsToAdd
121
  });
122
  }
123
  } catch (err) {
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/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