跳至主要內容

NIP-54: Wiki

在 Nostr 上建立去中心化的維基百科式知識庫

概述

NIP-54 定義了在 Nostr 上建立維基(Wiki)風格內容的標準。使用者可以建立和編輯 主題條目,透過社群協作建立去中心化的知識庫。每個條目都是可定址事件, 支援版本歷史和多作者貢獻。

事件類型

Kind 說明 類型
30818 Wiki 條目 可定址(可替換)

事件結構

Content

條目內容使用 Markdown 格式,支援標題、列表、連結、程式碼區塊等。 可使用 nostr: URI 連結到其他 Nostr 內容。

必要標籤

標籤 說明 範例
d 條目識別符(主題名稱,小寫) ["d", "bitcoin"]

可選標籤

標籤 說明 範例
title 顯示標題 ["title", "Bitcoin"]
summary 簡短摘要 ["summary", "去中心化數位貨幣"]
a 引用其他條目 ["a", "30818:pubkey:lightning-network"]
t 主題標籤 ["t", "cryptocurrency"]
published_at 發布時間戳 ["published_at", "1704067200"]

主題命名規則

d 標籤的值應遵循以下規則:

  • 使用小寫字母
  • 空格替換為連字符 -
  • 移除特殊字符
  • 保持簡潔明確

範例

主題 d 標籤值
Bitcoin bitcoin
Lightning Network lightning-network
Nostr Protocol nostr-protocol
中本聰 satoshi-nakamoto

範例

基本 Wiki 條目

{
  "kind": 30818,
  "content": "# Bitcoin\n\nBitcoin 是一種去中心化的數位貨幣,由中本聰於 2008 年提出,2009 年正式上線。\n\n## 特點\n\n- **去中心化**:沒有中央機構控制\n- **有限供應**:總量限制為 2100 萬枚\n- **開源**:任何人都可以審查程式碼\n\n## 相關主題\n\n- [[lightning-network|閃電網路]]\n- [[satoshi-nakamoto|中本聰]]\n\n## 參考資料\n\n- [Bitcoin 白皮書](https://bitcoin.org/bitcoin.pdf)",
  "tags": [
    ["d", "bitcoin"],
    ["title", "Bitcoin"],
    ["summary", "去中心化的點對點電子現金系統"],
    ["t", "cryptocurrency"],
    ["t", "區塊鏈"],
    ["published_at", "1704067200"]
  ]
}

引用其他條目

{
  "kind": 30818,
  "content": "# 閃電網路\n\n閃電網路是建立在 [[bitcoin|Bitcoin]] 之上的二層擴展方案。\n\n## 工作原理\n\n透過支付通道實現快速、低成本的交易...\n\n## 相關技術\n\n- [[htlc|HTLC]]\n- [[payment-channel|支付通道]]",
  "tags": [
    ["d", "lightning-network"],
    ["title", "閃電網路"],
    ["summary", "Bitcoin 的二層擴展解決方案"],
    ["a", "30818:author-pubkey:bitcoin", "wss://relay.example.com"],
    ["a", "30818:author-pubkey:htlc", "wss://relay.example.com"],
    ["t", "lightning"],
    ["t", "layer2"]
  ]
}

在內容中可以使用 Wiki 風格的連結語法:

[[topic-name]]           → 連結到同一作者的條目
[[topic-name|顯示文字]]   → 自訂顯示文字

TypeScript 實作

建立 Wiki 條目

import { finalizeEvent } from 'nostr-tools';

interface WikiArticle {
  topic: string;
  title: string;
  content: string;
  summary?: string;
  tags?: string[];
  references?: string[]; // 其他條目的座標
}

function createWikiEvent(
  article: WikiArticle,
  secretKey: Uint8Array
) {
  const tags: string[][] = [
    ['d', article.topic],
    ['title', article.title],
  ];

  if (article.summary) {
    tags.push(['summary', article.summary]);
  }

  article.references?.forEach((ref) => {
    tags.push(['a', ref]);
  });

  article.tags?.forEach((tag) => {
    tags.push(['t', tag]);
  });

  tags.push(['published_at', Math.floor(Date.now() / 1000).toString()]);

  const event = {
    kind: 30818,
    content: article.content,
    tags,
    created_at: Math.floor(Date.now() / 1000),
  };

  return finalizeEvent(event, secretKey);
}

// 使用範例
const wikiEvent = createWikiEvent(
  {
    topic: 'nostr-protocol',
    title: 'Nostr 協議',
    content: '# Nostr\n\nNostr 是一個簡單、開放的去中心化社交協議...',
    summary: '去中心化社交協議',
    tags: ['protocol', 'decentralized', 'social'],
    references: ['30818:pubkey:bitcoin', '30818:pubkey:lightning-network'],
  },
  secretKey
);

解析 Wiki 條目

interface ParsedWikiArticle {
  topic: string;
  title: string;
  content: string;
  summary?: string;
  tags: string[];
  references: string[];
  publishedAt?: number;
  author: string;
}

function parseWikiEvent(event: any): ParsedWikiArticle | null {
  if (event.kind !== 30818) return null;

  const getTag = (name: string) => {
    const tag = event.tags.find((t: string[]) => t[0] === name);
    return tag?.[1];
  };

  const getAllTags = (name: string) => {
    return event.tags
      .filter((t: string[]) => t[0] === name)
      .map((t: string[]) => t[1]);
  };

  const topic = getTag('d');
  if (!topic) return null;

  const publishedAtStr = getTag('published_at');

  return {
    topic,
    title: getTag('title') || topic,
    content: event.content,
    summary: getTag('summary'),
    tags: getAllTags('t'),
    references: getAllTags('a'),
    publishedAt: publishedAtStr ? parseInt(publishedAtStr) : undefined,
    author: event.pubkey,
  };
}

解析 Wiki 連結

interface WikiLink {
  topic: string;
  displayText: string;
  start: number;
  end: number;
}

function parseWikiLinks(content: string): WikiLink[] {
  const regex = /\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g;
  const links: WikiLink[] = [];

  let match;
  while ((match = regex.exec(content)) !== null) {
    links.push({
      topic: match[1].trim(),
      displayText: match[2]?.trim() || match[1].trim(),
      start: match.index,
      end: match.index + match[0].length,
    });
  }

  return links;
}

function renderWikiContent(
  content: string,
  authorPubkey: string,
  onLinkClick?: (topic: string) => void
): string {
  const links = parseWikiLinks(content);

  // 從後往前替換,避免位置偏移
  let result = content;
  for (let i = links.length - 1; i >= 0; i--) {
    const link = links[i];
    const href = `/wiki/${authorPubkey}/${link.topic}`;
    const replacement = `${link.displayText}`;
    result = result.slice(0, link.start) + replacement + result.slice(link.end);
  }

  return result;
}

搜尋 Wiki 條目

import { SimplePool } from 'nostr-tools';

interface WikiSearchOptions {
  topic?: string;
  author?: string;
  tags?: string[];
  limit?: number;
}

async function searchWikiArticles(
  pool: SimplePool,
  relays: string[],
  options: WikiSearchOptions = {}
): Promise<ParsedWikiArticle[]> {
  const filter: any = {
    kinds: [30818],
    limit: options.limit || 50,
  };

  if (options.author) {
    filter.authors = [options.author];
  }

  if (options.topic) {
    filter['#d'] = [options.topic];
  }

  if (options.tags && options.tags.length > 0) {
    filter['#t'] = options.tags;
  }

  const events = await pool.querySync(relays, filter);

  return events
    .map(parseWikiEvent)
    .filter((a): a is ParsedWikiArticle => a !== null)
    .sort((a, b) => (b.publishedAt || 0) - (a.publishedAt || 0));
}

// 取得特定條目的所有版本
async function getArticleVersions(
  pool: SimplePool,
  relays: string[],
  topic: string,
  author?: string
): Promise<ParsedWikiArticle[]> {
  const filter: any = {
    kinds: [30818],
    '#d': [topic],
  };

  if (author) {
    filter.authors = [author];
  }

  const events = await pool.querySync(relays, filter);

  return events
    .map(parseWikiEvent)
    .filter((a): a is ParsedWikiArticle => a !== null)
    .sort((a, b) => (b.publishedAt || 0) - (a.publishedAt || 0));
}

React 元件

import ReactMarkdown from 'react-markdown';

interface WikiArticleViewProps {
  article: ParsedWikiArticle;
  onNavigate?: (topic: string) => void;
}

function WikiArticleView({ article, onNavigate }: WikiArticleViewProps) {
  // 處理 Wiki 連結
  const processedContent = article.content.replace(
    /\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g,
    (_, topic, text) => `[${text || topic}](/wiki/${article.author}/${topic})`
  );

  return (
    <article className="wiki-article">
      <header>
        <h1>{article.title}</h1>
        {article.summary && (
          <p className="summary">{article.summary}</p>
        )}
        <div className="meta">
          <span className="author">作者: {article.author.slice(0, 8)}...</span>
          {article.publishedAt && (
            <span className="date">
              {new Date(article.publishedAt * 1000).toLocaleDateString()}
            </span>
          )}
        </div>
        <div className="tags">
          {article.tags.map((tag) => (
            <span key={tag} className="tag">#{tag}</span>
          ))}
        </div>
      </header>
      <div className="content">
        <ReactMarkdown>{processedContent}</ReactMarkdown>
      </div>
    </article>
  );
}

function WikiSearch() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState<ParsedWikiArticle[]>([]);

  const handleSearch = async () => {
    const articles = await searchWikiArticles(pool, relays, {
      tags: [query.toLowerCase()],
      limit: 20,
    });
    setResults(articles);
  };

  return (
    <div className="wiki-search">
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="搜尋 Wiki 條目..."
      />
      <button onClick={handleSearch}>搜尋</button>
      <div className="results">
        {results.map((article) => (
          <div key={`${article.author}:${article.topic}`} className="result">
            <h3>{article.title}</h3>
            <p>{article.summary}</p>
          </div>
        ))}
      </div>
    </div>
  );
}

使用場景

社群知識庫

  • Bitcoin/加密貨幣百科
  • 技術文檔和教程
  • 專業領域知識庫

協作編輯

  • 多人貢獻同一主題
  • 版本歷史追蹤
  • 分散式知識管理

個人筆記

  • 個人知識管理系統
  • 學習筆記整理
  • 研究資料彙整

最佳實踐

  • 規範命名:使用一致的主題命名規則
  • 充分引用:使用 Wiki 連結連接相關條目
  • 提供摘要:方便搜尋和預覽
  • 版本管理:重大修改時更新 published_at
  • 標籤分類:使用標籤幫助發現
  • NIP-01:基本協議 - 事件格式
  • NIP-23:長文內容 - 類似格式
  • NIP-27:文字引用 - nostr: URI

參考資源

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