fix(llm): omit unversioned private plugins from DeepSeek inventory
This commit is contained in:
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' })
|
||||
|
||||
+82
@@ -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,
|
||||
},
|
||||
}
|
||||
`)
|
||||
})
|
||||
Reference in new issue
Block a user