Files
2026-09-23 18:45:57 +08:00

132 lines
4.1 KiB
Dart

import 'dart:convert';
import 'dart:io';
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as shelf_io;
import 'package:shelf_router/shelf_router.dart';
import '../print/backend.dart';
import 'print_queue.dart';
import 'protocol.dart';
/// 协议 HTTP 服务:候选端口依次尝试绑定(与前端扫描约定一致:38200 起顺延两个)。
class AgentHttpServer {
AgentHttpServer({
required this.queue,
required this.backend,
required this.mock,
required this.manifestBackendName,
this.appVersion = '',
this.agentName = 'desktop-plugin',
});
final PrintQueue queue;
/// mock 模式下是 MockBackend,否则为系统对应后端
final PrintBackend backend;
final bool mock;
/// manifest 里的 backend 字段按操作系统报(pavo/cups),与是否 mock 无关(对齐 C# 版)
final String manifestBackendName;
/// 构建版本(打包脚本注入),随 manifest 暴露给前端核对
final String appVersion;
final String agentName;
HttpServer? _server;
int? _port;
int? get port => _port;
Future<int> start({List<int> candidatePorts = const [38200, 38201, 38202]}) async {
final handler = const Pipeline().addMiddleware(_cors()).addHandler(_router.call);
Object? lastError;
for (final port in candidatePorts) {
try {
_server = await shelf_io.serve(
handler,
InternetAddress.loopbackIPv4,
port,
poweredByHeader: null,
);
_port = port;
return port;
} on SocketException catch (e) {
lastError = e;
}
}
throw StateError('端口 ${candidatePorts.first}-${candidatePorts.last} 均被占用,启动失败:$lastError');
}
Future<void> stop() async {
await _server?.close(force: true);
_server = null;
_port = null;
}
Router get _router {
final router = Router()
..get('/manifest', (Request r) => _ok({
'plugins': [ManifestPlugin().toJson()],
'agent': agentName,
'version': appVersion,
'backend': manifestBackendName,
'mock': mock,
}))
..get('/api/card-printer/status', (Request r) async {
try {
return _ok((await backend.getStatus()).toJson());
} catch (e) {
return _ok(PrinterStatus.offline('状态读取失败:$e').toJson());
}
})
..post('/api/card-printer/print', (Request r) async {
try {
final body = await r.readAsString();
final Map<String, dynamic> json;
try {
json = jsonDecode(body) as Map<String, dynamic>;
} catch (_) {
return _err('请求体不是合法 JSON');
}
final taskId = queue.submit(PrintRequest.fromJson(json));
return _ok({'taskId': taskId});
} catch (e) {
return _err(PrintQueue.errorMessageOf(e));
}
})
..get('/api/card-printer/task/<taskId>', (Request r, String taskId) {
final task = queue.get(taskId);
return task == null ? _err('任务不存在或已过期') : _ok(task.toJson());
});
return router;
}
/// 助手只绑回环,CORS 放开 * 安全(含 OPTIONS 预检,供管理端 HTTPS 页面跨源访问)。
Middleware _cors() {
const headers = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Max-Age': '86400',
};
return (inner) => (Request req) async {
if (req.method == 'OPTIONS') {
return Response.ok('', headers: headers);
}
final res = await inner(req);
return res.change(headers: headers);
};
}
}
Response _ok(Object? data) => Response.ok(
jsonEncode({'code': 0, 'msg': 'ok', 'data': data}),
headers: {'content-type': 'application/json; charset=utf-8'},
);
Response _err(String msg) => Response.ok(
jsonEncode({'code': 1, 'msg': msg, 'data': null}),
headers: {'content-type': 'application/json; charset=utf-8'},
);