create-windy 0.2.32 → 0.2.34
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/dist/cli.js +9 -1
- package/package.json +1 -1
- package/template/.agents/skills/windy-business-module/SKILL.md +10 -1
- package/template/.windy-template.json +2 -2
- package/template/AGENTS.md +11 -0
- package/template/apps/agent-server/README.md +10 -0
- package/template/apps/agent-server/src/southwind_agent_server/app.py +62 -1
- package/template/apps/agent-server/src/southwind_agent_server/contracts.py +35 -0
- package/template/apps/agent-server/src/southwind_agent_server/engine.py +2 -0
- package/template/apps/agent-server/src/southwind_agent_server/openai_engine.py +31 -3
- package/template/apps/agent-server/src/southwind_agent_server/openai_provider.py +22 -9
- package/template/apps/agent-server/src/southwind_agent_server/provider_audit.py +144 -0
- package/template/apps/agent-server/src/southwind_agent_server/provider_governance.py +227 -0
- package/template/apps/agent-server/src/southwind_agent_server/service.py +1 -0
- package/template/apps/agent-server/tests/test_api.py +16 -1
- package/template/apps/agent-server/tests/test_openai_provider.py +79 -0
- package/template/apps/agent-server/tests/test_provider_governance.py +215 -0
- package/template/apps/server/src/agent/provider-input-governance.test.ts +82 -0
- package/template/apps/server/src/agent/provider-input-governance.ts +140 -0
- package/template/apps/server/src/agent/remote-runtime.test.ts +22 -0
- package/template/apps/server/src/agent/remote-runtime.ts +1 -0
- package/template/apps/server/src/agent/run-contracts.ts +2 -0
- package/template/apps/server/src/agent/run-facade.test.ts +52 -2
- package/template/apps/server/src/agent/run-facade.ts +36 -2
- package/template/apps/server/src/index.ts +28 -45
- package/template/apps/server/src/module-host/initialize-contracts.ts +6 -0
- package/template/apps/server/src/module-host/initialize.ts +30 -44
- package/template/apps/server/src/module-host/request-context.ts +23 -17
- package/template/apps/server/src/module-host/resource-file-policies.test.ts +197 -0
- package/template/apps/server/src/module-host/resource-file-policies.ts +108 -0
- package/template/apps/server/src/module-host/route-declaration.ts +45 -0
- package/template/apps/server/src/module-host/route-installer.ts +27 -3
- package/template/apps/server/src/module-host/storage-port.test.ts +170 -1
- package/template/apps/server/src/module-host/storage-port.ts +134 -1
- package/template/apps/server/src/module-host/storage-scope.ts +96 -0
- package/template/apps/server/src/module-host/tool-handlers.test.ts +305 -0
- package/template/apps/server/src/module-host/tool-handlers.ts +38 -4
- package/template/apps/server/src/module-host/user-directory-composition.test.ts +126 -0
- package/template/apps/server/src/module-host/user-directory-port.test.ts +238 -0
- package/template/apps/server/src/module-host/user-directory-port.ts +143 -0
- package/template/apps/server/src/module-host.ts +4 -0
- package/template/apps/server/src/runtime-development.ts +1 -0
- package/template/apps/server/src/settings/drizzle-watermark-repository.integration.test.ts +4 -0
- package/template/apps/server/src/settings/drizzle-watermark-repository.ts +6 -0
- package/template/apps/server/src/settings/watermark-routes.test.ts +13 -3
- package/template/apps/server/src/settings/watermark-routes.ts +41 -3
- package/template/apps/server/src/system/built-in-roles.ts +1 -0
- package/template/apps/web/src/app/auth/login-page.ts +4 -0
- package/template/apps/web/src/composables/useLoginFlow.webtest.ts +91 -0
- package/template/apps/web/src/composables/useWatermarkSettings.ts +7 -3
- package/template/apps/web/src/composables/useWatermarkSettings.webtest.ts +14 -0
- package/template/apps/web/src/composables/watermark-settings.ts +131 -9
- package/template/apps/web/src/composables/watermark-settings.webtest.ts +73 -7
- package/template/apps/web/src/layout/GlobalWatermark.layering.webtest.ts +3 -1
- package/template/apps/web/src/layout/GlobalWatermark.vue +24 -11
- package/template/apps/web/src/layout/GlobalWatermark.webtest.ts +25 -11
- package/template/apps/web/src/pages/auth/login-page-extension.ts +9 -0
- package/template/apps/web/src/pages/auth/login-page-extension.webtest.ts +10 -0
- package/template/apps/web/src/pages/settings/WatermarkSettingsSection.vue +102 -4
- package/template/apps/web/src/router/index.ts +2 -2
- package/template/apps/web/src/router/shared-scaffold-neutrality.webtest.ts +3 -2
- package/template/docker-compose.yml +2 -0
- package/template/docs/architecture/ai-egress.md +49 -7
- package/template/docs/architecture/ai-runtime.md +20 -1
- package/template/docs/architecture/data-scope-query-enforcement.md +4 -0
- package/template/docs/architecture/object-storage.md +22 -2
- package/template/docs/development/custom-login-page.md +90 -0
- package/template/docs/platform/agent-runtime.md +44 -1
- package/template/docs/platform/business-user-directory.md +82 -0
- package/template/docs/platform/platform-shell-settings.md +14 -9
- package/template/docs/platform/web-system-crud.md +1 -1
- package/template/packages/database/drizzle/0034_funny_mesmero.sql +5 -0
- package/template/packages/database/drizzle/meta/0034_snapshot.json +7253 -0
- package/template/packages/database/drizzle/meta/_journal.json +7 -0
- package/template/packages/database/src/schema/platform-settings.ts +13 -0
- package/template/packages/modules/package.json +1 -1
- package/template/packages/modules/src/ai-operation-composition.test.ts +68 -0
- package/template/packages/modules/src/ai-operation-composition.ts +115 -0
- package/template/packages/modules/src/manifest.ts +26 -0
- package/template/packages/modules/src/provider-data-policy-composition.test.ts +74 -0
- package/template/packages/modules/src/system-audit-actions.ts +5 -0
- package/template/packages/modules/src/system-features.ts +5 -0
- package/template/packages/modules/src/system-module-catalog.test.ts +16 -0
- package/template/packages/modules/src/system-module-catalog.ts +26 -3
- package/template/packages/modules/src/system-permissions.ts +2 -0
- package/template/packages/server-sdk/index.ts +11 -0
- package/template/packages/server-sdk/package.json +3 -2
- package/template/packages/server-sdk/src/ai-operation-registration.ts +2 -1
- package/template/packages/server-sdk/src/module-host.ts +8 -0
- package/template/packages/server-sdk/src/request-context.ts +2 -0
- package/template/packages/server-sdk/src/storage-port.ts +66 -0
- package/template/packages/server-sdk/src/tool-registration.ts +6 -0
- package/template/packages/server-sdk/src/user-directory-port.ts +42 -0
- package/template/packages/shared/src/platform-settings.ts +17 -1
|
@@ -6,9 +6,9 @@ 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 { loginPageComponent } from "@/app/auth/login-page";
|
|
9
10
|
import AccessDeniedPage from "@/pages/AccessDeniedPage.vue";
|
|
10
11
|
import ServiceUnavailablePage from "@/pages/ServiceUnavailablePage.vue";
|
|
11
|
-
import LoginPage from "@/pages/auth/LoginPage.vue";
|
|
12
12
|
import ChangePasswordPage from "@/pages/auth/ChangePasswordPage.vue";
|
|
13
13
|
// @windy-module system.license begin
|
|
14
14
|
import LicensePage from "@/pages/license/LicensePage.vue";
|
|
@@ -72,7 +72,7 @@ export const routes: RouteRecordRaw[] = [
|
|
|
72
72
|
{
|
|
73
73
|
path: "/login",
|
|
74
74
|
name: "login",
|
|
75
|
-
component:
|
|
75
|
+
component: loginPageComponent,
|
|
76
76
|
meta: { title: "登录", layout: "auth", public: true, guestOnly: true },
|
|
77
77
|
},
|
|
78
78
|
{
|
|
@@ -5,10 +5,11 @@ import fixtureSource from "./router-test-fixtures.ts?raw";
|
|
|
5
5
|
import { collectRouteAccess } from "./router-test-fixtures";
|
|
6
6
|
|
|
7
7
|
describe("Router shared-scaffold 中性契约", () => {
|
|
8
|
-
test("
|
|
8
|
+
test("平台登录路由使用 project-owned 业务选择 seam", () => {
|
|
9
9
|
expect(routerSource).toContain(
|
|
10
|
-
'import
|
|
10
|
+
'import { loginPageComponent } from "@/app/auth/login-page";',
|
|
11
11
|
);
|
|
12
|
+
expect(routerSource).toContain("component: loginPageComponent");
|
|
12
13
|
expect(fixtureSource).toContain("collectRouteAccess");
|
|
13
14
|
});
|
|
14
15
|
|
|
@@ -64,6 +64,8 @@ services:
|
|
|
64
64
|
environment:
|
|
65
65
|
AGENT_SERVER_INTERNAL_TOKEN: ${AGENT_SERVER_INTERNAL_TOKEN:-}
|
|
66
66
|
AGENT_RUN_STORE_PATH: /app/data/agent/runs.sqlite3
|
|
67
|
+
AI_PROVIDER_AUDIT_STORE_PATH: /app/data/agent/provider-audit.sqlite3
|
|
68
|
+
AI_PROVIDER_AUDIT_RETENTION_DAYS: ${AI_PROVIDER_AUDIT_RETENTION_DAYS:-90}
|
|
67
69
|
QWEN_API_URL: ${QWEN_API_URL:-}
|
|
68
70
|
QWEN_API_KEY: ${QWEN_API_KEY:-}
|
|
69
71
|
QWEN_MODEL: ${QWEN_MODEL:-}
|
|
@@ -23,6 +23,27 @@ Operation 分别声明 Provider egress 与 Tool execution network policy:
|
|
|
23
23
|
mode: "configured-endpoint",
|
|
24
24
|
deploymentKey: "ai.openai-compatible",
|
|
25
25
|
},
|
|
26
|
+
providerDataPolicy: {
|
|
27
|
+
inputFields: [
|
|
28
|
+
{
|
|
29
|
+
jsonPointer: "/question",
|
|
30
|
+
classification: "internal",
|
|
31
|
+
dlpAction: "redact",
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
envelopeFields: [
|
|
35
|
+
{
|
|
36
|
+
field: "instructions",
|
|
37
|
+
classification: "internal",
|
|
38
|
+
dlpAction: "reject",
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
field: "user-prompt",
|
|
42
|
+
classification: "internal",
|
|
43
|
+
dlpAction: "redact",
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
},
|
|
26
47
|
toolNetworkPolicy: {
|
|
27
48
|
mode: "forbidden",
|
|
28
49
|
},
|
|
@@ -36,6 +57,14 @@ Operation 分别声明 Provider egress 与 Tool execution network policy:
|
|
|
36
57
|
不能携带 URL、Host 或 Secret。
|
|
37
58
|
- 当前 Tool 网络策略只支持 `forbidden`,缺省也是 `forbidden`。Provider egress 不会
|
|
38
59
|
继承给 Tool。
|
|
60
|
+
- `inputFields` 是业务 raw input 的 JSON Pointer allowlist。Server 先选择并执行 DLP,
|
|
61
|
+
再把最小输入交给业务 `prepare` Handler;未列字段不会进入 Handler、Agent Server
|
|
62
|
+
DTO 或 Provider 请求。
|
|
63
|
+
- `envelopeFields` 是 Provider outbound envelope 的语义字段 allowlist,当前包含
|
|
64
|
+
`instructions`、`user-prompt`、`tool-definitions`、`provider-output` 和
|
|
65
|
+
`tool-results`。每轮实际发送前重新检查,字段遗漏或未知角色 fail closed。
|
|
66
|
+
- `dlpAction=reject` 在命中后禁止发送;`redact` 使用固定占位符替换。内置检测覆盖
|
|
67
|
+
credential/token、Bearer、邮箱、手机号、身份证号和银行卡/长账号。
|
|
39
68
|
|
|
40
69
|
## Provider 强制执行
|
|
41
70
|
|
|
@@ -50,9 +79,25 @@ Key。Agent Server 在发出模型请求前核对:
|
|
|
50
79
|
2. Deployment Key 必须等于当前 Adapter 的配置 Key;
|
|
51
80
|
3. 启动时已验证配置 Endpoint 的 HTTPS Host allowlist;
|
|
52
81
|
4. URL 仍来自 Agent Server 配置,Key 仍由 `SecretRef` 最后时刻解析。
|
|
82
|
+
5. raw input 已按 JSON Pointer allowlist 最小化,outbound envelope 也命中显式字段规则;
|
|
83
|
+
6. DLP 已完成且本次 Provider attempt 审计成功落库。
|
|
53
84
|
|
|
54
85
|
浏览器 API、SSE、错误响应和 Web 环境均不包含 URL、Host allowlist 或 API Key。
|
|
55
86
|
|
|
87
|
+
## Provider attempt 审计与留存
|
|
88
|
+
|
|
89
|
+
OpenAI-compatible Adapter 不启用隐式 HTTP retry。每次
|
|
90
|
+
`POST /chat/completions` 都对应唯一 `(runId, attempt)`,在打开网络请求前追加一条
|
|
91
|
+
Provider 发送审计;审计失败即禁止该次请求。记录只包含 Deployment、Operation、
|
|
92
|
+
Run、Attempt、分类集合、实际序列化 HTTP request body 字节数、策略决定和时间,不
|
|
93
|
+
保存 prompt、body、Tool result、Provider response 或 Secret。
|
|
94
|
+
|
|
95
|
+
`AI_PROVIDER_AUDIT_STORE_PATH` 必须在配置 Provider 时存在;
|
|
96
|
+
`AI_PROVIDER_AUDIT_RETENTION_DAYS` 默认 90 天、范围 1 到 3650 天。记录在留存期内
|
|
97
|
+
append-only,只有 TTL 清理可以删除过期行,并保留最近/累计删除计数。需要内部 Token
|
|
98
|
+
的 `GET /health/provider-governance` 只返回聚合计数与时间,不返回 Run ID 或内容。
|
|
99
|
+
当前 SQLite Adapter 只支持单副本;共享多副本必须换成集中式 append-only Adapter。
|
|
100
|
+
|
|
56
101
|
## Tool 强制执行
|
|
57
102
|
|
|
58
103
|
短时 Execution Grant 保存宿主校验过的 Operation。每次 Tool 回调重新读取 Grant,并
|
|
@@ -65,16 +110,13 @@ Key。Agent Server 在发出模型请求前核对:
|
|
|
65
110
|
|
|
66
111
|
## 尚未覆盖
|
|
67
112
|
|
|
68
|
-
|
|
113
|
+
当前实现完成契约拆分、Provider 精确配置绑定、字段 allowlist、DLP、逐 attempt 审计、
|
|
114
|
+
TTL 与聚合观测,但
|
|
69
115
|
不把以下能力宣称为已完成:
|
|
70
116
|
|
|
71
|
-
- 现有 `dataClassification` 是 Operation 级分类,尚未提供字段级数据分类、发送字段
|
|
72
|
-
allowlist、脱敏或 DLP 检查;
|
|
73
|
-
- 现有 Run 与 Tool 审计不等于逐次 Provider 发送审计;尚未记录目标 Deployment、
|
|
74
|
-
数据分类、字节数与策略决定的独立 append-only 事实;
|
|
75
117
|
- 同进程 project-owned Tool Handler 不是 OS 网络沙箱。平台契约不授予网络 Port,
|
|
76
118
|
但无法拦截业务代码直接调用全局 `fetch`;生产 deny-egress 仍需容器网络策略或防火墙;
|
|
77
119
|
- DNS 解析钉扎、代理治理和多 Provider/多 Endpoint 动态路由尚未实现。
|
|
78
120
|
|
|
79
|
-
|
|
80
|
-
|
|
121
|
+
不能声明业务 Tool 已获得进程级强制断网。DLP 是保守的模式检测,不替代行业专用实体
|
|
122
|
+
识别、人工审批或部署侧代理/防火墙。
|
|
@@ -272,13 +272,32 @@ interface ModuleAiOperationDefinition {
|
|
|
272
272
|
mode: "forbidden" | "configured-endpoint";
|
|
273
273
|
deploymentKey?: string;
|
|
274
274
|
};
|
|
275
|
+
providerDataPolicy?: {
|
|
276
|
+
inputFields: readonly {
|
|
277
|
+
jsonPointer: string;
|
|
278
|
+
classification: "public" | "internal" | "sensitive";
|
|
279
|
+
dlpAction: "reject" | "redact";
|
|
280
|
+
}[];
|
|
281
|
+
envelopeFields: readonly {
|
|
282
|
+
field:
|
|
283
|
+
| "instructions"
|
|
284
|
+
| "user-prompt"
|
|
285
|
+
| "tool-definitions"
|
|
286
|
+
| "provider-output"
|
|
287
|
+
| "tool-results";
|
|
288
|
+
classification: "public" | "internal" | "sensitive";
|
|
289
|
+
dlpAction: "reject" | "redact";
|
|
290
|
+
}[];
|
|
291
|
+
};
|
|
275
292
|
toolNetworkPolicy?: {
|
|
276
293
|
mode: "forbidden";
|
|
277
294
|
};
|
|
278
295
|
}
|
|
279
296
|
```
|
|
280
297
|
|
|
281
|
-
|
|
298
|
+
Provider 与 Tool 网络策略缺省均 fail closed。configured Provider 还必须声明字段
|
|
299
|
+
allowlist;业务 raw input 会在 Handler 前按 JSON Pointer 裁剪,outbound envelope 在
|
|
300
|
+
每个 Provider attempt 前再次治理。Provider 的实际 Endpoint 与 HTTPS Host allowlist 只存在于
|
|
282
301
|
服务端 Deployment 配置,不由业务 Manifest、浏览器或模型提供。详见
|
|
283
302
|
[Agent Provider 与 Tool 网络边界](./ai-egress.md)。
|
|
284
303
|
|
|
@@ -24,3 +24,7 @@ Core 只理解 ownership dimension 的稳定 key/value 契约,不知道下游
|
|
|
24
24
|
通用资源路由的 `dataAccess` 是必填判别联合:资源必须明确声明 `request` 并提供 scoped access,或声明 `platform`,不能因漏配可选字段静默回退裸 Repository。V1 异步 bulk handler 只接受显式 `platform` 资源,由服务端构造 nominal `all` context;需要继承申请人数据范围的异步资源必须先设计不可篡改的 context snapshot,不允许套用当前平台模式。工单运营快照同样使用服务端平台任务 Actor,不再暴露或调用裸 Repository。
|
|
25
25
|
|
|
26
26
|
Search Provider 只能在 D1–D3 全部闭环后接入,不得先用 Provider 内存裁剪冒充 Repository 级强制。
|
|
27
|
+
|
|
28
|
+
业务模块用户目录同样复用这条 Repository 强制链。V1 `tenantId=single-tenant` 是宿主
|
|
29
|
+
nominal `ScopeContext` 的明确固定租户,不是省略租户判断;业务调用方只提交分页与
|
|
30
|
+
搜索,不能提交 Actor、tenant、department 或 scope。
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
## 模块与 seam
|
|
4
4
|
|
|
5
|
-
`@southwind-ai/storage`
|
|
5
|
+
`@southwind-ai/storage` 是平台文件能力的唯一底层模块。`BlobStore` 只供平台 Storage
|
|
6
|
+
实现使用:流式写入、流式打开和删除;文件名、MIME、上传会话、Owner、权限与审计不
|
|
7
|
+
进入这个底层接口,由上层上传模块统一处理。可安装业务模块只学习
|
|
8
|
+
`@southwind-ai/server-sdk` 的 `ModuleStoragePort`。
|
|
6
9
|
|
|
7
10
|
`ArtifactRef` 使用 `blob_` 加 128 位随机值,不包含目录、租户或内容摘要,不能由业务主键推算。`ContentHash` 当前固定为 SHA-256。调用方可以声明大小和摘要,本地 Adapter 会在写入过程中验证;任何不一致都不会发布 Blob。
|
|
8
11
|
|
|
@@ -60,14 +63,29 @@ Logo 使用 `branding-logo` 上传策略。匿名 `/api/platform/files/:id` 只
|
|
|
60
63
|
|
|
61
64
|
## 模块受限 Upload/BlobRead Port
|
|
62
65
|
|
|
63
|
-
业务模块不直接使用 `UploadService` / `FileService`,而是经
|
|
66
|
+
业务模块不直接使用 `UploadService` / `FileService`,而是经
|
|
67
|
+
`ModuleRequestContext.storage` 或 `ModuleToolContext.storage` 获得
|
|
68
|
+
`ModuleStoragePort`(宿主实现见 `apps/server/src/module-host/storage-port.ts`):
|
|
64
69
|
|
|
65
70
|
- `createUploadSession` 的 purpose 强制属于本模块经 Host 注册的 Upload Policy;跨模块用途和平台基础用途(如 `bulk-data`、`branding-logo`)一律以 `UPLOAD_FORBIDDEN` 拒绝。Owner 由宿主以当前 Actor 补齐,模块无法代他人创建会话。
|
|
66
71
|
- `openFile` 走 `FileService.open` 的 Owner 约束,并在返回前校验文件用途属于本模块——即使是本人文件,用途越界也拒绝。
|
|
67
72
|
- `openPublicFile` 走 `FileService.openPublic` 的 Purpose 约束,且只允许本模块已注册用途。
|
|
73
|
+
- `openResourceFile` 用于同一业务范围内的跨上传者读取。模块必须先通过
|
|
74
|
+
`registerResourceFileAccessPolicies()` 为精确 `purpose + resourceType` 注册
|
|
75
|
+
project-owned 授权 Resolver,并绑定同 Manifest 的 Feature、Permission 与 Audit
|
|
76
|
+
Action。Host 每次调用都使用当前请求或 Tool grant 的 Actor 与服务端 ScopeContext
|
|
77
|
+
重跑 Guard;Resolver 必须从业务 Repository 回查 `resourceId -> fileId` 绑定和案件/
|
|
78
|
+
项目成员资格,客户端传入的 ID 不是授权证据。
|
|
79
|
+
- 资源读取只返回 `id/purpose/filename/mimeType/byteSize/sha256/body`;不会返回
|
|
80
|
+
Owner、ArtifactRef、对象存储 Key 或 Repository。成功和拒绝都写稳定审计,仅记录
|
|
81
|
+
`resourceType/resourceId/fileId/purpose`,不记录文件名、路径或内容。
|
|
68
82
|
- 模块不直接接触 `BlobStore` 或 Upload Repository;宿主未挂载存储服务时,任何访问都以明确错误失败,不静默绕过。
|
|
69
83
|
|
|
70
84
|
用途唯一性仍在 Host 注册时全局校验:命名空间重叠的模块(如 `shop` 与 `shop.sub`)注册同一 purpose 会在启动时失败。
|
|
85
|
+
资源策略的 purpose 必须由当前模块或其 Manifest 显式依赖模块的 Upload Policy 注册;
|
|
86
|
+
这允许 `media -> evidence` 之类的明确消费者关系,但拒绝平台基础用途和未声明的跨模块
|
|
87
|
+
读取。Feature、Permission、Audit Action 和 resourceType 仍必须属于当前消费者模块,
|
|
88
|
+
任一未知或越界都会在监听前失败。
|
|
71
89
|
|
|
72
90
|
## 当前切片
|
|
73
91
|
|
|
@@ -75,3 +93,5 @@ Logo 使用 `branding-logo` 上传策略。匿名 `/api/platform/files/:id` 只
|
|
|
75
93
|
- F2:上传会话、分片续传、文件元数据、校验和清理已完成。
|
|
76
94
|
- F3:服务端授权下载、审计、bulk artifact 与 Logo 接入已完成。
|
|
77
95
|
- F4:模块受限 Upload/BlobRead Port 已完成,业务模块经 Host 注册用途并受命名空间约束。
|
|
96
|
+
- F5:Route 与 Tool 共用资源绑定读取 Port,支持受审计、受数据范围约束的跨上传者
|
|
97
|
+
业务文件流读取,不扩散 Storage/Admin Repository。
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# 业务项目定制登录页
|
|
2
|
+
|
|
3
|
+
Windy 允许业务项目替换完整登录页,但认证、会话和跳转安全仍复用平台能力。扩展入口是:
|
|
4
|
+
|
|
5
|
+
```text
|
|
6
|
+
apps/web/src/app/auth/login-page.ts
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
该文件在 `.windy/files.json` 中属于 `project-owned`。Windy 升级会为旧项目新增它,
|
|
10
|
+
但一旦文件存在,后续 `windy update` 只会保留,不会覆盖业务选择。平台默认登录页
|
|
11
|
+
`apps/web/src/pages/auth/LoginPage.vue` 仍是 `windy-managed`,Router 仍是
|
|
12
|
+
`shared-scaffold`;业务项目不应直接修改这两处。
|
|
13
|
+
|
|
14
|
+
## 默认行为
|
|
15
|
+
|
|
16
|
+
新项目和未定制的升级项目默认把 selector 指向平台登录页,因此视觉、品牌设置、开发
|
|
17
|
+
认证提示和认证流程与原有 Windy 体验一致:
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import PlatformLoginPage from "@/pages/auth/LoginPage.vue";
|
|
21
|
+
import { defineLoginPageComponent } from "@/pages/auth/login-page-extension";
|
|
22
|
+
|
|
23
|
+
export const loginPageComponent = defineLoginPageComponent(PlatformLoginPage);
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`defineLoginPageComponent` 是类型化 Composition seam。Router 只消费这个稳定导出,
|
|
27
|
+
不感知具体业务页面。
|
|
28
|
+
|
|
29
|
+
## 完整业务页面
|
|
30
|
+
|
|
31
|
+
将业务页面放在同一个 project-owned 目录,例如
|
|
32
|
+
`apps/web/src/app/auth/SharpSwordLoginPage.vue`,然后只修改 selector:
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import SharpSwordLoginPage from "./SharpSwordLoginPage.vue";
|
|
36
|
+
import { defineLoginPageComponent } from "@/pages/auth/login-page-extension";
|
|
37
|
+
|
|
38
|
+
export const loginPageComponent = defineLoginPageComponent(SharpSwordLoginPage);
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
业务 SFC 使用 Vue 3 Composition API 和 TypeScript。认证提交必须复用
|
|
42
|
+
`useLoginFlow()`,不要直接复制登录 API 或解析 `returnTo`:
|
|
43
|
+
|
|
44
|
+
```vue
|
|
45
|
+
<script setup lang="ts">
|
|
46
|
+
import DevelopmentAuthWarning from "@/components/auth/DevelopmentAuthWarning.vue";
|
|
47
|
+
import LoginForm from "@/components/auth/LoginForm.vue";
|
|
48
|
+
import { useLoginFlow } from "@/composables/useLoginFlow";
|
|
49
|
+
import { developmentAuthStatus } from "@/services/http";
|
|
50
|
+
|
|
51
|
+
const flow = useLoginFlow();
|
|
52
|
+
const developmentAuth = developmentAuthStatus();
|
|
53
|
+
</script>
|
|
54
|
+
|
|
55
|
+
<template>
|
|
56
|
+
<main>
|
|
57
|
+
<DevelopmentAuthWarning v-if="developmentAuth.configured" />
|
|
58
|
+
<LoginForm
|
|
59
|
+
:loading="flow.loading.value"
|
|
60
|
+
:error="flow.submitError.value"
|
|
61
|
+
@submit="flow.submit"
|
|
62
|
+
/>
|
|
63
|
+
</main>
|
|
64
|
+
</template>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`useLoginFlow` 负责调用真实账号认证、重置 Access/License/Session 派生状态,并通过
|
|
68
|
+
`safeReturnTo` 只允许站内目标。它会拒绝外部 URL、协议相对 URL、反斜杠、控制字符、
|
|
69
|
+
过长路径和登录页自循环。真实密码登录还会停用开发 Bearer Token;自定义页面应继续
|
|
70
|
+
显示 `DevelopmentAuthWarning`,避免把本地直通误认为生产认证。
|
|
71
|
+
|
|
72
|
+
品牌名、Logo 和说明可以通过 `usePlatformSettings()` 复用。业务页面可以完全重做
|
|
73
|
+
布局和内容,但不应绕过 Router 的 `guestOnly` Guard、首次改密跳转或平台认证
|
|
74
|
+
composable。
|
|
75
|
+
|
|
76
|
+
## 从 0.2.32 升级
|
|
77
|
+
|
|
78
|
+
先提交当前业务改动并保持 Git 工作区干净,然后执行:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
bunx --bun create-windy@0.2.34 update --check --cwd .
|
|
82
|
+
bunx --bun create-windy@0.2.34 update --dry-run --cwd .
|
|
83
|
+
bunx --bun create-windy@0.2.34 update --cwd .
|
|
84
|
+
bun install
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
计划中应看到 selector 以 `project-owned` 新增,Router 通过三方合并接入 selector。
|
|
88
|
+
若旧业务登录页曾放在 `apps/web/src/pages/auth/LoginPage.vue` 或直接改 Router,应将
|
|
89
|
+
业务 SFC 移到 `apps/web/src/app/auth/`,改 selector 指向它,再恢复受管文件。后续升级
|
|
90
|
+
会保留 selector 和业务 SFC。
|
|
@@ -37,12 +37,34 @@ providerEgress: {
|
|
|
37
37
|
mode: "configured-endpoint",
|
|
38
38
|
deploymentKey: "ai.openai-compatible",
|
|
39
39
|
},
|
|
40
|
+
providerDataPolicy: {
|
|
41
|
+
inputFields: [
|
|
42
|
+
{
|
|
43
|
+
jsonPointer: "/content",
|
|
44
|
+
classification: "internal",
|
|
45
|
+
dlpAction: "redact",
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
envelopeFields: [
|
|
49
|
+
{
|
|
50
|
+
field: "instructions",
|
|
51
|
+
classification: "internal",
|
|
52
|
+
dlpAction: "reject",
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
field: "user-prompt",
|
|
56
|
+
classification: "internal",
|
|
57
|
+
dlpAction: "redact",
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
},
|
|
40
61
|
toolNetworkPolicy: {
|
|
41
62
|
mode: "forbidden",
|
|
42
63
|
},
|
|
43
64
|
```
|
|
44
65
|
|
|
45
|
-
Provider URL 与 Secret 不在 Manifest 或浏览器 DTO 中;
|
|
66
|
+
Provider URL 与 Secret 不在 Manifest 或浏览器 DTO 中;Server 只把 `inputFields`
|
|
67
|
+
选出的治理后输入交给 Handler,Agent Server 只允许当前启动
|
|
46
68
|
配置的 Deployment。Tool policy 缺省为 forbidden,且不继承 Provider egress。完整
|
|
47
69
|
威胁模型、兼容行为与尚未覆盖项见
|
|
48
70
|
[Agent Provider 与 Tool 网络边界](../architecture/ai-egress.md)。
|
|
@@ -104,6 +126,7 @@ QWEN_API_URL=https://dashscope-intl.aliyuncs.com/compatible-mode/v1
|
|
|
104
126
|
QWEN_API_KEY=<服务端密钥>
|
|
105
127
|
QWEN_MODEL=qwen-plus
|
|
106
128
|
AI_PROVIDER_HTTPS_HOST_ALLOWLIST=dashscope-intl.aliyuncs.com
|
|
129
|
+
AI_PROVIDER_AUDIT_RETENTION_DAYS=90
|
|
107
130
|
|
|
108
131
|
# 可选安全上限
|
|
109
132
|
# AI_PROVIDER_CONNECT_TIMEOUT_MS=5000
|
|
@@ -125,10 +148,30 @@ bun run dev:agent
|
|
|
125
148
|
```text
|
|
126
149
|
GET http://agent-server:8080/health/live
|
|
127
150
|
GET http://agent-server:8080/health/ready
|
|
151
|
+
GET http://agent-server:8080/health/provider-governance
|
|
128
152
|
```
|
|
129
153
|
|
|
130
154
|
`ready` 会访问 Provider 的 `/models`。失败只返回稳定错误,不回显 Provider body、
|
|
131
155
|
Authorization、Secret、Endpoint query 或底层异常文本。
|
|
156
|
+
`provider-governance` 需要内部 Bearer Token,只返回留存、允许/拒绝/脱敏和 TTL 清理
|
|
157
|
+
聚合,不返回 Run ID、prompt 或正文。
|
|
158
|
+
|
|
159
|
+
## 字段治理迁移
|
|
160
|
+
|
|
161
|
+
启用 `providerEgress.mode=configured-endpoint` 的既有业务 Operation 必须同时新增
|
|
162
|
+
`providerDataPolicy`。升级后缺少策略会在 Manifest Composition 阶段以
|
|
163
|
+
`Provider 字段发送 allowlist 缺失` fail fast,不会静默发送或降级放行。
|
|
164
|
+
|
|
165
|
+
迁移时先枚举业务 input schema 中确实需要出境的最小 JSON Pointer;为每个字段声明
|
|
166
|
+
分类与 `reject/redact`。再枚举 outbound envelope:无 Tool 的单轮问答通常只需要
|
|
167
|
+
`instructions` 和 `user-prompt`;Tool schema、模型上一轮 Tool Call 和 Tool result
|
|
168
|
+
分别需要 `tool-definitions`、`provider-output`、`tool-results`。不要为消除启动错误
|
|
169
|
+
一次性允许全部字段。原始 input 会在进入 `prepare` 前裁剪,因此 Handler 必须只依赖
|
|
170
|
+
allowlist 中的字段。
|
|
171
|
+
|
|
172
|
+
Agent profile 还必须挂载 `AI_PROVIDER_AUDIT_STORE_PATH`。create-windy Starter 使用
|
|
173
|
+
`/app/data/agent/provider-audit.sqlite3` 并复用单副本持久卷;自行维护 Compose 的项目
|
|
174
|
+
需要显式补齐该变量。未配置时 Provider 启动 fail closed,并给出缺失变量名。
|
|
132
175
|
|
|
133
176
|
## 客户端协议
|
|
134
177
|
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# 业务模块只读用户目录
|
|
2
|
+
|
|
3
|
+
## Ownership 与 Seam
|
|
4
|
+
|
|
5
|
+
用户账号、启停状态、部门关系和数据范围属于 `system.identity`。业务模块需要选择案件
|
|
6
|
+
成员、协作人或负责人时,不得导入 Admin Repository、Drizzle schema 或 Server 内部
|
|
7
|
+
`RequestActor`。稳定 seam 是 `ModuleRequestContext.userDirectory`,其公开 Interface
|
|
8
|
+
只有一个方法:
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
const page = await context.userDirectory.list({
|
|
12
|
+
page: 1,
|
|
13
|
+
pageSize: 20,
|
|
14
|
+
q: "张",
|
|
15
|
+
});
|
|
16
|
+
// page.items: Array<{ id: string; displayName: string }>
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
这个深 Module 把授权、范围、状态过滤、搜索、分页、投影和审计藏在宿主 Adapter 中,
|
|
20
|
+
业务调用方只需学习查询条件与稳定错误码。删除该 Module 会迫使每个业务模块重复实现
|
|
21
|
+
同一组安全约束,因此该 seam 提供了跨模块的 leverage 和维护 locality。
|
|
22
|
+
|
|
23
|
+
## 安全不变量
|
|
24
|
+
|
|
25
|
+
- Actor、租户、部门和 Data Scope 只来自已验证请求的宿主
|
|
26
|
+
`RequestGuardContext`。Interface 不接受这些字段;HTTP body/query 中同名字段不能覆盖。
|
|
27
|
+
- V1 的 `tenantId` 是宿主 nominal `ScopeContext` 中的固定
|
|
28
|
+
`single-tenant`,不是忽略租户。`tenant` grant 仍经 Repository predicate 执行;未来
|
|
29
|
+
多租户只替换宿主 Adapter,不扩大业务 Interface。
|
|
30
|
+
- `all`、`tenant`、`department`、`department-and-children`、`custom` 和 `self`
|
|
31
|
+
都编译为用户 Repository 的查询条件,与启用状态、搜索、count 和分页共用同一个
|
|
32
|
+
`WHERE`。缺少 nominal `ScopeContext` 时返回 `data-scope-denied`,不会回退全量。
|
|
33
|
+
- 只返回启用、未删除用户的 `id` 和 `displayName`。部门、用户名、联系方式、角色、
|
|
34
|
+
凭据状态和审计字段不跨 seam 暴露。
|
|
35
|
+
- 调用者必须同时具备启用的 `system.user-directory` Feature 和
|
|
36
|
+
`system.user-directory.read` Permission。拒绝走统一 `security.denied` 审计。
|
|
37
|
+
- 每次成功查询在返回结果前以 critical 策略持久化
|
|
38
|
+
`system.user-directory.query`,元数据只含页码、页大小、结果数和总数,不记录搜索词
|
|
39
|
+
或返回用户明细;生产审计不可用时查询 fail closed。
|
|
40
|
+
- 页码与页大小必须为正整数;页码最多 10000、页大小最多 100,搜索词必须是最多 100
|
|
41
|
+
字符的字符串。非法输入返回 `USER_DIRECTORY_INVALID_QUERY`。
|
|
42
|
+
|
|
43
|
+
## 业务角色接入
|
|
44
|
+
|
|
45
|
+
业务 `ModuleRolePreset` 只能注册本模块命名空间内的 Permission,不能夹带
|
|
46
|
+
`system.user-directory.read` 绕过平台 ownership。部署管理员应在角色管理中显式为
|
|
47
|
+
需要选择用户的办案角色授予该平台 Permission,并根据角色配置 `all`、`tenant`、
|
|
48
|
+
部门、`custom` 或 `self` 数据范围。
|
|
49
|
+
|
|
50
|
+
业务 Handler 不要把浏览器提交的 `actorId`、`tenantId`、`departmentId` 或 scope
|
|
51
|
+
转发为授权参数:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
handler: async (context) => {
|
|
55
|
+
const page = await context.userDirectory.list({
|
|
56
|
+
page: Number(context.query.page || 1),
|
|
57
|
+
pageSize: Number(context.query.pageSize || 20),
|
|
58
|
+
q: context.query.q,
|
|
59
|
+
});
|
|
60
|
+
return page;
|
|
61
|
+
};
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
未挂载平台 Adapter 时返回 `USER_DIRECTORY_UNAVAILABLE`;Permission 或 Feature 拒绝
|
|
65
|
+
返回 `USER_DIRECTORY_FORBIDDEN`。业务路由可以把稳定错误码映射为自己的 HTTP 错误
|
|
66
|
+
语义,但不得捕获后改用 Admin 用户接口或裸 Repository。
|
|
67
|
+
|
|
68
|
+
## 生成与升级
|
|
69
|
+
|
|
70
|
+
`packages/server-sdk` 的 Interface、Server Adapter、Composition Root、Manifest、
|
|
71
|
+
本文件和 Starter skill 都是 Windy 模板受管内容。新项目自动生成这些文件;既有项目
|
|
72
|
+
由 `windy update` 通过 Base/Ours/Theirs 创建新增文件并合并 Composition Root。
|
|
73
|
+
升级后由管理员授予 Permission,再由 project-owned 业务 Handler 调用 Port;升级器
|
|
74
|
+
不会自动修改客户业务角色或业务页面。
|
|
75
|
+
|
|
76
|
+
运维排查顺序:
|
|
77
|
+
|
|
78
|
+
1. 确认 `system.user-directory` 在 Feature 管理中启用;
|
|
79
|
+
2. 确认 Actor 拥有 `system.user-directory.read`;
|
|
80
|
+
3. 检查 Actor 的角色数据范围与部门关系;
|
|
81
|
+
4. 查询 `security.denied` 和 `system.user-directory.query` 审计;
|
|
82
|
+
5. 若返回 `USER_DIRECTORY_UNAVAILABLE`,检查 Starter Composition Root 是否完成升级。
|
|
@@ -54,29 +54,34 @@
|
|
|
54
54
|
- 页面入口:全局设置 / 页面水印,不注册独立 Sidebar 菜单
|
|
55
55
|
|
|
56
56
|
水印是平台级强制策略,不是用户偏好。`PlatformWatermarkPolicy` 包含总开关
|
|
57
|
-
`enabled
|
|
58
|
-
`customContent
|
|
57
|
+
`enabled`、默认开启的业务页范围开关 `businessPagesEnabled`、`low / medium / high`
|
|
58
|
+
密度、内容字段勾选与最长 80 字符的 `customContent`。可选内容为用户名、姓名、手机号、
|
|
59
|
+
邮箱和自定义内容,至少保留一项;缺少已勾选的用户资料时不会渲染空片段,所有选项均无值时
|
|
60
|
+
兜底使用用户名。策略由 Admin 的全局设置页面维护,写入
|
|
59
61
|
`platform_watermark_policies`;每次保存都记录 `config.update` 审计,目标为
|
|
60
62
|
`platform-watermark/platform`。用户应用端没有水印开关,也不使用 localStorage
|
|
61
63
|
保存或覆盖策略。
|
|
62
64
|
|
|
63
65
|
管理接口 `GET/PUT /api/system/settings/watermark` 分别经过
|
|
64
66
|
`system.watermark.read/manage`、Feature 与 License Guard。业务应用通过公开只读接口
|
|
65
|
-
`GET /api/platform/watermark-policy`
|
|
67
|
+
`GET /api/platform/watermark-policy` 获取相同策略;公开响应只包含上述非敏感策略字段。
|
|
66
68
|
Feature 最终有效且 `enabled=true` 时 Admin 页面显示水印;业务页面还要求
|
|
67
69
|
`businessPagesEnabled=true`。Admin 设置区本身仍要求读取权限,但普通业务用户不需要
|
|
68
70
|
水印读取权限,也不能覆盖全局策略。
|
|
69
71
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
72
|
+
客户端按管理员勾选顺序从当前 Actor 派生身份水印,自定义内容只有被勾选时才加入。
|
|
73
|
+
渲染算法先按字符视觉宽度自动换行,再根据旋转后文本包围盒与密度预设计算 SVG 平铺
|
|
74
|
+
单元尺寸;每个单元都包含额外横纵安全间距,因此长文本和高密度模式不会让相邻水印重叠。
|
|
75
|
+
水印通过 CSS Mask 平铺并由 Vue `Teleport` 直接挂到 `body`,脱离应用壳的层叠上下文;
|
|
76
|
+
稳定审计顶层类 `z-[60]` 高于平台统一使用的
|
|
73
77
|
`z-50` Sidebar、Drawer、Overlay 与 Modal,并配合 `isolate`、`transform-gpu`
|
|
74
78
|
保证截图审计信息不会被浮层遮住。容器必须始终保留 `pointer-events-none` 与
|
|
75
79
|
`aria-hidden=true`,不能阻塞点击或进入辅助技术阅读顺序。
|
|
76
80
|
|
|
77
|
-
数据库迁移 `0031_panoramic_typhoid_mary.sql`
|
|
78
|
-
|
|
79
|
-
|
|
81
|
+
数据库迁移 `0031_panoramic_typhoid_mary.sql` 新增策略表,
|
|
82
|
+
`0034_funny_mesmero.sql` 新增密度与内容字段。升级时,已有自定义文字的策略自动迁移为
|
|
83
|
+
勾选“自定义内容”,避免改变既有展示;其它已有策略采用中密度与“用户名 + 姓名”。
|
|
84
|
+
未持久化策略采用默认值(总开关开启、业务页开启、中密度、用户名加姓名)。
|
|
80
85
|
|
|
81
86
|
## 个人设置
|
|
82
87
|
|
|
@@ -83,7 +83,7 @@ VITE_DEV_ADMIN_TOKEN=<显式开发 Token>
|
|
|
83
83
|
- Feature 管理页在列表最右侧操作栏直接显示启停开关,不再打开通用编辑弹窗;修改仍调用受 Guard 和关键审计保护的 Server API。
|
|
84
84
|
- Feature 开关成功修改会写入 `feature.update`,可在审计日志页通过“功能开关变更”直接查看。
|
|
85
85
|
- 系统资源表和字典后台任务表加载时保留表头与列宽,并显示带 `animate-pulse` 的骨架行;加载失败和空数据仍使用各自独立状态。
|
|
86
|
-
-
|
|
86
|
+
- 全局水印与业务页面范围默认开启。管理员在“全局设置”中维护总开关、低/中/高密度,并勾选用户名、姓名、手机号、邮箱或最多 80 个字符的自定义内容;保存写入审计并立即成为所有用户的只读策略。客户端按旋转文本包围盒自动计算平铺尺寸与安全间距,长内容和高密度模式也不会相互重叠。用户应用端不提供开关,也不使用 localStorage。`system.watermark` Feature 控制最终生效态,管理区另需 `system.watermark.read/manage` 权限。水印使用专用审计顶层覆盖 Sidebar、Drawer、Overlay 和 Modal,同时保持 `pointer-events-none`。服务端只向当前用户自己的会话响应补充这些身份字段。
|
|
87
87
|
- 站内通知由 PostgreSQL `platform_notifications` 和 `notification_reads` 持久化;管理人员可在 `/system/notifications` 广播发布和归档,用户从 Header 收件箱阅读正文并幂等写入已读回执。归档是生命周期操作,不物理删除通知或回执。当前发布范围是全体有阅读权限的用户,尚未提供组织、角色、用户定向和外部渠道投递。
|
|
88
88
|
- 字典页已切换为后台 NDJSON 导入导出,展示本人任务状态、进度、重试和终态文件下载;其它系统资源仍保留小数据同步 JSON 入口。详细契约见 [批量导入导出后台任务](./bulk-data-jobs.md)。当前 artifact 存在 10 MiB、50,000 行和 256 KiB 单行上限,扩大容量前需要对象存储 Adapter。
|
|
89
89
|
- 审计日志配置 `DATABASE_URL` 后读取 PostgreSQL `audit_logs`。可靠写入器提供有界重试、降级状态、关键事件本地恢复队列和健康信息;页面仍只读,且不会展示恢复队列中的事件正文。
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
ALTER TABLE "platform_watermark_policies" ADD COLUMN "density" varchar(16) DEFAULT 'medium' NOT NULL;--> statement-breakpoint
|
|
2
|
+
ALTER TABLE "platform_watermark_policies" ADD COLUMN "content_fields" jsonb DEFAULT '["username","displayName"]'::jsonb NOT NULL;--> statement-breakpoint
|
|
3
|
+
UPDATE "platform_watermark_policies"
|
|
4
|
+
SET "content_fields" = '["custom"]'::jsonb
|
|
5
|
+
WHERE btrim("custom_content") <> '';
|