npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@carys-cloud/collab-editor

v0.3.0

Published

可嵌入的协同文档编辑器(TipTap v3 + Yjs),carys-doc-collab 前端交付包。

Readme

@carys-cloud/collab-editor

可嵌入的 React 协同文档 + 故事图 / 代码图编辑器。在你自己的 React 应用里 import 即用, 用宿主的 React(peer,不自带),模型经 BYO aiProvider 注入——本包与协同引擎都不接触你的 key。

特性

  • 实时协同:TipTap v3 + Yjs + y-websocket,多人同房编辑、在线光标、断线重连。
  • 故事图 / 代码图:正文 ⇄ 结构图双向联动(@xyflow/react 画布)。graph.profile.type === 'codebase' 自动进入代码图模式(符号/调用邻域),否则是故事图模式(拍 ⇄ 节点 ⇄ 场景)。
  • BYO 模型:AI 编辑 / 摘要 / 拍·节点解读 / 读故事出图谱全走你注入的 AiProvider,key、prompt、端点都在你手里。
  • 黑白主题:--cc-* CSS 变量跟随 <html data-theme="dark|light">,无 attr 时回落 prefers-color-scheme。
  • 中英双语:locale?: 'zh' | 'en',经 LocaleContext 下发到所有子组件(含富块 NodeView)。
  • 富块:HTML 嵌入 / Mermaid 图 / KaTeX 公式,皆为 TipTap Node。
  • 字体色 / 背景色:选中气泡里的 A 套用上次用色,⌄ 展开调色板(8 字体色 + 16 背景色 + 恢复默认)。 字体色是 TextStyle + Color(<span style="color">),背景色是 multicolor Highlight(<mark data-color>)。 两者都随 Yjs 协同同步;但 Markdown 里表达不了颜色,getMarkdown() 快照只保留文字与 ==…== 结构。

现在主推 npm 组件(同进程):宿主直接渲染 <CollabDocWorkspace>,用 props/回调交互。 iframe 嵌入轨(bridge + postMessage)仍保留,但不是主推形态。


安装

pnpm add @carys-cloud/collab-editor
# peer:用宿主自己的 React,不由本包携带
pnpm add react react-dom          # ^18.3.1 || ^19.0.0

React 19(单实例)已实测可跑,无双 React hooks 冲突——只要宿主与本包共享同一份 React 即可。

引入样式(一次即可):

import '@carys-cloud/collab-editor/styles.css';
// 若用到公式富块(MathBlock),再引 katex 样式(宿主自行 add katex)
import 'katex/dist/katex.min.css';

快速上手

纯文档模式(不传 graph → 退化为 CollabDocEditor)

CollabDocWorkspace 不传 graph 时自身退化为纯协同富文档编辑器,同一套协同/富块/AI/主题内核。

import { CollabDocWorkspace, createAnonAuth } from '@carys-cloud/collab-editor';
import '@carys-cloud/collab-editor/styles.css';

export function Doc() {
  return (
    <CollabDocWorkspace
      docId="my-doc"                                  // = 协同房 id / WS roomname
      user={{ id: 'u1', name: '张三' }}
      adapters={{
        transport: { wsUrl: 'ws://127.0.0.1:8090' },  // CRDT 引擎地址,内部自动拼 /collab
        auth: createAnonAuth(),                        // 本地/受信网匿名连接
      }}
    />
  );
}

不需要故事图外壳时也可以直接用 CollabDocEditor(二者对文档模式等价)。

故事图 / 代码图模式(传 graph → 进入工作台)

import { CollabDocWorkspace, createAnonAuth, SAMPLE_GRAPH, SAMPLE_STORY_MD } from '@carys-cloud/collab-editor';
import '@carys-cloud/collab-editor/styles.css';

export function Workspace() {
  return (
    <CollabDocWorkspace
      docId="story-1"
      user={{ id: 'u1', name: '张三' }}
      adapters={{ transport: { wsUrl: 'ws://127.0.0.1:8090' }, auth: createAnonAuth() }}
      graph={SAMPLE_GRAPH}      // 传入即进入工作台三栏
      seed={SAMPLE_STORY_MD}    // 空房首次灌入的故事正文(仅文档为空时)
    />
  );
}

传 graph 后:故事模式是「大纲 · 正文 · 图谱」三栏并排;代码图模式(graph.profile.type === 'codebase')只保留 图谱 + 解读。图谱存进 Y.Doc 的 storygraph Map,多人改图 / AI 生成图谱都实时协同同步。


Adapters

依赖注入装配。除 transport 外全可选,缺失即降级:

| 字段 | 类型 | 缺失时 | |------|------|--------| | transport | TransportConfig({ wsUrl: string }) | 必填;缺失降级为只读占位(不白屏) | | auth | AuthAdapter | 匿名连接(WS 不带 ticket) | | aiProvider | AiProvider | AI 入口整组隐藏 | | docStore | DocStoreAdapter | 引擎自带 ystore 恢复正文 | | enterprise | unknown(占位,本包不实现) | 条款/审批 UI 整组隐藏 |

transport:wsUrl 是 CRDT 引擎基址(如 ws://127.0.0.1:8090),内部自动拼 /collab,最终连 ws://host/collab/{docId}?ticket=。

auth:五个工厂,都返回 AuthAdapter(契约就是 getTicket(docId): Promise<string | null>):

import {
  createAnonAuth,
  createBearerExchangeAuth,
  createCallbackAuth,
  createStaticAuth,
  createTicketAuth,
} from '@carys-cloud/collab-editor';

createAnonAuth();                       // 始终返回 null → WS 不带 ticket(引擎 COLLAB_ALLOW_ANON=1 时放行,本地演示默认)
createStaticAuth(myTicket);             // 宿主已自己调过签发端点,把现成 ticket 直接喂进来
createCallbackAuth(async (docId) => {    // internal_ticket 生产:只调用宿主自己的后端
  const res = await fetch('/api/collab-ticket', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ docId }),
  });
  if (!res.ok) throw new Error(`ticket: ${res.status}`);
  return (await res.json()).ticket;
});
createTicketAuth({                      // 仅本地/受信服务端:直调 collab /api/v1/tickets
  issuerUrl: '',                        // 默认同源,配合 dev proxy;内部拼 /api/v1/tickets
  sub: 'user-1',                        // 身份,写进 ticket 的 sub
  perm: 'edit',                         // 'view' | 'comment' | 'edit' | 'manage',默认 edit
  enterpriseId: 'ent-1',               // 可选,企业隔离
  internalToken: '...',                 // 可选,签发端点要求的入站内部 token
});
createBearerExchangeAuth({              // trusted_jwt 生产:复用宿主 Bearer JWT
  issuerUrl: 'https://collab.example.com',
  getBearerToken: () => authStore.getState().token,
});

getTicket 失败时 transport 会退回匿名连接(生产 COLLAB_ALLOW_ANON=0 时会被服务端拒绝), 不白屏。部署可以按宿主需求启用其中一种或同时启用两种:

  • trusted_jwt:使用 createBearerExchangeAuth,collab 验证宿主 JWT;宿主无需新增协同接口。
  • internal_ticket:使用 createCallbackAuth 调宿主后端;宿主后端完成 ACL 后,携带 COLLAB_INBOUND_INTERNAL_TOKEN 调 collab /api/v1/tickets。

严禁把 COLLAB_INBOUND_INTERNAL_TOKEN 放进浏览器、静态 bundle 或公开环境变量。 createTicketAuth({ internalToken }) 仅适合本地或受信服务端代码,不是生产浏览器方案。


BYO 模型(aiProvider)

编辑器只消费模型产出的 delta 流,怎么调模型由你实现。契约:

interface AiProvider {
  streamEdit(input: AiEditInput): AsyncIterable<string>;    // 选区改写 / 拍·节点 AI 解读(逐 delta)
  streamDraft(input: AiDraftInput): AsyncIterable<string>;  // 从提示起草(逐 delta)
  summarize(contentMarkdown: string): Promise<string>;      // 全文摘要(一次性)
  proposeAlternatives?(clauseText: string): Promise<string[]>;    // enterprise-only,未实现 → 入口隐藏
  analyzeStory?(storyMarkdown: string): Promise<StoryGraphDraft>; // 读故事出图谱,未实现 →「AI 生成图谱」按钮隐藏
}

推荐做法:继承 PromptedAiProvider,只实现 chatStream。 提示词层(edit/draft/summarize/analyze, 中英各一套、locale 感知)已在基类里拼好,你只负责「把 messages 送进模型」这一段传输:

import { PromptedAiProvider } from '@carys-cloud/collab-editor';
import type { ChatMessage, PromptLocale } from '@carys-cloud/collab-editor';

class MyAiProvider extends PromptedAiProvider {
  constructor(locale: PromptLocale, private token: string) { super(locale); }

  protected async *chatStream(messages: ChatMessage[], signal?: AbortSignal): AsyncIterable<string> {
    const res = await fetch('/api/chat/completions', {           // 打你自家后端,key 留在后端
      method: 'POST',
      headers: { 'Content-Type': 'application/json', Accept: 'text/event-stream',
                 Authorization: `Bearer ${this.token}` },
      body: JSON.stringify({ messages, stream: true }),
      signal,
    });
    if (!res.ok || !res.body) throw new Error(`model responded ${res.status}`);
    const reader = res.body.getReader();
    const decoder = new TextDecoder();
    let buffer = '';
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      buffer += decoder.decode(value, { stream: true });
      const lines = buffer.split('\n');
      buffer = lines.pop() ?? '';
      for (const line of lines) {
        const s = line.trim();
        if (!s.startsWith('data:')) continue;
        const payload = s.slice(5).trim();
        if (payload === '[DONE]') return;
        try {
          const delta = JSON.parse(payload).choices?.[0]?.delta?.content;
          if (typeof delta === 'string' && delta) yield delta;
        } catch { /* 忽略非补全帧 */ }
      }
    }
  }
}

AiEditInput:{ action: 'polish'|'translate'|'expand'|'shrink'|'tone'|'custom', selectedText, instruction?, contextBefore?, contextAfter?, signal? }; AiDraftInput:{ instruction, signal? }。

也可直接用内置实现省去自研:

  • MockAiProvider——演示用假流。
  • OpenAiCompatProvider({ baseUrl, apiKey, model, headers?, temperature?, locale? })——直连任意 OpenAI 兼容 /chat/completions 端点(浏览器直连需端点允许 CORS,否则走自家后端转发)。
  • PostMessageAiProvider——iframe 场景经宿主 postMessage 代发模型。

故事图里的「AI 生成图谱」「AI 解读这一拍 / 这个节点」全部经此契约,无额外接口。


<CollabDocWorkspace> props

继承 CollabDocEditorProps(docId / adapters / user / readOnly / className / locale),额外新增故事图相关项。

| prop | 类型 | 默认 | 说明 | |------|------|------|------| | docId | string | — | 必填。协同房 id(= WS roomname)。 | | adapters | Adapters | — | 必填。依赖注入(transport 必填 / auth?/ aiProvider?…)。 | | user | EditorUser({ id?, name?, color? }) | — | 驱动在线光标标签与配色;color 缺省按 id 确定性生成。 | | readOnly | boolean | false | 初始只读预览态。 | | className | string | — | 根节点样式类(纯文档模式生效)。 | | locale | 'zh' \| 'en' | 'zh' | UI 语言,经 LocaleContext 下发全部子组件;AI 解读指令也随此语言。 | | graph | StoryGraph | — | 传入即进入工作台三栏;不传退化为 CollabDocEditor。 | | seed | string | — | 空房首次 seed 的故事 markdown(仅文档为空时灌入,单权威去重)。 | | autoAnalyze | boolean | false | 初始化让 BYO 模型自动读故事生成图谱(需 aiProvider.analyzeStory;本房仅跑一次)。 | | view | 'split' \| 'doc' \| 'graph' | 'split' | 展示视图:三栏 / 只正文 / 只图谱(铺满,适合窄容器)。编辑器始终挂载(供拍数据)。 | | bridge | boolean | false | iframe 嵌入桥:点节点 postMessage collab:open-file;监听 collab:focus-node / collab:set-graph。 | | onOpenFile | (path: string, node: string) => void | — | npm 消费方:点图谱节点回调(path = SgNode.file,node = 节点 id)。 | | focusFile | string \| null | — | npm 消费方:高亮该文件对应的节点(反向导航,等价 iframe 的 collab:focus-node)。 | | onStoryChanged | (payload: StoryChangePayload) => void | — | 宿主桥:首次同步及正文变化后返回辅助文本和完整 beats 快照;连续更新合并 200ms。 | | focusBeat | string \| null | — | 宿主桥:高亮并滚动到指定拍;null 清除宿主控制的拍高亮。 | | onAgentRewrite | (beatId: string, text: string) => void | — | 宿主桥:把当前拍交给宿主 Agent;collab 不直接落盘。 | | onDeployChoice | (nodeId: string, choice: string) => void | — | 宿主桥:把部署选择交给宿主生成 Changes;collab 不直接修改图谱。 |

onOpenFile 与 bridge 可并存或二选一:同进程消费走 onOpenFile 回调,iframe 嵌入走 postMessage。 focusFile 匹配规则:先精确匹配 node.file === focusFile,再退化到 focusFile.endsWith(node.file)(同目录/后缀)。

onStoryChanged 会复用编辑器自身的拍解析,payload.beats 是宿主执行 baseline/diff 和生成 Changes 的权威数据。payload.markdown 由 TipTap 官方 Markdown 序列化器生成,供宿主预览或辅助判断;工作区 story.md 的正式变更仍应按 beats 生成 Changes,而不是直接整篇覆盖。hydrate、seed、本地编辑和远端协同合并都会触发同一通知链路。

onAgentRewrite 与 onDeployChoice 都是 controlled host action:回调后界面数据保持不变,直到宿主通过现有 props/协同数据回传新状态。


代码图模式 vs 故事图模式

由 graph.profile.type 决定,不是两套组件:

| | 故事图模式(profile.type !== 'codebase') | 代码图模式(profile.type === 'codebase') | |---|---|---| | 面板 | 大纲 · 正文 · 图谱三栏,多视图(拍 / 节点 / 场景 / 一致性…) | 只保留图谱 + 解读 tab(铺满,配 view="graph" 用) | | 节点 | 业务节点(name 中文名 / layer 分层 / verdict 判词三态) | 符号节点:显示符号名(name)、签名(signature)、调用邻域 | | 联动 | 正文里点拍 → 高亮节点 → 滚动到该拍;AI 解读这一拍 | 点节点 → onOpenFile 开源码;开文件 → focusFile 高亮节点 | | 边 | calls / writes / emits(SgEdge.kind) | calls / references / implements / extends / imports |

StoryGraph 结构:{ profile: { type, protagonist, axis }, nodes: SgNode[], edges: SgEdge[], beats: Record<拍id, 节点id[]>, scenarios: SgScenario[] }。 SgNode:{ id, name, kind, layer, constraint?, file?, signature?, origin?, choices?, verdict?('ok'|'pending'|'miss'), verdictText? }。 SgEdge:{ from, to, kind }。

标准 storygraph/story.md + graph.yaml + map.yaml + scenarios.yaml 可直接加载:

const { storyMarkdown, graph } = await loadStoryGraphBundle({
  readFile: (path) => hostWorkspaceApi.readFile(path),
});

宿主只负责读取文件,npm 包完成并行加载、YAML 解析和 StoryGraph 组合。


导出清单(index.ts)

  • 组件:CollabDocEditor、CollabDocWorkspace
  • AiProvider:PromptedAiProvider(抽象基类,推荐继承)、OpenAiCompatProvider、MockAiProvider、PostMessageAiProvider
  • 鉴权工厂:createAnonAuth、createStaticAuth、createCallbackAuth、createTicketAuth、createBearerExchangeAuth
  • StoryGraph 加载:loadStoryGraphBundle
  • 传输 hook / 工具:useCollabProvider、resolveWsBase
  • i18n:LocaleContext、useT(+ 类型 Locale)
  • 富块:HtmlBlock、MermaidBlock(+ DEFAULT_MERMAID)、MathBlock
  • 示例数据:SAMPLE_GRAPH、SAMPLE_STORY_MD、SAMPLE_GRAPH_EN、SAMPLE_STORY_MD_EN、sampleStory、sampleGraph
  • 类型:CollabDocEditorProps、CollabDocWorkspaceProps、EditorUser、Adapters、AuthAdapter、 DocStoreAdapter、TransportConfig、TicketAuthOptions、BearerExchangeAuthOptions、 StoryGraphBundle、StoryGraphBundleSource、StoryChangePayload、AiProvider、AiEditInput、AiEditAction、 AiDraftInput、ChatMessage、PromptLocale、OpenAiCompatOptions、StoryGraph、SgNode、SgEdge、 SgScenario、SgProfile、CollabConnStatus、CollabConnection、Locale、HtmlBlockAttrs、 HtmlBlockMode、MermaidBlockAttrs、MathBlockAttrs、MathDisplay

后端依赖

协同同步需要 CRDT 引擎:Python FastAPI + pycrdt-websocket,生产暴露 wss://host/collab/{docId}?ticket=,持久化 Postgres。本包直接连它,不经过不支持二进制 CRDT 帧的业务网关。两种生产鉴权模式、部署和 smoke 见仓根 DEPLOY.md;x-coder 的 trusted_jwt 示例见 docs/x-coder-integration.md。