easyai 1.7.0 → 2.1.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,210 +1,61 @@
1
- require 'fileutils'
2
- require 'tmpdir'
1
+ # frozen_string_literal: true
2
+
3
3
  require 'colored2'
4
+ require_relative '../command'
4
5
 
5
6
  module EasyAI
6
7
  class Command
8
+ # 通过 RubyGems 自更新 EasyAI 自身。
9
+ #
10
+ # 设计要点(详见 docs/设计文档.md §2.7):
11
+ # - 用 system('gem', 'update', 'easyai') 触发 RubyGems 自更新
12
+ # - 不用 exec,便于子进程结束后继续打印中文提示
13
+ # - 启动时 EasyAIApp 识别到子命令为 update 会跳过强制版本阻塞,避免死循环
7
14
  class Update < Command
8
- self.summary = '从代码仓库安装最新开发版本(仅用于测试)'
15
+ self.summary = '通过 RubyGems 更新 EasyAI 到最新发布版本'
9
16
  self.description = <<-DESC
10
- Gitee 代码仓库下载并安装最新的开发版本。
11
-
12
- 主要功能:
13
-
14
- * 从远程仓库拉取最新代码
15
-
16
- * 自动构建 gem 包
17
-
18
- * 安装到本地系统
19
-
20
- 使用示例:
17
+ 通过 `gem update easyai` 将 EasyAI 升级到 RubyGems 上的最新版本,
18
+ 等价于手动执行该命令,但用 system 而非 exec,子进程结束后会打印中文成功 / 失败提示。
21
19
 
22
- $ easyai update # 安装 master 分支
20
+ 如果当前与 RubyGems 网络不通导致启动时被强制版本检查阻塞,可临时跳过:
23
21
 
24
- $ easyai update --branch=dev # 安装指定分支
22
+ $ EASYAI_SKIP_FORCE_UPDATE=1 easyai <子命令>
25
23
 
26
- $ easyai update --keep-source # 保留源代码
24
+ 使用示例:
27
25
 
28
- $ easyai update --verbose # 显示详细输出
29
- DESC
26
+ $ easyai update # 静默模式,仅打印必要提示
27
+ $ easyai update --verbose # 显示 gem 命令的完整输出
28
+ DESC
30
29
 
31
30
  def self.options
32
31
  [
33
- ['--branch=BRANCH', '指定要安装的分支(默认: master)'],
34
- ['--keep-source', '保留下载的源代码'],
35
- ['--verbose', '显示详细输出']
32
+ ['--verbose', '显示 gem update 命令的完整输出']
36
33
  ].concat(super)
37
34
  end
38
35
 
39
- def initialize(argv)
40
- @branch = argv.option('branch') || 'master'
41
- @keep_source = argv.flag?('keep-source')
42
- @verbose = argv.flag?('verbose')
43
- super
44
- end
45
-
46
- def validate!
47
- super
48
- help! '未找到 git 命令。请先安装 git。' unless git_available?
49
- end
50
-
51
36
  def run
52
- puts "\n╔#{'═' * 58}╗".cyan
53
- puts "║#{'EasyAI 开发版本更新工具'.center(58)}║".cyan
54
- puts "╚#{'═' * 58}╝\n".cyan
55
-
56
- puts "⚠️ 注意:此命令仅用于测试开发版本".yellow
57
- puts "📦 生产环境请使用: #{'gem update easyai'.green}\n\n"
58
-
59
- # 确认继续
60
- unless confirm_update?
61
- puts "❌ 更新已取消".red
62
- return
63
- end
64
-
65
- # 创建临时目录
66
- temp_dir = Dir.mktmpdir('easyai-update-')
67
-
68
- begin
69
- # 克隆仓库
70
- clone_repository(temp_dir)
71
-
72
- # 执行安装脚本
73
- install_from_source(temp_dir)
74
-
75
- # 显示成功信息
76
- show_success_message
77
-
78
- rescue => e
79
- puts "\n❌ 更新失败: #{e.message}".red
80
- puts " 请检查网络连接或稍后重试".red
81
- exit 1
82
- ensure
83
- # 清理临时文件
84
- cleanup_temp_files(temp_dir) unless @keep_source
85
- end
86
- end
87
-
88
- private
89
-
90
- def git_available?
91
- system('which git > /dev/null 2>&1')
92
- end
93
-
94
- def confirm_update?
95
- print "确认从 Gitee 安装开发版本?[y/N]: ".yellow
96
- begin
97
- response = STDIN.gets
98
- return false unless response
99
- response = response.chomp.downcase
100
- response == 'y' || response == 'yes'
101
- rescue => e
102
- puts "\n❌ 无法读取输入: #{e.message}".red
103
- false
104
- end
105
- end
106
-
107
- def clone_repository(temp_dir)
108
- repo_url = 'https://gitee.com/goodtools/EasyAI.git'
109
- clone_path = File.join(temp_dir, 'EasyAI')
110
-
111
- puts "📥 正在下载源代码...".cyan
112
- puts " 仓库: #{repo_url}"
113
- puts " 分支: #{@branch}"
114
-
115
- # 构建 git clone 命令
116
- git_cmd = "git clone --depth 1 --branch #{@branch} #{repo_url} #{clone_path}"
117
- git_cmd += " > /dev/null 2>&1" unless @verbose
118
-
119
- # 执行克隆
120
- success = system(git_cmd)
121
-
122
- unless success
123
- raise "无法克隆仓库。请检查网络连接和分支名称。"
124
- end
125
-
126
- puts " ✓ 源代码下载完成".green
127
-
128
- clone_path
129
- end
130
-
131
- def install_from_source(temp_dir)
132
- source_dir = File.join(temp_dir, 'EasyAI')
133
-
134
- unless File.exist?(source_dir)
135
- raise "源代码目录不存在: #{source_dir}"
136
- end
137
-
138
- # 检查 test_local.sh 是否存在
139
- install_script = File.join(source_dir, 'test_local.sh')
140
- unless File.exist?(install_script)
141
- raise "安装脚本不存在: test_local.sh"
142
- end
143
-
144
- puts "\n🔧 正在构建并安装...".cyan
145
-
146
- # 切换到源代码目录并执行安装脚本
147
- Dir.chdir(source_dir) do
148
- # 确保脚本可执行
149
- File.chmod(0755, 'test_local.sh')
150
-
151
- # 执行安装脚本
152
- install_cmd = "./test_local.sh"
153
- install_cmd += " > /dev/null 2>&1" unless @verbose
154
-
155
- success = system(install_cmd)
156
-
157
- unless success
158
- raise "安装脚本执行失败"
159
- end
160
- end
161
-
162
- puts " ✓ 安装完成".green
163
- end
164
-
165
- def cleanup_temp_files(temp_dir)
166
- if @keep_source
167
- puts "\n📁 源代码保留在: #{temp_dir}".cyan
168
- else
169
- puts "\n🧹 正在清理临时文件...".cyan
170
- FileUtils.rm_rf(temp_dir)
171
- puts " ✓ 清理完成".green
172
- end
173
- end
174
-
175
- def show_success_message
176
- # 获取安装后的版本
177
- installed_version = get_installed_version
178
-
179
- puts "\n" + "─" * 60
180
- puts "✅ #{'开发版本安装成功!'.green}"
181
- puts "─" * 60
182
-
183
- if installed_version
184
- puts "📌 当前版本: #{installed_version}"
185
- end
186
-
187
- puts "\n提示:".cyan
188
- puts " • 这是开发版本,可能包含未发布的功能"
189
- puts " • 如需回退到稳定版本: gem install easyai"
190
- puts " • 查看版本信息: easyai --version"
191
-
192
- if @branch != 'master'
193
- puts "\n📝 已安装分支: #{@branch}".yellow
194
- end
195
- end
196
-
197
- def get_installed_version
198
- # 尝试获取已安装的版本
199
- output = `easyai --version 2>&1`
200
- if output =~ /EasyAI 版本:\s*(\S+)/
201
- $1
37
+ puts "开始更新 EasyAI...".cyan
38
+ puts "正在执行:gem update easyai".cyan
39
+
40
+ ok = if @verbose
41
+ system('gem', 'update', 'easyai')
42
+ else
43
+ system('gem', 'update', 'easyai', out: File::NULL, err: File::NULL)
44
+ end
45
+
46
+ if ok
47
+ puts "✓ 更新完成".green
48
+ puts "请重新打开终端,然后运行 `easyai --version` 查看新版本".cyan
202
49
  else
203
- nil
50
+ puts "✗ 更新失败".red
51
+ puts "请尝试以下排查方式:".yellow
52
+ puts " • 检查网络连接是否能访问 https://rubygems.org"
53
+ puts " • 确认 RubyGems 源可用:gem sources -l"
54
+ puts " • 权限不足时尝试:sudo gem update easyai"
55
+ puts " • 重新执行并加 --verbose 查看详细输出"
56
+ exit($?.respond_to?(:exitstatus) && $?.exitstatus ? $?.exitstatus : 1)
204
57
  end
205
- rescue
206
- nil
207
58
  end
208
59
  end
209
60
  end
210
- end
61
+ end
@@ -4,22 +4,18 @@ module EasyAI
4
4
  self.summary = '实用工具集合'
5
5
  self.description = <<-DESC
6
6
  提供各种实用工具。
7
-
7
+
8
8
  可用命令:
9
9
 
10
10
  * encry - 文件加密工具
11
11
 
12
12
  * decry - 文件解密工具
13
13
 
14
- * export - 导出 AI 配置信息
15
-
16
14
  使用示例:
17
15
 
18
16
  $ easyai utils encry file.txt # 加密文件
19
17
 
20
18
  $ easyai utils decry file.encrypted # 解密文件
21
-
22
- $ easyai utils export # 导出配置
23
19
  DESC
24
20
 
25
21
  self.abstract_command = true
@@ -33,5 +29,4 @@ end
33
29
 
34
30
  # 加载子命令
35
31
  require_relative 'utils/encry'
36
- require_relative 'utils/decry'
37
- require_relative 'utils/export'
32
+ require_relative 'utils/decry'
@@ -1,8 +1,6 @@
1
1
  require 'claide'
2
- require 'yaml'
3
2
  require 'fileutils'
4
3
  require 'colored2'
5
- require_relative 'base/system_keychain'
6
4
  require_relative 'base/version_checker'
7
5
 
8
6
  module EasyAI
@@ -11,7 +9,7 @@ module EasyAI
11
9
  self.abstract_command = true
12
10
  self.command = 'easyai'
13
11
  self.version = VERSION
14
- self.description = '简化 AI 命令行工具的 CLI 包装器,支持 Claude、Gemini 和 GPT'
12
+ self.description = '简化 AI 命令行工具的 CLI 包装器,支持 Claude、Gemini 和 Codex'
15
13
  self.plugin_prefixes = %w(easyai)
16
14
 
17
15
  # 定义帮助标志访问器(类似 pindo)
@@ -0,0 +1,190 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'fileutils'
5
+
6
+ module EasyAI
7
+ module Config
8
+ # 本地配置门面:~/.easyai/config.json 的唯一读写入口
9
+ module LocalConfig
10
+ SCHEMA_VERSION = '2.0.0'
11
+
12
+ class Error < StandardError; end
13
+ class NotFoundError < Error; end
14
+ class ParseError < Error; end
15
+ class IncompatibleVersionError < Error; end
16
+ class ToolNotConfiguredError < Error; end
17
+ class PlatformNotFoundError < Error; end
18
+
19
+ module_function
20
+
21
+ # 注意:必须每次动态 expand_path,避免常量在加载期 freeze 路径,
22
+ # 让 spec 中切换 ENV['HOME'] 后定位失败
23
+ def config_path
24
+ File.expand_path('~/.easyai/config.json')
25
+ end
26
+
27
+ def exists?
28
+ File.file?(config_path)
29
+ end
30
+
31
+ def load
32
+ path = config_path
33
+ raise NotFoundError, "未找到本地配置文件 #{path},请运行 easyai setup" unless File.file?(path)
34
+
35
+ raw = File.read(path)
36
+ data = begin
37
+ JSON.parse(raw)
38
+ rescue JSON::ParserError => e
39
+ raise ParseError,
40
+ "配置解析失败 #{path}: #{e.message};可运行 easyai setup --reset 重新生成"
41
+ end
42
+
43
+ version = data['version']
44
+ unless version == SCHEMA_VERSION
45
+ raise IncompatibleVersionError,
46
+ "配置版本 #{version.inspect} 不被支持,期望 #{SCHEMA_VERSION}"
47
+ end
48
+
49
+ data
50
+ end
51
+
52
+ def save(cfg)
53
+ path = config_path
54
+ FileUtils.mkdir_p(File.dirname(path))
55
+ File.write(path, JSON.pretty_generate(cfg))
56
+ File.chmod(0o600, path) unless Gem.win_platform?
57
+ path
58
+ end
59
+
60
+ PLATFORM_DISPLAY_NAMES = {
61
+ 'claude_official' => 'Claude 官方',
62
+ 'deepseek' => 'DeepSeek',
63
+ 'glm' => 'GLM(Coding Plan)',
64
+ 'kimi' => 'Kimi',
65
+ 'longcat' => 'LongCat(美团)',
66
+ 'minimax' => 'MiniMax',
67
+ 'openai_official' => 'ChatGPT Subscribe'
68
+ }.freeze
69
+
70
+ def platform_display_name(key)
71
+ PLATFORM_DISPLAY_NAMES.fetch(key, key)
72
+ end
73
+
74
+ # 已屏蔽的平台:保留模板与 config 数据,仅暂不对外提供(setup 不列出、runtime 不可选)。
75
+ # codex/deepseek:新版 codex 移除了 wire_api="chat",而 DeepSeek 暂无 Responses API
76
+ # (/responses 实测 404),接入即失败,故先屏蔽。待 DeepSeek 支持 Responses 或接入协议
77
+ # 转换代理后,从本表删除该项即可恢复(profile 模板与 KNOWN_PLATFORMS 条目均未删除)。
78
+ DISABLED_PLATFORMS = {
79
+ 'codex' => %w[deepseek]
80
+ }.freeze
81
+
82
+ def disabled_platform?(tool, platform)
83
+ Array(DISABLED_PLATFORMS[tool]).include?(platform)
84
+ end
85
+
86
+ def available_platforms(tool)
87
+ return [] unless exists?
88
+
89
+ cfg = load
90
+ platforms = cfg.dig(tool, 'platforms')
91
+ platforms.is_a?(Hash) ? platforms.keys : []
92
+ rescue Error
93
+ []
94
+ end
95
+
96
+ # 解析平台配置。选定平台后会 yield 平台名(如果给了 block),
97
+ # 让上层(AIToolBase)负责打印 UI 提示,保持 LocalConfig 的输出最小化。
98
+ # extra:调用方注入的虚拟平台 { key => data }(如 codex 的 ChatGPT Subscribe),
99
+ # 与 config.json 中真实平台合并参与选择;选中虚拟平台时返回其 data。
100
+ def resolve_platform(tool:, platform: nil, verbose: false, extra: {})
101
+ cfg = load
102
+ tool_cfg = cfg[tool]
103
+ platforms = tool_cfg.is_a?(Hash) ? tool_cfg['platforms'] : nil
104
+ platforms = {} unless platforms.is_a?(Hash)
105
+ extra = {} unless extra.is_a?(Hash)
106
+
107
+ # 显式指定了已屏蔽平台:直接拒绝(extra 虚拟平台不受屏蔽影响)
108
+ if platform && !platform.empty? && disabled_platform?(tool, platform) && !extra.key?(platform)
109
+ raise PlatformNotFoundError, "工具 #{tool} 的平台 #{platform} 已被屏蔽(暂不可用)"
110
+ end
111
+
112
+ # 屏蔽清单中的真实平台不参与选择(保留 config 数据,仅隐藏)
113
+ visible = platforms.reject { |name, _| disabled_platform?(tool, name) }
114
+ # 数据层:extra(虚拟平台,如 ChatGPT Subscribe)覆盖同名真实平台,保留其 marker
115
+ merged = visible.merge(extra)
116
+ if merged.empty?
117
+ raise ToolNotConfiguredError,
118
+ "工具 #{tool} 未配置任何平台,请运行 easyai setup --tool=#{tool}"
119
+ end
120
+
121
+ # 顺序层:extra 虚拟平台始终排在选择列表最前(如【ChatGPT Subscribe】恒为第一项)
122
+ available = extra.keys + (visible.keys - extra.keys)
123
+ selected =
124
+ if platform && !platform.empty?
125
+ unless available.include?(platform)
126
+ raise PlatformNotFoundError,
127
+ "工具 #{tool} 未配置平台 #{platform}(可用:#{available.join(', ')})"
128
+ end
129
+
130
+ platform
131
+ elsif available.size == 1
132
+ only = available.first
133
+ puts "使用平台 #{platform_display_name(only)}" if verbose
134
+ only
135
+ else
136
+ interactive_select(tool, available)
137
+ end
138
+
139
+ yield(platform_display_name(selected), selected) if block_given?
140
+ merged[selected]
141
+ end
142
+
143
+ def upsert_platform(tool, platform, data)
144
+ cfg = exists? ? load : { 'version' => SCHEMA_VERSION }
145
+ cfg['version'] ||= SCHEMA_VERSION
146
+ cfg[tool] ||= { 'platforms' => {} }
147
+ cfg[tool]['platforms'] ||= {}
148
+ cfg[tool]['platforms'][platform] = data
149
+ save(cfg)
150
+ cfg
151
+ end
152
+
153
+ def delete_platform(tool, platform)
154
+ return unless exists?
155
+
156
+ cfg = load
157
+ platforms = cfg.dig(tool, 'platforms')
158
+ return cfg unless platforms.is_a?(Hash) && platforms.key?(platform)
159
+
160
+ platforms.delete(platform)
161
+ cfg.delete(tool) if platforms.empty?
162
+ save(cfg)
163
+ cfg
164
+ end
165
+
166
+ # ----- private helpers -----
167
+
168
+ def interactive_select(tool, available)
169
+ puts "请选择平台(工具:#{tool}):"
170
+ available.each_with_index do |name, idx|
171
+ puts " #{idx + 1}) #{platform_display_name(name)}"
172
+ end
173
+ print '请输入序号 (q 取消): '
174
+ input = $stdin.gets
175
+ raise Interrupt, '用户取消平台选择' if input.nil?
176
+
177
+ input = input.chomp.strip
178
+ raise Interrupt, '用户取消平台选择' if input.empty? || input.casecmp('q').zero?
179
+
180
+ idx = Integer(input, 10) rescue nil
181
+ if idx.nil? || idx < 1 || idx > available.size
182
+ raise Interrupt, "非法输入:#{input}"
183
+ end
184
+
185
+ available[idx - 1]
186
+ end
187
+ private_class_method :interactive_select
188
+ end
189
+ end
190
+ end
@@ -1,3 +1,3 @@
1
1
  module EasyAI
2
- VERSION = '1.7.0'
2
+ VERSION = '2.1.0'
3
3
  end
data/lib/easyai.rb CHANGED
@@ -1,14 +1,15 @@
1
1
  require 'easyai/version'
2
- require 'easyai/config/config'
3
- require 'easyai/config/easyai_config'
2
+ require 'easyai/config/local_config'
4
3
  require 'easyai/command'
4
+ require 'easyai/command/ai_tool_base'
5
5
  require 'easyai/command/claude'
6
- require 'easyai/command/gemini'
7
- require 'easyai/command/gpt'
6
+ require 'easyai/command/codex'
8
7
  require 'easyai/command/utils'
9
8
  require 'easyai/command/clean'
10
9
  require 'easyai/command/update'
11
10
  require 'easyai/command/setup'
11
+ require 'easyai/command/backup'
12
+ require 'easyai/command/restore'
12
13
  require 'easyai/base/version_checker'
13
14
  require 'easyai/base/update_notifier'
14
15
  require 'easyai/base/system_info'
@@ -30,14 +31,8 @@ module EasyAI
30
31
  # 在运行命令前进行版本检查
31
32
  check_version_before_run(argv)
32
33
 
33
- # 设置退出时清理
34
- setup_cleanup_handler
35
-
36
34
  # 运行命令
37
35
  EasyAI::Command.run(argv)
38
- ensure
39
- # 确保清理临时文件
40
- cleanup_temp_files
41
36
  end
42
37
 
43
38
  private
@@ -64,21 +59,38 @@ module EasyAI
64
59
  require 'claide'
65
60
  require 'colored2'
66
61
  coerced_argv = CLAide::ARGV.coerce(argv)
67
-
62
+
63
+ # 识别第一个非选项位置参数(子命令名)
64
+ # 注意:CLAide::ARGV#arguments 返回剩余的位置参数列表
65
+ first_subcommand = coerced_argv.arguments.first
66
+
68
67
  # 如果是以下情况,跳过版本检查:
69
68
  # 1. 请求帮助信息 (--help)
70
69
  # 2. 显示版本信息 (--version)
71
70
  # 3. 主动检查更新 (--check-update)
72
- skip_check = coerced_argv.flag?('help') ||
73
- coerced_argv.flag?('version') ||
74
- coerced_argv.flag?('check-update')
75
-
71
+ # 4. 紧急跳过开关 (EASYAI_SKIP_FORCE_UPDATE=1)
72
+ # 5. 子命令为 update —— 避免"过期必须更新 → update 命令本身被阻塞"死循环
73
+ skip_reason =
74
+ if coerced_argv.flag?('help')
75
+ '--help'
76
+ elsif coerced_argv.flag?('version')
77
+ '--version'
78
+ elsif coerced_argv.flag?('check-update')
79
+ '--check-update'
80
+ elsif ENV['EASYAI_SKIP_FORCE_UPDATE']
81
+ 'EASYAI_SKIP_FORCE_UPDATE=1'
82
+ elsif first_subcommand == 'update'
83
+ '子命令为 update'
84
+ end
85
+
86
+ skip_check = !skip_reason.nil?
87
+
76
88
  if ENV['EASYAI_DEBUG']
77
89
  puts "📊 版本检查状态:".cyan
78
90
  puts " • 当前版本: v#{EasyAI::VERSION}".green
79
- puts " • 跳过检查: #{skip_check ? '是(--help/--version'.yellow : '否'.cyan}"
91
+ puts " • 跳过检查: #{skip_check ? "是(#{skip_reason}".yellow : '否'.cyan}"
80
92
  end
81
-
93
+
82
94
  return if skip_check
83
95
 
84
96
  # 强制更新检查(会阻塞执行)
@@ -97,24 +109,5 @@ module EasyAI
97
109
  puts e.backtrace.join("\n") if ENV['EASYAI_DEBUG']
98
110
  end
99
111
 
100
- # 设置清理处理器
101
- def setup_cleanup_handler
102
- # 注册信号处理器,确保 Ctrl+C 等信号时也能清理
103
- %w[INT TERM].each do |signal|
104
- Signal.trap(signal) do
105
- cleanup_temp_files
106
- exit(130) # 标准的 Ctrl+C 退出码
107
- end
108
- end
109
- end
110
-
111
- # 清理临时文件
112
- def cleanup_temp_files
113
- verbose = ENV['EASYAI_DEBUG'] || ENV['EASYAI_VERBOSE']
114
- EasyAIConfig.cleanup(verbose)
115
- rescue => e
116
- # 清理失败不影响退出
117
- puts "⚠️ 清理临时文件失败: #{e.message}" if ENV['EASYAI_DEBUG']
118
- end
119
112
  end
120
113
  end