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,391 +1,106 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'json'
2
- require 'uri'
3
- require_relative '../config/config'
4
- require_relative '../auth/authclaude'
5
- require_relative '../base/update_notifier'
6
- require_relative '../base/system_info'
4
+ require 'fileutils'
5
+ require 'open3'
6
+ require_relative 'ai_tool_base'
7
7
 
8
8
  module EasyAI
9
9
  class Command
10
- class Claude < Command
11
- self.summary = '运行 Claude CLI'
10
+ class Claude < AIToolBase
11
+ self.summary = '启动 Claude Code 命令行(多平台支持)'
12
12
  self.description = <<-DESC
13
- 启动 Anthropic Claude CLI 工具。
14
-
15
- 主要功能:
16
-
17
- * 支持多种认证平台 (Claude/Kimi/Deepseek)
18
-
19
- * 智能选择最优认证方式
20
-
21
- * 支持远程配置下载
22
-
23
- * 自动配置环境变量
24
-
25
- * 支持本地配置文件
26
-
27
- 使用示例:
28
-
29
- $ easyai claude # 自动选择认证平台
30
-
31
- $ easyai claude --platform # 交互式选择认证平台
32
-
33
- $ easyai claude ./config.json # 使用本地配置文件
34
-
35
- $ easyai claude --verbose # 显示详细信息
36
-
37
- $ easyai claude --no-keychain # 禁用密码存储
38
- DESC
39
-
40
- def self.options
41
- [
42
- ['--platform', '选择认证平台(交互式选择)'],
43
- ['--no-keychain', '禁用自动密码存储'],
44
- ['--verbose', '显示详细信息']
45
- ].concat(super)
46
- end
47
-
48
- def initialize(argv)
49
- @platform_flag = argv.flag?('platform')
50
- @no_keychain = argv.flag?('no-keychain')
51
- @verbose_mode = argv.flag?('verbose')
52
-
53
- super
54
-
55
- # 获取剩余参数
56
- remaining_args = @argv.remainder!
57
-
58
- # 检查第一个参数是否是配置文件
59
- if remaining_args.first && File.exist?(remaining_args.first) && remaining_args.first.end_with?('.json')
60
- @config_file = remaining_args.shift
61
- end
62
-
63
- # 剩余的参数传递给 claude
64
- @claude_args = remaining_args
65
- end
66
-
67
- def validate!
68
- super
69
- help! '未找到 Claude CLI。请安装:npm install -g @anthropic-ai/claude-code' unless claude_available?
70
- end
71
-
72
- def run
73
- begin
74
-
75
- # 首先尝试从远程下载配置
76
- remote_config = nil
77
-
78
- if @config_file
79
- # 使用指定的本地配置文件
80
- print_status("📁 使用本地配置", File.basename(@config_file))
81
- remote_config = load_local_config(@config_file)
82
- print_success("配置加载成功") if remote_config
83
- else
84
- # 从远程下载配置,传递选项(指定工具类型为 claude)
85
- print_status("🔄 获取远程配置", "默认用户")
86
- options = {
87
- no_keychain: @no_keychain,
88
- verbose: @verbose_mode,
89
- tool_type: "claude", # 指定使用 claude 组的配置
90
- platform_flag: @platform_flag # 传递 platform 标志
91
- }
92
- remote_config = ConfigManager.download_user_config(nil, options)
93
-
94
- # 处理用户取消授权的情况
95
- if remote_config == :user_cancelled
96
- print_error("用户取消了授权登录")
97
- exit 0
98
- end
99
-
100
- print_success("配置加载成功") if remote_config
101
- end
102
-
103
- # 检查是否使用 claude_auth,如果是则进行环境检查
104
- if remote_config && remote_config['_config_path']&.include?('claude_auth')
105
- unless check_claude_auth_environment
106
- exit 1
107
- end
108
- end
109
-
110
- # 如果远程配置获取失败,回退到本地配置
111
- if remote_config.nil?
112
- print_warning("使用本地配置")
113
- remote_config = load_local_yaml_config
114
-
115
- # 如果本地配置也为空,提示用户先进行设置
116
- if remote_config.empty?
117
- print_error("未找到有效配置")
118
- puts " 请先运行: #{'easyai --setup'.yellow}"
119
- exit 1
120
- end
121
- end
122
-
123
- # 使用 Auth::AuthClaude 模块配置认证
124
- print_status("🔐 配置认证", "Claude OAuth")
125
- unless Auth::AuthClaude.configure(remote_config, nil)
126
- print_error("配置认证失败")
127
- exit 1
128
- end
129
- print_success("认证配置完成")
130
-
131
- # 构建环境变量
132
- env = build_environment(remote_config)
133
-
134
- # 导出环境变量到当前进程(不包括 CLAUDE_CODE_OAUTH_TOKEN)
135
- export_environment_variables(env)
136
-
137
- # 打印分隔线
138
- puts "─" * 60
139
- puts "🚀 #{'Claude CLI 已启动'.green}"
140
- puts "─" * 60
141
- puts
142
-
143
- # 启动 Claude
144
- exec(env, 'claude', *@claude_args)
145
-
146
- rescue => e
147
- print_error("Claude 命令执行失败: #{e.message}")
148
- puts " 请检查配置文件和网络连接"
149
- puts " 错误详情: #{e.class.name}" if @verbose_mode
150
- exit 1
151
- end
152
- end
13
+ 启动 Anthropic Claude Code CLI,从 ~/.easyai/config.json 读取平台配置,并将凭证 / 代理仅注入子进程。
153
14
 
154
- private
15
+ 使用示例:
155
16
 
17
+ $ easyai claude # 多平台时进入交互选择
18
+ $ easyai claude --platform=kimi # 直接使用指定平台
19
+ $ easyai claude ./adhoc.json # 用一次性 JSON 覆盖(单平台扁平 schema)
20
+ $ easyai claude -- --help # 透传参数给 claude CLI
21
+ DESC
156
22
 
157
- def print_status(icon_text, detail = nil)
158
- if detail
159
- puts "#{icon_text.ljust(20)} #{detail.cyan}"
160
- else
161
- puts icon_text
162
- end
163
- end
23
+ self.arguments = [
24
+ CLAide::Argument.new('CONFIG.json', false),
25
+ CLAide::Argument.new('ARGS', false, true)
26
+ ]
164
27
 
165
- def print_success(message)
166
- puts " ✓ #{message.green}"
167
- end
28
+ # Claude Code 默认禁用遥测与非必要流量;用户可在 config.json 的 env 中显式覆盖
29
+ DEFAULT_ENV = {
30
+ 'DISABLE_TELEMETRY' => '1',
31
+ 'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC' => '1'
32
+ }.freeze
168
33
 
169
- def print_warning(message)
170
- puts " ⚠ #{message.yellow}"
34
+ def tool_name
35
+ 'claude'
171
36
  end
172
37
 
173
- def print_error(message)
174
- puts " ✗ #{message.red}"
38
+ def exec_command
39
+ 'claude'
175
40
  end
176
41
 
177
- def claude_available?
178
- # 跨平台命令检测
179
- Base::SystemInfo.which_command('claude')
42
+ def install_hint
43
+ '未找到 claude CLI。请安装:npm install -g @anthropic-ai/claude-code'
180
44
  end
181
45
 
182
- # 检查 claude_auth 环境要求
183
- def check_claude_auth_environment
184
- check_details = Base::SystemInfo.claude_auth_check_details
185
-
186
- puts "\n🔍 系统环境检查(Claude 官方认证)"
187
- puts "─" * 60
188
-
189
- # 1. 操作系统检查
190
- if check_details[:os_supported]
191
- puts " ✓ 操作系统: #{check_details[:os_name]}".green
192
- else
193
- puts " ❌ 操作系统: #{check_details[:os_name]}(不支持)".red
194
- end
195
-
196
- # 2. 显示地区和语言信息
197
- region_info = Base::SystemInfo.region_info
198
- if Base::SystemInfo.macos?
199
- puts " 系统地区: #{region_info[:apple_locale]}"
200
- puts " 首选语言: #{region_info[:apple_languages]}"
201
- elsif Base::SystemInfo.windows?
202
- puts " 系统地区: #{region_info[:system_locale]}"
203
- puts " 用户地区: #{region_info[:user_locale]}"
204
- else
205
- puts " 系统语言: #{region_info[:locale]}"
206
- end
207
-
208
- # 3. 地区检查
209
- if check_details[:is_us_region]
210
- puts " ✓ 地区检查: 美国地区".green
211
- else
212
- puts " ❌ 地区检查: 非美国地区(#{check_details[:region]})".red
213
- end
214
-
215
- # 4. 语言检查
216
- if check_details[:is_english]
217
- puts " ✓ 语言检查: 英语".green
218
- else
219
- puts " ❌ 语言检查: 非英语语言".red
220
- end
221
-
222
- puts "─" * 60
223
-
224
- # 如果检查失败,显示错误信息和解决方案
225
- unless check_details[:meets_requirements]
226
- puts
227
- puts " ⛔ 环境检查未通过".red
228
- puts
229
-
230
- failure_reasons = []
231
- failure_reasons << "Claude 官方认证不支持 Windows 系统" unless check_details[:os_supported]
232
- failure_reasons << "Claude 官方认证要求美国地区" unless check_details[:is_us_region]
233
- failure_reasons << "Claude 官方认证要求英语语言环境" unless check_details[:is_english]
234
-
235
- puts " 失败原因:".yellow
236
- failure_reasons.each_with_index do |reason, index|
237
- puts " #{index + 1}. #{reason}".yellow
238
- end
239
-
240
- puts
241
- puts " 解决方案:".cyan
242
- puts " 1. 使用其他认证平台: easyai claude --platform".cyan
243
- puts " 2. 切换到 macOS/Linux 系统(如使用 Windows)".cyan unless check_details[:os_supported]
244
- puts " 3. 切换系统地区设置到美国(英语)".cyan if !check_details[:is_us_region] || !check_details[:is_english]
245
- puts " 4. 联系管理员获取其他认证方式".cyan
246
- puts
247
-
248
- return false
249
- end
250
-
251
- puts " ✅ 所有检查通过".green
252
- puts
253
-
254
- true
46
+ def default_env
47
+ DEFAULT_ENV
255
48
  end
256
49
 
257
- def load_local_config(config_path)
258
- return nil unless File.exist?(config_path)
259
-
260
- begin
261
- config_content = File.read(config_path)
262
- JSON.parse(config_content)
263
- rescue JSON::ParserError => e
264
- puts "解析本地配置文件失败: #{e.message}".red
265
- nil
266
- rescue => e
267
- puts "读取本地配置文件失败: #{e.message}".red
268
- nil
269
- end
270
- end
271
-
272
- def load_local_yaml_config
273
- # 不再使用本地YAML配置文件,返回空配置
274
- # claude命令主要依赖远程配置或用户指定的JSON配置文件
275
- {}
50
+ # 启动 claude 之前把 Claude Code 状态强制对齐到"纯 token 模式",避免 OAuth 残留影响:
51
+ # 1. ~/.claude.json: hasCompletedOnboarding=true(跳过 onboarding)
52
+ # 2. ~/.claude.json: 删除 oauthAccount 字段(OAuth 账户元数据残留)
53
+ # 3. macOS Keychain: 删除 "Claude Code-credentials" 条目(OAuth token 残留)
54
+ # 三步全部幂等:已是目标状态则不操作;任意 IO/JSON/Keychain 异常降级为 warning,不阻塞 exec。
55
+ #
56
+ # 副作用提醒:如果用户直接跑 `claude`(不通过 easyai)走过 OAuth 登录,下一次跑 `easyai claude`
57
+ # 会清掉 OAuth 状态。EasyAI 设计前提是统一走 ANTHROPIC_AUTH_TOKEN 环境变量路径,OAuth 登录
58
+ # 态由 `easyai backup claude` / `easyai restore claude` 显式管理。
59
+ def pre_exec
60
+ reset_claude_state_to_token_mode
61
+ delete_keychain_credential(KEYCHAIN_LABEL)
276
62
  end
277
63
 
278
- def build_environment(config)
279
- env = ENV.to_h
64
+ private
280
65
 
281
- puts "\n📋 环境配置:" if @verbose_mode
282
- puts "─" * 60 if @verbose_mode
66
+ CLAUDE_STATE_FILE = File.expand_path('~/.claude.json').freeze
67
+ KEYCHAIN_LABEL = 'Claude Code-credentials'.freeze
283
68
 
284
- # 从配置中提取所有环境变量 - 动态处理 env 中的所有变量
285
- if config && config['env']
286
- config['env'].each do |key, value|
287
- # 将所有环境变量都添加到 env 中
288
- env[key] = value.to_s
69
+ def reset_claude_state_to_token_mode
70
+ data = File.exist?(CLAUDE_STATE_FILE) ? JSON.parse(File.read(CLAUDE_STATE_FILE)) : {}
289
71
 
290
- # 根据变量名称智能显示
291
- if key.include?('TOKEN') || key.include?('KEY') || key.include?('SECRET') || key.include?('PASSWORD')
292
- # 敏感信息:显示部分内容
293
- value_preview = value.to_s.length > 15 ? "#{value.to_s[0..15]}..." : value.to_s
294
- print_status("🔑 #{key}", value_preview)
295
- puts " 长度: #{value.to_s.length} 字符" if @verbose_mode
296
- elsif key.include?('URL') || key.include?('ENDPOINT')
297
- # URL类:显示完整内容
298
- print_status("🌐 #{key}", value.to_s)
299
- elsif key.include?('MODEL')
300
- # 模型配置:显示完整内容
301
- print_status("🤖 #{key}", value.to_s)
302
- elsif key.include?('TIMEOUT')
303
- # 超时配置:显示完整内容
304
- print_status("⏱️ #{key}", value.to_s)
305
- elsif key.include?('PROXY')
306
- # 代理配置:显示部分内容
307
- print_status("🔄 #{key}", value.to_s)
308
- else
309
- # 其他变量:根据 verbose 模式决定是否显示
310
- print_status("📝 #{key}", value.to_s) if @verbose_mode
311
- end
312
- end
72
+ changed = false
73
+ if data['hasCompletedOnboarding'] != true
74
+ data['hasCompletedOnboarding'] = true
75
+ changed = true
313
76
  end
314
-
315
- # 从代理配置中提取代理设置
316
- if config && config['claude_proxy']
317
- proxy_urls = []
318
- if config['claude_proxy']['HTTP_PROXY']
319
- env['HTTP_PROXY'] = config['claude_proxy']['HTTP_PROXY']
320
- env['http_proxy'] = config['claude_proxy']['HTTP_PROXY']
321
- proxy_urls << config['claude_proxy']['HTTP_PROXY']
322
- end
323
- if config['claude_proxy']['HTTPS_PROXY'] && config['claude_proxy']['HTTPS_PROXY'] != config['claude_proxy']['HTTP_PROXY']
324
- env['HTTPS_PROXY'] = config['claude_proxy']['HTTPS_PROXY']
325
- env['https_proxy'] = config['claude_proxy']['HTTPS_PROXY']
326
- proxy_urls << config['claude_proxy']['HTTPS_PROXY']
327
- elsif config['claude_proxy']['HTTPS_PROXY']
328
- env['HTTPS_PROXY'] = config['claude_proxy']['HTTPS_PROXY']
329
- env['https_proxy'] = config['claude_proxy']['HTTPS_PROXY']
330
- end
331
-
332
- if proxy_urls.any?
333
- # 统一显示方式:简化代理显示,隐藏密码和部分IP
334
- simplified_proxies = proxy_urls.uniq.map do |url|
335
- uri = URI(url) rescue nil
336
- if uri
337
- # 隐藏IP地址的前两段,显示后两段
338
- masked_host = uri.host
339
- if uri.host =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/
340
- masked_host = "*.*.#{$3}.#{$4}"
341
- end
342
-
343
- if uri.password
344
- "#{uri.scheme}://***@#{masked_host}:#{uri.port}"
345
- else
346
- "#{uri.scheme}://#{masked_host}:#{uri.port}"
347
- end
348
- else
349
- url
350
- end
351
- end
352
- print_status("🌐 代理已配置", simplified_proxies.join(', '))
353
-
354
- end
77
+ if data.key?('oauthAccount')
78
+ data.delete('oauthAccount')
79
+ changed = true
355
80
  end
81
+ return unless changed
356
82
 
357
- puts "─" * 60 if @verbose_mode
358
- puts if @verbose_mode
359
-
360
- env
83
+ FileUtils.mkdir_p(File.dirname(CLAUDE_STATE_FILE))
84
+ File.write(CLAUDE_STATE_FILE, JSON.pretty_generate(data))
85
+ File.chmod(0o600, CLAUDE_STATE_FILE) unless Base::SystemInfo.windows?
86
+ rescue JSON::ParserError, Errno::EACCES, Errno::ENOENT => e
87
+ warn " ⚠ 跳过 ~/.claude.json 状态对齐(#{e.class}: #{e.message}),如首次接入第三方平台请手动设置 hasCompletedOnboarding=true 并删除 oauthAccount"
361
88
  end
362
89
 
90
+ def delete_keychain_credential(label)
91
+ return unless Base::SystemInfo.macos?
363
92
 
364
- def export_environment_variables(env)
365
- # 导出所有环境变量(仅在内存中,不写入文件)
366
- env_exported = false
367
- proxy_exported = false
368
-
369
- env.each do |key, value|
370
- # 跳过 PATH 等系统关键变量,避免覆盖
371
- next if ['PATH', 'HOME', 'USER', 'SHELL'].include?(key)
93
+ # 先 find 检查是否存在;不存在 → 幂等直接 return(避免 delete 时的错误日志)
94
+ _, _, find_status = Open3.capture3('security', 'find-generic-password', '-l', label)
95
+ return unless find_status.success?
372
96
 
373
- # 设置环境变量
374
- ENV[key] = value
97
+ _, delete_status = Open3.capture2e('security', 'delete-generic-password', '-l', label)
98
+ return if delete_status.success?
375
99
 
376
- # 记录导出状态
377
- if key.include?('PROXY')
378
- proxy_exported = true
379
- else
380
- env_exported = true
381
- end
382
- end
383
-
384
- print_success("环境变量已设置") if env_exported && @verbose_mode
385
- print_success("代理已设置到环境变量") if proxy_exported && @verbose_mode
100
+ warn " ⚠ 删除 Keychain 条目「#{label}」失败(exit=#{delete_status.exitstatus}),可能影响 token 模式纯净性"
101
+ rescue StandardError => e
102
+ warn " ⚠ 删除 Keychain 条目「#{label}」异常:#{e.message}"
386
103
  end
387
-
388
-
389
104
  end
390
105
  end
391
- end
106
+ end