create-windy 0.2.19 → 0.2.21

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.
Files changed (127) hide show
  1. package/README.md +5 -1
  2. package/dist/cli.js +392 -160
  3. package/package.json +1 -1
  4. package/template/.windy-template.json +2 -2
  5. package/template/README.md +1 -0
  6. package/template/apps/agent-server/Dockerfile +20 -0
  7. package/template/apps/agent-server/README.md +46 -0
  8. package/template/apps/agent-server/pyproject.toml +44 -0
  9. package/template/apps/agent-server/src/southwind_agent_server/__init__.py +5 -0
  10. package/template/apps/agent-server/src/southwind_agent_server/adk_adapter.py +39 -0
  11. package/template/apps/agent-server/src/southwind_agent_server/app.py +263 -0
  12. package/template/apps/agent-server/src/southwind_agent_server/config.py +141 -0
  13. package/template/apps/agent-server/src/southwind_agent_server/contracts.py +99 -0
  14. package/template/apps/agent-server/src/southwind_agent_server/deterministic.py +52 -0
  15. package/template/apps/agent-server/src/southwind_agent_server/engine.py +85 -0
  16. package/template/apps/agent-server/src/southwind_agent_server/openai_engine.py +197 -0
  17. package/template/apps/agent-server/src/southwind_agent_server/openai_provider.py +246 -0
  18. package/template/apps/agent-server/src/southwind_agent_server/service.py +180 -0
  19. package/template/apps/agent-server/src/southwind_agent_server/sqlite_store.py +317 -0
  20. package/template/apps/agent-server/src/southwind_agent_server/sse.py +41 -0
  21. package/template/apps/agent-server/src/southwind_agent_server/store.py +144 -0
  22. package/template/apps/agent-server/src/southwind_agent_server/tool_host.py +80 -0
  23. package/template/apps/agent-server/tests/test_api.py +207 -0
  24. package/template/apps/agent-server/tests/test_config.py +56 -0
  25. package/template/apps/agent-server/tests/test_openai_provider.py +224 -0
  26. package/template/apps/agent-server/tests/test_sqlite_store.py +93 -0
  27. package/template/apps/agent-server/uv.lock +925 -0
  28. package/template/apps/server/src/agent/execution-grants.ts +89 -0
  29. package/template/apps/server/src/agent/remote-runtime.ts +128 -0
  30. package/template/apps/server/src/agent/run-contracts.ts +43 -0
  31. package/template/apps/server/src/agent/run-facade.test.ts +257 -0
  32. package/template/apps/server/src/agent/run-facade.ts +190 -0
  33. package/template/apps/server/src/agent/run-routes.ts +222 -0
  34. package/template/apps/server/src/agent/runtime-bootstrap.ts +53 -0
  35. package/template/apps/server/src/agent/tool-host.test.ts +242 -0
  36. package/template/apps/server/src/agent/tool-host.ts +153 -0
  37. package/template/apps/server/src/application-services.ts +2 -2
  38. package/template/apps/server/src/audit/search-summary.ts +15 -8
  39. package/template/apps/server/src/example-modules.ts +58 -70
  40. package/template/apps/server/src/foundation.ts +8 -4
  41. package/template/apps/server/src/guards.test.ts +13 -0
  42. package/template/apps/server/src/guards.ts +12 -2
  43. package/template/apps/server/src/index.ts +48 -95
  44. package/template/apps/server/src/installed-business-modules.ts +23 -4
  45. package/template/apps/server/src/module-bootstrap.ts +83 -0
  46. package/template/apps/server/src/module-composition.test.ts +54 -1
  47. package/template/apps/server/src/module-composition.ts +58 -0
  48. package/template/apps/server/src/module-host/initialize-contracts.ts +67 -0
  49. package/template/apps/server/src/module-host/initialize.test.ts +192 -0
  50. package/template/apps/server/src/module-host/initialize.ts +328 -0
  51. package/template/apps/server/src/module-host/request-context.test.ts +66 -0
  52. package/template/apps/server/src/module-host/request-context.ts +156 -0
  53. package/template/apps/server/src/module-host/role-presets.test.ts +94 -0
  54. package/template/apps/server/src/module-host/role-presets.ts +76 -0
  55. package/template/apps/server/src/module-host/route-installer.test.ts +317 -0
  56. package/template/apps/server/src/module-host/route-installer.ts +97 -0
  57. package/template/apps/server/src/module-host/route-registration.test.ts +160 -0
  58. package/template/apps/server/src/module-host/search-providers.ts +31 -0
  59. package/template/apps/server/src/module-host/storage-port.test.ts +165 -0
  60. package/template/apps/server/src/module-host/storage-port.ts +59 -0
  61. package/template/apps/server/src/module-host/task-definitions.ts +41 -0
  62. package/template/apps/server/src/module-host/test-fixtures.ts +26 -0
  63. package/template/apps/server/src/module-host/tool-handlers.ts +25 -0
  64. package/template/apps/server/src/module-host.test.ts +202 -58
  65. package/template/apps/server/src/module-host.ts +35 -113
  66. package/template/apps/server/src/module-runtime-validation.ts +40 -0
  67. package/template/apps/server/src/route-guards.test.ts +112 -1
  68. package/template/apps/server/src/runtime-feature.ts +66 -31
  69. package/template/apps/server/src/runtime.test.ts +1 -0
  70. package/template/apps/server/src/runtime.ts +3 -1
  71. package/template/apps/server/src/system/built-in-roles.ts +1 -1
  72. package/template/apps/server/src/work-order/foundation-modules.test.ts +30 -10
  73. package/template/apps/server/src/work-order/routes.test.ts +30 -18
  74. package/template/apps/server/src/work-order/routes.ts +104 -135
  75. package/template/apps/server/src/work-order/search-api.test.ts +14 -3
  76. package/template/apps/server/src/work-order/search-provider.test.ts +23 -15
  77. package/template/apps/server/src/work-order/search-provider.ts +17 -12
  78. package/template/apps/server/src/work-order/task.test.ts +16 -11
  79. package/template/apps/web/Dockerfile +4 -1
  80. package/template/apps/web/src/layout/GlobalWatermark.layering.webtest.ts +4 -1
  81. package/template/apps/web/src/layout/GlobalWatermark.vue +2 -0
  82. package/template/apps/web/src/layout/GlobalWatermark.webtest.ts +4 -1
  83. package/template/docker-compose.yml +25 -0
  84. package/template/docs/architecture/ai-runtime.md +744 -0
  85. package/template/docs/architecture/object-storage.md +12 -0
  86. package/template/docs/platform/agent-runtime.md +128 -0
  87. package/template/docs/platform/agent-tools.md +8 -1
  88. package/template/package.json +1 -0
  89. package/template/packages/config/index.test.ts +100 -0
  90. package/template/packages/config/index.ts +1 -0
  91. package/template/packages/config/package.json +2 -1
  92. package/template/packages/config/src/ai-openai-compatible.ts +131 -0
  93. package/template/packages/config/src/platform.ts +28 -1
  94. package/template/packages/database/package.json +1 -1
  95. package/template/packages/database/src/runner.ts +7 -9
  96. package/template/packages/example-work-order/index.ts +1 -0
  97. package/template/packages/example-work-order/package.json +2 -0
  98. package/template/packages/example-work-order/src/server-module.ts +61 -0
  99. package/template/packages/jobs/package.json +1 -1
  100. package/template/packages/jobs/src/types.ts +7 -1
  101. package/template/packages/modules/package.json +1 -1
  102. package/template/packages/modules/src/catalog-validation.test.ts +212 -0
  103. package/template/packages/modules/src/catalog-validation.ts +19 -2
  104. package/template/packages/modules/src/migration-bundle-validation.test.ts +191 -0
  105. package/template/packages/server-sdk/index.ts +53 -0
  106. package/template/packages/server-sdk/package.json +18 -3
  107. package/template/packages/server-sdk/src/actor.ts +15 -0
  108. package/template/packages/server-sdk/src/ai-operation-registration.ts +31 -0
  109. package/template/packages/server-sdk/src/audit-port.ts +22 -0
  110. package/template/packages/server-sdk/src/jobs-port.ts +24 -0
  111. package/template/packages/server-sdk/src/module-host.ts +41 -1
  112. package/template/packages/server-sdk/src/namespace.test.ts +26 -0
  113. package/template/packages/server-sdk/src/namespace.ts +13 -0
  114. package/template/packages/server-sdk/src/request-context.ts +21 -0
  115. package/template/packages/server-sdk/src/role-preset.ts +18 -0
  116. package/template/packages/server-sdk/src/route-definition.ts +26 -0
  117. package/template/packages/server-sdk/src/search-provider-port.ts +48 -0
  118. package/template/packages/server-sdk/src/storage-port.ts +25 -0
  119. package/template/packages/server-sdk/src/task-registration.ts +18 -0
  120. package/template/packages/server-sdk/src/tool-host-port.ts +22 -0
  121. package/template/packages/server-sdk/src/tool-registration.ts +29 -0
  122. package/template/packages/shared/index.ts +1 -0
  123. package/template/packages/shared/package.json +1 -1
  124. package/template/packages/shared/src/license-catalog.test.ts +73 -1
  125. package/template/packages/shared/src/license-catalog.ts +95 -0
  126. package/template/packages/{database/src/bundle-validation.ts → shared/src/migration-bundle-validation.ts} +2 -1
  127. package/template/apps/server/src/work-order/task.ts +0 -30
@@ -58,8 +58,20 @@ Logo 使用 `branding-logo` 上传策略。匿名 `/api/platform/files/:id` 只
58
58
  - 删除为幂等操作;第一次存在时返回 `true`,重复删除返回 `false`。
59
59
  - 进程在 Blob 发布与元数据发布之间退出时可能产生孤儿内容;上传生命周期模块基于过期时间、引用集合与 grace period 清理。
60
60
 
61
+ ## 模块受限 Upload/BlobRead Port
62
+
63
+ 业务模块不直接使用 `UploadService` / `FileService`,而是经 `ModuleRequestContext.storage` 获得 `ModuleStoragePort`(宿主实现见 `apps/server/src/module-host/storage-port.ts`):
64
+
65
+ - `createUploadSession` 的 purpose 强制属于本模块经 Host 注册的 Upload Policy;跨模块用途和平台基础用途(如 `bulk-data`、`branding-logo`)一律以 `UPLOAD_FORBIDDEN` 拒绝。Owner 由宿主以当前 Actor 补齐,模块无法代他人创建会话。
66
+ - `openFile` 走 `FileService.open` 的 Owner 约束,并在返回前校验文件用途属于本模块——即使是本人文件,用途越界也拒绝。
67
+ - `openPublicFile` 走 `FileService.openPublic` 的 Purpose 约束,且只允许本模块已注册用途。
68
+ - 模块不直接接触 `BlobStore` 或 Upload Repository;宿主未挂载存储服务时,任何访问都以明确错误失败,不静默绕过。
69
+
70
+ 用途唯一性仍在 Host 注册时全局校验:命名空间重叠的模块(如 `shop` 与 `shop.sub`)注册同一 purpose 会在启动时失败。
71
+
61
72
  ## 当前切片
62
73
 
63
74
  - F1:Blob 端口、SHA-256、随机引用和本地流式 Adapter 已完成。
64
75
  - F2:上传会话、分片续传、文件元数据、校验和清理已完成。
65
76
  - F3:服务端授权下载、审计、bulk artifact 与 Logo 接入已完成。
77
+ - F4:模块受限 Upload/BlobRead Port 已完成,业务模块经 Host 注册用途并受命名空间约束。
@@ -0,0 +1,128 @@
1
+ # Agent Runtime 接入与运行
2
+
3
+ 更新时间:2026-07-25。
4
+
5
+ Windy 的 Agent Runtime 是可选平台能力。业务模块声明稳定的 AI Operation 和允许调用的
6
+ Tool;Southwind AI Server 负责身份、Feature、Permission、License、Scope、Execution
7
+ Grant 与审计;独立 Agent Server 只负责模型循环、canonical event 和受限 Tool Host
8
+ 回调。业务模块不选择 Provider、模型、Endpoint 或凭据。
9
+
10
+ ## 当前可用边界
11
+
12
+ - OpenAI-compatible `/chat/completions` Provider,支持流式文本和 function calling。
13
+ - `QWEN_API_KEY` 使用服务端 `SecretRef`,只在 Provider Adapter 发请求前解析。
14
+ - 公网 Endpoint 必须为 HTTPS;`localhost`、容器服务名、RFC 1918 地址和
15
+ `.internal`/`.local` 主机可使用 HTTP。URL 禁止内嵌凭据、query、fragment 和云
16
+ metadata 地址。
17
+ - Provider 有连接/请求超时、响应大小上限、安全错误映射和 `/models` readiness 检查。
18
+ - Run 支持幂等创建、canonical SSE、cursor/`Last-Event-ID` 重放、取消和唯一终态。
19
+ - SQLite Store 为单副本有界持久化:默认最多 10,000 个 Run、每个 Run 2,000 个事件、
20
+ 16 MiB 输出、终态保留 7 天。它不保存原始输入,只保存请求指纹;进程重启会把未完成
21
+ Run 收敛为一个可重试失败终态。
22
+
23
+ SQLite 模式不能作为多副本共享 Store。需要水平扩容时,必须先提供实现同一 `RunStore`
24
+ 契约的外部持久化 Adapter。
25
+
26
+ ## Module 接入
27
+
28
+ Manifest 的 `aiOperations` 是声明事实。每个 Operation 必须声明自己的 Feature、
29
+ Permission、可选 Entitlement、输入/输出 schema 和 `allowedToolKeys`。模型只会收到该
30
+ 集合内的 Tool 定义。
31
+
32
+ 模块在 `setup(host)` 中注册模型无关的输入准备器:
33
+
34
+ ```ts
35
+ host.registerAiOperationHandlers([
36
+ {
37
+ operationKey: "example.summary.generate",
38
+ prepare(input, context) {
39
+ return {
40
+ instructions: "只根据用户提供的数据生成简洁摘要。",
41
+ userPrompt: JSON.stringify({
42
+ input,
43
+ actorId: context.actor.id,
44
+ }),
45
+ };
46
+ },
47
+ },
48
+ ]);
49
+ ```
50
+
51
+ 不要把 Permission、Feature、License、Scope、Execution Grant、Provider 或 API Key
52
+ 放进返回值。`prepare` 只负责把已经过宿主 Guard 的类型化输入转成指令和用户消息。
53
+
54
+ Tool handler 仍通过 `registerToolHandlers` 注册。模型提出 Tool Call 后,Agent Server
55
+ 携带短时 opaque grant 回调 Southwind AI Server;受限 Tool Host 会验证:
56
+
57
+ 1. grant 是否绑定当前 Run、Operation、Actor 且未过期;
58
+ 2. Tool 是否属于 Operation 的 `allowedToolKeys`;
59
+ 3. Operation 与 Tool 当前的 Feature、Permission 和 License;
60
+ 4. 宿主创建的 Actor 与请求 Scope。
61
+
62
+ Tool 请求不能携带权限列表、License、Scope 或“已授权”标志。客户端或模型提供的这些
63
+ 字段会被拒绝,未知 Tool 也不会形成注册表探测通道。
64
+
65
+ ## 本地 Compose profile
66
+
67
+ 选择 `system.agent` 的 create-windy 项目会生成 `agent` profile 和随机内部 Token。
68
+ 在 `.env` 中填写:
69
+
70
+ ```dotenv
71
+ PLATFORM_ENABLE_AI=true
72
+ AGENT_SERVER_INTERNAL_TOKEN=<至少 12 字符的随机值>
73
+ QWEN_API_URL=https://dashscope-intl.aliyuncs.com/compatible-mode/v1
74
+ QWEN_API_KEY=<服务端密钥>
75
+ QWEN_MODEL=qwen-plus
76
+
77
+ # 可选安全上限
78
+ # AI_PROVIDER_CONNECT_TIMEOUT_MS=5000
79
+ # AI_PROVIDER_REQUEST_TIMEOUT_MS=60000
80
+ # AI_PROVIDER_MAX_RESPONSE_BYTES=2097152
81
+ ```
82
+
83
+ 然后启动:
84
+
85
+ ```bash
86
+ bun run dev:agent
87
+ ```
88
+
89
+ `QWEN_*` 只注入 `agent-server` 容器;Web service 没有这些环境变量。不要增加 `VITE_`
90
+ 前缀,也不要把 `.env`、日志、错误响应或截图中的真实 Key 提交到仓库。
91
+
92
+ 健康检查:
93
+
94
+ ```text
95
+ GET http://agent-server:8080/health/live
96
+ GET http://agent-server:8080/health/ready
97
+ ```
98
+
99
+ `ready` 会访问 Provider 的 `/models`。失败只返回稳定错误,不回显 Provider body、
100
+ Authorization、Secret、Endpoint query 或底层异常文本。
101
+
102
+ ## 客户端协议
103
+
104
+ 浏览器继续使用 `@southwind-ai/ai-client` 访问同源 Southwind AI Server:
105
+
106
+ ```text
107
+ POST /api/agent/runs
108
+ GET /api/agent/runs/:runId/events
109
+ GET /api/agent/runs/:runId/result
110
+ POST /api/agent/runs/:runId/cancel
111
+ ```
112
+
113
+ SSE 事件包括 `output.delta`、`tool.requested`、`tool.completed`、usage,以及唯一一个
114
+ `run.completed`、`run.failed` 或 `run.cancelled` 终态。断线后使用已确认 sequence 作为
115
+ cursor 恢复;不要重新创建 Run 或重复执行 Tool。
116
+
117
+ ## 从 create-windy 0.2.20 升级
118
+
119
+ 先检查计划,再应用连续 recipe:
120
+
121
+ ```bash
122
+ bunx --bun create-windy@0.2.21 update --check
123
+ bunx --bun create-windy@0.2.21 update
124
+ bun install
125
+ ```
126
+
127
+ 升级器会更新受管模板文件;本地修改产生冲突时会 fail closed 并保留事务备份,不会静默
128
+ 覆盖。升级后按上面的环境变量启用 `agent` profile。
@@ -10,6 +10,11 @@ Server 端已提供 Agent Tool 的最小闭环:
10
10
  - 执行前使用 Permission Guard 校验 `permissionKey`。
11
11
  - 权限拒绝记录 `security.denied` 审计事件。
12
12
  - 执行成功记录 `agent.tool.execute` 审计事件。
13
+ - ModuleHost 通过 `registerAiOperationHandlers` 注册模型无关 Operation plan。
14
+ - Guarded Run facade 只把 Operation `allowedToolKeys` 中的定义交给模型。
15
+ - Agent Server 的 Tool Call 必须通过短时 Execution Grant 回到受限 Tool Host。
16
+ - Tool Host 使用宿主绑定的 Actor 与 Scope 重新执行 Operation/Tool Guard;客户端和模型
17
+ 不能传权限、License、Scope 或授权结论。
13
18
 
14
19
  OpenAPI 文档由 `@elysiajs/swagger` 提供,随 Server 启动自动暴露。
15
20
 
@@ -34,4 +39,6 @@ Authorization: Bearer dev-admin-token
34
39
  - 工具执行复用系统资源 Repository;未配置 `DATABASE_URL` 时使用内存仓储,配置后使用 Drizzle 仓储。
35
40
  - 工具参数当前只支持基础分页参数。
36
41
  - 审计事件写入内存 sink;配置 `DATABASE_URL` 后会通过有界重试写入 PostgreSQL `audit_logs`,持续失败会增加计数并输出告警,审计查询优先读取该表。详细降级语义见 [平台审计可靠写入](./audit-reliability.md)。
37
- - 后续应继续完善 Agent Tool 的结构化输入 schema 和执行结果追踪。
42
+ - Agent Runtime、环境变量、SSE Compose profile 见
43
+ [Agent Runtime 接入与运行](./agent-runtime.md)。
44
+ - 后续应继续完善 Agent Tool 的结构化输入 schema、写操作幂等和执行结果追踪。
@@ -15,6 +15,7 @@
15
15
  "dev:host": "bun run scripts/dev.ts",
16
16
  "dev:web": "bun run --cwd apps/web dev",
17
17
  "dev:server": "bun run --cwd apps/server dev",
18
+ "dev:agent": "docker compose --profile agent up --build",
18
19
  "dev:license": "bun run --cwd apps/license dev",
19
20
  "dev:license:docker": "docker compose --profile license up license",
20
21
  "dev:signing": "bun run --cwd apps/signing-service dev",
@@ -4,9 +4,11 @@ import {
4
4
  ConfigLoadError,
5
5
  defineModuleConfig,
6
6
  EnvironmentSecretResolver,
7
+ loadOpenAiCompatibleDeploymentConfig,
7
8
  loadModuleConfigSync,
8
9
  loadPlatformConfig,
9
10
  ModuleConfigRegistry,
11
+ normalizeOpenAiCompatibleBaseUrl,
10
12
  SecretRef,
11
13
  strictBoolean,
12
14
  } from "./index.js";
@@ -49,6 +51,63 @@ test("strict loader 对 unknown 与错误类型 fail-fast 且不回显输入值"
49
51
  }
50
52
  });
51
53
 
54
+ test("WINDY_LICENSE_VERSIONS 解析宿主自定义 License 版本目录", () => {
55
+ const config = loadPlatformConfig({
56
+ WINDY_LICENSE_VERSIONS: JSON.stringify([
57
+ { key: "oem-basic", label: "OEM 基础版", order: 10 },
58
+ {
59
+ key: "oem-pro",
60
+ label: "OEM 专业版",
61
+ description: "含高级治理能力",
62
+ order: 20,
63
+ },
64
+ ]),
65
+ });
66
+
67
+ expect(config.license.versions).toEqual([
68
+ { key: "oem-basic", label: "OEM 基础版", description: "", order: 10 },
69
+ {
70
+ key: "oem-pro",
71
+ label: "OEM 专业版",
72
+ description: "含高级治理能力",
73
+ order: 20,
74
+ },
75
+ ]);
76
+ expect(loadPlatformConfig({}).license.versions).toBeUndefined();
77
+ });
78
+
79
+ test("WINDY_LICENSE_VERSIONS 非法 JSON 或非法结构时 fail-fast", () => {
80
+ expect(() =>
81
+ loadPlatformConfig({ WINDY_LICENSE_VERSIONS: "not-a-json" }),
82
+ ).toThrow(
83
+ expect.objectContaining<Partial<ConfigLoadError>>({
84
+ code: "CONFIG_INVALID_VALUE",
85
+ fieldPath: "license.versions",
86
+ }),
87
+ );
88
+ expect(() =>
89
+ loadPlatformConfig({
90
+ WINDY_LICENSE_VERSIONS: JSON.stringify([{ key: "missing-order" }]),
91
+ }),
92
+ ).toThrow(
93
+ expect.objectContaining<Partial<ConfigLoadError>>({
94
+ code: "CONFIG_INVALID_VALUE",
95
+ }),
96
+ );
97
+ expect(() =>
98
+ loadPlatformConfig({
99
+ WINDY_LICENSE_VERSIONS: JSON.stringify([
100
+ { key: "fractional", label: "小数版本", order: 1.5 },
101
+ ]),
102
+ }),
103
+ ).toThrow(
104
+ expect.objectContaining<Partial<ConfigLoadError>>({
105
+ code: "CONFIG_INVALID_VALUE",
106
+ fieldPath: "license.versions",
107
+ }),
108
+ );
109
+ });
110
+
52
111
  test("必填 Secret 缺失时 fail-fast 且 registry 不暴露环境引用", () => {
53
112
  const definition = defineModuleConfig({
54
113
  key: "example.secure",
@@ -112,3 +171,44 @@ test("registry 拒绝重复模块和未知模块", () => {
112
171
  expect(() => registry.register(definition)).toThrow("配置模块重复注册");
113
172
  expect(() => registry.get("example.unknown")).toThrow("未知配置模块");
114
173
  });
174
+
175
+ test("OpenAI-compatible 部署只在服务端解析 Secret 与运行上限", () => {
176
+ const config = loadOpenAiCompatibleDeploymentConfig({
177
+ QWEN_API_URL:
178
+ "https://workspace.cn-beijing.maas.aliyuncs.com/compatible-mode/v1/",
179
+ QWEN_API_KEY: "server-only",
180
+ QWEN_MODEL: "qwen-plus",
181
+ AI_PROVIDER_CONNECT_TIMEOUT_MS: "2500",
182
+ AI_PROVIDER_REQUEST_TIMEOUT_MS: "45000",
183
+ AI_PROVIDER_MAX_RESPONSE_BYTES: "1048576",
184
+ });
185
+
186
+ expect(config).toEqual({
187
+ baseUrl:
188
+ "https://workspace.cn-beijing.maas.aliyuncs.com/compatible-mode/v1",
189
+ model: "qwen-plus",
190
+ apiKey: "server-only",
191
+ connectTimeoutMs: 2500,
192
+ requestTimeoutMs: 45000,
193
+ maxResponseBytes: 1048576,
194
+ });
195
+ });
196
+
197
+ test("OpenAI-compatible Endpoint 拒绝公网 HTTP 与 metadata", () => {
198
+ expect(() =>
199
+ normalizeOpenAiCompatibleBaseUrl("http://api.example.com/v1"),
200
+ ).toThrow("公网 OpenAI-compatible Endpoint 必须使用 HTTPS");
201
+ expect(() =>
202
+ normalizeOpenAiCompatibleBaseUrl("http://169.254.169.254/v1"),
203
+ ).toThrow("禁止访问云 metadata");
204
+ expect(() =>
205
+ normalizeOpenAiCompatibleBaseUrl("https://user:pass@example.com/v1?q=1"),
206
+ ).toThrow("不允许凭据、查询串或片段");
207
+
208
+ expect(
209
+ normalizeOpenAiCompatibleBaseUrl("http://qwen.internal:9000/v1/"),
210
+ ).toBe("http://qwen.internal:9000/v1");
211
+ expect(normalizeOpenAiCompatibleBaseUrl("http://10.20.30.40:8000/v1")).toBe(
212
+ "http://10.20.30.40:8000/v1",
213
+ );
214
+ });
@@ -1,4 +1,5 @@
1
1
  export * from "./src/definition.js";
2
2
  export * from "./src/environment-loader.js";
3
+ export * from "./src/ai-openai-compatible.js";
3
4
  export * from "./src/platform.js";
4
5
  export * from "./src/secrets.js";
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@southwind-ai/config",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "engines": {
@@ -8,6 +8,7 @@
8
8
  },
9
9
  "files": [
10
10
  "index.ts",
11
+ "src/ai-openai-compatible.ts",
11
12
  "src/**/*.ts",
12
13
  "!src/**/*.test.ts"
13
14
  ],
@@ -0,0 +1,131 @@
1
+ import { isIP } from "node:net";
2
+ import { z } from "zod";
3
+ import { defineModuleConfig } from "./definition.js";
4
+ import {
5
+ loadModuleConfigSync,
6
+ strictInteger,
7
+ type ConfigEnvironment,
8
+ } from "./environment-loader.js";
9
+ import { EnvironmentSecretResolver } from "./secrets.js";
10
+
11
+ export interface OpenAiCompatibleDeploymentConfig {
12
+ baseUrl: string;
13
+ model: string;
14
+ apiKey?: string;
15
+ connectTimeoutMs: number;
16
+ requestTimeoutMs: number;
17
+ maxResponseBytes: number;
18
+ }
19
+
20
+ const deploymentSchema: z.ZodType<OpenAiCompatibleDeploymentConfig> =
21
+ z.strictObject({
22
+ baseUrl: z.string().transform(normalizeOpenAiCompatibleBaseUrl),
23
+ model: z.string().trim().min(1).max(200),
24
+ apiKey: z.string().trim().min(1).optional(),
25
+ connectTimeoutMs: z.number().int().min(100).max(30_000),
26
+ requestTimeoutMs: z.number().int().min(1_000).max(300_000),
27
+ maxResponseBytes: z
28
+ .number()
29
+ .int()
30
+ .min(1_024)
31
+ .max(16 * 1_024 * 1_024),
32
+ });
33
+
34
+ export const openAiCompatibleDeploymentConfigDefinition =
35
+ defineModuleConfig<OpenAiCompatibleDeploymentConfig>({
36
+ key: "ai.openai-compatible",
37
+ label: "OpenAI-compatible 模型部署",
38
+ ownedEnvironmentPrefixes: ["QWEN_", "AI_PROVIDER_"],
39
+ schema: deploymentSchema,
40
+ environment: [
41
+ { key: "QWEN_API_URL", path: "baseUrl" },
42
+ { key: "QWEN_MODEL", path: "model" },
43
+ {
44
+ key: "QWEN_API_KEY",
45
+ path: "apiKey",
46
+ secret: { provider: "env" },
47
+ },
48
+ integer("AI_PROVIDER_CONNECT_TIMEOUT_MS", "connectTimeoutMs", 5_000),
49
+ integer("AI_PROVIDER_REQUEST_TIMEOUT_MS", "requestTimeoutMs", 60_000),
50
+ integer(
51
+ "AI_PROVIDER_MAX_RESPONSE_BYTES",
52
+ "maxResponseBytes",
53
+ 2 * 1_024 * 1_024,
54
+ ),
55
+ ],
56
+ });
57
+
58
+ export function loadOpenAiCompatibleDeploymentConfig(
59
+ environment: ConfigEnvironment = process.env,
60
+ ): OpenAiCompatibleDeploymentConfig {
61
+ return loadModuleConfigSync(
62
+ openAiCompatibleDeploymentConfigDefinition,
63
+ environment,
64
+ new EnvironmentSecretResolver(environment),
65
+ );
66
+ }
67
+
68
+ export function normalizeOpenAiCompatibleBaseUrl(raw: string): string {
69
+ let url: URL;
70
+ try {
71
+ url = new URL(raw.trim());
72
+ } catch {
73
+ throw new Error("OpenAI-compatible Endpoint 无效");
74
+ }
75
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
76
+ throw new Error("OpenAI-compatible Endpoint 只允许 HTTP 或 HTTPS");
77
+ }
78
+ if (url.username || url.password || url.search || url.hash) {
79
+ throw new Error("OpenAI-compatible Endpoint 不允许凭据、查询串或片段");
80
+ }
81
+ const hostname = url.hostname.toLowerCase();
82
+ if (isCloudMetadataHost(hostname)) {
83
+ throw new Error("OpenAI-compatible Endpoint 禁止访问云 metadata");
84
+ }
85
+ if (url.protocol === "http:" && !isPrivateHost(hostname)) {
86
+ throw new Error("公网 OpenAI-compatible Endpoint 必须使用 HTTPS");
87
+ }
88
+ url.pathname = url.pathname.replace(/\/+$/, "");
89
+ return url.toString().replace(/\/$/, "");
90
+ }
91
+
92
+ function integer(key: string, path: string, defaultValue: number) {
93
+ return { key, path, defaultValue, decode: strictInteger };
94
+ }
95
+
96
+ function isPrivateHost(hostname: string): boolean {
97
+ if (
98
+ hostname === "localhost" ||
99
+ hostname.endsWith(".localhost") ||
100
+ hostname.endsWith(".internal") ||
101
+ hostname.endsWith(".local") ||
102
+ (!hostname.includes(".") && isIP(hostname) === 0)
103
+ ) {
104
+ return true;
105
+ }
106
+ if (isIP(hostname) === 4) {
107
+ const [first = 0, second = 0] = hostname.split(".").map(Number);
108
+ return (
109
+ first === 10 ||
110
+ first === 127 ||
111
+ (first === 172 && second >= 16 && second <= 31) ||
112
+ (first === 192 && second === 168)
113
+ );
114
+ }
115
+ if (isIP(hostname) === 6) {
116
+ return (
117
+ hostname === "::1" ||
118
+ hostname.startsWith("fc") ||
119
+ hostname.startsWith("fd")
120
+ );
121
+ }
122
+ return false;
123
+ }
124
+
125
+ function isCloudMetadataHost(hostname: string): boolean {
126
+ return (
127
+ hostname === "169.254.169.254" ||
128
+ hostname === "metadata.google.internal" ||
129
+ hostname === "metadata.azure.internal"
130
+ );
131
+ }
@@ -1,4 +1,11 @@
1
- import type { DatabaseDialect } from "@southwind-ai/shared";
1
+ import type {
2
+ DatabaseDialect,
3
+ LicenseCatalogDefinition,
4
+ } from "@southwind-ai/shared";
5
+ import {
6
+ parseLicenseCatalog,
7
+ parseLicenseCatalogJson,
8
+ } from "@southwind-ai/shared";
2
9
  import { z } from "zod";
3
10
  import { defineModuleConfig, ModuleConfigRegistry } from "./definition.js";
4
11
  import {
@@ -33,6 +40,8 @@ export interface PlatformConfig {
33
40
  publicKeyId?: string;
34
41
  publicKey?: string;
35
42
  machineCode?: string;
43
+ /** 宿主自定义 License 版本目录;未配置时回退 shared 默认三级目录。 */
44
+ versions?: LicenseCatalogDefinition[];
36
45
  };
37
46
  audit: { enabled: boolean; redactFields: string[] };
38
47
  compatibility: {
@@ -75,6 +84,20 @@ const platformSchema: z.ZodType<PlatformConfig> = z.strictObject({
75
84
  publicKeyId: z.string().trim().min(1).optional(),
76
85
  publicKey: z.string().trim().min(1).optional(),
77
86
  machineCode: z.string().trim().min(1).optional(),
87
+ versions: z
88
+ .unknown()
89
+ .transform((value, context) => {
90
+ try {
91
+ return parseLicenseCatalog(value);
92
+ } catch (error) {
93
+ context.addIssue({
94
+ code: "custom",
95
+ message: error instanceof Error ? error.message : "版本目录无效",
96
+ });
97
+ return z.NEVER;
98
+ }
99
+ })
100
+ .optional(),
78
101
  }),
79
102
  audit: z.strictObject({
80
103
  enabled: z.boolean(),
@@ -105,6 +128,7 @@ export const platformConfigDefinition = defineModuleConfig<PlatformConfig>({
105
128
  "LICENSE_PUBLIC_KEY_ID",
106
129
  "LICENSE_PUBLIC_KEY",
107
130
  "LICENSE_MACHINE_CODE",
131
+ "WINDY_LICENSE_VERSIONS",
108
132
  ],
109
133
  environment: [
110
134
  value("PLATFORM_NAME", "name", "Windy Platform"),
@@ -133,6 +157,9 @@ export const platformConfigDefinition = defineModuleConfig<PlatformConfig>({
133
157
  value("LICENSE_PUBLIC_KEY_ID", "license.publicKeyId"),
134
158
  value("LICENSE_PUBLIC_KEY", "license.publicKey"),
135
159
  secret("LICENSE_MACHINE_CODE", "license.machineCode"),
160
+ value("WINDY_LICENSE_VERSIONS", "license.versions", undefined, (raw) =>
161
+ parseLicenseCatalogJson(raw, "WINDY_LICENSE_VERSIONS"),
162
+ ),
136
163
  value("AUDIT_ENABLED", "audit.enabled", true, strictBoolean),
137
164
  value(
138
165
  "AUDIT_REDACT_FIELDS",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@southwind-ai/database",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "engines": {
@@ -1,16 +1,14 @@
1
- import type {
2
- BundleMigrationDefinition,
3
- DatabaseCompatibilityAdapter,
4
- DatabaseDialect,
5
- MigrationBundle,
6
- MigrationManifestSource,
7
- MigrationStatement,
8
- } from "@southwind-ai/shared";
9
1
  import {
10
2
  normalizeManifestBundles,
11
3
  orderBundleMigrations,
12
4
  validateAndOrderBundles,
13
- } from "./bundle-validation.js";
5
+ type BundleMigrationDefinition,
6
+ type DatabaseCompatibilityAdapter,
7
+ type DatabaseDialect,
8
+ type MigrationBundle,
9
+ type MigrationManifestSource,
10
+ type MigrationStatement,
11
+ } from "@southwind-ai/shared";
14
12
  import { createDialectAdapter } from "./dialects.js";
15
13
  import type { MigrationHistoryEntry, MigrationHistoryStore } from "./store.js";
16
14
 
@@ -2,3 +2,4 @@ export * from "./src/contract.js";
2
2
  export * from "./src/manifest.js";
3
3
  export * from "./src/migration-bundle.js";
4
4
  export * from "./src/schema.js";
5
+ export * from "./src/server-module.js";
@@ -4,6 +4,8 @@
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "dependencies": {
7
+ "@southwind-ai/modules": "workspace:*",
8
+ "@southwind-ai/server-sdk": "workspace:*",
7
9
  "@southwind-ai/shared": "workspace:*",
8
10
  "zod": "^4.4.3"
9
11
  },
@@ -0,0 +1,61 @@
1
+ import type { ModuleManifest } from "@southwind-ai/modules";
2
+ import type {
3
+ InstallableServerModule,
4
+ ModuleRouteDefinition,
5
+ ModuleSearchProvider,
6
+ ModuleTaskHandler,
7
+ } from "@southwind-ai/server-sdk";
8
+ import { workOrderModule } from "./manifest.js";
9
+
10
+ export interface WorkOrderServerBindings {
11
+ readonly routes: readonly ModuleRouteDefinition[];
12
+ readonly snapshot: ModuleTaskHandler;
13
+ readonly searchProvider: ModuleSearchProvider;
14
+ }
15
+
16
+ /**
17
+ * 工单业务包拥有 Manifest 与 Host 注册过程;宿主只注入具体业务实现。
18
+ */
19
+ export function createWorkOrderServerModule(
20
+ bindings: WorkOrderServerBindings,
21
+ ): InstallableServerModule<ModuleManifest> {
22
+ return {
23
+ manifest: workOrderModule(),
24
+ setup(host) {
25
+ host.registerUploadPolicy({
26
+ purpose: "work-order.ticket.attachment",
27
+ maxByteSize: 20 * 1024 * 1024,
28
+ maxPartByteSize: 4 * 1024 * 1024,
29
+ sessionTtlMs: 2 * 60 * 60_000,
30
+ allowedTypes: {
31
+ "application/pdf": ["pdf"],
32
+ "image/png": ["png"],
33
+ "image/jpeg": ["jpg", "jpeg"],
34
+ "text/plain": ["txt"],
35
+ },
36
+ signature: "none",
37
+ });
38
+ host.registerRoutes(bindings.routes);
39
+ host.registerTaskHandlers([
40
+ {
41
+ taskKey: "work-order.ticket.snapshot",
42
+ handler: bindings.snapshot,
43
+ },
44
+ ]);
45
+ host.registerSearchProviders([bindings.searchProvider]);
46
+ host.registerRolePresets([
47
+ {
48
+ key: "work-order.ticket-manager",
49
+ label: "工单管理员",
50
+ description: "管理本部门及下级部门工单的模块角色预设。",
51
+ dataScope: "department-and-children",
52
+ permissionKeys: [
53
+ "work-order.ticket.create",
54
+ "work-order.ticket.manage",
55
+ "work-order.ticket.read",
56
+ ],
57
+ },
58
+ ]);
59
+ },
60
+ };
61
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@southwind-ai/jobs",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "engines": {
@@ -179,8 +179,14 @@ export interface SubmitDurableJobResult {
179
179
  created: boolean;
180
180
  }
181
181
 
182
- export interface DurableJobs {
182
+ /**
183
+ * 只保留提交能力的窄接口,用于向模块等受限调用方暴露 Durable Jobs。
184
+ */
185
+ export interface DurableJobSubmitter {
183
186
  submit(input: SubmitDurableJob): Promise<SubmitDurableJobResult>;
187
+ }
188
+
189
+ export interface DurableJobs extends DurableJobSubmitter {
184
190
  get(id: string): Promise<DurableJobSnapshot | undefined>;
185
191
  list(requestedBy: string, limit?: number): Promise<DurableJob[]>;
186
192
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@southwind-ai/modules",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "engines": {