共同編集とレビューのプラグイン
エディターを実務で使える文書ワークフローにする 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 のストアに入ります。
リビジョン履歴
サンドボックス化されたプレビュー用の 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();
スレッド形式のコメント
コメントは選択範囲に結び付けられ、強調表示とサイドバーのスレッドとして表示されます。作成、返信、解決、削除ができます。
設定
API