@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/abi/spec.mjs ADDED
@@ -0,0 +1,198 @@
1
+ /**
2
+ * 插件描述符:应用插件唯一需要写的东西。
3
+ *
4
+ * import { createPlugin, skillProvider } from '@capyclaw/foundation/abi'
5
+ *
6
+ * export const { name, inject, apply } = createPlugin({
7
+ * name: 'canvas',
8
+ * requires: ['canvas.runtime@^1'],
9
+ * implements: [skillProvider({ skill: './skills/canvas' })],
10
+ * }, import.meta.url)
11
+ *
12
+ * 设计要点:
13
+ * - 构建器在模块加载期就校验 schema,坏描述符不会活到 activate;
14
+ * - 描述符是纯数据 + 函数,不 import 内核、不持有 ctx;
15
+ * - createPlugin 产出的是普通 Cordis 插件形状(name/inject/apply),
16
+ * 装配细节全部收回内核的 mount。
17
+ */
18
+ import { ABI_MAJOR, getInterfaceKind } from './kinds.mjs'
19
+ import { validateSchema } from './schema.mjs'
20
+ import { validateInterface } from './validate.mjs'
21
+ import { AbiError, ERROR_CODES, toAbiError } from './errors.mjs'
22
+
23
+ const PLUGIN_NAME_RE = /^[a-z0-9][a-z0-9-]*$/
24
+ const CAPABILITY_NAME_RE = /^[a-zA-Z][a-zA-Z0-9]*(?:[.-][a-zA-Z0-9*]+)*$/
25
+ const RANGE_RE = /^(?:\^|~|>=|<=|>|<|=)?[0-9x*][0-9A-Za-z.*-]*$/
26
+
27
+ const KINDS = Object.freeze({
28
+ 'skill-provider': 'skill-provider@1',
29
+ 'tool-provider': 'tool-provider@1',
30
+ 'runtime-driver': 'runtime-driver@1',
31
+ 'http-router': 'http-router@1',
32
+ 'ui-panel': 'ui-panel@1',
33
+ 'lifecycle-hook': 'lifecycle-hook@1',
34
+ raw: 'raw@1',
35
+ })
36
+
37
+ /**
38
+ * 校验并冻结一个接口描述符。
39
+ * @param {string} kind
40
+ * @param {object} options
41
+ */
42
+ export function defineInterface(kind, options) {
43
+ const definition = getInterfaceKind(kind)
44
+ if (!definition) {
45
+ throw new AbiError(ERROR_CODES.E_KIND_UNKNOWN, `未知接口种类 ${kind}`, {
46
+ kind,
47
+ known: Object.keys(KINDS).map((key) => KINDS[key]),
48
+ })
49
+ }
50
+ if (options === null || typeof options !== 'object' || Array.isArray(options)) {
51
+ throw new AbiError(ERROR_CODES.E_SCHEMA, `${kind} 的描述符必须是对象`, { kind })
52
+ }
53
+ const errors = validateSchema(options, definition.schema)
54
+ definition.extra?.(options, errors)
55
+ if (errors.length) {
56
+ throw new AbiError(ERROR_CODES.E_SCHEMA, `${kind} 描述符非法:${errors.join(';')}`, { kind, errors })
57
+ }
58
+ return Object.freeze({ kind, ...options })
59
+ }
60
+
61
+ /* ------------------------------------------------------------------------- */
62
+ /* 六个接口构建器 + 逃生舱 */
63
+ /* ------------------------------------------------------------------------- */
64
+
65
+ export const skillProvider = (options) => defineInterface(KINDS['skill-provider'], options)
66
+ export const toolProvider = (options) => defineInterface(KINDS['tool-provider'], options)
67
+ export const runtimeDriver = (options) => defineInterface(KINDS['runtime-driver'], options)
68
+ export const httpRouter = (options) => defineInterface(KINDS['http-router'], options)
69
+ export const uiPanel = (options) => defineInterface(KINDS['ui-panel'], options)
70
+ export const lifecycleHook = (options) => defineInterface(KINDS['lifecycle-hook'], options)
71
+
72
+ /** 逃生舱:必须同时导出 apply,且不得与其他接口混用。 */
73
+ export const rawPlugin = (apply) => defineInterface(KINDS.raw, { apply })
74
+
75
+ /* ------------------------------------------------------------------------- */
76
+ /* 插件定义 */
77
+ /* ------------------------------------------------------------------------- */
78
+
79
+ function parseCapability(entry, field) {
80
+ if (typeof entry !== 'string' || !entry.trim()) {
81
+ throw new AbiError(ERROR_CODES.E_PLUGIN_SHAPE, `${field} 的每一项必须是非空字符串`, { entry })
82
+ }
83
+ const at = entry.lastIndexOf('@')
84
+ if (at <= 0 || at === entry.length - 1) {
85
+ throw new AbiError(ERROR_CODES.E_PLUGIN_SHAPE, `${field} 项 "${entry}" 必须写成 name@range`, { entry })
86
+ }
87
+ const name = entry.slice(0, at)
88
+ const range = entry.slice(at + 1)
89
+ if (!CAPABILITY_NAME_RE.test(name)) {
90
+ throw new AbiError(ERROR_CODES.E_PLUGIN_SHAPE, `${field} 项 "${entry}" 的能力名非法`, { entry, name })
91
+ }
92
+ if (!RANGE_RE.test(range)) {
93
+ throw new AbiError(ERROR_CODES.E_PLUGIN_SHAPE, `${field} 项 "${entry}" 的版本范围非法`, { entry, range })
94
+ }
95
+ return Object.freeze({ name, range, raw: entry })
96
+ }
97
+
98
+ /**
99
+ * @param {{
100
+ * name: string,
101
+ * apiVersion?: number,
102
+ * description?: string,
103
+ * requires?: string[],
104
+ * provides?: string[],
105
+ * implements: object[]
106
+ * }} options
107
+ */
108
+ export function definePlugin(options = {}) {
109
+ const { name, apiVersion = ABI_MAJOR, description, requires = [], provides = [], implements: impls } = options
110
+
111
+ if (!PLUGIN_NAME_RE.test(String(name || ''))) {
112
+ throw new AbiError(ERROR_CODES.E_PLUGIN_SHAPE, `插件名必须是 kebab-case:${name}`, { name })
113
+ }
114
+ if (!Number.isInteger(apiVersion) || apiVersion !== ABI_MAJOR) {
115
+ throw new AbiError(ERROR_CODES.E_ABI_MISMATCH,
116
+ `插件声明 ABI v${apiVersion},内核只支持 v${ABI_MAJOR}`, { name, apiVersion, supported: ABI_MAJOR })
117
+ }
118
+ if (!Array.isArray(impls) || impls.length === 0) {
119
+ throw new AbiError(ERROR_CODES.E_PLUGIN_SHAPE, '插件必须实现至少一个接口', { name })
120
+ }
121
+ if (description !== undefined && typeof description !== 'string') {
122
+ throw new AbiError(ERROR_CODES.E_PLUGIN_SHAPE, 'description 必须是字符串', { name })
123
+ }
124
+
125
+ for (const impl of impls) {
126
+ if (!impl || typeof impl !== 'object' || typeof impl.kind !== 'string') {
127
+ throw new AbiError(ERROR_CODES.E_PLUGIN_SHAPE, 'implements 的每一项必须是 define* 产出的描述符', { name })
128
+ }
129
+ const { definition, errors } = validateInterface(impl)
130
+ if (!definition) {
131
+ throw new AbiError(ERROR_CODES.E_KIND_UNKNOWN, `未知接口种类 ${impl.kind}`, { name, kind: impl.kind })
132
+ }
133
+ if (errors.length) {
134
+ throw new AbiError(ERROR_CODES.E_SCHEMA, `${impl.kind} 描述符非法:${errors.join(';')}`, {
135
+ name, kind: impl.kind, errors,
136
+ })
137
+ }
138
+ }
139
+
140
+ if (impls.some((impl) => getInterfaceKind(impl.kind)?.exclusive) && impls.length > 1) {
141
+ throw new AbiError(ERROR_CODES.E_PLUGIN_SHAPE, 'raw@1 不得与其他接口混用', { name })
142
+ }
143
+
144
+ const requiresEntries = requires.map((entry) => parseCapability(entry, 'requires'))
145
+ const providesEntries = provides.map((entry) => parseCapability(entry, 'provides'))
146
+ for (const entry of providesEntries) {
147
+ if (/^[\^~<>=]/.test(entry.range)) {
148
+ throw new AbiError(ERROR_CODES.E_PLUGIN_SHAPE,
149
+ `provides 必须写精确版本,不能写范围:${entry.raw}`, { entry: entry.raw })
150
+ }
151
+ }
152
+
153
+ return Object.freeze({
154
+ kind: 'plugin',
155
+ name,
156
+ apiVersion,
157
+ description,
158
+ requires: Object.freeze(requiresEntries),
159
+ provides: Object.freeze(providesEntries),
160
+ implements: Object.freeze([...impls]),
161
+ })
162
+ }
163
+
164
+ /* ------------------------------------------------------------------------- */
165
+ /* Cordis 接缝 */
166
+ /* ------------------------------------------------------------------------- */
167
+
168
+ /**
169
+ * 把插件定义编译成普通 Cordis 插件:{ name, inject, apply }。
170
+ *
171
+ * apply 的返回值是 disposer(与仓内既有插件一致),内核在卸载时调用。
172
+ * 形状错误(schema / 能力 / 唯一性)同步抛出,让插件加载直接失败;
173
+ * 运行时就绪(子进程拉起等)异步进行,通过 os.report() 观测。
174
+ *
175
+ * @param {object} definition definePlugin 的结果,或直接传 definePlugin 的入参
176
+ * @param {string} baseUrl 插件入口的 import.meta.url,用于解析相对路径
177
+ */
178
+ export function createPlugin(definition, baseUrl) {
179
+ const spec = definition?.kind === 'plugin' ? definition : definePlugin(definition)
180
+
181
+ function apply(ctx, config = {}) {
182
+ const os = ctx?.os
183
+ if (!os || typeof os.mount !== 'function') {
184
+ throw new AbiError(ERROR_CODES.E_KERNEL_MISSING,
185
+ 'ctx.os 不可用:请先安装并启用 @capyclaw/foundation', { plugin: spec.name })
186
+ }
187
+ if (!baseUrl) {
188
+ throw new AbiError(ERROR_CODES.E_BASE_URL_MISSING,
189
+ 'createPlugin 需要第二参数 baseUrl(传 import.meta.url)', { plugin: spec.name })
190
+ }
191
+ const handle = os.mount(spec, config, { baseUrl })
192
+ return () => handle.dispose()
193
+ }
194
+
195
+ return Object.freeze({ name: spec.name, inject: ['os'], apply, spec })
196
+ }
197
+
198
+ export { toAbiError }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * 描述符校验:把「kind 之外的全部字段」交给对应接口的 schema。
3
+ *
4
+ * 单独成模块,是因为 spec(构建期)与 loader(加载期)必须用同一套判定;
5
+ * 只要两处各写一遍,迟早会出现「构建能过、加载失败」或反过来。
6
+ */
7
+ import { getInterfaceKind } from './kinds.mjs'
8
+ import { validateSchema } from './schema.mjs'
9
+
10
+ /**
11
+ * @param {object} impl 描述符(含 kind)
12
+ * @returns {{ definition: object|null, errors: string[] }}
13
+ */
14
+ export function validateInterface(impl) {
15
+ const definition = getInterfaceKind(impl?.kind)
16
+ if (!definition) {
17
+ return { definition: null, errors: [`未知接口种类 ${impl?.kind}`] }
18
+ }
19
+ const body = { ...impl }
20
+ delete body.kind
21
+ const errors = validateSchema(body, definition.schema)
22
+ definition.extra?.(body, errors)
23
+ return { definition, errors }
24
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * 版本与范围:能力声明用 `^1` / `^1.2.0` / `>=1.2` / `1.x` / 精确版本。
3
+ *
4
+ * 只实现这份 ABI 需要的语义,不引入 semver 依赖:
5
+ * - `^1.2.3` → >=1.2.3 且同主版本;`^0.2.3` → >=0.2.3 且同次版本(0.x 约定)
6
+ * - `~1.2.3` → >=1.2.3 且同次版本
7
+ * - `1.x` / `1.*` → 同主版本
8
+ * - `>=` `>` `<=` `<` `=` 前缀
9
+ * - 裸版本 → 精确匹配(比较时忽略末位 0)
10
+ */
11
+
12
+ export function parseVersion(text) {
13
+ if (typeof text !== 'string') return null
14
+ const match = /^v?(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-[0-9A-Za-z.-]+)?$/.exec(text.trim())
15
+ if (!match) return null
16
+ return [Number(match[1]), Number(match[2] || 0), Number(match[3] || 0)]
17
+ }
18
+
19
+ function compareParts(a, b) {
20
+ for (let i = 0; i < 3; i++) {
21
+ if (a[i] !== b[i]) return a[i] < b[i] ? -1 : 1
22
+ }
23
+ return 0
24
+ }
25
+
26
+ export function compareVersions(a, b) {
27
+ const left = typeof a === 'string' ? parseVersion(a) : a
28
+ const right = typeof b === 'string' ? parseVersion(b) : b
29
+ if (!left || !right) return NaN
30
+ return compareParts(left, right)
31
+ }
32
+
33
+ /**
34
+ * @param {string} version 提供方版本,如 '1.2.3'
35
+ * @param {string} range 需求范围,如 '^1'
36
+ * @returns {boolean}
37
+ */
38
+ export function satisfies(version, range) {
39
+ const current = parseVersion(version)
40
+ if (!current) return false
41
+ const raw = String(range ?? '').trim()
42
+ if (!raw || raw === '*' || raw === 'x') return true
43
+
44
+ if (raw.startsWith('^')) {
45
+ const base = parseVersion(raw.slice(1))
46
+ if (!base) return false
47
+ if (compareParts(current, base) < 0) return false
48
+ return base[0] === 0 ? current[0] === 0 && current[1] === base[1] : current[0] === base[0]
49
+ }
50
+ if (raw.startsWith('~')) {
51
+ const base = parseVersion(raw.slice(1))
52
+ if (!base) return false
53
+ return compareParts(current, base) >= 0 && current[0] === base[0] && current[1] === base[1]
54
+ }
55
+ if (raw.startsWith('>=')) {
56
+ const base = parseVersion(raw.slice(2))
57
+ return Boolean(base) && compareParts(current, base) >= 0
58
+ }
59
+ if (raw.startsWith('<=')) {
60
+ const base = parseVersion(raw.slice(2))
61
+ return Boolean(base) && compareParts(current, base) <= 0
62
+ }
63
+ if (raw.startsWith('>')) {
64
+ const base = parseVersion(raw.slice(1))
65
+ return Boolean(base) && compareParts(current, base) > 0
66
+ }
67
+ if (raw.startsWith('<')) {
68
+ const base = parseVersion(raw.slice(1))
69
+ return Boolean(base) && compareParts(current, base) < 0
70
+ }
71
+ if (raw.startsWith('=')) return satisfies(version, raw.slice(1))
72
+
73
+ const xRange = /^(\d+)(?:\.(\d+))?\.[x*]$/.exec(raw)
74
+ if (xRange) {
75
+ const major = Number(xRange[1])
76
+ const minor = xRange[2] === undefined ? null : Number(xRange[2])
77
+ return current[0] === major && (minor === null || current[1] === minor)
78
+ }
79
+
80
+ const exact = parseVersion(raw)
81
+ return Boolean(exact) && compareParts(current, exact) === 0
82
+ }
@@ -0,0 +1,7 @@
1
+ <!--
2
+ @capyclaw/foundation 的 bundle patch:在 Cordis 树里登记内核,产出 ctx.os。
3
+ 应用插件由 loader 在各自的 apply 里挂载,不再单独产生 patch 行。
4
+ -->
5
+ - insert:
6
+ - id: capyclaw-foundation
7
+ name: '@capyclaw/foundation'
package/kit/index.mjs ADDED
@@ -0,0 +1,7 @@
1
+ /**
2
+ * libc 入口:纯函数基建,零副作用。
3
+ *
4
+ * 迁移期直接复用 @capyclaw/plugin-kit;后续把这些实现内联进 foundation 时,
5
+ * 这个入口的导出面保持不变,应用侧一行都不用改。
6
+ */
7
+ export * from '@capyclaw/plugin-kit'
@@ -0,0 +1,45 @@
1
+ /**
2
+ * http-router@1 适配器:routes(app) 往子 app 上挂路由,内核把子 app 挂到共享宿主的前缀下。
3
+ *
4
+ * 前缀冲突由 service-host 的 sharedHost.mount 检测(重复前缀直接抛错),
5
+ * 应用侧不接触 server、不选端口。
6
+ */
7
+ import { AbiError, ERROR_CODES } from '../../abi/errors.mjs'
8
+
9
+ export const kind = 'http-router@1'
10
+
11
+ export function createHttpRouterAdapter({ ctx, net, log }) {
12
+ return {
13
+ kind,
14
+ compile(impl, { spec, config }) {
15
+ if (!ctx?.serviceHost?.createApp) {
16
+ throw new AbiError(ERROR_CODES.E_CAP_MISSING,
17
+ 'ctx.serviceHost 不可用:无法创建子 app', { plugin: spec.name, capability: 'os.net' })
18
+ }
19
+
20
+ const app = ctx.serviceHost.createApp({ id: spec.name, title: spec.name })
21
+ try {
22
+ impl.routes(app, { spec, config, log })
23
+ } catch (error) {
24
+ throw new AbiError(ERROR_CODES.E_ADAPTER_THREW,
25
+ `路由注册失败:${error.message}`, { plugin: spec.name, cause: error })
26
+ }
27
+
28
+ let unmount
29
+ try {
30
+ unmount = net.mount(impl.mount, app)
31
+ } catch (error) {
32
+ throw new AbiError(ERROR_CODES.E_ADAPTER_THREW,
33
+ `挂载前缀 ${impl.mount} 失败:${error.message}`, { plugin: spec.name, prefix: impl.mount, cause: error })
34
+ }
35
+
36
+ log?.debug?.(`[foundation] ${spec.name} 路由挂到 ${impl.mount}`)
37
+ return {
38
+ app,
39
+ dispose: () => {
40
+ if (typeof unmount === 'function') unmount()
41
+ },
42
+ }
43
+ },
44
+ }
45
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * lifecycle-hook@1 的适配器。
3
+ *
4
+ * 运行时只执行 onStart / onStop;onInstall / onUpgrade 属于安装与升级流程,
5
+ * 由安装器(publisher / dsh profile 安装)在对应时点读取描述符后调用。
6
+ * 这里把它们登记进 hook 注册表,保证「声明了就找得到」。
7
+ */
8
+ export const kind = 'lifecycle-hook@1'
9
+
10
+ export function createHookRegistry() {
11
+ return {
12
+ install: [],
13
+ upgrade: [],
14
+ register(phase, entry) {
15
+ if (!this[phase]) throw new Error(`未知生命周期阶段 ${phase}`)
16
+ this[phase].push(entry)
17
+ return () => {
18
+ const index = this[phase].indexOf(entry)
19
+ if (index >= 0) this[phase].splice(index, 1)
20
+ }
21
+ },
22
+ list() {
23
+ return {
24
+ install: this.install.map((entry) => entry.plugin),
25
+ upgrade: this.upgrade.map((entry) => entry.plugin),
26
+ }
27
+ },
28
+ }
29
+ }
30
+
31
+ export function createLifecycleAdapter({ hooks, log }) {
32
+ return {
33
+ kind,
34
+ compile(impl, { spec }) {
35
+ const removers = []
36
+ if (typeof impl.onInstall === 'function') {
37
+ removers.push(hooks.register('install', { plugin: spec.name, run: impl.onInstall }))
38
+ }
39
+ if (typeof impl.onUpgrade === 'function') {
40
+ removers.push(hooks.register('upgrade', { plugin: spec.name, run: impl.onUpgrade }))
41
+ }
42
+ return {
43
+ async start() {
44
+ if (typeof impl.onStart === 'function') await impl.onStart({ plugin: spec.name })
45
+ },
46
+ dispose() {
47
+ if (typeof impl.onStop === 'function') {
48
+ try {
49
+ const result = impl.onStop({ plugin: spec.name })
50
+ if (result && typeof result.then === 'function') {
51
+ result.catch((error) => log?.error?.(`[foundation] ${spec.name} onStop 失败: ${error.message}`))
52
+ }
53
+ } catch (error) {
54
+ log?.error?.(`[foundation] ${spec.name} onStop 失败: ${error.message}`)
55
+ }
56
+ }
57
+ for (const remove of removers) remove()
58
+ },
59
+ }
60
+ },
61
+ }
62
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * runtime-driver@1 适配器:把描述符翻译成 broker 的服务规格。
3
+ *
4
+ * 应用侧不 spawn、不管重试、不做健康检查;这些由 service-broker 的 engine 负责。
5
+ * 端口由描述符或内核配置决定,应用不自己选端口。
6
+ */
7
+ import { fileURLToPath } from 'node:url'
8
+
9
+ import { AbiError, ERROR_CODES } from '../../abi/errors.mjs'
10
+
11
+ export const kind = 'runtime-driver@1'
12
+
13
+ export function createRuntimeDriverAdapter({ ctx, config, log }) {
14
+ const defaultPort = config.driverPort ?? 0
15
+
16
+ return {
17
+ kind,
18
+ compile(impl, mountContext) {
19
+ if (!ctx?.serviceBroker?.start || !ctx?.serviceBroker?.stop) {
20
+ throw new AbiError(ERROR_CODES.E_CAP_MISSING,
21
+ 'ctx.serviceBroker 不可用:无法拉起长驻进程',
22
+ { plugin: mountContext.spec.name, capability: 'os.proc' })
23
+ }
24
+ if (!mountContext?.context?.baseUrl) {
25
+ throw new AbiError(ERROR_CODES.E_BASE_URL_MISSING,
26
+ `插件 ${mountContext.spec.name} 缺少 baseUrl,无法解析 ${impl.entry}`,
27
+ { plugin: mountContext.spec.name })
28
+ }
29
+
30
+ const entryPath = fileURLToPath(new URL(impl.entry, mountContext.context.baseUrl))
31
+ const port = impl.port ?? defaultPort
32
+ const portArg = impl.portArg || '--port'
33
+ const cmd = [
34
+ process.execPath,
35
+ entryPath,
36
+ portArg,
37
+ String(port),
38
+ ...(impl.args || []),
39
+ ]
40
+
41
+ const serviceSpec = {
42
+ id: impl.id,
43
+ cmd,
44
+ ports: { main: port },
45
+ url: 'http://127.0.0.1:${PORT}/',
46
+ keepAlive: true,
47
+ }
48
+ if (impl.health?.path) serviceSpec.healthPath = impl.health.path
49
+ if (impl.dir) {
50
+ serviceSpec.cwd = fileURLToPath(new URL(impl.dir, mountContext.context.baseUrl))
51
+ }
52
+ const env = impl.env?.({ spec: mountContext.spec, config: mountContext.config })
53
+ if (env && typeof env === 'object') serviceSpec.env = env
54
+
55
+ let stopped = false
56
+ return {
57
+ serviceSpec,
58
+ async start() {
59
+ const result = await ctx.serviceBroker.start(serviceSpec)
60
+ const failure = (result?.errors || []).find((item) => item.id === impl.id)
61
+ if (failure) {
62
+ throw new AbiError(ERROR_CODES.E_ADAPTER_THREW,
63
+ `长驻进程 ${impl.id} 启动失败:${failure.error}`, { plugin: mountContext.spec.name, id: impl.id })
64
+ }
65
+ log?.info?.(`[foundation] ${impl.id} 已由 broker 拉起(port ${port})`)
66
+ },
67
+ dispose() {
68
+ if (stopped) return
69
+ stopped = true
70
+ try {
71
+ ctx.serviceBroker.stop(impl.id)
72
+ } catch (error) {
73
+ log?.warn?.(`[foundation] 停止 ${impl.id} 失败: ${error.message}`)
74
+ }
75
+ },
76
+ }
77
+ },
78
+ }
79
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * skill-provider@1 的适配器:目录 → provider → ctx.skills。
3
+ *
4
+ * 应用包只写 `skillProvider({ skill: './skills/canvas' })`,
5
+ * frontmatter 解析、契约注记、注册与释放都在这里完成。
6
+ */
7
+ import { readFileSync } from 'node:fs'
8
+ import { basename, dirname } from 'node:path'
9
+ import { fileURLToPath } from 'node:url'
10
+
11
+ import { AbiError, ERROR_CODES } from '../../abi/errors.mjs'
12
+ import { createSkillProvider } from '../skill/scan.mjs'
13
+
14
+ export const kind = 'skill-provider@1'
15
+
16
+ export function createSkillAdapter({ ctx, log }) {
17
+ return {
18
+ kind,
19
+ compile(impl, { spec, context }) {
20
+ const skills = ctx?.skills
21
+ if (!skills || typeof skills.registerProvider !== 'function') {
22
+ throw new AbiError(ERROR_CODES.E_CAP_MISSING,
23
+ 'ctx.skills.registerProvider 不可用:skills 服务未加载', { plugin: spec.name })
24
+ }
25
+ if (!context?.baseUrl) {
26
+ throw new AbiError(ERROR_CODES.E_BASE_URL_MISSING,
27
+ `插件 ${spec.name} 缺少 baseUrl,无法解析 ${impl.skill}`, { plugin: spec.name })
28
+ }
29
+
30
+ const skillPath = fileURLToPath(new URL(impl.skill, context.baseUrl))
31
+ let contract = null
32
+ if (impl.contract) {
33
+ const contractPath = fileURLToPath(new URL(impl.contract, context.baseUrl))
34
+ try {
35
+ contract = JSON.parse(readFileSync(contractPath, 'utf8'))
36
+ } catch (error) {
37
+ throw new AbiError(ERROR_CODES.E_ADAPTER_THREW,
38
+ `契约快照不可读:${contractPath}`, { plugin: spec.name, cause: error })
39
+ }
40
+ }
41
+
42
+ const provider = createSkillProvider({
43
+ assetRoot: dirname(skillPath),
44
+ skillDirs: [basename(skillPath)],
45
+ providerName: impl.name || `dsh-${spec.name}`,
46
+ rank: impl.rank,
47
+ contract,
48
+ contractNote: impl.note,
49
+ })
50
+
51
+ const dispose = skills.registerProvider(() => provider)
52
+ log?.debug?.(`[foundation] ${spec.name} 注册 skill ${impl.skill}`)
53
+ return { provider, dispose: typeof dispose === 'function' ? dispose : () => {} }
54
+ },
55
+ }
56
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * tool-provider@1 适配器:描述符里的 provide() 产出注册表,内核负责注册与反注册。
3
+ *
4
+ * 应用侧写法:
5
+ * toolProvider({ namespace: 'browser', provide: () => registry })
6
+ *
7
+ * 工具实现不经过 foundation,foundation 只决定「什么时候挂上去、挂到哪个命名空间」。
8
+ */
9
+ import { AbiError, ERROR_CODES } from '../../abi/errors.mjs'
10
+
11
+ export const kind = 'tool-provider@1'
12
+
13
+ export function createToolProviderAdapter({ ctx, log }) {
14
+ return {
15
+ kind,
16
+ compile(impl, { spec, config }) {
17
+ if (!ctx?.toolHost?.registerWithDsh) {
18
+ throw new AbiError(ERROR_CODES.E_CAP_MISSING,
19
+ 'ctx.toolHost 不可用:无法注册工具面', { plugin: spec.name, capability: 'os.tools' })
20
+ }
21
+
22
+ let registry
23
+ try {
24
+ registry = impl.provide({ spec, config, log })
25
+ } catch (error) {
26
+ throw new AbiError(ERROR_CODES.E_ADAPTER_THREW,
27
+ `工具注册表构建失败:${error.message}`, { plugin: spec.name, cause: error })
28
+ }
29
+ if (!registry || !Array.isArray(registry.TOOLS)) {
30
+ throw new AbiError(ERROR_CODES.E_ADAPTER_THREW,
31
+ 'provide() 必须返回 { TOOLS, TOOL_IMPL } 形状的注册表', { plugin: spec.name })
32
+ }
33
+
34
+ const dispose = ctx.toolHost.registerWithDsh(ctx, registry, { namespace: impl.namespace })
35
+ log?.debug?.(`[foundation] ${spec.name} 注册 ${registry.TOOLS.length} 个工具(${impl.namespace})`)
36
+ return {
37
+ registry,
38
+ dispose: typeof dispose === 'function' ? dispose : () => {},
39
+ }
40
+ },
41
+ }
42
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * ui-panel@1 适配器:把静态目录挂到共享宿主的挂载点。
3
+ *
4
+ * 文件读取、路径穿越防护、缓存头由 editor-host 负责;内核只负责解析目录与挂载。
5
+ */
6
+ import { fileURLToPath } from 'node:url'
7
+
8
+ import { AbiError, ERROR_CODES } from '../../abi/errors.mjs'
9
+
10
+ export const kind = 'ui-panel@1'
11
+
12
+ export function createUiPanelAdapter({ ctx, net, log }) {
13
+ return {
14
+ kind,
15
+ compile(impl, { spec, config, context }) {
16
+ if (!ctx?.serviceHost?.createApp || !ctx?.editorHost?.createEditorHost) {
17
+ throw new AbiError(ERROR_CODES.E_CAP_MISSING,
18
+ 'ctx.editorHost / ctx.serviceHost 不可用:无法挂静态面板',
19
+ { plugin: spec.name, capability: 'os.ui' })
20
+ }
21
+ if (!context?.baseUrl) {
22
+ throw new AbiError(ERROR_CODES.E_BASE_URL_MISSING,
23
+ `插件 ${spec.name} 缺少 baseUrl,无法解析 ${impl.root}`, { plugin: spec.name })
24
+ }
25
+
26
+ const dir = fileURLToPath(new URL(impl.root, context.baseUrl))
27
+ const editor = ctx.editorHost.createEditorHost({
28
+ id: spec.name,
29
+ dir,
30
+ basePath: impl.mount,
31
+ index: impl.index,
32
+ })
33
+
34
+ const app = ctx.serviceHost.createApp({ id: spec.name, title: spec.name })
35
+ for (const route of editor.routes()) app.use(route)
36
+
37
+ let unmount
38
+ try {
39
+ unmount = net.mount(impl.mount, app)
40
+ } catch (error) {
41
+ throw new AbiError(ERROR_CODES.E_ADAPTER_THREW,
42
+ `挂载面板 ${impl.mount} 失败:${error.message}`, { plugin: spec.name, prefix: impl.mount, cause: error })
43
+ }
44
+
45
+ log?.debug?.(`[foundation] ${spec.name} 面板挂到 ${impl.mount}`)
46
+ return {
47
+ app,
48
+ editor,
49
+ dispose: () => {
50
+ if (typeof unmount === 'function') unmount()
51
+ },
52
+ }
53
+ },
54
+ }
55
+ }