easyai 1.2.1 → 1.3.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/CLAUDE.md +87 -220
- data/easyai.gemspec +1 -0
- data/lib/easyai/auth/authclaude.rb +260 -135
- data/lib/easyai/auth/jpsloginhelper.rb +98 -0
- data/lib/easyai/base/cross_platform.rb +149 -0
- data/lib/easyai/base/file_crypto.rb +50 -9
- data/lib/easyai/base/system_keychain.rb +52 -18
- data/lib/easyai/command/claude.rb +76 -41
- data/lib/easyai/command/clean.rb +56 -20
- data/lib/easyai/command/gemini.rb +2 -6
- data/lib/easyai/command/gpt.rb +17 -11
- data/lib/easyai/command/setup.rb +494 -0
- data/lib/easyai/config/config.rb +284 -595
- data/lib/easyai/config/easyai_config.rb +258 -0
- data/lib/easyai/version.rb +1 -1
- data/lib/easyai.rb +32 -3
- metadata +27 -4
- data/lib/easyai/auth/jpslogin.rb +0 -655
data/lib/easyai/config/config.rb
CHANGED
@@ -1,663 +1,352 @@
|
|
1
1
|
require 'json'
|
2
|
-
require 'tmpdir'
|
3
2
|
require 'fileutils'
|
4
|
-
|
5
|
-
require_relative '
|
6
|
-
require_relative '../base/system_keychain'
|
7
|
-
require_relative '../auth/jpslogin'
|
3
|
+
require_relative '../auth/jpsloginhelper'
|
4
|
+
require_relative 'easyai_config'
|
8
5
|
|
9
6
|
module EasyAI
|
7
|
+
# ConfigManager 类负责业务配置逻辑和用户认证
|
10
8
|
class ConfigManager
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
@@tool_type = nil
|
19
|
-
|
20
|
-
# 管理配置仓库:如果存在则更新,不存在则下载
|
21
|
-
def self.manage_config_repo
|
22
|
-
# 确保 ~/.easyai 目录存在
|
23
|
-
FileUtils.mkdir_p(EASYAI_DIR) unless Dir.exist?(EASYAI_DIR)
|
24
|
-
|
25
|
-
if Dir.exist?(CONFIG_REPO_DIR)
|
26
|
-
update_config_repo
|
27
|
-
else
|
28
|
-
download_config_repo
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
|
-
# 更新现有的配置仓库
|
33
|
-
def self.update_config_repo
|
34
|
-
begin
|
35
|
-
# 检查是否是有效的 git 仓库
|
36
|
-
unless Dir.exist?(File.join(CONFIG_REPO_DIR, '.git'))
|
37
|
-
FileUtils.rm_rf(CONFIG_REPO_DIR)
|
38
|
-
return download_config_repo
|
39
|
-
end
|
40
|
-
|
41
|
-
# 切换到仓库目录并拉取更新
|
42
|
-
Dir.chdir(CONFIG_REPO_DIR) do
|
43
|
-
# 重置本地更改
|
44
|
-
reset_success = system('git reset --hard HEAD', out: File::NULL, err: File::NULL)
|
45
|
-
unless reset_success
|
46
|
-
FileUtils.rm_rf(CONFIG_REPO_DIR)
|
47
|
-
return download_config_repo
|
48
|
-
end
|
49
|
-
|
50
|
-
# 拉取最新更新,捕获错误信息
|
51
|
-
stdout, stderr, status = Open3.capture3('git pull origin master')
|
52
|
-
|
53
|
-
unless status.success?
|
54
|
-
puts "⚠️ 配置更新失败,使用缓存配置" if stderr && !stderr.empty?
|
55
|
-
FileUtils.rm_rf(CONFIG_REPO_DIR)
|
56
|
-
return download_config_repo
|
57
|
-
end
|
58
|
-
end
|
59
|
-
|
60
|
-
return true
|
61
|
-
|
62
|
-
rescue => e
|
63
|
-
FileUtils.rm_rf(CONFIG_REPO_DIR) if Dir.exist?(CONFIG_REPO_DIR)
|
64
|
-
return download_config_repo
|
65
|
-
end
|
9
|
+
attr_reader :verbose, :tool_type, :platform
|
10
|
+
|
11
|
+
def initialize(options = {})
|
12
|
+
@verbose = options[:verbose] || false
|
13
|
+
@tool_type = options[:tool_type]
|
14
|
+
@platform = options[:platform]
|
15
|
+
@platform_flag = options[:platform_flag] || false
|
66
16
|
end
|
67
|
-
|
68
|
-
#
|
69
|
-
def self.
|
70
|
-
|
71
|
-
|
72
|
-
begin
|
73
|
-
# 克隆仓库到固定位置,捕获错误信息
|
74
|
-
stdout, stderr, status = Open3.capture3("git clone --quiet #{REPO_URL} #{CONFIG_REPO_DIR}")
|
75
|
-
|
76
|
-
unless status.success?
|
77
|
-
puts "✗ 配置仓库下载失败"
|
78
|
-
puts " 错误信息: #{stderr.chomp}" if stderr && !stderr.empty?
|
79
|
-
puts " 请检查网络连接、git 安装状态"
|
80
|
-
return false
|
81
|
-
end
|
82
|
-
|
83
|
-
puts "✓ 配置仓库下载成功"
|
84
|
-
return true
|
85
|
-
|
86
|
-
rescue => e
|
87
|
-
puts "✗ 下载配置仓库失败: #{e.message}"
|
88
|
-
return false
|
89
|
-
end
|
17
|
+
|
18
|
+
# 类方法兼容接口
|
19
|
+
def self.download_user_config(user_name = nil, options = {})
|
20
|
+
manager = new(options)
|
21
|
+
manager.download_user_config(user_name)
|
90
22
|
end
|
91
|
-
|
92
|
-
# 从本地配置仓库加载配置
|
93
|
-
def self.load_from_local_repo(user_name = nil, options = {})
|
94
|
-
# 首先管理配置仓库
|
95
|
-
return nil unless manage_config_repo
|
96
|
-
|
97
|
-
# 设置全局选项
|
98
|
-
@@no_keychain = options[:no_keychain] || false
|
99
|
-
@@verbose = options[:verbose] || false
|
100
|
-
@@tool_type = options[:tool_type] || nil
|
101
|
-
|
102
|
-
puts "正在从本地配置仓库加载配置..." if @@verbose
|
103
|
-
puts " 工具类型: #{@@tool_type || '自动'}" if @@verbose && @@tool_type
|
104
|
-
|
105
|
-
begin
|
106
|
-
# 检查 index.json 文件,支持加密版本
|
107
|
-
index_file = File.join(CONFIG_REPO_DIR, "index.json")
|
108
|
-
encrypted_index_file = File.join(CONFIG_REPO_DIR, "index.json.encrypted")
|
109
|
-
|
110
|
-
# 优先尝试加密的 index 文件
|
111
|
-
if File.exist?(encrypted_index_file)
|
112
|
-
users = parse_encrypted_index_file(encrypted_index_file)
|
113
|
-
elsif File.exist?(index_file)
|
114
|
-
users = parse_index_file(index_file)
|
115
|
-
else
|
116
|
-
puts "未找到 index.json 或 index.json.encrypted,使用默认配置"
|
117
|
-
config_file = File.join(CONFIG_REPO_DIR, "claude_setting.json")
|
118
|
-
encrypted_config_file = File.join(CONFIG_REPO_DIR, "claude_setting.json.encrypted")
|
119
|
-
|
120
|
-
if File.exist?(encrypted_config_file)
|
121
|
-
return load_encrypted_config_file(encrypted_config_file)
|
122
|
-
elsif File.exist?(config_file)
|
123
|
-
return load_config_file(config_file)
|
124
|
-
else
|
125
|
-
puts "✗ 未找到配置文件"
|
126
|
-
return nil
|
127
|
-
end
|
128
|
-
end
|
129
|
-
|
130
|
-
return nil if users.nil?
|
131
23
|
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
end
|
137
|
-
|
138
|
-
# 获取用户名:优先使用提供的用户名,否则通过 JPS 登录获取
|
139
|
-
if user_name
|
140
|
-
selected_user = user_name
|
141
|
-
else
|
142
|
-
# 统一使用 JPS 登录获取用户名
|
143
|
-
selected_user = get_username_from_jps(@@verbose)
|
144
|
-
|
145
|
-
# 处理用户取消的情况
|
146
|
-
if selected_user == :user_cancelled
|
147
|
-
puts "✗ 用户取消了授权登录"
|
148
|
-
return :user_cancelled
|
149
|
-
elsif selected_user.nil? || selected_user.strip.empty?
|
150
|
-
puts "✗ JPS 登录失败,无法获取用户名"
|
151
|
-
return nil
|
152
|
-
end
|
153
|
-
end
|
154
|
-
|
155
|
-
# 查找用户配置文件(传入工具类型)
|
156
|
-
config_filename = find_user_config(users, selected_user.strip, @@tool_type)
|
157
|
-
|
158
|
-
if config_filename.nil?
|
159
|
-
puts "✗ 用户 '#{selected_user}' 未找到"
|
160
|
-
# 不显示可用用户列表,保护配置信息
|
161
|
-
puts " 请联系管理员确认用户权限"
|
162
|
-
return nil
|
163
|
-
end
|
24
|
+
# 主要接口:下载用户配置
|
25
|
+
def download_user_config(user_name = nil)
|
26
|
+
puts "正在加载配置..." if @verbose
|
27
|
+
log_config_info if @verbose
|
164
28
|
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
if File.exist?(encrypted_config_file)
|
173
|
-
load_encrypted_config_file(encrypted_config_file)
|
174
|
-
elsif File.exist?(config_file)
|
175
|
-
load_config_file(config_file)
|
176
|
-
else
|
177
|
-
puts "✗ 配置文件未找到: #{config_filename}"
|
178
|
-
return nil
|
179
|
-
end
|
180
|
-
|
181
|
-
rescue => e
|
182
|
-
puts "✗ 加载本地配置失败: #{e.message}"
|
183
|
-
nil
|
184
|
-
end
|
185
|
-
end
|
186
|
-
|
187
|
-
def self.download_user_config(user_name = nil, options = {})
|
188
|
-
# 优先尝试使用本地配置仓库
|
189
|
-
config = load_from_local_repo(user_name, options)
|
29
|
+
# 确保配置仓库就绪
|
30
|
+
EasyAIConfig.initialize(verbose: @verbose)
|
31
|
+
|
32
|
+
# 获取 index 配置
|
33
|
+
users = get_index_config
|
34
|
+
return nil unless users
|
35
|
+
return nil if users_empty?(users)
|
190
36
|
|
191
|
-
#
|
192
|
-
|
193
|
-
|
37
|
+
# 获取用户名
|
38
|
+
selected_user = user_name || get_username_from_jps
|
39
|
+
return handle_user_result(selected_user) unless selected_user.is_a?(String)
|
40
|
+
|
41
|
+
# 处理平台选择
|
42
|
+
if @platform_flag && @tool_type == "claude"
|
43
|
+
@platform = select_platform_for_user(users, selected_user.strip)
|
44
|
+
return nil unless @platform
|
194
45
|
end
|
195
46
|
|
196
|
-
|
47
|
+
# 查找并加载配置
|
48
|
+
config_filename = find_user_config(users, selected_user.strip)
|
49
|
+
return nil unless config_filename
|
197
50
|
|
198
|
-
#
|
199
|
-
|
200
|
-
download_user_config_temp(user_name, options)
|
51
|
+
puts "✓ 正在为 #{selected_user} 加载配置" if @verbose
|
52
|
+
get_user_config_content(config_filename)
|
201
53
|
end
|
202
|
-
|
203
|
-
# 原有的临时下载方式(作为备用方案)
|
204
|
-
def self.download_user_config_temp(user_name = nil, options = {})
|
205
|
-
puts "正在获取配置文件..."
|
206
|
-
|
207
|
-
# 设置全局选项
|
208
|
-
@@no_keychain = options[:no_keychain] || false
|
209
|
-
@@verbose = options[:verbose] || false
|
210
|
-
@@tool_type = options[:tool_type] || nil
|
211
|
-
|
212
|
-
begin
|
213
|
-
# 创建临时目录
|
214
|
-
temp_dir = Dir.mktmpdir("EasyAISetting")
|
215
|
-
|
216
|
-
# 克隆仓库
|
217
|
-
clone_cmd = "git clone --depth 1 --quiet #{REPO_URL} #{temp_dir}"
|
218
|
-
success = system(clone_cmd, out: File::NULL, err: File::NULL)
|
219
|
-
|
220
|
-
unless success
|
221
|
-
puts "✗ 配置文件获取失败"
|
222
|
-
puts " 请检查网络连接、git 安装,或使用本地配置文件"
|
223
|
-
cleanup_temp_dir(temp_dir)
|
224
|
-
return nil
|
225
|
-
end
|
226
|
-
|
227
|
-
puts "✓ 配置文件获取成功"
|
228
|
-
|
229
|
-
# 检查 index.json 文件,支持加密版本
|
230
|
-
index_file = File.join(temp_dir, "index.json")
|
231
|
-
encrypted_index_file = File.join(temp_dir, "index.json.encrypted")
|
232
|
-
|
233
|
-
# 优先尝试加密的 index 文件
|
234
|
-
if File.exist?(encrypted_index_file)
|
235
|
-
users = parse_encrypted_index_file(encrypted_index_file)
|
236
|
-
elsif File.exist?(index_file)
|
237
|
-
users = parse_index_file(index_file)
|
238
|
-
else
|
239
|
-
puts "未找到 index.json 或 index.json.encrypted,使用默认配置"
|
240
|
-
config_file = File.join(temp_dir, "claude_setting.json")
|
241
|
-
encrypted_config_file = File.join(temp_dir, "claude_setting.json.encrypted")
|
242
|
-
|
243
|
-
if File.exist?(encrypted_config_file)
|
244
|
-
config_content = load_encrypted_config_file(encrypted_config_file)
|
245
|
-
elsif File.exist?(config_file)
|
246
|
-
config_content = load_config_file(config_file)
|
247
|
-
else
|
248
|
-
puts "✗ 未找到配置文件"
|
249
|
-
cleanup_temp_dir(temp_dir)
|
250
|
-
return nil
|
251
|
-
end
|
252
|
-
|
253
|
-
cleanup_temp_dir(temp_dir)
|
254
|
-
return config_content
|
255
|
-
end
|
256
|
-
|
257
|
-
return nil if users.nil?
|
258
54
|
|
259
|
-
|
260
|
-
if is_users_empty?(users)
|
261
|
-
puts "✗ index.json 中未找到用户"
|
262
|
-
cleanup_temp_dir(temp_dir)
|
263
|
-
return nil
|
264
|
-
end
|
265
|
-
|
266
|
-
# 获取用户名:优先使用提供的用户名,否则通过 JPS 登录获取
|
267
|
-
if user_name
|
268
|
-
selected_user = user_name
|
269
|
-
else
|
270
|
-
# 统一使用 JPS 登录获取用户名
|
271
|
-
selected_user = get_username_from_jps(@@verbose)
|
55
|
+
private
|
272
56
|
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
return nil
|
278
|
-
elsif selected_user.nil? || selected_user.strip.empty?
|
279
|
-
puts "✗ JPS 登录失败,无法获取用户名"
|
280
|
-
cleanup_temp_dir(temp_dir)
|
281
|
-
return nil
|
282
|
-
end
|
283
|
-
end
|
284
|
-
|
285
|
-
# 查找用户配置文件(传入工具类型)
|
286
|
-
config_filename = find_user_config(users, selected_user.strip, @@tool_type)
|
287
|
-
|
288
|
-
if config_filename.nil?
|
289
|
-
puts "✗ 用户 '#{selected_user}' 未找到"
|
290
|
-
# 不显示可用用户列表,保护配置信息
|
291
|
-
puts " 请联系管理员确认用户权限"
|
292
|
-
cleanup_temp_dir(temp_dir)
|
293
|
-
return nil
|
294
|
-
end
|
57
|
+
def log_config_info
|
58
|
+
puts " 工具类型: #{@tool_type || '自动'}" if @tool_type
|
59
|
+
puts " 认证平台: #{@platform || (@platform_flag ? '交互式选择' : '自动选择')}" if @tool_type == "claude"
|
60
|
+
end
|
295
61
|
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
cleanup_temp_dir(temp_dir)
|
314
|
-
config_content
|
315
|
-
|
316
|
-
rescue => e
|
317
|
-
puts "✗ 配置获取失败: #{e.message}"
|
318
|
-
cleanup_temp_dir(temp_dir) if temp_dir
|
62
|
+
def get_index_config
|
63
|
+
# 尝试获取 index 配置,优先加密版本
|
64
|
+
users = EasyAIConfig.get_config('index', verbose: @verbose)
|
65
|
+
return users if users
|
66
|
+
|
67
|
+
# 回退到默认配置
|
68
|
+
puts "未找到 index.json,使用默认配置" if @verbose
|
69
|
+
EasyAIConfig.get_config('claude_setting', verbose: @verbose)
|
70
|
+
end
|
71
|
+
|
72
|
+
def handle_user_result(result)
|
73
|
+
case result
|
74
|
+
when :user_cancelled
|
75
|
+
puts "✗ 用户取消了授权登录"
|
76
|
+
:user_cancelled
|
77
|
+
when nil
|
78
|
+
puts "✗ JPS 登录失败,无法获取用户名"
|
319
79
|
nil
|
80
|
+
else
|
81
|
+
result
|
320
82
|
end
|
321
83
|
end
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
return
|
327
|
-
|
84
|
+
|
85
|
+
def get_user_config_content(config_filename)
|
86
|
+
# 直接使用 get_config 获取配置内容,它会自动处理解密和清理
|
87
|
+
config = EasyAIConfig.get_config(config_filename, verbose: @verbose)
|
88
|
+
return config if config
|
89
|
+
|
90
|
+
puts "✗ 无法加载配置文件: #{config_filename}" if @verbose
|
91
|
+
nil
|
328
92
|
end
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
93
|
+
|
94
|
+
# JPS 登录获取用户名
|
95
|
+
def get_username_from_jps
|
96
|
+
jps_login = Auth::JPSLoginHelper.new(verbose: @verbose)
|
97
|
+
login_result = jps_login.login
|
98
|
+
|
99
|
+
return :user_cancelled if login_result == :user_cancelled
|
100
|
+
return nil unless login_result
|
101
|
+
|
102
|
+
username = jps_login.get_username
|
103
|
+
if username && !username.empty?
|
104
|
+
puts "👤 用户: #{username}" if @verbose
|
105
|
+
username
|
106
|
+
else
|
339
107
|
nil
|
340
108
|
end
|
109
|
+
rescue => e
|
110
|
+
puts "❌ JPS 登录失败: #{e.message}" if @verbose
|
111
|
+
nil
|
341
112
|
end
|
342
|
-
|
343
|
-
# 通过 JPS 登录获取用户名
|
344
|
-
def self.get_username_from_jps(verbose = false)
|
345
|
-
begin
|
346
|
-
jps_login = Auth::JPSLogin.new(verbose: verbose)
|
347
|
-
login_result = jps_login.login
|
348
|
-
|
349
|
-
# 处理用户主动取消的情况
|
350
|
-
if login_result == :user_cancelled
|
351
|
-
return :user_cancelled
|
352
|
-
end
|
353
113
|
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
puts "👤 用户: #{username}"
|
358
|
-
return username
|
359
|
-
end
|
360
|
-
end
|
361
|
-
rescue => e
|
362
|
-
puts "❌ JPS 登录失败: #{e.message}"
|
363
|
-
end
|
114
|
+
# 检查用户列表是否为空
|
115
|
+
def users_empty?(users)
|
116
|
+
return true if users.nil? || users.empty?
|
364
117
|
|
365
|
-
|
366
|
-
end
|
367
|
-
|
368
|
-
def self.find_user_config(users, user_input_clean, tool_type = nil)
|
369
|
-
# 处理新的分组格式 {"claude": {...}, "gemini": {...}}
|
118
|
+
# 处理新的分组格式
|
370
119
|
if users.is_a?(Hash) && (users.key?("claude") || users.key?("gemini"))
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
else
|
381
|
-
# 未指定工具类型时,按优先级查找(claude > gemini > 其他)
|
382
|
-
%w[claude gemini gpt].each do |group|
|
383
|
-
next unless users[group]
|
384
|
-
users[group].each do |name, filename|
|
385
|
-
if name.downcase == user_input_clean.downcase
|
386
|
-
# 确保文件名包含 .json 后缀
|
387
|
-
filename = "#{filename}.json" unless filename.end_with?('.json')
|
388
|
-
return filename
|
120
|
+
%w[claude gemini gpt].each do |group|
|
121
|
+
next unless users[group]
|
122
|
+
|
123
|
+
if group == "claude" && users[group].is_a?(Hash)
|
124
|
+
# Claude 需要检查认证类型
|
125
|
+
users[group].each do |auth_type, auth_users|
|
126
|
+
next unless auth_users.is_a?(Hash)
|
127
|
+
auth_users.each do |name, filename|
|
128
|
+
return false if !filename.nil? && !filename.empty?
|
389
129
|
end
|
390
130
|
end
|
131
|
+
elsif !users[group].empty?
|
132
|
+
return false
|
391
133
|
end
|
392
134
|
end
|
135
|
+
true
|
393
136
|
else
|
394
|
-
|
395
|
-
users.each do |name, filename|
|
396
|
-
if name.downcase == user_input_clean.downcase
|
397
|
-
# 确保文件名包含 .json 后缀
|
398
|
-
filename = "#{filename}.json" unless filename.end_with?('.json')
|
399
|
-
return filename
|
400
|
-
end
|
401
|
-
end
|
137
|
+
users.empty?
|
402
138
|
end
|
403
|
-
|
404
|
-
nil
|
405
139
|
end
|
406
140
|
|
407
|
-
#
|
408
|
-
def
|
409
|
-
all_names = []
|
410
|
-
|
141
|
+
# 查找用户配置文件
|
142
|
+
def find_user_config(users, user_input_clean)
|
411
143
|
# 处理新的分组格式
|
412
144
|
if users.is_a?(Hash) && (users.key?("claude") || users.key?("gemini"))
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
145
|
+
if @tool_type == "claude" && users["claude"].is_a?(Hash)
|
146
|
+
find_claude_user_config(users["claude"], user_input_clean)
|
147
|
+
elsif @tool_type && users[@tool_type]
|
148
|
+
find_tool_user_config(users[@tool_type], user_input_clean)
|
149
|
+
else
|
150
|
+
# 未指定工具类型,按优先级查找
|
151
|
+
find_any_user_config(users, user_input_clean)
|
418
152
|
end
|
419
153
|
else
|
420
154
|
# 兼容旧格式
|
421
|
-
|
155
|
+
find_legacy_user_config(users, user_input_clean)
|
422
156
|
end
|
423
|
-
|
424
|
-
all_names
|
425
157
|
end
|
426
158
|
|
427
|
-
|
428
|
-
|
429
|
-
return [] unless tool_type
|
159
|
+
def find_tool_user_config(tool_users, user_input_clean)
|
160
|
+
return nil unless tool_users.is_a?(Hash)
|
430
161
|
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
else
|
436
|
-
# 旧格式返回所有用户(因为没有工具分组)
|
437
|
-
users.is_a?(Hash) ? users.keys : []
|
162
|
+
tool_users.each do |name, filename|
|
163
|
+
if name.downcase == user_input_clean.downcase
|
164
|
+
return normalize_config_filename(filename)
|
165
|
+
end
|
438
166
|
end
|
167
|
+
nil
|
439
168
|
end
|
440
169
|
|
441
|
-
|
442
|
-
|
443
|
-
|
170
|
+
def find_any_user_config(users, user_input_clean)
|
171
|
+
%w[claude gemini gpt].each do |group|
|
172
|
+
next unless users[group]
|
444
173
|
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
174
|
+
if group == "claude" && users[group].is_a?(Hash)
|
175
|
+
config = find_claude_user_config(users[group], user_input_clean)
|
176
|
+
return config if config
|
177
|
+
elsif users[group].is_a?(Hash)
|
178
|
+
config = find_tool_user_config(users[group], user_input_clean)
|
179
|
+
return config if config
|
450
180
|
end
|
451
|
-
true
|
452
|
-
else
|
453
|
-
# 兼容旧格式
|
454
|
-
users.empty?
|
455
181
|
end
|
182
|
+
nil
|
456
183
|
end
|
457
|
-
|
458
|
-
def
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
puts "✗ 解析配置文件失败: #{e.message}"
|
464
|
-
nil
|
465
|
-
rescue => e
|
466
|
-
puts "✗ 读取配置文件失败: #{e.message}"
|
467
|
-
nil
|
184
|
+
|
185
|
+
def find_legacy_user_config(users, user_input_clean)
|
186
|
+
users.each do |name, filename|
|
187
|
+
if name.downcase == user_input_clean.downcase
|
188
|
+
return normalize_config_filename(filename)
|
189
|
+
end
|
468
190
|
end
|
191
|
+
nil
|
469
192
|
end
|
470
|
-
|
471
|
-
def
|
472
|
-
|
473
|
-
|
474
|
-
# 使用新的密码验证机制
|
475
|
-
result = get_and_validate_password(encrypted_config_file, "请输入解密密码: ")
|
476
|
-
return nil unless result
|
477
|
-
|
478
|
-
begin
|
479
|
-
# 解析 JSON
|
480
|
-
JSON.parse(result[:content])
|
481
|
-
rescue JSON::ParserError => e
|
482
|
-
puts "✗ 解析解密后的配置文件失败: #{e.message}"
|
483
|
-
puts "文件可能已损坏或不是有效的JSON格式"
|
484
|
-
nil
|
485
|
-
end
|
193
|
+
|
194
|
+
def normalize_config_filename(filename)
|
195
|
+
filename = "#{filename}.json" unless filename.end_with?('.json')
|
196
|
+
filename
|
486
197
|
end
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
198
|
+
|
199
|
+
# 专门处理 Claude 的用户配置查找
|
200
|
+
def find_claude_user_config(claude_config, user_input_clean)
|
201
|
+
auth_priority = ["claude_auth", "kimi_auth", "deepseek_auth"]
|
202
|
+
|
203
|
+
# Windows 平台处理:从优先级中移除 claude_auth
|
204
|
+
if Gem.win_platform?
|
205
|
+
auth_priority = ["kimi_auth", "deepseek_auth"]
|
206
|
+
puts " ⚠ Windows 平台跳过 claude_auth,使用其他认证平台" if @verbose
|
207
|
+
end
|
208
|
+
|
209
|
+
if @platform && !@platform.empty?
|
210
|
+
# 指定了平台
|
211
|
+
return find_claude_config_by_platform(claude_config, @platform, user_input_clean, auth_priority)
|
212
|
+
else
|
213
|
+
# 未指定平台,按优先级查找
|
214
|
+
return find_claude_config_auto(claude_config, user_input_clean, auth_priority)
|
502
215
|
end
|
503
216
|
end
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
password_info = get_decryption_password_basic(prompt, retries > 0)
|
512
|
-
return nil unless password_info
|
513
|
-
|
514
|
-
# 尝试解密验证密码
|
515
|
-
begin
|
516
|
-
decrypted_content = decrypt_file_content(encrypted_file_path, password_info[:password])
|
517
|
-
|
518
|
-
# 解密成功,保存密码(如果不是从系统获取且未禁用存储)
|
519
|
-
unless password_info[:from_system] || @@no_keychain
|
520
|
-
if Base::SystemKeychain.store_password(password_info[:password])
|
521
|
-
puts "✓ 密码已安全存储,下次使用将自动获取"
|
522
|
-
end
|
523
|
-
end
|
524
|
-
|
525
|
-
return { content: decrypted_content, password_info: password_info }
|
526
|
-
|
527
|
-
rescue => e
|
528
|
-
retries += 1
|
529
|
-
|
530
|
-
# 分析错误类型并提供针对性提示
|
531
|
-
error_type = analyze_decryption_error(e)
|
532
|
-
|
533
|
-
# 处理系统密码错误
|
534
|
-
if password_info[:from_system]
|
535
|
-
puts "✗ 系统存储的密码无效"
|
536
|
-
puts " 错误详情:#{e.message}" if error_type[:show_details]
|
537
|
-
puts " 错误原因:#{error_type[:reason]}"
|
538
|
-
puts "正在清除无效的系统存储密码..."
|
539
|
-
Base::SystemKeychain.delete_stored_password
|
540
|
-
puts "✓ 已清除系统存储的无效密码"
|
541
|
-
puts "请重新输入正确的解密密码"
|
542
|
-
else
|
543
|
-
puts "✗ 解密失败"
|
544
|
-
puts " 错误详情:#{e.message}" if error_type[:show_details]
|
545
|
-
puts " 错误原因:#{error_type[:reason]}"
|
546
|
-
puts " 建议:#{error_type[:suggestion]}"
|
547
|
-
end
|
548
|
-
|
549
|
-
# 检查是否达到最大重试次数
|
550
|
-
if retries >= max_retries
|
551
|
-
puts "\n已达到最大重试次数 (#{max_retries})"
|
552
|
-
puts "可能的解决方案:"
|
553
|
-
puts " 1. 确认密码是否正确"
|
554
|
-
puts " 2. 检查文件是否完整下载"
|
555
|
-
puts " 3. 联系管理员确认密码是否已更改"
|
556
|
-
|
557
|
-
if ask_user_continue?
|
558
|
-
retries = 0 # 重置重试次数,继续尝试
|
559
|
-
puts "\n继续尝试解密..."
|
560
|
-
else
|
561
|
-
puts "用户选择退出解密"
|
562
|
-
return nil
|
563
|
-
end
|
564
|
-
else
|
565
|
-
remaining = max_retries - retries
|
566
|
-
puts "还可以重试 #{remaining} 次 (#{retries}/#{max_retries})"
|
567
|
-
puts "提示:按 Ctrl+C 可随时退出\n"
|
568
|
-
end
|
569
|
-
end
|
217
|
+
|
218
|
+
def find_claude_config_by_platform(claude_config, platform, user_input_clean, auth_priority)
|
219
|
+
# Windows 平台特殊处理 claude_auth
|
220
|
+
if Gem.win_platform? && platform == "claude_auth"
|
221
|
+
puts "✗ Windows 平台不支持 claude_auth" if @verbose
|
222
|
+
puts " 请选择其他认证平台:kimi_auth 或 deepseek_auth" if @verbose
|
223
|
+
return nil
|
570
224
|
end
|
225
|
+
|
226
|
+
unless auth_priority.include?(platform)
|
227
|
+
puts "✗ 认证平台 '#{platform}' 不存在" if @verbose
|
228
|
+
puts " 支持的平台: #{auth_priority.join(', ')}" if @verbose
|
229
|
+
return nil
|
230
|
+
end
|
231
|
+
|
232
|
+
config = find_in_auth_type(claude_config, platform, user_input_clean)
|
233
|
+
if config.nil? && @verbose
|
234
|
+
puts "✗ 用户 '#{user_input_clean}' 在 #{platform} 中未找到配置"
|
235
|
+
puts " 请联系管理员添加配置或尝试其他认证平台"
|
236
|
+
elsif config && @verbose
|
237
|
+
puts "✓ 使用认证平台: #{platform}"
|
238
|
+
end
|
239
|
+
config
|
571
240
|
end
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
241
|
+
|
242
|
+
def find_claude_config_auto(claude_config, user_input_clean, auth_priority)
|
243
|
+
auth_priority.each do |auth_type|
|
244
|
+
config = find_in_auth_type(claude_config, auth_type, user_input_clean)
|
245
|
+
if config
|
246
|
+
puts "✓ 自动选择认证平台: #{auth_type}" if @verbose
|
247
|
+
return config
|
579
248
|
end
|
580
|
-
password = Base::FileCrypto.get_password(prompt)
|
581
|
-
return password ? { password: password, from_system: false } : nil
|
582
249
|
end
|
583
|
-
|
584
|
-
|
585
|
-
|
586
|
-
|
587
|
-
if stored_password && !stored_password.empty?
|
588
|
-
puts "使用系统存储的密码进行解密..." if @@verbose
|
589
|
-
return { password: stored_password, from_system: true }
|
250
|
+
|
251
|
+
if @verbose
|
252
|
+
puts "✗ 用户 '#{user_input_clean}' 未找到任何配置"
|
253
|
+
puts " 已尝试: #{auth_priority.join(', ')}"
|
590
254
|
end
|
591
|
-
|
592
|
-
# 如果没有存储的密码,提示用户输入
|
593
|
-
password = Base::FileCrypto.get_password(prompt)
|
594
|
-
return password ? { password: password, from_system: false } : nil
|
255
|
+
nil
|
595
256
|
end
|
596
|
-
|
597
|
-
#
|
598
|
-
def
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
|
603
|
-
|
604
|
-
|
605
|
-
|
606
|
-
when /not supported by device/i
|
607
|
-
{
|
608
|
-
reason: "系统不支持此加密算法或文件格式错误",
|
609
|
-
suggestion: "请检查文件是否为正确的加密文件",
|
610
|
-
show_details: true
|
611
|
-
}
|
612
|
-
when /no such file/i, /cannot open/i
|
613
|
-
{
|
614
|
-
reason: "文件不存在或无法访问",
|
615
|
-
suggestion: "请检查文件路径和权限",
|
616
|
-
show_details: false
|
617
|
-
}
|
618
|
-
when /invalid base64/i
|
619
|
-
{
|
620
|
-
reason: "文件格式错误或已损坏",
|
621
|
-
suggestion: "请重新下载配置文件",
|
622
|
-
show_details: false
|
623
|
-
}
|
624
|
-
else
|
625
|
-
{
|
626
|
-
reason: "未知的解密错误",
|
627
|
-
suggestion: "请尝试重新下载文件或联系技术支持",
|
628
|
-
show_details: true
|
629
|
-
}
|
257
|
+
|
258
|
+
# 在特定认证类型中查找用户配置
|
259
|
+
def find_in_auth_type(claude_config, auth_type, user_input_clean)
|
260
|
+
return nil unless claude_config[auth_type].is_a?(Hash)
|
261
|
+
|
262
|
+
claude_config[auth_type].each do |name, filename|
|
263
|
+
if name.downcase == user_input_clean.downcase && !filename.nil? && !filename.empty?
|
264
|
+
filename = normalize_config_filename(filename)
|
265
|
+
return "claude/#{auth_type}/#{filename}"
|
266
|
+
end
|
630
267
|
end
|
268
|
+
nil
|
631
269
|
end
|
632
|
-
|
633
|
-
#
|
634
|
-
def
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
|
642
|
-
|
643
|
-
|
644
|
-
|
645
|
-
|
646
|
-
|
647
|
-
|
648
|
-
|
270
|
+
|
271
|
+
# 让用户选择可用的认证平台
|
272
|
+
def select_platform_for_user(users, user_input_clean)
|
273
|
+
return nil unless users["claude"].is_a?(Hash)
|
274
|
+
|
275
|
+
claude_config = users["claude"]
|
276
|
+
available_platforms = []
|
277
|
+
|
278
|
+
# 检查用户在哪些平台有配置
|
279
|
+
platforms_to_check = ["claude_auth", "kimi_auth", "deepseek_auth"]
|
280
|
+
|
281
|
+
# Windows 平台跳过 claude_auth
|
282
|
+
if Gem.win_platform?
|
283
|
+
platforms_to_check = ["kimi_auth", "deepseek_auth"]
|
284
|
+
end
|
285
|
+
|
286
|
+
platforms_to_check.each do |auth_type|
|
287
|
+
if find_in_auth_type(claude_config, auth_type, user_input_clean)
|
288
|
+
available_platforms << auth_type
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
# 如果没有可用平台
|
293
|
+
if available_platforms.empty?
|
294
|
+
puts "✗ 用户 '#{user_input_clean}' 在任何认证平台中都没有配置"
|
295
|
+
return nil
|
296
|
+
end
|
297
|
+
|
298
|
+
# 如果只有一个平台,直接使用
|
299
|
+
if available_platforms.length == 1
|
300
|
+
selected = available_platforms.first
|
301
|
+
puts "✓ 自动选择唯一可用平台: #{selected}" if @verbose
|
302
|
+
return selected
|
303
|
+
end
|
304
|
+
|
305
|
+
# 多个平台,让用户选择
|
306
|
+
puts "\n🔐 请选择认证平台:"
|
307
|
+
puts "─" * 40
|
308
|
+
|
309
|
+
available_platforms.each_with_index do |platform, index|
|
310
|
+
platform_name = case platform
|
311
|
+
when "claude_auth" then "Claude 官方认证"
|
312
|
+
when "kimi_auth" then "Kimi 认证"
|
313
|
+
when "deepseek_auth" then "Deepseek 认证"
|
314
|
+
else platform
|
315
|
+
end
|
316
|
+
puts " #{index + 1}. #{platform_name} (#{platform})"
|
317
|
+
end
|
318
|
+
|
319
|
+
puts "─" * 40
|
320
|
+
|
321
|
+
loop do
|
322
|
+
print "请输入选择 (1-#{available_platforms.length},输入 q 退出): "
|
323
|
+
|
324
|
+
begin
|
325
|
+
choice = STDIN.gets&.chomp
|
326
|
+
|
327
|
+
# 用户选择退出
|
328
|
+
if choice.nil? || choice.downcase == 'q'
|
329
|
+
puts "✗ 用户取消选择"
|
330
|
+
return nil
|
331
|
+
end
|
332
|
+
|
333
|
+
# 验证输入
|
334
|
+
choice_num = choice.to_i
|
335
|
+
if choice_num >= 1 && choice_num <= available_platforms.length
|
336
|
+
selected = available_platforms[choice_num - 1]
|
337
|
+
puts "✓ 已选择: #{selected}" if @verbose
|
338
|
+
return selected
|
339
|
+
else
|
340
|
+
puts "✗ 无效的选择,请输入 1-#{available_platforms.length} 之间的数字"
|
341
|
+
end
|
342
|
+
rescue Interrupt
|
343
|
+
puts "\n\n✗ 用户中断操作"
|
344
|
+
return nil
|
345
|
+
rescue => e
|
346
|
+
puts "✗ 输入错误: #{e.message}"
|
347
|
+
return nil
|
348
|
+
end
|
649
349
|
end
|
650
|
-
end
|
651
|
-
|
652
|
-
def self.decrypt_file_content(encrypted_file_path, password)
|
653
|
-
# 生成密钥
|
654
|
-
key = Base::FileCrypto.generate_key(password)
|
655
|
-
|
656
|
-
# 读取加密文件内容
|
657
|
-
encrypted_content = File.read(encrypted_file_path)
|
658
|
-
|
659
|
-
# 解密内容
|
660
|
-
Base::FileCrypto.aes_128_ecb_decrypt(key, encrypted_content)
|
661
350
|
end
|
662
351
|
end
|
663
352
|
end
|