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.
- checksums.yaml +4 -4
- data/AGENTS.md +10 -8
- data/CLAUDE.md +211 -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/command/ai_tool_base.rb +218 -0
- data/lib/easyai/command/backup/claude.rb +124 -0
- data/lib/easyai/command/backup.rb +23 -0
- data/lib/easyai/command/claude.rb +72 -357
- data/lib/easyai/command/clean.rb +90 -395
- data/lib/easyai/command/codex.rb +39 -0
- data/lib/easyai/command/gemini.rb +23 -41
- data/lib/easyai/command/restore/claude.rb +150 -0
- data/lib/easyai/command/restore.rb +24 -0
- data/lib/easyai/command/setup.rb +487 -378
- 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 +161 -0
- data/lib/easyai/version.rb +1 -1
- data/lib/easyai.rb +29 -35
- metadata +20 -37
- 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/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,603 @@
|
|
|
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'
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
['--
|
|
32
|
-
['--
|
|
33
|
-
['--
|
|
34
|
-
['--list
|
|
35
|
-
['--
|
|
36
|
-
['--
|
|
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
|
-
@
|
|
42
|
-
@
|
|
43
|
-
@
|
|
44
|
-
@
|
|
45
|
-
@
|
|
46
|
-
@
|
|
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
|
-
|
|
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
|
-
|
|
57
|
-
verify_configuration
|
|
58
|
-
return
|
|
59
|
-
end
|
|
177
|
+
private
|
|
60
178
|
|
|
61
|
-
|
|
62
|
-
cleanup_files
|
|
63
|
-
return
|
|
64
|
-
end
|
|
179
|
+
# ----- list -----
|
|
65
180
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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 "
|
|
83
|
-
puts
|
|
84
|
-
puts
|
|
85
|
-
end
|
|
190
|
+
puts "EasyAI 配置概览(#{Config::LocalConfig.config_path})".cyan.bold
|
|
191
|
+
puts '=' * 60
|
|
192
|
+
puts "schema 版本:#{cfg['version']}"
|
|
86
193
|
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
91
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
#
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
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
|
|
121
|
-
|
|
122
|
-
puts "-" * 40
|
|
123
|
-
end
|
|
229
|
+
def print_platform_data(data)
|
|
230
|
+
return unless data.is_a?(Hash)
|
|
124
231
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
|
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
|
|
135
|
-
|
|
251
|
+
def format_value(key, value)
|
|
252
|
+
Base::SecretMasker.format_value(key, value)
|
|
136
253
|
end
|
|
137
254
|
|
|
138
|
-
def
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
FileUtils.rm_rf(@repo_dir)
|
|
142
|
-
end
|
|
255
|
+
def sensitive_key?(key)
|
|
256
|
+
Base::SecretMasker.sensitive_key?(key)
|
|
257
|
+
end
|
|
143
258
|
|
|
144
|
-
|
|
259
|
+
def mask_sensitive(value)
|
|
260
|
+
Base::SecretMasker.mask(value)
|
|
261
|
+
end
|
|
145
262
|
|
|
146
|
-
|
|
147
|
-
output = nil
|
|
148
|
-
success = false
|
|
263
|
+
# ----- remove -----
|
|
149
264
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
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?
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
puts "
|
|
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
|
-
|
|
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
|
|
284
|
+
Config::LocalConfig.delete_platform(@tool, @remove)
|
|
285
|
+
print_success("已删除 #{@tool} / #{@remove}")
|
|
211
286
|
|
|
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
|
|
287
|
+
# 如果该 tool 已被一并清除,再做一次温和提示
|
|
288
|
+
unless Config::LocalConfig.available_platforms(@tool).any?
|
|
289
|
+
puts " 工具 #{@tool} 下已无任何平台,已从配置中移除该工具键。"
|
|
290
|
+
end
|
|
291
|
+
end
|
|
221
292
|
|
|
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
|
|
293
|
+
# ----- add -----
|
|
230
294
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
244
|
-
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
|
|
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
|
-
|
|
255
|
-
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
269
|
-
puts " 提示: 配置文件将在使用时解密"
|
|
335
|
+
print_status('提示', "原配置文件不存在 #{path},直接进入交互配置")
|
|
270
336
|
end
|
|
337
|
+
|
|
338
|
+
run_full_interactive
|
|
271
339
|
end
|
|
272
340
|
|
|
273
|
-
|
|
274
|
-
# 优先使用环境变量
|
|
275
|
-
if ENV['EASYAI_TEST_PASSWORD']
|
|
276
|
-
puts " 使用环境变量中的密码"
|
|
277
|
-
return ENV['EASYAI_TEST_PASSWORD']
|
|
278
|
-
end
|
|
341
|
+
# ----- edit -----
|
|
279
342
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
if
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
|
|
289
|
-
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
|
|
363
|
+
# 校验回写后是否仍可解析
|
|
302
364
|
begin
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
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
|
-
|
|
337
|
-
|
|
376
|
+
def run_full_interactive
|
|
377
|
+
if Config::LocalConfig.exists?
|
|
378
|
+
run_upsert_menu
|
|
338
379
|
else
|
|
339
|
-
|
|
380
|
+
run_first_time_setup
|
|
340
381
|
end
|
|
341
382
|
end
|
|
342
383
|
|
|
343
|
-
def
|
|
344
|
-
|
|
345
|
-
|
|
384
|
+
def run_first_time_setup
|
|
385
|
+
puts
|
|
386
|
+
puts 'EasyAI 首次配置向导'.green.bold
|
|
387
|
+
puts '=' * 60
|
|
346
388
|
|
|
347
|
-
|
|
348
|
-
|
|
389
|
+
tools = select_multi('请选择要配置的 AI 工具(用逗号分隔多个序号,回车默认全部)',
|
|
390
|
+
KNOWN_PLATFORMS.keys, allow_all_default: true)
|
|
349
391
|
|
|
350
|
-
|
|
351
|
-
puts
|
|
352
|
-
|
|
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
|
-
|
|
356
|
-
|
|
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
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
-
|
|
451
|
+
puts
|
|
452
|
+
run_list
|
|
383
453
|
end
|
|
384
454
|
|
|
385
|
-
|
|
386
|
-
puts "\n可用用户列表:"
|
|
387
|
-
puts "=" * 60
|
|
455
|
+
# ----- 通用交互辅助 -----
|
|
388
456
|
|
|
389
|
-
|
|
390
|
-
|
|
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
|
-
|
|
467
|
+
input = $stdin.gets
|
|
468
|
+
raise Interrupt, '用户取消选择' if input.nil?
|
|
393
469
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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
|
-
|
|
437
|
-
|
|
490
|
+
# 收集单个平台的 env / proxy 数据
|
|
491
|
+
def collect_platform_data(tool, platform)
|
|
492
|
+
spec = KNOWN_PLATFORMS.dig(tool, platform) || {}
|
|
493
|
+
env = {}
|
|
438
494
|
|
|
439
|
-
|
|
440
|
-
|
|
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
|
-
|
|
451
|
-
|
|
499
|
+
(spec[:optional_env] || {}).each do |key, default|
|
|
500
|
+
env[key] = ask_optional(key, default)
|
|
501
|
+
end
|
|
452
502
|
|
|
453
|
-
|
|
454
|
-
puts "\n清理临时文件..."
|
|
455
|
-
puts "=" * 60
|
|
503
|
+
proxy = ask_proxy
|
|
456
504
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
505
|
+
data = { 'env' => env }
|
|
506
|
+
data['proxy'] = proxy unless proxy.empty?
|
|
507
|
+
data
|
|
508
|
+
end
|
|
460
509
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
if
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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
|
-
|
|
471
|
-
|
|
526
|
+
def ask_optional(key, default)
|
|
527
|
+
if default && !default.to_s.empty?
|
|
528
|
+
print " #{key}(可选,回车使用默认 #{default}): "
|
|
472
529
|
else
|
|
473
|
-
|
|
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
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
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
|