Kunkun
构建扩展

记录存储

db.collection —— 每个插件的文档存储,支持查询、全文搜索和实时更新的 UI。

对于超过少量键值对的情况,Kunkun 为每个扩展提供真正的记录存储:db.collection<T>(name)。它是面向行的、可查询的、支持全文搜索、可分页且响应式的——后端由 SQLite + FTS5 驱动。

为什么不是 LocalStorage?

LocalStorage 适合少量标志。但是在 KV 中存储列表迫使你每次更改都要重写一个巨大的 JSON 对象,或者散落一堆无法筛选或分页的 key:id 条目。记录存储解决了这两个问题。两个 API 共享 storage 权限。

获取集合

import { db } from "@kunkunsh/sdk";

interface Bookmark { url: string; title: string; tags: string[]; visits: number }
const bookmarks = db.collection<Bookmark>("bookmarks");

集合在首次写入时创建。名称需匹配 ^[a-z0-9][a-z0-9-_]{0,63}$

写入

const id = await bookmarks.add({ url: "https://kunkun.sh", title: "Kunkun", tags: ["dev"], visits: 0 });
await bookmarks.set("my-id", data);                 // upsert,完全替换
await bookmarks.update(id, { visits: 5 });          // 浅合并
await bookmarks.delete(id);                          // 幂等
await bookmarks.bulkSet([{ data }, { data, searchText: "…" }]); // 最多 1000 条,单事务
await bookmarks.bulkDelete([id1, id2]);
await bookmarks.clear();                             // 清空集合

add 生成一个 ULID 格式的 id;你也可以通过 set / bulkSet 提供自己的 id(≤128 字符)。

读取

const rec = await bookmarks.get(id);          // DbRecord<Bookmark> | undefined
const recs = await bookmarks.getMany([a, b]); // 保持顺序,缺失的 id 会被省略

每条记录都有一个稳定的信封结构:

interface DbRecord<T> {
  id: string;        // ULID(或你的 id)
  data: T;           // 你的 JSON 文档
  createdAt: string; // ISO 格式,首次插入时设置
  updatedAt: string; // ISO 格式,每次写入时更新
}

查询

const page = await bookmarks.query({
  where: [["visits", ">=", 10], ["tags", "contains", "dev"]],
  search: "github",                 // FTS5 搜索 searchText(见下文)
  orderBy: [["visits", "desc"]],
  limit: 50,
  cursor: prev?.nextCursor,         // 键集分页
});
// => { items: DbRecord<Bookmark>[], nextCursor?: string }

const n = await bookmarks.count({ where: [["tags", "contains", "dev"]] });

运算符

运算符含义
== !=相等(与 null 比较时匹配 JSON null 或缺失字段)
< <= > >=比较
in字段是列表中的一项(≤100 个值)
contains数组字段包含该值
  • 字段路径 使用点符号进入你的文档:["author.name", "==", "Ada"]
  • 多个 where 子句是 AND 关系(≤10 个子句;OR 已计划)。
  • orderBy 接受 createdAtupdatedAtid 或 JSON 路径;id 总是作为决胜字段追加(默认排序为 id desc → 最新的 ULID 优先)。
  • 游标 是不透明的键集令牌,仅对相同的 where/orderBy 形状有效——将 nextCursor 传回以获取下一页。

ULID 是关键的

默认情况下 id 是 ULID:设备间无冲突且按字典序创建顺序排序。这就是"最新优先"不需要额外时间戳列的原因——也是使存储无需更改代码即可支持云同步的原因。

全文搜索

搜索是每条记录可选的,通过写入时提供的 searchText 字段实现;查询时 FTS5 匹配它:

await notes.add({ title: "购买食材", body: "牛奶、鸡蛋" }, { searchText: "购买食材 牛奶 鸡蛋" });
const hits = await notes.query({ search: "牛奶" });

响应式

watch

const unsubscribe = bookmarks.watch((event) => {
  // event: { collection, ids, op: "set" | "delete" | "clear", timestamp }
});
db.watch((event) => { /* 此插件中的任意集合 */ });

宿主在每次提交写入后发出 db:changed 事件,传递给你的扩展(无论其运行在 desktop custom-view、worker relay 还是 CLI/web 下)。

实时查询(推荐)

不要手动连接 watch + 重新获取——绑定查询并让它自动更新。

SveltecreateLiveQuery 是一个 store):

<script lang="ts">
  import { createLiveQuery } from "@kunkunsh/sdk";
  const q = createLiveQuery<Bookmark>("bookmarks", { orderBy: [["visits", "desc"]] });
</script>

{#if $q.isLoading}加载中…{:else}
  {#each $q.records as r (r.id)}<div>{r.data.title}</div>{/each}
  {#if $q.nextCursor}<button onclick={() => q.loadMore()}>更多</button>{/if}
{/if}

ReactuseQuery / useRecord):

import { useQuery, useRecord } from "@kunkunsh/sdk/utils";

function List() {
  const { data, isLoading, nextCursor, loadMore, revalidate } = useQuery<Bookmark>("bookmarks", {
    where: [["tags", "contains", "dev"]],
    orderBy: [["visits", "desc"]],
    limit: 20,
  });
  // 任何 add/set/update/delete → 自动重新查询 → 重新渲染
}

function Detail({ id }: { id: string }) {
  const { record, isLoading } = useRecord<Bookmark>("bookmarks", id);
}

错误处理

import { db, DbError } from "@kunkunsh/sdk";
try {
  await bookmarks.delete("nope");
} catch (e) {
  if (e instanceof DbError && e.code === "RECORD_NOT_FOUND") { /* … */ }
}

错误码:RECORD_NOT_FOUNDQUOTA_EXCEEDEDINVALID_QUERYINVALID_COLLECTION_NAMEBATCH_TOO_LARGEEMBEDDING_MODEL_NOT_BOUND。得益于 kkrpc 的错误恢复机制,instanceof DbError.code 在你的扩展中正常工作。

同步与限制

当前的起始限制(可调整):

限制
记录文档大小≤ 1 MB
searchText 长度≤ 16 KB
每个插件总计≤ 50 MB 软限制
批量操作≤ 1000 条
查询 limit / where 子句 / in≤ 500 / ≤ 10 / ≤ 100

该存储设计为以每条记录对象的形式接入 Kunkun 未来的云同步(scope_type: "record"),每条记录采用最后写入者胜出策略——这正是为什么 id 是 ULID 且写入是按行的。语义(向量)搜索是计划中的后续功能;目前请使用 FTS5 searchText

集合管理

await db.listCollections();          // 获取此插件所有集合的元数据
await db.deleteCollection("bookmarks");

On this page