diff --git a/build/dsh-desktop.patch.yml b/build/dsh-desktop.patch.yml index f31b9cd7..b4b9e102 100644 --- a/build/dsh-desktop.patch.yml +++ b/build/dsh-desktop.patch.yml @@ -21,6 +21,14 @@ - id: dsh-desktop-market-installer name: dsh-desktop-market-installer + # WorkBuddy PPT keeps one Host/render core across the external host and + # Desktop. This row loads the Harness 0.1.2 standard Composer client only. + - id: experimental-office-ppt-standard-adapter + name: '@deepseek-ai/dsh-experimental-office-ppt-standard-adapter' + disabled: !!js "!process.env.DSH_WORKBUDDY_PPT_RUNTIME_ROOT" + config: + root: !!js dshHomePath('office-ppt') + # dsh-market normally offers to restart its own Node process after mutations. # The desktop shell has the real lifecycle authority, so keep that behavior off # whenever the optional plugin is present in the composed profile. diff --git a/docs/development.md b/docs/development.md index 82581af6..cc404241 100644 --- a/docs/development.md +++ b/docs/development.md @@ -21,6 +21,8 @@ npm run dev `npm ci` runs the repository's `postinstall` hook. It reapplies the tracked `patch-package` patches, installs DSH brand assets into the pinned Harness frontend, and installs Electron. +On macOS Apple Silicon, the first `npm run dev` also downloads the pinned PPT Capability runtime from its public binary release, verifies the archive SHA-256 and the complete extracted file tree, and stores it in the user cache. Later runs reuse and reverify that cache. The SlideP and Tencent Docs engine runtime is distributed separately as closed-source runtime material; it is not committed to this repository and is not covered by this repository's MIT license. Run `npm run runtime:ensure` to prepare it without starting Electron. Set `DSH_WORKBUDDY_PPT_RUNTIME_ROOT` to use an already staged verified runtime, or `DSH_WORKBUDDY_PPT_RUNTIME_CACHE` to choose another cache root. + Development builds use the separate application name `DSH Desktop Dev` and the separate user-data directory `dsh-desktop-dev`, so they do not reuse production DSH Desktop data. Multiple development worktrees still share that development profile by default; avoid running them at the same time when testing profile, plugin, migration, or recovery changes. ## Validation @@ -89,6 +91,8 @@ npm run package:mac:x64 npm run package:win ``` +The Apple Silicon packaging command uses the same runtime downloader and verification gate as local development, then embeds the verified runtime into the application resources. Published Desktop applications therefore remain self-contained and do not download executable code at application startup. + Do not invoke `electron-builder --win` from macOS or Linux for a distributable Windows package. The target verification scripts intentionally reject host/target mismatches. For local unsigned development packages, use the corresponding `package:dev:*` command. Before handing off a Windows installer, verify that `resources/app/node_modules/node/bin/node.exe` exists in `win-unpacked` and require the packaged Windows Harness smoke test to pass. diff --git a/docs/workbuddy-ppt-integration.zh.md b/docs/workbuddy-ppt-integration.zh.md new file mode 100644 index 00000000..92c0d26c --- /dev/null +++ b/docs/workbuddy-ppt-integration.zh.md @@ -0,0 +1,96 @@ +# PPT Capability 桌面集成说明 + +## 目标与当前基线 + +本分支把 PPT Capability 作为 DSH Desktop 内置插件交付,并保留同一份 Slides/JSX 核心在外部 DeepSeek Harness 中装配的能力。 + +- PR:`dataelement/dsh-desktop#278` +- Desktop 分支:`codex/ppt-capability-alpha4` +- Desktop 上游基线:`upstream/main@7cb9e04`,包含 `0.8.0-rc.3` +- Harness 依赖基线:`0.1.2-alpha.4` +- Slides 源码分支:`codex/desktop-plugin-compatibility@fcb06ab786` + +分支已经合入 2026-09-02 的最新 `upstream/main`。上游新增的启动维护与 dshmarket 产物修复进入同一个 PR;PPT Capability 保持现有 Composer 布局补丁和交互行为。 + +## 单一路线 + +桌面端只显示 Slides 模式。独立 PPT 模式、Kimi/PPTD Skill 注入、`pptd_*` 工具、PPTD CLI 与编译器已经退出发布包。模型使用以下五个 Host 工具完成可编辑 PPTX: + +1. `ppt_list_templates` +2. `ppt_get_template_pages` +3. `ppt_create` +4. `ppt_write_page` +5. `ppt_render` + +选中的模板由 Host 持久化为 `selectedTemplateId`。每轮任务从同一份状态得到模板 id、名称与配色,并自动注入 `tencent-pptx` Skill。模型读取真实源页图片和语义 JSX 构图,写入完整 SlideP JSX;Host 执行路径约束、素材准入、静态语法、字号、溢出、碰撞、内容关系、模板一致性、渲染校验、原子发布和审计。 + +## Desktop 适配结构 + +```text +dsh-workbuddy-ppt +├── Slides Host、状态、模板、Skill、质量门禁与文件交付 +└── 标准 Composer 共享组件 + │ + ▼ +@deepseek-ai/dsh-experimental-office-ppt-standard-adapter +├── conversation.hero.modeActions:agent preset 右侧的 Slides 入口 +├── conversation.input.accessory:输入框内的已选模板参考 +└── conversation.composer.dock:输入框下方的固定模板目录 + │ + ▼ +DSH Desktop 0.8.0-rc.3 / Harness 0.1.2-alpha.4 + │ + ▼ +Resources/workbuddy-ppt-runtime +├── skills/tencent-pptx +├── slidep +└── tencent-docs-ai-engine +``` + +Slides 入口位于 agent preset 右侧。打开 Slides 后输入框保持原位;分类栏紧接输入框下方,模板目录占用其余可视高度并独立滚动。选中模板后,缩略图占据输入区左侧的小区域,正文从右侧同行开始;空态占位词贴近输入区顶部。任务被会话接受后,缩略图、Slides 入口和模板目录一起收起。 + +## 运行时与包体积 + +Slides 路线依赖目标平台运行时。源码仓库只保存 `runtime-distribution.json` 的公开二进制地址与压缩包锁,以及 `runtime-lock.json` 的解压运行时锁。macOS arm64 首次执行 `npm run dev` 或打包命令时自动下载 207,169,828 字节的闭源运行时,先校验压缩包 SHA-256,再校验 9,919 个解压文件的完整目录树。开发进程通过校验后的缓存路径启用 Slides;构建则把同一运行时写入应用资源。编辑引擎在插件数据目录工作并写日志,App Resources 保持只读。SlideP 与 Tencent Docs 编辑引擎不进入 Git,也不适用本仓库的 MIT 许可证。macOS x64 与 Windows x64 尚未发布对应运行时,缺少运行时时保持插件禁用。 + +公开二进制制品: + +```text +Release https://github.com/SuperstructureJH/dsh-desktop/releases/tag/ppt-capability-runtime-v1 +Archive ppt-capability-runtime-darwin-arm64-5.4.1-0.2.112-wb.tar.gz +Size 207169828 +SHA-256 ca273302786617b722e98aa0976386a497e287db72880d07257bb32cd3d33a1d +``` + +代码 tarball 的生产依赖只保留 Schemastery、FFlate、TypeScript 和 Zod。PPTD 相关渲染与编译依赖不会进入 Desktop 的生产 `node_modules`。包体积的主要部分仍是 332 张模板源页与平台 Slides 运行时。 + +固定代码产物: + +```text +f6682e3c42cc946f28cdddd51c177d1411139feabb51448508fc0dbeb48220e2 29238986 dsh-workbuddy-ppt-0.1.1-rc.2-desktop-20260903-public-gallery.tgz +e909330b53c179cca5ee49a9277ab1d87d3a84c5395e7e1cee8a50a1877f9184 6973632 deepseek-ai-dsh-experimental-office-ppt-standard-adapter-0.1.1-rc.2-desktop-20260903-public-gallery.tgz +``` + +本地 macOS arm64 体验包: + +```text +44a7892e56272f3685598b241168f75851bd6e75c3c5aedb3d7ada3bbf3242e5 412510079 ppt-capability-slides-adjacent-main-7cb9e04-mac-arm64.zip +``` + +体验包采用完整资源树的 ad-hoc 签名,仅用于本轮验证和内部体验。开发配置明确关闭自动证书选择,避免本机 Apple Development 身份在打包末尾部分替换签名。332 张模板源页与约 563 MiB 的腾讯 Slides 平台运行时仍是包体主体。 + +## 验证状态 + +| 证据层 | 状态 | 证据 | +| --- | --- | --- | +| 最新主线同步 | PASS | 分支合入 `upstream/main@7cb9e04`,基线包含 `0.8.0-rc.3` | +| Slides 源码 bundle | PASS | Host、Client 与标准适配器完成干净 bundle;聚焦 Client 31 项测试与两套 Client TypeScript 构建通过;发布归档只含 Slides 路线 | +| 发布归档闭包 | PASS | 固定 SHA;无 PPTD CLI、PPTD bundle、独立 PPT 工具与生产编译依赖;332 张模板源页保留 | +| Desktop 自动化 | PASS | 插件闭包测试通过;全量 84 个测试文件、695 项测试通过;`npm run typecheck` 与 `npm run build` 通过 | +| 公开运行时获取 | PASS | 新建空缓存后从公开 GitHub Release 下载 207,169,828 字节;压缩包 SHA-256、9,919 文件运行时树、关键文件与执行权限全部通过;第二次执行直接复用缓存 | +| macOS arm64 目录包 | PASS | 自动下载的运行时通过严格校验并进入 arm64 目录包;App ad-hoc 签名严格校验通过,Electron 43.4.0 命令行启动通过;Slides 运行时约 563 MiB | +| 原生 Slides 回归 | NOT_RUN | 用户复核 Slides 顶部入口、固定输入框、分类/模板独立滚动、模板选中态、任务开始收起与真实生成 | +| PowerPoint/WPS 往返 | NOT_RUN | 新包生成后执行编辑、保存、关闭与重开 | +| 正式签名与公证 | NOT_RUN | 开发候选使用 ad-hoc 签名;正式发布另行执行 Developer ID 签名与公证 | + +自动化验证证明代码、依赖和打包闭包;原生 UI 与 Office 往返继续作为独立验收层。 diff --git a/electron-builder.cjs b/electron-builder.cjs new file mode 100644 index 00000000..0baf3fb5 --- /dev/null +++ b/electron-builder.cjs @@ -0,0 +1,14 @@ +const packageJson = require('./package.json') + +const runtimeRoot = process.env.DSH_WORKBUDDY_PPT_RUNTIME_ROOT +const runtimeResources = runtimeRoot + ? [{ from: runtimeRoot, to: 'workbuddy-ppt-runtime' }] + : [] + +module.exports = { + ...packageJson.build, + extraResources: [ + ...packageJson.build.extraResources, + ...runtimeResources + ] +} diff --git a/electron-builder.dev.cjs b/electron-builder.dev.cjs index 8c8e09ca..a0a219e9 100644 --- a/electron-builder.dev.cjs +++ b/electron-builder.dev.cjs @@ -1,9 +1,37 @@ +const { execFileSync } = require('node:child_process') +const path = require('node:path') const packageJson = require('./package.json') +const baseConfig = require('./electron-builder.cjs') + +function signAdHocDevelopmentMac(context) { + if (context.electronPlatformName !== 'darwin') return + + const appPath = path.join( + context.appOutDir, + `${context.packager.appInfo.productFilename}.app` + ) + execFileSync( + '/usr/bin/codesign', + ['--force', '--deep', '--sign', '-', appPath], + { stdio: 'inherit' } + ) +} module.exports = { - ...packageJson.build, + ...baseConfig, appId: 'io.dsh.desktop.dev', productName: 'DSH Desktop Dev', + mac: { + ...baseConfig.mac, + // Development packages use ad-hoc signatures. Hardened Runtime would make + // macOS enforce a shared Team ID between the app and Electron's nested + // frameworks, which local ad-hoc signatures cannot provide. + hardenedRuntime: false, + // Keep the development artifact deterministic even when this machine has + // an Apple Development identity. afterPack signs the complete final tree; + // a second identity-based signing pass would replace only part of it. + identity: null + }, directories: { ...packageJson.build.directories, output: 'dist-dev' @@ -18,5 +46,6 @@ module.exports = { ...packageJson.build.nsis, artifactName: 'dsh-desktop-dev-windows-${arch}-setup.${ext}' }, + afterPack: signAdHocDevelopmentMac, publish: null } diff --git a/package-lock.json b/package-lock.json index 071e3b1b..61ccc2fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -101,6 +101,7 @@ "@deepseek-ai/dsh-credentials-local": "file:packages/harness-0.1.2-alpha.4/npm-dsh/deepseek-ai-dsh-credentials-local-0.1.2-alpha.4.tgz", "@deepseek-ai/dsh-deepseek-llm-api-extensions": "file:packages/harness-0.1.2-alpha.4/npm-dsh/deepseek-ai-dsh-deepseek-llm-api-extensions-0.1.2-alpha.4.tgz", "@deepseek-ai/dsh-deque": "file:packages/harness-0.1.2-alpha.4/npm-dsh/deepseek-ai-dsh-deque-0.1.2-alpha.4.tgz", + "@deepseek-ai/dsh-experimental-office-ppt-standard-adapter": "file:packages/workbuddy-ppt/deepseek-ai-dsh-experimental-office-ppt-standard-adapter-0.1.1-rc.2-desktop-20260903-public-gallery.tgz", "@deepseek-ai/dsh-file-reference": "file:packages/harness-0.1.2-alpha.4/npm-dsh/deepseek-ai-dsh-file-reference-0.1.2-alpha.4.tgz", "@deepseek-ai/dsh-file-reference-local": "file:packages/harness-0.1.2-alpha.4/npm-dsh/deepseek-ai-dsh-file-reference-local-0.1.2-alpha.4.tgz", "@deepseek-ai/dsh-fs": "file:packages/harness-0.1.2-alpha.4/npm-dsh/deepseek-ai-dsh-fs-0.1.2-alpha.4.tgz", @@ -240,6 +241,7 @@ "dsh-desktop-hmr-fallback": "file:packages/dsh-desktop-hmr-fallback", "dsh-desktop-market-installer": "file:packages/dsh-desktop-market-installer", "dsh-desktop-preset-transfer": "file:packages/dsh-desktop-preset-transfer", + "dsh-workbuddy-ppt": "file:packages/workbuddy-ppt/dsh-workbuddy-ppt-0.1.1-rc.2-desktop-20260903-public-gallery.tgz", "dshmarket": "file:packages/dshmarket", "electron-updater": "^6.8.9", "node": "24.9.0", @@ -2459,6 +2461,20 @@ "@deepseek-ai/cordis": "^4.0.2" } }, + "node_modules/@deepseek-ai/dsh-experimental-office-ppt-standard-adapter": { + "version": "0.1.1-rc.2", + "resolved": "file:packages/workbuddy-ppt/deepseek-ai-dsh-experimental-office-ppt-standard-adapter-0.1.1-rc.2-desktop-20260903-public-gallery.tgz", + "integrity": "sha512-19M2c03qWA8qysKRLHCEh6RcXwbdNgUZgFGE74pTEDKUtLACU1M9qHXSFJZJurWKj/JwatBE0qCNMohpVZvE/Q==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1", + "dsh-workbuddy-ppt": "^0.1.1-rc.2" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2 || ^0.1.2-alpha.1" + } + }, "node_modules/@deepseek-ai/dsh-file-reference": { "version": "0.1.2-alpha.4", "resolved": "file:packages/harness-0.1.2-alpha.4/npm-dsh/deepseek-ai-dsh-file-reference-0.1.2-alpha.4.tgz", @@ -10125,6 +10141,59 @@ "resolved": "packages/dsh-desktop-preset-transfer", "link": true }, + "node_modules/dsh-workbuddy-ppt": { + "version": "0.1.1-rc.2", + "resolved": "file:packages/workbuddy-ppt/dsh-workbuddy-ppt-0.1.1-rc.2-desktop-20260903-public-gallery.tgz", + "integrity": "sha512-C0mG2kXFL5rYE0JsjGJJy4uSNojbVeLzhXQMlBLpkkeoHEYEuZrR2Xd0+O49+7U/fGS6pu0a3IRLDwFBC/ccNA==", + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "^3.18.1", + "fflate": "^0.8.3", + "typescript": "^6.0.3", + "zod": "^4.4.3" + }, + "engines": { + "node": "^22.19.0 || >=24.0.0" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent": "^0.1.1-rc.2 || ^0.1.2-alpha.1", + "@deepseek-ai/dsh-brand": "^0.1.1-rc.2 || ^0.1.2-alpha.1", + "@deepseek-ai/dsh-client-connection": "^0.1.1-rc.2 || ^0.1.2-alpha.1", + "@deepseek-ai/dsh-client-locale": "^0.1.1-rc.2 || ^0.1.2-alpha.1", + "@deepseek-ai/dsh-client-runtime": "^0.1.1-rc.2", + "@deepseek-ai/dsh-client-ui-conversation": "^0.1.1-rc.2 || ^0.1.2-alpha.1", + "@deepseek-ai/dsh-client-ui-primitives": "^0.1.1-rc.2 || ^0.1.2-alpha.1", + "@deepseek-ai/dsh-client-ui-slots": "^0.1.1-rc.2 || ^0.1.2-alpha.1", + "@deepseek-ai/dsh-client-ui-tool": "^0.1.1-rc.2 || ^0.1.2-alpha.1", + "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2 || ^0.1.2-alpha.1", + "@deepseek-ai/dsh-llm": "^0.1.1-rc.2 || ^0.1.2-alpha.1", + "@deepseek-ai/dsh-session": "^0.1.1-rc.2 || ^0.1.2-alpha.1", + "@deepseek-ai/dsh-skill": "^0.1.1-rc.2 || ^0.1.2-alpha.1", + "@deepseek-ai/dsh-subprocess": "^0.1.1-rc.2 || ^0.1.2-alpha.1", + "@deepseek-ai/dsh-system-prompt": "^0.1.1-rc.2 || ^0.1.2-alpha.1", + "@deepseek-ai/dsh-tools": "^0.1.1-rc.2 || ^0.1.2-alpha.1", + "react": "^18.2.0" + }, + "peerDependenciesMeta": { + "@deepseek-ai/dsh-client-runtime": { + "optional": true + } + } + }, + "node_modules/dsh-workbuddy-ppt/node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/dshmarket": { "resolved": "packages/dshmarket", "link": true diff --git a/package.json b/package.json index 1e3a240a..82e646a2 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ ], "scripts": { "postinstall": "patch-package && node scripts/install-brand-assets.mjs && install-electron --no", - "dev": "electron-vite dev", + "dev": "node scripts/run-with-workbuddy-ppt-runtime.mjs --runtime-optional electron-vite dev", "build": "npm run build:market && electron-vite build", "build:market": "tsc -p packages/dshmarket/tsconfig.json", "icons:generate": "node scripts/generate-app-icons.mjs", @@ -32,15 +32,23 @@ "typecheck": "tsc --noEmit -p tsconfig.node.json", "test": "vitest run", "test:watch": "vitest", - "package:dir": "npm run build && electron-builder --dir", - "package:dev:dir": "npm run build && electron-builder --dir --config electron-builder.dev.cjs", - "package:dev:mac:arm64": "node scripts/verify-target.mjs darwin arm64 && npm run build && electron-builder --mac --arm64 --publish never --config electron-builder.dev.cjs", - "package:dev:mac:x64": "node scripts/verify-target.mjs darwin x64 && npm run build && electron-builder --mac --x64 --publish never --config electron-builder.dev.cjs", - "package:dev:win": "node scripts/verify-target.mjs win32 x64 && npm run build && electron-builder --win --x64 --publish never --config electron-builder.dev.cjs", - "package:mac": "npm run build && electron-builder --mac --publish never", - "package:mac:arm64": "node scripts/verify-target.mjs darwin arm64 && npm run build && electron-builder --mac --arm64 --publish never", - "package:mac:x64": "node scripts/verify-target.mjs darwin x64 && npm run build && electron-builder --mac --x64 --publish never", - "package:win": "node scripts/verify-target.mjs win32 x64 && npm run build && electron-builder --win --x64 --publish never" + "runtime:ensure": "node scripts/ensure-workbuddy-ppt-runtime.mjs", + "runtime:verify": "node scripts/verify-workbuddy-ppt-runtime.mjs", + "runtime:verify:optional": "node scripts/verify-workbuddy-ppt-runtime.mjs --optional", + "package:dir": "node scripts/run-with-workbuddy-ppt-runtime.mjs npm run package:dir:runtime", + "package:dir:runtime": "npm run runtime:verify && npm run build && electron-builder --dir --config electron-builder.cjs", + "package:dev:dir": "node scripts/run-with-workbuddy-ppt-runtime.mjs npm run package:dev:dir:runtime", + "package:dev:dir:runtime": "npm run runtime:verify && npm run build && electron-builder --dir --config electron-builder.dev.cjs", + "package:dev:mac:arm64": "node scripts/run-with-workbuddy-ppt-runtime.mjs npm run package:dev:mac:arm64:runtime", + "package:dev:mac:arm64:runtime": "node scripts/verify-target.mjs darwin arm64 && npm run runtime:verify && npm run build && electron-builder --mac --arm64 --publish never --config electron-builder.dev.cjs", + "package:dev:mac:x64": "node scripts/verify-target.mjs darwin x64 && npm run runtime:verify:optional && npm run build && electron-builder --mac --x64 --publish never --config electron-builder.dev.cjs", + "package:dev:win": "node scripts/verify-target.mjs win32 x64 && npm run runtime:verify:optional && npm run build && electron-builder --win --x64 --publish never --config electron-builder.dev.cjs", + "package:mac": "node scripts/run-with-workbuddy-ppt-runtime.mjs npm run package:mac:runtime", + "package:mac:runtime": "npm run runtime:verify && npm run build && electron-builder --mac --publish never --config electron-builder.cjs", + "package:mac:arm64": "node scripts/run-with-workbuddy-ppt-runtime.mjs npm run package:mac:arm64:runtime", + "package:mac:arm64:runtime": "node scripts/verify-target.mjs darwin arm64 && npm run runtime:verify && npm run build && electron-builder --mac --arm64 --publish never --config electron-builder.cjs", + "package:mac:x64": "node scripts/verify-target.mjs darwin x64 && npm run runtime:verify:optional && npm run build && electron-builder --mac --x64 --publish never --config electron-builder.cjs", + "package:win": "node scripts/verify-target.mjs win32 x64 && npm run runtime:verify:optional && npm run build && electron-builder --win --x64 --publish never --config electron-builder.cjs" }, "dependencies": { "@deepseek-ai/cordis": "file:packages/harness-0.1.2-alpha.4/npm-vendor/deepseek-ai-cordis-4.0.2.tgz", @@ -133,6 +141,7 @@ "@deepseek-ai/dsh-credentials": "file:packages/harness-0.1.2-alpha.4/npm-dsh/deepseek-ai-dsh-credentials-0.1.2-alpha.4.tgz", "@deepseek-ai/dsh-credentials-local": "file:packages/harness-0.1.2-alpha.4/npm-dsh/deepseek-ai-dsh-credentials-local-0.1.2-alpha.4.tgz", "@deepseek-ai/dsh-deepseek-llm-api-extensions": "file:packages/harness-0.1.2-alpha.4/npm-dsh/deepseek-ai-dsh-deepseek-llm-api-extensions-0.1.2-alpha.4.tgz", + "@deepseek-ai/dsh-experimental-office-ppt-standard-adapter": "file:packages/workbuddy-ppt/deepseek-ai-dsh-experimental-office-ppt-standard-adapter-0.1.1-rc.2-desktop-20260903-public-gallery.tgz", "@deepseek-ai/dsh-deque": "file:packages/harness-0.1.2-alpha.4/npm-dsh/deepseek-ai-dsh-deque-0.1.2-alpha.4.tgz", "@deepseek-ai/dsh-file-reference": "file:packages/harness-0.1.2-alpha.4/npm-dsh/deepseek-ai-dsh-file-reference-0.1.2-alpha.4.tgz", "@deepseek-ai/dsh-file-reference-local": "file:packages/harness-0.1.2-alpha.4/npm-dsh/deepseek-ai-dsh-file-reference-local-0.1.2-alpha.4.tgz", @@ -273,6 +282,7 @@ "dsh-desktop-hmr-fallback": "file:packages/dsh-desktop-hmr-fallback", "dsh-desktop-market-installer": "file:packages/dsh-desktop-market-installer", "dsh-desktop-preset-transfer": "file:packages/dsh-desktop-preset-transfer", + "dsh-workbuddy-ppt": "file:packages/workbuddy-ppt/dsh-workbuddy-ppt-0.1.1-rc.2-desktop-20260903-public-gallery.tgz", "dshmarket": "file:packages/dshmarket", "electron-updater": "^6.8.9", "node": "24.9.0", diff --git a/packages/workbuddy-ppt/README.md b/packages/workbuddy-ppt/README.md new file mode 100644 index 00000000..b262b25d --- /dev/null +++ b/packages/workbuddy-ppt/README.md @@ -0,0 +1,36 @@ +# PPT Capability 内置产物 + +本目录固定 DSH Desktop 内置 Slides/JSX 能力的代码产物与平台运行时契约: + +- `dsh-workbuddy-ppt-0.1.1-rc.2-desktop-20260903-public-gallery.tgz`:Slides Host、模板状态、`tencent-pptx` Skill 注入、五个生成工具、质量检查、审计与文件交付。 +- `deepseek-ai-dsh-experimental-office-ppt-standard-adapter-0.1.1-rc.2-desktop-20260903-public-gallery.tgz`:标准 Composer 适配器。Slides 入口紧接 agent preset 右侧;打开后输入框保持固定,分类与模板目录在下方独立滚动;选中模板后,缩略图在输入区左侧与正文同行;任务开始后选择界面收起。 +- `runtime-lock.json`:固定 `tencent-pptx` Skill、SlideP、Tencent Docs 编辑引擎和目标平台关键文件 SHA-256。 +- `runtime-distribution.json`:固定每个平台的公开二进制下载地址、压缩包大小与 SHA-256;不包含运行时文件本体。 + +桌面发布包只提供 Slides 入口。代码产物移除了 PPTD CLI、PPTD 编译器和独立 PPT 模式工具链。内置目录只包含 44 套 MIT 许可的 Kimi 模板及 332 张源页图片;`data-analysis` 和 `vitality-blue` 两套单独购买的模板及预览资源均不进入源码包和桌面产物。 + +产物来自相邻 DeepSeek Harness 工作树 `codex/desktop-plugin-compatibility@fcb06ab786`。根 `package.json` 通过仓库相对 `file:` 路径安装两个 tarball;Desktop patch 只挂载标准适配器,适配器在同一 Cordis fiber 中组合 Host 核心。 + +SHA-256 与大小: + +```text +f6682e3c42cc946f28cdddd51c177d1411139feabb51448508fc0dbeb48220e2 29238986 dsh-workbuddy-ppt-0.1.1-rc.2-desktop-20260903-public-gallery.tgz +e909330b53c179cca5ee49a9277ab1d87d3a84c5395e7e1cee8a50a1877f9184 6973632 deepseek-ai-dsh-experimental-office-ppt-standard-adapter-0.1.1-rc.2-desktop-20260903-public-gallery.tgz +``` + +## 桌面运行时闭包 + +`npm run dev` 和 macOS arm64 打包命令通过 `scripts/run-with-workbuddy-ppt-runtime.mjs` 启动。首次运行按照 `runtime-distribution.json` 从公开 GitHub Release 下载闭源运行时,校验压缩包大小和 SHA-256,安全解压后再按 `runtime-lock.json` 校验平台、版本、完整文件树、关键哈希和执行权限。校验通过的目录保存在用户缓存中,后续启动复用同一个内容寻址目录。开发进程获得 `DSH_WORKBUDDY_PPT_RUNTIME_ROOT` 后启用 Slides;Electron Builder 把同一目录复制到 `Resources/workbuddy-ppt-runtime`,安装后的 App 自包含运行时,不会在应用启动时下载可执行代码。 + +macOS Apple Silicon 公开运行时: + +```text +ca273302786617b722e98aa0976386a497e287db72880d07257bb32cd3d33a1d 207169828 ppt-capability-runtime-darwin-arm64-5.4.1-0.2.112-wb.tar.gz +https://github.com/SuperstructureJH/dsh-desktop/releases/tag/ppt-capability-runtime-v1 +``` + +SlideP 与 Tencent Docs 编辑引擎作为闭源二进制运行时独立分发,不进入本仓库,也不适用本仓库的 MIT 许可证。任何运行前都必须通过公开锁定值与完整运行时树校验。`DSH_WORKBUDDY_PPT_RUNTIME_ROOT` 可指定已准备的绝对目录;`DSH_WORKBUDDY_PPT_RUNTIME_CACHE` 可覆盖默认用户缓存位置。 + +当前公开分发并验证 `darwin-arm64`。macOS x64 与 Windows x64 使用可选门禁:构建机提供匹配平台的运行时时启用 Slides;省略运行时时生成 PPT Capability 未启用的桌面包。为这些平台发布运行时时,在两个锁文件中添加对应平台记录,开发和打包命令即可复用相同下载链路。 + +更新产物时,同步更新两个代码 tarball、根依赖、`runtime-lock.json`、`runtime-distribution.json`、公开 Release、这里的哈希、集成测试和集成说明。 diff --git a/packages/workbuddy-ppt/deepseek-ai-dsh-experimental-office-ppt-standard-adapter-0.1.1-rc.2-desktop-20260903-public-gallery.tgz b/packages/workbuddy-ppt/deepseek-ai-dsh-experimental-office-ppt-standard-adapter-0.1.1-rc.2-desktop-20260903-public-gallery.tgz new file mode 100644 index 00000000..dc7ba0ce Binary files /dev/null and b/packages/workbuddy-ppt/deepseek-ai-dsh-experimental-office-ppt-standard-adapter-0.1.1-rc.2-desktop-20260903-public-gallery.tgz differ diff --git a/packages/workbuddy-ppt/dsh-workbuddy-ppt-0.1.1-rc.2-desktop-20260903-public-gallery.tgz b/packages/workbuddy-ppt/dsh-workbuddy-ppt-0.1.1-rc.2-desktop-20260903-public-gallery.tgz new file mode 100644 index 00000000..f3b6ac11 Binary files /dev/null and b/packages/workbuddy-ppt/dsh-workbuddy-ppt-0.1.1-rc.2-desktop-20260903-public-gallery.tgz differ diff --git a/packages/workbuddy-ppt/runtime-distribution.json b/packages/workbuddy-ppt/runtime-distribution.json new file mode 100644 index 00000000..d1560e99 --- /dev/null +++ b/packages/workbuddy-ppt/runtime-distribution.json @@ -0,0 +1,11 @@ +{ + "schemaVersion": 1, + "platforms": { + "darwin-arm64": { + "archive": "tar.gz", + "archiveSize": 207169828, + "archiveSha256": "ca273302786617b722e98aa0976386a497e287db72880d07257bb32cd3d33a1d", + "url": "https://github.com/SuperstructureJH/dsh-desktop/releases/download/ppt-capability-runtime-v1/ppt-capability-runtime-darwin-arm64-5.4.1-0.2.112-wb.tar.gz" + } + } +} diff --git a/packages/workbuddy-ppt/runtime-lock.json b/packages/workbuddy-ppt/runtime-lock.json new file mode 100644 index 00000000..30f7c163 --- /dev/null +++ b/packages/workbuddy-ppt/runtime-lock.json @@ -0,0 +1,22 @@ +{ + "schemaVersion": 1, + "platforms": { + "darwin-arm64": { + "slidep": "5.4.1", + "editorEngine": "0.2.112-wb", + "runtimeTree": { + "fileCount": 9919, + "sha256": "6b65a93d812b51d10eb8b7d406afdc36579588c4c22b27a41bc2a6847f449ad8" + }, + "files": { + "manifest.json": "cbeaba6fb3008601c55156ef872de7fd06437e797a71cd86f107a02089502a6a", + "skills/tencent-pptx/SKILL.md": "06d318a6b2d46052084e9b52d43f1e7c8e4824f74dc6990106ef2c9121c8da50", + "slidep/package.json": "016788a7df8f4420903cfb253c18227206817edff96ee682849c8799c7794734", + "slidep/dist/slidep-start.js": "9f068441c2aecb43a26f90be1fa5b0b691777cf01555c79a0d5ff333957cfc4b", + "slidep/dist/slidep-validate.js": "22ca8b7bd0d21c1d5ba17f70ed568fcbc83ead1d797b0c6d8b4bff30e07c314f", + "tencent-docs-ai-engine/bin/darwin-arm64/editor_sdk": "7d55e4c0840465dcd91fc1d865126b6e79321392cbb0d999b9d450d4f4b8bba2", + "tencent-docs-ai-engine/bin/darwin-arm64/icudt72l.dat": "a405960537e72ec6e23b88bad4cce8bc4daa134d5845f3f10da0c7ada06e0331" + } + } + } +} diff --git a/patches/@deepseek-ai+dsh+0.1.2-alpha.4.patch b/patches/@deepseek-ai+dsh+0.1.2-alpha.4.patch index a4e8d5d0..82d26156 100644 --- a/patches/@deepseek-ai+dsh+0.1.2-alpha.4.patch +++ b/patches/@deepseek-ai+dsh+0.1.2-alpha.4.patch @@ -2,10 +2,11 @@ diff --git a/node_modules/@deepseek-ai/dsh/package.json b/node_modules/@deepseek index 28eaff9..2445f81 100644 --- a/node_modules/@deepseek-ai/dsh/package.json +++ b/node_modules/@deepseek-ai/dsh/package.json -@@ -28,6 +28,10 @@ +@@ -28,6 +28,11 @@ }, "license": "MIT", "dependencies": { ++ "@deepseek-ai/dsh-experimental-office-ppt-standard-adapter": "0.1.1-rc.2", + "dsh-desktop-client-ui": "0.1.0", + "dsh-desktop-hmr-fallback": "0.1.0", + "dsh-desktop-market-installer": "0.1.0", diff --git a/patches/@deepseek-ai+dsh-client-ui-conversation+0.1.2-alpha.4.patch b/patches/@deepseek-ai+dsh-client-ui-conversation+0.1.2-alpha.4.patch new file mode 100644 index 00000000..08dc8d3d --- /dev/null +++ b/patches/@deepseek-ai+dsh-client-ui-conversation+0.1.2-alpha.4.patch @@ -0,0 +1,223 @@ +diff --git a/node_modules/@deepseek-ai/dsh-client-ui-conversation/lib/client.js b/node_modules/@deepseek-ai/dsh-client-ui-conversation/lib/client.js +index 829b8cb..ecdf1cf 100644 +--- a/node_modules/@deepseek-ai/dsh-client-ui-conversation/lib/client.js ++++ b/node_modules/@deepseek-ai/dsh-client-ui-conversation/lib/client.js +@@ -14183,12 +14183,13 @@ window.__ModuleLoader__.load({ + //#endregion + //#region \0dsh-css:/Users/alex/Documents/Code/AI/deepseek-harness/packages/client/ui-conversation/src/client/skeleton/ConversationRoot.module.css.mjs + const css$4 = ".HNmDuG_root{background:var(--dsw-alias-bg-base);--dsh-chat-content-width:var(--dsh-chat-user-width,clamp(680px, calc(var(--dsh-conversation-column-width,0px) * .64), 920px));--dsh-composer-card-max-width:calc(var(--dsh-chat-content-width) + 32px);--dsh-composer-side-clearance:16px;--dsh-composer-dock-inset:8px;flex-direction:column;min-width:0;height:100%;display:flex;position:relative}.HNmDuG_header{border-bottom:1px solid #0000;flex:none;padding:12px 28px 0 20px;position:relative}.HNmDuG_header:after{content:\"\";z-index:0;background:var(--dsw-alias-border-l3);pointer-events:none;height:.5px;position:absolute;bottom:1px;left:0;right:0}.HNmDuG_headerHidden{display:none}.HNmDuG_titleRow{align-items:center;gap:0;min-height:32px;display:flex}.HNmDuG_titleCluster{flex:1;align-items:center;gap:10px;min-width:0;display:flex}.HNmDuG_crumbs{white-space:nowrap;align-items:center;gap:4px;min-width:0;display:flex;overflow:hidden}.HNmDuG_crumbSeg{align-items:center;gap:4px;min-width:0;display:inline-flex}.HNmDuG_crumbSep{color:var(--dsw-alias-label-caption);font-size:14px;line-height:20px}.HNmDuG_crumb{max-width:220px;color:var(--dsw-alias-label-tertiary);text-overflow:ellipsis;white-space:nowrap;cursor:pointer;background:0 0;border:none;border-radius:12px;padding:4px 8px;font-size:14px;line-height:20px;overflow:hidden}.HNmDuG_crumbSubagent{font-size:12px;line-height:18px}.HNmDuG_crumb:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.HNmDuG_crumbCurrent{color:var(--dsw-alias-label-primary);cursor:default;font-weight:500}.HNmDuG_headerActions{flex:none;align-items:center;gap:8px;display:flex}.HNmDuG_headerUtilities{flex:none;align-items:center;gap:8px;margin-left:20px;display:flex}.HNmDuG_headerUtilities:empty{display:none}.HNmDuG_tabs{z-index:1;gap:36px;margin-top:4px;padding-left:8px;display:flex;position:relative}.HNmDuG_tab{color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:none;padding:0 0 11px;font-size:13px;font-weight:500;line-height:16px;position:relative}.HNmDuG_tab:after{content:\"\";background:0 0;border-radius:2px;height:2px;position:absolute;bottom:1px;left:0;right:0}.HNmDuG_tabActive{color:var(--dsw-alias-state-business-primary)}.HNmDuG_tabActive:after{background:var(--dsw-alias-state-business-primary)}.HNmDuG_viewArea{flex-direction:column;flex:1;min-height:0;display:flex}.HNmDuG_widthHandle{z-index:8;width:min(40px, calc((100% - var(--dsh-chat-content-width)) / 2 - 24px - 24px));cursor:col-resize;position:absolute;top:0;bottom:0}.HNmDuG_widthHandle[data-side=left]{right:calc(50% + var(--dsh-chat-content-width) / 2 + 24px)}.HNmDuG_widthHandle[data-side=right]{left:calc(50% + var(--dsh-chat-content-width) / 2 + 24px)}.HNmDuG_widthHandle:after{content:\"\";background:linear-gradient(to bottom, transparent calc(var(--dsh-width-handle-pointer-y,50%) - 52px), var(--dsw-alias-scrollbar-hover-l1) calc(var(--dsh-width-handle-pointer-y,50%) - 12px), var(--dsw-alias-scrollbar-hover-l1) calc(var(--dsh-width-handle-pointer-y,50%) + 12px), transparent calc(var(--dsh-width-handle-pointer-y,50%) + 52px));opacity:0;pointer-events:none;border-radius:3px;width:3px;position:absolute;top:0;bottom:0}.HNmDuG_widthHandle[data-side=left]:after{right:16px}.HNmDuG_widthHandle[data-side=right]:after{left:16px}.HNmDuG_widthHandle:hover:after,.HNmDuG_widthHandle[data-dragging]:after{opacity:1}.HNmDuG_root:has([data-conversation-composer-overlay]) .HNmDuG_widthHandle{display:none}.HNmDuG_composerStack{--dsh-composer-stack-gap:6px;gap:var(--dsh-composer-stack-gap);flex-direction:column;display:flex}.HNmDuG_composerSeat{--dsh-composer-text-max-height:336px;flex-direction:column;flex:none;display:flex}.HNmDuG_root[data-phase=active]{overflow:hidden}.HNmDuG_root[data-phase=active] .HNmDuG_header{flex:none}.HNmDuG_body{flex-direction:column;flex:1;min-height:0;display:flex;position:relative}.HNmDuG_scrollBody{scrollbar-gutter:stable;flex-direction:column;flex:1;min-height:0;display:flex;overflow-y:auto}.HNmDuG_root[data-phase=active] .HNmDuG_viewArea{flex:1 0 auto;min-height:auto}.HNmDuG_root[data-phase=active] .HNmDuG_composerSeat{z-index:7;background:linear-gradient(180deg, color-mix(in srgb, var(--dsw-alias-bg-base) 0%, transparent) 0px, var(--dsw-alias-bg-base) 36px);position:sticky;bottom:0}.HNmDuG_root[data-phase=active] .HNmDuG_composerSeat:has([data-trigger-menu]){z-index:9}.HNmDuG_scrollBody:has([data-conversation-composer-overlay]){scrollbar-gutter:auto;position:relative;overflow:hidden auto}.HNmDuG_scrollBody:has([data-conversation-composer-overlay])>[data-slot=conversation\\.session]>.HNmDuG_viewArea{flex:1 1 0;min-height:0;overflow:hidden}.HNmDuG_scrollBody:has([data-conversation-composer-overlay])>.HNmDuG_composerSeat{right:var(--dsh-scrollbar-width);position:absolute;bottom:0;left:0}.HNmDuG_composerHero{width:min(calc(var(--dsh-composer-card-max-width) + 2 * var(--dsh-composer-side-clearance)), 100%);z-index:1;align-self:center;gap:8px;padding-bottom:32px}.HNmDuG_heroWorkspaceRow{align-items:center;gap:2px;min-width:0;margin-top:4px;padding-left:20px;display:flex}.HNmDuG_root[data-phase=hero] .HNmDuG_scrollBody{justify-content:center;overflow-y:auto}.HNmDuG_root[data-phase=settling] .HNmDuG_composerSeat{visibility:hidden}"; ++ const heroModeClusterCss = ".HNmDuG_heroModeCluster{width:max-content;max-width:100%;flex:none;align-items:center;gap:2px;display:flex}.HNmDuG_heroModeCluster [data-slot=conversation\\.hero\\.agentPreset]>span{min-width:0}"; + const tagId$4 = "@deepseek-ai/dsh-client-ui-conversation/ConversationRoot.module.css"; + if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$4) + "]") === null) { + const tag = document.createElement("style"); + tag.dataset.plugin = "@deepseek-ai/dsh-client-ui-conversation"; + tag.dataset.pluginCss = tagId$4; +- tag.textContent = css$4; ++ tag.textContent = css$4 + heroModeClusterCss; + document.head.appendChild(tag); + } + var ConversationRoot_module_css_default = { +@@ -14206,6 +14207,7 @@ window.__ModuleLoader__.load({ + "headerActions": "HNmDuG_headerActions", + "headerHidden": "HNmDuG_headerHidden", + "headerUtilities": "HNmDuG_headerUtilities", ++ "heroModeCluster": "HNmDuG_heroModeCluster", + "heroWorkspaceRow": "HNmDuG_heroWorkspaceRow", + "root": "HNmDuG_root", + "scrollBody": "HNmDuG_scrollBody", +@@ -14430,12 +14432,16 @@ window.__ModuleLoader__.load({ + setPickerOpen(false); + } + }), +- renderSlot("conversation.hero.agentPreset", {}) ++ (0, react_jsx_runtime.jsxs)("div", { ++ className: ConversationRoot_module_css_default.heroModeCluster, ++ children: [renderSlot("conversation.hero.agentPreset", {}), zone !== void 0 && renderSlot("conversation.hero.modeActions", zone)] ++ }) + ] + }); + const inert = sessionId === void 0 || hero && chipTitle === void 0; + const inputBar = renderSlot("conversation.composer.bar", { + variant: hero ? "hero" : "composer", ++ extensionZone: zone, + ...inert ? { + disabled: true, + placeholder: t("placeholder.workspace"), +@@ -15268,12 +15274,13 @@ window.__ModuleLoader__.load({ + //#endregion + //#region \0dsh-css:/Users/alex/Documents/Code/AI/deepseek-harness/packages/client/ui-conversation/src/client/skeleton/InputBar.module.css.mjs + const css$1 = ".dYRH2G_root{padding:0 var(--dsh-composer-side-clearance) 8px;flex-direction:column;align-items:center;display:flex}.dYRH2G_hero{padding:0 var(--dsh-composer-side-clearance)}.dYRH2G_notice{width:100%;max-width:var(--dsh-composer-card-max-width);background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary);border-radius:8px;margin-bottom:6px;padding:4px 8px;font-size:12px;line-height:18px}.dYRH2G_card{box-sizing:border-box;width:100%;max-width:var(--dsh-composer-card-max-width);--dsw-elevation-stroke-color:var(--dsw-alias-border-l2);background:var(--dsw-specific-input-major);box-shadow:var(--dsw-elevation-soft);font-size:var(--dsh-content-font-size,14px);line-height:calc(24px + var(--dsh-content-font-delta,0px));--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2);border:0;border-radius:22px;flex-direction:column;gap:12px;padding-top:10px;display:flex;position:relative}.dYRH2G_cardWorkspaceTrigger{--dsw-elevation-stroke-color:transparent;cursor:pointer}.dYRH2G_cardWorkspaceTrigger:after{content:\"\";background:var(--dsw-alias-border-l4);pointer-events:none;border-radius:22px;transition:background-color .1s;position:absolute;inset:-1px;-webkit-mask:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Crect width='100%25' height='100%25' fill='none' rx='22' ry='22' stroke='black' stroke-width='2' stroke-dasharray='4 4'/%3E%3C/svg%3E\");mask:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Crect width='100%25' height='100%25' fill='none' rx='22' ry='22' stroke='black' stroke-width='2' stroke-dasharray='4 4'/%3E%3C/svg%3E\")}.dYRH2G_cardWorkspaceTrigger :disabled{pointer-events:none}.dYRH2G_cardWorkspaceTrigger:hover:after{background:var(--dsw-alias-state-business-primary)}.dYRH2G_accessory{align-items:center;gap:8px;padding:10px 12px 0;display:flex}.dYRH2G_overlayAnchor{height:0;position:absolute;inset:0 0 auto}.dYRH2G_scroll{max-height:var(--dsh-composer-text-max-height);margin-right:4px;overflow-y:auto}.dYRH2G_scroll::-webkit-scrollbar-track{margin-top:8px}.dYRH2G_grow{position:relative}.dYRH2G_pending{corner-shape:round;background:var(--dsw-alias-state-business-primary);border-radius:50%;width:8px;height:8px;animation:1s ease-in-out infinite alternate dYRH2G_input-pending}@keyframes dYRH2G_input-pending{0%{opacity:.35}to{opacity:1}}.dYRH2G_input{box-sizing:border-box;font-family:var(--dsw-font-family);font-size:inherit;line-height:inherit;white-space:pre-wrap;word-break:break-word;overflow-wrap:anywhere;color:var(--dsw-alias-label-primary);caret-color:var(--dsw-alias-state-business-primary);outline:none;padding:4px 8px 0 16px}.dYRH2G_input p{margin:0}.dYRH2G_input p:last-child:after{content:var(--dsh-composer-hint);color:var(--dsw-alias-label-caption)}.dYRH2G_placeholder{color:var(--dsw-alias-label-caption);pointer-events:none;user-select:none;position:absolute;inset:4px 8px auto 16px}.dYRH2G_inputDisabled{color:var(--dsw-alias-label-tertiary);cursor:not-allowed}.dYRH2G_input[aria-haspopup=menu]{cursor:pointer}.dYRH2G_hero .dYRH2G_input{min-height:52px}.dYRH2G_row{flex-wrap:wrap;justify-content:space-between;align-items:center;gap:12px;min-width:0;padding:2px 8px 6px;display:flex;container-type:inline-size}.dYRH2G_tools,.dYRH2G_modes,.dYRH2G_trailing{align-items:center;min-width:0;display:flex}.dYRH2G_tools{gap:16px}.dYRH2G_modes{gap:12px}.dYRH2G_trailing{flex:none;gap:12px;margin-left:auto}.dYRH2G_add{corner-shape:round;background:var(--dsw-specific-selector);width:28px;height:28px;color:var(--dsw-alias-label-primary);cursor:pointer;border:none;border-radius:999px;flex:none;place-items:center;display:grid}.dYRH2G_add:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover-solid)}.dYRH2G_add:disabled{opacity:.5;cursor:default}.dYRH2G_select{max-width:220px;height:28px;color:var(--dsw-alias-label-secondary);white-space:nowrap;cursor:pointer;appearance:none;background-color:#0000;background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12' fill='none'%3E%3Cpath d='M3 4.5L6 7.5L9 4.5' stroke='%2381858C' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E\");background-position:right 4px center;background-repeat:no-repeat;background-size:12px 12px;border:none;border-radius:8px;outline:none;padding:0 20px 0 8px;font-size:13px;font-weight:500;line-height:20px}.dYRH2G_select:hover:not(:disabled){background-color:var(--dsw-alias-interactive-bg-hover)}.dYRH2G_select:disabled{opacity:.5;cursor:default}.dYRH2G_primary{corner-shape:round;background:var(--dsw-alias-button-info-fill);color:#fff;cursor:pointer;border:none;border-radius:999px;flex:none;place-items:center;width:34px;height:34px;transition:background-color .1s;display:grid;transform:translateY(-2px)}.dYRH2G_primary:hover:not(:disabled){background:var(--dsw-alias-button-info-hover)}.dYRH2G_primary:disabled{opacity:.4;cursor:default}.dYRH2G_retry{color:inherit;cursor:pointer;background:0 0;border:1px solid;border-radius:4px;margin-left:8px;padding:1px 8px;font-size:12px}"; ++ const pptAccessoryCss = ".dYRH2G_promptRow{display:contents}.dYRH2G_promptRow:has(.dYRH2G_accessory [data-slot=conversation\\.input\\.accessory]:not(:empty)){align-items:flex-start;gap:12px;min-width:0;display:flex}.dYRH2G_accessory{align-items:flex-start;flex:0 0 auto;min-width:0;padding-left:16px;display:none}.dYRH2G_accessory:has([data-slot=conversation\\.input\\.accessory]:not(:empty)){display:flex}.dYRH2G_promptRow:has(.dYRH2G_accessory [data-slot=conversation\\.input\\.accessory]:not(:empty))>.dYRH2G_scroll{flex:1 1 auto;min-width:0}.dYRH2G_promptRow:has(.dYRH2G_accessory [data-slot=conversation\\.input\\.accessory]:not(:empty))>.dYRH2G_scroll .dYRH2G_input{padding-left:0}.dYRH2G_promptRow:has(.dYRH2G_accessory [data-slot=conversation\\.input\\.accessory]:not(:empty))>.dYRH2G_scroll .dYRH2G_placeholder{left:0}"; + const tagId$1 = "@deepseek-ai/dsh-client-ui-conversation/InputBar.module.css"; + if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) { + const tag = document.createElement("style"); + tag.dataset.plugin = "@deepseek-ai/dsh-client-ui-conversation"; + tag.dataset.pluginCss = tagId$1; +- tag.textContent = css$1; ++ tag.textContent = css$1 + pptAccessoryCss; + document.head.appendChild(tag); + } + var InputBar_module_css_default = { +@@ -15292,6 +15299,7 @@ window.__ModuleLoader__.load({ + "pending": "dYRH2G_pending", + "placeholder": "dYRH2G_placeholder", + "primary": "dYRH2G_primary", ++ "promptRow": "dYRH2G_promptRow", + "retry": "dYRH2G_retry", + "root": "dYRH2G_root", + "row": "dYRH2G_row", +@@ -15316,7 +15324,7 @@ window.__ModuleLoader__.load({ + * The no-session state renders the SAME div inert as the Workspace-picker + * trigger instead of a parallel tree. + */ +- const InputBar = (0, react.memo)(function InputBar({ useSession, useInput, inputActions, keyboard, addImages, removeImage, draftImages, resolveSubmitMode, toggleCommandMenu, stop, command, t, renderSlot, useNotices, useLexicon, useMenuLauncher, useProjection, sessionId, variant, disabled: inert = false, blocked, workspacePickerOpen = false, onRequestWorkspace, placeholder, accessory }) { ++ const InputBar = (0, react.memo)(function InputBar({ useSession, useInput, inputActions, keyboard, addImages, removeImage, draftImages, resolveSubmitMode, toggleCommandMenu, stop, command, t, renderSlot, useNotices, useLexicon, useMenuLauncher, useProjection, sessionId, variant, disabled: inert = false, blocked, workspacePickerOpen = false, onRequestWorkspace, placeholder, accessory, extensionZone }) { + const input = useInput((s) => s); + const notice = useNotices((s) => s); + const commandMenuOpen = useMenuLauncher((source) => source === "command"); +@@ -15562,10 +15570,6 @@ window.__ModuleLoader__.load({ + className: InputBar_module_css_default.overlayAnchor, + children: renderSlot("conversation.input.overlay", {}) + }), +- accessory !== void 0 && (0, react_jsx_runtime.jsx)("div", { +- className: InputBar_module_css_default.accessory, +- children: accessory +- }), + renderSlot("conversation.input.attachments", { + attachments, + canAcceptDrop, +@@ -15578,13 +15582,19 @@ window.__ModuleLoader__.load({ + size: imageSizeText(imageLimits.maxImageBytes) + } + }), +- (0, react_jsx_runtime.jsx)("div", { +- ref: scrollRef, +- className: InputBar_module_css_default.scroll, +- "data-input-scroll": true, +- children: (0, react_jsx_runtime.jsxs)("div", { +- className: InputBar_module_css_default.grow, +- children: [ ++ (0, react_jsx_runtime.jsxs)("div", { ++ className: InputBar_module_css_default.promptRow, ++ "data-input-prompt-row": true, ++ children: [(accessory !== void 0 || extensionZone !== void 0) && (0, react_jsx_runtime.jsx)("div", { ++ className: InputBar_module_css_default.accessory, ++ children: accessory ?? renderSlot("conversation.input.accessory", extensionZone) ++ }), (0, react_jsx_runtime.jsx)("div", { ++ ref: scrollRef, ++ className: InputBar_module_css_default.scroll, ++ "data-input-scroll": true, ++ children: (0, react_jsx_runtime.jsxs)("div", { ++ className: InputBar_module_css_default.grow, ++ children: [ + (0, react_jsx_runtime.jsx)(ComposerContentEditable, { + editor: workspaceTrigger ? null : editor, + editable, +@@ -15606,8 +15616,9 @@ window.__ModuleLoader__.load({ + children: placeholderText + }), + (0, react_jsx_runtime.jsx)(DecoratorPortals, { editor: workspaceTrigger ? null : editor }) +- ] +- }) ++ ] ++ }) ++ })] + }), + (0, react_jsx_runtime.jsxs)("div", { + className: InputBar_module_css_default.row, +@@ -15713,7 +15724,7 @@ window.__ModuleLoader__.load({ + }) + ] + }), +- variant === "composer" && input !== void 0 && sessionId !== void 0 ? renderSlot("conversation.composer.dock", {}) : null ++ extensionZone !== void 0 ? renderSlot("conversation.composer.dock", extensionZone) : null + ] + }); + }); +@@ -16089,6 +16100,10 @@ window.__ModuleLoader__.load({ + "conversation.hero.agentPreset": { + kind: "single", + scope: "root" ++ }, ++ "conversation.hero.modeActions": { ++ kind: "list", ++ scope: "session" + } + }, + inject: (sessionId) => ({ +@@ -16169,6 +16184,10 @@ window.__ModuleLoader__.load({ + kind: "list", + scope: "session" + }, ++ "conversation.input.accessory": { ++ kind: "list", ++ scope: "session" ++ }, + "conversation.input.left": { + kind: "list", + scope: "session" +diff --git a/node_modules/@deepseek-ai/dsh-client-ui-conversation/lib/types/client/contract/slots.d.ts b/node_modules/@deepseek-ai/dsh-client-ui-conversation/lib/types/client/contract/slots.d.ts +index 2d41cda..70cdce4 100644 +--- a/node_modules/@deepseek-ai/dsh-client-ui-conversation/lib/types/client/contract/slots.d.ts ++++ b/node_modules/@deepseek-ai/dsh-client-ui-conversation/lib/types/client/contract/slots.d.ts +@@ -137,6 +137,12 @@ declare module '@deepseek-ai/dsh-client-ui-slots' { + scope: 'root'; + owner: HeroAgentPresetOwnerProps; + }; ++ /** Product-mode controls beside the blank-session agent preset. */ ++ 'conversation.hero.modeActions': { ++ kind: 'list'; ++ scope: 'session'; ++ owner: InputZone; ++ }; + /** Full-width entries above the composer card. */ + 'conversation.input.dock': { + kind: 'list'; +@@ -148,10 +154,17 @@ declare module '@deepseek-ai/dsh-client-ui-slots' { + kind: 'list'; + scope: 'session'; + }; ++ /** Compact template and reference material rendered before the editable prompt. */ ++ 'conversation.input.accessory': { ++ kind: 'list'; ++ scope: 'session'; ++ owner: InputZone; ++ }; + /** Ambient entries below the composer card. */ + 'conversation.composer.dock': { + kind: 'list'; + scope: 'session'; ++ owner: InputZone; + }; + /** Compact controls at the left of the composer tool row. */ + 'conversation.input.left': { +@@ -290,8 +303,10 @@ export interface ComposerBarOwnerProps { + /** Open the Workspace picker from the inert composer surface. */ + onRequestWorkspace?: () => void; + placeholder?: string; +- /** Optional content rendered above the composer surface. */ ++ /** Optional compact content rendered before the editable prompt. */ + accessory?: ReactNode; ++ /** Point-in-time Session and input state supplied to Composer extension slots. */ ++ extensionZone?: InputZone; + } + /** Package-private operations injected into the resident composer bar. */ + export interface ComposerBarInjected { +@@ -315,7 +330,7 @@ export interface InputControlOwnerProps { + locked: boolean; + } + /** Full props of the resident composer bar. */ +-export type ComposerBarProps = PropsRuntime<'conversation.composer.bar'> & PropsRenderSlots<'conversation.input.attachments' | 'conversation.input.overlay' | 'conversation.input.left' | 'conversation.input.plan' | 'conversation.input.right' | 'conversation.input.model' | 'conversation.composer.dock'> & InjectFace & PropsLocale<'conversation'>; ++export type ComposerBarProps = PropsRuntime<'conversation.composer.bar'> & PropsRenderSlots<'conversation.input.attachments' | 'conversation.input.overlay' | 'conversation.input.accessory' | 'conversation.input.left' | 'conversation.input.plan' | 'conversation.input.right' | 'conversation.input.model' | 'conversation.composer.dock'> & InjectFace & PropsLocale<'conversation'>; + /** Owner values used to elect a composer takeover. */ + export interface ComposerChainProps { + /** Current Session identity used by temporary business-owned entries. */ +@@ -333,7 +348,7 @@ export interface HeroBrandMarkOwnerProps { + className?: string | undefined; + } + /** Full props of the resident optional-Session Conversation shell. */ +-export type ConversationSlotProps = PropsRuntime<'conversation'> & PropsRenderSlots<'conversation.session' | 'conversation.session.header' | 'conversation.composer' | 'conversation.composer.bar' | 'conversation.input.dock' | 'conversation.hero.brand.mark' | 'conversation.hero.workspace' | 'conversation.hero.agentPreset'> & InjectFace & PropsLocale<'conversation'>; ++export type ConversationSlotProps = PropsRuntime<'conversation'> & PropsRenderSlots<'conversation.session' | 'conversation.session.header' | 'conversation.composer' | 'conversation.composer.bar' | 'conversation.input.dock' | 'conversation.hero.brand.mark' | 'conversation.hero.workspace' | 'conversation.hero.agentPreset' | 'conversation.hero.modeActions'> & InjectFace & PropsLocale<'conversation'>; + /** Shared target-neutral Conversation store handle. */ + export type ConversationStore = ReturnType; + /** Full props of the strict Session body. */ diff --git a/scripts/ensure-workbuddy-ppt-runtime.mjs b/scripts/ensure-workbuddy-ppt-runtime.mjs new file mode 100644 index 00000000..6a0c9edd --- /dev/null +++ b/scripts/ensure-workbuddy-ppt-runtime.mjs @@ -0,0 +1,235 @@ +import { createHash } from 'node:crypto' +import { execFile } from 'node:child_process' +import { createReadStream } from 'node:fs' +import { + access, + mkdir, + mkdtemp, + open, + readFile, + rename, + rm, + stat +} from 'node:fs/promises' +import os from 'node:os' +import path from 'node:path' +import { finished } from 'node:stream/promises' +import { fileURLToPath } from 'node:url' +import { promisify } from 'node:util' +import { verifyWorkBuddyPptRuntime } from './verify-workbuddy-ppt-runtime.mjs' + +const execFileAsync = promisify(execFile) +const DEFAULT_DISTRIBUTION_PATH = fileURLToPath( + new URL('../packages/workbuddy-ppt/runtime-distribution.json', import.meta.url) +) +const DEFAULT_LOCK_PATH = fileURLToPath( + new URL('../packages/workbuddy-ppt/runtime-lock.json', import.meta.url) +) + +function platformName(platform, arch) { + return `${platform}-${arch}` +} + +function defaultCacheRoot(platform, environment) { + if (environment.DSH_WORKBUDDY_PPT_RUNTIME_CACHE) { + return path.resolve(environment.DSH_WORKBUDDY_PPT_RUNTIME_CACHE) + } + if (platform === 'darwin') { + return path.join(os.homedir(), 'Library', 'Caches', 'dsh-desktop', 'ppt-capability') + } + if (platform === 'win32') { + const localAppData = environment.LOCALAPPDATA + if (localAppData) return path.join(localAppData, 'dsh-desktop', 'ppt-capability') + } + const xdgCache = environment.XDG_CACHE_HOME + return path.join(xdgCache || path.join(os.homedir(), '.cache'), 'dsh-desktop', 'ppt-capability') +} + +async function sha256File(filePath) { + const hash = createHash('sha256') + const stream = createReadStream(filePath) + stream.on('data', chunk => hash.update(chunk)) + await finished(stream) + return hash.digest('hex') +} + +function assertSecureDownloadUrl(value) { + const parsed = new URL(value) + if (parsed.protocol !== 'https:') { + throw new Error(`PPT Capability runtime download must use HTTPS: ${value}`) + } + return parsed.toString() +} + +export async function downloadRuntimeArchive(url, destination, options = {}) { + const fetchImpl = options.fetchImpl ?? globalThis.fetch + const output = options.output ?? process.stdout + const response = await fetchImpl(assertSecureDownloadUrl(url), { + redirect: 'follow', + signal: AbortSignal.timeout(15 * 60 * 1000) + }) + if (!response.ok || !response.body) { + throw new Error(`PPT Capability runtime download failed: HTTP ${response.status}`) + } + const handle = await open(destination, 'wx') + let received = 0 + let nextProgress = 32 * 1024 * 1024 + try { + for await (const value of response.body) { + const chunk = Buffer.from(value) + let offset = 0 + while (offset < chunk.byteLength) { + const { bytesWritten } = await handle.write(chunk, offset, chunk.byteLength - offset) + if (bytesWritten === 0) throw new Error('PPT Capability runtime archive write stopped early') + offset += bytesWritten + } + received += chunk.byteLength + if (received >= nextProgress) { + output.write(`PPT Capability runtime download: ${Math.round(received / 1024 / 1024)} MiB\n`) + nextProgress += 32 * 1024 * 1024 + } + } + } finally { + await handle.close() + } + return received +} + +function validateArchiveEntries(entries) { + for (const rawEntry of entries.split('\n')) { + const entry = rawEntry.trim().replace(/^\.\//u, '').replace(/\/$/u, '') + if (!entry) continue + const segments = entry.split('/') + if (path.posix.isAbsolute(entry) || entry.includes('\\') || segments.includes('..')) { + throw new Error(`PPT Capability runtime archive contains an unsafe path: ${rawEntry}`) + } + } +} + +export async function extractRuntimeArchive(archivePath, destination) { + const listed = await execFileAsync('tar', ['-tzf', archivePath], { maxBuffer: 64 * 1024 * 1024 }) + validateArchiveEntries(listed.stdout) + await mkdir(destination, { recursive: true }) + await execFileAsync('tar', ['-xzf', archivePath, '-C', destination], { + maxBuffer: 16 * 1024 * 1024 + }) +} + +async function downloadWithRetry(url, destination, downloadArchive, output) { + let lastError + for (let attempt = 1; attempt <= 3; attempt += 1) { + await rm(destination, { force: true }) + try { + return await downloadArchive(url, destination, { output }) + } catch (error) { + lastError = error + if (attempt < 3) output.write(`PPT Capability runtime download retry ${attempt + 1}/3\n`) + } + } + throw lastError +} + +export async function ensureWorkBuddyPptRuntime(options = {}) { + const platform = options.platform ?? process.platform + const arch = options.arch ?? process.arch + const environment = options.environment ?? process.env + const output = options.output ?? process.stdout + const distributionPath = options.distributionPath ?? DEFAULT_DISTRIBUTION_PATH + const lockPath = options.lockPath ?? DEFAULT_LOCK_PATH + const verifyRuntime = options.verifyRuntime ?? verifyWorkBuddyPptRuntime + const downloadArchive = options.downloadArchive ?? downloadRuntimeArchive + const extractArchive = options.extractArchive ?? extractRuntimeArchive + const configuredRoot = environment.DSH_WORKBUDDY_PPT_RUNTIME_ROOT?.trim() + + if (configuredRoot) { + if (!path.isAbsolute(configuredRoot)) { + throw new Error('DSH_WORKBUDDY_PPT_RUNTIME_ROOT must be absolute') + } + await verifyRuntime(configuredRoot, platform, arch, lockPath) + output.write(`PPT Capability runtime: ${configuredRoot}\n`) + return configuredRoot + } + + const targetPlatform = platformName(platform, arch) + const distribution = JSON.parse(await readFile(distributionPath, 'utf8')) + if (distribution.schemaVersion !== 1) { + throw new Error(`Unsupported PPT Capability runtime distribution schema: ${distribution.schemaVersion}`) + } + const descriptor = distribution.platforms?.[targetPlatform] + if (!descriptor) { + if (options.optional) { + output.write(`PPT Capability runtime is not published for ${targetPlatform}; continuing without Slides.\n`) + return undefined + } + throw new Error(`PPT Capability runtime is not published for ${targetPlatform}`) + } + if (descriptor.archive !== 'tar.gz') { + throw new Error(`Unsupported PPT Capability runtime archive: ${descriptor.archive}`) + } + if (!Number.isSafeInteger(descriptor.archiveSize) || descriptor.archiveSize <= 0) { + throw new Error('PPT Capability runtime archive has an invalid size lock') + } + if (!/^[0-9a-f]{64}$/u.test(descriptor.archiveSha256)) { + throw new Error('PPT Capability runtime archive has an invalid SHA-256 lock') + } + + const lock = JSON.parse(await readFile(lockPath, 'utf8')) + const platformLock = lock.platforms?.[targetPlatform] + if (!platformLock) throw new Error(`PPT Capability runtime lock has no ${targetPlatform} entry`) + if (!/^[0-9a-f]{64}$/u.test(platformLock.runtimeTree?.sha256)) { + throw new Error('PPT Capability runtime tree has an invalid SHA-256 lock') + } + const cacheRoot = options.cacheRoot ?? defaultCacheRoot(platform, environment) + const platformCache = path.join(cacheRoot, targetPlatform) + const runtimeRoot = path.join(platformCache, platformLock.runtimeTree.sha256) + + try { + await access(path.join(runtimeRoot, 'manifest.json')) + await verifyRuntime(runtimeRoot, platform, arch, lockPath) + output.write(`PPT Capability runtime cache: ${runtimeRoot}\n`) + return runtimeRoot + } catch (error) { + if (error?.code !== 'ENOENT') throw error + } + + await mkdir(platformCache, { recursive: true }) + const stagingRoot = await mkdtemp(path.join(platformCache, '.install-')) + const archivePath = path.join(stagingRoot, 'runtime.tar.gz') + const extractedRoot = path.join(stagingRoot, 'runtime') + const url = environment.DSH_WORKBUDDY_PPT_RUNTIME_URL?.trim() || descriptor.url + + try { + output.write(`Downloading PPT Capability runtime for ${targetPlatform}…\n`) + const received = await downloadWithRetry(url, archivePath, downloadArchive, output) + const archiveStats = await stat(archivePath) + if (received !== descriptor.archiveSize || archiveStats.size !== descriptor.archiveSize) { + throw new Error( + `PPT Capability runtime archive size ${archiveStats.size} does not match locked ${descriptor.archiveSize}` + ) + } + const archiveSha256 = await sha256File(archivePath) + if (archiveSha256 !== descriptor.archiveSha256) { + throw new Error('PPT Capability runtime archive SHA-256 mismatch') + } + await extractArchive(archivePath, extractedRoot) + await verifyRuntime(extractedRoot, platform, arch, lockPath) + try { + await rename(extractedRoot, runtimeRoot) + } catch (error) { + if (error?.code !== 'EEXIST' && error?.code !== 'ENOTEMPTY') throw error + await verifyRuntime(runtimeRoot, platform, arch, lockPath) + } + output.write(`PPT Capability runtime installed: ${runtimeRoot}\n`) + return runtimeRoot + } finally { + await rm(stagingRoot, { recursive: true, force: true }) + } +} + +async function main() { + await ensureWorkBuddyPptRuntime({ optional: process.argv.includes('--optional') }) +} + +if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + await main() +} diff --git a/scripts/run-with-workbuddy-ppt-runtime.mjs b/scripts/run-with-workbuddy-ppt-runtime.mjs new file mode 100644 index 00000000..a7ca1acc --- /dev/null +++ b/scripts/run-with-workbuddy-ppt-runtime.mjs @@ -0,0 +1,51 @@ +import { spawn } from 'node:child_process' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import { ensureWorkBuddyPptRuntime } from './ensure-workbuddy-ppt-runtime.mjs' + +export async function runWithWorkBuddyPptRuntime(command, args, options = {}) { + if (!command) throw new Error('A command is required after run-with-workbuddy-ppt-runtime.mjs') + const runtimeRoot = await ensureWorkBuddyPptRuntime(options.ensureOptions) + const environment = { + ...process.env, + ...options.environment + } + if (runtimeRoot) environment.DSH_WORKBUDDY_PPT_RUNTIME_ROOT = runtimeRoot + const child = spawn(command, args, { + cwd: options.cwd ?? process.cwd(), + env: environment, + stdio: 'inherit', + shell: process.platform === 'win32' + }) + + const forwardSignal = signal => child.kill(signal) + process.on('SIGINT', forwardSignal) + process.on('SIGTERM', forwardSignal) + + try { + return await new Promise((resolve, reject) => { + child.once('error', reject) + child.once('exit', (code, signal) => { + if (signal) return resolve(1) + resolve(code ?? 1) + }) + }) + } finally { + process.off('SIGINT', forwardSignal) + process.off('SIGTERM', forwardSignal) + } +} + +async function main() { + const rawArgs = process.argv.slice(2) + const optional = rawArgs[0] === '--runtime-optional' + const [command, ...args] = optional ? rawArgs.slice(1) : rawArgs + const exitCode = await runWithWorkBuddyPptRuntime(command, args, { + ensureOptions: { optional } + }) + process.exitCode = exitCode +} + +if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + await main() +} diff --git a/scripts/verify-workbuddy-ppt-runtime.mjs b/scripts/verify-workbuddy-ppt-runtime.mjs new file mode 100644 index 00000000..dc064bec --- /dev/null +++ b/scripts/verify-workbuddy-ppt-runtime.mjs @@ -0,0 +1,136 @@ +import { access, readFile, readdir, stat } from 'node:fs/promises' +import { createReadStream } from 'node:fs' +import { createHash } from 'node:crypto' +import { finished } from 'node:stream/promises' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const REQUIRED_SKILL_FILES = [ + 'skills/tencent-pptx/SKILL.md', + 'skills/tencent-pptx/references/create-from-scratch.md', + 'skills/tencent-pptx/references/create-from-material.md' +] +const REQUIRED_SLIDEP_FILES = [ + 'slidep/package.json', + 'slidep/dist/slidep-start.js', + 'slidep/dist/slidep-validate.js', + 'slidep/node_modules' +] + +function platformName(platform, arch) { + return `${platform}-${arch}` +} + +const DEFAULT_LOCK_PATH = fileURLToPath(new URL('../packages/workbuddy-ppt/runtime-lock.json', import.meta.url)) + +async function sha256File(file) { + const hash = createHash('sha256') + const stream = createReadStream(file) + stream.on('data', chunk => hash.update(chunk)) + await finished(stream) + return hash.digest('hex') +} + +async function runtimeFiles(root, current = root) { + const entries = await readdir(current, { withFileTypes: true }) + const files = [] + for (const entry of entries) { + if (entry.name === '.DS_Store') continue + const absolute = path.join(current, entry.name) + if (entry.isDirectory()) { + files.push(...await runtimeFiles(root, absolute)) + } else if (entry.isFile()) { + files.push(path.relative(root, absolute).split(path.sep).join('/')) + } else { + throw new Error(`WorkBuddy PPT runtime contains an unsupported entry: ${absolute}`) + } + } + return files +} + +/** Return a deterministic digest for every packaged runtime file except Finder metadata. */ +export async function runtimeTreeDigest(root) { + const files = (await runtimeFiles(root)).sort((left, right) => left.localeCompare(right, 'en')) + const hash = createHash('sha256') + for (const relative of files) { + hash.update(relative) + hash.update('\0') + hash.update(await sha256File(path.join(root, relative))) + hash.update('\n') + } + return { fileCount: files.length, sha256: hash.digest('hex') } +} + +export async function verifyWorkBuddyPptRuntime( + root, + platform = process.platform, + arch = process.arch, + lockPath = DEFAULT_LOCK_PATH +) { + if (!path.isAbsolute(root)) throw new Error('DSH_WORKBUDDY_PPT_RUNTIME_ROOT must be absolute') + const manifest = JSON.parse(await readFile(path.join(root, 'manifest.json'), 'utf8')) + const expectedPlatform = platformName(platform, arch) + if (manifest.platform !== expectedPlatform) { + throw new Error(`WorkBuddy PPT runtime platform ${manifest.platform} does not match ${expectedPlatform}`) + } + const editorBinary = platform === 'win32' ? 'editor_sdk.exe' : 'editor_sdk' + const required = [ + ...REQUIRED_SKILL_FILES, + ...REQUIRED_SLIDEP_FILES, + `tencent-docs-ai-engine/bin/${expectedPlatform}/${editorBinary}`, + `tencent-docs-ai-engine/bin/${expectedPlatform}/icudt72l.dat` + ] + await Promise.all(required.map(relative => access(path.join(root, relative)))) + if (!/^[0-9a-f]{64}$/u.test(manifest.skillSha256)) { + throw new Error('WorkBuddy PPT runtime manifest has an invalid Skill SHA-256') + } + const lock = JSON.parse(await readFile(lockPath, 'utf8')) + const platformLock = lock.platforms?.[expectedPlatform] + if (!platformLock) throw new Error(`WorkBuddy PPT runtime lock has no ${expectedPlatform} entry`) + if (platformLock.slidep !== manifest.slidep.version) { + throw new Error(`WorkBuddy PPT SlideP ${manifest.slidep.version} does not match locked ${platformLock.slidep}`) + } + if (platformLock.editorEngine !== manifest.editorEngine.version) { + throw new Error(`WorkBuddy PPT editor engine ${manifest.editorEngine.version} does not match locked ${platformLock.editorEngine}`) + } + for (const [relative, expected] of Object.entries(platformLock.files)) { + const actual = await sha256File(path.join(root, relative)) + if (actual !== expected) { + throw new Error(`WorkBuddy PPT runtime SHA-256 mismatch: ${relative}`) + } + } + const tree = await runtimeTreeDigest(root) + if (tree.fileCount !== platformLock.runtimeTree.fileCount) { + throw new Error(`WorkBuddy PPT runtime file count ${tree.fileCount} does not match locked ${platformLock.runtimeTree.fileCount}`) + } + if (tree.sha256 !== platformLock.runtimeTree.sha256) { + throw new Error('WorkBuddy PPT runtime tree SHA-256 mismatch') + } + // Windows does not expose POSIX executable bits, including for a fixture or + // cross-target runtime. Native macOS/Linux packaging still enforces them. + if (platform !== 'win32' && process.platform !== 'win32') { + const mode = (await stat(path.join(root, `tencent-docs-ai-engine/bin/${expectedPlatform}/${editorBinary}`))).mode + if ((mode & 0o111) === 0) throw new Error('WorkBuddy PPT editor_sdk must be executable') + } + return manifest +} + +async function main() { + const root = process.env.DSH_WORKBUDDY_PPT_RUNTIME_ROOT + if (!root && process.argv.includes('--optional')) { + process.stdout.write('PPT Capability runtime is not configured; packaging this platform with the Slides plugin disabled.\n') + return + } + if (!root) throw new Error('DSH_WORKBUDDY_PPT_RUNTIME_ROOT is required for Desktop packaging') + const manifest = await verifyWorkBuddyPptRuntime(root) + process.stdout.write([ + `WorkBuddy PPT runtime verified: ${root}`, + `${manifest.slidep.name}@${manifest.slidep.version}`, + `${manifest.editorEngine.name}@${manifest.editorEngine.version}`, + manifest.platform + ].join(' · ') + '\n') +} + +if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + await main() +} diff --git a/src/main/index.ts b/src/main/index.ts index 729cfbbf..03576229 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -125,6 +125,7 @@ import { buildSafeModeViewModel, shouldStartInSafeMode } from './safe-mode' import { aboutDetail, bundledHarnessVersion } from './version-info' import { windowsMenuViewBounds } from './windows-menu-view' import { shouldKeepRunningInBackground } from './close-to-tray' +import { workBuddyPptRuntimeEnvironment } from './runtime/workbuddy-ppt-resource' import { MAIN_WINDOW_RECOVERY_RELOAD_COOLDOWN_MS, shouldReloadAfterMainWindowRendererLoss @@ -2411,6 +2412,10 @@ async function bootstrap(): Promise { nativeTheme.themeSource = harnessThemePreference() ensureTray() createWindow() + const workBuddyPptEnvironment = workBuddyPptRuntimeEnvironment( + process.resourcesPath, + app.isPackaged + ) runtime = new HarnessRuntime({ dshEntryPath: dshEntryPath(), nodeExecutablePath: bundledNodePath(), @@ -2418,6 +2423,9 @@ async function bootstrap(): Promise { dshPatchPath: desktopResourcePath('dsh-desktop.patch.yml'), dshHome: join(app.getPath('userData'), 'harness'), logPath: join(app.getPath('logs'), 'harness.log'), + ...(workBuddyPptEnvironment === undefined + ? {} + : { environment: workBuddyPptEnvironment }), launchProcess: (executablePath, args, options) => process.platform === 'darwin' ? launchDisclaimedUtilityProcess(utilityProcess, args, options, { diff --git a/src/main/runtime/harness-runtime.ts b/src/main/runtime/harness-runtime.ts index 0ea3ca15..aab70e7b 100644 --- a/src/main/runtime/harness-runtime.ts +++ b/src/main/runtime/harness-runtime.ts @@ -13,6 +13,7 @@ export interface HarnessRuntimeOptions { dshPatchPath: string dshHome: string logPath: string + environment?: NodeJS.ProcessEnv launchProcess( executablePath: string, args: string[], @@ -232,9 +233,11 @@ export function buildHarnessSpawnOptions( launchDirectory: string, dshHome: string, platform: NodeJS.Platform = process.platform, - environment: NodeJS.ProcessEnv = process.env + environment: NodeJS.ProcessEnv = process.env, + injectedEnvironment: NodeJS.ProcessEnv = {} ): SpawnOptionsWithoutStdio { - const { ELECTRON_RUN_AS_NODE: _runAsNode, ...parentEnvironment } = environment + const mergedEnvironment = { ...environment, ...injectedEnvironment } + const { ELECTRON_RUN_AS_NODE: _runAsNode, ...parentEnvironment } = mergedEnvironment const pathKey = platform === 'win32' ? 'Path' : 'PATH' // ELECTRON_RUN_AS_NODE must not reach the Harness process itself: the macOS @@ -264,7 +267,7 @@ export function buildHarnessSpawnOptions( // the dedicated lock-recovery runner instead (see pnpm-runner.mjs). npm_config_side_effects_cache: 'false', PNPM_CONFIG_SIDE_EFFECTS_CACHE: 'false', - [pathKey]: resolveEnvironmentPath(environment, platform) + [pathKey]: resolveEnvironmentPath(mergedEnvironment, platform) }, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true, @@ -400,7 +403,8 @@ export class HarnessRuntime { launchDirectory, this.options.dshHome, process.platform, - resolveShellEnvironment() + resolveShellEnvironment(), + this.options.environment ) ) } catch (error) { diff --git a/src/main/runtime/workbuddy-ppt-resource.ts b/src/main/runtime/workbuddy-ppt-resource.ts new file mode 100644 index 00000000..c08032ff --- /dev/null +++ b/src/main/runtime/workbuddy-ppt-resource.ts @@ -0,0 +1,38 @@ +import { existsSync } from 'node:fs' +import { isAbsolute, join } from 'node:path' + +const PPT_RUNTIME_RESOURCE = 'workbuddy-ppt-runtime' + +export type WorkBuddyPptRuntimeEnvironment = { + DSH_WORKBUDDY_PPT_RUNTIME_ROOT: string +} + +/** + * Return the Harness environment only when this platform package contains the + * complete PPT runtime manifest. + */ +export function bundledWorkBuddyPptRuntimeEnvironment( + resourcesPath: string, + fileExists: (path: string) => boolean = existsSync +): WorkBuddyPptRuntimeEnvironment | undefined { + const runtimeRoot = join(resourcesPath, PPT_RUNTIME_RESOURCE) + if (!fileExists(join(runtimeRoot, 'manifest.json'))) return undefined + return { DSH_WORKBUDDY_PPT_RUNTIME_ROOT: runtimeRoot } +} + +/** + * Packaged builds use their immutable resource tree. Development builds accept + * only the absolute, verified cache root supplied by the runtime launcher. + */ +export function workBuddyPptRuntimeEnvironment( + resourcesPath: string, + isPackaged: boolean, + environment: NodeJS.ProcessEnv = process.env, + fileExists: (path: string) => boolean = existsSync +): WorkBuddyPptRuntimeEnvironment | undefined { + if (isPackaged) return bundledWorkBuddyPptRuntimeEnvironment(resourcesPath, fileExists) + const runtimeRoot = environment.DSH_WORKBUDDY_PPT_RUNTIME_ROOT?.trim() + if (!runtimeRoot || !isAbsolute(runtimeRoot)) return undefined + if (!fileExists(join(runtimeRoot, 'manifest.json'))) return undefined + return { DSH_WORKBUDDY_PPT_RUNTIME_ROOT: runtimeRoot } +} diff --git a/test/desktop-plugin-closure.test.ts b/test/desktop-plugin-closure.test.ts index cfdcfd87..c0f6613c 100644 --- a/test/desktop-plugin-closure.test.ts +++ b/test/desktop-plugin-closure.test.ts @@ -4,6 +4,20 @@ import { describe, expect, it } from 'vitest' import { parse as parseYaml } from 'yaml' import { patchPath, projectRoot } from './patch-path' +const profileConfigTags = [ + { + tag: 'tag:yaml.org,2002:js', + resolve: (value: string) => value + } +] + +async function readProfilePatch(): Promise<{ name?: string; insert?: { name?: string }[] }[]> { + return parseYaml( + await readFile(path.join(projectRoot, 'build', 'dsh-desktop.patch.yml'), 'utf8'), + { customTags: profileConfigTags } + ) as { name?: string; insert?: { name?: string }[] }[] +} + /** * Every desktop plugin the composed profile mounts has to be reachable from * the profile directory, and the profile does not resolve through the app's @@ -18,15 +32,13 @@ import { patchPath, projectRoot } from './patch-path' * whole plugin tree rather than just that row. */ describe('desktop plugin closure', () => { - it('injects every profile-mounted desktop plugin into the dsh dependency closure', async () => { - const profilePatch = parseYaml( - await readFile(path.join(projectRoot, 'build', 'dsh-desktop.patch.yml'), 'utf8') - ) as { name?: string; insert?: { name?: string }[] }[] + it('injects every profile-mounted package into the dsh dependency closure', async () => { + const profilePatch = await readProfilePatch() const mounted = profilePatch - .flatMap((row) => [...(row.insert ?? []), row]) + .flatMap((row) => row.insert ?? []) .map((entry) => entry.name) - .filter((name): name is string => typeof name === 'string' && name.startsWith('dsh-desktop-')) + .filter((name): name is string => typeof name === 'string') expect(mounted.length).toBeGreaterThan(0) @@ -36,18 +48,16 @@ describe('desktop plugin closure', () => { } }) - it('declares every profile-mounted desktop plugin as a production dependency', async () => { - const profilePatch = parseYaml( - await readFile(path.join(projectRoot, 'build', 'dsh-desktop.patch.yml'), 'utf8') - ) as { name?: string; insert?: { name?: string }[] }[] + it('declares every profile-mounted package as a local production dependency', async () => { + const profilePatch = await readProfilePatch() const manifest = JSON.parse( await readFile(path.join(projectRoot, 'package.json'), 'utf8') ) as { dependencies: Record } - for (const row of profilePatch.flatMap((entry) => [...(entry.insert ?? []), entry])) { - if (typeof row.name !== 'string' || !row.name.startsWith('dsh-desktop-')) continue - expect(manifest.dependencies[row.name]).toBe(`file:packages/${row.name}`) + for (const row of profilePatch.flatMap((entry) => entry.insert ?? [])) { + if (typeof row.name !== 'string') continue + expect(manifest.dependencies[row.name]).toMatch(/^file:packages\//u) } }) }) diff --git a/test/release.test.ts b/test/release.test.ts index 64f386bb..3086eb5f 100644 --- a/test/release.test.ts +++ b/test/release.test.ts @@ -305,7 +305,8 @@ describe('GitHub release contract', () => { 'package:dev:mac:x64', 'package:dev:win' ]) { - expect(packageJson.scripts[script]).toContain('--publish never') + const builderScript = packageJson.scripts[`${script}:runtime`] ?? packageJson.scripts[script] + expect(builderScript).toContain('--publish never') } }) @@ -317,18 +318,39 @@ describe('GitHub release contract', () => { path.join(projectRoot, 'electron-builder.dev.cjs'), 'utf8' ) + const baseConfig = await readFile(path.join(projectRoot, 'electron-builder.cjs'), 'utf8') const main = await readFile(path.join(projectRoot, 'src', 'main', 'index.ts'), 'utf8') - expect(packageJson.scripts['package:dev:dir']).toContain('npm run build') - expect(packageJson.scripts['package:dev:dir']).toContain('electron-builder.dev.cjs') - expect(packageJson.scripts['package:dev:mac:arm64']).toContain('verify-target.mjs darwin arm64') - expect(packageJson.scripts['package:dev:mac:arm64']).toContain('electron-builder.dev.cjs') + expect(packageJson.scripts.dev).toContain('run-with-workbuddy-ppt-runtime.mjs') + expect(packageJson.scripts['runtime:ensure']).toContain('ensure-workbuddy-ppt-runtime.mjs') + expect(packageJson.scripts['package:dev:dir']).toContain('run-with-workbuddy-ppt-runtime.mjs') + expect(packageJson.scripts['package:dev:dir:runtime']).toContain('npm run build') + expect(packageJson.scripts['package:dev:dir:runtime']).toContain('npm run runtime:verify') + expect(packageJson.scripts['package:dev:dir:runtime']).toContain('electron-builder.dev.cjs') + expect(packageJson.scripts['package:dev:mac:arm64']).toContain('run-with-workbuddy-ppt-runtime.mjs') + expect(packageJson.scripts['package:dev:mac:arm64:runtime']).toContain('verify-target.mjs darwin arm64') + expect(packageJson.scripts['package:dev:mac:arm64:runtime']).toContain('npm run runtime:verify') + expect(packageJson.scripts['package:dev:mac:arm64:runtime']).not.toContain('runtime:verify:optional') + expect(packageJson.scripts['package:dev:mac:arm64:runtime']).toContain('electron-builder.dev.cjs') expect(packageJson.scripts['package:dev:mac:x64']).toContain('verify-target.mjs darwin x64') + expect(packageJson.scripts['package:dev:mac:x64']).toContain('runtime:verify:optional') expect(packageJson.scripts['package:dev:mac:x64']).toContain('electron-builder.dev.cjs') expect(packageJson.scripts['package:dev:win']).toContain('verify-target.mjs win32 x64') + expect(packageJson.scripts['package:dev:win']).toContain('runtime:verify:optional') expect(packageJson.scripts['package:dev:win']).toContain('electron-builder.dev.cjs') expect(packageJson.scripts['package:dev:win']).toContain('--publish never') expect(developmentConfig).toContain("appId: 'io.dsh.desktop.dev'") + expect(developmentConfig).toContain("require('./electron-builder.cjs')") + expect(developmentConfig).toContain('...baseConfig.mac') + expect(developmentConfig).toContain('hardenedRuntime: false') + expect(developmentConfig).toContain('identity: null') + expect(developmentConfig).toContain('afterPack: signAdHocDevelopmentMac') + expect(developmentConfig).toContain("['--force', '--deep', '--sign', '-', appPath]") + expect(baseConfig).toContain('DSH_WORKBUDDY_PPT_RUNTIME_ROOT') + expect(baseConfig).toContain('const runtimeResources = runtimeRoot') + expect(baseConfig).not.toContain('is required for a self-contained Desktop package') + expect(baseConfig).toContain("to: 'workbuddy-ppt-runtime'") + expect(main).toContain('workBuddyPptRuntimeEnvironment(') expect(developmentConfig).toContain("productName: 'DSH Desktop Dev'") expect(developmentConfig).toContain("output: 'dist-dev'") expect(developmentConfig).toContain("dshDesktopChannel: 'development'") diff --git a/test/runtime.test.ts b/test/runtime.test.ts index ad9cb763..fc5a2c82 100644 --- a/test/runtime.test.ts +++ b/test/runtime.test.ts @@ -134,6 +134,25 @@ describe('Harness launch contract', () => { } }) + it('injects the packaged WorkBuddy PPT runtime into the Harness process', () => { + const options = buildHarnessSpawnOptions( + '/launch-root', + '/harness', + 'darwin', + { PATH: '/usr/bin' }, + { + DSH_WORKBUDDY_PPT_RUNTIME_ROOT: + '/Applications/DSH Desktop.app/Contents/Resources/workbuddy-ppt-runtime' + } + ) + + expect(options.env).toMatchObject({ + PATH: '/usr/bin', + DSH_WORKBUDDY_PPT_RUNTIME_ROOT: + '/Applications/DSH Desktop.app/Contents/Resources/workbuddy-ppt-runtime' + }) + }) + it('finds the Windows PATH when the environment block stores it lowercase', () => { // Windows environment variable names are case-insensitive and the captured // block is not normalised, so a machine whose registry PATH value name is diff --git a/test/workbuddy-ppt-integration.test.ts b/test/workbuddy-ppt-integration.test.ts new file mode 100644 index 00000000..f2549d6f --- /dev/null +++ b/test/workbuddy-ppt-integration.test.ts @@ -0,0 +1,209 @@ +import { createHash } from 'node:crypto' +import { readFile } from 'node:fs/promises' +import path from 'node:path' +import { gunzipSync } from 'node:zlib' +import { describe, expect, it } from 'vitest' +import { projectRoot } from './patch-path' + +const artifacts = { + core: { + file: 'dsh-workbuddy-ppt-0.1.1-rc.2-desktop-20260903-public-gallery.tgz', + sha256: 'f6682e3c42cc946f28cdddd51c177d1411139feabb51448508fc0dbeb48220e2' + }, + adapter: { + file: 'deepseek-ai-dsh-experimental-office-ppt-standard-adapter-0.1.1-rc.2-desktop-20260903-public-gallery.tgz', + sha256: 'e909330b53c179cca5ee49a9277ab1d87d3a84c5395e7e1cee8a50a1877f9184' + } +} as const + +async function artifact(name: keyof typeof artifacts): Promise { + return readFile(path.join(projectRoot, 'packages', 'workbuddy-ppt', artifacts[name].file)) +} + +function tarEntries(archive: Buffer): Map { + const tar = gunzipSync(archive) + const entries = new Map() + let offset = 0 + while (offset + 512 <= tar.length) { + const header = tar.subarray(offset, offset + 512) + const name = header.subarray(0, 100).toString('utf8').replace(/\0.*$/u, '') + if (name.length === 0) break + const prefix = header.subarray(345, 500).toString('utf8').replace(/\0.*$/u, '') + const sizeText = header.subarray(124, 136).toString('ascii').replace(/\0.*$/u, '').trim() + const size = Number.parseInt(sizeText || '0', 8) + const contentOffset = offset + 512 + const fullName = prefix.length === 0 ? name : `${prefix}/${name}` + entries.set(fullName, tar.subarray(contentOffset, contentOffset + size)) + offset = contentOffset + Math.ceil(size / 512) * 512 + } + return entries +} + +describe('WorkBuddy PPT built-in plugin', () => { + it('pins the reviewed plugin artifacts byte-for-byte', async () => { + for (const name of Object.keys(artifacts) as (keyof typeof artifacts)[]) { + expect(createHash('sha256').update(await artifact(name)).digest('hex')).toBe(artifacts[name].sha256) + } + }) + + it('ships a standard Composer client closure without the removed legacy runtime', async () => { + const entries = tarEntries(await artifact('adapter')) + const archive = entries.get('package/lib/client.js')?.toString('utf8') ?? '' + + expect(archive).toContain('conversation.hero.modeActions') + expect(archive).toContain('conversation.input.accessory') + expect(archive).toContain('conversation.composer.dock') + expect(archive).toContain('session.blank') + expect(archive).toContain('--office-ppt-template-panel-height') + expect(archive).toContain('data-native-wheel-owner') + expect(archive).not.toContain('centeredComposerScrollTop') + expect(archive).not.toContain('routeFlowWheel') + expect(archive).not.toContain('--office-ppt-flow-panel-height') + expect(archive).not.toContain('@deepseek-ai/dsh-client-runtime/client') + expect(archive).not.toContain('conversation.input.left') + expect(archive).not.toContain('conversation.input.dock') + expect(archive).not.toContain('conversation.hero.inputAccessory') + expect(archive).not.toContain('conversation.chat.turnTail') + }) + + it('ships the complete-JSX Slides route and its Host quality gates', async () => { + const entries = tarEntries(await artifact('core')) + const archive = entries.get('package/lib/index.js')?.toString('utf8') ?? '' + + expect(archive).toContain('workflow: direct Slides JSX authoring') + expect(archive).toContain('readability, collision, template fidelity, renderer validation') + expect(archive).toContain('submit every adapted page through ppt_write_page') + expect(archive).not.toContain('ppt_write_template_page') + expect(archive).not.toContain('ppt_get_template_reference') + expect(archive).not.toContain('pptd_render') + expect(archive).not.toContain('ppt_update_slide') + for (const privateTemplateMarker of [ + 'DATA_ANALYSIS_TEMPLATE_DATA', + 'VITALITY_BLUE_TEMPLATE_DATA', + 'VITALITY_BLUE_PREVIEWS', + 'vitality-blue-source-page', + ]) { + expect(archive).not.toContain(privateTemplateMarker) + } + }) + + it('renders the Slides action after agent preset and anchors the catalog after the input card', async () => { + const client = await readFile(path.join( + projectRoot, + 'node_modules', + '@deepseek-ai', + 'dsh-client-ui-conversation', + 'lib', + 'client.js' + ), 'utf8') + const cluster = client.indexOf('className: ConversationRoot_module_css_default.heroModeCluster') + const agentPreset = client.indexOf('renderSlot("conversation.hero.agentPreset", {})', cluster) + const modeActions = client.indexOf( + 'zone !== void 0 && renderSlot("conversation.hero.modeActions", zone)', + agentPreset + ) + const owner = client.indexOf('extensionZone: zone') + const input = client.indexOf('className: clsx(InputBar_module_css_default.card', owner) + const below = client.indexOf( + 'extensionZone !== void 0 ? renderSlot("conversation.composer.dock", extensionZone) : null', + input + ) + + expect(cluster).toBeGreaterThan(-1) + expect(agentPreset).toBeGreaterThan(cluster) + expect(modeActions).toBeGreaterThan(agentPreset) + expect(client).toContain('.HNmDuG_heroModeCluster{width:max-content;max-width:100%;flex:none;align-items:center;gap:2px;display:flex}') + expect(client).toContain( + '.HNmDuG_heroModeCluster [data-slot=conversation\\\\.hero\\\\.agentPreset]>span{min-width:0}' + ) + expect(client).toContain('"conversation.hero.modeActions": {') + expect(owner).toBeGreaterThan(-1) + expect(input).toBeGreaterThan(owner) + expect(below).toBeGreaterThan(input) + }) + + it('renders the selected template reference in the input card accessory seat', async () => { + const client = await readFile(path.join( + projectRoot, + 'node_modules', + '@deepseek-ai', + 'dsh-client-ui-conversation', + 'lib', + 'client.js' + ), 'utf8') + const owner = client.indexOf('extensionZone: zone') + const accessory = client.indexOf( + 'children: accessory ?? renderSlot("conversation.input.accessory", extensionZone)' + ) + + expect(owner).toBeGreaterThan(-1) + expect(accessory).toBeGreaterThan(-1) + expect(accessory).toBeGreaterThan(owner) + expect(client).toContain('"conversation.input.accessory": {') + }) + + it('keeps the empty prompt top-aligned and places the selected template inline before text', async () => { + const client = await readFile(path.join( + projectRoot, + 'node_modules', + '@deepseek-ai', + 'dsh-client-ui-conversation', + 'lib', + 'client.js' + ), 'utf8') + const promptRow = client.indexOf('className: InputBar_module_css_default.promptRow') + const accessory = client.indexOf('className: InputBar_module_css_default.accessory', promptRow) + const scroll = client.indexOf('ref: scrollRef', promptRow) + + expect(promptRow).toBeGreaterThan(-1) + expect(accessory).toBeGreaterThan(promptRow) + expect(scroll).toBeGreaterThan(accessory) + expect(client).toContain('.dYRH2G_promptRow{display:contents}') + expect(client).toContain( + '.dYRH2G_accessory{align-items:flex-start;flex:0 0 auto;min-width:0;padding-left:16px;display:none}' + ) + expect(client).toContain('>.dYRH2G_scroll .dYRH2G_input{padding-left:0}') + expect(client).toContain('>.dYRH2G_scroll .dYRH2G_placeholder{left:0}') + expect(client).toContain('tag.textContent = css$1 + pptAccessoryCss') + }) + + it('ships a production archive with only the Slides compiler route', async () => { + const entries = tarEntries(await artifact('core')) + const names = [...entries.keys()] + const host = entries.get('package/lib/index.js')?.toString('utf8') ?? '' + const manifest = JSON.parse(entries.get('package/package.json')?.toString('utf8') ?? '{}') as { + bin?: Record + exports?: Record + dependencies?: Record + } + + expect(host).toContain('workbuddy-ppt-runtime') + expect(host).toContain('selected_template_id') + expect(host).not.toContain('PPT mode') + expect(names).not.toContain('package/lib/bin.js') + expect(names).not.toContain('package/lib/pptd.js') + expect(names).not.toContain('package/skills/kimi-ppt/SKILL.md') + expect(manifest.bin).toBeUndefined() + expect(manifest.exports?.['./pptd']).toBeUndefined() + for (const dependency of ['@aiden0z/pptx-renderer', 'js-yaml', 'jsdom', 'pptxgenjs', 'sharp']) { + expect(manifest.dependencies?.[dependency]).toBeUndefined() + } + }) + + it('declares both local artifacts and mounts only the standard adapter', async () => { + const manifest = JSON.parse(await readFile(path.join(projectRoot, 'package.json'), 'utf8')) as { + dependencies: Record + } + const profilePatch = await readFile(path.join(projectRoot, 'build', 'dsh-desktop.patch.yml'), 'utf8') + + expect(manifest.dependencies['dsh-workbuddy-ppt']).toBe( + `file:packages/workbuddy-ppt/${artifacts.core.file}` + ) + expect(manifest.dependencies['@deepseek-ai/dsh-experimental-office-ppt-standard-adapter']).toBe( + `file:packages/workbuddy-ppt/${artifacts.adapter.file}` + ) + expect(profilePatch).toContain("name: '@deepseek-ai/dsh-experimental-office-ppt-standard-adapter'") + expect(profilePatch).toContain('disabled: !!js "!process.env.DSH_WORKBUDDY_PPT_RUNTIME_ROOT"') + expect(profilePatch).not.toContain('name: dsh-workbuddy-ppt') + }) +}) diff --git a/test/workbuddy-ppt-resource.test.ts b/test/workbuddy-ppt-resource.test.ts new file mode 100644 index 00000000..71943cc7 --- /dev/null +++ b/test/workbuddy-ppt-resource.test.ts @@ -0,0 +1,42 @@ +import path from 'node:path' +import { describe, expect, it, vi } from 'vitest' +import { + bundledWorkBuddyPptRuntimeEnvironment, + workBuddyPptRuntimeEnvironment +} from '../src/main/runtime/workbuddy-ppt-resource' + +describe('packaged WorkBuddy PPT runtime resource', () => { + it('injects the exact resource root when the platform runtime manifest exists', () => { + const resourcesPath = path.join(path.sep, 'Applications', 'DSH Desktop.app', 'Contents', 'Resources') + const runtimeRoot = path.join(resourcesPath, 'workbuddy-ppt-runtime') + const fileExists = vi.fn((candidate: string) => candidate === path.join(runtimeRoot, 'manifest.json')) + + expect(bundledWorkBuddyPptRuntimeEnvironment(resourcesPath, fileExists)).toEqual({ + DSH_WORKBUDDY_PPT_RUNTIME_ROOT: runtimeRoot + }) + expect(fileExists).toHaveBeenCalledWith(path.join(runtimeRoot, 'manifest.json')) + }) + + it('leaves the Harness environment unchanged when this platform has no bundled Slides runtime', () => { + expect(bundledWorkBuddyPptRuntimeEnvironment('C:\\Program Files\\DSH Desktop\\resources', () => false)) + .toBeUndefined() + }) + + it('injects the verified launcher cache into a development Harness process', () => { + const runtimeRoot = path.join(path.sep, 'Users', 'developer', 'Library', 'Caches', 'ppt-runtime') + const fileExists = vi.fn((candidate: string) => candidate === path.join(runtimeRoot, 'manifest.json')) + + expect(workBuddyPptRuntimeEnvironment('/unused', false, { + DSH_WORKBUDDY_PPT_RUNTIME_ROOT: runtimeRoot + }, fileExists)).toEqual({ DSH_WORKBUDDY_PPT_RUNTIME_ROOT: runtimeRoot }) + }) + + it('rejects an unverified or relative development runtime root', () => { + expect(workBuddyPptRuntimeEnvironment('/unused', false, { + DSH_WORKBUDDY_PPT_RUNTIME_ROOT: 'relative/runtime' + }, () => true)).toBeUndefined() + expect(workBuddyPptRuntimeEnvironment('/unused', false, { + DSH_WORKBUDDY_PPT_RUNTIME_ROOT: path.resolve('/missing/runtime') + }, () => false)).toBeUndefined() + }) +}) diff --git a/test/workbuddy-ppt-runtime-download.test.mjs b/test/workbuddy-ppt-runtime-download.test.mjs new file mode 100644 index 00000000..b1189082 --- /dev/null +++ b/test/workbuddy-ppt-runtime-download.test.mjs @@ -0,0 +1,206 @@ +import { createHash } from 'node:crypto' +import { chmod, cp, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' +import os from 'node:os' +import path from 'node:path' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { + downloadRuntimeArchive, + ensureWorkBuddyPptRuntime +} from '../scripts/ensure-workbuddy-ppt-runtime.mjs' +import { runtimeTreeDigest } from '../scripts/verify-workbuddy-ppt-runtime.mjs' + +const temporary = [] + +afterEach(async () => { + await Promise.all(temporary.splice(0).map(root => rm(root, { recursive: true, force: true }))) +}) + +async function fixture() { + const root = await mkdtemp(path.join(os.tmpdir(), 'dsh-runtime-download-test-')) + temporary.push(root) + const runtimeRoot = path.join(root, 'source-runtime') + const platform = 'darwin' + const arch = 'arm64' + const targetPlatform = `${platform}-${arch}` + const files = { + 'skills/tencent-pptx/SKILL.md': Buffer.from('# Tencent PPTX\n'), + 'skills/tencent-pptx/references/create-from-scratch.md': Buffer.from('# Scratch\n'), + 'skills/tencent-pptx/references/create-from-material.md': Buffer.from('# Material\n'), + 'slidep/package.json': Buffer.from('{"name":"@tencent/slidep","version":"5.4.1"}'), + 'slidep/dist/slidep-start.js': Buffer.from(''), + 'slidep/dist/slidep-validate.js': Buffer.from(''), + 'slidep/node_modules/.keep': Buffer.from(''), + [`tencent-docs-ai-engine/bin/${targetPlatform}/editor_sdk`]: Buffer.from('binary'), + [`tencent-docs-ai-engine/bin/${targetPlatform}/icudt72l.dat`]: Buffer.from('icu') + } + for (const [relative, content] of Object.entries(files)) { + const destination = path.join(runtimeRoot, relative) + await mkdir(path.dirname(destination), { recursive: true }) + await writeFile(destination, content) + } + await chmod(path.join(runtimeRoot, `tencent-docs-ai-engine/bin/${targetPlatform}/editor_sdk`), 0o755) + const manifest = Buffer.from(JSON.stringify({ + schemaVersion: 1, + platform: targetPlatform, + slidep: { name: '@tencent/slidep', version: '5.4.1' }, + editorEngine: { name: '@tencent/tencent-docs-ai-engine', version: '0.2.112-wb' }, + skillSha256: 'a'.repeat(64) + })) + await writeFile(path.join(runtimeRoot, 'manifest.json'), manifest) + + const runtimeTree = await runtimeTreeDigest(runtimeRoot) + const lockedFiles = [ + 'manifest.json', + 'skills/tencent-pptx/SKILL.md', + 'slidep/package.json', + 'slidep/dist/slidep-start.js', + 'slidep/dist/slidep-validate.js', + `tencent-docs-ai-engine/bin/${targetPlatform}/editor_sdk`, + `tencent-docs-ai-engine/bin/${targetPlatform}/icudt72l.dat` + ] + const lockPath = path.join(root, 'runtime-lock.json') + await writeFile(lockPath, JSON.stringify({ + schemaVersion: 1, + platforms: { + [targetPlatform]: { + slidep: '5.4.1', + editorEngine: '0.2.112-wb', + runtimeTree, + files: Object.fromEntries(await Promise.all(lockedFiles.map(async relative => { + const content = relative === 'manifest.json' ? manifest : files[relative] + return [relative, createHash('sha256').update(content).digest('hex')] + }))) + } + } + })) + + const archive = Buffer.from('locked runtime archive') + const distributionPath = path.join(root, 'runtime-distribution.json') + await writeFile(distributionPath, JSON.stringify({ + schemaVersion: 1, + platforms: { + [targetPlatform]: { + archive: 'tar.gz', + archiveSize: archive.byteLength, + archiveSha256: createHash('sha256').update(archive).digest('hex'), + url: 'https://example.test/runtime.tar.gz' + } + } + })) + return { root, runtimeRoot, platform, arch, lockPath, distributionPath, archive, runtimeTree } +} + +describe('WorkBuddy PPT runtime downloader', () => { + it('publishes an immutable HTTPS archive descriptor for Apple Silicon', async () => { + const descriptorPath = path.resolve( + import.meta.dirname, + '../packages/workbuddy-ppt/runtime-distribution.json' + ) + const distribution = JSON.parse(await readFile(descriptorPath, 'utf8')) + expect(distribution).toMatchObject({ + schemaVersion: 1, + platforms: { + 'darwin-arm64': { + archive: 'tar.gz', + archiveSize: 207169828, + archiveSha256: 'ca273302786617b722e98aa0976386a497e287db72880d07257bb32cd3d33a1d' + } + } + }) + expect(distribution.platforms['darwin-arm64'].url).toMatch( + /^https:\/\/github\.com\/SuperstructureJH\/dsh-desktop\/releases\/download\//u + ) + }) + + it('streams an HTTPS response into the requested archive file', async () => { + const root = await mkdtemp(path.join(os.tmpdir(), 'dsh-runtime-stream-test-')) + temporary.push(root) + const destination = path.join(root, 'runtime.tar.gz') + const archive = Buffer.from('downloaded archive') + const fetchImpl = vi.fn(async () => new Response(archive, { status: 200 })) + + await expect(downloadRuntimeArchive('https://example.test/runtime.tar.gz', destination, { + fetchImpl, + output: { write: vi.fn() } + })).resolves.toBe(archive.byteLength) + expect(fetchImpl).toHaveBeenCalledOnce() + }) + + it('downloads, verifies, atomically caches, and reuses a locked runtime', async () => { + const test = await fixture() + const downloadArchive = vi.fn(async (_url, destination) => { + await writeFile(destination, test.archive) + return test.archive.byteLength + }) + const extractArchive = vi.fn(async (_archive, destination) => { + await cp(test.runtimeRoot, destination, { recursive: true }) + }) + const options = { + platform: test.platform, + arch: test.arch, + cacheRoot: path.join(test.root, 'cache'), + lockPath: test.lockPath, + distributionPath: test.distributionPath, + downloadArchive, + extractArchive, + output: { write: vi.fn() } + } + + const installed = await ensureWorkBuddyPptRuntime(options) + expect(installed).toBe(path.join(options.cacheRoot, 'darwin-arm64', test.runtimeTree.sha256)) + expect(downloadArchive).toHaveBeenCalledTimes(1) + expect(extractArchive).toHaveBeenCalledTimes(1) + + await expect(ensureWorkBuddyPptRuntime(options)).resolves.toBe(installed) + expect(downloadArchive).toHaveBeenCalledTimes(1) + }) + + it('uses and verifies an explicitly configured runtime without downloading', async () => { + const test = await fixture() + const downloadArchive = vi.fn() + await expect(ensureWorkBuddyPptRuntime({ + platform: test.platform, + arch: test.arch, + lockPath: test.lockPath, + distributionPath: path.join(test.root, 'unused.json'), + environment: { DSH_WORKBUDDY_PPT_RUNTIME_ROOT: test.runtimeRoot }, + downloadArchive, + output: { write: vi.fn() } + })).resolves.toBe(test.runtimeRoot) + expect(downloadArchive).not.toHaveBeenCalled() + }) + + it('rejects an archive whose bytes do not match the public lock', async () => { + const test = await fixture() + const downloadArchive = vi.fn(async (_url, destination) => { + const changed = Buffer.from(test.archive) + changed[0] = changed[0] ^ 1 + await writeFile(destination, changed) + return changed.byteLength + }) + const extractArchive = vi.fn() + await expect(ensureWorkBuddyPptRuntime({ + platform: test.platform, + arch: test.arch, + cacheRoot: path.join(test.root, 'bad-cache'), + lockPath: test.lockPath, + distributionPath: test.distributionPath, + downloadArchive, + extractArchive, + output: { write: vi.fn() } + })).rejects.toThrow('archive SHA-256 mismatch') + expect(extractArchive).not.toHaveBeenCalled() + }) + + it('keeps development available without Slides on an unpublished platform', async () => { + const test = await fixture() + await expect(ensureWorkBuddyPptRuntime({ + platform: 'win32', + arch: 'x64', + distributionPath: test.distributionPath, + lockPath: test.lockPath, + optional: true, + output: { write: vi.fn() } + })).resolves.toBeUndefined() + }) +}) diff --git a/test/workbuddy-ppt-runtime.test.mjs b/test/workbuddy-ppt-runtime.test.mjs new file mode 100644 index 00000000..628cd0ce --- /dev/null +++ b/test/workbuddy-ppt-runtime.test.mjs @@ -0,0 +1,142 @@ +import { chmod, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises' +import { createHash } from 'node:crypto' +import { execFile } from 'node:child_process' +import os from 'node:os' +import path from 'node:path' +import { promisify } from 'node:util' +import { afterEach, describe, expect, it } from 'vitest' +import { + runtimeTreeDigest, + verifyWorkBuddyPptRuntime +} from '../scripts/verify-workbuddy-ppt-runtime.mjs' + +const temporary = [] +const execFileAsync = promisify(execFile) +const verifierPath = path.resolve(import.meta.dirname, '../scripts/verify-workbuddy-ppt-runtime.mjs') + +function environmentWithoutRuntime() { + const { DSH_WORKBUDDY_PPT_RUNTIME_ROOT: _runtimeRoot, ...environment } = process.env + return environment +} + +afterEach(async () => { + await Promise.all(temporary.splice(0).map(root => rm(root, { recursive: true, force: true }))) +}) + +async function fixture(platform = 'darwin', arch = 'arm64') { + const root = await mkdtemp(path.join(os.tmpdir(), 'dsh-workbuddy-runtime-')) + temporary.push(root) + const skill = Buffer.from('# Tencent PPTX\n') + const expectedPlatform = `${platform}-${arch}` + const files = { + 'skills/tencent-pptx/SKILL.md': skill, + 'skills/tencent-pptx/references/create-from-scratch.md': Buffer.from('# Scratch\n'), + 'skills/tencent-pptx/references/create-from-material.md': Buffer.from('# Material\n'), + 'slidep/package.json': Buffer.from('{"name":"@tencent/slidep"}'), + 'slidep/dist/slidep-start.js': Buffer.from(''), + 'slidep/dist/slidep-validate.js': Buffer.from(''), + 'slidep/node_modules/.keep': Buffer.from(''), + 'slidep/node_modules/transitive.js': Buffer.from('module.exports = {}\n'), + [`tencent-docs-ai-engine/bin/${expectedPlatform}/${platform === 'win32' ? 'editor_sdk.exe' : 'editor_sdk'}`]: Buffer.from('binary'), + [`tencent-docs-ai-engine/bin/${expectedPlatform}/icudt72l.dat`]: Buffer.from('icu') + } + for (const [relative, content] of Object.entries(files)) { + const destination = path.join(root, relative) + await mkdir(path.dirname(destination), { recursive: true }) + await writeFile(destination, content) + } + if (platform !== 'win32' && process.platform !== 'win32') { + await chmod(path.join(root, `tencent-docs-ai-engine/bin/${expectedPlatform}/editor_sdk`), 0o755) + } + const manifest = Buffer.from(JSON.stringify({ + schemaVersion: 1, + platform: expectedPlatform, + slidep: { name: '@tencent/slidep', version: '5.4.1' }, + editorEngine: { name: '@tencent/tencent-docs-ai-engine', version: '0.2.112-wb' }, + skillSha256: 'a'.repeat(64) + })) + await writeFile(path.join(root, 'manifest.json'), manifest) + const lockRoot = await mkdtemp(path.join(os.tmpdir(), 'dsh-workbuddy-runtime-lock-')) + temporary.push(lockRoot) + const lockPath = path.join(lockRoot, 'runtime-lock.json') + const lockedFiles = Object.fromEntries([ + 'manifest.json', + 'skills/tencent-pptx/SKILL.md', + 'slidep/package.json', + 'slidep/dist/slidep-start.js', + 'slidep/dist/slidep-validate.js', + `tencent-docs-ai-engine/bin/${expectedPlatform}/${platform === 'win32' ? 'editor_sdk.exe' : 'editor_sdk'}`, + `tencent-docs-ai-engine/bin/${expectedPlatform}/icudt72l.dat` + ].map(relative => [relative, relative === 'manifest.json' ? manifest : files[relative]])) + const runtimeTree = await runtimeTreeDigest(root) + await writeFile(lockPath, JSON.stringify({ + schemaVersion: 1, + platforms: { + [expectedPlatform]: { + slidep: '5.4.1', + editorEngine: '0.2.112-wb', + runtimeTree, + files: Object.fromEntries(Object.entries(lockedFiles).map(([relative, content]) => [ + relative, + createHash('sha256').update(content).digest('hex') + ])) + } + } + })) + return { root, lockPath } +} + +describe('WorkBuddy PPT Desktop runtime package gate', () => { + it('allows a platform package without a configured optional Slides runtime', async () => { + await expect(execFileAsync(process.execPath, [verifierPath, '--optional'], { + env: environmentWithoutRuntime() + })).resolves.toMatchObject({ + stdout: expect.stringContaining('packaging this platform with the Slides plugin disabled') + }) + }) + + it('keeps the self-contained runtime verifier strict', async () => { + await expect(execFileAsync(process.execPath, [verifierPath], { + env: environmentWithoutRuntime() + })).rejects.toThrow('DSH_WORKBUDDY_PPT_RUNTIME_ROOT is required for Desktop packaging') + }) + + it('accepts a complete runtime for the target platform', async () => { + const { root, lockPath } = await fixture() + await expect(verifyWorkBuddyPptRuntime(root, 'darwin', 'arm64', lockPath)).resolves.toMatchObject({ + platform: 'darwin-arm64', + slidep: { version: '5.4.1' } + }) + }) + + it('rejects a runtime built for another platform', async () => { + const { root, lockPath } = await fixture() + await expect(verifyWorkBuddyPptRuntime(root, 'win32', 'x64', lockPath)).rejects.toThrow( + 'does not match win32-x64' + ) + }) + + it('rejects a changed Tencent Skill body', async () => { + const { root, lockPath } = await fixture() + await writeFile(path.join(root, 'skills/tencent-pptx/SKILL.md'), '# changed\n') + await expect(verifyWorkBuddyPptRuntime(root, 'darwin', 'arm64', lockPath)).rejects.toThrow( + 'SHA-256 mismatch: skills/tencent-pptx/SKILL.md' + ) + }) + + it('rejects a missing transitive SlideP dependency file', async () => { + const { root, lockPath } = await fixture() + await rm(path.join(root, 'slidep/node_modules/transitive.js')) + await expect(verifyWorkBuddyPptRuntime(root, 'darwin', 'arm64', lockPath)).rejects.toThrow( + 'runtime file count' + ) + }) + + it.skipIf(process.platform === 'win32')('rejects a non-executable POSIX editor binary', async () => { + const { root, lockPath } = await fixture() + await chmod(path.join(root, 'tencent-docs-ai-engine/bin/darwin-arm64/editor_sdk'), 0o644) + await expect(verifyWorkBuddyPptRuntime(root, 'darwin', 'arm64', lockPath)).rejects.toThrow( + 'editor_sdk must be executable' + ) + }) +})