@capyclaw/foundation 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -2
- package/abi/errors.mjs +59 -0
- package/abi/index.mjs +19 -0
- package/abi/kinds.mjs +183 -0
- package/abi/loader.mjs +249 -0
- package/abi/schema.mjs +98 -0
- package/abi/spec.mjs +198 -0
- package/abi/validate.mjs +24 -0
- package/abi/version.mjs +82 -0
- package/cordis.patch.yml +7 -0
- package/kit/index.mjs +7 -0
- package/lib/adapters/http-router.mjs +45 -0
- package/lib/adapters/lifecycle-hook.mjs +62 -0
- package/lib/adapters/runtime-driver.mjs +79 -0
- package/lib/adapters/skill-provider.mjs +56 -0
- package/lib/adapters/tool-provider.mjs +42 -0
- package/lib/adapters/ui-panel.mjs +55 -0
- package/lib/host.mjs +22 -0
- package/lib/kernel.mjs +93 -0
- package/lib/skill/scan.mjs +87 -0
- package/lib/subsystems/events.mjs +55 -0
- package/lib/subsystems/log.mjs +27 -0
- package/lib/subsystems/net.mjs +49 -0
- package/package.json +49 -4
package/lib/host.mjs
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @capyclaw/foundation 的 Cordis 入口:向同进程插件提供 ctx.os。
|
|
3
|
+
*
|
|
4
|
+
* 这是整个工程里唯一一处「把内核交给 dsh」的接缝。应用插件不 import 这个文件,
|
|
5
|
+
* 它们只在自己的 apply 里拿到 ctx.os。
|
|
6
|
+
*/
|
|
7
|
+
import { createKernel } from './kernel.mjs'
|
|
8
|
+
|
|
9
|
+
export const name = 'capyclaw-foundation'
|
|
10
|
+
|
|
11
|
+
export function apply(ctx, config = {}) {
|
|
12
|
+
const kernel = createKernel(ctx, config)
|
|
13
|
+
const dispose = ctx.provide('os', kernel.os)
|
|
14
|
+
console.log('[foundation] ready(abi v' + kernel.os.abi + ')')
|
|
15
|
+
return () => {
|
|
16
|
+
try {
|
|
17
|
+
kernel.dispose()
|
|
18
|
+
} finally {
|
|
19
|
+
if (typeof dispose === 'function') dispose()
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
package/lib/kernel.mjs
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 内核装配:把 ABI 加载器、适配器与 os 子系统接到一起。
|
|
3
|
+
*
|
|
4
|
+
* 能力表是**探测出来**的,不是写死的:ctx 里没有 skills 服务,就不承诺 os.skills,
|
|
5
|
+
* 依赖它的插件在 mount 阶段拿到 E_CAP_MISSING,而不是运行到一半才炸。
|
|
6
|
+
*
|
|
7
|
+
* 惰性:os.net 只在第一次挂路由时创建共享宿主并 listen;纯 skill 插件不占端口。
|
|
8
|
+
*/
|
|
9
|
+
import { ABI_MAJOR } from '../abi/kinds.mjs'
|
|
10
|
+
import { createLoader } from '../abi/loader.mjs'
|
|
11
|
+
import { createSkillAdapter } from './adapters/skill-provider.mjs'
|
|
12
|
+
import { createToolProviderAdapter } from './adapters/tool-provider.mjs'
|
|
13
|
+
import { createRuntimeDriverAdapter } from './adapters/runtime-driver.mjs'
|
|
14
|
+
import { createHttpRouterAdapter } from './adapters/http-router.mjs'
|
|
15
|
+
import { createUiPanelAdapter } from './adapters/ui-panel.mjs'
|
|
16
|
+
import { createHookRegistry, createLifecycleAdapter } from './adapters/lifecycle-hook.mjs'
|
|
17
|
+
import { createEvents } from './subsystems/events.mjs'
|
|
18
|
+
import { createLogger } from './subsystems/log.mjs'
|
|
19
|
+
import { createNet } from './subsystems/net.mjs'
|
|
20
|
+
|
|
21
|
+
/** 内核自身就能提供的能力(不依赖任何 dsh 服务)。 */
|
|
22
|
+
export const BASE_CAPABILITIES = Object.freeze({
|
|
23
|
+
'os.events': '1.0.0',
|
|
24
|
+
'os.log': '1.0.0',
|
|
25
|
+
'os.caps': '1.0.0',
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
/** 按 ctx 里实际存在的服务生成能力表。 */
|
|
29
|
+
export function detectCapabilities(ctx) {
|
|
30
|
+
const capabilities = { ...BASE_CAPABILITIES }
|
|
31
|
+
if (ctx?.skills?.registerProvider) capabilities['os.skills'] = '1.0.0'
|
|
32
|
+
if (ctx?.serviceHost?.createSharedHost && ctx?.serviceHost?.createApp) capabilities['os.net'] = '1.0.0'
|
|
33
|
+
if (ctx?.toolHost?.registerWithDsh) capabilities['os.tools'] = '1.0.0'
|
|
34
|
+
if (ctx?.serviceBroker?.start) capabilities['os.proc'] = '1.0.0'
|
|
35
|
+
if (ctx?.editorHost?.createEditorHost) capabilities['os.ui'] = '1.0.0'
|
|
36
|
+
return capabilities
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const KERNEL_VERSION = '0.1.0'
|
|
40
|
+
export const ABI_VERSION = ABI_MAJOR
|
|
41
|
+
|
|
42
|
+
export function createKernel(ctx, config = {}) {
|
|
43
|
+
const log = createLogger(ctx, config)
|
|
44
|
+
const events = createEvents({
|
|
45
|
+
onError: (error, topic) => log.error(`事件订阅者抛错 (${topic}): ${error.message}`),
|
|
46
|
+
})
|
|
47
|
+
const hooks = createHookRegistry()
|
|
48
|
+
const net = createNet(ctx, config)
|
|
49
|
+
const capabilities = detectCapabilities(ctx)
|
|
50
|
+
|
|
51
|
+
const adapters = {
|
|
52
|
+
'skill-provider@1': createSkillAdapter({ ctx, log }),
|
|
53
|
+
'tool-provider@1': createToolProviderAdapter({ ctx, log }),
|
|
54
|
+
'runtime-driver@1': createRuntimeDriverAdapter({ ctx, config, log }),
|
|
55
|
+
'http-router@1': createHttpRouterAdapter({ ctx, net, log }),
|
|
56
|
+
'ui-panel@1': createUiPanelAdapter({ ctx, net, log }),
|
|
57
|
+
'lifecycle-hook@1': createLifecycleAdapter({ hooks, log }),
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const loader = createLoader({ adapters, capabilities, log })
|
|
61
|
+
const startedAt = Date.now()
|
|
62
|
+
|
|
63
|
+
const os = {
|
|
64
|
+
version: KERNEL_VERSION,
|
|
65
|
+
abi: ABI_VERSION,
|
|
66
|
+
mount: (spec, mountConfig = {}, meta = {}) => loader.mount(spec, mountConfig, meta),
|
|
67
|
+
report: () => ({
|
|
68
|
+
kernel: { version: KERNEL_VERSION, abi: ABI_VERSION, uptimeMs: Date.now() - startedAt },
|
|
69
|
+
...loader.report(),
|
|
70
|
+
}),
|
|
71
|
+
events,
|
|
72
|
+
log,
|
|
73
|
+
hooks,
|
|
74
|
+
net,
|
|
75
|
+
|
|
76
|
+
/** 能力检查与自报。 */
|
|
77
|
+
caps: {
|
|
78
|
+
list: () => Object.keys(capabilities),
|
|
79
|
+
has: (name) => loader.capability(name) !== null,
|
|
80
|
+
version: (name) => loader.capability(name),
|
|
81
|
+
report: () => loader.report(),
|
|
82
|
+
},
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
os,
|
|
87
|
+
dispose() {
|
|
88
|
+
loader.disposeAll()
|
|
89
|
+
events.clear()
|
|
90
|
+
void net.dispose()
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 技能扫描:SKILL.md 目录 → dsh skills provider。
|
|
3
|
+
*
|
|
4
|
+
* 这份实现是整个基石层里 skill-provider@1 的唯一实现。
|
|
5
|
+
* 此前 15 个包各自复制了一份 provider.mjs(sha256 相同),
|
|
6
|
+
* 迁移完成后由内核统一提供,应用包只声明目录。
|
|
7
|
+
*/
|
|
8
|
+
import { readFileSync } from 'node:fs'
|
|
9
|
+
import { readFile } from 'node:fs/promises'
|
|
10
|
+
import { isAbsolute, join, sep } from 'node:path'
|
|
11
|
+
|
|
12
|
+
const NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
|
|
13
|
+
const DEFAULT_RANK = 600
|
|
14
|
+
|
|
15
|
+
export function parseSkill(raw, path) {
|
|
16
|
+
const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/u.exec(raw)
|
|
17
|
+
if (!frontmatter?.[1]) throw new Error(`skill ${path} 缺少 YAML frontmatter`)
|
|
18
|
+
const meta = {}
|
|
19
|
+
for (const line of frontmatter[1].split(/\r?\n/)) {
|
|
20
|
+
const match = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(line)
|
|
21
|
+
if (match) meta[match[1]] = match[2].replace(/^['"]|['"]$/g, '').trim()
|
|
22
|
+
}
|
|
23
|
+
if (!meta.name || !meta.description) throw new Error(`skill ${path} 缺少 name/description`)
|
|
24
|
+
if (!NAME_RE.test(meta.name)) throw new Error(`skill ${path} name 必须是 kebab-case`)
|
|
25
|
+
return {
|
|
26
|
+
name: meta.name,
|
|
27
|
+
description: meta.description,
|
|
28
|
+
whenToUse: meta.whenToUse || undefined,
|
|
29
|
+
content: raw.slice(frontmatter[0].length).trim(),
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {{
|
|
35
|
+
* assetRoot: string,
|
|
36
|
+
* skillDirs: string[],
|
|
37
|
+
* providerName?: string,
|
|
38
|
+
* rank?: number,
|
|
39
|
+
* contract?: { version: string } | null,
|
|
40
|
+
* contractNote?: (version: string, skillName: string) => string
|
|
41
|
+
* }} config
|
|
42
|
+
*/
|
|
43
|
+
export function createSkillProvider(config) {
|
|
44
|
+
const assetRoot = config.assetRoot
|
|
45
|
+
if (!assetRoot || !isAbsolute(assetRoot)) throw new Error('createSkillProvider: assetRoot 必须是绝对路径')
|
|
46
|
+
const rank = config.rank ?? DEFAULT_RANK
|
|
47
|
+
const dirs = config.skillDirs || []
|
|
48
|
+
if (!dirs.length) throw new Error('createSkillProvider: skillDirs 不能为空')
|
|
49
|
+
|
|
50
|
+
const candidates = dirs.map((dir) => {
|
|
51
|
+
if (!NAME_RE.test(dir)) throw new Error(`skill 目录名必须是 kebab-case:${dir}`)
|
|
52
|
+
const directory = join(assetRoot, dir)
|
|
53
|
+
const locator = join(directory, 'SKILL.md')
|
|
54
|
+
const parsed = parseSkill(readFileSync(locator, 'utf8'), locator)
|
|
55
|
+
if (parsed.name !== dir) throw new Error(`skill ${locator} name 必须是 ${dir}`)
|
|
56
|
+
return {
|
|
57
|
+
name: parsed.name,
|
|
58
|
+
description: parsed.description,
|
|
59
|
+
whenToUse: parsed.whenToUse,
|
|
60
|
+
invocation: { modelInvocable: true, userInvocable: true },
|
|
61
|
+
provider: config.providerName || 'dsh-plugin',
|
|
62
|
+
source: 'bundled',
|
|
63
|
+
rank,
|
|
64
|
+
resourceBase: { kind: 'directory', path: directory.split(sep).join('/') },
|
|
65
|
+
locator,
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
const byName = new Map(candidates.map((candidate) => [candidate.name, candidate]))
|
|
70
|
+
const noteFor = (name) => (config.contract && config.contractNote
|
|
71
|
+
? config.contractNote(config.contract.contractVersion ?? config.contract.version, name)
|
|
72
|
+
: '')
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
name: config.providerName || 'dsh-plugin',
|
|
76
|
+
list: () => Promise.resolve(candidates),
|
|
77
|
+
async get(candidate, options = {}) {
|
|
78
|
+
const hit = byName.get(candidate.name)
|
|
79
|
+
if (!hit) throw new Error(`未知 skill ${candidate.name}`)
|
|
80
|
+
const raw = await readFile(hit.locator, { encoding: 'utf8', signal: options.signal })
|
|
81
|
+
const parsed = parseSkill(raw, hit.locator)
|
|
82
|
+
const { rank: _rank, locator: _locator, ...summary } = hit
|
|
83
|
+
const note = noteFor(candidate.name)
|
|
84
|
+
return { ...summary, content: parsed.content + (note ? `\n\n---\n\n${note}` : '') }
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 进程内事件总线:插件之间唯一的广播通道。
|
|
3
|
+
*
|
|
4
|
+
* 约定:emit 永不因订阅者抛错而中断;错误交给 onError 记录。
|
|
5
|
+
*/
|
|
6
|
+
export function createEvents({ onError } = {}) {
|
|
7
|
+
const listeners = new Map()
|
|
8
|
+
|
|
9
|
+
function on(topic, handler) {
|
|
10
|
+
if (typeof handler !== 'function') throw new TypeError('events.on 需要函数')
|
|
11
|
+
if (!listeners.has(topic)) listeners.set(topic, [])
|
|
12
|
+
listeners.get(topic).push(handler)
|
|
13
|
+
return () => off(topic, handler)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function once(topic, handler) {
|
|
17
|
+
const dispose = on(topic, (...args) => {
|
|
18
|
+
dispose()
|
|
19
|
+
handler(...args)
|
|
20
|
+
})
|
|
21
|
+
return dispose
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function off(topic, handler) {
|
|
25
|
+
const bucket = listeners.get(topic)
|
|
26
|
+
if (!bucket) return
|
|
27
|
+
const index = bucket.indexOf(handler)
|
|
28
|
+
if (index >= 0) bucket.splice(index, 1)
|
|
29
|
+
if (!bucket.length) listeners.delete(topic)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function emit(topic, payload) {
|
|
33
|
+
const bucket = listeners.get(topic)
|
|
34
|
+
if (!bucket) return 0
|
|
35
|
+
let delivered = 0
|
|
36
|
+
for (const handler of [...bucket]) {
|
|
37
|
+
try {
|
|
38
|
+
handler(payload)
|
|
39
|
+
delivered++
|
|
40
|
+
} catch (error) {
|
|
41
|
+
onError?.(error, topic)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return delivered
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
on,
|
|
49
|
+
once,
|
|
50
|
+
off,
|
|
51
|
+
emit,
|
|
52
|
+
topics: () => [...listeners.keys()],
|
|
53
|
+
clear: () => listeners.clear(),
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 结构化日志:优先复用 dsh 的 ctx.logger,没有就退回 console。
|
|
3
|
+
*
|
|
4
|
+
* 插件不自己拼前缀,一律通过 log.child({ plugin }) 拿作用域。
|
|
5
|
+
*/
|
|
6
|
+
const LEVELS = ['debug', 'info', 'warn', 'error']
|
|
7
|
+
|
|
8
|
+
function createScoped(underlying, scope) {
|
|
9
|
+
const prefix = scope ? `[${scope}]` : ''
|
|
10
|
+
const scoped = {}
|
|
11
|
+
for (const level of LEVELS) {
|
|
12
|
+
scoped[level] = (...args) => {
|
|
13
|
+
const message = args.map((arg) => (typeof arg === 'string' ? arg : JSON.stringify(arg))).join(' ')
|
|
14
|
+
const line = `${prefix} ${message}`.trim()
|
|
15
|
+
const fn = underlying?.[level]
|
|
16
|
+
if (typeof fn === 'function') fn.call(underlying, line)
|
|
17
|
+
else if (level !== 'debug') console[level](line)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
scoped.child = (next) => createScoped(underlying, scope ? `${scope}:${next}` : next)
|
|
21
|
+
return scoped
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function createLogger(ctx, config = {}) {
|
|
25
|
+
const underlying = ctx?.logger
|
|
26
|
+
return createScoped(underlying, config.scope || 'foundation')
|
|
27
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP 共享宿主:内核持有的唯一监听面。
|
|
3
|
+
*
|
|
4
|
+
* 惰性:第一次有插件挂路由时才创建共享宿主并 listen,
|
|
5
|
+
* 只注册 skill 的插件不会因为装了 foundation 就占一个端口。
|
|
6
|
+
*/
|
|
7
|
+
import { AbiError, ERROR_CODES } from '../../abi/errors.mjs'
|
|
8
|
+
|
|
9
|
+
export function createNet(ctx, config = {}) {
|
|
10
|
+
const http = config.http || {}
|
|
11
|
+
let host = null
|
|
12
|
+
let listening = null
|
|
13
|
+
let closed = false
|
|
14
|
+
|
|
15
|
+
function ensure() {
|
|
16
|
+
const serviceHost = ctx?.serviceHost
|
|
17
|
+
if (!serviceHost?.createSharedHost) {
|
|
18
|
+
throw new AbiError(ERROR_CODES.E_CAP_MISSING,
|
|
19
|
+
'ctx.serviceHost 不可用:无法挂载 HTTP 路由', { capability: 'os.net' })
|
|
20
|
+
}
|
|
21
|
+
if (!host) {
|
|
22
|
+
host = serviceHost.createSharedHost({ id: http.hostId || 'foundation-shared' })
|
|
23
|
+
listening = Promise.resolve(host.listen({ port: http.port ?? 0 }))
|
|
24
|
+
}
|
|
25
|
+
return host
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
ensure,
|
|
30
|
+
mount(prefix, child) {
|
|
31
|
+
return ensure().mount(prefix, child)
|
|
32
|
+
},
|
|
33
|
+
children: () => (host ? host.children() : []),
|
|
34
|
+
ready() {
|
|
35
|
+
ensure()
|
|
36
|
+
return listening
|
|
37
|
+
},
|
|
38
|
+
async dispose() {
|
|
39
|
+
if (closed) return
|
|
40
|
+
closed = true
|
|
41
|
+
if (host) {
|
|
42
|
+
try {
|
|
43
|
+
await host.close()
|
|
44
|
+
} catch { /* 已关闭 */ }
|
|
45
|
+
}
|
|
46
|
+
host = null
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,51 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capyclaw/foundation",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "dsh 插件基石层:接口 ABI、插件加载器与内核服务 ctx.os。应用插件只声明接口,不写装配代码。",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./lib/host.mjs",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./lib/host.mjs",
|
|
9
|
+
"./abi": "./abi/index.mjs",
|
|
10
|
+
"./kit": "./kit/index.mjs",
|
|
11
|
+
"./package.json": "./package.json"
|
|
12
|
+
},
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18",
|
|
15
|
+
"dsh": "^0.2.0-rc.2"
|
|
16
|
+
},
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"dsh": {
|
|
21
|
+
"bundle": {
|
|
22
|
+
"patch": "./cordis.patch.yml"
|
|
23
|
+
},
|
|
24
|
+
"category": "service",
|
|
25
|
+
"platform": "both",
|
|
26
|
+
"apiVersion": "1.0.0"
|
|
27
|
+
},
|
|
28
|
+
"meta": {
|
|
29
|
+
"title": "插件基石层",
|
|
30
|
+
"description": "接口 ABI、插件加载器与 ctx.os 内核服务;应用插件只声明接口"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"abi/",
|
|
34
|
+
"lib/",
|
|
35
|
+
"kit/",
|
|
36
|
+
"cordis.patch.yml",
|
|
37
|
+
"README.md"
|
|
38
|
+
],
|
|
39
|
+
"scripts": {
|
|
40
|
+
"test": "node --test test/*.test.mjs"
|
|
41
|
+
},
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@capyclaw/plugin-kit": "^0.2.0"
|
|
45
|
+
},
|
|
46
|
+
"repository": {
|
|
47
|
+
"type": "git",
|
|
48
|
+
"url": "git+https://github.com/Maple496/capyclaw.git",
|
|
49
|
+
"directory": "packages/capyclaw-foundation"
|
|
50
|
+
}
|
|
51
|
+
}
|