easyai 1.0.2 → 1.0.4

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.
@@ -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