easyai 1.0.2 → 1.0.3
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/CLAUDE.md +154 -5
- data/README.md +38 -57
- data/bin/easyai +5 -5
- data/lib/easyai/auth/authclaude.rb +440 -0
- data/lib/easyai/auth/jpslogin.rb +384 -0
- data/lib/easyai/base/file_crypto.rb +214 -0
- data/lib/easyai/base/system_keychain.rb +240 -0
- data/lib/easyai/base/update_notifier.rb +129 -0
- data/lib/easyai/base/version_checker.rb +329 -0
- data/lib/easyai/command/claude.rb +278 -0
- data/lib/easyai/command/clean.rb +453 -0
- data/lib/easyai/command/gemini.rb +58 -0
- data/lib/easyai/command/gpt.rb +58 -0
- data/lib/easyai/command/update.rb +210 -0
- data/lib/easyai/command/utils/decry.rb +102 -0
- data/lib/easyai/command/utils/encry.rb +102 -0
- data/lib/easyai/command/utils.rb +32 -0
- data/lib/easyai/command.rb +56 -0
- data/lib/easyai/config/config.rb +550 -0
- data/lib/easyai/version.rb +1 -1
- data/lib/easyai.rb +67 -55
- metadata +17 -4
- data/lib/easyai/claude.rb +0 -61
- data/lib/easyai/gemini.rb +0 -61
- data/lib/easyai/gpt.rb +0 -60
@@ -0,0 +1,240 @@
|
|
1
|
+
require 'rbconfig'
|
2
|
+
|
3
|
+
module EasyAI
|
4
|
+
module Base
|
5
|
+
module SystemKeychain
|
6
|
+
SERVICE_NAME = "easyai"
|
7
|
+
ACCOUNT_NAME = "config_password"
|
8
|
+
|
9
|
+
# 跨平台密码存储
|
10
|
+
def self.store_password(password)
|
11
|
+
case detect_platform
|
12
|
+
when :macos
|
13
|
+
store_password_macos(password)
|
14
|
+
when :windows
|
15
|
+
store_password_windows(password)
|
16
|
+
when :linux
|
17
|
+
store_password_linux(password)
|
18
|
+
else
|
19
|
+
puts "⚠ 当前平台不支持密码自动存储,将在会话中临时保存"
|
20
|
+
false
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# 跨平台密码获取
|
25
|
+
def self.get_stored_password
|
26
|
+
case detect_platform
|
27
|
+
when :macos
|
28
|
+
get_password_macos
|
29
|
+
when :windows
|
30
|
+
get_password_windows
|
31
|
+
when :linux
|
32
|
+
get_password_linux
|
33
|
+
else
|
34
|
+
nil
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# 跨平台密码删除
|
39
|
+
def self.delete_stored_password
|
40
|
+
case detect_platform
|
41
|
+
when :macos
|
42
|
+
delete_password_macos
|
43
|
+
when :windows
|
44
|
+
delete_password_windows
|
45
|
+
when :linux
|
46
|
+
delete_password_linux
|
47
|
+
else
|
48
|
+
true
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# 检测操作系统平台
|
53
|
+
def self.detect_platform
|
54
|
+
os = RbConfig::CONFIG['host_os']
|
55
|
+
case os
|
56
|
+
when /darwin/i
|
57
|
+
:macos
|
58
|
+
when /mswin|mingw|cygwin/i
|
59
|
+
:windows
|
60
|
+
when /linux/i
|
61
|
+
:linux
|
62
|
+
else
|
63
|
+
:unknown
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
# macOS Keychain 密码存储
|
70
|
+
def self.store_password_macos(password)
|
71
|
+
# 首先尝试更新现有条目
|
72
|
+
update_cmd = "security add-generic-password -a '#{ACCOUNT_NAME}' -s '#{SERVICE_NAME}' -w '#{password}' -U 2>/dev/null"
|
73
|
+
success = system(update_cmd)
|
74
|
+
|
75
|
+
unless success
|
76
|
+
# 如果更新失败,尝试添加新条目
|
77
|
+
add_cmd = "security add-generic-password -a '#{ACCOUNT_NAME}' -s '#{SERVICE_NAME}' -w '#{password}' 2>/dev/null"
|
78
|
+
success = system(add_cmd)
|
79
|
+
end
|
80
|
+
|
81
|
+
if success
|
82
|
+
puts "✓ 密码已安全存储到 macOS Keychain"
|
83
|
+
else
|
84
|
+
puts "⚠ 无法存储密码到 macOS Keychain"
|
85
|
+
end
|
86
|
+
|
87
|
+
success
|
88
|
+
rescue => e
|
89
|
+
puts "⚠ Keychain 存储出错: #{e.message}"
|
90
|
+
false
|
91
|
+
end
|
92
|
+
|
93
|
+
# macOS Keychain 密码获取
|
94
|
+
def self.get_password_macos
|
95
|
+
cmd = "security find-generic-password -a '#{ACCOUNT_NAME}' -s '#{SERVICE_NAME}' -w 2>/dev/null"
|
96
|
+
password = `#{cmd}`.chomp
|
97
|
+
|
98
|
+
return nil if password.empty? || $?.exitstatus != 0
|
99
|
+
password
|
100
|
+
rescue => e
|
101
|
+
nil
|
102
|
+
end
|
103
|
+
|
104
|
+
# macOS Keychain 密码删除
|
105
|
+
def self.delete_password_macos
|
106
|
+
cmd = "security delete-generic-password -a '#{ACCOUNT_NAME}' -s '#{SERVICE_NAME}' 2>/dev/null"
|
107
|
+
success = system(cmd)
|
108
|
+
|
109
|
+
if success
|
110
|
+
puts "✓ 已从 macOS Keychain 删除存储的密码"
|
111
|
+
end
|
112
|
+
|
113
|
+
success
|
114
|
+
rescue => e
|
115
|
+
false
|
116
|
+
end
|
117
|
+
|
118
|
+
# Windows 凭据管理器密码存储
|
119
|
+
def self.store_password_windows(password)
|
120
|
+
# 使用 cmdkey 命令存储密码
|
121
|
+
target_name = "#{SERVICE_NAME}_#{ACCOUNT_NAME}"
|
122
|
+
cmd = "cmdkey /add:#{target_name} /user:#{ACCOUNT_NAME} /pass:\"#{password}\""
|
123
|
+
|
124
|
+
success = system(cmd + " >nul 2>&1")
|
125
|
+
|
126
|
+
if success
|
127
|
+
puts "✓ 密码已安全存储到 Windows 凭据管理器"
|
128
|
+
else
|
129
|
+
puts "⚠ 无法存储密码到 Windows 凭据管理器"
|
130
|
+
end
|
131
|
+
|
132
|
+
success
|
133
|
+
rescue => e
|
134
|
+
puts "⚠ Windows 凭据存储出错: #{e.message}"
|
135
|
+
false
|
136
|
+
end
|
137
|
+
|
138
|
+
# Windows 凭据管理器密码获取
|
139
|
+
def self.get_password_windows
|
140
|
+
target_name = "#{SERVICE_NAME}_#{ACCOUNT_NAME}"
|
141
|
+
|
142
|
+
# 首先检查凭据是否存在
|
143
|
+
check_cmd = "cmdkey /list:#{target_name} >nul 2>&1"
|
144
|
+
return nil unless system(check_cmd)
|
145
|
+
|
146
|
+
# 使用 PowerShell 获取密码(简化版本,兼容性更好)
|
147
|
+
ps_script = <<~PS
|
148
|
+
try {
|
149
|
+
$credential = Get-StoredCredential -Target "#{target_name}" -ErrorAction Stop
|
150
|
+
$ptr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($credential.Password)
|
151
|
+
$password = [Runtime.InteropServices.Marshal]::PtrToStringAuto($ptr)
|
152
|
+
[Runtime.InteropServices.Marshal]::ZeroFreeBSTR($ptr)
|
153
|
+
Write-Output $password
|
154
|
+
} catch {
|
155
|
+
# 如果 Get-StoredCredential 不可用,尝试使用原生方法
|
156
|
+
$output = & cmdkey /list:"#{target_name}" 2>$null
|
157
|
+
if ($output -match "User: #{ACCOUNT_NAME}") {
|
158
|
+
Write-Output "CREDENTIAL_EXISTS_BUT_CANNOT_RETRIEVE"
|
159
|
+
}
|
160
|
+
}
|
161
|
+
PS
|
162
|
+
|
163
|
+
password = `powershell -Command "#{ps_script}" 2>nul`.chomp
|
164
|
+
|
165
|
+
# 如果无法获取密码但凭据存在,返回特殊标识
|
166
|
+
return nil if password.empty? || password == "CREDENTIAL_EXISTS_BUT_CANNOT_RETRIEVE"
|
167
|
+
password
|
168
|
+
rescue => e
|
169
|
+
nil
|
170
|
+
end
|
171
|
+
|
172
|
+
# Windows 凭据管理器密码删除
|
173
|
+
def self.delete_password_windows
|
174
|
+
target_name = "#{SERVICE_NAME}_#{ACCOUNT_NAME}"
|
175
|
+
cmd = "cmdkey /delete:#{target_name}"
|
176
|
+
|
177
|
+
success = system(cmd + " >nul 2>&1")
|
178
|
+
|
179
|
+
if success
|
180
|
+
puts "✓ 已从 Windows 凭据管理器删除存储的密码"
|
181
|
+
end
|
182
|
+
|
183
|
+
success
|
184
|
+
rescue => e
|
185
|
+
false
|
186
|
+
end
|
187
|
+
|
188
|
+
# Linux 密码存储(使用文件加密存储作为备用方案)
|
189
|
+
def self.store_password_linux(password)
|
190
|
+
keyring_dir = File.expand_path('~/.easyai/.keyring')
|
191
|
+
FileUtils.mkdir_p(keyring_dir) unless Dir.exist?(keyring_dir)
|
192
|
+
|
193
|
+
keyring_file = File.join(keyring_dir, 'config_key')
|
194
|
+
|
195
|
+
begin
|
196
|
+
# 使用简单的 Base64 编码存储(在实际生产中应该使用更安全的方法)
|
197
|
+
require 'base64'
|
198
|
+
encoded_password = Base64.strict_encode64(password)
|
199
|
+
File.write(keyring_file, encoded_password)
|
200
|
+
File.chmod(0600, keyring_file) # 设置为仅用户可读写
|
201
|
+
|
202
|
+
puts "✓ 密码已存储到本地加密文件"
|
203
|
+
true
|
204
|
+
rescue => e
|
205
|
+
puts "⚠ Linux 密码存储出错: #{e.message}"
|
206
|
+
false
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
# Linux 密码获取
|
211
|
+
def self.get_password_linux
|
212
|
+
keyring_file = File.expand_path('~/.easyai/.keyring/config_key')
|
213
|
+
return nil unless File.exist?(keyring_file)
|
214
|
+
|
215
|
+
begin
|
216
|
+
require 'base64'
|
217
|
+
encoded_password = File.read(keyring_file).chomp
|
218
|
+
Base64.strict_decode64(encoded_password)
|
219
|
+
rescue => e
|
220
|
+
nil
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
# Linux 密码删除
|
225
|
+
def self.delete_password_linux
|
226
|
+
keyring_file = File.expand_path('~/.easyai/.keyring/config_key')
|
227
|
+
|
228
|
+
if File.exist?(keyring_file)
|
229
|
+
File.delete(keyring_file)
|
230
|
+
puts "✓ 已删除本地存储的密码"
|
231
|
+
true
|
232
|
+
else
|
233
|
+
true
|
234
|
+
end
|
235
|
+
rescue => e
|
236
|
+
false
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'time'
|
3
|
+
|
4
|
+
module EasyAI
|
5
|
+
module Base
|
6
|
+
class UpdateNotifier
|
7
|
+
NOTIFICATION_FILE = File.expand_path('~/.easyai/update_notification.yml')
|
8
|
+
|
9
|
+
class << self
|
10
|
+
# 智能决定是否显示更新通知
|
11
|
+
def maybe_show_notification
|
12
|
+
return if ENV['EASYAI_NO_UPDATE_CHECK']
|
13
|
+
return unless should_show_notification?
|
14
|
+
|
15
|
+
notification = load_notification
|
16
|
+
return unless notification && notification['available']
|
17
|
+
|
18
|
+
# 使用简洁的单行提醒
|
19
|
+
show_brief_notification(notification)
|
20
|
+
update_shown_time
|
21
|
+
end
|
22
|
+
|
23
|
+
# 在程序退出时显示(如果有更新)
|
24
|
+
def show_on_exit
|
25
|
+
return if ENV['EASYAI_NO_UPDATE_CHECK']
|
26
|
+
|
27
|
+
notification = load_notification
|
28
|
+
return unless notification && notification['available']
|
29
|
+
return if notification['shown_today']
|
30
|
+
|
31
|
+
# 退出时显示稍微详细的信息
|
32
|
+
show_exit_notification(notification)
|
33
|
+
mark_as_shown_today
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def should_show_notification?
|
39
|
+
notification = load_notification
|
40
|
+
return false unless notification
|
41
|
+
|
42
|
+
# 检查显示频率
|
43
|
+
last_shown = notification['last_shown_at']
|
44
|
+
if last_shown
|
45
|
+
hours_since = (Time.now - Time.parse(last_shown)) / 3600
|
46
|
+
|
47
|
+
# 根据版本差异决定提醒频率
|
48
|
+
if major_update?(notification)
|
49
|
+
return hours_since > 6 # 主版本更新:6小时提醒一次
|
50
|
+
elsif minor_update?(notification)
|
51
|
+
return hours_since > 24 # 次版本更新:24小时提醒一次
|
52
|
+
else
|
53
|
+
return hours_since > 72 # 补丁更新:72小时提醒一次
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
true
|
58
|
+
end
|
59
|
+
|
60
|
+
def major_update?(notification)
|
61
|
+
return false unless notification['latest_version'] && notification['current_version']
|
62
|
+
|
63
|
+
latest_major = notification['latest_version'].split('.')[0].to_i
|
64
|
+
current_major = notification['current_version'].split('.')[0].to_i
|
65
|
+
|
66
|
+
latest_major > current_major
|
67
|
+
end
|
68
|
+
|
69
|
+
def minor_update?(notification)
|
70
|
+
return false unless notification['latest_version'] && notification['current_version']
|
71
|
+
|
72
|
+
latest_parts = notification['latest_version'].split('.').map(&:to_i)
|
73
|
+
current_parts = notification['current_version'].split('.').map(&:to_i)
|
74
|
+
|
75
|
+
latest_parts[0] == current_parts[0] && latest_parts[1] > current_parts[1]
|
76
|
+
end
|
77
|
+
|
78
|
+
def show_brief_notification(notification)
|
79
|
+
latest = notification['latest_version']
|
80
|
+
current = notification['current_version']
|
81
|
+
|
82
|
+
# 单行简洁提醒
|
83
|
+
if major_update?(notification)
|
84
|
+
puts "💡 重要更新: EasyAI #{latest} 已发布 (当前: #{current})。运行 'gem update easyai' 更新。".yellow
|
85
|
+
else
|
86
|
+
puts "💡 新版本: EasyAI #{latest} 可用。运行 'easyai --check-update' 查看详情。".cyan
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def show_exit_notification(notification)
|
91
|
+
latest = notification['latest_version']
|
92
|
+
current = notification['current_version']
|
93
|
+
|
94
|
+
puts "\n" + "─" * 50
|
95
|
+
puts "📦 EasyAI 更新提醒".yellow
|
96
|
+
puts " 当前版本: #{current}"
|
97
|
+
puts " 最新版本: #{latest}".green
|
98
|
+
puts " 更新命令: gem update easyai"
|
99
|
+
puts " 查看详情: easyai --check-update"
|
100
|
+
puts "─" * 50
|
101
|
+
end
|
102
|
+
|
103
|
+
def load_notification
|
104
|
+
return nil unless File.exist?(NOTIFICATION_FILE)
|
105
|
+
YAML.load_file(NOTIFICATION_FILE) rescue nil
|
106
|
+
end
|
107
|
+
|
108
|
+
def update_shown_time
|
109
|
+
notification = load_notification || {}
|
110
|
+
notification['last_shown_at'] = Time.now.to_s
|
111
|
+
save_notification(notification)
|
112
|
+
end
|
113
|
+
|
114
|
+
def mark_as_shown_today
|
115
|
+
notification = load_notification || {}
|
116
|
+
notification['shown_today'] = Date.today.to_s
|
117
|
+
save_notification(notification)
|
118
|
+
end
|
119
|
+
|
120
|
+
def save_notification(data)
|
121
|
+
FileUtils.mkdir_p(File.dirname(NOTIFICATION_FILE))
|
122
|
+
File.write(NOTIFICATION_FILE, data.to_yaml)
|
123
|
+
rescue
|
124
|
+
nil
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,329 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'json'
|
3
|
+
require 'yaml'
|
4
|
+
require 'fileutils'
|
5
|
+
require 'time'
|
6
|
+
require 'colored2'
|
7
|
+
|
8
|
+
module EasyAI
|
9
|
+
module Base
|
10
|
+
class VersionChecker
|
11
|
+
RUBYGEMS_API = 'https://rubygems.org/api/v1/gems/easyai.json'
|
12
|
+
CACHE_FILE = File.expand_path('~/.easyai/version_cache.yml')
|
13
|
+
CHECK_INTERVAL = 24 * 60 * 60 # 24小时检查一次
|
14
|
+
|
15
|
+
class << self
|
16
|
+
# 异步检查版本(不阻塞主程序)
|
17
|
+
def check_async
|
18
|
+
puts " 🔮 启动异步版本检查...".cyan if ENV['EASYAI_DEBUG']
|
19
|
+
|
20
|
+
return unless should_check?
|
21
|
+
|
22
|
+
puts " 🚀 后台检查进程已启动".green if ENV['EASYAI_DEBUG']
|
23
|
+
|
24
|
+
# 使用 fork 在后台检查(Unix系统)
|
25
|
+
if Process.respond_to?(:fork)
|
26
|
+
pid = fork do
|
27
|
+
# 子进程中执行检查
|
28
|
+
check_and_cache
|
29
|
+
exit(0)
|
30
|
+
end
|
31
|
+
# 父进程立即继续,不等待子进程
|
32
|
+
Process.detach(pid) if pid
|
33
|
+
else
|
34
|
+
# Windows 系统使用线程
|
35
|
+
Thread.new { check_and_cache }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# 检查是否需要强制更新
|
40
|
+
def require_force_update?
|
41
|
+
return false if ENV['EASYAI_SKIP_FORCE_UPDATE'] # 紧急跳过选项
|
42
|
+
|
43
|
+
# 检查缓存
|
44
|
+
if File.exist?(CACHE_FILE)
|
45
|
+
cache = YAML.load_file(CACHE_FILE) rescue {}
|
46
|
+
latest = cache['latest_version']
|
47
|
+
current = cache['current_version'] || EasyAI::VERSION
|
48
|
+
|
49
|
+
return needs_major_or_minor_update?(latest, current)
|
50
|
+
end
|
51
|
+
|
52
|
+
false
|
53
|
+
rescue
|
54
|
+
false
|
55
|
+
end
|
56
|
+
|
57
|
+
# 强制更新检查(同步)
|
58
|
+
def check_force_update!
|
59
|
+
puts " 📌 执行强制更新检查...".cyan if ENV['EASYAI_DEBUG']
|
60
|
+
|
61
|
+
# 如果缓存中已有需要强制更新的信息,直接使用
|
62
|
+
if require_force_update?
|
63
|
+
cache = YAML.load_file(CACHE_FILE) rescue {}
|
64
|
+
show_force_update_message(cache['latest_version'], EasyAI::VERSION)
|
65
|
+
exit(1)
|
66
|
+
end
|
67
|
+
|
68
|
+
# 如果缓存过期或不存在,立即检查
|
69
|
+
if should_check_immediately?
|
70
|
+
puts " 🔄 正在从 RubyGems 获取最新版本...".cyan if ENV['EASYAI_DEBUG']
|
71
|
+
latest = fetch_latest_version
|
72
|
+
if latest && needs_major_or_minor_update?(latest, EasyAI::VERSION)
|
73
|
+
# 更新缓存
|
74
|
+
update_cache_for_force_update(latest, EasyAI::VERSION)
|
75
|
+
show_force_update_message(latest, EasyAI::VERSION)
|
76
|
+
exit(1)
|
77
|
+
end
|
78
|
+
else
|
79
|
+
puts " 💾 使用缓存(未过期)".light_black if ENV['EASYAI_DEBUG']
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# 同步检查版本(用于 --check-update 命令)
|
84
|
+
def check_now
|
85
|
+
latest = fetch_latest_version
|
86
|
+
current = EasyAI::VERSION
|
87
|
+
|
88
|
+
if latest && newer_version?(latest, current)
|
89
|
+
show_update_message(latest, current)
|
90
|
+
true
|
91
|
+
else
|
92
|
+
puts "✓ 您正在使用最新版本 (#{current})".green
|
93
|
+
false
|
94
|
+
end
|
95
|
+
rescue => e
|
96
|
+
puts "⚠ 无法检查更新: #{e.message}".yellow if ENV['DEBUG']
|
97
|
+
false
|
98
|
+
end
|
99
|
+
|
100
|
+
# 显示缓存的更新提醒(如果有)
|
101
|
+
def show_cached_reminder
|
102
|
+
return unless File.exist?(CACHE_FILE)
|
103
|
+
|
104
|
+
cache = YAML.load_file(CACHE_FILE) rescue {}
|
105
|
+
return unless cache['new_version_available']
|
106
|
+
return if cache['reminder_shown_at'] &&
|
107
|
+
Time.now - Time.parse(cache['reminder_shown_at']) < 3600 # 1小时内不重复提醒
|
108
|
+
|
109
|
+
latest = cache['latest_version']
|
110
|
+
current = EasyAI::VERSION
|
111
|
+
|
112
|
+
if newer_version?(latest, current)
|
113
|
+
puts "\n#{get_update_banner(latest, current)}\n"
|
114
|
+
|
115
|
+
# 更新提醒时间
|
116
|
+
cache['reminder_shown_at'] = Time.now.to_s
|
117
|
+
save_cache(cache)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
private
|
122
|
+
|
123
|
+
def should_check?
|
124
|
+
return false if ENV['EASYAI_NO_UPDATE_CHECK'] # 允许用户禁用
|
125
|
+
|
126
|
+
# 检查缓存文件
|
127
|
+
if File.exist?(CACHE_FILE)
|
128
|
+
cache = YAML.load_file(CACHE_FILE) rescue {}
|
129
|
+
last_check = cache['last_check_at']
|
130
|
+
if last_check
|
131
|
+
time_since_check = Time.now - Time.parse(last_check)
|
132
|
+
return time_since_check > CHECK_INTERVAL
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
true
|
137
|
+
end
|
138
|
+
|
139
|
+
def check_and_cache
|
140
|
+
latest = fetch_latest_version
|
141
|
+
return unless latest
|
142
|
+
|
143
|
+
current = EasyAI::VERSION
|
144
|
+
is_new = newer_version?(latest, current)
|
145
|
+
|
146
|
+
cache = {
|
147
|
+
'last_check_at' => Time.now.to_s,
|
148
|
+
'latest_version' => latest,
|
149
|
+
'current_version' => current,
|
150
|
+
'new_version_available' => is_new
|
151
|
+
}
|
152
|
+
|
153
|
+
save_cache(cache)
|
154
|
+
|
155
|
+
# 同时更新通知文件
|
156
|
+
if is_new
|
157
|
+
update_notification(latest, current)
|
158
|
+
end
|
159
|
+
rescue => e
|
160
|
+
# 静默失败,不影响主程序
|
161
|
+
nil
|
162
|
+
end
|
163
|
+
|
164
|
+
def update_notification(latest, current)
|
165
|
+
notification_file = File.expand_path('~/.easyai/update_notification.yml')
|
166
|
+
notification = {
|
167
|
+
'available' => true,
|
168
|
+
'latest_version' => latest,
|
169
|
+
'current_version' => current,
|
170
|
+
'checked_at' => Time.now.to_s
|
171
|
+
}
|
172
|
+
|
173
|
+
# 保留上次显示时间
|
174
|
+
if File.exist?(notification_file)
|
175
|
+
old_data = YAML.load_file(notification_file) rescue {}
|
176
|
+
notification['last_shown_at'] = old_data['last_shown_at']
|
177
|
+
notification['shown_today'] = old_data['shown_today']
|
178
|
+
end
|
179
|
+
|
180
|
+
FileUtils.mkdir_p(File.dirname(notification_file))
|
181
|
+
File.write(notification_file, notification.to_yaml)
|
182
|
+
rescue
|
183
|
+
nil
|
184
|
+
end
|
185
|
+
|
186
|
+
def fetch_latest_version
|
187
|
+
uri = URI(RUBYGEMS_API)
|
188
|
+
|
189
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
190
|
+
http.use_ssl = true
|
191
|
+
http.open_timeout = 2 # 快速超时
|
192
|
+
http.read_timeout = 2
|
193
|
+
|
194
|
+
request = Net::HTTP::Get.new(uri)
|
195
|
+
response = http.request(request)
|
196
|
+
|
197
|
+
if response.code == '200'
|
198
|
+
data = JSON.parse(response.body)
|
199
|
+
data['version']
|
200
|
+
else
|
201
|
+
nil
|
202
|
+
end
|
203
|
+
rescue => e
|
204
|
+
nil # 网络错误时静默失败
|
205
|
+
end
|
206
|
+
|
207
|
+
def newer_version?(latest, current)
|
208
|
+
return false unless latest && current
|
209
|
+
|
210
|
+
# 解析版本号 (major.minor.patch)
|
211
|
+
latest_parts = latest.split('.').map(&:to_i)
|
212
|
+
current_parts = current.split('.').map(&:to_i)
|
213
|
+
|
214
|
+
# 比较版本号
|
215
|
+
(0..2).each do |i|
|
216
|
+
latest_part = latest_parts[i] || 0
|
217
|
+
current_part = current_parts[i] || 0
|
218
|
+
|
219
|
+
return true if latest_part > current_part
|
220
|
+
return false if latest_part < current_part
|
221
|
+
end
|
222
|
+
|
223
|
+
false
|
224
|
+
end
|
225
|
+
|
226
|
+
# 检查是否需要强制更新(主版本或次版本更新)
|
227
|
+
def needs_major_or_minor_update?(latest, current)
|
228
|
+
return false unless latest && current
|
229
|
+
|
230
|
+
latest_parts = latest.split('.').map(&:to_i)
|
231
|
+
current_parts = current.split('.').map(&:to_i)
|
232
|
+
|
233
|
+
# 主版本更新(如 1.x.x -> 2.x.x)
|
234
|
+
return true if latest_parts[0] > current_parts[0]
|
235
|
+
|
236
|
+
# 次版本更新(如 1.1.x -> 1.2.x)
|
237
|
+
return true if latest_parts[0] == current_parts[0] && latest_parts[1] > current_parts[1]
|
238
|
+
|
239
|
+
false
|
240
|
+
end
|
241
|
+
|
242
|
+
# 是否应该立即检查
|
243
|
+
def should_check_immediately?
|
244
|
+
return true unless File.exist?(CACHE_FILE)
|
245
|
+
|
246
|
+
cache = YAML.load_file(CACHE_FILE) rescue {}
|
247
|
+
last_check = cache['last_check_at']
|
248
|
+
|
249
|
+
# 如果没有检查记录或缓存超过6小时,立即检查
|
250
|
+
return true unless last_check
|
251
|
+
|
252
|
+
time_since_check = Time.now - Time.parse(last_check)
|
253
|
+
time_since_check > 6 * 60 * 60 # 6小时
|
254
|
+
end
|
255
|
+
|
256
|
+
# 更新缓存(强制更新场景)
|
257
|
+
def update_cache_for_force_update(latest, current)
|
258
|
+
cache = {
|
259
|
+
'last_check_at' => Time.now.to_s,
|
260
|
+
'latest_version' => latest,
|
261
|
+
'current_version' => current,
|
262
|
+
'new_version_available' => true,
|
263
|
+
'force_update_required' => true
|
264
|
+
}
|
265
|
+
save_cache(cache)
|
266
|
+
end
|
267
|
+
|
268
|
+
def save_cache(data)
|
269
|
+
FileUtils.mkdir_p(File.dirname(CACHE_FILE))
|
270
|
+
File.write(CACHE_FILE, data.to_yaml)
|
271
|
+
rescue => e
|
272
|
+
# 静默失败
|
273
|
+
nil
|
274
|
+
end
|
275
|
+
|
276
|
+
def show_update_message(latest, current)
|
277
|
+
puts get_update_banner(latest, current)
|
278
|
+
end
|
279
|
+
|
280
|
+
def get_update_banner(latest, current)
|
281
|
+
banner = []
|
282
|
+
banner << "┌─────────────────────────────────────────────────────┐".yellow
|
283
|
+
banner << "│ 🎉 新版本可用! │".yellow
|
284
|
+
banner << "│ │".yellow
|
285
|
+
banner << "│ 当前版本: #{current.ljust(39)}│".yellow
|
286
|
+
banner << "│ 最新版本: #{latest.ljust(39)}│".yellow
|
287
|
+
banner << "│ │".yellow
|
288
|
+
banner << "│ 更新方式: │".yellow
|
289
|
+
banner << "│ 稳定版: $ gem update easyai │".yellow
|
290
|
+
banner << "│ 开发版: $ easyai update (仅用于测试) │".yellow
|
291
|
+
banner << "│ │".yellow
|
292
|
+
banner << "│ 禁用检查: export EASYAI_NO_UPDATE_CHECK=1 │".yellow
|
293
|
+
banner << "└─────────────────────────────────────────────────────┘".yellow
|
294
|
+
banner.join("\n")
|
295
|
+
end
|
296
|
+
|
297
|
+
def show_force_update_message(latest, current)
|
298
|
+
latest_parts = latest.split('.').map(&:to_i)
|
299
|
+
current_parts = current.split('.').map(&:to_i)
|
300
|
+
|
301
|
+
is_major_update = latest_parts[0] > current_parts[0]
|
302
|
+
update_type = is_major_update ? "主版本" : "次版本"
|
303
|
+
|
304
|
+
puts "\n"
|
305
|
+
puts "╔═══════════════════════════════════════════════════════════╗".red
|
306
|
+
puts "║ ⚠️ 强制更新要求 ⚠️ ║".red
|
307
|
+
puts "╠═══════════════════════════════════════════════════════════╣".red
|
308
|
+
puts "║ ║".red
|
309
|
+
puts "║ 检测到#{update_type}更新,必须升级后才能继续使用: ║".red
|
310
|
+
puts "║ ║".red
|
311
|
+
puts "║ 当前版本: #{current.ljust(44)}║".red
|
312
|
+
puts "║ 最新版本: #{latest.ljust(44)}║".red
|
313
|
+
puts "║ ║".red
|
314
|
+
puts "║ 此更新包含重要变更,为确保功能正常,请立即更新: ║".red
|
315
|
+
puts "║ ║".red
|
316
|
+
puts "║ 稳定版: $ gem update easyai ║".red
|
317
|
+
puts "║ 开发版: $ easyai update (临时测试) ║".red
|
318
|
+
puts "║ ║".red
|
319
|
+
puts "║ 如遇紧急情况需要临时跳过(不推荐): ║".red
|
320
|
+
puts "║ $ export EASYAI_SKIP_FORCE_UPDATE=1 ║".red
|
321
|
+
puts "║ ║".red
|
322
|
+
puts "╚═══════════════════════════════════════════════════════════╝".red
|
323
|
+
puts "\n"
|
324
|
+
puts "程序将退出,请更新后重试。".red
|
325
|
+
end
|
326
|
+
end
|
327
|
+
end
|
328
|
+
end
|
329
|
+
end
|