@xiongtao.xt/tau-plugins 0.20.0 → 0.22.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,17 @@
1
1
  # 更新记录
2
2
 
3
+ ## 0.22.0
4
+
5
+ - 新增 `context.video.register`,接入视频来源(视频网站、自建服务器等):插件识别链接、解析标题与清晰度,返回播放源描述(单文件、音视频分离或交给宿主解析器),下载、缓存与播放由宿主完成。
6
+ - 视频账号与代理配置通过 `settings.register({ target: "video" })` 注册,接收每个插件一份的 `videoConfig`。
7
+ - 新增 `services.videoExtractor.probe`,查询视频页面的标题与可下载格式;新增插件分类 `video`(在线视频)。
8
+
9
+ ## 0.21.0
10
+
11
+ - 新增 context.publish 发布提供方、publishConfig 配置表单和文件式/快照式提交协议,本地导出仍由宿主提供。
12
+ - 新增受控 Git 快照发布与缓存目录选择服务,插件不获得任意命令或宿主文件访问。
13
+ - 插件分类新增博客发布。
14
+
3
15
  ## 0.20.0
4
16
 
5
17
  - manifest 支持 `categories`(插件中心分类)与 `keywords`(目录搜索),门户上传严格校验,客户端忽略未知值。
package/PUBLISH.md ADDED
@@ -0,0 +1,17 @@
1
+ # 博客发布提供方
2
+
3
+ 需要 SDK 0.21.0 及包含博客发布能力的客户端。提供方只在渲染进程注册和执行,网络使用 `context.fetch`。本地导出由宿主提供,不注册本地发布插件。
4
+
5
+ 在 activate 中调用 `context.publish.register({ id: context.pluginId, name, create(config) })`。create 返回文件式或快照式实例,配置是本轮固定的 JSON 副本。提供方 ID 必须等于所属插件 ID。
6
+
7
+ 两种实例都实现 `describe(signal)` 与 `checkConnection(signal)`;describe 返回 `identity`、`basePath`、可选 `publicUrl`。identity 是不含密钥的物理目标稳定身份,不随插件版本、模板、Token 或提交方式变化。basePath 以斜杠开头和结尾,根路径为 `/`。
8
+
9
+ 文件式实例声明 `mode: "files"`,实现 `uploadFile(path, Uint8Array, signal)` 与 `deleteFile(path, signal)`,可用 `concurrency` 声明 1–32 的上限。方法成功返回 void,失败抛出异常。宿主负责增量、重试、依赖先行和入口后置、旧资源清理,不把远端目录当成可任意删除的文件清单。
10
+
11
+ 快照式实例声明 `mode: "snapshot"`,实现 `publish(snapshot, signal)`。snapshot 包含本轮临时 id、待上传文件的路径和摘要及删除路径。只有远端确认整轮提交后才返回;失败抛异常,不返回部分成功。可选 dispose 在会话关闭时回收连接。
12
+
13
+ Git 快照使用 `context.services.git.publishSnapshot({ publicationId: snapshot.id, remote, branch, author: { name, email }, authentication: { username, password }, cacheId? }, { signal })`。remote 只接受不含凭据的 HTTPS 地址;认证通过任务通道使用,不写入 remote URL。publicationId 仅在所属窗口本轮发布中有效,不能用于读取任意本地文件。需要缓存目录时调用 `context.services.git.selectCacheDirectory({})`,由宿主选择并返回 `{ id, label }`;只保存 id 和展示用 label,不传入任意 cwd 或 shell 命令。
14
+
15
+ 配置视图使用 `context.settings.register({ id, title, target: "publish", component })`。组件接收 `PluginSettingsProps.publishConfig` 的 value 与 save,保存成功后再提交表单值,不用按工作区隔离的 context.storage 保存账号。普通插件详情页提供前往博客发布页的入口。
16
+
17
+ 离开发布页继续运行;停用、切库和发起窗口关闭使旧会话失效。每次请求都传递 signal,错误信息不得包含凭据。新安装默认停用,插件缺失或未启用时宿主保留用户配置及选择,不自动切换其他目标。
package/SERVICES.md CHANGED
@@ -15,6 +15,7 @@
15
15
  | `markdown.render(source, options?)` | Markdown 字符串 | `{ html, css, warnings }` |
16
16
  | `ffmpeg.probe(file, options?)` | `{ name, data: Blob }` | `{ format, streams }` |
17
17
  | `ffmpeg.run(input, options?)` | `{ files, args, outputs }` | `{ files, log }` |
18
+ | `videoExtractor.probe(url, options?)` | 视频页面地址,可选 `proxy`(SDK 0.22.0) | `{ title, cover?, formats }` |
18
19
 
19
20
  ## 在静态组件中获取服务
20
21
 
package/VIDEO.md ADDED
@@ -0,0 +1,40 @@
1
+ # 视频提供方
2
+
3
+ 需要 SDK 0.22.0 和支持该能力的客户端。通过 `context.video.register({ id: context.pluginId, name, create })` 注册,每个插件一个提供方。视频来源可以是视频网站,也可以是自建的视频服务(如 NAS)。
4
+
5
+ 启用后,编辑器、白板、项目视频笔记和学习资料的「添加视频」中会出现以 `name` 命名的入口。用户从该入口粘贴视频链接,客户端先用 `matches` 确认链接属于本插件,再调用 `resolve`。
6
+
7
+ 插件只负责视频来源本身的知识:识别链接、查询标题与可选清晰度、给出真实的流地址和所需请求头。选择弹窗、下载进度、合并音视频、缓存和播放都由客户端完成,插件不提供这些界面,也不传输视频字节。
8
+
9
+ ## 实例
10
+
11
+ `create(config)` 按当前插件配置同步创建实例,不发网络请求。实例需要实现四个方法:
12
+
13
+ - `matches(url)`:同步判断链接是否属于本来源,不访问网络。链接属于其他已启用插件时,客户端会提示用户改用对应入口。
14
+ - `resolve(url, signal)`:返回 `{ title, cover?, variants, defaultVariant? }`。`variants` 是可选清晰度(至少一项),每项为 `{ label, hint?, ref }`。
15
+ - `cacheKey(ref)`:返回稳定的缓存身份,应包含清晰度。视频已缓存时客户端不再调用 `getPlayback`,可以离线播放。
16
+ - `getPlayback(ref, signal)`:每次缓存前现取播放源,可以返回会过期的直链。
17
+
18
+ ## 视频引用
19
+
20
+ 用户选中的 `ref` 原样写入文档和数据库,以后只能通过它找回视频。它必须是可长期保存的 JSON,例如视频 ID 加清晰度,不要放会过期的直链、Cookie 或令牌。`type`、`title`、`thumbnail` 三个键由客户端占用,引用中不要使用。
21
+
22
+ ## 播放源
23
+
24
+ `getPlayback` 返回以下三种之一:
25
+
26
+ - `{ kind: "file", stream }`:单个音视频合一的文件。
27
+ - `{ kind: "split", video, audio }`:音视频分离的流,客户端分别下载后无损合并。
28
+ - `{ kind: "extract", url, videoFormat?, audioFormat?, proxy? }`:交给客户端内置的视频解析器下载,格式 ID 来自 `context.services.videoExtractor.probe`。
29
+
30
+ `stream` 为 `{ url, headers? }`。需要 Referer、Cookie 等请求头时放在 `headers` 中,客户端只在下载时使用,不写入文档或缓存索引。网络请求使用 `context.fetch` 并传入方法的 signal,它可以设置这些请求头。
31
+
32
+ ## 视频解析服务
33
+
34
+ `context.services.videoExtractor.probe(url, { proxy?, signal? })` 返回 `{ title, cover?, duration?, formats }`,`formats` 每项包含 `id`、`videoOnly`、`audioOnly`、`ext`、`width`、`height`、`fps`、`bitrate`、`size`、`note`。插件根据格式列表组织清晰度,再返回 `kind: "extract"` 的播放源;插件不能自行启动外部程序。
35
+
36
+ ## 配置
37
+
38
+ 账号、Cookie 或代理通过 `context.settings.register({ id, title, target: "video", component })` 注册配置视图,挂载在「设置 · 账号与连接 · 在线视频」。组件接收 `PluginSettingsProps.videoConfig`:`value` 和 `save(value)`,每个插件一份配置,保存后下一次调用 `create` 即读到新值。保存失败时保留草稿,成功后才提示已保存。
39
+
40
+ 插件停用或调用方取消时,进行中的请求会收到取消信号,迟到的结果被丢弃。插件未启用时,已保存的视频显示需要启用插件的提示,原数据保留。移动端和发布站点不运行插件,插件提供的视频在那里只显示说明。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xiongtao.xt/tau-plugins",
3
- "version": "0.20.0",
3
+ "version": "0.22.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -43,8 +43,10 @@ var PLUGIN_CATEGORIES = [
43
43
  { id: "editor", label: "\u7F16\u8F91\u5668" },
44
44
  { id: "whiteboard", label: "\u767D\u677F" },
45
45
  { id: "blog-template", label: "\u535A\u5BA2\u6A21\u677F" },
46
+ { id: "blog-publish", label: "\u535A\u5BA2\u53D1\u5E03" },
46
47
  { id: "ai", label: "AI" },
47
48
  { id: "voice", label: "\u8BED\u97F3" },
49
+ { id: "video", label: "\u5728\u7EBF\u89C6\u9891" },
48
50
  { id: "page", label: "\u9875\u9762" }
49
51
  ];
50
52
  var PLUGIN_KEYWORD_LIMIT = 5;
@@ -14,12 +14,18 @@ export declare const PLUGIN_CATEGORIES: readonly [{
14
14
  }, {
15
15
  readonly id: "blog-template";
16
16
  readonly label: "博客模板";
17
+ }, {
18
+ readonly id: "blog-publish";
19
+ readonly label: "博客发布";
17
20
  }, {
18
21
  readonly id: "ai";
19
22
  readonly label: "AI";
20
23
  }, {
21
24
  readonly id: "voice";
22
25
  readonly label: "语音";
26
+ }, {
27
+ readonly id: "video";
28
+ readonly label: "在线视频";
23
29
  }, {
24
30
  readonly id: "page";
25
31
  readonly label: "页面";
@@ -11,6 +11,8 @@ import type { PluginSync } from "./sync";
11
11
  import type { PluginUpload } from "./upload";
12
12
  import type { PluginTTS } from "./tts";
13
13
  import type { PluginImages } from "./images";
14
+ import type { PluginVideo } from "./video";
15
+ import type { PluginPublish } from "./publish";
14
16
  import type { PluginSidebar } from "./sidebar";
15
17
  import type { PluginExcerpts, PluginMaterials } from "./excerpts";
16
18
  import type { PluginMCP } from "./mcp";
@@ -46,6 +48,8 @@ export interface PluginContext {
46
48
  readonly upload: PluginUpload;
47
49
  readonly tts: PluginTTS;
48
50
  readonly images: PluginImages;
51
+ readonly video: PluginVideo;
52
+ readonly publish: PluginPublish;
49
53
  readonly sidebar: PluginSidebar;
50
54
  readonly excerpts: PluginExcerpts;
51
55
  readonly materials: PluginMaterials;
@@ -2,6 +2,8 @@ import type { PluginDefinition } from "./contract";
2
2
  export type * from "./upload";
3
3
  export type * from "./tts";
4
4
  export type * from "./images";
5
+ export type * from "./video";
6
+ export type * from "./publish";
5
7
  export type { JsonValue, Disposable, PluginStorage, PluginCommand, PluginContext, PluginInstance, PluginDefinition, } from "./contract";
6
8
  export type { PluginSettings, PluginSettingsDefinition, PluginSettingsProps, PluginSettingsHandle, } from "./settings";
7
9
  export * from "./editor";
@@ -0,0 +1,59 @@
1
+ import type { Disposable, JsonValue } from "./contract";
2
+ export interface PublishTarget {
3
+ /** 物理目标的稳定身份,不包含凭据、插件或模板版本。 */
4
+ identity: string;
5
+ basePath: string;
6
+ publicUrl?: string;
7
+ }
8
+ export interface PublicationSnapshot {
9
+ /** 宿主为本轮文件计划颁发的临时句柄,不能读取任意本地文件。 */
10
+ id: string;
11
+ files: readonly {
12
+ path: string;
13
+ hash: string;
14
+ }[];
15
+ deletions: readonly string[];
16
+ }
17
+ interface PublishInstanceBase {
18
+ describe(signal: AbortSignal): Promise<PublishTarget>;
19
+ checkConnection(signal: AbortSignal): Promise<boolean>;
20
+ dispose?(): void | Promise<void>;
21
+ }
22
+ export interface FilePublishInstance extends PublishInstanceBase {
23
+ mode: "files";
24
+ concurrency?: number;
25
+ uploadFile(path: string, content: Uint8Array, signal: AbortSignal): Promise<void>;
26
+ deleteFile(path: string, signal: AbortSignal): Promise<void>;
27
+ }
28
+ export interface SnapshotPublishInstance extends PublishInstanceBase {
29
+ mode: "snapshot";
30
+ publish(snapshot: PublicationSnapshot, signal: AbortSignal): Promise<void>;
31
+ }
32
+ export type PublishInstance = FilePublishInstance | SnapshotPublishInstance;
33
+ export interface PublishProviderDefinition {
34
+ id: string;
35
+ name: string;
36
+ create(config: JsonValue): PublishInstance;
37
+ }
38
+ export interface PluginPublish {
39
+ register(definition: PublishProviderDefinition): Disposable;
40
+ }
41
+ export interface GitSnapshotRequest {
42
+ publicationId: string;
43
+ remote: string;
44
+ branch: string;
45
+ author: {
46
+ name: string;
47
+ email: string;
48
+ };
49
+ authentication: {
50
+ username: string;
51
+ password: string;
52
+ };
53
+ cacheId?: string;
54
+ }
55
+ export interface GitCacheDirectory {
56
+ id: string;
57
+ label: string;
58
+ }
59
+ export {};
@@ -1,5 +1,6 @@
1
1
  import type { Descendant } from "slate";
2
2
  import type { MarkdownCodec } from "../../markdown/src/index";
3
+ import type { GitSnapshotRequest, GitCacheDirectory } from "./publish";
3
4
  export type { Descendant } from "slate";
4
5
  export interface ServiceOptions {
5
6
  signal?: AbortSignal;
@@ -38,7 +39,31 @@ export interface MediaInfo {
38
39
  channels?: number;
39
40
  }[];
40
41
  }
42
+ export interface VideoFormat {
43
+ id: string;
44
+ /** 只含视频或只含音频的流;两者都为 false 时是音视频合一的流。 */
45
+ videoOnly: boolean;
46
+ audioOnly: boolean;
47
+ ext?: string;
48
+ width?: number;
49
+ height?: number;
50
+ fps?: number;
51
+ /** 码率,单位 kbps。 */
52
+ bitrate?: number;
53
+ size?: number;
54
+ note?: string;
55
+ }
56
+ export interface VideoProbeResult {
57
+ title: string;
58
+ cover?: string;
59
+ duration?: number;
60
+ formats: VideoFormat[];
61
+ }
41
62
  export interface PluginServices {
63
+ git: {
64
+ publishSnapshot(input: GitSnapshotRequest, options?: ServiceOptions): Promise<void>;
65
+ selectCacheDirectory(input: Record<string, never>, options?: ServiceOptions): Promise<GitCacheDirectory | null>;
66
+ };
42
67
  tailwind: {
43
68
  compile(input: {
44
69
  source: string;
@@ -91,6 +116,12 @@ export interface PluginServices {
91
116
  log: string;
92
117
  }>;
93
118
  };
119
+ /** 宿主视频解析器(yt-dlp),只查询页面的标题与可下载格式,下载经视频播放源交给宿主。 */
120
+ videoExtractor: {
121
+ probe(url: string, options?: ServiceOptions & {
122
+ proxy?: string;
123
+ }): Promise<VideoProbeResult>;
124
+ };
94
125
  }
95
126
  /** 每个调用同时受调用方与插件实例生命周期约束。 */
96
127
  export declare function bindPluginServices(adapter: PluginServices | undefined, lifetime: AbortSignal, assertActive: () => void): PluginServices;
@@ -3,6 +3,10 @@ import type { Disposable, PluginContext } from "./index";
3
3
  import type { JsonValue } from "./json";
4
4
  export interface PluginSettingsProps {
5
5
  readonly context: PluginContext;
6
+ readonly publishConfig?: {
7
+ readonly value: JsonValue;
8
+ save(value: JsonValue): Promise<void>;
9
+ };
6
10
  readonly imageConfig?: {
7
11
  readonly instanceId: string;
8
12
  readonly value: JsonValue;
@@ -25,6 +29,10 @@ export interface PluginSettingsProps {
25
29
  readonly value: JsonValue;
26
30
  save(value: JsonValue): Promise<void>;
27
31
  };
32
+ readonly videoConfig?: {
33
+ readonly value: JsonValue;
34
+ save(value: JsonValue): Promise<void>;
35
+ };
28
36
  readonly syncConfig?: {
29
37
  readonly value: JsonValue;
30
38
  save(value: JsonValue): Promise<void>;
@@ -35,7 +43,7 @@ export interface PluginSettingsDefinition {
35
43
  title: string;
36
44
  description?: string;
37
45
  /** 提供方配置表单挂载到对应宿主设置;普通插件设置省略。 */
38
- target?: "sync" | "upload" | "tts" | "images";
46
+ target?: "sync" | "upload" | "tts" | "images" | "video" | "publish";
39
47
  component: ComponentType<PluginSettingsProps>;
40
48
  }
41
49
  export interface PluginSettingsHandle extends Disposable {
@@ -0,0 +1,58 @@
1
+ import type { Disposable, JsonValue } from "./contract";
2
+ /**
3
+ * 插件私有的视频引用,宿主原样写入文档与数据库;必须可长期保存,不放会过期的直链。
4
+ * 宿主在元数据中占用 type / title / thumbnail 三个键,引用不得使用。
5
+ */
6
+ export type VideoRef = {
7
+ readonly [key: string]: JsonValue;
8
+ };
9
+ export interface VideoStream {
10
+ url: string;
11
+ /** 宿主下载时附带的请求头(如 Referer、Cookie),不写入文档或缓存索引。 */
12
+ headers?: Record<string, string>;
13
+ }
14
+ /** 播放源描述:插件只说明流在哪里,下载、合并、缓存与播放由宿主完成。 */
15
+ export type VideoPlaybackSource = {
16
+ kind: "file";
17
+ stream: VideoStream;
18
+ } | {
19
+ kind: "split";
20
+ video: VideoStream;
21
+ audio: VideoStream;
22
+ } | {
23
+ kind: "extract";
24
+ /** 交给宿主视频解析服务的页面地址。 */
25
+ url: string;
26
+ videoFormat?: string;
27
+ audioFormat?: string;
28
+ proxy?: string;
29
+ };
30
+ export interface VideoVariant {
31
+ label: string;
32
+ hint?: string;
33
+ ref: VideoRef;
34
+ }
35
+ export interface ResolvedVideo {
36
+ title: string;
37
+ cover?: string;
38
+ /** 可选清晰度等变体,至少一项;用户选中项的 ref 被保存。 */
39
+ variants: readonly VideoVariant[];
40
+ defaultVariant?: number;
41
+ }
42
+ export interface VideoInstance {
43
+ /** 同步识别链接是否属于本提供方,不发网络请求。 */
44
+ matches(url: string): boolean;
45
+ resolve(url: string, signal: AbortSignal): Promise<ResolvedVideo>;
46
+ /** 稳定的缓存身份(应包含清晰度);已缓存时宿主不再调用 getPlayback。 */
47
+ cacheKey(ref: VideoRef): string;
48
+ getPlayback(ref: VideoRef, signal: AbortSignal): Promise<VideoPlaybackSource>;
49
+ }
50
+ export interface VideoProviderDefinition {
51
+ id: string;
52
+ name: string;
53
+ /** 按当前插件配置同步创建实例,不发网络请求。 */
54
+ create(config: JsonValue): VideoInstance;
55
+ }
56
+ export interface PluginVideo {
57
+ register(definition: VideoProviderDefinition): Disposable;
58
+ }