2
0

clash_tools.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448
  1. # -*- coding: utf-8 -*-
  2. from concurrent.futures import ThreadPoolExecutor, as_completed
  3. from odoo import fields, models, api
  4. from odoo.exceptions import UserError
  5. from urllib.parse import quote
  6. import httpx
  7. import subprocess
  8. class ClashTools(models.Model):
  9. _name = 'clash.tools'
  10. _description = 'Clash Tools'
  11. name = fields.Char('Name')
  12. localhost_ip = fields.Char('Localhost IP')
  13. api_ip = fields.Char('API IP')
  14. current_node = fields.Char('Current Node')
  15. total_nodes = fields.Integer('Total Nodes')
  16. use_type = fields.Selection([
  17. ('web3', 'WEB3'),
  18. ('depin', 'Depin'),
  19. ], string='Use Type', default='')
  20. current_node_state = fields.Char('Current Node State')
  21. line_ids = fields.One2many('clash.tools.line', 'clash_tools_id', string='Line')
  22. clash_no_skip_config_id = fields.Many2one('clash.no_skip.config', string='Clash Skip Config')
  23. def btn_init_data(self):
  24. # 一键创建所有局域网中的 clash 连接, 因为懒
  25. data_dict = {
  26. 'web3_01': '192.168.31.201:58001',
  27. 'web3_02': '192.168.31.201:58002',
  28. 'web3_03': '192.168.31.201:58003',
  29. 'web3_04': '192.168.31.201:58004',
  30. 'web3_05': '192.168.31.201:58005',
  31. 'web3_06': '192.168.31.201:58006',
  32. 'web3_07': '192.168.31.201:58007',
  33. 'web3_08': '192.168.31.201:58008',
  34. 'web3_09': '192.168.31.201:58009',
  35. 'web3_10': '192.168.31.201:58010',
  36. 'depin_01': '192.168.31.201:32001',
  37. 'depin_02': '192.168.31.201:32002',
  38. 'depin_03': '192.168.31.201:32003',
  39. 'depin_04': '192.168.31.201:32004',
  40. 'depin_05': '192.168.31.201:32005',
  41. 'depin_06': '192.168.31.201:32006',
  42. 'depin_07': '192.168.31.201:32007',
  43. 'depin_08': '192.168.31.201:32008',
  44. 'depin_09': '192.168.31.201:32009',
  45. 'depin_10': '192.168.31.201:32010',
  46. 'depin_11': '192.168.31.201:32011',
  47. 'depin_12': '192.168.31.201:32012',
  48. }
  49. all_data_name_list = [i.name for i in self.search([])]
  50. print(all_data_name_list)
  51. for key, value in data_dict.items():
  52. if key in all_data_name_list:
  53. continue
  54. else:
  55. use_type = ''
  56. if 'depin' in key:
  57. use_type = 'depin'
  58. elif 'web3' in key:
  59. use_type = 'web3'
  60. self.create({
  61. 'name': key,
  62. 'localhost_ip': value,
  63. 'use_type': use_type
  64. })
  65. def btn_get_all_node(self):
  66. for rec in self:
  67. if not rec.localhost_ip:
  68. continue
  69. # 先获取所有节点
  70. url = rec.localhost_ip
  71. if 'https' in url:
  72. raise UserError('Local network services do not require HTTPS.')
  73. if 'http' not in url:
  74. url = 'http://' + url
  75. self._set_global_proxy(url, rec)
  76. proxies_list = self._get_all_node(url, rec)
  77. if proxies_list:
  78. rec.total_nodes = len(proxies_list)
  79. # 清空当前 line
  80. rec.line_ids.unlink()
  81. # 循环添加节点到 line
  82. for proxies in proxies_list:
  83. if proxies == "DIRECT" or proxies == "REJECT" or proxies == "GLOBAL":
  84. continue
  85. rec.line_ids.create({
  86. 'name': proxies,
  87. 'clash_tools_id': rec.id
  88. })
  89. def btn_check_all_node(self):
  90. for rec in self:
  91. if not rec.localhost_ip:
  92. continue
  93. if not rec.line_ids:
  94. self.btn_get_all_node()
  95. url = rec.localhost_ip
  96. if 'https' in url:
  97. raise UserError('Local network services do not require HTTPS.')
  98. if 'http' not in url:
  99. url = 'http://' + url
  100. line_count = len(rec.line_ids)
  101. if line_count:
  102. rec.total_nodes = line_count
  103. with ThreadPoolExecutor(max_workers=line_count) as executor:
  104. # 提交任务到线程池
  105. futures = {executor.submit(self._check_node, quote(line.name, safe=""), url): line for line in rec.line_ids}
  106. # 处理线程池返回的结果
  107. for future in as_completed(futures):
  108. line = futures[future]
  109. try:
  110. res = future.result()
  111. if res != 9999:
  112. line.update({
  113. 'delay': res.setdefault('delay'),
  114. 'mean_delay': res.setdefault('meanDelay'),
  115. 'node_state': 'ok'
  116. })
  117. else:
  118. line.update({
  119. 'delay': 9999,
  120. 'mean_delay': 9999,
  121. 'node_state': 'error'
  122. })
  123. except Exception as e:
  124. print(str(e))
  125. line.update({
  126. 'delay': -1,
  127. 'mean_delay': -1,
  128. 'node_state': 'error'
  129. })
  130. result = rec._get_current_node()
  131. if result:
  132. rec.current_node = result
  133. def btn_select_node(self):
  134. selected_node_list = []
  135. for rec in self:
  136. if not rec.localhost_ip:
  137. continue
  138. if not rec.line_ids:
  139. self.btn_get_all_node()
  140. if not rec.line_ids:
  141. continue
  142. url = rec.localhost_ip
  143. if 'https' in url:
  144. raise UserError('Local network services do not require HTTPS.')
  145. if 'http' not in url:
  146. url = 'http://' + url
  147. # 拿到 line 中, 延迟最小的节点数据
  148. line_delay_min = self.line_ids.search([('clash_tools_id', '=', rec.id), ('node_state', '=', 'ok')], order='delay asc')
  149. for line in line_delay_min:
  150. if rec.clash_no_skip_config_id:
  151. try:
  152. no_skip_node_list = rec.clash_no_skip_config_id.no_skip_domains.split(';')
  153. except:
  154. raise UserError('Please enter the node name to skip, separated by semicolons.')
  155. # 查看是否存在不需要跳过的节点, 如果是, 查看有没有使用过这个节点, 如果使用过, 就跳过
  156. # 如果没有使用过, 则使用这个节点, 之后将这个节点添加到已使用列表中
  157. # 判定节点的时候, 部分节点是英文, 所以需要统一一下大小写
  158. selected = False
  159. for no_skip_node in no_skip_node_list:
  160. if line.name in selected_node_list:
  161. break
  162. if no_skip_node.strip().lower() in line.name.lower():
  163. self._use_select_node(line)
  164. selected_node_list.append(line.name)
  165. selected = True
  166. break
  167. if selected:
  168. break
  169. else:
  170. # 如果跳过节点的条件为空, 则判断是否使用过这个节点, 没有就使用
  171. if line.name in selected_node_list:
  172. continue
  173. self._use_select_node(line)
  174. selected_node_list.append(line.name)
  175. break
  176. def btn_check_current_node(self):
  177. for rec in self:
  178. if not rec.localhost_ip or not rec.line_ids or not rec.current_node:
  179. continue
  180. url = rec.localhost_ip
  181. if 'https' in url:
  182. raise UserError('Local network services do not require HTTPS.')
  183. if 'http' not in url:
  184. url = 'http://' + url
  185. result = self._check_node(quote(rec.current_node, safe=""), url)
  186. if result != 9999:
  187. rec.current_node_state = ';'.join([f'{k}:{v}' for k, v in result.items()])
  188. else:
  189. rec.current_node_state = 'error'
  190. def _set_global_proxy(self, url, rec):
  191. setting_url = url + '/api/configs'
  192. headers = {
  193. "Accept": "application/json, text/plain, */*",
  194. "Accept-Encoding": "gzip, deflate",
  195. "Accept-Language": "zh-CN,zh;q=0.8",
  196. "Connection": "keep-alive",
  197. "Content-Type": "application/json",
  198. "Host": rec.localhost_ip,
  199. "Origin": url,
  200. "Referer": url,
  201. "Sec-Gpc": "1",
  202. "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36"
  203. }
  204. data = {
  205. "mode": "Global"
  206. }
  207. # 发送PATCH请求
  208. try:
  209. response = httpx.patch(setting_url, headers=headers, json=data)
  210. if response.status_code != 204:
  211. raise UserError(f"{rec.name} Failed to set global proxy. Status code: {response.status_code}")
  212. except httpx.RequestError as e:
  213. print("Request failed:", e)
  214. def _get_all_node(self, url, rec):
  215. proxies_list_url = url + '/api/proxies'
  216. headers = {
  217. "Accept": "application/json, text/plain, */*",
  218. "Accept-Encoding": "gzip, deflate",
  219. "Accept-Language": "zh-CN,zh;q=0.8",
  220. "Connection": "keep-alive",
  221. "Host": rec.localhost_ip,
  222. "Referer": url,
  223. "Sec-Gpc": "1",
  224. "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36"
  225. }
  226. response = httpx.get(proxies_list_url, headers=headers)
  227. proxies_list = response.json()
  228. if self.clash_no_skip_config_id:
  229. try:
  230. skip_node_list = self.clash_no_skip_config_id.no_skip_domains.split(';')
  231. skip_node_set = {node.lower() for node in skip_node_list}
  232. result = [proxies for proxies in proxies_list.get('proxies') if any(skip_node in proxies.lower() for skip_node in skip_node_set)]
  233. except Exception as e:
  234. raise UserError(f'{self.name} Please enter the node name to skip, separated by semicolons.\nerror: {e}')
  235. else:
  236. result = proxies_list.get('proxies', [])
  237. return result
  238. def _check_node(self, encode_proxy_name, url):
  239. command = [
  240. "curl",
  241. "-X", "GET",
  242. f"{url}/api/proxies/{encode_proxy_name}/delay?timeout=5000&url=http:%2F%2Fwww.gstatic.com%2Fgenerate_204"
  243. ]
  244. try:
  245. result = subprocess.run(command, capture_output=True, text=True, check=True)
  246. if 'Timeout' in result.stdout:
  247. return 9999
  248. if 'An error occurred in the delay test' in result.stdout:
  249. return 9999
  250. res = eval(result.stdout)
  251. return res
  252. except subprocess.CalledProcessError as e:
  253. return 9999
  254. def _get_current_node(self):
  255. url = self.localhost_ip
  256. if 'https' in url:
  257. raise UserError('Local network services do not require HTTPS.')
  258. if 'http' not in url:
  259. url = 'http://' + url
  260. headers = {
  261. "Accept": "application/json, text/plain, */*",
  262. "Accept-Encoding": "gzip, deflate, br, zstd",
  263. "Accept-Language": "zh-CN,zh;q=0.8",
  264. "Connection": "keep-alive",
  265. "Host": self.localhost_ip,
  266. "Referer": url,
  267. "Sec-CH-UA": '"Chromium";v="134", "Not:A-Brand";v="24", "Brave";v="134"',
  268. "Sec-CH-UA-Mobile": "?0",
  269. "Sec-CH-UA-Platform": '"macOS"',
  270. "Sec-Fetch-Dest": "empty",
  271. "Sec-Fetch-Mode": "cors",
  272. "Sec-Fetch-Site": "same-origin",
  273. "Sec-GPC": "1",
  274. "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36"
  275. }
  276. try:
  277. response = httpx.get(url + '/api/proxies', headers=headers)
  278. if not response.json() or response.status_code != 200:
  279. print("JSON data is empty or request failed")
  280. return ''
  281. json_data = response.json()
  282. proxies = json_data.get("proxies")
  283. proxy_global = proxies.get("GLOBAL")
  284. now_proxy = proxy_global.get("now")
  285. return now_proxy
  286. except httpx.RequestError as e:
  287. print(f"Request failed: {e}")
  288. return ''
  289. def _use_select_node(self, line):
  290. url = line.clash_tools_id.localhost_ip
  291. if 'https' in url:
  292. raise UserError('Local network services do not require HTTPS.')
  293. if 'http' not in url:
  294. url = 'http://' + url
  295. url = url + "/api/proxies/GLOBAL"
  296. data = {"name": line.name}
  297. try:
  298. response = httpx.put(url, json=data)
  299. if response.status_code == 204:
  300. print(f"{line.clash_tools_id.localhost_ip} Switched to proxy: {line.name}")
  301. line.clash_tools_id.update({'current_node': line.clash_tools_id._get_current_node()})
  302. else:
  303. print(f"Failed to switch proxy: {response.status_code} - {line.name}")
  304. except Exception as e:
  305. print(f"Failed to switch proxy: {e}")
  306. class ClashToolsLine(models.Model):
  307. _name = 'clash.tools.line'
  308. _description = 'Clash Tools Line'
  309. _order = 'delay ASC'
  310. name = fields.Char('Name')
  311. delay = fields.Integer('Delay')
  312. mean_delay = fields.Integer('Mean Delay')
  313. node_state = fields.Selection([
  314. ('error', 'Error'),
  315. ('ok', 'OK'),
  316. ], string='Node State', default='')
  317. clash_tools_id = fields.Many2one('clash.tools', string='Clash Tools')
  318. def btn_use_this_node(self):
  319. url = self.clash_tools_id.localhost_ip
  320. if 'https' in url:
  321. raise UserError('Local network services do not require HTTPS.')
  322. if 'http' not in url:
  323. url = 'http://' + url
  324. url = url + "/api/proxies/GLOBAL"
  325. data = {"name": self.name}
  326. try:
  327. response = httpx.put(url, json=data)
  328. if response.status_code == 204:
  329. print(f"{self.clash_tools_id.localhost_ip} Switched to proxy: {self.name}")
  330. self.clash_tools_id.update({'current_node': self.clash_tools_id._get_current_node()})
  331. else:
  332. print(f"Failed to switch proxy: {response.status_code} - {self.name}")
  333. except Exception as e:
  334. print(f"Failed to switch proxy: {e}")
  335. def check_single_node(self):
  336. url = self.clash_tools_id.localhost_ip
  337. if 'https' in url:
  338. raise UserError('Local network services do not require HTTPS.')
  339. if 'http' not in url:
  340. url = 'http://' + url
  341. encode_proxy_name = quote(self.name, safe="")
  342. command = [
  343. "curl",
  344. "-X", "GET",
  345. f"{url}/api/proxies/{encode_proxy_name}/delay?timeout=5000&url=http:%2F%2Fwww.gstatic.com%2Fgenerate_204"
  346. ]
  347. try:
  348. result = subprocess.run(command, capture_output=True, text=True, check=True)
  349. if 'Timeout' in result.stdout:
  350. res = 9999
  351. if 'An error occurred in the delay test' in result.stdout:
  352. res = 9999
  353. res = eval(result.stdout)
  354. except subprocess.CalledProcessError as e:
  355. res = 9999
  356. if res != 9999:
  357. self.update({
  358. 'delay': res.setdefault('delay'),
  359. 'mean_delay': res.setdefault('meanDelay'),
  360. 'node_state': 'ok'
  361. })
  362. else:
  363. self.update({
  364. 'delay': res,
  365. 'mean_delay': res,
  366. 'node_state': 'error'
  367. })
  368. class ClashSkipConfig(models.Model):
  369. _name = 'clash.no_skip.config'
  370. _description = 'Clash No Skip Config'
  371. name = fields.Char('Name')
  372. no_skip_domains = fields.Char('No Skip Domains')
  373. @api.depends('name', 'no_skip_domains')
  374. def _compute_display_name(self):
  375. for record in self:
  376. record.display_name = f'{record.name} - {record.no_skip_domains}'