IoT 前端长连接架构深度复盘:WebSocket 连接池管理与多路数据流分层设计
IoT 前端长连接架构深度复盘WebSocket 连接池管理与多路数据流分层设计一、物联网大屏背后的连接灾难断连风暴与数据雪崩物联网监控中控屏通常是一块 4K 大屏或指挥中心的拼接屏需要同时展示数百台设备的实时状态。每台设备以 1Hz 频率上报遥测数据总计 500 台设备每秒产生 500 条数据推送。传统方案为每台设备建立独立的 WebSocket 连接500 个连接同时保活浏览器的网络栈在接近 256 连接上限时开始出现连接排队、心跳超时和随机断连。更严重的是断连风暴问题当网关或代理服务短暂不可用时500 个连接同时触发重连形成流量尖峰。WebSocket 的指数退避重连算法在连接数超过 100 时会出现共振——多个连接在同一时间窗口同时重试导致网关再次过载、连接再次断开形成一个自我放大的死亡循环。本文基于一个实际的工业园区 IoT 平台管理 3000 传感器、100 边缘网关的架构演进复盘 WebSocket 长连接管理的核心设计决策。二、WebSocket 连接管理的三层抽象2.1 连接池从每设备一连接到按 Topic 复用每设备一个 WebSocket 连接的架构在 50 台设备以下时可接受但超过 100 台后会出现三个硬伤浏览器连接上限Chromium 对同一域名的最大并发连接数为 256。超过此限制的新建连接会在队列中等待。内存堆积每个 WebSocket 连接的接收缓冲区默认 16KB64KB。500 个连接意味着 8MB32MB 的缓冲区占用如果数据流速不均匀某些缓冲区的数据堆积会造成内存持续增长。心跳风暴500 个连接各自维护心跳定时器每 30 秒产生 500 次 ping/pong 往返。连接池方案将设备抽象为 Topic多个 Topic 共享一条 WebSocket 连接。关键设计要素Topic 到连接的映射采用一致性哈希将 Topic 映射到连接确保同一 Topic 的消息始终走同一连接避免乱序同时支持连接的动态扩缩。消息多路复用在应用层加入 Topic 标识字段接收端通过 Topic ID 路由到对应的数据流。连接数规划基于设备数量和数据频率计算最小连接数。公式为minConnections ceil(∑(devices × frequency) / (bandwidth_per_conn × 0.7))其中带宽预留 30% 余量。2.2 重连调度器避免共振的随机抖动WebSocket 断连后的重连策略是连接管理中错误率最高的环节。标准指数退避的公式为delay min(baseDelay × 2^(attempt-1), maxDelay)但在多连接场景下会产生严重共振——N 个连接使用相同的 baseDelay 和相同的断连时间点它们将在完全一致的时间窗口内重试。解决方案是引入抖动量Jitter。推荐使用去关联抖动策略delay random(0, min(baseDelay × 2^(attempt-1), maxDelay))。每个连接独立随机将重连尖峰分散到整个退避窗口。生产数据将重连策略从固定指数退避切换到去关联抖动后网关的瞬时 TCP 连接数峰值从 3200 降到 180下降了 94%。2.3 背压控制消费者跟不上生产者的处理策略前端数据消费有三个典型速率瓶颈数据解码MessagePack 反序列化、时序数据缓存写入环形缓冲区索引更新、DOM 渲染ECharts setOption 调用。三个环节中任何一环的吞吐量低于数据流入速率都会导致队列堆积和内存泄漏。背压控制的三种策略丢弃策略Drop当缓冲区达到 80% 容量时丢弃最旧的数据帧。适用于仅展示最新状态的场景如设备状态指示灯。降频策略Throttle当缓冲区达到 50% 容量时将消费间隔从 200ms 延长到 500ms降低渲染频率以匹配消费能力。反压传播Backpressure Propagation当缓冲区达到 90% 时向 WebSocket 发送暂停信号通知网关降低推送频率。这是最彻底的方案但需要应用层协议支持。三种策略按缓冲水位从低到高依次启用形成三级防御梯度。三、连接池与数据流管理的核心实现/** * IoT 前端长连接管理 * 连接池 多路流管理 背压控制 随机抖动重连 */ // ---- 数据模型 ---- interface TopicMessage { topicId: string; // 设备/数据流唯一标识 payload: ArrayBuffer; // 二进制载荷MessagePack / Protobuf timestamp: number; // 服务端时间戳 seq: number; // 消息序号用于乱序检测 } interface ConnectionConfig { url: string; maxRetries: number; // 最大重连次数-1 表示无限 baseDelay: number; // 退避基值ms maxDelay: number; // 退避上限ms heartbeatInterval: number; // 心跳间隔ms heartbeatTimeout: number; // 心跳超时ms超过则判定断连 } interface BackpressureConfig { highWatermark: number; // 高水位线缓冲区占比如 0.8 mediumWatermark: number; // 中水位线如 0.5 lowWatermark: number; // 低水位线如 0.2 } // ---- 环形缓冲区 ---- class RingBufferT { private buffer: (T | null)[]; private readIndex 0; private writeIndex 0; private count 0; constructor(private capacity: number) { this.buffer new Array(capacity).fill(null); } get usage(): number { return this.count / this.capacity; } get isFull(): boolean { return this.count this.capacity; } push(item: T): boolean { if (this.isFull) { // 丢弃最旧的数据滑动窗口策略 this.readIndex (this.readIndex 1) % this.capacity; this.count--; } this.buffer[this.writeIndex] item; this.writeIndex (this.writeIndex 1) % this.capacity; this.count; return true; } /** * 批量读取返回 [readIndex, writeIndex) 区间内的所有元素 * 读取后不清空允许重复消费 */ snapshot(): T[] { const result: T[] []; let idx this.readIndex; for (let i 0; i this.count; i) { result.push(this.buffer[idx]!); idx (idx 1) % this.capacity; } return result; } /** * 消费式读取读取并清空 */ drain(): T[] { const items this.snapshot(); this.readIndex this.writeIndex; this.count 0; return items; } clear(): void { this.readIndex 0; this.writeIndex 0; this.count 0; } } // ---- 随机抖动重连调度器 ---- class ReconnectionScheduler { private attempt 0; private timerId: ReturnTypetypeof setTimeout | null null; constructor( private config: RequiredPickConnectionConfig, baseDelay | maxDelay | maxRetries, private onReconnect: () void, private onGiveUp: () void ) {} schedule(): void { if (this.config.maxRetries 0 this.attempt this.config.maxRetries) { this.onGiveUp(); return; } // 去关联抖动在 [0, cappedDelay] 之间随机取值 const cappedDelay Math.min( this.config.baseDelay * Math.pow(2, this.attempt), this.config.maxDelay ); const jitteredDelay Math.random() * cappedDelay; this.timerId setTimeout(() { this.attempt; this.onReconnect(); }, jitteredDelay); } /** 连接成功后重置退避状态 */ reset(): void { this.attempt 0; if (this.timerId) { clearTimeout(this.timerId); this.timerId null; } } cancel(): void { if (this.timerId) { clearTimeout(this.timerId); this.timerId null; } } } // ---- WebSocket 连接封装 ---- class ManagedConnection { private ws: WebSocket | null null; private heartbeatTimer: ReturnTypetypeof setInterval | null null; private heartbeatTimeoutTimer: ReturnTypetypeof setTimeout | null null; private reconnectScheduler: ReconnectionScheduler; private isIntentionallyClosed false; private messageQueue: TopicMessage[] []; // 断连期间的消息暂存 constructor( private id: number, private config: ConnectionConfig, private onMessage: (msg: TopicMessage) void, private onStateChange: (state: ConnectionState) void ) { this.reconnectScheduler new ReconnectionScheduler( { baseDelay: config.baseDelay, maxDelay: config.maxDelay, maxRetries: config.maxRetries, }, () this.connect(), () this.onGiveUp() ); } connect(): void { if (this.ws (this.ws.readyState WebSocket.OPEN || this.ws.readyState WebSocket.CONNECTING)) { return; // 已经连接或正在连接 } try { this.ws new WebSocket(this.config.url); this.ws.binaryType arraybuffer; // 使用二进制传输降低开销 this.ws.onopen () { this.onStateChange(connected); this.reconnectScheduler.reset(); this.startHeartbeat(); // 发送断连期间缓存的订阅重订阅 this.flushSubscribeQueue(); }; this.ws.onmessage (event: MessageEvent) { // 区分心跳响应和数据帧 if (typeof event.data string event.data pong) { this.onPong(); return; } // 解析数据帧假定为 MessagePack 编码 const msg this.decodeMessage(event.data); if (msg) { this.onMessage(msg); } }; this.ws.onclose (event) { this.onStateChange(disconnected); this.stopHeartbeat(); if (!this.isIntentionallyClosed) { // 非预期断连启动重连 this.reconnectScheduler.schedule(); } }; this.ws.onerror () { // onerror 之后必然跟随 onclose不在此处做额外处理 // 仅在日志中记录 }; } catch (err) { this.onStateChange(disconnected); this.reconnectScheduler.schedule(); } } disconnect(): void { this.isIntentionallyClosed true; this.reconnectScheduler.cancel(); this.stopHeartbeat(); if (this.ws) { this.ws.close(1000, Client disconnect); this.ws null; } } /** * 发送数据带 readyState 检查 */ send(data: ArrayBuffer): boolean { if (this.ws this.ws.readyState WebSocket.OPEN) { this.ws.send(data); return true; } return false; } get state(): ConnectionState { if (!this.ws) return disconnected; const map: Recordnumber, ConnectionState { [WebSocket.CONNECTING]: connecting, [WebSocket.OPEN]: connected, [WebSocket.CLOSING]: disconnected, [WebSocket.CLOSED]: disconnected, }; return map[this.ws.readyState] ?? disconnected; } private startHeartbeat(): void { this.heartbeatTimer setInterval(() { if (this.ws this.ws.readyState WebSocket.OPEN) { this.ws.send(ping); // 设置心跳超时如果 2 倍心跳间隔内未收到 pong判定为断连 this.heartbeatTimeoutTimer setTimeout(() { if (this.ws) { this.ws.close(4001, Heartbeat timeout); } }, this.config.heartbeatTimeout); } }, this.config.heartbeatInterval); } private onPong(): void { if (this.heartbeatTimeoutTimer) { clearTimeout(this.heartbeatTimeoutTimer); this.heartbeatTimeoutTimer null; } } private stopHeartbeat(): void { if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer); this.heartbeatTimer null; } if (this.heartbeatTimeoutTimer) { clearTimeout(this.heartbeatTimeoutTimer); this.heartbeatTimeoutTimer null; } } private decodeMessage(data: any): TopicMessage | null { try { // 假定使用 MessagePack 编码实际项目中需引入 msgpack/msgpack // return decode(data) as TopicMessage; return null; } catch { return null; } } private flushSubscribeQueue(): void { // 断连期间需要重新订阅的 Topic 列表 } private onGiveUp(): void { this.onStateChange(failed); } } // ---- 连接池 ---- type ConnectionState connected | connecting | disconnected | failed; class ConnectionPool { private connections: ManagedConnection[] []; private topicToConnection new Mapstring, number(); // Topic → 连接索引 private router: (msg: TopicMessage) void; private stateListener: (state: ConnectionState) void; constructor( private poolSize: number, private connectionConfig: ConnectionConfig ) {} /** * 初始化连接池创建 N 个 WebSocket 连接 */ initialize( router: (msg: TopicMessage) void, stateListener: (state: ConnectionState) void ): void { this.router router; this.stateListener stateListener; for (let i 0; i this.poolSize; i) { const conn new ManagedConnection( i, this.connectionConfig, (msg) this.router(msg), (state) { // 当所有连接都断开时报告连接池状态为 disconnected const allDisconnected this.connections.every( (c) c.state disconnected || c.state failed ); if (allDisconnected) { this.stateListener(disconnected); } } ); conn.connect(); this.connections.push(conn); } } /** * 将 Topic 映射到连接一致性哈希 */ subscribe(topicId: string): void { const connIndex this.hashTopic(topicId); this.topicToConnection.set(topicId, connIndex); // 向对应连接发送订阅指令 } unsubscribe(topicId: string): void { this.topicToConnection.delete(topicId); } /** * 一致性哈希映射 * 简单实现使用字符串哈希取模 */ private hashTopic(topicId: string): number { let hash 0; for (let i 0; i topicId.length; i) { hash ((hash 5) - hash) topicId.charCodeAt(i); hash | 0; // 32 位整数 } return Math.abs(hash) % this.poolSize; } /** * 动态扩缩连接池 */ resize(newSize: number): void { if (newSize this.poolSize) return; if (newSize this.poolSize) { // 扩容新增连接 for (let i this.poolSize; i newSize; i) { const conn new ManagedConnection( i, this.connectionConfig, (msg) this.router(msg), () {} ); conn.connect(); this.connections.push(conn); } // 重新分配 Topic因为模数变了 this.rebalance(); } else { // 缩容关闭多余的连接 for (let i newSize; i this.poolSize; i) { this.connections[i].disconnect(); } this.connections.length newSize; this.rebalance(); } this.poolSize newSize; } private rebalance(): void { // 重新对所有 Topic 执行哈希映射 const topics Array.from(this.topicToConnection.keys()); this.topicToConnection.clear(); for (const topic of topics) { this.topicToConnection.set(topic, this.hashTopic(topic)); } } destroy(): void { for (const conn of this.connections) { conn.disconnect(); } this.connections []; this.topicToConnection.clear(); } } // ---- 背压控制的流消费调度器 ---- class BackpressureAwareStream { private buffer: RingBufferTopicMessage; private consumeIntervalId: ReturnTypetypeof setInterval | null null; private isPaused false; constructor( private topicId: string, private bufferCapacity: number, private consumer: (messages: TopicMessage[]) void, private config: BackpressureConfig { highWatermark: 0.9, mediumWatermark: 0.5, lowWatermark: 0.2, } ) { this.buffer new RingBuffer(bufferCapacity); } /** * 推送消息到缓冲区 */ push(msg: TopicMessage): void { if (this.isPaused) return; // 暂停期间丢弃消息 this.buffer.push(msg); // 背压检查 if (this.buffer.usage this.config.highWatermark) { this.pause(); } } /** * 启动消费循环 * param interval 消费间隔ms */ startConsuming(interval: number): void { this.consumeIntervalId setInterval(() { const batch this.buffer.drain(); if (batch.length 0) { this.consumer(batch); } }, interval); } private pause(): void { this.isPaused true; // 暂停期间通知网关降低推送频率通过 WebSocket 发送反压信号 } private resume(): void { this.isPaused false; // 通知网关恢复正常推送频率 } stop(): void { if (this.consumeIntervalId) { clearInterval(this.consumeIntervalId); this.consumeIntervalId null; } } getUsage(): number { return this.buffer.usage; } } // ---- 渲染调度 ---- class RenderScheduler { private rafId: number | null null; private dirty false; constructor(private onFrame: () void) {} /** * 标记脏数据在下一帧渲染 * 多次调用在一帧内合并 */ markDirty(): void { if (this.dirty) return; this.dirty true; this.rafId requestAnimationFrame(() { this.dirty false; this.onFrame(); }); } cancel(): void { if (this.rafId ! null) { cancelAnimationFrame(this.rafId); this.rafId null; } } } export { ConnectionPool, ManagedConnection, ReconnectionScheduler, BackpressureAwareStream, RenderScheduler, RingBuffer, }; export type { TopicMessage, ConnectionConfig, ConnectionState };四、连接架构的三类典型故障与应对策略4.1 断连风暴的防护边界随机抖动重连解决了时间域上的共振但在空间域上仍有潜在问题当网关整体不可用时所有连接池最终都会放弃重连maxRetries 耗尽系统进入完全不可用状态。需要引入熔断器机制半开探测连接池进入failed状态后保留一个探测连接以 60 秒间隔尝试重连。恢复确认探测连接成功保持 30 秒后全量恢复连接池。冷却期连续 3 次全量恢复后又立刻断连进入 5 分钟冷却期避免反复震荡。4.2 消息序号的跨连接乱序当连接池发生 Rebalance扩缩容时同一 Topic 的消息可能从连接 A 切换到连接 B。由于不同连接的传输延迟不同消息 B先发送可能比消息 A后发送更晚到达前端。解决方案是服务端在推送消息时附带全局单调递增的序列号。前端按(topicId, seq)做乱序检测如果新到达消息的 seq 小于已处理的最大 seq丢弃该消息并上报乱序事件。当乱序率超过 1% 时报警并考虑关闭 Rebalance 功能使用连接预分配策略替代。4.3 长连接场景下的内存泄漏WebSocket 连接对象的闭包引用是长连接场景中的经典内存泄漏源。事件监听器onmessage、onclose持有外部作用域的引用如果不在disconnect时显式移除GC 无法回收已关闭的连接对象。建议在ManagedConnection.disconnect()中将所有事件处理器置为 nulldisconnect(): void { if (this.ws) { this.ws.onopen null; this.ws.onmessage null; this.ws.onclose null; this.ws.onerror null; this.ws.close(); this.ws null; } }五、总结IoT 前端的 WebSocket 架构核心在于三个维度的管理连接数量的管理连接池代替每设备一连接、重连策略的管理随机抖动代替固定退避、以及数据流速的管理背压控制代替无界缓冲。三者中背压控制是技术含量最高但业界实践最少的一环——多数 IoT 应用仍在使用祈祷消费者快过生产者的朴素策略。在实际工程中建议先以最简单的方案一个连接 无背压上线然后通过浏览器 Performance 面板和内存快照定位瓶颈按需引入连接池和背压控制。过早地设计一个完美的连接管理架构不仅浪费工程资源还会因为过度抽象导致调试困难。长连接管理是一个适合渐进演化的模块而非一次性的架构决策。

相关新闻

SpringBoot集成JVM监控与Prometheus+Grafana实践

SpringBoot集成JVM监控与Prometheus+Grafana实践

1. 项目背景与核心价值在Java应用开发中,SpringBoot已经成为事实上的标准框架。随着微服务架构的普及,单个服务实例的JVM健康状态监控变得尤为重要。JVM监控不仅能帮助开发者及时发现内存泄漏、线程阻塞等潜在问题,还能为性能优化提供数据支撑…

2026/10/12 23:42:09 阅读更多 →
用降AI工具会被认定学术不端吗?这个问题要搞清楚

用降AI工具会被认定学术不端吗?这个问题要搞清楚

论文降AI工具会导致学术不端吗?这个问题要搞清楚 用降AI工具会不会被认定为学术不端?这是很多同学的顾虑,担心用了工具反而出问题。 这个问题有必要认真说清楚。什么是学术不端 学术不端的核心定义是:在研究和学术过程中&#xff…

2026/10/7 20:22:01 阅读更多 →
Spring Batch企业级批处理系统设计与优化实践

Spring Batch企业级批处理系统设计与优化实践

1. 企业级批处理系统需求解析 批处理系统在企业级应用中扮演着关键角色,特别是在需要处理大量数据的场景下。传统的手工处理方式在面对百万级甚至千万级数据时,往往显得力不从心。Spring Batch作为Spring生态系统中的批处理框架,提供了一套完…

2026/10/13 6:18:22 阅读更多 →

最新新闻

用Structure Ref实现武器属性换肤:五属性大剑制作全流程解析

用Structure Ref实现武器属性换肤:五属性大剑制作全流程解析

玩武器设计这行,最怕听到“需求很简单”这五个字。尤其当你面对的需求是:做一把大剑,但要求它能裂变出火、冰、雷、毒、圣五种属性,每种属性都要有独立外观、独立数值,还得在战斗里切换得丝滑。我去年接了这么个项目&a…

2026/10/13 7:15:59 阅读更多 →
Java进阶自学路线图:并发编程、JVM与源码阅读实战指南

Java进阶自学路线图:并发编程、JVM与源码阅读实战指南

每当有朋友问起"Java 进阶自学路线图到底怎么定",我都能从对方的问题里听出同一种迷茫:基础学完了,面向对象、集合、IO 这些都会用,Spring Boot 项目也能跑起来,但一看到招聘要求里的"深入理解JVM"…

2026/10/13 7:15:59 阅读更多 →
Clawdbot安全拆解:开源智能体权限管理与防注入实践

Clawdbot安全拆解:开源智能体权限管理与防注入实践

我第一次看到 Clawdbot 演示的时候,是在一个技术交流群里,有人把它做成的自动化流程录成了视频。说实话,那个视频让我第一反应很不舒服:一个开源智能体,把“查资料、写文件、发消息、改配置”这件事做得过于顺滑了。作…

2026/10/13 7:15:59 阅读更多 →
大模型应用开发动手做 AI Agent:从调 API 到跑通任务的完整路径

大模型应用开发动手做 AI Agent:从调 API 到跑通任务的完整路径

简介:《大模型应用开发 动手做AI Agent》是黄佳撰写的一本面向Agent技术学习者与开发者的实战电子书,适合研究人员、开发人员、产品经理、企业负责人及高校相关专业师生阅读,帮助读者从零理解Agent的设计思路并掌握落地方法。全书围绕GPT-4、…

2026/10/13 7:15:59 阅读更多 →
小程序接口签名绕过与纵深防御:从签名机制到验证落地

小程序接口签名绕过与纵深防御:从签名机制到验证落地

1. 从一次接口排查说起:为什么小程序签名如此重要这几年小程序生态渗透到了各个业务场景,从电商、生活服务到企业办公,几乎每个领域都离不开它。但我实际在做安全测试和接口联调时发现,很多开发团队把签名校验当成一个“加上就安心…

2026/10/13 7:15:59 阅读更多 →
CO-RE实战:一次编译到处运行,解决eBPF内核适配难题

CO-RE实战:一次编译到处运行,解决eBPF内核适配难题

1. 为什么要折腾 CO-RE:从一次内核升级说起如果你在服务器上跑过基于 BPF 的追踪工具,大概率遇到过一个既无奈又经典的报错:libbpf: failed to find valid kernel BTF或者更早以前,你辛辛苦苦在开发机上编译好的 BPF 程序&#xf…

2026/10/13 7:14:58 阅读更多 →

日新闻

物联网系统集成避坑指南:从选型到验收的关键要点

物联网系统集成避坑指南:从选型到验收的关键要点

做了这么多年物联网系统集成的项目,我最大的感受是,大部分坑不是出在技术前沿,而是出在一些看起来很平常的地方。设备选了便宜的、协议没问清楚、现场没摸透、验收标准写得含糊——这些细枝末节,最后都会变成延期、超支和扯皮的大…

2026/10/13 0:00:57 阅读更多 →
ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

如果你玩ESP32有一阵子了,很难不碰到舵机控制这道坎。做智能小车要转方向,做机械臂要抬关节,做云台要偏航俯仰,背后都是同一个东西在干活:舵机。而我见过太多人卡在“代码能编译、舵机就是不听话”这个阶段&#xff0c…

2026/10/13 0:00:57 阅读更多 →
CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

1. 为什么CAN总线至今仍是工业现场的“硬通货”?——从汽车底盘到智能温室的真实需求你有没有拆过一辆老款燃油车的中控台?或者调试过某高校实验室里那台跑着Modbus协议却频频丢帧的PLC控制柜?又或者,在某个农业物联网项目里&…

2026/10/13 0:00:57 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/13 2:54:56 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/12 0:16:38 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

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

2026/10/12 21:05:09 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

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

2026/10/12 14:48:49 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

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

2026/10/12 18:11:17 阅读更多 →