阅读视图
栗子前端技术周刊第 114 期 - jQuery 4.0、Chrome 144、Node.js v25.4.0...
老板说拼图游戏太卷了,让我用Cocos做个3d版本的...
引言
哈喽大家好,我是亿元程序员,上次老板让做拼图游戏的那个小伙伴又来了:
亿哥,我又来了(捂脸),上次我们老板不是让我们趁热抢个拼图游戏头位吗?
结果还是慢了一节,投流基本纯亏,现在大家都在拼题材和变种了(哭笑不得)。
老板却异想天开,让我搞一个
3d版本的(捂脸),我这没有经验咋搞啊!
别慌,3d版本的拼图游戏?那不就是拼模型!其实很早就有小伙伴咨询笔者关于Cocos游戏开发中的动态切割模型!(之前还不知道意欲何为,现在想起大有乾坤。)
言归正传,本期带大家一起来看看,如何在Cocos游戏开发中,实现一个3d版本的拼图(拼模型)游戏,并加入到我的亿元Cocos小游戏实战合集中去。
本文源工程可在文末获取,小伙伴们自行前往。
简单分析一下
3d的拼图游戏其实可以做成拼模型的游戏,就是基本规则沿用拼图游戏的规则:
将完整的图片按照难度分割成
n*n的碎片,然后打乱,玩家通过交换拼图最终把图片恢复完整,包括一些特殊的规则,例如正确相邻的图片会合并成整体。
拼模型游戏就是按照上述规则,将图片替换成模型即可,其关键的第一步就是要把模型动态分割。
那么问题来了,如何将一个模型进行动态分割?
动态模型分割
其实关于动态模型分割,笔者之前的文章就已经做过了介绍,感兴趣的小伙伴可以前往阅读《如何在CocosCreator3.8中实现动态切割模型?》 。
动态模型分割,说是分割,其实是创建,就是动态创建被切割平面分割的两部分子模型,然后把原模型销毁。
其中最关键的API就是utils.MeshUtils.createMesh。这里面涉及的知识点实在是非常多,包括分割、生成、割面填充等等,不要求小伙伴们都会,会用就行,或者会指挥AI用就行。
既然分割起来这么麻烦,有没有更简单的方法?
美术妹子分割
美术妹子 : 我再次谢谢你!(为什么是再次?)
这时候又有小伙伴提出疑问了:
不是说程序能完成的事情,尽量不要麻烦美术妹子吗?
就不怕妹子追你九条街?
其实对于没有3d游戏开发经验的小伙伴来说,美术妹子分割好给到都不一定能完成游戏的开发,更何况通过程序去动态切割!
说到这里,我相信美术妹子会理解的,模型能切割成的份数,很多情况下是可以确定的,所以试试看,说不定能成。
话又说回来,没有3d游戏开发经验的小伙伴,该怎么着手开发拼模型游戏?
创建项目
万事开头难,首先第一步就要创建项目,这里注意要创建一个3d的项目。
区别于2d游戏项目,项目创建后,场景中会自带灯光和相机,以及三维的场景编辑器。
下面列举一些常用的操作:
-
定位物体 :可以通过双击或者快捷键
F,快速定位物体。 -
场景漫游 :按下鼠标右键后,可以通过快捷键
w、s、a、d、e、q漫游场景。 -
调整相机视角 :调整好合适的视角后,选中相机后可以通过菜单栏
节点->将节点对齐到场景视角快速调整相机视角。 -
快速回到相机视角 :选中相机后可以通过菜单栏
节点->将场景视角对齐到与节点快速回到相机视角。
这太基础了,你上你也行,那如何让界面既能显示3d模型又能显示UI?
多相机共存
**如上图所示,**游戏画面中包含UI和模型,意味着场景中可能包含至少2个相机。
控制相机拍摄哪些物体可以通过Visibility属性去控制。
控制相机的渲染优先级可以通过Priority属性去控制,如上面拍背景的相机为0、拍模型的相机为1,拍弹框的相机为2。
说了那么多,可以开始实战写模型游戏,写代码没?
拼模型游戏实战
相信小伙伴们通过上面的文章内容,已经学会如何着手开发拼模型游戏了吧!
由于本期还没有贴熟悉的代码,笔者还是决定补充一些关键代码:
-
屏幕点击3d物体 : 首先通过
screenPointToRay方法将点击转成射线,然后通过PhysicsSystem.instance.raycast获取到对应的物体。 -
拖拽3d物体 :
-
3d物体动画 : 直接用熟悉的
tween动画即可。
效果演示
结语
拼图游戏现在都玩出花了,要不要做个3d版本的试一试?
小伙伴们,你们都学会了吗?
本文实战完整源码已集成到亿元Cocos小游戏实战合集,内含体验链接,通过添加模型即可完成关卡的配置。
我是"亿元程序员",一位有着8年游戏行业经验的主程。在游戏开发中,希望能给到您帮助, 也希望通过您能帮助到大家。
AD:笔者线上的小游戏《打螺丝闯关》《贪吃蛇掌机经典》《重力迷宫球》《填色之旅》《方块掌机经典》大家可以自行点击搜索体验。
实不相瞒,想要个赞和爱心!请把该文章分享给你觉得有需要的其他小伙伴。谢谢!
推荐文章:
失业,仲裁,都赶上了(四)
供应链系统中的 Web 打印方案的探索实践
作者:刘锦泉
一、前言
在供应链业务系统中,打印是一项难以被完全数字化替代的基础能力。尽管系统形态不断向 Web 化、平台化演进,但在仓储、物流、配送等线下环节,大量核心流程依然依赖纸质单据(如面单、送货单、运输单)完成流转与交接。
打印的稳定性与一致性,往往直接影响现场作业效率。一旦打印出现异常,问题会迅速放大,甚至直接阻断线下业务流程。
基于真实的供应链项目实践,本文将围绕浏览器打印这一常被低估的能力,系统性地梳理主流方案的技术取舍,并重点展开基于 window.print 的实现思路与工程实践。
二、供应链系统中的主流打印方案调研
围绕“如何将单据稳定、可靠地打印出来”,在实际项目中常见的 Web 打印方案大致可以分为三类:
- 基于 DOM 的浏览器原生打印方案
- 基于可视化模板的前端打印方案
- 基于本地打印控件的软件方案
下面结合实际项目经验,对这三类方案进行简要分析。
1.基于DOM浏览器原生打印方案
这是目前最常见、也是前端介入成本最低的一种打印实现方式。
其核心思路是:
直接使用浏览器原生打印能力,将页面或指定 DOM 节点渲染为打印内容。
打印效果如下图:
实现方式
- 原生 window.print()
// window.print
window.print
- 使用 print-js 等开源库进行增强
官网地址:传送门(printjs.crabbly.com/)
// printJs
printJS({
printable: 'print-area',
type: 'html',
style: '@page { size: A4; margin: 10mm; }',
targetStyles: ['*']
});
优点
- 零额外依赖,完全基于浏览器原生能力
- 浏览器兼容性优秀:Chrome、Firefox、Safari、Edge 均支持
- 开发与维护成本低,对现代前端技术栈(React / Vue)非常友好
- 页面即模板,业务变更成本低,适合供应链中高频调整的单据场景
缺点
- 样式控制高度依赖 CSS,对分页、跨页控制要求较高
- 批量打印需自行实现调度与队列逻辑
-
打印体验受浏览器限制:
- 页眉页脚难以完全控制
- 无法绕开系统打印弹窗
2.基于可视化模板的前端打印方案(HiPrint)
以 HiPrint 为代表的方案,通常基于 jQuery,通过拖拽方式设计打印模板,再将业务数据动态渲染到模板中进行打印。HiPrint 官网地址:传送门(hiprint.io/)
配置示例效果如下图:
实现方式
// 基于jQuery的打印设计器
$('#element').hiPrint({
designer: true, // 开启可视化设计
templates: [...] // 预设模板
});
优点
-
所见即所得的可视化模板设计
- 拖拽式配置
- 非开发人员也可参与模板调整
-
布局组件丰富
- 表格、文本、条码、图片等组件齐全
- 支持动态数据绑定
-
模板管理能力完善
- 模板导入 / 导出
- 支持模板版本管理
缺点
- 技术栈依赖性强:强依赖 jQuery,与 React / Vue 整合成本高
-
性能问题明显:
- 模板复杂时渲染速度下降
- 大数据量场景容易卡顿
-
样式隔离难度大:
- 容易与现有系统样式冲突
- 需要额外处理样式覆盖与作用域
3.基于本地打印控件的软件方案(LODOP)
LODOP 是一类 依赖本地打印控件或客户端程序 的打印方案,通过浏览器与本地程序通信完成打印。在早期政务、财务、制造业系统中使用较多。官网地址:传送门(www.lodop.net/)
实现方式
- 先去Lodop官网下载相应的安装包
- 解压安装后将LodopFuncs.js放在项目中
- 具体页面引入 Lodop
import { getLodop } from "@/plugins/LodopFuncs";
let LODOP = getLodop()
- 执行打印指令
// 需要先安装LODOP软件
LODOP.PRINT_INIT("打印任务");
LODOP.ADD_PRINT_TEXT(50, 100, 200, 30, "供应链单据");
LODOP.PRINT();
优点
-
打印控制能力极强
- 支持毫米级定位
- 可直接控制打印机指令
- 对专业打印设备支持完善
缺点
-
浏览器兼容性极差
- 依赖 ActiveX
- 现代浏览器与移动端基本不支持
- 只支持windows 系统
-
部署与运维成本高
- 每台客户端需安装软件
- 需要管理员权限
-
安全性与可持续性问题
- ActiveX 存在安全隐患
- 已逐步被浏览器厂商淘汰
4.方案对比一览表
| 维度 | window.print | jQuery HiPrint | LODOP |
|---|---|---|---|
| 开发成本 | 极低 | 中等 | 高 |
| 打印精度 | 一般(受限于浏览器) | 良好 | 极高 |
| 跨浏览器 | 优秀 | 良好 | 极差 |
| 部署成本 | 零部署 | 低 | 高(需安装客户端) |
| 维护成本 | 低 | 中等 | 高 |
5.方案选型结论
结合当前业务特点:
- 需要快速适配不同客户与环境
- 系统主体基于 React 技术栈
- 单据模板相对稳定,支持页面预览
最终选择了 基于 DOM 的浏览器打印方案,并以 window.print 作为核心实现方式。
为什么直接使用 PrintJs ?
可能会有人疑惑,既然是 DOM 打印,为什么不使用 print-js 进行封装?主要原因如下:
-
print-js 需要能够直接获取到打印节点(dom 的id 或者原始的html)
- 必须将预览节点在打印前挂载到页面上。
-
iframe 隔离成本高
- 需要单独引入工程化依赖包CSS 资源。如: Antd等
-
样式维护成本高
- 额外样式需要么通过字符串方式注入,要么将工程的less 转换成css 引入,不利于后续维护。
总的来说,print-js 在工程化配置项目依旧不是那么方便还用。
三、window.print 的能力边界
window.print() 本质上是触发浏览器打印对话框。当打印动作触发时,浏览器会将当前页面最终渲染的结果输出为打印内容,或者导出为 PDF 文件。
也正因为这一点,window.print 天生存在一个非常重要的限制:
它无法直接指定“只打印某一个 DOM 区域”。
浏览器并不知道你只想打印某一个 div,它只会按照当前页面的渲染结果,完整地执行一次打印流程。
这意味着,如果页面上存在导航栏、按钮、筛选条件等元素,它们同样会被一并打印出来。
四、指定区域打印的实现思路
既然 window.print 无法直接指定打印区域,那么解决问题的思路就非常清晰了:
在触发打印之前,主动控制页面最终的渲染结果,让真正参与渲染的内容只剩下需要打印的部分。
顺着这个思路,第一个最直观的方案自然就出现了。
1.innerHTML 直接替换页面内容
既然浏览器只会打印当前页面的渲染结果,那么我们是否可以在打印前,直接用需要打印的内容替换整个页面?
实现方式也很简单:
- 缓存当前页面内容
- 将
document.body.innerHTML替换为打印区域内容 - 执行
window.print() - 打印完成后再恢复页面
代码示例如下:
// 获取预览组件的html
const getPreviewContainerHtml = (component) => {
const div = document.createElement('div');
flushSync(() => {
ReactDOM.render(component, div);
})
return div.innerHtml
}
const prinit = () => {
const oldHtml = document.body.innerHtml;
const printHtml = getPreviewContainerHtml(<PreviewContainer />);
document.body.innerHtml = printHtml;
window.print()
document.body.innerHtml = oldHtml;
}
这种方案在早期项目中非常常见。但随着实践的深入,很快就会发现一个问题:
直接替换 innerHTML 会彻底打破原有页面的渲染结构。
在 React / Vue 等现代前端框架中,这种方式会导致:
- 组件状态丢失
- 事件绑定失效
- 页面需要强制刷新才能恢复
因此,这种方案虽然思路直接,但在复杂系统中并不安全。
2.新开标签页打印
既然直接替换当前页面会破坏原有渲染结构,那么很自然就会想到:
如果不动当前页面,而是新开一个标签页,专门用来承载打印内容,是否就可以解决这个问题?
新开标签页后,我们可以:
- 在新页面中渲染完整的打印内容
- 调用新页面的
window.print() - 当前业务页面完全不受影响
从技术角度看,这个方案确实可以稳定地打印出我们想要的内容。但在真实业务场景中,很快又会暴露出新的问题:
- 浏览器可能拦截弹窗
- 用户需要多一步切换窗口的操作
- 打印流程被打断,体验不够流畅
对于仓库、物流等高频打印场景来说,这种额外的交互成本是不可忽略的。
于是问题进一步演化为:
能否在不新开标签页、不破坏当前页面的前提下,在当前页签内完成指定内容的打印?
3.iframe 当前页签打印
顺着这个问题继续往下推,自然而然就会引出 iframe 打印方案。
其核心思路是:
- 在当前页面中动态创建一个隐藏的
iframe - 将需要打印的内容完整渲染到 iframe 中
- 调用 iframe 内部的
print()方法完成打印
示例代码如下:
const print = () => {
const iframe = document.createElement('iframe');
iframe.src = `预览页地址`;
iframe.setAttribute(
'style',
'visibility: hidden; height: 0; width: 0; position: absolute; border: 0'
);
window.addEventListener('onafterprint', () => {
console.log('打印完成');
iframe.remove();
});
document.body.appendChild(iframe);
iframe.onload = () => {
setTimeout(function () {
iframe.contentWindow?.print();
}, 1000);
};
};
从浏览器的角度来看,iframe 本身就是一个完整的页面环境,因此可以独立完成打印流程,而不会影响主页面的渲染状态。
这一方案在实践中具备明显优势:
- 不破坏当前页面状态
- 不依赖新窗口或弹窗
- 对 React / Vue 等现代框架友好
- 非常适合供应链系统中的高频打印场景
4.批量打印
到目前为止,我们已经可以稳定地完成 单个单据的打印。但在供应链场景中,一个非常常见的需求是:
一次性打印多张面单、送货单或运输单。
在浏览器环境下,这一需求首先会受到一个客观限制:
- 浏览器在同一时间只能处理一个打印任务
- 调用
window.print()时,浏览器会弹出系统打印对话框 - 在用户完成打印设置并关闭对话框之前,页面线程会被阻塞
这意味着,通过循环多次调用 window.print() 来实现批量打印并不可行。
批量渲染内容 + CSS 分页
基于上述限制,一个更合理的思路是:在一次打印动作中,承载所有需要打印的单据内容。具体做法是:
- 遍历需要打印的单据数据
- 将每一张单据渲染为独立的打印容器
- 通过 CSS 控制分页规则
- 最终只触发一次 window.print()
示例代码如下:
// 批量创建打印容器
printPageList.map(() => {
return <PrintContent className="single-order-page" />
})
// css 配置
/* 确保每个订单在新页开始 */
.single-order-page {
page-break-before: always;
page-break-inside: avoid;
}
/* 第一个订单不需要分页 */
.single-order-page:first-child {
page-break-before: avoid;
}
通过这种方式,浏览器会将每个单据视为一页内容,顺序稳定、实现简单,非常适合版式统一的批量单据打印场景。
大批量场景下的性能考量
需要注意的是,当单据数量非常大(例如超过 50 张)时,一次性渲染所有打印内容,可能会带来内存占用和页面卡顿的问题。在这种情况下,可以考虑引入串行队列式的打印方案。
该方案的核心思路是:
- 构建一个打印任务队列
- 每次只渲染并打印一张单据
- 当前打印结束后,再进入下一张单据的处理
但需要特别注意的是,浏览器无法准确感知打印完成的时机,因此该方案通常需要额外的控制手段,例如:
- 由用户交互驱动(如点击“下一张”)
- 通过时间间隔进行节流控制
这种方式更适合作为大批量或特殊场景下的补充方案,而不是默认选择。
五、核心 CSS 打印配置:从“能打”到“好用”
在基于浏览器的打印方案中,CSS 决定了最终的输出效果。合理的打印样式配置,往往可以显著减少版式错乱、内容截断等问题,也是浏览器打印能否真正落地的重要前提。
下面结合实际项目,整理几类最常用、也最容易被忽视的打印样式配置。
1.使用 @media print 定义专用打印样式
浏览器提供了 @media print 媒体查询,用于在打印场景下覆盖页面的默认样式。
通过该方式,可以做到:
- 页面正常浏览时样式不受影响
- 打印时只应用与单据相关的样式规则
@media print {
/* 打印场景下隐藏页面中的非单据内容 */
.no-print {
display: none !important;
}
}
2.页面尺寸与边距控制
如果不对打印页面的尺寸和边距进行控制,常见问题包括:
- 内容被裁切
- 页面留白过多
- 不同浏览器下表现不一致
通过 @page 可以明确指定打印纸张规格与边距:
@media print {
@page {
size: A4;
margin: 10mm;
}
}
3.避免表格内容被强制拆页
表格是供应链单据中最常见的结构,同时也是打印问题的高发区域。
如果不加控制,浏览器可能会在任意位置拆分页,导致一行数据被拆成两页。
@media print {
table {
border-collapse: collapse;
/* 避免整个表格在打印时被拆分 */
page-break-inside: avoid;
}
tr {
/* 避免单行数据跨页 */
page-break-inside: avoid;
}
}
4.主动控制分页位置
在部分业务场景中,需要对分页位置进行明确控制,例如:
- 一张送货单单独占一页
- 合计信息必须在新页展示
此时可以通过自定义分页样式来实现:
@media print {
.page-break {
/* 在该元素前强制分页 */
page-break-before: always;
}
}
5.打印颜色与背景处理
浏览器在打印时,默认会忽略背景颜色与部分样式,这在以下场景中尤为明显:
- 面单背景色
- 标签类单据
- 状态标识区域
@media print {
body {
/* 强制按页面样式输出颜色 */
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
}
注:Window 电脑如果不设置 -webkit-print-color-adjust: exact 打印输出水印内容, 会出现空白现象。
六、总结
浏览器打印在供应链系统中看似只是一个基础能力,但在真实项目中,往往涉及方案选型、技术栈适配、用户体验以及长期维护成本等多方面的权衡。
本文结合具体的业务背景,对主流 Web 打印方案进行了梳理,并重点分析了基于 window.print 的实现思路及其工程边界。
可以看到,window.print 本身并不复杂,真正的挑战更多来自页面渲染控制、样式管理以及打印流程的工程化处理。这也使得浏览器打印更像是一项系统能力的组合,而非单一 API 的简单调用。
在当前阶段,window.print 配合 iframe 依然是 Web 打印方案中性价比最高、可控性最强的一种实现方式。随着业务复杂度的提升,打印方案仍有进一步演进与细化的空间。
子弈的 2025 年度总结
学习
2025 年在学习上可以简单分为以下几块内容:
AI 实践学习
每一个同学对于 AI 的了解和学习程度都不一样,我自己简单总结了一下在接触 AI 应用开发过程中可能经历的几个阶段:
今年我发现身边越来越多的同事从对 AI 保持观望到快速进入 Vibe Coding 时代,大家会积极了解和使用市面上成熟的 AI 产品。当然对于前端开发者而言,IDE 是我们日常最直接接触的工具之一,去年我主要使用 Github Copilot 进行辅助开发,而今年则改为使用更强大的智能 IDE 进行编码:
当然随着 AI 的快速发展,智能编码也将进入更加智能的阶段:
目前市面上的智能 IDE 还无法理解各自领域的专业知识,一些隐形的经验或者历史的技术方案也无法为 AI 所复用,除此之外,IDE 也只能聚焦于当前工程项目的智能代码生成,未来的开发模式还会在 Vibe Coding 的基础上进行衍生,走向更加强大的开发范式,例如 SDD:
当然我理解的 SDD 只是一种开发范式,其核心是构建复杂的领域知识体系,并且其难点是如何让内部的一些深度领域知识可以精准的被索引,从而基于实际需求精准智能的生成代码。这些领域知识包括跨工程的知识体系、业务的术语表、Codebase Knowledge Graph RAG(代码的关联关系)以及历史经验的复用等,例如 Qoder 的 Quest 模式。
除了使用市面上存在的一些 AI 产品,更重要的是了解这些 AI 产品的实现原理或者自己尝试去实践一些 AI 技术。去年我深入学习了 Github Copilot 的实现原理,对智能代码补全背后的机制有了深刻认知:
今年则进一步研究了 OpenManus 的设计思路,持续追踪并借鉴社区中的优秀 AI 开源项目。同时,也尝试使用 Python 系统性设计并实现了一个基于 Function Calling 与 Mcp 协议、采用 ReAct 架构的 Agent,从而更直观地理解了基于 Agent 智能调度工具的流程与设计逻辑。
此外,我也系统学习了 Langchain 框架,并基于该框架结合 Playwright Mcp,设计并实现了一款面向前端研发的提效 Agent。虽然这些尝试都还处于初期阶段,但在过程中我收获了大量关于 AI 应用落地的细节认知。
OpenManus 原理学习
OpenManus 的学习过程可以用两个 XMind 思维导图来建立:
OpenManus 是一个开源的 Manus(智能体 / Agent)Python 项目,使用 ReAct 框架设计 Agent 并可以智能调度内部封装的开发者工具,这些工具包括:
利用内部实现的 ReAct 框架,OpenManus Step by Step 智能决策并使用如下工具来实现用户指令,例如:
使用 Google 搜索“钉钉开放平台的前端 JS API 调用示例”,帮我设计一个 HTML,能够引入 JS API 的 CDN 资源,并编写调用示例。
OpenManus 会像人一样思考和操作,它首先利用 Google 搜索引擎工具搜索最相关的 10 个网址,然后利用浏览器(Browser Use)工具逐一打开网址查看是否存在前端 JS API 示例相关的内容,如果第一个网址不存在,则会继续浏览第二个搜索到的网址,直到找到相关的内容并进行提取,最后利用文件系统工具生成代码文件。
OpenManus 是一个学习 Agent 知识非常好的开源项目,它的整体框架如下所示:
ReAct 总体执行流程如下所示:
think 执行流程:
act执行流程:
Browser Use 执行流程:
除此之外,OpenManus 也实现了基于 MCP 协议的工具调用方式,并且实现了 Muti-Agent & 任务规划模式,在原来的基础上进行了框架升级,如下所示:
温馨提示:大家可以思考一下使用这种设计模式可能会存在什么问题?
在 Muti-Agent 模式下能够对用户输入的指令进行 Plan 规划拆解,然后每一个 Step 会寻找合适的 Agent 进行执行,如下所示:
最终我们可以了解一下 OpenManus 的大致执行流程:
温馨提示:如果 Agent 和 Agent 之间需要通信,也可以使用 A2A 标准协议。
Muti-Agent 模式的总体执行流程:
创建流程实例 flow 执行调用了FlowFactory.create_flow,主要用于绑定相应的 Agents 集合:
AI 智能请求:
调用 PlanningTool 生成计划文本,信息如下:
根据任务步骤调用相应的 Agent 执行 Step:
ReAct 总体执行流程:
think 执行流程:
act执行流程:
Browser Use 执行流程:
真正在学习的过程中可以了解更多的细节问题,例如:
- Agent 到底有哪些组成部分?这些组成部分各自有什么功能?
- 工具的调度方式有哪些?什么是 Funcion Calling? 它有什么优点?
- Function Calling 执行后的对话信息需要包含哪些内容,如何描述工具的执行过程和执行结果并使得 LLM 可以更好的决策下一个执行任务?
- 没有 Function Calling 之前或者某些模型不支持 Function Calling 时该如何实现工具的智能决策调用?
- ReAct 和 CoT 的区别是什么?CoT 相比 ReAct 有什么缺点?ReAct 有哪些优缺点?ReAct 在解决哪些问题时准确性较高?
- 什么是 Reflexion 框架,相比 ReAct 它的优点是什么?
- ReAct 架构如何防止任务死循环或者卡死(幻觉)?它的优缺点是什么?
- 我们如何判断 ReAct 规划的策略是否越来越低效(例如规划步骤花费时间过多并且没有成功的迹象以及在环境中遇到一系列连续相同的动作并产生了相同的结果等)?
- 什么是记忆,什么是短期记忆,什么是长期记忆?记忆超过 LLM 上下文 token 限制该如何处理?
- 在 OpenMansu 中如何管理对话状态(上下文工程信息),对话状态中会包含哪些信息类型?
- 简单描述一下基于 Mcp 协议的工具调用执行流程?是否手动实现过该执行流程?
- 业界有哪些基于任务规划的 Agent 产品(例如 Same)?
- Muti-Agent 适用的场景有哪些?任务规划适合在什么场景下使用?
- 社区有哪些开源的 Muti-Agent 框架?
- Muti-Agent 的优缺点是什么?
- OpenManus 是如何设计 Flow 任务规划的 System Prompt、Planning Tools Params Prompt?如何设计 Manus Agent 的 System Prompt、User Input Prompt、Next Step Prompt、Tools Params Prompt、Assistant Prompt、Tools Observed Prompt、Page Prompt(从页面提取用户输入相关的内容)等,整个上下文工程是其内部的精髓!
Langchain 框架学习
Langchain 的学习过程可以用两个 XMind 思维导图来建立:
主要学习了Langchain 框架的功能(不包含 LangGraph),也体会到了 Langchain 的强大之处,包括:
目前正在尝试使用 Langchain 来设计前端产研提效的 Agent 工具,能够支持应用的全链路开发模式。在学习和使用 Langchain 框架的同时也能系统地了解和 Agent 相关的功能设计,当然真正在实践的过程中还是会遇到各种问题,例如:
- 如何整理相应的知识文档,需要包含哪些应用研发信息,如何确保这些信息的 RAG 索引的准确性?
- 知识文档如何承载,如何实时更新索引信息?
- RAG 的实现流程可以分为哪几个步骤?
- 为什么需要对文档进行切割?文档切割的策略有哪些(Levels Of Text Splitting)?
- 如何保证检索的连贯性?如何进行国际化检索?RAG 的检索策略有哪些?
- 嵌入模型如何选择,需要考虑哪些衡量指标?向量数据库有哪些,是否有类似的 GUI 工具?
- Agent 设计中常用的 RAG 检索方案是什么?2-Step RAG 和 Agentic RAG 有什么优缺点?
- 如何评估 RAG 的准确性,有什么评估标准?
- 如何解决过多 Mcp 服务的连接压力和性能问题(本质是不要让每个 AI Agent / 会话与 N 个 MCP Server 全量长连接,应改成“按需路由 + 连接池 / 复用 + 分层聚合 + 隔离与限流”的架构,把连接数、握手成本、心跳与上下文维持成本从 N×1000 降到“实际用到的少数几个”)?
- 有哪些压缩记忆的方法方式,各自又有什么优缺点?压缩记忆后如何避免记忆失真?
- 如何抉择 ReAct 和 Flow 执行流程?
- 在 ReAct 中如何抉择并行调用多个工具还是单个工具串行调用?
- 如何处理多步工具调用(multi-step tool calling) 中常见的 "幻觉性规划"(hallucinated planning) 问题?
- Mutil-Agent 模式有什么优点? Tool calling 模式和 Handoffs 模式如何抉择?
- 什么情况下需要设计 Mutil-Agent 模式?
- 在 Langchain 中应该如何抉择使用 PlayWright Browser Toolkit、Browser Use、playwright-mcp 和 playwright?它们各自有什么优缺点?
- Playwright 自定义工具封装和 Playwright Mcp 如何抉择(一个可控且消耗 Token 小,一个不可控但功能更灵活)?
- Playwright 如何解决浏览器免登的问题?
- 在设计 Agent 的过程中有哪些可观测的工具可以追溯 Agent 内部的 ReAct 执行过程?
React 原理学习
虽然对于 Vue 2.x、Svelte 等框架的原理相对比较清晰,但是对于 React 一直没有深入进行原理了解,尽管学习这些原理可能不如学习 AI 来得实在,从某些程度来讲也稍微有些过时,不过出于好奇心,并且为了速成,参考了图解 React 原理快速学习了一把,如下所示:
学习了以后也会产生很多相关的问题,例如:
- React 的启动模式有哪些?什么是 Concurrent 模式?这些模式有什么区别?
- 在 Reconciler 运作流程中同步渲染和并发渲染的区别是什么?
- React 在哪些情况下会让出代码的执行权(可中断与恢复机制),从而用于提升用户体验?
- React 的时间切片为什么是 5ms?
- 简述 React 的任务调度过程?
- React 调度分片工具使用的是什么异步 API ? 为什么不是 rIC、rAF、setTimeout?为什么不使用微任务?
- 如果 Fiber 树的构造过程非常耗时,如何处理构造的中断和恢复?
- 简述一下 Fiber 树构造中使用的双缓冲技术?它有什么优点(并发模式、可中断、回退策略)?
-
setState在什么情况下分别进行同步和异步执行? - 在注册调度任务时对于同时连续多次
setState是如何处理的?调度中心是如何处理节流优化的? - 为什么 Fiber 是树 + 链表的结构?
- 渲染阶段和提交阶段都是可中断的吗?
- 什么是副作用链表?它为什么是链表结构(对于可中断与恢复机制的支持)?
- 在 Fiber 树构造时如何对副作用链表(例如周期函数、
useEffect、useLayoutEffect)进行处理? - 从 Fiber 树角度出发讲述父子节点的副作用执行顺序以及如何避免 DOM 的重排?
- Fiber 树的渲染可以分为哪几个阶段?
useEffect在 Fiber 树渲染的哪个阶段执行?它是异步执行吗? - React Hooks 和
useEffect是在宏任务中执行还是微任务中执行?它是通过调度任务执行的吗?从原理角度出发讲解它为什么能够模拟componentDidMount周期函数? - 在初次 Fiber 树渲染中
componentDidMount周期函数和useEffect副作用的执行时机谁更早? -
useLayoutEffect和componentDidMount、componentDidUpdate执行时机相等吗? - 为什么在 React 中要引入 Hook ?它的动机是什么?
- 在双缓冲机制中如何复用副作用链表?
-
useReducer会进行任务调度执行吗?同时连续的多次dispatch会进行节流优化吗? -
useLayoutEffect是同步执行的吗?为什么会阻塞浏览器的绘制? - 为什么在
useLayoutEffect中可以直接拿到 DOM 的尺寸(即在浏览器绘制之前)? - React 是如何判断 Hook 副作用函数的依赖项变化的?
金融
2025 年开始从 0 到 1 学习理财知识,想要做好理财首先需要学习相关的基础知识,今年学习到的基础知识如下所示:
当然在学习的过程中也需要阅读大量的金融书籍,借鉴伟人的经验和一些历史金融大事件经验,包括:
自从开始理财后,发现自己会关注国家政策的变化、世界的政治大事件、当日股市波动的利空利好信息等,也会关注一些金融博主的短视频信息。目前作为理财小白觉得自己最重要的几个实操学习点如下:
运动
学习固然重要,但是保持好健康的身体才能有更加持续的学习动力,2025 的整体运动情况如下所示:
How to Generate SSH Keys on Linux
拥抱PostgreSQL支持UI配置化
每日一题-最小绝对差🟢
给你个整数数组 arr,其中每个元素都 不相同。
请你找到所有具有最小绝对差的元素对,并且按升序的顺序返回。
每对元素对 [a,b] 如下:
-
a , b均为数组arr中的元素 a < b-
b - a等于arr中任意两个元素的最小绝对差
示例 1:
输入:arr = [4,2,1,3] 输出:[[1,2],[2,3],[3,4]]
示例 2:
输入:arr = [1,3,6,10,15] 输出:[[1,3]]
示例 3:
输入:arr = [3,8,-10,23,19,-4,-14,27] 输出:[[-14,-10],[19,23],[23,27]]
提示:
2 <= arr.length <= 10^5-10^6 <= arr[i] <= 10^6
Button Pattern 详解
【宫水三叶】简单排序模拟题
排序 + 模拟
数据范围为 $1e5$,我们不能通过枚举的方式遍历所有的点对找最小值。
我们可以对 arr 进行排序,容易得知差值最小值必然发生在排序数组的相邻元素之间,此时我们可以通过遍历排序数组并使用变量 min 记录当前差值最小值来统计答案。
代码:
###Java
class Solution {
public List<List<Integer>> minimumAbsDifference(int[] arr) {
Arrays.sort(arr);
List<List<Integer>> ans = new ArrayList<>();
int n = arr.length, min = arr[1] - arr[0];
for (int i = 0; i < n - 1; i++) {
int cur = arr[i + 1] - arr[i];
if (cur < min) {
ans.clear();
min = cur;
}
if (cur == min) {
List<Integer> temp = new ArrayList<>();
temp.add(arr[i]); temp.add(arr[i + 1]);
ans.add(temp);
}
}
return ans;
}
}
- 时间复杂度:$O(n\log{n})$
- 空间复杂度:$O(\log{n})$
同类型加餐
题太简单?今日份加餐:【面试高频题】难度 1.5/5,数据结构运用题 🎉 🎉
或是考虑加练如下「模拟」题 🍭🍭🍭
| 题目 | 题解 | 难度 | 推荐指数 |
|---|---|---|---|
| 6. Z 字形变换 | LeetCode 题解链接 | 中等 | 🤩🤩🤩 |
| 8. 字符串转换整数 (atoi) | LeetCode 题解链接 | 中等 | 🤩🤩🤩 |
| 12. 整数转罗马数字 | LeetCode 题解链接 | 中等 | 🤩🤩 |
| 59. 螺旋矩阵 II | LeetCode 题解链接 | 中等 | 🤩🤩🤩🤩 |
| 65. 有效数字 | LeetCode 题解链接 | 困难 | 🤩🤩🤩 |
| 73. 矩阵置零 | LeetCode 题解链接 | 中等 | 🤩🤩🤩🤩 |
| 89. 格雷编码 | LeetCode 题解链接 | 中等 | 🤩🤩🤩🤩 |
| 166. 分数到小数 | LeetCode 题解链接 | 中等 | 🤩🤩🤩🤩 |
| 260. 只出现一次的数字 III | LeetCode 题解链接 | 中等 | 🤩🤩🤩🤩 |
| 414. 第三大的数 | LeetCode 题解链接 | 中等 | 🤩🤩🤩🤩 |
| 419. 甲板上的战舰 | LeetCode 题解链接 | 中等 | 🤩🤩🤩🤩 |
| 443. 压缩字符串 | LeetCode 题解链接 | 中等 | 🤩🤩🤩🤩 |
| 457. 环形数组是否存在循环 | LeetCode 题解链接 | 中等 | 🤩🤩🤩🤩 |
| 528. 按权重随机选择 | LeetCode 题解链接 | 中等 | 🤩🤩🤩🤩 |
| 539. 最小时间差 | LeetCode 题解链接 | 中等 | 🤩🤩🤩🤩 |
| 726. 原子的数量 | LeetCode 题解链接 | 困难 | 🤩🤩🤩🤩 |
注:以上目录整理来自 wiki,任何形式的转载引用请保留出处。
最后
如果有帮助到你,请给题解点个赞和收藏,让更多的人看到 ~ ("▔□▔)/
也欢迎你 关注我 和 加入我们的「组队打卡」小群 ,提供写「证明」&「思路」的高质量题解。
所有题解已经加入 刷题指南,欢迎 star 哦 ~
[Python/Java/TypeScript/Go] 排序模拟
解题思路
排序后所有可能的最小绝对值由每对儿相邻的差构成
代码
###Python3
class Solution:
def minimumAbsDifference(self, arr: List[int]) -> List[List[int]]:
m, ans = inf, []
for a, b in pairwise(sorted(arr)):
if (cur := b - a) < m:
m, ans = cur, [[a, b]]
elif cur == m:
ans.append([a, b])
return ans
###Java
class Solution {
public List<List<Integer>> minimumAbsDifference(int[] arr) {
Arrays.sort(arr);
List<List<Integer>> list = new ArrayList<>();
int min = Integer.MAX_VALUE;
for (int i = 0; i < arr.length - 1; i++) {
int diff = arr[i + 1] - arr[i];
if (diff < min) {
min = diff;
list.clear();
List<Integer> cur = new ArrayList<>();
cur.add(arr[i]);
cur.add(arr[i + 1]);
list.add(cur);
} else if (diff == min) {
List<Integer> cur = new ArrayList<>();
cur.add(arr[i]);
cur.add(arr[i + 1]);
list.add(cur);
}
}
return list;
}
}
###TypeScript
function minimumAbsDifference(arr: number[]): number[][] {
arr.sort((a, b) => a - b)
let ans = new Array<Array<number>>(), min = Number.MAX_SAFE_INTEGER
for (let i = 0; i < arr.length - 1; i++) {
const cur = arr[i + 1] - arr[i]
if (cur < min) {
min = cur
ans = [[arr[i], arr[i + 1]]]
} else if (cur == min) {
ans.push([arr[i], arr[i + 1]])
}
}
return ans
};
###Go
func minimumAbsDifference(arr []int) (ans [][]int) {
sort.Ints(arr)
for i, min := 0, math.MaxInt32; i < len(arr) - 1; i++ {
if diff := arr[i + 1] - arr[i]; diff < min {
min = diff
ans = [][]int{[]int{arr[i], arr[i + 1]}}
} else if diff == min {
ans = append(ans, []int{arr[i], arr[i + 1]})
}
}
return
}
最小绝对差
方法一:排序 + 一次遍历
思路与算法
首先我们对数组 $\textit{arr}$ 进行升序排序。这样一来,拥有「最小绝对差」的元素对只能由有序数组中相邻的两个元素构成。
随后我们对数组 $\textit{arr}$ 进行一次遍历。当遍历到 $\textit{arr}[i]$ 和 $\textit{arr}[i+1]$ 时,它们的差为 $\delta = \textit{arr}[i+1] - \textit{arr}[i]$。我们使用一个变量 $\textit{best}$ 存储当前遇到的最小差,以及一个数组 $\textit{ans}$ 存储答案:
-
如果 $\delta < \textit{best}$,那么说明我们遇到了更小的差值,需要更新 $\textit{best}$,同时 $\textit{ans}$ 清空并放入 $\textit{arr}[i]$ 和 $\textit{arr}[i+1]$;
-
如果 $\delta = \textit{best}$,那么我们只需要将 $\textit{arr}[i]$ 和 $\textit{arr}[i+1]$ 放入答案数组即可。
代码
###C++
class Solution {
public:
vector<vector<int>> minimumAbsDifference(vector<int>& arr) {
int n = arr.size();
sort(arr.begin(), arr.end());
int best = INT_MAX;
vector<vector<int>> ans;
for (int i = 0; i < n - 1; ++i) {
if (int delta = arr[i + 1] - arr[i]; delta < best) {
best = delta;
ans = {{arr[i], arr[i + 1]}};
}
else if (delta == best) {
ans.emplace_back(initializer_list<int>{arr[i], arr[i + 1]});
}
}
return ans;
}
};
###Java
class Solution {
public List<List<Integer>> minimumAbsDifference(int[] arr) {
int n = arr.length;
Arrays.sort(arr);
int best = Integer.MAX_VALUE;
List<List<Integer>> ans = new ArrayList<List<Integer>>();
for (int i = 0; i < n - 1; ++i) {
int delta = arr[i + 1] - arr[i];
if (delta < best) {
best = delta;
ans.clear();
List<Integer> pair = new ArrayList<Integer>();
pair.add(arr[i]);
pair.add(arr[i + 1]);
ans.add(pair);
} else if (delta == best) {
List<Integer> pair = new ArrayList<Integer>();
pair.add(arr[i]);
pair.add(arr[i + 1]);
ans.add(pair);
}
}
return ans;
}
}
###C#
public class Solution {
public IList<IList<int>> MinimumAbsDifference(int[] arr) {
int n = arr.Length;
Array.Sort(arr);
int best = int.MaxValue;
IList<IList<int>> ans = new List<IList<int>>();
for (int i = 0; i < n - 1; ++i) {
int delta = arr[i + 1] - arr[i];
if (delta < best) {
best = delta;
ans.Clear();
IList<int> pair = new List<int>();
pair.Add(arr[i]);
pair.Add(arr[i + 1]);
ans.Add(pair);
} else if (delta == best) {
IList<int> pair = new List<int>();
pair.Add(arr[i]);
pair.Add(arr[i + 1]);
ans.Add(pair);
}
}
return ans;
}
}
###Python
class Solution:
def minimumAbsDifference(self, arr: List[int]) -> List[List[int]]:
n = len(arr)
arr.sort()
best, ans = float('inf'), list()
for i in range(n - 1):
if (delta := arr[i + 1] - arr[i]) < best:
best = delta
ans = [[arr[i], arr[i + 1]]]
elif delta == best:
ans.append([arr[i], arr[i + 1]])
return ans
###C
static inline int cmp(const void *pa, const void *pb) {
return *(int *)pa - *(int *)pb;
}
int** minimumAbsDifference(int* arr, int arrSize, int* returnSize, int** returnColumnSizes){
qsort(arr, arrSize, sizeof(int), cmp);
int best = INT_MAX;
int **ans = (int **)malloc(sizeof(int *) * (arrSize - 1));
int pos = 0;
for (int i = 0; i < arrSize - 1; ++i) {
int delta = arr[i + 1] - arr[i];
if (delta < best) {
best = delta;
for (int j = 0; j < pos; j++) {
free(ans[j]);
}
pos = 0;
ans[pos] = (int *)malloc(sizeof(int) * 2);
memcpy(ans[pos], arr + i, sizeof(int) * 2);
pos++;
}
else if (delta == best) {
ans[pos] = (int *)malloc(sizeof(int) * 2);
memcpy(ans[pos], arr + i, sizeof(int) * 2);
pos++;
}
}
*returnSize = pos;
*returnColumnSizes = (int *)malloc(sizeof(int) * pos);
for (int i = 0; i < pos; i++) {
(*returnColumnSizes)[i] = 2;
}
return ans;
}
###JavaScript
var minimumAbsDifference = function(arr) {
const n = arr.length;
arr.sort((a, b) => a - b);
let best = Number.MAX_VALUE;
let ans = [];
for (let i = 0; i < n - 1; ++i) {
let delta = arr[i + 1] - arr[i];
if (delta < best) {
best = delta;
ans = [];
const pair = [];
pair.push(arr[i]);
pair.push(arr[i + 1]);
ans.push(pair);
} else if (delta === best) {
const pair = [];
pair.push(arr[i]);
pair.push(arr[i + 1]);
ans.push(pair);
}
}
return ans;
};
###go
func minimumAbsDifference(arr []int) (ans [][]int) {
sort.Ints(arr)
for i, best := 0, math.MaxInt32; i < len(arr)-1; i++ {
if delta := arr[i+1] - arr[i]; delta < best {
best = delta
ans = [][]int{{arr[i], arr[i+1]}}
} else if delta == best {
ans = append(ans, []int{arr[i], arr[i+1]})
}
}
return
}
复杂度分析
-
时间复杂度:$O(n \log n)$,其中 $n$ 是数组 $\textit{arr}$ 的长度。排序需要的时间为 $O(n \log n)$,遍历需要的是时间为 $O(n)$,因此总时间复杂度为 $O(n \log n)$。
-
空间复杂度:$O(\log n)$,即为排序需要使用的栈空间。这里不计入返回值需要使用的空间。