Files
SkyJourney d90d2ecd03 初始化:私有维护版 Gemini-in-Chrome 安装脚本
从 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 及上游致谢
2026-08-16 13:30:42 +08:00

359 lines
13 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
# 强制启用 Chrome 中的 GeminiProject 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 sedmacOS/ GNU sedLinux)的原地编辑封装
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 "📌 请先完全退出 ChromeCmd+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