/* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD SPDX-License-Identifier: MIT */ import 'dart:convert'; import 'package:dio/dio.dart'; import 'package:flutter/widgets.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:stack_chan/model/model.dart'; import 'package:stack_chan/network/http.dart'; import 'package:stack_chan/network/urls.dart'; import 'package:stack_chan/util/mac_address_validator.dart'; import 'package:stack_chan/util/value_constant.dart'; import '../model/XiaoZhi/License.dart'; import '../model/XiaoZhi/XiaoZhi_model.dart'; import '../model/XiaoZhi/agent.dart'; import '../model/XiaoZhi/agent_create.dart'; import '../model/XiaoZhi/agent_template.dart'; import '../model/XiaoZhi/agents_devices_activate.dart'; import '../model/XiaoZhi/common_mcp_tool.dart'; import '../model/XiaoZhi/conversation.dart'; import '../model/XiaoZhi/conversation_message_data.dart'; import '../model/XiaoZhi/device.dart'; import '../model/XiaoZhi/endpoints_response.dart'; import '../model/XiaoZhi/generateLicense.dart'; import '../model/XiaoZhi/mcp_endpoints.dart'; import '../model/XiaoZhi/pagination.dart'; import '../model/XiaoZhi/product.dart'; import '../model/XiaoZhi/tts_list.dart'; class XiaoZhiUtil { static final XiaoZhiUtil shared = XiaoZhiUtil._internal(); XiaoZhiUtil._internal() { _dio.options.baseUrl = "https://XiaoZhi.me/"; _dio.options.connectTimeout = const Duration(seconds: 10); _dio.options.receiveTimeout = const Duration(seconds: 10); _dio.options.validateStatus = (state) { return state != null && state >= 200 && state < 500; }; _dio.interceptors.add( LogInterceptor(responseBody: true, logPrint: logPrint), ); _dio.interceptors.add( InterceptorsWrapper( onRequest: (options, handler) async { final token = await getToken(); if (token != null) { options.headers['Authorization'] = 'Bearer $token'; } options.headers['Accept'] = 'application/json'; options.headers['Content-Type'] = 'application/json'; handler.next(options); }, onResponse: (response, handler) async { if (response.statusCode == 401) { await _asyncPrefs.remove(_tokenKay); final newToken = await getTokenFromServer(); if (newToken != null) { //Resend request final Options newOptions = Options( method: response.requestOptions.method, headers: { ...response.requestOptions.headers, 'Authorization': 'Bearer $newToken', 'Accept': 'application/json', }, ); try { final newResponse = await _dio.request( response.requestOptions.path, options: newOptions, queryParameters: response.requestOptions.queryParameters, data: response.requestOptions.data, ); handler.resolve(newResponse); return; } catch (e) { handler.next(response); return; } } } //Handle session expiration response if (response.data != null) { try { XiaozhiResponse xiaozhiResponse = XiaozhiResponse.fromJsonT( response.data, ); if (!xiaozhiResponse.success && xiaozhiResponse.message == "Session expired or logged out") { //Refresh token final newToken = await refreshXiaoZhiToken(); if (newToken != null) { //Resend request final Options newOptions = Options( method: response.requestOptions.method, headers: { ...response.requestOptions.headers, 'Authorization': 'Bearer $newToken', 'Accept': 'application/json', }, ); try { final newResponse = await _dio.request( response.requestOptions.path, options: newOptions, queryParameters: response.requestOptions.queryParameters, data: response.requestOptions.data, ); handler.resolve(newResponse); return; } catch (e) { } } } } catch (e) { } } handler.next(response); }, ), ); } final SharedPreferencesAsync _asyncPrefs = SharedPreferencesAsync(); final String _tokenKay = "XiaoZhiToken"; final Dio _dio = Dio(); Future getToken() async { String? token = await _asyncPrefs.getString(_tokenKay); if (token == null || token.isEmpty) { return await getTokenFromServer(); } return token; } Future getTokenFromServer() async { final response = await Http.instance.get(Urls.xiaozhiToken); Model responseData = Model.fromJsonT(response.data); if (responseData.isSuccess()) { String? token = responseData.data; if (token != null) { await _asyncPrefs.setString(_tokenKay, token); return token; } } return null; } ///Refresh XiaoZhi token Future refreshXiaoZhiToken() async { final response = await Http.instance.get(Urls.xiaozhiTokenRefresh); Model responseData = Model.fromJsonT(response.data); if (responseData.isSuccess()) { String? token = responseData.data; if (token != null) { await _asyncPrefs.setString(_tokenKay, token); return token; } } return null; } ///Agent template Future> agentTemplatesList(int page, int pageSize) async { Map map = {"page": page, "pageSize": pageSize}; final response = await _dio.get( "api/developers/agent-templates/list", data: map, ); if (response.data != null) { XiaozhiResponse> xiaozhiResponse = XiaozhiResponse.fromJsonT( response.data, factory: (value) => ListData.fromJson( value, (value) => AgentTemplate.fromJson(value), ), ); if (xiaozhiResponse.success) { return xiaozhiResponse.data?.list ?? []; } } return []; } ///Query device by serial number Future serialNumberGetDevice(String serialNumber) async { final map = {'serial_number': serialNumber}; final response = await _dio.get( "api/developers/devices", queryParameters: map, ); if (response.data != null) { XiaozhiResponse> xiaozhiResponse = XiaozhiResponse.fromJsonT( response.data, factory: (value) => ListData.fromJson(value, (value) => Device.fromJson(value)), ); if (xiaozhiResponse.success) { final list = xiaozhiResponse.data?.list ?? []; //Safe access: check if list is empty first return list.isNotEmpty ? list.first : null; } } return null; } //Get device list Future> getDevice(String macAddress) async { final map = {'mac_address': MacAddressValidator.formatMac(macAddress)}; try { final response = await _dio.get( "api/developers/devices", queryParameters: map, ); if (response.data != null) { XiaozhiResponse> xiaozhiResponse = XiaozhiResponse.fromJsonT( response.data, factory: (value) => ListData.fromJson(value, (value) => Device.fromJson(value)), ); if (xiaozhiResponse.success) { final list = xiaozhiResponse.data?.list ?? []; if (list.isEmpty) { return await getCapitalLettersMacDevice(macAddress); } else { return list; } } else { throw Exception('查询设备失败'); } } return []; } catch (e) { return []; } } ///Get authorization list Future getLicenses(String serialNumber, String productId) async { final map = {"query": serialNumber}; final response = await _dio.get( "api/developers/products/$productId/licenses", queryParameters: map, ); if (response.data != null) { XiaozhiResponse xiaozhiResponse = XiaozhiResponse.fromJsonT( response.data, factory: (value) => Licenses.fromJson(value), ); if (xiaozhiResponse.success) { return xiaozhiResponse.data?.licenses.first; } } return null; } Future getProductsList() async { final response = await _dio.get("api/developers/products/list"); if (response.data != null) { XiaozhiResponse> xiaozhiResponse = XiaozhiResponse.fromJsonT( response.data, factory: (value) => ListData.fromJson(value, (value) => Product.fromJson(value)), ); if (xiaozhiResponse.success) { return xiaozhiResponse.data?.list.first; } } return null; } Future> getCapitalLettersMacDevice(String macAddress) async { final map = { 'mac_address': MacAddressValidator.formatLowerCaseMac(macAddress), }; try { final response = await _dio.get( "api/developers/devices", queryParameters: map, ); if (response.data != null) { XiaozhiResponse> xiaozhiResponse = XiaozhiResponse.fromJsonT( response.data, factory: (value) => ListData.fromJson(value, (value) => Device.fromJson(value)), ); if (xiaozhiResponse.success) { return xiaozhiResponse.data?.list ?? []; } } return []; } catch (e) { return []; } } ///Get voice list Future getTtsList() async { final response = await _dio.get("api/user/tts-list"); if (response.data != null) { XiaozhiResponse xiaozhiResponse = XiaozhiResponse.fromJsonT( response.data, factory: (value) => TTsList.fromJson(value), ); if (xiaozhiResponse.success) { return xiaozhiResponse.data; } } return null; } ///Get model list Future> getModelList() async { final response = await _dio.get("api/roles/model-list"); if (response.data != null) { XiaozhiResponse xiaozhiResponse = XiaozhiResponse.fromJsonT( response.data, factory: (value) => XiaoZhiModel.fromJson(value), ); if (xiaozhiResponse.success) { return xiaozhiResponse.data?.modelList ?? []; } } return []; } ///Get official MCP tools Future> getCommonMcpTool() async { final response = await _dio.get("api/agents/common-mcp-tool/list"); if (response.data != null) { XiaozhiResponse> xiaozhiResponse = XiaozhiResponse.fromJsonT( response.data, factory: (value) => CommonMcpTool.fromListJson(value), ); if (xiaozhiResponse.success) { return xiaozhiResponse.data ?? []; } } return []; } ///Create agent Future createAgent(AgentCreate agentParams) async { final response = await _dio.post("api/agents", data: agentParams.toJson()); if (response.data != null) { XiaozhiResponse xiaozhiResponse = XiaozhiResponse.fromJsonT( response.data, ); if (xiaozhiResponse.success) { int? id = xiaozhiResponse.data["id"] as int?; return id; } } return null; } ///Get agent list Future> getAgents({ int page = 1, int pageSize = 24, String? keyword, }) async { final Map params = { "page": page, "pageSize": pageSize, if (keyword != null && keyword.isNotEmpty) "keyword": keyword, }; final response = await _dio.get("api/agents", queryParameters: params); if (response.data != null) { XiaozhiResponse> xiaozhiResponse = XiaozhiResponse.fromJsonT( response.data, factory: (value) => Agent.fromListJson(value), ); if (xiaozhiResponse.success) { return xiaozhiResponse.data ?? []; } } return []; } ///Get agent details Future getAgentDetail(int agentId) async { final response = await _dio.get("api/agents/$agentId"); if (response.data != null) { XiaozhiResponse> xiaozhiResponse = XiaozhiResponse.fromJsonT(response.data); if (xiaozhiResponse.success && xiaozhiResponse.data != null) { if (xiaozhiResponse.data!["agent"] != null) { final agent = Agent.fromJson(xiaozhiResponse.data!["agent"]); return agent; } } } return null; } ///Update agent Future updateAgent(int agentId, AgentCreate agentParams) async { final response = await _dio.post( "api/agents/$agentId/config", data: agentParams.toJson(), ); if (response.data != null) { XiaozhiResponse xiaozhiResponse = XiaozhiResponse.fromJsonT( response.data, ); if (xiaozhiResponse.success) { return true; } } return false; } ///Bind device to agent (core: add device to specified agent Future bindDeviceToAgent(int agentId, String verificationCode) async { final response = await _dio.post( "api/agents/$agentId/devices", data: {"verificationCode": verificationCode}, ); if (response.data != null) { XiaozhiResponse xiaozhiResponse = XiaozhiResponse.fromJsonT( response.data, ); if (xiaozhiResponse.success) { return true; } } return false; } ///Unbind device Future unbindDevice(int deviceId) async { final response = await _dio.post( "api/developers/unbind-device", data: {"device_id": deviceId}, ); if (response.data != null) { XiaozhiResponse xiaozhiResponse = XiaozhiResponse.fromJsonT( response.data, ); if (xiaozhiResponse.success) { return true; } } return false; } ///Generate device authorization license ///[macAddress]: Device MAC address (replaces seed parameter) ///Returns: Authorization info (includes serial number), null on failure Future generateLicense(String macAddress) async { try { // Get generateLicenseToken License final generateResponse = await Http.instance.get( Urls.xiaozhiGenerateLicenseToken, ); if (generateResponse.data == null) { return null; } Model generateLicenseModel = Model.fromJsonT( generateResponse.data, ); if (!generateLicenseModel.isSuccess() || generateLicenseModel.data == null) { return null; } final Map queryParams = { ValueConstant.token: generateLicenseModel.data, ValueConstant.seed: macAddress, }; final response = await _dio.get( "api/developers/generate-license", queryParameters: queryParams, ); if (response.data != null) { XiaozhiResponse xiaozhiResponse = XiaozhiResponse.fromJsonT( response.data, factory: (value) => GenerateLicense.fromJson(value), ); return xiaozhiResponse.data; } return null; } catch (e) { return null; } } ///Enterprise device activation API Future agentsDevicesActivate( String serialNumber, String macAddress, ) async { final Map map = { "serial_number": serialNumber, "mac_address": macAddress, }; final response = await _dio.post("api/agents/devices/activate", data: map); if (response.statusCode == 200) { if (response.data != null) { XiaozhiResponse xiaozhiResponse = XiaozhiResponse.fromJsonT( response.data, factory: (value) => AgentsDevicesActivate.fromJson(value), ); if (xiaozhiResponse.success) { //Activation successful return true; } } } else if (response.statusCode == 400) { ///Already added XiaozhiResponse xiaozhiResponse = XiaozhiResponse.fromJsonT( response.data, ); if (xiaozhiResponse.message == "该设备已经添加过,请不要重复添加") { return true; } } return false; } ///Get MCP endpoint list Future> mcpEndpoints() async { final response = await _dio.get("api/developers/mcp-endpoints"); if (response.data != null) { XiaozhiResponse> xiaozhiResponse = XiaozhiResponse.fromJsonT( response.data, factory: (value) => McpEndpoints.fromListJson(value), ); if (xiaozhiResponse.success) { return xiaozhiResponse.data ?? []; } } return []; } ///Create mcp endpoint Future createMcpEndpoints( String name, String description, bool enabled, ) async { final Map map = { "name": name, "description": description, "enabled": enabled, }; final response = await _dio.post("api/developers/mcp-endpoints", data: map); if (response.data != null) { XiaozhiResponse res = XiaozhiResponse.fromJsonT(response.data); return res.success; } return false; } ///Get agent MCP endpoint Future generateMcpEndpointToken(int id) async { String url = "api/agents/$id/generate-mcp-endpoint-token"; final response = await _dio.post(url); if (response.data != null) { XiaozhiResponse xiaozhiResponse = XiaozhiResponse.fromJsonT( response.data, ); if (xiaozhiResponse.success) { return xiaozhiResponse.token; } } return null; } ///Get endpoint token Future getEndpointToken(int id) async { String url = "api/developers/mcp-endpoints/$id/generate-endpoint-token"; final response = await _dio.post(url); if (response.data != null) { XiaozhiResponse xiaozhiResponse = XiaozhiResponse.fromJsonT( response.data, ); if (xiaozhiResponse.success) { return xiaozhiResponse.token; } } return null; } Future endpointsList(int endpointIds) async { String url = "https://api.XiaoZhi.me/mcp/endpoints/list"; final map = {"endpoint_ids": "agent_$endpointIds"}; final response = await _dio.get(url, queryParameters: map); if (response.data != null) { EndpointsResponse data = EndpointsResponse.fromJson(response.data); return data; } return null; } ///Edit MCP endpoint information Future editEndpoints( int id, { String? name, String? description, bool? enabled, }) async { final Map map = {}; if (name != null) { map["name"] = name; } if (description != null) { map["description"] = description; } if (enabled != null) { map["enabled"] = enabled; } String url = "api/developers/mcp-endpoints/$id"; final response = await _dio.post(url, data: map); if (response.data != null) { XiaozhiResponse res = XiaozhiResponse.fromJsonT(response.data); return res.success; } return false; } ///Delete MCP endpoint Future deleteEndpoints(int id) async { String url = "api/developers/mcp-endpoints/$id"; final response = await _dio.delete(url); if (response.data != null) { XiaozhiResponse xiaozhiResponse = XiaozhiResponse.fromJsonT( response.data, ); if (xiaozhiResponse.success) { return true; } } return false; } Future> getConversationList( String startDate, int? deviceId, int? page, int? pageSize, int? agentId, ) async { final response = await _dio.get( "api/chats/list", queryParameters: { "startDate": startDate, "deviceId": deviceId, "page": page, "pageSize": pageSize, "agentId": agentId, }, ); if (response.data != null) { XiaozhiResponse> xiaozhiResponse = XiaozhiResponse.fromJsonT( response.data, factory: (value) => ListData.fromJson( value, (value) => Conversation.fromJson(value), ), ); if (xiaozhiResponse.success) { final list = xiaozhiResponse.data?.list; if (list != null) { return list; } } } return []; } ///Delete conversation Future deleteConversation(int agentId, int id) async { String url = "api/agents/$agentId/chats/$id"; final response = await _dio.delete(url); if (response.data != null) { XiaozhiResponse xiaozhiResponse = XiaozhiResponse.fromJsonT( response.data, ); if (xiaozhiResponse.success) { return true; } } return false; } ///Get message list Future> getChatsMessages( Map data, ) async { final response = await _dio.get( "api/chats/messages", queryParameters: data, ); if (response.data != null) { XiaozhiResponse> xiaozhiResponse = XiaozhiResponse.fromJsonT( response.data, factory: (value) => ListData.fromJson( value, (value) => ConversationMessageData.fromJson(value), ), ); if (xiaozhiResponse.success) { return xiaozhiResponse.data?.list ?? []; } } return []; } } class XiaozhiResponse { bool success = false; T? data; String? message; Pagination? pagination; String? token; XiaozhiResponse({ required this.success, this.data, this.message, this.pagination, this.token, }); XiaozhiResponse.fromJson( Map map, { T Function(dynamic)? factory, }) { if (map["data"] != null && map["data"] != "null" && factory != null) { data = factory(map["data"]); } else { data = map["data"]; } if (map["message"] != null && map["message"] != "null") { message = map["message"]; } if (map["success"] != null && map["success"] != "null") { success = map["success"]; } if (map["pagination"] != null && map["pagination"] != "null") { pagination = Pagination.fromJson(map["pagination"]); } if (map["token"] != null && map["token"] != "null") { token = map["token"]; } } XiaozhiResponse.fromJsonT(dynamic data, {T Function(dynamic)? factory}) : this.fromJson(data is String ? jsonDecode(data) : data, factory: factory); XiaozhiResponse.fromJsonString( String? jsonString, { T Function(dynamic)? factory, }) : this.fromJson(jsonDecode(jsonString ?? ""), factory: factory); } class ListData { List list; Pagination? pagination; //Fix constructor: Use generic list instead of fixed Device type ListData({required this.list, this.pagination}); //Generic factory method: support parsing any type list factory ListData.fromJson( Map json, T Function(Map) fromJsonT, //Type conversion function ) { return ListData( list: json['list'] != null ? List.from( (json['list'] as List).map( (x) => fromJsonT(x as Map), ), ) : [], pagination: json['pagination'] != null ? Pagination.fromJson(json['pagination'] as Map) : null, ); } //Convert to JSON: Support any generic type serialization Map toJson(dynamic Function(T) toJsonT) { final Map data = {}; data['list'] = list.map((v) => toJsonT(v)).toList(); if (pagination != null) { data['pagination'] = pagination!.toJson(); } return data; } } class Licenses { final List licenses; final Pagination? pagination; Licenses({required this.licenses, this.pagination}); factory Licenses.fromJson(Map json) { //Core fix: Correctly call License.fromJson(x) final licenseList = json['licenses'] as List? ?? []; final licenses = licenseList .map((x) => License.fromJson(x as Map)) .toList(); return Licenses( licenses: licenses, pagination: json['pagination'] != null ? Pagination.fromJson(json['pagination'] as Map) : null, ); } }