从 appsail/Gemini-in-Chrome (MIT) fork 并重写 install.sh / install.ps1: - 原脚本用正则/sed 做“仅替换已存在字段”,字段本不存在时静默不生效 - 改为 JSON 感知读写(jq 优先,探测/自动装失败时降级为 sed+awk 零依赖兜底) - 修复过程中实测发现并修掉的坑:BSD sed 不支持 GNU 的 0,/re/ 插入语法、 正则不容忍冒号后空格导致插入重复 key、macOS 系统自带 bash 3.2 在脚本文件模式下 中文紧跟未加花括号的 $VAR 会解析错位丢字节 - 新增 test-fixtures/ 下完全人工构造(非真实数据脱敏)的多 profile 测试样本 - README 改为中文,下载链接指向本仓库,补充 MIT LICENSE 及上游致谢
359 lines
13 KiB
Bash
Executable File
359 lines
13 KiB
Bash
Executable File
#!/bin/bash
|
||
|
||
# 强制启用 Chrome 中的 Gemini(Project Glic)
|
||
# 通过修补 Chrome 的 Local State 配置文件,绕过地区资格限制
|
||
# 支持系统:macOS、Linux
|
||
#
|
||
# 设计说明:
|
||
# - 优先用 jq 做 JSON 级别的读写(准确、可读性好);
|
||
# - 没有 jq 时按系统类型自动尝试安装(macOS 用 brew,Linux 探测常见包管理器);
|
||
# - 自动安装失败(例如设备网络受限)时,降级为 sed(处理两个顶层标量/数组字段)
|
||
# + awk(对 profile.info_cache 做带引号转义感知的括号深度扫描,逐个 profile 插入字段)
|
||
# 的零依赖兜底方案,只重写 info_cache 这一小段字节范围,其余内容原样透传。
|
||
#
|
||
# 编码注意事项(实测踩过的坑):
|
||
# macOS 自带 /bin/bash 是 3.2.57(苹果因 GPLv3 许可问题多年未升级)。以脚本文件方式
|
||
# 运行时,一旦某个多字节 UTF-8 字符(中文、全角标点等)紧跟一个**未加花括号**的
|
||
# `$VAR`,bash 3.2 的分词会解析错位、丢字节,输出会出现乱码或字段被吞掉——交互式敲同样
|
||
# 的命令反而不会复现。本文件里所有紧跟中文/全角字符之后的变量引用一律写成 `${VAR}`。
|
||
|
||
set -e
|
||
|
||
echo ""
|
||
echo "🚀 Gemini in Chrome Enabler"
|
||
echo ""
|
||
|
||
# 探测系统类型,确定配置文件路径
|
||
OS_TYPE=$(uname -s)
|
||
case "$OS_TYPE" in
|
||
Darwin)
|
||
CHROME_STATE=~/Library/Application\ Support/Google/Chrome/Local\ State
|
||
CHROME_PROCESS="Google Chrome"
|
||
;;
|
||
Linux)
|
||
CHROME_STATE=~/.config/google-chrome/Local\ State
|
||
CHROME_PROCESS="chrome"
|
||
;;
|
||
*)
|
||
echo "💡 Windows 用户请直接运行同目录下的 install.ps1"
|
||
exit 1
|
||
;;
|
||
esac
|
||
|
||
# 跨 BSD sed(macOS)/ GNU sed(Linux)的原地编辑封装
|
||
sed_inplace() {
|
||
if [[ "$OS_TYPE" == "Darwin" ]]; then
|
||
sed -i '' "$@"
|
||
else
|
||
sed -i "$@"
|
||
fi
|
||
}
|
||
|
||
# 检查 Chrome 是否在运行;Local State 会在 Chrome 退出时被覆写,
|
||
# 所以修改前必须确保 Chrome 完全退出
|
||
check_chrome_running() {
|
||
pgrep -x "$CHROME_PROCESS" > /dev/null 2>&1
|
||
}
|
||
|
||
if check_chrome_running; then
|
||
echo "⚠️ Chrome 正在运行!"
|
||
if [[ "$OS_TYPE" == "Darwin" ]]; then
|
||
echo "📌 请先完全退出 Chrome(Cmd+Q)再继续"
|
||
else
|
||
echo "📌 请先完全退出 Chrome 再继续"
|
||
fi
|
||
echo ""
|
||
read -p "关闭 Chrome 后按回车继续... " -r
|
||
echo ""
|
||
|
||
if check_chrome_running; then
|
||
echo "❌ Chrome 仍在运行,请退出后重试。"
|
||
exit 1
|
||
fi
|
||
fi
|
||
|
||
# 检查配置文件是否存在
|
||
if [ ! -f "$CHROME_STATE" ]; then
|
||
echo "❌ 未找到 Chrome 配置文件:${CHROME_STATE}"
|
||
exit 1
|
||
fi
|
||
|
||
# 备份原文件
|
||
cp "$CHROME_STATE" "$CHROME_STATE.bak"
|
||
echo "✓ 已备份:Local State.bak"
|
||
|
||
# 探测本机安装的 Chrome 版本号(写入 variations_permanent_consistency_country 用)
|
||
detect_chrome_version() {
|
||
local version=""
|
||
if [[ "$OS_TYPE" == "Darwin" ]]; then
|
||
version=$(mdls -name kMDItemVersion -raw "/Applications/Google Chrome.app" 2>/dev/null || true)
|
||
if [[ -z "$version" || "$version" == "(null)" ]]; then
|
||
version=$(defaults read "/Applications/Google Chrome.app/Contents/Info" CFBundleShortVersionString 2>/dev/null || true)
|
||
fi
|
||
else
|
||
if command -v google-chrome >/dev/null 2>&1; then
|
||
version=$(google-chrome --version 2>/dev/null | grep -oE '[0-9]+(\.[0-9]+){2,3}' | head -1 || true)
|
||
elif command -v google-chrome-stable >/dev/null 2>&1; then
|
||
version=$(google-chrome-stable --version 2>/dev/null | grep -oE '[0-9]+(\.[0-9]+){2,3}' | head -1 || true)
|
||
fi
|
||
fi
|
||
echo "$version"
|
||
}
|
||
|
||
CHROME_VERSION=$(detect_chrome_version)
|
||
if [[ -z "$CHROME_VERSION" ]]; then
|
||
echo "⚠️ 未能自动探测 Chrome 版本号,variations_permanent_consistency_country 的版本位会留空"
|
||
fi
|
||
|
||
# 探测 jq;没有则按系统类型自动尝试安装(绝不自动安装 Homebrew 本身)
|
||
ensure_jq() {
|
||
if command -v jq >/dev/null 2>&1; then
|
||
return 0
|
||
fi
|
||
|
||
echo "ℹ️ 未检测到 jq,尝试自动安装..."
|
||
case "$OS_TYPE" in
|
||
Darwin)
|
||
if command -v brew >/dev/null 2>&1; then
|
||
brew install jq || true
|
||
else
|
||
echo "⚠️ 未检测到 Homebrew,跳过自动安装(不会自动装 Homebrew)"
|
||
fi
|
||
;;
|
||
Linux)
|
||
if command -v apt-get >/dev/null 2>&1; then
|
||
sudo apt-get update -y && sudo apt-get install -y jq || true
|
||
elif command -v dnf >/dev/null 2>&1; then
|
||
sudo dnf install -y jq || true
|
||
elif command -v yum >/dev/null 2>&1; then
|
||
sudo yum install -y jq || true
|
||
elif command -v pacman >/dev/null 2>&1; then
|
||
sudo pacman -Sy --noconfirm jq || true
|
||
elif command -v apk >/dev/null 2>&1; then
|
||
sudo apk add jq || true
|
||
else
|
||
echo "⚠️ 未找到已知的包管理器,跳过自动安装"
|
||
fi
|
||
;;
|
||
esac
|
||
|
||
if command -v jq >/dev/null 2>&1; then
|
||
echo "✓ jq 安装成功"
|
||
return 0
|
||
else
|
||
echo "⚠️ jq 安装失败或被跳过,降级为 sed+awk 零依赖方案"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
TMP_FILE="${CHROME_STATE}.tmp.$$"
|
||
PATCH_METHOD=""
|
||
|
||
if ensure_jq; then
|
||
PATCH_METHOD="jq"
|
||
# -c 保持压缩单行输出:一是跟 Chrome 自己写出来的格式一致、改动面最小,
|
||
# 二是脚本末尾的校验用 grep 做字面量匹配,没有 -c 的话 jq 会输出带缩进的多行
|
||
# JSON,字段其实生效了,但 grep 会因为格式不匹配而误报"未生效"——实测踩过这个坑。
|
||
jq -c --arg ver "$CHROME_VERSION" '
|
||
.variations_country = "us"
|
||
| .variations_permanent_consistency_country = [$ver, "us"]
|
||
| .profile.info_cache = ((.profile.info_cache // {}) | with_entries(.value.is_glic_eligible = true))
|
||
' "$CHROME_STATE" > "$TMP_FILE"
|
||
else
|
||
PATCH_METHOD="sed+awk"
|
||
|
||
cp "$CHROME_STATE" "$TMP_FILE"
|
||
|
||
# 顶层字段:存在则替换,不存在则插入到根对象的第一个 '{' 之后。
|
||
# 注意:这里用 `1s/{/.../ `(POSIX 通用写法,Local State 本身就是单行 JSON)
|
||
# 而不是 `0,/{/s//.../ ` —— 后者是 GNU sed 的扩展语法,在 macOS 自带的 BSD sed
|
||
# 上会静默不生效(退出码仍是 0,但完全没有插入),实测踩过这个坑。
|
||
# 冒号两侧允许出现空白再匹配——真实 Chrome 写出的 Local State 是压缩无空格的单行
|
||
# JSON,理论上用不到这个容错,但如果“判断是否存在”和“实际替换”的正则宽松度不一致,
|
||
# 一旦真的遇到带空格的输入,会把“判断为不存在”和“已存在但没匹配上替换”这两种情况
|
||
# 搞混,插入一个重复 key(JSON 语法上合法,但解析器通常只认最后一个,等于新值被
|
||
# 静默吞掉)。这里两处判断都用同一个宽松正则,避免这种不一致。
|
||
if grep -Eq '"variations_country"[[:space:]]*:[[:space:]]*"[^"]*"' "$TMP_FILE"; then
|
||
sed_inplace -E 's/"variations_country"[[:space:]]*:[[:space:]]*"[^"]*"/"variations_country":"us"/' "$TMP_FILE"
|
||
else
|
||
sed_inplace "1s/{/{\"variations_country\":\"us\",/" "$TMP_FILE"
|
||
fi
|
||
|
||
if grep -Eq '"variations_permanent_consistency_country"[[:space:]]*:[[:space:]]*\[' "$TMP_FILE"; then
|
||
sed_inplace -E "s/\"variations_permanent_consistency_country\"[[:space:]]*:[[:space:]]*\[[^]]*\]/\"variations_permanent_consistency_country\":[\"${CHROME_VERSION}\",\"us\"]/" "$TMP_FILE"
|
||
else
|
||
sed_inplace "1s/{/{\"variations_permanent_consistency_country\":[\"${CHROME_VERSION}\",\"us\"],/" "$TMP_FILE"
|
||
fi
|
||
|
||
# 已存在 is_glic_eligible:false 的 profile 直接原地翻转为 true——
|
||
# 这个字段是布尔标量,全文只会以 profile 的直接子字段形式出现,
|
||
# 用简单正则翻转是安全的,不需要结构感知
|
||
sed_inplace -e 's/"is_glic_eligible":[[:space:]]*false/"is_glic_eligible":true/g' "$TMP_FILE"
|
||
|
||
# profile.info_cache 下完全没有 is_glic_eligible 字段的 profile 补插入:需要结构感知,
|
||
# 用 awk 做带引号转义感知的括号深度扫描,只重写 info_cache 这一小段
|
||
AWK_SCRIPT="${TMP_FILE}.awk"
|
||
cat > "$AWK_SCRIPT" <<'AWK_EOF'
|
||
{
|
||
if (NR == 1) content = $0
|
||
else content = content "\n" $0
|
||
}
|
||
END {
|
||
# 定位 "info_cache" 键之后的 '{',冒号两侧允许空白(跟顶层字段那两处保持同一容错水平,
|
||
# 避免"字符串精确匹配"这个原脚本的老问题在这里换了个地方重新出现)
|
||
key = "\"info_cache\""
|
||
keyPos = index(content, key)
|
||
if (keyPos == 0) {
|
||
# 没有 info_cache(理论上不该发生,Chrome 至少会有一个 profile),原样输出
|
||
printf "%s\n", content
|
||
exit
|
||
}
|
||
p = keyPos + length(key)
|
||
n0 = length(content)
|
||
while (p <= n0 && substr(content, p, 1) ~ /[ \t\r\n]/) p++
|
||
if (substr(content, p, 1) != ":") {
|
||
printf "%s\n", content
|
||
exit
|
||
}
|
||
p++
|
||
while (p <= n0 && substr(content, p, 1) ~ /[ \t\r\n]/) p++
|
||
if (substr(content, p, 1) != "{") {
|
||
printf "%s\n", content
|
||
exit
|
||
}
|
||
objStart = p
|
||
|
||
# 第一遍扫描:定位 info_cache 对象的匹配右括号
|
||
depth = 0
|
||
inStr = 0
|
||
n = length(content)
|
||
i = objStart
|
||
objEnd = 0
|
||
while (i <= n) {
|
||
c = substr(content, i, 1)
|
||
if (inStr) {
|
||
if (c == "\\") { i += 2; continue }
|
||
else if (c == "\"") { inStr = 0 }
|
||
} else {
|
||
if (c == "\"") { inStr = 1 }
|
||
else if (c == "{" || c == "[") { depth++ }
|
||
else if (c == "}" || c == "]") {
|
||
depth--
|
||
if (depth == 0) { objEnd = i; break }
|
||
}
|
||
}
|
||
i++
|
||
}
|
||
|
||
if (objEnd == 0) {
|
||
# 括号没配平,说明文件本身有问题,不做任何改动,原样输出并报错
|
||
printf "%s\n", content > "/dev/stderr"
|
||
print "AWK_BRACE_MISMATCH" > "/dev/stderr"
|
||
exit 1
|
||
}
|
||
|
||
span = substr(content, objStart, objEnd - objStart + 1)
|
||
|
||
# 第二遍扫描:只在 info_cache 的直接子对象(每个 profile)里检查/插入 is_glic_eligible
|
||
out = ""
|
||
depth2 = 0
|
||
inStr2 = 0
|
||
childStart = 0
|
||
m = length(span)
|
||
j = 1
|
||
while (j <= m) {
|
||
c = substr(span, j, 1)
|
||
out = out c
|
||
if (inStr2) {
|
||
if (c == "\\") {
|
||
j++
|
||
if (j <= m) out = out substr(span, j, 1)
|
||
} else if (c == "\"") {
|
||
inStr2 = 0
|
||
}
|
||
} else if (c == "\"") {
|
||
inStr2 = 1
|
||
} else if (c == "{" || c == "[") {
|
||
depth2++
|
||
if (depth2 == 2 && c == "{") childStart = length(out)
|
||
} else if (c == "}" || c == "]") {
|
||
if (depth2 == 2 && c == "}") {
|
||
childSlice = substr(out, childStart + 1, length(out) - childStart - 1)
|
||
if (index(childSlice, "\"is_glic_eligible\"") == 0) {
|
||
out = substr(out, 1, length(out) - 1) ",\"is_glic_eligible\":true}"
|
||
}
|
||
}
|
||
depth2--
|
||
}
|
||
j++
|
||
}
|
||
|
||
newContent = substr(content, 1, objStart - 1) out substr(content, objEnd + 1)
|
||
printf "%s\n", newContent
|
||
}
|
||
AWK_EOF
|
||
|
||
awk -f "$AWK_SCRIPT" "$TMP_FILE" > "${TMP_FILE}.out"
|
||
mv "${TMP_FILE}.out" "$TMP_FILE"
|
||
rm -f "$AWK_SCRIPT"
|
||
|
||
# 零依赖场景下没有真正的 JSON 解析器可用,做一个括号配平 + 首尾定界符的
|
||
# 保守合法性检查兜底(不是完整 JSON 校验,但能拦住most常见的写坏场景)
|
||
balance_check() {
|
||
awk '
|
||
{
|
||
if (NR == 1) content = $0
|
||
else content = content "\n" $0
|
||
}
|
||
END {
|
||
depth = 0
|
||
inStr = 0
|
||
n = length(content)
|
||
for (i = 1; i <= n; i++) {
|
||
c = substr(content, i, 1)
|
||
if (inStr) {
|
||
if (c == "\\") { i++; continue }
|
||
else if (c == "\"") inStr = 0
|
||
} else {
|
||
if (c == "\"") inStr = 1
|
||
else if (c == "{" || c == "[") depth++
|
||
else if (c == "}" || c == "]") depth--
|
||
}
|
||
}
|
||
exit (depth == 0) ? 0 : 1
|
||
}' "$1"
|
||
}
|
||
|
||
if ! balance_check "$TMP_FILE"; then
|
||
echo "❌ 兜底方案生成的内容括号不配平,判定为写坏,已放弃本次修改(原文件未被触碰)"
|
||
rm -f "$TMP_FILE"
|
||
exit 1
|
||
fi
|
||
fi
|
||
|
||
mv "$TMP_FILE" "$CHROME_STATE"
|
||
|
||
# 校验结果
|
||
echo ""
|
||
echo "(本次修补方式:${PATCH_METHOD})"
|
||
errors=0
|
||
if grep -q '"variations_country":"us"' "$CHROME_STATE"; then
|
||
echo "✓ variations_country 已设为 us"
|
||
else
|
||
echo "⚠️ variations_country 未生效"
|
||
((errors++)) || true
|
||
fi
|
||
if grep -q '"is_glic_eligible":true' "$CHROME_STATE"; then
|
||
echo "✓ is_glic_eligible 已启用"
|
||
else
|
||
echo "⚠️ is_glic_eligible 未生效"
|
||
((errors++)) || true
|
||
fi
|
||
|
||
echo ""
|
||
if [ $errors -eq 0 ]; then
|
||
echo "✅ 完成!请完全重启 Chrome 使改动生效。"
|
||
else
|
||
echo "⚠️ 部分改动可能未生效,请检查 Chrome 版本或改用 --force-glic-eligible 之类的实验开关。"
|
||
fi
|