diff --git a/README.md b/README.md index 60bfb6c..a3d2fcc 100644 --- a/README.md +++ b/README.md @@ -73,34 +73,30 @@ beautiCode 可以让你: ### 一键安装插件(推荐) -只要给 DeepSeek Harness 加背景,装插件即可,不必下 Windows 安装包,也不必开托盘。请先自己装好 DSH。 +只要给 DeepSeek Harness 加背景,装插件即可,不必 fork 仓库、不必下 Windows 安装包,也不必开托盘。请先自己装好 DSH 和 Node.js。 -在项目目录执行: - -```powershell -powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\install-dsh-plugin.ps1 +```sh +npx beauticode-dsh +npx @deepseek-ai/dsh web ``` -这条脚本会把插件写进你的 DSH profile,**不需要 pnpm,也不需要再执行 `dsh plugin add`**。 +`npx beauticode-dsh` 会从 npm 下载插件并写入你的 DSH profile,**不需要 pnpm,也不需要再执行 `dsh plugin add`**。已把 `dsh` 装到 PATH 时,第二行也可以写成 `dsh web`。 -然后自己启动: +打开网页后,侧栏「设置」上方有「背景」:可从文件夹选图片或 MP4、清除、开关声音、切换已保存主题。已保存主题里自带「画窗」。网页控制台没有摸鱼。外观浅色/深色仍用 DSH 自己的设置。下次启动会恢复上次背景。 + +也可以用 `/bg`、`/bg-theme`、`/bg-clear`,或直接跟 AI 说把本机图片/视频设成背景。 + +已有 pnpm 时,也可以让 DSH 自己装这个 npm 包: ```sh -dsh web -# 未把 dsh 装到 PATH 时: +npx @deepseek-ai/dsh plugin --profile web add beauticode-dsh npx @deepseek-ai/dsh web ``` -打开网页后,侧栏「设置」上方有「背景」:可从文件夹选图片或 MP4、清除、开关声音、切换已保存主题。网页控制台没有摸鱼。外观浅色/深色仍用 DSH 自己的设置。下次启动会恢复上次背景。 - -也可以用 `/bg`、`/bg-theme`、`/bg-clear`,或直接跟 AI 说把本机图片/视频设成背景。 - -若一键脚本不可用,再手动装(`dsh plugin add` 需要本机有 pnpm): +卸载: ```sh -dsh plugin --profile web add file:%CD%/integrations/deepseek-harness -# 或 -npx @deepseek-ai/dsh plugin --profile web add file:%CD%/integrations/deepseek-harness +npx beauticode-dsh --remove ``` aa80cb6fa224ff3bd45a2c43e3f9a7b6 @@ -155,6 +151,13 @@ npx @deepseek-ai/dsh web # npm run tray ``` +或手动把本地插件目录加进 DSH(需要 pnpm): + +```sh +npx @deepseek-ai/dsh plugin --profile web add file:%CD%/integrations/deepseek-harness +npx @deepseek-ai/dsh web +``` + 或打开宿主选择器: ```powershell diff --git a/docs/deepseek-harness.md b/docs/deepseek-harness.md index 322b9df..d4fec98 100644 --- a/docs/deepseek-harness.md +++ b/docs/deepseek-harness.md @@ -22,6 +22,20 @@ Windows 安装包会在安装结束时(以及选择 DeepSeek Harness 时)自 若改过安装目录,看安装文件夹里的 `集成说明.txt`,不要照抄默认路径。 +不需要 fork 仓库。一行安装(不需要 pnpm): + +```sh +npx beauticode-dsh +npx @deepseek-ai/dsh web +``` + +已有 pnpm 时也可以: + +```sh +npx @deepseek-ai/dsh plugin --profile web add beauticode-dsh +npx @deepseek-ai/dsh web +``` + 也可手动(`dsh plugin add` 需要本机有 pnpm): ```sh @@ -34,7 +48,7 @@ dsh plugin --profile web add file:%LOCALAPPDATA%\Programs\beautiCode\integration npx @deepseek-ai/dsh plugin --profile web add file:%LOCALAPPDATA%\Programs\beautiCode\integrations\deepseek-harness ``` -包尚未发布到 npm,请按路径安装。未把 `dsh` 装到 PATH 时,用上面的 `npx @deepseek-ai/dsh` 写法。 +未把 `dsh` 装到 PATH 时,用上面的 `npx @deepseek-ai/dsh` 写法。 ## 启用插件 diff --git a/integrations/deepseek-harness/README.zh-CN.md b/integrations/deepseek-harness/README.zh-CN.md index 9d11012..6d33156 100644 --- a/integrations/deepseek-harness/README.zh-CN.md +++ b/integrations/deepseek-harness/README.zh-CN.md @@ -8,7 +8,21 @@ beautiCode **不启动** DeepSeek Harness。请先自己运行 `dsh web`。 -推荐在仓库根目录一键写入插件(不需要 pnpm): +不需要 fork 仓库。一行安装: + +```sh +npx beauticode-dsh +npx @deepseek-ai/dsh web +``` + +已有 pnpm 时也可以: + +```sh +npx @deepseek-ai/dsh plugin --profile web add beauticode-dsh +npx @deepseek-ai/dsh web +``` + +在仓库根目录一键写入插件(不需要 pnpm): ```powershell powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\install-dsh-plugin.ps1 diff --git a/integrations/deepseek-harness/bin/beauticode-dsh b/integrations/deepseek-harness/bin/beauticode-dsh new file mode 100644 index 0000000..e1cbe2c --- /dev/null +++ b/integrations/deepseek-harness/bin/beauticode-dsh @@ -0,0 +1,219 @@ +#!/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"; + +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); + }); +} diff --git a/integrations/deepseek-harness/cli.js b/integrations/deepseek-harness/cli.js new file mode 100644 index 0000000..e1cbe2c --- /dev/null +++ b/integrations/deepseek-harness/cli.js @@ -0,0 +1,219 @@ +#!/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"; + +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); + }); +} diff --git a/integrations/deepseek-harness/cordis.patch.yml b/integrations/deepseek-harness/cordis.patch.yml new file mode 100644 index 0000000..4b0da46 --- /dev/null +++ b/integrations/deepseek-harness/cordis.patch.yml @@ -0,0 +1,4 @@ +- insert: + - id: beauticode-bridge + name: '@beauticode/dsh-plugin' + inject: [webServer] diff --git a/integrations/deepseek-harness/host-apply.mjs b/integrations/deepseek-harness/host-apply.mjs index 87fa10d..008a7e5 100644 --- a/integrations/deepseek-harness/host-apply.mjs +++ b/integrations/deepseek-harness/host-apply.mjs @@ -10,7 +10,7 @@ import { canvasImagePath } from "./presets.mjs"; const sessions = new Map(); export const ENGINE_MISSING_MESSAGE = - "beautiCode 插件未能加载本机导入引擎。请先自己运行 dsh web,并从完整安装目录加载桥接。"; + "beautiCode 插件未能加载本机导入引擎。请执行 npx beauticode-dsh,或从完整安装目录加载桥接。"; export function resolvePluginBaseUrl(ctx) { const port = ctx?.webServer?.port; @@ -28,9 +28,10 @@ function sessionKey(dataRoot) { return path.resolve(dataRoot); } -async function loadAdapter() { +export async function loadAdapter() { const errors = []; for (const specifier of [ + new URL("./vendor/adapter-dsh/index.js", import.meta.url).href, "@beauticode/adapter-dsh", new URL("../../packages/adapter-dsh/dist/index.js", import.meta.url).href, ]) { diff --git a/integrations/deepseek-harness/package.json b/integrations/deepseek-harness/package.json index 6f45303..403ca8d 100644 --- a/integrations/deepseek-harness/package.json +++ b/integrations/deepseek-harness/package.json @@ -4,6 +4,9 @@ "description": "Cordis plugin: image/video backgrounds for DeepSeek Harness web.", "type": "module", "main": "./index.mjs", + "bin": { + "beauticode-dsh": "bin/beauticode-dsh" + }, "exports": { ".": { "import": "./index.mjs" @@ -18,8 +21,34 @@ "agent.mjs", "control-client.mjs", "host-apply.mjs", - "ui-host.mjs" + "ui-host.mjs", + "cli.js", + "bin/beauticode-dsh", + "cordis.patch.yml", + "vendor", + "themes" + ], + "dsh": { + "bundle": { + "patch": "./cordis.patch.yml" + } + }, + "publishConfig": { + "access": "public" + }, + "keywords": [ + "dsh", + "dsh-plugin", + "deepseek", + "deepseek-harness", + "background", + "wallpaper" ], + "repository": { + "type": "git", + "url": "git+https://github.com/starsstreaming/beautiCode.git", + "directory": "integrations/deepseek-harness" + }, "engines": { "node": ">=22" }, diff --git a/integrations/deepseek-harness/presets.mjs b/integrations/deepseek-harness/presets.mjs index 2a01154..480e9a0 100644 --- a/integrations/deepseek-harness/presets.mjs +++ b/integrations/deepseek-harness/presets.mjs @@ -41,7 +41,13 @@ export function themeAssetsDir() { path.join(here, "..", "..", "assets", "themes", "internal-beyond"), ]; for (const dir of candidates) { - if (fs.existsSync(path.join(dir, "bg-internal.jpg"))) return dir; + if ( + fs.existsSync(path.join(dir, "bg-canvas-4k.png")) || + fs.existsSync(path.join(dir, "bg-canvas.png")) || + fs.existsSync(path.join(dir, "bg-internal.jpg")) + ) { + return dir; + } } return candidates[candidates.length - 1]; } diff --git a/integrations/deepseek-harness/test/pack.test.mjs b/integrations/deepseek-harness/test/pack.test.mjs new file mode 100644 index 0000000..33b7200 --- /dev/null +++ b/integrations/deepseek-harness/test/pack.test.mjs @@ -0,0 +1,42 @@ +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import test from "node:test"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import { stageDshPlugin } from "../../../scripts/pack-dsh-plugin.mjs"; +import { runCli } from "../cli.js"; + +const here = path.dirname(fileURLToPath(import.meta.url)); + +test("staged npm plugin is a self-contained DSH bundle with a vendored engine", async () => { + 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.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); + const adapter = path.join(dest, "vendor", "adapter-dsh", "index.js"); + const canvas = path.join(dest, "themes", "internal-beyond", "bg-canvas-4k.png"); + await fs.access(adapter); + await fs.access(canvas); + const session = await import(pathToFileURL(adapter).href); + assert.equal(typeof session.DshSession, "function"); + await fs.rm(dest, { recursive: true, force: true }); +}); + +test("npx installer writes a DSH home patch without a web profile", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "bc-dsh-npx-")); + const dshHome = path.join(root, "dsh"); + const pluginHome = path.join(root, "plugin"); + await runCli(["--dsh-home", dshHome, "--plugin-home", pluginHome]); + const patch = await fs.readFile(path.join(dshHome, "cordis.patch.yml"), "utf8"); + assert.match(patch, /id: beauticode-bridge/); + assert.match(patch, /file:/); + await fs.access(path.join(pluginHome, "index.mjs")); + await fs.access(path.join(pluginHome, "vendor", "adapter-dsh", "index.js")); + await runCli(["--remove", "--dsh-home", dshHome, "--plugin-home", pluginHome]); + await assert.rejects(() => fs.access(pluginHome)); + await fs.rm(root, { recursive: true, force: true }); +}); diff --git a/package.json b/package.json index e7baa08..5a42e2b 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,9 @@ "bc": "node scripts/beauticode.mjs", "smoke:live": "npm run build && node scripts/live-smoke.mjs", "tray": "npm run build && powershell -NoProfile -ExecutionPolicy Bypass -File apps/tray/start-tray.ps1 -SkipBuild", - "installer:windows": "powershell -NoProfile -ExecutionPolicy Bypass -File scripts/build-windows-installer.ps1" + "installer:windows": "powershell -NoProfile -ExecutionPolicy Bypass -File scripts/build-windows-installer.ps1", + "plugin:pack": "node scripts/pack-dsh-plugin.mjs", + "plugin:publish": "node scripts/pack-dsh-plugin.mjs --publish" }, "engines": { "node": ">=22" diff --git a/scripts/build-windows-installer.ps1 b/scripts/build-windows-installer.ps1 index 71fb128..a72066c 100644 --- a/scripts/build-windows-installer.ps1 +++ b/scripts/build-windows-installer.ps1 @@ -240,6 +240,8 @@ foreach ($relativeFile in @( "integrations\deepseek-harness\agent.mjs", "integrations\deepseek-harness\control-client.mjs", "integrations\deepseek-harness\host-apply.mjs", + "integrations\deepseek-harness\cli.js", + "integrations\deepseek-harness\cordis.patch.yml", "integrations\deepseek-harness\package.json", "assets\themes\internal-beyond\bg-internal.jpg", "assets\themes\internal-beyond\bg-infernal.jpg", diff --git a/scripts/pack-dsh-plugin.mjs b/scripts/pack-dsh-plugin.mjs new file mode 100644 index 0000000..106406b --- /dev/null +++ b/scripts/pack-dsh-plugin.mjs @@ -0,0 +1,191 @@ +#!/usr/bin/env node +/** + * Stage a self-contained @beauticode/dsh-plugin for npm / npx. + * Copies the Cordis plugin, vendored core + adapter-dsh, and 画窗 assets. + */ +import { spawnSync } from "node:child_process"; +import fs from "node:fs"; +import fsp from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; + +const here = path.dirname(fileURLToPath(import.meta.url)); +const repoRoot = path.resolve(here, ".."); +const pluginSrc = path.join(repoRoot, "integrations", "deepseek-harness"); + +const PLUGIN_FILES = [ + "index.mjs", + "client.js", + "console.js", + "atmosphere.js", + "presets.mjs", + "agent.mjs", + "control-client.mjs", + "host-apply.mjs", + "ui-host.mjs", + "cli.js", + "bin/beauticode-dsh", + "cordis.patch.yml", + "package.json", + "README.zh-CN.md", +]; + +const THEME_FILES = [ + "bg-canvas-4k.png", + "bg-canvas.png", + "bg-internal.jpg", + "bg-infernal.jpg", + "NOTICE.md", +]; + +export function defaultStageDir() { + return path.join(repoRoot, "artifacts", "dsh-plugin"); +} + +function runBuild() { + const result = spawnSync( + process.platform === "win32" ? "npm.cmd" : "npm", + ["run", "build", "-w", "@beauticode/core"], + { cwd: repoRoot, stdio: "inherit", shell: process.platform === "win32" }, + ); + if (result.status !== 0) { + throw new Error("Failed to build @beauticode/core."); + } + const adapter = spawnSync( + process.platform === "win32" ? "npm.cmd" : "npm", + ["run", "build", "-w", "@beauticode/adapter-dsh"], + { cwd: repoRoot, stdio: "inherit", shell: process.platform === "win32" }, + ); + if (adapter.status !== 0) { + throw new Error("Failed to build @beauticode/adapter-dsh."); + } +} + +async function copyJsTree(fromDir, toDir) { + await fsp.mkdir(toDir, { recursive: true }); + const entries = await fsp.readdir(fromDir, { withFileTypes: true }); + for (const entry of entries) { + if (entry.name.endsWith(".map") || entry.name.endsWith(".d.ts")) continue; + const source = path.join(fromDir, entry.name); + const dest = path.join(toDir, entry.name); + if (entry.isDirectory()) { + await copyJsTree(source, dest); + continue; + } + if (!entry.isFile() || !entry.name.endsWith(".js")) continue; + await fsp.copyFile(source, dest); + } +} + +function rewriteCoreImports(jsText) { + return jsText + .replaceAll('from "@beauticode/core"', 'from "../core/index.js"') + .replaceAll("from '@beauticode/core'", "from '../core/index.js'"); +} + +export async function stageEngineInto(destRoot) { + const vendorAdapter = path.join(destRoot, "vendor", "adapter-dsh"); + const vendorCore = path.join(destRoot, "vendor", "core"); + const themesDest = path.join(destRoot, "themes", "internal-beyond"); + await copyJsTree(path.join(repoRoot, "packages", "core", "dist"), vendorCore); + await copyJsTree( + path.join(repoRoot, "packages", "adapter-dsh", "dist"), + vendorAdapter, + ); + await fsp.writeFile( + path.join(vendorCore, "package.json"), + `${JSON.stringify({ name: "@beauticode/core", type: "module", main: "./index.js" }, null, 2)}\n`, + "utf8", + ); + await fsp.writeFile( + path.join(vendorAdapter, "package.json"), + `${JSON.stringify({ name: "@beauticode/adapter-dsh", type: "module", main: "./index.js" }, null, 2)}\n`, + "utf8", + ); + const adapterFiles = await fsp.readdir(vendorAdapter); + for (const name of adapterFiles) { + if (!name.endsWith(".js")) continue; + const filePath = path.join(vendorAdapter, name); + const next = rewriteCoreImports(await fsp.readFile(filePath, "utf8")); + await fsp.writeFile(filePath, next, "utf8"); + } + await fsp.mkdir(themesDest, { recursive: true }); + const themesSrc = path.join(repoRoot, "assets", "themes", "internal-beyond"); + for (const name of THEME_FILES) { + const source = path.join(themesSrc, name); + if (!fs.existsSync(source)) continue; + await fsp.copyFile(source, path.join(themesDest, name)); + } +} + +export async function applyPublishName(destRoot, publishName) { + const pkgPath = path.join(destRoot, "package.json"); + const pkg = JSON.parse(await fsp.readFile(pkgPath, "utf8")); + const previous = pkg.name; + pkg.name = publishName; + await fsp.writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`, "utf8"); + for (const relative of ["cordis.patch.yml", "cli.js", "bin/beauticode-dsh"]) { + const filePath = path.join(destRoot, relative); + if (!fs.existsSync(filePath)) continue; + const text = await fsp.readFile(filePath, "utf8"); + await fsp.writeFile(filePath, text.replaceAll(previous, publishName), "utf8"); + } +} + +export async function stageDshPlugin(destRoot = defaultStageDir(), opts = {}) { + if (opts.build !== false) runBuild(); + await fsp.rm(destRoot, { recursive: true, force: true }); + await fsp.mkdir(destRoot, { recursive: true }); + for (const name of PLUGIN_FILES) { + const source = path.join(pluginSrc, name); + if (!fs.existsSync(source)) { + throw new Error(`Missing plugin file: ${name}`); + } + const dest = path.join(destRoot, name); + await fsp.mkdir(path.dirname(dest), { recursive: true }); + await fsp.copyFile(source, dest); + } + await stageEngineInto(destRoot); + if (opts.publishName) await applyPublishName(destRoot, opts.publishName); + return destRoot; +} + +function parseArgs(argv) { + const otpIdx = argv.indexOf("--otp"); + const nameIdx = argv.indexOf("--name"); + return { + publish: argv.includes("--publish"), + dryRun: argv.includes("--dry-run"), + build: !argv.includes("--no-build"), + otp: otpIdx === -1 ? null : argv[otpIdx + 1] ?? null, + publishName: nameIdx === -1 ? "beauticode-dsh" : argv[nameIdx + 1] ?? "beauticode-dsh", + }; +} + +async function main() { + const args = parseArgs(process.argv.slice(2)); + const dest = defaultStageDir(); + await stageDshPlugin(dest, { build: args.build, publishName: args.publishName }); + const pkg = JSON.parse(await fsp.readFile(path.join(dest, "package.json"), "utf8")); + process.stdout.write(`Staged ${pkg.name}@${pkg.version} at ${dest}\n`); + if (!args.publish && !args.dryRun) return; + const npmArgs = ["publish", "--access", "public"]; + if (args.dryRun) npmArgs.push("--dry-run"); + if (args.otp) npmArgs.push("--otp", args.otp); + const published = spawnSync( + process.platform === "win32" ? "npm.cmd" : "npm", + npmArgs, + { cwd: dest, stdio: "inherit", shell: process.platform === "win32" }, + ); + if (published.status !== 0) { + process.exit(published.status ?? 1); + } +} + +const launchedDirectly = + Boolean(process.argv[1]) && + pathToFileURL(path.resolve(process.argv[1])).href.toLowerCase() === + import.meta.url.toLowerCase(); +if (launchedDirectly) { + await main(); +}