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 +4 -4
- data/lib/easyai/command/claude.rb +33 -25
- 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 +112 -25
- 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
|
@@ -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 = {
|
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 "
|
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 "
|
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
|
236
|
-
"#{uri.
|
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
|
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
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
364
|
-
|
365
|
-
|
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
|
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
|