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