create-windy 0.3.4 → 0.3.6
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 +20 -3
- package/dist/cli.js +585 -111
- package/package.json +1 -1
- package/template/.windy-template.json +2 -2
- package/template/AGENTS.md +4 -0
- package/template/README.md +18 -11
- package/template/apps/server/src/application-services.ts +16 -0
- package/template/apps/server/src/data-governance/export/authorization-port.test.ts +5 -1
- package/template/apps/server/src/data-governance/export/drizzle-repository.integration.test.ts +2 -0
- package/template/apps/server/src/data-governance/export/drizzle-repository.ts +1 -0
- package/template/apps/server/src/data-governance/export/job-runtime.test.ts +3 -1
- package/template/apps/server/src/data-governance/export/job-runtime.ts +53 -14
- package/template/apps/server/src/data-governance/export/ndjson-stream.ts +8 -0
- package/template/apps/server/src/data-governance/export/postgres-recovery.integration.test.ts +2 -0
- package/template/apps/server/src/data-governance/export/renderer-job-runtime.test.ts +235 -0
- package/template/apps/server/src/data-governance/export/renderer-runtime.ts +293 -0
- package/template/apps/server/src/data-governance/export/renderer-selection.test.ts +67 -0
- package/template/apps/server/src/data-governance/export/repository.test.ts +2 -0
- package/template/apps/server/src/data-governance/export/request-fingerprint.test.ts +13 -0
- package/template/apps/server/src/data-governance/export/request-fingerprint.ts +2 -0
- package/template/apps/server/src/data-governance/export/routes.ts +11 -1
- package/template/apps/server/src/data-governance/export/service-support.ts +61 -4
- package/template/apps/server/src/data-governance/export/service.ts +8 -2
- package/template/apps/server/src/data-governance/export/types.ts +27 -0
- package/template/apps/server/src/index.ts +1 -0
- package/template/apps/server/src/jobs/artifact-service.test.ts +14 -2
- package/template/apps/server/src/jobs/artifact-service.ts +10 -0
- package/template/apps/server/src/jobs/drizzle-execution-repository.ts +17 -18
- package/template/apps/server/src/jobs/drizzle-execution.integration.test.ts +19 -1
- package/template/apps/server/src/jobs/drizzle-mappers.ts +3 -1
- package/template/apps/server/src/jobs/drizzle-recovery-repository.ts +49 -2
- package/template/apps/server/src/jobs/execution-repository.ts +17 -0
- package/template/apps/server/src/jobs/in-memory-control.ts +135 -0
- package/template/apps/server/src/jobs/in-memory-execution.ts +34 -65
- package/template/apps/server/src/jobs/worker.ts +27 -3
- package/template/apps/server/src/module-host/governed-export-route-support.ts +24 -5
- package/template/apps/server/src/module-host/governed-export-routes.test.ts +5 -1
- package/template/apps/server/src/module-host/governed-export-routes.ts +1 -2
- package/template/apps/server/src/module-host/governed-renderer-registration.test.ts +247 -0
- package/template/apps/server/src/module-host/governed-resources.test.ts +1 -0
- package/template/apps/server/src/module-host/governed-resources.ts +56 -0
- package/template/apps/server/src/module-host/http-runtime.ts +3 -0
- package/template/apps/server/src/module-host/job-port.test.ts +262 -0
- package/template/apps/server/src/module-host/job-port.ts +334 -0
- package/template/apps/server/src/module-host/request-context.ts +14 -26
- package/template/apps/server/src/module-host/route-installer.ts +11 -0
- package/template/apps/web/src/app/business-routes.ts +22 -0
- package/template/apps/web/src/components/common/AccessSnapshotBoundary.vue +112 -0
- package/template/apps/web/src/components/common/AccessSnapshotBoundary.webtest.ts +130 -0
- package/template/apps/web/src/components/common/AccessSnapshotNotice.vue +117 -0
- package/template/apps/web/src/composables/access-snapshot-change.ts +131 -0
- package/template/apps/web/src/composables/access-snapshot-change.webtest.ts +103 -0
- package/template/apps/web/src/composables/useAccessSnapshot.ts +48 -2
- package/template/apps/web/src/composables/useAccessSnapshot.webtest.ts +54 -0
- package/template/apps/web/src/composables/useAccessSnapshotMonitor.ts +42 -0
- package/template/apps/web/src/layout/AppShell.vue +28 -25
- package/template/apps/web/src/layout/AppShell.webtest.ts +1 -0
- package/template/apps/web/src/router/business-routes.ts +68 -0
- package/template/apps/web/src/router/business-routes.webtest.ts +85 -0
- package/template/apps/web/src/router/index.ts +7 -0
- package/template/apps/web/src/router/shared-scaffold-neutrality.webtest.ts +12 -0
- package/template/apps/web/src/services/access-api.ts +5 -1
- package/template/apps/web/src/services/access-api.webtest.ts +35 -0
- package/template/docs/architecture/ai-egress.md +12 -9
- package/template/docs/architecture/durable-jobs.md +7 -1
- package/template/docs/architecture/foundation-infrastructure.md +163 -0
- package/template/docs/architecture/module-host.md +115 -0
- package/template/docs/architecture/server-module-interface.md +39 -0
- package/template/docs/architecture/web-access-snapshot-resilience.md +60 -0
- package/template/docs/development/business-web-routes.md +66 -0
- package/template/docs/development/custom-login-page.md +3 -3
- package/template/docs/development/module-lifecycle.md +71 -0
- package/template/docs/operations/observability.md +5 -1
- package/template/docs/platform/agent-runtime.md +6 -6
- package/template/docs/platform/agent-tool-sandbox.md +58 -0
- package/template/docs/platform/document-ai-runtime.md +204 -0
- package/template/docs/platform/governed-export-renderers.md +132 -0
- package/template/docs/platform/navigation-registry.md +5 -0
- package/template/docs/platform/organization-membership.md +2 -1
- package/template/docs/platform/speech-ai-runtime.md +156 -0
- package/template/docs/platform/system-crud-api.md +7 -1
- package/template/docs/platform/vision-ai-runtime.md +97 -0
- package/template/docs/platform/web-system-crud.md +6 -0
- package/template/packages/config/package.json +1 -1
- package/template/packages/database/drizzle/0042_puzzling_rumiko_fujikawa.sql +27 -0
- package/template/packages/database/drizzle/meta/0042_snapshot.json +8541 -0
- package/template/packages/database/drizzle/meta/_journal.json +7 -0
- package/template/packages/database/package.json +1 -1
- package/template/packages/database/src/schema/durable-jobs.ts +1 -0
- package/template/packages/database/src/schema/governed-exports.ts +15 -0
- package/template/packages/database/src/schema-governed-exports.test.ts +21 -1
- package/template/packages/jobs/package.json +1 -1
- package/template/packages/jobs/src/handlers.ts +1 -0
- package/template/packages/jobs/src/types.ts +1 -0
- package/template/packages/modules/package.json +1 -1
- package/template/packages/server-sdk/index.ts +11 -0
- package/template/packages/server-sdk/package.json +1 -1
- package/template/packages/server-sdk/src/data-governance-registration.ts +46 -0
- package/template/packages/server-sdk/src/jobs-port.ts +69 -0
- package/template/packages/server-sdk/src/request-context.ts +2 -2
- package/template/packages/shared/package.json +1 -1
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
assertNoRouteCollisions,
|
|
4
|
+
materializeBusinessAdminRoutes,
|
|
5
|
+
} from "./business-routes";
|
|
6
|
+
|
|
7
|
+
describe("project-owned 业务路由接缝", () => {
|
|
8
|
+
test("统一生成带 Feature 与 RBAC Guard 的管理路由", () => {
|
|
9
|
+
const [route] = materializeBusinessAdminRoutes([
|
|
10
|
+
{
|
|
11
|
+
key: "case.record",
|
|
12
|
+
path: "/cases",
|
|
13
|
+
name: "case-records",
|
|
14
|
+
title: "案件",
|
|
15
|
+
component: {},
|
|
16
|
+
featureKey: "case.record",
|
|
17
|
+
permissionKey: "case.record.read",
|
|
18
|
+
},
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
expect(route).toMatchObject({
|
|
22
|
+
path: "/cases",
|
|
23
|
+
name: "case-records",
|
|
24
|
+
meta: {
|
|
25
|
+
title: "案件",
|
|
26
|
+
layout: "admin",
|
|
27
|
+
businessRouteKey: "case.record",
|
|
28
|
+
featureKey: "case.record",
|
|
29
|
+
permissionKey: "case.record.read",
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("拒绝空 Guard、重复 Key 与兜底路径", () => {
|
|
35
|
+
expect(() =>
|
|
36
|
+
materializeBusinessAdminRoutes([
|
|
37
|
+
{
|
|
38
|
+
key: "case.record",
|
|
39
|
+
path: "/cases",
|
|
40
|
+
title: "案件",
|
|
41
|
+
component: {},
|
|
42
|
+
featureKey: "",
|
|
43
|
+
permissionKey: "case.record.read",
|
|
44
|
+
},
|
|
45
|
+
]),
|
|
46
|
+
).toThrow("featureKey 不能为空");
|
|
47
|
+
expect(() =>
|
|
48
|
+
materializeBusinessAdminRoutes([
|
|
49
|
+
registration("/cases"),
|
|
50
|
+
registration("/other"),
|
|
51
|
+
]),
|
|
52
|
+
).toThrow("业务 Web Route Key 重复");
|
|
53
|
+
expect(() =>
|
|
54
|
+
materializeBusinessAdminRoutes([
|
|
55
|
+
registration("/:pathMatch(.*)*"),
|
|
56
|
+
]),
|
|
57
|
+
).toThrow("不能注册兜底路径");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("组合时拒绝与平台路由重复的路径或名称", () => {
|
|
61
|
+
expect(() =>
|
|
62
|
+
assertNoRouteCollisions([
|
|
63
|
+
{ path: "/admin", name: "admin-home", component: {} },
|
|
64
|
+
{ path: "/admin", name: "business-home", component: {} },
|
|
65
|
+
]),
|
|
66
|
+
).toThrow("Web Route 路径重复:/admin");
|
|
67
|
+
expect(() =>
|
|
68
|
+
assertNoRouteCollisions([
|
|
69
|
+
{ path: "/admin", name: "admin-home", component: {} },
|
|
70
|
+
{ path: "/cases", name: "admin-home", component: {} },
|
|
71
|
+
]),
|
|
72
|
+
).toThrow("Web Route 名称重复:admin-home");
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
function registration(path: string) {
|
|
77
|
+
return {
|
|
78
|
+
key: "case.record",
|
|
79
|
+
path,
|
|
80
|
+
title: "案件",
|
|
81
|
+
component: {},
|
|
82
|
+
featureKey: "case.record",
|
|
83
|
+
permissionKey: "case.record.read",
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
} from "vue-router";
|
|
7
7
|
import DashboardPage from "@/pages/dashboard/DashboardPage.vue";
|
|
8
8
|
import HomePage from "@/app/home/HomePage.vue";
|
|
9
|
+
import { installedBusinessAdminRoutes } from "@/app/business-routes";
|
|
9
10
|
import { loginPageComponent } from "@/app/auth/login-page";
|
|
10
11
|
import { errorPageComponents } from "@/app/error-pages";
|
|
11
12
|
import AccessDeniedPage from "@/pages/AccessDeniedPage.vue";
|
|
@@ -41,6 +42,10 @@ import { evaluateWebAccess, type WebAccessSnapshot } from "@/layout/navigation";
|
|
|
41
42
|
import { isApiErrorStatus } from "@/services/http";
|
|
42
43
|
import { loginRoute, safeReturnTo } from "./auth-navigation";
|
|
43
44
|
import { safeErrorReturnTo } from "./error-navigation";
|
|
45
|
+
import {
|
|
46
|
+
assertNoRouteCollisions,
|
|
47
|
+
materializeBusinessAdminRoutes,
|
|
48
|
+
} from "./business-routes";
|
|
44
49
|
import {
|
|
45
50
|
manifestAdminRoute,
|
|
46
51
|
validateWebModuleComposition,
|
|
@@ -195,6 +200,7 @@ export const routes: RouteRecordRaw[] = [
|
|
|
195
200
|
component: ModuleConfigurationPage,
|
|
196
201
|
}),
|
|
197
202
|
// @windy-module system.configuration end
|
|
203
|
+
...materializeBusinessAdminRoutes(installedBusinessAdminRoutes),
|
|
198
204
|
{
|
|
199
205
|
path: "/:pathMatch(.*)*",
|
|
200
206
|
name: "not-found",
|
|
@@ -212,6 +218,7 @@ export const routes: RouteRecordRaw[] = [
|
|
|
212
218
|
},
|
|
213
219
|
];
|
|
214
220
|
|
|
221
|
+
assertNoRouteCollisions(routes);
|
|
215
222
|
validateWebModuleComposition(routes);
|
|
216
223
|
|
|
217
224
|
export function createAppRouter(
|
|
@@ -25,6 +25,18 @@ describe("Router shared-scaffold 中性契约", () => {
|
|
|
25
25
|
expect(routerSource).toContain("component: errorPageComponents.network");
|
|
26
26
|
});
|
|
27
27
|
|
|
28
|
+
test("平台路由组合 project-owned 业务注册表且保持在兜底页之前", () => {
|
|
29
|
+
expect(routerSource).toContain(
|
|
30
|
+
'import { installedBusinessAdminRoutes } from "@/app/business-routes";',
|
|
31
|
+
);
|
|
32
|
+
const businessIndex = routerSource.indexOf(
|
|
33
|
+
"...materializeBusinessAdminRoutes(installedBusinessAdminRoutes)",
|
|
34
|
+
);
|
|
35
|
+
const fallbackIndex = routerSource.indexOf('path: "/:pathMatch(.*)*"');
|
|
36
|
+
expect(businessIndex).toBeGreaterThan(0);
|
|
37
|
+
expect(businessIndex).toBeLessThan(fallbackIndex);
|
|
38
|
+
});
|
|
39
|
+
|
|
28
40
|
test("测试快照只从项目注入路由递归派生访问键", () => {
|
|
29
41
|
expect(collectRouteAccess([])).toEqual({
|
|
30
42
|
permissionKeys: [],
|
|
@@ -23,7 +23,11 @@ interface FoundationResponse {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
export async function loadAccessSnapshot(): Promise<WebAccessSnapshot> {
|
|
26
|
-
const foundation = await apiRequest<FoundationResponse>(
|
|
26
|
+
const foundation = await apiRequest<FoundationResponse>(
|
|
27
|
+
"/admin/bootstrap",
|
|
28
|
+
{},
|
|
29
|
+
{ suppressFailureHandler: true },
|
|
30
|
+
);
|
|
27
31
|
return {
|
|
28
32
|
actor: foundation.access.actor,
|
|
29
33
|
menus: foundation.menus,
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, expect, test, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const mocks = vi.hoisted(() => ({ apiRequest: vi.fn() }));
|
|
4
|
+
vi.mock("./http", () => mocks);
|
|
5
|
+
|
|
6
|
+
import { loadAccessSnapshot } from "./access-api";
|
|
7
|
+
|
|
8
|
+
describe("权限快照 API", () => {
|
|
9
|
+
test("由调用方处理刷新失败,避免全局错误页卸载编辑实例", async () => {
|
|
10
|
+
mocks.apiRequest.mockResolvedValue({
|
|
11
|
+
menus: [],
|
|
12
|
+
permissions: [],
|
|
13
|
+
auditActionCatalog: [],
|
|
14
|
+
licenseCatalog: [],
|
|
15
|
+
access: {
|
|
16
|
+
actor: {
|
|
17
|
+
id: "user_1",
|
|
18
|
+
type: "user",
|
|
19
|
+
name: "测试员",
|
|
20
|
+
roleCodes: [],
|
|
21
|
+
permissionKeys: [],
|
|
22
|
+
},
|
|
23
|
+
features: [],
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
await loadAccessSnapshot();
|
|
28
|
+
|
|
29
|
+
expect(mocks.apiRequest).toHaveBeenCalledWith(
|
|
30
|
+
"/admin/bootstrap",
|
|
31
|
+
{},
|
|
32
|
+
{ suppressFailureHandler: true },
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -108,15 +108,18 @@ append-only,只有 TTL 清理可以删除过期行,并保留最近/累计删
|
|
|
108
108
|
出网 Adapter。若未来开放 Tool 出网,必须新增宿主实现的受限 Port,按 Adapter 和精确
|
|
109
109
|
目的 Host 授权,并在 Manifest 组合、运行时和部署防火墙三层同时允许。
|
|
110
110
|
|
|
111
|
-
|
|
111
|
+
生产 Compose 还把 Server、PostgreSQL 与内部应用通信放入 `internal: true` 网络;
|
|
112
|
+
只有 Web 同时加入独立 ingress 网络接收宿主机流量。Server 不加入任何具有默认外网
|
|
113
|
+
路由的网络,因此仍在 Server 进程执行的 project-owned Handler 即使直接调用全局
|
|
114
|
+
`fetch`,也不能访问公网。需要模型 Provider 出网时,由独立 Agent Server 进程加入显式
|
|
115
|
+
egress 网络;纯计算 Tool 继续使用 `network_mode: none` 的 Tool Sandbox。
|
|
112
116
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
117
|
+
这项部署边界意味着 SMTP、对象存储或其它合法外部集成不能直接从 Server 访问公网;
|
|
118
|
+
生产项目应在 internal application 网络中接入专用 relay/Adapter 服务,由该独立进程
|
|
119
|
+
持有目的地址和凭据。不得为了某个业务 Tool 临时把 Server 接回公网网络。
|
|
116
120
|
|
|
117
|
-
|
|
118
|
-
但无法拦截业务代码直接调用全局 `fetch`;生产 deny-egress 仍需容器网络策略或防火墙;
|
|
119
|
-
- DNS 解析钉扎、代理治理和多 Provider/多 Endpoint 动态路由尚未实现。
|
|
121
|
+
## 尚未覆盖
|
|
120
122
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
+
DNS 解析钉扎、企业代理治理和多 Provider/多 Endpoint 动态路由仍未实现。DLP 是保守的
|
|
124
|
+
模式检测,不替代行业专用实体识别、人工审批或客户网络侧纵深防御。非 Compose 部署必须
|
|
125
|
+
提供等价的 Server deny-egress 规则后,才能沿用“业务 Tool 不能直接出网”的声明。
|
|
@@ -64,6 +64,12 @@ nominal Scope、资源与 fileId 绑定以及业务对象授权;成功与拒
|
|
|
64
64
|
|
|
65
65
|
任务产物读取统一经过 `DurableArtifactService`:先按 `jobId + kind` 回读持久化元数据,再校验 Owner 或显式 manage 能力、留存时间、Blob 大小和 SHA-256。调用方不能凭 `ArtifactRef` 猜测下载;API 层仍需执行对应 RBAC Guard 并记录下载审计。
|
|
66
66
|
|
|
67
|
+
模块 Route 通过 `ModuleJobPort` 控制本人提交的后台任务。宿主不会把完整
|
|
68
|
+
`DurableJobSnapshot` 下发给业务,而是投影为不含 payload、内部错误、租约和 Artifact
|
|
69
|
+
引用的 `ModuleJobView`。每次 get/cancel/recover 都重新执行当前 Route Guard,并要求
|
|
70
|
+
Module Queue、Handler 命名空间、Actor 和提交 Scope 快照全部匹配;范围缩小、扩大或角色/
|
|
71
|
+
权限撤销都会 fail closed,未知、越权与不存在保持同形。
|
|
72
|
+
|
|
67
73
|
## 数据库并发基础
|
|
68
74
|
|
|
69
75
|
- `durable_jobs` 具有提交幂等唯一索引和按 queue/status/available/priority 的领取索引。
|
|
@@ -75,7 +81,7 @@ nominal Scope、资源与 fileId 绑定以及业务对象授权;成功与拒
|
|
|
75
81
|
|
|
76
82
|
Claim 使用 `FOR UPDATE SKIP LOCKED`,多实例只会得到一个任务。数据库只保存 lease token 的 SHA-256;worker 后续写入还必须同时匹配 job/stage/attempt/lease、token hash、active 状态、未过期时间和单调 fencing token。旧 worker 即使晚到也不能完成新 attempt。
|
|
77
83
|
|
|
78
|
-
Lease 或 timeout sweep 会终结旧 attempt 并写 Failure;只有 `currentAttempt < maxAttempts` 才按 `availableAt` 重排。queued 取消立即进入最终状态;running
|
|
84
|
+
Lease 或 timeout sweep 会终结旧 attempt 并写 Failure;只有 `currentAttempt < maxAttempts` 才按 `availableAt` 重排。queued 取消立即进入最终状态;running 取消先记录请求,同时向当前进程持有的 Handler `AbortSignal` 传播,协作 worker 或 expiry sweep 负责最终收口;Worker 重启后仍以数据库取消事实为准。人工恢复只接受 failed/cancelled 且没有 active lease 的任务,使用调用方读到的 attempt/status/version 做 CAS,保留历史 attempts/failures,并在 `durable_job_recoveries` 记录带宿主幂等键的恢复事实。内存 Adapter 与 PostgreSQL Adapter 遵循相同状态、CAS 和幂等结果。
|
|
79
85
|
|
|
80
86
|
## Scheduler 与 Bulk 兼容接入
|
|
81
87
|
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# 平台基础设施设计记录
|
|
2
|
+
|
|
3
|
+
## 当前落地范围
|
|
4
|
+
|
|
5
|
+
当前已经形成可运行的平台基础闭环:真实密码会话、系统 CRUD、动态 Feature Flag、License 版本 → Feature → RBAC Guard、可靠审计、后台批量任务、定时任务、运维总览、License V1 签发/安装契约、独立签名服务、Vault Transit Provider 和 PostgreSQL 权威存储均已接入。国产数据库真实验证和 RuoYi 旧库运行时写入仍明确延期。
|
|
6
|
+
|
|
7
|
+
已落地内容:
|
|
8
|
+
|
|
9
|
+
- `@southwind-ai/shared` 定义 RBAC、Feature Flag、License、Audit、数据库兼容和 RuoYi-Vue Adapter 公共类型。
|
|
10
|
+
- `@southwind-ai/modules` 定义模块 manifest、模块 registry、菜单、权限、API 权限、功能开关、迁移、工具注册和 License 要求。
|
|
11
|
+
- `@southwind-ai/config` 通过类型化 `ModuleConfigDefinition`、严格 Zod schema、白名单环境变量和 `SecretRef` / `SecretResolver` 定义平台配置入口,覆盖数据库方言、平台 Redis、License、审计和 RuoYi-Vue 兼容开关;未知字段、错误类型和缺失必填 Secret 在监听端口前 fail-fast。
|
|
12
|
+
|
|
13
|
+
<!-- @windy-module system.configuration begin -->
|
|
14
|
+
|
|
15
|
+
详细契约见[类型化模块配置与 Secret 边界](../platform/module-configuration.md)。
|
|
16
|
+
<!-- @windy-module system.configuration end -->
|
|
17
|
+
|
|
18
|
+
- 配置版本只保存 Module Definition 显式开放的非敏感字段;PostgreSQL Repository 提供连续版本、diff、apply 状态、启动恢复与“回滚即新版本”,System Manifest 统一注册 `system.config` Feature、RBAC、API、菜单和审计动作。普通 API、日志与审计不返回 Secret 或 `SecretRef`。
|
|
19
|
+
- `@southwind-ai/database` 定义平台 migration runner、方言 Adapter、dry-run、执行报告和迁移历史记录接口。
|
|
20
|
+
- `apps/server` 暴露严格受 `system.admin` / `system.admin.access` Guard 保护的 `/api/admin/bootstrap`,只汇总 Windy Admin Route、菜单和访问判定所需元数据。
|
|
21
|
+
- `apps/web` 建立通用 Loading、Error、Empty、Theme、Toast 基座,Header 接入主题切换。
|
|
22
|
+
|
|
23
|
+
## 运行时边界
|
|
24
|
+
|
|
25
|
+
当前 Server Guard 负责表达并执行统一判定结构:
|
|
26
|
+
|
|
27
|
+
- Request Guard:生成请求上下文和 requestId。
|
|
28
|
+
- License 版本 Guard:仅当 Feature 在 Module Manifest 声明 `allowedLicenseVersions` 时,校验当前 V1 License 的单一版本标记是否在业务应用允许集合内。
|
|
29
|
+
- Feature Guard:根据 Feature Flag 的 enabled / visible 判断是否可用。
|
|
30
|
+
- Permission Guard:根据权限要求判断是否匿名放行或缺少 RBAC 权限。
|
|
31
|
+
|
|
32
|
+
Server 已具备 Guard 运行时上下文,可通过真实密码登录签发的 Bearer token 或 `windy.sid` cookie 解析 actor、活跃角色代码、权限和 License 版本,并收集拒绝审计事件。配置 `DATABASE_URL` 后,会从 `server_sessions` 的 token hash 解析未过期、未吊销会话,并通过数据库活跃用户和角色表聚合角色与权限;登录、失败、退出和吊销均审计,生产强制关闭开发 Token 与 `user:` 直通。Server V1 License Resolver 会读取 `licenses` / `license_keys` 并用 `signed_payload` 执行 ED25519 验签,只返回单一 License 版本标记;legacy prototype 模块授权保留在独立读取器中,不能进入正式 Guard。系统资源路由、运维总览和 Agent Tool 列表/执行入口共用 License 版本 → 动态 Feature Flag → RBAC pipeline;后台任务也有同序 Guard 执行契约,任务执行器必须在每次实际执行前获取新上下文,不能复用过期判定。旧 `requiredEntitlement` 仅作为尚未迁移业务代码的兼容字段保留,正式 V1 文件和新模块不再使用。
|
|
33
|
+
|
|
34
|
+
Feature Flag Key 及其元数据只由 Module Manifest 定义。Server 启动时幂等同步 Manifest:新 Key 自动注册、已有 Key 只更新元数据并保留数据库中的 `enabled` / `visible`、Manifest 已移除的 Key 归档保留。Feature 管理 API 不提供创建、导入或删除,只允许更新 `enabled` / `visible`;调用者同时需要 `system.feature.manage` 权限和 `super-admin` 或 `developer` 活跃角色,普通管理员即使误获该权限也会被角色边界拒绝。成功修改与越权拒绝都会进入审计。Feature 管理页和 API 是恢复控制面,不受被管理 Feature 自身约束,避免关闭 RBAC 等开关后无法恢复;它们仍受上述权限与角色边界保护。Guard 以请求为缓存边界按 Key 读取数据库状态,同一请求复用结果,下一请求重新读取;因此 API、Agent Tool 和后台任务无需重启即可看到开关变化。无数据库的开发环境回退 Manifest 默认值,生产无数据库、Key 缺失或读取失败时 fail closed。
|
|
35
|
+
|
|
36
|
+
基础系统模块已具备 Server 端 CRUD API,覆盖用户、角色、部门、菜单、字典、功能开关和 License;未配置 `DATABASE_URL` 时使用内存 Repository,配置后使用 Drizzle Repository 持久化。该实现用于打通 API、权限、分页、过滤、导入导出、软删除和审计事件闭环。
|
|
37
|
+
|
|
38
|
+
Web 端已接入 Vue Router,并在所有 AppShell 路由前从 `/api/admin/bootstrap` 获取当前 actor、权限和 Feature 有效状态快照。侧边栏按服务端 Admin 菜单声明、权限和 Feature 结果过滤;直接访问路由时由 Router Guard 拒绝越权页面;页面操作按钮使用同一快照,Feature 开关编辑能力由 `system.feature.manage` 权限决定,不硬编码角色名称。普通业务账号在根路由前即被拒绝,业务 Module 不因 Server 已安装就进入 Windy Web。前端只承担可见性和交互约束,最终授权仍由 Server Guard 决定。
|
|
39
|
+
|
|
40
|
+
Server 已通过 Swagger 插件提供 OpenAPI 文档,并暴露 Agent Tool 注册、原生执行入口和 `/api/mcp` 无状态 Streamable HTTP Adapter。Agent Tool 来自模块 Manifest 的类型化 JSON Schema 契约,统一 Registry 执行 License 版本、Feature、RBAC、数据范围、输入输出校验、超时和脱敏审计;MCP 入口另受 `system.agent` 与 `system.agent.read` 保护,只发现当前 Actor 获准的只读查询 Tool,调用时再次授权。
|
|
41
|
+
|
|
42
|
+
为吸收 RuoYi-Vue 的系统闭环能力,Server 已提供 `/api/system/operations` 运维总览接口,Web 已提供 `/system/operations` 页面。当前覆盖在线用户、服务监控、缓存监控、定时任务、代码生成和系统接口的状态入口。定时任务区块通过 `/api/system/operations/scheduler` 读取 OPS-02 的真实代码注册定义、PostgreSQL 持久状态与最近历史;`stale` 仍是只读派生观察,但 OPS-04 允许操作员在确认旧 handler 已停止并核对业务副作用后,通过精确版本 CAS 和关键审计把该历史行终结为 `blocked`。该动作只释放后续调度,不会自动执行、重试或补偿任务。缓存监控只读取平台自己的 `REDIS_URL`,不会把延期的 `RUOYI_REDIS_URL` 旧库兼容配置混入平台运行态;接口在 License 版本、动态 Feature Flag 和 RBAC Guard 全部通过后才发起探活。
|
|
43
|
+
|
|
44
|
+
CRUD 代码生成器已从 canonical 模块元数据生成独立的 DTO、PostgreSQL Drizzle schema、Repository、Service、Guard 路由和 Module Manifest,目标存在时拒绝覆盖。运维总览的系统接口权限目录只汇总 composition root 已注册 Manifest,展示 method/path、Permission、Feature、允许的 License 版本和 Guard 顺序;不会扫描源码猜测绑定。
|
|
45
|
+
|
|
46
|
+
Redis 探活使用 Bun 原生客户端并设置总超时,返回正常、超时、认证失败、不可用和未配置五类稳定状态。正常时同时展示完整探活延迟、当前数据库键数、连接客户端数、已用内存、内存上限和使用率。响应中的连接目标会移除用户名、密码和查询参数,底层错误不直接返回前端。默认超时为 1500 ms,可通过 `REDIS_HEALTH_TIMEOUT_MS` 调整;值会限制在 50 至 10000 ms。真实容器回归命令为 `bun run test:redis:health`,会验证带认证 Redis 的成功、错误或缺失密码、暂停容器超时和未配置状态,并在结束时清理容器。
|
|
47
|
+
|
|
48
|
+
## RuoYi-Vue 兼容边界
|
|
49
|
+
|
|
50
|
+
兼容层继续遵循 [RuoYi-Vue 兼容与迁移设计](../adapters/ruoyi-vue-compatibility.md) 的结论:
|
|
51
|
+
|
|
52
|
+
- 新平台使用 canonical schema。
|
|
53
|
+
- RuoYi-Vue 兼容能力是可插拔 Adapter,新项目默认关闭。
|
|
54
|
+
- 业务模块不得直接依赖旧 `sys_*` 表。
|
|
55
|
+
- 旧库运行时兼容默认只读;受控写入本轮明确延期,现有 Adapter 只保留未来扩展边界。
|
|
56
|
+
|
|
57
|
+
本轮已把核心表枚举、迁移计划、迁移报告、权限快照、导入 Adapter、运行时 Adapter 和报告 Adapter 定义到共享契约中。
|
|
58
|
+
|
|
59
|
+
## 已确认决策
|
|
60
|
+
|
|
61
|
+
### License 签发与客户端 SDK
|
|
62
|
+
|
|
63
|
+
License 签发能力作为独立服务放在 `apps/license`,只在厂商内部集中部署,不和客户业务 API 服务混在一起。生产签发中心直接以 PostgreSQL Repository 维护客户、项目、合同版本、节点、独立操作员/会话、公钥元数据、签发文件和追加事件;缺少数据库、migration 或权威 schema 时在监听端口前 fail closed。显式 legacy development 模式仍可读取旧原型状态,但不能签发正式 V1。
|
|
64
|
+
|
|
65
|
+
当前正式能力:
|
|
66
|
+
|
|
67
|
+
- 浏览器控制台维护“客户主体 → 客户项目 → 合同版本 → 部署节点”,支持扫码或手工录入设备码,并签发/下载每台设备独立的 License 文件。
|
|
68
|
+
- V1 使用 `formatVersion: 1`、RFC 8785 canonical JSON、ED25519、`publicKeyId`、最长 10 年必填到期时间和单一 License 版本快照;不包含 Entitlement、Feature、`notBefore`、容量字段或敏感合同资料。
|
|
69
|
+
- 正常签发无需双人审批;签发、下载、替换、撤销标记、备注更正、密钥轮换和操作员治理均通过追加事件表达,不可变数据库触发器禁止改写签发事实。
|
|
70
|
+
- 签发前会检查目标软件版本是否兼容当前公钥;旧公钥保留期覆盖其关联 License 最晚到期时间和兼容期。数据库只保存公钥与轮换元数据,不保存私钥。
|
|
71
|
+
- `@southwind-ai/license` 支持多公钥选择、严格 V1 解析和验签、产品/设备/期限检查、防历史文件降级、5 分钟默认 NTP 容差、时间恢复、固定路径读取和文件系统原子替换。
|
|
72
|
+
- 客户业务 Server 只维护一份当前 License,任意已认证用户可上传通过完整校验的新文件;缺失、过期、验签失败或明显时间回拨时进入限制模式,并在 60/30/7 天提供分级到期提醒。
|
|
73
|
+
|
|
74
|
+
生产内部密钥服务 Provider 已通过 loopback HTTP 连接独立 `windy-signing-service`,后者使用最小权限凭据调用 Vault Transit;未配置、Vault sealed、凭据失效、公钥回验失败时继续 fail closed,不能用本地临时私钥替代。现有公钥轮换 API 只登记外部产生的公钥与兼容关系,不负责创建或验证内部服务中的私钥。客户运行时的安装队列和可信时间高水位当前仍是单进程状态;多副本部署需先增加单写者/跨进程协调与可信时间持久化设计。
|
|
75
|
+
|
|
76
|
+
### 数据库 migration runner 推荐
|
|
77
|
+
|
|
78
|
+
推荐采用“Drizzle Kit + 平台自定义 runner”的组合。
|
|
79
|
+
|
|
80
|
+
Drizzle Kit 负责:
|
|
81
|
+
|
|
82
|
+
- TypeScript schema 到 SQL migration 的生成。
|
|
83
|
+
- PostgreSQL 优先路径下的迁移快照和差异生成。
|
|
84
|
+
- 常规开发环境的 schema 演进。
|
|
85
|
+
|
|
86
|
+
平台自定义 runner 负责:
|
|
87
|
+
|
|
88
|
+
- 读取模块 manifest 中的 migration 描述并按模块顺序执行。
|
|
89
|
+
- 在 PostgreSQL、MySQL、达梦、人大金仓之间选择方言 Adapter。
|
|
90
|
+
- 记录执行历史、耗时、失败原因、审计事件和回滚建议。
|
|
91
|
+
- 支持 dry-run、客户现场迁移报告、RuoYi-Vue 导入前置检查。
|
|
92
|
+
- 屏蔽国产数据库连接、分页、JSON、自增、保留字等差异。
|
|
93
|
+
|
|
94
|
+
不建议只用 Drizzle Kit:它更适合生成和执行标准迁移,不承担产品级审计、客户现场报告、国产库适配和模块化启停编排。
|
|
95
|
+
|
|
96
|
+
不建议完全自研 migration 生成器:早期成本高,容易重复造 schema diff 能力。平台应该封装 runner 和 Adapter,而不是重写 Drizzle Kit 的 schema diff。
|
|
97
|
+
|
|
98
|
+
当前代码状态:
|
|
99
|
+
|
|
100
|
+
- `packages/database` 已实现平台 runner 和内存迁移历史 store。
|
|
101
|
+
- 已支持 dry-run、依赖排序、跳过已执行迁移、收集 SQL statement、失败记录。
|
|
102
|
+
- 已实现 PostgreSQL、MySQL、达梦、人大金仓的方言 Adapter 边界。
|
|
103
|
+
- Drizzle Kit 已接入根配置 `drizzle.config.ts`,首批 canonical schema 位于 `packages/database/src/schema/`,并已生成 `packages/database/drizzle/0000_organic_madame_web.sql`。
|
|
104
|
+
- PostgreSQL 优先路径已实现 `PostgresMigrationHistoryStore`、SQL statement 执行器和 Drizzle SQL 拆分工具。
|
|
105
|
+
- 达梦、人大金仓执行器差异后续通过真实环境验证后补充。
|
|
106
|
+
|
|
107
|
+
### 国产数据库适配方式
|
|
108
|
+
|
|
109
|
+
达梦、人大金仓的 JSON、分页、自增和保留字差异后置实现,但边界采用中间层转换。
|
|
110
|
+
|
|
111
|
+
适配层类似 Router:
|
|
112
|
+
|
|
113
|
+
- 上层 Repository 和业务模块只表达 canonical query / migration 意图。
|
|
114
|
+
- Dialect Adapter 根据目标数据库把 JSON、分页、自增、保留字、类型映射转换为具体 SQL。
|
|
115
|
+
- 每个数据库方言有独立 Adapter,不允许业务模块拼接数据库特定 SQL。
|
|
116
|
+
- 真实差异通过国产库环境验证后补充到 Adapter 测试用例和迁移报告。
|
|
117
|
+
|
|
118
|
+
### RuoYi-Vue 运行时兼容
|
|
119
|
+
|
|
120
|
+
旧库运行时受控写入已明确延期,本轮不实现。
|
|
121
|
+
|
|
122
|
+
默认策略:
|
|
123
|
+
|
|
124
|
+
- 新项目默认不启用 RuoYi-Vue 兼容层。
|
|
125
|
+
- 旧库运行时兼容默认只读。
|
|
126
|
+
- 现有 Adapter 只保留未来扩展边界,不实现旧库写入、双写、补偿或 Redis 运行态兼容。
|
|
127
|
+
- 后续重新启动该范围时,仍必须通过 RuoYi Adapter、审计日志和迁移/兼容报告闭环。
|
|
128
|
+
|
|
129
|
+
### Web 端缺失组件
|
|
130
|
+
|
|
131
|
+
Web 端缺失组件可以在项目内自行实现,放入 `apps/web/src/components`。
|
|
132
|
+
|
|
133
|
+
约束:
|
|
134
|
+
|
|
135
|
+
- shadcn-vue 基础组件仍保留在 `components/ui`。
|
|
136
|
+
- 业务通用组件放在 `components/common`。
|
|
137
|
+
- 页面或布局专用组件放在对应 layout/page 目录。
|
|
138
|
+
- 不为缺失小组件引入无关 UI 库。
|
|
139
|
+
|
|
140
|
+
## 仍需后置实现或环境验收的问题
|
|
141
|
+
|
|
142
|
+
- LIC-02 的厂商侧单机拓扑、本机认证、真实 Provider、Vault Transit adapter 与集成回归已经完成;正式启用仍须完成域名/TLS、Vault 初始化解封、Secret 权限、镜像 digest 和公钥指纹部署验收,本地测试 Provider 不能作为生产配置。
|
|
143
|
+
- 客户 License 当前按单进程、单写者运行边界实现;多实例共享文件、分布式锁和可信时间高水位持久化尚未设计。
|
|
144
|
+
- 审计日志已有 PostgreSQL 有界重试、降级计数/告警和关键事件本地原子恢复队列。业务变更与审计写入仍不是同一数据库/文件事务,失败重试前必须先查询业务状态;恢复队列的磁盘与权限也需纳入部署监控。
|
|
145
|
+
- 后台 NDJSON 批量任务当前只注册字典处理器,artifact 文本存于 PostgreSQL 并受 10 MiB、50,000 行和 256 KiB 单行上限保护。扩大容量前应接入对象存储 Adapter,不能直接把 PostgreSQL 当作无限文件存储。
|
|
146
|
+
- 当前基线的 scheduler `stale` 只观察和提示人工处置,不自动恢复;handler 还必须自行保证外部副作用幂等。
|
|
147
|
+
- 代码生成器不会直接改写 canonical schema 或生产 migration;生成结果必须人工接入并审阅 Drizzle 差异。
|
|
148
|
+
- 客户业务 Web 已闭环真实登录、会话恢复、401 清理和服务端访问快照,但自动化验证不覆盖真实浏览器 Cookie、反向代理 TLS、跨 Origin 或多实例负载均衡。跨 Origin 前必须补 Origin 白名单和独立 CSRF 防护。
|
|
149
|
+
- Ubuntu、Debian、CentOS、OpenEuler、UOS、银河麒麟的真实部署矩阵尚未逐一验证;
|
|
150
|
+
`create-windy`、`@southwind-ai/*` scope 和 `private-enterprise` profile 已确定。
|
|
151
|
+
- 国产数据库 Adapter 的真实达梦、人大金仓验证本轮明确延期。
|
|
152
|
+
- RuoYi-Vue 旧库运行时受控写入、双写、补偿和旧 Redis 运行态兼容本轮明确延期。
|
|
153
|
+
|
|
154
|
+
## 验证命令
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
/Users/rayjason/.bun/bin/bun run typecheck
|
|
158
|
+
/Users/rayjason/.bun/bin/bun run test:redis:health
|
|
159
|
+
/Users/rayjason/.bun/bin/bun run --cwd apps/web build
|
|
160
|
+
PORT=3101 /Users/rayjason/.bun/bin/bun run --cwd apps/server dev
|
|
161
|
+
curl -sS http://127.0.0.1:3101/api/health
|
|
162
|
+
curl -sS http://127.0.0.1:3101/api/foundation
|
|
163
|
+
```
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# 模块初始化 Host
|
|
2
|
+
|
|
3
|
+
## 目标与边界
|
|
4
|
+
|
|
5
|
+
`@southwind-ai/server-sdk` 是业务模块接入 Server 运行时的稳定 Interface。业务模块同时贡献 Module Manifest 与 `setup(host)` 初始化器:Manifest 声明能力,初始化器注册实现,宿主不再手工逐项接线。业务 package 不直接依赖 Elysia、Drizzle 或 Server 内部 Repository,只获得受限 Port。
|
|
6
|
+
|
|
7
|
+
初始化器在 Storage 和 Durable Job Worker 创建前顺序执行,只允许登记定义,不启动监听、Worker 或后台循环。Host 扩展点:
|
|
8
|
+
|
|
9
|
+
- `registerRoutes()`:注册 Guarded API 路由;`method + path` 与权限必须与 Manifest `apiPermissions` 声明一致,Guard(权限 / Feature / License)与 `security.denied` 审计由 `evaluateRouteGuards` 统一执行。声明 `auditAction` 时宿主在 Handler 成功后自动写审计;
|
|
10
|
+
- `registerTaskHandlers()`:为 Manifest `tasks` 已声明的定时任务注册执行体,调度元数据一律以 Manifest 为准;
|
|
11
|
+
- `registerToolHandlers()`:为 Manifest `tools` 已声明的 Agent Tool 注册类型化 JSON 执行体,输入输出 Schema、`scopeMode` 与 Handler 均接入双向校验;
|
|
12
|
+
- `registerRolePresets()`:注册角色预设,`permissionKeys` 必须属于本模块 Manifest 权限声明;预设并入内置角色同步(不存在则创建、漂移则非破坏性合并),不改变 4 个内置角色语义;
|
|
13
|
+
- `registerSearchProviders()`:注册 Manifest `providers` 已声明的搜索 Provider;
|
|
14
|
+
- `registerUploadPolicy()`:按业务用途注册大小、分片、MIME、扩展名和签名策略;
|
|
15
|
+
- `registerResourceFileAccessPolicies()`:注册本模块业务资源到平台文件的受限读取
|
|
16
|
+
Resolver;purpose 必须来自当前模块或 Manifest 显式依赖模块,resourceType、
|
|
17
|
+
Feature、Permission 和 Audit Action 必须来自当前消费者模块;
|
|
18
|
+
- `registerDurableHandler()`:注册强类型 payload parser 与模块 Durable Handler;执行时
|
|
19
|
+
由宿主重建当前 Actor 与提交时授权范围,并只额外提供受限资源文件读取 Port。
|
|
20
|
+
- `registerGovernedResources()`:为 Manifest `dataPolicies` 注册对象授权 Resolver、
|
|
21
|
+
去除治理字段后的普通投影、受控字段值读取、可选 cursor 分页导出 Source,以及
|
|
22
|
+
可选服务端 Renderer 和 Manifest 已声明的 masked/reveal/export Route;宿主负责
|
|
23
|
+
Guard、掩码、再认证、一次性 Grant、审批、Durable Job、水印、格式选择、资源上限、
|
|
24
|
+
TTL、下载/取消与关键审计。
|
|
25
|
+
|
|
26
|
+
注册的所有 Key 必须以模块名作为命名空间。未知模块、重复初始化器、重复注册、命名空间越界、未在 Manifest 声明的实现都会在 Server 监听前以中文错误失败。
|
|
27
|
+
|
|
28
|
+
## 受限 Port(ModuleRequestContext / ModuleToolContext)
|
|
29
|
+
|
|
30
|
+
路由 Handler 只收到 `ModuleRequestContext`,不含 Elysia 内部对象、数据库连接或任何 Repository:
|
|
31
|
+
|
|
32
|
+
- `actor`:当前操作者只读视图(身份、角色、权限),凭据状态与数据范围令牌不下发;
|
|
33
|
+
- `audit`:只允许 emit 本模块 Manifest `auditActions` 已注册的动作,`critical` 事件走宿主 Critical 写入策略;
|
|
34
|
+
- `jobs`:受限 Durable Job 控制 Port。提交只能使用本模块命名空间 Handler,Queue 与
|
|
35
|
+
Actor 由宿主固定;读取、取消和人工恢复只返回最小投影,并在每次操作时重新执行当前
|
|
36
|
+
路由的 Feature、Permission、License 与 Scope Guard;
|
|
37
|
+
|
|
38
|
+
<!-- @windy-module system.storage begin -->
|
|
39
|
+
|
|
40
|
+
- `storage`:受限 Upload/BlobRead Port(详见[对象存储](./object-storage.md));宿主未挂载时任何访问都以明确错误失败。
|
|
41
|
+
|
|
42
|
+
<!-- @windy-module system.storage end -->
|
|
43
|
+
|
|
44
|
+
- `userDirectory`:受 Feature、Permission、宿主 Actor 与 Repository Data Scope 约束的
|
|
45
|
+
启用用户只读最小投影;不暴露 Admin Repository,详见
|
|
46
|
+
[业务模块只读用户目录](../platform/business-user-directory.md)。
|
|
47
|
+
|
|
48
|
+
Agent Tool 也获得同一个 `storage` Interface,但 Port 在每次 Tool Invocation 内根据
|
|
49
|
+
当前 Execution Grant 恢复的 Actor 与服务端 ScopeContext 重新构造;不会复用 Run
|
|
50
|
+
创建时的权限快照。`scopeMode: "request"`、Tool 自身 Feature/Permission Guard、资源
|
|
51
|
+
策略 Feature/Permission Guard 与业务 Resolver 必须全部通过。
|
|
52
|
+
|
|
53
|
+
Module Durable Handler 获得独立的 `ModuleDurableHandlerContext`。它不复用提交 HTTP
|
|
54
|
+
请求对象,也不保存进程内闭包:宿主从持久化 Job 读取 `requestedBy` 与提交 Scope,
|
|
55
|
+
每次执行重新解析当前用户与授权,Scope 漂移即拒绝。其 `storage` 只包含
|
|
56
|
+
`openResourceFile()`,因此后台 Parser 可以按稳定业务资源绑定读取同案其他上传者的原件,
|
|
57
|
+
但不能创建上传、按 Owner 任意读取或接触平台存储实现。
|
|
58
|
+
|
|
59
|
+
`ModuleRequestContext.jobs` 提供 `submit/get/cancel/recover`,但不下发 Repository、
|
|
60
|
+
Worker 或 Queue 选择权。控制操作同时要求任务 Queue/Handler 属于当前模块、任务提交人
|
|
61
|
+
等于当前 Actor,且当前 nominal Scope 与提交快照完全一致;任一不满足都与不存在同形。
|
|
62
|
+
`get` 不返回 payload、租约、Artifact 引用、内部错误或审计明细,只返回状态、数值进度、
|
|
63
|
+
attempt、稳定失败码、时间、可操作性与 CAS version。取消和恢复理由会限长并脱敏,关键
|
|
64
|
+
审计只记录 Actor、模块、任务、状态、attempt/failure/recovery 计数、结果和脱敏理由。
|
|
65
|
+
|
|
66
|
+
取消对 queued 任务原子终结,对 running 任务原子写入取消请求,并由当前 Worker 立即
|
|
67
|
+
abort Handler signal;后续 heartbeat、progress、Artifact 发布和 complete 仍以持久化
|
|
68
|
+
取消事实 fail closed。恢复只接受 failed/cancelled 且无 active lease 的任务,调用方
|
|
69
|
+
必须回传 `attempt + status + version`;宿主生成恢复幂等键,Repository 在同一事务内
|
|
70
|
+
执行 CAS 并追加 Recovery,不覆盖原 Attempt 或 Failure。
|
|
71
|
+
|
|
72
|
+
业务治理注册把 `authorize()` 与 `read()` 分开:按 ID 能读取到记录不是对象级授权。
|
|
73
|
+
普通读取由宿主按 Manifest 字段策略生成 `MaskResult`;明文 Route 必须同时通过当前
|
|
74
|
+
Feature、`sensitive-disclosure` Permission、业务对象授权、理由校验和当前账号密码
|
|
75
|
+
再认证。宿主只在关键审计成功后返回带一次性 Grant ID 的明文结果;未知、越权和业务
|
|
76
|
+
Resolver 失败不返回底层 Repository 细节。
|
|
77
|
+
|
|
78
|
+
治理导出 Source 只接收宿主固定的 Policy、查询快照、当前 Actor、每页上限、不透明
|
|
79
|
+
cursor 与取消 Signal。客户端不能选择策略或治理字段,业务不能获取平台 Workflow、
|
|
80
|
+
Job Repository、BlobStore 或下载 Port。Worker 在开始及每一页前重新解析 Actor,
|
|
81
|
+
复查 Feature、Permission、License 与 Scope;任何撤销或范围漂移都会在下一页前
|
|
82
|
+
fail-closed。
|
|
83
|
+
|
|
84
|
+
治理导出 Renderer 只接收上述逐页授权记录组成的 `AsyncIterable`、平台水印、
|
|
85
|
+
`AbortSignal` 与 heartbeat。Host 在监听前校验格式、模板、Manifest JSON Schema、
|
|
86
|
+
MIME、扩展名和 records/bytes/timeout 上限;请求固化选择,恢复执行时注册丢失或版本
|
|
87
|
+
漂移均 fail closed。详见[受治理导出 Renderer](../platform/governed-export-renderers.md)。
|
|
88
|
+
|
|
89
|
+
## Composition 顺序
|
|
90
|
+
|
|
91
|
+
1. 加载并校验已安装的 Module Manifest;
|
|
92
|
+
2. 按安装顺序执行 `InstallableServerModule.setup(host)`(注册时即校验同一安装单元的 Manifest 声明一致性);
|
|
93
|
+
3. 合并平台和业务上传策略,创建 Storage Runtime;
|
|
94
|
+
4. 同步内置角色与模块角色预设;
|
|
95
|
+
5. 把业务 Durable Handler 安装到 Registry,由 Host 注册的任务 Handler 与 Manifest 合并为调度定义;
|
|
96
|
+
6. 创建 Worker、安装模块 Guarded Route、适配搜索 Provider 与 Agent Tool;
|
|
97
|
+
7. 最终 `validateServerModuleComposition` 校验:声明了没实现、实现了没声明都报错。
|
|
98
|
+
|
|
99
|
+
示例工单(work-order)是声明式装配样板:初始化器注册路由、任务、搜索 Provider、角色预设与附件上传策略,宿主运行时就绪后经 `attach()` 延迟挂载模块服务。未安装该模块时,初始化器与全部注册项都不会进入生成项目。
|
|
100
|
+
|
|
101
|
+
## 约束
|
|
102
|
+
|
|
103
|
+
- Host Interface 只承载跨模块稳定能力,不暴露数据库连接或业务 Repository。
|
|
104
|
+
- 初始化器不得跨模块注册 key,也不得依赖注册顺序读取另一个模块的私有状态。
|
|
105
|
+
- 上传授权仍由 Server Route Guard 和文件 Owner/RBAC 负责;注册策略不等于获得上传权限。
|
|
106
|
+
- 跨上传者资源读取只允许通过资源策略:Resolver 只返回授权判定,必须回查业务关联,
|
|
107
|
+
不能返回或下发 Repository;直接 fileId、文件名或不可猜引用都不能代替资源授权。
|
|
108
|
+
|
|
109
|
+
<!-- @windy-module system.scheduler begin -->
|
|
110
|
+
|
|
111
|
+
- Durable Handler 仍需满足幂等、租约、取消和 fencing 约束,详见[Durable Job 深模块](./durable-jobs.md)。
|
|
112
|
+
|
|
113
|
+
<!-- @windy-module system.scheduler end -->
|
|
114
|
+
|
|
115
|
+
- 整体设计取舍见[Server Module Interface](./server-module-interface.md)。
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Server Module Interface
|
|
2
|
+
|
|
3
|
+
## 定位
|
|
4
|
+
|
|
5
|
+
Server Module Interface 是可安装业务模块与平台宿主之间的唯一契约:下游业务包(独立仓库)以一个 `InstallableServerModule` 同时贡献 `manifest`(声明)与 `setup(host)`(实现注册),宿主按统一顺序装配 Route、Task、Tool、Upload Policy、Role Preset、Search Provider 与 Migration Bundle,不再为每个模块手工接线。
|
|
6
|
+
|
|
7
|
+
分层:
|
|
8
|
+
|
|
9
|
+
- `packages/server-sdk`:公开 Interface。纯类型加少量校验辅助,不依赖 Elysia / Drizzle / 宿主内部类型;
|
|
10
|
+
- `apps/server/src/module-host/`:宿主实现。注册收集、Manifest 一致性校验、Guarded Route 安装、受限 Port 构造与各子系统适配;
|
|
11
|
+
- `packages/example-work-order`:拥有 Manifest 与 `setup(host)` 注册过程;
|
|
12
|
+
- `apps/server/src/example-modules.ts`:只注入 Repository、Handler 等宿主适配器,并提供统一的运行时 attach;
|
|
13
|
+
- `apps/server/src/installed-business-modules.ts`:唯一安装清单,Foundation、Host 初始化和运行时 attach 都从该清单派生。
|
|
14
|
+
|
|
15
|
+
## 声明与实现的一致性
|
|
16
|
+
|
|
17
|
+
Manifest 是能力的唯一事实源,双向校验保证不漂移:
|
|
18
|
+
|
|
19
|
+
- 注册时(`initializeModuleHosts`):安装单元的 Manifest 必须与平台能力目录是同一声明;实现必须与 Manifest 声明一致——路由绑定、任务、工具、Provider 未声明即抛错;角色预设的权限必须属于本模块声明;
|
|
20
|
+
- 装配后(`validateServerModuleComposition`):声明了没实现(缺 Handler)与实现了没声明(`moduleRegistrations` 比对)都在启动前报错;
|
|
21
|
+
- 所有 Key 强制模块命名空间(等于模块名或以 `<moduleName>.` 开头),跨模块重复注册立即失败。
|
|
22
|
+
|
|
23
|
+
## 受限 Port 的取舍
|
|
24
|
+
|
|
25
|
+
- **Actor 只读白名单视图**:宿主为 `ModuleActor` 构造冻结的最小对象,不把凭据状态与数据范围令牌带入模块运行时。宿主内部通过不可伪造的映射恢复原始 `RequestActor`,外部模块无法扩权或伪造该关联。
|
|
26
|
+
- **初始化两阶段**:`setup(host)` 在 Storage / Durable Worker 创建之前运行,只能登记定义;需要宿主服务的实现(路由 Handler、任务执行体、Provider)通过延迟绑定(`requireService` 闭包 + 安装清单统一 `attach()`)解析,与平台既有 `DeferredLegacyExecutor` 模式一致。
|
|
27
|
+
- **Queue 与提交人不可伪造**:模块 Jobs Port 只能提交本模块命名空间的 Handler,Queue 强制为模块名,提交人由宿主以当前 Actor 补齐;Worker 与 Repository 始终由宿主持有。
|
|
28
|
+
- **审计白名单**:模块只能 emit 自己 Manifest 注册的审计动作;Guard 拒绝(`security.denied`)由宿主统一记录,模块无法关闭。
|
|
29
|
+
- **存储最小授权**:上传 purpose 强制属于本模块已注册 Upload Policy,Owner 由宿主
|
|
30
|
+
以当前 Actor 补齐。Owner 读取保持用途约束;跨上传者读取的 purpose 只可来自当前
|
|
31
|
+
模块或 Manifest 显式依赖模块,还必须命中 `purpose + resourceType` 策略、当前
|
|
32
|
+
Actor/Scope、消费者 Manifest Feature/Permission 和 project-owned 资源绑定
|
|
33
|
+
Resolver。返回值移除 Owner 与 ArtifactRef,模块接触不到 `BlobStore`、Upload
|
|
34
|
+
Repository 或任意 manage 能力。
|
|
35
|
+
- **角色预设非破坏性合并**:预设随内置角色同步,手工追加的授权保留;与既有角色 Code 冲突直接启动失败,不做静默覆盖。
|
|
36
|
+
|
|
37
|
+
## 暂不迁移的部分
|
|
38
|
+
|
|
39
|
+
系统模块(auth / license / storage / scheduler 等平台能力)仍保持原有的宿主内部接线,不经过 ModuleHost 路由注册;Module Interface 面向的是业务模块。平台任务的硬编码 Handler 映射(`createPlatformTaskDefinitions`)同样保留,业务任务一律改走 `registerTaskHandlers`。
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Web 权限快照韧性
|
|
2
|
+
|
|
3
|
+
## 目标
|
|
4
|
+
|
|
5
|
+
管理端不能把登录时或首次导航时取得的权限长期当作事实。用户停留在页面期间,角色、
|
|
6
|
+
Permission、Feature 或菜单策略可能被管理员调整;Web 必须尽快同步,撤销时停止展示和
|
|
7
|
+
操作,同时不能静默丢失正在编辑的内容。
|
|
8
|
+
|
|
9
|
+
服务端 Guard 仍是最终授权边界。本机制只负责让客户端在服务端拒绝请求之前完成一致的
|
|
10
|
+
界面降级,不能代替 API 的 Permission、Feature、License 与数据范围检查。
|
|
11
|
+
|
|
12
|
+
## 快照身份与变化
|
|
13
|
+
|
|
14
|
+
`useAccessSnapshot` 是唯一前端权限快照源。每次成功加载会对以下授权事实生成排序后的
|
|
15
|
+
确定性签名和 `access-v1-*` 指纹:
|
|
16
|
+
|
|
17
|
+
- Actor ID、类型、角色和 Permission;
|
|
18
|
+
- Feature 的 enabled、visible、allowed 与 reason;
|
|
19
|
+
- 菜单 Key、路径、Permission/Feature 绑定和层级。
|
|
20
|
+
|
|
21
|
+
比较使用完整签名,不依赖短指纹碰撞结果。变化按能力集合分类:
|
|
22
|
+
|
|
23
|
+
- `reduced`:至少一个 Permission、已授权 Feature 或可见菜单被移除;
|
|
24
|
+
- `expanded`:没有移除且至少新增一项能力;
|
|
25
|
+
- `changed`:签名变化但能力集合不变,例如角色或策略原因变化。
|
|
26
|
+
|
|
27
|
+
快照版本只在当前登录生命周期内单调递增;退出或重置会清空版本、指纹和变化提示。
|
|
28
|
+
|
|
29
|
+
## 运行中刷新
|
|
30
|
+
|
|
31
|
+
管理端进入路由时仍由 Router Guard 强制刷新。停留期间还会:
|
|
32
|
+
|
|
33
|
+
- 每 60 秒刷新一次;
|
|
34
|
+
- 窗口重新获得焦点、浏览器恢复在线或页面重新可见时刷新;
|
|
35
|
+
- 对短时间连续浏览器事件做 5 秒合并;
|
|
36
|
+
- 复用 `useAccessSnapshot` 的 single-flight,避免并发请求覆盖。
|
|
37
|
+
|
|
38
|
+
刷新成功后,侧栏、按钮和当前路由判断同时读取新快照。刷新失败不删除最后一个成功
|
|
39
|
+
快照,但访问边界会以全屏阻断层停止整个管理端的交互,直到重新检查成功;权限快照请求
|
|
40
|
+
会抑制通用网络错误页跳转,以免卸载当前编辑实例。旧快照只用于保留上下文,不能继续
|
|
41
|
+
授权界面操作。Router Guard 主动导航时仍会把失败导向统一服务不可用页。
|
|
42
|
+
|
|
43
|
+
## 页面安全降级与编辑保护
|
|
44
|
+
|
|
45
|
+
`AccessSnapshotBoundary` 包住管理端页面 slot:
|
|
46
|
+
|
|
47
|
+
1. 当前路由不再满足最新 Permission 或 Feature 时,页面节点不卸载,但会设为 inert、
|
|
48
|
+
隐藏并阻止指针和键盘交互。
|
|
49
|
+
2. 快照刷新失败时采用同样的 fail-closed 降级,并提供重新检查。
|
|
50
|
+
3. 路由仍获准时,统一提示能力收紧、扩展或其它变化;“安全刷新”走浏览器刷新。
|
|
51
|
+
4. 返回首页走 Vue Router,不绕过页面已有的 `onBeforeRouteLeave`。
|
|
52
|
+
|
|
53
|
+
页面实例不卸载,因此响应式表单值不会因权限变化被清空。存在未保存编辑时:
|
|
54
|
+
|
|
55
|
+
- 返回首页或其它应用内导航继续使用统一 `UnsavedChangesDialog`;
|
|
56
|
+
- 浏览器安全刷新继续触发页面已有的 `beforeunload`;
|
|
57
|
+
- 保存失败时保留输入和当前页面,不自动跳转。
|
|
58
|
+
|
|
59
|
+
权限已撤销后,保存请求仍必须由服务端 Guard 拒绝;用户可以等待权限恢复后重新检查,
|
|
60
|
+
或使用页面原有的明确重置/放弃路径再离开。
|