dsh-cosplay-studio 2.0.3 → 2.1.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/lib/index.js CHANGED
@@ -8,7 +8,7 @@
8
8
  // 4. post-processing (de-oil / JPEG cap / 2×2 preview / delivery copy);
9
9
  // 5. the JSON API the browser half talks to, plus project media serving.
10
10
 
11
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
11
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
12
12
  import { join, relative, resolve, sep } from 'node:path'
13
13
  import {
14
14
  API_PREFIX, MEDIA_PREFIX, PROJECTS_DIR, dshHome, logLine, patchConfig, readConfig, setupLog, clearLog,
@@ -18,13 +18,15 @@ import {
18
18
  modelStatus, resolveComfyLayout, selfTest, startComfy, stopComfy, waitForComfy,
19
19
  } from './comfy.js'
20
20
  import {
21
- buildCard, defaultDetails, llmConfigured, researchCharacter, ruleScript, shotPrompt, writeScript,
21
+ SCRIPT_SYSTEM, buildCard, chatComplete, defaultDetails, llmConfigured, normalizeShotItems, researchCharacter,
22
+ ruleScript, scriptUserText, shotPrompt, writeScript,
22
23
  } from './card.js'
23
24
  import {
24
- cancelQueue, createProject, enqueueBase, enqueueFinalize, enqueueReferenceBase, enqueueShots,
25
+ cancelQueue, createProject, enqueueBase, enqueueEdit, enqueueFinalize, enqueueShots,
25
26
  listProjects, projectDirs, queueSnapshot, readProject, saveProject, updateProject,
26
27
  } from './project.js'
27
28
  import { recommend } from './hot.js'
29
+ import { createRandomProject, llmText, parseJson, researchState, startResearch } from './research.js'
28
30
 
29
31
  const inject = ['webServer']
30
32
 
@@ -44,11 +46,17 @@ const API = {
44
46
  update: `${API_PREFIX}/project/update`,
45
47
  reference: `${API_PREFIX}/project/reference`,
46
48
  base: `${API_PREFIX}/project/base`,
49
+ baseEdit: `${API_PREFIX}/project/base/edit`,
50
+ shotEdit: `${API_PREFIX}/project/shot/edit`,
51
+ restoreShot: `${API_PREFIX}/project/shot/restore`,
47
52
  script: `${API_PREFIX}/project/script`,
53
+ scriptAdd: `${API_PREFIX}/project/script/add`,
48
54
  generate: `${API_PREFIX}/project/generate`,
49
55
  finalize: `${API_PREFIX}/project/finalize`,
50
56
  queue: `${API_PREFIX}/queue`,
51
57
  openFolder: `${API_PREFIX}/project/open-folder`,
58
+ research: `${API_PREFIX}/project/research`,
59
+ random: `${API_PREFIX}/project/random`,
52
60
  }
53
61
 
54
62
  const TUTORIAL = [
@@ -280,7 +288,7 @@ function referenceSave(id, body) {
280
288
  function applyShots(id, shots) {
281
289
  const project = readProject(id)
282
290
  if (project === null) throw new Error('项目不存在')
283
- const normalized = (Array.isArray(shots) ? shots : []).slice(0, 6).map((shot, index) => ({
291
+ const normalized = (Array.isArray(shots) ? shots : []).slice(0, 10).map((shot, index) => ({
284
292
  id: index + 1,
285
293
  type: String(shot.type ?? 'A'),
286
294
  title: String(shot.title ?? `镜头 ${index + 1}`),
@@ -295,10 +303,103 @@ function applyShots(id, shots) {
295
303
  return updateProject(id, { shots: normalized })
296
304
  }
297
305
 
306
+ function mediaTypeOfName(name) {
307
+ const lower = String(name).toLowerCase()
308
+ if (lower.endsWith('.png')) return 'image/png'
309
+ if (lower.endsWith('.webp')) return 'image/webp'
310
+ return 'image/jpeg'
311
+ }
312
+
313
+ /** Script model path: configured chat endpoint first, then the DSH host model,
314
+ * then the built-in templates. Keeps existing shots for smart additions. */
315
+ async function generateShotList(ctx, project, count, existing = []) {
316
+ const config = readConfig()
317
+ const user = scriptUserText(project.details, count, existing)
318
+ const startIndex = existing.length + 1
319
+ if (llmConfigured(config)) {
320
+ try {
321
+ const text = await chatComplete(config, SCRIPT_SYSTEM, user)
322
+ const parsed = parseJson(text)
323
+ const list = Array.isArray(parsed) ? parsed : parsed?.shots
324
+ return { shots: normalizeShotItems(list, startIndex), note: '镜头脚本由脚本模型生成' }
325
+ } catch {
326
+ // fall through to the host model
327
+ }
328
+ }
329
+ try {
330
+ let lastError = null
331
+ for (const maxTokens of [9000, 12000]) {
332
+ try {
333
+ const text = await llmText(ctx, `${SCRIPT_SYSTEM}\n\n${user}\n\n只输出 JSON 数组,不要输出任何思考过程或解释。`, { maxTokens })
334
+ const parsed = parseJson(text)
335
+ const list = Array.isArray(parsed) ? parsed : parsed?.shots
336
+ return { shots: normalizeShotItems(list, startIndex), note: '镜头脚本由 DSH 模型生成' }
337
+ } catch (error) {
338
+ lastError = error
339
+ }
340
+ }
341
+ throw lastError ?? new Error('脚本模型没有返回内容')
342
+ } catch (error) {
343
+ return { shots: ruleScript(project.details, Math.max(1, Math.min(6, count))), note: `内置脚本模板(${error?.message ?? error})` }
344
+ }
345
+ }
346
+
347
+ /** Smart edit: compare the picture with the reference / approved base, let the
348
+ * vision model name the flaws and produce one English img2img instruction. */
349
+ async function smartEditInstruction(ctx, project, target, file, note = '') {
350
+ const dirs = projectDirs(project.id)
351
+ const images = []
352
+ const add = (folder, name) => {
353
+ if (name === '') return
354
+ const path = join(folder, name)
355
+ if (!existsSync(path) || images.some((image) => image.name === name)) return
356
+ images.push({ data: readFileSync(path), mediaType: mediaTypeOfName(name), name })
357
+ }
358
+ add(target === 'base' ? dirs.base : dirs.raw, file)
359
+ add(dirs.refs, String(project.referenceFile ?? ''))
360
+ add(dirs.base, String(project.approvedBase ?? ''))
361
+ const ask = note === ''
362
+ ? '没有额外要求,请自行找出最需要修的瑕疵(手、脚、五官、服装边缘、穿模)'
363
+ : `用户的修改要求:${note}`
364
+ const prompt = `你在为一套真人 cosplay 照片做精修指导,只输出 JSON。\n\n角色设定(英文):\n${String(project.card ?? '').slice(0, 1800)}\n\n${ask}\n\n请比对图片:第一张是待修改的图片${images.length > 1 ? ',后面的图片是该角色的参考图 / 已确认基准图' : ''}。找出需要修的 1-4 处具体问题,并给出一句可直接用于 img2img 重绘的英文编辑指令:只描述要改的局部,明确保持同一人物、同一姿势、同一机位、同一服装与背景不变;优先修手、脚、五官和衣物边缘这类细节。\n\n只输出:{"issues":["中文问题1","中文问题2"],"fix":"one English editing instruction"}`
365
+ let lastError = null
366
+ for (const maxTokens of [4000, 6000]) {
367
+ try {
368
+ const text = await llmText(ctx, `${prompt}\n\n不要输出任何思考过程。`, { images, maxTokens })
369
+ const parsed = parseJson(text)
370
+ const issues = Array.isArray(parsed?.issues) ? parsed.issues.map((value) => String(value)).filter(Boolean).slice(0, 4) : []
371
+ const fix = String(parsed?.fix ?? '').trim()
372
+ if (fix === '') throw new Error('智能分析没有给出修改指令')
373
+ return { issues, fix }
374
+ } catch (error) {
375
+ lastError = error
376
+ }
377
+ }
378
+ throw lastError ?? new Error('智能分析失败')
379
+ }
380
+
298
381
  /* ----------------------------------------------------------------- apply */
299
382
 
300
383
  export function apply(ctx) {
301
384
  const disposers = []
385
+ const disposeAll = () => {
386
+ while (disposers.length > 0) {
387
+ const dispose = disposers.pop()
388
+ try {
389
+ dispose()
390
+ } catch {
391
+ // ignore
392
+ }
393
+ }
394
+ }
395
+ // The plugin loader ignores a plugin's return value, so binding cleanup to
396
+ // the returned function alone leaks routes: a hot reload then fails with
397
+ // "webserver: duplicate exact route ...". Bind it to the fiber instead.
398
+ if (typeof ctx.effect === 'function') {
399
+ ctx.effect(() => disposeAll)
400
+ } else if (typeof ctx.on === 'function') {
401
+ ctx.on('dispose', disposeAll)
402
+ }
302
403
  ctx.logger?.info?.('[cosplay-studio] host half loaded')
303
404
 
304
405
  const route = (path, handler) => {
@@ -324,6 +425,9 @@ export function apply(ctx) {
324
425
  updatedAt: project.updatedAt,
325
426
  finalCount: (project.finals ?? []).length,
326
427
  baseCount: (project.baseImages ?? []).length,
428
+ research: project.research
429
+ ? { status: project.research.status, stage: project.research.stage, stageLabel: project.research.stageLabel }
430
+ : null,
327
431
  }))
328
432
  sendJson(response, 200, {
329
433
  ok: true,
@@ -481,7 +585,10 @@ export function apply(ctx) {
481
585
  sendJson(response, 404, { ok: false, error: '项目不存在' })
482
586
  return
483
587
  }
484
- sendJson(response, 200, { ok: true, project })
588
+ // A host restart leaves research jobs marked running forever; normalise so
589
+ // the panel shows "重新研究" instead of a spinner that never ends.
590
+ const research = researchState(project)
591
+ sendJson(response, 200, { ok: true, project: research === null ? project : { ...project, research } })
485
592
  })
486
593
 
487
594
  route(API.recommend, async (request, response) => {
@@ -517,6 +624,7 @@ export function apply(ctx) {
517
624
  return
518
625
  }
519
626
  const series = String(body?.series ?? '').trim()
627
+ const source = String(body?.source ?? 'name')
520
628
  const config = readConfig()
521
629
  let details = body?.details !== null && typeof body?.details === 'object' ? { ...body.details } : null
522
630
  let note = ''
@@ -526,17 +634,30 @@ export function apply(ctx) {
526
634
  details = await researchCharacter(config, { character, series, referencePath: body?.referenceFile ?? '' })
527
635
  note = '角色卡由脚本模型生成'
528
636
  } catch (error) {
529
- details = defaultDetails(character, series)
637
+ details = defaultDetails(character, series, { reference: source === 'image' })
530
638
  note = `脚本模型不可用(${error?.message ?? error}),已用内置模板`
531
639
  }
532
640
  } else {
533
- details = defaultDetails(character, series)
534
- note = llmConfigured(config) ? '按内置模板生成角色卡' : '未配置脚本模型,按内置模板生成角色卡'
641
+ details = defaultDetails(character, series, { reference: source === 'image' })
642
+ note = source === 'image'
643
+ ? '参考图项目:人物外观以参考图为准,角色卡里的发型/服装/道具只是占位描述,可留空'
644
+ : llmConfigured(config) ? '按内置模板生成角色卡' : '未配置脚本模型,角色卡用通用模板;想做得像请上传参考图'
535
645
  }
536
646
  }
537
647
  const project = createProject({ character, series, details, source: body?.source ?? 'name' })
538
648
  updateProject(project.id, { notes: [note] })
539
- sendJson(response, 200, { ok: true, project: readProject(project.id), note })
649
+ // Name projects start the slow research right away (web search → pictures →
650
+ // vision analysis → draft). Image projects start after the upload arrives.
651
+ let researching = false
652
+ if (body?.research !== false && source !== 'image') {
653
+ try {
654
+ startResearch(ctx, config, project.id)
655
+ researching = true
656
+ } catch (error) {
657
+ updateProject(project.id, { notes: [note, `角色研究未能启动:${error?.message ?? error}`] })
658
+ }
659
+ }
660
+ sendJson(response, 200, { ok: true, project: readProject(project.id), note, researching })
540
661
  })
541
662
 
542
663
  route(API.update, async (request, response) => {
@@ -561,7 +682,14 @@ export function apply(ctx) {
561
682
  if (Array.isArray(body?.shots)) {
562
683
  applyShots(id, body.shots)
563
684
  }
564
- if (typeof body?.status === 'string') patch.status = body.status
685
+ if (typeof body?.status === 'string') {
686
+ patch.status = body.status
687
+ // Remember which base the user approved: later shots are img2img from it.
688
+ if (body.status === 'base-approved') {
689
+ const bases = Array.isArray(project.baseImages) ? project.baseImages : []
690
+ if (bases.length > 0) patch.approvedBase = bases[bases.length - 1]
691
+ }
692
+ }
565
693
  if (Object.keys(patch).length > 0) updateProject(id, patch)
566
694
  sendJson(response, 200, { ok: true, project: readProject(id) })
567
695
  })
@@ -610,19 +738,22 @@ export function apply(ctx) {
610
738
  sendJson(response, 409, { ok: false, error: '正在生成其它任务,请稍候' })
611
739
  return
612
740
  }
613
- const useReference = body?.useReference === true && String(project.referenceFile ?? '') !== ''
614
- if (useReference) enqueueReferenceBase(config, layout, id)
615
- else enqueueBase(config, layout, id, body?.count === 2 ? 2 : 1)
741
+ // Reference projects default to img2img from the uploaded picture; the
742
+ // text-to-image path stays available with an explicit useReference:false.
743
+ const useReference = String(project.referenceFile ?? '') !== '' && body?.useReference !== false
744
+ enqueueBase(config, layout, id, body?.count === 2 ? 2 : 1, { useReference })
616
745
  sendJson(response, 200, { ok: true, queue: queueSnapshot() })
617
746
  })
618
747
 
619
- route(API.script, async (request, response) => {
748
+ // Manual / smart retouch: 'base' appends a new base version, 'shot' replaces
749
+ // one raw final. Smart mode asks the vision model what to fix first.
750
+ const editRoute = (target) => async (request, response) => {
620
751
  if (request.method !== 'POST') {
621
752
  response.writeHead(405, { allow: 'POST' })
622
753
  response.end()
623
754
  return
624
755
  }
625
- const body = await readBody(request, 1024 * 1024)
756
+ const body = await readBody(request, 64 * 1024)
626
757
  const id = String(body?.id ?? '')
627
758
  const project = readProject(id)
628
759
  if (project === null) {
@@ -630,26 +761,194 @@ export function apply(ctx) {
630
761
  return
631
762
  }
632
763
  const config = readConfig()
633
- const count = Math.max(4, Math.min(6, Number(body?.count ?? config.shotCount ?? 6)))
634
- let shots = null
635
- let note = ''
636
- if (body?.useModel !== false && llmConfigured(config)) {
764
+ const { layout } = detectComfyDir(config)
765
+ if (layout === null || (await comfyStats(config.comfyPort)) === null) {
766
+ sendJson(response, 409, { ok: false, error: 'ComfyUI 没有运行,请先在「安装」页启动' })
767
+ return
768
+ }
769
+ if (queueSnapshot().running) {
770
+ sendJson(response, 409, { ok: false, error: '正在生成其它任务,请稍候' })
771
+ return
772
+ }
773
+ const file = String(body?.file ?? '').trim()
774
+ if (target === 'shot' && file === '') {
775
+ sendJson(response, 400, { ok: false, error: '请选择要修改的成片' })
776
+ return
777
+ }
778
+ let instruction = String(body?.instruction ?? '').trim()
779
+ const notes = []
780
+ if (body?.smart === true) {
637
781
  try {
638
- shots = await writeScript(config, project.details, count)
639
- note = '镜头脚本由脚本模型生成'
782
+ const analysis = await smartEditInstruction(ctx, project, target, file, instruction)
783
+ instruction = analysis.fix
784
+ if (analysis.issues.length > 0) notes.push(`智能微调分析:${analysis.issues.join(';')}`)
640
785
  } catch (error) {
641
- shots = ruleScript(project.details, count)
642
- note = `脚本模型不可用(${error?.message ?? error}),已用内置脚本`
786
+ sendJson(response, 500, { ok: false, error: `智能分析失败:${error?.message ?? error}` })
787
+ return
643
788
  }
644
- } else {
645
- shots = ruleScript(project.details, count)
646
- note = '内置脚本模板'
647
789
  }
790
+ if (instruction === '') {
791
+ sendJson(response, 400, { ok: false, error: '请输入修改要求,或改用智能修改' })
792
+ return
793
+ }
794
+ try {
795
+ enqueueEdit(config, layout, id, { target, file, instruction, denoise: Number(body?.denoise), notes })
796
+ sendJson(response, 200, { ok: true, project: readProject(id), issues: notes })
797
+ } catch (error) {
798
+ sendJson(response, 500, { ok: false, error: error?.message ?? String(error) })
799
+ }
800
+ }
801
+ route(API.baseEdit, editRoute('base'))
802
+ route(API.shotEdit, editRoute('shot'))
803
+
804
+ // Undo the most recent retouch of one raw final (backups live in raw/.history).
805
+ route(API.restoreShot, async (request, response) => {
806
+ if (request.method !== 'POST') {
807
+ response.writeHead(405, { allow: 'POST' })
808
+ response.end()
809
+ return
810
+ }
811
+ const body = await readBody(request, 16 * 1024)
812
+ const id = String(body?.id ?? '')
813
+ const project = readProject(id)
814
+ if (project === null) {
815
+ sendJson(response, 404, { ok: false, error: '项目不存在' })
816
+ return
817
+ }
818
+ const file = String(body?.file ?? '').trim()
819
+ if (file === '' || file.includes('/') || file.includes('\\') || file.includes('..')) {
820
+ sendJson(response, 400, { ok: false, error: '文件名不合法' })
821
+ return
822
+ }
823
+ const dirs = projectDirs(id)
824
+ const history = join(dirs.raw, '.history')
825
+ const current = join(dirs.raw, file)
826
+ if (!existsSync(history) || !existsSync(current)) {
827
+ sendJson(response, 404, { ok: false, error: '没有可恢复的历史版本' })
828
+ return
829
+ }
830
+ const versions = readdirSync(history)
831
+ .filter((name) => name.startsWith(`${file}.`) && name.endsWith('.png'))
832
+ .sort()
833
+ if (versions.length === 0) {
834
+ sendJson(response, 404, { ok: false, error: '没有可恢复的历史版本' })
835
+ return
836
+ }
837
+ const latest = versions[versions.length - 1]
838
+ writeFileSync(join(history, `${file}.${Date.now()}.png`), readFileSync(current))
839
+ writeFileSync(current, readFileSync(join(history, latest)))
840
+ const updated = updateProject(id, {
841
+ pendingFinals: [...new Set([...(project.pendingFinals ?? []), file])],
842
+ notes: [...(project.notes ?? []), `已恢复上一版成片:${file}`],
843
+ })
844
+ sendJson(response, 200, { ok: true, project: updated })
845
+ })
846
+
847
+ route(API.research, async (request, response) => {
848
+ if (request.method !== 'POST') {
849
+ response.writeHead(405, { allow: 'POST' })
850
+ response.end()
851
+ return
852
+ }
853
+ const body = await readBody(request, 64 * 1024)
854
+ const id = String(body?.id ?? '')
855
+ if (readProject(id) === null) {
856
+ sendJson(response, 404, { ok: false, error: '项目不存在' })
857
+ return
858
+ }
859
+ const config = readConfig()
860
+ try {
861
+ const project = startResearch(ctx, config, id, { supplement: body?.supplement === true })
862
+ sendJson(response, 200, { ok: true, project })
863
+ } catch (error) {
864
+ sendJson(response, 500, { ok: false, error: error?.message ?? String(error) })
865
+ }
866
+ })
867
+
868
+ route(API.random, async (request, response) => {
869
+ if (request.method !== 'POST') {
870
+ response.writeHead(405, { allow: 'POST' })
871
+ response.end()
872
+ return
873
+ }
874
+ const config = readConfig()
875
+ try {
876
+ const pick = await createRandomProject(ctx, config)
877
+ const project = createProject({
878
+ character: pick.character,
879
+ series: pick.series,
880
+ details: defaultDetails(pick.character, pick.series),
881
+ source: 'random',
882
+ })
883
+ updateProject(project.id, { notes: [pick.reason === '' ? '随机推荐角色' : `随机推荐:${pick.reason}`] })
884
+ startResearch(ctx, config, project.id)
885
+ sendJson(response, 200, { ok: true, project: readProject(project.id), pick })
886
+ } catch (error) {
887
+ sendJson(response, 500, { ok: false, error: error?.message ?? String(error) })
888
+ }
889
+ })
890
+
891
+ route(API.script, async (request, response) => {
892
+ if (request.method !== 'POST') {
893
+ response.writeHead(405, { allow: 'POST' })
894
+ response.end()
895
+ return
896
+ }
897
+ const body = await readBody(request, 1024 * 1024)
898
+ const id = String(body?.id ?? '')
899
+ const project = readProject(id)
900
+ if (project === null) {
901
+ sendJson(response, 404, { ok: false, error: '项目不存在' })
902
+ return
903
+ }
904
+ const config = readConfig()
905
+ const count = Math.max(4, Math.min(6, Number(body?.count ?? config.shotCount ?? 6)))
906
+ const { shots, note } = await generateShotList(ctx, project, count)
648
907
  const updated = applyShots(id, shots)
649
908
  updateProject(id, { status: 'awaiting-script-approval', notes: [...(updated.notes ?? []), note] })
650
909
  sendJson(response, 200, { ok: true, project: readProject(id), note })
651
910
  })
652
911
 
912
+ // Smart add: the script model designs extra shots that must not repeat the
913
+ // ones already in the list.
914
+ route(API.scriptAdd, async (request, response) => {
915
+ if (request.method !== 'POST') {
916
+ response.writeHead(405, { allow: 'POST' })
917
+ response.end()
918
+ return
919
+ }
920
+ const body = await readBody(request, 64 * 1024)
921
+ const id = String(body?.id ?? '')
922
+ const project = readProject(id)
923
+ if (project === null) {
924
+ sendJson(response, 404, { ok: false, error: '项目不存在' })
925
+ return
926
+ }
927
+ const existing = Array.isArray(project.shots) ? project.shots : []
928
+ if (existing.length === 0) {
929
+ sendJson(response, 409, { ok: false, error: '还没有镜头脚本,请先生成' })
930
+ return
931
+ }
932
+ const add = Math.max(1, Math.min(3, Number(body?.count ?? 2)))
933
+ const limit = Math.max(0, 10 - existing.length)
934
+ if (limit === 0) {
935
+ sendJson(response, 409, { ok: false, error: '已经有 10 个分镜,先删除一些再补' })
936
+ return
937
+ }
938
+ const { shots: fresh, note } = await generateShotList(ctx, project, Math.min(add, limit), existing)
939
+ const merged = [...existing, ...fresh].slice(0, 10).map((shot, index) => ({
940
+ ...shot,
941
+ id: index + 1,
942
+ prompt: `${shot.type ?? 'A'}|${shot.title ?? `镜头 ${index + 1}`}`,
943
+ }))
944
+ const updated = updateProject(id, {
945
+ shots: merged,
946
+ status: 'awaiting-script-approval',
947
+ notes: [...(project.notes ?? []), note, `智能补充分镜 ${fresh.length} 个`],
948
+ })
949
+ sendJson(response, 200, { ok: true, project: updated, note })
950
+ })
951
+
653
952
  route(API.generate, async (request, response) => {
654
953
  if (request.method !== 'POST') {
655
954
  response.writeHead(405, { allow: 'POST' })
@@ -700,7 +999,13 @@ export function apply(ctx) {
700
999
  sendJson(response, 409, { ok: false, error: '已有任务在运行' })
701
1000
  return
702
1001
  }
703
- enqueueFinalize(config, layout, id)
1002
+ enqueueFinalize(config, layout, id, {
1003
+ deoil: body?.deoil === true,
1004
+ makeup: body?.makeup === true,
1005
+ flatten: body?.flatten !== false,
1006
+ preview: body?.preview !== false,
1007
+ copy: body?.copy !== false,
1008
+ })
704
1009
  sendJson(response, 200, { ok: true })
705
1010
  })
706
1011
 
@@ -798,13 +1103,7 @@ export function apply(ctx) {
798
1103
  disposers.push(disposeMedia)
799
1104
 
800
1105
  return async () => {
801
- for (const dispose of disposers.reverse()) {
802
- try {
803
- dispose()
804
- } catch {
805
- // ignore
806
- }
807
- }
1106
+ disposeAll()
808
1107
  }
809
1108
  }
810
1109