dsh-remotion 0.3.0 → 0.3.1

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/README.en.md CHANGED
@@ -10,7 +10,7 @@ DSH (DeepSeek Harness) video-creation skill plugin: installing it registers the
10
10
 
11
11
  ## Compatibility
12
12
 
13
- Verified against `@deepseek-ai/dsh@0.1.2-alpha.2` on 2026-08-31. Built for the cordis patch-bundle plugin model (`cordis.patch.yml` + `dsh.bundle.patch`). No runtime imports of `@deepseek-ai/*` internals.
13
+ Aligned with the skill registration contract of `@deepseek-ai/dsh@0.1.3-alpha.1` (2026-09-05). Built for the cordis patch-bundle plugin model (`cordis.patch.yml` + `dsh.bundle.patch`). No runtime imports of `@deepseek-ai/*` internals.
14
14
 
15
15
  ## Installation
16
16
 
@@ -35,7 +35,7 @@ Then restart the web service. To clean up fully, also remove the plugin entry fr
35
35
 
36
36
  ## Requirements
37
37
 
38
- Node.js ≥ 20 + npx (npm registry); rendering needs ffmpeg (guided by the skill itself).
38
+ Node.js ≥ 22 + npx (npm registry); rendering needs ffmpeg (guided by the skill itself).
39
39
 
40
40
  ## Porting notes
41
41
 
@@ -55,6 +55,35 @@ Skills use the open Agent Skills (SKILL.md) format — **not just DSH**. Copy th
55
55
  Port once, use everywhere.
56
56
 
57
57
 
58
+ ## Health checks and reloads
59
+
60
+ `remotion_health` rereads every `SKILL.md`, verifies readable files, valid YAML frontmatter, names matching their directories, and nonempty descriptions/bodies, then queries the host's `skills.get`. The effective name, description, body and resource directory must match this plugin instance's loaded snapshot. Existing files alone do not prove successful or still-active registration.
61
+
62
+ Health checks do not mutate files or registrations. After changing a file or repairing one that failed initial loading, reload the plugin (or restart DSH). The result reports `changed`, `not_registered` or `registration_failed` until then. A previously loaded file that was temporarily missing becomes healthy again if its exact original content is restored and its registration remains active.
63
+
64
+ Each item retains `name / ok / detail` and adds `code / fileOk / registered / registryChecked / reloadRequired`. `registered` means the registry still matches the loaded version, so a changed file can have `registered: true` and `ok: false`. Missing or failed registry lookup produces `registry_unavailable`; a disposed plugin produces `disposed`. The public `checkBundledSkills()` is disk-only, while the original non-throwing `parseSkillFile()` helper remains available.
65
+
66
+ ## Development and shared implementation
67
+
68
+ `src/index.ts` declares only package identity, skill names and the resource directory. Parsing, validation, registration and health logic live in `src/skill-bundle.ts`. The canonical source is in `dsh-hyperframes`; Remotion carries an identical version-controlled copy. Each package builds and ships its own `lib/skill-bundle.js`, with no cross-package runtime dependency and no sibling checkout required for building or installation.
69
+
70
+ With dependencies already installed:
71
+
72
+ ```bash
73
+ node node_modules/typescript/bin/tsc -p tsconfig.json
74
+ node --test "test/*.test.mjs"
75
+ ```
76
+
77
+ When developing the sibling repositories together, edit the shared module and regression tests in HyperFrames, then synchronize:
78
+
79
+ ```bash
80
+ # Run in dsh-hyperframes; updates only three shared files in sibling dsh-remotion
81
+ node scripts/sync-skill-bundle.mjs
82
+ node scripts/sync-skill-bundle.mjs --check
83
+ ```
84
+
85
+ Both suites compare shared source and regression tests to prevent drift. A standalone checkout skips only that cross-repository comparison. Tests cover invalid YAML, unreadable files, empty bodies, rejected/inactive registrations, file changes and repairs, disposal races and cleanup failures, without invoking video or speech services.
86
+
58
87
  ## License
59
88
 
60
89
  MIT for the porting arrangement; skill content copyright remains with Remotion.
package/README.md CHANGED
@@ -13,7 +13,7 @@ DSH(DeepSeek Harness)视频创作技能插件:**安装即把 Remotion 官
13
13
 
14
14
  ## 兼容性
15
15
 
16
- 在 `@deepseek-ai/dsh@0.1.2-alpha.2` 上验证(2026-08-31)。遵循 cordis 组合包补丁模型(`cordis.patch.yml` + `dsh.bundle.patch`),运行时不 import 任何 `@deepseek-ai/*` 内部模块。
16
+ 对齐 `@deepseek-ai/dsh@0.1.3-alpha.1` 的技能注册契约(2026-09-05)。遵循 cordis 组合包补丁模型(`cordis.patch.yml` + `dsh.bundle.patch`),运行时不 import 任何 `@deepseek-ai/*` 内部模块。
17
17
 
18
18
  ## 安装
19
19
 
@@ -38,7 +38,7 @@ dsh plugin --profile web remove dsh-remotion
38
38
 
39
39
  ## 依赖
40
40
 
41
- Node.js ≥ 20 + npx(npm registry);渲染需 ffmpeg(Remotion 自带指引)。
41
+ Node.js ≥ 22 + npx(npm registry);渲染需 ffmpeg(Remotion 自带指引)。
42
42
 
43
43
  ## 移植说明
44
44
 
@@ -58,6 +58,35 @@ Node.js ≥ 20 + npx(npm registry);渲染需 ffmpeg(Remotion 自带指
58
58
  一次移植,处处可用。
59
59
 
60
60
 
61
+ ## 技能自检与重载
62
+
63
+ 运行 `remotion_health` 会重新读取每个 `SKILL.md`,验证文件可读、YAML frontmatter 合法、名称与目录一致、描述与正文非空,再通过宿主 `skills.get` 确认实际生效的名称、描述、正文和资源目录与本次加载内容相符。文件存在但未注册、注册失败或后来被移除时,自检都会失败。
64
+
65
+ 自检不修改文件或注册表。修改技能文件,或修复加载时损坏的技能后,应重载插件(也可重启 DSH)。它会报告 `changed`、`not_registered` 或 `registration_failed`,不会把磁盘修复直接当成运行时修复。若原先已成功加载,只是文件暂时丢失,恢复与加载时完全相同的内容即可重新通过检查。
66
+
67
+ 每项结果保留 `name / ok / detail`,并增加 `code / fileOk / registered / registryChecked / reloadRequired`:`registered` 表示注册表仍匹配加载时的版本,因此文件变更时它可以为 true 而 `ok` 为 false。缺少或无法查询宿主注册表时返回 `registry_unavailable`;插件卸载后返回 `disposed`。`checkBundledSkills()` 仅检查磁盘文件,不推断运行时状态;原有不抛错的 `parseSkillFile()` 解析入口保持可用。
68
+
69
+ ## 开发与共享实现
70
+
71
+ `src/index.ts` 只声明包名、技能清单和目录,解析、校验、注册与自检集中在包内 `src/skill-bundle.ts`。规范源位于 `dsh-hyperframes/src/skill-bundle.ts`,Remotion 保存相同的受版本控制副本;两包都编译并分发自己的 `lib/skill-bundle.js`,没有跨包运行时依赖,也不需要另一个仓库即可构建或安装。
72
+
73
+ 依赖预先安装后,可直接离线构建和测试:
74
+
75
+ ```bash
76
+ node node_modules/typescript/bin/tsc -p tsconfig.json
77
+ node --test "test/*.test.mjs"
78
+ ```
79
+
80
+ 两个仓库并列开发时,在 HyperFrames 修改公共模块与公共回归测试,再同步:
81
+
82
+ ```bash
83
+ # 在 dsh-hyperframes 中执行;只更新相邻 dsh-remotion 的三个公共文件
84
+ node scripts/sync-skill-bundle.mjs
85
+ node scripts/sync-skill-bundle.mjs --check
86
+ ```
87
+
88
+ 两包测试都会检查公共源码与回归测试是否一致,防止副本漂移;单独检出时只跳过跨仓库比对,其余测试正常运行。测试覆盖损坏 YAML、不可读文件、空正文、注册异常/失效、内容变更与修复、卸载及清理异常,全程不调用视频或语音服务。
89
+
61
90
  ## License
62
91
 
63
92
  MIT(移植编排);技能内容版权归 Remotion。
package/lib/index.d.ts CHANGED
@@ -1,45 +1,12 @@
1
- /** cordis 服务注入:apply 里要用 ctx.skills,必须显式声明。 */
1
+ import { type SkillsPluginContext } from './skill-bundle.js';
2
+ export { parseSkillFile } from './skill-bundle.js';
3
+ export type { SkillRegistration, SkillsPluginContext } from './skill-bundle.js';
2
4
  export declare const name = "remotion-skills";
3
5
  export declare const inject: string[];
4
- /** 技能注册定义(最小面)。 */
5
- export interface SkillRegistration {
6
- name: string;
7
- description: string;
8
- content: string;
9
- resourceBase: {
10
- kind: 'directory';
11
- path: string;
12
- };
13
- source?: string;
14
- }
15
- /** 插件所需的最小 ctx 面。 */
16
- export interface SkillsPluginContext {
17
- skills: {
18
- register(definition: SkillRegistration): () => void;
19
- };
20
- tools?: {
21
- register(definition: Record<string, unknown>): () => void;
22
- };
23
- on?(event: string, listener: () => void): () => void;
24
- }
25
- /** 随包分发的技能清单(Remotion 官方 v4.0.519:总纲路由 + 11 个领域技能)。 */
26
6
  export declare const SKILL_NAMES: readonly ["remotion-best-practices", "remotion-captions", "remotion-create", "remotion-docs", "remotion-interactivity", "remotion-maps", "remotion-markup", "remotion-multimedia", "remotion-render", "remotion-saas", "remotion-studio", "remotion-upgrade"];
27
- /** 打包技能目录的绝对路径。 */
7
+ /** Absolute resource root shipped in this independently installable package. */
28
8
  export declare function bundledSkillsDir(): string;
29
- /** 检查随包技能资源完整性:每个技能的 SKILL.md 是否存在。 */
30
- export declare function checkBundledSkills(): Array<{
31
- name: string;
32
- ok: boolean;
33
- detail: string;
34
- }>;
35
- /** 拆分 SKILL.md 的 frontmatter 与正文。 */
36
- export declare function parseSkillFile(text: string): {
37
- name: string;
38
- description: string;
39
- content: string;
40
- };
41
- /**
42
- * 插件入口:把技能注册进技能注册表。
43
- * 单个技能文件缺失只告警,不影响其余技能与宿主启动。
44
- */
9
+ /** Re-read and validate disk resources; runtime state is checked by the health tool. */
10
+ export declare function checkBundledSkills(): import("./skill-bundle.js").SkillFileCheck[];
11
+ /** Register this package's skills and its read-only health tool. */
45
12
  export declare function apply(ctx: SkillsPluginContext): void;
package/lib/index.js CHANGED
@@ -1,19 +1,13 @@
1
1
  /**
2
- * dsh-remotion —— Remotion 视频创作技能插件:加载即把技能注册进 ctx.skills。
3
- *
4
- * 技能正文与资源随包分发(skills/ 目录),resourceBase 指向打包目录,
5
- * 技能正文里的相对引用(rules/、references/ 等)由技能加载器解析。
6
- *
7
- * @module dsh-remotion
2
+ * dsh-remotion: packaged video skills with disk and registry health checks.
3
+ * The portable implementation lives in skill-bundle.ts; this entry only
4
+ * supplies the package identity, skill names and resource directory.
8
5
  */
9
- import { existsSync, readFileSync } from 'node:fs';
10
6
  import { fileURLToPath } from 'node:url';
11
- import { join } from 'node:path';
12
- import YAML from 'yaml';
13
- /** cordis 服务注入:apply 里要用 ctx.skills,必须显式声明。 */
7
+ import { createSkillBundle } from './skill-bundle.js';
8
+ export { parseSkillFile } from './skill-bundle.js';
14
9
  export const name = 'remotion-skills';
15
10
  export const inject = ['skills', 'tools'];
16
- /** 随包分发的技能清单(Remotion 官方 v4.0.519:总纲路由 + 11 个领域技能)。 */
17
11
  export const SKILL_NAMES = [
18
12
  "remotion-best-practices",
19
13
  "remotion-captions",
@@ -28,93 +22,17 @@ export const SKILL_NAMES = [
28
22
  "remotion-studio",
29
23
  "remotion-upgrade",
30
24
  ];
31
- /** 打包技能目录的绝对路径。 */
25
+ /** Absolute resource root shipped in this independently installable package. */
32
26
  export function bundledSkillsDir() {
33
27
  return fileURLToPath(new URL('../skills/', import.meta.url));
34
28
  }
35
- /** 检查随包技能资源完整性:每个技能的 SKILL.md 是否存在。 */
36
- export function checkBundledSkills() {
37
- return SKILL_NAMES.map((skillName) => {
38
- const file = join(bundledSkillsDir(), skillName, 'SKILL.md');
39
- const ok = existsSync(file);
40
- return { name: skillName, ok, detail: ok ? file : 'SKILL.md 缺失:' + file };
41
- });
42
- }
43
- /** 拆分 SKILL.md 的 frontmatter 与正文。 */
44
- export function parseSkillFile(text) {
45
- const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(text);
46
- if (!match)
47
- return { name: '', description: '', content: text };
48
- let name = '';
49
- let description = '';
50
- try {
51
- const parsed = YAML.parse(match[1]);
52
- if (typeof parsed === 'object' && parsed !== null) {
53
- const record = parsed;
54
- if (typeof record.name === 'string')
55
- name = record.name;
56
- if (typeof record.description === 'string')
57
- description = record.description;
58
- }
59
- }
60
- catch { /* 保留空值,由调用方处理 */ }
61
- return { name, description, content: text.slice(match[0].length).trimStart() };
62
- }
63
- /**
64
- * 插件入口:把技能注册进技能注册表。
65
- * 单个技能文件缺失只告警,不影响其余技能与宿主启动。
66
- */
67
- export function apply(ctx) {
68
- const disposers = [];
69
- for (const skillName of SKILL_NAMES) {
70
- try {
71
- const dir = join(bundledSkillsDir(), skillName);
72
- const text = readFileSync(join(dir, 'SKILL.md'), 'utf8');
73
- const parsed = parseSkillFile(text);
74
- if (parsed.name === '' || parsed.description === '' || parsed.content === '') {
75
- console.warn('[dsh-remotion] 技能 ' + skillName + ' 的 frontmatter 不完整,已跳过。');
76
- continue;
77
- }
78
- disposers.push(ctx.skills.register({
79
- name: parsed.name,
80
- description: parsed.description,
81
- content: parsed.content,
82
- resourceBase: { kind: 'directory', path: dir },
83
- source: 'runtime',
84
- }));
85
- }
86
- catch (error) {
87
- console.warn('[dsh-remotion] 技能 ' + skillName + ' 加载失败:' + (error instanceof Error ? error.message : String(error)));
88
- }
89
- }
90
- if (typeof ctx.tools?.register === 'function') {
91
- disposers.push(ctx.tools.register({
92
- name: 'remotion_health',
93
- description: 'dsh-remotion 自检:检查随包技能资源(SKILL.md)是否完整。遇到问题时先运行本工具定位。',
94
- parameters: { type: 'object', properties: {} },
95
- output: {
96
- schema: { type: 'object', additionalProperties: true },
97
- render(_args, value) {
98
- const rec = (value ?? {});
99
- const skills = Array.isArray(rec.skills) ? rec.skills : [];
100
- const lines = ['dsh-remotion 自检' + (rec.ok === true ? ':技能资源完整。' : ':发现缺失。')];
101
- for (const item of skills) {
102
- const s = (item ?? {});
103
- lines.push('- ' + String(s.name) + ':' + (s.ok === true ? '✅' : '❌ ' + String(s.detail ?? '')));
104
- }
105
- return [{ type: 'text', text: lines.join('\n') }];
106
- },
107
- },
108
- async execute() {
109
- const skills = checkBundledSkills();
110
- return { ok: skills.every((s) => s.ok), plugin: 'dsh-remotion', skills };
111
- },
112
- }));
113
- }
114
- if (typeof ctx.on === 'function') {
115
- ctx.on('dispose', () => {
116
- for (const dispose of disposers)
117
- dispose();
118
- });
119
- }
120
- }
29
+ const bundle = createSkillBundle({
30
+ plugin: 'dsh-remotion',
31
+ healthTool: 'remotion_health',
32
+ names: SKILL_NAMES,
33
+ directory: bundledSkillsDir,
34
+ });
35
+ /** Re-read and validate disk resources; runtime state is checked by the health tool. */
36
+ export function checkBundledSkills() { return bundle.checkFiles(); }
37
+ /** Register this package's skills and its read-only health tool. */
38
+ export function apply(ctx) { bundle.apply(ctx); }
@@ -0,0 +1,56 @@
1
+ export interface SkillRegistration {
2
+ name: string;
3
+ description: string;
4
+ content: string;
5
+ resourceBase: {
6
+ kind: 'directory';
7
+ path: string;
8
+ };
9
+ source?: string;
10
+ }
11
+ export interface SkillsPluginContext {
12
+ skills: {
13
+ register(definition: SkillRegistration): () => void;
14
+ get?(name: string): Promise<SkillRegistration | undefined> | SkillRegistration | undefined;
15
+ };
16
+ tools?: {
17
+ register(definition: Record<string, unknown>): () => void;
18
+ };
19
+ on?(event: string, listener: () => void): (() => void) | void;
20
+ logger?: {
21
+ warn(message: string): void;
22
+ };
23
+ }
24
+ export interface ParsedSkillFile {
25
+ name: string;
26
+ description: string;
27
+ content: string;
28
+ }
29
+ export interface SkillFileCheck {
30
+ name: string;
31
+ ok: boolean;
32
+ detail: string;
33
+ file: string;
34
+ code: 'valid' | 'unreadable' | 'invalid';
35
+ }
36
+ export interface SkillHealthCheck extends Omit<SkillFileCheck, 'code'> {
37
+ code: SkillFileCheck['code'] | 'ready' | 'not_registered' | 'registration_failed' | 'changed' | 'inactive' | 'registry_unavailable' | 'disposed';
38
+ fileOk: boolean;
39
+ registered: boolean;
40
+ registryChecked: boolean;
41
+ reloadRequired: boolean;
42
+ }
43
+ export interface SkillBundleOptions {
44
+ plugin: string;
45
+ healthTool: string;
46
+ names: readonly string[];
47
+ directory: () => string;
48
+ }
49
+ /** Preserve the original non-throwing public parsing helper and return shape. */
50
+ export declare function parseSkillFile(text: string): ParsedSkillFile;
51
+ /** File validation is shared by initial loading and every later health call. */
52
+ export declare function createSkillBundle(options: SkillBundleOptions): {
53
+ /** Disk-only check; does not imply that any plugin instance registered the skills. */
54
+ checkFiles(): SkillFileCheck[];
55
+ apply(ctx: SkillsPluginContext): void;
56
+ };
@@ -0,0 +1,218 @@
1
+ /**
2
+ * Portable skill-bundle implementation. Canonical source: dsh-hyperframes.
3
+ * dsh-remotion vendors this exact file; scripts/sync-skill-bundle.mjs and
4
+ * test/shared-source.test.mjs prevent drift without a runtime dependency.
5
+ */
6
+ import { createHash } from 'node:crypto';
7
+ import { readFileSync } from 'node:fs';
8
+ import { join, resolve } from 'node:path';
9
+ import YAML from 'yaml';
10
+ // Matches Harness 0.1.3-alpha.1's public skill-name grammar.
11
+ const SKILL_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
12
+ function message(error) { return error instanceof Error ? error.message : String(error); }
13
+ /** One parser for the public helper, disk validation and runtime registration. */
14
+ function analyze(text) {
15
+ const match = /^\uFEFF?---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/.exec(text);
16
+ const parsed = { name: '', description: '', content: match ? text.slice(match[0].length).trimStart() : text };
17
+ if (!match)
18
+ return { parsed, error: '缺少完整的 YAML frontmatter(--- 分隔行)。' };
19
+ try {
20
+ const doc = YAML.parseDocument(match[1], { prettyErrors: false, uniqueKeys: true });
21
+ if (doc.errors.length || doc.warnings.length)
22
+ throw new Error([...doc.errors, ...doc.warnings].map((error) => error.message).join('; '));
23
+ const value = doc.toJS({ maxAliasCount: 100 });
24
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
25
+ return { parsed, error: 'frontmatter 必须是 YAML 对象。' };
26
+ const fields = value;
27
+ if (typeof fields.name === 'string')
28
+ parsed.name = fields.name;
29
+ if (typeof fields.description === 'string')
30
+ parsed.description = fields.description;
31
+ }
32
+ catch (error) {
33
+ return { parsed, error: 'frontmatter YAML 无效:' + message(error) };
34
+ }
35
+ if (!SKILL_NAME.test(parsed.name))
36
+ return { parsed, error: 'name 必须为非空小写 kebab-case 名称。' };
37
+ if (!parsed.description.trim())
38
+ return { parsed, error: 'description 必须为非空字符串。' };
39
+ if (!parsed.content.trim())
40
+ return { parsed, error: '技能正文为空。' };
41
+ return { parsed };
42
+ }
43
+ /** Preserve the original non-throwing public parsing helper and return shape. */
44
+ export function parseSkillFile(text) { return analyze(text).parsed; }
45
+ function inspect(directory, name) {
46
+ const file = join(directory, name, 'SKILL.md');
47
+ let text;
48
+ try {
49
+ text = readFileSync(file, 'utf8');
50
+ }
51
+ catch (error) {
52
+ return { check: { name, file, ok: false, code: 'unreadable', detail: 'SKILL.md 不可读取:' + message(error) } };
53
+ }
54
+ const result = analyze(text);
55
+ const error = result.error ?? (result.parsed.name === name ? undefined : 'frontmatter name 与技能目录名称不一致:' + result.parsed.name);
56
+ if (error)
57
+ return { check: { name, file, ok: false, code: 'invalid', detail: error } };
58
+ return {
59
+ check: { name, file, ok: true, code: 'valid', detail: file },
60
+ parsed: result.parsed,
61
+ fingerprint: createHash('sha256').update(text).digest('hex'),
62
+ };
63
+ }
64
+ function matches(actual, expected) {
65
+ return actual !== undefined && actual.name === expected.name && actual.description === expected.description
66
+ && actual.content === expected.content && actual.resourceBase?.kind === 'directory'
67
+ && resolve(actual.resourceBase.path) === resolve(expected.resourceBase.path);
68
+ }
69
+ /** File validation is shared by initial loading and every later health call. */
70
+ export function createSkillBundle(options) {
71
+ const names = [...options.names];
72
+ if (new Set(names).size !== names.length || names.some((name) => !SKILL_NAME.test(name)))
73
+ throw new Error('Skill bundle requires unique kebab-case directory names');
74
+ const inspectAll = () => names.map((name) => inspect(resolve(options.directory()), name));
75
+ return {
76
+ /** Disk-only check; does not imply that any plugin instance registered the skills. */
77
+ checkFiles() { return inspectAll().map((result) => result.check); },
78
+ apply(ctx) {
79
+ const registrations = new Map();
80
+ const disposers = [];
81
+ let disposed = false;
82
+ const warn = (detail) => {
83
+ const text = '[' + options.plugin + '] ' + detail;
84
+ if (ctx.logger?.warn)
85
+ ctx.logger.warn(text);
86
+ else
87
+ console.warn(text);
88
+ };
89
+ const dispose = () => {
90
+ if (disposed)
91
+ return;
92
+ disposed = true;
93
+ for (const remove of disposers.splice(0).reverse()) {
94
+ try {
95
+ remove();
96
+ }
97
+ catch (error) {
98
+ warn('卸载失败:' + message(error));
99
+ }
100
+ }
101
+ };
102
+ ctx.on?.('dispose', dispose);
103
+ for (const result of inspectAll()) {
104
+ const registration = {};
105
+ registrations.set(result.check.name, registration);
106
+ if (!result.parsed || !result.fingerprint) {
107
+ warn(result.check.name + ':' + result.check.detail);
108
+ continue;
109
+ }
110
+ const definition = {
111
+ ...result.parsed,
112
+ resourceBase: { kind: 'directory', path: join(resolve(options.directory()), result.check.name) },
113
+ source: 'runtime',
114
+ };
115
+ try {
116
+ const remove = ctx.skills.register(definition);
117
+ if (typeof remove !== 'function')
118
+ throw new Error('skills.register 未返回卸载函数。');
119
+ disposers.push(remove);
120
+ registration.definition = definition;
121
+ registration.fingerprint = result.fingerprint;
122
+ }
123
+ catch (error) {
124
+ registration.error = message(error);
125
+ warn(result.check.name + ' 注册失败:' + registration.error);
126
+ }
127
+ }
128
+ const health = async () => {
129
+ const skills = [];
130
+ for (const result of inspectAll()) {
131
+ const registration = registrations.get(result.check.name);
132
+ let registered = false;
133
+ let registryChecked = false;
134
+ let registryError;
135
+ if (!disposed && registration.definition) {
136
+ if (typeof ctx.skills.get !== 'function')
137
+ registryError = '宿主未提供 skills.get,无法确认技能是否实际生效。';
138
+ else {
139
+ try {
140
+ registered = matches(await ctx.skills.get(result.check.name), registration.definition);
141
+ registryChecked = true;
142
+ }
143
+ catch (error) {
144
+ registryError = '读取宿主技能注册表失败:' + message(error);
145
+ }
146
+ }
147
+ }
148
+ let code = 'ready';
149
+ let detail = result.check.file;
150
+ let reloadRequired = false;
151
+ if (disposed) {
152
+ code = 'disposed';
153
+ registered = false;
154
+ detail = '插件已卸载。';
155
+ reloadRequired = true;
156
+ }
157
+ else if (!result.check.ok) {
158
+ code = result.check.code;
159
+ detail = result.check.detail;
160
+ }
161
+ else if (registration.error) {
162
+ code = 'registration_failed';
163
+ detail = '加载时注册失败:' + registration.error;
164
+ reloadRequired = true;
165
+ }
166
+ else if (!registration.definition) {
167
+ code = 'not_registered';
168
+ detail = '文件现已有效,但加载时未注册;请重载插件。';
169
+ reloadRequired = true;
170
+ }
171
+ else if (result.fingerprint !== registration.fingerprint) {
172
+ code = 'changed';
173
+ detail = '文件已变化,当前注册仍是加载时版本;请重载插件。';
174
+ reloadRequired = true;
175
+ }
176
+ else if (registryError) {
177
+ code = 'registry_unavailable';
178
+ detail = registryError;
179
+ }
180
+ else if (!registered) {
181
+ code = 'inactive';
182
+ detail = '宿主注册表中的技能缺失或已被其他内容替代;请重载插件。';
183
+ reloadRequired = true;
184
+ }
185
+ skills.push({ ...result.check, ok: code === 'ready', code, detail, fileOk: result.check.ok, registered, registryChecked, reloadRequired });
186
+ }
187
+ return { ok: !disposed && skills.every((skill) => skill.ok), plugin: options.plugin, skills };
188
+ };
189
+ if (typeof ctx.tools?.register === 'function') {
190
+ try {
191
+ const remove = ctx.tools.register({
192
+ name: options.healthTool,
193
+ description: options.plugin + ' 自检:重新验证技能文件、内容与当前宿主注册状态。文件变化或修复后提示重载;自检不修改注册表。',
194
+ parameters: { type: 'object', properties: {} },
195
+ output: {
196
+ schema: { type: 'object', additionalProperties: true },
197
+ render(_args, value) {
198
+ const result = value;
199
+ const lines = [options.plugin + ' 自检' + (result.ok ? ':技能资源完整且已注册。' : ':发现技能文件或注册状态异常。')];
200
+ for (const skill of result.skills)
201
+ lines.push('- ' + skill.name + ':' + (skill.ok ? '✅' : '❌ ' + skill.detail));
202
+ return [{ type: 'text', text: lines.join('\n') }];
203
+ },
204
+ },
205
+ execute: health,
206
+ });
207
+ if (typeof remove !== 'function')
208
+ throw new Error('tools.register 未返回卸载函数。');
209
+ disposers.push(remove);
210
+ }
211
+ catch (error) {
212
+ dispose();
213
+ throw error;
214
+ }
215
+ }
216
+ },
217
+ };
218
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-remotion",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "DSH 视频创作技能插件:注册 Remotion 官方移植技能(React 编程式视频:动画/音频/字幕/3D/图表/字体,38 个规则文件),安装即用。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",