├── test ├── .gitignore ├── pytest.ini ├── test_network.py ├── test_common.py ├── test_mdns.py ├── test_spec.py ├── conftest.py └── test_lan.py ├── .gitignore ├── doc ├── images │ ├── cloud_control.jpg │ ├── local_control.jpg │ ├── cloud_control_zh.jpg │ └── local_control_zh.jpg └── CONTRIBUTING_zh.md ├── hacs.json ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ └── bug_report.yaml └── workflows │ ├── release.yaml │ ├── test.yaml │ └── validate.yaml ├── custom_components └── xiaomi_home │ ├── manifest.json │ ├── miot │ ├── specs │ │ ├── spec_filter.yaml │ │ ├── spec_modify.yaml │ │ └── bool_trans.yaml │ ├── web_pages.py │ ├── miot_i18n.py │ ├── miot_error.py │ ├── i18n │ │ ├── zh-Hans.json │ │ ├── zh-Hant.json │ │ ├── ja.json │ │ ├── en.json │ │ ├── nl.json │ │ ├── pt-BR.json │ │ ├── pt.json │ │ ├── ru.json │ │ ├── de.json │ │ ├── fr.json │ │ └── it.json │ ├── const.py │ ├── resource │ │ └── oauth_redirect_page.html │ └── common.py │ ├── button.py │ ├── event.py │ ├── binary_sensor.py │ ├── select.py │ ├── switch.py │ ├── number.py │ ├── sensor.py │ └── notify.py ├── install.sh ├── LegalNotice.md ├── tools ├── common.py └── update_lan_rule.py ├── LICENSE.md ├── CONTRIBUTING.md └── CHANGELOG.md /test/.gitignore: -------------------------------------------------------------------------------- 1 | miot 2 | test_cache -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .pytest_cache 3 | .vscode 4 | .idea 5 | requirements.txt 6 | -------------------------------------------------------------------------------- /doc/images/cloud_control.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sweetdream-001/pro-33824/HEAD/doc/images/cloud_control.jpg -------------------------------------------------------------------------------- /doc/images/local_control.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sweetdream-001/pro-33824/HEAD/doc/images/local_control.jpg -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Xiaomi Home", 3 | "homeassistant": "2024.4.4", 4 | "hacs": "1.34.0" 5 | } 6 | -------------------------------------------------------------------------------- /doc/images/cloud_control_zh.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sweetdream-001/pro-33824/HEAD/doc/images/cloud_control_zh.jpg -------------------------------------------------------------------------------- /doc/images/local_control_zh.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sweetdream-001/pro-33824/HEAD/doc/images/local_control_zh.jpg -------------------------------------------------------------------------------- /test/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | markers: 3 | github: tests for github actions 4 | update: update or re-sort config file -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Feature Suggestion / 功能建议 4 | url: https://github.com/XiaoMi/ha_xiaomi_home/discussions/new?category=ideas 5 | about: Share ideas for enhancements or new features. / 建议改进或增加新功能 6 | 7 | - name: Support and Help / 支持与帮助 8 | url: https://github.com/XiaoMi/ha_xiaomi_home/discussions/categories/q-a 9 | about: Please ask and answer questions here. / 请在这里提问和答疑 10 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | release-zip: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | 13 | - name: ZIP Component Dir 14 | run: | 15 | cd ${{ github.workspace }}/custom_components/xiaomi_home 16 | zip -r xiaomi_home.zip ./ 17 | 18 | - name: Upload zip to release 19 | uses: svenstaro/upload-release-action@v2 20 | with: 21 | repo_token: ${{ secrets.GITHUB_TOKEN }} 22 | file: ${{ github.workspace }}/custom_components/xiaomi_home/xiaomi_home.zip 23 | asset_name: xiaomi_home.zip 24 | tag: ${{ github.ref }} 25 | overwrite: true 26 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | workflow_dispatch: 11 | 12 | jobs: 13 | check-rule-format: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout the repository 17 | uses: actions/checkout@v4 18 | 19 | - name: Install dependencies 20 | run: | 21 | python -m pip install --upgrade pip 22 | pip install pytest pytest-asyncio pytest-dependency zeroconf paho.mqtt psutil cryptography slugify 23 | 24 | - name: Check rule format with pytest 25 | run: | 26 | pytest -v -s -m github ./test/check_rule_format.py 27 | 28 | - name: Unit test with pytest 29 | run: | 30 | pytest -v -s -m github ./test/ 31 | -------------------------------------------------------------------------------- /custom_components/xiaomi_home/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "xiaomi_home", 3 | "name": "Xiaomi Home", 4 | "codeowners": [ 5 | "@XiaoMi" 6 | ], 7 | "config_flow": true, 8 | "dependencies": [ 9 | "http", 10 | "persistent_notification", 11 | "ffmpeg", 12 | "zeroconf" 13 | ], 14 | "documentation": "https://github.com/XiaoMi/ha_xiaomi_home/blob/main/README.md", 15 | "integration_type": "hub", 16 | "iot_class": "cloud_polling", 17 | "issue_tracker": "https://github.com/XiaoMi/ha_xiaomi_home/issues", 18 | "loggers": [ 19 | "Xiaomi Home" 20 | ], 21 | "requirements": [ 22 | "construct>=2.10.56", 23 | "paho-mqtt<2.0.0", 24 | "numpy", 25 | "cryptography", 26 | "psutil" 27 | ], 28 | "version": "v0.1.5b2", 29 | "zeroconf": [ 30 | "_miot-central._tcp.local." 31 | ] 32 | } -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # Check the number of input parameters. 5 | if [ $# -ne 1 ]; then 6 | echo "usage: $0 [config_path]" 7 | exit 1 8 | fi 9 | # Get the config path. 10 | config_path=$1 11 | # Check if config path exists. 12 | if [ ! -d "$config_path" ]; then 13 | echo "$config_path does not exist" 14 | exit 1 15 | fi 16 | 17 | # Get the script path. 18 | script_path=$(dirname "$0") 19 | 20 | # Set source and target 21 | component_name=xiaomi_home 22 | source_path="$script_path/custom_components/$component_name" 23 | target_root="$config_path/custom_components" 24 | target_path="$target_root/$component_name" 25 | 26 | # Remove the old version. 27 | rm -rf "$target_path" 28 | 29 | # Copy the new version. 30 | mkdir -p "$target_root" 31 | cp -r "$source_path" "$target_path" 32 | 33 | # Done. 34 | echo "Xiaomi Home installation is completed. Please restart Home Assistant." 35 | exit 0 36 | -------------------------------------------------------------------------------- /custom_components/xiaomi_home/miot/specs/spec_filter.yaml: -------------------------------------------------------------------------------- 1 | urn:miot-spec-v2:device:air-purifier:0000A007:zhimi-ma4: 2 | properties: 3 | - 9.* 4 | - 13.* 5 | - 15.* 6 | services: 7 | - '10' 8 | urn:miot-spec-v2:device:curtain:0000A00C:lumi-hmcn01: 9 | properties: 10 | - '5.1' 11 | services: 12 | - '4' 13 | - '7' 14 | - '8' 15 | urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1: 16 | events: 17 | - '2.1' 18 | urn:miot-spec-v2:device:health-pot:0000A051:chunmi-a1: 19 | services: 20 | - '5' 21 | urn:miot-spec-v2:device:light:0000A001:philips-strip3: 22 | properties: 23 | - '2.2' 24 | services: 25 | - '1' 26 | - '3' 27 | urn:miot-spec-v2:device:light:0000A001:yeelink-color2: 28 | properties: 29 | - 3.* 30 | - '2.5' 31 | urn:miot-spec-v2:device:light:0000A001:yeelink-dnlight2: 32 | services: 33 | - '3' 34 | urn:miot-spec-v2:device:light:0000A001:yeelink-mbulb3: 35 | services: 36 | - '3' 37 | urn:miot-spec-v2:device:motion-sensor:0000A014:xiaomi-pir1: 38 | services: 39 | - '1' 40 | - '5' 41 | urn:miot-spec-v2:device:router:0000A036:xiaomi-rd03: 42 | services: 43 | - '*' 44 | -------------------------------------------------------------------------------- /test/test_network.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Unit test for miot_network.py.""" 3 | import logging 4 | import pytest 5 | import asyncio 6 | 7 | _LOGGER = logging.getLogger(__name__) 8 | 9 | # pylint: disable=import-outside-toplevel, unused-argument 10 | 11 | 12 | @pytest.mark.asyncio 13 | async def test_network_monitor_loop_async(): 14 | from miot.miot_network import MIoTNetwork, InterfaceStatus, NetworkInfo 15 | miot_net = MIoTNetwork() 16 | 17 | async def on_network_status_changed(status: bool): 18 | _LOGGER.info('on_network_status_changed, %s', status) 19 | miot_net.sub_network_status(key='test', handler=on_network_status_changed) 20 | 21 | async def on_network_info_changed( 22 | status: InterfaceStatus, info: NetworkInfo): 23 | _LOGGER.info('on_network_info_changed, %s, %s', status, info) 24 | miot_net.sub_network_info(key='test', handler=on_network_info_changed) 25 | 26 | await miot_net.init_async() 27 | await asyncio.sleep(3) 28 | _LOGGER.info('net status: %s', miot_net.network_status) 29 | _LOGGER.info('net info: %s', miot_net.network_info) 30 | await miot_net.deinit_async() 31 | -------------------------------------------------------------------------------- /test/test_common.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Unit test for miot_common.py.""" 3 | import pytest 4 | 5 | # pylint: disable=import-outside-toplevel, unused-argument 6 | 7 | 8 | @pytest.mark.github 9 | def test_miot_matcher(): 10 | from miot.common import MIoTMatcher 11 | 12 | matcher: MIoTMatcher = MIoTMatcher() 13 | # Add 14 | for l1 in range(1, 11): 15 | matcher[f'test/{l1}/#'] = f'test/{l1}/#' 16 | for l2 in range(1, 11): 17 | matcher[f'test/{l1}/{l2}'] = f'test/{l1}/{l2}' 18 | if not matcher.get(topic=f'test/+/{l2}'): 19 | matcher[f'test/+/{l2}'] = f'test/+/{l2}' 20 | # Match 21 | match_result: list[str] = list(matcher.iter_all_nodes()) 22 | assert len(match_result) == 120 23 | match_result: list[str] = list(matcher.iter_match(topic='test/1/1')) 24 | assert len(match_result) == 3 25 | assert set(match_result) == set(['test/1/1', 'test/+/1', 'test/1/#']) 26 | # Delete 27 | if matcher.get(topic='test/1/1'): 28 | del matcher['test/1/1'] 29 | assert len(list(matcher.iter_all_nodes())) == 119 30 | match_result: list[str] = list(matcher.iter_match(topic='test/1/1')) 31 | assert len(match_result) == 2 32 | assert set(match_result) == set(['test/+/1', 'test/1/#']) 33 | -------------------------------------------------------------------------------- /LegalNotice.md: -------------------------------------------------------------------------------- 1 | # 法律声明 2 | 3 | 版权声明 (C) 2024 小米。 4 | Home Assistant 米家集成(Xiaomi Home Integration)所使用的米家云服务 API 接口(以下简称小米云接口)的所有权及其知识产权为小米所有。您仅限于在[米家集成许可证](./LICENSE.md)规定的范围内使用,任何超出前述许可证规定范围外的行为,包括但不限于在非 Home Assistant 平台上使用小米云接口、以及基于商业目的在 Home Assistant 平台上使用小米云接口等行为均应被视为侵权行为,小米有权对您使用的小米云接口采取包括但不限于停止使用、删除、屏蔽、断开连接等措施,同时保留向您追究相关法律责任的权利。 5 | 小米拥有本声明的最终解释权。 6 | 7 | --- 8 | 9 | # Legal Notice 10 | 11 | Copyright (C) 2024 Xiaomi Corporation. 12 | All rights, title, interest and intellectual property rights of the Xiaomi Cloud Service API interface (hereinafter referred to as Xiaomi Cloud Interface) provided to use the Home Assistant Xiaomi Home Integration shall be solely owned by Xiaomi. You are only permitted to use the Xiaomi Cloud Interface within the scope specified in the [Xiaomi Home Integration License](./LICENSE.md). Any behavior beyond the scope of the aforesaid license, including but not limited to using the Xiaomi Cloud Interface on non-Home Assistant platforms and using the Xiaomi Cloud Interface on the Home Assistant platform for any commercial purposes, shall be deemed as infringement. Xiaomi has the right to take measures, including but not limited to stopping usage, deleting, blocking and disconnecting the Xiaomi Cloud Interface used by You, and also reserves the right to pursue relevant legal responsibilities against You. 13 | Xiaomi reserves the right of the final interpretation of this notice. 14 | -------------------------------------------------------------------------------- /test/test_mdns.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Unit test for miot_mdns.py.""" 3 | import asyncio 4 | import logging 5 | import pytest 6 | from zeroconf import IPVersion 7 | from zeroconf.asyncio import AsyncZeroconf 8 | 9 | _LOGGER = logging.getLogger(__name__) 10 | 11 | # pylint: disable=import-outside-toplevel, unused-argument 12 | 13 | 14 | @pytest.mark.asyncio 15 | async def test_service_loop_async(): 16 | from miot.miot_mdns import MipsService, MipsServiceState 17 | 18 | async def on_service_state_change( 19 | group_id: str, state: MipsServiceState, data: dict): 20 | _LOGGER.info( 21 | 'on_service_state_change, %s, %s, %s', group_id, state, data) 22 | 23 | async with AsyncZeroconf(ip_version=IPVersion.V4Only) as aiozc: 24 | mips_service = MipsService(aiozc) 25 | mips_service.sub_service_change('test', '*', on_service_state_change) 26 | await mips_service.init_async() 27 | # Wait for service to discover 28 | await asyncio.sleep(3) 29 | services_detail = mips_service.get_services() 30 | _LOGGER.info('get all service, %s', list(services_detail.keys())) 31 | for name, data in services_detail.items(): 32 | _LOGGER.info( 33 | '\tinfo, %s, %s, %s, %s', 34 | name, data['did'], data['addresses'], data['port']) 35 | await mips_service.deinit_async() 36 | -------------------------------------------------------------------------------- /tools/common.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Common functions.""" 3 | import json 4 | import yaml 5 | from urllib.parse import urlencode 6 | from urllib.request import Request, urlopen 7 | 8 | 9 | def load_yaml_file(yaml_file: str) -> dict: 10 | with open(yaml_file, 'r', encoding='utf-8') as file: 11 | return yaml.safe_load(file) 12 | 13 | 14 | def save_yaml_file(yaml_file: str, data: dict) -> None: 15 | with open(yaml_file, 'w', encoding='utf-8') as file: 16 | yaml.safe_dump( 17 | data=data, stream=file, allow_unicode=True) 18 | 19 | 20 | def load_json_file(json_file: str) -> dict: 21 | with open(json_file, 'r', encoding='utf-8') as file: 22 | return json.load(file) 23 | 24 | 25 | def save_json_file(json_file: str, data: dict) -> None: 26 | with open(json_file, 'w', encoding='utf-8') as file: 27 | json.dump(data, file, ensure_ascii=False, indent=4) 28 | 29 | 30 | def http_get( 31 | url: str, params: dict = None, headers: dict = None 32 | ) -> dict: 33 | if params: 34 | encoded_params = urlencode(params) 35 | full_url = f'{url}?{encoded_params}' 36 | else: 37 | full_url = url 38 | request = Request(full_url, method='GET', headers=headers or {}) 39 | content: bytes = None 40 | with urlopen(request) as response: 41 | content = response.read() 42 | return ( 43 | json.loads(str(content, 'utf-8')) 44 | if content is not None else None) 45 | -------------------------------------------------------------------------------- /custom_components/xiaomi_home/miot/specs/spec_modify.yaml: -------------------------------------------------------------------------------- 1 | urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:1: 2 | prop.2.1: 3 | name: access-mode 4 | access: 5 | - read 6 | - notify 7 | prop.2.2: 8 | name: ip-address 9 | icon: mdi:ip 10 | prop.2.3: 11 | name: wifi-ssid 12 | access: 13 | - read 14 | - notify 15 | urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:2: urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:1 16 | urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:3: urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:1 17 | urn:miot-spec-v2:device:outlet:0000A002:chuangmi-212a01:1: 18 | prop.5.1: 19 | name: power-consumption 20 | expr: round(src_value/1000, 3) 21 | urn:miot-spec-v2:device:outlet:0000A002:chuangmi-212a01:2: urn:miot-spec-v2:device:outlet:0000A002:chuangmi-212a01:1 22 | urn:miot-spec-v2:device:outlet:0000A002:chuangmi-212a01:3: urn:miot-spec-v2:device:outlet:0000A002:chuangmi-212a01:1 23 | urn:miot-spec-v2:device:outlet:0000A002:cuco-cp1md:1: 24 | prop.2.2: 25 | name: power-consumption 26 | expr: round(src_value/1000, 3) 27 | urn:miot-spec-v2:device:outlet:0000A002:cuco-v3:1: 28 | prop.11.1: 29 | name: power-consumption 30 | expr: round(src_value/100, 2) 31 | urn:miot-spec-v2:device:outlet:0000A002:cuco-v3:2: urn:miot-spec-v2:device:outlet:0000A002:cuco-v3:1 32 | urn:miot-spec-v2:device:outlet:0000A002:zimi-zncz01:2:0000C816: 33 | prop.3.1: 34 | name: electric-power 35 | expr: round(src_value/100, 2) 36 | urn:miot-spec-v2:device:router:0000A036:xiaomi-rd08:1: 37 | prop.2.1: 38 | name: download-speed 39 | icon: mdi:download 40 | unit: B/s 41 | prop.2.2: 42 | name: upload-speed 43 | icon: mdi:upload 44 | unit: B/s 45 | -------------------------------------------------------------------------------- /.github/workflows/validate.yaml: -------------------------------------------------------------------------------- 1 | name: Validate 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | workflow_dispatch: 11 | 12 | jobs: 13 | validate-hassfest: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout the repository 17 | uses: actions/checkout@v4 18 | 19 | - name: Hassfest validation 20 | uses: home-assistant/actions/hassfest@master 21 | 22 | validate-hacs: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - name: Checkout the repository 26 | uses: actions/checkout@v4 27 | 28 | - name: HACS validation 29 | uses: hacs/action@main 30 | with: 31 | category: integration 32 | 33 | validate-lint: 34 | runs-on: ubuntu-latest 35 | steps: 36 | - name: Checkout the repository 37 | uses: actions/checkout@v4 38 | 39 | - name: Install dependencies 40 | run: | 41 | python -m pip install --upgrade pip 42 | pip install pylint 43 | 44 | - name: Static analyse the code with pylint 45 | run: | 46 | pylint $(git ls-files '*.py') 47 | 48 | validate-setup: 49 | runs-on: ubuntu-latest 50 | steps: 51 | - name: Checkout the repository 52 | uses: actions/checkout@v4 53 | 54 | - name: Install the integration 55 | run: | 56 | export config_path=./test_config 57 | mkdir $config_path 58 | ./install.sh $config_path 59 | echo "default_config:" >> $config_path/configuration.yaml 60 | echo "logger:" >> $config_path/configuration.yaml 61 | echo " default: info" >> $config_path/configuration.yaml 62 | echo " logs:" >> $config_path/configuration.yaml 63 | echo " custom_components.xiaomi_home: debug" >> $config_path/configuration.yaml 64 | 65 | - name: Setup Home Assistant 66 | id: homeassistant 67 | uses: ludeeus/setup-homeassistant@main 68 | with: 69 | config-dir: ./test_config 70 | -------------------------------------------------------------------------------- /doc/CONTRIBUTING_zh.md: -------------------------------------------------------------------------------- 1 | # 贡献指南 2 | 3 | [English](../CONTRIBUTING.md) | [简体中文](./CONTRIBUTING_zh.md) 4 | 5 | 感谢您考虑为我们的项目做出贡献!您的努力将使我们的项目变得更好。 6 | 7 | 在您开始贡献之前,请花一点时间阅读以下准则: 8 | 9 | ## 我可以如何贡献? 10 | 11 | ### 报告问题 12 | 13 | 如果您在项目中遇到错误,请在 GitHub 上[报告问题](https://github.com/XiaoMi/ha_xiaomi_home/issues/new/),并提供关于错误的详细信息,包括复现步骤、 debug 级日志以及错误出现的时间。 14 | 15 | 集成开启 debug 级日志的[方法](https://www.home-assistant.io/integrations/logger/#log-filters): 16 | 17 | ``` 18 | # configuration.yaml 设置打印日志等级 19 | 20 | logger: 21 | default: critical 22 | logs: 23 | custom_components.xiaomi_home: debug 24 | ``` 25 | 26 | ### 建议增强功能 27 | 28 | 如果您有增强或新功能的想法,欢迎您在 GitHub 讨论区[创建想法](https://github.com/XiaoMi/ha_xiaomi_home/discussions/new?category=ideas) 。我们期待您的建议! 29 | 30 | ### 贡献代码 31 | 32 | 1. Fork 该仓库并从 `main` 创建您的分支。 33 | 2. 确保您的代码符合项目的编码规范。 34 | 3. 确保您的提交消息描述清晰。 35 | 4. 提交请求应附有明确的问题描述和解决方案。 36 | 5. 如果必要,请更新文档。 37 | 6. 请运行测试并确保测试通过。 38 | 39 | ## 拉取请求准则 40 | 41 | 在提交拉取请求之前,请确保满足以下要求: 42 | 43 | - 您的拉取请求解决了单个问题或功能。 44 | - 您已在本地测试过您的更改。 45 | - 您的代码遵循项目的[代码规范](#代码规范),已运行 [`pylint`](https://github.com/google/pyink) 搭配本项目的 [pylintrc](../.pylintrc) 检查代码。 46 | - 所有现有测试都通过,并且如果适用,您已添加了新的测试。 47 | - 任何依赖更改都有文档说明。 48 | 49 | ## 代码规范 50 | 51 | 本项目的代码格式遵循 [Google Style](https://google.github.io/styleguide/pyguide.html) 。请确保您的贡献符合该指南。 52 | 53 | ## Commit Message 格式 54 | 55 | ``` 56 | : 57 | 58 | 59 | 60 |