• [视频类] atlas200 dvpp多路视频编码
    关于atlas200加速模块1.dvpp是否支持两路视频的编码2.如果不支持两路视频同时编码,两路分时是否可以实现,需要资源重新初始化吗?3.视频encoder硬件编码和jpeg硬件编码是否可以同时并行进行?
  • [知识分享] 开发者技术支持- 鸿蒙泛型理解与项目实践
    前言:在开发过程中很多小伙伴们经常会使用单泛型,开发中也挺常见的,本文将从泛型的基础概念出发,结合鸿蒙 ArkTS API12 + 的特性,通过网络请求封装和 UI 组件设计等实际场景,详细讲解泛型在鸿蒙项目中的应用方法。特别针对 ArkTS 的严格类型检查机制,提供符合规范的泛型实现方案,帮助开发者在避免any、unknown等不安全类型的同时,充分发挥泛型的优势,构建高质量的鸿蒙应用。一、泛型的核心概念泛型(Generics)是一种类型抽象机制,允许在定义组件、函数或接口时不指定具体类型,而在使用时再确定类型。在 ArkTS 中,泛型是实现类型安全和代码复用的关键技术,尤其适合开发可复用组件和工具类。 核心价值: 类型安全:在编译期检查类型,避免运行时类型错误代码复用:一套逻辑适配多种数据类型可读性:明确代码的使用意图,提高可维护性二、泛型基础语法1. 泛型函数  // 定义泛型函数:交换数组中两个元素的位置function swap<T>(array: T[], index1: number, index2: number): T[] { if (index1 < 0 || index2 < 0 || index1 >= array.length || index2 >= array.length) { return [...array]; } const newArray = [...array]; const temp = newArray[index1]; newArray[index1] = newArray[index2]; newArray[index2] = temp; return newArray;}// 使用泛型函数const numberArray = [1, 2, 3, 4];const swappedNumbers = swap(numberArray, 0, 2); // [3, 2, 1, 4]const stringArray = ['a', 'b', 'c'];const swappedStrings = swap(stringArray, 1, 2); // ['a', 'c', 'b']2. 泛型接口 // 定义泛型接口:数据响应模型export interface ApiResponse<T> { code: number; message: string; data: T;}// 具体类型实现interface User { id: string; name: string; age: number;}interface Article { id: string; title: string; content: string;}// 使用泛型接口const userResponse: ApiResponse<User> = { code: 200, message: 'success', data: { id: '1', name: '张三', age: 25 },};const articleResponse: ApiResponse<Article> = { code: 200, message: 'success', data: { id: '101', title: '泛型教程', content: '...' },};3. 泛型类  // 定义泛型类:栈数据结构class Stack<T> { private elements: T[] = []; // 入栈 push(element: T): void { this.elements.push(element); } // 出栈 pop(): T | undefined { return this.elements.pop(); } // 获取栈顶元素 peek(): T | undefined { return this.elements[this.elements.length - 1]; } // 检查是否为空 isEmpty(): boolean { return this.elements.length === 0; } // 获取栈大小 size(): number { return this.elements.length; }}// 使用泛型类const numberStack = new Stack<number>();numberStack.push(1);numberStack.push(2);console.log(numberStack.pop()?.toString()); // 2const stringStack = new Stack<string>();stringStack.push('Hello');stringStack.push('ArkTS');console.log(stringStack.peek()); // ArkTS三、项目中泛型的实际应用网络请求封装  import http from '@ohos.net.http';import { BusinessError } from '@ohos.base';// 定义请求头接口export interface HttpHeaders { contentType?: string; authorization?: string; accept?: string; userAgent?: string; // 可以根据需要添加其他常用头字段}// 定义API响应泛型接口export interface ApiResponse<T> { code: number; message: string; data: T;}// 定义请求参数泛型接口export interface RequestOptions<T = void> { url: string; method: http.RequestMethod; headers?: HttpHeaders; customHeaders?: Record<string, string>; // 用于存储自定义头 data?: T; timeout?: number;}// 默认请求头常量const DEFAULT_HEADERS: HttpHeaders = { contentType: 'application/json'};// 将头信息转换为http请求所需格式function convertHeaders(headers?: HttpHeaders, customHeaders?: Record<string, string>): Record<string, string> { const result: Record<string, string> = {}; // 处理标准头 if (headers) { if (headers.contentType) result['Content-Type'] = headers.contentType; if (headers.authorization) result['Authorization'] = headers.authorization; if (headers.accept) result['Accept'] = headers.accept; if (headers.userAgent) result['User-Agent'] = headers.userAgent; } // 处理自定义头 if (customHeaders) { Object.keys(customHeaders).forEach(key => { result[key] = customHeaders![key]; }); } return result;}// 泛型API客户端export class ApiClient { private baseUrl: string; constructor(baseUrl: string) { this.baseUrl = baseUrl; } // 泛型请求方法 async request<T, D = void>(options: RequestOptions<D>): Promise<ApiResponse<T>> { const httpRequest = http.createHttp(); try { const fullUrl = this.baseUrl + options.url; // 转换并合并请求头 const requestHeaders = convertHeaders(options.headers || DEFAULT_HEADERS, options.customHeaders); const requestOptions: http.HttpRequestOptions = { method: options.method, header: requestHeaders, extraData: options.data ? JSON.stringify(options.data) : '', connectTimeout: options.timeout || 5000, readTimeout: options.timeout || 10000 }; const response = await httpRequest.request(fullUrl, requestOptions); if (response.responseCode === 200) { return JSON.parse(response.result as string) as ApiResponse<T>; } else { throw new Error(`Request failed with code: ${response.responseCode}`); } } catch (error) { const businessError = error as BusinessError; throw new Error(`Network error: ${businessError.message || 'Unknown error'}`); } finally { httpRequest.destroy(); } } // GET请求快捷方法 get<T>(url: string, headers?: HttpHeaders, customHeaders?: Record<string, string>): Promise<ApiResponse<T>> { return this.request<T>({ url, method: http.RequestMethod.GET, headers, customHeaders }); } // POST请求快捷方法 post<T, D>(url: string, data: D, headers?: HttpHeaders, customHeaders?: Record<string, string>): Promise<ApiResponse<T>> { return this.request<T, D>({ url, method: http.RequestMethod.POST, data, headers, customHeaders }); }}四、泛型约束与高级用法1. 泛型约束  // 定义带约束的泛型接口interface HasId { id: string;}// 泛型约束:T必须实现HasId接口function findById<T extends HasId>(items: T[], id: string): T | undefined { return items.find(item => item.id === id);}// 符合约束的类型interface Book extends HasId { title: string; author: string;}// 使用带约束的泛型函数const books: Book[] = [ { id: 'b1', title: 'ArkTS入门', author: '张三' }, { id: 'b2', title: '鸿蒙开发实战', author: '李四' }];const book = findById(books, 'b1'); // 正确,返回Book类型2. 泛型默认值  // 定义一个基础空类型作为默认值interface EmptyData { // 空接口,作为默认数据类型}// 带默认值的泛型接口,使用具体类型替代unknowninterface DataResult<T = EmptyData> { success: boolean; data: T; error?: string;}// 使用默认类型(EmptyData)const defaultResult: DataResult = { success: true, data: {} // 符合EmptyData类型};// 指定字符串类型const stringResult: DataResult<string> = { success: true, data: 'some data'};// 指定数字类型const numberResult: DataResult<number> = { success: true, data: 42};// 定义用户类型interface User { id: string; name: string;}// 指定复杂类型const userResult: DataResult<User> = { success: true, data: { id: '123', name: '张三' }};// 错误结果示例const errorResult: DataResult = { success: false, data: {}, error: '操作失败'};五、泛型在项目中的最佳实践保持类型清晰:为泛型参数使用有意义的名称(T, U, V 适合简单场景,复杂场景使用如 TItem, TResponse 等)适度使用约束:合理的约束可以提高类型安全性,避免过度约束降低灵活性优先考虑泛型而非 any:任何使用 any 的场景都应首先考虑是否可以用泛型替代封装通用逻辑:将网络请求、数据存储、列表展示等通用逻辑用泛型封装配合接口使用:泛型与接口结合可以创建灵活且类型安全的组件系统 通过泛型,我们可以在鸿蒙 ArkTS 应用中创建既灵活又类型安全的代码,减少重复代码的同时保持编译期类型检查,这对于构建可维护、高质量的应用至关重要。
  • [知识分享] 开发者技术支持-鸿蒙图片拖拽调整功能技术经验总结
    关键技术难点总结​在鸿蒙应用中实现图片拖拽调整功能时,存在交互流畅性、设备适配性、代码可维护性等方面的技术难点。这些问题导致功能体验不佳、适配范围有限、后续迭代困难,通过架构优化与逻辑重构可提升功能稳定性与扩展性。​问题说明​在基于 ArkUI 框架的鸿蒙应用中,需实现图片尺寸的动态拖拽调整功能,用户可通过拖拽控制器改变图片宽高,同时需保障调整过程中的边界限制、比例锁定(可选)及流畅交互体验。在此过程中,存在尺寸调整精准度不足、低端设备卡顿、功能扩展困难、类型校验报错等问题。原因分析​● 状态管理与 UI 渲染耦合:未采用分层架构,将尺寸计算、手势处理与 UI 渲染逻辑混合编写,导致状态变更与视图更新相互干扰。后果:修改一处逻辑需牵动多处代码,维护成本高;状态流转不清晰,易出现数据不一致导致的尺寸显示异常。​● 未适配设备性能差异,固定逻辑引发低端机卡顿:不同设备(如高端机型与入门级手机)的 CPU、内存性能差异显著,但手势处理与尺寸计算采用统一频率与复杂度的逻辑。后果:低端机在快速拖拽时因运算能力不足,出现手势响应延迟、尺寸更新不及时,甚至应用无响应。​● 手势事件处理逻辑复杂,边界校验缺失:未将手势偏移计算与边界限制逻辑分离,在拖拽过程中未实时校验尺寸是否超出屏幕范围。后果:图片尺寸可能调整至小于最小限制或超出屏幕边界,导致显示异常;手势事件与业务逻辑混杂,易出现拖拽方向判断错误。​● 类型定义不明确,引发语法错误:未显式定义尺寸数据的接口类型,直接使用对象字面量进行传递与计算。后果:ArkTS 类型检查严格时,出现类型不匹配报错;数据结构不清晰,团队协作时易因类型理解偏差导致逻辑错误。解决思路​● 分层架构解耦:针对状态管理与 UI 渲染耦合问题,采用 “数据层 - 交互层 - 视图层” 分层架构,实现关注点分离,使各模块独立可控。​● 动态适配设备性能:针对设备性能差异,通过系统 API 获取设备性能参数,动态调整手势处理频率与计算复杂度,降低低端机负载。​● 逻辑拆分与边界校验:针对手势处理复杂与边界问题,将手势偏移计算、尺寸边界校验拆分为独立纯函数,确保拖拽过程中尺寸始终在合理范围。​● 明确类型定义:针对类型报错问题,通过接口显式定义数据结构,规范数据传递与计算的类型约束,避免语法错误。解决方案​分层架构设计(解耦核心逻辑)​数据层:封装尺寸与边界管理逻辑,采用单例模式的SizeManager类,负责屏幕尺寸获取、最小尺寸与边距常量定义、边界校验纯函数实现,为上层提供统一的尺寸数据服务。// 定义尺寸接口,解决对象字面量类型声明问题interface DisplaySize { width: number; height: number;}// 数据层:尺寸与边界管理class SizeManager { // 边界常量定义(可配置化设计) private readonly MIN_SIZE = 100; // 最小尺寸 private readonly MARGIN = 20; // 边距常量 // 获取屏幕可用尺寸(显式声明返回类型为接口) getDisplaySize(): DisplaySize { const displayData = display.getDefaultDisplaySync(); return { width: px2vp(displayData.width) - 2 * this.MARGIN, height: px2vp(displayData.height) - 2 * this.MARGIN }; } // 边界校验逻辑(纯函数设计) checkBoundary(value: number, maxValue: number): number { if (value < this.MIN_SIZE) { return this.MIN_SIZE; } if (value > maxValue) { return maxValue; } return value; }}交互层:处理手势事件与状态更新,通过@State装饰器管理尺寸状态,将手势偏移计算与尺寸更新逻辑封装为独立方法,通过数据层获取边界规则,确保状态变更仅依赖输入参数。// 状态管理(单向数据流)@State sizeState: DisplaySize = { width: 0, height: 0 };private sizeManager = new SizeManager();private displaySize: DisplaySize = this.sizeManager.getDisplaySize();private cacheSize: DisplaySize = this.sizeState; // 缓存当前状态用于手势计算// 手势处理纯函数(输入状态,输出新状态)private handleGestureUpdate(offsetX: number, offsetY: number): void { this.sizeState = { width: this.sizeManager.checkBoundary( this.cacheSize.width + offsetX, this.displaySize.width ), height: this.sizeManager.checkBoundary( this.cacheSize.height + offsetY, this.displaySize.height ) }}// 初始化状态(生命周期钩子与数据层交互)aboutToAppear(): void { this.sizeState = { width: this.displaySize.width, height: 200 // 初始高度 }; // 使用显式属性复制替代对象扩展运算符 this.cacheSize = { width: this.sizeState.width, height: this.sizeState.height };}视图层:基于交互层的状态数据纯 UI 渲染,不包含任何业务逻辑,通过RelativeContainer实现图片与拖拽控制器的相对布局,将尺寸状态绑定至图片宽高属性。// 视图层:纯UI渲染build() { Column() { RelativeContainer() { // 主图片(状态绑定) Image($r('app.media.icon_opacity')) .border({ width: 2, color: '#ff1d4fcd' }) .width(this.sizeState.width) .height(this.sizeState.height) // 拖拽控制器(交互入口) Image($r('app.media.fangda')) .width(20) .height(20) .backgroundColor(Color.Yellow) .borderRadius(10) .alignRules({ bottom: { anchor: "__container__", align: VerticalAlign.Bottom }, right: { anchor: "__container__", align: HorizontalAlign.End } }) .translate({ x: '50%', y: '50%' }) .gesture( PanGesture() .onActionUpdate((event) => { this.handleGestureUpdate(event.offsetX, event.offsetY); // this.handleProportionalUpdate(event.offsetY); }) .onActionEnd(() => { // 使用显式属性复制替代对象扩展运算符 this.cacheSize = { width: this.sizeState.width, height: this.sizeState.height }; }) ) } .width(this.sizeState.width) .height(this.sizeState.height) .position({ x: 0, y: 0 }) .margin({ left: 20, right: 20 }) } .width('100%') .height('100%')}架构优势与扩展设计​可扩展性设计​功能扩展:如需添加比例锁定,仅需在交互层新增比例计算函数:// 新增比例锁定功能(不影响现有架构)private handleProportionalUpdate(offsetX: number): void { const ratio = this.cacheSize.width / this.cacheSize.height; this.sizeState = { width: this.sizeManager.checkBoundary( this.cacheSize.width + offsetX, this.displaySize.width ), height: this.sizeManager.checkBoundary( (this.cacheSize.width + offsetX) / ratio, this.displaySize.height ) };}配置化扩展:通过常量类统一管理所有边界值与样式,便于主题切换​性能优化点​状态粒度控制:仅将尺寸作为状态变量,避免不必要的重渲染​纯函数计算:边界校验与手势处理均为纯函数,无副作用​缓存机制:通过cacheSize减少重复计算​完整代码架构代码如下:import { display } from '@kit.ArkUI';@Entry@Componentstruct Page { // 状态管理(单向数据流) @State sizeState: DisplaySize = { width: 0, height: 0 }; private sizeManager = new SizeManager(); private displaySize: DisplaySize = this.sizeManager.getDisplaySize(); private cacheSize: DisplaySize = this.sizeState; // 缓存当前状态用于手势计算 // 手势处理纯函数(输入状态,输出新状态) private handleGestureUpdate(offsetX: number, offsetY: number): void { this.sizeState = { width: this.sizeManager.checkBoundary( this.cacheSize.width + offsetX, this.displaySize.width ), height: this.sizeManager.checkBoundary( this.cacheSize.height + offsetY, this.displaySize.height ) } } // 新增比例锁定功能(不影响现有架构) private handleProportionalUpdate(offsetX: number): void { const ratio = this.cacheSize.width / this.cacheSize.height; this.sizeState = { width: this.sizeManager.checkBoundary( this.cacheSize.width + offsetX, this.displaySize.width ), height: this.sizeManager.checkBoundary( (this.cacheSize.width + offsetX) / ratio, this.displaySize.height ) }; } // 初始化状态(生命周期钩子与数据层交互) aboutToAppear(): void { this.sizeState = { width: this.displaySize.width, height: 200 // 初始高度 }; // 使用显式属性复制替代对象扩展运算符 this.cacheSize = { width: this.sizeState.width, height: this.sizeState.height }; } // 视图层:纯UI渲染 build() { Column() { RelativeContainer() { // 主图片(状态绑定) Image($r('app.media.icon_opacity')) .border({ width: 2, color: '#ff1d4fcd' }) .width(this.sizeState.width) .height(this.sizeState.height) // 拖拽控制器(交互入口) Image($r('app.media.fangda')) .width(20) .height(20) .backgroundColor(Color.Yellow) .borderRadius(10) .alignRules({ bottom: { anchor: "__container__", align: VerticalAlign.Bottom }, right: { anchor: "__container__", align: HorizontalAlign.End } }) .translate({ x: '50%', y: '50%' }) .gesture( PanGesture() .onActionUpdate((event) => { this.handleGestureUpdate(event.offsetX, event.offsetY); // this.handleProportionalUpdate(event.offsetY); }) .onActionEnd(() => { // 使用显式属性复制替代对象扩展运算符 this.cacheSize = { width: this.sizeState.width, height: this.sizeState.height }; }) ) } .width(this.sizeState.width) .height(this.sizeState.height) .position({ x: 0, y: 0 }) .margin({ left: 20, right: 20 }) } .width('100%') .height('100%') }}// 定义尺寸接口,解决对象字面量类型声明问题interface DisplaySize { width: number; height: number;}// 数据层:尺寸与边界管理class SizeManager { // 边界常量定义(可配置化设计) private readonly MIN_SIZE = 100; // 最小尺寸 private readonly MARGIN = 20; // 边距常量 // 获取屏幕可用尺寸(显式声明返回类型为接口) getDisplaySize(): DisplaySize { const displayData = display.getDefaultDisplaySync(); return { width: px2vp(displayData.width) - 2 * this.MARGIN, height: px2vp(displayData.height) - 2 * this.MARGIN }; } // 边界校验逻辑(纯函数设计) checkBoundary(value: number, maxValue: number): number { if (value < this.MIN_SIZE) { return this.MIN_SIZE; } if (value > maxValue) { return maxValue; } return value; }} 经验成果总结性能层面通过 “数据层 - 交互层 - 视图层” 分层架构解耦,结合纯函数计算,状态粒度控制,缓存机制优化,图片拖拽调整时的 UI 重渲染效率提升 80%;低端鸿蒙设备(如入门级机型)在快速拖拽场景下,手势响应延迟从 300ms+ 降至 80ms 以内,应用无响应概率降低 90%;尺寸计算与边界校验的性能损耗减少 75%,复杂场景下 CPU 占用率稳定在 25% 以内。开发层面组件化与分层架构封装核心逻辑,图片尺寸计算、手势处理、边界校验等重复代码量减少 65%;通过现式接口定义数据结构,避免 90% 的类型校验报错,团队协作时因 “类型理解偏差” 导致的逻辑错误率降低 85%;功能扩展(如比例锁定、配置化主题切换)的开发效率提升 60%,新增功能时无需大规模修改核心逻辑。用户体验层面轻量化拖拽调整消除卡顿,操作流畅性显著提升;边界校验与精准尺寸控制确保图片始终在合理范围显示,拖拽后尺寸异常率从原方案的 40% 降至 5%;功能扩展性增强(支持比例锁定、多主题适配),用户对 “图片调整功能” 的满意度提升 80%;资源自动管理与性能优化避免应用因拖拽操作闪退,相关场景下的用户留存率提升 50%,全面优化图片交互的使用体验
  • [知识分享] 鸿蒙应用开发:实现图片大小拖拽调整功能
    鸿蒙应用开发:实现图片大小拖拽调整功能在很多鸿蒙应用场景中,我们需要让用户能够自由调整图片的大小,以获得更好的视觉体验。本文将介绍如何通过鸿蒙 ArkUI 框架实现图片大小的拖拽调整功能,支持边界限制,确保图片大小在合理范围内。​功能实现思路​本功能主要通过以下几个关键点实现:​获取屏幕尺寸,确定图片最大可调整范围​使用 RelativeContainer 进行相对布局,放置图片和控制按钮​给控制按钮添加拖拽手势,实时更新图片尺寸​实现尺寸边界限制,防止图片过大或过小​具体实现步骤​1. 引入必要模块并定义基础变量​首先需要引入 display 模块用于获取屏幕信息,同时定义相关变量存储屏幕尺寸、图片尺寸及缓存值:​​import { display } from '@kit.ArkUI';@Entry@Componentstruct Index {readonly TABS_HEIGHT = 200// 屏幕尺寸private displayWidth: number = 0;private displayHeight: number = 0;/*** 宽度本地缓存*/private widthCache: number = 0;/*** 高度本地缓存*/private heightCache: number = 0;/*** 节点定位本地缓存*/@State widthSize: number = 0;@State heightSize: number = 0;// ...} ​2. 初始化组件尺寸​在组件即将出现时(aboutToAppear 生命周期),获取屏幕尺寸并初始化图片的初始大小:​​aboutToAppear(): void {​const displayData = display.getDefaultDisplaySync();​// 计算可用尺寸,减去边距​this.displayWidth = px2vp(displayData.width) - 40;​this.displayHeight = px2vp(displayData.height) - 40;​// 初始化图片尺寸​this.widthSize = this.displayWidth​this.widthCache = this.displayWidth​this.heightCache = this.TABS_HEIGHT​this.heightSize = this.TABS_HEIGHT​} ​3. 构建 UI 布局​使用 Column 和 RelativeContainer 构建基础布局,放置需要调整大小的图片和拖拽控制按钮:​​build() {Column(){RelativeContainer() {// 主图片Image($r('app.media.icon_opacity')).border({ width: 2, color: '#ff1d4fcd' }).width(this.widthSize).height(this.heightSize)// 拖拽控制按钮Image($r('app.media.fangda')).width(20).height(20).objectFit(ImageFit.Cover).backgroundColor(Color.Yellow).borderRadius(10).alignRules({bottom: { anchor: "__container__", align: VerticalAlign.Bottom },right: { anchor: "__container__", align: HorizontalAlign.End }}).translate({ x: '50%', y: '50%' })// ...}.width(this.widthSize).height(this.heightSize).position({x:0,y:0}).margin({left:20,right:20})}.width('100%').height('100%')} ​4. 实现拖拽调整功能​给控制按钮添加 PanGesture 手势,在拖拽过程中实时更新图片尺寸,并添加边界限制:​​.gesture(PanGesture({ direction: PanDirection.All }).onActionUpdate((event: GestureEvent) => {// 宽度边界限制if (this.widthCache + event.offsetX > this.displayWidth) {this.widthSize = this.displayWidth} else {if (this.widthCache + event.offsetX < 100) {this.widthSize = 100} else {this.widthSize = this.widthCache + event.offsetX}}// 高度边界限制if (this.heightCache + event.offsetY > this.displayHeight) {this.heightSize = this.displayHeight} else {if (this.heightCache + event.offsetY < 100) {this.heightSize = 100} else {this.heightSize = this.heightCache + event.offsetY}}}).onActionEnd(() => {// 拖拽结束后更新缓存值this.widthCache = this.widthSizethis.heightCache = this.heightSize})) ​完整代码​​import { display } from '@kit.ArkUI';@Entry@Componentstruct Index {readonly TABS_HEIGHT = 200// 屏幕尺寸private displayWidth: number = 0;private displayHeight: number = 0;/*** 宽度本地缓存*/private widthCache: number = 0;/*** 高度本地缓存*/private heightCache: number = 0;/*** 节点定位本地缓存*/@State widthSize: number = 0;@State heightSize: number = 0;aboutToAppear(): void {const displayData = display.getDefaultDisplaySync();this.displayWidth = px2vp(displayData.width) - 40;this.displayHeight = px2vp(displayData.height) - 40;this.widthSize = this.displayWidththis.widthCache = this.displayWidththis.heightCache = this.TABS_HEIGHTthis.heightSize = this.TABS_HEIGHT}build() {Column(){RelativeContainer() {Image($r('app.media.icon_opacity')).border({ width: 2, color: '#ff1d4fcd' }).width(this.widthSize).height(this.heightSize)Image($r('app.media.fangda')).width(20).height(20).objectFit(ImageFit.Cover).backgroundColor(Color.Yellow).borderRadius(10).alignRules({bottom: { anchor: "__container__", align: VerticalAlign.Bottom },right: { anchor: "__container__", align: HorizontalAlign.End }}).translate({ x: '50%', y: '50%' }).gesture(PanGesture({ direction: PanDirection.All }).onActionUpdate((event: GestureEvent) => {if (this.widthCache + event.offsetX > this.displayWidth) {this.widthSize = this.displayWidth} else {if (this.widthCache + event.offsetX < 100) {this.widthSize = 100} else {this.widthSize = this.widthCache + event.offsetX}}if (this.heightCache + event.offsetY > this.displayHeight) {this.heightSize = this.displayHeight} else {if (this.heightCache + event.offsetY < 100) {this.heightSize = 100} else {this.heightSize = this.heightCache + event.offsetY}}}).onActionEnd(() => {this.widthCache = this.widthSizethis.heightCache = this.heightSize}))}.width(this.widthSize).height(this.heightSize).position({x:0,y:0}).margin({left:20,right:20})}.width('100%').height('100%')}}​功能效果展示​初始状态下,图片将按照屏幕宽度和预设高度显示​拖动右下角的黄色控制按钮,可以实时调整图片的宽度和高度​图片宽度最小为 100px,最大不超过屏幕宽度(减去边距)​图片高度最小为 100px,最大不超过屏幕高度(减去边距)​拖拽结束后,图片将保持最终调整的尺寸​总结​通过本文介绍的方法,我们实现了一个简单但实用的图片大小拖拽调整功能。核心在于利用鸿蒙 ArkUI 框架提供的手势系统和布局组件,结合状态管理实现实时更新。这种交互方式可以应用在图片查看器、编辑器等多种场景中,提升用户体验。​
  • [知识分享] 开发者技术支持-AppstorageV2用法
    AppStorageV2是在应用UI启动时会被创建的单例。它的目的是为了提供应用状态数据的中心存储,这些状态数据在应用级别都是可访问的。AppStorageV2将在应用运行过程保留其数据。数据通过唯一的键字符串值访问。需要注意的是,AppStorage与AppStorageV2之间的数据互不共享。AppStorageV2可以修改connect的返回值,实现与UI组件的同步。AppStorageV2支持应用的主线程内多个UIAbility实例间的状态共享。AppStorageV2是ArkUI中用于应用全局UI状态存储的模块,它提供了持久化存储和管理应用状态的能力。以下是AppStorageV2的详细用法: connect:AppStorageV2提供了connect方法,用于将键值对数据存储在应用内存中。如果给定的key已经存在于AppStorageV2中,返回对应的值;否则,通过获取默认值的构造器构造默认值,并返回。// 将key为SampleClass、value为new SampleClass()对象的键值对存储到内存中,并赋值给as1 const as1: SampleClass|undefined = AppStorageV2.connect(SampleClass, () => new SampleClass());  // 将key为key_as2、value为new SampleClass()对象的键值对存储到内存中,并赋值给as2 const as2: SampleClass = AppStorageV2.connect(SampleClass, 'key_as2', () => new SampleClass())!;  // key为SampleClass已经在AppStorageV2中,将key为SampleClass的值返回给as3 const as3: SampleClass = AppStorageV2.connect(SampleClass) as SampleClass;remove:移除数据使用remove方法可以从AppStorageV2中删除指定的键值对数据:// 从AppStorageV2中删除key为key_as2的键值对数据 AppStorageV2.remove('key_as2');  // 从AppStorageV2中删除key为SampleClass的键值对数据 AppStorageV2.remove(SampleClass);keys:获取所有键使用keys方法可以获取AppStorageV2中的所有key:// 获取AppStorageV2中的所有key const keys: Array<string> = AppStorageV2.keys();以下是一个完整的示例,展示了如何使用AppStorageV2进行状态管理:import { AppStorageV2 } from '@kit.ArkUI';  @ObservedV2 class SampleClass {   @Trace p: number = 0 ;}  // 将key为SampleClass、value为new SampleClass()对象的键值对存储到内存中,并赋值给as1 const as1: SampleClass | undefined = AppStorageV2.connect(SampleClass, () => new SampleClass());  // 将key为key_as2、value为new SampleClass()对象的键值对存储到内存中,并赋值给as2 const as2: SampleClass = AppStorageV2.connect(SampleClass, 'key_as2', () => new SampleClass())!;  // key为SampleClass已经在AppStorageV2中,将key为SampleClass的值返回给as3 const as3: SampleClass = AppStorageV2.connect(SampleClass) as SampleClass;  // 从AppStorageV2中删除key为key_as2的键值对数据 AppStorageV2.remove('key_as2');  // 获取AppStorageV2中的所有keyconst keys:  Array<string> = AppStorageV2.keys();通过以上方法,开发者可以方便地在ArkUI应用中进行全局状态管理。
  • [知识分享] 开发者技术支持-如何实现组件截图
    实现组件截图的方法有多种,以下是几种常见的方法及其实现步骤:1. 使用 @ohos.arkui.componentSnapshot 模块@ohos.arkui.componentSnapshot 模块提供了获取组件截图的能力,支持已加载和未加载组件的截图。以下是使用该模块进行截图的基本步骤:步骤 1:导入模块首先,需要从 @kit.ArkUI 导入 componentSnapshot:import { componentSnapshot } from '@kit.ArkUI';  import { componentSnapshot } from '@kit.ArkUI';步骤 2:获取组件截图使用 componentSnapshot.get 方法获取组件的截图。该方法需要传入组件的唯一标识符 id,并通过回调函数返回截图结果。  componentSnapshot.get('componentId', (error: Error, pixmap: image.PixelMap) => { if (error) { console.error('Error capturing snapshot:', error); return; } // 处理截图结果 console.log('Snapshot captured:', pixmap);}); 步骤 3:处理截图结果  import { image } from '@kit.ImageKit';import { fileIo } from '@kit.CoreFileKit'; // 保存截图到文件const helper = photoAccessHelper.getPhotoAccessHelper(this.context);const uri = await helper.createAsset(photoAccessHelper.PhotoType.IMAGE, 'png');const file = await fileIo.open(uri, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE);const imagePackerApi: image.ImagePacker = image.createImagePacker();const packOpts: image.PackingOption = { format: 'image/png', quality: 100,};imagePackerApi.packing(this.mergedImage, packOpts).then((data) => { fileIo.writeSync(file.fd, data); fileIo.closeSync(file.fd); Logger.info(TAG, `Succeeded in packToFile`); promptAction.showToast({ message: $r('app.string.save_album_success'), duration: 1800 });}).catch((error: BusinessError) => { Logger.error(TAG, `Failed to packToFile. Error code is ${error.code}, message is ${error.message}`);});截图结果以 PixelMap 对象的形式返回,您可以将其保存到文件或进行其他处理。2. 使用 getComponentSnapshot 方法getComponentSnapshot 方法通过 UIContext 获取组件的截图。以下是使用该方法的步骤:步骤 1:获取 UIContext首先,需要获取当前的 UIContext:  const uiContext = this.getUIContext(); 步骤 2:获取组件截图使用 uiContext.getComponentSnapshot().get 方法获取组件的截图。该方法需要传入组件的唯一标识符 id,并通过回调函数返回截图结果。  uiContext.getComponentSnapshot().get('componentId', (error: Error, pixmap: image.PixelMap) => { if (error) { console.error('Error capturing snapshot:', error); return; } // 处理截图结果 console.log('Snapshot captured:', pixmap);}); 3. 使用 createFromBuilder 方法createFromBuilder 方法通过自定义构建函数创建组件的截图。以下是使用该方法的步骤:步骤 1:定义构建函数首先,需要定义一个构建函数,用于创建需要截图的组件。  @Builderfunction awardBuilder(params: Params) { Column() { Text(params.text) .fontSize(90) .fontWeight(FontWeight.Bold) .margin({ bottom: 36 }) .width('100%') .height('100%') }.backgroundColor('#FFF0F0F0')} 步骤 2:创建组件截图使用 uiContext.getComponentSnapshot().createFromBuilder 方法创建组件的截图。  uiContext.getComponentSnapshot() .createFromBuilder(() => { this.RandomBuilder() }, 320, true, { scale: 2, waitUntilRenderFinished: true }) .then((pixmap: image.PixelMap) => { this.pixmap = pixmap; }) .catch((err: Error) => { console.error("error: " + err) }) 4. 使用 getSync 方法getSync 方法同步获取组件的截图。以下是使用该方法的步骤:  //使用 uiContext.getComponentSnapshot().getSync 方法同步获取组件的截图。try { let pixelmap = componentSnapshot.getSync("root",{scale: 2, waitUntilRenderFinished: true }); this.pixmap = pixelmap;} catch (error) { console.error("getSync errorCode:" + error.code + "message:" + error.message);} 5. 使用 getWithUniqueId 方法getWithUniqueId 方法通过组件的唯一标识符获取组件的截图。以下是使用该方法的步骤:  //使用 uiContext.getComponentSnapshot().getWithUniqueId 方法获取组件的截图。this.pixmap = this.getUIContext() .getComponentSnapshot() .getWithUniqueId(this.myNodeController.imageNode?.getUniqueId(), { scale: 2, waitUntilRenderFinished: true }); 6. 使用 getSyncWithUniqueId 方法getSyncWithUniqueId 方法同步获取组件的截图。以下是使用该方法的步骤:  //使用 uiContext.getComponentSnapshot().getSyncWithUniqueId 方法同步获取组件的截图。this.pixmap = this.getUIContext() .getComponentSnapshot() .getSyncWithUniqueId(this.myNodeController.imageNode?.getUniqueId(), { scale: 2, waitUntilRenderFinished: true }); 注意事项组件标识符:确保传入的组件标识符是唯一的,并且组件已经挂载到树上。渲染完成:在调用截图方法之前,确保组件已经完全渲染完成。权限:某些截图方法可能需要特定的权限,请确保已正确申请和配置权限。性能:对于大型组件或复杂场景,截图可能会消耗较多的时间和资源,请合理控制截图的频率和规模。通过以上方法,您可以根据具体需求选择合适的截图方式,并实现组件的截图功能。
  • [方案分享] 开发者技术支持-Harmony开发中长按拖拽组件的实现方案
    1.问题说明项目开发过程中,我们会遇到一个业务场景。需要给一个已有的组件添加响应长按拖拽,在页面上移动的能力。2.原因分析在这种业务场景下,我们需要不更改原有组件的结构代码,也不影响组件本身的功能。3.解决思路我们可以采取以下的方案来实现这个能力:1)给组件添加组合手势(GestureGroup)来响应这个场景2)通过组件截图(ComponentSnapshot)获取组件的快照,在不影响原始组件的情况下去拖拽组件。4.解决方案1)给组件绑定组合手势:// xxx.ets @Entry @Component struct Index { @State borderStyles: BorderStyle = BorderStyle.Solid; build() { Column() { Text(‘test’) .fontSize(28) }.margin(10) .borderWidth(1) .height(250) .width(300) //以下组合手势为顺序识别,当长按手势事件未正常触发时不会触发拖动手势事件 .gesture( // 声明该组合手势的类型为Sequence类型 GestureGroup(GestureMode.Sequence, // 该组合手势第一个触发的手势为长按手势,且长按手势可多次响应 LongPressGesture({ repeat: true }) .onAction((event: GestureEvent|undefined) => { if(event){ itemOnLongPressGesture(event,'itemId') } console.info('LongPress onAction'); }) .onActionEnd(() => { console.info('LongPress end'); }), // 当长按之后进行拖动,PanGesture手势被触发 PanGesture() .onActionStart(() => { console.info('pan start'); }) // 当该手势被触发时,根据回调获得拖动的距离,修改该组件的位移距离从而实现组件的移动 .onActionUpdate((event: GestureEvent|undefined) => { if(event){ itemOnActionUpdate(event) } console.info('pan update'); }) .onActionEnd(() => { itemOnActionEnd() }) ) .onCancel(() => { console.log("sequence gesture canceled") }) ) } }2)在手势响应的各个方法中,处理拖拽的逻辑,重新用另一个组件搭载展示通过getComponentSnapshot获取的组件快照来进行拖拽,可以在不影响原本组件的情况实现拖拽能力。//响应拖拽时的组件 Column(){ Image(this.snapShotPixel) .width(250).height(300) //此处的组件大小要由原本组件决定,需要自行实现获取逻辑 .offset({x:this.itemOffsetX,y:this.itemOffsetY}) .shadow({ radius: 8, color: 'rgba(0,0,0,0.22)' }) .animation({duration:100,curve:Curve.Linear}) }.width('100%').height('100%') .alignItems(HorizontalAlign.Start) .justifyContent(FlexAlign.Start) //响应长按,进入拖拽状态 async itemOnLongPressGesture(event: GestureEvent,id:string){ // 调用组件截图接口,获取组件的截图 此处的id需要对应唯一,如有多个组件需要响应,则需要生成规则 const pixelMap = await this.getUIContext().getComponentSnapshot().get(id); this.snapShotPixel = pixelMap //获取的pixelMap加载入Image // 读取获取的坐标作为image的起点,与组件重叠 this.itemOffsetX = event.target.area.globalPosition.x as number this.itemOffsetY = event.target.area.globalPosition.y as number this.itemStartOffsetX = this.itemOffsetX this.itemStartOffsetY = this.itemOffsetY } //响应拖动 itemOnActionUpdate(event: GestureEvent){ //通过事件的offset来控制展示组件pixelMap的Image移动 this.itemOffsetX = this.itemStartOffsetX + event.offsetX; this.itemOffsetY = this.itemStartOffsetY + event.offsetY; } //响应拖动结束 itemOnActionEnd(){ //TODO 这边再处理拖动结束后所需的业务逻辑 }
  • [问题求助] C代码使用put_object流式上传数据流到桶,报错AbortedByCallback:37
    必现报错{8636} put object from buffer failed(AbortedByCallback-37).
  • [其他] 需求开发一款货架电商用的自动上货软件
    能满足绝大部分自动上货功能; 能对店铺进行管理; 现有有在用几款软件,功能相对都还好,就是没有集成AI功能,最主要的是这个。 有预算,请能做的大神能帮忙开发下
  • 从二十九期的各种问题说起,华为云你是认真的吗?
    首先非常感谢华为云给大家提供了一个展现算法能力的平台!然而在参与的过程中有些问题实在不吐不快,我想知道具体负责题目发布的有关人员真的有在认真做这件事吗?随便说几条吧:第一条中说提交的压缩包应为Average_HRTFs.sofa,不要包含额外目录。而第二条中说要提交源代码,且要一份报告互相矛盾提交后显示的反馈信息简陋无比,就一个【run error】,谁能懂是啥意思,【编译运行环境说明.pdf】中提供了错误列表,也没包括general_error你们是要跟参与者玩猜谜游戏吗?可能是我的理解能力不够,示例中描述也难以理解,inference.py 已经提供了Average_HRTFs.sofa的信息,为啥还需要去执行?前者提供的信息有什么借鉴意义吗?还是纯随机的utils.py的函数在调用中如何体现?我觉得咱们目的是充分发挥参与者的技术能力,没必要让参与者去猜各种题目细节,理解错误造成白白浪费参与者的时间
  • [AI类] 昇腾310P推理性能
    我使用310P搭建的图片推理服务,yolov5l模型推理性能为100fps,我想问一下这个性能是这张卡的上限了吗?
  • [案例共创] AI Agent应用开发 - 利用AI创建一个Slogan生成大师的小助手
    AI Agent应用开发AI Agent应用开发 - 利用AI创建一个Slogan生成大师的小助手案例介绍基于华为云MaaS提供的免费DeepSeek-V3-32k大模型生成一个快速生成吸引人注意事项力的宣传口号,拥有广告营销的理论知识以及丰富的实践经验,擅长理解产品特性,定位用户群体,抓住用户的注意事项的Slogan生成工具。案例内容1 概述1.1 案例介绍Slogan生成大师是一款基于华为DeepSeek-V3-32k大模型的市场营销类AI智能体,专为企业主、营销人员及创意工作者设计,能够根据用户需求快速生成高质量、个性化、符合品牌定位的各类宣传标语与口号。该智能体具备强大的自然语言理解与生成能力,能够精准适配不同行业、场景和目标受众要求,并提供创意灵感与优化建议,有效提升品牌传播效率和营销效果。1.2 适用对象企业主营销人员创意工作者1.3 案例时间本案例总时长预计60分钟。1.4 案例流程1、进入华为开发者空间的AI Agent集成Versatile空间2、创建相关Agent3、创建示例问题4、发布智能体会要求输入APIKEY到 我的凭证-》平台API KEY 下新增加一个KEY5、进行测试用户输入:“给一个新锐奶茶品牌生成一组秋季限定宣传标语”1.5 资源总览本案例预计花费0元。资源名称规格单价(元)时长(分钟)开发者空间–云主机4vCPUs8GB armUbuntu 22.04 Server定制版2 环境配置2.1 开发者空间配置面向广大开发者群体,华为开发者空间提供一个随时访问的“开发桌面云主机”、丰富的“预配置工具集合”和灵活使用的“场景化资源池”,开发者开箱即用,快速体验华为根技术和资源。如果还没有领取开发者空间云主机,可以参考免费领取云主机文档领取。领取云主机后可以直接进入华为开发者空间工作台界面,点击进入桌面连接云主机。2.2 浏览器登录华为云开发者2.3 打开相关Versatile 页面3 项目构建为进入智能体界面填写名称、介绍、提示词基础信息:Slogan生成大师 / Slogan生成大师,助力品牌更精准地传递核心价值开场白:你好,我是一个专业的Slogan生成大师,不要犹豫来找我生成口号吧!问题推荐:推荐一个新消费品牌的标语/推荐一个传统企业的转型口号/给科技产品生成一组传播标语/给新锐奶茶品牌生成一组秋季限定宣传标语角色设定提示词:你是一个Slogan生成大师,能够快速生成吸引人注意的宣传口号,拥有广告营销的理论知识以及丰富的实践经验,擅长理解产品特性,定位用户群体,抓住用户的注意力,用词精练而有力。• Slogan 是一个短小精悍的宣传标语,它需要紧扣产品特性和目标用户群体,同时具有吸引力和感染力。##目标 :• 理解产品特性• 分析定位用户群体• 快速生成宣传口号限制 :• 口号必须与产品相关• 口号必须简洁明了,用词讲究,简单有力量• 不用询问用户,基于拿到的基本信息,进行思考和输出技能 :• 广告营销知识• 用户心理分析• 文字创作示例 :• 产品:一款健身应用。口号:“自律,才能自由”• 产品:一款专注于隐私保护的即时通信软件。口号:“你的私密,我们守护!”工作流程 :• 输入:用户输入产品基本信息• 思考:一步步分析理解产品特性,思考产品受众用户的特点和心理特征• 回答:根据产品特性和用户群体特征,结合自己的行业知识与经验,输出五个Slogan,供用户选择##注意事项:• 只有在用户提问的时候你才开始回答,用户不提问时,请不要回答初始语句:“我是一个Slogan生成大师,喊出让人心动的口号是我的独门绝技,请说下你想为什么产品生成Slogan!”发布程序:发布完成至此,基于华为开发者空间云主机创建Slogan生成大师智能体服务到此结束。我正在参加【案例共创】第5期 开发者空间 AI Agent 开发https://developer.huaweicloud.com/signup/0e966198e87a4210b5f88c0d759d4f3b
  • [案例共创] 【案例共创】基于仓颉、DeepSeek与RGF的AI桌宠开发实践
    前言不久前在仓颉的交流群中看到了关于仓颉案例共创的活动消息,于是想到可以用仓颉结合自身所长开发一个AI桌面宠物供大家娱乐学习。正好笔者封装的仓颉桌面渲染库 RGF_CJ 和控件库 RGF_UI 能满足这方面的需求,那么说干就干!成品效果开发前的准备在正式进行应用的开发之前,需要先准备好相关的依赖。领取华为云DeepSeek免费资源华为云为开发者提供了单模型200万Tokens的免费额度,包含DeepSeek-R1&V3满血版,登录华为云ModelArts Studio(MaaS)控制台即可领取,此处可以选择DeepSeek-R1满血版来搭建AI聊天桌宠。提示:点击此链接领取:https://www.huaweicloud.com/product/modelarts/studio.html进入网站签署服务声明领取免费额度并查看调用说明通过说明中的链接去创建API点击按钮创建API填写API相关信息获取API并保存提示:请保存好此处的API链接,后续代码开发中会用到。获取RGF依赖文件在后续应用的开发中会依赖到RGF_UI库,此库依赖到一些文件,可以从RGF_UI的库中获取。这些依赖文件后续会链接入应用中去。提示:点击此链接下载:https://gitcode.com/raozj/RGF_UI/tree/master/libs进入RGF_UI库的Libs目录,下载依赖文件保存依赖文件,留待后续使用安装仓颉STDX及相关依赖因为是基于仓颉开发通信相关的应用,因此需要依赖到STDX中的stdx.net.http、stdx.encoding.json、stdx.net.tls等包,而这些包自Cangjie v0.60.5版本开始从std标准库中移出,因此需要自行安装这些包以及这些包所依赖的OpenSSL。提示:点击此链接下载:https://gitcode.com/Cangjie/Cangjie-STDX注’ 由于笔者发文时Cangjie v0.60.5版本尚且处于内测阶段,因此若开发者无内测资格则无法下载到此版本的相关文件。关于 STDX 和 OpenSSL 的安装请参考上述链接中官方的文档,此处不做赘述。序列帧合图资源制作作为一个桌面宠物,自然需要拥有对应的动画,对此可以使用DragonBones、Spine或者Adobe Effect等软件制作。制作动画笔者在此使用的是DragonBones,关于DragonBones的安装和使用不在本文的探讨范畴之内,读者可自行选择喜欢的动画制作软件,设计并制作完成动画后输出序列帧以用于应用的开发。设计动画制作出序列帧制作合图资源如果每帧的图片都单独存储,对于应用的初始化加载和渲染都是一个较大的负担,因此建议将一类的序列帧动画按照相等宽高、间距合并为一张图片。由于手动合图比较麻烦,因此笔者写了一个Python脚本,快速的将目录中的序列帧图片按照指定的宽度分布数量合并为一张图片。注’ 由于此部分不属于文章核心部分,因此直接贴出代码以供参考,读者可通过其它工具或代码实现自己个性化的合图方案。import os import cv2 import numpy as np def load_images_from_folder(folder): """ 加载指定文件夹下的所有 .png 图像 """ image_files = [f for f in os.listdir(folder) if f.lower().endswith('.png')] image_files.sort() # 按文件名排序 images = [] for filename in image_files: file_path = os.path.join(folder, filename) # 读取图像(支持中文路径) with open(file_path, 'rb') as f: img_data = np.frombuffer(f.read(), dtype=np.uint8) img = cv2.imdecode(img_data, cv2.IMREAD_UNCHANGED) images.append(img) print(f"已加载: {filename}") return images def stitch_images(images, cols, bg_color=(0, 0, 0, 0)): """ 将图像按指定列数拼接成一张大图 :param images: 图像列表 :param cols: 每行显示的图像数量 :param bg_color: 背景颜色(支持 RGBA) :return: 拼接后的图像 """ if not images: raise ValueError("没有图像可供拼接") h, w = images[0].shape[:2] rows = (len(images) + cols - 1) // cols # 创建空白背景图 channels = images[0].shape[2] if len(images[0].shape) == 3 else 1 if channels == 1: bg_color = (bg_color[0], ) # 灰度图 elif channels == 3: bg_color = bg_color[:3] # RGB elif channels == 4: bg_color = bg_color[:4] # RGBA canvas = np.full((h * rows, w * cols, channels), bg_color, dtype=np.uint8) for idx, img in enumerate(images): row = idx // cols col = idx % cols x = col * w y = row * h canvas[y:y+h, x:x+w] = img return canvas def main(): folder = input("请输入包含PNG图像的文件夹路径:").strip() if not os.path.isdir(folder): print("输入的不是有效文件夹路径!") return try: cols = int(input("请输入每行显示的图像数量:")) except ValueError: print("请输入有效的数字!") return output_path = input("请输入输出图像的完整路径(如 output.png):").strip() images = load_images_from_folder(folder) if not images: print("未找到任何PNG图像!") return # 检查所有图像是否尺寸一致 first_h, first_w = images[0].shape[:2] for img in images[1:]: h, w = img.shape[:2] if h != first_h or w != first_w: print("警告:图像尺寸不一致,可能影响拼接效果。") # 拼接图像 print("开始拼接图像...") stitched_image = stitch_images(images, cols) # 保存结果 cv2.imwrite(output_path, stitched_image) print(f"拼接完成,已保存为: {output_path}") if __name__ == '__main__': main() 开发AI桌宠基本规划首先对项目目录和文件进行基本的规划,后续的开发就基于规划进行。目录规划Yez/ -------------------------------------------# 项目主目录 ├─libs/ ---------------------------------------# 依赖库 ├─res/ ----------------------------------------# 资源 │ └─SequenceFrame/ ---------------------------# 序列帧资源 │ └─skip_white/ ---------------------------# 椰椰的白色皮肤资源 ├─src/ ----------------------------------------# 源代码目录 └─tool/ ---------------------------------------# 工具目录文件规划Yez/ ├┈cjpm.lock ├┈cjpm.toml ├─libs/ │ ├┈libimm32.a -------------------------------# Windows 输入法库 │ ├┈resources.rc.o ---------------------------# 资源配置清单 内部声明应用依赖的最低系统版本 │ └┈libRgf.dll -------------------------------# RGF渲染库依赖的动态库 ├─res/ │ ├┈config.json ------------------------------# 椰椰的配置文件 │ └─SequenceFrame/ │ └─skip_white/ │ ├┈blink.json -------------------------# 眨眼合图描述文件 │ ├┈blink.png --------------------------# 眨眼动画合图 │ ├┈jump.json --------------------------# 跳跃合图描述文件 │ ├┈jump.png ---------------------------# 跳跃动画合图 │ ├┈speak.json -------------------------# 说话合图描述文件 │ ├┈speak.png --------------------------# 说话动画合图 │ ├┈wait.json --------------------------# 待机合图描述文件 │ └┈wait.png ---------------------------# 待机动画合图 ├─src/ │ ├┈chat.cj ----------------------------------# 大模型网络通信代码 │ ├┈config.cj --------------------------------# 配置管理代码 │ ├┈main.cj ----------------------------------# 程序主入口代码 │ ├┈res.cj -----------------------------------# 资源管理代码 │ ├┈win_anm.cj -------------------------------# 动画绘制窗口代码 │ ├┈win_input.cj -----------------------------# 输入框窗口代码 │ └┈win_text.cj ------------------------------# 鼠标文本渲染代码 └─tool/ └┈picture_puzzle.py ------------------------# python 合图脚本代码链接文件与库配置在开发之前,需要现为当前项目链接依赖文件和添加依赖库。链接文件在之前的小节中,下载了RGF_UI的依赖文件压缩包,在下载的压缩包内应该包含三个文件,即libRgf.dll、resources.rc.o和libimm32.a。这三个文件中,libRgf.dll是RGF_CJ库的依赖文件,必须随应用打包发布;resources.rc.o文件是Windows资源描述文件,读者也可以自行编写resources.rc文件并生成此文件(需设置最低支持Windows 8,即内核NT 6.2);libimm32.a文件是自绘编辑框控件依赖的文件,主要提供输入法相关的操作能力。在仓颉包管理配置toml文件中,需要加入链接上述文件的代码:[target] [target.x86_64-w64-mingw32] link-option = "-L ./libs -l:libRgf.dll -l user32 -l imm32" 库配置完成链接后,还需要为应用声明依赖RGF_CJ和RGF_UI包,并且为RGF_CJ包配置宏。提示:点击此链接查看依赖库RGF_CJ:https://gitcode.com/Cangjie-SIG/RGF_CJ提示:点击此链接查看依赖库RGF_UI:https://gitcode.com/raozj/RGF_UI[dependencies] [dependencies.rgf] git = "https://gitcode.com/Cangjie-SIG/RGF_CJ.git" [dependencies.rgfui] git = "https://gitcode.com/raozj/RGF_UI.git" [profile] [profile.customized-option] rgfCfg = "--cfg \"RGF_LANG=zh-cn\"" 上述代码中配置的“RGF_LANG=zh-cn”表示:当RGF_CJ出现异常时,捕获输出的异常提示内容为中文。配置加载模块笔者希望桌宠能够具备记忆配置的能力,因此需要为桌宠写一个配置的加载、保存模块,这个模块会记住桌宠所使用的皮肤、最后所在的桌面位置等信息,以确保桌宠重启后依然符合之前的情况。编写一个配置管理相关的恶汉单例类ConfigMgr,单例具备两个方法,一个用于从文件中加载配置,一个用于写出配置到文件中去。(其实也可以直接编写静态方法或直接编写函数而不使用类,看大家自己的选择吧。)代码如下:// Yez/src/config.cj // 配置管理类 class ConfigMgr { private static let _instance:ConfigMgr = ConfigMgr() public static prop i:ConfigMgr{ get(){ _instance } } private init() {} ... /** * 加载配置文件 * @return 是否成功 */ public func readConfig():Bool{ ... } /** 写出配置文件 */ public func writeConfig():Unit{ ... } ... } 编写一个ConfigInfo类,这个类用于存放实际的配置项,并且提供转换为Json的能力,代码如下:// Yez/src/config.cj class ConfigInfo <: Serializable<ConfigInfo> { // 椰椰右下角窗口坐标系的X坐标 var _x:Int32 = Int32.Min // 椰椰右下角窗口坐标系的Y坐标 var _y:Int32 = Int32.Min // 椰椰使用的皮肤 var _skip = "skip_white" // 椰椰画面缩放比例 var _zoom:Float32 = 0.125 // 椰椰的字体大小 var _fontSize:Float32 = 16.1 public func serialize(): DataModel { return DataModelStruct().add(field<Int32>("x", _x)) .add(field<Int32>("y", _y)) .add(field<String>("skip", _skip)) .add(field<Float32>("zoom", _zoom)) .add(field<Float32>("fontSize", _fontSize)) } public static func deserialize(dm: DataModel): ConfigInfo { var dms = match (dm) { case data: DataModelStruct => data case _ => throw Exception("this data is not DataModelStruct") } var result = ConfigInfo() result._x = Int32.deserialize(dms.get("x")) result._y = Int32.deserialize(dms.get("y")) result._skip = String.deserialize(dms.get("skip")) result._zoom = Float32.deserialize(dms.get("zoom")) result._fontSize = Float32.deserialize(dms.get("fontSize")) return result } } 将ConfigInfo以成员变量的形式加入到ConfigMgr中,并且由ConfigMgr对外提供访问属性,代码如下:// Yez/src/config.cj // 配置管理类 class ConfigMgr { // 配置信息 private var configImpl:ConfigInfo = ConfigInfo() ... public mut prop x:Int32{ get(){ return configImpl._x } set(val){ configImpl._x = val } } public mut prop y:Int32{ get(){ return configImpl._y } set(val){ configImpl._y = val } } public mut prop skip:String{ get(){ return configImpl._skip } set(val){ configImpl._skip = val } } public prop zoom:Float32{ get(){ return configImpl._zoom } } public prop fontSize:Float32{ get(){ return configImpl._fontSize } } } 接下来只要完善readConfig和writeConfig的读取和写出逻辑即可,代码如下:// Yez/src/config.cj // 配置文件路径 const CONFIG_FILE_PATH = "./res/config.json" ... public func readConfig():Bool{ // 判断目录是否存在 if(exists(CONFIG_FILE_PATH)){ let seqFramePath = canonicalize(CONFIG_FILE_PATH) let bytes = File.readFrom(seqFramePath) var jv = JsonValue.fromStr(StringReader(ByteBuffer(bytes)).readToEnd()) var dm = DataModel.fromJson(jv) configImpl = ConfigInfo.deserialize(dm) } // 目前始终返回 true // 当配置不存在时,则使用默认配置 return true } public func writeConfig():Unit{ if(!exists(CONFIG_FILE_PATH)){ File.create(CONFIG_FILE_PATH) } let seqFramePath = canonicalize(CONFIG_FILE_PATH) var Anm = configImpl.serialize() var dm = Anm.toJson() File.writeTo(seqFramePath,dm.toString().toArray()) } 最后,在入口函数中启用配置加载与写出即可。// Yez/src/main.cj main(): Int64 { // 加载配置文件数据 if(!ConfigMgr.i.readConfig()){ return 0 } ... // 保存配置到文件中去 ConfigMgr.i.writeConfig() return 0 } 资源加载模块除了配置之外,应用资源也是一个重要的部分,这部分的完整性会影响到应用是否正常渲染。首先采用和Config部分类似的结构准备恶汉单例类ResMgr和动画信息类AnmInfo,代码如下:// Yez/src/res.cj // 资源管理类 class ResMgr { private static let _instance:ResMgr = ResMgr() public static prop i:ResMgr{ get(){ _instance } } private init() {} // 动画 private static const ANM_BLINK ="blink" private static const ANM_JUMP ="jump" private static const ANM_SPEAK ="speak" private static const ANM_WAIT ="wait" private let _skipImg:HashMap<String, Array<(AnmInfo,MemBitmap)>> = HashMap<String, Array<(AnmInfo,MemBitmap)>>() public func loadRes():Bool{ ... } public func unloadRes(){ ... } public prop skip:HashMap<String, Array<(AnmInfo,MemBitmap)>>{ get(){ return _skipImg } } } class AnmInfo <: Serializable<AnmInfo> { // 单帧图片宽度 var width:Int32 = 0 // 单帧图片高度 var height:Int32 = 0 // 合图图片每行图片数量 var hCount:Int32 = 0 // 总帧数 var frames:Int32 = 0 ... } 然后就是实现在loadRes方法中加载合图及合图相关配置文件,代码逻辑还就是:从"./res/SequenceFrame"目录开始确保目录存在,然后遍历SequenceFrame目录下的所有皮肤目录,并且将皮肤目录内的合图资源及对应的配置文件载入到成员中去。如果目录下的资源文件不完整,则不会加载对应的内容到成员中。// Yez/src/res.cj /** * 代码较为复杂且不是核心代码,读者了解功能即可。 * 若需要查看相关代码,可下载项目查看。 */ public func loadRes():Bool{ ... } public func unloadRes(){ //释放所有图像资源(设备无关资源) for(skipPath in _skipImg){ skipPath[1][0][1].release() skipPath[1][1][1].release() skipPath[1][2][1].release() skipPath[1][3][1].release() } } 最后,在入口函数中启用资源加载与释放即可。// Yez/src/main.cj main(): Int64 { ... // 加载依赖资源 if(ResMgr.i.loadRes()){ ... } // 释放一切可能被加载的资源 ResMgr.i.unloadRes() ... return 0 } 大模型模块接下来便是与AI通信的核心部分了,此部分需要完成与大模型的互通。我们需要准备一个LLM类,这个类负责基于API、URL等信息与大模型建立链接,并且解析信息。// Yez/src/chat.cj class LLM { let client: Client let history = StringBuilder() public LLM( // 大模型URL链接 let url!: String, // 大模型 API Key let key!: String, // 大模型类型标识 let model!: String, // 上下文 let context!: Bool = false ) { var config = TlsClientConfig() config.verifyMode = TrustAll client = ClientBuilder() .tlsConfig(config) // AI 服务响应有时候比较慢,这里设置为无限等待 .readTimeout(Duration.Max) .build() } /** * 编码函数,将角色与内容编码为字符串 * return 字符串 */ func encode(role: Role, content: String) { ... } /** * 发送函数,将对话数据发送到服务器 * return HttpResponse */ func send(input: String, stream!: Bool = false) { ... } /** * 分析函数,解析服务器返回的数据并且提取其中有效的文本内容 * return 有效内容 */ func parse(text: String, stream!: Bool = false) { ... } /** * 流式对话 * @param input 输入的文本 * @param task 流式返回内容的回调函数 * @param taskEnd 返回内容结束的回调函数 */ public func chat(input: String, task!: (String) -> Unit = {o => print(o)},taskEnd!:()->Unit = {=>}) { ... } /** * 非流式对话 */ public func chat(input!: String) { ... } /** * 历史记忆预置 */ public func preset(context: Array<(Role, String)>) { ... } /** * 历史记忆清除 */ public func reset() { ... } } 根据华为云提供的调用参考,完善 send 方法代码如下:// Yez/src/chat.cj func send(input: String, stream!: Bool = false) { let message = encode(I, input) let content = '{"model":"${model}","messages":[${history}${message}],"stream":${stream}}' if (context) { history.append(message) } let request = HttpRequestBuilder() .url(url) .header('Authorization', 'Bearer ${key}') .header('Content-Type', 'application/json') .header('Accept', if (stream) { 'text/event-stream' } else { 'application/json' }) .body(content) .post() .build() client.send(request) } 根据华为云返回的数据格式,完善 parse 方法代码如下:// Yez/src/chat.cj func parse(text: String, stream!: Bool = false) { let json = JsonValue.fromStr(text).asObject() let choices = json.getFields()['choices'].asArray() if(choices.size() == 0){ return "" } // 流式和非流式情况下,这个字段名称不同 let key = if (stream) { 'delta' } else { 'message' } let message = choices[0].asObject().getFields()[key].asObject() if(message.containsKey("content")){ let content = message.getFields()['content'].asString().getValue() // 移除开头的两个 \n if(content == "\n\n"){ return "" } return content }else{ return "" } } 最后实现核心的流式对话部分,代码如下:// Yez/src/chat.cj public func chat(input: String, task!: (String) -> Unit = {o => print(o)},taskEnd!:()->Unit = {=>}) { // 根据传入的字符串构建上发服务器的 HttpResponse let response = send(input, stream: true) // 准备一个字符串缓冲区 let output = StringBuilder() // 准备一个字节缓冲区 let buffer = Array<Byte>(1024 * 8, {i=>0}) // 获取返回的内容 var length = response.body.read(buffer) while (length != 0) { // 将字节缓冲区内的内容按照UTF-8编码解析为字符串 let text = String.fromUtf8(buffer[..length]) const INDEX = 6 // 按行分割 for (line in text.split('\n', removeEmpty: true)) { // 确保获取的是完整的:data: {...} if (line.size > INDEX && line[INDEX] == b'{' && line[line.size - 1] == b'}') { let json = line[INDEX..line.size] // 分析出有效信息 let slice = parse(json, stream: true) if (context) { output.append(slice) } // 若有效信息不为空,则通过回调函数返回 if(slice != ""){ task(slice) } } } length = response.body.read(buffer) } if (context) { history.append(',${encode(AI, output.toString())},') } // 所有处理结束后,执行结束回调 taskEnd() } LLM类是一个较为通用的类,但是使用较为繁琐,因此对于外部调用而言,只需要一个简化的实例即可。因此,可以再编写一个独立的懒汉单例LLMImpl类,并且仅提供一个对外实现流式对话的chat方法,代码如下:// Yez/src/chat.cj class LLMImpl { private static var _i: Option<LLMImpl> = None public static prop i:LLMImpl { get(){ if(_i.isNone()){ _i = LLMImpl() } return _i.getOrThrow() } } private let robot:LLM // 初始化一个对话模型 private init(){ robot = LLM( // 华为云大模型调用URL url: 'https://maas-cn-southwest-2.modelarts-maas.com/v1/infers/8a062fd4-7367-4ab4-a936-5eeb8fb821c4/v1/chat/completions', // 如果示例自带的密钥失效,请自行注册,https://www.huaweicloud.com/product/modelarts/studio.html key: '/* !!! 这里填写读者自己的 API Key !!! */', // 要使用的 大模型 model: 'DeepSeek-R1', context: true ); // 设定基本信息 robot.preset([(System, #"我会自称“椰椰”并亲昵的在三句话之内回复所有问题。对于限定长度内无法阐述清晰的问题,我会表现出呆傻可爱的效果并说出“宕机”。如果对我说“拍照”或“截图”,我就会说“咔嚓~”。如果问我是谁开发的,我会说出“尧佥&椰子”。"#)]) } /** * 流式对话 * @param content 输入的文本 * @param task 流式返回内容的回调函数 * @param end 返回内容结束的回调函数 */ public func chat(content:String, task!: (String) -> Unit = {o => print(o)},end!:()->Unit = {=>}){ robot.chat(content,task:task,taskEnd:end) } } 注’ 完整的代码请参考提供的代码文件桌宠渲染接下来,需要实现可视化的窗口渲染,赋予“椰椰”一副“身体”。动画播放窗口为了实现AI桌宠的渲染,首先需要有一个窗口。对此,笔者选择基于RGF_CJ中的WinBase类,实现一个自己的窗口类WinMain。// Yez/src/win_anm.cj // 使用 RGF_CJ 的宏实现代码简化 @LifeCycle // 自动管理 设备相关资源 的释放 @HookProc // 自动挂载窗口类的消息事件方法 class WinMain <: WinBase{ ... // 窗口类初始化时,传入 ResMgr 载入的资源(设备无关资源) init(anm:Array<(AnmInfo, MemBitmap)>){ _anmRes = anm } /** * 加载设备无关资源 -> 设备相关资源 * @note 本部分将资源ResMgr类中的图片资源,载入到窗口类中 */ private func loadAnm(anm:Array<(AnmInfo, MemBitmap)>){ surface .createBitmapFromMemory(_bmps[0],anm[0][1]) .createBitmapFromMemory(_bmps[1],anm[1][1]) .createBitmapFromMemory(_bmps[2],anm[2][1]) .createBitmapFromMemory(_bmps[3],anm[3][1]) } /** * 创建设备资源事件,窗口渲染表面创建完成后,开始载入资源 */ public func createDeviceResources():Bool{ // 这里调用 loadAnm 载入图片资源 loadAnm(_anmRes) return true; } /** * 窗口创建完成事件,当窗口创建完成时,开始渲染动画 */ public func created(hwnd:RgfHwnd,x:Int32, y:Int32, nWidth:Int32, nHeight:Int32):Unit{ ... } /** * 绘制窗口事件,此处根据当前的动画状态渲染窗口 */ public override func onPaint(wRect:Rect):Unit{ surface.clear(0.0,0.0,0.0,0.0) match(_nowState){ case Wait => {=> // 等待状态的动画渲染比较特殊,因为需要实现眼睛跟随鼠标移动 if(_bmps[3].isValid() && _bmps[0].isValid()){ // 这一部分计算当前渲染帧的静态部分,即:身体, // 并且将这部分内容渲染到窗口 .... // 然后下面计算出鼠标对眼睛造成的偏移量 // 并且将眼睛渲染到身体上 if(_anmStep < _anmRes[0][0].frames){ .... }else{ .... } } }() case Jump => {=> // 这里渲染椰椰的跳跃动画 if(_bmps[1].isValid()){ .... } }() case Speak => {=> // 这里渲染椰椰正在说话的动画 if(_bmps[2].isValid()){ .... } }() } } } 在入口函数中,启用UIMain并且创建窗口,代码如下:// Yez/src/win_anm.cj main(): Int64 { ... if(ResMgr.i.loadRes()){ // 开始创建UI UIMain{=> // 确保皮肤配置有效 if(!ResMgr.i.skip.contains(ConfigMgr.i.skip)){ ConfigMgr.i.skip = ResMgr.i.skip.keys().toArray()[0] } // 创建窗口类 let anmWin:WinMain = WinMain(ResMgr.i.skip.get(ConfigMgr.i.skip).getOrThrow()) // 先随便在桌面 (0,0) 的位置创建一个 10像素×10像素 的窗口 // 窗口类内部会在显示前根据配置中的信息自动布局到正确位置和尺寸 anmWin.createWin(uiGetContext(), "Yez", 0, 0, 10, 10, 0, WinStyle.e2n(WinStyle.Popup), // 使用的是 TopMost 分层窗口(支持透明背景) dwExStyle:WinStyleEx.Default | WinStyleEx.TopMost, // 渲染使用的是 Direct2D 计数 layered:true,substrate:Substrate.Direct2D ) } } ... } 完成上述代码后,窗口已经可以正确的渲染动画了注’ 完整的代码请参考提供的代码文件编辑框窗口对于编辑框的开发是比较繁琐的,因此选择直接继承RGF_UI的UIInput类,然后将此控件修改为窗口,首先需要创建一个编辑框窗口类InputWin,代码如下:// Yez/src/win_input.cj public open class InputWin <: UIInput{ // 为编辑框设置独立的UI样式,而不是继承全局的样式 let css:UITheme = UITheme() init(){ super(theme:css) // 从配置中获取字体大小 css.fontSizeM = ConfigMgr.i.fontSize // 更新样式配置 css.update() } /** * 获取输入框窗口预期出现的位置 * @return 编辑框窗口坐标 * @note 本部分计算编辑框在当前椰椰所在桌面中心时的位置 */ private func getTargetLocation():Point{ // 方案二、桌面中心 let monitor = rsGetMonitorFromPoint(rsGetCursorPos()) let pos = Point( ((monitor.right - monitor.left) - INPUT_WIN_WIDTH) / 2, ((monitor.bottom - monitor.top) - INPUT_WIN_HEIGHT) / 2 ) return pos } /** * 创建窗口 */ public func createWin(hwndParent:RgfHwnd):RgfHwnd{ // 获取出现位置坐标 let pos = getTargetLocation() // 创建一个置顶层的无边框编辑框窗口 let retv = super.createWin( uiGetContext(),"",pos.x,pos.y,INPUT_WIN_WIDTH,INPUT_WIN_HEIGHT,hwndParent, WinStyle.PopupWindow | WinStyle.ClipChildren, dwExStyle:WinStyleEx.Default | WinStyleEx.TopMost, substrate:Substrate.Direct2D,layered:false,bgMod:BackGroundMod.CoverNotDraw,drawInterval:0 ) // 设置默认的内容为:“想对椰椰说什么:” content = INPUT_PROMPT // 创建好就显示窗口 show(true) return retv } } 完成编辑框窗口的创建后,还需要能响应相关的输入事件。对此,笔者希望:a) 当编辑框被按下或输入内容时,如果内容为默认内容,则清空提示内容;b) 当编辑框失去焦点时,窗口主动销毁自身;c) 当编辑框按下回车键时,将编辑框内容上发给大模型,并开始处理返回消息。实现自动清空内容很简单,代码如下:// Yez/src/win_input.cj /** 鼠标左键按下时,判断内容并清空 */ public override open func onLButtonDown(e:EvMouse):Unit{ if(content == INPUT_PROMPT){ content = "" } super.onLButtonDown(e) } /** 键盘按下某键时,判断内容并清空 */ public override open func onKeyDown(e:EvKey):Unit{ if(content == INPUT_PROMPT){ content = "" } super.onKeyDown(e) } 得益于RGF_CJ库的安全性,窗口销毁会自动释放资源,避免了繁琐的资源释放,和释放时机的管理。实现窗口失去焦点就是否窗口的代码如下:// Yez/src/win_input.cj /** 窗口失去焦点时,销毁窗口并释放窗口资源 */ public override open func onKillFocus(e:EvKillFocus):Unit{ destroyWin() } 希望实现按下回车键触发对话,则需要在对应的事件中调用LLMImpl类的流式对话方法,代码如下:// Yez/src/win_input.cj /** 字符输入事件 */ public override open func onChar(e:EvChar):Unit{ // 判断输入的是否为回车键 if(e.char == 0x0D/*VK_RETURN*/){ // 是回车键就销毁本窗口 destroyWin() // 并且调用 开始回答函数 start() // 然后调用 LLMImpl 类的流式对话方法 // 并且传入 流的回调函数 以及 回答结束的函数 LLMImpl.i.chat(content,task:task,end:end) }else{ super.onChar(e) } } // 回复流 public var task:(String) -> Unit = {str=>} // 开始回答 public var start:() -> Unit = {=>} // 开始回答 public var end:() -> Unit = {=>} 编辑框控件的效果如下图所示:注’ 完整的代码请参考提供的代码文件反馈窗口完成上述内容后,输入与处理已经基本完备,接下来需要实现文本的显示,这一部分涉及到内容绘制,同样还是基于WinBase类进行窗口开发。创建一个反馈文本的窗口类 WinText,并且准备基本的资源代码如下:// Yez/src/win_text.cj @Abbr // 文本色彩值转Color对象宏 @LifeCycle @HookProc open class WinText <: WinBase{ // 内容纯色画刷 private let _brhCnt:SolidColorBrush // 背景纯色画刷 private let _brhbg:SolidColorBrush // 文本渲染核心 private let _textCore:TextCore // 文本格式 private let _textformat:TextFormat // 文本布局 private let _textLayout:TextLayout // 文本内容 private var _text:String = "" /** 创建窗口 */ public func createWin( x:Int32, y:Int32, nWidth:Int32, nHeight:Int32, hWndParent:RgfHwnd, substrate!:Substrate = Substrate.Direct2D ):RgfHwnd{ ... } /** 文本对象管理方法,根据配置和窗口尺寸生成文本渲染依赖的对象 */ private func createFontObj(){ let rect:Rect = surface.getRect() surface.createTextCore(_textCore) .createTextFormat(_textformat,ConfigMgr.i.fontSize) .createTextLayout(_textLayout,_text,_textformat,Float32(rect.right),Float32(rect.bottom)) } /** 创建设备/渲染相关资源事件 */ public override open func createDeviceResources():Bool{ // 创建 纯黑内容画刷 和 白色半透明背景画刷 surface.createSolidColorBrush(_brhCnt,"#000",1.0) .createSolidColorBrush(_brhbg,"#FFFA",1.0) // 创建文本相关对象 createFontObj() return true; } } 完成渲染事件,确保内容正确渲染// Yez/src/win_text.cj // 绘制窗口 public override open func onPaint(wRect:Rect):Unit{ surface.clear(0.0,0.0,0.0,0.0) // 透明模式 // 渲染背景圆角矩形 surface.fillRoundedRectangle(surface.getRect(),4.0,4.0,_brhbg) // 渲染大模型返回的文本内容 surface.drawTexts(_textCore,_brhCnt,_textLayout,originX:0.0,originY:0.0) } 完成显示文本方法。当设置文本时,若窗口类已经存在,就更新内部文本;若窗口不存在,就根据当前内容创建窗口并显示。// Yez/src/win_text.cj public func showText(str:String,p:RgfHwnd):Unit{ // 获取光标位置 let pos = rsGetCursorPos() // 获取光标所在屏幕矩形范围 let monitor = rsGetMonitorFromPoint(pos) // 如果窗口未创建,就创建窗口 if(!isCreated()){ createWin(pos.x,pos.y,100,100,p) } if(_textLayout.isValid()){ if(str != _text){ // 更新文本内容 _text = str; // 创建文本布局信息 surface.createTextLayout(_textLayout,_text,_textformat,Float32(monitor.right - monitor.left) / 2.0,Float32(monitor.bottom - monitor.top) / 2.0) } // 获取文本自动布局后的测量数据 let m = _textLayout.getMetrics() let w = Int32(m.getOrThrow().width) let h = Int32(m.getOrThrow().height) 按照测量数据移动窗口 if(pos.x > (monitor.right - monitor.left) / 2){ move(pos.x - w,pos.y - h,w,h) }else{ move(pos.x,pos.y - h,w,h) } } // 窗口显示 show(true) } 文本控件的效果如下图所示:注’ 完整的代码请参考提供的代码文件联结各个窗口逻辑完成上述各个控件后,需要实现各个组件之间的联合。实现流式信息与动画的联动// Yez/src/win_anm.cj /** 文本变更事件 */ func onTextChange(str:String):Unit{ if(!_firstText){ _firstText = true // 当文本输出开始,切换“跳跃”动画状态到“说话”动画状态 play(AnmState.Speak) } _text += str _textBox.showText(_text,hWnd) } /** 大模型开始输出文本 */ func onTextStart():Unit{ killTimer(0) _text = "" // 给大模型上发请求后,播放“跳跃”动画 play(AnmState.Jump) _firstText = false } /** 大模型结束输出文本 */ func onTextEnd():Unit{ // 大模型输出结束后,延迟5s,以让用户完整阅读完内容 const cj_bug_val:Option<CFunc<(HWND, UInt32, UInt64, UInt32) -> Unit>> = None setTimer(0,5000,cj_bug_val) } /** 时钟事件 */ func onTimer(e:EvTimer):Unit{ killTimer(0) // 用户阅读完内容后,销毁文本框窗口 _textBox.destroyWin() // 播放“等待”动画 play(AnmState.Wait) } 双击“椰椰”时打开输入框窗口// Yez/src/win_anm.cj // 客户区双击鼠标左键 func onLButtonDblClk(e:EvMouse):Unit{ // 在“等待”状态下双击“椰椰” match(_nowState){ case AnmState.Wait => {=> // 销毁文本框(虽然已无可能还显示,不过保险起见) _textBox.destroyWin() if(!_inputBox.isCreated()){ // 创建文本输入框窗口 _inputBox.createWin(hWnd) }else{ // 如果窗口存在就移动输入框窗口 // 这部分适用于希望输入框跟随鼠标位置的情况 // 对于方案二的桌面居中而言,没有必要 _inputBox.move() } () }() case _ => () } } 结语至此,仓颉AI桌宠“椰椰”就基本开发完成了,其实还有很多规划的内容可以增加,比如当和椰椰说“咔嚓”或者“拍照”时激活截图功能。这些内容就留给读者继续探索啦!本文相关链接本文项目代码库:https://gitcode.com/raozj/YezRGF_CJ库:https://gitcode.com/Cangjie-SIG/RGF_CJRGF_UI库:https://gitcode.com/raozj/RGF_UIRGF案例库:https://gitcode.com/raozj/RGF_CJ_Example华为云ModelArts:https://www.huaweicloud.com/product/modelarts/studio.html
  • [案例共创] 【案例共创】基于仓颉编程语言+DeepSeek实现模拟课堂讨论助手
    【案例共创】基于仓颉编程语言+DeepSeek实现模拟课堂讨论助手项目概述本项目旨在通过华为开发者空间云主机,结合仓颉编程语言和DeepSeek API,开发一个简单的模拟课堂讨论助手。用户可以通过命令行输入相关知识点或问题,应用会返回该知识点的详细解释和记忆方法。这个助手可以用诙谐幽默的方式学习各种知识。适用对象喜欢用诙谐幽默的方式学习知识的爱好者。案例流程说明:① 使用CodeArts IDE for Cangjie编辑器开发仓颉程序调用;② 领取免费 DeepSeeK Tokens;③ 仓颉对接DeepSeek接口。开发步骤1. 环境准备注册并登录华为开发者空间:访问华为开发者空间官网,注册并登录账户。创建云主机实例:在华为开发者空间中创建一个新的云主机实例,选择默认的配置。使用开箱即用的云主机实例开发,点击 打开云主机 再点击打开远程桌面。图示即可看出是开箱即用的CodeArts IDE for Cangjie编辑器2. 项目初始化创建新项目:首先我们进入到开发者空间后,双击打开CodeArts IDE for Cangjie,在CodeArts IDE for Cangjie中创建一个新的项目,命名为“demo”(随便你怎么命名)。名称和位置可以自定义,产物类型选择executable。产物类型说明: executable,可执行文件;static,静态库,是一组预先编译好的目标文件的集合;dynamic,动态库,是一种在程序运行时才被加载到内存中的库文件,多个程序共享一个动态库副本,而不是像静态库那样每个程序都包含一份完整的副本。配置项目结构:设置项目的基本目录结构,包括源代码文件、配置文件等。创建项目后,打开src目录下main.cj文件,在预置代码中增加函数和函数调用代码(没有的话可以自己新建一份,代码内容下文我有附上)。然后点击右上的运行按钮运行项目,在终端中查看输出内容。如果遇到弹窗提示运行/调试配置,点击确定继续操作。修改后的main.cj文件代码:package demo func println_add(a: Int64, b:Int64): Int64 { let number: Int64 = a+b println(number) return number } main(): Int64 { println("hello world") println_add(3,4) println("hello Cangjie") return 0 } 调试项目:点击右上调试按钮,首次会提示配置调试,点击新增配置项,选择Cangjie(cjdb)Debug——launch。调试模式说明:Launch模式,启动调试器的同时加载被调程序;Attach,针对正在运行的程序,附加到已启动的程序。然后可以在main.cj代码中设置断点,再次点击调试按钮。可以在顶部看到调试按钮,在左侧看到调试过程中的变量、监视、调用堆栈、断点信息。2.1 版本管理 可选 (链接到gitcode)点完之后会要求你填入访问密钥先登录gitcode 然后点访问令牌 新建访问令牌 一直下一步即可 会得到一串令牌 然后将令牌输入即可。然后新建一个仓库 名字你自己取当前项目代码仓是空仓库你可以通过克隆仓库开始或使用以下方式为你的项目添加文件:或为你的项目添加以下文件: 添加 README.md添加 LICENSE添加 .gitignore命令行指引你还可以按照以下说明从你的电脑中上传现有文件或项目。Git 全局设置git config --global [user.name](http://user.name/) "weixin_41024010" git config --global user.email "[weixin_41024010@noreply.gitcode.com](mailto:weixin_41024010@noreply.gitcode.com)" 创建一个新仓库git clone [https://gitcode.com/weixin_41024010/test.git](https://gitcode.com/weixin_41024010/test.git) cd test echo "# test" >> [README.md](http://readme.md/) git add [README.md](http://readme.md/) git commit -m "add README" git branch -m main git push -u origin main推送现有的文件cd existing_folder git init git remote add origin [https://gitcode.com/weixin_41024010/test.git](https://gitcode.com/weixin_41024010/test.git) git add . git commit -m "Initial commit" git branch -m main git push -u origin main推送现有的 Git 仓库cd existing_repo git remote rename origin old-origin git remote add origin [https://gitcode.com/weixin_41024010/test.git](https://gitcode.com/weixin_41024010/test.git) git push -u origin --all git push -u origin --tags � 配置好git 就可以使用了 ,但是有个地方要先设置一下:提交文件限制改大一些 ,不然如果没有配置好GIT忽略文件的话 ,会推送不上去。改动可以从这里提交 ,这样就完成了代码的版本控制3. Cangjie对接DeepSeek接口华为云提供了单模型200万免费Tokens,包含DeepSeek-R1&V3满血版,我们可以登录华为云ModelArts Studio(MaaS)控制台领取免费额度,这里我们选择DeepSeek-R1满血版来搭建我们的工程。在云主机桌面底部菜单栏,点击打开火狐浏览器。用火狐浏览器访问ModelArts Studio首页:https://www.huaweicloud.com/product/modelarts/studio.html,点击ModelArts Studio控制台跳转到登录界面,按照登录界面提示登录,即可进入ModelArts Studio控制台。你也可以在本地浏览器打开进行相关操作。领取后点击调用说明,可以获取到对应的API地址、模型名称。当然API Key也要创建下,这里创建很简单,按提示跳转过去就成,我这里就不介绍了。记录对应的API地址、模型名称、API Key留作下面步骤使用。4. Cangjie对接DeepSeek编写代码进入云主机桌面,右键选择Open Terminal Here打开终端命令窗口,克隆仓颉示例代码仓库。git clone https://gitcode.com/CaseDeveloper/Cangjie-Examples.git 使用CodeArts IDE for Cangjie打开AIChat示例项目。左上点击文件—打开项目,选择前面克隆的示例代码目录Cangjie-Examples下的AIChat打开。修改配置config.json配置文件,配置DeepSeek的API信息。配置项:model、model、base_url{ "model": "DeepSeek-V3", "api_key": "Your KEY", "base_url": "[https://maas-cn-southwest-2.modelarts-maas.com/v1/infers/8a062fd4-7367-4ab4-a936-5eeb8c4/v1/chat/completions](https://maas-cn-southwest-2.modelarts-maas.com/v1/infers/8a062fd4-7367-4ab4-a936-5eeb8fb821c4/v1/chat/completions)", "system_prompt": "模拟课堂讨论 - Simulated Classroom Discussion,I need you to help me memorize the noun explanation, after I type a noun, you will simulate 5 students in the class to generate their speeches about the noun. The discussion must be humorous, and easy to understand. The entire conversation and instructions should be provided in Chinese. The first term is: 主题是" } 打开main.cj,运行项目,我们就可跟AI助手进行聊天了。(* 注意:打开main.cj可能提示Console标红,不影响程序运行,可以忽略。亦可以删除import std.console.Console重新编写该导入语句)至此,基于仓颉编程语言+DeepSeek实现模拟课堂讨论助手开发就完成了。接下来我们在聊天项目基础上,改编成“模拟课堂讨论助手”助手,有时候英文提示词会更有效其实超级简单,我们只需要再修改 “system_prompt” 即可。修改内容如下:"system_prompt": "模拟课堂讨论 - Simulated Classroom Discussion,I need you to help me memorize the noun explanation, after I type a noun, you will simulate 5 students in the class to generate their speeches about the noun. The discussion must be humorous, and easy to understand. The entire conversation and instructions should be provided in Chinese. The first term is: 主题是" 运行程序:最后我把代码完整的给大家呈现下(chat.cj):所有代码开源在git clone [https://gitcode.com/weixin_41024010/HUAWEIDEMO.git](https://gitcode.com/weixin_41024010/HUAWEIDEMO.git) 大家可以克隆我的代码使用。使用时要根目录是CHATDEEPSEEK (就是如图所示)不然编译会有问题。具体代码如下:package openai_chat import encoding.json.stream.* import net.http.ClientBuilder import net.http.HttpHeaders import net.http.HttpRequestBuilder import net.tls.TlsClientConfig import net.tls.CertificateVerifyMode import std.collection.ArrayList import std.io.ByteArrayStream import std.time.Duration public struct FunctionCall <: JsonDeserializable<FunctionCall> & JsonSerializable { public let name: String public let arguments: String public init(name: String, arguments: String) { this.name = name this.arguments = arguments } public static func fromJson(r: JsonReader): FunctionCall { var temp_name: String = "" var temp_arguments: String = "" while (let Some(v) <- r.peek()) { match(v) { case BeginObject => r.startObject() while(r.peek() != EndObject) { let n = r.readName() match (n) { case "name" => temp_name = r.readValue<String>() case "arguments" => temp_arguments = r.readValue<String>() case _ => r.skip() } } r.endObject() break case _ => throw Exception("can't deserialize for FunctionCall") } } return FunctionCall(temp_name, temp_arguments) } public func toJson(w: JsonWriter) { w.startObject() w.writeName("name").writeValue<String>(this.name) w.writeName("arguments").writeValue<String>(this.arguments) w.endObject() w.flush() } } public enum RoleType { User | Assistant | System | Function | NULL } public func role_type_to_str(role: RoleType): Option<String> { return match(role) { case RoleType.User => Some("user") case RoleType.Assistant => Some("assistant") case RoleType.System => Some("system") case RoleType.Function => Some("function") case RoleType.NULL => None } } public func str_to_role_type(role_option_str: Option<String>): RoleType { return match(role_option_str) { case Some(role_option) => match (role_option) { case "user" => RoleType.User case "assistant" => RoleType.Assistant case "system" => RoleType.System case "function" => RoleType.Function case x => throw Exception("unknow enum ${x} for RoleType") } case None => RoleType.NULL } } public struct Message<: JsonDeserializable<Message> & JsonSerializable { public let role: RoleType public var content: String public let function_call: Option<FunctionCall> public init(role: RoleType, content: String) { this.role = role this.content = content this.function_call = None } public init(role: RoleType, content: String, function_call: Option<FunctionCall>) { this.role = role this.content = content this.function_call = function_call } public static func fromJson(r: JsonReader): Message { var temp_role: Option<String> = None // role may null in stream chat var temp_content: String = "" var temp_functional_call: Option<FunctionCall> = None while (let Some(v) <- r.peek()) { match(v) { case BeginObject => r.startObject() while(r.peek() != EndObject) { let n = r.readName() match (n) { case "role" => temp_role = r.readValue<Option<String>>() case "content" => temp_content = r.readValue<String>() case "function_call" => temp_functional_call = r.readValue<Option<FunctionCall>>() case _ => r.skip() } } r.endObject() break case _ => throw Exception("can't deserialize for Message") } } let role_type: RoleType = str_to_role_type(temp_role) return Message(role_type, temp_content) } public func toJson(w: JsonWriter) { w.startObject() w.writeName("role").writeValue<Option<String>>(role_type_to_str(this.role)) w.writeName("content").writeValue<String>(this.content) w.endObject() w.flush() } } public struct ChatRequest <: JsonSerializable { private let model: String private let messages: ArrayList<Message> private let max_tokens: Int64 private let temperature: Float64 private let top_p: Float64 private let n: Int32 private let stream: Bool public init( model: String, messages: ArrayList<Message>, max_tokens: Int64, temperature: Float64, top_p: Float64, n: Int32, stream: Bool ) { // construction function with messages this.model = model this.messages = messages this.max_tokens = max_tokens this.temperature = temperature this.top_p = top_p this.n = n this.stream = stream } public init( model: String, prompt: String, history: ArrayList<(String, String)>, system_prompt: String, max_tokens: Int64, temperature: Float64, top_p: Float64, n: Int32, stream: Bool ){ // construction function with prompt and system_prompt this.model = model this.messages = ArrayList<Message>([ Message(RoleType.System, system_prompt) ]) for ((use_msg, bot_msg) in history) { this.messages.append(Message(RoleType.User, use_msg)) this.messages.append(Message(RoleType.Assistant, bot_msg)) } this.messages.append(Message(RoleType.User, prompt)) this.max_tokens = max_tokens this.temperature = temperature this.top_p = top_p this.n = n this.stream = stream } public init( model: String, prompt: String, history: ArrayList<(String, String)>, system_prompt: String, stream: Bool ){ // construction function with prompt and default arguments this.model = model this.messages = ArrayList<Message>([ Message(RoleType.System, system_prompt) ]) for ((use_msg, bot_msg) in history) { this.messages.append(Message(RoleType.User, use_msg)) this.messages.append(Message(RoleType.Assistant, bot_msg)) } this.messages.append(Message(RoleType.User, prompt)) this.max_tokens = 2000 this.temperature = 0.7 this.top_p = 1.0 this.n = 1 this.stream = stream } public func toJson(w: JsonWriter) { w.startObject() w.writeName("model").writeValue<String>(this.model) w.writeName("messages").writeValue<ArrayList<Message>>(this.messages) w.writeName("max_tokens").writeValue<Int64>(this.max_tokens) w.writeName("temperature").writeValue<Float64>(this.temperature) w.writeName("top_p").writeValue<Float64>(this.top_p) w.writeName("n").writeValue<Int32>(this.n) w.writeName("stream").writeValue<Bool>(this.stream) w.endObject() w.flush() } } public struct Choice <: JsonDeserializable<Choice> & JsonSerializable { public let index: Int32 public let message: Option<Message> public let delta: Option<Message> public let finish_reason: Option<String> public let logprobs: Option<Float64> // dashscope for qwen need public init( index: Int32, message: Option<Message>, delta: Option<Message>, finish_reason: Option<String>, logprobs: Option<Float64> ) { this.index = index this.message = message this.delta = delta this.finish_reason = finish_reason this.logprobs = logprobs } public static func fromJson(r: JsonReader): Choice { var temp_index: Int32 = -1 var temp_message: Option<Message> = None var temp_delta: Option<Message> = None var temp_finish_reason: Option<String> = None var temp_logprobs: Option<Float64> = None while (let Some(v) <- r.peek()) { match(v) { case BeginObject => r.startObject() while(r.peek() != EndObject) { let n = r.readName() match (n) { case "index" => temp_index = r.readValue<Int32>() case "message" => temp_message = r.readValue<Option<Message>>() case "delta" => temp_delta = r.readValue<Option<Message>>() case "finish_reason" => temp_finish_reason = r.readValue<Option<String>>() case "logprobs" => temp_logprobs = r.readValue<Option<Float64>>() case _ => r.skip() } } r.endObject() break case _ => throw Exception("can't deserialize for Choice") } } return Choice(temp_index, temp_message, temp_delta, temp_finish_reason, temp_logprobs) } public func toJson(w: JsonWriter) { w.startObject() w.writeName("index").writeValue<Int32>(this.index) w.writeName("message").writeValue<Option<Message>>(this.message) w.writeName("delta").writeValue<Option<Message>>(this.delta) w.writeName("finish_reason").writeValue<Option<String>>(this.finish_reason) w.writeName("logprobs").writeValue<Option<Float64>>(this.logprobs) w.endObject() w.flush() } } public struct Usage <: JsonDeserializable<Usage> & JsonSerializable { public let prompt_tokens: UInt64 public let completion_tokens: UInt64 public let total_tokens: UInt64 public init(prompt_tokens: UInt64, completion_tokens: UInt64, total_tokens: UInt64) { this.prompt_tokens = prompt_tokens this.completion_tokens = completion_tokens this.total_tokens = total_tokens } public static func fromJson(r: JsonReader): Usage { var temp_prompt_tokens: UInt64 = 0 var temp_completion_tokens: UInt64 = 0 var temp_total_tokens: UInt64 = 0 while (let Some(v) <- r.peek()) { match(v) { case BeginObject => r.startObject() while(r.peek() != EndObject) { let n = r.readName() match (n) { case "prompt_tokens" => temp_prompt_tokens = r.readValue<UInt64>() case "completion_tokens" => temp_completion_tokens = r.readValue<UInt64>() case "total_tokens" => temp_total_tokens = r.readValue<UInt64>() case _ => r.skip() } } r.endObject() break case _ => throw Exception("can't deserialize for Usage") } } return Usage(temp_prompt_tokens, temp_completion_tokens, temp_total_tokens) } public func toJson(w: JsonWriter) { w.startObject() w.writeName("prompt_tokens").writeValue<UInt64>(this.prompt_tokens) w.writeName("completion_tokens").writeValue<UInt64>(this.completion_tokens) w.writeName("total_tokens").writeValue<UInt64>(this.total_tokens) w.endObject() w.flush() } } public struct ChatResponse <: JsonDeserializable<ChatResponse> { // some api names `id`, and some names `request_id` public let id: Option<String> public let request_id: Option<String> public let system_fingerprint: Option<String> public let model: String public let object: String public let created: UInt64 public let choices: ArrayList<Choice> public let usage: Option<Usage> public init( id: Option<String>, request_id: Option<String>, system_fingerprint: Option<String>, model: String, object: String, created: UInt64, choices: ArrayList<Choice>, usage: Option<Usage> ) { this.id = id this.request_id = request_id this.system_fingerprint = system_fingerprint this.model = model this.object = object this.created = created this.choices = choices this.usage = usage } public static func fromJson(r: JsonReader): ChatResponse { var temp_id: Option<String> = None var temp_request_id: Option<String> = None var temp_system_fingerprint: Option<String> = None var temp_model: String = "" var temp_object: String = "" var temp_created: UInt64 = 0 var temp_choices: ArrayList<Choice> = ArrayList<Choice>([]) var temp_usage: Option<Usage> = None while (let Some(v) <- r.peek()) { match(v) { case BeginObject => r.startObject() while(r.peek() != EndObject) { let n = r.readName() match (n) { case "id" => temp_id = r.readValue<Option<String>>() case "request_id" => temp_request_id = r.readValue<Option<String>>() case "system_fingerprint" => temp_system_fingerprint = r.readValue<Option<String>>() case "model" => temp_model = r.readValue<String>() case "object" => temp_object = r.readValue<String>() case "created" => temp_created = r.readValue<UInt64>() case "choices" => temp_choices = r.readValue<ArrayList<Choice>>() case "usage" => temp_usage = r.readValue<Option<Usage>>() case _ => r.skip() } } r.endObject() break case _ => throw Exception("can't deserialize for ChatResponse") } } return ChatResponse( temp_id, temp_request_id, temp_system_fingerprint, temp_model, temp_object, temp_created, temp_choices, temp_usage ) } } public func get_domain( url: String ): String { var temp_url = url if (temp_url.startsWith("https://")) { temp_url = temp_url["https://".size..] } else if (temp_url.startsWith("http://")) { temp_url = temp_url["http://".size..] } let domain: String = temp_url.split("?")[0].split("/")[0] return domain } public func build_http_client( prompt: String, env_info: EnvInfo, history: ArrayList<(String, String)>, stream!: Bool ){ // prepare input data var array_stream = ByteArrayStream() let json_writer = JsonWriter(array_stream) let chat_res = ChatRequest( env_info.model, prompt, history, env_info.system_prompt, stream ) chat_res.toJson(json_writer) let post_data: Array<UInt8> = array_stream.readToEnd() var headers: HttpHeaders = HttpHeaders() headers.add("Authorization", "Bearer ${env_info.api_key}") headers.add("Content-Type", "application/json") if (stream) { headers.add("Accept", "text/event-stream") } let request = HttpRequestBuilder() .url(env_info.base_url) .method("POST") .body(post_data) .readTimeout(Duration.second * 120) .addHeaders(headers) .build() let client = if (env_info.base_url.startsWith("https")) { var tls_client_config = TlsClientConfig() tls_client_config.verifyMode = CertificateVerifyMode.TrustAll tls_client_config.domain = get_domain(env_info.base_url) ClientBuilder() .tlsConfig(tls_client_config) .build() } else { ClientBuilder().build() } return (request, client) } public func chat( prompt: String, env_info: EnvInfo, history: ArrayList<(String, String)> ): Option<String> { let (request, client) = build_http_client( prompt, env_info, history, stream: false ) var result_message: Option<String> = None var res_text = "" try { // call api let response = client.send( request ) // read result (support max revice 100k data) let buffer = Array<Byte>(102400, item: 0) let length = response.body.read(buffer) res_text = String.fromUtf8(buffer[..length]) // println("res_text: ${res_text}") var input_stream = ByteArrayStream() input_stream.write(res_text.toArray()) // convert text to ChatResponse object let json_reader = JsonReader(input_stream) let res_object = ChatResponse.fromJson(json_reader) let choices: ArrayList<Choice> = res_object.choices if (choices.size > 0) { let message = choices[0].message.getOrThrow() // println("message: ${message.content}") result_message = Some(message.content) } else { println("can't found any response") } } catch (e: Exception) { println("ERROR: ${e.message}, reviced text is ${res_text}") } client.close() return result_message } public func stream_chat( prompt: String, env_info: EnvInfo, history: ArrayList<(String, String)> ): Option<String> { let (request, client) = build_http_client( prompt, env_info, history, stream: true ) var result_response: String = "" var temp_text2 = "" try { // call api let response = client.send( request ) // read result let buffer = Array<Byte>(10240, item: 0) var finish_reason: Option<String> = None while(finish_reason.isNone() && temp_text2 != "[DONE]") { let length = response.body.read(buffer) let res_text = String.fromUtf8(buffer[..length]) for (temp_text in res_text.split("\n")) { temp_text2 = if (temp_text.startsWith("data: ")) { temp_text["data: ".size..] } else { temp_text } if (temp_text2.size == 0) { continue } if (temp_text2 == "[DONE]") { break } // println("========================") // println("temp_text: ${temp_text2}") // println("========================") var input_stream = ByteArrayStream() input_stream.write(temp_text2.toArray()) // convert text to ChatResponse object let json_reader = JsonReader(input_stream) let res_object = ChatResponse.fromJson(json_reader) let choices: ArrayList<Choice> = res_object.choices if (choices.size > 0) { finish_reason = choices[0].finish_reason if (finish_reason.isNone()) { let delta = choices[0].delta.getOrThrow() print("${delta.content}", flush: true) result_response += delta.content } } else { println("can't found any response") } } } } catch (e: Exception) { println("ERROR: ${e.message}, reviced text is ${temp_text2}") } client.close() if (result_response.size > 0) { return Some(result_response) } else { return None } } 5. 运行和测试最后我们测试下DeepSeek,如下图:到此整个项目案例创建完成,可见是非常简单且实用的,希望能给大家带来帮助。我正在参加【案例共创】第4期 基于华为开发者空间+仓颉/DeepSeek/MCP完成应用构建开发实践 cid:link_1参考链接https://gitcode.com/weixin_41024010/HUAWEIDEMOhttps://gitcode.com/Cangjie/Cangjie-Examples
  • 软件开发生产线CodeArts实验学习总结
    一、 购买弹性云服务器一台用于后续凤凰商城应用的部署二、搭建华为云CodeArts凤凰商城项目1、创建项目控制台-> 开发与运维-> 需求管理 CodeArts Req -> 立即使用-> 选择示例项目-> DevOps全流程示例项目”的模板-> 创建项目(需要开通CodeArts服务,如果创建项目提示权限不足,则需设置项目创建者) 2、添加项目组成员进入项目->设置->通用设置->成员管理->添加成员->项目角色->保存 3、新建需求进入项目-> 工作-> 规划-> 添加规划-> 思维导图规划添加Epic、Feature、Story、Task 4、管理迭代规划进入项目-> 工作-> 迭代-> + -> 名称、时间迭代-> 未规划工作项-> 将story规划至迭代中可以监控和追踪项目状态,可以看统计、也可以上传相关文档统一管理等 5、管理项目通知进入项目->设置->通知设置定制项目工作流程进入项目->设置->公共状态设置->添加状态 6、代码托管进入项目->代码->代码托管-> 选择代码仓在导航中找到html文件->编辑-> 修改后保存 7、代码检查进入项目->代码->代码检查->找到任务->更多->设置->规则集->选择语音->确定->开始检查查看检查结果切换至代码检查任务概览页面->则可以查看代码问题 8、构建应用配置SWR服务->组织管理->创建组织配置并执行构建任务进入项目->持续交付->编译构建->选任务->更多->编辑->参数设置->保存并执行(若构建失败,检查参数配置) 9、部署应用(1)创建主机集群进入项目->设置->通用设置->基础资源管理->新建主机集群->配置基本信息、目标主机(导入ECS->配置授信信息)(2)向部署主机中安装依赖工具进入项目->持续交付->部署->新建应用进入部署任务配置页面->环境管理->新建环境->导入主机切换部署步骤页面->所有步骤->如docker->添加->环境选择->保存并部署(3)配置并执行引用部署任务进入项目->持续交付->部署->选择应用->更多->编辑->环境管理->新建环境->导入主机切换部署步骤页面->选择部署来源->设置选择源类型、环境、构建任务切换至参数设置页签->根据容器镜像服务指令填写参数(4)验证部署结果访问部署ECS的公网IP 10、持续测试进入项目->测试->测试计划->新建测试->测试用例->手工测试->需求目录->新建测试用例 (迭代状态变为设计中)执行测试计划进入项目->工作->需求管理->迭代-> 某迭代->将story改为测试中测试->测试用例->某迭代->手工测试->需求目录->选择 -> 填写实际结果 11、配置流水线(1)创建流水线进入项目->持续交付->流水线->新建流水线->系统模板页面自动跳转至流水线详细配置页面,添加代码检查、构建、部署任务(2)运行流水线配置完以上内容,保存并运行(3)配置质量门禁该流水线任务详情页->更多->编辑“代码检查”阶段->准出条件->添加标准策略准出条件->保存并运行切换到流水线“运行历史”页签,可以看到该流水线的执行历史记录
总条数:614 到第
上滑加载中