ColorOS 15 ,微信消息经常延迟 5 到 15 分钟,偶尔延迟好几个小时的情况也见过。还有别人打过来的微信视频也经常接不到。

别的程序也容易出现后台被杀,或权限被杀。比如 Tasker 的访问通知权限和无障碍权限经常隔一两天就没了,虽然在设置中看到这些权限的状态依然是开启的,但实际权限已经没了,需要重新开启才行。

能设置的都设置了:
1 、已完全允许后台
2 、已允许微信自启
3 、关闭了睡眠待机优化
4 、已在任务视图锁定微信

重塑传统自动化漏洞挖掘的Multi-Agent框架攻防一体化实践

前段时间在某大厂做安全研究时,针对SDLC的重复性审计工作结合大模型Agent思索了一些可行的思路,便在不断摸索中构建了一个Multi-Agent的协同漏洞挖掘框架系统,目前个人使用来看对于开源的web应用的实战效果相比传统的SAST、DAST以及纯LLM的漏洞挖掘工具来说还是很不错的,便记录此篇框架实现过程和当今Agent赋能漏挖的可行性与优势供师傅们交流指点....

0x00 传统漏洞挖掘的困局

当前针对Web应用后端的自动化漏洞挖掘技术主要受困于“覆盖率”与“准确性”难以两全的矛盾:

  • 传统的静态分析技术虽能提供全量的代码覆盖,但由于缺乏对程序运行时状态和复杂业务逻辑的语义理解,往往导致海量的误报噪声,极大地增加了安全工程师的审计成本
  • 而动态应用程序安全测试虽能在黑盒方面挖掘漏洞更具真实性,却受限于黑盒视角的路径探索能力,难以触及深层业务逻辑,会存在很多漏报
  • 目前大语言模型的出现为代码语义分析带来了新的契机,但受限于Context Window 的约束以及生成式模型固有的幻觉问题,直接依赖原生LLM进行大规模代码审计往往导致分析结果碎片化且缺乏可信度,并且直接将代码喂给大模型容易受与漏洞无关代码的影响

0x01 探索漏洞挖掘框架的新出路?

在探索新的框架实现时,我们可以思考是否能将黑白盒的现有技术互补结合来引导漏洞挖掘?以及我们可以看到几年LLM与Agent相关技术如MCP、RAG的工程化落地,能否用LLM赋于框架更好的语义理解和丰富的上下文能力,再通过Agent做一套自动化流程?

为突破上述技术瓶颈,我在探索新的漏洞挖掘框架时也看了一些目前学术界的相关LLM赋能的研究与github开源的技术实现,总体的探索方法还是在论文与现实实践中思考各个方面的优势与缺陷,最终确定做一个基于Muti-Agent协同的智能化漏洞挖掘框架:构建一个从静态分析到动态验证的闭环生态。技术上引入MCP 来作为连接LLM推理能力与静态分析工具的桥梁,利用RAG 技术通过构建高质量漏洞专家知识库来校准模型判定,深度缓解LLM的“幻觉”与知识盲区;同时,结合运行时自动化的流量Fuzz模糊测试技术,将白盒的逻辑推演与黑盒的攻击验证深度融合,减少漏洞的误报和漏报。

这里放一个当时挖到的有CNVD证书的水洞,通过项目上传与聊天,自动化分析审计出多处SQL注入漏洞,并且能够给出攻击POC,以及后续完整的修复方案

image.png

0x02 框架核心:打破黑白盒壁垒

该框架核心架构旨在重构传统安全检测的边界,提出了一种 “白盒语义指引黑盒,黑盒动态验证白盒”的深度融合范式。框架并非单一工具的线性叠加,而是一个基于Multi-Agent编排(Agent Orchestration)的异构系统。

  • 白盒分析维度:框架引入了MCP作为智能体的执行接口,驱动底层的静态分析工具与正则匹配引擎,对代码AST进行初步扫描,快速锚定潜在的危险函数调用Sink。为解决静态分析中常见的上下文缺失问题,进一步融合了RAG 技术:通过引入高质量的博客记录的高精度漏洞知识库,系统能够为大语言模型提供特定漏洞类型的完备的Context上下文与判定依据,从而在保持高代码覆盖率的同时,抑制传统模式匹配带来的误报,实现了从“语法”到“语义”的代码的全面理解提升。
  • 黑盒验证维度:框架构建了运行时的自动化Fuzz模糊测试。该模块独立承担着对Web通用漏洞(如XSS、SQL注入)及敏感信息泄露的覆盖任务。当白盒Agent发现疑似逻辑漏洞时,通过黑盒上的Fuzz可在流量侧生成针对性的变异Payload进行动态优化,通过分析HTTP响应状态来实证漏洞的可利用性。

我认为将静态视角的逻辑推演与动态视角的攻击验证相结合的机制,能极大地提升了漏洞检测的置信度,实现了真正意义上的全链路攻防评估,刚开始时候画的大致架构草图,仅贴示了主要功能,一些细节实现并未展示:

image.png

0x03 智能化Agent设计细节

1. Static Orchestration Agent:基于MCP协议的异构工具编排

在传统的LLM应用中,模型往往被禁锢在文本交互的孤岛中,难以触及本地庞大的代码仓库,且面临着Context Window对海量代码理解的限制。本框架设计的漏洞定位Agent,本质上是一个 静态分析增强型智能体(Static Orchestration Agen) ,通过引入MCP与构建Prompt定义角色任务将LLM从被动的文本生成者转变为主动的工具使用者,通过静态分析获取代码结构中的丰富语义上下文

MCP驱动的“深层感知”

不同于简单的API调用,MCP协议使得Agent能够理解工具的输入输出Schema,实现复杂的推理链条:

  • 工具与模型的语义对齐:通过定义标准化的MCP接口,将底层的静态代码分析工具封装为LLM可调用的能力。
  • 意图驱动的执行:构造合适的CoT思维链Prompt让Agent根据当前的分析任务代码(例如“寻找未授权访问漏洞”),自主决策调用何种工具、传入何种参数。这可以让Agent模拟安全专家的思维过程,主动去探测代码中的漏洞点。

SINK点定位与攻击面收敛

针对LLM处理大规模代码时的“大海捞针”难题,高效定位漏洞利用链

  • SINK点精准锚定:Agent并不直接阅读全量代码,而是利用MCP驱动底层扫描器,基于AST解析和高精度的正则模式,快速提取代码中的SINK点(需要根据不同语言类型的不同漏洞进行扩充分类)

image.png

  • 代码切片与上下文聚焦:一旦定位到SINK点,系统会通过静态分析工具获取sink点污染的上下文Code Slice,并且做到变量语句级,将无关语句统统移除(这里详细的实现师傅们可以去阅读Joern等工具的源码和他的论文,主要在于CPG代码属性图的构建和后向切片等算法技术)。极大地收敛了分析范围,过滤大量无关业务代码,确保输送给LLM进行深度研判的每一行代码都具有潜在的安全价值(无论是控制流还是数据依赖流都对漏洞的存在有潜在的约束和影响)。这不仅大幅降低了Token消耗,更显著提升了后续漏洞验证的准确性。

2. Contextual Reasoning Agent:基于RAG的领域知识增强与检索优化

作为本框架保障检测精度的核心组件,校验 Contextual Reasoning Agent承担着“校验”的角色。针对通用大语言模型在特定安全领域存在的专业知识匮乏逻辑幻觉 问题,本模块引入RAG 技术,人为构建了一个可随时扩展的领域专家知识文档库,通过实时注入精确的先验知识来约束和校准模型的推理过程。

RAG知识库的结构化重构与向量化

为了让非结构化的安全知识能够被机器高效理解,摒弃粗暴的文本截断,采用基于Markdown语法树的结构化清洗策略。系统依据标题层级对海量的漏洞PoC、修复方案及原理分析文档进行逻辑切分,确保每个Chunk都包含完整的语义单元

例如一个简易的MARKDOWN文档:

image.png

动态滑窗与重叠分块策略

在知识切片过程中,为了规避硬切分导致的语义断层,切片策略采用基于重叠策略(Overlapping Strategy)的动态滑窗机制

  • 语义连贯性保障:设定固定的Token阈值作为基础窗口大小,同时引入预设比例的重叠缓冲区。每一分块的末尾段落会被完整保留并作为下一分块的起始上下文。
  • 边界信息无损传输:这种机制确保了跨越分块边界的逻辑描述(如一段跨越多行的代码逻辑或长难句的漏洞解释)不会被割裂,保证了向量检索时上下文信息的完整性与连贯性。

image.png

向量检索与推理运行

采用all-MiniLM-L6-v2模型作为Embedding引擎。该模型在保持低延迟推理的同时,在多语言的语义相似度任务上有更好的泛化能力;数据库采用集成Qdrant向量数据库,支撑大规模向量的高并发检索

  • 上下文感知的推理校准:当定位Agent上报疑似SINK点时,校验Agent会提取当前代码特征,在向量库中实时检索最相似的Top-K个历史漏洞模式和修复示例。这些检索结果被作为增强上下文 注入到LLM的Prompt中,迫使模型基于检索到的“事实依据”而非单纯的概率预测进行最终判定,减少了误报的产生

0x04 动态流量FUZZ

我从以往的安全研究触发,针对通用型漏洞的工具做了大量的调研,并基于BurpSuite原生API开发了自动化Fuzz工具如:反射性和存储型XSS、SSRF、CORS、敏感信息泄露等(同时也是在锻炼开发能力,也让日常重复性漏洞渗透工作能够做的更高效),再结合MCP集成给Agent。该模块并非简单的随机测试,而是作为一个流式检测组件,实时拦截、解析并重放业务流量,对潜在漏洞动态扫描。而对于敏感信息泄露则是比较容易 ,针对Spring Boot Actuator、Swagger UI、Druid Monitor等常见中间件的指纹来做识别。同时,结合模式匹配,对响应包中的JWT Token、阿里云AK/SK、AWS凭证等高熵字符串进行实时监测,有效发现硬编码或调试信息泄露。

下面挑了几个通用型漏洞的Fuzz来做简单做下原理解释

1. 通用XSS漏洞的自动化Fuzz

比如针对XSS反射型和存储型漏洞,开发时采用了全量参数解析+动态污点标记的检测策略,确保对异构http包结构中参数的全面覆盖。

  • 深度参数提取与结构化解析
    不仅仅局限于URL Query参数,还有针对JSON、XML、Multipart-form等多种数据格式的解析器。能够递归遍历HTTP Request Body中的每一层嵌套结构,提取所有用户可控的叶子节点作为Fuzz入口。
  • 唯一性污点标记
    为了解决并发扫描时的结果混淆问题,引擎摒弃了静态Payload,转而采用动态生成的唯一性测试标记


    • Payload构造:Timestamp + RandomStr + Vector(例如:CurrentTime等高熵字符串)
    • 状态映射表:内存中维护一张高并发的HashMap,记录RequestID <-> ParameterName <-> UniquePayload的映射关系。
    • 响应回显与验证
      发送测试请求后,引擎自动捕获HTTP Response,通过高效的字符串匹配算法检索之前的唯一标记。一旦检测到标记回显且上下文未经过滤(如HTML实体编码缺失),即判定存在可疑XSS漏洞,并自动关联原始请求数据生成漏洞条目。

(当时研究设计思路时绘制的草图)

image.png

2. 访问控制与配置缺陷的CORS漏洞检测

自动化Fuzz HTTP请求头中的Origin字段,构造包括恶意第三方域名、特殊字符(如null)及子域名在内的多种变异Payload

  • 高危利用判定:当响应头Access-Control-Allow-Origin和攻击者Payload一样或为小写null,且同时存在Access-Control-Allow-Credentials: true时,将其标记为高危漏洞。此类配置允许攻击者绕过同源策略(SOP)窃取用户敏感数据
  • 严格语法校验:针对协议规范的边缘场景进行校验,例如检测到Access-Control-Allow-Origin: Null(大写)时,引擎会自动识别其为无效配置(浏览器不识别大写Null),从而将其作为无效处理
    以及服务端错误配置导致Access-Control-Allow-Origin始终和Origin一样,这里放一张示例图便于理解:

image.png

0x05 构建认知型安全智能体的未来图景

在对Multi-Agent探索自动化漏洞挖掘实践的探索过程中,其实我们一直在试图回答一个核心问题:如何在安全攻防领域,构建一个具备“感知-推理-决策-行动”完整闭环的智能系统。目前的Agent主要还停留在“检测与验证”阶段,之后更完备的阶段是自动化环境的感知探索与白盒源码的结合,以及能够基于当前的Shell环境或数据库权限,自主规划后续的横向移动与权限提升路径。另一个重要的方面是自适应Payload生成:比如利用强化学习反馈机制,让Agent在面对WAF拦截时,能够动态调整Payload的混淆策略,实现智能化的WAF绕过

希望本文的实践能为各位师傅提供一种新的视角供师傅们交流指点~

struct类型的定义以关键字struct开头,后跟struct的名字,接着是定义在一对花括号中的struct定义体。struct定义体中可以定义一系列的成员变量、成员属性、静态初始化器、构造函数和成员函数。

定义struct类型

以下是定义struct类型的一个示例:

struct Rectangle {
    let width: Int64
    let height: Int64

    public init(width: Int64, height: Int64) {
        this.width = width
        this.height = height
    }

    public func area() {
        width * height
    }
}

上例中定义了名为Rectangle的struct类型,它有两个Int64类型的成员变量width和height,一个有两个Int64类型参数的构造函数init,以及一个成员函数area,用于返回width和height的乘积。

1. struct成员变量

struct成员变量分为实例成员变量和静态成员变量(使用static修饰符修饰,且必须有初值),二者访问上的区别在于实例成员变量只能通过struct实例访问,静态成员变量只能通过struct类型名访问。

实例成员变量定义时可以不设置初值(但必须标注类型),如上例中的width和height。也可以设置初值,例如:

struct Rectangle {
    let width = 10
    let height = 20
}

2. struct静态初始化器

struct支持定义静态初始化器,并在静态初始化器中通过赋值表达式来对静态成员变量进行初始化。

静态初始化器以关键字组合static init开头,后跟无参参数列表和函数体,且不能被访问修饰符修饰。函数体中必须完成对所有未初始化的静态成员变量的初始化,否则编译报错。

struct Rectangle {
    static let degree: Int64
    static init() {
        degree = 180
    }
}

一个struct中最多允许定义一个静态初始化器,否则报重定义错误。

struct Rectangle {
    static let degree: Int64
    static init() {
        degree = 180
    }
    static init() { // 错误!用前面的静态init函数重新定义
        degree = 180
    }
}

3. struct构造函数

struct支持两类构造函数:普通构造函数和主构造函数。

普通构造函数以关键字init开头,后跟参数列表和函数体,函数体中必须完成对所有未初始化的实例成员变量的初始化,否则编译报错。

struct Rectangle {
    let width: Int64
    let height: Int64

    public init(width: Int64, height: Int64) { // 错误! 'height'未在构造函数中初始化
        this.width = width
    }
}

一个struct中可以定义多个普通构造函数,但它们必须构成重载,否则报重定义错误。

struct Rectangle {
    let width: Int64
    let height: Int64

    public init(width: Int64) {
        this.width = width
        this.height = width
    }

    public init(width: Int64, height: Int64) { // 正确!用第一个init函数重载
        this.width = width
        this.height = height
    }

    public init(height: Int64) { // 错误!使用第一个init函数重新定义
        this.width = height
        this.height = height
    }
}

除了可以定义若干普通的以init为名字的构造函数外,struct内还可以定义(最多)一个主构造函数。主构造函数的名字和struct类型名相同,它的参数列表中可以有两种形式的形参:普通形参和成员变量形参(需要在参数名前加上let或var),成员变量形参同时扮演定义成员变量和构造函数参数的功能。

使用主构造函数通常可以简化struct的定义,例如,上述包含一个init构造函数的Rectangle可以简化为如下定义:

struct Rectangle {
    public Rectangle(let width: Int64, let height: Int64) {}
}

主构造函数的参数列表中也可以定义普通形参,例如:

struct Rectangle {
    public Rectangle(name: String, let width: Int64, let height: Int64) {}
}

如果struct定义中不存在自定义构造函数(包括主构造函数),并且所有实例成员变量都有初始值,则会自动为其生成一个无参构造函数(调用此无参构造函数会创建一个所有实例成员变量的值均等于其初值的对象);否则,不会自动生成此无参构造函数。例如,对于如下struct定义,注释中给出了自动生成的无参构造函数:

struct Rectangle {
    let width: Int64 = 10
    let height: Int64 = 10
    /* Auto-generated memberwise constructor:
    public init() {
    }
    */
}

4. struct成员函数

struct成员函数分为实例成员函数和静态成员函数(使用static修饰符修饰),二者的区别在于:实例成员函数只能通过struct实例访问,静态成员函数只能通过struct类型名访问;静态成员函数中不能访问实例成员变量,也不能调用实例成员函数,但在实例成员函数中可以访问静态成员变量以及静态成员函数。

下例中,area是实例成员函数,typeName是静态成员函数。

struct Rectangle {
    let width: Int64 = 10
    let height: Int64 = 20

    public func area() {
        this.width * this.height
    }

    public static func typeName(): String {
        "Rectangle"
    }
}

实例成员函数中可以通过this访问实例成员变量,例如:

struct Rectangle {
    let width: Int64 = 1
    let height: Int64 = 1

    public func area() {
        this.width * this.height
    }
}

5. struct成员的访问修饰符

struct的成员,包括成员变量、成员属性、构造函数、成员函数、操作符函数,可以用4种访问修饰符修饰:private、internal、protected和public,缺省的修饰符是internal。

  • private表示在struct定义内可见。
  • internal表示仅当前包及子包内可见。
  • protected表示当前模块可见。
  • public表示模块内外均可见。

下面的例子中,width是public修饰的成员,在类外可以访问,height是缺省访问修饰符的成员,仅在当前包及子包可见,其他包无法访问。

package a
publicstructRectangle {
    public var width: Int64
    var height: Int64
    private var area: Int64
    ...
}

func samePkgFunc() {
    var r = Rectangle(10, 20)
    r.width = 8               // Ok: public 'width' can be accessed here
    r.height = 24             // Ok: 'height' has no modifier and can be accessed here
    r.area = 30               // 错误!, private 'area' can't be accessed here
}
package b
import a.*
main() {
    var r = Rectangle(10, 20)
    r.width = 8               // Ok: public 'width' can be accessed here
    r.height = 24             // 错误!, no modifier 'height' can't be accessed here
    r.area = 30               // 错误!, private 'area' can't be accessed here
}

6. 禁止递归struct

递归和互递归定义的struct均是非法的。例如:

struct R1 { // 错误!'R1' 递归引用自身
    let other: R1
}
struct R2 { // 错误!'R2' 和 'R3' 递归引用自身
    let other: R3
}
struct R3 { // 错误!'R2' 和 'R3' 递归引用自身
    let other: R2
}

创建struct实例

定义了struct类型后,即可通过调用struct的构造函数来创建struct实例。在struct定义之外,通过struct类型名调用构造函数。例如,下例中定义了一个Rectangle类型的变量r。

let r = Rectangle(10, 20)

创建了struct实例之后,可以通过实例访问它的(public修饰的)实例成员变量和实例成员函数。例如,下例中通过r.width和r.height可分别访问r中width和height的值,通过r.area()可以调用r的成员函数area。

let r = Rectangle(10, 20)
let width = r.width   // width = 10
let height = r.height // height = 20
let a = r.area()      // a = 200

如果希望通过struct实例去修改成员变量的值,需要将struct类型的变量定义为可变变量,并且被修改的成员变量也必须是可变成员变量(使用var定义)。举例如下:

struct Rectangle {
    public var width: Int64
    public var height: Int64

    public init(width: Int64, height: Int64) {
        this.width = width
        this.height = height
    }

    public func area() {
        width * height
    }
}

main() {
    var r = Rectangle(10, 20) // r.width = 10, r.height = 20
    r.width = 8               // r.width = 8
    r.height = 24             // r.height = 24
    let a = r.area()          // a = 192
}

在赋值或传参时,会对struct实例进行复制,生成新的实例,对其中一个实例的修改并不会影响另外一个实例。以赋值为例,下面的例子中,将r1赋值给r2之后,修改r1的width和height的值,并不会影响r2的width和height值。

struct Rectangle {
    public var width: Int64
    public var height: Int64

    public init(width: Int64, height: Int64) {
        this.width = width
        this.height = height
    }

    public func area() {
        width * height
    }
}

main() {
    var r1 = Rectangle(10, 20) // r1.width = 10, r1.height = 20
    var r2 = r1                // r2.width = 10, r2.height = 20
    r1.width = 8               // r1.width = 8
    r1.height = 24             // r1.height = 24
    let a1 = r1.area()         // a1 = 192
    let a2 = r2.area()         // a2 = 200
}

mut函数

struct类型是值类型,其实例成员函数无法修改实例本身。例如,下例中,成员函数g中不能修改成员变量i的值。

struct Foo {
    var i = 0

    public func g() {
        i += 1  // 错误!无法在实例成员函数中修改实例成员变量的值
    }
}

mut函数是一种可以修改struct实例本身的特殊的实例成员函数。在mut函数内部,this的语义是特殊的,这种this拥有原地修改字段的能力。

:只允许在interface、struct和struct的扩展内定义mut函数,禁止在class中定义mut函数。

mut函数与普通的实例成员函数相比,多一个mut关键字来修饰。

例如,下例中在函数g之前增加mut修饰符之后,即可在函数体内修改成员变量i的值。

struct Foo {
    var i = 0

    public mut func g() {
        i += 1  // 正确
    }
}

参考引用

背景

在开发“智能带办”应用时涉及到用户体系,开发阶段使用固定验证码形式跑通,在上线前准备接入短信服务时却遇到了难题,短信服务目前只对企业开发者开放了,个人开发者没办法再使用短信服务。为了顺利上架,退后求其次,改为了使用邮箱验证码等了。

邮箱验证码登录有两个弊端,一是不方便,很多用户进来发现是邮箱验证码登录不方便直接就退出应用了;二是合规风险,在申请安全评估报告时如果涉及到用户体系要求实名,邮箱没办法保证实名,还得再加入额外的实名体系,不仅麻烦而且很多都限制个人开发者没法使用。

其实最开始也考虑过要接入华为登录,看了一键登录文档发现也是只针对企业开发者,以为也是只有企业开发者可以使用,后面看了“华为账号登录”后发现个人开发者也可以使用,只是取不到手机号,正好不使用手机号可以规避合规方面的风险。
image.png

华为登录能力介绍

华为账号服务简介

Account Kit(华为账号服务)提供简单、快速、安全的登录功能,让用户快捷地使用华为账号登录应用。用户授权后,Account Kit可提供头像、昵称、手机号码等信息,帮助应用更了解用户。华为账号服务提供了登录、获取华为账号用户信息、未成年模式等。在开发过程中涉及下面几个概念:

  • OpenID:应用维度用户标识符,是华为账号用户在应用/元服务的唯一标识。不同应用/元服务(不管是否在同一个开发者账号下)获取到用户的OpenID不同。
  • UnionID:开发者维度用户标识符,华为账号用户同一开发者账号下的唯一标识。开发者有多个应用/元服务时,同一个开发者账号下的应用/元服务获取到用户的UnionID相同。
  • GroupUnionID:关联主体账号组维度用户标识符,是华为账号用户在关联主体账号组内的唯一标识。不同开发者账号加入同一关联主体账号组后,其组内所有开发者的应用/元服务获取到用户的GroupUnionID相同。
  • permission:数据或接口权限,通过该权限判断应用是否能获取对应数据或调用对应接口。
  • scopes:scope列表,用于获取用户数据。开发者向华为账号服务申请不同类型用户数据的标识。比如头像昵称(profile)、匿名手机号(quickLoginAnonymousPhone)等。
  • Authorization Code:授权码,用户使用华为账号登录成功之后,可通过返回的凭据解析出授权码,通过授权码可获取Access Token、Refresh Token、ID Token等。
  • Access Token:访问凭证,是访问被权限管控资源的应用级凭证。可使用Access Token调用获取用户信息接口获取用户信息。
  • ID Token:用户身份凭证,是OIDC (OpenID Connect) 协议相对于OAuth 2.0 协议扩展的一个用户身份凭证,包含用户信息。用户使用华为账号登录成功之后,可通过返回的凭据解析出Authorization Code、ID Token等数据。

在我们接口华为用户服务后,可以使用OpenId和UnionID绑定我们自己的账号体系。

华为账号服务交互流程

由于个人开发者无法使用“一键登录”,本文主要介绍 “华为账号登录”按钮登录。使用按钮登录我们可以使用Account Kit提供的华为账号登录按钮及服务端交互获取华为账号用户身份标识UnionID、OpenID,通过UnionID、OpenID完成用户登录;或者与应用账号完成绑定,绑定后用于登录或者验证。

华为账号登录按钮包含文本、标志和文本、标志三种样式,以满足应用对界面风格一致性和灵活性的要求。
image.png

账号服务开发者与华为能力交互流程如下图所示:
image.png

交互流程说明如下:
流程说明:

  1. 调用登录按钮展示登录页阶段(序号1-3):

    1. 用户打开应用进行登录,应用设置LoginType类型为LoginType.ID后拉起应用自己的登录页并展示“华为账号登录”按钮,用户点击按钮,请求华为账号授权信息。
  2. 用户点击登录阶段(序号4-6):

    1. 如华为账号未登录,将拉起华为账号登录页,用户登录后,将返回Authorization Code等数据给应用。
    2. 如华为账号已登录,将直接返回Authorization Code等数据给应用。
  3. 用户关联应用账号阶段(序号7-16):

    1. 应用服务端通过Authorization Code获取到Access Token,再使用Access Token调用解析凭证接口获取用户相关信息。通过Authorization Code凭证获取用户信息可以有效避免黑客通过数据遍历、身份伪造、重放攻击等手段导致的安全风险。
    2. 应用服务端将业务登录凭证SessionId、UnionID/OpenID传给应用,应用获取到UnionID/OpenID可用于判断华为账号是否登录等功能。
    3. 应用对用户身份标识UnionID/OpenID、业务登录凭证SessionId信息进行认证后,通过UnionID/OpenID判断用户是否已关联应用系统数据库,如已关联,则完成用户登录;如未关联,则创建新用户,绑定UnionID/OpenID。

华为账号服务提供了LoginWithHuaweiIDButton组件,构造中需要传入LoginWithHuaweiIDButtonParams类型和 LoginWithHuaweiIDButtonController类型的参数,LoginWithHuaweiIDButtonParams属性如下:

名称类型只读可选说明
styleStyleLoginWithHuaweiIDButton组件的样式。支持样式包括:BUTTON_RED、BUTTON_WHITE、BUTTON_WHITE_OUTLINE、BUTTON_BLACK、ICON_RED、ICON_WHITE、ICON_WHITE_OUTLINE、ICON_BLACK、ICON_GRAY、BUTTON_GRAY、BUTTON_CUSTOM。
borderRadiusnumber按钮边框圆角半径。取值范围:[0,+∞),值小于0时,按0处理。默认值:height属性取值的一半。单位:vp。
iconRadiusnumberIcon类型按钮的半径。取值范围:[0,+∞),值小于0时,按0处理。默认值:24。单位:vp。
supportDarkModeboolean表示按钮的样式是否随系统深浅色模式变化。true:按钮的样式会随着系统深浅色模式变化。false:按钮的样式不会随着系统深浅色模式变化。默认值:true。
loginTypeLoginType华为账号登录类型。默认值:LoginType.ID。一键登录请使用LoginType.QUICK_LOGIN。
textAndIconStyleboolean是否展示图文混合样式的华为账号登录按钮。true:按钮支持Icon和文字混合样式。false:按钮仅支持文本样式。默认值:false。当loginType不等于LoginType.QUICK_LOGIN且style等于BUTTON_RED、BUTTON_WHITE、BUTTON_WHITE_OUTLINE、BUTTON_BLACK、BUTTON_GRAY时该参数生效。起始版本:5.0.0(12)
customButtonParamsCustomButtonParamsBUTTON_CUSTOM按钮样式参数。起始版本:5.0.0(12)
verifyPhoneNumberboolean华为账号用户在过去90天内未进行短信验证,是否拉起Account Kit提供的短信验证码页面。true:拉起Account Kit提供的短信验证码页面。false:不拉起Account Kit提供的短信验证码页面。需要应用验证手机号时效性。默认值:true。起始版本:5.0.0(12)
extraStyleExtraStyle如果应用想使用华为账号提供的固定样式之外的效果,可使用此接口自定义按钮样式。起始版本:5.0.0(12)
loginButtonTextTypeLoginButtonTextType当loginType为LoginType.QUICK_LOGIN时,可传入此参数,控制按钮文本内容显示。默认值:LoginButtonTextType.QUICK_LOGIN。当该参数为LoginButtonTextType.QUICK_LOGIN时,按钮文本内容显示“华为账号一键登录”。当该参数为LoginButtonTextType.QUICK_REGISTRATION时,按钮文本内容显示“华为账号一键注册”。起始版本:5.0.0(12)
riskLevelboolean是否需要获取华为账号用户风险等级。仅登录类型为LoginType.QUICK_LOGIN时需要设置该参数。true:需要获取用户风险等级。false:不获取用户风险等级。默认值:false。起始版本:5.1.0(18)
securityVerificationboolean用户开启华为账号一键登录增强身份验证后,应用会在登录过程中通过华为账号使用生物识别或短信进行身份验证。如果需要获取用户一键登录增强身份验证的开关状态,需设置该字段为false。仅登录类型为LoginType.QUICK_LOGIN时需要设置该参数。true:响应结果HuaweiIDCredential将不会返回 enableSecurityVerification。false:响应结果HuaweiIDCredential将返回 enableSecurityVerification。默认值:true。起始版本:6.0.0(20)

智能带办接入过程

目前应用只支持华为登录,页面UI如下:
image.png

在页面中配置红色的LoginWithHuaweiIDButton:

LoginWithHuaweiIDButton({  
    params: {  
      // LoginWithHuaweiIDButton支持的样式  
      style: loginComponentManager.Style.BUTTON_RED,  
      // 账号登录按钮在登录过程中展示加载态  
      extraStyle: {  
        buttonStyle: new loginComponentManager.ButtonStyle().loadingStyle({  
          show: true  
        })  
      },  
      // LoginWithHuaweiIDButton的边框圆角半径  
      borderRadius: 24,  
      // LoginWithHuaweiIDButton支持的登录类型  
      loginType: loginComponentManager.LoginType.ID,  
      // LoginWithHuaweiIDButton支持按钮的样式跟随系统深浅色模式切换  
      supportDarkMode: true  
    },  
    controller: this.controller  
  })  
}  
.height(40)  
.width('100%')  
.margin({top:50})  
.padding({left:25, right:25})

控制器controller定义如下:

controller: loginComponentManager.LoginWithHuaweiIDButtonController =  
  new loginComponentManager.LoginWithHuaweiIDButtonController()  
    .setAgreementStatus(loginComponentManager.AgreementStatus.NOT_ACCEPTED)  
    .onClickLoginWithHuaweiIDButton((error: BusinessError, response: loginComponentManager.HuaweiIDCredential) => {  
      if (error) {  
        this.dealAllError(error);  
        return;  
      }  
  
      if (response) {  
        Logger.i(TAG, 'Succeeded in getting response.');  
        const authCode = response.authorizationCode;  
        // 开发者处理authCode  
        this.getUserInfoPermission(authCode)  
      }  
    });

在controller中获取回调,如果登录成功则通过authorizationCode继续申请用户华为头像和昵称授权:

getUserInfoPermission(authCode:string){  
  // 创建授权请求,并设置参数  
  const authRequest = new authentication.HuaweiIDProvider().createAuthorizationWithHuaweiIDRequest();  
  // 获取头像昵称需要传如下scope  
  authRequest.scopes = ['profile'];  
  // 若开发者需要进行服务端开发以获取头像昵称,则需传如下permission获取authorizationCode  
  authRequest.permissions = ['serviceauthcode'];  
  // 用户是否需要登录授权,该值为true且用户未登录或未授权时,会拉起用户登录或授权页面  
  authRequest.forceAuthorization = true;  
  // 用于防跨站点请求伪造  
  authRequest.state = util.generateRandomUUID();  
  // 执行授权请求  
  try {  
    const controller = new authentication.AuthenticationController(this.getUIContext().getHostContext());  
    controller.executeRequest(authRequest).then((data) => {  
      const authorizationWithHuaweiIDResponse = data as authentication.AuthorizationWithHuaweiIDResponse;  
      const state = authorizationWithHuaweiIDResponse.state;  
      if (state && authRequest.state !== state) {  
        Logger.i(TAG, `Failed to authorize. The state is different, response state: ${state}`);  
        return;  
      }  
      Logger.i(TAG,'Succeeded in authentication.');  
      const authorizationWithHuaweiIDCredential = authorizationWithHuaweiIDResponse?.data;  
      const avatarUri = authorizationWithHuaweiIDCredential?.avatarUri;  
      const nickName = authorizationWithHuaweiIDCredential?.nickName;  
      // 开发者处理avatarUri, nickName  
      const authorizationCode = authorizationWithHuaweiIDCredential?.authorizationCode;  
      Logger.i(TAG, 'getUserInfoPermission:' + JsonUtils.toJSONString(authorizationWithHuaweiIDCredential))  
      this.sendLoginRequest(authorizationCode??authCode)  
      // 涉及服务端开发以获取头像昵称场景,开发者处理authorizationCode  
    }).catch((err: BusinessError) => {  
      this.dealAllError(err);  
    });  
  } catch (error) {  
    this.dealAllError(error);  
  }  
}

用户授权成功后请求服务端接口,服务端通过authorizationCode调用华为服务获取accessToken,接着获取用户信息,绑定自己的账号体系返回自己账号体系的token即可。通过下面接口获取用户级凭证:

POST /oauth2/v3/token HTTP/1.1
Host: oauth-login.cloud.huawei.com
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&code=<code>&client_id=<client_id>&client_secret=<client_secret>

接着通过下面示例获取用户昵称和头像:

POST /rest.php?nsp_svc=GOpen.User.getInfo HTTP/1.1
Host: account.cloud.huawei.com
Content-Type: application/x-www-form-urlencoded

access_token=<Access Token>

必须在手机上调起授权获取用户授权后这里才可以请求到用户头像和昵称。

总结

本次“智能带办”应用的登录体系接入实践,源于上线前短信服务仅对企业开发者开放的限制,迫使我们从固定验证码、邮箱验证码转向华为账号登录方案。初期因误判“一键登录”仅限企业开发者而忽略“华为账号登录”,后发现个人开发者虽无法获取手机号,但恰好规避了邮箱登录的用户体验差(用户因不便退出)与实名合规风险(需额外实名体系),成为关键破局点。

华为账号服务(Account Kit)通过OpenID(应用唯一标识)、UnionID(开发者唯一标识)等核心概念,为个人开发者提供了安全高效的登录能力:既支持自定义样式的登录按钮(如本文配置的红色BUTTON_RED按钮),又通过Authorization CodeAccess Token→用户信息的流程保障安全,避免身份伪造等风险。接入过程中,我们通过LoginWithHuaweiIDButton组件实现前端交互,结合服务端解析凭证绑定自有账号体系,最终完成用户登录闭环。

此次实践的核心启示在于:面对企业级服务限制时,需深度挖掘平台对个人开发者的差异化能力——华为账号登录虽不提供手机号,却以“去实名化”特性解决了合规痛点,同时依托成熟的OAuth 2.0/OIDC协议与丰富组件(如支持深色模式、自定义圆角的按钮),兼顾了开发效率与用户体验。未来,可进一步探索UnionID在多应用间的用户打通能力,或结合GroupUnionID拓展关联主体场景,持续完善登录体系的灵活性与扩展性。

本系列介绍增强现代智能体系统可靠性的设计模式,以直观方式逐一介绍每个概念,拆解其目的,然后实现简单可行的版本,演示其如何融入现实世界的智能体系统。本系列一共 14 篇文章,这是第 14 篇。原文:Building the 14 Key Pillars of Agentic AI

优化智能体解决方案需要软件工程确保组件协调、并行运行并与系统高效交互。例如预测执行,会尝试处理可预测查询以降低时延,或者进行冗余执行,即对同一智能体重复执行多次以防单点故障。其他增强现代智能体系统可靠性的模式包括:

  • 并行工具:智能体同时执行独立 API 调用以隐藏 I/O 时延。
  • 层级智能体:管理者将任务拆分为由执行智能体处理的小步骤。
  • 竞争性智能体组合:多个智能体提出答案,系统选出最佳。
  • 冗余执行:即两个或多个智能体解决同一任务以检测错误并提高可靠性。
  • 并行检索和混合检索:多种检索策略协同运行以提升上下文质量。
  • 多跳检索:智能体通过迭代检索步骤收集更深入、更相关的信息。

还有很多其他模式。

本系列将实现最常用智能体模式背后的基础概念,以直观方式逐一介绍每个概念,拆解其目的,然后实现简单可行的版本,演示其如何融入现实世界的智能体系统。

所有理论和代码都在 GitHub 仓库里:🤖 Agentic Parallelism: A Practical Guide 🚀

代码库组织如下:

agentic-parallelism/
    ├── 01_parallel_tool_use.ipynb
    ├── 02_parallel_hypothesis.ipynb
    ...
    ├── 06_competitive_agent_ensembles.ipynb
    ├── 07_agent_assembly_line.ipynb
    ├── 08_decentralized_blackboard.ipynb
    ...
    ├── 13_parallel_context_preprocessing.ipynb
    └── 14_parallel_multi_hop_retrieval.ipynb

深度推理的多跳检索

许多复杂的用户查询并非单一问题,而是比较性的、多步骤的调研任务,需要从多个不同来源的文档中综合信息。

并行多跳

解决方案是 并行多跳检索(Parallel Multi-Hop Retrieval) 架构,这种模式将 RAG 系统提升为真正的调研代理,工作流模拟人类研究员如何处理复杂问题的过程:

  1. 分解(Decompose):高级元代理首先分析复杂的用户查询,将其分解为几个更简单、独立的子问题。
  2. 分散(并行检索):每个子问题都被派发给各自的专用检索代理。这些代理并行运行,每个代理执行标准 RAG 流程,为特定子问题寻找答案。
  3. 收集与综合:元代理收集所有子问题的答案,进行最终推理步骤,将它们综合为对原始复杂查询的单一、全面的答案。

我们将以一个无法通过单一检索回答的比较性问题为例,构建并比较简单 RAG 系统与多跳 RAG 系统,证明只有多跳系统才能成功收集必要的证据,以提供准确且富有洞察力的最终答案。

首先为初始分解步骤定义 Pydantic 模型,从而结构化元代理规划阶段输出的内容。

from langchain_core.pydantic_v1 import BaseModel, Field
from typing import List

class SubQuestions(BaseModel):
    """分解代理输出的Pydantic模型,包含一组独立的子问题"""
    questions: List[str] = Field(description="A list of 2-3 simple, self-contained questions that, when answered together, will fully address the original complex query.")

这个 SubQuestions 模型是元代理首次行动的合约,迫使 LLM 将复杂查询分解为一系列简单、可回答的问题,是并行"分而治之"策略的基础步骤。

然后构建高级多跳系统作为 LangGraph 图。第一个节点将是"分解器",即元代理的规划角色。

from typing import TypedDict, List, Dict, Annotated
import operator

class MultiHopRAGState(TypedDict):
    original_question: str
    sub_questions: List[str]
    # 字典以问题作为键,存储每个子问题的答案
    sub_question_answers: Annotated[Dict[str, str], operator.update]
    final_answer: str

# 节点 1:分解器(元代理的第一步)
decomposer_prompt = ChatPromptTemplate.from_template(
    "You are a query decomposition expert. Your job is to break down a complex question into simple, independent sub-questions that can be answered by a retrieval system. "
    "Do not try to answer the questions yourself.\n\n"
    "Question: {question}"
)

decomposer_chain = decomposer_prompt | llm.with_structured_output(SubQuestions)

def decomposer_node(state: MultiHopRAGState):
    """获取原始复杂问题并将其分解为子问题列表"""
    print("--- [Meta-Agent] Decomposing complex question... ---")
    result = decomposer_chain.invoke({"question": state['original_question']})
    print(f"--- [Meta-Agent] Generated {len(result.questions)} sub-questions. ---")
    return {"sub_questions": result.questions}

decomposer_node 是研究代理的战略大脑,它不会尝试回答查询,其唯一且关键的任务是分析用户意图并将其分解为一组独立、可并行化的研究任务。

下一个节点将并行为每个子问题协调执行标准的 RAG 流程。

from concurrent.futures import ThreadPoolExecutor, as_completed

# 标准、自包含的RAG链,是并行检索代理的“引擎”
sub_question_rag_chain = (
    {"context": retriever | format_docs, "question": RunnablePassthrough()}
    | generator_prompt
    | llm
    | StrOutputParser()
)

def retrieval_agent_node(state: MultiHopRAGState):
    """节点 2:为每个子问题并行运行完整 RAG 进程"""
    print(f"--- [Retrieval Agents] Answering {len(state['sub_questions'])} sub-questions in parallel... ---")
    
    answers = {}
    # 用 ThreadPoolExecutor 对每个子问题并发运行‘sub_question_rag_chain’
    with ThreadPoolExecutor(max_workers=len(state['sub_questions'])) as executor:
        # 为每个待回答子问题构建一个 future
        future_to_question = {executor.submit(sub_question_rag_chain.invoke, q): q for q in state['sub_questions']}
        for future in as_completed(future_to_question):
            question = future_to_question[future]
            try:
                answer = future.result()
                answers[question] = answer
                print(f"  - Answer found for sub-question: '{question}'")
            except Exception as e:
                answers[question] = f"Error answering question: {e}"
    # 将结果收集到“sub_question_answers”字典中
    return {"sub_question_answers": answers}

retrieval_agent_node 是系统中的分散-聚合核心,接收 sub_questions 列表,并用 ThreadPoolExecutor 将每个条目分配到各自独立的 RAG 链。这是一种强大的并行形式,同时运行多个完整 RAG 流程。在所有并行代理找到答案后,该节点将所有发现汇总到 sub_question_answers 字典中。

最后,“合成器”节点作为元代理的最终步骤,将并行发现整合为一个连贯的答案。

# 节点 3:合成器(元代理的最后一步)
synthesizer_prompt = ChatPromptTemplate.from_template(
    "You are a synthesis expert. Your job is to combine the answers to several sub-questions into a single, cohesive, and comprehensive answer to the user's original complex question.\n\n"
    "Original Question: {original_question}\n\n"
    "Sub-Question Answers:\n{sub_question_answers}"
)

synthesizer_chain = synthesizer_prompt | llm | StrOutputParser()

def synthesizer_node(state: MultiHopRAGState):
    """获取子问题的答案,并合成最终的全面答案"""
    print("--- [Meta-Agent] Synthesizing final answer... ---")
    
    # 将收集的子问题答案格式化为最终提示
    sub_answers_str = "\n".join([f"- Q: {q}\n- A: {a}" for q, a in state['sub_question_answers'].items()])
    
    final_answer = synthesizer_chain.invoke({
        "original_question": state['original_question'],
        "sub_question_answers": sub_answers_str
    })
    return {"final_answer": final_answer}

synthesizer_node 是至关重要的最终推理步骤,它本身不执行任何检索,任务是接收 sub_question_answers 中的预处理事实,并将其构造为能直接回应用户原始复杂查询的连贯叙述。

最后按线性顺序组装图:分解 -> 并行检索 -> 综合。

from langgraph.graph import StateGraph, END

workflow = StateGraph(MultiHopRAGState)
workflow.add_node("decompose", decomposer_node)
workflow.add_node("retrieve_in_parallel", retrieval_agent_node)
workflow.add_node("synthesize", synthesizer_node)

workflow.set_entry_point("decompose")

workflow.add_edge("decompose", "retrieve_in_parallel")
workflow.add_edge("retrieve_in_parallel", "synthesize")
workflow.add_edge("synthesize", END)
multi_hop_rag_app = workflow.compile()

并行多跳检索

给两个系统一个复杂且需要比较的问题,这个问题无法通过单次检索调用正确回答,从而对比分析两种查询方式。

# 查询需要比较两个产品,信息在独立、不重叠的文档中
user_query = "Compare the QLeap-V4 and the Eco-AI-M2, focusing on their target use case and power consumption."

# --- 执行简单 RAG ---
print("="*60)
print("                  SIMPLE RAG SYSTEM OUTPUT")
print("="*60 + "\n")
print(f"Final Answer:\n{simple_answer}")

# --- 执行多跳 RAG ---
print("\n" + "="*60)
print("                 MULTI-HOP RAG SYSTEM OUTPUT")
print("="*60 + "\n")
print("--- Sub-Question Answers ---")
for i, (q, a) in enumerate(multi_hop_result['sub_question_answers'].items()):
    print(f"{i+1}. Q: {q}\n   A: {a}")
print("\n--- Final Synthesized Answer ---")
print(multi_hop_result['final_answer'])

# --- 最终分析 ---
print("\n" + "="*60)
print("                     ACCURACY & QUALITY ANALYSIS")
print("="*60 + "\n")
print("**Simple RAG Performance:**")
print("- Result: COMPLETE FAILURE.")
print("- Reason: The user's query contained terms for both products. Vector search found the documents that were, on average, most semantically similar to the entire query, retrieving only documents about the Eco-AI-M2. It completely failed to retrieve any information about the QLeap-V4. Without the necessary context for both products, a comparison was impossible.\n")
print("**Multi-Hop RAG Performance:**")
print("- Result: COMPLETE SUCCESS.")
print("- Reason: The system's intelligence was in the initial decomposition step. The Meta-Agent broke the complex comparative query into two simple, focused sub-questions: 1. Get info on Product A. and 2. Get info on Product B. The parallel Retrieval Agents had no trouble answering these simple questions, each retrieving the correct, focused context. The final Synthesizer agent then received a perfect, complete set of facts about both products, making the final comparison trivial.")

输出为……

#### 输出 ####
============================================================
                  SIMPLE RAG SYSTEM OUTPUT
============================================================

Final Answer:
Based on the provided context, the Eco-AI-M2 chip is designed for edge computing and mobile devices, with a primary feature of low power consumption at only 15W under full load. The context does not contain information about the QLeap-V4, so I cannot provide a comparison.

============================================================
                 MULTI-HOP RAG SYSTEM OUTPUT
============================================================
--- Sub-Question Answers ---
1. Q: What is the target use case and power consumption of the QLeap-V4?
   A: The QLeap-V4 processor is designed for maximum performance in data centers, with a primary use case of large-scale AI model training. It consumes 1200W of power under full load.
2. Q: What is the target use case and power consumption of the Eco-AI-M2?
   A: The Eco-AI-M2 chip is designed for edge computing and mobile devices like drones and smart cameras. Its key feature is low power consumption, drawing only 15W under full load.
--- Final Synthesized Answer ---
The QLeap-V4 and the Eco-AI-M2 are designed for very different purposes, primarily distinguished by their target use case and power consumption.
-   **QLeap-V4**: This is a high-performance processor intended for data centers. Its main use case is large-scale AI model training, and it has a high power consumption of 1200W.
-   **Eco-AI-M2**: This is a low-power chip designed for edge computing and mobile devices. Its focus is on energy efficiency, consuming only 15W, making it suitable for applications like drones and smart cameras.

最终分析得出明确结论,性能差异并非渐进式,而是一次能力上的飞跃。

  • 单次检索步骤无法解决比较查询歧义,仅检索了两个产品中的一个上下文,从根本上无法收集必要的证据。
  • 多跳系统之所以成功,是因为没有试图一次性回答复杂问题,而是识别了查询的比较性质,并将问题分解。
  • 通过并行、专注的 RAG 代理来解决每个简单的子问题,确保收集了所有必要证据,最后的综合步骤只是简单的将预先处理的事实结合起来。

Hi,我是俞凡,一名兼具技术深度与管理视野的技术管理者。曾就职于 Motorola,现任职于 Mavenir,多年带领技术团队,聚焦后端架构与云原生,持续关注 AI 等前沿方向,也关注人的成长,笃信持续学习的力量。在这里,我会分享技术实践与思考。欢迎关注公众号「DeepNoMind」,星标不迷路。也欢迎访问独立站 www.DeepNoMind.com,一起交流成长。

本文由mdnice多平台发布

最近看到一个职场社区帖子,吐槽了一个关于面试和 offer 的相关话题,参与讨论的同学非常多。

问题描述差不多是这样:

“我发现凡是给 offer 的公司,面试时基本不问技术细节,那些问得又多又细的公司,后面基本就没下文了……”

那关于这个问题,不知道大家有没有类似的体验或者经历?

你信心满满地去一家公司,面试官是个看起来技术大拿模样的人,一上来就给你整了个高并发场景下的分布式锁实现,问你 JVM 调优的十八般武艺,甚至还要跟你探讨一下 Linux 内核的源码细节。

你虽然答得满头大汗,但自我感觉还不错,仿佛自己把毕生所学都展示出来了。

但是最后结果呢?客气地送你一句等通知,然后便石沉大海。或者回去等了个三五天、一个星期,最后等来的是一句冰冷的不合适。

而反观另外一些面试经历,你可能就是抱着去溜达一圈的心态去转转的,面试让你感觉像在聊天,聊聊项目,聊聊过往经历,聊聊技术。

你心里还在犯嘀咕,没了?就这?

结果第二天,HR 就打电话过来找你谈薪,然后询问入职时间,速度快得让你怀疑人生。

看到这里,你是不是也挺疑惑,这到底是为什么?

难道某些公司就爱玩反向筛选?还是说问技术细节本身就是一种送客的委婉方式?这背后到底有没有什么可以遵循的逻辑原理可以分析分析。

所以今天咱们也用一篇文章的篇幅来聊一聊这个话题,也欢迎大家分享交流自己的观点和看法。

对于那些问得又细又深,最后却没给 offer 的,往往有这么几种情况。

第一种,也是最现实、最常见的大环境筛选

什么意思呢?

现在的求职大环境大家也知道,岗位有限,候选人太多。HR 和面试官手里攥着一堆 985、211 甚至大厂背景的简历。

简单点说,他们不缺候选人,所以他们有资格挑。

对于中间段位的候选人,也就是我们大多数普通人,他们不需要看你有多优秀,只需要找出你简历里的一个瑕疵,一个技术细节没答上来,或许就有可能会把你刷掉。毕竟对于他们来说,能选择的太多。

其次,还有一个比较现实的问题是,对于那些问得细的公司,不代表真的招人

当一个团队实际并不缺人,或者只是抱着宁缺毋滥的心态在招人时,他们就有资本去挑刺。

这时候面试官常常带着一种找漏洞的心态。他们的问题像一张细密的筛网,目的似乎不是看你有多合适,而是为了证明你哪里不合适。

说实话,这种还是挺恶心的。

第三种,也是最最扎心的一种情况:你只是他们的「免费咨询顾问」

更直白一点说就是在套方案。

现在的行情下,很多公司业务停滞,不怎么招人,但又面临一些棘手的技术难题。

他们打着招聘的旗号,实际上是把市场上优秀的工程师请过来,所谓的面试其实也就是一场免费的头脑风暴。他们会故意引导你去讲你上一家公司的架构设计、服务拆分方案、甚至是具体的排错思路。

整个面试过程你自认为胸有成竹,方案和思路也讲得滔滔不绝,殊不知,人家还另有企图呢。

有一说一,这种是最最恶心的一种情况。

而对于那些问得不多、但 offer 倒是给的挺痛快的公司,通常又是怎么回事呢?

首先,这往往意味着这个公司是「真·缺人」呐。

这种公司通常处于一种“生死存亡”或者业务极速扩张的阶段。老板或者团队负责人可能已经被缺人折磨得寝食难安了。

他们的核心诉求非常明确:找个能立刻干活、能立刻上手的人。

这时候,他们不会跟你去扯什么虚头巴脑的设计模式,更不会去考你那些冷门的技术知识。

他们关心的是:你能不能明天就来上班,你能不能把这个烂摊子代码接过去维护,你能不能抗住连续一个月的强度。

在这种极度的需求面前,所谓的技术细节反倒成了次要的。

但是说实话,这种 offer 虽然来得容易,但兄弟记住,这往往也是把双刃剑

因为“真·缺人”的背后,往往意味着技术债巨多、管理混乱,或者是一个谁都不愿意接的坑。

拿到这种 offer,你既可能是一飞冲天的救世主,也有可能是一头扎进泥潭的接盘侠。

当然,还有一种情况,虽然不那么好听,但也必须提一嘴。

那就是,有些公司其实是在广撒网。他们可能并没有确切的 HC,或者他们需要的只是一个廉价的劳动力。

对于这种公司,问太多技术细节反而会吓跑你,他们更希望用更轻松的面试体验和更高薪的承诺来把你招进去,至于技术匹配度嘛,额……那是入职以后的事情了。

文章的最后我想说的是,面试是一个双向选择的过程,也是一个互相试探的过程。

当你遇到那个问得特别细的面试官时,别急着心里骂娘,也别急着觉得自己没戏了。你可以试着把这场技术拷问变成一场技术交流。

如果对方是在套方案,你可以适当保留,点到为止;如果对方是真的在考察技术深度,那正好展示你的技术功底。

而当你遇到那个聊两句就给 offer 的公司时,也别急着狂喜。

可以多问问团队现状,问问业务体量,问问技术栈,这时候,一定要记住,你该反问的要反问,该考察的要考察

因为虽说大环境寒冷,但是我觉得找到一个不坑的公司有时候比拿到一个所谓的 offer 更加重要,大家觉得呢?

好了,今天就先聊这么多吧,希望能对大家有所启发,我们下篇见。

注:本文在GitHub开源仓库「编程之路」 https://github.com/rd2coding/Road2Coding 中已经收录,里面有我整理的6大编程方向(岗位)的自学路线+知识点大梳理、面试考点、我的简历、几本硬核pdf笔记,以及程序员生活和感悟,欢迎star。

InheritableThreadLocal相比ThreadLocal多一个能力:在创建子线程Thread时,子线程Thread会自动继承父线程的InheritableThreadLocal信息到子线程中,进而实现在在子线程获取父线程的InheritableThreadLocal值的目的。

关于ThreadLocal详细内容,可以看这篇文章:史上最全ThreadLocal 详解

和 ThreadLocal 的区别

举个简单的栗子对比下InheritableThreadLocal和ThreadLocal:

public class InheritableThreadLocalTest {    
    private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();    
    private static final InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();    

    public static void main(String[] args) {        
        testThreadLocal();        
        testInheritableThreadLocal();    
    }    

    /**     * threadLocal测试     */    
    public static void testThreadLocal() {       
         // 在主线程中设置值到threadLocal        
         threadLocal.set("我是父线程threadLocal的值");        
         // 创建一个新线程并启动        
         new Thread(() -> {            
                 // 在子线程里面无法获取到父线程设置的threadLocal,结果为null            
                 System.out.println("从子线程获取到threadLocal的值: " + threadLocal.get());           }
         ).start();    
     }    
 
     /**     * inheritableThreadLocal测试     */  
    public static void testInheritableThreadLocal() {        
        // 在主线程中设置一个值到inheritableThreadLocal        
        inheritableThreadLocal.set("我是父线程inheritableThreadLocal的值");        
        // 创建一个新线程并启动        
        new Thread(() -> {            
                // 在子线程里面可以自动获取到父线程设置的inheritableThreadLocal    
                System.out.println("从子线程获取到inheritableThreadLocal的值: " + inheritableThreadLocal.get());        
            }).start();    
        }
    }

执行结果:

从子线程获取到threadLocal的值:null
从子线程获取到inheritableThreadLocal的值:我是父线程inheritableThreadLocal的值

可以看到子线程中可以获取到父线程设置的inheritableThreadLocal值,但不能获取到父线程设置的threadLocal值

实现原理

InheritableThreadLocal 的实现原理相当精妙,它通过在创建子线程的瞬间,“复制”父线程的线程局部变量,从而实现了数据从父线程到子线程的一次性、创建时的传递 。

其核心工作原理可以清晰地通过以下序列图展示,它描绘了当父线程创建一个子线程时,数据是如何被传递的:

sequenceDiagram
    participant Parent as 父线程
    participant Thread as Thread构造方法
    participant ITL as InheritableThreadLocal
    participant ThMap as ThreadLocalMap
    participant Child as 子线程

    Parent->>Thread: 创建 new Thread()
    Note over Parent,Thread: 关键步骤:初始化
    Thread->>Thread: 调用 init() 方法
    Note over Thread,ITL: 检查父线程的 inheritableThreadLocals
    Thread->>+ThMap: createInheritedMap(<br/>parent.inheritableThreadLocals)
    ThMap->>ThMap: 新建一个ThreadLocalMap
    loop 遍历父线程Map中的每个Entry
        ThMap->>+ITL: 调用 key.childValue(parentValue)
        ITL-->>-ThMap: 返回子线程初始值<br/>(默认返回父值,可重写)
        ThMap->>ThMap: 将 (key, value) 放入新Map
    end
    ThMap-->>-Thread: 返回新的ThreadLocalMap对象
    Thread->>Child: 将新Map赋给子线程的<br/>inheritableThreadLocals属性
    Note over Child: 子线程拥有父线程变量的副本

下面我们来详细拆解图中的关键环节。

### 核心实现机制

  1. **数据结构基础:Thread类内部维护了两个 ThreadLocalMap类型的变量 :

    • threadLocals:用于存储普通 ThreadLocal设置的变量副本。
    • inheritableThreadLocals:专门用于存储 InheritableThreadLocal设置的变量副本 。InheritableThreadLocal通过重写 getMapcreateMap方法,使其所有操作都针对 inheritableThreadLocals字段,从而与普通 ThreadLocal分离开 。
  2. 继承触发时刻:子线程的创建。继承行为发生在子线程被创建(即执行 new Thread())时。在 Thread类的 init方法中,如果判断需要继承(inheritThreadLocals参数为 true父线程(当前线程)的 inheritableThreadLocals不为 null,则会执行复制逻辑 。
  3. 复制过程的核心:createInheritedMap。这是实现复制的核心方法 。它会创建一个新的 ThreadLocalMap,并将父线程 inheritableThreadLocals中的所有条目遍历拷贝到新 Map 中。

    • Key的复制:Key(即 InheritableThreadLocal对象本身)是直接复制的引用。
    • Value的生成:Value 并非直接复制引用,而是通过调用 InheritableThreadLocalchildValue(T parentValue)方法来生成子线程中的初始值。默认实现是直接返回父值return parentValue;),这意味着对于对象类型,父子线程将共享同一个对象引用 。

关键特性与注意事项

  1. 创建时复制,后续独立:继承只发生一次,即在子线程对象创建的瞬间。此后,父线程和子线程对各自 InheritableThreadLocal变量的修改互不影响 。
  2. 在线程池中的局限性:这是 InheritableThreadLocal最需要警惕的问题。线程池中的线程是复用的,这些线程在首次创建时可能已经从某个父线程继承了值。但当它们被用于执行新的任务时,新的任务提交线程(逻辑上的“父线程”)与工作线程已无直接的创建关系,因此之前继承的值不会更新,这会导致数据错乱(如用户A的任务拿到了用户B的信息)或内存泄漏​ 。对于线程池场景,应考虑使用阿里开源的 TransmittableThreadLocal (TTL)​ 。
  3. 浅拷贝与对象共享:由于 childValue方法默认是浅拷贝,如果存入的是可变对象(如 MapList),父子线程实际持有的是同一个对象的引用。在一个线程中修改该对象的内部状态,会直接影响另一个线程 。若需隔离,可以重写 childValue方法实现深拷贝 。
  4. 内存泄漏风险:与 ThreadLocal类似,如果线程长时间运行(如线程池中的核心线程),并且未及时调用 remove方法清理,那么该线程的 inheritableThreadLocals会一直持有值的强引用,导致无法被GC回收。良好的实践是在任务执行完毕后主动调用 remove()

线程池中局限性

一般来说,在真实的业务场景下,没人会直接 new Thread,而都是使用线程池的,因此InheritableThreadLocal在线程池中的使用局限性要额外注意

首先,我们先理解 InheritableThreadLocal的继承前提

  • InheritableThreadLocal的继承只发生在 新线程被创建时(即 new Thread()并启动时)。在创建过程中,子线程会复制父线程的 InheritableThreadLocal值。
  • 在线程池中,线程是预先创建或按需创建的,并且会被复用。因此,继承只会在线程池创建新线程时发生,而不会在复用现有线程时发生。

再看线程池创建新线程的条件,对于标准的 ThreadPoolExecutor,新线程的创建遵循以下规则:

  1. 当前线程数 < 核心线程数:当提交新任务时,如果当前运行的线程数小于核心线程数,即使有空闲线程,线程池也会创建新线程来处理任务。此时,新线程会继承父线程(提交任务的线程)的 InheritableThreadLocal
  2. 当前线程数 >= 核心线程数 && 队列已满 && 线程数 < 最大线程数:当任务队列已满,且当前线程数小于最大线程数时,线程池会创建新线程来处理任务。同样,新线程会继承父线程的 InheritableThreadLocal

不会继承的场景

  • 线程复用:当线程池中有空闲线程时(例如,当前线程数 >= 核心线程数,但队列未满),任务会被分配给现有线程执行。此时,没有新线程创建,因此不会发生继承。现有线程的 InheritableThreadLocal值保持不变(可能是之前任务设置的值),这可能导致数据错乱(如用户A的任务看到用户B的数据)。
  • 线程数已达最大值:如果线程数已达最大线程数,且队列已满,新任务会被拒绝(根据拒绝策略),也不会创建新线程,因此不会继承。

不只是线程池污染,线程池使用 InheritableThreadLocal 还可能存在获取不到值的情况。例如,在执行异步任务的时候,复用了某个已有的线程A,并且当时创建该线程A的时候,没有继承InheritableThreadLocal,进而导致后面复用该线程的时候,从InheritableThreadLocal获取到的值为null:

public class InheritableThreadLocalWithThreadPoolTest {    
    private static final InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();    
    // 这里线程池core/max数量都只有2    
    private static final ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(            
        2,            
        2,            
        0L,            
        TimeUnit.MILLISECONDS,            
        new LinkedBlockingQueue<Runnable>(3000),            
        new ThreadPoolExecutor.CallerRunsPolicy()    
    );    
    
    public static void main(String[] args) {        
    // 先执行了不涉及InheritableThreadLocal的子任务初始化线程池线程 
           testAnotherFunction();        
           testAnotherFunction();        
           // 后执行了涉及InheritableThreadLocal
           testInheritableThreadLocalWithThreadPool("张三");        
           testInheritableThreadLocalWithThreadPool("李四");        
           threadPoolExecutor.shutdown();    
     }    
     
     /**     * inheritableThreadLocal+线程池测试     */    
        public static void testInheritableThreadLocalWithThreadPool(String param) {        
            // 1. 在主线程中设置一个值到inheritableThreadLocal        
             inheritableThreadLocal.set(param);        
            // 2. 提交异步任务到线程池        
            threadPoolExecutor.execute(() -> {            
            // 3. 在线程池-子线程里面可以获取到父线程设置的inheritableThreadLocal吗?            
                System.out.println("线程名: " + Thread.currentThread().getName() + ", 父线程设置的inheritableThreadLocal值: " + param + ", 子线程获取到inheritableThreadLocal的值: " + inheritableThreadLocal.get());        
            });        
            // 4. 清除inheritableThreadLocal        
            inheritableThreadLocal.remove();    
       }    
                   
       /**     * 模拟另一个独立的功能     */   
       public static void testAnotherFunction() {        
           // 提交异步任务到线程池        
           threadPoolExecutor.execute(() -> {            
           // 在线程池-子线程里面可以获取到父线程设置的inheritableThreadLocal吗?            
               System.out.println("线程名: " + Thread.currentThread().getName() + ", 线程池-子线程摸个鱼");        
           });    
       }
}

执行结果:

线程名:pool-1-thread-2,线程池-子线程摸个鱼
线程名:pool-1-thread-1,线程池-子线程摸个鱼
线程名:pool-1-thread-1,父线程设置的inheritableThreadLocal值:李四,子线程获取到inheritableThreadLocal的值:null
线程名:pool-1-thread-2,父线程设置的inheritableThreadLocal值:张三,子线程获取到inheritableThreadLocal的值:null

当然了,解决这个问题可以考虑使用阿里开源的 TransmittableThreadLocal (TTL),​或者在提交异步任务前,先获取线程数据,再传入。例如:

// 1. 在主线程中先获取inheritableThreadLocal的值
String name = inheritableThreadLocal.get();    
    
// 2. 提交异步任务到线程池        
threadPoolExecutor.execute(() -> {            
// 3. 在线程池-子线程里面直接传入数据  
System.out.println("线程名: " + Thread.currentThread().getName() + ", 父线程设置的inheritableThreadLocal值: " + param + ", 子线程获取到inheritableThreadLocal的值: " + name);        
            });        

与 ThreadLocal 的对比

特性ThreadLocalInheritableThreadLocal
数据隔离线程绝对隔离线程绝对隔离
子线程继承不支持支持(创建时)
底层存储字段Thread.threadLocalsThread.inheritableThreadLocals
适用场景线程内全局变量,避免传参父子线程间需要传递上下文数据

大家好,我是R哥。

话说我昨天不是发了《IDEA 出现重大 Bug!不要升级!不要升级!》这篇文章吗?

今天上午就收到了某同学的反馈:

今天确实也收到 IDEA 2025.3.1.1 版本的更新了:

难道 IntelliJ IDEA 连夜就修复了我这个 BUG??

这也太巧了吧?!

抱着预期的心情更新了 2025.3.1.1,结局让我有点失望,还是那样。。

删除各种缓存,试了各种方法都没有用,就差重装了(估计也没用),社区一堆的 BUG 贴都还是 OPEN 状态呢。

于是我去查了 2025.3.1.1 的更新说明:

https://youtrack.jetbrains.com/articles/IDEA-A-2100662602/Int...

确实修复了几个大 BUG,包括 IDEA 2025.3.1 打开大 Maven 项目时会卡死的问题也修复了,但弹窗空白这个 BUG 并没有涵盖其中。。

似乎官方是解决不了这个 BUG?

这个问题在 24.2.5 版本后就开始出现了,一直都没有解决,一个这么重大的 BUG 拖了这么久不修复,着实难以理解!

先勉强用着吧,后面如果官方修了,或者有绕过方案,我也会第一时间再跟大家同步。

好了,今天的分享就到这里了,后面我也会分享更多好玩的 Java 技术和最新的技术资讯,关注Java技术栈第一时间推送。

版权声明: 本文系公众号 "Java技术栈" 原创,转载、引用本文内容请注明出处,抄袭、洗稿一律投诉侵权,后果自负,并保留追究其法律责任的权利。

本周AI领域迎来密集进展,大模型在动漫生图(Niji V7)、端侧智能(AgentCPM-Explore)、医疗(Baichuan-M3)、多模态生图(GLM-Image)、视频生成(Veo 3.1、PixVerse R1)及机器人(1X World Model、LimX COSA)等垂直场景实现性能突破与场景适配;AI工具则聚焦电商、办公、音频处理等高频需求推出,Google UCP、Claude Cowork、Voice-Pro等高效解决方案,技术则在药物研发(DrugCLIP)、大模型部署(Engram模块)、生物研究(Stack模型)等领域实现跨学科赋能,一起来回顾本周的AI新鲜事儿吧!

AI 大模型

Midjourney联合推出动漫风格AI生图模型「Niji V7」

1月9日,Midjourney联合推出动漫风格AI生图模型「Niji V7」,核心更新包括图像质量提升(连贯性增强、细节如眼睛反射、花瓣更清晰,实现“高清升级”)、提示词遵循能力强化(精准理解位置/数量等具体请求,sref风格参考功能向前兼容,cref角色参考暂不支持)、设计美学突破(线条可传达更多形体质感信息,支持简约风格留白,线条与空间结合呈现更平面化效果),且个性化与情绪板功能即将上线。实测线条流畅、细节优化,但复杂场景和中式风格仍有不足。

OpenBMB开源社区联合发布4B「AgentCPM-Explore」端侧智能体模型

1月13日,OpenBMB开源社区联合清华大学自然语言处理实验室、中国人民大学及面壁智能发布4B参数的「AgentCPM-Explore」端侧智能体模型,是首个支持GAIA、Xbench等8个长难智能体任务的4B模型,可实现100+轮稳定环境交互,在主流评测基准上取得同尺寸SOTA表现,越级赶超8B级模型、比肩部分30B级以上及「Claude-4.5-Sonnet」等闭源大模型,还展现出“质疑”“求真”等类人思考逻辑,通过模型融合、信号去噪、信息精炼三大技术破解小模型训练难题,全流程开源AgentDock工具沙盒平台、AgentRL强化学习框架与AgentToLeaP一键测评平台支持复现与扩展。

Arc研究所开源单细胞基础模型「Stack」与「Perturb Sapiens」图谱

1月13日,Arc研究所宣布开源首个无需重新训练即可学习新任务的单细胞基础模型「Stack」及预测性细胞反应全景图谱「Perturb Sapiens」,「Stack」基于1.49亿个人类单细胞数据预训练、5500万个细胞后训练,通过表格化Transformer模块、基因模块表征符的架构创新及上下文学习的训练策略创新,能以细胞为“提示”预测目标细胞群在全新环境中的反应,在零样本下游任务中表现优于基线模型和现有方案;「Perturb Sapiens」则依托其能力生成约20000个“细胞类型-组织-扰动”预测组合,填补了相关实验空白,模型及图谱均已开源。

百川智能发布开源新一代医疗大模型「Baichuan-M3」,医疗幻觉率降至3.5

1月13日,百川智能正式开源新一代医疗大模型「Baichuan-M3」,在全球权威医疗AI评测HealthBench等多项权威评测中全面超越「GPT-5.2」,且以3.5的全球最低幻觉率刷新行业底线(通过将医学事实一致性融入训练实现)。该模型创新提出“严肃问诊范式”与SCAN原则,借助SPAR算法和SCAN-bench全流程动态评测体系,具备原生端到端严肃问诊能力,在安全分层、信息澄清等四大维度显著高于真人医生平均水平,同时其医疗应用「百小应」已同步接入该模型向医生与患者开放。

美团龙猫LongCat升级全新稀疏注意力机制「LoZA」,解码快10倍

1月13日消息,美团龙猫LongCat系列升级全新稀疏注意力机制「LoZA」(LongCat ZigZag Attention),通过给MLA模块配可学习权重α筛选50%低重要性模块替换为线性复杂度的SSA,形成ZigZag交错结构并设计1024 Token稀疏窗口,在中期训练阶段即可完成改造,使模型上下文窗口从256K扩展至1M,128K文本解码速度快10倍、256K预加载提速50%且解码省30%算力,日常任务性能持平原版,长文本任务表现更优,还计划支持动态稀疏比例及多模态长内容处理。

1X公司为家用人形机器人NEO推出全新世界模型「1X World Model」

1月13日,1X公司为家用人形机器人NEO推出全新世界模型「1X World Model」,相关内容浏览量超500万次。该模型基于视频预训练技术,通过“世界模型主干(文本条件扩散模型,经互联网视频预训练、人类第一视角中期训练、NEO专属微调)+逆动力学模型IDM”两阶段对齐,无需大规模机器人数据即可泛化到全新物体、动作与任务,能通过生成“成功完成任务”的视频倒推动作轨迹,支持抓取、双手协调、人机交互等任务且保持稳定成功率。

智谱与华为联合开源首个基于国产芯片训练的SOTA生图模型「GLM-Image」

1月14日,智谱与华为联合发布中国首个全程基于国产华为Ascend A2芯片及昇思MindSpore框架训练的SOTA多模态生图模型「GLM-Image」,采用“9B自回归模型+7B DiT扩散解码器”混合架构,擅长文字精准渲染,拿下CVTG-2K和LongText-Bench双榜单开源第一,原生支持1024x1024至2048x2048任意尺寸,API调用仅0.1元/张,可适配小红书封面、商业海报等多场景,已开源并提供多个平台接入地址,印证了国产算力底座支撑前沿模型训练的能力。

Google升级视频模型「Veo 3.1」,首次原生支持9:16竖屏视频

1月14日,Google升级视频模型「Veo 3.1」,首次原生支持9:16竖屏视频(适配YouTube Shorts等移动端平台,无需裁剪)并新增4K分辨率,同时提升创意能力(简单提示词可生成小剧场)、强化角色与背景物体一致性(跨场景保持元素完整)、改善元素融合能力(无缝组合多图元素),普通用户可通过YouTube Shorts、Gemini等体验,企业用户可借助Flow、Gemini API等使用;Google依托YouTube的平台、流量与生态优势,形成“创作-分发-反馈-优化”正向循环,而AI视频竖屏化已成趋势,OpenAI、迪士尼及国内可灵AI等均有相关布局。

爱诗科技发布全球首个通用实时世界模型「PixVerse R1」

1月14日,爱诗科技发布全球首个支持最高1080P分辨率实时生成的世界模型「PixVerse R1」,区别于传统AI视频的高延迟、固定时长与单向生成,凭借Omni原生多模态模型(统一多模态为连续Token流)、自回归流式生成机制(支持无限时长与长时序一致性)、瞬时响应引擎IRE(采样步骤1-4步,效率提升数百倍)三大技术创新,实现瞬时响应、实时共创,支持多模态交互与最高1080P输出,开启视频即交互、世界可共创的新范式,适用于游戏、电影、直播等场景。

生数科技Vidu AI开放平台发布「一键生成AI MV」功能

1月14日,生数科技Vidu AI开放平台发布「一键生成AI MV」功能,依托深度协同的多智能体系统,用户仅需提交音乐、1-7张参考图及文本指令,即可全自动实现分钟级输出(适配10-300 S主流流媒体时长),通过攻克角色与风格一致性、歌词驱动叙事、帧级音画融合等行业痛点,解决了传统“手工作坊”模式的效率与质量瓶颈,大幅降低创作门槛、压缩成本(刊例价为同行业50%),推动音乐视觉内容叙事权从主流机构向个体创作者转移,定义了AI原生MV的质量基线,重塑音乐产业生产与消费范式。

AI 工具

Google发布专为AI智能体设计的通用商业协议「UCP」及「Gemini CX」

1月12日,Google官宣发布Agentic电商解决方案,包括专为AI智能体设计的通用商业协议「UCP」(Universal Commerce Protocol)及企业端的「Gemini CX」(Gemini Enterprise for Customer Experience)。「UCP」接入Shopify、沃尔玛等伙伴,贯穿商品发现到售后全流程;「Gemini CX」具备复杂推理、多模态交互、执行授权操作能力,可覆盖客户服务全生命周期,已落地麦当劳等企业;国内阿里、1688、京东、抖音也纷纷推出电商相关AI工具与功能。

Anthropic基于Claude Code底层架构推出智能协作工具「Claude Cowork」

1月13日,Anthropic基于Claude Code底层架构推出智能协作工具「Claude Cowork」,核心定位是从“对话助手”转变为能理解任务、制定计划并持续执行的“数字同事”,支持用户授权访问指定本地文件进行分类、信息提取、报告整理等非编码工作,还具备内置虚拟机隔离、浏览器自动化支持等创新体验与安全功能。目前以研究预览版形式面向macOS平台的Claude Max订阅用户开放,后续计划加入跨设备同步、Windows版本及强化安全机制。

夸克AI浏览器上线千问划词「快捷指令」功能,划选即调用告别复制粘贴

1月13日,夸克AI浏览器上线千问划词「快捷指令」功能,用户只需三步(开启划词工具栏、添加自定义指令并命名保存)即可完成设置,浏览网页或文档时划选内容便能一键调用AI指令,无需复制粘贴,该功能提供了学术润色、种草文案撰写、情侣聊天支招、内容创作润色、代码优化、外语翻译、职场黑话解读等多场景指令模板,助力提升各类场景下的使用效率。

5.6K Star开源神器「Voice-Pro」,免费本地实现视频翻译+声音克隆

1月13日消息,GitHub上5.6K Star的开源工具「Voice-Pro」原是韩国创业团队的付费软件,现因新项目开发停止维护并完全开源,它整合WhisperX、F5-TTS等先进语音模型,在Windows等主流PC平台实现“视频下载-人声分离-字幕识别-文本翻译-声音克隆配音-视频合成”一站式本地运行,支持100多种语言处理、零样本语音克隆,无需代码,通过脚本即可轻松安装,免费无字符限制且不上传云端,是ElevenLabs等商业工具的优质替代方案,适配视频创作者和出海玩家需求。

Vercel Labs开源AI Agents浏览器自动化CLI工具「Agent-browser」

1月14日,Vercel Labs发布开源AI Agents浏览器自动化CLI工具「Agent-browser」,发布两天即获3.4k GitHub星,相比传统Playwright MCP可节省93%上下文,其中外层基于Rust编写,通过返回清洗后的可访问性树并为可交互元素打标签(Ref),让AI以简单指令精准操控浏览器,零配置且支持无头/有头模式,兼容多款AI工具,能降低Token消耗、提升AI注意力与稳定性,安装仅需两步命令。

技术突破

清华团队研发的AI药物虚拟筛选平台「DrugCLIP」登上Science

1月9日,清华大学联合团队研发的AI药物虚拟筛选平台「DrugCLIP」相关成果发表于《Science》,其通过语义检索技术实现筛选速度较传统方法提升百万倍,首次完成人类基因组规模虚拟筛选,实验验证对NET、TRIP12等靶点的筛选有效性,构建全球最大蛋白-配体筛选数据库并免费开放,配套服务平台已服务千余名用户,未来将助力抗癌、罕见病等领域新药研发。

逐际动力发布全球首个具身智能体系统「LimX COSA」

1月12日,逐际动力在深圳正式发布具身智能体系统「LimX COSA」,这是面向物理世界原生、深度融合高阶认知与全身运控的Agentic OS,采用自底向上的小脑基础模型、大小脑融合高阶技能层、自主认知决策层三层结构,赋予全尺寸人形机器人Oli高阶认知推理、语义记忆与主动感知、实时感知全身移动操作三大核心能力,实现“能想能动、知行合一”,标志着具身智能从Demo迈向产品落地,推动多领域的广泛应用。

DeepSeek V4核心技术「Engram」曝光:CPU替GPU存参,性能与降本双突破

1月13日,DeepSeek联合北京大学发布新论文,曝光「DeepSeek-V4」核心技术「Engram模块」,该模块基于N-gram改造,通过哈希函数映射与门控机制快速检索静态知识,以CPU内存替代GPU显存存储大规模参数(推理损耗<3%),相关模型在知识、推理、代码、长文本任务上显著优于现有模型,印证V4性能突破,降低超大规模模型部署成本。

在企业级表格应用场景中,排版规整度直接影响文档的专业质感与可读性——无论是财务报表、项目方案还是正式汇报材料,文本在单元格内的分布均匀性往往成为细节加分项。此前,面对“文本两端对齐”这一高频排版需求,开发者常需通过复杂自定义实现,且难以保证与Excel的兼容性。

SpreadJS V19.0 正式推出单元格两端对齐(Justify Alignment) 功能,完美复刻Excel排版逻辑,兼顾美学呈现与实用体验,为纯前端表格应用带来排版升级,让专业文档制作更高效、更精准。

在这里插入图片描述

一、核心功能:双向对齐,文本分布更均匀

两端对齐功能提供水平与垂直两个维度的精准排版能力,适配不同文本展示需求,实现“边界对齐、内部均匀”的视觉效果:

1. 水平两端对齐(Horizontal Justify)

  • 核心逻辑:每行文本的首字符紧贴单元格左边界,末字符对齐右边界,仅最后一行保持左对齐
  • 实现原理:通过智能调整字间距与行间距,让文本在水平方向均匀分布,避免单侧留白过多的问题
  • 适用场景:长文本段落展示(如项目说明、备注信息)、多列数据标签对齐

2. 垂直两端对齐(Vertical Justify)

  • 核心逻辑:文本首行紧贴单元格上边界,末行对齐下边界;若仅含一行文本,则保持顶部对齐
  • 实现原理:通过调整行间距优化垂直方向分布,解决多行文本垂直居中时上下留白不均的痛点
  • 适用场景:高单元格内多行文本书写(如产品描述、规格说明)、复杂表格布局中的文本适配

3. 组合对齐:水平+垂直双向优化

支持同时启用水平与垂直两端对齐,让文本在单元格内实现“上下左右全边界对齐、内部均匀分布”,适用于对排版精度要求极高的正式文档(如财务报表附注、合同条款)。

二、特性亮点:适配多元场景,兼顾兼容性与灵活性

1. 自动换行强制启用,无需手动配置

启用两端对齐时,系统将自动开启“自动换行”功能,文本将根据单元格宽度智能拆分换行,避免因手动设置遗漏导致的排版错乱,降低操作门槛。

2. 无缝适配合并单元格

针对合并后的大尺寸单元格,两端对齐功能可根据合并后的实际宽高自适应调整文本分布,无需额外设置适配规则,完美支持复杂表格布局(如报表标题、分类汇总区域)。

3. 普通文本与富文本全面支持

无论是基础纯文本,还是包含字体样式、颜色、链接的富文本,均可正常使用两端对齐功能。仅需注意:富文本在旋转文本场景下需遵循特殊适配逻辑,确保排版一致性。

4. 智能分词规则,适配多语言场景

针对不同语言文本的排版特性,两端对齐功能内置智能分词策略:

  • 普通文本:按空格分词,多个连续空格仅第一个用于分词,其余保留为文本一部分(例:"This a word" 分词为 ["This", " a", " word"])
  • CJK(中日韩)文本:整体视为一个“词”,但内部空格可作为分割依据(例:"这是Example サンプル예시" 分词为 ["这是", "Example", "サンプル", "예시"])
  • 支持自定义分词逻辑:通过 CultureManager 配置分词规则,满足特殊业务场景需求

三、使用场景:覆盖企业级文档核心需求

  1. 财务报表制作:会计科目说明、报表附注等长文本区域,通过水平两端对齐实现多列文本整齐排列,提升报表专业度
  2. 正式文档导出:需导出为PDF的合同、方案文档,通过双向两端对齐保证与Excel源文件排版一致,避免导出后格式错乱
  3. 复杂表格布局:合并单元格较多的仪表盘、数据看板,通过垂直两端对齐优化文本垂直分布,让界面更规整
  4. 多语言文档处理:支持中英文、中日韩等多语言文本的均匀排版,适配国际化业务场景

在这里插入图片描述

四、注意事项:这些细节让排版更精准

  1. 自动换行强制生效:启用两端对齐后,将忽略手动关闭的“自动换行”设置,优先保证排版效果
  2. 部分功能兼容限制:

    1. 缩小字体填充(shrink to fit):多行文本场景下不生效,两端对齐逻辑优先
    2. 显示省略号(ellipsis):两端对齐功能优先生效,省略号设置将被忽略
    3. 缩进(indent):水平两端对齐时,缩进设置无效,文本将紧贴左右边界
  3. 富文本特殊适配:旋转状态下的富文本需注意排版预览,建议结合实际效果调整单元格尺寸

五、总结:排版升级,效率与专业度双提升

SpreadJS V19.0 两端对齐功能的推出,不仅填补了纯前端表格在专业排版领域的空白,更通过“Excel兼容、智能适配、低操作门槛”的设计,让开发者无需编写复杂自定义代码,即可快速实现高质量排版效果。

无论是企业级报表制作、正式文档导出,还是复杂表格布局设计,这一功能都能有效提升文档质感与可读性,同时降低开发与维护成本。SpreadJS 始终以“复刻Excel体验、赋能前端开发”为核心,持续优化细节功能,让纯前端表格应用更贴合企业实际业务需求。

SpreadJS V19.0 即将正式发布,更多实用特性等待解锁,敬请期待!如需提前体验两端对齐功能,可访问 SpreadJS 官方Demo 或联系技术支持获取试用版本。

聊聊复制过滤的那些隐藏陷阱

适合读者:DBA / 后端架构师 / 运维工程师

关键词:MySQL 复制、binlog_do_db、replicate_do_db、数据不一致

一、背景

在许多 MySQL 体系的数据库环境中,为了降低 binlog / relay log 日志量、缓解从库复制压力或减少同步延迟,往往会引入 主库 binlog 过滤从库复制过滤 的配置方案。 这些手段在一定程度上能够缓解资源消耗,但如果对其工作机制理解不充分,使用了不合理的过滤策略,极易引入隐蔽且不可逆的数据不一致风险。更为危险的是,这类问题在系统运行过程中通常不会立刻暴露,当业务侧发现数据异常时,往往已经无法通过常规手段进行补救。

本文将从 主库与从库两种过滤方式的实现机制入手,分析它们各自的优缺点及潜在风险。

二、复制过滤的判断逻辑

明确主库和从库在处理 SQL 和 row event 时的判断逻辑存在差异。

2.1 主库:是否写 binlog

判断发生在 SQL 执行完成之后。

binlog_do_db / binlog_ignore_db 仅根据当前会话的 USE db 判断,而不关注 SQL 实际操作的目标表。

2.2 从库:是否执行 relay log

判断发生在 SQL Thread 回放阶段

判断依据包括:

  1. replicate_* 复制过滤参数
  2. row event 真实的 db / table
  3. 表是否存在
  4. GTID 执行状态

结论:当主库和从库判断条件不一致时,即使 binlog 已记录,从库也可能未执行对应 row event,从而导致数据不一致。

三、主库过滤参数及风险

3.1 binlog_do_db / binlog_ignore_db 的行为示例

主库参数设置:
binlog_do_db = db1

主库执行SQL:
USE db1;
INSERT INTO db2.t2 VALUES (1);

执行结果:

  1. 主库 binlog 会记录该事务。
  2. 记录的 row event 目标表为 db2.t2,与 USE db1 不一致。

3.2 相关风险

  1. binlog 语义与实际操作对象脱钩
  2. 新从库或延迟从库无法补全缺失数据
  3. binlog 回放、审计等可能出现语义错误

四、从库复制过滤参数及风险

4.1 常用复制过滤参数

从库复制过滤前提条件就是主库的binlog必须完整。

  1. Replicate_Do_DB:
  2. Replicate_Ignore_DB:
  3. Replicate_Do_Table:
  4. Replicate_Ignore_Table:
  5. Replicate_Wild_Do_Table:
  6. Replicate_Wild_Ignore_Table:

4.2 复制或忽略库参数

说明:

Replicate_Do_DB/Replicate_Ignore_DB 这两个参数一个是只同步某些库,另一个是只忽略某些库,判断依据是relay log中记录use的数据库,并不是SQL语句实际操作的库。

测试:

  1. 从库配置复制过滤
STOP SLAVE;
CHANGE REPLICATION FILTER Replicate_Do_DB = test1;
START SLAVE;
  1. 主库不配置过滤并执行操作
USE test;
CREATE TABLE TEST1.T1 LIKE TEST.T1;
INSERT INTO TEST1.T1 VALUES(1,'A');
  1. 验证数据

主库查看数据:

greatsql> SELECT * FROM TEST1.T1;
+----+-------+
| id | cname |
+----+-------+
|  1 | A     |
+----+-------+
1 row in set (0.00 sec)

从库查看数据:

greatsql> SELECT * FROM TEST1.T1;
ERROR 1146 (42S02): Table 'test1.t1' doesn't exist

结论:

从库报错表不存在,所以这样会导致从库同步数据失败,因为use的是test库。

风险:

多库写入(跨库SQL)、存储过程、触发器、应用层不指定USE库都会导致数据不同步的风险。

4.3 复制或忽略表参数

说明:

Replicate_Do_Table/Replicate_Ignore_Table 这两个参数一个是只同步指定表,另一个是只忽略指定表,两个参数都不支持通配符,可以精确到表但使用要确保库名表名正确。

测试:

  1. 从库配置复制过滤
STOP SLAVE;
CHANGE REPLICATION FILTER Replicate_Ignore_Table= (test1.t1_tmp);
START SLAVE;
  1. 主库不配置过滤并执行DDL操作
RENAME TABLE test1.t1 TO test1.t1_bak;
RENAME TABLE test1.t1_tmp TO test1.t1;
  1. 验证数据

主库查看数据:

greatsql> use test1
Database changed
greatsql> show tables;
+-----------------+
| Tables_in_test1 |
+-----------------+
| t1              |
| t1_bak          |
+-----------------+
2 rows in set (0.01 sec)

从库查看数据:

greatsql> USE test1
Database changed
greatsql> SHOW tables;
+-----------------+
| Tables_in_test1 |
+-----------------+
| t1_bak          |
| t1_tmp          |
+-----------------+
2 rows in set (0.01 sec)

结论:

由于主库执行rename操作将t1表更为t1_bak,t1_tmp更为t1,而从库忽略了t1_tmp导致sql同步失败,如果业务往新t1表插入数据从库就会因表不存在而断开复制链路,这是典型的“表级过滤被 DDL 绕过”事故。

风险:

  1. 未匹配的表默认全部不复制
  2. 新增表需要人工维护配置
  3. 与 DDL 操作存在天然冲突
  4. 如果过滤表过多添加在配置文件中只能一个参数匹配一个表

4.4 指定复制或忽略库参数

说明:

Replicate_Wild_Do_Table/Replicate_Wild_Ignore_Table 这两个参数一个是同步指定表,另一个是忽略指定表,两个参数都支持通配符,使用要确保库名表名没有通配符的隐患存在。

匹配方式%_(LIKE 语义)

测试:

  1. 从库配置复制过滤

忽略日志类表,不需要同步到从库。

STOP SLAVE;
CHANGE REPLICATION FILTER Replicate_Wild_Ignore_Table = (test1.log%);
START SLAVE;
  1. 主库不配置过滤并执行DML操作

一年后业务上线新业务test1.log_important

  1. 验证表结构

主库查看数据:

greatsql> USE test1
Database changed
greatsql> SHOW tables;
+-----------------+
| Tables_in_test1 |
+-----------------+
| log_important   |
+-----------------+
1 row in set (0.00 sec)

从库查看数据:

greatsql> USE test1
Database changed
greatsql> SHOW tables;
Empty set (0.00 sec)

结论:

log_importantlog_% 命中新业务数据未同步到从库,主从复制正常但是从库数据丢失,如果主库故障切换到从库才发现数据不一致就会导致故障,这是典型的“通配规则忽略业务表”事故。

风险:

  1. 匹配范围过宽
  2. 新表“自动进入过滤范围”
  3. DDL 影响范围不可控

五、最常见的踩坑配置

主库从库风险是否推荐
binlog_do_dbReplicate_Do_DB/Replicate_Ignore_DB跨库静默丢数据不推荐
binlog_do_dbreplicate_wild_ignore从库失效不推荐
binlog_ignore_db无过滤永久不可补不推荐
无过滤Replicate_Do_DB/Replicate_Ignore_DB跨库静默丢数据不推荐
无过滤Replicate_Do_Table/Replicate_Ignore_Table与DDL操作存在冲突,人工维护成本高可用,前提是过滤表数量少
无过滤Replicate_Wild_Do_Table/Replicate_Wild_Ignore_Table匹配范围过宽,通配符需要转义可用,前提是确保通配符不会影响其他表

六、最终建议(可直接当规范)

  1. 如果可以不做过滤就不做,做了就会有数据风险。
  2. 主库禁止做库表忽略,主库的binlog必须完整。
  3. 从库Replicate_Do_DB/Replicate_Ignore_DB最好不使用,业务操作并非DBA可以控制,但数据不一致就是DBA的锅。
  4. 从库Replicate_Do_Table/Replicate_Ignore_Table看似精确,但对 DDL 极其敏感,一旦表结构或命名发生变化,复制语义就可能在无感知的情况下被破坏。
  5. 从库Replicate_Wild_Do_Table/Replicate_Wild_Ignore_Table可以使用,库表都可做过滤,前提是一定要做转义,规避不应该发生的数据问题。
  6. 有条件可以使用GreatSQL 的gt checksum工具定期做主从数据校验。

大家好,我是良许。

在嵌入式开发中,串口通信是我们最常用的通信方式之一。

但很多初学者经常会被 UART、RS232、RS485 这几个概念搞混,不清楚它们之间到底有什么区别和联系。

今天我就来详细聊聊这三者的区别,帮助大家彻底理解这些概念。

1. 基本概念解析

1.1 UART 是什么

UART(Universal Asynchronous Receiver/Transmitter)的中文名称是通用异步收发器,它本质上是一种通信协议和硬件电路

UART 定义了数据如何在设备之间进行串行传输,包括数据格式、传输速率、起始位、停止位等。

简单来说,UART 是一种逻辑层面的协议标准。

它规定了数据帧的格式,比如一个标准的 UART 数据帧通常包含:1 个起始位(低电平)、5 到 8 个数据位、可选的校验位、1 到 2 个停止位(高电平)。

在我们的 STM32 单片机中,UART 就是芯片内部集成的一个硬件模块,负责将并行数据转换为串行数据发送出去,或者将接收到的串行数据转换为并行数据。

UART 通信只需要两根线:TX(发送)和 RX(接收),再加上一根地线 GND。

1.2 RS232 是什么

RS232 是由美国电子工业协会(EIA)制定的一种物理层标准,全称是 EIA-RS-232。

它定义了数据终端设备(DTE)与数据通信设备(DCE)之间的物理接口标准,包括电气特性、机械特性、功能特性等。

RS232 最重要的特点是它的电平标准:逻辑 1(MARK)的电压范围是-15V 到-3V,逻辑 0(SPACE)的电压范围是 +3V 到 +15V。

注意,这个电平标准和我们单片机的 TTL 电平(0V 和 3.3V 或 5V)是完全不同的。

RS232 通常使用 DB9 或 DB25 接口,最大传输距离约为 15 米,最大传输速率一般不超过 20kbps(理论上可以更高,但实际应用中受限于线缆长度和质量)。

1.3 RS485 是什么

RS485 同样是一种物理层标准,它是 RS232 的改进版本。

RS485 采用差分信号传输方式,使用两根线(A 和 B)来传输数据,通过两根线之间的电压差来表示逻辑 0 和 1。

RS485 的主要优势包括:传输距离可达 1200 米,传输速率可达 10Mbps(短距离下),支持多点通信(最多可以连接 128 个设备),抗干扰能力强。由于采用差分信号,RS485 在工业环境中的应用非常广泛。

2. 三者之间的关系

理解了基本概念后,我们来看看它们之间的关系。

简单来说:

UART 是协议层,RS232 和 RS485 是物理层。

这就好比我们说话时,UART 定义了"说什么"(语言规则),而 RS232 和 RS485 定义了"怎么说"(声音的大小、传播方式)。

一个完整的串口通信系统,既需要 UART 协议来组织数据,也需要 RS232 或 RS485 这样的物理层标准来实际传输数据。

在实际应用中,我们的单片机 UART 输出的是 TTL 电平信号(比如 0V 和 3.3V),如果要通过 RS232 接口通信,就需要使用电平转换芯片(如 MAX232)将 TTL 电平转换为 RS232 电平;如果要通过 RS485 通信,就需要使用 RS485 收发器芯片(如 MAX485)进行转换。

3. 详细对比分析

3.1 电气特性对比

从电气特性来看,三者有明显的区别:

UART(TTL 电平):逻辑 1 通常是 3.3V 或 5V,逻辑 0 是 0V。这是单片机内部直接使用的电平标准,驱动能力弱,抗干扰能力差,只适合板级通信。

RS232:采用负逻辑,逻辑 1 是-3V 到-15V,逻辑 0 是 +3V 到 +15V。这种较大的电压摆幅提供了一定的抗干扰能力,但功耗相对较高。RS232 是单端信号传输,容易受到共模干扰的影响。

RS485:采用差分信号传输,两根线之间的电压差大于 +200mV 表示逻辑 1,小于-200mV 表示逻辑 0。差分传输的最大优势是抗共模干扰能力强,即使两根线同时受到相同的干扰,只要它们之间的电压差保持不变,就不会影响数据传输。

3.2 传输距离和速率对比

在实际应用中,传输距离和速率是我们选择通信方式的重要考虑因素:

UART(TTL 电平):传输距离非常有限,一般不超过 1 米,速率可以很高,但受限于线缆和驱动能力。在 PCB 板上的芯片间通信非常合适。

RS232:标准规定最大传输距离为 15 米,但在实际应用中,如果降低波特率,可以达到更远的距离。比如在 9600bps 的速率下,可以传输 30 米甚至更远。但随着距离增加,信号衰减和干扰会导致通信质量下降。

RS485:这是三者中传输距离最远的,标准距离可达 1200 米。而且 RS485 的传输速率和距离是可以权衡的:短距离下可以达到 10Mbps,而在最大距离 1200 米时,速率通常限制在 100kbps 左右。

3.3 通信方式对比

从通信拓扑结构来看:

UART/RS232:只支持点对点通信,即一个发送端对应一个接收端。如果需要连接多个设备,就需要多个串口,或者使用串口服务器等设备。

RS485:支持多点通信(也叫总线型通信),可以在同一条总线上连接多达 128 个设备(理论值,实际应用中需要考虑负载能力)。这使得 RS485 在工业控制系统中非常受欢迎,可以大大减少布线成本。

另外,RS485 支持半双工和全双工两种模式。半双工模式只需要两根线(A 和 B),但同一时刻只能有一个设备发送数据;全双工模式需要四根线,可以同时收发数据。

4. 实际应用场景

4.1 UART 的典型应用

在嵌入式开发中,UART 最常见的应用场景包括:

  1. 单片机与 PC 之间的调试通信,通过 USB 转 TTL 模块连接。
  2. 单片机与各种传感器模块的通信,比如 GPS 模块、蓝牙模块、WiFi 模块等。
  3. 单片机之间的短距离通信。

下面是一个 STM32 使用 HAL 库进行 UART 通信的简单示例:

// UART初始化
UART_HandleTypeDef huart1;
​
void MX_USART1_UART_Init(void)
{
    huart1.Instance = USART1;
    huart1.Init.BaudRate = 115200;
    huart1.Init.WordLength = UART_WORDLENGTH_8B;
    huart1.Init.StopBits = UART_STOPBITS_1;
    huart1.Init.Parity = UART_PARITY_NONE;
    huart1.Init.Mode = UART_MODE_TX_RX;
    huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
    
    if (HAL_UART_Init(&huart1) != HAL_OK)
    {
        Error_Handler();
    }
}
​
// 发送数据
uint8_t txData[] = "Hello UART!\r\n";
HAL_UART_Transmit(&huart1, txData, sizeof(txData)-1, 1000);
​
// 接收数据
uint8_t rxData[100];
HAL_UART_Receive(&huart1, rxData, 10, 1000);

4.2 RS232 的典型应用

RS232 虽然是比较老的标准,但在很多场合仍然在使用:

  1. 工业设备的配置和调试接口,很多老设备都配备 RS232 接口。
  2. 一些专业设备如示波器、频谱分析仪的通信接口。
  3. PLC(可编程逻辑控制器)的编程和监控接口。

在使用 RS232 时,我们需要在单片机的 UART 和 RS232 接口之间加入电平转换芯片。

以 MAX232 为例,它可以将 TTL 电平转换为 RS232 电平,反之亦然。

电路连接非常简单,只需要几个外围电容即可。

4.3 RS485 的典型应用

RS485 在工业自动化领域应用极为广泛:

  1. 工业现场的传感器网络,比如温度、压力、流量等传感器的数据采集。
  2. 楼宇自动化系统,如门禁、照明、空调控制等。
  3. 智能电网的抄表系统。
  4. 工业机器人的控制系统。

使用 RS485 时,需要注意以下几点:

  1. 总线两端需要加 120 欧姆的终端电阻,以消除信号反射。
  2. 在没有数据传输时,需要将总线拉到确定的电平状态,通常使用上拉和下拉电阻。
  3. 在多主机通信时,需要设计好通信协议,避免总线冲突。

下面是一个使用 MAX485 进行 RS485 通信的示例代码:

// 定义RS485方向控制引脚
#define RS485_DE_GPIO_Port GPIOA
#define RS485_DE_Pin GPIO_PIN_8
​
// 设置为发送模式
void RS485_TX_Mode(void)
{
    HAL_GPIO_WritePin(RS485_DE_GPIO_Port, RS485_DE_Pin, GPIO_PIN_SET);
    HAL_Delay(1); // 等待芯片切换
}
​
// 设置为接收模式
void RS485_RX_Mode(void)
{
    HAL_GPIO_WritePin(RS485_DE_GPIO_Port, RS485_DE_Pin, GPIO_PIN_RESET);
    HAL_Delay(1);
}
​
// 发送数据
void RS485_SendData(uint8_t *data, uint16_t len)
{
    RS485_TX_Mode();
    HAL_UART_Transmit(&huart1, data, len, 1000);
    RS485_RX_Mode();
}
​
// 接收数据
void RS485_ReceiveData(uint8_t *data, uint16_t len)
{
    RS485_RX_Mode();
    HAL_UART_Receive(&huart1, data, len, 1000);
}

5. 如何选择合适的通信方式

在实际项目中,我们应该如何选择呢?可以参考以下原则:

  1. 短距离板级通信:直接使用 UART 的 TTL 电平即可,简单、成本低、速度快。比如单片机与传感器模块之间的通信。
  2. 中等距离点对点通信:如果距离在几米到十几米之间,并且只需要连接两个设备,可以选择 RS232。虽然 RS232 比较老,但它的兼容性很好,很多设备都支持。
  3. 长距离或多设备通信:如果传输距离超过 15 米,或者需要连接多个设备,那么 RS485 是最佳选择。特别是在工业环境中,RS485 的抗干扰能力和多点通信能力使它成为首选。
  4. 高速短距离通信:如果需要高速传输且距离不远,可以考虑使用 LVDS(低压差分信号)等其他技术。
  5. 无线通信需求:如果布线困难或需要移动通信,可以考虑使用蓝牙、WiFi、LoRa 等无线通信方式。

6. 总结

通过以上的详细分析,我们可以清楚地看到 UART、RS232、RS485 之间的区别和联系:

UART 是一种通信协议和硬件模块,定义了数据的组织方式;RS232 和 RS485 则是物理层标准,定义了信号的电气特性和传输方式。

它们不是互相替代的关系,而是协同工作的关系。

在实际应用中,我们通常是在单片机的 UART 基础上,根据具体需求选择合适的物理层标准。

如果是短距离通信,直接使用 UART 的 TTL 电平;如果需要更远的传输距离或更强的抗干扰能力,就通过电平转换芯片将 TTL 电平转换为 RS232 或 RS485 电平。

理解这些概念对于我们进行嵌入式系统设计非常重要,可以帮助我们在不同的应用场景中选择最合适的通信方式,设计出稳定可靠的系统。

希望这篇文章能够帮助大家彻底搞清楚这三者的区别,在以后的项目中能够灵活运用。

JDK 26

连续第二周,JDK 26 的早期访问版本仍为Build 29。更多详情请参阅其发布说明

JDK 27

同样,JDK 27 的早期访问版本当前仍为Build 3。详细信息可查阅其发布说明

 

对于JDK 26JDK 27,鼓励开发者通过Java Bug数据库报告缺陷。

Spring Framework

Spring Shell 4.0.0 正式发布GA版本,包含缺陷修复、文档改进、依赖项升级以及多项新特性,包括,命令编程模型重构,在使用 Spring Boot 时,不再需要@EnableCommand@CommandScan注解,并修复了@Command注解的意外行为;全新升级的 DSL,解决了CommandRegistration.Builder实例与 Spring Security 的SecurityFilterChain接口在新构建器格式下的匹配问题;与 Spring Framework 7.0 和 Spring Boot 4.0 对齐;新增对JSpecify的空安全(null safety)支持。更多细节请参见发布说明

JReleaser

JReleaser 1.22.0发布,这是一个用于简化 Java 项目发布流程的工具,本次更新包括缺陷修复、文档改进、依赖项升级以及新功能,包括,Signing模块全面重构,支持同时使用多种方法对构件(artifacts)进行签名;新增对Minisign(一个用于文件签名和验证的工具)的支持;支持在部署构件到 Maven Central 时跳过等待期。更多详细信息请见发布说明

TornadoVM

TornadoVM团队宣布,其开源 IntelliJ 插件TornadoInsight(旨在提升 TornadoVM 的开发体验)现已兼容最新发布的TornadoVM 2.0。相关配置指南也已同步更新。关于 TornadoInsight 的更多信息,可参考 InfoQ 的新闻报道

Apache Camel

Apache Camel 4.14.3发布,包含缺陷修复、依赖项升级及功能改进,包括,在使用Camel JBang时,可通过--repos命令为Camel Kamelet相关操作指定 Maven 仓库;Camel Neo4j组件改进了消息体的检测逻辑,避免内部错误;修复了Camel Netty中 SSL 客户端证书主题名称(subject name)从可读字符串表述被错误转换为晦涩的LDAP格式的问题。更多详情请查阅发布说明

 

原文链接:

Java News Roundup: Spring Shell, JReleaser, TornadoInsight, Apache Camel

事件背景

2026 年 1 月 16 日,OpenAI 通过官方 X(原 Twitter)账号正式宣布,将在未来数周内开始在 ChatGPT 的免费版和新推出的 ChatGPT Go($8/月)中测试广告投放。与此同时,Plus($20/月)、Pro($200/月)及企业版将继续保持无广告体验。这一决策迅速引发了科技圈的广泛关注和激烈讨论。

OpenAI官方公告推文

OpenAI 官方推文宣布广告计划,并发布广告原则说明 | 来源:X @OpenAI

值得注意的是,OpenAI 同时发布了一份详尽的"广告原则"(Our Ad Principles),试图向用户保证广告不会影响 ChatGPT 的回答质量和隐私保护。然而,这份承诺并未能平息用户的担忧——社交媒体上的反应呈现出高度两极分化的态势。

OpenAI 的广告原则解读

OpenAI广告原则详图

OpenAI 发布的广告原则框架:强调使命对齐、答案独立、对话隐私、用户控制与长期价值

📋 OpenAI 官方承诺清单

答案独立性:ChatGPT 的回答始终基于客观有用性,广告不会影响答案内容

对话隐私:不向广告商出售用户数据,对话内容保持私密

用户控制:用户可随时关闭个性化广告,清除广告相关数据

付费保护:Plus、Pro、Business、Enterprise 等高价层级永不显示广告

未成年保护:18 岁以下用户不会看到广告

敏感话题禁区:政治、健康、心理健康等敏感话题禁止广告投放

商业化背后的财务压力

从华尔街日报、彭博社等主流财经媒体的报道来看,OpenAI 此举并非心血来潮,而是面对真实财务压力的"不得已之举"。据公开数据显示:

在如此悬殊的付费转化率面前,广告变现被多家媒体评价为"不可避免"的选择。富国银行预测,ChatGPT 在搜索市场的占比将从 2025 年底的 17%增长到 2030 年的三分之一,这为广告业务提供了巨大的潜在市场空间。

社交媒体的激烈反应

公告发布后,X 平台上的评论区迅速沦陷。从截图可见,用户反应从嘲讽、愤怒到直接引用 Sam Altman 此前的反广告言论,形成了鲜明的对比和讽刺效果。

用户评论截图

X 平台用户对 OpenAI 广告公告的部分反应 | Grok 引用了 Altman 2024 年称广告是"反乌托邦"的言论

Sam Altman 在 2024 年曾称将广告嵌入 ChatGPT 回复是一种"反乌托邦"的想法:"很容易想象那种未来的反乌托邦场景——你问 ChatGPT 一个问题,它回答说'你应该考虑买这个产品'或者'你应该去这里度假'之类的。"

—— 来源:Grok @grok 引用 Altman 2024 年采访

这种前后矛盾的表态成为用户攻击的焦点。有用户直言:"直接说你们需要更多钱不就得了"(Just say you guys need more money),简洁而犀利地戳破了官方话术的包装。

四大核心担忧

1. 答案中立性与商业影响的矛盾

用户普遍担忧:一旦 AI 提供的建议与商业利益相关联,就很难保证答案仍然是纯粹基于"客观有用性"的判断。有用户形象地比喻:"感觉就像在心理咨询师办公室里竖起了广告牌。"

2. 数据隐私与"监听"恐惧

尽管 OpenAI 承诺"不会出售用户数据给广告商",但用户对此类承诺持谨慎态度。有 Reddit 用户反映,在 ChatGPT 中讨论特定话题后,很快在其他平台看到相关广告,这加深了数据被滥用的担忧。

3. 前科问题:App Recommendations 事件

2025 年 12 月,ChatGPT Plus 付费用户在对话中看到来自 Target、Peloton 等品牌的"推荐"。OpenAI 最初辩称这不是广告,只是应用发现功能,但最终在用户强烈反对下关闭了该功能。首席研究官 Mark Chen 道歉承认公司"做得不够好"。这一事件严重损害了用户对 OpenAI 承诺的信任。

4. Instagram 模式类比的逻辑悖论

CEO Sam Altman 提到欣赏 Instagram 的广告模式,但用户尖锐地指出:Instagram 之所以成功,正是因为 Meta 大规模收集和出售了用户的个人数据——这与 OpenAI 声称的"隐私优先"立场本质矛盾,形成了无法调和的逻辑悖论。

广告形态预览

根据 OpenAI 展示的概念图,广告将以"Sponsored"标签的形式出现在 ChatGPT 回复的底部,与回答内容明确分离。在下图的示例中,当用户询问墨西哥晚宴菜谱时,系统在给出食谱建议后,底部会显示相关食材的赞助商购买链接。

ChatGPT广告界面示例

ChatGPT 广告投放概念设计:广告以"Sponsored"标签形式出现在回复底部,与答案内容分离

这种设计理论上可以降低用户对答案被"污染"的担忧,但批评者指出,长期来看广告逻辑一旦被引入系统,算法污染可能是微妙且难以察觉的——即使不是故意为之。

前科回顾:信任的裂痕

  • 2024 年 Altman 公开反对广告

Sam Altman 在采访中称将广告嵌入 ChatGPT 回复是"反乌托邦"的想法,表示更倾向于订阅模式以避免用户成为产品。

  • 2025 年 12 月 App Recommendations 争议

ChatGPT Plus 付费用户发现对话中出现 Target、Peloton 等品牌推荐。OpenAI 先是否认为广告,后在舆论压力下关闭该功能并道歉。

  • 2026 年 1 月 16 日正式宣布广告测试

OpenAI 官宣在免费版和 Go 版本中测试广告,同时发布"广告原则"框架,承诺付费用户永远不会看到广告。

这一系列事件的累积效应是:用户现在不再轻易相信 OpenAI 关于"高价订阅永远不会有广告"的承诺。Reddit 社区中大量评论指出,这正是流媒体巨头采用过的老套路——"先在免费端试水,再慢慢侵入付费端"。

有条件的宽容声音

💡部分理性用户的接受条件

并非所有反应都是负面的。部分用户认为,如果 OpenAI 能够做到以下几点,免费用户看广告是一种合理的交换:

1. 透明性:广告必须明确标注为"Sponsored",不能伪装成自然回答

2. 相关性:广告应与当前对话相关,而非完全无关的干扰

3. 可控性:用户可以关闭个性化广告设置,或清除用于投放广告的对话记录

4. 底线:高价订阅(Plus/Pro)必须永远保持无广告体验

这类"有条件宽容"的声音提醒我们,用户并非完全不能接受商业化,关键在于执行的边界和信任的维护。

行业视角:竞争压力与战略转向

从更宏观的行业视角来看,OpenAI 的这一决策也反映了 AI 领域日益激烈的商业化竞争。谷歌的 Gemini 和 Meta 的 AI 产品已经内置广告机制,OpenAI 不想在市场份额争夺中落于下风。

Marketing AI Institute 的分析尤其指出,OpenAI 内部正面临巨大的商业化压力。公司聘请前 Facebook 和 Instacart 高管 Fidji Simo 担任应用业务 CEO,这一人事任命本身就暗示了公司的战略方向——从技术研究机构向消费级商业平台的全面转型。

OpenAI 的创新尝试在于"对话语境驱动的广告"(contextual ads triggered by current conversation),理论上这种做法可以降低隐私风险。但实践中,用户很难确信系统不会进行隐蔽的数据关联。

结论:信任与商业化的钢丝行走

社交媒体反应以担忧和怀疑为主,核心议题围绕信任、隐私和"前科"问题。用户普遍采取了"show me"的态度——可以测试,但任何迹象表明承诺被破坏就会转向竞品。

主流媒体的评价则务实与批判并存:认可这一决策的商业必然性,但广泛质疑其能否在不伤害信任的前提下成功。最尖锐的评论来自社区用户的讽刺——AGI 实际上是"Ads Generating Income"(广告创造收入)。这反映了一个更深层的焦虑:开放人工智能的使命(AGI 造福全人类)与商业化压力之间可能存在根本性冲突。

OpenAI 正在走钢丝——既要维持无广告体验的付费用户的付费意愿,又要通过免费/低价层的广告收入覆盖高昂的运营成本。这个平衡能维持多久,将决定 ChatGPT 是否会重蹈社交媒体平台从纯净到被商业完全入侵的老路。

ChatGPT 将上线广告

1 月 16 日,OpenAI 宣布计划在未来几周内,开始在 ChatGPT 中测试广告功能。首批测试将针对美国市场的用户展开,随后逐步推广至全球。广告将主要出现在免费版以及每月订阅费用为 8 美元的低价方案 Go(在印度测试数月后推广至更多市场)中,而 Plus、Pro 及企业版的高级订阅用户目前将不会看到广告。

OpenAI 承诺,广告不会干预或改变 ChatGPT 生成的答案内容。广告将以带有清晰标识的独立方框形式,出现在聊天机器人回复内容的下方。例如,当用户询问纽约的旅行建议时,ChatGPT 会先提供标准的行程规划,随后可能在下方展示当地酒店的广告。OpenAI 应用负责人表示,还将探索更具交互性的广告形式,例如未来用户可以直接向广告内容提问以辅助购买决策。

针对用户普遍关心的隐私问题,OpenAI 明确表示不会将用户数据出售给广告商,也不会向广告商泄露具体的对话内容。广告商仅能获得如展示次数、点击量等汇总后的效果数据。虽然系统会根据对话主题和部分个性化数据来匹配广告,但用户可以随时在设置中关闭用于广告投放的数据权限。此外,OpenAI 设定了严格的屏蔽机制,涉及健康、政治等敏感话题的对话,以及未成年用户的对话中,将不会展示广告。

OpenAI 面临着巨大的商业化压力。尽管 ChatGPT 目前已拥有超过 8 亿的周活跃用户,但其中绝大多数为不产生直接收益的免费用户。作为一家成立十年、累计融资约 640 亿美元的科技巨头,OpenAI 亟需在现有的订阅模式之外开辟新的收入来源,以支撑高昂的模型训练与运营成本,并应对来自 Google Gemini 等竞争对手日益激烈的挑战。


医保药品比价小程序全国上线

1 月 16 日,国家医保局宣布,已在全国完成「定点药店医保药品比价」小程序上线工作。该小程序整合了全国医保定点零售药店的实时数据资源,能够提供及时更新的药品价格、库存状态及生产企业等信息。

据介绍,参保人可通过微信、支付宝搜索进入本地医保服务平台,或直接使用国家医保服务平台 app,找到比价功能模块。输入药品名称后,系统便会显示所在区域内各定点药店的该药品价格区间、具体库存情况,并支持按照价格从低到高、距离由近及远等多种方式进行排序筛选。选定意向药店后,用户还可直接使用内置的导航功能前往,或一键拨打药店电话进行咨询,从而减少盲目奔波与不必要的支出。

在基础比价功能全面落地的基础上,各地医保部门结合群众在使用过程中的反馈,正持续拓展小程序的服务场景与应用功能。例如,针对老年群体可能存在的打字输入困难,多个省份的小程序已新增「拍照识药」功能,只需对准药盒拍照即可自动识别药品并查询比价信息;部分区域还对接了线上购药与配送上门服务,为行动不便或有紧急需求的居民提供便利。

此外,一些地区进一步探索了医用耗材价格查询、处方匹配寻药、药品价格波动趋势分析等特色服务,并同步展示药店基于医保药品「量价比较」的指数评级,辅助群众更全面地做出购药选择。


新规禁止在直播间销售 13 类食品

据新华社报道,市场监管总局于近日发布《直播电商经营者落实食品安全主体责任监督管理规定》,将于 2026 年 3 月 20 日起施行,其中明确了 13 类不得直播经营的食品,包括:用非食品原料生产、添加有毒有害物质的食品;致病性微生物、重金属超标食品;过期、腐败变质、霉变生虫食品;病死毒死或检疫不合格的畜禽水产肉类及其制品;无标签预包装食品;国家明令禁止生产经营的食品等。

《规定》将直播电商平台经营者、直播间运营者、直播营销人员、直播营销人员服务机构全部纳入监管范畴。这些主体都必须按照《规定》要求,履行相应的食品安全主体责任。其中,食品生产经营者开设直播间需要公示许可信息、查验供货资质,非食品生产经营者需要建立严格的选品制度;直播营销人员要加强选品把关;平台要建立审查登记、培训、风险管控等制度措施,配备食品安全管理人员,制定食品安全风险管控清单,建立「智能监测、排查调度、快速处置」的工作机制。

在强化监管措施、完善消费者权益保护方面,《规定》要求市场监管部门将直播经营食品纳入到日常监管和年度抽检计划中,明确技术监测记录可以作为电子数据证据;平台必须开通便捷的投诉举报功能,及时处置消费者诉求。


Setapp 放弃在欧洲开设第三方 iOS 应用商店

近日,乌克兰知名软件开发商 MacPaw 宣布,将于 2 月 16 日正式关闭其面向欧盟用户的第三方 iOS 应用商店 Setapp Mobile。MacPaw 在官方支持页面中解释称,做出这一决定是因为应用市场「仍在不断演变且复杂的商业条款」已不再契合公司当前的商业模式,暗示盈利困难。

Setapp Mobile 于 2024 年 9 月在欧盟地区启动公开测试,通过订阅制为用户提供 iOS 应用分发服务。服务正式关停后,用户通过该平台获取的所有应用将被移除。官方建议用户在截止日期前备份重要数据,以免服务终止后无法访问。Setapp 基于订阅制的 Mac 版将保持正常运营,不受此次移动端业务调整的影响。

Setapp Mobile 的诞生得益于欧盟《数字市场法案》(DMA)的生效,该法案强制 Apple 在欧盟地区开放第三方应用侧载。然而,这一新兴的分发渠道面临着严峻挑战。尤其是 Apple 引入的「核心技术费」(Core Technology Fee)规则,要求应用在超过一定安装阈值后,必须为每次年度首次安装向 Apple 支付费用。这极大地增加了第三方商店及其开发者的运营成本。

目前,欧盟市场仍有包括 Epic Games Store 在内的另外五家第三方商店在运营。Epic 曾多次抨击 Apple 的收费政策阻碍了竞争对手立足,但目前仍坚持运营并等待欧盟监管机构对 Apple 规则的进一步审查。


英伟达博客笔误写错单位,勘误后引起铜价波动

据财新报道,近日,英伟达于 2025 年 5 月发布的一篇博客文章重获市场关注。该文中,英伟达称 1 兆瓦(MW)的机架需要 200 公斤铜母线,1 吉瓦(GW)数据中心的机架母线需要 50 万吨(half a million tons)铜。

由于 1 吉瓦相当于 1000 兆瓦,按比例换算,1 吉瓦数据中心需要的铜应为 200 公斤的 1000 倍,即 20 万公斤。因此,英伟达原文所谓的「50 万吨」应为笔误。英伟达后来修订了该错误。

然而,这一数据已经被许多市场调研报告引用,而英伟达的勘误直接使国际铜价短线大跌:伦铜(LME)在 1 月 14 日创造每吨 13407 美元的历史新高后,连续两日回调,累计回撤约3.4%,暂时跌破了 10 天移动平均线(MA10)。

事实上,受美国关税套利、铜矿供应受限、AI 等新需求因素影响,2025 下半年铜价屡创新高,伦铜从 2025 年 9 月初的 9900 美元/吨,一路上涨,于 2026 年 1 月历史首次突破每吨 1.3 万美元。花旗预计铜价将在未来三个月升至每吨 1.4 万美元。


2025 华为手机出货量 5 年来重回中国第一

据日经新闻援引 IDC 发布的数据报道,从 2025 年中国手机出货量来看,华为时隔 5 年后再次重回首位。从绝对数字上看,华为 2025 年的手机出货量比 2024 年减少 1.9%,为 4670 万部。由于 2024 年位居首位的 vivo 大幅下降 6.6%,华为逆转升至首位。

此前,华为采购高性能半导体受限,无法实现 5G,导致消费者远离。而近年,华为借助麒麟芯片,重振了销售。2025 年 11 月发售最新机型 Mate80 提高了性能,具备 AI 自动处理各种工作的功能,同时相比前代产品降价。

出货量排在第 2 位的是美国苹果,增长 4%,达到 4620 万部。2025 年 9 月上市的 iPhone17 系列销售强劲。2025 年底苹果通过官方销售渠道对高端机型 Pro 和 Pro Max 提供 300 元优惠刺激了需求。

2025 年,中国的整体手机出货量减少 0.6%,为 2 亿 8460 万部,2 年来首次低于上年。虽然鼓励以旧换新的政府补贴起到了推动作用,但有些地区提前用完补贴额度,显得后继乏力。鉴于消费低迷,IDC 预测 2026 年的出货量为 2 亿 7800 万部,持续低于上年。


看看就行的小道消息

  • 1 月 16 日下午,西贝莜面村创始人贾国龙在个人微博宣布,将在晚上 10 点,就罗永浩对西贝的重大污蔑诽谤一一全面回应,并邀请媒体、网友和政府有关部门关注。罗永浩随即转发该微博,并表示自己会尽量忍耐。当日晚间 10 点,贾国龙的个人微博并未更新内容。目前,罗永浩、贾国龙两人的账号被禁言。贾国龙所说的回应最终在西贝集团的认证账号(@西贝人心声)上以图文形式发布。贾国龙称,罗永浩曾在发布的文章中暗示贾国龙联合相关部门对其实施了「跨省抓捕」,是恶意煽动公共情绪,要求罗永浩,必须将此事解释清楚,一起去政府部门查明有没有报警或要求抓捕。随后上述微博被删除。微博 CEO 王高飞(@来去之间)发微博援引网信办《网络名人账号行为负面清单》的规定,表示「以后想论战,应该还是需要通过媒体采访的方式来进行」。罗永浩后来承认被禁言 15 天,表示以后不会再评论西贝事件。
  • 1 月 15 日,中国内地的 App Store 已经无法查询到近期因名称特别而受关注的独居安全 app「死了么」;其他地区的 App Store 仍然可以下载。此前,1 月 13 日,「死了么」在官方微博宣布将于即将发布的新版本中,正式启用全球化品牌名 Demumu;1 月 14 日,其官方账号表示,之前的更名尝试未能尽如人意,在全网征集创意。


少数派的近期动态

如果你从浏览器访问少数派,可能已经注意到我们于近日优化了首页样式。

如你所见,我们此次主要优化了——

  1. 整体设计风格,使其更加现代、统一;
  2. 顶部横幅(banner)区域的版式,新增作者信息、活动信息等多项展示功能;及
  3. 内容流区域的布局,提供更灵活多变的内容展示栏位。

通过此次更新,我们希望以更清晰的层级关系,让不同类型的信息在同一视野中有序呈现,同时反映少数派立足内容而多元发展的面向。我们还将在此次更新建立的基础上,持续改进并增加更多功能,敬请期待。

我们鼓励各位读者积极探索、尝试新的版式、组件和交互功能,并提出问题和建议。如你有任何反馈,可以通过下方表单告知我们。

首页反馈收集


你可能错过的好文章


    疯狂投入了十几个周末进行优化,在一个 issue 里和用户共讨论了 150 多楼,每周末都开发出一个新的 RC 版本,从 RC1 干到 RC11 ,终于发布了一个稳定版。

    作为一个新项目,知道的人还不多,现在才 100 多的 star ,却比同类老项目( mosh: 13k 多的 star )的功能强大很多,详见 README 里的功能对比。支持 多平台低延迟保持连接切换网络SSH X11 转发SSH Agent 转发SSH 端口转发输出上下滚动OSC52 复制粘贴多级 ProxyJumptmux -CC 集成等。

    欢迎大家来试用,与同类老项目 mosh 比较比较。在服务器安装 tsshd,在本地安装 tssh,然后使用下面的命令登录服务器即可:

    tssh --udp <服务器 IP 或别名>
    

    注意 tsshd 默认随机监听 61001 - 61999 的 UDP 端口,防火墙要放开相应策略。支持在命令行指定 UDP 端口和 tsshd 安装路径,如下:

    tssh --udp --tsshd-path /path/on/server/tsshd --tsshd-port 10000-10010 xxx
    ( 和 ssh xxx 一样的效果,配置可写在 ~/.ssh/config 中,然后直接 tssh xxx 登录 )
    
    • 使用 tsshd ,你的 ssh 可以避免掉线(从此告别 ssh 在弱网环境下经常掉线的苦恼,无需经常重新登录)
    • 使用 tsshd ,你的 ssh 可以支持漫游(举例:你的笔记本在公司 ssh 登录后,带回到家还可以保持连接)
    • 使用 tsshd ,你的 ssh 可以获得低延迟(特别适用于海外服务器,避免 ssh 按键不跟手的情况)。tsshd 支持 KCP 协议(可以获得更稳定的更低的延迟),命令行通过 tssh --kcp xxx 启用,也支持配置指定 KCP 协议(默认使用传输吞吐量更高一些的 QUIC 协议)。

    作为 ssh 工具,tssh 和 tsshd 都很注重安全的,详见 README 里的安全介绍。欢迎各位大佬来分析评估其安全性。(说到安全,我周末基本都献给开源 了,CTF 界少了个 PWN 选手,开源界多了几个项目: trzsztsshtsshd 等)

    服务端开源(对标 mosh ,并提供相应 Client 库):tsshd https://github.com/trzsz/tsshd

    客户端开源(支持标准 ssh ,扩展了很多实用功能):tssh https://github.com/trzsz/trzsz-ssh

    本月初,腾讯法务向超过 30 个 GitHub 项目发出了 DMCA 通知,导致这些项目被迫下架。腾讯法务指控开发者们违反了 DMCA 绕过技术保护措施的条款、违反了微信禁止逆向工程条款、威胁用户隐私和安全,以及侵犯知识产权。

    TG 上的评论:让用户访问自己的微信数据是非法的

    https://www.solidot.org/story?sid=83334
    https://github.com/github/dmca/blob/master/2026/01/2026-01-08-tencent.md
    https://github.com/ellermister/wechat-clean

    前贴: https://www.v2ex.com/t/1186284

    看到贴文内容之后,我针对各个关键节点版本做了部署测试,得出如下结论:

    从 3.77.0 版本开始,组件数上限 10w ,日请求上限 20w 。从 3.87.0 版本开始,组件数上限降为 4w ,日请求数降为 10w 。

    版本详细介绍,以及上限验证结果,详见博文: https://wiki.eryajf.net/pages/b6b711/

    除了记录了对应版本,我还针对历史镜像做了归档,有需要的同学可以自取。

    如果你跟我一样,同时在用 Claude Code, Cursor, Windsurf, Copilot 等好几个工具,肯定被这件事折磨过:

    每改一次 Custom Instruction 或 Rule ,都要去四五个地方手动同步一遍。 只要漏掉一个,AI 的表现就打折扣。

    我撸了个小工具 AI Global ,核心逻辑只有一句话:一个中心化配置,全平台指令同步。

    🚀 核心功能:

    一键分发:支持自动识别并同步 30+ 种 AI 工具(几乎涵盖目前市面所有主流工具)。

    共享大脑:所有工具的指令自动合并到 global.md ,改一处,处处生效。

    模块化资源:你可以沉淀一套自己的 .md 技能库、规则库,所有 AI 助手瞬间共享你的“武器库”。

    极简且美观:全 256 色系着色的 CLI 输出,状态一目了然。

    安全第一:自带备份逻辑,支持一键 unlink 无痕还原。

    🛠️ 安装使用:

    npm install -g ai-global

    然后只需运行

    ai-global

    它会自动帮你把家里的活儿全干了。

    GitHub:

    https://github.com/nanxiaobei/ai-global

    欢迎大家体验,有喜欢的工具没适配的,欢迎提 Issue 或 PR !

    题主人在加拿大,混迹已有 5 年。我刚来的时候,对“免费医疗”这四个字是有滤镜的。心想:发达国家嘛,医疗一定又好又人性。后来真正用上了,才发现这事儿得慢慢听我说。

    先说个大前提:在加拿大,不管你看啥病,第一步几乎永远是“预约”。家庭医生要预约,专科要转诊再预约,时间单位通常是“周”起步。真要是紧急情况?那只有一个地方——急诊。

    听起来很清晰,对吧?但现实往往很骨感。

    我儿子有一次出水痘,症状挺明显的,我们也不敢拖,直接去了急诊。结果你猜怎么着?等了 11 个小时。那种感觉你应该能想象:孩子难受,大人焦虑,夜里灯光惨白,塑料椅子坐到怀疑人生。好不容易轮到医生了,医生看了两眼,说: “不是水痘。” 然后就让我们回去了。

    后来事实证明真的是水痘。就这么一句“不是”,硬生生耽误了病情。那一刻,我对加拿大医疗的第一层幻想,算是碎了。

    但话也不能只说一半。加拿大的医疗,也有让我挺感动的时候。

    有一次我太太在申请医保卡的过程中,医保还没正式生效(超过了官方说的处理时间但依然没处理完)。偏偏那天做饭切到了手,口子不小,只能去急诊。我们心里其实是有点慌的,因为没有医保,急诊费用至少一千多加币起。果不其然,医院也明确说了:先记账,账单随后会寄给你们。

    第二天我抱着试试看的心态,给医保部门打了个电话,把情况说明了一下。没想到对方效率极高,直接把我的申请加急处理,两个工作日后就确认生效,随后那次急诊费用全免。

    那一刻你会觉得:
    这个系统慢是慢,但它有它的规则感和人情味——只要你真的符合条件,它不会为难你。

    再说回整体感受。发达国家的“免费医疗”,本质上是很容易被挤兑的。大家都不用掏钱,结果就是:能拖就拖,能等就等,系统的优先级极其严格。你不“濒死”,在系统里就不算急。听说有个在加拿大得了肾结石的,排队排了几天看不上病,马上买票回国把石头打掉了。

    反过头看国内。

    国内的问题大家也都知道:人多、医院挤、医生累。但有个特别现实的优势——病人多,医生见得多,经验是真的被“堆”出来的。很多病,国内医生一眼就知道怎么回事。而且说句大实话:看病是真的方便。不舒服?挂号。想做检查?当天安排。想找专家?多花点钱,但路径清晰。

    这点在我自己身上体现得特别明显。

    我有一次痔疮发作,是真的惨。疼得坐立不安,还发烧,马应龙都已经压不住了。放在国内,基本就是一句话:“来,检查一下,该处理处理。”

    但在加拿大不一样。医生很冷静地告诉我:“不算紧急。”于是——不处理。

    给我开了个带抗生素的痔疮栓,说观察。就这么过去了。

    那一刻我内心其实已经做了决定:等哪天回国,彻底处理掉。

    加拿大医生的逻辑是:标准化、风险控制、避免过度医疗;
    国内医生的逻辑是:经验驱动、效率优先、先把你治舒服了再说。

    你说哪种好?真不好一刀切。

    说到底,这两套系统就像两种性格的人:

    • 加拿大医疗:
      慢、冷静、规则第一,但兜底能力强,不会让你破产。
    • 中国医疗:
      快、直接、经验值爆表,花钱,但解决问题。

    至于我这个痔疮患者,目前的状态是:人在加拿大,心在国内肛肠科。

    医疗这事儿,真不是“免费”和“不免费”那么简单,只有真正用过,才知道自己更适合哪一套。

    对了,加拿大的免费医疗是:看病免费,药不免费,全免是我以前的误解。

    GistLedger

    GistLedger 是一个基于 GitHub Gist 的极简个人记账应用。它利用 GitHub Gist 作为免费、私有的云端数据库,实现数据的安全存储与多端同步。

    体验地址: https://gist-ledger.knowsky404.com

    🌐 核心理念: Own your data (数据隐私) | Serverless (无后端) | Lightweight (轻量化)

    Deploy with Vercel

    📸 项目预览

    Transaction Form
    History View
    Statistics View

    ✨ 功能特性

    1. 📝 极简记账 (Journal)

    • 快速录入: 支持收入/支出切换,金额、分类、日期、备注一键录入。
    • 最近记录: 首页实时展示最近 5 笔交易,方便快速核对。
    • 完全私有: 数据仅存储在你的 GitHub Gist 中,无第三方服务器读取。

    2. 📊 统计报表 (Statistics)

    • 双重视图:
      • 月度视图: 聚焦本月收支,展示当年 12 个月的收支变化趋势,辅助判断本月消费水位。
      • 年度视图: 聚焦全年收支,展示近 5 年的长期收支变化趋势,掌握宏观财务健康状况。
    • 多维筛选: 支持按分类(可多选)筛选统计数据,例如查看“餐饮”+“交通”的年度支出趋势。
    • 动态图表: 交互式图表实时响应筛选和日期切换。

    3. 🔍 查询管理 (Query)

    • 多维筛选: 支持按类型(收入/支出)、日期范围、关键词(分类/备注)进行组合查询。
    • 数据管理: 支持对历史记录进行修改删除
    • 客户端分页: 即使数据量大也能流畅分页浏览。

    🛠 技术栈

    🚀 快速开始

    前置准备

    1. 拥有一个 GitHub 账号。
    2. 生成一个 GitHub Personal Access Token (Classic)
      • Scope 权限: 必须勾选 gist 权限。

    本地运行

    # 1. 克隆项目
    git clone https://github.com/KnowSky404/gist-ledger.git
    cd gist-ledger
    
    # 2. 安装依赖 (推荐使用 pnpm)
    pnpm install
    
    # 3. 启动开发服务器
    pnpm dev
    

    使用说明

    1. 打开应用后,在登录页输入你的 GitHub Personal Access Token
    2. 点击 **"连接数据库"**。
      • 如果是首次使用,应用会自动在你的 Gist 中创建一个名为 GistLedger-Data 的私有 Gist 和 ledger_data.json 文件。
      • 如果已有数据,会自动同步拉取。
    3. 开始记账!你的 Token 和 Gist ID 会保存在本地浏览器缓存中,下次访问无需重复输入(除非清除缓存或点击退出)。

    🔒 数据安全

    • 应用不会将你的 Token 发送给除 GitHub API 以外的任何服务器。
    • 数据存储在你的私有 Gist 中,只有拥有该 Token 的人才能访问。
    • 建议定期备份 Gist 数据或使用 GitHub 的版本历史功能回滚误操作。

    📄 License

    GNU General Public License v3.0 (GPL-3.0)