jpsclient 0.2.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.
Files changed (88) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/bin/jpsclient +28 -0
  4. data/lib/jpsclient/api/app_level.rb +138 -0
  5. data/lib/jpsclient/api/app_resource.rb +164 -0
  6. data/lib/jpsclient/api/app_resource_version.rb +52 -0
  7. data/lib/jpsclient/api/apple_account.rb +140 -0
  8. data/lib/jpsclient/api/application.rb +268 -0
  9. data/lib/jpsclient/api/application_category.rb +120 -0
  10. data/lib/jpsclient/api/application_design.rb +118 -0
  11. data/lib/jpsclient/api/application_income.rb +54 -0
  12. data/lib/jpsclient/api/application_sales.rb +52 -0
  13. data/lib/jpsclient/api/application_version.rb +77 -0
  14. data/lib/jpsclient/api/assets_category.rb +120 -0
  15. data/lib/jpsclient/api/bug.rb +140 -0
  16. data/lib/jpsclient/api/cert.rb +27 -0
  17. data/lib/jpsclient/api/client.rb +268 -0
  18. data/lib/jpsclient/api/collect.rb +52 -0
  19. data/lib/jpsclient/api/collection.rb +162 -0
  20. data/lib/jpsclient/api/commit_log.rb +104 -0
  21. data/lib/jpsclient/api/creative.rb +52 -0
  22. data/lib/jpsclient/api/custom_application.rb +230 -0
  23. data/lib/jpsclient/api/custom_application_web.rb +52 -0
  24. data/lib/jpsclient/api/document_text.rb +30 -0
  25. data/lib/jpsclient/api/experience.rb +143 -0
  26. data/lib/jpsclient/api/experience_category.rb +97 -0
  27. data/lib/jpsclient/api/fgui_export.rb +52 -0
  28. data/lib/jpsclient/api/file.rb +84 -0
  29. data/lib/jpsclient/api/game_assets.rb +140 -0
  30. data/lib/jpsclient/api/healthy.rb +30 -0
  31. data/lib/jpsclient/api/icon_and_snapshot.rb +54 -0
  32. data/lib/jpsclient/api/idea.rb +121 -0
  33. data/lib/jpsclient/api/jssdk.rb +30 -0
  34. data/lib/jpsclient/api/lark_bitable.rb +30 -0
  35. data/lib/jpsclient/api/lark_chat_group.rb +30 -0
  36. data/lib/jpsclient/api/lark_comment.rb +118 -0
  37. data/lib/jpsclient/api/lark_task.rb +140 -0
  38. data/lib/jpsclient/api/lark_task_list.rb +118 -0
  39. data/lib/jpsclient/api/lark_task_section.rb +74 -0
  40. data/lib/jpsclient/api/lark_user.rb +30 -0
  41. data/lib/jpsclient/api/lark_wiki_node.rb +30 -0
  42. data/lib/jpsclient/api/lark_wiki_space.rb +30 -0
  43. data/lib/jpsclient/api/lazy_client.rb +290 -0
  44. data/lib/jpsclient/api/login.rb +96 -0
  45. data/lib/jpsclient/api/m3u8.rb +30 -0
  46. data/lib/jpsclient/api/menu.rb +143 -0
  47. data/lib/jpsclient/api/modular_client.rb +228 -0
  48. data/lib/jpsclient/api/project.rb +46 -0
  49. data/lib/jpsclient/api/project_package.rb +249 -0
  50. data/lib/jpsclient/api/publisher.rb +165 -0
  51. data/lib/jpsclient/api/publisher_category.rb +142 -0
  52. data/lib/jpsclient/api/publisher_group.rb +118 -0
  53. data/lib/jpsclient/api/publisher_group_category.rb +120 -0
  54. data/lib/jpsclient/api/requirements.rb +143 -0
  55. data/lib/jpsclient/api/requirements_category.rb +97 -0
  56. data/lib/jpsclient/api/resource_category.rb +120 -0
  57. data/lib/jpsclient/api/role.rb +165 -0
  58. data/lib/jpsclient/api/simple_search.rb +162 -0
  59. data/lib/jpsclient/api/sketch.rb +74 -0
  60. data/lib/jpsclient/api/sketch_category.rb +97 -0
  61. data/lib/jpsclient/api/sov.rb +30 -0
  62. data/lib/jpsclient/api/statistics.rb +30 -0
  63. data/lib/jpsclient/api/store.rb +30 -0
  64. data/lib/jpsclient/api/survey.rb +143 -0
  65. data/lib/jpsclient/api/survey_category.rb +97 -0
  66. data/lib/jpsclient/api/tag.rb +142 -0
  67. data/lib/jpsclient/api/tool.rb +121 -0
  68. data/lib/jpsclient/api/tool_category.rb +120 -0
  69. data/lib/jpsclient/api/trending.rb +30 -0
  70. data/lib/jpsclient/api/ud_id.rb +118 -0
  71. data/lib/jpsclient/api/user.rb +99 -0
  72. data/lib/jpsclient/api/util.rb +30 -0
  73. data/lib/jpsclient/api/video_cover.rb +30 -0
  74. data/lib/jpsclient/api/webhook.rb +118 -0
  75. data/lib/jpsclient/api/workflow.rb +118 -0
  76. data/lib/jpsclient/auth/auth.rb +676 -0
  77. data/lib/jpsclient/auth/token.rb +209 -0
  78. data/lib/jpsclient/base/api_config.rb +225 -0
  79. data/lib/jpsclient/base/exception.rb +5 -0
  80. data/lib/jpsclient/http/http_client.rb +148 -0
  81. data/lib/jpsclient/upload/upload_client.rb +334 -0
  82. data/lib/jpsclient/upload/upload_config.rb +128 -0
  83. data/lib/jpsclient/upload/upload_progress.rb +73 -0
  84. data/lib/jpsclient/utils/aes.rb +49 -0
  85. data/lib/jpsclient/utils/logger.rb +38 -0
  86. data/lib/jpsclient/version.rb +3 -0
  87. data/lib/jpsclient.rb +34 -0
  88. metadata +269 -0
@@ -0,0 +1,209 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'json'
4
+ require 'fileutils'
5
+ require 'net/http'
6
+ require 'uri'
7
+ require 'jpsclient/utils/aes'
8
+ require 'jpsclient/base/exception'
9
+ require 'jpsclient/utils/logger'
10
+
11
+ module JPSClient
12
+
13
+ # Token 管理类
14
+ class Token
15
+ attr_reader :token, :username, :expires_at
16
+
17
+ def initialize(config)
18
+ @config = config
19
+ @aes_key = config.aes_key if config
20
+
21
+ # 只使用新的 token 文件
22
+ @token_dir = File.expand_path('~/.pindo')
23
+ @token_file = File.join(@token_dir, '.jpstoken')
24
+
25
+ # token 数据
26
+ @token = nil
27
+ @username = nil
28
+ @expires_at = nil
29
+ @created_at = nil
30
+
31
+ # 调试模式
32
+ @verbose = ENV['PINDO_DEBUG'] == 'true'
33
+ end
34
+
35
+ # 加载 token
36
+ def load
37
+ return false unless File.exist?(@token_file)
38
+
39
+ begin
40
+ file_content = File.read(@token_file)
41
+
42
+ # 根据是否有 AES 密钥决定解密方式
43
+ token_data = if @aes_key
44
+ begin
45
+ aes = AES.new(@aes_key)
46
+ decrypted = aes.decrypt(file_content)
47
+ JSON.parse(decrypted)
48
+ rescue => e
49
+ # 解密失败,可能是明文,尝试直接解析
50
+ puts "尝试解密失败,作为明文读取: #{e.message}" if @verbose
51
+ JSON.parse(file_content)
52
+ end
53
+ else
54
+ JSON.parse(file_content)
55
+ end
56
+
57
+ @token = token_data['token']
58
+ @username = token_data['username']
59
+ @expires_at = token_data['expires_at']
60
+ @created_at = token_data['created_at']
61
+
62
+ return true if @token
63
+ rescue => e
64
+ puts "读取 token 失败: #{e.message}" if @verbose
65
+ clear_corrupted_file
66
+ end
67
+
68
+ false
69
+ end
70
+
71
+ # 保存 token
72
+ def save(token, username, expires_at)
73
+ return false unless token
74
+
75
+ @token = token
76
+ @username = username
77
+ @expires_at = expires_at
78
+ @created_at = Time.now.to_i
79
+
80
+ # 确保目录存在
81
+ FileUtils.mkdir_p(@token_dir) unless Dir.exist?(@token_dir)
82
+
83
+ token_data = {
84
+ 'token' => @token,
85
+ 'username' => @username,
86
+ 'expires_at' => @expires_at,
87
+ 'created_at' => @created_at
88
+ }
89
+
90
+ # 根据是否有 AES 密钥决定加密方式
91
+ content = if @aes_key
92
+ aes = AES.new(@aes_key)
93
+ aes.encrypt(token_data.to_json)
94
+ else
95
+ token_data.to_json
96
+ end
97
+
98
+ File.write(@token_file, content)
99
+ puts "✓ Token 已保存到 #{@token_file}" if @verbose
100
+
101
+ true
102
+ rescue => e
103
+ puts "保存 token 失败: #{e.message}"
104
+ false
105
+ end
106
+
107
+ # 验证 token 有效性
108
+ def valid?
109
+ return false unless @token
110
+
111
+ # 1. 检查本地时间过期
112
+ if expired?
113
+ puts "Token 已过期 (本地时间检查)" if @verbose
114
+ return false
115
+ end
116
+
117
+ # 2. 可选:API 验证
118
+ # 为了避免频繁调用,只在接近过期时验证
119
+ if should_verify_with_api?
120
+ return verify_with_api
121
+ end
122
+
123
+ true
124
+ end
125
+
126
+ # 检查是否过期
127
+ def expired?
128
+ return true unless @expires_at
129
+ Time.now.to_i > @expires_at
130
+ end
131
+
132
+ # API 验证 token
133
+ def verify_with_api
134
+ return false unless @token && @config
135
+
136
+ begin
137
+ # 使用配置中的验证端点
138
+ base_url = @config.api_endpoint.split('/api/')[0] # 获取基础 URL
139
+ verify_endpoint = @config.respond_to?(:token_verify_endpoint) ? @config.token_verify_endpoint : '/api/user/profile'
140
+ uri = URI("#{base_url}#{verify_endpoint}")
141
+
142
+ request = Net::HTTP::Get.new(uri)
143
+ request['Authorization'] = "Bearer #{@token}"
144
+
145
+ response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
146
+ http.request(request)
147
+ end
148
+
149
+ if response.code == '200'
150
+ puts "Token API 验证成功" if @verbose
151
+ return true
152
+ else
153
+ puts "Token API 验证失败: #{response.code}" if @verbose
154
+ return false
155
+ end
156
+ rescue => e
157
+ puts "Token API 验证出错: #{e.message}" if @verbose
158
+ # API 验证失败时,回退到本地验证
159
+ return !expired?
160
+ end
161
+ end
162
+
163
+ # 清除 token
164
+ def clear
165
+ @token = nil
166
+ @username = nil
167
+ @expires_at = nil
168
+ @created_at = nil
169
+
170
+ FileUtils.rm_f(@token_file) if File.exist?(@token_file)
171
+ puts "✓ Token 已清除" if @verbose
172
+ end
173
+
174
+ # 转换为 Hash(兼容旧代码)
175
+ def to_h
176
+ return nil unless @token
177
+
178
+ {
179
+ 'token' => @token,
180
+ 'username' => @username,
181
+ 'expires_at' => @expires_at
182
+ }
183
+ end
184
+
185
+ # 从 Auth 实例更新 token
186
+ def update_from_auth(auth)
187
+ return false unless auth.access_token
188
+
189
+ save(auth.access_token, auth.username, auth.expires_at)
190
+ end
191
+
192
+ private
193
+
194
+ # 是否应该通过 API 验证
195
+ def should_verify_with_api?
196
+ return false unless @expires_at
197
+
198
+ # 策略:最后 24 小时内进行 API 验证
199
+ remaining_time = @expires_at - Time.now.to_i
200
+ remaining_time > 0 && remaining_time < 24 * 60 * 60
201
+ end
202
+
203
+ # 清除损坏的文件
204
+ def clear_corrupted_file
205
+ FileUtils.rm_f(@token_file) if File.exist?(@token_file)
206
+ puts "已清除损坏的 token 文件" if @verbose
207
+ end
208
+ end
209
+ end
@@ -0,0 +1,225 @@
1
+ require 'json'
2
+ require 'jpsclient/base/exception'
3
+
4
+ module JPSClient
5
+ # API 配置管理类
6
+ class ApiConfig
7
+ attr_reader :endpoints, :base_url
8
+
9
+ def initialize(config_json)
10
+ @base_url = config_json["pgyerapps_base_url"]
11
+
12
+ # 兼容旧配置格式
13
+ if config_json["api_path_config"]
14
+ @legacy_config = config_json["api_path_config"]
15
+ @endpoints = convert_legacy_config(@legacy_config)
16
+ elsif config_json["api_endpoints"]
17
+ @endpoints = config_json["api_endpoints"]
18
+ else
19
+ raise ExceptionError, "API配置格式错误:缺少api_path_config或api_endpoints"
20
+ end
21
+
22
+ @error_codes = config_json["error_codes"] || default_error_codes
23
+ @rate_limits = config_json["rate_limits"] || default_rate_limits
24
+ end
25
+
26
+ # 获取API端点信息
27
+ def get_endpoint(category, action)
28
+ if @legacy_config
29
+ # 兼容旧格式
30
+ path = @legacy_config["#{category}_#{action}"] || @legacy_config[action]
31
+ return nil unless path
32
+
33
+ {
34
+ "path" => path,
35
+ "method" => guess_method(action),
36
+ "requires_auth" => !action.include?("login"),
37
+ "description" => "#{category}.#{action}"
38
+ }
39
+ else
40
+ # 新格式
41
+ @endpoints.dig(category.to_s, action.to_s)
42
+ end
43
+ end
44
+
45
+ # 获取完整的API URL
46
+ def get_url(category, action)
47
+ endpoint = get_endpoint(category, action)
48
+ return nil unless endpoint
49
+
50
+ "#{@base_url}#{endpoint['path']}"
51
+ end
52
+
53
+ # 获取API方法
54
+ def get_method(category, action)
55
+ endpoint = get_endpoint(category, action)
56
+ endpoint ? endpoint['method'] : 'GET'
57
+ end
58
+
59
+ # 是否需要认证
60
+ def requires_auth?(category, action)
61
+ endpoint = get_endpoint(category, action)
62
+ endpoint ? endpoint.fetch('requires_auth', true) : true
63
+ end
64
+
65
+ # 获取参数定义
66
+ def get_parameters(category, action)
67
+ endpoint = get_endpoint(category, action)
68
+ endpoint ? endpoint.fetch('parameters', {}) : {}
69
+ end
70
+
71
+ # 验证参数
72
+ def validate_params(category, action, params)
73
+ param_definitions = get_parameters(category, action)
74
+ errors = []
75
+
76
+ # 检查必需参数
77
+ param_definitions.each do |name, definition|
78
+ if definition['required'] && !params.key?(name.to_sym) && !params.key?(name.to_s)
79
+ errors << "缺少必需参数: #{name}"
80
+ end
81
+
82
+ # 类型检查(如果提供了参数)
83
+ value = params[name.to_sym] || params[name.to_s]
84
+ if value && definition['type']
85
+ unless check_type(value, definition['type'])
86
+ errors << "参数 #{name} 类型错误,期望 #{definition['type']}"
87
+ end
88
+ end
89
+
90
+ # 枚举值检查
91
+ if value && definition['enum'] && !definition['enum'].include?(value)
92
+ errors << "参数 #{name} 值无效,必须是: #{definition['enum'].join(', ')}"
93
+ end
94
+ end
95
+
96
+ errors
97
+ end
98
+
99
+ # 获取错误描述
100
+ def get_error_message(code)
101
+ @error_codes[code.to_s] || "未知错误 (#{code})"
102
+ end
103
+
104
+ private
105
+
106
+ # 将旧配置格式转换为新格式
107
+ def convert_legacy_config(legacy)
108
+ converted = {}
109
+
110
+ legacy.each do |key, value|
111
+ # 处理新格式(包含 http_method 和 url)
112
+ if value.is_a?(Hash) && value['url']
113
+ endpoint = {
114
+ "path" => value['url'],
115
+ "method" => value['http_method'] || guess_method(key),
116
+ "alias" => value['alias'] || key,
117
+ "description" => value['description'],
118
+ "requires_auth" => !key.include?('login') && !key.include?('send_code')
119
+ }
120
+
121
+ # 根据 key 分类到不同的组
122
+ category, action = categorize_endpoint(key)
123
+ converted[category] ||= {}
124
+ converted[category][action] = endpoint
125
+ # 处理旧格式(只有路径字符串)
126
+ elsif value.is_a?(String)
127
+ endpoint = build_endpoint(value, guess_method(key), !key.include?('login'))
128
+ category, action = categorize_endpoint(key)
129
+ converted[category] ||= {}
130
+ converted[category][action] = endpoint
131
+ end
132
+ end
133
+
134
+ converted
135
+ end
136
+
137
+ def build_endpoint(path, method, requires_auth = true)
138
+ return nil unless path
139
+ {
140
+ "path" => path,
141
+ "method" => method,
142
+ "requires_auth" => requires_auth,
143
+ "parameters" => {}
144
+ }
145
+ end
146
+
147
+ # 根据 key 分类端点
148
+ def categorize_endpoint(key)
149
+ case key
150
+ when /app_list|app_info/
151
+ ["apps", key.gsub(/^(get_|update_)/, '')]
152
+ when /upload|signed_url/
153
+ ["upload", key.gsub(/^(post_|multi_)/, '')]
154
+ when /version|comment/
155
+ ["version", key.gsub(/^(get_|update_)/, '')]
156
+ when /cert|resign/
157
+ ["cert", key.gsub(/^(get_|post_)/, '')]
158
+ when /login|send_code|profile/
159
+ ["auth", key.gsub(/^do_/, '')]
160
+ when /message|notification/
161
+ ["notification", key.gsub(/^send_/, '')]
162
+ else
163
+ ["misc", key]
164
+ end
165
+ end
166
+
167
+ # 根据动作名称猜测HTTP方法
168
+ def guess_method(action)
169
+ case action
170
+ when /^(get|list|fetch|query|search)/i
171
+ "GET"
172
+ when /^(post|create|add|send|do|update|upload)/i
173
+ "POST"
174
+ when /^(put|update|modify)/i
175
+ "PUT"
176
+ when /^(delete|remove)/i
177
+ "DELETE"
178
+ else
179
+ "GET"
180
+ end
181
+ end
182
+
183
+ # 检查值类型
184
+ def check_type(value, expected_type)
185
+ case expected_type.downcase
186
+ when 'string'
187
+ value.is_a?(String)
188
+ when 'integer', 'int'
189
+ value.is_a?(Integer)
190
+ when 'number', 'float'
191
+ value.is_a?(Numeric)
192
+ when 'boolean', 'bool'
193
+ [true, false].include?(value)
194
+ when 'array'
195
+ value.is_a?(Array)
196
+ when 'object', 'hash'
197
+ value.is_a?(Hash)
198
+ else
199
+ true
200
+ end
201
+ end
202
+
203
+ # 默认错误码
204
+ def default_error_codes
205
+ {
206
+ "200" => "成功",
207
+ "400" => "请求参数错误",
208
+ "401" => "未授权或token过期",
209
+ "403" => "权限不足",
210
+ "404" => "资源不存在",
211
+ "500" => "服务器内部错误"
212
+ }
213
+ end
214
+
215
+ # 默认速率限制
216
+ def default_rate_limits
217
+ {
218
+ "default" => {
219
+ "requests_per_minute" => 60,
220
+ "requests_per_hour" => 1000
221
+ }
222
+ }
223
+ end
224
+ end
225
+ end
@@ -0,0 +1,5 @@
1
+ module JPSClient
2
+ # JPS 模块的统一异常类
3
+ class ExceptionError < StandardError
4
+ end
5
+ end
@@ -0,0 +1,148 @@
1
+ require 'json'
2
+ require 'faraday'
3
+ require 'faraday/retry'
4
+
5
+ module JPSClient
6
+ # JPS 专用 HTTP 客户端 - 优化版
7
+ class HttpClient
8
+ attr_accessor :base_url
9
+ attr_accessor :token
10
+
11
+ def initialize(base_url: nil, token: nil)
12
+ @base_url = base_url
13
+ @token = token
14
+ @connection = nil
15
+ end
16
+
17
+ # GET 请求
18
+ def get(path, params: nil)
19
+ request(:get, path, params: params)
20
+ end
21
+
22
+ # POST 请求
23
+ def post(path, body: nil, timeout: nil)
24
+ request(:post, path, body: body, timeout: timeout)
25
+ end
26
+
27
+ # 更新 token
28
+ def update_token(new_token)
29
+ @token = new_token
30
+ end
31
+
32
+ private
33
+
34
+ # 统一的请求处理
35
+ def request(method, path, params: nil, body: nil, timeout: nil)
36
+ url = @base_url + path
37
+
38
+ begin
39
+ response = connection.send(method) do |req|
40
+ req.url url
41
+ req.headers['Content-Type'] = 'application/json'
42
+ req.headers['token'] = @token if @token
43
+ req.params = params if params
44
+ req.body = body.to_json if body && method == :post
45
+ req.options.timeout = timeout if timeout
46
+ end
47
+
48
+ parse_response(response)
49
+ rescue Faraday::Error => e
50
+ # 处理网络错误
51
+ {
52
+ code: 0,
53
+ body: nil,
54
+ success: false,
55
+ error: e.message
56
+ }
57
+ end
58
+ end
59
+
60
+ # 获取或创建连接(复用连接)
61
+ def connection
62
+ @connection ||= Faraday.new do |config|
63
+ # 重试配置
64
+ config.request :retry, {
65
+ max: 3,
66
+ interval: 0.5,
67
+ backoff_factor: 2,
68
+ interval_randomness: 0.5,
69
+ exceptions: [Faraday::TimeoutError, Faraday::ConnectionFailed]
70
+ }
71
+
72
+ # 代理配置
73
+ if ENV['http_proxy'] || ENV['https_proxy']
74
+ config.proxy = {
75
+ uri: ENV['http_proxy'] || ENV['https_proxy'],
76
+ user: ENV['HTTP_PROXY_USER'],
77
+ password: ENV['HTTP_PROXY_PASSWORD']
78
+ }.compact
79
+ end
80
+
81
+ config.adapter Faraday.default_adapter
82
+ end
83
+ end
84
+
85
+ # 解析响应
86
+ def parse_response(response)
87
+ result = {
88
+ code: response.status,
89
+ body: parse_body(response.body),
90
+ success: response.success?
91
+ }
92
+
93
+ # 标记需要登录
94
+ result[:need_login] = true if response.status == 401
95
+
96
+ result
97
+ end
98
+
99
+ # 解析响应体
100
+ def parse_body(body)
101
+ return nil if body.nil? || body.empty?
102
+
103
+ JSON.parse(body)
104
+ rescue JSON::ParserError
105
+ body
106
+ end
107
+ end
108
+
109
+ end
110
+
111
+ module JPSClient
112
+ # JPS API 响应封装类 - 简化版
113
+ class Response
114
+ attr_reader :code
115
+ attr_reader :data
116
+ attr_reader :msg
117
+
118
+ def initialize(http_response)
119
+ @success = http_response[:success]
120
+ @need_login = http_response[:need_login]
121
+
122
+ if http_response[:body].is_a?(Hash)
123
+ @code = http_response[:body]['code'] || http_response[:code]
124
+ @data = http_response[:body]['data']
125
+ @msg = http_response[:body]['msg'] || http_response[:body]['message']
126
+ else
127
+ @code = http_response[:code]
128
+ @data = http_response[:body]
129
+ end
130
+ end
131
+
132
+ def success?
133
+ @success && @code.to_s == '200'
134
+ end
135
+
136
+ def need_login?
137
+ @need_login || @code.to_s == '401'
138
+ end
139
+
140
+ def to_h
141
+ {
142
+ 'code' => @code,
143
+ 'data' => @data,
144
+ 'msg' => @msg
145
+ }
146
+ end
147
+ end
148
+ end