mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
295 lines
8.7 KiB
TypeScript
295 lines
8.7 KiB
TypeScript
import { Context, Fiber, Inject } from 'cordis'
|
|
import { deepEqual, isNullable } from 'cosmokit'
|
|
import { Loader } from '../index.ts'
|
|
import { EntryGroup } from './group.ts'
|
|
import { EntryTree } from './tree.ts'
|
|
import { evaluate, interpolate } from './utils.ts'
|
|
|
|
/** Serialized plugin entry options stored in loader config files. */
|
|
export interface EntryOptions {
|
|
/** Stable id inside the containing entry tree. */
|
|
id: string
|
|
/** Module specifier imported by the entry tree. */
|
|
name: string
|
|
/** Config passed to the plugin. */
|
|
config?: any
|
|
/** Marks this entry as a nested group. */
|
|
group?: boolean | null
|
|
/** Prevents this entry and descendants from running. */
|
|
disabled?: boolean | null
|
|
/** Required services or service intercept config for this entry. */
|
|
inject?: Inject | null
|
|
}
|
|
|
|
function updateError(stage: 'import' | 'dispose' | 'apply' | 'rollback', options: EntryOptions, cause: unknown) {
|
|
const detail = cause instanceof Error ? cause.message : String(cause)
|
|
return new Error(`failed to ${stage} loader entry ${options.id} (${options.name}): ${detail}`, { cause })
|
|
}
|
|
|
|
function takeEntries(object: {}, keys: string[]) {
|
|
const result: [string, any][] = []
|
|
for (const key of keys) {
|
|
if (!(key in object)) continue
|
|
result.push([key, object[key]])
|
|
delete object[key]
|
|
}
|
|
return result
|
|
}
|
|
|
|
function sortKeys<T extends {}>(object: T, prepend = ['id', 'name'], append = ['config']): T {
|
|
const part1 = takeEntries(object, prepend)
|
|
const part2 = takeEntries(object, append)
|
|
const rest = takeEntries(object, Object.keys(object)).sort(([a], [b]) => a.localeCompare(b))
|
|
return Object.assign(object, Object.fromEntries([...part1, ...rest, ...part2]))
|
|
}
|
|
|
|
function replaceKeys<T extends {}>(target: T, source: T): T {
|
|
for (const key of Object.keys(target)) Reflect.deleteProperty(target, key)
|
|
return Object.assign(target, source)
|
|
}
|
|
|
|
/** One configured plugin node inside an `EntryTree`. */
|
|
export class Entry {
|
|
static readonly key = Symbol.for('cordis.entry')
|
|
|
|
public ctx: Context
|
|
public fiber?: Fiber
|
|
public parent!: EntryGroup
|
|
// safety: call `entry.update()` immediately after creating an entry
|
|
public options = {} as EntryOptions
|
|
public subgroup?: EntryGroup
|
|
public subtree?: EntryTree
|
|
|
|
_initTask?: Promise<void>
|
|
_disposing = 0
|
|
|
|
constructor(public loader: Loader) {
|
|
this.ctx = loader.ctx.extend({ [Entry.key]: this })
|
|
this.context.emit('loader/entry-init', this)
|
|
}
|
|
|
|
get context(): Context {
|
|
return this.ctx
|
|
}
|
|
|
|
get id() {
|
|
let id = this.options.id
|
|
if (this.parent.tree.ctx.fiber.entry) {
|
|
id = this.parent.tree.ctx.fiber.entry.id + EntryTree.sep + id
|
|
}
|
|
return id
|
|
}
|
|
|
|
/** True when this entry or any owning parent entry is disabled. */
|
|
get disabled() {
|
|
return this._disabled(this.options)
|
|
}
|
|
|
|
private _disabled(options: EntryOptions) {
|
|
// group is always enabled
|
|
if (options.group) return false
|
|
if (options.disabled) return true
|
|
let entry = this.parent.ctx.fiber.entry
|
|
while (entry) {
|
|
if (entry.options.disabled) return true
|
|
entry = entry.parent.ctx.fiber.entry
|
|
}
|
|
return false
|
|
}
|
|
|
|
evaluate(expr: string) {
|
|
return evaluate(this.ctx, expr)
|
|
}
|
|
|
|
_resolveConfig(plugin: any): [any, any?] {
|
|
if (plugin[EntryGroup.key]) return this.options.config
|
|
return interpolate(this.ctx, this.options.config)
|
|
}
|
|
|
|
private async _patchContext(diff: string[]) {
|
|
await this.context.waterfall('loader/patch-context', this, async () => {
|
|
Object.setPrototypeOf(this.ctx, this.parent.ctx)
|
|
|
|
if (this.fiber?.uid && (diff.includes('config') || this.options.group)) {
|
|
await this.fiber.update(this._resolveConfig(this.fiber.runtime!.callback), true)
|
|
}
|
|
})
|
|
}
|
|
|
|
async refresh() {
|
|
if (this.fiber) return
|
|
if (this.disabled) return
|
|
await this.init()
|
|
}
|
|
|
|
async _dispose(fiber = this.fiber) {
|
|
if (!fiber) return
|
|
if (this.fiber === fiber) this.fiber = undefined
|
|
this._disposing += 1
|
|
try {
|
|
await fiber.dispose()
|
|
} finally {
|
|
this._disposing -= 1
|
|
}
|
|
}
|
|
|
|
/** Merge new options, restart as needed, and persist through the parent tree. */
|
|
async update(options: Partial<EntryOptions>, create = false, force = false) {
|
|
const previousOptions = this.options
|
|
const legacy = { ...previousOptions }
|
|
const candidate = create ? options as EntryOptions : { ...previousOptions }
|
|
if (!create) {
|
|
for (const [key, value] of Object.entries(options)) {
|
|
if (isNullable(value)) {
|
|
delete candidate[key as keyof EntryOptions]
|
|
} else {
|
|
candidate[key as keyof EntryOptions] = value as never
|
|
}
|
|
}
|
|
}
|
|
sortKeys(candidate)
|
|
|
|
const diff = Object
|
|
.keys({ ...candidate, ...legacy })
|
|
.filter(key => !deepEqual(candidate[key as keyof EntryOptions], legacy[key as keyof EntryOptions]))
|
|
if (!diff.length && !force) return
|
|
|
|
const commit = () => {
|
|
if (create) return
|
|
this.options = replaceKeys(previousOptions, candidate)
|
|
}
|
|
|
|
const previous = this.fiber
|
|
if (!previous?.uid) {
|
|
this.fiber = undefined
|
|
this.options = candidate
|
|
try {
|
|
if (!this._disabled(candidate)) await this.init()
|
|
} catch (error) {
|
|
this.options = previousOptions
|
|
throw error
|
|
}
|
|
commit()
|
|
return
|
|
}
|
|
|
|
if (this._disabled(candidate)) {
|
|
this.options = candidate
|
|
try {
|
|
await this._dispose(previous)
|
|
} catch (error) {
|
|
this.options = previousOptions
|
|
throw updateError('dispose', candidate, error)
|
|
}
|
|
commit()
|
|
this.context.emit('loader/partial-dispose', this, legacy, true)
|
|
return
|
|
}
|
|
|
|
const replace = diff.some(key => key === 'name' || key === 'inject' || key === 'group')
|
|
if (!replace) {
|
|
this.options = candidate
|
|
try {
|
|
await this._patchContext(diff)
|
|
} catch (error) {
|
|
this.options = previousOptions
|
|
try {
|
|
await this._patchContext(diff)
|
|
} catch (rollbackError) {
|
|
throw updateError('rollback', legacy, new AggregateError([error, rollbackError]))
|
|
}
|
|
this.context.emit('loader/partial-dispose', this, candidate, true)
|
|
throw updateError('apply', candidate, error)
|
|
}
|
|
commit()
|
|
this.context.emit('loader/partial-dispose', this, legacy, true)
|
|
return
|
|
}
|
|
|
|
let plugin: any
|
|
try {
|
|
plugin = diff.includes('name')
|
|
? this.loader.unwrapExports(await this.parent.tree.import(candidate.name, this.getOuterStack))
|
|
: previous.runtime!.callback
|
|
} catch (error) {
|
|
throw updateError('import', candidate, error)
|
|
}
|
|
|
|
const previousPlugin = previous.runtime!.callback
|
|
this.options = candidate
|
|
try {
|
|
await this._dispose(previous)
|
|
} catch (error) {
|
|
this.options = previousOptions
|
|
throw updateError('dispose', candidate, error)
|
|
}
|
|
|
|
try {
|
|
await this._start(plugin)
|
|
} catch (error) {
|
|
this.options = previousOptions
|
|
try {
|
|
await this._start(previousPlugin)
|
|
} catch (rollbackError) {
|
|
throw updateError('rollback', legacy, new AggregateError([error, rollbackError]))
|
|
}
|
|
this.context.emit('loader/partial-dispose', this, candidate, true)
|
|
throw updateError('apply', candidate, error)
|
|
}
|
|
commit()
|
|
this.context.emit('loader/partial-dispose', this, legacy, true)
|
|
}
|
|
|
|
getOuterStack = () => {
|
|
let entry: Entry | undefined = this
|
|
const result: string[] = []
|
|
do {
|
|
result.push(` at ${entry.parent.tree.ctx.baseUrl}#${entry.options.id}`)
|
|
entry = entry.parent.ctx.fiber.entry
|
|
} while (entry)
|
|
return result
|
|
}
|
|
|
|
/** Import and start the configured plugin if it is not already running. */
|
|
async init() {
|
|
try {
|
|
await (this._initTask ??= this._init())
|
|
} finally {
|
|
this._initTask = undefined
|
|
if (!this.loader.getTasks().length) this.ctx.reflect.notify(['loader'])
|
|
}
|
|
await this.fiber?.await()
|
|
}
|
|
|
|
private async _init() {
|
|
let plugin: any
|
|
try {
|
|
plugin = this.loader.unwrapExports(await this.parent.tree.import(this.options.name, this.getOuterStack))
|
|
} catch (error) {
|
|
throw updateError('import', this.options, error)
|
|
}
|
|
try {
|
|
await this._start(plugin)
|
|
} catch (error) {
|
|
throw updateError('apply', this.options, error)
|
|
}
|
|
}
|
|
|
|
private async _start(plugin: any) {
|
|
let fiber: Fiber | undefined
|
|
try {
|
|
fiber = await this._create(plugin)
|
|
await fiber.await()
|
|
} catch (error) {
|
|
await this._dispose(fiber)
|
|
throw error
|
|
}
|
|
}
|
|
|
|
private async _create(plugin: any): Promise<Fiber> {
|
|
await this._patchContext([])
|
|
this.loader.showLog(this, 'apply')
|
|
return this.fiber = this.ctx.registry.plugin(plugin, this._resolveConfig(plugin), this.getOuterStack)
|
|
}
|
|
}
|