news 2026/10/22 2:05:59

threejs-landscape 实战:用 Three.js 构建极坐标网格地形、实例化草地与昼夜过渡的程序化风景

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
threejs-landscape 实战:用 Three.js 构建极坐标网格地形、实例化草地与昼夜过渡的程序化风景

【免费下载链接】Skills

Agent skills for designers and builders using Codex, Claude, Cursor, and other AI coding agents

项目地址:https://gitcode.com/gh_mirrors/skills48/Skills
点击查看免费下载

导读

本文围绕开源仓库 Skills 中的threejs-landscapeskill(SKILL.md)展开,完整讲解如何用 Three.js 构建一块"能站住主体"的程序化风景背景:极坐标网格地形让分辨率跟随镜头、域扭曲噪声生成侵蚀感山体、顶点着色器塑形的实例化草地在零 CPU 成本下随风摆动,以及用"完整状态插值"实现的晨/午/黄昏/夜四时段无缝过渡。读完本文,你可以将这套方案直接用于 hero 背景、产品展示台、滚动叙事世界等任何需要"让建筑、物体或角色坐在一个真实场景里,而不是漂浮在一张渐变图上"的页面,并配套掌握可运行示例(demo/index.html)中的全部实现细节与验收清单。

场景示例:参考实现 demo/index.html 的运行截图,展示近景 15 万根实例化草叶、极坐标网格地形与六站天空共同构成的程序化风景(图片来自 demo/preview.jpg)。

背景的使命:让主体"身在某处",然后退场

skill 的开篇立意非常明确:背景只有一份工作——让主体看起来真的待在某处(somewhere),一眼读懂场景后马上让出注意力(见 SKILL.md 引言)。因此整个方案的设计取舍都围绕"前景主体"展开,而不是让背景自身成为焦点。

在仓库的目录组织上,threejs-landscape与两个兄弟 skill 构成组合拳:

  • threejs-weather:在风景之上叠加雨、风暴或雪;
  • threejs-towers:当站在风景中的主体是建筑时使用。

对应的 Agent 配置(agents/openai.yaml)给出的默认提示词即"Use $threejs-landscape to put this subject in a real place: noise terrain, instanced grass, a gradient sky and a time-of-day system",直接点明了该 skill 交付的四件套:噪声地形、实例化草、渐变天空、昼夜系统。

配套的演示提示词文档(demo/PROMPT.md)进一步定义了体验契约:一块延伸到地平线柔和地形的草地平原;拖拽环绕、滚轮/双指缩放;四个时段共用一个控件、以交叉淡化而非硬切切换、夜晚出现星星;还有一个 GRID 开关可以打开网格线框,让极坐标地形结构一目了然。

用极坐标网格"环摄"相机:分辨率跟随镜头

为什么不能用正方形高度场

文档给出的第一个硬性结论是:不要构建正方形高度场(SKILL.md 第 2 节)。长焦镜头只能看到狭窄的楔形视野,正方形网格的大部分三角形都浪费在相机背后,而真正被拍到的地平线方向却又很快耗尽分辨率。

正确的做法是以相机脚下为中心采样极坐标网格,径向环随距离越来越疏:

const AN = 900, RN = 52, R0 = 2.0, R1 = 700; // angular, radial, near, far for (let r = 0; r <= RN; r++) { const t = r / RN; const rad = R0 + (R1 - R0) * Math.pow(t, 2.4); // dense near, sparse far for (let a = 0; a < AN; a++) { const th = a / AN * Math.PI * 2; push(Math.cos(th) * rad, landH(x, z), Math.sin(th) * rad); } }

pow(t, 2.4)是整个技巧的核心:近处环密、远处环疏,使得每一环大致覆盖相同数量的屏幕像素,几何体预算被花在真正可见的地方。文档给出的经验对比是"主体附近两千个三角形胜过均匀铺开的两万个"——从源码结构看,参考 demo(demo/index.html)的具体参数略有收紧:AN=620, RN=48, R0=1.6, R1=760,也就是说约 620×48 个采样点,并按同样的 2.4 次幂曲线分布。

小心绕线方向(winding)

极坐标网格上最容易犯的错误是把所有四边形都绕反,最终从地面看到天空。文档给出的索引顺序是(a0, b1, b0, a0, a1, b1),并在 demo 中落地为(见 demo/index.html 索引构建段):

const a1 = (a + 1) % AN; const a0 = r * AN + a, b0 = r * AN + a1, a2 = (r + 1) * AN + a, b2 = (r + 1) * AN + a1; idx.push(a0, b2, b0, a0, a2, b2);

注意 demo 中环形(角度方向)索引做了% AN环绕闭合,这是极坐标网格独有的细节。验收标准是:在搭建任何其他内容之前,先故意把相机绕到地平线以下看一眼,如果透过地面能看到天空,说明绕线反了。

在叠加八度之前先扭曲采样域

域扭曲(domain warp)是"侵蚀感"的最大来源

文档给出的判断很直白:朴素的 fBm 读起来像"揉皱的纸",而在求值地形之前先用另一个噪声场扭曲采样位置,是通往"看起来被侵蚀过"的地形的最大一步:

function landH(x, z) { const wx = x + fbm(x * 0.012, z * 0.012, 3) * 26; // domain warp const wz = z + fbm(x * 0.012 + 41, z * 0.012 - 17, 3) * 26; let h = fbm(wx * 0.0075, wz * 0.0075, 5) * 34; // broad landforms h += ridged(wx * 0.021, wz * 0.021, 3) * 9; // ridge lines return h; }

要点有三:

  1. 先把采样位置扭曲(两个不同种子偏移的 fBm 各贡献 26 个单位的位移),再在扭曲后的位置叠加大尺度地形的 fBm 与山脊的 ridged 噪声;
  2. 保持解析函数独立于网格。草、石头、雾以及任何散落物都必须采样同一个landH,否则它们会悬浮或下沉。demo 中这一原则贯彻得很彻底:地形网格顶点、石头放置、法线计算(landN用有限差分landH(x+e,z)-landH(x-e,z)求法线)全部走同一个函数(见 demo/index.html 的landH/landN);
  3. 山脊层必须随距离缩放:在 40 个单位处读起来刚好的山脊振幅,在 600 个单位处就不可见了,远处的环需要把振幅乘上去,否则地平线会"变平"。

demo 的landH还额外演示了一个实用技巧:将近场压平。相机在 80 个单位高度环绕,绝对不能被包进山体里,所以代码把d<110的整片近场用地形高度乘以t*t*(3-2*t)(t=(d-110)/170的平滑过渡)压成平地,让地形成形都发生在远处(见 demo/index.html 的landH实现)。这解释了 demo 中"相机脚下永远是平地、草场永远平坦"的观感——草 patch 平移也因此只需要做平移而无需重新采样地形。

按"地形在做什么"着色,而不是贴图

斜率与湿度派生的顶点色

平铺地面贴图总会暴露自己。文档给出的方案是用地形自身的属性计算顶点色(SKILL.md 第 4 节):

const slope = 1 - normal.y; // steep = rock const moist = smoothstep(-4, 6, -height); // low = wet, green const c = rock.clone() .lerp(grass, (1 - slope * 3.2) * (0.35 + moist * 0.65)) .lerp(sand, Math.max(0, 0.5 - moist) * 0.6);

这带来三个"免费"收益:悬崖变石色、洼地变绿色、山脊变浅色——不需要 UV,也没有接缝。为避免颜色出现条带,还要在顶点色上叠加一层极低频噪声。demo 的实现与之对应(demo/index.html 地形着色段):

const slope = 1 - n.y; const moist = Math.max(0, Math.min(1, (3.0 - y) / 12)); const tint = NZ.fbm(x * 0.02, z * 0.02, 2) * 0.5 + 0.5; c.copy(ROCK) .lerp(GRASS, Math.max(0, Math.min(1, 1 - slope * 3.4)) * (0.34 + moist * 0.66)) .lerp(SAND, Math.max(0, 0.42 - moist) * 0.55) .lerp(DARK, tint * 0.22);

两个细节值得注意:

  • demo 中slope由有限差分法线(landN)的n.y计算,而不是网格自身的顶点法线——这样石头、草丛与地形对"坡度"的判断完全一致;
  • demo 额外把远环向雾色渐变:c.lerp(new THREE.Color(0xd8c9a4), Math.pow(t, 2.6) * 0.55),让地平线没有硬边,与雾色自然衔接。

配色上 demo 使用MeshLambertMaterial({vertexColors:true}),色板为 ROCK/GRASS/SAND/DARK 四色;线框模式的 wire 网格(MeshBasicMaterial({wireframe:true, opacity:0.16}))则用于 GRID 开关,直观展示极坐标网格结构(demo/index.html)。

草:一条 ribbon,全部在顶点着色器里塑形

十万根草 = 一个 InstancedMesh

文档的核心主张是:10 万根草只是单条五段 ribbon 的InstancedMesh,每一次弯曲、倾斜、收窄和阵风都发生在顶点着色器里,因此风的成本在 CPU 上为零(SKILL.md 第 5 节):

const geo = new THREE.BufferGeometry(); // 11 verts: a strip + a tip const mesh = new THREE.InstancedMesh(geo, mat, 104000); mat.onBeforeCompile = sh => { Object.assign(sh.uniforms, grassUni); sh.vertexShader = ` uniform float uTime, uWindAmp; uniform vec2 uWind; attribute vec4 aParams; // height, phase, tint, lean varying float vT; varying float vTint; ` + sh.vertexShader.replace('#include <begin_vertex>', ` float gT = position.y; // 0 at root, 1 at tip float gBend = uRestBend + sin(uTime * 1.7 + aParams.y) * uWindAmp; vec2 gRib = ... ; // sweep the blade along an arc vec3 transformed = vec3(gRib.x, gT * aParams.x, gRib.y); `); }; mat.customProgramCacheKey = () => 'grass'; // or every instance recompiles

两个工程要点:

  1. customProgramCacheKey必须有。onBeforeCompile会改变着色器源码,若不提供自定义缓存键,每个实例都会触发一次重编译;
  2. 草场 patch 要跟着相机走。保持一片固定网格的草围绕观察者,把"网格"按格子尺寸做整数对齐平移,而不是向外扩张草场。玩家永远到不了边缘,你也永远不为身后的草付费。

demo 把这一整套落实得很完整(demo/index.html 草场段):

  • 几何体:5 段BSEG=5条带(每段 2 顶点)+ 1 个尖端顶点,共 11 个顶点;
  • 实例数:GRASS_N=150000(skill 示例为 104000,参考 demo 为 15 万);
  • 每实例aParams四分量:高度0.30 + Math.random()*0.36、相位0~2π、色调随机、静息倾角0.10~0.36;
  • patch 半径GRASS_R=64,按面积均匀采样(rad=Math.sqrt(Math.random())*GRASS_R);
  • 草场沿镜头前方GRASS_AHEAD=74个单位、按CELL=2的格子取整平移(placeCamera中的Math.round(.../CELL)*CELL),同时把uOrigin传给着色器;
  • 顶点着色器中风相位基于世界坐标而非局部坐标(gWorld=gRoot.xz+uOrigin后取 sin),否则阵风纹理会随着 patch 一起"漂移";
  • patch 边缘淡出:gEdge = 1.0 - smoothstep(0.74, 1.0, length(gRoot.xz)/uRadius),避免 patch 边缘在草地上留下一道"标尺线";
  • 整体im.position.y = -0.5下移,与近场被压平的地形吻合。

一个能坑掉你一小时的颜色陷阱

文档专门用一段强调了:不要在片元着色器里硬编码草色,然后指望材质颜色能改它。diffuseColor.rgb *= mix(base, tip, t)是"乘"运算——白色材质乘上绿色常量仍然是绿色,每一个 debug 输出都会告诉你材质是白的,但画面就是不变。凡是需要给草重新上色的场景(积雪、季节、夜间调色板),那个 mix 必须使用自己的 uniform。

demo 用uSnow演示了正确姿势(demo/index.html 草片元着色器段):

vec3 gBase = vec3(0.150, 0.180, 0.092) * (0.74 + vTint * 0.54); vec3 gTip = vec3(0.560, 0.630, 0.330) * (0.76 + vTint * 0.56); gBase = mix(gBase, vec3(0.60, 0.65, 0.72), uSnow * 0.86); gTip = mix(gTip , vec3(0.90, 0.94, 1.00), uSnow); diffuseColor.rgb *= mix(gBase, gTip, pow(vT, 0.78)) * 1.30;

草色深浅在着色器内完成,材质(MeshLambertMaterial({color:0xffffff}))保持白色,任何外部 recolor 都走uSnow之类的 uniform。

石头:来自同一个高度函数

石头与草不同,它们要给眼睛提供"度量尺"。文档给出的做法是:按坡度做拒绝采样(rejection sampling)散点,然后setMatrixAt写入InstancedMesh;把 icosphere 压扁并焊接,让石头看起来是"嵌入"地面而非"搁"在地上(SKILL.md 第 6 节):

geo.scale(1, 0.62, 1); geo.translate(0, 0.3, 0); // sunk, not resting

三四种尺寸的几千块石头就足够了——它们最要紧的是出现在主体附近,给眼睛一个度量尺度的参照。demo 的实现细节(demo/index.html 石头段):

  • IcosahedronGeometry(0.5, 1)压扁(scale(1, 0.62, 1))并上移(translate(0, 0.3, 0))实现"下沉";
  • 1600 块,径向2.0 + Math.pow(Math.random(), 0.55) * 95分布;
  • 坡度拒绝:if (landN(x, z).y < 0.80) continue;——只有法线足够竖直(坡度够缓)的位置才放石头,石头不会出现在悬崖上;
  • 放置高度取自同一landH(x,z),再减去sc*0.12让石头微陷入土;
  • 每块石头在 x/y/z 三个轴向独立随机缩放(约 0.10~0.52),打破重复感。

天空:8×512 画布上的六个渐变压点

把垂直渐变画进一张小 canvas,再映射到背面球体(MeshBasicMaterial({side: THREE.BackSide})),重绘成本低到可以"在过渡的每一帧都重画"(SKILL.md 第 7 节):

const grd = ctx.createLinearGradient(0, 0, 0, 512); [0, 0.30, 0.52, 0.68, 0.84, 1].forEach((s, i) => grd.addColorStop(s, cols[i])); ctx.fillStyle = grd; ctx.fillRect(0, 0, 8, 512); skyMat.map.needsUpdate = true;

六个压点是定数:三个压点会变成"CSS 渐变",十个压点又无法调校。而且地平线压点要略微放在几何地平线之下,让雾色与天空在没有可见线条的情况下相接。

demo 的实现(demo/index.html 天空段):SKY_STOPS=[0, 0.30, 0.52, 0.68, 0.84, 1],天空球SphereGeometry(900, 32, 24),renderOrder=-20保证最先绘制、永不遮挡场景,fog:false, depthWrite:false让天空不受雾影响也不参与深度写入;paintSky在每帧时间过渡中通过skyMat.map.needsUpdate=true重画贴图,实现天空颜色的连续插值。

星星:只放在相机够得到的纬度带里

最常见的错误是把星星撒满整个球面。文档的估算很直观:约 10° 镜头的画面只看到球面的约 0.5%,把 1200 颗星撒满球面,屏幕上大概只有 6 颗,看起来像 bug(SKILL.md 第 8 节)。正确做法是:

  • 把星星限制在相机仰角夹子实际允许到达的纬度带内;
  • 用三个尺寸等级、不同点尺寸,让亮度有变化;
  • 用devicePixelRatio缩放PointsMaterial.size,否则视网膜屏上星星会消失;
  • 开加色混合(AdditiveBlending)、sizeAttenuation:false、fog:false、depthWrite:false。

文档给出的量级是三个等级约 3.9 万个点,每层一个 draw call。demo 的实现(demo/index.html 星星段)略作收敛:[22000, 1.7*DPR], [6000, 2.7*DPR], [1400, 4.6*DPR]共 29400 点,纬度带u限制在[-0.06, 0.85](与相机仰角夹子EL_MIN=-0.004, EL_MAX=0.20对应),并且:

  • 星色做了三种温度采样:w>0.88暖色([1, 0.87, 0.70])、w<0.13冷蓝([0.74, 0.84, 1])、其余近白([0.97, 0.97, 0.93]),再乘0.46 + Math.pow(rnd(), 1.7) * 0.54的随机亮度;
  • 点纹理来自 64×64 canvas 的径向渐变软点;
  • 星星初始opacity:0,由昼夜系统的stars值统一驱动显隐(m.opacity = st; m.visible = st > 0.005),夜晚才出现。

时间系统:插值,而不是切换

每个时段是一整套"完整状态"

文档的关键设计是:把每个时段存成完整状态——太阳位置与颜色、半球光、环境光、补光、轮廓光、雾色与远近、地面色、草色、六个天空压点、阴影强度、星星不透明度——然后在两个状态之间 lerp(SKILL.md 第 9 节):

function applyState(A, B, t) { key.position.setFromSpherical(lerpAngle(A.sun, B.sun, t)); key.color.copy(A.sunC).clone().lerp(B.sunC, t); scene.fog.color.copy(A.fog).lerp(B.fog, t); for (let i = 0; i < 6; i++) CUR.sky[i].copy(A.sky[i]).lerp(B.sky[i], t); paintSky(CUR.sky); }

demo 完整实现了这一架构(demo/index.html 时间系统段):THEMES数组定义 MORNING / NOON / SUNSET / NIGHT 四套状态,每套包含css(页面主题色,用于同步 UI 的浅/深色)、sky六色、fog及远近、ground/grass、sun(方位角、高度角、颜色、强度)、hemiC/ambC/fillC三组灯光、stars不透明度。applyState逐项插值:

  • 太阳按方位角/高度角换算成DirectionalLight位置,并让阴影相机随太阳高度角动态收放(ext = 22 + 34 * Math.cos(el));
  • 半球光、环境光、补光、雾、地面色、草色、六个天空压点、星星不透明度全部同步插值;
  • 过渡时长 1.25 秒,使用 easeInOut 曲线(themeMix<0.5 ? 2*t*t : 1 - Math.pow(-2*t+2,2)/2),逐帧调用applyState并重绘天空 canvas。

中途切换:冻结当前插值态,而不是回跳

文档点名的第二个坑是:用户在过渡中途切换时,要把"当前已插值出的状态"冻结为新的起点 A,而不是回到上一个预设,否则每次不耐烦的点击都会跳变(SKILL.md 第 9 节)。demo 的applyTheme正是这样实现的:切换时把themeFrom里每个字段按当前进度t重新插值一遍(A.sunC = A.sunC.clone().lerp(B.sunC, t)等),再以themeMix = 0重新开始——连续点按 TIME 按钮只会连续平滑渐变,绝不跳变。

天气是"乘数",不是"更多预设"

最后一个架构原则:天气作为叠加在上面的乘数,永远不要做成新的预设。四个时段 × 四种天气应该是"四个状态 + 四个修饰符",而不是十六个状态(SKILL.md 第 9 节)。demo 中uSnow就是这种"乘数式"天气的雏形——它不改任何时段定义,只作为 uniform 叠加在草色 mix 上;更完整的雨/雪/风暴方案可进一步参考 threejs-weather。

收尾前的验收清单

文档在第 10 节给出了五条"做完之前必须检查"的项目,这也是参考 demo 的可验证验收标准(SKILL.md 第 10 节):

  1. 把相机绕到地平线以下一次。如果透过地面能看到天空,绕线反了;
  2. 看最远的一环。如果远处地形是平的,说明你的山脊振幅没有随距离缩放;
  3. 连续两次在过渡中途切换时段。画面任何时候都不该跳变;
  4. 在视网膜屏上检查。星星和细几何体是 DPR bug 最先暴露的地方;
  5. 盯 draw call 数量,而不是三角形数量。文档给出的对比主张是"18 个 draw call 内的 130 万三角形可以跑 120fps,而 900 个 draw call 的 20 万三角形跑不到"——批量提交比顶点总量更关键。

demo 为此还内置了无头验证钩子(demo/index.html 末尾):window.__theme可瞬间切换时段并渲染,window.__stats返回{fps, tri, calls, grass, stones, theme}的运行时统计数据,右上角常驻显示实时的 fps / 三角形数 / draw call 数——你可以用这些数字直接核对上述第 5 条,而不必肉眼估算。

把这个 skill 用起来:Agent 提示词三板斧

如果你通过 Codex、Claude、Cursor 等 AI 编码 Agent 使用该 skill,demo/PROMPT.md 提供了三档提示词模板:

  • 最小提示:Use $threejs-landscape to put this scene in a real place: a noise heightfield, instanced grass, a gradient sky, and morning/noon/sunset/night that cross-fade.——一句话锁定全部核心交付;
  • 复刻演示:要求以 demo/index.html 为视觉、动效与性能参考,实现"A Place To Stand Something In"——一份独立 HTML,包含可拖拽环绕/缩放、四时段交叉淡化、GRID 线框开关,并满足实现契约中的全部条目(极坐标网格、域扭曲、无贴图顶点色、顶点着色器塑形草、纬度带星星、插值时段);
  • 混搭改造:Use $threejs-landscape but make it a coastal version: wet sand, tide line, and marram grass in clumps rather than an even field.,或加上跟随地形的慢速镜头推拉。

在具体集成时,请保持"解析地形函数与网格分离"这条总纲:所有散落物(草、石头、星星、雾)采样同一个landH,所有时段只做状态插值,所有天气只做乘数——这样你就得到了一个分辨率跟随镜头、成本可控、可被任意主体"站在上面"的程序化风景。

小结

从 SKILL.md 的方法论到 demo/index.html 的落地实现,这套方案的技术主线可以归纳为四条:

  1. 几何预算跟着镜头走——极坐标网格 +pow(t, 2.4)让每一环覆盖等量像素;
  2. 一切程序化、拒绝贴图——域扭曲噪声出地形、斜率/湿度出顶点色、六压点 canvas 出天空;
  3. CPU 零成本的风——单条 ribbon 的InstancedMesh把全部草形变放进顶点着色器,patch 平移而非扩张;
  4. 时间是插值不是开关——完整状态 + 中途冻结,昼夜无缝,天气只做乘数。

配合 demo/PROMPT.md 的提示词与 agents/openai.yaml 的默认配置,即可在任意 AI 编码工作流中复现这一套"安静退场的风景"。

【免费下载链接】Skills

Agent skills for designers and builders using Codex, Claude, Cursor, and other AI coding agents

项目地址:https://gitcode.com/gh_mirrors/skills48/Skills
点击查看免费下载

相关推荐

上一篇:Boot2Docker测试策略:单元测试与集成测试容器化
下一篇:WinUtil终极指南:一键批量安装软件与Windows系统优化神器

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 19:40:11

Carp 标准库文档完全指南:在线查阅、本地副本与一键重新生成

编程语言编译器 【免费下载链接】Carp A statically typed lisp, without a GC, for real-time applications. 项目地址&#xff1a; https://gitcode.com/gh_mirrors/ca/Carp 点击查看 免费下载 Carp 是一个无 GC、静态类型、面向实时应用场景的 Lisp 方言&#xff08;参见仓…

作者头像 李华
网站建设 2026/10/14 2:40:35

PMIC+MCU协同电源管理:PCA9422与MKV46F256VLH16实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/16 14:16:53

UWB定位算法仿真包:6种算法+实测数据+Matlab工程

简介&#xff1a;本资源是一套面向通信与定位算法学习者的UWB高精度定位仿真代码包&#xff0c;聚焦卡尔曼滤波在超宽带室内定位中的建模、实现与优化&#xff0c;适用于高校研究生、嵌入式定位开发工程师及物联网定位方向实践者。包内共12个文件&#xff0c;含9个MATLAB核心算…

作者头像 李华
网站建设 2026/10/16 9:39:49

openJiuwen agent-core 工作流表达式条件判断 ExpressionCondition 完全指南

人工智能AI AgentAgent 框架大模型工具调用RAG提示工程强化学习 【免费下载链接】agent-core openJiuwen agent-core可提供AI Agent开发、运行、调优与演进相关的全套SDK能力 项目地址&#xff1a; https://gitcode.com/openJiuwen/agent-core 点击查看 免费下载 本篇技术指南…

作者头像 李华