@capyclaw/service-broker 0.1.0 → 0.2.1

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 CHANGED
@@ -1,4 +1,4 @@
1
- # dsh-service-broker
1
+ # @capyclaw/service-broker(服务编排 broker)
2
2
 
3
3
  DSH 服务中间件。服务解耦标准的执行者:**启动清单/后端/用户不直接拉服务,
4
4
  一切结构化命令投递到这里,由中间件把每个服务作为独立进程拉起、守护、登记。**
@@ -12,6 +12,15 @@ DSH 服务中间件。服务解耦标准的执行者:**启动清单/后端/用
12
12
  | 跨端口调动交给中间件 | `requires` 拓扑、端口分配、复用/冲突仲裁、discovery 都在这里 |
13
13
  | 不硬耦合 | 引擎零 dsh 依赖;embedded(cordis 插件壳)与 standalone(独立端口)同一份逻辑 |
14
14
 
15
+ ## 变更记录
16
+
17
+ - **0.2.0**(2026-11-04):新增**统一服务 spec**——`service.schema.json`(JSON Schema)
18
+ 与 `lib/spec.mjs`(代码面校验 `validateServiceSpec`/`validateServiceManifest`),
19
+ `loadManifest` 在装载期拒绝结构非法的清单(同清单重复 id 也在此报错);
20
+ 新增 exports `./spec`、`./schema`。`stack-services.json`(dsh-stack-kit 编译)与
21
+ `service-manifest.json`(@capyclaw/suite-kit 物化)自此共用同一份 spec 契约。
22
+ - 0.1.0:初始发布。
23
+
15
24
  ## 两种形态
16
25
 
17
26
  ### standalone(桌面/外部场景的主形态)
package/dsh-suite.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
- "$schema": "../dsh-suite-kit/suite.schema.json",
2
+ "$schema": "../@capyclaw/suite-kit/suite.schema.json",
3
3
  "name": "dsh-service-broker-suite",
4
- "version": "0.1.0",
4
+ "version": "0.2.0",
5
5
  "title": "Service Broker",
6
6
  "description": "服务中间件:按清单/结构化命令以独立进程拉起各服务,负责守护、发现与控制端点。",
7
7
  "packages": {
8
8
  "dsh-service-broker": {
9
- "role": "middleware",
10
- "version": "^0.1.0",
9
+ "role": "runtime",
10
+ "version": "^0.2.0",
11
11
  "path": ".",
12
12
  "provides": {
13
13
  "services": ["serviceBroker"],
14
14
  "capabilities": { "service.broker": "1.0.0" },
15
- "ports": ["auto"]
15
+ "ports": []
16
16
  }
17
17
  }
18
18
  },
package/lib/engine.mjs CHANGED
@@ -23,7 +23,7 @@ const VAR_RE = /\$\{([^{}]+)\}/g
23
23
  const IS_WIN = process.platform === 'win32'
24
24
 
25
25
  /* ------------------------------------------------------------------ *
26
- * 基础探测原语(与 dsh-suite-kit/src/runtime.mjs 同口径)
26
+ * 基础探测原语(与 @capyclaw/suite-kit/src/runtime.mjs 同口径)
27
27
  * ------------------------------------------------------------------ */
28
28
 
29
29
  export function pidAlive(pid) {
package/lib/manifest.mjs CHANGED
@@ -9,6 +9,7 @@
9
9
  import { existsSync, readFileSync } from 'node:fs'
10
10
  import path from 'node:path'
11
11
  import { fileURLToPath } from 'node:url'
12
+ import { validateServiceManifest } from './spec.mjs'
12
13
 
13
14
  export const PKG_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
14
15
 
@@ -23,6 +24,8 @@ export function loadManifest(manifestPath) {
23
24
  const raw = JSON.parse(readFileSync(file, 'utf8'))
24
25
  const services = Array.isArray(raw) ? raw : raw.services
25
26
  if (!Array.isArray(services)) throw new Error(`manifest 格式非法(缺 services 数组): ${file}`)
27
+ const specErrors = validateServiceManifest(services, file)
28
+ if (specErrors.length) throw new Error(`manifest 服务 spec 非法:\n- ${specErrors.join('\n- ')}`)
26
29
  const base = path.dirname(file)
27
30
  return services.map((service) => expandPackageDir(service, base))
28
31
  }
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * 技能内容(SKILL.md 正文)不再通过 ctx.skills.registerProvider 进程内注册,
5
5
  * 而是从独立技能服务拉取——dsh 进程里只剩这一个瘦适配层。
6
- * 服务端形状见 bridges/ppt-bridge/dsh-skills-service(GET /skills、GET /skills/:name)。
6
+ * 服务端形状见 bridges/ppt-bridge/capyclaw-skills-service(GET /skills、GET /skills/:name)。
7
7
  */
8
8
  export function createSkillsClient(baseUrl) {
9
9
  const root = String(baseUrl || '').replace(/\/$/, '')
package/lib/spec.mjs ADDED
@@ -0,0 +1,78 @@
1
+ /**
2
+ * 服务 spec 校验:service.schema.json 的代码面。
3
+ *
4
+ * stack-services.json(dsh-stack-kit 编译)与 service-manifest.json
5
+ * (@capyclaw/suite-kit 物化)共用这一份 spec;本模块是装载期与跨包测试
6
+ * 共同调用的唯一校验器(结构级——语义由 engine/discovery 在运行期裁决)。
7
+ */
8
+ const ID_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/
9
+
10
+ function checkPortValue(value, label, errors) {
11
+ if (value === undefined) return
12
+ if (value === 'auto') return
13
+ if (!Number.isInteger(value) || value <= 0 || value > 65535) {
14
+ errors.push(`${label} 必须是 1-65535 整数或 "auto"`)
15
+ }
16
+ }
17
+
18
+ /** 校验单个服务 spec;返回错误数组(空 = 通过)。 */
19
+ export function validateServiceSpec(service, at = 'service') {
20
+ const errors = []
21
+ if (!service || typeof service !== 'object' || Array.isArray(service)) {
22
+ return [`${at} 必须是对象`]
23
+ }
24
+ if (typeof service.id !== 'string' || !ID_RE.test(service.id)) {
25
+ errors.push(`${at}.id 必须是非空的服务标识(字母数字开头,可含 . _ -)`)
26
+ }
27
+ if (!Array.isArray(service.cmd) || service.cmd.length === 0 || service.cmd.some((item) => typeof item !== 'string')) {
28
+ errors.push(`${at}.cmd 必须是非空 string[](完整命令行)`)
29
+ }
30
+ if (service.ports !== undefined) {
31
+ if (typeof service.ports !== 'object' || service.ports === null || Array.isArray(service.ports)) {
32
+ errors.push(`${at}.ports 必须是对象(main 或具名端口)`)
33
+ } else {
34
+ for (const [name, value] of Object.entries(service.ports)) {
35
+ checkPortValue(value, `${at}.ports.${name}`, errors)
36
+ }
37
+ }
38
+ }
39
+ if (service.env !== undefined && (typeof service.env !== 'object' || service.env === null
40
+ || Array.isArray(service.env) || Object.values(service.env).some((value) => typeof value !== 'string'))) {
41
+ errors.push(`${at}.env 必须是 string -> string 对象`)
42
+ }
43
+ if (service.keepAlive !== undefined && typeof service.keepAlive !== 'boolean') {
44
+ errors.push(`${at}.keepAlive 必须是 boolean`)
45
+ }
46
+ if (service.healthPath !== undefined && typeof service.healthPath !== 'string') {
47
+ errors.push(`${at}.healthPath 必须是 string`)
48
+ }
49
+ if (service.url !== undefined && typeof service.url !== 'string') errors.push(`${at}.url 必须是 string`)
50
+ if (service.cwd !== undefined && typeof service.cwd !== 'string') errors.push(`${at}.cwd 必须是 string`)
51
+ if (service.logFile !== undefined && typeof service.logFile !== 'string') errors.push(`${at}.logFile 必须是 string`)
52
+ if (service.requires !== undefined && (!Array.isArray(service.requires)
53
+ || service.requires.some((item) => typeof item !== 'string'))) {
54
+ errors.push(`${at}.requires 必须是 string[]`)
55
+ }
56
+ if (service.portFrom !== undefined && !['content', 'urlPort'].includes(service.portFrom)) {
57
+ errors.push(`${at}.portFrom 只允许 content | urlPort`)
58
+ }
59
+ if (service.dirFrom !== undefined && service.dirFrom !== 'contentState') {
60
+ errors.push(`${at}.dirFrom 只允许 contentState`)
61
+ }
62
+ return errors
63
+ }
64
+
65
+ /** 校验整份清单(services 数组 + 同清单 id 唯一);返回错误数组(空 = 通过)。 */
66
+ export function validateServiceManifest(services, label = 'manifest') {
67
+ if (!Array.isArray(services)) return [`${label}: services 必须是数组`]
68
+ const errors = []
69
+ const seen = new Set()
70
+ services.forEach((service, index) => {
71
+ errors.push(...validateServiceSpec(service, `${label}[${index}]`))
72
+ if (service && typeof service === 'object' && typeof service.id === 'string') {
73
+ if (seen.has(service.id)) errors.push(`${label}: 重复的服务 id ${service.id}`)
74
+ seen.add(service.id)
75
+ }
76
+ })
77
+ return errors
78
+ }
package/package.json CHANGED
@@ -1,31 +1,58 @@
1
1
  {
2
2
  "name": "@capyclaw/service-broker",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "DSH 服务中间件(唯一进程内插件):按清单/结构化命令把各服务作为独立进程拉起,负责守护、发现(discovery)、控制端点;服务与服务之间不互相带动,跨端口组织一律经过这里。",
5
5
  "type": "module",
6
6
  "main": "./lib/host.mjs",
7
7
  "exports": {
8
8
  ".": "./lib/host.mjs",
9
9
  "./engine": "./lib/engine.mjs",
10
+ "./manifest": "./lib/manifest.mjs",
11
+ "./spec": "./lib/spec.mjs",
12
+ "./schema": "./service.schema.json",
10
13
  "./package.json": "./package.json"
11
14
  },
15
+ "engines": {
16
+ "node": ">=18",
17
+ "dsh": "^0.2.0-rc.2"
18
+ },
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
12
22
  "dsh": {
13
23
  "bundle": {
14
24
  "patch": "./cordis.patch.yml"
15
25
  },
16
26
  "suite": "./dsh-suite.json",
17
- "apiVersion": "1.0.0"
27
+ "apiVersion": "1.0.0",
28
+ "category": "service",
29
+ "platform": "both"
30
+ },
31
+ "meta": {
32
+ "title": "服务中间件",
33
+ "description": "按清单把各服务作为独立进程拉起,守护、发现与控制端点;服务之间不互相带动"
18
34
  },
19
35
  "files": [
20
36
  "lib/",
21
37
  "server.mjs",
22
38
  "services.json",
39
+ "service.schema.json",
23
40
  "cordis.patch.yml",
24
41
  "dsh-suite.json",
25
42
  "README.md"
26
43
  ],
27
44
  "license": "MIT",
28
- "scripts": {
29
- "test": "node --test test/unit.test.mjs test/e2e.test.mjs test/stability.test.mjs test/adopt-detached.test.mjs"
30
- }
45
+ "scripts": {},
46
+ "dependencies": {
47
+ "@capyclaw/plugin-kit": "^0.2.0"
48
+ },
49
+ "repository": {
50
+ "type": "git",
51
+ "url": "git+https://github.com/Maple496/capyclaw.git",
52
+ "directory": "packages/capyclaw-service-broker"
53
+ },
54
+ "bugs": {
55
+ "url": "https://github.com/Maple496/capyclaw/issues"
56
+ },
57
+ "homepage": "https://github.com/Maple496/capyclaw/tree/main/packages/capyclaw-service-broker#readme"
31
58
  }
@@ -0,0 +1,42 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://dsh.local/schemas/service.schema.json",
4
+ "title": "DSH Broker 服务清单(唯一服务 spec)",
5
+ "description": "stack-services.json(dsh-stack-kit 编译)与 service-manifest.json(@capyclaw/suite-kit 物化)是同一 spec 的两个 runtime 工件;本 schema 是二者共同的契约。${PACKAGE_DIR}/${PORT}/${PORT_<NAME>} 占位符由装载/仲裁层展开。",
6
+ "type": "object",
7
+ "required": ["services"],
8
+ "properties": {
9
+ "services": { "type": "array", "items": { "$ref": "#/$defs/service" } }
10
+ },
11
+ "$defs": {
12
+ "port": {
13
+ "anyOf": [
14
+ { "type": "integer", "minimum": 1, "maximum": 65535 },
15
+ { "type": "string", "enum": ["auto"] }
16
+ ]
17
+ },
18
+ "service": {
19
+ "type": "object",
20
+ "required": ["id", "cmd"],
21
+ "additionalProperties": true,
22
+ "properties": {
23
+ "id": { "type": "string", "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._-]*$", "description": "服务 id(同清单内唯一)" },
24
+ "cmd": { "type": "array", "items": { "type": "string" }, "minItems": 1, "description": "完整命令行(占位符原样保留,broker 展开分配)" },
25
+ "ports": {
26
+ "type": "object",
27
+ "additionalProperties": { "$ref": "#/$defs/port" },
28
+ "description": "main 为主端口;具名端口映射 PORT_<NAME>"
29
+ },
30
+ "url": { "type": "string", "description": "服务入口 URL 模板" },
31
+ "healthPath": { "type": "string", "description": "健康探测路径(默认 /healthz)" },
32
+ "keepAlive": { "type": "boolean", "description": "进程死亡后由 broker 原端口复活" },
33
+ "env": { "type": "object", "additionalProperties": { "type": "string" } },
34
+ "cwd": { "type": "string" },
35
+ "requires": { "type": "array", "items": { "type": "string" }, "description": "同清单内依赖的服务 id" },
36
+ "logFile": { "type": "string" },
37
+ "portFrom": { "type": "string", "enum": ["content", "urlPort"], "description": "suite 物化期提示:主端口取 runtime content/urlPort(物化后删除)" },
38
+ "dirFrom": { "type": "string", "enum": ["contentState"], "description": "suite 物化期提示:--dir 参数指向 runtime contentState(物化后删除)" }
39
+ }
40
+ }
41
+ }
42
+ }