使用说明
指令:
自动识别模式: [待分析文件] xy auto "问题"
一键诊断模式: xy diag ["问题"]
通用询问模式: [待分析的文件] xy ask "问题"
日志分析模式: [待分析的文件] xy log "问题"
错误排查模式: [待分析的文件] xy err "问题"
配置审查模式: [待分析的文件] xy conf "问题"
系统诊断模式: [待分析的文件] xy sys "问题"
系统帮助模式: xy help
说明:
- 进入粘贴信息模式:指令后加-p进入粘贴信息模式,粘贴信息后ctrl+d等待解答
- 系统提示词:修改User_Data内容
示例:
tail -n 200 /var/log/nginx/error.log | xy log "分析"
cat /etc/nginx/nginx.conf | xy conf "审查"
(free -h; df -h) | xy sys "诊断"
xy ask -p "问题"
环境变量:
LAI_API_BASE 默认 http://127.0.0.1:1234/v1
LAI_MODEL 默认 qwen2.5-7b
LAI_API_KEY 线上Key
LAI_MAX_TOKENS 默认 2048,输出强制限制
LAI_KEEP_CHARS 默认 20000,会自动把输入截断到最后
LAI_DEBUG=0 打印调试信息,0/1安装步骤
1.更新插件。
sudo apt update && sudo apt install -y curl jq2.新建脚本目录及脚本文件,这里假设为/bin/xy。
#!/usr/bin/env bash
set -euo pipefail
# ====== 配置区(可改 / 可用环境变量覆盖) ======
API_BASE="${LAI_API_BASE:-https://dashscope.aliyuncs.com/compatible-mode/v1}"
MODEL="${LAI_MODEL:-qwen3-max-preview}"
API_KEY="${LAI_API_KEY:-sk-abcdefghijk491db}"
MAX_TOKENS="${LAI_MAX_TOKENS:-2048}"
TEMPERATURE="${LAI_TEMPERATURE:-0.2}"
KEEP_CHARS="${LAI_KEEP_CHARS:-20000}"
DEBUG="${LAI_DEBUG:-0}"
DEFAULT_PASTE="${LAI_DEFAULT_PASTE:-0}"
# curl 超时/重试(避免网络卡死)
#多久连不上服务器才退出
CURL_CONNECT_TIMEOUT="${LAI_CONNECT_TIMEOUT:-30}"
#整次请求最多允许跑多久
CURL_MAX_TIME="${LAI_MAX_TIME:-300}"
#允许重复多少次
CURL_RETRY="${LAI_RETRY:-2}"
# =============================================
usage() {
cat <<'EOF'
指令:
自动识别模式: [待分析文件] xy auto "问题"
一键诊断模式: xy diag ["问题"]
通用询问模式: [待分析的文件] xy ask "问题"
日志分析模式: [待分析的文件] xy log "问题"
错误排查模式: [待分析的文件] xy err "问题"
配置审查模式: [待分析的文件] xy conf "问题"
系统诊断模式: [待分析的文件] xy sys "问题"
系统帮助模式: xy help
说明:
- 进入粘贴信息模式:指令后加-p进入粘贴信息模式,粘贴信息后ctrl+d等待解答
- 系统提示词:修改User_Data内容
示例:
tail -n 200 /var/log/nginx/error.log | xy log "分析"
cat /etc/nginx/nginx.conf | xy conf "审查"
(free -h; df -h) | xy sys "诊断"
xy ask -p "问题"
环境变量:
LAI_API_BASE 默认 http://127.0.0.1:1234/v1
LAI_MODEL 默认 qwen2.5-7b
LAI_API_KEY 线上Key
LAI_MAX_TOKENS 默认 2048,输出强制限制
LAI_KEEP_CHARS 默认 20000,会自动把输入截断到最后
LAI_DEBUG=0 打印调试信息,0/1
EOF
}
#检查依赖命令是否存在,否则退出。
need() { command -v "$1" >/dev/null 2>&1 || { echo "缺少依赖:$1" >&2; exit 1; }; }
#按字符数从尾部截断字符串。
truncate_tail() {
local s="${1-}" # 若 $1 不存在,用空字符串
local n="${2-0}" # 若 $2 不存在,用 0
local len="${#s}"
if (( len <= n )); then
printf "%s" "$s"
else
printf "%s" "${s:len-n:n}"
fi
}
#用 jq 将字符串安全转为 JSON 字符串(处理引号/换行)
json_escape() { jq -Rs '.' <<<"$1"; }
#DEBUG=1 时打印当前配置到 stderr
debug_log() {
[[ "$DEBUG" == "1" ]] || return 0
echo "[xy debug] API_BASE=$API_BASE" >&2
echo "[xy debug] MODEL=$MODEL" >&2
echo "[xy debug] MAX_TOKENS=$MAX_TOKENS KEEP_CHARS=$KEEP_CHARS TEMP=$TEMPERATURE" >&2
}
#读取输入材料(文件/管道/粘贴),并做截断
read_input_any() {
# 参数:file paste_flag
local file="${1:-}" paste_flag="${2:-0}"
local raw=""
if [[ -n "$file" ]]; then
raw="$(cat "$file")"
else
# 有管道/重定向:读 stdin
if [[ -t 0 ]]; then
# stdin 是终端:只有显式 paste 或 DEFAULT_PASTE 才读取
if [[ "$paste_flag" == "1" || "$DEFAULT_PASTE" == "1" ]]; then
raw="$(cat)"
else
raw=""
fi
else
raw="$(cat)"
fi
fi
truncate_tail "$raw" "$KEEP_CHARS"
}
#组装请求 JSON,调用接口,处理错误并输出结果
call_llm() {
local system="$1" user="$2"
local system_json user_json payload
system_json=$(json_escape "$system")
user_json=$(json_escape "$user")
payload=$(cat <<EOF
{
"model": "$(printf "%s" "$MODEL")",
"temperature": $TEMPERATURE,
"max_tokens": $MAX_TOKENS,
"messages": [
{"role":"system","content": $system_json},
{"role":"user","content": $user_json}
]
}
EOF
)
local auth_header=()
if [[ -n "$API_KEY" ]]; then auth_header=(-H "Authorization: Bearer $API_KEY"); fi
# 请求
local resp
resp="$(curl -sS \
--connect-timeout "$CURL_CONNECT_TIMEOUT" \
--max-time "$CURL_MAX_TIME" \
--retry "$CURL_RETRY" \
--retry-delay 1 \
--retry-all-errors \
"${API_BASE}/chat/completions" \
-H "Content-Type: application/json" \
"${auth_header[@]}" \
-d "$payload" || true)"
# 解析错误
local err
err="$(jq -r '.error.message // empty' <<<"$resp" 2>/dev/null || true)"
if [[ -n "$err" ]]; then
echo "接口返回错误:$err" >&2
# 尝试打印状态信息
echo "$resp" | jq -r '.' >&2 || true
exit 2
fi
# 正常输出
jq -r '.choices[0].message.content // .choices[0].text // empty' <<<"$resp"
}
#自动判断输入更像 err/conf/log/sys/ask 哪一种。
detect_mode_from_text() {
# 输入:raw(可能为空)
# 输出:ask/log/err/conf/sys
local raw="$1"
# 强错误特征
if grep -Eiq '(traceback|exception|panic|segfault|fatal|stack trace|permission denied|no such file|cannot|failed|error:|ERR_|E\d{3,})' <<<"$raw"; then
echo "err"; return 0
fi
# 配置特征
if grep -Eiq '(^\s*\[Unit\]|\bserver\s*\{|\bhttp\s*\{|\bevents\s*\{|apiVersion:|kind:|yaml:|^---$|^\s*listen\s+|^\s*location\s+|^\s*upstream\s+|^\s*proxy_pass\s+)' <<<"$raw"; then
echo "conf"; return 0
fi
# 日志特征:时间戳/level
if grep -Eiq '(\b(20[0-9]{2}-[01][0-9]-[0-3][0-9])[ T][0-2][0-9]:[0-5][0-9]:[0-5][0-9]\b|\b[0-2]?[0-9]:[0-5][0-9]:[0-5][0-9]\b|\b(INFO|WARN|WARNING|ERROR|DEBUG|TRACE)\b|\[[A-Z]+\])' <<<"$raw"; then
echo "log"; return 0
fi
# 系统信息特征
if grep -Eiq '(\bload average\b|\bMem:\b|\bSwap:\b|\bKiB Mem\b|\bFilesystem\b|\bTasks:\b|\btop -\b|\buptime\b|\bdf -h\b|\bfree -h\b|\bsystemctl\b|\bdmesg\b)' <<<"$raw"; then
echo "sys"; return 0
fi
echo "ask"
}
#采集系统关键状态信息(用于一键诊断)
collect_diag() {
# 采集一套常用系统信息(尽量短、可用)
{
echo "### uptime"
uptime 2>&1 || true
echo
echo "### uname -a"
uname -a 2>&1 || true
echo
echo "### date"
date 2>&1 || true
echo
echo "### free -h"
free -h 2>&1 || true
echo
echo "### df -h"
df -h 2>&1 || true
echo
echo "### top (head)"
top -b -n1 2>&1 | head -n 25 || true
echo
echo "### ss -tunlp (head)"
ss -tunlp 2>&1 | head -n 40 || true
echo
echo "### systemctl --failed"
systemctl --failed 2>&1 || true
echo
echo "### dmesg (tail)"
dmesg 2>&1 | tail -n 120 || true
}
}
# -------------------- 解析命令 --------------------
need curl
need jq
paste_flag=0
mode="${1:-help}"
shift || true
# xy paste <mode> ...
if [[ "$mode" == "paste" ]]; then
paste_flag=1
mode="${1:-help}"
shift || true
fi
# help
if [[ "$mode" == "help" || "$mode" == "-h" || "$mode" == "--help" ]]; then
usage
exit 0
fi
# diag
if [[ "$mode" == "diag" ]]; then
question="${1:-这台机器现在可能有什么问题?请给最短排查步骤和命令。}"
# diag 不从文件读取;若用户用管道附加内容,也会读进来并拼接
extra="$(read_input_any "" "0")"
raw="$(collect_diag)"
if [[ -n "$extra" ]]; then
raw="${raw}\n\n### extra_input\n${extra}"
fi
raw="$(truncate_tail "$raw" "$KEEP_CHARS")"
mode="sys"
file=""
else
# 支持模式:ask/log/err/conf/sys/auto
case "$mode" in
ask|log|err|conf|sys|auto) ;;
*) echo "未知模式:$mode(用 xy help 查看用法)" >&2; exit 1 ;;
esac
# 解析 -p/--paste 选项(放在问题前/后都可)
# 用法例:xy log -p "问题" 或 xy log "问题" -p
args=()
for a in "$@"; do
if [[ "$a" == "-p" || "$a" == "--paste" ]]; then
paste_flag=1
else
args+=("$a")
fi
done
# 重组参数:期望 args[0]=question args[1]=file(optional)
question="${args[0]:-}"
file="${args[1]:-}"
if [[ -z "$question" ]]; then
echo "需要提供问题字符串,例如:xy log \"这些日志什么意思\" [file]" >&2
exit 1
fi
raw="$(read_input_any "$file" "$paste_flag")"
if [[ "$mode" == "auto" ]]; then
detected="$(detect_mode_from_text "$raw")"
mode="$detected"
fi
fi
debug_log
# -------------------- 组装提示词 --------------------
# 你说“系统提示词:修改 User_Data 内容”,这里保留一个集中变量
User_Data="操作系统:Ubuntu 24.04.3 LTS。
请直接给结论和可执行步骤,不要长篇推理,不要复述输入内容,不要输出无关背景。输出尽量短(要点列表即可)。"
case "$mode" in
ask)
system="你是一个Linux服务器运维助手。${User_Data}"
user="${question}\n\n【上下文/材料】\n${raw}"
;;
log)
system="你是资深SRE,擅长快速读日志定位问题。${User_Data} 优先给:最可能原因TOP3 + 下一步排查命令。"
user="${question}\n\n【日志片段(可能已截断,仅最后部分)】\n${raw}"
;;
err)
system="你是排障助手,擅长从报错输出定位根因并给修复步骤。${User_Data} 给:根因 + 最短修复路径 + 验证命令。"
user="${question}\n\n【错误输出(可能已截断,仅最后部分)】\n${raw}"
;;
conf)
system="你是安全与运维配置审查助手。${User_Data} 关注:风险点、错误配置、最小修改建议。"
user="${question}\n\n【配置内容(可能已截断,仅最后部分)】\n${raw}"
;;
sys)
system="你是Linux系统诊断助手。${User_Data} 关注:资源/服务状态/内核/磁盘/网络。"
user="${question}\n\n【系统信息(可能已截断,仅最后部分)】\n${raw}"
;;
*)
# 理论上不会到这里
system="你是Linux服务器运维助手。${User_Data}"
user="${question}\n\n【材料】\n${raw}"
;;
esac
call_llm "$system" "$user"3.进入如脚本所在目录,授予其执行权限。
chmod +x xy4.回到./bin目录。
5.将脚本所在的./bin目录添加至环境变量,使系统全局均能访问脚本。
echo 'export PATH="/home/user/桌面/xy/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc
which xy6.打开终端,即可随时进行问答。
xy help
评论区