ai项目(四编*/)

举报
落雨✦ 发表于 2025/10/20 13:01:23 2025/10/20
【摘要】 华为云AI Agent集成鸿蒙车票查询项目文档 一、项目概述 1.1 项目背景随着AI技术与移动应用的深度融合,本项目旨在通过华为云配置AI Agent实现智能车票查询能力,并基于鸿蒙操作系统(HarmonyOS)开发前端应用,为用户提供便捷的列车票务查询服务。 1.2 项目目标在华为云平台配置AI Agent,实现车票查询、车次信息解析等核心能力开发鸿蒙应用,通过前端页面与华为云AI A...

华为云AI Agent集成鸿蒙车票查询项目文档

一、项目概述

1.1 项目背景

随着AI技术与移动应用的深度融合,本项目旨在通过华为云配置AI Agent实现智能车票查询能力,并基于鸿蒙操作系统(HarmonyOS)开发前端应用,为用户提供便捷的列车票务查询服务。

1.2 项目目标

  • 在华为云平台配置AI Agent,实现车票查询、车次信息解析等核心能力
  • 开发鸿蒙应用,通过前端页面与华为云AI Agent交互,完成车票查询功能
  • 实现城市信息解析、令牌管理、数据存储等辅助功能,保障应用稳定性

二、技术栈

  • 前端框架:HarmonyOS ArkUI(ETS语言)
  • 后端服务:华为云AI Agent
  • 网络通信:HTTP/HTTPS、SSE(Server-Sent Events)
  • 数据存储:鸿蒙本地存储(Preferences)
  • 开发工具:DevEco Studio、华为云控制台

三、华为云AI Agent配置步骤

3.1 登录华为云控制台

  1. 访问华为云官网(https://www.huaweicloud.com/),登录账号并进入控制台
  2. 搜索“AI Agent”服务,进入服务管理页面

3.2 创建AI Agent实例

  1. 点击“创建Agent”,配置基本信息(名称、描述、所属区域等)
  2. 选择“自定义技能”,配置车票查询相关能力:
    • 定义输入参数:出发城市、到达城市、出发日期
    • 定义输出格式:车次列表(包含车次号、出发时间、到达时间、余票信息等)
  3. 配置API访问凭证(Access Key、Secret Key),记录Agent访问地址(Endpoint)

3.3 测试AI Agent

通过华为云控制台的“在线调试”功能,输入测试参数(如“北京到上海,2025-10-25”),验证Agent是否能正确返回车票信息

四、鸿蒙应用开发架构

4.1 项目目录结构

ets/
├── entryability/              # 应用入口能力
├── pages/                     # 页面组件
│   ├── API/                   # 网络请求相关
│   │   └── http_ai.ets        # 调用华为云AI Agent的HTTP工具
│   ├── Common/                # 公共模型与工具
│   │   └── Model/             # 数据模型定义
│   │       ├── cityModel.ets  # 城市信息模型
│   │       ├── searchHistoryModel.ets  #查询记录模型
│   │       ├── ticketModel.ets  # 车票信息模型
│   │       └── tokenApi/      # 令牌相关模型
│   │           ├── ReceiveModel.ets  # 令牌响应模型
│   │           └── TokenModel.ets    # 令牌请求模型
│   └── Utils/                 # 工具类
│       ├── getData.ets        # 数据获取工具
│       ├── cityParse.ets      # 城市信息解析工具
│       ├── getToken.ets       # 令牌获取与管理
│       ├── MakeCall.ets       # AI Agent调用封装
│       ├── SSEParser.ets      # SSE响应解析工具
│       └── TicketDataStore.ets# 车票数据本地存储
├── index.ets                  # 应用首页
├── TrainHomePage.ets          # 车票查询主页
└── TrainDetails.ets           # 车次详情页

五、核心文件实现

5.1 entryability(应用入口)

import { AbilityConstant, ConfigurationConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';
import { util } from '@kit.ArkTS';
import { BusinessError } from '@kit.BasicServicesKit';
import { CityListResponse } from '../pages/Common/Model/cityModel';
import json from '@ohos.util.json';
import { notificationManager } from '@kit.NotificationKit';

const TAG: string = '[PublishOperation]';
const DOMAIN_NUMBER: number = 0xFF00;
const DOMAIN = 0x0000;

export default class EntryAbility extends UIAbility {
  private fruits: TextCascadePickerRangeContent[] = []

  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET);
    hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onCreate');

    // city.json获取
    try {
      this.context.resourceManager.getRawFileContent("city.json")
        .then((value: Uint8Array) => {
          console.log("city初始数据:" + value);
          let textDecoder = util.TextDecoder.create('utf-8', { ignoreBOM: true });
          console.log("city通过解码器转为UTF-8后的数据:" + json.stringify(textDecoder));
          let decodedData = textDecoder.decodeWithStream(value, { stream: false });
          console.log("city完成解码后的数据:" + decodedData);
          try {
            let parseData: CityListResponse = JSON.parse(decodedData);
            console.log("city通过JSON工具类解析后的数据:" + json.stringify(parseData));
            for (let i = 0; i < parseData.cityList.cities.length; i++) {
              this.fruits.push({
                text: parseData.cityList.cities[i].name
              })
            }
            console.log("city通过JSON工具类解析后放入数组的数据:" + JSON.stringify(this.fruits));
            AppStorage.setOrCreate<TextCascadePickerRangeContent[]>('cityList', this.fruits)
          } catch (error) {
            console.error("promise getRawFileContent failed, error is " + error);
          }
        })
        .catch((error: BusinessError) => {
          console.error("getRawFileContent promise error is " + error);
        });
    } catch (error) {
      let code = (error as BusinessError).code;
      let message = (error as BusinessError).message;
      console.error(`promise getRawFileContent failed, error code: ${code}, message: ${message}.`);
    }

    // 请求通知权限
    notificationManager.isNotificationEnabled().then((data: boolean) => {
      hilog.info(DOMAIN_NUMBER, TAG, "isNotificationEnabled success, data: " + JSON.stringify(data));
      if(!data){
        notificationManager.requestEnableNotification(this.context).then(() => {
          hilog.info(DOMAIN_NUMBER, TAG, `[ANS] requestEnableNotification success`);
        }).catch((err: BusinessError) => {
          if(1600004 == err.code){
            hilog.error(DOMAIN_NUMBER, TAG, `[ANS] requestEnableNotification refused, code is ${err.code}, message is ${err.message}`);
          } else {
            hilog.error(DOMAIN_NUMBER, TAG, `[ANS] requestEnableNotification failed, code is ${err.code}, message is ${err.message}`);
          }
        });
      }
    }).catch((err: BusinessError) => {
      hilog.error(DOMAIN_NUMBER, TAG, `isNotificationEnabled fail, code is ${err.code}, message is ${err.message}`);
    });

    // 发布通知
    let notificationRequest: notificationManager.NotificationRequest = {
      id: 1,
      content: {
        notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT, // 普通文本类型通知
        normal: {
          title: 'test_title',
          text: 'test_text',
          additionalText: 'test_additionalText',
        }
      }
    };
    notificationManager.publish(notificationRequest, (err: BusinessError) => {
      if (err) {
        hilog.error(DOMAIN_NUMBER, TAG, `Failed to publish notification. Code is ${err.code}, message is ${err.message}`);
        return;
      }
      hilog.info(DOMAIN_NUMBER, TAG, 'Succeeded in publishing notification.');
    });

  }

  onDestroy(): void {
    hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onDestroy');
  }


  onWindowStageCreate(windowStage: window.WindowStage): void {
    // Main window is created, set main page for this ability
    hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageCreate');

    windowStage.loadContent('pages/Index', (err) => {
      if (err.code) {
        hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
        return;
      }
      hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.');
    });
  }


  onWindowStageDestroy():
    void {
    // Main window is destroyed, release UI related resources
    hilog
      .info
      (
        DOMAIN,
        'testTag',
        '%{public}s',
        'Ability onWindowStageDestroy'
      )
    ;
  }

  onForeground():
    void {
    // Ability has brought to foreground
    hilog
      .info
      (
        DOMAIN,
        'testTag',
        '%{public}s',
        'Ability onForeground'
      )
    ;
  }

  onBackground():
    void {
    // Ability has back to background
    hilog
      .info
      (
        DOMAIN,
        'testTag',
        '%{public}s',
        'Ability onBackground'
      )
    ;
  }
}

5.2 页面组件

5.2.1 index.ets(应用首页)

import { router } from '@kit.ArkUI'
import { getData } from './Utils/getData'
import { common } from '@kit.AbilityKit'
import { AITicketResponse } from './Commons/Model/ticketModel'
import { SearchHistory } from './Commons/Model/searchHistoryModel'
import { relationalStore } from '@kit.ArkData'

@Entry
@Component
struct Index {
  @State cityList: TextCascadePickerRangeContent[] = []
  @State fromCityIndex: number = 0
  @State fromCityString: string = '郑州市'
  @State toCityIndex: number = 0
  @State toCityString: string = '上海市'
  @State selectedDate: Date = new Date()
  @State aiResponse: AITicketResponse | null = null
  @State searchHistory: SearchHistory[] = [];
  private context = getContext(this) as common.UIAbilityContext;
  private rdbStore: relationalStore.RdbStore | null = null;
  private readonly STORE_CONFIG: relationalStore.StoreConfig = {
    name: 'TrainTicketDB.db',
    securityLevel: relationalStore.SecurityLevel.S2
  };
  private readonly TABLE_NAME: string = 'search_history';
  private readonly SQL_CREATE_TABLE: string = `
    CREATE TABLE IF NOT EXISTS ${this.TABLE_NAME} (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      fromCity TEXT NOT NULL,
      toCity TEXT NOT NULL
    )
  `;

  async initRdbStore(): Promise<void> {
    try {
      this.rdbStore = await relationalStore.getRdbStore(this.context, this.STORE_CONFIG);
      await this.rdbStore.executeSql(this.SQL_CREATE_TABLE);
      console.info('数据库初始化成功');
    } catch (err) {
      console.error(`数据库初始化失败: ${JSON.stringify(err)}`);
    }
  }

  async saveSearchHistory(): Promise<void> {
    if (!this.rdbStore) {
      await this.initRdbStore();
    }

    console.info('数据库保存开始', this.fromCityString, this.toCityString);

    // 修复:使用正确的目的地
    const valueBucket: relationalStore.ValuesBucket = {
      'fromCity': this.fromCityString,
      'toCity': this.toCityString
    };

    try {
      await this.rdbStore!.insert(this.TABLE_NAME, valueBucket);
      console.info('搜索记录保存到数据库成功');
      await this.loadSearchHistory();
    } catch (err) {
      console.error(`保存搜索记录到数据库失败: ${err}`);
    }
  }

  async loadSearchHistory(): Promise<void> {
    if (!this.rdbStore) {
      await this.initRdbStore();
    }

    try {
      const predicates = new relationalStore.RdbPredicates(this.TABLE_NAME);
      predicates.orderByDesc('id');
      predicates.limitAs(10);

      const resultSet = await this.rdbStore!.query(predicates,
        ['id', 'fromCity', 'toCity']);

      this.searchHistory = [];
      while (resultSet.goToNextRow()) {
        const history: SearchHistory = {
          id: resultSet.getLong(resultSet.getColumnIndex('id')),
          fromCity: resultSet.getString(resultSet.getColumnIndex('fromCity')),
          toCity: resultSet.getString(resultSet.getColumnIndex('toCity'))
        };
        this.searchHistory.push(history);
      }
      resultSet.close();
    } catch (err) {
      console.error(`加载搜索历史失败: ${JSON.stringify(err)}`);
    }
  }

  aboutToAppear(): void {
    // 初始化数据库,完成后保存当前搜索记录
    this.initRdbStore().then(() => {
      this.saveSearchHistory();
    });
  }

  build() {
    Column({ space: 10 }) {
      // 出发地、目的地的选择和显示功能
      Row({ space: 10 }) {
        Column({ space: 10 }) {
          Button('出发地')
            .onClick(() => {
              this.getUIContext().showTextPickerDialog({
                range: AppStorage.get('cityList') as TextCascadePickerRangeContent[],
                selected: this.fromCityIndex,
                onChange: (value: TextPickerResult) => {
                  this.fromCityString = value.value as string
                },
                onAccept: (value: TextPickerResult) => {
                  this.fromCityIndex = value.index as number
                  this.fromCityString = value.value as string
                }
              });
            })
          Text(this.fromCityString)
        }

        Text('——').fontWeight(FontWeight.Bold)

        Column({ space: 10 }) {
          Button('目的地')
            .onClick(() => {
              this.getUIContext().showTextPickerDialog({
                range: AppStorage.get('cityList') as TextCascadePickerRangeContent[],
                selected: this.toCityIndex,
                onChange: (value: TextPickerResult) => {
                  this.toCityString = value.value as string
                },
                onAccept: (value: TextPickerResult) => {
                  this.toCityIndex = value.index as number
                  this.toCityString = value.value as string
                }
              });
            })
          Text(this.toCityString)
        }
      }
      .width('100%')
      .justifyContent(FlexAlign.Center)

      // 日期选择功能
      Row({ space: 10 }) {
        Text(this.selectedDate.toLocaleDateString() + '')
          .padding({
            left: 15,
            right: 15,
            top: 10,
            bottom: 10
          })
          .borderRadius(20)
          .fontColor('#ffffffff')
          .backgroundColor('#007dfe')
          .fontWeight(FontWeight.Bold)
          .onClick(() => {
            this.getUIContext().showDatePickerDialog({
              start: new Date(),
              selected: this.selectedDate,
              onDateChange: (value: Date) => {
                this.selectedDate = value
              }
            })
          })
      }

      // 搜索记录展示
      Row() {
        ForEach(this.searchHistory, (item: SearchHistory, index) => {
          if (index <= 2) {
            Text(item.fromCity + '--' + item.toCity)
              .fontSize(12)
              .fontColor('#8a8a8a')
              .layoutWeight(1)
          }
        })
      }
      .width('90%')

      Button('查询车次')
        .onClick(() => {
          router.pushUrl({
            url: 'pages/TrainDetails',
            params: {
              fromCity: this.fromCityString,
              toCity: this.toCityString,
              selectedDate: this.selectedDate.toLocaleDateString(),
              aiResponse: this.aiResponse
            }
          }, (err) => {
            if (err) {
              console.error(`Invoke pushUrl failed, code is ${err.code}, message is ${err.message}`);
              return;
            }
            console.info('Invoke pushUrl succeeded.');
          })
          this.saveSearchHistory()
        })

      Button('展示AI查询结果')
        .onClick(async () => {
          await getData(this.fromCityString, this.toCityString, this.selectedDate.toLocaleDateString(),
            getContext(this) as common.UIAbilityContext)
            .then((response) => {
              this.aiResponse = response;
            })
        })

      Scroll() {
        Text(this.aiResponse ? JSON.stringify(this.aiResponse) : '暂无数据')
      }
    }
    .width('100%')
    .height('100%')
  }
}

5.2.2 TrainHomePage.ets(车票查询主页)

import { webview } from '@kit.ArkWeb';

@Entry
@Component
struct TrainHomePage {
  webviewController: webview.WebviewController = new webview.WebviewController();

  build() {
    Column() {
      Web({
        src: "https://www.12306.cn",
        controller: this.webviewController,
      })
    }
    .width('100%')
    .height('100%')
  }
}

5.2.3 TrainDetails.ets(车次详情页)

import { router } from '@kit.ArkUI'
import { TicketItem, SeatType, AITicketResponse } from './Common/Model/ticketModel'
import { JSON } from '@kit.ArkTS'
import { MakeCall } from './Utils/MakeCall'

class TrainDetailsProps {
  fromCity?: string
  toCity?: string
  selectedDate?: string
  aiResponse: AITicketResponse | null = null
}

@Entry
@Component
struct TrainDetails {
  scroller: Scroller = new Scroller();
  @State fromCity: string = '郑州' // 出发城市
  @State toCity: string = '上海' // 目的城市
  @State aiResponse: AITicketResponse | null = null
  @State selectedDateStr: string = '今天 10.19' // 日期显示字符串
  @State ticketList: TicketItem[] = this.aiResponse?.ticketList || []
  @State phoneNumber: string = '400 - 888 - 8888'

  aboutToAppear(): void {
    const params = router.getParams() as TrainDetailsProps
    if (params) {
      this.fromCity = params.fromCity || '郑州'
      this.toCity = params.toCity || '上海'
      this.aiResponse = params.aiResponse
      // 日期参数处理(若需动态日期,可扩展此处)
      if (params.selectedDate) {
        this.selectedDateStr = `今天 ${params.selectedDate}`
      }
    }

    console.log('ai对话返回结果:', JSON.stringify(this.aiResponse))
    console.log('ai对话 selectedDate:', JSON.stringify(this.aiResponse))
    console.log('ai对话返回列表结果:', JSON.stringify(this.aiResponse?.ticketList))
    console.log('ai对话赋值给列表:', this.ticketList.toString())
    console.log('ai对话返回的信息中的的温馨提示:', this.aiResponse?.tips.toString())

  }

  // 查找第一个有余票的席位(只包含"张"的才算有票)
  getFirstAvailableSeat(seatTypes: SeatType[]): SeatType | null {
    for (let seat of seatTypes) {
      if (seat.ticketStatus.includes('张')) {
        return seat
      }
    }
    return null
  }

  //构建车次项
  @Builder
  TrainItemBuilder(item: TicketItem, index: number) {
    // 在@Builder中调用方法获取有余票的席位
    // let availableSeat: SeatType | null = this.getFirstAvailableSeat(item.seatTypes)

    Column() {
      // 车次核心信息行:出发时间/站 + 车次/历时 + 到达时间/站 + 票价
      Row({ space: 20 }) {
        // 出发信息:时间 + 站名
        Column({ space: 5 }) {
          Text(item.departureTime).fontSize(18).fontColor(Color.Black)
          Text(item.departureStation).fontSize(14).fontColor('#666')
        }

        // 车次与历时
        Column({ space: 5 }) {
          Text(item.trainNo).fontSize(16).fontColor(Color.Black)
          Text(item.duration).fontSize(14).fontColor('#666')
        }

        // 到达信息:时间 + 站名
        Column({ space: 5 }) {
          Text(item.arrivalTime).fontSize(18).fontColor(Color.Black)
          Text(item.arrivalStation).fontSize(14).fontColor('#666')
        }

        // 票价显示:显示第一个有余票的席位价格,如果没有则显示售罄
        Column({ space: 5 }) {
          // if (availableSeat) {
          Row() {
            Text('¥').fontSize(14).fontColor('#f98131')
            Text(item.seatTypes[0].price.split('元')[0]).fontSize(16).fontColor('#f98131')
            Text('起').fontSize(14).fontColor('#f98131')
          }
        }
      }
      .padding(10)

      Divider().width('90%')

      // 席别与余票状态行
      Row({ space: 15 }) {
        ForEach(item.seatTypes, (seat: SeatType) => {
          Row({ space: 5 }) {
            Text(seat.seatName).fontSize(12).fontColor('#666')
            // 不同余票状态,颜色区分
            if (seat.ticketStatus === '候补+') {
              Text(seat.ticketStatus).fontSize(12).fontColor('#007dff')
            } else if (seat.ticketStatus.includes('张')) {
              Text(seat.ticketStatus).fontSize(12).fontColor('#ff32e90b')
            } else if (seat.ticketStatus === '售罄') {
              Text(seat.ticketStatus).fontSize(12).fontColor('#999')
            } else {
              Text(seat.ticketStatus).fontSize(12).fontColor('#666')
            }
          }
        })
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceAround)
      .padding({
        left: 10,
        right: 10,
        top: 10,
        bottom: 10
      })

    }
    .width('95%')
    .margin({
      left: '2.5%',
      right: '2.5%',
      top: index == 0 ? 10 : 5,
      bottom: 5
    })
    .borderRadius(8)
    .backgroundColor(Color.White)
  }

  build() {
    Column() {
      // 顶部导航栏:城市切换 + 日期显示
      Row({ space: 10 }) {
        Text('<').fontSize(20).fontColor(Color.White).fontWeight(FontWeight.Bolder)
          .onClick(() => router.back())

        Row() {
          Text(this.fromCity).fontColor(Color.White).fontWeight(FontWeight.Bolder)
          Text(' <> ').fontColor(Color.White).fontWeight(FontWeight.Bolder)
          Text(this.toCity).fontColor(Color.White).fontWeight(FontWeight.Bolder)
        }

        Text(this.selectedDateStr).fontColor(Color.White).fontWeight(FontWeight.Bolder)
      }
      .width('100%')
      .padding({
        left: 15,
        right: 15,
        top: 10,
        bottom: 10
      })
      .backgroundColor('#3b99fb')
      .justifyContent(FlexAlign.SpaceBetween)

      // 车次标签页(直达/中转)
      Tabs() {
        // 直达标签内容
        TabContent() {
          Column() {
            List() {
              ForEach(this.aiResponse?.ticketList, (item: TicketItem, index) => {
                ListItem() {
                  Column() {
                    // 使用@Builder构建每个车次项
                    this.TrainItemBuilder(item, index)
                    // 温馨提示
                    if (index == (this.aiResponse?.ticketList.length as number) - 1) {
                      Column({ space: 5 }) {
                        Text('温馨提示').width('90%').fontSize(14).fontColor('#ffff7500').margin({ top: 10 })
                        ForEach(this.aiResponse?.tips, (tip: string) => {
                          Text(tip).width('90%').fontSize(12).fontColor('#999')
                        })
                      }

                      Column({ space: 10}) {
                        Text('12306官网')
                          .fontColor('#1867f5')
                          .onClick(() => {
                            router.push({
                              url: 'pages/TrainHomePage'
                            })
                          })
                        Text(`可咨询详情请拨打:${this.phoneNumber}`)
                          .fontColor('#1867f5')
                          .onClick(() => {
                            MakeCall.makeCall(this.phoneNumber)
                          })
                      }
                      .margin({ top: 10 })
                    }
                  }
                }
              })
            }
            .width('100%')
            .height('100%')
            .scrollBar(BarState.Off)
            .backgroundColor('#f5f5f5')


          }
        }
        .tabBar('直达')


        // 中转标签内容
        TabContent() {
          Text('中转功能待完善')
            .width('100%')
            .height('100%')
            .textAlign(TextAlign.Center)
            .backgroundColor('#f5f5f5')
        }.tabBar('中转')
      }
      .width('100%')
      .height('100%')
      .barHeight(50)
    }
    .width('100%')
    .height('100%')
  }
}

5.3 API模块(网络请求)

5.3.1 http_ai.ets(AI Agent HTTP请求工具)

import { rcp } from '@kit.RemoteCommunicationKit'
import { auth, Inputs } from '../Common/Model/tokenApi/TokenModel'

// const url =  'https://123.249.99.67/v1/9d72d169ec634537bd3cfe7fa066eed6/agents/0a32ae0d-8f66-42b1-9d83-bb827d445332/conversations/1?version=1760701576547' // 王
const url =  'https://123.249.99.67/v1/b188216baed24ebfae78ed6de041db54/agents/450c4d64-1caf-4189-b881-5d829380e3e3/conversations/1?version=1760932844103' // 杨
export async function get_ai(token:string,a:Inputs){
  try {
    const headers:rcp.RequestHeaders = {
      'X-Auth-Token':token,
      'Content-Type': 'application/json'
    }
    const  security:rcp.SecurityConfiguration = {
      remoteValidation:'skip'
    }
    const session =  rcp.createSession({
      headers:headers,
      requestConfiguration:{
        security:security
      }
    })

    const post_ai =await session.post(url,a)
    return post_ai;
  }catch (e) {
    console.log('ai回答_get_ai_error'+JSON.stringify(e))
    return null
  }
}


const url_token = 'https://iam.cn-north-4.myhuaweicloud.com/v3/auth/tokens'
export async function token_api():Promise<rcp.Response | undefined>{
  const headers: rcp.RequestHeaders = {
    'Content-Type': 'application/json'
  }
  const session = rcp.createSession({
    headers: headers
  })
  try {

    return await session.post(url_token, new auth() )

  } catch (e) {
    console.error(JSON.stringify(e) + "ai调用3")
  }
  return undefined
}

5.4 Common/Model模块(数据模型)

5.4.1 cityModel.ets(城市信息模型)

// city.json解析接口
export interface City {
  id: number;
  name: string;
  province: string;
  pinyin: string;
  shortPinyin: string;
}

export interface CityList {
  updateTime: string;
  cities: City[];
}

export interface CityListResponse {
  cityList: CityList;
}

5.4.2 searchHistoryModel.ets(查询记录模型)

export  interface SearchHistory {
  id: number;
  fromCity: string;
  toCity: string;
}

5.4.3 ticketModel.ets(车票信息模型)

// pages/Common/Model/ticketModel.ets
export interface QueryCondition {
  departureCity: string; // 出发城市(与AI JSON字段一致)
  arrivalCity: string;   // 到达城市
  travelDate: string;    // 出行日期(yyyy-MM-dd)
}

export interface SeatType {
  seatName: string;      // 席别名称(如"二等座")
  price: string;         // 票价(含单位,如"553元")
  ticketStatus: string;  // 余票状态(如"有票"/"无票"/"候补")
}

export interface TicketItem {
  trainNo: string;               // 车次编号(如"G101")
  departureStation: string;      // 出发车站(如"北京南站")
  arrivalStation: string;        // 到达车站(如"上海虹桥站")
  departureTime: string;         // 出发时间(yyyy-MM-dd HH:mm:ss)
  arrivalTime: string;           // 到达时间(yyyy-MM-dd HH:mm:ss)
  duration: string;              // 历时(如"6小时30分")
  seatTypes: SeatType[];         // 该车次的席别列表
}

export interface TicketMetadata {
  dataGenerateTime: string;  // 数据生成时间(yyyy-MM-dd HH:mm:ss)
  dataSource: string;        // 数据来源(如"12306-MCP对接数据")
  status: "success" | "fail";// 数据状态
}

export interface AITicketResponse {
  queryCondition: QueryCondition; // 查询条件
  ticketList: TicketItem[];       // 车次列表
  metadata: TicketMetadata;       // 元数据
  tips: string[];                 // 提示信息
}

export interface LocalTicketResponse {
  parseStatus: "success" | "fail" | "no_data"; // 解析状态
  message: string;                             // 状态描述(如错误信息)
  data?: AITicketResponse;                     // 结构化票务数据(成功时存在)
}

5.4.4 tokenApi/TokenModel.ets(令牌请求模型)

// pages/Common/Model/tokenApi/TokenModel.ets
//调取请求AIM获取token的请求体
class Domain {
  // name:string ="GT-gcw_XaRUNQkg_Syk" // 王
  name: string = "hid_4ocll2m1t6hwqj2" // 杨
}

class User {
  domain: Domain = new Domain()
  // name:string = "wwh" // 王
  name: string = "Luo Yu" // 杨
  // password:string = "wang2006" // 王
  password: string = "By107871895123." // 杨
}

class Password {
  user: User = new User()
}

class Identity {
  methods: string[] = ["password"]
  password: Password = new Password()
}

class Project {
  // id:string = "9d72d169ec634537bd3cfe7fa066eed6" // 王
  name: string = "cn-north-4" // 杨
}

class Scope {
  project: Project = new Project()
}


interface GeneratedTypeLiteralInterface_1 {
  identity: Identity;
  scope: Scope;
}

export class auth {
  auth: GeneratedTypeLiteralInterface_1 = {
    identity: new Identity(),
    scope: new Scope()
  }
}

//ai参数
interface GeneratedTypeLiteralInterface_2 {
  query: string
}

export class Inputs {
  inputs: GeneratedTypeLiteralInterface_2 = {
    query: '你好'
  }
}
一、角色与任务:作为“火车票查询系统”数据生成Agent,基于用户查询条件,返回标准化结构化数据,供鸿蒙端应用(ArkTS开发)API调用,含直达车次、票价、余票等信息。

二、查询条件必填项(需校验):
- 出发城市:国内地级市及以上名称(如“深圳”,不可为“广东省”等);
- 到达城市:同出发城市规范,且与出发城市不重复;
- 出行日期:“yyyy-MM-dd”格式,需为当前日期及未来15天内(符合12306预售期)。
未提供任一必填项,返回提示:“请补充完整查询条件(出发城市、到达城市、出行日期,日期格式为yyyy-MM-dd)”。

三、输出数据字段规范:
1. 查询条件回显
- departureCity:出发城市(字符串,非空,与输入一致);
- arrivalCity:到达城市(字符串,非空,与输入一致且≠departureCity);
- travelDate:出行日期(yyyy-MM-dd,非空,在当前+15天内)。

2. 车票列表(array)
- trainNo:车次编号(G/D/C/Z/T/K+数字,非空);
- departureStation:出发车站(含城市名,非空,属出发城市);
- arrivalStation:到达车站(含城市名,非空,属到达城市);
- departureTime:出发时间(HH:mm,非空,匹配出行日期);
- arrivalTime:到达时间(HH:mm,非空,晚于出发时间);
- duration:历时(如“6小时30分”,非空,按出发/到达时间准确计算);
- seatTypes(array):席别信息,至少含“二等座”“一等座”,最多加“商务座”“无座”;
  - seatName:席别名称(标准名称,非空);
  - price:票价(含“元”,非空,符合市场范围);
  - ticketStatus:余票状态(仅“有票”“无票”“候补”,非空)。

3. 数据元信息
- dataGenerateTime:数据生成时间(yyyy-MM-dd HH:mm:ss,非空,当前时间);
- dataSource:固定“12306-MCP对接数据”;
- status:返回状态(“success”或“fail”,非空)。

4. 温馨提示
- tips(array):火车票相关提示(退票、改签等),至少2条,最多5条。

四、返回格式约束(强制JSON):
- 仅返回JSON,无多余文本;
- 字段大小写严格匹配(如departureCity);
- 数组(ticketList、seatTypes、tips)格式统一,非空;
- 无结果时,ticketList为空数组,status为“success”,tips加“当前日期无从{departureCity}{arrivalCity}的直达车次,请调整日期或查询中转方案”。
示例格式(需按实际条件生成真实数据):
{
  "queryCondition": {
    "departureCity": "深圳",
    "arrivalCity": "西安",
    "travelDate": "2025-10-01"
  },
  "ticketList": [
    {
      "trainNo": "G82",
      "departureStation": "深圳北站",
      "arrivalStation": "西安北站",
      "departureTime": "08:00:00",
      "arrivalTime": "14:30:00",
      "duration": "6小时30分",
      "seatTypes": [
        {
          "seatName": "二等座",
          "price": "580元",
          "ticketStatus": "有票"
        },
        {
          "seatName": "一等座",
          "price": "920元",
          "ticketStatus": "有票"
        },
        {
          "seatName": "商务座",
          "price": "1780元",
          "ticketStatus": "候补"
        }
      ]
    }
  ],
  "metadata": {
    "dataGenerateTime": "2025-09-30 10:15:30",
    "dataSource": "12306-MCP对接数据",
    "status": "success"
  },
  "tips": [
    "退票手续费:发车前8天以上免,48小时-8天5%,24-48小时10%,24小时内20%",
    "乘车需携带购票时的有效身份证件原件"
  ]
}

五、数据约束补充:
- 车次编号符合G(高铁)、D(动车)等真实编码逻辑;
- 出发/到达车站为对应城市真实车站;
- 历时计算准确(如08:00-14:30为“6小时30分”)。
搜素流程1.根据所传参数调用mcp查询车票根据更偏查询结果生成符合json格式的数据不得编造不得遗漏返回数据不得有其他提示如(好的,等等)

5.4.5 tokenApi/ReceiveModel.ets(令牌响应模型)

// pages/Common/Model/tokenApi/ReceiveModel.ets
/**
 * 华为云IAM认证接口模型
 *
 */
export interface AuthResponse {

  token: Token;
}

/**
 * Token核心数据结构(JSON中token对象的内容)
 */
export interface Token {
  // token过期时间
  expires_at: string;
  methods: string[];

}

export interface data {
  exent: string
  content: string
  createdTime: string
}

5.5 Utils模块(工具类)

5.5.1 getToken.ets(令牌获取与管理)

// pages/Utils/getToken.ets
import { preferences } from "@kit.ArkData"
import { common } from "@kit.AbilityKit"
import { token_api } from "../API/http_ai"
import { BusinessError } from "@kit.BasicServicesKit"
import { util } from "@kit.ArkTS"
import { AuthResponse } from "../Common/Model/tokenApi/ReceiveModel"

interface returnData {
  token_get: string
  time_get: string
}

export class Token_management {
  private name: string = 'Token'
  private preferences: preferences.Preferences | null = null
  private context: common.UIAbilityContext

  // 外部调用方传入上下文
  constructor(context: common.UIAbilityContext) {
    this.context = context
  }

  //初始化preferences
  async init(): Promise<boolean> {
    try {
      this.preferences = await preferences.getPreferences(this.context, this.name)
      console.log('初始化成功')
      return true
    } catch (err) {
      return false
    }
  }

  //调用获取token的api
  async get_new_token(): Promise<returnData | null> {
    if (!this.preferences) {
      return null
    }

    let token_get: string = ''
    let time_get: string = ''
    try {
      await token_api().then(response => {
        console.log('获取token' + response)
        token_get = response?.headers['x-subject-token'] as string;
        time_get = buf2String(response?.body).token.expires_at
      })

      if (token_get && time_get) {
        console.log('get_new_token' + "获取成功")
        return { token_get, time_get }
      } else {
        console.log("返回值为空")
        return null
      }
    } catch (err) {
      console.error('token_get' + err)
      return null
    }
  }

  /**
   * 储存新的token
   */
  async set_token(): Promise<string | undefined> {
    if (!this.preferences) {
      console.log(this.preferences + '')
      return undefined
    }
    //判读本地是否有token
    if (this.preferences?.hasSync('token') && this.preferences.hasSync('time')) {
      const storedToken = this.preferences.getSync('token', '') as string;
      const storedExpireTime = this.preferences.getSync('time', '') as string;
      //判断是否过期
      if (!this.is_overdue(storedExpireTime)) {
        console.log('token可以直接使用')
        return storedToken
      } else {
        //已经过期重新将新的放在容器中 获取新的token
        const new_token_vessel = await this.get_new_token()
        const new_token = new_token_vessel?.token_get
        const new_time = new_token_vessel?.time_get
        //将获取到的token等持久化
        this.persistent_token(new_token, new_time)
        return new_token
      }
    } else {
      //本地并没有储存调用接口来进行储存
      const new_token_vessel = await this.get_new_token()
      const new_token = new_token_vessel?.token_get
      const new_time = new_token_vessel?.time_get
      //将获取到的token等持久化
      this.persistent_token(new_token, new_time)
      return new_token
    }
  }

  /**
   * 拿到token可以自动判断是否过期
   * 获取token
   */
  async get_token(): Promise<string | null> {
    try {
      if (this.preferences?.hasSync('token') && this.preferences.hasSync('time')) {
        const storedExpireTime = this.preferences.getSync('time', 'defValue') as string;
        if (!this.is_overdue(storedExpireTime)) {
          //没有过期可以直接使用
          const token_get = this.preferences.getSync('token', 'defValue')
          return token_get as string
        } else {
          //已经过期或不存在
          return null
        }
      } else {
        console.log('没有本地储存,进行储存')
        // this.persistent_token()
        return null
      }
    } catch (err) {
      console.log('获取token失败' + err)
      return null
    }
  }

  //将获取到的token等持久化
  async persistent_token(new_token: string | undefined, new_time: string | undefined): Promise<boolean> {
    if (!this.preferences) {
      return false
    }
    if (new_token && new_time) {
      this.preferences.putSync(new_token, 'new_token')
      this.preferences.putSync(new_time, 'new_time')

      await new Promise<void>((resolve, reject) => {
        this.preferences!.flush((err: BusinessError | undefined) => {
          if (err) {
            console.error(`Token持久化失败: 代码=${err.code}, 消息=${err.message}`);
            reject(err);
          } else {
            console.info('Token已成功持久化到本地');
            resolve();
          }
        });
      });

      return true
    } else {
      return false
    }
  }

  //判断token是否过期如果过期就重新获取token
  async is_overdue(End_time: string): Promise<boolean> {
    try {
      const end_time = new Date(End_time)
      const Current_time = new Date()
      return end_time.getTime() < Current_time.getTime()
    } catch (err) {
      console.log(err + '时间比对失败')
      return true
    }
  }

  //获取当前时间
  get_Current_time(): string {
    return new Date().toISOString()
  }

  async loadToken(): Promise<string | undefined> {
    if (!this.get_token() == null) {
      //可以正常获取到token
      const token_get = await this.preferences?.getSync('token', 'defValue') as string
      return token_get
    } else {
      //token过期或不存在时调用储存方法来重新获取
      return await this.set_token()
    }
  }
}
//接收返回数据进行解析
function buf2String(buf: ArrayBuffer | undefined) {
  let msgArray = new Uint8Array(buf as ArrayBuffer);
  let textDecoder = util.TextDecoder.create("utf-8");
  let rString = textDecoder.decodeWithStream(msgArray);
  return JSON.parse(rString) as AuthResponse;
}

5.5.2 cityParse.ets(城市信息解析工具)

// pages/Utils/cityParse.ets
import { AbilityConstant, ConfigurationConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { util } from '@kit.ArkTS';
import json from '@ohos.util.json';
import { CityListResponse } from '../Common/Model/cityModel';

const DOMAIN = 0x0000;

export default class EntryAbility extends UIAbility {
  private fruits: TextCascadePickerRangeContent[] = []

  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    try {
      this.context.resourceManager.getRawFileContent("city.json")
        .then((value: Uint8Array) => {
          //////
          let textDecoder = util.TextDecoder.create('utf-8', { ignoreBOM: true });
          let decodedData = textDecoder.decodeWithStream(value, { stream: false });
          try {
            let parsedData: CityListResponse = JSON.parse(decodedData);
            console.log("city通过JSON工具类解析后的数据:" + json.stringify(parsedData));
            for (let i = 0; i < parsedData.cityList.cities.length; i++) {
              this.fruits.push({
                text: parsedData.cityList.cities[i].name
              })
            }
            console.log("city通过JSON工具类解析后放入数组的数据:" + this.fruits);
            AppStorage.SetOrCreate<TextCascadePickerRangeContent[]>('globalCityListData', this.fruits);
            //////////
          } catch (e) {
            console.error('json文件' + json.stringify(e))
          }
        })
        .catch((error: BusinessError) => {
          console.error("json文件getRawFileContent promise error is " + error);
        });
    } catch (error) {
      let code = (error as BusinessError).code;
      let message = (error as BusinessError).message;
      console.error(`json文件promise getRawFileContent failed, error code: ${code}, message: ${message}.`);
    }
    this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET);
    hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onCreate');
  }
}

5.5.3 MakeCall.ets(AI Agent调用封装)

// pages/Utils/MakeCall.ets
// import需要的模块
import { call } from '@kit.TelephonyKit';
import { BusinessError } from '@kit.BasicServicesKit';

export class MakeCall {
   public static  makeCall(phoneNumber: string): void {
     // 调用查询能力接口
     let isSupport = call.hasVoiceCapability();
     if (isSupport) {
       // 如果设备支持呼叫能力,则继续跳转到拨号界面,并显示拨号的号码
       // 从API15开始支持tel格式电话号码,如:"tel:13xxxx"
       call.makeCall(phoneNumber, (err: BusinessError) => {
         if (!err) {
           console.info("make call success.");
         } else {
           console.error("make call fail, err is:" + JSON.stringify(err));
         }
       });
     }
  }
}



5.5.4 SSEParser.ets(SSE响应解析工具)

// pages/Utils/SSEParser.ets
import { AITicketResponse, LocalTicketResponse } from '../Common/Model/ticketModel';
import { TicketDataStore } from './TicketDataStore';

// SSE原始事件结构
interface RawSSEEvent {
  event: string;       // 事件类型(如"summary_response"/"message")
  content?: string;    // 核心内容
  role?: string;       // 可选字段
  createdTime?: number;// 可选字段
}

// SSE解析器
export class SSEParser {
  // 主方法:解析SSE数据 → 结构化票务数据 → 存入全局存储
  public static parseAndStore(sseData: string): LocalTicketResponse {
    try {
      // 1. 筛选有效SSE行(仅保留"data:"开头且非空的行)
      const validLines = sseData.split('\n')
        .map(line => line.trim())
        .filter(line => line.startsWith('data:') && line.slice(5).trim());
      if (validLines.length === 0) {
        const res: LocalTicketResponse = {
          parseStatus: "no_data",
          message: "SSE响应无有效内容"
        };
        TicketDataStore.getInstance().setData(res);
        return res;
      }

      // 2. 优先解析"summary_response"事件
      for (const line of validLines) {
        const sseEvent = SSEParser.parseSingleLine(line);
        if (sseEvent?.event === 'summary_response' && sseEvent.content) {
          console.log('原始content:', sseEvent.content); // 打印原始内容
          const cleanedContent = SSEParser.cleanContent(sseEvent.content);
          if (!cleanedContent) {
            throw new Error("清理后无有效JSON内容");
          }
          console.log('清理后content:', cleanedContent);
          // 解析纯净的JSON
          const aiData = JSON.parse(cleanedContent) as AITicketResponse;
          const res: LocalTicketResponse = {
            parseStatus: "success",
            message: "解析成功(来源:summary_response,已清理格式)",
            data: aiData
          };
          TicketDataStore.getInstance().setData(res);
          return res;
        }
      }

      // 3. 解析"message"事件(多段拼接)
      const fullContent = validLines
        .map(line => SSEParser.parseSingleLine(line))
        .filter(event => event?.event === 'message')
        .map(event => event?.content || '')
        .join('');

      if (fullContent) {
        const aiData = JSON.parse(fullContent) as AITicketResponse;
        const res: LocalTicketResponse = {
          parseStatus: "success",
          message: "解析成功(来源:message拼接)",
          data: aiData
        };
        TicketDataStore.getInstance().setData(res);
        return res;
      }

      // 4. 无匹配事件
      const noMatchRes: LocalTicketResponse = {
        parseStatus: "no_data",
        message: "未找到summary_response或message事件"
      };
      TicketDataStore.getInstance().setData(noMatchRes);
      return noMatchRes;

    } catch (error) {
      // 解析异常(如JSON格式错误)
      const errMsg = error instanceof Error ? error.message : "未知错误";
      const errRes: LocalTicketResponse = {
        parseStatus: "fail",
        message: `SSE解析失败:${errMsg}`
      };
      TicketDataStore.getInstance().setData(errRes);
      return errRes;
    }
  }

  /**
   * 清理content格式:移除```json开头、```结尾及多余换行/空格
   * @param rawContent - 原始content字符串
   * @returns 纯净的JSON字符串(失败返回空)
   */
  private static cleanContent(rawContent: string): string {
    if (!rawContent) return "";
    // 1. 移除开头的换行符(\n)和空格
    let cleaned = rawContent.trimStart();
    // 2. 移除开头的```json标记(不区分大小写,兼容可能的格式差异)
    const startMarker = /^```json/i; // 正则:匹配开头的```json(忽略大小写)
    cleaned = cleaned.replace(startMarker, "").trimStart();
    // 3. 移除结尾的```标记(可能带换行或空格)
    const endMarker = /```\s*$/; // 正则:匹配结尾的```及后续空格/换行
    cleaned = cleaned.replace(endMarker, "").trimEnd();
    return cleaned;
  }

  // 辅助方法:解析单行SSE(移除"data:"前缀,转JSON)
  private static parseSingleLine(line: string): RawSSEEvent | null {
    try {
      const jsonStr = line.slice(5).trim();
      return JSON.parse(jsonStr);
    } catch (e) {
      console.warn(`单行SSE解析失败(跳过): ${line}`);
      return null;
    }
  }
}

5.5.5 TicketDataStore.ets(车票数据本地存储)

// pages/Utils/TicketDataStore.ets
import { LocalTicketResponse } from '../Common/Model/ticketModel';

// 票务数据单例
export class TicketDataStore {
  private static instance: TicketDataStore;
  private ticketData: LocalTicketResponse; // 存储解析后的票务数据

  // 私有构造,初始化默认状态
  private constructor() {
    this.ticketData = {
      parseStatus: "no_data",
      message: "尚未查询票务数据"
    };
  }

  // 获取单例实例
  public static getInstance(): TicketDataStore {
    if (!TicketDataStore.instance) {
      TicketDataStore.instance = new TicketDataStore();
    }
    return TicketDataStore.instance;
  }

  // 存储票务数据
  public setData(data: LocalTicketResponse): void {
    this.ticketData = data;
  }

  // 获取票务数据
  public getData(): LocalTicketResponse {
    return this.ticketData;
  }

  // 清空数据
  public clearData(): void {
    this.ticketData = {
      parseStatus: "no_data",
      message: "数据已清空,等待新查询"
    };
  }
}

5.5.6 getData.ets(通用数据获取工具)

// pages/Utils/getData.ets
import { AITicketResponse, LocalTicketResponse } from '../Common/Model/ticketModel';
import { Inputs } from '../Common/Model/tokenApi/TokenModel';
import { get_ai } from '../API/http_ai';
import { SSEParser } from './SSEParser';
import { Token_management } from './getToken';
import { TicketDataStore } from './TicketDataStore';
import { common } from '@kit.AbilityKit';


let aiData: AITicketResponse | null = null
let token: string | undefined = ''
let tokenManager: Token_management | null = null;
let ticketData: LocalTicketResponse = TicketDataStore.getInstance().getData();

/**
 * 获取数据
 * @param fromStation 传入始站
 * @param toStation 传入终点
 * @param date 传入日期
 * @param context 传入上下文 示例:getContext(this) as common.UIAbilityContext
 * @returns : AITicketResponse | null 返回数据
 */
export async function getData(fromStation: string, toStation: string, date: string,
  context: common.UIAbilityContext): Promise<AITicketResponse | null> {
  try {
    tokenManager = new Token_management(context)
    await tokenManager.init()
    token = await tokenManager.loadToken()

    const aa = new Inputs();
    aa.inputs.query = `{
  "fromStation": ${fromStation},
  "toStation": ${toStation},
  "date": ${date}
}`
    await get_ai(token as string, aa).then(async (car) => {
      console.log("ai对话 token" , car)
      ticketData = SSEParser.parseAndStore(car?.toString() as string)
      console.log("ai对话 解析成功1" , ticketData.message)
      aiData = ticketData.data as AITicketResponse
      return aiData
    })
  } catch (error) {
    ticketData = {
      parseStatus: "fail",
      message: `查询失败:${(error as Error).message}`
    };
  }
  ;
  return aiData
}

六、测试与部署

6.1 本地测试

  1. 在DevEco Studio中配置鸿蒙模拟器或连接真实设备
  2. 运行应用,测试核心流程:
    • 首页跳转至查询页
    • 输入城市和日期,点击查询
    • 验证是否能正确显示车票列表
    • 点击车次进入详情页,验证数据展示

6.2 华为云AI Agent联调

  1. 确保AI Agent已正确配置并启动
  2. 检查Access Key、Secret Key和Endpoint是否正确
  3. 测试令牌获取功能,验证是否能正常拿到accessToken
  4. 模拟异常场景(如网络中断、参数错误),验证错误处理逻辑

6.3 应用部署

  1. 生成鸿蒙应用签名(参考华为开发者文档)
  2. 在DevEco Studio中打包HAP文件
  3. 通过华为应用市场或本地安装部署到设备

七、注意事项

  1. 华为云Access Key和Secret Key需妥善保管,避免硬编码在前端代码中(建议通过后端服务转发)
  2. 令牌有效期通常为24小时,需确保getToken.ets中的缓存逻辑正确
  3. AI Agent的响应格式需严格约定,避免解析失败
  4. 网络请求需处理超时和异常,提升用户体验
  5. 本地存储数据需定期清理,避免占用过多设备空间

八、总结

本项目通过华为云AI Agent实现了智能车票查询的后端能力,结合鸿蒙应用的前端交互,为用户提供了便捷的票务查询服务。核心实现包括AI Agent配置、令牌管理、网络通信、数据解析与存储等模块,各模块职责清晰、分工明确,可扩展性强。后续可优化方向:增加车票预订功能、集成用户登录、优化AI响应速度等。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。