跳至主要內容
進階

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 可以讓你的貼文在所有客戶端正確顯示對話脈絡, 並確保相關用戶收到通知。

已複製連結
已複製到剪貼簿