普通视图

发现新文章,点击刷新页面。
今天 — 2026年3月31日首页

Everything Claude Code:让我把 AI 编程效率再翻一倍的东西

作者 清汤饺子
2026年3月31日 09:32

Hi~大家好呀,我是清汤饺子。

先说个让我差点砸键盘的场景。

我打开一个新的 Claude Code session,准备继续前天没写完的功能。

Claude 热情地跟我打招呼:嗨!很高兴再次见到你,有什么我可以帮你?

我说:继续前天的任务。

Claude:好的!请问你想做什么?

我:就是那个功能模块啊,前天做到一半的那个。

Claude:好的!请问你想做什么功能?

我:……你刚才不是说了"再次见到我吗"?

Claude:哦,那只是客气话,我的记忆撑不过一个 session。

我:……行吧。

这个对话你是不是也似曾相识?

是不是也想问 AI:你礼貌吗?

一、我的痛点:AI 每次都是"新人"

Claude Code 的 memory 功能我深度用过——CLAUDE.md 配了、项目规范配了、技术栈配了。

但它只能记住"静态上下文",记不住"动态进度" :上次做到哪了、上次做了什么决定、上次遇到了什么问题。

更崩溃的是——有时候 Claude 会"选择性失忆"。明明配置了 memory,它偏偏没触发。有一次我让它帮我重构一个模块,它完全忽略了我们的代码规范,输出了一套我完全不认识的风格。

我开始认真想:有没有一套系统,能让 AI 的"记忆"真正 work?

然后我发现了 Everything Claude Code。

二、ECC 是什么

GitHub 110K+ stars,Anthropic Hackathon 冠军作品。

作者是 affaan-m,做了 10 个月每天高强度在真实项目里打磨出来的。定位不是"配置文件合集",而是:一套完整的 AI Agent 性能优化系统

ECC 官方有一张对比表,说清了它的核心价值:

Without ECC With ECC
AI 不了解团队的代码模式 AI 通过 rules 和 skills 学习团队规范
测试靠手动写,覆盖率不稳定 TDD 流程内置,测试先行,覆盖率透明
安全漏洞靠人工 review AgentShield 实时扫描,102 条规则自动拦截
团队没有统一的代码标准 skills 和 agents 全团队共享
每个 session 从零开始 Continuous Learning 跨 session 积累

这张表说清楚了 ECC 解决的问题。但光看表感受不深,我用了两个月,说说具体是什么体验。

三、GitHub App:把 commit 历史变成团队规范

这是 ecc.tools 最让我惊喜的功能。

ECC 提供一个 GitHub App(免费安装),它的工作方式是这样的:

  1. 在你的仓库安装 ECC Tools GitHub App
  2. 在任意 issue 下评论 /ecc-tools analyze
  3. ECC 自动分析你的 commit 历史、代码模式、团队规范
  4. 自动生成一个 Pull Request,把这些历史转成 skills 和 defaults

翻译成人话:你的 git 提交记录里藏着团队多年的工程经验,ECC 自动把这些经验提取出来,变成 AI 可以复用的规范。

这个 PR 不是直接合并的——你审核、修改、确认之后才生效。完全可控。

我试了一下,第一次跑完它生成了大约 30 条 rules,覆盖了我们的 commit message 规范、分支命名规则、还有 API 错误处理的一些惯用模式。

最厉害的是:这个 PR 里的内容是专门针对你这个仓库的,不是通用模板。ECC 读的是你真实的 git 历史,提取的是你团队真实在用的规范。

四、Token 优化:让 AI 跑得更快

Context window 是有限的,AI 跑着跑着就开始"失忆"前面的内容。ECC 有几个实用的省 tokens 方法,都是踩坑踩出来的经验。

  • 模型选择:大多数日常任务用 Sonnet 4.5 就够了,复杂任务(跨 5+ 文件、架构决策、安全关键代码)升 Opus,重复性劳动降级到 Haiku 当 worker。类比一下:能用摩托车拉的不用卡车,卡车油耗高,还不好停。

  • 工具替换:Claude 默认用 grep 或 ripgrep 搜索代码,tokens 消耗大。换 mgrep,平均节省 50%——就像从手动档换成自动挡,不改变目的地,但脚不酸了。

  • 后台进程:不需要 AI 实时处理输出的任务,用 tmux 丢后台,不占用 context。这就像让 AI 同时处理多项任务——实际上它是把不重要的任务先寄存起来。

  • 模块化代码库:文件越小(几百行 vs 几千行),AI 消耗的 tokens 越少,出错率也越低。

五、Memory 持久化:AI 不再是"金鱼"

这是 ECC 最打动我的功能,也是它和"普通配置文件"的本质区别所在。

原生 Claude Code memory 只能存"静态模板"——项目规范、技术栈、代码风格。但它存不住"动态进度" :上次做到哪了、遇到了什么问题、做了什么决策。

ECC 的解法是把三个 Hook 串联起来,形成完整的记忆链条。

第一棒:SessionComplete Hook,session 结束时自动存档

Session 结束时,Claude 自动把当前状态写入 .tmp 文件——完成的任务、遇到的阻塞、关键决策、下次继续需要的信息,全都存下来。

第二棒:SessionStart Hook,新 session 开始时自动恢复

新 session 开始时,Claude 自动读取上次的 .tmp 文件。它会主动问:"检测到上次有未完成的任务,要继续吗?"

第三棒:PreCompact Hook,提前预警该整理了

在你积累了很多上下文的时候,提前提示你"该整理一下了",避免等到 AI 开始"失忆"才后悔。

三个 hook 串联起来,实现的是:跨 session 真正零手动干预的连续记忆。我第一次用这套组合的感觉是——Claude 终于不是"金鱼"了,甚至有点像一个记性比我还好的 senior。

六、Continuous Learning:让 AI 从错误中进化

核心问题:同一个错误,AI 犯一次两次三次,永远记不住。

解法:告诉 Claude "记住它",它把这个模式自动写入 skills,下次遇到类似场景自动调用。

触发方式有两种:

自动:session 结束时运行 /learn,自动提取这次 session 里发现的有效模式。

手动:中途解决了什么非平凡的问题,马上 /learn 即时提取。

我连续三次让 Claude 帮我写 API 接口,它第三次就自己学会了"我们项目里 API 文件放哪里、命名规范是什么、错误处理用什么模式"。

这感觉就像养成了一个会自动学习的好习惯——不用催,它自己就记住了

七、验证与安全

AI 执行命令是有风险的。Prompt injection、未经授权的文件修改、"AI 误删整个 node_modules"这种事,社区里见过太多了。

解法:ECC 提供了 AgentShield——一个独立的安全扫描工具,102 条规则、1282 个测试用例、98% 覆盖率,采用 Red Team / Blue Team / Auditor 三层 Pipeline。

这阵容,比很多公司的安全团队都专业。

效果:扫描输出分级展示,critical 问题直接标红。

运行效果是这样的:

$ npx ecc-agentshield scan ./CLAUDE.md

 CRITICAL  Unrestricted file system access via Bash tool
 WARNING    No rate limiting on external API calls
 WARNING    Missing secret detection guardrail
 PASS       Tool permissions properly scoped
 PASS       Destructive action confirmation required
 PASS       No prompt injection vectors detected

Security Score: 72/1001 critical, 2 warnings, 3 passed
Full report saved to ./agentshield-report.json

我之前差点让 Claude 把整个 node_modules 删了——它问都没问我直接动手。幸好当时没执行,不然一天白干。有 AgentShield,那种"先斩后奏"的命令直接被拦截,连求情的机会都不给

八、技术原理

看完 GitHub 仓库,我发现 ECC 比"配置文件合集"要系统得多。它的核心不是某一个功能,而是一套层次化的 Agent 优化架构

1. 五类组件:底层基础设施

ECC 的仓库由五类组件构成,每一类解决不同层次的问题:

  • Agents(智能体):30+ 专业子代理,负责特定领域的任务执行。比如 code-reviewer 专门做 code review,build-error-resolver 专门修编译错误,chief-of-staff 专门做任务规划和进度管理。每一个 agent 只做一件事,做得很专注。

  • Skills(技能):可复用的任务模式库,分两类——

    • 语言生态:TypeScript、Python、Go、Rust、Java、PHP、Perl、Kotlin、C++ 等,每个语言有对应的 patterns 和 conventions
    • 垂直领域:django、laravel、springboot、pytorch 等框架完整技能栈,覆盖从开发到部署的全流程
  • Commands(命令):斜杠命令是快速触发技能的入口,比如 /plan 触发任务规划、/tdd 启动 TDD 流程、/learn 即时提取好模式。命令和技能联动,构成了 ECC 的交互层。

  • Rules(规则):始终遵循的约束,放置在 .claude/rules/ 目录下。AI 每个 session 都会读取,是最低层次的"铁律"。Rules 不同于 Skills——Skills 是告诉 AI"怎么做",Rules 是告诉 AI"绝对不能怎么做"。

  • Hooks(钩子):挂在 Session 生命周期上的自动化脚本,这是 ECC 最具特色的设计。每个 Hook 有明确的触发时机:

    • PreToolUse:工具执行前触发,比如拦截危险命令
    • PostToolUse:工具执行后触发,比如自动格式化、自动运行 lint
    • SessionComplete:session 结束时触发,自动存档
    • SessionStart:session 开始时触发,自动恢复上下文
    • PreCompact:上下文即将溢出前触发,提前预警

2. SKILL.md:技能自动发现的秘密

ECC 的 Skills 不是靠手动调用的,而是靠 description 字段自动触发

每个 Skill 文件(Markdown 格式)顶部有一段 YAML metadata:

---
name: tdd-workflow
description: Use when the user wants to do test-driven development - sets up TDD flow with RED first
---

当 AI 判断当前任务符合 description 的条件时,自动激活对应 Skill,整个过程不需要你做任何事情。

这意味着 ECC 的 Skills 系统本质上是上下文感知的——AI 根据当前任务状态自动匹配最佳实践,而不是等你一步步指示。

3. SQLite 状态存储:持久化的秘密

ECC 用 SQLite 作为状态存储数据库,记录:

  • 已安装的 Skills 列表和版本
  • Session 历史摘要
  • Continuous Learning 的演化记录
  • 各平台(Claude Code / Codex / Cursor / OpenCode)的配置状态

这让 ECC 具备"状态记忆"——不是每次从零开始,而是知道"上次装了什么、上次做了什么、哪里出了问题"。支持增量更新,不用每次全量重装。

4. Continuous Learning 的技术实现

ECC 的 Continuous Learning 不是靠"更长的 context window",而是靠自动提取 + 写入 Skills 目录

  1. AI 在 session 中发现一个有效的模式(比如"这个项目的 API 错误处理用 Result type")
  2. 自动把这个模式写入 ~/.everything-claude-code/skills/
  3. 下次遇到类似场景,Skills 触发,模式复用

本质是把隐性知识显性化,把单次经验变成可复用资产。这解决了 AI "同类错误重复犯" 的根本问题。

5. AgentShield 的技术实现

AgentShield 不是简单的"危险命令黑名单",而是一套多层次安全扫描机制

  • Hook 层:在命令执行前拦截,扫描 rm -rfcurl | bashgit push --force 等危险模式
  • CVE 数据库:集成了常见漏洞数据库,扫描依赖包是否有已知漏洞
  • Sandbox 隔离:危险操作在隔离环境执行,不直接影响主项目

6. 安装架构:Manifest 驱动

ECC 的安装不是"一键全装",而是Manifest 驱动的选择性安装

./install.sh --profile full        # 全量安装
./install.sh typescript            # 只装 TypeScript 相关
./install.sh --target cursor python  # 只给 Cursor 装 Python 生态

install-plan.jsinstall-apply.js 负责解析 Manifest,按需安装。SQLite 状态存储记录"装了什么",支持增量更新。ECC_HOOK_PROFILE 环境变量还可以控制 Hook 的严格程度(minimal / standard / strict)。

GitHub 仓库:github.com/affaan-m/ev…

九、和 Superpowers + OpenSpec 的关系

这三个工具解决的问题正好互补:

工具 解决的问题
OpenSpec 需求对齐——先签字再动手
Superpowers 工程纪律——TDD、task 分解、子 Agent 编排
ECC 性能和记忆——Token 优化、Memory 持久化、安全扫描

OpenSpec 在最上游——它管的是做什么

Superpowers 在中游——它管的是怎么做

ECC 在底层——它管的是怎么跑得更好

三个一起用,才是完整的 AI 编程工作流。


写在最后

ECC 解决了一个根本问题:AI 不是"真的智能",它是"真的没有记忆"

110K+ stars 说明这套方法论经过了大量开发者的验证。我用了两个月,最大的感受是——AI 编程终于有点像"和一个靠谱的同事合作",而不是"和一个热情的实习生搏斗"——热情是热情,但每次都要我来收拾残局。

当然它不是银弹。配置成本不低,学习曲线陡峭,踩坑也需要时间。如果你每天用 AI 写代码,这点投入值得;如果只是偶尔用用,原生体验可能就够了——省下的配置时间够你手动写好几屏代码了

你被 AI coding 的"失忆症"困扰过吗?有没有什么土办法?

欢迎评论区聊聊,看看大家都有什么奇葩经历,互相种草避坑。

如果觉得有帮助,点个赞收藏一下,我会更有动力更新下一期。

也欢迎关注我的公众号「清汤饺子」,获取更多技术干货!

追觅生态链多了家清华系公司,要用AI储能融入智能家居体系|硬氪专访

2026年3月31日 09:30

作者|黄楠

编辑|袁斯来

2024年进入储能市场,是个有些反常识的决定。

储能在2022年前后经历过一次疯狂的增长,产能过剩后又惨烈洗牌,到曹治鹏入场时,已经相对平静。华宝新能、正浩创新、安克、大疆、华为等等大厂,都在其中盘踞一块领地。

但曹治鹏发现,市场并非死水一潭。

曹治鹏从事能源行业近20年,他曾任职于沈飞、华硕、阿特斯等上市企业,主导参与了户用储能、工商业储能等产品从0到1的研发与设计,亲历过储能从专业设备走向家庭场景的全过程。这也让他对行业的演变节奏有清晰判断:硬件迭代趋同之后,真正的机会或许不在参数的线性竞赛中。

自2024年中旬开始,德国等成熟市场的政府补贴开始退坡,储能市场逐渐褪去早期的政策依赖与概念狂热,进入静默竞争期。

这意味着硬件参数竞赛正让位于一场更为复杂的综合较量。老牌公司还未觉醒,新创团队仍有机会。

2024年7月,曹治鹏成立星空源储。其核心团队来自俞浩母校清华,并作为生态伙伴加入追觅生态,目前产品已经覆盖户外便携、家庭储电、阳台光储及城市移动补能场景。

可以看到,星空源储最大的差异化是背靠追觅体系,跳过了创业公司前期最艰难的拉新期。

当然,正如曹治鹏所说,“我们虽然是追觅的生态伙伴,但用户对清洁电器的信任,并不会直接转移到储能这个新领域。”即便有追觅品牌的加持,打仗还是靠自身产品和渠道实力。

毕竟,这个赛道已经相当残酷。星空源储有一个好的开始,却绝不敢懈怠。

“从专业品牌到消费电子巨头都进来了,导致技术快速普及,价格战也越来越激烈。同时,电芯等原材料成本又在下降,这使得行业整体利润空间被持续压缩。对我们来说,必须在成本和运营效率上做到极致。”曹治鹏告诉硬氪。

在储能行业,硬件层面的“微创新”已经褪去光环。行业的决胜点转向软件深度与运营体系的隐性较量,用户的核心诉求已演变为对一套贯穿全周期“能源服务”的期待。

我们和星空源储执行董事总经理曹治鹏聊了聊,交流他们如何在有限的时间窗口内,尽快在用户心中建立心智。

以下是硬氪与星空源储执行董事总经理曹治鹏的对谈实录,内容经编辑:

不同场景的差异化解法

硬氪:近年来,哪些因素在驱动市场对便捷储电的需求增长?

曹治鹏:受环保政策与新能源产业发展的双重推动下,锂电池成本显著下降、性能持续提升,便携储能设备从专业领域进入家庭,成为越来越多用户负担得起的实用商品。

这一趋势背后,是三大关键动力的共同作用。一是“露营体验升级”带来的场景革命;露营本身不是个新事物,许多国家长期都有这类活动传统,而过去的露营用品往往是“专器专用”,比如烹煮需要用到气罐和卡式炉,烧烤的需要用到烧烤架和炭。便携电源的出现,让家用电器得以直接用于户外,带来一种颠覆性的便捷,降低露营门槛。

二是特定地区家庭应急备灾意识的普遍提升。尤其在日本、美国部分州等自然灾害高发地区,以及受地缘冲突影响的欧洲市场,家庭对电力自给的需求日益迫切,便携储能成为了家庭应急储备中的关键一环。

三是能源焦虑催生的新的用电逻辑。受海外电价持续上涨及电网稳定性不足等因素影响,便携储能在用户眼中不只是“大号充电宝”,还具备了在用电高峰放电的经济价值,可以节约电费并保障用电稳定。

硬氪:这些转变反映到便捷储电的需求端,带来了哪些影响?

曹治鹏:综合来看,呈现出几个特征。一方面是高增长与高竞争并存;目前,全球市场正以超过20%的年复合增长率快速扩张,但便携储电行业的发展,高度依赖对具体应用场景的持续挖掘与深化,如果未能找到精准场景匹配技术,将难以兑现其技术价值。

另一方面,在格局未定的激烈市场竞争中,也意味着各家很难仅凭单一优势立足,必须实现品牌、渠道与产品生态上协同。

硬氪:星空源储团队如何定位自己的产品和目标用户?

曹治鹏:我们的目标用户主要分为两类,包括追求品质户外生活与应急备灾的家庭用户, 他们不仅需要大功率、长续航的供电能力,更看重产品与智能家居设备的无缝联动和便捷的操控体验。

瞄准差异化场景的多SKU产品(图源/企业)

从有家庭能源管理初步需求的都市用户端来看,他们关注电费节约和用电稳定性,希望从阳台光伏储能等轻量级方案入手,作为未来全面家庭能源管理的起点。

硬氪:过去几年,便携储能领域已形成品牌与规模优势的巨头,星空源储的竞争力体现在哪些维度?

曹治鹏:主要来自两个方面。第一,星空源储是追觅的生态伙伴企业,能直接融入到追觅已经搭建好的智能家居场景里。用户买了我们的储能产品,可以很自然地成为他智慧家庭的一部分。 从户外电源、阳台储能系统到户用储能,都可以跟家里的电器联动,实现智能的电力调度。

第二是团队的清华研发背景,能更紧密地和清华等顶尖高校的实验室合作,去提前布局一些更前沿的技术,比如下一代固态电池、更聪明的电池管理算法等等。

硬氪:面对需求迥异的场景,你们在技术和产品定义上的首要考量分别是什么?用户洞察如何体现?

曹治鹏:首先是设计思路上最根本的差别。户外便携电源比如露营、自驾游用,我们做的是“一体式集成”,可以将其类比成一个高级的“大号充电宝”,电芯、逆变器、控制系统全部精密地集成在一个箱体里。目标是极致紧凑、拎起来就走。

家庭储能产品思路就完全相反,是“模块化扩展”。电池包、逆变器都是独立的模块,你可以像搭积木一样,按家里用电需求灵活组合、后续增加,性能方面更注重安全性和耐用性。

技术路径也因场景而异。比如户外场景、用户最焦虑的是没电了怎么办,因此,技术重点一定是快速补电、支持快充;但在家储场景,产品是家庭能源的一部分,每天都要循环充放电,所以我们也会更注重产品的长寿命和高稳定性,要保证它用十年依然可靠,这才是真正的省心和经济。

户外场景的一体化集成思路(图源/企业)

如何「聪明」用电

硬氪:便携和家储,你们现阶段更聚焦哪一个?

曹治鹏:我们的核心是从家庭生态出发的,家庭储能(户储)是我们的业务轴心。先把便携电源作为家庭用电生态的“户外延伸”推给用户,快速让用户接触到我们,建立起“安全、智能”的品牌认知。

硬氪:从公司整体战略看,这两大业务板块分别扮演什么角色?

曹治鹏:储能不只是一两个产品,而是一个完整的能源生态。在我们的布局中,户储和便携储能不是分开的两条线,而是互相推动。

其中,便携储能更像是能源体验的“入口”,让电变得可携带、可触摸,在户外、旅行、应急等场景里,用户可以实现对能源的自主掌控权。户用储能则是能源智能的“中枢”,它不止存电,更懂怎么管电。通过AI系统,它能连接光伏、储能和家里的电器,让家庭成为一个可以自主调节的微型能源网络。

硬氪:家储作为整体行业的新增长点,在策略上有什么不同?

曹治鹏:渠道策略完全不同。便携产品主要通过线上电商直接触达用户,路径很直接;而家储则需要与光伏安装商、系统集成商这些专业伙伴共建生态,以整体解决方案的形式落地,这是一个更重但也更深厚的模式。

更重要的是客户服务。家储是一个“重服务”的长链条业务,从方案设计、安装调试到长期的运维监控,都需要我们建立完整的服务体系。这也完全不同于便携储能相对标准化的售后支持。

总的来说,便携储能是“产品逻辑”,家储是“系统逻辑”,它会是接下来重要的战略支点。

硬氪:市场上很多储能产品都在提AI,它具体是怎么帮用户规划用电的?

曹治鹏:其实用户的需求很清晰,就是从“有电可用”变成了要“聪明地用”。大家不只想存电,更希望省电费、操作简单、用得安心,市场上的AI功能也都是朝着这三个方向去解决问题的。

AI就像一个家庭能源管家,它主要为用户做两件事;一是“精打细算” ,根据实时电价自动调度充放电、省电费;二是“自主学习” ,摸清用户的用电习惯后,自动制定最优计划。

这一切的实现,依赖于软硬件的深度耦合。AI指令会毫秒级下发给逆变器执行,同时实时接收电池的温度、电量等数据,确保所有操作都在安全健康的范围内,还能作为全屋智能中枢、与空调等家用电器设备联动。

帮助用户“聪明用电”(图源/企业)

硬氪:你们的目标市场是哪些国家和地区?

曹治鹏:我们目前重点关注了北美、日本和欧洲。选择它们,是因为这些市场既有强劲的购买力,又有非常现实和迫切的需求,不仅有支持高端清洁能源产品的消费基础和环保政策,而且不同市场用户的痛点都足够清晰和强烈。

北美用户的消费实力雄厚,更愿意为高品质、好体验的产品买单。日本由于地震等灾害频发,用户有着极强的应急意识。欧洲地区受地缘政治冲突影响,能源安全焦虑和电价波动是核心痛点,当前能源安全和电费波动是家庭的核心焦虑,因此用户对实现能源独立、节省电费有着迫切渴望,加上悠久的户外传统,为产品提供了多元的使用场景。

硬氪:不同市场对产品特性的需求存在哪些差异化?

曹治鹏:在欧美市场,特别是北美,用户需求很直接,强调“大容量与高功率”。这背后对应着两种典型场景,一类是家庭应对极端天气导致的长时间停电,需要能为冰箱、医疗设备持续供电;第二是源于其深厚的户外文化,露营时甚至要带动电烤炉、咖啡机这种高功率电器。所以用户特别看重产品的“供电能力”和“带得动什么”。

日本市场则完全围绕 “安全与精细” 。因为地震多发且居住空间紧凑,用户对安全性和可靠性的要求几乎是全球最高、到了严苛的程度,同时希望产品设计精致、能安静地融入家居环境。在日本,赢得信任比单纯强调参数更重要。

相比之下,澳大利亚、东南亚等新兴市场更偏向 “实用与性价比”。用户主要为了户外活动或基础备电,他们最关心产品是否耐用、适应性强,并且价格实在,满足的是最核心的用电保障需求。

便携储能提供能源入口,电变得可携带、可触摸(图源/企业)

硬氪:具体到产品的功能和设计上,是如何针对性优化的?

曹治鹏:欧美的核心需求是应对长时间停电。因此,我们产品的优化重点就放在“更强供电”和“更多接口”上。通过采用高性能磷酸铁锂电芯和智能电池管理系统,在确保安全的前提下,尽可能延长供电时间,保证冰箱、医疗设备等关键电器在停电时持续运转。同时,增加多种输出接口,满足家庭同时为多个设备充电的应急需求。

日本用户更追求精致、安静、节能的产品体验,所以我们的设计方向就转向小型化、轻量和静音,外观采用简约风格,以便融入家居环境。软件上也采用了智能待机和能效管理算法,最大限度降低设备自身的能耗,更贴合当地用户注重细节和节能的习惯。

竞争生态化

硬氪:近年受到补贴退坡影响,德国户储市场出现下滑。从市场端来看,政策转向给行业竞争带来了什么?

曹治鹏:这反而是行业走向成熟的一个信号。这意味着,行业发展的驱动力正在从“政策推动”转向“价值驱动”,用户不再单纯为了补贴而安装,而是更关注储能系统本身能带来什么价值。

现在的政策导向,更多是从鼓励“多装”变成了鼓励“装好、用好” ,更加关注系统质量与能源效率。包括推动光储充一体化,发展“虚拟电厂”,探索社区微网与能源共享等等。

可以看到,过去企业之间可能比的是谁卖出的设备多,而现在比的是谁的系统更智能、谁能创造持续的价值。当储能行业从政策红利走向智能红利,才算进入了真正的能源新时代。

硬氪:作为一个新品牌,出海过程中,星空源储团队面临最大的压力和挑战是什么?

曹治鹏:在高端市场,抢占用户心智是一场硬仗。我们最大的压力源于“时���”,如何在有限的窗口期内,快速建立起足以让挑剔的用户愿意放弃成熟品牌、转而选择我们的“信任壁垒”。

展开来讲,它来自三个方面。首先是市场和利润的双重挤压。 这个赛道现在非常拥挤,从专业品牌到消费电子巨头都进来了,导致技术快速普及,价格战也越来越激烈。同时,电芯等原材料成本又在下降,这使得行业整体利润空间被持续压缩。对我们来说,必须在成本和运营效率上做到极致。

其次,是建立品牌信任的紧迫感。 储能产品不是冲动消费,用户买的是一份长期的安全保障,决策时天然会倾向已经熟悉的品牌。我们虽然是追觅的生态伙伴,但用户对清洁电器的信任,并不会直接转移到储能这个新领域。如何快速建立起专业、可靠的品牌认知,是我们必须打赢的一场心智攻坚战。

最后,是产品持续创新的内生压力。市场上产品难免有同质化,但用户对性能、智能化的期待却在不断升高。我们必须把在快充、固态电池这些领域的技术储备,更快地转化成实实在在的产品差异点,否则很容易被淹没。

全场景智慧能源生态(图源/企业)

硬氪:随着储能市场进入静默竞争期,叠加需求端持续升级,后续竞争会在何时打破现有格局?技术应用、产品功能层面会出现哪些关键博弈点?

曹治鹏:未来几年便携储能会从一个“应急电源”,进化成一个“智慧能源系统”。它不再只是解决临时用电,而是开始构建一种更长期的能源关系。

一方面是技术智能化,AI将成为储能的“大脑”,让能量管理、安全控制和系统优化变得前所未有的高效。未来的储能设备不再只是一个静态电池,而是具备学习和预测能力,能根据用户的习惯自动调整,变成一个聪明的“动态能源伙伴”。

第二,是场景融合化。 模块化设计、光储充一体会越来越普遍乃至成为标配,便携储能和家庭储能的界限被打破。储能系统会变得更加灵活,能量可以在露营、家庭甚至社区小网络中自由调度。

从长远来看,竞争也会转向生态化,从价格比拼步入“智能体验与生态服务”的较量,比的是哪家能构建一个更懂用户、能持续学习和升级的智慧能源网络。

星空源储的迭代方向非常明确,我们将专注于开发AI能量调度、智能安全控制和设备互联技术,构建一个覆盖个人、家庭到社区的智慧能源生态,同时也同步建设海外的本地化服务体系。

我们相信,储能的未来不仅仅是储存电能,更是储备一种更高效、更自主、可持续的生活方式。

A股三大指数开盘涨跌不一,贵州茅台涨超3%

2026年3月31日 09:26
36氪获悉,A股三大指数开盘涨跌不一,沪指高开0.02%,深成指低开0.31%,创业板指低开0.8%;白酒、家用电器、能源设备板块走强,美的集团涨超4%,贵州茅台涨超3%,海默科技涨1%;通信设备、半导体、软饮料板块走弱,扬杰科技、长光华芯、太辰光跌超4%,东鹏饮料跌3%。

恒指开盘涨0.27%,恒生科技指数涨0.08%

2026年3月31日 09:22
36氪获悉,恒指开盘涨0.27%,恒生科技指数涨0.08%;医药生物、汽车、有色金属板块领涨,昭衍新药、北京汽车涨超3%,中国铝业涨超2%,蔚来涨超1%;半导体、建材、硬件设备板块走弱,兆易创新跌超4%,禾赛跌超3%,海螺水泥跌超1%。

排列算法完全指南 - 从全排列到N皇后,一套模板搞定所有排列问题

作者 wuhen_n
2026年3月31日 09:13

上篇文章我们聊了回溯算法中的组合/子集问题,本文将聚焦于LeetCode上面的:46(全排列)、47(全排列II)、22(括号生成)、51(N皇后)、37(解数独),以及剑指offer - 38(去重排列),这几道非常经典的题目,带领大家彻底拿下排列算法。

排列 VS 组合

基本概念

很多人容易把 排列组合 混为一谈,但实际上它们有本质上的区别。它们的核心用一句话概括:

  • 组合(Combination):选出来就行,顺序不重要 [1,2] 和 [2,1] 是同一个。
  • 排列(Permutation):顺序很重要,[1,2] 和 [2,1] 是两个不同的结果。

通用模板

组合/回溯的通用模板

在本文开始之前,我们先回忆一下组合/回溯的通用模板:

function backtrack(路径, 选择列表) {
    if (满足结束条件) {
        result.push([...路径]); // 存放结果
        return;
    }

    for (选择 in 选择列表) {
        // 1. 做选择(前序遍历)
        路径.push(选择);
        
        // 2. 进入下一层决策树(递归)
        backtrack(新的路径, 新的选择列表);
        
        // 3. 撤销选择(后序遍历)
        路径.pop();
    }
}

排列的通用模板

const used = new Array(nums.length).fill(false);  // 关键1:used数组
const backtrack = (path) => {
    if (满足结束条件) {
        result.push([...path]);
        return;
    }
    
    // 关键2:每次都从0开始遍历
    for (let i = 0; i < nums.length; i++) {
        // 关键3:跳过已使用的元素
        if (used[i]) continue;
        
        // 做选择
        path.push(nums[i]);
        used[i] = true;
        
        // 递归
        backtrack(path);
        
        // 撤销选择
        path.pop();
        used[i] = false;
    }
};

排列 vs 组合的模板对比

对比维度 组合模板 排列模板
参数 backtrack(start, path) backtrack(path)
遍历起点 for (let i = start; i < n; i++) for (let i = 0; i < n; i++)
去重方式 用 start 控制不回头 用 used 数组标记
空间结构 不需要额外数组 需要 used 数组
结果数量 C(n, k) P(n, k) = n!

为什么排列要每次都从0开始?

  • 因为 [1,2] 和 [2,1] 是两个不同的结果
  • 第一个位置可以选任何元素,第二个位置也可以选任何元素(除了已选的)

为什么需要 used 数组?

防止同一个元素被重复选取(排列不允许重复使用同一个元素)

下面的所有题都是在这个模版的基础上增加剪枝条件!

排列问题的入门(LeetCode 46)

题目

给定一个没有重复数字的序列,返回其所有可能的全排列。

示例:nums = [1,2,3] 输出: [ [1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1] ]

思路解析

排列问题的核心: 只要顺序不同,那就是不同的结果。比如第一个位置可以选1、2、3中的任意一个;选完第一个后,第二个位置只能在剩下的数中选。

图解过程

flowchart TD
    Start(("[]"))
    
    Start -->|"选1"| A1["[1]"]
    Start -->|"选2"| A2["[2]"]
    Start -->|"选3"| A3["[3]"]
    
    A1 -->|"选2"| B1["[1,2]"]
    A1 -->|"选3"| B2["[1,3]"]
    
    A2 -->|"选1"| C1["[2,1]"]
    A2 -->|"选3"| C2["[2,3]"]
    
    A3 -->|"选1"| D1["[3,1]"]
    A3 -->|"选2"| D2["[3,2]"]
    
    B1 -->|"选3"| E1["[1,2,3]"]
    B2 -->|"选2"| E2["[1,3,2]"]
    C1 -->|"选3"| E3["[2,1,3]"]
    C2 -->|"选1"| E4["[2,3,1]"]
    D1 -->|"选2"| E5["[3,1,2]"]
    D2 -->|"选1"| E6["[3,2,1]"]

代码实现

var permute = function(nums) {
    const result = [];
    const used = new Array(nums.length).fill(false);
    
    const backtrack = (path) => {
        // 结束条件:找到一个完整排列
        if (path.length === nums.length) {
            result.push([...path]);
            return;
        }
        
        // 每次都从0开始,尝试所有未使用的元素
        for (let i = 0; i < nums.length; i++) {
            if (used[i]) continue;  // 已使用的跳过
            
            // 做选择
            path.push(nums[i]);
            used[i] = true;
            
            // 递归
            backtrack(path);
            
            // 撤销选择
            path.pop();
            used[i] = false;
        }
    };
    
    backtrack([]);
    return result;
};

全排列 II(LeetCode 47)—— 有重复元素的全排列

题目

给定一个可能包含重复数字的序列,返回所有不重复的全排列。

示例:nums = [1,1,2] 输出: [[1,1,2], [1,2,1], [2,1,1]]

注意:不能有重复的排列。

思路解析

这题和上诉 46 题解析思路基本一致,唯一一点需要注意:当数组有重复元素时,需要去重,例如:两个 1 互换位置,但它们值相同,需要去重:

  • 排序:让相同的元素挨在一起。
  • 剪枝:在排列模板的基础上,增加同层去重判断。

代码实现

var permuteUnique = function(nums) {
    const result = [];
    const used = new Array(nums.length).fill(false);
    nums.sort((a, b) => a - b); // 排序是去重的前提
    
    const backtrack = (path) => {
        if (path.length === nums.length) {
            result.push([...path]);
            return;
        }
        
        for (let i = 0; i < nums.length; i++) {
            // 基础剪枝:已使用过的跳过
            if (used[i]) continue;
            
            // 去重剪枝:同一层相同元素跳过
            if (i > 0 && nums[i] === nums[i - 1] && !used[i - 1]) continue;
            
            path.push(nums[i]);
            used[i] = true;
            backtrack(path);
            path.pop();
            used[i] = false;
        }
    };
    
    backtrack([]);
    return result;
};

字符串的排列(剑指Offer 38)

题目

输入一个字符串,打印出该字符串中字符的所有排列。可以以任意顺序返回这个字符串数组,但不能有重复元素。

示例:s = "abc" 输出:["abc","acb","bac","bca","cab","cba"]

思路解析

这题和上述 LeetCode 47 题完全一样,只是输入从数组变成了字符串。

代码实现

var permutation = function(s) {
    const result = [];
    const arr = s.split('');
    const used = new Array(arr.length).fill(false);
    arr.sort(); // 排序去重
    
    const backtrack = (path) => {
        if (path.length === arr.length) {
            result.push(path.join(''));
            return;
        }
        
        for (let i = 0; i < arr.length; i++) {
            if (used[i]) continue;
            // 去重:同一层相同字符跳过
            if (i > 0 && arr[i] === arr[i - 1] && !used[i - 1]) continue;
            
            path.push(arr[i]);
            used[i] = true;
            backtrack(path);
            path.pop();
            used[i] = false;
        }
    };
    
    backtrack([]);
    return result;
};

括号生成(LeetCode 22)—— 特殊的排列问题

题目

数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且有效的括号组合。

示例:n = 3 输出: [ "((()))", "(()())", "(())()", "()(())", "()()()" ]

思路解析

这题虽然看起来并不是传统的排列问题,但它本质上也是一个排列问题:

  • 选择列表:( 和 ) 两个字符。
  • 约束条件:左括号数量不超过 n,右括号数量不超过左括号数量。
  • 不需要 used 数组,因为括号可以重复使用。

排列模板的变体

const backtrack = (path, left, right) => {
    // 结束条件
    if (left === n && right === n) { // TODO }
    
    // 选择1:选左括号
    if (left < n) {
        backtrack(path + '(', left + 1, right);
    }
    
    // 选择2:选右括号
    if (right < left) {
        backtrack(path + ')', left, right + 1);
    }
};

代码实现

var generateParenthesis = function(n) {
    const result = [];
    
    const backtrack = (path, left, right) => {
        // 结束条件:左右括号都用完了
        if (left === n && right === n) {
            result.push(path);
            return;
        }
        
        // 剪枝1:左括号数量不能超过 n
        if (left < n) {
            backtrack(path + '(', left + 1, right);
        }
        
        // 剪枝2:右括号数量不能超过左括号数量
        if (right < left) {
            backtrack(path + ')', left, right + 1);
        }
    };
    
    backtrack('', 0, 0);
    return result;
};

N皇后(LeetCode 51)—— 二维棋盘上的排列问题

题目

n 皇后问题研究的是如何将 n 个皇后放置在 n × n 的棋盘上,并且使皇后彼此之间不能相互攻击(即任意两个皇后都不能处于同一行、同一列或同一斜线上)。给你一个整数 n,返回所有不同的 n 皇后问题的解决方案。

示例:n = 4 输出: [ [".Q..", // 解法 1 "...Q", "Q...", "..Q."],

["..Q.", // 解法 2 "Q...", "...Q", ".Q.."] ]

思路解析

N皇后问题可以看作一个特殊的排列问题:

  • 每行只能放一个皇后,所以我们可以用 row 来表示当前处理到第几行
  • 每列也只能放一个皇后,所以我们需要记录哪些列已经被占用
  • 还需要考虑两个斜线方向

排列模板的变体

const backtrack = (row) => {
    if (row === n) {
        // 找到一个有效解
    }
    
    for (let col = 0; col < n; col++) {
        if (isValid(row, col)) {
            board[row] = col;  // 相当于做选择
            backtrack(row + 1); // 递归下一行
            board[row] = -1;    // 撤销选择
        }
    }
};

代码实现

var solveNQueens = function(n) {
    const result = [];
    // board 是一维数组,索引表示行,值表示该行皇后所在的列
    const board = new Array(n).fill(-1);
    
    // 检查在 (row, col) 放置皇后是否合法
    const isValid = (row, col) => {
        for (let i = 0; i < row; i++) {
            // 检查列冲突
            if (board[i] === col) return false;
            // 检查对角线冲突:行差 === 列差
            if (Math.abs(row - i) === Math.abs(col - board[i])) return false;
        }
        return true;
    };
    
    // 将 board 转换成题目要求的字符串数组格式
    const formatBoard = () => {
        return board.map(col => {
            const row = new Array(n).fill('.');
            row[col] = 'Q';
            return row.join('');
        });
    };
    
    const backtrack = (row) => {
        // 结束条件:所有行都放置了皇后
        if (row === n) {
            result.push(formatBoard());
            return;
        }
        
        // 在当前行尝试每一列
        for (let col = 0; col < n; col++) {
            if (!isValid(row, col)) continue; // 剪枝
            
            // 做选择:在 (row, col) 放置皇后
            board[row] = col;
            // 递归到下一行
            backtrack(row + 1);
            // 撤销选择
            board[row] = -1;
        }
    };
    
    backtrack(0);
    return result;
};

N皇后 vs 全排列:

维度 全排列 N皇后
选择列表 所有未使用的数字 所有列
递归参数 path row
约束条件 不能重复选 列、对角线不冲突
结束条件 path.length === n row === n

解数独(LeetCode 37)—— 排列问题的终极形态

题目

编写一个程序,通过填充空格来解决数独问题。数独的解法需遵循如下规则:

  1. 数字 1-9 在每一行只能出现一次
  2. 数字 1-9 在每一列只能出现一次
  3. 数字 1-9 在每一个 3x3 宫格内只能出现一次

思路解析

解数独是排列问题的终极形态,它结合了:

  • 行的排列约束:每行数字不能重复。
  • 列的排列约束:每列数字不能重复。
  • 宫的排列约束:每个 3x3 宫格内数字不能重复。

排列模板的变体

const backtrack = () => {
    for (let i = 0; i < 9; i++) {
        for (let j = 0; j < 9; j++) {
            if (board[i][j] === '.') {
                for (let num = 1; num <= 9; num++) {
                    if (isValid(i, j, num)) {
                        board[i][j] = num;
                        if (backtrack()) return true;
                        board[i][j] = '.';
                    }
                }
                return false;
            }
        }
    }
    return true;
};

代码实现

var solveSudoku = function(board) {
    const isValid = (row, col, num) => {
        const numStr = num.toString();
        
        // 检查行
        for (let i = 0; i < 9; i++) {
            if (board[row][i] === numStr) return false;
        }
        
        // 检查列
        for (let i = 0; i < 9; i++) {
            if (board[i][col] === numStr) return false;
        }
        
        // 检查 3x3 宫格
        const boxRow = Math.floor(row / 3) * 3;
        const boxCol = Math.floor(col / 3) * 3;
        for (let i = 0; i < 3; i++) {
            for (let j = 0; j < 3; j++) {
                if (board[boxRow + i][boxCol + j] === numStr) return false;
            }
        }
        
        return true;
    };
    
    const backtrack = () => {
        for (let i = 0; i < 9; i++) {
            for (let j = 0; j < 9; j++) {
                // 找到空白格
                if (board[i][j] === '.') {
                    // 尝试填入 1-9
                    for (let num = 1; num <= 9; num++) {
                        if (isValid(i, j, num)) {
                            // 做选择
                            board[i][j] = num.toString();
                            // 递归
                            if (backtrack()) return true;
                            // 撤销选择
                            board[i][j] = '.';
                        }
                    }
                    return false; // 1-9 都不行,说明之前的选择有问题
                }
            }
        }
        return true; // 所有格子都填满了
    };
    
    backtrack();
    return board;
};

排列问题的核心套路

做完这几道题,我们会发现它们其实都是排列思想的变形。

各题模板对比

题目 递归参数 选择列表 剪枝条件 特殊点
46.全排列 path 所有未使用元素 used[i] 无重复,基础模板
47.全排列II path 所有未使用元素 used[i] + 同层去重 需要排序 + 去重
剑指38.字符串排列 path 所有未使用字符 used[i] + 同层去重 和47一样
22.括号生成 path, left, right 左括号/右括号 left < n 和 right < left 不固定长度,约束特殊
51.N皇后 row 所有列 列冲突 + 对角线冲突 每行一个皇后,用一维数组记录
37.解数独 无(全局遍历) 1-9 行+列+宫格约束 最复杂的排列问题

解题要点

  • 排列用 used:排列问题都要用 used 数组记录哪些元素已用。
  • 去重先排序:有重复元素时,排序 + 同层去重。
  • 约束是剪枝:不合法的情况提前 continue 或 return。
  • 递归深度是条件:路径长度等于元素个数时,收获结果。

结语

希望这篇文章能帮大家彻底搞懂排列算法。下次遇到"所有排列"、"全排列"、"N皇后"这类问题,别忘了拿出这个万能模板试一试!

对于文章中错误的地方或有任何疑问,欢迎在评论区留言讨论!

两部门:推动人工智能、脑机接口等与医疗装备融合创新

2026年3月31日 09:12
36氪获悉,3月27日,推进医疗装备发展应用领导小组工作会议在重庆召开。会议强调,“十四五”时期,我国医疗装备发展应用取得显著成效,产业规模稳步扩大,技术创新不断突破,一批产品投入临床、惠及民生。“十五五”时期,要坚持统筹推进,滚动实施制造业重点产业链高质量发展行动,持续提升医疗装备产业链韧性和安全水平。要坚持需求导向,推动人工智能、脑机接口等与医疗装备融合创新,培育新的增长点。要坚持医工协同,全链条推进技术攻关、成果应用,提升软硬一体化供给能力

微信 ClawBot 接入本地 AI Agent 的实现原理

作者 Cobyte
2026年3月31日 09:09

1. 前言

我们知道微信最近推出了微信 ClawBot,用于在微信中与 OpenClaw 收发消息。掘金签约作者群里的一位大佬说:

01.jpg

受到启发,我就去研究了一下微信半公开的官方文档后发现,我们确实也可以通过用微信 ClawBot 接入任何 AI Agent

至于为什么说官方文档是半公开呢,因为官方暂时还没有公开的文档地址,但又可以通过某些渠道看到(怎么可以看到,本文最后揭晓)。

本文就将带你一步步实现如何通过微信 ClawBot 接入自己开发的 AI Agent。其实我们只需要做三件事:

  1. 扫码登录,拿到微信 ClawBot 的身份凭证;
  2. 长轮询等待消息,一有消息立刻获取;
  3. 把消息交给本地 Agent 处理,再把回复发回微信。

第一步,扫码登录。

2. 扫码登录

根据微信 ClawBot 文档的要求,我们先要获取一个二维码,等用户用微信扫描并确认后,服务端就会返回一个 bot_token 的通信凭证,后续所有请求都必须带着这个 token。这个跟我们平时的开发是一样,我们登录之后才能进行操作。

2.1 拉取二维码

首先微信 ClawBot 的接口地址是:

BASE_URL = "https://ilinkai.weixin.qq.com"

其次,登录的第一步是向服务端请求二维码。我们根据微信 ClawBot 的文档可以知道请求的接口是:

ilink/bot/get_bot_qrcode?bot_type=3

请求的 HTTP 方式是 GET。值得注意的是参数 bot_type 在微信 ClawBot 的文档中只出现了在获取二维码的时候,值是 3,而其他枚举值的情况,文档中并没有说明。

因为要用到 HTTP 的 GET 请求,所以我们需要封装一个 GET 请求的方法:

import json
import urllib.request
import urllib.error

# 省略...

def _get(url: str, headers: dict = {}, timeout: int = 35) -> dict:
    """发送 GET 请求"""
    req = urllib.request.Request(url, headers=headers, method="GET")
    try:
        with urllib.request.urlopen(req, timeout=timeout) as resp:
            return json.loads(resp.read().decode("utf-8"))
    except urllib.error.HTTPError as e:
        raise RuntimeError(f"HTTP {e.code} GET {url}: {e.read().decode(errors='replace')}") from e

接着我们封装拉取二维码的请求函数:

def fetchQRCode():
    base = BASE_URL.rstrip("/") + "/"
    url = base + "ilink/bot/get_bot_qrcode?bot_type=3"
    resp = _get(url)                      # GET 请求
    qrcode_raw = resp.get("qrcode")       # 服务端用于轮询的标识
    qrcode_url = resp.get("qrcode_img_content")   # 可扫描的二维码链接
    return qrcode_raw, qrcode_url

微信 ClawBot 服务端返回的 qrcode_img_content 是一个可以直接扫码的链接。我们在终端里可以通过安装 qrcode 库把它打印成 ASCII 二维码或者直接打印链接让用户打开链接,通过手机微信进行扫码。字段 qrcode 则是服务端的二维码标识,用于后续轮询二维码的状态,是否已经被扫码等。

我们测试一下上述代码:

print(fetchQRCode())

打印结果如下:

('50189c0db1817eb74a2bfc11e4ccdb35', 'https://liteapp.weixin.qq.com/q/7GiQu1?qrcode=50189c0db1817eb74a2bfc11e4ccdb35&bot_type=3')

我们打开上述链接在浏览器打开是一个微信二维码。

image.png

2.2 轮询扫码状态

二维码生成后,我们每隔一秒向微信 ClawBot 提供的二维码状态查询接口进行请求,直到用户完成确认。接着我们封装一个轮询扫码状态的请求接口函数:

def pollQRStatus(qrcode_raw):
    base = BASE_URL.rstrip("/") + "/"
    poll_url = base + f"ilink/bot/get_qrcode_status?qrcode={urllib.parse.quote(qrcode_raw)}"
    deadline = time.time() + 480    # 最多等 8 分钟
    # 这个是微信 ClawBot 规定的,没得解析
    headers = { "iLink-App-ClientVersion": "1" }

    while time.time() < deadline:
        try:
            s = _get(poll_url, headers)
        except Exception as e:
            print(f"  [轮询错误] {e}", flush=True)
            time.sleep(2)
            continue

        status = s.get("status", "wait")

        if status == "wait":
            # 还没扫,继续等,打一个点表示进度
            sys.stdout.write(".")
            sys.stdout.flush()

        elif status == "scaned":
            # 已经扫了,等用户在微信里点确认
            print("\n👀 已扫码,请在微信中点击确认...", flush=True)

        elif status == "confirmed":
            # ✅ 用户点了确认,登录成功!
            token      = s.get("bot_token", "")
            account_id = s.get("ilink_bot_id", "")
            # 账号 ID 规范化:把 @ 和 . 换成 -,例如 abc@im.wechat → abc-im-wechat
            account_id = account_id.replace("@", "-").replace(".", "-")
            real_base  = s.get("baseurl") or BASE_URL
            print(f"\n✅ 登录成功!account_id={account_id}", flush=True)
            return {"token": token, "account_id": account_id, "base_url": real_base}

        elif status == "expired":
            raise RuntimeError("二维码已过期,请重新运行程序。")
        # 每次轮询后(除非已返回成功)都会休眠 1 秒,避免高频请求对服务端造成压力
        time.sleep(1)
    raise RuntimeError("登录超时(8分钟),请重试。")

上述函数主要是实现了根据状态码处理不同情况。服务端返回的 JSON 中包含 status 字段,表示当前二维码的状态。我们根据其值进行分支处理:

status == "wait"

  • 表示二维码尚未被扫描。
  • 在终端打印一个点 .(不换行),表示程序仍在等待,给用户视觉反馈。

status == "scaned"

  • 表示用户已经扫描了二维码,但尚未在微信中点击“确认”。
  • 打印提示信息 👀 已扫码,请在微信中点击确认...,告知用户当前进度。

status == "confirmed"

  • 成功状态:用户已确认,登录成功。
  • 从响应中提取 bot_tokenilink_bot_id(机器人唯一标识)、baseurl(可选的后端地址)。
  • 并且对 account_id 进行规范化处理,将 @ 和 . 替换为 -,例如 abc@im.wechat 变为 abc-im-wechat
  • 打印成功信息,并返回一个包含 tokenaccount_idbase_url 的字典,供上层保存和后续请求使用。

status == "expired"

  • 二维码已过期(我们这里设置 8 分钟未扫描或确认即为过期)。
  • 抛出 RuntimeError,提示用户重新运行程序获取新二维码。

最后,每次轮询后(除非已返回成功)都会休眠 1 秒,避免高频请求对服务端造成压力。

2.3 实现登录并保存 token 到本地

我们在上面实现了拉取二维码的函数 fetchQRCode 和轮询等待扫码确认的函数 pollQRStatus,我们就可以实现一个登录函数 login 将整个流程串联起来了。实现如下:

def login() -> dict:
    """
    扫码登录,返回 {"token": "...", "account_id": "...", "base_url": "..."}
    """
    # ── 第 1 步:拉取二维码 ──
    [qrcode_raw, qrcode_url] = fetchQRCode() 

    if not qrcode_raw:
        raise RuntimeError(f"获取二维码失败")

    # ── 第 2 步:在终端打印二维码 ──
    print("\n请用微信扫描下方二维码:\n", flush=True)
    try:
        import qrcode                          # pip install qrcode[pil]
        qr = qrcode.QRCode(version=1, border=1)
        qr.add_data(qrcode_url)
        qr.make(fit=True)
        qr.print_ascii(invert=True)            # 用 ASCII 字符在终端渲染,尺寸最小
    except ImportError:
        # 没有安装 qrcode 库时,直接打印链接,用浏览器打开也能扫
        print(f"  {qrcode_url}\n", flush=True)

    # ── 第 3 步:轮询等待扫码 ──
    print("等待扫码...", flush=True)
    return pollQRStatus(qrcode_raw) 

上述登录函数最后返回的数据结构如下:

{
'token': 'd5b3973bb743@im.bot:060000215ac9e1ce7116aeb48b3d998c5b2e4e', 
'account_id': 'd5b3973bb743-im-bot',
'base_url': 'https://ilinkai.weixin.qq.com
'}

为了下次启动不用重新扫码,我们把 token 和 account_id 保存到文件 .weixin_token.json 中:

# token 的本地文件路径
TOKEN_FILE  = Path(__file__).parent / ".weixin_token.json"   # 保存登录后的 token

# 保存 token 和账号
def save_token(data: dict) -> None:
    """把 token 信息保存到本地文件,下次启动不用重新扫码。"""
    TOKEN_FILE.write_text(json.dumps(data, indent=2, ensure_ascii=False), "utf-8")
    TOKEN_FILE.chmod(0o600)    # 仅当前用户可读,保护 token 安全

这样,下次运行程序时,如果文件存在就直接加载,跳过登录流程。所以我们还需要有一个从本地文件读取上次保存的 token 和账号的函数。实现如下:

def load_token() -> Optional[dict]:
    """从本地文件读取上次保存的 token(如果有的话)。"""
    if TOKEN_FILE.exists():
        try:
            return json.loads(TOKEN_FILE.read_text("utf-8"))
        except Exception:
            pass
    return None

所以整个主流程就是:

def main():
    """
    程序入口,只做两件事:
      1. 登录(拿 token)
      2. 调 run_monitor() 开始监听
    """

    # 优先读取上次保存的 token,有就跳过扫码
    creds = load_token()

    if not creds:
        print("=== 微信扫码登录 ===", flush=True)
        creds = login()
        save_token(creds)
        print(f"[✓] token 已保存到 {TOKEN_FILE}", flush=True)

    token      = creds["token"]
    account_id = creds["account_id"]
    base_url   = creds.get("base_url", BASE_URL)
    print(f"\n[启动] account={account_id}  base={base_url}", flush=True)

    # 登录完成,进入消息监听循环
    # todo run_monitor()

登录完成之后,接下来就是进入消息监听循环了。

3. 长轮询监听循环

拿到 token 后我们就需要发起一个长轮询的 HTTP 接口请求,微信 ClawBot 服务端会“憋着”不返回,直到有新消息或超时(约 35 秒)才返回。这个接口就是:

ilink/bot/getupdates

它采用 POST 方式请求,所以我们需要封装一个 POST 请求的方法,并且微信 ClawBot 的所有 POST 的请求都需要一个通用请求头,通用请求头的要求如下:

Header 说明
Content-Type application/json
AuthorizationType 固定值 ilink_bot_token
Authorization Bearer <token>(登录后获取)
X-WECHAT-UIN 随机 uint32 的 base64 编码

所以我们先封装一个每次请求都需要带的 HTTP 请求头的函数:

def _headers(token: Optional[str] = None) -> dict:
    """
    构造每次请求都需要带的 HTTP 请求头。
    - AuthorizationType: 固定值,告诉服务端这是 bot token 认证
    - Authorization: 登录拿到的 token,未登录时不带
    - X-WECHAT-UIN: 随机数的 base64,模拟微信客户端标识
    """
    h = {
        "Content-Type": "application/json",
        "AuthorizationType": "ilink_bot_token",
        # 随机 uint32 → 十进制字符串 → base64,与原版协议一致
        "X-WECHAT-UIN": base64.b64encode(
            str(struct.unpack(">I", os.urandom(4))[0]).encode()
        ).decode(),
    }
    if token:
        h["Authorization"] = f"Bearer {token}"
    return h

接着我们封装一个通用 POST 请求函数:

def _url(path: str) -> str:
    """拼接完整 URL,确保 BASE_URL 末尾有斜杠。"""
    base = BASE_URL.rstrip("/") + "/"
    return base + path
    
def _post(path: str, body: dict, token: Optional[str] = None, timeout: int = 15) -> dict:
    """
    发送 POST JSON 请求,返回解析后的响应字典。
    所有与微信后端的通信都走这个函数。
    """
    data = json.dumps(body).encode("utf-8")
    req  = urllib.request.Request(
        _url(path),
        data    = data,
        headers = {**_headers(token), "Content-Length": str(len(data))},
        method  = "POST",
    )
    try:
        with urllib.request.urlopen(req, timeout=timeout) as resp:
            return json.loads(resp.read().decode("utf-8"))
    except urllib.error.HTTPError as e:
        raise RuntimeError(f"HTTP {e.code} /{path}: {e.read().decode(errors='replace')}") from e

所有与微信 ClawBot 后端的通信都走这个 _POST 函数。

接着我们封装长轮询接收消息函数:

def getUpdates(token: str, buf: str = "", timeout: int = 35) -> dict:
    """
    长轮询接口:向服务端发请求,服务端"憋着"不回,直到有新消息或超时才返回。

    参数:
      buf     - 上次返回的游标,传给服务端表示"从这里继续",首次传空字符串
      timeout - 等待秒数,服务端通常在 35 秒内有消息就返回,无消息就返回空

    返回值里的重要字段:
      msgs            - 新消息列表(可能为空)
      get_updates_buf - 新游标,下次请求要带上它
    """
    try:
        return _post(
            "ilink/bot/getupdates",
            body    = {"get_updates_buf": buf, "base_info": {"channel_version": "mini-bridge-1.0"}},
            token   = token,
            timeout = timeout + 5,    # 客户端超时比服务端多 5 秒,避免误判
        )
    except (TimeoutError, OSError) as e:
        if "timed out" in str(e).lower():
            # 超时是正常现象,不是错误,直接返回空结果,继续下一轮
            return {"ret": 0, "msgs": [], "get_updates_buf": buf}
        raise

上述 getUpdates 函数返回两个重要的字段 msgs(消息列表)和 get_updates_buf(游标),消息列表我们很好理解,但游标我们需要了解一下。

什么是游标?

游标就是一个字符串,每次 getUpdates 返回时都会同时返回一个新的游标,表示“下一次请求从这里开始”。我们可以把游标持久化到文件 .weixin_buf.txt 中,这样即使程序重启,也能接着之前的位置继续收消息,不会漏掉中间的消息。

游标存储的本地文件路径设置如下:

# 游标存储的本地文件路径
BUF_FILE    = Path(__file__).parent / ".weixin_buf.txt"      # 保存消息游标(断点续传)

根据微信 ClawBot 的官方文档我们可以知道 getUpdates 接口返回的字段如下:

字段 类型 说明
ret number 返回码,0 = 成功
errcode number? 错误码(如 -14 = 会话超时)
errmsg string? 错误描述
msgs WeixinMessage[] 消息列表(结构见下方)
get_updates_buf string 新的同步游标,下次请求时回传
longpolling_timeout_ms number? 服务端建议的下次长轮询超时(ms)

根据上述的资料我们就可以实现对 getUpdates 接口的长轮询监听循环了,实现如下:

def run_monitor(token: str) -> None:
    """
    长轮询监听循环
    """

    # ── 加载上次的消息游标(断点续传)──
    # 游标让服务端知道"从哪条消息开始",程序重启后不会漏掉中途的消息
    buf = BUF_FILE.read_text("utf-8").strip() if BUF_FILE.exists() else ""
    if buf:
        print("[✓] 从上次游标恢复", flush=True)
    print("[监听中] 等待微信消息...\n", flush=True)

    fail_count = 0    # 连续失败计数,失败太多就暂停一会儿

    while True:

        # ── 第一件事:等消息 ──
        try:
            resp = getUpdates(token, buf=buf)
        except Exception as e:
            # 网络抖动或服务端异常,失败超过 3 次才真正暂停
            fail_count += 1
            print(f"[错误] getUpdates 失败 ({fail_count}/3): {e}", flush=True)
            if fail_count >= 3:
                print("[退避] 连续失败 3 次,等待 30 秒后重试...", flush=True)
                fail_count = 0
                time.sleep(30)
            else:
                time.sleep(2)
            continue

        fail_count = 0

        # 服务端返回了业务错误码,打印后稍等再重试
        if resp.get("ret", 0) != 0 or resp.get("errcode", 0) != 0:
            print(f"[服务端错误] {resp}", flush=True)
            time.sleep(2)
            continue

        # 更新并持久化游标(下次重启可以从这里接着取消息)
        new_buf = resp.get("get_updates_buf", "")
        if new_buf:
            buf = new_buf
            BUF_FILE.write_text(buf, "utf-8")

上述实现 run_monitor 函数目前能长轮询监听微信 ClawBot 服务器消息的接收。主要功能如下:

  • 首先加载之前保存的消息游标(buf)实现断点续传。

  • 进入监听循环:

    1. 调用 getUpdates(token, buf) 获取消息(可能阻塞直到有消息或超时)。
    2. 如果调用失败(异常),则增加失败计数,连续失败 3 次后休眠 30 秒再重试;否则休眠 2 秒后继续。
    3. 如果返回的业务错误码非 0,则打印错误并休眠 2 秒后继续。
    4. 成功获取响应后,提取 get_updates_buf 新游标,更新 buf 并持久化到文件,以便下次重启恢复

我们知道上述 getUpdates 接口还返回了 msgs 消息列表,我们需要遍历返回的消息列表,提取文本,交给本地 AI Agent 进行处理。

4. 处理返回的消息

根据微信 ClawBot 的官方文档我们可以知道 getUpdates 接口返回的 msgs 消息列表字段结构如下:

字段 类型 说明
seq number? 消息序列号
message_id number? 消息唯一 ID
from_user_id string? 发送者 ID
to_user_id string? 接收者 ID
create_time_ms number? 创建时间戳(ms)
session_id string? 会话 ID
message_type number? 1 = USER, 2 = BOT
message_state number? 0 = NEW, 1 = GENERATING, 2 = FINISH
item_list MessageItem[]? 消息内容列表
context_token string? 会话上下文令牌,回复时需回传

然后字段 item_list(消息内容列表)的字段结构又如下:

字段 类型 说明
type number 1 TEXT, 2 IMAGE, 3 VOICE, 4 FILE, 5 VIDEO
text_item { text: string }? 文本内容
image_item ImageItem? 图片(含 CDN 引用和 AES 密钥)
voice_item VoiceItem? 语音(SILK 编码)
file_item FileItem? 文件附件
video_item VideoItem? 视频
ref_msg RefMessage? 引用消息

根据上述资料我们就可以处理微信 ClawBot 服务器返回的消息了。处理如下:

def run_monitor(token: str) -> None:

    # 省略...

    while True:

        # 省略...

        # ── 第二件事 + 第三件事:处理每条消息,回复用户 ──
        for msg in resp.get("msgs") or []:

            # 只处理用户发来的消息(message_type=1),忽略 bot 自己发的(=2)
            if msg.get("message_type") != 1:
                continue

            from_user = msg.get("from_user_id", "")
            ctx_token = msg.get("context_token", "")  # ← 必须原样回传给 send_message

            # 从消息的 item_list 里找 type=1(文本)的那一项
            text = ""
            for item in msg.get("item_list") or []:
                if item.get("type") == 1:               # type=1 是文本消息
                    text = (item.get("text_item") or {}).get("text", "")
                    break

            if not text.strip():
                continue    # 非文本消息(图片、语音等)暂不处理

            print(f"[收到] {from_user}: {text[:60]}", flush=True)

实现也很简单,遍历 msgs 消息列表,然后再从消息的 item_list 里找 type=1(文本)的那一项。而非文本消息(图片、语音等)我们暂不处理,先跑通主流程再说。

经过上述处理后我们就拿到了微信 ClawBot 服务器返回的文本消息了,我们接着就把它交给本地 Agent 进行处理。

5. 接入本地 AI Agent

前面的步骤已经实现了本地接收到微信 ClawBot 发来的信息了,现在就需要接入一个本地 AI Agent 来处理微信用户发来的信息了。接入本地 AI Agent 也很简单,我们前面的文章已经实现了一个 Agent Loop,我们直接使用就可以了。

我们定义一个函数 askAgent,用它来管理每个用户的对话历史,并将用户的新消息交给 Agent 处理:

# ── 导入本地 Agent ──
from agent import agent_loop, SYSTEM as AGENT_SYSTEM

# 每个微信用户维护一份独立的对话历史,key 是用户 ID
_sessions: dict[str, list] = {}

def askAgent(user_id: str, user_text: str) -> str:
    """
    把用户的消息交给 Agent 处理,返回 Agent 的回复文本。

    - 每个用户有自己独立的对话历史(_sessions),实现多用户隔离
    - agent_loop 会循环调用大模型直到得到最终回复
    """
    # 第一次对话时,初始化这个用户的历史,带上系统提示词
    if user_id not in _sessions:
        _sessions[user_id] = [{"role": "system", "content": AGENT_SYSTEM}]

    # 把用户这条消息追加到历史
    _sessions[user_id].append({"role": "user", "content": user_text})

    # 交给 Agent 处理,agent_loop 会直接修改传入的列表(追加 assistant 回复)
    try:
        reply = agent_loop(_sessions[user_id])
        return reply or "(无回复)"
    except Exception as e:
        return f"[Agent 出错] {e}"

我们上述函数 ask_agent 实现了把用户的消息交给 Agent 处理,然后返回 Agent 的回复文本,并且还实现每个用户有自己独立的对话历史,实现了多用户隔离。

接下来就是把 Agent 的回复发回微信。

6. 把 Agent 的回复发回微信

回复消息的接口是 ilink/bot/sendmessage,它最重要的参数是 context_token,这个 token 是从收到的消息里原样取出的,服务端依靠它来将回复与对话关联起来(类似于会话 ID)。我们来实现一个 sendMessage 函数进行发送信息:

def sendMessage(token: str, to_user_id: str, text: str, context_token: str) -> None:
    """
    向微信用户发送一条文本消息。

    重要:context_token 必须原样从收到的消息里取出并回传,
    服务端靠它把回复和对话关联起来。没有它,消息发不出去。
    """
    _post(
        "ilink/bot/sendmessage",
        token = token,
        body  = {
            "msg": {
                "from_user_id" : "",                            # bot 发送,留空
                "to_user_id"   : to_user_id,                   # 发给谁
                "client_id"    : f"mini-{secrets.token_hex(8)}",  # 本次消息的唯一ID,防重复
                "message_type" : 2,                            # 2 = BOT 消息
                "message_state": 2,                            # 2 = 消息已完成(非流式)
                "item_list"    : [{"type": 1, "text_item": {"text": text}}],  # type=1 是文本
                "context_token": context_token,                # ← 关键!必须带上
            },
            "base_info": {"channel_version": "mini-bridge-1.0"},
        },
    )

这里我们固定使用 message_type=2(机器人消息)、message_state=2(已完成,非流式)。client_id 是消息的唯一标识,用于去重,这里随机生成即可。

7. 整合运行

现在我们就可以把登录、收消息、处理消息、发消息串起来了,整合运行。

def run_monitor(token: str) -> None:
    """
    长轮询监听循环:持续等待微信消息,收到后交给 Agent 处理并回复。

    整个循环做三件事:
      1. 调 getUpdates() 等消息(服务端"憋着",有消息才返回)
      2. 遍历返回的消息列表,提取文本,交给 ask_agent() 得到回复
      3. 调 sendMessage() 把回复发回给用户

    参数:
      token - 登录后拿到的 bot_token,每次请求都要带上
    """

    # ── 加载上次的消息游标(断点续传)──
    # 游标让服务端知道"从哪条消息开始",程序重启后不会漏掉中途的消息
    buf = BUF_FILE.read_text("utf-8").strip() if BUF_FILE.exists() else ""
    if buf:
        print("[✓] 从上次游标恢复", flush=True)
    print("[监听中] 等待微信消息...\n", flush=True)

    fail_count = 0    # 连续失败计数,失败太多就暂停一会儿

    while True:

        # ── 第一件事:等消息 ──
        try:
            resp = getUpdates(token, buf=buf)
        except Exception as e:
            # 网络抖动或服务端异常,失败超过 3 次才真正暂停
            fail_count += 1
            print(f"[错误] getUpdates 失败 ({fail_count}/3): {e}", flush=True)
            if fail_count >= 3:
                print("[退避] 连续失败 3 次,等待 30 秒后重试...", flush=True)
                fail_count = 0
                time.sleep(30)
            else:
                time.sleep(2)
            continue

        fail_count = 0

        # 服务端返回了业务错误码,打印后稍等再重试
        if resp.get("ret", 0) != 0 or resp.get("errcode", 0) != 0:
            print(f"[服务端错误] {resp}", flush=True)
            time.sleep(2)
            continue

        # 更新并持久化游标(下次重启可以从这里接着取消息)
        new_buf = resp.get("get_updates_buf", "")
        if new_buf:
            buf = new_buf
            BUF_FILE.write_text(buf, "utf-8")

        # ── 第二件事 + 第三件事:处理每条消息,回复用户 ──
        for msg in resp.get("msgs") or []:

            # 只处理用户发来的消息(message_type=1),忽略 bot 自己发的(=2)
            if msg.get("message_type") != 1:
                continue

            from_user = msg.get("from_user_id", "")
            ctx_token = msg.get("context_token", "")  # ← 必须原样回传给 send_message

            # 从消息的 item_list 里找 type=1(文本)的那一项
            text = ""
            for item in msg.get("item_list") or []:
                if item.get("type") == 1:               # type=1 是文本消息
                    text = (item.get("text_item") or {}).get("text", "")
                    break

            if not text.strip():
                continue    # 非文本消息(图片、语音等)暂不处理

            print(f"[收到] {from_user}: {text[:60]}", flush=True)

            # 第二件事:把文本交给 Agent,得到回复
            reply = askAgent(from_user, text)
            print(f"[回复] {reply[:60]}", flush=True)

            # 第三件事:把 Agent 的回复发回微信
            try:
                send_message(token, from_user, reply, ctx_token)
                print("[✓] 已发送", flush=True)
            except Exception as e:
                print(f"[✗] 发送失败: {e}", flush=True)

最后,在主函数中,我们读取或登录获取 token,然后启动 run_monitor

def main():
    """
    程序入口,只做两件事:
      1. 登录(拿 token)
      2. 调 run_monitor() 开始监听
    """

    # 优先读取上次保存的 token,有就跳过扫码
    creds = load_token()
    # 不存在 token 就扫码登录
    if not creds:
        print("=== 微信扫码登录 ===", flush=True)
        creds = login()
        save_token(creds)
        print(f"[✓] token 已保存到 {TOKEN_FILE}", flush=True)

    token      = creds["token"]
    account_id = creds["account_id"]
    base_url   = creds.get("base_url", BASE_URL)
    print(f"\n[启动] account={account_id}  base={base_url}", flush=True)

    # 登录完成,进入消息监听循环
    run_monitor(token)

在运行前我们需要安装一下相关依赖。

requirements.txt 内容如下:

openai==2.24.0
itchat-uos>=1.3.10
qrcode_terminal == 0.8.0

然后执行:

pip install -r requirements.txt

接着我们运行上述代码结果显示如下:

image.png

接着我们使用微信扫码结果显示如下:

image.png

我们点击按钮继续,这时可以看到终端显示如下:

image.png

微信端显示如下:

image.png

聊天栏显示:

c4a8f799d97ee7c9a29b75fa359f5263.jpg

这时我们就可以通过微信 ClawBot 和我们本地自己写的 Agent 进行通讯了。比如我们之前实现的一个可以读取本地文件的 AI Agent,我们创建一个测试文件 test.txt,写上以下内容:

通过本文,我们完整实现了一个基于微信 ClawBot 协议的机器人

然后在微信 ClawBot 中输入:帮我读取 test.txt 的文件内容,显示如下

image.png

终端内容显示如下:

image.png

8. 总结与扩展

通过本文,我们完整实现了一个基于微信 ClawBot 协议的机器人,它能够:

  • 通过扫码登录
  • 长轮询接收消息
  • 调用任意本地 AI Agent 处理消息
  • 将回复发回给微信用户

整个程序的核心代码不到 200 行,却涵盖了微信 ClawBot 协议的关键点。你可以在此基础上轻松扩展:

  • 支持多轮对话:通过会话历史管理,我们已经实现了多轮对话的基础。
  • 支持图片、语音:解析消息中的 item_list,识别图片或语音,调用相应的 AI 模型(如图像识别、语音转文字)。
  • 支持命令识别:在文本中检测特定前缀(如 /help),触发不同功能。
  • 接入更强大的 Agent:例如集成 LangChain 实现复杂工作流、接入 Ollama 或 vLLM 等本地推理框架运行开源大模型、或增加联网搜索、RAG(检索增强生成)等能力。

最重要的是,这套方法不依赖任何第三方中间件,完全基于微信官方 ClawBot 协议,相对稳定可靠。你只需要一个微信账号,就能让你的 AI 助手 7×24 小时在线。

希望本文能帮你打开一扇窗,让你在微信这个庞大的社交平台上,用自己的 AI 能力创造更多有趣的应用。动手试一试吧,你会发现过程比想象中简单许多!

我是 程序员Cobyte,欢迎添加 v: icobyte,学习交流 AI 全栈。

最后怎么查看微信 ClawBot 的官方文档,可以通过 npm 安装 @tencent-weixin/openclaw-weixin-cli@tencent-weixin/openclaw-weixin 包,然后在 node_modules 目录中找对应的包里面有源码和文档。当然微信团队不公开可能后续会随时改变策略,所以须谨慎评估风险。

认识 Service Worker

作者 云浪
2026年3月31日 08:54

Service Worker 是一个运行在浏览器后台的 JavaScript 脚本,独立于网页主线程,是构建渐进式 Web 应用(PWA)的核心技术。它作为浏览器与网络之间的代理,可以实现离线访问、消息推送、后台同步等原生应用般的功能。

Service Worker 的主要特点:

  • 独立线程:不会阻塞页面渲染,也无法直接操作 DOM。

  • 网络代理:可以拦截页面发出的所有网络请求,并决定如何响应

  • 事件驱动:生命周期由一系列事件(install、activate、fetch 等)驱动

  • 需要 HTTPS:出于安全考虑,Service Worker 只在 HTTPS(或 localhost)环境下生效

if ("serviceWorker" in navigator) {
  const swUrl = new URL("./sw.js", window.location.href);
  const swScope = new URL("./", window.location.href).pathname;

  navigator.serviceWorker
    .register(swUrl, { scope: swScope })
    .then((reg) => console.log("SW registered", reg.scope))
    .catch((err) => console.error("SW registration failed", err));

  navigator.serviceWorker.ready.then(() => {
    console.log("Service Worker 已就绪");
  });

  navigator.serviceWorker.oncontrollerchange = () => {
    console.log("Service Worker 已激活");
  };
}

async function fetchData() {
  const resultDiv = document.getElementById("result");
  resultDiv.textContent = "正在请求中...";

  try {
    const response = await fetch(
      "https://jsonplaceholder.typicode.com/posts/1"
    );

    if (!response.ok) {
      throw new Error(`HTTP 错误!状态码:${response.status}`);
    }

    const data = await response.json();
    resultDiv.textContent = "请求成功!\n\n" + JSON.stringify(data, null, 2);
  } catch (error) {
    resultDiv.textContent = "请求失败:" + error.message;
    console.error("错误:", error);
  }
}

new URL() 是 JavaScript 内置的构造函数,用于解析和构建 URL 地址。它返回一个 URL 对象,该对象包含 URL 的各个组成部分(如协议、主机名、路径、查询参数等),并提供了方便的属性和方法来操作 URL。

new URL() 构造函数接受两个参数:要解析的 URL 地址和可选的基 URL 地址。如果省略了基 URL 地址,则默认为 undefined

如果 new URL()传入的第一个参数 url 是相对路径,则必须提供 base(基准 URL)来解析成完整地址。

如果传人的第一个参数 url 是绝对路径,则 base 参数将被忽略。

new URL(url);
new URL(url, base);
// 使用传入的相对 url 和基 url 构造一个绝对 url
const swUrl = new URL("./sw.js", window.location.href);

serviceWorker.register()scope 参数指定了 Service Worker 能够控制的范围,也就是它能拦截哪些路径下的网络请求。

如果不指定 scope,Service Worker 默认控制脚本所在目录及其子目录。例如,如果脚本在 /js/sw.js,则默认控制 /js/ 及更深的路径(如 /js/page/)。

设置 scope: "/" 表示整个网站(同源下所有页面)都会被该 Service Worker 拦截。如果设置 scope: "/admin/",则只有以 /admin/ 开头的页面和请求会被控制。

scope 不能超出脚本所在的路径层级。即 Service Worker 只能控制与其同目录或子目录的范围,不能控制父目录(除非脚本位于根目录,才能设置 scope: "/")。

navigator.serviceWorker.register(swUrl, { scope: swScope });

oncontrollerchange 事件在当前页面所关联的 Service Worker 控制器(controller)发生变化时触发。

navigator.serviceWorker.oncontrollerchange = () => {
  console.log("Service Worker 已激活");
};

Service Worker 有 4 个生命周期,该生命周期由浏览器管理,主要分为以下阶段:

  1. 注册(Registration):页面通过 JavaScript 告诉浏览器 Service Worker 脚本的位置。

  2. 安装(Install):首次注册或版本更新时触发,适合预缓存静态资源。

  3. 激活(Activate):安装成功后触发,可用于清理旧缓存、接管页面。

  4. 空闲/终止:当没有事件需要处理时,浏览器会终止 Service Worker 以节省内存,下次需要时再唤醒。

sw.js 文件内容如下:

self.addEventListener("install", () => {
  console.log("安装");
  self.skipWaiting(); // 跳过等待,直接激活
});

self.addEventListener("activate", (event) => {
  console.log("激活");
  event.waitUntil(self.clients.claim()); // 立即接管所有页面
});

self.addEventListener("fetch", (event) => {
  console.log("Service Worker 拦截到请求:", event.request.url);
  event.respondWith(fetch(event.request));
});

fetchXMLHttpRequest 的一个很大区别是,fetch 可以在 Service Worker 中使用。

self.skipWaiting() 的作用是让新安装的 Service Worker 跳过 waiting 阶段,尽快进入 active 状态 。

正常流程里,新的 SW 安装后会先 waiting ,要等旧 SW 不再控制任何页面才会激活。

调用 self.skipWaiting() 后,新 SW 不用等那么久,会尝试立即激活。

clients.claim() 让“已激活 SW 立刻接管页面”。

event.waitUntil() 的作用是: 告诉浏览器“这个事件还有异步任务没完成,请先别结束” 。

监听 fetch 事件的作用是:拦截当前 Service Worker 作用域内的所有网络请求,并决定如何返回响应 。

event.respondWith(fetch(event.request)) 把请求原样转发到网络,再把网络结果返回给页面。

event.respondWith()的作用是: 在 fetch 事件里接管这次请求,并指定返回给页面的响应。

总结

Service Worker 是运行在浏览器后台的独立线程脚本,作为浏览器与网络之间的代理,可用于离线访问、消息推送和后台同步等功能。

  • 核心特点:独立线程、事件驱动、可拦截网络请求、仅在 HTTPS(或 localhost)下生效。

  • 注册与路径:通过 navigator.serviceWorker.register() 注册,常用 new URL() 构造 sw.js 路径;scope 用于限定控制范围,不能超出脚本所在目录。

  • 生命周期:四个阶段 —— 注册(Registration)、安装(Install,可用于预缓存)、激活(Activate,可清理旧缓存并接管页面)、空闲/终止(节省资源,按需唤醒)。

  • 常用 API 与模式

    • self.skipWaiting()(跳过 waiting,快速激活)、
    • clients.claim()(激活后立即接管页面)、
    • event.waitUntil()(延长事件直至异步任务完成)、
    • fetch 事件中使用 event.respondWith() 拦截并返回响应。
  • 实践示例:文中示例的 sw.js 展示了 installactivatefetch 的基本写法,以及在 fetch 中将请求透传到网络 event.respondWith(fetch(event.request)) 的简单用法。

以上要点概览了 Service Worker 的作用、注册方式、作用范围、生命周期与常见实现模式,便于快速上手和理解在实践中如何拦截与处理请求。

参考

Using Service Workers

萝卜快跑正式启动迪拜全无人商业化运营

2026年3月31日 08:53
36氪获悉,3月30日,百度旗下萝卜快跑(Apollo Go)在迪拜正式启动全无人驾驶商业化运营。由此,萝卜快跑成为迪拜首个且唯一提供无人驾驶服务的一站式自营平台,并已建成当地规模最大的无人车队。

中信证券:价格上涨+国产突破,碳纤维或迎机遇

2026年3月31日 08:48
36氪获悉,中信证券研报称,在成本提升驱动产品涨价、高端供给紧平衡及国产技术进步三重因素驱动下,碳纤维行业有望迎来投资机遇。建议围绕盈利中枢提升和国产技术突破两条投资主线进行布局:一、国内头部企业凭借技术壁垒与产能优势有望充分受益于价格上涨与高端需求扩张;二、在T系列和M系列碳纤维国产技术突破中具有先发卡位优势的公司有望率先受益。

两市融资余额增加78.02亿元

2026年3月31日 08:47
36氪获悉,截至3月30日,上交所融资余额报13194.68亿元,较前一交易日增加37.75亿元;深交所融资余额报12710.26亿元,较前一交易日增加40.27亿元;两市合计25904.94亿元,较前一交易日增加78.02亿元。

三星电子宣布注销价值14.5亿韩元库存股

2026年3月31日 08:47
三星电子周二表示,作为一项旨在提升股东价值的股票回购计划的一部分,该公司计划注销价值14.5万亿韩元(约合95亿美元)的库存股。该公司在一份监管文件中称,约7330万股普通股和1360万股优先股将于周四注销。该公司表示,此举是继去年2月和7月董事会决定回购自身股份之后采取的。(新浪财经)

中信证券:存力升级为当前智能体推理核心需求,坚定看好存储成长趋势

2026年3月31日 08:45
36氪获悉,中信证券研报称,AI从“简单对话”向“智能体(Agent)”演进,驱动上下文长度激增。据Epoch AI数据,最长上下文窗口约每年增长30x,KV Cache显存容量和上下文长度呈线性增长关系,远超硬件配置增速。目前大模型厂商、硬件厂商主要通过量化、分层存储、模型架构优化的方式解决存力瓶颈,但仍不改显存需求爆发。中信证券认为,显存优化有望降低单Token生成成本,进而刺激用户开启更高并发与更长上下文,总存力需求将不减反增,存力升级为当前Agent推理核心需求,坚定看好存储成长趋势。

黑芝麻智能2025年营收创历史新高,三大业务引擎协同驱动

2026年3月31日 08:45
3月31日,黑芝麻智能发布2025年业绩公告,全年营收达8.22亿元,同比增长73.4%,营收连续三年实现⾼增⻓,其中具身智能解决方案营收高达9630万元;⽑利从1.95亿元稳步增⻓至3.37亿元。得益于具身智能及解决方案业务高速增长,高阶辅助驾驶及解决方案随规模放量带来的毛利提升和运营成本的降低,黑芝麻智能全年经营性亏损同比收窄17.5%,财务数据整体呈现业务快速放量、战略投入聚焦、成长质量提升的良好态势。

“元思生肽”完成1.5亿美元B轮融资

2026年3月31日 08:42
36氪获悉,“元思生肽”宣布完成1.5亿美元B轮融资,由一家国际生物科技基金领投,并由德诚资本与鼎晖VGC联合领投,阿布扎比投资局(ADIA)旗下全资子公司、淡马锡独立全资子公司淡明资本、启明创投、博远资本及知名产业投资机构跟投。同时,原有股东阿斯利康、礼来亚洲基金、创新工场、五源资本、高瓴创投(GL Ventures)、Biotech Development Fund及联想创投等机构持续加持。资金将重点用于深化其自主研发的大环肽发现平台Synova™的智能化迭代,并加速公司多元化创新药管线向临床阶段转化。
❌
❌