HarmonyOS鸿蒙Next华为云AI Agent集成车票查询项目

HarmonyOS鸿蒙Next华为云AI Agent集成车票查询项目

华为云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  # 城市信息模型
│   │       ├── 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](/user/kit).AbilityKit';
import { hilog } from '[@kit](/user/kit).PerformanceAnalysisKit';
import { window } from '[@kit](/user/kit).ArkUI';
import { util } from '[@kit](/user/kit).ArkTS';
import { BusinessError } from '[@kit](/user/kit).BasicServicesKit';
import { CityListResponse } from '../pages/Common/Model/cityModel';
import json from '@ohos.util.json';
import { notificationManager } from '[@kit](/user/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](/user/kit).ArkUI'
import { getData } from './Utils/getData'
import { common } from '[@kit](/user/kit).AbilityKit'
import { AITicketResponse } from './Common/Model/ticketModel'

[@Entry](/user/Entry)
[@Component](/user/Component)
struct Index {
  [@State](/user/State) cityList: TextCascadePickerRangeContent[] = []
  [@State](/user/State) fromCityIndex: number = 0 // 出发地选择索引
  [@State](/user/State) fromCityString: string = '郑州市' // 出发地
  [@State](/user/State) toCityIndex: number = 0 // 目的地选择索引
  [@State](/user/State) toCityString: string = '上海市' // 目的地
  [@State](/user/State) selectedDate: Date = new Date() // 出发时间
  [@State](/user/State) aiResponse: AITicketResponse | null = null
  [@State](/user/State) isLoading: boolean = false


  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) => {
                  // 使用value属性获取选中的文本
                  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) => {
                  // 使用value属性获取选中的文本
                  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
              }
            })
          })
      }

      // 查询详情标题功能
      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.');
          })


        })



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

      Text(this.aiResponse ? JSON.stringify(this.aiResponse) : '暂无数据')

    }
    .width('100%')
    .height('100%')
  }
}

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

import { webview } from '[@kit](/user/kit).ArkWeb';

[@Entry](/user/Entry)
[@Component](/user/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](/user/kit).ArkUI'
import { TicketItem, SeatType, AITicketResponse } from './Common/Model/ticketModel'
import { JSON } from '[@kit](/user/kit).ArkTS'
import { MakeCall } from './Utils/MakeCall'

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

[@Entry](/user/Entry)
[@Component](/user/Component)
struct TrainDetails {
  scroller: Scroller = new Scroller();
  [@State](/user/State) fromCity: string = '郑州' // 出发城市
  [@State](/user/State) toCity: string = '上海' // 目的城市
  [@State](/user/State) aiResponse: AITicketResponse | null = null
  [@State](/user/State) selectedDateStr: string = '今天 10.19' // 日期显示字符串
  [@State](/user/State) ticketList: TicketItem[] = this.aiResponse?.ticketList || []
  [@State](/user/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](/user/Builder)
  TrainItemBuilder(item: TicketItem, index: number) {
    // 在[@Builder](/user/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).

更多关于HarmonyOS鸿蒙Next华为云AI Agent集成车票查询项目的实战教程也可以访问 https://www.itying.com/category-93-b0.html

2 回复

HarmonyOS Next通过华为云AI Agent集成车票查询功能,主要基于ArkTS语言开发,利用分布式能力实现多设备协同。系统通过AI Agent调用云端API处理查询请求,结合原子化服务实现卡片式信息展示。数据流转采用统一数据管理框架,保障跨端信息同步。集成过程涉及声明式UI开发、端云协同组件调用及分布式数据对象传递,无需依赖Java或C语言基础模块。

更多关于HarmonyOS鸿蒙Next华为云AI Agent集成车票查询项目的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


这是一个非常完整和专业的HarmonyOS Next车票查询项目实现文档,展示了华为云AI Agent与鸿蒙应用集成的优秀实践。

项目亮点:

  1. 架构设计清晰:前后端分离,前端使用ArkUI框架,后端依托华为云AI Agent,模块划分合理。

  2. 技术栈选择恰当:采用ETS语言开发,使用Preferences本地存储,HTTP/HTTPS网络通信,符合HarmonyOS开发规范。

  3. AI Agent集成完善:实现了完整的令牌管理机制(Token_management类),包含token获取、缓存、过期判断等逻辑。

  4. 数据处理规范:定义了完整的车票数据模型(AITicketResponse、TicketItem等),SSE响应解析工具处理AI返回数据。

  5. 用户体验考虑周到:包含城市选择、日期选择、车次展示、详情页面等完整用户流程。

代码质量方面:

  • entryability中正确初始化城市数据并存储到AppStorage
  • 页面组件使用了ArkUI的声明式语法,状态管理清晰
  • 工具类封装良好,职责单一(SSEParser、TicketDataStore等)
  • 错误处理机制完善,网络异常、解析失败等场景都有相应处理

建议优化点:

  1. 敏感信息(如华为云账号密码)应通过环境变量或配置文件管理,避免硬编码
  2. 可考虑增加网络状态检测和重试机制
  3. 界面可进一步优化加载状态和空数据展示

这个项目很好地展示了如何在HarmonyOS Next中集成云服务AI能力,为其他开发者提供了很好的参考范例。

回到顶部