- 协议与 C# 版 card-printer-agent 逐字段对齐(38200-38202 顺延、CORS *、串行任务队列、30 分钟保留) - macOS CUPS 后端:lpoptions 队列发现 + lp 参数列表提交(LC_ALL=C 防本地化粘连坑) - --mock 模拟模式 + bin/agent.dart headless 入口 + 测试卡生成工具 - 真机验收:HiTi CS-200e status 就绪 + 真实出卡;macOS 通用二进制构建通过 - App Sandbox 已关闭(服务需绑回环端口并调用 lp 子进程);zaxprint URL scheme 已注册
127 lines
3.9 KiB
Dart
127 lines
3.9 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.agentName = 'desktop-plugin',
|
|
});
|
|
|
|
final PrintQueue queue;
|
|
|
|
/// mock 模式下是 MockBackend,否则为系统对应后端
|
|
final PrintBackend backend;
|
|
final bool mock;
|
|
|
|
/// manifest 里的 backend 字段按操作系统报(pavo/cups),与是否 mock 无关(对齐 C# 版)
|
|
final String manifestBackendName;
|
|
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,
|
|
'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'},
|
|
);
|