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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '04579f0d2347a0226227a5d1385fa7e80bfc6e5e0fbad684c45541a768352665'
4
- data.tar.gz: df9f71c2a71f06cf3499b2920f4737cd97ebec66e7b2aab22a459f224836b80a
3
+ metadata.gz: 92482873797bd9a6936d1b493799244501b94440c155fe49e15b5aa889291ada
4
+ data.tar.gz: e3e619b6bd230d469dbb21fc2372e714813a504162e392680907f8fb3eec39aa
5
5
  SHA512:
6
- metadata.gz: 7fb5b852906e9a6fbf3d4ff07fbb27e30eba6d3c5fdc917b40965405473163feb72c44115e07353fd50e374f8ac0e2f6cecca0fc1978bf54da5246bd48c39678
7
- data.tar.gz: 3fec979042cf13ff735d66afbeac7180c873e9088c26c2e2c1eeb3ba6da48b570f264ff2441f7acfc1142fb21634c42caa1260088ca37ce697141d2d45d9156b
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
- - **主命令 (`lib/easyai.rb`)**:抽象基础命令,处理全局选项(`--setup`、`--config`)和配置管理
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
- - **基础模块 (`lib/easyai/base/`)**:核心功能模块,如文件加密 (`file_crypto.rb`) 和系统钥匙串 (`system_keychain.rb`)
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
- - 主命令通过 `self.version = VERSION`
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.7'
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'
@@ -14,7 +14,7 @@ module EasyAI
14
14
  class JPSLogin
15
15
  attr_reader :access_token, :username, :expires_at
16
16
 
17
- def initialize
17
+ def initialize(options = {})
18
18
  @client_id = "cli_a7bc7fe9b3d1d00b"
19
19
  @server_port = 8898
20
20
  @state = "client_login"
@@ -32,7 +32,10 @@ module EasyAI
32
32
  @access_token = nil
33
33
  @username = nil
34
34
  @expires_at = nil
35
-
35
+
36
+ # 从选项中获取 verbose 标志
37
+ @verbose = options[:verbose] || false
38
+
36
39
  # token 存储路径
37
40
  @token_dir = File.expand_path('~/.easyai')
38
41
  @token_file = File.join(@token_dir, '.jpstoken')
@@ -44,9 +47,14 @@ module EasyAI
44
47
  if load_stored_token && validate_token
45
48
  return true
46
49
  end
47
-
50
+
48
51
  puts "🔐 需要登录 JPS..."
49
- return authorize_and_login
52
+ result = authorize_and_login
53
+
54
+ # 如果用户主动取消,返回特殊标识
55
+ return :user_cancelled if result == :user_cancelled
56
+
57
+ return result
50
58
  end
51
59
 
52
60
  # 获取用户名(登录后可用)
@@ -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: #{authorization_uri}"
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
- puts "自动获取授权码失败,您可以手动输入:"
97
- puts "1. 授权码 (直接复制 'code=' 后面的内容)"
98
- puts "2. 完整回调 URL"
99
- print "> "
100
- input = STDIN.gets.chomp
101
-
102
- if input.start_with?("http")
103
- # 尝试从 URL 中提取 code
118
+ loop do
119
+ puts "\n自动获取授权码失败,请选择:"
120
+ puts "1. 输入授权码 (直接复制 'code=' 后面的内容)"
121
+ puts "2. 输入完整回调 URL"
122
+ puts "3. 重新打开授权网页"
123
+ puts "4. 退出"
124
+ print "> "
125
+
104
126
  begin
105
- uri = URI(input)
106
- query_params = URI.decode_www_form(uri.query || '').to_h
107
- code = query_params['code']
108
- puts "从 URL 中成功提取授权码" if code
109
- rescue => e
110
- puts "无法从 URL 中提取授权码: #{e.message}"
127
+ choice = STDIN.gets&.chomp
128
+
129
+ # 处理 Ctrl+C 中断
130
+ if choice.nil?
131
+ puts "\n用户中断操作"
132
+ return :user_cancelled
133
+ end
134
+
135
+ case choice
136
+ when "1"
137
+ puts "请输入授权码:"
138
+ print "> "
139
+ code_input = STDIN.gets&.chomp
140
+ if code_input.nil?
141
+ puts "用户中断操作"
142
+ return :user_cancelled
143
+ elsif !code_input.empty?
144
+ code = code_input
145
+ break
146
+ else
147
+ puts "授权码不能为空,请重新选择"
148
+ end
149
+ when "2"
150
+ puts "请输入完整回调 URL:"
151
+ print "> "
152
+ url_input = STDIN.gets&.chomp
153
+ if url_input.nil?
154
+ puts "用户中断操作"
155
+ return :user_cancelled
156
+ elsif url_input.start_with?("http")
157
+ # 尝试从 URL 中提取 code
158
+ begin
159
+ uri = URI(url_input)
160
+ query_params = URI.decode_www_form(uri.query || '').to_h
161
+ code = query_params['code']
162
+ if code
163
+ puts "✓ 从 URL 中成功提取授权码"
164
+ break
165
+ else
166
+ puts "✗ URL 中没有找到授权码,请重新选择"
167
+ end
168
+ rescue => e
169
+ puts "✗ 无法从 URL 中提取授权码: #{e.message}"
170
+ end
171
+ else
172
+ puts "✗ 无效的 URL 格式,请重新选择"
173
+ end
174
+ when "3"
175
+ # 重新打开授权网页
176
+ puts "正在重新打开授权网页..."
177
+ open_browser(authorization_uri)
178
+ # 重新启动服务器尝试获取授权码
179
+ code = start_callback_server
180
+ if code
181
+ break
182
+ end
183
+ # 如果还是失败,继续循环让用户选择
184
+ when "4"
185
+ puts "已退出授权流程"
186
+ return :user_cancelled
187
+ else
188
+ puts "无效的选择,请输入 1-4 之间的数字"
189
+ end
190
+ rescue Interrupt
191
+ puts "\n\n用户中断操作"
192
+ return :user_cancelled
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
- system("start", url)
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
- Logger: WEBrick::Log.new("/dev/null"),
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 2>&1"
144
- return nil unless system(check_cmd)
145
-
146
- # 使用 PowerShell 获取密码(简化版本,兼容性更好)
147
- ps_script = <<~PS
148
- try {
149
- $credential = Get-StoredCredential -Target "#{target_name}" -ErrorAction Stop
150
- $ptr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($credential.Password)
151
- $password = [Runtime.InteropServices.Marshal]::PtrToStringAuto($ptr)
152
- [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($ptr)
153
- Write-Output $password
154
- } catch {
155
- # 如果 Get-StoredCredential 不可用,尝试使用原生方法
156
- $output = & cmdkey /list:"#{target_name}" 2>$null
157
- if ($output -match "User: #{ACCOUNT_NAME}") {
158
- Write-Output "CREDENTIAL_EXISTS_BUT_CANNOT_RETRIEVE"
159
- }
160
- }
161
- PS
162
-
163
- password = `powershell -Command "#{ps_script}" 2>nul`.chomp
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("未找到有效配置")
@@ -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
- if selected_user.nil? || selected_user.strip.empty?
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
- if selected_user.nil? || selected_user.strip.empty?
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
- if jps_login.login
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
 
@@ -1,3 +1,3 @@
1
1
  module EasyAI
2
- VERSION = '1.1.0'
2
+ VERSION = '1.1.3'
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: easyai
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
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.7'
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.7'
53
+ version: '1.9'
54
54
  - !ruby/object:Gem::Dependency
55
55
  name: bundler
56
56
  requirement: !ruby/object:Gem::Requirement