跳到主要内容

通过 AI SDK 发送自定义数据和元数据

· 阅读需 3 分钟
1adybug
子虚伊人

Vercel 的 AI SDK 很方便,把前后端的代码都封装起来了,但是有一点不好,就是文档写得不够详细,只介绍了一些初级用法,稍微复杂一点的用法就需要自己去探索。

UIMessage

UIMessage 是 AI SDK 中的消息类型,是最核心的内容。

/**
The data types that can be used in the UI message for the UI message data parts.
*/
type UIDataTypes = Record<string, unknown>

type UITools = Record<string, UITool>

/**
AI SDK UI Messages. They are used in the client and to communicate between the frontend and the API routes.
*/
interface UIMessage<METADATA = unknown, DATA_PARTS extends UIDataTypes = UIDataTypes, TOOLS extends UITools = UITools> {
/**
A unique identifier for the message.
*/
id: string
/**
The role of the message.
*/
role: "system" | "user" | "assistant"
/**
The metadata of the message.
*/
metadata?: METADATA
/**
The parts of the message. Use this for rendering the message in the UI.

System messages should be avoided (set the system prompt on the server instead).
They can have text parts.

User messages can have text parts and file parts.

Assistant messages can have text, reasoning, tool invocation, and file parts.
*/
parts: Array<UIMessagePart<DATA_PARTS, TOOLS>>
}

UIMessage 接受三个泛型参数:

  • METADATA:消息的元数据
  • DATA_PARTS:消息的数据部分
  • TOOLS:消息的工具

以下使我们创建的一个定义消息类型 MyUIMessage

export interface Metadata {
conversationId: string
messageId: string
modelIdSnapshot: string
usedTokens: number
}

// 由于 extends Record<string, unknown> 的限制,DataParts 不能是接口,必须是类型
export type DataParts = {
timestamp: number
}

export type MyUIMessage = UIMessage<Metadata, DataParts>

METADATA

metadata 就是消息的元数据,每一条消息(目前来看只有响应的消息)都可以有 metadata 属性,metadata 是附加在消息级别的自定义信息,用于描述消息本身的属性,而不是消息内容的一部分,我们可以使用 metadata 来传递一些额外的信息,比如当前的会话 id

特点:

  • 附加在 message.metadata 对象上
  • 通过 toUIMessageStreamResponse 中的 messageMetadata 回调发送
  • 描述整个消息的属性信息
  • 持久化在消息历史中

适用场景:

  • 适合存储时间戳、模型信息、token 使用量、用户上下文等消息级别的描述性信息

使用实例:

// 服务端
return result.toUIMessageStreamResponse<MyUIMessage>({
messageMetadata: ({ part }) => {
if (part.type === "start") {
return {
conversationId: "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11",
messageId: "6ec0bd7f-11c0-43da-975e-2a8ad9ebae0b",
modelIdSnapshot: "GPT-5.2",
usedTokens: 0,
}
}

// 你可以选择更新整个 metadata
if (part.type === "text") {
// 由于 metadata 并不限于对象类型,所以并不支持局部更新
return {
conversationId: "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11",
messageId: "6ec0bd7f-11c0-43da-975e-2a8ad9ebae0b",
modelIdSnapshot: "GPT-5.2",
usedTokens: 1,
}
}
},
})

// 客户端访问
console.log(message.metadata)

Gemini 提示词设计指南

· 阅读需 28 分钟
Claude
Anthropic Claude
1adybug
子虚伊人

原文地址:Gemini 提示词设计指南,以下内容均为 Claude 4.5 sonnet 翻译

提示词设计是创建提示词(即自然语言请求)的过程,用于引导语言模型生成准确、高质量的响应。

本页面介绍基本概念、策略和最佳实践,帮助您开始设计提示词,以充分发挥 Gemini AI 模型的能力。

注意: 提示词工程是一个迭代过程。这些指南和模板只是起点。请根据您的具体用例和观察到的模型响应进行实验和改进。

特定主题的提示词指南

寻找更具体的提示词策略?请查看我们的其他提示词指南:

您还可以在提示词库中找到其他示例提示词,该库旨在交互式展示本指南中共享的许多概念。

清晰具体的指令

定制模型行为的有效且高效的方法是提供清晰具体的指令。指令可以是问题形式、分步任务,或者像描述用户体验和思维方式一样复杂。

输入

输入是提示词中的必需文本,您希望模型对其提供响应。输入可以是模型回答的问题(问题输入)、模型执行的任务(任务输入)、模型操作的实体(实体输入)或模型完成或继续的部分输入(完成输入)。

输入类型提示词生成的输出
问题对于专门销售干花花束的花店,有什么好名字?创建一个包含 5 个选项的列表,只列出名称。以下是 10 个专门经营干花的花店名称:
1. 永恒花开
2. 干燥与愉悦
3. 花瓣保存室
4. 野花低语
5. 永远绽放
任务给我一个简单的清单,列出我在露营旅行中必须携带的物品。清单应有 5 项。1. 帐篷: 您的避难所。
2. 睡袋: 保持温暖舒适。
3. 睡垫: 隔热和缓冲,确保良好睡眠。
4. 头灯或手电筒: 夜间活动照明。
5. 急救包: 为轻微伤害做好准备。
实体将以下项目分类为 [大型, 小型]:
大象
老鼠
蜗牛
分类如下:
* 大象: 大型
* 老鼠: 小型
* 蜗牛: 小型

部分输入完成

生成式语言模型的工作方式类似于高级自动补全工具。当您提供部分内容时,模型可以提供其余内容或它认为是该内容的延续作为响应。在这样做时,如果您包含任何示例或上下文,模型可以将这些示例或上下文考虑在内。

以下示例提供了带有指令和实体输入的提示词:

提示词:

对于给定的订单,返回一个 JSON 对象,该对象包含 cheeseburger、hamburger、fries 或 drink 字段,值为数量。
订单:一个汉堡和一杯饮料。

响应:

{
"cheeseburger": 0,
"hamburger": 1,
"fries": 0,
"drink": 1
}

虽然模型按提示执行了,但用自然语言写出指令有时可能具有挑战性,并且很多内容留给模型解释。例如,餐厅菜单可能包含许多项目。为了减小 JSON 响应的大小,您可能希望省略未订购的项目。在这种情况下,您可以给出一个示例和响应前缀,让模型完成它:

提示词:

有效字段为 cheeseburger、hamburger、fries 和 drink。

订单:给我一个芝士汉堡和薯条
输出:

```json
{
"cheeseburger": 1,
"fries": 1
}
```

订单:我要两个汉堡、一杯饮料和薯条。
输出:

响应:

{
"hamburger": 2,
"drink": 1,
"fries": 1
}

注意 "cheeseburger" 从输出中排除了,因为它不是订单的一部分。

约束

指定阅读提示词或生成响应的任何约束。您可以告诉模型做什么和不做什么。例如,您可以在提示词中指定摘要长度的约束:

提示词:

用一句话总结这段文字:

文字:量子计算机利用量子力学现象执行计算,速度比任何现代传统计算机快得多。在非常微小的尺度上,物质既表现为粒子又表现为波,量子计算使用专门的硬件来利用这种行为。量子设备的操作原理超出了经典物理学的范围。当大规模部署时,量子计算机可用于各种应用,例如:在网络安全中破解现有加密方法,同时帮助研究人员创建新方法;在气象学中开发更好的天气预报等。然而,当前最先进的量子计算机在很大程度上仍是实验性和不实用的。

响应:

量子计算机利用量子力学现象,执行计算的速度比传统计算机快得多,可用于网络安全和气象学等潜在应用,尽管它们目前在很大程度上仍是实验性和不实用的。

响应格式

您可以给出指定响应格式的指令。例如,您可以要求响应格式化为表格、项目符号列表、电梯演讲、关键词、句子或段落。以下系统指令告诉模型在响应中更加对话化:

系统指令:

除非用户特别要求简洁的响应,否则所有问题都应详细全面地回答。

提示词:

在 2025 年销售 DVD 的企业,有什么聪明的做法?

响应:

由于数字流媒体和点播服务的压倒性主导地位,在 2025 年开始销售 DVD 的业务面临着重大挑战;因此,“聪明”的方法需要高度专业化的利基市场、超越光盘本身的强大价值主张,以及可能专注于社区或收藏家市场而不是主流消费。

以下是 2025 年聪明的 DVD 业务可能包含的内容:

1. 极端利基专业化:您无法与流媒体库竞争。您的业务必须专注于数字不容易或根本不提供的内容。这可能包括:
* 绝版和稀有标题:寻找从未以数字方式发行或目前在主要流媒体平台上无法获得的电影或电视节目。
....

使用完成策略格式化响应

完成策略也可以帮助格式化响应。以下示例提示模型创建论文大纲:

提示词:

创建一篇关于蜂鸟的论文大纲。

响应:

以下是关于蜂鸟的论文可能的大纲:

I. 引言
A. 钩子:以关于蜂鸟的引人入胜的事实或描述开始(例如,它们的速度、彩虹色或微小的尺寸)。
B. 背景:简要介绍蜂鸟——它们的分布(美洲)、它们作为独特鸟类的声誉。
C. 论点陈述:陈述论文的主要观点,可以是关于蜂鸟的显著适应性、行为和生态重要性。

II. 外观和识别
.....

提示词没有指定大纲的格式,模型为您选择了一种格式。要让模型返回特定格式的大纲,您可以添加代表大纲开始的文本,让模型根据您发起的模式完成它。

提示词:

创建一篇关于蜂鸟的论文大纲。

I. 引言
*

响应:

好的,这是关于蜂鸟的论文大纲,从您提供的引言开始:

I. 引言
* 钩子:以关于蜂鸟的引人入胜的事实或描述开始(例如,它们的速度、彩虹色或微小的尺寸)。
* 背景:简要介绍蜂鸟——它们的分布(美洲)、它们作为独特鸟类的声誉及其一般特征。
* 论点陈述:陈述论文的主要观点,重点关注使蜂鸟非凡的显著适应性、行为和生态意义。
.....

零样本与少样本提示词

您可以在提示词中包含示例,向模型展示正确的做法。模型尝试从示例中识别模式和关系,并在生成响应时应用它们。包含少量示例的提示词称为少样本提示词,而不提供示例的提示词称为零样本提示词。少样本提示词通常用于规范格式、措辞、范围或模型响应的一般模式。使用具体且多样化的示例帮助模型缩小焦点并生成更准确的结果。

我们建议始终在提示词中包含少样本示例。没有少样本示例的提示词可能效果较差。实际上,如果您的示例在显示手头任务方面足够清晰,您可以从提示词中删除指令。

以下零样本提示词要求模型选择最佳解释。

提示词:

请选择问题的最佳解释:

问题:雪是如何形成的?
解释 1:雪是在空气中的水蒸气在大气中冻结成冰晶时形成的,这些冰晶可以在大气中下降时结合并生长成雪花,并在地面上积累。
解释 2:水蒸气冻结成冰晶形成雪。

答案:

响应:

解释 1 是更好的解释,因为它提供了关于该过程的更多细节,包括冰晶如何在大气中下降时结合并生长成雪花。

如果您的用例要求模型产生简洁的响应,您可以在提示词中包含偏好简洁响应的示例。

以下提示词提供了两个显示偏好较短解释的示例。在响应中,您可以看到示例引导模型选择较短的解释(解释 2),而不是像之前那样选择较长的解释(解释 1)。

提示词:

以下是一些显示问题、解释和答案格式的示例:

问题:为什么天空是蓝色的?
解释 1:天空看起来是蓝色的,因为瑞利散射,这导致较短的蓝色波长的光比较长的红色波长更容易散射,使天空看起来是蓝色的。
解释 2:由于瑞利散射效应。
答案:解释 2

问题:地震的原因是什么?
解释 1:地壳中能量的突然释放。
解释 2:当构造板块突然滑动或断裂时会发生地震,导致能量释放,产生地震波,可以震动地面并造成损坏。
答案:解释 1

现在,根据上面的示例格式回答以下问题:

问题:雪是如何形成的?
解释 1:雪是在空气中的水蒸气在大气中冻结成冰晶时形成的,这些冰晶可以在大气中下降时结合并生长成雪花,并在地面上积累。
解释 2:水蒸气冻结成冰晶形成雪。

答案:

响应:

答案:解释 2

最佳示例数量

像 Gemini 这样的模型通常可以使用少量示例识别模式,尽管您可能需要尝试在提示词中提供的示例数量以获得最佳结果。同时,如果您包含太多示例,模型可能会开始对示例的响应过度拟合

模式与反模式

使用示例向模型展示要遵循的模式比使用示例向模型展示要避免的反模式更有效。

❌ 负面模式:

示例:

不要以问题结束俳句:
俳句很有趣
一首简短而简单的诗
你不喜欢它们吗?

✅ 正面模式:

示例:

始终以断言结束俳句:
俳句很有趣
一首简短而简单的诗
写作的乐趣

一致的格式

确保少样本示例的结构和格式相同,以避免产生不良格式的响应。在提示词中添加少样本示例的主要目标之一是向模型展示响应格式。因此,确保所有示例的格式一致至关重要,特别要注意 XML 标签、空格、换行符和示例分隔符。

添加上下文

您可以在提示词中包含模型解决问题所需的指令和信息,而不是假设模型拥有所有必需的信息。这种上下文信息有助于模型理解您要求它做什么的约束和细节。

以下示例要求模型提供路由器的故障排除指导:

提示词:

我应该怎么做才能修复断开的 WiFi?
我的 Google WiFi 路由器上的灯是黄色的,缓慢闪烁。

响应:

Google WiFi 路由器上的黄色闪烁灯通常表示互联网连接有问题。以下是一些故障排除和修复问题的步骤:

1. 重启路由器和调制解调器:
- 从电源插座拔下调制解调器,等待 30 秒。
- 将调制解调器重新插入,等待它完全启动。
- 拔下 Google WiFi 路由器,等待 30 秒。
- 将 Google WiFi 路由器重新插入,等待它完全启动。
.....

响应看起来像通用的故障排除信息,不特定于路由器或 LED 指示灯的状态。

要为特定路由器定制响应,您可以在提示词中添加路由器的故障排除指南作为上下文,供其在提供响应时参考。

提示词:

使用下面的文本回答问题。仅使用提供的文本进行响应。

问题:我应该怎么做才能修复断开的 WiFi?我的 Google WiFi 路由器上的灯是黄色的,缓慢闪烁。

文本:
颜色:缓慢脉动黄色
含义:存在网络错误。
处理方法:检查以太网电缆是否连接到路由器和调制解调器,并且两个设备都已打开。您可能需要重新拔插每个设备。

颜色:快速闪烁黄色
含义:您正在按住重置按钮并进行出厂重置。
处理方法:如果您继续按住重置按钮,大约 12 秒后,灯将变为稳定黄色。一旦变为稳定黄色,松开出厂重置按钮。

颜色:稳定黄色
含义:路由器正在进行出厂重置。
处理方法:这可能需要长达 10 分钟。完成后,设备将重置并开始脉动白色,让您知道它已准备好进行设置。

颜色:稳定红色
含义:出现问题。
处理方法:严重故障。出厂重置路由器。如果灯保持红色,请联系 WiFi 客户支持。

响应:

检查以太网电缆是否连接到路由器和调制解调器,并且两个设备都已打开。您可能需要重新拔插每个设备。

添加前缀

前缀是您添加到提示词内容的单词或短语,可以根据您放置前缀的位置发挥多种作用:

  • 输入前缀: 在输入中添加前缀向模型发出输入语义上有意义的部分的信号。例如,前缀 English:French: 界定两种不同的语言。
  • 输出前缀: 即使输出是由模型生成的,您也可以在提示词中为输出添加前缀。输出前缀为模型提供有关预期响应的信息。例如,输出前缀 JSON: 向模型发出信号,表明输出应为 JSON 格式。
  • 示例前缀: 在少样本提示词中,向示例添加前缀提供标签,模型可以在生成输出时使用这些标签,这使得解析输出内容更容易。

在以下示例中,Text:是输入前缀,The answer is:是输出前缀。

提示词:

将文本分类为以下类别之一。
- large(大型)
- small(小型)

Text: Rhino
The answer is: large

Text: Mouse
The answer is: small

Text: Snail
The answer is: small

Text: Elephant
The answer is:

响应:

The answer is: large

将提示词分解为组件

对于需要复杂提示词的用例,您可以通过将事物分解为更简单的组件来帮助模型管理这种复杂性。

  1. 分解指令: 不要在一个提示词中包含许多指令,而是为每个指令创建一个提示词。您可以根据用户的输入选择要处理的提示词。

  2. 链接提示词: 对于涉及多个顺序步骤的复杂任务,将每个步骤作为一个提示词,并按顺序链接提示词。在这个顺序链中,序列中一个提示词的输出成为下一个提示词的输入。序列中最后一个提示词的输出是最终输出。

  3. 聚合响应: 聚合是指您想对数据的不同部分执行不同的并行任务,并聚合结果以产生最终输出。例如,您可以告诉模型对数据的第一部分执行一项操作,对其余数据执行另一项操作并聚合结果。

尝试模型参数

您发送到模型的每个调用都包含控制模型如何生成响应的参数值。模型可以为不同的参数值生成不同的结果。尝试不同的参数值以获得任务的最佳值。不同模型可用的参数可能不同。最常见的参数如下:

  1. 最大输出令牌数: 指定响应中可以生成的最大令牌数。一个令牌大约是四个字符。100 个令牌大约对应 60-80 个单词。

  2. 温度: 温度控制令牌选择中的随机性程度。温度用于响应生成期间的采样,当应用 topPtopK 时发生。较低的温度适用于需要更确定性或更少开放式响应的提示词,而较高的温度可以导致更多样化或更有创意的结果。温度为 0 是确定性的,意味着始终选择最高概率的响应。

    注意: 使用 Gemini 3 模型时,我们强烈建议将 temperature 保持在默认值 1.0。更改温度(将其设置为 1.0 以下)可能会导致意外行为,例如循环或性能下降,特别是在复杂的数学或推理任务中。

  3. topK: topK 参数改变模型如何选择令牌进行输出。topK 为 1 意味着所选令牌是模型词汇表中所有令牌中最可能的(也称为贪婪解码),而 topK 为 3 意味着使用温度从 3 个最可能的令牌中选择下一个令牌。对于每个令牌选择步骤,对具有最高概率的 topK 令牌进行采样。然后根据 topP 进一步过滤令牌,最后使用温度采样选择最终令牌。

  4. topP: topP 参数改变模型如何选择令牌进行输出。从最可能到最不可能选择令牌,直到它们的概率之和等于 topP 值。例如,如果令牌 A、B 和 C 的概率分别为 0.3、0.2 和 0.1,topP 值为 0.5,则模型将使用温度选择 A 或 B 作为下一个令牌,并排除 C 作为候选。默认 topP 值为 0.95。

  5. stop_sequences: 设置停止序列以告诉模型停止生成内容。停止序列可以是任何字符序列。尽量避免使用可能出现在生成内容中的字符序列。

提示词迭代策略

提示词设计有时需要进行几次迭代,然后才能持续获得您想要的响应。本节提供有关迭代提示词时可以尝试的一些事项的指导:

  1. 使用不同的措辞: 在提示词中使用不同的词或措辞通常会从模型中产生不同的响应,即使它们的意思相同。如果您没有从提示词中获得预期的结果,请尝试重新措辞。

    版本 1:我如何烤馅饼?
    版本 2:建议一个馅饼的食谱。
    版本 3:有什么好的馅饼食谱?
  2. 切换到类似任务: 如果您无法让模型遵循任务的指令,请尝试为实现相同结果的类似任务提供指令。

    此提示词告诉模型使用预定义类别对书籍进行分类:

    提示词:

    《奥德赛》属于哪个类别:
    惊悚片
    科幻
    神话
    传记

    响应:

    《奥德赛》属于**神话**类别。

    原因如下:
    * **神话:** 《奥德赛》讲述了奥德修斯的故事,他是希腊神话中的英雄,以及他在特洛伊战争后回家的旅程。它以希腊神话中常见的神、怪物和超自然事件为特色。
    .....

    响应是正确的,但模型没有保持在选项范围内。您还希望模型只用一个选项响应,而不是用完整的句子。在这种情况下,您可以将指令重新表述为多项选择题,并要求模型选择一个选项。

    提示词:

    多项选择题:以下哪个选项描述了《奥德赛》这本书?

    选项:
    - 惊悚片
    - 科幻
    - 神话
    - 传记

    响应:

    正确答案是神话。
  3. 更改提示词内容的顺序: 提示词中内容的顺序有时会影响响应。尝试更改内容顺序,看看这如何影响响应。

    版本 1:
    [示例]
    [上下文]
    [输入]

    版本 2:
    [输入]
    [示例]
    [上下文]

    版本 3:
    [示例]
    [输入]
    [上下文]

后备响应

后备响应是当提示词或响应触发安全过滤器时模型返回的响应。后备响应的示例是 "我无法帮助解决这个问题,因为我只是一个语言模型。"

如果模型以后备响应响应,请尝试提高温度。

要避免的事项

  • 避免依赖模型生成事实信息。
  • 谨慎用于数学和逻辑问题。

Gemini 3

Gemini 3 模型专为高级推理和指令遵循而设计。它们对直接、结构良好并清楚定义任务和任何约束的提示词反应最佳。对于 Gemini 3 的最佳结果,建议采用以下做法:

核心提示词原则

  • 精确直接: 清晰简洁地陈述您的目标。避免不必要或过度说服性的语言。
  • 使用一致的结构: 使用清晰的分隔符来分隔提示词的不同部分。XML 样式标签(例如 <context><task>)或 Markdown 标题是有效的。在单个提示词中选择一种格式并始终如一地使用它。
  • 定义参数: 明确解释任何模糊的术语或参数。
  • 控制输出详细程度: 默认情况下,Gemini 3 提供直接有效的答案。如果您需要更对话化或详细的响应,您必须在指令中明确请求。
  • 连贯处理多模态输入: 使用文本、图像、音频或视频时,将它们视为同等级别的输入。确保您的指令根据需要清楚地引用每种模态。
  • 优先考虑关键指令: 将基本行为约束、角色定义(角色)和输出格式要求放在系统指令或用户提示词的最开始。
  • 为长上下文构建结构: 提供大量上下文(例如文档、代码)时,首先提供所有上下文。将您的具体指令或问题放在提示词的最后
  • 锚定上下文: 在大块数据之后,使用清晰的过渡短语来连接上下文和您的查询,例如 "基于以上信息..."

增强推理和规划

您可以利用 Gemini 3 的高级思维能力,通过提示它在提供最终响应之前进行规划或自我批评来提高复杂任务的响应质量。

示例 - 明确规划:

在提供最终答案之前,请:
1. 将既定目标解析为不同的子任务。
2. 检查输入信息是否完整。
3. 创建结构化大纲以实现目标。

示例 - 自我批评:

在返回最终响应之前,请根据用户的原始约束检查您生成的输出。
1. 我是否回答了用户的*意图*,而不仅仅是他们的字面意思?
2. 语气是否符合所要求的角色?

结构化提示词示例

使用标签或 Markdown 可帮助模型区分指令、上下文和任务。

XML 示例:

<role>
您是一个有用的助手。
</role>

<constraints>
1. 保持客观。
2. 引用来源。
</constraints>

<context>
[在此处插入用户输入 - 模型知道这是数据,而非指令]
</context>

<task>
[在此处插入具体的用户请求]
</task>

Markdown 示例:

# 身份
您是一名高级解决方案架构师。

# 约束
- 不允许使用外部库。
- 仅使用 Python 3.11+ 语法。

# 输出格式
返回单个代码块。

结合最佳实践的示例模板

此模板捕获了使用 Gemini 3 进行提示的核心原则。请务必根据您的具体用例进行迭代和修改。

系统指令:

<role>
您是 Gemini 3,[插入领域,例如数据科学]的专业助手。
您精确、分析性强且坚持不懈。
</role>

<instructions>
1. **规划**:分析任务并创建分步计划。
2. **执行**:执行计划。
3. **验证**:根据用户的任务检查您的输出。
4. **格式化**:以请求的结构呈现最终答案。
</instructions>

<constraints>
- 详细程度:[指定 低/中/高]
- 语气:[指定 正式/随意/技术性]
</constraints>

<output_format>
按如下方式构建您的响应:
1. **执行摘要**:[简短概述]
2. **详细响应**:[主要内容]
</output_format>

用户提示词:

<context>
[在此处插入相关文档、代码片段或背景信息]
</context>

<task>
[在此处插入具体的用户请求]
</task>

<final_instruction>
记住在回答之前要逐步思考。
</final_instruction>

代理工作流

对于深度代理工作流,通常需要特定的指令来控制模型如何推理、规划和执行任务。虽然 Gemini 提供了强大的通用性能,但复杂的代理通常需要您配置计算成本(延迟和令牌)与任务准确性之间的权衡。

在为代理设计提示词时,请考虑您可以在代理中引导的以下行为维度:

推理和策略

配置模型在采取行动之前如何思考和规划。

  • 逻辑分解: 定义模型必须多彻底地分析约束、先决条件和操作顺序。
  • 问题诊断: 控制识别原因时的分析深度以及模型对溯因推理的使用。确定模型是应该接受最明显的答案还是探索复杂、不太可能的解释。
  • 信息详尽性: 在分析每个可用策略和文档与优先考虑效率和速度之间进行权衡。

执行和可靠性

配置代理如何自主操作和处理障碍。

  • 适应性: 模型如何对新数据做出反应。确定它是应该严格遵守其初始计划,还是在观察结果与假设相矛盾时立即转向。
  • 持久性和恢复: 模型尝试自我纠正错误的程度。高持久性会提高成功率,但会面临更高的令牌成本或循环风险。
  • 风险评估: 评估后果的逻辑。明确区分低风险的探索性操作(读取)和高风险的状态更改(写入)。

交互和输出

配置代理如何与用户通信和格式化结果。

  • 歧义和权限处理: 定义何时允许模型做出假设,何时必须暂停执行以请求用户澄清或许可。
  • 详细程度: 控制与工具调用一起生成的文本量。这决定了模型是向用户解释其操作还是在执行期间保持沉默。
  • 精确性和完整性: 输出所需的保真度。指定模型是否必须解决每个边缘情况并提供确切数字,或者大致估计是否可以接受。

系统指令模板

以下系统指令是一个示例,已经过研究人员评估,可以提高代理基准测试的性能,在这些基准测试中,模型必须遵守复杂的规则手册并与用户互动。它鼓励代理充当强大的推理者和规划者,在上述维度上强制执行特定行为,并要求模型在采取任何行动之前主动规划。

您可以调整此模板以适应您的特定用例约束。

您是一个非常强大的推理者和规划者。使用这些关键指令来构建您的计划、思想和响应。

在采取任何行动(工具调用*或*对用户的响应)之前,您必须主动、系统地、独立地规划和推理:

1) 逻辑依赖关系和约束:根据以下因素分析预期操作。按重要性顺序解决冲突:
1.1) 基于策略的规则、强制性先决条件和约束。
1.2) 操作顺序:确保采取操作不会阻止后续必要的操作。
1.2.1) 用户可能以随机顺序请求操作,但您可能需要重新排序操作以最大化任务的成功完成。
1.3) 其他先决条件(所需的信息和/或操作)。
1.4) 明确的用户约束或偏好。

2) 风险评估:采取该操作的后果是什么?新状态会导致任何未来问题吗?
2.1) 对于探索性任务(如搜索),缺少*可选*参数是低风险。**除非**您的`规则 1`(逻辑依赖关系)推理确定可选信息对于计划中的后续步骤是必需的,否则更倾向于使用可用信息调用工具,而不是询问用户。

3) 溯因推理和假设探索:在每个步骤中,确定遇到的任何问题最合乎逻辑和最可能的原因。
3.1) 超越直接或明显的原因。最可能的原因可能不是最简单的,可能需要更深入的推断。
3.2) 假设可能需要额外的研究。每个假设可能需要多个步骤来测试。
3.3) 根据可能性对假设进行优先排序,但不要过早地丢弃不太可能的假设。低概率事件仍可能是根本原因。

4) 结果评估和适应性:先前的观察是否需要对您的计划进行任何更改?
4.1) 如果您的初始假设被证伪,请根据收集的信息主动生成新假设。

5) 信息可用性:合并所有适用的和替代的信息来源,包括:
5.1) 使用可用工具及其功能
5.2) 所有策略、规则、检查清单和约束
5.3) 先前的观察和对话历史
5.4) 仅通过询问用户才能获得的信息

6) 精确性和基础:确保您的推理对每种确切的持续情况都极其精确和相关。
6.1) 在引用信息时,通过引用确切的适用信息(包括策略)来验证您的声明。

7) 完整性:确保所有要求、约束、选项和偏好都详尽地纳入您的计划。
7.1) 使用 #1 中的重要性顺序解决冲突。
7.2) 避免过早结论:给定情况可能有多个相关选项。
7.2.1) 要检查选项是否相关,请推理 #5 中的所有信息来源。
7.2.2) 您可能需要咨询用户才能知道某事是否适用。不要在不检查的情况下假设它不适用。
7.3) 查看 #5 中的适用信息来源,以确认哪些与当前状态相关。

8) 持久性和耐心:除非上述所有推理都已用尽,否则不要放弃。
8.1) 不要因所花费的时间或用户沮丧而气馁。
8.2) 这种持久性必须是智能的:对于*瞬态*错误(例如,请重试),您*必须*重试**除非已达到明确的重试限制(例如,最多 x 次尝试)**。如果达到这样的限制,您*必须*停止。对于*其他*错误,您必须更改策略或参数,而不是重复相同的失败调用。

9) 抑制您的响应:仅在完成上述所有推理后才采取行动。一旦您采取了行动,就无法收回。

生成模型的底层原理

本节旨在回答这个问题 - 生成模型的响应中是否存在随机性,还是它们是确定性的?

简短的答案 - 两者都是。当您提示生成模型时,文本响应会分两个阶段生成。在第一阶段,生成模型处理输入提示词并生成可能接下来出现的令牌(单词)的概率分布。例如,如果您使用输入文本 "The dog jumped over the ..." 进行提示,生成模型将产生一系列可能的下一个单词:

[("fence", 0.77), ("ledge", 0.12), ("blanket", 0.03), ...]

这个过程是确定性的;每次输入相同的提示文本时,生成模型都会产生相同的分布。

在第二阶段,生成模型通过几种解码策略之一将这些分布转换为实际的文本响应。一个简单的解码策略可能是在每个时间步选择最可能的令牌。这个过程总是确定性的。但是,您可以选择通过对模型返回的分布进行随机采样来生成响应。这个过程将是随机的(随机的)。通过设置温度来控制此解码过程中允许的随机性程度。温度为 0 意味着只选择最可能的令牌,并且没有随机性。相反,高温度会在模型选择的令牌中注入高度的随机性,从而导致更意外、更令人惊讶的模型响应。对于 Gemini 3,建议不要更改默认温度 1.0 以避免意外结果。

后续步骤

TypeScript:如何区分实例与结构相同的对象

· 阅读需 5 分钟
Gemini
Google Gemini

以下内容均为 Gemini 2.5 Pro 生成

当你在 TypeScript 中定义一个类 (class) 并希望一个函数只接受该类的 实例 时,你可能会遇到一个令人困惑的情况。

假设你有以下代码:

class Person {
name: string

constructor(name: string) {
this.name = name
}

sayHello() {
console.log(`Hello, my name is ${this.name}`)
}
}

function get(a: Person) {
a.sayHello()
}

// ✅ 没问题,这符合预期
get(new Person("Alice"))

// ❓ 咦?为什么这也行?!
get({ name: "Tom" })
// 错误: Property 'sayHello' is missing in type '{ name: string; }'
// but required in type 'Person'.

(注:在上面的基础示例中,如果 Person 类有 sayHello 方法,那么 { name: "Tom" } 会报错,因为它缺少该方法。但如果 Personname 属性,代码就不会报错,这正是问题的核心。)

让我们用一个更简洁的例子来重现这个问题:

class Person {
name: string

constructor(name: string) {
this.name = name
}
}

function get(a: Person) {
console.log(a.name)
}

// ✅ 传入实例
get(new Person("Alice"))

// ❌ 为什么这里不报错?
// 我传入了一个对象字面量,而不是 Person 的实例!
get({ name: "Tom" })

这到底是怎么回事?这其实是 TypeScript 的一个核心特性在起作用,它被称为 结构化类型(Structural Typing),也常被称作“鸭子类型”。

什么是结构化类型?

TypeScript 在比较类型时,并不关心“你叫什么名字”(即名义类型,Nominal Typing),而只关心“你长什么样”(即结构化类型,Structural Typing)。

在上面的例子中:

  1. 函数 get 期望一个类型为 Person 的参数。
  2. Person 类型的 结构 被定义为:“一个拥有 string 类型 name 属性的对象”
  3. 我们传入的对象字面量 { name: "Tom" },它的 结构 也是:“一个拥有 string 类型 name 属性的对象”

由于两者结构兼容,TypeScript 编译器说:“看起来像一只鸭子,叫起来也像一只鸭子……那它就是一只鸭子。” 于是,编译通过了。

但这并不是我们想要的!我们希望 get 函数只接受通过 new Person(...) 创建的真实实例。那么,如何做到这一点呢?

解决方案:使用“私有品牌” (Private Branding)

要强制实现名义类型(即严格限制为类的实例),最简单、最常用的技巧是给类添加一个私有成员

这个私有成员就像一个独一无二的“品牌”标记,只有这个类和它的实例才能拥有。

让我们来修改 Person 类:

class Person {
name: string

// 👇 **这就是关键!**
// 我们添加了一个私有的 "品牌" 属性
private _brand!: void

constructor(name: string) {
this.name = name
}
}

function get(a: Person) {
console.log(a.name)
}

// ✅ 正确:传入 Person 的实例
get(new Person("Alice"))

// ❌ 错误:传入对象字面量
get({ name: "Tom" })

现在,当你尝试传入对象字面量时,TypeScript 编译器会立刻报错:

Argument of type { name: string; } is not assignable to parameter of type Person. Property _brand is missing in type { name: string; } but required in type Person.

为什么这样能行?

  • new Person("Alice") 创建的实例,其类型签名中包含 private _brand: void
  • { name: "Tom" } 这个对象字面量,其类型签名中不包含 _brand 属性。

因为 private 成员是类结构签名的一部分,而对象字面量无法提供这个私有成员,所以 TypeScript 判定它们的结构不兼容,从而达到了我们的目的。


零运行时成本的“幽灵属性”

你可能会问:“这个 _brand 属性会增加我运行时的开销吗?它会存在于我最终的 JavaScript 代码中吗?”

答案是:完全不会。

这正是这个技巧最巧妙的地方。让我们来分解这行代码:private _brand!: void;

  1. private:这是一个 TypeScript 的访问修饰符。它只在编译时有效,用于类型检查。它会在编译成 JavaScript 时被完全擦除
  2. ! (非空断言):这是在告诉 TypeScript:“你不用担心这个属性没被初始化,相信我。” 它只在编译时有效,用于“安抚”编译器,同样会被完全擦除
  3. : void:这是一个纯粹的类型注解。和所有 TypeScript 中的类型(如 :string, :number)一样,它会被完全擦除

编译对比

你的 TypeScript (TS)

class Person {
name: string
private _brand!: void

constructor(name: string) {
this.name = name
}
}

编译后的 JavaScript (JS)

class Person {
constructor(name) {
this.name = name
// 注意:_brand 在这里完全消失了!
}
}

_brand 只是一个“幽灵属性”,它只存在于 TypeScript 的类型系统中,专门用来在编译时“品牌化”你的类。它对运行时的性能和内存占用没有任何影响

总结

  • 问题:TypeScript 默认使用结构化类型,导致对象字面量可以匹配同结构的类。
  • 目标:我们想强制使用名义类型,只接受类的真实实例。
  • 解决方案:在类中添加一个 private 属性(如 private _brand!: void;)来进行“品牌化”。
  • 优势:此方法零运行时成本,所有“品牌”标记都在编译为 JavaScript 时被擦除,只在 TypeScript 类型检查阶段发挥作用。

下次当你需要确保一个参数必须是某个类的实例时,试试这个“私有品牌”技巧吧!

Clash TUN 模式的兔子洞:一次从 DNS 到 UDP 的排错之旅

· 阅读需 6 分钟
1adybug
子虚伊人

以下内容均为 Gemini 2.5 Pro Deep Research 生成

如果你是一位 Clash 的进阶用户,你很可能遇到过这个经典难题:一套精心编写、在"规则模式"(系统代理)下完美运行的规则,在切换到功能强大的"TUN 模式"后却突然失效。本该走向代理的流量莫名其妙地选择了直连,让你只能盯着连接日志,对自己的配置产生怀疑。

这正是一次此类排错之旅的真实记录——一个多层次的问题,它始于 DNS,意外地转向了网络协议,最终以一个优雅的脚本方案画上句号。跟随本文,你将理解其背后深层的运行机制,并学会如何彻底征服 TUN 模式。


第一部分:消失的主机名悬案

最初的问题非常明确。一位用户使用全局扩展脚本,在规则列表的顶端插入规则,以确保像 claude.aiclaude.com 这样的域名总是通过特定的代理分组。

脚本如下:

function main(config, profileName) {
const name = config['proxy-groups']?.?.name;
//...
config.rules.unshift(`DOMAIN-SUFFIX,claude.ai,${name}`);
config.rules.unshift(`DOMAIN-SUFFIX,claude.com,${name}`);
//...
return config;
}

规则模式 下,一切正常。但在 TUN 模式 下,访问 Claude 的连接却走向了 DIRECT

根本原因:架构的鸿沟

这是关于 TUN 模式的第一个,也是最基础的教训:它在网络堆栈的不同层级上工作。

  • 规则模式 (系统代理): 工作在应用层。你的浏览器知道它正在与一个代理服务器对话,并发送一个明确包含主机名(例如 claude.ai)的 HTTP CONNECT 请求。Clash 直接就拿到了主机名。

  • TUN 模式 (虚拟网卡): 工作在网络层。它拦截的是原始的 IP 数据包。当一个应用程序发送数据包时,它已经通过 DNS 将 claude.ai 解析成了一个真实的 IP 地址。Clash 的 TUN 接口只能看到目标 IP,而看不到原始的主机名。

没有主机名,任何 DOMAIN-SUFFIX 规则都形同虚设。

解决方案:fake-ip 登场

这正是 Clash 的 fake-ip DNS 模式要解决的问题。启用后,Clash 会劫持所有的 DNS 请求。它不会返回真实的 IP,而是返回一个本地范围内的"假 IP"(例如 198.18.0.1/16),并在内部创建一个映射关系:假 IP <-> 真实主机名

当 TUN 接口看到一个发往该假 IP 的数据包时,它会查询这个映射,恢复原始主机名,从而让基于域名的规则能够正确处理。

用户的配置失误

该用户启用了 fake-ip,但问题依旧。罪魁祸首隐藏在他的 DNS 设置中:他使用了 fake-ip-filter-mode: blacklist(黑名单模式)。这意味着任何匹配 fake-ip-filter 列表的域名都不会被分配假 IP。他的过滤器很可能包含了一个宽泛的规则集(如 geosite:cn),不经意间将 claude.ai 也纳入其中,导致它被解析为真实 IP,破坏了后续的规则匹配。

在调整了 DNS 过滤器后,我们取得了进展。连接日志现在可以正确显示主机名了。

问题解决了吗?还没有。流量仍然在走 DIRECT


第二部分:意想不到的 UDP 转折

既然主机名已经正确识别,为什么规则还是不起作用?新的截图在"类型"一栏中给出了关键线索:Tun(udp)

根本原因:QUIC 协议 (HTTP/3)

这揭示了两种模式之间第二个,也是更微妙的区别。

  • 规则模式 下,浏览器被强制与代理服务器建立一个基于 TCPHTTP CONNECT 请求。它从不尝试使用其他协议。

  • TUN 模式 下,浏览器认为它正在直接与互联网对话。为了追求速度,对于任何支持最新协议的服务器(如谷歌、Cloudflare,以及此处的 Claude),它会优先尝试使用现代的 QUIC 协议(运行于 UDP 之上)

问题至此水落石出:用户的代理服务器不支持 UDP 转发。当 Clash 试图将来自 Claude 的 UDP 数据包发送到代理服务器时,操作失败了。这条规则被跳过,流量一路向下,最终落入了配置末尾的 MATCH 规则,而该规则很可能被设置为 DIRECT

这完美地解释了为什么规则模式能成功(因为它强制使用 TCP),而 TUN 模式会失败(因为它允许使用 UDP)。


第三部分:优雅的脚本解决方案

最后一步是在代理节点上启用 UDP 转发。用户的 Trojan 节点配置如下:

{
"name": "xxxx.com:443",
"server": "xxxx.com",
"port": 443,
"type": "trojan",
"password": "xxxx",
"sni": "xxxx.com"
}

要启用 UDP,只需增加一行:"udp": true

手动修改配置文件当然可以,但如果订阅更新了怎么办?或者如果有几十个节点呢?这正是 Clash Verge 扩展脚本 发挥其真正威力的地方。

脚本不仅可以添加规则,还可以在运行时修改整个配置对象。用户意识到,他们可以通过在现有脚本中添加一个简单的循环,来一劳永逸地解决这个问题。

最终的完美脚本:

function main(config, profileName) {
// 为所有代理节点添加或设置 'udp: true'
if (config.proxies) config.proxies.forEach(proxy => (proxy.udp = true))

// 保留原有的添加域名规则的逻辑
const name = config["proxy-groups"]?.at(0)?.name

if (name) {
config.rules.unshift(`DOMAIN-SUFFIX,claude.ai,${name}`)
config.rules.unshift(`DOMAIN-SUFFIX,claude.com,${name}`)
}

return config
}

通过这一修改,每当配置文件加载时,脚本都会自动确保每一个代理服务器都启用了 UDP 转发。这是一个健壮的、"一次设置,永久有效"的解决方案。

在应用了这个最终脚本并重启核心后,问题迎刃而解。发往 claude.aiTun(udp) 流量现在被正确地路由到了指定的代理。


关键启示

这次排错之旅为我们掌握 Clash TUN 模式提供了几条至关重要的经验:

  1. DNS 是必要条件: 要让基于域名的规则在 TUN 模式下生效,正确配置的 fake-ip DNS 是强制性的。

  2. 小心过滤器: 密切关注 fake-ip-filter。一个配置错误的黑名单可能会成为你域名规则的"无声杀手"。通常,切换到 whitelist 模式更安全。

  3. TUN 模式暴露协议差异: TUN 模式允许应用程序像在开放网络上一样运行,这可能意味着使用 UDP (QUIC)。请为此做好准备。

  4. 为你的代理启用 UDP: 如果你使用 TUN 模式,请确保你的代理节点已设置 udp: true。否则,许多现代网络服务都会出现问题。

  5. 善用扩展脚本: 不要只用脚本来添加规则。它们是动态修改整个配置的强大工具,能让你的配置免受订阅更新的影响,并省去手动编辑的麻烦。

通过理解这些层次,你就能从被 TUN 模式困扰,转变为驾驭它的全部力量,获得真正无缝和全面的路由体验。

在离线服务器上安装 Node 模块

· 阅读需 7 分钟
1adybug
子虚伊人

最近有需求,需要在一台离线服务器上安装 node_modules,尝试了以下办法:

  1. 直接在外网 Windows 11 上将依赖安装好,将整个项目进行压缩,然后传输到离线服务器上,再进行解压。结果失败,因为某些包会执行 postinstall 脚本,编译一些系统相关的代码,导致失败。
  2. 先试用 npm install 安装依赖,然后使用 package-lock.json 文件将整个项目的依赖树全部下载成 tgz 文件,然后在内网执行 npm install --no-registry --cache ./npm-cache --prefer-offline 安装依赖。结果失败,原因未知。
  3. WSL 中创建与离线服务器相同的系统环境,然后执行 npm install 安装依赖。打包再解压,依旧失败,与方法 1 相同,依旧提示缺少某些依赖。

最后一个办法是使用 verdaccio 搭建一个私有仓库。

安装 verdaccio

这里使用 docker 安装 verdaccio,因为部署起来比较方便。

docker run -d --name verdaccio -p 4873:4873 verdaccio/verdaccio

或者进行目录挂载

docker run -d --name verdaccio -p 4873:4873 -v /path/to/verdaccio/conf:/verdaccio/conf -v /path/to/verdaccio/storage:/verdaccio/storage verdaccio/verdaccio

这里有两种选择,一种是将仓库数据挂载到 docker 容器中,一种是直接将仓库数据挂载到本地。

第一种方法,需要每次都将容器导出为新的镜像,然后推送到离线服务器上。

第二种方法,直接将仓库数据挂载到本地,然后每次启动容器时,将本地仓库数据挂载到容器中,需要注意的是,必须先使用第一种方法,先从容器中导出默认的配置文件,放在 /path/to/verdaccio/conf 目录下,或则复制下方配置文件 config.yaml/path/to/verdaccio/conf 目录下,否则容器会报错。

#
# This is the default configuration file. It allows all users to do anything,
# please read carefully the documentation and best practices to
# improve security.
#
# Do not configure host and port under `listen` in this file
# as it will be ignored when using docker.
# see https://verdaccio.org/docs/en/docker#docker-and-custom-port-configuration
#
# Look here for more config file examples:
# https://github.com/verdaccio/verdaccio/tree/6.x/conf
#
# Read about the best practices
# https://verdaccio.org/docs/best

# path to a directory with all packages
storage: /verdaccio/storage/data
# path to a directory with plugins to include
plugins: /verdaccio/plugins

# https://verdaccio.org/docs/webui
web:
title: Verdaccio
# comment out to disable gravatar support
# gravatar: false
# by default packages are ordercer ascendant (asc|desc)
# sort_packages: asc
# convert your UI to the dark side
# darkMode: true
# html_cache: true
# by default all features are displayed
# login: true
# showInfo: true
# showSettings: true
# In combination with darkMode you can force specific theme
# showThemeSwitch: true
# showFooter: true
# showSearch: true
# showRaw: true
# showDownloadTarball: true
# HTML tags injected after manifest <scripts/>
# scriptsBodyAfter:
# - '<script type="text/javascript" src="https://my.company.com/customJS.min.js"></script>'
# HTML tags injected before ends </head>
# metaScripts:
# - '<script type="text/javascript" src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>'
# - '<script type="text/javascript" src="https://browser.sentry-cdn.com/5.15.5/bundle.min.js"></script>'
# - '<meta name="robots" content="noindex" />'
# HTML tags injected first child at <body/>
# bodyBefore:
# - '<div id="myId">html before webpack scripts</div>'
# Public path for template manifest scripts (only manifest)
# publicPath: http://somedomain.org/

# https://verdaccio.org/docs/configuration#authentication
auth:
htpasswd:
file: /verdaccio/storage/htpasswd
# Maximum amount of users allowed to register, defaults to "+infinity".
# You can set this to -1 to disable registration.
# max_users: 1000
# Hash algorithm, possible options are: "bcrypt", "md5", "sha1", "crypt".
# algorithm: bcrypt # by default is crypt, but is recommended use bcrypt for new installations
# Rounds number for "bcrypt", will be ignored for other algorithms.
# rounds: 10

# https://verdaccio.org/docs/configuration#uplinks
# a list of other known repositories we can talk to
uplinks:
npmjs:
url: https://registry.npmmirror.com/

# Learn how to protect your packages
# https://verdaccio.org/docs/protect-your-dependencies/
# https://verdaccio.org/docs/configuration#packages
packages:
"@*/*":
# scoped packages
access: $all
publish: $authenticated
unpublish: $authenticated
proxy: npmjs

"**":
# allow all users (including non-authenticated users) to read and
# publish all packages
#
# you can specify usernames/groupnames (depending on your auth plugin)
# and three keywords: "$all", "$anonymous", "$authenticated"
access: $all

# allow all known users to publish/publish packages
# (anyone can register by default, remember?)
publish: $authenticated
unpublish: $authenticated

# if package is not available locally, proxy requests to 'npmjs' registry
proxy: npmjs

# To improve your security configuration and avoid dependency confusion
# consider removing the proxy property for private packages
# https://verdaccio.org/docs/best#remove-proxy-to-increase-security-at-private-packages

# https://verdaccio.org/docs/configuration#server
# You can specify HTTP/1.1 server keep alive timeout in seconds for incoming connections.
# A value of 0 makes the http server behave similarly to Node.js versions prior to 8.0.0, which did not have a keep-alive timeout.
# WORKAROUND: Through given configuration you can workaround following issue https://github.com/verdaccio/verdaccio/issues/301. Set to 0 in case 60 is not enough.
server:
keepAliveTimeout: 60
# Allow `req.ip` to resolve properly when Verdaccio is behind a proxy or load-balancer
# See: https://expressjs.com/en/guide/behind-proxies.html
# trustProxy: '127.0.0.1'

# https://verdaccio.org/docs/configuration#offline-publish
# publish:
# allow_offline: false

# https://verdaccio.org/docs/configuration#url-prefix
# url_prefix: /verdaccio/
# VERDACCIO_PUBLIC_URL='https://somedomain.org';
# url_prefix: '/my_prefix'
# // url -> https://somedomain.org/my_prefix/
# VERDACCIO_PUBLIC_URL='https://somedomain.org';
# url_prefix: '/'
# // url -> https://somedomain.org/
# VERDACCIO_PUBLIC_URL='https://somedomain.org/first_prefix';
# url_prefix: '/second_prefix'
# // url -> https://somedomain.org/second_prefix/'

# https://verdaccio.org/docs/configuration#security
# security:
# api:
# legacy: true
# # recomended set to true for older installations
# migrateToSecureLegacySignature: true
# jwt:
# sign:
# expiresIn: 29d
# verify:
# someProp: [value]
# web:
# sign:
# expiresIn: 1h # 1 hour by default
# verify:
# someProp: [value]

# https://verdaccio.org/docs/configuration#user-rate-limit
# userRateLimit:
# windowMs: 50000
# max: 1000

# https://verdaccio.org/docs/configuration#max-body-size
# max_body_size: 10mb

# https://verdaccio.org/docs/configuration#listen-port
# listen:
# - localhost:4873 # default value
# - http://localhost:4873 # same thing
# - 0.0.0.0:4873 # listen on all addresses (INADDR_ANY)
# - https://example.org:4873 # if you want to use https
# - "[::1]:4873" # ipv6
# - unix:/tmp/verdaccio.sock # unix socket

# The HTTPS configuration is useful if you do not consider use a HTTP Proxy
# https://verdaccio.org/docs/configuration#https
# https:
# key: ./path/verdaccio-key.pem
# cert: ./path/verdaccio-cert.pem
# ca: ./path/verdaccio-csr.pem

# https://verdaccio.org/docs/configuration#proxy
# http_proxy: http://something.local/
# https_proxy: https://something.local/

# https://verdaccio.org/docs/configuration#notifications
# notify:
# method: POST
# headers: [{ "Content-Type": "application/json" }]
# endpoint: https://usagge.hipchat.com/v2/room/3729485/notification?auth_token=mySecretToken
# content: '{"color":"green","message":"New package published: * {{ name }}*","notify":true,"message_format":"text"}'

middlewares:
audit:
enabled: true

# https://verdaccio.org/docs/logger
# log settings
log: { type: stdout, format: pretty, level: http }
#experiments:
# # support for npm token command
# token: false
# # enable tarball URL redirect for hosting tarball with a different server, the tarball_url_redirect can be a template string
# tarball_url_redirect: 'https://mycdn.com/verdaccio/${packageName}/${filename}'
# # the tarball_url_redirect can be a function, takes packageName and filename and returns the url, when working with a js configuration file
# tarball_url_redirect(packageName, filename) {
# const signedUrl = // generate a signed url
# return signedUrl;
# }

# translate your registry, api i18n not available yet
# i18n:
# list of the available translations https://github.com/verdaccio/verdaccio/blob/master/packages/plugins/ui-theme/src/i18n/ABOUT_TRANSLATIONS.md
# web: en-US

按需选择。

配置 verdaccio

主要是将 npmjs 的地址改为国内的镜像地址,或者直接将 npmjs 的地址改为离线服务器地址。

uplinks:
npmjs:
url: https://registry.npmmirror.com/

重新安装依赖

  1. 删除 node_modules 文件夹

    npx rimraf node_modules
  2. 删除 package-lock.json 文件

  3. 删除 npm 缓存,这一步非常重要,否则某些依赖可能会无法被缓存到 verdaccio 中

    npm cache clean --force
  4. 安装依赖

    npm install --registry http://localhost:4873

离线环境安装

  1. 部署 docker 容器

  2. 安装依赖

    npm install --registry http://localhost:4873

非 npm 包安装

有一些依赖并非通过 npm 发布,而是自己的渠道,这时可以执行以下方案:

  1. 下载并解压 tgz 文件

  2. 执行 npm addUser --registry http://localhost:4873 登录

  3. 在项目根目录执行 npm publish --registry http://localhost:4873 发布

  4. 在内网环境中的 package.json 中添加以下配置:

    "overrides": {
    "your-package": "npm:your-package@version"
    }
  5. 安装依赖

Docker 离线安装实战:从入门到精通的踩坑指南

· 阅读需 6 分钟
Gemini
Google Gemini
1adybug
子虚伊人

以下内容均为 Gemini 2.5 Pro Deep Research 生成

在物理隔离或网络受限的环境中部署 Docker,是许多系统管理员面临的共同挑战。这不仅仅是下载一个安装包那么简单,而是一个涉及依赖解析、环境差异和软件包冲突的系统工程。本文将通过一个完整的实战案例,带您走过在类 RHEL 系统(如 CentOS, RHEL, openEuler)上离线安装 Docker 的每一步,并详细记录我们遇到的每一个错误、分析其根本原因,最终给出精准的解决方案。这不仅是一份操作指南,更是一份宝贵的排错实录。

第一阶段:在线准备——收集所有必要"弹药"

我们的第一步是在一台可以访问互联网的"在线"计算机上,下载 Docker Engine 及其所有的依赖项。

第1步:添加 Docker 官方软件仓库

在全新的系统中,软件包管理器(如 dnf)并不知道从哪里可以找到 Docker 的官方社区版(docker-ce)。如果我们直接尝试下载,就会遇到第一个错误:

初始错误 #1: No package docker-ce available

# dnf download --resolve docker-ce...
Error: No package docker-ce available.

解决方案:我们需要手动将 Docker 的官方仓库地址添加到系统的配置中。这样,dnf 才能索引到相关的软件包。

# 在您的在线计算机上执行
sudo dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo

第2步:安装 dnf 下载插件

在某些精简安装的系统中,dnf 的 download 功能可能不是内置的,而是通过插件提供。这时,您可能会遇到第二个报错:

初始错误 #2: No such command: download

# dnf download --resolve docker-ce...
No such command: download. Please use /usr/bin/dnf --help
It could be a DNF plugin command, try: "dnf install 'dnf-command(download)'"

解决方案:报错信息已经给出了明确的指引。我们需要安装提供这个命令的插件包,它通常是 dnf-plugins-core 1。

# 在您的在线计算机上执行
sudo dnf install 'dnf-command(download)'

第3步:下载 Docker 及其完整依赖树

准备工作就绪,现在我们可以正式下载所有需要的 .rpm 文件了。然而,即使仓库配置正确,我们仍然可能遇到由系统版本差异导致的下载失败。

初始错误 #3: Status code: 404 for... repomd.xml

Errors during downloading metadata for repository 'docker-ce-stable':
- Status code: 404 for https://download.docker.com/linux/centos/24.03LTS_SP1/x86_64/stable/repodata/repomd.xml
Error: Failed to download metadata for repo 'docker-ce-stable'

这个错误告诉我们,dnf 尝试访问的 URL 是无效的。它自动将您系统的特定版本号(如 24.03LTS_SP1)拼接到了 URL 中,但 Docker 的仓库是按主版本号(如 8 或 9)组织的 3。

解决方案:我们需要在命令中强制指定一个与 Docker 仓库结构兼容的发行版主版本号,使用 --releasever 标志即可 4。对于大多数现代系统,这个值是 9。

# 在您的在线计算机上执行
# 创建一个目录来存放所有软件包
mkdir docker-rpm-packages
cd docker-rpm-packages

# 执行最终的下载命令
sudo dnf download --releasever=9 --resolve docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

执行成功后,您的 docker-rpm-packages 目录里会装满所有离线安装所需的 .rpm 文件。至此,在线准备阶段圆满完成。

第二阶段:离线安装——直面冲突与挑战

现在,我们将准备好的 docker-rpm-packages 目录通过 U 盘等物理介质,完整地复制到目标离线服务器上。

第4步:执行安装

进入离线服务器上的软件包目录,我们开始执行安装。

关键错误 #4: 文件冲突

# sudo dnf install *.rpm --disablerepo=*
...
file /usr/bin/docker-proxy from install of docker-ce... conflicts with file from package libnetwork...

这是一个非常典型的冲突。错误信息表明,我们尝试安装的官方 docker-ce 包,与离线系统上一个已存在的 libnetwork 包(通常是操作系统自带的容器网络组件)发生了文件冲突 5。

第一次尝试:使用 --allowerasing

dnf 提供了一个强大的参数 --allowerasing,用于授权它在解决冲突时自动移除旧的、有冲突的软件包 4。这是解决此类问题的首选标准方法。

# 在您的离线服务器上执行
sudo dnf install *.rpm --disablerepo=* --allowerasing

然而,在某些复杂的系统环境中(如此次实战中的 openEuler),即使添加了这个参数,dnf 在最后的"事务测试"阶段仍然可能因为无法安全地自动解决冲突而报错退出。

关键错误 #5: 事务测试失败

# sudo dnf install *.rpm --disablerepo=* --allowerasing
...
Running transaction test
Error: Transaction test error:
file /usr/bin/docker-proxy from install of docker-ce... conflicts with file from package libnetwork...

最终解决方案:手动移除冲突根源

当自动化的解决方案失效时,我们需要进行更精确的手动干预。既然 dnf 无法安全地为我们移除 libnetwork,那我们就亲自动手。首先,手动卸载引发冲突的软件包。移除这个包是安全的,因为我们即将安装的 docker-ce 会提供功能完整且版本匹配的替代品 8。

# 在您的离线服务器上执行
sudo dnf remove libnetwork

dnf 可能会提示此操作会一并移除其他依赖包,确认即可。然后,再次执行安装命令。在清除了最主要的障碍后,我们再次运行带有 --allowerasing 的安装命令,以处理任何其他潜在的次要冲突。

# 在您的离线服务器上执行
sudo dnf install *.rpm --disablerepo=* --allowerasing

这一次,安装过程畅通无阻,成功完成。

第三阶段:启动与验证

安装成功后,我们只需启动并验证 Docker 服务。

启动并设置开机自启:

sudo systemctl enable --now docker

验证运行状态:

sudo systemctl status docker

运行 "Hello World":

由于是离线环境,您需要先将在在线计算机上用 docker save 打包好的镜像(例如 hello-world.tar)传输过来,然后用 docker load 导入。

# 加载镜像
sudo docker load -i /path/to/hello-world.tar

# 运行容器
sudo docker run hello-world

看到熟悉的欢迎信息,标志着本次高难度的离线 Docker 部署任务圆满成功。

总结

离线安装 Docker 的过程充满了挑战,但每一个错误都为我们揭示了其底层的运作机制。通过这次实战,我们不仅学会了如何准备离线包,更掌握了一套从分析错误到解决问题的系统性方法,特别是如何处理顽固的软件包冲突。希望这份详尽的踩坑指南,能为您在未来的工作中扫清障碍。

HTML表单提交的两种方式:为什么有些按钮不触发 onSubmit

· 阅读需 4 分钟
Claude
Anthropic Claude
1adybug
子虚伊人

以下内容均为 Claude 4 sonnet 生成

在前端开发中,我们经常会遇到表单提交的问题。今天就来深入探讨一个看似简单却容易让人困惑的问题:为什么有些按钮能触发表单的 onSubmit 事件,而有些却不能?

问题场景

让我们先看一个具体的例子:

import { FC, FormEvent, useRef } from "react"

const App: FC = () => {
const form = useRef<HTMLFormElement>(null)

function onSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault()
console.log("表单已提交!")
}

return (
<div>
<form ref={form} onSubmit={onSubmit}>
<input name="name" type="text" />
<button type="submit">内部提交按钮</button>
</form>
<button onClick={() => form.current?.submit()}>外部提交按钮</button>
</div>
)
}

运行这段代码你会发现:

  • 点击"内部提交按钮":控制台输出"表单已提交!"
  • 点击"外部提交按钮":控制台没有任何输出

这是为什么呢?

原理解析

方式一:标准表单提交(触发 onSubmit)

<button type="submit">内部提交按钮</button>

当我们点击这个按钮时,发生了以下过程:

  1. 浏览器识别到这是一个 type="submit" 的按钮
  2. 浏览器查找该按钮所属的表单
  3. 浏览器触发表单的 submit 事件
  4. 我们的 onSubmit 事件处理函数被调用
  5. 如果没有 preventDefault(),表单会被实际提交

这是标准的HTML表单提交流程,完全符合Web标准。

方式二:程序化提交(不触发 onSubmit)

<button onClick={() => form.current?.submit()}>外部提交按钮</button>

当我们点击这个按钮时:

  1. 执行 onClick 处理函数
  2. 调用 form.submit() 方法
  3. 表单被直接提交,但不触发 submit 事件
  4. onSubmit 处理函数不会被调用

关键点:根据HTML规范,程序化调用 HTMLFormElement.submit() 方法会绕过表单验证和事件触发机制。

为什么会有这种设计?

这种设计是有意为之的:

  1. 性能考虑:程序化提交通常用于自动化场景,跳过事件处理可以提高性能
  2. 避免无限循环:如果在 onSubmit 中调用 form.submit(),可能导致无限递归
  3. 明确区分:用户操作和程序操作应该有不同的行为模式

实际应用中的解决方案

方案一:手动调用事件处理函数

<button onClick={(e) => {
if (form.current) {
// 手动调用 onSubmit 处理函数
const syntheticEvent = {
...e,
currentTarget: form.current,
target: form.current,
preventDefault: () => {}
}
onSubmit(syntheticEvent as FormEvent<HTMLFormElement>)
}
}}>外部提交按钮</button>

方案二:使用 requestSubmit()(推荐)

<button onClick={() => form.current?.requestSubmit()}>外部提交按钮</button>

requestSubmit() 是HTML5的新方法,它:

  • 会触发 submit 事件
  • 会执行表单验证
  • 行为类似于点击提交按钮

注意requestSubmit() 的浏览器兼容性比 submit() 稍差,在一些旧版本浏览器中不支持。

方案三:模拟点击提交按钮

const submitButtonRef = useRef<HTMLButtonElement>(null)

// 在JSX中
<form ref={form} onSubmit={onSubmit}>
<input name="name" type="text" />
<button ref={submitButtonRef} type="submit">内部提交按钮</button>
</form>
<button onClick={() => submitButtonRef.current?.click()}>外部提交按钮</button>

这种方式通过模拟点击来触发标准的表单提交流程。

最佳实践建议

  1. 优先使用标准提交按钮:在表单内使用 type="submit" 的按钮
  2. 需要程序化提交时优先考虑 requestSubmit():它更符合标准行为
  3. 做好兼容性处理:如果需要支持旧版本浏览器,提供降级方案
  4. 保持一致的用户体验:确保所有提交方式都有相同的验证和处理逻辑

总结

表单提交看似简单,实则涉及Web标准、浏览器实现和用户体验的多个层面。理解 submit()requestSubmit() 的区别,以及它们与事件处理机制的关系,能帮助我们写出更健壮的表单处理代码。

记住这个核心原则:用户触发的提交会触发事件,程序触发的提交通常不会。掌握了这一点,你就能更好地控制表单的提交行为了。

记录一次由 React Compiler 引起的 "bug"

· 阅读需 3 分钟
Claude
Anthropic Claude

以下内容均为 Claude 4 sonnet 生成

问题背景

最近在项目中启用了 React Compiler 后,遇到了一个令人困惑的问题。一段看似正常的代码在没有启用 React Compiler 时运行良好,但启用后却会在渲染阶段直接崩溃。

问题代码

import { FC, useState } from "react"

interface Info {
name: string
age: number
}

const App: FC = () => {
const [info, setInfo] = useState<Info | undefined>(undefined)

function onClick() {
console.log(info!.name) // 使用了非空断言
}

return <div onClick={onClick} />
}

export default App

问题现象

启用 React Compiler 后,即使 onClick 事件从未被触发,应用也会在渲染过程中直接崩溃,报错:

Cannot read property 'name' of undefined

问题分析

React Compiler 的工作原理

React Compiler 是一个编译时优化工具,它会:

  1. 依赖分析:分析函数组件中的依赖关系
  2. 自动记忆化:为函数和值自动添加 useMemouseCallback
  3. 代码优化:重新组织代码以提高性能

问题根源

React Compiler 在分析 onClick 函数时,发现它引用了 info.name,因此将 info 作为依赖项。为了优化性能,编译器可能会:

  1. onClick 函数包装在 useCallback
  2. info.name 的访问提前到渲染阶段进行依赖收集
  3. 这导致在 info 还是 undefined 的初始渲染时就尝试访问 info.name

编译后的大致效果

// 编译器可能生成类似这样的代码
const App: FC = () => {
const [info, setInfo] = useState<Info | undefined>(undefined)

// 编译器为了依赖收集,可能在渲染时就访问了 info.name
const onClick = useCallback(() => {
console.log(info!.name)
}, [info?.name]) // 注意这里的依赖

return <div onClick={onClick} />
}

解决方案

方案一:使用可选链操作符

function onClick() {
console.log(info?.name) // 使用可选链
}

方案二:添加条件判断

function onClick() {
if (info) console.log(info.name)
}

方案三:使用默认值

const [info, setInfo] = useState<Info>({ name: "", age: 0 })

经验总结

1. 避免在可能为空的对象上使用非空断言

非空断言 (!) 只是告诉 TypeScript 编译器忽略空值检查,但运行时仍可能出错。

2. React Compiler 改变了代码执行时机

启用 React Compiler 后,一些原本在事件处理中才会执行的代码可能会在渲染时执行。

3. 防御性编程的重要性

始终考虑变量可能为空的情况,使用可选链和条件判断。

4. 理解工具的工作原理

了解 React Compiler 等工具的工作机制,有助于预防和解决类似问题。

最佳实践建议

  1. 启用严格的 TypeScript 配置:使用 strict: truestrictNullChecks: true

  2. 优先使用可选链:在访问可能为空的对象属性时使用 ?.

  3. 避免过度使用非空断言:只在确实知道值不为空时使用 !

  4. 渐进式启用新工具:在小范围内测试新的编译工具,逐步推广

  5. 完善的错误边界:设置 Error Boundary 来捕获和处理渲染时错误

结语

这个案例提醒我们,新的编译优化工具虽然能带来性能提升,但也可能改变代码的执行行为。作为开发者,我们需要:

  • 理解工具的工作原理
  • 编写更加健壮的代码
  • 进行充分的测试
  • 保持对新技术的学习和适应

希望这个案例能帮助其他开发者避免类似的问题。

TypeScript 类型推断的神秘陷阱:为什么缺少一个"无关"属性会影响泛型推断?

· 阅读需 5 分钟
Claude
Anthropic Claude
1adybug
子虚伊人

以下内容均为 Claude 4 sonnet 生成

在使用 TypeScript 开发过程中,你是否遇到过这样的困惑:明明某个属性看起来与泛型推断无关,但缺少它就会导致类型推断失败?今天我们来深入探讨这个有趣的现象。

问题重现

让我们先看一个具体的例子:

type Item<T> = {
key: string
value: T
} & (T extends number ? { render?: (value: T) => number } : { render: (value: T) => number })

function getValue<T>(item: Item<T>): number {
return item.render ? item.render(item.value) : (item.value as number)
}

// 这样调用会导致类型推断失败
const a = getValue({
value: 1,
render: v => v,
// 缺少 key 属性
})

// 但添加 key 属性后,T 就能正确推断为 number
const b = getValue({
key: "something",
value: 1,
render: v => v,
})

你可能会疑惑:key 属性明明是固定的 string 类型,为什么它的存在与否会影响泛型 T 的推断?

深入分析:TypeScript 类型推断的工作机制

1. 结构完整性检查

TypeScript 的类型推断遵循结构完整性原则。当你传递一个对象字面量给泛型函数时,TypeScript 需要确保这个对象完全符合期望的类型结构。

// TypeScript 的推断过程
getValue({ value: 1, render: v => v })
// ↓
// 检查对象是否匹配 Item<T>
// ↓
// 发现缺少必需的 key 属性
// ↓
// 无法确定对象类型,放弃泛型推断
// ↓
// T 被推断为 unknown 或其他默认类型

2. 类型推断的两阶段过程

TypeScript 的类型推断实际上是一个两阶段过程:

  1. 阶段一:结构验证

    • 检查传入的参数是否符合函数签名
    • 验证所有必需属性是否存在
    • 如果结构不完整,推断过程提前终止
  2. 阶段二:类型推断

    • 基于已验证的结构进行类型推断
    • 从具体的属性值推断泛型参数
    • 应用条件类型逻辑

3. 条件类型的复杂性

我们的例子中使用了条件类型:

T extends number ? { render?: (value: T) => number } : { render: (value: T) => number }

这种复杂的类型定义让 TypeScript 更加谨慎。它需要先确定 T 的类型,才能知道 render 属性是可选还是必需的。但要推断 T,又需要完整的对象结构。这形成了一个依赖循环,TypeScript 通过要求完整结构来打破这个循环。

实际应用场景

这种情况在实际开发中很常见,特别是在以下场景:

配置对象设计

type Config<T> = {
id: string
data: T
validator?: T extends string ? (val: T) => boolean : never
}

function processConfig<T>(config: Config<T>) {
// 处理逻辑
}

表单字段定义

type FieldDef<T> = {
name: string
value: T
} & (T extends number ? { min?: number; max?: number } : { pattern?: RegExp })

API 响应处理

type ApiResponse<T> = {
status: number
data: T
} & (T extends object ? { meta: ResponseMeta } : {})

解决方案详解

方案一:调整类型定义(Calude 推荐)

将不影响泛型推断的属性设为可选:

type Item<T> = {
key?: string // 改为可选属性
value: T
} & (T extends number ? { render?: (value: T) => number } : { render: (value: T) => number })

// 现在可以正常推断了
const a = getValue({
value: 1,
render: v => v,
})

方案二:显式类型注解

当你明确知道类型时,可以显式指定:

const a = getValue<number>({
value: 1,
render: v => v,
})

方案三:分离核心类型(1adybug 推荐)

将核心业务逻辑类型与辅助属性分离:

type ItemCore<T> = {
value: T
} & (T extends number ? { render?: (value: T) => number } : { render: (value: T) => number })

type Item<T> = ItemCore<T> & { key: string }

// 为核心类型提供单独的函数
function getValueCore<T>(item: ItemCore<T>): number {
return item.render ? item.render(item.value) : (item.value as number)
}

方案四:使用工厂函数

创建辅助函数来构造完整对象:

function createItem<T>(value: T, render: T extends number ? ((value: T) => number) | undefined : (value: T) => number, key?: string): Item<T> {
return {
key: key || "default",
value,
render,
} as Item<T>
}

const a = getValue(createItem(1, v => v))

最佳实践建议

1. 优先考虑 API 设计

在设计类型时,考虑哪些属性是核心的,哪些是辅助的:

// 好的设计:核心属性在前,辅助属性可选
type GoodItem<T> = {
value: T
render?: T extends number ? (value: T) => number : (value: T) => number
id?: string
metadata?: Record<string, any>
}

// 避免的设计:辅助属性必需
type BadItem<T> = {
id: string // 辅助属性但是必需
metadata: Record<string, any> // 可能不相关但必需
value: T
render?: (value: T) => number
}

2. 合理使用类型推断

不要过度依赖类型推断,在复杂场景下适当使用显式类型:

// 简单场景:依赖推断
const simple = getValue({ key: "something", value: 1 })

// 复杂场景:显式指定
const complex = getValue<ComplexType>({
key: "something",
value: complexData,
render: customRenderer,
})

3. 提供类型友好的工厂函数

为复杂类型提供便捷的构造函数:

export const ItemBuilder = {
forNumber: (value: number, key?: string, render?: (v: number) => number) => ({
key: key || `num_${value}`,
value,
render,
}),

forString: (value: string, key?: string, render: (v: string) => number) => ({
key: key || `str_${value}`,
value,
render,
}),
}

总结

TypeScript 的类型推断看似神秘,实际上遵循着严格的逻辑:

  1. 结构完整性优先:TypeScript 需要完整的对象结构才能进行类型推断
  2. 安全性考虑:宁可推断失败也不愿意产生不安全的类型
  3. 复杂性处理:条件类型等复杂特性会让推断更加保守

理解这些原理有助于我们:

  • 设计更好的类型定义
  • 写出更类型友好的代码
  • 在推断失败时快速定位问题
  • 选择合适的解决方案

下次遇到类似问题时,你就知道这不是 TypeScript 的 bug,而是它保守而明智的设计选择。通过合理的类型设计和适当的显式注解,我们可以既享受类型推断的便利,又保证代码的类型安全。


@internationalized/date 中的 ZonedDateTime、CalendarDate、CalendarDateTime、Time 几种时间类型的区别

· 阅读需 4 分钟
Gemini
Google Gemini
1adybug
子虚伊人

以下内容均为 Gemini 2.5 Pro 生成

@internationalized/date:深入解析四种核心时间类型

在 JavaScript 中处理日期和时间常常因为时区、日历系统和国际化等问题而变得复杂。@internationalized/date 库提供了一套强大且设计精良的工具来应对这些挑战。该库的核心是其不可变的时间对象:ZonedDateTimeCalendarDateCalendarDateTimeTime。理解它们之间的差异对于正确和高效地处理日期与时间至关重要。

以下是这四种时间类型的详细区别:


CalendarDate:纯粹的日期

CalendarDate 对象表示一个不含任何时间信息的特定日期。它非常适合用来表示与一天中的具体时间无关的事件,例如:

  • 生日
  • 节假日
  • 全天日历事件

核心特性:

  • 仅包含日期部分: 年、月、日。
  • 无时间概念: 不涉及小时、分钟或秒。
  • 无时区信息: 它代表的是一个日历上的日期,与任何特定时区无关。
  • 支持多种日历系统: 可以表示公历(Gregorian)以及希伯来(Hebrew)、伊斯兰(Islamic)等多种日历。

示例:

import { CalendarDate } from "@internationalized/date"

// 表示 2025 年 7 月 24 日
const birthday = new CalendarDate(2025, 7, 24)

console.log(birthday.toString()) // 输出: 2025-07-24

Time:纯粹的时间

CalendarDate 相对,Time 对象表示一个不含任何日期信息的特定时间。它适用于表示与具体哪一天无关的时间点,例如:

  • 每天的闹钟时间
  • 商店的营业时间

核心特性:

  • 仅包含时间部分: 小时、分钟、秒和毫秒。
  • 无日期概念: 不涉及年、月、日。
  • 无时区信息: 它代表的是时钟上的一个时间,与任何特定时区无关。

示例:

import { Time } from "@internationalized/date"

// 表示上午 9 点 30 分
const openingHour = new Time(9, 30)

console.log(openingHour.toString()) // 输出: 09:30:00

CalendarDateTime:日期与时间的结合

CalendarDateTime 对象是 CalendarDateTime 的结合体,它表示一个具体的日期和时间,但 不包含任何时区信息。这在表示一个特定时间点,但该时间点在不同时区下可能对应不同实际时刻的场景中非常有用。

核心特性:

  • 包含日期和时间部分: 年、月、日、小时、分钟、秒。
  • 无时区信息: 它是一个“本地”的日期时间,其确切的时间点取决于观察者所在的时区。
  • 支持多种日历系统。

示例:

import { CalendarDateTime } from "@internationalized/date"

// 表示 2025 年 10 月 26 日上午 10 点
const localEvent = new CalendarDateTime(2025, 10, 26, 10)

console.log(localEvent.toString()) // 输出: 2025-10-26T10:00:00

这个时间可以被理解为“任何时区的上午10点”。


ZonedDateTime:完整的、带时区的日期时间

ZonedDateTime 是这四种类型中信息最全面的对象。它表示一个在 特定时区 下的精确日期和时间。这是处理需要明确、无歧义时间点的最佳选择,例如:

  • 航班起飞时间
  • 国际会议的安排
  • 任何需要考虑夏令时和时区转换的场景

核心特性:

  • 包含日期和时间部分。
  • 包含明确的时区信息: 它与一个IANA时区标识符(如 "America/New_York")相关联。
  • 准确无歧义: 它代表了地球上的一个精确时刻。
  • 自动处理夏令时: 库会根据指定的时区正确处理夏令时的变化。
  • 支持多种日历系统。

示例:

import { getLocalTimeZone, ZonedDateTime } from "@internationalized/date"

// 表示在纽约时区 2025 年 11 月 5 日下午 8 点
const flightDeparture = new ZonedDateTime(
2025,
11,
5,
"America/New_York",
20, // 20 点
0,
)

console.log(flightDeparture.toString()) // 输出类似: 2025-11-05T20:00:00-05:00[America/New_York]

// 获取用户本地时区的当前时间
const nowInLocal = new ZonedDateTime(new Date(), getLocalTimeZone())

总结与选择指南

类型包含日期包含时间包含时区适用场景
CalendarDate生日、全天事件、不关心具体时间的日期
Time每日重复的闹钟、营业时间
CalendarDateTime本地化的日历事件,时间点相对于观察者时区
ZonedDateTime航班时刻、国际会议、任何需要精确时刻的场景

通过理解这四种类型的不同职责,开发者可以更加精确和可靠地在应用程序中处理各种复杂的日期和时间逻辑,避免由时区和日历系统带来的常见错误。