فهرست منبع

Merge branch 'feat/plugin-mgmt-3-settings' into feat/plugin-mgmt-4-web

Yichen Jiang 1 ماه پیش
والد
کامیت
864d659f01

+ 2 - 2
docs/config-catalog.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/config-catalog.md
-config-catalog.md: a67b2f3a2c9cd61b7eeefa2c8721b5a5d7912d4e
-config-catalog.zh.md: 2249ad2a24e5e9a99573ed0de750d4299f240289
+config-catalog.md: 4a6e55d6a21376021cf8bec86f95c3293136c7b8
+config-catalog.zh.md: 557349db15c05ea868cf55a9a71b734e97037166

+ 1 - 1
docs/config-catalog.md

@@ -931,7 +931,7 @@ export interface Config {
 }
 ```
 
-Source: [`packages/host/plugin-manager/src/index.ts:98`](../packages/host/plugin-manager/src/index.ts)
+Source: [`packages/host/plugin-manager/src/index.ts:99`](../packages/host/plugin-manager/src/index.ts)
 
 <a id="deepseek-aidsh-host-webserver"></a>
 

+ 1 - 1
docs/config-catalog.zh.md

@@ -933,7 +933,7 @@ export interface Config {
 }
 ```
 
-来源:[`packages/host/plugin-manager/src/index.ts:98`](../packages/host/plugin-manager/src/index.ts)
+来源:[`packages/host/plugin-manager/src/index.ts:99`](../packages/host/plugin-manager/src/index.ts)
 
 <a id="deepseek-aidsh-host-webserver"></a>
 

+ 1 - 0
packages/preset/agent-presets/src/discovery.ts

@@ -291,6 +291,7 @@ async function overlayProblem(path: string, harnessBase: string): Promise<string
     return `the user patch layer ${OVERLAY_FILE} cannot be applied: ${full.replace(/\n[\s\S]*$/, '')}`
   }
   // The caller statted this file moments ago; gone in between reads as nothing to apply.
+  /* v8 ignore next -- a deletion between the stat and this read cannot be provoked deterministically */
   if (patches === undefined) return undefined
   const presetBase = new URL('.', pathToFileURL(path)).href
   for (const [index, patch] of patches.entries()) {

+ 3 - 0
packages/preset/agent-presets/src/index.ts

@@ -910,6 +910,7 @@ async function overlayDigest(overlayPath: string | undefined): Promise<string> {
     text = await readFile(overlayPath, 'utf8')
   } catch {
     // A layer deleted since discovery is a layer that applies nothing.
+    /* v8 ignore next -- a deletion between the layer read and this digest cannot be provoked deterministically */
     return ''
   }
   return createHash('sha1').update(text).digest('hex')
@@ -933,6 +934,8 @@ function stampKey(stamp: CompositionStamp): string {
 async function readOverlay(preset: AgentPreset): Promise<PatchOptions[]> {
   if (preset.overlayPath === undefined) return []
   try {
+    // The inventory re-discovers before it reads; a layer deleted between the two is a race no test can provoke.
+    /* v8 ignore next */
     return await readPatchListFile('agent-presets', preset.overlayPath, 'user patch layer') ?? []
   } catch {
     // Discovery already reported an unparsable layer as the preset's health;

+ 29 - 2
packages/preset/agent-presets/tests/overlay.spec.ts

@@ -7,7 +7,7 @@
  */
 
 import { existsSync } from 'node:fs'
-import { mkdir, mkdtemp, readFile, writeFile } from 'node:fs/promises'
+import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
 import { tmpdir } from 'node:os'
 import { dirname, join } from 'node:path'
 import { fileURLToPath, pathToFileURL } from 'node:url'
@@ -26,6 +26,7 @@ import { afterEach, describe, expect, it } from 'vitest'
 import AgentPresets, {
   COMPOSITION_FILE, discoverPresets, livePresetMounts, overlayFacts, OVERLAY_FILE, scanRoot,
 } from '@deepseek-ai/dsh-agent-presets'
+import { fileComposition } from '../src/composition-inventory.ts'
 import type { Config } from '@deepseek-ai/dsh-agent-presets'
 
 const FIXTURES = join(dirname(fileURLToPath(import.meta.url)), 'fixtures')
@@ -246,6 +247,19 @@ describe('inventory', () => {
     ])
   })
 
+  it('answers from the composition alone when the layer disappears before the inventory reads it', async () => {
+    const root = await userRoot({ standard: { [OVERLAY_FILE]: overlayInserting('gamma') } })
+    const ctx = await harness(rosterOver(root))
+    await rm(join(root, 'standard', OVERLAY_FILE))
+
+    const fromFile = (await ctx.agentPresets.compositionInventory()).find(composition => composition.id === 'standard')
+
+    expect(fromFile?.rows.map(row => [row.entryId, row.source])).toEqual([
+      ['alpha', 'preset'],
+      ['alpha-extra', 'preset'],
+    ])
+  })
+
   it('answers from the composition alone when the layer stopped reading', async () => {
     const root = await userRoot({ standard: { [OVERLAY_FILE]: overlayInserting('gamma') } })
     const ctx = await harness(rosterOver(root))
@@ -267,7 +281,20 @@ describe('inventory', () => {
       { id: 'b', disabled: { __jsExpr: 'true' } as unknown as boolean },
       { id: 'c', config: {} },
       { insert: [{ id: 'd', name: 'x' }, { id: 'g', name: 'cordis:group', group: true, config: [{ id: 'e', name: 'y' }] }] },
-    ])).toEqual({ inserted: new Set(['d', 'g', 'e']), disabled: new Set(['a']) })
+      // A group inserted without members contributes its own id alone; an anonymous row contributes nothing.
+      { insert: [{ id: 'h', name: 'cordis:group', group: true }, { name: 'anonymous' } as never] },
+    ])).toEqual({ inserted: new Set(['d', 'g', 'e', 'h']), disabled: new Set(['a']) })
+  })
+
+  it('reads a composition file under a layer that targets a row it does not have', async () => {
+    // A patch matching no row is the Loader's warning at mount time; the
+    // inventory applies the rest and stays silent about it.
+    const dir = await mkdtemp(join(tmpdir(), 'dsh-preset-inventory-'))
+    const path = join(dir, COMPOSITION_FILE)
+    await writeFile(path, '- id: alpha\n  name: ../../plugins/contribute.js\n')
+    expect(await fileComposition(path, () => false, [{ id: 'missing', disabled: true }])).toEqual({
+      rows: [{ entryId: 'alpha', moduleName: '../../plugins/contribute.js', enabled: true, source: 'preset' }],
+    })
   })
 })
 

+ 2 - 2
packages/util/patch-file/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/util/patch-file/README.md
-README.md: b2e25491c6633e0e5f190112e7b13e2fa61040f7
-README.zh.md: 427bf9217828a8fc534b27cb620e3da7d7bbee58
+README.md: d8b13e3be77d28693c30b8ef40f6c57efeacb57a
+README.zh.md: a690dad3a7b79d503347cefb271d2f6cfe71c153

+ 3 - 0
packages/util/patch-file/README.md

@@ -30,6 +30,9 @@ Read a patch layer with `parsePatchList` (text in hand) or `readPatchListFile` (
 Write through `mutatePatchFile`. The callback receives a `PatchDocument` and edits it by row id, the way the Loader addresses rows:
 
 ```ts
+import { mutatePatchFile } from '@deepseek-ai/dsh-patch-file'
+
+const file = '/home/me/.dsh/profiles/web/cordis.patch.yml'
 await mutatePatchFile(file, (document) => {
   document.setRowField('tool-web', 'disabled', true)      // the id-targeted patch is created when absent
   document.deleteRowField('tool-web', 'config')           // a patch reduced to its id is removed whole

+ 3 - 0
packages/util/patch-file/README.zh.md

@@ -30,6 +30,9 @@ kind: "package-reference"
 通过 `mutatePatchFile` 写入。回调拿到一个 `PatchDocument`,按 Loader 寻址行的方式以行 id 编辑:
 
 ```ts
+import { mutatePatchFile } from '@deepseek-ai/dsh-patch-file'
+
+const file = '/home/me/.dsh/profiles/web/cordis.patch.yml'
 await mutatePatchFile(file, (document) => {
   document.setRowField('tool-web', 'disabled', true)      // the id-targeted patch is created when absent
   document.deleteRowField('tool-web', 'config')           // a patch reduced to its id is removed whole