easyai 1.1.0 → 1.1.3
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/CLAUDE.md +72 -6
- data/easyai.gemspec +1 -1
- data/lib/easyai/auth/jpslogin.rb +126 -39
- data/lib/easyai/base/system_keychain.rb +50 -32
- data/lib/easyai/command/claude.rb +9 -2
- data/lib/easyai/config/config.rb +41 -15
- data/lib/easyai/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 92482873797bd9a6936d1b493799244501b94440c155fe49e15b5aa889291ada
|
4
|
+
data.tar.gz: e3e619b6bd230d469dbb21fc2372e714813a504162e392680907f8fb3eec39aa
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1bb5fef07b98087e297e3db2a37f29abe34966771a58fca4c6297f7c29cb2e0136ef97cd7467d2bf8eff730bb2e693334776eeb241d7148ea94b12fdaa7f148e
|
7
|
+
data.tar.gz: 5a46f13a20d253320c6a039802cbd4caf93780bd5e1570680b51442722c8a42f9eb1b8fa607d4f8eb11ccb2fcf936764d2ad8ba70653a97af9eec2d010bd4b90
|
data/CLAUDE.md
CHANGED
@@ -12,10 +12,17 @@ EasyAI 是一个 Ruby CLI 工具,作为 AI 命令行工具(Claude、Gemini
|
|
12
12
|
|
13
13
|
代码库采用基于 CLAide 的模块化架构:
|
14
14
|
|
15
|
-
-
|
15
|
+
- **应用入口 (`lib/easyai.rb`)**:`EasyAIApp` 类负责启动标志显示、版本检查和命令执行
|
16
|
+
- **主命令 (`lib/easyai/command.rb`)**:抽象基础命令,处理全局选项和帮助信息
|
16
17
|
- **AI 工具子命令 (`lib/easyai/command/*.rb`)**:每个 AI 工具(claude、gemini、gpt)都有自己的命令类,继承自主命令
|
17
18
|
- **工具命令 (`lib/easyai/command/utils/`)**:实用工具集合,包括文件加密解密功能
|
18
|
-
-
|
19
|
+
- **认证模块 (`lib/easyai/auth/`)**:处理 AI 工具的认证配置,如 `authclaude.rb`
|
20
|
+
- **配置管理 (`lib/easyai/config/`)**:管理配置文件的加载、保存和远程下载
|
21
|
+
- **基础模块 (`lib/easyai/base/`)**:
|
22
|
+
- `file_crypto.rb`:文件加密解密核心实现
|
23
|
+
- `system_keychain.rb`:系统钥匙串访问
|
24
|
+
- `version_checker.rb`:版本检查和更新
|
25
|
+
- `update_notifier.rb`:更新通知显示
|
19
26
|
- **环境隔离**:使用 Ruby 的 `exec()` 替换当前进程为目标 AI 工具,在注入环境变量的同时保持交互性
|
20
27
|
- **跨平台支持**:处理 Unix(`which`)和 Windows(`where`)之间的命令检测差异
|
21
28
|
|
@@ -37,6 +44,10 @@ gem install easyai-x.x.x.gem # 安装特定版本
|
|
37
44
|
# 测试加密解密功能
|
38
45
|
EASYAI_TEST_PASSWORD=testdir123 easyai utils encry /tmp/test_dir
|
39
46
|
EASYAI_TEST_PASSWORD=testdir123 easyai utils decry /tmp/test_dir_encrypted
|
47
|
+
|
48
|
+
# 调试模式
|
49
|
+
EASYAI_DEBUG=1 ruby bin/easyai claude # 启用调试信息输出
|
50
|
+
EASYAI_SKIP_FORCE_UPDATE=1 easyai claude # 跳过版本更新检查
|
40
51
|
```
|
41
52
|
|
42
53
|
### 配置管理
|
@@ -48,6 +59,9 @@ easyai --setup
|
|
48
59
|
easyai --config
|
49
60
|
|
50
61
|
# 配置文件:~/.easyai/config.yml
|
62
|
+
|
63
|
+
# 使用本地配置文件
|
64
|
+
easyai claude ./config.json # 加载指定的 JSON 配置文件
|
51
65
|
```
|
52
66
|
|
53
67
|
### 工具命令
|
@@ -89,14 +103,42 @@ easyai clean # 清理临时文件和缓存
|
|
89
103
|
- **密码管理**:支持交互式密码输入和环境变量密码(`EASYAI_TEST_PASSWORD`)
|
90
104
|
- **错误处理**:完整的错误处理和用户友好提示
|
91
105
|
|
106
|
+
### 认证优先级机制
|
107
|
+
AI 工具的认证令牌获取优先级(`lib/easyai/auth/authclaude.rb`):
|
108
|
+
1. **远程配置**:首先尝试下载远程配置文件
|
109
|
+
2. **本地配置**:如果指定了本地 JSON 配置文件,加载该文件
|
110
|
+
3. **系统钥匙串**:从系统钥匙串获取(macOS Keychain)
|
111
|
+
4. **环境变量**:从环境变量读取(如 `CLAUDE_CODE_OAUTH_TOKEN`)
|
112
|
+
5. **交互输入**:最后提示用户输入并保存到配置文件
|
113
|
+
|
114
|
+
### 版本更新机制
|
115
|
+
项目包含智能版本管理系统 (`lib/easyai/base/version_checker.rb`):
|
116
|
+
- **启动检查**:在 `EasyAIApp#run` 中自动执行版本检查
|
117
|
+
- **强制更新**:`check_force_update!` 方法会阻塞执行直到检查完成
|
118
|
+
- **同步检查**:`check_sync` 方法复用已获取的版本信息
|
119
|
+
- **更新通知**:`UpdateNotifier` 模块负责显示更新提示
|
120
|
+
- **跳过机制**:
|
121
|
+
- 设置 `EASYAI_SKIP_FORCE_UPDATE=1` 环境变量跳过更新检查
|
122
|
+
- `--help`、`--version` 命令自动跳过更新检查
|
123
|
+
- **更新命令**:`easyai update` 执行 `gem update easyai`
|
124
|
+
|
92
125
|
## 版本管理
|
93
126
|
|
94
127
|
版本在 `lib/easyai/version.rb` 中定义,自动被以下文件引用:
|
95
|
-
- `easyai.gemspec` 通过 `require_relative`
|
96
|
-
-
|
97
|
-
-
|
128
|
+
- `easyai.gemspec` 通过 `require_relative 'lib/easyai/version'`
|
129
|
+
- `lib/easyai/command.rb` 通过 `self.version = VERSION`
|
130
|
+
- `lib/easyai.rb` 在启动标志中使用 `VERSION`
|
131
|
+
- 发布脚本 (`test_local.sh`, `release_remote.sh`) 通过正则提取版本号
|
98
132
|
|
99
|
-
只需更新 `lib/easyai/version.rb`
|
133
|
+
只需更新 `lib/easyai/version.rb` 中的 `VERSION` 常量 - 其他引用是自动的。
|
134
|
+
|
135
|
+
### 发布流程
|
136
|
+
`release_remote.sh` 脚本完成以下步骤:
|
137
|
+
1. 提交所有未提交的更改
|
138
|
+
2. 合并到 master 分支
|
139
|
+
3. 构建和安装 gem
|
140
|
+
4. 创建并推送 Git 标签(格式:v版本号)
|
141
|
+
5. 发布到 RubyGems
|
100
142
|
|
101
143
|
## 依赖管理
|
102
144
|
|
@@ -104,6 +146,7 @@ easyai clean # 清理临时文件和缓存
|
|
104
146
|
- **运行时依赖**:
|
105
147
|
- `claide ~> 1.0`:CLI 框架
|
106
148
|
- `colored2 ~> 3.1`:终端颜色输出
|
149
|
+
- `webrick ~> 1.9`:Web 服务器(用于版本检查,使用 1.9.1)
|
107
150
|
- **开发依赖**:
|
108
151
|
- `bundler ~> 2.0`:依赖管理
|
109
152
|
- `rake ~> 13.0`:构建工具
|
@@ -117,6 +160,8 @@ easyai clean # 清理临时文件和缓存
|
|
117
160
|
3. **测试流程**:使用 `./test_local.sh` 进行本地测试,确保功能正常后再发布
|
118
161
|
4. **跨平台兼容性**:添加新功能时要考虑 Windows、Linux、macOS 的兼容性
|
119
162
|
5. **环境隔离**:确保不影响用户的系统环境变量,所有配置都应在子进程中生效
|
163
|
+
6. **错误处理**:所有命令都应有完善的错误处理和用户友好的错误提示
|
164
|
+
7. **调试信息**:使用 `ENV['EASYAI_DEBUG']` 控制调试信息输出
|
120
165
|
|
121
166
|
## 中文化要求
|
122
167
|
|
@@ -183,6 +228,22 @@ DESC
|
|
183
228
|
|
184
229
|
## 技术实现注意事项
|
185
230
|
|
231
|
+
### 启动流程
|
232
|
+
1. **入口点**:`bin/easyai` 调用 `EasyAI::EasyAIApp.new.run(ARGV)`
|
233
|
+
2. **启动标志**:`EasyAIApp#show_banner` 显示版本信息(跳过 --help 和 --version)
|
234
|
+
3. **版本检查**:`EasyAIApp#check_version_before_run` 执行更新检查
|
235
|
+
- 强制更新检查:`Base::VersionChecker.check_force_update!`
|
236
|
+
- 同步检查:`Base::VersionChecker.check_sync`
|
237
|
+
- 更新通知:`Base::UpdateNotifier.maybe_show_notification`
|
238
|
+
4. **命令执行**:`EasyAI::Command.run(argv)` 处理实际命令
|
239
|
+
|
240
|
+
### 输出格式规范
|
241
|
+
各命令使用统一的输出格式(参考 `lib/easyai/command/claude.rb`):
|
242
|
+
- `print_status(icon_text, detail)`:状态信息,图标左对齐,详情用 cyan 色
|
243
|
+
- `print_success(message)`:成功信息,绿色勾号
|
244
|
+
- `print_warning(message)`:警告信息,黄色警告符
|
245
|
+
- `print_error(message)`:错误信息,红色叉号
|
246
|
+
|
186
247
|
### CLAide 框架相关
|
187
248
|
|
188
249
|
#### 1. --help 参数处理
|
@@ -233,6 +294,11 @@ end
|
|
233
294
|
- 父类的 `validate!` 会处理 help 标志
|
234
295
|
- 如果检测到 help 标志,会调用 `help!` 抛出异常,阻止 `run` 方法执行
|
235
296
|
|
297
|
+
#### 4. 抽象命令设置
|
298
|
+
使用 `abstract_command = true` 标记抽象命令:
|
299
|
+
- `EasyAI::Command` 是抽象基类
|
300
|
+
- `EasyAI::Command::Utils` 也是抽象命令,包含子命令 Encry 和 Decry
|
301
|
+
|
236
302
|
### 命令类继承结构
|
237
303
|
```
|
238
304
|
CLAide::Command
|
data/easyai.gemspec
CHANGED
@@ -20,7 +20,7 @@ Gem::Specification.new do |spec|
|
|
20
20
|
|
21
21
|
spec.add_dependency 'claide', '~> 1.0'
|
22
22
|
spec.add_dependency 'colored2', '~> 3.1'
|
23
|
-
spec.add_dependency 'webrick', '~> 1.
|
23
|
+
spec.add_dependency 'webrick', '~> 1.9'
|
24
24
|
|
25
25
|
spec.add_development_dependency 'bundler', '~> 2.0'
|
26
26
|
spec.add_development_dependency 'rake', '~> 13.0'
|
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
|
# 获取用户名(登录后可用)
|
@@ -78,13 +86,27 @@ module EasyAI
|
|
78
86
|
|
79
87
|
private
|
80
88
|
|
89
|
+
# 获取跨平台的 null 设备路径
|
90
|
+
def get_null_device
|
91
|
+
case RbConfig::CONFIG['host_os']
|
92
|
+
when /mswin|mingw|cygwin/
|
93
|
+
'NUL'
|
94
|
+
else
|
95
|
+
'/dev/null'
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
81
99
|
# 完整的授权和登录流程
|
82
100
|
def authorize_and_login
|
83
101
|
# 构建授权 URL
|
84
102
|
authorization_uri = build_authorization_uri
|
85
103
|
puts "正在打开浏览器进行飞书 OAuth 授权..."
|
86
|
-
puts "授权 URL
|
87
|
-
|
104
|
+
puts "\n授权 URL(如自动打开失败,请手动复制下面的链接到浏览器):"
|
105
|
+
puts "=" * 80
|
106
|
+
puts authorization_uri
|
107
|
+
puts "=" * 80
|
108
|
+
puts ""
|
109
|
+
|
88
110
|
# 在浏览器中打开授权 URL
|
89
111
|
open_browser(authorization_uri)
|
90
112
|
|
@@ -93,33 +115,88 @@ module EasyAI
|
|
93
115
|
|
94
116
|
# 如果自动获取失败,提示用户手动输入
|
95
117
|
if code.nil?
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
118
|
+
loop do
|
119
|
+
puts "\n自动获取授权码失败,请选择:"
|
120
|
+
puts "1. 输入授权码 (直接复制 'code=' 后面的内容)"
|
121
|
+
puts "2. 输入完整回调 URL"
|
122
|
+
puts "3. 重新打开授权网页"
|
123
|
+
puts "4. 退出"
|
124
|
+
print "> "
|
125
|
+
|
104
126
|
begin
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
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
|
111
193
|
end
|
112
|
-
else
|
113
|
-
# 将输入直接作为 code
|
114
|
-
code = input unless input.empty?
|
115
194
|
end
|
116
195
|
end
|
117
196
|
|
118
197
|
if code
|
119
|
-
puts "成功获取授权码!正在使用飞书身份登录 JPS..."
|
120
198
|
if exchange_code_for_token(code)
|
121
|
-
puts "✓ JPS
|
122
|
-
puts " 用户名: #{@username}"
|
199
|
+
puts "✓ JPS 登录成功!用户名: #{@username}"
|
123
200
|
store_token
|
124
201
|
return true
|
125
202
|
end
|
@@ -150,7 +227,8 @@ module EasyAI
|
|
150
227
|
def open_browser(url)
|
151
228
|
case RbConfig::CONFIG['host_os']
|
152
229
|
when /mswin|mingw|cygwin/
|
153
|
-
|
230
|
+
# Windows: 使用双引号包围URL避免参数解析问题
|
231
|
+
system("start \"\" \"#{url}\"")
|
154
232
|
when /darwin/
|
155
233
|
system("open", url)
|
156
234
|
when /linux|bsd/
|
@@ -170,19 +248,28 @@ module EasyAI
|
|
170
248
|
return nil
|
171
249
|
end
|
172
250
|
|
173
|
-
puts "启动本地服务器,监听端口 #{@server_port}..."
|
174
|
-
|
251
|
+
puts "启动本地服务器,监听端口 #{@server_port}..." if @verbose
|
252
|
+
puts "提示:按 Ctrl+C 可以中断并获得更多选择"
|
253
|
+
puts "🔄 正在使用飞书身份登录 JPS..."
|
254
|
+
|
175
255
|
begin
|
176
256
|
server = WEBrick::HTTPServer.new(
|
177
257
|
Port: @server_port,
|
178
|
-
|
258
|
+
BindAddress: '127.0.0.1',
|
259
|
+
Logger: WEBrick::Log.new(get_null_device),
|
179
260
|
AccessLog: []
|
180
261
|
)
|
181
262
|
rescue Errno::EADDRINUSE
|
182
263
|
puts "✗ 端口 #{@server_port} 仍被占用,无法启动服务器"
|
183
264
|
return nil
|
265
|
+
rescue Errno::ENOENT
|
266
|
+
puts "✗ 启动服务器失败: 系统找不到指定的路径或文件"
|
267
|
+
return nil
|
184
268
|
rescue => e
|
185
269
|
puts "✗ 启动服务器失败: #{e.message}"
|
270
|
+
if RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/
|
271
|
+
puts " 提示: 如果是Windows系统,请确保防火墙允许Ruby访问网络"
|
272
|
+
end
|
186
273
|
return nil
|
187
274
|
end
|
188
275
|
|
@@ -191,19 +278,19 @@ module EasyAI
|
|
191
278
|
begin
|
192
279
|
# 解析请求参数
|
193
280
|
query_params = URI.decode_www_form(req.query_string || '').to_h
|
194
|
-
puts "接收到回调请求,参数: #{query_params.inspect}"
|
281
|
+
puts "接收到回调请求,参数: #{query_params.inspect}" if @verbose
|
195
282
|
|
196
283
|
if query_params['error']
|
197
|
-
puts "授权错误: #{query_params['error']}"
|
284
|
+
puts "授权错误: #{query_params['error']}" if @verbose
|
198
285
|
res.content_type = "text/html; charset=UTF-8"
|
199
286
|
res.body = build_error_page(query_params['error'])
|
200
287
|
elsif query_params['code']
|
201
288
|
code = query_params['code']
|
202
|
-
puts "成功获取授权码"
|
289
|
+
puts "成功获取授权码" if @verbose
|
203
290
|
res.content_type = "text/html; charset=UTF-8"
|
204
291
|
res.body = build_success_page
|
205
292
|
else
|
206
|
-
puts "未获取到授权码"
|
293
|
+
puts "未获取到授权码" if @verbose
|
207
294
|
res.content_type = "text/html; charset=UTF-8"
|
208
295
|
res.body = build_error_page("未获取到授权码")
|
209
296
|
end
|
@@ -257,14 +344,14 @@ module EasyAI
|
|
257
344
|
request['Content-Type'] = 'application/json'
|
258
345
|
request.body = request_data.to_json
|
259
346
|
|
260
|
-
puts "正在请求 JPS API: #{@api_endpoint}"
|
347
|
+
puts "正在请求 JPS API: #{@api_endpoint}" if @verbose
|
261
348
|
response = http.request(request)
|
262
|
-
|
263
|
-
puts "API 响应状态码: #{response.code}"
|
349
|
+
|
350
|
+
puts "API 响应状态码: #{response.code}" if @verbose
|
264
351
|
|
265
352
|
if response.body
|
266
353
|
result = JSON.parse(response.body)
|
267
|
-
puts "API 响应: #{result.inspect}"
|
354
|
+
puts "API 响应: #{result.inspect}" if @verbose
|
268
355
|
|
269
356
|
if result['meta'] && result['meta']['code'] == 200 && result['data']
|
270
357
|
data = result['data']
|
@@ -304,7 +391,7 @@ module EasyAI
|
|
304
391
|
}
|
305
392
|
|
306
393
|
File.write(@token_file, token_data.to_json)
|
307
|
-
puts "✓ JPS Token 和用户名已存储到 #{@token_file}"
|
394
|
+
puts "✓ JPS Token 和用户名已存储到 #{@token_file}" if @verbose
|
308
395
|
rescue => e
|
309
396
|
puts "⚠ 存储 JPS Token 失败: #{e.message}"
|
310
397
|
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'rbconfig'
|
2
|
+
require 'fileutils'
|
2
3
|
|
3
4
|
module EasyAI
|
4
5
|
module Base
|
@@ -120,15 +121,30 @@ module EasyAI
|
|
120
121
|
# 使用 cmdkey 命令存储密码
|
121
122
|
target_name = "#{SERVICE_NAME}_#{ACCOUNT_NAME}"
|
122
123
|
cmd = "cmdkey /add:#{target_name} /user:#{ACCOUNT_NAME} /pass:\"#{password}\""
|
123
|
-
|
124
|
+
|
124
125
|
success = system(cmd + " >nul 2>&1")
|
125
|
-
|
126
|
+
|
127
|
+
# 同时保存到文件作为备用
|
128
|
+
keyring_dir = File.expand_path('~/.easyai/.keyring')
|
129
|
+
FileUtils.mkdir_p(keyring_dir) unless Dir.exist?(keyring_dir)
|
130
|
+
|
131
|
+
keyring_file = File.join(keyring_dir, 'windows_credential')
|
132
|
+
|
133
|
+
begin
|
134
|
+
require 'base64'
|
135
|
+
encoded_password = Base64.strict_encode64(password)
|
136
|
+
File.write(keyring_file, encoded_password)
|
137
|
+
File.chmod(0600, keyring_file) rescue nil # 尝试设置权限,Windows 可能不支持
|
138
|
+
rescue => e
|
139
|
+
# 忽略文件写入错误
|
140
|
+
end
|
141
|
+
|
126
142
|
if success
|
127
143
|
puts "✓ 密码已安全存储到 Windows 凭据管理器"
|
128
144
|
else
|
129
145
|
puts "⚠ 无法存储密码到 Windows 凭据管理器"
|
130
146
|
end
|
131
|
-
|
147
|
+
|
132
148
|
success
|
133
149
|
rescue => e
|
134
150
|
puts "⚠ Windows 凭据存储出错: #{e.message}"
|
@@ -138,33 +154,29 @@ module EasyAI
|
|
138
154
|
# Windows 凭据管理器密码获取
|
139
155
|
def self.get_password_windows
|
140
156
|
target_name = "#{SERVICE_NAME}_#{ACCOUNT_NAME}"
|
141
|
-
|
157
|
+
|
142
158
|
# 首先检查凭据是否存在
|
143
|
-
check_cmd = "cmdkey /list:#{target_name} >nul
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
# 如果无法获取密码但凭据存在,返回特殊标识
|
166
|
-
return nil if password.empty? || password == "CREDENTIAL_EXISTS_BUT_CANNOT_RETRIEVE"
|
167
|
-
password
|
159
|
+
check_cmd = "cmdkey /list:#{target_name} 2>nul"
|
160
|
+
check_output = `#{check_cmd}`
|
161
|
+
return nil unless check_output.include?(target_name)
|
162
|
+
|
163
|
+
# 使用备用文件存储方式
|
164
|
+
# 因为 Windows 凭据管理器的密码获取需要特殊权限或 CredentialManager 模块
|
165
|
+
# 我们使用加密文件作为备用方案
|
166
|
+
keyring_file = File.expand_path('~/.easyai/.keyring/windows_credential')
|
167
|
+
|
168
|
+
if File.exist?(keyring_file)
|
169
|
+
begin
|
170
|
+
require 'base64'
|
171
|
+
encoded_password = File.read(keyring_file).chomp
|
172
|
+
Base64.strict_decode64(encoded_password)
|
173
|
+
rescue => e
|
174
|
+
nil
|
175
|
+
end
|
176
|
+
else
|
177
|
+
# 提示用户重新输入密码
|
178
|
+
nil
|
179
|
+
end
|
168
180
|
rescue => e
|
169
181
|
nil
|
170
182
|
end
|
@@ -173,13 +185,19 @@ module EasyAI
|
|
173
185
|
def self.delete_password_windows
|
174
186
|
target_name = "#{SERVICE_NAME}_#{ACCOUNT_NAME}"
|
175
187
|
cmd = "cmdkey /delete:#{target_name}"
|
176
|
-
|
188
|
+
|
177
189
|
success = system(cmd + " >nul 2>&1")
|
178
|
-
|
190
|
+
|
191
|
+
# 同时删除备用文件
|
192
|
+
keyring_file = File.expand_path('~/.easyai/.keyring/windows_credential')
|
193
|
+
if File.exist?(keyring_file)
|
194
|
+
File.delete(keyring_file) rescue nil
|
195
|
+
end
|
196
|
+
|
179
197
|
if success
|
180
198
|
puts "✓ 已从 Windows 凭据管理器删除存储的密码"
|
181
199
|
end
|
182
|
-
|
200
|
+
|
183
201
|
success
|
184
202
|
rescue => e
|
185
203
|
false
|
@@ -72,8 +72,15 @@ module EasyAI
|
|
72
72
|
else
|
73
73
|
# 从远程下载配置,传递选项
|
74
74
|
print_status("🔄 获取远程配置", "默认用户")
|
75
|
-
options = { no_keychain: @no_keychain }
|
75
|
+
options = { no_keychain: @no_keychain, verbose: @verbose_mode }
|
76
76
|
remote_config = ConfigManager.download_user_config(nil, options)
|
77
|
+
|
78
|
+
# 处理用户取消授权的情况
|
79
|
+
if remote_config == :user_cancelled
|
80
|
+
print_error("用户取消了授权登录")
|
81
|
+
exit 0
|
82
|
+
end
|
83
|
+
|
77
84
|
print_success("配置加载成功") if remote_config
|
78
85
|
end
|
79
86
|
|
@@ -81,7 +88,7 @@ module EasyAI
|
|
81
88
|
if remote_config.nil?
|
82
89
|
print_warning("使用本地配置")
|
83
90
|
remote_config = load_local_yaml_config
|
84
|
-
|
91
|
+
|
85
92
|
# 如果本地配置也为空,提示用户先进行设置
|
86
93
|
if remote_config.empty?
|
87
94
|
print_error("未找到有效配置")
|
data/lib/easyai/config/config.rb
CHANGED
@@ -14,6 +14,7 @@ module EasyAI
|
|
14
14
|
|
15
15
|
# 类变量初始化
|
16
16
|
@@no_keychain = false
|
17
|
+
@@verbose = false
|
17
18
|
|
18
19
|
# 管理配置仓库:如果存在则更新,不存在则下载
|
19
20
|
def self.manage_config_repo
|
@@ -65,7 +66,7 @@ module EasyAI
|
|
65
66
|
|
66
67
|
# 下载配置仓库到固定位置
|
67
68
|
def self.download_config_repo
|
68
|
-
puts "正在下载配置仓库到 #{CONFIG_REPO_DIR}..."
|
69
|
+
puts "正在下载配置仓库到 #{CONFIG_REPO_DIR}..." if @@verbose
|
69
70
|
|
70
71
|
begin
|
71
72
|
# 克隆仓库到固定位置,捕获错误信息
|
@@ -94,8 +95,9 @@ module EasyAI
|
|
94
95
|
|
95
96
|
# 设置全局选项
|
96
97
|
@@no_keychain = options[:no_keychain] || false
|
98
|
+
@@verbose = options[:verbose] || false
|
97
99
|
|
98
|
-
puts "正在从本地配置仓库加载配置..."
|
100
|
+
puts "正在从本地配置仓库加载配置..." if @@verbose
|
99
101
|
|
100
102
|
begin
|
101
103
|
# 检查 index.json 文件,支持加密版本
|
@@ -134,8 +136,13 @@ module EasyAI
|
|
134
136
|
selected_user = user_name
|
135
137
|
else
|
136
138
|
# 统一使用 JPS 登录获取用户名
|
137
|
-
selected_user = get_username_from_jps
|
138
|
-
|
139
|
+
selected_user = get_username_from_jps(@@verbose)
|
140
|
+
|
141
|
+
# 处理用户取消的情况
|
142
|
+
if selected_user == :user_cancelled
|
143
|
+
puts "✗ 用户取消了授权登录"
|
144
|
+
return :user_cancelled
|
145
|
+
elsif selected_user.nil? || selected_user.strip.empty?
|
139
146
|
puts "✗ JPS 登录失败,无法获取用户名"
|
140
147
|
return nil
|
141
148
|
end
|
@@ -176,8 +183,14 @@ module EasyAI
|
|
176
183
|
def self.download_user_config(user_name = nil, options = {})
|
177
184
|
# 优先尝试使用本地配置仓库
|
178
185
|
config = load_from_local_repo(user_name, options)
|
186
|
+
|
187
|
+
# 如果用户取消了授权,返回特殊标识
|
188
|
+
if config == :user_cancelled
|
189
|
+
return :user_cancelled
|
190
|
+
end
|
191
|
+
|
179
192
|
return config if config
|
180
|
-
|
193
|
+
|
181
194
|
# 如果本地仓库失败,回退到临时下载方式
|
182
195
|
puts "本地配置仓库不可用,使用临时下载..."
|
183
196
|
download_user_config_temp(user_name, options)
|
@@ -189,6 +202,7 @@ module EasyAI
|
|
189
202
|
|
190
203
|
# 设置全局选项
|
191
204
|
@@no_keychain = options[:no_keychain] || false
|
205
|
+
@@verbose = options[:verbose] || false
|
192
206
|
|
193
207
|
begin
|
194
208
|
# 创建临时目录
|
@@ -248,8 +262,14 @@ module EasyAI
|
|
248
262
|
selected_user = user_name
|
249
263
|
else
|
250
264
|
# 统一使用 JPS 登录获取用户名
|
251
|
-
selected_user = get_username_from_jps
|
252
|
-
|
265
|
+
selected_user = get_username_from_jps(@@verbose)
|
266
|
+
|
267
|
+
# 处理用户取消的情况
|
268
|
+
if selected_user == :user_cancelled
|
269
|
+
puts "✗ 用户取消了授权登录"
|
270
|
+
cleanup_temp_dir(temp_dir)
|
271
|
+
return nil
|
272
|
+
elsif selected_user.nil? || selected_user.strip.empty?
|
253
273
|
puts "✗ JPS 登录失败,无法获取用户名"
|
254
274
|
cleanup_temp_dir(temp_dir)
|
255
275
|
return nil
|
@@ -315,11 +335,17 @@ module EasyAI
|
|
315
335
|
end
|
316
336
|
|
317
337
|
# 通过 JPS 登录获取用户名
|
318
|
-
def self.get_username_from_jps
|
338
|
+
def self.get_username_from_jps(verbose = false)
|
319
339
|
begin
|
320
|
-
jps_login = Auth::JPSLogin.new
|
321
|
-
|
322
|
-
|
340
|
+
jps_login = Auth::JPSLogin.new(verbose: verbose)
|
341
|
+
login_result = jps_login.login
|
342
|
+
|
343
|
+
# 处理用户主动取消的情况
|
344
|
+
if login_result == :user_cancelled
|
345
|
+
return :user_cancelled
|
346
|
+
end
|
347
|
+
|
348
|
+
if login_result
|
323
349
|
username = jps_login.get_username
|
324
350
|
if username && !username.empty?
|
325
351
|
puts "👤 用户: #{username}"
|
@@ -329,7 +355,7 @@ module EasyAI
|
|
329
355
|
rescue => e
|
330
356
|
puts "❌ JPS 登录失败: #{e.message}"
|
331
357
|
end
|
332
|
-
|
358
|
+
|
333
359
|
return nil
|
334
360
|
end
|
335
361
|
|
@@ -356,7 +382,7 @@ module EasyAI
|
|
356
382
|
end
|
357
383
|
|
358
384
|
def self.load_encrypted_config_file(encrypted_config_file)
|
359
|
-
puts "正在解密配置文件..."
|
385
|
+
puts "正在解密配置文件..." if @@verbose
|
360
386
|
|
361
387
|
# 使用新的密码验证机制
|
362
388
|
result = get_and_validate_password(encrypted_config_file, "请输入解密密码: ")
|
@@ -373,7 +399,7 @@ module EasyAI
|
|
373
399
|
end
|
374
400
|
|
375
401
|
def self.parse_encrypted_index_file(encrypted_index_file)
|
376
|
-
puts "正在解密 index.json..."
|
402
|
+
puts "正在解密 index.json..." if @@verbose
|
377
403
|
|
378
404
|
# 使用新的密码验证机制
|
379
405
|
result = get_and_validate_password(encrypted_index_file, "请输入解密密码: ")
|
@@ -472,7 +498,7 @@ module EasyAI
|
|
472
498
|
stored_password = Base::SystemKeychain.get_stored_password
|
473
499
|
|
474
500
|
if stored_password && !stored_password.empty?
|
475
|
-
puts "使用系统存储的密码进行解密..."
|
501
|
+
puts "使用系统存储的密码进行解密..." if @@verbose
|
476
502
|
return { password: stored_password, from_system: true }
|
477
503
|
end
|
478
504
|
|
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.1.
|
4
|
+
version: 1.1.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Wade
|
@@ -43,14 +43,14 @@ dependencies:
|
|
43
43
|
requirements:
|
44
44
|
- - "~>"
|
45
45
|
- !ruby/object:Gem::Version
|
46
|
-
version: '1.
|
46
|
+
version: '1.9'
|
47
47
|
type: :runtime
|
48
48
|
prerelease: false
|
49
49
|
version_requirements: !ruby/object:Gem::Requirement
|
50
50
|
requirements:
|
51
51
|
- - "~>"
|
52
52
|
- !ruby/object:Gem::Version
|
53
|
-
version: '1.
|
53
|
+
version: '1.9'
|
54
54
|
- !ruby/object:Gem::Dependency
|
55
55
|
name: bundler
|
56
56
|
requirement: !ruby/object:Gem::Requirement
|