news 2026/9/9 3:33:12

UE4接入Steam好友系统实战:ISteamFriends集成与回调机制详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
UE4接入Steam好友系统实战:ISteamFriends集成与回调机制详解

简介:面向UE4开发者(尤其是需要接入Steam好友系统的研发人员),这份演示资源提供了一套小型C++项目源码,展示如何在UE4中集成Steam Friends API。资源围绕好友列表获取、邀请发送以及接受邀请后的会话加入三个核心环节,包含GetFriendsListCallBackProxy(异步蓝图节点)、NetBlueprintFunctionLibrary(蓝图函数库)与NetGameInstance(自定义GameInstance)等模块,可用于研究Steam子系统调用及多人联机会话衔接的实现思路。包体共7个文件,其中3个头文件与3个C++源文件构成了主要代码逻辑,另有1个Markdown说明文件辅助阅读;整个压缩包仅7KB,体量轻量,便于快速导入分析。目前已有309人学习,适合具备一定C++与UE4蓝图基础、希望理解Steam Friends接口回调与在线会话流程的开发者参考。

1. 项目概述与方案选型

1.1 这个 Demo 到底要交付什么

SteamFriendsUE4 这个项目,说穿了就是给 UE4 工程接一套 Steam 好友体系的最小可运行 Demo:登录后拉取好友列表、展示昵称头像和在线状态、判断好友当前在玩什么游戏、给好友发组队邀请,以及把 Steam 好友面板的 Overlay 正确弹出来。我是在一个联机合作项目起步阶段做的这套东西,当时团队里没人摸过 Steam Friends API 和 UE4 的对接细节,与其后面在正式项目里试错,不如先用一个独立 Demo 把整条链路跑通,后面接大厅、接匹配、接邀请都有底。

这套内容适合两类人看:第一类是 UE4 联机项目的开发,尤其是第一次碰 Steam 服务的;第二类是想搞明白 OnlineSubsystem 和原生 Steamworks SDK 到底什么关系的人。我会以 C++ 实现为主,蓝图侧只给事件转发思路,最后把调试阶段踩过的所有坑按速查表形式整理出来,可以直接照着排查。

1.2 两条技术路径怎么选

UE4 里做 Steam 好友功能,面前有两条路:一条是通过引擎自带的 OnlineSubsystemSteam 插件,走 IOnlineFriends、IOnlinePresence 这些抽象接口;另一条是直接包含 Steamworks SDK 头文件,调用 ISteamFriends 原生接口。说实话我一开始也是用抽象层做的,做到一半发现很别扭。

拿一封邀请这件事举例,抽象层 IOnlineFriends 只给了 SendInvite 这类通用方法,参数是抽象的会话 ID;但 Steam 好友体系里很多核心能力根本不在抽象层里,比如拉取好友的头像、读取好友正在玩的游戏名字、配置富文本状态的本地化 token、把 Overlay 精确弹到某个好友对话框,这些全都得下钻到 ISteamFriends 才能做。与其绕了一圈最终还是落回原生接口,这种演示项目不如一开始就直连 Steamworks SDK,代码反而干净,Steam 官方文档也能逐行对上。

不过正式项目我的建议是混合策略:用 OnlineSubsystem 管理网络传输和会话,只在好友展示、邀请这种需要深度控制的地方直接调 SDD。下面先给一个对比,后面代码都是基于第二种方案。

方案学习曲线功能覆盖维护成本适用场景
OnlineSubsystem 抽象层基础好友/状态/邀请低,引擎升级自动适配功能简单、快速上线
直接调 ISteamFriends中低全部好友能力+头像+Overlay+富文本中,需自己管理回调演示项目、功能深度定制

2. 环境搭建与 Steamworks 配置

2.1 拿 AppID 与 SDK 的正确姿势

要跑通 Steam Friends API,第一步不是写代码,而是把环境身份搞定。你需要去 Steamworks 官网后台注册一个应用,拿到属于自己的 AppID。如果你只是想本地验证功能,可以用官方测试 ID 480(Spacewar),在开发环境里跑完全没问题;但这里有个隐藏坑:用 480 拉取好友列表时,系统返回的是 Spacewar 的游戏关系链,不是你自己应用的好友关系,所以真正联调前切记换成自己的 AppID。

Steamworks SDK 可以在 Steamworks 后台的“SDK 下载”页面拿到,下载后解压,里面关键的目录就两个:public/steam 下面全是 C++ 头文件,redistributable_bin 里是各个平台的动态库。UE4 引擎其实已经在 ThirdParty 里内置了一份 Steamworks,源码在 Engine/Source/ThirdParty/Steamworks/Steamvxxx 目录下,OnlineSubsystemSteam 插件用的就是这一份。我在项目里直接引用了引擎内置版本,没有额外拷贝 SDK,省掉了版本冲突的麻烦。

Windows 平台上要注意的是:打包出来的游戏要带着 steam_api64.dll 或 steam_api.dll,编辑器模式下引擎会自己加载,但独立打包时如果你引用了原生接口,必须在 Build.cs 里把对应模块带上,否则运行时直接崩溃,报“无法定位程序输入点”之类的问题。

2.2 工程级配置:Build.cs 与 DefaultEngine.ini

先改项目根目录下的 .uproject 文件,确保 OnlineSubsystemSteam 插件启用。用记事本打开自己的工程文件,在 Plugins 数组里加上这一项:

"Plugins": [ { "Name": "OnlineSubsystemSteam", "Enabled": true } ]

然后是 Source 目录里的 Build.cs。注意我们要直接包含 Steamworks 头文件,所以要引用 Steamworks 这个第三方模块,同时把 OnlineSubsystem 相关模块也带上,因为引擎的 Steam 网络驱动还依赖它:

PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "OnlineSubsystem", "OnlineSubsystemUtils" }); PrivateDependencyModuleNames.AddRange(new string[] { "Steamworks", "OnlineSubsystemSteam" });

接下来是 Config/DefaultEngine.ini。这里有两段关键配置:第一段把默认联机服务切换到 Steam,第二段配置 Steam 插件的开关和测试 AppID。如果你不准备用 Steam 做网络传输,只想拉好友数据,第一段可以省略,但保险起见建议都配上:

[/Script/Engine.GameEngine] +NetDriverDefinitions=(Name="Steam",DriverClassName="OnlineSubsystemSteam.SteamNetDriver",DriverClassNameFallback="OnlineSubsystemUtils.IpNetDriver") [OnlineSubsystem] DefaultPlatformService=Steam [OnlineSubsystemSteam] bEnabled=true SteamDevAppId=480 bVACEnabled=false

SteamDevAppId 就是刚才说的测试 ID。还有一个小细节:在 Windows 平台上,如果你从编辑器直接 PIE 运行,有一些 Steam 接口(尤其是 Overlay)可能不生效,解决思路是在工程目录下建一个名为 steam_appid.txt 的纯文本文件,内容只写一行 AppID 数字,这样非 Steam 启动的进程也能初始化 API。注意这个文件不要提交到版本库,因为不同环境 AppID 可能不同。

2.3 初始化流程与运行时自检

UE4 的 OnlineSubsystemSteam 插件在引擎启动时会自动调用 SteamAPI_Init,所以一般情况下你不需要手动初始化,但 Demo 里我建议在 GameInstance 的 Init 阶段做一次健壮性检查,确认 Steam 客户端真的在运行、用户真的登录了,否则后面所有调用都会静默失败或者返回空数据。检查代码大概长这样:

bool UMySteamManager::CheckSteamReady() { if (!SteamAPI_Init()) { UE_LOG(LogTemp, Error, TEXT("[SteamFriends] SteamAPI_Init failed")); return false; } if (!SteamUser() || !SteamFriends() || !SteamUtils()) { UE_LOG(LogTemp, Error, TEXT("[SteamFriends] Steam interface unavailable")); return false; } if (!SteamUser()->BLoggedOn()) { UE_LOG(LogTemp, Warning, TEXT("[SteamFriends] User not logged in")); return false; } int32 AppID = SteamUtils()->GetAppID(); UE_LOG(LogTemp, Log, TEXT("[SteamFriends] Steam ready, AppID=%d"), AppID); return true; }

这里有个容易忽略的点:SteamAPI_Init 在插件已经初始化过的情况下再次调用也会返回 true,所以不用怕重复调用,但前提是你没有在初始化前就去拿 ISteamFriends 指针,否则拿到的是空指针。必须严格遵循“先初始化,后取接口”的顺序。

3. 核心功能落地:Friend API 调用实践

3.1 好友列表与在线状态

拿到 ISteamFriends 之后,第一个要做的就是从 Steam 后端拉好友列表。Steam 的好友关系分为几个分类标志,最常用的是 k_EFriendFlagImmediate,它表示直接好友(你主动加且对方同意的),不包含关注者。另一个 k_EFriendFlagAll 会把关注者也拉进来,一般展示给玩家的列表用 Immediate 就够了。

遍历好友的代码如下。这里重点说 GetFriendGamePlayed 这个接口:它返回的是一个 FriendGameInfo_t 结构体,里面包含好友正在玩的游戏 ID、IP 端口和所在大厅。如果返回值是 false,说明好友当前没在游戏中,我们可以借此区分“在线但空闲”和“正在玩某个游戏”两种状态:

int32 FriendCount = SteamFriends()->GetFriendCount(k_EFriendFlagImmediate); for (int32 i = 0; i < FriendCount; i++) { CSteamID FriendID = SteamFriends()->GetFriendByIndex(i, k_EFriendFlagImmediate); FString Name = UTF8_TO_TCHAR(SteamFriends()->GetFriendPersonaName(FriendID)); EPersonaState State = SteamFriends()->GetFriendPersonaState(FriendID); // State 取值: Offline / Online / Busy / Away / Snooze / LookingToTrade / LookingToPlay FriendGameInfo_t GameInfo; bool bPlaying = SteamFriends()->GetFriendGamePlayed(FriendID, &GameInfo); bool bInMyApp = bPlaying && (GameInfo.m_gameID.AppID() == SteamUtils()->GetAppID()); FString PlayingName = bInMyApp ? TEXT("正在玩本游戏") : UTF8_TO_TCHAR(GameInfo.m_rgchGameName); }

拿回来的数据别直接塞给 UI,建议包一层自己的数据结构,比如 FSteamFriendInfo,里面放 SteamID、昵称、状态 enum、游戏名、头像纹理引用,再用 TArray 暴露给蓝图或者绑定到 UMG ListView。

上面代码里我用了 UTF8_TO_TCHAR 宏,这里必须提醒:Steam 的字符串接口返回的都是 UTF-8 编码 const char*,UE4 内部默认使用 TCHAR(Windows 上是 UTF-16),不做转换会出现中文昵称乱码。转换一定要用 UTF8_TO_TCHAR,而不是 ANSI_TO_TCHAR,这是很多人第一次接 Steam 都会踩的字符编码坑。

3.2 头像拉取与纹理转换

好友头像这件事比想象中麻烦。Steam 的流程分为两步:先用 GetSmallFriendAvatar / GetMediumFriendAvatar / GetLargeFriendAvatar 拿到一个整型头像句柄,再用 SteamUtils()->GetImageRGBA 把这个句柄对应的图像数据拉到内存。三种尺寸分别是 32×32、64×64 和 184×184,列表缩略图用 64 就够了。

拿到原始字节后就要建立 UTexture2D。这中间有个大坑:GetImageRGBA 名字里写 RGBA,实际上返回的是 BGRA 顺序,如果喂给纹理后颜色发蓝发红,多半就是这里没处理。我有两个解决办法,一是直接创建 PF_B8G8R8A8 格式的纹理,省一次像素翻转;二是手动交换 R 和 B 通道,再走 PF_R8G8B8A8。推荐前者,性能更好。核心代码:

UTexture2D* UMySteamManager::LoadAvatarAsTexture(CSteamID FriendID) { int32 AvatarHandle = SteamFriends()->GetMediumFriendAvatar(FriendID); if (AvatarHandle == 0) return nullptr; uint32 Width = 0, Height = 0; if (!SteamUtils()->GetImageSize(AvatarHandle, &Width, &Height)) return nullptr; TArray<uint8> RawData; RawData.SetNum(Width * Height * 4); if (!SteamUtils()->GetImageRGBA(AvatarHandle, RawData.GetData(), RawData.Num())) return nullptr; UTexture2D* AvatarTex = UTexture2D::CreateTransient(Width, Height, PF_B8G8R8A8); if (!AvatarTex) return nullptr; FTexture2DMipMap& Mip = AvatarTex->PlatformData->Mips[0]; uint8* DestData = (uint8*)Mip.BulkData.Lock(LOCK_READ_WRITE); FMemory::Memcpy(DestData, RawData.GetData(), RawData.Num()); Mip.BulkData.Unlock(); AvatarTex->UpdateResource(); return AvatarTex; }

写在最后:如果头像句柄返回 0,不要慌,大概率是对方 Steam 没有设置头像,或者当前进程因为 AppID 不对导致头像服务不可用。返回 nullptr 后 UI 层要兜底显示占位图。

3.3 邀请加入与富文本状态

邀请好友进游戏有两种姿势:一种是 ActivateGameOverlayToUser,把 Steam 原生的“邀请”对话框弹到指定好友面前,用户点完由 Steam 自己处理;另一种是 InviteUserToGame,直接给好友发一条带连接字符串的邀请消息,好友那边点了之后会在他的客户端触发回调。个人测试下来,联机项目更适合第二种,因为连接字符串里可以带服务器 IP 或会话 ID,方便自己控制进场逻辑。我用的是后者:

FString ConnectString = FString::Printf(TEXT("connect 192.168.1.100:7777")); SteamFriends()->InviteUserToGame(FriendID, TCHAR_TO_UTF8(*ConnectString));

同时要让好友那边看得懂你在干嘛,就得设置富文本状态。SetRichPresence 可以设置任意自定义键值对,但 Steam 的展示规则有些特殊:steam_display 这个键对应的是 Steamworks 后台配置过的本地化 token 模板,模板里可以引用其他键名的占位符。比如后台配置了一个 token 叫 #Status_CurrentMap,文本内容是“正在地图 {map_name} 中战斗”,代码里就要这么设:

SteamFriends()->SetRichPresence("steam_display", "#Status_CurrentMap"); SteamFriends()->SetRichPresence("map_name", TCHAR_TO_UTF8(*CurrentMapName)); SteamFriends()->SetRichPresence("status", "playing");

这里强调两点:第一,steam_display 的 token 一定要在 Steamworks 后台的“本地化字符串”里配好,否则好友看到的是原始 token 名;第二,富文本状态有推送延迟,实测从设置到好友端可见通常有 2 到 5 秒延迟,这是 Steam 服务器做了聚合缓存,不是你的 bug。

3.4 回调机制与每帧泵送

Steam 的 C++ API 使用回调机制,好友接受邀请、好友状态变化、用户点击了“加入游戏”这些事件都会以回调对象的形式排队。问题在于,这些回调不会自动分发,你得在每帧调用 SteamAPI_RunCallbacks 把它们泵出来。OnlineSubsystemSteam 在引擎 Tick 里会处理自己注册的那部分回调,但直接调用 SDD 注册的自定义回调必须自己泵。我在 PlayerController 或自建的 Manager 的 Tick 里加了一句:

void UMySteamManager::Tick(float DeltaTime) { SteamAPI_RunCallbacks(); }

接下来是重中之重:处理好友点击“加入游戏”的回调 GameRichPresenceJoinRequested_t。这个回调在你的进程里触发,数据里带着你在 InviteUserToGame 时传入的连接字符串。收到之后,用 ClientTravel 发起连接:

class FSteamFriendsCallbackHandler { STEAM_CALLBACK(FSteamFriendsCallbackHandler, OnJoinRequested, GameRichPresenceJoinRequested_t); }; void FSteamFriendsCallbackHandler::OnJoinRequested(GameRichPresenceJoinRequested_t* CallbackData) { if (CallbackData && CallbackData->m_rgchConnect) { FString ConnectStr = UTF8_TO_TCHAR(CallbackData->m_rgchConnect); // 转发给 GameInstance / PlayerController UMySteamManager::Get()->HandleJoinRequest(ConnectStr); } }

顺便也要监听 PersonaStateChange_t,好友头像、昵称、状态发生变化时它会触发,用来实时刷新好友列表,比定时轮询优雅得多。

4. 实操记录:双账号联调全过程

4.1 本地联调环境怎么搭

好友功能必须有两个 Steam 账号才能真正测起来。我的做法是:账号 A 在开发机跑编辑器,账号 B 在另一台机器跑打包版本;两台机器处于同一局域网,这样既能测邀请链路,又能验证后续联机连接。如果你的网络环境不允许两台机器,也可以考虑在一台机器上通过 Steam 客户端切换账号来分步验证,但 Overlay 和加入游戏回调会受进程限制,效果打折扣。

还有一个容易被忽略的点:用编辑器 PIE 模式跑 Demo 时,Steam Overlay 是弹不出来的,因为编辑器进程不是由 Steam 客户端直接拉起的。所以正式验证 Overlay 和加入邀请流程,一定要用打包版本并设置成从 Steam 库中启动,或者用命令行 -applaunch AppID 拉起。

4.2 邀请闭环完整流程记录

我的实际测试链路是这样的:账号 A 在 Demo 里创建监听服务器,设置富文本状态为“正在地图 Dust2 中战斗”;账号 B 打开 Steam 好友列表,看到 A 的状态,点击“加入游戏”;Steam 会拉起 B 的 Demo 进程(如果没在运行的话),进入进程后触发 GameRichPresenceJoinRequested_t;回调里取出连接字符串,调用 ClientTravel,B 进入 A 的服务器;同时 A 端收到 PersonaStateChange_t,知道 B 上线了,立刻刷新好友列表。整条链路从 B 点击到进服,延迟约 3 到 6 秒,主要耗时在 Steam 拉起进程和连接握手。

这里要特别说一下 ClientTravel 的目标地址格式。如果服务器在局域网,连接字符串是“connect 192.168.x.x:7777”;如果是带会话的 Steam 联机,还可以直接用大厅 ID 去 JoinLobby。Demo 为了简单我用了 IP 直连,实际在线项目建议走 Steam 的会话接口,因为 Steam 网络驱动会处理好 NAT 穿透的问题。

4.3 实测中的参数细节

这次测试我整理了三个有参考价值的细节。

第一个是好友列表标志位的选择。k_EFriendFlagAll 会把关注者(Follower)也包括进来,实测一个 Steam 老账号的关注者能有几千人,直接卡 UI。演示用 k_EFriendFlagImmediate,先保证功能闭环,后面要做粉丝联动再单独走。

第二个是头像尺寸与内存关系。185×185 的大头像,一张图原始数据约 135KB,如果是几百人的列表,一次性全拉会导致明显的卡顿。我的做法是列表行先显示 64px 中头像,点开详情页时再异步拉大头像。

第三个是 RemoveRichPresence 的使用。玩家退出关卡或者断线时,如果不显式清除富文本,好友端会维持旧状态很久。在 GameInstance 的 Shutdown 里调用 SteamFriends()->ClearRichPresence(),比重新塞一个空字符串规范,实测清状态几乎立即生效。

5. 常见问题排查与两个连带坑位

5.1 问题速查表

把演示过程中真正踩过、以及帮别人排查过的问题整理成一张表,直接对照查:

现象根因解决思路
SteamAPI_Init 返回 falseAppID 配置错误或 Steam 未启动检查 SteamDevAppId 与 steam_appid.txt;确保 Steam 客户端已登录
好友列表始终为空标志位用错或对方隐私设置改用 k_EFriendFlagImmediate;让对方 Steam 资料里好友列表设为公开
好友中文昵称乱码UTF-8 转 TCHAR 用了错误的宏统一用 UTF8_TO_TCHAR,禁止 ANSI_TO_TCHAR
头像贴图颜色怪异Steam 返回 BGRA 字节序纹理格式用 PF_B8G8R8A8,或手动交换 R/B
Overlay 弹不出来非 Steam 启动的编辑器进程打包版本从 Steam 库启动,并确认后台启用了 Overlay
SetRichPresence 好友看不到steam_display token 未后台配置Steamworks 后台配置本地化字符串,键名要和代码一致
好友加入请求不触发未调用 SteamAPI_RunCallbacks在 Tick 里每帧泵回调
打包后找不到 DLL 崩溃缺少 steam_api64.dll从 SDK 的 redistributable_bin/win64 拷贝到打包目录

5.2 外接设备映射的连带坑

Demo 做到后半段,我想让第二个玩家在本机用手柄或自制的 USB 外设控制角色,方便单人模拟双人一起接受邀请,结果在“外接设备映射”上又耗了半天。UE4 的默认输入系统对手柄支持很成熟,但如果你用的是非标准的 USB 外设(方向盘、街机摇杆、自研按键板),事情就复杂了。

最核心的问题是:UE4 传统 Axis Mapping 无法区分同类型的多个外接设备,插两个手柄时它们会共用一套映射,你没法知道输入来自哪个设备。解决思路是启用 RawInput 插件,直接在 Plugins 列表里勾选,然后在项目设置里配置 VendorId 和 ProductId,按设备实例读取原始输入报告,再按 DeviceHandle 分发到不同角色。

另一个坑和 Steam 本身有关:Steam 客户端默认会对手柄做输入接管,一旦 Steam Input 生效,你项目里拿到的输入可能被 Steam 层重映射过。我在测试时发现手柄按键错乱,最后是在 Steam 设置里把对应类型手柄的“控制器配置支持”选项关掉,让 UE4 直接读取原始 XInput 数据,问题才消失。如果你的 Demo 目标平台是独立的室内设备而不是 PC Steam 平台,这一点尤其要注意。

5.3 导入模型枢纽偏移处理

邀请流程通了之后,我想让好友进入关卡时,用他的头像生成一个立牌展示,就导入了几个外部 3D 模型,结果又撞上“枢纽偏移”问题,也就是模型导入 UE4 后 Pivot 点不在预期位置。现象非常典型:在 Blender / 3ds Max 里模型底面中心是重心,导入 UE4 后整个模型悬空或者嵌入地面,枢纽点跑到模型角落。

原因有几个:一是 DCC 软件里模型带有额外的根节点,或者父级节点有负 Scale、旋转,导入时 UE4 把整个变换链的偏移一起带进来了;二是模型单位不一致,FBX 导出的单位是厘米,但导入设置里勾选了错误的比例,导致 Pivot 偏移被放大。最省事的处理是在 DCC 里把模型层级清理干净,所有子节点原地清零变换,把 Pivot 对齐到世界原点再导出。如果模型已经导入工程,还可以在运行时做个兜底修正,用包围盒中心反推偏移量:

FBoxSphereBounds Bounds = MeshComp->CalcBounds(MeshComp->GetComponentTransform()); FVector PivotOffset = Bounds.Origin - MeshComp->GetComponentLocation(); MeshComp->AddLocalOffset(-PivotOffset);

这段代码的意思是把组件位置从“原点”挪到模型实际包围盒中心,让立牌落在地面上。注意它只适合静态展示用的模型,角色骨骼网格和物理资产的枢纽偏移得回到 DCC 里根治。

如果模型是从网上下载的通用资源,导入设置里建议把“Convert Scene Unit”按源软件的单位正确勾选,并保留“Use T0 As Ref Pose”,可以避免大部分比例和位移的问题。


最后说点关于这套 Demo 的体会。整个项目实际开发不到一周,但大头时间全花在三个地方:回调泵送的机制理解、双账号联调的环境准备、还有那两件跟 Steam 本身无关的外围问题。如果重新来一次,我会在第一天就盯着 SteamAPI_RunCallbacks 这个点不放,因为绝大多数“接口明明调了却没反应”的诡异问题都是它引起的。调试回调时有个小技巧很值得一试:在 SteamAPI_RunCallbacks 上下一个条件断点,观察每一帧到底 dispatch 了什么类型的回调,这比在业务代码里打日志高效得多,因为你立刻能看到 Steam 层有没有把事件送进来,从而快速定位问题在“进门”还是“进门之后”。这套链路跑通之后,后面再接大厅匹配就顺理成章了。

本文还有配套的精品资源,点击获取

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

嵌入式Linux串口与Modbus RTU通信实战:从termios配置到RS485稳定轮询

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

作者头像 李华
网站建设 2026/9/9 3:25:09

基于DeepSeek API和OneBot协议的QQ机器人拟人化聊天实现

之前一直想给 QQ 群接入一个“真正能聊起来”的 AI 机器人&#xff0c;但试过几种方案之后发现一个问题&#xff1a;要么回复太机械&#xff0c;要么每条消息都秒回&#xff0c;看起来特别假。后来用 DeepSeek 官方 API 配合 OneBot 11 协议自己写了一个机器人&#xff0c;加入…

作者头像 李华
网站建设 2026/9/9 3:18:36

前端AI编码工作流:CLI+Codex+VS Code三角闭环实战

1. 这不是“技能库”&#xff0c;而是一套前端开发者私有化AI编码工作流的落地实践最近在几个前端技术群和开源协作频道里&#xff0c;反复看到有人问&#xff1a;“skills 是什么&#xff1f;是不是又一个 CLI 工具&#xff1f;”、“npx skill add dietrichgebert/ponytail 能…

作者头像 李华
网站建设 2026/9/9 3:18:25

hermes-agent:一个稳定可落地的轻量级Agent框架设计与实践

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

作者头像 李华
网站建设 2026/9/9 3:18:08

Kali Linux网络故障排查:从虚拟机网卡到DNS的完整解决思路

装好Kali Linux之后第一件事是什么&#xff1f;不是打开终端敲命令&#xff0c;不是找渗透测试工具&#xff0c;而是先确认能不能上网。这个问题听起来简单&#xff0c;但群里几乎每天都有新手卡在这一步&#xff1a;刚装完系统&#xff0c;浏览器怎么都打不开网页&#xff0c;…

作者头像 李华
网站建设 2026/9/9 3:13:33

ponytail:利用npx skill一键聚合项目上下文,提升AI辅助开发效率

1. 先搞明白&#xff1a;ponytail 到底是什么 先说结论&#xff0c; ponytail 不是发型教程&#xff0c;也不是某个前端组件库&#xff0c;它是一个通过 npx 安装、以“马尾辫”为隐喻的开发者技能包 。我最初是在技术社区刷到 npx skill add dietrichgebert/ponytail 这条…

作者头像 李华