revert: restore to 03e51a9 baseline (remove cleanup scripts)
Browse files- Dockerfile +3 -6
- cleanup_tokens.sh +0 -217
- config.yaml +3 -1
- entrypoint.sh +0 -54
Dockerfile
CHANGED
|
@@ -2,20 +2,17 @@ FROM eceasy/cli-proxy-api:latest
|
|
| 2 |
|
| 3 |
USER root
|
| 4 |
|
| 5 |
-
RUN apk add --no-cache bash libc6-compat gcompat
|
| 6 |
|
| 7 |
WORKDIR /app
|
| 8 |
RUN cp /CLIProxyAPI/CLIProxyAPI ./cli-proxy-api && chmod +x ./cli-proxy-api
|
| 9 |
RUN mkdir -p /tmp/.cli-proxy-api /tmp/logs /tmp/pg_cache/pgstore && chmod -R 777 /tmp
|
| 10 |
|
| 11 |
COPY config.yaml /app/config.yaml
|
| 12 |
-
|
| 13 |
-
COPY entrypoint.sh /app/entrypoint.sh
|
| 14 |
-
|
| 15 |
RUN cp /app/config.yaml /app/config.example.yaml
|
| 16 |
-
RUN chmod +x /app/cleanup_tokens.sh /app/entrypoint.sh
|
| 17 |
|
| 18 |
ENV TZ=Asia/Shanghai
|
| 19 |
EXPOSE 7860
|
| 20 |
|
| 21 |
-
CMD ["/app/
|
|
|
|
| 2 |
|
| 3 |
USER root
|
| 4 |
|
| 5 |
+
RUN apk add --no-cache bash libc6-compat gcompat
|
| 6 |
|
| 7 |
WORKDIR /app
|
| 8 |
RUN cp /CLIProxyAPI/CLIProxyAPI ./cli-proxy-api && chmod +x ./cli-proxy-api
|
| 9 |
RUN mkdir -p /tmp/.cli-proxy-api /tmp/logs /tmp/pg_cache/pgstore && chmod -R 777 /tmp
|
| 10 |
|
| 11 |
COPY config.yaml /app/config.yaml
|
| 12 |
+
# 修复 config.example.yaml 缺失
|
|
|
|
|
|
|
| 13 |
RUN cp /app/config.yaml /app/config.example.yaml
|
|
|
|
| 14 |
|
| 15 |
ENV TZ=Asia/Shanghai
|
| 16 |
EXPOSE 7860
|
| 17 |
|
| 18 |
+
CMD ["./cli-proxy-api", "--config", "/app/config.yaml"]
|
cleanup_tokens.sh
DELETED
|
@@ -1,217 +0,0 @@
|
|
| 1 |
-
#!/bin/sh
|
| 2 |
-
#
|
| 3 |
-
# cleanup_tokens.sh - Remove invalid/unavailable auth tokens via CLIProxyAPI Management API
|
| 4 |
-
#
|
| 5 |
-
# Environment variables:
|
| 6 |
-
# MANAGEMENT_PASSWORD CLIProxyAPI management API password (required)
|
| 7 |
-
# FEISHU_WEBHOOK_URL Feishu bot webhook URL for notifications (optional)
|
| 8 |
-
# CLEANUP_CONCURRENCY Max parallel DELETE requests (default: 20)
|
| 9 |
-
#
|
| 10 |
-
|
| 11 |
-
API_BASE="http://localhost:7860/v0/management"
|
| 12 |
-
TIMESTAMP="$(date '+%Y-%m-%d %H:%M:%S')"
|
| 13 |
-
LOG_PREFIX="[cleanup_tokens] ${TIMESTAMP}"
|
| 14 |
-
TMP_FILE="/tmp/auth_files_$$.json"
|
| 15 |
-
TMP_NAMES="/tmp/auth_names_$$.txt"
|
| 16 |
-
TMP_DIR="/tmp/cleanup_$$"
|
| 17 |
-
|
| 18 |
-
CONCURRENCY="${CLEANUP_CONCURRENCY:-20}"
|
| 19 |
-
|
| 20 |
-
cleanup_tmp() {
|
| 21 |
-
rm -f "$TMP_FILE" "$TMP_NAMES"
|
| 22 |
-
rm -rf "$TMP_DIR"
|
| 23 |
-
}
|
| 24 |
-
trap cleanup_tmp EXIT
|
| 25 |
-
|
| 26 |
-
# ---------------------------------------------------------------------------
|
| 27 |
-
# notify_feishu TITLE BODY
|
| 28 |
-
# ---------------------------------------------------------------------------
|
| 29 |
-
notify_feishu() {
|
| 30 |
-
[ -z "$FEISHU_WEBHOOK_URL" ] && return 0
|
| 31 |
-
PAYLOAD=$(jq -n --arg title "$1" --arg body "$2" '{
|
| 32 |
-
msg_type: "post",
|
| 33 |
-
content: { post: { zh_cn: {
|
| 34 |
-
title: $title,
|
| 35 |
-
content: [[{"tag": "text", "text": $body}]]
|
| 36 |
-
}}}
|
| 37 |
-
}')
|
| 38 |
-
curl -sf -X POST -H "Content-Type: application/json" \
|
| 39 |
-
-d "$PAYLOAD" "$FEISHU_WEBHOOK_URL" > /dev/null 2>&1 \
|
| 40 |
-
&& echo "${LOG_PREFIX} Feishu notification sent." \
|
| 41 |
-
|| echo "${LOG_PREFIX} WARNING: Failed to send Feishu notification."
|
| 42 |
-
}
|
| 43 |
-
|
| 44 |
-
# ---------------------------------------------------------------------------
|
| 45 |
-
# delete_one NAME — writes result to $TMP_DIR/<name>.result
|
| 46 |
-
# Format: "OK <name>" or "ERR <name> <http_status>"
|
| 47 |
-
# ---------------------------------------------------------------------------
|
| 48 |
-
delete_one() {
|
| 49 |
-
NAME="$1"
|
| 50 |
-
ENCODED=$(printf '%s' "$NAME" | sed 's/@/%40/g; s/ /%20/g')
|
| 51 |
-
RESULT_FILE="${TMP_DIR}/$(printf '%s' "$NAME" | md5sum | cut -d' ' -f1).result"
|
| 52 |
-
|
| 53 |
-
HTTP_ST=$(curl -s -o /dev/null -w "%{http_code}" \
|
| 54 |
-
-X DELETE \
|
| 55 |
-
-H "Authorization: Bearer ${MANAGEMENT_PASSWORD}" \
|
| 56 |
-
"${API_BASE}/auth-files?name=${ENCODED}" 2>/dev/null)
|
| 57 |
-
|
| 58 |
-
if [ "$HTTP_ST" = "200" ]; then
|
| 59 |
-
printf 'OK %s\n' "$NAME" > "$RESULT_FILE"
|
| 60 |
-
else
|
| 61 |
-
printf 'ERR %s %s\n' "$NAME" "$HTTP_ST" > "$RESULT_FILE"
|
| 62 |
-
fi
|
| 63 |
-
}
|
| 64 |
-
|
| 65 |
-
# ---------------------------------------------------------------------------
|
| 66 |
-
# Main
|
| 67 |
-
# ---------------------------------------------------------------------------
|
| 68 |
-
|
| 69 |
-
if [ -z "$MANAGEMENT_PASSWORD" ]; then
|
| 70 |
-
echo "${LOG_PREFIX} ERROR: MANAGEMENT_PASSWORD not set, skipping cleanup"
|
| 71 |
-
exit 1
|
| 72 |
-
fi
|
| 73 |
-
|
| 74 |
-
echo "${LOG_PREFIX} Starting invalid token cleanup... (concurrency=${CONCURRENCY})"
|
| 75 |
-
|
| 76 |
-
# --- Fetch auth file list ---------------------------------------------------
|
| 77 |
-
HTTP_STATUS=$(curl -s -o "$TMP_FILE" -w "%{http_code}" \
|
| 78 |
-
-H "Authorization: Bearer ${MANAGEMENT_PASSWORD}" \
|
| 79 |
-
"${API_BASE}/auth-files" 2>/dev/null)
|
| 80 |
-
|
| 81 |
-
if [ "$HTTP_STATUS" != "200" ]; then
|
| 82 |
-
MSG="API 返回 HTTP ${HTTP_STATUS},服务可能尚未就绪。"
|
| 83 |
-
echo "${LOG_PREFIX} ERROR: ${MSG}"
|
| 84 |
-
notify_feishu "❌ Token 清理失败" "[TIME] ${TIMESTAMP}
|
| 85 |
-
|
| 86 |
-
[ERROR] ${MSG}"
|
| 87 |
-
exit 1
|
| 88 |
-
fi
|
| 89 |
-
|
| 90 |
-
if ! jq empty "$TMP_FILE" 2>/dev/null; then
|
| 91 |
-
echo "${LOG_PREFIX} ERROR: Invalid JSON response"
|
| 92 |
-
notify_feishu "❌ Token 清理失败" "[TIME] ${TIMESTAMP}
|
| 93 |
-
|
| 94 |
-
[ERROR] API 返回了无效的 JSON 响应"
|
| 95 |
-
exit 1
|
| 96 |
-
fi
|
| 97 |
-
|
| 98 |
-
TOTAL=$(jq '.files | length' "$TMP_FILE")
|
| 99 |
-
echo "${LOG_PREFIX} Total auth files: ${TOTAL}"
|
| 100 |
-
|
| 101 |
-
[ "$TOTAL" -eq 0 ] && { echo "${LOG_PREFIX} No auth files found."; exit 0; }
|
| 102 |
-
|
| 103 |
-
# --- Identify tokens to delete ---------------------------------------------
|
| 104 |
-
jq -r '
|
| 105 |
-
.files[] |
|
| 106 |
-
select(.runtime_only != true) |
|
| 107 |
-
select(
|
| 108 |
-
.unavailable == true or
|
| 109 |
-
(.status != null and (
|
| 110 |
-
.status == "error" or .status == "expired" or
|
| 111 |
-
.status == "invalid" or .status == "failed" or
|
| 112 |
-
.status == "unauthorized" or .status == "quota_exceeded"
|
| 113 |
-
))
|
| 114 |
-
) |
|
| 115 |
-
.name
|
| 116 |
-
' "$TMP_FILE" > "$TMP_NAMES"
|
| 117 |
-
|
| 118 |
-
TO_DELETE=$(grep -c '' "$TMP_NAMES" 2>/dev/null || echo 0)
|
| 119 |
-
echo "${LOG_PREFIX} Tokens to delete: ${TO_DELETE}"
|
| 120 |
-
|
| 121 |
-
if [ "$TO_DELETE" -eq 0 ]; then
|
| 122 |
-
echo "${LOG_PREFIX} All tokens healthy, nothing to clean up."
|
| 123 |
-
notify_feishu "✅ Token 状态检查" "[TIME] ${TIMESTAMP}
|
| 124 |
-
|
| 125 |
-
[STATS] 共 ${TOTAL} 个 token,全部健康
|
| 126 |
-
|
| 127 |
-
[RESULT] 无需清理"
|
| 128 |
-
exit 0
|
| 129 |
-
fi
|
| 130 |
-
|
| 131 |
-
# Log the plan
|
| 132 |
-
while IFS= read -r NAME; do
|
| 133 |
-
STATUS=$(jq -r --arg n "$NAME" '.files[] | select(.name==$n) | .status // "unknown"' "$TMP_FILE")
|
| 134 |
-
echo "${LOG_PREFIX} - ${NAME} (${STATUS})"
|
| 135 |
-
done < "$TMP_NAMES"
|
| 136 |
-
|
| 137 |
-
# --- Parallel DELETE with concurrency control ------------------------------
|
| 138 |
-
mkdir -p "$TMP_DIR"
|
| 139 |
-
|
| 140 |
-
ACTIVE=0
|
| 141 |
-
while IFS= read -r NAME; do
|
| 142 |
-
[ -z "$NAME" ] && continue
|
| 143 |
-
|
| 144 |
-
# Launch delete in background
|
| 145 |
-
delete_one "$NAME" &
|
| 146 |
-
ACTIVE=$((ACTIVE + 1))
|
| 147 |
-
|
| 148 |
-
# When we hit the concurrency limit, wait for all current batch to finish
|
| 149 |
-
if [ "$ACTIVE" -ge "$CONCURRENCY" ]; then
|
| 150 |
-
wait
|
| 151 |
-
ACTIVE=0
|
| 152 |
-
fi
|
| 153 |
-
done < "$TMP_NAMES"
|
| 154 |
-
|
| 155 |
-
# Wait for any remaining background jobs
|
| 156 |
-
wait
|
| 157 |
-
|
| 158 |
-
# --- Collect results -------------------------------------------------------
|
| 159 |
-
DELETED=0
|
| 160 |
-
ERRORS=0
|
| 161 |
-
DETAIL_LINES=""
|
| 162 |
-
ERROR_LINES=""
|
| 163 |
-
|
| 164 |
-
# Re-read the names file to preserve status info for the notification
|
| 165 |
-
while IFS= read -r NAME; do
|
| 166 |
-
[ -z "$NAME" ] && continue
|
| 167 |
-
RESULT_FILE="${TMP_DIR}/$(printf '%s' "$NAME" | md5sum | cut -d' ' -f1).result"
|
| 168 |
-
RESULT=$(cat "$RESULT_FILE" 2>/dev/null)
|
| 169 |
-
|
| 170 |
-
case "$RESULT" in
|
| 171 |
-
OK*)
|
| 172 |
-
STATUS=$(jq -r --arg n "$NAME" '.files[] | select(.name==$n) | .status // "unknown"' "$TMP_FILE")
|
| 173 |
-
echo "${LOG_PREFIX} DELETED: ${NAME}"
|
| 174 |
-
DELETED=$((DELETED + 1))
|
| 175 |
-
DETAIL_LINES="${DETAIL_LINES} • ${NAME} [${STATUS}]\n"
|
| 176 |
-
;;
|
| 177 |
-
ERR*)
|
| 178 |
-
HTTP_ST=$(echo "$RESULT" | awk '{print $3}')
|
| 179 |
-
echo "${LOG_PREFIX} ERROR deleting ${NAME} (HTTP ${HTTP_ST})"
|
| 180 |
-
ERRORS=$((ERRORS + 1))
|
| 181 |
-
ERROR_LINES="${ERROR_LINES} • ${NAME} (HTTP ${HTTP_ST})\n"
|
| 182 |
-
;;
|
| 183 |
-
*)
|
| 184 |
-
echo "${LOG_PREFIX} WARNING: No result for ${NAME}"
|
| 185 |
-
ERRORS=$((ERRORS + 1))
|
| 186 |
-
ERROR_LINES="${ERROR_LINES} • ${NAME} (no result)\n"
|
| 187 |
-
;;
|
| 188 |
-
esac
|
| 189 |
-
done < "$TMP_NAMES"
|
| 190 |
-
|
| 191 |
-
echo "${LOG_PREFIX} Done. Deleted: ${DELETED}, Errors: ${ERRORS}"
|
| 192 |
-
|
| 193 |
-
# --- Feishu notification ---------------------------------------------------
|
| 194 |
-
# printf to interpret \n in the accumulated lines
|
| 195 |
-
DETAIL_LINES=$(printf '%b' "$DETAIL_LINES")
|
| 196 |
-
ERROR_LINES=$(printf '%b' "$ERROR_LINES")
|
| 197 |
-
|
| 198 |
-
if [ "$ERRORS" -gt 0 ]; then
|
| 199 |
-
notify_feishu "⚠️ Token 清理完成(有错误)" "[TIME] ${TIMESTAMP}
|
| 200 |
-
|
| 201 |
-
[STATS] 共 ${TOTAL} 个 token,发现 ${TO_DELETE} 个失效
|
| 202 |
-
|
| 203 |
-
────────────────────
|
| 204 |
-
[SUCCESS] 已删除 ${DELETED} 个:
|
| 205 |
-
${DETAIL_LINES}
|
| 206 |
-
[FAIL] 删除失败 ${ERRORS} 个:
|
| 207 |
-
${ERROR_LINES}────────────────────"
|
| 208 |
-
else
|
| 209 |
-
notify_feishu "🧹 Token 清理完成" "[TIME] ${TIMESTAMP}
|
| 210 |
-
|
| 211 |
-
[STATS] 共 ${TOTAL} 个 token,清理 ${DELETED} 个失效
|
| 212 |
-
|
| 213 |
-
────────────────────
|
| 214 |
-
[REMOVED] 已删除:
|
| 215 |
-
${DETAIL_LINES}────────────────────
|
| 216 |
-
[RESULT] Success: ${DELETED}/${TO_DELETE} | Failed: 0/${TO_DELETE}"
|
| 217 |
-
fi
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
config.yaml
CHANGED
|
@@ -1,6 +1,8 @@
|
|
| 1 |
host: "0.0.0.0"
|
| 2 |
port: 7860
|
| 3 |
|
|
|
|
|
|
|
| 4 |
auth-dir: "/tmp/.cli-proxy-api"
|
| 5 |
logging-to-file: true
|
| 6 |
logs-dir: "/tmp/logs"
|
|
@@ -10,4 +12,4 @@ remote-management:
|
|
| 10 |
secret-key: "${MANAGEMENT_PASSWORD}"
|
| 11 |
|
| 12 |
commercial-mode: true
|
| 13 |
-
debug: false
|
|
|
|
| 1 |
host: "0.0.0.0"
|
| 2 |
port: 7860
|
| 3 |
|
| 4 |
+
pgstore-dsn: "${PGSTORE_DSN}"
|
| 5 |
+
|
| 6 |
auth-dir: "/tmp/.cli-proxy-api"
|
| 7 |
logging-to-file: true
|
| 8 |
logs-dir: "/tmp/logs"
|
|
|
|
| 12 |
secret-key: "${MANAGEMENT_PASSWORD}"
|
| 13 |
|
| 14 |
commercial-mode: true
|
| 15 |
+
debug: false
|
entrypoint.sh
DELETED
|
@@ -1,54 +0,0 @@
|
|
| 1 |
-
#!/bin/sh
|
| 2 |
-
#
|
| 3 |
-
# entrypoint.sh - Start crond for scheduled cleanup, then launch CLIProxyAPI
|
| 4 |
-
#
|
| 5 |
-
# Environment variables:
|
| 6 |
-
# CLEANUP_CRON_SCHEDULE - Cron schedule for token cleanup (default: "0 */6 * * *", every 6h)
|
| 7 |
-
# CLEANUP_STARTUP_DELAY - Seconds to wait after service is ready before first cleanup (default: 10)
|
| 8 |
-
#
|
| 9 |
-
|
| 10 |
-
echo "[entrypoint] Starting up at $(date)"
|
| 11 |
-
|
| 12 |
-
CLEANUP_CRON_SCHEDULE="${CLEANUP_CRON_SCHEDULE:-0 */6 * * *}"
|
| 13 |
-
CLEANUP_STARTUP_DELAY="${CLEANUP_STARTUP_DELAY:-10}"
|
| 14 |
-
CRON_OK=0
|
| 15 |
-
|
| 16 |
-
mkdir -p /tmp/logs /tmp/crontabs
|
| 17 |
-
|
| 18 |
-
# Write crontab to /tmp (HuggingFace root filesystem is read-only)
|
| 19 |
-
cat > /tmp/crontabs/root << EOF
|
| 20 |
-
${CLEANUP_CRON_SCHEDULE} /app/cleanup_tokens.sh >> /tmp/logs/cleanup.log 2>&1
|
| 21 |
-
EOF
|
| 22 |
-
|
| 23 |
-
echo "[entrypoint] Cleanup schedule: ${CLEANUP_CRON_SCHEDULE}"
|
| 24 |
-
|
| 25 |
-
# Try crond; if it fails, fall back to a sleep loop
|
| 26 |
-
if crond -c /tmp/crontabs -l 8 -L /tmp/logs/crond.log 2>/dev/null; then
|
| 27 |
-
echo "[entrypoint] crond started"
|
| 28 |
-
CRON_OK=1
|
| 29 |
-
else
|
| 30 |
-
echo "[entrypoint] WARNING: crond failed, will use sleep-loop fallback"
|
| 31 |
-
fi
|
| 32 |
-
|
| 33 |
-
# Background: wait for API ready, run initial cleanup, then optionally loop
|
| 34 |
-
(
|
| 35 |
-
MAX_WAIT=120
|
| 36 |
-
WAITED=0
|
| 37 |
-
while ! wget -q -O /dev/null http://localhost:7860/ 2>/dev/null; do
|
| 38 |
-
sleep 2
|
| 39 |
-
WAITED=$((WAITED + 2))
|
| 40 |
-
[ "$WAITED" -ge "$MAX_WAIT" ] && break
|
| 41 |
-
done
|
| 42 |
-
sleep "${CLEANUP_STARTUP_DELAY}"
|
| 43 |
-
/app/cleanup_tokens.sh >> /tmp/logs/cleanup.log 2>&1
|
| 44 |
-
|
| 45 |
-
if [ "$CRON_OK" = "0" ]; then
|
| 46 |
-
while true; do
|
| 47 |
-
sleep 21600
|
| 48 |
-
/app/cleanup_tokens.sh >> /tmp/logs/cleanup.log 2>&1
|
| 49 |
-
done
|
| 50 |
-
fi
|
| 51 |
-
) &
|
| 52 |
-
|
| 53 |
-
echo "[entrypoint] Launching cli-proxy-api..."
|
| 54 |
-
exec /app/cli-proxy-api --config /app/config.yaml "$@"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|