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 +4 -4
- data/lib/easyai/auth/jpslogin.rb +106 -37
- data/lib/easyai/command/claude.rb +41 -26
- data/lib/easyai/command/clean.rb +27 -30
- data/lib/easyai/command/gpt.rb +215 -11
- data/lib/easyai/command/utils/export.rb +262 -0
- data/lib/easyai/command/utils.rb +6 -1
- data/lib/easyai/config/config.rb +153 -40
- data/lib/easyai/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a805223b3184cf7726f32044944ac33f5dc54e2058e2908d6423e8b846ae0483
|
4
|
+
data.tar.gz: 832ea4df5a956690ec3bedc93d995130e6a9ba23d01e684ec8d247e46a1382e1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c88789162eab4d2502697a5533e6d077ca595dc50fd25a9d7b5743c4457cd55dcce0d3ade4c383986937c4a17b2e37db14bc50c8d124fd5ecd429a8ab44aa6d7
|
7
|
+
data.tar.gz: f718001928ab81c0fb4e9c245deb6f93ef7518e7407fcf9621301e8d3466beb3fb6dbbef6adc3c1d20f4d6c109023521b357550d3635af0831a2e3d47a2ac2f9
|
data/lib/easyai/auth/jpslogin.rb
CHANGED
@@ -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
|
-
|
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
|
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
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
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
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
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 = {
|
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 "
|
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 "
|
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
|
229
|
-
"#{uri.
|
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
|
data/lib/easyai/command/clean.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
338
|
-
|
339
|
+
"Claude Code",
|
340
|
+
"Claude-API",
|
341
|
+
"claude-api",
|
342
|
+
"claude-credentials",
|
343
|
+
"claude",
|
344
|
+
"anthropic-claude",
|
345
|
+
"anthropic",
|
339
346
|
]
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
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
|
-
|
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
|
|
data/lib/easyai/command/gpt.rb
CHANGED
@@ -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
|
-
*
|
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
|
-
|
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
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
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
|
data/lib/easyai/command/utils.rb
CHANGED
@@ -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'
|
data/lib/easyai/config/config.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
338
|
-
|
339
|
-
|
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
|
|
data/lib/easyai/version.rb
CHANGED
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.
|
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
|