ddns 4.0.2__py2.py3-none-any.whl → 4.1.0b2__py2.py3-none-any.whl

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.
ddns/provider/alidns.py CHANGED
@@ -2,182 +2,150 @@
2
2
  """
3
3
  AliDNS API
4
4
  阿里DNS解析操作库
5
- https://help.aliyun.com/document_detail/29739.html
6
- @author: New Future
5
+ @author: NewFuture
7
6
  """
8
7
 
9
- from hashlib import sha1
10
- from hmac import new as hmac
11
- from uuid import uuid4
12
- from base64 import b64encode
13
- from json import loads as jsondecode
14
- from logging import debug, info, warning
15
- from datetime import datetime
8
+ from ._base import TYPE_FORM, BaseProvider, join_domain, encode_params
9
+ from ._signature import hmac_sha256_authorization, sha256_hash
10
+ from time import strftime, gmtime, time
16
11
 
17
- try: # python 3
18
- from http.client import HTTPSConnection
19
- from urllib.parse import urlencode, quote_plus, quote
20
- except ImportError: # python 2
21
- from httplib import HTTPSConnection
22
- from urllib import urlencode, quote_plus, quote
23
12
 
24
- __author__ = 'New Future'
25
- # __all__ = ["request", "ID", "TOKEN", "PROXY"]
13
+ class AliBaseProvider(BaseProvider):
14
+ """阿里云基础Provider,提供通用的_request方法"""
26
15
 
16
+ endpoint = "https://alidns.aliyuncs.com"
17
+ content_type = TYPE_FORM # 阿里云DNS API使用表单格式
18
+ api_version = "2015-01-09" # API版本,v3签名需要
27
19
 
28
- class Config:
29
- ID = "id"
30
- TOKEN = "TOKEN"
31
- PROXY = None # 代理设置
32
- TTL = None
20
+ def _request(self, action, method="POST", **params):
21
+ # type: (str, str, **(Any)) -> dict
22
+ """Aliyun v3 https://help.aliyun.com/zh/sdk/product-overview/v3-request-structure-and-signature"""
23
+ params = {k: v for k, v in params.items() if v is not None}
33
24
 
34
-
35
- class API:
36
- # API 配置
37
- SITE = "alidns.aliyuncs.com" # API endpoint
38
- METHOD = "POST" # 请求方法
39
-
40
-
41
- def signature(params):
42
- """
43
- 计算签名,返回签名后的查询参数
44
- """
45
- params.update({
46
- 'Format': 'json',
47
- 'Version': '2015-01-09',
48
- 'AccessKeyId': Config.ID,
49
- 'Timestamp': datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ'),
50
- 'SignatureMethod': 'HMAC-SHA1',
51
- 'SignatureNonce': uuid4(),
52
- 'SignatureVersion': "1.0",
53
- })
54
- query = urlencode(sorted(params.items()))
55
- query = query.replace('+', '%20')
56
- debug(query)
57
- sign = API.METHOD + "&" + quote_plus("/") + "&" + quote(query, safe='')
58
- debug("signString: %s", sign)
59
-
60
- sign = hmac((Config.TOKEN + "&").encode('utf-8'),
61
- sign.encode('utf-8'), sha1).digest()
62
- sign = b64encode(sign).strip()
63
- params["Signature"] = sign
64
- return params
65
-
66
-
67
- def request(param=None, **params):
68
- """
69
- 发送请求数据
70
- """
71
- if param:
72
- params.update(param)
73
- params = dict((k, params[k]) for k in params if params[k] is not None)
74
- params = signature(params)
75
- info("%s: %s", API.SITE, params)
76
-
77
- if Config.PROXY:
78
- conn = HTTPSConnection(Config.PROXY)
79
- conn.set_tunnel(API.SITE, 443)
80
- else:
81
- conn = HTTPSConnection(API.SITE)
82
- conn.request(API.METHOD, '/', urlencode(params),
83
- {"Content-type": "application/x-www-form-urlencoded"})
84
- response = conn.getresponse()
85
- data = response.read().decode('utf8')
86
- conn.close()
87
-
88
- if response.status < 200 or response.status >= 300:
89
- warning('%s : error[%d]: %s', params['Action'], response.status, data)
90
- raise Exception(data)
91
- else:
92
- data = jsondecode(data)
93
- debug('%s : result:%s', params['Action'], data)
94
- return data
95
-
96
-
97
- def get_domain_info(domain):
98
- """
99
- 切割域名获取主域名和对应ID
100
- https://help.aliyun.com/document_detail/29755.html
101
- http://alidns.aliyuncs.com/?Action=GetMainDomainName&InputString=www.example.com
102
- """
103
- res = request(Action="GetMainDomainName", InputString=domain)
104
- sub, main = res.get('RR'), res.get('DomainName')
105
- return sub, main
106
-
107
-
108
- def get_records(domain, **conditions):
109
- """
110
- 获取记录ID
111
- 返回满足条件的所有记录[]
112
- https://help.aliyun.com/document_detail/29776.html
113
- TODO 大于500翻页
114
- """
115
- if not hasattr(get_records, "records"):
116
- get_records.records = {} # "静态变量"存储已查询过的id
117
- get_records.keys = ("RecordId", "RR", "Type", "Line",
118
- "Locked", "Status", "Priority", "Value")
119
-
120
- if domain not in get_records.records:
121
- get_records.records[domain] = {}
122
- data = request(Action="DescribeDomainRecords",
123
- DomainName=domain, PageSize=500)
124
- if data:
125
- for record in data.get('DomainRecords').get('Record'):
126
- get_records.records[domain][record["RecordId"]] = {
127
- k: v for (k, v) in record.items() if k in get_records.keys}
128
- records = {}
129
- for (rid, record) in get_records.records[domain].items():
130
- for (k, value) in conditions.items():
131
- if record.get(k) != value:
132
- break
133
- else: # for else push
134
- records[rid] = record
135
- return records
136
-
137
-
138
- def update_record(domain, value, record_type='A'):
139
- """
140
- 更新记录
141
- update
142
- https://help.aliyun.com/document_detail/29774.html
143
- add
144
- https://help.aliyun.com/document_detail/29772.html?
145
- """
146
- debug(">>>>>%s(%s)", domain, record_type)
147
- sub, main = get_domain_info(domain)
148
- if not sub:
149
- raise Exception("invalid domain: [ %s ] " % domain)
150
-
151
- records = get_records(main, RR=sub, Type=record_type)
152
- result = {}
153
-
154
- if records:
155
- for (rid, record) in records.items():
156
- if record["Value"] != value:
157
- debug(sub, record)
158
- res = request(Action="UpdateDomainRecord", RecordId=rid,
159
- Value=value, RR=sub, Type=record_type, TTL=Config.TTL)
160
- if res:
161
- # update records
162
- get_records.records[main][rid]["Value"] = value
163
- result[rid] = res
164
- else:
165
- result[rid] = "update fail!\n" + str(res)
166
- else:
167
- result[rid] = domain
168
- else: # https://help.aliyun.com/document_detail/29772.html
169
- res = request(Action="AddDomainRecord", DomainName=main,
170
- Value=value, RR=sub, Type=record_type, TTL=Config.TTL)
171
- if res:
172
- # update records INFO
173
- rid = res.get('RecordId')
174
- get_records.records[main][rid] = {
175
- 'Value': value,
176
- "RecordId": rid,
177
- "RR": sub,
178
- "Type": record_type
179
- }
180
- result = res
25
+ if method in ("GET", "DELETE"):
26
+ # For GET and DELETE requests, parameters go in query string
27
+ query_string = encode_params(params) if len(params) > 0 else ""
28
+ body_content = ""
29
+ else:
30
+ # For POST requests, parameters go in body
31
+ body_content = self._encode_body(params)
32
+ query_string = ""
33
+
34
+ path = "/"
35
+ content_hash = sha256_hash(body_content)
36
+ # 构造请求头部
37
+ headers = {
38
+ "host": self.endpoint.split("://", 1)[1].strip("/"),
39
+ "content-type": self.content_type,
40
+ "x-acs-action": action,
41
+ "x-acs-content-sha256": content_hash,
42
+ "x-acs-date": strftime("%Y-%m-%dT%H:%M:%SZ", gmtime()),
43
+ "x-acs-signature-nonce": str(hash(time()))[2:],
44
+ "x-acs-version": self.api_version,
45
+ }
46
+
47
+ # 使用通用签名函数
48
+ authorization = hmac_sha256_authorization(
49
+ secret_key=self.token,
50
+ method=method,
51
+ path=path,
52
+ query=query_string,
53
+ headers=headers,
54
+ body_hash=content_hash,
55
+ signing_string_format="ACS3-HMAC-SHA256\n{HashedCanonicalRequest}",
56
+ authorization_format=(
57
+ "ACS3-HMAC-SHA256 Credential=" + self.id + ",SignedHeaders={SignedHeaders},Signature={Signature}"
58
+ ),
59
+ )
60
+ headers["Authorization"] = authorization
61
+ # 对于v3签名的RPC API,参数在request body中
62
+ path = path if not query_string else path + "?" + format(query_string)
63
+ return self._http(method, path, body=body_content, headers=headers)
64
+
65
+
66
+ class AlidnsProvider(AliBaseProvider):
67
+ """阿里云DNS Provider"""
68
+
69
+ def _split_zone_and_sub(self, domain):
70
+ # type: (str) -> tuple[str | None, str | None, str]
71
+ """
72
+ AliDNS 支持直接查询主域名和RR,无需循环查询。
73
+ 返回没有DomainId,用DomainName代替
74
+ https://help.aliyun.com/zh/dns/api-alidns-2015-01-09-getmaindomainname
75
+ """
76
+ res = self._request("GetMainDomainName", InputString=domain)
77
+ sub, main = res.get("RR"), res.get("DomainName")
78
+ return (main, sub, main or domain)
79
+
80
+ def _query_zone_id(self, domain):
81
+ """调用_split_zone_and_sub可直接获取,无需调用_query_zone_id"""
82
+ raise NotImplementedError("_split_zone_and_sub is used to get zone_id")
83
+
84
+ def _query_record(self, zone_id, subdomain, main_domain, record_type, line, extra):
85
+ """https://help.aliyun.com/zh/dns/api-alidns-2015-01-09-describesubdomainrecords"""
86
+ sub = join_domain(subdomain, main_domain)
87
+ data = self._request(
88
+ "DescribeSubDomainRecords",
89
+ SubDomain=sub, # aliyun API要求SubDomain为完整域名
90
+ DomainName=main_domain,
91
+ Type=record_type,
92
+ Line=line,
93
+ PageSize=500,
94
+ Lang=extra.get("Lang"), # 默认中文
95
+ Status=extra.get("Status"), # 默认全部状态
96
+ )
97
+ records = data.get("DomainRecords", {}).get("Record", [])
98
+ if not records:
99
+ self.logger.warning(
100
+ "No records found for [%s] with %s <%s> (line: %s)", zone_id, subdomain, record_type, line
101
+ )
102
+ elif not isinstance(records, list):
103
+ self.logger.error("Invalid records format: %s", records)
181
104
  else:
182
- result = domain + " created fail!"
183
- return result
105
+ return next((r for r in records), None)
106
+ return None
107
+
108
+ def _create_record(self, zone_id, subdomain, main_domain, value, record_type, ttl, line, extra):
109
+ """https://help.aliyun.com/zh/dns/api-alidns-2015-01-09-adddomainrecord"""
110
+ data = self._request(
111
+ "AddDomainRecord",
112
+ DomainName=main_domain,
113
+ RR=subdomain,
114
+ Value=value,
115
+ Type=record_type,
116
+ TTL=ttl,
117
+ Line=line,
118
+ **extra
119
+ )
120
+ if data and data.get("RecordId"):
121
+ self.logger.info("Record created: %s", data)
122
+ return True
123
+ self.logger.error("Failed to create record: %s", data)
124
+ return False
125
+
126
+ def _update_record(self, zone_id, old_record, value, record_type, ttl, line, extra):
127
+ """https://help.aliyun.com/zh/dns/api-alidns-2015-01-09-updatedomainrecord"""
128
+ # 阿里云DNS update新旧值不能一样,先判断是否发生变化
129
+ if (
130
+ old_record.get("Value") == value
131
+ and old_record.get("Type") == record_type
132
+ and (not ttl or old_record.get("TTL") == ttl)
133
+ ):
134
+ domain = join_domain(old_record.get("RR"), old_record.get("DomainName"))
135
+ self.logger.warning("No changes detected, skipping update for record: %s", domain)
136
+ return True
137
+ data = self._request(
138
+ "UpdateDomainRecord",
139
+ RecordId=old_record.get("RecordId"),
140
+ Value=value,
141
+ RR=old_record.get("RR"),
142
+ Type=record_type,
143
+ TTL=ttl,
144
+ Line=line or old_record.get("Line"),
145
+ **extra
146
+ )
147
+ if data and data.get("RecordId"):
148
+ self.logger.info("Record updated: %s", data)
149
+ return True
150
+ self.logger.error("Failed to update record: %s", data)
151
+ return False
@@ -0,0 +1,129 @@
1
+ # coding=utf-8
2
+ """
3
+ AliESA API
4
+ 阿里云边缘安全加速(ESA) DNS 解析操作库
5
+ @author: NewFuture, GitHub Copilot
6
+ """
7
+
8
+ from .alidns import AliBaseProvider
9
+ from ._base import join_domain, TYPE_JSON
10
+ from time import strftime
11
+
12
+
13
+ class AliesaProvider(AliBaseProvider):
14
+ """阿里云边缘安全加速(ESA) DNS Provider"""
15
+
16
+ endpoint = "https://esa.cn-hangzhou.aliyuncs.com"
17
+ api_version = "2024-09-10" # ESA API版本
18
+ content_type = TYPE_JSON
19
+ remark = "Managed by DDNS %s" % strftime("%Y-%m-%d %H:%M:%S")
20
+
21
+ def _query_zone_id(self, domain):
22
+ # type: (str) -> str | None
23
+ """
24
+ 查询站点ID
25
+ https://help.aliyun.com/zh/edge-security-acceleration/esa/api-esa-2024-09-10-listsites
26
+ """
27
+ res = self._request(method="GET", action="ListSites", SiteName=domain, PageSize=500)
28
+ sites = res.get("Sites", [])
29
+
30
+ for site in sites:
31
+ if site.get("SiteName") == domain:
32
+ site_id = site.get("SiteId")
33
+ self.logger.debug("Found site ID %s for domain %s", site_id, domain)
34
+ return site_id
35
+
36
+ self.logger.error("Site not found for domain: %s", domain)
37
+ return None
38
+
39
+ def _query_record(self, zone_id, subdomain, main_domain, record_type, line, extra):
40
+ # type: (str, str, str, str, str | None, dict) -> dict | None
41
+ """
42
+ 查询DNS记录
43
+ https://help.aliyun.com/zh/edge-security-acceleration/esa/api-esa-2024-09-10-listrecords
44
+ """
45
+ full_domain = join_domain(subdomain, main_domain)
46
+ res = self._request(
47
+ method="GET",
48
+ action="ListRecords",
49
+ SiteId=int(zone_id),
50
+ RecordName=full_domain,
51
+ Type=self._get_type(record_type),
52
+ RecordMatchType="exact", # 精确匹配
53
+ PageSize=100,
54
+ )
55
+
56
+ records = res.get("Records", [])
57
+ if len(records) == 0:
58
+ self.logger.warning("No records found for [%s] with %s <%s>", zone_id, full_domain, record_type)
59
+ return None
60
+
61
+ # 返回第一个匹配的记录
62
+ record = records[0]
63
+ self.logger.debug("Found record: %s", record)
64
+ return record
65
+
66
+ def _create_record(self, zone_id, subdomain, main_domain, value, record_type, ttl, line, extra):
67
+ # type: (str, str, str, str, str, int | str | None, str | None, dict) -> bool
68
+ """
69
+ 创建DNS记录
70
+ https://help.aliyun.com/zh/edge-security-acceleration/esa/api-esa-2024-09-10-createrecord
71
+ """
72
+ full_domain = join_domain(subdomain, main_domain)
73
+ extra["Comment"] = extra.get("Comment", self.remark)
74
+ extra["BizName"] = extra.get("BizName", "web")
75
+ extra["Proxied"] = extra.get("Proxied", True)
76
+ data = self._request(
77
+ method="POST",
78
+ action="CreateRecord",
79
+ SiteId=int(zone_id),
80
+ RecordName=full_domain,
81
+ Type=self._get_type(record_type),
82
+ Data={"Value": value},
83
+ Ttl=ttl or 1,
84
+ **extra
85
+ )
86
+
87
+ if data and data.get("RecordId"):
88
+ self.logger.info("Record created: %s", data)
89
+ return True
90
+
91
+ self.logger.error("Failed to create record: %s", data)
92
+ return False
93
+
94
+ def _update_record(self, zone_id, old_record, value, record_type, ttl, line, extra):
95
+ # type: (str, dict, str, str, int | str | None, str | None, dict) -> bool
96
+ """
97
+ 更新DNS记录
98
+ https://help.aliyun.com/zh/edge-security-acceleration/esa/api-esa-2024-09-10-updaterecord
99
+ """
100
+ # 检查是否需要更新
101
+ if (
102
+ old_record.get("Data", {}).get("Value") == value
103
+ and old_record.get("RecordType") == self._get_type(record_type)
104
+ and (not ttl or old_record.get("Ttl") == ttl)
105
+ ):
106
+ self.logger.warning("No changes detected, skipping update for record: %s", old_record.get("RecordName"))
107
+ return True
108
+
109
+ extra["Comment"] = extra.get("Comment", self.remark)
110
+ extra["Proxied"] = extra.get("Proxied", old_record.get("Proxied"))
111
+ data = self._request(
112
+ method="POST",
113
+ action="UpdateRecord",
114
+ RecordId=old_record.get("RecordId"),
115
+ Data={"Value": value},
116
+ Ttl=ttl,
117
+ **extra
118
+ )
119
+
120
+ if data and data.get("RecordId"):
121
+ self.logger.info("Record updated: %s", data)
122
+ return True
123
+
124
+ self.logger.error("Failed to update record: %s", data)
125
+ return False
126
+
127
+ def _get_type(self, record_type):
128
+ # type: (str) -> str
129
+ return "A/AAAA" if record_type in ("A", "AAAA") else record_type
ddns/provider/callback.py CHANGED
@@ -3,115 +3,76 @@
3
3
  Custom Callback API
4
4
  自定义回调接口解析操作库
5
5
 
6
- @author: 老周部落
6
+ @author: 老周部落, NewFuture
7
7
  """
8
-
9
- from json import loads as jsondecode
10
- from logging import debug, info, warning
8
+ from ._base import TYPE_JSON, SimpleProvider
11
9
  from time import time
12
-
13
- try: # python 3
14
- from http.client import HTTPSConnection, HTTPConnection
15
- from urllib.parse import urlencode, urlparse, parse_qsl
16
- except ImportError: # python 2
17
- from httplib import HTTPSConnection, HTTPConnection
18
- from urlparse import urlparse, parse_qsl
19
- from urllib import urlencode
20
-
21
- __author__ = '老周部落'
22
-
23
-
24
- class Config:
25
- ID = None # 自定义回调 URL
26
- TOKEN = None # 使用 JSON 编码的 POST 参数
27
- PROXY = None # 代理设置
28
- TTL = None
29
-
30
-
31
- def request(method, action, param=None, **params):
32
- """
33
- 发送请求数据
34
- """
35
- if param:
36
- params.update(param)
37
-
38
- URLObj = urlparse(Config.ID)
39
- params = dict((k, params[k]) for k in params if params[k] is not None)
40
- info("%s/%s : %s", URLObj.netloc, action, params)
41
-
42
- if Config.PROXY:
43
- if URLObj.netloc == "http":
44
- conn = HTTPConnection(Config.PROXY)
45
- else:
46
- conn = HTTPSConnection(Config.PROXY)
47
- conn.set_tunnel(URLObj.netloc, URLObj.port)
48
- else:
49
- if URLObj.netloc == "http":
50
- conn = HTTPConnection(URLObj.netloc, URLObj.port)
51
- else:
52
- conn = HTTPSConnection(URLObj.netloc, URLObj.port)
53
-
54
- headers = {}
55
-
56
- if method == "GET":
57
- if params:
58
- action += '?' + urlencode(params)
59
- params = ""
60
- else:
61
- headers["Content-Type"] = "application/x-www-form-urlencoded"
62
-
63
- params = urlencode(params)
64
-
65
- conn.request(method, action, params, headers)
66
- response = conn.getresponse()
67
- res = response.read().decode('utf8')
68
- conn.close()
69
- if response.status < 200 or response.status >= 300:
70
- warning('%s : error[%d]:%s', action, response.status, res)
71
- raise Exception(res)
72
- else:
73
- debug('%s : result:%s', action, res)
74
- return res
75
-
76
-
77
- def replace_params(domain, record_type, ip, params):
78
- """
79
- 替换定义常量为实际值
80
- """
81
- dict = {"__DOMAIN__": domain, "__RECORDTYPE__": record_type,
82
- "__TTL__": Config.TTL, "__TIMESTAMP__": time(), "__IP__": ip}
83
- for key, value in params.items():
84
- if dict.get(value):
85
- params[key] = dict.get(value)
86
- return params
10
+ from json import loads as jsondecode
87
11
 
88
12
 
89
- def update_record(domain, value, record_type="A"):
13
+ class CallbackProvider(SimpleProvider):
90
14
  """
91
- 更新记录
15
+ 通用自定义回调 Provider,支持 GET/POST 任意接口。
16
+ Generic custom callback provider, supports GET/POST arbitrary API.
92
17
  """
93
- info(">>>>>%s(%s)", domain, record_type)
94
-
95
- result = {}
96
-
97
- if not Config.TOKEN: # 此处使用 TOKEN 参数透传 POST 参数所用的 JSON
98
- method = "GET"
99
- URLObj = urlparse(Config.ID)
100
- path = URLObj.path
101
- query = dict(parse_qsl(URLObj.query))
102
- params = replace_params(domain, record_type, value, query)
103
- else:
104
- method = "POST"
105
- URLObj = urlparse(Config.ID)
106
- path = URLObj.path
107
- params = replace_params(domain, record_type,
108
- value, jsondecode(Config.TOKEN))
109
-
110
- res = request(method, path, params)
111
-
112
- if res:
113
- result = "Callback Request Success!\n" + res
114
- else:
115
- result = "Callback Request Fail!\n"
116
18
 
117
- return result
19
+ endpoint = "" # CallbackProvider uses id as URL, no fixed API endpoint
20
+ content_type = TYPE_JSON
21
+ decode_response = False # Callback response is not JSON, it's a custom response
22
+
23
+ def set_record(self, domain, value, record_type="A", ttl=None, line=None, **extra):
24
+ """
25
+ 发送自定义回调请求,支持 GET/POST
26
+ Send custom callback request, support GET/POST
27
+ """
28
+ self.logger.info("%s => %s(%s)", domain, value, record_type)
29
+ url = self.id # 直接用 id 作为 url
30
+ token = self.token # token 作为 POST 参数
31
+ extra.update(
32
+ {
33
+ "__DOMAIN__": domain,
34
+ "__RECORDTYPE__": record_type,
35
+ "__TTL__": ttl,
36
+ "__IP__": value,
37
+ "__TIMESTAMP__": time(),
38
+ "__LINE__": line,
39
+ }
40
+ )
41
+ url = self._replace_vars(url, extra)
42
+ method, params = "GET", None
43
+ if token:
44
+ # 如果有 token,使用 POST 方法
45
+ method = "POST"
46
+ # POST 方式,token 作为 POST 参数
47
+ params = token if isinstance(token, dict) else jsondecode(token)
48
+ for k, v in params.items():
49
+ if hasattr(v, "replace"): # 判断是否支持字符串替换, 兼容py2,py3
50
+ params[k] = self._replace_vars(v, extra)
51
+
52
+ try:
53
+ res = self._http(method, url, body=params)
54
+ if res is not None:
55
+ self.logger.info("Callback result: %s", res)
56
+ return True
57
+ else:
58
+ self.logger.warning("Callback received empty response.")
59
+ except Exception as e:
60
+ self.logger.error("Callback failed: %s", e)
61
+ return False
62
+
63
+ def _replace_vars(self, string, mapping):
64
+ # type: (str, dict) -> str
65
+ """
66
+ 替换字符串中的变量为实际值
67
+ Replace variables in string with actual values
68
+ """
69
+ for k, v in mapping.items():
70
+ string = string.replace(k, str(v))
71
+ return string
72
+
73
+ def _validate(self):
74
+ # CallbackProvider uses id as URL, not as regular ID
75
+ if self.endpoint or (not self.id or "://" not in self.id):
76
+ # 如果 endpoint 已经设置,或者 id 不是有效的 URL,则抛出异常
77
+ self.logger.critical("endpoint [%s] or id [%s] 必须是有效的URL", self.endpoint, self.id)
78
+ raise ValueError("endpoint or id must be configured with URL")