@xiongtao.xt/tau-plugins 0.19.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # 更新记录
2
2
 
3
+ ## 0.20.0
4
+
5
+ - manifest 支持 `categories`(插件中心分类)与 `keywords`(目录搜索),门户上传严格校验,客户端忽略未知值。
6
+ - 博客模板包(`template` 分支)同样支持分类与关键词,只含静态站点产物,不能同时声明代码插件入口。
7
+ - publish-data:站点级数据(`settings`、`collections`、`cardTagTree`、`projectTrees`)改为只在清单中出现;内容文件新增 `links`,只含本文用到且已发布的站内链接目标。
8
+ - publish-client:`readData` 随正文带回清单的 `settings`;检索引擎按索引缓存,不再每次查询重建。
9
+
3
10
  ## 0.19.0
4
11
 
5
12
  - 补齐 publish-data 类型、publish-client 纯数据客户端及 publish-artifacts 协议校验/定义求值入口;数据与产物声明不连带载入 Editor/React/UI 类型。
package/PUBLISH-DATA.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  普通 Tau 插件仍然使用 SDK 类型开发,构建时将宿主注入模块设为 external。SDK 根入口和 UI 包不携带编辑器、组件或页面的运行实现。
4
4
 
5
- 独立站点可使用 `@tau/plugins/publish-data` 的纯数据类型,以及 `@tau/plugins/publish-client` 的可执行辅助客户端。后者只有数据读取、摘要校验、取消和搜索,不包含 React、渲染器、路由或预览界面。
5
+ 博客模板一般直接使用 `@tau/blog`:它包含数据读取、搜索、正文渲染和内容插件接入,见该包 README。只想读取数据、自己渲染正文的站点,可使用 `@tau/plugins/publish-data` 的纯数据类型和 `@tau/plugins/publish-client` 的数据客户端。客户端只有数据读取、摘要校验、取消和搜索,不包含 React、渲染器、路由或预览界面。
6
6
 
7
7
  ```ts
8
8
  import { createClient } from "@tau/plugins/publish-client";
@@ -12,8 +12,18 @@ const manifest = await client.readManifest();
12
12
  const result = await client.readData({ key: manifest.items[0].key });
13
13
  ```
14
14
 
15
- 模板也可以自行读取协议 JSON。模板自行携带渲染实现并消费 C23 插件产物;宿主不为站点提供 UI 模块。资源以清单或所属数据文件的 URL 为上下文解析,公开路由由模板决定。
15
+ ## 数据分布
16
16
 
17
- `@tau/plugins/publish-artifacts` 提供 C23 清单校验、定义求值与节点归属检查,仅处理协议,不提供 UI、模块映射或资源生命周期。其声明不连带加载 SDK 根入口的 Editor/Slate/UI 类型;共享模块由模板通过实际公开能力提供。
17
+ - 清单 `site.<摘要>.json`:站点信息、全部内容的 key 与数据文件、搜索索引引用,以及只出现一次的站点级数据——`settings`(发布外观设置)、`collections`(项目列表)、`cardTagTree`(卡片标签树)、`projectTrees`(按项目 id 的文档目录)。`readData` 会把 `settings` 一并带回。
18
+ - 内容文件:`key`、`meta`、`content`,以及可选的 `links`(本文正文里已发布的站内链接目标)、`pluginRenderers`(内容插件产物)和 `contentStyles`(正文动态样式)。资源以所属数据文件的 URL 为上下文解析。
19
+ - 公开路由完全由模板决定,数据里只有 ContentRef(`{ key, type, id? }`),没有 URL。
18
20
 
19
- 可选 `@tau/editor` 是单独的可执行通用编辑器,包含默认内置功能和已编译样式。模板起步工程中的 PublishedContent 负责读取已验证正文、加载插件产物和伴生 CSS,再将扩展及节点交给 Editor;dataUrl 和发布协议不进入 Editor 的 props。普通 SDK/UI 根入口继续保持类型与 Tau 注入边界。
21
+ 正文的站内链接由 `@tau/blog` 按 `links` 渲染,模板只提供 `hrefFor`。自己渲染正文的站点按 `links` 的键(`${contentType}:${refId}`,与求索资料的 `citeKey` 同一口径)查目标,查不到即目标未发布。
22
+
23
+ ## 样式
24
+
25
+ `contentStyles` 是宿主为正文里的 HTML 节点编译的 Tailwind 工具类,属于过渡产物:按原文加载,含 `:root` 主题变量和全局类名,正文挂载期间对整个页面生效。模板若自定义了同名 Tailwind 主题变量,正文挂载时可能被这些默认值覆盖。
26
+
27
+ ## 插件产物
28
+
29
+ `@tau/plugins/publish-artifacts` 提供 C23 清单校验、定义求值与节点归属检查,仅处理协议,不提供 UI、模块映射或资源生命周期。共享模块由模板通过实际公开能力提供;`@tau/blog` 已内置这部分接入。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xiongtao.xt/tau-plugins",
3
- "version": "0.19.0",
3
+ "version": "0.20.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -36,6 +36,53 @@ function comparePluginVersions(left, right) {
36
36
  return 0;
37
37
  }
38
38
 
39
+ // packages/plugins/src/categories.ts
40
+ var PLUGIN_CATEGORIES = [
41
+ { id: "sync", label: "\u540C\u6B65" },
42
+ { id: "upload", label: "\u8D44\u6E90\u4E0A\u4F20" },
43
+ { id: "editor", label: "\u7F16\u8F91\u5668" },
44
+ { id: "whiteboard", label: "\u767D\u677F" },
45
+ { id: "blog-template", label: "\u535A\u5BA2\u6A21\u677F" },
46
+ { id: "ai", label: "AI" },
47
+ { id: "voice", label: "\u8BED\u97F3" },
48
+ { id: "page", label: "\u9875\u9762" }
49
+ ];
50
+ var PLUGIN_KEYWORD_LIMIT = 5;
51
+ var PLUGIN_KEYWORD_MAX_LENGTH = 20;
52
+ var categoryIds = new Set(PLUGIN_CATEGORIES.map((item) => item.id));
53
+ var isPluginCategory = (value) => typeof value === "string" && categoryIds.has(value);
54
+ function readCatalogFields(item, strict) {
55
+ const fail = (message) => {
56
+ if (strict) throw new Error(message);
57
+ };
58
+ const list = (value, name) => {
59
+ if (value === void 0) return [];
60
+ if (Array.isArray(value)) return value;
61
+ fail(`${name} \u5FC5\u987B\u662F\u6570\u7EC4`);
62
+ return [];
63
+ };
64
+ const categories = [];
65
+ for (const value of list(item.categories, "categories")) {
66
+ if (!isPluginCategory(value)) fail(`\u672A\u77E5\u63D2\u4EF6\u5206\u7C7B\uFF1A${String(value)}`);
67
+ else if (!categories.includes(value)) categories.push(value);
68
+ }
69
+ const keywords = [];
70
+ for (const value of list(item.keywords, "keywords")) {
71
+ const keyword = typeof value === "string" ? value.trim() : "";
72
+ if (!keyword || keyword.length > PLUGIN_KEYWORD_MAX_LENGTH)
73
+ fail(`\u5173\u952E\u8BCD\u5FC5\u987B\u4E3A 1 \u81F3 ${PLUGIN_KEYWORD_MAX_LENGTH} \u5B57`);
74
+ else if (!keywords.includes(keyword)) keywords.push(keyword);
75
+ }
76
+ if (keywords.length > PLUGIN_KEYWORD_LIMIT) {
77
+ fail(`\u5173\u952E\u8BCD\u6700\u591A ${PLUGIN_KEYWORD_LIMIT} \u4E2A`);
78
+ keywords.length = PLUGIN_KEYWORD_LIMIT;
79
+ }
80
+ return {
81
+ ...categories.length ? { categories: Object.freeze(categories) } : {},
82
+ ...keywords.length ? { keywords: Object.freeze(keywords) } : {}
83
+ };
84
+ }
85
+
39
86
  // packages/plugins/src/package.ts
40
87
  var PLUGIN_SHARED_MODULES = [
41
88
  "@tau/plugins",
@@ -48,7 +95,7 @@ var PLUGIN_SHARED_MODULES = [
48
95
  "react/jsx-runtime",
49
96
  "react/jsx-dev-runtime"
50
97
  ];
51
- function validatePluginManifest(value) {
98
+ function validatePluginManifest(value, options = {}) {
52
99
  if (!value || typeof value !== "object" || Array.isArray(value))
53
100
  throw new Error("\u63D2\u4EF6\u6E05\u5355\u5FC5\u987B\u662F\u5BF9\u8C61");
54
101
  const item = value;
@@ -73,6 +120,7 @@ function validatePluginManifest(value) {
73
120
  if (item.editor !== void 0 && item.editor !== true) throw new Error("editor \u53EA\u80FD\u58F0\u660E\u4E3A true");
74
121
  if (item.whiteboard !== void 0 && item.whiteboard !== true)
75
122
  throw new Error("whiteboard \u53EA\u80FD\u58F0\u660E\u4E3A true");
123
+ const catalog = readCatalogFields(item, options.strict === true);
76
124
  const integrity = {};
77
125
  for (const name of [main, ...styles ? [styles] : []]) {
78
126
  const hash = item.integrity && typeof item.integrity === "object" ? item.integrity[name] : void 0;
@@ -92,21 +140,12 @@ function validatePluginManifest(value) {
92
140
  main,
93
141
  ...item.editor === true ? { editor: true } : {},
94
142
  ...item.whiteboard === true ? { whiteboard: true } : {},
143
+ ...catalog,
95
144
  integrity: Object.freeze(integrity),
96
145
  ...styles === void 0 ? {} : { styles }
97
146
  });
98
147
  }
99
148
 
100
- // packages/plugins/src/sidebar.ts
101
- var SIDEBAR_GROUPS = Object.freeze({
102
- daily: "daily",
103
- knowledge: "knowledge",
104
- learning: "learning",
105
- ai: "ai",
106
- sharing: "sharing",
107
- plugins: "plugins"
108
- });
109
-
110
149
  // packages/plugins/src/rendering.ts
111
150
  var PLUGIN_RENDER_VERSION = 1;
112
151
  function evaluatePluginPackage(artifact, modules) {
@@ -197,7 +236,6 @@ function assertRenderTypes(definition, expected) {
197
236
  }
198
237
 
199
238
  // packages/plugins/src/publish-artifacts.ts
200
- var SIDEBAR_GROUPS2 = SIDEBAR_GROUPS;
201
239
  var PLUGIN_RENDER_VERSION2 = PLUGIN_RENDER_VERSION;
202
240
  function evaluatePluginPackage2(artifact, modules) {
203
241
  return evaluatePluginPackage(artifact, modules);
@@ -208,7 +246,6 @@ function assertRenderTypes2(definition, types) {
208
246
  export {
209
247
  PLUGIN_RENDER_VERSION2 as PLUGIN_RENDER_VERSION,
210
248
  PLUGIN_SHARED_MODULES,
211
- SIDEBAR_GROUPS2 as SIDEBAR_GROUPS,
212
249
  assertRenderTypes2 as assertRenderTypes,
213
250
  evaluatePluginPackage2 as evaluatePluginPackage,
214
251
  validatePluginManifest
package/publish-client.js CHANGED
@@ -45,6 +45,7 @@ function createClient({ manifestUrl }) {
45
45
  const cache = /* @__PURE__ */ new Map();
46
46
  let manifestPromise;
47
47
  let searchPromise;
48
+ let engine;
48
49
  function json(url, expected) {
49
50
  assert(/^[a-f0-9]{64}$/.test(expected), "INVALID_DATA", "\u6458\u8981\u683C\u5F0F\u9519\u8BEF");
50
51
  const filenameHash = new URL(url).pathname.match(/\.([a-f0-9]{64})\.json$/)?.[1];
@@ -207,7 +208,11 @@ function createClient({ manifestUrl }) {
207
208
  "IDENTITY_MISMATCH",
208
209
  "\u5185\u5BB9\u8EAB\u4EFD\u4E0D\u5339\u914D"
209
210
  );
210
- return { data: value, dataUrl };
211
+ return {
212
+ data: value,
213
+ dataUrl,
214
+ ...source.settings ? { settings: source.settings } : {}
215
+ };
211
216
  },
212
217
  async search(query, { limit = 12, signal } = {}) {
213
218
  cancelled(signal);
@@ -219,13 +224,17 @@ function createClient({ manifestUrl }) {
219
224
  signal
220
225
  );
221
226
  cancelled(signal);
222
- const engine = new Fuse(documents, {
223
- keys: ["title", "summary", "content", "tags"],
224
- includeMatches: true,
225
- threshold: 0.3,
226
- ignoreLocation: true
227
- });
228
- return engine.search(normalized, { limit }).map(({ item, matches }) => {
227
+ if (engine?.documents !== documents)
228
+ engine = {
229
+ documents,
230
+ fuse: new Fuse(documents, {
231
+ keys: ["title", "summary", "content", "tags"],
232
+ includeMatches: true,
233
+ threshold: 0.3,
234
+ ignoreLocation: true
235
+ })
236
+ };
237
+ return engine.fuse.search(normalized, { limit }).map(({ item, matches }) => {
229
238
  const { content: _content, ...hit } = item;
230
239
  const ranges = (matches ?? []).flatMap((match) => {
231
240
  if (!match.value || !["title", "summary", "content", "tags"].includes(match.key ?? ""))
@@ -41,7 +41,7 @@ export interface MarkdownCodec {
41
41
  */
42
42
  fence?: {
43
43
  language: string | readonly string[];
44
- /** 源码落进元素的字段名,默认 "content"(mermaid 用 "chart") */
44
+ /** 源码落进元素的字段名,默认 "content",由领域声明覆盖。 */
45
45
  property?: string;
46
46
  };
47
47
  /**
@@ -69,7 +69,7 @@ export interface MarkdownCodec {
69
69
  parent: Descendant | null;
70
70
  previousSibling: Descendant | null;
71
71
  nextSibling: Descendant | null;
72
- /** 把块级子元素降级为行内形态(image→inline-image、html-block→html-inline),无声明则原样返回 */
72
+ /** 按领域声明把块级子元素降级为行内形态,无声明则原样返回。 */
73
73
  demote: (child: Descendant) => Descendant;
74
74
  }) => Descendant[] | null;
75
75
  /** 按目标类型贡献规范化,先于该类型自身 normalize;按 codec 顺序首个非 null 替换生效。 */
@@ -0,0 +1,40 @@
1
+ /** 插件中心分类按扩展点划分,顺序即展示顺序;新增面向用户的扩展点时同步扩充。 */
2
+ export declare const PLUGIN_CATEGORIES: readonly [{
3
+ readonly id: "sync";
4
+ readonly label: "同步";
5
+ }, {
6
+ readonly id: "upload";
7
+ readonly label: "资源上传";
8
+ }, {
9
+ readonly id: "editor";
10
+ readonly label: "编辑器";
11
+ }, {
12
+ readonly id: "whiteboard";
13
+ readonly label: "白板";
14
+ }, {
15
+ readonly id: "blog-template";
16
+ readonly label: "博客模板";
17
+ }, {
18
+ readonly id: "ai";
19
+ readonly label: "AI";
20
+ }, {
21
+ readonly id: "voice";
22
+ readonly label: "语音";
23
+ }, {
24
+ readonly id: "page";
25
+ readonly label: "页面";
26
+ }];
27
+ export type PluginCategory = (typeof PLUGIN_CATEGORIES)[number]["id"];
28
+ export declare const PLUGIN_KEYWORD_LIMIT = 5;
29
+ export declare const PLUGIN_KEYWORD_MAX_LENGTH = 20;
30
+ export declare const isPluginCategory: (value: unknown) => value is PluginCategory;
31
+ export interface PluginCatalogFields {
32
+ categories?: readonly PluginCategory[];
33
+ keywords?: readonly string[];
34
+ }
35
+ /**
36
+ * 读取 manifest 中的分类与关键词。
37
+ * strict 用于门户上传,非法值直接报错;客户端用宽松模式静默丢弃,
38
+ * 使老客户端不会因词表新增或上限调整而拒收插件。
39
+ */
40
+ export declare function readCatalogFields(item: Record<string, unknown>, strict: boolean): PluginCatalogFields;
@@ -1,5 +1,7 @@
1
1
  export { assertLocalId } from "./ids";
2
+ import { type PluginCategory } from "./categories";
2
3
  export { comparePluginVersions } from "./version";
4
+ export { PLUGIN_CATEGORIES, PLUGIN_KEYWORD_LIMIT, PLUGIN_KEYWORD_MAX_LENGTH, isPluginCategory, type PluginCategory, } from "./categories";
3
5
  export declare const PLUGIN_SHARED_MODULES: readonly ["@tau/plugins", "@tau/plugins/whiteboard", "@tau/plugins/sync", "@tau/ui", "react", "react-dom", "react-dom/client", "react/jsx-runtime", "react/jsx-dev-runtime"];
4
6
  export interface PluginManifest {
5
7
  formatVersion: 1;
@@ -16,6 +18,10 @@ export interface PluginManifest {
16
18
  /** 静态编辑器贡献在窗口启动时装配,启停需重启。 */
17
19
  editor?: true;
18
20
  whiteboard?: true;
21
+ /** 插件中心分类,作者自填,不与实际注册对账。 */
22
+ categories?: readonly PluginCategory[];
23
+ /** 仅参与搜索匹配。 */
24
+ keywords?: readonly string[];
19
25
  integrity: Record<string, string>;
20
26
  }
21
27
  /** 宿主内部传输的已读取产物,不属于作者运行时 API。 */
@@ -24,6 +30,9 @@ export interface DevelopmentPluginPackage {
24
30
  code: string;
25
31
  css?: string;
26
32
  }
27
- export declare function validatePluginManifest(value: unknown): PluginManifest;
33
+ /** strict 供门户上传使用:分类或关键词不合规时报错,客户端默认静默丢弃。 */
34
+ export declare function validatePluginManifest(value: unknown, options?: {
35
+ strict?: boolean;
36
+ }): PluginManifest;
28
37
  /** 静态内容贡献在启动时固定,启停与更新需要重启。 */
29
38
  export declare const isContentPlugin: (manifest: Pick<PluginManifest, "editor" | "whiteboard">) => boolean;
@@ -1,5 +1,4 @@
1
1
  export { validatePluginManifest, PLUGIN_SHARED_MODULES } from "./package";
2
- export declare const SIDEBAR_GROUPS: Readonly<Record<string, string>>;
3
2
  import type { DevelopmentPluginPackage } from "./package";
4
3
  export declare const PLUGIN_RENDER_VERSION: 1;
5
4
  export type { PublishedPluginRender } from "./render-artifact";
@@ -27,6 +27,15 @@ export interface SiteData {
27
27
  mimeType: string;
28
28
  };
29
29
  }
30
+ export interface PublishSettings {
31
+ appearance?: {
32
+ image?: {
33
+ borderRadius?: string;
34
+ shadow?: string | boolean;
35
+ };
36
+ };
37
+ }
38
+ /** 站点级数据只在清单中出现一次,各内容文件不重复携带。 */
30
39
  export interface DataManifest {
31
40
  version: 1;
32
41
  site: SiteData;
@@ -35,6 +44,11 @@ export interface DataManifest {
35
44
  integrity: string;
36
45
  };
37
46
  items: ManifestItem[];
47
+ settings?: PublishSettings;
48
+ collections?: Collection[];
49
+ cardTagTree?: CardTagTree[];
50
+ /** 键为项目 id。 */
51
+ projectTrees?: Record<string, TreeItem[]>;
38
52
  }
39
53
  export interface ContentNode {
40
54
  [field: string]: unknown;
@@ -205,20 +219,10 @@ export interface CardTagTree {
205
219
  export interface PublishedCommon {
206
220
  version: 1;
207
221
  key: string;
208
- tree?: TreeItem[];
209
- collections?: Collection[];
210
- cardTagTree?: CardTagTree[];
211
- contentLinkTargets?: Record<string, ContentRef>;
222
+ /** 本文正文里已发布的站内链接目标,键与 QuestMaterial.citeKey 同一口径;未发布的链接不在其中。 */
223
+ links?: Record<string, ContentRef>;
212
224
  pluginRenderers?: import("./render-artifact").PublishedPluginRender[];
213
225
  contentStyles?: ResourceRef[];
214
- settings?: {
215
- appearance?: {
216
- image?: {
217
- borderRadius?: string;
218
- shadow?: string | boolean;
219
- };
220
- };
221
- };
222
226
  }
223
227
  export type PublishedData = {
224
228
  [K in DataKind]: PublishedCommon & {
@@ -239,6 +243,8 @@ export type PublishedData = {
239
243
  export interface DataResult {
240
244
  data: PublishedData;
241
245
  dataUrl: string;
246
+ /** 清单中的发布设置,由客户端随正文带回。 */
247
+ settings?: PublishSettings;
242
248
  }
243
249
  export interface SearchDocument extends ContentRef {
244
250
  type: "card" | "article" | "doc" | "quest";