mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-11-09 20:07:43 +08:00
refactor(office): remove command launchers and shell compatibility changes
This commit is contained in:
+2
-2
@@ -9,8 +9,8 @@
|
||||
en: a9984a594cdac98d
|
||||
zh: 38768e7699bc8d39
|
||||
/agent-note-progressive-office-skills-with-ordinary-shell-execution/decision:
|
||||
en: b4572697def440af
|
||||
zh: d0c16ab71a1c3aaf
|
||||
en: d057c558d6b9803c
|
||||
zh: acf3fdbc4cd7cee3
|
||||
/agent-note-progressive-office-skills-with-ordinary-shell-execution/alternatives-considered:
|
||||
en: 8bca955255e6d982
|
||||
zh: bc313a4bcab3f6f9
|
||||
|
||||
@@ -11,9 +11,9 @@ Combined authoring and rendering instructions spend context on operations unrela
|
||||
## Decision
|
||||
|
||||
|
||||
The [Office provider](../../../../packages/skill/skill-office/README.md) lists a compact router and six format-specific authoring or operation skills. The model loads full instructions by name and selects rendering for appearance-focused changes unless the user explicitly skips it. Rendering uses a private, randomly named temporary directory inside the task workspace and removes it after inspection. Skills invoke `office-check` and `dsoffice` through ordinary Bash or PowerShell. SDK and Desktop own their Office composition separately; ordinary Web and the shared base do not mount these plugins.
|
||||
The [Office provider](../../../../packages/skill/skill-office/README.md) lists a compact router and six format-specific authoring or operation skills. The model loads full instructions by name and selects rendering for appearance-focused changes unless the user explicitly skips it. Rendering uses a private, randomly named temporary directory inside the task workspace and removes it after inspection. Skills invoke the shared Python checker and configured LibreOffice Kit CLI through ordinary Bash or PowerShell. SDK and Desktop own their Office composition separately; ordinary Web and the shared base do not mount these plugins.
|
||||
|
||||
The command plugin contributes local launchers through [shell-env](../../../../packages/shell/shell-env/README.md) to each shell call's `PATH`. Carriers own interpreter and resource locations; those paths stay outside skill instructions. Loading skills and registering commands do not prepare engines or authoring payloads. Standalone rendering, PDF export, and recalculation require only their operation skill. This keeps instruction loading independent of installed runtime layout while shell execution retains its existing permissions and process management.
|
||||
Carriers own interpreter and resource locations. Operation skills append the configured Node and CLI paths only when their bodies are loaded; authoring skills use the selected Python and shared checker resource. Loading instructions does not prepare engines or authoring payloads. Ordinary shell execution retains its existing permissions and process management.
|
||||
|
||||
Office dependency ranges allow patch updates; the root lock records the resolved adapter and engine versions.
|
||||
|
||||
|
||||
@@ -11,9 +11,9 @@ Status: implemented
|
||||
## 决策
|
||||
|
||||
|
||||
[Office 提供者](../../../../packages/skill/skill-office/README.zh.md) 列出精简路由和六个各格式创作或操作技能。模型按名称加载完整说明;以外观为目标的修改会选择渲染,除非用户明确要求跳过。渲染使用任务工作区内随机命名的私有临时目录,并在检查后删除。技能通过普通 Bash 或 PowerShell 调用 `office-check` 和 `dsoffice`。SDK 和 Desktop 分别拥有 Office 组合;普通 Web 和共享 base 不挂载这些插件。
|
||||
[Office 提供者](../../../../packages/skill/skill-office/README.zh.md) 列出精简路由和六个各格式创作或操作技能。模型按名称加载完整说明;以外观为目标的修改会选择渲染,除非用户明确要求跳过。渲染使用任务工作区内随机命名的私有临时目录,并在检查后删除。技能通过普通 Bash 或 PowerShell 调用共享 Python 检查器和配置的 LibreOffice Kit CLI。SDK 和 Desktop 分别拥有 Office 组合;普通 Web 和共享 base 不挂载这些插件。
|
||||
|
||||
命令插件通过 [shell-env](../../../../packages/shell/shell-env/README.zh.md) 将本地启动器加入每次 shell 调用的 `PATH`。载体负责解释器和资源位置;这些路径不进入技能说明。加载技能和注册命令不准备引擎或创作 payload。独立渲染、PDF 导出和重算只需对应操作技能。这使说明加载独立于已安装运行时的布局,同时 shell 执行保留现有权限和进程管理。
|
||||
载体负责解释器和资源位置。操作技能仅在加载正文时附加配置的 Node 与 CLI 路径;创作技能使用选定的 Python 和共享检查器资源。加载说明不准备引擎或创作 payload。普通 shell 执行保留现有权限和进程管理。
|
||||
|
||||
Office 依赖范围允许补丁版本更新;根锁文件记录实际解析的适配器与引擎版本。
|
||||
|
||||
|
||||
@@ -462,7 +462,7 @@ it.each(['unset', 'empty', 'bundled', 'full', 'python-only', 'python-only-no-cli
|
||||
if (address === null || typeof address === 'string') throw new Error('model fixture did not bind')
|
||||
const home = join(root, 'home')
|
||||
const cliPatch = join(root, 'cli.patch.yml')
|
||||
await writeFile(cliPatch, JSON.stringify(mode === 'python-only-no-cli' ? [{ id: 'skill-office', config: { assetRoot: join(root, 'resources', 'office-skills') } }, { id: 'office-commands', config: { command: false } }] : []))
|
||||
await writeFile(cliPatch, JSON.stringify(mode === 'python-only-no-cli' ? [{ id: 'skill-office', config: { assetRoot: join(root, 'resources', 'office-skills'), cli: false } }] : []))
|
||||
const officeLaunch = resolveExampleLaunch({
|
||||
srcBin: fileURLToPath(new URL('../../../src/bin.ts', import.meta.url)), mode: 'lib',
|
||||
configArgs: ['--profile', 'sdk', '--patch', cliPatch],
|
||||
@@ -498,7 +498,7 @@ it.each(['unset', 'empty', 'bundled', 'full', 'python-only', 'python-only-no-cli
|
||||
expect(names).not.toContain('office_tools')
|
||||
expect(names).not.toContain('render_document')
|
||||
for (const name of ['office-docx', 'office-pptx', 'office-xlsx']) {
|
||||
expect(JSON.stringify(requests[0]!.messages).includes(name)).toBe(false)
|
||||
expect(JSON.stringify(requests[0]!.messages).includes(name)).toBe(enabled && mode !== 'missing-assets')
|
||||
}
|
||||
if (mode === 'wrong-type') {
|
||||
expect(requests).toHaveLength(2)
|
||||
|
||||
@@ -35,16 +35,7 @@ it.skipIf(!process.env.DEEPSEEK_API_KEY || process.platform === 'win32')(`agent
|
||||
...(process.env.DSH_OFFICE_TEST_API === 'openai-completions' ? [{ id: 'llm-deepseek', disabled: true }, { id: 'llm-pi-ai', config: { providers: { 'office-test': { api: 'openai-completions', apiKeyEnv: 'DEEPSEEK_API_KEY', baseURL: process.env.DEEPSEEK_BASE_URL, compat: { thinkingFormat: 'deepseek' }, models: [{ id: process.env.MODEL_NAME ?? 'deepseek-v4-flash', contextWindow: 128000, maxTokens: 8000 }] } } } }] : []),
|
||||
{ id: 'session-persistence-jsonl', config: { root: join(root, 'sessions'), compression: 'none' } },
|
||||
{ id: 'skill-office', disabled: false },
|
||||
{ id: 'office-commands', disabled: false },
|
||||
...assetRoot === undefined ? [] : [{ id: 'skill-office', config: { assetRoot } }],
|
||||
...node === undefined && cli === undefined && assetRoot === undefined ? [] : [{
|
||||
id: 'office-commands', config: {
|
||||
...node === undefined && cli === undefined ? {} : {
|
||||
command: [node ?? process.execPath, cli ?? fileURLToPath(import.meta.resolve('@deepseek-ai/libreoffice-kit/cli'))],
|
||||
},
|
||||
...assetRoot === undefined ? {} : { assetRoot },
|
||||
},
|
||||
}],
|
||||
{ id: 'skill-office', config: { ...assetRoot === undefined ? {} : { assetRoot }, ...node === undefined ? {} : { node }, ...cli === undefined ? {} : { cli } } },
|
||||
]))
|
||||
// A private POSIX tool directory also excludes /usr/bin/node on Linux.
|
||||
const path = join(root, 'bin')
|
||||
@@ -113,7 +104,7 @@ it.skipIf(!process.env.DEEPSEEK_API_KEY || process.platform === 'win32')(`agent
|
||||
if (typeof call.arguments !== 'string') throw new Error('Shell tool call did not contain serialized arguments')
|
||||
return (JSON.parse(call.arguments) as { command: string }).command
|
||||
}).join('\n')
|
||||
for (const operation of ['capabilities', 'convert']) expect(commandText, diagnostic).toMatch(new RegExp(`\\bdsoffice["']?\\s+${operation}\\b`, 'u'))
|
||||
for (const operation of ['capabilities', 'convert']) expect(commandText, diagnostic).toMatch(new RegExp(`\\s${operation}\\b`, 'u'))
|
||||
expect(existsSync(decoyMarker), 'the agent must not invoke system Office when the bundled CLI is supplied').toBe(false)
|
||||
const capabilities = JSON.parse(await readFile(join(root, 'capabilities.json'), 'utf8')) as { runtime: { version: string; cliPath: string } }
|
||||
expect(capabilities.runtime.version).toBe('0.1.5')
|
||||
|
||||
@@ -30,7 +30,6 @@
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/cordis-plugin-loader": "workspace:~",
|
||||
"@deepseek-ai/cordis-plugin-include": "workspace:~",
|
||||
"@deepseek-ai/dsh-skill": "workspace:*",
|
||||
"@deepseek-ai/dsh-shell-env": "workspace:*"
|
||||
"@deepseek-ai/dsh-skill": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import { fileURLToPath } from 'node:url'
|
||||
import { runtimeArchivePath } from './office-engine.ts'
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import * as officeSkills from '@deepseek-ai/dsh-skill-office'
|
||||
import * as officeCommands from '@deepseek-ai/dsh-skill-office/commands'
|
||||
import * as workspaceDependencies from '@deepseek-ai/dsh-tool-workspace-dependencies'
|
||||
|
||||
/** Loader identity for the application-owned Office composition. */
|
||||
@@ -33,10 +32,7 @@ export async function apply(ctx: Context, config: Config): Promise<void> {
|
||||
const packageRoot = dirname(archive === undefined ? manifest : join(`${archive}.unpacked`, relative(archive, manifest)))
|
||||
await ctx.plugin(officeSkills, {
|
||||
assetRoot: join(dirname(config.source), 'office-skills'),
|
||||
})
|
||||
await ctx.plugin(officeCommands, {
|
||||
assetRoot: join(dirname(config.source), 'office-skills'),
|
||||
command: [join(config.source, 'dependencies', 'node', 'bin', process.platform === 'win32' ? 'node.exe' : 'node'), join(packageRoot, 'lib', 'cli.js')],
|
||||
checkCommand: [join(config.source, 'dependencies', 'python', process.platform === 'win32' ? 'python.exe' : 'bin/python3'), join(dirname(config.source), 'office-skills', 'scripts', 'check_office.py')],
|
||||
node: join(config.source, 'dependencies', 'node', 'bin', process.platform === 'win32' ? 'node.exe' : 'node'),
|
||||
cli: join(packageRoot, 'lib', 'cli.js'),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,9 +8,7 @@ import Include from '@deepseek-ai/cordis-plugin-include'
|
||||
import AgentRegistry from '@deepseek-ai/dsh-agent'
|
||||
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
||||
import SkillRegistry from '@deepseek-ai/dsh-skill'
|
||||
import ToolRuntime, { type ToolExecution } from '@deepseek-ai/dsh-tools'
|
||||
import { ToolCallId } from '@deepseek-ai/dsh-llm'
|
||||
import * as ShellEnv from '@deepseek-ai/dsh-shell-env'
|
||||
import ToolRuntime from '@deepseek-ai/dsh-tools'
|
||||
import { expect, it, vi } from 'vitest'
|
||||
import * as desktopOffice from '../src/office.ts'
|
||||
|
||||
@@ -18,8 +16,7 @@ it('loads Desktop Office CLI paths and removes them on disposal', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'desktop-office-'))
|
||||
vi.stubEnv('DSH_HOME', root)
|
||||
const ctx = new Context()
|
||||
const execution: ToolExecution = { signal: new AbortController().signal, token: Symbol('office') as ToolExecution['token'],
|
||||
callId: ToolCallId('office'), rootCallId: ToolCallId('office'), name: 'bash', arguments: {} }
|
||||
|
||||
try {
|
||||
const assets = join(root, 'runtime', 'office-skills')
|
||||
await cp(new URL('../../../packages/skill/skill-office/assets/', import.meta.url), assets, { recursive: true })
|
||||
@@ -33,7 +30,7 @@ it('loads Desktop Office CLI paths and removes them on disposal', async () => {
|
||||
expect(ctx.loader.unwrapExports(desktopOffice)).toBe(desktopOffice)
|
||||
const modules = new Map<string, unknown>([
|
||||
['agents', AgentRegistry], ['systemPrompt', SystemPrompt], ['tools', ToolRuntime],
|
||||
['skills', SkillRegistry], ['shellEnv', ShellEnv], ['office', desktopOffice],
|
||||
['skills', SkillRegistry], ['office', desktopOffice],
|
||||
])
|
||||
ctx.loader.internal = {
|
||||
version: 'v2',
|
||||
@@ -44,7 +41,7 @@ it('loads Desktop Office CLI paths and removes them on disposal', async () => {
|
||||
} as unknown as NonNullable<typeof ctx.loader.internal>
|
||||
const config = join(root, 'cordis.yml')
|
||||
await writeFile(config, [
|
||||
'- name: agents', '- name: systemPrompt', '- name: tools', '- name: skills', '- name: shellEnv', '- name: office',
|
||||
'- name: agents', '- name: systemPrompt', '- name: tools', '- name: skills', '- name: office',
|
||||
' config:', ` source: ${JSON.stringify(join(root, 'runtime', 'primary-runtime'))}`,
|
||||
` root: ${JSON.stringify(join(root, 'installed'))}`,
|
||||
` runtimeDir: ${JSON.stringify(join(root, 'dsh'))}`, '',
|
||||
@@ -56,14 +53,12 @@ it('loads Desktop Office CLI paths and removes them on disposal', async () => {
|
||||
expect((await ctx.skills.get('office-pptx'))?.resourceBase).toEqual({ kind: 'directory', path: join(assets, 'office-pptx') })
|
||||
expect((await ctx.skills.get('office-pptx'))?.content).toContain('office-render')
|
||||
expect((await ctx.skills.get('office-pptx'))?.content).not.toContain(nodeDirectory)
|
||||
expect(ctx.shellEnv.collect(execution).PATH).toContain(join(root, 'commands'))
|
||||
expect(ctx.tools.schemas().map(tool => tool.name)).toEqual(['load_workspace_dependencies'])
|
||||
const entry = [...ctx.loader.entries()].find(entry => entry.options.name === 'office')
|
||||
expect(entry).toBeDefined()
|
||||
await entry?.fiber?.dispose()
|
||||
expect(await ctx.skills.list()).toEqual([])
|
||||
expect(ctx.tools.schemas()).toEqual([])
|
||||
expect(ctx.shellEnv.collect(execution)).not.toHaveProperty('PATH')
|
||||
} finally {
|
||||
try { await ctx.fiber.dispose() }
|
||||
finally {
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
en: 069fdfd72d2ea26a
|
||||
zh: 76ae549d43c99bb4
|
||||
/deepseek-harness-desktop/key-technical-decisions/bundled-workspace-dependencies:
|
||||
en: 31051ec14e651ce0
|
||||
zh: 698a441dd3de95b0
|
||||
en: cc2d4a2d5aecfaa0
|
||||
zh: 2a1d612560f6d95d
|
||||
/deepseek-harness-desktop/bundled-command-runtime:
|
||||
en: 09f0b75af9f0ed14
|
||||
zh: 60bbc2e67d5e87a8
|
||||
@@ -51,8 +51,8 @@
|
||||
en: 5f05072ec82cc62d
|
||||
zh: a3d63b58d1ef2db7
|
||||
/deepseek-harness-desktop/package/runtime-file-selection:
|
||||
en: 26cec76343af3d33
|
||||
zh: 93a39b6dd4a79b38
|
||||
en: 8b4cca1467f0bea6
|
||||
zh: fa3487fc56a53fc4
|
||||
/deepseek-harness-desktop/package/upload-updates:
|
||||
en: f48baa680e841b0d
|
||||
zh: 25ded1aeaac6dad1
|
||||
|
||||
@@ -64,7 +64,7 @@ Signed Windows packaging scans the primary runtime and application production de
|
||||
|
||||
Desktop carries independent Python, Node.js and pnpm distributions. Python includes numpy, pandas, python-docx, python-pptx, openpyxl, Pillow, lxml and XlsxWriter with their complete dependencies. The `load_workspace_dependencies` tool installs this payload offline on first use under `$DSH_HOME/dsh-runtimes/dsh-primary-runtime` (normally `~/.dsh/dsh-runtimes/dsh-primary-runtime`) and returns absolute interpreter, pnpm script and library paths plus `pythonDistributions`, the bundled distribution names and versions. The version report excludes user-installed additions. Office tasks prefer these libraries unless user or workspace instructions select another environment. Execute the pnpm script with the returned Node executable. The returned Node library directory is reserved for bundled libraries, not pnpm's global installation directory.
|
||||
|
||||
Desktop exposes the compact `office` router and named authoring/operation subskills. Authoring uses bundled Python libraries and the shared structural checker; operation skills invoke the stable `dsoffice` command. Skills live in `runtime/office-skills` outside ASAR so Python can read resources. See the [Office skill package](../../packages/skill/skill-office/README.md) for progressive loading and limitations.
|
||||
Desktop exposes the compact `office` router and named authoring/operation subskills. Authoring uses bundled Python libraries and the shared structural checker; operation skills invoke the configured Node and CLI paths. Skills live in `runtime/office-skills` outside ASAR so Python can read resources. See the [Office skill package](../../packages/skill/skill-office/README.md) for progressive loading and limitations.
|
||||
|
||||
The payload follows the Desktop release. `runtime.json` records the Desktop version, target, top-level interpreter/package-manager versions and the Python distribution map, and a digest of the selected target’s locked payload inputs and assembly format. Distribution names use PEP 503 normalization; duplicate normalized names reject the manifest. Legacy `components` manifests remain readable through normalization, with their existing library-version consistency checks. Matching installations are reused; a dependency or archive change replaces the directory after a complete staged copy even when the Desktop version stays unchanged. Older manifests without a digest are replaced on their next installation. User-added Python packages remain only while the payload identity matches. A failed directory replacement retains the previous installation; Windows may refuse replacement while an interpreter is still running.
|
||||
|
||||
@@ -280,7 +280,7 @@ The [Electron shell file list](scripts/electron-builder-config.mjs) excludes rec
|
||||
|
||||
The [Office conversion provider](../../packages/document/office-to-pdf/README.md) carries the target’s declared native engine, or WASM when the kit declares no matching native target. Preparation rejects a missing target engine before packaging. The complete Office dependencies, including CLI, JavaScript libraries, and the selected engine’s executables, data, licenses, and notices, are unpacked under `resources/app.asar.unpacked/dsh/node_modules/`. The Desktop Host resolves engine manifests to these physical directories and supplies the Office command provider with absolute standalone Node and unpacked CLI paths. Node lives in `resources/runtime/primary-runtime/dependencies/node/bin/`; the CLI is the unpacked `@deepseek-ai/libreoffice-kit/lib/cli.js`. On macOS, the native helper receives the JIT entitlement required by [LibreOffice’s UNO bridge](https://github.com/LibreOffice/core/blob/master/sysui/desktop/macosx/hardened_runtime.xcent.in).
|
||||
|
||||
The packaged application runs compiled JavaScript and pre-generated Typert metadata; it does not compile TypeScript plugins. Source-level debugger navigation and editor declarations remain available in development packages. [Copy-policy tests](tests/runtime-file-policy.spec.ts) cover exclusions and retained assets; The [payload smoke](tests/fixtures/runtime-payload-smoke.mjs) runs under Electron RunAsNode before the Host smoke and final inventory verification. The payload smoke resolves the search tool’s ripgrep executable and verifies text search and file enumeration. Signed Windows builds run these checks after dependency signing; other builds run them during `prepare:dsh`. The [Host smoke](scripts/smoke-runtime.ts) creates DOCX, XLSX, and PPTX inputs with bundled Python, converts each through the real Office provider, and checks PDF output. Every assembled application, including directory and unsigned builds, repeats the payload and Host checks against ASAR. Archive integrity checks compare the complete archived descriptor with preparation, file bytes and membership in both archive and unpacked trees, recorded executable flags for archived files, and physical permissions for unpacked files. The Host smoke also runs `dsoffice` capabilities and DOCX conversion through the registered shell environment with an empty caller PATH. Conversion failures stop packaging before a release record is written; macOS DMG/ZIP builds run these checks before notarization.
|
||||
The packaged application runs compiled JavaScript and pre-generated Typert metadata; it does not compile TypeScript plugins. Source-level debugger navigation and editor declarations remain available in development packages. [Copy-policy tests](tests/runtime-file-policy.spec.ts) cover exclusions and retained assets; The [payload smoke](tests/fixtures/runtime-payload-smoke.mjs) runs under Electron RunAsNode before the Host smoke and final inventory verification. The payload smoke resolves the search tool’s ripgrep executable and verifies text search and file enumeration. Signed Windows builds run these checks after dependency signing; other builds run them during `prepare:dsh`. The [Host smoke](scripts/smoke-runtime.ts) creates DOCX, XLSX, and PPTX inputs with bundled Python, converts each through the real Office provider, and checks PDF output. Every assembled application, including directory and unsigned builds, repeats the payload and Host checks against ASAR. Archive integrity checks compare the complete archived descriptor with preparation, file bytes and membership in both archive and unpacked trees, recorded executable flags for archived files, and physical permissions for unpacked files. The Host smoke also runs CLI capabilities and DOCX conversion using paths supplied by the operation skill with an empty caller PATH. Conversion failures stop packaging before a release record is written; macOS DMG/ZIP builds run these checks before notarization.
|
||||
|
||||
Windows release qualification also runs [directory and replacement checks](scripts/smoke-windows.ps1) manually after the Desktop build. Set `$Makensis`, `$SevenZip`, and `$PluginDir` to the pinned builder’s NSIS compiler, 7-Zip executable, and x86-unicode NSIS plugin directory; pass the prepared `window-frame.dll` as `-FrameLibrary` to exercise the native extraction path and its failure report as well. From the repository root, run the command below. It verifies directory replacement and rollback, and both locked-file replacement modes; it is not part of the unit-test lane.
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ Windows 签名打包按 PE 文件内容扫描第一方运行时和应用生产
|
||||
|
||||
Desktop 携带独立的 Python、Node.js 和 pnpm 分发包。Python 包含 numpy、pandas、python-docx、python-pptx、openpyxl、Pillow、lxml、XlsxWriter 及其完整依赖。`load_workspace_dependencies` 工具首次使用时,将该产物离线安装到 `$DSH_HOME/dsh-runtimes/dsh-primary-runtime`(通常为 `~/.dsh/dsh-runtimes/dsh-primary-runtime`),并返回解释器、pnpm 脚本和库目录的绝对路径,以及记录内置分发包名称与版本的 `pythonDistributions`。版本报告不包含用户自行安装的包。Office 任务默认使用这些库,用户或工作区指令指定其他环境时遵循其要求。pnpm 脚本通过返回的 Node 可执行文件运行。返回的 Node 库目录为随包交付的库预留,不是 pnpm 的全局安装目录。
|
||||
|
||||
Desktop 展示精简的 `office` 路由及可具名调用的创作/操作子技能。创作使用内置 Python 库和共享结构检查器;操作技能调用稳定的 `dsoffice` 命令。技能位于 ASAR 外的 `runtime/office-skills`,便于 Python 读取资源。渐进加载与限制见 [Office 技能包](../../packages/skill/skill-office/README.zh.md)。
|
||||
Desktop 展示精简的 `office` 路由及可具名调用的创作/操作子技能。创作使用内置 Python 库和共享结构检查器;操作技能调用配置的 Node 和 CLI 路径。技能位于 ASAR 外的 `runtime/office-skills`,便于 Python 读取资源。渐进加载与限制见 [Office 技能包](../../packages/skill/skill-office/README.zh.md)。
|
||||
|
||||
该产物随 Desktop 版本发布。`runtime.json` 记录 Desktop 版本、目标平台、顶层解释器和包管理器版本及 Python 分发包版本表,以及所选目标的锁定产物输入与组装格式的摘要。分发包名称按 PEP 503 归一化;名称归一化后重复时,清单会被拒绝。旧 `components` 清单通过归一化继续可读,并保留其原有库版本一致性校验。匹配的安装会被复用;依赖或压缩包变化后,即使 Desktop 版本不变,也会在完整暂存副本完成后替换目录。不含摘要的旧清单会在下次安装时被替换。用户自行添加的 Python 包仅在产物身份一致时保留。目录替换失败时保留之前的安装;解释器仍在运行时,Windows 可能拒绝替换。
|
||||
|
||||
@@ -282,7 +282,7 @@ Desktop 在本地打包工作区包,并通过目标捆绑的 Node 和 pnpm 安
|
||||
|
||||
[Office 转换提供方](../../packages/document/office-to-pdf/README.zh.md)携带目标已声明的原生引擎;kit 未声明匹配原生目标时携带 WASM 引擎。准备阶段在打包前拒绝缺少目标引擎的情况。完整 Office 依赖(CLI、JavaScript 库和选定引擎的可执行文件、数据、许可证及 notices)解包到 `resources/app.asar.unpacked/dsh/node_modules/` 下。Desktop Host 将引擎清单解析到这些物理目录,并向 Office 命令提供方提供独立 Node 和解包后 CLI 的绝对路径。Node 位于 `resources/runtime/primary-runtime/dependencies/node/bin/`;CLI 位于解包后的 `@deepseek-ai/libreoffice-kit/lib/cli.js`。macOS 上的原生辅助程序获得 [LibreOffice UNO 桥](https://github.com/LibreOffice/core/blob/master/sysui/desktop/macosx/hardened_runtime.xcent.in)所需的 JIT entitlement。
|
||||
|
||||
打包应用运行编译后的 JavaScript 和预生成的 Typert 元数据,不编译 TypeScript 插件。源码级调试导航和编辑器声明仍可从开发包中获取。[复制规则测试](tests/runtime-file-policy.spec.ts)覆盖排除项和保留资源;[产物 smoke](tests/fixtures/runtime-payload-smoke.mjs) 在 Host smoke 和最终清单验证之前,使用 Electron RunAsNode 执行。产物 smoke 解析搜索工具使用的 ripgrep 可执行文件,并验证文本搜索和文件枚举。Windows 签名构建在依赖签名后运行这些检查;其他构建在 `prepare:dsh` 中运行。[Host smoke](scripts/smoke-runtime.ts) 使用捆绑的 Python 创建 DOCX、XLSX 和 PPTX 输入,通过真实 Office 提供方逐一转换并检查 PDF 输出。每个组装后的应用(包括目录包和未签名构建)都会针对 ASAR 重复产物和 Host 检查。归档完整性检查将归档内完整描述符与准备结果比对,并核对归档和解包目录中的文件内容与清单、归档内文件记录的执行标志,以及解包文件的物理权限。Host smoke 还在调用方 PATH 为空时,通过已注册的 shell 环境运行 `dsoffice` 的 capabilities 和 DOCX 转换。转换失败会在写入发布记录前终止打包;macOS DMG/ZIP 构建在公证前执行这些检查。
|
||||
打包应用运行编译后的 JavaScript 和预生成的 Typert 元数据,不编译 TypeScript 插件。源码级调试导航和编辑器声明仍可从开发包中获取。[复制规则测试](tests/runtime-file-policy.spec.ts)覆盖排除项和保留资源;[产物 smoke](tests/fixtures/runtime-payload-smoke.mjs) 在 Host smoke 和最终清单验证之前,使用 Electron RunAsNode 执行。产物 smoke 解析搜索工具使用的 ripgrep 可执行文件,并验证文本搜索和文件枚举。Windows 签名构建在依赖签名后运行这些检查;其他构建在 `prepare:dsh` 中运行。[Host smoke](scripts/smoke-runtime.ts) 使用捆绑的 Python 创建 DOCX、XLSX 和 PPTX 输入,通过真实 Office 提供方逐一转换并检查 PDF 输出。每个组装后的应用(包括目录包和未签名构建)都会针对 ASAR 重复产物和 Host 检查。归档完整性检查将归档内完整描述符与准备结果比对,并核对归档和解包目录中的文件内容与清单、归档内文件记录的执行标志,以及解包文件的物理权限。Host smoke 还使用操作技能提供的路径执行 CLI 能力查询和 DOCX 转换,并清空调用方 PATH。转换失败会在写入发布记录前终止打包;macOS DMG/ZIP 构建在公证前执行这些检查。
|
||||
|
||||
Windows 发布验收还需在 Desktop 构建后手动运行[目录和替换检查](scripts/smoke-windows.ps1)。将 `$Makensis`、`$SevenZip` 和 `$PluginDir` 分别设为锁定版本构建器的 NSIS 编译器、7-Zip 可执行文件和 x86-unicode NSIS 插件目录;通过 `-FrameLibrary` 传入已准备好的 `window-frame.dll`,即可同时覆盖原生解压路径及其失败报告。从仓库根目录运行以下命令。它验证 目录替换与回滚和两种文件占用替换方式;不属于单元测试通道。
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** Boot the materialized target runtime without access to a user's Harness profile. */
|
||||
|
||||
import { appendFileSync, cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, symlinkSync, writeFileSync } from 'node:fs'
|
||||
import { appendFileSync, cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import { rm } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
@@ -40,7 +40,6 @@ export async function smokeDesktopRuntime(
|
||||
let timer: ReturnType<typeof setTimeout> | undefined
|
||||
try {
|
||||
writeFileSync(join(home, 'npmrc'), '')
|
||||
if (process.platform !== 'win32') symlinkSync('/bin/bash', join(home, 'bash'))
|
||||
createPluginProfile(profile)
|
||||
const pluginName = 'desktop-runtime-smoke-plugin'
|
||||
const plugin = join(home, pluginName)
|
||||
@@ -86,7 +85,8 @@ export function apply(ctx) {
|
||||
}))
|
||||
writeFileSync(join(plugin, 'index.js'), `
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { inspect } from 'node:util'
|
||||
import { inspect, promisify } from 'node:util'
|
||||
import { execFile } from 'node:child_process'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
export function apply(ctx) {
|
||||
if (!(ctx instanceof Context)) throw new Error('desktop runtime: external plugin loaded another Cordis instance')
|
||||
@@ -105,26 +105,14 @@ export function apply(ctx) {
|
||||
async handler(_request, response) {
|
||||
try {
|
||||
const skill = await ctx.skills.get('office-export-pdf')
|
||||
if (!skill?.content.includes('dsoffice convert')) throw new Error('Office skill did not supply the direct command')
|
||||
const managed = ctx.shellEnv.collect({})
|
||||
const delimiter = process.platform === 'win32' ? ';' : ':'
|
||||
const dshEnv = { ...managed, PATH: [managed.PATH.split(delimiter)[0], ${JSON.stringify(home)}].join(delimiter) }
|
||||
const json = skill?.content.match(/\\n(\\{\\n[\\s\\S]+)$/u)?.[1]
|
||||
if (json === undefined) throw new Error('Office skill did not supply CLI paths')
|
||||
const { libreofficeKit: { node, cli } } = JSON.parse(json)
|
||||
const options = { cwd: ${JSON.stringify(home)}, env: { ...process.env, PATH: '' }, timeout: 120_000 }
|
||||
const capabilities = await promisify(execFile)(node, [cli, 'capabilities'], options)
|
||||
const output = ${JSON.stringify(join(home, 'cli.pdf'))}
|
||||
async function run(command) {
|
||||
const execution = await ctx.shell.execute(ctx.shell.resolve({ command,
|
||||
workdir: ${JSON.stringify(home)}, timeoutMs: 120_000, dshEnv,
|
||||
sandboxPolicy: { mode: 'workspace-write', workspaceRoot: ${JSON.stringify(home)} },
|
||||
env: { PATH: '', OFFICE_SMOKE_INPUT: ${JSON.stringify(join(home, 'input.docx'))}, OFFICE_SMOKE_OUTPUT: output },
|
||||
}))
|
||||
const result = await execution.result()
|
||||
if (result.exitCode !== 0 || result.timedOut || result.aborted) throw new Error(JSON.stringify(result))
|
||||
return result.stdout.text
|
||||
}
|
||||
const capabilities = await run('dsoffice capabilities')
|
||||
await run(process.platform === 'win32'
|
||||
? 'dsoffice convert --input "$env:OFFICE_SMOKE_INPUT" --output "$env:OFFICE_SMOKE_OUTPUT"'
|
||||
: 'dsoffice convert --input "$OFFICE_SMOKE_INPUT" --output "$OFFICE_SMOKE_OUTPUT"')
|
||||
response.end(JSON.stringify({ capabilities: JSON.parse(capabilities), pdf: (await readFile(output)).toString('base64') }))
|
||||
await promisify(execFile)(node, [cli, 'convert', '--input', ${JSON.stringify(join(home, 'input.docx'))}, '--output', output], options)
|
||||
response.end(JSON.stringify({ capabilities: JSON.parse(capabilities.stdout), pdf: (await readFile(output)).toString('base64') }))
|
||||
} catch (error) {
|
||||
response.statusCode = 500
|
||||
response.end(inspect(error, { depth: 5 }))
|
||||
@@ -147,7 +135,7 @@ export function apply(ctx) {
|
||||
}
|
||||
}
|
||||
`)
|
||||
writeFileSync(join(plugin, 'bundle.yml'), '- insert:\n - id: desktop-runtime-smoke-plugin\n name: desktop-runtime-smoke-plugin\n inject: [webServer, officeToPdf, skills, shellEnv, shell, pluginManager]\n')
|
||||
writeFileSync(join(plugin, 'bundle.yml'), '- insert:\n - id: desktop-runtime-smoke-plugin\n name: desktop-runtime-smoke-plugin\n inject: [webServer, officeToPdf, skills, pluginManager]\n')
|
||||
cpSync(plugin, join(profile, 'node_modules', pluginName), { recursive: true })
|
||||
const manifest = JSON.parse(readFileSync(join(profile, 'package.json'), 'utf8')) as {
|
||||
dependencies?: Record<string, string>
|
||||
|
||||
@@ -107,8 +107,6 @@ describe('web e2e: lifecycle & chrome (workspace flow / reload / dark mode)', ()
|
||||
await writeComposerDraft(page, input, '/cpt')
|
||||
await expect.poll(() => menu.getByRole('option').allTextContents()).toEqual([
|
||||
'CompactCompact older conversation history',
|
||||
'office-pptxCreate, read, edit, and validate PowerPoint presentations (.pptx).',
|
||||
'office-export-pdfConvert an Office document to a requested PDF with LibreOffice Kit.',
|
||||
])
|
||||
const fuzzySnapshot = await captureStableAria(page, '[role="listbox"]', scaffold.workspaceCwd)
|
||||
await compareOrRefreshGolden(FUZZY_COMMAND_MENU_EXPECTED, fuzzySnapshot, MODE)
|
||||
|
||||
@@ -2983,7 +2983,7 @@ export interface Config extends SessionTitleLlmConfig {
|
||||
|
||||
## `@deepseek-ai/dsh-shell-env`
|
||||
|
||||
- `source`: [`packages/shell/shell-env/src/index.ts:31`](../packages/shell/shell-env/src/index.ts)
|
||||
- `source`: [`packages/shell/shell-env/src/index.ts:30`](../packages/shell/shell-env/src/index.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Plugin config (all optional — the built-in facts resolve without defaults). */
|
||||
@@ -3055,13 +3055,17 @@ export interface Config {
|
||||
## `@deepseek-ai/dsh-skill-office`
|
||||
|
||||
- `inject`: `skills`
|
||||
- `source`: [`packages/skill/skill-office/src/index.ts:15`](../packages/skill/skill-office/src/index.ts)
|
||||
- `source`: [`packages/skill/skill-office/src/index.ts:16`](../packages/skill/skill-office/src/index.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Office skill resource location. */
|
||||
export interface Config {
|
||||
/** Absolute assets directory containing skill folders and shared scripts; defaults to carrier resources or packaged assets. */
|
||||
/** Absolute assets directory containing skill folders and shared scripts; defaults to packaged assets. */
|
||||
assetRoot?: string
|
||||
/** Standalone Node executable; defaults to the current executable outside Electron and SEA. */
|
||||
node?: string
|
||||
/** Absolute LibreOffice Kit CLI entry; false explicitly disables CLI access. */
|
||||
cli?: string | false
|
||||
}
|
||||
```
|
||||
<!-- END GENERATED config-catalog:@deepseek-ai/dsh-skill-office -->
|
||||
|
||||
@@ -2985,7 +2985,7 @@ export interface Config extends SessionTitleLlmConfig {
|
||||
|
||||
## `@deepseek-ai/dsh-shell-env`
|
||||
|
||||
- `source`: [`packages/shell/shell-env/src/index.ts:31`](../packages/shell/shell-env/src/index.ts)
|
||||
- `source`: [`packages/shell/shell-env/src/index.ts:30`](../packages/shell/shell-env/src/index.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Plugin config (all optional — the built-in facts resolve without defaults). */
|
||||
@@ -3057,13 +3057,17 @@ export interface Config {
|
||||
## `@deepseek-ai/dsh-skill-office`
|
||||
|
||||
- `inject`: `skills`
|
||||
- `source`: [`packages/skill/skill-office/src/index.ts:15`](../packages/skill/skill-office/src/index.ts)
|
||||
- `source`: [`packages/skill/skill-office/src/index.ts:16`](../packages/skill/skill-office/src/index.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Office skill resource location. */
|
||||
export interface Config {
|
||||
/** Absolute assets directory containing skill folders and shared scripts; defaults to carrier resources or packaged assets. */
|
||||
/** Absolute assets directory containing skill folders and shared scripts; defaults to packaged assets. */
|
||||
assetRoot?: string
|
||||
/** Standalone Node executable; defaults to the current executable outside Electron and SEA. */
|
||||
node?: string
|
||||
/** Absolute LibreOffice Kit CLI entry; false explicitly disables CLI access. */
|
||||
cli?: string | false
|
||||
}
|
||||
```
|
||||
<!-- END GENERATED config-catalog:@deepseek-ai/dsh-skill-office -->
|
||||
|
||||
@@ -469,6 +469,7 @@ flowchart TD
|
||||
pkg_subprocess --> pkg_http_proxy
|
||||
pkg_remote_mock --> pkg_typert_protocol
|
||||
pkg_typert_loader --> pkg_typert_registry
|
||||
pkg_skill_office --> pkg_skill
|
||||
pkg_web_fetch_http --> pkg_http_proxy
|
||||
pkg_web_fetch_http --> pkg_timeout
|
||||
pkg_web_fetch_http --> pkg_web
|
||||
@@ -1029,9 +1030,6 @@ flowchart TD
|
||||
pkg_token_meter --> pkg_llm_retry
|
||||
pkg_token_meter --> pkg_session
|
||||
pkg_token_meter --> pkg_session_projection
|
||||
pkg_skill_office --> pkg_home_paths
|
||||
pkg_skill_office --> pkg_shell_env
|
||||
pkg_skill_office --> pkg_skill
|
||||
pkg_session_query --> pkg_brand
|
||||
pkg_session_query --> pkg_llm
|
||||
pkg_session_query --> pkg_session
|
||||
@@ -1576,6 +1574,7 @@ flowchart TD
|
||||
| [`subprocess`](../packages/subprocess/subprocess) | `subprocess` | [`http-proxy`](../packages/util/http-proxy) |
|
||||
| [`remote-mock`](../packages/test-support/remote-mock) | `test-support` | [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`typert-loader`](../packages/typert/loader) | `typert` | [`typert-registry`](../packages/typert/registry) |
|
||||
| [`skill-office`](../packages/skill/skill-office) | `skill` | [`skill`](../packages/skill/skill) |
|
||||
| [`web-fetch-http`](../packages/web/web-fetch-http) | `web` | [`http-proxy`](../packages/util/http-proxy), [`timeout`](../packages/util/timeout), [`web`](../packages/web/web) |
|
||||
| [`web-search-exa`](../packages/web/web-search-exa) | `web` | [`launch-environment`](../packages/util/launch-environment), [`web`](../packages/web/web) |
|
||||
| [`web-search-perplexity`](../packages/web/web-search-perplexity) | `web` | [`launch-environment`](../packages/util/launch-environment), [`web`](../packages/web/web) |
|
||||
@@ -1713,7 +1712,6 @@ flowchart TD
|
||||
| [`tool-workflow`](../packages/workflow/tool-workflow) | `workflow` | [`agent`](../packages/core/agent), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
|
||||
| [`plugin-package-inventory-deepseek`](../packages/llm/plugin-package-inventory-deepseek) | `llm` | [`agent`](../packages/core/agent), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions), [`session`](../packages/core/session) |
|
||||
| [`token-meter`](../packages/llm/token-meter) | `llm` | [`compaction`](../packages/compaction/compaction), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) |
|
||||
| [`skill-office`](../packages/skill/skill-office) | `skill` | [`home-paths`](../packages/util/home-paths), [`shell-env`](../packages/shell/shell-env), [`skill`](../packages/skill/skill) |
|
||||
| [`session-query`](../packages/session-query/session-query) | `session-query` | [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-title`](../packages/session/session-title), [`tool-todo`](../packages/todo/tool-todo) |
|
||||
| [`api-settings-controller`](../packages/api/settings-controller) | `api` | [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`credentials`](../packages/credentials/credentials), [`native-command`](../packages/util/native-command), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`web-app`](../packages/bundle/web-app) | `bundle` | [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt) |
|
||||
|
||||
@@ -471,6 +471,7 @@ flowchart TD
|
||||
pkg_subprocess --> pkg_http_proxy
|
||||
pkg_remote_mock --> pkg_typert_protocol
|
||||
pkg_typert_loader --> pkg_typert_registry
|
||||
pkg_skill_office --> pkg_skill
|
||||
pkg_web_fetch_http --> pkg_http_proxy
|
||||
pkg_web_fetch_http --> pkg_timeout
|
||||
pkg_web_fetch_http --> pkg_web
|
||||
@@ -1031,9 +1032,6 @@ flowchart TD
|
||||
pkg_token_meter --> pkg_llm_retry
|
||||
pkg_token_meter --> pkg_session
|
||||
pkg_token_meter --> pkg_session_projection
|
||||
pkg_skill_office --> pkg_home_paths
|
||||
pkg_skill_office --> pkg_shell_env
|
||||
pkg_skill_office --> pkg_skill
|
||||
pkg_session_query --> pkg_brand
|
||||
pkg_session_query --> pkg_llm
|
||||
pkg_session_query --> pkg_session
|
||||
@@ -1578,6 +1576,7 @@ flowchart TD
|
||||
| [`subprocess`](../packages/subprocess/subprocess) | `subprocess` | [`http-proxy`](../packages/util/http-proxy) |
|
||||
| [`remote-mock`](../packages/test-support/remote-mock) | `test-support` | [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`typert-loader`](../packages/typert/loader) | `typert` | [`typert-registry`](../packages/typert/registry) |
|
||||
| [`skill-office`](../packages/skill/skill-office) | `skill` | [`skill`](../packages/skill/skill) |
|
||||
| [`web-fetch-http`](../packages/web/web-fetch-http) | `web` | [`http-proxy`](../packages/util/http-proxy), [`timeout`](../packages/util/timeout), [`web`](../packages/web/web) |
|
||||
| [`web-search-exa`](../packages/web/web-search-exa) | `web` | [`launch-environment`](../packages/util/launch-environment), [`web`](../packages/web/web) |
|
||||
| [`web-search-perplexity`](../packages/web/web-search-perplexity) | `web` | [`launch-environment`](../packages/util/launch-environment), [`web`](../packages/web/web) |
|
||||
@@ -1715,7 +1714,6 @@ flowchart TD
|
||||
| [`tool-workflow`](../packages/workflow/tool-workflow) | `workflow` | [`agent`](../packages/core/agent), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
|
||||
| [`plugin-package-inventory-deepseek`](../packages/llm/plugin-package-inventory-deepseek) | `llm` | [`agent`](../packages/core/agent), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions), [`session`](../packages/core/session) |
|
||||
| [`token-meter`](../packages/llm/token-meter) | `llm` | [`compaction`](../packages/compaction/compaction), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) |
|
||||
| [`skill-office`](../packages/skill/skill-office) | `skill` | [`home-paths`](../packages/util/home-paths), [`shell-env`](../packages/shell/shell-env), [`skill`](../packages/skill/skill) |
|
||||
| [`session-query`](../packages/session-query/session-query) | `session-query` | [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-title`](../packages/session/session-title), [`tool-todo`](../packages/todo/tool-todo) |
|
||||
| [`api-settings-controller`](../packages/api/settings-controller) | `api` | [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`credentials`](../packages/credentials/credentials), [`native-command`](../packages/util/native-command), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`web-app`](../packages/bundle/web-app) | `bundle` | [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt) |
|
||||
|
||||
@@ -20,8 +20,8 @@ type DshEnvironmentKey = `${typeof DSH_ENV_PREFIX}${string}`
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Trusted Harness variables and optional local command PATH for one child-process execution. */
|
||||
type DshEnvironment = Readonly<Record<DshEnvironmentKey, string> & { PATH?: string }>
|
||||
/** Trusted DeepSeek Harness variables for one child-process execution. */
|
||||
type DshEnvironment = Readonly<Record<DshEnvironmentKey, string>>
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
|
||||
@@ -20,8 +20,8 @@ type DshEnvironmentKey = `${typeof DSH_ENV_PREFIX}${string}`
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Trusted Harness variables and optional local command PATH for one child-process execution. */
|
||||
type DshEnvironment = Readonly<Record<DshEnvironmentKey, string> & { PATH?: string }>
|
||||
/** Trusted DeepSeek Harness variables for one child-process execution. */
|
||||
type DshEnvironment = Readonly<Record<DshEnvironmentKey, string>>
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
|
||||
@@ -32,7 +32,5 @@
|
||||
disabled: !!js "!(process.env.DSH_PRIMARY_RUNTIME ?? process.env.DSH_BUNDLED_PRIMARY_RUNTIME) && !process.env.DSH_BUNDLED_OFFICE_CLI"
|
||||
config:
|
||||
assetRoot: !!js "process.env.DSH_BUNDLED_OFFICE_SKILLS ?? process.getBuiltinModule('node:path').resolve(process.env.DSH_PRIMARY_RUNTIME ?? process.env.DSH_BUNDLED_PRIMARY_RUNTIME ?? '', '..', 'office-skills')"
|
||||
|
||||
- id: office-commands
|
||||
name: '@deepseek-ai/dsh-skill-office/commands'
|
||||
disabled: !!js "!(process.env.DSH_PRIMARY_RUNTIME ?? process.env.DSH_BUNDLED_PRIMARY_RUNTIME) && !process.env.DSH_BUNDLED_OFFICE_CLI"
|
||||
cli: !!js "process.env.DSH_BUNDLED_OFFICE_CLI === '' ? false : process.env.DSH_BUNDLED_OFFICE_CLI"
|
||||
node: !!js "process.env.DSH_BUNDLED_OFFICE_NODE ?? process.getBuiltinModule('node:path').resolve(process.env.DSH_PRIMARY_RUNTIME ?? process.env.DSH_BUNDLED_PRIMARY_RUNTIME ?? '', 'dependencies', 'node', 'bin', process.platform === 'win32' ? 'node.exe' : 'node')"
|
||||
|
||||
@@ -4827,7 +4827,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
},
|
||||
{
|
||||
name: 'BashEnvContributor',
|
||||
declaration: 'export interface BashEnvContributor {\n name: string;\n commandDirectories?: readonly string[];\n variables: Readonly<Record<DshEnvironmentKey, BashEnvVariable>>;\n resolve(execution: ToolExecution): Readonly<Partial<Record<DshEnvironmentKey, string>>>;\n}',
|
||||
declaration: 'export interface BashEnvContributor {\n name: string;\n variables: Readonly<Record<DshEnvironmentKey, BashEnvVariable>>;\n resolve(execution: ToolExecution): Readonly<Partial<Record<DshEnvironmentKey, string>>>;\n}',
|
||||
},
|
||||
{
|
||||
name: 'BashEnvVariable',
|
||||
@@ -5291,7 +5291,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
},
|
||||
{
|
||||
name: 'DshEnvironment',
|
||||
declaration: 'export type DshEnvironment = Readonly<Record<DshEnvironmentKey, string> & {\n PATH?: string;\n}>;',
|
||||
declaration: 'export type DshEnvironment = Readonly<Record<DshEnvironmentKey, string>>;',
|
||||
},
|
||||
{
|
||||
name: 'DshEnvironmentKey',
|
||||
|
||||
@@ -24,8 +24,8 @@
|
||||
en: 47119e8b4e884a5d
|
||||
zh: f9dfb155f06c4b73
|
||||
/deepseek-ai-dsh-pwsh-local/use-this-package/running-commands:
|
||||
en: 6659d1bbc7af561d
|
||||
zh: 8ccb16ceb39d67f2
|
||||
en: d0efd217ca8ce07d
|
||||
zh: 76eda93aa7300f87
|
||||
/deepseek-ai-dsh-pwsh-local/use-this-package/background-processes:
|
||||
en: 3c580842cc8452af
|
||||
zh: 5a156670a7cacc2a
|
||||
@@ -36,14 +36,14 @@
|
||||
en: 05e2abe4a25886fd
|
||||
zh: 3f7f1326dc3c1d3f
|
||||
/deepseek-ai-dsh-pwsh-local/understand-the-implementation/design-concept:
|
||||
en: 09017c5641b0a5ec
|
||||
zh: 944ddfb922cf72f3
|
||||
en: d065be99a0cea908
|
||||
zh: 50eee843e3b6860c
|
||||
/deepseek-ai-dsh-pwsh-local/understand-the-implementation/source-map:
|
||||
en: aa160f9ec57ff627
|
||||
zh: 048d3292dddf1d8b
|
||||
/deepseek-ai-dsh-pwsh-local/understand-the-implementation/main-flow:
|
||||
en: d7ea7ad3e310aaeb
|
||||
zh: 8af73c715f5c56df
|
||||
en: 1cb78cfddb484576
|
||||
zh: 94e16b60c9cd458b
|
||||
/deepseek-ai-dsh-pwsh-local/understand-the-implementation/invariants-and-ownership:
|
||||
en: db6d1d14205d38ed
|
||||
zh: 148b3764daee54e8
|
||||
|
||||
@@ -59,8 +59,6 @@ The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-a
|
||||
|
||||
Run a command by awaiting the execution's `result()` projection; a nonzero exit, a timeout, or a cancellation resolves descriptively, and only infrastructure failures reject. The command string rides as one argument to `-Command`: PowerShell parses the text itself and no intermediate shell exists, so there is no shell-quoting layer to escape and native Win32 paths pass through unchanged. Every command pins UTF-8 output first, so non-ASCII output is not garbled even on the Windows PowerShell 5.1 fallback. The environment is model-friendly: `NO_COLOR=1 PAGER=cat GIT_PAGER=cat` (no `TERM=dumb` — a POSIX concept), with explicit caller-provided entries still winning.
|
||||
|
||||
Each child process selects `ExecutionPolicy Bypass` so host-generated scripts can run without changing the user's persistent policy. Machine and user Group Policy still take precedence.
|
||||
|
||||
```text
|
||||
const execution = await ctx.shell.execute(ctx.shell.resolve({ command: 'Get-ChildItem' }))
|
||||
const result = await execution.result()
|
||||
@@ -88,7 +86,7 @@ This section explains the design of the executor and points at the code that rea
|
||||
|
||||
### Design concept
|
||||
|
||||
The executor is the PowerShell Service Provider for the `ctx.shell` seam built on the subprocess capability: it owns everything pwsh-shaped — executable resolution, command defaulting and caps, deadline fusion and cause classification, UTF-8 output pinning, the model-friendly terminal environment, and the background read merge — while managed-range mechanics (bounded spill-backed output, credential scrub, termination escalation, quiescence, and disposal) belong to the subprocess service. Every call spawns a fresh non-interactive `pwsh -Command` with `-NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass`, so commands are deterministic and profile state never leaks between calls.
|
||||
The executor is the PowerShell Service Provider for the `ctx.shell` seam built on the subprocess capability: it owns everything pwsh-shaped — executable resolution, command defaulting and caps, deadline fusion and cause classification, UTF-8 output pinning, the model-friendly terminal environment, and the background read merge — while managed-range mechanics (bounded spill-backed output, credential scrub, termination escalation, quiescence, and disposal) belong to the subprocess service. Every call spawns a fresh non-interactive `pwsh -Command` with `-NoLogo -NoProfile -NonInteractive`, so commands are deterministic and profile state never leaks between calls.
|
||||
|
||||
### Source map
|
||||
|
||||
@@ -100,7 +98,7 @@ The executor is the PowerShell Service Provider for the `ctx.shell` seam built o
|
||||
|
||||
### Main flow
|
||||
|
||||
A call runs through three steps: `resolve()` fills `workdir`/`timeoutMs`/`stdoutMaxBytes` from config (capping the per-call `timeoutMs` override); the executor builds the pwsh argv — `pwsh -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command <encoding preamble + command>` — fuses the config-clamped timeout with the caller's abort signal into one deadline, and spawns through `ctx.subprocess` with explicit byte caps and the `graceMs`; the settled outcome is classified and projected into a `ShellRunResult`. Windows reports forced termination as exit 1 without a signal, so signal-stamped facts are POSIX-only there; the timeout/abort classification is platform-independent.
|
||||
A call runs through three steps: `resolve()` fills `workdir`/`timeoutMs`/`stdoutMaxBytes` from config (capping the per-call `timeoutMs` override); the executor builds the pwsh argv — `pwsh -NoLogo -NoProfile -NonInteractive -Command <encoding preamble + command>` — fuses the config-clamped timeout with the caller's abort signal into one deadline, and spawns through `ctx.subprocess` with explicit byte caps and the `graceMs`; the settled outcome is classified and projected into a `ShellRunResult`. Windows reports forced termination as exit 1 without a signal, so signal-stamped facts are POSIX-only there; the timeout/abort classification is platform-independent.
|
||||
|
||||
The foreground deadline starts before argv preparation and retains the same signal and remaining budget through execution. Preparation timeout returns empty output, `timedOut: true`, and null `exitCode` and `signal`; caller cancellation before process publication still rejects. Late preparation success or failure cannot trigger a spawn.
|
||||
|
||||
|
||||
@@ -59,8 +59,6 @@ kind: "package-reference"
|
||||
|
||||
通过等待执行句柄的 `result()` 投影来运行命令;非零退出、超时或取消都会返回描述性结果,只有基础设施失败才会导致调用被拒绝。命令字符串作为单个参数传给 `-Command`:由 PowerShell 自己解析文本,不存在中间 shell,因此没有需要转义的 shell 引号层,原生 Win32 路径也原样通过。每条命令都先固定 UTF-8 输出,因此即使在 Windows PowerShell 5.1 兜底上,非 ASCII 输出也不会乱码。环境默认面向模型:`NO_COLOR=1 PAGER=cat GIT_PAGER=cat`(没有 `TERM=dumb`——那是 POSIX 概念),调用方显式提供的条目仍然优先。
|
||||
|
||||
每个子进程使用 `ExecutionPolicy Bypass`,使主机生成的脚本可以运行,而不修改用户的持久策略。计算机和用户组策略仍然优先。
|
||||
|
||||
```text
|
||||
const execution = await ctx.shell.execute(ctx.shell.resolve({ command: 'Get-ChildItem' }))
|
||||
const result = await execution.result()
|
||||
@@ -88,7 +86,7 @@ if (result.timedOut) console.log('timed out after', result.timeoutMs)
|
||||
|
||||
### 设计概念
|
||||
|
||||
本执行器是基于 subprocess 能力的 `ctx.shell` seam 的 PowerShell Service Provider:它负责所有 pwsh 层职责——可执行文件解析、命令默认化与上限、deadline 融合与原因分类、UTF-8 输出固定、面向模型的终端环境,以及后台读取合并——而 managed-range 机制(有界 spill 输出、凭据清除、终止升级、完全停稳与 dispose(资源释放))属于 subprocess 服务。每次调用都 spawn 全新的非交互 `pwsh -Command`,并带 `-NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass`,因此命令是确定性的,profile 状态绝不会在调用之间泄漏。
|
||||
本执行器是基于 subprocess 能力的 `ctx.shell` seam 的 PowerShell Service Provider:它负责所有 pwsh 层职责——可执行文件解析、命令默认化与上限、deadline 融合与原因分类、UTF-8 输出固定、面向模型的终端环境,以及后台读取合并——而 managed-range 机制(有界 spill 输出、凭据清除、终止升级、完全停稳与 dispose(资源释放))属于 subprocess 服务。每次调用都 spawn 全新的非交互 `pwsh -Command`,并带 `-NoLogo -NoProfile -NonInteractive`,因此命令是确定性的,profile 状态绝不会在调用之间泄漏。
|
||||
|
||||
### 源码地图
|
||||
|
||||
@@ -100,7 +98,7 @@ if (result.timedOut) console.log('timed out after', result.timeoutMs)
|
||||
|
||||
### 主要流程
|
||||
|
||||
一次调用分三步:`resolve()` 从配置填充 `workdir`/`timeoutMs`/`stdoutMaxBytes`(并限制每次调用的 `timeoutMs` 覆盖值);执行器构建 pwsh argv——`pwsh -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command <编码 preamble + 命令>`——把按配置钳位的超时与调用方的中止信号融合为一个 deadline,再以显式字节上限与 `graceMs` 通过 `ctx.subprocess` spawn;结算的结果被分类并投影为 `ShellRunResult`。Windows 把强制终止报告为退出码 1 且无信号,因此带信号标记的事实在那里仅限 POSIX;超时/取消分类则与平台无关。
|
||||
一次调用分三步:`resolve()` 从配置填充 `workdir`/`timeoutMs`/`stdoutMaxBytes`(并限制每次调用的 `timeoutMs` 覆盖值);执行器构建 pwsh argv——`pwsh -NoLogo -NoProfile -NonInteractive -Command <编码 preamble + 命令>`——把按配置钳位的超时与调用方的中止信号融合为一个 deadline,再以显式字节上限与 `graceMs` 通过 `ctx.subprocess` spawn;结算的结果被分类并投影为 `ShellRunResult`。Windows 把强制终止报告为退出码 1 且无信号,因此带信号标记的事实在那里仅限 POSIX;超时/取消分类则与平台无关。
|
||||
|
||||
前台 deadline 从 argv 准备开始,并在准备与执行之间保持同一信号和剩余预算。准备阶段超时返回空输出、`timedOut: true`,且 `exitCode` 和 `signal` 均为 `null`;调用方在发布进程前取消仍会拒绝调用。准备晚到的成功或失败不会触发 spawn。
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Local PowerShell Service Provider for the bash capability seam. Each command runs
|
||||
* as `pwsh -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command <command>` in a managed
|
||||
* as `pwsh -NoLogo -NoProfile -NonInteractive -Command <command>` in a managed
|
||||
* process spawned through `ctx.subprocess`; the executor owns command
|
||||
* defaulting, deadlines and cause classification, the model-friendly terminal
|
||||
* environment, and the model-facing stdout/stderr merge for background reads.
|
||||
@@ -191,8 +191,7 @@ export class PwshLocalExecutor extends ShellExecutor {
|
||||
* `@deepseek-ai/dsh-pwsh-sandbox`).
|
||||
*/
|
||||
protected argv(spec: ShellExecSpec): string[] {
|
||||
return [this.pwshPath, '-NoLogo', '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command',
|
||||
`${ENCODING_PREAMBLE}${spec.command}`]
|
||||
return [this.pwshPath, '-NoLogo', '-NoProfile', '-NonInteractive', '-Command', `${ENCODING_PREAMBLE}${spec.command}`]
|
||||
}
|
||||
|
||||
/** Map one resolved spec plus its argv onto a fully-specified subprocess spawn. */
|
||||
|
||||
@@ -237,10 +237,8 @@ describe('spawn construction (pure, every platform)', () => {
|
||||
await run(ctx.shell, ctx.shell.resolve({ command: 'Write-Output 你好' }))
|
||||
expect(subprocess.specs).toHaveLength(1)
|
||||
const { argv } = subprocess.specs[0]!
|
||||
expect(argv.slice(0, 7)).toEqual([
|
||||
expect.any(String), '-NoLogo', '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command',
|
||||
])
|
||||
expect(argv[7]).toBe(`${ENCODING_PREAMBLE}Write-Output 你好`)
|
||||
expect(argv.slice(0, 5)).toEqual([expect.any(String), '-NoLogo', '-NoProfile', '-NonInteractive', '-Command'])
|
||||
expect(argv[5]).toBe(`${ENCODING_PREAMBLE}Write-Output 你好`)
|
||||
expect(ENCODING_PREAMBLE).toContain('[Console]::OutputEncoding')
|
||||
expect(ENCODING_PREAMBLE).toContain('$OutputEncoding')
|
||||
})
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
# After editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/shell/shell-env/README.md
|
||||
/:
|
||||
en: 03aa15ebe8f97a43
|
||||
zh: b606e28eb19c933e
|
||||
en: c54342d0151b089c
|
||||
zh: 9ad6284f24c5e3f6
|
||||
/deepseek-ai-dsh-shell-env:
|
||||
en: d26b77029fa722ab
|
||||
zh: 8e6bed763372b561
|
||||
/deepseek-ai-dsh-shell-env/summary:
|
||||
en: f27f8c4cdea7bcd4
|
||||
zh: 33cf3b28330bdf0f
|
||||
en: 021be213f32988e1
|
||||
zh: f3ce1ef82377b4f4
|
||||
/deepseek-ai-dsh-shell-env/table-of-contents:
|
||||
en: d152484eb41ac6b4
|
||||
zh: 09388d293f9be9cb
|
||||
@@ -19,16 +19,16 @@
|
||||
zh: 7753b97c91743901
|
||||
/deepseek-ai-dsh-shell-env/use-this-package/what-every-shell-call-receives:
|
||||
en: eae86664fefc6690
|
||||
zh: b8829aae034599c1
|
||||
zh: 05a4e5946c8d2b87
|
||||
/deepseek-ai-dsh-shell-env/use-this-package/adding-your-own-environment-facts:
|
||||
en: 8e3813746d322857
|
||||
zh: 91f10c71a21d8e80
|
||||
en: 21ef223626e35905
|
||||
zh: 5c468055099b0135
|
||||
/deepseek-ai-dsh-shell-env/use-this-package/choosing-the-harness-home:
|
||||
en: 0041438f2b7f7e4f
|
||||
zh: 93fa165b07adcdf4
|
||||
/deepseek-ai-dsh-shell-env/use-this-package/what-can-go-wrong:
|
||||
en: bf8db851cc8d373d
|
||||
zh: 27e046770564c60c
|
||||
en: 42c75945b581b7e6
|
||||
zh: 6b5c487cfa28b206
|
||||
/deepseek-ai-dsh-shell-env/understand-the-implementation:
|
||||
en: f782557ca8da3138
|
||||
zh: e8f0082b9692cacc
|
||||
@@ -39,20 +39,20 @@
|
||||
en: 72eba3bfadcfa581
|
||||
zh: dcd83e02a1910f27
|
||||
/deepseek-ai-dsh-shell-env/understand-the-implementation/collection:
|
||||
en: 24b540add25646f3
|
||||
zh: ba759ae3164cb1a3
|
||||
en: 0c2d7d5b2131630f
|
||||
zh: b44d4f0ddf89421b
|
||||
/deepseek-ai-dsh-shell-env/further-exploration:
|
||||
en: 6a4175df7b7341c7
|
||||
zh: 7885831274008502
|
||||
/deepseek-ai-dsh-shell-env/model-experience:
|
||||
en: 6e1e1b75c1fc4e94
|
||||
zh: a8cb0c273e040975
|
||||
en: 8acf3cd9aeef5f55
|
||||
zh: 69725e78f73c2f16
|
||||
/deepseek-ai-dsh-shell-env/model-experience/kv-cache-effect:
|
||||
en: e95e649f0410fa75
|
||||
zh: b3d64877420b4c0d
|
||||
/deepseek-ai-dsh-shell-env/known-limitations-and-deferred-work:
|
||||
en: 2a4da893f45fa8c4
|
||||
zh: a06bd59769c78e53
|
||||
en: be9888161315f026
|
||||
zh: 97d1d05c1c322596
|
||||
/deepseek-ai-dsh-shell-env/known-limitations-and-deferred-work/dev-note:
|
||||
en: e67f633dc6fe7773
|
||||
zh: 3bb0c20c93f16f98
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
description: "Configure per-call Harness shell variables and directories for deployment-supplied commands."
|
||||
description: "The managed DSH_* shell environment for users and maintainers choosing, configuring, or extending the environment every model shell call runs with."
|
||||
kind: "package-reference"
|
||||
---
|
||||
|
||||
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
|
||||
|
||||
## Summary
|
||||
|
||||
`dsh-shell-env` gives Bash and PowerShell tool calls current Harness home, session, and profile variables. Plugins can contribute declared `DSH_*` values and directories for deployment-supplied commands. Each call receives the current registrations without changing `process.env` or the user's shell configuration. Duplicate variable ownership and undeclared values fail loudly. Registrations follow plugin lifetimes; the shell tools retain their schemas and prompts.
|
||||
`dsh-shell-env` provides the trusted `DSH_*` environment that every model shell call — bash or pwsh — runs with: built-in facts such as `DSH_HOME`, `DSH_SHELL=1`, the agent's `DSH_SESSION_ID`, and the launched profile's `DSH_PROFILE` and `DSH_PROFILE_DIR`. Plugin authors can register their own facts with declared keys, collected per execution and disposed with their plugin; duplicate ownership or undeclared runtime keys fail loudly instead of silently overwriting. The registry changes nothing else the model sees — the shell tools own their own schemas and prompts. Choose it in any composition that mounts a model shell tool; configuration only picks the Harness home directory.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
@@ -52,8 +52,6 @@ export function apply(ctx: Context): void {
|
||||
|
||||
Contributors must declare every key they return; returning an undeclared or non-string value fails the call. Registration is disposed with the registering plugin, so hot-reloading a plugin removes its facts.
|
||||
|
||||
Contributors may also declare absolute `commandDirectories`. The registry prepends these directories to the local shell's `PATH` in contributor-name order, preserving each contributor's directory order and removing repeated directories. The inherited `PATH` follows them. With no command directories, it leaves `PATH` unchanged. Directory registration does not install commands or verify their contents; the contributing plugin owns those files and their lifetime. The [Office commands plugin](../../skill/skill-office/README.md) supplies `dsoffice` and `office-check` this way.
|
||||
|
||||
### Choosing the Harness home
|
||||
|
||||
The single config field picks the home directory exposed as `DSH_HOME`; the default resolution order is the `dshHome` config, then ambient `$DSH_HOME`, then `~/.dsh`.
|
||||
@@ -66,7 +64,7 @@ The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-a
|
||||
|
||||
### What can go wrong
|
||||
|
||||
Two contributors declaring the same key, or a contributor claiming a reserved built-in (`DSH_HOME`, `DSH_SHELL`, `DSH_SESSION_ID`, `DSH_PROFILE`, `DSH_PROFILE_DIR`), fails plugin load loudly. A `DSH_*` key must be all-caps with underscores (for example `DSH_REGION`), and a missing description fails registration. Relative command directories also fail registration.
|
||||
Two contributors declaring the same key, or a contributor claiming a reserved built-in (`DSH_HOME`, `DSH_SHELL`, `DSH_SESSION_ID`, `DSH_PROFILE`, `DSH_PROFILE_DIR`), fails plugin load loudly. A `DSH_*` key must be all-caps with underscores (for example `DSH_REGION`), and a missing description fails registration.
|
||||
|
||||
-----
|
||||
|
||||
@@ -92,7 +90,7 @@ This section explains the design decisions behind the registry and points at the
|
||||
|
||||
### Collection
|
||||
|
||||
`collect(execution)` combines built-in facts, contributor values, and any command-directory `PATH` into a frozen snapshot passed through `ShellExecRequest.dshEnv`. This trusted snapshot takes precedence over ordinary caller environment entries. `list()` enumerates variable declarations without running resolvers; it does not include command directories or execution-dependent values.
|
||||
`collect(execution)` starts from the built-ins, adds the session id when the execution carries an agent, then merges each registered contributor's resolved values sorted by contributor name. The result is a frozen, key-sorted snapshot passed through `ShellExecRequest.dshEnv`. `list()` enumerates declarations without running resolvers, so it cannot reflect execution-dependent values.
|
||||
|
||||
</details>
|
||||
|
||||
@@ -115,7 +113,7 @@ Read these pages when the package-level contract is not enough. They move from t
|
||||
<a id="model-experience"></a>
|
||||
## Model Experience
|
||||
|
||||
Indirectly, through the shell tools (`dsh-tool-bash`, `dsh-tool-pwsh`), which expose this registry's managed `DSH_*` facts and command lookup paths in every shell-tool call.
|
||||
Indirectly, through the shell tools (`dsh-tool-bash`, `dsh-tool-pwsh`), which expose this registry's managed `DSH_*` facts in every shell-tool call.
|
||||
|
||||
#### KV Cache effect
|
||||
|
||||
@@ -129,7 +127,6 @@ The managed environment never enters the request prefix, so it does not invalida
|
||||
These limits define when the registry is a poor fit or needs care. They are current package constraints, not a task backlog.
|
||||
|
||||
- **`list()` enumerates plugin-contributed variables only** — registry-owned built-ins (`DSH_HOME`, `DSH_SHELL`, `DSH_SESSION_ID`, `DSH_PROFILE`, `DSH_PROFILE_DIR`) are not included, so diagnostics, prompt, or UI code must not treat `list()` as an exhaustive environment catalog.
|
||||
- **Command directories belong to the local host.** A remote execution provider needs commands installed and configured in its own execution environment; local directory contributions do not provision the remote host.
|
||||
|
||||
<a id="dev-note"></a>
|
||||
### Dev Note
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
description: "配置每次 Harness shell 调用的变量,以及部署提供的命令目录。"
|
||||
description: "受管 DSH_* shell 环境,供选择、配置或扩展每次模型 shell 调用所运行环境的使用者与维护者阅读。"
|
||||
kind: "package-reference"
|
||||
---
|
||||
|
||||
@@ -9,7 +9,7 @@ kind: "package-reference"
|
||||
|
||||
## 概述
|
||||
|
||||
`dsh-shell-env` 为 Bash 和 PowerShell 工具调用提供当前的 Harness 主目录、会话与 profile 变量。插件可以贡献已声明的 `DSH_*` 值,以及部署提供的命令目录。每次调用都会收到当前注册内容,不修改 `process.env` 或用户 shell 配置。变量所有权重复或返回未声明的值会明确报错。注册项随插件生命周期释放;shell 工具保留各自的 schema 与提示词。
|
||||
`dsh-shell-env` 提供每次模型 shell 调用——bash 或 pwsh——所运行的受信 `DSH_*` 环境:内置事实如 `DSH_HOME`、`DSH_SHELL=1`、agent(智能体)的 `DSH_SESSION_ID`,以及所启动 profile 的 `DSH_PROFILE` 与 `DSH_PROFILE_DIR`。插件作者可以注册自己的事实,带声明键、按每次执行收集,并随插件释放;重复所有权或未声明的运行时键会明确报错,而不是静默覆盖。注册表不会改变模型看到的其他任何内容——shell 工具拥有各自的 schema 与提示词。任何挂载了模型 shell 工具的组合都适合选择它;配置只决定 Harness 主目录。
|
||||
|
||||
## 目录
|
||||
|
||||
@@ -29,7 +29,7 @@ kind: "package-reference"
|
||||
|
||||
### 每次 shell 调用都会收到什么
|
||||
|
||||
每次调用都会收到 `DSH_HOME`(Harness 主目录的绝对路径)、`DSH_SHELL=1`,agent(智能体)调用还会收到 `DSH_SESSION_ID`(调用方会话的 id)。启动器提供了 profile 上下文时,每次调用还会收到 `DSH_PROFILE`(profile 名)与 `DSH_PROFILE_DIR`(其绝对目录;其 `node_modules` 只存放 profile 自行安装的包,harness 自带的组合包从 dsh 安装目录解析);不带 profile 启动的组合则两者都没有。
|
||||
每次调用都会收到 `DSH_HOME`(Harness 主目录的绝对路径)、`DSH_SHELL=1`,agent 调用还会收到 `DSH_SESSION_ID`(调用方会话的 id)。启动器提供了 profile 上下文时,每次调用还会收到 `DSH_PROFILE`(profile 名)与 `DSH_PROFILE_DIR`(其绝对目录;其 `node_modules` 只存放 profile 自行安装的包,harness 自带的组合包从 dsh 安装目录解析);不带 profile 启动的组合则两者都没有。
|
||||
|
||||
### 添加你自己的环境事实
|
||||
|
||||
@@ -52,8 +52,6 @@ export function apply(ctx: Context): void {
|
||||
|
||||
contributor 必须声明它返回的每个键;返回未声明或非字符串的值会让该次调用失败。注册随注册插件的释放而释放,因此热重载插件会移除它的事实。
|
||||
|
||||
contributor 还可以声明绝对路径的 `commandDirectories`。注册表按 contributor 名称顺序将这些目录前置到本地 shell 的 `PATH`,保留各 contributor 内的目录顺序并去除重复目录,随后接上继承的 `PATH`。没有命令目录时,`PATH` 保持不变。目录注册不安装命令或验证其内容;贡献插件负责这些文件及其生命周期。[Office 命令插件](../../skill/skill-office/README.zh.md) 通过这种方式提供 `dsoffice` 和 `office-check`。
|
||||
|
||||
### 选择 Harness 主目录
|
||||
|
||||
唯一配置字段决定暴露为 `DSH_HOME` 的主目录;默认解析顺序为 `dshHome` 配置、环境变量 `$DSH_HOME`,然后是 `~/.dsh`。
|
||||
@@ -66,7 +64,7 @@ contributor 还可以声明绝对路径的 `commandDirectories`。注册表按 c
|
||||
|
||||
### 可能出什么问题
|
||||
|
||||
两个 contributor 声明同一个键,或 contributor 声称拥有保留内置键(`DSH_HOME`、`DSH_SHELL`、`DSH_SESSION_ID`、`DSH_PROFILE`、`DSH_PROFILE_DIR`),都会导致插件加载时明确报错。`DSH_*` 键必须全大写并带下划线(例如 `DSH_REGION`),缺少描述也会让注册失败。相对命令目录也会导致注册失败。
|
||||
两个 contributor 声明同一个键,或 contributor 声称拥有保留内置键(`DSH_HOME`、`DSH_SHELL`、`DSH_SESSION_ID`、`DSH_PROFILE`、`DSH_PROFILE_DIR`),都会导致插件加载时明确报错。`DSH_*` 键必须全大写并带下划线(例如 `DSH_REGION`),缺少描述也会让注册失败。
|
||||
|
||||
-----
|
||||
|
||||
@@ -92,7 +90,7 @@ contributor 还可以声明绝对路径的 `commandDirectories`。注册表按 c
|
||||
|
||||
### 收集
|
||||
|
||||
`collect(execution)` 将内置事实、contributor 值和命令目录的 `PATH` 合并为冻结快照,通过 `ShellExecRequest.dshEnv` 传递。该受信快照优先于普通调用方环境项。`list()` 枚举变量声明而不运行 resolver;不包含命令目录或依赖执行的值。
|
||||
`collect(execution)` 从内置键出发,当执行携带 agent 时加入会话 id,再按 contributor 名称排序合并每个已注册 contributor 解析出的值。结果是一个冻结、按键排序的快照,通过 `ShellExecRequest.dshEnv` 传递。`list()` 枚举声明而不运行 resolver,因此无法反映依赖执行的值。
|
||||
|
||||
</details>
|
||||
|
||||
@@ -115,7 +113,7 @@ contributor 还可以声明绝对路径的 `commandDirectories`。注册表按 c
|
||||
<a id="model-experience"></a>
|
||||
## 模型体验
|
||||
|
||||
通过 shell 工具(`dsh-tool-bash`、`dsh-tool-pwsh`)间接产生影响;这些工具在每次调用中提供本注册表的受管 `DSH_*` 事实和命令查找路径。
|
||||
通过 shell 工具(`dsh-tool-bash`、`dsh-tool-pwsh`)间接产生影响;这些工具把本注册表的受管 `DSH_*` 事实暴露在每次 shell 工具调用中。
|
||||
|
||||
#### KV Cache 影响
|
||||
|
||||
@@ -129,7 +127,6 @@ contributor 还可以声明绝对路径的 `commandDirectories`。注册表按 c
|
||||
这些限制说明注册表何时不合适或需要小心使用。它们是当前包约束,不是任务积压。
|
||||
|
||||
- **`list()` 只枚举插件贡献的变量**——注册表自有的内置键(`DSH_HOME`、`DSH_SHELL`、`DSH_SESSION_ID`、`DSH_PROFILE`、`DSH_PROFILE_DIR`)不包含在内,因此诊断、提示词或 UI 代码不得把 `list()` 当作完整的环境目录。
|
||||
- **命令目录属于本地主机。** 远程执行提供方需要在自己的执行环境中安装和配置命令;本地目录贡献不会为远程主机准备资源。
|
||||
|
||||
<a id="dev-note"></a>
|
||||
### 开发备注
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
*/
|
||||
|
||||
import { Service, type Context } from '@deepseek-ai/cordis'
|
||||
import { delimiter, isAbsolute } from 'node:path'
|
||||
import z from '@deepseek-ai/schemastery'
|
||||
import { DSH_ENV_PREFIX } from '@deepseek-ai/dsh-shell'
|
||||
import type { DshEnvironment, DshEnvironmentKey } from '@deepseek-ai/dsh-shell'
|
||||
@@ -52,8 +51,6 @@ export interface BashEnvVariable {
|
||||
export interface BashEnvContributor {
|
||||
/** Stable contributor name used in diagnostics and duplicate detection. */
|
||||
name: string
|
||||
/** Absolute command directories prepended to the local shell PATH; no process-global environment is changed. */
|
||||
commandDirectories?: readonly string[]
|
||||
/** Complete set of `DSH_*` keys this contributor may return. */
|
||||
variables: Readonly<Record<DshEnvironmentKey, BashEnvVariable>>
|
||||
/**
|
||||
@@ -122,9 +119,6 @@ export class ShellEnvRegistry extends Service {
|
||||
if (this.contributors.has(contributor.name)) {
|
||||
throw new Error(`bash env contributor "${contributor.name}" is already registered`)
|
||||
}
|
||||
if (contributor.commandDirectories?.some(directory => !isAbsolute(directory))) {
|
||||
throw new Error(`bash env contributor "${contributor.name}" requires absolute command directories`)
|
||||
}
|
||||
|
||||
const variables = Object.entries(contributor.variables) as [DshEnvironmentKey, BashEnvVariable][]
|
||||
for (const [key, variable] of variables) {
|
||||
@@ -187,14 +181,7 @@ export class ShellEnvRegistry extends Service {
|
||||
}
|
||||
}
|
||||
|
||||
const directories = [...this.contributors.values()].sort((left, right) => left.name.localeCompare(right.name))
|
||||
.flatMap(contributor => contributor.commandDirectories ?? [])
|
||||
return Object.freeze({
|
||||
...Object.fromEntries(Object.entries(values).sort(([left], [right]) => left.localeCompare(right))),
|
||||
...(directories.length === 0 ? {} : {
|
||||
PATH: [...new Set(directories), ...(process.env.PATH ? [process.env.PATH] : [])].join(delimiter),
|
||||
}),
|
||||
})
|
||||
return Object.freeze(Object.fromEntries(Object.entries(values).sort(([left], [right]) => left.localeCompare(right))))
|
||||
}
|
||||
|
||||
// TODO(bash-env-list-builtins): Include registry-owned built-ins before diagnostics,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import { homedir } from 'node:os'
|
||||
import { delimiter, join, resolve } from 'node:path'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { ToolCallId } from '@deepseek-ai/dsh-llm'
|
||||
@@ -164,39 +164,6 @@ describe('ShellEnvRegistry', () => {
|
||||
variables: { DSH_BLANK_DESCRIPTION: { description: ' ' } },
|
||||
resolve: () => ({}),
|
||||
})).toThrow(/must describe/)
|
||||
expect(() => registry.register({
|
||||
name: 'relative-command-directory', commandDirectories: ['relative/bin'], variables: {}, resolve: () => ({}),
|
||||
})).toThrow(/absolute command directories/)
|
||||
})
|
||||
|
||||
it('prepends unique command directories in contributor order without changing the process PATH', async () => {
|
||||
const ambient = resolve('ambient-bin'), first = resolve('first-bin'), second = resolve('second-bin')
|
||||
vi.stubEnv('PATH', ambient)
|
||||
const ctx = new Context()
|
||||
try {
|
||||
const registry = new ShellEnvRegistry(ctx, { dshHome: './test-dsh-home' })
|
||||
registry.register({ name: 'b', commandDirectories: [second, first], variables: {}, resolve: () => ({}) })
|
||||
const dispose = registry.register({ name: 'a', commandDirectories: [first], variables: {}, resolve: () => ({}) })
|
||||
expect(registry.collect(execution()).PATH).toBe([first, second, ambient].join(delimiter))
|
||||
expect(process.env.PATH).toBe(ambient)
|
||||
dispose()
|
||||
expect(registry.collect(execution()).PATH).toBe([second, first, ambient].join(delimiter))
|
||||
} finally {
|
||||
await ctx.fiber.dispose()
|
||||
}
|
||||
})
|
||||
|
||||
it('exposes only contributed directories when the inherited PATH is empty', async () => {
|
||||
vi.stubEnv('PATH', '')
|
||||
const ctx = new Context()
|
||||
try {
|
||||
const registry = new ShellEnvRegistry(ctx, { dshHome: './test-dsh-home' }), bin = resolve('command-bin')
|
||||
registry.register({ name: 'local-command', commandDirectories: [bin], variables: {}, resolve: () => ({}) })
|
||||
expect(registry.collect(execution()).PATH).toBe(bin)
|
||||
expect(process.env.PATH).toBe('')
|
||||
} finally {
|
||||
await ctx.fiber.dispose()
|
||||
}
|
||||
})
|
||||
|
||||
it('rejects undeclared variables returned by a contributor', () => {
|
||||
|
||||
@@ -9,26 +9,26 @@
|
||||
en: f855ac5e12b4e604
|
||||
zh: b2e875d31d56cb2e
|
||||
/deepseek-ai-dsh-skill-office/summary:
|
||||
en: 6203c381070b96f6
|
||||
zh: 4ff7a778f7c43b89
|
||||
en: 42234b5ae272eba3
|
||||
zh: 9df9a2a3ea9a9697
|
||||
/deepseek-ai-dsh-skill-office/table-of-contents:
|
||||
en: c361195a73cf5755
|
||||
zh: 745b04e7be0be369
|
||||
/deepseek-ai-dsh-skill-office/use-this-package:
|
||||
en: f9dfa0282f0aacfd
|
||||
zh: 989a91e5f63b757f
|
||||
en: 8bfffcb6f69eb597
|
||||
zh: a859a1ed72b43fde
|
||||
/deepseek-ai-dsh-skill-office/use-this-package/progressive-loading:
|
||||
en: fd13620d7ddda0a6
|
||||
zh: 08ad6f76b0a56f47
|
||||
/deepseek-ai-dsh-skill-office/use-this-package/structural-checks:
|
||||
en: 81efb4310276e7a9
|
||||
zh: 7c7a0fa8f2512c08
|
||||
en: 2c1f1fa964a3901f
|
||||
zh: 43436136362d4158
|
||||
/deepseek-ai-dsh-skill-office/understand-the-implementation:
|
||||
en: a06162bae16bddad
|
||||
zh: 40c3f73993b2fe7d
|
||||
en: 81eafcae9e11965e
|
||||
zh: 082123177625b3d8
|
||||
/deepseek-ai-dsh-skill-office/further-exploration:
|
||||
en: a93f275e608a6bab
|
||||
zh: 191e8d36c433e8a2
|
||||
en: 23830c100287ffb8
|
||||
zh: 7a4100e87057bc20
|
||||
/deepseek-ai-dsh-skill-office/model-experience:
|
||||
en: bbd979fd20e7b308
|
||||
zh: 7e6b6e04f2aedb02
|
||||
|
||||
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
|
||||
|
||||
## Summary
|
||||
|
||||
The catalog lists the Office router and six authoring or operation skills. Full instructions load on demand. Skills use `office-check` and `dsoffice` through existing shell tools without exposing interpreter or package paths. SDK and Desktop each mount the skill provider, command plugin, and dependency query; ordinary Web does not mount them by default.
|
||||
The catalog lists the Office router and six authoring or operation skills. Full instructions load on demand. Authoring skills use the selected Python and shared checker script; operation skills supply the configured Node and LibreOffice Kit CLI paths. SDK and Desktop mount the skill provider and dependency query separately.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
@@ -26,30 +26,20 @@ The catalog lists the Office router and six authoring or operation skills. Full
|
||||
<a id="use-this-package"></a>
|
||||
## Use this package
|
||||
|
||||
Mount the skill provider beside the skill registry and `dsh-tool-skill`, and the command plugin beside `dsh-shell-env`. The provider supplies instructions and checker resources; the command plugin makes `dsoffice` and `office-check` available to local Bash and PowerShell tool calls.
|
||||
Mount the skill provider beside the skill registry and `dsh-tool-skill`. The provider supplies instructions and checker resources; operation skills append absolute interpreter and CLI paths when loaded.
|
||||
|
||||
```yaml
|
||||
- name: '@deepseek-ai/dsh-skill-office'
|
||||
- name: '@deepseek-ai/dsh-skill-office/commands'
|
||||
|
||||
```
|
||||
|
||||
| Field | Default | Meaning |
|
||||
|---|---|---|
|
||||
| `assetRoot` | Carrier resources or packaged `assets/` | Absolute directory containing the seven skill folders and shared `scripts/`. |
|
||||
| `node` | Host Node.js | Standalone Node executable for operation skills; packaged carriers supply it explicitly. |
|
||||
| `cli` | Installed Kit CLI | Absolute CLI entry; `false` disables operation execution. |
|
||||
|
||||
Invalid skill assets reject registration. Loading instructions does not resolve interpreters or prepare engines. Bash or PowerShell owns approvals, sandboxing, cancellation, and output management. Resources and execution tools must refer to the same host.
|
||||
|
||||
The command plugin accepts separate configuration:
|
||||
|
||||
| Field | Default | Meaning |
|
||||
|---|---|---|
|
||||
| `command` | Carrier or installed Kit | Executable and fixed arguments for `dsoffice`; `false` omits that command. |
|
||||
| `checkCommand` | Configured Python checker | Executable and fixed arguments for `office-check`; `false` omits that command. |
|
||||
| `assetRoot` | Carrier resources or packaged `assets/` | Directory containing the shared checker and skills. |
|
||||
|
||||
Command overrides require an absolute executable as their first argument. Launchers receive the caller's remaining arguments unchanged and live only while the command plugin is mounted. Windows launchers preserve empty arguments, embedded quotes, trailing backslashes, and Unicode paths on Windows PowerShell 5.1 and PowerShell 7; PowerShell arrays supplied as one argument become comma-separated text. Launchers accept input file paths and do not forward pipeline input. A missing executable returns a nonzero exit code. The shell environment prepends their private directory to each tool call's `PATH`; it does not change the process or user's shell configuration. Disabling a command does not prevent skill loading. `office-check` directly runs the Python checker with the selected carrier interpreter or explicit `checkCommand`; it is omitted when neither is supplied. The checker uses only Python standard-library modules. Command registration and execution neither prepare nor copy a runtime. The general `load_workspace_dependencies` tool supplies authoring interpreters and libraries independently.
|
||||
|
||||
`DSH_PRIMARY_RUNTIME` selects authoring and checker resources. Kit commands use the carrier's Node.js when supplied, otherwise the host Node.js, so Python-only authoring payloads are supported.
|
||||
Invalid skill assets reject registration. Authoring instructions do not resolve renderer paths; loading operation instructions validates existing interpreter and CLI files without preparing engines. Bash or PowerShell owns approvals, sandboxing, cancellation, and output management. Resources and execution tools must refer to the same host.
|
||||
|
||||
<a id="progressive-loading"></a>
|
||||
### Progressive loading
|
||||
@@ -89,7 +79,7 @@ New files and content, data, or formula edits do not require rendering solely be
|
||||
|
||||
### Structural checks
|
||||
|
||||
`office-check <file> --out <report.json>` reads DOCX, PPTX, and XLSX without changing the source. The shared Python checker validates ZIP/XML and internal relationships, summarizes document structure, and optionally checks required text or slide/sheet counts. It requires only the Python standard library. Invalid files and report-write failures return a JSON failure report. Structural success does not establish appearance, feature preservation, or calculated formula values.
|
||||
`"<python>" "<skill-directory>/../scripts/check_office.py" <file> --out <report.json>` reads DOCX, PPTX, and XLSX without changing the source. The shared Python checker validates ZIP/XML and internal relationships, summarizes document structure, and optionally checks required text or slide/sheet counts. It requires only the Python standard library. Invalid files and report-write failures return a JSON failure report. Structural success does not establish appearance, feature preservation, or calculated formula values.
|
||||
|
||||
-----
|
||||
|
||||
@@ -101,12 +91,12 @@ New files and content, data, or formula edits do not require rendering solely be
|
||||
|
||||
The provider reads descriptions at registration and bodies on demand. Registry precedence, scope, and invocation permissions apply to every skill. Each loaded skill includes its resource directory for relative references. Disposal unregisters all candidates.
|
||||
|
||||
The provider registers no tools or activation projection. Loaded skill results remain logged through the generic skill loader. Resume and fork use existing session history and tool declaration reconciliation. The command plugin hides carrier-specific executable paths behind local launchers and registers their directory with the shell environment; disposal removes both the contribution and the directory. Windows launcher directories support long `DSH_HOME` paths; `PATH` entries use ordinary absolute paths. When an executable path exceeds the Windows process launch limit, the plugin temporarily links its parent directory through a short path under the system temporary directory and removes that link on disposal.
|
||||
The provider registers no tools or activation projection. Loaded skill results remain logged through the generic skill loader. Resume and fork use existing session history and tool declaration reconciliation. Interpreter and CLI paths are resolved only when an operation skill is loaded.
|
||||
|
||||
| File | Responsibility |
|
||||
|---|---|
|
||||
| [`src/index.ts`](src/index.ts) | Resource registration and unchanged skill bodies. |
|
||||
| [`src/commands.ts`](src/commands.ts) | Local launchers, carrier selection, and shell environment contribution. |
|
||||
|
||||
| [`assets/`](assets/) | Router, six subskills, and read-only OOXML checker. |
|
||||
|
||||
</details>
|
||||
@@ -118,7 +108,6 @@ The provider registers no tools or activation projection. Loaded skill results r
|
||||
|
||||
- [Skill registry](../skill/README.md) — discovery and precedence.
|
||||
- [Skill tool](../tool-skill/README.md) — model catalogs and loaded bodies.
|
||||
- [Shell environment](../../shell/shell-env/README.md) — command directories and managed variables.
|
||||
- [File delivery](../../deliverables/tool-present/README.md) — current source-path delivery.
|
||||
|
||||
-----
|
||||
|
||||
@@ -9,7 +9,7 @@ kind: "package-reference"
|
||||
|
||||
## 概述
|
||||
|
||||
目录列出 Office 路由及六个创作或操作技能,完整说明按需加载。技能通过现有 shell 工具使用 `office-check` 和 `dsoffice`,不暴露解释器或包路径。SDK 和 Desktop 分别挂载技能提供者、命令插件及依赖查询;普通 Web 默认不挂载它们。
|
||||
目录列出 Office 路由及六个创作或操作技能,完整说明按需加载。创作技能使用选定的 Python 和共享检查脚本;操作技能提供配置的 Node 与 LibreOffice Kit CLI 路径。SDK 和 Desktop 分别挂载技能提供者和依赖查询。
|
||||
|
||||
## 目录
|
||||
|
||||
@@ -26,30 +26,20 @@ kind: "package-reference"
|
||||
<a id="use-this-package"></a>
|
||||
## 使用本包
|
||||
|
||||
在技能注册表和 `dsh-tool-skill` 旁挂载技能提供者,在 `dsh-shell-env` 旁挂载命令插件。提供者负责说明和检查器资源;命令插件让本地 Bash 和 PowerShell 工具调用可以使用 `dsoffice` 与 `office-check`。
|
||||
在技能注册表和 `dsh-tool-skill` 旁挂载技能提供者。提供者负责说明和检查器资源;操作技能在加载时附加解释器和 CLI 的绝对路径。
|
||||
|
||||
```yaml
|
||||
- name: '@deepseek-ai/dsh-skill-office'
|
||||
- name: '@deepseek-ai/dsh-skill-office/commands'
|
||||
|
||||
```
|
||||
|
||||
| 字段 | 默认值 | 含义 |
|
||||
|---|---|---|
|
||||
| `assetRoot` | 载体资源或包内 `assets/` | 包含七个技能目录及共享 `scripts/` 的绝对路径。 |
|
||||
| `node` | 宿主 Node.js | 操作技能使用的独立 Node 可执行程序;打包载体显式提供。 |
|
||||
| `cli` | 已安装 Kit CLI | CLI 入口的绝对路径;`false` 禁用操作执行。 |
|
||||
|
||||
无效技能资源会阻止注册。加载说明不解析解释器或准备引擎。Bash 或 PowerShell 负责审批、沙箱、取消和输出管理。资源与执行工具必须位于同一主机。
|
||||
|
||||
命令插件接受独立配置:
|
||||
|
||||
| 字段 | 默认值 | 含义 |
|
||||
|---|---|---|
|
||||
| `command` | 载体或已安装 Kit | `dsoffice` 的可执行程序及固定参数;`false` 不提供该命令。 |
|
||||
| `checkCommand` | 配置的 Python 检查器 | `office-check` 的可执行程序及固定参数;`false` 不提供该命令。 |
|
||||
| `assetRoot` | 载体资源或包内 `assets/` | 包含共享检查器和技能的目录。 |
|
||||
|
||||
命令覆盖的首个参数必须是可执行程序的绝对路径。启动器原样接收调用方的其余参数,仅在命令插件挂载期间存在。Windows 启动器在 Windows PowerShell 5.1 和 PowerShell 7 下保留空参数、内嵌引号、末尾反斜杠和 Unicode 路径;作为单个参数传入的 PowerShell 数组会转为逗号分隔文本。启动器通过文件路径接收输入,不转发管道输入。可执行程序不存在时,命令返回非零退出码。shell 环境将启动器的私有目录前置到每次工具调用的 `PATH`;不修改进程或用户 shell 配置。禁用命令不阻止技能加载。`office-check` 使用所选载体解释器或显式 `checkCommand` 直接运行 Python 检查器;两者均未提供时不注册该命令。检查器只使用 Python 标准库。命令注册和执行均不准备或复制运行时。通用 `load_workspace_dependencies` 工具独立提供创作解释器和库。
|
||||
|
||||
`DSH_PRIMARY_RUNTIME` 选择创作和检查器资源。Kit 命令使用载体提供的 Node.js,否则使用宿主 Node.js,因此支持仅含 Python 的创作载荷。
|
||||
无效技能资源会阻止注册。创作说明不解析渲染器路径;操作说明加载时校验现有解释器和 CLI 文件,不准备引擎。Bash 或 PowerShell 负责审批、沙箱、取消和输出管理。资源与执行工具必须位于同一主机。
|
||||
|
||||
<a id="progressive-loading"></a>
|
||||
### 渐进加载
|
||||
@@ -89,7 +79,7 @@ flowchart TD
|
||||
|
||||
### 结构检查
|
||||
|
||||
`office-check <file> --out <report.json>` 读取 DOCX、PPTX 和 XLSX,不修改源文件。共享 Python 检查器验证 ZIP/XML 和内部关系、汇总文档结构,并可检查必需文本或幻灯片/工作表数量。它仅依赖 Python 标准库。无效文件和报告写入失败会返回 JSON 失败报告。结构检查成功不代表外观、功能保留或公式计算值通过验证。
|
||||
`"<python>" "<skill-directory>/../scripts/check_office.py" <file> --out <report.json>` 读取 DOCX、PPTX 和 XLSX,不修改源文件。共享 Python 检查器验证 ZIP/XML 和内部关系、汇总文档结构,并可检查必需文本或幻灯片/工作表数量。它仅依赖 Python 标准库。无效文件和报告写入失败会返回 JSON 失败报告。结构检查成功不代表外观、功能保留或公式计算值通过验证。
|
||||
|
||||
-----
|
||||
|
||||
@@ -101,12 +91,12 @@ flowchart TD
|
||||
|
||||
提供者在注册时读取摘要,按需读取正文。注册表优先级、作用域和调用权限适用于全部技能。加载结果包含用于解析相对引用的资源目录。卸载提供者会移除全部候选技能。
|
||||
|
||||
提供者不注册工具或激活投影。通用技能加载器记录已加载技能的结果。恢复和 fork 使用现有会话历史及工具声明更新机制。命令插件用本地启动器封装载体专属可执行路径,并将其目录注册到 shell 环境;释放时移除该贡献与目录。Windows 启动器目录支持较长的 `DSH_HOME` 路径;`PATH` 条目使用普通绝对路径。可执行程序路径超过 Windows 进程启动长度限制时,插件在系统临时目录下建立通往其父目录的短路径链接,并在释放时移除该链接。
|
||||
提供者不注册工具或激活投影。通用技能加载器记录已加载技能的结果。恢复和 fork 使用现有会话历史及工具声明更新机制。解释器和 CLI 路径仅在加载操作技能时解析。
|
||||
|
||||
| 文件 | 职责 |
|
||||
|---|---|
|
||||
| [`src/index.ts`](src/index.ts) | 注册资源并返回原始技能正文。 |
|
||||
| [`src/commands.ts`](src/commands.ts) | 本地启动器、载体选择和 shell 环境贡献。 |
|
||||
|
||||
| [`assets/`](assets/) | 路由、六个子技能与只读 OOXML 检查器。 |
|
||||
|
||||
</details>
|
||||
@@ -118,7 +108,6 @@ flowchart TD
|
||||
|
||||
- [技能注册表](../skill/README.zh.md) — 发现和优先级。
|
||||
- [技能工具](../tool-skill/README.zh.md) — 模型目录和加载正文。
|
||||
- [Shell 环境](../../shell/shell-env/README.zh.md) — 命令目录和受管变量。
|
||||
- [文件交付](../../deliverables/tool-present/README.zh.md) — 当前源文件路径交付。
|
||||
|
||||
-----
|
||||
|
||||
@@ -33,14 +33,16 @@ python-docx does not paginate or render documents. Do not represent ordinary rep
|
||||
|
||||
Run the shared structural checker when `office-check` is available:
|
||||
|
||||
In PowerShell, prefix the quoted Python executable with `&`. Resolve the checker path relative to this skill directory.
|
||||
|
||||
```text
|
||||
office-check <document.docx> --out <checks.json>
|
||||
"<python>" "<skill-directory>/../scripts/check_office.py" <document.docx> --out <checks.json>
|
||||
```
|
||||
|
||||
It checks ZIP/XML integrity and internal relationships, and reports paragraphs, logical table dimensions, and sections. Optional `--contains TEXT` arguments assert required text. A successful structural check does not verify pagination, clipping, fonts, or visual appearance. Compare the summary and reopened document with the user's request, including unchanged content that matters to an edit.
|
||||
|
||||
If `office-check` is unavailable, use an already configured environment to check the ZIP/XML package and internal relationships. Report the missing environment if those checks cannot run; do not claim structural validation.
|
||||
If the Python checker cannot run, use an already configured environment to check the ZIP/XML package and internal relationships. Report the missing environment if those checks cannot run; do not claim structural validation.
|
||||
|
||||
Deliver after the requested content and structural checks pass. Creating a file or changing content with incidental basic formatting does not require additional operations. Load `office-render` for requested images or visual inspection and for appearance-focused styling, spacing, sizing, or alignment changes unless the user explicitly skips rendering. For PDF output load `office-export-pdf`.
|
||||
Deliver after the requested content and structural checks pass. Creating a file or changing content with incidental basic formatting does not require additional operations. Load `office-render` for requested images or visual inspection. When styling, font color, spacing, sizing, or text alignment is the requested edit, load `office-render` and inspect the changed page even without a separate visual-check request, unless the user explicitly skips rendering. For PDF output load `office-export-pdf`.
|
||||
|
||||
Call `present({"files":[{"path":"report.docx"}]})` with the actual final DOCX path. It exposes the current source file without copying or preserving its bytes, so keep that file in place and do not present temporary QA reports unless requested. If `present` is unavailable, provide the final workspace path using the session's supported file delivery method.
|
||||
|
||||
@@ -5,12 +5,12 @@ description: Convert an Office document to a requested PDF with LibreOffice Kit.
|
||||
|
||||
# Export an Office document to PDF
|
||||
|
||||
Use the installed LibreOffice Kit CLI through the shell with `dsoffice`. Prefer bundled LibreOffice unless the user requests another installation. If the Office sidecar is missing, follow [sidecar setup](../office/references/sidecar.md): ask whether the user wants to download it and install only after they agree. Do not invoke system soffice, use npx, or install a replacement to work around a failed conversion with an installed sidecar.
|
||||
Use the installed LibreOffice Kit CLI through the shell with the absolute Node and CLI paths supplied below. In Bash, invoke `"<node>" "<cli>"`; in PowerShell, invoke `& "<node>" "<cli>"`. Prefer bundled LibreOffice unless the user requests another installation. If the Office sidecar is missing, follow [sidecar setup](../office/references/sidecar.md): ask whether the user wants to download it and install only after they agree. Do not invoke system soffice, use npx, or install a replacement to work around a failed conversion with an installed sidecar.
|
||||
|
||||
Convert the final saved Office file to a new PDF in the task workspace. Preserve the source. Quote each input and output path.
|
||||
|
||||
```text
|
||||
dsoffice convert --input <final-office-file> --output <new-file.pdf>
|
||||
"<node>" "<cli>" convert --input <final-office-file> --output <new-file.pdf>
|
||||
```
|
||||
|
||||
Check the command result, nonempty output, and `%PDF-` file signature. If a PDF reader library is already available, compare page count and extractable text with the requested content. If it is unavailable, report the extent of validation; do not generate images or install extra tools to validate a conversion. Spreadsheet PDF pages follow workbook print settings; change those settings only when requested. Conversion may differ from the native application's pagination or fonts.
|
||||
|
||||
@@ -57,13 +57,15 @@ Check that text and images fit the slide dimensions, titles form a useful sequen
|
||||
|
||||
Run the shared structural checker when `office-check` is available:
|
||||
|
||||
In PowerShell, prefix the quoted Python executable with `&`. Resolve the checker path relative to this skill directory.
|
||||
|
||||
```text
|
||||
office-check <presentation.pptx> --out <checks.json>
|
||||
"<python>" "<skill-directory>/../scripts/check_office.py" <presentation.pptx> --out <checks.json>
|
||||
```
|
||||
|
||||
It checks ZIP/XML integrity and internal relationships and reports slide count and extracted text. Use repeated `--contains TEXT` arguments for required slide text and `--count N` for a requested slide count; `--contains` excludes chart text and speaker notes. Reopen the file to check the requested edits, chart data, and notes. Structural success does not establish text fit, alignment, readable contrast, or rendering fidelity.
|
||||
|
||||
If `office-check` is unavailable, use an already configured environment to check the ZIP/XML package and internal relationships. Report the missing environment if those checks cannot run; do not claim structural validation.
|
||||
If the Python checker cannot run, use an already configured environment to check the ZIP/XML package and internal relationships. Report the missing environment if those checks cannot run; do not claim structural validation.
|
||||
|
||||
Deliver after the requested content and structural checks pass. Creating a file or changing content with incidental basic formatting does not require additional operations. Load `office-render` for requested images or visual inspection and for appearance-focused styling, spacing, sizing, or alignment changes unless the user explicitly skips rendering. For PDF output load `office-export-pdf`.
|
||||
|
||||
|
||||
@@ -5,12 +5,12 @@ description: Refresh spreadsheet formula caches and verify calculated values wit
|
||||
|
||||
# Recalculate spreadsheet formulas
|
||||
|
||||
Use the installed LibreOffice Kit CLI through the shell with `dsoffice`. Prefer bundled LibreOffice unless the user requests another installation. If the Office sidecar is missing, follow [sidecar setup](../office/references/sidecar.md): ask whether the user wants to download it and install only after they agree. Do not invoke system soffice, use npx, or install a replacement to work around a failed recalculation with an installed sidecar.
|
||||
Use the installed LibreOffice Kit CLI through the shell with the absolute Node and CLI paths supplied below. In Bash, invoke `"<node>" "<cli>"`; in PowerShell, invoke `& "<node>" "<cli>"`. Prefer bundled LibreOffice unless the user requests another installation. If the Office sidecar is missing, follow [sidecar setup](../office/references/sidecar.md): ask whether the user wants to download it and install only after they agree. Do not invoke system soffice, use npx, or install a replacement to work around a failed recalculation with an installed sidecar.
|
||||
|
||||
Keep the original workbook and write a new file in the task workspace. Quote each input and output path.
|
||||
|
||||
```text
|
||||
dsoffice recalculate --input <workbook.xlsx> --output <recalculated.xlsx>
|
||||
"<node>" "<cli>" recalculate --input <workbook.xlsx> --output <recalculated.xlsx>
|
||||
```
|
||||
|
||||
Verify the saved result with openpyxl, using `load_workspace_dependencies` for the Python path unless a project environment is required. Open it with both `data_only=False` and `data_only=True`. Check that formulas survive and cached results match the requested logic, expected totals, and cell types. Check required unchanged sheets, styles, and data. Recalculation does not validate business logic or guarantee Excel-specific feature compatibility.
|
||||
|
||||
@@ -5,7 +5,7 @@ description: Render requested document images or inspect visual layout with Libr
|
||||
|
||||
# Document images and visual inspection
|
||||
|
||||
Use the installed LibreOffice Kit CLI through the shell with `dsoffice`. Prefer bundled LibreOffice unless the user requests another installation. If the Office sidecar is missing, follow [sidecar setup](../office/references/sidecar.md): ask whether the user wants to download it and install only after they agree. Do not invoke system soffice, use npx, install a replacement renderer, or build an alternative rendering pipeline to work around a failed render with an installed sidecar.
|
||||
Use the installed LibreOffice Kit CLI through the shell with the absolute Node and CLI paths supplied below. In Bash, invoke `"<node>" "<cli>"`; in PowerShell, invoke `& "<node>" "<cli>"`. Prefer bundled LibreOffice unless the user requests another installation. If the Office sidecar is missing, follow [sidecar setup](../office/references/sidecar.md): ask whether the user wants to download it and install only after they agree. Do not invoke system soffice, use npx, install a replacement renderer, or build an alternative rendering pipeline to work around a failed render with an installed sidecar.
|
||||
|
||||
Before generating images only for inspection, establish that the current model accepts images. Otherwise finish the applicable content and structure checks and report the requested visual check as unavailable. Do not generate unreadable previews, retry image reads, or choose a second model. Requested image exports can still be delivered.
|
||||
|
||||
@@ -14,8 +14,8 @@ Before generating images only for inspection, establish that the current model a
|
||||
Keep the saved deliverable in the task workspace. For requested image exports, use the requested output directory and retain the images. For inspection-only previews, create a private, uniquely named temporary parent inside that workspace and pass a child path that does not yet exist as `--output-dir`; the CLI creates that child. On POSIX, use `preview_root=$(mktemp -d "./.dsh-office.XXXXXXXX")` (owner-only permissions) and `--output-dir "$preview_root/render"`. On PowerShell, create a parent named `.dsh-office-` plus `[guid]::NewGuid().ToString('N')` under the task workspace and pass an uncreated `render` child; it inherits the workspace ACL. Quote each input and output path. Retain the parent until image inspection finishes, then remove only that parent, including after a failed render. Never use a fixed directory name or remove the workspace itself.
|
||||
|
||||
```text
|
||||
dsoffice render --input <final-file> --output-dir <new-directory> --pages "1,3,4"
|
||||
dsoffice render --input <final-workbook.xlsx> --output-dir <new-directory> --sheet <exact-name> --range <A1-range>
|
||||
"<node>" "<cli>" render --input <final-file> --output-dir <new-directory> --pages "1,3,4"
|
||||
"<node>" "<cli>" render --input <final-workbook.xlsx> --output-dir <new-directory> --sheet <exact-name> --range <A1-range>
|
||||
```
|
||||
|
||||
For DOCX, PPTX, or PDF, batch requested pages/slides in one call. In PowerShell, quote comma-separated `--pages` values so they remain one argument. Inspect all pages only for a requested whole-document review. For worksheets, choose the exact affected range, including chart/drawing regions; worksheet numbers are not PDF page numbers. Inspect the final saved file, reuse existing images for an unchanged source, and rerender only after requested source changes or for previously unrendered regions. Change print areas or scaling only when the user requests print layout.
|
||||
|
||||
@@ -43,13 +43,15 @@ Do not rename `.xls`, `.xlsb`, encrypted files, or macro-enabled files to `.xlsx
|
||||
|
||||
Run the shared structural checker when `office-check` is available:
|
||||
|
||||
In PowerShell, prefix the quoted Python executable with `&`. Resolve the checker path relative to this skill directory.
|
||||
|
||||
```text
|
||||
office-check <workbook.xlsx> --out <checks.json>
|
||||
"<python>" "<skill-directory>/../scripts/check_office.py" <workbook.xlsx> --out <checks.json>
|
||||
```
|
||||
|
||||
It checks ZIP/XML integrity and internal relationships, and reports sheet names, populated-cell counts, and formula counts. Repeated `--contains TEXT` arguments check string cells and sheet names, and `--count N` checks sheet count. `--contains` excludes numeric cells and does not validate formula results; verify those separately with openpyxl. The checker does not calculate formulas or judge workbook appearance. Compare relevant values, types, formulas, styles, and totals with the task's source data.
|
||||
|
||||
If `office-check` is unavailable, use an already configured environment to check the ZIP/XML package and internal relationships. Report the missing environment if those checks cannot run; do not claim structural validation.
|
||||
If the Python checker cannot run, use an already configured environment to check the ZIP/XML package and internal relationships. Report the missing environment if those checks cannot run; do not claim structural validation.
|
||||
|
||||
Load `office-render` for requested images or visual inspection and whenever the user's primary task targets appearance, including cell fill colors, text alignment, spacing, or sizing. These appearance changes require rendering even without a separate visual-check request, unless the user explicitly skips it. New workbooks and data or formula edits do not require rendering solely because they include incidental basic formatting. In particular, bold headers and number formats accompanying data edits do not require loading `office-render` or running its commands.
|
||||
|
||||
|
||||
@@ -18,34 +18,23 @@
|
||||
"types": "./lib/types/index.d.ts",
|
||||
"default": "./lib/index.js"
|
||||
},
|
||||
"./package.json": "./package.json",
|
||||
"./commands": {
|
||||
"types": "./lib/types/commands.d.ts",
|
||||
"default": "./lib/commands.js"
|
||||
}
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"files": [
|
||||
"lib/index.js",
|
||||
"lib/commands.js",
|
||||
"assets",
|
||||
"lib/types/**/*.d.ts"
|
||||
],
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-skill": "workspace:*",
|
||||
"@deepseek-ai/cordis": "workspace:~",
|
||||
"@deepseek-ai/dsh-shell-env": "workspace:*",
|
||||
"@deepseek-ai/dsh-home-paths": "workspace:*"
|
||||
"@deepseek-ai/cordis": "workspace:~"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-pwsh-local": "workspace:*",
|
||||
"@deepseek-ai/dsh-subprocess-local": "workspace:*",
|
||||
"@deepseek-ai/dsh-skill": "workspace:*",
|
||||
"@deepseek-ai/cordis": "workspace:~",
|
||||
"@deepseek-ai/cordis-plugin-loader": "workspace:~",
|
||||
"@deepseek-ai/cordis-plugin-include": "workspace:~",
|
||||
"@deepseek-ai/dsh-shell-env": "workspace:*",
|
||||
"@deepseek-ai/dsh-home-paths": "workspace:*"
|
||||
"@deepseek-ai/cordis-plugin-include": "workspace:~"
|
||||
},
|
||||
"dependencies": {
|
||||
"@deepseek-ai/libreoffice-kit": "^0.1.5",
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
/** Local Office command launchers contributed to model shell calls. */
|
||||
import { chmodSync, existsSync, mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { basename, dirname, isAbsolute, join, toNamespacedPath } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import z from '@deepseek-ai/schemastery'
|
||||
import { resolveDshHome } from '@deepseek-ai/dsh-home-paths'
|
||||
import type {} from '@deepseek-ai/dsh-shell-env'
|
||||
|
||||
/** Local interpreter and resource selection; commands execute in the same host as these resources. */
|
||||
export interface Config {
|
||||
/** dsoffice argv; defaults to the carrier or installed Kit. False disables dsoffice. */
|
||||
command?: string[] | false
|
||||
/** Python checker command argv; defaults to the supplied runtime; absent without a runtime. */
|
||||
checkCommand?: string[] | false
|
||||
/** Directory containing the shared scripts/ and Office skills. */
|
||||
assetRoot?: string
|
||||
}
|
||||
|
||||
/** Validate local command configuration. */
|
||||
export const Config: z<Config> = z.object({
|
||||
command: z.union([z.array(z.string()).min(1), z.const(false)]),
|
||||
checkCommand: z.union([z.array(z.string()).min(1), z.const(false)]),
|
||||
assetRoot: z.string().min(1),
|
||||
})
|
||||
|
||||
/** Cordis plugin identity. */
|
||||
export const name = 'office-commands'
|
||||
/** Registry that publishes the launcher directory for each shell execution. */
|
||||
export const inject = ['shellEnv']
|
||||
|
||||
function quote(value: string): string { return `'${value.replaceAll("'", "'\\''")}'` }
|
||||
|
||||
function writeLauncher(directory: string, name: string, command: readonly string[]): void {
|
||||
const executable = command[0]
|
||||
if (executable === undefined || !isAbsolute(executable)) throw new Error(`office-commands: ${name} requires an absolute executable`)
|
||||
if (command.some(argument => /[\0\r\n]/u.test(argument))) throw new Error('office-commands: command arguments must not contain NUL or newlines')
|
||||
const script = join(directory, name)
|
||||
writeFileSync(script, `#!/bin/sh\nexec ${command.map(quote).join(' ')} "$@"\n`, { mode: 0o700, flag: 'wx' })
|
||||
chmodSync(script, 0o700)
|
||||
if (process.platform === 'win32') {
|
||||
const literal = (value: string): string => `'${value.replaceAll("'", "''")}'`
|
||||
const args = command.slice(1).map(literal).join(', ')
|
||||
// Windows PowerShell counts escaped quotes when deciding whether to wrap an argument; doubled quotes keep each value enclosed.
|
||||
const nativeArguments = String.raw`$quoteReplacement = if ($PSVersionTable.PSVersion.Major -le 5) { '$1$1""' } else { '$1$1\"' }
|
||||
$nativeArgs = @(foreach ($argument in $commandArgs) {
|
||||
$value = if ($argument -is [array]) { $argument -join ',' } else { [string]$argument }
|
||||
'"' + ($value -replace '(\\*)"', $quoteReplacement -replace '(\\+)$', '$1$1') + '"'
|
||||
})`
|
||||
writeFileSync(`${script}.ps1`, '\ufeff$PSNativeCommandArgumentPassing = \'Legacy\'\n'
|
||||
+ '$OutputEncoding = [System.Text.UTF8Encoding]::new($false)\n'
|
||||
+ `$commandArgs = @(${args}) + $args\n${nativeArguments}\n$global:LASTEXITCODE = 127\n`
|
||||
+ `& ${literal(executable)} @nativeArgs\n`
|
||||
+ 'exit $LASTEXITCODE\n', { flag: 'wx' })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish stable Office commands without preparing interpreters or engines at activation.
|
||||
* @param ctx - Local shell environment.
|
||||
* @param config - Command overrides and interpreter resources.
|
||||
*/
|
||||
export function apply(ctx: Context, config: Config = {}): void {
|
||||
const bundled = process.env.DSH_PRIMARY_RUNTIME ?? process.env.DSH_BUNDLED_PRIMARY_RUNTIME
|
||||
const carrierRuntime = process.env.DSH_BUNDLED_PRIMARY_RUNTIME
|
||||
const node = process.env.DSH_BUNDLED_OFFICE_NODE ?? (carrierRuntime ? join(carrierRuntime, 'dependencies/node/bin', process.platform === 'win32' ? 'node.exe' : 'node') : process.execPath)
|
||||
const assetRoot = config.assetRoot ?? process.env.DSH_BUNDLED_OFFICE_SKILLS ?? (bundled ? join(dirname(bundled), 'office-skills') : fileURLToPath(new URL('../assets/', import.meta.url)))
|
||||
if (!isAbsolute(assetRoot)) throw new Error('office-commands: assetRoot must be an absolute directory')
|
||||
const kit = config.command ?? (process.env.DSH_BUNDLED_OFFICE_CLI === '' ? false : [node, process.env.DSH_BUNDLED_OFFICE_CLI
|
||||
?? join(dirname(fileURLToPath(import.meta.resolve('@deepseek-ai/libreoffice-kit/package.json'))), 'lib/cli.js')])
|
||||
const checker = join(assetRoot, 'scripts/check_office.py')
|
||||
const check = config.checkCommand ?? (bundled
|
||||
? [join(bundled, 'dependencies/python', process.platform === 'win32' ? 'python.exe' : 'bin/python3'), checker]
|
||||
: false)
|
||||
const parent = join(resolveDshHome(), 'commands')
|
||||
mkdirSync(parent, { recursive: true, mode: 0o700 })
|
||||
// Windows mkdtemp needs extended paths; shell PATH keeps ordinary absolute directories.
|
||||
const directory = join(parent, basename(mkdtempSync(toNamespacedPath(join(parent, 'office-')))))
|
||||
const shortcutDirectories: string[] = []
|
||||
const shortCommand = (command: readonly string[]): readonly string[] => {
|
||||
const executable = command[0]
|
||||
if (process.platform !== 'win32' || executable === undefined || executable.length < 260
|
||||
|| !isAbsolute(executable) || !existsSync(toNamespacedPath(dirname(executable)))) return command
|
||||
const shortcutDirectory = mkdtempSync(join(tmpdir(), 'dsh-office-exe-'))
|
||||
shortcutDirectories.push(shortcutDirectory)
|
||||
const junction = join(shortcutDirectory, 'bin')
|
||||
symlinkSync(toNamespacedPath(dirname(executable)), junction, 'junction')
|
||||
return [join(junction, basename(executable)), ...command.slice(1)]
|
||||
}
|
||||
const removeDirectories = () => {
|
||||
rmSync(directory, { recursive: true, force: true })
|
||||
for (const shortcutDirectory of shortcutDirectories) rmSync(shortcutDirectory, { recursive: true, force: true })
|
||||
}
|
||||
try {
|
||||
if (kit !== false) writeLauncher(directory, 'dsoffice', shortCommand(kit))
|
||||
if (check !== false) writeLauncher(directory, 'office-check', shortCommand(check))
|
||||
ctx.effect(() => () => { removeDirectories() })
|
||||
ctx.shellEnv.register({ name, commandDirectories: [directory], variables: {}, resolve: () => ({}) })
|
||||
} catch (error) {
|
||||
removeDirectories()
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
import { readFileSync, statSync } from 'node:fs'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { dirname, isAbsolute, join, sep } from 'node:path'
|
||||
import { isSea } from 'node:sea'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import z from '@deepseek-ai/schemastery'
|
||||
@@ -13,13 +14,19 @@ const SKILL_NAMES = ['office', 'office-docx', 'office-pptx', 'office-xlsx', 'off
|
||||
|
||||
/** Office skill resource location. */
|
||||
export interface Config {
|
||||
/** Absolute assets directory containing skill folders and shared scripts; defaults to carrier resources or packaged assets. */
|
||||
/** Absolute assets directory containing skill folders and shared scripts; defaults to packaged assets. */
|
||||
assetRoot?: string
|
||||
/** Standalone Node executable; defaults to the current executable outside Electron and SEA. */
|
||||
node?: string
|
||||
/** Absolute LibreOffice Kit CLI entry; false explicitly disables CLI access. */
|
||||
cli?: string | false
|
||||
}
|
||||
|
||||
/** Validated resource configuration. */
|
||||
export const Config: z<Config> = z.object({
|
||||
assetRoot: z.string().min(1),
|
||||
node: z.string().min(1),
|
||||
cli: z.union([z.string().min(1), z.const(false)]),
|
||||
})
|
||||
|
||||
/** Cordis plugin identity. */
|
||||
@@ -37,18 +44,30 @@ function parseSkill(raw: string, path: string): { description: string; content:
|
||||
return { description, content: raw.slice(frontmatter[0].length).trim() }
|
||||
}
|
||||
|
||||
function resolveAssetRoot(config: Config): string {
|
||||
const bundled = process.env.DSH_PRIMARY_RUNTIME ?? process.env.DSH_BUNDLED_PRIMARY_RUNTIME
|
||||
return config.assetRoot ?? process.env.DSH_BUNDLED_OFFICE_SKILLS ?? (bundled ? join(dirname(bundled), 'office-skills') : fileURLToPath(new URL('../assets/', import.meta.url)))
|
||||
function officeRuntime(config: Config): string {
|
||||
if (config.cli === false) return '\n\nLibreOffice Kit is disabled in this deployment.'
|
||||
if (config.node === undefined && (isSea() || process.versions.electron !== undefined)) {
|
||||
throw new Error('skill-office: packaged applications must supply a standalone Node executable')
|
||||
}
|
||||
const node = config.node ?? process.execPath
|
||||
let cli = config.cli
|
||||
if (cli === undefined) {
|
||||
const manifestPath = fileURLToPath(import.meta.resolve('@deepseek-ai/libreoffice-kit/package.json'))
|
||||
cli = join(dirname(manifestPath), 'lib', 'cli.js')
|
||||
}
|
||||
for (const path of [node, cli]) {
|
||||
if (!isAbsolute(path) || !statSync(path).isFile()) throw new Error(`skill-office: expected an absolute executable file: ${path}`)
|
||||
}
|
||||
return `\n\n## Installed LibreOffice Kit\n\nUse these absolute paths for every LibreOffice Kit command. Pass the CLI entry as the first argument to Node.\n\n${JSON.stringify({ libreofficeKit: { node, cli } }, undefined, 2)}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Register Office skills with resources readable by the script interpreter.
|
||||
* @param ctx - Context carrying the skill registry.
|
||||
* @param input - Optional packaged resource override.
|
||||
* @param config - Optional external assets directory for packaged applications.
|
||||
*/
|
||||
export function apply(ctx: Context, input: Config = {}): void {
|
||||
const assetRoot = resolveAssetRoot(input)
|
||||
export function apply(ctx: Context, config: Config = {}): void {
|
||||
const assetRoot = config.assetRoot ?? fileURLToPath(new URL('../assets/', import.meta.url))
|
||||
if (!isAbsolute(assetRoot)) throw new Error('skill-office: assetRoot must be an absolute directory')
|
||||
if (!statSync(join(assetRoot, 'scripts', 'check_office.py')).isFile()) {
|
||||
throw new Error('skill-office: assets must contain scripts/check_office.py')
|
||||
@@ -71,6 +90,7 @@ export function apply(ctx: Context, input: Config = {}): void {
|
||||
const { rank: _rank, locator, ...summary } = candidate
|
||||
const raw = await readFile(locator as string, { encoding: 'utf8', signal: options.signal })
|
||||
const content = parseSkill(raw, locator as string).content
|
||||
+ (['office-render', 'office-recalculate', 'office-export-pdf'].includes(candidate.name) ? officeRuntime(config) : '')
|
||||
return { ...summary, content }
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
/** Published direct checker launcher smoke; CI supplies built package exports before this keyless lane. */
|
||||
import { existsSync } from 'node:fs'
|
||||
import { copyFile, mkdir, mkdtemp, rm, symlink, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { execa } from 'execa'
|
||||
import { expect, it } from 'vitest'
|
||||
|
||||
const packageRoot = fileURLToPath(new URL('..', import.meta.url))
|
||||
const built = existsSync(join(packageRoot, 'lib/commands.js'))
|
||||
|
||||
it.skipIf(!built)('runs the emitted direct checker launcher and preserves its arguments and exit status', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'dsh-built-office-'))
|
||||
try {
|
||||
const runtime = join(root, 'runtime'), assets = join(root, 'assets')
|
||||
const python = join(runtime, 'dependencies/python', process.platform === 'win32' ? 'python.exe' : 'bin/python3')
|
||||
await mkdir(dirname(python), { recursive: true })
|
||||
// The fixture executable speaks the checker's argv/exit protocol; the carrier smoke owns Python libraries.
|
||||
if (process.platform === 'win32') await copyFile(process.execPath, python)
|
||||
else await symlink(process.execPath, python)
|
||||
await mkdir(join(assets, 'scripts'), { recursive: true })
|
||||
await writeFile(join(assets, 'scripts/check_office.py'), "require('node:fs').writeFileSync(process.argv[2], JSON.stringify(process.argv.slice(3))); process.exitCode = 7;\n")
|
||||
const script = `
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { delimiter, join } from 'node:path';
|
||||
import { Context } from '@deepseek-ai/cordis';
|
||||
import * as ShellEnv from '@deepseek-ai/dsh-shell-env';
|
||||
import * as Commands from '@deepseek-ai/dsh-skill-office/commands';
|
||||
import { execa } from 'execa';
|
||||
const root = process.argv[1], ctx = new Context();
|
||||
try {
|
||||
await ctx.plugin(ShellEnv);
|
||||
await ctx.plugin(Commands, { command: false, assetRoot: join(root, 'assets') });
|
||||
const env = ctx.shellEnv.collect({ signal: new AbortController().signal, token: Symbol(), callId: 'smoke', rootCallId: 'smoke', name: 'bash', arguments: {} });
|
||||
const directory = env.PATH.split(delimiter)[0];
|
||||
const launcher = join(directory, process.platform === 'win32' ? 'office-check.ps1' : 'office-check');
|
||||
const source = await readFile(launcher, 'utf8');
|
||||
assert(source.includes('check_office.py'));
|
||||
assert(!source.includes('check.js'));
|
||||
assert(!source.includes('tsx'));
|
||||
const args = [join(root, 'arguments.json'), 'space and 中文', "apostrophe'value"];
|
||||
if (process.platform === 'win32') {
|
||||
const { LocalSubprocessRuntime } = await import('@deepseek-ai/dsh-subprocess-local');
|
||||
const { PwshLocalExecutor } = await import('@deepseek-ai/dsh-pwsh-local');
|
||||
await ctx.plugin(LocalSubprocessRuntime);
|
||||
await ctx.plugin(PwshLocalExecutor, { pwshPath: 'powershell.exe' });
|
||||
const command = 'office-check ' + args.map(value => "'" + value.replaceAll("'", "''") + "'").join(' ') + '; exit $LASTEXITCODE';
|
||||
const result = await (await ctx.shell.execute(ctx.shell.resolve({ command,
|
||||
env: { PSExecutionPolicyPreference: 'Restricted' }, dshEnv: env }))).result();
|
||||
assert.equal(result.exitCode, 7, result.stderr.text);
|
||||
} else {
|
||||
const result = await execa(launcher, args, { env, reject: false });
|
||||
assert.equal(result.exitCode, 7, result.stderr);
|
||||
}
|
||||
assert.deepEqual(JSON.parse(await readFile(args[0], 'utf8')), args.slice(1));
|
||||
process.stdout.write('built checker passed');
|
||||
} finally { await ctx.fiber.dispose(); }
|
||||
`
|
||||
const result = await execa(process.execPath, ['--input-type=module', '-e', script, root], {
|
||||
cwd: packageRoot,
|
||||
env: {
|
||||
DSH_HOME: join(root, 'home'), DSH_PRIMARY_RUNTIME: runtime, DSH_BUNDLED_PRIMARY_RUNTIME: '',
|
||||
NODE_OPTIONS: '', TSX_TSCONFIG_PATH: '',
|
||||
},
|
||||
timeout: 30_000, killSignal: 'SIGKILL', reject: false,
|
||||
})
|
||||
expect(result.timedOut, result.stderr).toBe(false)
|
||||
expect(result.signal, result.stderr).toBeUndefined()
|
||||
expect(result.exitCode, result.stderr).toBe(0)
|
||||
expect(result.stdout).toBe('built checker passed')
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
@@ -1,265 +0,0 @@
|
||||
/** Shell command discovery, argument preservation, and launcher ownership. */
|
||||
import { copyFile, mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { delimiter, dirname, join, toNamespacedPath } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { ToolCallId } from '@deepseek-ai/dsh-llm'
|
||||
import * as ShellEnv from '@deepseek-ai/dsh-shell-env'
|
||||
import type { ToolExecution } from '@deepseek-ai/dsh-tools'
|
||||
import { execa } from 'execa'
|
||||
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
|
||||
import * as Commands from '../src/commands.ts'
|
||||
|
||||
const contexts: Context[] = []
|
||||
let root: string
|
||||
const execution: ToolExecution = {
|
||||
signal: new AbortController().signal,
|
||||
token: Symbol('office-command') as ToolExecution['token'],
|
||||
callId: ToolCallId('office-command'), rootCallId: ToolCallId('office-command'),
|
||||
name: 'bash', arguments: { command: 'dsoffice capabilities' },
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
root = await mkdtemp(join(tmpdir(), 'dsh-office-commands-'))
|
||||
vi.stubEnv('DSH_HOME', root)
|
||||
for (const key of ['DSH_PRIMARY_RUNTIME', 'DSH_BUNDLED_PRIMARY_RUNTIME', 'DSH_BUNDLED_OFFICE_CLI', 'DSH_BUNDLED_OFFICE_NODE', 'DSH_BUNDLED_OFFICE_SKILLS']) vi.stubEnv(key, undefined)
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
for (const ctx of contexts.splice(0)) await ctx.fiber.dispose()
|
||||
vi.unstubAllEnvs()
|
||||
await rm(root, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
async function context() {
|
||||
const ctx = new Context()
|
||||
contexts.push(ctx)
|
||||
await ctx.plugin(ShellEnv)
|
||||
return ctx
|
||||
}
|
||||
|
||||
function directory(ctx: Context): string {
|
||||
const path = ctx.shellEnv.collect(execution).PATH
|
||||
if (path === undefined) throw new Error('Missing Office command directory')
|
||||
return path.split(delimiter)[0]!
|
||||
}
|
||||
|
||||
it.skipIf(process.platform === 'win32')('executes the installed capabilities command without Node on PATH', async () => {
|
||||
vi.stubEnv('PATH', '')
|
||||
const ctx = await context()
|
||||
await ctx.plugin(Commands)
|
||||
const env = ctx.shellEnv.collect(execution)
|
||||
const result = await execa('dsoffice', ['capabilities'], { cwd: root, env, extendEnv: false })
|
||||
expect(JSON.parse(result.stdout)).toMatchObject({ runtime: {
|
||||
cliPath: fileURLToPath(import.meta.resolve('@deepseek-ai/libreoffice-kit/cli')),
|
||||
} })
|
||||
expect(process.env.PATH).toBe('')
|
||||
expect(await readdir(root)).toEqual(['commands'])
|
||||
await expect(readFile(join(directory(ctx), 'office-check'))).rejects.toMatchObject({ code: 'ENOENT' })
|
||||
})
|
||||
|
||||
it.skipIf(process.platform === 'win32')('preserves configured and forwarded shell arguments literally', async () => {
|
||||
const ctx = await context()
|
||||
const fixed = ['', "apostrophe' and space", '$(printf substituted)', '`printf substituted`', '%PATH%', 'double"quote']
|
||||
const command = [process.execPath, '-e', 'process.stdout.write(JSON.stringify(process.argv.slice(1)))', ...fixed]
|
||||
await ctx.plugin(Commands, { command, checkCommand: command })
|
||||
for (const name of ['dsoffice', 'office-check']) {
|
||||
const result = await execa(join(directory(ctx), name), ['forwarded value', ''], { env: { PATH: '' }, extendEnv: false })
|
||||
expect(JSON.parse(result.stdout)).toEqual([...fixed, 'forwarded value', ''])
|
||||
}
|
||||
})
|
||||
|
||||
it('owns separate launcher directories under a long home until each contributing context is disposed', async () => {
|
||||
const home = join(root, 'a'.repeat(120), 'b'.repeat(120))
|
||||
vi.stubEnv('DSH_HOME', home)
|
||||
expect(home.length).toBeGreaterThan(260)
|
||||
const first = await context(), second = await context()
|
||||
const [one, two] = await Promise.all([
|
||||
first.plugin(Commands, { command: false, checkCommand: false }),
|
||||
second.plugin(Commands, { command: false, checkCommand: false }),
|
||||
])
|
||||
const firstDirectory = directory(first), secondDirectory = directory(second)
|
||||
expect(firstDirectory).not.toBe(secondDirectory)
|
||||
expect(dirname(firstDirectory)).toBe(join(home, 'commands'))
|
||||
expect(dirname(secondDirectory)).toBe(join(home, 'commands'))
|
||||
await one.dispose()
|
||||
await expect(readdir(firstDirectory)).rejects.toMatchObject({ code: 'ENOENT' })
|
||||
expect(first.shellEnv.collect(execution)).not.toHaveProperty('PATH')
|
||||
expect(await readdir(secondDirectory)).toEqual([])
|
||||
await two.dispose()
|
||||
await expect(readdir(secondDirectory)).rejects.toMatchObject({ code: 'ENOENT' })
|
||||
})
|
||||
|
||||
it('defers missing executable failures to invocation and permits each command to be disabled', async () => {
|
||||
const ctx = await context()
|
||||
await ctx.plugin(Commands, { command: [join(root, 'missing-kit')], checkCommand: false })
|
||||
const files = await readdir(directory(ctx))
|
||||
expect(files.sort()).toEqual(process.platform === 'win32' ? ['dsoffice', 'dsoffice.ps1'] : ['dsoffice'])
|
||||
const disabled = await context()
|
||||
await disabled.plugin(Commands, { command: false, checkCommand: [join(root, 'missing-python')] })
|
||||
expect(await readdir(directory(disabled))).toContain('office-check')
|
||||
})
|
||||
|
||||
it.each([[], ['relative-command'], [process.execPath, 'line\nbreak'], [process.execPath, 'nul\0byte']].map(command => ({ command })))(
|
||||
'rejects an invalid command and removes its partial directory: $command', async ({ command }) => {
|
||||
const ctx = await context()
|
||||
expect(() => { Commands.apply(ctx, { command: [process.execPath], checkCommand: command }) })
|
||||
.toThrow(/absolute executable|NUL or newlines/)
|
||||
expect(await readdir(join(root, 'commands'))).toEqual([])
|
||||
expect(ctx.shellEnv.collect(execution)).not.toHaveProperty('PATH')
|
||||
},
|
||||
)
|
||||
|
||||
it('rejects relative asset roots before publishing commands', async () => {
|
||||
const ctx = await context()
|
||||
expect(() => { Commands.apply(ctx, { assetRoot: 'relative-assets' }) }).toThrow('assetRoot must be an absolute directory')
|
||||
expect(ctx.shellEnv.collect(execution)).not.toHaveProperty('PATH')
|
||||
expect(await readdir(root)).toEqual([])
|
||||
})
|
||||
|
||||
it('runs Kit with the host Node when the selected authoring environment contains only Python', async () => {
|
||||
vi.stubEnv('DSH_PRIMARY_RUNTIME', join(root, 'python-only'))
|
||||
const ctx = await context()
|
||||
await ctx.plugin(Commands)
|
||||
const result = await execa(process.execPath, [fileURLToPath(import.meta.resolve('@deepseek-ai/libreoffice-kit/cli')), 'capabilities'], {
|
||||
cwd: root, env: { ...ctx.shellEnv.collect(execution), PATH: directory(ctx) }, extendEnv: false,
|
||||
})
|
||||
expect(JSON.parse(result.stdout)).toHaveProperty('runtime.cliPath')
|
||||
expect(await readFile(join(directory(ctx), 'dsoffice'), 'utf8')).toContain(process.execPath)
|
||||
expect(await readFile(join(directory(ctx), 'office-check'), 'utf8')).toContain(join(root, 'python-only/dependencies/python'))
|
||||
})
|
||||
|
||||
it.each(['win32', 'linux'])('uses bundled interpreters and explicit resources without preparing the payload on %s', async (platformName) => {
|
||||
const platform = Object.getOwnPropertyDescriptor(process, 'platform')!
|
||||
Object.defineProperty(process, 'platform', { value: platformName })
|
||||
try {
|
||||
const bundled = join(root, 'carrier', 'primary-runtime')
|
||||
const cli = join(root, 'carrier', 'kit-cli.js'), assets = join(root, 'custom-assets')
|
||||
vi.stubEnv('DSH_BUNDLED_PRIMARY_RUNTIME', bundled)
|
||||
vi.stubEnv('DSH_BUNDLED_OFFICE_CLI', cli)
|
||||
const ctx = await context()
|
||||
await ctx.plugin(Commands, { assetRoot: assets })
|
||||
const kit = await readFile(join(directory(ctx), 'dsoffice'), 'utf8')
|
||||
expect(kit).toContain(cli)
|
||||
expect(kit).toContain(join(bundled, 'dependencies/node/bin', platformName === 'win32' ? 'node.exe' : 'node'))
|
||||
const checker = await readFile(join(directory(ctx), 'office-check'), 'utf8')
|
||||
expect(checker).toContain(join(bundled, 'dependencies/python', platformName === 'win32' ? 'python.exe' : 'bin/python3'))
|
||||
expect(checker).toContain(join(assets, 'scripts/check_office.py'))
|
||||
await expect(readdir(join(root, 'carrier'))).rejects.toMatchObject({ code: 'ENOENT' })
|
||||
} finally {
|
||||
Object.defineProperty(process, 'platform', platform)
|
||||
}
|
||||
})
|
||||
|
||||
it('writes PowerShell launchers with literal arguments and the selected checker environment', async () => {
|
||||
const ctx = await context()
|
||||
const platform = Object.getOwnPropertyDescriptor(process, 'platform')!
|
||||
Object.defineProperty(process, 'platform', { value: 'win32' })
|
||||
try {
|
||||
const bundled = join(root, 'primary-runtime')
|
||||
vi.stubEnv('DSH_BUNDLED_PRIMARY_RUNTIME', bundled)
|
||||
Commands.apply(ctx, { command: [process.execPath, 'percent%value', 'double"quote', "apostrophe'value"] })
|
||||
const launcher = await readFile(join(directory(ctx), 'dsoffice.ps1'))
|
||||
expect([...launcher.subarray(0, 3)]).toEqual([0xef, 0xbb, 0xbf])
|
||||
expect(launcher.toString('utf8')).toContain("$commandArgs = @('percent%value', 'double\"quote', 'apostrophe''value') + $args")
|
||||
expect(launcher.toString('utf8')).toContain("$PSNativeCommandArgumentPassing = 'Legacy'")
|
||||
expect(await readFile(join(directory(ctx), 'office-check.ps1'), 'utf8')).toContain('python.exe')
|
||||
} finally {
|
||||
Object.defineProperty(process, 'platform', platform)
|
||||
}
|
||||
})
|
||||
|
||||
it('removes short executable paths without removing the original interpreter', async () => {
|
||||
const platform = Object.getOwnPropertyDescriptor(process, 'platform')!
|
||||
Object.defineProperty(process, 'platform', { value: 'win32' })
|
||||
try {
|
||||
const runtime = join(root, 'a'.repeat(120), 'b'.repeat(120))
|
||||
await mkdir(toNamespacedPath(runtime), { recursive: true })
|
||||
const node = join(runtime, 'node.exe')
|
||||
expect(node.length).toBeGreaterThan(260)
|
||||
await writeFile(toNamespacedPath(node), 'fixture')
|
||||
const ctx = await context()
|
||||
const commands = await ctx.plugin(Commands, { command: [node], checkCommand: false })
|
||||
const launcher = await readFile(join(directory(ctx), 'dsoffice.ps1'), 'utf8')
|
||||
const shortNode = /& '([^']+)' @nativeArgs/u.exec(launcher)?.[1]
|
||||
if (shortNode === undefined) throw new Error('Missing executable path in the PowerShell launcher')
|
||||
expect(shortNode).not.toBe(node)
|
||||
expect(await readFile(shortNode, 'utf8')).toBe('fixture')
|
||||
await commands.dispose()
|
||||
await expect(readFile(shortNode)).rejects.toMatchObject({ code: 'ENOENT' })
|
||||
expect(await readFile(toNamespacedPath(node), 'utf8')).toBe('fixture')
|
||||
} finally {
|
||||
Object.defineProperty(process, 'platform', platform)
|
||||
}
|
||||
})
|
||||
|
||||
it.skipIf(process.platform !== 'win32').each([undefined, 'powershell.exe'])(
|
||||
'preserves native arguments through the Windows shell executor: %s', async (pwshPath) => {
|
||||
const { LocalSubprocessRuntime } = await import('@deepseek-ai/dsh-subprocess-local')
|
||||
const { PwshLocalExecutor } = await import('@deepseek-ai/dsh-pwsh-local')
|
||||
const runtime = join(root, 'runtime 中文', 'a'.repeat(220))
|
||||
await mkdir(toNamespacedPath(runtime), { recursive: true })
|
||||
const node = join(runtime, 'node.exe'), fixture = join(runtime, 'arguments.mjs')
|
||||
expect(node.length).toBeGreaterThan(260)
|
||||
await copyFile(process.execPath, toNamespacedPath(node))
|
||||
await writeFile(toNamespacedPath(fixture), "process.stdout.write(JSON.stringify({ args: process.argv.slice(2) })); process.stderr.write('fixture stderr'); process.exitCode = 7;\n")
|
||||
const argumentsToPreserve = ['', 'space and 中文', "apostrophe'value", 'double"quote', 'quote" with space',
|
||||
'slash\\" quote', 'trailing\\', 'space trailing\\', '%PATH%', '!literal!', '&<|>']
|
||||
const quoted = argumentsToPreserve.map(value => `'${value.replaceAll("'", "''")}'`).join(' ')
|
||||
if (pwshPath === undefined) vi.stubEnv('DSH_HOME', join(root, 'a'.repeat(120), 'b'.repeat(120)))
|
||||
const ctx = await context()
|
||||
await ctx.plugin(LocalSubprocessRuntime)
|
||||
await ctx.plugin(PwshLocalExecutor, pwshPath === undefined ? {} : { pwshPath })
|
||||
const commands = await ctx.plugin(Commands, { command: [node, fixture, ...argumentsToPreserve], checkCommand: false })
|
||||
const commandDirectory = directory(ctx)
|
||||
if (pwshPath === undefined) expect(commandDirectory.length).toBeGreaterThan(260)
|
||||
const result = await (await ctx.shell.execute(ctx.shell.resolve({
|
||||
command: `dsoffice ${quoted}; exit $LASTEXITCODE`,
|
||||
env: { PSExecutionPolicyPreference: 'Restricted' }, dshEnv: ctx.shellEnv.collect(execution),
|
||||
}))).result()
|
||||
expect(result.exitCode, result.stderr.text).toBe(7)
|
||||
expect(result.stderr.text).toContain('fixture stderr')
|
||||
const output = JSON.parse(result.stdout.text) as { args: string[] }
|
||||
expect(output.args).toEqual([...argumentsToPreserve, ...argumentsToPreserve])
|
||||
const pages = await (await ctx.shell.execute(ctx.shell.resolve({
|
||||
command: 'dsoffice --pages 1,3,4; exit $LASTEXITCODE',
|
||||
dshEnv: ctx.shellEnv.collect(execution),
|
||||
}))).result()
|
||||
expect(pages.exitCode, pages.stderr.text).toBe(7)
|
||||
expect((JSON.parse(pages.stdout.text) as { args: string[] }).args).toEqual([...argumentsToPreserve, '--pages', '1,3,4'])
|
||||
await commands.dispose()
|
||||
await expect(readdir(commandDirectory)).rejects.toMatchObject({ code: 'ENOENT' })
|
||||
expect(ctx.shellEnv.collect(execution)).not.toHaveProperty('PATH')
|
||||
},
|
||||
)
|
||||
|
||||
it.skipIf(process.platform !== 'win32')('reports a missing Windows executable as a failed shell command', async () => {
|
||||
const { LocalSubprocessRuntime } = await import('@deepseek-ai/dsh-subprocess-local')
|
||||
const { PwshLocalExecutor } = await import('@deepseek-ai/dsh-pwsh-local')
|
||||
const ctx = await context()
|
||||
await ctx.plugin(LocalSubprocessRuntime)
|
||||
await ctx.plugin(PwshLocalExecutor, { pwshPath: 'powershell.exe' })
|
||||
await ctx.plugin(Commands, { command: [join(root, 'missing.exe')], checkCommand: false })
|
||||
const result = await (await ctx.shell.execute(ctx.shell.resolve({
|
||||
command: 'dsoffice --pages 1,3,4; exit $LASTEXITCODE',
|
||||
dshEnv: ctx.shellEnv.collect(execution),
|
||||
}))).result()
|
||||
expect(result.exitCode).toBe(127)
|
||||
expect(result.stderr.text).toContain('missing.exe')
|
||||
})
|
||||
|
||||
it('uses the downloaded sidecar Node and disables Kit for an authoring-only carrier', async () => {
|
||||
const node = join(root, 'sidecar/node'), cli = join(root, 'sidecar/cli.js')
|
||||
vi.stubEnv('DSH_BUNDLED_OFFICE_NODE', node)
|
||||
vi.stubEnv('DSH_BUNDLED_OFFICE_CLI', cli)
|
||||
const ctx = await context()
|
||||
await ctx.plugin(Commands)
|
||||
const launcher = await readFile(join(directory(ctx), 'dsoffice'), 'utf8')
|
||||
expect(launcher).toContain(node)
|
||||
expect(launcher).toContain(cli)
|
||||
vi.stubEnv('DSH_BUNDLED_OFFICE_CLI', '')
|
||||
const disabled = await context()
|
||||
await disabled.plugin(Commands)
|
||||
expect(await readdir(directory(disabled))).toEqual([])
|
||||
})
|
||||
@@ -94,7 +94,7 @@ it.each(['office-render', 'office-recalculate', 'office-export-pdf'])('loads %s
|
||||
const { agent, adapter } = await setup('native', [name])
|
||||
await send(agent)
|
||||
const loaded = JSON.stringify(adapter.requests.at(-1))
|
||||
expect(loaded).toContain('dsoffice')
|
||||
expect(loaded).toContain('libreofficeKit')
|
||||
expect(loaded).not.toContain('Office task routing')
|
||||
expect(loaded).not.toContain('# Word documents')
|
||||
if (name !== 'office-render') expect(loaded).not.toMatch(/render --input|uniformly blank|fragment rectangles|DPI/)
|
||||
|
||||
@@ -23,7 +23,6 @@ async function prepareOffice(cwd: string) {
|
||||
await writeFile(join(cwd, 'office.patch.json'), JSON.stringify([
|
||||
...officeModelPatches(),
|
||||
{ insert: [{ id: 'skill-office', name: '@deepseek-ai/dsh-skill-office', config: { assetRoot: join(repo, 'packages/skill/skill-office/assets') } }] },
|
||||
{ insert: [{ id: 'office-commands', name: '@deepseek-ai/dsh-skill-office/commands', config: { assetRoot: join(repo, 'packages/skill/skill-office/assets') } }] },
|
||||
{ insert: [{ id: 'workspace-dependencies', name: '@deepseek-ai/dsh-tool-workspace-dependencies', config: { source: runtime! } }] },
|
||||
{ id: 'session-persistence-jsonl', config: { root: join(cwd, '.sessions'), compression: 'none' } },
|
||||
]))
|
||||
@@ -58,7 +57,7 @@ it.skipIf(!process.env.DEEPSEEK_API_KEY || !runtime).each([{ format: 'docx', mod
|
||||
const skills = calls.filter(call => call.name === 'skill').map(call => JSON.parse(call.arguments) as { name: string })
|
||||
expect(skills.some(skill => skill.name === 'office-render'), JSON.stringify(skills)).toBe(true)
|
||||
const commands = calls.filter(call => call.name === 'bash' || call.name === 'pwsh').map(call => (JSON.parse(call.arguments) as { command: string }).command).join('\n')
|
||||
expect(commands).toMatch(/\bdsoffice["']?\s+render\b/u)
|
||||
expect(commands).toMatch(/\srender\b/u)
|
||||
expect(commands).toContain('--pages')
|
||||
expect(commands).toContain('--output-dir')
|
||||
const pixels = await execa(python!, ['-c', "from pathlib import Path\nfrom PIL import Image\nfiles=list(Path('preview').glob('*.png'))\nassert files\nfor f in files:\n im=Image.open(f).convert('RGBA')\n visible=Image.alpha_composite(Image.new('RGBA',im.size,'white'),im).convert('RGB')\n print('content' if any(a!=b for a,b in visible.getextrema()) else 'blank')"], { cwd })
|
||||
@@ -67,7 +66,7 @@ it.skipIf(!process.env.DEEPSEEK_API_KEY || !runtime).each([{ format: 'docx', mod
|
||||
const authoring = calls.findIndex(call => call.name === 'skill' && /office-(docx|pptx|xlsx)/u.test(call.arguments))
|
||||
if (pixels.stdout.includes('blank')) {
|
||||
if (authoring !== -1) {
|
||||
const rendered = calls.findIndex(call => (call.name === 'bash' || call.name === 'pwsh') && /\bdsoffice["']?\s+render\b/u.test(call.arguments))
|
||||
const rendered = calls.findIndex(call => (call.name === 'bash' || call.name === 'pwsh') && /\srender\b/u.test(call.arguments))
|
||||
expect(authoring, 'content checks after a blank preview may need the format skill').toBeGreaterThan(rendered)
|
||||
}
|
||||
const final = events.filter(e => e.type === 'assistant/message').at(-1)?.data.message.content
|
||||
@@ -122,8 +121,8 @@ it.skipIf(!process.env.DEEPSEEK_API_KEY || !runtime)('exports PDF through the op
|
||||
expect(skills.some(name => ['office-docx', 'office-pptx', 'office-xlsx'].includes(name))).toBe(false)
|
||||
const commands = calls.filter(call => call.name === 'bash' || call.name === 'pwsh')
|
||||
.map(call => (JSON.parse(call.arguments) as { command: string }).command).join('\n')
|
||||
expect(commands).toMatch(/\bdsoffice["']?\s+convert\b/u)
|
||||
expect(commands).not.toMatch(/\bdsoffice["']?\s+render\b/u)
|
||||
expect(commands).toMatch(/\sconvert\b/u)
|
||||
expect(commands).not.toMatch(/\srender\b/u)
|
||||
expect((await readFile(join(cwd, 'result.pdf'))).subarray(0, 5).toString()).toBe('%PDF-')
|
||||
expect(await readFile(join(cwd, 'input.docx'))).toEqual(original)
|
||||
},
|
||||
|
||||
@@ -7,8 +7,12 @@ import Loader from '@deepseek-ai/cordis-plugin-loader'
|
||||
import Include from '@deepseek-ai/cordis-plugin-include'
|
||||
import SkillRegistry from '@deepseek-ai/dsh-skill'
|
||||
import * as SkillOffice from '@deepseek-ai/dsh-skill-office'
|
||||
import { isSea } from 'node:sea'
|
||||
import { execa } from 'execa'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('node:sea', () => ({ isSea: vi.fn(() => false) }))
|
||||
|
||||
const assets = fileURLToPath(new URL('../assets/', import.meta.url))
|
||||
const names = ['office', 'office-docx', 'office-export-pdf', 'office-pptx', 'office-recalculate', 'office-render', 'office-xlsx']
|
||||
|
||||
@@ -47,7 +51,8 @@ describe('bundled Office skills', () => {
|
||||
expect(loaded?.resourceBase).toEqual({ kind: 'directory', path: join(assets, skill.name).split(sep).join('/') })
|
||||
const raw = await readFile(join(assets, skill.name, 'SKILL.md'), 'utf8')
|
||||
const body = raw.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/u, '').trim()
|
||||
expect(loaded?.content).toBe(body)
|
||||
expect(loaded?.content.startsWith(body)).toBe(true)
|
||||
if (!['office-render', 'office-recalculate', 'office-export-pdf'].includes(skill.name)) expect(loaded?.content).toBe(body)
|
||||
}
|
||||
await fiber.dispose()
|
||||
expect(await ctx.skills.list()).toEqual([])
|
||||
@@ -103,27 +108,6 @@ describe('bundled Office skills', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it.each([false, true])('loads carrier resources without its interpreters or renderer (explicit resources: %s)', async (explicit) => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'dsh-office-carrier-'))
|
||||
const ctx = new Context()
|
||||
try {
|
||||
const relocated = join(root, 'office-skills')
|
||||
await cp(assets, relocated, { recursive: true })
|
||||
vi.stubEnv('DSH_PRIMARY_RUNTIME', undefined)
|
||||
vi.stubEnv('DSH_BUNDLED_PRIMARY_RUNTIME', explicit ? undefined : join(root, 'primary-runtime'))
|
||||
vi.stubEnv('DSH_BUNDLED_OFFICE_SKILLS', explicit ? relocated : undefined)
|
||||
await ctx.plugin(SkillRegistry)
|
||||
await ctx.plugin(SkillOffice)
|
||||
for (const name of names) {
|
||||
expect((await ctx.skills.get(name))?.resourceBase).toEqual({ kind: 'directory', path: join(relocated, name).split(sep).join('/') })
|
||||
}
|
||||
} finally {
|
||||
vi.unstubAllEnvs()
|
||||
await ctx.fiber.dispose()
|
||||
await rm(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it.each([
|
||||
['description: "Quoted: description"', '\r\n', 'Quoted: description'],
|
||||
['description: >-\n Folded\n description', '\n', 'Folded description'],
|
||||
@@ -170,3 +154,58 @@ describe('bundled Office skills', () => {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('executes capabilities using only paths returned by the loaded skill from a separate workspace', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'office CLI 工作目录 '))
|
||||
const ctx = new Context()
|
||||
try {
|
||||
await ctx.plugin(SkillRegistry)
|
||||
await ctx.plugin(SkillOffice)
|
||||
const skill = await ctx.skills.get('office-render')
|
||||
const json = skill?.content.match(/\n(\{\n[\s\S]+)$/u)?.[1]
|
||||
expect(json).toBeDefined()
|
||||
const { libreofficeKit } = JSON.parse(json!) as { libreofficeKit: { node: string; cli: string } }
|
||||
const result = await execa(libreofficeKit.node, [libreofficeKit.cli, 'capabilities'], {
|
||||
cwd: root, env: { PATH: '' }, timeout: 20_000, killSignal: 'SIGKILL', reject: false,
|
||||
})
|
||||
expect(result.timedOut, result.stderr).toBe(false)
|
||||
expect(result.signal, result.stderr).toBeUndefined()
|
||||
expect(result.exitCode, result.stderr).toBe(0)
|
||||
expect(JSON.parse(result.stdout)).toMatchObject({ runtime: { version: '0.1.5', cliPath: libreofficeKit.cli } })
|
||||
} finally {
|
||||
await ctx.fiber.dispose()
|
||||
await rm(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
it('validates operation paths on loading while authoring remains available', async () => {
|
||||
const ctx = new Context()
|
||||
const original = Object.getOwnPropertyDescriptor(process.versions, 'electron')
|
||||
try {
|
||||
await ctx.plugin(SkillRegistry)
|
||||
for (const config of [{ node: 'node' }, { cli: 'cli.js' }, { node: assets }, { cli: assets }, { cli: join(assets, 'missing.js') }]) {
|
||||
const fiber = await ctx.plugin(SkillOffice, config)
|
||||
expect((await ctx.skills.get('office-docx'))?.content).toContain('python-docx')
|
||||
await expect(ctx.skills.get('office-render')).rejects.toThrow()
|
||||
await fiber.dispose()
|
||||
}
|
||||
const carrier = await ctx.plugin(SkillOffice)
|
||||
vi.mocked(isSea).mockReturnValue(true)
|
||||
await expect(ctx.skills.get('office-render')).rejects.toThrow('standalone Node')
|
||||
vi.mocked(isSea).mockReturnValue(false)
|
||||
Object.defineProperty(process.versions, 'electron', { value: '44.0.0', configurable: true })
|
||||
await expect(ctx.skills.get('office-render')).rejects.toThrow('standalone Node')
|
||||
await carrier.dispose()
|
||||
const disabled = await ctx.plugin(SkillOffice, { cli: false })
|
||||
expect((await ctx.skills.get('office-render'))?.content).toContain('disabled in this deployment')
|
||||
await disabled.dispose()
|
||||
await ctx.plugin(SkillOffice, { node: process.execPath })
|
||||
expect((await ctx.skills.get('office-render'))?.content).toContain(JSON.stringify(process.execPath))
|
||||
} finally {
|
||||
vi.mocked(isSea).mockReturnValue(false)
|
||||
if (original === undefined) Reflect.deleteProperty(process.versions, 'electron')
|
||||
else Object.defineProperty(process.versions, 'electron', original)
|
||||
await ctx.fiber.dispose()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -84,8 +84,7 @@ it.skipIf(!process.env.DEEPSEEK_API_KEY || !runtime).each(cases)('$format $kind
|
||||
const fixture = await renderCliFixture(controlRoot, python!, intent === 'blank', callLogPath)
|
||||
const testAssetRoot = process.env.DSH_OFFICE_TEST_ASSET_ROOT ?? assetRoot
|
||||
await writeFile(configPath, JSON.stringify([
|
||||
{ insert: [{ id: 'skill-office', name: '@deepseek-ai/dsh-skill-office', config: { assetRoot: testAssetRoot } }] },
|
||||
{ insert: [{ id: 'office-commands', name: '@deepseek-ai/dsh-skill-office/commands', config: { assetRoot: testAssetRoot, command: [process.execPath, fixture.cli] } }] },
|
||||
{ insert: [{ id: 'skill-office', name: '@deepseek-ai/dsh-skill-office', config: { assetRoot: testAssetRoot, node: process.execPath, cli: fixture.cli } }] },
|
||||
{ insert: [{ id: 'workspace-dependencies', name: '@deepseek-ai/dsh-tool-workspace-dependencies', config: { source: runtime! } }] },
|
||||
...officeModelPatches(),
|
||||
{ id: 'session-persistence-jsonl', config: { root: sessionRoot, compression: 'none' } },
|
||||
|
||||
@@ -99,8 +99,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY || !runtime).each(['native', 'ptc'
|
||||
const fixture = kind === 'blank' || kind === 'formula'
|
||||
? await renderCliFixture(controlRoot, python!, kind === 'blank') : undefined
|
||||
await writeFile(configPath, JSON.stringify([
|
||||
{ insert: [{ id: 'skill-office', name: '@deepseek-ai/dsh-skill-office', config: { assetRoot } }] },
|
||||
{ insert: [{ id: 'office-commands', name: '@deepseek-ai/dsh-skill-office/commands', config: { assetRoot, ...fixture ? { command: [process.execPath, fixture.cli] } : {} } }] },
|
||||
{ insert: [{ id: 'skill-office', name: '@deepseek-ai/dsh-skill-office', config: { assetRoot, node: process.execPath, ...fixture ? { cli: fixture.cli } : {} } }] },
|
||||
{ insert: [{ id: 'workspace-dependencies', name: '@deepseek-ai/dsh-tool-workspace-dependencies', config: { source: runtime! } }] },
|
||||
...officeModelPatches(),
|
||||
{ id: 'attachment-local', config: { dshHome: imageRoot } },
|
||||
@@ -187,16 +186,16 @@ else:
|
||||
? answer.data.message.content.flatMap(block => block.type === 'text' ? [block.text] : []).join('\n').slice(0, 800) : undefined })
|
||||
expect(loaded, diagnostic).toContain('office-xlsx')
|
||||
expect(calls.some(call => call.name === 'load_workspace_dependencies'), diagnostic).toBe(true)
|
||||
expect(commands, diagnostic).toContain('office-check')
|
||||
expect(commands, diagnostic).toContain('check_office.py')
|
||||
if (kind === 'formula') {
|
||||
expect(loaded, diagnostic).toContain('office-recalculate')
|
||||
}
|
||||
if (kind === 'layout' || kind === 'blank') {
|
||||
expect(calls.some(call => call.name === 'skill' && call.arguments.includes('office-render'))).toBe(true)
|
||||
expect(commands).toMatch(/\bdsoffice["']?\s+render\b/u)
|
||||
expect(commands).toMatch(/\srender\b/u)
|
||||
expect(commands).toContain('--sheet')
|
||||
expect(commands).toContain('--range')
|
||||
expect(commands).not.toMatch(/\bdsoffice["']?\s+convert\b/u)
|
||||
expect(commands).not.toMatch(/\sconvert\b/u)
|
||||
const reads = calls.filter(call => call.name === 'read_image')
|
||||
const results = events.flatMap(event => event.type === 'tool/result' ? [event.data.message] : [])
|
||||
if (previewHasContent) {
|
||||
|
||||
@@ -19,12 +19,6 @@
|
||||
},
|
||||
{
|
||||
"path": "../skill"
|
||||
},
|
||||
{
|
||||
"path": "../../shell/shell-env"
|
||||
},
|
||||
{
|
||||
"path": "../../util/home-paths"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { defineConfig } from 'tsdown'
|
||||
|
||||
export default defineConfig({
|
||||
entry: { index: 'lib/types/index.js', commands: 'lib/types/commands.js' },
|
||||
outDir: 'lib', format: ['esm'], platform: 'node', target: 'es2024',
|
||||
fixedExtension: false, dts: false, clean: false,
|
||||
})
|
||||
@@ -1,2 +0,0 @@
|
||||
/** Resolve the SDK provider from its owning package for external profile fixtures. */
|
||||
export { Config, apply, inject, name } from '@deepseek-ai/dsh-subagent-dsh-sdk'
|
||||
@@ -36,8 +36,8 @@
|
||||
en: e3631497344c2ebd
|
||||
zh: c4ebc7b08f0a316e
|
||||
/deepseek-ai-dsh-subprocess/use-this-package/environment-every-child-starts-from:
|
||||
en: ad39aefe049a3ce0
|
||||
zh: c24b6034969869fe
|
||||
en: 4d4cbb311575d071
|
||||
zh: 7be2d265adb661bc
|
||||
/deepseek-ai-dsh-subprocess/use-this-package/what-can-go-wrong:
|
||||
en: 51f2f43087a1ed18
|
||||
zh: 6d2ebb099b707df3
|
||||
|
||||
@@ -79,8 +79,6 @@ A terminal request can opt into `shellActivity`. `inspectActivity()` combines su
|
||||
|
||||
Children never inherit the harness's ambient secrets: credential-shaped names and ambient `DSH_*` facts are scrubbed, and the caller's explicit `env` merges after that scrub. A deliberately forwarded credential or a current `DSH_*` deployment fact still reaches the child; an explicit `undefined` tombstone removes an ordinary ambient entry.
|
||||
|
||||
The trusted `DshEnvironment` also permits a `PATH` selected by [shell-env](../../shell/shell-env/README.md) for deployment-supplied commands; shell consumers merge that snapshot after ordinary caller environment entries.
|
||||
|
||||
### What can go wrong
|
||||
|
||||
An executable that cannot be resolved fails loudly with a stable error. A spawn that never starts rejects `done`; there is no buffered output for a process that never ran. `waitForExit()` also rejects when the provider cannot prove its selected range is empty, and a provider fallback may not own descendants that escape its process group or observed session. When a transport owns its own spawn (the SDK client, MCP), route around the service and import `scrubbedParentEnv` directly so environment policy stays single-sourced.
|
||||
|
||||
@@ -79,8 +79,6 @@ const output = handle.collected.stdout?.readFrom(0)
|
||||
|
||||
子进程永远不会隐式继承 harness 的环境秘密:形似凭据的名称与环境中的 `DSH_*` 事实都会被清除,调用方显式的 `env` 在该清除之后合并。有意转发的凭据或当前的 `DSH_*` 部署事实仍会到达子进程;显式的 `undefined` 墓碑值则移除一个普通的环境项。
|
||||
|
||||
受信的 `DshEnvironment` 还允许 [shell-env](../../shell/shell-env/README.zh.md) 为部署提供的命令选择 `PATH`;shell 消费方在普通调用方环境项之后合并该快照。
|
||||
|
||||
### 可能出错的地方
|
||||
|
||||
无法解析可执行文件时,服务会明确报出稳定的错误。从未启动成功的 spawn 会让 `done` reject;从未运行过的进程没有任何缓冲输出。提供方无法证明所选范围为空时,`waitForExit()` 也会 reject;提供方 fallback 可能无法拥有逃离其进程组或已观察会话的后代。当传输拥有自己的 spawn(SDK 客户端、MCP)时,请绕开本服务并直接导入 `scrubbedParentEnv`,让环境策略保持单一来源。
|
||||
|
||||
@@ -15,8 +15,8 @@ export const DSH_ENV_PREFIX = 'DSH_' as const
|
||||
/** One environment key inside the managed {@link DSH_ENV_PREFIX} namespace. */
|
||||
export type DshEnvironmentKey = `${typeof DSH_ENV_PREFIX}${string}`
|
||||
|
||||
/** Trusted Harness variables and optional local command PATH for one child-process execution. */
|
||||
export type DshEnvironment = Readonly<Record<DshEnvironmentKey, string> & { PATH?: string }>
|
||||
/** Trusted DeepSeek Harness variables for one child-process execution. */
|
||||
export type DshEnvironment = Readonly<Record<DshEnvironmentKey, string>>
|
||||
|
||||
/** One captured stream: the (possibly truncated) text plus recovery info. */
|
||||
export interface CollectedOutput {
|
||||
|
||||
Generated
-15
@@ -814,9 +814,6 @@ importers:
|
||||
'@deepseek-ai/cordis-plugin-loader':
|
||||
specifier: workspace:~
|
||||
version: link:../../vendor/loader
|
||||
'@deepseek-ai/dsh-shell-env':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/shell/shell-env
|
||||
'@deepseek-ai/dsh-skill':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/skill/skill
|
||||
@@ -12955,21 +12952,9 @@ importers:
|
||||
'@deepseek-ai/cordis-plugin-loader':
|
||||
specifier: workspace:~
|
||||
version: link:../../../vendor/loader
|
||||
'@deepseek-ai/dsh-home-paths':
|
||||
specifier: workspace:*
|
||||
version: link:../../util/home-paths
|
||||
'@deepseek-ai/dsh-pwsh-local':
|
||||
specifier: workspace:*
|
||||
version: link:../../shell/pwsh-local
|
||||
'@deepseek-ai/dsh-shell-env':
|
||||
specifier: workspace:*
|
||||
version: link:../../shell/shell-env
|
||||
'@deepseek-ai/dsh-skill':
|
||||
specifier: workspace:*
|
||||
version: link:../skill
|
||||
'@deepseek-ai/dsh-subprocess-local':
|
||||
specifier: workspace:*
|
||||
version: link:../../subprocess/subprocess-local
|
||||
|
||||
packages/skill/tool-skill:
|
||||
dependencies:
|
||||
|
||||
@@ -306,7 +306,6 @@ describe('package payload constraints', () => {
|
||||
])
|
||||
})
|
||||
|
||||
|
||||
it('accepts the Agent Team icon payload and rejects its omission', () => {
|
||||
const dir = 'packages/experimental/agent-team-profile'
|
||||
const manifest = JSON.parse(readFileSync(new URL(`../${dir}/package.json`, import.meta.url), 'utf8')) as WorkspaceManifest['manifest']
|
||||
@@ -404,14 +403,12 @@ it('requires the shared Web injection entry in the published payload', () => {
|
||||
} })).toEqual([expect.stringContaining('package.json files must be')])
|
||||
})
|
||||
|
||||
it('requires Office commands, skill bodies, and helpers in the published payload', () => {
|
||||
it('requires Office skill bodies and helpers in the published payload', () => {
|
||||
const manifest = JSON.parse(readFileSync(new URL('../packages/skill/skill-office/package.json', import.meta.url), 'utf8')) as WorkspaceManifest['manifest']
|
||||
expect(checkWorkspaceManifest({ dir: 'packages/skill/skill-office', manifest })).toEqual([])
|
||||
for (const omitted of ['lib/commands.js', 'assets']) {
|
||||
expect(checkWorkspaceManifest({ dir: 'packages/skill/skill-office', manifest: {
|
||||
...manifest, files: manifest.files!.filter(file => file !== omitted),
|
||||
} })).toEqual([expect.stringContaining('package.json files must be')])
|
||||
}
|
||||
expect(checkWorkspaceManifest({ dir: 'packages/skill/skill-office', manifest: {
|
||||
...manifest, files: ['lib/index.js', 'lib/types/**/*.d.ts'],
|
||||
} })).toEqual([expect.stringContaining('package.json files must be')])
|
||||
})
|
||||
|
||||
it('requires the local speech worker and locked runtime in the published payload', () => {
|
||||
|
||||
@@ -206,7 +206,7 @@ const packageFileExtras: Readonly<Record<string, readonly string[]>> = {
|
||||
// also shares its generated FFI code through a hashed runtime chunk.
|
||||
'@deepseek-ai/dsh-sandbox-windows-acl': ['lib/runner.js', 'lib/types-*.js', 'assets'],
|
||||
'@deepseek-ai/dsh-skill-badge': ['assets'],
|
||||
'@deepseek-ai/dsh-skill-office': ['lib/commands.js', 'assets'],
|
||||
'@deepseek-ai/dsh-skill-office': ['assets'],
|
||||
'@deepseek-ai/dsh-subprocess': ['lib/control.js'],
|
||||
// SSH launches a private helper and shares wire definitions and TLS setup
|
||||
// between that helper and the connection owner.
|
||||
|
||||
@@ -1,37 +1,20 @@
|
||||
/** Exercise the wheel's Office API and model-shell commands from a shipped profile. */
|
||||
import { mkdtemp, readFile, rm, symlink, writeFile } from 'node:fs/promises'
|
||||
import { delimiter, dirname, join } from 'node:path'
|
||||
import { readFile, writeFile } from 'node:fs/promises'
|
||||
import { execFile } from 'node:child_process'
|
||||
import { promisify } from 'node:util'
|
||||
import { createConverter } from '@deepseek-ai/libreoffice-kit'
|
||||
|
||||
export const name = 'python-sdk-office-smoke'
|
||||
|
||||
export async function apply(ctx, config) {
|
||||
const environment = ctx.shellEnv.collect({})
|
||||
const commandDirectory = environment.PATH?.split(delimiter)[0]
|
||||
if (!commandDirectory) throw new Error('Office commands were not registered in the profile')
|
||||
const commandPaths = [commandDirectory]
|
||||
if (process.platform !== 'win32') {
|
||||
const shellDirectory = await mkdtemp(join(dirname(config.input), '.office-shell-'))
|
||||
ctx.effect(() => () => rm(shellDirectory, { recursive: true, force: true }))
|
||||
await symlink('/bin/bash', join(shellDirectory, 'bash'))
|
||||
commandPaths.push(shellDirectory)
|
||||
}
|
||||
const quote = value => process.platform === 'win32'
|
||||
? `'${value.replaceAll("'", "''")}'` : `'${value.replaceAll("'", "'\\''")}'`
|
||||
async function cli(args) {
|
||||
const spec = ctx.shell.resolve({
|
||||
command: ['dsoffice', ...args.map(quote)].join(' '), workdir: dirname(config.input),
|
||||
timeoutMs: 120_000, stdoutMaxBytes: 64 * 1024,
|
||||
env: { PATH: '' }, dshEnv: { ...environment, PATH: commandPaths.join(delimiter) },
|
||||
})
|
||||
const result = await (await ctx.shell.execute(spec)).result()
|
||||
if (result.timedOut || result.aborted || result.signal || result.exitCode !== 0 || result.stdout.truncated) {
|
||||
throw new Error(`Office shell command failed: ${JSON.stringify(result)}`)
|
||||
}
|
||||
return JSON.parse(result.stdout.text)
|
||||
}
|
||||
const skill = await ctx.skills.get('office-export-pdf')
|
||||
if (!skill?.content.includes('dsoffice convert')) throw new Error('Office skill did not name the installed command')
|
||||
const json = skill?.content.match(/\n(\{\n[\s\S]+)$/u)?.[1]
|
||||
if (json === undefined) throw new Error('Office skill did not supply CLI paths')
|
||||
const { libreofficeKit: { node, cli: entry } } = JSON.parse(json)
|
||||
async function cli(args) {
|
||||
const result = await promisify(execFile)(node, [entry, ...args], { timeout: 120_000, env: { ...process.env, PATH: '' } })
|
||||
return JSON.parse(result.stdout)
|
||||
}
|
||||
const capabilities = await cli(['capabilities', '--json'])
|
||||
await cli(['convert', '--input', config.input, '--output', config.output + '.cli.pdf'])
|
||||
const pdf = await readFile(config.output + '.cli.pdf')
|
||||
|
||||
@@ -93,14 +93,6 @@ describe.each([experimental, ...Object.values(EXPERIMENTAL_PACKAGE_NAME_EXCEPTIO
|
||||
expect(() => verifyInstalledProductIsolation(entry)).toThrow(experimental)
|
||||
})
|
||||
|
||||
it('finds installed packages whose names also identify Node builtins', () => {
|
||||
const root = fixture()
|
||||
const entry = writePackage(root, '@deepseek-ai/dsh', { dependencies: { fs: '1.0.0' } })
|
||||
expect(() => verifyInstalledProductIsolation(entry)).toThrow('dependency is missing: @deepseek-ai/dsh -> fs')
|
||||
writePackage(root, 'fs', {})
|
||||
expect(verifyInstalledProductIsolation(entry)).toBe(2)
|
||||
})
|
||||
|
||||
it('permits missing optional packages and optional peers but rejects missing dependencies', () => {
|
||||
const root = fixture()
|
||||
const entry = writePackage(root, '@deepseek-ai/dsh', {
|
||||
|
||||
@@ -62,10 +62,10 @@ function rejectExperimental(name: string, chain: readonly string[]): void {
|
||||
}
|
||||
}
|
||||
|
||||
/** Resolve a declared package through ancestor node_modules, including names shared with Node builtins. */
|
||||
/** Resolve a dependency directory through the installed package's ancestor node_modules. */
|
||||
function installedPackage(from: string, name: string): string | undefined {
|
||||
const resolver = createRequire(join(from, 'package.json'))
|
||||
for (const directory of resolver.resolve.paths(`${name}/package.json`) ?? []) {
|
||||
for (const directory of resolver.resolve.paths(name) ?? []) {
|
||||
const candidate = join(directory, name)
|
||||
if (existsSync(join(candidate, 'package.json'))) return candidate
|
||||
}
|
||||
|
||||
@@ -1016,7 +1016,7 @@ def smoke_sdk_office(executable: Path) -> None:
|
||||
patch = root / f"{mode}.patch.yml"
|
||||
patch.write_text(json.dumps([{"insert": [{
|
||||
"id": "python-sdk-office-smoke",
|
||||
"inject": ["skills", "shell", "shellEnv"],
|
||||
"inject": ["skills"],
|
||||
"name": plugin.as_uri(),
|
||||
"config": {"input": str(document), "output": str(output), "result": str(result_path),
|
||||
"imageInput": str(documents["pptx"]), "imageOutput": str(root / "node-pptx")},
|
||||
|
||||
@@ -68,9 +68,6 @@
|
||||
name: '@deepseek-ai/dsh-tool-skill'
|
||||
disabled: true
|
||||
|
||||
- id: workspace-dependencies
|
||||
disabled: true
|
||||
|
||||
- id: tool-jobs
|
||||
name: '@deepseek-ai/dsh-tool-jobs'
|
||||
disabled: true
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
disabled: !!js process.env.DSH_SNAPSHOT !== 'record'
|
||||
|
||||
- id: subagent-dsh-sdk
|
||||
name: '../../../packages/subagent/subagent-dsh-sdk/tests/fixtures/loader/provider.mjs'
|
||||
name: '@deepseek-ai/dsh-subagent-dsh-sdk'
|
||||
config:
|
||||
profile: sdk
|
||||
patches: !!js JSON.parse(process.env.DSH_TEST_CHILD_PATCHES ?? '[]')
|
||||
|
||||
@@ -65,9 +65,6 @@
|
||||
name: '@deepseek-ai/dsh-skill-filesystem'
|
||||
disabled: true
|
||||
|
||||
- id: skill-office
|
||||
disabled: true
|
||||
|
||||
- id: tool-skill
|
||||
name: '@deepseek-ai/dsh-tool-skill'
|
||||
disabled: true
|
||||
|
||||
@@ -57,9 +57,6 @@
|
||||
name: '@deepseek-ai/dsh-skill-filesystem'
|
||||
disabled: true
|
||||
|
||||
- id: skill-office
|
||||
disabled: true
|
||||
|
||||
- id: tool-skill
|
||||
name: '@deepseek-ai/dsh-tool-skill'
|
||||
disabled: true
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
disabled: false
|
||||
config:
|
||||
assetRoot: !!js process.cwd() + '/office-skills'
|
||||
cli: false
|
||||
|
||||
- id: tool-bash
|
||||
disabled: true
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
disabled: false
|
||||
config:
|
||||
assetRoot: !!js process.cwd() + '/office-skills'
|
||||
cli: false
|
||||
|
||||
- id: tool-bash
|
||||
disabled: true
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -52,6 +52,8 @@
|
||||
disabled: false
|
||||
config:
|
||||
assetRoot: !!js process.cwd() + '/office-skills'
|
||||
node: !!js process.cwd() + '/office-node'
|
||||
cli: !!js process.cwd() + '/render-cli.cjs'
|
||||
|
||||
- id: tool-bash
|
||||
disabled: true
|
||||
@@ -68,10 +70,3 @@
|
||||
launch:
|
||||
kind: node-script
|
||||
executable: !!js process.cwd() + '/office-ptc-node'
|
||||
|
||||
- insert:
|
||||
- id: office-commands
|
||||
name: '@deepseek-ai/dsh-skill-office/commands'
|
||||
disabled: false
|
||||
config:
|
||||
command: !!js "[process.cwd() + '/office-node', process.cwd() + '/render-cli.cjs']"
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
name: '@deepseek-ai/dsh-skill-office'
|
||||
config:
|
||||
assetRoot: !!js process.cwd() + '/office-skills'
|
||||
node: !!js process.cwd() + '/office-node'
|
||||
cli: !!js process.cwd() + '/render-cli.cjs'
|
||||
|
||||
- id: tool-bash
|
||||
disabled: true
|
||||
@@ -13,9 +15,3 @@
|
||||
- id: tools
|
||||
config:
|
||||
mode: ptc
|
||||
|
||||
- insert:
|
||||
- id: office-commands
|
||||
name: '@deepseek-ai/dsh-skill-office/commands'
|
||||
config:
|
||||
command: !!js "[process.cwd() + '/office-node', process.cwd() + '/render-cli.cjs']"
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -52,16 +52,11 @@
|
||||
disabled: false
|
||||
config:
|
||||
assetRoot: !!js process.cwd() + '/office-skills'
|
||||
node: !!js process.cwd() + '/office-node'
|
||||
cli: !!js process.cwd() + '/render-cli.cjs'
|
||||
|
||||
- id: tool-bash
|
||||
disabled: true
|
||||
|
||||
- id: tool-pwsh
|
||||
disabled: true
|
||||
|
||||
- insert:
|
||||
- id: office-commands
|
||||
name: '@deepseek-ai/dsh-skill-office/commands'
|
||||
disabled: false
|
||||
config:
|
||||
command: !!js "[process.cwd() + '/office-node', process.cwd() + '/render-cli.cjs']"
|
||||
|
||||
@@ -3,15 +3,11 @@
|
||||
name: '@deepseek-ai/dsh-skill-office'
|
||||
config:
|
||||
assetRoot: !!js process.cwd() + '/office-skills'
|
||||
node: !!js process.cwd() + '/office-node'
|
||||
cli: !!js process.cwd() + '/render-cli.cjs'
|
||||
|
||||
- id: tool-bash
|
||||
disabled: true
|
||||
|
||||
- id: tool-pwsh
|
||||
disabled: true
|
||||
|
||||
- insert:
|
||||
- id: office-commands
|
||||
name: '@deepseek-ai/dsh-skill-office/commands'
|
||||
config:
|
||||
command: !!js "[process.cwd() + '/office-node', process.cwd() + '/render-cli.cjs']"
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -52,6 +52,7 @@
|
||||
disabled: false
|
||||
config:
|
||||
assetRoot: !!js process.cwd() + '/office-skills'
|
||||
cli: false
|
||||
|
||||
- id: tool-bash
|
||||
disabled: true
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
disabled: false
|
||||
config:
|
||||
assetRoot: !!js process.cwd() + '/office-skills'
|
||||
cli: false
|
||||
|
||||
- id: tool-bash
|
||||
disabled: true
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -21,16 +21,11 @@
|
||||
disabled: false
|
||||
config:
|
||||
assetRoot: !!js process.cwd() + '/office-skills'
|
||||
node: !!js process.cwd() + '/office-node'
|
||||
cli: !!js process.cwd() + '/render-cli.cjs'
|
||||
|
||||
- id: tool-bash
|
||||
disabled: true
|
||||
|
||||
- id: tool-pwsh
|
||||
disabled: true
|
||||
|
||||
- insert:
|
||||
- id: office-commands
|
||||
name: '@deepseek-ai/dsh-skill-office/commands'
|
||||
disabled: false
|
||||
config:
|
||||
command: false
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
name: '@deepseek-ai/dsh-skill-office'
|
||||
config:
|
||||
assetRoot: !!js process.cwd() + '/office-skills'
|
||||
node: !!js process.cwd() + '/office-node'
|
||||
cli: !!js process.cwd() + '/render-cli.cjs'
|
||||
|
||||
- id: llm-deepseek
|
||||
config:
|
||||
@@ -17,9 +19,3 @@
|
||||
|
||||
- id: tool-pwsh
|
||||
disabled: true
|
||||
|
||||
- insert:
|
||||
- id: office-commands
|
||||
name: '@deepseek-ai/dsh-skill-office/commands'
|
||||
config:
|
||||
command: false
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -50,16 +50,11 @@
|
||||
disabled: false
|
||||
config:
|
||||
assetRoot: !!js process.cwd() + '/office-skills'
|
||||
node: !!js process.cwd() + '/office-node'
|
||||
cli: !!js process.cwd() + '/office-cli.js'
|
||||
|
||||
- id: tool-bash
|
||||
disabled: true
|
||||
|
||||
- id: tool-pwsh
|
||||
disabled: true
|
||||
|
||||
- insert:
|
||||
- id: office-commands
|
||||
name: '@deepseek-ai/dsh-skill-office/commands'
|
||||
disabled: false
|
||||
config:
|
||||
command: !!js "[process.cwd() + '/office-node', process.cwd() + '/office-cli.js']"
|
||||
|
||||
@@ -3,15 +3,11 @@
|
||||
name: '@deepseek-ai/dsh-skill-office'
|
||||
config:
|
||||
assetRoot: !!js process.cwd() + '/office-skills'
|
||||
node: !!js process.cwd() + '/office-node'
|
||||
cli: !!js process.cwd() + '/office-cli.js'
|
||||
|
||||
- id: tool-bash
|
||||
disabled: true
|
||||
|
||||
- id: tool-pwsh
|
||||
disabled: true
|
||||
|
||||
- insert:
|
||||
- id: office-commands
|
||||
name: '@deepseek-ai/dsh-skill-office/commands'
|
||||
config:
|
||||
command: !!js "[process.cwd() + '/office-node', process.cwd() + '/office-cli.js']"
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -79,14 +79,6 @@
|
||||
name: '@deepseek-ai/dsh-skill-filesystem'
|
||||
disabled: true
|
||||
|
||||
- id: skill-office
|
||||
name: '@deepseek-ai/dsh-skill-office'
|
||||
disabled: true
|
||||
|
||||
- id: workspace-dependencies
|
||||
name: '@deepseek-ai/dsh-tool-workspace-dependencies'
|
||||
disabled: true
|
||||
|
||||
- id: tool-skill
|
||||
name: '@deepseek-ai/dsh-tool-skill'
|
||||
disabled: true
|
||||
|
||||
@@ -71,14 +71,6 @@
|
||||
name: '@deepseek-ai/dsh-skill-filesystem'
|
||||
disabled: true
|
||||
|
||||
- id: skill-office
|
||||
name: '@deepseek-ai/dsh-skill-office'
|
||||
disabled: true
|
||||
|
||||
- id: workspace-dependencies
|
||||
name: '@deepseek-ai/dsh-tool-workspace-dependencies'
|
||||
disabled: true
|
||||
|
||||
- id: tool-skill
|
||||
name: '@deepseek-ai/dsh-tool-skill'
|
||||
disabled: true
|
||||
|
||||
@@ -82,14 +82,6 @@
|
||||
name: '@deepseek-ai/dsh-skill-filesystem'
|
||||
disabled: true
|
||||
|
||||
- id: skill-office
|
||||
name: '@deepseek-ai/dsh-skill-office'
|
||||
disabled: true
|
||||
|
||||
- id: workspace-dependencies
|
||||
name: '@deepseek-ai/dsh-tool-workspace-dependencies'
|
||||
disabled: true
|
||||
|
||||
- id: tool-skill
|
||||
name: '@deepseek-ai/dsh-tool-skill'
|
||||
disabled: true
|
||||
|
||||
@@ -74,14 +74,6 @@
|
||||
name: '@deepseek-ai/dsh-skill-filesystem'
|
||||
disabled: true
|
||||
|
||||
- id: skill-office
|
||||
name: '@deepseek-ai/dsh-skill-office'
|
||||
disabled: true
|
||||
|
||||
- id: workspace-dependencies
|
||||
name: '@deepseek-ai/dsh-tool-workspace-dependencies'
|
||||
disabled: true
|
||||
|
||||
- id: tool-skill
|
||||
name: '@deepseek-ai/dsh-tool-skill'
|
||||
disabled: true
|
||||
|
||||
@@ -72,14 +72,6 @@
|
||||
name: '@deepseek-ai/dsh-skill-filesystem'
|
||||
disabled: true
|
||||
|
||||
- id: skill-office
|
||||
name: '@deepseek-ai/dsh-skill-office'
|
||||
disabled: true
|
||||
|
||||
- id: workspace-dependencies
|
||||
name: '@deepseek-ai/dsh-tool-workspace-dependencies'
|
||||
disabled: true
|
||||
|
||||
- id: tool-skill
|
||||
name: '@deepseek-ai/dsh-tool-skill'
|
||||
disabled: true
|
||||
|
||||
@@ -64,14 +64,6 @@
|
||||
name: '@deepseek-ai/dsh-skill-filesystem'
|
||||
disabled: true
|
||||
|
||||
- id: skill-office
|
||||
name: '@deepseek-ai/dsh-skill-office'
|
||||
disabled: true
|
||||
|
||||
- id: workspace-dependencies
|
||||
name: '@deepseek-ai/dsh-tool-workspace-dependencies'
|
||||
disabled: true
|
||||
|
||||
- id: tool-skill
|
||||
name: '@deepseek-ai/dsh-tool-skill'
|
||||
disabled: true
|
||||
|
||||
@@ -78,8 +78,6 @@
|
||||
"@deepseek-ai/dsh-experimental-browser-use-runtime/mcp": ["./packages/experimental/browser-use-runtime/src/mcp.ts"],
|
||||
|
||||
"@deepseek-ai/dsh-office-to-pdf/types": ["./packages/document/office-to-pdf/src/types.ts"],
|
||||
"@deepseek-ai/dsh-skill-office/commands": ["./packages/skill/skill-office/src/commands.ts"],
|
||||
"@deepseek-ai/dsh-skill-office/check": ["./packages/skill/skill-office/src/check.ts"],
|
||||
"@deepseek-ai/dsh-api-terminal-controller": ["./packages/api/terminal-controller/src/index.ts"],
|
||||
"@deepseek-ai/dsh-api-terminal-controller/types": ["./packages/api/terminal-controller/src/types.ts"],
|
||||
"@deepseek-ai/dsh-api-terminal-controller/client": ["./packages/api/terminal-controller/src/client/index.ts"],
|
||||
|
||||
Reference in New Issue
Block a user