commit d90d2ecd0329ab50fe8dc88ab652c219c2caf7e0 Author: SkyJourney Date: Sun Aug 16 13:30:42 2026 +0800 初始化:私有维护版 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 及上游致谢 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..59e313f --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +# macOS +.DS_Store + +# Backup files +*.bak + +# Editor +.vscode/ +.idea/ +*.swp +*.swo diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0f162c6 --- /dev/null +++ b/LICENSE @@ -0,0 +1,28 @@ +MIT License + +Copyright (c) 2026 Shunzhi Jiang + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +--- + +This project is a fork/rewrite of appsail/Gemini-in-Chrome +(https://github.com/appsail/Gemini-in-Chrome), which is stated by its README +to be MIT licensed (no LICENSE file was present in the upstream source at the +time of forking). diff --git a/README.md b/README.md new file mode 100644 index 0000000..9bf8b44 --- /dev/null +++ b/README.md @@ -0,0 +1,92 @@ +# Gemini in Chrome + +> Fork 自:[appsail/Gemini-in-Chrome](https://github.com/appsail/Gemini-in-Chrome)(MIT License) + +一键脚本,用于强制启用 Chrome 内置的 Gemini AI 功能(绕过非美区的资格限制)。 + +基于上述原项目重写,修复了原脚本"字段不存在时静默不生效"的问题,改为 JSON 感知读写 +(`jq` 优先,装不上时自动降级为 `sed`+`awk` 零依赖兜底方案),并支持同一台机器上的 +多个 Chrome 账号(profile)。 + +## 快速开始 + +### macOS / Linux + +```bash +curl -fsSL https://gitea.rk-health.com/jiangshunzhi/Gemini-in-Chrome/raw/branch/main/install.sh | bash +``` + +### Windows + +打开 PowerShell 执行: + +```powershell +irm https://gitea.rk-health.com/jiangshunzhi/Gemini-in-Chrome/raw/branch/main/install.ps1 | iex +``` + +## 脚本做了什么 + +1. ✅ 检查 Chrome 是否在运行(在运行的话会提示你先完全退出——Local State 会在 Chrome 退出时被覆写,不退出改了也白改) +2. 💾 备份原配置文件(`Local State.bak`) +3. 🔧 修补以下字段(存在则替换、不存在则插入,而不是原脚本那种"只替换已存在字段"的逻辑): + - 顶层 `variations_country` → `"us"` + - 顶层 `variations_permanent_consistency_country` → `[当前 Chrome 版本号, "us"]` + - `profile.info_cache` 下**每一个** profile 的 `is_glic_eligible` → `true`(不只是已存在该字段的那个) +4. ✓ 写回前后都做合法性校验,`sed`+`awk` 兜底路径下用括号配平检查兜底;校验不过就自动放弃改动,不会覆盖原文件 +5. ✓ 汇报本次实际用的是 `jq` 路径还是 `sed`+`awk` 兜底路径,以及各字段是否生效 + +## 还原原始配置 + +**macOS:** +```bash +mv ~/Library/Application\ Support/Google/Chrome/Local\ State.bak \ + ~/Library/Application\ Support/Google/Chrome/Local\ State +``` + +**Linux:** +```bash +mv ~/.config/google-chrome/Local\ State.bak \ + ~/.config/google-chrome/Local\ State +``` + +**Windows PowerShell:** +```powershell +Move-Item -Path "$env:LOCALAPPDATA\Google\Chrome\User Data\Local State.bak" ` + -Destination "$env:LOCALAPPDATA\Google\Chrome\User Data\Local State" -Force +``` + +## 安全性 + +- 改动前一定会先备份原配置 +- 只改本地 Chrome 配置,不上传任何数据、不访问网络(除了下载脚本本身,以及 `jq` 缺失时按需自动安装) +- 改动前后都做合法性校验,写坏了会自动回滚、不覆盖原文件 +- 随时可还原 +- 非官方脚本,仅供个人研究/自用,风险自负 + +## jq 依赖说明 + +macOS 上如果检测到 `jq` 就直接用;没有的话,检测到 Homebrew 会尝试 `brew install jq`, +没有 Homebrew 就直接走零依赖兜底,不会替你装 Homebrew。 + +Linux 上会按顺序探测 `apt-get`/`dnf`/`yum`/`pacman`/`apk` 并尝试自动安装(可能会弹 `sudo` +密码提示);装不上(比如设备网络受限)同样自动降级为 `sed`+`awk` 方案。 + +`install.ps1`(Windows)用 PowerShell 原生的 `ConvertFrom-Json`/`ConvertTo-Json`,本身就不 +依赖 `jq`。 + +## 配置文件路径 + +| 系统 | 路径 | +|----|------| +| macOS | `~/Library/Application Support/Google/Chrome/Local State` | +| Linux | `~/.config/google-chrome/Local State` | +| Windows | `%LOCALAPPDATA%\Google\Chrome\User Data\Local State` | + +## 测试 + +`test-fixtures/` 下有一份完全人工构造(非真实账号脱敏)的多 profile 测试样本, +覆盖了"字段缺失 / 已是 false / 已是 true"三种场景,用法见该目录下的 README。 + +## License + +MIT,见 [LICENSE](./LICENSE)。基于 [appsail/Gemini-in-Chrome](https://github.com/appsail/Gemini-in-Chrome) 重写。 diff --git a/install.ps1 b/install.ps1 new file mode 100644 index 0000000..5525076 --- /dev/null +++ b/install.ps1 @@ -0,0 +1,137 @@ +# 强制启用 Chrome 中的 Gemini(Project Glic)(PowerShell 版本) +# 通过修补 Chrome 的 Local State 配置文件,绕过地区资格限制 +# 支持系统:Windows +# +# PowerShell 原生自带 ConvertFrom-Json / ConvertTo-Json,天然是 JSON 感知的, +# 不需要像 macOS/Linux 那边为了避免依赖 jq 而手写括号深度扫描兜底方案。 + +Write-Host "" +Write-Host "🚀 Gemini in Chrome Enabler" -ForegroundColor Cyan +Write-Host "" + +# Chrome 配置文件路径 +$chromeStatePath = "$env:LOCALAPPDATA\Google\Chrome\User Data\Local State" + +# 检查 Chrome 是否在运行;Local State 会在 Chrome 退出时被覆写, +# 所以修改前必须确保 Chrome 完全退出 +$chromeProcesses = Get-Process -Name "chrome" -ErrorAction SilentlyContinue +if ($chromeProcesses) { + Write-Host "⚠️ Chrome 正在运行,请先完全退出再继续。" -ForegroundColor Yellow + Read-Host "关闭 Chrome 后按回车继续" + $chromeProcesses = Get-Process -Name "chrome" -ErrorAction SilentlyContinue + if ($chromeProcesses) { + Write-Host "❌ Chrome 仍在运行,请退出后重试。" -ForegroundColor Red + exit 1 + } +} + +# 检查配置文件是否存在 +if (-not (Test-Path $chromeStatePath)) { + Write-Host "❌ 未找到 Chrome 配置文件:$chromeStatePath" -ForegroundColor Red + exit 1 +} + +# 备份原文件 +$backupPath = "$chromeStatePath.bak" +Copy-Item -Path $chromeStatePath -Destination $backupPath -Force +Write-Host "✓ 已备份:Local State.bak" -ForegroundColor Green + +# 探测本机安装的 Chrome 版本号(写入 variations_permanent_consistency_country 用) +function Get-ChromeVersion { + $candidates = @( + "$env:ProgramFiles\Google\Chrome\Application\chrome.exe", + "${env:ProgramFiles(x86)}\Google\Chrome\Application\chrome.exe", + "$env:LOCALAPPDATA\Google\Chrome\Application\chrome.exe" + ) + foreach ($path in $candidates) { + if (Test-Path $path) { + return (Get-Item $path).VersionInfo.ProductVersion + } + } + return "" +} + +$chromeVersion = Get-ChromeVersion +if (-not $chromeVersion) { + Write-Host "⚠️ 未能自动探测 Chrome 版本号,variations_permanent_consistency_country 的版本位会留空" -ForegroundColor Yellow +} + +# 读取并解析 JSON(-Depth 需要给够,避免嵌套结构在读取阶段被截断) +$json = Get-Content -Path $chromeStatePath -Raw -Encoding UTF8 | ConvertFrom-Json -Depth 100 + +# 对可能不存在的属性做"存在则改、不存在则插入"的统一封装, +# 因为 PSCustomObject 对不存在的属性不能直接用 `=` 赋值 +function Set-JsonProperty { + param( + [Parameter(Mandatory)] $Object, + [Parameter(Mandatory)] [string] $Name, + [Parameter(Mandatory)] $Value + ) + if ($Object.PSObject.Properties.Name -contains $Name) { + $Object.$Name = $Value + } else { + $Object | Add-Member -NotePropertyName $Name -NotePropertyValue $Value -Force + } +} + +Set-JsonProperty -Object $json -Name "variations_country" -Value "us" +Set-JsonProperty -Object $json -Name "variations_permanent_consistency_country" -Value @($chromeVersion, "us") + +# profile.info_cache 下每个 profile 都补 is_glic_eligible,而不仅仅是已存在该字段的 profile +if (-not ($json.PSObject.Properties.Name -contains "profile")) { + $json | Add-Member -NotePropertyName "profile" -NotePropertyValue ([PSCustomObject]@{}) -Force +} +if (-not ($json.profile.PSObject.Properties.Name -contains "info_cache")) { + $json.profile | Add-Member -NotePropertyName "info_cache" -NotePropertyValue ([PSCustomObject]@{}) -Force +} +foreach ($entry in $json.profile.info_cache.PSObject.Properties) { + Set-JsonProperty -Object $entry.Value -Name "is_glic_eligible" -Value $true +} + +# 写回:UTF-8 无 BOM(Chrome 要求),-Depth 同样要给够—— +# 默认的 -Depth 2 会把深层嵌套对象压成 "System.Object[]" 之类的字符串,直接写坏配置 +$newContent = $json | ConvertTo-Json -Depth 100 -Compress +$utf8NoBom = New-Object System.Text.UTF8Encoding $false +$tmpPath = "$chromeStatePath.tmp" +[System.IO.File]::WriteAllText($tmpPath, $newContent, $utf8NoBom) + +# 校验写回的内容仍是合法 JSON,且目标字段确实生效;不合法就回滚、不覆盖原文件 +try { + $verify = Get-Content -Path $tmpPath -Raw -Encoding UTF8 | ConvertFrom-Json -Depth 100 + if ($verify.variations_country -ne "us") { + throw "variations_country 未生效" + } + Move-Item -Path $tmpPath -Destination $chromeStatePath -Force +} catch { + Write-Host "❌ 校验失败,已放弃本次修改(原文件未被触碰):$_" -ForegroundColor Red + Remove-Item -Path $tmpPath -ErrorAction SilentlyContinue + exit 1 +} + +# 汇报结果 +Write-Host "" +$errors = 0 +$final = Get-Content -Path $chromeStatePath -Raw -Encoding UTF8 | ConvertFrom-Json -Depth 100 +if ($final.variations_country -eq "us") { + Write-Host "✓ variations_country 已设为 us" -ForegroundColor Green +} else { + Write-Host "⚠️ variations_country 未生效" -ForegroundColor Yellow + $errors++ +} +$allEligible = $true +foreach ($entry in $final.profile.info_cache.PSObject.Properties) { + if ($entry.Value.is_glic_eligible -ne $true) { $allEligible = $false } +} +if ($allEligible) { + Write-Host "✓ is_glic_eligible 已对所有 profile 启用" -ForegroundColor Green +} else { + Write-Host "⚠️ 部分 profile 的 is_glic_eligible 未生效" -ForegroundColor Yellow + $errors++ +} + +Write-Host "" +if ($errors -eq 0) { + Write-Host "✅ 完成!请完全重启 Chrome 使改动生效。" -ForegroundColor Green +} else { + Write-Host "⚠️ 部分改动可能未生效,请检查 Chrome 版本。" -ForegroundColor Yellow +} diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..51b8608 --- /dev/null +++ b/install.sh @@ -0,0 +1,358 @@ +#!/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 diff --git a/test-fixtures/Local State.sample.json b/test-fixtures/Local State.sample.json new file mode 100644 index 0000000..e3fcc79 --- /dev/null +++ b/test-fixtures/Local State.sample.json @@ -0,0 +1 @@ +{"autofill":{"ablation_seed":"AAAAAAAAAAA="},"breadcrumbs":{"enabled":false,"enabled_time":"13400000000000000"},"browser":{"first_run_finished":true,"last_whats_new_version":151,"whats_new":{"enabled_order":["VerticalTabsLaunch"]}},"demographics_birth_year_offset":0,"enterprise_reporting":{"saas_usage":{"last_trigger_time":"13400000000000000"}},"hardware_acceleration_mode_previous":true,"legacy":{"profile":{"name":{"migrated":true}}},"management":{"platform":{"enterprise_mdm_mac":0,"last_log_time":"13400000000000000"}},"network_time":{"network_time_mapping":{"local":1700000000000.0,"network":1700000000000.0,"ticks":90000000000.0,"uncertainty":1000000.0}},"optimization_guide":{"model_cache_key_mapping":{"TESTKEY000000001":"TESTMODEL0000001"},"model_store_metadata":{"1":{"TESTMODEL0000001":{"et":"13400000000000000","kbvd":true,"mbd":"1/TESTKEY000000001/TESTBLOB00000001","v":"1"}}},"on_device":{"last_version":"151.0.7922.138","model_crash_count":0,"performance_class":5,"performance_class_version":"151.0.7922.138","vram_mb":"8192"},"predictionmodelfetcher":{"last_fetch_attempt":"13400000000000000","last_fetch_success":"13400000000000000"}},"password_manager":{"had_biometrics_available":false},"performance_intervention":{"last_daily_sample":"13400000000000000"},"performance_tuning":{"last_battery_use":{"timestamp":"13400000000000000"}},"policy":{"last_statistics_update":"13400000000000000"},"profile":{"info_cache":{"Default":{"active_time":1700000000.0,"ai_subscription_tier":1,"avatar_icon":"chrome://theme/IDR_PROFILE_AVATAR_26","background_apps":false,"default_avatar_fill_color":-14868944,"default_avatar_stroke_color":-3816227,"enterprise_label":"","force_signin_profile_locked":false,"gaia_given_name":"Test","gaia_id":"100000000000000000001","gaia_name":"Test User One","gaia_picture_file_name":"Google Profile Picture.png","hosted_domain":"NO_HOSTED_DOMAIN","is_consented_primary_account":true,"is_ephemeral":false,"is_managed":0,"is_using_default_avatar":true,"is_using_default_name":false,"last_downloaded_gaia_picture_url_with_size":"https://example.com/fake-avatar-1.png","managed_user_id":"","metrics_bucket_index":1,"name":"Test","profile_color_seed":-16775169,"profile_highlight_color":-14868944,"signin.with_credential_provider":false,"user_accepted_account_management":false,"user_name":"test.user.one@example.com"},"Profile 1":{"active_time":1700000001.0,"ai_subscription_tier":0,"avatar_icon":"chrome://theme/IDR_PROFILE_AVATAR_5","gaia_given_name":"Work","gaia_id":"100000000000000000002","gaia_name":"Test User Two","hosted_domain":"NO_HOSTED_DOMAIN","is_glic_eligible":false,"is_using_default_avatar":false,"name":"Work","user_name":"test.user.two@example.com"},"Profile 2":{"active_time":1700000002.0,"ai_subscription_tier":1,"avatar_icon":"chrome://theme/IDR_PROFILE_AVATAR_9","gaia_given_name":"Personal","gaia_id":"100000000000000000003","gaia_name":"Test User Three","hosted_domain":"NO_HOSTED_DOMAIN","is_glic_eligible":true,"is_using_default_avatar":true,"name":"Personal","user_name":"test.user.three@example.com"}},"last_active_profiles":["Profile 1","Profile 2"],"metrics":{"next_bucket_index":4},"profile_counts_reported":"13400000000000000","profiles_order":["Default","Profile 1","Profile 2"]},"restart":{"last":{"session":{"on":{"shutdown":false}}}},"segmentation_platform":{"ukm_most_recent_allowed_time_key":"13400000000000000"},"session_id_generator_last_value":"1000000000","signin":{"active_accounts":{"FAKEHASH0000000000000000000000000000000001=":"13400000000000000"},"active_accounts_last_emitted":"13400000000000000","active_accounts_managed":{"FAKEHASH0000000000000000000000000000000001=":false}},"subresource_filter":{"ruleset_version":{"checksum":0,"content":"9.70.0","format":38}},"tab_stats":{"discards_external":0,"discards_frozen":0,"discards_proactive":0,"discards_suggested":0,"discards_urgent":0,"last_daily_sample":"13400000000000000","max_tabs_per_window":1,"reloads_external":0,"reloads_frozen":0,"reloads_proactive":0,"reloads_suggested":0,"reloads_urgent":0,"total_tab_count_max":1,"window_count_max":1},"toast":{"non_milestone_update_toast_version":"151.0.7922.138"},"ukm":{"client_id":"1000000000000000000","persisted_logs":[{"data":"FAKEBASE64DATA0000000000000000000000000000","hash":"FAKEHASH00000000000000000000=","signature":"FAKESIGNATURE0000000000000000000000000000=","timestamp":"1700000000","type":0}],"session_id":1},"uninstall_metrics":{"installation_date2":"1700000000"},"updateclientdata":{"apps":{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa":{"cohort":"1:0000:","cohortname":"Stable","dlrc":1,"fp":"","installdate":1,"max_pv":"0.0.0.0","pf":"00000000-0000-0000-0000-000000000000","pv":"1.0.0.0"}}},"was":{"restarted":false}} \ No newline at end of file diff --git a/test-fixtures/README.md b/test-fixtures/README.md new file mode 100644 index 0000000..186c41f --- /dev/null +++ b/test-fixtures/README.md @@ -0,0 +1,35 @@ +# 测试用 Local State fixture + +`Local State.sample.json` 是**完全人工构造**的样本文件,不是从任何真实 Chrome 安装脱敏而来—— +所有 `gaia_id`、姓名、邮箱、头像 URL 等字段都是占位符(`test.user.one@example.com` 之类), +不包含任何真实账号信息,可以放心提交到仓库、拿去别的机器测试。 + +结构上覆盖了 `profile.info_cache` 的三种场景,用来验证 `install.sh` / `install.ps1` +对每个 profile 的处理是否正确: + +| profile | 初始状态 | 用来验证什么 | +|---|---|---| +| `Default` | 完全没有 `is_glic_eligible` 字段 | 插入逻辑 | +| `Profile 1` | `is_glic_eligible: false` | 翻转逻辑 | +| `Profile 2` | `is_glic_eligible: true` | 已生效时保持不变、不产生重复 key | + +顶层也没有 `variations_country` / `variations_permanent_consistency_country`,用来验证插入逻辑。 + +## 在 Windows 上测试 install.ps1 + +1. 先备份你自己真实的 Local State(如果要在真实 Chrome 目录下测): + ```powershell + Copy-Item "$env:LOCALAPPDATA\Google\Chrome\User Data\Local State" "$env:LOCALAPPDATA\Google\Chrome\User Data\Local State.real.bak" + ``` +2. 把这份 fixture 拷贝过去顶替(**记得测完要恢复**): + ```powershell + Copy-Item ".\test-fixtures\Local State.sample.json" "$env:LOCALAPPDATA\Google\Chrome\User Data\Local State" + ``` +3. 运行 `..\install.ps1`,检查输出里三个 profile 是否都提示 `is_glic_eligible` 已启用。 +4. 测完恢复真实文件: + ```powershell + Copy-Item "$env:LOCALAPPDATA\Google\Chrome\User Data\Local State.real.bak" "$env:LOCALAPPDATA\Google\Chrome\User Data\Local State" -Force + ``` + +也可以不碰真实路径,直接改一份 `install.ps1` 的副本把 `$chromeStatePath` 硬编码指向这份 +fixture,跑完直接看输出文件内容,更安全、不依赖"记得恢复"这一步。