From 62135e210dea859e8826656645d676c1b902b9d6 Mon Sep 17 00:00:00 2001 From: reflect <2946843254lw@gmail.com> Date: Fri, 21 Aug 2026 22:48:18 +0800 Subject: [PATCH 1/7] =?UTF-8?q?feat:=20=E5=AE=8C=E6=88=90=20DSH=201.0.14?= =?UTF-8?q?=20=E9=9B=B6=E5=A4=8D=E5=88=B6=E5=AF=BC=E5=85=A5=E4=B8=8E?= =?UTF-8?q?=E8=83=8C=E6=99=AF=E6=B8=85=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/tray/session-host.mjs | 74 ++- .../dsh-background-bar/design-spec.md | 9 + .../dsh-background-bar/direction-approved.md | 6 + .../direction-c-media-ledger.html | 4 + .../direction-c-media-ledger.png | Bin 0 -> 115221 bytes integrations/deepseek-harness/README.zh-CN.md | 4 +- integrations/deepseek-harness/agent.mjs | 219 +++++++- .../deepseek-harness/bin/beauticode-dsh | 222 +------- integrations/deepseek-harness/cli.js | 293 ++++++++-- integrations/deepseek-harness/client.js | 106 +++- integrations/deepseek-harness/console.js | 383 ++++++++++--- .../deepseek-harness/control-client.mjs | 83 ++- .../deepseek-harness/cordis.patch.yml | 2 +- .../deepseek-harness/gallery-host.mjs | 253 +++++++++ integrations/deepseek-harness/gallery.js | 178 ++++++ integrations/deepseek-harness/index.mjs | 64 ++- integrations/deepseek-harness/package.json | 7 +- .../deepseek-harness/skin-center.json | 3 + .../deepseek-harness/test/agent.test.mjs | 71 ++- .../deepseek-harness/test/pack.test.mjs | 2 +- .../deepseek-harness/test/plugin.test.mjs | 77 +++ .../deepseek-harness/test/ui-host.test.mjs | 435 ++++++++++++++- integrations/deepseek-harness/ui-host.mjs | 507 +++++++++++++++++- packages/adapter-codex/src/session.ts | 26 +- packages/adapter-dsh/src/bridge.ts | 11 +- packages/adapter-dsh/src/session.ts | 87 ++- packages/adapter-dsh/test/adapter.test.js | 96 +++- packages/core/src/apply-transaction.ts | 161 ++++-- packages/core/src/background-store.ts | 308 +++++++---- packages/core/src/index.ts | 1 + packages/core/src/media-server.ts | 85 ++- packages/core/src/media-source.ts | 42 ++ packages/core/src/media-validation.ts | 51 +- packages/core/src/types.ts | 35 +- packages/core/test/background-store.test.js | 137 ++++- packages/core/test/media-server.test.js | 61 +++ scripts/beauticode.mjs | 7 +- scripts/pack-dsh-plugin.mjs | 3 + 38 files changed, 3503 insertions(+), 610 deletions(-) create mode 100644 design-demos/dsh-background-bar/design-spec.md create mode 100644 design-demos/dsh-background-bar/direction-approved.md create mode 100644 design-demos/dsh-background-bar/direction-c-media-ledger.html create mode 100644 design-demos/dsh-background-bar/direction-c-media-ledger.png create mode 100644 integrations/deepseek-harness/gallery-host.mjs create mode 100644 integrations/deepseek-harness/gallery.js create mode 100644 integrations/deepseek-harness/skin-center.json create mode 100644 packages/core/src/media-source.ts diff --git a/apps/tray/session-host.mjs b/apps/tray/session-host.mjs index e7bb49b..aac2f3c 100644 --- a/apps/tray/session-host.mjs +++ b/apps/tray/session-host.mjs @@ -9,10 +9,11 @@ * Protocol (JSON): * GET /health * GET /status - * POST /apply/image { "imagePath": "..." } - * POST /apply/video { "videoPath": "...", "imagePath"?: "...", "startAt"?: number } + * POST /apply/image { "imagePath": "...", "source"?: "managed"|"local" } + * POST /apply/video { "videoPath": "...", "imagePath"?: "...", "source"?: "managed"|"local", "startAt"?: number } * POST /apply/clear {} * POST /reapply {} // republish active background into live sessions + * POST /theme/apply { "name": "...", "input": ApplyInput } * POST /theme/save { "name": "..." } // keep current image/video * GET /theme/list * POST /theme/use { "id": "..." } @@ -51,6 +52,54 @@ function argValue(name) { return value && !value.startsWith("--") ? value : null; } +function parseImportMode(value) { + if (value == null) return undefined; + if (value === "managed" || value === "local") return value; + throw new Error("source 必须是 managed 或 local。"); +} + +function parseThemeApplyInput(value) { + if (!value || typeof value !== "object" || Array.isArray(value)) { + throw new Error("input 必须是图片或视频导入参数。"); + } + if (value.type === "image") { + if (typeof value.imagePath !== "string" || !value.imagePath) { + throw new Error("图片主题必须提供 imagePath。"); + } + const input = { + type: "image", + imagePath: path.resolve(value.imagePath), + source: parseImportMode(value.source), + }; + if (value.effects && typeof value.effects === "object") { + input.effects = value.effects; + } + return input; + } + if (value.type === "video") { + if (typeof value.videoPath !== "string" || !value.videoPath) { + throw new Error("视频主题必须提供 videoPath。"); + } + const input = { + type: "video", + videoPath: path.resolve(value.videoPath), + source: parseImportMode(value.source), + }; + if (typeof value.imagePath === "string" && value.imagePath) { + input.imagePath = path.resolve(value.imagePath); + } + if (value.startAt != null) { + const startAt = Number(value.startAt); + if (!Number.isFinite(startAt) || startAt < 0) { + throw new Error("startAt 必须是非负数字(秒)。"); + } + input.startAt = startAt; + } + return input; + } + throw new Error("input.type 必须是 image 或 video。"); +} + const hostKind = argValue("--host") ?? "codex"; if (hostKind !== "codex" && hostKind !== "dsh") { console.error("--host 必须是 codex 或 dsh。"); @@ -312,6 +361,7 @@ const server = http.createServer( type: "image", imagePath: path.resolve(body.imagePath), }; + imageInput.source = parseImportMode(body.source); if (body.effects && typeof body.effects === "object") { imageInput.effects = body.effects; } @@ -332,6 +382,7 @@ const server = http.createServer( type: "video", videoPath: path.resolve(body.videoPath), }; + videoInput.source = parseImportMode(body.source); if (typeof body.imagePath === "string" && body.imagePath) { videoInput.imagePath = path.resolve(body.imagePath); } @@ -357,6 +408,25 @@ const server = http.createServer( send(res, result.ok ? 200 : 422, result); return; } + if (req.method === "POST" && url === "/theme/apply") { + const body = await readBody(req); + if (typeof body.name !== "string" || !body.name.trim()) { + send(res, 400, { ok: false, error: "必须提供主题名称。" }); + return; + } + if (typeof session.applyAndSaveTheme !== "function") { + send(res, 501, { ok: false, error: "当前会话不支持应用并保存主题。" }); + return; + } + const input = parseThemeApplyInput(body.input); + const result = await session.applyAndSaveTheme(input, body.name.trim()); + send( + res, + result.ok ? 200 : 422, + result.ok ? { ...result, theme: publicTheme(result.theme) } : result, + ); + return; + } if (req.method === "POST" && url === "/theme/save") { const body = await readBody(req); if (typeof body.name !== "string" || !body.name.trim()) { diff --git a/design-demos/dsh-background-bar/design-spec.md b/design-demos/dsh-background-bar/design-spec.md new file mode 100644 index 0000000..a3a1526 --- /dev/null +++ b/design-demos/dsh-background-bar/design-spec.md @@ -0,0 +1,9 @@ +# DSH 背景栏视觉方向说明 + +本次不是重做 DSH,也不是把背景功能扩张成独立的素材管理器。目标是让侧边栏里的「背景」入口更像一个稳定、可信、长期使用的本地工具:用户能在几秒内完成图片导入、视频导入、声音控制、清除背景和已保存主题切换;正在处理时知道系统在做什么;发生错误或超时时按钮一定恢复;同时不把绝对路径、技术协议或多余统计暴露给普通用户。背景栏仍然依附 DSH 左侧栏,不能抢过会话、模型和输入区的主层级。 + +受众是主要在 Windows 本机使用 DSH 的用户。观看距离约为普通笔记本 60–90 厘米,面板宽度应控制在 224–272 像素,正文不低于 13 像素,辅助标签不低于 11 像素。视觉温度需要冷静、可靠、轻微有人味,避免常见 AI 产品的紫蓝渐变、发光描边、Bento 卡片堆叠、每个动作都配一个装饰图标,以及过度圆润的胶囊按钮。颜色从真实「画窗」背景里的雨蓝、雾灰和 DSH 现有深色侧栏采样,收敛为一组中性灰阶加一个低饱和蓝;错误只在发生时使用克制的红色。字体沿用 DSH 系统字体,不额外下载字体。 + +内容结构固定为四层:第一层是当前背景和来源状态;第二层是最常用的图片、视频导入;第三层是声音、清除、皮肤中心等次级操作;第四层是已保存主题。主题列表只使用现有名称、媒体类型、来源类型和选中状态,不新增缩略图接口,不把本地路径送到网页。忙碌状态必须局部化:当前操作显示进度文本,其余按钮暂时不可用,但面板仍可关闭;45 秒客户端上限后恢复交互并说明原背景保持不变。原生文件选择器是用户控制的系统窗口,不套用该操作超时。 + +三个方向共享以上功能,但构图互异。方向 A 用包豪斯式规则线和编号建立明确秩序,强调“工具盒”;方向 B 最大程度融入 DSH 本体,把状态和动作做成低噪声的原生侧栏;方向 C 借鉴纸质媒体清单,把导入动作和已保存主题放在一条连续的纵向轨道里,强调主题切换效率。三版都不依赖新增后端能力,选定后只改 `console.js` 的结构和样式,不动 local 导入、事务或媒体服务协议。 diff --git a/design-demos/dsh-background-bar/direction-approved.md b/design-demos/dsh-background-bar/direction-approved.md new file mode 100644 index 0000000..dc8f820 --- /dev/null +++ b/design-demos/dsh-background-bar/direction-approved.md @@ -0,0 +1,6 @@ +# DSH 背景栏方向确认 + +- 已评审:A「网格工具盒」、B「原生侧栏」、C「媒体清单」 +- 最终设计:`direction-c-media-ledger.html`、`direction-c-media-ledger.png` +- 用户选择原话:`C吧` +- 结论:正式背景栏采用 C「媒体清单」方向;保持现有 DSH 侧栏位置、local 零复制导入、主题切换和超时恢复语义。 diff --git a/design-demos/dsh-background-bar/direction-c-media-ledger.html b/design-demos/dsh-background-bar/direction-c-media-ledger.html new file mode 100644 index 0000000..6843793 --- /dev/null +++ b/design-demos/dsh-background-bar/direction-c-media-ledger.html @@ -0,0 +1,4 @@ + +方向 C · 媒体清单
探索未至之境
diff --git a/design-demos/dsh-background-bar/direction-c-media-ledger.png b/design-demos/dsh-background-bar/direction-c-media-ledger.png new file mode 100644 index 0000000000000000000000000000000000000000..997755b6a44247793e83f3fc9a678fedcb534dcb GIT binary patch literal 115221 zcmb6AbyOVB6E_O)vI{H(2)4K^?gWCnyDhRf3BiK9BoGJ^2(Ezu!5x;x7bi$?_XH;p z2ol^axqN@mbI$wTKX1=DRXtTT{i&Jm=~LZP^Y6#MbpWBdvYIjg2m}Ctj|br25_?7kx|jI&{I(}(U6hBxM57JY#f{% zl=M7&JnVcd>>TX>H3G!O#>R!R$<$) z>4L|0ss0D~|A+ectS~^2u#Y-6qW|O=K=7kukIw$*7LW*ofgeOnBKKSu{D?^^A5!~o z5rFro#2^9@0b~HzP=tkUb-`)b93r!V*q~gmAeGagj9WLFNRd+m3FX|yfg3=*i=#P- zl+=w2O@W$FAg)k8sZ}0>GP@E*IYbvX-;n*$5Wy*5jxf@*P_Ia3F4rrYO`J0*mk(sy z8FKscH;d>dFL~f1P*J~9U~yKdM341#vBVCdr1Lyz7>xI>5J15#a*VC7b0R^fk$}7@ zSR$|x&e-GH@%OtOJUcxzXpwk^S^TC#PqlAcvnXLbgjjHq;>?!^_1_yY|nonz+o7t0|L5kk53_}p zpJy!x8Vm-u%5O@B5WvV{0mf89leYphi=Uz^be>4b_3C1Rlk;sbUgt-Tqu!pc8b{}ktc6`yL2Pr*!T7aW{snGl|;-TYG>~z zb+FzPip&ESxWRO5oc%(jPpqT|ujfSNZn>@u%zC6MzRaq@eLZA`LpzYSg@*-*7iH3W z7yu?o??S91Dda8k%3!gxqSFU1V3+;zpDP9ba|NC$r@WXL2z!TQ=>>iryhB=3_F1<> z9C%KlU%98NiaWMRF=?N&ChxRG;Xt%m8_e(AgVUT9byj36V_a$6_$<|^wVV`$x560w z9w*wtd@ffTjytBjyO-19L*OLo?f9kQ4t{IAzOK+W&~_VBZ#tXg8Y>gC;6V4c*ra%nXCvuxsmt=p0PY;IULo?o67$ZBp=f{L6+#@4 z7mm%3!T#5~Da$6>4}#2#Cg5H8xk3!`^s zOuJWO{vAKtZ&Jn5be^hD8Lc0ziAnF{fMKde_>^eydU`<-N&}q9Lv?lgX_{^%B6t9P z*39RBeLlm9S8CA;s@+ExDz;B3p7dGI-_Br3P-HGw{uppkDN?M&;Tio0P)8$Pu{mIA zUgfU=V98>{UYe{#ZQfTxjRp)V7^-2U-1%MXvXW|p7@6O!M-%xeY5E-kGdXX0W?$&gx=gxs@E@mE-%vpWd!zV z)6v{2+HF)x4X_-E*weY^dXc&AvZ_lE{&fmW%mmF_jK*OpkP7EzSczusrVflM2?b2h zOfKWfT7L7O(KeE@=k=}n;eC;G8PfL}w~_R-?Sqx3#(QoR?Hfv%YCbxF+Ep(^xwst1Zq~@krY+4>@@e>&~B0*53e;1rk!$u>|Cc zfcXCV;7F{ezHCbrSC=ERoMsS4@>Mmdp*;s9xn$Mrfazi@A4F3Y{j1DwkNSpS%6P7d zZ;rs^*BMoTeU?p>sv-oW@qqoD3kt1NIoGY}kRH4UJ{(rSU!Tm|qp$ED7@G%^4CF|w z@whLqE^p-7jy9|RJ10@BgON6XlC1e}Txr%?CmYbdoux0H+U|iVKe+4sZ-U9;`(m4R z)^(>3A9i$3NwvzB4JlfWv84Y2;)pBXEL5--eMjAAK0sz4#7>Zl65>dI9_gH^&a&BM zi=IV1k0=@b`L9wfD*=-2aq17`emjQ=IybQ(+8Yt zvGMnWP7`kuevu50JCz zZdLIZWy@A;8#f01T5@vvgWb&i zWo2%#Dc0-BWs+&unl~!HLf)D6ztf%%+r?Nuc6-{8@c!imIXS3?ZG?=?_a`NZ7zJ{z zFTiOFBi88qQIPyS{DJBCfZ${A!OPUv$dmXkjyf z_>&gm&8HaBWnWTYD!=PfnKmG$hLWj-)yB60yQSo7JANNyl<>QjE==GdL-aZ89Qenu zSq+gTgG`@W+Pt&1KP3kSDg5*o%i~NLNM*^gLeoDMv-TIOB*=glL3E@1|`m$m4P)s*c-SlWUxAEwb=*&J>e&ene#)20FU0u zhE9CmG+c^>=hA`)N;MlWwGB>(EfQx)4wFzGW}*(LTD^iv!TDzX8|ONvMSCYP9L5yG6#k(E&`R7CDCdLnSb9(B3^)eR%^&*CEk&l(Yc@$RT}Yh@NwJKHdC$ z85fxI`=`3uXZ-KddG!;H?Fsmw=^aHX@ciXAK7A)%IGs~(elB-#aG*Y4?xal{y1erb zfP{|DI|_rFp;Lg2jHrXN(pbdG^oqt#6MS$`4|^^8Eu$CHYKt_q0^b2};hG{esW;k{ zj6oFpi_j~&5`J(~VaMOaOa&i!bzH&S7Awbu#jX-D_F{AT6Do>i)Gm_$mmpkoEyNJQ z_(rDxQxOJEs9d@v72}$HZqq+&qtn{={C(4 z=EE{bmS^0YYYfVyiy89zgM7%fRi6>Ls3sG5scew>e#P;QT7Db9U6tIxwQZi*t&T%? z(RiEM_(ztZgIi+EW?Gzj4-%^^@m+cIV8#_36Fexz-)DEjKR%O29#hV)8Y=#d%k)+; zxGzGHN;_xu0^B9gXTQ+UtuJ|o;53VKS5p$6SDrc2#b9LYw^J+KnIB%g-xQw5CYlck zJzS0nek0{1Zjd(wmjl5vf2g%U$mhD^8}cz0nhmmRn+ZO3dvhrN;#bT z#4aBR4^hU%v_$k7Bl9BikwS_F!LxN7MRp~NmdsV0>{43%5u8BNNl1R=IL8Ng>^)}` z`I7-D&MsP=;XFZdDr#(6)=&H&L*=eYRJ)*GJh;`O#H2V1?EynVUvRqS#=u?Jgn@hA zl}TFWlUlk$3?#B+@}_04#p2J`<%_oKEi9S@u#{QRG;nj5kM8yoI#Pj1y$E(U(Qvw| zhjKj1L%tza&6(+q;vQKxic29};GP}U+ju8a%1_);Y&N$YWggy`i09?GpYJ)AuzFOV zzbO`7tb+UslT!3X$=U{23=cTFlZllgHRQ{P5(P-VP@9l5r>B?xS|SOXXD@gD{@lht zm=l;ukw$ELds+TVz*N6u|hJGEN zOEu_5#4sLMZ=iw&#wf+_dm+04(+w@bY-cCoX5p99@wqgsR@977@Cb6G)@*^0SKi@IlTOxK?3O5i>lZx z*YXG^y8D?To*K$9$b_sD>rZsg_$9SXj=*###lfn2s&gKe8jae`jP7UNAzWd0Yf*qE z?oiO-l3PQW^)C2I6xrg*;s^3%PG8qUTrL|?<7pd{V2ROfloK9FaJw)$CQM6i)T}hO z`H~Dp(WIEVkvC7)o>lrCDh#c}CSxC8qGW%+Fw2X<+Tb<0OaWLYL`6?Gxh&N?ji>^8 z#iAtbYWF&fe$>CUI*cM4&DIJ@Dc`23?6 zm1Jm^%dFfjv(&^z{I3wYa+#iJ^8;)1Gm(7U5QrT={|_S(-=#}#6X6u+aBfBJQ?-NZ z+-x67;~=Z;t5>^SFMq#qiPHCSspU1A_v{C&bY%_buVQl1Iz1tA+bklA^YpYW_ z;AJh;=_$PzRfZA!Jau%-lD+Blgs%^)GJMQDPeq6TvG-1@{mq6pC(F~}gCrqh%##)^ zgT~-klB|CKacEdGmTwq{Qs}v3Yez%NJ|M1J6?Wf;*^kN40lO+2z5GF)Xt1sVo4CG1 zl2seW`Cg#=hg1TSc3CfQsGB*8`C|VBiRbfYVv%_eGq5~1APYDol)N%6yPo?~ z#rL9y+$#y2f9Mj0hj~{Eb|vP6L|#<|p%z>)qMx`UpX=Onw8o&k^tZS_T9J}65+x=(Lbmw1BV}1Y zI3&h(4}zzN!}rXc+B|`f6O_e@YaIG3ZPiS+bip(qJ}o3SJ=W_5Rq;EdNX7X`Lfy;z z(g&L3&V2HBPj(W*_Rn22_f3~XgtVJL&&^d>&SVM+#etcA(?`Qi!GR1jdqTPTw<^Ua zF|F42rnMWInTQrbddC`8X$=49Ym{!~9n*`wyNJ@$&h;RXpI>SI0aOKLRC(T%B!B<( z?b|!qAN4_4PXKbX$IO00JG|EkA#A_Mcg9P1eLtFQP)W3YcWHd4)OB}A$(#!p!_qOm zU*)oxAevv9T$-|&cYLYhe5ir%6Ni@S870tJQyGx{^y`lHX!t1X&FAScotQTfAN##E zT<1{IA?+dt#2aRMYG%RNsOB74@4NlC_iejE(o!4$0OrRo>Th_z_ALAZ>{dI^BH5XO zep15DMME;LX^l%aH#+7nbs+c1Yro(??ir_dY#N)U)lY8g9WFRFOt;(Kj_^sOsUaA# zi3HVd)NZBnZ`IZp{{ir7&(pTzc+1h`GZxh%#p=&LDDpI6mS}s7y>r*a4NMF`?yqTd za;%b^ckcX-e&ZHzFH{}q<0nK1EtmaLi6lnQOWNUSGojzzA^ri*-NOF?s`vf@9$sWN z$5_x4oaB-oFKM}bYu7L$5i8*j4Sq^@S(w1JsSUJ5C*D1+?7IhZQF1qN>y$Kva z6|sFoaW-YRPLsH^^l{6o?5Qo6cwj%f{L=06^y#z%?{#rDSupv`3YGU+YTQya7B3Ew zho?5KD#)W+?4jaT;U8eF-RvJ=hWEW`?qYS3%)?z@=fm^g7ln)HT}i)PzwkE$;d_6+ z<{aj19a9f$v6Lb_UTYWH_H?7F?yzLB5QXVXz>{3;e*mtxhl?xPSWX&$~QtjVi(cs#)1*Ps*5l&iROU@_H&u<~qvXkb3?4D1b zb4hi*t9O6J7O%y+13LtVGUJYz`X(Q@J#q2v?Mdk>6(DhmCTS;>qeaq1r5gj!h@W$5 zAbFBqnJ^3vV4f1wm=$==ivbb|EJd31{##28tq*}Hsr`b!CMyw<1$h;jK-bxoGeQV1 zfG%lOtnxFDn;|wn^Jw6lD$1F=@sERdtA$UtE%HUa?@CGI{4X^H$=L78!!B?e%Kg#r z^xxEP0v6t4hgigcm>haF!?2tBk^HW))tXWJ2E!p4sRrcFtvmQjtnigoQ_?Gm<(T5_ zB8jBfqa$P4Nu|v!Atc>GEQjO#%KRtW;fe8b*l6N@+^~MTf-jlcld4kq4jS4aIJ$Uk zAGYQ4J`q$jfdf%pG&#NcJZXNu{NPan?4CNKdg)-RH#wNn4>6Wz243yuiSJQLgO{W? zE7mSUi+GXS^-j7Y4Q<9`mMAQ|e*h{T^_$O<9aFc@OP?Mbmj)uL`0ejly?XW9JICwU zlSX|}_OBH`>N+RqWeo^T+cK%kXAyz@%55>x3X@h6hRDl<7^S3dHbgU-V?NvxtKk!rlmiSa&*th3z)qevq@`ovo#sdKiPrw8?WmQ^v zIFTC>F+h_Kk-7d>CPqnaez#00+BT;~*=5z(bHUlBTEfbs2Tq|^WPVSI2w^zU^J(1; zhpI`z9UXfIX(BPo`yVR}T&DBiNL?4qz_6y_fywSVX9dlEBE|A|xJ2WycMcy8hX51+ z%#9BKRhrFA;pE;#AghNVQu7HyT~~{8DF2BvrWKkDlL9IqiHqBXELUV_h)z<0GJe2E zD=GqQmrc@R2E~4uDxvTf-;ncGUc4Rt7T*YyGuCw?9xF(20g4FT@;Dn zxQv`ivW+~GI*uB|1744)KNqb#Nh9{e?+n*@e^~7EV`Dj99xNM*xQow`vdu~{auRJ7 zWaPRz%3}RQf9l2xB4zUex`9$s=;Iv2h9h74Blelu-=uWs)T<)UgaFV6d&o*iu*9ij zk{X{J(GWJuzAleI+79VnFA(1E&YI-7Y2k@%r1_;uMC)*c;xudS6+TsC^=K-7h46uR zZJkys>iAppe5Umk?rwz)sH-w$$^-mW)E$!=GWb?@+|10J2m4)pXaOaRn3o!ETSu0f zY7-GxE}6;rS&)(w^gWWrys zJo)&kn|?Q77GE?kyKRFl6Imr$B+ER+8XBl)#Rh}dhmMjTK$v}VWJ1az>g8-zV#N`o z(J3<1WkZ=JPe|5C>twBzsl+Y|tOrc>!$e8Dh}E*J<2kf#ehbc1od0?p@cLbPI@(Th za<)`nY(!nOUxminzMPub?`e6z#Kp4oYRjlJB6VA#p;EGK3#bVr4GsDMmSvN|m);0R zdrh~vY!ym+9G95QbiXR2<|`dxRLlC^$Q2MnwuEcbz%!y z85^&w?+O-A0^Z^wJ=G(3rDVb_o7-Z8KJOCLFa0#z;DYNrq@wB47=;PQH&;XV+xQXG zxn=19xP%II=Fi{)e(CKn_&&ySsvY`B3(}v3a zeE|RhXi5DiKi;~->qmshlZgdB)%_X4-fq+@h};>Hw(M{pC=w2%$4 zH^XJ=zd>h?_Zv;e57d`Ad6J=cuCCymu=4AGImg5=-HPRaD3T*{DLvQLvL^NJq4j%A ze?Eh7>S$#@pDLai;QjZv7gpjpUW+45UkdHUZ8Zr780 zFE-dCF@NxCD%p0R2xXTM@pBS#G&c80dm|y-LRLDQr6EvDba5xX3&EsnG8@}pXpFu` z_y#{WGq>EUX&pDmE#ZQ*{daA6qpZx_sK)7J5%GwaVzEF3-E-#vZ3w zYp~j7Np^5r6;x+*#2dQs; zwr#QlHVN>@O&Os)+~D)myH7BqUXavaryM%H_}lQ>HNIj~xL~X8qwx5)e9+LJz#U!- z?n61tV&7l=5&?RGv5nqG_7TfJ)!<;dIvp8KPY`oEjlb-6xsS0bo}ZjWks|xAfK08a zI%aK4jDYP{tfld#ioCoCq?bcbr?`s8a3D$ zv=OV1(p_ZFX;jk zzZUj}EhC7YFh|jF92j315z0!0DXU6Gk27r@jtnjLW_L&%tVqEd%tY#}9t}Qc)k{Bd1##i+g1PA?&gH z018%nqB)ay8peH=LE7_yPU(fX(v8!yNKYlVM~J4d@HZW%{Cj*7vn5Jwl4!woQtJ(l zU;~V3E^&k*rZK53T%NrT{`gYW8ghnPVES;DR8l4Hyo|)+-2RX>UsRU=O1ZL~gCgI9 zRj!Ck(=IqCsgFO|fBG`9>&$eLQf3_Kkt_!|gUZ@HrQI7|2dXMdrm{Kk=h!EDETIW{ z2n3XIGC2tKW@vin%8L(AQOHED~I`(sZ_rST~<6XB1nK#eXCh`0Yt?sh{z_F^ak= zANqtq=hcxJiOwERt2p;XQW7E`k*Suk3OKtWza#PKzx`PA{e3bD-$DPNHeLQ*UN*UeQxw zVS&3!_y)&-bmbWbg25`<*C`tAHt)(uA$&oSdWFwl({coVT(*IwUFH2S9G_;JUoG$6 zjCS&zRAXh0AT57zEz0d_$OsMY>P?iJAjMdnx6(Wm$T`1z8| zailSoOKHv*aMBFF(?W< ziioYeSPFf+N#;Sqobc0Bv*HRO^M%y|V0oE02X1mm?QmYt%vq zR`zz8YZ7d?T%u90s>50iUNWc2*we>}*UomjXf@&mPP7~t(~b4dK1;F}xEv^!oe-t* zIxtPX7PrC2U3%>xR;3e3_^!WWuTf=t5{FnkBpnrf?F4n58u%b4!PRVvJJ2S2_qK1E&Ac!rq z4Y1bMlD_Pfaj1|l^_}BX9@R(4(gDV+A1=#j-@qQESj()xDjHL;#{G<2j!dwt(!$qPZJ)n#r97&OVf)Y95HEsYb{dKN;oP}=j=26*wOYED)K0ZC^ilMuP)htqyWd^6GOt= z`;Xw}t`~lPR%+j(UN&AH=0~XLQPA~Lkt;WD(W=+*$q_1d#Vd?01MNl9DJB0zdu+T zZFlZdS*W9_|Km9>6Z3+#MtTBcEw|gH*4vq^LbTl_Nfhs#bH9-lx8nwiwt}_E=gNz-tWBTR>Xzw&!7(t#< z)-8Q5aH{)(M5f(o8dEP%HfvkprOxA5XXzIXzTcJ`l^S>ra2V^?e9oXEF09ZIcY%AP zPeHjeyF9AZFJ1@j*{*gaAb)D-pQ+%QEo%sfeO1rUF@Tn@H1Q3k)hSBw*ACq(L)%TQ zc;eV)QEqMvn3^hL6Z$3!)2Z&O^M0e}n*Hc}kiM^jb_)WU?BGP3nqvMy^^-*|t9TtB znK_%{+q1tk9%z8bNvedPa>sfA7~5qeRrKKO&AgJ zrka*Mm3jkjk{Q(oUZf7sn(My#`?&-th3_jwrjoAO!xv^glE$+llhvArAZ=j{RCF&~ zO^=_hX?aD%FY<(Nri&usw}tG+T)*&eT`XwK)0^xv&(pV%YHtZ$lemx`>OpsbAgGZu z!FJ$g)N*b>Y(XTD5PKH6-g%@7MGE$Tp_Aqn_o1%={x(UhmG}_K7i2n}w^|et`x@|T za?S3}@PSo0Sv+jah5#PoT$8NqAN?+YV5ccZHNHQ_dGbe!1FIZBOAzBFrf)3W`gh@w zdWt@+59zwxZ)hhR-{=b2C{fjBnURf>a@wg%h9SHU6Jg3hf*Z_pU|i$goD{<}vjC+T zRv@O$`BmB2EmNsuU3eA6`GlP2;|e`lJf~p?gN8KTgS+i?p*;BuBJVs=OAFP zijb3A;LsxA55694(qput}uVAX!(*%(sV$Kr^_j(z0dx*T8|? zQr|QC_^_W%s->J1EF2fmy2IvTr%D0JFcR7_-x=#oxEHBI7bbE^1?r)kr=-rvSx`{X z8d347rCbQ{n1zvIz&WO-I62p6u1BUBBF5BVFw0m`6p_|7V{dE_wZxid`kXyGvL34F}>s)$EdbwUtKNi-egz0MEMf9{z23irdWXcC4l zb7D^?Swg8@Zpe^mrjNC7M}~Km2ZllU`h@8(haWf5h*zj;hg|<%r@Yz` zDxUqahVCL_Mc0IrG4C9TuNE#mck#9n>qyK5=JVWFI^?)@u5S(lkvyRlg-+@x+AjH; zznS7^^`JHQX?}FoQ*`HLGFjkq5kOYJ#7Ok&xUK=s-GKHdv@D`U&x(B}xT*A|W)@vw zS+<y5ssScoBYVze{l=jI3 zP}c8R(*Sc@L%IQ!pyvHdUz@C`{-^5L42*J&mOy#ze4Z6dVC!Vt&>f>E_CSY~E%UKT zf5VCo9jjBgDfV8O(ogsYK4NFZ)#hHNHk?zQQ2XoV0$l47^9^-n6l zGrq^0o`lj{LCfdwT~le>1Pt`5i1U_^JLH6sb_$hN=(h|E%MAR)G2rA5Oxq{8hbT zI>RJ07y84lUFNpNvv9g`*1ePXZI#rDIF`oeEWi;Sa*$TbO}}iu)@#2{!UZenrY6S<0xXHE{=j`tC*kwo%hPV1U$-U@`LN~p zYB`FYK?d1c`4@Ui8Oj^rr3<3voX|G;;g0_HNSq~LH=EG66CMR~YdeLy(Be;Btm1y_ zb0dsj%m*5scSO)JuXtBsFIIScLpZ@FIkSem6;5OaZOjXJCN5tUfL(0W9URW}OcF>= zV22+B)4xWB$U=&xsKsXagCtHUv$xD~mAUA?dDT+|-%WvfZV2lel0`1F;=J7_conJ` zRyr@^2<9}RFw_Xe61=qR7kz`Jg;sS+xlF2;o}#xhQiB(<-~LT?hF~c2AKK5>wO231 z7gYQvmSZcu*J3k9x3$*^ucy;pM4I7gG#t?0epfz?;A(c4nC9g`z=CloGRHPi>L)^l zI{yH=UjG2Z{{SG9eWirknB42n*HqWb{{ZCm38}N?dhLggdqx%v2oJqt98QCBUG>(A zPa%e#Po~YjJL&Zu*ySq5;)tNB8zHp5q3>}Agjhe^K8t&xIR1HVy>q!nf2^TR{C9uK z`|UqKHb2-bg~qPAGlkUX)*@9#$*}iUbqE=RaF^c^0QvCdi4L2AeF1i zW(j8g=JsT0{6}NIyXkz(m3fAIcsfQX1GYsN~R{mtL2|3R~L_a8xQ_XGkBBG|26k%zvR3286&-USEj;GHn2ia z5)D-!%I@o7SA-p#hm@9+Ejh+l3A@kUrta!8$Uil&2D;|I5rn%k_`qwM`Cv9gM~ki1 z_5w>h{{Y6Km8KG*bNysQtA8R}L=l6QMwNpTXrJ@HR;ZHp3Yc0-QWhd#NnPd#Pi#0% zzzmO0dQJfn3+jJI<=LDp_uTWTBN%&BUiIC$iXchyx+;IOw)M>n2zzC#B6RH1z^B-} zb;5huK4q|r9(O9g@#BbixEpVvV+oZE%Bw?&^1_f8i_6!5m4*mY$4cTmy}mP&55QNo zns2hu*e05lCMH!G}37S1`CSPmBiq#lArOP^U^%U0U4^~DgWBvNji;+*`Yz1W2x;=W5{GrD~~D}8uzFAaTiU@Ow?yf7#C_rc|Au`Ud-e>9V9*1xT=`C zs3SBsRhuDWA6Zp_XDB8@g0g5d98R^w^CK2FV7AJ4?6db%eVqzak;T--B}}7`@{cCU zC|I-v*nJqmb%kU+S7~zeOEY7!%*sT45ZsW6_B$&U5USng-H^XvtgBUAE&i!`AeOo2 zZspE}--n42T10b_7`VH^mGAbajOovb4(v-<$h?lDVYVy+ZVgb|)y9)e8kMUwED-Nv zv`TWD-vdtgJEhbnm}c5G{l;7aSB6aoz)cN^lcdD;d6sZsiUfGm{ZD%;#fTx5DdY|q zOb!!JCv?0RMLuEIZ9yytK9e93C`<0>Kp6+n-TPV)4b~hG^ukaB;AK?KVBv+OOWhQj zgtW3PMVlrO6W|jHWvb}YvTfVDD=WW!{Aq`j6bZG?Y&QsXjH9d&Vm#p&Lq^w>`iaJK zZhWFBC$&TzoXP^Xe=2urems>lPclqM^7G<0SzNjgro?^9$`eEU^Fr3TyQjt zDXc+ONeyb>C#M93B1A|HSU-$;CSMe6JMci8$Ly^9C!-zmd*>G*Sm}9GM33PymRLRk zfRs=pNUS813jumZY$t)>1jQ;Q9T$re_okedwq}6gViAC_Rfkj~kzO96R}-9O>9O;e zdO~Ca>BaEJ=_hynyN>=_6J2B3-hR!iNVT6bn)f7h9WufpKn5W0aH(FsdV+@COkou##hPguEXYYFd0#6lx zMt3=iJ@%^Q*RZ;{G6=<;bWb|u9gTI_827>G_-615CJCiju?uq~zAZLUv;+3dW(42oMtJ-wDCTbqsNt*^P?!Yr*%^es^8ee7;_D6a*gXpv3pj$hW4DU zFhlUlGJrhT&Jz^AcevUtIPj8V?Pe_9GUZ)QlQ?*BQznittPhylLUT4&R>->62*CrJ zCLq4hspeB<%lUo;ID82<6#pA#=d!_!(VLFTs&bEXC;VwSI?!pVAxDnG2f+p2?OwT; zdRqg0SQCG7K5oOPVg=nQWVj2~hGBeIiivB+5SQCG{aCghufxg-J@@37+-QJg52exl zf)E`Tul7g?ShVo;n33?`hr3uZ#h#c{HO}J|X-{|Jlr>Y0b?UZ{?m9%kwYT4$&`Jy) zm*d^Z~ULR8i)vG zr625DqThP*AQeoX2>%#O!knc9{h+8?pIP$JlMBC4u;L<%{2~%971M|omcz{Qqad2* zD*k!7^v4|7P0UaP{E_o_h5e_v`Q9HBV%6pG-s~|^x|Od`yC01*+CNkJHTBPTw_mo^ z$(Jq4325P#^+wo9Zv(DzcWEw012ntje)NtI<)&H;u5%~ypP7w1$adx8tEW=ZJ!7tnHj2<=(pRGKgCRT+eHr-amKQTV6Ep{GJb&4Myyu6Bg zNqogytn*l>zl!_6>?PGzv3icr+M>)6)&FH*S&r0GgZ_`G*8hLTk5&~G3FB>dDi53b z|A!e37_~#$+xI&g-meP1_&?TPg^uKm;4l7Xru5t5q44hsa`VOUY~h(}(9}P`o4K1N z8T)^LcE{`Q?X^Z)Hfey%>k!<47Oi*Hz7IxC$7z3#bmoR^aK&s6wkHm{-yur$zy_B z2ky(JE#o#nA9P^1t8=P-WeyX{EFUT#Mpsb4+N|bt3{Ph6+GcHNEZMqj+U5*%z--0^ zhpe?+p|rUdcN{GGE(0Y<^*pjvtS#_a+G78J(o(uczd!p{Wp>;7X|FpbZLN)nM&+zp zAzy9_PzUpT%XEuy#lfH9mSwe+O`17X`hVG69YX$2ICz*8FOW_ z&G4WpZPQg<&p9_741FpE z_IhA{?RBi^E#&TZGNG#{_9Y0PYiPnsM{umB&oWYShENW>nEv>S-RepWS5Y1*W!S8t zhV?s&OOzwnW}XYqS`k&CJxS^fDmIR+Y{;|kf0Hk$Uax03CHc!kQDsHnVFi?F*Cj?M z7C&p6ek8~bR%0SEnO>0eV%suP$@UBUe@HqDsHXn^jgN*2NJ|Ke#u3sDGNijUN)+i0 zhIFH}FdC%0b99GH1exCR9dfwUK^Iqqw=-s+q zv*0bCnZ7)~G6M^v%-d-MJ12$4PpBzDnhay3kg9#->hSWZ9Ag?xoZ{n^+(H|ZmK-#gN4{vY-xy{z{Hq~Sc#qZ53nuB!iLW?H!;$yDQXmwYFF%D`Tt2%^HX3ikW4fM(J!ss#hVsG`bnQqmN>T~z|JxH8{LoK*A{ z-J0x&dXP7Wbq72?Pp+N98IPy}pz6Up1(XKH#wc)-;s5SGfGCyVk>RLB1awHflox}G z$f^)mA4OG88h)nW#MQQs-)~WqWzGngNh8i+jBDi3NVr&M#bbC z@3|l)5VY937ALXwlkk{ujzI}`!0$>QTekQGL19icB9x?Eb9e`iVzSPYi2w-N#pt8e zw$}-t;6|JMHa&&2KC)%0;+0$X@%k9py8ZG`Z7CHZ{n7+U_?-`?&?47zDTh!=5kk@n3n4*XH=sr-s$Q(-V}~h1XWnhu_Du+If!_Qqh_i{VLoLEycWB zKGkr1!=m#?q3ny+B)reUgezQ7k>kc=WVp%3u<>J#W&5272cW9&*<40vTGPQ=qY_si z9sxbpACWZlX2G|%*r2L=wQ-;23d5wR(la@R7lP_aYt{>@HMf7=oj(PFv!{;Mp+pUba&C)H5pZ7#!ar`mF|S|F>vkhiX@#jLbEH$7PpFPlFSjv9S6f%{tRs#qX>ty|wH50x}9QvZm^BZS+*(EJ6PO@s5GzZT4F^lnX|a ziu6>Rmo}k(VHs#^q>YCSK{v8r$WBr=;Ttj3YyzaoxlcHm z%M5WQdENgPS~_bs^{LZd6Z>`Hz<2?&@uKZLjSvItt~Mu#*?#my0B}qSPNnfnEya>) zw1j$qH2SYN>YHY&J`Md1%UfgEzAw15(Bu{EnQ0LXF6slD2^983tJ1K>m(fD`dt#5>{SN$?6+}Gzrpx*)Y={i=Z0m*57faQ zrdB~;g$%j}528=APG8K+_d>@Gh)+@X3i&VoXD!18b7u*xa>b7E#LrrIW+^K?dS~Xo zUj*%LaGHn-&uhTwuXH#krB*>M_m`fzxn6c!Smd&Om2&0{Zt1-o^ifV*A+GoH42s8J125kykBl^XF_hKbIezh+IT+eML=HhM1TS(~R5g}lo zC!k+O3|L@EtiHgw!`)~LEnwbEZi=sqa91WSq`}~ZqDV0gAV$a50u3sE77V#GE=l1d z`-7RXgNeiC#}v!L}nCXUbK>y7Y?M zdxuiDRgQ$nYa!cy!sNp4V5*a+xv7zYB4G%Dw4<1JDJ;b zQzLkQxxFyvquNm7kg zqM5eGg_g&%;+Z;$J4M~|=^lsnfWD(AXm)M-k}lpego~pMIuvyI2Wnlg8eav?M$MI8AzH zCXeF>*ksAx>3l*ynqXUY2~g6kGrBX}+%K^n6_g(!-n5yM#WBy~bxHNHX5)`xKwH?;g6Nwqllqgpe-6ALkZ5gmLsZ2Y@0GR0mNx8mpl71$=H>q$Q zX2#OhgR4>*VJwLXOZEDThDhsKJ~tM z55Jb>=gPL%OLyjzj!KhveEK5i`&axWv4-AiIF4p;6>f;a;b!$Sdsxgz_4ze&<;;C; zr2JdeCSR5?t;(yGmH_>%AdXQ+A{*Jqvq4W*#4H^#!~ZEKBgZIpWXLHxaNG zBDb^GDQZOxJ4Rn5x1hy}fPk20;m3W4Z+4%58Mx`u$*xL6Hl?`FcJ^iq=><}7R2klWhA;GM9b@+VGWDw0PwXEOyMP-Na5-*$V70BpWi#Vhl zR1rKVT-mc|yVV@D;mD7JQ`fdH;u*8p+Qn+w->Y%hGT^t_P-=^ z4@j9Jag&}o{LDIeN#^v)aAEje6OJx!$|$y?W!>TY5@QyCv$$5T>T%zvfvSlAm`K~= z{L{fL0&lH%pp72b1(A&>evBmuO4~dsd>Zg`GT$WlulQdX>-42r6V7hk12NXg0$fF! zA>F|b9;ZCk+cHyha;a|jUruS?+b#cbY<~X4qvIv{93iKke`D1nkA5&WyfAHtSUS`n zHKk9mN>nD32S5EesK#ZM>ENEpaCWAsowiF~Dug&|PC5P-oU=D+)0HZBHHKbIedv1Q z7?q_n7y>MpKSC%tU`?=^KN1SwW*hkE?$@*~vUB6q8SJipUc};~XKN}{`)A_A?Vphl zcfof2T`7P`#{7#W=eB&CsXTc*oAcVTreWt(F@I+94O~BeS)|M6S`8^3zr%qJ0d$VtWNc`@fHx{~Zr)b2wj z|Gpki(o0dJ9jAbP&%ck;Bm{C*K7YBJxByBFdag~}U^jXEKI%ly*Ee+YBfr32ccGR1~Cg~j5=v_KFzf})Zi8e$k_oQ6H! z6si^#aM36lV?D=7Fan4P1fmR%fEZe0LBKPp=2@c)3dxzs`nqa~IBHRVbnW>E zZZx&!YyUpQ8ucCi0a%<4vI{RSoi{p%AkAGXT9DTu#a}0xVm{42I9j{LH9+RR!j}a% zSt>ocg<^45^%5vpbpTUlO_jiY$z$;&xqfYQQV45L&!rWlK(j%M+X#E-VvA&pB(!!TAX{~|qm1bF3ms6qIG47R z^EBmVm$_x=Pdva|@fgzDB6D>OM?$}v_}JCvP;Wf>YTciEyUY4a#Zr9?cQsR4<*O|n zkKCp-ij6HB!^cJ#3g)Hg`o_?_vz5K@MY6I7?F?k`keYcs`%y&zJsuI-shP+%sEhIC zSnC&tq^#GGn!gTeKgFaAlEv1%hjXfab(c*za*}L4RDI!2NYuQDyeUM3-}4qcLQbd| zQ9g5}DI$8)SplwUuSPWt+VngzqAzac(xR}oV)?*8TB_%vA2`quY zfI=bk4ed?DP5^4QGpMRbkinbNy9k=JTGT%{(rr+{rpT@5i=rX`d5A%+YeRz#RKXFC zG)b%!S-qe%9#ER_N)xCdmS!;w53Q}LMQDm~M=clzJCX@k>cTa}32?-5s&OLICSh>S z{z(yz)FvHL^%71>?F?E@8})XN&5YsXg9=)2jig&&LSBIPK-}OZfzlv-nFY)nhnVaN zwG=$<@fC8ARxrOpa+rnw&=)1FO@tv(p;6I$PD9;KsrAl;zBIXiI7rBi`Q0Fff5w`p zWEQwQEUd(+{4im-{Ew+J*~f#82}QiiU&n7!WkU=JIZEF!V4O(YKYjQ^WsA4;#WZcY zh5)4Zl0E0?rdIrCCVI%oODvHGy#tq2uvy@$ARHaP>AL{e3Ut5xm@Oe76!IUkX*5GW z^5Rz*6{UcWn>`9-PN7oeX`ydvPImV_HER(>r88`x!Ide(EZ4A%^;UbE%F<07OsIWO zdn}4`96_>*14384S zRBoop1D^tH!k(*fd*qkbhhM4rM>FD!WERz!XsAvm=B(U{tn_;1KXsr&izt`SR2?$J)pdClSqL`9(!5C-F z`*&I;fqvC<;%jvC28g+)Xnmz~&cPR*|gFyQ_ zXUYh3a0y{V+um332wxXi{!EE5brqmfb&6?-@0gDIz5sBQ^EpZaL9x z46f1hm}{8){JNj$^Xs;F|9OyV#EZ-!_RDp5Wi|Pe%`f&`Mbaa*kGwA->>>~GjqvD- z=BlFE-ul(5U=iXjzP0r`K>~}ETgBVEA#d?PBChaIkBb*y5c4a*+tBsaxtfyPZyH=7 zy9Gmh)KDDYu6*26$bm(Vr@5`=k#Upah(5`7zVr2|&Ns4QPHpR03qDVP;>#}8UGvJ! zucExZ%SmCIRSPYqqL@%3%^4RyL&eUHohf6-AbynkA=U<<2H*6Rj zeLIvpOhBx;@4b@rDOsf&;LMvhb$JNESlHWHb9SDM9paT>w67#O-Y({CE9qOT$=Pn=iWvGn_R~?<1n6J%1)0kobt4w0UgLX4%6cx5<2U zo7`QCB`l(*Gu6Ajgcb}@s-0=T{90HOEL~X93_A+JfB4SdJdsgjFHaAzzACENGZh>` zHbvw4<2Sgr!03R$$Q?J0qXgs7I#C0_@{$?(KlCj(opXmQ`-Jc-c&@~$F8sVHe>bPw zbP}z4ged&sWn5O=s?pxNDQ-Jbe%icY2Q($$8c(%-y^!ro7AF0S)xR*XH4`zvksBjC z1vj$zE&2~I9@v{3C^^G-($;HqMQfuKaD(-X3B#UzkS_l3xw6=QE9Pjeb9G(kHvi}i z8juNGV1BE3s!)_a&-rgH&GY~N7^NzB<{<9BwT~9wye;41E8?xf90bAcPTsOt9RIhZ zQh%XzOvYlX@&_s(+H;>DRa;qAY?OZU-;##X|CZ!hn&-^^_q2%dzvV|LHdwkG{C@-R zF8#MHi^W)IF+E}0|3*$YS^D3;cN+j(r2he6{~Mz6|HnI8n8~@Qeu>>_>)H>lE8uo^k?sw1Pd1d04 zcG1x@Xi)cOiGDjt+SuF<(e<0MH_JyGd?L$c1|cDs1zH;qpnaY0$4o28OFqjGa0kz> z<}>9|oi;ldMhDP;6)awLMc}@gA5K&xZ6Nw+}17>do zkYlpzg8|eQ&p#yaAJ|LRbe=jJuXSy?J{X3RdM{!uzD2JqH(#C6R!*=3-p+8wV;F;E z-aucsKy9&Q^SYDMZVKDm+e#7h*X|cWl5cR_h*mrOVFJD+)H6d4PQ3AJuxD{jv*r)| ztbzdO+RzZL?b64kH7%~jNwnT^7`qBd51Jd+lgx8TQjeVE86epd4--$TcD-Frgc zLh0@Uz90!p$YSLTnvTK7S=(qmv_1N304xA#xQpAReCcnLFV83W*{EM|{XQ5 zf4uGPsO1lnW?3k3(w!B?|9$M-5nZ7jeS#ns=b#bNFy_bC_N1Ob#tOft<3@wU?6JvOFiKsml(pw@H~mX7}rQi-o-Ud-$$Fg;a#&Mj&v&HRrqI=!_`Pc$76q!F)xsZv!}cFJYg%db`3Co3OFEOOYq~Znqt%4F0h?`HBh-k zO~g@gtlT9z*+ELt^ zhqIcDK6x;gxou$ijMVVuLBFh7^fqs@o7e#90a~!95)4S9ejNBDxTil@$RiKK9mU{0 zcCi_rvwJlOA`G5wNcIc%Lumyt&Y4AV2>FF+f@KbW02|@X9}CF@@ki9rs*Vg=ymEnV zsIePw2lkE62n%7i0yhgmsw%D9(HiH?B7dyYBj(n~ziPuMwu6FawPS|b;I$ddHl;g} zQ239NeGOgkd!EZ0^J==>th+@Q@k8r1O@EU$EDcq%WpE($y?e&}q2XH^x^r4PelAE{ z!C=^wQ-K)wPsZ%KW^-FdN23GDF*=(=ntaM0JMq@PMREK1Kr@Vnm0m?W=luC*T2Ldr z7Drau1z$}H{d%ays-%FUm*u;Lb|_A$=nw;_>b*`M!*0rjNvaHVpIZ^sAL?6RrP)%C z=VF@388Q}+aW|z<+WV%bSz?2S`*0U!l(CB(H3Y^JpIuz2=A};g`-quP39=}N)yo7d zh6z^CZA4MC`=OM$H$DN}JuC!69=n--7gXfrO$*`v;1wGA)mCC2uY7RPW3O5iQ7dBc z3S-83i=FPV%vG5sq*-a$WiYGqx&#Xn>X%9bVks}gIWgv)X@Of{ffPVe$cqh;@?5WM zuHaBzoD;-^(Y^i5XCg9fJ7KiFWO*mF@<;FA51R8aV8 zmm(B5Xzawv*~kU?4E|wduj*S+LYv&X;l>kjN;lVnWK~}lECL{)x>JaZ>pOIuc&9ny zelU4{9&^O2)gN*u?{6b!ULvXuTXv0gk<<#Gn3OqqBe+X4HpY2-%Y2%R{M|}SuFhqA zgIYljeyOEv_iHb3s9?cpI$Qp#a*=8!S;h|u?s3@qVlGeD|E&+Ymo@$+_46BR8r6a2 zUtMz-qK!k71wxUnim$aFW-tB&JopFrmjCqd#r@Wo{J%!~zfX4*!e;(3&pxEQ>fugm zzK_$i^mQ_ScR#?cZFPx%>08%$+RO8jp|Yo>Igsg9nU+ko`9q1xeFcBIw2z-YQ-)F5 zJOloGG_xU(el?ds+7Nggk)0mwQ90Rz#mIA9dARw`H%%gP--L=fHg_^@GM8JrS*)t5 z>4;diYY_Pq+?$X-Q(sc>;S03ce=EJ6{#1CzS8}G8a^CV$t6=5o%~A(`)F=s2o`Tm& zAkUF((BoNbJQw)S<3-=wHgkzhOYx%HYwvjSMS7>EZ;~=aJwxR?%7u2j7VE!g^zqlbjzDafrat?jJU+KKl_|g_=^C@em6QoM|DPQp9p`n<^4-+GmYmb5RA2*+$AI;vk zY-rY{*IqgKJ9YZSjqSUT&S->vX0V_yyzbWK86h~~+lHd2)_@j`u%cUiY zS>LA-N6no`2(;H}FyeHoAQiACX&SG54`Er@9tvXp_P)sH{%QGD0`;Zkvq<0J>OmKQ zdAp3CG4*1Z_K#CuNzs0MR3`2BzaWX)=0{j3cXCynr6#KCBul*mipM>EzMLfuKzi|r|bR!Dt<00@DSNv9=5wsT6DCI+F;yex;K zZBdR_AB$m>v|0VImmN#&2%wk^6B}vqTxs%SnK!o{KkZ~DjuKuzNNv9)37g|p+9Gt= z*YB*ff8Bl-lK1erQn4pF_Gf1istjy+VmiE>EMKit%&AA4HK#Eq2kUZc28hUD;yO+u z^WXT-787R`HT9^2=o517e=%u8_|1SDvT)%CI@b)U;<0P^s^TcD$k@LdV)+tV({z%4Cg= zkTWHTJ!6(qu_P4mm_AE%6NTZw8p0zKfe%`zch_!9aftP6w@UJ^>p2z&OBJ+A3RlS_C%_~SRJ)lb}|~(0UF}j{haE6ksf|L3`GDX zVXl*^_*ADyz!<4}pU2tv;>x|By2-w`#Q;3%Nsg2IxJOZ&^9oDzReXH$K2)|rv*v8& z>^SsA{{WFBI#L9&RFG28#5@izHU@jAQKiLIxNOOuAteqAVO-m4dD28OK6r*7a{!)bT0tSz18yznok?7xST4R>y4>51L*s zF!{&4PD4sRXnz|m7g(+G8Fr34N@FG+xFBFNYx7+ws$=INU^~d|d#I&e6STNOQQhntW zcxYpY9>gG)SR4m1(~ttuHpaI%-mUXD>SnAHr9q45sPe*AI@}cP3@|PHRLV0P8Dlit zQgvxVW=XN%univAKbY&7rWlHrnu_tMiO3&m^3V_#>k1o!Az_)6ihEU~IK@KB^XM

*e*`YE>ublQa!dY%rp&XmiISTHDCx;t>j+a> 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 literal 0 HcmV?d00001 diff --git a/integrations/deepseek-harness/README.zh-CN.md b/integrations/deepseek-harness/README.zh-CN.md index 6d33156..f64a7fb 100644 --- a/integrations/deepseek-harness/README.zh-CN.md +++ b/integrations/deepseek-harness/README.zh-CN.md @@ -2,7 +2,7 @@ 该 Cordis 插件向 DSH Web 注入 beautiCode 浏览器客户端,并提供本机鉴权接口。支持图片、MP4、播放位置、静音、摸鱼模式以及清除背景。 -装好插件并运行 `dsh web` 后,侧栏「设置」上方会出现「背景」:可从文件夹选图片或 MP4、清除、开关声音、切换已保存主题。网页控制台没有摸鱼;浅色/深色跟 DSH 自己的外观走。下次打开会恢复上次背景。 +装好插件并运行 `dsh web` 后,侧栏「设置」上方会出现「背景」:在 Windows 上点击图片或视频会打开系统文件选择器,选好后在页面内命名,背景立即应用并进入已保存主题;也可以清除、开关声音、切换或删除主题,以及打开「皮肤中心」安装已审核的社区皮肤。本地导入只在 Node 端保存绝对路径,并通过带 Range 支持的回环媒体服务读取原文件,不复制用户选择的图片或整段视频主文件。Windows 强制使用本地引用;原生选择器不可用时会明确报错,不会静默上传或复制媒体。非 Windows 环境保留兼容上传,并明确提示该模式会保存托管副本。网页控制台没有摸鱼;浅色/深色跟 DSH 自己的外观走。下次打开会恢复上次背景。设置 `BEAUTICODE_SKIN_CENTER` 或填写 `skin-center.json` 后,皮肤中心才会显示远程目录。 插件也会注册对话工具(`beauticode_*`)和斜杠命令(`/bg`、`/bg-theme`、`/bg-clear`)。不需要托盘;托盘若已在跑则复用它。 @@ -40,7 +40,7 @@ npx @deepseek-ai/dsh web 手动接入时: -1. 修改 `cordis.patch.example.yml` 中的 `file:///.../index.mjs`。 +1. 修改 `cordis.patch.yml` 中的插件接入项;优先使用上面的安装命令自动完成接线。 2. 确保 DSH 和 beautiCode 使用相同的 `BEAUTICODE_DATA_ROOT`。 3. 自己运行 `dsh web`(或带 `--patch`)。 4. 打开页面后用侧栏「背景」,或输入 `/bg <本机绝对路径>`,或直接跟 AI 说把某个视频/图片设为背景。 diff --git a/integrations/deepseek-harness/agent.mjs b/integrations/deepseek-harness/agent.mjs index f4b1810..7cb952e 100644 --- a/integrations/deepseek-harness/agent.mjs +++ b/integrations/deepseek-harness/agent.mjs @@ -1,3 +1,4 @@ +import path from "node:path"; import { callDshControl, formatStatusText, @@ -5,7 +6,7 @@ import { matchSavedTheme, stripPathQuotes, } from "./control-client.mjs"; -import { resolveApplyBackend, stopInProcessSession } from "./host-apply.mjs"; +import { loadAdapter, resolveApplyBackend, stopInProcessSession } from "./host-apply.mjs"; import { ATMOSPHERE_PRESETS, effectsForPreset, presetImagePath } from "./presets.mjs"; const APPLY_TIMEOUT_MS = 180_000; @@ -32,6 +33,60 @@ function fail(error) { throw error instanceof Error ? error : new Error(String(error)); } +export function themeNameFromFilePath(filePath, fallback = "主题") { + const base = path.basename(String(filePath ?? "").replaceAll("\\", "/")); + const ext = path.extname(base); + let name = (ext ? base.slice(0, -ext.length) : base).trim(); + name = name + .replace(/[<>:"/\\|?*]/g, " ") + .replace(/[\u0000-\u001f]/g, "") + .replace(/\s+/g, " ") + .trim(); + if (!name) name = fallback; + if (name.length > 80) name = name.slice(0, 80).trim(); + if (!name) name = fallback; + return name; +} + +async function chineseError(error) { + const raw = error instanceof Error ? error.message : String(error ?? ""); + try { + const adapter = await loadAdapter(); + if (typeof adapter.toChineseErrorMessage === "function") { + return adapter.toChineseErrorMessage(error); + } + } catch { + /* keep raw */ + } + return raw || "操作失败。"; +} + +async function unwrapApplyResult(result, fallbackMode, message) { + if (!result || result.ok === false) { + const failure = new Error(await chineseError(result?.error || "操作失败。")); + if (result?.sourceMode != null) failure.sourceMode = result.sourceMode; + if (result?.timings != null) failure.timings = result.timings; + throw failure; + } + return { + ok: true, + generation: result.generation ?? null, + mode: result.mode ?? fallbackMode, + sourceMode: result.sourceMode ?? null, + timings: result.timings ?? null, + message, + ...(result.theme + ? { + theme: { + id: result.theme.id, + name: result.theme.name, + type: result.theme.type ?? null, + }, + } + : {}), + }; +} + function commandResultFromError(error) { return { kind: "error", @@ -57,10 +112,20 @@ function unwrapApply(result, fallbackMode, message) { generation: result.generation ?? null, mode: result.mode ?? fallbackMode, message, + ...(result.theme + ? { + theme: { + id: result.theme.id, + name: result.theme.name, + type: result.theme.type ?? null, + }, + } + : {}), }; } function presentStatus(status) { + const background = status.manifest?.background ?? status.background ?? null; return { ok: true, hostReady: status.hostReady === true || status.sessions > 0, @@ -68,7 +133,13 @@ function presentStatus(status) { fish: status.fish === true, muted: status.muted !== false, tone: status.tone ?? "dark", - background: status.manifest?.background ?? status.background ?? null, + background, + sourceMode: background + ? background.source?.kind === "local" + ? "local" + : "managed" + : "clear", + themeId: typeof status.themeId === "string" && status.themeId ? status.themeId : null, message: formatStatusText(status), }; } @@ -106,24 +177,45 @@ export function createBeauticodeActions(dataRootOrOptions) { fail("beauticode_apply_image 只接受图片文件。"); } const effects = effectsForPreset(options?.effects?.preset) || options?.effects || null; - const input = { type: "image", imagePath: inspected.path }; + const persistTheme = options?.persistTheme !== false; + const themeName = + String(options?.themeName ?? "").trim() || + themeNameFromFilePath(inspected.path, "图片"); + const source = options?.source === "managed" ? "managed" : "local"; + const input = { type: "image", imagePath: inspected.path, source }; if (effects) input.effects = effects; const resolved = await backend(); if (resolved.kind === "tray") { - const body = { imagePath: inspected.path }; - if (effects) body.effects = effects; - return unwrapApply( - await request({ - method: "POST", - path: "/apply/image", - body, - signal, - }), + const result = await request({ + method: "POST", + path: persistTheme ? "/theme/apply" : "/apply/image", + body: persistTheme + ? { name: themeName, input } + : { + imagePath: inspected.path, + source, + ...(effects ? { effects } : {}), + }, + signal, + }); + return unwrapApplyResult( + result, "image", - "已将图片设为背景。", + persistTheme + ? `已将「${result.theme?.name || themeName}」设为背景。` + : "已将图片设为背景。", ); } - return unwrapApply(await resolved.session.apply(input), "image", "已将图片设为背景。"); + const applied = persistTheme + ? await resolved.session.applyAndSaveTheme(input, themeName) + : await resolved.session.apply(input); + return unwrapApplyResult( + applied, + "image", + persistTheme + ? `已将「${applied.theme?.name || themeName}」设为背景。` + : "已将图片设为背景。", + ); }, async applyPreset(id, signal) { @@ -132,6 +224,7 @@ export function createBeauticodeActions(dataRootOrOptions) { if (!preset || !imagePath) fail("未找到内置主题文件。"); const result = await this.applyImage(imagePath, signal, { effects: effectsForPreset(id), + persistTheme: false, }); try { await this.setTone(preset.tone, signal); @@ -167,8 +260,13 @@ export function createBeauticodeActions(dataRootOrOptions) { if (inspected.kind !== "video") { fail("beauticode_apply_video 只接受 .mp4 文件。"); } - const body = { videoPath: inspected.path }; - const localInput = { type: "video", videoPath: inspected.path }; + const persistTheme = input?.persistTheme !== false; + const themeName = + String(input?.themeName ?? "").trim() || + themeNameFromFilePath(inspected.path, "视频"); + const source = input?.source === "managed" ? "managed" : "local"; + const body = { videoPath: inspected.path, persistTheme, themeName, source }; + const localInput = { type: "video", videoPath: inspected.path, source }; if (typeof input.poster === "string" && input.poster.trim()) { const poster = await inspectLocalMedia(input.poster); if (!poster.ok) fail(poster.error); @@ -186,21 +284,29 @@ export function createBeauticodeActions(dataRootOrOptions) { } const resolved = await backend(); if (resolved.kind === "tray") { - return unwrapApply( - await request({ - method: "POST", - path: "/apply/video", - body, - signal, - }), + const result = await request({ + method: "POST", + path: persistTheme ? "/theme/apply" : "/apply/video", + body: persistTheme ? { name: themeName, input: localInput } : body, + signal, + }); + return unwrapApplyResult( + result, "video", - "已将视频设为背景。", + persistTheme + ? `已将「${result.theme?.name || themeName}」设为背景。` + : "已将视频设为背景。", ); } - return unwrapApply( - await resolved.session.apply(localInput), + const applied = persistTheme + ? await resolved.session.applyAndSaveTheme(localInput, themeName) + : await resolved.session.apply(localInput); + return unwrapApplyResult( + applied, "video", - "已将视频设为背景。", + persistTheme + ? `已将「${applied.theme?.name || themeName}」设为背景。` + : "已将视频设为背景。", ); }, @@ -240,6 +346,39 @@ export function createBeauticodeActions(dataRootOrOptions) { }); }, + async importTheme(input, signal) { + const name = String(input?.name ?? "").trim(); + const imagePath = String(input?.imagePath ?? "").trim(); + if (!name || !imagePath) fail("导入皮肤必须提供名称和图片。"); + const body = { + name, + imagePath, + }; + if (typeof input.videoPath === "string" && input.videoPath.trim()) { + body.videoPath = input.videoPath.trim(); + } + if (input.effects) body.effects = input.effects; + if (input.source) body.source = input.source; + const resolved = await backend(); + if (resolved.kind === "tray") { + const result = await request({ + method: "POST", + path: "/theme/import", + body, + signal, + timeoutMs: 30 * 60 * 1000, + }); + if (!result || result.ok === false) fail(result?.error || "导入皮肤失败。"); + return { + ok: true, + theme: result.theme, + message: `已保存皮肤「${result.theme.name}」。`, + }; + } + const theme = await resolved.session.importSavedTheme(body); + return { ok: true, theme, message: `已保存皮肤「${theme.name}」。` }; + }, + async listThemes(signal) { const resolved = await backend(); if (resolved.kind === "tray") { @@ -278,6 +417,32 @@ export function createBeauticodeActions(dataRootOrOptions) { }; }, + async deleteTheme(id, signal) { + const themeId = String(id ?? "").trim(); + if (!themeId) fail("必须提供主题。"); + const resolved = await backend(); + if (resolved.kind === "tray") { + const result = await request({ + method: "POST", + path: "/theme/delete", + body: { id: themeId }, + signal, + timeoutMs: QUICK_TIMEOUT_MS, + }); + if (!result || result.ok === false) { + fail(await chineseError(result?.error || "删除主题失败。")); + } + return { ok: true, deleted: true, message: "已删除主题。" }; + } + try { + const deleted = await resolved.session.deleteSavedTheme(themeId); + if (!deleted) fail("未找到已保存的主题。"); + return { ok: true, deleted: true, message: "已删除主题。" }; + } catch (error) { + fail(await chineseError(error)); + } + }, + async setFish(enabled, signal) { const resolved = await backend(); const want = Boolean(enabled); diff --git a/integrations/deepseek-harness/bin/beauticode-dsh b/integrations/deepseek-harness/bin/beauticode-dsh index e1cbe2c..d038d73 100644 --- a/integrations/deepseek-harness/bin/beauticode-dsh +++ b/integrations/deepseek-harness/bin/beauticode-dsh @@ -1,219 +1,7 @@ #!/usr/bin/env node -/** - * One-line installer: npx beauticode-dsh - * - * Copies this package to a stable local folder and writes the DSH patch so - * `dsh web` loads beautiCode. Does not start DeepSeek Harness. - */ -import fs from "node:fs"; -import fsp from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import { fileURLToPath, pathToFileURL } from "node:url"; +import { runCli } from "../cli.js"; -const here = path.dirname(fileURLToPath(import.meta.url)); -const pluginName = "@beauticode/dsh-plugin"; -const bridgeId = "beauticode-bridge"; - -function argValue(argv, name) { - const idx = argv.indexOf(name); - if (idx === -1) return null; - const value = argv[idx + 1]; - return value && !value.startsWith("--") ? value : null; -} - -function defaultDataRoot() { - if (process.env.BEAUTICODE_DATA_ROOT) return process.env.BEAUTICODE_DATA_ROOT; - if (process.env.LOCALAPPDATA) { - return path.join(process.env.LOCALAPPDATA, "beautiCode"); - } - return path.join(os.homedir(), ".beauticode"); -} - -function defaultDshHome() { - return process.env.DSH_HOME || path.join(os.homedir(), ".dsh"); -} - -function toFileUri(filePath) { - const full = path.resolve(filePath).replaceAll("\\", "/"); - if (/^[A-Za-z]:/.test(full)) return `file:///${full}`; - return pathToFileURL(full).href; -} - -function fileUriInsert(uri) { - return [ - "# beauticode-bridge (installer)", - "- insert:", - " - id: beauticode-bridge", - ` name: '${uri}'`, - " inject: [webServer]", - "", - ].join("\n"); -} - -function packageInsert() { - return [ - "# beauticode-bridge (installer)", - "- insert:", - " - id: beauticode-bridge", - ` name: '${pluginName}'`, - " inject: [webServer]", - "", - ].join("\n"); -} - -function hasBridge(text) { - return new RegExp(`^\\s*-\\s*id:\\s*${bridgeId}\\s*$`, "m").test(text); -} - -function stripBridge(text) { - const patterns = [ - /(?:^|\r?\n)# beauticode-bridge \(installer\)\r?\n- insert:\r?\n(?:[ \t]+.*\r?\n)*/g, - /(?:^|\r?\n)- insert:\r?\n(?:[ \t]+.*\r?\n)*?[ \t]+-\s*id:\s*beauticode-bridge\r?\n(?:[ \t]+.*\r?\n)*/g, - ]; - let next = text; - for (const pattern of patterns) next = next.replace(pattern, "\n"); - return next; -} - -async function writePatch(filePath, body) { - await fsp.mkdir(path.dirname(filePath), { recursive: true }); - if (!fs.existsSync(filePath)) { - await fsp.writeFile(filePath, body, "utf8"); - return; - } - const raw = await fsp.readFile(filePath, "utf8"); - if (hasBridge(raw)) { - const replaced = raw.replace( - /(?:# beauticode-bridge \(installer\)\r?\n)?- insert:\r?\n(?:[ \t]+.*\r?\n)*?[ \t]+-\s*id:\s*beauticode-bridge\r?\n(?:[ \t]+.*\r?\n)*/, - body, - ); - await fsp.writeFile(filePath, replaced.endsWith("\n") ? replaced : `${replaced}\n`, "utf8"); - return; - } - const stripped = raw.trim(); - if (stripped === "" || stripped === "[]") { - await fsp.writeFile(filePath, body, "utf8"); - return; - } - await fsp.writeFile(filePath, `${stripped}\n\n${body}`, "utf8"); -} - -async function removePatch(filePath) { - if (!fs.existsSync(filePath)) return false; - const raw = await fsp.readFile(filePath, "utf8"); - if (!hasBridge(raw)) return false; - const cleaned = stripBridge(raw).trim(); - if (cleaned === "" || cleaned === "[]") { - await fsp.writeFile( - filePath, - "# Your patch layer for this dsh profile.\n[]\n", - "utf8", - ); - return true; - } - await fsp.writeFile(filePath, `${cleaned}\n`, "utf8"); - return true; -} - -function shouldCopy(source) { - const relative = path.relative(here, source); - if (relative.startsWith("test") || relative.includes(`${path.sep}test${path.sep}`)) { - return false; - } - if (relative.endsWith(".test.mjs")) return false; - return true; -} - -async function copyPackage(dest) { - await fsp.rm(dest, { recursive: true, force: true }); - await fsp.cp(here, dest, { - recursive: true, - filter: (source) => shouldCopy(source), - }); -} - -async function ensureEngine(dest) { - const vendor = path.join(dest, "vendor", "adapter-dsh", "index.js"); - if (fs.existsSync(vendor)) return; - const packPath = path.resolve(here, "../../scripts/pack-dsh-plugin.mjs"); - if (!fs.existsSync(packPath)) { - throw new Error("插件包不完整:缺少本机导入引擎。请重新执行 npx beauticode-dsh。"); - } - const { stageEngineInto } = await import(pathToFileURL(packPath).href); - await stageEngineInto(dest); -} - -async function install(opts) { - const dest = path.resolve(opts.pluginHome); - const dshHome = path.resolve(opts.dshHome); - const webProfile = path.join(dshHome, "profiles", "web"); - const webPatch = path.join(webProfile, "cordis.patch.yml"); - const webPackage = path.join(webProfile, "package.json"); - const homePatch = path.join(dshHome, "cordis.patch.yml"); - - await fsp.mkdir(dest, { recursive: true }); - await copyPackage(dest); - await ensureEngine(dest); - const indexFile = path.join(dest, "index.mjs"); - if (!fs.existsSync(indexFile)) { - throw new Error(`缺少插件入口:${indexFile}`); - } - - if (fs.existsSync(webPackage)) { - await writePatch(webPatch, fileUriInsert(toFileUri(indexFile))); - if (fs.existsSync(homePatch)) { - const homeRaw = await fsp.readFile(homePatch, "utf8"); - if (hasBridge(homeRaw)) await removePatch(homePatch); - } - console.log(`已写入 ${webPatch}`); - console.log(`插件已复制到 ${dest}`); - console.log("请自己运行:npx @deepseek-ai/dsh web"); - return { dest, patch: webPatch }; - } - - await fsp.mkdir(dshHome, { recursive: true }); - await writePatch(homePatch, fileUriInsert(toFileUri(indexFile))); - console.log(`DSH web profile 还不存在,已写入 ${homePatch}`); - console.log(`插件已复制到 ${dest}`); - console.log("请自己运行:npx @deepseek-ai/dsh web"); - return { dest, patch: homePatch }; -} - -async function uninstall(opts) { - const dshHome = path.resolve(opts.dshHome); - const removed = []; - if (await removePatch(path.join(dshHome, "profiles", "web", "cordis.patch.yml"))) { - removed.push("web patch"); - } - if (await removePatch(path.join(dshHome, "cordis.patch.yml"))) { - removed.push("home patch"); - } - const dest = path.resolve(opts.pluginHome); - if (fs.existsSync(dest)) { - await fsp.rm(dest, { recursive: true, force: true }); - removed.push(dest); - } - console.log(removed.length ? `已移除:${removed.join("、")}` : "没有可移除的 beautiCode 插件接线。"); - return { removed }; -} - -export async function runCli(argv = process.argv.slice(2)) { - const dshHome = argValue(argv, "--dsh-home") || defaultDshHome(); - const pluginHome = - argValue(argv, "--plugin-home") || path.join(defaultDataRoot(), "plugin"); - const opts = { dshHome, pluginHome }; - if (argv.includes("--remove")) return uninstall(opts); - return install(opts); -} - -const launchedDirectly = - Boolean(process.argv[1]) && - pathToFileURL(path.resolve(process.argv[1])).href.toLowerCase() === - import.meta.url.toLowerCase(); -if (launchedDirectly) { - runCli().catch((error) => { - console.error(error instanceof Error ? error.message : String(error)); - process.exit(1); - }); -} +runCli().catch((error) => { + console.error(error instanceof Error ? error.message : String(error)); + process.exit(1); +}); diff --git a/integrations/deepseek-harness/cli.js b/integrations/deepseek-harness/cli.js index e1cbe2c..f95b548 100644 --- a/integrations/deepseek-harness/cli.js +++ b/integrations/deepseek-harness/cli.js @@ -11,8 +11,24 @@ import os from "node:os"; import path from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; -const here = path.dirname(fileURLToPath(import.meta.url)); -const pluginName = "@beauticode/dsh-plugin"; +function findPackageRoot(startDir) { + let dir = startDir; + for (let i = 0; i < 5; i += 1) { + if ( + fs.existsSync(path.join(dir, "package.json")) && + fs.existsSync(path.join(dir, "index.mjs")) + ) { + return dir; + } + const parent = path.dirname(dir); + if (parent === dir) break; + dir = parent; + } + return startDir; +} + +const here = findPackageRoot(path.dirname(fileURLToPath(import.meta.url))); +const pluginName = "beauticode-dsh"; const bridgeId = "beauticode-bridge"; function argValue(argv, name) { @@ -34,6 +50,14 @@ function defaultDshHome() { return process.env.DSH_HOME || path.join(os.homedir(), ".dsh"); } +function defaultPluginHome(dshHome) { + return path.join(path.resolve(dshHome), "plugins", pluginName); +} + +function legacyDefaultPluginHome() { + return path.join(defaultDataRoot(), "plugin"); +} + function toFileUri(filePath) { const full = path.resolve(filePath).replaceAll("\\", "/"); if (/^[A-Za-z]:/.test(full)) return `file:///${full}`; @@ -67,44 +91,110 @@ function hasBridge(text) { } function stripBridge(text) { - const patterns = [ - /(?:^|\r?\n)# beauticode-bridge \(installer\)\r?\n- insert:\r?\n(?:[ \t]+.*\r?\n)*/g, - /(?:^|\r?\n)- insert:\r?\n(?:[ \t]+.*\r?\n)*?[ \t]+-\s*id:\s*beauticode-bridge\r?\n(?:[ \t]+.*\r?\n)*/g, - ]; - let next = text; - for (const pattern of patterns) next = next.replace(pattern, "\n"); - return next; + const lines = text.split(/\r?\n/); + const removed = new Array(lines.length).fill(false); + const indentOf = (line) => line.match(/^[ \t]*/)?.[0].length ?? 0; + + for (let i = 0; i < lines.length; i += 1) { + const insert = lines[i].match(/^([ \t]*)-\s*insert:\s*(?:#.*)?$/); + if (!insert) continue; + const insertIndent = insert[1].length; + let blockEnd = i + 1; + while (blockEnd < lines.length) { + const line = lines[blockEnd]; + if (line.trim() && indentOf(line) <= insertIndent) break; + blockEnd += 1; + } + + let foundBridge = false; + for (let j = i + 1; j < blockEnd; ) { + const bridge = lines[j].match( + /^([ \t]*)-\s*id:\s*beauticode-bridge\s*(?:#.*)?$/, + ); + if (!bridge || bridge[1].length <= insertIndent) { + j += 1; + continue; + } + foundBridge = true; + const itemIndent = bridge[1].length; + let itemEnd = j + 1; + while (itemEnd < blockEnd) { + const line = lines[itemEnd]; + if (line.trim() && indentOf(line) <= itemIndent) break; + itemEnd += 1; + } + for (let k = j; k < itemEnd; k += 1) removed[k] = true; + j = itemEnd; + } + + if (!foundBridge) continue; + const marker = `${insert[1]}# beauticode-bridge (installer)`; + if (i > 0 && lines[i - 1].trimEnd() === marker) removed[i - 1] = true; + const hasSibling = lines + .slice(i + 1, blockEnd) + .some( + (line, offset) => + !removed[i + 1 + offset] && + line.trim() !== "" && + !line.trimStart().startsWith("#"), + ); + if (!hasSibling) { + for (let k = i; k < blockEnd; k += 1) removed[k] = true; + } + i = blockEnd - 1; + } + + return lines.filter((_, index) => !removed[index]).join("\n"); +} + +function overlayPayload(text) { + return text + .split(/\r?\n/) + .map((line) => line.trim()) + .filter((line) => line && !line.startsWith("#")) + .join("\n"); +} + +function isEmptyOverlay(text) { + const payload = overlayPayload(text); + return payload === "" || payload === "[]"; +} + +// DSH seeds overlays as `# comment\n[]`. `[]` is already a complete YAML +// document, so appending `- insert:` makes js-yaml throw. +function keptOverlay(text) { + const withoutFlowEmpty = text + .split(/\r?\n/) + .filter((line) => line.trim() !== "[]") + .join("\n"); + if (isEmptyOverlay(withoutFlowEmpty)) return ""; + return withoutFlowEmpty.trim(); +} + +function withTrailingNewline(text) { + return text.endsWith("\n") ? text : `${text}\n`; } async function writePatch(filePath, body) { await fsp.mkdir(path.dirname(filePath), { recursive: true }); + const insert = withTrailingNewline(body); if (!fs.existsSync(filePath)) { - await fsp.writeFile(filePath, body, "utf8"); + await fsp.writeFile(filePath, insert, "utf8"); return; } const raw = await fsp.readFile(filePath, "utf8"); - if (hasBridge(raw)) { - const replaced = raw.replace( - /(?:# beauticode-bridge \(installer\)\r?\n)?- insert:\r?\n(?:[ \t]+.*\r?\n)*?[ \t]+-\s*id:\s*beauticode-bridge\r?\n(?:[ \t]+.*\r?\n)*/, - body, - ); - await fsp.writeFile(filePath, replaced.endsWith("\n") ? replaced : `${replaced}\n`, "utf8"); - return; - } - const stripped = raw.trim(); - if (stripped === "" || stripped === "[]") { - await fsp.writeFile(filePath, body, "utf8"); - return; - } - await fsp.writeFile(filePath, `${stripped}\n\n${body}`, "utf8"); + const remainder = hasBridge(raw) ? stripBridge(raw) : raw; + const kept = keptOverlay(remainder); + const next = kept ? `${kept}\n\n${insert}` : insert; + await fsp.writeFile(filePath, withTrailingNewline(next), "utf8"); } async function removePatch(filePath) { if (!fs.existsSync(filePath)) return false; const raw = await fsp.readFile(filePath, "utf8"); if (!hasBridge(raw)) return false; - const cleaned = stripBridge(raw).trim(); - if (cleaned === "" || cleaned === "[]") { + const kept = keptOverlay(stripBridge(raw)); + if (!kept) { await fsp.writeFile( filePath, "# Your patch layer for this dsh profile.\n[]\n", @@ -112,7 +202,7 @@ async function removePatch(filePath) { ); return true; } - await fsp.writeFile(filePath, `${cleaned}\n`, "utf8"); + await fsp.writeFile(filePath, withTrailingNewline(kept), "utf8"); return true; } @@ -125,12 +215,48 @@ function shouldCopy(source) { return true; } +async function sameInstalledVersion(dest) { + try { + const incoming = JSON.parse(await fsp.readFile(path.join(here, "package.json"), "utf8")); + const installed = JSON.parse(await fsp.readFile(path.join(dest, "package.json"), "utf8")); + return ( + incoming.name === installed.name && + incoming.version === installed.version && + fs.existsSync(path.join(dest, "index.mjs")) && + fs.existsSync(path.join(dest, "vendor", "adapter-dsh", "index.js")) + ); + } catch { + return false; + } +} + async function copyPackage(dest) { + if (await sameInstalledVersion(dest)) return false; await fsp.rm(dest, { recursive: true, force: true }); await fsp.cp(here, dest, { recursive: true, filter: (source) => shouldCopy(source), }); + return true; +} + +async function removeLegacyManagedPlugin(legacyHome, currentHome) { + if (!legacyHome) return false; + const legacy = path.resolve(legacyHome); + if (legacy === path.resolve(currentHome) || !fs.existsSync(legacy)) return false; + try { + const pkg = JSON.parse(await fsp.readFile(path.join(legacy, "package.json"), "utf8")); + if ( + ![pluginName, "@beauticode/dsh-plugin"].includes(pkg.name) || + !fs.existsSync(path.join(legacy, "index.mjs")) + ) { + return false; + } + } catch { + return false; + } + await fsp.rm(legacy, { recursive: true, force: true }); + return true; } async function ensureEngine(dest) { @@ -144,6 +270,83 @@ async function ensureEngine(dest) { await stageEngineInto(dest); } +function pluginLinkPath(webProfile) { + return path.join(webProfile, "node_modules", pluginName); +} + +function legacyPluginLinkPath(webProfile) { + return path.join(webProfile, "node_modules", "@beauticode", "dsh-plugin"); +} + +function linkSpecFor(pluginHome) { + return `link:${path.resolve(pluginHome).replaceAll("\\", "/")}`; +} + +async function sameLinkTarget(link, dest) { + try { + const stat = await fsp.lstat(link); + if (stat.isSymbolicLink()) { + const target = await fsp.readlink(link); + return path.resolve(path.dirname(link), target) === path.resolve(dest); + } + if (stat.isDirectory()) { + return path.resolve(link) === path.resolve(dest); + } + } catch { + return false; + } + return false; +} + +async function linkPluginIntoProfile(webProfile, dest) { + const link = pluginLinkPath(webProfile); + const legacy = legacyPluginLinkPath(webProfile); + if (fs.existsSync(legacy)) { + await fsp.rm(legacy, { recursive: true, force: true }); + } + await fsp.mkdir(path.dirname(link), { recursive: true }); + if (fs.existsSync(link)) { + if (await sameLinkTarget(link, dest)) return; + await fsp.rm(link, { recursive: true, force: true }); + } + const type = process.platform === "win32" ? "junction" : "dir"; + await fsp.symlink(path.resolve(dest), link, type); +} + +async function ensureWebPackageDep(webPackage, pluginHome) { + const raw = await fsp.readFile(webPackage, "utf8"); + const json = JSON.parse(raw); + if (!json.dependencies || typeof json.dependencies !== "object" || Array.isArray(json.dependencies)) { + json.dependencies = {}; + } + const spec = linkSpecFor(pluginHome); + const hadLegacy = Object.prototype.hasOwnProperty.call( + json.dependencies, + "@beauticode/dsh-plugin", + ); + delete json.dependencies["@beauticode/dsh-plugin"]; + const current = json.dependencies[pluginName]; + if (current === spec && !hadLegacy) return; + json.dependencies[pluginName] = spec; + await fsp.writeFile(webPackage, `${JSON.stringify(json, null, 2)}\n`, "utf8"); +} + +async function removeWebPackageDep(webPackage) { + if (!fs.existsSync(webPackage)) return false; + const json = JSON.parse(await fsp.readFile(webPackage, "utf8")); + if (!json.dependencies || typeof json.dependencies !== "object") return false; + let changed = false; + for (const name of [pluginName, "@beauticode/dsh-plugin"]) { + if (Object.prototype.hasOwnProperty.call(json.dependencies, name)) { + delete json.dependencies[name]; + changed = true; + } + } + if (!changed) return false; + await fsp.writeFile(webPackage, `${JSON.stringify(json, null, 2)}\n`, "utf8"); + return true; +} + async function install(opts) { const dest = path.resolve(opts.pluginHome); const dshHome = path.resolve(opts.dshHome); @@ -153,7 +356,7 @@ async function install(opts) { const homePatch = path.join(dshHome, "cordis.patch.yml"); await fsp.mkdir(dest, { recursive: true }); - await copyPackage(dest); + const copied = await copyPackage(dest); await ensureEngine(dest); const indexFile = path.join(dest, "index.mjs"); if (!fs.existsSync(indexFile)) { @@ -161,34 +364,50 @@ async function install(opts) { } if (fs.existsSync(webPackage)) { - await writePatch(webPatch, fileUriInsert(toFileUri(indexFile))); + await linkPluginIntoProfile(webProfile, dest); + await ensureWebPackageDep(webPackage, dest); + await writePatch(webPatch, packageInsert()); if (fs.existsSync(homePatch)) { const homeRaw = await fsp.readFile(homePatch, "utf8"); if (hasBridge(homeRaw)) await removePatch(homePatch); } + const migrated = await removeLegacyManagedPlugin(opts.legacyPluginHome, dest); console.log(`已写入 ${webPatch}`); - console.log(`插件已复制到 ${dest}`); + console.log(copied ? `插件已安装到 ${dest}` : `已复用已安装的插件 ${dest}`); + if (migrated) console.log("已迁移 1.0.5 的旧插件目录,已保留主题数据。"); console.log("请自己运行:npx @deepseek-ai/dsh web"); return { dest, patch: webPatch }; } await fsp.mkdir(dshHome, { recursive: true }); await writePatch(homePatch, fileUriInsert(toFileUri(indexFile))); + const migrated = await removeLegacyManagedPlugin(opts.legacyPluginHome, dest); console.log(`DSH web profile 还不存在,已写入 ${homePatch}`); - console.log(`插件已复制到 ${dest}`); + console.log(copied ? `插件已安装到 ${dest}` : `已复用已安装的插件 ${dest}`); + if (migrated) console.log("已迁移 1.0.5 的旧插件目录,已保留主题数据。"); console.log("请自己运行:npx @deepseek-ai/dsh web"); return { dest, patch: homePatch }; } async function uninstall(opts) { const dshHome = path.resolve(opts.dshHome); + const webProfile = path.join(dshHome, "profiles", "web"); const removed = []; - if (await removePatch(path.join(dshHome, "profiles", "web", "cordis.patch.yml"))) { + if (await removePatch(path.join(webProfile, "cordis.patch.yml"))) { removed.push("web patch"); } if (await removePatch(path.join(dshHome, "cordis.patch.yml"))) { removed.push("home patch"); } + if (await removeWebPackageDep(path.join(webProfile, "package.json"))) { + removed.push("web package.json"); + } + for (const link of [pluginLinkPath(webProfile), legacyPluginLinkPath(webProfile)]) { + if (fs.existsSync(link)) { + await fsp.rm(link, { recursive: true, force: true }); + removed.push(link); + } + } const dest = path.resolve(opts.pluginHome); if (fs.existsSync(dest)) { await fsp.rm(dest, { recursive: true, force: true }); @@ -200,9 +419,13 @@ async function uninstall(opts) { export async function runCli(argv = process.argv.slice(2)) { const dshHome = argValue(argv, "--dsh-home") || defaultDshHome(); - const pluginHome = - argValue(argv, "--plugin-home") || path.join(defaultDataRoot(), "plugin"); - const opts = { dshHome, pluginHome }; + const requestedPluginHome = argValue(argv, "--plugin-home"); + const pluginHome = requestedPluginHome || defaultPluginHome(dshHome); + const opts = { + dshHome, + pluginHome, + legacyPluginHome: requestedPluginHome ? null : legacyDefaultPluginHome(), + }; if (argv.includes("--remove")) return uninstall(opts); return install(opts); } diff --git a/integrations/deepseek-harness/client.js b/integrations/deepseek-harness/client.js index 359332f..dda4de7 100644 --- a/integrations/deepseek-harness/client.js +++ b/integrations/deepseek-harness/client.js @@ -9,6 +9,7 @@ `bc-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`; const desiredModes = { fish: false, muted: true, tone: "auto" }; let activePayload = null; + let renderPhase = "idle"; let playbackBlocked = false; const systemDarkMedia = globalThis.matchMedia?.("(prefers-color-scheme: dark)") ?? null; let themeSyncQueued = false; @@ -161,6 +162,9 @@ html[data-bc-fish="true"] #root{opacity:0!important;visibility:hidden!important; } async function acknowledgeRender(payload, ok, visible, error = null) { + if (activePayload === payload) { + renderPhase = ok ? "ready" : "failed"; + } await postAck({ kind: "render", generation: payload.generation, @@ -189,7 +193,6 @@ html[data-bc-fish="true"] #root{opacity:0!important;visibility:hidden!important; function loadImage(url) { const image = new Image(); image.alt = ""; - image.crossOrigin = "anonymous"; image.decoding = "async"; return new Promise((resolve, reject) => { image.onload = () => resolve(image); @@ -198,7 +201,19 @@ html[data-bc-fish="true"] #root{opacity:0!important;visibility:hidden!important; }); } - function waitForVideo(video) { + function describeVideoState(video, phase) { + const mediaErrorNames = { + 1: "MEDIA_ERR_ABORTED", + 2: "MEDIA_ERR_NETWORK", + 3: "MEDIA_ERR_DECODE", + 4: "MEDIA_ERR_SRC_NOT_SUPPORTED", + }; + const code = Number(video?.error?.code) || 0; + const mediaError = code ? mediaErrorNames[code] || `MEDIA_ERR_${code}` : "none"; + return `${phase};mediaError=${mediaError};readyState=${video?.readyState ?? -1};networkState=${video?.networkState ?? -1};paused=${Boolean(video?.paused)}`; + } + + function waitForVideo(video, timeoutMs = 20_000) { if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) return Promise.resolve(); return new Promise((resolve, reject) => { const done = () => { @@ -207,9 +222,14 @@ html[data-bc-fish="true"] #root{opacity:0!important;visibility:hidden!important; }; const failed = () => { cleanup(); - reject(new Error("MP4 加载或解码失败")); + reject(new Error(describeVideoState(video, "MP4 加载或解码失败"))); }; + const timer = setTimeout(() => { + cleanup(); + reject(new Error(describeVideoState(video, "等待视频首帧超时"))); + }, timeoutMs); const cleanup = () => { + clearTimeout(timer); video.removeEventListener("loadeddata", done); video.removeEventListener("error", failed); }; @@ -218,6 +238,67 @@ html[data-bc-fish="true"] #root{opacity:0!important;visibility:hidden!important; }); } + function waitForStablePlayback(video, timeoutMs = 8_000) { + return new Promise((resolve, reject) => { + const startedAt = performance.now(); + const initialTime = Number.isFinite(video.currentTime) ? video.currentTime : 0; + let frameSeen = false; + let settled = false; + let frameRequest = null; + + const cleanup = () => { + clearInterval(timer); + video.removeEventListener("playing", check); + video.removeEventListener("timeupdate", check); + video.removeEventListener("error", failed); + if (frameRequest != null && typeof video.cancelVideoFrameCallback === "function") { + video.cancelVideoFrameCallback(frameRequest); + } + }; + const finish = (error) => { + if (settled) return; + settled = true; + cleanup(); + if (error) reject(error); + else resolve(); + }; + const failed = () => { + finish(new Error(describeVideoState(video, "视频解码器报告失败"))); + }; + const check = () => { + if (settled) return; + if (video.error) { + failed(); + return; + } + const progressed = + Number.isFinite(video.currentTime) && video.currentTime >= initialTime + 0.08; + if ( + video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA && + !video.paused && + (frameSeen || progressed) + ) { + finish(); + return; + } + if (performance.now() - startedAt >= timeoutMs) { + finish(new Error(describeVideoState(video, "视频未在稳定窗口内输出首帧"))); + } + }; + const timer = setInterval(check, 100); + video.addEventListener("playing", check); + video.addEventListener("timeupdate", check); + video.addEventListener("error", failed, { once: true }); + if (typeof video.requestVideoFrameCallback === "function") { + frameRequest = video.requestVideoFrameCallback(() => { + frameSeen = true; + check(); + }); + } + check(); + }); + } + function seekVideo(video, value) { const requested = Number(value); const duration = Number(video.duration); @@ -255,6 +336,7 @@ html[data-bc-fish="true"] #root{opacity:0!important;visibility:hidden!important; async function applyBackground(payload) { if (!Number.isSafeInteger(payload?.generation)) return; activePayload = payload; + renderPhase = "pending"; document.documentElement.dataset.bcGeneration = String(payload.generation); syncGallery(payload); if (payload.media === "clear") { @@ -298,13 +380,14 @@ html[data-bc-fish="true"] #root{opacity:0!important;visibility:hidden!important; video.playsInline = true; video.preload = "auto"; video.poster = payload.imageUrl; - video.crossOrigin = "anonymous"; video.src = payload.videoUrl; node.replaceChildren(image, video); await waitForVideo(video); if (activePayload !== payload) return; seekVideo(video, payload.startAt); await playWithPreference(video); + await waitForStablePlayback(video); + if (activePayload !== payload) return; document.documentElement.dataset.bcActive = "true"; document.documentElement.dataset.bcMedia = "video"; const visible = video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA && !video.paused; @@ -339,8 +422,8 @@ html[data-bc-fish="true"] #root{opacity:0!important;visibility:hidden!important; } catch { playbackBlocked = desiredModes.muted === false; } - if (activePayload?.media === "video") { - await acknowledgeRender(activePayload, video.readyState >= 2 && !video.paused, true); + if (activePayload?.media === "video" && renderPhase === "ready") { + await acknowledgeRender(activePayload, true, true); } } else { playbackBlocked = false; @@ -363,8 +446,15 @@ html[data-bc-fish="true"] #root{opacity:0!important;visibility:hidden!important; setInterval(() => { const video = activeVideo(); - if (activePayload?.media === "video" && video instanceof HTMLVideoElement) { - void acknowledgeRender(activePayload, video.readyState >= 2 && !video.paused, true); + if ( + activePayload?.media === "video" && + renderPhase === "ready" && + video instanceof HTMLVideoElement + ) { + // A heartbeat is observational, not a second render verdict. Playback can + // briefly pause while Chromium changes modes or refills an 8K buffer; do + // not downgrade an already-rendered generation or fail a pending one. + void acknowledgeRender(activePayload, true, true); } }, 1_000); })(); diff --git a/integrations/deepseek-harness/console.js b/integrations/deepseek-harness/console.js index 31e6c27..cdb5e72 100644 --- a/integrations/deepseek-harness/console.js +++ b/integrations/deepseek-harness/console.js @@ -16,27 +16,48 @@ #beauticode-console.rail .bc-label{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0)} #beauticode-console .bc-icon{flex:none;display:block} #beauticode-console .bc-label{white-space:nowrap;overflow:hidden} -#beauticode-console-pop{position:fixed;z-index:2000;box-sizing:border-box;width:220px;padding:10px;border:1px solid rgba(255,255,255,.08);border-radius:16px;background:#2c323c;color:#e8eaed;box-shadow:var(--dsw-shadow-lv2,0 8px 24px rgba(0,0,0,.28));font:inherit;font-size:13px} +#beauticode-console-pop{position:fixed;z-index:2000;box-sizing:border-box;width:256px;padding:0 14px 12px;border:1px solid rgba(23,26,29,.24);border-top:4px solid #252a30;border-radius:2px;background:#ece9e2;color:#202327;box-shadow:0 16px 36px rgba(0,0,0,.34);font:inherit;font-size:13px} #beauticode-console-pop *{box-sizing:border-box;font-family:inherit} -body:not([data-ds-dark-theme]) #beauticode-console-pop{background:#fff;color:#1b1f24;border-color:rgba(0,0,0,.08)} -#beauticode-console-pop .bc-status{color:var(--dsw-alias-label-secondary,#9aa3ad);font-size:12px;line-height:18px;margin:0 0 8px} -#beauticode-console-pop .bc-row{display:flex;gap:6px;margin:0 0 6px} -#beauticode-console-pop .bc-btn,#beauticode-console-pop .bc-theme-toggle{cursor:pointer;flex:1;min-width:0;height:32px;padding:0 8px;border:1px solid rgba(255,255,255,.1);border-radius:12px;background:rgba(255,255,255,.06);color:inherit;font-size:13px;line-height:20px} -body:not([data-ds-dark-theme]) #beauticode-console-pop .bc-btn, -body:not([data-ds-dark-theme]) #beauticode-console-pop .bc-theme-toggle{border-color:rgba(0,0,0,.08);background:#f4f6f8} -#beauticode-console-pop .bc-btn:hover,#beauticode-console-pop .bc-theme-toggle:hover{background:rgba(255,255,255,.12)} -body:not([data-ds-dark-theme]) #beauticode-console-pop .bc-btn:hover, -body:not([data-ds-dark-theme]) #beauticode-console-pop .bc-theme-toggle:hover{background:#eceff3} -#beauticode-console-pop .bc-btn:disabled,#beauticode-console-pop .bc-theme-toggle:disabled,#beauticode-console-pop .bc-theme-item:disabled{opacity:.45;cursor:default} -#beauticode-console-pop .bc-btn.on{background:rgba(255,255,255,.16)} -#beauticode-console-pop .bc-theme-toggle{width:100%;text-align:left} -#beauticode-console-pop .bc-theme-list{margin:6px 0 0;max-height:160px;overflow:auto;border:1px solid rgba(255,255,255,.08);border-radius:12px;background:#232830;padding:4px} -body:not([data-ds-dark-theme]) #beauticode-console-pop .bc-theme-list{border-color:rgba(0,0,0,.08);background:#f4f6f8} -#beauticode-console-pop .bc-theme-item{cursor:pointer;display:block;width:100%;height:32px;padding:0 8px;border:none;border-radius:8px;background:transparent;color:inherit;font-size:13px;line-height:32px;text-align:left;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} -#beauticode-console-pop .bc-theme-item:hover{background:rgba(255,255,255,.08)} -body:not([data-ds-dark-theme]) #beauticode-console-pop .bc-theme-item:hover{background:rgba(0,0,0,.06)} -#beauticode-console-pop .bc-msg{color:var(--dsw-alias-label-secondary,#9aa3ad);font-size:12px;line-height:16px;margin:6px 0 0;max-height:3.2em;overflow:hidden} +body:not([data-ds-dark-theme]) #beauticode-console-pop{background:#f3f0e9;color:#202327;border-color:rgba(23,26,29,.22)} +#beauticode-console-pop .bc-head{display:flex;align-items:flex-end;justify-content:space-between;gap:10px;padding:12px 0 10px;border-bottom:1px solid #b9b6af} +#beauticode-console-pop .bc-title{margin:0;font:650 16px/20px Georgia,"Songti SC","STSong",serif;letter-spacing:.02em} +#beauticode-console-pop .bc-status{min-width:0;max-width:142px;color:#686d71;font:10px/15px ui-monospace,"Cascadia Mono",monospace;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +#beauticode-console-pop .bc-import{cursor:pointer;display:grid;grid-template-columns:34px minmax(0,1fr) 18px;align-items:center;width:100%;min-height:49px;padding:0;border:0;border-bottom:1px solid #c8c5be;background:transparent;color:inherit;text-align:left} +#beauticode-console-pop .bc-import:hover{background:rgba(32,35,39,.05)} +#beauticode-console-pop .bc-index{color:#74787b;font:10px ui-monospace,"Cascadia Mono",monospace} +#beauticode-console-pop .bc-import-copy{min-width:0} +#beauticode-console-pop .bc-import strong{display:block;font-size:13px;line-height:17px;font-weight:650} +#beauticode-console-pop .bc-import small{display:block;color:#717579;font-size:10px;line-height:14px} +#beauticode-console-pop .bc-arrow{font-size:16px;text-align:right} +#beauticode-console-pop .bc-controls{display:flex;align-items:center;gap:12px;padding:9px 0;border-bottom:1px solid #c8c5be} +#beauticode-console-pop .bc-link{cursor:pointer;height:auto;padding:0;border:0;background:transparent;color:#595e62;font-size:11px;line-height:18px;text-decoration:underline;text-underline-offset:3px} +#beauticode-console-pop .bc-link:hover{color:#171a1d} +#beauticode-console-pop .bc-theme-toggle{cursor:pointer;display:flex;align-items:center;justify-content:space-between;width:100%;height:31px;padding:8px 0 4px;border:0;background:transparent;color:#686d71;font:10px ui-monospace,"Cascadia Mono",monospace;letter-spacing:.08em;text-align:left} +#beauticode-console-pop .bc-theme-list{max-height:150px;overflow:auto;scrollbar-width:thin} +#beauticode-console-pop .bc-theme-row{display:flex;align-items:center;min-width:0;border-bottom:1px dotted #bbb8b1} +#beauticode-console-pop .bc-theme-row:last-child{border-bottom:0} +#beauticode-console-pop .bc-theme-item{cursor:pointer;display:flex;align-items:center;flex:1;min-width:0;height:31px;padding:0;border:0;background:transparent;color:inherit;font-size:12px;line-height:31px;text-align:left;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +#beauticode-console-pop .bc-theme-item::before{content:"";flex:none;width:16px;font-size:9px} +#beauticode-console-pop .bc-theme-item[aria-current="true"]{font-weight:650} +#beauticode-console-pop .bc-theme-item[aria-current="true"]::before{content:"●"} +#beauticode-console-pop .bc-theme-item:hover{background:rgba(32,35,39,.05)} +#beauticode-console-pop .bc-source{margin-left:auto;padding-left:6px;color:#777b7e;font:9px ui-monospace,"Cascadia Mono",monospace} +#beauticode-console-pop .bc-theme-del{cursor:pointer;flex:none;width:22px;height:28px;padding:0;border:0;background:transparent;color:#777b7e;font-size:15px;line-height:28px;opacity:.66} +#beauticode-console-pop .bc-theme-del:hover{color:#802f2f;opacity:1} +#beauticode-console-pop .bc-btn:disabled,#beauticode-console-pop .bc-theme-toggle:disabled,#beauticode-console-pop .bc-theme-item:disabled,#beauticode-console-pop .bc-theme-del:disabled{opacity:.38;cursor:default} +#beauticode-console-pop[data-busy="true"] .bc-head::before{content:"";width:5px;height:5px;margin:0 0 6px;background:#6d7f8c;animation:bc-pulse .9s steps(2,end) infinite} +#beauticode-console-pop .bc-msg{margin:9px 0 0;color:#656a6e;font-size:11px;line-height:16px;max-height:3.2em;overflow:hidden} +@keyframes bc-pulse{50%{opacity:.25}} #beauticode-console-file{display:none !important} +#beauticode-name-dialog{position:fixed;inset:0;z-index:3000;display:grid;place-items:center;padding:24px;background:rgba(0,0,0,.42);font:inherit} +#beauticode-name-dialog .bc-name-card{display:flex;flex-direction:column;gap:10px;width:min(380px,calc(100vw - 48px));padding:20px;border:1px solid #aaa69e;border-top:4px solid #252a30;border-radius:2px;background:#ece9e2;color:#202327;box-shadow:0 18px 60px rgba(0,0,0,.35)} +#beauticode-name-dialog .bc-name-title{margin:0;font:650 17px Georgia,"Songti SC","STSong",serif} +#beauticode-name-dialog .bc-name-file,#beauticode-name-dialog .bc-name-note,#beauticode-name-dialog .bc-name-error{margin:0;color:#666b6f;font-size:12px;line-height:18px;overflow-wrap:anywhere} +#beauticode-name-dialog .bc-name-error{color:#8d3030} +#beauticode-name-dialog input{height:38px;padding:0 10px;border:1px solid #a8a49c;border-radius:0;background:#f7f4ed;color:inherit;font:inherit;font-size:13px} +#beauticode-name-dialog .bc-name-actions{display:flex;justify-content:flex-end;gap:8px} +#beauticode-name-dialog button{cursor:pointer;height:34px;padding:0 13px;border:1px solid #8d8a84;border-radius:0;background:transparent;color:inherit;font:inherit} +#beauticode-name-dialog button[data-name="confirm"]{background:#252a30;border-color:#252a30;color:#f7f4ed} `; document.head.append(style); @@ -55,17 +76,16 @@ body:not([data-ds-dark-theme]) #beauticode-console-pop .bc-theme-item:hover{back pop.id = "beauticode-console-pop"; pop.hidden = true; pop.innerHTML = - '

未就绪

' + - '
' + - '' + - '' + - "
" + - '
' + - '' + - '' + + '

背景清单

未就绪
' + + '' + + '' + + '
' + + '' + + '' + + '' + "
" + '" + ''; @@ -86,6 +106,7 @@ body:not([data-ds-dark-theme]) #beauticode-console-pop .bc-theme-item:hover{back let busy = false; let muted = true; let currentThemeId = ""; + let themesExpanded = true; function findSettingsTrigger() { const buttons = [...document.querySelectorAll('button[aria-haspopup="dialog"]')]; @@ -150,12 +171,27 @@ body:not([data-ds-dark-theme]) #beauticode-console-pop .bc-theme-item:hover{back : data.media === "image" ? "图片" : "无背景"; - statusEl.textContent = label; - if (data.atmosphere === "gallery") currentThemeId = "builtin-gallery"; + const sourceLabel = + data.sourceMode === "local" + ? "本地引用" + : data.sourceMode === "managed" + ? "托管副本" + : ""; + if (typeof data.themeId === "string" && data.themeId) { + currentThemeId = data.themeId; + } else if (data.atmosphere === "gallery") { + currentThemeId = "builtin-gallery"; + } else { + currentThemeId = ""; + } muted = data.muted !== false; soundBtn.classList.toggle("on", !muted); - soundBtn.textContent = muted ? "声音" : "声音开"; + soundBtn.textContent = muted ? "声音已关" : "声音已开"; const themes = Array.isArray(data.themes) ? data.themes : []; + const selected = themes.find((theme) => theme.id === currentThemeId); + const currentLabel = selected?.name || label; + const compactSource = sourceLabel === "本地引用" ? "本地" : sourceLabel === "托管副本" ? "托管" : "已应用"; + statusEl.textContent = `${currentLabel} / ${compactSource}`; if (themes.length === 0) { themesBox.hidden = true; themeList.innerHTML = ""; @@ -165,13 +201,20 @@ body:not([data-ds-dark-theme]) #beauticode-console-pop .bc-theme-item:hover{back } themesBox.hidden = false; themeList.innerHTML = themes - .map( - (theme) => - ``, - ) + .map((theme) => { + const del = + theme.bundled === true + ? "" + : ``; + const source = + theme.sourceMode === "local" ? "本地" : theme.bundled ? "内置" : "托管"; + const current = theme.id === currentThemeId ? ' aria-current="true"' : ""; + return `
${del}
`; + }) .join(""); - const selected = themes.find((theme) => theme.id === currentThemeId); - themeToggle.textContent = selected ? selected.name : "已保存主题"; + themeToggle.innerHTML = `SAVED / ${String(themes.length).padStart(2, "0")}${themesExpanded ? "−" : "+"}`; + themeList.hidden = !themesExpanded; + themeToggle.setAttribute("aria-expanded", themesExpanded ? "true" : "false"); } function escapeText(value) { @@ -185,18 +228,36 @@ body:not([data-ds-dark-theme]) #beauticode-console-pop .bc-theme-item:hover{back return escapeText(value).replaceAll('"', """); } - async function request(path, init) { - const response = await fetch(path, { - ...init, - headers: { - ...(init?.headers || {}), - }, - }); - const body = await response.json().catch(() => null); - if (!response.ok || body?.ok === false) { - throw new Error(body?.error || `请求失败(${response.status})`); + async function request(path, init, options = {}) { + const timeoutMs = options.timeoutMs === 0 ? 0 : options.timeoutMs || 45_000; + const controller = timeoutMs > 0 ? new AbortController() : null; + const timer = controller + ? setTimeout(() => controller.abort(new Error("background_request_timeout")), timeoutMs) + : null; + try { + const response = await fetch(path, { + ...init, + ...(controller ? { signal: controller.signal } : {}), + headers: { + ...(init?.headers || {}), + }, + }); + const body = await response.json().catch(() => null); + if (!response.ok || body?.ok === false) { + const error = new Error(body?.error || `请求失败(${response.status})`); + error.status = response.status; + error.code = body?.code || ""; + throw error; + } + return body; + } catch (error) { + if (controller?.signal.aborted) { + throw new Error("背景操作超时,控件已恢复。原背景保持不变,请重试。"); + } + throw error; + } finally { + if (timer) clearTimeout(timer); } - return body; } async function refresh() { @@ -210,18 +271,146 @@ body:not([data-ds-dark-theme]) #beauticode-console-pop .bc-theme-item:hover{back async function run(task) { if (busy) return; busy = true; - for (const button of pop.querySelectorAll(".bc-btn, .bc-theme-toggle, .bc-theme-item")) button.disabled = true; - showMessage(""); + pop.dataset.busy = "true"; + let afterRun = null; + for (const button of pop.querySelectorAll(".bc-btn, .bc-theme-toggle, .bc-theme-item, .bc-theme-del")) button.disabled = true; + showMessage("正在处理,请稍候…"); try { const result = await task(); - if (result?.message) showMessage(result.message); + if (typeof result?.afterRun === "function") afterRun = result.afterRun; + if (result?.theme?.id) currentThemeId = result.theme.id; + if (result?.message) { + const source = + result.sourceMode === "local" + ? "本地引用,未复制主媒体" + : result.sourceMode === "managed" + ? "托管副本" + : ""; + const totalMs = Number(result.importTimings?.applyAndSaveMs ?? result.timings?.totalMs); + const duration = Number.isFinite(totalMs) ? `${Math.round(totalMs)} ms` : ""; + showMessage([result.message, source, duration].filter(Boolean).join(" · ")); + } else { + showMessage(""); + } await refresh(); } catch (error) { showMessage(error instanceof Error ? error.message : String(error)); } finally { busy = false; - for (const button of pop.querySelectorAll(".bc-btn, .bc-theme-toggle, .bc-theme-item")) button.disabled = false; + delete pop.dataset.busy; + for (const button of pop.querySelectorAll(".bc-btn, .bc-theme-toggle, .bc-theme-item, .bc-theme-del")) button.disabled = false; + } + if (afterRun) queueMicrotask(afterRun); + } + + function validateThemeName(value) { + const name = String(value || "").trim(); + if (!name) return "主题名不能为空。"; + if (name.length > 80) return "主题名不能超过 80 个字符。"; + if (/[<>:"/\\|?*]/.test(name) || /[\u0000-\u001f]/.test(name)) { + return '主题名不能包含 < > : " / \\ | ? * 或控制字符。'; + } + return ""; + } + + function defaultThemeName(fileName) { + const suggested = String(fileName || "") + .replace(/\.[^.]+$/, "") + .trim() + .slice(0, 80); + return suggested || "新主题"; + } + + function askThemeName(fileName, suggestedName, options = {}) { + return new Promise((resolve) => { + const dialog = document.createElement("div"); + dialog.id = "beauticode-name-dialog"; + dialog.innerHTML = + '"; + document.body.append(dialog); + const input = dialog.querySelector('input[aria-label="主题名称"]'); + const errorEl = dialog.querySelector(".bc-name-error"); + let closed = false; + + const close = (value) => { + if (closed) return; + closed = true; + dialog.remove(); + resolve(value); + }; + const confirm = () => { + const error = validateThemeName(input.value); + if (error) { + errorEl.hidden = false; + errorEl.textContent = error; + input.focus(); + return; + } + close(input.value.trim()); + }; + dialog.querySelector('[data-name="confirm"]').addEventListener("click", confirm); + dialog.querySelector('[data-name="cancel"]').addEventListener("click", () => close(null)); + input.addEventListener("input", () => { + errorEl.hidden = true; + errorEl.textContent = ""; + }); + input.addEventListener("keydown", (event) => { + if (event.key === "Enter") confirm(); + if (event.key === "Escape") close(null); + }); + dialog.addEventListener("click", (event) => { + if (event.target === dialog) close(null); + }); + input.focus(); + input.select(); + }); + } + + async function pickAndImport(kind) { + let picked; + try { + picked = await request( + "/__beauticode/ui/pick", + { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ kind }), + }, + // The user controls how long the native dialog stays open. Import and + // theme switching still use the bounded request timeout above. + { timeoutMs: 0 }, + ); + } catch (error) { + if (error?.code !== "native_picker_unavailable") throw error; + return { + ok: true, + afterRun: () => { + fileInput.accept = kind === "video" ? VIDEO_ACCEPT : IMAGE_ACCEPT; + fileInput.dataset.compatibilityUpload = "true"; + fileInput.click(); + }, + }; } + if (picked.cancelled) return { ok: true }; + const themeName = await askThemeName( + picked.name, + picked.suggestedThemeName, + ); + if (!themeName) return { ok: true }; + return request("/__beauticode/ui/import-selected", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ selectionId: picked.selectionId, themeName }), + }); } trigger.addEventListener("click", (event) => { @@ -229,16 +418,26 @@ body:not([data-ds-dark-theme]) #beauticode-console-pop .bc-theme-item:hover{back setOpen(pop.hidden); }); pop.querySelector('[data-act="image"]').addEventListener("click", () => { - fileInput.accept = IMAGE_ACCEPT; - fileInput.click(); + void run(() => pickAndImport("image")); }); pop.querySelector('[data-act="video"]').addEventListener("click", () => { - fileInput.accept = VIDEO_ACCEPT; - fileInput.click(); + void run(() => pickAndImport("video")); + }); + pop.querySelector('[data-act="gallery"]').addEventListener("click", (event) => { + event.stopPropagation(); + setOpen(false); + if (window.BeauticodeGallery) { + void window.BeauticodeGallery.open(); + return; + } + showMessage("皮肤中心脚本尚未加载。"); }); pop.querySelector('[data-act="clear"]').addEventListener("click", () => { - currentThemeId = ""; - void run(() => request("/__beauticode/ui/clear", { method: "POST" })); + void run(async () => { + const result = await request("/__beauticode/ui/clear", { method: "POST" }); + currentThemeId = ""; + return result; + }); }); soundBtn.addEventListener("click", () => { void run(() => @@ -251,39 +450,68 @@ body:not([data-ds-dark-theme]) #beauticode-console-pop .bc-theme-item:hover{back }); themeToggle.addEventListener("click", (event) => { event.stopPropagation(); - const open = themeList.hidden; - themeList.hidden = !open; - themeToggle.setAttribute("aria-expanded", open ? "true" : "false"); + themesExpanded = !themesExpanded; + themeList.hidden = !themesExpanded; + themeToggle.querySelector("span:last-child").textContent = themesExpanded ? "−" : "+"; + themeToggle.setAttribute("aria-expanded", themesExpanded ? "true" : "false"); }); themeList.addEventListener("click", (event) => { + const del = event.target.closest("[data-theme-delete]"); + if (del) { + event.stopPropagation(); + const id = del.getAttribute("data-theme-delete") || ""; + const name = del.getAttribute("data-theme-name") || "主题"; + if (!id) return; + if (!window.confirm(`确定删除主题「${name}」?`)) return; + void run(async () => { + const result = await request("/__beauticode/ui/theme/delete", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ id }), + }); + if (currentThemeId === id) currentThemeId = ""; + return result; + }); + return; + } const item = event.target.closest("[data-theme-id]"); if (!item) return; - currentThemeId = item.getAttribute("data-theme-id") || ""; + const targetThemeId = item.getAttribute("data-theme-id") || ""; themeList.hidden = true; themeToggle.setAttribute("aria-expanded", "false"); - globalThis.BeauticodeAtmosphere?.setWindowMode?.( - currentThemeId === "builtin-gallery" ? "on" : "closed", - ); - void run(() => - request("/__beauticode/ui/theme/use", { + void run(async () => { + const result = await request("/__beauticode/ui/theme/use", { method: "POST", headers: { "content-type": "application/json" }, - body: JSON.stringify({ id: currentThemeId }), - }), - ); + body: JSON.stringify({ id: targetThemeId }), + }); + currentThemeId = targetThemeId; + globalThis.BeauticodeAtmosphere?.setWindowMode?.( + currentThemeId === "builtin-gallery" ? "on" : "closed", + ); + return result; + }); }); fileInput.addEventListener("change", () => { const file = fileInput.files?.[0]; + const compatibilityUpload = fileInput.dataset.compatibilityUpload === "true"; fileInput.value = ""; + delete fileInput.dataset.compatibilityUpload; if (!file) return; - currentThemeId = ""; - void run(() => - request("/__beauticode/ui/import", { + void run(async () => { + const themeName = await askThemeName(file.name, defaultThemeName(file.name), { + compatibilityUpload, + }); + if (!themeName) return { ok: true }; + return request("/__beauticode/ui/import", { method: "POST", - headers: { "x-beauticode-filename": encodeURIComponent(file.name) }, + headers: { + "x-beauticode-filename": encodeURIComponent(file.name), + "x-beauticode-theme-name": encodeURIComponent(themeName), + }, body: file, - }), - ); + }); + }); }); document.addEventListener("click", (event) => { @@ -294,6 +522,9 @@ body:not([data-ds-dark-theme]) #beauticode-console-pop .bc-theme-item:hover{back document.addEventListener("keydown", (event) => { if (event.key === "Escape" && !pop.hidden) setOpen(false); }); + document.addEventListener("beauticode-gallery-installed", () => { + void refresh(); + }); const observer = new MutationObserver(() => place()); observer.observe(document.documentElement, { childList: true, subtree: true }); diff --git a/integrations/deepseek-harness/control-client.mjs b/integrations/deepseek-harness/control-client.mjs index 1eae13c..8d410eb 100644 --- a/integrations/deepseek-harness/control-client.mjs +++ b/integrations/deepseek-harness/control-client.mjs @@ -90,6 +90,47 @@ export function isPidAlive(pid) { } } +let livenessModulePromise; + +function loadCoreLiveness() { + if (!livenessModulePromise) { + livenessModulePromise = (async () => { + const candidates = [ + "@beauticode/core", + new URL("./vendor/core/index.js", import.meta.url).href, + new URL("../../packages/core/dist/index.js", import.meta.url).href, + ]; + for (const specifier of candidates) { + try { + const mod = await import(specifier); + if (typeof mod.isRecordedPidLive === "function") return mod; + } catch { + /* try the next resolution path */ + } + } + return null; + })(); + } + return livenessModulePromise; +} + +async function isLiveRecordedPid(pid, startedAt, mtimeMs) { + if (!isPidAlive(pid)) return false; + const recorded = + typeof startedAt === "string" && startedAt + ? startedAt + : Number.isFinite(mtimeMs) + ? new Date(mtimeMs).toISOString() + : null; + try { + const core = await loadCoreLiveness(); + if (core) return await core.isRecordedPidLive(pid, recorded); + } catch { + /* fall back to the cheap PID check */ + } + return true; +} + export function stripPathQuotes(value) { const text = String(value ?? "").trim(); if (text.length >= 2) { @@ -195,6 +236,7 @@ export async function writeDshControlFile(opts) { pid, url: new URL(url).origin, token, + startedAt: new Date().toISOString(), }); } @@ -219,6 +261,7 @@ export async function writeSessionHostFile(opts) { pid, url: new URL(url).origin, token, + startedAt: new Date().toISOString(), }); } @@ -253,8 +296,11 @@ export async function removeDshControlFile(opts) { export async function readDshControlFile(dataRoot, opts = {}) { const file = controlFilePath(dataRoot); let raw; + let mtimeMs = 0; try { - raw = await fs.readFile(file, "utf8"); + const [text, stat] = await Promise.all([fs.readFile(file, "utf8"), fs.stat(file)]); + raw = text; + mtimeMs = stat.mtimeMs; } catch (error) { if (error && typeof error === "object" && error.code === "ENOENT") return null; throw error; @@ -277,13 +323,19 @@ export async function readDshControlFile(dataRoot, opts = {}) { ) { return null; } - if (!opts.allowDead && !isPidAlive(parsed.pid)) return null; + if ( + !opts.allowDead && + !(await isLiveRecordedPid(parsed.pid, parsed.startedAt, mtimeMs)) + ) { + return null; + } return { schema: CONTROL_SCHEMA, host: "dsh", pid: parsed.pid, url: new URL(parsed.url).origin, token: parsed.token, + startedAt: typeof parsed.startedAt === "string" ? parsed.startedAt : null, }; } @@ -305,8 +357,11 @@ export async function removeSessionHostFile(opts) { export async function readSessionHostFile(dataRoot, opts = {}) { const file = sessionHostFilePath(dataRoot); let raw; + let mtimeMs = 0; try { - raw = await fs.readFile(file, "utf8"); + const [text, stat] = await Promise.all([fs.readFile(file, "utf8"), fs.stat(file)]); + raw = text; + mtimeMs = stat.mtimeMs; } catch (error) { if (error && typeof error === "object" && error.code === "ENOENT") return null; throw error; @@ -329,21 +384,30 @@ export async function readSessionHostFile(dataRoot, opts = {}) { ) { return null; } - if (!opts.allowDead && !isPidAlive(parsed.pid)) return null; + if ( + !opts.allowDead && + !(await isLiveRecordedPid(parsed.pid, parsed.startedAt, mtimeMs)) + ) { + return null; + } return { schema: SESSION_HOST_SCHEMA, host: parsed.host, pid: parsed.pid, url: new URL(parsed.url).origin, token: parsed.token, + startedAt: typeof parsed.startedAt === "string" ? parsed.startedAt : null, }; } export async function readTrayClaim(dataRoot, opts = {}) { const file = trayClaimFilePath(dataRoot); let raw; + let mtimeMs = 0; try { - raw = await fs.readFile(file, "utf8"); + const [text, stat] = await Promise.all([fs.readFile(file, "utf8"), fs.stat(file)]); + raw = text; + mtimeMs = stat.mtimeMs; } catch (error) { if (error && typeof error === "object" && error.code === "ENOENT") return null; throw error; @@ -362,7 +426,12 @@ export async function readTrayClaim(dataRoot, opts = {}) { ) { return null; } - if (!opts.allowDead && !isPidAlive(parsed.pid)) return null; + if ( + !opts.allowDead && + !(await isLiveRecordedPid(parsed.pid, parsed.startedAt, mtimeMs)) + ) { + return null; + } return { schema: TRAY_CLAIM_SCHEMA, pid: parsed.pid, @@ -439,6 +508,8 @@ export async function callDshControl(dataRoot, spec) { const error = new Error(message); error.statusCode = response.status; error.payload = payload; + if (payload?.sourceMode != null) error.sourceMode = payload.sourceMode; + if (payload?.timings != null) error.timings = payload.timings; throw error; } return payload; diff --git a/integrations/deepseek-harness/cordis.patch.yml b/integrations/deepseek-harness/cordis.patch.yml index 4b0da46..4c5c12a 100644 --- a/integrations/deepseek-harness/cordis.patch.yml +++ b/integrations/deepseek-harness/cordis.patch.yml @@ -1,4 +1,4 @@ - insert: - id: beauticode-bridge - name: '@beauticode/dsh-plugin' + name: beauticode-dsh inject: [webServer] diff --git a/integrations/deepseek-harness/gallery-host.mjs b/integrations/deepseek-harness/gallery-host.mjs new file mode 100644 index 0000000..cd3f1a8 --- /dev/null +++ b/integrations/deepseek-harness/gallery-host.mjs @@ -0,0 +1,253 @@ +import fs from "node:fs"; +import fsp from "node:fs/promises"; +import path from "node:path"; +import crypto from "node:crypto"; +import { Readable } from "node:stream"; +import { Transform } from "node:stream"; +import { pipeline } from "node:stream/promises"; +import { fileURLToPath } from "node:url"; + +const here = path.dirname(fileURLToPath(import.meta.url)); +const SKIN_ID = /^skin-[a-z0-9]{8,40}$/; +const MAX_IMAGE_BYTES = 18 * 1024 * 1024; +const MAX_VIDEO_BYTES = 800 * 1024 * 1024; +const INSTALL_TIMEOUT_MS = 30 * 60 * 1000; +const LOOPBACK = new Set(["127.0.0.1", "localhost", "::1"]); + +export function isSafeSkinId(id) { + return typeof id === "string" && SKIN_ID.test(id); +} + +export function normalizeSkinCenterUrl(value) { + if (typeof value !== "string") return null; + const trimmed = value.trim(); + if (!trimmed) return null; + try { + const url = new URL(trimmed); + if (url.username || url.password || url.hash) return null; + if (url.protocol !== "https:" && url.protocol !== "http:") return null; + if (url.protocol === "http:" && !LOOPBACK.has(url.hostname.toLowerCase())) return null; + const pathname = url.pathname === "/" ? "" : url.pathname.replace(/\/+$/, ""); + return `${url.origin}${pathname}`; + } catch { + return null; + } +} + +export async function readBundledSkinCenterUrl() { + try { + const raw = JSON.parse(await fsp.readFile(path.join(here, "skin-center.json"), "utf8")); + return normalizeSkinCenterUrl(raw.url); + } catch { + return null; + } +} + +export async function resolveConfiguredSkinCenterUrl() { + return ( + normalizeSkinCenterUrl(process.env.BEAUTICODE_SKIN_CENTER) ?? + (await readBundledSkinCenterUrl()) + ); +} + +export function skinUrl(center, id, part = "") { + const origin = normalizeSkinCenterUrl(center); + if (!origin || !isSafeSkinId(id)) { + throw new Error("Skin center is not configured."); + } + return part ? `${origin}/api/skins/${id}/${part}` : `${origin}/api/skins/${id}`; +} + +export async function downloadToFile(url, dest, { maxBytes, expectedOrigin, onProgress } = {}) { + const expected = new URL(url); + if (expectedOrigin && expected.origin !== expectedOrigin) { + throw new Error("Skin media download host mismatch."); + } + const response = await fetch(url, { redirect: "follow" }); + if (!response.ok || !response.body) { + throw new Error("Skin media download failed."); + } + const finalUrl = new URL(response.url); + if (finalUrl.origin !== expected.origin) { + throw new Error("Skin media download host mismatch."); + } + const length = Number(response.headers.get("content-length")); + if (Number.isFinite(length) && length > maxBytes) { + throw new Error("Skin media download exceeded the size limit."); + } + await fsp.mkdir(path.dirname(dest), { recursive: true }); + let size = 0; + const limiter = new Transform({ + transform(chunk, _enc, callback) { + size += chunk.length; + if (size > maxBytes) { + callback(new Error("Skin media download exceeded the size limit.")); + return; + } + onProgress?.(size, Number.isFinite(length) ? length : 0); + callback(null, chunk); + }, + }); + await pipeline(Readable.fromWeb(response.body), limiter, fs.createWriteStream(dest)); + return { bytes: size }; +} + +function extensionOf(url, fallback) { + try { + const ext = path.extname(new URL(url).pathname).toLowerCase(); + if (ext) return ext; + } catch { + /* use fallback */ + } + return fallback; +} + +export function createGalleryHandlers({ dataRoot, actions }) { + async function importTheme(input) { + if (typeof actions.importTheme === "function") { + return actions.importTheme(input); + } + throw new Error("当前引擎不支持导入皮肤。"); + } + + return { + async config(req, res, sendJson, isSameOrigin) { + if (req.method !== "GET") { + res.writeHead(405).end(); + return; + } + if (!isSameOrigin(req)) { + res.writeHead(403).end(); + return; + } + const url = await resolveConfiguredSkinCenterUrl(); + sendJson(res, 200, { ok: true, url, enabled: Boolean(url) }); + }, + + async catalog(req, res, sendJson, isSameOrigin) { + if (req.method !== "GET") { + res.writeHead(405).end(); + return; + } + if (!isSameOrigin(req)) { + res.writeHead(403).end(); + return; + } + const center = await resolveConfiguredSkinCenterUrl(); + if (!center) { + sendJson(res, 200, { + ok: false, + error: "尚未配置皮肤中心地址。", + skins: [], + }); + return; + } + const incoming = new URL(req.url || "/", "http://127.0.0.1"); + const target = new URL("/api/catalog", `${center}/`); + target.search = incoming.search; + const response = await fetch(target, { headers: { accept: "application/json" } }); + const body = await response.json().catch(() => null); + if (!response.ok || !body || body.ok === false) { + sendJson(res, 422, { + ok: false, + error: body?.error || "无法读取皮肤目录。", + skins: [], + }); + return; + } + sendJson(res, 200, { + ok: true, + skins: Array.isArray(body.skins) ? body.skins : [], + nextCursor: body.nextCursor ?? null, + url: center, + }); + }, + + async install(req, res, sendJson, isSameOrigin, readJson) { + if (req.method !== "POST") { + res.writeHead(405).end(); + return; + } + if (!isSameOrigin(req)) { + res.writeHead(403).end(); + return; + } + const body = await readJson(req); + const id = String(body.id ?? "").trim(); + if (!isSafeSkinId(id)) { + sendJson(res, 400, { ok: false, error: "皮肤 ID 无效。" }); + return; + } + const center = await resolveConfiguredSkinCenterUrl(); + if (!center) { + sendJson(res, 422, { ok: false, error: "尚未配置皮肤中心地址。" }); + return; + } + const origin = new URL(center).origin; + res.writeHead(200, { + "content-type": "application/x-ndjson; charset=utf-8", + "cache-control": "no-store", + }); + const write = (payload) => { + if (!res.writableEnded) res.write(`${JSON.stringify(payload)}\n`); + }; + const tmpDir = path.join(dataRoot, "tmp", "gallery", `${Date.now()}-${crypto.randomBytes(4).toString("hex")}`); + try { + write({ phase: "fetch" }); + const metaRes = await fetch(skinUrl(center, id), { headers: { accept: "application/json" } }); + const meta = await metaRes.json().catch(() => null); + const skin = meta?.skin; + if (!metaRes.ok || !skin || skin.status && skin.status !== "approved") { + throw new Error("Skin is not available for download."); + } + await fsp.mkdir(tmpDir, { recursive: true }); + const imageUrl = skinUrl(center, id, "image"); + const imagePath = path.join(tmpDir, `image${extensionOf(imageUrl, ".png")}`); + write({ phase: "download", part: "image" }); + await downloadToFile(imageUrl, imagePath, { + maxBytes: MAX_IMAGE_BYTES, + expectedOrigin: origin, + onProgress: (done, total) => write({ phase: "download", part: "image", done, total }), + }); + let videoPath; + if (skin.type === "video") { + const videoUrl = skinUrl(center, id, "video"); + videoPath = path.join(tmpDir, "background.mp4"); + write({ phase: "download", part: "video" }); + await downloadToFile(videoUrl, videoPath, { + maxBytes: MAX_VIDEO_BYTES, + expectedOrigin: origin, + onProgress: (done, total) => write({ phase: "download", part: "video", done, total }), + }); + } + write({ phase: "import" }); + const imported = await importTheme({ + name: String(skin.name || id).slice(0, 80), + imagePath, + ...(videoPath ? { videoPath } : {}), + ...(skin.effects ? { effects: skin.effects } : {}), + source: { kind: "skin-center", skinId: id, centerUrl: center }, + }); + write({ phase: "apply" }); + const applied = await actions.useTheme(imported.theme?.id || imported.id, undefined); + fetch(skinUrl(center, id, "download"), { method: "POST" }).catch(() => {}); + write({ + ok: true, + phase: "done", + theme: imported.theme || imported, + message: applied.message || `已安装并应用「${skin.name}」。`, + }); + } catch (error) { + write({ + ok: false, + error: error instanceof Error ? error.message : String(error), + }); + } finally { + await fsp.rm(tmpDir, { recursive: true, force: true }).catch(() => {}); + res.end(); + } + }, + }; +} + +export const GALLERY_INSTALL_TIMEOUT_MS = INSTALL_TIMEOUT_MS; diff --git a/integrations/deepseek-harness/gallery.js b/integrations/deepseek-harness/gallery.js new file mode 100644 index 0000000..64ac3b9 --- /dev/null +++ b/integrations/deepseek-harness/gallery.js @@ -0,0 +1,178 @@ +(() => { + "use strict"; + if (window.__beauticodeGalleryLoaded) return; + window.__beauticodeGalleryLoaded = true; + + const style = document.createElement("style"); + style.textContent = ` +#beauticode-gallery{position:fixed;inset:0;z-index:3000;display:flex;align-items:center;justify-content:center;background:rgba(11,13,18,.62)} +#beauticode-gallery[hidden]{display:none} +#beauticode-gallery .bcg-panel{width:min(880px,calc(100vw - 32px));height:min(640px,calc(100vh - 32px));display:flex;flex-direction:column;border:1px solid rgba(255,255,255,.08);border-radius:18px;background:#2c323c;color:#e8eaed;box-shadow:0 16px 48px rgba(0,0,0,.4);overflow:hidden} +body:not([data-ds-dark-theme]) #beauticode-gallery .bcg-panel{background:#fff;color:#1b1f24;border-color:rgba(0,0,0,.08)} +#beauticode-gallery .bcg-head{display:flex;align-items:center;gap:10px;padding:12px 14px;border-bottom:1px solid rgba(255,255,255,.08)} +#beauticode-gallery .bcg-head h2{margin:0;font-size:15px;font-weight:600} +#beauticode-gallery .bcg-head input,#beauticode-gallery .bcg-head select{height:32px;border:1px solid rgba(255,255,255,.1);border-radius:10px;background:rgba(255,255,255,.06);color:inherit;padding:0 8px} +#beauticode-gallery .bcg-grid{flex:1;overflow:auto;padding:12px;display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:10px;align-content:start} +#beauticode-gallery .bcg-card{display:block;border:none;padding:0;border-radius:12px;overflow:hidden;background:#232830;color:inherit;text-align:left;cursor:pointer} +#beauticode-gallery .bcg-card img{width:100%;aspect-ratio:16/10;object-fit:cover;display:block;background:#111} +#beauticode-gallery .bcg-card span{display:block;padding:8px 10px;font-size:13px} +#beauticode-gallery .bcg-msg,#beauticode-gallery .bcg-foot{padding:0 14px 12px;color:#9aa3ad;font-size:12px} +#beauticode-gallery .bcg-close{margin-left:auto} +#beauticode-gallery .bcg-btn{height:32px;padding:0 10px;border:1px solid rgba(255,255,255,.1);border-radius:10px;background:rgba(255,255,255,.06);color:inherit;cursor:pointer} +#beauticode-gallery .bcg-btn.primary{background:#4d6bfe;border-color:transparent} + `; + document.head.append(style); + + const host = document.createElement("div"); + host.id = "beauticode-gallery"; + host.hidden = true; + host.innerHTML = + '"; + document.body.append(host); + + const grid = host.querySelector(".bcg-grid"); + const msg = host.querySelector(".bcg-msg"); + const foot = host.querySelector(".bcg-foot"); + const queryInput = host.querySelector(".bcg-q"); + const typeSelect = host.querySelector(".bcg-type"); + let centerUrl = ""; + let busy = false; + + function escapeText(value) { + return String(value ?? "") + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">"); + } + + async function request(path, init) { + const response = await fetch(path, init); + if ((response.headers.get("content-type") || "").includes("ndjson")) { + return readNdjson(response); + } + const body = await response.json().catch(() => null); + if (!response.ok || body?.ok === false) { + throw new Error(body?.error || `请求失败(${response.status})`); + } + return body; + } + + async function readNdjson(response) { + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ""; + let last = null; + while (true) { + const { value, done } = await reader.read(); + buffer += decoder.decode(value || new Uint8Array(), { stream: !done }); + const lines = buffer.split("\n"); + buffer = lines.pop() || ""; + for (const line of lines) { + if (!line.trim()) continue; + last = JSON.parse(line); + if (last.phase === "download" && last.total) { + const pct = Math.round((last.done / last.total) * 100); + msg.textContent = `正在下载${last.part === "video" ? "视频" : "图片"} ${pct}%`; + } else if (last.phase === "import") { + msg.textContent = "正在写入本机主题…"; + } else if (last.phase === "apply") { + msg.textContent = "正在应用到当前窗口…"; + } + if (last.ok === false) throw new Error(last.error || "安装失败。"); + } + if (done) break; + } + if (last?.ok) return last; + throw new Error(last?.error || "安装失败。"); + } + + async function load() { + msg.textContent = "正在读取目录…"; + const params = new URLSearchParams(); + if (queryInput.value.trim()) params.set("q", queryInput.value.trim()); + if (typeSelect.value) params.set("type", typeSelect.value); + const data = await request(`/__beauticode/ui/gallery/catalog?${params}`); + centerUrl = data.url || centerUrl; + grid.innerHTML = (data.skins || []) + .map( + (skin) => + ``, + ) + .join(""); + msg.textContent = data.skins?.length ? "" : "目录是空的。"; + foot.innerHTML = centerUrl + ? `上传与审核在
皮肤中心网站。安装会下载到本机后再应用。` + : "未配置皮肤中心地址。在插件的 skin-center.json 或环境变量 BEAUTICODE_SKIN_CENTER 里填入你的域名。"; + } + + async function open() { + host.hidden = false; + const config = await request("/__beauticode/ui/gallery/config"); + centerUrl = config.url || ""; + if (!config.enabled) { + grid.innerHTML = ""; + msg.textContent = "尚未配置皮肤中心地址。"; + foot.textContent = "设置 BEAUTICODE_SKIN_CENTER,或在 skin-center.json 填写站点 URL。"; + return; + } + await load(); + } + + function close() { + host.hidden = true; + } + + host.querySelector(".bcg-close").addEventListener("click", close); + host.addEventListener("click", (event) => { + if (event.target === host) close(); + }); + queryInput.addEventListener("keydown", (event) => { + if (event.key === "Enter") { + event.preventDefault(); + void load(); + } + }); + typeSelect.addEventListener("change", () => void load()); + grid.addEventListener("click", (event) => { + const card = event.target.closest("[data-id]"); + if (!card || busy) return; + const id = card.getAttribute("data-id"); + busy = true; + msg.textContent = "开始安装…"; + request("/__beauticode/ui/gallery/install", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ id }), + }) + .then((result) => { + msg.textContent = result.message || "已安装。"; + document.dispatchEvent(new CustomEvent("beauticode-gallery-installed")); + }) + .catch((error) => { + msg.textContent = error instanceof Error ? error.message : String(error); + }) + .finally(() => { + busy = false; + }); + }); + document.addEventListener("keydown", (event) => { + if (event.key === "Escape" && !host.hidden) { + event.stopPropagation(); + close(); + } + }); + + window.BeauticodeGallery = { open, close }; +})(); diff --git a/integrations/deepseek-harness/index.mjs b/integrations/deepseek-harness/index.mjs index 2284179..6af7800 100644 --- a/integrations/deepseek-harness/index.mjs +++ b/integrations/deepseek-harness/index.mjs @@ -168,12 +168,20 @@ function publicStatus(current, modes, clients, clientStates) { const playback = activeAcks.find((ack) => ack.ok === true && ack.playback?.hasVideo === true) ?.playback ?? null; + // Only an explicit renderer error is terminal. Older clients may send a + // transient ok:false heartbeat while a large video is still warming up. + const failedAcks = activeAcks.filter( + (ack) => ack.ok === false && typeof ack.error === "string" && ack.error, + ); return { ok: true, connectedClients: clients.size, current, readyClients: activeAcks.filter((ack) => ack.ok === true).length, - failedClients: activeAcks.filter((ack) => ack.ok !== true).length, + failedClients: failedAcks.length, + lastRenderError: + failedAcks.find((ack) => typeof ack.error === "string" && ack.error)?.error ?? + null, visibleClients: activeAcks.filter((ack) => ack.ok === true && ack.visible === true).length, modeReadyClients: modeReady.length, blockedClients: modeReady.filter((ack) => ack.blocked === true).length, @@ -198,6 +206,10 @@ export function apply(ctx, config = {}) { sendJson, isSameOrigin, readJson, + pickMedia: config.pickMedia, + allowManagedUpload: config.allowManagedUpload, + now: config.now, + selectionTtlMs: config.selectionTtlMs, }); const broadcast = (payload) => { @@ -211,7 +223,8 @@ export function apply(ctx, config = {}) { const script = '' + '' + - ''; + '' + + ''; return html.includes("") ? html.replace("", `${script}`) : `${html}${script}`; @@ -312,6 +325,23 @@ export function apply(ctx, config = {}) { res.end(req.method === "HEAD" ? undefined : source); }, }), + ctx.webServer.register({ + kind: "exact", + path: "/__beauticode/gallery.js", + handler: async (req, res) => { + if (req.method !== "GET" && req.method !== "HEAD") { + res.writeHead(405).end(); + return; + } + const source = await fs.readFile(path.join(here, "gallery.js")); + res.writeHead(200, { + "content-type": "text/javascript; charset=utf-8", + "cache-control": "no-store", + "content-length": source.length, + }); + res.end(req.method === "HEAD" ? undefined : source); + }, + }), ctx.webServer.register({ kind: "exact", path: "/__beauticode/console.js", @@ -339,6 +369,16 @@ export function apply(ctx, config = {}) { path: "/__beauticode/ui/import", handler: (req, res) => ui.importFile(req, res), }), + ctx.webServer.register({ + kind: "exact", + path: "/__beauticode/ui/pick", + handler: (req, res) => ui.pickMedia(req, res), + }), + ctx.webServer.register({ + kind: "exact", + path: "/__beauticode/ui/import-selected", + handler: (req, res) => ui.importSelected(req, res), + }), ctx.webServer.register({ kind: "exact", path: "/__beauticode/ui/clear", @@ -354,11 +394,31 @@ export function apply(ctx, config = {}) { path: "/__beauticode/ui/theme/use", handler: (req, res) => ui.useTheme(req, res), }), + ctx.webServer.register({ + kind: "exact", + path: "/__beauticode/ui/theme/delete", + handler: (req, res) => ui.deleteTheme(req, res), + }), ctx.webServer.register({ kind: "exact", path: "/__beauticode/ui/preset", handler: (req, res) => ui.applyPreset(req, res), }), + ctx.webServer.register({ + kind: "exact", + path: "/__beauticode/ui/gallery/config", + handler: (req, res) => ui.galleryConfig(req, res), + }), + ctx.webServer.register({ + kind: "exact", + path: "/__beauticode/ui/gallery/catalog", + handler: (req, res) => ui.galleryCatalog(req, res), + }), + ctx.webServer.register({ + kind: "exact", + path: "/__beauticode/ui/gallery/install", + handler: (req, res) => ui.galleryInstall(req, res), + }), ctx.webServer.register({ kind: "exact", path: "/__beauticode/events", diff --git a/integrations/deepseek-harness/package.json b/integrations/deepseek-harness/package.json index 403ca8d..d7f18e9 100644 --- a/integrations/deepseek-harness/package.json +++ b/integrations/deepseek-harness/package.json @@ -1,6 +1,6 @@ { - "name": "@beauticode/dsh-plugin", - "version": "1.0.0", + "name": "beauticode-dsh", + "version": "1.0.14", "description": "Cordis plugin: image/video backgrounds for DeepSeek Harness web.", "type": "module", "main": "./index.mjs", @@ -16,6 +16,9 @@ "index.mjs", "client.js", "console.js", + "gallery.js", + "gallery-host.mjs", + "skin-center.json", "atmosphere.js", "presets.mjs", "agent.mjs", diff --git a/integrations/deepseek-harness/skin-center.json b/integrations/deepseek-harness/skin-center.json new file mode 100644 index 0000000..c7b4bdb --- /dev/null +++ b/integrations/deepseek-harness/skin-center.json @@ -0,0 +1,3 @@ +{ + "url": "https://hnnulwh.cn" +} diff --git a/integrations/deepseek-harness/test/agent.test.mjs b/integrations/deepseek-harness/test/agent.test.mjs index 165fe1b..28d4c97 100644 --- a/integrations/deepseek-harness/test/agent.test.mjs +++ b/integrations/deepseek-harness/test/agent.test.mjs @@ -205,6 +205,20 @@ test("tools and slash commands reuse the tray apply routes", async (t) => { json(res, 200, { ok: true, generation: 5, mode: "video" }); return; } + if (url === "/theme/apply") { + const type = body.input.type; + json(res, 200, { + ok: true, + generation: type === "video" ? 5 : 4, + mode: type, + theme: { + id: `theme-${type}`, + name: body.name, + type, + }, + }); + return; + } if (url === "/apply/clear") { json(res, 200, { ok: true, generation: 6, mode: "clear" }); return; @@ -242,16 +256,20 @@ test("tools and slash commands reuse the tray apply routes", async (t) => { await writeControl(root, tray.url); const actions = createBeauticodeActions(root); - assert.equal((await actions.applyImage(image)).message, "已将图片设为背景。"); + assert.equal((await actions.applyImage(image)).message, "已将「wall」设为背景。"); assert.equal((await actions.applyVideo({ path: video, startAt: 12 })).ok, true); assert.equal((await actions.useTheme("雨夜")).theme.id, "theme-rain"); assert.equal((await actions.setFish(true)).fish, true); assert.match(await runBgCommand(root, ""), /背景:视频/); - assert.equal(await runBgCommand(root, `"${image}"`), "已将图片设为背景。"); + assert.equal(await runBgCommand(root, `"${image}"`), "已将「wall」设为背景。"); - const videoApply = tray.received.find((item) => item.url === "/apply/video"); - assert.equal(videoApply.body.videoPath, video); - assert.equal(videoApply.body.startAt, 12); + const videoApply = tray.received.find( + (item) => item.url === "/theme/apply" && item.body.input.type === "video", + ); + assert.equal(videoApply.body.name, "bg"); + assert.equal(videoApply.body.input.videoPath, video); + assert.equal(videoApply.body.input.startAt, 12); + assert.equal(videoApply.body.input.source, "local"); const themeUse = tray.received.find((item) => item.url === "/theme/use"); assert.equal(themeUse.body.id, "theme-rain"); @@ -261,6 +279,43 @@ test("tools and slash commands reuse the tray apply routes", async (t) => { ); }); +test("failed apply preserves source mode and phase timings for diagnostics", async (t) => { + const root = await createTempRoot(); + const image = path.join(root, "wall.png"); + await fs.writeFile(image, PNG_1X1); + const tray = await startFakeTray(({ url }, res) => { + if (url === "/health") { + json(res, 200, { ok: true, open: true, hostReady: true }); + return; + } + if (url === "/theme/apply") { + json(res, 200, { + ok: false, + error: "renderer failed", + sourceMode: "local", + timings: { totalMs: 1400, phases: { rendererVerify: 1200, rollback: 30 } }, + }); + return; + } + json(res, 404, { ok: false, error: "not found" }); + }); + t.after(async () => { + await tray.close(); + await fs.rm(root, { recursive: true, force: true }); + }); + await writeControl(root, tray.url); + + await assert.rejects( + () => createBeauticodeActions(root).applyImage(image), + (error) => { + assert.equal(error.sourceMode, "local"); + assert.equal(error.timings.phases.rendererVerify, 1200); + assert.equal(error.timings.phases.rollback, 30); + return true; + }, + ); +}); + test("plugin registers tools and commands through optional inject", async (t) => { const root = await createTempRoot(); t.after(() => fs.rm(root, { recursive: true, force: true })); @@ -373,6 +428,12 @@ test("in-process apply imports a video without the tray", async (t) => { assert.match(String(current.videoUrl), /^http:\/\/127\.0\.0\.1:\d+\//); const status = await actions.status(); assert.equal(status.background?.type, "video"); + assert.equal(status.background?.source?.kind, "local"); + assert.equal(status.background?.video, undefined); + assert.deepEqual((await fs.readdir(path.join(dataRoot, "active"))).sort(), [ + "background.json", + "poster.png", + ]); }); test("prompt section failure does not prevent tool registration", () => { diff --git a/integrations/deepseek-harness/test/pack.test.mjs b/integrations/deepseek-harness/test/pack.test.mjs index 33b7200..a7a3ab5 100644 --- a/integrations/deepseek-harness/test/pack.test.mjs +++ b/integrations/deepseek-harness/test/pack.test.mjs @@ -13,7 +13,7 @@ test("staged npm plugin is a self-contained DSH bundle with a vendored engine", const dest = path.join(os.tmpdir(), `bc-dsh-pack-${process.pid}`); await stageDshPlugin(dest, { build: false }); const pkg = JSON.parse(await fs.readFile(path.join(dest, "package.json"), "utf8")); - assert.equal(pkg.name, "@beauticode/dsh-plugin"); + assert.equal(pkg.name, "beauticode-dsh"); assert.equal(pkg.dsh.bundle.patch, "./cordis.patch.yml"); assert.equal(pkg.bin["beauticode-dsh"], "bin/beauticode-dsh"); assert.equal(await fs.readFile(path.join(dest, "cordis.patch.yml"), "utf8").then((text) => text.includes("beauticode-bridge")), true); diff --git a/integrations/deepseek-harness/test/plugin.test.mjs b/integrations/deepseek-harness/test/plugin.test.mjs index 8cf6e00..725eb47 100644 --- a/integrations/deepseek-harness/test/plugin.test.mjs +++ b/integrations/deepseek-harness/test/plugin.test.mjs @@ -98,6 +98,13 @@ test("plugin injects its client script exactly once", async (t) => { assert.equal(response.status, 200); const source = await response.text(); assert.doesNotThrow(() => new Function(source)); + assert.match(source, /waitForStablePlayback/); + assert.match(source, /renderPhase = "pending"/); + assert.match(source, /renderPhase === "ready"/); + assert.doesNotMatch(source, /acknowledgeRender\(activePayload, video\.readyState >= 2/); + assert.match(source, /requestVideoFrameCallback/); + assert.match(source, /currentTime.*initialTime \+ 0\.08/s); + assert.match(source, /MEDIA_ERR_DECODE/); assert.match(source, /:has\(#root \[data-phase="active"\]\)/); assert.match(source, /:has\(#root \[data-phase="settling"\]\)/); assert.doesNotMatch(source, /:has\(#root \[data-phase="hero"\]\)/); @@ -262,6 +269,7 @@ test("authenticated apply reaches SSE client and same-origin ack becomes ready", current: { ...payload, videoUrl: null, startAt: null }, readyClients: 1, failedClients: 0, + lastRenderError: null, visibleClients: 1, modeReadyClients: 0, blockedClients: 0, @@ -409,3 +417,72 @@ test("video apply and display modes are broadcast and acknowledged", async (t) = assert.equal(body.resolvedTone, "light"); assert.equal(body.playback.currentTime, 4.8); }); + +test("transient video heartbeat is pending until an explicit renderer verdict", async (t) => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "beauticode-dsh-plugin-")); + const tokenFile = path.join(root, "token"); + await fs.writeFile(tokenFile, TOKEN); + const plugin = await createPluginServer(tokenFile); + const events = await openEvents(plugin.origin, "client-video-pending-01"); + t.after(async () => { + events.request.destroy(); + events.response.destroy(); + await plugin.dispose(); + await fs.rm(root, { recursive: true, force: true }); + }); + + const payload = { + generation: 120, + media: "video", + imageUrl: "http://127.0.0.1:45678/media/image?t=poster", + videoUrl: "http://127.0.0.1:45678/media/video?t=movie", + startAt: 0, + }; + assert.equal((await fetch(`${plugin.origin}/__beauticode/apply`, { + method: "POST", + headers: { Authorization: `Bearer ${TOKEN}`, "content-type": "application/json" }, + body: JSON.stringify(payload), + })).status, 200); + + const headers = { Origin: plugin.origin, "content-type": "application/json" }; + assert.equal((await fetch(`${plugin.origin}/__beauticode/ack`, { + method: "POST", + headers, + body: JSON.stringify({ + clientId: "client-video-pending-01", + kind: "render", + generation: 120, + media: "video", + ok: false, + visible: true, + error: null, + }), + })).status, 200); + + let status = await (await fetch(`${plugin.origin}/__beauticode/status`, { + headers: { Authorization: `Bearer ${TOKEN}` }, + })).json(); + assert.equal(status.readyClients, 0); + assert.equal(status.failedClients, 0); + assert.equal(status.lastRenderError, null); + + const decodeError = "视频解码器报告失败;mediaError=MEDIA_ERR_DECODE"; + assert.equal((await fetch(`${plugin.origin}/__beauticode/ack`, { + method: "POST", + headers, + body: JSON.stringify({ + clientId: "client-video-pending-01", + kind: "render", + generation: 120, + media: "video", + ok: false, + visible: false, + error: decodeError, + }), + })).status, 200); + status = await (await fetch(`${plugin.origin}/__beauticode/status`, { + headers: { Authorization: `Bearer ${TOKEN}` }, + })).json(); + assert.equal(status.failedClients, 1); + assert.equal(status.lastRenderError, decodeError); +}); diff --git a/integrations/deepseek-harness/test/ui-host.test.mjs b/integrations/deepseek-harness/test/ui-host.test.mjs index aa0f6cd..686c050 100644 --- a/integrations/deepseek-harness/test/ui-host.test.mjs +++ b/integrations/deepseek-harness/test/ui-host.test.mjs @@ -1,11 +1,18 @@ import assert from "node:assert/strict"; import fs from "node:fs/promises"; import http from "node:http"; +import { EventEmitter } from "node:events"; import os from "node:os"; import path from "node:path"; +import { PassThrough } from "node:stream"; import test from "node:test"; import { apply } from "../index.mjs"; -import { parseImportFilename } from "../ui-host.mjs"; +import { + buildWindowsPickerScript, + createWindowsMediaPicker, + parseImportFilename, + parseImportThemeName, +} from "../ui-host.mjs"; import { writeDshControlFile, } from "../control-client.mjs"; @@ -31,7 +38,7 @@ class FakeWebServer { } } -async function createPluginServer(tokenFile) { +async function createPluginServer(tokenFile, config = {}) { const webServer = new FakeWebServer(); const effects = []; apply( @@ -41,7 +48,7 @@ async function createPluginServer(tokenFile) { effects.push(factory()); }, }, - { tokenFile }, + { tokenFile, ...config }, ); const server = http.createServer(async (req, res) => { const pathname = new URL(req.url || "/", "http://x").pathname; @@ -74,7 +81,7 @@ test("plugin injects a compact sidebar console script", async (t) => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "beauticode-ui-")); const tokenFile = path.join(root, "token"); await fs.writeFile(tokenFile, TOKEN); - const plugin = await createPluginServer(tokenFile); + const plugin = await createPluginServer(tokenFile, { allowManagedUpload: true }); t.after(async () => { await plugin.dispose(); await fs.rm(root, { recursive: true, force: true }); @@ -96,9 +103,19 @@ test("plugin injects a compact sidebar console script", async (t) => { assert.doesNotMatch(source, /