fix(cli): isolate Web runtime context from headless

This commit is contained in:
Yichen Jiang
2026-08-06 20:11:44 +08:00
parent 0cf1ba7f87
commit 50c46be948
9 changed files with 46 additions and 12 deletions

View File

@@ -46,11 +46,13 @@
# `dshClient` rows are the browser roster the modules node half scans into
# window.__DSH_BOOT__; the modules row is simultaneously a host row.
- insert:
# Launcher-provided builtin: owns Web GUI orientation in the model prompt
# and the matching managed Bash variables. Profiles with a complete prompt
# contract disable this row instead of relying on launcher special cases.
# Launcher-provided builtin: Headless keeps this shared row disabled, while
# `dsh web` enables it before the later personal/--config layer. Profiles
# with a complete prompt contract can therefore disable both Web GUI
# orientation and its matching managed Bash variables.
- id: web-runtime-context
name: cordis:web-runtime-context
disabled: true
- id: session-projection
name: '@deepseek-ai/dsh-session-projection'

View File

@@ -141,9 +141,15 @@ export interface AppCLIEntryOptions {
*/
overlayPath: string
/**
* Optional explicit overlay applied after {@link overlayPath} and before
* Launcher-owned patches applied after {@link overlayPath} and before the
* personal or explicit overlay, so user configuration can still override
* surface activation choices.
*/
launcherPatches?: readonly PatchOptions[]
/**
* Optional explicit overlay applied after {@link launcherPatches} and before
* this entry's own profile/flag patches. When absent, the personal
* `$DSH_HOME/config.yaml` overlay is applied instead.
* `$DSH_HOME/config.yaml` overlay is applied in the same position instead.
*/
extraOverlayPath?: string
/** Whether to append client-bundle HMR (the Web surface's prod/dev difference). */
@@ -261,10 +267,12 @@ export class AppCLIEntry {
private async bootTree(): Promise<void> {
// One include of the shared base with every overlay as a sibling patch
// list: patches never cross an include boundary, so nesting them would
// silently stop reaching base rows. The surface overlay applies first, then
// this entry's profile-json and CLI-flag patches, which therefore win.
// silently stop reaching base rows. The shared surface overlay applies
// first, then launcher activation, user configuration, and finally this
// entry's profile-json and CLI-flag patches.
const compose = (overlay: PatchOptions[]): PatchOptions[] => [
...loadOverlayPatches('dsh', this.options.overlayPath),
...(this.options.launcherPatches ?? []),
...overlay,
...this.patches,
]

View File

@@ -14,6 +14,7 @@ import {
type ConfigDumpLayer,
} from '@deepseek-ai/dsh-app-boot'
import { resolveDshHome } from '@deepseek-ai/dsh-paths'
import { WEB_RUNTIME_CONTEXT_ENABLE_PATCH } from './web.ts'
const NAME = 'dsh'
const BASE_CONFIG = fileURLToPath(new URL('../config/base.cordis.yml', import.meta.url))
@@ -36,6 +37,7 @@ export function runDumpConfig(surface: 'config' | 'web', defaultOnly: boolean, c
}
} else {
layers.push({ label: basename(WEB_OVERLAY), patches: loadOverlayPatches(NAME, WEB_OVERLAY) })
layers.push({ label: 'dsh web launcher', patches: [WEB_RUNTIME_CONTEXT_ENABLE_PATCH] })
if (!defaultOnly) {
if (config === undefined) {
const personal = loadPersonalPatches(NAME)

View File

@@ -8,6 +8,7 @@
import { fileURLToPath } from 'node:url'
import type { Context } from 'cordis'
import type { PatchOptions } from '@cordisjs/plugin-include'
import { addHarnessSourceSection, resolveConfigPath } from '@deepseek-ai/dsh-app-boot'
import type {} from '@deepseek-ai/dsh-host-webserver'
import type {} from '@deepseek-ai/dsh-system-prompt'
@@ -24,6 +25,12 @@ const DSH_WEB_URL = 'DSH_WEB_URL' as const
const DSH_WEB_MODE = 'DSH_WEB_MODE' as const
const WEB_RUNTIME_CONTEXT_BUILTIN = 'web-runtime-context' as const
/** Web-launcher activation applied before personal or explicit configuration. */
export const WEB_RUNTIME_CONTEXT_ENABLE_PATCH = {
id: WEB_RUNTIME_CONTEXT_BUILTIN,
disabled: false,
} as const satisfies PatchOptions
type WebMode = 'production' | 'development'
// Display-only mirror of the webserver schema's loopback host: the address the
@@ -125,6 +132,7 @@ export async function runWeb(
const entry = new AppCLIEntry({
configPath: BASE_CONFIG,
overlayPath: WEB_OVERLAY,
launcherPatches: [WEB_RUNTIME_CONTEXT_ENABLE_PATCH],
...config !== undefined && { extraOverlayPath: resolveConfigPath(config, undefined) },
dev,
prepare: (ctx) => { prepareWebRuntimeContext(ctx, SOURCE_ROOT, mode) },

View File

@@ -10,6 +10,7 @@ const repoRoot = fileURLToPath(new URL('../../../', import.meta.url))
const dshBin = join(repoRoot, 'apps/cli/lib/bin.js')
const rawOverlay = fileURLToPath(new URL('./fixtures/raw-overlay.cordis.yml', import.meta.url))
const rawInvalidProvider = fileURLToPath(new URL('./fixtures/raw-invalid-provider.cordis.yml', import.meta.url))
const coreWebOverlay = fileURLToPath(new URL('../config/core-web.cordis.yml', import.meta.url))
async function runBuiltBin(
args: readonly string[] = [],
@@ -200,6 +201,17 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
expect(code).toBe(0)
expect(stdout).toContain("name: '@deepseek-ai/dsh-host-webserver'")
expect(stdout).toContain('provider: personal-provider')
expect(stdout).toMatch(/- id: web-runtime-context\n name: cordis:web-runtime-context\n disabled: false/u)
}, 30_000)
it('lets an explicit Web profile override launcher activation', async () => {
const { stdout, code, stderr } = await runBuiltBin(
['web', '--dump-config', '--config', coreWebOverlay],
{ DSH_HOME: home },
)
expect(code).toBe(0)
expect(stderr).toBe('')
expect(stdout).toMatch(/- id: web-runtime-context\n name: cordis:web-runtime-context\n disabled: true/u)
}, 30_000)
})
})