Skip to content

Transform Plugin Data #678

Transform Plugin Data

Transform Plugin Data #678

name: Transform Plugin Data
on:
schedule:
# 每小时执行一次 (UTC时间)
- cron: '0 * * * *'
workflow_dispatch: # 允许手动触发
permissions:
contents: write
actions: read
jobs:
transform-data:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
token: ${{ secrets.PAT_TOKEN }}
fetch-depth: 0
- name: Configure Git
run: |
git config --local user.email "[email protected]"
git config --local user.name "GitHub Action"
echo "✅ Git 配置完成"
- name: Fetch original plugin data
id: fetch-data
run: |
echo "开始获取原始插件数据..."
echo "当前工作目录: $(pwd)"
echo "PAT_TOKEN 权限检查..."
if [ -n "${{ secrets.PAT_TOKEN }}" ]; then
echo "✓ PAT_TOKEN 已设置"
else
echo "✗ PAT_TOKEN 未设置"
fi
# 创建临时文件存储响应和HTTP状态码
temp_response="temp_response.txt"
temp_headers="temp_headers.txt"
# 获取GitHub原始文件内容
github_url="https://raw.githubusercontent.com/AstrBotDevs/AstrBot_Plugins_Collection/main/plugins.json"
# 使用curl获取数据,添加-L参数自动跟随重定向,增加重定向限制
http_code=$(curl -L -s --max-time 30 --retry 3 --retry-delay 5 \
--max-redirs 10 \
-H "Authorization: token ${{ secrets.PAT_TOKEN }}" \
-H "User-Agent: GitHub-Action-Plugin-Transformer" \
-H "Accept: application/json" \
-w "%{http_code}" \
-D "$temp_headers" \
-o "$temp_response" \
"$github_url")
curl_exit_code=$?
# 检查curl命令是否执行成功
if [ $curl_exit_code -ne 0 ]; then
echo "❌ 网络请求失败,curl退出码: $curl_exit_code"
case $curl_exit_code in
5) echo "无法解析代理" ;;
6) echo "无法解析主机名" ;;
7) echo "无法连接到服务器" ;;
28) echo "请求超时" ;;
35) echo "SSL连接错误" ;;
47) echo "重定向次数过多" ;;
*) echo "其他网络错误" ;;
esac
echo "should_update=false" >> $GITHUB_OUTPUT
rm -f "$temp_response" "$temp_headers"
exit 0
fi
echo "HTTP状态码: $http_code"
# 检查是否发生了重定向
if [ -f "$temp_headers" ]; then
redirect_count=$(grep -c "^HTTP/" "$temp_headers" || echo "1")
if [ "$redirect_count" -gt 1 ]; then
echo "ℹ️ 检测到重定向,共发生 $((redirect_count - 1)) 次重定向"
# 显示重定向链
echo "重定向详情:"
grep -E "^(HTTP/|Location:)" "$temp_headers" | head -10
fi
fi
# 检查HTTP状态码
if [ "$http_code" -ne 200 ]; then
echo "❌ 最终返回非200状态码: $http_code"
case $http_code in
301) echo "永久重定向 (301 Moved Permanently) - 可能需要更新URL" ;;
302) echo "临时重定向 (302 Found)" ;;
404) echo "文件不存在或仓库不可访问 (404 Not Found)" ;;
403) echo "访问被拒绝,可能是API限制 (403 Forbidden)" ;;
500) echo "GitHub服务器内部错误 (500 Internal Server Error)" ;;
*) echo "HTTP错误状态码: $http_code" ;;
esac
echo "should_update=false" >> $GITHUB_OUTPUT
rm -f "$temp_response" "$temp_headers"
exit 0
fi
# 读取响应内容
if [ ! -f "$temp_response" ]; then
echo "❌ 响应文件不存在"
echo "should_update=false" >> $GITHUB_OUTPUT
exit 0
fi
response=$(cat "$temp_response")
# 检查响应是否为空
if [ -z "$response" ] || [ "$response" = "" ]; then
echo "❌ 获取到的响应为空,跳过更新"
echo "should_update=false" >> $GITHUB_OUTPUT
rm -f "$temp_response" "$temp_headers"
exit 0
fi
# 检查响应大小
response_size=$(wc -c < "$temp_response")
if [ "$response_size" -lt 50 ]; then
echo "❌ 响应内容过小 ($response_size 字节),可能是错误响应"
echo "should_update=false" >> $GITHUB_OUTPUT
rm -f "$temp_response" "$temp_headers"
exit 0
fi
# 检查是否为有效的JSON
if ! echo "$response" | jq . > /dev/null 2>&1; then
echo "❌ 响应不是有效的JSON格式,跳过更新"
echo "Content preview: $(echo "$response" | head -c 200)"
echo "should_update=false" >> $GITHUB_OUTPUT
rm -f "$temp_response" "$temp_headers"
exit 0
fi
# 检查JSON是否为空对象或空数组
if [ "$response" = "{}" ] || [ "$response" = "[]" ] || [ "$response" = "null" ]; then
echo "❌ 获取到空的JSON数据,跳过更新"
echo "should_update=false" >> $GITHUB_OUTPUT
rm -f "$temp_response" "$temp_headers"
exit 0
fi
# 保存原始数据到临时文件
echo "$response" > original_plugins.json
echo "should_update=true" >> $GITHUB_OUTPUT
echo "✅ 成功获取原始插件数据 ($response_size 字节)"
# 清理临时文件
rm -f "$temp_response" "$temp_headers"
- name: Load existing cache for fallback
if: steps.fetch-data.outputs.should_update == 'true'
id: load-cache
run: |
echo "检查现有缓存文件..."
if [ -f plugin_cache_original.json ]; then
echo "发现现有缓存文件,将用作回退数据"
cp plugin_cache_original.json existing_cache.json
echo "has_existing_cache=true" >> $GITHUB_OUTPUT
else
echo "没有现有缓存文件"
echo "has_existing_cache=false" >> $GITHUB_OUTPUT
fi
- name: Get GitHub API info for repositories
if: steps.fetch-data.outputs.should_update == 'true'
id: get-repo-info
run: |
echo "开始获取仓库信息..."
# 创建一个临时文件存储仓库信息
echo "{}" > repo_info.json
# 初始化统计计数器
total_repos=0
success_count=0
failed_count=0
deleted_count=0
network_error_count=0
redirect_count=0
# 重试配置
MAX_RETRIES=5
BASE_DELAY=2
MAX_DELAY=30
# 重试函数
retry_api_call() {
local owner="$1"
local repo="$2"
local attempt="$3"
# 计算退避延迟 (指数退避 + 随机抖动)
local delay=$((BASE_DELAY * (2 ** (attempt - 1))))
if [ $delay -gt $MAX_DELAY ]; then
delay=$MAX_DELAY
fi
# 添加随机抖动 (0-50% 的延迟时间)
local jitter=$((RANDOM % (delay / 2 + 1)))
delay=$((delay + jitter))
echo " 第 $attempt 次尝试 (延迟 ${delay}s)..."
sleep $delay
# 使用临时文件捕获响应头
local temp_headers="temp_api_headers_${total_repos}_${attempt}.txt"
# 执行API调用,增强网络配置
local response=$(curl -L -s \
--max-time 20 \
--connect-timeout 10 \
--retry 0 \
--max-redirs 5 \
--keepalive-time 60 \
--tcp-nodelay \
-H "Authorization: token ${{ secrets.PAT_TOKEN }}" \
-H "Accept: application/vnd.github.v3+json" \
-H "User-Agent: GitHub-Action-Plugin-Transformer" \
-H "Connection: keep-alive" \
-D "$temp_headers" \
-w "HTTPSTATUS:%{http_code}:CURL_EXIT:%{exitcode}" \
"https://api.github.com/repos/$owner/$repo" 2>/dev/null || echo "CURL_ERROR:-1")
# 解析响应
if [[ "$response" == "CURL_ERROR"* ]]; then
rm -f "$temp_headers"
return 1
fi
# 提取状态码和curl退出码
local http_code=$(echo "$response" | grep -o "HTTPSTATUS:[0-9]*" | cut -d: -f2)
local curl_exit=$(echo "$response" | grep -o "CURL_EXIT:[0-9]*" | cut -d: -f2)
local body=$(echo "$response" | sed 's/HTTPSTATUS:[0-9]*:CURL_EXIT:[0-9]*$//')
# 检查curl退出码
if [ "$curl_exit" != "0" ]; then
echo " CURL错误码: $curl_exit"
rm -f "$temp_headers"
return 1
fi
# 检查HTTP状态码是否需要重试
case "$http_code" in
200)
# 验证响应是否为有效JSON
if echo "$body" | jq -e '.stargazers_count' > /dev/null 2>&1; then
echo "$body"
rm -f "$temp_headers"
return 0
else
echo " 响应不是有效JSON"
rm -f "$temp_headers"
return 1
fi
;;
429|502|503|504)
# 这些状态码应该重试
echo " 临时错误 HTTP $http_code,将重试"
rm -f "$temp_headers"
return 1
;;
301|302|404|403)
# 这些状态码不应该重试,直接返回
echo "$body:HTTP:$http_code"
rm -f "$temp_headers"
return 0
;;
*)
echo " 未知HTTP状态码: $http_code"
rm -f "$temp_headers"
return 1
;;
esac
}
# 从原始数据中提取所有仓库URL
jq -r 'to_entries[] | .value.repo // empty' original_plugins.json | while read -r repo_url; do
# 提取GitHub用户名和仓库名
if [[ "$repo_url" =~ https://github\.com/([^/]+)/([^/]+) ]]; then
owner="${BASH_REMATCH[1]}"
repo="${BASH_REMATCH[2]}"
total_repos=$((total_repos + 1))
echo "[$total_repos] 获取仓库信息: $owner/$repo"
# 执行重试逻辑
api_response=""
success=false
for attempt in $(seq 1 $MAX_RETRIES); do
if [ $attempt -eq 1 ]; then
echo " 初次尝试..."
# 第一次尝试,无延迟
temp_headers="temp_api_headers_${total_repos}_1.txt"
api_response=$(curl -L -s \
--max-time 15 \
--connect-timeout 8 \
--retry 0 \
--max-redirs 5 \
-H "Authorization: token ${{ secrets.PAT_TOKEN }}" \
-H "Accept: application/vnd.github.v3+json" \
-H "User-Agent: GitHub-Action-Plugin-Transformer" \
-D "$temp_headers" \
-w "HTTPSTATUS:%{http_code}" \
"https://api.github.com/repos/$owner/$repo" 2>/dev/null || echo "CURL_ERROR")
if [[ "$api_response" != "CURL_ERROR" ]]; then
http_code=$(echo "$api_response" | grep -o "HTTPSTATUS:[0-9]*" | cut -d: -f2)
api_response=$(echo "$api_response" | sed 's/HTTPSTATUS:[0-9]*$//')
# 检查是否成功或不需要重试的错误
case "$http_code" in
200)
if echo "$api_response" | jq -e '.stargazers_count' > /dev/null 2>&1; then
success=true
break
fi
;;
301|302|404|403)
# 不需要重试的状态码
success=true
break
;;
429|502|503|504)
# 需要重试的状态码
echo " 临时错误 HTTP $http_code,准备重试"
;;
esac
fi
rm -f "$temp_headers"
else
# 重试调用
retry_response=$(retry_api_call "$owner" "$repo" "$attempt")
if [ $? -eq 0 ]; then
api_response="$retry_response"
success=true
break
fi
fi
# 如果不是最后一次尝试,显示重试信息
if [ $attempt -lt $MAX_RETRIES ]; then
echo " 尝试 $attempt/$MAX_RETRIES 失败,准备重试..."
fi
done
# 处理最终结果
stars=0
updated_at=""
version=""
status="unknown"
if [ "$success" = true ]; then
# 检查是否包含HTTP状态码信息
if [[ "$api_response" == *":HTTP:"* ]]; then
http_code=$(echo "$api_response" | grep -o ":HTTP:[0-9]*" | cut -d: -f3)
api_response=$(echo "$api_response" | sed 's/:HTTP:[0-9]*$//')
fi
case "$http_code" in
200)
if echo "$api_response" | jq -e '.stargazers_count' > /dev/null 2>&1; then
stars=$(echo "$api_response" | jq -r '.stargazers_count // 0')
updated_at=$(echo "$api_response" | jq -r '.updated_at // ""')
success_count=$((success_count + 1))
status="success"
echo " ✅ 成功 - Stars: $stars, 更新时间: $updated_at"
# 获取metadata版本
for metadata_file in "metadata.yml" "metadata.yaml"; do
metadata_response=$(curl -L -s --max-time 10 --max-redirs 3 \
-H "Authorization: token ${{ secrets.PAT_TOKEN }}" \
-H "Accept: application/vnd.github.v3.raw" \
-H "User-Agent: GitHub-Action-Plugin-Transformer" \
"https://api.github.com/repos/$owner/$repo/contents/$metadata_file" 2>/dev/null || echo "{}")
if [[ ! "$metadata_response" =~ "Not Found" ]] && [[ ! "$metadata_response" =~ "Bad Gateway" ]]; then
# 检查是否是base64编码的内容
if echo "$metadata_response" | jq -e '.content' > /dev/null 2>&1; then
metadata_content=$(echo "$metadata_response" | jq -r '.content' | base64 -d 2>/dev/null || echo "")
else
metadata_content="$metadata_response"
fi
# 尝试解析YAML并提取版本
if [ ! -z "$metadata_content" ]; then
parsed_version=$(echo "$metadata_content" | grep -E "^version:\s*['\"]?([^'\"]+)['\"]?" | sed -E "s/version:\s*['\"]?([^'\"]+)['\"]?/\1/" || echo "")
# 去除注释和多余的空白字符
cleaned_version=$(echo "$parsed_version" | sed -E 's/[#].*$//' | sed -E 's/\r$//' | xargs)
if [ ! -z "$cleaned_version" ]; then
version="$cleaned_version"
break
fi
fi
fi
done
fi
;;
301|302)
echo " 🔄 仓库重定向 ($http_code)"
redirect_count=$((redirect_count + 1))
status="redirected"
;;
404)
echo " 🗑️ 仓库已删除或不可访问 (404)"
deleted_count=$((deleted_count + 1))
status="deleted"
;;
403)
echo " ⚠️ API限制或访问被拒绝 (403)"
failed_count=$((failed_count + 1))
status="api_limit"
;;
esac
else
echo " ❌ 所有重试均失败"
network_error_count=$((network_error_count + 1))
status="network_error"
fi
# 如果失败,尝试使用缓存数据
if [ "$status" != "success" ] && [ "${{ steps.load-cache.outputs.has_existing_cache }}" = "true" ]; then
cached_data=$(jq -r --arg url "$repo_url" '.data // {} | to_entries[] | select(.value.repo == $url) | .value | {stars: .stars, updated_at: .updated_at, version: .version}' existing_cache.json 2>/dev/null || echo "{}")
if [ "$cached_data" != "{}" ] && [ "$cached_data" != "" ]; then
cached_stars=$(echo "$cached_data" | jq -r '.stars // 0')
cached_updated=$(echo "$cached_data" | jq -r '.updated_at // ""')
cached_version=$(echo "$cached_data" | jq -r '.version // ""')
if [ "$cached_stars" != "0" ] || [ "$cached_updated" != "" ]; then
echo " 🔄 使用缓存数据: Stars: $cached_stars"
stars="$cached_stars"
updated_at="$cached_updated"
version="$cached_version"
status="cached"
fi
fi
fi
# 将信息添加到repo_info.json
jq --arg url "$repo_url" \
--arg stars "$stars" \
--arg updated "$updated_at" \
--arg version "$version" \
--arg status "$status" \
'. + {($url): {stars: ($stars | tonumber), updated_at: $updated, version: $version, status: $status}}' \
repo_info.json > temp_repo_info.json && mv temp_repo_info.json repo_info.json
# 添加基础延迟避免API限制
sleep 0.5
fi
done
# 成功率检查
if [ $total_repos -gt 0 ]; then
success_rate=$((success_count * 100 / total_repos))
echo "📈 成功率: $success_rate%"
if [ $success_rate -lt 50 ]; then
echo "⚠️ 警告: 成功率过低,可能存在网络问题或GitHub服务异常"
if [ "${{ steps.load-cache.outputs.has_existing_cache }}" = "true" ]; then
echo "已启用缓存回退机制"
fi
fi
fi
echo "✅ 仓库信息获取完成"
- name: Transform plugin data
if: steps.fetch-data.outputs.should_update == 'true'
run: |
echo "开始转换插件数据格式..."
# 使用jq转换数据格式,增加容错处理,并过滤掉404的仓库
jq --slurpfile repo_info repo_info.json '
to_entries |
# 只过滤掉确认已删除(404)的仓库,保留网络错误的仓库
map(select(
if .value.repo and ($repo_info[0][.value.repo]) then
($repo_info[0][.value.repo].status != "deleted")
else
true
end
)) |
map({
key: .key,
value: (
.value + {
# 保持原有字段
desc: .value.desc,
author: .value.author,
repo: .value.repo,
tags: (.value.tags // [])
} +
# 仅当social_link存在且不为空时添加
(if .value.social_link then { social_link: .value.social_link } else {} end) +
# 添加新字段,从repo_info中获取
(if .value.repo and ($repo_info[0][.value.repo]) then
($repo_info[0][.value.repo] | {
stars: .stars,
updated_at: .updated_at,
version: (if .version != "" then .version else "1.0.0" end)
})
else
{
stars: 0,
version: "1.0.0"
}
end)
)
}) | from_entries' original_plugins.json > temp_plugin_cache_original.json
# 格式化JSON使其更易读
jq . temp_plugin_cache_original.json > plugin_cache_original.json
echo "✅ 数据转换完成"
# 显示转换统计
original_count=$(jq 'keys | length' original_plugins.json)
new_count=$(jq 'keys | length' plugin_cache_original.json)
removed_count=$((original_count - new_count))
# 统计不同状态的仓库
success_repos=$(jq '[.[] | select(.status == "success")] | length' repo_info.json)
cached_repos=$(jq '[.[] | select(.status == "cached")] | length' repo_info.json)
redirected_repos=$(jq '[.[] | select(.status == "redirected")] | length' repo_info.json)
deleted_repos=$(jq '[.[] | select(.status == "deleted")] | length' repo_info.json)
failed_repos=$(jq '[.[] | select(.status != "success" and .status != "cached" and .status != "redirected" and .status != "deleted")] | length' repo_info.json)
echo ""
echo "📊 转换统计:"
echo " 插件数量变化: $original_count -> $new_count"
if [ $removed_count -gt 0 ]; then
echo " 🗑️ 已移除: $removed_count 个失效插件"
fi
echo " ✅ 实时数据: $success_repos 个仓库"
echo " 🔄 缓存数据: $cached_repos 个仓库"
echo " 🔄 重定向: $redirected_repos 个仓库"
echo " 🗑️ 已删除(已移除): $deleted_repos 个仓库"
echo " ❌ 网络错误(已保留): $failed_repos 个仓库"
# 列出被移除的仓库
if [ $removed_count -gt 0 ]; then
echo ""
echo "🗑️ 以下仓库已从缓存中移除:"
jq -r 'to_entries[] | select(.value.status == "deleted") | " - " + .key + " (404 Not Found)"' repo_info.json
fi
# 列出网络错误的仓库(保留但使用缓存数据)
if [ "$failed_repos" -gt 0 ]; then
echo ""
echo "❌ 网络错误的仓库(已保留,使用缓存数据):"
jq -r 'to_entries[] | select(.value.status != "success" and .value.status != "cached" and .value.status != "redirected" and .value.status != "deleted") | " - " + .key + " (" + .value.status + ")"' repo_info.json
fi
# 列出重定向的仓库(保留但标记)
if [ "$redirected_repos" -gt 0 ]; then
echo ""
echo "🔄 发生重定向的仓库列表(已保留):"
jq -r 'to_entries[] | select(.value.status == "redirected") | " - " + .key' repo_info.json
fi
- name: Pull latest changes before checking (rebase with autostash)
run: |
set -e
git fetch origin main --depth=1
# 在 Actions 的 detached HEAD 下也能切换到 main
if git rev-parse --abbrev-ref HEAD | grep -q '^HEAD$'; then
git checkout -B main origin/main
else
git checkout main 2>/dev/null || git checkout -b main origin/main
fi
# 使用 autostash 自动 stash 未暂存更改,rebase 完成后会自动 pop
if git pull --rebase --autostash origin main; then
echo "✅ pull --rebase --autostash 成功"
else
echo "❌ pull --rebase --autostash 失败,工作流将退出以便人工检查"
git rebase --abort 2>/dev/null || true
exit 1
fi
- name: Check for changes
if: steps.fetch-data.outputs.should_update == 'true'
id: git-check
run: |
echo "检查文件状态..."
echo "检查远程仓库是否存在 plugin_cache_original.json..."
if git ls-tree --name-only -r origin/main | grep -q "^plugin_cache_original.json$"; then
echo "文件在远程仓库中已存在"
remote_exists="true"
else
echo "文件在远程仓库中不存在"
remote_exists="false"
fi
echo "检查本地 plugin_cache_original.json 是否存在..."
if [ -f plugin_cache_original.json ]; then
echo "文件存在,大小: $(wc -c < plugin_cache_original.json) bytes"
# 验证JSON格式
if jq empty plugin_cache_original.json > /dev/null 2>&1; then
echo "✅ JSON格式有效"
else
echo "❌ JSON格式无效"
echo "has_changes=false" >> $GITHUB_OUTPUT
exit 1
fi
else
echo "❌ 本地文件不存在"
echo "has_changes=false" >> $GITHUB_OUTPUT
exit 1
fi
# 检查是否有变更
if [ -f plugin_cache_original.json ]; then
if [ "$remote_exists" = "true" ]; then
# 文件在远程存在,检查是否有内容变更
git add plugin_cache_original.json # 先添加到暂存区以便比较
if git diff --cached --exit-code plugin_cache_original.json > /dev/null 2>&1; then
echo "has_changes=false" >> $GITHUB_OUTPUT
echo "ℹ️ 文件内容没有变化"
else
echo "has_changes=true" >> $GITHUB_OUTPUT
echo "✅ 检测到文件内容变更"
echo "变更详情:"
git diff --cached plugin_cache_original.json
fi
else
# 文件在远程不存在,这是新文件
echo "has_changes=true" >> $GITHUB_OUTPUT
echo "✅ 这是新文件,需要提交"
# 预先添加到暂存区
git add plugin_cache_original.json
fi
else
# 本地文件不存在
echo "has_changes=false" >> $GITHUB_OUTPUT
echo "❌ 本地文件不存在,跳过提交"
exit 1
fi
# 输出 Git 状态以便调试
echo "Git 状态:"
git status
- name: Commit and push changes
if: steps.fetch-data.outputs.should_update == 'true' && steps.git-check.outputs.has_changes == 'true'
run: |
# 验证认证状态
echo "验证Git认证状态..."
if git ls-remote origin HEAD > /dev/null 2>&1; then
echo "✅ Git认证成功"
else
echo "❌ Git认证失败,检查PAT_TOKEN权限"
exit 1
fi
# 添加和提交文件
git add plugin_cache_original.json
# 获取统计信息用于提交信息
total_plugins=$(jq '.data | keys | length' plugin_cache_original.json 2>/dev/null || echo "0")
success_repos=$(jq '[.[] | select(.status == "success")] | length' repo_info.json 2>/dev/null || echo "0")
commit_message="🔄 Update plugin cache: $total_plugins plugins, $success_repos fresh updates - $(date -u '+%Y-%m-%d %H:%M:%S UTC')"
git commit -m "$commit_message"
# 推送更改
echo "推送更改到远程仓库..."
if git push origin HEAD; then
echo "✅ 成功推送到远程仓库"
else
echo "❌ 推送失败,可能是权限问题"
exit 1
fi
- name: Clean up
if: always()
run: |
# 清理所有临时文件
rm -f temp_plugin_cache_original.json temp_response.txt temp_headers.txt original_plugins.json repo_info.json temp_repo_info.json existing_cache.json temp_api_headers_*.txt
echo "🧹 临时文件清理完成"
- name: Summary
if: always()
run: |
if [ "${{ steps.fetch-data.outputs.should_update }}" = "true" ]; then
if [ "${{ steps.git-check.outputs.has_changes }}" = "true" ]; then
echo "✅ 插件数据已成功转换并提交"
# 显示详细统计
if [ -f plugin_cache_original.json ]; then
total_plugins=$(jq 'keys | length' plugin_cache_original.json)
echo "📊 最终结果: $total_plugins 个插件已更新"
fi
else
echo "ℹ️ 数据获取和转换成功,但内容未发生变化"
fi
else
echo "❌ 由于网络问题、GitHub服务错误或数据异常,跳过了数据转换"
echo "请检查GitHub服务状态或查看上面的错误详情"
fi