Nicksxs's Blog

What hurts more, the pain of hard work or the pain of regret?

程序员中例如我这样的主要是服务端的其实要开发一款app会面临很多问题,相对便捷的可能是做一个网页版的,套用下组件啥的,网页版的要做成app的话之前比较可行的是通过 Electron ,但是也是学习成本比较大的一件事,现在要介绍的这个pakeplus是基于rust tauri开发的一个傻瓜式地把网页转换成app的开源软件
首先它的github地址是 https://github.com/Sjj1024/PakePlus?tab=readme-ov-file

可以在 https://github.com/Sjj1024/PakePlus/releases 下载对应的版本
下载后打开长这样

点击➕号就能开始创建一个app
然后就是填入网页的地址,比如我这边想把我常用的一个crontab工具打包成一个app,
那就填入地址,以及相关的软件描述,还可以控制是否开启调试等

保存后就可以开始点击预览

这样一个简单的网页就打包成了一个mac上的app了,这里可能更多的场景是比如我就写了一个网页版的小工具,想打包成一个app来方便随时打开使用
比如本地的json工具等等,有些是自己定制的更趁手,还是非常不错的,记得之前这个开发者是可能做的命令行版的
这次是更彻底的界面化的比较傻瓜式的打包工具
方便很多不了解开发,命令行的同学进行使用

最近写代码的时候碰到个问题,
比如我们有一个对象列表,对象中有个排序字段,比如时间或者索性就有个序号

1
2
3
4
5
6
7
8
9
10
11
12
public class Demo {

private int order = 0;

public int getOrder() {
return order;
}

public void setOrder(int order) {
this.order = order;
}
}

比如这样,有一个前提,就是这个列表其实是有序的,只是为了防止万一
所以对这个列表进行了排序,就直接使用了ArrayList自带的sort方法,由于是不小心用在了多线程环境
就有出现了一个并发变更报错
也就是这个异常 java.util.ConcurrentModificationException#ConcurrentModificationException()
看下注释

1
2
This exception may be thrown by methods that have detected concurrent modification of an object when such modification is not permissible.
For example, it is not generally permissible for one thread to modify a Collection while another thread is iterating over it. In general, the results of the iteration are undefined under these circumstances. Some Iterator implementations (including those of all the general purpose collection implementations provided by the JRE) may choose to throw this exception if this behavior is detected. Iterators that do this are known as fail-fast iterators, as they fail quickly and cleanly, rather that risking arbitrary, non-deterministic behavior at an undetermined time in the future.

一般情况就是并发环境下进行了变更,
只是这里比较特殊,这个列表没有进行增删,同时是有序的,理论上应该是不会出现变更才对
但是它的确出现了,就得看下这个产生的原因
主要还是来看下这个java.util.ArrayList#sort的实现跟这个异常的抛出原理

1
2
3
4
5
6
7
public void sort(Comparator<? super E> c) {
final int expectedModCount = modCount;
Arrays.sort((E[]) elementData, 0, size, c);
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
modCount++;
}

可以看到在排序前会有个 modCount 的赋值,如果在进行排序后,这个初始的 modCount 跟当前的不等的话就会抛出这个异常
那么什么时候会出现这两者不相等呢
简单推演下这个并发排序下的情况,线程1和线程2同时进入这个方法,拿到了一样的 modCount , 然后进行排序,第一个线程比较快,sort玩之后发现两者相等
就到了 modCount++ , 这时候第二个线程刚排序完,发现 modCount 已经比 expectedModCount 大了
这个时候就出现了这个异常的抛出时机
如果这么理解这个异常的确是应该会出现
而不是我们理解的,如果不变更元素,不调整顺序,就不会触发这个并发变更的异常
还是有很多值得学习的点

鉴于用upic配置r2存储的图床失败后,目前的方式退化了一下,直接使用rclone作为临时替代
但是也才了个小坑,因为rclone在做copy的时候是不管bucket是否存在的,直接会调用 CreateBucket 命令
同时我们的accessKey一般是只有对bucket内部文件的读写权限,所以会导致一个比较尴尬的问题
直接用copy就是报

1
2025/05/11 21:16:04 NOTICE: Failed to copy: failed to prepare upload: operation error S3: CreateBucket, https response error StatusCode: 403, RequestID: , HostID: , api error AccessDenied: Access Denied

这个403的错误,结果谷歌搜索了下
可以通过这个选项进行屏蔽

1
--s3-no-check-bucket

这样我就能用

1
./rclone --s3-no-check-bucket copy ~/Downloads/r2/test_for_rclone.png r2_new:blog

来上传了,只是这里相比uPic麻烦了两步,第一截图后需要对文件进行命名,不然就是qq截图或者微信截图的文件名,
第二上传后需要自己拼图片的链接,当然希望是能够使用uPic的,但目前的问题是uPic似乎对于s3协议的支持不那么好
噢,对了,我用的是直接基于开源代码的自己打包版本,个人感觉还有点不太习惯从免费用成付费软件,主要是它是开源的
打算空了再折腾打包试试,或者自己调试下,实在不行就付费了

在大模型的演进过程中,mcp是个对于使用者非常有用的一个协议或者说工具,Model Context Protocol (MCP) 是一种专为大型语言模型和 AI 系统设计的通信协议框架,它解决了 AI 交互中的一个核心问题:如何有效地管理、传递和控制上下文信息。打个比方,如果把 AI 模型比作一个智能助手,那么 MCP 就是确保这个助手能够”记住”之前的对话、理解当前问题的背景,并按照特定规则进行回应的通信机制。
MCP 的工作原理
基本结构
MCP 的基本结构可以分为三个主要部分:

  1. 上下文容器 (Context Container):存储对话历史、系统指令和用户背景等信息
  2. 控制参数 (Control Parameters):调节模型行为的设置,如温度、最大输出长度等
  3. 消息体 (Message Body):当前需要处理的输入内容
    ┌─────────────────────────────────┐
    │ MCP 请求/响应结构 │

├─────────────────────────────────┤
│ │
│ ┌─────────────────────────┐ │
│ │ 上下文容器 │ │
│ │ ┌─────────────────┐ │ │
│ │ │ 对话历史 │ │ │
│ │ └─────────────────┘ │ │
│ │ ┌─────────────────┐ │ │
│ │ │ 系统指令 │ │ │
│ │ └─────────────────┘ │ │
│ │ ┌─────────────────┐ │ │
│ │ │ 用户背景 │ │ │
│ │ └─────────────────┘ │ │
│ └─────────────────────────┘ │
│ │
│ ┌─────────────────────────┐ │
│ │ 控制参数 │ │
│ └─────────────────────────┘ │
│ │
│ ┌─────────────────────────┐ │
│ │ 消息体 │ │
│ └─────────────────────────┘ │
│ │
└─────────────────────────────────┘

实操

首先基于start.spring.io创建一个springboot应用,需要springboot的版本3.3+和jdk17+
然后添加maven依赖

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server</artifactId>
<version>1.0.0-M8</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>6.1.12</version>
</dependency>

我们就可以实现一个mcp的demo server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
package com.nicksxs.mcp_demo;

import com.jayway.jsonpath.JsonPath;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;

import java.util.List;
import java.util.Map;

@Service
public class WeatherService {

private final RestClient restClient;

public WeatherService() {
this.restClient = RestClient.builder()
.baseUrl("https://api.weather.gov")
.defaultHeader("Accept", "application/geo+json")
.defaultHeader("User-Agent", "WeatherApiClient/1.0 (your@email.com)")
.build();
}

@Tool(description = "Get weather forecast for a specific latitude/longitude")
public String getWeatherForecastByLocation(
double latitude, // Latitude coordinate
double longitude // Longitude coordinate
) {
// 首先获取点位信息
String pointsResponse = restClient.get()
.uri("/points/{lat},{lon}", latitude, longitude)
.retrieve()
.body(String.class);

// 从点位响应中提取预报URL
String forecastUrl = JsonPath.read(pointsResponse, "$.properties.forecast");

// 获取天气预报
String forecast = restClient.get()
.uri(forecastUrl)
.retrieve()
.body(String.class);

// 从预报中提取第一个周期的详细信息
String detailedForecast = JsonPath.read(forecast, "$.properties.periods[0].detailedForecast");
String temperature = JsonPath.read(forecast, "$.properties.periods[0].temperature").toString();
String temperatureUnit = JsonPath.read(forecast, "$.properties.periods[0].temperatureUnit");
String windSpeed = JsonPath.read(forecast, "$.properties.periods[0].windSpeed");
String windDirection = JsonPath.read(forecast, "$.properties.periods[0].windDirection");

// 构建返回信息
return String.format("Temperature: %s°%s\nWind: %s %s\nForecast: %s",
temperature, temperatureUnit, windSpeed, windDirection, detailedForecast);
// Returns detailed forecast including:
// - Temperature and unit
// - Wind speed and direction
// - Detailed forecast description
}

@Tool(description = "Get weather alerts for a US state")
public String getAlerts(@ToolParam(description = "Two-letter US state code (e.g. CA, NY)") String state) {

// 获取指定州的天气警报
String alertsResponse = restClient.get()
.uri("/alerts/active/area/{state}", state)
.retrieve()
.body(String.class);

// 检查是否有警报
List<Map<String, Object>> features = JsonPath.read(alertsResponse, "$.features");
if (features.isEmpty()) {
return "当前没有活动警报。";
}

// 构建警报信息
StringBuilder alertInfo = new StringBuilder();
for (Map<String, Object> feature : features) {
String event = JsonPath.read(feature, "$.properties.event");
String area = JsonPath.read(feature, "$.properties.areaDesc");
String severity = JsonPath.read(feature, "$.properties.severity");
String description = JsonPath.read(feature, "$.properties.description");
String instruction = JsonPath.read(feature, "$.properties.instruction");

alertInfo.append("警报类型: ").append(event).append("\n");
alertInfo.append("影响区域: ").append(area).append("\n");
alertInfo.append("严重程度: ").append(severity).append("\n");
alertInfo.append("描述: ").append(description).append("\n");
if (instruction != null) {
alertInfo.append("安全指示: ").append(instruction).append("\n");
}
alertInfo.append("\n---\n\n");
}

return alertInfo.toString();
}

// ......
}

通过 api.weather.gov 来请求天气服务,给出结果
然后通过一个客户端来访问请求下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static void main(String[] args) {
var stdioParams = ServerParameters.builder("java")
.args("-jar", "/Users/shixuesen/Downloads/mcp-demo/target/mcp-demo-0.0.1-SNAPSHOT.jar")
.build();

var stdioTransport = new StdioClientTransport(stdioParams);

var mcpClient = McpClient.sync(stdioTransport).build();

mcpClient.initialize();

// McpSchema.ListToolsResult toolsList = mcpClient.listTools();

McpSchema.CallToolResult weather = mcpClient.callTool(
new McpSchema.CallToolRequest("getWeatherForecastByLocation",
Map.of("latitude", "47.6062", "longitude", "-122.3321")));
System.out.println(weather.content());

McpSchema.CallToolResult alert = mcpClient.callTool(
new McpSchema.CallToolRequest("getAlerts", Map.of("state", "NY")));

mcpClient.closeGracefully();
}

这个就是实现了mcp的简单示例,几个问题,要注意java的多版本管理,我这边主要是用jdk1.8,切成 17需要对应改maven的配置等

上次简单介绍了openmanus的使用,但是它究竟是怎么个原理还是一知半解的,如果想要能比较深入的理解,最直接粗暴的就是阅读源码了,然而对于很多人包括我来说阅读源码不是件简单的事情,有时候会陷入局部细节,不得要领
正好这次我发现了有个理解项目的神器,这次不加双引号是因为这个真的好
比如我们就拿openmanus举例,首先python的语法就没那么熟,以及对整体结构的理解
那么我们就可以打开 openmanus 的仓库地址
https://github.com/mannaandpoem/OpenManus
然后把 github 替换成 deepwiki,
页面变成了这样

左边是结构大纲,中间我们可以看到项目的主体介绍,包括有核心的架构图,agent的继承关系,tool的生态系统,LLM的集成

这里就展示了核心架构,通过这个方式我们如果想对一个项目有个初始的认识,就变得非常简单,因为很多项目的当前的代码都是非常复杂的,没有足够的时间精力是没办法一下子学习到项目的整体结构,因为除非我们是要真正投入到一个项目的开发贡献中,我们大概率都是从了解整体的概况,再分模块的去学习,而这个正是这个deepwiki做得非常牛的地方,相当于给每个项目都加了一个更详细具体的wiki,更牛的还有我们可以通过对话的形式进行提问题,比如我们想自己开发个工具,让openmanus集成进去进行调用

它就给出了一个非常详尽的回答,
首先

  1. 创建自定义工具类,创建一个基于 BaseTool 的新工具类
  2. 将工具添加到Manus代理,修改Manus类的available_tools定义,将您的工具添加到默认工具列表
  3. 使用MCP协议集成远程工具(可选)
  4. 工具执行流程
    一旦您的工具被集成,Manus代理会在执行过程中使用它:
  • 代理的think()方法会向LLM发送请求,包含所有可用工具的信息
  • LLM会决定使用哪个工具(包括您的Excel工具)
  • 代理的act()方法会执行工具调用
  • 您的工具的execute()方法会被调用,执行Excel函数并返回结果
  • 结果会被添加到代理的记忆中,用于后续决策
    这样子就让一个项目的理解跟上手变得非常简单,甚至比如我们想要参与这个项目的开源贡献,也能借助这个 deepwiki 来让我们能快速上手。
    如果对这个结果不满意还可以开启deep research,能让大模型通过深度思考来给出更加合理的答案,这个deepwiki是目前为止我觉得大模型对程序员最有效的一个工具了。
0%