feat: 饭卡打印助手 Flutter 版(Phase 1/2:协议服务器 + macOS CUPS 真机打印)
- 协议与 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 已注册
This commit is contained in:
commit
689a07ca44
46 files changed
+2986
No files matched your search
@@ -0,0 +1,126 @@
|
||||
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'},
|
||||
);
|
||||
Reference in new issue
Block a user