news 2026/9/11 12:27:19

Sentry 前端设计系统 ESLint 规则开发指南:基于 eslintPluginScraps 的规则创建、测试与自动修复全流程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Sentry 前端设计系统 ESLint 规则开发指南:基于 eslintPluginScraps 的规则创建、测试与自动修复全流程

Sentry 前端设计系统 ESLint 规则开发指南:基于 eslintPluginScraps 的规则创建、测试与自动修复全流程

【免费下载链接】sentryDeveloper-first error tracking and performance monitoring项目地址: https://gitcode.com/GitHub_Trending/sen/sentry

导读

本文是一份面向 Sentry 开源仓库前端团队的实战指南,围绕仓库内lint-new技能文档(.agents/skills/lint-new/SKILL.md)及其三份参考文档(rule-archetypes、schema-patterns、style-collector-guide)展开,系统讲解如何在eslintPluginScraps插件中从零创建一个带测试、可注册、支持自动修复(autofix)的 ESLint 规则。读者将掌握:如何按意图选择合适的规则原型(Archetype)、如何复用插件内置的 AST 分析工具、如何编写规则与测试骨架、何时实现 autofix、以及如何完成规则注册与配置驱动的规则扩展,从而为 Sentry 的设计系统(@sentry/scraps)维护一套可持续演进的样式与结构约束。

一、背景:eslintPluginScraps 是什么

在 Sentry 仓库中,前端采用 Emotion(CSS-in-JS)进行样式开发,并逐步迁移到@sentry/scraps核心组件与语义化 token 体系(参见 static/AGENTS.md 中的 Design System 规范:布局用Flex/Grid/Stack/Container,排版用Text/Heading,禁用手写display: flex/grid等)。

eslintPluginScraps就是承载这些约束的 ESLint 插件,位于 static/oxlint/eslintPluginScraps/。从规则索引 static/oxlint/eslintPluginScraps/src/rules/index.ts 可以看到当前已内置 7 条规则:

规则名职责方向
no-core-import禁止从sentry/components/core导入,自动改写为@sentry/scraps
no-double-dollar-interpolation模板字符串中的双重$$插值检测
no-token-import禁止直接导入 token
prefer-info-textInfoTip/InfoText替代裸Tooltip
prefer-stack-for-column-flex优先用Stack替代display: flex列布局
restrict-jsx-slot-children限制特定 prop 槽位中允许出现的 JSX 元素
use-semantic-token强制theme.tokens.*只能配合语义匹配的 CSS 属性使用

lint-new技能描述明确其适用场景:"create a lint rule"、"add an eslint rule"、"scaffold a rule"、"write a new scraps rule"、"new design system lint rule",即当开发者需要为设计系统新增一条约束时,按本文档流程操作。

二、规则开发工作流总览

SKILL.md给出的核心指令是:在eslintPluginScraps插件中创建一条名为$ARGUMENTS的新 ESLint 规则。完整流程分四步,外加 autofix、扩展、命名三个专项主题:

  1. 选择规则原型(Archetype)——决定 AST 访问策略;
  2. 检查共享工具——复用插件内置的 AST 工具,避免重复造轮子;
  3. 创建规则文件与测试——套用模板生成规则.ts规则.spec.ts
  4. 注册规则——加入规则索引与 ESLint 配置并验证。

下面逐节展开。

三、第一步:选择规则原型(Archetype)

在编写任何 AST 遍历逻辑之前,先阅读 references/rule-archetypes.md,根据规则意图匹配对应的原型。该文档提供了一张决策表:

你想做的事原型(Archetype)关键模式示例规则
重写导入路径Import rewriteImportDeclaration访问器 +fixer.replaceText(node.source, ...)no-core-import
校验 token/值可用于哪些 CSS 属性Property validationcreateStyleCollector+Program:exit延迟校验use-semantic-token
限制特定 prop 中的 JSX 元素JSX structural导入追踪 + 递归 JSX 树遍历 + 配置 schemarestrict-jsx-slot-children
检测静态 CSS 文本中的模式Template text analysisTaggedTemplateExpression→ 遍历quasi.quasis静态文本no-dom-coupling

文档强调:每种原型决定了应使用哪些 AST 访问器、哪些共享工具适用,以及哪些模式不适用于该方案。写代码前必须读完对应参考。

原型 1:Import Rewrite(导入重写)

适用场景:规则检查 import 来源并重写之。模式:单个ImportDeclaration访问器,autofix 替换 source 字符串:

create(context) { return { ImportDeclaration(node) { const importPath = node.source.value; if (typeof importPath === 'string' && importPath.startsWith(FORBIDDEN)) { context.report({ node, messageId: '...', fix(fixer) { return fixer.replaceText(node.source, `'${newPath}'`); }, }); } }, }; }

Autofix 安全性:几乎总是安全的——修复只是修改一个字符串字面量。边界情况:类型导入(import type)、混合命名导入、re-export 都被自动处理,因为只替换 source 路径字符串。

仓库中的规范实现是 noCoreImport.ts(SKILL 文档中称为no-core-import.ts,实际仓库文件名采用 camelCase)。它定义FORBIDDEN_PATH = 'sentry/components/core/'REPLACEMENT_PATH = '@sentry/scraps',在fix中还会解析子路径并保留一级组件名(importPath.split('/')[3]),将sentry/components/core/foo改写为@sentry/scraps/foo,说明导入重写不仅限于"改前缀",还可以做路径结构的映射。

原型 2:Property Validation(属性校验,基于 Style Collector)

适用场景:校验动态值(theme token、变量)被用在哪些 CSS 属性上。核心思路两阶段法——遍历时收集,遍历后校验

  1. createStyleCollector(context)返回{collector, visitors},把visitors展开进规则的返回对象;
  2. Program:exit中遍历collector.getAll()校验每个StyleDeclaration
  3. 结束时调用collector.clear()做清理。
create(context) { if (!shouldAnalyze(context)) return {}; // 快速退出 const {collector, visitors} = createStyleCollector(context); return { ...visitors, 'Program:exit'() { for (const decl of collector.getAll()) { // decl.property.name — CSS 属性(已归一化) // decl.values — 数组 {rawNode, tokenInfo: {tokenPath, node}} validateDeclaration(decl); } collector.clear(); }, }; }

关键要点

  • collector 只处理模板字面量中的插值表达式${...}部分),不分析quasis 中的静态 CSS 文本。若需检测静态文本本身的模式(如裸十六进制颜色、嵌套选择器),应改用原型 4。
  • 配置驱动规则:若校验规则随类别变化,把映射放进src/config/并从那里加载(参见tokenRules.ts),这样新增类别无需改动规则逻辑。
  • shouldAnalyze:始终作为快速预扫描的退出条件。它通过正则检查 Emotion 导入/使用模式,跳过明显不使用 styled-components 的文件。

仓库中的规范实现是 useSemanticToken.ts:在create()首行调用shouldAnalyze(context)快速退出;通过createStyleCollector收集后,在Program:exit中对每条声明做校验:跳过--开头的 CSS 自定义属性,通过findRuleForToken(tokenPath)找到 token 所属规则,若属性不在该规则的allowedProperties内,则依据PROPERTY_TO_RULE反向映射给出带建议类别的错误消息(invalidPropertyWithSuggestion),否则给出invalidProperty

原型 3:JSX Structural Constraint(JSX 结构约束)

适用场景:限制特定 prop 或槽位中可以出现哪些 JSX 元素。模式:用createImportTracker(来自 src/ast/tracker/imports.ts)做导入解析,配合JSXAttribute访问器做树遍历:

  1. createImportTracker()—— 合并其visitors,再用resolve(localName)findLocalNames(source, name)检查导入;
  2. JSXAttribute—— 发现配置的 prop 后,递归遍历 JSX 树,逐个检查元素是否属于允许集合。
create(context) { const importTracker = createImportTracker(); return { ...importTracker.visitors, JSXAttribute(node) { // 用 importTracker.resolve(displayName) 检查元素来源 // 用 importTracker.findLocalNames(source, name) 查找本地别名 }, }; }

关键模式

  • 处理导入别名:import {Foo as Bar}Bar是本地名——importTracker.resolve('Bar')返回{source, imported: 'Foo'}
  • 处理成员表达式:MenuComponents.Alert必须匹配${localName}.${member}
  • 递归遍历:直接 JSX 子节点、三元表达式、逻辑表达式(&&||??)、JSXExpressionContainerJSXFragment、箭头函数表达式体;
  • 跳过React.Fragment/<Fragment>(透明包装器);
  • 不允许的元素处停止递归(上报后返回)。

Schema:使用带嵌套数组的复杂 options schema,完整模式参考restrict-jsx-slot-children规则。Autofix:一般不安全——替换 JSX 元素需要理解组件 API,超出了 AST 单独能提供的信息。

原型 4:Template Text Analysis(模板文本分析)

适用场景:检测模板字面量静态 CSS 文本中的模式(而非插值表达式)。模式:使用createQuasiScanner(SKILL 文档指向 src/ast/scanner/index.ts),它内部替你处理了shouldAnalyze退出、通过getStyledCallInfo做标签检测、以及 quasi 迭代:

import {createQuasiScanner} from '../ast/scanner/index'; create(context) { return createQuasiScanner(context, (cssText, quasi, info) => { // cssText: 该 quasi 段的静态 CSS 文本 // quasi: TemplateElement 节点(用于错误上报) // info: { kind: 'element' | 'component' | 'css', name?: string } for (const match of cssText.matchAll(MY_PATTERN)) { context.report({ node: quasi, messageId: '...' }); } }); }

扫描器会对文件中每个 styled/css 标签模板的每个 quasi 元素调用analyze回调,并自动跳过没有 Emotion 使用的文件。

原型 2 vs 原型 4 的选择:若在 CSS文本本身中找模式(原始颜色、嵌套选择器、属性名),用createQuasiScanner;若校验通过插值(${theme.tokens.X})传给 CSS 属性的,用createStyleCollector

标签检测工具getStyledCallInfo(node)(来自 src/ast/utils/styled.ts)把任意TaggedTemplateExpressionCallExpression分类为{kind: 'element', name}{kind: 'component', name}{kind: 'css'}null,覆盖styled.divstyled('div')styled(Component)styled(Component).attrs(...)css模式。扫描器内部使用它,也可在自定义访问器中直接调用。

需要说明的是,当前仓库ast/目录实际包含extractor/tracker/utils/三个子目录(见 static/oxlint/eslintPluginScraps/src/ast/),scanner/目录与no-dom-coupling规则在本仓库快照中尚未出现,SKILL 文档中的描述可视为该原型的既定设计目标,实现落地时以实际代码为准。

四、第二步:检查共享 AST 工具

在编写 AST 遍历逻辑之前,先检查 src/ast/ 下是否有可复用代码。SKILL.md给出了完整工具清单:

工具位置用途
getStyledCallInfosrc/ast/utils/styled.ts将 styled/css 调用分类为 element、component 或 css
createQuasiScannersrc/ast/scanner/index.ts扫描模板字面量中的静态 CSS 文本(原型 4)
createImportTrackersrc/ast/tracker/imports.ts解析本地名称从何处导入
createStyleCollectorsrc/ast/extractor/index.ts收集 CSS-in-JS动态值声明(非静态文本)
shouldAnalyzesrc/ast/extractor/index.ts快速预扫描,跳过无 Emotion 使用的文件
normalizePropertyNamesrc/ast/utils/normalizePropertyName.ts归一化 CSS 属性名
decomposeValuesrc/ast/extractor/value-decomposer.ts把复杂表达式分解为所有可能的值
Theme trackersrc/ast/tracker/theme.ts追踪useTheme()与回调式 theme 绑定

复用原则:如果已有规则解决了类似问题,把共享逻辑提取到src/ast/utils/并复用,而不是复制粘贴。

从源码看,extractor/index.ts 是这套工具的核心聚合点:createStyleCollector内部先创建createThemeTracker()(extractor 依赖它),再创建createStyledExtractor()(处理styled.div\...`styled(X)`...`)、createCssPropExtractor()(处理css={}css`...`prop)、createStylePropExtractor()(处理style={{}}prop),最后通过mergeVisitors合并四份 visitor 并返回{collector, visitors, themeTracker}shouldAnalyze的正则预扫描同时检查@emotion/styled@emotion/react导入与useThemestyled[.(、`` css[({] ``、css=/style=等使用模式。

五、第三步:创建规则文件与测试

文件清单

  1. 规则文件static/oxlint/eslintPluginScraps/src/rules/$ARGUMENTS.ts
  2. 测试文件static/oxlint/eslintPluginScraps/src/rules/$ARGUMENTS.spec.ts

规则模板

import {ESLintUtils} from '@typescript-eslint/utils'; export const $RULE_NAME = ESLintUtils.RuleCreator.withoutDocs({ meta: { type: 'problem', docs: { description: '[Rule description]', }, fixable: 'code', // 若规则有 autofix 则包含——参见 Autofix 指导 schema: [], messages: { forbidden: 'Error message shown to user', }, }, create(context) { return { // AST 访问器方法——参见所选原型 }; }, });

如果规则需要可配置选项,加载 references/schema-patterns.md(详见下文第八节)。

测试模板

import {RuleTester} from '@typescript-eslint/rule-tester'; import {$RULE_NAME} from './$ARGUMENTS'; const ruleTester = new RuleTester(); ruleTester.run('$ARGUMENTS', $RULE_NAME, { valid: [ { code: '// valid code', filename: '/project/src/file.tsx', }, ], invalid: [ { code: '// invalid code', filename: '/project/src/file.tsx', errors: [{messageId: 'forbidden'}], output: '// expected output after autofix', // 可修复规则必填 }, ], });

运行测试

pnpm test-ci "static/oxlint/eslintPluginScraps/src/rules/$ARGUMENTS.spec.ts"

仓库既有规则的测试采用与规则同名同目录的.spec.ts文件组织(如 noCoreImport.spec.ts、useSemanticToken.spec.ts),AST 工具层也有独立测试(如 imports.spec.ts、styled.spec.ts),可对照学习。

六、Autofix 实现指南

默认立场:实现 autofix,除非转换存在歧义或可能改变运行时行为。

安全的 autofix 模式

  • 导入路径重写(以no-core-import.ts为规范示例);
  • 添加/移除已知值的 JSX 属性;
  • 用已知组件包裹表达式;
  • 无遮蔽风险的标识符重命名。

禁止 autofix 的场景

  • 存在多个合理修复方案、正确选择需要人工判断;
  • 修复需要 AST 无法提供的类型信息;
  • 转换会改变控制流或运行时行为;
  • 修改跨越多个文件。

Fixer API

context.report({ node, messageId: 'forbidden', fix(fixer) { return fixer.replaceText(node, newText); // 还有:fixer.replaceTextRange([start, end], text) // fixer.insertTextBefore(node, text) // fixer.insertTextAfter(node, text) // fixer.remove(node) // 返回单个修复或修复数组 }, });

铁律:规则一旦可修复,每个 invalid 测试用例都必须包含output,声明修复后的期望代码。仓库中的noCoreImport正是这样做的:meta.fixable: 'code',并在fix回调里用fixer.replaceText(node.source, ...)改写导入源字符串。

七、第四步:注册规则

1. 规则索引

在 src/rules/index.ts 中注册:

import {$RULE_NAME} from './$ARGUMENTS'; export const rules = { // existing rules... $ARGUMENTS: $RULE_NAME, };

对照现有索引可以看到,插件采用"kebab-case 注册名 → camelCase 导出"的映射方式(如'no-core-import': noCoreImport'use-semantic-token': useSemanticToken),新增规则照此模式追加即可。

2. ESLint 配置

eslint.config.tsname: 'plugin/@sentry/scraps'配置块中添加:

'@sentry/scraps/$ARGUMENTS': 'error', // 或带选项: '@sentry/scraps/$ARGUMENTS': ['error', { /* options */ }],

3. 验证

pnpm test-ci "static/oxlint/eslintPluginScraps/src/rules/$ARGUMENTS.spec.ts"

八、规则选项 Schema 模式

当规则需要可配置选项时,按 references/schema-patterns.md 选择匹配的 schema 形态。

无选项(默认)

大多数规则无需选项,使用空 schema。注意类型参数用never[]而非[]——[]会违反@typescript-eslint/no-restricted-types

ESLintUtils.RuleCreator.withoutDocs<never[], MessageIds>({ meta: { schema: [] }, defaultOptions: [], create(context) { ... }, });

简单选项:字符串数组

适合"可配置启用特性集合"的规则:

interface Options { enabledCategories?: string[]; } ESLintUtils.RuleCreator.withoutDocs<[Options], MessageIds>({ meta: { schema: [{ type: 'object', properties: { enabledCategories: { type: 'array', items: { type: 'string' }, }, }, additionalProperties: false, }], }, defaultOptions: [{}], create(context, [options = {}]) { const enabled = options.enabledCategories ? new Set(options.enabledCategories) : null; // null = 全部启用 ... }, });

配置写法:'@sentry/scraps/rule-name': ['error', {enabledCategories: ['background', 'border']}]

这正是use-semantic-token采用的形式:enabledCategoriesnull时全部启用,否则仅校验集合内的类别(见 useSemanticToken.ts 中的isCategoryEnabled)。

复杂选项:嵌套配置

适合槽位限制这类结构化配置:

interface Options { slots: Array<{ propNames: [string, ...string[]]; allowed: Array<{ source: string; names: [string, ...string[]]; }>; componentNames?: string[]; }>; }

Schema 与 TypeScript 接口一一对应(propNamesallowed[].sourceallowed[].namescomponentNames均需minItems: 1,对象用required+additionalProperties: false收紧)。

配置驱动规则(Config-Driven)

当校验逻辑通用、只有数据随类别变化时,把配置拆到独立文件:

src/config/tokenRules.ts ← 类别定义、属性映射 src/rules/use-semantic-token.ts ← 通用校验逻辑

这样新增类别只需改配置、不动规则逻辑。仓库中的 tokenRules.ts 正是如此:它是 token 检测关键词、属性校验白名单、autofix 建议反向映射的单一事实来源(SINGLE SOURCE OF TRUTH),并采用"最具体者胜"的匹配策略——当多个关键词命中时,路径中最深/最后的关键词胜出(如interactive.border.content命中 content 规则)。

反向映射模式(Reverse Mapping)

配置是"类别 → 允许属性"时,通常还需要反向的"属性 → 期望类别"用于错误消息:

function buildPropertyToRule(rules: TokenRule[]) { const result = new Map<string, string>(); for (const rule of rules) { for (const property of rule.allowedProperties) { result.set(property, rule.name); // Last writer wins } } return result; }

顺序很重要:当多个类别共享同一属性(如box-shadow同时属于focusshadow),数组中最后一个类别赢得反向映射,这决定了错误消息中建议哪个类别。新增类别时务必注意此副作用。

九、扩展现有规则

如果修改现有规则而非新建,按以下步骤:

  1. 先读现有规则及其配置文件,理解架构;
  2. 配置驱动规则(如use-semantic-token):改动通常只需编辑配置文件(如src/config/tokenRules.ts),无需动规则逻辑;
  3. 警惕反向映射副作用——新增类别可能改变共享属性建议的类别(buildPropertyToRule中"后写者胜");
  4. 为任何行为变更更新现有测试,然后补充新测试用例。

十、命名规范

SKILL.md明确了命名三件套:

  • 规则名(kebab-case):my-rule-name,采用"动词-名词"模式(如no-token-importuse-semantic-token);
  • 导出名(camelCase):myRuleName
  • 文件名:与规则名完全一致(my-rule-name.tsmy-rule-name.spec.ts)。

需要说明的是,当前仓库既有的 7 条规则实际采用了 camelCase 文件名(如noCoreImport.tsuseSemanticToken.ts),但注册名仍为 kebab-case。新增规则时建议遵循 SKILL 文档规范,并保持与rules/index.ts注册映射的一致性。

十一、Style Collector 深入指南

references/style-collector-guide.md 是原型 2 的配套深度文档。

适用边界

使用createStyleCollector:校验 theme token 用于哪些 CSS 属性、检查插值值是否符合期望类型/类别、分析 CSS 属性与动态值的关系。不要使用它来:检测静态 CSS 文本模式(改用createQuasiScanner)、检查导入路径(用ImportDeclaration访问器)、限制 JSX 元素用法(用 JSX 树遍历 +createImportTracker)。

架构

File: src/ast/extractor/index.ts createStyleCollector(context) ├── createThemeTracker() ← 追踪 useTheme() / 回调式 theme 绑定 ├── createStyledExtractor() ← 处理 styled.div`...` 和 styled(X)`...` ├── createCssPropExtractor() ← 处理 css={} 和 css`...` prop └── createStylePropExtractor() ← 处理 style={{}} prop 返回: { collector, visitors, themeTracker }

Collector 捕获的内容

collector.getAll()中的每条StyleDeclaration

interface StyleDeclaration { property: { name: string; // 归一化后的 CSS 属性(如 'background-color') node: TSESTree.Node; // 属性名 AST 节点 }; values: Array<{ rawNode: TSESTree.Node; tokenInfo?: { tokenPath: string; // 如 'content.primary'、'border.secondary' node: TSESTree.Node; // token 访问的 AST 节点 }; }>; }

Collector 不捕获的内容

  • 模板字面量 quasis 中的静态文本(非插值部分);
  • 只出现在静态文本中、无动态值的 CSS 属性名;
  • 注释、空白、格式。

为何延迟校验

一个 styled 块中,属性与值可能分布在多个 AST 节点上(template quasis + expressions),collector 先聚合全部信息,再在Program:exit校验完整图景。collector.clear()必调——为下一个文件做清理。

常见陷阱:Collector 与静态文本

头号错误:需要分析静态 CSS 文本时却用了createStyleCollector

const Box = styled.div` color: #ff0000; ← 这是 quasi 中的静态文本——collector 看不到 background: ${p => p.theme.tokens.background.primary}; ← 这个会被捕获 `;

检测原始十六进制颜色、嵌套选择器等文本本身的模式时,改用createQuasiScanner(见 rule-archetypes.md 的 "Template Text Analysis" 原型)。

十二、实战流程速查

把以上内容压缩为一份可执行清单:

  1. 定意图→ 查决策表选 Archetype(1 导入重写 / 2 属性校验 / 3 JSX 结构 / 4 模板文本);
  2. 查工具→ 遍历src/ast/确认可复用工具,有相似规则则提取共享逻辑;
  3. 建文件→ 用规则模板与测试模板创建rules/$name.tsrules/$name.spec.ts,可配置则按 schema-patterns 三选一;
  4. 写 autofix→ 默认实现,遵守安全/禁止清单,invalid 用例必填output
  5. 注册rules/index.ts添加导出,eslint.config.tsplugin/@sentry/scraps块中置为'error'
  6. 验证pnpm test-ci "static/oxlint/eslintPluginScraps/src/rules/$name.spec.ts"
  7. 维护→ 配置驱动规则优先改src/config/,留意反向映射"后写者胜"副作用,同步更新测试。

遵循这套流程,开发者即可为 Sentry 设计系统持续沉淀新的 lint 约束,将设计规范转化为可自动化执行的工程质量防线。

【免费下载链接】sentryDeveloper-first error tracking and performance monitoring项目地址: https://gitcode.com/GitHub_Trending/sen/sentry

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 12:26:38

孩子为何总想逃离AI玩伴?陪伴机器人亟需一场“去教育化”变革

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 12:25:58

EMD-KPCA-LSTM组合模型:MATLAB下多维时间序列预测实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 12:25:39

context-mode实战:为终端问答助手构建四层上下文管理机制

聊一个最近在多个项目里反复踩坑又反复受益的话题&#xff1a;context-mode。简单说&#xff0c;它就是一种“带着上下文去处理请求”的模式。如果你在做 AI 应用、命令行工具、自动化脚本&#xff0c;或者任何涉及多轮交互的软件&#xff0c;你迟早会遇到这个问题&#xff1a;…

作者头像 李华
网站建设 2026/9/11 12:23:58

专科生AI时代工具选择指南:8款实测推荐

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华