纯情 发布的文章

BCS 逆向货币 N 系统

中文说明

1. 项目目的

现在很多人面临失业、年龄歧视和就业机会收缩,尤其是 35 岁以后很难再找到稳定工作。有人说“不雇佣 35 岁以上员工的公司,就不要买它们的产品”,但现实中很难执行,因为普通消费者无法把每次购买和就业责任稳定连接起来。本项目希望用 N 货币把这种关系抽象出来:你不是直接和某家公司签一份就业承诺,而是在货币规则中加入“被需要”的结算维度,让消费、销售、就业和 N 流动形成可验证的经济反馈。

这个项目不是为了让人一夜暴富,也不是为了给普通人增加负担。它只是使用了区块链和数字货币的技术形式,但和挖矿、炒币、算力竞争没有关系。它的目标是让货币重新成为更平衡的社会资源分配工具,用逆向货币 N 调节现行单向 D 货币长期积累下来的失衡,减少人被市场经济淘汰的风险,并为避免经济危机提供一种新的制度工具。

当前项目还只是一个很初级的开始。它不是最终答案,而是一个可以运行、可以讨论、可以改进的原型。就像早期比特币只是一个很小的实验,后来改变了很多人对货币和网络协作的理解一样,BCS 和 N 货币也需要更多人加入,一起把这个想法从代码、规则、治理、应用和社会理解上逐步完善。


2. 思想来源

本项目的思想来源我的 Bidirectional Currency System ,也就是“双向货币系统”或“逆向货币系统”的基本思想。

在人类早期的物物交换中,一次交换通常同时满足两个方向。你需要别人的东西,说明你的需求被满足;别人愿意接受你的东西,说明你也被别人需要。也就是说,需求和被需求在同一次交换中同时出现。虽然物物交换效率很低,需要双方刚好互相需要对方的东西,但它保留了一种直接的互惠关系。

后来货币出现以后,交换效率大幅提高。货币解决了物物交换中“双重巧合”的问题,让人们可以先卖出自己的劳动或商品,得到货币,再用货币购买别人的商品。货币让分工、储蓄、价格、市场和长期契约成为可能,这是人类社会的重要发明。

但货币也带来了一个结构性变化:购买时,买方的需求被满足了,但买方自身是否被别人需要,并不会在同一笔交易中自动得到确认。现代社会中,一个人是否“被需要”,主要通过就业、工资、订单、职位和收入来体现。只要能找到工作、能卖出劳动或产品,这个问题不明显;但如果工业化、自动化、平台化和资本集中不断发展,越来越多的人可能消费能力不足、就业机会不足、议价能力下降,那么“被需要”这一侧就会逐渐丢失。

现行单向 D 货币的核心问题不在于它没有价值,而在于它只很好地表达了“需求”和“购买力”,却没有在货币结算中直接表达“被需要”。在工业化早期,商品不够多,劳动力需求旺盛,这个缺点不明显。随着生产能力越来越强,商品越来越多,自动化越来越强,单向货币系统会越来越容易出现一个矛盾:社会有能力生产很多东西,但很多人因为没有工作或收入不足,无法参与消费;企业为了利润继续降低用工,进一步削弱社会总需求。

BCS 的出发点就是补上这个缺失的方向。D 仍然代表现实货币、价格和支付; N 则代表“被需要”的结算资产。销售和工资不再只是 D 的单向流动,而是同时触发 N 的反向流动。这样,市场不是被取消,而是被增加了一个新的反馈维度。


3. 这个项目是什么,不是什么

这个项目是一个逆向货币 N 的技术原型。它使用区块链、UTXO 、身份认证、治理、多节点同步、离线交易和可选隐私证明等技术,来验证 BCS 货币规则是否可以被工程化实现。

它不是普通公链项目。它不追求开放挖矿,不依赖 PoW 算力竞争,不鼓励投机炒作,也不把“币价上涨”作为主要目标。它的重点不是制造一个新的投机资产,而是建立一个可以表达“需求”和“被需求”双向关系的结算系统。

它不是直接替代现实货币。当前阶段,现实货币、银行、现金、支付网关、发票和工资单仍然按原来的方式存在。项目链上主要处理 N 货币。D 不强制上链,不强制接入银行或支付接口,而是先用 external_amount 表示外部现实金额,作为计算 N 流动的依据。外部支付凭证可以作为可选引用保存,后续如果发展需要,可以再接入银行、支付网关、发票系统、工资系统、oracle 或链上 D 资产。

它也不是一个保证任何人马上获得工作的系统。N 货币不能凭空创造岗位,也不能替代现实企业经营。它要做的是让“谁创造就业、谁提供被需要机会、谁消耗社会需求”这些关系进入可计算、可审计、可治理的货币流动中,形成长期调节力量。


4. 核心概念

4.1 D:需求货币

D 可以理解为现实中的普通货币或普通支付金额。它可以来自现金、银行转账、银行卡、微信、支付宝、Stripe 、发票、工资单或其他现实支付系统。

在当前项目里,D 不作为链上资产强制发行。系统不托管用户的现实资金,不处理法币充值提现,不做银行清算,不强制接入支付网关。链上只需要知道一个外部金额 external_amount,用它计算对应的 N 流动。

4.2 N:被需要货币

N 是本项目真正处理的链上货币。它表达的是经济关系中的“被需要”维度。N 可以被发放、转移、销毁、补充和审计。

在销售中,商家获得现实支付金额后,需要向买家回馈一定比例的 N 。这个规则表达:商家从消费者需求中获得收入,也要释放一部分“被需要”能力给消费者。

在工资中,雇主支付现实工资后,工人需要向雇主转移一定比例的 N 。这个规则表达:雇主提供了工作机会,帮助工人获得现实收入,因此雇主获得一部分 N ,用来支撑它未来的销售能力。

4.3 phi 和 psi

phi 是销售规则参数。它决定销售外部金额需要对应多少 N 回馈给买家。

N_to_buyer >= ceil(external_amount * phi)

psi 是工资规则参数。它决定工资外部金额需要对应多少 N 转移给雇主。

N_to_employer >= ceil(external_amount * psi)

这两个参数不是随便写死的。它们应该由系统治理决定,并根据试点效果、就业情况、N 流动情况、商户压力、用户接受度和经济稳定目标逐步调整。

4.4 身份认证

系统需要知道谁是用户、谁是商户、谁是雇主、谁是治理者。当前方案使用 DID 和 VC 。用户先生成 DID ,由信任锚或治理认可机构签发 VC ,再提交链上注册。身份通过后,用户才能参与某些关键流程,例如接收初始 N 、参与治理或进行高权限交易。

4.5 治理

前期系统由创始人和合伙人共同表决治理。这样做是为了快速试错、避免早期规则被恶意利用,也方便修复系统问题。随着网络逐步成熟,治理权应逐步移交给整个系统,让用户、节点、商户、雇主和其他参与者通过规则参与表决。

治理不只是投票。治理要决定参数、信任锚、身份认证策略、N 发放规则、补充规则、验证者集合、升级计划和风险处置。


5. 项目整体运行流程图

flowchart TD
    A["用户/商户/雇主创建钱包"] --> B["生成地址和 DID"]
    B --> C["提交 DID 文档和 VC 凭证"]
    C --> D{"身份是否通过认证"}
    D -- "否" --> E["等待复核或补充材料"]
    D -- "是" --> F["进入系统身份注册表"]

    F --> G["治理或发行模块发放初始 N"]
    G --> H["用户获得 N UTXO"]

    H --> I{"发生现实经济活动"}
    I -- "普通 N 转账" --> J["构造 TRANSFER 交易"]
    I -- "销售" --> K["买方用现实支付方式付款"]
    I -- "工资" --> L["雇主用现实支付方式发薪"]

    K --> M["销售交易写入 external_amount"]
    M --> N["可选写入银行/现金/支付网关/发票引用"]
    N --> O["计算最低 N: ceil(external_amount * phi)"]
    O --> P["卖方向买方输出 N"]

    L --> Q["工资交易写入 external_amount"]
    Q --> R["可选写入工资单/银行/支付凭证引用"]
    R --> S["计算最低 N: ceil(external_amount * psi)"]
    S --> T["工人向雇主输出 N"]

    J --> U["签名交易"]
    P --> U
    T --> U

    U --> V{"是否在线"}
    V -- "在线" --> W["提交节点 API"]
    V -- "离线" --> X["本地缓存离线交易"]
    X --> Y["恢复联网后同步"]
    Y --> W

    W --> Z["节点验证 UTXO/签名/身份/参数"]
    Z --> AA{"是否满足 N 规则"}
    AA -- "否" --> AB["拒绝交易并返回原因"]
    AA -- "是" --> AC["进入 mempool"]
    AC --> AD["PoA/PoA-BFT 验证者出块"]
    AD --> AE["区块确认并更新 UTXO"]

    AE --> AF["N 流动改变销售能力和就业反馈"]
    AF --> AG["治理观察数据并调整参数"]
    AG --> H


6. 流程文字说明

6.1 身份进入系统

用户第一次进入系统时,不是先去挖矿,也不是先购买投机资产,而是创建钱包、生成密钥和 DID 。DID 是用户在系统中的去中心化身份标识。随后,用户需要通过信任锚或治理认可机构获得 VC 凭证,证明自己是合法参与者。

节点收到身份注册请求后,会验证 DID 控制权、VC 签名、issuer 是否可信、凭证是否过期,以及注册请求是否符合治理规则。通过后,身份进入系统注册表。身份状态会影响后续 N 发放、交易权限和治理资格。

6.2 初始 N 发放

N 不是通过挖矿获得。前期可以由治理或发行模块根据身份认证结果进行初始发放。发放规则需要透明,例如每个通过认证的用户获得一定初始 N ,或者根据试点规则给商户、工人、雇主分配不同额度。

这一步的核心是公平和可审计。系统要记录谁获得了 N 、在什么高度获得、数量是多少、由哪些治理签名确认、是否有发放上限。这样可以避免 N 一开始就被少数人随意控制。

6.3 普通 N 转账

普通 N 转账类似普通数字货币转账。用户选择 UTXO ,填写收款地址和金额,签名后提交节点。节点验证输入是否存在、是否未花费、签名是否正确、输出金额是否合理,然后把交易放入 mempool ,等待出块确认。

普通 N 转账不涉及外部支付金额,也不涉及 phipsi

6.4 销售交易

销售交易是系统最重要的流程之一。现实中,买方可以使用现金、银行、微信、支付宝、支付网关或其他方式向商户付款。链上不强制处理这笔现实支付,也不强制接入支付网关。

链上销售交易至少需要写入 external_amount,也就是现实销售金额的计算基数。商户可以选择附带订单号、发票哈希、银行流水、支付网关订单号或其他凭证引用,但这些是可选字段。

节点验证销售交易时,会读取当前 phi,计算最低 N 回馈。如果商户给买方的 N 输出不足,交易会被拒绝。如果 N 足够,交易可以进入 mempool 并等待确认。

销售规则的意义在于:商户不能只从社会需求中获得 D 收入,也要付出一部分 N 。商户销售规模越大,对 N 的需求越大。这样,N 就成为限制无限扩张、连接消费和就业的一种经济约束。

6.5 工资交易

工资交易是另一条关键回路。现实中,雇主通过现金、银行、工资单或支付系统向工人发工资。链上不强制处理工资支付本身,也不强制保存工资单。

链上工资交易至少需要写入 external_amount,也就是工资金额的计算基数。工人向雇主转移一定比例的 N ,比例由 psi 决定。工资单、银行流水和支付凭证可以作为可选引用。

工资规则的意义在于:提供就业机会的雇主可以获得 N ,而 N 又能支撑未来销售能力。这样,雇佣劳动不只是企业的成本,也成为企业获得 N 的渠道之一。系统希望用这种方式把“提供工作”重新变成企业长期发展的重要条件。

6.6 离线交易

项目设计了离线支付能力。用户在短时间断网时,可以基于最近同步的 UTXO 快照构造交易、本地签名并缓存。恢复联网后,钱包会把交易提交给节点。

离线交易可能遇到冲突,例如同一个 UTXO 已经被别的交易花费,或者离线期间 phipsi 参数发生变化。系统需要识别冲突,尝试重建交易、重新计算 N 、提示用户补签,或者明确拒绝。

离线能力很重要,因为现实支付并不总是在网络稳定环境中发生。一个面向普通人的货币系统,不能只适合高质量网络和专业用户。

6.7 节点验证和出块

节点收到交易后,会进行多层验证。第一层是交易结构,检查版本、输入、输出、金额、序列化格式。第二层是 UTXO 和签名,检查输入是否存在、是否未花费、签名是否满足锁定脚本。第三层是身份,检查相关用户是否已认证或是否被暂停。第四层是 BCS 规则,检查销售和工资交易是否满足 N 比例。第五层是治理参数,检查当前高度对应的 phipsi 和其他规则。

通过验证的交易进入 mempool 。验证者节点使用 PoA 或 PoA-BFT 出块。这个系统不挖矿,不消耗大量算力。验证者由治理授权,前期可以由创始人和合伙人共同维护,后期逐步开放给系统治理决定。

6.8 治理闭环

系统不是一次写死规则。治理者需要观察 N 流通、销售容量、就业反馈、用户体验、商户压力、交易失败原因、离线冲突、身份滥用和外部支付接入需求。

如果 phi 太高,商户压力可能过大,交易失败率可能上升。如果 phi 太低,N 对销售规模的约束可能不足。如果 psi 太高,工人负担可能过重;如果 psi 太低,雇主通过就业获得 N 的能力可能不足。治理的任务就是在这些目标之间寻找动态平衡。


7. N 和 D 的关系

本项目当前阶段主要处理 N 。

D 不是强制链上资产。D 可以是现实货币,也可以是银行支付、现金支付、支付网关、发票金额、工资单金额或其他现实经济金额。链上交易用 external_amount 表示 D 侧金额,用来计算 N 的比例流动。

外部支付引用是可选的。也就是说,销售或工资交易可以只写入金额和参与方,也可以附带银行流水、发票、工资单或支付网关凭证。是否强制凭证,不应该在早期系统里一开始就写死。更合理的方式是:MVP 阶段先降低接入门槛,允许手动声明和可选引用;试点阶段增加凭证哈希、商户签名或信任锚验证;成熟阶段再根据需要接入支付网关、银行 API 、发票系统、工资系统或 oracle 。

这样做有几个好处。

第一,用户更容易理解。现实支付照常进行,BCS 钱包只负责 N 的流动。

第二,工程更容易落地。系统不需要一开始就解决银行接入、法币清算、支付牌照和稳定币托管问题。

第三,合规压力更小。项目先作为 N 规则账本和身份治理系统运行,不直接托管现实资金。

第四,后续扩展空间更大。等实际需求明确后,再决定是否强制某些行业提供凭证,或者是否引入链上 D 。


8. 为什么这可能缓解就业和危机问题

现行市场经济中,企业追求利润最大化。如果技术进步能用更少人生产更多商品,企业会倾向于减少用工。短期看,企业成本下降、利润上升;长期看,如果很多人失去收入,社会总需求就会下降。商品越来越多,但有购买力的人越来越少,经济就会出现需求不足、产能过剩、失业和危机。

传统政策通常用财政刺激、货币宽松、补贴、转移支付、公共工程或就业扶持来修正这些问题。这些政策有作用,但往往依赖政府判断、预算能力、政治共识和执行效率,也可能造成资产泡沫、债务积累或资源错配。

N 货币的想法是把调节机制放进交易规则本身。商家销售越多,就越需要 N ;雇主提供工作,就能通过工资规则获得 N 。消费者购买商品时获得 N ,工人获得工资时付出 N ,企业通过雇佣和市场获得 N ,再用 N 支撑销售。这样,消费、就业和销售之间不再完全断开。

这不是反市场,而是给市场补上一个反馈环。市场仍然可以定价、竞争、创新和分工,但不能完全忽视“谁在提供就业机会、谁在维持大众收入、谁在消耗社会购买力”这些问题。

如果这个机制运行良好,它可能形成一种自动稳定器。当企业大量销售却不提供足够就业或不获得足够 N 时,销售能力会受到约束;当企业提供更多就业时,可以获得更多 N ,从而拥有更大的销售空间。这种规则有可能减少极端集中、降低需求断裂风险,并缓解经济危机的形成条件。


9. 技术运行方式

当前项目使用 Python 实现,核心目录是 bcs_chain/。系统包括以下模块:

模块 作用
core/ 交易、区块、UTXO 、脚本、验证器、mempool 、状态
currency/ N 货币规则、phi/psi、N 生命周期、可行性检查
identity/ DID 、VC 、身份注册、信任锚、权限认证
offline/ 离线交易缓存、同步、冲突解决、轻客户端视图
wallet/ 钱包、交易构建、离线模式、导入导出
api/ REST 和 gRPC 接口
network/ P2P 节点、消息广播、peer 管理
consensus/ PoA/PoA-BFT 验证者共识
zk/ 可选零知识证明原型
simulation/ 经济仿真和压力测试

系统的基本数据结构采用 UTXO 模型。N 作为链上资产存在于 UTXO 中。交易花费旧 UTXO ,创建新 UTXO 。这样做有利于离线支付、双花检测和并行验证。

共识层采用授权验证者方式。它不需要矿工通过算力竞争来决定出块权,而是由治理认可的验证者节点出块和签名。这更适合当前项目的目标,因为 BCS 是一个规则治理型经济系统,不是匿名算力竞赛系统。

身份层使用 DID 和 VC 。DID 证明用户控制某个身份,VC 证明用户被某个信任锚认证。信任锚可以是创始团队、合作机构、治理委员会或后续系统表决认可的认证方。


10. 前期治理和后期治理

前期治理建议由创始人和合伙人共同表决决定。原因很现实:早期系统规则还不稳定,参数还需要试错,安全问题还需要快速修复,用户规模也不足以支撑完全开放治理。如果一开始就完全开放,很容易被投机者、攻击者或短期利益绑架。

前期治理应该负责:

  • 确认初始验证者节点。
  • 确认信任锚名单。
  • 决定初始 phipsi
  • 决定初始 N 发放规则。
  • 处理身份认证争议。
  • 修复协议和代码漏洞。
  • 决定试点范围。
  • 判断是否接入外部支付凭证验证。

但前期治理不能永久垄断系统。随着系统稳定,治理权应逐步移交给整个系统。后期可以采用更开放的表决治理,例如用户代表、商户代表、验证者、开发者、N 持有者、就业贡献方和其他角色共同参与。

治理移交可以分阶段:

  1. 创始人和合伙人多签治理。
  2. 加入早期节点和试点商户共同治理。
  3. 建立正式提案和投票流程。
  4. 把参数变更、信任锚变更、验证者变更逐步交给系统投票。
  5. 形成透明的链上治理记录。

治理的最终目标不是让某个团队控制系统,而是让系统有能力长期自我修正。


11. 当前阶段和路线图

当前项目处于早期原型阶段。它已经有比较完整的代码骨架和文档,包括交易、区块、UTXO 、身份、货币规则、离线同步、API 、钱包、Docker 、ZK 原型和仿真模块。但这不代表它已经可以直接生产上线。

下一阶段应重点完成:

  1. 稳定交易格式和 extra schema 。
  2. 完成 DID/VC 身份注册的端到端流程。
  3. 完善 N 初始发放和补充规则。
  4. 完善销售和工资交易构建器。
  5. 完善节点间同步和出块流程。
  6. 增加更多测试,尤其是离线冲突和参数变更测试。
  7. 建立清晰的治理提案和多签流程。
  8. 做一个小规模试点网络。
  9. 通过仿真观察 phi/psi 对就业、销售和 N 流动的影响。
  10. 决定是否、何时、如何接入外部支付凭证验证。

更长期路线可以分为四步:

阶段 1: N-only MVP ,链上只处理 N ,D 侧只用 external_amount
阶段 2: 可选外部凭证验证,接入发票、工资单、支付凭证哈希
阶段 3: 试点治理网络,引入更多节点和参与者表决
阶段 4: 根据实际发展决定是否接入银行、支付网关、oracle 或链上 D


12. 对参与者的意义

对普通用户来说,N 货币希望让消费不再只是单向花钱。用户购买商品时,能够通过规则获得 N 。N 不只是奖励积分,而是和商家销售能力、系统治理和经济反馈相关的资产。

对劳动者来说,系统希望让“被雇佣、被需要、提供劳动”不再只是被动接受工资,而是进入更大的货币反馈结构。劳动关系会通过 N 影响企业未来销售能力。

对商户和企业来说,N 不是惩罚,而是一种新的经营约束。企业如果想扩大销售,需要管理自己的 N 来源。提供就业、参与系统、获得用户信任、从市场获得 N ,都会成为经营的一部分。

对治理者来说,N 是一个调节工具。它不是简单发钱,也不是简单征税,而是通过交易规则改变市场反馈。

对开发者来说,这个项目是一个结合货币理论、区块链工程、身份系统、离线支付和治理机制的开放实验。


13. 风险和限制

这个项目有很多不确定性。

第一,经济模型需要试验。phipsi 设置不当,可能导致商户压力过大、工人负担过重、N 流动不足或投机行为。

第二,外部支付真实性在 MVP 阶段不能完全由链上证明。因为现实货币、银行、现金、支付网关、发票和工资单都是链外系统。当前方案把它们作为可选引用,后续需要根据业务需求逐步增加验证。

第三,身份认证需要谨慎。过严会阻碍用户进入,过松会导致滥用和虚假交易。

第四,治理可能被少数人控制。前期创始团队治理是为了启动系统,但后期必须逐步透明化和系统化。

第五,技术实现还很早期。代码中仍有原型、简化实现和需要安全审计的部分。不能把当前版本当成生产级金融系统直接使用。

第六,社会理解需要时间。N 货币不是传统积分,也不是普通代币。它代表一种新的经济反馈规则,需要通过文档、演示、试点和真实数据逐步建立共识。


14. 如何加入

这个项目需要很多方向的参与者:

  • 货币理论和经济模型研究者。
  • 区块链底层开发者。
  • Python 后端开发者。
  • 钱包和前端开发者。
  • DID/VC 身份系统开发者。
  • 密码学和 ZK 研究者。
  • 仿真和数据分析人员。
  • 商户、雇主和试点组织。
  • 关注就业、年龄歧视、经济危机和社会资源分配的人。

如果你认同一个基本判断:货币不只是支付工具,也是社会资源分配工具;现行单向 D 货币在工业化和自动化之后逐渐暴露结构性缺点;那么 N 货币和 BCS 可能值得一起探索。

这个项目不承诺马上成功,但它提出了一个可以工程化、可以治理、可以试点、可以被反驳也可以被改进的方向。

https://github.com/gufenglees/BCS-Reverse-N-Money-System

BWH DMIT 都没有活动了 可以考虑买个月付对付对付 ,这里只列了 3 个,有其他地区需要的可以自己去产品也看

优惠码 10%OFF

VMISS - US.LA.CN2.Basic

  • 1 Core
  • 1024 MB
  • 10GB SSD
  • 200Mbps Port
  • 300GB Bandwidth
  • 1 IPv4 Included

ℹ️ 节点说明:洛杉矶 CN2 ;三网 CN2 GIA 优化; 1 CAD ≈ 5.1 RMB
💰 套餐价格:$6.00 CAD / 每月
🎫 优惠码10%OFF
🛍️ 购买连接


VMISS - US.LA.CMIN2.Basic

  • 1 Core
  • 1024 MB
  • 10GB SSD
  • 200Mbps Port
  • 400GB Bandwidth
  • 1 IPv4 Included

ℹ️ 节点说明:洛杉矶 CMIN2 ;三网 CMIN2 优化; 1 CAD ≈ 5.1 RMB
💰 套餐价格:$5.00 CAD / 每月
🎫 优惠码10%OFF
🛍️ 购买连接


VMISS - US.LA.9929.Basic

  • 1 Core
  • 1024 MB
  • 10GB SSD
  • 200Mbps Port
  • 500GB Bandwidth
  • 1 IPv4 Included

ℹ️ 节点说明:洛杉矶 9929 ;电信/联通 9929 、移动 CMIN2 ; 1 CAD ≈ 5.1 RMB
💰 套餐价格:$5.00 CAD / 每月
🎫 优惠码10%OFF
🛍️ 购买连接

房间里连接 wifi 信号很差,于是我把闲置的一台路由器拿出来,准备通过无线桥接的方式作为副路由使用。主路由和副路由品牌不同
我测试了摆放位置,给副路由找了个房间里连接信号好,同时又和主路由之间没有阻挡的位置。
设置完成后,发现无法访问互联网。检查副路由设置无问题,但电脑能打开副路由后台,电脑 IP 在副路由网段下,说明上联到主路由这一段有问题。
所以怀疑主路由有问题。登录到主路由后台,发现主路由开启了 Mesh 功能,关闭之后问题解决,能正常上网了。
总结:路由器 Mesh 功能方便同品牌组网,但在我的这组设备上,主路由开启 Mesh 后影响了跨品牌无线桥接。跨品牌组网遇到问题可以排查 Mesh 功能的兼容性。

避坑指南:免费大模型API全是坑,连沙特土豪喜欢的Groq都没救

大家好,我是彪哥。

一、免费API就是个“智商税”

找免费大模型API这件事,折腾了我一上午。结论先放前面:免费的基本都不行。

为什么?因为低质量模型的智商上限就在那里。翻译虽然是个基础任务,不要求推理能力,但它至少需要模型能理解上下文、处理长句结构。

很多参数小的模型连这点都做不到。你花时间调提示词、优化参数,最后发现和默认效果差不多——不是方法的问题,是模型底子的问题。

我的需求其实很明确,就四点:

1.免费。不是新用户送额度,不是邀请好友解锁,是注册就能免费用。

2.有并发。没有并发的API跟网页端手动粘贴没区别。

3.量够用。别搞什么每分钟3次、每天200次那种。

4.不搞身份验证。邮箱注册即可,不要手机号实名。

这要求不算过分。但市面上那些被吹上天的“免费API”,我挨个实测了一遍,结果一个能打的都没有。

第一个让我失望的,是智谱。

二、智谱——新老模型,两套待遇

智谱的免费API,分两个版本。

老模型 GLM-4-Flash,以前我试过,最高支持 200并发。翻译任务勉强够用,量大管饱,虽然效果差点。

新模型 GLM-4.7-Flash,是另一回事。

我登录账号实测,调通API后发并发请求,结果:没有并发。请求全部排队,一个个处理。

image-20260501102537247

没有并发,API和网页端手动粘贴就没区别了。并发不给,每天的请求量和Token上限也不用指望。

老模型保持200并发,新模型 GLM-4.7-Flash 直接不给。智谱的策略很清晰——新模型只让你“试用”,不让你“批量用”。

三、硅基流动——伪免费的文字游戏

硅基流动是网上推荐最多的。理由是“注册送免费额度”。

但送额度和免费,是两码事。额度用完就没了,等于试用,不是免费。

这不算重点。真正的槽点是:硅基流动把所有国外模型全部下架了。一个不剩。

image-20260501131633993

官网的口号写的是“致力于成为全球领先的AI能力提供商”。国外模型一个没有,怎么服务全球用户?改成“致力于成为中国领先的AI能力提供商”更准确。

不过吐槽归吐槽,后面的事情让我发现,有些服务光看口号不行,得看实际能干什么。这是后话。

四、Groq——额度管够,模型不行

智谱新模型没并发,硅基流动送的是体验额度。绕了一圈,我找到了Groq。

为什么一开始觉得它靠谱?

细看Groq的模型限制表,我发现了点不一样的东西。除了Llama这样的主流模型,

它的表单里明确列着两个阿拉伯语相关的模型:allam-2-7b(一个由沙特政府主导开发的阿拉伯语大模型)和 canopylabs/orpheus-arabic-saudi(一个专精沙特口音的语音合成模型)。

这种待遇,我在其他“免费API”平台还真没见到过。

能让沙特政府把国家级模型放在这儿当“免费用”的首选推理平台,甚至为沙特口音专门优化模型,说明背后有不一般的关系。

一个能让产油国掏钱、部署自己“国产模型”的平台,技术底子还是有点料的。

Groq的条件很直接:免费,邮箱注册就能用,不需要实名,不需要拉新。这就是我要的。

它是按模型给限制的,每个模型有自己的每日请求量和每分钟并发数。我扫了一遍它的免费模型限额定表:

模型每分钟请求每天请求
llama-3.1-8b-instant3014,400
llama-3.3-70b-versatile301,000
其他常规模型301,000左右

差距很明显。只有 llama-3.1-8b-instant 给到了每天 14,400 次请求,其他模型普遍只给 1,000 次。

当时我的判断是:选 8B 这个。翻译嘛,又不是写论文。

我还让gemini做了一个简单的对比分析:

Llama-3.1-8BLlama-3.1-70B
翻译质量85-90分95分
适用场景日常/技术翻译文学级/复杂长文
每天免费次数14,4001,000

结论很明确:翻译任务不需要95分,85分够了。选量大的。

我用 Python 调了 API 跑了一遍,速度也很快,2秒一个翻译请求:

image-20260501110611117

额度、速度、注册门槛,全达标了。到这里为止,Groq 看起来就是最优解。

实际用起来什么样 ?

一上真实文本,问题全出来了。

稍微复杂一点的句子,翻译就崩。长句结构理不清,修饰关系搞反,技术术语胡乱对应。

别说 85 分,60 分都勉强。

结论就是:8B 模型连翻译任务都胜任不了,不建议使用。基本上就是没脑子的东西。

额度再多、速度再快,翻译结果是废的,就全是零。

回头看开头那句话——免费API只能处理一加一的事情,一加二做不了。翻译这件事,对8B来说,已经是“一加二”了。

Groq 的免费额度够诚意,并发给得足。但模型底子决定了上限。免费+量大管饱,架不住质量不及格。

五、免费的路,走不通

智谱新模型不给并发,硅基流动是试用,Groq 模型能力扛不住翻译。

全试了一遍,结论很简单:免费的都不行。

连沙特土豪都发不起免费的靠谱API,我们还能指望什么。

回过头看,硅基流动虽然免费策略让人不爽,但作为付费服务,它的模型生态和稳定性确实是国内第一梯队。吐槽归吐槽,干活还是得靠它。

如果你也试过一圈免费的、发现实在不行,可以用我的邀请链接注册,双方各得16元奖励券:

https://cloud.siliconflow.cn/i/ajjF89Lm

这篇文章不是广告。以后谁再跟你说“翻译用免费API足够了”,把这篇文章甩给他——我替你踩过坑了。

抱拳了

感谢各位朋友捧场!要是觉得内容有有点意思,别客气,点赞、在看、转发,直接安排上!

想以后第一时间看着咱的文章,别忘了点个星标⭐,别到时候找不着了。

行了,今儿就到这儿。

image-20260501151305264

论成败,人生豪迈,我们下期再见!

背景:Apple Watch 6 年老用户,几乎每天都用它监测睡眠和设置闹钟。所有系统版本第一时间通宵更新。
版本:iOS 26.4.2 ,WatchOS 26.4 。

这么多年我对 Apple Watch 闹钟不响这种反馈都持怀疑态度,因为自身从没遇到,只有自己有意识关了又睡过去了,没有印象因为闹钟没响导致睡过头的。

不过今年 4 月份整整发生了两次这样的事情。

第一次是醒来发现闹钟界面还在,但是没声音,我怀疑是系统问题,而且 Reddit 和 V2 有人说是自己无意识覆屏静音了,我就没在意,但也疑惑为什么在全屏闹钟模式下,动画还在但不响铃。

昨晚上的经历更加让人恼火和毛骨悚然。

因为今天早上约了健身早课,我昨晚早早就上床了,设置好睡眠模式和起床闹钟之后,我发现手表在睡眠模式下并不会息屏,所以又重启了一遍。

这里有个小插曲,之前版本的系统把睡眠模式长按数码表冠解锁的功能,改成了按一下表冠就能解锁,之前因为这个我出现过误触,所以我最近一年时间都是故意让手表留在输入密码的锁定状态,也没有影响睡眠监测和闹钟。

在我确认手表已锁定,能息屏,闹钟定好之后就睡了,结果早起直接睡过了一个多小时。

起来之后我发现,闹钟界面完全没有,睡眠模式自动关闭了,而手表也已经解锁了。

手机离我很远,所以我不可能早起无意识操作了手机。

手表我想要在刚醒的迷糊状态,去关闭闹钟+输入六位密码,我觉得很难做出来。

自此我觉得这系统绝对有点说法,可能大家的反馈并非孔雀来风。

最近合同到期,要转签无固定期限合同时,老板不续签合同了。

在公司待了 10 年,都 40 了,时间是飞快。
从后端到产品,从产品到技术总监,啥都干过了,啥都经历了。

感慨下,被裁的原因不是因为公司业绩不行,而是“方法”不对,没能给出完美的“PPT”。

人生短短几十年不应该只有工作,家庭才是最重要的。
慢慢思考下以后能做点什么有意义的事,不想再做些心里不认可,但又不得不做的事。

兄弟们,Google Play 充值 google play credit 110$准备购买 gpt pro ,结果翻车了。点升级后 the purchase was unsuccessful. pleasw try again later 。
现在钱退也退不了,买也买不了,怎么办。
卡密网的又容易掉会员封号,智谱又抢不到,中转又不想用。只想有个稳定的,好用的 AI coding ,咋就这么麻烦。

图片加载库的缓存直接决定用户感知:命中就瞬间上屏,没命中就闪一下占位图再切换。ImageKnifePro 的缓存要同时处理多线程并发读写、内存字节级上限管理、磁盘文件的并发保护三件事,整套实现在 C++ 层用 lru11 加 std::mutex 完成。

一、内存缓存的数据结构

MemoryCache 是一个 Meyer's Singleton,核心数据结构声明如下:

lru11::Cache<std::string, std::shared_ptr<ImageDataCache>, std::mutex> cache_;

lru11 是一个 header-only 的 C++11 LRU 库,内部用 std::list 维护访问顺序,用 std::unordered_map 做 O(1) 查找。每次 tryGet 命中时,对应节点从链表中摘出、移到头部,表示"最近被使用过"。淘汰时从链表尾部取,尾部就是最久没被访问的条目。

第三个模板参数决定锁类型。传 std::mutex 让 lru11 在 insert/remove/tryGet 等操作内部自动加互斥锁;传 NullLock 则无锁。选 std::mutex 的原因是图片解码线程写缓存、UI 线程读缓存,这两个方向必须互斥。

Get 方法用的是 tryGet 而不是 getget() 在 key 不存在时抛 KeyNotFound 异常,而缓存未命中是高频场景,用异常控制流开销太大。tryGet 命中返回 true 并刷新访问顺序,未命中直接返回 false。

LRU 数据结构

二、并发安全与内存上限

内存统计用 std::atomic<long> currentMemory_ 追踪。Put 方法的实现值得细看:

void MemoryCache::Put(const std::string &key, std::shared_ptr<ImageDataCache> value)
{
    cache_.insert(key, value, true);  // 第三个参数 true:跳过 lru11 内置 prune
    currentMemory_ += value->GetImageCacheSize();

    while (currentMemory_ >= allowMaxMemory_ || cache_.size() >= allowMaxSize_) {
        std::string keyOut;
        std::shared_ptr<ImageDataCache> valueOut;
        if (!cache_.getTail(keyOut, valueOut)) {
            break;
        }
        cache_.remove(keyOut);
        currentMemory_ -= valueOut->GetImageCacheSize();
        if (currentMemory_ < 0) {
            currentMemory_ = 0;
        }
    }
}

insert 的第三个参数传了 true,跳过 lru11 自带的 prune 逻辑。原因很直接:lru11 在自动 prune 时只从内部数据结构中删除条目,不会通知外部扣减 currentMemory_。如果让它自动淘汰,currentMemory_ 会只增不减,和真实占用脱节。

手动 prune 循环用 getTail 取链表尾部,remove 后在外部扣减字节数。currentMemory_ < 0 的兜底判断属于防御性编程——atomic 操作在高并发下可能出现 remove 和 Put 交错执行的情况,负数归零避免累积误差。

默认容量是 256 条、128MB。SetCacheLimit 可以调整,但做了硬上限:条目数最大 4096,内存最大 1GB。超过的入参直接截断到上限值。调整后调 cache_.resize(allowMaxSize_) 通知 lru11 更新容量参数,但 resize 不会立即淘汰多出来的条目——淘汰推迟到下一次 Put 时自然发生。可以选择立即淘汰,但那会在调整容量的调用点引入不确定的阻塞时间,"推迟到写入时"让淘汰成本分摊到后续每次写入。

三、文件缓存的 FileDesc 状态机

文件缓存 FileCache 的 LRU 中,value 存放的是一个 shared_ptr<FileDesc> 智能指针,用于描述文件状态,文件数据留在磁盘上。FileDesc 里嵌了一个 atomic<FileState>

enum class FileState { IDLE, READING, DELETING, WRITING };

struct FileDesc {
    size_t size = 0;
    std::atomic<FileState> state = FileState::IDLE;
    std::string extension;
};

四种状态围绕一个核心目标:不要删正在被读的文件。

FileDesc 状态机

写入时(SaveImageToDisk),FileDesc 创建即置为 WRITING,文件成功落盘后变为 IDLE,失败则从 cache 中 remove 掉这个 key。读取时(GetFileDescForRead),先检查当前 state,如果是 DELETINGWRITING 就返回 nullptr 表示缓存不可用,否则置为 READING,读完后归位 IDLE

std::shared_ptr<FileCache::FileDesc> FileCache::GetFileDescForRead(const std::string &fileKey)
{
    auto fileDesc = Get(fileKey);
    if (fileDesc == nullptr) {
        return nullptr;
    }
    if (fileDesc->state == FileState::DELETING || fileDesc->state == FileState::WRITING) {
        return nullptr;
    }
    fileDesc->state = FileState::READING;
    return fileDesc;
}

淘汰逻辑在 Put 内部的 while 循环里:

if (valueOut->state != FileState::READING) {
    valueOut->state = FileState::DELETING;
    cache_.remove(keyOut);
    DeleteFile(keyOut);
    RemoveMemorySize(valueOut->size);
} else {
    break;
}

尾部条目如果正在被读取,直接 break 放弃本轮淘汰。这看起来会让缓存暂时超限,但下一次 Put 会再次尝试,到那时读操作大概率已经结束。这个权衡是"宁可短暂超限,不要中断读操作"。每个 FileDesc 的 state 是独立的 atomic 变量,不同文件的读写互不阻塞,只有同一个文件的并发操作才通过状态检查来协调,比一把大锁锁住整个文件缓存并发度高得多。

四、大端缓存和小端缓存

默认的 FileCache 实例称为"大端缓存",存主流程的图片。很多应用里,头像和商品大图的访问模式完全不同:头像数量多、体积小、更新少;商品大图数量也多、体积大、更新频繁。如果它们共用一个 LRU,商品图的高频更新会把头像挤出缓存,用户每次进个人页都要重新加载。

FileCacheInterceptorDefault 内部用 unordered_map<string, FileCache*> 管理所有小端实例。addSmallEndFileCache 创建时做了三重校验:cacheName 不能为空(空字符串保留给大端);filePath 不能与大端目录相同(防止文件名冲突);不能和已有小端重名或同路径。三个校验分别返回 CACHE_NAME_EMPTYFILE_PATH_OCCUPIEDCACHE_NAME_OCCUPIED 错误码。

使用时在 ImageKnifeOption 中设置 fileCacheName 字段,拦截器在处理请求时检查这个字段,找到对应的小端实例就用它替代大端做读写。

大端小端架构

小端缓存的默认上限比大端低一半——条目数 2048 vs 4096,磁盘容量 256MB vs 512MB。这个不对称设置是合理的:小端通常存放体积较小的图片(头像、图标),单个文件占用少,同样的条目数下总容量不需要那么大。

所有 FileCache 实例的磁盘占用都计入一个全局的 static std::atomic<size_t> g_deviceDiskUsage,硬上限 1.5GB。每次写入时 AddMemorySize 同时累加实例级的 currentMemory_ 和全局计数器。淘汰时 RemoveMemorySize 同时扣减两者。g_deviceDiskUsagesize_t(无符号类型),扣减前要判断是否小于待扣减值,否则减出来会溢出成一个超大正数。

void FileCache::RemoveMemorySize(size_t value)
{
    currentMemory_ -= value;
    if (currentMemory_ < 0) {
        currentMemory_ = 0;
    }
    if (g_deviceDiskUsage < value) {
        g_deviceDiskUsage = 0;
    } else {
        g_deviceDiskUsage -= value;
    }
}

五、缓存键为什么用 SHA256

文件缓存的 key 由 DefaultCacheKeyGenerator::GenerateFileKey 生成。它把 loadSrc(URL 或本地路径)加上可选的 signature 拼接后做一次 SHA256 哈希。

std::string DefaultCacheKeyGenerator::GenerateFileKey(const ImageSource *imageSrc,
    const std::string &signature)
{
    std::ostringstream src;
    std::string fileKey;
    imageSrc->GetString(fileKey);
    src << "loadSrc==" << fileKey << ";";
    if (!signature.empty()) {
        src << "signature=" << signature << ";";
    }
    DoMd5Hash(src.str(), fileName);
    return fileName;
}

URL 里可能包含 /?&= 等字符,这些在大多数文件系统中要么非法要么有特殊含义。URL 长度也没有上限,而文件名通常限制在 255 字节。SHA256 哈希固定 64 个十六进制字符,既安全又不会超长。

内存缓存的 key 策略完全不同——它是明文拼接,包含 loadSrc、transformation、降采样参数、orientation、dynamicRange 等信息。同一张图经过不同变换会产生不同的内存 key,各自独立缓存。文件缓存 key 不含 transformation,因为磁盘存的是原始数据,变换在解码后执行。

CacheKeyGenerator 是一个虚基类,可以通过 SetCacheKeyGenerator 替换默认实现。如果业务的 URL 里带了时间戳或 token 参数,每次请求 URL 都不同,就需要自定义 key 生成逻辑把这些参数剥掉,否则同一张图会被当成不同的缓存条目反复下载。

六、四种写入策略

ImageKnifeOption 上有一个 writeCacheStrategy 字段,四种取值对应不同的业务场景。

DEFAULT 同时写内存和文件缓存,绝大多数场景用这个。MEMORY 只写内存缓存,适合验证码、一次性广告弹窗——这类图片下次启动不会再用,没必要占磁盘。FILE 只写文件缓存不写内存缓存,用于预加载场景——提前把图片下载到磁盘但不占用内存,等用户真正浏览到的时候再从磁盘读取解码。NONE 完全不缓存,适合监控画面、实时地图瓦片等内容持续变化的场景。

if (option->writeCacheStrategy == CacheStrategy::FILE ||
    option->writeCacheStrategy == CacheStrategy::NONE) {
    writeMemoryCache = false;
}
if (option->writeCacheStrategy == CacheStrategy::MEMORY ||
    option->writeCacheStrategy == CacheStrategy::NONE) {
    writeFileCache = false;
}

七、冷启动时的文件缓存恢复

冷启动时内存缓存是空的,文件缓存需要从磁盘恢复。InitFileCacheopendir/readdir 遍历缓存目录,对每个文件调 stat 获取 st_atimest_size。按 st_atime 降序排列后依次插入 LRU,最近访问的排在链表热端。如果累计容量超限,后面的文件直接删除。

std::sort(filesVector.begin(), filesVector.end(), [](const FileInfo &a, const FileInfo &b) {
    return a.lastAccessTime > b.lastAccessTime;
});
for (const auto &fileInfo : filesVector) {
    if ((currentMemory_ + fileInfo.fileSize) >= allowMaxMemory_ || (cache_.size() + 1) >= allowMaxSize_) {
        DeleteFile(fileInfo.fileName);
    } else {
        cache_.insert(fileInfo.fileName, std::make_shared<FileDesc>(fileInfo.fileSize, fileInfo.extension));
        currentMemory_ += fileInfo.fileSize;
    }
}
g_deviceDiskUsage += currentMemory_;
isFileInitComplete_ = true;

初始化完成前所有读写请求都会被 isFileInitComplete_ 标志位拦住,返回 nullptr。这个保护避免了"LRU 还没建好就查询"导致的假性未命中——明明文件在磁盘上,但 LRU 里还没录入,查不到就走网络重新下载,白白浪费带宽。全局磁盘计数器 g_deviceDiskUsage 在初始化结束后一次性累加,不在遍历过程中逐文件计数,减少 atomic 操作次数。

以上就是本篇内容的所有了~有什么问题欢迎在评论区提出


项目地址:ImageKnifePro

ImageKnifePro 的加载引擎围绕拦截器-责任链模式构建。缓存、加载、解码各自独立成链,每一层拦截器只做一件事,做不到就传给链上的下一个。运行时随时可以插入、替换、重排。这篇从源码看四层拦截器的内部结构,它们怎么串联、怎么短路、怎么处理异步下载的线程分离,以及自定义拦截器怎么塞进去。

一、Interceptor 基类

Interceptor 定义在 include/interceptor.h 里,核心结构精简到三样东西:一个 Resolve 纯虚函数、一个 Process 虚方法、一个指向下一个拦截器的 next_ 智能指针。

class Interceptor {
public:
    std::string name;
    virtual bool Resolve(std::shared_ptr<ImageKnifeTask> task) = 0;
    virtual void Cancel(std::shared_ptr<ImageKnifeTask> task);
    virtual bool Process(std::shared_ptr<ImageKnifeTask> task,
                 std::function<bool(std::shared_ptr<ImageKnifeTask>)> resolveCallback = nullptr);
    virtual ~Interceptor() = default;
protected:
    std::shared_ptr<Interceptor> next_ = nullptr;
};

Resolve 是子类必须实现的业务逻辑入口,返回 true 表示"我处理了",false 表示"让下一个来"。Cancel 是可选的取消接口,基类给了空实现,只有需要中断异步操作的拦截器才需要覆写。

Process 的执行逻辑分四步。第一步做前置检查:向下转型为 ImageKnifeTaskInternal,检查致命错误或请求已被销毁,任何一个条件成立直接返回 false。第二步 SetInterceptor(this) 把自己的裸指针记录在 task 上,后续的 Cancel 操作能找到正在执行的拦截器。第三步调用 Resolve(或外部传入的 resolveCallback),通过 ExecuteResolveFunction 包装执行,负责写 HiTrace 追踪标记和日志输出。第四步根据返回值决定走向:

bool result = ExecuteResolveFunction(this, taskInternal, resolveFunction);
if (taskInternal->IsDetached() && IsLoadInterceptor(this)) {
    return true;
}
if (result) {
    taskInternal->ClearInterceptorPtr();
    return true;          // 短路
} else if (next_ != nullptr) {
    return next_->Process(task);  // 传递
} else {
    taskInternal->ClearInterceptorPtr();
    return false;         // 链尾
}

name 字段用于日志和 HiTrace 追踪。每个默认拦截器在构造函数里取名("Default DownloadInterceptor""Default DecodeInterceptor Avif" 等),ExecuteResolveFunctionname 拼接缓存操作类型(Read/Write)作为 trace 名称。在海量并发请求中定位单条请求的路径变得可行。

二、四类拦截器子类

Interceptor 派生出四个抽象子类,分别对应图片加载流水线的四个阶段。每个子类都把 Process 标记为 final——开发者写自定义拦截器时只需要实现 Resolve

拦截器类图

MemoryCacheInterceptor 是内存缓存层。默认实现 MemoryCacheInterceptorDefaultResolve 根据 cacheTask.type 走读或写分支。读操作用 cacheKeyMemoryCache 单例查找,命中就把 imageDataCache 塞进 task 的 product 里返回 true。写操作在缓存里不存在对应 key 时放入解码后的 ImageData。读和写复用同一个拦截器实例,通过 cacheTask.type 区分——同一条内存缓存链既服务于加载前的"查缓存",也服务于解码后的"写缓存"。

bool Read(std::shared_ptr<ImageKnifeTask> task)
{
    if (MemoryCache::GetInstance()->Contains(task->cacheTask.cacheKey)) {
        task->product.imageDataCache = MemoryCache::GetInstance()->Get(task->cacheTask.cacheKey);
        if (task->product.imageDataCache == nullptr) {
            task->EchoError("Empty ImageData");
            return false;
        }
        return true;
    }
    return false;
}

FileCacheInterceptor 是文件缓存层。FileCacheInterceptorDefault 采用单例模式,因为磁盘缓存需要全局统一管理容量。它内部维护一个主缓存实例"大端"和一个 fileCacheMap_"小端"集合。Resolve 的第一件事是确定用哪个 FileCache 实例——如果请求类型是主图且 fileCacheNamefileCacheMap_ 里能找到,就用对应的小端;否则用大端。读操作调 GetImageFromDisk,写操作调 SaveImageToDisk,支持文件后缀感知:开启 EnableExtension 后用 FileTypeUtil::CheckImageFormat 检测 buffer 头部魔数,给缓存文件加上 .jpg.png 等后缀。

LoadInterceptor 是加载层,四类中逻辑最复杂的一个。它比基类多了三样东西:Detach 异步分离方法、OnComplete 回调归队方法、isLoadFromRemote 布尔标志。

isLoadFromRemote 决定了 Process 的行为差异。DownloadInterceptorDefault 默认为 true,Process 会启用 RetryFallbackUrls 作为 resolveCallback,激活多域名重试和 CRC32 校验逻辑。ResourceInterceptorDefault 设为 false,直接走基类的普通链式调用,不做重试。

bool LoadInterceptor::Process(std::shared_ptr<ImageKnifeTask> task,
                              std::function<bool(std::shared_ptr<ImageKnifeTask>)> resolveCallback)
{
    if (!isLoadFromRemote || task->GetImageRequestType() != ImageRequestType::MAIN_SRC) {
        return Interceptor::Process(task);
    }
    return Interceptor::Process(task, [this](std::shared_ptr<ImageKnifeTask> t) {
        return RetryFallbackUrls(t, this);
    });
}

默认加载链上挂了两个拦截器:DownloadInterceptorDefault 排在前面,ResourceInterceptorDefault 排在后面。网络下载失败时,基类 Processnext_ 调用自然地把任务传给后者,不需要额外的 fallback 分支。

DecodeInterceptor 是解码层。默认链上有两个拦截器:DecodeInterceptorDefault 在前,DecodeInterceptorAvif 在后(条件添加)。DecodeInterceptorDefault 遇到 AVIFUNKNOWNCUSTOM_FORMAT 就返回 false,任务自动滑到下一个。

三、Detach 分离机制

网络下载是异步操作,如果让下载线程阻塞等待 RCP 回调,线程池会很快耗尽。Detach 在发起异步请求后立即释放当前工作线程。

void LoadInterceptor::Detach(std::shared_ptr<ImageKnifeTask> task)
{
    auto taskInternal = std::dynamic_pointer_cast<ImageKnifeTaskInternal>(task);
    taskInternal->Detach();
}

分离后的任务由 RCP 的回调线程通过 OnComplete 重新接管。OnComplete 内部的流程比较复杂:先做 CRC32 校验(如果配置了),校验失败则清空 imageBuffer 并标记错误。如果下载失败且配置了 fallbackUrls,会在当前线程中重试下一个 URL,重试过程中可能再次 Detach。全部 URL 耗尽仍然失败,且 next_ 非空时,构造新的 ImageKnifeTaskInternal(因为分离后的 task 不能复用),调 next_->Process 把任务传给 ResourceInterceptorDefault。最终通过 FinishLoadChain 将任务推回 TaskWorker 队列。

API 版本低于 13 的设备不支持 Detach。IsDownloadDetachEnabled() 做了检测,低版本回退到 std::promise/future 方案:ResponseCallback 不调 OnComplete,而是用 data->waitDownload.set_value(result) 通知等待线程。

四、RetryFallbackUrls 多域名重试

ImageKnifeOption 可以配置 fallbackUrls 备用地址列表。RetryFallbackUrlsfallbackUrlIndex 开始逐个尝试。

bool RetryFallbackUrls(std::shared_ptr<ImageKnifeTask> task, LoadInterceptor *downloadInterceptor)
{
    for (int i = taskInternal->fallbackUrlIndex; i < (int)option->fallbackUrls.size(); i++) {
        if (request->IsDestroy() || taskInternal->IsFatalErrorHappened()) {
            return false;
        }
        if (taskInternal->fallbackUrlIndex > -1) {
            taskInternal->SetFallbackUrl(option->fallbackUrls[i]);
        }
        taskInternal->fallbackUrlIndex++;
        bool result = downloadInterceptor->Resolve(task);
        if (taskInternal->IsDetached()) {
            return true;
        }
        if (result && IsImageCrc32Match(taskInternal, option)) {
            return true;
        }
    }
    taskInternal->ResetFallbackUrl();
    return false;
}

fallbackUrlIndex 初始值是 -1,表示首次使用原始地址。第一轮循环时 SetFallbackUrl 不会被调用,Resolve 用的是原始 URL。原始 URL 失败后 index 递增到 0,开始使用 fallbackUrls[0]

每次重试中间都检查了 IsDetached()——如果某个备用地址的下载也走了 Detach,当前循环中断,后续重试由 OnComplete 回调继续驱动。重试全部失败后 ResetFallbackUrl 恢复到初始状态,确保链上下一个拦截器拿到的是干净的原始请求。

CRC32 校验用 zlib 的 crc32() 函数对整个 imageBuffer 计算校验值。crc32 设为 0 表示跳过校验。校验发生在两条路径上:同步路径在 RetryFallbackUrls 的每次 Resolve 成功后调用;异步分离路径在 OnComplete 回调中对远端加载结果调用。两条路径最终汇入同一个 IsImageCrc32Match 函数,校验逻辑保持一致。

五、自定义拦截器的插入

ImageKnifeLoader 为四条链各提供一个 Add 方法,参数是拦截器实例和插入位置。Position 枚举只有 STARTEND 两个值。

void ImageKnifeLoaderInternal::AddLoadInterceptor(
    std::shared_ptr<LoadInterceptor> interceptor, Position position)
{
    if (loadInterceptorHead_ == nullptr) {
        loadInterceptorHead_ = loadInterceptorTail_ = interceptor;
        return;
    }
    if (position == Position::START) {
        interceptor->SetNext(loadInterceptorHead_);
        loadInterceptorHead_ = interceptor;
    } else {
        loadInterceptorTail_->SetNext(interceptor);
        loadInterceptorTail_ = interceptor;
    }
}

四条链的 Add 方法代码结构完全一致,只是类型参数不同。写一个自定义加载拦截器:继承对应子类,实现 Resolve,构造时给 name 赋值用于追踪,调 Add 方法指定位置。比如想在网络下载前先查一层自研的 P2P 缓存,写一个 P2PCacheInterceptor : public LoadInterceptor,命中返回 true 短路跳过网络请求,未命中返回 false 让 Processnext_ 自动传给 DownloadInterceptorDefaultPosition::START 保证 P2P 拦截器在默认下载拦截器之前执行。

loader 既可以全局默认应用,也可以针对特定请求单独配置。每个 ImageKnifeOption 可以绑定不同的 loader 实例,实现"不同业务场景用不同的拦截器组合"。CreateEmptyImageLoader 创建四条链全空的 loader,完全由开发者自行填充。CreateDefaultImageLoader 则预装全套默认拦截器,开箱即用。

请求流程图

六、责任链模式的实际收益

四层拆分的收益体现在三个维度上。

职责边界DownloadInterceptorDefault 只管发 HTTP 请求拿到 buffer,缓存读写由 FileCacheInterceptor 链负责,解码由 DecodeInterceptor 链负责。改动缓存策略不会碰到下载代码。

扩展方式。新增加载源只需要继承对应子类、实现 Resolve、调 AddLoadInterceptor 插入链上,不碰任何现有代码。DecodeInterceptorAvif 的条件添加就是这个思路的实际案例——CreateDefaultImageLoader 中用 if (ImageKnifeDecoderAvif::IsAvifEnable()) 决定是否把 AVIF 拦截器挂到解码链。

失败处理。链式调用天然支持 fallback:DownloadInterceptorDefault 返回 false,基类 Processnext_ 自动把任务传给 ResourceInterceptorDefault,后者尝试本地路径。这个流转完全由基类驱动,业务拦截器不需要知道自己的前后是谁。

以上就是本篇内容的所有了~有什么问题欢迎在评论区提出


项目地址:ImageKnifePro

图片加载的后半段是把字节流变成可渲染的 PixelMap。ImageKnifePro 用 C++ 拦截器链驱动解码分发,并通过 dlopen 在运行时动态加载 AVIF 解码器。编译期通过 #ifdef 控制头文件引入,运行时通过 dlopen 检测设备能力,两层防线保证在不支持 AVIF 的环境下安全降级。

一、FileTypeUtil 的文件头识别

解码之前得知道拿到的是什么格式。ImageKnifePro 不依赖文件扩展名,直接读取字节流头部的魔数(magic number)判断。网络请求拿回来的数据不一定带扩展名,缓存文件的命名经常用 hash 替代原名,只有二进制头部的魔数是可靠的。

FileTypeUtilstd::map<ImageFormat, std::vector<std::vector<uint8_t>>> 做签名表。每种格式可以有多条候选签名(比如 AVIF 有 ftyp avifftyp avis 两条,前者是静态 AVIF,后者是序列图/动图)。

static std::map<ImageFormat, std::vector<std::vector<uint8_t>>> fileSignatureMap_ = {
    {ImageFormat::JPG, {{0xFF, 0xD8}}},
    {ImageFormat::PNG, {{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}}},
    {ImageFormat::GIF, {{0x47, 0x49, 0x46}}},
    {ImageFormat::WEBP, {{0x52, 0x49, 0x46, 0x46, 0, 0, 0, 0, 0x57, 0x45, 0x42, 0x50}}},
    {ImageFormat::HEIC, {{0x00, 0x00, 0x00, 0x18, 0x66, 0x74, 0x79, 0x70, 0x68, 0x65, 0x69, 0x63}, ...}},
    {ImageFormat::AVIF, {{0x00, 0x00, 0x00, 0x1C, 0x66, 0x74, 0x79, 0x70, 0x61, 0x76, 0x69, 0x66},
        {0x00, 0x00, 0x00, 0x20, 0x66, 0x74, 0x79, 0x70, 0x61, 0x76, 0x69, 0x73}}},
    // ...
};

MatchesSignature 方法有两处特殊处理。HEIC 和 AVIF 都基于 ISOBMFF 容器格式,头部结构是一个 Box:前 4 字节是 Box 的 size,后面才是类型标识 ftyp。比对时从第 4 字节开始,跳过 size 字段。WEBP 文件以 RIFF 四字节开头,紧跟 4 字节的 fileSize,然后才是 WEBP 标识。比对逻辑拆成两段:先比前 4 字节 RIFF,跳过 4-8 字节的 size 字段,再从第 8 字节开始比对 WEBP

if (fileType == ImageFormat::WEBP) {
    constexpr int riffEnd = 4;
    for (int i = 0; i < riffEnd; i++) {
        if (data[i] != signature[i]) return false;
    }
    constexpr int typeOffset = 8;
    for (int i = typeOffset; i < signature.size(); i++) {
        if (data[i] != signature[i]) return false;
    }
    return true;
}

CheckImageFormat 入口处有一个前置长度校验:文件字节流不足 32 字节直接返回 UNKNOWN,避免后续比对越界。32 字节足够覆盖所有已知格式的签名长度。

格式识别流程

二、DecodeInterceptorDefault 的标准解码路径

默认解码拦截器 DecodeInterceptorDefault 处理系统 OH_ImageSourceNative 支持的所有格式——JPG、PNG、GIF、WebP、BMP、HEIC、TIFF、ICO 等。Resolve() 开头检查文件格式,遇到 AVIFUNKNOWNCUSTOM_FORMAT 就返回 false,任务自动滑到链上的下一个拦截器。

bool Resolve(std::shared_ptr<ImageKnifeTask> task) override
{
    auto fileTypeInfo = task->GetFileTypeInfo();
    if (fileTypeInfo->format == ImageFormat::UNKNOWN ||
        fileTypeInfo->format == ImageFormat::CUSTOM_FORMAT ||
        fileTypeInfo->format == ImageFormat::AVIF) {
        return false;
    }
    if (taskInternal->IsFrameDecodeMode()) {
        return DecodeFrame(taskInternal);
    } else {
        return Decode(taskInternal);
    }
}

Decode 方法的流程分三个阶段。GetImageSourceimageBuffer 创建 OH_ImageSourceNativeConfigDecodeOption 装配 OH_DecodingOptions——设置期望解码尺寸(降采样)、JPEG 的 NV21 像素格式优化、期望动态范围。JPEG NV21 优化的判断条件是:全局开启 jpegOptimizeDecoding 且当前请求没有 multiTransformationtransformation。出于"NV21 格式不支持后续图形变换"的限制,有变换需求时不能走这条优化路径。

bool ConfigDecodeOption(DecodeArgs &args, std::shared_ptr<ImageKnifeTaskInternal> &task)
{
    OH_DecodingOptions_Create(&args.decodeOption);
    Image_Size imageSize = task->GetDesiredImageSize();
    if (imageSize.width != 0 && imageSize.height != 0) {
        OH_DecodingOptions_SetDesiredSize(args.decodeOption, &imageSize);
    }
    if (task->GetFileTypeInfo()->format == ImageFormat::JPG && IsYuvEnable(task)) {
        OH_DecodingOptions_SetPixelFormat(args.decodeOption, PIXEL_FORMAT_NV21);
    }
    OH_DecodingOptions_SetDesiredDynamicRange(args.decodeOption, GetDesiredDynamicRange(task));
    return true;
}

多帧解码走 DecodePixelmapList(),调 OH_ImageSourceNative_CreatePixelmapList() 一次解出所有帧。帧延迟信息优先从 FileTypeInfo 的缓存中获取;缓存没有就重新调 OH_ImageSourceNative_GetDelayTimeList() 获取,这个 fallback 避免了解码阶段因缺少帧时长而失败。

单帧解码走 CreatePixelmapByAllocator(),优先尝试 DMA 内存分配模式。如果指定模式不被支持(返回 IMAGE_SOURCE_UNSUPPORTED_ALLOCATOR_TYPE),自动回退到 IMAGE_ALLOCATOR_TYPE_AUTO。如果封装的 API 函数指针为空(老版本 SDK),走标准的 OH_ImageSourceNative_CreatePixelmap

三、EXIF 方向校正

解码完成后还有一步 EXIF 方向修正。Orientate() 只在主图请求且 option->orientation == AUTO 时执行。它从 OH_ImageSourceNative 读出 Orientation 字符串,传给 PixelmapUtils::Orientate()

void Orientate(std::shared_ptr<ImageKnifeTask> task, OH_ImageSourceNative *source)
{
    if (task->GetImageRequestType() != ImageRequestType::MAIN_SRC) return;
    if (option->orientation != Orientation::AUTO) return;
    std::string keyStr = "Orientation";
    Image_String key = {.data = (char *)keyStr.c_str(), .size = keyStr.length()};
    Image_String value = {.data = nullptr, .size = 0};
    OH_ImageSourceNative_GetImageProperty(source, &key, &value);
    std::string valueStr(value.data, value.size);
    PixelmapUtils::Orientate(task->product.imageData, valueStr);
    free(value.data);
}

EXIF 标准定义了 8 种 Orientation 值,分别对应不同的翻转/旋转组合。PixelmapUtils::Orientate()unordered_map<string, function<void(OH_PixelmapNative*)>> 把每种方向映射到操作函数,对每一帧都执行。Image_String 返回的 data 不包含尾 0,必须用 std::string(value.data, value.size) 构造,并且按接口说明 free(value.data) 释放分配的内存。

四、DecodeInterceptorAvif 的独立拦截器

AVIF 由独立拦截器 DecodeInterceptorAvif 处理。CreateDefaultImageLoader 组装默认链时有条件地添加它:

loader->AddDecodeInterceptor(decodeInterceptor);       // Default 在链头
if (ImageKnifeDecoderAvif::IsAvifEnable()) {
    loader->AddDecodeInterceptor(decodeInterceptorAvif, Position::END); // AVIF 在链尾
}

如果设备不支持 AVIF,这个拦截器压根不会被添加。遇到 AVIF 图片时,默认解码器返回 false,链尾没有下一个节点,整条解码链返回 false,上层报解码失败。格式支持的增减变成了拦截器的有无,不影响已有拦截器的任何代码。

Resolve() 只在 fileTypeInfo->format == ImageFormat::AVIF 时接管。Decode() 方法先尝试从 task 上获取可复用的 ImageKnifeDecoderAvif 实例——AVIF 序列图的逐帧解码场景下同一个 decoder 实例会被多次复用,避免重复解析 AVIF 容器头部。

根据 IsFrameDecodeMode() 的值决定走向。逐帧模式调 DecodeFrame(task->GetDecodeFrameIndex(), &pixelmap) 只解一帧。批量模式分配 OH_PixelmapNative* 数组,循环调用 DecodeFrame(i, &pixelmapList[i])

批量模式有一个防御设计:循环开始前就构造 ImageData 对象持有 pixelmapList 指针。如果中途某一帧解码失败,函数 return false 时 imageData 是局部变量,析构时自动释放已解出的帧,不会泄漏。

auto imageData = std::make_shared<ImageData>(pixelmapList, decoder->GetDelayTimeList(), frameCount);
for (int i = 0; i < frameCount; i++) {
    std::string errorInfo = decoder->DecodeFrame(i, &pixelmapList[i]);
    if (!errorInfo.empty()) {
        task->EchoError(errorInfo);
        return false;  // imageData 析构释放 pixelmapList
    }
}
task->product.imageData = imageData;

解码器类图

五、AvifApi 的 dlopen 动态导入

ImageKnifeDecoderAvif 内部嵌套了一个 AvifApi 单例类,负责在运行时通过 dlopen 加载 libavif.so.16。出于"AVIF 解码依赖系统是否安装了 libavif 动态库"的考虑,不能在编译期硬链接,否则在不带 libavif 的设备上程序直接加载失败。

AvifApi 的构造函数调 dlopen("libavif.so.16", RTLD_NOW)RTLD_NOW 表示立即解析所有符号——如果 so 内部有未解析的依赖,dlopen 阶段就会失败,而不是等到实际调用时才崩溃。dlopen 成功后批量 dlsym 导出 11 个 libavif 的公开接口:

std::vector<std::string> funcNames = {
    "avifDecoderCreate", "avifDecoderDestroy", "avifRGBImageFreePixels",
    "avifDecoderSetIOMemory", "avifDecoderParse", "avifDecoderNthImageTiming",
    "avifDecoderNthImage", "avifRGBImageSetDefaults", "avifRGBImageAllocatePixels",
    "avifImageYUVToRGB", "avifImageScale"
};
for (auto &funcName : funcNames) {
    void *func = dlsym(handle_, funcName.c_str());
    if (func == nullptr) {
        available_ = false;
    } else {
        apiMap_[funcName] = func;
    }
}

函数指针缓存在 apiMap_unordered_map<string, void*>)中。每个包装方法(如 AvifDecoderCreate())内部用 static auto func = GetApiFuncByName<...>(name) 取函数指针。static 局部变量保证每个函数指针只查一次 map,后续调用零开销直接走缓存好的裸指针。

如果 dlsym 有任何一个符号失败,available_ 设 false 并立即 dlclose(handle_) 释放句柄,不让半初始化的 so 占用资源。析构函数对 handle_ 做了空指针检查后再 dlclose,确保正常路径也能正确释放。

六、编译期控制与 mock 实现

AvifApi 内部类、avifDecoder_ 指针、avifRGBImage rgbImage_ 等成员全部包裹在 #ifdef IMAGE_KNIFE_ENABLE_AVIF_DECODER 条件编译内。没有这个宏时,头文件中 ImageKnifeDecoderAvif 只剩公开方法声明,没有任何 libavif 类型依赖。

CMakeLists.txt 通过 IMAGEKNIFE_USING_LIBAVIF 选项控制:

if (IMAGEKNIFE_USING_LIBAVIF)
    add_definitions(-DIMAGE_KNIFE_ENABLE_AVIF_DECODER)
    include_directories(imageknifepro PUBLIC
        ${NATIVERENDER_ROOT_PATH}/thirdparty/libavif/${OHOS_ARCH}/include)
    target_sources(imageknifepro PRIVATE decoder/imageknife_decoder_avif.cpp)
else()
    target_sources(imageknifepro PRIVATE decoder/imageknife_decoder_avif_mock.cpp)
endif()

不启用时编译的是 imageknife_decoder_avif_mock.cpp——所有方法都是空实现:IsAvifEnable() 返回 false,Init()DecodeFrame() 返回固定错误字符串 "ImageKnife Avif Decoder Not Enable"GetWidth()/GetHeight()/GetFrameCount() 返回 0。这保证了没有 libavif 头文件和库的环境下也能正常编译链接。

这种双重防线的设计:编译期通过 #ifdef 决定是否引入 avif 头文件和真实实现代码,解决"编译环境没有 libavif SDK"的问题;运行时通过 dlopen 决定是否有可用的 so 库,解决"目标设备没装 libavif 运行时"的问题。即便编译时开启了 AVIF 支持,到了不带 libavif 的设备上,dlopen 返回 nullptr,available_ 设 false,Loader 初始化时不挂载 AVIF 拦截器,整条链路安全降级。

七、DecodeFrame 的单帧解码

ImageKnifeDecoderAvif::DecodeFrame() 的四步流程构成了 AVIF 像素解码的核心。

第一步,AvifDecoderNthImage(decoder_, frameIndex) 定位到指定帧。libavif 的 avifDecoderNthImage 支持随机访问——AVIF 序列图的每一帧都是独立可寻址的,不需要从第 0 帧开始顺序解码。

第二步,检查是否需要缩放。如果 desiredWidth_desiredHeight_ 与原图尺寸不一致且不为 0,调 AvifImageScale() 在 YUV 域完成缩放。YUV 域缩放比先转 RGB 再缩放少一次完整的色彩空间转换,对大尺寸图片能省下可观的计算量。

第三步,YUV 转 RGB。AvifRGBImageSetDefaults() 根据 avifImage 初始化 RGB 参数,AvifRGBImageAllocatePixels() 分配像素缓冲区,AvifImageYUVToRGB() 执行 YUV-to-RGBA 转换。

第四步,CreatePixelmapNative() 把 RGBA 像素数据写进 OH_PixelmapNative。这里有一个位深适配处理:libavif 支持 10bit 和 12bit 图片,以 uint16 存储每个通道,但 OH_PixelmapNativePIXEL_FORMAT_RGBA_8888 只支持 8bit。CovertToRGBA8888() 通过右移操作把每个通道从高位深缩放到 8bit:

size_t byteGap = rgbImage.depth - 8;
outData[pos]     = data[pos]     >> byteGap;  // R
outData[pos + 1] = data[pos + 1] >> byteGap;  // G
outData[pos + 2] = data[pos + 2] >> byteGap;  // B
outData[pos + 3] = data[pos + 3] >> byteGap;  // A

比如 10bit 图片的 depth 是 10,byteGap 为 2,每个 uint16 值右移 2 位截取高 8 位。这种处理会丢失低位精度,但在 8bit 显示设备上差异肉眼不可见。创建 PixelMap 时同样优先尝试 DMA allocator 接口,不支持再回退 AUTO 模式。

以上就是本篇内容的所有了~有什么问题欢迎在评论区提出


项目地址:ImageKnifePro

ImageKnifePro 的图片来源不止 HTTP 一种。datashare:// 开头的图库图片、打包进 HAP 的 Resource 资源、应用沙箱里的本地文件、base64 字符串,以及业务方自己注册的 loader,都需要统一接入同一条加载管线。加载拦截器链上默认挂了两个实现——DownloadInterceptorDefault 处理网络下载,ResourceInterceptorDefault 处理本地和资源加载——通过 Resolve() 返回值决定谁来处理。

一、DownloadInterceptorDefault 的 RCP 下载

DownloadInterceptorDefault 在构造函数里创建了一个 Rcp_Session(成员变量 session_),整个应用生命周期内复用。同时维护了一个 std::stack<Rcp_Session*> 作为备用池,GetRcpSession() 先尝试从栈里弹出一个现成的 session,栈空就新建。用完后 RecycleSession() 压回栈,std::mutex 保护。

DownloadInterceptorDefault()
{
    name = "Default DownloadInterceptor";
    uint32_t errorCode = 0;
    session_ = HMS_Rcp_CreateSession(NULL, &errorCode);
    if (errorCode) {
        session_ = nullptr;
    }
}

RCP(Remote Communication Platform)是 HarmonyOS 系统层的网络栈,比 ArkTS 的 @ohos.net.http 少一层 NAPI 跨语言调用开销。在批量加载场景下吞吐量更高。

Resolve() 拿到 URL 后先做排除判断:以 /data/storagefile:data:image/ 开头的直接返回 false,让链上的下一个拦截器接手。空 URL 返回 false 并记录错误。只有标准的 HTTP/HTTPS URL 才进入 LoadImageFromUrl()

bool Resolve(std::shared_ptr<ImageKnifeTask> task) override
{
    std::string url;
    if (task->GetImageSource()->GetString(url)) {
        if (url.find("/data/storage") == 0 || url.find("file:") == 0 || url.find("data:image/") == 0) {
            return false;
        } else if (url.empty()) {
            task->EchoError("Empty Url");
            return false;
        }
        return LoadImageFromUrl(url, taskInternal);
    }
    return false;
}

LoadImageFromUrl() 创建 Rcp_Request,通过 ConfigHttpRequest 配置参数——HTTP headers、GET 方法、连接超时、传输超时、CA 证书路径、DNS 规则、下载进度回调、自动重定向。参数配置完成后调 HMS_Rcp_Fetch() 发起异步请求。

RCP 异步时序图

fetch 返回后立即调 Detach(task),当前线程不再等待下载完成。ResponseCallback 是一个静态函数,由 RCP 在下载完成或失败后回调。它拿到 Rcp_Response 后检查 statusCode,成功则把 response->body.buffer 包装成 shared_ptr<uint8_t[]> 写入 task->product

这里有一个精巧的内存管理细节——shared_ptr 的自定义删除器里调的是 response->destroyResponse(response),buffer 的生命周期由 RCP 管理,ImageKnifePro 只持有一个引用。当引用计数降到 0 时 RCP 才真正释放这块内存,避免了一次内存拷贝。

auto deleter = [response](void *) {
    response->destroyResponse(response);
};
task->product.imageBuffer = std::shared_ptr<uint8_t []>(
    (uint8_t *)response->body.buffer, deleter);
task->product.imageLength = response->body.length;

DNS 配置有三层优先级。ConfigDns() 先检查全局动态 DNS 规则(GetGlobalDynamicDnsRule()),有的话直接覆盖。全局动态 DNS 为空时检查请求级别的 DnsOptionInternal。请求级别也为空时用全局 DNS 配置。这三层覆盖关系保证了单个请求可以用独立的 DNS 策略,但又不丢全局默认值。

进度回调通过 Rcp_ConfigurationhttpEventsHandler.onDownloadProgress 注册,参数是 totalSizetransferredSize,只在主图请求时注册。回调函数很简练——transferredSize / totalSize 计算比例后调用用户传入的 progressListener

二、ResourceInterceptorDefault 的多路径读取

ResourceInterceptorDefault 在构造时把 isLoadFromRemote 设为 false,Process 不走 RetryFallbackUrls。它的 Resolve() 按优先级尝试四种来源。

第一种,Resource 对象。通过 task->GetImageSource()->GetResource(resource) 获取。LoadImageFromResource() 内部有三层兜底:先用 OH_ResourceManager_GetMedia() 按 ID 取,失败且 ID 为 -1 时用 GetBufferFromOtherModule() 按名称跨包取,再失败用 GetBufferFromRawfile() 从 rawfile 目录读。

bool Resolve(std::shared_ptr<ImageKnifeTask> task) override
{
    Resource resource;
    if (task->GetImageSource()->GetResource(resource)) {
        return LoadImageFromResource(resource, task);
    }
    std::string filePath = task->GetImageSource()->ToString();
    if (filePath.find("/data/storage") == 0) {
        return ReadFromLocal(task, filePath);
    }
    if (filePath.find("file:") == 0) {
        // file: URI 转本地路径
        char *pathResult = nullptr;
        OH_FileUri_GetPathFromUri(filePath.c_str(), filePath.size(), &pathResult);
        std::string localPath(pathResult);
        free(pathResult);
        return ReadFromLocal(task, localPath);
    }
    if (filePath.find("data:image/") == 0) {
        return DecodeBase64(task, filePath);
    }
    return false;
}

GetBufferFromOtherModule() 里的文件名提取逻辑是从 resource.param 字符串的最后一个 . 往后截取。resource.param 的格式是 [module].media.xxx,截取后得到资源名用于 OH_ResourceManager_GetMediaByNameGetBufferFromRawfile()OH_ResourceManager_OpenRawFile64 读取 rawfile 目录下的文件,适合图片资源没有注册到资源管理系统而是直接放在 rawfile 目录的情况。

第二种,本地沙箱路径。/data/storage 开头的路径走 ReadFromLocal(),用标准 C 的 stat/fopen/fread/fclose 读取。先用 stat 获取文件大小校验合法性,再 malloc 分配 buffer,fread 一次读完。buffer 用 shared_ptr 包装,自定义删除器里调 free

bool ReadFromLocal(std::shared_ptr<ImageKnifeTask> task, std::string &filePath)
{
    struct stat fileStat;
    if (stat(filePath.c_str(), &fileStat) == -1 || fileStat.st_size <= 0) {
        return false;
    }
    FILE *fd = fopen(filePath.c_str(), "r");
    if (fd == nullptr) return false;
    uint8_t *buffer = static_cast<uint8_t *>(malloc((fileStat.st_size + 1) * sizeof(uint8_t)));
    int64_t length = fread(buffer, sizeof(uint8_t), fileStat.st_size, fd);
    // ...
    task->product.imageBuffer = std::shared_ptr<uint8_t[]>(buffer, [](void *ptr) {
        free((uint8_t*)ptr);
    });
    task->product.imageLength = length;
    fclose(fd);
    return true;
}

第三种,file: URI。图库图片、跨应用共享的文件通常用这种协议。OH_FileUri_GetPathFromUri() 将 URI 转成本地路径后走 ReadFromLocal。这里 pathResult 指针必须用 free() 释放——HarmonyOS NDK 文档里明确说明了这一点,如果用 delete 会导致内存管理不匹配。

第四种,base64 字符串。data:image/ 开头,调 DecodeBase64()。解码过程:找到 ;base64, 分隔符位置,计算 base64 编码数据长度,分配输出 buffer(大小为 length * 3 / 4 + 1,因为 base64 每 4 个字符编码 3 个字节),调 libbase64 库的 base64_decode() 完成解码。

三、加载链上的两个拦截器如何协作

CreateDefaultImageLoader()DownloadInterceptorDefaultResourceInterceptorDefault 串在同一条加载链上:

loader->AddLoadInterceptor(downloadInterceptor);
loader->AddLoadInterceptor(resourceInterceptor);

downloadInterceptor 先添加所以在链头,resourceInterceptor 在链尾。请求进来后先走 DownloadInterceptorDefault。如果 URL 以 /data/storagefile:data:image/ 开头,Resolve 直接返回 false 跳过,基类 Processnext_ 自动把任务传给 ResourceInterceptorDefault。如果是 HTTP URL,走下载流程。下载失败时同样经过 next_ 传递,ResourceInterceptorDefault 会再尝试本地读取。

这个职责划分通过 URL 前缀硬编码实现。可以用更通用的 scheme 注册机制替代 if-else,但在图片加载场景下来源类型是有限且稳定的(HTTP、Resource、file、base64、sandbox),硬编码带来的确定性——不需要查注册表、不存在注册遗漏——比泛化更重要。

加载策略选择流程

四、进度回调的实现

进度回调只在网络下载场景有意义。ConfigHttpRequest 中检查 option->progressListener != nullptrtask->GetImageRequestType() == ImageRequestType::MAIN_SRC 时才注册。占位图和错误图的加载不需要进度通知。

if (option->progressListener != nullptr && task->GetImageRequestType() == ImageRequestType::MAIN_SRC) {
    config->tracingConfiguration.httpEventsHandler.onDownloadProgress = {
        .callback = OnProgressCallback,
        .usrObject = &(option->progressListener)
    };
}

OnProgressCallback 是一个静态函数,参数是 totalSizetransferredSize。RCP 在传输过程中持续调用它,频率取决于网络栈的内部缓冲策略。回调内容很简单——计算 transferredSize / totalSize 的比例,调用用户传入的 std::function<void(double)>

进度值是 0 到 1 之间的 double,而不是百分比整数。这样做的好处是精度更高,UI 层可以自己决定怎么展示——圆形进度条可能需要 0-360 度的角度,百分比文字可能需要 0-100 的整数,都从 0-1 转换即可。

五、RegisterLoader——自定义加载管线

RegisterLoader() 提供了比单个拦截器更粗粒度的定制能力:

void ImageKnifeInternal::RegisterLoader(std::string name,
                                         std::shared_ptr<ImageKnifeLoader> loader) {
    if (loader != nullptr) {
        loaderMap_[name] = loader;
    }
}

注册进 loaderMap_ 的是一个完整的 ImageKnifeLoader 对象,包含自己的内存缓存拦截器、文件缓存拦截器、下载拦截器和解码拦截器——四条链可以完全独立配置。ArkTS 层的 ImageKnifeOption 通过字符串指定要用哪个 loader,NAPI 解析层调 GetRegisterLoader(name) 从 map 中查找。

这种方式允许注册多个不同用途的 loader。一个走 CDN 下载并使用 WebP 解码,另一个走内网专线并使用自研格式解码,在请求级别按名称切换,互不干扰。和单个拦截器的插入相比,RegisterLoader 适合需要完全控制加载流程的场景——比如某个业务线的图片需要经过特殊的鉴权、解密、解码全套流程,和默认管线没有任何交集。

六、取消机制的竞态处理

取消操作在 Detach 场景下需要处理竞态。DownloadInterceptorDefault::Cancel() 先加 cancelMutex_ 锁把 rcpRequestCanceled 标记为 true,取出 rcpRequest 指针后解锁,再通过 HMS_Rcp_CancelRequest() 中断请求。

void Cancel(std::shared_ptr<ImageKnifeTask> task) override
{
    auto taskInternal = std::static_pointer_cast<ImageKnifeTaskInternal>(task);
    taskInternal->cancelMutex_.lock();
    taskInternal->rcpRequestCanceled = true;
    auto rcpRequest = taskInternal->rcpRequest;
    taskInternal->rcpRequest = nullptr;
    taskInternal->cancelMutex_.unlock();
    if (rcpRequest != nullptr && session_ != nullptr) {
        HMS_Rcp_CancelRequest(session_, rcpRequest);
    }
}

LoadImageFromUrl() 里 fetch 和 task->rcpRequest = rcpRequest 赋值之间存在时间窗口——如果 Cancel 在这个窗口内被调用,因为 rcpRequest 还是 null,Cancel 无法中断请求。所以赋值后会再检查一次 rcpRequestCanceled,如果为 true 就补调 Cancel。两把锁分开操作——sessionLock_ 保护全局 session,cancelMutex_ 保护单个任务状态——避免嵌套加锁导致的死锁风险。

ResponseCallback 中也做了对称处理:加锁取出 rcpRequest 并置空,如果请求被取消(rcpRequest 为 null),由取消方负责销毁;否则由回调方销毁。无论哪条路径,DestroyConfigurationHMS_Rcp_DestroyRequest 都只会被调用一次。

以上就是本篇内容的所有了~有什么问题欢迎在评论区提出


项目地址:ImageKnifePro

先放 ip 地址: 2607:8140:212:106::/64

要屏蔽某个 ip 地址应该在出境的路由器上做屏蔽,就算有地区墙省墙也不会出现一整个运营商都挂掉其他家没事的情况

983936a1c25462625df60c0811483f03.png

但碰到一个日本的 ipv6 地址被电信整个墙了,连 icmp 都不通的那种,traceroute 只能看到一跳就没了

traceroute to 2607:8140:212:106:: (2607:8140:212:106::), 30 hops max, 72 byte packets
 1  xxxxxxxxxxxx  1.303 ms
 2  *
 3  *
 4  *
 5  *
 6  *
 7  *
 8  *
 9  *
10  *
11  *
12  *
13  *
14  *
15  *
16  *
17  *
18  *
19  *
20  *
21  *
22  *
23  *
24  *
25  *
26  *
27  *
28  *
29  *
30  *

这是新型的墙的方式吗?还是说电信方向的路由炸掉了

最近搞了个小项目,给一段 prompt 加一份长文档,自动出一版可以继续编辑的 PPT 草稿。本来以为最难的部分是 prompt 设计或者 PPT 渲染,结果 80% 的时间都耗在了文档解析上,记录一下踩过的坑,顺便想请教下各位在这块都是怎么处理的。

场景大概是这样:用户上传的资料是产品白皮书、研究报告、需求文档之类的,几十页起步,PDF 居多,也有 Word 和 Excel 。要做的事其实就一句话——把内容读懂,然后让 LLM 出一版 outline 加各页要点。

第一版我懒,直接把 PDF 转 base64 丢给 Gemini ,反正它号称百万 token 。跑了几次发现:

  • 表格里的数字模型经常算错,碰到一份白皮书把"营收"和"利润"两列加在一起算了
  • 章节层级基本崩,3.2 节的内容会跑到附录里去
  • 模型自由发挥的成分肉眼可见地高,幻觉很重
  • token 烧得也猛,账单看一眼就关了

第二版自己写解析。pdf.js + mammoth + SheetJS 一套全上。理论上很美好,跑起来就是另一回事:

  • pdf.js 出来就是流水账文本,标题正文连段落都断不准
  • 表格被压成空格分隔的字符串,模型一看就开始胡编
  • 图片直接没了
  • 多 sheet 的 Excel 还没来得及处理,docx 里的嵌套列表层级先丢了一半

写到第二天凌晨看着满屏 if/else 兼容代码我开始怀疑这条路。这事本身就不是手搓能搞定的,它涉及版面识别、OCR 、表格还原、章节关系恢复,本质上是一个独立的工程问题,不是周末项目能糊出来的。

然后开始看现成的方案,目前我试过和了解过的:

  • LlamaParse:表格处理还行,国内访问要折腾代理,定价对小项目不算友好
  • Unstructured:开源,自己部署。能跑,复杂表格的还原一般,要自己写一层 post-processing
  • Knowhere:API 形式,异步 job 模型(创建 job → 拿 upload_url → PUT 文件 → 轮询 → 下载 ZIP )。返回 chunks 带 type / 层级 / 页码 / bbox ,表格是 HTML ,图片单独抽出来还带摘要。我目前接的这个,主要图省事,省了自己做 OCR 和表格还原的活。缺点是 fallback 到自有 pipeline 比较麻烦,要做缓存层
  • 某些大厂的 OCR / 文档智能 API:识别准但定位偏文字抽取,结构化这部分还得自己拼
  • MinerU:开源里口碑不错,但部署起来对 GPU 有要求,小项目跑不太起

选完之后问题没全解决,下游怎么用结构化结果也得想。我现在的做法:

  • chunks 持久化进 Postgres ,每个 chunk 单独存,方便后面按需引用
  • 喂给 LLM 的优先是 sections + table summaries + image highlights ,原文只在事实核对的时候按需调
  • 解析这步包成异步 workflow (用 Vercel Workflow ),失败可重试,命中缓存直接复用

这套改完之后同一份白皮书重新跑,10 页 PPT 之前有 4 页跑偏、2 个数据错,现在基本对得上原文。表格那块改善最明显,之前我都不敢让 LLM 直接看原始表格。

写下来还有几个事情没想清楚,想请教下大家:

  1. 多文档场景下,cross-document 的引用和对比怎么处理?现在简单按 section 对齐,但两份报告对同一指标给出不同结论这种,很难自动对得上
  2. 大表格(几百行)整块塞 prompt 里 token 吃不消,有没有比较成熟的"按列采样"或者"先 summary 再 drill down"的做法
  3. 自己拼开源 pipeline vs 直接调 API ,长期成本你们怎么算的?我现在用 API 主要是图省事,但跑量上去之后心里没底
  4. async job 这种模式,前端轮询和 SSE 你们更倾向哪种?我现在是混着来的,感觉不太干净

技术栈顺手记一下:Next.js 16 + Bun + Turborepo + oRPC + Drizzle + Postgres + Vercel AI SDK + Vercel Blob 。

https://github.com/Ontos-AI/knowhere-pitchpilot

前言

经过几个月的精心打磨,小遥搜索 XiaoyaoSearch v2.0.0 今天正式发布了!

这是一个重大版本更新,带来了全新的 Notion 温暖明亮设计风格,完整的设计系统规范,以及全面的UI 视觉升级

相比 v1.x 版本,v2.0.0 不仅保留了强大的多模态 AI 搜索能力,更在视觉体验上实现了质的飞跃


v2.0.0 有什么不同?

🎨 全新设计系统

Notion 温暖明亮设计风格

借鉴 Notion 的设计理念,打造专业、舒适、高效的视觉体验:

  • 纯白背景 #ffffff + 温暖白 #f6f5f4:营造干净舒适的视觉环境
  • 品牌蓝色 #0075de:统一品牌色调,提升识别度
  • 超细边框 rgba(0,0,0,0.1):柔和过渡,精致细节
  • 多层阴影堆叠:4-5 层阴影,增强视觉层次感

视觉体验提升数据

指标 v1.x v2.0 提升
视觉舒适度 60% 100% +67%
品牌识别度 50% 100% +100%
信息可读性 60% 100% +67%
专业度感知 60% 100% +67%


🔤 系统字体栈(零网络依赖)

告别 Google Fonts ,拥抱系统字体

v1.x 版本使用 Google Fonts CDN 加载字体,存在网络依赖和性能问题。v2.0.0 采用精心挑选的系统字体栈:

font-family: 
  -apple-system,           /* Apple 系统字体 */
  BlinkMacSystemFont,       /* macOS Chrome */
  "SF Pro Text",            /* macOS 专业字体 */
  "Segoe UI",               /* Windows 系统字体 */
  "PingFang SC",            /* Apple 中文字体 */
  "Microsoft YaHei",        /* Windows 中文字体 */
  sans-serif;               /* 后备字体 */

性能对比

指标 v1.x ( Google Fonts ) v2.0 (系统字体) 提升
字体加载时间 200-500ms <10ms +98%
首屏渲染时间 2.0-2.5s <2.0s +20%
网络请求数 +2 个 0 个 +100%
离线可用性


🎯 Lucide Icons 图标系统

1000+ 图标,统一视觉语言

采用 Lucide Icons 作为标准图标库,通过 @iconify/vue 集成:

  • 图标数量:1000+ 图标,持续更新
  • 设计风格:现代简洁,线条优雅
  • 可定制性:支持颜色、大小、粗细调整
  • 按需加载:自动按需加载,无性能损耗


📐 完整设计系统规范

CSS Variables + Design Tokens

建立完整的设计令牌系统,确保设计一致性:

/* 颜色系统 */
--bg-primary: #ffffff;              /* 主背景 */
--bg-secondary: #f6f5f4;            /* 次级背景 */
--text-primary: rgba(0,0,0,0.95);   /* 主文字 */
--text-secondary: #615d59;          /* 次要文字 */
--brand-blue: #0075de;              /* 品牌蓝色 */
--border-standard: rgba(0,0,0,0.1); /* 标准边框 */

/* 间距系统( 8px 基准) */
--space-xs: 4px;
--space-sm: 8px;
--space-md: 12px;
--space-lg: 16px;
--space-xl: 20px;
--space-2xl: 24px;
--space-3xl: 32px;

/* 圆角系统 */
--radius-sm: 4px;
--radius-md: 6px;
--radius-lg: 10px;
--radius-xl: 12px;

/* 阴影系统 */
--shadow-card: 0 1px 2px rgba(0,0,0,0.04), 0 2px 4px rgba(0,0,0,0.04),
               0 4px 8px rgba(0,0,0,0.04), 0 8px 16px rgba(0,0,0,0.04);
--shadow-elevated: 0 2px 4px rgba(0,0,0,0.05), 0 4px 8px rgba(0,0,0,0.05),
                  0 8px 16px rgba(0,0,0,0.05), 0 16px 32px rgba(0,0,0,0.05);


✨ 所有页面 UI 升级

10 个页面/组件全面升级

页面/组件 主要升级
顶部导航栏 Logo 、导航菜单、右侧操作区全新设计
底部状态栏 索引统计、搜索统计、数据统计信息展示
搜索首页 搜索区域、多模态指示器、结果列表精致卡片
搜索结果卡片 卡片样式、图标、标签、操作按钮
设置页面 标签页、表单、开关全新设计
索引管理页面 统计卡片、索引列表、模态框样式升级
术语库管理页面 列表、创建/编辑模态框、表单优化
术语管理页面 列表、CSV 导入、表单样式全面升级
帮助页面 FAQ 、折叠面板、搜索功能升级
关于作者页面 作者信息、联系方式、二维码展示优化


小遥搜索是什么?

简单来说,它是一个本地 AI 搜索桌面应用,核心特点:

🎤 多模态输入

  • 语音搜索:点一下录音,说出你要找的内容,30 秒内语音自动转文字搜索
  • 文本搜索:输入关键词,AI 理解语义,精准匹配文件内容
  • 图片搜索:上传一张图片,AI 理解图像内容,帮你搜索相关文件

🔍 深度检索

  • 文档:TXT 、Markdown 、Word 、Excel 、PPT 、PDF 全文检索
  • 音视频:MP4 、AVI 、MP3 、WAV 内容索引和搜索
  • 文件名:传统的文件名搜索也不缺席

🧠 AI 技术

集成了多个先进 AI 模型:

  • BGE-M3:文本嵌入,理解语义
  • FasterWhisper:语音识别,语音转文字
  • CN-CLIP:图像理解,以图搜图
  • Ollama:本地大语言模型
  • OpenAI/DeepSeek/阿里云:云端大模型(可选)

🎨 v2.0.0 全新 UI

  • Notion 温暖明亮设计风格:专业、舒适、高效
  • 系统字体栈:零网络依赖,加载速度 < 10ms
  • Lucide Icons:1000+ 图标,统一视觉语言
  • 完整设计系统:确保设计一致性

🔒 隐私安全

  • 完全本地:所有数据处理都在本地,不上传云端
  • 隐私模式:可选不记录搜索历史
  • 自主可控:数据完全由你自己掌控


核心界面一览

搜索主界面( v2.0.0 全新设计)

设计亮点

  • 居中布局,视觉焦点集中
  • 多模态指示器,清晰展示输入模式
  • 搜索框圆角设计,蓝色光晕效果
  • 结果卡片精致阴影,悬停加深

文本搜索

功能特点

  • 实时搜索结果展示
  • 匹配度高亮显示
  • 支持语义理解和关键词匹配
  • 一键打开文件或定位文件夹

语音搜索

功能特点

  • 30 秒内语音录制
  • 自动语音转文字
  • 支持中英文混合输入
  • 智能语义理解

图片搜索

功能特点

  • 上传图片即可搜索
  • AI 理解图像内容
  • 支持相似图片匹配
  • 以图搜图,精准定位

索引管理界面( v2.0.0 全新设计)

设计亮点

  • 统计卡片,悬停提升效果
  • 索引列表,精致阴影堆叠
  • 操作菜单,流畅动画过渡

功能特点

  • 实时索引状态监控
  • 文件夹路径管理
  • 一键重建索引
  • 索引进度可视化

设置界面( v2.0.0 全新设计)

设计亮点

  • 标签页,次级背景,激活状态清晰
  • 表单布局,标签左对齐,必填标记红色
  • 开关控件,品牌蓝色,平滑过渡

功能特点

  • AI 模型配置
  • 数据源管理
  • 搜索偏好设置
  • 界面主题选择

术语库管理界面( v2.0.0 新增)

功能亮点

  • 多术语库集合管理
  • 术语同义词扩展
  • CSV 导入/导出
  • 预置专业术语库( IT 、产品、医疗、法律)

使用场景

  • 产品经理:PRD 、产品需求文档、产品规格书
  • 医生:CT 、计算机断层扫描、CT 扫描
  • 开发者:API 、应用程序接口、接口文档
  • 法律从业者:合同、协议、契约


技术架构

v2.0.0 技术亮点

新增技术栈

  • @iconify/vue:图标按需加载系统
  • CSS Variables:完整设计令牌系统
  • Design Tokens:TypeScript 类型定义
  • 系统字体栈:零网络依赖字体方案

性能优化

  • 字体加载时间 < 10ms ( vs 200-500ms )
  • 首屏渲染时间 < 2.0s ( vs 2.0-2.5s )
  • 网络请求数 -2 个(移除 Google Fonts )
  • 离线可用性 ✅


版本对比:v1.x vs v2.0.0

设计对比

方面 v1.x v2.0.0
设计风格 Ant Design 默认风格 Notion 温暖明亮风格
字体方案 Google Fonts (网络依赖) 系统字体栈(零依赖)
图标系统 Ant Design Icons ( 300+) Lucide Icons ( 1000+)
阴影效果 简单阴影 4-5 层堆叠阴影
边框样式 标准边框 超细边框( whisper )
圆角系统 混合使用 统一规范( 4/6/10/12px )
动画效果 基础过渡 精致动画( fadeInUp 、hover )
响应式设计 部分支持 完整支持(桌面/平板/移动)

功能对比

功能 v1.x v2.0.0
多模态搜索
本地文件检索
AI 模型配置
插件化架构
数据源扩展 ✅ 语雀、飞书、钉钉 ✅ + 语雀、飞书、钉钉
术语库系统 ✅ + UI 升级
MCP 服务器
Agent Skills
UI 视觉系统 基础 全面升级


快速体验

环境要求

  • 操作系统:Windows / MacOS / Linux
  • Python:3.10.11+
  • Node.js:21.x+
  • 内存:建议 16GB 以上( 8GB 可用)
  • 显卡:建议 RTX3060 6GB 以上(可选)

安装步骤

方式一:整合包部署(推荐)

下载整合包

从百度网盘下载最新的 Windows 整合包:

安装步骤

  1. 解压整合包到任意目录
  2. 运行 scripts/setup.bat 自动安装依赖
  3. 安装并启动 Ollama
  4. 下载 AI 模型
  5. 运行 scripts/startup.bat 启动应用

方式二:开发者部署

1. 克隆项目

git clone https://github.com/dtsola/xiaoyaosearch.git
cd xiaoyaosearch

2. 后端启动

cd backend
pip install -r requirements.txt
python main.py

3. 前端启动

cd frontend
npm install
npm run dev

详细安装指南:https://github.com/dtsola/xiaoyaosearch/blob/main/README.md


产品路线图

当前版本( v2.0.0 )✅

核心功能

  • ✅ 多模态 AI 搜索(语音、文本、图片)
  • ✅ 本地文件深度检索
  • ✅ AI 模型灵活配置(本地/云端)
  • Notion 温暖明亮设计风格
  • 系统字体栈(零网络依赖)
  • Lucide Icons 图标系统( 1000+)
  • 完整设计系统规范
  • ✅ 插件化架构(语雀、飞书、钉钉)
  • ✅ 专业术语库系统
  • ✅ MCP 服务器支持
  • ✅ Agent Skills 支持

UI 升级亮点

  • ✅ 所有页面/组件 UI 全面升级
  • ✅ 视觉舒适度提升 67%
  • ✅ 品牌识别度提升 100%
  • ✅ 信息可读性提升 67%
  • ✅ 专业度感知提升 67%

未来规划

🎨 UI/UX 持续优化

  • 暗色模式支持(用户呼声最高的功能)
  • 自定义主题配色
  • 更多动画效果和过渡

🚀 性能优化

  • 大文件处理优化
  • 索引构建速度提升
  • 内存占用优化

🔌 更多数据源支持

  • Notion 知识库
  • GitHub 代码仓库
  • GitLab 代码仓库
  • Confluence Wiki

🧠 AI 能力增强

  • 自适应分块算法
  • 知识图谱构建
  • RAG 系统升级

💬 智能聊天助手

  • 多轮对话和上下文记忆
  • 基于本地知识库的问答
  • 自然语言交互

详细路线图:https://github.com/dtsola/xiaoyaosearch/blob/main/ROADMAP.md


适合谁使用?

知识工作者

搜索本地文档、笔记、研究报告,快速定位关键信息

使用场景

  • 搜索产品需求文档( PRD )
  • 查找技术方案和架构文档
  • 检索会议记录和决策文档
  • 整理项目资料和学习笔记

内容创作者

搜索素材和灵感,整理音视频内容,管理创作资源

使用场景

  • 搜索视频脚本和分镜
  • 查找音频素材和音效
  • 整理设计素材和灵感
  • 管理项目资源和交付物

技术开发者

搜索代码库和技术文档,整理学习资料,管理项目文件

使用场景

  • 搜索代码片段和技术实现
  • 查找 API 文档和使用示例
  • 整理学习资料和教程
  • 管理项目文件和依赖

研究人员

搜索论文和研究资料,整理文献笔记,管理研究数据

使用场景

  • 搜索论文和参考文献
  • 查找实验数据和结果
  • 整理文献笔记和观点
  • 管理研究项目资料


为什么选择小遥搜索?

🎯 专为知识工作者设计

  • 多模态输入:语音、文本、图片,用最自然的方式搜索
  • 语义理解:AI 理解你的意图,不只是关键词匹配
  • 深度检索:搜索文件内容,不只是文件名

🔒 隐私安全优先

  • 100% 本地:所有数据处理都在本地,不上传云端
  • 隐私模式:可选不记录搜索历史
  • 自主可控:数据完全由你自己掌控

🧠 AI 技术领先

  • 多模型集成:BGE-M3 + FasterWhisper + CN-CLIP + Ollama
  • 云端可选:支持 OpenAI/DeepSeek/阿里云等云端模型
  • 持续升级:AI 模型和技术持续更新

🎨 v2.0.0 视觉体验

  • 温暖明亮:Notion 设计风格,专业舒适
  • 精致细节:多层阴影、超细边框、流畅动画
  • 品牌识别:统一的视觉语言,提升专业度
  • 高性能:零网络依赖,加载速度极快

🤖 开发者友好

  • 100% 开源:所有源码、文档、开发经验全部开源
  • 插件化架构:支持自定义扩展和数据源
  • MCP 支持:可被 Claude Desktop 等 AI 应用连接
  • Vibe Coding 案例:完整的 AI 辅助开发实践


Vibe Coding 实践案例

100% AI 辅助开发

这个项目从零开始,完全通过 Vibe Coding 实现,包括:

  • ✅ 完整源代码(前端 + 后端 + 所有功能模块)
  • ✅ 设计文档( PRD 、技术方案、数据库设计、API 文档)
  • ✅ 开发流程(任务分解、进度跟踪、测试验证)
  • ✅ 部署配置(环境搭建、依赖管理、打包发布)
  • v2.0.0 UI 视觉系统全面升级

开源的价值

对于想要学习以下内容的开发者,这是一个完整的参考实现:

  • AI 辅助开发实践:如何用 AI 实现完整项目
  • Electron 桌面应用:从零构建桌面应用
  • Vue 3 前端开发:Composition API + TypeScript
  • Python FastAPI 后端:异步架构 + AI 模型集成
  • 设计系统构建:从零建立完整设计规范
  • 插件化架构:可扩展的插件系统设计
  • MCP 协议实现:AI 应用集成最佳实践


项目亮点总结

技术亮点

  1. 多模态 AI 搜索:语音、文本、图片三种输入方式
  2. 深度文件检索:支持文档、音视频的内容和文件名搜索
  3. AI 模型集成:BGE-M3 、FasterWhisper 、CN-CLIP 、Ollama
  4. 插件化架构:支持语雀、飞书、钉钉等数据源
  5. MCP 协议支持:可被 Claude Desktop 等 AI 应用连接
  6. v2.0.0 UI 视觉升级:Notion 设计风格 + 完整设计系统

设计亮点( v2.0.0 )

  1. Notion 温暖明亮风格:专业、舒适、高效
  2. 系统字体栈:零网络依赖,最优性能
  3. Lucide Icons:1000+ 图标,统一视觉语言
  4. 多层阴影堆叠:4-5 层精致阴影
  5. 完整设计系统:确保设计一致性
  6. 响应式设计:完美适配各种设备

开源亮点

  1. 100% AI 辅助开发:完整的 Vibe Coding 实践案例
  2. 完整技术栈:前端 + 后端 + AI 模型
  3. 详尽文档:PRD 、技术方案、API 文档、数据库设计
  4. 插件化架构:可扩展的插件系统设计
  5. 持续更新:活跃开发,功能持续增强


邀请你参与

为什么需要你?

一个人的力量有限,开源社区的力量是无限的!

优先贡献方向

🎨 UI/UX 优化(高优先级)

  • 暗黑模式支持(用户呼声最高)
    • 设计暗黑主题配色方案
    • 实现主题切换功能
    • 优化暗黑模式下的视觉体验
  • 自定义主题配色
    • 允许用户自定义品牌色
    • 提供多种预设配色方案
    • 支持导入导出主题配置
  • 更多动画效果
    • 页面切换动画
    • 加载动画优化
    • 微交互动画效果

🔌 更多数据源支持(高优先级)

  • Notion 知识库
    • Notion API 集成
    • 元数据解析
    • 原文链接跳转
  • GitHub/GitLab
    • 代码仓库搜索
    • Wiki 文档索引
    • Issue 和 PR 检索

⚡ 性能优化(中优先级)

  • 大文件处理优化
    • 分块索引策略
    • 流式读取优化
    • 内存占用控制
  • 索引构建速度提升
    • 并行索引构建
    • 增量索引更新
    • 智能缓存机制

🧪 测试覆盖(中优先级)

  • 单元测试补充
    • 核心模块测试覆盖
    • 边界条件测试
    • 错误处理测试
  • 集成测试完善
    • 端到端测试场景
    • 跨平台兼容性测试
    • 性能基准测试

如何贡献?

# 1. Fork 项目
git clone https://github.com/dtsola/xiaoyaosearch.git
cd xiaoyaosearch

# 2. 创建分支
git checkout -b feature/your-feature-name

# 3. 进行开发
# 按照项目代码规范开发
# 确保代码有适当注释
# 运行测试确保功能正常

# 4. 提交代码
git add .
git commit -m "feat(scope): 简洁描述你的改动"
git push origin feature/your-feature-name

# 5. 提交 Pull Request
# 在 GitHub 上创建 PR ,详细描述改动内容

贡献者权益

  • 📝 在贡献者列表中展示你的名字
  • 🏆 对项目有重大贡献者可成为核心维护者
  • 💼 优秀贡献者可获得推荐信或工作机会
  • 🎁 顶级贡献者可获得项目周边礼品


项目地址

GitHubhttps://github.com/dtsola/xiaoyaosearch

欢迎:

  • ⭐️ Star 本项目,关注最新进展
  • 🍴 Fork 本项目,开始你的贡献
  • 👀 Watch 本项目,及时获取更新
  • 🐛 提 Issue,报告问题和建议功能
  • 💡 参与讨论,分享你的想法


关于我

dtsola - IT 解决方案架构师 | 一人公司实践者


开源协议

本项目采用小遥搜索软件授权协议

  • ✅ 免费使用(非商业用途)
  • ✅ 可以学习和研究代码
  • ✅ 可以修改后二次分发(需保留版权声明和协议)
  • ✅ 可以集成到其他非商业项目
  • ❌ 商业使用需授权

这是一个类似 CC-BY-NC-SA 的开源协议,鼓励学习、分享和贡献!

详细协议:https://github.com/dtsola/xiaoyaosearch/blob/main/LICENSE


版本历史

v2.0.0 (2026-04-24) - UI 视觉系统升级版 🎨

核心升级

  • ✅ Notion 温暖明亮设计风格
  • ✅ 系统字体栈(零网络依赖)
  • ✅ Lucide Icons 图标系统( 1000+)
  • ✅ 完整设计系统规范
  • ✅ 所有页面 UI 全面升级( 10 个页面/组件)
  • ✅ 响应式设计完善

v1.9.0 (2026-04-12) - 搜索优化版

  • ✅ 术语扩展时机优化
  • ✅ 智能合并去重
  • ✅ 并发控制优化
  • ✅ 专业术语召回率提升 60%

v1.8.0 (2026-04-08) - 钉钉文档支持

  • ✅ 钉钉文档数据源支持
  • ✅ 元数据文件解析(.xyddjson 格式)
  • ✅ 原文链接跳转功能

v1.7.0 (2026-03-31) - 飞书文档支持

  • ✅ 飞书文档数据源支持
  • ✅ 元数据块解析
  • ✅ 原文链接跳转功能

v1.6.0 (2026-03-26) - 云端嵌入模型

  • ✅ 云端嵌入模型调用能力
  • ✅ 本地/云端互斥切换
  • ✅ 批量文本嵌入优化

v1.5.0 (2026-03-20) - Agent Skills

  • ✅ MCP Agent Skills 支持
  • ✅ 为 Claude Code/VS Code/Cursor 提供工具调用能力

v1.4.0 (2026-03-15) - MCP 服务器支持

  • ✅ MCP 协议支持
  • ✅ 可被 Claude Desktop 连接
  • ✅ SSE 传输方式

v1.3.0 (2026-03-10) - OpenAI 云端模型

  • ✅ OpenAI 兼容云端大模型
  • ✅ 动态表单配置
  • ✅ API 密钥加密存储

v1.2.0 (2026-03-05) - 插件化架构

  • ✅ 插件化架构框架
  • ✅ 语雀知识库数据源

v1.1.0 (2026-02-28) - i18n 国际化

  • ✅ 中英文双语支持
  • ✅ i18n 框架集成

v1.0.0 (2026-02-20) - MVP 版本

  • ✅ 多模态搜索(文本/语音/图像)
  • ✅ 本地文件深度检索
  • ✅ BGE-M3 + FasterWhisper + CN-CLIP
  • ✅ Faiss + Whoosh 混合搜索


结语

小遥搜索 v2.0.0 是我们对本地 AI 搜索工具的一次重大升级。

v1.x 时代:我们实现了核心的 AI 搜索能力,打造了稳定可用的本地搜索工具。

v2.0.0 时代:我们全面升级了视觉体验,建立了完整的设计系统,让产品不仅强大,而且美观易用。

未来:我们会持续优化性能,增加更多数据源,引入更多 AI 能力,让小遥搜索成为知识工作者的必备工具。

无论你是:

  • 🔨 想要贡献代码的开发者
  • 💡 想要提供建议的产品经理
  • 📖 想要学习 AI 应用的学生
  • 🚀 想要参与创业的伙伴

都欢迎加入我们,一起打造更好的本地 AI 搜索工具!

让我们一起,用 AI 技术改变知识管理方式! 🚀


跨境物流是反向海淘运营的核心痛点之一,涉及国内集货、跨境运输、海外派送等多个环节,流程复杂、不确定性强,且物流信息不透明、订单跟踪繁琐,不仅影响运营效率,还容易引发客户投诉。对于技术从业者而言,手动对接物流渠道、跟踪物流轨迹,不仅耗时耗力,还容易出现数据错误;而自主开发物流对接系统,需要对接多个物流平台API,开发周期长、维护成本高,不符合轻量化副业的需求。taoify作为反向海淘专用SaaS工具,已内置跨境物流对接功能,实现物流全流程自动化,同时开放物流API接口,支持技术从业者进行个性化拓展,彻底解决跨境物流的运营痛点。
从技术实现角度来看,taoify的跨境物流对接功能,核心基于物流API对接、数据同步与自动化跟踪技术,其底层逻辑分为三个核心模块:物流渠道管理模块、物流轨迹跟踪模块、物流数据统计模块,各模块协同工作,实现跨境物流全流程自动化运营。
物流渠道管理模块,是实现物流自动化的基础。taoify已提前对接全球主流跨境物流渠道(如邮政小包、国际快递、专线物流等),无需用户手动对接物流平台、申请物流账号,仅需在后台选择适配的物流渠道,完成账号授权,即可实现物流渠道的快速对接。同时,该模块支持物流渠道自定义配置,技术从业者可基于自身运营需求,对接个性化物流渠道,通过taoify的开放API,实现物流渠道的灵活拓展与管理。
物流轨迹跟踪模块,是提升客户体验、降低运营成本的核心。taoify通过对接各物流平台的API接口,实现物流轨迹的实时抓取、同步与展示,用户下单后,系统会自动生成物流单号,绑定订单信息,物流轨迹实时更新,运营者可在后台统一查看所有订单的物流状态,无需手动查询;同时,系统会自动向客户推送物流节点通知(如订单发货、包裹入境、包裹派送等),让客户随时了解包裹进度,减少客户咨询频次,提升客户体验。对于技术从业者而言,可基于taoify的物流API,开发个性化物流跟踪功能,如物流轨迹可视化、异常物流自动提醒等,进一步提升运营效率。
物流数据统计模块,为运营决策提供数据支撑。taoify会自动统计物流相关数据(如物流时效、物流成本、包裹破损率、异常物流数量等),生成可视化数据分析报表,运营者可通过报表,清晰了解各物流渠道的运营效果,优化物流渠道选择,降低物流成本、提升物流效率。技术从业者可基于该模块的开放API,开发自定义数据分析脚本,深入分析物流数据,为物流优化提供更精准的决策支撑。
此外,taoify的物流对接功能还具备容错机制,若某一物流渠道API出现异常,系统会自动切换至备用物流渠道,确保物流运营的连续性;同时具备物流成本自动核算功能,根据订单重量、目的地、物流渠道,自动计算物流费用,便于运营者控制成本。对于兼职副业的技术从业者而言,无需投入大量时间处理物流对接与跟踪问题,借助taoify的物流自动化功能,即可实现跨境物流全流程高效运营,将时间精力聚焦于核心运营环节。

编者按:

过去一年,企业 AI Coding 的讨论往往集中在模型能力、部署成本与合规约束上。DeepSeek V4 的出现,的确让私有化部署首次拥有了接近闭源旗舰的现实选项,也部分缓解了中国企业长期面临的工具死锁。但模型问题缓解之后,更深层的约束随之浮现:代码库中的业务隐知识、历史决策与架构习惯,并不会因为模型升级而自动变得可理解。本文借“AI 上下文负债”这一概念提醒我们,AI 编程的真正难点,正在从模型供给侧转向组织治理侧——从选工具,转向补文档、立规范、清理历史欠账。

对企业而言,接下来的竞争不只是接入 AI 的速度,更是谁更早完成知识治理、工程规范和渐进重构。

去年秋天,一个朋友所在的上市公司开始推动 AI 辅助编程。安全部门花三个月审了五款工具,结论是不能用——数据要出内网。IT 部门转而自研,装上了内部 GPU 集群,部署了一个半年前开源的大模型,在 IDE 里接了一个对话插件。研发团队用了一周,没人再打开了。

他说:“你试过让一个不了解你代码库的 AI 帮你修 bug 吗?就像叫一个刚下飞机的出租车司机走一条他连路口都没见过的巷子。”

这不是一个模型能力问题。那款私有化部署的模型写标准 API、生成单元测试、补全常规逻辑,能力够用。问题在于它面对的代码库是一个维护了九年的财务后台系统。数据库表名是八年前两个已离职的项目经理起的,订单状态不靠主表字段判断而要查日志表最后一条关联记录,核心业务规则一部分在存储过程里、一部分散落在三百多个 Controller 文件中。没有任何地方把这些规则完整记下来过。

给这样一个系统加“部分退款”功能,AI 会建一个干净的 refund 表、写标准 CRUD、关联订单 ID——代码组织得挺好。审查的人必须逐行比对:它知不知道退款要同时写三张表才能保证财务对账?知不知道该业务有个隐藏规则——发货超三十天的订单走人工通道?都不知道。生成的代码语法完美、业务上下文里错得不着痕迹。

代码越混乱,AI 的效率提升越可疑——审查成本的增长速度很可能超过了生成速度的节省。

AI 上下文负债

今年四月,科技从业者 Abbas Raza 在一篇博文里将这个现象命名为 AI 上下文负债(AI context debt):代码库知道关于自己的信息,与 AI 工具需要知道才能生成正确输出所需信息之间的缺口。

这个概念解释了一个反复出现的现象:同样部署了 AI 编码工具,绿地和棕地团队的体验判若云泥。绿地项目从零建立规范——架构规则随代码生长、提示模式在决策漂移前就被锁定——效果接近当初的承诺。棕地团队面对的是两到五年的决策层积、离职者留下的隐知识、八个月没打开的 Wiki。Raza 举了具体的例子:AI 不知道你的异常类叫 AppException,它抛泛型 Error;不知道你有一层带结构化字段的日志封装——运维的看板和告警全依赖这些字段,它写了 console.log,这个模块从部署第一天就从监控栈里消失了;旧模式有 40000 行存量、新模式只有 8000 行,AI 必然倾向旧模式。

这些没有一个以明显故障出现。它们积累为“微妙的错误”:代码在抽象层面正确,在具体上下文里错误。传统技术债有纸面记录可追溯,AI 上下文负债出问题之前无从察觉。MIT 2025 年一项调查的数字因此变得可理解:95% 的企业没有从 AI 投资中获得有意义的回报。原因不是模型不行。

合规的死锁

如果只是上下文负债,解法是清楚的——更好的模型、更好的上下文工程。但对需要面对安全合规的中国上市公司和金融机构而言,这一步之前就已经被卡住了。

安全部门拿出数据出境管理规定,外部工具不能用。IT 部门采购 GPU 服务器,选一个较新的开源模型做私有化部署。然后合规流程启动:安全审计、渗透测试、数据脱敏验证——短则三四个月,长则半年。走完一圈,当初选的模型版本已过时,换个新版再走一圈。自研工具跑的始终是老旧模型,开发者用一周,不用了。

这不是懈怠或资金短缺。合规节奏追不上模型迭代速度,工具建设者和业务开发者的认知之间存在断层——基础架构团队评测用 HumanEval 和 MBPP,不是“能不能理解我们存储过程里的隐规则“。最需要用 AI 提效的老旧代码库,恰恰最难让 AI 进入。

DeepSeek V4 打破了一环

这个死锁在 2026 年 4 月 24 日出现了一个关键的松动。

当天 DeepSeek 发布了 V4 预览版并同步开源——选在和 GPT-5.5 同一天。同时发布的有两个版本:V4-Pro 总参数 1.6 万亿、激活 490 亿;V4-Flash 总参数 2840 亿、激活 130 亿;二者均支持 100 万 token 上下文窗口。V4-Pro 在编程评测 Codeforces 上得分 3206,比肩 GPT-5.4;在软件工程基准 SWE-bench 上达到 80.6%,接近 Claude Opus 4.6;Agentic Coding 能力在开源模型中排名最高,内部测试中交付质量接近 Sonnet 4.5——此前这个层级的能力几乎被闭源厂商垄断。

但这不只是又一个模型性能突破的故事,真正深远的变化发生在算力层。DeepSeek V4 首次彻底脱离英伟达 CUDA 生态,全面适配华为昇腾平台完成训练。华为同日宣布昇腾超节点全系列产品支持 V4,昇腾 950 超节点推理延迟做到 20 毫秒,昇腾 A3 超节点吞吐量 2000+ TPS。这意味着“国产模型 + 国产芯片”的全栈闭环首次在大规模开源旗舰模型上跑通了。

这对中国企业 AI 编程落地意味着什么?简单说,合规死锁的第一环——“私有化部署的模型跟不上闭源旗舰的性能”——被突破了。一家上市公司现在可以采购昇腾服务器,部署 DeepSeek V4,数据不出内网,模型能力却足够接近世界顶尖水平。它不需要跟英伟达打交道,不需要担心 API 数据出境,不需要在合规审批周期和模型迭代速度之间做不可能的选择。

紧接着在 4 月 25 日,DeepSeek 宣布 V4-Pro API 限时 2.5 折优惠至 5 月 5 日。优惠后输入(缓存命中)降至每百万 token 0.25 元——几乎等于免费的上下文复用。输入未命中 3 元、输出 6 元。对比半年前主流闭源模型的单价,这是一个数量级的差距。定价信号的含义不言自明:当推理成本降到这个水平,企业不再需要在高性能和低成本之间二选一。

这一天离 V3 发布隔了 15 个月。如果把 V4 的性能跃迁和昇腾全栈适配放在这个时间跨度里看,速度是惊人的——15 个月前,一个合规受限的中国企业要在内网跑一个编程能力足够强的模型,要么偷偷接外部 API(违规),要么用性能差一截的开源模型(低效),要么买英伟达高端 GPU 跑开源模型(贵且受制于出口管制)。现在这三条路合成了一条:国产芯片跑国产开源旗舰模型,性能追平闭源。

对企业的 AI Coding 场景而言,V4 的 Agentic Coding 能力是尤其值得关注的。在 SWE-bench 上 80.6% 这个数字意味着什么?它意味着模型不只是能补全一个函数或生成一段算法——它能理解一个软件工程任务(“给订单模块增加部分退款功能”),定位到需要改动的文件,写出跨文件的修改,并且让代码真的跑通。这是企业日常开发中最常见的需求形态,也是对私有化工具来说此前最薄弱的能力环节。V4 让这个环节有了一个开源可部署的选项,不需要依赖外部 SaaS 工具。

但这里有一个关键的转折。DeepSeek V4 打破的是模型供应侧的瓶颈——高性能开源模型加国产算力,让受合规约束的企业终于有了一个能力不掉队的私有化选项。然而它无法打破另一个瓶颈。

模型好了,上下文负债还在

回到文章开头那家上市公司。假设他们现在采购了昇腾服务器,部署了 DeepSeek V4——模型的代码生成质量会比之前那个半年前的老模型好得多,但那个九年前的财务后台系统里散落的隐知识,不会因为模型换了就自动消失。订单状态的判断逻辑、三张表的对账规则、三十天人工通道的约定——这些仍然不存在于任何可以被 AI 读取的结构化文档里。

V4 的 100 万 token 上下文窗口确实是一个有用的能力。理论上,你可以把整个项目的相关代码文件、数据库 schema、甚至部分业务文档一次性塞进上下文。但这解决的是“信息获取范围”的问题,不是“信息是否存在”的问题。如果那些业务规则从来没有被写下来过,上下文窗口再大也装不进不存在的东西。

所以 DeepSeek V4 带来的变量不是“AI 编程终于可以落地了”,而是“模型供给侧的瓶颈被打破了,组织知识管理变成了唯一的瓶颈”。

先理债,后提效——现在模型够用了

Raza 提出的五件基础工作,在新的格局下反而变得比之前更加迫切:一份架构规则文件,告诉 AI 代码库的不可逾越边界;一份系统行为文档,写清楚运行时依赖和故障模式;一份领域知识文档,把代码表面读不出来的业务概念记下来;一套经过实战验证的提示模板库;一套 PR 审查标准,要求 AI 辅助生成的代码注明用了什么上下文、参考了什么文件、审查过了什么。

这三样放在以前,你可以说“模型本身还不够好,做了这些也白做”。现在模型足够好了。DeepSeek V4 在编程和 Agent 能力上已经接近甚至部分追平了闭源旗舰——开源最强、成本地板价、国产芯片可跑。一个合规受限的企业现在没有“模型不行”这个借口了。唯一剩下的瓶颈是自己的知识管理欠账。

这个认知翻转是有分量的:过去十年,企业可以说文档少是因为“写了也没人看”;现在不写,AI 就会把代码写错。AI 没有让文档变得不重要,它让文档从一个可有可无的交付物变成了直接影响代码质量的工程输入。

在模型过硬的年代,流程怎么跟上

知识工件是地基。往上走一层,是怎么把“改代码”这件事和 AI 的协作方式重新设计。SDD(Specification-Driven Development,规格驱动开发)是当前最成体系的尝试——规格不从属于代码,代码从属于规格。产品需求文档不是开发指南,而是开发的发生器;技术方案是精确到能生成实现的定义。

GitHub 的 spec-kit 把这一套拆成了“写规格—出方案—拆任务”三步,整个过程规格文件跟着代码一起版本化。OpenSpec 则明确说自己是“built for brownfield not just greenfield”,可以在老旧项目上增量加。

但对于老旧项目,SDD 天然只能蚕食——在新功能或重构模块上写 spec,不追求全量覆盖。老旧项目没有完整的规格说明书,它的 spec 就是代码本身。强制在每次改动前先写 spec,对资源紧张的团队来说时间账算不平。

蚕食还有一个隐性的坑。重构模块有了 spec、AI 按 spec 生成了干净的新代码,但它仍要和老模块交互——老模块没有 spec,接口不规整,状态转换的隐性条件藏在旧代码里。系统内部被画出一条边界:这边有 spec,那边没有。新代码加了个校验,老代码那边恰好依赖校验不存在时的默认行为——测试在 spec 范围内全过,集成到一起崩了。这类问题往往无法靠增加自动化测试来预防,因为你不知道老代码那边有多少行为是设计如此、有多少是曾经的 bug 被当成了 feature。

真正的效率回报可能要等到 spec 覆盖率达到某个临界点之后才会出现——那时大部分新开发已经不用在散落的隐知识里摸索。这个临界点在哪里,没有人能给出精确数字。业界的定量研究还没跟上。

这个次序说出来似乎平淡:先把知识工件补齐,让 AI 至少了解它面对的是什么;引入渐进式 SDD,接受早期摩擦成本;同步推进工具的工程化集成——上下文切片、RAG 知识库、工具链打通。但这个平淡的次序恰好是它最难落地的地方。所有企业都知道文档重要,都说过“下次一定补”,最后都没补。

区别在于,过去那个“下次”没有紧迫性,但现在有了。DeepSeek V4 和昇腾的组合,把中国企业 AI 编程落地中“能用什么模型”和“在哪里跑模型”这两个问题闭合了,而且是过去 15 年开源运动史上第一次由一家中国公司在编程和 Agent 两个核心能力上追平了全球闭源旗舰。剩下来的全是组织层面的事:知识管理、工程规范、渐进重构、团队对齐。工具没有立场,但欠的债有复利。

茶餐厅那场聊天快结束的时候,我朋友说:“我们现在用 AI,其实就是在用一个放大器。代码库是干净的,它就放大效率和创造力;代码库一团乱麻,它就放大混乱。”

他喝完最后一口冻柠茶,把杯子推到一边。

“不过现在至少不用再纠结模型本身行不行了。剩下的,是我们自己的事。”

参考链接:

  • Abbas Raza. “The Brownfield Problem: How Engineering Teams Are Operationalizing AI Development in 2026“. Leadership in Tech, Product, and Growth, 2026 年 4 月 12 日. https://abbasraza.com/the-brownfield-problem-how-engineering-teams-are-operationalizing-ai-development-in-2026/

  • GitHub spec-kit. “Specification-Driven Development (SDD)“. github.com/github/spec-kit, 2026 年 4 月. https://github.com/github/spec-kit/blob/main/spec-driven.md

  • OpenSpec. “Spec-Driven Development for AI Coding Assistants“. Fission-AI, 2026. https://openspec.pro/

  • Kyle Wiggers. “VCs predict strong enterprise AI adoption next year — again“. TechCrunch, 2025 年 12 月 29 日.(引用 MIT 2025 年 8 月调查数据)

大家好,我最近在做「蜂壳云 / Phones-Cloud 」,之前很容易把它讲成“iPhone 临时用 Android”。

这确实是一个入口,但最近和一些团队聊下来,我更想验证另一个更具体的方向:

给团队使用的云端 Android 工作机。

典型问题是这样的:

  1. 业务账号装在某台实体安卓机上,谁拿着手机谁才能处理。
  2. 员工休假、离职、外出后,账号交接和登录态回收都麻烦。
  3. 远程成员或外包人员需要操作 Android 业务环境时,公司要么寄手机,要么让对方用个人设备登录。
  4. 客服、测试、开发要复现 Android 端问题时,经常围绕截图、录屏、借设备来回沟通。

所以我们现在尝试的方案是:

把真实 Android 设备放在云端,团队成员通过 iPhone 或 Web 远程连接。业务 App 、账号和设备环境留在云端,需要谁接手,就按权限让谁连接。

它不是在 iPhone 里安装 Android ,也不是普通模拟器。更像是把一台公司 Android 工作机从“某个人手里”挪到云端。

目前我们认为更适合这些场景:

  • 客服 / 运营 / 售后团队共用一个 Android 业务环境
  • 远程员工或外包成员临时接手公司 Android 流程
  • 小团队做 Android App 、H5 、客服问题复现
  • 不想为了低频但必要的移动端流程买、寄、维护实体手机

边界也先说清楚:

  • 不是自动化平台,不主张批量操作或绕过第三方平台规则。
  • 不承诺所有第三方 App 都一定适配,必须用真实业务流程先试。
  • 如果每天重度使用、强依赖极低延迟,实体手机可能仍然更合适。
  • 如果只是本地开发调试,模拟器可能更快、更便宜。

现在 iOS 端已经上架,App Store 搜索「蜂壳云」可以找到。

官网:
https://www.phones-cloud.cn

产品截图:
https://www.phones-cloud.cn/screenshots/ios-app-control.png

想听听大家的真实反馈:

  1. 你们团队有没有遇到过“业务手机在谁手里”的问题?
  2. 如果让远程成员接手一台云端 Android 工作机,你最担心权限、稳定性、隐私、价格,还是第三方 App 适配?
  3. 这个方向在你看来更像运维工具、客服工具、测试工具,还是没那么成立?

感谢。

我 30 岁出头,4 月 22 日感觉很不舒服,起初以为是没睡好,23 号去医院检查,被确诊。具体症状:右侧面瘫,抬额右侧无额纹,皱眉左右极不对称,右眼睑闭合不完全,右眼吹风刺激性强,右侧嘴角漏气,无法鼓气,伴有后脑勺头痛,无耳痛。核磁共振报告显示颅内未发现异常。致病原因至今不明。

24 号中午去办理住院输液,输了两次后,25 号办理出院,改为服用药物。医生给我开的口服药:碳酸钙 D3 片,一天一次,一次一片;醋酸泼尼松片,一天一次,一次 8 粒,共 40mg ;复合维生素 B 片,一天三次,每次两粒;甲钴胺片,一天三次,一次一粒。阿昔洛韦片一日三次,一次两粒,共吃 7 天。泼尼松片每五天减一片,直到减完为止停止所有用药(共计四十天)。

截至今天( 5 月 1 日),我的右眼睑闭合还是不完全,但只有微小的缝了,可以往右侧咧嘴,脸颊肌肉有明显的变化,抬额时右侧额头也有了一道额纹,总之就是已经恢复了将近一半。

昨天我首先去了中医院老同学(曾在那个医院任职)推荐的中医那去,医生说:针灸两周,每周四次,扎一次几分钟搞定,建议针药并用,即要吃中药。他说开住院的去针灸要便宜些,门诊扎贵一点,并且中药也贵,还建议我泼尼松最多吃两周,他质疑我一开始的剂量即每天 8 片共 40mg 太少了,应该先急用药,药程两周即可。我问他针灸是好得更快还是好得更多,他答复好得更快。

后来我去了人民医院的康复科门诊,医生也说要针灸,半个月,每天一次,每次一到两个小时,也可能扎几天就好了就不扎了,但跟我说现在泼尼松吃得太多了,建议我减到 10mg 或者 20mg 。

我身边的人无论医生还是曾经的病患都说要针灸,我现在非常纠结,好像每个医生的方案都有些差异,到底该相信谁。问了一大圈 AI 都说针灸非必需。用药不到一周恢复到这个水平可以不用针灸吗?