返回 CodeWhale
skills.ts
根目录 / crates / tui / extension-host / src / shims / skills.ts
1 /** Reviewed bundle roots contribute to Rust's existing skill catalog. */
2 import { Service } from '@deepseek-ai/cordis'
3 import type { RpcPeer } from '../rpc.ts'
4 import { OwnedRegistrations, type OwnedEntry, type OwnerBase } from './owned.ts'
5
6 export const MAX_SKILL_ROOTS_PER_OWNER = 8
7 export const MAX_SKILL_ROOTS_PER_HOST = 64
8 export interface SkillRootDefinition { readonly path: string }
9 export interface LocalSkillRoot<O extends OwnerBase = OwnerBase> extends OwnedEntry<O> {}
10
11 export function normalizeSkillRoot(value: unknown): SkillRootDefinition {
12 if (typeof value !== 'object' || value === null || Array.isArray(value)
13 || Object.keys(value).some(key => key !== 'path')) throw new TypeError('skill root supports only path')
14 const { path } = value as { path?: unknown }
15 if (typeof path !== 'string' || !path || Buffer.byteLength(path, 'utf8') > 512
16 || /[\\:\u0000-\u001f\u007f-\u009f]/u.test(path)
17 || path.split('/').some(part => !part || part === '.' || part === '..')) {
18 throw new TypeError('skill root path must be a bounded bundle-relative path with normal slash-separated components')
19 }
20 return Object.freeze({ path })
21 }
22
23 export class SkillRoots<O extends OwnerBase> {
24 private readonly registrations: OwnedRegistrations<O, LocalSkillRoot<O>>
25 private readonly owners = new Map<O, Map<string, () => void>>()
26 private count = 0
27 constructor(rpc: RpcPeer, ownedBy: (owner: O) => Map<number, LocalSkillRoot<O>>, warn: (message: string, owner: O) => void) {
28 this.registrations = new OwnedRegistrations(rpc, 'skill_root', ownedBy, warn)
29 }
30 register(owner: O, definition: SkillRootDefinition): () => void {
31 if (owner.state !== 'activating' && owner.state !== 'active') throw new Error('skill owner is not live')
32 const { path } = normalizeSkillRoot(definition)
33 const roots = this.owners.get(owner) ?? new Map<string, () => void>()
34 if (roots.has(path)) throw new Error('skill root is already registered; dispose it before registering it again')
35 if (roots.size >= MAX_SKILL_ROOTS_PER_OWNER || this.count >= MAX_SKILL_ROOTS_PER_HOST) throw new RangeError('skill root owner or host registration limit reached')
36 const undo = this.registrations.add({ owner, name: path, disposed: false }, { name: path, description: '' })
37 const dispose = () => {
38 if (roots.get(path) !== dispose) return
39 roots.delete(path)
40 this.count--
41 if (!roots.size) this.owners.delete(owner)
42 undo()
43 }
44 roots.set(path, dispose)
45 this.owners.set(owner, roots)
46 this.count++
47 return dispose
48 }
49 forget(owner: O) {
50 for (const dispose of [...(this.owners.get(owner)?.values() ?? [])]) dispose()
51 this.registrations.forget(owner)
52 }
53 }
54
55 export function defineSkillsService<O extends OwnerBase>(host: { ownerOf(ctx: any): O | undefined, skillRoots: SkillRoots<O> }) {
56 class SkillsShim extends Service {
57 constructor(ctx: any) { super(ctx, 'skills') }
58 registerRoot(definition: SkillRootDefinition): () => void {
59 const ctx: any = this.ctx
60 const owner = host.ownerOf(ctx)
61 if (!owner) throw new Error('skills.registerRoot called outside an extension owner')
62 const root = normalizeSkillRoot(definition)
63 return ctx.effect(() => host.skillRoots.register(owner, root), `skills.registerRoot(${JSON.stringify(root.path)})`)
64 }
65 }
66 Object.freeze(SkillsShim.prototype)
67 return SkillsShim
68 }
69
69 lines TYPESCRIPT