1. 删除本地临时调试文件、工作流模板、闲置脚本与脑暴记录 2. 新增极简在线记事本API部署文件 3. 修复Flutter端拾光Sheet布局与状态更新问题 4. 完善句子来源后端API与前端导入逻辑 5. 修复macOS平台secure存储插件引用
247 lines
7.4 KiB
Dart
247 lines
7.4 KiB
Dart
// ============================================================
|
||
// 闲言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 [];
|
||
}
|
||
}
|
||
}
|