refactor(cua): remove automatic Coding Tools configuration
This commit is contained in:
1 parent
adde9215aa
commit
74e178cb03
17 files changed
+45
-259
No files matched your search
@@ -14,7 +14,7 @@ import { captureStableAria, compareOrRefreshGolden, launchWebScaffold, webSnapsh
|
||||
import { connectFreshWorkspaceZh, writeComposerDraft, ZH_BROWSER_LOCALE } from './support.ts'
|
||||
|
||||
// The shipped bundle admits only macOS Hosts; unsupported Hosts are exercised in bundle-platform.e2e.ts.
|
||||
it.skipIf(process.platform !== 'darwin').each([undefined, 'minimal'] as const)('hot-reloads CUA and changes the default %s only from its detail page', async (selectedDefault) => {
|
||||
it.skipIf(process.platform !== 'darwin').each([undefined, 'minimal'] as const)('hot-reloads CUA while preserving Coding Tools and changes the default %s only from its detail page', async (selectedDefault) => {
|
||||
const directory = await mkdtemp(join(tmpdir(), 'cua-optin-web-'))
|
||||
let ownedScaffold: WebScaffold | undefined
|
||||
let browser: Browser | undefined
|
||||
@@ -28,6 +28,10 @@ it.skipIf(process.platform !== 'darwin').each([undefined, 'minimal'] as const)('
|
||||
] })
|
||||
const initialDefault = selectedDefault ?? 'standard'
|
||||
if (selectedDefault !== undefined) await scaffold.ctx.settings.update('agent-preset-registry', { selectedDefault })
|
||||
const codingTools = selectedDefault !== undefined
|
||||
await scaffold.ctx.settings.update('ui-settings', { enabled: codingTools })
|
||||
const settingsPath = join(harnessHome, 'profiles/scaffold/cordis.patch.yml')
|
||||
const beforeEnable = await readFile(settingsPath, 'utf8')
|
||||
browser = await chromium.launch()
|
||||
const page = await browser.newPage({ viewport: { width: 1680, height: 1000 }, locale: ZH_BROWSER_LOCALE })
|
||||
await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
|
||||
@@ -119,11 +123,12 @@ it.skipIf(process.platform !== 'darwin').each([undefined, 'minimal'] as const)('
|
||||
expect(await setup.getByText('Computer Use 插件已开启,但暂时无法检测系统权限。请前往插件详情页查看原因并重试。', { exact: true }).count()).toBe(1)
|
||||
await setup.getByRole('button', { name: '稍后', exact: true }).click()
|
||||
await setup.waitFor({ state: 'hidden' })
|
||||
const settingsPath = join(harnessHome, 'profiles/scaffold/cordis.patch.yml')
|
||||
let saved = await readFile(settingsPath, 'utf8')
|
||||
expect(saved).toBe(beforeEnable)
|
||||
expect(scaffold.ctx.settings.describe().find(row => row.ns === 'ui-settings')?.value).toMatchObject({ enabled: codingTools })
|
||||
expect(saved).not.toContain('executionMode:')
|
||||
expect(load(saved)).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ id: 'ui-settings', config: { enabled: true } }),
|
||||
expect.objectContaining({ id: 'ui-settings', config: { enabled: codingTools } }),
|
||||
]))
|
||||
if (selectedDefault === undefined) {
|
||||
expect(load(saved)).not.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'agent-preset-registry' })]))
|
||||
@@ -208,6 +213,7 @@ it.skipIf(process.platform !== 'darwin').each([undefined, 'minimal'] as const)('
|
||||
expect(await setup.getByText('Computer Use 插件已开启,但暂时无法检测系统权限。请前往插件详情页查看原因并重试。', { exact: true }).count()).toBe(1)
|
||||
expect(await setup.getByRole('button', { name: '设为 PTC', exact: true }).count()).toBe(0)
|
||||
expect(await readFile(settingsPath, 'utf8')).toBe(confirmedSettings)
|
||||
expect(scaffold.ctx.settings.describe().find(row => row.ns === 'ui-settings')?.value).toMatchObject({ enabled: codingTools })
|
||||
await runtime.release(owner)
|
||||
} finally {
|
||||
try { await browser?.close() } finally {
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
en: 87679c51aa3693f5
|
||||
zh: 11684a48a5806afb
|
||||
/deepseek-ai-dsh-plugin-manager/use-this-package:
|
||||
en: 8e8a34186b091682
|
||||
zh: c94654f370a1baa1
|
||||
en: 69851ea018e162c7
|
||||
zh: ec007c03f7a6b20b
|
||||
/deepseek-ai-dsh-plugin-manager/use-this-package/version-compatibility-and-exemptions:
|
||||
en: 483285658e267dd5
|
||||
zh: 846bec2daac59a96
|
||||
|
||||
@@ -39,9 +39,7 @@ For a deployment without agent presets, enable the tool in the profile patch. Pr
|
||||
|
||||
A plugin toggle updates only `disabled` in the last matching override in the profile's `cordis.patch.yml`, or appends an override when none matches. Matching uses the entry id and any module-name assertion. An override holding only `id` and `disabled` is removed, and none is appended, when the bundle layers and the remaining profile patches already give the entry the requested state, so the profile patch records only departures from the bundles. Home and invocation patches are left out of that comparison; they apply after the profile patch and do not decide what it records. A bundle toggle changes `package.json`'s ordered `dsh.profile.bundles` list. Disabling retains the dependency; enabling appends the bundle at the end, which can change configuration precedence. Installation enables a new bundle by default. Home and invocation patches retain their higher priority.
|
||||
|
||||
Bundle platform declarations are checked before saving an enabled selection. Unsupported Hosts return `unsupported-platform` without applying runtime overrides. The bundle remains visible with its display metadata and can still be deselected. Automatic activation after a package operation uses the same check. The agent-facing next step asks the user to run the bundle on a supported Host.
|
||||
|
||||
Bundles may declare `dsh.bundle.configureOnEnable`, mapping existing profile entry ids to top-level config fields. Explicit enablement merges changed fields into the user patch before the ordinary HMR reconciliation, preserving unrelated settings. Missing or ambiguous entries, whole-config `!!js` expressions, and conflicting home or launch overlays reject the operation before saving. Express the target config as a literal mapping to allow enablement edits; expressions on fields being set are replaced by the requested literals, while unrelated field expressions are preserved. Configuration anchors remain available to other entries; anchored overrides are retained and followed by the activation override. Matching values cause no rewrite. Disablement retains these settings. The CLI and manual bundle-list edits do not apply these enablement settings.
|
||||
Bundle platform declarations are checked before saving an enabled selection. Unsupported Hosts return `unsupported-platform`. The bundle remains visible with its display metadata and can still be deselected. Automatic activation after a package operation uses the same check. The agent-facing next step asks the user to run the bundle on a supported Host.
|
||||
|
||||
A selected bundle that cannot load remains in `listBundles` with an `error`; `enabled` records the saved selection, not successful loading. The plugin page shows the error and allows deselection. A broken bundle cannot be enabled. Management bundles remain protected if their files become unreadable.
|
||||
|
||||
|
||||
@@ -39,9 +39,7 @@ kind: "package-reference"
|
||||
|
||||
插件开关只更新 profile 的 `cordis.patch.yml` 中最后一条匹配覆盖项的 `disabled`;没有匹配项时追加。匹配依据是条目 id,以及覆盖项声明的模块名称。如果组合包层和其余 profile patch 已使条目处于所请求的状态,仅含 `id` 与 `disabled` 的覆盖项会被移除,也不会追加覆盖项,因此 profile patch 只记录与组合包不同的设置。该比较不计入 home 和单次启动 patch;它们在 profile patch 之后应用,不决定 profile patch 记录什么。组合包开关修改 `package.json` 的有序 `dsh.profile.bundles` 列表。关闭保留依赖;开启追加到列表末尾,可能改变配置优先级。安装新组合包默认启用。home 和单次启动 patch 保留更高优先级。
|
||||
|
||||
组合包的平台声明会在保存启用选择前检查。Host 不受支持时返回 `unsupported-platform`,不应用运行时配置覆盖。组合包仍保留显示信息,并允许取消选择。包操作后的自动启用执行同一检查。给模型的下一步说明会提示用户在受支持的 Host 上运行该组合包。
|
||||
|
||||
组合包可声明 `dsh.bundle.configureOnEnable`,将现有 profile 条目 id 映射到配置顶层字段。显式启用先将变化的字段合并到用户 patch,再通过正常 HMR 重组应用,保留无关设置;条目缺失、存在歧义、整段 config 使用 `!!js` 表达式,或 home、启动覆盖层冲突时,写入前拒绝操作。启用设置需要将目标 config 写成普通映射,目标字段上的表达式会被指定值替换,无关字段的表达式保留。配置中的锚点继续供其他条目引用;含锚点的覆盖项保留,启用覆盖项追加在后。值已匹配时不改写。关闭保留这些设置。CLI 和手动修改组合包列表不应用这些启用设置。
|
||||
组合包的平台声明会在保存启用选择前检查。Host 不受支持时返回 `unsupported-platform`。组合包仍保留显示信息,并允许取消选择。包操作后的自动启用执行同一检查。给模型的下一步说明会提示用户在受支持的 Host 上运行该组合包。
|
||||
|
||||
已选择但无法加载的组合包仍会出现在 `listBundles` 中,并携带 `error`;`enabled` 表示保存的选择,不代表加载成功。插件页面显示错误并允许取消选择。损坏的组合包无法启用。管理组合包的文件变得不可读后仍受保护。
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ import { classifyInstallFailure } from './install-failure.ts'
|
||||
import { officialBundleInstallTarget } from './official-install-target.ts'
|
||||
import { dependencySpec, InvalidInstallSpecError, parseInstallSpec, type ParsedInstallSpec } from './install-spec.ts'
|
||||
import { attributeFailure, normalizeRegistry, NPMMIRROR_REGISTRY, registryPlan } from './registry.ts'
|
||||
import { flatten, prepareBundleConfiguration, writePluginEnabled } from './patch.ts'
|
||||
import { flatten, writePluginEnabled } from './patch.ts'
|
||||
import { incompatiblePlugin, ManagementFailure } from './failure.ts'
|
||||
import { approveBuilds, recordPendingBuilds } from './build-approval.ts'
|
||||
import { checkGithubConnection } from './github-connection.ts'
|
||||
@@ -817,7 +817,6 @@ export class PluginManager extends TypertRemoteService {
|
||||
private async selectBundle(name: string, enabled: boolean): Promise<void> {
|
||||
const manifest = readProfileManifest('dsh', this.profile.dir)
|
||||
const previous = bundleSelection(manifest)
|
||||
let enableConfiguration: string | undefined
|
||||
if (enabled || !previous.includes(name)) {
|
||||
const metadata = bundleManifest(name, this.profile.dir, this.profile.installAnchor)
|
||||
if (metadata === undefined) throw new ManagementFailure('not-bundle')
|
||||
@@ -826,18 +825,12 @@ export class PluginManager extends TypertRemoteService {
|
||||
const compatibility = evaluatePluginCompatibility(metadata, readProfileVersionExemptions(this.profile.dir))
|
||||
if (compatibility !== undefined && !compatibility.exempted) throw new ManagementFailure('incompatible-version', [incompatiblePlugin(compatibility)])
|
||||
this.bundleRows(name)
|
||||
if (metadata.dsh?.bundle?.configureOnEnable !== undefined) {
|
||||
enableConfiguration = await prepareBundleConfiguration(this.profile, metadata.dsh.bundle.configureOnEnable)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!enabled && previous.includes(name)) {
|
||||
if (this.protectsManager(name)) throw new ManagementFailure('management-required')
|
||||
}
|
||||
const bundles = enabled ? [...previous, ...previous.includes(name) ? [] : [name]] : previous.filter(item => item !== name)
|
||||
if (enableConfiguration !== undefined) {
|
||||
await writeFileAtomic(this.profile.patchPath, enableConfiguration, { mode: 0o600 })
|
||||
}
|
||||
if (JSON.stringify(previous) === JSON.stringify(bundles)) return
|
||||
manifest.dsh = { ...manifest.dsh, profile: { ...manifest.dsh?.profile, bundles } }
|
||||
await saveManifest(this.profile.dir, manifest)
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
/** Comment-preserving profile plugin enablement edits and the entry flattening they compare against. */
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { isMap, isNode, isScalar, isSeq, parseDocument, Scalar, visit } from 'yaml'
|
||||
import { isDeepStrictEqual } from 'node:util'
|
||||
import { z } from 'zod'
|
||||
import { isJsExpr, type EntryOptions } from '@deepseek-ai/cordis-plugin-loader'
|
||||
import { composeEntries, loadOptionalPatches, loadProfileDirectory, readBundlePatches, readProfilePatches, type ProfileContext } from '@deepseek-ai/dsh-app-boot'
|
||||
import { isMap, isNode, isScalar, isSeq, parseDocument } from 'yaml'
|
||||
import type { EntryOptions } from '@deepseek-ai/cordis-plugin-loader'
|
||||
import { composeEntries, loadOptionalPatches, readBundlePatches, type ProfileContext } from '@deepseek-ai/dsh-app-boot'
|
||||
import { writeFileAtomic } from '@deepseek-ai/dsh-atomic-write'
|
||||
|
||||
/** Flatten only the groups addressable by the profile's patch composer.
|
||||
@@ -15,88 +13,6 @@ export function flatten(rows: EntryOptions[]): EntryOptions[] {
|
||||
return rows.flatMap(row => [row, ...(row.group && Array.isArray(row.config) ? flatten(row.config as EntryOptions[]) : [])])
|
||||
}
|
||||
|
||||
/** Read the shared profile patch dialect while retaining comments and expression tags. */
|
||||
async function readPatchDocument(filename: string) {
|
||||
let text: string
|
||||
try {
|
||||
text = await readFile(filename, 'utf8')
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
|
||||
text = '[]\n'
|
||||
}
|
||||
const document = parseDocument(text, {
|
||||
customTags: [{ tag: 'tag:yaml.org,2002:js', resolve: (value: string) => value }],
|
||||
})
|
||||
const error = document.errors[0]
|
||||
if (error !== undefined) throw error
|
||||
if (!isSeq(document.contents)) throw new Error('Profile patch must be a YAML sequence')
|
||||
return { document, sequence: document.contents }
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare persistent settings for explicit bundle enablement.
|
||||
* @param profile Profile whose existing entries receive top-level field overrides.
|
||||
* @param input Untrusted bundle manifest declaration, keyed by profile entry id.
|
||||
* @returns Complete profile patch text, or undefined when the requested settings already match.
|
||||
* @throws Invalid declarations, missing or ambiguous entries, whole-config expressions, and higher-layer overrides.
|
||||
*/
|
||||
export async function prepareBundleConfiguration(
|
||||
profile: ProfileContext, input: unknown,
|
||||
): Promise<string | undefined> {
|
||||
const configuration = z.record(z.string().min(1), z.record(z.string(), z.json())).parse(input)
|
||||
const loaded = loadProfileDirectory('dsh', profile.dir, profile.installAnchor)
|
||||
const rows = flatten(composeEntries([...loaded.layers.map(layer => layer.patches), loaded.patches]))
|
||||
const patches = Object.entries(configuration).map(([id, fields]) => {
|
||||
const matches = rows.filter(row => row.id === id)
|
||||
const entry = matches[0]
|
||||
const current: unknown = entry?.config
|
||||
if (isJsExpr(current)) {
|
||||
throw new Error(`Bundle enablement cannot edit the whole-config expression for "${id}"; use a literal config mapping`)
|
||||
}
|
||||
if (matches.length !== 1 || entry === undefined || entry.group
|
||||
|| (current !== undefined && (current === null || typeof current !== 'object' || Array.isArray(current)))) {
|
||||
throw new Error(`Bundle enablement requires one configurable profile entry "${id}"`)
|
||||
}
|
||||
const config = { ...current, ...fields }
|
||||
return { id, name: entry.name, config }
|
||||
})
|
||||
const effective = flatten(composeEntries([readProfilePatches('dsh', profile, {
|
||||
...loaded, patches: [...loaded.patches, ...patches],
|
||||
})]))
|
||||
for (const [id, fields] of Object.entries(configuration)) {
|
||||
const config: unknown = effective.find(row => row.id === id)?.config
|
||||
if (config === null || typeof config !== 'object' || Array.isArray(config)
|
||||
|| Object.entries(fields).some(([key, value]) => !isDeepStrictEqual((config as Record<string, unknown>)[key], value))) {
|
||||
throw new Error(`Bundle enablement configuration for "${id}" is overridden by a home patch or command-line overlay`)
|
||||
}
|
||||
}
|
||||
if (patches.every(patch => isDeepStrictEqual(rows.find(row => row.id === patch.id)?.config, patch.config))) return undefined
|
||||
const { document, sequence } = await readPatchDocument(profile.patchPath)
|
||||
sequence.flow = false
|
||||
for (const { id, name, config } of patches) {
|
||||
const index = sequence.items.findLastIndex((item, index) => isMap(item)
|
||||
&& document.getIn([index, 'id']) === id && !item.has('insert')
|
||||
&& (!item.has('name') || document.getIn([index, 'name']) === name))
|
||||
const previous = sequence.items[index]
|
||||
const anchors: string[] = []
|
||||
if (isNode(previous)) visit(previous, { Node(_key, node) {
|
||||
if ('anchor' in node && node.anchor) {
|
||||
anchors.push(node.anchor)
|
||||
return visit.BREAK
|
||||
}
|
||||
} })
|
||||
if (index < 0 || anchors.length > 0) document.add(document.createNode({ id, name, config }))
|
||||
else document.setIn([index, 'config'], document.createNode(config))
|
||||
}
|
||||
visit(document, { Map(_key, node) {
|
||||
if (node.items.length !== 1 || typeof node.get('__jsExpr') !== 'string') return
|
||||
const expression = new Scalar(node.get('__jsExpr'))
|
||||
expression.tag = 'tag:yaml.org,2002:js'
|
||||
return expression
|
||||
} })
|
||||
return String(document)
|
||||
}
|
||||
|
||||
/** Replace the last matching override or append one after existing insertions. An override that holds only `id` and
|
||||
* `disabled`, without anchors, is removed, or not appended, when the bundle layers and the remaining profile patches
|
||||
* already give the entry the requested literal `disabled` value; home patches and launch overlays are not consulted.
|
||||
@@ -108,10 +24,22 @@ export async function prepareBundleConfiguration(
|
||||
*/
|
||||
export async function writePluginEnabled(profile: ProfileContext, id: string, name: string, enabled: boolean): Promise<boolean> {
|
||||
const filename = profile.patchPath
|
||||
const { document, sequence } = await readPatchDocument(filename)
|
||||
let text: string
|
||||
try {
|
||||
text = await readFile(filename, 'utf8')
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
|
||||
text = '[]\n'
|
||||
}
|
||||
const document = parseDocument(text, {
|
||||
customTags: [{ tag: 'tag:yaml.org,2002:js', resolve: (value: string) => value }],
|
||||
})
|
||||
const error = document.errors[0]
|
||||
if (error !== undefined) throw error
|
||||
if (!isSeq(document.contents)) throw new Error('Profile patch must be a YAML sequence')
|
||||
const patches = loadOptionalPatches('dsh', filename) ?? []
|
||||
const bundlePatches = readBundlePatches('dsh', profile)
|
||||
const items = sequence.items
|
||||
const items = document.contents.items
|
||||
const target = items.findLast((item, index) => {
|
||||
if (!isMap(item) || document.getIn([index, 'id']) !== id || item.has('insert')) return false
|
||||
const expectedName = document.getIn([index, 'name'])
|
||||
@@ -134,7 +62,7 @@ export async function writePluginEnabled(profile: ProfileContext, id: string, na
|
||||
const comment = target.commentBefore
|
||||
const next = items[index]
|
||||
if (comment && isNode(next)) next.commentBefore = next.commentBefore ? `${comment}\n${next.commentBefore}` : comment
|
||||
else if (comment && items.length === 0) sequence.commentBefore = comment
|
||||
else if (comment && items.length === 0) document.contents.commentBefore = comment
|
||||
else if (comment) document.comment = document.comment ? `${comment}\n${document.comment}` : comment
|
||||
} else if (document.getIn([index, 'disabled']) === !enabled) {
|
||||
return false
|
||||
|
||||
@@ -89,41 +89,11 @@ async function fixture(reload: 'live' | 'startup' = 'live', overlay = false, pre
|
||||
return { ctx, dir, manager: ctx.pluginManager, bundle, profile, stopHmr, overlays, connection }
|
||||
}
|
||||
|
||||
it.each(['live', 'startup'] as const)('persists enablement settings, applies them with %s reload and retains them after disablement', async (reload) => {
|
||||
const { manager, dir, ctx } = await fixture(reload, false, undefined, {}, undefined, (dir) => {
|
||||
const core = join(dir, 'node_modules/core/cordis.patch.yml')
|
||||
const patches = JSON.parse(readFileSync(core, 'utf8')) as Array<{ insert: object[] }>
|
||||
patches[0]!.insert.push({ id: 'runtime', name: join(dir, 'node_modules/extra/plugin.mjs'),
|
||||
config: { service: 'activationProbe', executionMode: 'per-call', timeout: 42 } })
|
||||
writeFileSync(core, JSON.stringify(patches))
|
||||
const path = join(dir, 'node_modules/extra/package.json')
|
||||
const meta = JSON.parse(readFileSync(path, 'utf8')) as { dsh: { bundle: object } }
|
||||
meta.dsh.bundle = { ...meta.dsh.bundle, configureOnEnable: { runtime: { executionMode: 'persistent-in-memory' } } }
|
||||
writeFileSync(path, JSON.stringify(meta))
|
||||
})
|
||||
await manager.setBundleEnabled('extra', false)
|
||||
const runtime = [...ctx.loader.entries()].find(entry => entry.options.id === 'runtime')!
|
||||
expect(await manager.setBundleEnabled('extra', true)).toMatchObject({
|
||||
application: reload === 'live' ? 'applied' : 'restart-required', changed: true,
|
||||
})
|
||||
expect(runtime.options.config).toEqual({ service: 'activationProbe', executionMode: reload === 'live' ? 'persistent-in-memory' : 'per-call', timeout: 42 })
|
||||
const appliedFiber = runtime.fiber
|
||||
const saved = readFileSync(join(dir, 'cordis.patch.yml'), 'utf8')
|
||||
expect(parse(saved)).toContainEqual({ id: 'runtime', name: pathToFileURL(join(dir, 'node_modules/extra/plugin.mjs')).href,
|
||||
config: { service: 'activationProbe', executionMode: 'persistent-in-memory', timeout: 42 } })
|
||||
await manager.setBundleEnabled('extra', false)
|
||||
expect(readFileSync(join(dir, 'cordis.patch.yml'), 'utf8')).toBe(saved)
|
||||
expect(runtime.fiber === appliedFiber).toBe(true)
|
||||
await manager.setBundleEnabled('extra', true)
|
||||
expect(readFileSync(join(dir, 'cordis.patch.yml'), 'utf8')).toBe(saved)
|
||||
expect(runtime.fiber === appliedFiber).toBe(true)
|
||||
})
|
||||
|
||||
it.each(['live', 'startup'] as const)('refuses an unsupported bundle before saving or applying it with %s reload', async (reload) => {
|
||||
const { manager, dir, ctx } = await fixture(reload, false, undefined, {}, undefined, (dir) => {
|
||||
const path = join(dir, 'node_modules/extra/package.json')
|
||||
writeFileSync(path, JSON.stringify({ name: 'extra', version: '1.0.0', description: 'Platform-limited bundle',
|
||||
dsh: { bundle: { patch: './cordis.patch.yml', experimental: true, configureOnEnable: { absent: { mode: 'new' } }, platforms: [process.platform === 'darwin' ? 'win32' : 'darwin'] } } }))
|
||||
dsh: { bundle: { patch: './cordis.patch.yml', experimental: true, platforms: [process.platform === 'darwin' ? 'win32' : 'darwin'] } } }))
|
||||
const manifest = readProfileManifest('test', dir)
|
||||
manifest.dsh = { profile: { bundles: ['core'] } }
|
||||
writeFileSync(join(dir, 'package.json'), JSON.stringify(manifest))
|
||||
|
||||
@@ -5,102 +5,10 @@ import { tmpdir } from 'node:os'
|
||||
import { expect, it, onTestFinished } from 'vitest'
|
||||
import { applyEntryPatches } from '@deepseek-ai/cordis-plugin-include'
|
||||
import { initProfile, loadOptionalPatches, type ProfileContext } from '@deepseek-ai/dsh-app-boot'
|
||||
import { prepareBundleConfiguration, writePluginEnabled } from '../src/patch.ts'
|
||||
import { writePluginEnabled } from '../src/patch.ts'
|
||||
|
||||
const TOOL = { id: 'tool', name: 'package' }
|
||||
|
||||
it('prepares durable activation settings while preserving prior fields, expressions and comments', async () => {
|
||||
const profile = await fixture('# personal\n- id: runtime\n config:\n timeout: 42\n command: !!js process.execPath\n- id: locale\n config: { preference: zh }\n', [
|
||||
{ id: 'runtime', name: 'runtime-package', config: { executionMode: 'per-call' } },
|
||||
{ id: 'presets', name: 'preset-package', config: { default: 'standard' } },
|
||||
{ id: 'locale', name: 'locale-package' },
|
||||
])
|
||||
const before = await readFile(profile.patchPath, 'utf8')
|
||||
const input = { runtime: { executionMode: 'persistent-in-memory' }, presets: { selectedDefault: 'ptc' } }
|
||||
const prepared = await prepareBundleConfiguration(profile, input)
|
||||
expect(await readFile(profile.patchPath, 'utf8')).toBe(before)
|
||||
expect(prepared).toContain('# personal')
|
||||
expect(prepared).toContain('!!js process.execPath')
|
||||
if (prepared === undefined) throw new Error('Expected changed enablement settings')
|
||||
await writeFile(profile.patchPath, prepared)
|
||||
expect(loadOptionalPatches('test', profile.patchPath)).toEqual([
|
||||
{ id: 'runtime', config: { timeout: 42, command: { __jsExpr: 'process.execPath' }, executionMode: 'persistent-in-memory' } },
|
||||
{ id: 'locale', config: { preference: 'zh' } },
|
||||
{ id: 'presets', name: 'preset-package', config: { default: 'standard', selectedDefault: 'ptc' } },
|
||||
])
|
||||
expect(await prepareBundleConfiguration(profile, input)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('accepts unrelated launch overrides when the requested field has the intended effective value', async () => {
|
||||
const profile = await fixture('[]\n', [{ id: 'runtime', name: 'runtime-package', config: { timeout: 42 } }])
|
||||
const launch = { ...profile, overlays: [{ id: 'runtime', config: { executionMode: 'persistent-in-memory', timeout: 10 } }] }
|
||||
const prepared = await prepareBundleConfiguration(launch, { runtime: { executionMode: 'persistent-in-memory' } })
|
||||
if (prepared === undefined) throw new Error('Expected saved runtime configuration')
|
||||
await writeFile(profile.patchPath, prepared)
|
||||
expect(loadOptionalPatches('test', profile.patchPath)).toEqual([
|
||||
{ id: 'runtime', name: 'runtime-package', config: { timeout: 42, executionMode: 'persistent-in-memory' } },
|
||||
])
|
||||
})
|
||||
|
||||
it('refuses a whole-config expression without evaluating it or replacing the user patch', async () => {
|
||||
const before = '- id: runtime\n config: !!js "(() => { throw new Error(\'must not evaluate\') })()"\n'
|
||||
const profile = await fixture(before, [{ id: 'runtime', name: 'runtime-package' }])
|
||||
await expect(prepareBundleConfiguration(profile, { runtime: { executionMode: 'persistent-in-memory' } }))
|
||||
.rejects.toThrow('Bundle enablement cannot edit the whole-config expression for "runtime"; use a literal config mapping')
|
||||
expect(await readFile(profile.patchPath, 'utf8')).toBe(before)
|
||||
})
|
||||
|
||||
it('replaces a target field expression while preserving unrelated expressions', async () => {
|
||||
const profile = await fixture('- id: runtime\n config:\n executionMode: !!js process.env.MODE\n timeout: !!js process.env.TIMEOUT\n', [
|
||||
{ id: 'runtime', name: 'runtime-package' },
|
||||
])
|
||||
const prepared = await prepareBundleConfiguration(profile, { runtime: { executionMode: 'persistent-in-memory' } })
|
||||
expect(prepared).toContain('executionMode: persistent-in-memory')
|
||||
expect(prepared).toContain('timeout: !!js process.env.TIMEOUT')
|
||||
expect(prepared).not.toContain('process.env.MODE')
|
||||
})
|
||||
|
||||
it('retains configuration anchors referenced by other entries', async () => {
|
||||
const profile = await fixture('- id: runtime\n config: &runtimeConfig\n timeout: &timeout 30000\n- id: other\n config:\n timeout: *timeout\n inherited: *runtimeConfig\n', [
|
||||
{ id: 'runtime', name: 'runtime-package' }, { id: 'other', name: 'other-package' },
|
||||
])
|
||||
const prepared = await prepareBundleConfiguration(profile, { runtime: { executionMode: 'persistent-in-memory' } })
|
||||
if (prepared === undefined) throw new Error('Expected activation settings')
|
||||
await writeFile(profile.patchPath, prepared)
|
||||
expect(loadOptionalPatches('test', profile.patchPath)).toEqual([
|
||||
{ id: 'runtime', config: { timeout: 30000 } },
|
||||
{ id: 'other', config: { timeout: 30000, inherited: { timeout: 30000 } } },
|
||||
{ id: 'runtime', name: 'runtime-package', config: { timeout: 30000, executionMode: 'persistent-in-memory' } },
|
||||
])
|
||||
expect(await prepareBundleConfiguration(profile, { runtime: { executionMode: 'persistent-in-memory' } })).toBeUndefined()
|
||||
})
|
||||
|
||||
it.each(['runtime-package', 'retired-runtime'])('keeps name-qualified activation overrides for %s', async (name) => {
|
||||
const profile = await fixture(`- id: runtime\n name: ${name}\n config: { timeout: 42 }\n`, [
|
||||
{ id: 'runtime', name: 'runtime-package', config: { timeout: 10 } },
|
||||
])
|
||||
const prepared = await prepareBundleConfiguration(profile, { runtime: { executionMode: 'persistent-in-memory' } })
|
||||
if (prepared === undefined) throw new Error('Expected activation settings')
|
||||
await writeFile(profile.patchPath, prepared)
|
||||
const patch = { id: 'runtime', name: 'runtime-package', config: {
|
||||
timeout: name === 'runtime-package' ? 42 : 10, executionMode: 'persistent-in-memory',
|
||||
} }
|
||||
expect(loadOptionalPatches('test', profile.patchPath)).toEqual(name === 'runtime-package' ? [patch] : [
|
||||
{ id: 'runtime', name, config: { timeout: 42 } }, patch,
|
||||
])
|
||||
})
|
||||
|
||||
it('refuses invalid activation settings, absent entries and higher-layer overrides before writing', async () => {
|
||||
const profile = await fixture('[]\n', [{ id: 'runtime', name: 'runtime-package', config: {} }])
|
||||
for (const input of [[], { runtime: false }, { absent: { mode: 'new' } }]) {
|
||||
await expect(prepareBundleConfiguration(profile, input)).rejects.toThrow()
|
||||
expect(await readFile(profile.patchPath, 'utf8')).toBe('[]\n')
|
||||
}
|
||||
await writeFile(join(profile.home, 'cordis.patch.yml'), '- id: runtime\n config: { executionMode: per-call }\n')
|
||||
await expect(prepareBundleConfiguration(profile, { runtime: { executionMode: 'persistent-in-memory' } })).rejects.toThrow('overridden')
|
||||
expect(await readFile(profile.patchPath, 'utf8')).toBe('[]\n')
|
||||
})
|
||||
|
||||
/** Create a profile whose patch file holds `text` (none when omitted) and whose one selected bundle inserts `rows`. */
|
||||
async function fixture(text?: string, rows: object[] = []): Promise<ProfileContext> {
|
||||
const home = await mkdtemp(join(tmpdir(), 'manager-patch-'))
|
||||
|
||||
@@ -15,11 +15,11 @@
|
||||
en: b563e4ec4727d0de
|
||||
zh: d0228da978238e05
|
||||
/deepseek-ai-dsh-cua-bundle/use-this-package:
|
||||
en: c493c75904aea7c4
|
||||
zh: 40e8e7b162270a48
|
||||
en: dfbc7b6cef715c99
|
||||
zh: 973a18c78cfa7dd6
|
||||
/deepseek-ai-dsh-cua-bundle/understand-the-implementation:
|
||||
en: 504f9d26fa575e15
|
||||
zh: 2b8b7af4767b52b0
|
||||
en: 8cf76d5bc5ab785b
|
||||
zh: 18d24f49fe8fa14f
|
||||
/deepseek-ai-dsh-cua-bundle/further-exploration:
|
||||
en: 9f15d56b5b16e1ca
|
||||
zh: 75eb08e8828c8877
|
||||
|
||||
@@ -33,13 +33,13 @@ pnpm dsh web
|
||||
|
||||
For the source Desktop launcher and its separate development profile, follow [Desktop development](../../../apps/desktop/README.md). Enable Computer Use in each profile that needs it.
|
||||
|
||||
The switch saves bundle selection in the profile’s `package.json`. Enabling preserves PTC execution mode. The detail page’s **Use persistent mode** action saves `ptc-runtime.executionMode: persistent-in-memory` in the user’s `cordis.patch.yml`. Already-persistent runtimes show a disabled button. Computer Use is designed for PTC and installed only in PTC sessions. When system permissions need attention, activation guidance opens the plugin details. The detail page offers **Set to PTC** for the new-session default; existing sessions keep their preset. HMR maintains these settings and the provider’s session tool registrations. Native components prepare on activation and source startup; subsequent `pnpm run build` checks the selected Web profile’s incremental native build. First preparation requires local compiler tools, and failure preserves diagnostics without publishing a new executable.
|
||||
The switch saves bundle selection in the profile’s `package.json`. Enabling preserves Coding Tools preferences and PTC execution mode. The detail page’s **Use persistent mode** action saves `ptc-runtime.executionMode: persistent-in-memory` in the user’s `cordis.patch.yml`. Already-persistent runtimes show a disabled button. Computer Use is designed for PTC and installed only in PTC sessions. When system permissions need attention, activation guidance opens the plugin details. The detail page offers **Set to PTC** for the new-session default; existing sessions keep their preset. HMR maintains these settings and the provider’s session tool registrations. Native components prepare on activation and source startup; subsequent `pnpm run build` checks the selected Web profile’s incremental native build. First preparation requires local compiler tools, and failure preserves diagnostics without publishing a new executable.
|
||||
|
||||
Disabling unloads the provider and withdraws CUA tools from its Agent scopes. It retains the selected default, runtime configuration and retained PTC variables. Enabling again does not recreate the runtime. Explicitly changing from per-call to persistent execution replaces the shared runtime through HMR and can interrupt an active PTC call.
|
||||
|
||||
Computer Use is available only on a macOS DSH Host. Other Hosts refuse enablement before saving settings. An already selected bundle is skipped at startup with a diagnostic and remains available for deselection; saved user settings remain. A browser on another operating system can connect to a supported Mac Host.
|
||||
|
||||
If preparation fails, the saved selection and PTC settings remain, while CUA operations stay unavailable. Refresh or reopen the plugin page to read the component diagnostic, correct its cause, then restart DSH.
|
||||
If preparation fails, the saved selection remains, while CUA operations stay unavailable. Refresh or reopen the plugin page to read the component diagnostic, correct its cause, then restart DSH.
|
||||
|
||||
Explicit activation runs a read-only permission check, which may start a helper without opening a system dialog. Open the [permission page](../client-ui-cua/README.md) for system requests, settings navigation and automatic refresh. Tasks ask separately before accessing an application. Saved application decisions use the existing profile configuration; session decisions remain in memory across turns and connections until DSH restarts or Computer Use is disabled. Revoke access on the same page.
|
||||
|
||||
@@ -51,7 +51,7 @@ Explicit activation runs a read-only permission check, which may start a helper
|
||||
<details>
|
||||
<summary>Maintainer details</summary>
|
||||
|
||||
The bundle patch mounts the shared native provider and permission UI with `agentPreset: ptc`. The provider registers tools through existing Agent scopes, so other presets receive no CUA tool or guidance. Preset documents remain unchanged. `configureOnEnable` persists `ui-settings.enabled: true` through [Plugin Manager](../../boot/plugin-manager/README.md). The detail-page action uses the existing settings form to save `agent-preset-registry.selectedDefault: ptc` only when clicked, preserving the deployment `default`. The provider defaults to PTC scope even when a saved grants override omits that field. [CUA](../cua/README.md) owns registration and disposal.
|
||||
The bundle patch mounts the shared native provider and permission UI with `agentPreset: ptc`. The provider registers tools through existing Agent scopes, so other presets receive no CUA tool or guidance. Preset documents remain unchanged. The detail-page action uses the existing settings form to save `agent-preset-registry.selectedDefault: ptc` only when clicked, preserving the deployment `default`. The provider defaults to PTC scope even when a saved grants override omits that field. [CUA](../cua/README.md) owns registration and disposal.
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ pnpm dsh web
|
||||
|
||||
源码 Desktop 的启动方式和独立开发配置档见[桌面开发说明](../../../apps/desktop/README.zh.md)。需要使用 Computer Use 的配置档分别开启该功能。
|
||||
|
||||
开关将组合包选择保存到 profile 的 `package.json`。开启时保留 PTC 执行方式。详情页的“设为持久化”按钮将 `ptc-runtime.executionMode: persistent-in-memory` 保存到用户的 `cordis.patch.yml`;已配置为持久化时禁用按钮。Computer Use 针对 PTC 模式设计,仅安装在 PTC 会话中。系统权限尚未就绪时,启用引导会进入插件详情页。详情页提供“设为 PTC”按钮来修改新会话默认模式;已有会话保留原 preset。HMR 维护这些设置和提供方的会话工具注册。本机组件在启用及源码启动时准备,后续 `pnpm run build` 检查已选中 Web profile 的本机增量构建。首次准备需要本机编译工具,失败保留诊断且不发布新的可执行文件。
|
||||
开关将组合包选择保存到 profile 的 `package.json`。开启时保留 Coding Tools 设置和 PTC 执行方式。详情页的“设为持久化”按钮将 `ptc-runtime.executionMode: persistent-in-memory` 保存到用户的 `cordis.patch.yml`;已配置为持久化时禁用按钮。Computer Use 针对 PTC 模式设计,仅安装在 PTC 会话中。系统权限尚未就绪时,启用引导会进入插件详情页。详情页提供“设为 PTC”按钮来修改新会话默认模式;已有会话保留原 preset。HMR 维护这些设置和提供方的会话工具注册。本机组件在启用及源码启动时准备,后续 `pnpm run build` 检查已选中 Web profile 的本机增量构建。首次准备需要本机编译工具,失败保留诊断且不发布新的可执行文件。
|
||||
|
||||
关闭会卸载提供方,并从其 Agent 作用域中撤回 CUA 工具。用户选择的默认值、执行器配置和变量均保留。再次开启不会重建执行器。用户显式从 per-call 切换为持久化执行时,HMR 会重载共享执行器,可能中断正在执行的 PTC 调用。
|
||||
|
||||
@@ -51,7 +51,7 @@ Computer Use 仅支持 macOS DSH Host。其他 Host 会在保存设置前拒绝
|
||||
<details>
|
||||
<summary>维护者说明</summary>
|
||||
|
||||
组合包 patch 通过 `agentPreset: ptc` 挂载共享原生提供方和权限界面。提供方在现有 Agent 作用域中注册工具,其他 preset 不获得 CUA 工具或指引,preset 文档保持不变。`configureOnEnable` 通过[插件管理器](../../boot/plugin-manager/README.zh.md)保存 `ui-settings.enabled: true`。详情页按钮复用现有设置表单,仅在用户点击后保存 `agent-preset-registry.selectedDefault: ptc`,保留部署级 `default`。即使旧授权配置没有声明作用域字段,提供方也默认只为 PTC 会话注册工具。[CUA](../cua/README.zh.md)负责注册和销毁。
|
||||
组合包 patch 通过 `agentPreset: ptc` 挂载共享原生提供方和权限界面。提供方在现有 Agent 作用域中注册工具,其他 preset 不获得 CUA 工具或指引,preset 文档保持不变。详情页按钮复用现有设置表单,仅在用户点击后保存 `agent-preset-registry.selectedDefault: ptc`,保留部署级 `default`。即使旧授权配置没有声明作用域字段,提供方也默认只为 PTC 会话注册工具。[CUA](../cua/README.zh.md)负责注册和销毁。
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
@@ -36,11 +36,6 @@
|
||||
"bundle": {
|
||||
"patch": "./cordis.patch.yml",
|
||||
"experimental": true,
|
||||
"configureOnEnable": {
|
||||
"ui-settings": {
|
||||
"enabled": true
|
||||
}
|
||||
},
|
||||
"platforms": [
|
||||
"darwin"
|
||||
]
|
||||
@@ -49,7 +44,6 @@
|
||||
"dependencies": {
|
||||
"@deepseek-ai/dsh-experimental-cua-macos": "workspace:*",
|
||||
"@deepseek-ai/dsh-computer-use": "workspace:*",
|
||||
"@deepseek-ai/dsh-ptc-runtime-node": "workspace:*",
|
||||
"@deepseek-ai/dsh-client-ui-cua": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
en: d152484eb41ac6b4
|
||||
zh: 1db86285abf6c499
|
||||
/deepseek-ai-dsh-package-manifest/use-this-package:
|
||||
en: c4bbd42bbe88fd5b
|
||||
zh: f948c43f2eb44b18
|
||||
en: ddfb5b63be499457
|
||||
zh: 0ae1d9e727ae6a83
|
||||
/deepseek-ai-dsh-package-manifest/understand-the-implementation:
|
||||
en: 41ec449dee927f04
|
||||
zh: ec1b583bd9025d84
|
||||
|
||||
@@ -52,7 +52,6 @@ The following metadata fields are optional. Omitting them leaves the format vers
|
||||
| `dsh.manifestVersion` | Manifest format identifier; the declared format is `1`, independent of the npm package version and Session format version. |
|
||||
| `dsh.bundle.platforms` | Optional nonempty list of Node Host platform names, such as `["darwin"]`; omission allows every operating system. The launcher and plugin manager enforce it before composing or selecting the bundle. |
|
||||
| `dsh.bundle.experimental` | Explicit experimental feature label in plugin management; independent of the package name and enabled state. |
|
||||
| `dsh.bundle.configureOnEnable` | Entry-id map of JSON config fields that Plugin Manager saves on explicit enablement. Settings use ordinary profile reconciliation and remain after disablement; see [Plugin Manager](../../boot/plugin-manager/README.md). |
|
||||
| `engines.dsh` | Author-declared compatible DSH versions as a SemVer range, including exact prerelease versions. This field sits beside `engines.node` and `engines.npm`; an engines object may omit `dsh`. |
|
||||
|
||||
`LocalizedText` carries literal text or a language map with a required English fallback. `PluginLocalizedMeta` carries optional display title, description, an image data URL resolved from a package root's `package.json.icon` or an exported `<specifier>/icon`, and metadata diagnostics for installed plugins. [App boot](../../boot/app-boot/README.md) reads these values; this package only supplies their types.
|
||||
|
||||
@@ -52,7 +52,6 @@ const manifest: DshPackageManifest = {
|
||||
| `dsh.manifestVersion` | manifest(元数据清单)格式标识;声明的格式为 `1`,独立于 npm 包版本和 Session 格式版本。 |
|
||||
| `dsh.bundle.platforms` | 可选的非空 Node Host 平台名称列表,例如 `["darwin"]`;省略时允许所有操作系统。启动器和插件管理器在组合或选择该组合包前检查此字段。 |
|
||||
| `dsh.bundle.experimental` | 在插件页显示实验性标签,不改变包名或启用状态。 |
|
||||
| `dsh.bundle.configureOnEnable` | 以条目 id 为键的 JSON 配置字段映射,由插件管理器在显式启用时保存。通过正常 profile 重组生效,关闭保留;见[插件管理器](../../boot/plugin-manager/README.zh.md)。 |
|
||||
| `engines.dsh` | 作者声明的兼容 DSH 版本,使用 SemVer 范围,也可填写精确的预发布版本。此字段与 `engines.node`、`engines.npm` 并列;engines 对象可省略 `dsh`。 |
|
||||
|
||||
`LocalizedText` 携带字面文本或带必需英文回退值的语言映射。`PluginLocalizedMeta` 携带已安装插件的可选展示标题、描述、从包根 `package.json.icon` 或导出的 `<标识>/icon` 解析的图片 data URL,以及元信息诊断。[App boot](../../boot/app-boot/README.zh.md) 读取这些值;本包只提供其类型。
|
||||
|
||||
@@ -67,10 +67,6 @@ export interface DshEnginesManifest {
|
||||
|
||||
/** The configuration layer exported by a bundle package. */
|
||||
export interface DshBundleManifest {
|
||||
/** Top-level fields saved to existing profile entries on explicit enablement.
|
||||
* Settings remain after disablement.
|
||||
*/
|
||||
configureOnEnable?: Record<string, Record<string, unknown>>
|
||||
/** Supported Node Host platform names; omission allows every operating system. */
|
||||
platforms?: string[]
|
||||
/** Mark this optional feature as experimental in plugin management. */
|
||||
|
||||
Generated
-3
@@ -8679,9 +8679,6 @@ importers:
|
||||
'@deepseek-ai/dsh-experimental-cua-macos':
|
||||
specifier: workspace:*
|
||||
version: link:../cua-macos
|
||||
'@deepseek-ai/dsh-ptc-runtime-node':
|
||||
specifier: workspace:*
|
||||
version: link:../../ptc-runtime/ptc-runtime-node
|
||||
devDependencies:
|
||||
'@deepseek-ai/cordis':
|
||||
specifier: workspace:~
|
||||
|
||||
Reference in new issue
Block a user