fix(llm): omit unversioned private plugins from DeepSeek inventory

This commit is contained in:
turtle1999 committed 2026-09-30 18:47:07 +08:00
1 parent c006c6c074
commit 28107071fe
6 files changed
+125 -5

No files matched your search

@@ -18,8 +18,8 @@
en: dea20f16d15856af
zh: 6548dc9da63375e5
/deepseek-ai-dsh-plugin-package-inventory-deepseek/collection:
en: f66c9c9d4c3f79c0
zh: ef792b9986c12413
en: 694eb443f46a14f0
zh: 76e1f846335a96e5
/deepseek-ai-dsh-plugin-package-inventory-deepseek/model-experience:
en: 215e7ba838619b7b
zh: a311da3843709f90
@@ -37,7 +37,7 @@ Every request re-reads active non-group entries from the host Loader tree. When
At activation, the plugin samples the optional `ctx.pluginPackages` service. Shipped app boot mounts that service before profile Loader entries, so bare package and package-subpath identities use the same authoritative runtime resolution as their imports, including its misses, without requiring a `./package.json` export. If the service is absent, the plugin retains native Node package-search lookup for its lifetime and does not switch an existing inventory when a resolver mounts later. A low-level embedder that wants runtime resolution identities must therefore mount `PluginPackages` before both the Loader entries and this plugin.
Each ordinary entry uses its owning Loader tree base. A standing preset's root entries use the harness base, matching the preset Loader's deliberate bare-package override; nested includes retain their own bases. Relative and absolute modules walk to their nearest manifest; a manifest without `name` marks a loose module and contributes no package identity. A named package manifest must also declare a non-empty `version`, and malformed package metadata fails request preparation. Exact name/version pairs are deduplicated and sorted with a locale-independent comparison, while simultaneously active different versions remain separate.
Each ordinary entry uses its owning Loader tree base. A standing preset's root entries use the harness base, matching the preset Loader's deliberate bare-package override; nested includes retain their own bases. Relative and absolute modules walk to their nearest manifest; a manifest without `name` marks a loose module and contributes no package identity. Named private packages (`private: true`) without a `version` are also omitted; a nested package does not inherit its parent's identity. Other named package manifests must declare a non-empty `version`, and malformed package metadata, including an explicitly invalid private-package version, fails request preparation. Exact name/version pairs are deduplicated and sorted with a locale-independent comparison, while simultaneously active different versions remain separate.
The version-1 `dsh_plugin_packages` field contains only `{ name, version }` pairs. Disabled, pending, failed, disposed, unloading, structural `cordis:` rows, ordinary dependencies, loose files without an owning package identity, programmatically mounted child fibers, and in-memory dynamic plugins are excluded.
@@ -37,7 +37,7 @@ kind: "package-reference"
插件在激活时读取一次可选的 `ctx.pluginPackages` 服务。随附 app boot 会在 profile Loader 配置项之前挂载该服务,因此裸包和包子路径的身份使用与其 import 相同的权威 runtime resolution,包括它未命中的情况,并且无需包导出 `./package.json`。若该服务不存在,插件会在整个生命周期中保留 Node 原生包搜索,之后再挂载 resolver 也不会切换既有清单。因此,希望使用 runtime resolution 身份的底层嵌入方必须先挂载 `PluginPackages`,再挂载 Loader 配置项和本插件。
每个普通配置项使用其所属 Loader 树的基址。standing preset 的根配置项使用宿主基址,与 preset Loader 对裸包的显式覆写保持一致;嵌套 include 仍使用自身基址。相对与绝对模块会向上查找最近的 manifest(元数据清单);没有 `name` 的 manifest 只标记松散模块,不贡献包身份。具名包 manifest 还必须声明非空 `version`,格式错误的包元数据会使请求准备失败。系统使用与 locale 无关的比较按确切名称/版本对去重并排序,同时存活的不同版本仍会分开保留。
每个普通配置项使用其所属 Loader 树的基址。standing preset 的根配置项使用宿主基址,与 preset Loader 对裸包的显式覆写保持一致;嵌套 include 仍使用自身基址。相对与绝对模块会向上查找最近的 manifest(元数据清单);没有 `name` 的 manifest 只标记松散模块,不贡献包身份。未声明 `version` 的具名私有包(`private: true`)也会被省略;嵌套包不会继承父包的身份。其他具名包 manifest 必须声明非空 `version`,格式错误的包元数据(包括显式无效的私有包版本)会使请求准备失败。系统使用与 locale 无关的比较按确切名称/版本对去重并排序,同时存活的不同版本仍会分开保留。
版本 1 的 `dsh_plugin_packages` 字段只包含 `{ name, version }` 对。系统会排除禁用、pending、failed、disposed、unloading 状态,结构性 `cordis:` 配置项,普通依赖,没有所属包身份的松散文件,以编程方式挂载的子 fiber,以及内存动态插件。
@@ -42,6 +42,7 @@ export const Config: z<Config> = z.object({
interface PackageManifest {
readonly name?: unknown
readonly version?: unknown
readonly private?: unknown
}
interface ActiveEntry {
@@ -58,10 +59,12 @@ function barePackageName(specifier: string): string | undefined {
return first.startsWith('@') ? `${first}/${second}` : first
}
/** Read one manifest identity, optionally treating an absent name as a loose-module marker. */
/** Read a versioned identity; omit unversioned private packages and optional anonymous module markers. */
function identityFromManifest(path: string, allowAnonymous: boolean): DeepSeekPluginPackageIdentity | undefined {
const manifest = JSON.parse(readFileSync(path, 'utf8')) as PackageManifest
if (allowAnonymous && manifest.name === undefined) return undefined
if (manifest.private === true && manifest.version === undefined
&& typeof manifest.name === 'string' && manifest.name.length > 0) return undefined
if (typeof manifest.name !== 'string' || manifest.name.length === 0
|| typeof manifest.version !== 'string' || manifest.version.length === 0) {
throw new Error(`plugin-package-inventory-deepseek: ${path} must declare non-empty name and version`)
@@ -114,6 +114,41 @@ describe('DeepSeek plugin package inventory', () => {
.resolves.toMatchObject({ fields: { dsh_plugin_packages: { version: 1, packages: [] } } })
})
it.each(['file', 'bare'])('omits an unversioned private %s package without borrowing its parent identity', async (kind) => {
const { ctx, root } = await harness(undefined, true)
const parent = await packagePlugin(root, 'node_modules/inspector', { name: 'inspector', version: '1.0.0' })
const nested = await packagePlugin(root, kind === 'file' ? 'node_modules/inspector/skill' : 'node_modules/inspector-skill',
{ name: 'inspector-skill', private: true })
const versioned = await packagePlugin(root, 'versioned-private', { name: 'versioned-private', private: true, version: '2.0.0' })
await ctx.loader.create({ name: kind === 'file' ? pathToFileURL(join(root, nested)).href : 'inspector-skill/plugin.mjs' })
await ctx.loader.create({ name: versioned })
const prepared = await ctx.deepseekLlmApiExtensions.prepare({ body: { messages: [] }, signal: SIGNAL })
expect(prepared.fields.dsh_plugin_packages?.packages).toEqual([
{ name: 'versioned-private', version: '2.0.0' },
])
await ctx.loader.create({ name: parent })
const withParent = await ctx.deepseekLlmApiExtensions.prepare({ body: { messages: [] }, signal: SIGNAL })
expect(withParent.fields.dsh_plugin_packages?.packages).toEqual([
{ name: 'inspector', version: '1.0.0' },
{ name: 'versioned-private', version: '2.0.0' },
])
})
it.each([
{ name: 'private-invalid', private: true, version: '' },
{ name: 'private-invalid', private: true, version: null },
{ name: 'private-invalid', private: true, version: 1 },
{ name: '', private: true },
{ name: 'public-unversioned' },
])('rejects malformed package identity %j', async (manifest) => {
const { ctx, root } = await harness()
const plugin = await packagePlugin(root, 'invalid', manifest)
await ctx.loader.create({ name: plugin })
await expect(ctx.deepseekLlmApiExtensions.prepare({ body: { messages: [] }, signal: SIGNAL }))
.rejects.toThrow('must declare non-empty name and version')
})
it('uses the host inventory when a request has no matching or joined live agent', async () => {
const { ctx, root } = await harness()
const plugin = await packagePlugin(root, 'host-only', { name: 'host-only', version: '3.0.0' })
@@ -0,0 +1,82 @@
/** Unversioned private file plugins do not block Loader-composed DeepSeek requests. */
import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { pathToFileURL } from 'node:url'
import { afterEach, expect, it } from 'vitest'
import { Context, FiberState } from '@deepseek-ai/cordis'
import Loader from '@deepseek-ai/cordis-plugin-loader'
import Include from '@deepseek-ai/cordis-plugin-include'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import DeepSeekLlmApiExtensionRegistry from '@deepseek-ai/dsh-deepseek-llm-api-extensions'
import * as PluginInventory from '../src/index.ts'
let root: string | undefined
let context: Context | undefined
afterEach(async () => {
try {
await context?.fiber.dispose()
} finally {
context = undefined
if (root !== undefined) await rm(root, { recursive: true, force: true })
root = undefined
}
})
it('prepares DeepSeek inventory with a versioned sibling while omitting a nested private package', async () => {
root = await mkdtemp(join(tmpdir(), 'dsh-private-package-composition-'))
const parent = join(root, 'inspector')
const privateDir = join(parent, 'skill')
const siblingDir = join(parent, 'sibling')
await mkdir(privateDir, { recursive: true })
await mkdir(siblingDir)
await writeFile(join(parent, 'package.json'), JSON.stringify({ name: 'inspector', version: '1.0.0', type: 'module' }))
await writeFile(join(privateDir, 'package.json'), JSON.stringify({ name: 'inspector-skill', private: true, type: 'module' }))
await writeFile(join(siblingDir, 'package.json'), JSON.stringify({ name: 'inspector-sibling', version: '2.0.0', type: 'module' }))
await writeFile(join(privateDir, 'plugin.js'), 'export default () => {}\n')
await writeFile(join(siblingDir, 'plugin.js'), 'export default () => {}\n')
const privateUrl = pathToFileURL(join(privateDir, 'plugin.js')).href
const siblingUrl = pathToFileURL(join(siblingDir, 'plugin.js')).href
const configPath = join(root, 'cordis.yml')
await writeFile(configPath, [
'- name: cordis:agents',
'- name: cordis:deepseek-extensions',
'- name: cordis:plugin-inventory',
`- name: ${JSON.stringify(privateUrl)}`,
`- name: ${JSON.stringify(siblingUrl)}`,
'',
].join('\n'))
const ctx = new Context()
context = ctx
ctx.baseUrl = pathToFileURL(configPath).href
await ctx.plugin(Loader)
ctx.loader.builtins.include = Include
ctx.loader.builtins.agents = AgentRegistry
ctx.loader.builtins['deepseek-extensions'] = DeepSeekLlmApiExtensionRegistry
ctx.loader.builtins['plugin-inventory'] = PluginInventory
await ctx.loader.create({ name: 'cordis:include', config: { path: pathToFileURL(configPath).href } })
await ctx.loader.await()
const activeNames = [...ctx.loader.entries()]
.filter(entry => entry.fiber?.state === FiberState.ACTIVE)
.map(entry => entry.options.name)
expect(activeNames).toEqual(expect.arrayContaining(['cordis:plugin-inventory', privateUrl, siblingUrl]))
const prepared = await ctx.deepseekLlmApiExtensions.prepare({
body: { messages: [] }, signal: new AbortController().signal,
})
expect(prepared.fields).toMatchInlineSnapshot(`
{
"dsh_plugin_packages": {
"packages": [
{
"name": "inspector-sibling",
"version": "2.0.0",
},
],
"version": 1,
},
}
`)
})