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.
- checksums.yaml +4 -4
- data/CLAUDE.md +154 -5
- data/README.md +38 -57
- data/bin/easyai +5 -5
- data/easyai.gemspec +1 -0
- 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 +31 -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,440 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'etc'
|
3
|
+
require 'open3'
|
4
|
+
require 'fileutils'
|
5
|
+
|
6
|
+
module EasyAI
|
7
|
+
module Auth
|
8
|
+
class AuthClaude
|
9
|
+
def self.configure(config, user_name = nil)
|
10
|
+
return false unless config
|
11
|
+
|
12
|
+
# 检查认证配置优先级
|
13
|
+
is_macos = RUBY_PLATFORM.include?('darwin')
|
14
|
+
keychain_exists = is_macos && config["key_chain"] && config["key_chain"]["claudeAiOauth"] && config["key_chain"]["service_name"]
|
15
|
+
env_token_exists = config["env"] && config["env"]["CLAUDE_CODE_OAUTH_TOKEN"]
|
16
|
+
|
17
|
+
# 如果使用 Keychain,需要将令牌从 Keychain 读取到配置中
|
18
|
+
if keychain_exists
|
19
|
+
token_from_keychain = get_token_from_keychain(config["key_chain"]["service_name"])
|
20
|
+
if token_from_keychain
|
21
|
+
config["env"] ||= {}
|
22
|
+
config["env"]["CLAUDE_CODE_OAUTH_TOKEN"] = token_from_keychain
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# 根据优先级确定认证方法
|
27
|
+
if keychain_exists
|
28
|
+
configure_keychain(config)
|
29
|
+
clean_env_variables
|
30
|
+
elsif env_token_exists
|
31
|
+
verify_token_config(config)
|
32
|
+
clean_keychain if is_macos
|
33
|
+
else
|
34
|
+
token = prompt_for_token
|
35
|
+
return false if token.nil? || token.empty?
|
36
|
+
|
37
|
+
# 将令牌添加到配置中进行处理
|
38
|
+
config["env"] ||= {}
|
39
|
+
config["env"]["CLAUDE_CODE_OAUTH_TOKEN"] = token
|
40
|
+
|
41
|
+
verify_token_config(config)
|
42
|
+
clean_keychain
|
43
|
+
end
|
44
|
+
|
45
|
+
# 更新 ~/.claude.json 文件
|
46
|
+
update_claude_json(config) if config["claude_json"]
|
47
|
+
|
48
|
+
# 配置代理设置
|
49
|
+
configure_proxy(config) if config["claude_proxy"]
|
50
|
+
|
51
|
+
true
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def self.get_token_from_keychain(service_name)
|
57
|
+
return nil unless RUBY_PLATFORM.include?('darwin')
|
58
|
+
|
59
|
+
account_name = Etc.getlogin || ENV['USER'] || ENV['LOGNAME']
|
60
|
+
|
61
|
+
cmd = [
|
62
|
+
"security",
|
63
|
+
"find-generic-password",
|
64
|
+
"-a", account_name,
|
65
|
+
"-s", service_name,
|
66
|
+
"-w"
|
67
|
+
]
|
68
|
+
|
69
|
+
begin
|
70
|
+
stdout, stderr, status = Open3.capture3(*cmd)
|
71
|
+
|
72
|
+
if status.success? && !stdout.strip.empty?
|
73
|
+
# 解析 JSON 格式的密码
|
74
|
+
credentials = JSON.parse(stdout.strip)
|
75
|
+
return credentials["claudeAiOauth"]
|
76
|
+
end
|
77
|
+
rescue JSON::ParserError, StandardError
|
78
|
+
# 如果不是 JSON 格式,尝试直接返回
|
79
|
+
return stdout.strip if status.success? && !stdout.strip.empty?
|
80
|
+
end
|
81
|
+
|
82
|
+
nil
|
83
|
+
end
|
84
|
+
|
85
|
+
def self.configure_keychain(config)
|
86
|
+
credentials = {
|
87
|
+
"claudeAiOauth" => config["key_chain"]["claudeAiOauth"]
|
88
|
+
}
|
89
|
+
|
90
|
+
service_name = config["key_chain"]["service_name"]
|
91
|
+
account_name = Etc.getlogin || ENV['USER'] || ENV['LOGNAME']
|
92
|
+
label = service_name
|
93
|
+
password = credentials.to_json
|
94
|
+
|
95
|
+
cmd = [
|
96
|
+
"security",
|
97
|
+
"add-generic-password",
|
98
|
+
"-a", account_name,
|
99
|
+
"-s", service_name,
|
100
|
+
"-l", label,
|
101
|
+
"-w", password,
|
102
|
+
"-U",
|
103
|
+
"-T", ""
|
104
|
+
]
|
105
|
+
|
106
|
+
system(*cmd)
|
107
|
+
|
108
|
+
if $?.success?
|
109
|
+
verify_keychain_entry(account_name, service_name)
|
110
|
+
else
|
111
|
+
puts "✗ 保存凭证到 Keychain 失败"
|
112
|
+
return false
|
113
|
+
end
|
114
|
+
|
115
|
+
true
|
116
|
+
end
|
117
|
+
|
118
|
+
def self.verify_keychain_entry(account_name, service_name)
|
119
|
+
verify_cmd = [
|
120
|
+
"security",
|
121
|
+
"find-generic-password",
|
122
|
+
"-a", account_name,
|
123
|
+
"-s", service_name
|
124
|
+
]
|
125
|
+
|
126
|
+
unless system(*verify_cmd, out: File::NULL, err: File::NULL)
|
127
|
+
puts "✗ 无法验证 Keychain 条目"
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def self.verify_token_config(config)
|
132
|
+
token = config["env"]["CLAUDE_CODE_OAUTH_TOKEN"]
|
133
|
+
return false unless token
|
134
|
+
|
135
|
+
# 只验证令牌存在且不为空,不写入任何配置文件
|
136
|
+
if token && !token.strip.empty?
|
137
|
+
# 清理任何现有的环境配置文件中的令牌定义
|
138
|
+
clean_env_variables
|
139
|
+
return true
|
140
|
+
else
|
141
|
+
puts "✗ 令牌配置无效"
|
142
|
+
return false
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def self.update_claude_json(config)
|
147
|
+
user_claude_file = File.expand_path("~/.claude.json")
|
148
|
+
|
149
|
+
begin
|
150
|
+
# 读取现有文件或创建空哈希
|
151
|
+
if File.exist?(user_claude_file)
|
152
|
+
user_config = JSON.parse(File.read(user_claude_file))
|
153
|
+
else
|
154
|
+
user_config = {}
|
155
|
+
end
|
156
|
+
|
157
|
+
# 合并 claude_json 内容,过滤掉代理相关字段
|
158
|
+
claude_config = config["claude_json"].dup
|
159
|
+
claude_config.delete("claude_proxy")
|
160
|
+
claude_config.delete("HTTP_PROXY")
|
161
|
+
claude_config.delete("HTTPS_PROXY")
|
162
|
+
|
163
|
+
user_config.merge!(claude_config)
|
164
|
+
|
165
|
+
# 写回文件
|
166
|
+
File.write(user_claude_file, JSON.pretty_generate(user_config))
|
167
|
+
|
168
|
+
rescue JSON::ParserError => e
|
169
|
+
puts "✗ 解析现有 ~/.claude.json 失败: #{e.message}"
|
170
|
+
false
|
171
|
+
rescue => e
|
172
|
+
puts "✗ 更新 ~/.claude.json 失败: #{e.message}"
|
173
|
+
false
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
def self.configure_proxy(config)
|
178
|
+
return unless config["claude_proxy"] && (config["claude_proxy"]["HTTP_PROXY"] || config["claude_proxy"]["HTTPS_PROXY"])
|
179
|
+
|
180
|
+
http_proxy = config["claude_proxy"]["HTTP_PROXY"]
|
181
|
+
https_proxy = config["claude_proxy"]["HTTPS_PROXY"]
|
182
|
+
|
183
|
+
shell_files = get_shell_files
|
184
|
+
|
185
|
+
shell_files.each do |shell_file|
|
186
|
+
next unless File.exist?(shell_file)
|
187
|
+
|
188
|
+
begin
|
189
|
+
content = File.read(shell_file)
|
190
|
+
lines = content.split("\n", -1)
|
191
|
+
lines.pop if lines.last == ""
|
192
|
+
|
193
|
+
# 移除所有现有的代理相关配置
|
194
|
+
lines = remove_proxy_config(lines, shell_file)
|
195
|
+
|
196
|
+
# 只添加代理别名/函数,不直接设置环境变量
|
197
|
+
lines = add_proxy_aliases(lines, shell_file, http_proxy, https_proxy)
|
198
|
+
|
199
|
+
# 写回文件
|
200
|
+
File.write(shell_file, lines.join("\n") + "\n")
|
201
|
+
|
202
|
+
rescue => e
|
203
|
+
puts " ✗ 更新 #{shell_file} 失败: #{e.message}"
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
# 显示代理配置完成信息
|
208
|
+
puts "✅ 代理别名已配置 | claude_proxy / unclaude_proxy"
|
209
|
+
end
|
210
|
+
|
211
|
+
def self.prompt_for_token
|
212
|
+
puts "请输入您的 CLAUDE_CODE_OAUTH_TOKEN:"
|
213
|
+
print "> "
|
214
|
+
|
215
|
+
begin
|
216
|
+
# 简化输入处理,直接使用 STDIN
|
217
|
+
STDIN.gets&.chomp
|
218
|
+
rescue => e
|
219
|
+
puts "✗ 无法读取输入: #{e.message}"
|
220
|
+
puts " 请使用本地配置文件或 --user 参数"
|
221
|
+
nil
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
def self.clean_env_variables
|
226
|
+
shell_files = get_shell_files
|
227
|
+
|
228
|
+
shell_files.each do |shell_file|
|
229
|
+
next unless File.exist?(shell_file)
|
230
|
+
|
231
|
+
begin
|
232
|
+
content = File.read(shell_file)
|
233
|
+
lines = content.split("\n", -1)
|
234
|
+
lines.pop if lines.last == ""
|
235
|
+
|
236
|
+
patterns = get_token_patterns(shell_file)
|
237
|
+
original_count = lines.length
|
238
|
+
lines.reject! { |line| patterns.any? { |pattern| line.match(pattern) } }
|
239
|
+
removed_count = original_count - lines.length
|
240
|
+
|
241
|
+
if removed_count > 0
|
242
|
+
File.write(shell_file, lines.join("\n") + "\n")
|
243
|
+
end
|
244
|
+
rescue => e
|
245
|
+
puts " ✗ 清理 #{shell_file} 失败: #{e.message}"
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
def self.clean_keychain
|
251
|
+
is_macos = RUBY_PLATFORM.include?('darwin')
|
252
|
+
|
253
|
+
if is_macos
|
254
|
+
service_name = "Claude Code-credentials"
|
255
|
+
account_name = Etc.getlogin || ENV['USER'] || ENV['LOGNAME']
|
256
|
+
|
257
|
+
cmd = [
|
258
|
+
"security",
|
259
|
+
"delete-generic-password",
|
260
|
+
"-a", account_name,
|
261
|
+
"-s", service_name
|
262
|
+
]
|
263
|
+
|
264
|
+
stdout, stderr, status = Open3.capture3(*cmd)
|
265
|
+
|
266
|
+
unless status.success? || stderr.include?("could not be found")
|
267
|
+
puts " 警告: 清理 Keychain 失败: #{stderr}"
|
268
|
+
end
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
def self.get_shell_files
|
273
|
+
shell_files = []
|
274
|
+
current_shell = ENV['SHELL']&.split('/')&.last || 'bash'
|
275
|
+
|
276
|
+
case current_shell
|
277
|
+
when 'zsh'
|
278
|
+
shell_files << File.expand_path("~/.zshrc")
|
279
|
+
when 'bash'
|
280
|
+
bash_profile = File.expand_path("~/.bash_profile")
|
281
|
+
bashrc = File.expand_path("~/.bashrc")
|
282
|
+
shell_files << (File.exist?(bash_profile) ? bash_profile : bashrc)
|
283
|
+
when 'fish'
|
284
|
+
fish_config = File.expand_path("~/.config/fish/config.fish")
|
285
|
+
shell_files << fish_config
|
286
|
+
|
287
|
+
# 确保 fish 配置目录存在
|
288
|
+
fish_dir = File.dirname(fish_config)
|
289
|
+
unless File.exist?(fish_dir)
|
290
|
+
FileUtils.mkdir_p(fish_dir)
|
291
|
+
puts " 创建目录: #{fish_dir}"
|
292
|
+
end
|
293
|
+
else
|
294
|
+
shell_files << File.expand_path("~/.profile")
|
295
|
+
end
|
296
|
+
|
297
|
+
# 总是添加 ~/.profile 作为备份
|
298
|
+
profile_file = File.expand_path("~/.profile")
|
299
|
+
shell_files << profile_file unless shell_files.include?(profile_file)
|
300
|
+
|
301
|
+
shell_files
|
302
|
+
end
|
303
|
+
|
304
|
+
def self.get_token_patterns(shell_file)
|
305
|
+
key = "CLAUDE_CODE_OAUTH_TOKEN"
|
306
|
+
|
307
|
+
if shell_file.include?("config.fish")
|
308
|
+
[
|
309
|
+
/^set\s+-gx\s+#{Regexp.escape(key)}\s/,
|
310
|
+
/^set\s+-x\s+#{Regexp.escape(key)}\s/,
|
311
|
+
/^#set\s+-gx\s+#{Regexp.escape(key)}\s/
|
312
|
+
]
|
313
|
+
else
|
314
|
+
[
|
315
|
+
/^export\s+#{Regexp.escape(key)}=/,
|
316
|
+
/^#{Regexp.escape(key)}=/,
|
317
|
+
/^#export\s+#{Regexp.escape(key)}=/
|
318
|
+
]
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
def self.get_export_line(shell_file, token)
|
323
|
+
key = "CLAUDE_CODE_OAUTH_TOKEN"
|
324
|
+
|
325
|
+
if shell_file.include?("config.fish")
|
326
|
+
"set -gx #{key} \"#{token}\""
|
327
|
+
else
|
328
|
+
"export #{key}=\"#{token}\""
|
329
|
+
end
|
330
|
+
end
|
331
|
+
|
332
|
+
def self.remove_proxy_config(lines, shell_file)
|
333
|
+
if shell_file.include?("config.fish")
|
334
|
+
# Fish shell 代理配置清理 - 移除直接的环境变量设置和代理相关的别名/函数
|
335
|
+
lines.reject! { |line|
|
336
|
+
line.match(/^set\s+-[gx]+\s+(HTTP_PROXY|HTTPS_PROXY|http_proxy|https_proxy)\s/) ||
|
337
|
+
line.match(/^export\s+(HTTP_PROXY|HTTPS_PROXY|http_proxy|https_proxy)=/) ||
|
338
|
+
line.match(/^alias\s+claude_proxy=/) ||
|
339
|
+
line.match(/^alias\s+unclaude_proxy=/) ||
|
340
|
+
line.match(/^function\s+(claude_proxy|unclaude_proxy)/)
|
341
|
+
}
|
342
|
+
|
343
|
+
# 移除函数块
|
344
|
+
in_function = false
|
345
|
+
lines.select do |line|
|
346
|
+
if line.match(/^function\s+(claude_proxy|unclaude_proxy)/)
|
347
|
+
in_function = true
|
348
|
+
false
|
349
|
+
elsif in_function && line.strip == "end"
|
350
|
+
in_function = false
|
351
|
+
false
|
352
|
+
elsif in_function
|
353
|
+
false
|
354
|
+
else
|
355
|
+
true
|
356
|
+
end
|
357
|
+
end
|
358
|
+
else
|
359
|
+
# Bash/zsh 代理配置清理 - 移除直接的环境变量设置和代理相关的别名/函数
|
360
|
+
lines.reject! { |line|
|
361
|
+
line.match(/^export\s+(HTTP_PROXY|HTTPS_PROXY|http_proxy|https_proxy)=/) ||
|
362
|
+
line.match(/^(HTTP_PROXY|HTTPS_PROXY|http_proxy|https_proxy)=/) ||
|
363
|
+
line.match(/^alias\s+(claude_proxy|unclaude_proxy)=/) ||
|
364
|
+
line.match(/^(claude_proxy|unclaude_proxy)\(\)/)
|
365
|
+
}
|
366
|
+
|
367
|
+
# 移除函数块
|
368
|
+
in_function = false
|
369
|
+
function_depth = 0
|
370
|
+
lines.select do |line|
|
371
|
+
if line.match(/^(claude_proxy|unclaude_proxy)\(\)/)
|
372
|
+
in_function = true
|
373
|
+
function_depth = 0
|
374
|
+
false
|
375
|
+
elsif in_function
|
376
|
+
function_depth += 1 if line.include?("{")
|
377
|
+
if line.include?("}")
|
378
|
+
function_depth -= 1
|
379
|
+
in_function = false if function_depth <= 0
|
380
|
+
end
|
381
|
+
false
|
382
|
+
else
|
383
|
+
true
|
384
|
+
end
|
385
|
+
end
|
386
|
+
end
|
387
|
+
end
|
388
|
+
|
389
|
+
def self.add_proxy_aliases(lines, shell_file, http_proxy, https_proxy)
|
390
|
+
if shell_file.include?("config.fish")
|
391
|
+
# Fish shell - 只添加函数,不直接设置环境变量
|
392
|
+
proxy_commands = []
|
393
|
+
proxy_commands << ""
|
394
|
+
proxy_commands << "function claude_proxy"
|
395
|
+
if http_proxy
|
396
|
+
proxy_commands << " set -gx HTTP_PROXY \"#{http_proxy}\""
|
397
|
+
proxy_commands << " set -gx http_proxy \"#{http_proxy}\""
|
398
|
+
end
|
399
|
+
if https_proxy
|
400
|
+
proxy_commands << " set -gx HTTPS_PROXY \"#{https_proxy}\""
|
401
|
+
proxy_commands << " set -gx https_proxy \"#{https_proxy}\""
|
402
|
+
end
|
403
|
+
proxy_commands << " echo '代理已启用: HTTP_PROXY=#{http_proxy || 'not set'}, HTTPS_PROXY=#{https_proxy || 'not set'}'"
|
404
|
+
proxy_commands << "end"
|
405
|
+
|
406
|
+
proxy_commands << ""
|
407
|
+
proxy_commands << "function unclaude_proxy"
|
408
|
+
proxy_commands << " set -e HTTP_PROXY"
|
409
|
+
proxy_commands << " set -e http_proxy"
|
410
|
+
proxy_commands << " set -e HTTPS_PROXY"
|
411
|
+
proxy_commands << " set -e https_proxy"
|
412
|
+
proxy_commands << " echo '代理已禁用'"
|
413
|
+
proxy_commands << "end"
|
414
|
+
|
415
|
+
lines.concat(proxy_commands)
|
416
|
+
else
|
417
|
+
# Bash/zsh - 只添加别名,不直接设置环境变量
|
418
|
+
proxy_exports = []
|
419
|
+
if http_proxy
|
420
|
+
proxy_exports << "export HTTP_PROXY=\"#{http_proxy}\""
|
421
|
+
proxy_exports << "export http_proxy=\"#{http_proxy}\""
|
422
|
+
end
|
423
|
+
if https_proxy
|
424
|
+
proxy_exports << "export HTTPS_PROXY=\"#{https_proxy}\""
|
425
|
+
proxy_exports << "export https_proxy=\"#{https_proxy}\""
|
426
|
+
end
|
427
|
+
|
428
|
+
if proxy_exports.any?
|
429
|
+
lines << ""
|
430
|
+
lines << "alias claude_proxy='#{proxy_exports.join("; ")}; echo \"代理已启用: HTTP_PROXY=#{http_proxy || 'not set'}, HTTPS_PROXY=#{https_proxy || 'not set'}\"'"
|
431
|
+
end
|
432
|
+
|
433
|
+
lines << "alias unclaude_proxy='unset HTTP_PROXY http_proxy HTTPS_PROXY https_proxy; echo \"代理已禁用\"'"
|
434
|
+
end
|
435
|
+
|
436
|
+
lines
|
437
|
+
end
|
438
|
+
end
|
439
|
+
end
|
440
|
+
end
|