create-windy 0.6.8 → 0.6.10
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 +4 -1
- package/dist/cli.js +1012 -329
- package/package.json +1 -1
- package/template/.windy-template.json +3 -3
- package/template/README.md +8 -2
- package/template/apps/agent-server/src/southwind_agent_server/contracts.py +2 -0
- package/template/apps/agent-server/tests/test_multimodal.py +30 -0
- package/template/apps/agent-server/tests/test_postgres_store.py +1 -0
- package/template/apps/agent-server/tests/test_sqlite_store.py +1 -0
- package/template/apps/server/src/agent/remote-runtime.test.ts +1 -0
- package/template/apps/server/src/agent/run-audit.ts +1 -0
- package/template/apps/server/src/agent/run-contracts.ts +1 -0
- package/template/apps/server/src/agent/run-facade.ts +2 -2
- package/template/apps/server/src/agent/run-operation-registry.ts +7 -0
- package/template/apps/server/src/agent/run-output-schema.ts +15 -0
- package/template/apps/server/src/agent/run-release.ts +11 -0
- package/template/apps/server/src/agent/supervisor-output-schema.test.ts +95 -0
- package/template/apps/server/src/agent/vision-input.ts +37 -2
- package/template/apps/server/src/license/pairing-http.ts +138 -0
- package/template/apps/server/src/license/pairing-routes.test.ts +282 -0
- package/template/apps/server/src/license/pairing-routes.ts +257 -0
- package/template/apps/server/src/license/pairing-service.test.ts +320 -0
- package/template/apps/server/src/license/pairing-service.ts +321 -0
- package/template/apps/server/src/license/restriction.test.ts +11 -0
- package/template/apps/server/src/license/restriction.ts +5 -0
- package/template/apps/server/src/license/routes.ts +8 -1
- package/template/apps/server/src/license/runtime-service.ts +20 -0
- package/template/apps/server/src/runtime-development.ts +1 -0
- package/template/apps/web/package.json +6 -4
- package/template/apps/web/src/components/license/LicensePairingCard.vue +177 -0
- package/template/apps/web/src/components/license/LicensePairingCard.webtest.ts +113 -0
- package/template/apps/web/src/composables/useLicensePairing.ts +168 -0
- package/template/apps/web/src/composables/useLicenseRuntime.ts +11 -0
- package/template/apps/web/src/layout/AppSidebar.vue +1 -0
- package/template/apps/web/src/pages/license/LicensePage.vue +8 -0
- package/template/apps/web/src/pages/ui/UiComponentPage.vue +7 -0
- package/template/apps/web/src/pages/ui/UiComponentPage.webtest.ts +94 -0
- package/template/apps/web/src/pages/ui/component-catalog.ts +248 -0
- package/template/apps/web/src/pages/ui/components/ComponentPreviewCard.vue +50 -0
- package/template/apps/web/src/pages/ui/components/DataComponentShowcases.vue +131 -0
- package/template/apps/web/src/pages/ui/components/FeedbackComponentShowcases.vue +158 -0
- package/template/apps/web/src/pages/ui/components/FoundationComponentShowcases.vue +129 -0
- package/template/apps/web/src/pages/ui/components/NavigationComponentShowcases.vue +155 -0
- package/template/apps/web/src/pages/ui/components/ProjectComponentShowcases.vue +31 -0
- package/template/apps/web/src/pages/ui/components/UiComponentGallery.vue +238 -0
- package/template/apps/web/src/router/index.ts +2 -0
- package/template/apps/web/src/router/manifest-routes.webtest.ts +1 -0
- package/template/apps/web/src/router/platform-admin-route-bindings.ts +3 -1
- package/template/apps/web/src/services/license-api.ts +60 -0
- package/template/apps/web/src/style.webtest.ts +5 -6
- package/template/docs/architecture/ai-runtime.md +5 -0
- package/template/docs/development/business-web-routes.md +3 -1
- package/template/docs/development/ui-component-gallery.md +44 -0
- package/template/docs/license/license-pairing-activation.md +67 -0
- package/template/docs/operations/incremental-delivery.md +10 -9
- package/template/docs/platform/agent-supervisor-output-contract.md +80 -0
- package/template/packages/ai-client/package.json +1 -1
- package/template/packages/ai-client/src/agent/contracts/final-answer.test.ts +67 -0
- package/template/packages/ai-client/src/agent/contracts/final-answer.ts +53 -0
- package/template/packages/ai-client/src/agent/contracts/index.ts +1 -0
- package/template/packages/modules/index.ts +1 -0
- package/template/packages/modules/package.json +1 -1
- package/template/packages/modules/src/system-admin-routes.ts +8 -0
- package/template/packages/modules/src/system-admin-ui-catalog.ts +6 -2
- package/template/packages/modules/src/system-agent-supervisor.test.ts +48 -0
- package/template/packages/modules/src/system-agent-supervisor.ts +101 -0
- package/template/packages/modules/src/system-api-control-plane.ts +37 -0
- package/template/packages/modules/src/system-features.ts +5 -0
- package/template/packages/modules/src/system-license-pairing.test.ts +44 -0
- package/template/packages/modules/src/system-module-catalog.ts +10 -19
- package/template/packages/modules/src/system-module-menu-selection.ts +15 -0
- package/template/packages/modules/src/system-module-schema-catalog.ts +14 -0
- package/template/packages/modules/src/system-modules.ts +13 -0
- package/template/packages/modules/src/system-permissions.ts +2 -0
- package/template/packages/modules/src/system-ui-components.test.ts +36 -0
- package/template/packages/server-sdk/index.ts +5 -0
- package/template/packages/server-sdk/package.json +3 -1
- package/template/packages/server-sdk/src/ai-operation-registration.ts +4 -0
- package/template/packages/server-sdk/src/system-agent-supervisor.test.ts +87 -0
- package/template/packages/server-sdk/src/system-agent-supervisor.ts +89 -0
|
@@ -367,6 +367,11 @@ Endpoint 配置属于高风险管理动作,必须具备独立 Permission 和
|
|
|
367
367
|
|
|
368
368
|
## LLM、VLM 与 Agent SSE 协议
|
|
369
369
|
|
|
370
|
+
通用规划、Tool/节点汇总与最终回答不由业务仓库各自复制协议。选择 `system.agent` 的
|
|
371
|
+
项目使用[Agent Supervisor 与最终回答契约](../platform/agent-supervisor-output-contract.md)
|
|
372
|
+
组合业务 Operation;只有通过 V1 typed `finalAnswer` 校验的 completed Run 结果才能成为
|
|
373
|
+
用户答案。
|
|
374
|
+
|
|
370
375
|
### 适用范围
|
|
371
376
|
|
|
372
377
|
以下文本或结构化输出统一使用 SSE:
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
整个 `apps/web/src` 都是 project-owned 应用源码。布局、页面、组件库封装、Router、
|
|
6
6
|
导航呈现与 Host 实现均可由项目修改、替换或删除;普通 `windy update` 和模块命令不能
|
|
7
|
-
|
|
7
|
+
为了接入入口而整文件重写这些文件。一次性兼容迁移可以使用版本 recipe 修改已登记的
|
|
8
|
+
窄锚点,具体例外见[项目 UI 组件展厅](./ui-component-gallery.md)。
|
|
8
9
|
|
|
9
10
|
## package 默认贡献
|
|
10
11
|
|
|
@@ -63,6 +64,7 @@ Foundation 访问快照改善导航和直达体验。`admin.visible=false` 只
|
|
|
63
64
|
- 客户保留、修改与删除均由项目决定;
|
|
64
65
|
- 删除以 tombstone 跨版本保留;
|
|
65
66
|
- 模块 package 更新只改变依赖与自身导出,不改客户 layout、Router 或页面;
|
|
67
|
+
- 已登记版本 recipe 只追加明确的兼容锚点,缺失或不唯一时 fail closed;
|
|
66
68
|
- Host 不再实现协议时,CLI/构建只诊断并 fail closed,不能覆盖客户实现。
|
|
67
69
|
|
|
68
70
|
未来若提供整套 Admin 预设更新,它必须是独立、显式选择的三方合并能力;当前没有
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# 项目 UI 组件展厅
|
|
2
|
+
|
|
3
|
+
更新时间:2026-08-21。
|
|
4
|
+
|
|
5
|
+
## 能力边界
|
|
6
|
+
|
|
7
|
+
`/ui/component` 展示当前项目在
|
|
8
|
+
`apps/web/src/components/ui/*/index.ts` 中封装的全部组件族。页面不维护第二份目录清单:
|
|
9
|
+
Vite 在构建当前项目时扫描每个 `index.ts`,因此下游新增 `radio-group`、`textarea` 等
|
|
10
|
+
组件后,会自动出现在“项目扩展”分组并列出真实具名导出。
|
|
11
|
+
|
|
12
|
+
Windy 自带组件提供可操作的主题内示例。项目扩展组件默认只展示发现结果与导出名,因为
|
|
13
|
+
任意组件所需的 props、slot 和业务上下文无法安全推断;项目可继续在自己的页面中增加
|
|
14
|
+
针对性预览,组件入口本身仍由项目拥有。
|
|
15
|
+
|
|
16
|
+
该页面属于 `system` Module,统一使用以下 Manifest 事实:
|
|
17
|
+
|
|
18
|
+
- Feature:`system.ui-components`;
|
|
19
|
+
- Permission:`system.ui-components.read`;
|
|
20
|
+
- Admin Route:`system.ui-components`,路径 `/ui/component`;
|
|
21
|
+
- 菜单入口与直达路由均经过现有 Feature/RBAC Guard。
|
|
22
|
+
|
|
23
|
+
## 下游升级
|
|
24
|
+
|
|
25
|
+
`create-windy@0.6.9` 的 `starter-0.6.8-to-0.6.9` recipe 会创建新的展厅页面文件,并在
|
|
26
|
+
下游项目现有 Router 中只追加两处登记内容:
|
|
27
|
+
|
|
28
|
+
1. `platformAdminPageComponents` 中的懒加载页面 binding;
|
|
29
|
+
2. `PlatformAdminPageKey` 与 `system.ui-components` Route binding。
|
|
30
|
+
|
|
31
|
+
这不是对整个 Admin 预设的重新同步。其余 `apps/web/src` 文件仍为 project-owned,
|
|
32
|
+
客户的 layout、业务 Route、页面和组件不会被模板覆盖。recipe 只接受唯一的稳定锚点;
|
|
33
|
+
若项目已经完全替换 Host,或锚点缺失/不唯一,dry-run 会报告冲突并在写入前停止。完成
|
|
34
|
+
升级后建议检查:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
bun run windy update --dry-run
|
|
38
|
+
bun run windy update
|
|
39
|
+
bun run windy doctor
|
|
40
|
+
bun run --cwd apps/web typecheck
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
验收时应确认 `/ui/component` 可访问,并且组件数量与当前项目
|
|
44
|
+
`apps/web/src/components/ui` 下含 `index.ts` 的目录数量一致。
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# License 手机扫码配对激活
|
|
2
|
+
|
|
3
|
+
## 产品目标
|
|
4
|
+
|
|
5
|
+
扫码配对激活把人工操作压缩为“客户服务器展示二维码、手机签发、客户服务器输入 8 位码”。客户服务器不访问互联网,用户不下载、复制或输入完整 License;手机 Web 在后台把同一份 `SignedLicenseV1` 回传到客户内网。
|
|
6
|
+
|
|
7
|
+
8 位 Crockford Base32 配对码不是 License、兑换码或签名。它只确认手机显示的签发结果与客户服务器当前一次性会话一致。License 版本和有效期只能在完整 V1 通过 ED25519、公钥、产品、设备码、时间和防降级校验后读取。
|
|
8
|
+
|
|
9
|
+
## 交互与数据流
|
|
10
|
+
|
|
11
|
+
1. 已认证且具备 `system.license.manage` 的用户创建 5 分钟激活会话。
|
|
12
|
+
2. Web 把浏览器当前 Origin 交给客户 Server;Server 要求它与请求 `Origin` 精确一致,再生成不可预测的 `sessionId`、投递 `token`、`nonce` 和同源回调,返回 canonical `LicensePairingRequestV1` 供 Web 渲染二维码。
|
|
13
|
+
3. 厂商操作员登录 `apps/license` 并扫码。二维码只提供设备码、产品、会话和回调信息,不能提供客户、项目、合同、License 版本或到期时间。
|
|
14
|
+
4. 操作员按签发中心权威资料完成核对,既有签发服务生成 `SignedLicenseV1`。
|
|
15
|
+
5. 手机 Web 使用无凭据、无 Referrer、禁止重定向的跨 Origin 请求,把 canonical License 和一次性 Token 自动 POST 到客户内网回调。
|
|
16
|
+
6. 客户 Server 先验签并确认产品、设备码和有效期,只把合法候选保存在当前进程的短期会话中。
|
|
17
|
+
7. 手机和客户 Server 分别按同一 SDK 协议计算 8 位码。用户输入后,Server 最多允许五次尝试;匹配后仍通过现有串行化 `LicenseInstaller` 完成防降级、原子替换和审计。
|
|
18
|
+
|
|
19
|
+
普通设备码扫码仍保留签发后下载文件的既有行为。配对回传失败时,License 已经签发,手机页会保留该结果并直接重试自动回传,不会再次调用签发 API;最近签发记录仍可下载文件兜底。
|
|
20
|
+
|
|
21
|
+
## 协议边界
|
|
22
|
+
|
|
23
|
+
`LicensePairingRequestV1` 使用严格字段集合:
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"type": "windy-license-pairing",
|
|
28
|
+
"version": 1,
|
|
29
|
+
"sessionId": "一次性会话标识",
|
|
30
|
+
"token": "一次性高熵投递 Token",
|
|
31
|
+
"nonce": "一次性高熵挑战值",
|
|
32
|
+
"productCode": "windy-platform",
|
|
33
|
+
"machineCode": "客户服务器设备码",
|
|
34
|
+
"callbackUrl": "客户内网 HTTPS 投递地址",
|
|
35
|
+
"expiresAt": "会话失效时间"
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
SDK 拒绝未知字段、非法时间、用户信息、查询串、Fragment、userinfo、错误路径和公网回调地址。生产只允许 HTTPS;HTTP 仅允许 loopback 开发地址。当前内网主机接受 RFC 1918、IPv4/IPv6 link-local、IPv6 ULA、localhost 以及 `.local`、`.internal`、`.lan` 后缀。
|
|
40
|
+
|
|
41
|
+
配对码绑定 `sessionId`、`nonce`、产品、设备码和 canonical V1 摘要,取 SHA-256 前 40 bit 后编码为 8 位 Crockford Base32。配对码不包含投递 Token,不进入 License 文件,不替代 ED25519。
|
|
42
|
+
|
|
43
|
+
## Server API 与 Guard
|
|
44
|
+
|
|
45
|
+
| 方法 | 路径 | 访问边界 |
|
|
46
|
+
| --------- | ---------------------------------------------------------- | ----------------------------------- |
|
|
47
|
+
| `POST` | `/api/license/runtime/pairing/sessions` | 已认证、License 管理权限 |
|
|
48
|
+
| `GET` | `/api/license/runtime/pairing/sessions/:sessionId` | 已认证、License 管理权限 |
|
|
49
|
+
| `OPTIONS` | `/api/license/runtime/pairing/sessions/:sessionId/deliver` | 严格 Origin 的 CORS/PNA 预检 |
|
|
50
|
+
| `POST` | `/api/license/runtime/pairing/sessions/:sessionId/deliver` | 一次性 Token、严格 Origin、匿名投递 |
|
|
51
|
+
| `POST` | `/api/license/runtime/pairing/sessions/:sessionId/confirm` | 已认证、License 管理权限和安装审计 |
|
|
52
|
+
|
|
53
|
+
License 缺失、损坏或过期时,上述恢复入口仍在限制模式窄 allowlist 中;其它业务 API 不会因此开放。所有配对响应使用 `Cache-Control: no-store`,回调只回显稳定状态,不返回 License 或 Token。
|
|
54
|
+
|
|
55
|
+
## 部署与浏览器条件
|
|
56
|
+
|
|
57
|
+
客户 Server 使用 `WINDY_LICENSE_PAIRING_ORIGINS` 配置允许执行手机回传的签发中心 Origin,多个 Origin 以逗号分隔。生产缺少配置时扫码配对 fail closed;开发默认只允许 `http://localhost:9843`。不得配置 `*`,也不启用 credentialed CORS。
|
|
58
|
+
|
|
59
|
+
手机必须同时能够通过移动网络访问厂商 HTTPS 签发中心,并通过客户 Wi-Fi 或 VPN 访问二维码中的客户内网 HTTPS 地址。客户内网证书必须被目标手机信任。浏览器可能对公网页面访问私网地址执行 Private Network Access 预检,客户 Server 只对精确允许的 Origin 返回对应 CORS/PNA 响应。
|
|
60
|
+
|
|
61
|
+
如果现场禁止手机访问客户内网,系统无法在单个 8 位码中安全传输完整签名 License,应改用 `.license.json`、响应二维码或 W1;不得降级为 SDK 内置共享密钥或截断签名。
|
|
62
|
+
|
|
63
|
+
## 运行边界
|
|
64
|
+
|
|
65
|
+
配对会话有固定数量、固定 TTL、一次投递和五次确认限制;失效或锁定时立即丢弃待安装 License。普通日志和审计不保存二维码、Token、完整 License 或配对码,只保留会话/License 标识、摘要和稳定结果。
|
|
66
|
+
|
|
67
|
+
当前客户 License 运行时仍是单进程单写者设计,配对会话也只存在于当前 Server 进程。多进程或多副本部署必须先提供共享短期会话存储、原子消费和与 License 单写者一致的锁语义,不能依赖负载均衡粘性会话冒充正确性。
|
|
@@ -107,11 +107,11 @@ package `bin`,避免 fresh `bun install` 改写受管源码的 POSIX mode。
|
|
|
107
107
|
|
|
108
108
|
```bash
|
|
109
109
|
sudo install -d -m 0755 /opt/windy/runtime /opt/windy/bin
|
|
110
|
-
sudo cp -R ./packages/delivery /opt/windy/runtime/delivery-0.6.
|
|
111
|
-
sudo chmod 0555 /opt/windy/runtime/delivery-0.6.
|
|
112
|
-
sudo mv /opt/windy/runtime/delivery-0.6.
|
|
113
|
-
/opt/windy/runtime/delivery-0.6.
|
|
114
|
-
sudo ln -s /opt/windy/runtime/delivery-0.6.
|
|
110
|
+
sudo cp -R ./packages/delivery /opt/windy/runtime/delivery-0.6.9.tmp
|
|
111
|
+
sudo chmod 0555 /opt/windy/runtime/delivery-0.6.9.tmp/bin/windy-delivery
|
|
112
|
+
sudo mv /opt/windy/runtime/delivery-0.6.9.tmp \
|
|
113
|
+
/opt/windy/runtime/delivery-0.6.9
|
|
114
|
+
sudo ln -s /opt/windy/runtime/delivery-0.6.9/bin/windy-delivery \
|
|
115
115
|
/opt/windy/bin/windy-delivery.next
|
|
116
116
|
sudo mv -Tf /opt/windy/bin/windy-delivery.next \
|
|
117
117
|
/opt/windy/bin/windy-delivery
|
|
@@ -175,10 +175,11 @@ resource fork 或 Finder metadata。路径组件以 `._` 开头时直接拒绝
|
|
|
175
175
|
|
|
176
176
|
## 构建与旧项目升级
|
|
177
177
|
|
|
178
|
-
`create-windy@0.6.7`
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
layout(含 `oci-layout`、`index.json` 和
|
|
178
|
+
`packages/delivery`、本说明和 `delivery:remote` 脚本自 `create-windy@0.6.7` 起进入受管
|
|
179
|
+
Starter;当前公开版为 `0.6.9`,最低支持基线 `0.6.6` 可通过连续 recipe 事务升级,现有
|
|
180
|
+
project-owned 发布脚本不会被覆盖。正式构建先准备一个完整 release 目录,其中
|
|
181
|
+
`images.tar` 必须是 OCI image layout(含 `oci-layout`、`index.json` 和
|
|
182
|
+
`blobs/sha256/*`),再运行:
|
|
182
183
|
|
|
183
184
|
```bash
|
|
184
185
|
bun run delivery:remote -- pack \
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Agent Supervisor 与最终回答契约
|
|
2
|
+
|
|
3
|
+
更新时间:2026-08-21。
|
|
4
|
+
|
|
5
|
+
## 边界
|
|
6
|
+
|
|
7
|
+
`system.agent` 拥有通用 Supervisor 的规划、Tool/节点汇总约束、typed final-answer
|
|
8
|
+
协议和平台发布坐标。业务模块继续拥有 Operation Key、输入、Feature、Permission、
|
|
9
|
+
Audit、数据范围、业务指令、允许的 Tool、附加输出字段、会话持久化和页面投影;不得复制
|
|
10
|
+
一份会随项目漂移的通用 Supervisor Prompt。
|
|
11
|
+
|
|
12
|
+
业务 Agent Operation 通过 `@southwind-ai/server-sdk` 的
|
|
13
|
+
`defineSystemAgentSupervisorHandler()` 接入。该 seam 会:
|
|
14
|
+
|
|
15
|
+
1. 在业务指令前加入平台 Supervisor 指令;
|
|
16
|
+
2. 把平台 Workflow/Prompt/Route 版本与业务版本组合成不可变 Run 坐标;
|
|
17
|
+
3. 要求运行时输出 Schema 包含 canonical `finalAnswer` V1;
|
|
18
|
+
4. 标记 Handler,使 Server 在启动 Run 前核对运行时 Schema 与 Manifest Schema 完全一致。
|
|
19
|
+
|
|
20
|
+
## V1 envelope
|
|
21
|
+
|
|
22
|
+
面向用户的答案只有一个事实入口:
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"finalAnswer": {
|
|
27
|
+
"schemaVersion": 1,
|
|
28
|
+
"text": "面向用户的完整最终回答"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Operation 可以在顶层增加业务字段,例如引用、表单建议或受治理成果引用;这些字段必须与
|
|
34
|
+
`finalAnswer` 一起进入该 Operation 自己的 Manifest output schema。Schema 使用
|
|
35
|
+
`composeSystemAgentFinalAnswerSchemaV1()` 组合,不能覆盖 `finalAnswer`、放宽其额外字段
|
|
36
|
+
或省略顶层 required。
|
|
37
|
+
|
|
38
|
+
Agent Runtime 仍以 Provider strict JSON Schema 和终态 `jsonschema` 校验为最终边界。
|
|
39
|
+
Server 只把与 Manifest 字节语义一致的 Supervisor Schema 交给 Runtime。Run release
|
|
40
|
+
同时固定 Operation、Workflow、Prompt、Route、Manifest digest、Tool Schema digest 和
|
|
41
|
+
独立 output-schema digest;因此旧 release ID 不会静默代表新 Prompt 或新 envelope。
|
|
42
|
+
|
|
43
|
+
## 消费规则
|
|
44
|
+
|
|
45
|
+
Web、Server 持久化和其它消费端统一使用 `projectAgentFinalAnswer()`:
|
|
46
|
+
|
|
47
|
+
- `finalAnswer.schemaVersion === 1` 且 `text` 为非空字符串时才形成用户答案;
|
|
48
|
+
- typed answer 与旧字段同时存在时,typed answer 始终优先;
|
|
49
|
+
- typed answer 存在但无效时 fail closed,不能退回旧字段;
|
|
50
|
+
- 默认不读取顶层 `text`,更不递归搜索任意节点、Tool 结果、`total` 或其它统计字段;
|
|
51
|
+
- 迁移期只有显式 `allowLegacyTopLevelText: true` 才允许一次顶层 `text` fallback,字符串内
|
|
52
|
+
JSON 不再二次解包。
|
|
53
|
+
|
|
54
|
+
流式 `output.delta` 是生成进度,不是可持久化的最终回答。只有 completed Run 的结构化
|
|
55
|
+
result 通过上述投影后,才能写入聊天历史或业务成果。
|
|
56
|
+
|
|
57
|
+
## 下游迁移
|
|
58
|
+
|
|
59
|
+
从旧 `{ text }` 输出迁移时:
|
|
60
|
+
|
|
61
|
+
1. 将 Manifest output schema 改为 `composeSystemAgentFinalAnswerSchemaV1()`,业务附加字段
|
|
62
|
+
保持顶层类型化声明;
|
|
63
|
+
2. 把 Handler 改为 `defineSystemAgentSupervisorHandler()`,业务只保留自身指令和版本;
|
|
64
|
+
3. 将模型示例和测试输出改为 `{ finalAnswer: { schemaVersion: 1, text } }`;
|
|
65
|
+
4. Web、Server 持久化和后台消费改用 `projectAgentFinalAnswer()`;
|
|
66
|
+
5. 只在读取发布前历史结果时临时开启顶层 fallback,并记录命中情况;
|
|
67
|
+
6. 运行 focused Server/Domain 测试、typecheck、build,再用真实 Provider 走一次 canonical
|
|
68
|
+
Run/SSE/result 回答验收。
|
|
69
|
+
|
|
70
|
+
下列条件全部满足后删除 project-owned 临时兼容实现:
|
|
71
|
+
|
|
72
|
+
- 项目已升级到包含本契约的 Windy 版本;
|
|
73
|
+
- 所有可创建新 Run 的 Operation 都声明并产生 typed `finalAnswer`;
|
|
74
|
+
- 在线消费、断线恢复和服务端历史持久化都只走统一投影器;
|
|
75
|
+
- 发布前历史数据已迁移,或其只读路径被单独、明确地保留,不与新 Run 共用 fallback;
|
|
76
|
+
- focused 测试、typecheck、build 与真实 canonical Runtime 回答验收均通过。
|
|
77
|
+
|
|
78
|
+
删除对象包括业务仓库复制的通用 Supervisor Prompt、递归 `text`/JSON 解包器、任意节点
|
|
79
|
+
答案猜测和新 Run 路径上的 legacy fallback。业务专属 Prompt、权限、数据范围、会话与产品
|
|
80
|
+
投影不属于临时兼容层。
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
AgentContractParseError,
|
|
4
|
+
projectAgentFinalAnswer,
|
|
5
|
+
} from "@southwind-ai/ai-client/contracts";
|
|
6
|
+
|
|
7
|
+
describe("Agent typed final answer", () => {
|
|
8
|
+
test("多节点结果始终优先使用 typed finalAnswer", () => {
|
|
9
|
+
expect(
|
|
10
|
+
projectAgentFinalAnswer({
|
|
11
|
+
finalAnswer: { schemaVersion: 1, text: "可信最终回答" },
|
|
12
|
+
text: "旧顶层回答",
|
|
13
|
+
nodes: [
|
|
14
|
+
{ text: "规划文本" },
|
|
15
|
+
{ output: { text: "Tool 文本", total: 999 } },
|
|
16
|
+
],
|
|
17
|
+
total: 2,
|
|
18
|
+
}),
|
|
19
|
+
).toEqual({
|
|
20
|
+
finalAnswer: { schemaVersion: 1, text: "可信最终回答" },
|
|
21
|
+
source: "typed-v1",
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("缺少 typed answer 时不猜测任意 text 或 total", () => {
|
|
26
|
+
for (const value of [
|
|
27
|
+
{ nodes: [{ text: "中间节点" }] },
|
|
28
|
+
{ result: { text: "嵌套结果" } },
|
|
29
|
+
{ total: 42 },
|
|
30
|
+
{ text: '{"text":"重复序列化"}' },
|
|
31
|
+
]) {
|
|
32
|
+
expect(() => projectAgentFinalAnswer(value)).toThrow(
|
|
33
|
+
AgentContractParseError,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("迁移开关只接受一次顶层 text fallback", () => {
|
|
39
|
+
expect(
|
|
40
|
+
projectAgentFinalAnswer(
|
|
41
|
+
{ text: "旧版回答", nodes: [{ text: "无关" }] },
|
|
42
|
+
{ allowLegacyTopLevelText: true },
|
|
43
|
+
),
|
|
44
|
+
).toEqual({
|
|
45
|
+
finalAnswer: { schemaVersion: 1, text: "旧版回答" },
|
|
46
|
+
source: "legacy-top-level-text",
|
|
47
|
+
});
|
|
48
|
+
expect(() =>
|
|
49
|
+
projectAgentFinalAnswer(
|
|
50
|
+
{ result: { text: "不允许嵌套 fallback" } },
|
|
51
|
+
{ allowLegacyTopLevelText: true },
|
|
52
|
+
),
|
|
53
|
+
).toThrow("output.text");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("typed answer 存在但无效时禁止降级旧字段", () => {
|
|
57
|
+
expect(() =>
|
|
58
|
+
projectAgentFinalAnswer(
|
|
59
|
+
{
|
|
60
|
+
finalAnswer: { schemaVersion: 2, text: "未知协议" },
|
|
61
|
+
text: "旧版回答",
|
|
62
|
+
},
|
|
63
|
+
{ allowLegacyTopLevelText: true },
|
|
64
|
+
),
|
|
65
|
+
).toThrow("只支持版本 1");
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { AgentContractParseError, recordAt, stringAt } from "./validation.js";
|
|
2
|
+
|
|
3
|
+
export const AGENT_FINAL_ANSWER_SCHEMA_VERSION = 1 as const;
|
|
4
|
+
|
|
5
|
+
export interface AgentFinalAnswerV1 {
|
|
6
|
+
readonly schemaVersion: 1;
|
|
7
|
+
readonly text: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface AgentFinalAnswerProjection {
|
|
11
|
+
readonly finalAnswer: AgentFinalAnswerV1;
|
|
12
|
+
readonly source: "typed-v1" | "legacy-top-level-text";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 只读取 canonical typed answer。迁移期可显式允许旧输出的顶层 `text`;不会递归
|
|
17
|
+
* 搜索节点、Tool 结果、统计字段或再次解析字符串中的 JSON。
|
|
18
|
+
*/
|
|
19
|
+
export function projectAgentFinalAnswer(
|
|
20
|
+
value: unknown,
|
|
21
|
+
options: { readonly allowLegacyTopLevelText?: boolean } = {},
|
|
22
|
+
): AgentFinalAnswerProjection {
|
|
23
|
+
const output = recordAt(value, "output");
|
|
24
|
+
if (output.finalAnswer !== undefined) {
|
|
25
|
+
const answer = recordAt(output.finalAnswer, "output.finalAnswer");
|
|
26
|
+
if (answer.schemaVersion !== AGENT_FINAL_ANSWER_SCHEMA_VERSION) {
|
|
27
|
+
throw new AgentContractParseError(
|
|
28
|
+
"output.finalAnswer.schemaVersion",
|
|
29
|
+
"只支持版本 1",
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
finalAnswer: {
|
|
34
|
+
schemaVersion: AGENT_FINAL_ANSWER_SCHEMA_VERSION,
|
|
35
|
+
text: stringAt(answer, "text", "output.finalAnswer"),
|
|
36
|
+
},
|
|
37
|
+
source: "typed-v1",
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
if (options.allowLegacyTopLevelText) {
|
|
41
|
+
return {
|
|
42
|
+
finalAnswer: {
|
|
43
|
+
schemaVersion: AGENT_FINAL_ANSWER_SCHEMA_VERSION,
|
|
44
|
+
text: stringAt(output, "text", "output"),
|
|
45
|
+
},
|
|
46
|
+
source: "legacy-top-level-text",
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
throw new AgentContractParseError(
|
|
50
|
+
"output.finalAnswer",
|
|
51
|
+
"缺少 typed final answer",
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -70,6 +70,14 @@ export const systemAdminRoutes: ModuleManifest["adminRoutes"] = [
|
|
|
70
70
|
"system.feature.read",
|
|
71
71
|
"system.feature",
|
|
72
72
|
),
|
|
73
|
+
route(
|
|
74
|
+
"system.ui-components",
|
|
75
|
+
"/ui/component",
|
|
76
|
+
"组件总览",
|
|
77
|
+
"sidebar",
|
|
78
|
+
"system.ui-components.read",
|
|
79
|
+
"system.ui-components",
|
|
80
|
+
),
|
|
73
81
|
// @windy-module system.notification begin
|
|
74
82
|
route(
|
|
75
83
|
"system.notification",
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { AdminUiManifest } from "./admin-ui-manifest.js";
|
|
2
2
|
import { systemAdminRoutes } from "./system-admin-routes.js";
|
|
3
|
+
import { systemModuleDefinitions } from "./system-module-catalog.js";
|
|
3
4
|
|
|
4
5
|
const definitions = [
|
|
5
|
-
["system", ["system.feature"]],
|
|
6
|
+
["system", ["system.feature", "system.ui-components"]],
|
|
6
7
|
[
|
|
7
8
|
"system.identity",
|
|
8
9
|
[
|
|
@@ -58,11 +59,14 @@ const definitions = [
|
|
|
58
59
|
const routeByKey = new Map(
|
|
59
60
|
systemAdminRoutes.map((route) => [route.key, route] as const),
|
|
60
61
|
);
|
|
62
|
+
const versionByName = new Map(
|
|
63
|
+
systemModuleDefinitions.map(({ name, version }) => [name, version] as const),
|
|
64
|
+
);
|
|
61
65
|
|
|
62
66
|
export function systemAdminUiManifests(): AdminUiManifest[] {
|
|
63
67
|
return definitions.map(([name, routeKeys]) => ({
|
|
64
68
|
name,
|
|
65
|
-
version: "0.1.0",
|
|
69
|
+
version: versionByName.get(name) ?? "0.1.0",
|
|
66
70
|
adminRoutes: routeKeys.map((key) => {
|
|
67
71
|
const route = routeByKey.get(key);
|
|
68
72
|
if (!route) throw new Error(`系统 Admin Route 未定义:${key}`);
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { describe, expect, test } from "bun:test";
|
|
3
|
+
import {
|
|
4
|
+
assertSystemAgentFinalAnswerSchemaV1,
|
|
5
|
+
composeSystemAgentFinalAnswerSchemaV1,
|
|
6
|
+
SYSTEM_AGENT_SUPERVISOR_INSTRUCTIONS,
|
|
7
|
+
SYSTEM_AGENT_SUPERVISOR_PROMPT_VERSION,
|
|
8
|
+
} from "./system-agent-supervisor.js";
|
|
9
|
+
|
|
10
|
+
describe("system.agent Supervisor contract", () => {
|
|
11
|
+
test("Prompt 正文变化必须同步提升不可变 promptVersion", () => {
|
|
12
|
+
expect({
|
|
13
|
+
promptVersion: SYSTEM_AGENT_SUPERVISOR_PROMPT_VERSION,
|
|
14
|
+
sha256: createHash("sha256")
|
|
15
|
+
.update(SYSTEM_AGENT_SUPERVISOR_INSTRUCTIONS)
|
|
16
|
+
.digest("hex"),
|
|
17
|
+
}).toEqual({
|
|
18
|
+
promptVersion: "1",
|
|
19
|
+
sha256:
|
|
20
|
+
"466484c68229181316fcc2476d589b3ddb1c5af3898745d5ca657e4901589b6d",
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("组合业务字段但保留唯一 canonical finalAnswer", () => {
|
|
25
|
+
const schema = composeSystemAgentFinalAnswerSchemaV1({
|
|
26
|
+
properties: { citations: { type: "array", items: { type: "string" } } },
|
|
27
|
+
required: ["citations"],
|
|
28
|
+
});
|
|
29
|
+
expect(() => assertSystemAgentFinalAnswerSchemaV1(schema)).not.toThrow();
|
|
30
|
+
expect(schema.required).toEqual(["finalAnswer", "citations"]);
|
|
31
|
+
expect(() =>
|
|
32
|
+
composeSystemAgentFinalAnswerSchemaV1({
|
|
33
|
+
properties: { finalAnswer: { type: "string" } },
|
|
34
|
+
}),
|
|
35
|
+
).toThrow("不能覆盖");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("拒绝缺少 typed answer 或放宽额外字段的 Schema", () => {
|
|
39
|
+
expect(() =>
|
|
40
|
+
assertSystemAgentFinalAnswerSchemaV1({
|
|
41
|
+
type: "object",
|
|
42
|
+
properties: { text: { type: "string" } },
|
|
43
|
+
required: ["text"],
|
|
44
|
+
additionalProperties: false,
|
|
45
|
+
}),
|
|
46
|
+
).toThrow("canonical finalAnswer V1");
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { JsonObject, JsonValue } from "@southwind-ai/shared";
|
|
2
|
+
import type { ModuleJsonSchemaDefinition } from "./manifest.js";
|
|
3
|
+
|
|
4
|
+
export const SYSTEM_AGENT_FINAL_ANSWER_SCHEMA_VERSION = 1 as const;
|
|
5
|
+
export const SYSTEM_AGENT_FINAL_ANSWER_SCHEMA_KEY =
|
|
6
|
+
"system.agent.final-answer.v1";
|
|
7
|
+
|
|
8
|
+
export const SYSTEM_AGENT_SUPERVISOR_WORKFLOW_VERSION = "1";
|
|
9
|
+
export const SYSTEM_AGENT_SUPERVISOR_PROMPT_VERSION = "1";
|
|
10
|
+
export const SYSTEM_AGENT_SUPERVISOR_ROUTE_VERSION = "1";
|
|
11
|
+
|
|
12
|
+
export const SYSTEM_AGENT_SUPERVISOR_INSTRUCTIONS = [
|
|
13
|
+
"你是 Windy system.agent 提供的通用 Supervisor。",
|
|
14
|
+
"先理解用户目标,再在当前 Operation 允许的 Tool 与数据范围内规划、执行和汇总;不得扩大权限、猜测未返回的业务事实或暴露内部协议。",
|
|
15
|
+
"Tool 和子步骤结果只是中间事实,不得把任意节点的 text、total、统计字段或原始 JSON 直接当作用户答案。",
|
|
16
|
+
"最终只输出符合 Operation JSON Schema 的对象;面向用户的中文答案必须写入 finalAnswer,schemaVersion 固定为 1,text 为完整最终回答。",
|
|
17
|
+
].join("\n");
|
|
18
|
+
|
|
19
|
+
export const systemAgentFinalAnswerPropertySchemaV1 = {
|
|
20
|
+
type: "object",
|
|
21
|
+
properties: {
|
|
22
|
+
schemaVersion: { const: SYSTEM_AGENT_FINAL_ANSWER_SCHEMA_VERSION },
|
|
23
|
+
text: { type: "string", minLength: 1, maxLength: 1_048_576 },
|
|
24
|
+
},
|
|
25
|
+
required: ["schemaVersion", "text"],
|
|
26
|
+
additionalProperties: false,
|
|
27
|
+
} as const satisfies JsonObject;
|
|
28
|
+
|
|
29
|
+
export const systemAgentSupervisorSchemas: ModuleJsonSchemaDefinition[] = [
|
|
30
|
+
{
|
|
31
|
+
key: SYSTEM_AGENT_FINAL_ANSWER_SCHEMA_KEY,
|
|
32
|
+
schema: systemAgentFinalAnswerPropertySchemaV1,
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
export function composeSystemAgentFinalAnswerSchemaV1(input?: {
|
|
37
|
+
readonly properties?: Readonly<Record<string, JsonValue>>;
|
|
38
|
+
readonly required?: readonly string[];
|
|
39
|
+
}): JsonObject {
|
|
40
|
+
if (input?.properties?.finalAnswer !== undefined) {
|
|
41
|
+
throw new Error("业务输出不能覆盖 system.agent finalAnswer 契约");
|
|
42
|
+
}
|
|
43
|
+
const required = [...new Set(["finalAnswer", ...(input?.required ?? [])])];
|
|
44
|
+
if (
|
|
45
|
+
required.every(
|
|
46
|
+
(key) =>
|
|
47
|
+
key === "finalAnswer" ||
|
|
48
|
+
Object.prototype.hasOwnProperty.call(input?.properties ?? {}, key),
|
|
49
|
+
)
|
|
50
|
+
) {
|
|
51
|
+
return {
|
|
52
|
+
type: "object",
|
|
53
|
+
properties: {
|
|
54
|
+
finalAnswer: structuredClone(systemAgentFinalAnswerPropertySchemaV1),
|
|
55
|
+
...(input?.properties ?? {}),
|
|
56
|
+
},
|
|
57
|
+
required,
|
|
58
|
+
additionalProperties: false,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
throw new Error("业务输出 required 引用了未声明字段");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function assertSystemAgentFinalAnswerSchemaV1(
|
|
65
|
+
value: JsonValue | undefined,
|
|
66
|
+
): asserts value is JsonObject {
|
|
67
|
+
if (!isRecord(value) || value.type !== "object") {
|
|
68
|
+
throw new Error("Supervisor 输出 Schema 必须是 object");
|
|
69
|
+
}
|
|
70
|
+
const properties = isRecord(value.properties) ? value.properties : undefined;
|
|
71
|
+
const required = Array.isArray(value.required) ? value.required : undefined;
|
|
72
|
+
if (
|
|
73
|
+
!properties ||
|
|
74
|
+
!required?.includes("finalAnswer") ||
|
|
75
|
+
value.additionalProperties !== false ||
|
|
76
|
+
canonicalJson(properties.finalAnswer) !==
|
|
77
|
+
canonicalJson(systemAgentFinalAnswerPropertySchemaV1)
|
|
78
|
+
) {
|
|
79
|
+
throw new Error("Supervisor 输出 Schema 缺少 canonical finalAnswer V1");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function isRecord(value: JsonValue | undefined): value is JsonObject {
|
|
84
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function canonicalJson(value: unknown): string {
|
|
88
|
+
return JSON.stringify(canonicalValue(value));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function canonicalValue(value: unknown): unknown {
|
|
92
|
+
if (Array.isArray(value)) return value.map(canonicalValue);
|
|
93
|
+
if (value !== null && typeof value === "object") {
|
|
94
|
+
return Object.fromEntries(
|
|
95
|
+
Object.entries(value as Record<string, unknown>)
|
|
96
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
97
|
+
.map(([key, item]) => [key, canonicalValue(item)]),
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
return value;
|
|
101
|
+
}
|
|
@@ -29,6 +29,43 @@ export function systemControlPlaneApiPermissionBindings(): ApiPermissionBinding[
|
|
|
29
29
|
"system.license",
|
|
30
30
|
"license.update",
|
|
31
31
|
),
|
|
32
|
+
binding(
|
|
33
|
+
"POST",
|
|
34
|
+
"/api/license/runtime/pairing/sessions",
|
|
35
|
+
"system.license.manage",
|
|
36
|
+
"system.license",
|
|
37
|
+
"license.verify",
|
|
38
|
+
),
|
|
39
|
+
binding(
|
|
40
|
+
"GET",
|
|
41
|
+
"/api/license/runtime/pairing/sessions/:sessionId",
|
|
42
|
+
"system.license.manage",
|
|
43
|
+
"system.license",
|
|
44
|
+
"license.verify",
|
|
45
|
+
),
|
|
46
|
+
binding(
|
|
47
|
+
"POST",
|
|
48
|
+
"/api/license/runtime/pairing/sessions/:sessionId/confirm",
|
|
49
|
+
"system.license.manage",
|
|
50
|
+
"system.license",
|
|
51
|
+
"license.update",
|
|
52
|
+
),
|
|
53
|
+
binding(
|
|
54
|
+
"OPTIONS",
|
|
55
|
+
"/api/license/runtime/pairing/sessions/:sessionId/deliver",
|
|
56
|
+
"system.license.manage",
|
|
57
|
+
"system.license",
|
|
58
|
+
undefined,
|
|
59
|
+
true,
|
|
60
|
+
),
|
|
61
|
+
binding(
|
|
62
|
+
"POST",
|
|
63
|
+
"/api/license/runtime/pairing/sessions/:sessionId/deliver",
|
|
64
|
+
"system.license.manage",
|
|
65
|
+
"system.license",
|
|
66
|
+
undefined,
|
|
67
|
+
true,
|
|
68
|
+
),
|
|
32
69
|
// @windy-module system.license end
|
|
33
70
|
// @windy-module system.agent begin
|
|
34
71
|
binding(
|
|
@@ -37,6 +37,11 @@ export const systemFeatures: ModuleManifest["features"] = [
|
|
|
37
37
|
"功能开关管理",
|
|
38
38
|
"查看和恢复平台 Feature 状态的核心控制面能力。",
|
|
39
39
|
),
|
|
40
|
+
feature(
|
|
41
|
+
"system.ui-components",
|
|
42
|
+
"UI 组件总览",
|
|
43
|
+
"查看 Windy Web 已封装基础组件的真实主题与交互示例。",
|
|
44
|
+
),
|
|
40
45
|
feature(
|
|
41
46
|
"system.user-directory",
|
|
42
47
|
"业务用户目录",
|