@oh-story/dsh 0.1.6 → 0.1.8

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.
Files changed (41) hide show
  1. package/README.md +29 -12
  2. package/lib/client.js +356 -89
  3. package/lib/client.js.map +4 -4
  4. package/lib/drama/manifest.json +34 -34
  5. package/lib/drama/skills/short-drama/SKILL.md +4 -1
  6. package/lib/drama/skills/short-drama/references/contract-and-ownership.md +2 -0
  7. package/lib/drama/skills/short-drama/references/creator-documents.md +30 -4
  8. package/lib/drama/skills/short-drama/references/creator-workflow.md +8 -2
  9. package/lib/drama/skills/short-drama/references/reference-roles.md +16 -2
  10. package/lib/drama/skills/short-drama/scripts/creator_markdown_check.py +328 -48
  11. package/lib/drama/skills/short-drama-produce/SKILL.md +4 -1
  12. package/lib/drama/skills/short-drama-produce/scripts/production_tool.py +34 -2
  13. package/lib/drama/skills/short-drama-storyboard/SKILL.md +27 -5
  14. package/lib/drama/skills/short-drama-storyboard/references/review-and-fixtures.md +2 -1
  15. package/lib/drama/skills/short-drama-storyboard/references/stage-contract.md +2 -2
  16. package/lib/drama/skills/short-drama-video-prompts/SKILL.md +11 -2
  17. package/lib/drama/skills/short-drama-video-prompts/assets/video-prompts.md +6 -1
  18. package/lib/drama/skills/short-drama-video-prompts/references/minimax-h3.md +19 -3
  19. package/lib/drama/skills/short-drama-video-prompts/references/stage-contract.md +8 -2
  20. package/lib/drama/skills/short-drama-video-prompts/references/target-model-profile.md +1 -0
  21. package/lib/index.js +194 -62
  22. package/lib/index.js.map +4 -4
  23. package/lib/types/client/drama-production-view.d.ts +1 -0
  24. package/lib/types/client/drama-production-view.d.ts.map +1 -1
  25. package/lib/types/client/drama-production.d.ts +3 -0
  26. package/lib/types/client/drama-production.d.ts.map +1 -1
  27. package/lib/types/client/index.d.ts.map +1 -1
  28. package/lib/types/client/video-studio.d.ts +2 -1
  29. package/lib/types/client/video-studio.d.ts.map +1 -1
  30. package/lib/types/client/workbench-presence.d.ts +29 -0
  31. package/lib/types/client/workbench-presence.d.ts.map +1 -0
  32. package/lib/types/drama-adapters.d.ts +65 -0
  33. package/lib/types/drama-adapters.d.ts.map +1 -0
  34. package/lib/types/host-python.d.ts +19 -0
  35. package/lib/types/host-python.d.ts.map +1 -0
  36. package/lib/types/index.d.ts +1 -0
  37. package/lib/types/index.d.ts.map +1 -1
  38. package/lib/types/skill-provider.d.ts +1 -1
  39. package/lib/types/skill-provider.d.ts.map +1 -1
  40. package/lib/types/workspace-route.d.ts.map +1 -1
  41. package/package.json +41 -41
package/lib/index.js CHANGED
@@ -3,7 +3,107 @@ import z from "@deepseek-ai/schemastery";
3
3
 
4
4
  // src/skill-provider.ts
5
5
  import { readdir, readFile } from "node:fs/promises";
6
- import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
6
+ import { basename, dirname as dirname2, isAbsolute, join as join2, relative, resolve as resolve2 } from "node:path";
7
+
8
+ // src/drama-adapters.ts
9
+ import { createHash } from "node:crypto";
10
+ import { access, lstat, mkdir, rename, writeFile } from "node:fs/promises";
11
+ import { tmpdir } from "node:os";
12
+ import { dirname, join, resolve } from "node:path";
13
+ var DRAMA_ADAPTER_CONFIG_ENV = "OH_STORY_DRAMA_ADAPTER_CONFIG";
14
+ var PROVIDER_SCRIPT = "short-drama-produce/scripts/provider_adapters.py";
15
+ var DRAMA_ADAPTERS = [
16
+ {
17
+ name: "gpt-image-2",
18
+ label: "GPT Image 2",
19
+ modality: "image",
20
+ requiredEnv: ["OPENAI_API_KEY"],
21
+ optionalEnv: ["OPENAI_BASE_URL"],
22
+ timeoutSeconds: 600,
23
+ reference: "short-drama-produce/references/providers/gpt-image-2.md"
24
+ },
25
+ {
26
+ name: "seedance",
27
+ label: "Seedance",
28
+ modality: "video",
29
+ requiredEnv: ["ARK_API_KEY", "SEEDANCE_MODEL"],
30
+ optionalEnv: ["SEEDANCE_BASE_URL", "SEEDANCE_ALLOWED_RATIOS", "SEEDANCE_MIN_DURATION", "SEEDANCE_MAX_DURATION"],
31
+ timeoutSeconds: 3600,
32
+ reference: "short-drama-produce/references/providers/seedance.md"
33
+ },
34
+ {
35
+ name: "minimax-h3",
36
+ label: "MiniMax H3",
37
+ modality: "video",
38
+ requiredEnv: ["MINIMAX_API_KEY", "MINIMAX_VIDEO_MODEL", "MINIMAX_VIDEO_RESOLUTIONS"],
39
+ optionalEnv: ["MINIMAX_VIDEO_BASE_URL", "MINIMAX_VIDEO_RATIOS", "MINIMAX_VIDEO_MIN_DURATION", "MINIMAX_VIDEO_MAX_DURATION"],
40
+ timeoutSeconds: 3600,
41
+ reference: "short-drama-produce/references/providers/minimax-h3-video.md"
42
+ },
43
+ {
44
+ name: "minimax-music",
45
+ label: "MiniMax Music",
46
+ modality: "music",
47
+ requiredEnv: ["MINIMAX_API_KEY"],
48
+ optionalEnv: ["MINIMAX_BASE_URL"],
49
+ timeoutSeconds: 600,
50
+ reference: "short-drama-produce/references/providers/minimax-music.md"
51
+ }
52
+ ];
53
+ function generatedDirectory(temporaryRoot) {
54
+ const uid = process.getuid?.();
55
+ return join(temporaryRoot, uid === void 0 ? "oh-story-dsh" : `oh-story-dsh-${String(uid)}`);
56
+ }
57
+ function dramaAdapterConfigPath(skillRoot, env = process.env, temporaryRoot = tmpdir()) {
58
+ const custom = env[DRAMA_ADAPTER_CONFIG_ENV] ?? "";
59
+ if (custom !== "") return { path: resolve(custom), generated: false };
60
+ const key = createHash("sha256").update(resolve(skillRoot)).digest("hex").slice(0, 12);
61
+ return { path: join(generatedDirectory(temporaryRoot), `drama-adapters-${key}.json`), generated: true };
62
+ }
63
+ async function privateDirectory(path) {
64
+ await mkdir(path, { recursive: true, mode: 448 });
65
+ const info = await lstat(path);
66
+ if (!info.isDirectory()) return false;
67
+ const uid = process.getuid?.();
68
+ return uid === void 0 || info.uid === uid;
69
+ }
70
+ function dramaAdapterConfigDocument(skillRoot, python = "python3") {
71
+ const script = resolve(skillRoot, PROVIDER_SCRIPT);
72
+ const adapters = {};
73
+ for (const adapter of DRAMA_ADAPTERS) {
74
+ adapters[adapter.name] = { command: [python, script, adapter.name], timeout_seconds: adapter.timeoutSeconds };
75
+ }
76
+ return { adapters };
77
+ }
78
+ async function ensureDramaAdapterConfig(skillRoot, options = {}) {
79
+ const location = dramaAdapterConfigPath(skillRoot, options.env ?? process.env, options.temporaryRoot ?? tmpdir());
80
+ if (!location.generated) {
81
+ return { ...location, ok: await access(location.path).then(() => true, () => false) };
82
+ }
83
+ try {
84
+ if (!await privateDirectory(dirname(location.path))) return { ...location, ok: false };
85
+ const existing = await lstat(location.path).catch(() => void 0);
86
+ if (existing !== void 0 && !existing.isFile()) return { ...location, ok: false };
87
+ const staging = `${location.path}.${String(process.pid)}.tmp`;
88
+ await writeFile(staging, `${JSON.stringify(dramaAdapterConfigDocument(skillRoot, options.python ?? "python3"), null, 2)}
89
+ `, { encoding: "utf8", mode: 384 });
90
+ await rename(staging, location.path);
91
+ return { ...location, ok: true };
92
+ } catch {
93
+ return { ...location, ok: false };
94
+ }
95
+ }
96
+ function dramaAdapterStatuses(env = process.env) {
97
+ return DRAMA_ADAPTERS.map((adapter) => {
98
+ const missing = adapter.requiredEnv.filter((name2) => (env[name2] ?? "") === "");
99
+ return { name: adapter.name, label: adapter.label, modality: adapter.modality, configured: missing.length === 0, missing };
100
+ });
101
+ }
102
+ function dramaAdapterSummary() {
103
+ return DRAMA_ADAPTERS.map((adapter) => `${adapter.name} (${adapter.modality}: ${adapter.requiredEnv.join(" + ")})`).join(", ");
104
+ }
105
+
106
+ // src/skill-provider.ts
7
107
  import { fileURLToPath, pathToFileURL } from "node:url";
8
108
  import {
9
109
  BUNDLED_SKILL_RANK
@@ -50,7 +150,7 @@ var DSH_DRAMA_BRIDGE = [
50
150
  ].join("\n");
51
151
  var DSH_DRAMA_OVERRIDES = {
52
152
  "short-drama": "A dashboard request means focus or use the native \u77ED\u5267 tab in this DSH Session. Do not run the bundled standalone Dashboard script. New projects follow only the v0.6 creator-first contract and create only documents required by the current request.",
53
- "short-drama-produce": "Use an upstream adapter only after the creator explicitly confirms the exact current job. Its source must be the current creator-first Markdown and its output belongs under \u5267\u96C6/<EP>/\u5236\u4F5C\u6210\u679C/. DSH permissions and approval UI remain authoritative. When oh_story_production is visible, register the previewed job with track_job after the confirmation is valid and before run \u2014 never at prepare time \u2014 passing the same job ID, target, modality and count the creator just approved.",
153
+ "short-drama-produce": (skillRoot) => `Use an upstream adapter only after the creator explicitly confirms the exact current job. Its source must be the current creator-first Markdown and its output belongs under \u5267\u96C6/<EP>/\u5236\u4F5C\u6210\u679C/. DSH permissions and approval UI remain authoritative. When oh_story_production is visible, register the previewed job with track_job after the confirmation is valid and before run \u2014 never at prepare time \u2014 passing the same job ID, target, modality and count the creator just approved. DeepSeek generates no media: every image, video, or music result comes from a provider adapter. The bundled adapters are ${dramaAdapterSummary()}; they are registered for this DSH host in ${dramaAdapterConfigPath(skillRoot).path} \u2014 pass that file as --adapter-config unless the creator names another config (${DRAMA_ADAPTER_CONFIG_ENV}). Credentials are read only from the DSH host process environment. Before preparing a paid job, name the adapter that will run it; if its variables are missing, tell the creator exactly which ones to export before starting DSH instead of failing inside run. Never read, print, or write credential values.`,
54
154
  "short-drama-assets": "Keep one stable `- ID\uFF1AVISUAL-*` line on every \u4EBA\u7269/\u9020\u578B/\u5730\u70B9/\u9053\u5177/\u72B6\u6001 entry in \u89C6\u89C9\u8BBE\u5B9A.md, and never change an existing ID when editing its heading or text. The \u751F\u4EA7 view identifies canvas nodes by that ID; entries without one still render, but the workbench reports their node identity as unstable."
55
155
  };
56
156
  var DSH_GAME_BRIDGE = [
@@ -196,25 +296,25 @@ function dshSkillContent(name2, content) {
196
296
  ${native ?? content}`;
197
297
  }
198
298
  function defaultBundledSkillRoot() {
199
- const current = dirname(fileURLToPath(import.meta.url));
200
- return basename(current) === "src" ? resolve(current, "../../knowledge/oh-story/skills") : resolve(current, "oh-story/skills");
299
+ const current = dirname2(fileURLToPath(import.meta.url));
300
+ return basename(current) === "src" ? resolve2(current, "../../knowledge/oh-story/skills") : resolve2(current, "oh-story/skills");
201
301
  }
202
302
  function defaultDramaSkillRoot() {
203
- const current = dirname(fileURLToPath(import.meta.url));
204
- return basename(current) === "src" ? resolve(current, "../../knowledge/drama/skills") : resolve(current, "drama/skills");
303
+ const current = dirname2(fileURLToPath(import.meta.url));
304
+ return basename(current) === "src" ? resolve2(current, "../../knowledge/drama/skills") : resolve2(current, "drama/skills");
205
305
  }
206
306
  function defaultNovelToGameSkillRoot() {
207
- const current = dirname(fileURLToPath(import.meta.url));
208
- return basename(current) === "src" ? resolve(current, "../../knowledge/novel-to-game/skills") : resolve(current, "novel-to-game/skills");
307
+ const current = dirname2(fileURLToPath(import.meta.url));
308
+ return basename(current) === "src" ? resolve2(current, "../../knowledge/novel-to-game/skills") : resolve2(current, "novel-to-game/skills");
209
309
  }
210
310
  function createBundledSkillProvider(providerName, skillRoot, content) {
211
- const root = resolve(skillRoot);
311
+ const root = resolve2(skillRoot);
212
312
  return {
213
313
  name: providerName,
214
314
  async list() {
215
315
  const directories = (await readdir(root, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
216
316
  return Promise.all(directories.map(async (directory) => {
217
- const path = join(root, directory, "SKILL.md");
317
+ const path = join2(root, directory, "SKILL.md");
218
318
  const parsed = parseBundledSkill(await readFile(path, "utf8"));
219
319
  if (parsed.name !== directory) throw new Error(`Bundled skill directory "${directory}" does not match name "${parsed.name}".`);
220
320
  return {
@@ -223,7 +323,7 @@ function createBundledSkillProvider(providerName, skillRoot, content) {
223
323
  invocation: { modelInvocable: true, userInvocable: parsed.userInvocable },
224
324
  provider: providerName,
225
325
  source: "bundled",
226
- resourceBase: { kind: "directory", path: join(root, directory) },
326
+ resourceBase: { kind: "directory", path: join2(root, directory) },
227
327
  rank: BUNDLED_SKILL_RANK,
228
328
  locator: pathToFileURL(path),
229
329
  path
@@ -232,7 +332,7 @@ function createBundledSkillProvider(providerName, skillRoot, content) {
232
332
  },
233
333
  async get(candidate) {
234
334
  if (candidate.provider !== providerName || typeof candidate.path !== "string") return void 0;
235
- const path = resolve(candidate.path);
335
+ const path = resolve2(candidate.path);
236
336
  const relativePath = relative(root, path);
237
337
  if (relativePath === "" || relativePath.startsWith("..") || isAbsolute(relativePath)) {
238
338
  throw new Error("Bundled skill locator escaped the packaged skill root.");
@@ -245,7 +345,7 @@ function createBundledSkillProvider(providerName, skillRoot, content) {
245
345
  invocation: { modelInvocable: true, userInvocable: parsed.userInvocable },
246
346
  provider: providerName,
247
347
  source: "bundled",
248
- resourceBase: { kind: "directory", path: join(root, parsed.name) },
348
+ resourceBase: { kind: "directory", path: join2(root, parsed.name) },
249
349
  path,
250
350
  content: content(parsed.name, parsed.content)
251
351
  };
@@ -255,15 +355,16 @@ function createBundledSkillProvider(providerName, skillRoot, content) {
255
355
  function createOhStorySkillProvider(skillRoot = defaultBundledSkillRoot()) {
256
356
  return createBundledSkillProvider(STORY_PROVIDER_NAME, skillRoot, dshSkillContent);
257
357
  }
258
- function dshDramaSkillContent(name2, content) {
259
- const override = DSH_DRAMA_OVERRIDES[name2];
358
+ function dshDramaSkillContent(name2, content, skillRoot = defaultDramaSkillRoot()) {
359
+ const entry = DSH_DRAMA_OVERRIDES[name2];
360
+ const override = typeof entry === "function" ? entry(skillRoot) : entry;
260
361
  return `${DSH_DRAMA_BRIDGE}${override === void 0 ? "" : `
261
362
  <skill-specific-dsh-override>${override}</skill-specific-dsh-override>`}
262
363
 
263
364
  ${content}`;
264
365
  }
265
366
  function createDramaSkillProvider(skillRoot = defaultDramaSkillRoot()) {
266
- return createBundledSkillProvider(DRAMA_PROVIDER_NAME, skillRoot, dshDramaSkillContent);
367
+ return createBundledSkillProvider(DRAMA_PROVIDER_NAME, skillRoot, (name2, content) => dshDramaSkillContent(name2, content, skillRoot));
267
368
  }
268
369
  function dshNovelToGameSkillContent(_name, content) {
269
370
  return `${DSH_GAME_BRIDGE}
@@ -274,8 +375,8 @@ function createNovelToGameSkillProvider(skillRoot = defaultNovelToGameSkillRoot(
274
375
  return createBundledSkillProvider(GAME_PROVIDER_NAME, skillRoot, dshNovelToGameSkillContent);
275
376
  }
276
377
  function defaultVideoRecapSkillRoot() {
277
- const current = dirname(fileURLToPath(import.meta.url));
278
- return basename(current) === "src" ? resolve(current, "../../knowledge/video-recap/skills") : resolve(current, "video-recap/skills");
378
+ const current = dirname2(fileURLToPath(import.meta.url));
379
+ return basename(current) === "src" ? resolve2(current, "../../knowledge/video-recap/skills") : resolve2(current, "video-recap/skills");
279
380
  }
280
381
  function dshVideoRecapSkillContent(_name, content) {
281
382
  return `${DSH_VIDEO_BRIDGE}
@@ -286,6 +387,35 @@ function createVideoRecapSkillProvider(skillRoot = defaultVideoRecapSkillRoot())
286
387
  return createBundledSkillProvider(VIDEO_PROVIDER_NAME, skillRoot, dshVideoRecapSkillContent);
287
388
  }
288
389
 
390
+ // src/host-python.ts
391
+ import { execFile } from "node:child_process";
392
+ import { promisify } from "node:util";
393
+ var execFileAsync = promisify(execFile);
394
+ async function commandOutput(command, args) {
395
+ try {
396
+ const { stdout, stderr } = await execFileAsync(command, [...args], { encoding: "utf8", timeout: 5e3, maxBuffer: 4 * 1024 * 1024 });
397
+ return `${stdout}${stderr}`.trim();
398
+ } catch {
399
+ return void 0;
400
+ }
401
+ }
402
+ function pythonVersion(value) {
403
+ const match = /Python\s+(\d+)\.(\d+)(?:\.(\d+))?/u.exec(value ?? "");
404
+ if (match === null) return { ok: false };
405
+ const major = Number(match[1]);
406
+ const minor = Number(match[2]);
407
+ return { ok: major > 3 || major === 3 && minor >= 10, version: match[0].replace(/^Python\s+/u, "") };
408
+ }
409
+ async function hostPython(probe = commandOutput) {
410
+ let fallback;
411
+ for (const command of ["python3", "python"]) {
412
+ const parsed = pythonVersion(await probe(command, ["--version"]));
413
+ if (parsed.ok) return { command, probe: parsed };
414
+ if (parsed.version !== void 0) fallback ??= { command, probe: parsed };
415
+ }
416
+ return fallback ?? { command: "python3", probe: { ok: false } };
417
+ }
418
+
289
419
  // src/native-hooks.ts
290
420
  import { createUserMessage } from "@deepseek-ai/dsh-llm";
291
421
  var MUTATION_TOOLS = /* @__PURE__ */ new Set(["write", "edit", "str_replace_editor"]);
@@ -395,21 +525,21 @@ import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools";
395
525
 
396
526
  // src/reference-tool.ts
397
527
  import { readdir as readdir2, readFile as readFile2, realpath } from "node:fs/promises";
398
- import { isAbsolute as isAbsolute2, join as join2, relative as relative2, resolve as resolve2 } from "node:path";
528
+ import { isAbsolute as isAbsolute2, join as join3, relative as relative2, resolve as resolve3 } from "node:path";
399
529
  import { defineTool } from "@deepseek-ai/dsh-tools";
400
530
  var OH_STORY_REFERENCE_TOOL_NAME = "oh_story_bundled_reference";
401
531
  async function collectMarkdown(root, directory = root) {
402
532
  const entries = await readdir2(directory, { withFileTypes: true });
403
533
  const nested = await Promise.all(entries.map(async (entry) => {
404
- const path = join2(directory, entry.name);
534
+ const path = join3(directory, entry.name);
405
535
  if (entry.isDirectory()) return collectMarkdown(root, path);
406
536
  return entry.isFile() && entry.name.endsWith(".md") ? [path] : [];
407
537
  }));
408
538
  return nested.flat();
409
539
  }
410
540
  async function bundledReferences(storySetupRoot) {
411
- const root = await realpath(resolve2(storySetupRoot));
412
- const referenceRoot = await realpath(join2(root, "references", "agent-references"));
541
+ const root = await realpath(resolve3(storySetupRoot));
542
+ const referenceRoot = await realpath(join3(root, "references", "agent-references"));
413
543
  const referenceDirectory = relative2(root, referenceRoot);
414
544
  if (referenceDirectory === "" || referenceDirectory.startsWith("..") || isAbsolute2(referenceDirectory)) {
415
545
  throw new Error("Bundled Oh Story reference directory escaped its package root.");
@@ -427,7 +557,7 @@ async function bundledReferences(storySetupRoot) {
427
557
  }));
428
558
  return values.sort((left, right) => left.name.localeCompare(right.name));
429
559
  }
430
- async function createOhStoryReferenceTool(storySetupRoot = join2(defaultBundledSkillRoot(), "story-setup")) {
560
+ async function createOhStoryReferenceTool(storySetupRoot = join3(defaultBundledSkillRoot(), "story-setup")) {
431
561
  const references = await bundledReferences(storySetupRoot);
432
562
  if (references.length === 0) throw new Error("No bundled Oh Story references were found.");
433
563
  const paths = references.map((reference) => reference.name);
@@ -473,7 +603,7 @@ function bundledReferenceGuard(definition, tools) {
473
603
 
474
604
  // src/role-provider.ts
475
605
  import { readFile as readFile3 } from "node:fs/promises";
476
- import { basename as basename2, dirname as dirname2, join as join3, resolve as resolve3 } from "node:path";
606
+ import { basename as basename2, dirname as dirname3, join as join4, resolve as resolve4 } from "node:path";
477
607
  import { fileURLToPath as fileURLToPath2 } from "node:url";
478
608
  var OH_STORY_ROLE_NAMES = [
479
609
  "chapter-extractor",
@@ -489,11 +619,11 @@ function roleBody(source) {
489
619
  return source.slice(frontmatter?.[0].length ?? 0).trim();
490
620
  }
491
621
  function defaultBundledRoleRoot() {
492
- const current = dirname2(fileURLToPath2(import.meta.url));
493
- return basename2(current) === "src" ? resolve3(current, "../../knowledge/oh-story/roles") : resolve3(current, "oh-story/roles");
622
+ const current = dirname3(fileURLToPath2(import.meta.url));
623
+ return basename2(current) === "src" ? resolve4(current, "../../knowledge/oh-story/roles") : resolve4(current, "oh-story/roles");
494
624
  }
495
625
  async function loadBundledRole(name2, roleRoot = defaultBundledRoleRoot(), execution = "tool-free") {
496
- const body = roleBody(await readFile3(join3(resolve3(roleRoot), `${name2}.md`), "utf8"));
626
+ const body = roleBody(await readFile3(join4(resolve4(roleRoot), `${name2}.md`), "utf8"));
497
627
  if (body.length === 0) throw new Error(`Bundled role "${name2}" is empty.`);
498
628
  const integration = execution === "tool-free" ? [
499
629
  "You are running inside an Oh Story review-required DSH collaboration. The caller supplies every permitted input in the prompt.",
@@ -723,12 +853,10 @@ function registerOhStoryProductionTool(context) {
723
853
  }
724
854
 
725
855
  // src/workspace-route.ts
726
- import { createHash } from "node:crypto";
856
+ import { createHash as createHash2 } from "node:crypto";
727
857
  import { createReadStream } from "node:fs";
728
858
  import { readFile as readNodeFile, realpath as nodeRealpath, stat as nodeStat } from "node:fs/promises";
729
- import { extname as extname2, isAbsolute as isAbsolute3, relative as relative3, resolve as resolve4 } from "node:path";
730
- import { execFile } from "node:child_process";
731
- import { promisify } from "node:util";
859
+ import { extname as extname2, isAbsolute as isAbsolute3, relative as relative3, resolve as resolve5 } from "node:path";
732
860
  import { FsError } from "@deepseek-ai/dsh-fs";
733
861
  import { SessionId } from "@deepseek-ai/dsh-session";
734
862
 
@@ -991,8 +1119,8 @@ var FILE_LIMIT = 1e3;
991
1119
  var PREVIEW_FILE_LIMIT = 32 * 1024 * 1024;
992
1120
  var BUNDLED_GAME_EXAMPLE = "jin-ping-mei";
993
1121
  var workspaceVerificationTracker = new WorkspaceVerificationTracker();
994
- var execFileAsync = promisify(execFile);
995
1122
  var videoPreflightCache;
1123
+ var dramaPreflightCache;
996
1124
  var WorkspaceHttpError = class extends Error {
997
1125
  constructor(status, message) {
998
1126
  super(message);
@@ -1000,34 +1128,28 @@ var WorkspaceHttpError = class extends Error {
1000
1128
  }
1001
1129
  status;
1002
1130
  };
1003
- async function commandOutput(command, args) {
1004
- try {
1005
- const { stdout, stderr } = await execFileAsync(command, [...args], { encoding: "utf8", timeout: 5e3, maxBuffer: 4 * 1024 * 1024 });
1006
- return `${stdout}${stderr}`.trim();
1007
- } catch {
1008
- return void 0;
1009
- }
1010
- }
1011
- function pythonVersion(value) {
1012
- const match = /Python\s+(\d+)\.(\d+)(?:\.(\d+))?/u.exec(value ?? "");
1013
- if (match === null) return { ok: false };
1014
- const major = Number(match[1]);
1015
- const minor = Number(match[2]);
1016
- return { ok: major > 3 || major === 3 && minor >= 10, version: match[0].replace(/^Python\s+/u, "") };
1017
- }
1018
1131
  function configured(...names) {
1019
1132
  return names.some((name2) => (process.env[name2] ?? "") !== "");
1020
1133
  }
1134
+ var dramaPreflightInFlight;
1135
+ async function dramaPreflight() {
1136
+ if (dramaPreflightCache !== void 0 && dramaPreflightCache.expires > Date.now()) return dramaPreflightCache.value;
1137
+ dramaPreflightInFlight ??= (async () => {
1138
+ try {
1139
+ const python = await hostPython();
1140
+ const adapterConfig = await ensureDramaAdapterConfig(defaultDramaSkillRoot(), { python: python.command });
1141
+ const value = { python: python.probe, adapterConfig, adapters: dramaAdapterStatuses() };
1142
+ dramaPreflightCache = { expires: Date.now() + 3e4, value };
1143
+ return value;
1144
+ } finally {
1145
+ dramaPreflightInFlight = void 0;
1146
+ }
1147
+ })();
1148
+ return dramaPreflightInFlight;
1149
+ }
1021
1150
  async function videoPreflight() {
1022
1151
  if (videoPreflightCache !== void 0 && videoPreflightCache.expires > Date.now()) return videoPreflightCache.value;
1023
- let python = { ok: false };
1024
- for (const command of ["python3", "python"]) {
1025
- const parsed = pythonVersion(await commandOutput(command, ["--version"]));
1026
- if (parsed.version !== void 0) {
1027
- python = parsed;
1028
- break;
1029
- }
1030
- }
1152
+ const python = (await hostPython()).probe;
1031
1153
  const [ffmpegOutput, ffmpegFilters, ffprobeOutput] = await Promise.all([
1032
1154
  commandOutput("ffmpeg", ["-version"]),
1033
1155
  commandOutput("ffmpeg", ["-hide_banner", "-filters"]),
@@ -1342,7 +1464,7 @@ async function previewDigest(realm, projectRoot) {
1342
1464
  }
1343
1465
  };
1344
1466
  await visit(app, "");
1345
- return { ready, version: createHash("sha256").update(entries.sort().join("\n")).digest("hex").slice(0, 16) };
1467
+ return { ready, version: createHash2("sha256").update(entries.sort().join("\n")).digest("hex").slice(0, 16) };
1346
1468
  }
1347
1469
  function headingTitle(content, fallback) {
1348
1470
  const heading = content?.split(/\r?\n/u).find((line) => /^#\s+/u.test(line));
@@ -1408,14 +1530,14 @@ async function workspaceVideoProjects(realm, files, maxBytes) {
1408
1530
  }));
1409
1531
  }
1410
1532
  function bundledExampleRoot() {
1411
- return resolve4(defaultNovelToGameSkillRoot(), `../examples/${BUNDLED_GAME_EXAMPLE}`);
1533
+ return resolve5(defaultNovelToGameSkillRoot(), `../examples/${BUNDLED_GAME_EXAMPLE}`);
1412
1534
  }
1413
1535
  async function bundledGameExample() {
1414
1536
  const root = bundledExampleRoot();
1415
1537
  const [example, verification, manifest] = await Promise.all([
1416
- readNodeFile(resolve4(root, "example.json"), "utf8"),
1417
- readNodeFile(resolve4(root, "qa/verification.json"), "utf8"),
1418
- readNodeFile(resolve4(defaultNovelToGameSkillRoot(), "../manifest.json"), "utf8")
1538
+ readNodeFile(resolve5(root, "example.json"), "utf8"),
1539
+ readNodeFile(resolve5(root, "qa/verification.json"), "utf8"),
1540
+ readNodeFile(resolve5(defaultNovelToGameSkillRoot(), "../manifest.json"), "utf8")
1419
1541
  ]);
1420
1542
  const exampleJson = JSON.parse(example);
1421
1543
  const manifestJson = JSON.parse(manifest);
@@ -1516,8 +1638,8 @@ async function previewBytes(context, pathname) {
1516
1638
  const id = segments.shift();
1517
1639
  const path = segments.join("/") || "index.html";
1518
1640
  if (id !== BUNDLED_GAME_EXAMPLE || !safeRelativePath(path)) throw new WorkspaceHttpError(404, "\u6E38\u620F\u793A\u4F8B\u4E0D\u5B58\u5728\u3002");
1519
- const appRoot = resolve4(bundledExampleRoot(), "build/app");
1520
- const target2 = resolve4(appRoot, path);
1641
+ const appRoot = resolve5(bundledExampleRoot(), "build/app");
1642
+ const target2 = resolve5(appRoot, path);
1521
1643
  const escaped = relative3(appRoot, target2);
1522
1644
  if (escaped.startsWith("..") || isAbsolute3(escaped)) throw new WorkspaceHttpError(403, "\u9884\u89C8\u8D44\u6E90\u79BB\u5F00\u4E86\u6E38\u620F\u76EE\u5F55\u3002");
1523
1645
  const info = await nodeStat(target2).catch(() => void 0);
@@ -1597,6 +1719,10 @@ async function handle(context, request, response, options) {
1597
1719
  send(response, 200, await videoPreflight());
1598
1720
  return;
1599
1721
  }
1722
+ if (url.pathname === "/oh-story/drama-preflight" && request.method === "GET") {
1723
+ send(response, 200, await dramaPreflight());
1724
+ return;
1725
+ }
1600
1726
  if (url.pathname === "/oh-story/file" && request.method === "GET") {
1601
1727
  const realm = await workspaceRealm(context, url);
1602
1728
  const path = url.searchParams.get("path");
@@ -1667,10 +1793,13 @@ async function apply(context, config = {}) {
1667
1793
  registerOhStoryProductionTool(context);
1668
1794
  await registerOhStoryRoleTool(context);
1669
1795
  registerWorkspaceRoute(context, { maxBytes: config.editorMaxBytes ?? 2097152, trustedHosts });
1796
+ await ensureDramaAdapterConfig(defaultDramaSkillRoot(), { python: (await hostPython()).command });
1670
1797
  }
1671
1798
  var index_default = { name, inject, Config, apply };
1672
1799
  export {
1673
1800
  Config,
1801
+ DRAMA_ADAPTERS,
1802
+ DRAMA_ADAPTER_CONFIG_ENV,
1674
1803
  OH_STORY_PRODUCTION_TOOL_NAME,
1675
1804
  OH_STORY_REFERENCE_TOOL_NAME,
1676
1805
  OH_STORY_ROLE_NAMES,
@@ -1685,6 +1814,9 @@ export {
1685
1814
  createOhStorySkillProvider,
1686
1815
  createVideoRecapSkillProvider,
1687
1816
  index_default as default,
1817
+ dramaAdapterConfigPath,
1818
+ dramaAdapterStatuses,
1819
+ ensureDramaAdapterConfig,
1688
1820
  inject,
1689
1821
  loadBundledRole,
1690
1822
  name,