标签 npm 下的文章

网络安全研究人员披露了一起针对 Open VSX 插件仓库的供应链攻击详情,不明身份的威胁攻击者攻陷了一名合法开发者的相关资源,借此向下游用户推送恶意更新包。
“2026 年 1 月 30 日,由开发者 oorzc 发布的四款成熟 Open VSX 插件,被上传了嵌入GlassWorm 恶意软件加载器的恶意版本”,Socket 安全研究员基里尔・博伊琴科在周六发布的报告中指出。
“这些插件此前一直以正规开发者工具的身份存在(部分插件发布时间已超过两年),在恶意版本发布前,累计下载量已超 22000 次。”
这家供应链安全公司表示,此次攻击的核心是开发者的插件发布凭据遭窃取,Open VSX 安全团队评估认为,攻击者的作案手段要么是利用泄露的令牌,要么是通过其他方式实现了未授权访问。目前,这些恶意插件版本已被从 Open VSX 平台下架。
已确认的涉事插件名单如下:
  • FTP/SFTP/SSH 同步工具(oorzc.ssh-tools — 版本 0.5.1)
  • 国际化工具(oorzc.i18n-tools-plus — 版本 1.6.8)
  • vscode 思维导图(oorzc.mind-map — 版本 1.0.61)
  • scss 转 css(oorzc.scss-to-css-compile — 版本 1.3.4)
Socket 指出,这些被篡改的插件版本,其设计目的是投递一款与已知攻击活动相关联的加载器恶意软件,即 GlassWorm。该加载器可在运行时解密并执行嵌入的恶意代码,采用了一种日趋武器化的技术EtherHiding来获取命令与控制(C2)服务器地址,最终会执行恶意代码,窃取苹果 macOS 系统的账户凭据以及加密货币钱包数据。
同时,这款恶意软件并非立即触发执行,而是会先对受感染设备进行环境探查,确认设备不属于俄语区域后才会启动。这种模式在俄语区相关威胁组织开发的恶意程序中十分常见,目的是避免在本土范围内遭到法律追责。
该恶意软件窃取的信息类型包括:
  • 火狐浏览器及基于 Chromium 内核的浏览器数据(登录凭证、Cookie、上网记录,以及 MetaMask 等钱包插件数据)
  • 加密货币钱包文件(涵盖 Electrum、Exodus、Atomic、Ledger Live、Trezor Suite、币安、TonKeeper 等主流钱包)
  • iCloud 钥匙串数据库
  • Safari 浏览器 Cookie
  • 苹果备忘录数据
  • 桌面、文档、下载文件夹中的用户文件
  • FortiClient VPN 配置文件
  • 开发者凭据(例如~/.aws 和~/.ssh 目录下的文件)
针对开发者信息的窃取行为存在严重风险,可能导致企业环境面临云账户被攻陷、攻击者横向渗透内网等威胁。
“该恶意载荷包含专门的程序逻辑,可定位并提取日常开发流程中使用的认证信息,包括检查 npm 配置中的_authToken 令牌、读取 GitHub 认证相关文件等,这些信息可被用于访问私有代码仓库、持续集成密钥以及发布自动化系统。” 博伊琴科补充道。
此次攻击的一个显著特点在于,它与此前发现的 GlassWorm 攻击特征存在差异 ——攻击者借助遭攻陷的合法开发者账户来传播恶意软件。而在以往的攻击活动中,该威胁组织通常会采用 “仿冒拼写插件名”“品牌劫持” 的手段,上传伪造插件以实现恶意传播。
“该威胁组织的攻击行为完全融入开发者的日常工作流程,将恶意执行逻辑隐藏在加密的、运行时解密的加载器中,还利用 Solana 区块链备忘录作为动态秘密传输点,无需重新发布插件即可更换中转服务器。”Socket 表示,“这些设计方案降低了静态特征检测的有效性,迫使防御方将防护重心转向行为检测与快速响应。”

Pagefind 是一个专为静态网站设计的开源搜索引擎,它能够自动索引你的网站并提供完全离线的搜索体验。

核心特性

  • 按需加载:只下载搜索相关的内容片段,而不是整个索引
  • 轻量级:核心 JS 仅约 20KB,索引文件高度压缩(相比 Lunr.js 减少 85%)
  • 零配置:自动识别内容,开箱即用
  • 多语言支持:内置中文、日文等多语言分词器
  • 完全静态:无需服务器端支持,支持完全离线

快速上手

三步启用搜索

# 1. 构建你的静态网站
npm run build

# 2. 生成搜索索引
npx pagefind --source "dist"

# 3. 在 HTML 中添加搜索界面
<link href="/pagefind/pagefind-ui.css" rel="stylesheet">
<div id="search"></div>
<script src="/pagefind/pagefind-ui.js"></script>
<script>
    new PagefindUI({ element: "#search" });
</script>

Pagefind 会自动在 dist/pagefind/ 目录下生成索引文件。

核心用法

控制索引范围

使用 data-pagefind-body 标记要索引的内容:

<main data-pagefind-body>
    <h1>文章标题</h1>
    <p>这部分内容会被索引</p>
</main>

<!-- 使用 data-pagefind-ignore 排除特定内容 -->
<div data-pagefind-ignore>
    <h2>评论</h2>
    <div class="comments">...</div>
</div>

添加元数据和权重

<!-- 自定义元数据 -->
<article data-pagefind-body
         data-pagefind-meta="author:张三,date:2024-01-01">
    <h1 data-pagefind-weight="10">文章标题</h1>
    <p data-pagefind-weight="5">摘要内容...</p>
    <div>正文内容...</div>
</article>

配置文件

# pagefind.yml
source: "dist"
exclude_selectors:
  - "nav"
  - ".sidebar"
force_language: "zh-cn"

自定义搜索 UI

import * as pagefind from '/pagefind/pagefind.js';

const search = await pagefind.search("React");
const results = await Promise.all(
    search.results.map(r => r.data())
);

实战指南

集成到构建流程

{
  "scripts": {
    "build": "vite build",
    "postbuild": "pagefind --source dist"
  }
}

React 自定义搜索组件

import { useState } from 'react';

function Search() {
    const [results, setResults] = useState([]);

    const handleSearch = async (e) => {
        const { default: pagefind } = await import('/pagefind/pagefind.js');
        const search = await pagefind.search(e.target.value);
        const data = await Promise.all(
            search.results.slice(0, 5).map(r => r.data())
        );
        setResults(data);
    };

    return (
        <>
            <input type="search" onChange={handleSearch} />
            {results.map((r, i) => (
                <a key={i} href={r.url}>
                    <h3>{r.meta.title}</h3>
                    <p dangerouslySetInnerHTML={{ __html: r.excerpt }} />
                </a>
            ))}
        </>
    );
}

最佳实践

1. 只索引主要内容

<!-- ✅ 推荐 -->
<main data-pagefind-body>
    <article>...</article>
</main>

2. 使用权重优化结果

<h1 data-pagefind-weight="10">标题</h1>
<p data-pagefind-weight="5">摘要</p>

3. CLI 参数配置

# 排除选择器
pagefind --source "dist" --exclude-selectors "nav" --exclude-selectors "footer"

# 强制语言
pagefind --source "dist" --force-language "zh-cn"

配置参考

HTML 属性

属性说明
data-pagefind-body标记要索引的主要内容区域
data-pagefind-ignore排除该元素及其子元素
data-pagefind-meta添加自定义元数据
data-pagefind-filter定义可过滤的字段
data-pagefind-sort定义可排序的字段
data-pagefind-weight设置内容权重(1-10)

JavaScript API

// 高级搜索
const search = await pagefind.search("React", {
  filters: { category: "tutorial" },
  sort: { date: "desc" },
  limit: 10
});

// 获取结果
const results = await Promise.all(
  search.results.map(r => r.data())
);

原理深度解析

整体架构

首先通过架构图了解 Pagefind 的整体设计:

graph TB
    subgraph "构建阶段 Build Time"
        A[HTML 文件] --> B[内容扫描器]
        B --> C[内容提取器]
        C --> D[多语言分词器]
        D --> E[倒排索引构建器]
        E --> F[索引分片器]
        F --> G[压缩引擎]
        G --> H[索引文件]
    end

    subgraph "运行阶段 Runtime"
        I[用户查询] --> J[查询分词]
        J --> K[哈希计算]
        K --> L[按需加载器]
        H --> L
        L --> M[索引查询]
        M --> N[TF-IDF 评分]
        N --> O[结果排序]
        O --> P[内容片段加载]
        P --> Q[摘要生成]
        Q --> R[搜索结果]
    end

    subgraph "缓存层 Cache Layer"
        S[浏览器缓存]
        T[内存缓存]
        L -.-> S
        L -.-> T
    end

    style A fill:#e1f5ff
    style H fill:#e1f5ff
    style I fill:#fff3e0
    style R fill:#fff3e0

索引构建过程

Pagefind 的工作流程可以分为两个阶段:构建时索引运行时搜索

1. 构建时索引(Build Time)

当你运行 pagefind --source "dist" 时,Pagefind 会执行以下步骤:

flowchart TD
    Start([开始构建]) --> Scan[扫描 HTML 文件]
    Scan --> Parse[解析 HTML DOM]
    Parse --> Extract[提取内容]

    Extract --> CheckBody{检查 data-pagefind-body}
    CheckBody -->|找到| UseBody[使用标记的内容]
    CheckBody -->|未找到| UseDefault[使用 body 全部内容]

    UseBody --> Filter[应用排除规则]
    UseDefault --> Filter

    Filter --> Meta[提取元数据]
    Meta --> Tokenize[文本分词]

    Tokenize --> CheckLang{检测语言}
    CheckLang -->|英文| EnTokenizer[英文分词器]
    CheckLang -->|中文| ZhTokenizer[中文分词器 n-gram]
    CheckLang -->|其他| OtherTokenizer[对应语言分词器]

    EnTokenizer --> BuildIndex[构建倒排索引]
    ZhTokenizer --> BuildIndex
    OtherTokenizer --> BuildIndex

    BuildIndex --> CalcWeight[计算词条权重]
    CalcWeight --> Shard[索引分片 256个桶]

    Shard --> Compress[压缩处理]
    Compress --> GenFragment[生成内容片段]
    GenFragment --> WriteFiles[写入文件]

    WriteFiles --> Output[输出到 pagefind/]
    Output --> End([构建完成])

    style Start fill:#90EE90
    style End fill:#FFB6C1
    style BuildIndex fill:#FFE4B5
    style Compress fill:#E0FFFF

关键技术点:

  • 倒排索引:对于每个词条,记录它出现在哪些文档的哪些位置
  • 分片存储:将索引拆分成小块,按需加载(使用一致性哈希算法分配到 256 个桶)
  • 压缩算法:使用高效的压缩减少文件大小

索引结构详解:

pagefind/
├── pagefind.js           # 核心搜索引擎(~20KB)
│                         # - 包含哈希函数
│                         # - 索引加载器
│                         # - 搜索算法
│
├── pagefind-ui.js        # UI 组件(~15KB)
├── pagefind-ui.css       # 样式文件(~3KB)
│
├── index/                # 索引分片(256 个)
│   ├── index_00.pf       # 哈希值 0x00-0x00
│   ├── index_01.pf       # 哈希值 0x01-0x01
│   ├── ...
│   └── index_ff.pf       # 哈希值 0xFF-0xFF
│
├── fragment/             # 内容片段
│   ├── en_<hash>.pf      # 英文页面片段
│   ├── zh_<hash>.pf      # 中文页面片段
│   └── ...
│
└── filter/               # 过滤器数据(如果使用)
    ├── category.pf
    └── tags.pf

2. 运行时搜索(Runtime)

当用户输入搜索查询时的完整时序:

sequenceDiagram
    actor User as 用户
    participant UI as 搜索界面
    participant Core as Pagefind 核心
    participant Cache as 浏览器缓存
    participant Server as 静态服务器

    User->>UI: 输入 "React 教程"
    UI->>UI: 防抖延迟 (300ms)

    UI->>Core: search("React 教程")
    Core->>Core: 分词 ["React", "教程"]

    par 并行计算哈希
        Core->>Core: hash("React") = 0x42
        Core->>Core: hash("教程") = 0xA7
    end

    par 并行加载索引分片
        Core->>Cache: 检查 index_42.pf
        Cache-->>Core: 缓存未命中
        Core->>Server: GET /pagefind/index/index_42.pf
        Server-->>Core: 返回索引数据 (5KB)

        Core->>Cache: 检查 index_a7.pf
        Cache-->>Core: 缓存命中
        Cache-->>Core: 返回缓存数据
    end

    Core->>Core: 解析索引分片
    Core->>Core: 查找匹配文档<br/>"React": [1,5,23]<br/>"教程": [1,8,15]<br/>交集: [1]

    Core->>Core: 计算 TF-IDF 得分
    Core->>Core: 排序结果

    Core->>Cache: 检查 fragment_1.pf
    Cache-->>Core: 缓存未命中
    Core->>Server: GET /pagefind/fragment/zh_1.pf
    Server-->>Core: 返回内容片段 (12KB)

    Core->>Core: 提取摘要<br/>高亮关键词
    Core->>Core: 生成结果对象

    Core-->>UI: 返回搜索结果
    UI->>UI: 渲染结果列表
    UI-->>User: 显示搜索结果

    Note over Core,Server: 总耗时: ~80ms<br/>网络请求: 2 个 (17KB)<br/>缓存命中: 1 个

性能分析:

阶段耗时说明
用户输入 + 防抖300ms等待用户完成输入
分词 + 哈希计算<5ms纯计算,无 I/O
加载索引分片20-50ms取决于网络和缓存
索引查询 + 评分5-10ms纯内存操作
加载内容片段15-30ms取决于网络和缓存
摘要生成 + 渲染5-10msDOM 操作
总计(首次)~80ms不含防抖延迟
总计(缓存)~25ms索引和片段均已缓存

核心技术解析

1. 按需加载机制

Pagefind 最大的创新是渐进式加载。传统的客户端搜索(如 Lunr.js)需要加载完整索引:

// 传统方案:需要加载整个索引
// 假设网站有 1000 个页面,索引文件可能有 5MB
await loadFullIndex(); // 加载 5MB
search("React");

Pagefind 的方案:

// Pagefind:按需加载
search("React");
// 1. 根据 "React" 计算哈希 -> 只加载包含 "React" 的索引分片(可能只有 10KB)
// 2. 找到匹配的文档 ID
// 3. 只加载这些文档的内容片段(可能 20KB)
// 总共只需要下载 30KB,而不是 5MB

实现原理:

查询词 "React"
    ↓
计算哈希:hash("React") = 0x3A7F
    ↓
确定分片:0x3A7F % 256 = 127
    ↓
加载:GET /pagefind/index/index_127.pf
    ↓
解析分片,找到文档 ID: [5, 23, 87]
    ↓
加载内容:GET /pagefind/fragment/en_005.pf

2. 倒排索引结构

倒排索引是搜索引擎的核心数据结构:

正向索引(文档 → 词条):
文档1: ["React", "教程", "入门"]
文档2: ["Vue", "教程", "进阶"]
文档3: ["React", "进阶", "Hooks"]

倒排索引(词条 → 文档):
"React"  → [文档1, 文档3]
"Vue"    → [文档2]
"教程"   → [文档1, 文档2]
"入门"   → [文档1]
"进阶"   → [文档2, 文档3]
"Hooks"  → [文档3]

当搜索 "React 教程" 时:

  1. 查找 "React" → [文档1, 文档3]
  2. 查找 "教程" → [文档1, 文档2]
  3. 取交集 → [文档1]

3. TF-IDF 相关性评分

Pagefind 使用 TF-IDF 算法计算搜索结果的相关性:

TF(词频):词条在文档中出现的频率

TF(t, d) = 词条 t 在文档 d 中出现的次数 / 文档 d 的总词数

IDF(逆文档频率):词条的稀有程度

IDF(t) = log(总文档数 / 包含词条 t 的文档数)

TF-IDF 得分

TF-IDF(t, d) = TF(t, d) × IDF(t)

示例计算:

假设我们有 100 个文档,搜索 "React Hooks":

文档A:
- "React" 出现 10 次,文档总词数 100
  TF("React", A) = 10/100 = 0.1
  包含 "React" 的文档有 30 个
  IDF("React") = log(100/30) = 0.52
  TF-IDF("React", A) = 0.1 × 0.52 = 0.052

- "Hooks" 出现 5 次
  TF("Hooks", A) = 5/100 = 0.05
  包含 "Hooks" 的文档有 5 个
  IDF("Hooks") = log(100/5) = 1.30
  TF-IDF("Hooks", A) = 0.05 × 1.30 = 0.065

文档A 总分 = 0.052 + 0.065 = 0.117

"Hooks" 更稀有,所以权重更高。

4. 多语言分词

Pagefind 内置了多种语言的分词器:

英文分词(基于空格和标点):

"Hello, world!" → ["hello", "world"]

中文分词(基于字典和统计):

"自然语言处理" → ["自然", "语言", "处理"]
或 → ["自然语言", "处理"]
或 → ["自然语言处理"]

Pagefind 使用 n-gram 技术处理 CJK 文本:

"搜索引擎" → ["搜索", "搜索引", "搜索引擎", "索引", "索引擎", "引擎"]

这样即使查询 "搜索" 或 "引擎",也能匹配到 "搜索引擎"。

性能优化技术

Pagefind 通过多种技术实现高性能:

索引压缩(原始 10MB → 500KB,压缩率 95%):

  • 去除 HTML 标签和属性
  • 词干提取(stemming):"running" → "run"
  • 停用词过滤(去除 "the", "a", "is" 等常见词)
  • 增量编码 + Gzip 压缩

并行加载
支持 HTTP/2 多路复用,多个词条的索引分片并行加载,总耗时 = max(单个加载时间)。

技术内幕深度剖析

1. 核心算法实现

Pagefind 是用 Rust 编写并编译为 WASM,核心逻辑包括:

哈希计算(FNV-1a 算法):

// 词条归一化(转小写、去除特殊字符)→ FNV-1a 哈希 → 映射到 0-255
hash("React") = 0x42 (66)
hash("react") = 0x42 (66)  // 大小写不敏感

索引加载器

  1. 计算词条哈希 → 确定分片编号
  2. 检查内存缓存 → 未命中则加载对应的 .pf 文件
  3. 解析二进制格式 → 存入缓存
  4. 返回词条对应的文档 ID 列表

TF-IDF 评分器

// 计算每个文档的相关性得分
score = Σ(TF × IDF × weight) × lengthNorm
// - TF: 词频
// - IDF: 逆文档频率(缓存优化)
// - weight: 自定义权重
// - lengthNorm: 长度归一化(防止长文档占优)

2. .pf 文件格式

Pagefind 使用自定义的 .pf(Pagefind Format)二进制格式:

索引文件(index_XX.pf)

  • Header:Magic Number (0x5046 'PF') + 版本 + 标志 + 条目数
  • Entries:每个词条 → 文档 ID 列表(增量编码)

示例:"React" → [1, 5, 23] 存储为 [1, +4, +18]

内容片段(fragment_XX.pf)

  • Header:Magic Number + 压缩类型 + 文档 ID + 长度
  • Metadata:JSON 格式(title, url, excerpt 等)
  • Content:原始文本 + 词条位置映射

3. 四层压缩策略

graph LR
    A[原始数据<br/>100KB] --> B[增量编码<br/>50KB]
    B --> C[VarInt 编码<br/>40KB]
    C --> D[词干提取<br/>30KB]
    D --> E[Gzip 压缩<br/>25KB]

    style E fill:#90EE90

Level 1: 增量编码(Delta Encoding)

  • 文档 ID [1, 5, 23, 45][1, +4, +18, +22]
  • 节省 50% 存储空间

Level 2: 变长整数编码(VarInt)

  • 小数字用 1 字节,大数字自动扩展
  • 1 → [0x01]128 → [0x80, 0x01]

Level 3: 词干提取(Stemming)

  • "running", "runs", "runner" → "run"
  • 减少唯一词条数量 30-40%

Level 4: Gzip 压缩

  • 文本压缩率 60-80%
  • 最终实现 95% 总压缩率

4. 三层缓存架构

graph TD
    A[搜索请求] --> B{L1 内存缓存}
    B -->|命中| C[返回结果]
    B -->|未命中| D{L2 HTTP 缓存}
    D -->|命中| C
    D -->|未命中| E{L3 Service Worker}
    E -->|命中| C
    E -->|未命中| F[网络请求]
    F --> G[更新所有缓存]
    G --> C

    style B fill:#FFE4B5
    style D fill:#E0FFFF
    style E fill:#F0E68C
缓存层级命中延迟容量适用场景
L1 内存缓存<1ms~10MB频繁访问的索引(LRU 淘汰)
L2 HTTP 缓存~5ms~100MB已访问的所有索引(Cache-Control)
L3 Service Worker~10ms~50MB离线访问(可选)
网络请求50-200ms-首次访问

性能提升

  • 首次搜索:~80ms
  • 后续搜索(缓存命中):~25ms
  • 离线模式:~25ms

服务器配置(Nginx):

location /pagefind/ {
    add_header Cache-Control "public, max-age=31536000, immutable";
    gzip on;
}

性能对比

方案初次加载索引大小 (1000页)搜索速度离线支持
Pagefind~20KB~500KB<50ms
Algolia0 (CDN)N/A<10ms
Lunr.js~30KB~3MB~100ms

实际数据(500 页文档网站):

  • 首次搜索:下载 45KB,耗时 ~80ms
  • 后续搜索:下载 10KB,耗时 ~25ms
  • 对比 Lunr.js:减少 97% 的下载量

常见问题

Q: Pagefind 与 Algolia 如何选择?

  • Pagefind:中小型网站(< 10,000 页)、免费、离线支持、重视隐私
  • Algolia:大型网站、高级功能、极致速度、付费

Q: 支持哪些框架?
框架无关,支持 VitePress、Docusaurus、Hugo、Jekyll、Astro、Next.js(SSG)等任何生成 HTML 的工具。

Q: 是否影响 SEO?
不影响。Pagefind 的搜索 UI 是客户端渲染的,原始 HTML 内容完全不受影响。

Q: 如何更新索引?
每次构建时重新生成索引。在 CI/CD 中使用 postbuild 脚本自动化。

总结

Pagefind 为静态网站提供了轻量、高性能的搜索方案:

  • 轻量级:核心 20KB,按需加载
  • 高性能:搜索响应 < 50ms
  • 零配置:开箱即用
  • 完全静态:无需服务器,支持离线
  • 多语言:内置 CJK 分词

核心原理

  1. 倒排索引 + 分片:将索引拆分成 256 个小块
  2. 按需加载:根据查询词哈希值只加载相关分片
  3. TF-IDF 评分:计算相关性智能排序
  4. 多语言分词:支持中英文等智能分词

相关资源

在过去一年中,我们见证了多起重大攻击事件,例如 Shai-Hulud 蠕虫攻击、Nx 构建系统被攻破,以及通过 tj-actions/changed-files 漏洞导致机密信息泄露到公开的 GitHub Actions 日志中。但仅仅是罗列各种攻击事件,就足以占据本文的全部篇幅,更不用说深入探讨了。

作为一个行业和生态系统,我们能感受到攻击频率的日益增加。仅在 2024 年,报告的恶意软件包数量就同比增长了 156%。鉴于 Mend 托管的 Renovate Cloud 平台受信于超过 130 万个代码仓库,我们在保护开源软件消费者方面处于非常有利的地位,同时也为自托管 Renovate 的用户提供了更强的安全默认设置。在一系列备受瞩目的 npm 供应链安全攻击之后,Mend Renovate 的维护者们决定,对于选择采纳“最佳实践”配置的用户,默认启用这项安全功能是最佳选择。

为了帮助客户更好地应对这些日益增多的攻击,维护团队正在 Mend Renovate 现有的“最佳实践”配置之上进行构建。该团队一直致力于提供更多“默认即安全”的配置,并首先从 npm 生态系统着手。

在最新的 Mend Renovate 42 版本中,使用“最佳实践”配置的用户将会发现,npm 生态系统中的依赖项更新现在需要通过一个“最短发布时间”的检查,即某个更新发布后必须经过 3 天的窗口期,Mend Renovate 才会提议进行更新。通过这种方法,组织可以确保只有经过验证的、稳定的和值得信赖的依赖项更新才能进入生产环境,从而在保持开发者效率的同时,最终降低供应链攻击的风险。

这有何帮助?
尽管影响广泛,但这些攻击通常利用了两种常见情况:

  • 依赖项的精确版本未被锁定。
  • 依赖项的精确版本已被锁定,但我们在其发布后极短时间内就尝试更新。

不锁定依赖项版本可能有其合理的原因。例如,在 npm 生态系统中,当你发布一个拥有若干依赖项且被许多其他包所依赖的包时。

如果每次你提升一个依赖版本都需要发布自己的包,那么所有依赖于你的包也同样需要提升版本并发布新版,从而在整个生态系统中引发连锁效应。

其中一些过程可以通过自动化来简化——自然是使用像 Mend Renovate 或 GitHub 的 Dependabot 这样的工具,但这仍然需要一定程度的人工审查。

与此同时,不锁定我们的依赖项可能会导致问题,用户可能会立即开始下载一个包的新版本。
在推荐锁定依赖版本后,下一个问题是我们应该多久更新一次。许多工具中现有的默认设置是“一旦有新版本就立即更新”,这可能导致一个恶意升级在其发布几分钟内就被创建为拉取请求 (Pull Request)。

尽管那个恶意的依赖项可能不会进入您开发人员的机器——但它有可能从您的自动化构建管道中窃取机密或其他特权信息——或者利用您 AI 驱动的代码审查工具中的提示注入漏洞。

如果我们增加软件包发布与它出现在您项目的拉取请求中的时间间隔,这就为安全研究人员和自动化安全工具提供了更多时间来发现软件包中的恶意意图,从而减少供应链攻击的可能性。
Mend Renovate 如何助力保障整个生态系统的安全

如上所述,在 Mend Renovate 的最新版本中,我们为所有使用“最佳实践”配置的用户启用了“最短发布时间”检查的强制执行。这适用于更新任何使用 npm 数据源的包,无论其使用的是何种 JavaScript/TypeScript 包管理器。

这项强制执行将:

  • 确保给定的依赖项更新包含其发布时间的元数据(“发布时间戳”)。
  • 确保在该版本发布后未满最少 3 天之前,不会创建任何分支。

如果发现不满足此要求的包更新,Mend Renovate 的依赖项仪表板中将包含一个“等待状态”的条目,并且需要人工明确请求才能更新——从而确保只有“安全”的包更新才会被提出。

(这里的一个告诫是,增加等待时间并不一定意味着所有问题都能被发现——由于针对性攻击或复杂的规避技术,所有问题可能无法都被捕获。)

通过将此功能直接添加到我们的“最佳实践”配置中,那些已经选择遵循行业最佳实践的用户将默认受到保护。而其他所有人也能够添加此功能,例如:
codeJSON

{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": ["security:minimumReleaseAgeNpm"]
}

此外,还可以调整此行为——将等待窗口设置得任意长或短——或者对受信任的内部开发包绕过“最短发布时间”功能。

纵深防御
除了让 Mend Renovate 在满足特定条件(即经过一个给定的窗口期)前不发起更新之外,我们还建议建立多层防御:

在可能的情况下,在您的包管理器中启用此功能,以保护开发人员的机器;和/或在您的自动化构建管道中启用此功能,在发布窗口期过去之前使构建失败。

在撰写本文时,pnpm 10.6 和 yarn 4.2.0 已添加了对这些功能的支持,我们也看到其他包管理器正在考虑添加类似功能。

下一步计划?
继此版本的工作之后,维护团队将继续研究其他包生态系统,以便为我们的“最佳实践”配置启用相应功能,从而进一步保障面向消费者的产品和内部开发环境的安全!

在过去一年中,我们见证了多起重大攻击事件,例如 Shai-Hulud 蠕虫攻击、Nx 构建系统被攻破,以及通过 tj-actions/changed-files 漏洞导致机密信息泄露到公开的 GitHub Actions 日志中。但仅仅是罗列各种攻击事件,就足以占据本文的全部篇幅,更不用说深入探讨了。

作为一个行业和生态系统,我们能感受到攻击频率的日益增加。仅在 2024 年,报告的恶意软件包数量就同比增长了 156%。鉴于 Mend 托管的 Renovate Cloud 平台受信于超过 130 万个代码仓库,我们在保护开源软件消费者方面处于非常有利的地位,同时也为自托管 Renovate 的用户提供了更强的安全默认设置。在一系列备受瞩目的 npm 供应链安全攻击之后,Mend Renovate 的维护者们决定,对于选择采纳“最佳实践”配置的用户,默认启用这项安全功能是最佳选择。

为了帮助客户更好地应对这些日益增多的攻击,维护团队正在 Mend Renovate 现有的“最佳实践”配置之上进行构建。该团队一直致力于提供更多“默认即安全”的配置,并首先从 npm 生态系统着手。

在最新的 Mend Renovate 42 版本中,使用“最佳实践”配置的用户将会发现,npm 生态系统中的依赖项更新现在需要通过一个“最短发布时间”的检查,即某个更新发布后必须经过 3 天的窗口期,Mend Renovate 才会提议进行更新。通过这种方法,组织可以确保只有经过验证的、稳定的和值得信赖的依赖项更新才能进入生产环境,从而在保持开发者效率的同时,最终降低供应链攻击的风险。

这有何帮助?
尽管影响广泛,但这些攻击通常利用了两种常见情况:

  • 依赖项的精确版本未被锁定。
  • 依赖项的精确版本已被锁定,但我们在其发布后极短时间内就尝试更新。

不锁定依赖项版本可能有其合理的原因。例如,在 npm 生态系统中,当你发布一个拥有若干依赖项且被许多其他包所依赖的包时。

如果每次你提升一个依赖版本都需要发布自己的包,那么所有依赖于你的包也同样需要提升版本并发布新版,从而在整个生态系统中引发连锁效应。

其中一些过程可以通过自动化来简化——自然是使用像 Mend Renovate 或 GitHub 的 Dependabot 这样的工具,但这仍然需要一定程度的人工审查。

与此同时,不锁定我们的依赖项可能会导致问题,用户可能会立即开始下载一个包的新版本。
在推荐锁定依赖版本后,下一个问题是我们应该多久更新一次。许多工具中现有的默认设置是“一旦有新版本就立即更新”,这可能导致一个恶意升级在其发布几分钟内就被创建为拉取请求 (Pull Request)。

尽管那个恶意的依赖项可能不会进入您开发人员的机器——但它有可能从您的自动化构建管道中窃取机密或其他特权信息——或者利用您 AI 驱动的代码审查工具中的提示注入漏洞。

如果我们增加软件包发布与它出现在您项目的拉取请求中的时间间隔,这就为安全研究人员和自动化安全工具提供了更多时间来发现软件包中的恶意意图,从而减少供应链攻击的可能性。
Mend Renovate 如何助力保障整个生态系统的安全

如上所述,在 Mend Renovate 的最新版本中,我们为所有使用“最佳实践”配置的用户启用了“最短发布时间”检查的强制执行。这适用于更新任何使用 npm 数据源的包,无论其使用的是何种 JavaScript/TypeScript 包管理器。

这项强制执行将:

  • 确保给定的依赖项更新包含其发布时间的元数据(“发布时间戳”)。
  • 确保在该版本发布后未满最少 3 天之前,不会创建任何分支。

如果发现不满足此要求的包更新,Mend Renovate 的依赖项仪表板中将包含一个“等待状态”的条目,并且需要人工明确请求才能更新——从而确保只有“安全”的包更新才会被提出。

(这里的一个告诫是,增加等待时间并不一定意味着所有问题都能被发现——由于针对性攻击或复杂的规避技术,所有问题可能无法都被捕获。)

通过将此功能直接添加到我们的“最佳实践”配置中,那些已经选择遵循行业最佳实践的用户将默认受到保护。而其他所有人也能够添加此功能,例如:
codeJSON

{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": ["security:minimumReleaseAgeNpm"]
}

此外,还可以调整此行为——将等待窗口设置得任意长或短——或者对受信任的内部开发包绕过“最短发布时间”功能。

纵深防御
除了让 Mend Renovate 在满足特定条件(即经过一个给定的窗口期)前不发起更新之外,我们还建议建立多层防御:

在可能的情况下,在您的包管理器中启用此功能,以保护开发人员的机器;和/或在您的自动化构建管道中启用此功能,在发布窗口期过去之前使构建失败。

在撰写本文时,pnpm 10.6 和 yarn 4.2.0 已添加了对这些功能的支持,我们也看到其他包管理器正在考虑添加类似功能。

下一步计划?
继此版本的工作之后,维护团队将继续研究其他包生态系统,以便为我们的“最佳实践”配置启用相应功能,从而进一步保障面向消费者的产品和内部开发环境的安全!

这看起来只是又一个普通的 UI 库。名为 ansi-universal-ui 的包声称是 “适用于现代 Web 应用的轻量型模块化 UI 组件系统”,但在专业的描述和版本历史背后,隐藏着一个复杂的信息窃取恶意软件 —— 安全研究机构 Aikido 将其命名为 “G_Wagon”
该恶意包于 2026 年 1 月 23 日 被发现,它并非简单脚本,而是一个多阶段攻击平台,目标直指开发者环境中最有价值的机密信息。
攻击始于这个具有欺骗性的 npm 包。在普通开发者眼中,ansi-universal-ui 看似合法无害,但一旦安装,它就会执行一段 “高度混淆的载荷”,并下载专属的 Python 运行时,以此绕过本地环境限制。
这款恶意软件的数据窃取胃口极大。报告显示,它会窃取浏览器凭据、加密货币钱包、云服务密钥(AWS/Azure/GCP 等)以及 Discord 令牌,并将这些数据上传至一个 Appwrite 存储桶。
报告梳理了其攻击演进时间线:仅两天内,攻击者就发布了 10 个版本,可见其通过 “试错迭代” 完善攻击功能的过程:
  • 第一天:攻击者测试基础功能。1.0.0 版本为 “使用 npm 的 tar 模块搭建的初始框架”,随后迅速发布多个版本修复依赖项和重定向问题。
  • 第二天:攻击者全面激活恶意功能。1.3.5 版本新增了命令与控制(C2)服务器 URL;到 1.3.8 版本时,该恶意软件已集成 “完整的 Python 载荷及浏览器注入功能”。
研究人员指出:“这个恶意软件的特别之处在于,我们能完整看到它的开发过程。攻击者两天内发布 10 个版本,每个版本都揭示了攻击构建的一部分。”
“G_Wagon” 的技术复杂度远超普通脚本小子编写的恶意软件。其 Python 代码中嵌入了一个 “大型 Base64 编码数据块”,解码后发现是经过 XOR 加密的 Windows 动态链接库(DLL)。
这款恶意软件并非简单运行,而是深度植入系统:它利用 NtAllocateVirtualMemoryNtCreateThreadEx 等高级原生 API,将该 DLL 注入浏览器进程。“恶意软件内置了完整的 PE 文件解析器,会遍历导出表查找名为‘Initialize’的函数 —— 这是注入后执行的入口点。”
攻击者显然瞄准了高价值目标。针对大型文件,恶意软件会 “将数据分割为 5MB 的分片”,确保即使是海量窃取的数据也能可靠上传。“开发者显然为拥有大量敏感数据的受害者做好了准备。”
对于可能已安装 ansi-universal-ui 包的开发者,研究人员敦促立即采取彻底的应急措施:
  1. 删除 node_modules 目录并卸载该恶意包;
  2. 检查用户主目录中是否存在 .gwagon_status 文件—— 这是感染的直接证据;
  3. 轮换所有凭据,重点包括浏览器保存的密码、云服务密钥(AWS/Azure/GCP)及 SSH 密钥。

作为开发者,我们已经习惯了使用 Claude Code、Cursor、GitHub Copilot。但这些工具都有一个共同的局限——它们只能读写文件,不能真正控制你的电脑。

我开发了 xAgent CLI,因为我想拥有一个能帮我搞定一切的 AI 助手。

xAgent CLI 是什么?

xAgent CLI 是世界上首个结合顶尖 AI 模型与真·GUI 自动化的开源助手。它不只是会读写文件,而是能真正控制你的鼠标和键盘

核心特性

🖱️ 真·GUI 自动化

与传统 AI 工具不同,xAgent CLI 能直接控制你的桌面:

xagent gui --url https://example.com/login
点击坐标 (500, 300) 的登录按钮
在用户名框输入 "myemail@example.com"
按 Tab 键切换到密码框
输入密码
点击提交按钮

这意味着:

  • 浏览器自动化操作
  • 网页表单自动填写
  • UI 测试
  • 跨应用操作
  • 工作流自动化

🧠 顶尖模型免费用

开箱即用,免费访问世界顶级模型:

模型厂商特点
MiniMax M2.1MiniMax高性能推理与编程
GLM-4.7智谱AI前沿多模态模型
Kimi K2月之暗面MoE 架构,1T 上下文
Qwen3 Coder阿里巴巴编程专用模型

无需 API Key,无限使用。

💻 开发者友好

  • 上下文感知的代码分析
  • 自动识别项目架构
  • SubAgent 系统处理复杂任务
  • 中断后对话恢复

🏠 生活助手

整理我的桌面,按类型分类文件
设置每天自动备份到云盘
下载这个页面上所有 PDF
查找并删除重复文件

🔒 安全可控

提供 5 种执行模式,满足不同安全需求:

模式说明
YOLO完全信任,无需确认
ACCEPT_EDITS仅文件编辑权限
PLAN先展示计划再执行
DEFAULT需要用户审批
SMARTAI 根据任务智能判断

快速开始

npm i -g @xagent-ai/cli
xagent start

支持 Windows、macOS、Linux。

结语

xAgent CLI 代表了 AI 助手的新范式——从"读写文件"到"真正干活"。它是开源的、免费的,并且尊重你的隐私。

项目链接:

一、引言:软件供应链安全的”狼来了”困境

想象这样一个场景:你是一名开发者,每天打开 CI/CD 系统,迎接你的是数百条安全警报——”检测到依赖包存在高危漏洞,请立即修复!” 但当你花费大量时间逐一排查后却发现,绝大多数警报都是虚惊一场:那些所谓的”漏洞代码” 根本就没有被你的应用调用。这种”警报疲劳”已成为软件供应链安全领域的痛点,也是众多安全检测工具难以实际落地应用的重要原因。

这正是奇安信技术研究院和清华大学研究团队在 NDSS 2026 会议上发表的论文所要解决的核心问题。论文题目为《From Noise to Signal: Precisely Identify Affected Packages of Known Vulnerabilities in npm Ecosystem》,作者为蒲应元(奇安信星图实验室),应凌云博士(奇安信星图实验室)和谷雅聪博士(清华大学)。这项研究针对全球最大的开源软件生态系统——npm(拥有超过 300 万个包,2024 年处理了约 4.5 万亿次请求),提出了一套基于函数调用关系的细粒度漏洞传播关系识别方法和分析框架。论文分析结果表明传统工具所产生的漏洞警告中,高达 68.28% 都是”噪声”,即漏洞代码实际上根本无法被触达。

二、问题的本质:为什么传统方法会产生如此高的误报?

npm生态系统的复杂性源于其极度碎片化的包依赖结构。已有研究显示,约四分之一的npm包版本依赖于存在已知漏洞的包。以 pac-resolver为例,这个每周下载量达 300 万次的 npm 包曾曝出高危远程代码执行漏洞,导致 GitHub 上超过 28.5 万个公共仓库可能面临风险。但问题的关键在于:依赖存在漏洞的包,不等于你的应用真的受到影响。当前主流的软件成分分析(SCA)工具,如npm audit、GitHub Dependabot等,都采用包级别的分析方法。它们的逻辑很简单:如果你的依赖树中存在包A的v1.0版本,并且包A的v1.0版本存在漏洞,则发出警报提醒你的应用受到影响。 但这种粗粒度分析忽略了三个关键问题:

  1. 未使用的依赖:你的 package.json 声明了依赖,但代码中从未引入(require/import)该包的任何模块;
  2. 浅层的 API 使用:即使引入了包,可能只使用了其中若干个函数,而漏洞函数根本未被调用;
  3. 传递性衰减:通过多层依赖传递时,每一跳的使用范围都在缩小。

理论上,函数级可达性分析是最佳解决方案——只有当存在从应用入口到漏洞函数的调用路径时,才认为应用真正可能受到影响。但在 npm 生态实施函数级分析面临三大技术挑战:

  • 首先是可扩展性挑战:传统方法需要为每个项目构建完整的调用图(Call Graph),也包含其所有依赖,对于复杂项目,依赖数量可达数百甚至上千个包。每次分析都要从头开始,计算成本呈指数级增长。
  • 其次是 JavaScript 的动态特性带来的程序分析挑战。极其灵活的语法特性为静态分析制造了诸多盲区:代码中广泛存在的动态属性访问(利用变量而非字面量调用函数)、将函数作为参数传递的高阶函数机制(回调),以及允许在运行时动态修改对象原型链的特性,都让静态分析器难以在运行前确定具体的调用目标和完整的控制流,从而极易导致依赖分析链路的断裂或缺失。具体代码示例如下:
// 动态属性访问 
obj[propName]();  // propName是变量,静态分析难以确定调用目标  

// 高阶函数 
function process(callback) {    
    callback();  // 不知道传入的是哪个函数
}  

// 原型链动态修改 ,增加了分析的不确定性
Object.prototype.newMethod = function() { ... }; 
  • 最后,JavaScript 语言模块系统的复杂性进一步加剧了分析难度:CommonJS (require)和 ESM (import/export) 不同的模块机制、module.exports对象可在运行时修改,以及require()的参数可以是动态表达式 ,这些都进一步加剧了分析难度。

三、VulTracer的核心设计和解决方案

面对这些挑战,我们设计并实现了 VulTracer 这个分析框架。它的核心洞察在于:npm包一旦发布就不可变,因此可以为每个包预计算可复用的分析结果。这开启了”分析一次,复用多次”的新范式。

VulTracer 将传统的整体式分析分解为三个独立阶段,核心设计和架构如上图所示。以下将详细介绍每一个部分的设计逻辑和细节。

3.1 富语义图生成 (RSG Generation)

首先,VulTracer 利用程序静态分析技术,为每一个包构建了一个富语义图(Rich Semantic Graph, RSG)。这张图不仅看清了包内部的函数调用脉络,更关键的是,它显式地刻画了包的“边界”——哪些函数被暴露给了外部,又有哪些地方调用了外部依赖。传统的调用图(Call Graph)只记录”谁调用了谁”,而RSG设计了一个多层次的图结构,完整保留包的边界信息,图中的实体结构和详细定义如 下图 DEF1 所示,包含了三类不同的顶点集合和边集合。

3.2 接口契约提取 (Interface Contract Extraction)

虽然 RSG 保留了包的全部内部细节,但如何让独立分析的包能够正确”对接”?这就涉及到了提取形式化的接口契约。VulTracer 从这张复杂的图中提取出了一份简洁的形式化接口契约(Interface Contract)。这就像是给每个软件模块定义了标准的“插头”和“插座”,契约中清晰地记录了 API 的导出方式(Export Manifold)和导入方式(Import Manifest)。这一步至关重要,它充当了一道“语义防火墙”,屏蔽了复杂的内部实现细节,只保留了交互所需的关键信息。具体的定义如下图DEF2 所示。

3.3 拓扑排序驱动的按需组合式合成 (Compositional Synthesis)

最后,当需要检测某个具体项目时,VulTracer 不再需要深究源代码,而是像拼乐高积木一样,根据依赖关系,将预先计算好的 RSG 和契约进行组合式合成(Compositional Synthesis)形成一个新的生态级调用图 (ECG)。并且该 ECG 可根据任意真实项目的依赖关系按需组装。这种设计使得分析速度和扩展性得到了质的飞跃——在处理复杂的真实依赖图时,VulTracer 的成功率高达 99.41%,而对比的工具Jelly仅为 37.37%。

四、生态级实证研究:揭示漏洞传播的真相

在这项工作中,我们利用 VulTracer 对整个 npm 生态进行了史上最大规模的函数级漏洞传播影响分析。

4.1 数据集构建

首先我们构建了两个核心的数据集:

  • npm 生态数据集: 包含了 3,267,273个唯一npm包 以及其 34,685,976 个不同版本 。同时解析并构建了整个生态中超过9亿条的依赖关系。
  • 漏洞数据集:我们采用双维度选择策略,确保选择的漏洞样本既有代表性又有多样性。一是高影响力漏洞,从 2024 年下载量排行 TOP 10 的软件包 lodash, debug, semver, minimatch 这四个核心库中,找到了影响他们的6个CVE漏洞,每个软件包都有数十万直接依赖包,并且漏洞影响了超过百万的下游软件包。二是多样性维度,对齐 2024 CWE-Top-25 的类型,覆盖注入(CWE-79)、原型污染(CWE-1321)等21个不同类型的 CVE 漏洞,代表不同的攻击向量。最终我们的研究涵盖了27个CVE,涉及9,868,514条潜在传播路径

4.2 单跳分析:分析衰减的根本原因

我们首先聚焦于d₁ → d₀的单跳关系,这样可以排除多跳传播的复杂因素,精确归因。在我们的研究中建立了三层漏洞传播条件:仅引入模块 (C_mod)、调用任意函数 (C_func)、调用漏洞函数 (C_vuln_func)。定义如下图所示:

只有 C_mod ∧ C_func ∧ C_vuln_func 同时为真,才认为漏洞真正传播。最终单跳的分析结果如下表所示。

我们发现平均 22.80% 的直接依赖包声明了依赖,但从未导入任何模块(C_mod失败)。以 lodash 为例:存在 396,112 个声明依赖的包,但是有 131,933个”僵尸依赖” (33.31%)。这13万多个包背上了”有漏洞”的标签,但实际上完全不受影响。同时我们还发现,npm 第三方库的 API 设计决定传播率。同样的对于 lodash 这样一个综合工具库,拥有242个函数,但漏洞函数 template 只占所有调用的0.30%,排名第49位,详细分析如下图所示。说明这个函数的下游使用率并不高。与之相反的是 debug 库,它功能单一专注于调试,其核心功能函数就是其主函数,导致直接依赖者的受影响比例高达 71.77%。

4.3 多跳分析:揭示传递性衰减规律

单跳分析揭示了初始衰减,但漏洞会通过传递依赖传播多远?我们追踪了完整的传播路径。在分析中,我们追踪了9,868,514条潜在传播路径,涉及1,663,634个包版本。 最终不同漏洞的传播结果如下表所示。

在表格数据中, 以 CVE-2022-3517 (minimatch) 为例,数据揭示了粗粒度分析带来的严重误报问题。包级别分析报告了 497,595 条潜在传播路径,涉及 286,731 个受影响的包版本。然而,经由 VulTracer 的函数级可达性分析,确证受影响的包版本仅为 22,557 个。从全局统计维度来看,函数级分析所识别的受影响库数量平均仅为包级别分析结果的 31.72% 。这一数据统计表明,现有包级别依赖扫描工具产生的警报中,约 68.28% 属于漏洞代码不可达的误报(False Positives)。

最后,在上图也更进一步可视化了漏洞传播随依赖链路深度的衰减过程,分别从两个不同的视角来进行呈现。图(a)展示了每一跳(Hop)中新增受影响包数量的分布情况。对比显示,函数级别(红色曲线)的传播在 3 跳之后呈现出急剧的衰减趋势,与包级别(蓝色曲线)的长尾分布形成显著差异。这证实了真实的漏洞影响范围会随着依赖深度的增加而迅速减弱。而图 (b) 展示了传播过程中的累积概率分布情况进一步佐证了这一“浅层效应”:函数级传播曲线迅速收敛并达到平台期,数据显示 96.59% 的真实受影响包均收敛在 4 跳 的范围内。这意味着,尽管依赖图谱可能具有较深的层级结构,但具有实际威胁的漏洞传播主要局限于浅层依赖网络中。

五、结论:从噪声中提取信号

面对日益复杂的开源生态,我们的研究证明,传统的“版本比对”模式已经难以为继。由现有包级别工具识别出的潜在风险中,高达 68.28% 的漏洞代码实际上从未被调用 。换言之,近七成的“受影响”项目其实是安全的,并不需要火急火燎地去修复。这种高误报率不仅制造了巨大的“噪声”,更导致了严重的警报疲劳,反而掩盖了真正的威胁。因此,转向更细粒度的函数级可达性分析已是行业必经之路。通过 VulTracer,我们可以从噪声中提取出那 30% 的真实信号。这不仅能让开发者从无效的运维工作中解脱出来,更能让安全团队聚焦于真正具有可利用性的威胁。这才是让供应链安全治理走出困境、迈向精准防御的未来方向 。

最近在维护公司老项目和开发新项目的时候,因为框架的问题老项目需要用低版本的 nodeJs 不然安装和编译过不了,然后新项目用的框架又用不了低版本的,以前都是直接用官方 msi 安装的,这样对我来说就很麻烦,然后我就发现这个 nodeJs 版本管理工具。看了一下没人写过完整的安装指南,那我就写(水)一篇

ps: 在我写这篇安装指南的时候看到有佬友发过可视化的版本( nvm-managernvm-Desktop),喜欢可视化的可以看看,NVM-Windows 是终端命令行的~

一、安装前准备

1. 卸载已有 Node.js

若已通过 MSI 安装过 Node.js,请先卸载:

  • 打开 控制面板 → 程序和功能

  • 找到 Node.js,右键卸载

  • 删除残留目录(如果存在):

    C:\Program Files\nodejs C:\Users\<你的用户名>\AppData\Roaming\npm 

重要:未卸载旧版会导致 nvm 无法正确接管 Node 环境 。

2. 准备安装路径

  • 创建无中文、无空格的目录用于安装 NVM,例如:

    D:\NVM 
  • 创建无中文、无空格的目录用于存放 nodeJs 相关的东西,例如:

    /**
    * nvm映射nodeJs的路径,这是能够指定版本的关键。映射之后里面会多出一个nodeJs的文件链接,指向指定的nodeJs版本文件夹
    */
    D:\nodeJs\execPath

    /*
    * * 缓存和全局包目录 */
    D:\nodeJs\node_cache
    D:\nodeJs\node_global

    /*
    * * 我们安装的各个nodeJs版本目录,比如我安装了 22.13.0 LTS 和 24.9.0 两个版本,就会 v22.13.0 和 v24.9.0 两个文件夹,nvm的文件夹链接就是链接到这里面的某个文件夹,实现多版本切换。 */
    D:\nodeJs\nodeJsPackage

路径含空格会导致 nvm use 命令失败


二、安装 nvm-windows

1. 下载安装包

访问官方 GitHub Release 页面:
Releases · coreybutler/nvm-windows · GitHub

  • 推荐下载nvm-setup.exe(图形化安装,自动配置环境变量)

2. 执行安装

  • 右键 → 以管理员身份运行 nvm-setup.exe
  1. 安装路径:D:\NVM(或你自定义的路径)

  2. 选择 NodeJs 链接路径,默认是 C:\nvm4w\nodejs,我们换成上面创建的 D:\nodeJs\execPath**(这里因为我已经安装过了,所以会显示链接路径出来,首次是没有的)** 然后点下一步

  3. 后面的看自己需要自行选择即可,最后点击 Install 完成安装

3. 验证安装

打开 新的终端窗口(必须重启终端),执行:

nvm version

若输出版本号(如 1.2.2),说明安装成功 。


三、关键配置

1. 配置镜像加速(国内必备)

编辑 D:\NVM\settings.txt(nvm 安装目录下),添加或修改:

root: D:\nodeJs\nodeJsPackage
path: D:\nodeJs\execPath\nodejs
node_mirror: https://npmmirror.com/mirrors/node/
npm_mirror: http://mirrors.tencent.com/npm/

镜像源使用 npmmirror.com(原淘宝 NPM 镜像已停用)。


四、使用 nvm 管理 Node.js

常用命令

功能命令
查看可安装版本nvm list available
安装指定版本nvm install 24.10.0
安装 LTS 版本nvm install lts
切换版本nvm use 24.10.0(如果没有跟着上面的配置来的话需要管理员终端才能执行,因为默认映射的路径是 C:\Program Files\npm,微软要求操作 C:\Program Files 下的东西都需要管理员权限才能执行)
查看已安装版本nvm list

示例:安装并使用 Node.js 18

# 安装
nvm install 24.10.0

# 切换
nvm use 24.10.0

# 验证
node -v  # 输出 v24.10.0
npm -v   # 输出对应版本 

配置 npm 全局路径与缓存(仓库我选择了腾讯镜像,阿里有些包更新不及时。使用 pnpm 代替 npm)

npm config set prefix "D:\nodeJs\node_global"
npm config set cache "D:\nodeJs\node_cache"
npm config set registry http://mirrors.tencent.com/npm/
npm config set strict-ssl false

npm install -g pnpm

验证配置:

npm config list

配置系统环境变量

  • 用户变量 PATH 中添加:

    D:\nodeJs\node_global 
  • (可选)新建用户变量 NODE_PATH

    D:\nodeJs\node_global\node_modules 

此步骤确保全局命令(如 yarn, vue)可在任意终端使用 。


然后你就拥有一个了 多版本共存、无权限烦恼、下载飞快 的 Node.js 开发环境了!


📌 转载信息
原作者:
ExpOuter
转载时间:
2026/1/18 09:36:06

Gemini CLI 安装

下面教程主要介绍如何安装 gemini cli 并且使用第三方 api 提供的模型。

操作系统为 windows 11,安装工具是 nodejs。

1. 安装 node.js

如果使用 node 安装过 claude code cli,那么可以跳过这一步。

到官网下载 node.js 的安装包程序,点击完成安装。都默认配置,下一步完成安装即可。

安装后在终端工具中输入 node -v 和 npm -v 查询 node 和 npm 的版本,检查是否安装成功。如果成功返回版本号就表示安装成功了。

D:\Users\qozi>node -v
v20.19.5

D:\Users\qozi>npm -v
10.8.2

效果图如下:

2. 使用 npm 工具安装 gemini cli

在终端中输入下面的命令,然后等待完成安装:

npm install -g @google/gemini-cli --registry=https://registry.npmmirror.com

–registry=https://registry.npmmirror.com 这个参数是指定下载源,如果有梯子网络比较好,可以不加。

效果图如下:

使用 npm 命令时间比较久,大家耐心等待。如果喜欢研究的可以了解一下 yarn 和 pnpm。

3. 配置第三方 api 站的地址和密钥

完成上面的步骤就已经可以在终端中使用 gemini 命令使用了,会进入到认证页面。因为我们要使用第三方 api,所以没必要现在就启动,先按照下面的步骤完成第三方 api 的配置。

主要两个配置 .env​ 和 settings.json 两个文件。(我不太喜欢配置系统的环境变量,所以一般都是在配置文件中设置)

首先要注意这两个文件的位置:都是在当前用户目录下的 .gemini 文件夹下面。(安装过 claude code 的可以参照 .claude 文件夹,他们是在同一个层级下)

我的用户目录是 D:\Users\qozi ,对应的这两个文件的位置如下图:

如果用户目录没有 .gemini 文件夹的,就照着手动创建一下。

3.1 .env 文件的内容

#开头的是注释,这个可有可无。 GOOGLE_GEMINI_BASE_URL​ 填你的 api 的地址,GEMINI_API_KEY 填你的密钥。

### gemini cli  .env 配置 # imyal api
GOOGLE_GEMINI_BASE_URL=https://hk-api.gptbest.vip
GEMINI_API_KEY=sk-xxx
GEMINI_MODEL=gemini-3-pro-preview

3.2 settings.json 文件的配置

security.auth.selectedType 指定认证类型。使用第三方 api,必须这样填。该参数默认是 undefine。

{
  "security": {
    "auth": {
      "selectedType": "gemini-api-key"
    }
  }
}

settings.json 的配置只写了 api 认证需要的的配置,其他的配置还有很多,大家可以自行探索。文档最后会给出官方的文档地址和他提供的一个示例。

配置好后就可以在终端中输入 gemini 开始使用了。

上面配置后正常是可以用的,如果无法使用可以先确认一下 api 的地址和密钥是否正确,是否支持 gemini 的模型。

4. 参考文档:

官方配置文档地址

官方提供的一个完整 settings.json 栗子(注意仅适用于 v0.3.0 及之后的版本且参数不是全部的):

{
  "general": {
    "vimMode": true,
    "preferredEditor": "code",
    "sessionRetention": {
      "enabled": true,
      "maxAge": "30d",
      "maxCount": 100
    }
  },
  "ui": {
    "theme": "GitHub",
    "hideBanner": true,
    "hideTips": false,
    "customWittyPhrases": [
      "You forget a thousand things every day. Make sure this is one of ’em",
      "Connecting to AGI"
    ]
  },
  "tools": {
    "sandbox": "docker",
    "discoveryCommand": "bin/get_tools",
    "callCommand": "bin/call_tool",
    "exclude": ["write_file"]
  },
  "mcpServers": {
    "mainServer": {
      "command": "bin/mcp_server.py"
    },
    "anotherServer": {
      "command": "node",
      "args": ["mcp_server.js", "--verbose"]
    }
  },
  "telemetry": {
    "enabled": true,
    "target": "local",
    "otlpEndpoint": "http://localhost:4317",
    "logPrompts": true
  },
  "privacy": {
    "usageStatisticsEnabled": true
  },
  "model": {
    "name": "gemini-1.5-pro-latest",
    "maxSessionTurns": 10,
    "summarizeToolOutput": {
      "run_shell_command": {
        "tokenBudget": 100
      }
    }
  },
  "context": {
    "fileName": ["CONTEXT.md", "GEMINI.md"],
    "includeDirectories": ["path/to/dir1", "~/path/to/dir2", "../path/to/dir3"],
    "loadFromIncludeDirectories": true,
    "fileFiltering": {
      "respectGitIgnore": false
    }
  },
  "advanced": {
    "excludedEnvVars": ["DEBUG", "DEBUG_MODE", "NODE_ENV"]
  }
}

📌 转载信息
原作者:
qozier
转载时间:
2026/1/18 08:51:25

解决什么问题

想要使用本地 monorepo 开发 + 纯 npm 管理的项目,不想对项目进行 pnpm 改造的开发者

项目地址

https://github.com/alamhubb/mono

Mono

零侵入式 Monorepo 开发工具

直接使用 TypeScript 源码开发,无需构建,无需改造项目


什么是 Mono?

Mono 是一套零侵入式 monorepo 开发工具。它允许你在开发期间直接使用 TypeScript 源码,无需构建包或重构项目。

pnpm workspace 的「链式污染」问题

使用 pnpm workspace 意味着整条依赖链都被迫使用 pnpm

项目 A (pnpm) → 必须用 pnpm
  └── 项目 B → 必须用 pnpm(被污染)
        └── 项目 C → 必须用 pnpm(被污染)
              └── 项目 D → 必须用 pnpm(被污染)
  • 所有相关项目都必须改成 pnpm
  • 所有开发人员都必须安装 pnpm
  • 所有 CI/CD 环境都必须配置 pnpm
  • 新成员 npm install 直接报错:workspace:*

详细分析请阅读:「链式污染」—— 为什么一个 pnpm 项目会逼着整条依赖链都改成 pnpm

Mono 的解决方案

使用 Mono,你只需:

  • 运行 mono ./src/index.ts - 就这么简单!
  • 无需重构项目,无需 workspace:*
  • 无需构建包,直接使用源码
  • 修改立即生效,npm/yarn/pnpm 都兼容

pnpm workspace vs Mono

方面pnpm workspaceMono
安装必须安装 pnpm可选
配置文件需要 pnpm-workspace.yaml不需要
package.json需要修改为 workspace:*不需要修改
克隆后使用必须 pnpm installnpm/yarn/pnpm 都可以
依赖包需要先构建直接使用源码
团队协作所有人必须用 pnpm不强制


包列表

本仓库包含两个协同工作的包:

用途安装
mono-mjsNode.js CLI - 用于构建工具、Vite 插件npm install -g mono-mjs
vite-plugin-monoVite 插件 - 用于浏览器运行时npm install -D vite-plugin-mono

何时使用哪个?

场景工具
运行脚本、构建工具mono
Vite 插件、编译器mono
浏览器端导入vite-plugin-mono
Vue/React 组件vite-plugin-mono


快速开始

1. 全局安装 CLI

npm install -g mono-mjs

2. 运行项目

# 直接运行 TypeScript
mono ./src/index.ts

# 使用本地包运行 Vite
mono ./node_modules/vite/bin/vite.js

3. (可选)添加 Vite 插件用于浏览器端

npm install -D vite-plugin-mono
// vite.config.ts import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import { viteMono } from 'vite-plugin-mono' export default defineConfig({
  plugins: [
    viteMono(),  // 放第一个! vue()
  ]
})


特性

  • 零侵入 - 无需重构项目,无需配置文件
  • 自动发现 - 递归查找所有本地包
  • 即时生效 - 修改立即生效
  • 包管理器无关 - 支持 npm、yarn、pnpm、bun
  • 零配置 - 默认 ./src/index.ts,可选 local 字段


工作原理

包发现

直线向上查找距离最远的项目根目录 (.idea/.vscode/.git/package.json)
  └── 递归扫描
      └── 查找所有 package.json
          └── 根据 "name" 字段注册

导入拦截

// 你的代码 import { utils } from 'my-utils' // Mono 重定向到源码 // → /path/to/my-utils/src/index.ts 


配置

零配置(默认)

所有包默认使用 ./src/index.ts。无需任何配置!

自定义入口(可选)

package.json 中添加 local 字段:

{ "name": "my-package", "local": "./src/main.ts" } 


环境要求

  • Node.js >= 18.19.0
  • ESM 项目 - package.json 中需要 "type": "module"



📌 转载信息
转载时间:
2026/1/14 18:00:12

脚本是 AI 搓的,没看具体内容。
每天上班第一件事就是先运行一下脚本
运行结果:

脚本如下:

// update_ai_tools.js const { execSync } = require('child_process');
const os = require('os');

console.log('\n======================================');
console.log('       AI CLI 工具更新助手');
console.log('======================================\n');

// 1. 权限检查 function isRunningAsAdmin() {
  if (os.platform() === 'win32') {
    try {
      execSync('net session', { stdio: 'ignore' });
      return true;
    } catch (e) {
      return false;
    }
  }
  return process.geteuid && process.geteuid() === 0;
}

if (!isRunningAsAdmin()) {
  console.error('❌ 请以管理员身份运行此脚本。\n');
  process.stdin.once('data', () => process.exit());
  return;
}

const packages = [
  { name: 'Gemini CLI', npm: '@google/gemini-cli' },
  { name: 'GitHub Copilot', npm: '@github/copilot' },
  { name: 'Codex CLI', npm: '@openai/codex' },
  { name: 'Claude Code', npm: '@anthropic-ai/claude-code' },
  { name: 'Qwen Code', npm: '@qwen-code/qwen-code' }
];

// 2. 获取本地版本 (这一步很快) let localVersions = {};
try {
  const res = execSync('npm list -g --depth=0 --json', { 
    encoding: 'utf8', 
    stdio: ['ignore', 'pipe', 'ignore'] 
  });
  const parsed = JSON.parse(res);
  if (parsed.dependencies) {
    for (const key in parsed.dependencies) {
      localVersions[key] = parsed.dependencies[key].version;
    }
  }
} catch (e) {
  if (e.stdout) {
    try {
      const parsed = JSON.parse(e.stdout);
      if (parsed.dependencies) Object.assign(localVersions, parsed.dependencies);
    } catch (err) {}
  }
}

// 3. 核心逻辑:逐个检查并立即打印结果 console.log('正在检查版本状态...\n');
const tasks = [];

packages.forEach(pkg => {
  const localVer = localVersions[pkg.npm];
  let remoteVer = null;

  try {
    // 联网查询,可能会慢
    remoteVer = execSync(`npm view ${pkg.npm} version`, { encoding: 'utf8' }).trim();
  } catch (e) {
    console.log(`⚠️  [${pkg.name}] 查询失败,跳过。`);
    return; // 跳过当前循环
  }

  // 立即打印结果,提供实时反馈 if (!localVer) {
    console.log(`⚪ [${pkg.name}] 未安装 -> 🆕 ${remoteVer}`);
    tasks.push({ ...pkg, action: 'install' });
  } else if (localVer !== remoteVer) {
    console.log(`🔻 [${pkg.name}] 本地 ${localVer} -> 🆙 ${remoteVer}`);
    tasks.push({ ...pkg, action: 'update' });
  } else {
    console.log(`✅ [${pkg.name}] ${localVer} (已是最新)`);
  }
});

// 4. 执行更新 if (tasks.length === 0) {
  console.log('\n✨ 所有工具已是最新。');
} else {
  console.log();
  console.log(`🚀 开始更新 ${tasks.length} 个工具...`);
  console.log();

  tasks.forEach((task, index) => {
    process.stdout.write(`[${index + 1}/${tasks.length}] 更新 ${task.name}... `);
    try {
      execSync(`npm install -g ${task.npm}@latest`, { stdio: 'pipe' });
      console.log(`✔ 成功`);
    } catch (e) {
      console.log(`✖ 失败`);
      if (e.stderr) console.error('    ' + e.stderr.toString().split('\n')[0]); 
    }
  });
  
  console.log('\n✨ 全部完成。');
}

console.log('\n(按任意键退出)');
process.stdin.setRawMode(true);
process.stdin.resume();
process.stdin.on('data', () => process.exit());

📌 转载信息
原作者:
Thousand_Star
转载时间:
2026/1/9 18:19:14

最近听说 Droid 好用,就想着试试,故而发现了这个项目 droid-path:GitHub - kingsword09/droid-patch: CLI tool to patch the droid binary with various modifications.

也可以直接安装

npm install -g droid-patch
# 或直接使用 npx
npx droid-patch --help 

项目文档写的特别好特别详细 简要说一下怎么用 (懒人版):

npx droid-patch --is-custom --skip-login --websearch --disable-telemetry --reasoning-effort droid-full

这行命令可以实现:

  • 为自定义模型启用上下文压缩
  • 跳过登录验证
  • 使用外部搜索
  • 禁用遥测数据上传和 Sentry 错误报告
  • 为自定义模型启用推理强度

当 Droid 更新后可以运行如下命令进行同步修改:

npx droid-patch update

自定义模型的话可以使用站内大佬的工具 DroidGear:搞了个 Droid 配置工具 DroidGear

对于不支持设置推理强度的模型,可以手动修改一下:~/..factory/settings.json 中的:

"reasoningEffort": "none" //修改patch默认后的high为none 

建议先自定义模型再 path,如上文所示运行 droid-full 就可以了。亲测使用 Wong 大佬公益站的 sonnet 成功。

社恐第一次发帖,如有不合规之处还望佬友及时提醒,感谢开源作者。


📌 转载信息
原作者:
NocturneRain
转载时间:
2026/1/8 12:14:17

Claude Code 官方文档
https://code.claude.com/docs/zh-CN/overview
Claude Code 安装
npm install -g @anthropic-ai/claude-code

之后在根目录下就会有 ./claude 文件夹,在命令行中输入 claude 后配置自己的账号后就可以使用了。

使用第三方的 api (屯屯鼠的乐趣)

这里建议使用 ccr 或者 CCSwitch 或者 ccNexus.

https://github.com/musistudio/claude-code-router
https://github.com/farion1231/cc-switch
https://github.com/lich0821/ccNexus
Claude 技能增强包

这里我使用 MyClaude 来一键安装。它预先设置了一系列的自定义命令,以方便使用

MyClaude 安装:

git clone https://github.com/username/myclaude.git
cd myclaude

执行命令
mac: python3 install.py --install-dir ~/.claude
win: python install.py --install-dir %USERPROFILE%\.claude

如果上面的命令不行试试下面的
python install.py
使用命令
# 核心开发命令
/dev              # 完整开发流程
/code             # 代码生成
/debug            # 调试
/test             # 测试生成
/review           # 代码审查 # 辅助命令
/refactor         # 重构代码
/optimize         # 优化代码
/docs             # 生成文档
/bugfix           # 修复错误
/ask              # 询问问题
/think            # 思考分析
/enhance-prompt   # 增强提示词 

触发这些命令后 会调用 /agents 目录下的各种代理,使用 /skills 下的各种技能

简单理解:
Commands = 你说的话(用户界面)
Agents   = 执行者(工作流程)
Skills   = 工具箱(技能库)
查看当前有哪些命令工具
方式1:命令查看
python install.py --list-modules

方式2:查看目录
# Mac/Linux ls ~/.claude/commands/    # 查看所有命令 ls ~/.claude/skills/      # 查看所有技能 # Windows dir %USERPROFILE%\.claude\commands\
dir %USERPROFILE%\.claude\skills\


方式3:在 Claude Code 中查看
/help 
或者直接问它:“列出所有可用命令” 

这个项目的规则中已经配置了安全配置,就不用再单独增加了,如果需要可编辑 claude.md 文件

使用 skills.

skills 介绍

这里是官方文档, 简单理解就是预先设置了某个流程的步骤,AI 在使用时判断是否需要加载,如果加载就会按照预先设置的规则按序执行。

skills 为什么会节省 token 呢?

简单来讲就是预先设置了一系列规则后,AI 在遇到这样的逻辑后,从以前的全盘去找改为了根据各自 skills 中的规则去找,不用再读取全盘的数据,这样更快,也更减少 token 的消耗
本站里另一个佬的帖子很好,可以查看 实战 skills - 发票分类查询

自动生成任意网站 / 项目 / 文件且适合自己的 skills

在配置了上面三点后,基本上对绝大多数的使用者的效率都有所提升。对于某个领域的效率提升,就需要自己在各自领域编写适合自己的 skills 了。 这里我使用的 Skill_Seekers 项目。

Skill_Seekers 项目介绍

Skill Seekers 是一个文档转 Claude AI 技能包的自动化工具。它能帮你去读取某个网站的 url, 然后帮你生成对应的 skills 文件:

  1. 输入:任何文档网站 URL(如 React、Django、Godot 官方文档)
  2. 处理:自动爬取、分类、增强、打包
  3. 输出:.zip 文件,可直接上传到 Claude AI

典型流程:
文档网站 → 爬取数据 → 生成 SKILL.md → 打包 .zip → 上传 Claude

使用配置:

安装: 方式1:
pip install skill-seekers

方式2:
uv tool install skill-seekers

这两种是最常用的,还有可以通过源码/脚本来安装,具体可以看github的项目页面.

skill-seekers 参数详解

它需要主命令 + 子命令 + 参数的形式去加载,这里列举几个简单的子命令

skill-seekers <command> [options]

子命令数据源处理方式需要什么参数
scrape文档网站用 HTTP + BeautifulSoup 解析 HTML, 它是专门爬网页–url https://…
githubGitHub 仓库用 GitHub API + PyGithub 获取仓库数据–repo owner/repo
pdfPDF 文件用 PyMuPDF 解析 PDF–pdf /path/to/file.pdf
unified多个源组合上面三种–config xxx.json

各自子命令的参数都不同 这里我折叠起来方法观看

子命令 scrape 参数

skill-seekers scrape [options]

参数说明示例
–config配置文件路径–config configs/react.json
–name技能名称(快速模式)–name myproject
–url文档 URL(快速模式)–url https://docs.example.com/
–description技能描述–description “React 开发框架”
–skip-scrape跳过爬取,使用缓存数据–skip-scrape
–enhanceAPI 增强(需 ANTHROPIC_API_KEY)–enhance
–enhance-local本地增强(用 Claude Code,无需 API)–enhance-local
–dry-run预览模式,不实际执行–dry-run
–async异步模式(2-3x 更快)–async
–workers异步工作线程数–workers 8

典型用法:
使用配置文件
skill-seekers scrape --config configs/react.json --enhance-local

快速模式(无需配置文件)
skill-seekers scrape --name myproject --url https://docs.example.com/

使用缓存重建(秒级完成)
skill-seekers scrape --config configs/react.json --skip-scrape

子命令 github 参数

skill-seekers github [options]

参数说明示例
–config配置文件路径–config configs/react_github.json
–repoGitHub 仓库(owner/repo)–repo facebook/react
–name技能名称–name react-source
–description技能描述–description “React 源码分析”

典型用法:
skill-seekers github --repo microsoft/TypeScript --name typescript

子命令 pdf 的参数

skill-seekers pdf [options]

参数说明示例
–config配置文件路径–config configs/example_pdf.json
–pdfPDF 文件路径–pdf /path/to/doc.pdf
–name技能名称–name my-manual
–description技能描述–description “产品手册”
–from-json从已提取的 JSON 构建–from-json output/my_data/
子命令 unified 参数 - 多源统一爬取

skill-seekers unified --config <config.json> [options]

参数说明示例
–config必需 统一配置文件–config configs/react_unified.json
–merge-mode合并模式–merge-mode claude-enhanced
–dry-run预览模式–dry-run

合并模式:

  • rule-based:基于规则自动合并(默认)
  • claude-enhanced:使用 Claude AI 智能合并
AI 增强命令 enhance

skill-seekers enhance <skill_directory>

参数说明示例
skill_directory必需 技能目录路径output/react/

作用:将基础的 SKILL.md(~75 行)增强为专业指南(~500 行),包含:

  • 真实代码示例
  • 快速参考
  • 使用场景建议

使用 --enhance-local, 不调用 API,而是启动本地 Claude Code 终端

skill-seekers scrape --config configs/react.json --enhance-local

这样可以使用你已配置的 Claude Code(包括自定义 API 设置)。

skill-seekers 参数加载方式

方式 1: 快速模式 直接在命令后面拼接参数
skill-seekers scrape --name myproject --url https://docs.example.com/

方式 2: 交互模式(引导式)
这个模式我没测试,就是 工具会一步步问你哪些参数需要怎么配置
skill-seekers scrape --interactive

方式 3:使用 json 配置文件的方式
skill-seekers scrape --config configs/myproject.json

方式 4: 加载的是 GitHub 仓库
需要提前在环境变量中设置 GITHUBTOKEN, 不然 github 会限制请求
skill-seekers github --repo facebook/react --name react-code

在 AI 增强模块,我测试暂时不能使用本地模型,虽然文档说支持,但我测试下来提示找不到命令,已经给开发者提 bug 了,估计后续版本会修改吧.

当前三方汇总的 Claude skills

如果觉得这篇文章对你有用,可否点个赞,评论有用。谢谢

ps: 实在不会好看的排版了 慢慢来吧


📌 转载信息
转载时间:
2025/12/29 15:05:18