csenayeem025/google-workspace-sdk

Google Workspace API SDK for Laravel

1.0.0 2023-10-10 07:58 UTC

This package is auto-updated.

Last update: 2024-09-10 11:28:03 UTC


README

概述

Google Workspace SDK 是一个开源的 Composer 包,由 GitLab IT Engineering 创建,用于在 GitLab Access Manager Laravel 应用程序中连接到 Google API 端点,用于用户、组、组成员以及其他相关功能的配置和取消配置。

免责声明:这不是由 Google 或 GitLab 产品和开发团队维护的官方包。这是一个我们作为公司价值观的一部分开源的 GitLab IT 部门的内部工具。

请自行承担风险,并为您遇到的所有错误创建问题。

依赖项

注意:此包运行需要 glamstack/google-auth-sdk 包。这已在 composer.json 文件中配置为必需包,并在安装此包时应自动加载。

默认情况下,此包的所有配置都将位于 glamstack-google-workspace.php 文件中,当安装此包时将加载。有关进一步指导,请参阅 安装文档

维护者

工作原理

该包利用 glamstack/google-auth-sdk 包创建 Google JWT Web Token 以与 Google Workspace API 进行身份验证。

有关 glamstack/google-auth-sdk 的更多信息,请参阅 Google Auth SDK README.md

此包并不打算为 Google Workspace API 的每个端点提供功能。端点将根据需要构建。如果您希望添加任何其他端点,请参阅 CONTRIBUTING

如果您需要的端点尚未创建,我们已提供 REST 类,可以对 Google Workspace API 文档中找到的任何端点执行 GET、POST、PUT 和 DELETE 请求。该类将处理 API 响应、错误处理和分页。

⚠️ PATCH 请求目前不可用,但将在将来实现。

此包基于由 Guzzle HTTP 客户端提供的 Laravel HTTP 客户端的简单性,为 Google Workspace API 的响应提供“最后代码解析”,以改善开发人员的体验。

// Initialized Client with `connection_key` parameter
$google_workspace_api = new \Glamstack\GoogleWorkspace\ApiClient('workspace');
  
// Retrieves a paginated list of either deleted users or all users in a domain.  
// https://developers.google.com/admin-sdk/directory/reference/rest/v1/users/list  
$records = $google_workspace_api->rest()->get('https://admin.googleapis.com/admin/directory/v1/users');  
  
// Retrieves a paginated list of either deleted users or all users in a domain  
// with query parameters included.  
// https://developers.google.com/admin-sdk/directory/reference/rest/v1/users/list  
// https://developers.google.com/admin-sdk/directory/reference/rest/v1/users/list#OrderBy  
// https://developers.google.com/admin-sdk/directory/v1/guides/search-users  
$records = $google_workspace_api->rest()->get('https://admin.googleapis.com/admin/directory/v1/users',[  
    'maxResults' => '200',
    'orderBy' => 'EMAIL',
    'query' => [
        'orgDepartment' => 'Test Department'
    ]
]);  
  
// Get a specific user from Google Workspace  
// https://developers.google.com/admin-sdk/directory/reference/rest/v1/users/get  
$user_key = 'klibby@example.com';  
$record = $google_workspace_api->rest()->get('https://admin.googleapis.com/admin/directory/v1/users/'.$user_key);  
  
// Create new Google Workspace User  
// https://developers.google.com/admin-sdk/directory/reference/rest/v1/users/insert  
// https://developers.google.com/admin-sdk/directory/reference/rest/v1/users#User  
$record = $google_workspace_api->rest()->post('https://admin.googleapis.com/admin/directory/v1/users', [
    'name' => [
        'familyName' => 'Libby',
        'givenName' => 'Kate'
    ],
    'password' => 'ac!dBurnM3ss3sWithTheB4$t',
    'primaryEmail' => 'klibby@example.com'
]);  
  
// Update an existing Google Workspace User  
// https://developers.google.com/admin-sdk/directory/reference/rest/v1/users/update  
$user_key = 'klibby@example.com';  
$record = $google_workspace_api->rest()->put('https://admin.googleapis.com/admin/directory/v1/users/'.$user_key, [  
    'name' => [
        'givenName' => 'Libby-Murphy'
    ]
]);  
  
// Delete a Google Workspace User  
// https://developers.google.com/admin-sdk/directory/reference/rest/v1/users/delete  
$user_key = 'klibby@example.com';  
$record = $google_workspace_api->rest()->delete('https://admin.googleapis.com/admin/directory/v1/users/'.$user_key);  

包初始化

此包通过配置初始化,请参阅 如何使用配置文件初始化 了解初始化方法的说明。

安装

要求

添加 Composer 包

本包使用日历版本控制

我们建议始终在您的composer.json文件中使用特定版本,并在假定最新版本适合您的项目之前,查看变更日志以查看每次发布中的重大更改。

composer require csenayeem025/google-workspace-sdk

如果您正在为该包做出贡献,请参阅CONTRIBUTING,了解配置具有符号链接的本地composer包的说明。

发布配置文件

php artisan vendor:publish --tag=glamstack-google-workspace

版本升级

如果您已升级到该包的新版本,请备份现有的配置文件,以避免您的自定义配置被覆盖。

cp config/glamstack-google-workspace.php config/glamstack-google-workspace.php.bak

php artisan vendor:publish --tag=glamstack-google-workspace

日历版本控制

GitLab IT工程团队使用的是日历版本控制(CalVer)的修改版本,而不是语义版本控制(SemVer)。CalVer有YY(例如2021 => 21),但对我们来说,版本21.xx感觉不太直观。由于我们的团队从2021年开始,我们决定只使用年份的最后一位整数(2021 => 1.x,2022 => 2.x等)。

版本号表示发布日期,格式为vY.M.D

为什么我们不使用语义版本控制

  1. 我们持续向main/master/production发布,并在大多数版本中做出重大更改,因此对我们来说,具有语义向后兼容的版本号是不直观的。
  2. 我们不喜欢争论如何称呼我们的发布/里程碑,以及它是否是主要版本、次要版本还是补丁版本。我们只是编写代码,编写变更日志,并在完成的那一天发布。变更日志发布日期成为标记的版本号(例如2022-02-01v2.2.1)。我们可能会为更大的发布使用更大的版本号(例如v2.2),但这仅用于月度里程碑计划和规范目的。所有代码标记都包括发布日期(例如v2.2.1)。
  3. 这使我们能够使用GitLab CI/CD自动化版本标记过程,基于管道作业运行的日期。
  4. 我们将在计划变更窗口期间更新使用此包的所有项目composer.json文件,以特定或新版本号,而无需担心“保持最新版本”的差异和/或重大更改。我们不维护任何分叉或分支。
  5. 我们的包使用您现有Laravel应用程序中的底层包,因此保持您的Laravel应用程序版本更新可以解决大多数安全问题。

初始化SDK

API客户端的初始化可以通过传递一个(字符串)连接密钥或传递一个(数组)连接配置来完成。

Google API身份验证

该包使用glamstack/google-auth-sdk包来创建Google JWT Web Token,以与Google Cloud API端点进行身份验证。

有关glamstack/google-auth-sdk的更多信息,请参阅Google Auth SDK README.md

连接密钥

我们使用连接密钥的概念,它引用了config/glamstack-google-workspace.php中的配置数组,允许您预先配置一个或多个API连接。

每个连接密钥都与一个GCP服务账户的JSON密钥相关联。这可以用来配置不同的认证范围连接和权限,用于您的GCP组织或不同的GCP项目(根据您使用的API调用)。这允许针对特定的API调用实现最小权限,并且您还可以配置多个与相同GCP项目关联的连接,这些连接具有不同的API令牌和权限级别。

示例连接密钥初始化

// Initialize the SDK using the `test` configuration from `glamstack-google-workspace.php`
$client = new Glamstack\GoogleWorkspace\ApiClient('test');

示例连接密钥配置

return [
    'connections' => [
        'test' => [
            'api_scopes' => [
                'https://www.googleapis.com/auth/admin.directory.group',
                'https://www.googleapis.com/auth/admin.directory.user'
            ],
            'json_key_file_path' => storage_path(env('GOOGLE_WORKSPACE_TEST_JSON_KEY_FILE_PATH')),
            'log_channels' => ['single'],
            'customer_id' => env('GOOGLE_WORKSPACE_TEST_CUSTOMER_ID'),
            'domain' => env('GOOGLE_WORKSPACE_TEST_DOMAIN'),
            'subject_email' => env('GOOGLE_WORKSPACE_TEST_SUBJECT_EMAIL'),
            'test_group_email' => env('GOOGLE_WORKSPACE_TEST_GROUP_EMAIL')
        ],
    ]
]

动态连接配置数组

如果您不想预先配置您的连接,并希望动态使用存储在您数据库中的连接变量,您可以通过数组传递所需的配置(参见示例连接配置数组初始化),使用ApiClient构造方法的第二个参数中的connection_config数组。

必需参数

在您的文件系统上使用JSON密钥文件

$client = new Glamstack\GoogleWorkspace\ApiClient(null, [
    'api_scopes' => [
        'https://www.googleapis.com/auth/admin.directory.group',
        'https://www.googleapis.com/auth/contacts'
    ],
    'customer_id' => config('tests.connections.test.customer_id'),
    'domain' => config('tests.connections.test.domain'),
    'json_key_file_path' => storage_path('keys/glamstack-google-workspace/test.json'),
    'log_channels' => ['single'],
    'subject_email' => config('tests.connections.test.subject_email')
]);

在您的数据库中使用JSON密钥字符串

安全警告:您绝对不应该将服务账户密钥(JSON内容)作为变量提交到源代码中,以避免泄露您GCP组织或项目的凭据。

建议在加密之前将JSON密钥转换为base 64编码的字符串,因为这是GCP服务账户API用于privateKeyData字段的格式。

// Get service account from your model (`GoogleServiceAccount` is an example)
$service_account = \App\Models\GoogleServiceAccount::where('id', '123456')->firstOrFail();

// Get JSON key string from database column that has an encrypted value
$json_key_string = decrypt(json_decode($service_account->json_key));

$client = new \Glamstack\GoogleWorkspace\ApiClient(null, [
    'api_scopes' => [
        'https://www.googleapis.com/auth/admin.directory.group',
        'https://www.googleapis.com/auth/contacts'
    ],
    'customer_id' => config('tests.connections.test.customer_id'),
    'domain' => config('tests.connections.test.domain'),
    'json_key' => $json_key_string,
    'log_channels' => ['single'],
    'subject_email' => config('tests.connections.test.subject_email')
]);

以下示例显示了存储在您的数据库中的JSON密钥的值。

// Get service account from your model (`GoogleServiceAccount` is an example)
$service_account = \App\Models\GoogleServiceAccount::where('id', '123456')->firstOrFail();

dd(decrypt(json_decode($service_account->json_key));
// {
//     "type": "service_account",
//     "project_id": "project_id",
//     "private_key_id": "key_id",
//     "private_key": "key_data",
//     "client_email": "xxxxx@xxxxx.iam.gserviceaccount.com",
//     "client_id": "123455667897654",
//     "auth_uri": "https://#/o/oauth2/auth",
//     "token_uri": "https://oauth2.googleapis.com/token",
//     "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
//     "client_x509_cert_url": "some stuff"
// }

自定义日志配置

默认情况下,我们使用所有日志都配置在您的应用程序的config/logging.php文件中的single通道。这会将所有Google Workspace日志消息发送到storage/logs/laravel.log文件。

如果您希望将Google Workspace日志显示在单独的日志文件中,以便更容易地进行故障排除,而无需无关的日志消息,您可以创建一个自定义日志通道。例如,我们建议使用glamstack-google-workspace的值,但您可以选择任何您喜欢的名称。

将自定义日志通道添加到config/logging.php

    'channels' => [  
        // Add anywhere in the `channels` array  
        'glamstack-google-workspace' => [
            'name' => 'glamstack-google-workspace',
            'driver' => 'single',
            'level' => 'debug',
            'path' => storage_path('logs/glamstack-google-workspace.log')
        ]
    ],  

更新channels.stack.channels数组以包括自定义通道的数组键(例如glamstack-google-workspace)。请确保将glamstack-google-workspace添加到现有的数组值中,而不是替换现有的值。

    'channels' => [
        'stack' => [
            'driver' => 'stack',
            'channels' => [
                'single','slack', 'glamstack-google-workspace'
            ],
            'ignore_exceptions' => false
        ]
    ],  

REST API请求

您可以对Google Workspace Admin SDK Directory文档中任何资源端点的API进行请求。

内联使用

// Initialize the SDK  
$api_client = new \Glamstack\GoogleWorkspace\ApiClient('workspace');
$response = $api_client->rest()->get('https://admin.googleapis.com/admin/directory/v1/users');

GET请求

当使用REST类时,端点将需要完整的端点URL。

例如,列出Google Workspace用户API文档显示了端点

GET https://admin.googleapis.com/admin/directory/v1/users  

使用SDK时,您使用带有Google Workspace用户端点的get()方法。

$google_workspace_api->rest()->get('https://admin.googleapis.com/admin/directory/v1/users');  

您还可以使用变量或数据库模型来获取用于构建端点数据。

$endpoint = 'https://admin.googleapis.com/admin/directory/v1/users';  
$records = $google_workspace_api->rest()->get($endpoint);  

以下是一些使用端点的更多示例。

// Get a list of Google Workspace Users  
// https://developers.google.com/admin-sdk/directory/reference/rest/v1/users/list  
$records = $google_workspace_api->rest()->get('https://admin.googleapis.com/admin/directory/v1/users');  
  
// Get a specific Google Workspace User  
// https://developers.google.com/admin-sdk/directory/reference/rest/v1/users/get  
$user_key = 'klibby@example.com';  
$record = $google_workspace_api->get('https://admin.googleapis.com/admin/directory/v1/users/' . $userKey);  

带有查询字符串参数的GET请求

get()方法的第二个参数是一个可选的数组参数,该参数由SDK和Laravel HTTP客户端解析,并自动添加?&作为查询字符串。

// Retrieves a paginated list of either deleted users or all users in a domain  
// with query parameters included.  
// https://developers.google.com/admin-sdk/directory/reference/rest/v1/users/list  
// https://developers.google.com/admin-sdk/directory/reference/rest/v1/users/list#OrderBy  
// https://developers.google.com/admin-sdk/directory/v1/guides/search-users  
$records = $google_workspace_api->rest()->get('https://admin.googleapis.com/admin/directory/v1/users',[  
    'maxResults' => '200',
    'orderBy' => 'EMAIL'
]);  
  
// This will parse the array and render the query string  
// https://admin.googleapis.com/admin/directory/v1/users?maxResults='200'&orderBy='EMAIL'  

POST请求

post()方法几乎与带有参数数组的get()请求相同,但是参数作为application/json内容类型的数据通过表单数据传递,而不是在URL中作为查询字符串。这是行业标准,并非仅限于SDK。

您可以在Laravel HTTP 客户端文档中了解更多关于请求数据的信息。

// Create new Google Workspace User  
// https://developers.google.com/admin-sdk/directory/reference/rest/v1/users/insert  
// https://developers.google.com/admin-sdk/directory/reference/rest/v1/users#User  
$record = $google_workspace_api->rest()->post('https://admin.googleapis.com/admin/directory/v1/users', [  
    'name' => [
        'familyName' => 'Libby',
        'givenName' => 'Kate'
    ],
    'password' => 'ac!dBurnM3ss3sWithTheB4$t',
    'primaryEmail' => 'klibby@example.com'
]);  

PUT 请求

使用 put() 方法来更新现有记录(类似于 PATCH 请求)。您需要确保要更新的记录的 ID 存在于第一个参数(URI)中。

在大多数应用程序中,这通常是从数据库或其他位置获取的变量,而不是硬编码。

// Update an existing Google Workspace User  
// https://developers.google.com/admin-sdk/directory/reference/rest/v1/users/update  
$user_key = 'klibby@example.com';  
$record = $google_workspace_api->rest()->put('https://admin.googleapis.com/admin/directory/v1/users/' . $user_key, [
    'name' => [
        'givenName' => 'Libby-Murphy'
    ]
]);  

DELETE 请求

使用 delete() 方法来根据您提供的 ID 销毁资源。

请注意,delete() 方法将根据供应商的不同返回不同的状态码(例如,200、201、202、204 等)。Google Workspace API 将成功删除的资源返回 204 状态码。

// Delete a Google Workspace User  
// https://developers.google.com/admin-sdk/directory/reference/rest/v1/users/delete  
$user_key = 'klibby@example.com';  
$record = $google_workspace_api->rest()->delete('https://admin.googleapis.com/admin/directory/v1/users/' . $user_key);  

类方法

上面的示例展示了适用于大多数用例的基本内联使用。如果您更喜欢使用类和构造函数,下面的示例将提供一个有用的例子。

<?php  
  
use Glamstack\GoogleWorkspace\ApiClient;  
  
class GoogleWorkspaceUserService  
{  
    protected $google_workspace_api;  
    public function __construct() {
        $this->google_workspace_api = new \Glamstack\GoogleWorkspace\ApiClient();
    }  
    
    public function listUsers(array $query = []) : object
    {
        $users = $this->google_workspace_api->rest()->get('https://admin.googleapis.com/admin/directory/v1/users', $query);
        return $users->object;
    }
    
    public function getUser(string $user_key, array $query = []) : object
    {
        $user = $this->google_workspace_api->rest()->get('https://admin.googleapis.com/admin/directory/v1/users/' . $user_key, $query);
        return $user->object;
    } 
    
    public function storeUser(string $user_key, array $request_data = []) : object
    {
        $response = $this->google_workspace_api->rest()->post('https://admin.googleapis.com/admin/directory/v1/users/' . $user_key, $request_data);
        return $response->object;
    }

    public function updateUser(string $user_key, array $request_data = []) : object
    {
        $response = $this->google_workspace_api->rest()->put('https://admin.googleapis.com/admin/directory/v1/users/' . $user_key, $request_data);
        return $response->object;
    } 

    public function deleteUser(string $user_key) : bool
    {
        $response = $this->google_workspace_api->rest()->delete('https://admin.googleapis.com/admin/directory/v1/users/' . $user_key);
        return $response->status->successful;
    }
}  

API 响应

此 SDK 使用 GLAM Stack 标准 API 响应格式化。

// API Request  
$response = $this->google_workspace_api->rest()->get('https://admin.googleapis.com/admin/directory/v1/users/' . $user_key);  
  
// API Response  
$response->headers; // object  
$response->json; // json  
$response->object; // object  
$response->status; // object  
$response->status->code; // int (ex. 200)  
$response->status->ok; // bool  
$response->status->successful; // bool  
$response->status->failed; // bool  
$response->status->serverError; // bool  
$response->status->clientError; // bool  

API 响应头部

$response = $this->google_workspace_api->rest()->get('https://admin.googleapis.com/admin/directory/v1/users/' . $user_key);  
$response->headers;  
{  
    +"ETag": ""nMRgLWac8h8NyH7Uk5VvV4DiNp4uxXg5gNUd9YhyaJE/MgKWL9SwIVWCY7rRA988mR8yR-k""
    +"Content-Type": "application/json; charset=UTF-8"    
    +"Vary": "Origin X-Origin Referer"    
    +"Date": "Thu, 20 Jan 2022 16:36:03 GMT"    
    +"Server": "ESF"    
    +"Content-Length": "1257"    
    +"X-XSS-Protection": "0"    
    +"X-Frame-Options": "SAMEORIGIN"    
    +"X-Content-Type-Options": "nosniff"    
    +"Alt-Svc": "h3=":443"; ma=2592000,h3-29=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43""
}  

API 响应特定头部

$headers = (array) $response->headers;  
$content_type = $headers['Content-Type'];  
application/json  

API 响应 JSON

$response = $this->google_workspace_api->rest()->get('https://admin.googleapis.com/admin/directory/v1/users/' . $user_key);  
$response->json;  
{
    "kind":"admin#directory#user","id":"1111111111111",
    "etag":"\"nMRgLWac8h8NyH7Uk5VvV4DiNp4uxXg5gNUd9YhyaJE\/MgKWL9SwIVWCY7rRA988mR8yR-k\"",
    "primaryEmail":"klibby@example.com",
    "name":{
        "givenName":"Kate",
        "familyName":"Libby",
        "fullName":"Kate Libby"
    },
    "isAdmin":true,
    "isDelegatedAdmin":false,
    "lastLoginTime":"2022-01-18T15:26:16.000Z",
    "creationTime":"2021-12-08T13:15:43.000Z",
    "agreedToTerms":true,
    "suspended":false,
    "archived":false,
    "changePasswordAtNextLogin":false,
    "ipWhitelisted":false,
    "emails":[
        {
            "address":"klibby@example.com",
            "type":"work"
        },
        {
            "address":"klibby@example.com",
            "primary":true
        },
        {
            "address":"klibby@example.com.test-google-a.com"
        }
    ],
    "phones":[
        {
            "value":"5555555555",
            "type":"work"
        }
    ],
    "languages":[
        {
            "languageCode":"en",
            "preference":"preferred"
        }
    ],
    "nonEditableAliases":[
        "klibby@example.com.test-google-a.com"
    ],
    "customerId":"C000aaaaa",
    "orgUnitPath":"\/",
    "isMailboxSetup":true,
    "isEnrolledIn2Sv":false,
    "isEnforcedIn2Sv":false,
    "includeInGlobalAddressList":true
} 

API 响应对象

$response = $this->google_workspace_api->rest()->get('https://admin.googleapis.com/admin/directory/v1/users/' . $user_key);  
$response->object;  
{#1256  
  +"kind": "admin#directory#user"
  +"id": "1111111111111"  
  +"etag": ""nMRgLWac8h8NyH7Uk5VvV4DiNp4uxXg5gNUd9YhyaJE/MgKWL9SwIVWCY7rRA988mR8yR-k""  
  +"primaryEmail": "klibby@example.com"  
  +"name": {#1242  
    +"givenName": "Kate"    
    +"familyName": "Libby"    
    +"fullName": "Kate Libby"  
  }  
  +"isAdmin": true  
  +"isDelegatedAdmin": false  
  +"lastLoginTime": "2022-01-18T15:26:16.000Z"  
  +"creationTime": "2021-12-08T13:15:43.000Z"  
  +"agreedToTerms": true  
  +"suspended": false  
  +"archived": false  
  +"changePasswordAtNextLogin": false  
  +"ipWhitelisted": false  
  +"emails": array:3 [
    0 => {#1253  
      +"address": "klibby@example.com"      
      +"type": "work"    
    }  
    1 => {#1258  
      +"address": "klibby@example.com"      
      +"primary": true  
    }  
    2 => {#1259  
      +"address": "klibby@example.com.test-google-a.com"
    }  
  ]  
  +"phones": array:1 [    
    0 => {#1247  
      +"value": "5555555555"      
      +"type": "work"    
    }  
  ]  
  +"languages": array:1 [    
    0 => {#1250  
      +"languageCode": "en"      
      +"preference": "preferred"    
    }  
  ]
  +"nonEditableAliases": array:1 [  
    0 => "klibby@example-test.com.test-google-a.com"  
  ]  
  +"customerId": "C000aaaaa"  
  +"orgUnitPath": "/"  
  +"isMailboxSetup": true  
  +"isEnrolledIn2Sv": false  
  +"isEnforcedIn2Sv": false  
  +"includeInGlobalAddressList": true  
}  

API 响应状态

有关不同状态布尔值的更多信息,请参阅Laravel HTTP 客户端文档

$response = $this->google_workspace_api->rest()->get('https://admin.googleapis.com/admin/directory/v1/users/' . $user_key);  
$response->status;  
{  
  +"code": 200  
  +"ok": true  
  +"successful": true  
  +"failed": false  
  +"serverError": false  
  +"clientError": false  
}  

API 响应状态码

$response = $this->google_workspace_api->rest()->get('https://admin.googleapis.com/admin/directory/v1/users/' . $user_key);  
$response->status->code;  
200  

错误处理

API 响应的 HTTP 状态码包含在每条日志消息和 JSON 的 status_code 中。任何内部 SDK 错误也包括等效的状态码,具体取决于错误的类型。《message》包括 SDK 友好的消息。如果抛出异常,则 reference

如果 API 返回 5xx 错误,则 GoogleWorkspaceApiClienthandleException 方法将返回一个响应。

有关 SDK 如何处理错误和日志的更多信息,请参阅下面的日志输出

日志输出

错误消息的输出显示在 README 中,以便搜索引擎索引这些消息以支持开发者调试。任何 5xx 错误消息都将作为 Symfony\Component\HttpKernel\Exception\HttpException 或配置错误返回,包括 ApiClient::setApiConnectionVariables() 方法中的任何错误。

问题跟踪和错误报告

请访问我们的问题跟踪器并创建一个问题或对现有问题进行评论。

贡献

请参阅CONTRIBUTING.md 了解如何贡献。