easyai 1.1.3 → 1.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 92482873797bd9a6936d1b493799244501b94440c155fe49e15b5aa889291ada
4
- data.tar.gz: e3e619b6bd230d469dbb21fc2372e714813a504162e392680907f8fb3eec39aa
3
+ metadata.gz: a805223b3184cf7726f32044944ac33f5dc54e2058e2908d6423e8b846ae0483
4
+ data.tar.gz: 832ea4df5a956690ec3bedc93d995130e6a9ba23d01e684ec8d247e46a1382e1
5
5
  SHA512:
6
- metadata.gz: 1bb5fef07b98087e297e3db2a37f29abe34966771a58fca4c6297f7c29cb2e0136ef97cd7467d2bf8eff730bb2e693334776eeb241d7148ea94b12fdaa7f148e
7
- data.tar.gz: 5a46f13a20d253320c6a039802cbd4caf93780bd5e1570680b51442722c8a42f9eb1b8fa607d4f8eb11ccb2fcf936764d2ad8ba70653a97af9eec2d010bd4b90
6
+ metadata.gz: c88789162eab4d2502697a5533e6d077ca595dc50fd25a9d7b5743c4457cd55dcce0d3ade4c383986937c4a17b2e37db14bc50c8d124fd5ecd429a8ab44aa6d7
7
+ data.tar.gz: f718001928ab81c0fb4e9c245deb6f93ef7518e7407fcf9621301e8d3466beb3fb6dbbef6adc3c1d20f4d6c109023521b357550d3635af0831a2e3d47a2ac2f9
@@ -1,3 +1,5 @@
1
+ require 'json'
2
+ require 'uri'
1
3
  require_relative '../config/config'
2
4
  require_relative '../auth/authclaude'
3
5
  require_relative '../base/update_notifier'
@@ -31,7 +33,7 @@ module EasyAI
31
33
  def self.options
32
34
  [
33
35
  ['--no-keychain', '禁用自动密码存储'],
34
- ['--verbose', '显示详细信息(包括完整的代理和令牌)']
36
+ ['--verbose', '显示详细信息']
35
37
  ].concat(super)
36
38
  end
37
39
 
@@ -70,9 +72,13 @@ module EasyAI
70
72
  remote_config = load_local_config(@config_file)
71
73
  print_success("配置加载成功") if remote_config
72
74
  else
73
- # 从远程下载配置,传递选项
75
+ # 从远程下载配置,传递选项(指定工具类型为 claude)
74
76
  print_status("🔄 获取远程配置", "默认用户")
75
- options = { no_keychain: @no_keychain, verbose: @verbose_mode }
77
+ options = {
78
+ no_keychain: @no_keychain,
79
+ verbose: @verbose_mode,
80
+ tool_type: "claude" # 指定使用 claude 组的配置
81
+ }
76
82
  remote_config = ConfigManager.download_user_config(nil, options)
77
83
 
78
84
  # 处理用户取消授权的情况
@@ -190,17 +196,16 @@ module EasyAI
190
196
  # 从配置中提取环境变量 - 只设置 CLAUDE_CODE_OAUTH_TOKEN
191
197
  if config && config['env'] && config['env']['CLAUDE_CODE_OAUTH_TOKEN']
192
198
  env['CLAUDE_CODE_OAUTH_TOKEN'] = config['env']['CLAUDE_CODE_OAUTH_TOKEN']
193
-
199
+
200
+ # 统一显示方式:不显示完整的令牌
201
+ token_preview = config['env']['CLAUDE_CODE_OAUTH_TOKEN'].length > 15 ?
202
+ "#{config['env']['CLAUDE_CODE_OAUTH_TOKEN'][0..15]}..." :
203
+ config['env']['CLAUDE_CODE_OAUTH_TOKEN']
204
+ print_status("🔑 令牌已配置", token_preview)
205
+
194
206
  if @verbose_mode
195
- # verbose 模式:显示完整的令牌和代理
196
- puts "\n🔑 CLAUDE_CODE_OAUTH_TOKEN:"
197
- puts " #{config['env']['CLAUDE_CODE_OAUTH_TOKEN'].green}"
198
- else
199
- # 普通模式:只显示令牌预览
200
- token_preview = config['env']['CLAUDE_CODE_OAUTH_TOKEN'].length > 15 ?
201
- "#{config['env']['CLAUDE_CODE_OAUTH_TOKEN'][0..15]}..." :
202
- config['env']['CLAUDE_CODE_OAUTH_TOKEN']
203
- print_status("🔑 令牌已配置", token_preview)
207
+ # verbose 模式:显示更多状态信息(但不显示敏感内容)
208
+ puts " 令牌长度: #{config['env']['CLAUDE_CODE_OAUTH_TOKEN'].length} 字符"
204
209
  end
205
210
  end
206
211
 
@@ -222,23 +227,26 @@ module EasyAI
222
227
  end
223
228
 
224
229
  if proxy_urls.any?
230
+ # 统一显示方式:简化代理显示,隐藏密码
231
+ simplified_proxies = proxy_urls.uniq.map do |url|
232
+ uri = URI(url) rescue nil
233
+ if uri && uri.password
234
+ "#{uri.scheme}://***@#{uri.host}:#{uri.port}"
235
+ else
236
+ url
237
+ end
238
+ end
239
+ print_status("🌐 代理已配置", simplified_proxies.join(', '))
240
+
225
241
  if @verbose_mode
226
- # verbose 模式:显示完整的代理地址
227
- puts "\n🔗 代理配置:"
242
+ # verbose 模式:显示更多状态信息(但不显示敏感内容)
243
+ puts " 代理类型: #{proxy_urls.uniq.count == 1 ? '统一代理' : '分离代理'}"
228
244
  proxy_urls.uniq.each do |url|
229
- puts " #{url.green}"
230
- end
231
- else
232
- # 普通模式:简化代理显示,隐藏密码
233
- simplified_proxies = proxy_urls.uniq.map do |url|
234
245
  uri = URI(url) rescue nil
235
- if uri && uri.password
236
- "#{uri.scheme}://***@#{uri.host}:#{uri.port}"
237
- else
238
- url
246
+ if uri
247
+ puts " 代理服务器: #{uri.host}:#{uri.port}"
239
248
  end
240
249
  end
241
- print_status("🌐 代理已配置", simplified_proxies.join(', '))
242
250
  end
243
251
  end
244
252
  end
@@ -328,43 +328,40 @@ module EasyAI
328
328
 
329
329
  def get_claude_keychain_entries
330
330
  return [] unless RUBY_PLATFORM.include?('darwin')
331
-
331
+
332
332
  entries = []
333
-
334
- # 检查各种可能的 Claude 相关 keychain 条目
333
+ account_name = Etc.getlogin || ENV['USER'] || ENV['LOGNAME']
334
+
335
+ # 定义所有可能的 Claude 相关服务名
336
+ # 基于 authclaude.rb 和常见的 Claude 应用服务名
335
337
  possible_services = [
336
338
  "Claude Code-credentials",
337
- # 从 authclaude.rb 中的 configure_keychain 方法可以看到,service_name 来自配置
338
- # 但我们不知道具体的服务名,所以需要通过搜索来查找
339
+ "Claude Code",
340
+ "Claude-API",
341
+ "claude-api",
342
+ "claude-credentials",
343
+ "claude",
344
+ "anthropic-claude",
345
+ "anthropic",
339
346
  ]
340
-
341
- account_name = Etc.getlogin || ENV['USER'] || ENV['LOGNAME']
342
-
343
- # 搜索所有包含 "claude" 的 keychain 条目
344
- search_cmd = ["security", "dump-keychain", "-d"]
345
-
346
- begin
347
- stdout, stderr, status = Open3.capture3(*search_cmd)
347
+
348
+ # 逐个检查已知的服务名,避免使用 dump-keychain
349
+ possible_services.each do |service_name|
350
+ # 使用 find-generic-password 检查条目是否存在
351
+ # 不使用 -w 选项,只检查是否存在,不获取密码内容
352
+ cmd = ["security", "find-generic-password", "-a", account_name, "-s", service_name]
353
+ stdout, stderr, status = Open3.capture3(*cmd)
354
+
355
+ # 如果命令成功(返回码0),说明条目存在
348
356
  if status.success?
349
- # 解析输出,查找 Claude 相关条目
350
- stdout.lines.each do |line|
351
- if line.include?("claude") && line.include?("svce")
352
- # 提取服务名称
353
- match = line.match(/"([^"]*claude[^"]*)"/)
354
- entries << match[1] if match
355
- end
356
- end
357
- end
358
- rescue => e
359
- # 如果搜索失败,回退到检查已知的服务名
360
- possible_services.each do |service_name|
361
- cmd = ["security", "find-generic-password", "-a", account_name, "-s", service_name]
362
- if system(*cmd, out: File::NULL, err: File::NULL)
363
- entries << service_name
364
- end
357
+ entries << service_name
365
358
  end
366
359
  end
367
-
360
+
361
+ # 额外检查:使用 list-keychains 安全地获取钥匙串列表
362
+ # 但不解密内容,只是查看是否有相关条目
363
+ # 注意:这个方法更安全,不会触发密码提示
364
+
368
365
  entries.uniq
369
366
  end
370
367
 
@@ -1,15 +1,22 @@
1
+ require 'json'
2
+ require 'uri'
3
+ require 'colored2'
4
+ require_relative '../config/config'
5
+
1
6
  module EasyAI
2
7
  class Command
3
8
  class GPT < Command
4
9
  self.summary = '运行 OpenAI CLI'
5
10
  self.description = <<-DESC
6
11
  启动 OpenAI GPT CLI 工具。
7
-
12
+
8
13
  主要功能:
9
14
 
15
+ * 支持远程配置下载
16
+
10
17
  * 自动配置 API 密钥
11
18
 
12
- * 支持 API 调用
19
+ * 支持本地配置文件
13
20
 
14
21
  * 透传所有参数
15
22
 
@@ -17,14 +24,40 @@ module EasyAI
17
24
 
18
25
  $ easyai gpt # 启动 OpenAI CLI
19
26
 
27
+ $ easyai gpt ./config.json # 使用本地配置文件
28
+
20
29
  $ easyai gpt api chat.completions # 调用 Chat API
21
30
 
22
31
  $ easyai gpt --help # 查看 OpenAI 帮助
32
+
33
+ $ easyai gpt --verbose # 显示详细信息
34
+
35
+ $ easyai gpt --no-keychain # 禁用密码存储
23
36
  DESC
24
37
 
38
+ def self.options
39
+ [
40
+ ['--no-keychain', '禁用自动密码存储'],
41
+ ['--verbose', '显示详细信息']
42
+ ].concat(super)
43
+ end
44
+
25
45
  def initialize(argv)
46
+ @no_keychain = argv.flag?('no-keychain')
47
+ @verbose_mode = argv.flag?('verbose')
48
+
26
49
  super
27
- @gpt_args = @argv.remainder!
50
+
51
+ # 获取剩余参数
52
+ remaining_args = @argv.remainder!
53
+
54
+ # 检查第一个参数是否是配置文件
55
+ if remaining_args.first && File.exist?(remaining_args.first) && remaining_args.first.end_with?('.json')
56
+ @config_file = remaining_args.shift
57
+ end
58
+
59
+ # 剩余的参数传递给 openai
60
+ @gpt_args = remaining_args
28
61
  end
29
62
 
30
63
  def validate!
@@ -33,14 +66,62 @@ module EasyAI
33
66
  end
34
67
 
35
68
  def run
36
- # 直接使用环境变量,不再依赖配置文件
37
- env = ENV.to_h
38
-
39
- # 如果环境变量中已经设置了 API KEY,直接使用
40
- # 用户可以通过 export OPENAI_API_KEY=xxx 来设置
41
-
42
- puts "正在运行: openai #{@gpt_args.join(' ')}".blue if verbose?
43
- exec(env, 'openai', *@gpt_args)
69
+ begin
70
+ # 首先尝试获取配置
71
+ remote_config = nil
72
+
73
+ if @config_file
74
+ # 使用指定的本地配置文件
75
+ print_status("📁 使用本地配置", File.basename(@config_file))
76
+ remote_config = load_local_config(@config_file)
77
+ print_success("配置加载成功") if remote_config
78
+ else
79
+ # 从远程下载配置,传递选项(指定工具类型为 gpt)
80
+ print_status("🔄 获取远程配置", "默认用户")
81
+ options = {
82
+ no_keychain: @no_keychain,
83
+ verbose: @verbose_mode,
84
+ tool_type: "gpt" # 指定使用 gpt 组的配置
85
+ }
86
+ remote_config = ConfigManager.download_user_config(nil, options)
87
+
88
+ # 处理用户取消授权的情况
89
+ if remote_config == :user_cancelled
90
+ print_error("用户取消了授权登录")
91
+ exit 0
92
+ end
93
+
94
+ print_success("配置加载成功") if remote_config
95
+ end
96
+
97
+ # 如果远程配置获取失败,回退到本地配置
98
+ if remote_config.nil?
99
+ print_warning("使用本地配置")
100
+ remote_config = load_local_yaml_config
101
+
102
+ # 如果本地配置也为空,提示用户先进行设置
103
+ if remote_config.empty?
104
+ print_error("未找到有效配置")
105
+ puts " 请先运行: #{'easyai --setup'.yellow}"
106
+ exit 1
107
+ end
108
+ end
109
+
110
+ # 构建环境变量
111
+ env = build_environment(remote_config)
112
+
113
+ # 运行 OpenAI CLI
114
+ print_status("🚀 启动 OpenAI", "openai #{@gpt_args.join(' ')}")
115
+ exec(env, 'openai', *@gpt_args)
116
+
117
+ rescue Interrupt
118
+ puts "\n已取消"
119
+ exit 0
120
+ rescue => e
121
+ print_error("运行失败: #{e.message}")
122
+ puts e.backtrace if @verbose_mode
123
+ exit 1
124
+ end
44
125
  end
45
126
 
46
127
  private
@@ -53,6 +134,129 @@ module EasyAI
53
134
  system('which openai > /dev/null 2>&1')
54
135
  end
55
136
  end
137
+
138
+ def load_local_config(config_file)
139
+ begin
140
+ content = File.read(config_file)
141
+ JSON.parse(content)
142
+ rescue JSON::ParserError => e
143
+ print_error("配置文件格式错误: #{e.message}")
144
+ nil
145
+ rescue => e
146
+ print_error("读取配置文件失败: #{e.message}")
147
+ nil
148
+ end
149
+ end
150
+
151
+ def load_local_yaml_config
152
+ config_file = File.expand_path('~/.easyai/config.yml')
153
+ return {} unless File.exist?(config_file)
154
+
155
+ begin
156
+ require 'yaml'
157
+ YAML.load_file(config_file) || {}
158
+ rescue => e
159
+ print_error("读取本地配置失败: #{e.message}")
160
+ {}
161
+ end
162
+ end
163
+
164
+ def build_environment(config)
165
+ env = ENV.to_h
166
+
167
+ # 设置 OpenAI API 密钥
168
+ if config["env"] && config["env"]["OPENAI_API_KEY"]
169
+ env["OPENAI_API_KEY"] = config["env"]["OPENAI_API_KEY"]
170
+
171
+ # 统一显示方式:不显示完整的 API 密钥
172
+ print_status("🔑 API 密钥", mask_token(config["env"]["OPENAI_API_KEY"]))
173
+
174
+ if @verbose_mode
175
+ # verbose 模式:显示更多状态信息(但不显示敏感内容)
176
+ puts " 密钥长度: #{config["env"]["OPENAI_API_KEY"].length} 字符"
177
+ end
178
+ end
179
+
180
+ # 设置代理(如果配置中有)
181
+ if config["gpt_proxy"]
182
+ proxy_urls = []
183
+
184
+ if config["gpt_proxy"]["HTTP_PROXY"]
185
+ env["HTTP_PROXY"] = config["gpt_proxy"]["HTTP_PROXY"]
186
+ env["http_proxy"] = config["gpt_proxy"]["HTTP_PROXY"]
187
+ proxy_urls << config["gpt_proxy"]["HTTP_PROXY"]
188
+ end
189
+
190
+ if config["gpt_proxy"]["HTTPS_PROXY"]
191
+ env["HTTPS_PROXY"] = config["gpt_proxy"]["HTTPS_PROXY"]
192
+ env["https_proxy"] = config["gpt_proxy"]["HTTPS_PROXY"]
193
+ proxy_urls << config["gpt_proxy"]["HTTPS_PROXY"] unless proxy_urls.include?(config["gpt_proxy"]["HTTPS_PROXY"])
194
+ end
195
+
196
+ if proxy_urls.any?
197
+ # 统一显示方式:简化代理显示
198
+ simplified_proxies = proxy_urls.map { |url| mask_url(url) }
199
+ print_status("🌐 代理已配置", simplified_proxies.join(', '))
200
+
201
+ if @verbose_mode
202
+ # verbose 模式:显示更多状态信息(但不显示敏感内容)
203
+ puts " 代理类型: #{proxy_urls.uniq.count == 1 ? '统一代理' : '分离代理'}"
204
+ proxy_urls.uniq.each do |url|
205
+ uri = URI(url) rescue nil
206
+ if uri
207
+ puts " 代理服务器: #{uri.host}:#{uri.port}"
208
+ end
209
+ end
210
+ end
211
+ end
212
+ end
213
+
214
+ env
215
+ end
216
+
217
+ def mask_token(token)
218
+ return nil unless token
219
+ return "空令牌" if token.empty?
220
+
221
+ if token.length > 10
222
+ "#{token[0..5]}...#{token[-4..-1]}"
223
+ else
224
+ "*" * token.length
225
+ end
226
+ end
227
+
228
+ def mask_url(url)
229
+ return url unless url
230
+
231
+ if url =~ /^(https?:\/\/)([^:]+):([^@]+)@(.+)$/
232
+ protocol, user, pass, rest = $1, $2, $3, $4
233
+ masked_pass = '*' * [pass.length, 8].min
234
+ "#{protocol}#{user}:#{masked_pass}@#{rest}"
235
+ else
236
+ url
237
+ end
238
+ end
239
+
240
+ def print_status(icon_text, detail = nil)
241
+ if detail
242
+ icon_width = 5
243
+ puts sprintf("%-#{icon_width}s %s", icon_text, detail.cyan)
244
+ else
245
+ puts icon_text
246
+ end
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
56
260
  end
57
261
  end
58
262
  end
@@ -0,0 +1,262 @@
1
+ require 'json'
2
+ require 'etc'
3
+ require 'open3'
4
+ require 'fileutils'
5
+ require_relative '../../config/config'
6
+ require_relative '../../auth/authclaude'
7
+
8
+ module EasyAI
9
+ class Command
10
+ class Utils < Command
11
+ class Export < Utils
12
+ self.summary = '导出 AI 配置信息'
13
+ self.description = <<-DESC
14
+ 导出当前系统的 AI 配置信息到 JSON 文件。
15
+
16
+ 主要功能:
17
+
18
+ * 从 ~/.claude.json 读取配置
19
+
20
+ * 从 Keychain 读取凭证(macOS)
21
+
22
+ * 从环境变量读取令牌
23
+
24
+ * 自动生成文件名(使用邮箱地址)
25
+
26
+ 使用示例:
27
+
28
+ $ easyai utils export # 自动生成文件名
29
+
30
+ $ easyai utils export --output=config.json # 指定输出文件名
31
+
32
+ $ easyai utils export --verbose # 显示详细信息
33
+ DESC
34
+
35
+ def self.options
36
+ [
37
+ ['--output=FILE', '指定输出文件名'],
38
+ ['--verbose', '显示详细信息']
39
+ ].concat(super)
40
+ end
41
+
42
+ def initialize(argv)
43
+ @output_file = argv.option('output')
44
+ @verbose = argv.flag?('verbose')
45
+ super
46
+ end
47
+
48
+ def run
49
+ puts "正在导出 AI 配置信息..."
50
+ puts "" if @verbose
51
+
52
+ # 收集配置信息
53
+ export_config = {}
54
+
55
+ # 1. 读取 ~/.claude.json
56
+ user_claude_file = File.expand_path("~/.claude.json")
57
+
58
+ if File.exist?(user_claude_file)
59
+ print_status("📄 读取配置", user_claude_file) if @verbose
60
+
61
+ begin
62
+ user_config = JSON.parse(File.read(user_claude_file))
63
+
64
+ # 排除 projects 字段
65
+ claude_config = user_config.reject { |key, _| key == "projects" }
66
+
67
+ # 添加到导出配置
68
+ export_config["claude_json"] = claude_config
69
+
70
+ print_success("读取 ~/.claude.json 成功") if @verbose
71
+
72
+ # 如果未指定输出文件名,尝试从配置中获取邮箱地址
73
+ if @output_file.nil? && claude_config["oauthAccount"]
74
+ email = claude_config["oauthAccount"]["emailAddress"]
75
+ if email && !email.empty?
76
+ # 使用邮箱地址作为文件名(替换特殊字符)
77
+ safe_filename = email.gsub(/[@.]/, '_')
78
+ @output_file = "#{safe_filename}.json"
79
+ puts " 使用邮箱生成文件名: #{@output_file}" if @verbose
80
+ end
81
+ end
82
+
83
+ rescue JSON::ParserError => e
84
+ print_error("解析 ~/.claude.json 失败: #{e.message}")
85
+ exit 1
86
+ rescue => e
87
+ print_error("读取 ~/.claude.json 失败: #{e.message}")
88
+ exit 1
89
+ end
90
+ else
91
+ print_warning("未找到 ~/.claude.json 文件")
92
+ end
93
+
94
+ # 2. 从 Keychain 读取凭证(macOS)
95
+ if RUBY_PLATFORM.include?('darwin')
96
+ print_status("🔐 读取 Keychain", "Claude Code-credentials") if @verbose
97
+
98
+ keychain_data = read_keychain_credentials
99
+ if keychain_data
100
+ export_config["key_chain"] = {
101
+ "service_name" => "Claude Code-credentials",
102
+ "claudeAiOauth" => keychain_data
103
+ }
104
+ print_success("读取 Keychain 凭证成功") if @verbose
105
+ else
106
+ print_warning("未找到 Keychain 凭证(可选)") if @verbose
107
+ end
108
+ else
109
+ puts " ⚠️ 非 macOS 系统,跳过 Keychain 读取" if @verbose
110
+ end
111
+
112
+ # 3. 从环境变量读取令牌(始终包含 env 字段)
113
+ token_value = ENV['CLAUDE_CODE_OAUTH_TOKEN'] || ""
114
+
115
+ export_config["env"] = {
116
+ "CLAUDE_CODE_OAUTH_TOKEN" => token_value
117
+ }
118
+
119
+ if token_value && !token_value.empty?
120
+ print_status("🔑 读取环境变量", "CLAUDE_CODE_OAUTH_TOKEN") if @verbose
121
+ print_success("读取环境变量成功") if @verbose
122
+ else
123
+ print_warning("CLAUDE_CODE_OAUTH_TOKEN 为空(将导出空值)") if @verbose
124
+ end
125
+
126
+ # 4. 读取代理配置(始终包含 claude_proxy 字段)
127
+ proxy_config = {}
128
+
129
+ # 检查 HTTP_PROXY 相关环境变量
130
+ http_proxy = ENV['HTTP_PROXY'] || ENV['http_proxy'] || ""
131
+ proxy_config['HTTP_PROXY'] = http_proxy
132
+
133
+ # 检查 HTTPS_PROXY 相关环境变量
134
+ https_proxy = ENV['HTTPS_PROXY'] || ENV['https_proxy'] || ""
135
+ proxy_config['HTTPS_PROXY'] = https_proxy
136
+
137
+ # 始终添加 claude_proxy 字段
138
+ export_config["claude_proxy"] = proxy_config
139
+
140
+ # 显示状态信息
141
+ if @verbose
142
+ has_http = !http_proxy.empty?
143
+ has_https = !https_proxy.empty?
144
+
145
+ if has_http || has_https
146
+ print_status("🌐 读取代理配置", "环境变量")
147
+ print_success("读取代理配置成功")
148
+ puts " HTTP_PROXY: #{has_http ? mask_url(http_proxy) : '(空)'}"
149
+ puts " HTTPS_PROXY: #{has_https ? mask_url(https_proxy) : '(空)'}"
150
+ else
151
+ print_warning("代理配置为空(将导出空值)")
152
+ end
153
+ end
154
+
155
+ # 检查是否收集到任何配置
156
+ if export_config.empty?
157
+ print_error("未找到任何配置信息可导出")
158
+ puts " 请确保已配置 Claude 或运行 'easyai --setup'"
159
+ exit 1
160
+ end
161
+
162
+ # 设置默认输出文件名
163
+ @output_file ||= "claude_setting.json"
164
+
165
+ # 写入文件
166
+ puts "" if @verbose
167
+ print_status("💾 写入文件", @output_file)
168
+
169
+ begin
170
+ # 美化 JSON 输出
171
+ json_output = JSON.pretty_generate(export_config)
172
+
173
+ # 写入文件
174
+ File.write(@output_file, json_output)
175
+
176
+ print_success("配置成功导出到 #{@output_file}")
177
+
178
+ # 显示导出内容摘要
179
+ if @verbose
180
+ puts "\n导出内容摘要:"
181
+ puts " ✓ claude.json 配置" if export_config["claude_json"]
182
+ puts " ✓ Keychain 凭证" if export_config["key_chain"]
183
+ puts " ✓ 环境变量令牌" if export_config["env"]
184
+ puts " ✓ 代理配置" if export_config["claude_proxy"]
185
+ end
186
+
187
+ rescue => e
188
+ print_error("写入文件失败: #{e.message}")
189
+ exit 1
190
+ end
191
+ end
192
+
193
+ private
194
+
195
+ def claude_available?
196
+ system('which claude > /dev/null 2>&1')
197
+ end
198
+
199
+ def read_keychain_credentials
200
+ return nil unless RUBY_PLATFORM.include?('darwin')
201
+
202
+ service_name = "Claude Code-credentials"
203
+ account_name = Etc.getlogin || ENV['USER'] || ENV['LOGNAME']
204
+
205
+ # 获取 Keychain 中的密码
206
+ cmd = [
207
+ "security",
208
+ "find-generic-password",
209
+ "-a", account_name,
210
+ "-s", service_name,
211
+ "-w" # 只输出密码
212
+ ]
213
+
214
+ stdout, stderr, status = Open3.capture3(*cmd)
215
+
216
+ if status.success? && !stdout.strip.empty?
217
+ # 尝试解析 JSON 格式的凭证
218
+ begin
219
+ credentials = JSON.parse(stdout.strip)
220
+ return credentials["claudeAiOauth"]
221
+ rescue JSON::ParserError
222
+ # 如果不是 JSON,直接返回字符串
223
+ return stdout.strip
224
+ end
225
+ end
226
+
227
+ nil
228
+ end
229
+
230
+ def mask_url(url)
231
+ # 隐藏代理 URL 中的敏感信息
232
+ return url unless @verbose
233
+
234
+ if url =~ /^(https?:\/\/)([^:]+):([^@]+)@(.+)$/
235
+ protocol, user, pass, rest = $1, $2, $3, $4
236
+ masked_pass = '*' * [pass.length, 8].min
237
+ "#{protocol}#{user}:#{masked_pass}@#{rest}"
238
+ else
239
+ url
240
+ end
241
+ end
242
+
243
+ def print_status(icon_text, detail)
244
+ icon_width = 5 # 图标部分的固定宽度(包括空格)
245
+ puts sprintf("%-#{icon_width}s %s", icon_text, detail.cyan)
246
+ end
247
+
248
+ def print_success(message)
249
+ puts " ✓ #{message}".green
250
+ end
251
+
252
+ def print_warning(message)
253
+ puts " ⚠️ #{message}".yellow
254
+ end
255
+
256
+ def print_error(message)
257
+ puts "✗ #{message}".red
258
+ end
259
+ end
260
+ end
261
+ end
262
+ end
@@ -11,11 +11,15 @@ module EasyAI
11
11
 
12
12
  * decry - 文件解密工具
13
13
 
14
+ * export - 导出 AI 配置信息
15
+
14
16
  使用示例:
15
17
 
16
18
  $ easyai utils encry file.txt # 加密文件
17
19
 
18
20
  $ easyai utils decry file.encrypted # 解密文件
21
+
22
+ $ easyai utils export # 导出配置
19
23
  DESC
20
24
 
21
25
  self.abstract_command = true
@@ -29,4 +33,5 @@ end
29
33
 
30
34
  # 加载子命令
31
35
  require_relative 'utils/encry'
32
- require_relative 'utils/decry'
36
+ require_relative 'utils/decry'
37
+ require_relative 'utils/export'
@@ -15,6 +15,7 @@ module EasyAI
15
15
  # 类变量初始化
16
16
  @@no_keychain = false
17
17
  @@verbose = false
18
+ @@tool_type = nil
18
19
 
19
20
  # 管理配置仓库:如果存在则更新,不存在则下载
20
21
  def self.manage_config_repo
@@ -92,12 +93,14 @@ module EasyAI
92
93
  def self.load_from_local_repo(user_name = nil, options = {})
93
94
  # 首先管理配置仓库
94
95
  return nil unless manage_config_repo
95
-
96
+
96
97
  # 设置全局选项
97
98
  @@no_keychain = options[:no_keychain] || false
98
99
  @@verbose = options[:verbose] || false
99
-
100
+ @@tool_type = options[:tool_type] || nil
101
+
100
102
  puts "正在从本地配置仓库加载配置..." if @@verbose
103
+ puts " 工具类型: #{@@tool_type || '自动'}" if @@verbose && @@tool_type
101
104
 
102
105
  begin
103
106
  # 检查 index.json 文件,支持加密版本
@@ -125,8 +128,9 @@ module EasyAI
125
128
  end
126
129
 
127
130
  return nil if users.nil?
128
-
129
- if users.empty?
131
+
132
+ # 检查是否有可用用户(支持新旧格式)
133
+ if is_users_empty?(users)
130
134
  puts "✗ index.json 中未找到用户"
131
135
  return nil
132
136
  end
@@ -148,17 +152,17 @@ module EasyAI
148
152
  end
149
153
  end
150
154
 
151
- # 查找用户配置文件
152
- config_filename = find_user_config(users, selected_user.strip)
153
-
155
+ # 查找用户配置文件(传入工具类型)
156
+ config_filename = find_user_config(users, selected_user.strip, @@tool_type)
157
+
154
158
  if config_filename.nil?
155
159
  puts "✗ 用户 '#{selected_user}' 未找到"
156
- puts " 可用用户: #{users.keys.join(', ')}"
160
+ # 不显示可用用户列表,保护配置信息
161
+ puts " 请联系管理员确认用户权限"
157
162
  return nil
158
163
  end
159
-
160
- # 加载配置文件,支持加密版本
161
- config_filename += '.json' unless config_filename.end_with?('.json')
164
+
165
+ # 加载配置文件,支持加密版本(find_user_config 已确保包含 .json)
162
166
  config_file = File.join(CONFIG_REPO_DIR, config_filename)
163
167
  encrypted_config_file = File.join(CONFIG_REPO_DIR, "#{config_filename}.encrypted")
164
168
 
@@ -199,10 +203,11 @@ module EasyAI
199
203
  # 原有的临时下载方式(作为备用方案)
200
204
  def self.download_user_config_temp(user_name = nil, options = {})
201
205
  puts "正在获取配置文件..."
202
-
206
+
203
207
  # 设置全局选项
204
208
  @@no_keychain = options[:no_keychain] || false
205
209
  @@verbose = options[:verbose] || false
210
+ @@tool_type = options[:tool_type] || nil
206
211
 
207
212
  begin
208
213
  # 创建临时目录
@@ -250,8 +255,9 @@ module EasyAI
250
255
  end
251
256
 
252
257
  return nil if users.nil?
253
-
254
- if users.empty?
258
+
259
+ # 检查是否有可用用户(支持新旧格式)
260
+ if is_users_empty?(users)
255
261
  puts "✗ index.json 中未找到用户"
256
262
  cleanup_temp_dir(temp_dir)
257
263
  return nil
@@ -276,18 +282,18 @@ module EasyAI
276
282
  end
277
283
  end
278
284
 
279
- # 查找用户配置文件
280
- config_filename = find_user_config(users, selected_user.strip)
281
-
285
+ # 查找用户配置文件(传入工具类型)
286
+ config_filename = find_user_config(users, selected_user.strip, @@tool_type)
287
+
282
288
  if config_filename.nil?
283
289
  puts "✗ 用户 '#{selected_user}' 未找到"
284
- puts " 可用用户: #{users.keys.join(', ')}"
290
+ # 不显示可用用户列表,保护配置信息
291
+ puts " 请联系管理员确认用户权限"
285
292
  cleanup_temp_dir(temp_dir)
286
293
  return nil
287
294
  end
288
-
289
- # 加载配置文件,支持加密版本
290
- config_filename += '.json' unless config_filename.end_with?('.json')
295
+
296
+ # 加载配置文件,支持加密版本(find_user_config 已确保包含 .json)
291
297
  config_file = File.join(temp_dir, config_filename)
292
298
  encrypted_config_file = File.join(temp_dir, "#{config_filename}.encrypted")
293
299
 
@@ -359,14 +365,95 @@ module EasyAI
359
365
  return nil
360
366
  end
361
367
 
362
- def self.find_user_config(users, user_input_clean)
363
- users.each do |name, filename|
364
- if name.downcase == user_input_clean.downcase
365
- return filename
368
+ def self.find_user_config(users, user_input_clean, tool_type = nil)
369
+ # 处理新的分组格式 {"claude": {...}, "gemini": {...}}
370
+ if users.is_a?(Hash) && (users.key?("claude") || users.key?("gemini"))
371
+ # 如果指定了工具类型,只在该组中查找
372
+ if tool_type && users[tool_type]
373
+ users[tool_type].each do |name, filename|
374
+ if name.downcase == user_input_clean.downcase
375
+ # 确保文件名包含 .json 后缀
376
+ filename = "#{filename}.json" unless filename.end_with?('.json')
377
+ return filename
378
+ end
379
+ end
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
389
+ end
390
+ end
391
+ end
392
+ end
393
+ 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
366
401
  end
367
402
  end
403
+
368
404
  nil
369
405
  end
406
+
407
+ # 获取所有可用的用户名列表
408
+ def self.get_all_user_names(users)
409
+ all_names = []
410
+
411
+ # 处理新的分组格式
412
+ if users.is_a?(Hash) && (users.key?("claude") || users.key?("gemini"))
413
+ %w[claude gemini gpt].each do |group|
414
+ if users[group] && users[group].is_a?(Hash)
415
+ group_names = users[group].keys
416
+ all_names.concat(group_names.map { |name| "#{name} (#{group})" })
417
+ end
418
+ end
419
+ else
420
+ # 兼容旧格式
421
+ all_names = users.keys
422
+ end
423
+
424
+ all_names
425
+ end
426
+
427
+ # 获取特定工具的用户名列表
428
+ def self.get_tool_user_names(users, tool_type)
429
+ return [] unless tool_type
430
+
431
+ # 处理新的分组格式
432
+ if users.is_a?(Hash) && (users.key?("claude") || users.key?("gemini"))
433
+ # 新格式:返回指定工具组的用户
434
+ users[tool_type].is_a?(Hash) ? users[tool_type].keys : []
435
+ else
436
+ # 旧格式返回所有用户(因为没有工具分组)
437
+ users.is_a?(Hash) ? users.keys : []
438
+ end
439
+ end
440
+
441
+ # 检查用户列表是否为空
442
+ def self.is_users_empty?(users)
443
+ return true if users.nil?
444
+
445
+ # 处理新的分组格式
446
+ if users.is_a?(Hash) && (users.key?("claude") || users.key?("gemini"))
447
+ # 检查所有组是否都为空
448
+ %w[claude gemini gpt].each do |group|
449
+ return false if users[group] && !users[group].empty?
450
+ end
451
+ true
452
+ else
453
+ # 兼容旧格式
454
+ users.empty?
455
+ end
456
+ end
370
457
 
371
458
  def self.load_config_file(config_file)
372
459
  begin
@@ -1,3 +1,3 @@
1
1
  module EasyAI
2
- VERSION = '1.1.3'
2
+ VERSION = '1.2.0'
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: easyai
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.3
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wade
@@ -122,6 +122,7 @@ files:
122
122
  - lib/easyai/command/utils.rb
123
123
  - lib/easyai/command/utils/decry.rb
124
124
  - lib/easyai/command/utils/encry.rb
125
+ - lib/easyai/command/utils/export.rb
125
126
  - lib/easyai/config/config.rb
126
127
  - lib/easyai/version.rb
127
128
  homepage: https://github.com/wade/easyai