Files
Astral/docs/舞萌DX_扫码登录协议技术规范.md
T
Txiaochen 41909b9169 docs: WaHappy 开发文档
孤儿分支,只含 docs/。内容包括:
- 项目总览 / 主脚本编写指南 / 日志指南
- 上传成绩开发文档 / UpsertUserAll 踩坑清单(28 条)
- 真机 Hook 对照报告 / 实跑验证记录
- 购买功能票流程(含被服务端拒绝的排查过程)
  · 扫码登录协议技术规范
2026-10-06 14:42:32 +08:00

586 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 舞萌DX(maimai DX)扫码登录协议技术规范
> **来源文档**:
> - `docs-old/舞萌DX_aimedb调用链分析.md` —— 完整调用链、二维码格式、ChimeLib/aimedb 关系
> - `docs-old/chimelib协议逆向.md` —— 纯网络实现细节、签名算法、抓包验证、关键坑点
>
> **适用场景**:纯网络(纯 HTTP)复刻舞萌DX国服微信扫码登录,获取 userId 与 token
> **协议路径**:路径 A —— 微信扫码(HTTPS + JSON,`chimelib_dll.dll` + WinHTTP)
> **非适用**:路径 B —— 传统 Aime 物理卡(TCP 二进制,`amdaemon_api.dll` + IPC)
---
## 一、架构总览
### 1.1 两条 aimedb 通信路径
| 路径 | 名称 | 协议 | 核心组件 | 你实际用的 |
|------|------|------|----------|------------|
| **A** | 微信扫码 | HTTPS + JSON | `chimelib_dll.dll` (WinHTTP) | ✅ **是** |
| B | 传统 Aime 物理卡 | TCP 二进制 | `amdaemon_api.dll` (IPC) | ❌ 否 |
### 1.2 调用链(路径 A)
```
游戏客户端 Assembly-CSharp
│ ChimeReaderManager (驱动扫码、取结果)
▼
ChimeLib.NET.dll (托管封装)
│ Chime / ChimeUnit / ChimeResult / CCommGetUserData / CCommServAliveCheck
▼
chimelib_dll.dll (x64 原生,62KB,静态链接 WinHTTP)
│ CCommGetUserData_Create/Execute/IsEnd/GetUserID/GetToken
│ CCommServAliveCheck_Create/StartCheck/Execute/GetIndex/IsAlive
▼
aimedb 服务器集群
│ https://ai.sys-all.cn/ (主)
│ https://ai.sys-allnet.cn/ (备)
▼
POST /wc_aime/api/get_data → 返回 userID + token
POST /wc_aime/api/alive_check → 返回 "alive"
```
### 1.3 关键数据流向
```
二维码串(SGWCMAID...)
→ ChimeLib.NET.CCommQRCode.IsValid() 校验
→ EraseHashIdentifier() 取 [20:] 64位载荷
→ CCommGetUserData_Create(strGameID="MAID", strChipID, strCommonKey="", strQRData=载荷, strTitleKey="SDGB", serverURLIndex)
→ WinHTTP HTTPS POST /wc_aime/api/get_data
→ 响应 {errorID, userID, token, key, timestamp}
→ ChimeResult.GetChimeId() → userId (uint)
→ ChimeResult.GetToken() → token (string)
→ OperationManager.SetToken(userId, token) 缓存
→ 后续游戏服务器 API 携带 token
```
---
## 二、二维码格式规范
### 2.1 完整二维码字符串(84 字符)
```
结构:SGWC(4) + MAID(4) + timestamp(12) + payload(64 hex) = 84 字符
偏移 长度 字段 说明 示例
0–3 4 微信标识 常量 "SGWC" SGWC
4–7 4 GameID "MAID"=舞萌DX MAID
8–19 12 时间戳 yymmddHHMMss (UTC+9) 260613133715
20–83 64 载荷/哈希 64位十六进制(疑似SHA-256) 77503FD2...427759BCA5
```
### 2.2 校验规则(`CCommQRCode.IsValid()`)
```python
def is_valid(qr: str) -> bool:
return len(qr) == 84 and qr[0:4] == "SGWC" and qr[4:8] == "MAID"
```
### 2.3 传给原生层的数据(`EraseHashIdentifier()`)
```python
def erase_hash_identifier(qr: str) -> str:
"""去掉前 20 字符,返回 64 位载荷"""
return qr[20:] # 即 payload 部分
```
### 2.4 二维码 URL ⇄ 字符串互转
**URL 格式**:
```
https://wq.wahlap.net/qrcode/req/{GameID+timestamp+payload}.html?l=...&t=...&d=...
└──────────── 80 字符 ────────────┘
```
**提取规则**:
```python
from urllib.parse import urlparse
import re
def url_to_qr_string(url: str) -> str:
"""URL → 完整 84 字符二维码串"""
path = urlparse(url).path
match = re.search(r"/qrcode/req/([^/]+)\.html", path)
if not match:
raise ValueError("URL 路径格式不符")
return "SGWC" + match.group(1) # 前加 SGWC 前缀
def qr_string_to_url_body(qr: str) -> str:
"""完整二维码串 → URL 路径段(80字符,无 SGWC)"""
return qr[4:] # 去掉 SGWC
```
**查询参数**(`l`/`t`/`d`):**不参与**机台协议,仅为微信侧生成二维码时的元数据。
---
## 三、服务器端点与网络协议
### 3.1 服务器地址
| 环境 | 域名 | IP | 端口 | TLS |
|------|------|-----|------|-----|
| 主 | `ai.sys-all.cn` | 81.71.193.236 | 443 | TLSv1.2 |
| 备 | `ai.sys-allnet.cn` | (同解析) | 443 | TLSv1.2 |
### 3.2 API 端点
| 端点 | Method | Content-Type | User-Agent | 用途 |
|------|--------|--------------|------------|------|
| `/wc_aime/api/alive_check` | POST | `application/json` (body `{}`) | 无 | 探活,返回 `200 alive` |
| `/qrcode/api/alive_check` | POST | 无(空body) | `WC_AIME_LIB` | 探活变体,返回 `200 alive` |
| `/wc_aime/api/get_data` | POST | **无** | `WC_AIME_LIB` | **核心登录接口** |
### 3.3 请求头(关键:无 Content-Type)
```http
POST /wc_aime/api/get_data HTTP/1.1
Host: ai.sys-allnet.cn
User-Agent: WC_AIME_LIB
Connection: Keep-Alive
Content-Length: 243
{"chipID":"...","openGameID":"MAID","key":"...","qrCode":"...","timestamp":"...","titlekey":"SDGB"}
```
> ⚠️ **严禁添加 `Content-Type: application/json`**,否则服务端返回 `errorID=57`(HTTP 200 + 合法 JSON,极易误判为签名错误)。
### 3.4 请求体 JSON 字段
| 字段 | 类型 | 来源 | 说明 |
|------|------|------|------|
| `chipID` | string | Keychip ID | 机台唯一标识,如 `XXXX-XXXXXXXXXXX` |
| `openGameID` | string | 固定 | `"MAID"` |
| `key` | string | 签名计算 | SHA-256 大写 hex,**核心签名字段** |
| `qrCode` | string | 二维码[20:] | **64 位载荷,非完整 84 字符串** |
| `timestamp` | string | 本机本地时间 | yymmddHHMMss 格式 |
| `titlekey` | string | 固定 | `"SDGB"` |
### 3.5 响应体 JSON 字段
| 字段 | 类型 | 说明 |
|------|------|------|
| `errorID` | int | 0=成功,非0=失败 |
| `userID` | int | 成功时为正整数(如 12345678),失败时 -1 |
| `token` | string | 成功时 20 位临时凭据,失败时空串 |
| `key` | string | 服务端返回的签名(可忽略) |
| `timestamp` | string | 服务端时间戳 |
---
## 四、签名算法(核心)
### 4.1 算法规范
```python
import hashlib
SIGN_KEY = "XcW5FW4cPArBXEk4vzKz3CIrMuA5EVVW" # 32字符,含前导 X
def calculate_key(chip_id: str, timestamp: str) -> str:
"""
key = SHA-256(chipID + timestamp + SIGN_KEY).hexdigest().upper()
参数:
chip_id: 如 "XXXX-XXXXXXXXXXX"
timestamp: 如 "261001004115" (yymmddHHMMss)
返回:64字符大写十六进制
"""
sign_input = chip_id + timestamp + SIGN_KEY
return hashlib.sha256(sign_input.encode()).hexdigest().upper()
```
### 4.2 技术细节
| 项目 | 值 | 验证来源 |
|------|-----|----------|
| 哈希算法 | SHA-256 | Hook `CryptCreateHash` ALGID=0x800C (CALG_SHA_256) |
| 密钥串 | `XcW5FW4cPArBXEk4vzKz3CIrMuA5EVVW` | .rdata UTF-16 @0x00B040,**含前导 X** |
| 拼接顺序 | `chipID + timestamp + KEY` | **不含 qrCode、不含 titlekey** |
| 输出编码 | 大写十六进制 | hex 表 `0123456789ABCDEF` @0x00A440 |
### 4.3 抓包验证向量
```
输入:
chipID = "XXXX-XXXXXXXXXXX"
timestamp = "261001004115"
SIGN_KEY = "XcW5FW4cPArBXEk4vzKz3CIrMuA5EVVW"
SHA256("XXXX-XXXXXXXXXXX" + "261001004115" + "XcW5FW4cPArBXEk4vzKz3CIrMuA5EVVW")
= 67015F28CFC3C7FA5E629BECE54893B581A6DBD153426AE5E6BA1681C6D8E327
```
与 mitmproxy 抓包完全一致 ✓
---
## 五、完整请求流程(纯 Python 实现)
### 5.1 最小可用代码
```python
import hashlib, http.client, json, ssl
from datetime import datetime
SIGN_KEY = "XcW5FW4cPArBXEk4vzKz3CIrMuA5EVVW"
CHIP_ID = "XXXX-XXXXXXXXXXX" # 可配置
HOST = "ai.sys-allnet.cn"
PATH = "/wc_aime/api/get_data"
def parse_qr_login(qr_input: str, chip_id: str = CHIP_ID) -> tuple[int, str]:
"""
输入:二维码串(84字符)或 URL
返回:(user_id: int, token: str)
"""
# 1. 统一输入 → 标准 84 字符串
qr_string = normalize_qr_input(qr_input)
# 2. 校验格式
validate_qr_string(qr_string)
# 3. 构造请求体
ts = datetime.now().strftime("%y%m%d%H%M%S")
key = calculate_key(chip_id, ts)
qr_code = qr_string[20:] # 仅 64 位载荷
body_dict = {
"chipID": chip_id,
"openGameID": "MAID",
"key": key,
"qrCode": qr_code,
"timestamp": ts,
"titlekey": "SDGB",
}
body_bytes = json.dumps(body_dict, separators=(",", ":")).encode()
# 4. 发送请求(关键:无 Content-Type)
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
conn = http.client.HTTPSConnection(HOST, 443, context=ctx, timeout=20)
try:
conn.request("POST", PATH, body=body_bytes, headers={
"Connection": "Keep-Alive",
"User-Agent": "WC_AIME_LIB",
"Content-Length": str(len(body_bytes)),
# ★ 绝不加 Content-Type
})
resp = conn.getresponse()
data = json.loads(resp.read().decode())
finally:
conn.close()
# 5. 解析响应
if data.get("errorID") != 0 or data.get("userID") == -1:
raise RuntimeError(f"登录失败: errorID={data.get('errorID')}, userID={data.get('userID')}")
return data["userID"], data["token"]
def normalize_qr_input(qr_input: str) -> str:
"""URL 或字符串 → 标准 84 字符二维码串"""
qr = qr_input.strip()
if qr.startswith("http://") or qr.startswith("https://"):
qr = url_to_qr_string(qr)
return qr
def validate_qr_string(qr: str) -> None:
"""校验二维码格式,不通过抛 ValueError"""
if len(qr) != 84:
raise ValueError(f"长度应为84,实际{len(qr)}")
if not qr.startswith("SGWC"):
raise ValueError("必须以 SGWC 开头")
if qr[4:8] != "MAID":
raise ValueError(f"GameID 必须为 MAID,实际 {qr[4:8]}")
# 可选:校验 payload 为 64 位 hex
if not all(c in "0123456789ABCDEF" for c in qr[20:]):
raise ValueError("载荷必须为大写 hex")
```
### 5.2 关键实现要点清单
| 要点 | 正确做法 | 错误做法后果 |
|------|----------|--------------|
| qrCode 字段 | `qr_string[20:]` (64位) | 传完整 84 字符 → 签名不匹配 |
| 签名拼接 | `chipID + timestamp + KEY` | 加入 qrCode/titlekey → 签名错误 |
| 密钥串 | 含前导 `X` (32字符) | 漏掉 X → 签名完全不同 |
| 请求头 | 无 `Content-Type` | 加上 → `errorID=57` |
| timestamp | 本机本地时间即可 | 纠结时区 → 其实服务端不强校验 |
| SSL 验证 | 关闭验证 | 开启 → 证书链可能不通过 |
---
## 六、错误码对照
| errorID | 含义 | 处理建议 |
|---------|------|----------|
| 0 | 成功 | 正常返回 userID + token |
| 1-8 | 原生 ChimeErrorId | 读卡器/服务器/二维码无效等通用错误 |
| **57** | **二维码已失效/已被使用/请求头不符** | 检查:二维码是否过期(~10分钟)、是否重复提交、**请求头是否含 Content-Type** |
| `userID:-1` | 失败标志 | 配合 errorID 判断具体原因 |
> ### ★ 实测补充(2026-10-06)
>
> 本工程用同一套参数实测到的失败响应是 **`errorID=2`**(不是 57):
>
> ```json
> {"errorID": 2, "key": "B744ADD5...", "timestamp": "261006125706",
> "userID": -1, "token": ""}
> ```
>
> **触发条件是「二维码过期」,不是「用过一次」。**
>
> | 二维码生成时刻(timestamp 内嵌) | 使用时刻 | 结果 |
> |---|---|---|
> | `261006012047`(01:20:47) | 01:21:55(+68s) | ✅ 成功 |
> | `261006124637`(12:46:37) | 12:52:22(+5m45s) | ✅ 成功 |
> | `261006130621`(13:06:21) | 13:10:42(+4m21s) | ✅ 成功 |
> | 上面那个 12:46:37 的二维码 | 13:0x(+约 15min) | ❌ `errorID=2` |
>
> 即:**二维码是「有时效」的,不是「按次数计量」的**。
>
> **有效期 = 10 分钟**(实测至少 5 分 45 秒内均可用,超时后返回 `errorID=2`)。
> 所以报错时**不要怀疑「用过了」**,重新生成一个即可。
---
## 七、时间戳处理细节
| 时间戳位置 | 时区 | 格式 | 说明 |
|------------|------|------|------|
| 二维码内 (偏移8-19) | **UTC+9 (日本时间)** | yymmddHHMMss | 微信侧生成,约 10 分钟有效期 |
| 请求体 `timestamp` | **本机本地时间** | yymmddHHMMss | 实测 UTC+8 与 DLL 行为一致,服务端不强校验时区 |
| 响应 `timestamp` | 服务端时间 | yymmddHHMMss | 仅作参考 |
> **注意**:本机时区非 UTC+8 时,请求体 timestamp 仍用本地时间,但需确保系统时间准确(NTP 同步)。
---
## 八、探活接口(可选,纯 Python 可独立实现)
### 8.1 探活请求
```python
def check_alive(host: str = "ai.sys-allnet.cn") -> bool:
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
conn = http.client.HTTPSConnection(host, 443, context=ctx, timeout=10)
try:
conn.request("POST", "/wc_aime/api/alive_check", body=b"{}", headers={
"Content-Type": "application/json", # 探活需加 Content-Type
"Connection": "Keep-Alive",
})
resp = conn.getresponse()
return resp.status == 200 and resp.read().decode().strip() == "alive"
finally:
conn.close()
```
### 8.2 行为差异
| 端点 | Content-Type | User-Agent | 响应 |
|------|--------------|------------|------|
| `/wc_aime/api/alive_check` | `application/json` | 无 | `200 alive` |
| `/qrcode/api/alive_check` | 无 | `WC_AIME_LIB` | `200 alive` |
| `/wc_aime/api/get_data` | **无** | `WC_AIME_LIB` | 业务响应 |
> DLL 实测:`CCommServAliveCheck` 探活阶段**不发真实网络请求**(WinHTTP hook 不触发),纯 Python 可直接调用 alive_check 验证服务器存活。
---
## 九、原生 DLL 接口签名(供 P/Invoke 参考)
### 9.1 CCommServAliveCheck 系列
```c
// 创建探活会话
HANDLE CCommServAliveCheck_Create();
// 设置连接权限
void CCommServAliveCheck_setConnectPermission(HANDLE h, BOOL permit);
// 开始探活
void CCommServAliveCheck_startCheck(HANDLE h);
// 执行轮询
void CCommServAliveCheck_execute(HANDLE h);
// 是否忙碌
BOOL CCommServAliveCheck_isBusy(HANDLE h);
// 获取存活服务器索引
int CCommServAliveCheck_getIndex(HANDLE h);
// 是否存活
BOOL CCommServAliveCheck_isAlive(HANDLE h);
// 销毁
void CCommServAliveCheck_Destroy(HANDLE h);
```
### 9.2 CCommGetUserData 系列
```c
// 创建登录会话
// strGameID="MAID", strChipID=KeychipID, strCommonKey="", strQRData=二维码[20:], strTitleKey="SDGB", serverURLIndex=探活返回索引
HANDLE CCommGetUserData_Create(
LPCSTR strGameID,
LPCSTR strChipID,
LPCSTR strCommonKey,
LPCSTR strQRData,
LPCSTR strTitleKey,
int serverURLIndex
);
// 执行请求
void CCommGetUserData_execute(HANDLE h);
// 是否结束
BOOL CCommGetUserData_isEnd(HANDLE h);
// 获取错误码 (0=成功)
int CCommGetUserData_getErrorID(HANDLE h);
// 获取 userID (uint)
unsigned int CCommGetUserData_getUserID(HANDLE h);
// 获取 token (ANSI 字符串)
LPCSTR CCommGetUserData_getToken(HANDLE h);
// 销毁
void CCommGetUserData_Destroy(HANDLE h);
```
### 9.3 托管层调用序列(ChimeUnit.Execute 状态机)
```csharp
// 1. 探活
var alive = CCommServAliveCheck_Create();
CCommServAliveCheck_setConnectPermission(alive, true);
CCommServAliveCheck_startCheck(alive);
while (CCommServAliveCheck_isBusy(alive)) CCommServAliveCheck_execute(alive);
int urlIndex = CCommServAliveCheck_getIndex(alive);
bool isAlive = CCommServAliveCheck_isAlive(alive);
CCommServAliveCheck_Destroy(alive);
// 2. 登录
var login = CCommGetUserData_Create(
"MAID", // gameID
keychipId, // chipID (如 XXXX-XXXXXXXXXXX)
"", // commonKey (托管层传空,原生层内部处理)
qrString.Substring(20), // qrData (64位载荷)
"SDGB", // titleKey
urlIndex // 探活返回的服务器索引
);
while (!CCommGetUserData_isEnd(login)) CCommGetUserData_execute(login);
if (CCommGetUserData_getErrorID(login) == 0) {
uint userId = CCommGetUserData_getUserID(login);
string token = CCommGetUserData_getToken(login); // PtrToStringAnsi
}
CCommGetUserData_Destroy(login);
```
---
## 十、常量汇总表
```python
# ===== 签名与密钥 =====
SIGN_KEY = "XcW5FW4cPArBXEk4vzKz3CIrMuA5EVVW" # 含前导 X,32字符
# ===== 默认机台标识 =====
DEFAULT_CHIP_ID = "XXXX-XXXXXXXXXXX"
# ===== 服务器 =====
PRIMARY_HOST = "ai.sys-all.cn"
BACKUP_HOST = "ai.sys-allnet.cn"
PORT = 443
LOGIN_PATH = "/wc_aime/api/get_data"
ALIVE_PATH = "/wc_aime/api/alive_check"
# ===== 固定请求字段 =====
OPEN_GAME_ID = "MAID"
TITLE_KEY = "SDGB"
USER_AGENT = "WC_AIME_LIB"
# ===== 二维码常量 =====
QR_PREFIX = "SGWC"
QR_GAME_ID = "MAID"
QR_PREFIX_LEN = 20 # SGWC(4) + MAID(4) + timestamp(12)
QR_PAYLOAD_LEN = 64
QR_TOTAL_LEN = 84
# ===== HTTP 头(登录接口) =====
LOGIN_HEADERS = {
"Connection": "Keep-Alive",
"User-Agent": USER_AGENT,
# "Content-Type": "application/json", # ★ 绝不加这一行
}
```
---
## 十一、抓包调试方法论
### 11.1 强制走代理
```csharp
// Hook WinHttpOpen
// dwAccessType = 3 (WINHTTP_ACCESS_TYPE_NAMED_PROXY)
// pwszProxy = "127.0.0.1:8890"
```
### 11.2 mitmproxy CA 证书安装
```powershell
# 必须用 X509Store 静默导入,Import-Certificate 会因 UI 限制失败
$store = New-Object System.Security.Cryptography.X509Certificates.X509Store("Root", "CurrentUser")
$store.Open("ReadWrite")
$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2("mitmproxy-ca-cert.pem")
$store.Add($cert)
$store.Close()
```
### 11.3 落盘与读取
```bash
mitmdump -w flow.mitm
# Python 读取
from mitmproxy.io import FlowReader
with open("flow.mitm", "rb") as f:
for flow in FlowReader(f).stream():
print(flow.request.pretty_url)
print(flow.request.content)
print(flow.response.content)
```
### 11.4 捕获签名输入
```csharp
// Hook CryptHashData 导出入口
// 捕获 pbData 指针指向的字节数组 → 即签名原文
// 对比:chipID + timestamp + KEY
```
### 11.5 常见 Hook 坑点
| 坑点 | 原因 | 解决 |
|------|------|------|
| IAT hook 全失效 | pefile `imp.address` 已是完整 VA,再加 ImageBase 导致地址错误 | 直接用 `imp.address` + ASLR_delta |
| IAT 写入后还原 | CrackProof 驱动写保护页面 | 改用全局 inline hook:改写 advapi32 导出函数入口 + capstone 精确 trampoline |
| x64 trampoline 崩 | 指令切碎不完整 | capstone 逐条解析,累计 >=14 字节再跳转 |
| 误判加壳 | Hook 全失效以为加壳 | 实测:.text 熵 6.2、465 个 FF 15、入口点可正常反汇编 → **未加壳** |
---
## 十二、实测验证记录
| # | 二维码时间戳 | userID | token (20位) | 备注 |
|---|--------------|--------|--------------|------|
| 1 | 260925205225 | 12345678 | TESTTOKEN0000000000 | DLL 验证成功 |
| 2 | 260930235204 | 12345678 | e1Tsql1aP1SHLUthRdYI | |
| 3 | 261001001304 | 12345678 | rI48n34Ttne1qR9FDFnY | 同一时间戳多次请求 token 各异 |
| 4 | 261001001304 | 12345678 | znoWwYW1bMpwlNUzcTpX | |
| 5 | 261001001304 | 12345678 | 8Cp6IztEGvLlYt5SRjxw | |
**结论**:
- userID 固定(同一账号)
- token 每次请求生成新的 20 位临时凭据(一次性)
- 探活+登录总耗时 ~800ms(网络延迟主导)
---
## 十三、最小复刻检查清单
实现纯网络扫码登录,必须覆盖:
- [ ] 二维码输入归一化(URL → 84字符串)
- [ ] 格式校验(长度84、前缀SGWC、GameID=MAID、载荷64位hex)
- [ ] 载荷提取(`qr_string[20:]`)
- [ ] 时间戳生成(本地时间 `%y%m%d%H%M%S`)
- [ ] 签名计算(SHA256(chipID+timestamp+KEY) 大写hex)
- [ ] 请求体构造(紧凑JSON,字段顺序固定)
- [ ] HTTPS POST(无Content-Type,UA=WC_AIME_LIB,Keep-Alive)
- [ ] SSL 不验证证书
- [ ] 响应解析(errorID=0、userID>0、token非空)
- [ ] 错误处理(errorID=57→请求头问题,errorID=2→二维码过期)
---
*本文档仅包含技术协议实现细节,不含项目结构、代码规范、文件路径等工程化内容。*