easyai 1.1.1 → 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: 60e6aec7d6d0d2475a75e26f61f21f1f5354b75ba828006293cdcdd3a6961f8a
4
- data.tar.gz: bd0c31dccfcb0918707972a72415382b6ca778015ae67932626e4ad40933b747
3
+ metadata.gz: a805223b3184cf7726f32044944ac33f5dc54e2058e2908d6423e8b846ae0483
4
+ data.tar.gz: 832ea4df5a956690ec3bedc93d995130e6a9ba23d01e684ec8d247e46a1382e1
5
5
  SHA512:
6
- metadata.gz: 31725ddd0f5c1ce5e3acda536b8c502addefc67271abb0b719521349fd3f3d840f2c41151982fbaca3044d34e6e110ef5e663155ee5c71ff636a6eb5af6b749a
7
- data.tar.gz: 3045e64aa95c96a0ca58934f080dc6e82512121348290db4b4d8859b0af016f343893a8208f281f6be33af75afd4e2160fbc04451adf8f89ff9d62a21b202719
6
+ metadata.gz: c88789162eab4d2502697a5533e6d077ca595dc50fd25a9d7b5743c4457cd55dcce0d3ade4c383986937c4a17b2e37db14bc50c8d124fd5ecd429a8ab44aa6d7
7
+ data.tar.gz: f718001928ab81c0fb4e9c245deb6f93ef7518e7407fcf9621301e8d3466beb3fb6dbbef6adc3c1d20f4d6c109023521b357550d3635af0831a2e3d47a2ac2f9
@@ -14,7 +14,7 @@ module EasyAI
14
14
  class JPSLogin
15
15
  attr_reader :access_token, :username, :expires_at
16
16
 
17
- def initialize
17
+ def initialize(options = {})
18
18
  @client_id = "cli_a7bc7fe9b3d1d00b"
19
19
  @server_port = 8898
20
20
  @state = "client_login"
@@ -32,7 +32,10 @@ module EasyAI
32
32
  @access_token = nil
33
33
  @username = nil
34
34
  @expires_at = nil
35
-
35
+
36
+ # 从选项中获取 verbose 标志
37
+ @verbose = options[:verbose] || false
38
+
36
39
  # token 存储路径
37
40
  @token_dir = File.expand_path('~/.easyai')
38
41
  @token_file = File.join(@token_dir, '.jpstoken')
@@ -44,9 +47,14 @@ module EasyAI
44
47
  if load_stored_token && validate_token
45
48
  return true
46
49
  end
47
-
50
+
48
51
  puts "🔐 需要登录 JPS..."
49
- return authorize_and_login
52
+ result = authorize_and_login
53
+
54
+ # 如果用户主动取消,返回特殊标识
55
+ return :user_cancelled if result == :user_cancelled
56
+
57
+ return result
50
58
  end
51
59
 
52
60
  # 获取用户名(登录后可用)
@@ -93,8 +101,12 @@ module EasyAI
93
101
  # 构建授权 URL
94
102
  authorization_uri = build_authorization_uri
95
103
  puts "正在打开浏览器进行飞书 OAuth 授权..."
96
- puts "授权 URL: #{authorization_uri}"
97
-
104
+ puts "\n授权 URL(如自动打开失败,请手动复制下面的链接到浏览器):"
105
+ puts "=" * 80
106
+ puts authorization_uri
107
+ puts "=" * 80
108
+ puts ""
109
+
98
110
  # 在浏览器中打开授权 URL
99
111
  open_browser(authorization_uri)
100
112
 
@@ -103,33 +115,88 @@ module EasyAI
103
115
 
104
116
  # 如果自动获取失败,提示用户手动输入
105
117
  if code.nil?
106
- puts "自动获取授权码失败,您可以手动输入:"
107
- puts "1. 授权码 (直接复制 'code=' 后面的内容)"
108
- puts "2. 完整回调 URL"
109
- print "> "
110
- input = STDIN.gets.chomp
111
-
112
- if input.start_with?("http")
113
- # 尝试从 URL 中提取 code
118
+ loop do
119
+ puts "\n自动获取授权码失败,请选择:"
120
+ puts "1. 输入授权码 (直接复制 'code=' 后面的内容)"
121
+ puts "2. 输入完整回调 URL"
122
+ puts "3. 重新打开授权网页"
123
+ puts "4. 退出"
124
+ print "> "
125
+
114
126
  begin
115
- uri = URI(input)
116
- query_params = URI.decode_www_form(uri.query || '').to_h
117
- code = query_params['code']
118
- puts "从 URL 中成功提取授权码" if code
119
- rescue => e
120
- puts "无法从 URL 中提取授权码: #{e.message}"
127
+ choice = STDIN.gets&.chomp
128
+
129
+ # 处理 Ctrl+C 中断
130
+ if choice.nil?
131
+ puts "\n用户中断操作"
132
+ return :user_cancelled
133
+ end
134
+
135
+ case choice
136
+ when "1"
137
+ puts "请输入授权码:"
138
+ print "> "
139
+ code_input = STDIN.gets&.chomp
140
+ if code_input.nil?
141
+ puts "用户中断操作"
142
+ return :user_cancelled
143
+ elsif !code_input.empty?
144
+ code = code_input
145
+ break
146
+ else
147
+ puts "授权码不能为空,请重新选择"
148
+ end
149
+ when "2"
150
+ puts "请输入完整回调 URL:"
151
+ print "> "
152
+ url_input = STDIN.gets&.chomp
153
+ if url_input.nil?
154
+ puts "用户中断操作"
155
+ return :user_cancelled
156
+ elsif url_input.start_with?("http")
157
+ # 尝试从 URL 中提取 code
158
+ begin
159
+ uri = URI(url_input)
160
+ query_params = URI.decode_www_form(uri.query || '').to_h
161
+ code = query_params['code']
162
+ if code
163
+ puts "✓ 从 URL 中成功提取授权码"
164
+ break
165
+ else
166
+ puts "✗ URL 中没有找到授权码,请重新选择"
167
+ end
168
+ rescue => e
169
+ puts "✗ 无法从 URL 中提取授权码: #{e.message}"
170
+ end
171
+ else
172
+ puts "✗ 无效的 URL 格式,请重新选择"
173
+ end
174
+ when "3"
175
+ # 重新打开授权网页
176
+ puts "正在重新打开授权网页..."
177
+ open_browser(authorization_uri)
178
+ # 重新启动服务器尝试获取授权码
179
+ code = start_callback_server
180
+ if code
181
+ break
182
+ end
183
+ # 如果还是失败,继续循环让用户选择
184
+ when "4"
185
+ puts "已退出授权流程"
186
+ return :user_cancelled
187
+ else
188
+ puts "无效的选择,请输入 1-4 之间的数字"
189
+ end
190
+ rescue Interrupt
191
+ puts "\n\n用户中断操作"
192
+ return :user_cancelled
121
193
  end
122
- else
123
- # 将输入直接作为 code
124
- code = input unless input.empty?
125
194
  end
126
195
  end
127
196
 
128
197
  if code
129
- puts "成功获取授权码!正在使用飞书身份登录 JPS..."
130
198
  if exchange_code_for_token(code)
131
- puts "✓ JPS 登录成功!"
132
- puts " 用户名: #{@username}"
199
+ puts "✓ JPS 登录成功!用户名: #{@username}"
133
200
  store_token
134
201
  return true
135
202
  end
@@ -181,8 +248,10 @@ module EasyAI
181
248
  return nil
182
249
  end
183
250
 
184
- puts "启动本地服务器,监听端口 #{@server_port}..."
185
-
251
+ puts "启动本地服务器,监听端口 #{@server_port}..." if @verbose
252
+ puts "提示:按 Ctrl+C 可以中断并获得更多选择"
253
+ puts "🔄 正在使用飞书身份登录 JPS..."
254
+
186
255
  begin
187
256
  server = WEBrick::HTTPServer.new(
188
257
  Port: @server_port,
@@ -209,19 +278,19 @@ module EasyAI
209
278
  begin
210
279
  # 解析请求参数
211
280
  query_params = URI.decode_www_form(req.query_string || '').to_h
212
- puts "接收到回调请求,参数: #{query_params.inspect}"
281
+ puts "接收到回调请求,参数: #{query_params.inspect}" if @verbose
213
282
 
214
283
  if query_params['error']
215
- puts "授权错误: #{query_params['error']}"
284
+ puts "授权错误: #{query_params['error']}" if @verbose
216
285
  res.content_type = "text/html; charset=UTF-8"
217
286
  res.body = build_error_page(query_params['error'])
218
287
  elsif query_params['code']
219
288
  code = query_params['code']
220
- puts "成功获取授权码"
289
+ puts "成功获取授权码" if @verbose
221
290
  res.content_type = "text/html; charset=UTF-8"
222
291
  res.body = build_success_page
223
292
  else
224
- puts "未获取到授权码"
293
+ puts "未获取到授权码" if @verbose
225
294
  res.content_type = "text/html; charset=UTF-8"
226
295
  res.body = build_error_page("未获取到授权码")
227
296
  end
@@ -275,14 +344,14 @@ module EasyAI
275
344
  request['Content-Type'] = 'application/json'
276
345
  request.body = request_data.to_json
277
346
 
278
- puts "正在请求 JPS API: #{@api_endpoint}"
347
+ puts "正在请求 JPS API: #{@api_endpoint}" if @verbose
279
348
  response = http.request(request)
280
-
281
- puts "API 响应状态码: #{response.code}"
349
+
350
+ puts "API 响应状态码: #{response.code}" if @verbose
282
351
 
283
352
  if response.body
284
353
  result = JSON.parse(response.body)
285
- puts "API 响应: #{result.inspect}"
354
+ puts "API 响应: #{result.inspect}" if @verbose
286
355
 
287
356
  if result['meta'] && result['meta']['code'] == 200 && result['data']
288
357
  data = result['data']
@@ -322,7 +391,7 @@ module EasyAI
322
391
  }
323
392
 
324
393
  File.write(@token_file, token_data.to_json)
325
- puts "✓ JPS Token 和用户名已存储到 #{@token_file}"
394
+ puts "✓ JPS Token 和用户名已存储到 #{@token_file}" if @verbose
326
395
  rescue => e
327
396
  puts "⚠ 存储 JPS Token 失败: #{e.message}"
328
397
  end
@@ -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,10 +72,21 @@ 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 }
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)
83
+
84
+ # 处理用户取消授权的情况
85
+ if remote_config == :user_cancelled
86
+ print_error("用户取消了授权登录")
87
+ exit 0
88
+ end
89
+
77
90
  print_success("配置加载成功") if remote_config
78
91
  end
79
92
 
@@ -81,7 +94,7 @@ module EasyAI
81
94
  if remote_config.nil?
82
95
  print_warning("使用本地配置")
83
96
  remote_config = load_local_yaml_config
84
-
97
+
85
98
  # 如果本地配置也为空,提示用户先进行设置
86
99
  if remote_config.empty?
87
100
  print_error("未找到有效配置")
@@ -183,17 +196,16 @@ module EasyAI
183
196
  # 从配置中提取环境变量 - 只设置 CLAUDE_CODE_OAUTH_TOKEN
184
197
  if config && config['env'] && config['env']['CLAUDE_CODE_OAUTH_TOKEN']
185
198
  env['CLAUDE_CODE_OAUTH_TOKEN'] = config['env']['CLAUDE_CODE_OAUTH_TOKEN']
186
-
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
+
187
206
  if @verbose_mode
188
- # verbose 模式:显示完整的令牌和代理
189
- puts "\n🔑 CLAUDE_CODE_OAUTH_TOKEN:"
190
- puts " #{config['env']['CLAUDE_CODE_OAUTH_TOKEN'].green}"
191
- else
192
- # 普通模式:只显示令牌预览
193
- token_preview = config['env']['CLAUDE_CODE_OAUTH_TOKEN'].length > 15 ?
194
- "#{config['env']['CLAUDE_CODE_OAUTH_TOKEN'][0..15]}..." :
195
- config['env']['CLAUDE_CODE_OAUTH_TOKEN']
196
- print_status("🔑 令牌已配置", token_preview)
207
+ # verbose 模式:显示更多状态信息(但不显示敏感内容)
208
+ puts " 令牌长度: #{config['env']['CLAUDE_CODE_OAUTH_TOKEN'].length} 字符"
197
209
  end
198
210
  end
199
211
 
@@ -215,23 +227,26 @@ module EasyAI
215
227
  end
216
228
 
217
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
+
218
241
  if @verbose_mode
219
- # verbose 模式:显示完整的代理地址
220
- puts "\n🔗 代理配置:"
242
+ # verbose 模式:显示更多状态信息(但不显示敏感内容)
243
+ puts " 代理类型: #{proxy_urls.uniq.count == 1 ? '统一代理' : '分离代理'}"
221
244
  proxy_urls.uniq.each do |url|
222
- puts " #{url.green}"
223
- end
224
- else
225
- # 普通模式:简化代理显示,隐藏密码
226
- simplified_proxies = proxy_urls.uniq.map do |url|
227
245
  uri = URI(url) rescue nil
228
- if uri && uri.password
229
- "#{uri.scheme}://***@#{uri.host}:#{uri.port}"
230
- else
231
- url
246
+ if uri
247
+ puts " 代理服务器: #{uri.host}:#{uri.port}"
232
248
  end
233
249
  end
234
- print_status("🌐 代理已配置", simplified_proxies.join(', '))
235
250
  end
236
251
  end
237
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'
@@ -14,6 +14,8 @@ module EasyAI
14
14
 
15
15
  # 类变量初始化
16
16
  @@no_keychain = false
17
+ @@verbose = false
18
+ @@tool_type = nil
17
19
 
18
20
  # 管理配置仓库:如果存在则更新,不存在则下载
19
21
  def self.manage_config_repo
@@ -65,7 +67,7 @@ module EasyAI
65
67
 
66
68
  # 下载配置仓库到固定位置
67
69
  def self.download_config_repo
68
- puts "正在下载配置仓库到 #{CONFIG_REPO_DIR}..."
70
+ puts "正在下载配置仓库到 #{CONFIG_REPO_DIR}..." if @@verbose
69
71
 
70
72
  begin
71
73
  # 克隆仓库到固定位置,捕获错误信息
@@ -91,11 +93,14 @@ module EasyAI
91
93
  def self.load_from_local_repo(user_name = nil, options = {})
92
94
  # 首先管理配置仓库
93
95
  return nil unless manage_config_repo
94
-
96
+
95
97
  # 设置全局选项
96
98
  @@no_keychain = options[:no_keychain] || false
97
-
98
- puts "正在从本地配置仓库加载配置..."
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
99
104
 
100
105
  begin
101
106
  # 检查 index.json 文件,支持加密版本
@@ -123,8 +128,9 @@ module EasyAI
123
128
  end
124
129
 
125
130
  return nil if users.nil?
126
-
127
- if users.empty?
131
+
132
+ # 检查是否有可用用户(支持新旧格式)
133
+ if is_users_empty?(users)
128
134
  puts "✗ index.json 中未找到用户"
129
135
  return nil
130
136
  end
@@ -134,24 +140,29 @@ module EasyAI
134
140
  selected_user = user_name
135
141
  else
136
142
  # 统一使用 JPS 登录获取用户名
137
- selected_user = get_username_from_jps
138
- if selected_user.nil? || selected_user.strip.empty?
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?
139
150
  puts "✗ JPS 登录失败,无法获取用户名"
140
151
  return nil
141
152
  end
142
153
  end
143
154
 
144
- # 查找用户配置文件
145
- config_filename = find_user_config(users, selected_user.strip)
146
-
155
+ # 查找用户配置文件(传入工具类型)
156
+ config_filename = find_user_config(users, selected_user.strip, @@tool_type)
157
+
147
158
  if config_filename.nil?
148
159
  puts "✗ 用户 '#{selected_user}' 未找到"
149
- puts " 可用用户: #{users.keys.join(', ')}"
160
+ # 不显示可用用户列表,保护配置信息
161
+ puts " 请联系管理员确认用户权限"
150
162
  return nil
151
163
  end
152
-
153
- # 加载配置文件,支持加密版本
154
- config_filename += '.json' unless config_filename.end_with?('.json')
164
+
165
+ # 加载配置文件,支持加密版本(find_user_config 已确保包含 .json)
155
166
  config_file = File.join(CONFIG_REPO_DIR, config_filename)
156
167
  encrypted_config_file = File.join(CONFIG_REPO_DIR, "#{config_filename}.encrypted")
157
168
 
@@ -176,8 +187,14 @@ module EasyAI
176
187
  def self.download_user_config(user_name = nil, options = {})
177
188
  # 优先尝试使用本地配置仓库
178
189
  config = load_from_local_repo(user_name, options)
190
+
191
+ # 如果用户取消了授权,返回特殊标识
192
+ if config == :user_cancelled
193
+ return :user_cancelled
194
+ end
195
+
179
196
  return config if config
180
-
197
+
181
198
  # 如果本地仓库失败,回退到临时下载方式
182
199
  puts "本地配置仓库不可用,使用临时下载..."
183
200
  download_user_config_temp(user_name, options)
@@ -186,9 +203,11 @@ module EasyAI
186
203
  # 原有的临时下载方式(作为备用方案)
187
204
  def self.download_user_config_temp(user_name = nil, options = {})
188
205
  puts "正在获取配置文件..."
189
-
206
+
190
207
  # 设置全局选项
191
208
  @@no_keychain = options[:no_keychain] || false
209
+ @@verbose = options[:verbose] || false
210
+ @@tool_type = options[:tool_type] || nil
192
211
 
193
212
  begin
194
213
  # 创建临时目录
@@ -236,8 +255,9 @@ module EasyAI
236
255
  end
237
256
 
238
257
  return nil if users.nil?
239
-
240
- if users.empty?
258
+
259
+ # 检查是否有可用用户(支持新旧格式)
260
+ if is_users_empty?(users)
241
261
  puts "✗ index.json 中未找到用户"
242
262
  cleanup_temp_dir(temp_dir)
243
263
  return nil
@@ -248,26 +268,32 @@ module EasyAI
248
268
  selected_user = user_name
249
269
  else
250
270
  # 统一使用 JPS 登录获取用户名
251
- selected_user = get_username_from_jps
252
- if selected_user.nil? || selected_user.strip.empty?
271
+ selected_user = get_username_from_jps(@@verbose)
272
+
273
+ # 处理用户取消的情况
274
+ if selected_user == :user_cancelled
275
+ puts "✗ 用户取消了授权登录"
276
+ cleanup_temp_dir(temp_dir)
277
+ return nil
278
+ elsif selected_user.nil? || selected_user.strip.empty?
253
279
  puts "✗ JPS 登录失败,无法获取用户名"
254
280
  cleanup_temp_dir(temp_dir)
255
281
  return nil
256
282
  end
257
283
  end
258
284
 
259
- # 查找用户配置文件
260
- config_filename = find_user_config(users, selected_user.strip)
261
-
285
+ # 查找用户配置文件(传入工具类型)
286
+ config_filename = find_user_config(users, selected_user.strip, @@tool_type)
287
+
262
288
  if config_filename.nil?
263
289
  puts "✗ 用户 '#{selected_user}' 未找到"
264
- puts " 可用用户: #{users.keys.join(', ')}"
290
+ # 不显示可用用户列表,保护配置信息
291
+ puts " 请联系管理员确认用户权限"
265
292
  cleanup_temp_dir(temp_dir)
266
293
  return nil
267
294
  end
268
-
269
- # 加载配置文件,支持加密版本
270
- config_filename += '.json' unless config_filename.end_with?('.json')
295
+
296
+ # 加载配置文件,支持加密版本(find_user_config 已确保包含 .json)
271
297
  config_file = File.join(temp_dir, config_filename)
272
298
  encrypted_config_file = File.join(temp_dir, "#{config_filename}.encrypted")
273
299
 
@@ -315,11 +341,17 @@ module EasyAI
315
341
  end
316
342
 
317
343
  # 通过 JPS 登录获取用户名
318
- def self.get_username_from_jps
344
+ def self.get_username_from_jps(verbose = false)
319
345
  begin
320
- jps_login = Auth::JPSLogin.new
321
-
322
- if jps_login.login
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
+
354
+ if login_result
323
355
  username = jps_login.get_username
324
356
  if username && !username.empty?
325
357
  puts "👤 用户: #{username}"
@@ -329,18 +361,99 @@ module EasyAI
329
361
  rescue => e
330
362
  puts "❌ JPS 登录失败: #{e.message}"
331
363
  end
332
-
364
+
333
365
  return nil
334
366
  end
335
367
 
336
- def self.find_user_config(users, user_input_clean)
337
- users.each do |name, filename|
338
- if name.downcase == user_input_clean.downcase
339
- 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
340
401
  end
341
402
  end
403
+
342
404
  nil
343
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
344
457
 
345
458
  def self.load_config_file(config_file)
346
459
  begin
@@ -356,7 +469,7 @@ module EasyAI
356
469
  end
357
470
 
358
471
  def self.load_encrypted_config_file(encrypted_config_file)
359
- puts "正在解密配置文件..."
472
+ puts "正在解密配置文件..." if @@verbose
360
473
 
361
474
  # 使用新的密码验证机制
362
475
  result = get_and_validate_password(encrypted_config_file, "请输入解密密码: ")
@@ -373,7 +486,7 @@ module EasyAI
373
486
  end
374
487
 
375
488
  def self.parse_encrypted_index_file(encrypted_index_file)
376
- puts "正在解密 index.json..."
489
+ puts "正在解密 index.json..." if @@verbose
377
490
 
378
491
  # 使用新的密码验证机制
379
492
  result = get_and_validate_password(encrypted_index_file, "请输入解密密码: ")
@@ -472,7 +585,7 @@ module EasyAI
472
585
  stored_password = Base::SystemKeychain.get_stored_password
473
586
 
474
587
  if stored_password && !stored_password.empty?
475
- puts "使用系统存储的密码进行解密..."
588
+ puts "使用系统存储的密码进行解密..." if @@verbose
476
589
  return { password: stored_password, from_system: true }
477
590
  end
478
591
 
@@ -1,3 +1,3 @@
1
1
  module EasyAI
2
- VERSION = '1.1.1'
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.1
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