easyai 1.7.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,283 +0,0 @@
1
- require 'rbconfig'
2
- require 'fileutils'
3
- require_relative 'system_info'
4
-
5
- module EasyAI
6
- module Base
7
- module SystemKeychain
8
- SERVICE_NAME = "easyai"
9
- ACCOUNT_NAME = "config_password"
10
-
11
- # 跨平台密码存储
12
- def self.store_password(password)
13
- case SystemInfo.platform
14
- when :macos
15
- store_password_macos(password)
16
- when :windows
17
- store_password_windows(password)
18
- when :linux
19
- store_password_linux(password)
20
- else
21
- puts "⚠ 当前平台不支持密码自动存储,将在会话中临时保存"
22
- false
23
- end
24
- end
25
-
26
- # 跨平台密码获取
27
- def self.get_stored_password
28
- case SystemInfo.platform
29
- when :macos
30
- get_password_macos
31
- when :windows
32
- get_password_windows
33
- when :linux
34
- get_password_linux
35
- else
36
- nil
37
- end
38
- end
39
-
40
- # 跨平台密码删除
41
- def self.delete_stored_password
42
- case SystemInfo.platform
43
- when :macos
44
- delete_password_macos
45
- when :windows
46
- delete_password_windows
47
- when :linux
48
- delete_password_linux
49
- else
50
- true
51
- end
52
- end
53
-
54
- # 检测操作系统平台(已迁移到 SystemInfo)
55
- def self.detect_platform
56
- SystemInfo.platform
57
- end
58
-
59
- private
60
-
61
- # macOS Keychain 密码存储
62
- def self.store_password_macos(password)
63
- # 检查 security 命令是否可用
64
- unless system('which security > /dev/null 2>&1')
65
- puts "⚠ macOS Keychain 不可用,将使用文件存储"
66
- return store_password_linux(password)
67
- end
68
-
69
- # 首先尝试更新现有条目
70
- update_cmd = "security add-generic-password -a '#{ACCOUNT_NAME}' -s '#{SERVICE_NAME}' -w '#{password}' -U 2>/dev/null"
71
- success = system(update_cmd)
72
-
73
- unless success
74
- # 如果更新失败,尝试添加新条目
75
- add_cmd = "security add-generic-password -a '#{ACCOUNT_NAME}' -s '#{SERVICE_NAME}' -w '#{password}' 2>/dev/null"
76
- success = system(add_cmd)
77
- end
78
-
79
- if success
80
- puts "✓ 密码已安全存储到 macOS Keychain"
81
- else
82
- puts "⚠ 无法存储密码到 macOS Keychain,尝试使用文件存储"
83
- return store_password_linux(password)
84
- end
85
-
86
- success
87
- rescue => e
88
- puts "⚠ Keychain 存储出错: #{e.message},将使用文件存储"
89
- store_password_linux(password)
90
- end
91
-
92
- # macOS Keychain 密码获取
93
- def self.get_password_macos
94
- # 检查 security 命令是否可用
95
- unless system('which security > /dev/null 2>&1')
96
- return get_password_linux
97
- end
98
-
99
- cmd = "security find-generic-password -a '#{ACCOUNT_NAME}' -s '#{SERVICE_NAME}' -w 2>/dev/null"
100
- password = `#{cmd}`.chomp
101
-
102
- if password.empty? || $?.exitstatus != 0
103
- # 如果 Keychain 中没有,尝试从文件读取
104
- return get_password_linux
105
- end
106
-
107
- password
108
- rescue => e
109
- get_password_linux
110
- end
111
-
112
- # macOS Keychain 密码删除
113
- def self.delete_password_macos
114
- success = false
115
-
116
- # 尝试从 Keychain 删除
117
- if system('which security > /dev/null 2>&1')
118
- cmd = "security delete-generic-password -a '#{ACCOUNT_NAME}' -s '#{SERVICE_NAME}' 2>/dev/null"
119
- success = system(cmd)
120
-
121
- if success
122
- puts "✓ 已从 macOS Keychain 删除存储的密码"
123
- end
124
- end
125
-
126
- # 同时尝试删除文件存储
127
- delete_password_linux
128
-
129
- success
130
- rescue => e
131
- delete_password_linux
132
- end
133
-
134
- # Windows 凭据管理器密码存储
135
- def self.store_password_windows(password)
136
- # 使用 cmdkey 命令存储密码
137
- target_name = "#{SERVICE_NAME}_#{ACCOUNT_NAME}"
138
- cmd = "cmdkey /add:#{target_name} /user:#{ACCOUNT_NAME} /pass:\"#{password}\""
139
-
140
- success = system(cmd + " >nul 2>&1")
141
-
142
- # 同时保存到文件作为备用
143
- keyring_dir = File.expand_path('~/.easyai/.keyring')
144
- FileUtils.mkdir_p(keyring_dir) unless Dir.exist?(keyring_dir)
145
-
146
- keyring_file = File.join(keyring_dir, 'windows_credential')
147
-
148
- begin
149
- require 'base64'
150
- encoded_password = Base64.strict_encode64(password)
151
- File.write(keyring_file, encoded_password)
152
- # Windows 不支持 Unix 权限模型,但文件默认仅当前用户可访问
153
- unless Gem.win_platform?
154
- File.chmod(0600, keyring_file) rescue nil
155
- end
156
- rescue => e
157
- # 忽略文件写入错误
158
- end
159
-
160
- if success
161
- puts "✓ 密码已安全存储到 Windows 凭据管理器"
162
- else
163
- puts "⚠ 无法存储密码到 Windows 凭据管理器"
164
- end
165
-
166
- success
167
- rescue => e
168
- puts "⚠ Windows 凭据存储出错: #{e.message}"
169
- false
170
- end
171
-
172
- # Windows 凭据管理器密码获取
173
- def self.get_password_windows
174
- target_name = "#{SERVICE_NAME}_#{ACCOUNT_NAME}"
175
-
176
- # 首先检查凭据是否存在
177
- check_cmd = "cmdkey /list:#{target_name} 2>nul"
178
- check_output = `#{check_cmd}`
179
- return nil unless check_output.include?(target_name)
180
-
181
- # 使用备用文件存储方式
182
- # 因为 Windows 凭据管理器的密码获取需要特殊权限或 CredentialManager 模块
183
- # 我们使用加密文件作为备用方案
184
- keyring_file = File.expand_path('~/.easyai/.keyring/windows_credential')
185
-
186
- if File.exist?(keyring_file)
187
- begin
188
- require 'base64'
189
- encoded_password = File.read(keyring_file).chomp
190
- Base64.strict_decode64(encoded_password)
191
- rescue => e
192
- nil
193
- end
194
- else
195
- # 提示用户重新输入密码
196
- nil
197
- end
198
- rescue => e
199
- nil
200
- end
201
-
202
- # Windows 凭据管理器密码删除
203
- def self.delete_password_windows
204
- target_name = "#{SERVICE_NAME}_#{ACCOUNT_NAME}"
205
- cmd = "cmdkey /delete:#{target_name}"
206
-
207
- success = system(cmd + " >nul 2>&1")
208
-
209
- # 同时删除备用文件
210
- keyring_file = File.expand_path('~/.easyai/.keyring/windows_credential')
211
- if File.exist?(keyring_file)
212
- File.delete(keyring_file) rescue nil
213
- end
214
-
215
- if success
216
- puts "✓ 已从 Windows 凭据管理器删除存储的密码"
217
- end
218
-
219
- success
220
- rescue => e
221
- false
222
- end
223
-
224
- # Linux 密码存储(使用文件加密存储作为备用方案)
225
- def self.store_password_linux(password)
226
- keyring_dir = File.expand_path('~/.easyai/.keyring')
227
- FileUtils.mkdir_p(keyring_dir) unless Dir.exist?(keyring_dir)
228
-
229
- keyring_file = File.join(keyring_dir, 'config_key')
230
-
231
- begin
232
- # 使用简单的 Base64 编码存储(在实际生产中应该使用更安全的方法)
233
- require 'base64'
234
- encoded_password = Base64.strict_encode64(password)
235
- File.write(keyring_file, encoded_password)
236
- # 设置文件权限(仅在支持的平台上)
237
- if !Gem.win_platform?
238
- begin
239
- File.chmod(0600, keyring_file)
240
- rescue => e
241
- # 忽略权限设置错误,某些文件系统可能不支持
242
- end
243
- end
244
-
245
- puts "✓ 密码已存储到本地加密文件"
246
- true
247
- rescue => e
248
- puts "⚠ Linux 密码存储出错: #{e.message}"
249
- false
250
- end
251
- end
252
-
253
- # Linux 密码获取
254
- def self.get_password_linux
255
- keyring_file = File.expand_path('~/.easyai/.keyring/config_key')
256
- return nil unless File.exist?(keyring_file)
257
-
258
- begin
259
- require 'base64'
260
- encoded_password = File.read(keyring_file).chomp
261
- Base64.strict_decode64(encoded_password)
262
- rescue => e
263
- nil
264
- end
265
- end
266
-
267
- # Linux 密码删除
268
- def self.delete_password_linux
269
- keyring_file = File.expand_path('~/.easyai/.keyring/config_key')
270
-
271
- if File.exist?(keyring_file)
272
- File.delete(keyring_file)
273
- puts "✓ 已删除本地存储的密码"
274
- true
275
- else
276
- true
277
- end
278
- rescue => e
279
- false
280
- end
281
- end
282
- end
283
- end
@@ -1,259 +0,0 @@
1
- require 'json'
2
- require 'uri'
3
- require 'colored2'
4
- require_relative '../config/config'
5
- require_relative '../base/system_info'
6
-
7
- module EasyAI
8
- class Command
9
- class GPT < Command
10
- self.summary = '运行 OpenAI CLI'
11
- self.description = <<-DESC
12
- 启动 OpenAI GPT CLI 工具。
13
-
14
- 主要功能:
15
-
16
- * 支持远程配置下载
17
-
18
- * 自动配置 API 密钥
19
-
20
- * 支持本地配置文件
21
-
22
- * 透传所有参数
23
-
24
- 使用示例:
25
-
26
- $ easyai gpt # 启动 OpenAI CLI
27
-
28
- $ easyai gpt ./config.json # 使用本地配置文件
29
-
30
- $ easyai gpt api chat.completions # 调用 Chat API
31
-
32
- $ easyai gpt --help # 查看 OpenAI 帮助
33
-
34
- $ easyai gpt --verbose # 显示详细信息
35
-
36
- $ easyai gpt --no-keychain # 禁用密码存储
37
- DESC
38
-
39
- def self.options
40
- [
41
- ['--no-keychain', '禁用自动密码存储'],
42
- ['--verbose', '显示详细信息']
43
- ].concat(super)
44
- end
45
-
46
- def initialize(argv)
47
- @no_keychain = argv.flag?('no-keychain')
48
- @verbose_mode = argv.flag?('verbose')
49
-
50
- super
51
-
52
- # 获取剩余参数
53
- remaining_args = @argv.remainder!
54
-
55
- # 检查第一个参数是否是配置文件
56
- if remaining_args.first && File.exist?(remaining_args.first) && remaining_args.first.end_with?('.json')
57
- @config_file = remaining_args.shift
58
- end
59
-
60
- # 剩余的参数传递给 openai
61
- @gpt_args = remaining_args
62
- end
63
-
64
- def validate!
65
- super
66
- help! '未找到 OpenAI CLI。请安装:pip install openai' unless gpt_available?
67
- end
68
-
69
- def run
70
- begin
71
- # 首先尝试获取配置
72
- remote_config = nil
73
-
74
- if @config_file
75
- # 使用指定的本地配置文件
76
- print_status("📁 使用本地配置", File.basename(@config_file))
77
- remote_config = load_local_config(@config_file)
78
- print_success("配置加载成功") if remote_config
79
- else
80
- # 从远程下载配置,传递选项(指定工具类型为 gpt)
81
- print_status("🔄 获取远程配置", "默认用户")
82
- options = {
83
- no_keychain: @no_keychain,
84
- verbose: @verbose_mode,
85
- tool_type: "gpt" # 指定使用 gpt 组的配置
86
- }
87
- remote_config = ConfigManager.download_user_config(nil, options)
88
-
89
- # 处理用户取消授权的情况
90
- if remote_config == :user_cancelled
91
- print_error("用户取消了授权登录")
92
- exit 0
93
- end
94
-
95
- print_success("配置加载成功") if remote_config
96
- end
97
-
98
- # 如果远程配置获取失败,回退到本地配置
99
- if remote_config.nil?
100
- print_warning("使用本地配置")
101
- remote_config = load_local_yaml_config
102
-
103
- # 如果本地配置也为空,提示用户先进行设置
104
- if remote_config.empty?
105
- print_error("未找到有效配置")
106
- puts " 请先运行: #{'easyai --setup'.yellow}"
107
- exit 1
108
- end
109
- end
110
-
111
- # 构建环境变量
112
- env = build_environment(remote_config)
113
-
114
- # 运行 OpenAI CLI
115
- print_status("🚀 启动 OpenAI", "openai #{@gpt_args.join(' ')}")
116
- exec(env, 'openai', *@gpt_args)
117
-
118
- rescue Interrupt
119
- puts "\n已取消"
120
- exit 0
121
- rescue => e
122
- print_error("运行失败: #{e.message}")
123
- puts e.backtrace if @verbose_mode
124
- exit 1
125
- end
126
- end
127
-
128
- private
129
-
130
- def gpt_available?
131
- # 跨平台命令检测
132
- Base::SystemInfo.which_command('openai')
133
- end
134
-
135
- def load_local_config(config_file)
136
- begin
137
- content = File.read(config_file)
138
- JSON.parse(content)
139
- rescue JSON::ParserError => e
140
- print_error("配置文件格式错误: #{e.message}")
141
- nil
142
- rescue => e
143
- print_error("读取配置文件失败: #{e.message}")
144
- nil
145
- end
146
- end
147
-
148
- def load_local_yaml_config
149
- config_file = File.expand_path('~/.easyai/config.yml')
150
- return {} unless File.exist?(config_file)
151
-
152
- begin
153
- require 'yaml'
154
- YAML.load_file(config_file) || {}
155
- rescue => e
156
- print_error("读取本地配置失败: #{e.message}")
157
- {}
158
- end
159
- end
160
-
161
- def build_environment(config)
162
- env = ENV.to_h
163
-
164
- # 设置 OpenAI API 密钥
165
- if config["env"] && config["env"]["OPENAI_API_KEY"]
166
- env["OPENAI_API_KEY"] = config["env"]["OPENAI_API_KEY"]
167
-
168
- # 统一显示方式:不显示完整的 API 密钥
169
- print_status("🔑 API 密钥", mask_token(config["env"]["OPENAI_API_KEY"]))
170
-
171
- if @verbose_mode
172
- # verbose 模式:显示更多状态信息(但不显示敏感内容)
173
- puts " 密钥长度: #{config["env"]["OPENAI_API_KEY"].length} 字符"
174
- end
175
- end
176
-
177
- # 设置代理(如果配置中有)
178
- if config["gpt_proxy"]
179
- proxy_urls = []
180
-
181
- if config["gpt_proxy"]["HTTP_PROXY"]
182
- env["HTTP_PROXY"] = config["gpt_proxy"]["HTTP_PROXY"]
183
- env["http_proxy"] = config["gpt_proxy"]["HTTP_PROXY"]
184
- proxy_urls << config["gpt_proxy"]["HTTP_PROXY"]
185
- end
186
-
187
- if config["gpt_proxy"]["HTTPS_PROXY"]
188
- env["HTTPS_PROXY"] = config["gpt_proxy"]["HTTPS_PROXY"]
189
- env["https_proxy"] = config["gpt_proxy"]["HTTPS_PROXY"]
190
- proxy_urls << config["gpt_proxy"]["HTTPS_PROXY"] unless proxy_urls.include?(config["gpt_proxy"]["HTTPS_PROXY"])
191
- end
192
-
193
- if proxy_urls.any?
194
- # 统一显示方式:简化代理显示
195
- simplified_proxies = proxy_urls.map { |url| mask_url(url) }
196
- print_status("🌐 代理已配置", simplified_proxies.join(', '))
197
-
198
- end
199
- end
200
-
201
- env
202
- end
203
-
204
- def mask_token(token)
205
- return nil unless token
206
- return "空令牌" if token.empty?
207
-
208
- if token.length > 10
209
- "#{token[0..5]}...#{token[-4..-1]}"
210
- else
211
- "*" * token.length
212
- end
213
- end
214
-
215
- def mask_url(url)
216
- return url unless url
217
-
218
- begin
219
- uri = URI(url)
220
-
221
- # 隐藏IP地址的前两段,显示后两段
222
- masked_host = uri.host
223
- if uri.host =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/
224
- masked_host = "*.*.#{$3}.#{$4}"
225
- end
226
-
227
- if uri.password
228
- "#{uri.scheme}://***@#{masked_host}:#{uri.port}"
229
- else
230
- "#{uri.scheme}://#{masked_host}:#{uri.port}"
231
- end
232
- rescue URI::InvalidURIError
233
- url
234
- end
235
- end
236
-
237
- def print_status(icon_text, detail = nil)
238
- if detail
239
- icon_width = 5
240
- puts sprintf("%-#{icon_width}s %s", icon_text, detail.cyan)
241
- else
242
- puts icon_text
243
- end
244
- end
245
-
246
- def print_success(message)
247
- puts " ✓ #{message}".green
248
- end
249
-
250
- def print_warning(message)
251
- puts " ⚠️ #{message}".yellow
252
- end
253
-
254
- def print_error(message)
255
- puts "✗ #{message}".red
256
- end
257
- end
258
- end
259
- end