本次更新涵盖多个功能模块的优化与新增: 1. 新增Wi-Fi直连配对方式与协作画布模块 2. 完成设备管理重命名API与前端适配 3. 优化日签卡片空数据保护与UI细节 4. 新增剪贴板工具与每日运势会话 5. 修复应用锁恢复、语音消息录制等已知问题 6. 完善文件传输统计与配对逻辑 7. 更新安卓权限配置与iOS隐私描述 8. 新增自动化测试脚本与文档 9. 清理旧版审计报告与测试文件
671 lines
19 KiB
Dart
671 lines
19 KiB
Dart
// ============================================================
|
||
// 闲言APP — 信令服务器接口测试脚本
|
||
// 创建时间: 2026-05-14
|
||
// 更新时间: 2026-05-14
|
||
// 作用: 测试WebSocket信令服务器核心接口
|
||
// - 连接/注册/设备发现/消息转发/文件元数据/配对/心跳
|
||
// - 验证discoverMyDevices去重逻辑
|
||
// 上次更新: 初始版本
|
||
// 运行: dart run Scripts/signaling_test.dart
|
||
// ============================================================
|
||
|
||
import 'dart:async';
|
||
import 'dart:convert';
|
||
import 'dart:io';
|
||
|
||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||
|
||
const String kSignalingUrl = 'wss://tools.wktyl.com:9443';
|
||
const Duration kTimeout = Duration(seconds: 10);
|
||
const Duration kHeartbeatInterval = Duration(seconds: 30);
|
||
|
||
int _passCount = 0;
|
||
int _failCount = 0;
|
||
|
||
void _result(String name, bool pass, {String? detail}) {
|
||
final icon = pass ? '✅' : '❌';
|
||
final status = pass ? 'PASS' : 'FAIL';
|
||
_passCount += pass ? 1 : 0;
|
||
_failCount += pass ? 0 : 1;
|
||
print('$icon [$status] $name${detail != null ? ' — $detail' : ''}');
|
||
}
|
||
|
||
class TestDevice {
|
||
TestDevice({required this.localId, required this.alias, this.userId});
|
||
|
||
final String localId;
|
||
String? serverId;
|
||
final String alias;
|
||
final String? userId;
|
||
WebSocketChannel? _channel;
|
||
bool _isConnected = false;
|
||
bool get isConnected => _isConnected;
|
||
String get effectiveId => serverId ?? localId;
|
||
|
||
final StreamController<Map<String, dynamic>> _messageController =
|
||
StreamController<Map<String, dynamic>>.broadcast();
|
||
Stream<Map<String, dynamic>> get onMessage => _messageController.stream;
|
||
|
||
final List<Map<String, dynamic>> allMessages = [];
|
||
Timer? _heartbeatTimer;
|
||
|
||
Future<bool> connect() async {
|
||
try {
|
||
print('\n🔗 [$alias] Connecting to $kSignalingUrl...');
|
||
_channel = WebSocketChannel.connect(Uri.parse(kSignalingUrl));
|
||
await _channel!.ready.timeout(kTimeout);
|
||
|
||
_channel!.stream.listen(
|
||
(data) {
|
||
_handleMessage(data as String);
|
||
},
|
||
onDone: () {
|
||
_isConnected = false;
|
||
print('[$alias] Connection closed');
|
||
},
|
||
onError: (Object error) {
|
||
_isConnected = false;
|
||
print('[$alias] Connection error: $error');
|
||
},
|
||
);
|
||
|
||
_isConnected = true;
|
||
_sendRegister();
|
||
_startHeartbeat();
|
||
|
||
print('[$alias] Connected, waiting for server ID...');
|
||
return true;
|
||
} catch (e) {
|
||
print('[$alias] Connection failed: $e');
|
||
return false;
|
||
}
|
||
}
|
||
|
||
void _sendRegister() {
|
||
_send({
|
||
'type': 'register',
|
||
'from': localId,
|
||
'payload': {
|
||
'alias': alias,
|
||
'fingerprint': 'test-fp-${localId.substring(0, 8)}',
|
||
'deviceType': 'headless',
|
||
'deviceModel': 'SignalingTestDevice',
|
||
if (userId != null) 'userId': userId,
|
||
},
|
||
'ts': DateTime.now().millisecondsSinceEpoch,
|
||
});
|
||
}
|
||
|
||
void _startHeartbeat() {
|
||
_heartbeatTimer?.cancel();
|
||
_heartbeatTimer = Timer.periodic(kHeartbeatInterval, (_) {
|
||
if (_isConnected) {
|
||
_send({
|
||
'type': 'heartbeat',
|
||
'from': effectiveId,
|
||
'ts': DateTime.now().millisecondsSinceEpoch,
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
void _handleMessage(String data) {
|
||
try {
|
||
final json = jsonDecode(data) as Map<String, dynamic>;
|
||
allMessages.add(json);
|
||
_messageController.add(json);
|
||
|
||
final type = json['type'] as String? ?? '';
|
||
|
||
if (type == 'registered') {
|
||
final assignedId =
|
||
json['id'] as String? ??
|
||
json['payload']?['deviceId'] as String? ??
|
||
json['payload']?['id'] as String?;
|
||
if (assignedId != null && serverId == null) {
|
||
serverId = assignedId;
|
||
print('[$alias] Server assigned ID: $serverId');
|
||
}
|
||
}
|
||
|
||
if (type == 'display-name') {
|
||
final sid =
|
||
json['payload']?['id'] as String? ??
|
||
json['id'] as String? ??
|
||
json['sender'] as String?;
|
||
if (sid != null && serverId == null) {
|
||
serverId = sid;
|
||
print('[$alias] Got ID from display-name: $serverId');
|
||
}
|
||
}
|
||
} catch (_) {}
|
||
}
|
||
|
||
void _send(Map<String, dynamic> msg) {
|
||
if (!_isConnected || _channel == null) return;
|
||
_channel!.sink.add(jsonEncode(msg));
|
||
}
|
||
|
||
void sendDiscover() {
|
||
_send({
|
||
'type': 'discover',
|
||
'from': effectiveId,
|
||
'ts': DateTime.now().millisecondsSinceEpoch,
|
||
});
|
||
print('[$alias] Sent discover');
|
||
}
|
||
|
||
void sendDiscoverMyDevices(String uid) {
|
||
_send({
|
||
'type': 'discoverMyDevices',
|
||
'from': effectiveId,
|
||
'payload': {'userId': uid},
|
||
'ts': DateTime.now().millisecondsSinceEpoch,
|
||
});
|
||
print('[$alias] Sent discoverMyDevices for userId=$uid');
|
||
}
|
||
|
||
void sendTextMessage(String targetId, String text) {
|
||
_send({
|
||
'type': 'text-message',
|
||
'from': effectiveId,
|
||
'to': targetId,
|
||
'payload': {
|
||
'text': text,
|
||
'timestamp': DateTime.now().millisecondsSinceEpoch,
|
||
},
|
||
'ts': DateTime.now().millisecondsSinceEpoch,
|
||
});
|
||
print('[$alias] Sent text-message to $targetId');
|
||
}
|
||
|
||
void sendFileMeta(
|
||
String targetId, {
|
||
required String fileName,
|
||
required int fileSize,
|
||
required String mimeType,
|
||
required String taskId,
|
||
}) {
|
||
_send({
|
||
'type': 'file-meta',
|
||
'from': effectiveId,
|
||
'to': targetId,
|
||
'payload': {
|
||
'fileName': fileName,
|
||
'fileSize': fileSize,
|
||
'mimeType': mimeType,
|
||
'taskId': taskId,
|
||
},
|
||
'ts': DateTime.now().millisecondsSinceEpoch,
|
||
});
|
||
print('[$alias] Sent file-meta to $targetId: $fileName');
|
||
}
|
||
|
||
void sendPairRequest(String targetId) {
|
||
_send({
|
||
'type': 'pair-request',
|
||
'from': effectiveId,
|
||
'to': targetId,
|
||
'payload': {'pin': '123456'},
|
||
'ts': DateTime.now().millisecondsSinceEpoch,
|
||
});
|
||
print('[$alias] Sent pair-request to $targetId');
|
||
}
|
||
|
||
void sendPairResponse(String targetId, bool accepted) {
|
||
_send({
|
||
'type': 'pair-response',
|
||
'from': effectiveId,
|
||
'to': targetId,
|
||
'payload': {'accepted': accepted},
|
||
'ts': DateTime.now().millisecondsSinceEpoch,
|
||
});
|
||
print('[$alias] Sent pair-response to $targetId: accepted=$accepted');
|
||
}
|
||
|
||
void sendHeartbeat() {
|
||
_send({
|
||
'type': 'heartbeat',
|
||
'from': effectiveId,
|
||
'ts': DateTime.now().millisecondsSinceEpoch,
|
||
});
|
||
print('[$alias] Sent heartbeat');
|
||
}
|
||
|
||
Future<Map<String, dynamic>?> waitForMessage(
|
||
String type, {
|
||
Duration timeout = const Duration(seconds: 8),
|
||
}) async {
|
||
try {
|
||
return await onMessage
|
||
.firstWhere((m) => m['type'] == type)
|
||
.timeout(timeout);
|
||
} catch (_) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
Future<void> disconnect() async {
|
||
_heartbeatTimer?.cancel();
|
||
if (_isConnected && _channel != null) {
|
||
_send({
|
||
'type': 'leave',
|
||
'from': effectiveId,
|
||
'ts': DateTime.now().millisecondsSinceEpoch,
|
||
});
|
||
await _channel?.sink.close();
|
||
}
|
||
_isConnected = false;
|
||
await _messageController.close();
|
||
}
|
||
}
|
||
|
||
Future<void> main() async {
|
||
print('╔══════════════════════════════════════════════════════════╗');
|
||
print('║ 闲言APP — 信令服务器接口测试 ║');
|
||
print('║ Signaling Server: $kSignalingUrl ║');
|
||
print('╚══════════════════════════════════════════════════════════╝\n');
|
||
|
||
await testWebSocketConnection();
|
||
await testDeviceRegister();
|
||
await testDiscoverMyDevicesDedup();
|
||
await testTextMessageForwarding();
|
||
await testFileMetaForwarding();
|
||
await testPairRequestResponse();
|
||
await testHeartbeat();
|
||
|
||
print('\n╔══════════════════════════════════════════════════════════╗');
|
||
print('║ 测试结果汇总 ║');
|
||
print('╠══════════════════════════════════════════════════════════╣');
|
||
print('║ ✅ 通过: $_passCount ║');
|
||
print('║ ❌ 失败: $_failCount ║');
|
||
print('║ 📊 总计: ${_passCount + _failCount} ║');
|
||
print('╚══════════════════════════════════════════════════════════╝');
|
||
|
||
exit(_failCount > 0 ? 1 : 0);
|
||
}
|
||
|
||
Future<void> testWebSocketConnection() async {
|
||
print('\n━━━ 1. WebSocket 连接测试 ━━━');
|
||
|
||
final device = TestDevice(
|
||
localId: 'conn-test-${DateTime.now().millisecondsSinceEpoch}',
|
||
alias: 'ConnTest',
|
||
);
|
||
|
||
final connected = await device.connect();
|
||
_result('WebSocket连接建立', connected);
|
||
|
||
if (connected) {
|
||
final registered = await device.waitForMessage('registered');
|
||
_result(
|
||
'收到registered响应',
|
||
registered != null,
|
||
detail: registered != null
|
||
? 'serverId=${registered['id'] ?? registered['payload']?['deviceId']}'
|
||
: '未收到registered消息',
|
||
);
|
||
}
|
||
|
||
await device.disconnect();
|
||
await Future<void>.delayed(const Duration(seconds: 1));
|
||
}
|
||
|
||
Future<void> testDeviceRegister() async {
|
||
print('\n━━━ 2. 设备注册测试 ━━━');
|
||
|
||
final device = TestDevice(
|
||
localId: 'reg-test-${DateTime.now().millisecondsSinceEpoch}',
|
||
alias: 'RegisterTest',
|
||
userId: 'test_user_register',
|
||
);
|
||
|
||
final connected = await device.connect();
|
||
_result('注册设备连接', connected);
|
||
|
||
if (connected) {
|
||
final registered = await device.waitForMessage('registered');
|
||
_result(
|
||
'注册成功收到确认',
|
||
registered != null,
|
||
detail: registered != null
|
||
? 'payload=${registered['payload']}'
|
||
: '超时未收到',
|
||
);
|
||
|
||
final hasServerId = device.serverId != null;
|
||
_result('服务器分配ID', hasServerId, detail: 'serverId=${device.serverId}');
|
||
}
|
||
|
||
await device.disconnect();
|
||
await Future<void>.delayed(const Duration(seconds: 1));
|
||
}
|
||
|
||
Future<void> testDiscoverMyDevicesDedup() async {
|
||
print('\n━━━ 3. 设备发现测试(discoverMyDevices 去重验证) ━━━');
|
||
|
||
final testUserId = 'dedup_test_user_${DateTime.now().millisecondsSinceEpoch}';
|
||
|
||
final device1 = TestDevice(
|
||
localId: 'dedup-a-${DateTime.now().millisecondsSinceEpoch}',
|
||
alias: 'DedupDevice-A',
|
||
userId: testUserId,
|
||
);
|
||
final device2 = TestDevice(
|
||
localId: 'dedup-b-${DateTime.now().millisecondsSinceEpoch}',
|
||
alias: 'DedupDevice-B',
|
||
userId: testUserId,
|
||
);
|
||
|
||
final c1 = await device1.connect();
|
||
final c2 = await device2.connect();
|
||
_result('两台设备均连接成功', c1 && c2);
|
||
|
||
if (!c1 || !c2) {
|
||
await device1.disconnect();
|
||
await device2.disconnect();
|
||
return;
|
||
}
|
||
|
||
await Future<void>.delayed(const Duration(seconds: 2));
|
||
|
||
final discoverer = TestDevice(
|
||
localId: 'dedup-disc-${DateTime.now().millisecondsSinceEpoch}',
|
||
alias: 'Discoverer',
|
||
userId: testUserId,
|
||
);
|
||
final c3 = await discoverer.connect();
|
||
_result('发现者设备连接', c3);
|
||
|
||
if (!c3) {
|
||
await device1.disconnect();
|
||
await device2.disconnect();
|
||
return;
|
||
}
|
||
|
||
await Future<void>.delayed(const Duration(seconds: 1));
|
||
|
||
discoverer.sendDiscoverMyDevices(testUserId);
|
||
|
||
final response = await discoverer.waitForMessage('myDevicesResponse');
|
||
_result(
|
||
'收到myDevicesResponse',
|
||
response != null,
|
||
detail: response != null ? 'raw=${jsonEncode(response)}' : '超时未收到',
|
||
);
|
||
|
||
if (response != null) {
|
||
final devicesList =
|
||
(response['devices'] as List<dynamic>?) ??
|
||
(response['payload']?['devices'] as List<dynamic>?) ??
|
||
[];
|
||
_result(
|
||
'发现设备数量≥1',
|
||
devicesList.isNotEmpty,
|
||
detail: '发现${devicesList.length}台设备',
|
||
);
|
||
|
||
final fingerprints = <String>{};
|
||
var duplicateCount = 0;
|
||
for (final d in devicesList) {
|
||
if (d is Map<String, dynamic>) {
|
||
final fp =
|
||
d['fingerprint'] as String? ?? d['id'] as String? ?? '';
|
||
if (fp.isNotEmpty) {
|
||
if (fingerprints.contains(fp)) {
|
||
duplicateCount++;
|
||
print(' ⚠️ 发现重复设备: fingerprint=$fp');
|
||
}
|
||
fingerprints.add(fp);
|
||
}
|
||
}
|
||
}
|
||
_result(
|
||
'去重验证: 无重复设备',
|
||
duplicateCount == 0,
|
||
detail: duplicateCount > 0
|
||
? '发现$duplicateCount个重复项'
|
||
: '所有设备fingerprint唯一',
|
||
);
|
||
}
|
||
|
||
await device1.disconnect();
|
||
await device2.disconnect();
|
||
await discoverer.disconnect();
|
||
await Future<void>.delayed(const Duration(seconds: 1));
|
||
}
|
||
|
||
Future<void> testTextMessageForwarding() async {
|
||
print('\n━━━ 4. 文本消息转发测试 ━━━');
|
||
|
||
final sender = TestDevice(
|
||
localId: 'txt-snd-${DateTime.now().millisecondsSinceEpoch}',
|
||
alias: 'TextSender',
|
||
);
|
||
final receiver = TestDevice(
|
||
localId: 'txt-rcv-${DateTime.now().millisecondsSinceEpoch}',
|
||
alias: 'TextReceiver',
|
||
);
|
||
|
||
final c1 = await sender.connect();
|
||
final c2 = await receiver.connect();
|
||
_result('发送方和接收方连接', c1 && c2);
|
||
|
||
if (!c1 || !c2) {
|
||
await sender.disconnect();
|
||
await receiver.disconnect();
|
||
return;
|
||
}
|
||
|
||
await Future<void>.delayed(const Duration(seconds: 2));
|
||
|
||
final targetId = receiver.serverId ?? receiver.localId;
|
||
final testText = 'Hello from signaling_test at ${DateTime.now()}';
|
||
|
||
sender.sendTextMessage(targetId, testText);
|
||
|
||
final received = await receiver.waitForMessage('text-message');
|
||
_result(
|
||
'接收方收到text-message',
|
||
received != null,
|
||
detail: received != null
|
||
? 'from=${received['from']}, text=${received['payload']?['text']}'
|
||
: '超时未收到',
|
||
);
|
||
|
||
if (received != null) {
|
||
final receivedText = received['payload']?['text'] as String? ?? '';
|
||
_result(
|
||
'消息内容匹配',
|
||
receivedText == testText,
|
||
detail: receivedText == testText
|
||
? '内容一致'
|
||
: '不匹配: 期望"$testText", 实际"$receivedText"',
|
||
);
|
||
}
|
||
|
||
await sender.disconnect();
|
||
await receiver.disconnect();
|
||
await Future<void>.delayed(const Duration(seconds: 1));
|
||
}
|
||
|
||
Future<void> testFileMetaForwarding() async {
|
||
print('\n━━━ 5. 文件元数据转发测试 ━━━');
|
||
|
||
final sender = TestDevice(
|
||
localId: 'file-snd-${DateTime.now().millisecondsSinceEpoch}',
|
||
alias: 'FileSender',
|
||
);
|
||
final receiver = TestDevice(
|
||
localId: 'file-rcv-${DateTime.now().millisecondsSinceEpoch}',
|
||
alias: 'FileReceiver',
|
||
);
|
||
|
||
final c1 = await sender.connect();
|
||
final c2 = await receiver.connect();
|
||
_result('文件发送方和接收方连接', c1 && c2);
|
||
|
||
if (!c1 || !c2) {
|
||
await sender.disconnect();
|
||
await receiver.disconnect();
|
||
return;
|
||
}
|
||
|
||
await Future<void>.delayed(const Duration(seconds: 2));
|
||
|
||
final targetId = receiver.serverId ?? receiver.localId;
|
||
const testFileName = 'test_document.pdf';
|
||
const testFileSize = 1048576;
|
||
const testMimeType = 'application/pdf';
|
||
const testTaskId = 'task-file-meta-test-001';
|
||
|
||
sender.sendFileMeta(
|
||
targetId,
|
||
fileName: testFileName,
|
||
fileSize: testFileSize,
|
||
mimeType: testMimeType,
|
||
taskId: testTaskId,
|
||
);
|
||
|
||
final received = await receiver.waitForMessage('file-meta');
|
||
_result(
|
||
'接收方收到file-meta',
|
||
received != null,
|
||
detail: received != null
|
||
? 'from=${received['from']}, fileName=${received['payload']?['fileName']}'
|
||
: '超时未收到',
|
||
);
|
||
|
||
if (received != null) {
|
||
final payload = received['payload'] as Map<String, dynamic>? ?? {};
|
||
_result(
|
||
'文件名匹配',
|
||
payload['fileName'] == testFileName,
|
||
detail: '期望"$testFileName", 实际"${payload['fileName']}"',
|
||
);
|
||
_result(
|
||
'文件大小匹配',
|
||
payload['fileSize'] == testFileSize,
|
||
detail: '期望$testFileSize, 实际${payload['fileSize']}',
|
||
);
|
||
_result(
|
||
'MIME类型匹配',
|
||
payload['mimeType'] == testMimeType,
|
||
detail: '期望"$testMimeType", 实际"${payload['mimeType']}"',
|
||
);
|
||
_result(
|
||
'任务ID匹配',
|
||
payload['taskId'] == testTaskId,
|
||
detail: '期望"$testTaskId", 实际"${payload['taskId']}"',
|
||
);
|
||
}
|
||
|
||
await sender.disconnect();
|
||
await receiver.disconnect();
|
||
await Future<void>.delayed(const Duration(seconds: 1));
|
||
}
|
||
|
||
Future<void> testPairRequestResponse() async {
|
||
print('\n━━━ 6. 配对请求/接受测试 ━━━');
|
||
|
||
final deviceA = TestDevice(
|
||
localId: 'pair-a-${DateTime.now().millisecondsSinceEpoch}',
|
||
alias: 'PairDevice-A',
|
||
);
|
||
final deviceB = TestDevice(
|
||
localId: 'pair-b-${DateTime.now().millisecondsSinceEpoch}',
|
||
alias: 'PairDevice-B',
|
||
);
|
||
|
||
final c1 = await deviceA.connect();
|
||
final c2 = await deviceB.connect();
|
||
_result('配对双方连接', c1 && c2);
|
||
|
||
if (!c1 || !c2) {
|
||
await deviceA.disconnect();
|
||
await deviceB.disconnect();
|
||
return;
|
||
}
|
||
|
||
await Future<void>.delayed(const Duration(seconds: 2));
|
||
|
||
final targetB = deviceB.serverId ?? deviceB.localId;
|
||
deviceA.sendPairRequest(targetB);
|
||
|
||
final pairReq = await deviceB.waitForMessage('pair-request');
|
||
_result(
|
||
'B收到配对请求',
|
||
pairReq != null,
|
||
detail: pairReq != null
|
||
? 'from=${pairReq['from']}, pin=${pairReq['payload']?['pin']}'
|
||
: '超时未收到',
|
||
);
|
||
|
||
if (pairReq != null) {
|
||
final targetA = deviceA.serverId ?? deviceA.localId;
|
||
deviceB.sendPairResponse(targetA, true);
|
||
|
||
final pairResp = await deviceA.waitForMessage('pair-response');
|
||
_result(
|
||
'A收到配对响应',
|
||
pairResp != null,
|
||
detail: pairResp != null
|
||
? 'accepted=${pairResp['payload']?['accepted']}'
|
||
: '超时未收到',
|
||
);
|
||
|
||
if (pairResp != null) {
|
||
final accepted = pairResp['payload']?['accepted'] as bool? ?? false;
|
||
_result('配对已接受', accepted, detail: 'accepted=$accepted');
|
||
}
|
||
}
|
||
|
||
await deviceA.disconnect();
|
||
await deviceB.disconnect();
|
||
await Future<void>.delayed(const Duration(seconds: 1));
|
||
}
|
||
|
||
Future<void> testHeartbeat() async {
|
||
print('\n━━━ 7. 心跳测试 ━━━');
|
||
|
||
final device = TestDevice(
|
||
localId: 'hb-test-${DateTime.now().millisecondsSinceEpoch}',
|
||
alias: 'HeartbeatTest',
|
||
);
|
||
|
||
final connected = await device.connect();
|
||
_result('心跳测试设备连接', connected);
|
||
|
||
if (!connected) {
|
||
await device.disconnect();
|
||
return;
|
||
}
|
||
|
||
await Future<void>.delayed(const Duration(seconds: 1));
|
||
|
||
device.sendHeartbeat();
|
||
print(' 📤 已发送心跳包, 等待服务器响应...');
|
||
|
||
await Future<void>.delayed(const Duration(seconds: 3));
|
||
|
||
final stillConnected = device.isConnected;
|
||
_result(
|
||
'心跳后连接仍保持',
|
||
stillConnected,
|
||
detail: stillConnected ? '连接正常' : '连接已断开',
|
||
);
|
||
|
||
final pingReceived = await device.waitForMessage('ping',
|
||
timeout: const Duration(seconds: 5));
|
||
_result(
|
||
'收到服务器ping',
|
||
pingReceived != null,
|
||
detail: pingReceived != null
|
||
? '服务器主动ping, 连接保活正常'
|
||
: '未收到ping(可能服务器不主动ping, 连接仍正常)',
|
||
);
|
||
|
||
await device.disconnect();
|
||
await Future<void>.delayed(const Duration(seconds: 1));
|
||
}
|