构建使用 Pub/Sub 的 Google Chat 应用

本页面介绍了如何创建使用 Cloud Pub/Sub 从 Chat 接收事件的聊天应用。如果您的 Chat 应用位于防火墙后面,或者您想使用 Google Workspace Events API 发送或接收有关 Chat 聊天室或用户的事件,此架构会非常有用。

下图展示了使用 Pub/Sub 构建的聊天应用的架构:

使用 Pub/Sub 实现的聊天应用的架构。

在上图中,与 Pub/Sub 聊天应用互动的用户的信息流如下所示:

  1. 用户通过以下方式与 Chat 应用互动:例如,向 Chat 应用发送消息、发出命令,或在 Chat 聊天室中添加或移除 Chat 应用。

  2. Chat 将消息发送到 Pub/Sub 主题。

  3. 应用服务器(即包含 Chat 应用逻辑的云端或本地系统)订阅 Pub/Sub 主题,以便通过防火墙接收消息。

  4. Chat 应用可以选择调用 Chat API 来异步发布消息或执行其他操作。

前提条件

Node.js

Python

Java

启用 API

在使用 Google API 之前,您需要在 Google Cloud 项目中将其开启。您可以在单个 Google Cloud 项目中启用一个或多个 API。
  • 在 Google Cloud 控制台中,启用 Google Chat API 和 Pub/Sub API。

    启用 API

设置 Pub/Sub

  1. 创建一个 Pub/Sub 主题,供 Chat API 向其发送消息。建议您为每个聊天应用使用一个主题。

  2. 为聊天应用创建服务账号,以便通过 Pub/Sub 和 Chat 进行授权,并将私钥文件保存到您的工作目录。

  3. 创建对主题的拉取订阅

  4. 为之前创建的服务账号分配订阅的 Pub/Sub 订阅者角色

编写脚本

在本部分中,您将定义 Chat 应用的应用逻辑。您将编写一个脚本,该脚本可向 Google Cloud 进行身份验证并订阅 Pub/Sub 主题,以接收来自 Chat 的事件,例如当用户向您的 Chat 应用发送消息时。

当脚本收到消息时,它会处理事件数据,并使用 Google Chat API 将响应发布回用户或聊天室。此设置可让您的聊天应用在防火墙后正常运行,同时仍能与 Chat 用户互动。

Node.js

  1. 在 CLI 中,提供服务账号凭据

    export GOOGLE_APPLICATION_CREDENTIALS=SERVICE_ACCOUNT_FILE_PATH
    
  2. 在 CLI 中,提供 Google Cloud 项目 ID:

    export PROJECT_ID=PROJECT_ID
    
  3. 在 CLI 中,提供您之前创建的 Pub/Sub 订阅的订阅 ID:

    export SUBSCRIPTION_ID=SUBSCRIPTION_ID
    
  4. 在工作目录中,创建一个名为 package.json 的文件。

  5. package.json 文件中,粘贴以下代码:

    {
      "name": "pub-sub-app",
      "version": "1.0.0",
      "description": "Google Chat App that listens for messages via Cloud Pub/Sub",
      "main": "index.js",
      "scripts": {
        "start": "node index.js",
        "test": "echo \"Error: no test specified\" && exit 1"
      },
      "dependencies": {
        "@google-apps/chat": "^0.4.0",
        "@google-cloud/pubsub": "^4.5.0"
      },
      "license": "Apache-2.0"
    }
    
  6. 在工作目录中,创建一个名为 index.js 的文件。

  7. index.js 中,粘贴以下代码:

    const {ChatServiceClient} = require('@google-apps/chat');
    const {MessageReplyOption} = require('@google-apps/chat').protos.google.chat.v1.CreateMessageRequest;
    const {PubSub} = require('@google-cloud/pubsub');
    const {SubscriberClient} = require('@google-cloud/pubsub/build/src/v1');
    
    // Receives messages from a pull subscription.
    function receiveMessages() {
      const chat = new ChatServiceClient({
        keyFile: process.env.GOOGLE_APPLICATION_CREDENTIALS,
        scopes: ['https://www.googleapis.com/auth/chat.bot'],
      });
    
      const subscriptionPath = new SubscriberClient()
        .subscriptionPath(process.env.PROJECT_ID, process.env.SUBSCRIPTION_ID)
      const subscription = new PubSub()
        .subscription(subscriptionPath);
    
      // Handle incoming message, then acknowledge the received message
      const messageHandler = message => {
        console.log(`Id : ${message.id}`);
        const event = JSON.parse(message.data);
        console.log(`Data : ${JSON.stringify(event)}`);
    
        // Post the response to Google Chat.
        const request = formatRequest(event);
        if (request != null) {
          chat.createMessage(request);
        }
    
        // Acknowledge the message.
        message.ack();
      }
    
      subscription.on('message', messageHandler);
      console.log(`Listening for messages on ${subscriptionPath}`);
    
      // Keep main thread from exiting while waiting for messages
      setTimeout(() => {
        subscription.removeListener('message', messageHandler);
        console.log(`Stopped listening for messages.`);
      }, 60 * 1000);
    }
    
    // Send message to Google Chat based on the type of event
    function formatRequest(event) {
      const chatEvent = event.chat || {};
    
      // If the app was removed, we don't respond.
      if (chatEvent.removedFromSpacePayload) {
        console.log(`App removed from space.`);
        return null;
      }
    
      const payload = chatEvent.messagePayload || chatEvent.addedToSpacePayload;
      const spaceName = payload?.space?.name;
    
      if (!spaceName) {
        console.log('No space name in event.');
        return null;
      }
    
      if (chatEvent.addedToSpacePayload) {
        // An app can also be added to a space by @mentioning it in a
        // message. In that case, we fall through to the message case
        // and let the app respond. If the app was added using the
        // invite flow, we just post a thank you message in the space.
        return {
          parent: spaceName,
          message: { text: 'Thank you for adding me!' },
        };
      } else if (chatEvent.messagePayload) {
        // In case of message, post the response in the same thread.
        const message = chatEvent.messagePayload.message;
        return {
          parent: spaceName,
          messageReplyOption: MessageReplyOption.REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD,
          message: {
            text: 'You said: `' + message.text + '`',
            thread: { name: message.thread.name },
          },
        };
      }
    }
    
    if (!process.env.PROJECT_ID) {
      console.log('Missing PROJECT_ID env var.');
      process.exit(1);
    }
    if (!process.env.SUBSCRIPTION_ID) {
      console.log('Missing SUBSCRIPTION_ID env var.');
      process.exit(1);
    }
    if (!process.env.GOOGLE_APPLICATION_CREDENTIALS) {
      console.log('Missing GOOGLE_APPLICATION_CREDENTIALS env var.');
      process.exit(1);
    }
    
    receiveMessages();
    

Python

  1. 在 CLI 中,提供服务账号凭据

    export GOOGLE_APPLICATION_CREDENTIALS=SERVICE_ACCOUNT_FILE_PATH
    
  2. 在 CLI 中,提供 Google Cloud 项目 ID:

    export PROJECT_ID=PROJECT_ID
    
  3. 在 CLI 中,提供您之前创建的 Pub/Sub 订阅的订阅 ID:

    export SUBSCRIPTION_ID=SUBSCRIPTION_ID
    
  4. 在工作目录中,创建一个名为 requirements.txt 的文件。

  5. requirements.txt 文件中,粘贴以下代码:

    google-cloud-pubsub>=2.23.0
    google-apps-chat==0.1.9
    
  6. 在工作目录中,创建一个名为 app.py 的文件。

  7. app.py 中,粘贴以下代码:

    import json
    import logging
    import os
    import sys
    import time
    from google.apps import chat_v1 as google_chat
    from google.cloud import pubsub_v1
    from google.oauth2.service_account import Credentials
    
    def receive_messages():
      """Receives messages from a pull subscription."""
    
      scopes = ['https://www.googleapis.com/auth/chat.bot']
      service_account_key_path = os.environ.get('GOOGLE_APPLICATION_CREDENTIALS')
      creds = Credentials.from_service_account_file(service_account_key_path)
      chat = google_chat.ChatServiceClient(
          credentials=creds, client_options={'scopes': scopes}
      )
    
      project_id = os.environ.get('PROJECT_ID')
      subscription_id = os.environ.get('SUBSCRIPTION_ID')
      subscriber = pubsub_v1.SubscriberClient()
      subscription_path = subscriber.subscription_path(project_id, subscription_id)
    
      # Handle incoming message, then acknowledge the received message
      def callback(message):
        event = json.loads(message.data)
        logging.info('Data : %s', event)
    
        # Post the response to Google Chat.
        request = format_request(event)
        if request is not None:
          chat.create_message(request)
    
        # Acknowledge the message.
        message.ack()
    
      subscriber.subscribe(subscription_path, callback = callback)
      logging.info('Listening for messages on %s', subscription_path)
    
      # Keep main thread from exiting while waiting for messages
      while True:
        time.sleep(60)
    
    def format_request(event):
      """Send message to Google Chat based on the type of event.
      Args:
        event: A dictionary with the event data.
      """
      chat_event = event.get('chat', {})
    
      # If the app was removed, we don't respond.
      if 'removedFromSpacePayload' in chat_event:
        logging.info('App removed from space.')
        return
    
      payload = chat_event.get('messagePayload') or chat_event.get(
          'addedToSpacePayload'
      )
      space_name = payload.get('space', {}).get('name') if payload else None
    
      if not space_name:
        logging.warning('No space name in event.')
        return
    
      if 'addedToSpacePayload' in chat_event:
        # An app can also be added to a space by @mentioning it in a
        # message. In that case, we fall through to the message case
        # and let the app respond. If the app was added using the
        # invite flow, we just post a thank you message in the space.
        return google_chat.CreateMessageRequest(
            parent = space_name,
            message = {
              'text': 'Thank you for adding me!'
            }
        )
      elif 'messagePayload' in chat_event:
        # In case of message, post the response in the same thread.
        message = chat_event['messagePayload']['message']
        return google_chat.CreateMessageRequest(
            parent = space_name,
            message_reply_option = google_chat.CreateMessageRequest.MessageReplyOption.REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD,
            message = {
              'text': 'You said: `' + message['text'] + '`',
              'thread': {
                'name': message['thread']['name']
              }
            }
        )
    
    if __name__ == '__main__':
      if 'PROJECT_ID' not in os.environ:
        logging.error('Missing PROJECT_ID env var.')
        sys.exit(1)
    
      if 'SUBSCRIPTION_ID' not in os.environ:
        logging.error('Missing SUBSCRIPTION_ID env var.')
        sys.exit(1)
    
      if 'GOOGLE_APPLICATION_CREDENTIALS' not in os.environ:
        logging.error('Missing GOOGLE_APPLICATION_CREDENTIALS env var.')
        sys.exit(1)
    
      logging.basicConfig(
          level=logging.INFO,
          style='{',
          format='{levelname:.1}{asctime} {filename}:{lineno}] {message}')
      receive_messages()
    

Java

  1. 在 CLI 中,提供服务账号凭据

    export GOOGLE_APPLICATION_CREDENTIALS=SERVICE_ACCOUNT_FILE_PATH
    
  2. 在 CLI 中,提供 Google Cloud 项目 ID:

    export PROJECT_ID=PROJECT_ID
    
  3. 在 CLI 中,提供您之前创建的 Pub/Sub 订阅的订阅 ID:

    export SUBSCRIPTION_ID=SUBSCRIPTION_ID
    
  4. 在工作目录中,创建一个名为 pom.xml 的文件。

  5. pom.xml 文件中,粘贴以下代码:

    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
     <modelVersion>4.0.0</modelVersion>
    
     <groupId>com.google.chat.addon</groupId>
     <artifactId>pubsub-addon-chat-app</artifactId>
     <version>0.1.0</version>
    
     <name>pubsub-addon-chat-app-java</name>
    
     <properties>
       <maven.compiler.release>11</maven.compiler.release>
       <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
     </properties>
    
     <dependencyManagement>
       <dependencies>
         <dependency>
           <groupId>com.google.cloud</groupId>
           <artifactId>libraries-bom</artifactId>
           <version>26.41.0</version> <!-- Use a recent BOM version -->
           <type>pom</type>
           <scope>import</scope>
         </dependency>
       </dependencies>
     </dependencyManagement>
    
     <dependencies>
       <!-- Google Chat GAPIC library -->
       <dependency>
         <groupId>com.google.cloud</groupId>
         <artifactId>google-cloud-chat</artifactId>
       </dependency>
       <!-- Google Cloud Pub/Sub library -->
       <dependency>
         <groupId>com.google.cloud</groupId>
         <artifactId>google-cloud-pubsub</artifactId>
       </dependency>
       <!-- Google Apps Add-ons Event Object -->
       <dependency>
         <groupId>com.google.apps.addons.v1</groupId>
         <artifactId>google-apps-addons-v1-java</artifactId>
         <version>0.2.0</version> <!-- Check for latest version -->
       </dependency>
       <!-- Protobuf JSON utility -->
       <dependency>
         <groupId>com.google.protobuf</groupId>
         <artifactId>protobuf-java-util</artifactId>
       </dependency>
       <!-- Google Auth Library -->
       <dependency>
         <groupId>com.google.auth</groupId>
         <artifactId>google-auth-library-oauth2-http</artifactId>
       </dependency>
       <dependency>
         <groupId>com.google.api</groupId>
         <artifactId>gax</artifactId>
       </dependency>
       <!-- JSON utilities for PubSub message (if needed, though protobuf-java-util is primary for EventObject) -->
       <dependency>
         <groupId>com.fasterxml.jackson.core</groupId>
         <artifactId>jackson-databind</artifactId>
         <version>2.14.2</version>
       </dependency>
       <dependency>
         <groupId>org.slf4j</groupId>
         <artifactId>slf4j-jdk14</artifactId>
         <version>1.7.36</version>
         <scope>runtime</scope>
       </dependency>
     </dependencies>
    
     <build>
       <plugins>
         <plugin>
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-compiler-plugin</artifactId>
           <version>3.13.0</version>
           <configuration>
             <source>11</source>
             <target>11</target>
           </configuration>
         </plugin>
         <plugin>
           <groupId>org.codehaus.mojo</groupId>
           <artifactId>exec-maven-plugin</artifactId>
           <version>3.3.0</version>
           <configuration>
             <mainClass>Main</mainClass>
           </configuration>
         </plugin>
       </plugins>
     </build>
    </project>
    
  6. 在工作目录中,创建目录结构 src/main/java

  7. src/main/java 目录中,创建一个名为 Main.java 的文件。

  8. Main.java 中,粘贴以下代码:

    import com.fasterxml.jackson.databind.JsonNode;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.google.api.gax.core.FixedCredentialsProvider;
    import com.google.auth.oauth2.GoogleCredentials;
    import com.google.chat.v1.ChatServiceClient;
    import com.google.chat.v1.ChatServiceSettings;
    import com.google.chat.v1.CreateMessageRequest;
    import com.google.chat.v1.CreateMessageRequest.MessageReplyOption;
    import com.google.chat.v1.Message;
    import com.google.chat.v1.Thread;
    import com.google.cloud.pubsub.v1.AckReplyConsumer;
    import com.google.cloud.pubsub.v1.MessageReceiver;
    import com.google.cloud.pubsub.v1.Subscriber;
    import com.google.pubsub.v1.ProjectSubscriptionName;
    import com.google.pubsub.v1.PubsubMessage;
    import java.io.FileInputStream;
    import java.util.Collections;
    
    public class Main {
    
      public static final String PROJECT_ID_ENV_PROPERTY = "PROJECT_ID";
      public static final String SUBSCRIPTION_ID_ENV_PROPERTY = "SUBSCRIPTION_ID";
      public static final String CREDENTIALS_PATH_ENV_PROPERTY = "GOOGLE_APPLICATION_CREDENTIALS";
    
      public static void main(String[] args) throws Exception {
        ProjectSubscriptionName subscriptionName =
            ProjectSubscriptionName.of(
                System.getenv(Main.PROJECT_ID_ENV_PROPERTY),
                System.getenv(Main.SUBSCRIPTION_ID_ENV_PROPERTY));
    
        // Instantiate app, which implements an asynchronous message receiver.
        EchoApp echoApp = new EchoApp();
    
        // Create a subscriber for <var>SUBSCRIPTION_ID</var> bound to the message receiver
        final Subscriber subscriber = Subscriber.newBuilder(subscriptionName, echoApp).build();
        System.out.println("Subscriber is listening to events...");
        subscriber.startAsync();
    
        // Wait for termination
        subscriber.awaitTerminated();
      }
    }
    
    /**
     * A demo app which implements {@link MessageReceiver} to receive messages.
     * It echoes incoming messages.
     */
    class EchoApp implements MessageReceiver {
    
      // Path to the private key JSON file of the service account to be used for posting response
      // messages to Google Chat.
      // In this demo, we are using the same service account for authorizing with Cloud Pub/Sub to
      // receive messages and authorizing with Google Chat to post messages. If you are using
      // different service accounts, set the path to the private key JSON file of the service
      // account used to post messages to Google Chat here.
      private static final String SERVICE_ACCOUNT_KEY_PATH =
        System.getenv(Main.CREDENTIALS_PATH_ENV_PROPERTY);
    
      // Developer code for Google Chat API scope.
      private static final String GOOGLE_CHAT_API_SCOPE = "https://www.googleapis.com/auth/chat.bot";
    
      private static final String ADDED_RESPONSE = "Thank you for adding me!";
    
      ChatServiceClient chatServiceClient;
    
      EchoApp() throws Exception {
        GoogleCredentials credential =
            GoogleCredentials.fromStream(new FileInputStream(SERVICE_ACCOUNT_KEY_PATH))
                .createScoped(Collections.singleton(GOOGLE_CHAT_API_SCOPE));
    
        // Create the ChatServiceSettings with the app credentials
        ChatServiceSettings chatServiceSettings =
            ChatServiceSettings.newBuilder()
                .setCredentialsProvider(FixedCredentialsProvider.create(credential))
                .build();
    
        // Set the Chat service client
        chatServiceClient = ChatServiceClient.create(chatServiceSettings);
      }
    
      // Called when a message is received by the subscriber.
      @Override
      public void receiveMessage(PubsubMessage pubsubMessage, AckReplyConsumer consumer) {
        System.out.println("Id : " + pubsubMessage.getMessageId());
        // Handle incoming message, then acknowledge the received message
        try {
          ObjectMapper mapper = new ObjectMapper();
          JsonNode dataJson = mapper.readTree(pubsubMessage.getData().toStringUtf8());
          System.out.println("Data : " + dataJson.toString());
          handle(dataJson);
          consumer.ack();
        } catch (Exception e) {
          System.out.println(e);
          // Negative acknowledgement makes Pub/Sub redeliver the message.
          consumer.nack();
        }
      }
    
      // Send message to Google Chat based on the type of event.
      public void handle(JsonNode eventJson) throws Exception {
        // Google Chat events for add-ons are wrapped in a 'chat' object.
        if (!eventJson.has("chat")) {
          System.out.println("Ignored: Not a Chat event (missing 'chat' field).");
          return;
        }
    
        JsonNode chatNode = eventJson.get("chat");
        CreateMessageRequest createMessageRequest = null;
    
        if (chatNode.has("messagePayload")) {
          // HANDLE MESSAGE
          JsonNode messagePayload = chatNode.get("messagePayload");
          JsonNode message = messagePayload.get("message");
          JsonNode space = messagePayload.get("space");
    
          String spaceName = space.get("name").asText();
          String userText = message.has("text") ? message.get("text").asText() : "";
          String threadName = message.has("thread") ? message.get("thread").get("name").asText() : "";
    
          System.out.println("Received message in " + spaceName + ": " + userText);
    
          createMessageRequest =
              CreateMessageRequest.newBuilder()
                  .setParent(spaceName)
                  .setMessageReplyOption(MessageReplyOption.REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD)
                  .setMessage(
                      Message.newBuilder()
                          .setText("You said: `" + userText + "`")
                          .setThread(Thread.newBuilder().setName(threadName).build())
                          .build())
                  .build();
    
        } else if (chatNode.has("addedToSpacePayload")) {
          // HANDLE ADDED TO SPACE
          JsonNode addedPayload = chatNode.get("addedToSpacePayload");
          JsonNode space = addedPayload.get("space");
          String spaceName = space.get("name").asText();
    
          System.out.println("Added to space: " + spaceName);
    
          createMessageRequest =
              CreateMessageRequest.newBuilder()
                  .setParent(spaceName)
                  .setMessage(Message.newBuilder().setText(ADDED_RESPONSE).build())
                  .build();
    
        } else if (chatNode.has("removedFromSpacePayload")) {
          System.out.println("Removed from space.");
          return;
        } else {
          System.out.println("Ignored: Unhandled Chat event type.");
          return;
        }
    
        if (createMessageRequest != null) {
          // Post the response to Google Chat.
          chatServiceClient.createMessage(createMessageRequest);
          System.out.println("Sent reply.");
        }
      }
    }
    

配置聊天应用

在 Google Cloud 控制台中配置 Chat 应用,以提供其名称和头像等详细信息,并设置与 Pub/Sub 主题的连接。

通过连接到 Pub/Sub 主题,您可以让 Chat 向您的应用发送事件。订阅该主题的脚本随后可以接收这些事件并响应用户。

  1. 在 Google Cloud 控制台中,依次前往菜单 > API 和服务 > 已启用的 API 和服务 > Google Chat API > 配置

    前往 Chat API 配置

  2. 为 Pub/Sub 配置聊天应用:

    1. 应用名称中,输入 Add-on Chat App
    2. 头像网址中,输入 https://developers.google.com/workspace/add-ons/images/quickstart-app-avatar.png
    3. 说明中,输入 Quickstart app
    4. 功能下,选择加入聊天室和群组对话
    5. 连接设置下,选择 Cloud Pub/Sub,然后粘贴您之前创建的 Pub/Sub 主题的名称。
    6. 公开范围下方,选择面向网域中的特定人员和群组提供此 Google Chat 聊天应用,然后输入您的电子邮件地址。
    7. 日志下,选择将错误记录到 Logging
  3. 点击保存

配置完 Chat 应用后,您必须更新 Pub/Sub 配置。

  1. Chat API 配置页面上的连接设置下,复制服务账号电子邮件地址,这是为您的 Google Cloud 项目生成的唯一电子邮件地址。
  2. 通过将 Pub/Sub Publisher 角色分配给您之前复制的服务账号电子邮件地址,授予 Chat 向该主题发布消息的权限

该应用已准备好在 Chat 中接收和回复消息。

运行脚本

在 CLI 中,切换到工作目录并运行脚本:

Node.js

npm install
npm start

Python

python -m venv env
source env/bin/activate
pip install -r requirements.txt -U
python app.py

Java

mvn compile exec:java -Dexec.mainClass=Main

运行代码后,应用会开始监听发布到 Pub/Sub 主题的消息。

测试聊天应用

如需测试 Chat 应用,请打开与该 Chat 应用的私信对话,然后发送消息:

  1. 使用您在添加自己为可信测试员时提供的 Google Workspace 账号打开 Google Chat。

    前往 Google Chat

  2. 点击 发起新聊天
  3. 添加 1 位或多位用户字段中,输入 Chat 应用的名称。
  4. 从结果中选择您的聊天应用。系统会打开私信对话。

  5. 在与应用来往的新私信中,输入 Hello 并按 enter

如需添加可信测试员并详细了解如何测试互动功能,请参阅测试 Google Chat 应用的互动功能

问题排查

当 Google Chat 应用或卡片返回错误时,Chat 界面会显示一条消息,提示“出了点问题”。或“无法处理您的请求”。有时,Chat 界面不会显示任何出错提示,但聊天应用或卡片会产生意外结果;例如,卡片消息可能不会显示。

虽然聊天界面中可能不会显示出错提示,但当您为聊天应用启用日志记录功能后,系统会提供描述性出错提示和日志数据,帮助您修复错误。如需有关查看、调试和修复错误的帮助,请参阅排查和修复 Google Chat 错误

清理

为避免系统因本教程中使用的资源向您的 Google Cloud 账号收取费用,我们建议您删除 Cloud 项目。

  1. 在 Google Cloud 控制台中,前往管理资源页面。依次点击 菜单 > IAM 和管理 > 管理资源

    前往资源管理器

  2. 在项目列表中,选择要删除的项目,然后点击删除
  3. 在对话框中输入项目 ID,然后点击关停以删除项目。