feat: implement TS3 protocol layer, session management, and ServerQuery client
CI/CD / Test (ubuntu-latest) (push) Successful in 2m3s
CI/CD / Build Frontend (push) Failing after 11s
CI/CD / Test (macos-latest) (push) Has been cancelled
CI/CD / Test (windows-latest) (push) Has been cancelled
CI/CD / Build Desktop (linux) (push) Has been cancelled
CI/CD / Build Desktop (macos) (push) Has been cancelled
CI/CD / Build Desktop (windows) (push) Has been cancelled
CI/CD / Release (push) Has been cancelled
CI/CD / Test (ubuntu-latest) (push) Successful in 2m3s
CI/CD / Build Frontend (push) Failing after 11s
CI/CD / Test (macos-latest) (push) Has been cancelled
CI/CD / Test (windows-latest) (push) Has been cancelled
CI/CD / Build Desktop (linux) (push) Has been cancelled
CI/CD / Build Desktop (macos) (push) Has been cancelled
CI/CD / Build Desktop (windows) (push) Has been cancelled
CI/CD / Release (push) Has been cancelled
- Add real X25519 ECDH key exchange for initivexpand2 bootstrap - Add P-256 identity key generation with TeamSpeak tomcrypt format - Add session event loop with tokio::select! for packet/command handling - Add ServerQuery TCP client with typed parsing (channels, clients, permissions) - Add Tauri commands: connect (session-based), join_channel, send_message, disconnect - Add frontend ServerQuery snapshot panel - Fix CI: Node 22, rustup setup, npm install, workspace checks - Add mock UDP handshake tests through initserver - 54 tests passing across shared, tscore, tsaudio, tsdb
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
|
||||
interface Identity {
|
||||
@@ -18,6 +18,33 @@ interface Bookmark {
|
||||
last_connected: string | null;
|
||||
}
|
||||
|
||||
interface ServerQueryChannel {
|
||||
id: number;
|
||||
name: string;
|
||||
total_clients: number;
|
||||
}
|
||||
|
||||
interface ServerQueryClient {
|
||||
id: number;
|
||||
database_id: number;
|
||||
nickname: string;
|
||||
}
|
||||
|
||||
interface ServerQueryServerInfo {
|
||||
name: string;
|
||||
platform: string;
|
||||
version: string;
|
||||
max_clients: number;
|
||||
clients_online: number;
|
||||
}
|
||||
|
||||
interface ServerQuerySnapshot {
|
||||
server: ServerQueryServerInfo | null;
|
||||
channels: ServerQueryChannel[];
|
||||
clients: ServerQueryClient[];
|
||||
permissions: unknown[];
|
||||
}
|
||||
|
||||
function App() {
|
||||
const [identities, setIdentities] = useState<Identity[]>([]);
|
||||
const [bookmarks, setBookmarks] = useState<Bookmark[]>([]);
|
||||
@@ -25,12 +52,21 @@ function App() {
|
||||
const [nickname, setNickname] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [connected, setConnected] = useState(false);
|
||||
const [queryPort, setQueryPort] = useState(10011);
|
||||
const [querySnapshot, setQuerySnapshot] = useState<ServerQuerySnapshot | null>(null);
|
||||
const [queryLoading, setQueryLoading] = useState(false);
|
||||
const [queryError, setQueryError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadIdentities();
|
||||
loadBookmarks();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setQuerySnapshot(null);
|
||||
setQueryError(null);
|
||||
}, [selectedBookmark]);
|
||||
|
||||
async function loadIdentities() {
|
||||
try {
|
||||
const result = await invoke<Identity[]>('get_identities');
|
||||
@@ -74,6 +110,31 @@ function App() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleLoadServerQuery() {
|
||||
if (!selectedBookmark || queryLoading) return;
|
||||
|
||||
setQueryLoading(true);
|
||||
setQueryError(null);
|
||||
try {
|
||||
const snapshot = await invoke<ServerQuerySnapshot>('server_query_snapshot', {
|
||||
request: {
|
||||
address: selectedBookmark.address,
|
||||
port: queryPort,
|
||||
username: null,
|
||||
password: null,
|
||||
virtual_server_id: null,
|
||||
include_permissions: false,
|
||||
},
|
||||
});
|
||||
setQuerySnapshot(snapshot);
|
||||
} catch (error) {
|
||||
setQueryError(String(error));
|
||||
setQuerySnapshot(null);
|
||||
} finally {
|
||||
setQueryLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
<header className="app-header">
|
||||
@@ -91,6 +152,7 @@ function App() {
|
||||
<aside className="sidebar">
|
||||
<section className="bookmarks-section">
|
||||
<h2>服务器书签</h2>
|
||||
<div className="identity-summary">身份数量:{identities.length}</div>
|
||||
<ul className="bookmark-list">
|
||||
{bookmarks.map((bookmark) => (
|
||||
<li
|
||||
@@ -108,45 +170,109 @@ function App() {
|
||||
|
||||
<div className="content">
|
||||
{selectedBookmark ? (
|
||||
<div className="connect-form">
|
||||
<h2>连接到 {selectedBookmark.name}</h2>
|
||||
<div className="form-group">
|
||||
<label>服务器地址</label>
|
||||
<input
|
||||
type="text"
|
||||
value={`${selectedBookmark.address}:${selectedBookmark.port}`}
|
||||
disabled
|
||||
/>
|
||||
<div className="server-panel">
|
||||
<div className="connect-form">
|
||||
<h2>连接到 {selectedBookmark.name}</h2>
|
||||
<div className="form-group">
|
||||
<label>服务器地址</label>
|
||||
<input
|
||||
type="text"
|
||||
value={`${selectedBookmark.address}:${selectedBookmark.port}`}
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>昵称</label>
|
||||
<input
|
||||
type="text"
|
||||
value={nickname}
|
||||
onChange={(e) => setNickname(e.target.value)}
|
||||
placeholder={selectedBookmark.nickname || '请输入昵称'}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>密码</label>
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="可选"
|
||||
/>
|
||||
</div>
|
||||
<div className="form-actions">
|
||||
{connected ? (
|
||||
<button className="disconnect-btn" onClick={handleDisconnect}>
|
||||
断开连接
|
||||
</button>
|
||||
) : (
|
||||
<button className="connect-btn" onClick={handleConnect}>
|
||||
连接
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>昵称</label>
|
||||
<input
|
||||
type="text"
|
||||
value={nickname}
|
||||
onChange={(e) => setNickname(e.target.value)}
|
||||
placeholder={selectedBookmark.nickname || '请输入昵称'}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>密码</label>
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="可选"
|
||||
/>
|
||||
</div>
|
||||
<div className="form-actions">
|
||||
{connected ? (
|
||||
<button className="disconnect-btn" onClick={handleDisconnect}>
|
||||
断开连接
|
||||
</button>
|
||||
) : (
|
||||
<button className="connect-btn" onClick={handleConnect}>
|
||||
连接
|
||||
</button>
|
||||
|
||||
<section className="query-panel">
|
||||
<div className="query-header">
|
||||
<div>
|
||||
<h2>ServerQuery 快照</h2>
|
||||
<p>读取公开 ServerQuery 信息,默认端口通常是 10011。</p>
|
||||
</div>
|
||||
<div className="query-actions">
|
||||
<input
|
||||
type="number"
|
||||
value={queryPort}
|
||||
min={1}
|
||||
max={65535}
|
||||
onChange={(e) => setQueryPort(Number(e.target.value))}
|
||||
aria-label="ServerQuery port"
|
||||
/>
|
||||
<button className="connect-btn" onClick={handleLoadServerQuery} disabled={queryLoading}>
|
||||
{queryLoading ? '读取中...' : '读取快照'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{queryError && <div className="query-error">{queryError}</div>}
|
||||
|
||||
{querySnapshot && (
|
||||
<div className="query-grid">
|
||||
<div className="query-card">
|
||||
<h3>{querySnapshot.server?.name || '服务器'}</h3>
|
||||
<p>{querySnapshot.server?.platform || '未知平台'}</p>
|
||||
<p>{querySnapshot.server?.version || '未知版本'}</p>
|
||||
<strong>
|
||||
{querySnapshot.server?.clients_online ?? querySnapshot.clients.length}/
|
||||
{querySnapshot.server?.max_clients ?? '-'} 在线
|
||||
</strong>
|
||||
</div>
|
||||
|
||||
<div className="query-card">
|
||||
<h3>频道</h3>
|
||||
<ul className="query-list">
|
||||
{querySnapshot.channels.map((channel) => (
|
||||
<li key={channel.id}>
|
||||
<span>{channel.name}</span>
|
||||
<small>{channel.total_clients} 人</small>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="query-card">
|
||||
<h3>客户端</h3>
|
||||
<ul className="query-list">
|
||||
{querySnapshot.clients.map((client) => (
|
||||
<li key={client.id}>
|
||||
<span>{client.nickname}</span>
|
||||
<small>#{client.id}</small>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
) : (
|
||||
<div className="welcome">
|
||||
|
||||
@@ -92,6 +92,12 @@ body {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.identity-summary {
|
||||
margin-bottom: 12px;
|
||||
color: var(--text-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.bookmark-list {
|
||||
list-style: none;
|
||||
}
|
||||
@@ -134,6 +140,13 @@ body {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.server-panel {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(320px, 400px) minmax(0, 1fr);
|
||||
gap: 24px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.connect-form {
|
||||
max-width: 400px;
|
||||
}
|
||||
@@ -201,6 +214,11 @@ body {
|
||||
background-color: var(--primary-dark);
|
||||
}
|
||||
|
||||
.connect-btn:disabled {
|
||||
opacity: 0.65;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.disconnect-btn {
|
||||
background-color: var(--error-color);
|
||||
color: white;
|
||||
@@ -229,3 +247,105 @@ body {
|
||||
font-size: 16px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.query-panel {
|
||||
padding: 20px;
|
||||
background-color: var(--surface-color);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.query-header {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.query-header h2 {
|
||||
font-size: 18px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.query-header p {
|
||||
color: var(--text-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.query-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.query-actions input {
|
||||
width: 96px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.query-error {
|
||||
padding: 10px 12px;
|
||||
margin-bottom: 16px;
|
||||
color: var(--error-color);
|
||||
background-color: #ffebee;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.query-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.query-card {
|
||||
min-width: 0;
|
||||
padding: 14px;
|
||||
background-color: var(--background-color);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.query-card h3 {
|
||||
margin-bottom: 8px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.query-card p,
|
||||
.query-card small {
|
||||
color: var(--text-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.query-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
max-height: 220px;
|
||||
overflow-y: auto;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.query-list li {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.query-list span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.server-panel,
|
||||
.query-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.query-header {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user