feat: 母艦サーバー HTTP 実装を簡素化 (UUID ライブラリ不要・環境変数から読み込む)

This commit is contained in:
joe 2026-03-06 12:17:42 +09:00
parent 485c4c232f
commit 0ac6edac9f

291
bin/mothership_server.dart Normal file
View file

@ -0,0 +1,291 @@
// Version: 1.0.0
import 'dart:async';
import 'dart:convert';
import 'dart:io';
///
///
/// 1
///
///
/// `.env` 使
/// - MOTHERSHIP_HOST: localhost / 0.0.0.0
/// - MOTHERSHIP_PORT: 8787
/// - MOTHERSHIP_API_KEY: API
/// - MOTHERSHIP_DATA_DIR:
class MothershipServer {
//
final statusFile = File('data/mothership/status.json');
//
Map<String, ClientData> _clientStatuses = {};
//
List<ChatMessage> _chatQueue = [];
///
Future<void> start() async {
//
final dataDir = Directory(args['MOTHERSHIP_DATA_DIR'] ?? 'data/mothership');
await dataDir.create(recursive: true);
//
_chatQueue = [];
print('=== 母艦「お局様」サーバー起動 ===');
print('ホスト:${args['MOTHERSHIP_HOST'] ?? '0.0.0.0'}:${args['MOTHERSHIP_PORT'] ?? '8787'}');
print('API キー:${args['MOTHERSHIP_API_KEY']}');
print('データディレクトリ:${dataDir.path}');
// HTTP dart:io httpServer 使
final server = await HttpServer.bind(
args['MOTHERSHIP_HOST'] ?? '0.0.0.0',
int.parse(args['MOTHERSHIP_PORT'] ?? '8787'),
);
print('サーバー起動完了。HTTP リッスン開始');
//
await for (final request in server.request) {
final response = switch (request.url.path) {
'/health' => _handleHealth(request),
'/status' => _handleStatus(request),
'/sync/heartbeat' => _handleHeartbeat(request),
'/chat/send' => _handleChatSend(request),
'/chat/pending' => _handleChatPending(request),
'/chat/ack' => _handleChatAck(request),
'/backup/drive' => _handleBackupDrive(request),
_ => _handleNotFound(request),
};
await request.response.write(response);
}
}
/// GET /health
String _handleHealth(HttpRequest request) {
return jsonEncode({
'status': 'ok',
'timestamp': DateTime.now().toUtc().toIso8601String(),
'clients_connected': _clientStatuses.length,
});
}
/// GET /status
String _handleStatus(HttpRequest request) {
final clients = <String, Map<String, dynamic>>{};
_clientStatuses.forEach((clientId, clientData) {
clients[clientId] = {
'id': clientData.id,
'last_heartbeat': clientData.lastHeartbeat?.toIso8601String(),
'hash_chain_root': clientData.hashChainRoot,
'expiry_remaining_days': clientData.expiryRemainingDays,
};
});
return jsonEncode({
'server_time': DateTime.now().toUtc().toIso8601String(),
'clients': clients,
});
}
/// POST /sync/heartbeat
Future<String> _handleHeartbeat(HttpRequest request) async {
final body = jsonDecode(utf8.decode(request.requestedBytes)) as Map<String, dynamic>;
final apiKey = body['api_key'] as String?;
if (apiKey != args['MOTHERSHIP_API_KEY']) {
return jsonEncode({
'error': '未認証',
'status_code': 401,
});
}
final clientId = body['client_id'] as String?;
if (clientId == null) {
return jsonEncode({'error': 'client_id が不足'});
}
final now = DateTime.now().toUtc();
_clientStatuses[clientId] = ClientData(
id: clientId,
lastHeartbeat: now,
hashChainRoot: body['hash_chain_root'] as String?,
expiryRemainingDays: body['expiry_remaining_days'] ?? 90,
);
// JSON
await _persistStatus();
return jsonEncode({
'status': 'success',
'client_id': clientId,
'last_heartbeat': now.toIso8601String(),
'expiry_warning': (_clientStatuses[clientId]?.expiryRemainingDays ?? 90) <= 30,
});
}
/// POST /chat/send
String _handleChatSend(HttpRequest request) {
final body = jsonDecode(utf8.decode(request.requestedBytes)) as Map<String, dynamic>;
final apiKey = body['api_key'] as String?;
if (apiKey != args['MOTHERSHIP_API_KEY']) {
return jsonEncode({
'error': '未認証',
'status_code': 401,
});
}
final clientMessage = ChatMessage(
fromClient: body['client_id'] as String?,
message: body['message'] as String,
priority: body['priority'] as int? ?? 0,
);
_chatQueue.add(clientMessage);
return jsonEncode({
'status': 'queued',
'queue_length': _chatQueue.length,
});
}
/// GET /chat/pending
String _handleChatPending(HttpRequest request) {
//
final clientQueues = <String, List<ChatMessage>>{};
for (final message in _chatQueue) {
if (message.fromClient != null) {
clientQueues.putIfAbsent(message.fromClient!, () => []).add(message);
} else {
//
final pending = clientQueues.values.fold([], (acc, curr) => [...acc, ...curr]);
if (pending.isNotEmpty) {
clientQueues['all_clients'] = pending;
}
}
}
return jsonEncode({'queues': clientQueues});
}
/// POST /chat/ack
String _handleChatAck(HttpRequest request) {
final body = jsonDecode(utf8.decode(request.requestedBytes)) as Map<String, dynamic>;
final apiKey = body['api_key'] as String?;
if (apiKey != args['MOTHERSHIP_API_KEY']) {
return jsonEncode({'error': '未認証'});
}
final acknowledgedIds = <String>[];
_chatQueue.removeWhere((msg) => msg.id == body['message_id']);
for (final msg in _chatQueue) {
acknowledgedIds.add(msg.id);
}
return jsonEncode({
'status': 'acknowledged',
'acknowledged_count': acknowledgedIds.length,
});
}
/// POST /backup/drive
String _handleBackupDrive(HttpRequest request) {
final apiKey = body['api_key'] as String?;
if (apiKey != args['MOTHERSHIP_API_KEY']) {
return jsonEncode({'error': '未認証'});
}
// Drive API OAuth2
return jsonEncode({
'status': 'backup_ready',
'drive_quota_gb': 15,
'used_space_gb': 0.5,
});
}
///
String _handleNotFound(HttpRequest request) {
return jsonEncode({
'error': 'Not Found',
'path': request.url.path,
'method': request.method,
}, statusCode: HttpStatus.notFound);
}
/// JSON
Future<void> _persistStatus() async {
final now = DateTime.now().toUtc();
final data = {
'server_time': now.toIso8601String(),
'clients': {},
};
_clientStatuses.forEach((id, client) {
data['clients'][id] = {
'last_heartbeat': client.lastHeartbeat?.toIso8601String(),
'hash_chain_root': client.hashChainRoot,
'expiry_remaining_days': client.expiryRemainingDays,
};
});
final file = File(statusFile.path);
await file.writeAsString(jsonEncode(data));
}
}
///
class ClientData {
final String id;
final DateTime? lastHeartbeat;
final String? hashChainRoot;
final int expiryRemainingDays;
ClientData({
required this.id,
this.lastHeartbeat,
this.hashChainRoot,
required this.expiryRemainingDays,
});
}
///
class ChatMessage {
final String? fromClient;
final String message;
final int priority;
final String id; //
ChatMessage({
this.fromClient,
required this.message,
this.priority = 0,
String? id,
}) : id = id ?? const String.fromCharCodes([0x4a, 0x61, 0x70, 0x2d, DateTime.now().millisecondsSinceEpoch % 1000]);
}
void main(List<String> args) async {
//
final host = Platform.environment['MOTHERSHIP_HOST'] ?? '0.0.0.0';
final port = Platform.environment['MOTHERSHIP_PORT'] ?? '8787';
final apiKey = Platform.environment['MOTHERSHIP_API_KEY'] ?? 'TEST_MOTHERSHIP_KEY';
final dataDir = Platform.environment['MOTHERSHIP_DATA_DIR'] ?? 'data/mothership';
print('環境変数取得完了:');
print(' HOST: $host');
print(' PORT: $port');
print(' API_KEY: ${apiKey?.substring(0, min(apiKey!.length, 10))}...');
final server = MothershipServer();
await server.start();
}
/// min
int min(int a, int b) => a < b ? a : b;