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,494 +1,603 @@
1
- require_relative '../config/easyai_config'
2
- require_relative '../base/system_keychain'
3
- require 'colored2'
1
+ # frozen_string_literal: true
2
+
3
+ require 'io/console'
4
4
  require 'json'
5
- require 'open3'
5
+ require 'colored2'
6
+ require_relative '../command'
7
+ require_relative '../config/local_config'
8
+ require_relative '../base/secret_masker'
6
9
 
7
10
  module EasyAI
8
11
  class Command
12
+ # 交互式本地配置生成器
13
+ #
14
+ # 子命令矩阵:
15
+ # easyai setup # 全量交互(首次)/ 选择菜单 upsert(已存在)
16
+ # easyai setup --tool=claude # 仅配置指定工具
17
+ # easyai setup --add=kimi --tool=claude # 追加 / 覆盖单平台
18
+ # easyai setup --remove=kimi --tool=claude # 删除单平台
19
+ # easyai setup --list # 脱敏概览
20
+ # easyai setup --reset # 删除现有 config 后重走交互
21
+ # easyai setup --edit # 用 $EDITOR 直接编辑 config.json
9
22
  class Setup < Command
10
- self.summary = '初始化配置环境'
23
+ self.summary = '配置 EasyAI(写入 ~/.easyai/config.json)'
11
24
  self.description = <<-DESC
12
- 初始化 EasyAI 配置环境,自动下载并设置配置仓库。
13
-
14
- 主要功能:
15
- * 自动下载配置仓库
16
- * 验证配置完整性
17
- * 解密配置文件(如需要)
18
- * 显示可用用户列表
19
- * 管理密码存储
20
-
21
- 使用示例:
22
- $ easyai setup # 标准初始化
23
- $ easyai setup --force # 强制重新下载配置
24
- $ easyai setup --branch dev # 使用开发分支
25
- $ easyai setup --verify # 仅验证配置状态
26
- $ easyai setup --list-users # 列出可用用户
27
- DESC
25
+ 配置 EasyAI 的本地凭证 / 代理设置。配置文件位于 ~/.easyai/config.json。
26
+
27
+ 使用示例:
28
+
29
+ $ easyai setup # 首次:全量交互;已存在:upsert 选择菜单
30
+ $ easyai setup --tool=claude # 仅配置 claude
31
+ $ easyai setup --add=kimi --tool=claude # 追加或覆盖单个平台
32
+ $ easyai setup --remove=kimi --tool=claude # 删除单个平台
33
+ $ easyai setup --list # 脱敏方式打印当前配置概览
34
+ $ easyai setup --reset # 删除现有 config 后重走交互
35
+ $ easyai setup --edit # 用 $EDITOR 打开 config.json 直接编辑
36
+ DESC
37
+
38
+ # setup 内含的硬编码平台清单;runtime(AIToolBase)不读取此常量
39
+ KNOWN_PLATFORMS = {
40
+ 'claude' => {
41
+ 'claude_official' => {
42
+ label: 'Claude 官方',
43
+ required_env: %w[ANTHROPIC_AUTH_TOKEN],
44
+ optional_env: {
45
+ 'ANTHROPIC_BASE_URL' => 'https://api.anthropic.com'
46
+ }
47
+ },
48
+ 'kimi' => {
49
+ label: 'Kimi(Moonshot)',
50
+ required_env: %w[ANTHROPIC_AUTH_TOKEN],
51
+ optional_env: {
52
+ 'ANTHROPIC_BASE_URL' => 'https://api.kimi.com/coding/'
53
+ }
54
+ },
55
+ 'deepseek' => {
56
+ label: 'DeepSeek',
57
+ required_env: %w[ANTHROPIC_AUTH_TOKEN],
58
+ optional_env: {
59
+ 'ANTHROPIC_BASE_URL' => 'https://api.deepseek.com/anthropic',
60
+ 'ANTHROPIC_MODEL' => 'deepseek-v4-pro[1m]',
61
+ 'ANTHROPIC_DEFAULT_OPUS_MODEL' => 'deepseek-v4-pro[1m]',
62
+ 'ANTHROPIC_DEFAULT_SONNET_MODEL' => 'deepseek-v4-pro[1m]',
63
+ 'ANTHROPIC_DEFAULT_HAIKU_MODEL' => 'deepseek-v4-flash',
64
+ 'CLAUDE_CODE_SUBAGENT_MODEL' => 'deepseek-v4-flash',
65
+ 'CLAUDE_CODE_EFFORT_LEVEL' => 'max'
66
+ }
67
+ },
68
+ 'aliqwen' => {
69
+ label: '阿里千问 Coding Plan(兼容 Anthropic 协议)',
70
+ required_env: %w[ANTHROPIC_AUTH_TOKEN],
71
+ optional_env: {
72
+ 'ANTHROPIC_BASE_URL' => 'https://coding.dashscope.aliyuncs.com/apps/anthropic',
73
+ 'ANTHROPIC_MODEL' => 'qwen3.6-plus',
74
+ 'ANTHROPIC_DEFAULT_OPUS_MODEL' => 'qwen3.6-plus',
75
+ 'ANTHROPIC_DEFAULT_SONNET_MODEL' => 'qwen3.6-plus',
76
+ 'ANTHROPIC_DEFAULT_HAIKU_MODEL' => 'qwen3.6-plus',
77
+ 'ANTHROPIC_SMALL_FAST_MODEL' => 'qwen3.6-plus',
78
+ 'CLAUDE_CODE_SUBAGENT_MODEL' => 'qwen3.6-plus',
79
+ 'CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS' => '1'
80
+ }
81
+ },
82
+ 'minimax' => {
83
+ label: 'MiniMax(兼容 Anthropic 协议)',
84
+ required_env: %w[ANTHROPIC_AUTH_TOKEN],
85
+ optional_env: {
86
+ 'ANTHROPIC_BASE_URL' => 'https://api.minimaxi.com/anthropic',
87
+ 'ANTHROPIC_MODEL' => 'MiniMax-M2.7-highspeed',
88
+ 'ANTHROPIC_DEFAULT_SONNET_MODEL' => 'MiniMax-M2.7-highspeed',
89
+ 'ANTHROPIC_DEFAULT_HAIKU_MODEL' => 'MiniMax-M2.7-highspeed',
90
+ 'ANTHROPIC_DEFAULT_OPUS_MODEL' => 'MiniMax-M2.7-highspeed'
91
+ }
92
+ },
93
+ 'glm' => {
94
+ label: 'GLM Coding Plan(bigmodel.cn)',
95
+ required_env: %w[ANTHROPIC_AUTH_TOKEN],
96
+ optional_env: {
97
+ 'ANTHROPIC_BASE_URL' => 'https://open.bigmodel.cn/api/anthropic',
98
+ 'API_TIMEOUT_MS' => '3000000',
99
+ 'ANTHROPIC_DEFAULT_OPUS_MODEL' => 'glm-5.1',
100
+ 'ANTHROPIC_DEFAULT_SONNET_MODEL' => 'glm-5-turbo',
101
+ 'ANTHROPIC_DEFAULT_HAIKU_MODEL' => 'glm-4.5-air'
102
+ }
103
+ }
104
+ },
105
+ 'gemini' => {
106
+ 'google_official' => {
107
+ label: 'Google 官方',
108
+ required_env: %w[GEMINI_API_KEY],
109
+ optional_env: {}
110
+ }
111
+ },
112
+ 'codex' => {
113
+ 'openai_official' => {
114
+ label: 'OpenAI 官方',
115
+ required_env: %w[OPENAI_API_KEY],
116
+ optional_env: {
117
+ 'OPENAI_BASE_URL' => 'https://api.openai.com/v1'
118
+ }
119
+ },
120
+ 'azure_openai' => {
121
+ label: 'Azure OpenAI',
122
+ required_env: %w[AZURE_OPENAI_API_KEY AZURE_OPENAI_ENDPOINT],
123
+ optional_env: {}
124
+ },
125
+ 'deepseek' => {
126
+ label: 'DeepSeek(兼容 OpenAI 协议)',
127
+ required_env: %w[OPENAI_API_KEY],
128
+ optional_env: {
129
+ 'OPENAI_BASE_URL' => 'https://api.deepseek.com/v1'
130
+ }
131
+ }
132
+ }
133
+ }.freeze
134
+
135
+ # 敏感字段脱敏统一走 Base::SecretMasker,保留常量别名仅用于历史兼容
136
+ SENSITIVE_KEY_PATTERN = Base::SecretMasker::SENSITIVE_KEY_PATTERN
28
137
 
29
138
  def self.options
30
139
  [
31
- ['--force', '强制重新下载配置仓库'],
32
- ['--branch=BRANCH', '指定配置仓库分支 (默认: master)'],
33
- ['--verify', '仅验证现有配置,不进行修改'],
34
- ['--list-users', '列出所有可用用户'],
35
- ['--clean', '清理所有临时解密文件'],
36
- ['--no-password', '跳过密码设置'],
140
+ ['--tool=NAME', '指定要配置的工具(claude / gemini / codex)'],
141
+ ['--add=PLATFORM', '在指定工具下追加或覆盖单个平台'],
142
+ ['--remove=PLATFORM', '在指定工具下删除单个平台'],
143
+ ['--list', '脱敏方式展示当前所有配置'],
144
+ ['--reset', '删除现有 config.json 后重走全量交互'],
145
+ ['--edit', '使用 $EDITOR / $VISUAL 直接编辑 config.json']
37
146
  ].concat(super)
38
147
  end
39
148
 
40
149
  def initialize(argv)
41
- @force = argv.flag?('force')
42
- @branch = argv.option('branch') || 'master'
43
- @verify_only = argv.flag?('verify')
44
- @list_users = argv.flag?('list-users')
45
- @clean = argv.flag?('clean')
46
- @skip_password = argv.flag?('no-password')
150
+ @tool = argv.option('tool')
151
+ @add = argv.option('add')
152
+ @remove = argv.option('remove')
153
+ @list = argv.flag?('list')
154
+ @reset = argv.flag?('reset')
155
+ @edit = argv.flag?('edit')
47
156
  super
48
-
49
- @config_dir = File.expand_path('~/.easyai')
50
- @repo_dir = File.join(@config_dir, 'EasyAISetting')
51
157
  end
52
158
 
53
159
  def run
54
- show_welcome_banner
160
+ return run_list if @list
161
+ return run_remove if @remove
162
+ return run_add if @add
163
+ return run_reset if @reset
164
+ return run_edit if @edit
165
+
166
+ run_full_interactive
167
+ rescue Config::LocalConfig::ParseError, Config::LocalConfig::IncompatibleVersionError => e
168
+ print_error(e.message)
169
+ puts " 可运行: #{'easyai setup --reset'.yellow} 重新生成配置"
170
+ exit 1
171
+ rescue Interrupt
172
+ puts
173
+ print_error('用户取消操作')
174
+ exit 130
175
+ end
55
176
 
56
- if @verify_only
57
- verify_configuration
58
- return
59
- end
177
+ private
60
178
 
61
- if @clean
62
- cleanup_files
63
- return
64
- end
179
+ # ----- list -----
65
180
 
66
- if @list_users
67
- show_available_users
68
- return
181
+ def run_list
182
+ unless Config::LocalConfig.exists?
183
+ print_error("未找到本地配置 #{Config::LocalConfig.config_path}")
184
+ puts " 请运行: #{'easyai setup'.yellow}"
185
+ exit 1
69
186
  end
70
187
 
71
- # 执行主要设置流程
72
- perform_setup
73
-
74
- # 显示完成信息
75
- show_completion_info
76
- end
77
-
78
- private
79
-
80
- def show_welcome_banner
188
+ cfg = Config::LocalConfig.load
81
189
  puts
82
- puts "🚀 " + "EasyAI 配置环境初始化".green.bold
83
- puts "=" * 60
84
- puts
85
- end
190
+ puts "EasyAI 配置概览(#{Config::LocalConfig.config_path})".cyan.bold
191
+ puts '=' * 60
192
+ puts "schema 版本:#{cfg['version']}"
86
193
 
87
- def perform_setup
88
- step_counter = 0
194
+ printed_any = false
195
+ %w[claude gemini codex].each do |tool|
196
+ tool_cfg = cfg[tool]
197
+ next unless tool_cfg.is_a?(Hash)
89
198
 
90
- # 步骤1: 检查并创建目录
91
- step_counter += 1
92
- print_step(step_counter, "检查配置目录")
93
- ensure_directories
199
+ platforms = tool_cfg['platforms']
200
+ next unless platforms.is_a?(Hash) && !platforms.empty?
94
201
 
95
- # 步骤2: 下载或更新配置仓库
96
- step_counter += 1
97
- print_step(step_counter, "下载配置仓库")
98
- if @force || !repo_exists?
99
- download_repo
100
- else
101
- update_repo
202
+ printed_any = true
203
+ puts
204
+ puts "[#{tool}]".green.bold
205
+ platforms.each do |name, data|
206
+ puts " - #{name.cyan}"
207
+ print_platform_data(data)
208
+ end
102
209
  end
103
210
 
104
- # 步骤3: 处理加密文件
105
- step_counter += 1
106
- print_step(step_counter, "处理配置文件")
107
- handle_encrypted_files unless @skip_password
211
+ # 列出未列入三家但用户自定义的工具键
212
+ cfg.each do |key, value|
213
+ next if key == 'version'
214
+ next if %w[claude gemini codex].include?(key)
215
+ next unless value.is_a?(Hash) && value['platforms'].is_a?(Hash)
108
216
 
109
- # 步骤4: 验证配置
110
- step_counter += 1
111
- print_step(step_counter, "验证配置完整性")
112
- verify_basic_config
217
+ printed_any = true
218
+ puts
219
+ puts "[#{key}]".green.bold
220
+ value['platforms'].each do |name, data|
221
+ puts " - #{name.cyan}"
222
+ print_platform_data(data)
223
+ end
224
+ end
113
225
 
114
- # 步骤5: 显示可用用户
115
- step_counter += 1
116
- print_step(step_counter, "加载用户列表")
117
- show_available_users(compact: true)
226
+ puts ' (暂无任何平台配置)' unless printed_any
118
227
  end
119
228
 
120
- def print_step(number, description)
121
- puts "\n" + "步骤 #{number}:".cyan + " #{description}"
122
- puts "-" * 40
123
- end
229
+ def print_platform_data(data)
230
+ return unless data.is_a?(Hash)
124
231
 
125
- def ensure_directories
126
- unless Dir.exist?(@config_dir)
127
- FileUtils.mkdir_p(@config_dir)
128
- puts " ✓ 创建配置目录: #{@config_dir}"
232
+ env = data['env']
233
+ if env.is_a?(Hash) && !env.empty?
234
+ puts ' env:'
235
+ env.each do |k, v|
236
+ puts " #{k} = #{format_value(k, v)}"
237
+ end
129
238
  else
130
- puts " ✓ 配置目录已存在: #{@config_dir}"
239
+ puts ' env: (空)'
240
+ end
241
+
242
+ proxy = data['proxy']
243
+ if proxy.is_a?(Hash) && !proxy.empty?
244
+ puts ' proxy:'
245
+ proxy.each do |k, v|
246
+ puts " #{k} = #{v}"
247
+ end
131
248
  end
132
249
  end
133
250
 
134
- def repo_exists?
135
- Dir.exist?(@repo_dir) && Dir.exist?(File.join(@repo_dir, '.git'))
251
+ def format_value(key, value)
252
+ Base::SecretMasker.format_value(key, value)
136
253
  end
137
254
 
138
- def download_repo
139
- if repo_exists? && @force
140
- puts " 正在删除现有仓库..."
141
- FileUtils.rm_rf(@repo_dir)
142
- end
255
+ def sensitive_key?(key)
256
+ Base::SecretMasker.sensitive_key?(key)
257
+ end
143
258
 
144
- puts " 正在从 Gitee 下载配置仓库 (#{@branch} 分支)..."
259
+ def mask_sensitive(value)
260
+ Base::SecretMasker.mask(value)
261
+ end
145
262
 
146
- # Gitee 不需要代理,临时清除代理环境变量
147
- output = nil
148
- success = false
263
+ # ----- remove -----
149
264
 
150
- if Gem.win_platform?
151
- # Windows: 保存并清除环境变量
152
- old_env = {}
153
- ['HTTP_PROXY', 'HTTPS_PROXY', 'http_proxy', 'https_proxy'].each do |key|
154
- old_env[key] = ENV[key]
155
- ENV.delete(key)
156
- end
265
+ def run_remove
266
+ unless @tool && !@tool.empty?
267
+ print_error('--remove 必须配合 --tool=<name> 使用')
268
+ exit 1
269
+ end
157
270
 
158
- begin
159
- cmd = "git clone --depth 1 --branch #{@branch} #{EasyAIConfig::REPO_URL} #{@repo_dir} 2>&1"
160
- puts " 执行命令: #{cmd}" if ENV['EASYAI_DEBUG']
161
- output = `#{cmd}`
162
- success = $?.success?
163
- ensure
164
- # 恢复环境变量
165
- old_env.each { |k, v| ENV[k] = v if v }
166
- end
167
- else
168
- # Unix/Linux/macOS: 使用 env -u
169
- cmd = "env -u HTTP_PROXY -u HTTPS_PROXY -u http_proxy -u https_proxy git clone --depth 1 --branch #{@branch} #{EasyAIConfig::REPO_URL} #{@repo_dir} 2>&1"
170
- puts " 执行命令: #{cmd}" if ENV['EASYAI_DEBUG']
171
- output = `#{cmd}`
172
- success = $?.success?
271
+ unless Config::LocalConfig.exists?
272
+ print_error("未找到本地配置 #{Config::LocalConfig.config_path}")
273
+ puts " 请运行: #{'easyai setup'.yellow}"
274
+ exit 1
173
275
  end
174
276
 
175
- if success
176
- puts " ✓ 配置仓库下载成功"
177
- else
178
- puts " 下载失败: #{output}".red
277
+ available = Config::LocalConfig.available_platforms(@tool)
278
+ unless available.include?(@remove)
279
+ print_error("工具 #{@tool} 下不存在平台 #{@remove}")
280
+ puts " 当前可用:#{available.join(', ')}" unless available.empty?
179
281
  exit 1
180
282
  end
181
- end
182
283
 
183
- def update_repo
184
- puts " 检查配置仓库更新..."
185
-
186
- Dir.chdir(@repo_dir) do
187
- # 获取当前分支
188
- current_branch = `git branch --show-current`.chomp
189
-
190
- if current_branch != @branch
191
- puts " 切换到 #{@branch} 分支..."
192
- # 不使用代理
193
- if Gem.win_platform?
194
- # Windows: 临时清除环境变量
195
- old_env = {}
196
- ['HTTP_PROXY', 'HTTPS_PROXY', 'http_proxy', 'https_proxy'].each do |key|
197
- old_env[key] = ENV[key]
198
- ENV.delete(key)
199
- end
200
-
201
- begin
202
- system("git fetch origin #{@branch} > nul 2>&1")
203
- ensure
204
- old_env.each { |k, v| ENV[k] = v if v }
205
- end
206
- else
207
- system("env -u HTTP_PROXY -u HTTPS_PROXY -u http_proxy -u https_proxy git fetch origin #{@branch} > /dev/null 2>&1")
208
- end
209
- system("git checkout #{@branch} > /dev/null 2>&1")
210
- end
284
+ Config::LocalConfig.delete_platform(@tool, @remove)
285
+ print_success("已删除 #{@tool} / #{@remove}")
211
286
 
212
- # 拉取更新(不使用代理)
213
- output = nil
214
- if Gem.win_platform?
215
- # Windows: 临时清除环境变量
216
- old_env = {}
217
- ['HTTP_PROXY', 'HTTPS_PROXY', 'http_proxy', 'https_proxy'].each do |key|
218
- old_env[key] = ENV[key]
219
- ENV.delete(key)
220
- end
287
+ # 如果该 tool 已被一并清除,再做一次温和提示
288
+ unless Config::LocalConfig.available_platforms(@tool).any?
289
+ puts " 工具 #{@tool} 下已无任何平台,已从配置中移除该工具键。"
290
+ end
291
+ end
221
292
 
222
- begin
223
- output = `git pull --force 2>&1`
224
- ensure
225
- old_env.each { |k, v| ENV[k] = v if v }
226
- end
227
- else
228
- output = `env -u HTTP_PROXY -u HTTPS_PROXY -u http_proxy -u https_proxy git pull --force 2>&1`
229
- end
293
+ # ----- add -----
230
294
 
231
- if $?.success?
232
- if output.include?("Already up to date")
233
- puts " ✓ 配置已是最新版本"
234
- else
235
- puts " ✓ 配置已更新到最新版本"
236
- end
237
- else
238
- puts " ⚠️ 更新失败,使用现有配置"
239
- end
295
+ def run_add
296
+ unless @tool && !@tool.empty?
297
+ print_error('--add 必须配合 --tool=<name> 使用')
298
+ exit 1
240
299
  end
241
- end
242
300
 
243
- def handle_encrypted_files
244
- encrypted_files = Dir.glob(File.join(@repo_dir, '**/*.encrypted'))
301
+ tool_known = KNOWN_PLATFORMS[@tool]
302
+ unless tool_known
303
+ print_error("未支持的工具:#{@tool}")
304
+ puts " 可用工具:#{KNOWN_PLATFORMS.keys.join(', ')}"
305
+ exit 1
306
+ end
245
307
 
246
- if encrypted_files.empty?
247
- puts " 未发现加密文件"
248
- return
308
+ unless tool_known.key?(@add)
309
+ print_error("未知平台:#{@add}(不在 KNOWN_PLATFORMS[#{@tool}] 中)")
310
+ puts " 可用平台:#{tool_known.keys.join(', ')}"
311
+ puts ' 提示:如确需新增自定义平台,可使用 --edit 手工编辑 config.json'
312
+ exit 1
249
313
  end
250
314
 
251
- puts " 发现 #{encrypted_files.length} 个加密文件"
315
+ existing = Config::LocalConfig.available_platforms(@tool).include?(@add)
316
+ if existing
317
+ print_status('覆盖', "#{@tool} / #{@add}(已存在,将覆盖原有数据)")
318
+ else
319
+ print_status('新增', "#{@tool} / #{@add}")
320
+ end
252
321
 
253
- # 获取密码
254
- password = get_setup_password
255
- return unless password
322
+ data = collect_platform_data(@tool, @add)
323
+ Config::LocalConfig.upsert_platform(@tool, @add, data)
324
+ print_success("已写入 #{@tool} / #{@add}")
325
+ end
256
326
 
257
- # 测试密码
258
- if test_password(password, encrypted_files.first)
259
- puts " ✓ 密码验证成功"
327
+ # ----- reset -----
260
328
 
261
- # 保存到钥匙串
262
- if should_save_password?
263
- if Base::SystemKeychain.store_password(password)
264
- puts " ✓ 密码已保存到系统钥匙串"
265
- end
266
- end
329
+ def run_reset
330
+ path = Config::LocalConfig.config_path
331
+ if File.exist?(path)
332
+ File.delete(path)
333
+ print_success("已删除原配置文件 #{path}")
267
334
  else
268
- puts " 密码验证失败".red
269
- puts " 提示: 配置文件将在使用时解密"
335
+ print_status('提示', "原配置文件不存在 #{path},直接进入交互配置")
270
336
  end
337
+
338
+ run_full_interactive
271
339
  end
272
340
 
273
- def get_setup_password
274
- # 优先使用环境变量
275
- if ENV['EASYAI_TEST_PASSWORD']
276
- puts " 使用环境变量中的密码"
277
- return ENV['EASYAI_TEST_PASSWORD']
278
- end
341
+ # ----- edit -----
279
342
 
280
- # 检查钥匙串
281
- stored_password = Base::SystemKeychain.get_stored_password
282
- if stored_password && !stored_password.empty?
283
- puts " 使用系统钥匙串中的密码"
284
- return stored_password
343
+ def run_edit
344
+ editor = ENV['VISUAL'] || ENV['EDITOR']
345
+ if editor.nil? || editor.empty?
346
+ print_error('未设置 $VISUAL / $EDITOR 环境变量')
347
+ puts ' 请先 export EDITOR=vim 等并重试'
348
+ exit 1
285
349
  end
286
350
 
287
- # 提示用户输入
288
- puts " 配置文件已加密,请输入解密密码"
289
- print " 密码: "
351
+ path = Config::LocalConfig.config_path
352
+ unless File.exist?(path)
353
+ print_status('提示', "config 不存在,先创建空骨架:#{path}")
354
+ Config::LocalConfig.save('version' => Config::LocalConfig::SCHEMA_VERSION)
355
+ end
290
356
 
291
- begin
292
- require 'io/console'
293
- password = STDIN.noecho(&:gets)&.chomp
294
- puts
295
- password
296
- rescue
297
- nil
357
+ print_status('编辑', "#{editor} #{path}")
358
+ unless system(editor, path)
359
+ print_error("编辑器退出非零:#{editor}")
360
+ exit 1
298
361
  end
299
- end
300
362
 
301
- def test_password(password, test_file)
363
+ # 校验回写后是否仍可解析
302
364
  begin
303
- require_relative '../base/file_crypto'
304
- key = Base::FileCrypto.generate_key(password)
305
- content = File.read(test_file)
306
- Base::FileCrypto.aes_128_ecb_decrypt(key, content)
307
- true
308
- rescue
309
- false
365
+ Config::LocalConfig.load
366
+ print_success('配置文件解析通过')
367
+ rescue Config::LocalConfig::Error => e
368
+ print_error("配置解析失败:#{e.message}")
369
+ puts " 可运行: #{'easyai setup --reset'.yellow} 重新生成"
370
+ exit 1
310
371
  end
311
372
  end
312
373
 
313
- def should_save_password?
314
- print " 是否保存密码到系统钥匙串?(y/n): "
315
- response = STDIN.gets&.chomp&.downcase
316
- response == 'y' || response == 'yes'
317
- end
318
-
319
- def verify_basic_config
320
- required_files = [
321
- 'index.json.encrypted',
322
- 'jps_client_config.json.encrypted'
323
- ]
324
-
325
- missing_files = []
326
- required_files.each do |file|
327
- path = File.join(@repo_dir, file)
328
- if File.exist?(path)
329
- puts " ✓ #{file}"
330
- else
331
- missing_files << file
332
- puts " ✗ #{file} (缺失)".red
333
- end
334
- end
374
+ # ----- 全量交互 / upsert 选择菜单 -----
335
375
 
336
- if missing_files.empty?
337
- puts " ✓ 核心配置文件完整"
376
+ def run_full_interactive
377
+ if Config::LocalConfig.exists?
378
+ run_upsert_menu
338
379
  else
339
- puts " ⚠️ 缺少 #{missing_files.length} 个配置文件".yellow
380
+ run_first_time_setup
340
381
  end
341
382
  end
342
383
 
343
- def show_available_users(compact: false)
344
- # 初始化配置
345
- EasyAIConfig.initialize(verbose: false)
384
+ def run_first_time_setup
385
+ puts
386
+ puts 'EasyAI 首次配置向导'.green.bold
387
+ puts '=' * 60
346
388
 
347
- # 获取 index 配置
348
- index_config = EasyAIConfig.get_config('index', verbose: false)
389
+ tools = select_multi('请选择要配置的 AI 工具(用逗号分隔多个序号,回车默认全部)',
390
+ KNOWN_PLATFORMS.keys, allow_all_default: true)
349
391
 
350
- unless index_config
351
- puts " ✗ 无法加载用户配置".red
352
- return
392
+ tools.each do |tool|
393
+ puts
394
+ puts "[#{tool}]".green.bold
395
+ available = KNOWN_PLATFORMS[tool].keys
396
+ chosen = select_multi(
397
+ " 请选择 #{tool} 下要配置的平台(逗号分隔多个序号,回车默认全部)",
398
+ available, allow_all_default: true
399
+ )
400
+
401
+ chosen.each do |platform|
402
+ data = collect_platform_data(tool, platform)
403
+ Config::LocalConfig.upsert_platform(tool, platform, data)
404
+ print_success("已写入 #{tool} / #{platform}")
405
+ end
353
406
  end
354
407
 
355
- if compact
356
- user_count = count_users(index_config)
357
- puts " ✓ 发现 #{user_count} 个可用用户配置"
358
- else
359
- display_users_table(index_config)
360
- end
361
- rescue => e
362
- puts " ✗ 加载用户列表失败: #{e.message}".red
408
+ puts
409
+ run_list
363
410
  end
364
411
 
365
- def count_users(config)
366
- count = 0
367
-
368
- if config.is_a?(Hash)
369
- %w[claude gemini gpt].each do |tool|
370
- next unless config[tool]
412
+ def run_upsert_menu
413
+ if @tool && !@tool.empty?
414
+ tools = [@tool]
415
+ unless KNOWN_PLATFORMS.key?(@tool)
416
+ print_error("未支持的工具:#{@tool}")
417
+ puts " 可用工具:#{KNOWN_PLATFORMS.keys.join(', ')}"
418
+ exit 1
419
+ end
420
+ else
421
+ puts
422
+ puts 'EasyAI upsert 配置菜单'.green.bold
423
+ puts '=' * 60
424
+ tools = select_multi('请选择要更新的 AI 工具(逗号分隔多个序号)',
425
+ KNOWN_PLATFORMS.keys, allow_all_default: false)
426
+ end
371
427
 
372
- if tool == "claude" && config[tool].is_a?(Hash)
373
- config[tool].each do |_auth_type, users|
374
- count += users.keys.length if users.is_a?(Hash)
375
- end
376
- elsif config[tool].is_a?(Hash)
377
- count += config[tool].keys.length
428
+ tools.each do |tool|
429
+ available_known = KNOWN_PLATFORMS[tool].keys
430
+ puts
431
+ puts "[#{tool}]".green.bold
432
+ chosen = select_multi(
433
+ " 请选择 #{tool} 下要追加 / 覆盖的平台(逗号分隔多个序号)",
434
+ available_known, allow_all_default: false
435
+ )
436
+
437
+ chosen.each do |platform|
438
+ existed = Config::LocalConfig.available_platforms(tool).include?(platform)
439
+ if existed
440
+ print_status('覆盖', "#{tool} / #{platform}(已存在,将覆盖原有数据)")
441
+ else
442
+ print_status('新增', "#{tool} / #{platform}")
378
443
  end
444
+
445
+ data = collect_platform_data(tool, platform)
446
+ Config::LocalConfig.upsert_platform(tool, platform, data)
447
+ print_success("已写入 #{tool} / #{platform}")
379
448
  end
380
449
  end
381
450
 
382
- count
451
+ puts
452
+ run_list
383
453
  end
384
454
 
385
- def display_users_table(config)
386
- puts "\n可用用户列表:"
387
- puts "=" * 60
455
+ # ----- 通用交互辅助 -----
388
456
 
389
- %w[claude gemini gpt].each do |tool|
390
- next unless config[tool]
457
+ # 多选列表,返回选中的 name 列表
458
+ def select_multi(prompt, options, allow_all_default:)
459
+ loop do
460
+ puts "#{prompt}:"
461
+ options.each_with_index do |name, idx|
462
+ puts " #{idx + 1}) #{Config::LocalConfig.platform_display_name(name)}"
463
+ end
464
+ tail = allow_all_default ? '(回车选全部,q 取消)' : '(q 取消)'
465
+ print "请输入 #{tail} > "
391
466
 
392
- puts "\n#{tool.upcase}:"
467
+ input = $stdin.gets
468
+ raise Interrupt, '用户取消选择' if input.nil?
393
469
 
394
- if tool == "claude" && config[tool].is_a?(Hash)
395
- config[tool].each do |auth_type, users|
396
- next unless users.is_a?(Hash)
397
- puts " #{auth_type}:"
398
- users.each do |name, _file|
399
- puts " - #{name}"
400
- end
401
- end
402
- elsif config[tool].is_a?(Hash)
403
- config[tool].each do |name, _file|
404
- puts " - #{name}"
405
- end
470
+ input = input.chomp.strip
471
+ raise Interrupt, '用户取消选择' if input.casecmp('q').zero?
472
+
473
+ if input.empty?
474
+ return options.dup if allow_all_default
475
+
476
+ print_error('输入不能为空,请重试')
477
+ next
406
478
  end
407
- end
408
- end
409
479
 
410
- def verify_configuration
411
- puts "\n验证配置状态..."
412
- puts "=" * 60
413
-
414
- # 检查目录
415
- puts "\n目录检查:"
416
- puts " 配置目录: #{@config_dir} - " + (Dir.exist?(@config_dir) ? "✓".green : "✗".red)
417
- puts " 仓库目录: #{@repo_dir} - " + (repo_exists? ? "✓".green : "✗".red)
418
-
419
- # 检查 Git 状态
420
- if repo_exists?
421
- puts "\nGit 状态:"
422
- Dir.chdir(@repo_dir) do
423
- branch = `git branch --show-current`.chomp
424
- puts " 当前分支: #{branch}"
425
-
426
- status = `git status --short`
427
- if status.empty?
428
- puts " 工作区: 干净 ✓".green
429
- else
430
- puts " 工作区: 有修改 ⚠️".yellow
431
- end
480
+ indices = input.split(/[,\s]+/).reject(&:empty?).map { |s| Integer(s, 10) rescue nil }
481
+ if indices.any?(&:nil?) || indices.any? { |i| i < 1 || i > options.size }
482
+ print_error("非法序号:#{input},请重试")
483
+ next
432
484
  end
485
+
486
+ return indices.uniq.map { |i| options[i - 1] }
433
487
  end
488
+ end
434
489
 
435
- # 检查配置文件
436
- puts "\n配置文件:"
437
- verify_basic_config
490
+ # 收集单个平台的 env / proxy 数据
491
+ def collect_platform_data(tool, platform)
492
+ spec = KNOWN_PLATFORMS.dig(tool, platform) || {}
493
+ env = {}
438
494
 
439
- # 检查密码
440
- puts "\n密码状态:"
441
- if ENV['EASYAI_TEST_PASSWORD']
442
- puts " 环境变量密码: ✓".green
443
- elsif Base::SystemKeychain.get_stored_password
444
- puts " 系统钥匙串密码: ✓".green
445
- else
446
- puts " 未设置密码 ⚠️".yellow
495
+ Array(spec[:required_env]).each do |key|
496
+ env[key] = ask_required(key)
447
497
  end
448
498
 
449
- # 尝试加载用户
450
- show_available_users(compact: true)
451
- end
499
+ (spec[:optional_env] || {}).each do |key, default|
500
+ env[key] = ask_optional(key, default)
501
+ end
452
502
 
453
- def cleanup_files
454
- puts "\n清理临时文件..."
455
- puts "=" * 60
503
+ proxy = ask_proxy
456
504
 
457
- # 清理解密的 JSON 文件
458
- json_files = Dir.glob(File.join(@repo_dir, '**/*.json'))
459
- cleanup_count = 0
505
+ data = { 'env' => env }
506
+ data['proxy'] = proxy unless proxy.empty?
507
+ data
508
+ end
460
509
 
461
- json_files.each do |file|
462
- encrypted_file = "#{file}.encrypted"
463
- if File.exist?(encrypted_file)
464
- FileUtils.rm_f(file)
465
- cleanup_count += 1
466
- puts " 删除: #{File.basename(file)}"
510
+ def ask_required(key)
511
+ loop do
512
+ if sensitive_key?(key)
513
+ print " #{key}(必填,输入不回显): "
514
+ value = read_secret_line
515
+ else
516
+ print " #{key}(必填): "
517
+ value = read_line
467
518
  end
519
+
520
+ return value if value && !value.empty?
521
+
522
+ print_error("#{key} 不能为空,请重试")
468
523
  end
524
+ end
469
525
 
470
- if cleanup_count > 0
471
- puts "\n ✓ 清理了 #{cleanup_count} 个临时文件".green
526
+ def ask_optional(key, default)
527
+ if default && !default.to_s.empty?
528
+ print " #{key}(可选,回车使用默认 #{default}): "
472
529
  else
473
- puts " 没有需要清理的文件".green
530
+ print " #{key}(可选,回车跳过): "
474
531
  end
532
+
533
+ value =
534
+ if sensitive_key?(key)
535
+ read_secret_line
536
+ else
537
+ read_line
538
+ end
539
+
540
+ return default.to_s if (value.nil? || value.empty?) && default
541
+ return '' if value.nil?
542
+
543
+ value
475
544
  end
476
545
 
477
- def show_completion_info
478
- puts "\n" + "=" * 60
479
- puts "✅ " + "配置初始化完成!".green.bold
480
- puts
481
- puts "您可以开始使用以下命令:"
482
- puts " easyai claude".cyan + " - 启动 Claude"
483
- puts " • easyai gemini".cyan + " - 启动 Gemini"
484
- puts " • easyai gpt".cyan + " - 启动 GPT"
485
- puts
486
- puts "其他有用的命令:"
487
- puts " • easyai setup --verify".cyan + " - 验证配置状态"
488
- puts " • easyai setup --list-users".cyan + " - 查看可用用户"
489
- puts " • easyai setup --clean".cyan + " - 清理临时文件"
546
+ def ask_proxy
547
+ print ' 是否配置 HTTP/HTTPS 代理?(y/N) > '
548
+ ans = read_line
549
+ return {} unless ans && ans.downcase.start_with?('y')
550
+
551
+ print ' HTTP_PROXY (例如 http://127.0.0.1:7890): '
552
+ http = read_line
553
+ print ' HTTPS_PROXY (回车与 HTTP_PROXY 相同): '
554
+ https = read_line
555
+ https = http if https.nil? || https.empty?
556
+
557
+ proxy = {}
558
+ proxy['HTTP_PROXY'] = http unless http.nil? || http.empty?
559
+ proxy['HTTPS_PROXY'] = https unless https.nil? || https.empty?
560
+ proxy
561
+ end
562
+
563
+ def read_line
564
+ line = $stdin.gets
565
+ raise Interrupt, '用户取消输入' if line.nil?
566
+
567
+ line.chomp
568
+ end
569
+
570
+ def read_secret_line
571
+ line =
572
+ if $stdin.respond_to?(:noecho)
573
+ $stdin.noecho(&:gets)
574
+ else
575
+ # 在非 TTY(如测试用 StringIO)下退化为普通 gets,避免 NoMethodError
576
+ $stdin.gets
577
+ end
490
578
  puts
579
+ raise Interrupt, '用户取消输入' if line.nil?
580
+
581
+ line.chomp
582
+ end
583
+
584
+ # ----- 输出辅助 -----
585
+
586
+ def print_status(icon_text, detail)
587
+ puts " #{icon_text.to_s.ljust(4).cyan} #{detail}"
588
+ end
589
+
590
+ def print_success(message)
591
+ puts " ✓ #{message.green}"
592
+ end
593
+
594
+ def print_warning(message)
595
+ puts " ⚠ #{message.yellow}"
596
+ end
597
+
598
+ def print_error(message)
599
+ puts " ✗ #{message.red}"
491
600
  end
492
601
  end
493
602
  end
494
- end
603
+ end