1
0

index.ts 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. /**
  2. * Active Loader-backed plugin package inventory for official DeepSeek requests.
  3. * Host entries and the requesting agent's standing preset are resolved at request time;
  4. * installed dependencies and plugin fibers without Loader package provenance are excluded.
  5. * @module @deepseek-ai/dsh-plugin-package-inventory-deepseek
  6. */
  7. import { existsSync, readFileSync } from 'node:fs'
  8. import { createRequire } from 'node:module'
  9. import { dirname, isAbsolute, join, parse } from 'node:path'
  10. import { fileURLToPath, pathToFileURL } from 'node:url'
  11. import { FiberState, type Context } from '@deepseek-ai/cordis'
  12. import z from '@deepseek-ai/schemastery'
  13. import { brandString } from '@deepseek-ai/dsh-brand'
  14. import type { Entry, EntryTree } from '@deepseek-ai/cordis-plugin-loader'
  15. import type {} from '@deepseek-ai/dsh-agent'
  16. import type {} from '@deepseek-ai/dsh-deepseek-llm-api-extensions'
  17. import type { SessionId } from '@deepseek-ai/dsh-session'
  18. import type {} from '@deepseek-ai/dsh-agent-presets'
  19. import type { DeepSeekPluginPackageIdentity, DeepSeekPluginPackageInventoryExtension } from './types.ts'
  20. import type {} from './types.ts'
  21. export type * from './types.ts'
  22. /** Cordis plugin name. */
  23. export const name = 'plugin-package-inventory-deepseek'
  24. /** Services required to locate host/requesting-agent entries and contribute the field. */
  25. export const inject = ['agents', 'deepseekLlmApiExtensions', 'loader']
  26. /** Plugin-package request contribution configuration. */
  27. export interface Config {
  28. /** Contribute `dsh_plugin_packages` to official DeepSeek requests. Defaults to `true`. */
  29. enabled?: boolean
  30. }
  31. /** Validated plugin-package request contribution configuration. */
  32. export const Config: z<Config> = z.object({
  33. enabled: z.boolean().default(true),
  34. })
  35. interface PackageManifest {
  36. readonly name?: unknown
  37. readonly version?: unknown
  38. }
  39. interface ActiveEntry {
  40. readonly entry: Entry
  41. /** Bare-package base used by the Loader path that activated this entry. */
  42. readonly bareBaseUrl?: string
  43. }
  44. /** Parse a bare package or package-subpath specifier into its package name. */
  45. function barePackageName(specifier: string): string | undefined {
  46. if (specifier.startsWith('.') || specifier.includes(':') || isAbsolute(specifier)) return undefined
  47. const [first = '', second = ''] = specifier.split('/')
  48. // An active Loader entry already passed module resolution, so a scoped bare name has its package segment.
  49. return first.startsWith('@') ? `${first}/${second}` : first
  50. }
  51. /** Read one manifest identity, optionally treating an absent name as a loose-module marker. */
  52. function identityFromManifest(path: string, allowAnonymous: boolean): DeepSeekPluginPackageIdentity | undefined {
  53. const manifest = JSON.parse(readFileSync(path, 'utf8')) as PackageManifest
  54. if (allowAnonymous && manifest.name === undefined) return undefined
  55. if (typeof manifest.name !== 'string' || manifest.name.length === 0
  56. || typeof manifest.version !== 'string' || manifest.version.length === 0) {
  57. throw new Error(`plugin-package-inventory-deepseek: ${path} must declare non-empty name and version`)
  58. }
  59. return { name: manifest.name, version: manifest.version }
  60. }
  61. /** Resolve a bare package without requiring it to export `./package.json`. */
  62. function barePackageManifest(packageName: string, anchors: readonly string[]): string | undefined {
  63. for (const anchor of anchors) {
  64. const searchPaths = createRequire(anchor).resolve.paths(packageName)
  65. /* v8 ignore next -- active non-builtin package entries always have Node package search paths */
  66. if (searchPaths === null) continue
  67. for (const searchPath of searchPaths) {
  68. const manifest = join(searchPath, packageName, 'package.json')
  69. if (existsSync(manifest)) return manifest
  70. }
  71. }
  72. return undefined
  73. }
  74. /** Find the nearest owning manifest for a relative or absolute plugin module. */
  75. function nearestManifest(modulePath: string): string | undefined {
  76. let current = dirname(modulePath)
  77. const root = parse(current).root
  78. while (true) {
  79. const manifest = join(current, 'package.json')
  80. if (existsSync(manifest)) return manifest
  81. if (current === root) return undefined
  82. current = dirname(current)
  83. }
  84. }
  85. /** Exact package identity resolver with immutable per-process manifest caching. */
  86. class PackageIdentityResolver {
  87. // TODO: Invalidate manifest identities if in-process package-version replacement becomes a supported upgrade path.
  88. private readonly cache = new Map<string, DeepSeekPluginPackageIdentity | undefined>()
  89. constructor(private readonly hostBaseUrl: string) {}
  90. /** Resolve one Loader entry's owning package, or absence for a non-package loose module. */
  91. resolve({ entry, bareBaseUrl }: ActiveEntry): DeepSeekPluginPackageIdentity | undefined {
  92. /* v8 ignore next -- Loader entry trees inherit a base URL; the fallback supports direct embedders. */
  93. const treeBase = entry.parent.tree.ctx.baseUrl ?? this.hostBaseUrl
  94. const anchors = [...new Set([bareBaseUrl ?? treeBase, treeBase, this.hostBaseUrl, import.meta.url])]
  95. const key = `${anchors.join('\u0000')}\u0000${entry.options.name}`
  96. if (this.cache.has(key)) return this.cache.get(key)
  97. const packageName = barePackageName(entry.options.name)
  98. let manifest: string | undefined
  99. if (packageName !== undefined) {
  100. manifest = barePackageManifest(packageName, anchors)
  101. if (manifest === undefined) {
  102. throw new Error(`plugin-package-inventory-deepseek: cannot resolve active package ${JSON.stringify(packageName)}`)
  103. }
  104. } else if (!entry.options.name.startsWith('cordis:')) {
  105. const moduleUrl = isAbsolute(entry.options.name)
  106. ? pathToFileURL(entry.options.name)
  107. : new URL(entry.options.name, treeBase)
  108. if (moduleUrl.protocol === 'file:') manifest = nearestManifest(fileURLToPath(moduleUrl))
  109. }
  110. const identity = manifest === undefined ? undefined : identityFromManifest(manifest, packageName === undefined)
  111. this.cache.set(key, identity)
  112. return identity
  113. }
  114. }
  115. /** Yield active, non-structural entries from one Loader tree. */
  116. function activeEntries(tree: EntryTree, rootBareBaseUrl?: string): ActiveEntry[] {
  117. return [...tree.entries()]
  118. .filter(entry => !entry.options.group
  119. && !entry.disabled
  120. && entry.fiber?.state === FiberState.ACTIVE)
  121. .map(entry => ({
  122. entry,
  123. ...entry.parent.tree === tree && rootBareBaseUrl !== undefined
  124. ? { bareBaseUrl: rootBareBaseUrl }
  125. : {},
  126. }))
  127. }
  128. /** Deterministic text order independent of the host's ICU data and locale. */
  129. function compareWireText(left: string, right: string): number {
  130. return left < right ? -1 : left > right ? 1 : 0
  131. }
  132. /** Collect the full active package set for one request. */
  133. async function collectActivePluginPackages(
  134. ctx: Context,
  135. resolver: PackageIdentityResolver,
  136. hostBaseUrl: string,
  137. sessionId?: string,
  138. ): Promise<DeepSeekPluginPackageIdentity[]> {
  139. const entries = activeEntries(ctx.loader)
  140. if (sessionId !== undefined && ctx.get('agentPresets') !== undefined) {
  141. const agent = ctx.agents.get(brandString<SessionId>(sessionId))
  142. if (agent !== undefined) {
  143. // The optional peer is loaded only when its service is present. Its existing
  144. // mount query keeps Loader internals off the public AgentPresets service.
  145. const { standingMountFor } = await import('@deepseek-ai/dsh-agent-presets')
  146. const presetTree = standingMountFor(agent.ctx)?.tree
  147. // PresetTree deliberately resolves its root bare rows from the harness;
  148. // nested ordinary includes retain their own tree base.
  149. if (presetTree !== undefined) entries.push(...activeEntries(presetTree, hostBaseUrl))
  150. }
  151. }
  152. const unique = new Map<string, DeepSeekPluginPackageIdentity>()
  153. for (const activeEntry of entries) {
  154. const identity = resolver.resolve(activeEntry)
  155. if (identity === undefined) continue
  156. unique.set(`${identity.name}\u0000${identity.version}`, identity)
  157. }
  158. return [...unique.values()].sort((left, right) => (
  159. compareWireText(left.name, right.name) || compareWireText(left.version, right.version)
  160. ))
  161. }
  162. /**
  163. * Register the complete `dsh_plugin_packages` request contribution when enabled.
  164. * @param ctx - plugin context carrying Loader provenance and the DeepSeek request-extension registry.
  165. * @param config - validated default-on configuration.
  166. */
  167. export function apply(ctx: Context, config: Config): void {
  168. if (config.enabled === false) return
  169. const hostBaseUrl = ctx.baseUrl ?? import.meta.url
  170. const resolver = new PackageIdentityResolver(hostBaseUrl)
  171. ctx.deepseekLlmApiExtensions.register('dsh_plugin_packages', {
  172. prepare: async (request) => {
  173. const value: DeepSeekPluginPackageInventoryExtension = {
  174. version: 1,
  175. packages: await collectActivePluginPackages(ctx, resolver, hostBaseUrl, request.sessionId),
  176. }
  177. return { value }
  178. },
  179. })
  180. }