easyai 1.7.0 → 2.0.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.
@@ -1,519 +0,0 @@
1
- require 'json'
2
- require 'etc'
3
- require 'open3'
4
- require 'fileutils'
5
- require_relative '../base/system_info'
6
-
7
- module EasyAI
8
- module Auth
9
- class AuthClaude
10
- # 常量定义
11
- SERVICE_NAME = "Claude Code-credentials"
12
- TOKEN_KEY = "CLAUDE_CODE_OAUTH_TOKEN"
13
- ALT_TOKEN_KEY = "ANTHROPIC_AUTH_TOKEN"
14
- CLAUDE_JSON_PATH = File.expand_path("~/.claude.json")
15
-
16
- def self.configure(config, user_name = nil)
17
- return false unless config
18
-
19
- # 第一步:清理系统环境,确保干净的运行环境
20
- clean_system_environment
21
-
22
- # 检查认证配置优先级
23
- keychain_exists = macos? && config["key_chain"] && config["key_chain"]["claudeAiOauth"] && config["key_chain"]["service_name"]
24
- # 检查两种可能的令牌键
25
- env_token_exists = config["env"] && (config["env"][TOKEN_KEY] || config["env"][ALT_TOKEN_KEY])
26
-
27
- # 如果使用 Keychain,需要将令牌从 Keychain 读取到配置中
28
- if keychain_exists
29
- token_from_keychain = get_token_from_keychain(config["key_chain"]["service_name"])
30
- if token_from_keychain
31
- config["env"] ||= {}
32
- config["env"][TOKEN_KEY] = token_from_keychain
33
- end
34
- end
35
-
36
- # 根据优先级确定认证方法
37
- if keychain_exists
38
- configure_keychain(config)
39
- clean_env_variables
40
- elsif env_token_exists
41
- verify_token_config(config)
42
- clean_keychain if macos?
43
- else
44
- # 先检查环境变量中是否已有令牌
45
- token = check_env_tokens
46
-
47
- if token.nil? || token.empty?
48
- # 环境变量中没有,才提示用户输入
49
- token = prompt_for_token
50
- return false if token.nil? || token.empty?
51
- else
52
- log_verbose(" ✓ 使用环境变量中的令牌")
53
- end
54
-
55
- # 将令牌添加到配置中进行处理
56
- # 如果配置中已经有 ANTHROPIC_AUTH_TOKEN,使用该键;否则使用 CLAUDE_CODE_OAUTH_TOKEN
57
- config["env"] ||= {}
58
- if config["env"][ALT_TOKEN_KEY]
59
- config["env"][ALT_TOKEN_KEY] = token
60
- else
61
- config["env"][TOKEN_KEY] = token
62
- end
63
-
64
- verify_token_config(config)
65
- clean_keychain
66
- end
67
-
68
- # 更新 ~/.claude.json 文件
69
- update_claude_json(config) if config["claude_json"]
70
-
71
- # 配置代理设置
72
- configure_proxy(config) if config["claude_proxy"]
73
-
74
- true
75
- end
76
-
77
- private
78
-
79
- # 辅助方法
80
- def self.macos?
81
- Base::SystemInfo.macos?
82
- end
83
-
84
- def self.current_user
85
- Base::SystemInfo.current_user
86
- end
87
-
88
- def self.log_verbose(message)
89
- puts message if ENV['EASYAI_DEBUG'] || ENV['EASYAI_VERBOSE']
90
- end
91
-
92
- def self.log_debug(message)
93
- puts message if ENV['EASYAI_DEBUG']
94
- end
95
-
96
- # 通用的 shell 文件处理方法
97
- def self.process_shell_files(operation_name)
98
- shell_files = get_shell_files
99
-
100
- shell_files.each do |shell_file|
101
- next unless File.exist?(shell_file)
102
-
103
- begin
104
- content = File.read(shell_file)
105
- lines = content.split("\n", -1)
106
- lines.pop if lines.last == ""
107
-
108
- original_count = lines.length
109
- lines = yield(lines, shell_file)
110
- removed_count = original_count - lines.length
111
-
112
- if removed_count > 0
113
- File.write(shell_file, lines.join("\n") + "\n")
114
- log_verbose(" ✓ 已从 #{File.basename(shell_file)} 中清理 #{removed_count} 行#{operation_name}")
115
- end
116
- rescue => e
117
- log_debug(" ⚠ 处理 #{shell_file} 失败: #{e.message}")
118
- end
119
- end
120
- end
121
-
122
- def self.clean_system_environment
123
- log_verbose("🧹 清理系统环境...")
124
-
125
- # 1. 清理 ~/.claude.json 中的 oauthAccount
126
- clean_claude_json_oauth
127
-
128
- # 2. 清理 Keychain(仅 macOS)
129
- clean_keychain if macos?
130
-
131
- # 3. 清理环境变量中的 token
132
- clean_env_variables
133
-
134
- # 4. 清理环境变量中的代理设置
135
- clean_proxy_variables
136
-
137
- log_verbose(" ✓ 系统环境清理完成")
138
- end
139
-
140
- def self.clean_claude_json_oauth
141
- return unless File.exist?(CLAUDE_JSON_PATH)
142
-
143
- begin
144
- content = File.read(CLAUDE_JSON_PATH)
145
- config = JSON.parse(content)
146
-
147
- # 删除 oauthAccount 字段
148
- if config.delete("oauthAccount")
149
- # 写回文件
150
- File.write(CLAUDE_JSON_PATH, JSON.pretty_generate(config))
151
- log_verbose(" ✓ 已清理 ~/.claude.json 中的 oauthAccount")
152
- end
153
- rescue JSON::ParserError => e
154
- log_debug(" ⚠ 解析 ~/.claude.json 失败: #{e.message}")
155
- rescue => e
156
- log_debug(" ⚠ 清理 ~/.claude.json 失败: #{e.message}")
157
- end
158
- end
159
-
160
- def self.clean_proxy_variables
161
- process_shell_files("代理配置") do |lines, shell_file|
162
- remove_all_proxy_config(lines, shell_file)
163
- end
164
- end
165
-
166
- def self.get_token_from_keychain(service_name)
167
- return nil unless macos?
168
-
169
- cmd = [
170
- "security",
171
- "find-generic-password",
172
- "-a", current_user,
173
- "-s", service_name,
174
- "-w"
175
- ]
176
-
177
- begin
178
- stdout, stderr, status = Open3.capture3(*cmd)
179
-
180
- if status.success? && !stdout.strip.empty?
181
- # 解析 JSON 格式的密码
182
- credentials = JSON.parse(stdout.strip)
183
- return credentials["claudeAiOauth"]
184
- end
185
- rescue JSON::ParserError, StandardError
186
- # 如果不是 JSON 格式,尝试直接返回
187
- return stdout.strip if status.success? && !stdout.strip.empty?
188
- end
189
-
190
- nil
191
- end
192
-
193
- def self.configure_keychain(config)
194
- credentials = {
195
- "claudeAiOauth" => config["key_chain"]["claudeAiOauth"]
196
- }
197
-
198
- service_name = config["key_chain"]["service_name"]
199
- password = credentials.to_json
200
-
201
- cmd = [
202
- "security",
203
- "add-generic-password",
204
- "-a", current_user,
205
- "-s", service_name,
206
- "-l", service_name,
207
- "-w", password,
208
- "-U",
209
- "-T", ""
210
- ]
211
-
212
- system(*cmd)
213
-
214
- if $?.success?
215
- verify_keychain_entry(current_user, service_name)
216
- else
217
- puts "✗ 保存凭证到 Keychain 失败"
218
- return false
219
- end
220
-
221
- true
222
- end
223
-
224
- def self.verify_keychain_entry(account_name, service_name)
225
- verify_cmd = [
226
- "security",
227
- "find-generic-password",
228
- "-a", account_name,
229
- "-s", service_name
230
- ]
231
-
232
- unless system(*verify_cmd, out: File::NULL, err: File::NULL)
233
- puts "✗ 无法验证 Keychain 条目"
234
- end
235
- end
236
-
237
- def self.verify_token_config(config)
238
- # 检查两种可能的令牌键
239
- token = config["env"][TOKEN_KEY] || config["env"][ALT_TOKEN_KEY]
240
- return false unless token
241
-
242
- # 只验证令牌存在且不为空,不写入任何配置文件
243
- if token && !token.strip.empty?
244
- # 清理任何现有的环境配置文件中的令牌定义
245
- clean_env_variables
246
- return true
247
- else
248
- puts "✗ 令牌配置无效"
249
- return false
250
- end
251
- end
252
-
253
- def self.update_claude_json(config)
254
- begin
255
- # 读取现有文件或创建空哈希
256
- if File.exist?(CLAUDE_JSON_PATH)
257
- user_config = JSON.parse(File.read(CLAUDE_JSON_PATH))
258
- else
259
- user_config = {}
260
- end
261
-
262
- # 合并 claude_json 内容,过滤掉代理相关字段
263
- claude_config = config["claude_json"].dup
264
- claude_config.delete("claude_proxy")
265
- claude_config.delete("HTTP_PROXY")
266
- claude_config.delete("HTTPS_PROXY")
267
-
268
- user_config.merge!(claude_config)
269
-
270
- # 写回文件
271
- File.write(CLAUDE_JSON_PATH, JSON.pretty_generate(user_config))
272
-
273
- rescue JSON::ParserError => e
274
- puts "✗ 解析现有 ~/.claude.json 失败: #{e.message}"
275
- false
276
- rescue => e
277
- puts "✗ 更新 ~/.claude.json 失败: #{e.message}"
278
- false
279
- end
280
- end
281
-
282
- def self.configure_proxy(config)
283
- return unless config["claude_proxy"] && (config["claude_proxy"]["HTTP_PROXY"] || config["claude_proxy"]["HTTPS_PROXY"])
284
-
285
- http_proxy = config["claude_proxy"]["HTTP_PROXY"]
286
- https_proxy = config["claude_proxy"]["HTTPS_PROXY"]
287
-
288
- shell_files = get_shell_files
289
-
290
- shell_files.each do |shell_file|
291
- next unless File.exist?(shell_file)
292
-
293
- begin
294
- content = File.read(shell_file)
295
- lines = content.split("\n", -1)
296
- lines.pop if lines.last == ""
297
-
298
- # 移除所有现有的代理相关配置
299
- lines = remove_all_proxy_config(lines, shell_file)
300
-
301
- # 只添加代理别名/函数,不直接设置环境变量
302
- lines = add_proxy_aliases(lines, shell_file, http_proxy, https_proxy)
303
-
304
- # 写回文件
305
- File.write(shell_file, lines.join("\n") + "\n")
306
-
307
- rescue => e
308
- puts " ✗ 更新 #{shell_file} 失败: #{e.message}"
309
- end
310
- end
311
-
312
- # 显示代理配置完成信息
313
- puts "✅ 代理别名已配置 | claude_proxy / unclaude_proxy"
314
- end
315
-
316
- # 检查环境变量中的令牌
317
- def self.check_env_tokens
318
- # 优先使用 CLAUDE_CODE_OAUTH_TOKEN,其次是 ANTHROPIC_AUTH_TOKEN
319
- token = ENV[TOKEN_KEY] || ENV[ALT_TOKEN_KEY]
320
- token&.strip&.empty? ? nil : token
321
- end
322
-
323
- def self.prompt_for_token
324
- puts "请输入您的 CLAUDE_CODE_OAUTH_TOKEN:"
325
- print "> "
326
-
327
- begin
328
- # 简化输入处理,直接使用 STDIN
329
- STDIN.gets&.chomp
330
- rescue => e
331
- puts "✗ 无法读取输入: #{e.message}"
332
- puts " 请使用本地配置文件或 --user 参数"
333
- nil
334
- end
335
- end
336
-
337
- def self.clean_env_variables
338
- process_shell_files("令牌配置") do |lines, shell_file|
339
- patterns = get_token_patterns(shell_file)
340
- lines.reject { |line| patterns.any? { |pattern| line.match(pattern) } }
341
- end
342
- end
343
-
344
- def self.clean_keychain
345
- return unless macos?
346
-
347
- cmd = [
348
- "security",
349
- "delete-generic-password",
350
- "-a", current_user,
351
- "-s", SERVICE_NAME
352
- ]
353
-
354
- stdout, stderr, status = Open3.capture3(*cmd)
355
-
356
- if status.success?
357
- log_verbose(" ✓ 已清理 Keychain 中的 Claude Code 凭证")
358
- elsif !stderr.include?("could not be found")
359
- log_debug(" ⚠ 清理 Keychain 失败: #{stderr}")
360
- end
361
- end
362
-
363
- def self.get_shell_files
364
- Base::SystemInfo.shell_config_files
365
- end
366
-
367
- def self.get_token_patterns(shell_file)
368
- if shell_file.include?("config.fish")
369
- [
370
- /^set\s+-gx\s+#{Regexp.escape(TOKEN_KEY)}\s/,
371
- /^set\s+-x\s+#{Regexp.escape(TOKEN_KEY)}\s/,
372
- /^#set\s+-gx\s+#{Regexp.escape(TOKEN_KEY)}\s/
373
- ]
374
- else
375
- [
376
- /^export\s+#{Regexp.escape(TOKEN_KEY)}=/,
377
- /^#{Regexp.escape(TOKEN_KEY)}=/,
378
- /^#export\s+#{Regexp.escape(TOKEN_KEY)}=/
379
- ]
380
- end
381
- end
382
-
383
- def self.get_export_line(shell_file, token)
384
- if shell_file.include?("config.fish")
385
- "set -gx #{TOKEN_KEY} \"#{token}\""
386
- else
387
- "export #{TOKEN_KEY}=\"#{token}\""
388
- end
389
- end
390
-
391
- def self.remove_all_proxy_config(lines, shell_file)
392
- # 更全面的代理配置清理,包括所有可能的代理设置
393
- if shell_file.include?("config.fish")
394
- # Fish shell 代理配置清理
395
- lines.reject! { |line|
396
- # 清理环境变量设置
397
- line.match(/^set\s+-[gx]+\s+(HTTP_PROXY|HTTPS_PROXY|http_proxy|https_proxy|ALL_PROXY|all_proxy|NO_PROXY|no_proxy)\s/) ||
398
- line.match(/^export\s+(HTTP_PROXY|HTTPS_PROXY|http_proxy|https_proxy|ALL_PROXY|all_proxy|NO_PROXY|no_proxy)=/) ||
399
- # 清理别名
400
- line.match(/^alias\s+(claude_proxy|unclaude_proxy|proxy|unproxy)=/) ||
401
- # 清理函数
402
- line.match(/^function\s+(claude_proxy|unclaude_proxy|proxy|unproxy)/)
403
- }
404
-
405
- # 移除函数块
406
- in_function = false
407
- lines = lines.select do |line|
408
- if line.match(/^function\s+(claude_proxy|unclaude_proxy|proxy|unproxy)/)
409
- in_function = true
410
- false
411
- elsif in_function && line.strip == "end"
412
- in_function = false
413
- false
414
- elsif in_function
415
- false
416
- else
417
- true
418
- end
419
- end
420
- else
421
- # Bash/zsh 代理配置清理
422
- lines.reject! { |line|
423
- # 清理环境变量设置
424
- line.match(/^export\s+(HTTP_PROXY|HTTPS_PROXY|http_proxy|https_proxy|ALL_PROXY|all_proxy|NO_PROXY|no_proxy)=/) ||
425
- line.match(/^(HTTP_PROXY|HTTPS_PROXY|http_proxy|https_proxy|ALL_PROXY|all_proxy|NO_PROXY|no_proxy)=/) ||
426
- # 清理别名
427
- line.match(/^alias\s+(claude_proxy|unclaude_proxy|proxy|unproxy)=/) ||
428
- # 清理函数
429
- line.match(/^(claude_proxy|unclaude_proxy|proxy|unproxy)\(\)/) ||
430
- # 清理函数定义的另一种格式
431
- line.match(/^function\s+(claude_proxy|unclaude_proxy|proxy|unproxy)/)
432
- }
433
-
434
- # 移除函数块
435
- in_function = false
436
- function_depth = 0
437
- lines = lines.select do |line|
438
- if line.match(/^(claude_proxy|unclaude_proxy|proxy|unproxy)\(\)/) ||
439
- line.match(/^function\s+(claude_proxy|unclaude_proxy|proxy|unproxy)/)
440
- in_function = true
441
- function_depth = 0
442
- false
443
- elsif in_function
444
- function_depth += 1 if line.include?("{")
445
- if line.include?("}")
446
- function_depth -= 1
447
- if function_depth <= 0
448
- in_function = false
449
- false
450
- else
451
- false
452
- end
453
- else
454
- false
455
- end
456
- else
457
- true
458
- end
459
- end
460
- end
461
-
462
- lines
463
- end
464
-
465
- # remove_proxy_config 方法已被 remove_all_proxy_config 替代
466
- # remove_all_proxy_config 提供更全面的清理功能,包括 ALL_PROXY 和 NO_PROXY
467
-
468
- def self.add_proxy_aliases(lines, shell_file, http_proxy, https_proxy)
469
- if shell_file.include?("config.fish")
470
- # Fish shell - 只添加函数,不直接设置环境变量
471
- proxy_commands = []
472
- proxy_commands << ""
473
- proxy_commands << "function claude_proxy"
474
- if http_proxy
475
- proxy_commands << " set -gx HTTP_PROXY \"#{http_proxy}\""
476
- proxy_commands << " set -gx http_proxy \"#{http_proxy}\""
477
- end
478
- if https_proxy
479
- proxy_commands << " set -gx HTTPS_PROXY \"#{https_proxy}\""
480
- proxy_commands << " set -gx https_proxy \"#{https_proxy}\""
481
- end
482
- proxy_commands << " echo '代理已启用: HTTP_PROXY=#{http_proxy || 'not set'}, HTTPS_PROXY=#{https_proxy || 'not set'}'"
483
- proxy_commands << "end"
484
-
485
- proxy_commands << ""
486
- proxy_commands << "function unclaude_proxy"
487
- proxy_commands << " set -e HTTP_PROXY"
488
- proxy_commands << " set -e http_proxy"
489
- proxy_commands << " set -e HTTPS_PROXY"
490
- proxy_commands << " set -e https_proxy"
491
- proxy_commands << " echo '代理已禁用'"
492
- proxy_commands << "end"
493
-
494
- lines.concat(proxy_commands)
495
- else
496
- # Bash/zsh - 只添加别名,不直接设置环境变量
497
- proxy_exports = []
498
- if http_proxy
499
- proxy_exports << "export HTTP_PROXY=\"#{http_proxy}\""
500
- proxy_exports << "export http_proxy=\"#{http_proxy}\""
501
- end
502
- if https_proxy
503
- proxy_exports << "export HTTPS_PROXY=\"#{https_proxy}\""
504
- proxy_exports << "export https_proxy=\"#{https_proxy}\""
505
- end
506
-
507
- if proxy_exports.any?
508
- lines << ""
509
- lines << "alias claude_proxy='#{proxy_exports.join("; ")}; echo \"代理已启用: HTTP_PROXY=#{http_proxy || 'not set'}, HTTPS_PROXY=#{https_proxy || 'not set'}\"'"
510
- end
511
-
512
- lines << "alias unclaude_proxy='unset HTTP_PROXY http_proxy HTTPS_PROXY https_proxy; echo \"代理已禁用\"'"
513
- end
514
-
515
- lines
516
- end
517
- end
518
- end
519
- end
@@ -1,98 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- require 'jpsclient'
4
- require 'fileutils'
5
- require 'json'
6
- require_relative '../config/easyai_config'
7
-
8
- module EasyAI
9
- module Auth
10
- class JPSLoginHelper
11
- attr_reader :username
12
-
13
- def initialize(options = {})
14
- @verbose = options[:verbose] || false
15
-
16
- # 如果传入了配置文件路径,直接使用
17
- if options[:config_file]
18
- @config_file_path = options[:config_file]
19
- else
20
- # 通过 EasyAIConfig 获取 jpsclient 配置文件路径
21
- # 首先确保配置仓库已初始化
22
- EasyAIConfig.initialize(verbose: @verbose)
23
-
24
- # 获取 jpsclient 配置文件路径(会自动解密到临时目录)
25
- @config_file_path = EasyAIConfig.get_config_path('jps_client_config', verbose: @verbose)
26
- end
27
-
28
- @client = nil
29
- @username = nil
30
- end
31
-
32
- # 主登录入口 - 兼容原 JPSLogin 接口
33
- def login
34
- begin
35
- # 检查配置文件路径
36
- if @config_file_path.nil?
37
- puts "✗ 未找到 jpsclient 配置文件"
38
- puts " 请确保配置仓库已下载并解密"
39
- return false
40
- end
41
-
42
- # 检查配置文件是否存在
43
- unless File.exist?(@config_file_path)
44
- puts "✗ 配置文件不存在: #{@config_file_path}"
45
- puts " 请先从配置仓库下载并解密配置文件"
46
- return false
47
- end
48
-
49
- @client ||= JPSClient::Client.new(config_file: @config_file_path)
50
-
51
- # 执行登录
52
- result = @client.do_login(force_login: false)
53
-
54
- if result == :user_cancelled
55
- return :user_cancelled
56
- end
57
-
58
- if result
59
- # 登录成功,获取用户名
60
- @username = get_username_from_client
61
- return true
62
- end
63
-
64
- return false
65
- rescue => e
66
- puts "✗ JPS 登录失败: #{e.message}" if @verbose
67
- return false
68
- end
69
- end
70
-
71
- # 获取用户名 - 兼容原 JPSLogin 接口
72
- def get_username
73
- return @username if @username
74
-
75
- # 如果客户端存在且已登录,尝试获取用户名
76
- if @client
77
- @username = get_username_from_client
78
- end
79
-
80
- @username
81
- end
82
-
83
- private
84
-
85
- # 从客户端获取用户名
86
- def get_username_from_client
87
- if @client && @client.token
88
- # 从 token 中获取用户名
89
- token_info = @client.token
90
- if token_info.is_a?(Hash) && token_info['username']
91
- return token_info['username']
92
- end
93
- end
94
- nil
95
- end
96
- end
97
- end
98
- end