chore: 汇总批量提交的功能优化与bug修复

本次提交包含多项迭代优化和问题修复:
1. 新增缩略图图片组件、数字格式化工具类,补充多语言翻译类型与本地化支持
2. 优化底部导航栏主题色统一使用动态accent色值
3. 修复多处图表动画、路由跳转、API请求相关问题
4. 简化服务器公告文案,调整默认分屏状态为关闭
5. 新增安卓/iOS桌面快捷方式配置
6. 重构多处状态管理类使用SafeNotifierInit统一异常保护
7. 替换硬编码蓝色为主题色,更新版本号获取方式为动态读取
8. 优化缓存预加载逻辑,移除无用代码
9. 调整默认设置项,优化用户体验细节
This commit is contained in:
Developer
2026-05-31 12:24:05 +08:00
parent 0da8906f5d
commit 9ea8d3d606
298 changed files with 48547 additions and 21836 deletions

116
scripts/auto_git_stash.ps1 Normal file
View File

@@ -0,0 +1,116 @@
# -------------------------------------------------------
# 文件: auto_git_stash.ps1
# 创建: 2026-05-31
# 更新: 2026-05-31
# 名称: Git 自动暂存脚本
# 作用: 每5分钟自动检测并暂存 Git 仓库的未提交更改
# 上次更新内容: 初始创建
# -------------------------------------------------------
param(
[int]$IntervalMinutes = 5,
[string]$RepoPath = "e:\project\flutter\f\xianyan",
[switch]$DryRun
)
$ErrorActionPreference = "Continue"
$stamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
Write-Host ""
Write-Host "========================================" -ForegroundColor Cyan
Write-Host " Git Auto Stash - 自动暂存守护脚本" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host " 仓库路径 : $RepoPath" -ForegroundColor Gray
Write-Host " 检查间隔 : $IntervalMinutes 分钟" -ForegroundColor Gray
Write-Host " 模拟运行 : $DryRun" -ForegroundColor Gray
Write-Host " 启动时间 : $stamp" -ForegroundColor Gray
Write-Host " 按 Ctrl+C 停止" -ForegroundColor Yellow
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
if (-not (Test-Path $RepoPath)) {
Write-Host "[错误] 仓库路径不存在: $RepoPath" -ForegroundColor Red
exit 1
}
$logFile = Join-Path $RepoPath "auto_stash.log"
function Write-Log {
param([string]$Message)
$ts = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$line = "[$ts] $Message"
Add-Content -Path $logFile -Value $line -Encoding UTF8
Write-Host $line
}
function Test-HasChanges {
param([string]$Path)
$status = git -C $Path status --porcelain 2>&1
if ($LASTEXITCODE -ne 0) {
Write-Log "[错误] git status 失败: $status"
return $false
}
return ($status -and $status.ToString().Trim().Length -gt 0)
}
function Get-StashCount {
param([string]$Path)
$list = git -C $Path stash list 2>&1
if ($LASTEXITCODE -ne 0 -or -not $list) { return 0 }
return ($list -split "`n" | Where-Object { $_.Trim().Length -gt 0 }).Count
}
function Invoke-AutoStash {
$now = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$branch = git -C $RepoPath rev-parse --abbrev-ref HEAD 2>&1
if ($LASTEXITCODE -ne 0) {
Write-Log "[错误] 无法获取当前分支"
return
}
$hasChanges = Test-HasChanges -Path $RepoPath
if (-not $hasChanges) {
Write-Log "[$branch] 无未提交更改,跳过"
return
}
$stashLabel = "auto-stash-$((Get-Date).ToString('yyyyMMdd-HHmmss'))"
if ($DryRun) {
Write-Log "[$branch] [模拟] 检测到更改,将执行: git stash push -m `"$stashLabel`""
return
}
$beforeCount = Get-StashCount -Path $RepoPath
git -C $RepoPath stash push -m $stashLabel 2>&1 | ForEach-Object {
Write-Log "[$branch] $_"
}
if ($LASTEXITCODE -eq 0) {
$afterCount = Get-StashCount -Path $RepoPath
if ($afterCount -gt $beforeCount) {
Write-Log "[$branch] ✓ 已暂存: $stashLabel (stash@{$afterCount - 1})"
} else {
Write-Log "[$branch] stash 执行但未创建新条目(可能无实际更改)"
}
} else {
Write-Log "[$branch] ✗ stash 失败"
}
}
Write-Log "===== 自动暂存守护启动 ====="
while ($true) {
try {
Invoke-AutoStash
} catch {
Write-Log "[异常] $($_.Exception.Message)"
}
$nextTime = (Get-Date).AddMinutes($IntervalMinutes).ToString("HH:mm:ss")
Write-Host " 下次检查: $nextTime (间隔 ${IntervalMinutes}min)" -ForegroundColor DarkGray
Start-Sleep -Seconds ($IntervalMinutes * 60)
}

591
scripts/test_tool_api.py Normal file
View File

@@ -0,0 +1,591 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
闲言APP — 工具中心API验证脚本
创建时间: 2026-05-30
更新时间: 2026-05-30
作用: 验证工具中心所有API接口测试hanzi_search和searchall两种搜索类型以及外部API和RSS源
上次更新: 新增汇率换算、音乐识别、RSS源可达性测试确认fortune接口仍可用
"""
import requests
import json
import sys
import time
BASE_URL = 'https://tools.wktyl.com'
HANZI_SEARCH_TYPES = {
'poetry': {'keyword': '', 'name': '古诗词'},
'brainteaser': {'keyword': '什么', 'name': '脑筋急转弯'},
'couplet': {'keyword': '', 'name': '对联大全'},
'wisdom': {'keyword': '人生', 'name': '名人名言'},
'story': {'keyword': '故事', 'name': '故事大全'},
'saying': {'keyword': '', 'name': '谚语大全'},
'riddle': {'keyword': '月亮', 'name': '谜语'},
'xiehouyu': {'keyword': '', 'name': '歇后语'},
'zuowen': {'keyword': '春天', 'name': '作文大全'},
'why': {'keyword': '为什么', 'name': '十万个为什么'},
'drug': {'keyword': '感冒', 'name': '药品查询'},
'food': {'keyword': '鸡蛋', 'name': '食物相克'},
'herbal': {'keyword': '人参', 'name': '中药材'},
'pianfang': {'keyword': '咳嗽', 'name': '民间偏方'},
'tisana': {'keyword': '菊花', 'name': '药茶大全'},
'changshi': {'keyword': '生活', 'name': '生活常识'},
'lyric': {'keyword': '月亮', 'name': '歌词大全'},
}
SEARCHALL_TYPES = {
'zgjm': {'keyword': '', 'name': '周公解梦'},
'joke': {'keyword': '小明', 'name': '笑话大全'},
'illness': {'keyword': '头痛', 'name': '疾病自查'},
'surname': {'keyword': '', 'name': '姓氏起源'},
'jieqi': {'keyword': '立春', 'name': '节气查询'},
'nation': {'keyword': '中国', 'name': '国家查询'},
}
OTHER_APIS = {
'hanzi_zi': {
'method': 'POST',
'url': '/api/hanzi/zi',
'data': {'zi': ''},
'name': '查字',
},
'hanzi_zuci': {
'method': 'POST',
'url': '/api/hanzi/zuci',
'data': {'zi': ''},
'name': '组词查询',
},
'hanzi_cidian': {
'method': 'POST',
'url': '/api/hanzi/cidian',
'data': {'zi': '快乐'},
'name': '词典查询',
},
'hanzi_chengyu': {
'method': 'POST',
'url': '/api/hanzi/chengyu',
'data': {'zi': '一心一意'},
'name': '成语查询',
},
'hanzi_jinyici': {
'method': 'POST',
'url': '/api/hanzi/jinyici',
'data': {'zi': '快乐'},
'name': '近义词查询',
},
'hanzi_juzi': {
'method': 'POST',
'url': '/api/hanzi/juzi',
'data': {'zi': '春天'},
'name': '句子查询',
},
'hanzi_danci': {
'method': 'POST',
'url': '/api/hanzi/danci',
'data': {'zi': 'hello'},
'name': '英语单词',
},
'hanzi_suoxie': {
'method': 'POST',
'url': '/api/hanzi/suoxie',
'data': {'zi': 'AI'},
'name': '英文缩写',
},
'hitokoto_random': {
'method': 'GET',
'url': '/api/hitokoto/random',
'params': {'num': 1},
'name': '一言随机',
},
'hitokoto_categories': {
'method': 'GET',
'url': '/api/hitokoto/categories',
'params': None,
'name': '一言分类',
},
'jiufang_search': {
'method': 'GET',
'url': '/api/webapi/jiufang_search',
'params': {'keyword': '人参'},
'name': '酒方搜索',
},
'daily_recommend': {
'method': 'GET',
'url': '/api/daily/recommend',
'params': None,
'name': '每日推荐',
},
'china_colors': {
'method': 'GET',
'url': '/api/hanzi/china_colors',
'params': None,
'name': '中国传统色',
},
'nick_gen': {
'method': 'POST',
'url': '/api/hanzi/nick',
'data': {'zi': ''},
'name': '网名生成',
},
'ip_query': {
'method': 'POST',
'url': '/api/webapi/ip',
'data': {'ip': '8.8.8.8'},
'name': 'IP查询',
},
'ip_check': {
'method': 'POST',
'url': '/api/ipcheck/check',
'data': {'ip': '8.8.8.8', 'port': 443},
'name': 'IP连通性检测',
},
'stats_overview': {
'method': 'GET',
'url': '/api/webapi/stats_overview',
'params': None,
'name': '站点统计概览',
},
'stats_hot_tools': {
'method': 'GET',
'url': '/api/webapi/stats_hot_tools',
'params': {'limit': 5},
'name': '热门工具排行',
},
'stats_content': {
'method': 'GET',
'url': '/api/webapi/stats_content',
'params': None,
'name': '内容数据统计',
},
'check_sources': {
'method': 'GET',
'url': '/api/check/sources',
'params': None,
'name': '查重数据源列表',
},
'check_exact': {
'method': 'POST',
'url': '/api/check/exact',
'data': {'text': '春眠不觉晓'},
'name': '精确查重',
},
'fortune_daily': {
'method': 'GET',
'url': '/api/fortune/daily',
'params': {'uid': 'test_user_api'},
'name': '每日运势',
},
'fortune_themes': {
'method': 'GET',
'url': '/api/fortune/themes',
'params': None,
'name': '运势卡片风格',
},
'fortune_60s': {
'method': 'GET',
'url': '/api/fortune/60s',
'params': None,
'name': '60秒新闻',
},
'fortune_huangli': {
'method': 'GET',
'url': '/api/fortune/huangli',
'params': None,
'name': '黄历数据',
},
'fortune_horoscope': {
'method': 'GET',
'url': '/api/fortune/horoscope',
'params': {'sign': '白羊', 'time': 'today'},
'name': '星座运势',
},
'feed_stats': {
'method': 'GET',
'url': '/api/feed/stats',
'params': None,
'name': 'Feed信息流统计',
},
'searchall_suggest': {
'method': 'GET',
'url': '/api/searchall/suggest',
'params': {'keyword': ''},
'name': '搜索建议',
},
'unit_temp': {
'method': 'POST',
'url': '/api/hanzi/temp',
'data': {'value': '100', 'type': 'c2f'},
'name': '温度换算',
},
'unit_speed': {
'method': 'POST',
'url': '/api/hanzi/speed',
'data': {'value': '100', 'type': 'kmh2ms'},
'name': '速度换算',
},
'unit_weight': {
'method': 'POST',
'url': '/api/hanzi/weight',
'data': {'value': '1', 'type': 'kg2lb'},
'name': '重量换算',
},
}
EXTERNAL_APIS = {
'exchange_rate_cny': {
'method': 'GET',
'url': 'https://open.er-api.com/v6/latest/CNY',
'name': '汇率换算(CNY基准)',
'validate': 'rates_field',
'required_currencies': ['USD', 'EUR', 'JPY'],
},
'audd_lyrics_search': {
'method': 'GET',
'url': 'https://api.audd.io/findLyrics/',
'params': {'api_token': 'test', 'q': 'hello'},
'name': '音乐识别-歌词搜索',
'validate': 'reachable',
},
'audd_recognize': {
'method': 'POST',
'url': 'https://api.audd.io/recognizeWithOffset',
'data': {'api_token': 'test'},
'name': '音乐识别-API可达性',
'validate': 'reachable',
},
}
RSS_FEEDS = {
'36kr': {
'url': 'https://36kr.com/feed',
'name': '36氪',
},
'ifanr': {
'url': 'https://www.ifanr.com/feed',
'name': '爱范儿',
},
'sspai': {
'url': 'https://sspai.com/feed',
'name': '少数派',
},
'v2ex': {
'url': 'https://www.v2ex.com/index.xml',
'name': 'V2EX',
},
}
def test_hanzi_search():
print('\n' + '=' * 60)
print(' 一、hanzi_search 接口测试 (17种类型)')
print('=' * 60)
results = {'pass': 0, 'fail': 0, 'errors': []}
for type_key, info in HANZI_SEARCH_TYPES.items():
try:
resp = requests.post(
f'{BASE_URL}/api/hanzi/search',
data={'zi': info['keyword'], 'type': type_key, 'page': 1, 'limit': 5},
timeout=10,
)
data = resp.json()
if data.get('code') == 1:
total = data.get('data', {}).get('total', 0)
list_count = len(data.get('data', {}).get('list', []))
print(f'{info["name"]:8s} (type={type_key:14s}) — 总{total}条, 返回{list_count}')
results['pass'] += 1
else:
msg = data.get('msg', '未知错误')
print(f'{info["name"]:8s} (type={type_key:14s}) — {msg}')
results['fail'] += 1
results['errors'].append(f'{info["name"]}: {msg}')
except Exception as e:
print(f' 💥 {info["name"]:8s} (type={type_key:14s}) — 请求异常: {e}')
results['fail'] += 1
results['errors'].append(f'{info["name"]}: 请求异常 {e}')
time.sleep(0.3)
return results
def test_searchall():
print('\n' + '=' * 60)
print(' 二、searchall 接口测试 (6种类型)')
print('=' * 60)
results = {'pass': 0, 'fail': 0, 'errors': []}
for type_key, info in SEARCHALL_TYPES.items():
try:
resp = requests.get(
f'{BASE_URL}/api/searchall/search',
params={'keyword': info['keyword'], 'type': type_key, 'page': 1, 'limit': 5},
timeout=10,
)
data = resp.json()
if data.get('code') == 1:
total = data.get('data', {}).get('total', 0)
list_count = len(data.get('data', {}).get('list', []))
print(f'{info["name"]:8s} (type={type_key:14s}) — 总{total}条, 返回{list_count}')
results['pass'] += 1
else:
msg = data.get('msg', '未知错误')
print(f'{info["name"]:8s} (type={type_key:14s}) — {msg}')
results['fail'] += 1
results['errors'].append(f'{info["name"]}: {msg}')
except Exception as e:
print(f' 💥 {info["name"]:8s} (type={type_key:14s}) — 请求异常: {e}')
results['fail'] += 1
results['errors'].append(f'{info["name"]}: 请求异常 {e}')
time.sleep(0.3)
return results
def test_other_apis():
print('\n' + '=' * 60)
print(' 三、其他工具接口测试')
print('=' * 60)
results = {'pass': 0, 'fail': 0, 'errors': []}
for api_key, info in OTHER_APIS.items():
try:
if info['method'] == 'POST':
resp = requests.post(
f'{BASE_URL}{info["url"]}',
data=info.get('data'),
timeout=10,
)
else:
resp = requests.get(
f'{BASE_URL}{info["url"]}',
params=info.get('params'),
timeout=10,
)
data = resp.json()
if data.get('code') == 1:
data_preview = ''
if isinstance(data.get('data'), dict):
keys = list(data['data'].keys())[:3]
data_preview = f' — 字段: {", ".join(keys)}...'
elif isinstance(data.get('data'), list):
data_preview = f'{len(data["data"])}'
print(f'{info["name"]:12s} ({api_key:20s}){data_preview}')
results['pass'] += 1
else:
msg = data.get('msg', '未知错误')
print(f'{info["name"]:12s} ({api_key:20s}) — {msg}')
results['fail'] += 1
results['errors'].append(f'{info["name"]}: {msg}')
except requests.exceptions.JSONDecodeError:
print(f' ⚠️ {info["name"]:12s} ({api_key:20s}) — 返回非JSON(可能已下线)')
results['fail'] += 1
results['errors'].append(f'{info["name"]}: 返回非JSON')
except Exception as e:
print(f' 💥 {info["name"]:12s} ({api_key:20s}) — 请求异常: {e}')
results['fail'] += 1
results['errors'].append(f'{info["name"]}: 请求异常 {e}')
time.sleep(0.3)
return results
def test_error_type():
print('\n' + '=' * 60)
print(' 四、错误类型验证(确认不支持的类型返回正确错误)')
print('=' * 60)
invalid_types = ['invalid_type', 'zgjm', 'joke', 'illness']
for t in invalid_types:
try:
resp = requests.post(
f'{BASE_URL}/api/hanzi/search',
data={'zi': '测试', 'type': t, 'page': 1, 'limit': 5},
timeout=10,
)
data = resp.json()
if data.get('code') == 0 and '不支持的搜索类型' in data.get('msg', ''):
print(f' ✅ type={t:14s} — 正确返回错误: {data["msg"][:40]}...')
else:
print(f' ⚠️ type={t:14s} — 意外响应: code={data.get("code")}, msg={data.get("msg")}')
except Exception as e:
print(f' 💥 type={t:14s} — 请求异常: {e}')
time.sleep(0.3)
def test_external_apis():
print('\n' + '=' * 60)
print(' 五、外部API接口测试汇率/音乐识别)')
print('=' * 60)
results = {'pass': 0, 'fail': 0, 'errors': []}
for api_key, info in EXTERNAL_APIS.items():
try:
if info['method'] == 'POST':
resp = requests.post(info['url'], data=info.get('data'), timeout=15)
else:
resp = requests.get(info['url'], params=info.get('params'), timeout=15)
validate_type = info.get('validate', 'reachable')
if validate_type == 'rates_field':
data = resp.json()
rates = data.get('rates', {})
if not rates:
print(f'{info["name"]:20s} ({api_key}) — rates字段为空')
results['fail'] += 1
results['errors'].append(f'{info["name"]}: rates字段为空')
continue
missing = [c for c in info.get('required_currencies', []) if c not in rates]
if missing:
print(f'{info["name"]:20s} ({api_key}) — 缺少货币: {", ".join(missing)}')
results['fail'] += 1
results['errors'].append(f'{info["name"]}: 缺少货币 {", ".join(missing)}')
else:
base = data.get('base_code', '?')
currency_count = len(rates)
sample = {c: rates[c] for c in info.get('required_currencies', [])[:3]}
print(f'{info["name"]:20s} ({api_key}) — 基准:{base}, {currency_count}种货币, 示例:{sample}')
results['pass'] += 1
elif validate_type == 'reachable':
if resp.status_code < 500:
status = resp.status_code
try:
body = resp.json()
result_status = body.get('status', '')
print(f'{info["name"]:20s} ({api_key}) — HTTP {status}, status={result_status}')
except Exception:
print(f'{info["name"]:20s} ({api_key}) — HTTP {status}, API可达')
results['pass'] += 1
else:
print(f'{info["name"]:20s} ({api_key}) — HTTP {resp.status_code}, 服务不可达')
results['fail'] += 1
results['errors'].append(f'{info["name"]}: HTTP {resp.status_code}')
except requests.exceptions.Timeout:
print(f' ⏱️ {info["name"]:20s} ({api_key}) — 请求超时(15s)')
results['fail'] += 1
results['errors'].append(f'{info["name"]}: 请求超时')
except Exception as e:
print(f' 💥 {info["name"]:20s} ({api_key}) — 请求异常: {e}')
results['fail'] += 1
results['errors'].append(f'{info["name"]}: 请求异常 {e}')
time.sleep(0.5)
return results
def test_rss_feeds():
print('\n' + '=' * 60)
print(' 六、RSS源可达性测试')
print('=' * 60)
results = {'pass': 0, 'fail': 0, 'errors': []}
for feed_key, info in RSS_FEEDS.items():
try:
resp = requests.get(info['url'], timeout=15, headers={
'User-Agent': 'Mozilla/5.0 (compatible; XianYanApp/1.0; RSS Reader)'
})
if resp.status_code != 200:
print(f'{info["name"]:8s} ({feed_key:8s}) — HTTP {resp.status_code}')
results['fail'] += 1
results['errors'].append(f'{info["name"]}: HTTP {resp.status_code}')
continue
content = resp.text
has_xml = '<rss' in content or '<feed' in content or '<xml' in content or '<?xml' in content
if has_xml:
tag = '<rss' if '<rss' in content else ('<feed' if '<feed' in content else '<?xml')
item_count = content.count('<item') + content.count('<entry')
print(f'{info["name"]:8s} ({feed_key:8s}) — HTTP 200, 含{tag}, 约{item_count}条内容')
results['pass'] += 1
else:
print(f' ⚠️ {info["name"]:8s} ({feed_key:8s}) — HTTP 200, 但未检测到XML标签')
results['fail'] += 1
results['errors'].append(f'{info["name"]}: 无XML标签')
except requests.exceptions.Timeout:
print(f' ⏱️ {info["name"]:8s} ({feed_key:8s}) — 请求超时(15s)')
results['fail'] += 1
results['errors'].append(f'{info["name"]}: 请求超时')
except Exception as e:
print(f' 💥 {info["name"]:8s} ({feed_key:8s}) — 请求异常: {e}')
results['fail'] += 1
results['errors'].append(f'{info["name"]}: 请求异常 {e}')
time.sleep(0.5)
return results
def main():
print('╔══════════════════════════════════════════════════════════╗')
print('║ 闲言APP — 工具中心API全面验证脚本 ║')
print('║ 基础URL: https://tools.wktyl.com ║')
print('╚══════════════════════════════════════════════════════════╝')
all_results = []
r1 = test_hanzi_search()
all_results.append(('hanzi_search', r1))
r2 = test_searchall()
all_results.append(('searchall', r2))
r3 = test_other_apis()
all_results.append(('other_apis', r3))
test_error_type()
r5 = test_external_apis()
all_results.append(('external_apis', r5))
r6 = test_rss_feeds()
all_results.append(('rss_feeds', r6))
print('\n' + '=' * 60)
print(' 汇总报告')
print('=' * 60)
total_pass = sum(r['pass'] for _, r in all_results)
total_fail = sum(r['fail'] for _, r in all_results)
total = total_pass + total_fail
for name, r in all_results:
status = '✅ 全部通过' if r['fail'] == 0 else f'{r["fail"]}个失败'
print(f' {name:16s}: {r["pass"]}通过 / {r["fail"]}失败 — {status}')
print(f'\n 总计: {total_pass}/{total} 通过 ({total_pass * 100 // max(total, 1)}%)')
if total_fail > 0:
print('\n ❌ 失败详情:')
for name, r in all_results:
for err in r['errors']:
print(f' - [{name}] {err}')
print('\n' + '=' * 60)
return 0 if total_fail == 0 else 1
if __name__ == '__main__':
sys.exit(main())