進階
NIP-10 回覆與標記
深入了解 Nostr 的對話串結構,回覆、標記和事件引用機制。
10 分鐘
什麼是 NIP-10?
NIP-10 定義了 Nostr 中對話串(threading)的結構,規範如何在事件中標記回覆、 引用和提及。這讓客戶端可以正確顯示對話脈絡和通知相關用戶。
核心概念: NIP-10 使用 "e" 標籤引用事件、"p" 標籤標記用戶,並通過 marker 區分 根事件(root)、回覆對象(reply)和一般引用(mention)。
標籤類型
| 標籤 | 用途 | 格式 |
|---|---|---|
| "e" | 引用事件 | ["e", "<event-id>", "<relay>", "<marker>"] |
| "p" | 標記用戶 | ["p", "<pubkey>", "<relay>"] |
事件引用 Marker
| Marker | 說明 | 數量 |
|---|---|---|
| "root" | 對話串的起始事件(原始貼文) | 最多 1 個 |
| "reply" | 直接回覆的目標事件 | 最多 1 個 |
| "mention" | 一般引用(不是回覆) | 任意數量 |
對話串結構
# 對話串範例
原始貼文 (A)
├── 回覆 (B) - reply: A, root: A
│ ├── 回覆 (C) - reply: B, root: A
│ └── 回覆 (D) - reply: B, root: A
└── 回覆 (E) - reply: A, root: A
└── 回覆 (F) - reply: E, root: A
# 所有回覆都保留 root 指向原始貼文
# reply 指向直接回覆的對象 事件範例
原始貼文
{
"id": "aaa...",
"kind": 1,
"content": "這是一篇原始貼文",
"tags": [] // 原始貼文沒有 e 標籤
} 直接回覆
{
"id": "bbb...",
"kind": 1,
"content": "這是對原始貼文的回覆",
"tags": [
["e", "aaa...", "wss://relay.com", "root"],
["e", "aaa...", "wss://relay.com", "reply"],
["p", "<原作者pubkey>", "wss://relay.com"]
]
}
// 當 root 和 reply 相同時,表示直接回覆原始貼文 巢狀回覆
{
"id": "ccc...",
"kind": 1,
"content": "這是對回覆的回覆",
"tags": [
["e", "aaa...", "wss://relay.com", "root"], // 原始貼文
["e", "bbb...", "wss://relay.com", "reply"], // 回覆對象
["p", "<原作者pubkey>"],
["p", "<回覆者pubkey>"]
]
} 引用(不是回覆)
{
"id": "ddd...",
"kind": 1,
"content": "看看這篇有趣的貼文:nostr:nevent1...",
"tags": [
["e", "aaa...", "wss://relay.com", "mention"]
]
}
// mention 表示引用但不是回覆
// 不會出現在原貼文的回覆串中 用戶標記
{
"kind": 1,
"content": "嗨 nostr:npub1abc... 你好嗎?",
"tags": [
["p", "abc...", "wss://relay.com"]
]
}
// "p" 標籤用於:
// 1. 回覆時標記原作者
// 2. 在內容中 @ 提及用戶
// 3. 讓被標記的用戶收到通知 程式碼範例
建立回覆事件
function createReply(originalEvent, replyContent, relay) {
// 找出 root 事件
const rootTag = originalEvent.tags.find(
t => t[0] === 'e' && t[3] === 'root'
)
const rootId = rootTag ? rootTag[1] : originalEvent.id
// 建立回覆事件
const reply = {
kind: 1,
created_at: Math.floor(Date.now() / 1000),
content: replyContent,
tags: [
['e', rootId, relay, 'root'],
['e', originalEvent.id, relay, 'reply'],
['p', originalEvent.pubkey, relay]
]
}
// 加入原對話串中提及的用戶
originalEvent.tags
.filter(t => t[0] === 'p')
.forEach(t => {
if (!reply.tags.some(rt => rt[1] === t[1])) {
reply.tags.push(['p', t[1], t[2] || ''])
}
})
return reply
} 解析對話串
function parseThread(event) {
const eTags = event.tags.filter(t => t[0] === 'e')
const pTags = event.tags.filter(t => t[0] === 'p')
// 使用 marker 解析
const root = eTags.find(t => t[3] === 'root')
const reply = eTags.find(t => t[3] === 'reply')
const mentions = eTags.filter(t => t[3] === 'mention')
return {
rootId: root?.[1] || null,
replyToId: reply?.[1] || null,
mentionedEvents: mentions.map(t => t[1]),
mentionedUsers: pTags.map(t => t[1]),
isReply: !!(root || reply),
isTopLevel: !root && !reply
}
}
// 使用
const thread = parseThread(event)
if (thread.isReply) {
console.log('回覆至:', thread.replyToId)
console.log('對話串根:', thread.rootId)
} 查詢對話串
// 查詢特定貼文的所有回覆
["REQ", "replies", {
"kinds": [1],
"#e": ["<event-id>"]
}]
// 查詢整個對話串(所有引用 root 的事件)
["REQ", "thread", {
"kinds": [1],
"#e": ["<root-event-id>"]
}]
// 查詢標記我的事件(通知)
["REQ", "mentions", {
"kinds": [1],
"#p": ["<my-pubkey>"]
}] 舊版格式(位置型)
舊版 NIP-10 使用位置來區分 root 和 reply,現已不推薦:
// 舊版格式(不推薦)
{
"tags": [
["e", "root-id"], // 第一個 = root
["e", "reply-id"] // 最後一個 = reply
]
}
// 新版格式(推薦)
{
"tags": [
["e", "root-id", "", "root"],
["e", "reply-id", "", "reply"]
]
} 相容性: 客戶端應該能解析兩種格式。發送時使用新版 marker 格式, 接收時先檢查 marker,沒有則回退到位置型解析。
最佳實踐
應該做
- • 使用 marker 明確標記類型
- • 回覆時包含 root 和 reply
- • 標記被回覆的用戶
- • 提供中繼器提示
避免
- • 只用位置型格式
- • 省略 root 標籤
- • 混淆 reply 和 mention
- • 忘記標記用戶
提示: 正確使用 NIP-10 可以讓你的貼文在所有客戶端正確顯示對話脈絡, 並確保相關用戶收到通知。
已複製連結