← すべてのドキュメント
共同編集とレビュー

共同編集とレビューのプラグイン

エディターを実務で使える文書ワークフローにする 6 つのプラグインです。スラッシュコマンド、@メンション、変更履歴、スレッド形式のコメント、リビジョン履歴、Yjs によるプレゼンスがあります。すべて標準ライセンスに含まれており、上位の製品を購入する必要はありません。

スラッシュコマンド

空の行で(または空白の後に)/ を入力すると、インラインのコマンドピッカーが開きます。15 個の組み込みのブロック(H1–H3、箇条書き/番号付きリスト、引用、コード、区切り線、表、画像、リンク、絵文字、今日の日付)と、登録されているすべての AI Toolkit のアクションを使えます。

設定

{
    slashCommandEnabled: true,          // default when this plugin is loaded
    slashCommandIncludeAi: true         // pull aiToolkitActions into the menu
}

API

editor.slashCommands.register({ id, label, keywords, run });
editor.slashCommands.remove(id);
editor.slashCommands.list();
editor.slashCommands.open(); / .close();

@メンション

@、#、[[wiki]]、または任意の独自のプレフィックスに対応するトリガー文字の仕組みです。非同期のデータソース(デバウンスされ、読み込み中の状態を表示)に対応しています。挿入されるのは分割できない編集不可のピルで、Backspace でチップ全体が削除されます。

トリガーを登録する

editor.mentions.register({
    trigger: "@",
    insertClass: "rte-mention-person",
    search: function (query, done) {
        fetch("/api/users?q=" + encodeURIComponent(query))
            .then(r => r.json())
            .then(done);                // Array<{ id, name, color? }>
    },
    renderItem: function (user) { return user.name; },
    renderInsert: function (user) { return "@" + user.name; }
});

変更履歴(人による提案モード)

Word と同じ形式の提案です。挿入は現在のユーザーの色の下線付きで表示され、削除には取り消し線が付きます(承認されるまでは削除されません)。連続した削除は 1 つの span にまとめられ、自分が入力したばかりの挿入の中で Backspace を押すと、その挿入が短くなります。

設定

{
    trackChangesEnabled: true,
    currentUser: { id: "maya", name: "Maya Patel", color: "#9333ea" },
    reviewCanDecide: (entry, action, user) => user.role === "editor"
}

レビュードロワーから、作成者ごとに承認/却下できます。アプリケーションのレビュー担当者のポリシーを適用するには reviewCanDecide を使ってください。拒否された判断では提案は保留のまま残り、review_decision_denied が発生します。承認または却下された提案には、監査証跡として decidedBy が保存されます。AI の提案とも併用でき、どちらも同じ editor.reviewLedger のストアに入ります。

スレッド形式のコメント

コメントは選択範囲に結び付けられ、強調表示とサイドバーのスレッドとして表示されます。作成、返信、解決、削除ができます。

設定

{
    commentsEnabled: true,
    currentUser: { id: "alice", name: "Alice Lee", color: "#2563eb" }
}

API

editor.comments.add(range, text);
editor.comments.reply(threadId, text);
editor.comments.resolve(threadId);
editor.comments.delete(threadId);
editor.comments.list();

リビジョン履歴

サンドボックス化されたプレビュー用の iframe と、LCS ベースの行単位の差分を備えたスナップショットのブラウザーです。手動のスナップショットと、デバウンスされた自動のスナップショットがあります。保存先は既定では localStorage で、サーバーのエンドポイントに POST することもできます。

設定

{
    revisionHistoryEnabled: true,
    revisionHistoryMaxEntries: 50,
    revisionHistoryAutoSnapshotMs: 10000,
    revisionHistoryUrl: "/api/revisions",       // optional
    revisionHistoryRequestHeaders: () => ({
        "X-CSRF-Token": readCsrfToken()
    }),
    onRevisionHistorySyncError: ({ reason, status }) => {
        reportSyncProblem(reason, status);
    }
}

既存のセッションと CSRF 対策を使う、同一オリジンのエンドポイントを使ってください。ヘッダーの値は関数で指定できるので、有効期間の短い偽造防止トークンをリクエスト時に読み取れます。有効期間の長い API キーをエディターの設定に置いてはいけません。

API

editor.revisionHistory.snapshot(label?, metadata?);
editor.revisionHistory.promptAndSnapshot(defaultLabel?);   // native prompt + snapshot
editor.revisionHistory.rename(id, label);
editor.revisionHistory.list();                             // all snapshots
editor.revisionHistory.listNamed();                        // user-labeled only
editor.revisionHistory.diff(idA, idB);                     // { lines, oldText, newText }
editor.revisionHistory.restore(id);
editor.revisionHistory.delete(id);
editor.revisionHistory.openBrowser();

Yjs のプレゼンスと同時入力

リアルタイムのカーソル、プレゼンスのアバター、共有のレビュー記録があります。Yjs はピア依存関係で、プラグインに同梱されることはありません。Yjs とプロバイダー(WebSocket / WebRTC / Hocuspocus)を自分で読み込んで渡します。

アタッチする

import * as Y from "yjs";
import { WebsocketProvider } from "y-websocket";

const ydoc = new Y.Doc();
const provider = new WebsocketProvider("wss://collab.example.com", docId, ydoc);

editor.collab.attach({
    doc: ydoc,
    provider: provider,
    user: { id: "alice", name: "Alice", color: "#2563eb" },
    textSync: "crdt",      // per-node Yjs CRDT; load crdt-engine.min.js first
    requireCrdt: true       // do not silently fall back to legacy snapshots
});

textSync: "crdt"。ノード単位のバインディングは、編集可能な DOM を Y.XmlFragment に反映し、従来の本文全体のリセットなしに、同時に行われたテキストと書式の変更を保持します。アタッチした後に editor.collab.getStatus() を呼び出して、有効なモードと、フォールバックした場合はその理由を記録してください。

本番環境の WebSocket のエッジ

同梱の共同編集サーバーは、署名付きでルーム単位のトークン、永続的な LevelDB の状態、ブラウザーのオリジンの許可リスト、ルームごとの接続数の上限に対応しています。本番環境では 4 つすべての制御を使ってください。TLS はプロキシで終端し、誰でも接続できる WebSocket のエンドポイントを公開しないでください。

RTE_COLLAB_SECRET="use-a-long-random-secret"
RTE_COLLAB_PERSIST="/var/lib/rte-collab"
RTE_COLLAB_ALLOWED_ORIGINS="https://app.example.com,https://admin.example.com"
RTE_COLLAB_MAX_CONNECTIONS_PER_ROOM="100"
RTE_COLLAB_REDIS_URL="redis://redis:6379"
node server.js

クラウドのトークンのエンドポイントは、1 つのルーム用の有効期間の短い認証情報を発行する前に、文書の所有権を確認します。サーバーは、ブラウザーのオリジンがない場合や許可リストにない場合は HTTP 403 で拒否し、1 つのルームに集中した超過分のピアは HTTP 429 で拒否します。すべてのレプリカに同じ Redis の URL を設定すると、スティッキーセッションなしで文書とアウェアネスの更新を中継できます。アクティブなルームに参加したレプリカは、最初のクライアントを同期する前に、CRDT の状態の完全なスナップショットを要求します。設定した Redis が使えない場合、/healthz は HTTP 503 と status: "degraded" を返します。再接続した後も、内部の Pub/Sub の往復によって購読が再び受信していることが確認されるまで、準備状態は degraded のままです。

すべてのピアは信頼できない入力である

CRDT は、ノードを実際の文書に直接書き込むことでリモートの編集を適用します。貼り付けの処理や setHTMLCode を通らないので、この 2 つの入り口だけでフィルタリングしているエディターは、共同編集者の <iframe srcdoc> をほかのすべての参加者のブラウザーで平然と実行し、さらにそれを先へ複製してしまいます。共有文書の共著者は、貼り付けられた HTML とまったく同じように扱ってください。

sanitizer.js はその経路をふさぎます。編集領域に挿入されるノードと属性を監視し、何によって生じたものであっても、実行可能なものは実行される前にフィルタリングします。テキストノードはスキップされ、危険なものが現れたときにだけフィルターが働くので、通常の入力では負荷はかかりません。同期のレイヤーがすでに上流でサニタイズしている場合は、sanitizerGuardLiveDom: false で無効にできます。このチェックは実際に動作する検証ページで自分で実行できます。

リリース時のテストでは、分離された本物の Redis 互換プロセスに対して 2 つの独立した WebSocket サーバーのプロセスを起動し、遅れての参加、双方向のコンテンツとプレゼンス、ルーム/トークンの分離、バックエンドの喪失、準備状態の回復、回復後の最初の編集を検証します。これはベンダーが実施した結合テストの結果であり、マネージドサービスの SLA や外部の信頼性認証ではありません。

共有のレビュー記録

AI の提案、人による変更履歴の提案、コメントは、すべて 1 つのストア editor.reviewLedger に集まります。そのため、レビュードロワーはそれらを混在させて表示でき、出どころにかかわらず、承認/却下は同じように動作します。

API

editor.reviewLedger.add(entry);            // { id, changeType, author, text, replies[], ... }
editor.reviewLedger.update(id, patch);
editor.reviewLedger.remove(id);
editor.reviewLedger.get(id);
editor.reviewLedger.list();
editor.reviewLedger.refreshPanel();