mcp-server-sms 0.1.0__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.
- mcp_server_sms/__init__.py +3 -0
- mcp_server_sms/auth.py +46 -0
- mcp_server_sms/client.py +309 -0
- mcp_server_sms/models.py +132 -0
- mcp_server_sms/server.py +555 -0
- mcp_server_sms-0.1.0.dist-info/METADATA +110 -0
- mcp_server_sms-0.1.0.dist-info/RECORD +10 -0
- mcp_server_sms-0.1.0.dist-info/WHEEL +4 -0
- mcp_server_sms-0.1.0.dist-info/entry_points.txt +2 -0
- mcp_server_sms-0.1.0.dist-info/licenses/LICENSE +202 -0
mcp_server_sms/auth.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Per-call credentials. HTTP never falls back to the server's environment."""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import binascii
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from collections.abc import Mapping
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AuthenticationError(ValueError):
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True, repr=False)
|
|
16
|
+
class Credentials:
|
|
17
|
+
access_key: str
|
|
18
|
+
secret_key: str
|
|
19
|
+
session_token: str = ""
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def from_headers(cls, headers: Mapping[str, str] | None):
|
|
23
|
+
if headers is None:
|
|
24
|
+
values = {
|
|
25
|
+
"AccessKeyId": os.getenv("VOLCENGINE_ACCESS_KEY"),
|
|
26
|
+
"SecretAccessKey": os.getenv("VOLCENGINE_SECRET_KEY"),
|
|
27
|
+
"SessionToken": os.getenv("VOLCENGINE_SESSION_TOKEN", ""),
|
|
28
|
+
}
|
|
29
|
+
else:
|
|
30
|
+
authorization = headers.get("authorization", "")
|
|
31
|
+
scheme, separator, encoded = authorization.partition(" ")
|
|
32
|
+
if not separator or scheme.lower() != "bearer" or len(encoded) > 16384:
|
|
33
|
+
raise AuthenticationError("需要 Authorization: Bearer <Base64 JSON 短信凭据>")
|
|
34
|
+
try:
|
|
35
|
+
values = json.loads(base64.b64decode(encoded, validate=True))
|
|
36
|
+
except (ValueError, UnicodeError, binascii.Error):
|
|
37
|
+
raise AuthenticationError("短信调用凭据格式无效") from None
|
|
38
|
+
if not isinstance(values, dict) or any(
|
|
39
|
+
not isinstance(values.get(key), str) or not values[key]
|
|
40
|
+
for key in ("AccessKeyId", "SecretAccessKey")
|
|
41
|
+
):
|
|
42
|
+
raise AuthenticationError("需要完整的短信 AccessKeyId 和 SecretAccessKey")
|
|
43
|
+
token = values.get("SessionToken", "")
|
|
44
|
+
if not isinstance(token, str):
|
|
45
|
+
raise AuthenticationError("SessionToken 必须是字符串")
|
|
46
|
+
return cls(values["AccessKeyId"], values["SecretAccessKey"], token)
|
mcp_server_sms/client.py
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
"""SMS HTTP boundary: official V4 signing, one request, structured outcomes."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from http.cookiejar import CookieJar, DefaultCookiePolicy
|
|
6
|
+
from typing import Any, Literal
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
from volcengine.auth.SignerV4 import SignerV4
|
|
10
|
+
from volcengine.base.Request import Request
|
|
11
|
+
from volcengine.Credentials import Credentials as SigningCredentials
|
|
12
|
+
|
|
13
|
+
from .auth import Credentials
|
|
14
|
+
|
|
15
|
+
API_HOST = "sms.volcengineapi.com"
|
|
16
|
+
API_VERSION = "2026-01-01"
|
|
17
|
+
API_REGION = "cn-north-1"
|
|
18
|
+
API_SERVICE = "volcSMS"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class Action:
|
|
23
|
+
method: Literal["GET", "POST"]
|
|
24
|
+
write: bool = False
|
|
25
|
+
result_format: Literal["json", "csv", "id"] = "json"
|
|
26
|
+
required_any: tuple[str, ...] = ()
|
|
27
|
+
required_all: tuple[str, ...] = ()
|
|
28
|
+
opaque_result_fields: tuple[str, ...] = ()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# Only named tools can select an action; callers cannot supply arbitrary APIs.
|
|
32
|
+
ACTIONS = {
|
|
33
|
+
"ListSubAccountForAgent": Action("POST"),
|
|
34
|
+
"GetSubAccountDetail": Action("GET"),
|
|
35
|
+
"ListAllSmsProduct": Action("GET"),
|
|
36
|
+
"GetAccountIdentRankForAgent": Action("GET"),
|
|
37
|
+
"GetSignatureIdentificationList": Action("POST"),
|
|
38
|
+
"GetOCRLicenseForAgent": Action("POST"),
|
|
39
|
+
"ThreeElementEnterpriseCheckForAgent": Action("POST"),
|
|
40
|
+
"ThreeElementPersonCheckForAgent": Action("POST"),
|
|
41
|
+
"SendSmsVerifyCodeByMobile": Action("POST", True, required_any=("messageId",)),
|
|
42
|
+
"CheckSmsVerifyCodeByMobile": Action("POST", True, required_any=("status",)),
|
|
43
|
+
"ApplySignatureIdentificationForAgent": Action("POST", True, "id"),
|
|
44
|
+
"ListSignatureForAgent": Action("POST"),
|
|
45
|
+
"ApplySmsSignatureV2": Action("POST", True, required_any=("applyId", "status")),
|
|
46
|
+
"ListSmsTemplateForAgent": Action("POST"),
|
|
47
|
+
"ListSecondTemplate": Action("GET"),
|
|
48
|
+
"ApplySmsTemplateV2": Action("POST", True, required_any=("templateId", "status")),
|
|
49
|
+
"SendSmsForAgent": Action("POST", True, required_any=("MessageId", "MessageIds")),
|
|
50
|
+
"ListSmsSendLogForAgent": Action("POST"),
|
|
51
|
+
"ListTotalSendCountStatForAgent": Action("POST"),
|
|
52
|
+
"GetUploadTosURL": Action(
|
|
53
|
+
"GET", True, required_all=("file", "url"), opaque_result_fields=("url",)
|
|
54
|
+
),
|
|
55
|
+
"TemplateUploadDemo": Action("POST", result_format="csv"),
|
|
56
|
+
"SetBatchTask": Action("POST", True, required_any=("taskId",)),
|
|
57
|
+
"GetBatchTaskDetail": Action("GET"),
|
|
58
|
+
"GetBatchTaskList": Action("GET"),
|
|
59
|
+
"ConsentBatchTask": Action("POST", True),
|
|
60
|
+
"DeleteBatchTask": Action("POST", True),
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
_CREDENTIAL_FIELDS = frozenset(
|
|
64
|
+
{
|
|
65
|
+
"accesskeyid",
|
|
66
|
+
"accesskey",
|
|
67
|
+
"secretaccesskey",
|
|
68
|
+
"secretkey",
|
|
69
|
+
"sessiontoken",
|
|
70
|
+
"securitytoken",
|
|
71
|
+
"authorization",
|
|
72
|
+
"cookie",
|
|
73
|
+
"set-cookie",
|
|
74
|
+
}
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# These status codes explicitly reject the request. Unstructured timeout,
|
|
78
|
+
# conflict and gateway-specific responses remain uncertain for mutations.
|
|
79
|
+
_REJECTED_HTTP_STATUSES = frozenset({400, 401, 403, 404, 405, 413, 415, 422, 429})
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class _RejectCookies(DefaultCookiePolicy):
|
|
83
|
+
"""Connection pooling must not introduce cross-caller cookie state."""
|
|
84
|
+
|
|
85
|
+
def set_ok(self, cookie, request):
|
|
86
|
+
return False
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def safe_result(
|
|
90
|
+
value: Any, credentials: Credentials, *, opaque_fields: tuple[str, ...] = ()
|
|
91
|
+
) -> Any:
|
|
92
|
+
"""Redact credentials while preserving declared top-level API references."""
|
|
93
|
+
if isinstance(value, dict):
|
|
94
|
+
return {
|
|
95
|
+
key: item if key in opaque_fields else safe_result(item, credentials)
|
|
96
|
+
for key, item in value.items()
|
|
97
|
+
if key.lower() not in _CREDENTIAL_FIELDS
|
|
98
|
+
}
|
|
99
|
+
if isinstance(value, list):
|
|
100
|
+
return [safe_result(item, credentials) for item in value]
|
|
101
|
+
if isinstance(value, str):
|
|
102
|
+
for secret in (credentials.access_key, credentials.secret_key, credentials.session_token):
|
|
103
|
+
if secret:
|
|
104
|
+
value = value.replace(secret, "[REDACTED]")
|
|
105
|
+
return value
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def failure(action, code, message, request_id=None, *, outcome_unknown=False, http_status=None):
|
|
109
|
+
error = {"code": code, "message": message, "outcome_unknown": outcome_unknown}
|
|
110
|
+
if http_status is not None:
|
|
111
|
+
error["http_status"] = http_status
|
|
112
|
+
return {
|
|
113
|
+
"success": False,
|
|
114
|
+
"action": action,
|
|
115
|
+
"request_id": request_id,
|
|
116
|
+
"result": None,
|
|
117
|
+
"error": error,
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class SmsClient:
|
|
122
|
+
def __init__(self, *, transport: httpx.AsyncBaseTransport | None = None, timeout: float = 15):
|
|
123
|
+
self._http = httpx.AsyncClient(
|
|
124
|
+
transport=transport,
|
|
125
|
+
timeout=timeout,
|
|
126
|
+
follow_redirects=False,
|
|
127
|
+
cookies=CookieJar(policy=_RejectCookies()),
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
async def __aenter__(self):
|
|
131
|
+
await self._http.__aenter__()
|
|
132
|
+
return self
|
|
133
|
+
|
|
134
|
+
async def __aexit__(self, exc_type, exc_value, traceback):
|
|
135
|
+
await self._http.__aexit__(exc_type, exc_value, traceback)
|
|
136
|
+
|
|
137
|
+
async def call(self, action: str, params: dict, credentials: Credentials) -> dict:
|
|
138
|
+
spec = ACTIONS[action]
|
|
139
|
+
wire = {key: value for key, value in params.items() if value is not None}
|
|
140
|
+
request = Request()
|
|
141
|
+
request.schema = "https"
|
|
142
|
+
request.set_host(API_HOST)
|
|
143
|
+
request.set_path("/")
|
|
144
|
+
request.set_method(spec.method)
|
|
145
|
+
request.set_headers({"Accept": "application/json", "Content-Type": "application/json"})
|
|
146
|
+
query = {"Action": action, "Version": API_VERSION}
|
|
147
|
+
if spec.method == "GET":
|
|
148
|
+
query.update(
|
|
149
|
+
{
|
|
150
|
+
key: str(value).lower() if isinstance(value, bool) else str(value)
|
|
151
|
+
for key, value in wire.items()
|
|
152
|
+
}
|
|
153
|
+
)
|
|
154
|
+
else:
|
|
155
|
+
request.set_body(json.dumps(wire, ensure_ascii=False, separators=(",", ":")))
|
|
156
|
+
request.set_query(query)
|
|
157
|
+
SignerV4.sign(
|
|
158
|
+
request,
|
|
159
|
+
SigningCredentials(
|
|
160
|
+
credentials.access_key,
|
|
161
|
+
credentials.secret_key,
|
|
162
|
+
API_SERVICE,
|
|
163
|
+
API_REGION,
|
|
164
|
+
credentials.session_token,
|
|
165
|
+
),
|
|
166
|
+
)
|
|
167
|
+
try:
|
|
168
|
+
response = await self._http.request(
|
|
169
|
+
spec.method,
|
|
170
|
+
request.build(),
|
|
171
|
+
headers=request.headers,
|
|
172
|
+
content=request.body.encode("utf-8") if request.body else None,
|
|
173
|
+
)
|
|
174
|
+
except (httpx.ConnectError, httpx.ConnectTimeout, httpx.PoolTimeout):
|
|
175
|
+
return failure(action, "connection_failed", "未建立 API 连接,请检查网络")
|
|
176
|
+
except httpx.HTTPError:
|
|
177
|
+
return failure(
|
|
178
|
+
action,
|
|
179
|
+
"outcome_unknown" if spec.write else "transport_error",
|
|
180
|
+
"请求结果暂时无法确认;写操作不要自动重试",
|
|
181
|
+
outcome_unknown=spec.write,
|
|
182
|
+
)
|
|
183
|
+
return self.decode(action, response, credentials)
|
|
184
|
+
|
|
185
|
+
@staticmethod
|
|
186
|
+
def decode(action: str, response: httpx.Response, credentials: Credentials) -> dict:
|
|
187
|
+
spec = ACTIONS[action]
|
|
188
|
+
request_id = response.headers.get("x-tt-logid") or response.headers.get("x-request-id")
|
|
189
|
+
request_id = safe_result(request_id, credentials)
|
|
190
|
+
try:
|
|
191
|
+
payload = response.json()
|
|
192
|
+
except ValueError:
|
|
193
|
+
content_type = response.headers.get("content-type", "").split(";", 1)[0]
|
|
194
|
+
if (
|
|
195
|
+
spec.result_format == "csv"
|
|
196
|
+
and response.is_success
|
|
197
|
+
and content_type
|
|
198
|
+
in {
|
|
199
|
+
"text/csv",
|
|
200
|
+
"application/csv",
|
|
201
|
+
"application/octet-stream",
|
|
202
|
+
"application/vnd.ms-excel",
|
|
203
|
+
"text/comma-separated-values",
|
|
204
|
+
}
|
|
205
|
+
):
|
|
206
|
+
try:
|
|
207
|
+
result = {
|
|
208
|
+
"content": response.content.decode("utf-8-sig"),
|
|
209
|
+
"contentType": "text/csv",
|
|
210
|
+
}
|
|
211
|
+
except UnicodeError:
|
|
212
|
+
return failure(
|
|
213
|
+
action,
|
|
214
|
+
"invalid_response",
|
|
215
|
+
"CSV 响应不是 UTF-8",
|
|
216
|
+
request_id,
|
|
217
|
+
http_status=response.status_code,
|
|
218
|
+
)
|
|
219
|
+
return {
|
|
220
|
+
"success": True,
|
|
221
|
+
"action": action,
|
|
222
|
+
"request_id": request_id,
|
|
223
|
+
"result": safe_result(result, credentials),
|
|
224
|
+
"error": None,
|
|
225
|
+
}
|
|
226
|
+
payload = None
|
|
227
|
+
metadata = payload.get("ResponseMetadata", {}) if isinstance(payload, dict) else {}
|
|
228
|
+
if isinstance(metadata, dict):
|
|
229
|
+
request_id = metadata.get("RequestId") or request_id
|
|
230
|
+
else:
|
|
231
|
+
metadata = {}
|
|
232
|
+
request_id = safe_result(request_id, credentials)
|
|
233
|
+
if response.status_code >= 500 and spec.write:
|
|
234
|
+
return failure(
|
|
235
|
+
action,
|
|
236
|
+
"outcome_unknown",
|
|
237
|
+
"服务端异常,不能确认写入结果",
|
|
238
|
+
request_id,
|
|
239
|
+
outcome_unknown=True,
|
|
240
|
+
http_status=response.status_code,
|
|
241
|
+
)
|
|
242
|
+
api_error = metadata.get("Error")
|
|
243
|
+
if isinstance(api_error, dict):
|
|
244
|
+
# API error messages can echo identity documents, codes or auth input.
|
|
245
|
+
# Preserve the public error code and RequestId, never echo raw input.
|
|
246
|
+
code = safe_result(str(api_error.get("Code") or "api_error"), credentials)
|
|
247
|
+
return failure(
|
|
248
|
+
action,
|
|
249
|
+
code,
|
|
250
|
+
"短信 API 返回错误,请结合错误码和 RequestId 排查",
|
|
251
|
+
request_id,
|
|
252
|
+
http_status=response.status_code,
|
|
253
|
+
)
|
|
254
|
+
if response.status_code in _REJECTED_HTTP_STATUSES:
|
|
255
|
+
return failure(
|
|
256
|
+
action,
|
|
257
|
+
f"http_{response.status_code}",
|
|
258
|
+
"短信 API 拒绝了请求",
|
|
259
|
+
request_id,
|
|
260
|
+
http_status=response.status_code,
|
|
261
|
+
)
|
|
262
|
+
if not response.is_success or not isinstance(payload, dict) or "Result" not in payload:
|
|
263
|
+
return failure(
|
|
264
|
+
action,
|
|
265
|
+
"outcome_unknown" if spec.write else "invalid_response",
|
|
266
|
+
"未收到可确认的短信 API 响应",
|
|
267
|
+
request_id,
|
|
268
|
+
outcome_unknown=spec.write,
|
|
269
|
+
http_status=response.status_code,
|
|
270
|
+
)
|
|
271
|
+
result = payload["Result"]
|
|
272
|
+
valid = True
|
|
273
|
+
if spec.result_format == "id":
|
|
274
|
+
valid = type(result) is int and result > 0
|
|
275
|
+
elif spec.required_any:
|
|
276
|
+
valid = isinstance(result, dict) and any(
|
|
277
|
+
key in result
|
|
278
|
+
and result[key] is not None
|
|
279
|
+
and result[key] != ""
|
|
280
|
+
and result[key] != []
|
|
281
|
+
for key in spec.required_any
|
|
282
|
+
)
|
|
283
|
+
if spec.required_all:
|
|
284
|
+
valid = (
|
|
285
|
+
valid
|
|
286
|
+
and isinstance(result, dict)
|
|
287
|
+
and all(
|
|
288
|
+
isinstance(result.get(key), str) and bool(result[key])
|
|
289
|
+
for key in spec.required_all
|
|
290
|
+
)
|
|
291
|
+
)
|
|
292
|
+
if not valid:
|
|
293
|
+
return failure(
|
|
294
|
+
action,
|
|
295
|
+
"outcome_unknown" if spec.write else "invalid_response",
|
|
296
|
+
"接口响应缺少必要结果,不能确认操作完成",
|
|
297
|
+
request_id,
|
|
298
|
+
outcome_unknown=spec.write,
|
|
299
|
+
http_status=response.status_code,
|
|
300
|
+
)
|
|
301
|
+
return {
|
|
302
|
+
"success": True,
|
|
303
|
+
"action": action,
|
|
304
|
+
"request_id": request_id,
|
|
305
|
+
# A signed upload URL is an operational capability, not a display
|
|
306
|
+
# string. Editing its query parameters would invalidate the signature.
|
|
307
|
+
"result": safe_result(result, credentials, opaque_fields=spec.opaque_result_fields),
|
|
308
|
+
"error": None,
|
|
309
|
+
}
|
mcp_server_sms/models.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Request schemas for SMS APIs, without drafts or client interaction state."""
|
|
2
|
+
|
|
3
|
+
from typing import Annotated, Literal
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
6
|
+
|
|
7
|
+
Channel = Literal["CN_OTP", "CN_NTC", "CN_MKT"]
|
|
8
|
+
BatchChannel = Literal["CN_NTC", "CN_MKT"]
|
|
9
|
+
Mobile = Annotated[str, Field(pattern=r"^1[3-9][0-9]{9}$")]
|
|
10
|
+
Text = Annotated[str, Field(min_length=1)]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ApiModel(BaseModel):
|
|
14
|
+
model_config = ConfigDict(extra="forbid", populate_by_name=True, hide_input_in_errors=True)
|
|
15
|
+
|
|
16
|
+
def payload(self) -> dict:
|
|
17
|
+
return self.model_dump(by_alias=True, exclude_none=True)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class MaterialFile(ApiModel):
|
|
21
|
+
file_type: int = Field(alias="fileType", ge=0, le=19)
|
|
22
|
+
file_content: str = Field(
|
|
23
|
+
alias="fileContent", repr=False, description="短信材料接口接受的文件引用;不是 MCP fileId"
|
|
24
|
+
)
|
|
25
|
+
file_suffix: str | None = Field(None, alias="fileSuffix")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class BusinessInfo(ApiModel):
|
|
29
|
+
business_certificate_type: Literal[1, 4, 6, 7] = Field(alias="businessCertificateType")
|
|
30
|
+
business_certificate_name: Text = Field(alias="businessCertificateName")
|
|
31
|
+
unified_social_credit_identifier: Text = Field(
|
|
32
|
+
alias="unifiedSocialCreditIdentifier", repr=False
|
|
33
|
+
)
|
|
34
|
+
legal_person_name: Text = Field(alias="legalPersonName", repr=False)
|
|
35
|
+
business_certificate: MaterialFile | None = Field(None, alias="businessCertificate")
|
|
36
|
+
validity_start: str = Field(alias="businessCertificateValidityPeriodStart")
|
|
37
|
+
validity_end: str = Field(alias="businessCertificateValidityPeriodEnd")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class PersonInfo(ApiModel):
|
|
41
|
+
certificate_type: Literal[0, 1, 2, 3, 4, 9] = Field(alias="certificateType")
|
|
42
|
+
person_name: str = Field(alias="personName", repr=False)
|
|
43
|
+
person_id_card: str = Field(alias="personIDCard", repr=False)
|
|
44
|
+
person_mobile: str | None = Field(None, alias="personMobile", repr=False)
|
|
45
|
+
person_certificate: list[MaterialFile] | None = Field(
|
|
46
|
+
None, alias="personCertificate", repr=False
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class QualificationApplication(ApiModel):
|
|
51
|
+
purpose: Literal[1, 2]
|
|
52
|
+
material_name: str = Field(alias="materialName", min_length=1, max_length=20)
|
|
53
|
+
business_info: BusinessInfo = Field(alias="businessInfo")
|
|
54
|
+
operator_person: PersonInfo = Field(alias="operatorPerson")
|
|
55
|
+
responsible_person_info: PersonInfo = Field(alias="responsiblePersonInfo")
|
|
56
|
+
same_operator: bool = Field(alias="sameOperator")
|
|
57
|
+
business_check_ticket: str | None = Field(None, alias="businessCheckTicket", repr=False)
|
|
58
|
+
operator_check_ticket: str | None = Field(None, alias="operatorCheckTicket", repr=False)
|
|
59
|
+
responsible_check_ticket: str | None = Field(None, alias="responsibleCheckTicket", repr=False)
|
|
60
|
+
legal_person: PersonInfo | None = Field(None, alias="legalPerson")
|
|
61
|
+
legal_check_ticket: str | None = Field(None, alias="legalCheckTicket", repr=False)
|
|
62
|
+
power_of_attorney: list[MaterialFile] | None = Field(None, alias="powerOfAttorney")
|
|
63
|
+
other_materials: list[MaterialFile] | None = Field(None, alias="otherMaterials")
|
|
64
|
+
effect_signatures: list[str] | None = Field(None, alias="effectSignatures")
|
|
65
|
+
authorizer: str | None = None
|
|
66
|
+
authorizee: str | None = None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class AppIcp(ApiModel):
|
|
70
|
+
app_icp_filling: str = Field(alias="appIcpFilling")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class Trademark(ApiModel):
|
|
74
|
+
trademark_cn: str | None = Field(None, alias="trademarkCn")
|
|
75
|
+
trademark_en: str | None = Field(None, alias="trademarkEn")
|
|
76
|
+
trademark_number: str | None = Field(None, alias="trademarkNumber")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class SignatureApplication(ApiModel):
|
|
80
|
+
content: Text
|
|
81
|
+
purpose: Literal[1, 2]
|
|
82
|
+
qualification_id: int = Field(alias="signatureIdentificationID", gt=0)
|
|
83
|
+
source: int
|
|
84
|
+
sub_accounts: list[str] | None = Field(None, alias="subAccounts")
|
|
85
|
+
channel_types: list[Channel] | None = Field(None, alias="channelTypes")
|
|
86
|
+
description: str | None = Field(None, alias="desc")
|
|
87
|
+
domain: str | None = None
|
|
88
|
+
scene: str | None = None
|
|
89
|
+
project_name: str | None = Field(None, alias="projectName")
|
|
90
|
+
app_icp: AppIcp | None = Field(None, alias="appIcp")
|
|
91
|
+
trademark: Trademark | None = None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class TemplateParameter(ApiModel):
|
|
95
|
+
name: Text
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class ShortUrlConfig(ApiModel):
|
|
99
|
+
is_enabled: str | None = Field(None, alias="isEnabled")
|
|
100
|
+
belong: str | None = None
|
|
101
|
+
is_need_click_details: str | None = Field(None, alias="isNeedClickDetails")
|
|
102
|
+
ua_check_strategy: int | None = Field(None, alias="uaCheckStrategy")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class TemplateApplication(ApiModel):
|
|
106
|
+
name: Text
|
|
107
|
+
content: Text
|
|
108
|
+
channel_type: Channel = Field(alias="channelType")
|
|
109
|
+
area: Literal["cn"] = "cn"
|
|
110
|
+
signatures: list[str] | None = None
|
|
111
|
+
sub_accounts: list[str] | None = Field(None, alias="subAccounts")
|
|
112
|
+
template_params: list[TemplateParameter] | None = Field(None, alias="templateParams")
|
|
113
|
+
description: str | None = Field(None, alias="desc")
|
|
114
|
+
project: str | None = None
|
|
115
|
+
short_url_config: ShortUrlConfig | None = Field(None, alias="shortUrlConfig")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class BatchTask(ApiModel):
|
|
119
|
+
sub_account: Text = Field(alias="subAccount")
|
|
120
|
+
name: Text
|
|
121
|
+
signature: Text
|
|
122
|
+
template_id: Text = Field(alias="templateId")
|
|
123
|
+
template_name: str = Field(alias="templateName")
|
|
124
|
+
channel_type: BatchChannel = Field(alias="channelType")
|
|
125
|
+
file_url: Text = Field(alias="fileUrl", description="GetUploadTosURL 返回的 file 对象 Key")
|
|
126
|
+
scheduled: bool
|
|
127
|
+
send_time: int = Field(
|
|
128
|
+
alias="sendTime",
|
|
129
|
+
ge=0,
|
|
130
|
+
description="短信 API 的发送时间;立即发送传 0,定时发送使用 Unix 秒",
|
|
131
|
+
)
|
|
132
|
+
extra: dict | None = None
|