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

- 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:
ReTeamSpeak
2026-05-12 17:24:02 +09:00
parent ea08823c97
commit 2b3ae9ae15
45 changed files with 3287 additions and 446 deletions
+164 -38
View File
@@ -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">
+120
View File
@@ -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;
}
}