feat: 添加 Netlify 徽章并引入贡献者公约

- 在页面底部添加 Netlify 部署徽章,包含悬停效果和无障碍访问支持
- 创建 CODE_OF_CONDUCT.md 文件,采用 Contributor Covenant 3.0 行为准则
- 更新英文 README 文件,添加行为准则链接说明
- 删除不再使用的 runtime-common.sh 脚本文件,简化项目结构
This commit is contained in:
Abner
2026-05-19 10:10:18 +08:00
parent ab03025ff2
commit 500140cd1b
5 changed files with 151 additions and 817 deletions

92
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,92 @@
# Contributor Covenant 3.0 Code of Conduct
## Our Pledge
We pledge to make our community welcoming, safe, and equitable for all.
We are committed to fostering an environment that respects and promotes the dignity, rights, and contributions of all individuals, regardless of characteristics including race, ethnicity, caste, color, age, physical characteristics, neurodiversity, disability, sex or gender, gender identity or expression, sexual orientation, language, philosophy or religion, national or social origin, socio-economic position, level of education, or other status. The same privileges of participation are extended to everyone who participates in good faith and in accordance with this Covenant.
## Encouraged Behaviors
While acknowledging differences in social norms, we all strive to meet our community's expectations for positive behavior. We also understand that our words and actions may be interpreted differently than we intend based on culture, background, or native language.
With these considerations in mind, we agree to behave mindfully toward each other and act in ways that center our shared values, including:
1. Respecting the **purpose of our community**, our activities, and our ways of gathering.
2. Engaging **kindly and honestly** with others.
3. Respecting **different viewpoints** and experiences.
4. **Taking responsibility** for our actions and contributions.
5. Gracefully giving and accepting **constructive feedback**.
6. Committing to **repairing harm** when it occurs.
7. Behaving in other ways that promote and sustain the **well-being of our community**.
## Restricted Behaviors
We agree to restrict the following behaviors in our community. Instances, threats, and promotion of these behaviors are violations of this Code of Conduct.
1. **Harassment.** Violating explicitly expressed boundaries or engaging in unnecessary personal attention after any clear request to stop.
2. **Character attacks.** Making insulting, demeaning, or pejorative comments directed at a community member or group of people.
3. **Stereotyping or discrimination.** Characterizing anyones personality or behavior on the basis of immutable identities or traits.
4. **Sexualization.** Behaving in a way that would generally be considered inappropriately intimate in the context or purpose of the community.
5. **Violating confidentiality**. Sharing or acting on someone's personal or private information without their permission.
6. **Endangerment.** Causing, encouraging, or threatening violence or other harm toward any person or group.
7. Behaving in other ways that **threaten the well-being** of our community.
### Other Restrictions
1. **Misleading identity.** Impersonating someone else for any reason, or pretending to be someone else to evade enforcement actions.
2. **Failing to credit sources.** Not properly crediting the sources of content you contribute.
3. **Promotional materials**. Sharing marketing or other commercial content in a way that is outside the norms of the community.
4. **Irresponsible communication.** Failing to responsibly present content which includes, links or describes any other restricted behaviors.
## Reporting an Issue
Tensions can occur between community members even when they are trying their best to collaborate. Not every conflict represents a code of conduct violation, and this Code of Conduct reinforces encouraged behaviors and norms that can help avoid conflicts and minimize harm.
When an incident does occur, it is important to report it promptly. To report a possible violation, please [open an issue](https://github.com/Silentely/esim-tools/issues) in this repository or contact the maintainers privately via email at Abner@cosr.eu.org.
Community Moderators take reports of violations seriously and will make every effort to respond in a timely manner. They will investigate all reports of code of conduct violations, reviewing messages, logs, and recordings, or interviewing witnesses and other participants. Community Moderators will keep investigation and enforcement actions as transparent as possible while prioritizing safety and confidentiality. In order to honor these values, enforcement actions are carried out in private with the involved parties, but communicating to the whole community may be part of a mutually agreed upon resolution.
## Addressing and Repairing Harm
If an investigation by the Community Moderators finds that this Code of Conduct has been violated, the following enforcement ladder may be used to determine how best to repair harm, based on the incident's impact on the individuals involved and the community as a whole. Depending on the severity of a violation, lower rungs on the ladder may be skipped.
1) **Warning**
- **Event**: A violation involving a single incident or series of incidents.
- **Consequence**: A private, written warning from the Community Moderators.
- **Repair**: Examples of repair include a private written apology, acknowledgement of responsibility, and seeking clarification on expectations.
2) **Temporarily Limited Activities**
- **Event**: A repeated incidence of a violation that previously resulted in a warning, or the first incidence of a more serious violation.
- **Consequence**: A private, written warning with a time-limited cooldown period designed to underscore the seriousness of the situation and give the community members involved time to process the incident. The cooldown period may be limited to particular communication channels or interactions with particular community members.
- **Repair**: Examples of repair may include making an apology, using the cooldown period to reflect on actions and impact, and being thoughtful about re-entering community spaces after the period is over.
3) **Temporary Suspension**
- **Event**: A pattern of repeated violation which the Community Moderators have tried to address with warnings, or a single serious violation.
- **Consequence**: A private written warning with conditions for return from suspension. In general, temporary suspensions give the person being suspended time to reflect upon their behavior and possible corrective actions.
- **Repair**: Examples of repair include respecting the spirit of the suspension, meeting the specified conditions for return, and being thoughtful about how to reintegrate with the community when the suspension is lifted.
4) **Permanent Ban**
- **Event**: A pattern of repeated code of conduct violations that other steps on the ladder have failed to resolve, or a violation so serious that the Community Moderators determine there is no way to keep the community safe with this person as a member.
- **Consequence**: Access to all community spaces, tools, and communication channels is removed. In general, permanent bans should be rarely used, should have strong reasoning behind them, and should only be resorted to if working through other remedies has failed to change the behavior.
- **Repair**: There is no possible repair in cases of this severity.
This enforcement ladder is intended as a guideline. It does not limit the ability of Community Managers to use their discretion and judgment, in keeping with the best interests of our community.
## Scope
This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public or other spaces. Examples of representing our community include using an official email address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
## Attribution
This Code of Conduct is adapted from the Contributor Covenant, version 3.0, permanently available at [https://www.contributor-covenant.org/version/3/0/](https://www.contributor-covenant.org/version/3/0/).
Contributor Covenant is stewarded by the Organization for Ethical Source and licensed under CC BY-SA 4.0. To view a copy of this license, visit [https://creativecommons.org/licenses/by-sa/4.0/](https://creativecommons.org/licenses/by-sa/4.0/)
For answers to common questions about Contributor Covenant, see the FAQ at [https://www.contributor-covenant.org/faq](https://www.contributor-covenant.org/faq). Translations are provided at [https://www.contributor-covenant.org/translations](https://www.contributor-covenant.org/translations). Additional enforcement and community guideline resources can be found at [https://www.contributor-covenant.org/resources](https://www.contributor-covenant.org/resources). The enforcement ladder was inspired by the work of [Mozillas code of conduct team](https://github.com/mozilla/inclusion).

View File

@@ -311,6 +311,10 @@ open tests/test_simyo_esim.html
![](https://starchart.cc/Silentely/eSIM-Tools.svg)
## 贡献者公约
本项目遵循[贡献者公约](CODE_OF_CONDUCT.md)行为准则。
## 📄 许可证
- 本项目的所有代码除另有说明外,均按照 [MIT License](LICENSE) 发布。

View File

@@ -317,6 +317,10 @@ If you encounter issues or have suggestions:
![](https://starchart.cc/Silentely/eSIM-Tools.svg)
## Code of Conduct
This project follows the [Contributor Covenant Code of Conduct](CODE_OF_CONDUCT.md).
## 📄 License
- All code in this project, unless otherwise stated, is released under the [MIT License](LICENSE).

View File

@@ -1,808 +0,0 @@
#!/bin/bash
#
# Runtime common functions for fuckits
# Shared between main.sh (EN) and zh_main.sh (ZH)
# This file is sourced at runtime, not during build
#
_fuck_json_escape() {
local input="$1"
if command -v python3 >/dev/null 2>&1; then
printf '%s' "$input" | python3 -c "import json,sys; print(json.dumps(sys.stdin.read())[1:-1], end='')"
return
fi
if command -v node >/dev/null 2>&1; then
printf '%s' "$input" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>process.stdout.write(JSON.stringify(d).slice(1,-1)))"
return
fi
# Minimal fallback: escape backslashes and double quotes only
printf '%s' "$input" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -e 's/ /\\t/g'
}
_fuck_truthy() {
local value="${1:-}"
local normalized
normalized=$(printf '%s' "$value" | tr '[:upper:]' '[:lower:]')
case "$normalized" in
1|true|yes|y|on|||)
return 0
;;
*)
return 1
;;
esac
}
_fuck_security_level_value() {
case "$1" in
block) printf '3\n' ;;
challenge) printf '2\n' ;;
warn) printf '1\n' ;;
*) printf '0\n' ;;
esac
}
_fuck_security_match_rule() {
local command="$1"
local table="$2"
local -a rules=()
# SECURITY NOTE: This eval is SAFE because:
# - $table only receives hardcoded internal array names from callers within this script
# - Valid values: _FUCK_SECURITY_BLOCK_RULES, _FUCK_SECURITY_CHALLENGE_RULES, _FUCK_SECURITY_WARN_RULES
# - No user input can reach this variable; it's purely for dynamic array name resolution
# - This pattern is a standard Bash idiom for indirect array access (Bash 3.x compatible)
eval "rules=(\"\${${table}[@]}\")"
local rule pattern reason
for rule in "${rules[@]}"; do
pattern=${rule%%|||*}
reason=${rule#*|||}
[ -z "$pattern" ] && continue
if printf '%s' "$command" | grep -Eiq -- "$pattern"; then
printf '%s\n' "$reason"
return 0
fi
done
return 1
}
_fuck_security_is_whitelisted() {
local command="$1"
local whitelist="${FUCK_SECURITY_WHITELIST:-}"
if [ -z "$whitelist" ]; then
return 1
fi
local normalized entry
normalized=$(printf '%s' "$whitelist" | tr ',' '\n')
while IFS= read -r entry; do
entry=$(printf '%s' "$entry" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
[ -z "$entry" ] && continue
# 前缀匹配:命令必须以白名单条目开头(或完全相等)
# 例如白名单 "ls" 匹配 "ls -la" 但不匹配 "vls"
if [[ "$command" == "$entry" ]] || [[ "$command" == "$entry "* ]]; then
return 0
fi
done <<< "$normalized"
return 1
}
_fuck_security_mode() {
local mode="${FUCK_SECURITY_MODE:-balanced}"
mode=$(printf '%s' "$mode" | tr '[:upper:]' '[:lower:]')
case "$mode" in
strict) printf 'strict\n' ;;
off|disabled|none) printf 'off\n' ;;
balanced|default|"") printf 'balanced\n' ;;
*) printf 'balanced\n' ;;
esac
}
_fuck_mark_static_cache_dirty() {
_FUCK_STATIC_CACHE_DIRTY=1
}
_fuck_load_static_cache() {
# Return early if cache is already loaded
if [ "${_FUCK_STATIC_CACHE_LOADED:-0}" -eq 1 ]; then
return 0
fi
_FUCK_STATIC_CACHE_LOADED=1
# Source cache file if it exists
if [ -f "$FUCK_SYSINFO_CACHE_FILE" ]; then
# shellcheck disable=SC1090
source "$FUCK_SYSINFO_CACHE_FILE" || true
fi
}
_fuck_persist_static_cache() {
# Only persist if cache is dirty
if [ "${_FUCK_STATIC_CACHE_DIRTY:-0}" -ne 1 ]; then
return 0
fi
# Ensure cache directory exists
local cache_dir
cache_dir=$(dirname "$FUCK_SYSINFO_CACHE_FILE")
if ! mkdir -p "$cache_dir" 2>/dev/null; then
return 1
fi
# Create temporary file for atomic write
local tmp_file
tmp_file=$(mktemp) || return 1
# Write cached variables to temporary file
{
printf '_FUCK_CACHED_DISTRO=%q\\n' "${_FUCK_CACHED_DISTRO:-}"
printf '_FUCK_CACHED_KERNEL=%q\\n' "${_FUCK_CACHED_KERNEL:-}"
printf '_FUCK_CACHED_ARCH=%q\\n' "${_FUCK_CACHED_ARCH:-}"
printf '_FUCK_CACHED_PKG_MANAGER=%q\\n' "${_FUCK_CACHED_PKG_MANAGER:-}"
} > "$tmp_file"
# Atomic move to final location
if command mv -f -- "$tmp_file" "$FUCK_SYSINFO_CACHE_FILE" 2>/dev/null; then
_FUCK_STATIC_CACHE_DIRTY=0
return 0
else
# Clean up temporary file on failure
rm -f "$tmp_file"
return 1
fi
}
_fuck_audit_log() {
# Check if audit logging is enabled
if [ "${FUCK_AUDIT_LOG:-false}" != "true" ]; then
return 0
fi
local event="$1"
local command="$2"
local exit_code="${3:--}"
local timestamp
timestamp=$(date -u '+%Y-%m-%d %H:%M:%S UTC' 2>/dev/null || date '+%Y-%m-%d %H:%M:%S')
local log_file="${FUCK_AUDIT_LOG_FILE:-$INSTALL_DIR/.audit.log}"
# Ensure log directory exists
mkdir -p "$(dirname "$log_file")" 2>/dev/null || true
# Sanitize command for logging (normalize newlines, escape delimiter, limit length)
local sanitized_cmd
local raw_len=${#command}
sanitized_cmd=$(printf '%s' "$command" | tr '\r\n' ' ' | sed 's/|/\\|/g' | head -c 200)
if [ "$raw_len" -gt 200 ]; then
sanitized_cmd="${sanitized_cmd}..."
fi
# Write to log file (format: timestamp|user|event|exit_code|command)
printf '%s|%s|%s|%s|%s\n' "${timestamp}" "${USER:-unknown}" "${event}" "${exit_code}" "${sanitized_cmd}" >> "$log_file" 2>/dev/null || true
# Secure the log file
chmod 600 "$log_file" 2>/dev/null || true
}
_fuck_detect_distro() {
_fuck_load_static_cache
# Return cached value if available
if [ -n "${_FUCK_CACHED_DISTRO:-}" ]; then
printf '%s\n' "$_FUCK_CACHED_DISTRO"
return 0
fi
local kernel_name distro id version pretty family
kernel_name=$(uname -s 2>/dev/null || printf 'unknown')
distro="unknown"
# macOS detection
if [ "$kernel_name" = "Darwin" ]; then
local product version
product=$(sw_vers -productName 2>/dev/null || printf 'macOS')
product=$(printf '%s' "$product" | tr -d '\r\n')
version=$(sw_vers -productVersion 2>/dev/null || printf 'unknown')
version=$(printf '%s' "$version" | tr -d '\r\n')
distro="$product $version"
# Linux detection using /etc/os-release
elif [ -r /etc/os-release ]; then
id=$(grep -E '^ID=' /etc/os-release | head -n1 | cut -d= -f2 | tr -d '"' | tr '[:upper:]' '[:lower:]')
version=$(grep -E '^VERSION_ID=' /etc/os-release | head -n1 | cut -d= -f2 | tr -d '"')
pretty=$(grep -E '^PRETTY_NAME=' /etc/os-release | head -n1 | cut -d= -f2- | tr -d '"')
# Determine OS family for better categorization
family=""
case "$id" in
ubuntu|debian)
family="Debian-based"
;;
centos|rhel|rocky|almalinux|fedora)
family="RHEL-based"
;;
arch|manjaro|endeavouros)
family="Arch-based"
;;
esac
# Format distribution string with family and version
if [ -n "$family" ]; then
distro="$family ${version:-}"
if [ -n "$pretty" ]; then
distro="$distro (${pretty})"
fi
else
distro="${pretty:-Linux $version}"
fi
else
distro="$kernel_name"
fi
# Cache and return result
_FUCK_CACHED_DISTRO="$distro"
_fuck_mark_static_cache_dirty
printf '%s\n' "$distro"
}
_fuck_detect_pkg_manager() {
_fuck_load_static_cache
# Return cached value if available
if [ -n "${_FUCK_CACHED_PKG_MANAGER:-}" ]; then
printf '%s\n' "$_FUCK_CACHED_PKG_MANAGER"
return 0
fi
local manager="unknown"
# Detect package manager in order of preference
if command -v apt-get &> /dev/null; then
manager="apt"
elif command -v yum &> /dev/null; then
manager="yum"
elif command -v dnf &> /dev/null; then
manager="dnf"
elif command -v pacman &> /dev/null; then
manager="pacman"
elif command -v zypper &> /dev/null; then
manager="zypper"
elif command -v brew &> /dev/null; then
manager="brew"
fi
# Cache and return result
_FUCK_CACHED_PKG_MANAGER="$manager"
_fuck_mark_static_cache_dirty
printf '%s\n' "$manager"
}
_fuck_get_architecture() {
_fuck_load_static_cache
# Return cached value if available
if [ -n "${_FUCK_CACHED_ARCH:-}" ]; then
printf '%s\n' "$_FUCK_CACHED_ARCH"
return 0
fi
local arch
arch=$(uname -m 2>/dev/null || printf 'unknown')
arch=$(printf '%s' "$arch" | tr -d '\r\n')
# Cache and return result
_FUCK_CACHED_ARCH="$arch"
_fuck_mark_static_cache_dirty
printf '%s\n' "$arch"
}
_fuck_get_kernel_version() {
_fuck_load_static_cache
# Return cached value if available
if [ -n "${_FUCK_CACHED_KERNEL:-}" ]; then
printf '%s\n' "$_FUCK_CACHED_KERNEL"
return 0
fi
local kernel
kernel=$(uname -sr 2>/dev/null || uname -s 2>/dev/null || printf 'unknown')
kernel=$(printf '%s' "$kernel" | tr -d '\r\n')
# Cache and return result
_FUCK_CACHED_KERNEL="$kernel"
_fuck_mark_static_cache_dirty
printf '%s\n' "$kernel"
}
_fuck_collect_tool_versions() {
local tools tool version result
tools="git docker npm kubectl curl wget"
result=""
for tool in $tools; do
version="not-installed"
if command -v "$tool" >/dev/null 2>&1; then
case "$tool" in
git|docker|curl|wget)
version=$("$tool" --version 2>/dev/null | head -n1)
;;
npm)
version=$("$tool" --version 2>/dev/null | head -n1)
[ -n "$version" ] && version="npm $version"
;;
kubectl)
version=$("$tool" version --client --short 2>/dev/null | head -n1)
;;
esac
fi
# Clean up version string
version=$(printf '%s' "${version:-unknown}" | tr '\r\n' ' ' | tr -s ' ' | sed -e 's/^ *//' -e 's/ *$//')
[ -z "$version" ] && version="unknown"
result="$result$tool:$version; "
done
# Remove trailing semicolon and space
result="${result%; }"
printf '%s' "$result"
}
_fuck_append_config_hint() {
local key="$1"
local comment="$2"
local sample="$3"
local quoted="${4:-1}"
[ -f "$CONFIG_FILE" ] || return
if grep -Eq "^\\s*#?\\s*export\\s+$key" "$CONFIG_FILE"; then
return
fi
local assignment
if [ "$quoted" = "1" ]; then
assignment="# export $key=\"$sample\""
else
assignment="# export $key=$sample"
fi
{
printf '\n'
printf '# %s\n' "$comment"
printf '%s\n' "$assignment"
} >> "$CONFIG_FILE"
}
_fuck_define_aliases() {
local default_alias="fuck"
if ! _fuck_truthy "${FUCK_DISABLE_DEFAULT_ALIAS:-0}"; then
alias "$default_alias"='_fuck_execute_prompt'
fi
if [ -n "${FUCK_ALIAS:-}" ] && [ "$FUCK_ALIAS" != "$default_alias" ]; then
alias "$FUCK_ALIAS"='_fuck_execute_prompt'
fi
}
_fuck_detect_dangerous_command() {
_fuck_security_evaluate_command "$1"
}
_fuck_local_system_prompt() {
local sysinfo="$1"
if [ "$FUCKITS_LOCALE" = "zh" ]; then
printf '你是一个专业的 shell 命令生成器。用户会用自然语言描述他们想要完成的任务。你的任务是生成直接可执行的 shell 命令来完成用户的目标。
重要规则:
1. 用户输入是自然语言描述意图,不是命令参数。例如"列出目录"意思是执行 ls 命令,而不是 ls "列出目录"
2. 生成直接可执行的命令,不要生成带参数判断的脚本模板(如 if [ $# -eq 0 ]
3. 对于简单任务直接返回单条命令,复杂任务可以是多行脚本
4. 不要提供任何解释、注释、markdown 格式(比如 ```bash或 shebang例如 #!/bin/bash
示例:
- 用户说"列出目录" → 输出: ls
- 用户说"显示详细文件列表" → 输出: ls -la
- 用户说"查找大于10MB的文件" → 输出: find . -type f -size +10M
用户的系统信息是:%s' "$sysinfo"
else
printf 'You are an expert shell command generator. Users describe tasks in natural language. Your task is to generate directly executable shell commands to accomplish their goals.
Important rules:
1. User input is natural language intent, NOT command arguments. For example, "list directory" means run ls, not ls "list directory"
2. Generate directly executable commands, not script templates with parameter handling (like if [ $# -eq 0 ])
3. For simple tasks return single commands, complex tasks can be multi-line scripts
4. Do not provide any explanation, comments, markdown formatting (like ```bash), or a shebang (e.g., #!/bin/bash)
Examples:
- User says "list directory" → Output: ls
- User says "show detailed file list" → Output: ls -la
- User says "find files larger than 10MB" → Output: find . -type f -size +10M
The user'"'"'s system info is: %s' "$sysinfo"
fi
}
_fuck_secure_config_file() {
if [ -f "$CONFIG_FILE" ]; then
chmod 600 "$CONFIG_FILE" 2>/dev/null || true
fi
}
_fuck_security_apply_mode() {
local mode="$1"
local severity="$2"
case "$mode" in
strict)
case "$severity" in
warn) severity="challenge" ;;
challenge) severity="block" ;;
esac
;;
esac
printf '%s\n' "$severity"
}
_fuck_security_prompt_phrase() {
local phrase="$1"
local input=""
printf "%b> %b" "$C_BOLD" "$C_RESET" >&2
if [ -r /dev/tty ]; then
if ! IFS= read -r input < /dev/tty; then
printf "\n" >&2
return 1
fi
else
if ! IFS= read -r input; then
printf "\n" >&2
return 1
fi
fi
printf "\n" >&2
[ "$input" = "$phrase" ]
}
_fuck_security_promote() {
local current="$1"
local candidate="$2"
local current_val candidate_val
current_val=$(_fuck_security_level_value "$current")
candidate_val=$(_fuck_security_level_value "$candidate")
if [ "$candidate_val" -gt "$current_val" ]; then
printf '%s\n' "$candidate"
else
printf '%s\n' "$current"
fi
}
_fuck_should_use_local_api() {
if [ -n "${FUCK_OPENAI_API_KEY:-}" ]; then
return 0
fi
return 1
}
# --- 以下为共享工具函数(从 main.sh/zh_main.sh 提取) ---
# 安全验证配置文件,防止代码注入
# 参数:$1 - 要验证的文件路径
# 返回0 如果安全1 如果不安全或出错
_fuck_validate_config_file() {
local file="$1"
# 文件必须存在且可读
if [ ! -f "$file" ] || [ ! -r "$file" ]; then
return 1
fi
# 检查文件权限 - 必须由当前用户拥有
if [ "$(stat -c '%u' "$file" 2>/dev/null || stat -f '%u' "$file" 2>/dev/null)" != "$(id -u)" ]; then
echo -e "${C_RED}Config file not owned by current user, refusing to source.${C_RESET}" >&2
return 1
fi
local line_num=0
local line
# 预定义正则表达式变量zsh 兼容性:变量形式避免特殊字符解析问题)
local re_cmd_sub='\$\('
local re_backtick='`'
local re_arith='\$\(\('
local re_semi=';'
local re_and='&&'
local re_or='\|\|'
local re_pipe='\|'
local re_gt='>'
local re_lt='<'
local re_amp='&'
local re_esc='\\\$'
while IFS= read -r line || [ -n "$line" ]; do
line_num=$((line_num + 1))
# 跳过空行
if [ -z "$line" ] || [[ "$line" =~ ^[[:space:]]*$ ]]; then
continue
fi
# 跳过注释行(以 # 开头,可有前导空白)
if [[ "$line" =~ ^[[:space:]]*# ]]; then
continue
fi
# 检查危险的 shell 元字符和命令替换
# 拒绝:$(), ``, $((), ;, &&, ||, | (管道), >, <, &, 换行转义
if [[ "$line" =~ $re_cmd_sub ]] || \
[[ "$line" =~ $re_backtick ]] || \
[[ "$line" =~ $re_arith ]] || \
[[ "$line" =~ $re_semi ]] || \
[[ "$line" =~ $re_and ]] || \
[[ "$line" =~ $re_or ]] || \
[[ "$line" =~ $re_pipe ]] || \
[[ "$line" =~ $re_gt ]] || \
[[ "$line" =~ $re_lt ]] || \
[[ "$line" =~ $re_amp ]] || \
[[ "$line" =~ $re_esc ]]; then
_fuck_debug "Config validation failed at line $line_num: dangerous metacharacter detected"
echo -e "${C_RED}Unsafe config file: dangerous shell metacharacter at line $line_num${C_RESET}" >&2
return 1
fi
# 只允许export FUCK_*=... 或 FUCK_*=...(可有前导空白)
# 也允许export FUCKITS_LOCALE=...
if [[ "$line" =~ ^[[:space:]]*(export[[:space:]]+)?(FUCK_[A-Z_]+|FUCKITS_LOCALE)= ]]; then
continue
fi
# 拒绝其他任何内容
_fuck_debug "Config validation failed at line $line_num: unrecognized pattern"
echo -e "${C_RED}Unsafe config file: unrecognized pattern at line $line_num${C_RESET}" >&2
return 1
done < "$file"
return 0
}
# 验证后安全地加载配置文件
# 参数:$1 - 要加载的文件路径
# 返回0 如果成功加载1 如果验证失败或文件不存在
_fuck_safe_source_config() {
local file="$1"
if [ ! -f "$file" ]; then
return 0 # 没有文件也没关系
fi
if _fuck_validate_config_file "$file"; then
# shellcheck disable=SC1090
source "$file"
return $?
else
echo -e "${C_YELLOW}Config file validation failed, skipping: $file${C_RESET}" >&2
return 1
fi
}
# 找用户 shell 配置文件的辅助函数
_installer_detect_profile() {
if [ -n "${SHELL:-}" ] && echo "$SHELL" | grep -q "zsh"; then
echo "$HOME/.zshrc"
elif [ -n "${SHELL:-}" ] && echo "$SHELL" | grep -q "bash"; then
echo "$HOME/.bashrc"
elif [ -f "$HOME/.profile" ]; then
# 兼容 sh, ksh 等
echo "$HOME/.profile"
elif [ -f "$HOME/.zshrc" ]; then
# SHELL 变量没设置时的备用方案
echo "$HOME/.zshrc"
elif [ -f "$HOME/.bashrc" ]; then
# SHELL 变量没设置时的备用方案
echo "$HOME/.bashrc"
else
echo "unknown_profile"
fi
}
# 收集用户信息包括权限级别
# 输出:用户信息字符串(如:"User=john uid=1000 level=sudoer Groups=john adm sudo"
_fuck_collect_user_info() {
local current_user uid groups level
current_user="${USER:-}"
# 如果 USER 未设置则使用备用方法
if [ -z "$current_user" ]; then
current_user=$(whoami 2>/dev/null || printf 'unknown')
fi
# 如果 id 命令可用则获取 UID 和用户组
uid="unknown"
groups="unknown"
if command -v id >/dev/null 2>&1; then
uid=$(id -u "$current_user" 2>/dev/null || id -u 2>/dev/null || printf 'unknown')
groups=$(id -Gn "$current_user" 2>/dev/null || id -Gn 2>/dev/null || printf 'unknown')
fi
# 确定权限级别
level="user"
if [ "$uid" = "0" ]; then
level="root"
elif printf '%s' "$groups" | grep -Eq '(^|[[:space:]])(sudo|wheel|admin)([[:space:]]|$)'; then
level="sudoer"
fi
printf 'User=%s uid=%s level=%s Groups=%s' "$current_user" "$uid" "$level" "$groups"
}
# 从 JSON 文件中提取命令内容
# 参数:$1 - JSON 文件路径
# 返回:命令内容到 stdout
_fuck_extract_command_from_json() {
local json_file="$1"
if command -v python3 >/dev/null 2>&1; then
python3 - "$json_file" <<'PY'
import json, sys
path = sys.argv[1]
with open(path, 'r', encoding='utf-8') as f:
data = json.load(f)
choices = data.get('choices') or []
if not choices:
sys.exit(1)
message = choices[0].get('message') or {}
content = (message.get('content') or '').strip()
if not content:
sys.exit(1)
print(content)
PY
return
fi
if command -v node >/dev/null 2>&1; then
node - "$json_file" <<'JS'
const fs = require('fs');
const path = process.argv[1];
const data = JSON.parse(fs.readFileSync(path, 'utf8'));
const choice = (data.choices && data.choices[0]) || {};
const message = choice.message || {};
const content = (message.content || '').trim();
if (!content) {
process.exit(1);
}
console.log(content);
JS
return
fi
echo -e "${C_RED}Cannot parse AI response: neither python3 nor node is available.${C_RESET}" >&2
return 1
}
# Spinner 加载动画
# 参数:$1 - 进程 PID$2 - 可选的前缀文本
_fuck_spinner() {
local pid=$1
local prefix="${2:-}"
local delay=0.1
local -a frames=("|" "/" "-" "\\")
local frame_count=${#frames[@]}
local frame_idx=0
local has_prefix=0
if [ -n "$prefix" ]; then
has_prefix=1
fi
# 仅在 stderr 是终端时操作光标
if [ -t 2 ]; then
tput civis 2>/dev/null || printf "\033[?25l" >&2
fi
while kill -0 "$pid" 2>/dev/null; do
if [ "$has_prefix" -eq 1 ]; then
printf "\r%s%s" "$prefix" "${frames[$frame_idx]}"
else
printf " %s " "${frames[$frame_idx]}"
printf "\b\b\b"
fi
frame_idx=$(( (frame_idx + 1) % frame_count ))
sleep "$delay"
done
if [ "$has_prefix" -eq 1 ]; then
printf "\r%s" "$prefix"
tput el 2>/dev/null || printf "\033[K"
else
printf " \b\b\b"
fi
if [ -t 2 ]; then
tput cnorm 2>/dev/null || printf "\033[?25h" >&2
fi
}
# 初始化历史文件
# 参数:$1 - 历史文件路径(可选,默认 $INSTALL_DIR/history.json
_fuck_init_history_file() {
local history_file="${1:-$INSTALL_DIR/history.json}"
if [ ! -f "$history_file" ]; then
cat > "$history_file" <<'HISTORY_EOF'
{
"version": "1.0.0",
"commands": [],
"favorites": []
}
HISTORY_EOF
chmod 600 "$history_file"
fi
}
# 记录命令执行到历史
# 参数:$1 - 提示词,$2 - 命令,$3 - 退出码(可选),$4 - 耗时(可选)
_fuck_log_history() {
local prompt="$1"
local command="$2"
local exit_code="${3:-0}"
local duration="${4:-0}"
local history_file="$INSTALL_DIR/history.json"
# 检查 jq 是否可用
if ! command -v jq &> /dev/null; then
return 1
fi
# 初始化历史文件
_fuck_init_history_file "$history_file"
# 生成唯一 ID
local cmd_id="cmd_$(date +%s)_$$"
local timestamp
timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date -u +"%Y-%m-%dT%H:%M:%S" 2>/dev/null || echo "unknown")
# 创建条目
local entry
entry=$(jq -n \
--arg id "$cmd_id" \
--arg timestamp "$timestamp" \
--arg prompt "$prompt" \
--arg command "$command" \
--argjson exit_code "$exit_code" \
--argjson duration "$duration" \
'{
id: $id,
timestamp: $timestamp,
prompt: $prompt,
command: $command,
exitCode: $exit_code,
duration: $duration
}' 2>/dev/null)
if [ -z "$entry" ]; then
return 1
fi
# 追加到历史并限制为 1000 条(避免竞态条件)
local temp_file="${history_file}.tmp"
if jq ".commands += [$entry] | .commands |= .[-1000:]" "$history_file" > "$temp_file" 2>/dev/null; then
command mv -f -- "$temp_file" "$history_file"
chmod 600 "$history_file"
else
rm -f "$temp_file"
fi
}

View File

@@ -43,6 +43,38 @@ function bindContainerAria(container) {
onLocaleChange(apply);
}
function createNetlifyBadge() {
const link = document.createElement('a');
link.href = 'https://www.netlify.com';
link.target = '_blank';
link.rel = 'noopener noreferrer';
link.setAttribute('aria-label', 'Deploys by Netlify');
link.style.display = 'block';
link.style.textAlign = 'center';
link.style.marginTop = '16px';
const img = document.createElement('img');
img.src = 'https://www.netlify.com/assets/badges/netlify-badge-light.svg';
img.alt = 'Deploys by Netlify';
img.style.height = '26px';
img.style.opacity = '0.7';
img.style.transition = 'opacity 0.2s ease';
img.style.filter = 'grayscale(20%)';
link.addEventListener('mouseenter', () => {
img.style.opacity = '0.9';
img.style.filter = 'grayscale(0%)';
});
link.addEventListener('mouseleave', () => {
img.style.opacity = '0.7';
img.style.filter = 'grayscale(20%)';
});
link.appendChild(img);
return link;
}
function bindCopyright(span, options = {}) {
const custom = typeof options.text === 'string' ? options.text.trim() : '';
if (custom) {
@@ -69,12 +101,16 @@ export function injectFooter(options = {}) {
// 3) 最后退回到 body避免因 body 为 flex row 导致横排:因此仅最后兜底)
const createCopyrightNode = () => {
const div = document.createElement('div');
div.style.display = 'flex';
div.style.alignItems = 'center';
div.style.justifyContent = 'center';
div.style.gap = '12px';
div.style.marginTop = '8px';
const wrapper = document.createElement('div');
wrapper.style.textAlign = 'center';
wrapper.style.marginTop = '8px';
// 版权信息行
const copyrightRow = document.createElement('div');
copyrightRow.style.display = 'flex';
copyrightRow.style.alignItems = 'center';
copyrightRow.style.justifyContent = 'center';
copyrightRow.style.gap = '12px';
const span = document.createElement('span');
bindCopyright(span, options);
@@ -104,10 +140,16 @@ export function injectFooter(options = {}) {
githubLink.appendChild(githubIcon);
bindGithubLink(githubLink);
div.appendChild(span);
div.appendChild(githubLink);
copyrightRow.appendChild(span);
copyrightRow.appendChild(githubLink);
return div;
// Netlify 徽章行(放在版权信息下方)
const netlifyBadge = createNetlifyBadge();
wrapper.appendChild(copyrightRow);
wrapper.appendChild(netlifyBadge);
return wrapper;
};
// 情形 1已有站内 footer如首页免责声明区