Files
xianyan/lib/features/source/services/url_analyzer_service.dart
Developer 7a6d555e4c chore: 清理临时文件与工作流文档,完善句子来源功能
1. 删除本地临时调试文件、工作流模板、闲置脚本与脑暴记录
2. 新增极简在线记事本API部署文件
3. 修复Flutter端拾光Sheet布局与状态更新问题
4. 完善句子来源后端API与前端导入逻辑
5. 修复macOS平台secure存储插件引用
2026-06-11 04:39:16 +08:00

247 lines
7.4 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================================
// 闲言APP — URL分析服务
// 创建时间: 2026-06-10
// 更新时间: 2026-06-10
// 作用: 请求URL并分析数据格式、字段结构、响应时间等
// 上次更新: 初始创建
// ============================================================
import 'package:dio/dio.dart';
import '../models/custom_channel.dart';
/// URL分析结果
class UrlAnalysisResult {
final DataFormat format;
final String method;
final String responseFormat;
final int count;
final int latencyMs;
final List<String> fields;
final List<Map<String, dynamic>> sampleData;
final Map<String, String> fieldSuggestions;
final bool isAccessible;
final String? errorMessage;
const UrlAnalysisResult({
required this.format,
this.method = 'GET',
this.responseFormat = 'JSON',
this.count = 0,
this.latencyMs = 0,
this.fields = const [],
this.sampleData = const [],
this.fieldSuggestions = const {},
this.isAccessible = true,
this.errorMessage,
});
}
/// URL分析服务
class UrlAnalyzerService {
static final UrlAnalyzerService _instance = UrlAnalyzerService._();
factory UrlAnalyzerService() => _instance;
UrlAnalyzerService._();
final Dio _dio = Dio(BaseOptions(
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 15),
headers: {'User-Agent': 'XianYanAPP/1.0'},
));
/// 分析URL
Future<UrlAnalysisResult> analyze(String url) async {
final stopwatch = Stopwatch()..start();
try {
final response = await _dio.get<dynamic>(url);
stopwatch.stop();
final latencyMs = stopwatch.elapsedMilliseconds;
final data = response.data;
if (data is! Map && data is! List) {
return UrlAnalysisResult(
format: DataFormat.custom,
latencyMs: stopwatch.elapsedMilliseconds,
isAccessible: false,
errorMessage: '返回数据不是有效的JSON格式',
);
}
// 取样本数据
final Map<String, dynamic> sampleItem;
final int count;
if (data is List && data.isNotEmpty) {
sampleItem = (data.first is Map<String, dynamic>)
? data.first as Map<String, dynamic>
: <String, dynamic>{};
count = data.length;
} else if (data is Map<String, dynamic>) {
sampleItem = data;
count = 1;
} else {
sampleItem = <String, dynamic>{};
count = 0;
}
// 检测格式
final format = _detectFormat(sampleItem);
// 提取字段
final fields = sampleItem.keys.toList();
// 样本数据最多5条
final sampleData = <Map<String, dynamic>>[];
if (data is List) {
for (int i = 0; i < data.length && i < 5; i++) {
if (data[i] is Map<String, dynamic>) {
sampleData.add(data[i] as Map<String, dynamic>);
}
}
} else {
sampleData.add(sampleItem);
}
// 字段建议映射
final suggestions = _suggestFieldMap(format, fields);
return UrlAnalysisResult(
format: format,
count: count,
latencyMs: latencyMs,
fields: fields,
sampleData: sampleData,
fieldSuggestions: suggestions,
);
} on DioException catch (e) {
stopwatch.stop();
return UrlAnalysisResult(
format: DataFormat.custom,
latencyMs: stopwatch.elapsedMilliseconds,
isAccessible: false,
errorMessage: _dioError(e),
);
} catch (e) {
stopwatch.stop();
return UrlAnalysisResult(
format: DataFormat.custom,
latencyMs: stopwatch.elapsedMilliseconds,
isAccessible: false,
errorMessage: e.toString(),
);
}
}
/// 检测数据格式
DataFormat _detectFormat(Map<String, dynamic> item) {
// Hitokoto格式
if (item.containsKey('hitokoto') && item.containsKey('type')) {
return DataFormat.hitokoto;
}
// 闲言v1格式
if (item.containsKey('title') &&
item.containsKey('content') &&
item.containsKey('category')) {
return DataFormat.xianyanV1;
}
return DataFormat.custom;
}
/// 建议字段映射
Map<String, String> _suggestFieldMap(DataFormat format, List<String> fields) {
const presets = <DataFormat, Map<String, String>>{
DataFormat.hitokoto: {
'title': 'hitokoto',
'category': 'type_name',
'content': 'hitokoto',
'detail': 'from_source',
'author': 'from_who',
'time': 'created_at',
},
DataFormat.xianyanV1: {
'title': 'title',
'category': 'category',
'content': 'content',
'detail': 'detail',
'author': 'author',
'time': 'time',
},
};
if (presets.containsKey(format)) return presets[format]!;
// 自动匹配
final result = <String, String>{};
const autoMap = <String, List<String>>{
'title': ['title', 'name', 'subject', 'hitokoto'],
'category': ['category', 'type', 'tag', 'type_name', 'group'],
'content': ['content', 'text', 'body', 'hitokoto', 'description', 'sentence'],
'detail': ['detail', 'source', 'from', 'from_source'],
'author': ['author', 'from_who', 'creator', 'writer'],
'time': ['time', 'created_at', 'date', 'timestamp', 'createtime'],
};
for (final entry in autoMap.entries) {
for (final candidate in entry.value) {
if (fields.contains(candidate)) {
result[entry.key] = candidate;
break;
}
}
}
return result;
}
String _dioError(DioException e) {
switch (e.type) {
case DioExceptionType.connectionTimeout:
case DioExceptionType.sendTimeout:
case DioExceptionType.receiveTimeout:
return '请求超时';
case DioExceptionType.connectionError:
return '网络连接失败';
default:
return '请求失败: ${e.message}';
}
}
/// 从URL拉取更多数据用于频道创建后补充数据
/// 对比analyze只取5条样本此方法取limit条完整数据
Future<List<Map<String, dynamic>>> fetchMore(String url, {int limit = 20}) async {
try {
// 对hitokoto类URL添加num参数
var fetchUrl = url;
if (url.contains('hitokoto.cn') && !url.contains('num=')) {
final sep = url.contains('?') ? '&' : '?';
fetchUrl = '$url${sep}num=$limit';
} else if (url.contains('sentence_source/sample') && !url.contains('num=')) {
final sep = url.contains('?') ? '&' : '?';
fetchUrl = '$url${sep}num=$limit';
}
final response = await _dio.get<dynamic>(fetchUrl);
final data = response.data;
final List<Map<String, dynamic>> result = [];
if (data is List) {
for (int i = 0; i < data.length && i < limit; i++) {
if (data[i] is Map<String, dynamic>) {
result.add(data[i] as Map<String, dynamic>);
}
}
} else if (data is Map<String, dynamic>) {
// 单条数据尝试从data字段提取列表
if (data.containsKey('data') && data['data'] is List) {
final list = data['data'] as List;
for (int i = 0; i < list.length && i < limit; i++) {
if (list[i] is Map<String, dynamic>) {
result.add(list[i] as Map<String, dynamic>);
}
}
} else {
result.add(data);
}
}
return result;
} catch (e) {
return [];
}
}
}