openclacky 1.1.6 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +37 -0
- data/CODE_OF_CONDUCT.md +1 -1
- data/CONTRIBUTING.md +92 -0
- data/README.md +10 -0
- data/README_CN.md +10 -0
- data/ROADMAP.md +29 -0
- data/docs/billing-system.md +340 -0
- data/docs/mcp-architecture.md +114 -0
- data/docs/mcp.example.json +22 -0
- data/lib/clacky/agent/cost_tracker.rb +37 -0
- data/lib/clacky/agent/llm_caller.rb +0 -1
- data/lib/clacky/agent/session_serializer.rb +2 -11
- data/lib/clacky/agent/skill_manager.rb +73 -26
- data/lib/clacky/agent/system_prompt_builder.rb +0 -5
- data/lib/clacky/agent/time_machine.rb +6 -0
- data/lib/clacky/agent.rb +26 -1
- data/lib/clacky/agent_config.rb +9 -19
- data/lib/clacky/billing/billing_record.rb +67 -0
- data/lib/clacky/billing/billing_store.rb +193 -0
- data/lib/clacky/cli.rb +108 -6
- data/lib/clacky/default_skills/browser-setup/SKILL.md +26 -4
- data/lib/clacky/default_skills/mcp-manager/SKILL.md +343 -0
- data/lib/clacky/idle_compression_timer.rb +4 -2
- data/lib/clacky/mcp/client.rb +204 -0
- data/lib/clacky/mcp/http_transport.rb +155 -0
- data/lib/clacky/mcp/registry.rb +229 -0
- data/lib/clacky/mcp/skill_provider.rb +75 -0
- data/lib/clacky/mcp/stdio_transport.rb +112 -0
- data/lib/clacky/mcp/transport.rb +23 -0
- data/lib/clacky/mcp/virtual_skill.rb +131 -0
- data/lib/clacky/message_history.rb +0 -1
- data/lib/clacky/server/channel/adapters/weixin/adapter.rb +2 -35
- data/lib/clacky/server/http_server.rb +519 -15
- data/lib/clacky/server/server_master.rb +8 -14
- data/lib/clacky/server/session_registry.rb +24 -2
- data/lib/clacky/server/web_ui_controller.rb +4 -0
- data/lib/clacky/session_manager.rb +41 -12
- data/lib/clacky/skill.rb +1 -5
- data/lib/clacky/skill_loader.rb +36 -5
- data/lib/clacky/tools/browser.rb +217 -38
- data/lib/clacky/tools/trash_manager.rb +154 -3
- data/lib/clacky/ui2/components/command_suggestions.rb +6 -2
- data/lib/clacky/ui_interface.rb +1 -0
- data/lib/clacky/utils/model_pricing.rb +11 -7
- data/lib/clacky/utils/trash_directory.rb +37 -6
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +2907 -1764
- data/lib/clacky/web/app.js +84 -10
- data/lib/clacky/web/billing.js +275 -0
- data/lib/clacky/web/brand.js +3 -0
- data/lib/clacky/web/i18n.js +242 -24
- data/lib/clacky/web/index.html +351 -134
- data/lib/clacky/web/mcp.js +328 -0
- data/lib/clacky/web/sessions.js +193 -11
- data/lib/clacky/web/settings.js +686 -174
- data/lib/clacky/web/sidebar.js +2 -0
- data/lib/clacky/web/trash.js +323 -60
- data/lib/clacky/web/ws-dispatcher.js +14 -1
- data/lib/clacky.rb +4 -0
- data/scripts/install.ps1 +23 -11
- metadata +30 -10
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "securerandom"
|
|
6
|
+
require_relative "billing_record"
|
|
7
|
+
|
|
8
|
+
module Clacky
|
|
9
|
+
module Billing
|
|
10
|
+
# Persistent storage for billing records using JSONL files
|
|
11
|
+
# Records are stored in monthly files: ~/.clacky/billing/YYYY-MM.jsonl
|
|
12
|
+
class BillingStore
|
|
13
|
+
BILLING_DIR = File.join(Dir.home, ".clacky", "billing")
|
|
14
|
+
|
|
15
|
+
def initialize(billing_dir: nil)
|
|
16
|
+
@billing_dir = billing_dir || BILLING_DIR
|
|
17
|
+
ensure_billing_dir
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Append a billing record to the current month's file
|
|
21
|
+
# @param record [BillingRecord] The record to append
|
|
22
|
+
# @return [String] The record ID
|
|
23
|
+
def append(record)
|
|
24
|
+
record.id ||= SecureRandom.uuid
|
|
25
|
+
record.timestamp ||= Time.now
|
|
26
|
+
|
|
27
|
+
month_file = current_month_file
|
|
28
|
+
File.open(month_file, "a") do |f|
|
|
29
|
+
f.puts(JSON.generate(record.to_h))
|
|
30
|
+
end
|
|
31
|
+
FileUtils.chmod(0o600, month_file)
|
|
32
|
+
|
|
33
|
+
record.id
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Query billing records with optional filters
|
|
37
|
+
# @param from [Time, nil] Start time (inclusive)
|
|
38
|
+
# @param to [Time, nil] End time (inclusive)
|
|
39
|
+
# @param model [String, nil] Filter by model name
|
|
40
|
+
# @param session_id [String, nil] Filter by session ID
|
|
41
|
+
# @param limit [Integer, nil] Maximum number of records to return
|
|
42
|
+
# @return [Array<BillingRecord>] Matching records, newest first
|
|
43
|
+
def query(from: nil, to: nil, model: nil, session_id: nil, limit: nil)
|
|
44
|
+
records = []
|
|
45
|
+
|
|
46
|
+
billing_files.each do |file|
|
|
47
|
+
File.foreach(file) do |line|
|
|
48
|
+
next if line.strip.empty?
|
|
49
|
+
|
|
50
|
+
begin
|
|
51
|
+
hash = JSON.parse(line, symbolize_names: true)
|
|
52
|
+
record = BillingRecord.from_h(hash)
|
|
53
|
+
|
|
54
|
+
# Apply filters
|
|
55
|
+
next if from && record.timestamp < from
|
|
56
|
+
next if to && record.timestamp > to
|
|
57
|
+
next if model && record.model != model
|
|
58
|
+
next if session_id && record.session_id != session_id
|
|
59
|
+
|
|
60
|
+
records << record
|
|
61
|
+
rescue JSON::ParserError
|
|
62
|
+
# Skip malformed lines
|
|
63
|
+
next
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Sort by timestamp descending (newest first)
|
|
69
|
+
records.sort_by! { |r| r.timestamp }.reverse!
|
|
70
|
+
|
|
71
|
+
# Apply limit
|
|
72
|
+
limit ? records.first(limit) : records
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Get summary statistics for a time period
|
|
76
|
+
# @param period [Symbol] :day, :week, :month, :year, or :all
|
|
77
|
+
# @return [Hash] Summary with total_cost, total_tokens, by_model, etc.
|
|
78
|
+
def summary(period: :month)
|
|
79
|
+
from_time = period_start(period)
|
|
80
|
+
records = query(from: from_time)
|
|
81
|
+
|
|
82
|
+
total_cost = records.sum { |r| r.cost_usd || 0 }
|
|
83
|
+
total_prompt = records.sum { |r| r.prompt_tokens || 0 }
|
|
84
|
+
total_completion = records.sum { |r| r.completion_tokens || 0 }
|
|
85
|
+
total_cache_read = records.sum { |r| r.cache_read_tokens || 0 }
|
|
86
|
+
total_cache_write = records.sum { |r| r.cache_write_tokens || 0 }
|
|
87
|
+
|
|
88
|
+
by_model = records.group_by(&:model).transform_values do |rs|
|
|
89
|
+
{
|
|
90
|
+
cost: rs.sum { |r| r.cost_usd || 0 },
|
|
91
|
+
prompt_tokens: rs.sum { |r| r.prompt_tokens || 0 },
|
|
92
|
+
completion_tokens: rs.sum { |r| r.completion_tokens || 0 },
|
|
93
|
+
requests: rs.size
|
|
94
|
+
}
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
by_day = records.group_by { |r| r.timestamp.strftime("%Y-%m-%d") }.transform_values do |rs|
|
|
98
|
+
rs.sum { |r| r.cost_usd || 0 }
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
{
|
|
102
|
+
period: period,
|
|
103
|
+
from: from_time&.iso8601,
|
|
104
|
+
to: Time.now.iso8601,
|
|
105
|
+
total_cost: total_cost.round(6),
|
|
106
|
+
total_tokens: total_prompt + total_completion,
|
|
107
|
+
prompt_tokens: total_prompt,
|
|
108
|
+
completion_tokens: total_completion,
|
|
109
|
+
cache_read_tokens: total_cache_read,
|
|
110
|
+
cache_write_tokens: total_cache_write,
|
|
111
|
+
by_model: by_model,
|
|
112
|
+
by_day: by_day,
|
|
113
|
+
record_count: records.size
|
|
114
|
+
}
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Get daily cost breakdown for the last N days
|
|
118
|
+
# @param days [Integer] Number of days to include
|
|
119
|
+
# @return [Array<Hash>] Daily summaries with date and cost
|
|
120
|
+
def daily_breakdown(days: 30)
|
|
121
|
+
from_time = Time.now - (days * 24 * 60 * 60)
|
|
122
|
+
records = query(from: from_time)
|
|
123
|
+
|
|
124
|
+
by_day = records.group_by { |r| r.timestamp.strftime("%Y-%m-%d") }
|
|
125
|
+
|
|
126
|
+
(0...days).map do |i|
|
|
127
|
+
date = (Time.now - (i * 24 * 60 * 60)).strftime("%Y-%m-%d")
|
|
128
|
+
day_records = by_day[date] || []
|
|
129
|
+
{
|
|
130
|
+
date: date,
|
|
131
|
+
cost: day_records.sum { |r| r.cost_usd || 0 }.round(6),
|
|
132
|
+
tokens: day_records.sum { |r| r.total_tokens },
|
|
133
|
+
prompt_tokens: day_records.sum { |r| r.prompt_tokens || 0 },
|
|
134
|
+
completion_tokens: day_records.sum { |r| r.completion_tokens || 0 },
|
|
135
|
+
cache_read_tokens: day_records.sum { |r| r.cache_read_tokens || 0 },
|
|
136
|
+
cache_write_tokens: day_records.sum { |r| r.cache_write_tokens || 0 },
|
|
137
|
+
requests: day_records.size
|
|
138
|
+
}
|
|
139
|
+
end.reverse
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Delete old billing records
|
|
143
|
+
# @param before [Time] Delete records before this time
|
|
144
|
+
# @return [Integer] Number of files deleted
|
|
145
|
+
def cleanup(before:)
|
|
146
|
+
deleted = 0
|
|
147
|
+
billing_files.each do |file|
|
|
148
|
+
# Parse month from filename (YYYY-MM.jsonl)
|
|
149
|
+
basename = File.basename(file, ".jsonl")
|
|
150
|
+
file_month = Time.parse("#{basename}-01") rescue nil
|
|
151
|
+
next unless file_month
|
|
152
|
+
|
|
153
|
+
# Delete if the entire month is before the cutoff
|
|
154
|
+
if file_month < before - (31 * 24 * 60 * 60)
|
|
155
|
+
File.delete(file)
|
|
156
|
+
deleted += 1
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
deleted
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
private def ensure_billing_dir
|
|
163
|
+
FileUtils.mkdir_p(@billing_dir) unless Dir.exist?(@billing_dir)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
private def current_month_file
|
|
167
|
+
File.join(@billing_dir, "#{Time.now.strftime('%Y-%m')}.jsonl")
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
private def billing_files
|
|
171
|
+
Dir.glob(File.join(@billing_dir, "*.jsonl")).sort.reverse
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
private def period_start(period)
|
|
175
|
+
now = Time.now
|
|
176
|
+
case period
|
|
177
|
+
when :day
|
|
178
|
+
Time.new(now.year, now.month, now.day)
|
|
179
|
+
when :week
|
|
180
|
+
now - (7 * 24 * 60 * 60)
|
|
181
|
+
when :month
|
|
182
|
+
Time.new(now.year, now.month, 1)
|
|
183
|
+
when :year
|
|
184
|
+
Time.new(now.year, 1, 1)
|
|
185
|
+
when :all
|
|
186
|
+
nil
|
|
187
|
+
else
|
|
188
|
+
Time.new(now.year, now.month, 1)
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
data/lib/clacky/cli.rb
CHANGED
|
@@ -101,7 +101,7 @@ module Clacky
|
|
|
101
101
|
end
|
|
102
102
|
|
|
103
103
|
# Validate and get working directory
|
|
104
|
-
working_dir = validate_working_directory(options[:path])
|
|
104
|
+
working_dir = validate_working_directory(options[:path], agent_config)
|
|
105
105
|
|
|
106
106
|
# Update agent config with CLI options
|
|
107
107
|
agent_config.permission_mode = options[:mode].to_sym if options[:mode]
|
|
@@ -385,6 +385,14 @@ module Clacky
|
|
|
385
385
|
|
|
386
386
|
CLI_DEFAULT_SESSION_NAME = "CLI Session"
|
|
387
387
|
|
|
388
|
+
# Format a number with thousand separators for display
|
|
389
|
+
# @param num [Integer, Float] The number to format
|
|
390
|
+
# @return [String] Formatted number string
|
|
391
|
+
private def format_number(num)
|
|
392
|
+
return "0" if num.nil? || num == 0
|
|
393
|
+
num.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
|
|
394
|
+
end
|
|
395
|
+
|
|
388
396
|
# Auto-name a CLI session from the first user message, mirroring server-side logic.
|
|
389
397
|
# Renames when the agent has no history yet (i.e. first message of the session).
|
|
390
398
|
private def auto_name_session(agent, input)
|
|
@@ -395,12 +403,14 @@ module Clacky
|
|
|
395
403
|
agent.rename(auto_name)
|
|
396
404
|
end
|
|
397
405
|
|
|
398
|
-
def validate_working_directory(path)
|
|
406
|
+
def validate_working_directory(path, config = nil)
|
|
399
407
|
working_dir = path || Dir.pwd
|
|
400
408
|
|
|
401
|
-
# If no path specified and currently in home directory, use
|
|
409
|
+
# If no path specified and currently in home directory, use configured
|
|
410
|
+
# default_working_dir (or ~/clacky_workspace as fallback)
|
|
402
411
|
if path.nil? && File.expand_path(working_dir) == File.expand_path(Dir.home)
|
|
403
|
-
|
|
412
|
+
default = config&.default_working_dir || File.expand_path("~/clacky_workspace")
|
|
413
|
+
working_dir = File.expand_path(default)
|
|
404
414
|
|
|
405
415
|
# Create directory if it doesn't exist
|
|
406
416
|
unless Dir.exist?(working_dir)
|
|
@@ -879,6 +889,98 @@ module Clacky
|
|
|
879
889
|
|
|
880
890
|
end
|
|
881
891
|
|
|
892
|
+
# ── billing command ────────────────────────────────────────────────────────
|
|
893
|
+
desc "billing", "Show billing summary and usage statistics"
|
|
894
|
+
long_desc <<-LONGDESC
|
|
895
|
+
Display billing summary with token usage and cost breakdown.
|
|
896
|
+
|
|
897
|
+
Period options:
|
|
898
|
+
day - Today's usage
|
|
899
|
+
week - Last 7 days
|
|
900
|
+
month - Current month (default)
|
|
901
|
+
year - Current year
|
|
902
|
+
all - All time
|
|
903
|
+
|
|
904
|
+
Examples:
|
|
905
|
+
$ clacky billing
|
|
906
|
+
$ clacky billing --period week
|
|
907
|
+
$ clacky billing --period all --json
|
|
908
|
+
LONGDESC
|
|
909
|
+
option :period, type: :string, default: "month",
|
|
910
|
+
desc: "Time period: day, week, month, year, all (default: month)"
|
|
911
|
+
option :json, type: :boolean, default: false,
|
|
912
|
+
desc: "Output as JSON"
|
|
913
|
+
option :days, type: :numeric, default: 30,
|
|
914
|
+
desc: "Number of days for daily breakdown (default: 30)"
|
|
915
|
+
option :help, type: :boolean, aliases: "-h", desc: "Show this help message"
|
|
916
|
+
def billing
|
|
917
|
+
if options[:help]
|
|
918
|
+
invoke :help, ["billing"]
|
|
919
|
+
return
|
|
920
|
+
end
|
|
921
|
+
|
|
922
|
+
require_relative "billing/billing_store"
|
|
923
|
+
|
|
924
|
+
store = Clacky::Billing::BillingStore.new
|
|
925
|
+
period = options[:period].to_sym
|
|
926
|
+
summary = store.summary(period: period)
|
|
927
|
+
|
|
928
|
+
if options[:json]
|
|
929
|
+
require "json"
|
|
930
|
+
puts JSON.pretty_generate(summary)
|
|
931
|
+
return
|
|
932
|
+
end
|
|
933
|
+
|
|
934
|
+
# Display formatted billing summary
|
|
935
|
+
puts ""
|
|
936
|
+
puts "📊 Billing Summary (#{period})"
|
|
937
|
+
puts "─" * 50
|
|
938
|
+
puts ""
|
|
939
|
+
|
|
940
|
+
# Total cost
|
|
941
|
+
cost_str = summary[:total_cost] > 0 ? "$#{format('%.4f', summary[:total_cost])}" : "$0.0000"
|
|
942
|
+
puts " 💰 Total Cost: #{cost_str}"
|
|
943
|
+
puts " 📝 Total Tokens: #{format_number(summary[:total_tokens])}"
|
|
944
|
+
puts " 📥 Prompt Tokens: #{format_number(summary[:prompt_tokens])}"
|
|
945
|
+
puts " 📤 Completion: #{format_number(summary[:completion_tokens])}"
|
|
946
|
+
puts " 🗄️ Cache Read: #{format_number(summary[:cache_read_tokens])}"
|
|
947
|
+
puts " 📝 Cache Write: #{format_number(summary[:cache_write_tokens])}"
|
|
948
|
+
puts " 🔢 API Requests: #{summary[:record_count]}"
|
|
949
|
+
puts ""
|
|
950
|
+
|
|
951
|
+
# By model breakdown
|
|
952
|
+
if summary[:by_model] && !summary[:by_model].empty?
|
|
953
|
+
puts "📈 By Model:"
|
|
954
|
+
puts "─" * 50
|
|
955
|
+
summary[:by_model].each do |model, data|
|
|
956
|
+
cost = data.is_a?(Hash) ? data[:cost] : data
|
|
957
|
+
requests = data.is_a?(Hash) ? data[:requests] : "?"
|
|
958
|
+
puts " #{model}"
|
|
959
|
+
puts " Cost: $#{format('%.4f', cost)} | Requests: #{requests}"
|
|
960
|
+
end
|
|
961
|
+
puts ""
|
|
962
|
+
end
|
|
963
|
+
|
|
964
|
+
# Daily breakdown (last N days)
|
|
965
|
+
daily = store.daily_breakdown(days: [options[:days], 14].min)
|
|
966
|
+
recent_days = daily.select { |d| d[:cost] > 0 }.last(7)
|
|
967
|
+
|
|
968
|
+
if recent_days.any?
|
|
969
|
+
puts "📅 Recent Daily Usage:"
|
|
970
|
+
puts "─" * 50
|
|
971
|
+
recent_days.each do |day|
|
|
972
|
+
bar_len = [(day[:cost] * 100).to_i, 30].min
|
|
973
|
+
bar = "█" * bar_len
|
|
974
|
+
puts " #{day[:date]} $#{format('%.4f', day[:cost])} #{bar}"
|
|
975
|
+
end
|
|
976
|
+
puts ""
|
|
977
|
+
end
|
|
978
|
+
|
|
979
|
+
puts "─" * 50
|
|
980
|
+
puts " Data stored in: ~/.clacky/billing/"
|
|
981
|
+
puts ""
|
|
982
|
+
end
|
|
983
|
+
|
|
882
984
|
# ── server command ─────────────────────────────────────────────────────────
|
|
883
985
|
desc "server", "Start the Clacky web UI server"
|
|
884
986
|
long_desc <<-LONGDESC
|
|
@@ -956,8 +1058,8 @@ module Clacky
|
|
|
956
1058
|
$stdout = Clacky::Server::EPIPESafeIO.new($stdout)
|
|
957
1059
|
$stderr = Clacky::Server::EPIPESafeIO.new($stderr)
|
|
958
1060
|
|
|
959
|
-
fd
|
|
960
|
-
master_pid
|
|
1061
|
+
fd = ENV["CLACKY_INHERIT_FD"].to_i
|
|
1062
|
+
master_pid = ENV["CLACKY_MASTER_PID"].to_i
|
|
961
1063
|
# Must use TCPServer.for_fd (not Socket.for_fd) so that accept_nonblock
|
|
962
1064
|
# returns a single Socket, not [Socket, Addrinfo] — WEBrick expects the former.
|
|
963
1065
|
socket = TCPServer.for_fd(fd)
|
|
@@ -19,6 +19,25 @@ allowed-tools:
|
|
|
19
19
|
|
|
20
20
|
Configure the browser tool for Clacky. Config is stored at `~/.clacky/browser.yml`.
|
|
21
21
|
|
|
22
|
+
## Region-Aware Download Links
|
|
23
|
+
|
|
24
|
+
Whenever you show the user a link to download or upgrade Chrome/Edge, pick the right one for their region instead of always using google.com.
|
|
25
|
+
|
|
26
|
+
Treat the user as **in China** when any of these is true:
|
|
27
|
+
- The user is talking to you in Chinese
|
|
28
|
+
- The system locale is Chinese (`echo $LANG` contains `zh_CN` / `zh_`)
|
|
29
|
+
- A previous run of `install_browser.sh` reported `Region: china` (visible in its output)
|
|
30
|
+
- `curl -s --max-time 3 https://www.google.com -o /dev/null -w "%{http_code}"` returns `000` while baidu.com works
|
|
31
|
+
|
|
32
|
+
Use these links accordingly:
|
|
33
|
+
|
|
34
|
+
| Region | Chrome | Edge |
|
|
35
|
+
|---|---|---|
|
|
36
|
+
| China | https://www.google.cn/chrome/ | https://www.microsoft.com/zh-cn/edge |
|
|
37
|
+
| Global | https://www.google.com/chrome/ | https://www.microsoft.com/edge |
|
|
38
|
+
|
|
39
|
+
When unsure, show **both** lines (label them "China:" and "Global:") so the user can pick.
|
|
40
|
+
|
|
22
41
|
## Command Parsing
|
|
23
42
|
|
|
24
43
|
| User says | Subcommand |
|
|
@@ -197,9 +216,10 @@ Parse the version number:
|
|
|
197
216
|
> ⚠️ Your browser version is v${VERSION}. Version 146+ is recommended for best compatibility.
|
|
198
217
|
> Continuing anyway...
|
|
199
218
|
- **version < 144 or "unknown"** → Stop:
|
|
200
|
-
> ❌ Browser version v${VERSION} is too old. Please upgrade Chrome or Edge to v146
|
|
201
|
-
>
|
|
202
|
-
> -
|
|
219
|
+
> ❌ Browser version v${VERSION} is too old. Please upgrade Chrome or Edge to v146+.
|
|
220
|
+
>
|
|
221
|
+
> Use the download link from the **Region-Aware Download Links** section above
|
|
222
|
+
> (pick `China` or `Global` based on the user's region).
|
|
203
223
|
>
|
|
204
224
|
> After upgrading, run `/browser-setup` again.
|
|
205
225
|
|
|
@@ -363,7 +383,9 @@ Connection
|
|
|
363
383
|
|
|
364
384
|
2. Upgrade your browser:
|
|
365
385
|
- Chrome v142 is too old (need v146+)
|
|
366
|
-
-
|
|
386
|
+
- Pick the download link for the user's region from the
|
|
387
|
+
**Region-Aware Download Links** section at the top of this skill
|
|
388
|
+
(China users → google.cn; others → google.com).
|
|
367
389
|
|
|
368
390
|
After fixing these issues, run `/browser-setup` again to verify.
|
|
369
391
|
```
|