惠州住房和建设局网站ui的设计网站
2026/4/18 15:50:20 网站建设 项目流程
惠州住房和建设局网站,ui的设计网站,大连网龙网络科技,福州网络营销推广目录 一、引言 二、代码修改 1. 先引入依赖 2. 核心工具类#xff08;含 Point 实体 映射逻辑#xff09; 3.OparetionServiceImpl 实现类 三、运行结果演示 一、引言 在前文 通义千问3-VL-Plus - 界面交互#xff08;本地图片改进#xff09;-CSDN博客 中我们完成了…目录一、引言二、代码修改1. 先引入依赖2. 核心工具类含 Point 实体 映射逻辑3.OparetionServiceImpl 实现类三、运行结果演示一、引言在前文 通义千问3-VL-Plus - 界面交互本地图片改进-CSDN博客 中我们完成了对GUI模型的接入但是我发现定位好像不准确一番查看后发现原来读取的是压缩图片的定位想要获取正确的还需要再进一步修改。二、代码修改1. 先引入依赖!-- Maven依赖 -- dependencies !-- OkHttp网络请求 -- dependency groupIdcom.squareup.okhttp3/groupId artifactIdokhttp/artifactId version4.12.0/version /dependency !-- 图像处理JDK自带无需额外引入 -- dependency groupIdjavax.imageio/groupId artifactIdimageio-api/artifactId version1.5.1/version scopeprovided/scope /dependency /dependencies2. 核心工具类含 Point 实体 映射逻辑package gzj.spring.ai.util; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; import org.apache.commons.codec.binary.Base64; import org.apache.commons.io.FileUtils; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.*; import java.math.RoundingMode; import java.net.HttpURLConnection; import java.net.URL; import java.text.DecimalFormat; /** * 坐标映射工具类兼容本地图片/网络URL Base64转换 模型坐标→原始坐标 */ public class CoordinateMappingUtil { // 静态OkHttp客户端复用连接 private static final OkHttpClient OK_HTTP_CLIENT new OkHttpClient().newBuilder().build(); /** * 坐标点实体类 */ public static class Point { private int x; private int y; public Point(int x, int y) { this.x x; this.y y; } // Getter/Setter public int getX() { return x; } public void setX(int x) { this.x x; } public int getY() { return y; } public void setY(int y) { this.y y; } Override public String toString() { return 原始图像坐标(x x , y y ); } } // 新增本地图片转Base64 /** * 本地图片转Base64带data:image前缀兼容GUI-Plus模型输入 * param localImagePath 本地图片绝对路径如E:\\test.png * return 带前缀的Base64字符串 * throws IOException 文件读取异常 */ public static String localImageToBase64(String localImagePath) throws IOException { File imageFile new File(localImagePath); // 校验文件存在性 if (!imageFile.exists()) { throw new FileNotFoundException(本地图片不存在 localImagePath); } // 读取文件字节 byte[] imageBytes FileUtils.readFileToByteArray(imageFile); // Base64编码 String base64Str Base64.encodeBase64String(imageBytes); // 自动识别图片格式 String suffix localImagePath.substring(localImagePath.lastIndexOf(.) 1).toLowerCase(); if (!suffix.matches(png|jpg|jpeg|bmp)) { suffix png; // 默认PNG } // 拼接data:image前缀 return String.format(data:image/%s;base64,%s, suffix, base64Str); } // 核心统一读取图片本地/网络 /** * 统一读取图片自动识别本地路径/网络URL * param imageSource 本地图片路径如E:\\test.png或网络URLhttp/https开头 * return 图片的BufferedImage用于获取原始宽高 * throws IOException 读取异常 */ private static BufferedImage getImage(String imageSource) throws IOException { // 判定是否为网络URL if (imageSource.startsWith(http://) || imageSource.startsWith(https://)) { return getImageFromUrl(imageSource); } else { // 本地图片路径 File localFile new File(imageSource); if (!localFile.exists()) { throw new FileNotFoundException(本地图片不存在 imageSource); } return ImageIO.read(localFile); } } /** * 从网络URL读取图片 */ private static BufferedImage getImageFromUrl(String imageUrl) throws IOException { // 兼容JDK原生URL兜底OkHttp try { URL url new URL(imageUrl); HttpURLConnection conn (HttpURLConnection) url.openConnection(); conn.setConnectTimeout(5000); conn.setReadTimeout(5000); conn.setRequestMethod(GET); if (conn.getResponseCode() 200) { return ImageIO.read(conn.getInputStream()); } else { throw new IOException(网络图片读取失败响应码 conn.getResponseCode()); } } catch (Exception e) { // 降级使用OkHttp Request request new Request.Builder().url(imageUrl).get().build(); try (Response response OK_HTTP_CLIENT.newCall(request).execute()) { if (!response.isSuccessful()) { throw new IOException(OkHttp读取图片失败响应码 response.code()); } byte[] imageBytes response.body().bytes(); return ImageIO.read(new ByteArrayInputStream(imageBytes)); } } } // 核心坐标映射兼容本地/网络图片 /** * 模型坐标映射到原始图像坐标支持本地图片路径/网络URL * param imageSource 本地图片路径 或 网络URL * param modelPoint 模型返回的坐标基于内部缩放图 * param factor 缩放基数默认28 * param maxPixels 最大像素值默认1280*28*28 * param minPixels 最小像素值默认4*28*28 * return 原始图像的绝对坐标 * throws IOException 读取图片异常 */ public static Point smartSize(String imageSource, Point modelPoint, Integer factor, Long maxPixels, Long minPixels) throws IOException { // 1. 默认参数 int defaultFactor factor null ? 28 : factor; long defaultMaxPixels maxPixels null ? 1280 * 28 * 28 : maxPixels; long defaultMinPixels minPixels null ? 4 * 28 * 28 : minPixels; // 2. 统一读取图片本地/网络获取原始尺寸 BufferedImage originalImage getImage(imageSource); int originalWidth originalImage.getWidth(); int originalHeight originalImage.getHeight(); // 3. 初始调整宽高为factor的整数倍round取整 double hBar Math.round((double) originalHeight / defaultFactor) * defaultFactor; double wBar Math.round((double) originalWidth / defaultFactor) * defaultFactor; // 4. 计算缩放因子beta根据像素阈值调整 double totalPixels hBar * wBar; double beta 1.0; if (totalPixels defaultMaxPixels) { beta Math.sqrt((originalHeight * originalWidth) / (double) defaultMaxPixels); hBar Math.floor(originalHeight / beta / defaultFactor) * defaultFactor; wBar Math.floor(originalWidth / beta / defaultFactor) * defaultFactor; } else if (totalPixels defaultMinPixels) { beta Math.sqrt(defaultMinPixels / (originalHeight * originalWidth)); hBar Math.ceil(originalHeight * beta / defaultFactor) * defaultFactor; wBar Math.ceil(originalWidth * beta / defaultFactor) * defaultFactor; } // 5. 模型坐标 → 原始坐标四舍五入 DecimalFormat df new DecimalFormat(#); df.setRoundingMode(RoundingMode.HALF_UP); int originalX Integer.parseInt(df.format((double) modelPoint.getX() / wBar * originalWidth)); int originalY Integer.parseInt(df.format((double) modelPoint.getY() / hBar * originalHeight)); return new Point(originalX, originalY); } // 测试示例 // public static void main(String[] args) { // try { // // 测试1网络URL场景 // String netImageUrl https://p3-flow-imagex-sign.byteimg.com/tos-cn-i-a9rns2rl98/1e8c83f9e6b94f428e21c754d1265406.png; // Point modelPoint1 new Point(1205, 278); // Point netOriginalPoint smartSize(netImageUrl, modelPoint1, null, null, null); // System.out.println(【网络图片】模型坐标 modelPoint1.getX() , modelPoint1.getY()); // System.out.println(【网络图片】原始坐标 netOriginalPoint); // // // 测试2本地图片场景转Base64 坐标映射 // String localImagePath E:\\screenshot\\desktop.png; // 替换为你的本地路径 // // 本地图片转Base64供模型调用 // String base64Str localImageToBase64(localImagePath); // System.out.println(【本地图片】Base64前50字符 base64Str.substring(0, 50) ...); // // 本地图片坐标映射 // Point modelPoint2 new Point(1205, 278); // Point localOriginalPoint smartSize(localImagePath, modelPoint2, null, null, null); // System.out.println(【本地图片】模型坐标 modelPoint2.getX() , modelPoint2.getY()); // System.out.println(【本地图片】原始坐标 localOriginalPoint); // // } catch (IOException e) { // e.printStackTrace(); // } // } }3.OparetionServiceImpl 实现类package gzj.spring.ai.Service.ServiceImpl; import com.alibaba.dashscope.aigc.multimodalconversation.MultiModalConversation; import com.alibaba.dashscope.aigc.multimodalconversation.MultiModalConversationParam; import com.alibaba.dashscope.aigc.multimodalconversation.MultiModalConversationResult; import com.alibaba.dashscope.common.MultiModalMessage; import com.alibaba.dashscope.common.Role; import com.alibaba.dashscope.exception.ApiException; import com.alibaba.dashscope.exception.NoApiKeyException; import com.alibaba.dashscope.exception.UploadFileException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import gzj.spring.ai.Request.OparetionRequest; import gzj.spring.ai.Service.OparetionService; import gzj.spring.ai.util.CoordinateMappingUtil; import io.reactivex.Flowable; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import java.awt.*; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.*; import java.util.List; import static com.alibaba.cloud.ai.graph.utils.TryConsumer.log; import static gzj.spring.ai.util.CoordinateMappingUtil.smartSize; /** * author DELL */ Service public class OparetionServiceImpl implements OparetionService { Value(${spring.ai.dashscope.api-key}) private String apiKey; Value(${spring.ai.dashscope.modelV2:gui-plus}) private String modelName; // 模型名配置化便于切换 /** * 工具方法本地图片转Base64带data:image前缀GUI-Plus支持格式 */ private String encodeLocalImageToBase64(String localPath) throws IOException { Path imagePath Paths.get(localPath); // 校验文件存在性 if (!Files.exists(imagePath)) { throw new IOException(本地图片不存在 localPath); } // 读取文件并Base64编码修复原有编码错误 byte[] imageBytes Files.readAllBytes(imagePath); String base64Str Base64.getEncoder().encodeToString(imageBytes); // 自动识别图片格式 String suffix localPath.substring(localPath.lastIndexOf(.) 1).toLowerCase(); if (!Arrays.asList(png, jpg, jpeg).contains(suffix)) { suffix png; // 默认PNG } return String.format(data:image/%s;base64,%s, suffix, base64Str); } /** * 工具方法构建图片内容优先级本地图片 网络URL */ private String buildImageContent(OparetionRequest request) throws IOException { if (request.getLocalImagePath() ! null !request.getLocalImagePath().isEmpty()) { log.info(使用本地图片{}, request.getLocalImagePath()); return encodeLocalImageToBase64(request.getLocalImagePath()); } else if (request.getImageUrl() ! null !request.getImageUrl().isEmpty()) { log.info(使用网络图片URL{}, request.getImageUrl()); return request.getImageUrl(); } else { throw new IllegalArgumentException(必须传入imageUrl网络图片或localImagePath本地图片); } } /** * 构建GUI-Plus核心提示词优化为Text Blocks提升可读性 */ private String buildSystemPrompt() { return ## 1. 核心角色 (Core Role) 你是一个顶级的AI视觉操作代理。你的任务是分析电脑屏幕截图理解用户的指令然后将任务分解为单一、精确的GUI原子操作。 ## 2. [CRITICAL] JSON Schema 绝对规则必须严格遵守 你的输出必须是一个**完整、合法、可直接解析**的JSON对象任何情况下都不能截断、遗漏字段、缺少闭合符号。 ### 强制规则 - [R1] 纯JSON输出回复只能是JSON对象无任何前缀、后缀、注释、解释性文字。 - [R2] 字段必填性 - 所有Action的parameters字段必须包含模板中**所有必填键**如CLICK必须有x、y整数缺一不可 - x/y必须是**单个整数**禁止数组/空值代表屏幕坐标像素 - thought字段必须是一句话描述思考过程不能为空。 - [R3] Action值规范只能是 CLICK/TYPE/SCROLL/KEY_PRESS/FINISH/FAIL大写、无空格。 - [R4] JSON格式校验生成后必须自检——确保大括号闭合、逗号正确、字符串用双引号、数值无引号。 ## 3. 工具集 (Available Actions) ### CLICK必填x、y可选description - 功能: 单击屏幕。 - 坐标规则: x、y是**当前截图的像素坐标**——以截图的左上角为原点向右为x轴正方向向下为y轴正方向坐标值为截图内的实际像素数值例如截图宽度是1920像素则x最大为1919。 - 必须返回如下完整JSON结构x/y为截图内的实际像素整数 { thought: 一句话描述思考过程, action: CLICK, parameters: { x: 1753, y: 278, description: 截图中右上角的豆包应用图标 } } ### TYPE必填text、needs_enter - 功能: 输入文本。 - 必须返回如下完整JSON结构 { thought: 一句话描述思考过程, action: TYPE, parameters: { text: 要输入的文本, needs_enter: true/false } } ### SCROLL必填direction、amount - 功能: 滚动窗口。 - 必须返回如下完整JSON结构 { thought: 一句话描述思考过程, action: SCROLL, parameters: { direction: up/down, amount: small/medium/large } } ### KEY_PRESS必填key - 功能: 按下功能键。 - 必须返回如下完整JSON结构 { thought: 一句话描述思考过程, action: KEY_PRESS, parameters: { key: enter/esc/altf4等 } } ### FINISH必填message - 功能: 任务成功完成。 - 必须返回如下完整JSON结构 { thought: 一句话描述思考过程, action: FINISH, parameters: { message: 总结任务完成情况 } } ### FAIL必填reason - 功能: 任务无法完成。 - 必须返回如下完整JSON结构 { thought: 一句话描述思考过程, action: FAIL, parameters: { reason: 清晰解释失败原因 } } ## 4. 思维与决策框架 1. 目标分析: 用户的最终目标是什么 2. 屏幕观察: 仅基于截图中的视觉证据决策看不见的元素不交互。 3. 行动决策: 选择最合适的Action确保parameters字段完整。 4. 最终校验: 检查JSON是否完整闭合、字段是否必填、格式是否合法再输出。 ; } /** * 非流式调用保留原有逻辑兼容本地图片 */ Override public String operation(OparetionRequest request) throws ApiException, NoApiKeyException, UploadFileException, IOException { // 1. 校验核心参数 if (request.getText() null || request.getText().isEmpty()) { throw new IllegalArgumentException(用户指令text不能为空); } // 2. 初始化客户端 MultiModalConversation conv new MultiModalConversation(); // 3. 构建系统提示词 MultiModalMessage systemMsg MultiModalMessage.builder() .role(Role.SYSTEM.getValue()) .content(Collections.singletonList(Collections.singletonMap(text, buildSystemPrompt()))) .build(); // 4. 构建用户消息图片文本 String imageContent buildImageContent(request); MultiModalMessage userMessage MultiModalMessage.builder() .role(Role.USER.getValue()) .content(Arrays.asList( Collections.singletonMap(image, imageContent), Collections.singletonMap(text, request.getText()) )).build(); // 5. 构建请求参数修复API Key使用矛盾 MultiModalConversationParam param MultiModalConversationParam.builder() // 统一使用配置文件的API Key .apiKey(apiKey) .model(modelName) .messages(Arrays.asList(systemMsg, userMessage)) .build(); // 6. 同步调用结果解析增加空指针防护 MultiModalConversationResult result conv.call(param); if (result null || result.getOutput() null || result.getOutput().getChoices() null || result.getOutput().getChoices().isEmpty()) { log.warn(GUI-Plus返回结果为空); // 返回空JSON避免前端解析异常 return {}; } ListMapString, Object content result.getOutput().getChoices().get(0).getMessage().getContent(); String resText content ! null !content.isEmpty() ? content.get(0).get(text).toString() : {}; log.info(GUI-Plus非流式调用完成结果{}, resText); // 坐标映射至实际原始图像 try { // 尝试解析模型返回的JSON文本 ObjectMapper objectMapper new ObjectMapper(); JsonNode jsonNode objectMapper.readTree(resText); int x jsonNode.path(parameters).path(x).asInt(); int y jsonNode.path(parameters).path(y).asInt(); CoordinateMappingUtil.Point modelPoint new CoordinateMappingUtil.Point(x, y); CoordinateMappingUtil.Point point CoordinateMappingUtil.smartSize(request.getLocalImagePath(), modelPoint, null, null, null); log.info(映射后的坐标{}, point); } catch (Exception e) { log.error(解析模型返回结果失败, e); } return resText; } /** * 新增SSE流式调用实时推送结果 */ Override public SseEmitter streamOperation(OparetionRequest request) { // 设置SSE超时时间30秒 SseEmitter emitter new SseEmitter(30000L); // 超时回调 emitter.onTimeout(() - handleEmitterError(emitter, SSE连接超时30秒)); // 客户端关闭回调 emitter.onCompletion(() - log.info(SSE连接已关闭)); // 异步执行流式调用避免阻塞主线程 new Thread(() - { MultiModalConversation conv new MultiModalConversation(); try { // 1. 校验参数 if (request.getText() null || request.getText().isEmpty()) { throw new IllegalArgumentException(用户指令text不能为空); } // 2. 构建图片内容消息 String imageContent buildImageContent(request); MultiModalMessage systemMsg MultiModalMessage.builder() .role(Role.SYSTEM.getValue()) .content(Collections.singletonList(Collections.singletonMap(text, buildSystemPrompt()))) .build(); MultiModalMessage userMessage MultiModalMessage.builder() .role(Role.USER.getValue()) .content(Arrays.asList( Collections.singletonMap(image, imageContent), Collections.singletonMap(text, request.getText()) )).build(); // 3. 构建流式请求参数 MultiModalConversationParam param MultiModalConversationParam.builder() .apiKey(apiKey) .model(modelName) .messages(Arrays.asList(systemMsg, userMessage)) .maxTokens(2048) .incrementalOutput(true) // 开启增量输出流式核心 .build(); // 4. 流式调用推送结果 FlowableMultiModalConversationResult resultFlow conv.streamCall(param); resultFlow.blockingForEach(item - { try { if (item.getOutput() null || item.getOutput().getChoices() null || item.getOutput().getChoices().isEmpty()) { return; // 空结果跳过 } ListMapString, Object content item.getOutput().getChoices().get(0).getMessage().getContent(); if (content ! null !content.isEmpty()) { String text content.get(0).get(text).toString(); // 推送单条流式数据event名称message emitter.send(SseEmitter.event().name(message).data(text)); log.debug(推送流式数据{}, text); } } catch (Exception e) { log.error(推送单条流式数据失败, e); handleEmitterError(emitter, 数据推送失败 e.getMessage()); } }); // 流式结束标记 emitter.send(SseEmitter.event().name(complete).data(流输出完成)); emitter.complete(); log.info(GUI-Plus流式调用完成); } catch (IOException e) { log.error(读取本地图片失败, e); handleEmitterError(emitter, 读取本地图片失败 e.getMessage()); } catch (ApiException | NoApiKeyException | UploadFileException e) { log.error(GUI-Plus API调用失败, e); handleEmitterError(emitter, API调用失败 e.getMessage()); } catch (IllegalArgumentException e) { log.error(请求参数异常, e); handleEmitterError(emitter, 参数错误 e.getMessage()); } catch (Exception e) { log.error(流式调用未知异常, e); handleEmitterError(emitter, 系统异常 e.getMessage()); } }).start(); return emitter; } /** * 工具方法统一处理SSE异常 */ private void handleEmitterError(SseEmitter emitter, String errorMsg) { try { emitter.send(SseEmitter.event().name(error).data(errorMsg)); emitter.completeWithError(new RuntimeException(errorMsg)); } catch (Exception e) { log.error(处理SSE发射器异常失败, e); } } }三、运行结果演示由于篇幅限制具体的分析我放在下一篇文章给大家捋一捋如果觉得这份修改实用、总结清晰别忘了动动小手点个赞再关注一下呀 后续还会分享更多 AI 接口封装、代码优化的干货技巧一起解锁更多好用的功能少踩坑多提效 你的支持就是我更新的最大动力咱们下次分享再见呀

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询