1. 我已经把电源计划选成卓越性能了,这里面硬盘是不会睡眠
  2. 我 BIOS 也改了一个配置,但是记不清了.

我为什么这么说呢?
我有一个脚本,第一运行是 8S,第二次是 0.3S,再次相差这么大只能是硬盘休眠了吧,不可能是其他原因吧.

我一般用阿里云和清华,还有其他高校镜像站没列出来,可以在下面的高校镜像站查看。

阿里云:
镜像下载、软件源、DNS 服务器、NTP 服务器
https://developer.aliyun.com/mirror/
公网访问地址
https://mirrors.aliyun.com/
ECS VPC 网络访问地址
http://mirrors.cloud.aliyuncs.com/
ECS 经典网络访问地址
http://mirrors.aliyuncs.com/

腾讯云:
https://mirrors.tencent.com/
公网访问地址:
http://mirrors.tencent.com/
内网访问地址:
http://mirrors.tencentyun.com/

华为云:
https://mirrors.huaweicloud.com/home

网易:
https://mirrors.163.com/

搜狐:
http://mirrors.sohu.com/

清华:
系统镜像、软件源、应用软件下载、字体下载
https://mirrors.tuna.tsinghua.edu.cn/
https://mirrors.tuna.tsinghua.edu.cn 自动选择
https://mirrors6.tuna.tsinghua.edu.cn 只解析 IPv6
https://mirrors4.tuna.tsinghua.edu.cn 只解析 IPv4

中科大:
系统镜像、软件源、软件下载
https://mirrors.ustc.edu.cn/
mirrors.ustc.edu.cn 自动解析
ipv4.mirrors.ustc.edu.cn IPv4 线路
ipv6.mirrors.ustc.edu.cn IPv6 线路
cernet.mirrors.ustc.edu.cn 教育网线路
chinanet.mirrors.ustc.edu.cn 电信线路
unicom.mirrors.ustc.edu.cn 联通线路
cmcc.mirrors.ustc.edu.cn 移动线路
rsync.mirrors.ustc.edu.cn Rsync 线路

北大:
https://mirrors.pku.edu.cn/

浙大:
https://mirrors.zju.edu.cn/

上交大:
https://ftp.sjtu.edu.cn/

高校镜像:
https://mirrors.cernet.edu.cn/site

阿里云 Maven:
https://maven.aliyun.com/

飞致云(凌霞软件)docker:
https://docker.1panel.live
说明: https://bbs.fit2cloud.com/t/topic/5886

七牛云 golang:
https://goproxy.cn/

序言

入侵排查:简单理解是针对权限维持技术的排查。
权限维持技术:攻击者入侵系统(Getshell)后,为了防止失去权限而通过持久化配置项保持权限的手段,包括注册表、计划任务、后面恶意账户等等。

基本的信息收集

1、收集系统主机的版本,右键此电脑->属性或者win+R->输入winver


会看到操作系统的版本,为什么要收集系统版本呢?因为根据版本就可以找到这个版本下的漏洞,如权限提升漏洞,可以根据提权漏洞的特点进行排查,如特点的日志等。
2、收集操作系统的其他信息,win+R->输入msinfo32

其中重点关注环境变量,另一种查看环境变量的方法是打开命令行输入set


3、关注环境变量的原因:环境变量劫持:我们都知道,命令行执行一条命令其实是执行一个.exe文件,而如果执行的.exe文件对应的命令不在当前目录,就会出现一种优先级,就是操作系统会找环境变量中指定的.exe文件,然后执行,而如果既不在目录,又不在环境变量中,就会提示不是内部命令。这样一来,想象一个场景,如果内部人员告诉你,自己执行ipconfig命令时,没有正常输出内容或者有安全设备提示有恶意外连等等时,你想拿正常的ipconfig.exe程序进行恶意文件分析,但是没有任何问题,就该注意是否有环境变量劫持了。这里简单做个实验,在环境变量中配置木马文件,

执行文件,并未出现正常内容,

并且C2中成功上线,

这里仅仅是简单的举例,这个例子很容易被发现异常,更隐蔽的做法是,也可以通过将正常的文件路径和木马路径绑在一起,效果就是正常程序和木马都运行了,那我们就可能在这一块儿很难发现恶意程序了。
4、网络连接信息收集,主要的命令netstat -ano、ipconfig /all、netstat -rn等等

这里重点关注分析状态为ESTABLISHED的IP地址,也可以借助威胁分析平台。netstat -rn是网络的路由信息

5、进程排查,主要方法是命令tasklist /v,可以看到进程的列表,

也可以在任务管理器->详细信息查看正在运行的进程

还有命令wmic process get * /value,格式化输出

这里比较重要的是CommandLine,它显示的是执行这个程序时所输入的完整命令,比如这个

为什么需要关注这个呢?我们知道,在C2工具生成有效载荷时,会有PowerShell Command,



执行成功后上线,

在通过wmic process get * /value查看,就能发现恶意进程。

这里也可以用条件表达式,直接过滤获取想看的可疑进程。

恶意用户排查

1、恶意用户:使用命令net user可以进行查看用户等等操作,这是添加了一个用户

我们也可以将用户进行隐藏,就是在用户后面加一个$符号,我们发现使用net user查询后并没有发现这个用户的信息,就是被隐藏了

但是这种方法对应其它命令和图形化注销时都能看到,因此这种隐藏用户还是比较鸡肋的。所以一些攻击者就会创建一些特定情况下存在的克隆账户,比如在域环境中存在hrbtgt用户,可能反而不会引起运维人员的关注。
2、排查:命令wmic useraccount get * /value,这个会显示所有用户的详细信息,包括刚刚创建的隐藏用户

另外通过注册表也可以看到用户,包括前面 提到的克隆账户其实就是把注册表中的信息进行调换,但是其实上面这条命令已经可以解决很多问题了。

持久化排查

1、注册表下的RUN子键和RUNONCE(只运行一次)子键开机启动项:

这里的启动项是通过管理员账户进行修改,但是在任意用户用户登录时运行,换句话说就是任意用户触发登录时都会运行,举个例子,重启电脑,进行登录,看会不会有cmd.exe运行

cmd.exe自启动

上面是其中一个目录,在注册表中还有一个其他目录也存在这个子键

那么区别显而易见了,就是前者是所以用户登录时触发,后者是只有当前用户登录时触发。另外,需要说明的是前者在前面说到了,只有管理员权限才能修改写入,普通用户只能修改后面的目录的内容。
2、services.msc服务文件:服务文件比较特殊的一点是它不需要用户登录,那么也就意味着它的可利用性更强,但是,创建服务文件的启动项必须是管理员权限才能创建的,同时当攻击者获取到权限时也会获取到system的权限。

这里配合C2做个示范,在C2工具中,有专门针对服务的程序,正常运行时运行不了,只能配合服务运行

这个命令就是进行服务启动项的命令(sc create 服务名称 binpath= "自启动的程序" start="auto" obj="服务运行时的权限")


就出现这个服务了,可以看到这个服务是没有描述的,除此之外,为了更加隐蔽,攻击者可能会添加其他字段来让这个服务更加隐蔽,这里讲排查,就不深入了

接下来进行服务启动

C2上线,并且权限很高,是system权限

另外重启一下看效果,这里重启就不需要登录,重启后上线

而且未登录

3、gpedit.msc本地组策略启动项:


点开启动->添加->添加恶意程序

这个依旧不需要登录,就可以上线C2
4、计划任务:命令schtasks

创建计划任务可以用命令schtasks /create /tn "计划任务名称" /tr "计划任务的所在位置" /sc minute /mo 1,表示一分钟执行一次

另外,计划任务图形化界面也可以操作

应急响应和基线加固的工具

1、这个有很多,比如说火绒、D盾、windows安全基线核查加固助手

入侵排查

1、如果知道恶意程序的名称,可以直接去注册表搜索,持久化的东西大多数都可以用注册表搜出来,比如前面的服务文件、本地组策略文件和计划任务等等


2、那么如何找到恶意程序的名称呢?比较重要的一点就是确定时间线,我们可以在文件资源管理器中,指定日期或日期范围进行搜索

另外也可以用everything工具进行搜索,这个工具也有特定的语法可以筛选和过滤

总结

对应入侵排查而言,这些仅仅可以解决一下基础的持久化的问题,而还有很多高端的不常见的甚至从未出现过的持久化手段需要更多的深入了解和学习的。

项目介绍

JeecgBoot是一款集成AI应用的,基于BPM流程的低代码平台,旨在帮助开发者快速实现低代码开发和构建、部署个性化的 AI 应用。 前后端分离架构Ant Design&Vue3,SpringBoot,SpringCloud,Mybatis,Shiro,强大的代码生成器让前后端代码一键生成,无需写任何代码! 成套AI大模型功能: AI模型、AI应用、知识库、AI流程编排、AI对话等; 引领AI低代码开发模式, 帮助Java项目解决80%的重复工作,让开发更多关注业务,提高效率,同时又不失灵活性!

发版时间:v3.9.1 | 2026-01-28

源码下载

升级日志

本次升级对 AI 平台进行了全面增强,升级 LangChain4j 至 1.9.1,引入推理模型、多会话与流式调用能力;千问模型支持参数调整与联网搜索,新增 AI 绘画、文生图、图生图和海报生成等多模态能力;AI 应用升级为智能体,支持记忆、变量、插件、流程与 MCP;流程能力新增变量、循环、SQL、定时、知识库写入等节点;AI 聊天支持文件上传、Chat2BI 生成图表。并推出 AI 工具箱,覆盖 AI 海报、AI 简历、AI 写作、AI 生图等场景;
AI 平台升级日志
核心升级
  • LangChain4j 升级至 1.9.1
  • MCP支持http和STDIO命令类型
  • 支持推理模型,深度思考不默认开启
  • 支持流式调用接口
  • 支持多会话模式
  • 支持文件解析
大模型与多模态
  • 千问模型支持参数调整和联网搜索
  • 支持 AI 图片模型(千问 / OpenAPI)
  • 支持文生图、图生图
  • 新增claude、vl模型、千帆大模型及通义千问的支持
AI 应用
  • 新增 AI 应用门户
  • 新增提示词管理
  • AI 应用升级为智能体
  • AI 应用支持记忆、变量、插件、流程、MCP、绘画
  • AI 应用支持卡片内容
AI 流程
  • 新增节点:变量提取节点、变量聚合节点、n8n循环节点、定时触发器、SQL节点、知识库写入节点
  • 支持流程复制
  • 流程可被应用直接调用
AI 聊天与 BI
  • AI 聊天支持上传文件并解析内容
  • Chat2BI 支持 AI 聊天生成图表
  • 支持 MCP 工具调用结果展示
  • 支持卡片式内容回复
Chat2BI(AI生成图表
  • 支持多种图表类型,包括柱状图、折线图、饼图、多列柱状图、多行折线图、折柱图、面积图、雷达图、仪表盘。
  • 支持多数据源查询,在系统里配置的数据源都可以进行图表查询,若不指定数据源,则默认使用系统数据库。
  • 支持自然语言查询,用户可以通过自然语言输入查询需求,智能体会自动解析并生成相应的图表。
  • 支持已知数据生成图表,用户可以直接输入数据,智能体会根据数据生成相应的图表。
AI工具箱
  • AI 简历生成(线 Word)
  • AI 商品搜索助手
  • 新增 AI 绘画和 AI 海报生成
  • AI写作
  • OCR识别
新增应用场景案例
  • 看图说话应用
  • 商品搜索回复应用
  • 帮我写作
  • 图片识别
平台功能升级
  • 新增接口签名校验注解 @SignatureCheck
  • 下拉多选支持字典颜色显示
  • 支持部门简称功能
  • 优化桌面应用中的文件预览功能
  • 推送接口默认集成 Uniapp 手机端消息推送机制
  • 升级积木报表至 v2.3.0
  • 升级积木 BI 大屏至 v2.3.0
Online功能升级
  • 在线表单列表列宽度不能设置么?也不能在表头那里拉宽么? · Issue #9123
  • Online报表查询异常 · Issue #9213
  • Online报表左联SQL运行错误 · Issue #9220
  • 修复Online编辑时long类型字段未赋值导致的报错问题。
  • 解决SQL Server环境下,online报表包含LEFT JOIN查询时异常的兼容性问题。
  • 优化AI账号配置校验,未配置或配置错误时,点击online生成测试数据提示信息更友好。
  • 修正online自定义按钮排序功能,支持清空排序设置。
  • Online表单和列表支持字典颜色显示
  • Online表单支持列表列宽拖动调整,新增默认列宽设置
  • Online表单修复 loaded 方法隐藏字段导致只读字段变可写的问题(issues/9223)
  • Online表单修复一对一子表编辑后详情页不更新的问题
  • SysDataSourceController的queryOptions接口添加权限检查 #9288
Issues修复
  • 租户几个无法加权限的接口,默认加上“加签注解”
  • 【AI】文档库本地上传,如果上传路径写的是相对路径解析会报错
  • 【AI】当前子流程不存在时,打开页面报错,死循环了
  • AI 流程中的http请求节点,超时时间如何设置 · Issue #9118
  • V3.9.0 Oracle11g 数据库 登录提示 无效的列类型: 1111 · Issue #9145
  • 后端代码没提交,租户用户模块保存时报错,检查后发现前端调用的/sys/user/addTenantUser,但是后端没有上传这个函数,麻烦上传下后端代码 · Issue #9158
  • v3.8.3版本存在命令执行漏洞 · Issue #9144
  • 报表编辑界面新增列及查看问题 · Issue #4296
  • AiragLocalCache超时时间如何设置 · Issue #9138
  • JVxeTable中的分页,切换pageSize时,pageChange事件加载了两次 · Issue #9169
  • 地图上只能显示一个数据,能不能做成支持多个数据显示 · Issue #4298
  • 关于聊天页面内容检索后的来源问题 · Issue #8404
  • 单据添加了按钮,用代码生成工具生成的vue文件里面就报这个错,不加就没事。 · Issue #9190
  • 导出异常 · Issue #9173
  • "用于后端字典翻译",同一枚举dictCode,keys传多个也只add第1个DictModel · Issue #9124
  • 【严重安全漏洞】未授权访问+权限绕过导致任意用户可加入任意租户组织;只要是登录用户都可以实现攻击 · Issue #9196
  • ai流程设计流程变量无法取到多个值的问题 · Issue #9159
  • AI MCP 插件没法使用有header 授权的 · Issue #9175
  • ai流程编排流式输出报错 · Issue #9168
  • Ai工作流报错 · Issue #9206
  • 使用useListPage的导出异常 · Issue #9209
  • AI模块知识库存在XXE漏洞 · Issue #9204
  • BasicDrawer结合useDescription,在生产环境中Description未正确渲染 · Issue #9126
  • AI应用接收LLM返回会话已关闭 · Issue #9200
  • jvxetable的数字输入框JVxeTypes.inputNumber没法直接限制最小值、最大值、精度 · Issue #9218
  • mcp服务连接未进行关闭 · Issue #9234
  • 导出格式错误 · Issue #9237
  • 正式环境的redis不支持订阅(SUBSCRIBE)命令 · Issue #9225
  • xxl-job bug · Issue #9189
  • 当配置了pagination: true时,BasicTable组件自适应高度异常 · Issue #9217
  • GitHub · Where software is built](https://github.com/jeecgboot/JeecgBoot/issues/9223)
  • 同步钉钉部门报错 · Issue #9228
  • 在同一个行条件中,同list_multi类型的字段切换,下拉框都是第一个字典的值 · Issue #9263
  • GitHub · Where software is built https://github.com/jeecgboot/JeecgBoot/issues/9186)
  • 流程设计时,工具调用节点的参数配置无法保存参数 · Issue #3 · jeecgboot/jeecg-ai · GitHub
  • 【issues/9282】下拉搜索框设置为自定义数据字典时,生成代码后台报错 #9282
  • 前端问题-用户选择组件 选中回显问题 #9275
  • SysAnnouncementController.downLoadFiles存在潜在的路径遍历漏洞 #9303
  • AIChatHandler.buildImageContents中潜在的路径遍历漏洞 #9302

技术交流

快速启动项目

AI应用平台介绍

JeecgBoot 平台提供了一套完善的AI应用管理系统模块,是一套类似DifyAIGC应用开发平台+知识库问答,是一款基于LLM大语言模型AI应用平台和 RAG 的知识库问答系统。 其直观的界面结合了 AI 流程编排、RAG 管道、知识库管理、模型管理、对接向量库、实时运行可观察等,让您可以快速从原型到生产,拥有AI服务能力。 详细专题介绍,请点击查看

适用项目

JeecgBoot低代码平台,可以应用在任何J2EE项目的开发中,支持信创国产化。尤其适合SAAS项目、企业信息管理系统(MIS)、内部办公系统(OA)、企业资源计划系统(ERP)、客户关系管理系统(CRM)、AI知识库等,其半智能手工Merge的开发方式,可以显著提高开发效率70%以上,极大降低开发成本。 又是一个全栈式 AI 开发平台,快速帮助企业构建和部署个性化的 AI 应用。

信创兼容说明

  • 操作系统:国产麒麟、银河麒麟等国产系统几乎都是基于 Linux 内核,因此它们具有良好的兼容性。
  • 数据库:达梦、人大金仓、TiDB
  • 中间件:东方通 TongWeb、TongRDS,宝兰德 AppServer、CacheDB, 信创配置文档

为什么选择 JeecgBoot?

开源界"小普元"超越传统商业平台。引领低代码开发模式(OnlineCoding-> 代码生成器 -> 手工MERGE),低代码开发同时又支持灵活编码, 可以帮助解决Java项目70%的重复工作,让开发更多关注业务。既能快速提高开发效率,节省成本,同时又不失灵活性。
  • 1.采用最新主流前后分离框架(Spring Boot + MyBatis + Ant Design4 + Vue3),容易上手;代码生成器依赖性低,灵活的扩展能力,可快速实现二次开发。
  • 2.前端大版本换代,最新版采用 Vue3.0 + TypeScript + Vite6 + Ant Design Vue4 等新技术方案。
  • 3.支持微服务Spring Cloud Alibaba(Nacos、Gateway、Sentinel、Skywalking),提供简易机制,支持单体和微服务自由切换(这样可以满足各类项目需求)。
  • 4.开发效率高,支持在线建表和AI建表,提供强大代码生成器,单表、树列表、一对多、一对一等数据模型,增删改查功能一键生成,菜单配置直接使用。
  • 5.代码生成器提供强大模板机制,支持自定义模板,目前提供四套风格模板(单表两套、树模型一套、一对多三套)。
  • 6.提供强大的报表和大屏可视化工具,支持丰富的数据源连接,能够通过拖拉拽方式快速制作报表、大屏和门户设计;支持多种图表类型:柱形图、折线图、散点图、饼图、环形图、面积图、漏斗图、进度图、仪表盘、雷达图、地图等。
  • 7.低代码能力:在线表单(无需编码,通过在线配置表单,实现表单的增删改查,支持单表、树、一对多、一对一等模型,实现人人皆可编码),在线配置零代码开发、所见即所得支持23种类控件。
  • 8.低代码能力:在线报表、在线图表(无需编码,通过在线配置方式,实现数据报表和图形报表,可以快速抽取数据,减轻开发压力,实现人人皆可编码)。
  • 9.Online支持在线增强开发,提供在线代码编辑器,支持代码高亮、代码提示等功能,支持多种语言(Java、SQL、JavaScript等)。
  • 10.封装完善的用户、角色、菜单、组织机构、数据字典、在线定时任务等基础功能,支持访问授权、按钮权限、数据权限等功能。
  • 11.前端UI提供丰富的组件库,支持各种常用组件,如表格、树形控件、下拉框、日期选择器等,满足各种复杂的业务需求 UI组件库文档
  • 12.提供APP配套框架,一份多代码多终端适配,一份代码多终端适配,小程序、H5、安卓、iOS、鸿蒙Next。
  • 13.新版APP框架采用Uniapp、Vue3.0、Vite、Wot-design-uni、TypeScript等最新技术栈,包括二次封装组件、路由拦截、请求拦截等功能。实现了与JeecgBoot完美对接:目前已经实现登录、用户信息、通讯录、公告、移动首页、九宫格、聊天、Online表单、仪表盘等功能,提供了丰富的组件。
  • 14.提供了一套成熟的AI应用平台功能,从AI模型、知识库到AI应用搭建,助力企业快速落地AI服务,加速智能化升级。
  • 15.AI能力:目前JeecgBoot支持AI大模型chatgpt和deepseek,现在最新版默认使用deepseek,速度更快质量更高。目前提供了AI对话助手、AI知识库、AI应用、AI建表、AI报表等功能。
  • 16.提供新行编辑表格JVXETable,轻松满足各种复杂ERP布局,拥有更高的性能、更灵活的扩展、更强大的功能。
  • 17.平台首页风格,提供多种组合模式,支持自定义风格;支持门户设计,支持自定义首页。
  • 18.常用共通封装,各种工具类(定时任务、短信接口、邮件发送、Excel导入导出等),基本满足80%项目需求。
  • 19.简易Excel导入导出,支持单表导出和一对多表模式导出,生成的代码自带导入导出功能。
  • 20.集成智能报表工具,报表打印、图像报表和数据导出非常方便,可极其方便地生成PDF、Excel、Word等报表。
  • 21.采用前后分离技术,页面UI风格精美,针对常用组件做了封装:时间、行表格控件、截取显示控件、报表组件、编辑器等。
  • 22.查询过滤器:查询功能自动生成,后台动态拼SQL追加查询条件;支持多种匹配方式(全匹配/模糊查询/包含查询/不匹配查询)。
  • 23.数据权限(精细化数据权限控制,控制到行级、列表级、表单字段级,实现不同人看不同数据,不同人对同一个页面操作不同字段)。
  • 24.接口安全机制,可细化控制接口授权,非常简便实现不同客户端只看自己数据等控制;也提供了基于AK和SK认证鉴权的OpenAPI功能。
  • 25.活跃的社区支持;近年来,随着网络威胁的日益增加,团队在安全和漏洞管理方面积累了丰富的经验,能够为企业提供全面的安全解决方案。
  • 26.权限控制采用RBAC(Role-Based Access Control,基于角色的访问控制)。
  • 27.页面校验自动生成(必须输入、数字校验、金额校验、时间空间等)。
  • 28.支持SaaS服务模式,提供SaaS多租户架构方案。
  • 29.分布式文件服务,集成MinIO、阿里OSS等优秀的第三方,提供便捷的文件上传与管理,同时也支持本地存储。
  • 30.主流数据库兼容,一套代码完全兼容MySQL、PostgreSQL、Oracle、SQL Server、MariaDB、达梦、人大金仓等主流数据库。
  • 31.集成工作流Flowable,并实现了只需在页面配置流程转向,可极大简化BPM工作流的开发;用BPM的流程设计器画出了流程走向,一个工作流基本就完成了,只需写很少量的Java代码。
  • 32.低代码能力:在线流程设计,采用开源Flowable流程引擎,实现在线画流程、自定义表单、表单挂靠、业务流转。
  • 33.多数据源:极其简易的使用方式,在线配置数据源配置,便捷地从其他数据抓取数据。
  • 34.提供单点登录CAS集成方案,项目中已经提供完善的对接代码。
  • 35.低代码能力:表单设计器,支持用户自定义表单布局,支持单表、一对多表单,支持select、radio、checkbox、textarea、date、popup、列表、宏等控件。
  • 36.专业接口对接机制,统一采用RESTful接口方式,集成Swagger-UI在线接口文档,JWT token安全验证,方便客户端对接。
  • 37.高级组合查询功能,在线配置支持主子表关联查询,可保存查询历史。
  • 38.提供各种系统监控,实时跟踪系统运行情况(监控Redis、Tomcat、JVM、服务器信息、请求追踪、SQL监控)。
  • 39.消息中心(支持短信、邮件、微信推送等);集成WebSocket消息通知机制。
  • 40.支持多语言,提供国际化方案。
  • 41.数据变更记录日志,可记录数据每次变更内容,通过版本对比功能查看历史变化。
  • 42.提供简单易用的打印插件,支持谷歌、火狐、IE11+等各种浏览器。
  • 43.后端采用Maven分模块开发方式;前端支持菜单动态路由。
  • 44.提供丰富的示例代码,涵盖了常用的业务场景,便于学习和参考。

技术架构:

前端
  • 前端环境要求:Node.js要求Node 20+ 版本以上、pnpm 要求9+ 版本以上
  • 依赖管理:node、npm、pnpm
  • 前端IDE建议:IDEA、WebStorm、Vscode
  • 采用 Vue3.0+TypeScript+Vite6+Ant-Design-Vue4等新技术方案,包括二次封装组件、utils、hooks、动态菜单、权限校验、按钮级别权限控制等功能
  • 最新技术栈:Vue3.0 + TypeScript + Vite6 + ant-design-vue4 + pinia + echarts + unocss + vxe-table + qiankun + es6
后端
  • IDE建议: IDEA (必须安装lombok插件 )
  • 语言:Java 默认jdk17(支持jdk8、jdk21)
  • 依赖管理:Maven
  • 基础框架:Spring Boot 2.7.18
  • 微服务框架: Spring Cloud Alibaba 2021.0.6.2
  • 持久层框架:MybatisPlus 3.5.3.2
  • 报表工具: JimuReport 1.9.5
  • 安全框架:Apache Shiro 1.13.0,Jwt 4.5.0
  • 微服务技术栈:Spring Cloud Alibaba、Nacos、Gateway、Sentinel、Skywalking
  • 数据库连接池:阿里巴巴Druid 1.1.24
  • AI大模型:支持 ChatGPT DeepSeek切换
  • 日志打印:logback
  • 缓存:Redis
  • 其他:autopoi, fastjson,poi,Swagger-ui,quartz, lombok(简化代码)等。
  • 默认提供MySQL5.7+数据库脚本

微服务架构图

微服务解决方案

微服务方式快速启动

  • 1、服务注册和发现 Nacos
  • 2、统一配置中心 Nacos
  • 3、路由网关 gateway(三种加载方式)
  • 4、分布式 http feign
  • 5、熔断降级限流 Sentinel
  • 6、分布式文件 Minio、阿里OSS
  • 7、统一权限控制 JWT + Shiro
  • 8、服务监控 SpringBootAdmin
  • 9、链路跟踪 Skywalking 参考文档
  • 10、消息中间件 RabbitMQ
  • 11、分布式任务 xxl-job
  • 12、分布式事务 Seata
  • 13、轻量分布式日志 Loki+grafana套件
  • 14、支持 docker-compose、k8s、jenkins
  • 15、CAS 单点登录
  • 16、路由限流

Jeecg Boot 产品功能蓝图

系统功能架构图

开源版功能清单

├─AI应用平台
│  ├─AI模型管理
│  ├─AI应用管理
│  ├─AI知识库
│  ├─AI流程编排
│  ├─AI聊天助手(支持图片、文件)
│  ├─AI聊天助手支持嵌入第三方、支持移动端
│  ├─MCP插件管理
│  ├─提示词管理
│  ├─AI应用门户(汇总各种AI应用场景)
│  ├─支持各种常见模型ChatGPT和DeepSeek、ollama等
├─工具箱
│  ├─OCR识别
│  ├─AI 海报
│  ├─AI 写作
│  ├─AI 简历
├─AI辅助功能
│  ├─AI建表(Online表单)
│  ├─AI生成报表(Online报表)
│  ├─AI生成大屏
├─系统管理
│  ├─用户管理
│  ├─角色管理
│  ├─菜单管理
│  ├─权限设置(支持按钮权限、数据权限)
│  ├─表单权限(控制字段禁用、隐藏)
│  ├─部门管理
│  ├─我的部门(二级管理员)
│  └─字典管理
│  └─分类字典
│  └─系统公告
│  └─职务管理
│  └─通讯录
│  ├─多数据源管理
│  └─多租户管理(租户管理、租户角色、我的租户)
├─Online在线开发(低代码)
│  ├─Online在线表单
│  ├─Online代码生成器
│  ├─Online在线报表
│  ├─仪表盘设计器
│  ├─系统编码规则
│  ├─系统校验规则
├─积木报表设计器
│  ├─打印设计器
│  ├─数据报表设计
│  ├─图形报表设计(支持echart)
├─消息中心
│  ├─消息管理
│  ├─模板管理
├─代码生成器(低代码)
│  ├─代码生成器功能(一键生成前后端代码,生成后无需修改直接用,绝对是后端开发福音)
│  ├─代码生成器模板(提供4套模板,分别支持单表和一对多模型,不同风格选择)
│  ├─代码生成器模板(生成代码,自带excel导入导出)
│  ├─查询过滤器(查询逻辑无需编码,系统根据页面配置自动生成)
│  ├─高级查询器(弹窗自动组合查询条件)
│  ├─Excel导入导出工具集成(支持单表,一对多 导入导出)
│  ├─平台移动自适应支持
│  ├─提供新版uniapp3的代码生成器模板
├─系统监控
│  ├─基于AK和SK认证鉴权OpenAPI功能
│  ├─Gateway路由网关
│  ├─性能扫描监控
│  │  ├─监控 Redis
│  │  ├─Tomcat
│  │  ├─jvm
│  │  ├─服务器信息
│  │  ├─请求追踪
│  │  ├─磁盘监控
│  ├─定时任务
│  ├─系统日志
│  ├─消息中心(支持短信、邮件、微信推送等等)
│  ├─数据日志(记录数据快照,可对比快照,查看数据变更情况)
│  ├─系统通知
│  ├─SQL监控
│  ├─swagger-ui(在线接口文档)
│─报表示例
│  ├─曲线图
│  └─饼状图
│  └─柱状图
│  └─折线图
│  └─面积图
│  └─雷达图
│  └─仪表图
│  └─进度条
│  └─排名列表
│  └─等等
│─大屏模板
│  ├─作战指挥中心大屏
│  └─物流服务中心大屏
│─常用示例
│  ├─自定义组件
│  ├─对象存储(对接阿里云)
│  ├─JVXETable示例(各种复杂ERP布局示例)
│  ├─单表模型例子
│  └─一对多模型例子
│  └─打印例子
│  └─一对多TAB例子
│  └─内嵌table例子
│  └─常用选择组件
│  └─异步树table
│  └─接口模拟测试
│  └─表格合计示例
│  └─异步树列表示例
│  └─一对多JEditable
│  └─JEditable组件示例
│  └─图片拖拽排序
│  └─图片翻页
│  └─图片预览
│  └─PDF预览
│  └─分屏功能
│─封装通用组件    
│  ├─行编辑表格JEditableTable
│  └─省略显示组件
│  └─时间控件
│  └─高级查询
│  └─用户选择组件
│  └─报表组件封装
│  └─字典组件
│  └─下拉多选组件
│  └─选人组件
│  └─选部门组件
│  └─通过部门选人组件
│  └─封装曲线、柱状图、饼状图、折线图等等报表的组件(经过封装,使用简单)
│  └─在线code编辑器
│  └─上传文件组件
│  └─验证码组件
│  └─树列表组件
│  └─表单禁用组件
│  └─等等
│─更多页面模板
│  ├─各种高级表单
│  ├─各种列表效果
│  └─结果页面
│  └─异常页面
│  └─个人页面
├─高级功能
│  ├─提供单点登录CAS集成方案
│  ├─提供APP发布方案
│  ├─集成Websocket消息通知机制
│  ├─支持electron桌面应用打包(支持windows、linux、macOS三大平台)
│  ├─docker容器支持
│  ├─提供移动APP框架及源码(Uniapp3版本)支持H5、小程序、APP、鸿蒙Next
│  ├─提供移动APP低代码设计(Online表单、仪表盘)

系统效果预览

AI模型与应用管理

AI流程编排

MCP和工具管理

AI知识库(支持各种文档格式,尤其markdown适配很好)

AI工具箱

AI聊天助手

AI写文章

PC端

在线聊天&通知

Online开发(在线配置表单和报表)

Online AI建表

图表示例

积木BI大屏

APP效果

PAD端

在线接口文档

积木报表

欢迎吐槽,欢迎star~

近期,我们围绕离线开发产品进行了一系列功能新增与优化,旨在为用户提供更智能、更高效的开发体验 。本次更新重点引入了离线AI“代码续写”功能,显著提升辅助编程效率;同时支持中英文自由切换,满足国际化业务需求 。在架构层面,新增了Doris SQL多计算引擎切换及业务流程跨工作流编排能力 。此外,我们还优化了Restful源端配置、实现了Python日志实时打印并强化了权限管控,全面赋能企业构建稳健的数据基座 。

一、功能新增

1.重点新增内容

1.1.离线AI功能新增「代码续写」功能

在数据开发场景中引入AI辅助编程能力,可根据用户已输入的代码片段,智能预测并生成后续代码内容,提升开发效率。
图片

1.2.离线开发平台支持中英文切换

为满足客户海外业务统一管理需求,产品界面新增中文/英文版本切换功能,完成国际化适配。
图片

1.3.Doris SQL任务支持多计算引擎切换

为提升业务数据管理效能,部分用户选择构建双集群环境,分别用于数据仓库建设与应用数据存储。在实际开发过程中,任务需根据具体的业务场景分发至相应集群执行。为此,离线数据开发相关功能已全面适配多集群架构,其支持范围涵盖以下内容:

  • 离线项目内支持对接控制台内多个 Doris 集群
  • 在 Doris SQL 任务中可以切换集群提交运行
  • 表查询支持查看对接了不同 Doris 集群中表的数据
  • 项目层面支持对不同集群分别绑定数据库账号密码
  • 测试项目任务发布到生产项目,支持配置 Doris 引擎两个项目间的映射关系

图片

图片

1.4.新增业务流程类型,支持跨工作流任务编排

针对跨工作流的任务依赖与全链路管理需求,系统引入“业务流程”单元。它打破了传统单一链路的局限,从业务维度整合多工作流任务,实现跨流编排、依赖管理与统一调度。

核心功能包括:

①任务整合与业务视图将分散在多个工作流中的相关任务统一纳入同一业务流程管理。自动形成可视化的业务链路视图,清晰展示任务间的业务逻辑关系。

②跨流程依赖配置支持任务之间、任务与业务流程之间的灵活依赖配置。可实现跨工作流、跨流程的依赖管理,满足复杂业务链路调度需求。

③调度与运行能力流程下的任务可独立配置调度策略,无需配置根节点即可直接提交。最终以“流程下的单个任务”为调度运行维度,实现灵活高效的执行控制。

④补数据能力支持流程内任务的统一或独立补数操作,确保业务链路数据一致性。
图片

图片

1.5.计算引擎与数据同步支持GaussDB 9.1

新增对GaussDB 9.1计算引擎的支持,涵盖周期任务、语法提示、数据同步等功能;数据同步任务在源端和目标端均可选择GaussDB 9.1作为读写数据源。
图片

1.6.inceptor数据同步支持一键生成目标表

当数据同步任务的目标端为inceptor时,支持一键自动创建目标表结构。
图片

1.7.数据同步向导模式支持Kafka 2.x

数据同步任务的源端与目标端均支持选择Kafka 2.x作为数据源,便于从Kafka进行周期性数据抽取。
图片

图片

1.8.数据地图DQL权限校验强化

优化表数据预览的权限逻辑,用户必须同时具备“表管理-查看”权限以及在数据地图中已申请获得的DQL权限,方可查看数据,确保权限边界清晰。
图片

图片

1.9.Hive表权限管控功能

新增「数据地图外表权限管控」配置功能。该功能在兼容历史客户使用习惯的基础上,提供更严格的数据安全防护。用户可根据实际场景选择是否限制对未纳入数据地图表的操作权限,从而有效防止越权访问和潜在数据风险。
图片

图片

1.10.SparkSQL 3.2支持读写Hudi 0.15.0

在控制台Spark集群内新增DataLake的hudi配置项后,SparkSQL任务支持对 Hudi 表执行完整的 DDL、DML、DQL 操作,用户可像操作 Hive 表一样直接进行 查询、创建、修改与写入,实现统一的 SQL 使用体验
图片

2.其他新增内容

  • 支持DMDB for Oracle计算引擎

新增对DMDB for Oracle计算引擎的支持,涵盖周期任务、整库同步、数据同步、手动任务、临时查询、语法提示、表查询、函数管理、存储过程、依赖推荐、任务上下游参数、代码模板、按项目或个人粒度绑定数据库账号、执行计划、数据导入等功能模块。

  • 支持读写AWS S3数据

离线数据同步任务、Spark任务、PySpark任务、Spark SQL任务及Hive SQL任务全面适配AWS S3存储底座。

  • SparkSQL 3.5计算引擎适配

Spark3.5支持更多特性包括自适应执行、向量化优化,对Paimon湖仓有更好支持。为提升平台性能与兼容性,满足客户在离线计算场景中对SparkSQL的最新特性需求,平台计算引擎SparkSQL3.5进行适配

功能如下:

计算引擎支持对接SparkSQL3.5版本,支持创建任务、周期运行、补数据等操作;SparkSQL3.5支持对Paimon 1.2进行操作,包括DDL、DML、DQL语法

  • Doris 3.x版本适配
    全面支持Doris 3.x作为计算引擎,覆盖周期任务、整库同步、数据同步、手动任务、临时查询、语法提示、表查询、函数管理、依赖推荐、任务上下游参数、代码模板、账号绑定、执行计划等功能。
  • HiveSQL 2.3.8支持读写Paimon 1.2

在构建相应连接jar包后,HiveSQL任务支持对Paimon表执行完整的DDL、DML、DQL操作。

二、功能优化

1.重点功能优化说明

1.1.任务依赖配错提示功能

系统可根据资产平台表血缘关系自动解析生成任务依赖。当用户手动配置的依赖存在多配或少配时,在提交任务时会进行弹窗提示,降低因依赖配置错误导致的运行时故障风险。

图片

1.2.同小时任务依赖逻辑优化

为满足交易类业务对小时级调度链路时效性的高要求,优化依赖匹配规则:支持“优先寻找同小时的上游实例”;若无同小时实例,则自动回退至最近时间的实例。新增“基于默认依赖周期的偏移”和“优先寻找同小时的上游实例”两种依赖方式选项。

适用于 3 类时间间隔场景(参考下方实例依赖图):

任务A与任务B间隔一致:

同小时匹配:B 11:30 → A 11:50;回退匹配:B 10:30 → A 前一天 14:50

任务A间隔小于任务B:

同小时匹配:B 15:30 → A 14:50;回退匹配:B 20:30 → A 16:50

任务A间隔大于任务B:

同小时匹配:B 10:50 → A 10:30;回退匹配:B 12:50 → A 10:30

图片

图片

图片

1.3.同步任务Restful源端配置优化

在数据同步任务配置Restful数据源时,增加Path路径填写项,用户可直接在任务内填写完整URL,简化多接口配置流程。

图片

1.4.脚本日志展示SQL影响行数

任务脚本执行完成后,在日志中明确展示SQL运行的影响行数:对DML语句(如INSERT、UPDATE、DELETE),日志中返回实际影响的行数;对DDL语句(如 TRUNCATE、DROP、ALTER、CREATE),日志中统一返回 -1

图片

1.5.Python on Agent日志实时打印

优化Python任务执行机制,支持在任务运行过程中于页面实时打印输出日志和错误信息,改变此前需等待任务结束后才查看日志的状况。

图片

1.6.告警规则新增“未按计划时间运行”触发条件

在告警规则中新增该触发方式,当任务超过计划时间一定阈值仍未开始运行时,系统自动触发告警,便于及时发现调度阻塞。

图片

1.7.支持配置任务实例默认并发数

新增为补数据任务和手动任务配置“最大并行实例数”默认值的能力,防止因误操作触发大量实例导致集群资源耗尽。

图片

1.8.操作设置页面布局调整

将操作设置中的众多配置项按“数据同步”、“SQL任务”、“调度”、“通用”四大模块进行重新分类展示,提升查找和配置效率。

图片

2.其他功能优化

  • 本地数据导入优化,支持大文件分片上传

针对本地上传大文件时页面卡顿、崩溃的问题,重构代码逻辑,采用分片上传技术,并将单个文件大小上限设置为500MB,提升上传稳定性和用户体验。

  • Sql Parser RPC改造

对Sql Parser进行RPC改造,显著提高解析服务的稳定性,减少因解析导致的IO飙高、进程异常及服务不可用情况。

  • Redis数据写入性能大幅提升

优化数据同步任务中Redis的写入逻辑,在单并发场景下,写入3000万条数据的耗时从超过15分钟缩短至2分钟以内,极大提升了同步效率。

  • 表生命周期统一管理入口

为解决多子产品中生命周期配置不一致可能导致数据误清理的问题,统一通过业务中心SDK维护表生命周期。系统自动判断表是否存在并执行插入或更新操作,确保配置一致性。

  • 函数列表请求方式优化

合并进入“数据开发”页面时对函数目录的重复接口调用,整合为一次性请求,解决因函数过多导致的页面加载缓慢问题。

  • 调用接口redux优化

减少页面打开时的不必要接口请求,优化请求数量,提升页面响应速度。

  • 对接资产数据脱敏规则

实现资产中心配置的Hive、Doris数据源脱敏规则在离线开发平台内同步生效,用户无需在两个产品内重复配置。

  • 知识库同步流程支持并发

优化AI知识库的数据同步流程,支持配置多线程并行同步,显著缩短同步耗时,改善生产环境同步体验。

本次版本 新增函数对象转换能力,扩展了达梦等多数据库迁移适配范围,并提升了批量转换的处理效率,进一步降低企业级数据库迁移的复杂度与成本。

一、核心特性

支持函数对象迁移

函数对象可随存储过程的迁移任务一键同步转换​。该能力的加入,让 SQLShift[1] 从一款“​存储过程迁移工具​”升级为“​核心业务逻辑对象全量迁移工具​”。随之也带来三重提升:

  1. 降低迁移风险与人工成本

    避免 函数对象 需人工逐个改写与反复校验,大幅减少因语法差异、返回值不一致引发的运行期错误。

  2. 提升非表对象整体迁移效率

    函数对象与存储过程 可在同一时间中完成迁移与校验,缩短整体迁移周期。

  3. 保障业务逻辑完整性与可用性

避免 函数对象 缺失导致上层存储过程等对象无法编译或运行的问题,有效降低迁移后集中调试与返工压力,提升割接与上线的稳定性。

函数对象迁移任务

<iframe src="//player.bilibili.com/player.html?isOutside=true&aid=115970207123078&bvid=BV1Gg6LBAEf3&cid=35655845383&p=1" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true"></iframe>

新增数据库迁移组合

本次升级,SQLShift 扩展了多项数据库迁移组合。

SQLShift 支持迁移链路

  • 新增 Oracle / OceanBase → 达梦

    降低了迁移至达梦数据库的复杂度和人工成本,帮助企业快速完成数据库替换或国产化改造。

  • 新增 PostgreSQL → OceanBase(Oracle 模式)

    减少了跨数据库迁移中的人工调整工作量,加快了从 PostgreSQL 向 OceanBase 的迁移进程。

二、其他更新

批量处理能力提升

支持同时上传多个 SQL 文件进行转换,提升大规模迁移场景下的处理效率。

免费试用限时开放!

👉 点击领取 你的转换额度,立即体验 SQLShift 智能化迁移带来的飞跃效率!

🧩 SQL 方言再多,转换也能一步到位,SQLShift 为你搞定!

SQLShift介绍

已经在虚拟机部署好Apache DolphinScheduler了,想尝试下在Flink新建一个Flink节点,然后用Flink消费Kafka数据。

Apache DolphinScheduler用的是单机部署,具体操作可以参考官方文档:DolphinScheduler | 文档中心(https://dolphinscheduler.apache.org/zh-cn/docs/3.3.2/guide/in...).

  • 前置条件:已经安装Java 11、DolphinScheduler 3.3.2、Flink 1.18.1、Kafka 3.6.0,Zookeeper用Kafka内置的。建议这些安装都下载二进制的安装包到虚拟机安装,用命令安装的不可控,我下载的二进制包如下:

配置好Flink的环境变量

1、编辑环境变量:

sudo vim ~/.bashrc

增加Flink的路径

2、使环境变量生效:

#使环境变量生效
source ~/.bashrc
#查看环境变量
echo $Flink_HOME

修改Kafka、Flink以及DolphinScheduler的配置文件

因为用的是虚拟机,为了让外面的主机能够访问到虚拟机的网络,需要修改下配置文件

  1. 修改Kafka配置:找到Kafka安装包下的config文件夹,修改config下的server.properties文件,修改listeners是为了外面的主机能够访问到虚拟机的Kafka,还有把advertised.listeners改成虚拟机地址,写样例的时候能连上虚拟机的Kafka地址,不然默认连localhost
broker.id=0
listeners=PLAINTEXT://0.0.0.0:9092
#192.168.146.132修改成虚拟机ip
advertised.listeners=PLAINTEXT://192.168.146.132:9092

  1. 修改Flink配置:找到Flink安装包下的conf文件夹,修改conf下的Flink-conf.yaml文件,把里面所有的localhost地址全部改成0.0.0.0,以便主机能访问到虚拟机的Flink。还有增加jobmanager和taskmanager的内存
jobmanager.rpc.address: 0.0.0.0
jobmanager.bind-host: 0.0.0.0
jobmanager.cpu.cores: 1
jobmanager.memory.process.size: 1600m
taskmanager.bind-host: 0.0.0.0
taskmanager.host: 0.0.0.0
taskmanager.memory.process.size: 2048m
taskmanager.cpu.cores: 1

  1. 修改Apache DolphinScheduler的配置文件,从Apache DolphinScheduler的启动脚本文件dolphinscheduler-daemon.sh可以看出,配置环境变量用的是bin/env文件夹下的dolphinscheduler_env.sh

查看dolphinscheduler-daemon.sh文件:

修改dolphinscheduler_env.sh文件,新增JAVA、Flink路径:

#修改成自己的JAVA、Flink路径
export JAVA_HOME=/data/jdk-11.0.29
export Flink_HOME=/data/Flink-1.18.1

关闭防火墙,启动应用

启动应用,包括Zookeeper、Kafka、Flink以及Apache DolphinScheduler。

#关闭防火墙
sudo systemctl stop firewalld
 
# 在 Flink 根目录下,执行以下命令启动 Flink 集群
bin/start-cluster.sh
 
# 启动 ZooKeeper
bin/zookeeper-server-start.sh config/zookeeper.properties &
 
# 启动 Kafka 服务器
bin/Kafka-server-start.sh config/server.properties &
 
#创建 Kafka 主题
bin/Kafka-topics.sh --create --topic test --bootstrap-server localhost:9092 --partitions 1 --replication-factor 1
 
#使用命令行生产者发送消息
bin/Kafka-console-producer.sh --topic test --bootstrap-server localhost:9092
 
#消费
bin/Kafka-console-consumer.sh --topic test --from-beginning --bootstrap-server localhost:9092

# 启动 Standalone Server 服务
bash ./bin/dolphinscheduler-daemon.sh start standalone-server

测试

测试Flink、Apache DolphinScheduler是否能访问成功。

  1. Flink访问地址:http://localhost:8081/,localhost改成自己虚拟机地址

  1. Apache DolphinScheduler访问地址:http://localhost:12345/dolphinscheduler/ui ,localhost改成自己虚拟机地址即可登录系统 UI。默认的用户名和密码是 admin/dolphinscheduler123

编写样例

用Flink消费Kafka数据,然后打包上传到Apache DolphinScheduler,启动Flink任务:

  1. 编写样例:

pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
 
    <groupId>com.example</groupId>
    <artifactId>Flink-Kafka-demo</artifactId>
    <version>1.0-SNAPSHOT</version>
 
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <Flink.version>1.18.1</Flink.version>
        <scala.binary.version>2.12</scala.binary.version>
        <Kafka.version>3.6.0</Kafka.version>
    </properties>
 
    <dependencies>
        <!-- Flink核心依赖 -->
        <dependency>
            <groupId>org.apache.Flink</groupId>
            <artifactId>Flink-java</artifactId>
            <version>${Flink.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.Flink</groupId>
            <artifactId>Flink-streaming-java</artifactId>
            <version>${Flink.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.Flink</groupId>
            <artifactId>Flink-clients</artifactId>
            <version>${Flink.version}</version>
        </dependency>
 
        <!-- 连接器基础依赖 -->
        <dependency>
            <groupId>org.apache.Flink</groupId>
            <artifactId>Flink-connector-base</artifactId>
            <version>${Flink.version}</version>
        </dependency>
 
        <!-- Kafka连接器(关键修改点) -->
        <dependency>
            <groupId>org.apache.Flink</groupId>
            <artifactId>Flink-connector-Kafka</artifactId>
            <version>3.1.0-1.18</version>
        </dependency>
        <dependency>
            <groupId>org.apache.Kafka</groupId>
            <artifactId>Kafka-clients</artifactId>
            <version>${Kafka.version}</version>
        </dependency>
 
        <!-- 日志依赖 -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
            <version>1.7.36</version>
            <scope>runtime</scope>
        </dependency>
    </dependencies>
 
    <repositories>
        <repository>
            <id>aliyun</id>
            <url>https://maven.aliyun.com/repository/public</url>
            <releases>
                <enabled>true</enabled>
            </releases>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
        <repository>
            <id>apache-releases</id>
            <url>https://repository.apache.org/content/repositories/releases/</url>
        </repository>
    </repositories>
 
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>${maven.compiler.source}</source>
                    <target>${maven.compiler.target}</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>3.2.4</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <artifactSet>
                                <excludes>
                                    <exclude>org.apache.Flink:force-shading</exclude>
                                    <exclude>com.google.code.findbugs:jsr305</exclude>
                                    <exclude>org.slf4j:*</exclude>
                                </excludes>
                            </artifactSet>
                            <filters>
                                <filter>
                                    <artifact>*:*</artifact>
                                    <excludes>
                                        <exclude>META-INF/*.SF</exclude>
                                        <exclude>META-INF/*.DSA</exclude>
                                        <exclude>META-INF/*.RSA</exclude>
                                    </excludes>
                                </filter>
                            </filters>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

FlinkKafkaConsumerExample.java

import org.apache.Flink.api.common.functions.FlatMapFunction;
import org.apache.Flink.api.java.tuple.Tuple2;
import org.apache.Flink.api.java.utils.ParameterTool;
import org.apache.Flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.Flink.streaming.api.datastream.DataStream;
import org.apache.Flink.streaming.api.functions.ProcessFunction;
import org.apache.Flink.streaming.api.functions.sink.RichSinkFunction;
import org.apache.Flink.util.Collector;
import org.apache.Flink.streaming.connectors.Kafka.FlinkKafkaConsumer;
import org.apache.Flink.api.common.serialization.SimpleStringSchema;
import org.apache.Kafka.clients.consumer.ConsumerConfig;
import org.apache.Kafka.common.serialization.StringDeserializer;
 
import java.util.Properties;
import java.util.concurrent.CompletableFuture;
 
 
public class FlinkKafkaConsumerExample {
    private static volatile int messageCount = 0;
    private static volatile boolean shouldStop = false;
    public static void main(String[] args) throws Exception {
        // 设置执行环境
        final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
 
        // Kafka 配置
        Properties properties = new Properties();
        properties.setProperty(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.146.132:9092"); // Kafka broker 地址
        properties.setProperty(ConsumerConfig.GROUP_ID_CONFIG, "test-group"); // 消费者组
        properties.setProperty(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
        properties.setProperty(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
 
        // 创建 Kafka 消费者
        FlinkKafkaConsumer<String> KafkaConsumer = new FlinkKafkaConsumer<>("test", new SimpleStringSchema(), properties);
        KafkaConsumer.setStartFromEarliest(); // 从最早的消息开始消费
        DataStream<String> stream = env.addSource(KafkaConsumer);
 
        // 处理数据:分词和计数
        DataStream<Tuple2<String, Integer>> counts = stream
                .flatMap(new Tokenizer())
                .keyBy(value -> value.f0)
                .sum(1);
 
 
        counts.addSink(new RichSinkFunction<Tuple2<String, Integer>>() {
            @Override
            public void invoke(Tuple2<String, Integer> value, Context context) {
                System.out.println(value);
                messageCount++;
 
                // 检查是否达到停止条件
                if (messageCount >= 2 && !shouldStop) {
                    System.out.println("Processed 2 messages, stopping job.");
                    shouldStop = true; // 设置标志位,表示应该停止
                }
            }
        });
 
        // 执行作业并获取 JobClient
        CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
            try {
                // 启动作业并获取 JobClient
                org.apache.Flink.core.execution.JobClient jobClient = env.executeAsync("Flink Kafka WordCount");
                System.out.println("Job ID: " + jobClient.getJobID());
 
                // 监测条件并取消作业
                while (!shouldStop) {
                    Thread.sleep(100); // 每100毫秒检查一次
                }
 
                // 达到停止条件时取消作业
                if (shouldStop) {
                    System.out.println("Cancelling the job...");
                    jobClient.cancel().get(); // 取消作业
                }
 
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
 
        // 在主线程中等待作业结束
        future.join(); // 等待作业完成
    }
 
    // Tokenizer 类用于将输入字符串转化为单词
    public static final class Tokenizer implements FlatMapFunction<String, Tuple2<String, Integer>> {
        @Override
        public void flatMap(String value, Collector<Tuple2<String, Integer>> out) {
            String[] tokens = value.toLowerCase().split("\\W+");
            for (String token : tokens) {
                if (token.length() > 0) {
                    out.collect(new Tuple2<>(token, 1));
                }
            }
        }
    }
 
}
  1. 打包上传到Apache DolphinScheduler

  1. 新建Flink节点,并启动

在Apache DolphinScheduler的任务实例看启动日志:

在虚拟机启动生产者,输出字符串,然后可以在Flink查看输出Kafka生产的消息:

原文链接:https://blog.csdn.net/Analyze_ing/article/details/156940553

【AI 探索】从 CodeReview 到全流程闭环:我的 AI 辅助开发实践心得

引言:惊鸿一瞥后的深度拥抱

一切的开始,源于一次令人惊叹的 AI CodeReview 体验。

在那之前,我对 AI 的辅助能力还停留在“代码补全”的印象中。但看到 AI 能够精准地指出代码逻辑中的隐患、提出优雅的重构建议后,我意识到:时代变了。这不仅仅是一个工具的升级,更是一种开发模式的变革。

受到这次冲击后,我开始较为深度地在日常开发中使用 AI IDE,尝试将更多的任务交付给它。经过一段时间的摸索与实践,我总结了一些心得,并在团队分享会上进行了汇报。今天,我想把这些思考落实成文,与大家分享。

核心理念:构建“自我闭环验证”能力

对于当前的 AI 模型能力,我持非常积极的态度。在大量的实战中,我悟出了一个关键道理:
只要让 AI 具备“自我闭环验证”的能力,它就能高效地帮我们完成更多事情。
很多时候,我们不敢放手让 AI 做事,是因为担心它写出“看似正确实则无法运行”的代码。一旦我们将 验证环节 也交给 AI,让它不仅负责“写”,还负责“证”,信任链条就打通了。

我的 AI 开发新流程

基于“闭环验证”的理念,我重构了自己的开发流水线。现在的完整流程如下:

  1. 业务代码开发 :由 AI 初步完成核心逻辑。
  2. 代码风格检查 :确保生成的代码符合团队规范(Linting)。
  3. 测试驱动与自我运行 :

    • 让 AI 编写接口测试和单元测试。
    • 关键点 :让 AI 自己运行这些测试,并修复报错,直到测试通过。
  4. 覆盖率验证 :通过测试覆盖率报告,确信代码的健壮性。
  5. AI CodeReview :提交到 GitLab 后,再次利用 AI 进行代码审查,查漏补缺。
    在这个流程中,我从“代码编写者”转变为了“需求定义者”和“最终验收者”。

实战案例:让 AI 自己证明自己

为了更直观地说明,举一个最近的实战需求: feat(order-warn-text) 。

需求背景 :支付成功率报警文案中的链接,需要改为“可点击”状态。

传统做法 :
我需要找到拼接字符串的地方,修改 HTML 标签,然后启动本地服务,造数据触发报警,查看效果。

AI 辅助下的做法 :

  1. 定位与指令 :我只需要确定大概修改哪个文件,然后用精确的语言告知 AI:“我希望将这里的报警文案链接改为 HTML href 格式,实现可点击效果。”
  2. 闭环验证要求 :我没有直接看它生成的业务代码,而是对它说:“请编写一个相关的单元测试,该测试的输出结果需要包含一段生成的 HTML 片段,我要直接看这段 HTML 来验证链接是否正确。”
  3. 结果验收 :AI 迅速写好了代码和测试,并跑出了结果。我直接查看测试输出的 HTML,确认标签结构无误,链接可跳转。
    结论 :
    在这个过程中,我完全不需要关心它是如何拼接字符串的,也不需要费力去启动整个服务。 只要它能通过测试输出让我信服的结果(比如那段 HTML),我就认可它的工作。

总结

AI 不仅仅是帮你少敲几个键盘的助手,它完全可以胜任更复杂的“开发-测试-验证”全流程。

关键在于我们如何给它下达指令,以及如何设计“验收标准”。当我们学会利用单元测试和自动化流程让 AI 实现“自我闭环”时,我们的生产力将得到质的飞跃。

未来已来,拥抱变化,让我们做更聪明的开发者。

写在最后

注:本文基于自己的心得,使用 AI 扩展而来。原文如下:

「AI 探索」

在看到 AI CodeReview 的令人惊叹的效果后,较为深度地开始使用 AI IDE 完成了不少任务,总结了心得并在分享会进行了团队内分享
对于当前 AI 模型能力持较为积极态度,意识到只要让 AI 做到自我闭环验证的能力就可以更高效的让 AI 完成更多事情
业务代码通过 AI 开发完成后,确保代码风格没有问题,让 AI 编写接口测试、单元测试并自我运行,通过测试覆盖率进行自我验证,提交到 Gitlab 后通过 AI 进行 CodeReview 的完整开发流程
例如 feat(order-warn-text): 支付成功率报警,文案链接改为可点击 这个需求,我只需要确定应该在哪里修改相关内容,用精确的语言告知 AI 我想要实现的目标和效果,并让他编写相关的单元测试,该单元测试能够生成 HTML 让我再检查一下是否真的可点击,就不需要我再关心他是如何实现这个需求的了

在科技演进的长周期中,一项技术从“效率工具”走向“社会底座”,往往意味着其角色已经发生结构性变化。进入 2026 年,人工智能正处于这样的转折点。相比前几年作为企业创新亮点的应用形态,AI 正逐步演化为一种通用型基础设施,开始承担类似算力、网络与操作系统的底层支撑职能。

这种变化并非概念升级,而是源于交付方式、成本结构以及组织使用方式的同步转变。

一、从工具到基础设施的界限变化

在行业实践中,工具型技术通常用于解决局部、离散的问题,需要明确的使用入口和操作主体;而基础设施则具备泛在性、稳定性与低感知度,其价值体现在持续支撑上层系统的运行,而非单点能力的展示。

到 2026 年,AI 已不再以独立模块存在,而是被原生嵌入到操作系统、数据平台与业务流程之中,成为默认可调用的系统能力。

二、推动基础设施化的三项关键变化

1. 交互成本的显著降低

随着多模态模型和自然语言接口的成熟,用户不再需要理解模型结构或提示技巧即可完成复杂指令。AI 的使用方式逐渐标准化,使其具备“即用即得”的特征。

2. 自主运行能力成为常态

行业中已普遍观察到,AI 从被动响应转向持续执行任务流,能够在后台完成跨系统协作与状态维护。“智能体来了”不再只是概念,而是企业系统中真实存在的一种运行形态。

3. 推理成本的结构性下降

在专用硬件与模型压缩技术推动下,推理的边际成本持续降低。AI 不再是需要单独核算 ROI 的高成本模块,而逐步成为企业 IT 架构中的基础性消耗项。

三、价值链角色的重新分配

对开发者而言,重心正在从实现具体功能,转向对业务规则与执行边界的定义。应用构建更多体现为对智能能力的编排,而非代码逻辑的堆叠。

对企业而言,关注点从“采购 AI 产品”转为“流程是否可被 AI 驱动”。业务流程的数字化程度,开始直接决定 AI 基础设施能够释放的价值上限。

对终端用户而言,AI 的存在感持续降低。多数智能行为通过系统默认完成,用户往往只感知结果,而不再感知技术本身。

四、建设逻辑的变化

在基础设施化趋势下,AI 的建设思路也随之转变:

  • 从点状集成,转向全流程嵌入
  • 从任务数据准备,转向持续演化的知识与向量资产
  • 从效果评估,转向稳定性、时延与单位成本控制
  • 从静态安全策略,转向动态合规与全生命周期治理

五、结语

2026 年的一个显著变化在于,AI 正逐渐从“显性的创新能力”转变为“隐性的运行背景”。它不再以改变世界的姿态出现,而是成为世界正常运转的一部分。

在这一阶段,真正的差异化不再来自是否使用 AI,而来自是否能够在这一基础之上,构建新的业务逻辑与组织能力。

随着AI的广泛应用,GitHub Copilot、Cursor等AI Coding Agents 已经像空气一样,渗透进开发者的日常。自动化生成代码、智能补全、一键找 Bug……听起来,程序员似乎终于要从繁重的体力活中解脱,迎来效率的跃升。

然而,AI 的加入,真的缩短了我们的开发周期吗?

最近,关于 AI 究竟是“提效神器”还是“效率黑洞”的讨论,正成为行业关注的焦点。我们拆解了多项深度调研与实验数据,发现了一个事实:AI Coding并没有真正缩短开发周期,它只是把“坑”换了个地方。

传统开发的“黄金比例”

传统软件开发中,调试和测试阶段通常占据了很大比例的时间。

根据经典的软件工程研究,集成、测试和调试阶段通常占据项目总工时的 30% 到 40% [1]。也有估算指出,开发者在验证和调试上花费的时间甚至高达 35% 到 50% [2]。

这意味着,在传统的手工编程时代,编码阶段和调试阶段时间比例大约是6:4。虽然编码看似占主要部分,但开发者依然需要花费近乎一半的时间去调试和修复问题。

AI介入的“效率悖论”

当 AI Coding Agent 介入后,开发者本以为写代码的时间会大幅缩减,从而带动整体效率起飞。但实际情况远比想象中复杂。

几项近期的对比实验揭示了 AI Coding Agent 的“双面性”:

  • 简单任务的“神速”: 在 GitHub Copilot 的一项随机对照试验(受试者实现简易HTTP服务器任务)中发现,使用AI工具时任务完成时间加速55.8% [3]。
  • 复杂场景的“翻车”: 然而,在更接近真实开发环境的 METR 组织实验中,对16位经验丰富的开源项目开发者进行RCT试验(允许一组开发者使用Cursor+Claude AI辅助,另一组不使用)时,结果却是使用AI组完成任务时间反而增加了19%[4],即AI并未加速这些老手的开发进度。开发者在实验开始前普遍预计AI会提高约24%的效率,但实验结束后使用AI的那组反而比未用AI慢了19%。

为什么资深开发者的效率反而下降了?

2025 年 Stack Overflow 的开发者调查给出了答案:66% 的开发者发现 AI 生成的代码“几乎正确,但又不完全正确”。这种“似是而非”的状态极大地增加了校对负担。更有 45.2%的受访者直言:调试 AI 生成的代码比调试人类写的代码更耗时 [5]。这些数据表明,虽然AI可以快速生成代码片段,但开发者往往需要花更多时间检查、修改和调试AI输出。

深度拆解:Debug时间变长了

既然 AI 写代码效率如此高,为什么整体进度却快不起来?我们总结了五个核心“陷阱”:

1. “几乎正确”的幻觉

METR研究者观察发现,AI 建议的方向通常是对的,但在细节上却经常“掉链子”。这种“差一点就对”的代码需要开发者进行极其细致的逐行检查,这大大增加了调试时间 [6]。

2. 额外的校对和调试工作

实验录像显示,使用 AI 的开发者频繁地在调试和清理 AI 输出的代码上耗费时间。AI 确实“写”得快,但由于不可控的错误和不贴合上下文的部分,开发者不得不反复阅读和修正 [7]。

3. 提示词工程(Prompt Engineering)

这是一种全新的时间消耗。AI辅助工具依赖自然语言提示,开发者在使用过程中为了让 AI 理解意图,需要精心构思提示词,同时也会将时间花在撰写有效提示或等待AI生成结果上 [7]。

4. 代码质量与可读性危机

AI 生成的代码有时缺乏风格一致性和上下文理解,导致维护难度增加。资深开发者反馈,AI往往生成冗长或与项目惯例不符的代码,导致他们必须“多读几遍才能看懂” [8]。数据也表明,高度依赖AI生成代码的项目可能引入更多bug和复杂度,略微降低交付速度[9]。

5. 认知负荷的转移

Cerbos博客分析指出,AI Coding Agent 会带来“表面速度”幻觉。让开发者感觉进展神速,但实际上,开发者在AI辅助环境下从传统的键盘敲击转移到更多思考和验证上,这虽然减轻了初期的编写负担,但并未减少总体工作量[8]。

下表对比了几项研究和调查中有关开发与调试时间的关键数据:

维度传统开发场景AI辅助的后变化数据来源
集成、测试和调试约30%–40%Pressman
验证和调试约占35%–50%ACM Queue
简单任务完成时间减少55.8%(提速55.8%)GitHub Copilot RCT
复杂任务完成时间增加19%(减速19%)METR RCT
开发者调研45.2%认为调试AI代码更耗时;66%认为代码“差不多但不完全对”stack Overflow

总结:开发周期真的变短了吗?

结论显而易见:目前的 AI Coding Agent 并没有显著缩短开发周期,而是将时间开销转移到了“代码验证”和“提示词工程”上。开发者普遍需要投入额外时间来审查、测试和修复AI生成的代码;同时,为了得到符合预期的输出,他们还需花费心力在有效提示设计上。

当前AI辅助开发的主要效益体现在繁琐任务自动化和认知负担减轻(如生成样板代码和文档),但在处理核心逻辑和复杂 Bug 时,人类的深度参与依然不可替代。

未来,想要真正降低Debug时间,一方面需要提高AI代码质量与可预测性,例如改进提示技巧和学习工具配合,以减少人工二次检查的需求;另一方面,由于信息传递时总是存在衰减,无论人还是AI在编程时不可避免留下Bug,因此需要有更强的Debug工具来辅助解决这些问题。在那个时代到来之前,程序员们可能还得继续在AI挖的坑里,苦练“找茬”的本领。


[1] Pressman,R.S. (2000). Software engineering: A practitioner's approach.

[2] ACM Queue. (2017). Developer time allocation in software development.

[3] Peng,S,et al. (2023). The Impact of AI on Developer Productivity.

[4] Becker,J,et al.(2025).Measuring the Impact of Early-2025 AI on Experienced Open-Source Developer Productivity.

[5] Stack Overflow. (2025). 2025 Developer Survey.

[6] Reuters.(2025). AI slows down some experienced software developers.

[7] Fortune.(2026). Does AI increase workplace productivity?

[8] Dziuba,L.(2025). The Productivity Paradox of AI Coding Assistants.

[9] Munteanu,N.(2025). Developer productivity statistics with AI coding tools (2025 report).

前言

作为一名长期深耕于外包公司的前端工程师,我大部分的项目都是使用 Vue2;此前学习的 Vue3React,却始终没有机会在实际项目中落地实践。为了避免陷入颓废、被行业淘汰的困境,我计划着手搭建个人后台管理项目,全程记录使用 Next.js 的搭建流程,同时结合官方文档与 AI 工具,一步步完成项目落地,既巩固技术,也给自己的成长留下印记。

0 开发环境及依赖版本

开发环境

我这边开发环境选的是Node.js + pnpm组合。版本管理工具用的是 Volta,它最方便的地方就是能给不同项目配置不同的Node版本,不用来回切换麻烦。
具体用法很简单,常用命令贴在这:

# 将 Node.js 安装为默认版本,安装最新的 LTS(长期支持)版本的 Node.js。
volta install node

# 安装特定版本
volta install node@16
volta install node@16.14.2

# 特定的 Node.js 版本固定到您的项目
volta pin node@16.14.2

pnpm 的话,直接用 npm install -g pnpm 命令安装就行。

我现在用的 Node.jspnpm都是最新版本,做技术嘛,就得追着最新的来,后续用到的其他技术栈也会保持最新,同时兼顾好兼容性,避免出现版本不匹配的问题。

可以使用 node -vpnpm -v 查看版本号。

因为项目是使用 Next 官方脚手架创建项目,默认给你配置好了最新的、可兼容的版本,其他的依赖直接上新版!咱使用的版本号如下:

依赖版本描述
next16.1.5Next.js 框架
react19.2.3React 核心
react-dom19.2.3React DOM 渲染
typescript^5静态类型检查
eslint^9代码检查
eslint-config-next16.1.5Next.js ESLint 规则
tailwindcss^4原子化 CSS 框架
@tailwindcss/postcss^4Tailwind CSS 编译

1. 初始化项目

1.1 创建项目

这边我使用的 Next.js 官方推荐的 create-next-app

npx create-next-app@latest

安装时,你将看到以下提示

? What is your project named? » my-app # 项目名称
? Would you like to use the recommended Next.js defaults? » - Use arrow-keys. Return to submit. # 推荐的Next.js默认值吗,
>   Yes, use recommended defaults - TypeScript, ESLint, Tailwind CSS, App Router # 是的,使用推荐的默认值-TypeScript、ESLint、Tailwind CSS、App Router
    No, reuse previous settings # 否,重复使用以前的设置
    No, customize settings # 否,自定义设置,我选这个
? Would you like to use TypeScript? » No / Yes # 你想使用TypeScript吗? Yes
? Which linter would you like to use? » - Use arrow-keys. Return to submit.# 你想选择哪种代码检查工具
>   ESLint # 选择主流
    Biome 
    None
? Would you like to use React Compiler?  # 您想使用React编译器吗?
  » No / Yes # Yes
? Would you like to use Tailwind CSS? # 您想使用Tailwind CSS 吗
  » No / Yes # Yes
? Would you like your code inside a `src/` directory? # 你想把代码放在`src/`目录中吗
  » No / Yes # Yes
? Would you like to use App Router? (recommended) 您想使用App Router吗?
  » No / Yes # Yes
? Would you like to customize the import alias (`@/*` by default)? # 是否要自定义导入别名(默认为“@/*”)
  » No / Yes # No

1.2 安装依赖

使用 VScode 打开前面创建的项目 my-app,打开终端,输入 pnpm install 安装项目所需依赖。

1.3 启动项目

查看 package.json 配置文件

{
  ...
  "scripts": {
    "dev": "next dev", // 启动开发环境服务器
    "build": "next build", // 为生产环境构建 / 打包项目
    "start": "next start", // 启动生产环境服务器
    "lint": "eslint" // 运行代码检查工具
  },
  ...
}

启动项目,测试是否运行成功:

  pnpm dev

项目正常启动后,在浏览器中访问http://localhost:3000/

若页面能正常显示,且控制台不报任何异常,则项目创建启动成功。

2. 调整项目结构

2.1 项目文件 / 文件夹作用全解析

my-app/
├─ .next/ # Next.js 开发 / 打包时自动生成的临时缓存目录
├─ node_modules # 项目所有第三方依赖包的存放目录
├─ public/ # 静态资源(图片、favicon)
├─ src/
│  ├─ app/ # App Router 的核心路由目录
│  │  ├─ layout.tsx
│  │  ├─ page.tsx
│  ├─ components/ # 可复用组件(尽量小、可组合)
│  ├─ hooks/ # 自定义 hooks(useAuth, useToast)
│  ├─ lib/ # 数据客户端、工具函数(prisma client, supabase client)
│  ├─ styles/ # globals, tailwind css entry
│  ├─ types/ # 全局类型声明
│  └─ utils/ # 小工具
├─ .env.local # 本地环境变量(不要提交)
├─ next.config.js # Next.js 项目的全局配置文件
├─ postcss.config.js # PostCSS 工具的配置文件
├─ eslint.config.mjs # ESLint 代码检查工具的配置文件
├─ tsconfig.json # TypeScript 配置文件
├─ package.json # 项目核心配置文件
└─ README.md # 项目核心配置文件

2.2 创建测试页面

App Router 是文件系统路由,即「文件 / 文件夹的路径 = 页面的 URL 路径」。我们来创建一个 /test 测试页面:

  1. src/app 目录下,新建一个名为 test 的文件夹。
  2. test 文件夹里,新建一个名为 page.tsx 的文件(这是 App Router 中 “页面文件” 的固定命名)。
  3. page.tsx 中写入测试代码:
// src/app/test/page.tsx
export default function TestPage() {
  return (
    <div style={{ padding: '2rem' }}>
      <h1>这是一个测试页面</h1>
      <p>访问路径:/test</p>
    </div>
  );
}

2.3 配置更多路由

如果你想快速体验多路由,还可以创建:

  • 首页src/app/page.tsx 就是默认的首页(访问路径 /),可以修改这个文件来定制首页内容。
  • 嵌套路由:比如创建 src/app/blog/[id]/page.tsx,就能实现动态路由 /blog/123([id] 是动态参数)。
  • 全局布局src/app/layout.tsx 是全局布局文件,所有页面都会继承这个布局(比如导航栏、页脚可以写在这里,不用每个页面重复写)。

至此,我们完成了项目的初始化和代码重构工作,包括:

  • 用 create-next-app 搭好了基础框架,整理了项目结构
  • 给项目整了个清晰的 src/ 目录结构,把业务代码和配置文件彻底分开
  • 搞定了 Tailwind CSS 和 TypeScript 的基础配置
  • 用 App Router 写了几个测试页面,验证了静态路由和动态路由的基本玩法,确保路由系统没问题
  • 把 package.json 里的脚本命令和依赖都梳理了一遍,确保启动、打包这些核心流程都跑通

END

下一篇文章里,我们来重点对 ESLint + TypeScript 进行配置 —— 主要是 .eslint.config.mjstsconfig.json 这两个核心文件,了解每个配置项的含义和作用。

做好这些配置,能帮项目规避语法错误、提前揪出类型问题,避免后续写业务时踩坑;还能提升代码可读性和可维护性,贴合 Next.js 16 + TS 5.x 的适配需求。

我也是个跟着文档和AI交流一步步摸索的菜鸟,如果你对本文讲的项目初始化、路由这些内容有疑问,或者实操时踩了坑,欢迎在评论区留言。咱们一起交流避坑.

本文由mdnice多平台发布

编者按: 如果你正在为边缘计算、本地部署或资源受限场景寻找高效的语言模型解决方案,你是否曾困惑:在众多小型语言模型(SLM)中,哪一个才是微调的最佳起点?是否真的存在“小而强”的模型,能在微调后媲美甚至超越规模大数十倍的教师模型?

近期,distil labs 团队进行了一项严谨的基准研究,或许能为你提供数据驱动的答案。他们在 8 类任务(涵盖分类、信息抽取、开卷与闭卷问答)上,对 12 个主流小型模型(包括 Qwen3、Llama、Gemma、Granite、SmolLM 等系列)进行了统一微调与评估,并对比了其与 120B 参数教师模型(GPT-OSS-120B)的性能差异。

作者 | Distil Labs

编译 | 岳扬

01 TL;DR

经过微调的小型语言模型(SLM)可以胜过规模大得多的模型:微调后的 Qwen3-4B 在 8 项基准测试中的 7 项上表现能够超越或战平 GPT-OSS-120B(一个比它模型规模大 30 倍的教师模型),剩下的一项差距也不到 3 个百分点。在 SQuAD 2.0 数据集上,微调后的学生模型甚至比教师模型高出 19 分。这意味着你只需极低的成本,就能在自己的硬件上实现前沿模型级别的准确率。

微调后性能最佳的模型:Qwen3 系列模型在微调后始终表现最强,其中 4B 版本整体表现最优。如果你的目标是在特定任务上获得最高准确率,Qwen3-4B 就是你的首选。

最具可微调性(🐟-ble)(微调收益最大):小型模型从微调中获得的提升远超大型模型。 如果你受限于使用非常小的模型(1B–3B),也不必担心 —— 它们能从微调中获益最多,能够大幅缩小与更大模型之间的性能差距。

02 引言

如果你正在构建需要在设备端、本地或边缘侧运行的 AI 应用,你很可能问过自己:我该微调哪个小型语言模型(SLM)?目前 SLM 领域选择众多(Qwen、Llama、Gemma、Granite、SmolLM),每个系列都提供多种模型规模的版本。选错基础模型可能意味着有数周时间在浪费计算资源,或者得到的模型始终无法达到生产质量要求。

我们进行了一项系统的基准测试,用数据来回答这个问题。借助 distil labs 平台,我们在 8 个不同的任务上(分类、信息抽取、开卷问答、闭卷问答)微调了 12 个模型,然后将它们的性能相互比较,并与用于生成合成训练数据的教师大模型进行对比。

本文回答了四个实际问题:

  • 哪个模型在微调后效果最好?
  • 哪个模型最具可微调性?(即微调后提升最大)
  • 哪个模型的基础性能最强?(即未经微调前)
  • 我们表现最好的学生模型,真的能媲美教师模型吗?

03 实验方法

我们评估了以下模型:

  • Qwen3 系列:Qwen3-8B、Qwen3-4B-Instruct-2507、Qwen3-1.7B、Qwen3-0.6B。注意,我们关闭了该系列的“thinking”功能,以保证实验的公平。  
  • Llama 系列:Llama-3.1-8B-Instruct、Llama-3.2-3B-Instruct、Llama-3.2-1B-Instruct  
  • SmolLM2 系列:SmolLM2-1.7B-Instruct、SmolLM2-135M-Instruct  
  • Gemma 系列:gemma-3-1b-it、gemma-3-270m-it  
  • Granite:granite-3.3-8b-instruct  

针对每个模型,我们测量了:

  • Base score:仅使用提示词(prompting)的小样本(few-shot)场景下的性能  
  • Finetuned score:在由我们的教师模型(GPT-OSS 120B)生成的合成数据上微调后的性能  

我们的 8 项基准测试涵盖分类(TREC、Banking77、Ecommerce、Mental Health)、文档理解(docs)以及问答任务(HotpotQA、Roman Empire QA、SQuAD 2.0)。

为了实现公平测量,我们分别计算了每个模型在各个基准测试上的排名,然后计算所有任务上的平均排名,并以 95% 置信区间作为误差棒(error bars)绘制在图中。平均排名越低,表示整体性能越好。

04 问题一:哪个模型在微调后效果最好?

冠军:Qwen3-4B-Instruct-2507(平均排名:2.25)

Qwen3 系列占据了排行榜前列,其中 Qwen3-4B-Instruct-2507 摘得桂冠。值得注意的是,这款 4B 模型的表现甚至超过了更大的 Qwen3-8B,这表明在蒸馏任务中,Qwen3 的较新版本(2025 年 7 月 25 日更新的版本)比之前的 8B SLM 效果更好。

核心结论:如果你希望获得效果最好的微调模型,并且拥有支持约 4B 参数规模模型微调的 GPU 显存,那么 Qwen3-4B-Instruct-2507 是你的首选。

05 问题二:哪个模型最具可微调性?(即微调后提升最大)

冠军: Llama-3.2-1B-Instruct(平均排名:3.44)

这里我们测量的是可微调性(tunability) —— 即从基础性能到微调后性能的提升幅度(finetuned_score - base_score)。一个高度可微调的模型初始表现可能较弱,但经过微调后提升显著。

有趣的是,可微调性排名与模型大小的排序正好相反。像 Llama-3.2-1B 和 Qwen3-0.6B 这样的小型模型,从微调中获得的提升最大。而规模最大的模型(如 Qwen3-8B、granite-3.3-8b)在可微调性排名中接近垫底 —— 这并非因为它们表现差,而是因为它们起点相对较高,进步空间相对有限。

核心结论:如果你受限于使用极小的模型(<2B 参数),不必灰心。这些模型从微调中获益最大,并且能够显著缩小与更大模型之间的性能差距。

06 问题三:哪个模型的基础性能最强?(即未经微调前)

冠军: Qwen3-8B (平均排名: 1.75)

在未经任何微调的情况下,哪个模型开箱即用的表现最好?

正如预期,基础性能与模型大小呈正相关。8B 模型占据了榜首位置,其中 Qwen3-8B 在所有基准测试中都展现出非常稳定的性能(标准差最低)。

核心结论:如果你需要在不进行微调的情况下在零样本/小样本场景下也获得较优的性能,大模型仍是你的最佳选择。但请记住 —— 经过微调后,这种优势会减弱。

07 问题四:我们表现最好的学生模型,真的能媲美教师模型吗?

是的。Qwen3-4B-Instruct-2507 在 8 项基准测试中的 7 项上达到或超越了教师模型。

经过微调的 4B 学生模型在 6 项基准测试上超越了 120B+ 参数的教师模型,在 1 项(HotpotQA)上持平,仅在 1 项(Banking77)上略微落后(差距在误差范围内)。提升最显著的是 SQuAD 2.0 闭卷问答任务,学生模型比教师模型高出 19 个百分点 —— 这充分证明,微调比单纯依赖提示词(prompting)能更有效地将领域知识注入模型。

核心结论:一个经过适当微调的 4B 参数模型,可以媲美甚至超越规模达其 30 倍的模型。这意味着推理成本可降低约 30 倍,并且能够完全在本地部署运行。

08 实用建议

基于我们的基准测试结果,以下是选择基础模型的建议:

09 后续我们将进行的工作

本次基准测试只是一个起点,我们正在积极努力让这些结果更加可靠:

  • 评估更多模型:SLM 领域发展迅速。我们计划在 Qwen3.5、Phi-4 和 Mistral 系列等新模型版本发布后及时纳入评测。
  • 增加运行轮次:目前我们的结果基于有限次数的运行取平均。我们将为每项基准测试增加更多运行轮次,以缩小置信区间,确保排名具有统计可靠性。
  • 扩展基准测试覆盖范围:我们希望纳入更多任务类型,如文本摘要、代码生成和多轮对话,从而更全面地反映模型能力。

10 训练细节

每个模型都在使用我们蒸馏流程生成的合成数据进行微调(有关数据合成过程的详细信息,请参见《Small Expert Agents from 10 Examples》[1])。针对每个基准测试,我们使用教师模型(GPTOss-120B)生成了 10,000 条训练样本。

微调采用 distil labs 的默认配置[2]:训练 4 个 epoch,学习率 5e-5,使用线性学习率调度器,以及 rank 为 64 的 LoRA。

所有模型均使用完全相同的超参数进行训练。评估在训练和合成数据生成过程中均未接触过的预留测试集上进行。

11 结论

并非所有小型模型的性能都差不多,但经过微调后,它们之间的差距会大幅缩小。我们的基准测试表明,Qwen3-4B-Instruct-2507 在整体微调性能上表现最佳,不仅能媲美 120B+ 参数的教师模型,还能在单块消费级 GPU 上部署运行。在资源极度受限的环境中,像 Llama-3.2-1B 这样的小模型展现出卓越的可微调性,能够大幅缩小与大模型的性能差距。

核心结论:微调比基础模型的选择更重要。一个经过良好微调的 1B 模型,可以胜过仅靠提示词(prompting)驱动的 8B 模型。

END

本期互动内容 🍻

❓你在微调小型语言模型时,最看重的是“开箱即用的强基础能力”,还是“微调后巨大的提升空间”?为什么?

文中链接

[1]https://www.distillabs.ai/blog/small-expert-agents-from-10-ex...

[2]https://docs.distillabs.ai/how-to/input-preparation/config

原文链接:

https://www.distillabs.ai/blog/we-benchmarked-12-small-langua...

大家好,我是V哥!今天要跟大家分享一个超级干货——如何在鸿蒙6(API21)上开发一个真正能用的AI智能体。不是那种玩具级别的Demo,而是能语音对话、能理解你意图、还能帮你干活的智能助手!

一、为什么要在鸿蒙上做AI智能体?

兄弟们,2026年了,AI Agent(智能体)绝对是最火的技术方向之一。什么是智能体?简单说就是:能感知、能思考、能行动的AI程序

鸿蒙6在AI这块可以说是下了血本:

  • 原生AI能力:MindSpore Lite端侧推理引擎
  • 语音能力:ASR语音识别 + TTS语音合成
  • 意图识别:智能理解用户需求
  • 大模型接入:轻松对接各种LLM API

今天V哥就手把手带你做一个多模态AI智能助手,它能:

  1. ✅ 语音唤醒,开口就能聊
  2. ✅ 智能对话,接入大模型
  3. ✅ 意图识别,理解你想干嘛
  4. ✅ 执行任务,帮你打开应用、设置闹钟等
  5. ✅ 多轮记忆,上下文连贯

废话不多说,直接上代码!


二、项目架构设计

┌─────────────────────────────────────────────────────────────────┐
│                      AI智能体架构(V哥设计)                      │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   ┌─────────────┐    ┌─────────────┐    ┌─────────────┐        │
│   │  语音输入   │───▶│  语音识别   │───▶│  意图理解   │        │
│   │   (ASR)    │    │   Engine    │    │   Engine    │        │
│   └─────────────┘    └─────────────┘    └──────┬──────┘        │
│                                                 │               │
│                                                 ▼               │
│   ┌─────────────┐    ┌─────────────┐    ┌─────────────┐        │
│   │  语音输出   │◀───│  回复生成   │◀───│  对话管理   │        │
│   │   (TTS)    │    │   (LLM)    │    │   Agent    │        │
│   └─────────────┘    └─────────────┘    └──────┬──────┘        │
│                                                 │               │
│                                                 ▼               │
│                                        ┌─────────────┐         │
│                                        │  任务执行   │         │
│                                        │  Actions   │         │
│                                        └─────────────┘         │
└─────────────────────────────────────────────────────────────────┘

三、项目创建与配置

步骤1:创建项目

DevEco Studio → New Project
→ Empty Ability (Stage模型)
→ Project name: VGeAIAgent
→ Bundle name: com.vge.aiagent
→ Compile SDK: 5.0.0(API 12) 或更高

步骤2:配置 module.json5

{
  "module": {
    "name": "entry",
    "type": "entry",
    "description": "$string:module_desc",
    "mainElement": "EntryAbility",
    "deviceTypes": ["phone", "tablet"],
    "deliveryWithInstall": true,
    "installationFree": false,
    "pages": "$profile:main_pages",
    "abilities": [
      {
        "name": "EntryAbility",
        "srcEntry": "./ets/entryability/EntryAbility.ets",
        "description": "$string:EntryAbility_desc",
        "icon": "$media:icon",
        "label": "$string:EntryAbility_label",
        "startWindowIcon": "$media:startIcon",
        "startWindowBackground": "$color:start_window_background",
        "exported": true,
        "skills": [
          {
            "entities": ["entity.system.home"],
            "actions": ["action.system.home"]
          }
        ]
      }
    ],
    "requestPermissions": [
      {
        "name": "ohos.permission.MICROPHONE",
        "reason": "$string:mic_reason",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      },
      {
        "name": "ohos.permission.INTERNET",
        "reason": "$string:net_reason",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "always"
        }
      },
      {
        "name": "ohos.permission.DISTRIBUTED_DATASYNC",
        "reason": "$string:sync_reason",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "always"
        }
      }
    ]
  }
}

四、核心代码实现

1. 消息数据模型 (model/MessageModel.ets)

// entry/src/main/ets/model/MessageModel.ets

/**
 * V哥设计的消息模型
 * 支持多种消息类型,为后续扩展预留空间
 */

// 消息角色
export enum MessageRole {
  USER = 'user',           // 用户消息
  ASSISTANT = 'assistant', // AI助手消息
  SYSTEM = 'system'        // 系统消息
}

// 消息类型
export enum MessageType {
  TEXT = 'text',           // 文本消息
  VOICE = 'voice',         // 语音消息
  ACTION = 'action',       // 执行动作
  THINKING = 'thinking'    // 思考中
}

// 意图类型
export enum IntentType {
  CHAT = 'chat',                    // 闲聊
  OPEN_APP = 'open_app',            // 打开应用
  SET_ALARM = 'set_alarm',          // 设置闹钟
  SET_REMINDER = 'set_reminder',    // 设置提醒
  QUERY_WEATHER = 'query_weather',  // 查询天气
  QUERY_TIME = 'query_time',        // 查询时间
  CONTROL_DEVICE = 'control_device',// 控制设备
  UNKNOWN = 'unknown'               // 未知意图
}

// 消息实体
export class Message {
  id: string = '';
  role: MessageRole = MessageRole.USER;
  type: MessageType = MessageType.TEXT;
  content: string = '';
  timestamp: number = 0;
  intent?: IntentType;
  intentParams?: Record<string, string>;
  isStreaming?: boolean;  // 是否流式输出中

  constructor(init?: Partial<Message>) {
    this.id = `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
    this.timestamp = Date.now();
    if (init) {
      Object.assign(this, init);
    }
  }
}

// 对话上下文(用于多轮对话)
export class ConversationContext {
  messages: Message[] = [];
  maxHistory: number = 10;  // 最多保留10轮对话

  addMessage(message: Message): void {
    this.messages.push(message);
    // 超过限制则移除最早的消息
    if (this.messages.length > this.maxHistory * 2) {
      this.messages = this.messages.slice(-this.maxHistory * 2);
    }
  }

  getHistory(): Message[] {
    return this.messages;
  }

  clear(): void {
    this.messages = [];
  }

  // 转换为LLM API需要的格式
  toAPIFormat(): Array<{role: string, content: string}> {
    return this.messages
      .filter(m => m.type === MessageType.TEXT)
      .map(m => ({
        role: m.role,
        content: m.content
      }));
  }
}

2. 意图识别引擎 (engine/IntentEngine.ets)

// entry/src/main/ets/engine/IntentEngine.ets

import { IntentType } from '../model/MessageModel';

/**
 * V哥的意图识别引擎
 * 使用规则+关键词匹配,生产环境可接入NLU模型
 */

interface IntentRule {
  intent: IntentType;
  keywords: string[];
  patterns: RegExp[];
  extractor?: (text: string) => Record<string, string>;
}

export class IntentEngine {
  private static instance: IntentEngine;
  private rules: IntentRule[] = [];

  private constructor() {
    this.initRules();
  }

  static getInstance(): IntentEngine {
    if (!IntentEngine.instance) {
      IntentEngine.instance = new IntentEngine();
    }
    return IntentEngine.instance;
  }

  /**
   * 初始化意图规则
   */
  private initRules(): void {
    this.rules = [
      // 打开应用
      {
        intent: IntentType.OPEN_APP,
        keywords: ['打开', '启动', '运行', '开启'],
        patterns: [
          /打开(.+?)(?:应用|app|APP)?$/,
          /启动(.+)/,
          /帮我开(.+)/
        ],
        extractor: (text: string) => {
          const appNames: Record<string, string> = {
            '相机': 'com.huawei.camera',
            '相册': 'com.huawei.photos',
            '设置': 'com.huawei.settings',
            '日历': 'com.huawei.calendar',
            '计算器': 'com.huawei.calculator',
            '备忘录': 'com.huawei.notes',
            '音乐': 'com.huawei.music',
            '视频': 'com.huawei.video',
            '浏览器': 'com.huawei.browser',
            '微信': 'com.tencent.mm',
            '支付宝': 'com.eg.android.AlipayGphone',
            '抖音': 'com.ss.android.ugc.aweme'
          };

          for (const [name, bundleName] of Object.entries(appNames)) {
            if (text.includes(name)) {
              return { appName: name, bundleName: bundleName };
            }
          }
          return {};
        }
      },

      // 设置闹钟
      {
        intent: IntentType.SET_ALARM,
        keywords: ['闹钟', '叫我', '提醒我起床', '定个闹钟'],
        patterns: [
          /(\d{1,2})[点::](\d{0,2}).*(?:闹钟|叫我|起床)/,
          /(?:明天|后天)?(?:早上|上午|中午|下午|晚上)?(\d{1,2})[点::]?(\d{0,2})?.*(?:闹钟|叫我)/,
          /设.*闹钟.*(\d{1,2})[点::](\d{0,2})?/
        ],
        extractor: (text: string) => {
          const timeMatch = text.match(/(\d{1,2})[点::](\d{0,2})?/);
          if (timeMatch) {
            const hour = timeMatch[1];
            const minute = timeMatch[2] || '00';
            return { hour, minute };
          }
          return {};
        }
      },

      // 设置提醒
      {
        intent: IntentType.SET_REMINDER,
        keywords: ['提醒我', '别忘了', '记得'],
        patterns: [
          /(\d+)(?:分钟|小时)后提醒我(.+)/,
          /提醒我(.+)/,
          /(\d{1,2})[点::](\d{0,2})?提醒我(.+)/
        ],
        extractor: (text: string) => {
          // 提取时间和内容
          const minuteMatch = text.match(/(\d+)分钟后提醒我(.+)/);
          if (minuteMatch) {
            return {
              delayMinutes: minuteMatch[1],
              content: minuteMatch[2]
            };
          }

          const hourMatch = text.match(/(\d+)小时后提醒我(.+)/);
          if (hourMatch) {
            return {
              delayMinutes: String(parseInt(hourMatch[1]) * 60),
              content: hourMatch[2]
            };
          }

          const contentMatch = text.match(/提醒我(.+)/);
          if (contentMatch) {
            return { content: contentMatch[1] };
          }

          return {};
        }
      },

      // 查询天气
      {
        intent: IntentType.QUERY_WEATHER,
        keywords: ['天气', '下雨', '温度', '气温', '穿什么'],
        patterns: [
          /(.+?)(?:的)?天气/,
          /(?:今天|明天|后天).*(?:天气|下雨|温度)/,
          /要不要带伞/
        ],
        extractor: (text: string) => {
          const cityMatch = text.match(/(.{2,4}?)(?:的)?天气/);
          if (cityMatch && !['今天', '明天', '后天', '这里', '现在'].includes(cityMatch[1])) {
            return { city: cityMatch[1] };
          }
          return { city: '北京' };  // 默认城市
        }
      },

      // 查询时间
      {
        intent: IntentType.QUERY_TIME,
        keywords: ['几点', '时间', '日期', '星期几', '今天几号'],
        patterns: [
          /现在几点/,
          /什么时间/,
          /今天.*(?:几号|星期几|周几)/
        ],
        extractor: () => ({})
      },

      // 控制设备
      {
        intent: IntentType.CONTROL_DEVICE,
        keywords: ['打开灯', '关灯', '开灯', '空调', '电视', '窗帘'],
        patterns: [
          /(打开|关闭|开|关)(.+?)(?:灯|空调|电视|窗帘)/,
          /把(.+?)(打开|关闭|开|关)/,
          /(.+?)(?:调到|设置为?)(\d+)度/
        ],
        extractor: (text: string) => {
          const actionMatch = text.match(/(打开|关闭|开|关)(.+)/);
          if (actionMatch) {
            return {
              action: actionMatch[1].includes('开') ? 'on' : 'off',
              device: actionMatch[2]
            };
          }
          return {};
        }
      }
    ];
  }

  /**
   * 识别用户意图
   */
  recognize(text: string): { intent: IntentType; params: Record<string, string>; confidence: number } {
    const normalizedText = text.toLowerCase().trim();

    for (const rule of this.rules) {
      // 关键词匹配
      const keywordMatch = rule.keywords.some(kw => normalizedText.includes(kw));

      // 正则匹配
      const patternMatch = rule.patterns.some(pattern => pattern.test(normalizedText));

      if (keywordMatch || patternMatch) {
        const params = rule.extractor ? rule.extractor(normalizedText) : {};
        const confidence = keywordMatch && patternMatch ? 0.95 : 0.75;

        console.info(`[IntentEngine] 识别结果: ${rule.intent}, 置信度: ${confidence}`);
        return {
          intent: rule.intent,
          params,
          confidence
        };
      }
    }

    // 默认为闲聊
    return {
      intent: IntentType.CHAT,
      params: {},
      confidence: 0.5
    };
  }
}

3. 大模型对话服务 (service/LLMService.ets)

// entry/src/main/ets/service/LLMService.ets

import { http } from '@kit.NetworkKit';
import { ConversationContext, Message, MessageRole } from '../model/MessageModel';
import { BusinessError } from '@kit.BasicServicesKit';

/**
 * V哥的LLM服务封装
 * 支持多种大模型API,这里以通用格式为例
 */

// LLM配置接口
interface LLMConfig {
  apiUrl: string;
  apiKey: string;
  model: string;
  maxTokens: number;
  temperature: number;
}

// API请求格式
interface ChatCompletionRequest {
  model: string;
  messages: Array<{ role: string; content: string }>;
  max_tokens: number;
  temperature: number;
  stream: boolean;
}

// API响应格式
interface ChatCompletionResponse {
  id: string;
  choices: Array<{
    message: {
      role: string;
      content: string;
    };
    finish_reason: string;
  }>;
  usage: {
    prompt_tokens: number;
    completion_tokens: number;
    total_tokens: number;
  };
}

export class LLMService {
  private static instance: LLMService;
  private config: LLMConfig;
  private systemPrompt: string;

  private constructor() {
    // 默认配置(实际使用时替换为你的API信息)
    this.config = {
      apiUrl: 'https://api.openai.com/v1/chat/completions',  // 或其他兼容API
      apiKey: 'your-api-key-here',  // 替换为你的API Key
      model: 'gpt-3.5-turbo',
      maxTokens: 2048,
      temperature: 0.7
    };

    // 系统提示词 - V哥精心调教
    this.systemPrompt = `你是一个运行在鸿蒙系统上的AI智能助手,名叫"小V助手"。

你的特点:
1. 友好、幽默、专业
2. 回答简洁有力,不啰嗦
3. 能理解用户意图,给出实用建议
4. 熟悉鸿蒙生态和华为设备
5. 在适当时候使用emoji增加亲和力

你可以帮用户:
- 回答各种问题
- 闲聊解闷
- 提供建议和帮助
- 解释技术概念

请用中文回复,保持回答在100字以内(除非用户明确要求详细解释)。`;
  }

  static getInstance(): LLMService {
    if (!LLMService.instance) {
      LLMService.instance = new LLMService();
    }
    return LLMService.instance;
  }

  /**
   * 更新配置
   */
  updateConfig(config: Partial<LLMConfig>): void {
    this.config = { ...this.config, ...config };
  }

  /**
   * 发送对话请求
   */
  async chat(userMessage: string, context: ConversationContext): Promise<string> {
    // 构建消息历史
    const messages: Array<{ role: string; content: string }> = [
      { role: 'system', content: this.systemPrompt },
      ...context.toAPIFormat(),
      { role: 'user', content: userMessage }
    ];

    const requestData: ChatCompletionRequest = {
      model: this.config.model,
      messages: messages,
      max_tokens: this.config.maxTokens,
      temperature: this.config.temperature,
      stream: false
    };

    try {
      const httpRequest = http.createHttp();

      const response = await httpRequest.request(
        this.config.apiUrl,
        {
          method: http.RequestMethod.POST,
          header: {
            'Content-Type': 'application/json',
            'Authorization': `Bearer ${this.config.apiKey}`
          },
          extraData: JSON.stringify(requestData),
          connectTimeout: 30000,
          readTimeout: 60000
        }
      );

      httpRequest.destroy();

      if (response.responseCode === 200) {
        const result = JSON.parse(response.result as string) as ChatCompletionResponse;
        const content = result.choices[0]?.message?.content || '抱歉,我没有理解你的意思';
        console.info(`[LLMService] 响应成功,Token使用: ${result.usage?.total_tokens}`);
        return content;
      } else {
        console.error(`[LLMService] API错误: ${response.responseCode}`);
        return this.getFallbackResponse(userMessage);
      }
    } catch (err) {
      const error = err as BusinessError;
      console.error(`[LLMService] 请求失败: ${error.code} - ${error.message}`);
      return this.getFallbackResponse(userMessage);
    }
  }

  /**
   * 离线兜底回复(当API不可用时)
   */
  private getFallbackResponse(userMessage: string): string {
    const fallbackResponses: Record<string, string[]> = {
      '你好': ['你好呀!有什么可以帮你的?', '嗨!我是小V助手,很高兴见到你!'],
      '谢谢': ['不客气!随时为你服务~', '应该的,还有什么需要帮助的吗?'],
      '再见': ['再见!期待下次聊天~', '拜拜,有事随时找我哦!'],
      '你是谁': ['我是小V助手,运行在鸿蒙系统上的AI助手!', '我叫小V,是V哥打造的智能助手~'],
      '你能做什么': ['我能陪你聊天、回答问题、帮你打开应用、设置提醒等等!试试看吧~', 
                   '我可以:闲聊解闷、回答问题、控制设备、设置闹钟提醒...功能多多!']
    };

    // 关键词匹配
    for (const [keyword, responses] of Object.entries(fallbackResponses)) {
      if (userMessage.includes(keyword)) {
        return responses[Math.floor(Math.random() * responses.length)];
      }
    }

    // 默认回复
    const defaultResponses = [
      '我现在网络不太好,稍后再试试吧~',
      '让我想想... 你能换个方式问我吗?',
      '抱歉,我没太理解,能再说一遍吗?',
      '网络开小差了,不过我们可以继续聊别的!'
    ];

    return defaultResponses[Math.floor(Math.random() * defaultResponses.length)];
  }

  /**
   * 流式对话(支持打字机效果)
   */
  async chatStream(
    userMessage: string,
    context: ConversationContext,
    onChunk: (chunk: string) => void,
    onComplete: (fullText: string) => void
  ): Promise<void> {
    // 简化实现:模拟流式输出
    const response = await this.chat(userMessage, context);

    let index = 0;
    const interval = setInterval(() => {
      if (index < response.length) {
        onChunk(response[index]);
        index++;
      } else {
        clearInterval(interval);
        onComplete(response);
      }
    }, 30);  // 每30ms输出一个字符
  }
}

4. 语音服务封装 (service/VoiceService.ets)

// entry/src/main/ets/service/VoiceService.ets

import { speechRecognizer } from '@kit.CoreSpeechKit';
import { textToSpeech } from '@kit.CoreSpeechKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { abilityAccessCtrl, Permissions } from '@kit.AbilityKit';

/**
 * V哥的语音服务封装
 * 整合ASR语音识别 + TTS语音合成
 */

export class VoiceService {
  private static instance: VoiceService;
  private asrEngine: speechRecognizer.SpeechRecognitionEngine | null = null;
  private ttsEngine: textToSpeech.TextToSpeechEngine | null = null;
  private isListening: boolean = false;

  private constructor() {}

  static getInstance(): VoiceService {
    if (!VoiceService.instance) {
      VoiceService.instance = new VoiceService();
    }
    return VoiceService.instance;
  }

  /**
   * 请求麦克风权限
   */
  async requestPermission(context: Context): Promise<boolean> {
    const atManager = abilityAccessCtrl.createAtManager();
    const permissions: Permissions[] = ['ohos.permission.MICROPHONE'];

    try {
      const result = await atManager.requestPermissionsFromUser(context, permissions);
      const granted = result.authResults.every(r => r === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED);
      console.info(`[VoiceService] 麦克风权限: ${granted ? '已授权' : '被拒绝'}`);
      return granted;
    } catch (err) {
      console.error('[VoiceService] 请求权限失败:', JSON.stringify(err));
      return false;
    }
  }

  /**
   * 初始化语音识别引擎
   */
  async initASR(): Promise<boolean> {
    try {
      const createParams: speechRecognizer.CreateEngineParams = {
        language: 'zh-CN',
        online: 1  // 1-在线识别 0-离线识别
      };

      this.asrEngine = await speechRecognizer.createEngine(createParams);
      console.info('[VoiceService] ASR引擎初始化成功');
      return true;
    } catch (err) {
      const error = err as BusinessError;
      console.error(`[VoiceService] ASR初始化失败: ${error.code} - ${error.message}`);
      return false;
    }
  }

  /**
   * 初始化语音合成引擎
   */
  async initTTS(): Promise<boolean> {
    try {
      const createParams: textToSpeech.CreateEngineParams = {
        language: 'zh-CN',
        person: 0,  // 发音人
        online: 1   // 1-在线合成 0-离线合成
      };

      const extraParams: Record<string, Object> = {
        style: 'normal',
        speed: 1.0,
        volume: 1.0,
        pitch: 1.0
      };

      this.ttsEngine = await textToSpeech.createEngine(createParams);
      console.info('[VoiceService] TTS引擎初始化成功');
      return true;
    } catch (err) {
      const error = err as BusinessError;
      console.error(`[VoiceService] TTS初始化失败: ${error.code} - ${error.message}`);
      return false;
    }
  }

  /**
   * 开始语音识别
   */
  async startListening(
    onResult: (text: string, isFinal: boolean) => void,
    onError: (error: string) => void
  ): Promise<void> {
    if (!this.asrEngine) {
      const success = await this.initASR();
      if (!success) {
        onError('语音识别引擎初始化失败');
        return;
      }
    }

    if (this.isListening) {
      console.warn('[VoiceService] 已经在监听中');
      return;
    }

    try {
      // 设置回调
      this.asrEngine!.setListener({
        onStart: (sessionId: string) => {
          console.info(`[VoiceService] 开始识别, sessionId: ${sessionId}`);
          this.isListening = true;
        },
        onEvent: (sessionId: string, eventCode: number) => {
          console.info(`[VoiceService] 事件: ${eventCode}`);
        },
        onResult: (sessionId: string, result: speechRecognizer.SpeechRecognitionResult) => {
          const text = result.result;
          const isFinal = result.isFinal;
          console.info(`[VoiceService] 识别结果: ${text}, isFinal: ${isFinal}`);
          onResult(text, isFinal);
        },
        onComplete: (sessionId: string) => {
          console.info(`[VoiceService] 识别完成`);
          this.isListening = false;
        },
        onError: (sessionId: string, errorCode: number, errorMessage: string) => {
          console.error(`[VoiceService] 识别错误: ${errorCode} - ${errorMessage}`);
          this.isListening = false;
          onError(errorMessage);
        }
      });

      // 开始识别
      const recognitionParams: speechRecognizer.StartParams = {
        sessionId: `session_${Date.now()}`,
        audioInfo: {
          audioType: 'pcm',
          sampleRate: 16000,
          soundChannel: 1,
          sampleBit: 16
        },
        extraParams: {
          vadBegin: 2000,  // 静音检测开始时间
          vadEnd: 3000,    // 静音检测结束时间
          maxAudioDuration: 60000  // 最大录音时长
        }
      };

      await this.asrEngine!.startListening(recognitionParams);
    } catch (err) {
      const error = err as BusinessError;
      console.error(`[VoiceService] 开始识别失败: ${error.code} - ${error.message}`);
      onError(error.message);
    }
  }

  /**
   * 停止语音识别
   */
  async stopListening(): Promise<void> {
    if (this.asrEngine && this.isListening) {
      try {
        await this.asrEngine.finish(`session_stop_${Date.now()}`);
        this.isListening = false;
        console.info('[VoiceService] 停止识别');
      } catch (err) {
        console.error('[VoiceService] 停止识别失败:', JSON.stringify(err));
      }
    }
  }

  /**
   * 语音合成(文字转语音)
   */
  async speak(text: string, onComplete?: () => void): Promise<void> {
    if (!this.ttsEngine) {
      const success = await this.initTTS();
      if (!success) {
        console.error('[VoiceService] TTS引擎不可用');
        onComplete?.();
        return;
      }
    }

    try {
      // 设置回调
      this.ttsEngine!.setListener({
        onStart: (requestId: string) => {
          console.info(`[VoiceService] 开始播放, requestId: ${requestId}`);
        },
        onProgress: (requestId: string, progress: number) => {
          // 播放进度
        },
        onFinish: (requestId: string) => {
          console.info(`[VoiceService] 播放完成`);
          onComplete?.();
        },
        onError: (requestId: string, errorCode: number, errorMessage: string) => {
          console.error(`[VoiceService] 播放错误: ${errorCode} - ${errorMessage}`);
          onComplete?.();
        }
      });

      // 合成参数
      const speakParams: textToSpeech.SpeakParams = {
        requestId: `speak_${Date.now()}`,
        extraParams: {
          speed: 1.0,
          volume: 1.0,
          pitch: 1.0
        }
      };

      await this.ttsEngine!.speak(text, speakParams);
    } catch (err) {
      const error = err as BusinessError;
      console.error(`[VoiceService] 语音合成失败: ${error.code} - ${error.message}`);
      onComplete?.();
    }
  }

  /**
   * 停止语音播放
   */
  async stopSpeaking(): Promise<void> {
    if (this.ttsEngine) {
      try {
        await this.ttsEngine.stop();
        console.info('[VoiceService] 停止播放');
      } catch (err) {
        console.error('[VoiceService] 停止播放失败:', JSON.stringify(err));
      }
    }
  }

  /**
   * 释放资源
   */
  async release(): Promise<void> {
    try {
      if (this.asrEngine) {
        await this.asrEngine.shutdown();
        this.asrEngine = null;
      }
      if (this.ttsEngine) {
        await this.ttsEngine.shutdown();
        this.ttsEngine = null;
      }
      console.info('[VoiceService] 资源释放完成');
    } catch (err) {
      console.error('[VoiceService] 释放资源失败:', JSON.stringify(err));
    }
  }

  /**
   * 获取监听状态
   */
  getListeningState(): boolean {
    return this.isListening;
  }
}

5. 任务执行器 (engine/ActionExecutor.ets)

// entry/src/main/ets/engine/ActionExecutor.ets

import { bundleManager, common, Want } from '@kit.AbilityKit';
import { IntentType } from '../model/MessageModel';
import { BusinessError } from '@kit.BasicServicesKit';

/**
 * V哥的任务执行器
 * 根据意图执行具体操作
 */

interface ActionResult {
  success: boolean;
  message: string;
  data?: object;
}

export class ActionExecutor {
  private static instance: ActionExecutor;
  private context: common.UIAbilityContext | null = null;

  private constructor() {}

  static getInstance(): ActionExecutor {
    if (!ActionExecutor.instance) {
      ActionExecutor.instance = new ActionExecutor();
    }
    return ActionExecutor.instance;
  }

  /**
   * 设置上下文
   */
  setContext(context: common.UIAbilityContext): void {
    this.context = context;
  }

  /**
   * 执行动作
   */
  async execute(intent: IntentType, params: Record<string, string>): Promise<ActionResult> {
    console.info(`[ActionExecutor] 执行意图: ${intent}, 参数: ${JSON.stringify(params)}`);

    switch (intent) {
      case IntentType.OPEN_APP:
        return this.openApp(params);

      case IntentType.SET_ALARM:
        return this.setAlarm(params);

      case IntentType.SET_REMINDER:
        return this.setReminder(params);

      case IntentType.QUERY_WEATHER:
        return this.queryWeather(params);

      case IntentType.QUERY_TIME:
        return this.queryTime();

      case IntentType.CONTROL_DEVICE:
        return this.controlDevice(params);

      default:
        return {
          success: false,
          message: '暂不支持该操作'
        };
    }
  }

  /**
   * 打开应用
   */
  private async openApp(params: Record<string, string>): Promise<ActionResult> {
    const bundleName = params.bundleName;
    const appName = params.appName;

    if (!bundleName) {
      return {
        success: false,
        message: `抱歉,我不知道怎么打开"${appName || '这个应用'}"`
      };
    }

    try {
      const want: Want = {
        bundleName: bundleName,
        action: 'action.system.home',
        entities: ['entity.system.home']
      };

      await this.context?.startAbility(want);

      return {
        success: true,
        message: `已为你打开${appName}`
      };
    } catch (err) {
      const error = err as BusinessError;
      console.error(`[ActionExecutor] 打开应用失败: ${error.code} - ${error.message}`);

      return {
        success: false,
        message: `打开${appName}失败,可能是应用未安装`
      };
    }
  }

  /**
   * 设置闹钟
   */
  private async setAlarm(params: Record<string, string>): Promise<ActionResult> {
    const hour = parseInt(params.hour || '8');
    const minute = parseInt(params.minute || '0');

    try {
      // 调用系统闹钟
      const want: Want = {
        action: 'ohos.want.action.setAlarm',
        parameters: {
          'ringtone': 'default',
          'hour': hour,
          'minute': minute
        }
      };

      await this.context?.startAbility(want);

      const timeStr = `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`;
      return {
        success: true,
        message: `好的,已为你设置${timeStr}的闹钟`
      };
    } catch (err) {
      console.error('[ActionExecutor] 设置闹钟失败:', JSON.stringify(err));

      return {
        success: false,
        message: '设置闹钟失败,请手动设置'
      };
    }
  }

  /**
   * 设置提醒
   */
  private async setReminder(params: Record<string, string>): Promise<ActionResult> {
    const content = params.content || '未命名提醒';
    const delayMinutes = parseInt(params.delayMinutes || '10');

    // 这里可以接入前面的日程提醒模块
    return {
      success: true,
      message: `收到!${delayMinutes}分钟后提醒你:${content}`
    };
  }

  /**
   * 查询天气
   */
  private async queryWeather(params: Record<string, string>): Promise<ActionResult> {
    const city = params.city || '北京';

    // 实际项目中对接天气API
    // 这里返回模拟数据
    const mockWeather = {
      city: city,
      temperature: Math.floor(Math.random() * 20) + 10,
      weather: ['晴', '多云', '阴', '小雨'][Math.floor(Math.random() * 4)],
      humidity: Math.floor(Math.random() * 40) + 40
    };

    return {
      success: true,
      message: `${city}今天${mockWeather.weather},气温${mockWeather.temperature}°C,湿度${mockWeather.humidity}%`,
      data: mockWeather
    };
  }

  /**
   * 查询时间
   */
  private queryTime(): ActionResult {
    const now = new Date();
    const weekDays = ['日', '一', '二', '三', '四', '五', '六'];

    const dateStr = `${now.getFullYear()}年${now.getMonth() + 1}月${now.getDate()}日`;
    const weekStr = `星期${weekDays[now.getDay()]}`;
    const timeStr = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`;

    return {
      success: true,
      message: `现在是${dateStr} ${weekStr} ${timeStr}`
    };
  }

  /**
   * 控制设备
   */
  private async controlDevice(params: Record<string, string>): Promise<ActionResult> {
    const device = params.device || '设备';
    const action = params.action === 'on' ? '打开' : '关闭';

    // 实际项目中对接智能家居API
    return {
      success: true,
      message: `好的,已${action}${device}`
    };
  }
}

6. AI智能体核心 (engine/AIAgent.ets)

// entry/src/main/ets/engine/AIAgent.ets

import { Message, MessageRole, MessageType, ConversationContext, IntentType } from '../model/MessageModel';
import { IntentEngine } from './IntentEngine';
import { ActionExecutor } from './ActionExecutor';
import { LLMService } from '../service/LLMService';
import { VoiceService } from '../service/VoiceService';

/**
 * V哥的AI智能体核心
 * 整合所有能力,实现智能对话
 */

export class AIAgent {
  private static instance: AIAgent;
  private context: ConversationContext;
  private intentEngine: IntentEngine;
  private actionExecutor: ActionExecutor;
  private llmService: LLMService;
  private voiceService: VoiceService;

  // 回调函数
  private onMessageCallback?: (message: Message) => void;
  private onStateChangeCallback?: (state: AgentState) => void;

  private constructor() {
    this.context = new ConversationContext();
    this.intentEngine = IntentEngine.getInstance();
    this.actionExecutor = ActionExecutor.getInstance();
    this.llmService = LLMService.getInstance();
    this.voiceService = VoiceService.getInstance();
  }

  static getInstance(): AIAgent {
    if (!AIAgent.instance) {
      AIAgent.instance = new AIAgent();
    }
    return AIAgent.instance;
  }

  /**
   * 设置消息回调
   */
  setOnMessage(callback: (message: Message) => void): void {
    this.onMessageCallback = callback;
  }

  /**
   * 设置状态回调
   */
  setOnStateChange(callback: (state: AgentState) => void): void {
    this.onStateChangeCallback = callback;
  }

  /**
   * 处理用户输入(核心方法)
   */
  async processInput(userInput: string): Promise<void> {
    if (!userInput.trim()) return;

    console.info(`[AIAgent] 处理用户输入: ${userInput}`);

    // 1. 创建用户消息
    const userMessage = new Message({
      role: MessageRole.USER,
      type: MessageType.TEXT,
      content: userInput
    });
    this.context.addMessage(userMessage);
    this.onMessageCallback?.(userMessage);

    // 2. 意图识别
    this.onStateChangeCallback?.(AgentState.THINKING);
    const { intent, params, confidence } = this.intentEngine.recognize(userInput);

    console.info(`[AIAgent] 意图识别: ${intent}, 置信度: ${confidence}`);

    // 3. 根据意图决定处理方式
    let response: string;

    if (intent !== IntentType.CHAT && confidence >= 0.7) {
      // 高置信度的功能意图,执行动作
      userMessage.intent = intent;
      userMessage.intentParams = params;

      const result = await this.actionExecutor.execute(intent, params);
      response = result.message;

      // 如果是需要补充信息的场景,继续调用LLM
      if (!result.success && intent !== IntentType.UNKNOWN) {
        response = await this.llmService.chat(
          `用户说"${userInput}",我尝试${this.getIntentDescription(intent)}但失败了。请给出友好的回复和建议。`,
          this.context
        );
      }
    } else {
      // 闲聊或低置信度,调用大模型
      response = await this.llmService.chat(userInput, this.context);
    }

    // 4. 创建助手回复
    const assistantMessage = new Message({
      role: MessageRole.ASSISTANT,
      type: MessageType.TEXT,
      content: response
    });
    this.context.addMessage(assistantMessage);
    this.onMessageCallback?.(assistantMessage);

    this.onStateChangeCallback?.(AgentState.IDLE);

    // 5. 语音播报回复
    await this.voiceService.speak(response);
  }

  /**
   * 开始语音输入
   */
  async startVoiceInput(): Promise<void> {
    this.onStateChangeCallback?.(AgentState.LISTENING);

    await this.voiceService.startListening(
      (text: string, isFinal: boolean) => {
        if (isFinal && text.trim()) {
          this.processInput(text);
        }
      },
      (error: string) => {
        console.error('[AIAgent] 语音识别错误:', error);
        this.onStateChangeCallback?.(AgentState.IDLE);
      }
    );
  }

  /**
   * 停止语音输入
   */
  async stopVoiceInput(): Promise<void> {
    await this.voiceService.stopListening();
    this.onStateChangeCallback?.(AgentState.IDLE);
  }

  /**
   * 获取对话历史
   */
  getHistory(): Message[] {
    return this.context.getHistory();
  }

  /**
   * 清空对话
   */
  clearHistory(): void {
    this.context.clear();
  }

  /**
   * 获取意图描述
   */
  private getIntentDescription(intent: IntentType): string {
    const descriptions: Record<IntentType, string> = {
      [IntentType.OPEN_APP]: '打开应用',
      [IntentType.SET_ALARM]: '设置闹钟',
      [IntentType.SET_REMINDER]: '设置提醒',
      [IntentType.QUERY_WEATHER]: '查询天气',
      [IntentType.QUERY_TIME]: '查询时间',
      [IntentType.CONTROL_DEVICE]: '控制设备',
      [IntentType.CHAT]: '闲聊',
      [IntentType.UNKNOWN]: '理解意图'
    };
    return descriptions[intent] || '执行操作';
  }
}

/**
 * 智能体状态
 */
export enum AgentState {
  IDLE = 'idle',           // 空闲
  LISTENING = 'listening', // 监听中
  THINKING = 'thinking',   // 思考中
  SPEAKING = 'speaking'    // 说话中
}

7. 主界面 (pages/Index.ets)

// entry/src/main/ets/pages/Index.ets

import { Message, MessageRole, MessageType } from '../model/MessageModel';
import { AIAgent, AgentState } from '../engine/AIAgent';
import { ActionExecutor } from '../engine/ActionExecutor';
import { VoiceService } from '../service/VoiceService';
import { common } from '@kit.AbilityKit';
import { promptAction } from '@kit.ArkUI';

@Entry
@Component
struct Index {
  @State messageList: Message[] = [];
  @State inputText: string = '';
  @State agentState: AgentState = AgentState.IDLE;
  @State isVoiceMode: boolean = false;

  private agent: AIAgent = AIAgent.getInstance();
  private voiceService: VoiceService = VoiceService.getInstance();
  private scroller: Scroller = new Scroller();
  private context = getContext(this) as common.UIAbilityContext;

  async aboutToAppear(): Promise<void> {
    // 初始化
    ActionExecutor.getInstance().setContext(this.context);

    // 请求权限
    await this.voiceService.requestPermission(this.context);

    // 设置回调
    this.agent.setOnMessage((message: Message) => {
      this.messageList = [...this.messageList, message];
      // 滚动到底部
      setTimeout(() => {
        this.scroller.scrollEdge(Edge.Bottom);
      }, 100);
    });

    this.agent.setOnStateChange((state: AgentState) => {
      this.agentState = state;
    });

    // 添加欢迎消息
    const welcomeMessage = new Message({
      role: MessageRole.ASSISTANT,
      type: MessageType.TEXT,
      content: '你好!我是小V助手 🤖\n\n我可以帮你:\n• 回答各种问题\n• 打开应用\n• 设置闹钟和提醒\n• 查询天气和时间\n• 控制智能设备\n\n试着对我说点什么吧!'
    });
    this.messageList.push(welcomeMessage);
  }

  /**
   * 发送消息
   */
  async sendMessage(): Promise<void> {
    if (!this.inputText.trim()) return;

    const text = this.inputText.trim();
    this.inputText = '';

    await this.agent.processInput(text);
  }

  /**
   * 切换语音模式
   */
  async toggleVoiceMode(): Promise<void> {
    if (this.isVoiceMode) {
      // 停止语音输入
      await this.agent.stopVoiceInput();
      this.isVoiceMode = false;
    } else {
      // 开始语音输入
      this.isVoiceMode = true;
      await this.agent.startVoiceInput();
    }
  }

  /**
   * 获取状态文本
   */
  getStateText(): string {
    switch (this.agentState) {
      case AgentState.LISTENING:
        return '正在听...';
      case AgentState.THINKING:
        return '思考中...';
      case AgentState.SPEAKING:
        return '说话中...';
      default:
        return '';
    }
  }

  /**
   * 格式化时间
   */
  formatTime(timestamp: number): string {
    const date = new Date(timestamp);
    const hour = date.getHours().toString().padStart(2, '0');
    const minute = date.getMinutes().toString().padStart(2, '0');
    return `${hour}:${minute}`;
  }

  build() {
    Column() {
      // 顶部标题栏
      Row() {
        Column() {
          Text('小V助手')
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
            .fontColor('#333333')

          if (this.agentState !== AgentState.IDLE) {
            Row() {
              LoadingProgress()
                .width(14)
                .height(14)
                .color('#007DFF')
              Text(this.getStateText())
                .fontSize(12)
                .fontColor('#007DFF')
                .margin({ left: 4 })
            }
            .margin({ top: 2 })
          }
        }
        .alignItems(HorizontalAlign.Start)

        Blank()

        // 清空对话按钮
        Button() {
          Image($r('app.media.ic_clear'))
            .width(20)
            .height(20)
            .fillColor('#666666')
        }
        .width(40)
        .height(40)
        .backgroundColor('#F0F0F0')
        .borderRadius(20)
        .onClick(() => {
          promptAction.showDialog({
            title: '清空对话',
            message: '确定要清空所有对话记录吗?',
            buttons: [
              { text: '取消', color: '#666666' },
              { text: '确定', color: '#007DFF' }
            ]
          }).then((result) => {
            if (result.index === 1) {
              this.messageList = [];
              this.agent.clearHistory();
            }
          });
        })
      }
      .width('100%')
      .height(60)
      .padding({ left: 16, right: 16 })
      .backgroundColor(Color.White)

      // 消息列表
      List({ scroller: this.scroller, space: 16 }) {
        ForEach(this.messageList, (message: Message) => {
          ListItem() {
            this.MessageBubble(message)
          }
        }, (message: Message) => message.id)
      }
      .width('100%')
      .layoutWeight(1)
      .padding({ left: 16, right: 16, top: 12, bottom: 12 })
      .backgroundColor('#F5F5F5')

      // 底部输入区域
      Row() {
        // 语音按钮
        Button() {
          Image(this.isVoiceMode ? $r('app.media.ic_keyboard') : $r('app.media.ic_voice'))
            .width(24)
            .height(24)
            .fillColor(this.isVoiceMode ? '#FF3B30' : '#666666')
        }
        .width(44)
        .height(44)
        .backgroundColor(this.isVoiceMode ? '#FFE5E5' : '#F0F0F0')
        .borderRadius(22)
        .onClick(() => this.toggleVoiceMode())

        if (this.isVoiceMode) {
          // 语音输入状态
          Column() {
            if (this.agentState === AgentState.LISTENING) {
              Row() {
                ForEach([1, 2, 3, 4, 5], (i: number) => {
                  Column()
                    .width(4)
                    .height(12 + Math.random() * 20)
                    .backgroundColor('#007DFF')
                    .borderRadius(2)
                    .margin({ left: 4, right: 4 })
                    .animation({
                      duration: 300,
                      iterations: -1,
                      curve: Curve.EaseInOut
                    })
                })
              }
              .justifyContent(FlexAlign.Center)
            }

            Text(this.agentState === AgentState.LISTENING ? '正在聆听...' : '点击麦克风开始说话')
              .fontSize(14)
              .fontColor('#666666')
              .margin({ top: 8 })
          }
          .layoutWeight(1)
          .height(44)
          .justifyContent(FlexAlign.Center)
        } else {
          // 文字输入框
          TextInput({ placeholder: '输入消息...', text: this.inputText })
            .layoutWeight(1)
            .height(44)
            .backgroundColor('#F5F5F5')
            .borderRadius(22)
            .padding({ left: 16, right: 16 })
            .margin({ left: 8, right: 8 })
            .onChange((value) => {
              this.inputText = value;
            })
            .onSubmit(() => {
              this.sendMessage();
            })

          // 发送按钮
          Button() {
            Image($r('app.media.ic_send'))
              .width(24)
              .height(24)
              .fillColor(Color.White)
          }
          .width(44)
          .height(44)
          .backgroundColor(this.inputText.trim() ? '#007DFF' : '#CCCCCC')
          .borderRadius(22)
          .enabled(this.inputText.trim().length > 0)
          .onClick(() => this.sendMessage())
        }
      }
      .width('100%')
      .height(70)
      .padding({ left: 12, right: 12, top: 8, bottom: 16 })
      .backgroundColor(Color.White)
    }
    .width('100%')
    .height('100%')
  }

  /**
   * 消息气泡组件
   */
  @Builder
  MessageBubble(message: Message) {
    Column() {
      if (message.role === MessageRole.USER) {
        // 用户消息(右侧)
        Row() {
          Blank()

          Column() {
            Text(message.content)
              .fontSize(15)
              .fontColor(Color.White)
              .lineHeight(22)
          }
          .padding(12)
          .backgroundColor('#007DFF')
          .borderRadius({
            topLeft: 16,
            topRight: 4,
            bottomLeft: 16,
            bottomRight: 16
          })
          .constraintSize({ maxWidth: '75%' })

          // 用户头像
          Image($r('app.media.ic_user'))
            .width(36)
            .height(36)
            .borderRadius(18)
            .margin({ left: 8 })
        }
        .width('100%')
        .justifyContent(FlexAlign.End)
      } else {
        // AI消息(左侧)
        Row() {
          // AI头像
          Stack() {
            Circle()
              .width(36)
              .height(36)
              .fill('#E6F2FF')

            Image($r('app.media.ic_robot'))
              .width(24)
              .height(24)
          }
          .margin({ right: 8 })

          Column() {
            Text(message.content)
              .fontSize(15)
              .fontColor('#333333')
              .lineHeight(22)

            // 显示时间
            Text(this.formatTime(message.timestamp))
              .fontSize(11)
              .fontColor('#999999')
              .margin({ top: 4 })
          }
          .padding(12)
          .backgroundColor(Color.White)
          .borderRadius({
            topLeft: 4,
            topRight: 16,
            bottomLeft: 16,
            bottomRight: 16
          })
          .constraintSize({ maxWidth: '75%' })
          .alignItems(HorizontalAlign.Start)
          .shadow({
            radius: 4,
            color: 'rgba(0,0,0,0.05)',
            offsetX: 0,
            offsetY: 2
          })

          Blank()
        }
        .width('100%')
        .justifyContent(FlexAlign.Start)
      }
    }
  }
}

8. 快捷指令面板 (components/QuickCommands.ets)

// entry/src/main/ets/components/QuickCommands.ets

/**
 * V哥设计的快捷指令面板
 * 方便用户快速触发常用功能
 */

interface QuickCommand {
  icon: Resource;
  label: string;
  command: string;
  color: string;
}

@Component
export struct QuickCommands {
  onCommand: (command: string) => void = () => {};

  private commands: QuickCommand[] = [
    { icon: $r('app.media.ic_weather'), label: '查天气', command: '今天天气怎么样', color: '#FFB800' },
    { icon: $r('app.media.ic_time'), label: '查时间', command: '现在几点了', color: '#007DFF' },
    { icon: $r('app.media.ic_alarm'), label: '设闹钟', command: '明天早上7点叫我起床', color: '#34C759' },
    { icon: $r('app.media.ic_remind'), label: '提醒我', command: '10分钟后提醒我喝水', color: '#FF9500' },
    { icon: $r('app.media.ic_app'), label: '打开相机', command: '打开相机', color: '#AF52DE' },
    { icon: $r('app.media.ic_home'), label: '开灯', command: '打开客厅的灯', color: '#FF3B30' }
  ];

  build() {
    Column() {
      Text('快捷指令')
        .fontSize(14)
        .fontColor('#999999')
        .margin({ bottom: 12 })

      Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
        ForEach(this.commands, (cmd: QuickCommand) => {
          Column() {
            Stack() {
              Circle()
                .width(44)
                .height(44)
                .fill(cmd.color)
                .opacity(0.15)

              Image(cmd.icon)
                .width(24)
                .height(24)
                .fillColor(cmd.color)
            }

            Text(cmd.label)
              .fontSize(12)
              .fontColor('#666666')
              .margin({ top: 6 })
          }
          .width('30%')
          .margin({ bottom: 16 })
          .onClick(() => {
            this.onCommand(cmd.command);
          })
        })
      }
    }
    .width('100%')
    .padding(16)
    .backgroundColor(Color.White)
    .borderRadius(12)
  }
}

五、资源文件准备

需要的图标资源

entry/src/main/resources/base/media/ 添加以下图标:

文件名用途建议尺寸
ic_robot.svgAI头像48x48
ic_user.svg用户头像48x48
ic_send.svg发送按钮24x24
ic_voice.svg语音按钮24x24
ic_keyboard.svg键盘按钮24x24
ic_clear.svg清空按钮24x24
ic_weather.svg天气图标24x24
ic_time.svg时间图标24x24
ic_alarm.svg闹钟图标24x24
ic_remind.svg提醒图标24x24
ic_app.svg应用图标24x24
ic_home.svg智能家居图标24x24

示例SVG

ic_robot.svg:

<svg viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
  <circle cx="24" cy="24" r="20" fill="#007DFF"/>
  <circle cx="17" cy="20" r="3" fill="white"/>
  <circle cx="31" cy="20" r="3" fill="white"/>
  <path d="M16 30 Q24 36 32 30" stroke="white" stroke-width="2" fill="none"/>
  <rect x="22" y="4" width="4" height="6" rx="2" fill="#007DFF"/>
  <circle cx="24" cy="4" r="3" fill="#007DFF"/>
</svg>

ic_voice.svg:

<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
  <path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3z"/>
  <path d="M17 11c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z"/>
</svg>

六、V哥总结:关键技术点

1. 意图识别的设计思路

用户输入 → 规则匹配(快速) → 高置信度直接执行
                ↓
         低置信度 → 调用LLM兜底

2. 对话管理的核心

// 多轮对话的关键:上下文管理
class ConversationContext {
  messages: Message[] = [];
  maxHistory: number = 10;  // 控制历史长度,避免Token浪费
}

3. 语音交互的最佳实践

  • 先请求权限,再初始化引擎
  • 语音识别和语音合成用完要释放
  • 做好错误处理和降级方案

4. 性能优化建议

  • 意图识别用本地规则,快速响应
  • LLM调用做好缓存和限流
  • 消息列表使用LazyForEach优化

七、V哥唠两句

兄弟们,这套代码是V哥实战中总结出来的,完整实现了一个能用的AI智能体。当然,实际项目中你还需要:

  1. 接入真实的LLM API(替换掉示例配置)
  2. 完善意图规则库(根据你的业务场景)
  3. 接入真实的智能家居API
  4. 做好异常处理和用户引导

AI智能体的核心不是技术多牛逼,而是用户体验做得好!


关注V哥不迷路!前行路上不犯怵!

交代下背景,本人刚结婚两年多,结婚时也曾因为彩礼而跟老婆闹过矛盾,因为当时答应的给老婆彩礼 20 个,老婆答应不要求立马买房子,也没有要求新买车等,因为家里已经有了个便宜的小朗逸当作代步车。但是父母由于前些年做生意亏了个大窟窿,当时父母也掏不出那么多钱,我就自己把我刚工作两年剩下的十几个全部给父母凑上了彩礼,因为这件事情也跟老婆闹过矛盾,差点刚结婚就离婚了。结婚后老婆也把彩礼加上订婚的 8.8 和陪嫁等都带回来了,等于老婆也带回来了差不多彩礼和订婚的钱数。然后把所有的钱都存了定期。

然后父母由于之前亏的太多,现在也换了个生意,第一年亏钱,第二年赚了一点,已经还了几十的外账,但是听说今年生意又不好做,估计也没剩下多少钱。父母除了欠的贷款,还有与亲戚借的二三十个的外账。也有其他亲戚因为特殊原因,也欠父母十几个,但是什么时候能还上也是遥遥无期。

前几天父母打电话,说有一笔贷款快到期了,一共大概五十多个,跟我商量让我先替还上 40 个,然后贷款的利息依旧每年给我们,但是本金啥时候能还上的要看做生意的情况。

就把这件事情跟老婆商量了一下,老婆感性上很想帮忙,但是说我们结婚后这两年总共也没攒下多少钱,而且还没有房子。就说要把这笔钱当作买房子的首付,担心一下子这么多钱给了父母后,不知道父母什么时候才能还清,本来打算明年年底开始看房,如果还不清的话,房子这件事情就又不知道什么时候才能买得起,所以老婆从小家的考虑上不建议替还这笔贷款,也想借机给父母上点压力,因为父母性格上花钱也会大手大脚。

如果帮还这笔贷款的话,加上结婚时候的彩礼,就等于工作四年差不多大部分的钱都交给了父母。

老婆就说即使有利息,就让父母慢慢专注还外账就行,买房子的首付什么的就不指望父母出力了,就靠我们慢慢攒钱就行。只求父母能够赶紧把所有的外账慢慢还完。甚至每年的贷款利息我们出也行。还说临近过年,今年过年在家就让我们多花点,因为之前每年都会花好多钱买酒之类的,现在清楚了家里的情况,打算今年好酒也不买了。买的酒基本都是剑南春类似的,每年都会买两箱以上。

老婆确实什么上都往我们小家上着想,包括结婚的时候的奖金,还有今年发的十几个的奖金,除了告诉我,就没跟其他人说过,都想当做房子的首付。

我也特别理解老婆,老婆从我一直一无所有也都愿意一直陪着。

但是父母这次张嘴又特别不好拒绝,不知道该怎么办。而且之前自己也帮亲戚周转过 20 个左右的钱,虽然很快还了,但是还有两三个没有还清。如果这次不帮还的话,等于亲戚都帮了,但是父母张嘴却没有帮忙。虽然心里已经大概拿定主意不帮还这笔贷款,但是又不知道怎么跟父母具体沟通。

因为当时父母给自己打电话的时候,自己也没想那么多,就说回去跟老婆商量一下。电话里可能父母已经当作我答应了。如果再打电话说不帮还的话,感觉父母就会觉得是老婆不同意,又担心会影响婆媳关系之类的。帮还的话,确实是比较大的一笔钱。

该怎么办呢,能不能给点建议。



前几天家附近开了个彩票站 , 在业主群里加了这个彩票站老板的微信 , 心血来潮买了几注双色球 , 结果差两个号就是两千万 , 可惜现在只有 200 , 我现在是该高兴还是悲伤😭

如果你有搞到的 Google 学生订阅福利优惠,那 API 调用也可以用了。

不用之前的反代模式才能拿到 banana 等 API 调用啦。

Google 宣布:AI Pro / Ultra 订阅现在自带 GDP Premium 权益——每月送 GCP 代金:Pro 送 10 美金、Ultra 送 100 美金,可直接拿去跑 Gemini API 、Vertex AI 、Cloud Run 等,把聊天里的点子一路推到上线。

一句话:从“玩模型”到“上生产”,Google 帮你把中间那道付费墙拆了一块。

文章详情: https://blog.google/innovation-and-ai/technology/developers-tools/gdp-premium-ai-pro-ultra/

目前看的一个洛斐 Flow2 ,不知道品质如何,有没有推荐的其他品牌的键盘(之前两把 keychron 都用坏了),目前用的苹果的键盘,十分难受,希望就是机械键盘,不要太大,可以放背包带着走的,HHKB 这种价格太高,而且配列感觉适应起来麻烦。差不多 500-800 左右的即可,三模充电的。

活动详情回顾: https://www.v2ex.com/t/1187043


本次征稿额外奖励(已发放完毕)

截止到 utc 时间 2026.1.27, 以下帖子获得额外的活动奖励:

  • 回复数最多(193 回复) -> 200$v2ex:

@287854442 <<一个大胆的预言:语音输入将成为绝对主流>>

  • 收藏数最多(14 人收藏) -> 500$v2ex

@sillydaddy <<vibe coding 的最佳实践到底是什么?>>

  • 感谢数最多(3 人感谢) -> 500$v2ex

@287854442 <<一个大胆的预言:语音输入将成为绝对主流>>


本次活动所有符合主题的帖子:

<<一个大胆的预言:语音输入将成为绝对主流>> 作者: @287854442

<<"AI 与编程" 几个月来高强度 vibe coding 的一点心得>> 作者: @mkq

<<AI 时代,笔记的结局是什么?>> 作者: @287854442

<<"AI 与编程" 近半年工作使用 Cursor 的感受>> 作者: @lenglengyuchen

<<vibe coding 的最佳实践到底是什么?>> 作者: @sillydaddy

<<我从来不是创造型人才 - AI 驱动编程下的迷思>> 作者: @Zhuzhuchenyan

<<未来 10 年程序员技术水平分布会是什么结构?>> 作者: @Kinnikuman

<<AI 狂热的冷思考>> 作者: @shoushen

<<AI 浪潮下,程序员教育该如何转型?>> 作者: @Dabney

<<大家都是如何使用 AI 提升工作效率的?>> 作者: @darktutu

<<我们真的应该完全放弃《古法编程》?>> 作者: @ybz

<<随着 AI 的发展,"眼高手低"会不会逐渐变成优势?>> 作者: @funtanstic


最后, 再次感谢所有参与分享与讨论的 V 友, 欢迎大家关注Joe's Talk, 让我们一起来建设新的无水板块.

3 个月前买过一把京东京造的智能锁 M30 Max ,这锁离了个大谱。 新锁第一次上电池,只坚持了 15 天就没电了,想着可能是因为新电池没满。充满装上只还是只坚持了 13 天,联系客服,说是可能电池问题,给补发了一个。换了新电池,新电池来了更夸张,只能坚持 10 天就没电了。再联系客服,客服反应说是可能设置有问题,找了师傅上门,师傅上门鼓捣一番下来,把联网功能,视频功能,除了开锁以外的所有功能全关了。本以为可以消停一下了,结果现在充满电后的电池只能用 8 天了。

现在又得新寄了一把一样一样的给我,说实话我打算换这个锁了,不知道有没有用过的,你们的电池是否正常?