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,263 +0,0 @@
1
- require 'json'
2
- require 'etc'
3
- require 'open3'
4
- require 'fileutils'
5
- require_relative '../../config/config'
6
- require_relative '../../auth/authclaude'
7
- require_relative '../../base/system_info'
8
-
9
- module EasyAI
10
- class Command
11
- class Utils < Command
12
- class Export < Utils
13
- self.summary = '导出 AI 配置信息'
14
- self.description = <<-DESC
15
- 导出当前系统的 AI 配置信息到 JSON 文件。
16
-
17
- 主要功能:
18
-
19
- * 从 ~/.claude.json 读取配置
20
-
21
- * 从 Keychain 读取凭证(macOS)
22
-
23
- * 从环境变量读取令牌
24
-
25
- * 自动生成文件名(使用邮箱地址)
26
-
27
- 使用示例:
28
-
29
- $ easyai utils export # 自动生成文件名
30
-
31
- $ easyai utils export --output=config.json # 指定输出文件名
32
-
33
- $ easyai utils export --verbose # 显示详细信息
34
- DESC
35
-
36
- def self.options
37
- [
38
- ['--output=FILE', '指定输出文件名'],
39
- ['--verbose', '显示详细信息']
40
- ].concat(super)
41
- end
42
-
43
- def initialize(argv)
44
- @output_file = argv.option('output')
45
- @verbose = argv.flag?('verbose')
46
- super
47
- end
48
-
49
- def run
50
- puts "正在导出 AI 配置信息..."
51
- puts "" if @verbose
52
-
53
- # 收集配置信息
54
- export_config = {}
55
-
56
- # 1. 读取 ~/.claude.json
57
- user_claude_file = File.expand_path("~/.claude.json")
58
-
59
- if File.exist?(user_claude_file)
60
- print_status("📄 读取配置", user_claude_file) if @verbose
61
-
62
- begin
63
- user_config = JSON.parse(File.read(user_claude_file))
64
-
65
- # 排除 projects 字段
66
- claude_config = user_config.reject { |key, _| key == "projects" }
67
-
68
- # 添加到导出配置
69
- export_config["claude_json"] = claude_config
70
-
71
- print_success("读取 ~/.claude.json 成功") if @verbose
72
-
73
- # 如果未指定输出文件名,尝试从配置中获取邮箱地址
74
- if @output_file.nil? && claude_config["oauthAccount"]
75
- email = claude_config["oauthAccount"]["emailAddress"]
76
- if email && !email.empty?
77
- # 使用邮箱地址作为文件名(替换特殊字符)
78
- safe_filename = email.gsub(/[@.]/, '_')
79
- @output_file = "#{safe_filename}.json"
80
- puts " 使用邮箱生成文件名: #{@output_file}" if @verbose
81
- end
82
- end
83
-
84
- rescue JSON::ParserError => e
85
- print_error("解析 ~/.claude.json 失败: #{e.message}")
86
- exit 1
87
- rescue => e
88
- print_error("读取 ~/.claude.json 失败: #{e.message}")
89
- exit 1
90
- end
91
- else
92
- print_warning("未找到 ~/.claude.json 文件")
93
- end
94
-
95
- # 2. 从 Keychain 读取凭证(macOS)
96
- if Base::SystemInfo.macos?
97
- print_status("🔐 读取 Keychain", "Claude Code-credentials") if @verbose
98
-
99
- keychain_data = read_keychain_credentials
100
- if keychain_data
101
- export_config["key_chain"] = {
102
- "service_name" => "Claude Code-credentials",
103
- "claudeAiOauth" => keychain_data
104
- }
105
- print_success("读取 Keychain 凭证成功") if @verbose
106
- else
107
- print_warning("未找到 Keychain 凭证(可选)") if @verbose
108
- end
109
- else
110
- puts " ⚠️ 非 macOS 系统,跳过 Keychain 读取" if @verbose
111
- end
112
-
113
- # 3. 从环境变量读取令牌(始终包含 env 字段)
114
- token_value = ENV['CLAUDE_CODE_OAUTH_TOKEN'] || ""
115
-
116
- export_config["env"] = {
117
- "CLAUDE_CODE_OAUTH_TOKEN" => token_value
118
- }
119
-
120
- if token_value && !token_value.empty?
121
- print_status("🔑 读取环境变量", "CLAUDE_CODE_OAUTH_TOKEN") if @verbose
122
- print_success("读取环境变量成功") if @verbose
123
- else
124
- print_warning("CLAUDE_CODE_OAUTH_TOKEN 为空(将导出空值)") if @verbose
125
- end
126
-
127
- # 4. 读取代理配置(始终包含 claude_proxy 字段)
128
- proxy_config = {}
129
-
130
- # 检查 HTTP_PROXY 相关环境变量
131
- http_proxy = ENV['HTTP_PROXY'] || ENV['http_proxy'] || ""
132
- proxy_config['HTTP_PROXY'] = http_proxy
133
-
134
- # 检查 HTTPS_PROXY 相关环境变量
135
- https_proxy = ENV['HTTPS_PROXY'] || ENV['https_proxy'] || ""
136
- proxy_config['HTTPS_PROXY'] = https_proxy
137
-
138
- # 始终添加 claude_proxy 字段
139
- export_config["claude_proxy"] = proxy_config
140
-
141
- # 显示状态信息
142
- if @verbose
143
- has_http = !http_proxy.empty?
144
- has_https = !https_proxy.empty?
145
-
146
- if has_http || has_https
147
- print_status("🌐 读取代理配置", "环境变量")
148
- print_success("读取代理配置成功")
149
- puts " HTTP_PROXY: #{has_http ? mask_url(http_proxy) : '(空)'}"
150
- puts " HTTPS_PROXY: #{has_https ? mask_url(https_proxy) : '(空)'}"
151
- else
152
- print_warning("代理配置为空(将导出空值)")
153
- end
154
- end
155
-
156
- # 检查是否收集到任何配置
157
- if export_config.empty?
158
- print_error("未找到任何配置信息可导出")
159
- puts " 请确保已配置 Claude 或运行 'easyai --setup'"
160
- exit 1
161
- end
162
-
163
- # 设置默认输出文件名
164
- @output_file ||= "claude_setting.json"
165
-
166
- # 写入文件
167
- puts "" if @verbose
168
- print_status("💾 写入文件", @output_file)
169
-
170
- begin
171
- # 美化 JSON 输出
172
- json_output = JSON.pretty_generate(export_config)
173
-
174
- # 写入文件
175
- File.write(@output_file, json_output)
176
-
177
- print_success("配置成功导出到 #{@output_file}")
178
-
179
- # 显示导出内容摘要
180
- if @verbose
181
- puts "\n导出内容摘要:"
182
- puts " ✓ claude.json 配置" if export_config["claude_json"]
183
- puts " ✓ Keychain 凭证" if export_config["key_chain"]
184
- puts " ✓ 环境变量令牌" if export_config["env"]
185
- puts " ✓ 代理配置" if export_config["claude_proxy"]
186
- end
187
-
188
- rescue => e
189
- print_error("写入文件失败: #{e.message}")
190
- exit 1
191
- end
192
- end
193
-
194
- private
195
-
196
- def claude_available?
197
- system('which claude > /dev/null 2>&1')
198
- end
199
-
200
- def read_keychain_credentials
201
- return nil unless Base::SystemInfo.macos?
202
-
203
- service_name = "Claude Code-credentials"
204
- account_name = Base::SystemInfo.current_user
205
-
206
- # 获取 Keychain 中的密码
207
- cmd = [
208
- "security",
209
- "find-generic-password",
210
- "-a", account_name,
211
- "-s", service_name,
212
- "-w" # 只输出密码
213
- ]
214
-
215
- stdout, stderr, status = Open3.capture3(*cmd)
216
-
217
- if status.success? && !stdout.strip.empty?
218
- # 尝试解析 JSON 格式的凭证
219
- begin
220
- credentials = JSON.parse(stdout.strip)
221
- return credentials["claudeAiOauth"]
222
- rescue JSON::ParserError
223
- # 如果不是 JSON,直接返回字符串
224
- return stdout.strip
225
- end
226
- end
227
-
228
- nil
229
- end
230
-
231
- def mask_url(url)
232
- # 隐藏代理 URL 中的敏感信息
233
- return url unless @verbose
234
-
235
- if url =~ /^(https?:\/\/)([^:]+):([^@]+)@(.+)$/
236
- protocol, user, pass, rest = $1, $2, $3, $4
237
- masked_pass = '*' * [pass.length, 8].min
238
- "#{protocol}#{user}:#{masked_pass}@#{rest}"
239
- else
240
- url
241
- end
242
- end
243
-
244
- def print_status(icon_text, detail)
245
- icon_width = 5 # 图标部分的固定宽度(包括空格)
246
- puts sprintf("%-#{icon_width}s %s", icon_text, detail.cyan)
247
- end
248
-
249
- def print_success(message)
250
- puts " ✓ #{message}".green
251
- end
252
-
253
- def print_warning(message)
254
- puts " ⚠️ #{message}".yellow
255
- end
256
-
257
- def print_error(message)
258
- puts "✗ #{message}".red
259
- end
260
- end
261
- end
262
- end
263
- end
@@ -1,357 +0,0 @@
1
- require 'json'
2
- require 'fileutils'
3
- require_relative '../auth/jpsloginhelper'
4
- require_relative 'easyai_config'
5
-
6
- module EasyAI
7
- # ConfigManager 类负责业务配置逻辑和用户认证
8
- class ConfigManager
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
16
- end
17
-
18
- # 类方法兼容接口
19
- def self.download_user_config(user_name = nil, options = {})
20
- manager = new(options)
21
- manager.download_user_config(user_name)
22
- end
23
-
24
- # 主要接口:下载用户配置
25
- def download_user_config(user_name = nil)
26
- puts "正在加载配置..." if @verbose
27
- log_config_info if @verbose
28
-
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)
36
-
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
45
- end
46
-
47
- # 查找并加载配置
48
- config_filename = find_user_config(users, selected_user.strip)
49
- return nil unless config_filename
50
-
51
- puts "✓ 正在为 #{selected_user} 加载配置" if @verbose
52
- get_user_config_content(config_filename)
53
- end
54
-
55
- private
56
-
57
- def log_config_info
58
- puts " 工具类型: #{@tool_type || '自动'}" if @tool_type
59
- puts " 认证平台: #{@platform || (@platform_flag ? '交互式选择' : '自动选择')}" if @tool_type == "claude"
60
- end
61
-
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 登录失败,无法获取用户名"
79
- nil
80
- else
81
- result
82
- end
83
- end
84
-
85
- def get_user_config_content(config_filename)
86
- # 获取配置文件路径
87
- config_path = EasyAIConfig.get_config_path(config_filename, verbose: @verbose)
88
- return nil unless config_path
89
-
90
- # 读取配置内容
91
- begin
92
- config_content = File.read(config_path)
93
- config = JSON.parse(config_content)
94
-
95
- # 添加配置文件路径信息(用于判断认证类型)
96
- config['_config_path'] = config_filename if config
97
-
98
- # 删除解密后的文件
99
- if File.exist?(config_path) && config_path.end_with?('.json')
100
- encrypted_path = "#{config_path}.encrypted"
101
- # 只有存在对应的加密文件时才删除解密文件
102
- if File.exist?(encrypted_path)
103
- FileUtils.rm_f(config_path)
104
- puts " 已清理临时解密文件: #{File.basename(config_path)}" if @verbose
105
- end
106
- end
107
-
108
- config
109
- rescue JSON::ParserError => e
110
- puts "✗ 解析配置文件失败: #{e.message}" if @verbose
111
- nil
112
- rescue => e
113
- puts "✗ 读取配置文件失败: #{e.message}" if @verbose
114
- nil
115
- end
116
- end
117
-
118
- # JPS 登录获取用户名
119
- def get_username_from_jps
120
- jps_login = Auth::JPSLoginHelper.new(verbose: @verbose)
121
- login_result = jps_login.login
122
-
123
- return :user_cancelled if login_result == :user_cancelled
124
- return nil unless login_result
125
-
126
- username = jps_login.get_username
127
- if username && !username.empty?
128
- puts "👤 用户: #{username}" if @verbose
129
- username
130
- else
131
- nil
132
- end
133
- rescue => e
134
- puts "❌ JPS 登录失败: #{e.message}" if @verbose
135
- nil
136
- end
137
-
138
- # 检查用户列表是否为空
139
- def users_empty?(users)
140
- return true if users.nil? || users.empty?
141
-
142
- # 处理新的分组格式
143
- if users.is_a?(Hash) && (users.key?("claude") || users.key?("gemini"))
144
- %w[claude gemini gpt].each do |group|
145
- next unless users[group]
146
-
147
- if group == "claude" && users[group].is_a?(Hash)
148
- # Claude 需要检查认证类型
149
- users[group].each do |auth_type, auth_users|
150
- next unless auth_users.is_a?(Hash)
151
- auth_users.each do |name, filename|
152
- return false if !filename.nil? && !filename.empty?
153
- end
154
- end
155
- elsif !users[group].empty?
156
- return false
157
- end
158
- end
159
- true
160
- else
161
- users.empty?
162
- end
163
- end
164
-
165
- # 查找用户配置文件
166
- def find_user_config(users, user_input_clean)
167
- # 处理新的分组格式
168
- if users.is_a?(Hash) && (users.key?("claude") || users.key?("gemini"))
169
- if @tool_type == "claude" && users["claude"].is_a?(Hash)
170
- find_claude_user_config(users["claude"], user_input_clean)
171
- elsif @tool_type && users[@tool_type]
172
- find_tool_user_config(users[@tool_type], user_input_clean)
173
- else
174
- # 未指定工具类型,按优先级查找
175
- find_any_user_config(users, user_input_clean)
176
- end
177
- else
178
- # 兼容旧格式
179
- find_legacy_user_config(users, user_input_clean)
180
- end
181
- end
182
-
183
- def find_tool_user_config(tool_users, user_input_clean)
184
- return nil unless tool_users.is_a?(Hash)
185
-
186
- tool_users.each do |name, filename|
187
- if name.downcase == user_input_clean.downcase
188
- return normalize_config_filename(filename)
189
- end
190
- end
191
- nil
192
- end
193
-
194
- def find_any_user_config(users, user_input_clean)
195
- %w[claude gemini gpt].each do |group|
196
- next unless users[group]
197
-
198
- if group == "claude" && users[group].is_a?(Hash)
199
- config = find_claude_user_config(users[group], user_input_clean)
200
- return config if config
201
- elsif users[group].is_a?(Hash)
202
- config = find_tool_user_config(users[group], user_input_clean)
203
- return config if config
204
- end
205
- end
206
- nil
207
- end
208
-
209
- def find_legacy_user_config(users, user_input_clean)
210
- users.each do |name, filename|
211
- if name.downcase == user_input_clean.downcase
212
- return normalize_config_filename(filename)
213
- end
214
- end
215
- nil
216
- end
217
-
218
- def normalize_config_filename(filename)
219
- filename = "#{filename}.json" unless filename.end_with?('.json')
220
- filename
221
- end
222
-
223
- # 专门处理 Claude 的用户配置查找
224
- def find_claude_user_config(claude_config, user_input_clean)
225
- auth_priority = ["claude_auth", "aliqwen_auth", "kimi_auth", "deepseek_auth"]
226
-
227
- if @platform
228
- # 指定了平台
229
- return find_claude_config_by_platform(claude_config, @platform, user_input_clean, auth_priority)
230
- else
231
- # 未指定平台,按优先级查找
232
- return find_claude_config_auto(claude_config, user_input_clean, auth_priority)
233
- end
234
- end
235
-
236
- def find_claude_config_by_platform(claude_config, platform, user_input_clean, auth_priority)
237
- unless auth_priority.include?(platform)
238
- puts "✗ 认证平台 '#{platform}' 不存在" if @verbose
239
- puts " 支持的平台: #{auth_priority.join(', ')}" if @verbose
240
- return nil
241
- end
242
-
243
- config = find_in_auth_type(claude_config, platform, user_input_clean)
244
- if config.nil? && @verbose
245
- puts "✗ 用户 '#{user_input_clean}' 在 #{platform} 中未找到配置"
246
- puts " 请联系管理员添加配置或尝试其他认证平台"
247
- elsif config && @verbose
248
- puts "✓ 使用认证平台: #{platform}"
249
- end
250
- config
251
- end
252
-
253
- def find_claude_config_auto(claude_config, user_input_clean, auth_priority)
254
- auth_priority.each do |auth_type|
255
- config = find_in_auth_type(claude_config, auth_type, user_input_clean)
256
- if config
257
- puts "✓ 自动选择认证平台: #{auth_type}" if @verbose
258
- return config
259
- end
260
- end
261
-
262
- if @verbose
263
- puts "✗ 用户 '#{user_input_clean}' 未找到任何配置"
264
- puts " 已尝试: #{auth_priority.join(', ')}"
265
- end
266
- nil
267
- end
268
-
269
- # 在特定认证类型中查找用户配置
270
- def find_in_auth_type(claude_config, auth_type, user_input_clean)
271
- return nil unless claude_config[auth_type].is_a?(Hash)
272
-
273
- claude_config[auth_type].each do |name, filename|
274
- if name.downcase == user_input_clean.downcase && !filename.nil? && !filename.empty?
275
- filename = normalize_config_filename(filename)
276
- return "claude/#{auth_type}/#{filename}"
277
- end
278
- end
279
- nil
280
- end
281
-
282
- # 让用户选择可用的认证平台
283
- def select_platform_for_user(users, user_input_clean)
284
- return nil unless users["claude"].is_a?(Hash)
285
-
286
- claude_config = users["claude"]
287
- available_platforms = []
288
-
289
- # 检查用户在哪些平台有配置
290
- ["claude_auth", "aliqwen_auth", "kimi_auth", "deepseek_auth"].each do |auth_type|
291
- if find_in_auth_type(claude_config, auth_type, user_input_clean)
292
- available_platforms << auth_type
293
- end
294
- end
295
-
296
- # 如果没有可用平台
297
- if available_platforms.empty?
298
- puts "✗ 用户 '#{user_input_clean}' 在任何认证平台中都没有配置"
299
- return nil
300
- end
301
-
302
- # 如果只有一个平台,直接使用
303
- if available_platforms.length == 1
304
- selected = available_platforms.first
305
- puts "✓ 自动选择唯一可用平台: #{selected}" if @verbose
306
- return selected
307
- end
308
-
309
- # 多个平台,让用户选择
310
- puts "\n🔐 请选择认证平台:"
311
- puts "─" * 40
312
-
313
- available_platforms.each_with_index do |platform, index|
314
- platform_name = case platform
315
- when "claude_auth" then "Claude 官方认证"
316
- when "aliqwen_auth" then "阿里通义千问认证"
317
- when "kimi_auth" then "Kimi 认证"
318
- when "deepseek_auth" then "Deepseek 认证"
319
- else platform
320
- end
321
- puts " #{index + 1}. #{platform_name} (#{platform})"
322
- end
323
-
324
- puts "─" * 40
325
-
326
- loop do
327
- print "请输入选择 (1-#{available_platforms.length},输入 q 退出): "
328
-
329
- begin
330
- choice = STDIN.gets&.chomp
331
-
332
- # 用户选择退出
333
- if choice.nil? || choice.downcase == 'q'
334
- puts "✗ 用户取消选择"
335
- return nil
336
- end
337
-
338
- # 验证输入
339
- choice_num = choice.to_i
340
- if choice_num >= 1 && choice_num <= available_platforms.length
341
- selected = available_platforms[choice_num - 1]
342
- puts "✓ 已选择: #{selected}" if @verbose
343
- return selected
344
- else
345
- puts "✗ 无效的选择,请输入 1-#{available_platforms.length} 之间的数字"
346
- end
347
- rescue Interrupt
348
- puts "\n\n✗ 用户中断操作"
349
- return nil
350
- rescue => e
351
- puts "✗ 输入错误: #{e.message}"
352
- return nil
353
- end
354
- end
355
- end
356
- end
357
- end