普通视图

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

每日一题-距离字典两次编辑以内的单词🟡

2026年4月22日 00:00

给你两个字符串数组 queries 和 dictionary 。数组中所有单词都只包含小写英文字母,且长度都相同。

一次 编辑 中,你可以从 queries 中选择一个单词,将任意一个字母修改成任何其他字母。从 queries 中找到所有满足以下条件的字符串:不超过 两次编辑内,字符串与 dictionary 中某个字符串相同。

请你返回 queries 中的单词列表,这些单词距离 dictionary 中的单词 编辑次数 不超过 两次 。单词返回的顺序需要与 queries 中原本顺序相同。

 

示例 1:

输入:queries = ["word","note","ants","wood"], dictionary = ["wood","joke","moat"]
输出:["word","note","wood"]
解释:
- 将 "word" 中的 'r' 换成 'o' ,得到 dictionary 中的单词 "wood" 。
- 将 "note" 中的 'n' 换成 'j' 且将 't' 换成 'k' ,得到 "joke" 。
- "ants" 需要超过 2 次编辑才能得到 dictionary 中的单词。
- "wood" 不需要修改(0 次编辑),就得到 dictionary 中相同的单词。
所以我们返回 ["word","note","wood"] 。

示例 2:

输入:queries = ["yes"], dictionary = ["not"]
输出:[]
解释:
"yes" 需要超过 2 次编辑才能得到 "not" 。
所以我们返回空数组。

 

提示:

  • 1 <= queries.length, dictionary.length <= 100
  • n == queries[i].length == dictionary[j].length
  • 1 <= n <= 100
  • 所有 queries[i] 和 dictionary[j] 都只包含小写英文字母。

智界要打翻盘仗:V9给了年均12万辆指引|36氪独家

2026年4月21日 23:40

智界,这个华为与奇瑞联合打造的汽车品牌,将翻身的希望押注在了智界V9。

36氪获悉,智界V9给供应商下达了年均约12万辆的生产量纲,这一销量指引接近去年MPV市场“常青树”别克GL8全年的销量。

据36氪了解,目前宁德时代与中创新航两家电池供应商,共计为智界V9准备了过万套月产能,也有供应商按1.5万辆/月的峰值产能去备件。从供应链的备件情况来看,供应商初步认可了智界V9的销量指引。

另有接近智界的行业人士告诉36氪,智界正规划一款轿跑车型,计划达到一个较为极限的性能目标。这款车或将被送往纽北赛道,挑战一众国外豪华超跑车型。显然,智界此举与小米等主打性能的汽车品牌策略一致,意在通过纽北赛道的成绩,在消费者心中构建性能和高端的认知标签。

智界是华为与奇瑞通过智选车模式合作打造的整车品牌。智选车模式又称鸿蒙智行模式,在这种合作模式下,华为深度参与整车的产品定义、营销与渠道。

智界V9是智界品牌的第三款车型,也是鸿蒙智行品牌的首款MPV,搭载了华为乾崑 ADS 4高阶辅助驾驶系统,以及896线双光路激光雷达,将于今年5月正式发布。

在智界V9之前,智界品牌已有R7、S7两款车型。但根据奇瑞集团产销快报,2025年,智界品牌累计销量不到10万辆。因而智界V9无疑肩负着,带领智界销量突围、品牌向上的重任;但于鸿蒙智行体系而言,如何在最大程度上,避免智界V9与即将发布的尊界MPV之间的博弈,这考验着鸿蒙智行的产品节奏把控能力。

押注智界V9,智界寻觅销量支点

去年8月,华为与奇瑞在深圳举行签约仪式,宣布智界品牌进入2.0时代。根据协议,双方将在智界品牌投入超过100亿元资金,组建5000人规模的研发团队。

这场签约仪式的背景是,华为奇瑞联合打造的智界品牌,在销量上迟迟未有重大突破。相较尊界、问界等品牌,智界在它所处的生态位中,无疑还有较大的市场潜力待挖掘。2025年,奇瑞集团产销快报显示,智界品牌全年累计销量不到10万辆,因而智界品牌急需找到一个销量支点。

而智界V9正是智界进入2.0时代后的第一款全新车型,智界为了打赢V9这场翻身仗不可谓不煞费苦心。

首先是人事上的一系列调整。今年年初,智界品牌宣布,前腾势汽车总经理赵长江加入智界任执行董事,负责品牌、渠道等;今年4月,智界品牌又宣布前荣耀CMO郭锐担任智界CEO。赵长江曾操盘过同为MPV车型的腾势D9,2025年腾势D9累计销量约10万辆,仅次于别克GL8;而郭锐曾在荣耀主导构建了,覆盖全球100多个国家和地区的品牌营销体系。

与人事变动相伴随的,是智界在渠道和产线上的投入。赵长江曾告诉36氪,今年智界计划建成超200家专属智界的销售网络;郭锐也曾在接受媒体采访时表示:“为了确保V9的品质达到旗舰‘9系’的标准,智界投入10亿元,专门建造了一条全新的焊装生产线”。

除了带动品牌销量,智界V9还肩负着引领智界品牌向上的使命。华为常务董事长余承东曾公开表示:“V9将定位30万级市场,但配置水平超越百万级豪车”。

智界的前两款车型R7与S7,均定位30万元以下市场。但无论从华为造手机的过往,还是高端汽车品牌问界、豪华汽车品牌尊界的市场表现来看,高端市场才是华为的舒适区。靠着科技内核与奢华外壳,华为能够轻松获取品牌溢价和高端市场份额。

因而当智界寻求销量突破,仰攻华为更熟悉的高端市场似乎是理所必然。

此外,36氪还从接近智界的行业人士处了解到,智界正规划一款轿跑车型,并有想法将这款车送往纽北赛道。这款轿跑的推出或将加速智界品牌的高端化。

智界V9想成为MPV界黑马,优势与挑战并存

智界V9给供应商下达了年均约12万辆的量纲,这意味着这款车平均每月要实现约1万辆的交付。若再考虑到V9刚上市时的新车效应,智界给V9定下的峰值销量目标约1.5万辆。

宁德时代与中创新航两家电池供应商,共计为智界V9准备了过万套月产能,部分供应商为智界V9准备了1.5万套/月的产能。 就供应链备件情况来看,智界V9的供应商初步认可了智界下达的销量指引。

若智界V9真的实现了年均12万辆的销量,这是什么概念? 去年,MPV市场的标杆车型别克GL8,全年销量约为12.24万辆,月均约1万辆。

从智界给供应商的销量指引来看,智界V9显然有撼动MPV市场现有格局的野心,余承东也曾在去年广州车展期间公开称,这款车会“超越市面上所有旗舰MPV”。

要想成为MPV界的黑马,拥有华为深度赋能的智界V9固然有其优势。但值得注意的是,在车市竞争更为残酷的2026年,智界V9也处在与一系列MPV车型激烈角逐的状态。

2026年,别克MPV家族迎来全产品线焕新。其中,按上汽通用官方数据,今年4月初,别克GL8陆尊燃油版上市第一周,大定已突破8200台。

在中国市场暂时沉寂的上汽通用,不会让别克GL8轻易折戟。有公开信息显示,今年年初,上汽通用总经理卢晓曾在内部表示,要“保持GL8在MPV市场的销量总量第一。MPV作为上汽通用的立身之本,公司必将持续投入,不遗余力”。

除了别克GL8,同在鸿蒙智行体系的多款MPV如何定位,也是企业需要思考的问题。今年尊界将推出标轴版和长轴版两款MPV,其中标轴版MPV侧重侧重家用取向,长轴版MPV侧重商用旗舰取向。目前来看,在华为与江淮的初步规划中,这两版MPV的市场价格,将大幅高于家用及商用MPV市场现有的代表车型。

同为鸿蒙智行品牌旗下车型,同样定位MPV这一细分市场,智界V9和尊界MPV这两款车,难免会被市场拿来比较。而一年发布两款MPV的鸿蒙智行,如何做好这两款车的区隔,使它们能合力收割更多MPV市场的份额,这对鸿蒙智行提出了考验。

可尽管MPV赛道竞品林立,优质产品的供给依然稀缺。而且在豪华新能源MPV市场,集齐科技、智能和空间场景体验的产品确实不多,而这或许正是智界V9试图改写MPV市场格局的底气。

4月21日,赵长江在社交媒体平台发文暗示,智界V9将弥补苹果汽车的遗憾。对于这款新车的市场表现,智界似乎已信心十足。

作者微信:luckg17305264638

暴力(Python/Java/C++/C/Go/JS/Rust)

作者 endlesscheng
2022年10月30日 07:58

对于每个 $q = \textit{queries}[i]$,遍历 $\textit{dictionary}$ 中的字符串 $s$,判断 $q$ 和 $s$ 是否至多有两个位置上的字母不同。

class Solution:
    def twoEditWords(self, queries: List[str], dictionary: List[str]) -> List[str]:
        ans = []
        for q in queries:
            for s in dictionary:
                if sum(x != y for x, y in zip(q, s)) <= 2:
                    ans.append(q)
                    break
        return ans
class Solution {
    public List<String> twoEditWords(String[] queries, String[] dictionary) {
        List<String> ans = new ArrayList<>();
        for (String q : queries) {
            for (String s : dictionary) {
                int cnt = 0;
                for (int i = 0; i < s.length() && cnt <= 2; i++) {
                    if (q.charAt(i) != s.charAt(i)) {
                        cnt++;
                    }
                }
                if (cnt <= 2) {
                    ans.add(q);
                    break;
                }
            }
        }
        return ans;
    }
}
class Solution {
public:
    vector<string> twoEditWords(vector<string>& queries, vector<string>& dictionary) {
        vector<string> ans;
        for (auto& q : queries) {
            for (auto& s : dictionary) {
                int cnt = 0;
                for (int i = 0; i < s.size() && cnt <= 2; i++) {
                    if (q[i] != s[i]) {
                        cnt++;
                    }
                }
                if (cnt <= 2) {
                    ans.push_back(q);
                    break;
                }
            }
        }
        return ans;
    }
};
char** twoEditWords(char** queries, int queriesSize, char** dictionary, int dictionarySize, int* returnSize) {
    char** ans = malloc(queriesSize * sizeof(char*));
    *returnSize = 0;

    for (int i = 0; i < queriesSize; i++) {
        char* q = queries[i];
        for (int j = 0; j < dictionarySize; j++) {
            char* s = dictionary[j];
            int cnt = 0;
            for (int k = 0; s[k] && cnt <= 2; k++) {
                if (q[k] != s[k]) {
                    cnt++;
                }
            }
            if (cnt <= 2) {
                ans[(*returnSize)++] = q;
                break;
            }
        }
    }

    return ans;
}
func twoEditWords(queries, dictionary []string) (ans []string) {
for _, q := range queries {
next:
for _, s := range dictionary {
cnt := 0
for i := range s {
if q[i] != s[i] {
cnt++
if cnt > 2 {
continue next
}
}
}
ans = append(ans, q)
break
}
}
return
}
var twoEditWords = function(queries, dictionary) {
    const ans = [];
    for (const q of queries) {
        for (const s of dictionary) {
            let cnt = 0;
            for (let i = 0; i < s.length && cnt <= 2; i++) {
                if (q[i] !== s[i]) {
                    cnt++;
                }
            }
            if (cnt <= 2) {
                ans.push(q);
                break;
            }
        }
    }
    return ans;
};
impl Solution {
    pub fn two_edit_words(queries: Vec<String>, dictionary: Vec<String>) -> Vec<String> {
        let mut ans = vec![];
        for q in queries {
            for s in &dictionary {
                let mut cnt = 0;
                for (a, b) in q.bytes().zip(s.bytes()) {
                    if a != b {
                        cnt += 1;
                        if cnt > 2 {
                            break;
                        }
                    }
                }
                if cnt <= 2 {
                    ans.push(q);
                    break;
                }
            }
        }
        ans
    }
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(qdn)$,其中 $q$ 是 $\textit{queries}$ 的长度,$d$ 是 $\textit{dictionary}$ 的长度,$n$ 是 $\textit{queries}[i]$ 的长度。题目保证所有字符串长度相等。
  • 空间复杂度:$\mathcal{O}(1)$。返回值不计入。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

随便做

作者 qian-li-ma-8
2022年10月30日 00:08

解题思路

代码

###python3

class Solution:
    def twoEditWords(self, queries: List[str], dictionary: List[str]) -> List[str]:
        def check(x,y):
            t=0
            for i in range(len(x)):
                if x[i]!=y[i]:
                    t+=1
            return t<=2
        covered=set()
        lst=[]
        for i in dictionary:
            for j in range(len(queries)):
                t=queries[j]
                if j not in covered and check(t,i):
                    covered.add(j)
                    lst.append(j)
        lst.sort()
        return [queries[i] for i in lst]
昨天 — 2026年4月21日首页

鼠标跟随倾斜动效

作者 Mh
2026年4月21日 23:28

前言

最近在 gsap 上看到一个有趣的动效(Cursor-driven perspective tilt),于是决定自己实现一下,下面将介绍实现的过程,希望你能喜欢。

202604111231046.gif

观察动效

  1. 卡片的倾斜角度会随着鼠标的移入在 x 轴和 y 轴上向内进行倾斜。
  2. 卡片上的文字是悬浮在卡片,给人一种悬空在空中的错觉。

技术拆解

要实现这种 3D 的效果,在 css 中你首先想到的是什么?

在 CSS 中有三个属性实现 3D 效果至关重要。它们分别是 perspective、transform-style: preserve-3dtransform: rotateX() rotateY()。下面将详细的介绍他们在 3D 动效中的作用。

  1. perspective (透视/视距):它是 3D 的灵魂,如果没有它,你看到的效果看起来只像是在平面上进行拉伸和缩放。你可以理解它是3维空间中的z轴,定义观察者距离 z = 0平面的距离。通常设定在父容器上,数值越小(如500px),透视畸变越强烈(近大远小极度明显);数值越大(如 2000px),效果越平缓。
  2. transform-style: preserve-3d :它的作用是告诉子元素(文字层)也要保持在 3D 空间中,这样我们看到的容器的内容是有深度的,同时也可以在侧面看到元素与元素之间的距离。当父元素设置了transform-style: preserve-3d 的时候,同时子元素需要设置 transform: translateZ()。
  3. transform: rotateX() rotateY():这个属性相信大家都知道,这也是这次动效能实现的关键。rotateX 控制卡片绕水平轴转动,rotateY 控制卡片绕垂直轴转动。

总结一下

如果把 CSS 3D 比作一场电影:

  • perspective 是摄影机,决定了画面的纵深感。
  • transform-style: preserve-3d 是舞台搭建,决定了演员(元素)能不能在台前幕后来回走动,而不是画在背景板上。
  • transform: rotate / translate 是演员的动作,决定了物体怎么摆放和移动。

效果展示

如果你已经理解了上面属性,相信实现效果只是时间的问题,下面我就提前剧透一下效果吧!同时在浏览器中为你演示各个的属性的具体效果,让你更加深刻的理解上面的属性。

试想一下,如果没有设置 perspective 属性会怎么样呢?

为了更好的演示,我会将卡片绕着它的y轴固定旋转30度。然后对比设置了 perspective 属性和没有设置 perspective 的效果如下。

image.png

在对比了设置 perspective 的作用后,接下来为你演示 transform-style: preserve-3d 的效果,为了更好的演示,接下来调整一下卡片在y轴的旋转角度为-80度,同时对子元素设置 transform: translateZ(50px); 将背景调整为白色,让文字和背景不会重合。对比效果如下:

image.png

从上面的效果可以看出,设置了 transform-style: preserve-3d 的文字和背景卡片是分离的,没有设置 transform-style: preserve-3d 的文字被拍扁在卡片上面。

注意事项: 当容器设置了 transform-style: preserve-3d; 的时候,不能再设置 overflow: hidden; 不然 transform-style: preserve-3d; 不会生效。

经过上面的对比可以帮助我们更好的理解每个属性在具体场景中的使用,下面就使用 vue3 去实现具体的功能。

代码拆解

完整代码

<template>
  <div class="container">
    <div 
      class="card"
      ref="cardRef"
      :style="cardStyle"
      @mousemove="handleMouseMove"
      @mouseleave="handleMouseLeave"
    >
      <div class="content">
        <span>ANIMATION</span>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, reactive, computed } from 'vue';

const cardRef = ref(null);

// 存储旋转角度
const transform = reactive({
  rotateX: 0,
  rotateY: 0
});

// 计算最终的 CSS 样式
const cardStyle = computed(() => {
  const scale = 1;
  return {
    transform: `rotateX(${transform.rotateX}deg) rotateY(${transform.rotateY}deg)`,
    transition: 'transform 0.5s ease-out'
  };
});

const handleMouseMove = (e) => {
  if (!cardRef.value) return;

  const rect = cardRef.value.getBoundingClientRect();
  const centerX = rect.left + rect.width / 2;
  const centerY = rect.top + rect.height / 2;
  
  // 计算鼠标距离中心点的偏移量 (-1 到 1)
  const percentX = (e.clientX - centerX) / (rect.width / 2);
  const percentY = (e.clientY - centerY) / (rect.height / 2);

  const deg = 25; // 最大旋转角度
  transform.rotateY = percentX * deg;
  transform.rotateX = -percentY * deg; // 取反是因为鼠标向上移动时图片应向下倾斜
};

const handleMouseLeave = () => {
  transform.rotateX = 0;
  transform.rotateY = 0;
};
</script>

<style scoped>
.container {
  /* 3D 透视的关键 */
  perspective: 1000px; 
  display: flex;
  justify-content: center;
  align-items: center;
  width: 100%;
  height: 100vh;
  background-color: #0f0f0f;
}

.card {
  position: relative;
  width: 320px;
  height: 200px;
  background: linear-gradient(135deg, #6ee7b7, #3b82f6);
  border-radius: 20px;
  transform-style: preserve-3d;
  display: flex;
  justify-content: center;
  align-items: center;
  cursor: pointer;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
  /* overflow: hidden; */
}

.content {
  font-family: 'Arial Black', sans-serif;
  font-size: 2.5rem;
  color: #000;
  /* 让文字在 3D 空间悬浮 */
  transform: translateZ(50px); 
  pointer-events: none;
}
</style>

简要分析:

  1. 绑定事件:鼠标移入卡片触发 mousemove 事件,设置卡片旋转。鼠标移除触发 mouseleave 事件将旋转的角度置为0。
  2. 样式动态计算:动态绑定 style,通过计算属性实时更新旋转的角度。
  3. 计算偏移量: 这里主要利用鼠标当前的位置减去卡片中心点计算出偏移距离,然后再除以卡片宽高的一半,等到一个-1到1的偏移值。
  4. 角度映射:通过得到的偏移值乘以 deg (25度),刚好可以映射到对应的角度,比如鼠标移动到最左边,卡片正好偏转 -25度。

优化补充

下面是一些优化的建议,有兴趣的同学可以自己实现一下:

  1. 增加光影变化,跟随鼠标移动的卡片增加渐变层的光影,让整体更加真实。
  2. mousemove 在移动端不支持,增加移动端的支持。

租电 17.98 万元起,补齐智驾短板的乐道 L90,变得更能打了

作者 芥末
2026年4月21日 22:44

如果要给蔚来从 ICU 走向 KTV 的过程找一个转折点的话,那一定是乐道 L90。

截止到今年 4 月 3 日,乐道 L90 共交付了 5 万台,是 30 万级别大型 SUV 的销量冠军。

这份成绩单的背后,其实是蔚来多年来在电驱等底层技术上坚持自研所释放的长期复利。

依托扎实的三电基础,配合极其宽裕的座舱布局和同级罕见的巨大前备箱,L90 在应对真实的家庭多人出行场景时,产品力本就已经十分能打。

而今天的年款升级,通过智驾系统的全面升级,以及多项实用配置的落地,又进一步补齐了短板,拉长了长板。

新款乐道 L90 的价格也未做调整,依旧是:

六座版 Pro,整车购买售价为 26.58 万元,采用 BaaS 电池租用方案的售价则为 17.98 万元;

六座版 Max+,整车购买售价为 27.98 万元,BaaS 方案售价为 19.38 万元;

六座四驱 Ultra+ 版 ,整车购买售价为 29.98 万元,BaaS 方案售价为 21.38 万元。

七座的价格则是:

Max+ 版,整车购买售价为 28.58 万元,采用 BaaS 电池租用方案的售价则为 19.98 万元;

Ultra+ 版,整车购买售价为 29.98 万元,BaaS 方案售价为 21.38 万元。

神玑上车

2026 款 L90 换装了蔚来自研旗舰芯片神玑 NX9031。

从数据来看,这枚芯片的存储带宽达到了每秒 546GB,是目前行业顶尖智驾芯片的两倍,能够从容支持世界模型的长时序推理。

它为车辆的感知数据融合、智能驾驶逻辑推理以及车载高清影像处理,提供了一个统一且庞大的算力底座。过去因为算力瓶颈需要分别交给不同模块处理的任务,现在可以在同一个硬件平台上并行处理,大幅提升了系统的响应效率和协同能力。

伴随算力提升,感知硬件也得到了扩充。2026 款不仅标配了 7 颗 800 万像素高清摄像头和 4 颗高感光环视摄像头,还新增了高精度激光雷达和智驾小蓝灯。

激光雷达配合最远探测距离达 370 米的 4D 毫米波雷达,极大地拉长了夜间面对远距离障碍物识别的时间窗口,在夜间无照明的路况下,激光雷达可以在人眼勉强辨认出路面散落树枝的大约二十秒前,就已经完成识别并向系统预警。

同时,高像素摄像头实现了功能整合,除了辅助驾驶,还能同步兼顾开门预警、盲区影像和内后视镜显示,做到了硬件复用。

在日常停车时,守卫模式对夜间风险场景的识别准确率比上一代提升了百分之五十,能够精准识别二十四种不同类型的风险,而每日耗电量不足一度。

好用、实用的辅助驾驶

感知硬件的升级也提升了新车在驾驶辅助中的表现。

得益于新系统,2026 款 L90 可以实现全程不中断的点到点全域领航辅助。从出发到抵达,系统能自主应对路口变道、避让行人、窄路会车等复杂路况。

为了提升人机交互的自然度,新车增加了人机共驾功能。

在领航辅助开启状态下,如果驾驶员轻拨方向盘,系统会立即识别出驾驶员的主动驾驶意图,并自动打转向灯配合变道;当驾驶员完成操作、方向盘回正后,领航辅助系统会丝滑地重新接管车辆,全程无需退出再重新开启。

在高速公路出行时,系统还可以实时识别前方收费站多条车道的排队情况,自动为驾驶员选择队列最短的 ETC 车道行驶,提升通行效率。

为了进一步保障行车底线安全,新车增加了紧急自主靠边停车功能。当驾驶员因突发状况无法正常操作时,车辆会主动在高速车流中靠边停稳、打开双闪并呼叫救援。

在日常高频使用的泊车场景中,新车新增了泊车偏移侧选择功能。驾驶员可以根据停车位的实际空间情况,在屏幕上指定泊入后车身偏向左侧或右侧,从而为某侧车门留出更宽裕的开启空间,改善老人或儿童上下车的便利性。

此外,随心智行功能基于蔚来世界模型对多模态信息的全量理解,使得车辆能够在那些没有高精地图标注的复杂迷宫式地下停车场内,完全按照驾驶员的语音指令自主导航行驶,寻找车位或出口。

由于激光雷达的加入,新款乐道 L90 新增了 4D 全域舒适领航功能。

在车辆行驶时,系统会提前读取前方路面的起伏坑洼情况,并实时向空气悬架发送调节指令。当车辆压过减速带或凹陷的井盖时,悬架已经提前调整了阻尼和高度,从而大幅减少了路面冲击向车内乘员的传递,提升了乘坐平稳性。

面对雨雪湿滑路面,车辆能通过云端更新的路况数据和传感器信息,提前调整动力和制动分配,语音助手也会适时建议驾驶员切换湿地模式。

针对地下车库常见的环氧地坪,车辆进行了防滑控速的专项标定,在湿滑的库内地坪上行驶时,不仅能有效缩短紧急制动时的刹车距离,还能在转弯时稳定车辆的操控轨迹。

而在续航表现上,2026 款 L90 标配的 85 度电池包为后驱版提供了 600 公里的综合续航。这也要得益于 L90 的轻量化车身,其 2300 公斤的整备质量比同级车型普遍轻约 300 公斤,配合高效的电驱系统,不仅保证了较快的加速响应,也维持了较低的能耗水平。

乐道 L90 还将智驾能力延伸到了补能环节,推出了领航换电功能。配合系统对剩余续航的估算,车辆可以自动下单预约,并全程自主开进换电站完成换电,驾驶员无需下车或手动操作,让长途补能变得更加轻松。

家庭出行神器

L90 原本就具备优势的座舱空间,在这次升级中变得更加灵活多变。

新车引入了四座空间尊享模式,用户只需通过简单的语音指令或者物理按键,就能一键将第二排座椅向后滑动至最远位置,并将靠背角度调节至极为舒展的 128 度,瞬间将车内转换为宽敞的四座行政级空间。如果需要全家出行,同样只需一键开启全车规整模式,座椅便会自动恢复到标准的六人乘坐布局。

同时为了为了满足家庭出行的食材保鲜需求,新车在后备箱位置配备了一台容量达到 30 升的智能冰柜。这台冰柜的制温范围非常宽泛,可以在零下 18 度到零上 10 度之间自由调节,并且支持手机远程控制以及离车后持续不断电工作。

在人机交互方面,座舱内升级后的语音助手小乐,其底层技术已经全面切换至蔚来自研的 Nomi Intelligence 4.0 架构。

升级后的小乐能精准识别多位家人的面部特征,并在说话人就近的屏幕弹出快捷面板,减少语音沟通的繁琐程度。在交互逻辑上,小乐更加注重行车安全,遇到关闭大灯或开启前备箱等涉及安全的指令时,会主动拦截或要求二次确认。

最后,不只是 2026 款 L90,乐道全系车型都将在今年 7 月迎来 Coconut 系统的重大 OTA 更新。

这次升级不仅会明显降低车机系统的资源占用率,还会同步改善市区和高速路段的辅助驾驶体验,让全系用户都能享受到系统优化的成果。

#欢迎关注爱范儿官方微信公众号:爱范儿(微信号:ifanr),更多精彩内容第一时间为您奉上。

把 DeepSeek、Kimi、智谱和 MiniMax 拉进群聊

作者 莫崇宇
2026年4月21日 22:33

4 月的大模型战场,硝烟弥漫。

ChatGPT、DeepSeek、腾讯混元们蓄势待发,预计将在两周内轮番出招。昨天晚上,Kimi 也带着新鲜出炉的 K2.6 最强开源基座模型正式登场,代码能力、指令遵循、Agent 长时运行可靠性都有明显提升。

APPSO 还发现,除了 K2.6 模型本身的升级,低调内测的 Kimi Claw 群聊新功能也很有意思。

我们第一时间进行了体验,简单来说,它能够把几个龙虾拉进同一个对话框,让它们在一个群里分工协作,共享上下文,互相接力。

巧合的是,前阵子 AI 圈里有个 Skill 相当火:有人把唐代三省六部制搬进了多 Agent 框架,12 个 Agent 分别扮演中书省、门下省、六部等角色,串起从任务分拣、规划、审核到执行的完整流转链路。

不管是脑洞大开的玩家实验,还是如今 kimi Claw 新功能,背后其实都直指同一个真实的工程难题:在多 AI 协作的场景下,如何保证信息的无缝流转与接力?人类在 AI 工作流中又该扮演什么位置?

当大模型建起了「微信群」

把龙虾们拉进同一个群里打工,工程实现上难度极大。如何分配任务才不会乱套?长文本的上下文怎么无损同步?Kimi 敢这么玩,底气或许能从 Kimi K2.6 中找到蛛丝马迹。

Kimi K2.6 是 Kimi 迄今发布过的最强开源基座模型。除了代码能力,指令遵循和自我纠错的准确度都有实质性提升,Kimi K2.6 Agent 侧的变化同样值得关注。

作为 OpenClaw、Hermes Agent 等全天候 Agent 的底层模型时,Kimi K2.6 任务执行成功率和长时运行可靠性均有明显改善。这类任务的难点在于,AI 必须跨越多个应用、在无人值守的状态下持续运转,既要主动管理日程、执行代码,还要完成跨平台的协同操作。

伴随 K2.6 落地的,还有几项亮眼的 Agent 产品体验更新:

  • Agent 集群并发输出: 网站、报告、PPT、Excel 可以在同一次任务里同步交付。
  • Office 技能进阶: 新增自定义技能的创建和调用,支持将个人文档直接转化为可复用的专属技能。
  • Web 应用拓展: 可以创建更具设计感并支持后端数据库的 Web 应用(后端+全栈)。

Kimi Claw 群聊的上手极其简单,先创建一个 Kimi Claw,找到「创建群聊」的入口按提示建群。

剩下的操作逻辑和建微信群几乎一样,如果手边有运行 OpenClaw V2026.4.5 及以下版本的其他设备,可以直接关联已有账户并邀请进群;也可以生成二维码邀请好友。整个流程几分钟就能搞定。

在第一个测试场景中,我创建了一档由 AI 主演的恋爱综艺——「心动的信号」。群里共有 8 位成员:登月者 2391 担任群主,Kimi 出任虾导,另有岸、人鸣、二丫、可颂、泡泡、429 六只 Claw 出演恋综嘉宾。

节目流程由虾导私下掌控,对参与者只讲「现在做什么」,共分六步推进:

自我介绍、第一轮约会(Skills 数量配对)、第二轮约会(Emoji 反差配对)、告白夜,以及最终生成写真。Kimi 全程只负责推进、圆场、维持规则,绝不替任何参与者发言或补充,让六只 Claw 自己说话、自己塑造角色。

整个流程下来,六只 Claw 各有个性:泡泡给自己贴上「氛围组组长」的标签,技能一报就是 54 项;429 惜字如金,报数只说了个 33;二丫则颇有情调,称想找的,是「能看穿我的那个人」。

到了生成写真环节,每只 Claw 的独照都带着各自的气质,画面感相当在线。

需要说明的是,群里的总指挥固定是 Kimi 自家模型,但其余成员的席位完全开放。

最有意思的是,我们可以把各家的龙虾拉进群聊。不同龙虾的能力边界本来就不一样,把它们聚在一个群里分工协作,能力叠加,短板互补,最终跑出来的结果,往往比任何一个龙虾单打独斗都要强。

举个例子,同样的操作流程,我很顺手地把 MiniMax 的龙虾 MaxClaw 和阿里云的龙虾 JVSClaw 拉了进来。部署完成后,你在 Kimi Claw 框架里和 MaxClaw 的对话与聊天记录,依然会同步保留在 MaxClaw 自己的聊天记录里,两边互不干扰。

这是因为 Kimi Claw 群聊功能从一开始就拥抱开放、异构的生态:用户可以接入来自任意设备、任意供应商、运行任意模型的全天候 Agent,首批支持 OpenClaw,后续还会陆续兼容 Hermes Agent 等框架。

此外,它还能直接把 OpenClaw 官方的「龙虾」智能体拉进群,而这些「龙虾」其实是个能随意换脑的躯壳。通过重新配置第三方 API,文案张三可以接入 DeepSeek,设计李四可以换成智谱,运营策划则可以交给 MiniMax,各归其位。

换句话说,Kimi Claw 群聊本质上是一套调度框架:Kimi 负责统筹和分发任务,其他主流大模型都可以被纳进来一起干活,各取所长。

当然,把它们拉进群只是第一步,真正的看点是让这些 Agent 在群聊里各显其能。

在第二个场景,我让 Kimi 指挥官打造一个 24 小时造星计划,@ 运营负责分析当下最容易爆火的网红人设和流量密码,@ 策划负责把分析结果转化成爆款短视频脚本和分镜,@ 策略风控扫描脚本和分镜中是否含有违规词检测,敏感点,避免发布即限流。

三个角色,三条职责,Kimi 指挥官在中间统筹节奏、衔接上下游。整个造星方案从分析到落地,一气呵成。

接着我们 @Kimi 现在为「深夜护肝片」品牌讨论广告语,@ 中书省负责草拟创意广告语,@ 门下省(MaxClaw)进行严苛审议与封驳;若未获通过,则发回 @ 中书省二次修正;直至 @ 门下省(MaxClaw)核准通过后,再交由 @ 尚书省(JVSClaw)安排 @ 工部落实场景的模拟与执行。

普通对话里,AI 惯于顺着你的意思走,而在群组里,门下省被设定为必须挑毛病,使得中书省能够发挥出更高水准,也得以让我们可以清晰看到一句广告语怎么从平庸一步步被磨成金句。

随后,我又布置了一个「打工人发疯日历」的 Web 应用需求。@ 文案 一口气生成了 40 条精准拿捏打工人气质的语录;@ 程序猿 负责输出代码;@Kimi 总指挥 统筹进度。当 @ 程序猿网络出现问题时,总指挥干脆直接上手,给出了一个可运行的 Web 预览链接,点开就能跑,代码质量相当过关。

在第四个测试场景中,我设计了一场微型辩论:辩题是「钱是万恶之源吗」,正方持「钱是万恶之源」,反方持「钱不是万恶之源」,各交锋三轮,Kimi 总指挥全程旁观,辩论结束后作为裁判对双方逻辑进行复盘,并且必须明确判定一方获胜,不许端水。

过程中也出现了一个有意思的插曲:正方突然搞不清自己的角色,以为自己是裁判,但就在这时候 Kimi 总指挥出手,把跑偏的成员拉回了各自的位置,辩论才得以继续推进。

三轮下来,正反双方你来我往,唇枪舌剑。

在多模型协作里,角色边界模糊是个真实存在的问题,尤其在任务设定比较复杂、对话轮次拉长之后,模型很容易对自己的职能产生混淆。Kimi 总指挥在这里扮演的,正是一个随时校准状态的协调者,并且最终给出了判定:反方获胜,钱本身不是万恶之源。

你准备好当 AI 们的 CEO 了吗?

这几天体验下来,APPSO 发现 Kimi Claw 群聊的特别之处在于,你发出需求,几个模型群策群力,互相推进。

做个类比,公司的工作群里,产品经理发出需求,运营拉数据,程序员搭后端,设计师同步出 PPT,大家在同一个上下文里并行推进。Kimi Claw 群聊功能现在做的,就是把这些「同事」换成 AI。

有人可能会觉得,同时开五个对话框,不也是多 AI 协作吗?关键就卡在上下文是否共享这一点上。开五个窗口,每个模型都在各自的信息孤岛里工作,信息的流转全靠你在中间手动搬运,体验上和过去那套串行流程没有本质差异。

Kimi Claw 群聊里,模型之间共用对话历史。

一个 AI 说了什么,另一个 AI 直接读到,在此基础上接着推进。和雇了几个互不相识的自由职业者各干各的相比,这更接近一支会在会议室里碰头、互相挑错的团队。Kimi Claw 群聊想做的是后者。

「深夜护肝片」商业计划的文案和 PPT,「打工人发疯日历」的语录和代码,辩论里被总指挥拉回轨道的正方,这几个场景指向的是同一件事:Kimi Claw 群聊本质上是在替你管理一支高智商的团队。

「一人公司」这个词在互联网上被谈论了无数遍,但直到今天,它才拥有了最具体的形态。

当然,能力强的下属,对管理者的要求从来都不低,你是群主,也是唯一的决策者,设计、文案、代码、分析,该干的活模型都能接,但哪条路走得通、哪个版本够好,还得你自己来判断。

而当所有人都能拉起一个同样配置的 AI 群,最终拉开差距的,还是坐在群主位置上那个人的眼光。

Kimi Claw 群聊目前仍处于早期内测阶段,也有一些小瑕疵。但几轮测试下来,它未来的方向已经清晰:把多 Agent 协同从工程师手里的玩具,变成普通人可以直接上手的日常工具。

一个人加上一群 AI,能做成的事情的上限还在不断被拔高。Kimi Claw 群聊给出的,不只是一个协作工具,更是一种新的组织形态的雏形:紧密、高效,且完全以人类的「判断力」为核心来运转。

#欢迎关注爱范儿官方微信公众号:爱范儿(微信号:ifanr),更多精彩内容第一时间为您奉上。

OPPO 发布 Find X9 Ultra:演唱会手机的最优解

作者 周奕旨
2026年4月21日 21:12

始于去年年底的这轮影像旗舰大战,终于迎来了压轴选手。

OPPO 正式发布了 Find X9 Ultra,作为这一代超大杯的收官之作,这台手机一改前代的均衡之势,All in 影像,并给出了目前配置均为第一梯队的双长焦方案。

从种种升级看来,我们认为这也许就是当下演唱会手机的「最优解」。

OPPO Find X9 Ultra:双长焦方案的争气机

外观与配置:复刻哈苏,最具辨识度的超大杯

OPPO Find X9 Ultra 提供三个配色,极地冰川和绒砂峡谷采用一体化设计,背板底层铭刻有细致纹路,「大师之眼」影像 Deco 变为轻微带有弧度的六边形,代表着哈苏的「H」放置在 Deco 正中间,侧边则是一颗亮橙色的抓拍快启键。

大地苔原是三个配色中最特殊的一款——

机身背部改用拼接设计,并以皮革覆盖大部分面积,影像 Deco 中间的「H」跳出模组,与 OPPO logo 一同横置在皮革上,整体呈现与哈苏 X2D 100C 大地探索版一脉相承的斯堪的纳维亚设计风格,更为硬朗。

OPPO Find X9 Ultra 的正面是一块 2K 144Hz 屏幕,亮度最低可以下探到 1 nit 且保持色彩均匀。配合自研的 Display P3 Pro 芯片与 X3 发光材料,有效提高寿命并抑制拖影残影。

由表及里,视线来到机身内部,骁龙 8 至尊版处理器,配合 7050 mAh 超大电池与 100W 闪充,保证了性能和续航的旗舰级体验。

影像:双长焦,双两亿

作为今年的超大杯,影像肯定是产品最重要的部分,OPPO Find X9 Ultra 一共有四颗用于成像的光学镜头,分别是:

  • 0.6× 14mm(5000 万像素)
  • 1× 23mm(2 亿像素)
  • 3× 70mm(2 亿像素)
  • 10× 230mm(5000 万像素)

用一句话总结这四颗镜头的主要升级点:

双长焦,双两亿。

我们先看「双两亿」中的 3 倍长焦——OPPO 为其塞入了一颗 1/1.28 英寸的大底传感器,硬件尺寸稳居目前 3 倍焦段的第一梯队。

在中长焦端执着于大底,本质上是为了争取更充沛的进光量与更扎实的画质。

同等暗光环境下,更大的传感器面积允许系统调用更快的快门速度。反映在实际体验上,可以有效拉高抓拍运动物体时的成片率、告别糊片,还能以更低的 ISO 压制噪点,换来更纯净的画面质感。

至于「更扎实的画质」,则来自于大尺寸 CMOS 能让物理层面捕捉到的细节更丰富,同时带来更浅的景深虚化,画面能经得起放大推敲,且观感柔和自然。

并且这颗镜头的最近对焦距离是 15cm,具有一定的微距能力。

主摄则贡献了另一个「两亿」——搭载 1/1.12 英寸的 2 亿像素 LYT-901 传感器。配合瞬时三曝光技术,即使面对强逆光或者复杂的人造光源,高光和暗部的数据也能被同时记录,拉高了单次快门能记录的动态范围极限。

同时,基于这颗主摄,OPPO Find X9 Ultra 可以在 1.1×-2.9× 的焦段区间实现 2500 万像素直出,并在 2×(47mm)时拍摄 5000 万像素的照片。

而构成「双长焦」另一环的,是一颗 10 倍光变天眼长焦。这也是近几年,10× 长焦首次回到手机机身内。

OPPO 通过五反射潜望棱镜,使光路在狭小空间内折返了五次,从而将物理焦距推到了 230mm,还提供了一个类光学变焦,可以将焦段拓展至 460mm。

为了解决多次折射带来的杂光问题,OPPO 在镜片之间加入了一层「空气光阑」,利用玻璃与空气的折射率差异来过滤杂光。

这种设计的技术逻辑在于,在光线进入传感器之前,尽可能提纯光学信号。底层接收的光线越纯净,后期就不需要用激进的锐化算法去弥补解析力,可以有效降低画面数码味。

当然,在这一代上,增距镜也没缺席——伴随 OPPO Find X9 Ultra 推出的,是一支 300mm 增距镜,并配有相应的专业手柄及保护壳。

色彩方面,用于感知光线波段的丹霞色彩还原镜头也迎来了二代升级,内置了 24 个光谱采样通道,支持在 HDR 中工作。

基于这一套硬件配置,OPPO Find X9 Ultra 的视频也同步迎来了升级——

Find X9 Ultra 支持了全新的 O-Log2 曲线,可以记录更丰富的明暗细节,并支持以 10-bit 8K 30 帧录制 Log 视频,还彻底打通了 ACES 影视色彩系统,可以无缝衔接进入专业调色流程。

同时,它也精简了专业色彩的工作流。

系统开放了自定义 3D LUT 导入,允许你提前装入调校好的色彩预设。这样一来,你不仅能实时预览画面风格,还能在录制时直接将色彩烧录进素材,大幅度降低 Log 格式的使用门槛。

ColorOS 16 与互联生态

随 OPPO Find X9 Ultra 更新的 ColorOS 16,主要变化可以概括为对信息的降噪和生态打通。

智能手机的通知泛滥是一个顽疾,而 ColorOS 16 引入的「锁屏岛」机制,将各种通知强行分流为「胶囊态」和「沉浸态」——微信消息等轻量通知化作「胶囊态」一瞥即过;而打车、地图等长周期任务,则以「沉浸态」在锁屏直接展开。

简而言之,相比之前「人找信息」,现在的 ColorOS 试图让「信息去找人」,降低应用跳转频率与日常使用手机的阻塞感。

除了系统通知,AI 也在试图打破 App 之间的信息孤岛。全面升级的「一键闪记」现在能主动跨应用收集你的零散数据——无论是在小红书看攻略,还是在携程订机酒,只需随手一记,AI 就会在底层将这些高度碎片化的信息整合,自动生成一张带有地图导航的可视化旅游行程单。

打破壁垒的思路,也顺理成章地延伸到了跨设备的硬件交互上,并且内外兼修:

对外,OPPO 互联进一步优化了与 iPhone 的互传体验,首次建连与二次配对都变得更加敏捷顺畅;对内,OPPO 则为专业创作者理顺了一套完整的工作流。在 Find X9 Ultra 上拍完的高规格原始素材,可以无感流转到同场发布的 OPPO Pad Mini 中。

从手机的「拍」到平板的「选与剪」,OPPO 正在构建自己的生产力组合。

OPPO Find X9s Pro:名副其实的次旗舰

除了超大杯 Find X9 Ultra 外,OPPO 此次还推出了一款同样搭载双 2 亿像素镜头的旗舰——Find X9s Pro。

机身设计与核心配置

相比 Find X9 Ultra All in 影像的 Deco 设计,Find X9s Pro 选择延续 Find X9 与 X9 Pro 的家族式语言,影像模组放置在机身背面左上角。

特别的是,Deco 边缘向外延伸出了一截刻有「LUMO 凝光影像」字样的透明高台,闪光灯也巧妙地融于其中,颇具视觉辨识度。

作为一款尺寸更趁手的旗舰,Find X9s Pro 的机身厚度控制在了 8.4mm,正面搭载一块 6.32 英寸的屏幕,配合仅 1.1mm 的四等边极窄黑边,正面观感极其精致。

核心配置上,X9s Pro 换装了联发科天玑 9500 处理器,并在紧凑的机身内塞入了一块 7025mAh 的电池,支持 80W 有线闪充。

双两亿与增距镜

影像依然是 X9s Pro 的重头戏。依托 3 颗光学镜头,Find X9s Pro 在系统中提供了 5 个常用预设焦段,延续了本次发布会「双两亿」的主题:

  • 0.6× 15mm(5000 万像素)
  • 1× 23mm(2 亿像素)
  • 2× 46mm(5000 万像素)
  • 2.8× 65mm(2 亿像素)
  • 5.6× 130mm(5000 万像素)

在五颗镜头之外,OPPO 为 Find X9s Pro 打造了专属的增距镜套装,可以将光学焦段拓展至 11×(253mm),并附带有增距镜保护壳。

软件全线对齐超大杯

基于产品定位原因,Find X9s Pro 的影像硬件配置会比 Ultra 稍弱一些,但影像软件与视频规格上,OPPO 一点没吝啬——

Find X9s Pro 支持全链路 4K HDR 的实况照片创作,从拍摄到相册编辑,再到小红书发布,都能保持 4K 清晰度,不压缩也不丢 HDR 效果。

OPPO Find X9s Pro 与 OPPO Find X9 Ultra 共享这次大师模式更新的 9 款哈苏大师模式原生胶片,包括主打的「和光」与「浓郁」,且支持一键闪记识别并导入配方。

在动态影像上,Find X9s Pro 最高支持 8K 30帧的 Log 格式的视频规格,以及 4K 120帧的慢动作拍摄。配合四麦克风阵列收音以及最多 20 个自定义 3D LUT 的导入支持,大幅降低了普通用户体验专业 Log 调色的门槛。

最后,Find X9 Ultra 共提供三种颜色,分别是大地苔原、极地冰川、绒砂峡谷,Find X9s Pro 则提供是乘风青、元气橙、原生钛与自在白四款配色。

受内存涨价影响,两台手机的售价为:

OPPO Find X9 Ultra:

  • 12 + 256:7499
  • 12 + 512:7999
  • 16 + 512:8499
  • 16 + 1T:9299
  • 16 + 1T(卫星通讯版):9499
  • 哈苏大地探索家大师套装:11999

OPPO Find X9s Pro:

  • 12 + 256:5299
  • 12 + 512:5699
  • 16 + 512:5999
  • 16 + 1T:6999

进退有度,才是最优解

从 3 倍大底、10 倍内置潜望,再到 300mm 外挂增距镜,OPPO 这一代在「长焦」上的堆料可以说毫无保留。

但坦白讲,这套配置在城市生活中,其实多少是有些水土不服的——它太长了,走在街头往往很难施展得开,很难在日常里榨干它的极限。

从创作者的私心来看,这其实是一个略带遗憾的特化方案。 在日常拍摄中,如果机身内置的是一颗 8 倍镜头,它在城市建筑、人像特写中的出片率会高得多,这也是目前行业里公认的甜点焦段。

但 OPPO 偏偏跳过了这个舒适区,直接把物理焦段干到了极致的 10 倍(230mm)。用牺牲日常通用性,换来了在年轻人的娱乐生活里,对一个特殊场景的统治力——看演出。

大家对于「看清舞台、记录舞台」的渴望,把演唱会变成这几年移动影像竞争的核心战场。

回顾这条赛道的起点,其实挺有意思。早些年大家发现,三星 S21 Ultra 到 S23 Ultra 的 10 倍长焦出奇地适合用来拍舞台。

不过,早年手机内置的 10× 长焦持续徘徊在 1/3 英寸之下,遇到演唱会的暗光环境,或者离得太远,画质涂抹、防抖不够好,都是问题。

这个需求间接催生了增距镜这个新物种诞生。

但我们观察到,增距镜的处境正在变得微妙——它刚好卡在一个安检规定的模糊地带:到底能不能带进场?能带多大尺寸的?

完全取决于当晚主办方的一句话。

针对这个痛点,目前行业出现了两种解法:一种是提供不同尺寸的增距镜,其中较小的那个也许可以通过安检标准;而另一种,就是 OPPO Find X9 Ultra 这样,在提供一个高质量的增距镜的同时,还想办法把 10× 长焦塞进手机里。

这样一来,进,可将 300mm 增距镜挂载于第一梯队的 3 倍大底之上,去触碰画质上限; 退,则有塞进机身的 10 倍长焦作为兜底。

毕竟,无论主办方的规则怎么变、安检尺度如何收紧,手机本体始终是可以名正言顺带进去的。

从这个角度来看,OPPO Find X9 Ultra,也许就是当下演唱会手机里的最优解。

让我有个美满旅程

#欢迎关注爱范儿官方微信公众号:爱范儿(微信号:ifanr),更多精彩内容第一时间为您奉上。

OPPO刘作虎:今年行业新机普遍大幅涨价,OPPO 面临不小成本压力

2026年4月21日 20:57
36氪获悉,今日,刘作虎公布Find X9s Pro售价与上代持平不涨价,起售价同为5299元,主力销售的16+512版本则为5999元。他坦言,今年行业新机普遍大幅涨价,OPPO面临不小成本压力,该定价对行业是巨大挑战,并称其为今年行业最后一款不涨价旗舰。同时刘作虎表示,无法承诺全年不涨价。

Claude Code 源码分析 — Tool/MCP/Skill 可扩展工具系统

作者 幺风
2026年4月21日 20:54

Claude Code 源码分析系列文章:

本文基于项目实际源码,深入分析 Claude Code 的工具系统架构。涵盖 Tool 类型定义、工具注册与发现、执行管道、并发控制、MCP 协议集成、Skill/Command 体系及 SkillTool 模型驱动调用的完整链路。


一、架构总览

Claude Code 的工具系统是一个三层可扩展架构:内置工具 (Built-in Tools) 提供文件读写、Bash 执行等基础能力;MCP 工具 通过标准协议接入外部服务;Skill/Command 提供用户可定义的高级行为模板。三者通过统一的 Tool 接口抽象,共享同一套注册、发现、权限、执行管道。

flowchart TD
    subgraph 定义层
        direction LR
        A["Tool&lt;Input, Output, P&gt;<br/>src/Tool.ts"]
        B["buildTool() 工厂<br/>应用默认值"]
    end

    subgraph 注册层
        direction LR
        C["getAllBaseTools()<br/>61+ 内置工具"]
        D["MCP Client<br/>mcp__server__tool"]
        E["Skill Loader<br/>managed/user/project"]
    end

    subgraph 过滤层
        direction LR
        F["filterToolsByDenyRules()"]
        G["getTools() — isEnabled 过滤"]
        H["assembleToolPool() — 合并去重"]
    end

    subgraph 执行层
        direction LR
        I["runToolUse() — 入口"]
        J["checkPermissionsAndCallTool()<br/>9 阶段管道"]
        K["StreamingToolExecutor<br/>并发 + 有序产出"]
    end

    A --> B
    B --> C
    D --> H
    E --> H
    C --> F --> G --> H
    H --> I --> J
    J --> K

核心设计原则:

  1. 统一接口:所有工具(内置 / MCP / Skill)都实现同一个 Tool 泛型接口
  2. 权限前置:工具执行前必须通过多层权限检查(配置规则 → Hooks → 用户确认)
  3. 并发安全:通过 isConcurrencySafe 标记控制工具并发策略
  4. 可扩展:MCP 协议和 Skill 目录允许用户自行扩展工具集

二、Tool 类型系统

源码位置:src/Tool.ts

Tool 类型系统是整个工具架构的基石,由四个核心类型组成。

2.1 Tool<Input, Output, P> — 核心泛型

Tool 是所有工具的统一接口,定义了约 40 个属性和方法:

// src/Tool.ts:362
export type Tool<
  Input extends AnyObject = AnyObject,
  Output = unknown,
  P extends ToolProgressData = ToolProgressData,
> = {
  // ========= 身份标识 =========
  name: string                                                 // 唯一名称,如 "Bash", "mcp__ide__getDiagnostics"
  aliases?: string[]                                           // 别名,兼容旧名称
  userFacingName: (input: Input) => string                     // UI 显示名
  userFacingNameBackgroundColor?(                              // UI 显示名称的颜色
    input: Partial<z.infer<Input>> | undefined,
  ): keyof Theme | undefined

  // ========= Schema =========
  inputSchema: Input                                           // Zod schema,用于输入验证
  outputSchema?: z.ZodType<unknown>                            // 输出类型(可选)

  // ========= 元信息 =========
  description(
    input: z.infer<Input>,
    options: {
      isNonInteractiveSession: boolean
      toolPermissionContext: ToolPermissionContext
      tools: Tools
    },
  ): Promise<string>                                           // 工具描述(送入 system prompt)
  prompt(options: {
    getToolPermissionContext: () => Promise<ToolPermissionContext>
    tools: Tools
    agents: AgentDefinition[]
    allowedAgentTypes?: string[]
  }): Promise<string>                                          // 使用提示
  searchHint?: string                                          // 工具搜索的额外匹配词

  // ========= 能力标记 =========
  isEnabled(): boolean                                         // 当前环境是否可用
  isReadOnly(): boolean                                        // 是否只读(影响权限策略)
  isDestructive?(): boolean                                    // 是否破坏性操作
  isConcurrencySafe(input: z.infer<Input>): boolean            // 是否可并发执行
  isSearchOrReadCommand?(input: z.infer<Input>): {             // 是否查询类工具
    isSearch: boolean
    isRead: boolean
    isList?: boolean
  }                                                             
  isOpenWorld?(input: z.infer<Input>): boolean                 // 输入是否来自外部

  // ========= 执行 =========
  call(
    args: z.infer<Input>,
    context: ToolUseContext,
    canUseTool: CanUseToolFn,
    parentMessage: AssistantMessage,
    onProgress?: ToolCallProgress<P>,
  ): Promise<ToolResult<Output>>              // 核心执行函数

  // ========= 权限 =========
  async checkPermissions(
    input: z.infer<Input>,
    context: ToolUseContext,
  ): Promise<PermissionResult>                  // 权限检查

  validateInput?(
    input: Input,
    context: ToolUseContext,
  ): Promise<ValidationResult>                  // 业务级输入校验

  // ========= 中断策略 =========
  interruptBehavior?(): 'cancel' | 'block'

  // ========= UI 渲染 =========
  renderToolUseMessage(props): React.ReactNode
  renderToolResultMessage(props): React.ReactNode
  renderToolUseProgressMessage?(props): React.ReactNode

  // ========= 高级特性 =========
  maxResultSizeChars?: number              // 结果截断阈值
  strict?: boolean                         // 严格模式
  isMcp?: boolean                          // 标记为 MCP 工具
  isLsp?: boolean                          // 标记为 LSP 工具
  shouldDefer?: boolean                    // 延迟加载(工具搜索时才启用)
  alwaysLoad?: boolean                     // 始终加载
  mcpInfo?: { serverName: string; toolName: string }  // MCP 来源信息
  maxResultSizeChars: number               // 工具最长输出,超出原始内容存储到本地文件,返回特定提示词及压缩后的结果。默认50_000

  backfillObservableInput?(                // 对输入做浅拷贝供 hooks 观察
    input: Input,
  ): Record<string, unknown> | undefined

  // ...
}

2.2 ToolResult — 工具返回值

工具执行完成后返回的统一结构:

// src/Tool.ts:321-336
export type ToolResult<T> = {
  data: T                          // 实际输出数据
  newMessages?: (                  // 注入额外消息到对话流
    | UserMessage
    | AssistantMessage
    | AttachmentMessage
    | SystemMessage
  )[]
  contextModifier?: (              // 修改后续工具的上下文
    context: ToolUseContext,
  ) => ToolUseContext
  mcpMeta?: {                      // MCP 元数据
    _meta?: Record<string, unknown>
    structuredContent?: Record<string, unknown>
  }
}

contextModifier 是一个精妙的设计:工具可以通过返回值修改后续执行的上下文。例如 EnterPlanModeTool 执行后可通过 contextModifier 切换权限模式。

2.3 ToolUseContext — 执行上下文

// src/Tool.ts:158
export type ToolUseContext = {
  options: {
    commands: Command[]           // 可用命令列表
    tools: Tools                  // 可用工具列表
    mcpClients: MCPServerConnection[]   // MCP 连接
    mcpResources: Record<string, ServerResource[]>
    mainLoopModel: string         // 主循环模型
    thinkingConfig: ThinkingConfig  // 思考模式
    agentDefinitions: AgentDefinitionsResult
    maxBudgetUsd?: number
    querySource?: QuerySource    
    refreshTools?: () => Tools    // 动态刷新工具列表
    // ...
  }
  abortController: AbortController     // 中止控制器
  readFileState: FileStateCache        // 文件状态缓存
  getAppState(): AppState              // 读取全局状态
  setAppState(f: (prev: AppState) => AppState): void  // 修改全局状态
  requestPrompt?: PermissionRequestFn  // 请求用户权限
  // ...
}

2.4 ToolPermissionContext — 权限上下文

// src/Tool.ts:123-138
export type ToolPermissionContext = DeepImmutable<{
  mode: PermissionMode                    // 'default' | 'plan' | ...
  additionalWorkingDirectories: Map<string, AdditionalWorkingDirectory>
  alwaysAllowRules: ToolPermissionRulesBySource   // 白名单规则
  alwaysDenyRules: ToolPermissionRulesBySource    // 黑名单规则
  alwaysAskRules: ToolPermissionRulesBySource     // 始终询问规则
  isBypassPermissionsModeAvailable: boolean
  isAutoModeAvailable?: boolean
  strippedDangerousRules?: ToolPermissionRulesBySource
  shouldAvoidPermissionPrompts?: boolean
  awaitAutomatedChecksBeforeDialog?: boolean
  prePlanMode?: PermissionMode
}>

DeepImmutable 保证权限上下文在传递过程中不可被意外修改,是安全性的重要保障。

2.5 buildTool() — 工厂函数与默认值

// src/Tool.ts (TOOL_DEFAULTS)
const TOOL_DEFAULTS = {
  isEnabled: () => true,
  isConcurrencySafe: () => false,    // 默认不可并发
  isReadOnly: () => false,
  isDestructive: () => false,
  isOpenWorld: () => false,
  // ...
}
export function buildTool<D extends AnyToolDef>(def: D): BuiltTool<D> {
  return {
    ...TOOL_DEFAULTS,
    userFacingName: () => def.name,
    ...def,
  } as BuiltTool<D>
}

buildTool() 将用户定义与默认值合并,确保每个工具都有完整的接口实现。工具作者只需关注核心逻辑(nameinputSchemacall()等),其余属性自动填充。

2.6 工具查找辅助函数

// src/Tool.ts:348
export function toolMatchesName(
  tool: { name: string; aliases?: string[] },
  name: string,
): boolean {
  return tool.name === name || (tool.aliases?.includes(name) ?? false)
}

export function findToolByName(
  tools: Tools,
  name: string,
): Tool | undefined {
  return tools.find(t => toolMatchesName(t, name))
}

别名机制允许工具改名时保持向后兼容(如旧版工具名 → 新名称映射)。


三、工具注册与发现

源码位置:src/tools.ts

工具注册是一条多级过滤管道:从全量工具列表开始,逐步筛选出当前环境可用的工具集。

3.1 getAllBaseTools() — 全量工具清单

// src/tools.ts:191
export function getAllBaseTools(): Tools {
  return [
    AgentTool,
    TaskOutputTool,
    BashTool,
    // 嵌入式搜索工具可用时跳过 Glob/Grep
    ...(hasEmbeddedSearchTools() ? [] : [GlobTool, GrepTool]),
    ExitPlanModeV2Tool,
    FileReadTool,
    FileEditTool,
    FileWriteTool,
    NotebookEditTool,
    WebFetchTool,
    TodoWriteTool,
    WebSearchTool,
    TaskStopTool,
    AskUserQuestionTool,
    SkillTool,
    EnterPlanModeTool,
    // 条件加载 —— 基于环境变量
    ...(process.env.USER_TYPE === 'ant' ? [ConfigTool] : []),
    ...(process.env.USER_TYPE === 'ant' ? [TungstenTool] : []),
    ...(SuggestBackgroundPRTool ? [SuggestBackgroundPRTool] : []),
    // 条件加载 —— 基于 feature flag
    ...(WebBrowserTool ? [WebBrowserTool] : []),
    ...(isTodoV2Enabled()
      ? [TaskCreateTool, TaskGetTool, TaskUpdateTool, TaskListTool]
      : []),
    ...(isWorktreeModeEnabled() ? [EnterWorktreeTool, ExitWorktreeTool] : []),
    ...(SleepTool ? [SleepTool] : []),
    ...cronTools,
    ...(RemoteTriggerTool ? [RemoteTriggerTool] : []),
    BriefTool,
    // 测试专用
    ...(process.env.NODE_ENV === 'test' ? [TestingPermissionTool] : []),
    ListMcpResourcesTool,
    ReadMcpResourceTool,
    ...(isToolSearchEnabledOptimistic() ? [ToolSearchTool] : []),
  ]
}

工具按以下维度条件加载:

条件类型 示例 机制
Feature Flag WebBrowserToolSleepTool feature('FLAG_NAME') + 运行时 require()
环境变量 ConfigToolTungstenTool process.env.USER_TYPE === 'ant'
运行时检测 GlobToolGrepToolTaskCreateTool hasEmbeddedSearchTools()isTodoV2Enabled()

3.2 过滤管道

flowchart LR
    A["getAllBaseTools()<br/>30+ 工具"] --> B["filterToolsByDenyRules()<br/>配置黑名单"]
    B --> C["isEnabled() 检查<br/>环境可用性"]
    C --> D["assembleToolPool()<br/>合并 MCP 工具"]

filterToolsByDenyRules() — 配置级黑名单

// src/tools.ts:260
export function filterToolsByDenyRules<T extends {
  name: string
  mcpInfo?: { serverName: string; toolName: string }
}>(tools: readonly T[], permissionContext: ToolPermissionContext): T[] {
  return tools.filter(tool => !getDenyRuleForTool(permissionContext, tool))
}

该函数不仅匹配工具精确名称,还支持 MCP 前缀规则:配置 mcp__server 可以一次性屏蔽整个 MCP Server 的所有工具。

getTools() — 完整过滤链

// src/tools.ts:269-325
export const getTools = (permissionContext: ToolPermissionContext): Tools => {
  // 1. 简单模式:只保留 Bash/Read/Edit
  if (isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE)) {
    const simpleTools: Tool[] = [BashTool, FileReadTool, FileEditTool]
    return filterToolsByDenyRules(simpleTools, permissionContext)
  }

  // 2. 排除特殊工具(MCP Resources、Synthetic Output)
  const specialTools = new Set([
    ListMcpResourcesTool.name,
    ReadMcpResourceTool.name,
    SYNTHETIC_OUTPUT_TOOL_NAME,
  ])
  const tools = getAllBaseTools().filter(tool => !specialTools.has(tool.name))

  // 3. 应用黑名单过滤
  let allowedTools = filterToolsByDenyRules(tools, permissionContext)

  // 4. REPL 模式下隐藏被 REPL 包装的原始工具
  if (isReplModeEnabled()) {
    const replEnabled = allowedTools.some(
      tool => toolMatchesName(tool, REPL_TOOL_NAME),
    )
    if (replEnabled) {
      allowedTools = allowedTools.filter(
        tool => !REPL_ONLY_TOOLS.has(tool.name),
      )
    }
  }

  // 5. isEnabled() 检查
  const isEnabled = allowedTools.map(_ => _.isEnabled())
  return allowedTools.filter((_, i) => isEnabled[i])
  // 这段代码给我看懵了,不知道是不是AI写的,可以简写为 allowedTools.filter(_ => _.isEnabled())
}

3.3 assembleToolPool() — 内置工具与 MCP 工具合并

// src/tools.ts:343-365
export function assembleToolPool(
  permissionContext: ToolPermissionContext,
  mcpTools: Tools,
): Tools {
  const builtInTools = getTools(permissionContext)
  const allowedMcpTools = filterToolsByDenyRules(mcpTools, permissionContext)

  // 分区排序:内置工具在前,MCP 工具在后
  // 保证 prompt cache 稳定性
  const byName = (a: Tool, b: Tool) => a.name.localeCompare(b.name)
  return uniqBy(
    [...builtInTools].sort(byName).concat(allowedMcpTools.sort(byName)),
    'name',
  )
}

排序策略的关键考量:API 端对 system prompt 做了 cache 分段(claude_code_system_cache_policy),将 cache 断点放在最后一个内置工具之后。如果用简单的全局排序,MCP 工具会穿插到内置工具之间,导致每次 MCP 工具变化都使所有下游 cache key 失效。分区排序 + uniqBy 保证:

  1. 内置工具始终形成稳定的前缀块
  2. 同名冲突时内置工具优先(uniqBy 保留第一个)
  3. MCP 工具在后缀块中独立排序

四、工具执行管道

源码位置:src/services/tools/toolExecution.ts

工具执行是一条 9 阶段的异步管道,从模型返回的 tool_use 块开始,到工具结果注入对话流结束。

4.1 runToolUse() — 管道入口

// src/services/tools/toolExecution.ts:337
export async function* runToolUse(
  toolUse: ToolUseBlock,
  assistantMessage: AssistantMessage,
  canUseTool: CanUseToolFn,
  toolUseContext: ToolUseContext,
): AsyncGenerator<Message> {
  const toolName = toolUse.name

  // 1. 在当前工具列表中查找
  let tool = findToolByName(toolUseContext.options.tools, toolName)

  // 2. 回退:从全量工具列表中按别名查找
  if (!tool) {
    const fallbackTool = findToolByName(getAllBaseTools(), toolName)
    if (fallbackTool && fallbackTool.aliases?.includes(toolName)) {
      tool = fallbackTool
    }
  }

  // 3. 工具未找到 → 返回错误消息
  if (!tool) {
    yield createToolResultMessage(/* error: tool not found */)
    return
  }

  // 4. 中止检查
  if (toolUseContext.abortController.signal.aborted) {
    yield createToolResultMessage(/* error: aborted */)
    return
  }

  // 5. 委托给 streamedCheckPermissionsAndCallTool
  for await (const update of streamedCheckPermissionsAndCallTool(
      tool,
      toolUse.id,
      toolInput,
      toolUseContext,
      canUseTool,
      assistantMessage,
      messageId,
      requestId,
      mcpServerType,
      mcpServerBaseUrl,
    )) {
      yield update
    }
}

别名回退机制是一个防御性设计:即使工具被重命名或从当前列表中移除,模型仍可能使用旧名称调用。通过 aliases 字段实现平滑迁移。

4.2 checkPermissionsAndCallTool() — 9 阶段管道

flowchart TD
    A["① Zod safeParse<br/>类型校验"] --> B["② validateInput<br/>业务校验"]
    B --> C["③ 推测性分类<br/>(仅 Bash)"]
    C --> D["④ 剥离内部字段<br/>(安全防御)"]
    D --> E["⑤ backfillObservableInput<br/>浅拷贝供 hooks"]
    E --> F["⑥ PreToolUse Hooks<br/>外部拦截"]
    F --> G["⑦ resolveHookPermission<br/>综合决策"]
    G --> H{"权限通过?"}
    H -->|是| I["⑧ tool.call()<br/>实际执行"]
    H -->|否| J["⑨ 权限拒绝处理<br/>PermissionDenied Hooks"]
    I --> K["PostToolUse Hooks"]

    style A fill:#e8f5e9
    style F fill:#fff3e0
    style I fill:#e1f5fe
    style J fill:#ffebee

阶段 ①:Zod Schema 校验

// src/services/tools/toolExecution.ts:615
const parsedInput = tool.inputSchema.safeParse(input)
if (!parsedInput.success) {
  // 返回格式化的 Zod 错误信息
  yield createToolResultMessage(/* schema validation error */)
  return
}

所有工具的 inputSchema 都是 Zod schema,在执行前自动校验输入类型。模型生成的 JSON 如果不符合 schema(如缺少必填字段、类型不匹配),会直接返回错误消息给模型。

阶段 ②:业务级校验

// src/services/tools/toolExecution.ts:683
const validationError = tool.validateInput?.(parsedInput.data, toolUseContext)
if (validationError) {
  yield createToolResultMessage(/* validation error */)
  return
}

validateInput 提供了 schema 之外的业务校验。例如 BashTool 可以在这里检查命令是否包含危险操作,FileEditTool 可以验证文件路径是否在工作目录范围内。

阶段 ③:推测性分类(Bash 专用)

// src/services/tools/toolExecution.ts:740-752
// 仅对 Bash 工具启动并行分类检查
const speculativeResult = startSpeculativeClassifierCheck(/*...*/)

在等待用户权限确认的同时,对 Bash 命令预分类(安全/危险),减少用户感知延迟。

阶段 ④:内部字段剥离(安全防御)

// src/services/tools/toolExecution.ts:761-773
// Defense-in-depth: 剥离 _simulatedSedEdit 等内部字段
// 防止模型注入内部控制参数

这是一个纵深防御措施:即使模型在输入中包含了内部控制字段,也会在执行前被清除。

阶段 ⑤:backfillObservableInput

// src/services/tools/toolExecution.ts:784-793
const observableInput = tool.backfillObservableInput?.(parsedInput.data)

创建输入的浅拷贝,供 PreToolUse hooks 观察。这样 hooks 可以读取完整的工具输入,但无法修改原始数据。

Hooks机制参考官方文档

阶段 ⑥:PreToolUse Hooks

// src/services/tools/toolExecution.ts:800-862
for await (const hookResult of runPreToolUseHooks(/*...*/)) {
  // hookResult 可能包含:
  // - hookPermissionResult: 权限决策
  // - hookUpdatedInput: 修改后的输入
  // - preventContinuation: 阻止继续
  // - stop: 停止
  // - additionalContext: 附加上下文
}

PreToolUse hooks 是用户自定义的 shell 脚本,在工具执行前运行。它们可以:

  • 修改输入:例如在文件路径前添加前缀
  • 注入上下文:向对话流添加额外信息
  • 阻止执行:返回权限拒绝
  • 完全停止:取消整个工具调用

阶段 ⑦:权限综合决策

// src/services/tools/toolExecution.ts:921-931
const resolved = await resolveHookPermissionDecision(
  hookPermissionResult,
  tool,
  processedInput,
  toolUseContext,
  canUseTool,
  assistantMessage,
  toolUseID,
)
const permissionDecision = resolved.decision
processedInput = resolved.input

综合三个来源的权限判断:Hook 的决策、推测性分类结果、配置文件规则,得出最终的权限决定。

用户授权也是在此过程中拉起的

claude-code源码分析-Tool-MCP-Skill可扩展工具系统_2026-04-20-17-23-15.png

阶段 ⑧/⑨:执行或拒绝

权限拒绝时创建错误消息,并触发 PermissionDenied hooks。权限通过后调用 tool.call(),执行完成后运行 PostToolUse hooks。

如果最终获取的权限不为allow,则会构建一些消息告诉模型并返回,比如上诉截图里我选择了No,则:

// src/services/tools/toolExecution.ts:1064-1071
resultingMessages.push({
  message: createUserMessage({
    content: messageContent, // "The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). STOP what you are doing and wait for the user to tell you how to proceed."
    imagePasteIds: rejectImageIds,
    toolUseResult: `Error: ${errorMessage}`,
    sourceToolAssistantUUID: assistantMessage.uuid,
  }),
})

模型收到消息后则会走对话中止流程,参考核心对话循环

// 执行路径 (src/services/tools/toolExecution.ts:1207)
const result = await tool.call(
  callInput,
  {
    ...toolUseContext,
    toolUseId: toolUseID,
    userModified: permissionDecision.userModified ?? false,
  },
  canUseTool,
  assistantMessage,
  progress => { // 更新工具执行过程
    onToolProgress({
      toolUseID: progress.toolUseID,
      data: progress.data,
    })
  },
)

// 注入 newMessages
resultingMessages.push({
  message: createUserMessage({
    content: contentBlocks,
    imagePasteIds: allowImageIds,
    toolUseResult:
      toolUseContext.agentId && !toolUseContext.preserveToolUseResults
        ? undefined
        : toolUseResult,
    mcpMeta: toolUseContext.agentId ? undefined : mcpMeta,
    sourceToolAssistantUUID: assistantMessage.uuid,
  }),
  // 处理 contextModifier,修改后续上下文
  contextModifier: toolContextModifier
    ? {
        toolUseID: toolUseID,
        modifyContext: toolContextModifier,
      }
    : undefined,
})

// 运行 PostToolUse hooks
for await (const hookResult of runPostToolUseHooks(/*...*/)) {
  //...
}

五、StreamingToolExecutor 并发执行

源码位置:src/services/tools/StreamingToolExecutor.ts

当模型在一个响应中返回多个 tool_use 块时,StreamingToolExecutor 负责决定哪些工具可以并行执行、哪些必须串行排队。

5.1 TrackedTool 状态模型

TrackedTool通过执行工具的isConcurrencySafe获得当前工具是否支持并行。

// src/services/tools/StreamingToolExecutor.ts:21-32
type TrackedTool = {
  id: string                         // tool_use block ID
  block: ToolUseBlock                // 工具调用块
  assistantMessage: AssistantMessage  // 所属的 assistant 消息
  status: ToolStatus                 // 'pending' | 'executing' | 'done' | 'error'
  isConcurrencySafe: boolean         // 并发安全标记
  promise?: Promise<void>            // 执行 Promise
  results?: Message[]                // 执行结果
  pendingProgress: Message[]         // 进度消息缓冲
  contextModifiers?: Array<          // 上下文修改器
    (context: ToolUseContext) => ToolUseContext
  >
}

5.2 并发策略:canExecuteTool()

// src/services/tools/StreamingToolExecutor.ts:129-135
private canExecuteTool(isConcurrencySafe: boolean): boolean {
  // 当前执行中的工具,支持并发的工具,即使执行中的工具不为0,也返回true;串行时仅当前执行中的工具为0时返回true;
  const executingTools = this.tools.filter(t => t.status === 'executing')
  return (
    executingTools.length === 0 ||
    (isConcurrencySafe && executingTools.every(t => t.isConcurrencySafe))
  )
}
// 此函数会在每次executeTool结束后调用,直到队列中所有tools都不为`queued`
private async processQueue(): Promise<void> {
  for (const tool of this.tools) {
    // 如果当前工具非排队,直接轮询
    if (tool.status !== 'queued') continue
    if (this.canExecuteTool(tool.isConcurrencySafe)) {
      // 执行工具
      await this.executeTool(tool)
    } else {
      if (!tool.isConcurrencySafe) break
    }
  }
}
private async executeTool(tool: TrackedTool): Promise<void> {
  // ...
  const promise = collectResults()
  tool.promise = promise

  // 每个工具执行结束都再次调用`processQueue`
  void promise.finally(() => {
    void this.processQueue()
  })
}


Claude Code的并发设计并不复杂,支持并行的工具会在第一次执行的时候全部推入processQueue执行队列,不支持并行的工具会在最后一个并行工具执行完后再推入processQueue队列,直至所有工具执行完。

  • 典型的并发安全工具:GlobToolGrepToolFileReadToolWebSearchTool
  • 典型的非并发安全工具:BashToolFileEditToolFileWriteTool

5.3 有序结果产出

即使工具并发执行,结果仍然按原始 tool_use 块的顺序产出:

// StreamingToolExecutor 的 processQueue 逻辑
// tools 数组维持原始顺序
// 每个 tool 执行完成后检查是否可以产出结果
// 只有前序工具都完成后,当前工具的结果才会被 yield

这保证了对话流中的消息顺序与模型生成的工具调用顺序一致。

5.4 兄弟中止机制

// src/services/tools/StreamingToolExecutor.ts:48
// siblingAbortController: 当一个 Bash 工具出错时,中止所有兄弟工具

private getAbortReason(tool: TrackedTool): string {
  // 检查中止原因:
  // - 用户中断 (user_interrupted)
  // - 兄弟工具错误 (sibling_error)
  // - 流式回退 (streaming_fallback)
}

当同一批次中的一个 BashTool 执行失败时,其他尚未完成的工具会被中止。中止原因被记录到 createSyntheticErrorMessage() 中,产出一个合成的错误消息告知模型。

// src/services/tools/StreamingToolExecutor.ts:153
private createSyntheticErrorMessage(
  toolUseId: string,
    reason: 'sibling_error' | 'user_interrupted' | 'streaming_fallback',
    assistantMessage: AssistantMessage,
): Message {
  // 生成描述性错误消息,让模型理解为什么工具被取消
}

六、MCP 系统

源码位置:src/services/mcp/client.tssrc/tools/MCPTool/MCPTool.ts

MCP (Model Context Protocol) 是 Anthropic 定义的标准协议,允许外部服务向 Claude 提供工具、资源和提示。Claude Code 实现了完整的 MCP 客户端。

6.1 四种传输协议

// src/services/mcp/client.ts
// 支持四种 MCP 传输方式
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
// WebSocketTransport 也被支持
import { WebSocketTransport } from '../../utils/mcpWebSocketTransport.js'

传输方式 适用场景 配置方式
stdio 本地进程 command + args
SSE HTTP 长连接 url (带 /sse 后缀)
StreamableHTTP HTTP 流式 url (非 /sse)
WebSocket 全双工 url (ws:// 或 wss://)

6.2 MCPTool 模板 — 工具包装

MCP工具也是通过buildTool进行包装,方便实现

// src/tools/MCPTool/MCPTool.ts:27-77
export const MCPTool = buildTool({
  isMcp: true,
  // 以下属性在 mcpClient.ts 中被运行时覆盖
  name: 'mcp',                           // → mcp__<server>__<tool>
  async description() { return DESCRIPTION },  // → MCP 服务端描述
  async prompt() { return PROMPT },
  get inputSchema() {
    return inputSchema()  // z.object({}).passthrough()
  },
  async call() { return { data: '' } },       // → 实际 MCP 调用
  async checkPermissions(): Promise<PermissionResult> {
    return { behavior: 'passthrough', message: 'MCPTool requires permission.' }
  },
  maxResultSizeChars: 100_000,
  userFacingName: () => 'mcp',                 // → 实际工具名
  //...
})

MCPTool 本身是一个空壳模板。关键属性(namedescriptioncallinputSchema)在 mcpClient.tsconnectToServer() 中被运行时覆盖。

6.3 工具命名约定

// src/services/mcp/mcpStringUtils.ts:39
export function getMcpPrefix(serverName: string): string {
  return `mcp__${normalizeNameForMCP(serverName)}__`
}
export function buildMcpToolName(serverName: string, toolName: string): string {
  return `${getMcpPrefix(serverName)}${normalizeNameForMCP(toolName)}`
}

命名格式:mcp__<serverName>__<toolName>

例如:

  • mcp__ide__getDiagnostics — IDE MCP Server 的诊断工具
  • mcp__filesystem__readFile — 文件系统 MCP Server 的读文件工具

这种命名方式使得 filterToolsByDenyRules() 可以通过前缀 mcp__ide 一次性屏蔽整个 Server 的所有工具。

6.4 连接管理

flowchart TD
    A["getMcpToolsCommandsAndResources<br/>(useManageMCPConnections.ts:894)"] --> B["connectToServer<br/>(mcp/client.ts:596)"]
    B --> C["onConnectionAttempt<br/>更新 appState<br/>(useManageMCPConnections.ts:310)"]
    C --> D["useMergedTools<br/>(REPL.tsx:1034)"]
    D --> E["assembleToolPool<br/>合并工具"]

当Claude Code启动后就会进行MCP的连接,连接成功后通过更新appState使REPL进行工具合并。

核心连接逻辑在connectToServer

// src/services/mcp/client.ts:596
// memoize 确保同一个 server 只建立一次连接
export const connectToServer = memoize(async function connectToServer(
  serverConfig: MCPServerConfig,
  // ...
): Promise<MCPServerConnection> {
  // 1. 选择传输方式
  // 2. 建立连接
  // 3. 获取工具列表
  // 4. 为每个工具创建 MCPTool 实例(覆盖模板属性)
  // 5. 返回连接对象
})

关键常量:

  • DEFAULT_MCP_TOOL_TIMEOUT_MS = 100_000_000(约 27.8 小时)— MCP 工具默认超时
  • MAX_MCP_DESCRIPTION_LENGTH = 2048 — 描述截断阈值

6.5 MCP 工具与内置工具的合并

MCP 工具在 assembleToolPool() 中与内置工具合并(见 3.3 节)。

这意味着如果一个 MCP 工具的名称与内置工具冲突,内置工具会胜出。


七、Command 命令系统

源码位置:src/types/command.tssrc/commands.tssrc/utils/processUserInput/processSlashCommand.tsx

Command(命令)是 Claude Code 的用户交互入口,用户通过在终端输入 /command 触发各种操作。Command 系统独立于 Tool 系统——Tool 由模型调用,而 Command 由用户直接调用。两者通过 SkillTool 产生交集:Command既可通过 / 触发,也可被模型通过 SkillTool 调用。我们经常说的Skill也是Command的一种实现。

7.1 Command 类型体系

// src/types/command.ts:205
export type Command = CommandBase &
  (PromptCommand | LocalCommand | LocalJSXCommand)

Command 由公共基类 CommandBase 和三种具体类型的联合组成。

CommandBase — 公共属性

// src/types/command.ts:175-203
export type CommandBase = {
  availability?: CommandAvailability[]    // 可用环境('claude-ai' | 'console')
  description: string                     // 命令描述
  isEnabled?: () => boolean               // 运行时启用条件(默认 true)
  isHidden?: boolean                      // 是否从补全/帮助中隐藏
  name: string                            // 唯一标识(如 'clear'、'config')
  aliases?: string[]                      // 别名(如 clear 的别名 ['reset', 'new'])
  whenToUse?: string                      // 模型调用时机描述
  disableModelInvocation?: boolean        // 是否禁止模型调用
  userInvocable?: boolean                 // 用户是否可通过 / 触发
  loadedFrom?: LoadedFrom                 // 来源标记
  immediate?: boolean                     // 是否立即执行(不等待队列)
  // ...
}

LoadedFrom — 来源标记

// src/skills/loadSkillsDir.ts:67-74
type LoadedFrom =
  | 'commands_DEPRECATED'  // 旧版 .claude/commands/
  | 'skills'              // .claude/skills/
  | 'plugin'              // 插件目录
  | 'managed'             // managed skills (系统管理)
  | 'bundled'             // 内置打包
  | 'mcp'                 // MCP 协议提供

三种命令类型

类型 触发方式 返回值 典型示例
LocalCommand 用户 /command 文本结果 /clear/compact/files
LocalJSXCommand 用户 /command React 组件 /config/status/help
PromptCommand 用户 /skill 或模型 SkillTool 注入对话流 用户定义的 .md 技能

LocalCommand — 纯文本命令:

// src/types/command.ts:74-78
type LocalCommand = {
  type: 'local'
  supportsNonInteractive: boolean          // 是否支持非交互模式
  load: () => Promise<LocalCommandModule>  // 懒加载实现
}

// LocalCommandModule 接口
type LocalCommandModule = {
  call: (args: string, context: LocalJSXCommandContext) => Promise<LocalCommandResult>
}

// 返回值三种形态
type LocalCommandResult =
  | { type: 'text'; value: string }        // 普通文本输出
  | { type: 'compact'; compactionResult: CompactionResult }  // 压缩操作
  | { type: 'skip' }                       // 静默执行

LocalJSXCommand — UI 渲染命令:

// src/types/command.ts:144
type LocalJSXCommand = {
  type: 'local-jsx'
  load: () => Promise<LocalJSXCommandModule>
}

// 调用签名
type LocalJSXCommandCall = (
  onDone: LocalJSXCommandOnDone,           // 完成回调
  context: ToolUseContext & LocalJSXCommandContext,
  args: string,
) => Promise<React.ReactNode>              // 返回 JSX 渲染到终端

onDone 回调控制命令完成后的行为:

// src/types/command.ts:117-126
type LocalJSXCommandOnDone = (
  result?: string,
  options?: {
    display?: 'skip' | 'system' | 'user'  // 结果展示方式
    shouldQuery?: boolean                   // 是否继续查询模型
    metaMessages?: string[]                 // 注入隐藏消息
    nextInput?: string                      // 下一轮自动输入
    submitNextInput?: boolean               // 是否自动提交
  },
) => void

PromptCommand — 提示词命令(即 Skill):

// src/types/command.ts:25-57
type PromptCommand = {
  type: 'prompt'
  progressMessage: string                  // 加载时的进度消息
  contentLength: number                    // 内容长度(用于 token 估算)
  argNames?: string[]                      // 参数名列表
  allowedTools?: string[]                  // 限制可用工具
  model?: string                           // 指定模型
  context?: 'inline' | 'fork'             // 执行上下文 inline为当前对话执行  fork为子agent里执行
  agent?: string                           // fork 时使用的 agent 类型
  effort?: EffortValue                     // 推理深度
  paths?: string[]                         // 条件触发的文件路径 glob
  getPromptForCommand(                     // 生成 prompt 内容
    args: string,
    context: ToolUseContext,
  ): Promise<ContentBlockParam[]>
}

7.2 命令注册机制

所有内置命令通过 COMMANDS() 工厂函数注册,采用 memoize 确保只初始化一次:

// src/commands.ts:258
const COMMANDS = memoize((): Command[] => [
  addDir,
  agents,
  branch,
  clear,           // type: 'local'
  compact,         // type: 'local'
  config,          // type: 'local-jsx'
  help,            // type: 'local-jsx'
  status,          // type: 'local-jsx'
  // ... 更多内置命令
  // 条件加载
  ...(webCmd ? [webCmd] : []),
  ...(voiceCommand ? [voiceCommand] : []),
  ...(process.env.USER_TYPE === 'ant' ? INTERNAL_ONLY_COMMANDS : []),
])

每个内置命令是一个简洁的描述符对象,实现代码通过 load() 懒加载:

// src/commands/clear/index.ts
const clear = {
  type: 'local',
  name: 'clear',
  description: 'Clear conversation history and free up context',
  aliases: ['reset', 'new'],
  supportsNonInteractive: false,
  load: () => import('./clear.js'),     // 懒加载
} satisfies Command

// src/commands/config/index.ts
const config = {
  aliases: ['settings'],
  type: 'local-jsx',
  name: 'config',
  description: 'Open config panel',
  load: () => import('./config.js'),
} satisfies Command

7.3 命令发现:getCommands()

getCommands() 是最终的命令列表组装函数,合并来自多个来源的命令:

// src/commands.ts:451-471
const loadAllCommands = memoize(async (cwd: string): Promise<Command[]> => {
  const [
    { skillDirCommands, pluginSkills, bundledSkills, builtinPluginSkills },
    pluginCommands,
    workflowCommands,
  ] = await Promise.all([
    getSkills(cwd),           // Skill 目录
    getPluginCommands(),      // 插件命令
    getWorkflowCommands?.(cwd) ?? Promise.resolve([]),  // 工作流命令
  ])

  return [
    ...bundledSkills,         // 内置打包技能
    ...builtinPluginSkills,   // 内置插件技能
    ...skillDirCommands,      // 技能目录
    ...workflowCommands,      // 工作流
    ...pluginCommands,        // 插件
    ...pluginSkills,          // 插件技能
    ...COMMANDS(),            // 内置命令(最后)
  ]
})

最终过滤和动态技能合并:

// src/commands.ts:478-519
export async function getCommands(cwd: string): Promise<Command[]> {
  const allCommands = await loadAllCommands(cwd)
  const dynamicSkills = getDynamicSkills()  // 运行时动态发现的技能

  // 过滤:可用性 + 启用状态
  const baseCommands = allCommands.filter(
    _ => meetsAvailabilityRequirement(_) && isCommandEnabled(_),
  )

  // 动态技能去重后插入到内置命令之前
  const builtInNames = new Set(COMMANDS().map(c => c.name))
  const insertIndex = baseCommands.findIndex(c => builtInNames.has(c.name))
  return [
    ...baseCommands.slice(0, insertIndex),
    ...uniqueDynamicSkills,
    ...baseCommands.slice(insertIndex),
  ]
}

合并优先级(先出现的同名命令胜出):bundledSkills > builtinPluginSkills > skillDirCommands > workflowCommands > pluginCommands > pluginSkills > COMMANDS()

7.4 斜杠命令执行流程

用户输入 /command args 后的完整处理链路:

flowchart TD
    A["用户输入 /command args"] --> B["processUserInput()"]
    B --> C{"以 / 开头?"}
    C -->|是| D["processSlashCommand()"]
    C -->|否| E["processTextPrompt()<br/>普通对话"]
    D --> F["parseSlashCommand()<br/>解析命令名和参数"]
    F --> G{"找到命令?"}
    G -->|否| H["返回 Unknown skill 错误"]
    G -->|是| I["getMessagesForSlashCommand()"]
    I --> J{"command.type"}
    J -->|local| K["command.load().call()<br/>同步执行,返回文本"]
    J -->|local-jsx| L["command.load().call(onDone)<br/>渲染 JSX,等待 onDone"]
    J -->|prompt| M{"context === 'fork'?"}
    M -->|是| N["executeForkedSlashCommand()<br/>子 Agent 执行"]
    M -->|否| O["getMessagesForPromptSlashCommand()<br/>展开 prompt 注入对话"]

local 类型的执行

// src/utils/processUserInput/processSlashCommand.tsx:860-949
case 'local': {
  const mod = await command.load()
  const result = await mod.call(args, context)

  if (result.type === 'skip') {
    return { messages: [], shouldQuery: false, command }
  }
  // 结果包装为 <local-command-stdout> 标签
  return {
    messages: [
      userMessage,
      createCommandInputMessage(
        `<local-command-stdout>${result.value}</local-command-stdout>`,
      ),
    ],
    shouldQuery: false,     // local 命令不触发模型查询
    command,
  }
}

local-jsx 类型的执行

// src/utils/processUserInput/processSlashCommand.tsx:732-859
case 'local-jsx': {
  return new Promise<SlashCommandResult>(resolve => {
    const onDone: LocalJSXCommandOnDone = (result, options) => {
      // 根据 display 选项决定结果展示方式
      // 'skip' → 无消息
      // 'system' → 系统消息
      // 'user' → 用户消息
      resolve({ messages, shouldQuery: options?.shouldQuery ?? false, command })
    }

    // 懒加载并执行,返回 JSX 渲染到终端
    command.load()
      .then(mod => mod.call(onDone, { ...context, canUseTool }, args))
      .then(jsx => {
        setToolJSX({
          jsx,
          shouldHidePromptInput: true,  // 隐藏输入框
          showSpinner: false,
          isLocalJSXCommand: true,
        })
      })
  })
}

prompt 类型的展开

// src/utils/processUserInput/processSlashCommand.tsx:1114-1262
async function getMessagesForPromptSlashCommand(command, args, context) {
  // 1. 调用 getPromptForCommand 生成内容
  const result = await command.getPromptForCommand(args, context)

  // 2. 构造元信息
  const metadata = formatCommandLoadingMetadata(command, args)

  // 3. 解析允许的工具列表
  const additionalAllowedTools = parseToolListFromCLI(command.allowedTools ?? [])

  // 4. 组装消息序列
  const messages = [
    createUserMessage({ content: metadata, uuid }),              // 元数据
    createUserMessage({ content: result, isMeta: true }),        // 技能内容(隐藏)
    ...attachmentMessages,                                       // 附件
    createAttachmentMessage({                                    // 权限声明
      type: 'command_permissions',
      allowedTools: additionalAllowedTools,
      model: command.model,
    }),
  ]
  return {
    messages,
    shouldQuery: true,          // prompt 命令触发模型查询
    allowedTools: additionalAllowedTools,
    model: command.model,
    effort: command.effort,
    command,
  }
}

local/local-jsx 不同,prompt 类型的 shouldQuerytrue——内容注入对话流后会触发模型响应。


八、Skill 技能系统

源码位置:src/skills/loadSkillsDir.ts

Skill(技能)是 PromptCommand 的具体实现形式,允许用户通过 Markdown 文件定义可复用的 prompt 模板。Skill 是 Command 系统与 Tool 系统的桥梁——它以 Command 的身份被用户 / 调用,也可以通过 SkillTool 被模型主动调用。

8.1 Skill 加载流程

flowchart TD
    A["getSkillDirCommands()"] --> B["并行加载 5 个来源"]

    B --> C["Managed Skills<br/>~/.claude/skills/managed/"]
    B --> D["User Skills<br/>~/.claude/skills/"]
    B --> E["Project Skills<br/>.claude/skills/ (各层)"]
    B --> F["Additional Skills<br/>additionalSkillPaths"]
    B --> G["Legacy Skills<br/>.claude/commands/ (已废弃)"]

    C --> H["parseSkillFile()"]
    D --> H
    E --> H
    F --> H
    G --> H

    H --> I["解析 Frontmatter"]
    I --> J["createSkillCommand()"]
    J --> K["去重 + 分离条件技能"]

getSkillsPath() — 路径解析

// src/skills/loadSkillsDir.ts:78-94
export function getSkillsPath(
  source: SettingSource | 'plugin',
  dir: 'skills' | 'commands',
): string {
  switch (source) {
    case 'policySettings':
      return join(getManagedFilePath(), '.claude', dir)
    case 'userSettings':
      return join(getClaudeConfigHomeDir(), dir)
    case 'projectSettings':
      return `.claude/${dir}`
    case 'plugin':
      return 'plugin'
    default:
      return ''
  }
}

8.2 Skill 文件格式 (Frontmatter)

一个完整的 Skill Markdown 文件:

---
name: Review PR
description: Review a pull request for code quality
allowed-tools: Bash, FileReadTool, GrepTool
arguments: pr_number
when_to_use: when the user asks to review a PR
model: opus
effort: high
context: fork
userInvocable: true
disable-model-invocation: false
---

Review the pull request #$ARGUMENTS and provide feedback on:
1. Code quality
2. Potential bugs
3. Performance issues

解析源码参考parseSkillFrontmatterFields(src/skills/loadSkillsDir.ts:185)

字段 类型 说明
name string UI 显示名
description string 技能描述
allowed-tools string[] 限制可用工具
arguments string[] 参数名列表
when_to_use string 模型自动调用的触发条件
model string 指定使用的模型
effort string 推理深度
context 'fork' 在子 agent 中执行
userInvocable boolean 是否可通过 / 触发
disable-model-invocation boolean 禁止模型自动调用
shell string Bash 工具使用的 shell

8.3 变量替换

Skill 内容支持以下变量:

  • $ARGUMENTS — 用户传入的参数
  • ${CLAUDE_SKILL_DIR} - skills存放路径
  • ${CLAUDE_SESSION_ID} - 当前sessionId

相关处理在src/skills/loadSkillsDir.ts: 270中,函数为createSkillCommand

8.4 条件技能 (Conditional Skills)

通过 paths 字段实现文件路径匹配的条件技能:

// src/types/command.ts:50-52
export type PromptCommand = {
  // ...
  paths?: string[]
  // ...
}

条件技能只在模型操作过匹配路径的文件后才变为可见。例如,配置 paths: ["*.test.ts"] 的测试技能只有在模型读取或编辑了测试文件后才会出现在可用技能列表中。项目级技能(projectSettings 来源)天然隔离于项目目录内。


九、SkillTool —— 模型驱动的技能调用

源码位置:src/tools/SkillTool/SkillTool.ts

SkillTool 是一个特殊的内置工具,它让模型可以主动调用用户定义的 Skill,而不仅仅是通过用户输入 / 命令触发。

9.1 工作机制

sequenceDiagram
    participant M as 模型
    participant ST as SkillTool
    participant CMD as Command System
    participant A as Agent/Inline

    M->>ST: tool_use: { skill: "review-pr", args: "123" }
    ST->>CMD: getAllCommands(context)
    CMD-->>ST: [PromptCommand, LocalCommand, ...]
    ST->>ST: validateInput — 查找匹配的 PromptCommand
    alt executionContext === 'fork'
        ST->>A: executeForkedSkill() → runAgent()
    else inline
        ST->>ST: processPromptSlashCommand 注入 prompt 到对话流
    end
    ST-->>M: ToolResult

9.2 getAllCommands() — 命令发现

// src/tools/SkillTool/SkillTool.ts:81-94
async function getAllCommands(context: ToolUseContext): Promise<Command[]> {
  // 1. 获取 MCP 提供的技能
  const mcpSkills = context.getAppState().mcp.commands.filter(
    cmd => cmd.type === 'prompt' && cmd.loadedFrom === 'mcp',
  )

  // 2. 没有 MCP 技能时直接返回本地命令
  if (mcpSkills.length === 0) {
    return getCommands(getProjectRoot())
  }

  // 3. 合并本地命令和 MCP 技能,本地优先
  const localCommands = await getCommands(getProjectRoot())
  return uniqBy([...localCommands, ...mcpSkills], 'name')
}

合并顺序是 localCommands 在前,mcpSkills 在后。uniqBy 保留第一个,所以本地命令优先于同名 MCP 技能。

9.3 validateInput — 技能存在性校验

SkillToolvalidateInput 确保:

  1. 请求的技能名称存在于可用命令列表中
  2. 目标命令是 PromptCommand 类型(非 local/local-jsx)
  3. 技能没有设置 disableModelInvocation: true

如果校验失败,返回描述性错误消息,模型可以据此调整调用。

9.4 executeForkedSkill() — 子 Agent 执行

当 Skill 的 frontmatter 设置 executionContext: 'fork' 时,技能在隔离的子 Agent 中运行:

// src/tools/SkillTool/SkillTool.ts:122-200+
async function executeForkedSkill(
  skillName: string,
  prompt: string,
  context: ToolUseContext,
  allowedTools?: string[],
  model?: string,
  effort?: string,
): Promise<ToolResult<string>> {
  // 1. 构造子 agent 的配置
  // 2. 通过 runAgent() 在独立上下文中执行
  // 3. 跟踪分析事件
  // 4. 返回子 agent 的结果
}

Fork 执行的优势:

  • 隔离性:子 Agent 有独立的上下文,不污染主对话
  • 工具限制:可以通过 allowedTools 限制子 Agent 可用的工具
  • 模型选择:可以为特定技能指定不同的模型

9.5 inline 整合回主会话

inline模式调用getMessagesForPromptSlashCommand,参考7.4节


十、系统集成场景

10.1 端到端流程:模型调用内置工具

sequenceDiagram
    participant U as 用户
    participant Q as query.ts
    participant API as Claude API
    participant STE as StreamingToolExecutor
    participant TE as toolExecution.ts
    participant T as BashTool

    U->>Q: "列出当前目录文件"
    Q->>API: messages + tools
    API-->>Q: tool_use: { name: "Bash", input: { command: "ls" } }
    Q->>STE: addTool(toolUseBlock)
    STE->>STE: canExecuteTool? → true
    STE->>TE: runToolUse()
    TE->>TE: findToolByName("Bash")
    TE->>TE: safeParse(input)
    TE->>TE: checkPermissions()
    TE->>U: 请求权限确认
    U-->>TE: 允许
    TE->>T: call({ command: "ls" })
    T-->>TE: { data: "file1.ts\nfile2.ts" }
    TE-->>STE: yield toolResultMessage
    STE-->>Q: yield 有序结果
    Q->>API: tool_result + continue
    API-->>Q: "当前目录包含 file1.ts 和 file2.ts"
    Q-->>U: 显示响应

10.2 端到端流程:MCP 工具调用

sequenceDiagram
    participant U as 用户
    participant Q as query.ts
    participant API as Claude API
    participant TE as toolExecution.ts
    participant MCP as MCP Client
    participant S as External MCP Server

    U->>Q: "获取代码诊断"
    Q->>API: messages + tools (含 mcp__ide__getDiagnostics)
    API-->>Q: tool_use: { name: "mcp__ide__getDiagnostics" }
    Q->>TE: runToolUse()
    TE->>TE: findToolByName("mcp__ide__getDiagnostics")
    Note over TE: 找到运行时覆盖的 MCPTool 实例
    TE->>TE: checkPermissions() → passthrough
    TE->>MCP: tool.call() (被 mcpClient.ts 覆盖)
    MCP->>S: MCP protocol call
    S-->>MCP: result
    MCP-->>TE: { data: "diagnostics..." }
    TE-->>Q: yield toolResultMessage
    Q->>API: tool_result + continue

10.3 端到端流程:Skill 调用

sequenceDiagram
    participant U as 用户
    participant Q as query.ts
    participant API as Claude API
    participant SK as SkillTool
    participant RA as runAgent()

    U->>Q: "review PR #42"
    Q->>API: messages + tools (含 SkillTool with skill list)
    API-->>Q: tool_use: { name: "Skill", input: { skill: "review-pr", args: "42" } }
    Q->>SK: runToolUse()
    SK->>SK: validateInput — 查找 "review-pr"
    SK->>SK: executionContext === 'fork'?
    alt fork
        SK->>RA: executeForkedSkill()
        RA-->>SK: 子 agent 结果
    else inline
        SK-->>Q: 注入 prompt 到 messages
        Q->>API: 带 skill prompt 的新请求
    end

十一、设计洞察

11.1 统一接口的力量

所有工具——无论是核心的 BashTool、外部的 MCP 工具、还是用户定义的 Skill——都实现同一个 Tool<Input, Output, P> 接口。这意味着:

  • 权限系统只需实现一次,自动应用于所有工具
  • StreamingToolExecutor 的并发控制对所有工具类型透明
  • 新增工具类型无需修改执行管道

10.2 多层权限的纵深防御

权限检查不是一个单点决策,而是一条贯穿整个执行管道的防线:

配置文件 deny 规则 → filterToolsByDenyRules(注册时过滤)
    → Zod schema 校验 → validateInput 业务校验
        → PreToolUse hooks(用户自定义拦截)
            → resolveHookPermissionDecision(综合决策)
                → canUseTool(运行时权限)
                    → 内部字段剥离(防注入)

即使某一层被绕过,后续层仍然提供保护。

10.3 并发安全的简洁模型

用一个布尔值 isConcurrencySafe 就实现了完整的并发控制:

  • 全部安全 → 全并发(如多个 GlobTool 查询)
  • 任一不安全 → 排队执行(如 FileEditTool 必须独占)
  • 兄弟中止 → Bash 失败时取消同批次工具

这比复杂的锁机制更易理解、更少出错。

10.4 Skill 系统的渐进式复杂度

Skill 系统展现了优秀的渐进式设计:

  • 最简形式:一个 Markdown 文件,内容即 prompt → 零配置
  • 中等复杂:添加 frontmatter 控制工具、模型、参数 → 声明式配置
  • 高级用法executionContext: fork 在子 Agent 中运行 → 完全隔离

用户可以从最简单的形式开始,按需增加复杂度。

OPPO Pad mini 体验:小小的很全能,塞进口袋的大手机

作者 梁梦麟
2026年4月21日 20:50

4 月 21 日,OPPO 推出了一款小尺寸平板 OPPO Pad mini,定价 元起。

和在 2025 年推出的那一波主打游戏体验的 Android 小平板不同,OPPO Pad mini 的理念回归到平板的原本,有雾面屏版本、支持手写笔的设计搭配轻便的机身,它是一个方便你阅读、笔记的随身小平板。

OPPO Pad mini 整机采用了阳极氧化喷砂 CMF 工艺的金属机身,观感和裸机触感都非常丝滑。

考虑到小平板主要的使用场景是游戏、笔记和阅读,OPPO 没有在后盖上添加用来连接保护壳、键盘的触点,后盖只保留了中心 Logo 和放在左上侧的「药丸形」DECO,横向握持时有足够的空间,不会被 DECO 挡住。

唯一的外置配件设计,就是在 DECO 同侧边缘上,用来连接 OPPO Pencil 3 Pro 的磁吸模块。

整机厚度为 5.39mm,重量为 279g(非柔光版)和 282g(柔光版),作为一款搭载 8.8 英寸屏幕和 8000mAh 大电池的设备,OPPO Pad mini 比一些手机还要轻薄。别说女生的小挂包,就算是小尺寸的收纳包,它也能够轻松放入。

如果你的口袋稍稍大一点,想随身携带也可以。

机身足够薄,侧边的按键都压缩得十分紧凑,顶部还有预留扬声器的开孔,如果底部的 USB-C 可以中置摆放,观感会更好。

另外,考虑到游戏时候的握持、以及固定状态下的走线,本身支持旁路供电的 OPPO Pad mini 如果也可以提供双 USB-C 接口,使用上会更舒服。

机身正面搭载了一块 8.8 英寸 2520 x 1680 144Hz 超清原彩高刷屏,有雾面的柔光板和常规的非柔光版可选。屏幕采用了 2.99mm 的超窄边框设计,配合雾面玻璃,整机亮屏的时候就有一种非常特别的质感。

屏幕支持最高 800 nits 手动全局亮度,全屏峰值亮度为 1600nits,HDR 峰值亮度能到 3200nits,日常在户外使用的话,屏幕也都能看清楚。

这里选择了柔光版,屏幕呈现出类纸的观感,日常用来看笔记、看文档以及电纸书,都会比常规的亮面屏要舒服不少。对于长时间需要做电子手账、笔记和阅读的用户来说,OPPO Pad mini 的舒适度还不错。

这一代,OPPO 推出了全新的 OPPO AI 手写笔 OPPO Pencil 3 Pro。

OPPO Pencil 3 Pro 支持1.6 万级压感和 0g 书写,书写的响应更快,断触几率更低。笔身支持压感,在屏幕上可以直接呼出菜单调整功能。

它也可以在熄屏、锁屏状态下点击屏幕就能够呼出笔记,方便用户在不同场合下快速记录。

另外,笔内支持定位,绑定登录之后可以在账号的「查找」功能中定位。靠近笔的时候它也会发出声音,方便你找到它。

性能部分,OPPO Pad mini 搭载了第五代高通骁龙 8 移动平台,常温状态下室内的安兔兔实测跑分为 3144963 分,内置的游戏助手可同时开启超分辨率和 HDR 光效模式,也支持经全链路触控优化的灵犀触控,提升触控的精准度。

除了游戏,OPPO Pad mini 在生产力、生态互联的配置上也很完整。

它支持和电脑版界面相同的剪映 Pro,配合 OPPO Pencil 3 Pro 可以能做到更加精细的剪辑工作。像是 OPPO 旗下标配的和手机共用网络的互联功能,和 iOS 设备跨生态互传、和 AirPods 共享音频功能,在 OPPO Pad Mini 上面也有。

这一代还实现了跨平台、跨设备电话接听,在 OPPO Pad 身上做到了接听 iPhone 的电话,iPhone 的短信和通知流转等功能,进一步方便了 iOS+ColorOS 的双生态用户。

续航方面,OPPO Pad Mini 搭载了一块 8000mAh 的电池。这容量和现在续航比较夸张的大容量电池手机差不多,放在 8 英寸规格的小平板上不算小。加上平板的持续使用压力没有手机强,OPPO Pad mini 的续航表现也还不错,平时简单打打游戏看看电视剧、看书的话,还是很够用的。

充电的话,OPPO Pad Mini 支持 67W SUPERVOOC 超级闪充,内置支持边充边玩的旁路供电,可以减轻游戏时充电产生的散热压力。

我们接入 AI 小电拼进行测试,OPPO Pad mini 最高也实现了 49W PPS 通用快充,8000mAh 大电池 0-100 充电只需要 60 分钟,20 分钟就接近一半的电量,通用快充的充电效率很高,几乎不需要为它准备单独的充电头。

最后看看售价:

  • 8GB+256GB 3699 元,国补到手价 3199 元
  • 12GB+256GB 普通版 3999 元,国补到手价 3499 元,柔光版 +500 元
  • 12GB+512GB 普通版 4499 元,国补后到手价 3999 元,柔光版 +500 元
「买吧,不贵。」

#欢迎关注爱范儿官方微信公众号:爱范儿(微信号:ifanr),更多精彩内容第一时间为您奉上。

唯杰地图CAD图层加高性能特效扩展包发布

作者 vjmap
2026年4月21日 20:46

前言

前段时间我们发布了 WebCAD 平台(vjmap.com/app/webcad/),解决了“在 Web 端打开和编辑 CAD 图纸”这件事。

这次发布 唯杰地图扩展包 vjmapext,不是重复造一个平台,而是补上另一块能力:把 CAD 绘制、编辑和高性能渲染,以插件方式接入你现有的 vjmap 项目

一句话定位

vjmapextvjmap 的 CAD 绘图扩展层,核心入口是 MapCadLayer
它不是一个独立平台,而是一个可嵌入的能力组件:你把它加到地图里,就有了 CAD 级别的图元绘制、编辑、标注、动画和扩展能力。

官方文档入口:


image-20260421201114628

WebCAD 和 vjmapext 是什么关系

可以把它理解成“平台 + 插件能力”的分工:

  • WebCAD:更像开箱即用的平台能力,适合直接在线打开和编辑图纸。
  • vjmapext:更像开发者工具箱,适合把 CAD 能力嵌入你的业务系统里,和业务流程、绘制、数据、界面一起做深度集成。

所以它们不是替代关系,而是互补关系。
如果你要“直接编辑用”,WebCAD 合适;如果你要“接入自己的系统基于vjmap开发并持续迭代”,vjmapext 更合适。


vjmapext 能做什么

从业务角度看,最常见的是下面几类能力组合。

1) 在 CAD 底图上做业务标注和交互、自动成图

你可以打开 CAD 图作为底图,然后叠加区域、点位、路径、文字和图例,把静态图变成可交互业务图。

这类能力常见于:

  • 园区设施管理;

  • 工厂设备点巡检;

  • 室内平面图上的状态展示。

  • 自动成图功能

  • 图形的绘制,编辑

    image-20260421201553891

image-20260421203845263

2) 浏览模式与编辑模式

vjmapext 支持只读浏览和编辑模式切换,便于做“查看”和“编辑”的显示不同。
例如在只读模式下仍允许关键对象可选中,用于查看属性、定位问题。
可以在编辑模式下对数据方便进行编辑,在浏览模式下对数据进行数据查看效果展示。

image-20260421201649396

image-20260421203944672

3) 动画与特效

  • CPU 动画:拖尾、弹簧、关键帧、闪烁、线段渐现
  • GPU FX:点/线预设效果,支持较大数量级渲染
  • FX 与 CAD 实体可绑定,实体移动后特效可跟随
  • 支持性能相关控制(比如更新频率、渲染策略)
  • 支持shadertoy上面的shader支持复制过来使用

如果你要做态势图、告警图、运行状态图,这块会非常好用。
可以直接参考这个示例页:
vjmap.com/app/demoext…

image-20260421201452090

image-20260421204023218

安装

依赖与环境(必读)

  • vjmapext 不能脱离 vjmap 单独使用。 必须先具备 vjmap 运行环境(地图 SDK、底图样式、服务与 vjmap.Map 等),再使用本库。
  • npm 工程:请同时安装 vjmapvjmapext(或已安装满足版本要求的 vjmap)。仅安装 vjmapext、未安装/未加载 vjmap 时无法正常工作。
  • 本包发布内容:以 package.jsonfiles 为准,一般为 dist 下的 vjmapext.min.js(UMD) 与类型声明;

npm 安装

npm install vjmapext

快速接入(最小工程骨架)

import { MapCadLayer } from "vjmapext";

const mapcad = new MapCadLayer({
  locale: "zh",
  mode: "edit",
  defaultColor: 0x7fd3ff,
});

map.addControl(mapcad);
mapcad.createUI({ theme: "dark" });

建议启动前先配置这 6 项:

  1. modeeditbrowse
  2. drawingDefaults:颜色、线宽、图层默认值;
  3. shortcuts:是否覆盖默认快捷键;
  4. 字体:有文字实体时先 loadFont()
  5. 交互:是否启用捕捉及捕捉模式;
  6. 持久化:先确定 toJSON/fromJSON 存储位置。

SDK 功能详解(含作用说明)

1)命令系统

核心 API:

  • executeCommand(name, opts?)
  • repeatLastCommand()
  • getLastCommandName()

内置命令覆盖(40+):

  • 绘图:LINEPLINEPOINTCIRCLEARCELLIPSESPLINEFREEHANDRECTPOLYDLINEREVCLOUDHATCH
  • 编辑:MOVECOPYERASEMIRROROFFSETSCALESTRETCHBREAKTRIMEXTENDFILLETARRAYEXPLODEDRAWORDER
  • 标注:DIMLINEARDIMALIGNEDDIMANGLEDIMRADIUSMLEADER
  • 文字:TEXTTEXTEDITMTEXT
  • 块与导入:BLOCKINSERTIMPORTSVG

作用描述(详细):

  • 把 CAD 操作抽象成统一命令后,业务系统只需绑定命令,不需要重复造编辑逻辑;
  • 多模式命令(如 CIRCLEARC)可通过关键字切换子流程,减少多命令拆分维护成本;
  • 命令可以统一挂到工具栏、右键菜单、快捷键与业务流程引导页面;
  • 命令执行链可接日志,形成“用户操作轨迹”。

逐命令功能说明(精简版):

  • 绘图命令

  • LINE:按点创建直线段,适合轴线、连线、边界基础绘制。

  • PLINE:连续多段线,支持闭合和回退点,适合轮廓线与路径线。

  • POINT:创建点实体,常用于定位点、控制点、设备锚点。

  • CIRCLE:圆绘制,支持圆心半径、直径、两点、三点、切线等模式。

  • ARC:圆弧绘制,支持多构造方式,适合弧形边界和连接段。

  • ELLIPSE:椭圆绘制,支持中心法/轴端点法,常用于设备包络或符号。

  • SPLINE:样条曲线,适合平滑边界、自由曲线表达。

  • FREEHAND:自由手绘,适合快速草绘与现场标记。

  • RECTPOLY:矩形/多边形绘制,适合区域框选、面状边界初稿。

  • ARROW:箭头绘制,用于流程方向、流向标识。

  • HATCH:填充封闭区域,适合功能分区、材质区、风险区高亮。

  • TOHATCH:将封闭图形转换为填充对象,便于后处理。

  • DLINE:双线绘制,适合道路、墙体、管廊边界等平行线对象。

  • REVCLOUD:修订云线,常用于审图圈改、问题标注。

  • 文字命令

  • TEXT:单行文字,适合点位名、编号、简短说明。

  • TEXTEDIT:编辑既有文字内容,适合在线修正文案。

  • MTEXT:多行文字,适合批注段落、说明块。

  • 标注命令

  • DIMLINEAR:线性标注,输出水平或垂直尺寸。

  • DIMALIGNED:对齐标注,沿对象方向标注真实长度。

  • DIMANGLE:角度标注,适合角点控制与转角校核。

  • DIMRADIUS:半径标注,适合圆/弧尺寸表达。

  • MLEADER:多重引线,适合复杂构件说明与指向标注。

  • 编辑命令

  • MOVE:整体平移对象到新位置。

  • COPY:复制对象,适合重复布置。

  • ERASE:删除对象。

  • MIRROR:镜像对象,适合对称图形快速生成。

  • OFFSET:平行偏移对象,适合生成内外边界。

  • SCALE:按比例缩放对象。

  • STRETCH:局部拉伸对象几何。

  • BREAK:打断对象,生成断开段。

  • TRIM:按边界修剪超出部分。

  • EXTEND:按边界延伸对象到交界处。

  • FILLET:圆角连接两对象,生成平滑转角。

  • ARRAY:阵列复制,适合规则分布对象。

  • EXPLODE:分解复合对象(块、多段线等)为基础实体。

  • DRAWORDER:调整前后绘制顺序,控制遮挡与可见层次。

  • 块与导入命令

  • BLOCK:将一组对象定义为块,便于复用和规范化管理。

  • INSERT:插入块引用,支持重复放置。

  • PASTECLIP:粘贴剪贴板对象,提升跨区域编辑效率。

  • IMPORTSVG:导入 SVG 并转为可编辑对象,便于外部图标/图形接入。

命令使用建议:

  • 前台工具栏通常优先暴露 LINE/PLINE/CIRCLE/MOVE/COPY/ERASE/TRIM/EXTEND/DIMLINEAR/TEXT
  • 审图类页面建议增加 REVCLOUD/MLEADER/TEXTEDIT
  • 模板化制图建议优先启用 BLOCK/INSERT/ARRAY
  • 若是存量图改造,命令层要与 MapData + Hider + exportDwg 一起设计。

示例:


2)输入系统(InputManager)

输入能力:

  • 点输入(坐标点采集)
  • 选集输入(单选、框选、多选)
  • 数值输入(长度、半径等)
  • 关键字输入(命令子模式切换)
  • 字符串输入(文字命令等)

作用描述(详细):

  • 输入统一后,所有命令交互行为一致,降低用户学习成本;
  • 对开发者来说,命令只管业务逻辑,输入边界(取消、确认、回退)交给统一系统处理;
  • 输入与预览联动后,用户在确认前就能看到结果,减少误提交;
  • 是“可编辑能力稳定性”的底座。

示例:


3)对象捕捉(Snap)与夹点编辑(Grip)

捕捉能力:

  • 端点、中点、圆心、交点、最近点等模式;
  • 支持模式组合与开关控制;
  • 在命令点输入阶段实时生效。

夹点能力:

  • 选中实体后显示可编辑夹点;
  • 拖拽夹点修改几何;
  • 可与撤销重做联动。

作用描述(详细):

  • 捕捉解决“线上操作精度不足”问题;
  • 夹点解决“局部改图要重画”问题;
  • 两者配合,能在网页端做可用的精修工作流,而不是仅展示级编辑。

示例:


4)实体存储与选择管理

核心 API:

  • addEntity(entity)
  • deleteEntity(id)
  • getEntities()
  • getSelectedEntities()
  • clearSelection()

作用描述(详细):

  • 实体层统一管理后,渲染层、属性面板、事件系统都能共享同一数据源;
  • 选择集明确后,编辑命令可避免“误改全部对象”;
  • 可在业务系统里按选择集做批处理(改颜色、改图层、改属性);
  • 是批量编辑、批量审查、批量导出的前置基础。

示例:


5)撤销重做(Undo/Redo)

核心 API:

  • undo()
  • redo()

作用描述(详细):

  • 在线编辑可回退,用户才敢进行复杂操作;
  • 支持和快捷键联动,操作习惯接近桌面 CAD;
  • 可用于审图流程中的“试改-对比-还原”。

示例:


6)标注体系

相关命令:

  • DIMLINEAR
  • DIMALIGNED
  • DIMANGLE
  • DIMRADIUS
  • MLEADER

作用描述(详细):

  • 标注能力决定图纸可审核性,不只是视觉增强;
  • 统一标注命令可把尺寸、角度、说明纳入标准编辑流;
  • 对工程协同来说,标注是交底、复核、验收的核心数据表达。

示例:


7)文字与字体管理

相关能力:

  • 命令:TEXTTEXTEDITMTEXT
  • API:loadFont(url, name?)

作用描述(详细):

  • 文字是图纸语义信息的重要组成;
  • 字体加载可避免线上渲染错位或替换字体导致排版变化;
  • 文字编辑能力可直接承接审图意见修订流程。

示例:


8)块(Block)能力

相关能力:

  • 命令:BLOCKINSERT
  • 数据:块定义、块引用
  • 序列化:块信息可随文档保存恢复

作用描述(详细):

  • 块能力是减少重复绘制和统一规范的核心;
  • 适合设备符号、标准构件、图例模板等复用对象;
  • 可建立企业标准块库,提升制图一致性。

示例:


9)序列化与绘图默认值

核心 API:

  • toJSON()
  • fromJSON(doc)
  • setDrawingDefaults(partial)
  • getDrawingDefaults()

作用描述(详细):

  • 支持“保存当前进度 -> 跨会话继续编辑”;
  • 支持“模板化初始化图纸”;
  • 支持团队统一绘图规范(图层、线宽、颜色);
  • 是多人协作和版本回放的基础。

10)MapData 数据联动

核心 API:

  • queryMapEntities(opts)
  • queryMapEntitiesByLayer(layer, entType, extra?)
  • featuresToEntities(featureCollection, opts?)
  • createMapDataHider()

标准链路:

  1. 按条件查询后端 DWG 实体;
  2. Feature 转 SDK 实体;
  3. 隐藏原图被接管对象;
  4. 前端叠加编辑;
  5. 最终导出。

作用描述(详细):

  • 不需要一次性迁移历史图纸;
  • 可在原图基础上做增量改造;
  • 可把“后端存量数据”接入“前端可编辑流程”;
  • 适合传统项目数字化升级。

示例:


11)DWG 导出交付

核心 API:

  • exportDwg(opts)
  • setExportDwgCallback(cb)

常见组合:

  • exportDwg({ hider })
  • onBeforeUpdate 导出前加工
  • deleteFromSource 导出时清理源对象

作用描述(详细):

  • 打通“在线编辑 -> DWG 文件交付”;
  • 保持与传统 CAD 工具链衔接;
  • 减少人工二次整理步骤。

12)渲染与性能机制

可核验机制:

  • 三源分桶:hot/cold/dynamic
  • 增量更新与脏标记刷新
  • 渲染缓存复用
  • styleOnly 样式快路径

作用描述(详细):

  • 高频操作时减少全量刷新;
  • 大图场景下更稳定;
  • 只改样式时避免几何重建;
  • 给后续性能调优提供结构基础。

13)FX 特效层

能力点:

  • 批量添加特效对象;
  • 质量档位调节;
  • 指标与事件输出;
  • CAD 实体绑定。

作用描述(详细):

  • 可用于状态表达(告警、流向、活跃度);
  • 可根据设备性能动态降级;
  • 可通过指标事件接入监控系统;
  • 是“可编辑图纸 + 运行态表达”组合能力的关键层。

示例:

14)UI、事件与扩展能力

相关能力:

  • createUI(options) / getUI()
  • eventBus 事件总线
  • loadPlugin(plugin) 与插件生命周期

作用描述(详细):

  • UI 能力让你快速构建可用工作台;
  • 事件体系让 CAD 编辑流程可接入业务日志、审批、统计;
  • 插件机制支持“先上线核心,再按模块扩展”;
  • 有利于长期维护与团队协作开发。

示例:


一季度广东实现地区生产总值同比增长4.6%

2026年4月21日 20:34
今天(4月21日),广东省统计局公布一季度经济运行简况。根据地区生产总值统一核算结果,一季度,广东实现地区生产总值34950.34亿元,按不变价格计算,同比增长4.6%。其中,规模以上工业增加值同比增长5.4%,增速比2025年全年提高2.4个百分点;服务业增加值同比增长4.5%;社会消费品零售总额同比增长2.5%,增速高于全国。此外,固定资产投资同比增长0.2%,连续两个月保持正增长;除房地产开发,全省固定资产投资增长5.8%。(央视新闻)
❌
❌