From e1b9de2a49fbf073bbe3ece8592a511a0217366b Mon Sep 17 00:00:00 2001
From: gongzhiyong
Date: Mon, 25 May 2026 17:41:43 +0800
Subject: [PATCH] feat: add azure key vault resource discovery
---
.gitignore | 6 +
docs/2026-05-02-daily-summary.txt | 2 +-
docs/README.md | 2 +-
.../azure-production-deploy-guardrails.md | 11 +-
...icode-runtime-auth-newapi-secret-design.md | 28 +-
docs/heicode.md | 8 +-
.../agnet-platform-request-contract.md | 12 +-
docs/plan.md | 6 +-
docs/product-package/01-product-overview.md | 2 +-
.../02-homepage-design-brief.md | 4 +-
.../04-platform-usage-guide.md | 2 +-
docs/product-package/05-ppt-copy.md | 10 +-
.../06-security-and-credential-management.md | 12 +-
.../07-integration-boundaries.md | 22 +-
docs/product-package/08-client-guide.md | 2 +-
docs/product-package/09-demo-script.md | 4 +-
.../10-frontend-detail-spec.md | 4 +-
.../12-narrated-user-operation-flow.md | 2 +-
.../13-platform-description.md | 8 +-
docs/product-package/README.md | 4 +-
heicode/.gitignore | 6 +
heicode/VERSION | 2 +-
.../controller/agnet_control_plane_test.go | 6 +-
.../controller/azure_resource_discovery.go | 311 ++++++++++
heicode/controller/resource.go | 53 +-
heicode/controller/resource_test.go | 208 ++++++-
heicode/controller/secret_store.go | 316 ++++++----
heicode/docker-compose.azure-vm.yml | 5 +
heicode/relay/helper/stream_scanner.go | 4 +
heicode/router/api-router.go | 10 +-
heicode/web/classic/dist/index.html | 10 +
heicode/web/default/.gitignore | 5 +-
heicode/web/default/bun.lock | 164 ++++--
heicode/web/default/dist/index.html | 10 +
.../default/src/components/query-state.tsx | 4 +-
.../azure-cloud-binding-sheet.tsx | 160 ++++--
.../src/features/agnet-console/pages.tsx | 543 ++++++++++--------
.../auth/sign-up/components/sign-up-form.tsx | 5 +
.../dashboard/components/home-hero.tsx | 82 +--
.../maintenance/secret-store-section.tsx | 72 +--
.../maintenance/section-registry.tsx | 4 +-
.../web/default/src/features/wallet/index.tsx | 12 +-
heicode/web/default/src/i18n/locales/en.json | 21 +-
heicode/web/default/src/i18n/locales/zh.json | 21 +-
.../web/default/src/lib/manager-resources.ts | 117 ++++
heicode/web/default/src/routeTree.gen.ts | 22 +
scripts/create_swarm_token.py | 6 +-
scripts/find_admin_user.py | 13 +-
48 files changed, 1649 insertions(+), 694 deletions(-)
create mode 100644 heicode/controller/azure_resource_discovery.go
create mode 100644 heicode/web/classic/dist/index.html
create mode 100644 heicode/web/default/dist/index.html
create mode 100644 heicode/web/default/src/lib/manager-resources.ts
diff --git a/.gitignore b/.gitignore
index 0e3c5b39..a888769a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,12 @@
# Build outputs
**/dist/
+!heicode/web/default/dist/
+heicode/web/default/dist/*
+!heicode/web/default/dist/index.html
+!heicode/web/classic/dist/
+heicode/web/classic/dist/*
+!heicode/web/classic/dist/index.html
**/.next/
**/out/
**/.rsbuild/
diff --git a/docs/2026-05-02-daily-summary.txt b/docs/2026-05-02-daily-summary.txt
index 0219b5c9..08806df5 100644
--- a/docs/2026-05-02-daily-summary.txt
+++ b/docs/2026-05-02-daily-summary.txt
@@ -143,5 +143,5 @@ Heicode 今日工作总结与后续计划
六、安全说明
- 本次文档和代码未写入任何真实密码、Token、Redis 密钥、PostgreSQL 密码、SSH 密码或 Git 密码。
-- 文档示例全部使用占位符或 vault:// secret_ref。
+- 文档示例使用占位符或 Azure Key Vault `azkv://...` secret_ref。
- 后续部署与推送仍需继续遵守:密钥只走环境变量、Secret Store、交互式凭据或平台安全注入,不进入 Git。
diff --git a/docs/README.md b/docs/README.md
index ed4517a2..7fe4f974 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -6,7 +6,7 @@
|------|------|
| [`heicode.md`](./heicode.md) | Heicode 当前产品定位、系统边界和架构共识 |
| [`plan.md`](./plan.md) | 按当前共识拆出的实施计划 |
-| [`heicode-runtime-auth-newapi-secret-design.md`](./heicode-runtime-auth-newapi-secret-design.md) | 用户输入、登录用户复用、NewAPI 扣费映射、OpenBao 短期凭证注入边界 |
+| [`heicode-runtime-auth-newapi-secret-design.md`](./heicode-runtime-auth-newapi-secret-design.md) | 用户输入、登录用户复用、NewAPI 扣费映射、Azure Key Vault 凭证托管与短期凭证注入边界 |
| [`integration/Heicode-登录接口对接文档.md`](./integration/Heicode-登录接口对接文档.md) | 已上线登录接口对接文档 |
| [`integration/agnet-platform-request-contract.md`](./integration/agnet-platform-request-contract.md) | Manager 请求 Agnet 平台时携带的部署、日志、监控、事件与审计接口参数 |
| [`deployment/azure-production-deploy-guardrails.md`](./deployment/azure-production-deploy-guardrails.md) | Azure VM / PostgreSQL / Redis / Agnet / NewAPI 生产部署前的安全守卫、环境变量注入和验证计划 |
diff --git a/docs/deployment/azure-production-deploy-guardrails.md b/docs/deployment/azure-production-deploy-guardrails.md
index 6add47ef..64cb11ee 100644
--- a/docs/deployment/azure-production-deploy-guardrails.md
+++ b/docs/deployment/azure-production-deploy-guardrails.md
@@ -2,7 +2,7 @@
本文用于 Heicode Manager / Agnet / NewAPI 相关生产发布前的人工执行检查。它只描述安全命令、环境变量名和验证项,不保存任何真实地址、账号、密码、Token、连接串、SSH key 或云访问密钥。
-适用范围:Azure VM、Azure PostgreSQL、Azure Redis、Git 同步、Nginx 统一入口、Heicode Manager 容器、OpenBao 内网密钥保管、Agnet 平台联调、NewAPI 网关能力验证。
+适用范围:Azure VM、Azure PostgreSQL、Azure Redis、Git 同步、Nginx 统一入口、Heicode Manager 容器、Azure Key Vault 密钥保管、Agnet 平台联调、NewAPI 网关能力验证。
## 1. 执行原则
@@ -12,7 +12,7 @@
| Git 发布 | 仅允许快进同步已审核提交;禁止在生产 VM 上提交代码或保存临时补丁。 |
| 数据库/Redis | Azure PostgreSQL / Redis 连接串只写入 VM 本地 `.env` 或 Secret Store;验证时只打印变量名和连通性结果,不打印值。 |
| Agnet / NewAPI | Manager 只传 `secret_ref`、部署计划、资源授权和审计上下文;禁止把明文云账号、数据库密码、模型 Key 放入请求体。 |
-| OpenBao | 只允许 Manager/Agnet 受控网络访问;如果 Manager 提供客户端验证和绑定接口,OpenBao 不暴露公网路由。 |
+| Azure Key Vault | 只允许 Manager/Agnet 通过 Managed Identity 和受控网络访问;如果 Manager 提供客户端验证和绑定接口,Key Vault 不暴露普通公网访问。 |
| 生产动作 | 执行 `up -d`、迁移、重启、回滚前必须记录当前镜像/提交和健康检查 URL;失败时停止扩大变更。 |
## 2. 本地发布前检查
@@ -69,6 +69,7 @@ REDIS_CONN_STRING=
SESSION_SECRET=
HEICODE_ROOT_EMAILS=
HEICODE_ADMIN_EMAILS=
+AZURE_KEY_VAULT_URL=
```
验证只打印 key,不打印 value:
@@ -96,8 +97,8 @@ ssh "$REMOTE" "cd '$REMOTE_DIR/heicode' && docker compose -f docker-compose.azur
- 对外域名统一使用 `heicode.xinghanlab.com`。
- Nginx 负责按路由转发 Manager 与 NewAPI,例如 Manager 主站、NewAPI 受控 API 或健康检查路由。
-- OpenBao 仅供 Manager/Agnet 服务端访问,不通过 `heicode.xinghanlab.com` 暴露给浏览器用户。
-- 若需要 OpenBao 运维 UI,也必须走临时 SSH tunnel、VPN、内网跳板或单独受保护管理入口,不走普通 SaaS 用户路由。
+- Azure Key Vault 仅供 Manager/Agnet 服务端通过 Managed Identity 访问,不通过 `heicode.xinghanlab.com` 暴露给浏览器用户。
+- Key Vault 应优先使用 Private Endpoint;如临时启用公网访问,必须限制来源并在验证后关闭。
构建和启动:
@@ -156,7 +157,7 @@ curl -fsS -X POST "$AGNET_BASE_URL/api/agnet/deployments" \
|---|---|---|
| 服务健康 | `curl -fsS http://127.0.0.1:3000/api/status` | 返回 `success=true`。 |
| Nginx 公网入口 | `curl -fsS https://heicode.xinghanlab.com/api/status` | 返回 Manager 健康状态;DNS 需解析到生产入口。 |
-| OpenBao 暴露面 | `curl -fsSI https://heicode.xinghanlab.com/v1/sys/health` | 普通公网入口不应返回 OpenBao 健康信息;预期为无路由、403 或 404。 |
+| Key Vault 暴露面 | Azure Portal/CLI 检查 Key Vault networking | 生产优先 `Public network access = Disabled`,通过 Private Endpoint 访问。 |
| 容器状态 | `docker compose ... ps heicode` | `heicode` 为 running/healthy。 |
| Git 版本 | `git rev-parse --short HEAD` | 与已审核提交一致。 |
| DB/Redis 注入 | `awk -F= ... .env` | 只打印 key,包含 `SQL_DSN`、`REDIS_CONN_STRING`。 |
diff --git a/docs/heicode-runtime-auth-newapi-secret-design.md b/docs/heicode-runtime-auth-newapi-secret-design.md
index b31ea228..501be253 100644
--- a/docs/heicode-runtime-auth-newapi-secret-design.md
+++ b/docs/heicode-runtime-auth-newapi-secret-design.md
@@ -2,7 +2,7 @@
日期:2026-05-04
-本文修正 Manager、Agnet 平台、NewAPI 与 OpenBao 之间的运行时边界。若本文与旧文档中 `tenant`、`project` 或平台侧批准描述冲突,以本文为准。
+本文修正 Manager、Agnet 平台、NewAPI 与 Azure Key Vault 之间的运行时边界。若本文与旧文档中 `tenant`、`project` 或平台侧批准描述冲突,以本文为准。
## 一、用户输入在哪里
@@ -60,7 +60,7 @@ JWT 中也包含:
Manager 可以有本地 user cache,但 canonical user identity 应来自登录接口返回的用户信息。除非未来产品明确引入企业组织、空间或项目账本,否则不要把 tenant/project 作为认证和扣费主轴。
-## 三、高危操作审批与 OpenBao 密钥注入
+## 三、高危操作审批与 Azure Key Vault 密钥注入
高危操作审批只在客户端完成。用户在客户端明确批准后,Manager/Agnet 平台才可以执行对应动作。Agnet 平台不是审批主体,不发起额外审批;它只校验 `approval_id`、审批主体、审批范围、TTL、`risk_level` 和策略是否匹配。
@@ -69,13 +69,13 @@ Manager 可以有本地 user cache,但 canonical user identity 应来自登录
```text
长期密钥
-> 用户授权或绑定资源
--> Manager 写入 OpenBao
+-> Manager 通过 VM Managed Identity 写入 Azure Key Vault
-> Manager DB 只保存 secret_ref
--> OpenBao 只允许 Manager/Agnet 受控网络访问,不对公网暴露
+-> Azure Key Vault 通过 Private Endpoint / 防火墙限制,只允许 Manager/Agnet 受控网络访问
高危操作
-> 客户端审批
--> Manager/Agnet 平台按 secret_ref 从 OpenBao 获取或派生短期凭证
+-> Manager/Agnet 平台按 secret_ref 从 Azure Key Vault 获取或派生短期凭证
-> 短期凭证可注入子 Agnet
-> 子 Agnet 完成任务后凭证过期或撤销
```
@@ -86,15 +86,15 @@ Manager 可以有本地 user cache,但 canonical user identity 应来自登录
- `heicode.xinghanlab.com` 是 Manager、NewAPI 与内部服务的统一公网域名入口。
- Nginx 可以为 Manager 和 NewAPI 制定路由,例如 Manager 主站与 NewAPI 受控 API 路由。
-- OpenBao 不应作为普通公网路由开放;如果 Manager 已经提供客户端验证、资源绑定、审批和 `secret_ref` 管理接口,客户端不需要直连 OpenBao。
-- OpenBao 访问应限制在容器网络、VM loopback、AKS 内网、Workload Identity 或其它受保护服务间通道。
+- Azure Key Vault 不应作为普通公网路由开放;如果 Manager 已经提供客户端验证、资源绑定、审批和 `secret_ref` 管理接口,客户端不需要直连 Azure Key Vault。
+- Azure Key Vault 访问应限制在 Private Endpoint、Azure 防火墙规则、VM Managed Identity、AKS Workload Identity 或其它受保护服务间通道。
-OpenBao 不暴露公网的检查口径:
+Azure Key Vault 不暴露普通公网入口的检查口径:
-- 公网 DNS、Nginx `server_name`、Ingress、LoadBalancer 和安全组规则不得直接指向 OpenBao 服务端口。
+- Key Vault 应优先关闭 Public Network Access,并通过 Private Endpoint 接入 `heicode-vnet`。
- 外部客户端只能通过 Manager 的认证、资源绑定、审批和 `secret_ref` 管理接口间接操作密钥引用。
-- Manager、Agnet 平台和子 Agnet 访问 OpenBao 时必须走内网地址、loopback、容器网络、AKS private endpoint 或 Workload Identity 绑定。
-- 健康检查和联调报告只能证明内网访问可用;不得把公网可访问的 OpenBao health endpoint 作为验收口径。
+- Manager、Agnet 平台和子 Agnet 访问 Azure Key Vault 时必须走 Private Endpoint / Workload Identity / Managed Identity 绑定。
+- 健康检查和联调报告只能证明受控网络访问可用;不得把公网可访问作为验收口径。
短期凭证注入必须满足:
@@ -183,7 +183,7 @@ Manager 请求 Agnet 平台部署或执行任务时,应携带登录用户上
]
},
"secret_context": {
- "secret_refs": ["vault://secret/resources/res_git_1"],
+ "secret_refs": ["azkv://heicode-kv.vault.azure.net/secrets/res_git_1"],
"inject_short_lived_credentials": true,
"approval_id": "approval_123"
}
@@ -192,7 +192,7 @@ Manager 请求 Agnet 平台部署或执行任务时,应携带登录用户上
这里的 `user_context` 用于确认身份,`billing_context` 用于 NewAPI 余额、用量和扣费映射,`agent_runtime` 用于 Agnet 平台独立选择子 Agnet 模型和实例数量,`work_context` 用于团队开发控制,`secret_context` 只传 `secret_ref` 和审批结果。
-Agnet 平台执行时,应把用户、角色、Git 绑定、deployment、Agnet model profile 和 NewAPI 映射写入 metadata 或审计日志。真实 NewAPI key 和 OpenBao 凭证由平台安全通道读取,不进入 Markdown。
+Agnet 平台执行时,应把用户、角色、Git 绑定、deployment、Agnet model profile 和 NewAPI 映射写入 metadata 或审计日志。真实 NewAPI key 和 Azure Key Vault 凭证由平台安全通道读取,不进入 Markdown。
## 六、当前主线结论
@@ -202,5 +202,5 @@ Agnet 平台执行时,应把用户、角色、Git 绑定、deployment、Agnet
- 团队开发控制优先由绑定 Git、允许路径、Agnet 角色、资源授权表达。
- 子 Agnet 的运行模型由 Agnet 平台独立配置,不和 NewAPI 扣费对象混在一起。
- NewAPI 负责模型网关、用户/Token/Group 额度、余额、日志和扣费。
-- OpenBao 负责长期密钥托管,子 Agnet 只拿短期、最小权限、可审计凭证。
+- Azure Key Vault 负责长期密钥托管,子 Agnet 只拿短期、最小权限、可审计凭证。
- 高危操作审批发生在客户端,审批结果随任务上下文传给 Manager/Agnet 平台。
diff --git a/docs/heicode.md b/docs/heicode.md
index fa379a52..086376fa 100644
--- a/docs/heicode.md
+++ b/docs/heicode.md
@@ -39,7 +39,7 @@ Manager 是用户操作入口;Agnet 是运行时执行层;NewAPI 是模型
7. Manager 负责资源绑定、权限分配和凭证托管能力;真实密钥放入 Secret Store。
8. 子 Agnet 不保存长期密钥,只接收角色、资源元数据、AGENT.md 和受控访问方式。
9. Agnet 平台在 AKS 上负责运行时身份、隔离、状态、事件和审计回传。
-10. 高危操作审批只在客户端完成;审批通过后可以把 OpenBao 派生的短期、最小权限凭证注入子 Agnet,但不能注入长期密钥。
+10. 高危操作审批只在客户端完成;审批通过后可以把密钥保管器派生的短期、最小权限凭证注入子 Agnet,但不能注入长期密钥。
11. 子 Agnet 的运行模型是 Agnet 平台的部署配置,独立于 NewAPI 的用户、Token、Group 扣费映射。
## 四、Manager 的核心功能
@@ -130,7 +130,7 @@ P1 permission manifest 示例:
"ref": "main",
"paths": ["services/api/**"]
},
- "secret_ref": "vault://secret/resources/repo_demo"
+ "secret_ref": "azkv://heicode-kv.vault.azure.net/secrets/repo-demo"
}
]
}
@@ -154,7 +154,7 @@ Secret Broker 负责:
- 接收 OAuth、GitHub App、云授权回调后的凭证。
- 生成按用户、资源和角色隔离的 secret path。
-- 写入 Vault、Infisical 或 Azure Key Vault。
+- 写入 Azure Key Vault。
- 创建或更新 policy。
- 保存 `secret_ref` 到 Manager DB。
- 轮换、撤销、禁用凭证。
@@ -185,7 +185,7 @@ Agnet 平台底层是 AKS,因此运行时权限应和 Kubernetes 身份绑定
| 受控注入 | Git clone、开发/测试环境、低风险资源 |
| 短期凭证注入 | 已经客户端审批的生产部署、数据库写入、高危云操作 |
-普通开发资源可受控注入;生产云资源和高危操作也只在客户端审批通过后,注入 OpenBao 派生的短期、最小权限凭证。
+普通开发资源可受控注入;生产云资源和高危操作也只在客户端审批通过后,注入密钥保管器派生的短期、最小权限凭证。
## 八、NewAPI 边界
diff --git a/docs/integration/agnet-platform-request-contract.md b/docs/integration/agnet-platform-request-contract.md
index e13a7dc9..8d6b0006 100644
--- a/docs/integration/agnet-platform-request-contract.md
+++ b/docs/integration/agnet-platform-request-contract.md
@@ -48,7 +48,7 @@ AGNET_PLATFORM_BASE_URL=https://agnet-platform.example.com
```text
AGNET_PLATFORM_BASE_URL=https://staging-agnet.example.com
-MANAGER_SERVICE_TOKEN_SECRET_REF=vault://secret/users/manager-service/bindings/agnet-platform/service-token
+MANAGER_SERVICE_TOKEN_SECRET_REF=azkv://heicode-kv.vault.azure.net/secrets/manager-service-agnet-platform-service-token
```
完整路径示例:
@@ -230,7 +230,7 @@ POST /api/agnet/deployments
"repo_url": "https://example.com/org/repo.git"
},
"status": "active",
- "secret_ref": "vault://secret/users/user_123/bindings/repo_default/resources/res_git_repo_001",
+ "secret_ref": "azkv://heicode-kv.vault.azure.net/secrets/user-123-repo-default-res-git-repo-001",
"audit": {
"created_by": "manager",
"approval_id": "approval_001"
@@ -419,7 +419,7 @@ POST /api/agnet/deployments
"service_name": "new-api"
},
"status": "active",
- "secret_ref": "vault://secret/users/user_123/bindings/newapi-prod/resources/res_newapi_vm",
+ "secret_ref": "azkv://heicode-kv.vault.azure.net/secrets/user-123-newapi-prod-res-newapi-vm",
"audit": {
"created_by": "manager",
"approval_id": "approval_newapi_001"
@@ -441,11 +441,11 @@ POST /api/agnet/deployments
"plaintext_export_forbidden": "true"
},
"metadata": {
- "secret_provider": "openbao",
+ "secret_provider": "azure_key_vault",
"scope": "newapi-runtime"
},
"status": "active",
- "secret_ref": "vault://secret/users/user_123/bindings/newapi-prod/resources/res_newapi_runtime_env",
+ "secret_ref": "azkv://heicode-kv.vault.azure.net/secrets/user-123-newapi-prod-res-newapi-runtime-env",
"audit": {
"created_by": "manager",
"approval_id": "approval_newapi_001"
@@ -912,7 +912,7 @@ Agnet 平台不承担高危操作审批主体。审批只发生在客户端;Ag
| 审批范围 | 审批范围必须覆盖 `binding_scope`、`permission_scope`、目标环境、资源 ID 和操作类型。 |
| TTL / 时间窗口 | 审批记录必须未过期;若使用 `window` 或 TTL,当前执行时间必须落在允许范围内。 |
| `risk_level` | 高危资源写入、生产部署、云资源修改、删除和扩容必须为 `high`。 |
-| 策略 | 平台 policy、OpenBao policy、Kubernetes/Workload Identity、网络策略和最小权限约束均必须允许本次动作。 |
+| 策略 | 平台 policy、Key Vault 访问策略、Kubernetes/Workload Identity、网络策略和最小权限约束均必须允许本次动作。 |
任一校验不通过时,Agnet 平台应返回 `POLICY_REJECTED` 或 `FORBIDDEN_SCOPE`,不得发起额外批准流程。
diff --git a/docs/plan.md b/docs/plan.md
index d682b6ec..4dbdb584 100644
--- a/docs/plan.md
+++ b/docs/plan.md
@@ -57,7 +57,7 @@
任务:
- 优先选型 HashiCorp Vault。
-- 保留 Infisical 和 Azure Key Vault 作为 Secret Provider 备选。
+- 当前生产方向使用 Azure Key Vault 作为 Secret Provider。
- 在 Manager 后端实现 Secret Broker。
- Secret Broker 负责写入、轮换、撤销、禁用和审计。
- Manager DB 只保存 `secret_ref`,不保存明文密钥。
@@ -76,10 +76,10 @@
- Agnet 平台支持 deployment / role 到 Kubernetes ServiceAccount 的映射。
- 支持 Vault Kubernetes Auth 或等价 Workload Identity。
-- 支持按 user / resource binding / role 生成 OpenBao policy。
+- 支持按 user / resource binding / role 生成密钥访问策略。
- 子 Agnet 运行时只能访问被授权的 secret。
- 普通开发资源支持受控注入。
-- 高危操作审批只在客户端完成;审批通过后允许向子 Agnet 注入 OpenBao 派生的短期、最小权限凭证。
+- 高危操作审批只在客户端完成;审批通过后允许向子 Agnet 注入密钥保管器派生的短期、最小权限凭证。
验收:
diff --git a/docs/product-package/01-product-overview.md b/docs/product-package/01-product-overview.md
index 1ffe5f5f..d15ff3cb 100644
--- a/docs/product-package/01-product-overview.md
+++ b/docs/product-package/01-product-overview.md
@@ -38,7 +38,7 @@ Heicode 是一款覆盖软件生命周期的智能开发 Code 工具。它面向
| Heicode Manager / 浏览器控制台 | 辅助控制台,负责账号与安全、客户端下载、Git/云资源绑定、Agnet 部署、任务状态总览、模型余额与用量、审计与日志查看 |
| Agnet 平台 | 执行层,负责部署和运行子 Agnet,在任务推进过程中完成需求、开发、测试、修复、交付与部署,并回传日志、状态、事件和指标 |
| CodeGW | 内部模型网关与计费服务,普通用户不直接进入后台 |
-| 密钥保管器 | OpenBao 实现,保存长期凭证,按审批和权限提供短期凭证租约 |
+| 密钥保管器 | Azure Key Vault 实现,保存长期凭证引用,按审批和权限提供短期访问能力 |
说明:Heicode 客户端是用户主体验,用户不在网页上编码。Heicode Manager 是浏览器里的辅助控制台,承担资源、部署、状态、余额、审计和下载等辅助操作。真正持续推进任务的是 Heicode 调度下的 Agnet 执行闭环,Agnet 在过程中还可以调用已授权的 SK 工具和外部能力,最终完成交付与部署并把结果回传到客户端。
diff --git a/docs/product-package/02-homepage-design-brief.md b/docs/product-package/02-homepage-design-brief.md
index 3fd45265..681063ca 100644
--- a/docs/product-package/02-homepage-design-brief.md
+++ b/docs/product-package/02-homepage-design-brief.md
@@ -136,7 +136,7 @@
| 客户端 | 主对话体验、任务推进和高危审批 |
| Agnet 平台 | AKS 执行层 |
| CodeGW | 模型网关与用量 |
-| OpenBao | 密钥保管器 |
+| Azure Key Vault | 密钥保管器 |
补充说明:
@@ -170,4 +170,4 @@
- 信息密度适中,避免空泛大词。
- 主色与 Heicode 和客户端保持一致。
- 图片优先使用真实界面和流程,不使用无意义装饰图。
-- 首页文案避免出现 CodeGW 后台、OpenBao 控制台、模型提供方配置等内部概念。
+- 首页文案避免出现 CodeGW 后台、Azure Key Vault 控制台、模型提供方配置等内部概念。
diff --git a/docs/product-package/04-platform-usage-guide.md b/docs/product-package/04-platform-usage-guide.md
index a912e9b1..a7c9e742 100644
--- a/docs/product-package/04-platform-usage-guide.md
+++ b/docs/product-package/04-platform-usage-guide.md
@@ -12,7 +12,7 @@
https://code.xinghanlab.com
```
-用户只需要登录 Heicode。普通用户不需要登录 CodeGW 后台,也不需要登录 OpenBao。
+用户只需要登录 Heicode。普通用户不需要登录 CodeGW 后台,也不需要登录 Azure Key Vault。
## 第一步:登录 Heicode
diff --git a/docs/product-package/05-ppt-copy.md b/docs/product-package/05-ppt-copy.md
index 20b3c12b..9513b433 100644
--- a/docs/product-package/05-ppt-copy.md
+++ b/docs/product-package/05-ppt-copy.md
@@ -65,7 +65,7 @@ Heicode 是一款全流程智能开发 Code 工具。它让用户输入想法,
讲述:
-用户不需要直接操作 CodeGW 后台,也不需要直接管理 OpenBao。所有操作都在 Heicode 和客户端内完成。
+用户不需要直接操作 CodeGW 后台,也不需要直接管理 Azure Key Vault。所有操作都在 Heicode 和客户端内完成。
## Slide 5:核心能力
@@ -91,13 +91,13 @@ Heicode 是一款全流程智能开发 Code 工具。它让用户输入想法,
-> Heicode Manager
-> Agnet 平台
-> CodeGW
--> OpenBao
+-> Azure Key Vault
-> Git / SK / 云资源
```
讲述:
-客户端是主体验,Manager 是辅助控制台。Agnet 平台是执行层,执行过程中可调用 SK。CodeGW 是模型网关和计费服务。OpenBao 是密钥保管器。
+客户端是主体验,Manager 是辅助控制台。Agnet 平台是执行层,执行过程中可调用 SK。CodeGW 是模型网关和计费服务。Azure Key Vault 是密钥保管器。
## Slide 7:安全模型
@@ -108,7 +108,7 @@ Heicode 是一款全流程智能开发 Code 工具。它让用户输入想法,
要点:
- Heicode 服务端数据库只保存 `secret_ref`。
-- 长期密钥进入 OpenBao。
+- 长期密钥进入 Azure Key Vault。
- 子 Agnet 只获得短期、最小权限凭证。
- 高危操作必须在客户端审批。
- 日志、Markdown、前端响应不包含明文密钥。
@@ -141,7 +141,7 @@ Heicode 是一款全流程智能开发 Code 工具。它让用户输入想法,
> 当前 MVP 先闭环三件事
-1. Heicode、CodeGW、OpenBao 解耦部署。
+1. Heicode、CodeGW、Azure Key Vault 解耦部署。
2. 资源绑定、Resource Grant、manifest 和密钥保管。
3. 客户端只登录 Heicode,只使用 Heicode 模型。
diff --git a/docs/product-package/06-security-and-credential-management.md b/docs/product-package/06-security-and-credential-management.md
index a39b3d7a..13f0b549 100644
--- a/docs/product-package/06-security-and-credential-management.md
+++ b/docs/product-package/06-security-and-credential-management.md
@@ -26,17 +26,17 @@ Heicode 是 SaaS 产品,不能把凭证管理转嫁给用户,也不能让密
用户可见名称:密钥保管器。
-技术实现:OpenBao,兼容 Vault API。
+技术实现:Azure Key Vault。生产环境中 Manager 通过 VM Managed Identity 访问 Key Vault,不在业务数据库保存明文密钥。
-用户不需要直接登录 OpenBao,也不需要看到 OpenBao 控制台。
+用户不需要直接登录 Azure Key Vault,也不需要看到 Key Vault 控制台。
## Secret Broker 流程
```text
用户绑定资源
-> Heicode 接收授权结果
--> Heicode Secret Broker 写入 OpenBao
--> OpenBao 返回或形成 secret_ref
+-> Heicode Secret Broker 写入 Azure Key Vault
+-> Azure Key Vault 返回或形成 azkv://... 形式的 secret_ref
-> Heicode DB 保存 secret_ref
-> 前端只展示脱敏引用和状态
```
@@ -87,7 +87,7 @@ Heicode 生成 Resource Grant
3. 禁止前端返回明文密钥。
4. 禁止在日志中打印密钥。
5. 禁止把长期云密钥注入子 Agnet。
-6. 禁止把 OpenBao 暴露为普通公网入口。
+6. 禁止把 Azure Key Vault 管理入口作为普通用户入口暴露。
7. 禁止把 CodeGW key 原文交给客户端长期保存。
## 用户侧解释
@@ -105,4 +105,4 @@ Heicode 生成 Resource Grant
| 日志 | 不包含 token、password、private key、access key |
| 子 Agnet | 不保存长期凭证 |
| 高危操作 | 有客户端审批记录 |
-| OpenBao | 不对普通公网暴露 |
+| Azure Key Vault | 普通用户不可直接进入;Manager 仅通过受控后端身份访问 |
diff --git a/docs/product-package/07-integration-boundaries.md b/docs/product-package/07-integration-boundaries.md
index 71aea205..af7b70f2 100644
--- a/docs/product-package/07-integration-boundaries.md
+++ b/docs/product-package/07-integration-boundaries.md
@@ -1,8 +1,8 @@
-# 07. CodeGW、OpenBao、Agnet 平台边界说明
+# 07. CodeGW、Azure Key Vault、Agnet 平台边界说明
## 目标
-本文用于讲清楚 Heicode 与 CodeGW、OpenBao、Agnet 平台之间的产品和技术边界,避免再次变成缝合式系统。
+本文用于讲清楚 Heicode 与 CodeGW、Azure Key Vault、Agnet 平台之间的产品和技术边界,避免再次变成缝合式系统。
## 总体边界
@@ -10,7 +10,7 @@
|------|----------|----------------------|
| Heicode | 产品总入口,包含客户端主体验与 Manager 辅助控制台 | 是 |
| CodeGW | 模型网关、模型调用、余额、额度、用量日志 | 否 |
-| OpenBao | 长期凭证保管、短期凭证租约 | 否 |
+| Azure Key Vault | 长期凭证托管、密钥引用、后端受控读取 | 否 |
| Agnet 平台 | AKS 执行层、状态、日志、事件、指标 | 否 |
## Heicode 与 CodeGW
@@ -34,20 +34,20 @@ Heicode 不做:
> CodeGW 是 Heicode 的模型网关和计费底座,用户在 Heicode 中查看模型和用量,不直接使用 CodeGW 后台。
-## Heicode 与 OpenBao
+## Heicode 与 Azure Key Vault
-Heicode 使用 OpenBao 的方式:
+Heicode 使用 Azure Key Vault 的方式:
- 写入用户授权后的长期凭证。
- 保存和管理 `secret_ref`。
-- 执行密钥 health、metadata、轮换、禁用、撤销。
+- 执行密钥健康检查、metadata、轮换、禁用、撤销。
- 为高危操作申请短期凭证。
Heicode 不做:
- 在数据库保存明文密钥。
-- 把 OpenBao token 交给前端。
-- 把 OpenBao 作为普通公网服务暴露。
+- 把 Azure Key Vault access token 交给前端。
+- 把 Azure Key Vault 管理入口作为普通用户入口暴露。
产品口径:
@@ -86,7 +86,7 @@ Heicode 不传:
- 长期明文密钥。
- CodeGW key 原文。
-- OpenBao root token。
+- Azure Key Vault 管理凭据。
- 数据库密码。
产品口径:
@@ -117,7 +117,7 @@ Manager 主做:
- 选择模型提供方。
- 配置 CodeGW 渠道。
-- 直接访问 OpenBao。
+- 直接访问 Azure Key Vault。
- 直接保存长期云密钥。
Manager 不做:
@@ -144,5 +144,5 @@ Heicode 到 Agnet 平台真实部署 API 的完整联调仍可延期。
1. 普通用户只进入 Heicode。
2. 客户端只登录 Heicode。
3. CodeGW 独立服务运行。
-4. OpenBao 只通过受控内网或后端访问。
+4. Azure Key Vault 只通过 Manager 后端受控身份访问,普通用户不直接进入。
5. Agnet 平台接口只收结构化上下文和 `secret_ref`,不收长期明文密钥。
diff --git a/docs/product-package/08-client-guide.md b/docs/product-package/08-client-guide.md
index 6ed894a5..60e767a3 100644
--- a/docs/product-package/08-client-guide.md
+++ b/docs/product-package/08-client-guide.md
@@ -104,7 +104,7 @@ macOS 注意:
2. 旧登录入口。
3. 第三方路由登录入口。
4. CodeGW 后台入口。
-5. OpenBao 地址或 token。
+5. Azure Key Vault 地址、access token 或密钥保管器后台信息。
6. 长期云密钥输入框。
7. 无法解释的个人 API key 自动复制。
diff --git a/docs/product-package/09-demo-script.md b/docs/product-package/09-demo-script.md
index 52d8b616..25e58a15 100644
--- a/docs/product-package/09-demo-script.md
+++ b/docs/product-package/09-demo-script.md
@@ -166,7 +166,7 @@
1. 不展示真实密钥。
2. 不进入 CodeGW 管理后台。
-3. 不展示 OpenBao token。
+3. 不展示 Azure Key Vault access token。
4. 不强调底层配置复杂度,强调用户流程。
5. 如果 Agnet 真实部署尚未联调,明确说当前展示的是 Heicode 侧 payload、manifest 和占位控制面。
@@ -174,4 +174,4 @@
话术:
-> Heicode 是从想法到上线的智能开发工具。用户主要在客户端输入想法和继续推进任务,在 Manager 绑定 Git、文档、SK 和云资源并部署 Agnet。Heicode 生成子 Agnet 团队、角色和权限,密钥进入密钥保管器,子 Agnet 只拿短期凭证。Agnet 在执行中可以调用 SK 工具,最后完成交付与部署;CodeGW 提供模型、余额和用量,Heicode 展示日志、状态和审计。用户不需要进入 CodeGW 后台,也不需要管理 OpenBao,只在 Heicode 和客户端完成整个开发流程。
+> Heicode 是从想法到上线的智能开发工具。用户主要在客户端输入想法和继续推进任务,在 Manager 绑定 Git、文档、SK 和云资源并部署 Agnet。Heicode 生成子 Agnet 团队、角色和权限,密钥进入密钥保管器,子 Agnet 只拿短期凭证。Agnet 在执行中可以调用 SK 工具,最后完成交付与部署;CodeGW 提供模型、余额和用量,Heicode 展示日志、状态和审计。用户不需要进入 CodeGW 后台,也不需要管理 Azure Key Vault,只在 Heicode 和客户端完成整个开发流程。
diff --git a/docs/product-package/10-frontend-detail-spec.md b/docs/product-package/10-frontend-detail-spec.md
index 9e42eac3..4e62f9a9 100644
--- a/docs/product-package/10-frontend-detail-spec.md
+++ b/docs/product-package/10-frontend-detail-spec.md
@@ -38,7 +38,7 @@ Heicode 不以后台菜单作为主体验。建议使用顶部轻量入口和主
- CodeGW 后台。
- 渠道管理。
- 模型供应商配置。
-- OpenBao 控制台。
+- Azure Key Vault 控制台。
- 模型提供方选择。
- 旧服务入口。
@@ -322,7 +322,7 @@ Heicode 只追问影响执行路径的少量问题:
## 文案规则
- 用“任务上下文”或“资源上下文”,不要把主流程叫成资源管理。
-- 用“密钥保管器”,不用“OpenBao”作为普通用户标题。
+- 用“密钥保管器”,不要用具体实现名作为普通用户标题。
- 用“客户端”,不用“本地模型提供方”。
- 用“模型与余额”,不用“CodeGW 后台”。
- 用“执行计划”,不用“控制面草案”。
diff --git a/docs/product-package/12-narrated-user-operation-flow.md b/docs/product-package/12-narrated-user-operation-flow.md
index d6d166b9..2f052679 100644
--- a/docs/product-package/12-narrated-user-operation-flow.md
+++ b/docs/product-package/12-narrated-user-operation-flow.md
@@ -334,7 +334,7 @@ Heicode 会复用之前的项目上下文、资源绑定和 Agnet 角色,生
## 口述时不要说
1. 让用户去 CodeGW 后台配置模型。
-2. 让用户去 OpenBao 控制台管理密钥。
+2. 让用户去 Azure Key Vault 控制台管理密钥。
3. 让客户端选择模型提供方。
4. 让用户手写复杂 manifest。
5. 把 tenant/project 当作当前产品主轴。
diff --git a/docs/product-package/13-platform-description.md b/docs/product-package/13-platform-description.md
index 843faabf..3db6faf8 100644
--- a/docs/product-package/13-platform-description.md
+++ b/docs/product-package/13-platform-description.md
@@ -256,7 +256,9 @@ Heicode 通过服务端调用 CodeGW,并把普通用户需要的信息展示
### 5.5 密钥保管器
-密钥保管器用于保存长期凭证。当前技术实现是 OpenBao。
+密钥保管器用于保存长期凭证。当前技术实现是 Azure Key Vault。
+
+生产环境中 Manager 使用 VM Managed Identity 访问 Azure Key Vault。用户绑定资源后,Manager 将敏感凭证写入 Key Vault,并在 Heicode 数据库中只保存 `azkv:///secrets/` 形式的 `secret_ref`。
它负责:
@@ -334,7 +336,7 @@ Heicode 判断当前任务需要哪些上下文
用户不需要直接使用:
- CodeGW 后台。
-- OpenBao 控制台。
+- Azure Key Vault 控制台。
- Agnet 平台后台。
用户不应该看到:
@@ -342,7 +344,7 @@ Heicode 判断当前任务需要哪些上下文
- 模型提供方选择。
- 旧服务入口。
- 明文密钥。
-- OpenBao token。
+- Azure Key Vault access token。
- CodeGW 管理员配置。
## 十一、常见问题
diff --git a/docs/product-package/README.md b/docs/product-package/README.md
index 2e3536c4..5176245b 100644
--- a/docs/product-package/README.md
+++ b/docs/product-package/README.md
@@ -14,7 +14,7 @@
| [`04-platform-usage-guide.md`](./04-platform-usage-guide.md) | 登录平台和使用平台指南,面向真实用户上手 |
| [`05-ppt-copy.md`](./05-ppt-copy.md) | 产品 PPT 文案,面向路演、客户介绍和团队同步 |
| [`06-security-and-credential-management.md`](./06-security-and-credential-management.md) | 安全与凭证管理说明,解释密钥保管器、`secret_ref`、高危审批 |
-| [`07-integration-boundaries.md`](./07-integration-boundaries.md) | CodeGW、OpenBao、Agnet 平台边界说明 |
+| [`07-integration-boundaries.md`](./07-integration-boundaries.md) | CodeGW、Azure Key Vault、Agnet 平台边界说明 |
| [`08-client-guide.md`](./08-client-guide.md) | Heicode 客户端说明,强调只登录 Heicode、只使用 Heicode 模型 |
| [`09-demo-script.md`](./09-demo-script.md) | 5 分钟演示脚本,用于产品演示和录屏 |
| [`10-frontend-detail-spec.md`](./10-frontend-detail-spec.md) | 前端细节说明,覆盖 Heicode 和客户端页面、组件、状态、文案规则 |
@@ -37,7 +37,7 @@ Heicode 是一款面向全流程智能开发的 SaaS Code 工具。用户注册
- `Heicode Manager` 是浏览器里的辅助控制台,负责账号与安全、客户端下载、Git/云资源绑定、Agnet 部署、任务状态、余额与审计。
- 继续开发时,Heicode 会持续调用 Agnet 推进需求、开发、测试、修复和部署等子环节。
- Agnet 在执行过程中可以调用已授权的 SK 工具,最终完成交付与部署并把结果回传给客户端和 Manager。
-- 用户侧叫 `密钥保管器`,技术实现可以说明是 OpenBao。
+- 用户侧叫 `密钥保管器`,技术实现可以说明是 Azure Key Vault;Manager 通过 VM Managed Identity 访问,不要求普通用户进入 Key Vault 后台。
- 用户侧叫 `资源绑定`,不要叫 Git 来源。
- 客户端不出现 `模型提供方`、`旧服务入口`、`第三方路由入口`。
- CodeGW 是内部模型网关和计费服务,不作为普通用户产品入口。
diff --git a/heicode/.gitignore b/heicode/.gitignore
index d49f2382..84f648f1 100644
--- a/heicode/.gitignore
+++ b/heicode/.gitignore
@@ -9,7 +9,13 @@ build
*.db-journal
logs
web/default/dist
+!web/default/dist/
+web/default/dist/*
+!web/default/dist/index.html
web/classic/dist
+!web/classic/dist/
+web/classic/dist/*
+!web/classic/dist/index.html
web/node_modules
web/dist
.env
diff --git a/heicode/VERSION b/heicode/VERSION
index 9df886c4..428b770e 100644
--- a/heicode/VERSION
+++ b/heicode/VERSION
@@ -1 +1 @@
-1.4.2
+1.4.3
diff --git a/heicode/controller/agnet_control_plane_test.go b/heicode/controller/agnet_control_plane_test.go
index 5047e73f..8e2f3ff9 100644
--- a/heicode/controller/agnet_control_plane_test.go
+++ b/heicode/controller/agnet_control_plane_test.go
@@ -91,7 +91,7 @@ func baseAgnetResourceGrantPlan() agnetOrchestrationPlan {
"repo_url": "https://example.invalid/acme/project.git",
},
Status: agnetGrantStatusActive,
- SecretRef: "vault://secret/users/user-p1/bindings/project-main/resources/res-git-main",
+ SecretRef: "azkv://heicode-kv.vault.azure.net/secrets/user-p1-project-main-res-git-main",
Audit: map[string]string{
"created_by": "manager-test",
},
@@ -162,7 +162,7 @@ func TestAgnetCreateDeploymentAcceptsP1ResourceGrantModel(t *testing.T) {
require.Equal(t, "https://example.invalid/acme/project.git#main", grants[0].BindingScope)
require.Equal(t, "builder", grants[0].TargetRole)
require.Equal(t, "agent-builder-1", grants[0].TargetAgentRef)
- require.Equal(t, "vault://secret/users/user-p1/bindings/project-main/resources/res-git-main", grants[0].SecretRef)
+ require.Equal(t, "azkv://heicode-kv.vault.azure.net/secrets/user-p1-project-main-res-git-main", grants[0].SecretRef)
require.Empty(t, grants[1].SecretRef, "project document grants should not require credential material")
require.Equal(t, "newapi", stored.Plan.BillingContext.Provider)
require.Equal(t, "channel-p1", stored.Plan.UserContext.ChannelID)
@@ -176,7 +176,7 @@ func TestAgnetCreateDeploymentAcceptsP1ResourceGrantModel(t *testing.T) {
require.Equal(t, "user-p1", stored.ResourceGrantManifest.UserID)
require.Equal(t, "builder", stored.ResourceGrantManifest.AgentRole)
require.Equal(t, "agent-builder-1", stored.ResourceGrantManifest.TargetAgentRef)
- require.Equal(t, "vault://secret/users/user-p1/bindings/project-main/resources/res-git-main", stored.ResourceGrantManifest.ResourceGrants[0].SecretRef)
+ require.Equal(t, "azkv://heicode-kv.vault.azure.net/secrets/user-p1-project-main-res-git-main", stored.ResourceGrantManifest.ResourceGrants[0].SecretRef)
}
func TestAgnetPermissionManifestExcludesInactiveResourceGrants(t *testing.T) {
diff --git a/heicode/controller/azure_resource_discovery.go b/heicode/controller/azure_resource_discovery.go
new file mode 100644
index 00000000..a7587241
--- /dev/null
+++ b/heicode/controller/azure_resource_discovery.go
@@ -0,0 +1,311 @@
+package controller
+
+import (
+ "errors"
+ "fmt"
+ "net/http"
+ "net/url"
+ "os"
+ "strings"
+ "time"
+
+ "github.com/gin-gonic/gin"
+ "github.com/heicode/manager/common"
+ "github.com/heicode/manager/model"
+ "gorm.io/gorm"
+)
+
+type azureResourceDiscoveryClient struct {
+ authorityHost string
+ armBaseURL string
+ client *http.Client
+}
+
+type azureARMResource struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ Type string `json:"type"`
+ Location string `json:"location"`
+ Tags map[string]any `json:"tags"`
+}
+
+type azureARMResourceListResponse struct {
+ Value []azureARMResource `json:"value"`
+ NextLink string `json:"nextLink"`
+}
+
+func newAzureResourceDiscoveryClientFromEnv() azureResourceDiscoveryClient {
+ authorityHost := strings.TrimRight(strings.TrimSpace(os.Getenv("AZURE_AUTHORITY_HOST")), "/")
+ if authorityHost == "" {
+ authorityHost = "https://login.microsoftonline.com"
+ }
+ armBaseURL := strings.TrimRight(strings.TrimSpace(os.Getenv("AZURE_ARM_BASE_URL")), "/")
+ if armBaseURL == "" {
+ armBaseURL = "https://management.azure.com"
+ }
+ return azureResourceDiscoveryClient{
+ authorityHost: authorityHost,
+ armBaseURL: armBaseURL,
+ client: &http.Client{Timeout: 20 * time.Second},
+ }
+}
+
+func DiscoverAzureResources(c *gin.Context) {
+ userId := c.GetInt("id")
+ var account model.ResourceBinding
+ if err := model.DB.Where("id = ? AND user_id = ?", c.Param("id"), userId).First(&account).Error; err != nil {
+ if errors.Is(err, gorm.ErrRecordNotFound) {
+ common.ApiErrorMsg(c, "resource not found")
+ return
+ }
+ common.ApiError(c, err)
+ return
+ }
+ if account.ResourceType != "cloud_account" || strings.ToLower(account.Provider) != "azure" {
+ common.ApiErrorMsg(c, "resource must be an active Azure cloud_account")
+ return
+ }
+ if account.Status != "active" {
+ common.ApiErrorMsg(c, "resource must be an active Azure cloud_account")
+ return
+ }
+
+ secretClient, err := newSecretStoreClientFromEnv()
+ if err != nil {
+ common.ApiError(c, err)
+ return
+ }
+ secretData, err := secretClient.getJSONSecret(account.SecretRef)
+ if err != nil {
+ common.ApiError(c, err)
+ return
+ }
+ metadata := unmarshalResourceJSON(account.Metadata)
+ credentials := azureDiscoveryCredentials{
+ SubscriptionID: firstString(metadata, secretData, account.ExternalId, "subscription_id"),
+ TenantID: firstString(metadata, secretData, account.TenantId, "tenant_id"),
+ ClientID: firstString(metadata, secretData, "", "client_id"),
+ ClientSecret: firstString(nil, secretData, "", "client_secret"),
+ }
+ if err := credentials.validate(); err != nil {
+ common.ApiError(c, err)
+ return
+ }
+
+ azureClient := newAzureResourceDiscoveryClientFromEnv()
+ token, err := azureClient.clientCredentialsToken(credentials)
+ if err != nil {
+ common.ApiError(c, err)
+ return
+ }
+ discovered, err := azureClient.listSubscriptionResources(credentials.SubscriptionID, token)
+ if err != nil {
+ common.ApiError(c, err)
+ return
+ }
+ items, err := upsertDiscoveredAzureResources(account, credentials, discovered)
+ if err != nil {
+ common.ApiError(c, err)
+ return
+ }
+ common.ApiSuccess(c, gin.H{
+ "account": resourceToResponse(account),
+ "items": items,
+ "discovered": len(items),
+ })
+}
+
+type azureDiscoveryCredentials struct {
+ SubscriptionID string
+ TenantID string
+ ClientID string
+ ClientSecret string
+}
+
+func (c azureDiscoveryCredentials) validate() error {
+ if strings.TrimSpace(c.SubscriptionID) == "" {
+ return errors.New("Azure subscription_id required")
+ }
+ if strings.TrimSpace(c.TenantID) == "" {
+ return errors.New("Azure tenant_id required")
+ }
+ if strings.TrimSpace(c.ClientID) == "" {
+ return errors.New("Azure client_id required")
+ }
+ if strings.TrimSpace(c.ClientSecret) == "" {
+ return errors.New("Azure client_secret required")
+ }
+ return nil
+}
+
+func firstString(primary map[string]any, secondary map[string]any, fallback string, key string) string {
+ if value := mapString(primary, key); value != "" {
+ return value
+ }
+ if value := mapString(secondary, key); value != "" {
+ return value
+ }
+ return strings.TrimSpace(fallback)
+}
+
+func mapString(values map[string]any, key string) string {
+ if values == nil {
+ return ""
+ }
+ switch value := values[key].(type) {
+ case string:
+ return strings.TrimSpace(value)
+ case fmt.Stringer:
+ return strings.TrimSpace(value.String())
+ default:
+ return ""
+ }
+}
+
+func (a azureResourceDiscoveryClient) clientCredentialsToken(credentials azureDiscoveryCredentials) (string, error) {
+ endpoint := fmt.Sprintf("%s/%s/oauth2/v2.0/token", a.authorityHost, url.PathEscape(credentials.TenantID))
+ form := url.Values{}
+ form.Set("grant_type", "client_credentials")
+ form.Set("client_id", credentials.ClientID)
+ form.Set("client_secret", credentials.ClientSecret)
+ form.Set("scope", "https://management.azure.com/.default")
+ req, err := http.NewRequest(http.MethodPost, endpoint, strings.NewReader(form.Encode()))
+ if err != nil {
+ return "", err
+ }
+ req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+ resp, err := a.client.Do(req)
+ if err != nil {
+ return "", fmt.Errorf("Azure client credential token request failed: %w", err)
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
+ message := readSecretStoreError(resp.Body)
+ if message == "" {
+ message = resp.Status
+ }
+ return "", fmt.Errorf("Azure client credential token request failed: %s", message)
+ }
+ var payload struct {
+ AccessToken string `json:"access_token"`
+ }
+ if err := common.DecodeJson(resp.Body, &payload); err != nil {
+ return "", err
+ }
+ if strings.TrimSpace(payload.AccessToken) == "" {
+ return "", errors.New("Azure client credential token response missing access_token")
+ }
+ return payload.AccessToken, nil
+}
+
+func (a azureResourceDiscoveryClient) listSubscriptionResources(subscriptionID string, token string) ([]azureARMResource, error) {
+ endpoint := fmt.Sprintf("%s/subscriptions/%s/resources?api-version=2021-04-01", a.armBaseURL, url.PathEscape(subscriptionID))
+ var out []azureARMResource
+ for page := 0; page < 20 && endpoint != ""; page++ {
+ req, err := http.NewRequest(http.MethodGet, endpoint, nil)
+ if err != nil {
+ return nil, err
+ }
+ req.Header.Set("Authorization", "Bearer "+token)
+ resp, err := a.client.Do(req)
+ if err != nil {
+ return nil, fmt.Errorf("Azure ARM resources request failed: %w", err)
+ }
+ var payload azureARMResourceListResponse
+ if resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusMultipleChoices {
+ err = common.DecodeJson(resp.Body, &payload)
+ } else {
+ message := readSecretStoreError(resp.Body)
+ if message == "" {
+ message = resp.Status
+ }
+ err = fmt.Errorf("Azure ARM resources request failed: %s", message)
+ }
+ resp.Body.Close()
+ if err != nil {
+ return nil, err
+ }
+ out = append(out, payload.Value...)
+ endpoint = strings.TrimSpace(payload.NextLink)
+ }
+ return out, nil
+}
+
+func upsertDiscoveredAzureResources(account model.ResourceBinding, credentials azureDiscoveryCredentials, discovered []azureARMResource) ([]resourceResponse, error) {
+ items := make([]resourceResponse, 0, len(discovered))
+ for _, armResource := range discovered {
+ if strings.TrimSpace(armResource.ID) == "" {
+ continue
+ }
+ metadata := map[string]any{
+ "subscription_id": credentials.SubscriptionID,
+ "tenant_id": credentials.TenantID,
+ "source_account_id": account.Id,
+ "type": armResource.Type,
+ "location": armResource.Location,
+ "resource_group": azureResourceGroupFromID(armResource.ID),
+ "tags": armResource.Tags,
+ }
+ metadataJSON, err := marshalResourceJSON(metadata)
+ if err != nil {
+ return nil, err
+ }
+ permissionScopeJSON, err := marshalResourceJSON(map[string]any{"actions": []string{"azure:read"}})
+ if err != nil {
+ return nil, err
+ }
+ constraints := unmarshalResourceJSON(account.Constraints)
+ constraintsJSON, err := marshalResourceJSON(constraints)
+ if err != nil {
+ return nil, err
+ }
+ name := strings.TrimSpace(armResource.Name)
+ if name == "" {
+ name = armResource.ID
+ }
+ bindingScope := fmt.Sprintf("azure:%s:%s", credentials.SubscriptionID, armResource.ID)
+ var resource model.ResourceBinding
+ err = model.DB.Where(
+ "user_id = ? AND resource_type = ? AND provider = ? AND external_id = ?",
+ account.UserId,
+ "cloud_resource",
+ "azure",
+ armResource.ID,
+ ).First(&resource).Error
+ if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
+ return nil, err
+ }
+ resource.UserId = account.UserId
+ resource.TenantId = account.TenantId
+ resource.ProjectId = account.ProjectId
+ resource.BindingScope = bindingScope
+ resource.Name = name
+ resource.ResourceType = "cloud_resource"
+ resource.Provider = "azure"
+ resource.ExternalId = armResource.ID
+ resource.SecretRef = account.SecretRef
+ resource.Metadata = metadataJSON
+ resource.PermissionScope = permissionScopeJSON
+ resource.Constraints = constraintsJSON
+ resource.Status = "active"
+ if resource.Id == 0 {
+ if err := model.DB.Create(&resource).Error; err != nil {
+ return nil, err
+ }
+ } else if err := model.DB.Save(&resource).Error; err != nil {
+ return nil, err
+ }
+ items = append(items, resourceToResponse(resource))
+ }
+ return items, nil
+}
+
+func azureResourceGroupFromID(id string) string {
+ parts := strings.Split(strings.Trim(id, "/"), "/")
+ for i := 0; i+1 < len(parts); i++ {
+ if strings.EqualFold(parts[i], "resourceGroups") {
+ return parts[i+1]
+ }
+ }
+ return ""
+}
diff --git a/heicode/controller/resource.go b/heicode/controller/resource.go
index aeac6c1c..cd4c731c 100644
--- a/heicode/controller/resource.go
+++ b/heicode/controller/resource.go
@@ -1,6 +1,8 @@
package controller
import (
+ "crypto/sha256"
+ "encoding/hex"
"errors"
"fmt"
"net/http"
@@ -45,6 +47,9 @@ var secretLikeKeys = map[string]bool{
"token": true,
}
+const azureKeyVaultSecretNameMaxLen = 127
+const azureKeyVaultSecretNameHashLen = 16
+
type resourcePayload struct {
TenantId string `json:"tenant_id"`
ProjectId string `json:"project_id"`
@@ -532,12 +537,13 @@ func UpsertResourceSecret(c *gin.Context) {
common.ApiError(c, err)
return
}
- secretPath := resourceSecretPath(resource)
- if err := client.putKV2(secretPath, payload.Data); err != nil {
+ secretName := resourceSecretName(resource)
+ secretRef, err := client.putSecret(secretName, payload.Data)
+ if err != nil {
common.ApiError(c, err)
return
}
- resource.SecretRef = fmt.Sprintf("vault://%s/%s", client.mount, secretPath)
+ resource.SecretRef = secretRef
if err := model.DB.Save(&resource).Error; err != nil {
common.ApiError(c, err)
return
@@ -548,7 +554,7 @@ func UpsertResourceSecret(c *gin.Context) {
})
}
-func resourceSecretPath(resource model.ResourceBinding) string {
+func resourceSecretName(resource model.ResourceBinding) string {
scope := resource.BindingScope
if strings.TrimSpace(scope) == "" {
scope = resource.ExternalId
@@ -556,20 +562,17 @@ func resourceSecretPath(resource model.ResourceBinding) string {
if strings.TrimSpace(scope) == "" {
scope = resource.Name
}
- return strings.Join([]string{
- "users",
- fmt.Sprintf("%d", resource.UserId),
- "bindings",
+ return compactAzureSecretName(
+ fmt.Sprintf("users-%d-bindings-", resource.UserId),
sanitizeSecretPathSegment(scope),
- "resources",
- fmt.Sprintf("%d", resource.Id),
- }, "/")
+ fmt.Sprintf("-resources-%d", resource.Id),
+ )
}
func sanitizeSecretPathSegment(value string) string {
value = strings.TrimSpace(value)
if value == "" {
- return "_"
+ return "resource"
}
var b strings.Builder
for _, r := range value {
@@ -580,7 +583,7 @@ func sanitizeSecretPathSegment(value string) string {
b.WriteRune(r)
case r >= '0' && r <= '9':
b.WriteRune(r)
- case r == '_' || r == '-' || r == '.':
+ case r == '-':
b.WriteRune(r)
default:
b.WriteRune('-')
@@ -589,6 +592,30 @@ func sanitizeSecretPathSegment(value string) string {
return b.String()
}
+func compactAzureSecretName(prefix string, segment string, suffix string) string {
+ name := prefix + segment + suffix
+ if len(name) <= azureKeyVaultSecretNameMaxLen {
+ return name
+ }
+ hash := sha256.Sum256([]byte(segment))
+ hashSuffix := hex.EncodeToString(hash[:])[:azureKeyVaultSecretNameHashLen]
+ maxSegmentLen := azureKeyVaultSecretNameMaxLen - len(prefix) - len(suffix) - 1 - azureKeyVaultSecretNameHashLen
+ if maxSegmentLen < 1 {
+ maxSegmentLen = 1
+ }
+ if len(segment) > maxSegmentLen {
+ segment = segment[:maxSegmentLen]
+ }
+ segment = strings.Trim(segment, "-")
+ if segment == "" {
+ segment = "resource"
+ if len(segment) > maxSegmentLen {
+ segment = segment[:maxSegmentLen]
+ }
+ }
+ return prefix + segment + "-" + hashSuffix + suffix
+}
+
func marshalResourcePayloadJSON(payload resourcePayload) (string, string, string, error) {
metadata, err := marshalResourceJSON(payload.Metadata)
if err != nil {
diff --git a/heicode/controller/resource_test.go b/heicode/controller/resource_test.go
index 21b9b610..20afb7ff 100644
--- a/heicode/controller/resource_test.go
+++ b/heicode/controller/resource_test.go
@@ -5,6 +5,7 @@ import (
"fmt"
"net/http"
"net/http/httptest"
+ "net/url"
"strings"
"testing"
@@ -65,7 +66,7 @@ func TestCreateResourceStoresMetadataAndSecretRefOnly(t *testing.T) {
"resource_type":"git",
"provider":"github",
"external_id":"https://example.com/org/repo",
- "secret_ref":"vault://secret/resources/repo",
+ "secret_ref":"azkv://heicode-kv.vault.azure.net/secrets/repo",
"metadata":{"repo_url":"https://example.com/org/repo","ref":"main","allowed_paths":["."]},
"permission_scope":{"actions":["read","write"]},
"constraints":{"environment":"dev"}
@@ -75,13 +76,13 @@ func TestCreateResourceStoresMetadataAndSecretRefOnly(t *testing.T) {
require.Equal(t, http.StatusOK, w.Code)
require.Contains(t, w.Body.String(), `"success":true`)
require.Contains(t, w.Body.String(), `"binding_scope":"https://example.com/org/repo#main"`)
- require.Contains(t, w.Body.String(), `"secret_ref":"vault://secret/resources/repo"`)
+ require.Contains(t, w.Body.String(), `"secret_ref":"azkv://heicode-kv.vault.azure.net/secrets/repo"`)
var resource model.ResourceBinding
require.NoError(t, db.First(&resource).Error)
require.Equal(t, "git", resource.ResourceType)
require.Equal(t, "https://example.com/org/repo#main", resource.BindingScope)
- require.Equal(t, "vault://secret/resources/repo", resource.SecretRef)
+ require.Equal(t, "azkv://heicode-kv.vault.azure.net/secrets/repo", resource.SecretRef)
require.NotContains(t, resource.Metadata, "token")
require.NotContains(t, resource.PermissionScope, "token")
require.NotContains(t, resource.Constraints, "token")
@@ -93,7 +94,7 @@ func TestCreateResourceRejectsPlaintextSecretKeys(t *testing.T) {
"name":"Cloud account",
"resource_type":"cloud_account",
"metadata":{"account_id":"sub-1","access_key":"do-not-store"},
- "secret_ref":"vault://secret/cloud/sub-1"
+ "secret_ref":"azkv://heicode-kv.vault.azure.net/secrets/cloud-sub-1"
}`
w := performResourceRequest(CreateResource, 7, http.MethodPost, "/", body)
@@ -110,7 +111,7 @@ func TestCreateResourceGrantAssignsBoundResourceToRoleAgnet(t *testing.T) {
Name: "SK repo",
ResourceType: "sk",
Provider: "git",
- SecretRef: "vault://secret/resources/sk-repo",
+ SecretRef: "azkv://heicode-kv.vault.azure.net/secrets/sk-repo",
Metadata: `{"repo_url":"https://example.com/sk.git"}`,
Status: "active",
}
@@ -173,7 +174,7 @@ func TestGenerateResourceGrantManifestIncludesActiveGrantsOnly(t *testing.T) {
ResourceType: "git",
Provider: "github",
ExternalId: "https://example.com/org/repo.git",
- SecretRef: "vault://secret/resources/repo-main",
+ SecretRef: "azkv://heicode-kv.vault.azure.net/secrets/repo-main",
Metadata: `{"repo_url":"https://example.com/org/repo.git"}`,
PermissionScope: `{"actions":["read","write"]}`,
Status: "active",
@@ -215,11 +216,11 @@ func TestGenerateResourceGrantManifestIncludesActiveGrantsOnly(t *testing.T) {
require.Contains(t, w.Body.String(), `"target_agent_ref":"agnet-backend-1"`)
require.Contains(t, w.Body.String(), `"resource_type":"git"`)
require.Contains(t, w.Body.String(), `"allowed_actions":["read"]`)
- require.Contains(t, w.Body.String(), `"secret_ref":"vault://secret/resources/repo-main"`)
+ require.Contains(t, w.Body.String(), `"secret_ref":"azkv://heicode-kv.vault.azure.net/secrets/repo-main"`)
require.NotContains(t, w.Body.String(), `"allowed_actions":["write"]`)
}
-func TestUpsertResourceSecretWritesOpenBaoAndStoresOnlySecretRef(t *testing.T) {
+func TestUpsertResourceSecretWritesAzureKeyVaultAndStoresOnlySecretRef(t *testing.T) {
db := setupResourceControllerTestDB(t)
resource := model.ResourceBinding{
UserId: 7,
@@ -231,38 +232,50 @@ func TestUpsertResourceSecretWritesOpenBaoAndStoresOnlySecretRef(t *testing.T) {
}
require.NoError(t, db.Create(&resource).Error)
+ tokenServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ require.Equal(t, http.MethodGet, r.Method)
+ require.Equal(t, "true", r.Header.Get("Metadata"))
+ require.Equal(t, "2018-02-01", r.URL.Query().Get("api-version"))
+ require.Equal(t, "https://vault.azure.net", r.URL.Query().Get("resource"))
+ _, _ = w.Write([]byte(`{"access_token":"manager-token","expires_in":"3600"}`))
+ }))
+ defer tokenServer.Close()
+
var writtenPath string
- var writtenBody map[string]map[string]any
- server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- require.Equal(t, http.MethodPost, r.Method)
- require.Equal(t, "/v1/secret/data/users/7/bindings/github-org-repo-main/resources/1", r.URL.Path)
- require.Equal(t, "manager-token", r.Header.Get("X-Vault-Token"))
+ var writtenBody map[string]any
+ vaultServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ require.Equal(t, http.MethodPut, r.Method)
+ require.Equal(t, "Bearer manager-token", r.Header.Get("Authorization"))
+ require.Equal(t, "/secrets/users-7-bindings-github-org-repo-main-resources-1", r.URL.Path)
+ require.Equal(t, "7.4", r.URL.Query().Get("api-version"))
require.NoError(t, common.DecodeJson(r.Body, &writtenBody))
writtenPath = r.URL.Path
w.WriteHeader(http.StatusOK)
- _, _ = w.Write([]byte(`{"data":{"version":1}}`))
+ _, _ = w.Write([]byte(`{"id":"https://heicode-kv.vault.azure.net/secrets/users-7-bindings-github-org-repo-main-resources-1/version"}`))
}))
- defer server.Close()
- t.Setenv("OPENBAO_ADDR", server.URL)
- t.Setenv("OPENBAO_TOKEN", "manager-token")
- t.Setenv("OPENBAO_KV_MOUNT", "secret")
+ defer vaultServer.Close()
+ t.Setenv("AZURE_KEY_VAULT_URL", vaultServer.URL)
+ t.Setenv("AZURE_MANAGED_IDENTITY_TOKEN_URL", tokenServer.URL)
body := `{"data":{"access_token":"do-not-echo","username":"octo"}}`
w := performResourceRequestWithRoute(UpsertResourceSecret, 7, http.MethodPost, "/:id/secret", "/1/secret", body)
require.Equal(t, http.StatusOK, w.Code)
require.Contains(t, w.Body.String(), `"success":true`)
- require.Contains(t, w.Body.String(), `"secret_ref":"vault://secret/users/7/bindings/github-org-repo-main/resources/1"`)
+ require.Contains(t, w.Body.String(), `"secret_ref":"azkv://127.0.0.1`)
+ require.Contains(t, w.Body.String(), `/secrets/users-7-bindings-github-org-repo-main-resources-1"`)
require.NotContains(t, w.Body.String(), "do-not-echo")
- require.Equal(t, "/v1/secret/data/users/7/bindings/github-org-repo-main/resources/1", writtenPath)
- require.Equal(t, "do-not-echo", writtenBody["data"]["access_token"])
+ require.Equal(t, "/secrets/users-7-bindings-github-org-repo-main-resources-1", writtenPath)
+ require.Equal(t, "application/json", writtenBody["contentType"])
+ require.JSONEq(t, `{"access_token":"do-not-echo","username":"octo"}`, writtenBody["value"].(string))
var stored model.ResourceBinding
require.NoError(t, db.First(&stored, resource.Id).Error)
- require.Equal(t, "vault://secret/users/7/bindings/github-org-repo-main/resources/1", stored.SecretRef)
+ require.Contains(t, stored.SecretRef, "azkv://127.0.0.1")
+ require.Contains(t, stored.SecretRef, "/secrets/users-7-bindings-github-org-repo-main-resources-1")
require.NotContains(t, stored.Metadata, "do-not-echo")
}
-func TestUpsertResourceSecretRequiresSecretStoreToken(t *testing.T) {
+func TestUpsertResourceSecretRequiresAzureKeyVaultURL(t *testing.T) {
db := setupResourceControllerTestDB(t)
resource := model.ResourceBinding{
UserId: 7,
@@ -272,14 +285,151 @@ func TestUpsertResourceSecretRequiresSecretStoreToken(t *testing.T) {
Status: "active",
}
require.NoError(t, db.Create(&resource).Error)
- t.Setenv("OPENBAO_ADDR", "http://127.0.0.1:8200")
- t.Setenv("OPENBAO_TOKEN", "")
- t.Setenv("VAULT_TOKEN", "")
- t.Setenv("OPENBAO_TOKEN_FILE", "")
- t.Setenv("VAULT_TOKEN_FILE", "")
+ t.Setenv("AZURE_KEY_VAULT_URL", "")
w := performResourceRequestWithRoute(UpsertResourceSecret, 7, http.MethodPost, "/:id/secret", "/1/secret", `{"data":{"token":"x"}}`)
require.Equal(t, http.StatusOK, w.Code)
require.Contains(t, w.Body.String(), `"success":false`)
- require.Contains(t, w.Body.String(), "secret store token is not configured")
+ require.Contains(t, w.Body.String(), "AZURE_KEY_VAULT_URL is not configured")
+}
+
+func TestDiscoverAzureResourcesReadsKeyVaultSecretAndStoresCloudResources(t *testing.T) {
+ db := setupResourceControllerTestDB(t)
+
+ tokenServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ require.Equal(t, http.MethodGet, r.Method)
+ require.Equal(t, "https://vault.azure.net", r.URL.Query().Get("resource"))
+ _, _ = w.Write([]byte(`{"access_token":"kv-token"}`))
+ }))
+ defer tokenServer.Close()
+
+ vaultServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ require.Equal(t, http.MethodGet, r.Method)
+ require.Equal(t, "Bearer kv-token", r.Header.Get("Authorization"))
+ require.Equal(t, "/secrets/azure-sub-1", r.URL.Path)
+ require.Equal(t, "7.4", r.URL.Query().Get("api-version"))
+ _, _ = w.Write([]byte(`{"value":"{\"client_secret\":\"sp-secret\"}"}`))
+ }))
+ defer vaultServer.Close()
+ vaultURL, err := url.Parse(vaultServer.URL)
+ require.NoError(t, err)
+
+ authorityServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ require.Equal(t, http.MethodPost, r.Method)
+ require.Equal(t, "/tenant-a/oauth2/v2.0/token", r.URL.Path)
+ require.NoError(t, r.ParseForm())
+ require.Equal(t, "client_credentials", r.Form.Get("grant_type"))
+ require.Equal(t, "client-a", r.Form.Get("client_id"))
+ require.Equal(t, "sp-secret", r.Form.Get("client_secret"))
+ require.Equal(t, "https://management.azure.com/.default", r.Form.Get("scope"))
+ _, _ = w.Write([]byte(`{"access_token":"arm-token"}`))
+ }))
+ defer authorityServer.Close()
+
+ armServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ require.Equal(t, http.MethodGet, r.Method)
+ require.Equal(t, "Bearer arm-token", r.Header.Get("Authorization"))
+ require.Equal(t, "/subscriptions/sub-a/resources", r.URL.Path)
+ require.Equal(t, "2021-04-01", r.URL.Query().Get("api-version"))
+ _, _ = w.Write([]byte(`{
+ "value": [
+ {
+ "id": "/subscriptions/sub-a/resourceGroups/rg-app/providers/Microsoft.Compute/virtualMachines/vm-api-01",
+ "name": "vm-api-01",
+ "type": "Microsoft.Compute/virtualMachines",
+ "location": "eastus",
+ "tags": {"env": "test"}
+ }
+ ]
+ }`))
+ }))
+ defer armServer.Close()
+
+ t.Setenv("AZURE_KEY_VAULT_URL", vaultServer.URL)
+ t.Setenv("AZURE_MANAGED_IDENTITY_TOKEN_URL", tokenServer.URL)
+ t.Setenv("AZURE_AUTHORITY_HOST", authorityServer.URL)
+ t.Setenv("AZURE_ARM_BASE_URL", armServer.URL)
+
+ account := model.ResourceBinding{
+ UserId: 7,
+ BindingScope: "azure:sub-a",
+ Name: "Test Azure",
+ ResourceType: "cloud_account",
+ Provider: "azure",
+ ExternalId: "sub-a",
+ SecretRef: fmt.Sprintf("azkv://%s/secrets/azure-sub-1", vaultURL.Host),
+ Metadata: `{"subscription_id":"sub-a","tenant_id":"tenant-a","client_id":"client-a"}`,
+ Status: "active",
+ }
+ require.NoError(t, db.Create(&account).Error)
+
+ w := performResourceRequestWithRoute(
+ DiscoverAzureResources,
+ 7,
+ http.MethodPost,
+ "/:id/azure/discover",
+ "/1/azure/discover",
+ "",
+ )
+ require.Equal(t, http.StatusOK, w.Code)
+ require.Contains(t, w.Body.String(), `"success":true`)
+ require.Contains(t, w.Body.String(), `"resource_group":"rg-app"`)
+ require.Contains(t, w.Body.String(), `"type":"Microsoft.Compute/virtualMachines"`)
+ require.NotContains(t, w.Body.String(), "sp-secret")
+
+ var resources []model.ResourceBinding
+ require.NoError(t, db.Where("user_id = ? AND resource_type = ?", 7, "cloud_resource").Find(&resources).Error)
+ require.Len(t, resources, 1)
+ require.Equal(t, "vm-api-01", resources[0].Name)
+ require.Equal(t, "azure", resources[0].Provider)
+ require.Equal(t, "active", resources[0].Status)
+ require.Equal(t, account.SecretRef, resources[0].SecretRef)
+ require.Contains(t, resources[0].ExternalId, "Microsoft.Compute/virtualMachines/vm-api-01")
+ require.Contains(t, resources[0].Metadata, `"resource_group":"rg-app"`)
+ require.NotContains(t, resources[0].Metadata, "sp-secret")
+}
+
+func TestResourceSecretNameFitsAzureKeyVaultRules(t *testing.T) {
+ name := resourceSecretName(model.ResourceBinding{
+ Id: 99,
+ UserId: 7,
+ BindingScope: strings.Repeat("repo/with spaces/", 20),
+ Name: "fallback",
+ })
+
+ require.LessOrEqual(t, len(name), azureKeyVaultSecretNameMaxLen)
+ require.Regexp(t, `^[A-Za-z0-9-]+$`, name)
+ require.Contains(t, name, "users-7-bindings-")
+ require.Contains(t, name, "-resources-99")
+}
+
+func TestGetSecretStoreStatusReportsAzureKeyVaultReachable(t *testing.T) {
+ tokenServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ require.Equal(t, "true", r.Header.Get("Metadata"))
+ _, _ = w.Write([]byte(`{"access_token":"manager-token"}`))
+ }))
+ defer tokenServer.Close()
+
+ vaultServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ require.Equal(t, http.MethodGet, r.Method)
+ require.Equal(t, "/secrets", r.URL.Path)
+ require.Equal(t, "Bearer manager-token", r.Header.Get("Authorization"))
+ _, _ = w.Write([]byte(`{"value":[]}`))
+ }))
+ defer vaultServer.Close()
+
+ t.Setenv("AZURE_KEY_VAULT_URL", vaultServer.URL)
+ t.Setenv("AZURE_MANAGED_IDENTITY_TOKEN_URL", tokenServer.URL)
+
+ r := gin.New()
+ r.GET("/status", GetSecretStoreStatus)
+ w := httptest.NewRecorder()
+ r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/status", nil))
+
+ require.Equal(t, http.StatusOK, w.Code)
+ require.Contains(t, w.Body.String(), `"success":true`)
+ require.Contains(t, w.Body.String(), `"configured":true`)
+ require.Contains(t, w.Body.String(), `"reachable":true`)
+ require.Contains(t, w.Body.String(), `"provider":"azure_key_vault"`)
+ require.Contains(t, w.Body.String(), "Azure Key Vault is reachable")
}
diff --git a/heicode/controller/secret_store.go b/heicode/controller/secret_store.go
index 726ff9b0..d479ce3f 100644
--- a/heicode/controller/secret_store.go
+++ b/heicode/controller/secret_store.go
@@ -6,6 +6,7 @@ import (
"fmt"
"io"
"net/http"
+ "net/url"
"os"
"strings"
"time"
@@ -16,73 +17,67 @@ import (
)
type secretStoreClient struct {
- address string
- mount string
- token string
- client *http.Client
+ vaultURL string
+ vaultHost string
+ tokenEndpoint string
+ clientID string
+ client *http.Client
}
func newSecretStoreClientFromEnv() (secretStoreClient, error) {
- address := strings.TrimRight(strings.TrimSpace(os.Getenv("OPENBAO_ADDR")), "/")
- if address == "" {
- address = strings.TrimRight(strings.TrimSpace(os.Getenv("VAULT_ADDR")), "/")
+ vaultURL := strings.TrimRight(strings.TrimSpace(os.Getenv("AZURE_KEY_VAULT_URL")), "/")
+ if vaultURL == "" {
+ return secretStoreClient{}, errors.New("AZURE_KEY_VAULT_URL is not configured")
}
- if address == "" {
- address = "http://127.0.0.1:8200"
+ parsed, err := url.Parse(vaultURL)
+ if err != nil || parsed.Scheme == "" || parsed.Host == "" {
+ return secretStoreClient{}, errors.New("AZURE_KEY_VAULT_URL must be an absolute URL")
}
- token := strings.TrimSpace(os.Getenv("OPENBAO_TOKEN"))
- if token == "" {
- token = strings.TrimSpace(os.Getenv("VAULT_TOKEN"))
+ if parsed.Scheme != "https" && !strings.HasPrefix(parsed.Host, "127.0.0.1") && !strings.HasPrefix(parsed.Host, "localhost") {
+ return secretStoreClient{}, errors.New("AZURE_KEY_VAULT_URL must use https")
}
- if token == "" {
- tokenFile := strings.TrimSpace(os.Getenv("OPENBAO_TOKEN_FILE"))
- if tokenFile == "" {
- tokenFile = strings.TrimSpace(os.Getenv("VAULT_TOKEN_FILE"))
- }
- if tokenFile != "" {
- data, err := os.ReadFile(tokenFile)
- if err != nil {
- return secretStoreClient{}, fmt.Errorf("failed to read secret store token file: %w", err)
- }
- token = strings.TrimSpace(string(data))
- }
- }
- if token == "" {
- return secretStoreClient{}, errors.New("secret store token is not configured")
- }
- mount := strings.Trim(strings.TrimSpace(os.Getenv("OPENBAO_KV_MOUNT")), "/")
- if mount == "" {
- mount = strings.Trim(strings.TrimSpace(os.Getenv("VAULT_KV_MOUNT")), "/")
- }
- if mount == "" {
- mount = "secret"
+ tokenEndpoint := strings.TrimSpace(os.Getenv("AZURE_MANAGED_IDENTITY_TOKEN_URL"))
+ if tokenEndpoint == "" {
+ tokenEndpoint = "http://169.254.169.254/metadata/identity/oauth2/token"
}
return secretStoreClient{
- address: address,
- mount: mount,
- token: token,
- client: &http.Client{Timeout: 10 * time.Second},
+ vaultURL: vaultURL,
+ vaultHost: parsed.Host,
+ tokenEndpoint: tokenEndpoint,
+ clientID: strings.TrimSpace(os.Getenv("AZURE_CLIENT_ID")),
+ client: &http.Client{Timeout: 10 * time.Second},
}, nil
}
-func (s secretStoreClient) putKV2(path string, data map[string]any) error {
+func (s secretStoreClient) putSecret(name string, data map[string]any) (string, error) {
if len(data) == 0 {
- return errors.New("secret data required")
+ return "", errors.New("secret data required")
}
- body, err := common.Marshal(map[string]any{"data": data})
+ value, err := common.Marshal(data)
if err != nil {
- return err
+ return "", err
}
- url := fmt.Sprintf("%s/v1/%s/data/%s", s.address, s.mount, strings.Trim(path, "/"))
- req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body))
+ body, err := common.Marshal(map[string]any{
+ "value": string(value),
+ "contentType": "application/json",
+ })
if err != nil {
- return err
+ return "", err
+ }
+ token, err := s.accessToken()
+ if err != nil {
+ return "", err
+ }
+ endpoint := fmt.Sprintf("%s/secrets/%s?api-version=7.4", s.vaultURL, url.PathEscape(name))
+ req, err := http.NewRequest(http.MethodPut, endpoint, bytes.NewReader(body))
+ if err != nil {
+ return "", err
}
req.Header.Set("Content-Type", "application/json")
- req.Header.Set("X-Vault-Token", s.token)
+ req.Header.Set("Authorization", "Bearer "+token)
resp, err := s.client.Do(req)
if err != nil {
- return err
+ return "", err
}
defer resp.Body.Close()
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
@@ -90,121 +85,206 @@ func (s secretStoreClient) putKV2(path string, data map[string]any) error {
if message == "" {
message = resp.Status
}
- return fmt.Errorf("secret store write failed: %s", message)
+ return "", fmt.Errorf("Azure Key Vault secret write failed: %s", message)
}
- return nil
+ return s.secretRef(name), nil
+}
+
+func (s secretStoreClient) secretRef(name string) string {
+ return fmt.Sprintf("azkv://%s/secrets/%s", s.vaultHost, name)
+}
+
+func (s secretStoreClient) getJSONSecret(secretRef string) (map[string]any, error) {
+ name, err := s.secretNameFromRef(secretRef)
+ if err != nil {
+ return nil, err
+ }
+ token, err := s.accessToken()
+ if err != nil {
+ return nil, err
+ }
+ endpoint := fmt.Sprintf("%s/secrets/%s?api-version=7.4", s.vaultURL, url.PathEscape(name))
+ req, err := http.NewRequest(http.MethodGet, endpoint, nil)
+ if err != nil {
+ return nil, err
+ }
+ req.Header.Set("Authorization", "Bearer "+token)
+ resp, err := s.client.Do(req)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
+ message := readSecretStoreError(resp.Body)
+ if message == "" {
+ message = resp.Status
+ }
+ return nil, fmt.Errorf("Azure Key Vault secret read failed: %s", message)
+ }
+ var payload struct {
+ Value string `json:"value"`
+ }
+ if err := common.DecodeJson(resp.Body, &payload); err != nil {
+ return nil, err
+ }
+ if strings.TrimSpace(payload.Value) == "" {
+ return nil, errors.New("Azure Key Vault secret value is empty")
+ }
+ var data map[string]any
+ if err := common.UnmarshalJsonStr(payload.Value, &data); err != nil {
+ return nil, errors.New("Azure Key Vault secret value must be JSON")
+ }
+ if len(data) == 0 {
+ return nil, errors.New("Azure Key Vault secret value is empty")
+ }
+ return data, nil
+}
+
+func (s secretStoreClient) secretNameFromRef(secretRef string) (string, error) {
+ parsed, err := url.Parse(strings.TrimSpace(secretRef))
+ if err != nil || parsed.Scheme != "azkv" || parsed.Host == "" {
+ return "", errors.New("secret_ref must use azkv:///secrets/")
+ }
+ if !strings.EqualFold(parsed.Host, s.vaultHost) {
+ return "", errors.New("secret_ref vault does not match configured Azure Key Vault")
+ }
+ parts := strings.Split(strings.Trim(parsed.Path, "/"), "/")
+ if len(parts) < 2 || parts[0] != "secrets" || strings.TrimSpace(parts[1]) == "" {
+ return "", errors.New("secret_ref must use azkv:///secrets/")
+ }
+ return parts[1], nil
+}
+
+func (s secretStoreClient) accessToken() (string, error) {
+ u, err := url.Parse(s.tokenEndpoint)
+ if err != nil {
+ return "", fmt.Errorf("invalid Azure managed identity token endpoint: %w", err)
+ }
+ q := u.Query()
+ if q.Get("api-version") == "" {
+ q.Set("api-version", "2018-02-01")
+ }
+ if q.Get("resource") == "" {
+ q.Set("resource", "https://vault.azure.net")
+ }
+ if s.clientID != "" && q.Get("client_id") == "" {
+ q.Set("client_id", s.clientID)
+ }
+ u.RawQuery = q.Encode()
+
+ req, err := http.NewRequest(http.MethodGet, u.String(), nil)
+ if err != nil {
+ return "", err
+ }
+ req.Header.Set("Metadata", "true")
+ resp, err := s.client.Do(req)
+ if err != nil {
+ return "", fmt.Errorf("Azure managed identity token request failed: %w", err)
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
+ message := readSecretStoreError(resp.Body)
+ if message == "" {
+ message = resp.Status
+ }
+ return "", fmt.Errorf("Azure managed identity token request failed: %s", message)
+ }
+ var payload struct {
+ AccessToken string `json:"access_token"`
+ }
+ if err := common.DecodeJson(resp.Body, &payload); err != nil {
+ return "", err
+ }
+ if strings.TrimSpace(payload.AccessToken) == "" {
+ return "", errors.New("Azure managed identity token response missing access_token")
+ }
+ return payload.AccessToken, nil
}
func readSecretStoreError(body io.Reader) string {
var payload struct {
Errors []string `json:"errors"`
+ Error struct {
+ Message string `json:"message"`
+ Code string `json:"code"`
+ } `json:"error"`
}
if err := common.DecodeJson(body, &payload); err != nil {
return ""
}
+ if payload.Error.Message != "" {
+ if payload.Error.Code != "" {
+ return payload.Error.Code + ": " + payload.Error.Message
+ }
+ return payload.Error.Message
+ }
return strings.Join(payload.Errors, "; ")
}
-// secretStoreHealth is what `GET /v1/sys/health` returns on the
-// OpenBao/Vault side. We only pull the public-safe fields — version
-// string, initialized/sealed/standby flags, server time. No cluster
-// IDs, no auth lease ttls, nothing that could leak operator detail.
-type secretStoreHealth struct {
- Initialized bool `json:"initialized"`
- Sealed bool `json:"sealed"`
- Standby bool `json:"standby"`
- Version string `json:"version"`
- ServerTime int64 `json:"server_time_utc"`
-}
-
-// fetchHealth pings the OpenBao /sys/health endpoint. That endpoint
-// is intentionally token-less in upstream Vault — it's the standard
-// liveness probe — so we don't include X-Vault-Token here. Vault
-// returns non-2xx HTTP codes for sealed/uninitialized states by
-// design (200=initialized+unsealed, 429=standby, 472=DR secondary,
-// 473=performance standby, 501=not initialized, 503=sealed). All of
-// those still ship a JSON body with the same fields, so we always
-// decode and let the caller interpret the status field separately.
-func (s secretStoreClient) fetchHealth() (secretStoreHealth, int, error) {
- url := fmt.Sprintf("%s/v1/sys/health", s.address)
- req, err := http.NewRequest(http.MethodGet, url, nil)
+func (s secretStoreClient) probe() (int, error) {
+ token, err := s.accessToken()
if err != nil {
- return secretStoreHealth{}, 0, err
+ return 0, err
}
+ endpoint := s.vaultURL + "/secrets?api-version=7.4&maxresults=1"
+ req, err := http.NewRequest(http.MethodGet, endpoint, nil)
+ if err != nil {
+ return 0, err
+ }
+ req.Header.Set("Authorization", "Bearer "+token)
resp, err := s.client.Do(req)
if err != nil {
- return secretStoreHealth{}, 0, err
+ return 0, err
}
defer resp.Body.Close()
- var h secretStoreHealth
- if err := common.DecodeJson(resp.Body, &h); err != nil {
- return secretStoreHealth{}, resp.StatusCode, err
- }
- return h, resp.StatusCode, nil
+ _, _ = io.Copy(io.Discard, resp.Body)
+ return resp.StatusCode, nil
}
// secretStoreStatusResponse is the public-safe envelope rendered by
-// the admin status page. Crucially does NOT include the configured
-// address (could leak internal network topology), the token, the
-// mount path, or any secret names.
+// the admin status page. It never includes secret names or values.
type secretStoreStatusResponse struct {
- Configured bool `json:"configured"` // env vars present?
- Reachable bool `json:"reachable"` // /sys/health responded?
- Initialized bool `json:"initialized,omitempty"`
- Sealed bool `json:"sealed,omitempty"`
- Standby bool `json:"standby,omitempty"`
- Version string `json:"version,omitempty"`
- Message string `json:"message,omitempty"` // human-readable status
- CheckedAt int64 `json:"checked_at"` // unix ms (server clock)
+ Configured bool `json:"configured"` // env vars present?
+ Reachable bool `json:"reachable"` // Key Vault data plane responded?
+ Provider string `json:"provider,omitempty"`
+ AuthMethod string `json:"auth_method,omitempty"`
+ Message string `json:"message,omitempty"` // human-readable status
+ CheckedAt int64 `json:"checked_at"` // unix ms (server clock)
}
-// GetSecretStoreStatus returns a sanitized vault health snapshot for
-// the admin "密钥保管器" status panel. Admin-only (route is mounted
-// under the rootRoute group in api-router.go so non-admins can't even
-// reach it). The handler is deliberately tolerant: missing env vars,
-// network errors, and sealed-vault responses ALL render usable JSON
-// the UI can present — we don't bubble HTTP 500 for any of them, the
-// "missing config" / "network error" / "sealed" states are valid
-// operating modes the UI must visualise distinctly.
+// GetSecretStoreStatus returns a sanitized Azure Key Vault status snapshot.
func GetSecretStoreStatus(c *gin.Context) {
out := secretStoreStatusResponse{
- CheckedAt: time.Now().UnixMilli(),
+ CheckedAt: time.Now().UnixMilli(),
+ Provider: "azure_key_vault",
+ AuthMethod: "managed_identity",
}
client, err := newSecretStoreClientFromEnv()
if err != nil {
- // Most common case in current production: OPENBAO_TOKEN not
- // set. Render as "not configured" instead of a server error.
out.Configured = false
- out.Message = "secret store env vars not set (OPENBAO_ADDR / OPENBAO_TOKEN). The vault container may still be running but Manager is not wired up to it yet."
+ out.Message = err.Error()
c.JSON(http.StatusOK, gin.H{"success": true, "data": out})
return
}
out.Configured = true
- health, status, err := client.fetchHealth()
+ status, err := client.probe()
if err != nil {
out.Reachable = false
- out.Message = "network error contacting secret store: " + err.Error()
+ out.Message = "Azure Key Vault probe failed: " + err.Error()
c.JSON(http.StatusOK, gin.H{"success": true, "data": out})
return
}
out.Reachable = true
- out.Initialized = health.Initialized
- out.Sealed = health.Sealed
- out.Standby = health.Standby
- out.Version = health.Version
- // Vault uses non-2xx HTTP for sealed/uninit states — surface a
- // short reason so the UI can show a coloured pill without parsing
- // the boolean matrix itself.
switch {
- case !health.Initialized:
- out.Message = fmt.Sprintf("secret store reachable but not initialized (HTTP %d)", status)
- case health.Sealed:
- out.Message = fmt.Sprintf("secret store reachable but SEALED — operator must unseal (HTTP %d)", status)
- case health.Standby:
- out.Message = fmt.Sprintf("secret store in standby mode (HTTP %d)", status)
+ case status == http.StatusForbidden:
+ out.Message = "Azure Key Vault reachable, but managed identity lacks list permission; secret writes can still work if set permission is granted"
+ case status == http.StatusUnauthorized:
+ out.Message = "Azure Key Vault reachable, but managed identity was not authorized"
+ case status >= http.StatusOK && status < http.StatusMultipleChoices:
+ out.Message = "Azure Key Vault is reachable"
default:
- out.Message = "secret store is healthy"
+ out.Message = fmt.Sprintf("Azure Key Vault returned HTTP %d", status)
}
c.JSON(http.StatusOK, gin.H{"success": true, "data": out})
}
diff --git a/heicode/docker-compose.azure-vm.yml b/heicode/docker-compose.azure-vm.yml
index a4ca882f..dea2bf9c 100644
--- a/heicode/docker-compose.azure-vm.yml
+++ b/heicode/docker-compose.azure-vm.yml
@@ -11,6 +11,7 @@
# SQL_DSN=postgresql://...@heicode.postgres.database.azure.com:5432/heicode?sslmode=require
# REDIS_CONN_STRING=rediss://:...@heicode.redis.cache.windows.net:6380
# SESSION_SECRET=...
+# AZURE_KEY_VAULT_URL=https://.vault.azure.net
# (以及项目要求的其它密钥,见官方 heicode 文档)
#
# 3. 构建 Heicode 定制镜像时与本地 override 一致:
@@ -47,6 +48,10 @@ services:
# Agnet 登录后 JIT 同步:邮箱命中以下白名单则自动提权
- HEICODE_ROOT_EMAILS=${HEICODE_ROOT_EMAILS:-}
- HEICODE_ADMIN_EMAILS=${HEICODE_ADMIN_EMAILS:-}
+ # Long-lived resource credentials are written to Azure Key Vault via
+ # the VM managed identity. No static secret-store token is required.
+ - AZURE_KEY_VAULT_URL=${AZURE_KEY_VAULT_URL:-}
+ - AZURE_CLIENT_ID=${AZURE_CLIENT_ID:-}
networks:
- heicode-network
healthcheck:
diff --git a/heicode/relay/helper/stream_scanner.go b/heicode/relay/helper/stream_scanner.go
index 0daf11ef..7b7c4ac2 100644
--- a/heicode/relay/helper/stream_scanner.go
+++ b/heicode/relay/helper/stream_scanner.go
@@ -23,6 +23,7 @@ import (
const (
InitialScannerBufferSize = 64 << 10 // 64KB (64*1024)
DefaultMaxScannerBufferSize = 64 << 20 // 64MB (64*1024*1024) default SSE buffer size
+ DefaultStreamingTimeout = 30 * time.Second
DefaultPingInterval = 15 * time.Second
)
@@ -51,6 +52,9 @@ func StreamScannerHandler(c *gin.Context, resp *http.Response, info *relaycommon
}()
streamingTimeout := time.Duration(constant.StreamingTimeout) * time.Second
+ if streamingTimeout <= 0 {
+ streamingTimeout = DefaultStreamingTimeout
+ }
var (
stopChan = make(chan bool, 3) // 增加缓冲区避免阻塞
diff --git a/heicode/router/api-router.go b/heicode/router/api-router.go
index 00132a95..d3c7fe09 100644
--- a/heicode/router/api-router.go
+++ b/heicode/router/api-router.go
@@ -29,10 +29,11 @@ func SetApiRouter(router *gin.Engine) {
apiRouter.GET("/uptime/status", controller.GetUptimeKumaStatus)
apiRouter.GET("/models", middleware.UserAuth(), controller.DashboardListModels)
apiRouter.GET("/status/test", middleware.AdminAuth(), controller.TestStatus)
- // M7 — secret-store (OpenBao / Vault) status snapshot for the
- // admin "密钥保管器" panel. Admin-only because the response
- // reveals seal state + version which we don't want public.
- apiRouter.GET("/secret-store/status", middleware.AdminAuth(), controller.GetSecretStoreStatus)
+ // Secret-store status snapshot. The handler returns only
+ // sanitized Azure Key Vault config/reachability flags and never
+ // exposes addresses, tokens, secret names or secret values, so
+ // resource binding can read it as an authenticated user.
+ apiRouter.GET("/secret-store/status", middleware.UserAuth(), controller.GetSecretStoreStatus)
apiRouter.GET("/notice", controller.GetNotice)
apiRouter.GET("/user-agreement", controller.GetUserAgreement)
apiRouter.GET("/privacy-policy", controller.GetPrivacyPolicy)
@@ -215,6 +216,7 @@ func SetApiRouter(router *gin.Engine) {
resourceRoute.GET("/", controller.ListResources)
resourceRoute.POST("/", controller.CreateResource)
resourceRoute.POST("/:id/secret", controller.UpsertResourceSecret)
+ resourceRoute.POST("/:id/azure/discover", controller.DiscoverAzureResources)
resourceRoute.PUT("/:id", controller.UpdateResource)
resourceRoute.DELETE("/:id", controller.DeleteResource)
}
diff --git a/heicode/web/classic/dist/index.html b/heicode/web/classic/dist/index.html
new file mode 100644
index 00000000..ae76d08f
--- /dev/null
+++ b/heicode/web/classic/dist/index.html
@@ -0,0 +1,10 @@
+
+
+
+
+ Heicode Manager classic placeholder
+
+
+
+
+
diff --git a/heicode/web/default/.gitignore b/heicode/web/default/.gitignore
index a613398c..2ecb96d5 100644
--- a/heicode/web/default/.gitignore
+++ b/heicode/web/default/.gitignore
@@ -9,6 +9,9 @@ lerna-debug.log*
node_modules
dist
+!dist/
+dist/*
+!dist/index.html
dist-ssr
*.local
@@ -24,4 +27,4 @@ dist-ssr
*.njsproj
*.sln
*.sw?
-.tanstack/*
\ No newline at end of file
+.tanstack/*
diff --git a/heicode/web/default/bun.lock b/heicode/web/default/bun.lock
index 56e0a3fd..56334fa9 100644
--- a/heicode/web/default/bun.lock
+++ b/heicode/web/default/bun.lock
@@ -8,6 +8,7 @@
"@fontsource-variable/public-sans": "^5.2.7",
"@hookform/resolvers": "^5.2.2",
"@lobehub/icons": "^4.0.3",
+ "@lobehub/ui": "^5.14.0",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-avatar": "^1.1.11",
@@ -30,6 +31,7 @@
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8",
"@radix-ui/react-use-controllable-state": "^1.2.2",
+ "@rspack/binding-win32-x64-msvc": "^2.0.0",
"@tailwindcss/postcss": "^4.2.2",
"@tanstack/react-query": "^5.95.2",
"@tanstack/react-router": "^1.168.23",
@@ -38,6 +40,7 @@
"@visactor/react-vchart": "^2.0.13",
"@visactor/vchart": "^2.0.13",
"ai": "^6.0.27",
+ "antd": "^6.4.3",
"auto-skeleton-react": "^1.0.5",
"axios": "^1.13.6",
"class-variance-authority": "^0.7.1",
@@ -83,6 +86,7 @@
"@tanstack/react-router-devtools": "^1.166.13",
"@tanstack/router-plugin": "^1.167.23",
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
+ "@types/hast": "^3.0.4",
"@types/node": "^25.5.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
@@ -118,7 +122,7 @@
"@ant-design/fast-color": ["@ant-design/fast-color@3.0.1", "", {}, "sha512-esKJegpW4nckh0o6kV3Tkb7NPIZYbPnnFxmQDUmL08ukXZAvV85TZBr70eGuke/CIArLaP6aw8lt9KILjnWuOw=="],
- "@ant-design/icons": ["@ant-design/icons@6.1.1", "", { "dependencies": { "@ant-design/colors": "^8.0.0", "@ant-design/icons-svg": "^4.4.0", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-AMT4N2y++TZETNHiM77fs4a0uPVCJGuL5MTonk13Pvv7UN7sID1cNEZOc1qNqx6zLKAOilTEFAdAoAFKa0U//Q=="],
+ "@ant-design/icons": ["@ant-design/icons@6.2.3", "", { "dependencies": { "@ant-design/colors": "^8.0.1", "@ant-design/icons-svg": "^4.4.2", "@rc-component/util": "^1.10.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-Pl3aoAtxQeKryYnt6VvDJtOxMOtA8wrRSACe/pTjOAIG3fdHrWm6Ivb4ku9tsFjYroSXBKirvuxG4QkwBXD9gg=="],
"@ant-design/icons-svg": ["@ant-design/icons-svg@4.4.2", "", {}, "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA=="],
@@ -382,7 +386,7 @@
"@lobehub/icons": ["@lobehub/icons@4.12.0", "", { "dependencies": { "antd-style": "^4.1.0", "lucide-react": "^0.469.0", "polished": "^4.3.1" }, "peerDependencies": { "@lobehub/ui": "^4.3.3", "antd": "^6.1.1", "react": "^19.0.0", "react-dom": "^19.0.0" } }, "sha512-DVH7pVzM6wEvua2LXH+Iv10/cLeBbueggMFBHa8IlfQel5u3I6JzuaNXXxj2qJu5QYjUCNL5LTpSWuh4TnuLGw=="],
- "@lobehub/ui": ["@lobehub/ui@4.38.4", "", { "dependencies": { "@ant-design/cssinjs": "^2.0.3", "@base-ui/react": "1.0.0", "@dnd-kit/core": "^6.3.1", "@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", "@emoji-mart/data": "^1.2.1", "@emoji-mart/react": "^1.1.1", "@emotion/is-prop-valid": "^1.4.0", "@floating-ui/react": "^0.27.17", "@giscus/react": "^3.1.0", "@mdx-js/mdx": "^3.1.1", "@mdx-js/react": "^3.1.1", "@pierre/diffs": "^1.0.10", "@radix-ui/react-slot": "^1.2.4", "@shikijs/core": "^3.22.0", "@shikijs/transformers": "^3.22.0", "@splinetool/runtime": "0.9.526", "ahooks": "^3.9.6", "antd-style": "^4.1.0", "chroma-js": "^3.2.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "dayjs": "^1.11.19", "emoji-mart": "^5.6.0", "es-toolkit": "^1.44.0", "fast-deep-equal": "^3.1.3", "immer": "^11.1.3", "katex": "^0.16.28", "leva": "^0.10.1", "lucide-react": "^0.563.0", "marked": "^17.0.1", "mermaid": "^11.12.2", "motion": "^12.30.0", "numeral": "^2.0.6", "polished": "^4.3.1", "query-string": "^9.3.1", "rc-collapse": "^4.0.0", "rc-footer": "^0.6.8", "rc-image": "^7.12.0", "rc-input-number": "^9.5.0", "rc-menu": "^9.16.1", "re-resizable": "^6.11.2", "react-avatar-editor": "^14.0.0", "react-error-boundary": "^6.1.0", "react-hotkeys-hook": "^5.2.4", "react-markdown": "^10.1.0", "react-merge-refs": "^3.0.2", "react-rnd": "^10.5.2", "react-zoom-pan-pinch": "^3.7.0", "rehype-github-alerts": "^4.2.0", "rehype-katex": "^7.0.1", "rehype-raw": "^7.0.0", "remark-breaks": "^4.0.0", "remark-cjk-friendly": "^1.2.3", "remark-gfm": "^4.0.1", "remark-github": "^12.0.0", "remark-math": "^6.0.0", "remend": "^1.2.0", "shiki": "^3.22.0", "shiki-stream": "^0.1.4", "swr": "^2.4.0", "ts-md5": "^2.0.1", "unified": "^11.0.5", "url-join": "^5.0.0", "use-merge-value": "^1.2.0", "uuid": "^13.0.0", "virtua": "^0.48.5" }, "peerDependencies": { "@lobehub/fluent-emoji": "^4.0.0", "@lobehub/icons": "^4.0.0", "antd": "^6.1.1", "react": "^19.0.0", "react-dom": "^19.0.0" } }, "sha512-FYQeWkR0CoZCaPqEX9AUGrhaIfkYeuacW2KtV+1GS7eGVjREFNNOAgY5PLk20ZMYV/cRFsn9fNG0rqn9PxChxw=="],
+ "@lobehub/ui": ["@lobehub/ui@5.15.0", "", { "dependencies": { "@ant-design/cssinjs": "^2.1.2", "@base-ui/react": "1.0.0", "@dnd-kit/core": "^6.3.1", "@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", "@emoji-mart/data": "^1.2.1", "@emoji-mart/react": "^1.1.1", "@emotion/is-prop-valid": "^1.4.0", "@floating-ui/react": "^0.27.19", "@giscus/react": "^3.1.0", "@mdx-js/mdx": "^3.1.1", "@mdx-js/react": "^3.1.1", "@pierre/diffs": "^1.1.19", "@radix-ui/react-slot": "^1.2.4", "@shikijs/core": "^4.0.2", "@shikijs/transformers": "^4.0.2", "@splinetool/runtime": "0.9.526", "ahooks": "^3.9.7", "antd-style": "^4.1.0", "chroma-js": "^3.2.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "dayjs": "^1.11.20", "emoji-mart": "^5.6.0", "es-toolkit": "^1.46.0", "fast-deep-equal": "^3.1.3", "immer": "^11.1.4", "katex": "^0.16.45", "leva": "^0.10.1", "lucide-react": "^1.11.0", "marked": "^17.0.6", "mermaid": "^11.14.0", "motion": "^12.38.0", "numeral": "^2.0.6", "polished": "^4.3.1", "query-string": "^9.3.1", "rc-collapse": "^4.0.0", "rc-footer": "^0.6.8", "rc-image": "^7.12.0", "rc-input-number": "^9.5.0", "rc-menu": "^9.16.1", "re-resizable": "^6.11.2", "react-avatar-editor": "^15.1.0", "react-error-boundary": "^6.1.1", "react-hotkeys-hook": "^5.2.4", "react-markdown": "^10.1.0", "react-merge-refs": "^3.0.2", "react-rnd": "^10.5.3", "react-zoom-pan-pinch": "^3.7.0", "rehype-github-alerts": "^4.2.0", "rehype-katex": "^7.0.1", "rehype-raw": "^7.0.0", "remark-breaks": "^4.0.0", "remark-cjk-friendly": "^2.0.1", "remark-gfm": "^4.0.1", "remark-github": "^12.0.0", "remark-math": "^6.0.0", "remend": "^1.3.0", "shiki": "^4.0.2", "shiki-stream": "^0.1.4", "swr": "^2.4.1", "ts-md5": "^2.0.1", "unified": "^11.0.5", "url-join": "^5.0.0", "use-merge-value": "^1.2.0", "uuid": "^13.0.0", "virtua": "^0.49.1" }, "peerDependencies": { "@lobehub/fluent-emoji": "^4.0.0", "@lobehub/icons": "^5.0.0", "antd": "^6.1.1", "react": "^19.0.0", "react-dom": "^19.0.0" } }, "sha512-zh+xWZiOX/yEuV5Vxp7234bXCu7X5lXzLtrh95DMPkaU22lkLOWAjBT24OV5uroc0YjpmmJNfwDB0VQsAXnrAg=="],
"@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="],
@@ -498,9 +502,9 @@
"@oxc-resolver/binding-win32-x64-msvc": ["@oxc-resolver/binding-win32-x64-msvc@11.19.1", "", { "os": "win32", "cpu": "x64" }, "sha512-6hIU3RQu45B+VNTY4Ru8ppFwjVS/S5qwYyGhBotmjxfEKk41I2DlGtRfGJndZ5+6lneE2pwloqunlOyZuX/XAw=="],
- "@pierre/diffs": ["@pierre/diffs@1.1.13", "", { "dependencies": { "@pierre/theme": "0.0.28", "@shikijs/transformers": "^3.0.0", "diff": "8.0.3", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "^3.0.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-lnX9Fy5eC+07b8g+D8krC3txOY6LRN5VNR1qr9bph9XEyLxbwwfGN7SFRu4HGozpkDdA76JARgxgWHN+uAihmg=="],
+ "@pierre/diffs": ["@pierre/diffs@1.2.3", "", { "dependencies": { "@pierre/theme": "1.0.3", "@shikijs/transformers": "^3.0.0", "diff": "8.0.3", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "^3.0.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-ul83DHH1yqgGxJAw2tqQm2gDO+oQsaF82ZVocwJYfXAm2FhZyyKPTdtv6jswR4A5eF/ILPjiQxyfScMhQcofbA=="],
- "@pierre/theme": ["@pierre/theme@0.0.28", "", {}, "sha512-1j/H/fECBuc9dEvntdWI+l435HZapw+RCJTlqCA6BboQ5TjlnE005j/ROWutXIs8aq5OAc82JI2Kwk4A1WWBgw=="],
+ "@pierre/theme": ["@pierre/theme@1.0.3", "", {}, "sha512-sWHv11TMoqKxKDgTIk5VbhQjdPhs8DCcBxbjh3mRlS3YOM/OcrWoGX6MM8eBGn9cUu3M46Py0JnxsG2nJaFTuA=="],
"@primer/octicons": ["@primer/octicons@19.23.1", "", { "dependencies": { "object-assign": "^4.1.1" } }, "sha512-CzjGmxkmNhyst6EekrS3SJPdtzgIkUMP/LSJch65y99/kmiFXbO1a+q7zoYe3hnI9NaOM0IN+ydDIbOmd8YqcA=="],
@@ -602,7 +606,7 @@
"@rc-component/async-validator": ["@rc-component/async-validator@5.1.0", "", { "dependencies": { "@babel/runtime": "^7.24.4" } }, "sha512-n4HcR5siNUXRX23nDizbZBQPO0ZM/5oTtmKZ6/eqL0L2bo747cklFdZGRN2f+c9qWGICwDzrhW0H7tE9PptdcA=="],
- "@rc-component/cascader": ["@rc-component/cascader@1.14.0", "", { "dependencies": { "@rc-component/select": "~1.6.0", "@rc-component/tree": "~1.2.0", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-Ip9356xwZUR2nbW5PRVGif4B/bDve4pLa/N+PGbvBaTnjbvmN4PFMBGQSmlDlzKP1ovxaYMvwF/dI9lXNLT4iQ=="],
+ "@rc-component/cascader": ["@rc-component/cascader@1.15.0", "", { "dependencies": { "@rc-component/select": "~1.6.0", "@rc-component/tree": "~1.3.0", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-ZzpMtwFCRo3fbXHuDnncARJMZQjdqA2w7aDuPofNQt+aDx39st1hgfIpEwTBLhe2Hqsvs/zOr8RTtgxTkCPySw=="],
"@rc-component/checkbox": ["@rc-component/checkbox@2.0.0", "", { "dependencies": { "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-3CXGPpAR9gsPKeO2N78HAPOzU30UdemD6HGJoWVJOpa6WleaGB5kzZj3v6bdTZab31YuWgY/RxV3VKPctn0DwQ=="],
@@ -612,23 +616,23 @@
"@rc-component/context": ["@rc-component/context@2.0.1", "", { "dependencies": { "@rc-component/util": "^1.3.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-HyZbYm47s/YqtP6pKXNMjPEMaukyg7P0qVfgMLzr7YiFNMHbK2fKTAGzms9ykfGHSfyf75nBbgWw+hHkp+VImw=="],
- "@rc-component/dialog": ["@rc-component/dialog@1.8.4", "", { "dependencies": { "@rc-component/motion": "^1.1.3", "@rc-component/portal": "^2.1.0", "@rc-component/util": "^1.9.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-Ay6PM7phkTkquplG8fWfUGFZ2GTLx9diTl4f0d8Eqxd7W1u1KjE9AQooFQHOHnhZf0Ya3z51+5EKCWHmt/dNEw=="],
+ "@rc-component/dialog": ["@rc-component/dialog@1.9.0", "", { "dependencies": { "@rc-component/motion": "^1.1.3", "@rc-component/portal": "^2.1.0", "@rc-component/util": "^1.9.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-zbAAogkg4kkKum79sLE6M+vq1jSAW25zdkafrahgcTP9t9S//SD634Znd1A4c8F2Gc12ZKnehGLsVaaOvZzD2A=="],
"@rc-component/drawer": ["@rc-component/drawer@1.4.2", "", { "dependencies": { "@rc-component/motion": "^1.1.4", "@rc-component/portal": "^2.1.3", "@rc-component/util": "^1.9.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-1ib+fZEp6FBu+YvcIktm+nCQ+Q+qIpwpoaJH6opGr4ofh2QMq+qdr5DLC4oCf5qf3pcWX9lUWPYX652k4ini8Q=="],
"@rc-component/dropdown": ["@rc-component/dropdown@1.0.2", "", { "dependencies": { "@rc-component/trigger": "^3.0.0", "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.11.0", "react-dom": ">=16.11.0" } }, "sha512-6PY2ecUSYhDPhkNHHb4wfeAya04WhpmUSKzdR60G+kMNVUCX2vjT/AgTS0Lz0I/K6xrPMJ3enQbwVpeN3sHCgg=="],
- "@rc-component/form": ["@rc-component/form@1.8.0", "", { "dependencies": { "@rc-component/async-validator": "^5.1.0", "@rc-component/util": "^1.6.2", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-eUD5KKYnIZWmJwRA0vnyO/ovYUfHGU1svydY1OrqU5fw8Oz9Tdqvxvrlh0wl6xI/EW69dT7II49xpgOWzK3T5A=="],
+ "@rc-component/form": ["@rc-component/form@1.8.1", "", { "dependencies": { "@rc-component/async-validator": "^5.1.0", "@rc-component/util": "^1.6.2", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-8O7TB55Fi2mWIGvSnwZjk8jFqVNYyKDAswglwGShcbndxqzKz4cHwNtNaLjZlAeRge9wcB0LL8IWsC/Bl18raQ=="],
- "@rc-component/image": ["@rc-component/image@1.8.1", "", { "dependencies": { "@rc-component/motion": "^1.0.0", "@rc-component/portal": "^2.1.2", "@rc-component/util": "^1.10.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-JfPCijmMl+EaMvbftsEs/4VHmTyJKsZBh5ujFowSA45i9NTVYS1vuHtgpVV/QrGa27kXwbVOZriffCe/PNKuMw=="],
+ "@rc-component/image": ["@rc-component/image@1.9.0", "", { "dependencies": { "@rc-component/motion": "^1.0.0", "@rc-component/portal": "^2.1.2", "@rc-component/util": "^1.10.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-khF7w7xkBH5B1bsBcI1FSUZdkyd1aqpl2eYyILCqCzzQH3XdfehGUaZTnptyaJJfs09/R5hv9jXWyazOMFIClQ=="],
- "@rc-component/input": ["@rc-component/input@1.1.2", "", { "dependencies": { "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-Q61IMR47piUBudgixJ30CciKIy9b1H95qe7GgEKOmSJVJXvFRWJllJfQry9tif+MX2cWFXWJf/RXz4kaCeq/Fg=="],
+ "@rc-component/input": ["@rc-component/input@1.3.1", "", { "dependencies": { "@rc-component/resize-observer": "^1.1.1", "@rc-component/util": "^1.11.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-iFvTUT9W+JC/MSin2aGAk8NqsVlTzcExNC9DZariON1IWirju9NoNeEk47an4Q8iHazkoVI/y1LnDi88+CPcig=="],
"@rc-component/input-number": ["@rc-component/input-number@1.6.2", "", { "dependencies": { "@rc-component/mini-decimal": "^1.0.1", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-Gjcq7meZlCOiWN1t1xCC+7/s85humHVokTBI7PJgTfoyw5OWF74y3e6P8PHX104g9+b54jsodFIzyaj6p8LI9w=="],
- "@rc-component/mentions": ["@rc-component/mentions@1.6.0", "", { "dependencies": { "@rc-component/input": "~1.1.0", "@rc-component/menu": "~1.2.0", "@rc-component/textarea": "~1.1.0", "@rc-component/trigger": "^3.0.0", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-KIkQNP6habNuTsLhUv0UGEOwG67tlmE7KNIJoQZZNggEZl5lQJTytFDb69sl5CK3TDdISCTjKP3nGEBKgT61CQ=="],
+ "@rc-component/mentions": ["@rc-component/mentions@1.9.0", "", { "dependencies": { "@rc-component/input": "~1.3.0", "@rc-component/menu": "~1.3.0", "@rc-component/trigger": "^3.0.0", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-WUwfFKDSOF5S9UPsNsXcLYtzjTxBGsftTXWRbZuxX6BYrsySISTnujfJNgaaQ6qVzaCDJ35QUkZKvsYxip1C5g=="],
- "@rc-component/menu": ["@rc-component/menu@1.2.0", "", { "dependencies": { "@rc-component/motion": "^1.1.4", "@rc-component/overflow": "^1.0.0", "@rc-component/trigger": "^3.0.0", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-VWwDuhvYHSnTGj4n6bV3ISrLACcPAzdPOq3d0BzkeiM5cve8BEYfvkEhNoM0PLzv51jpcejeyrLXeMVIJ+QJlg=="],
+ "@rc-component/menu": ["@rc-component/menu@1.3.0", "", { "dependencies": { "@rc-component/motion": "^1.1.4", "@rc-component/overflow": "^1.0.0", "@rc-component/trigger": "^3.0.0", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-u3NfiwpiEgT177qa5Yxm5QsI8i/93EBGpWj8HYZQDnh2pCZ2xtQCe/+w3pSR2NlwKOZDTCKzEhEyD09mGphssA=="],
"@rc-component/mini-decimal": ["@rc-component/mini-decimal@1.1.3", "", { "dependencies": { "@babel/runtime": "^7.18.0" } }, "sha512-bk/FJ09fLf+NLODMAFll6CfYrHPBioTedhW6lxDBuuWucJEqFUd4l/D/5JgIi3dina6sYahB8iuPAZTNz2pMxw=="],
@@ -636,13 +640,13 @@
"@rc-component/mutate-observer": ["@rc-component/mutate-observer@2.0.1", "", { "dependencies": { "@rc-component/util": "^1.2.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-AyarjoLU5YlxuValRi+w8JRH2Z84TBbFO2RoGWz9d8bSu0FqT8DtugH3xC3BV7mUwlmROFauyWuXFuq4IFbH+w=="],
- "@rc-component/notification": ["@rc-component/notification@1.2.0", "", { "dependencies": { "@rc-component/motion": "^1.1.4", "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-OX3J+zVU7rvoJCikjrfW7qOUp7zlDeFBK2eA3SFbGSkDqo63Sl4Ss8A04kFP+fxHSxMDIS9jYVEZtU1FNCFuBA=="],
+ "@rc-component/notification": ["@rc-component/notification@2.0.7", "", { "dependencies": { "@rc-component/motion": "^1.1.4", "@rc-component/util": "^1.11.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-nqZzpf6BPdaj+3ILx7si79LLmqPKyUmQoXa+/9gg0SkH0v1DbD66oJgRMSBEVnd/zUT3D4gwxWIHUKebYf2ZXQ=="],
"@rc-component/overflow": ["@rc-component/overflow@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.11.1", "@rc-component/resize-observer": "^1.0.1", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-GSlBeoE0XTBi5cf3zl8Qh7Uqhn7v8RrlJ8ajeVpEkNe94HWy5l5BQ0Mwn2TVUq9gdgbfEMUmTX7tJFAg7mz0Rw=="],
"@rc-component/pagination": ["@rc-component/pagination@1.2.0", "", { "dependencies": { "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-YcpUFE8dMLfSo6OARJlK6DbHHvrxz7pMGPGmC/caZSJJz6HRKHC1RPP001PRHCvG9Z/veD039uOQmazVuLJzlw=="],
- "@rc-component/picker": ["@rc-component/picker@1.9.1", "", { "dependencies": { "@rc-component/overflow": "^1.0.0", "@rc-component/resize-observer": "^1.0.0", "@rc-component/trigger": "^3.6.15", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "date-fns": ">= 2.x", "dayjs": ">= 1.x", "luxon": ">= 3.x", "moment": ">= 2.x", "react": ">=16.9.0", "react-dom": ">=16.9.0" }, "optionalPeers": ["date-fns", "dayjs", "luxon", "moment"] }, "sha512-9FBYYsvH3HMLICaPDA/1Th5FLaDkFa7qAtangIdlhKb3ZALaR745e9PsOhheJb6asS4QXc12ffiAcjdkZ4C5/g=="],
+ "@rc-component/picker": ["@rc-component/picker@1.10.0", "", { "dependencies": { "@rc-component/overflow": "^1.0.0", "@rc-component/resize-observer": "^1.0.0", "@rc-component/trigger": "^3.6.15", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "date-fns": ">= 2.x", "dayjs": ">= 1.x", "luxon": ">= 3.x", "moment": ">= 2.x", "react": ">=16.9.0", "react-dom": ">=16.9.0" }, "optionalPeers": ["date-fns", "dayjs", "luxon", "moment"] }, "sha512-vVOXP2RVWozwpERGUFAehVH1Jz6o/uRrAb9qSZm1LC+iJs8rvEwFo1bzz2jlOYV+uWwu0dIuG86tnDui14Ea0w=="],
"@rc-component/portal": ["@rc-component/portal@1.1.2", "", { "dependencies": { "@babel/runtime": "^7.18.0", "classnames": "^2.3.2", "rc-util": "^5.24.4" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-6f813C0IsasTZms08kfA8kPAGxbbkYToa8ALaiDIGGECU4i9hj8Plgbx0sNJDrey3EtHO30hmdaxtT0138xZcg=="],
@@ -664,25 +668,23 @@
"@rc-component/switch": ["@rc-component/switch@1.0.3", "", { "dependencies": { "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-Jgi+EbOBquje/XNdofr7xbJQZPYJP+BlPfR0h+WN4zFkdtB2EWqEfvkXJWeipflwjWip0/17rNbxEAqs8hVHfw=="],
- "@rc-component/table": ["@rc-component/table@1.9.1", "", { "dependencies": { "@rc-component/context": "^2.0.1", "@rc-component/resize-observer": "^1.0.0", "@rc-component/util": "^1.1.0", "@rc-component/virtual-list": "^1.0.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-FVI5ZS/GdB3BcgexfCYKi3iHhZS3Fr59EtsxORszYGrfpH1eWr33eDNSYkVfLI6tfJ7vftJDd9D5apfFWqkdJg=="],
+ "@rc-component/table": ["@rc-component/table@1.10.0", "", { "dependencies": { "@rc-component/context": "^2.0.1", "@rc-component/resize-observer": "^1.0.0", "@rc-component/util": "^1.1.0", "@rc-component/virtual-list": "^1.0.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-SjtpcCf+rL7dDc62GKT3rXTdERjVuJvRiqjpU7g0Jc/ewCifXynHc7Nm3Em1XsD+WhGrgQtxNDScI/0+Lpfr0w=="],
- "@rc-component/tabs": ["@rc-component/tabs@1.7.0", "", { "dependencies": { "@rc-component/dropdown": "~1.0.0", "@rc-component/menu": "~1.2.0", "@rc-component/motion": "^1.1.3", "@rc-component/resize-observer": "^1.0.0", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-J48cs2iBi7Ho3nptBxxIqizEliUC+ExE23faspUQKGQ550vaBlv3aGF8Epv/UB1vFWeoJDTW/dNzgIU0Qj5i/w=="],
-
- "@rc-component/textarea": ["@rc-component/textarea@1.1.2", "", { "dependencies": { "@rc-component/input": "~1.1.0", "@rc-component/resize-observer": "^1.0.0", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-9rMUEODWZDMovfScIEHXWlVZuPljZ2pd1LKNjslJVitn4SldEzq5vO1CL3yy3Dnib6zZal2r2DPtjy84VVpF6A=="],
+ "@rc-component/tabs": ["@rc-component/tabs@1.9.0", "", { "dependencies": { "@rc-component/dropdown": "~1.0.0", "@rc-component/menu": "~1.3.0", "@rc-component/motion": "^1.1.3", "@rc-component/resize-observer": "^1.0.0", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-tn1slmbbaTyt8mgwyWJcT8jo/qNiYUs6u1H7OgGQt9faYO06BJIkU5cTmMqORzIrNmSEeeUY6pD5i+JlqSHYhg=="],
"@rc-component/tooltip": ["@rc-component/tooltip@1.4.0", "", { "dependencies": { "@rc-component/trigger": "^3.7.1", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-8Rx5DCctIlLI4raR0I0xHjVTf1aF48+gKCNeAAo5bmF5VoR5YED+A/XEqzXv9KKqrJDRcd3Wndpxh2hyzrTtSg=="],
- "@rc-component/tour": ["@rc-component/tour@2.3.0", "", { "dependencies": { "@rc-component/portal": "^2.2.0", "@rc-component/trigger": "^3.0.0", "@rc-component/util": "^1.7.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-K04K9r32kUC+auBSQfr+Fss4SpSIS9JGe56oq/ALAX0p+i2ylYOI1MgR83yBY7v96eO6ZFXcM/igCQmubps0Ow=="],
+ "@rc-component/tour": ["@rc-component/tour@2.4.0", "", { "dependencies": { "@rc-component/portal": "^2.2.0", "@rc-component/trigger": "^3.0.0", "@rc-component/util": "^1.7.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-aui4r4TqmTzwaBgcQxHYep8kM8PTjZFufjokObpy35KfFeZ0k9ArquWFZqegQlH24P14t+F0qO0mGTgzlav1yg=="],
- "@rc-component/tree": ["@rc-component/tree@1.2.4", "", { "dependencies": { "@rc-component/motion": "^1.0.0", "@rc-component/util": "^1.8.1", "@rc-component/virtual-list": "^1.0.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-5Gli43+m4R7NhpYYz3Z61I6LOw9yI6CNChxgVtvrO6xB1qML7iE6QMLVMB3+FTjo2yF6uFdAHtqWPECz/zbX5w=="],
+ "@rc-component/tree": ["@rc-component/tree@1.3.2", "", { "dependencies": { "@rc-component/motion": "^1.0.0", "@rc-component/util": "^1.11.1", "@rc-component/virtual-list": "^1.2.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-bJFj46wEkpBPnWyTm18XmgAgNQ/4YvprxMOPPY2a6rmhGJYxLuNKEFiL5Qej4Qctu9wHJm8WW+v2SYskafE0kA=="],
- "@rc-component/tree-select": ["@rc-component/tree-select@1.8.0", "", { "dependencies": { "@rc-component/select": "~1.6.0", "@rc-component/tree": "~1.2.0", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-iYsPq3nuLYvGqdvFAW+l+I9ASRIOVbMXyA8FGZg2lGym/GwkaWeJGzI4eJ7c9IOEhRj0oyfIN4S92Fl3J05mjQ=="],
+ "@rc-component/tree-select": ["@rc-component/tree-select@1.9.0", "", { "dependencies": { "@rc-component/select": "~1.6.0", "@rc-component/tree": "~1.3.0", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-GXcFe15a+trUl1/J3OHWQhsVWFpwFpGFK2cqYWZ1sK22Zs3KZTvMwDpzr75PIo1s6QVioVxpE/pRwRopkeDQ6w=="],
"@rc-component/trigger": ["@rc-component/trigger@3.9.0", "", { "dependencies": { "@rc-component/motion": "^1.1.4", "@rc-component/portal": "^2.2.0", "@rc-component/resize-observer": "^1.1.1", "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-X8btpwfrT27AgrZVOz4swclhEHTZcqaHeQMXXBgveagOiakTa36uObXbdwerXffgV8G9dH1fAAE0DHtVQs8EHg=="],
"@rc-component/upload": ["@rc-component/upload@1.1.0", "", { "dependencies": { "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-LIBV90mAnUE6VK5N4QvForoxZc4XqEYZimcp7fk+lkE4XwHHyJWxpIXQQwMU8hJM+YwBbsoZkGksL1sISWHQxw=="],
- "@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="],
+ "@rc-component/util": ["@rc-component/util@1.11.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-awVlI3ub2vqfqkYxOBc/uQ0efm3jw0wcrhtO/YWLyZfxiKXczKwNbVuhlnyxytDt7H9pbbVQiqr+O6MLATtRYg=="],
"@rc-component/virtual-list": ["@rc-component/virtual-list@1.0.2", "", { "dependencies": { "@babel/runtime": "^7.20.0", "@rc-component/resize-observer": "^1.0.1", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-uvTol/mH74FYsn5loDGJxo+7kjkO4i+y4j87Re1pxJBs0FaeuMuLRzQRGaXwnMcV1CxpZLi2Z56Rerj2M00fjQ=="],
@@ -756,7 +758,7 @@
"@shikijs/themes": ["@shikijs/themes@4.0.2", "", { "dependencies": { "@shikijs/types": "4.0.2" } }, "sha512-mjCafwt8lJJaVSsQvNVrJumbnnj1RI8jbUKrPKgE6E3OvQKxnuRoBaYC51H4IGHePsGN/QtALglWBU7DoKDFnA=="],
- "@shikijs/transformers": ["@shikijs/transformers@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/types": "3.23.0" } }, "sha512-F9msZVxdF+krQNSdQ4V+Ja5QemeAoTQ2jxt7nJCwhDsdF1JWS3KxIQXA3lQbyKwS3J61oHRUSv4jYWv3CkaKTQ=="],
+ "@shikijs/transformers": ["@shikijs/transformers@4.1.0", "", { "dependencies": { "@shikijs/core": "4.1.0", "@shikijs/types": "4.1.0" } }, "sha512-YbuOcAA3kwqKDU9YSt00dtFLrY5lBXjKU3dWaMATyEyPSqBm9Jqblk/uVICxz7lcjwAHzYaEvIiMWX3mTpogkA=="],
"@shikijs/types": ["@shikijs/types@4.0.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-qzbeRooUTPnLE+sHD/Z8DStmaDgnbbc/pMrU203950aRqjX/6AFHeDYT+j00y2lPdz0ywJKx7o/7qnqTivtlXg=="],
@@ -1058,7 +1060,7 @@
"ansis": ["ansis@4.2.0", "", {}, "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig=="],
- "antd": ["antd@6.3.5", "", { "dependencies": { "@ant-design/colors": "^8.0.1", "@ant-design/cssinjs": "^2.1.2", "@ant-design/cssinjs-utils": "^2.1.2", "@ant-design/fast-color": "^3.0.1", "@ant-design/icons": "^6.1.1", "@ant-design/react-slick": "~2.0.0", "@babel/runtime": "^7.28.4", "@rc-component/cascader": "~1.14.0", "@rc-component/checkbox": "~2.0.0", "@rc-component/collapse": "~1.2.0", "@rc-component/color-picker": "~3.1.1", "@rc-component/dialog": "~1.8.4", "@rc-component/drawer": "~1.4.2", "@rc-component/dropdown": "~1.0.2", "@rc-component/form": "~1.8.0", "@rc-component/image": "~1.8.0", "@rc-component/input": "~1.1.2", "@rc-component/input-number": "~1.6.2", "@rc-component/mentions": "~1.6.0", "@rc-component/menu": "~1.2.0", "@rc-component/motion": "^1.3.2", "@rc-component/mutate-observer": "^2.0.1", "@rc-component/notification": "~1.2.0", "@rc-component/pagination": "~1.2.0", "@rc-component/picker": "~1.9.1", "@rc-component/progress": "~1.0.2", "@rc-component/qrcode": "~1.1.1", "@rc-component/rate": "~1.0.1", "@rc-component/resize-observer": "^1.1.2", "@rc-component/segmented": "~1.3.0", "@rc-component/select": "~1.6.15", "@rc-component/slider": "~1.0.1", "@rc-component/steps": "~1.2.2", "@rc-component/switch": "~1.0.3", "@rc-component/table": "~1.9.1", "@rc-component/tabs": "~1.7.0", "@rc-component/textarea": "~1.1.2", "@rc-component/tooltip": "~1.4.0", "@rc-component/tour": "~2.3.0", "@rc-component/tree": "~1.2.4", "@rc-component/tree-select": "~1.8.0", "@rc-component/trigger": "^3.9.0", "@rc-component/upload": "~1.1.0", "@rc-component/util": "^1.10.0", "clsx": "^2.1.1", "dayjs": "^1.11.11", "scroll-into-view-if-needed": "^3.1.0", "throttle-debounce": "^5.0.2" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-8BPz9lpZWQm42PTx7yL4KxWAotVuqINiKcoYRcLtdd5BFmAcAZicVyFTnBJyRDlzGZFZeRW3foGu6jXYFnej6Q=="],
+ "antd": ["antd@6.4.3", "", { "dependencies": { "@ant-design/colors": "^8.0.1", "@ant-design/cssinjs": "^2.1.2", "@ant-design/cssinjs-utils": "^2.1.2", "@ant-design/fast-color": "^3.0.1", "@ant-design/icons": "^6.2.3", "@ant-design/react-slick": "~2.0.0", "@babel/runtime": "^7.29.2", "@rc-component/cascader": "~1.15.0", "@rc-component/checkbox": "~2.0.0", "@rc-component/collapse": "~1.2.0", "@rc-component/color-picker": "~3.1.1", "@rc-component/dialog": "~1.9.0", "@rc-component/drawer": "~1.4.2", "@rc-component/dropdown": "~1.0.2", "@rc-component/form": "~1.8.1", "@rc-component/image": "~1.9.0", "@rc-component/input": "~1.3.0", "@rc-component/input-number": "~1.6.2", "@rc-component/mentions": "~1.9.0", "@rc-component/menu": "~1.3.0", "@rc-component/motion": "^1.3.2", "@rc-component/mutate-observer": "^2.0.1", "@rc-component/notification": "~2.0.7", "@rc-component/pagination": "~1.2.0", "@rc-component/picker": "~1.10.0", "@rc-component/progress": "~1.0.2", "@rc-component/qrcode": "~1.1.1", "@rc-component/rate": "~1.0.1", "@rc-component/resize-observer": "^1.1.2", "@rc-component/segmented": "~1.3.0", "@rc-component/select": "~1.6.15", "@rc-component/slider": "~1.0.1", "@rc-component/steps": "~1.2.2", "@rc-component/switch": "~1.0.3", "@rc-component/table": "~1.10.0", "@rc-component/tabs": "~1.9.0", "@rc-component/tooltip": "~1.4.0", "@rc-component/tour": "~2.4.0", "@rc-component/tree": "~1.3.1", "@rc-component/tree-select": "~1.9.0", "@rc-component/trigger": "^3.9.0", "@rc-component/upload": "~1.1.0", "@rc-component/util": "^1.11.0", "clsx": "^2.1.1", "dayjs": "^1.11.11", "scroll-into-view-if-needed": "^3.1.0", "throttle-debounce": "^5.0.2" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-6H2avkxCGfxcF67r3J2mwm9Ck50el1pks/73vfM1wDsPL/tPtj5vHuauMgJFnrqmq7CH3g8aoZ0VBQbt+jpAsw=="],
"antd-style": ["antd-style@4.1.0", "", { "dependencies": { "@ant-design/cssinjs": "^2.0.0", "@babel/runtime": "^7.24.1", "@emotion/cache": "^11.11.0", "@emotion/css": "^11.11.2", "@emotion/react": "^11.11.4", "@emotion/serialize": "^1.1.3", "@emotion/utils": "^1.2.1", "use-merge-value": "^1.2.0" }, "peerDependencies": { "antd": ">=6.0.0", "react": ">=18" } }, "sha512-vnPBGg0OVlSz90KRYZhxd89aZiOImTiesF+9MQqN8jsLGZUQTjbP04X9jTdEfsztKUuMbBWg/RmB/wHTakbtMQ=="],
@@ -1212,7 +1214,7 @@
"d3": ["d3@7.9.0", "", { "dependencies": { "d3-array": "3", "d3-axis": "3", "d3-brush": "3", "d3-chord": "3", "d3-color": "3", "d3-contour": "4", "d3-delaunay": "6", "d3-dispatch": "3", "d3-drag": "3", "d3-dsv": "3", "d3-ease": "3", "d3-fetch": "3", "d3-force": "3", "d3-format": "3", "d3-geo": "3", "d3-hierarchy": "3", "d3-interpolate": "3", "d3-path": "3", "d3-polygon": "3", "d3-quadtree": "3", "d3-random": "3", "d3-scale": "4", "d3-scale-chromatic": "3", "d3-selection": "3", "d3-shape": "3", "d3-time": "3", "d3-time-format": "4", "d3-timer": "3", "d3-transition": "3", "d3-zoom": "3" } }, "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA=="],
- "d3-array": ["d3-array@1.2.4", "", {}, "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw=="],
+ "d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="],
"d3-axis": ["d3-axis@3.0.0", "", {}, "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw=="],
@@ -1360,7 +1362,7 @@
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
- "es-toolkit": ["es-toolkit@1.45.1", "", {}, "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw=="],
+ "es-toolkit": ["es-toolkit@1.47.0", "", {}, "sha512-n1GuoD0WEQZMBk5tttoZSqwgyLx01oqa5XsBmCHwPyNe1S9jPBEmtR2pSgp2kJuWE3ciFZ6yRHmY4pM4C3OOkw=="],
"esast-util-from-estree": ["esast-util-from-estree@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "unist-util-position-from-estree": "^2.0.0" } }, "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ=="],
@@ -1856,9 +1858,9 @@
"micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="],
- "micromark-extension-cjk-friendly": ["micromark-extension-cjk-friendly@1.2.3", "", { "dependencies": { "devlop": "^1.1.0", "micromark-extension-cjk-friendly-util": "2.1.1", "micromark-util-chunked": "^2.0.1", "micromark-util-resolve-all": "^2.0.1", "micromark-util-symbol": "^2.0.1" }, "peerDependencies": { "micromark": "^4.0.0", "micromark-util-types": "^2.0.0" }, "optionalPeers": ["micromark-util-types"] }, "sha512-gRzVLUdjXBLX6zNPSnHGDoo+ZTp5zy+MZm0g3sv+3chPXY7l9gW+DnrcHcZh/jiPR6MjPKO4AEJNp4Aw6V9z5Q=="],
+ "micromark-extension-cjk-friendly": ["micromark-extension-cjk-friendly@2.0.1", "", { "dependencies": { "devlop": "^1.1.0", "micromark-extension-cjk-friendly-util": "3.0.1", "micromark-util-chunked": "^2.0.1", "micromark-util-resolve-all": "^2.0.1", "micromark-util-symbol": "^2.0.1" }, "peerDependencies": { "micromark": "^4.0.0", "micromark-util-types": "^2.0.0" }, "optionalPeers": ["micromark-util-types"] }, "sha512-OkzoYVTL1ChbvQ8Cc1ayTIz7paFQz8iS9oIYmewncweUSwmWR+hkJF9spJ1lxB90XldJl26A1F4IkPOKS3bDXw=="],
- "micromark-extension-cjk-friendly-util": ["micromark-extension-cjk-friendly-util@2.1.1", "", { "dependencies": { "get-east-asian-width": "^1.3.0", "micromark-util-character": "^2.1.1", "micromark-util-symbol": "^2.0.1" } }, "sha512-egs6+12JU2yutskHY55FyR48ZiEcFOJFyk9rsiyIhcJ6IvWB6ABBqVrBw8IobqJTDZ/wdSr9eoXDPb5S2nW1bg=="],
+ "micromark-extension-cjk-friendly-util": ["micromark-extension-cjk-friendly-util@3.0.1", "", { "dependencies": { "get-east-asian-width": "^1.4.0", "micromark-util-character": "^2.1.1", "micromark-util-symbol": "^2.0.1" } }, "sha512-GcbXqTTHOsiZHyF753oIddP/J2eH8j9zpyQPhkof6B2JNxfEJabnQqxbCgzJNuNes0Y2jTNJ3LiYPSXr6eJA8w=="],
"micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="],
@@ -2132,7 +2134,7 @@
"react": ["react@19.2.5", "", {}, "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA=="],
- "react-avatar-editor": ["react-avatar-editor@14.0.0", "", { "peerDependencies": { "react": "^0.14.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^0.14.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-NaQM3oo4u0a1/Njjutc2FjwKX35vQV+t6S8hovsbAlMpBN1ntIwP/g+Yr9eDIIfaNtRXL0AqboTnPmRxhD/i8A=="],
+ "react-avatar-editor": ["react-avatar-editor@15.1.0", "", { "peerDependencies": { "react": "^0.14.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^0.14.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Zto7u9l6Wd5LPPtjeFJ+7uwoT4bs01OSgkN2kxD18lWl8IiZ0GY3nWCbKPx4qIU7Au1vENsMJm19rfVWHHayaQ=="],
"react-colorful": ["react-colorful@5.6.1", "", { "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw=="],
@@ -2210,7 +2212,7 @@
"remark-breaks": ["remark-breaks@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-newline-to-break": "^2.0.0", "unified": "^11.0.0" } }, "sha512-IjEjJOkH4FuJvHZVIW0QCDWxcG96kCq7An/KVH2NfJe6rKZU2AsHeB3OEjPNRxi4QC34Xdx7I2KGYn6IpT7gxQ=="],
- "remark-cjk-friendly": ["remark-cjk-friendly@1.2.3", "", { "dependencies": { "micromark-extension-cjk-friendly": "1.2.3" }, "peerDependencies": { "@types/mdast": "^4.0.0", "unified": "^11.0.0" }, "optionalPeers": ["@types/mdast"] }, "sha512-UvAgxwlNk+l9Oqgl/9MWK2eWRS7zgBW/nXX9AthV7nd/3lNejF138E7Xbmk9Zs4WjTJGs721r7fAEc7tNFoH7g=="],
+ "remark-cjk-friendly": ["remark-cjk-friendly@2.0.1", "", { "dependencies": { "micromark-extension-cjk-friendly": "2.0.1" }, "peerDependencies": { "@types/mdast": "^4.0.0", "unified": "^11.0.0" }, "optionalPeers": ["@types/mdast"] }, "sha512-6WwkoQyZf/4j5k53zdFYrR8Ca+UVn992jXdLUSBDZR4eBpFhKyVxmA4gUHra/5fesjGIxrDhHesNr/sVoiiysA=="],
"remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="],
@@ -2512,7 +2514,7 @@
"vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
- "virtua": ["virtua@0.48.8", "", { "peerDependencies": { "react": ">=16.14.0", "react-dom": ">=16.14.0", "solid-js": ">=1.0", "svelte": ">=5.0", "vue": ">=3.2" }, "optionalPeers": ["react", "react-dom", "solid-js", "svelte", "vue"] }, "sha512-jpsxOw5V4B6hg44JePRLo9DL0TV7N1lBEVtPjKpAJebXyhI2s9lfiXJESaLapNtr3vtiSk/pWHiLf7B2a6UcgQ=="],
+ "virtua": ["virtua@0.49.1", "", { "peerDependencies": { "react": ">=16.14.0", "react-dom": ">=16.14.0", "solid-js": ">=1.0", "svelte": ">=5.0", "vue": ">=3.2" }, "optionalPeers": ["react", "react-dom", "solid-js", "svelte", "vue"] }, "sha512-6f79msqg3jzNFdqJiS0FSzhRN1EHlDhR7EvW7emp6z5qQ22VdsReiDHflkpMEMhoAyUuYr69nwT0aagiM7NrUg=="],
"void-elements": ["void-elements@3.1.0", "", {}, "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w=="],
@@ -2574,6 +2576,10 @@
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
+ "@ant-design/cssinjs/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="],
+
+ "@ant-design/cssinjs-utils/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="],
+
"@dotenvx/dotenvx/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
"@dotenvx/dotenvx/execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="],
@@ -2598,20 +2604,20 @@
"@eslint/config-array/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
+ "@lobehub/fluent-emoji/es-toolkit": ["es-toolkit@1.45.1", "", {}, "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw=="],
+
"@lobehub/fluent-emoji/lucide-react": ["lucide-react@0.562.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw=="],
"@lobehub/icons/lucide-react": ["lucide-react@0.469.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-28vvUnnKQ/dBwiCQtwJw7QauYnE7yd2Cyp4tTTJpvglX4EMpbflcdBgrgToX2j71B3YvugK/NH3BGUk+E/p/Fw=="],
- "@lobehub/ui/@shikijs/core": ["@shikijs/core@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA=="],
-
- "@lobehub/ui/lucide-react": ["lucide-react@0.563.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-8dXPB2GI4dI8jV4MgUDGBeLdGk8ekfqVZ0BdLcrRzocGgG75ltNEmWS+gE7uokKF/0oSUuczNDT+g9hFJ23FkA=="],
-
- "@lobehub/ui/shiki": ["shiki@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/engine-javascript": "3.23.0", "@shikijs/engine-oniguruma": "3.23.0", "@shikijs/langs": "3.23.0", "@shikijs/themes": "3.23.0", "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA=="],
+ "@lobehub/ui/lucide-react": ["lucide-react@1.16.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-dYwyPzb4MEKpGUmNYk3WKWPnMrHs3FKM+q94kAnJrcDIqqn1hq2xY8scaS2ovsOCM5D51ey2gaRG3PBb1vgoYQ=="],
"@mdx-js/mdx/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
"@modelcontextprotocol/sdk/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
+ "@pierre/diffs/@shikijs/transformers": ["@shikijs/transformers@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/types": "3.23.0" } }, "sha512-F9msZVxdF+krQNSdQ4V+Ja5QemeAoTQ2jxt7nJCwhDsdF1JWS3KxIQXA3lQbyKwS3J61oHRUSv4jYWv3CkaKTQ=="],
+
"@pierre/diffs/diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="],
"@pierre/diffs/shiki": ["shiki@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/engine-javascript": "3.23.0", "@shikijs/engine-oniguruma": "3.23.0", "@shikijs/langs": "3.23.0", "@shikijs/themes": "3.23.0", "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA=="],
@@ -2644,21 +2650,69 @@
"@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+ "@rc-component/checkbox/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="],
+
+ "@rc-component/collapse/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="],
+
+ "@rc-component/color-picker/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="],
+
+ "@rc-component/context/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="],
+
"@rc-component/dialog/@rc-component/portal": ["@rc-component/portal@2.2.0", "", { "dependencies": { "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-oc6FlA+uXCMiwArHsJyHcIkX4q6uKyndrPol2eWX8YPkAnztHOPsFIRtmWG4BMlGE5h7YIRE3NiaJ5VS8Lb1QQ=="],
"@rc-component/drawer/@rc-component/portal": ["@rc-component/portal@2.2.0", "", { "dependencies": { "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-oc6FlA+uXCMiwArHsJyHcIkX4q6uKyndrPol2eWX8YPkAnztHOPsFIRtmWG4BMlGE5h7YIRE3NiaJ5VS8Lb1QQ=="],
+ "@rc-component/drawer/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="],
+
+ "@rc-component/dropdown/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="],
+
"@rc-component/image/@rc-component/portal": ["@rc-component/portal@2.2.0", "", { "dependencies": { "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-oc6FlA+uXCMiwArHsJyHcIkX4q6uKyndrPol2eWX8YPkAnztHOPsFIRtmWG4BMlGE5h7YIRE3NiaJ5VS8Lb1QQ=="],
+ "@rc-component/input-number/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="],
+
+ "@rc-component/motion/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="],
+
+ "@rc-component/mutate-observer/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="],
+
+ "@rc-component/overflow/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="],
+
+ "@rc-component/pagination/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="],
+
+ "@rc-component/progress/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="],
+
+ "@rc-component/rate/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="],
+
+ "@rc-component/resize-observer/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="],
+
+ "@rc-component/segmented/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="],
+
+ "@rc-component/select/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="],
+
+ "@rc-component/slider/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="],
+
+ "@rc-component/steps/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="],
+
+ "@rc-component/switch/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="],
+
+ "@rc-component/tooltip/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="],
+
"@rc-component/tour/@rc-component/portal": ["@rc-component/portal@2.2.0", "", { "dependencies": { "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-oc6FlA+uXCMiwArHsJyHcIkX4q6uKyndrPol2eWX8YPkAnztHOPsFIRtmWG4BMlGE5h7YIRE3NiaJ5VS8Lb1QQ=="],
+ "@rc-component/tree/@rc-component/virtual-list": ["@rc-component/virtual-list@1.2.0", "", { "dependencies": { "@babel/runtime": "^7.20.0", "@rc-component/resize-observer": "^1.0.1", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-iavRm1Jo4GDbASQwdGa7jFyk93RvSOo9xHyBT4QL1pgFJj/Fdf1G+3RErH7/7BmAMvx2AkF62mjGYxDbXsK9TQ=="],
+
"@rc-component/trigger/@rc-component/portal": ["@rc-component/portal@2.2.0", "", { "dependencies": { "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-oc6FlA+uXCMiwArHsJyHcIkX4q6uKyndrPol2eWX8YPkAnztHOPsFIRtmWG4BMlGE5h7YIRE3NiaJ5VS8Lb1QQ=="],
+ "@rc-component/trigger/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="],
+
+ "@rc-component/upload/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="],
+
+ "@rc-component/virtual-list/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="],
+
"@rspack/binding-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="],
- "@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA=="],
+ "@shikijs/transformers/@shikijs/core": ["@shikijs/core@4.1.0", "", { "dependencies": { "@shikijs/primitive": "4.1.0", "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-jLJtSJeuFffqX6/inRE1zqU5aFv2hrszvYgq3OjbAgFRZiWv7abKMDdQzYxuSDfmUPQozZvI/kuy6VMTvnvqTQ=="],
- "@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="],
+ "@shikijs/transformers/@shikijs/types": ["@shikijs/types@4.1.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3EQWX54fMpniOrDblzAhiwiJwpiTMW6+B9DWyUd9ska483tbayFYuw47UxwuPknI31bKnySfVQ/QW+jFL4rFdA=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.9.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA=="],
@@ -2706,26 +2760,22 @@
"cytoscape-fcose/cose-base": ["cose-base@2.2.0", "", { "dependencies": { "layout-base": "^2.0.0" } }, "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g=="],
- "d3/d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="],
-
"d3/d3-dsv": ["d3-dsv@3.0.1", "", { "dependencies": { "commander": "7", "iconv-lite": "0.6", "rw": "1" }, "bin": { "csv2json": "bin/dsv2json.js", "csv2tsv": "bin/dsv2dsv.js", "dsv2dsv": "bin/dsv2dsv.js", "dsv2json": "bin/dsv2json.js", "json2csv": "bin/json2dsv.js", "json2dsv": "bin/json2dsv.js", "json2tsv": "bin/json2dsv.js", "tsv2csv": "bin/dsv2dsv.js", "tsv2json": "bin/dsv2json.js" } }, "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q=="],
"d3/d3-geo": ["d3-geo@3.1.1", "", { "dependencies": { "d3-array": "2.5.0 - 3" } }, "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q=="],
- "d3-contour/d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="],
-
"d3-dsv/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
"d3-dsv/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
"d3-fetch/d3-dsv": ["d3-dsv@3.0.1", "", { "dependencies": { "commander": "7", "iconv-lite": "0.6", "rw": "1" }, "bin": { "csv2json": "bin/dsv2json.js", "csv2tsv": "bin/dsv2dsv.js", "dsv2dsv": "bin/dsv2dsv.js", "dsv2json": "bin/dsv2json.js", "json2csv": "bin/json2dsv.js", "json2dsv": "bin/json2dsv.js", "json2tsv": "bin/json2dsv.js", "tsv2csv": "bin/dsv2dsv.js", "tsv2json": "bin/dsv2json.js" } }, "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q=="],
+ "d3-geo/d3-array": ["d3-array@1.2.4", "", {}, "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw=="],
+
+ "d3-sankey/d3-array": ["d3-array@1.2.4", "", {}, "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw=="],
+
"d3-sankey/d3-shape": ["d3-shape@1.3.7", "", { "dependencies": { "d3-path": "1" } }, "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw=="],
- "d3-scale/d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="],
-
- "d3-time/d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="],
-
"eslint/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
"estree-util-to-js/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
@@ -2822,20 +2872,12 @@
"@eslint/config-array/minimatch/brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="],
- "@lobehub/ui/@shikijs/core/@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="],
-
- "@lobehub/ui/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA=="],
-
- "@lobehub/ui/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g=="],
-
- "@lobehub/ui/shiki/@shikijs/langs": ["@shikijs/langs@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg=="],
-
- "@lobehub/ui/shiki/@shikijs/themes": ["@shikijs/themes@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA=="],
-
- "@lobehub/ui/shiki/@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="],
-
"@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
+ "@pierre/diffs/@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA=="],
+
+ "@pierre/diffs/@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="],
+
"@pierre/diffs/shiki/@shikijs/core": ["@shikijs/core@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA=="],
"@pierre/diffs/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA=="],
@@ -2848,6 +2890,14 @@
"@pierre/diffs/shiki/@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="],
+ "@rc-component/dialog/@rc-component/portal/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="],
+
+ "@rc-component/image/@rc-component/portal/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="],
+
+ "@rc-component/tour/@rc-component/portal/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="],
+
+ "@shikijs/transformers/@shikijs/core/@shikijs/primitive": ["@shikijs/primitive@4.1.0", "", { "dependencies": { "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-zx2/2Uwj2q9X3KSyYREEhXO23xBw5WUhP4orK2lE4r+t9JGITmEe0JH+wPmJhqHpOT2bRRs6lAL945+LDvOAGw=="],
+
"@ts-morph/common/minimatch/brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="],
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="],
diff --git a/heicode/web/default/dist/index.html b/heicode/web/default/dist/index.html
new file mode 100644
index 00000000..81088411
--- /dev/null
+++ b/heicode/web/default/dist/index.html
@@ -0,0 +1,10 @@
+
+
+
+
+ Heicode Manager placeholder
+
+
+
+
+
diff --git a/heicode/web/default/src/components/query-state.tsx b/heicode/web/default/src/components/query-state.tsx
index 812e0c7e..f636dd86 100644
--- a/heicode/web/default/src/components/query-state.tsx
+++ b/heicode/web/default/src/components/query-state.tsx
@@ -1,5 +1,5 @@
import type { ReactNode } from 'react'
-import { ShieldOff, WifiOff } from 'lucide-react'
+import { ShieldOff, WifiOff, type LucideIcon } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { EmptyState } from './empty-state'
import { ErrorState } from './error-state'
@@ -41,7 +41,7 @@ interface QueryStateProps {
loadingFallback?: ReactNode
emptyTitle?: string
emptyDescription?: string
- emptyIcon?: React.ComponentType<{ className?: string }>
+ emptyIcon?: LucideIcon
emptyAction?: ReactNode
}
diff --git a/heicode/web/default/src/features/agnet-console/azure-cloud-binding-sheet.tsx b/heicode/web/default/src/features/agnet-console/azure-cloud-binding-sheet.tsx
index 0587a5be..4a47141c 100644
--- a/heicode/web/default/src/features/agnet-console/azure-cloud-binding-sheet.tsx
+++ b/heicode/web/default/src/features/agnet-console/azure-cloud-binding-sheet.tsx
@@ -1,7 +1,14 @@
import { useState } from 'react'
-import { useTranslation } from 'react-i18next'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { AlertTriangle, Cloud, ShieldCheck } from 'lucide-react'
+import { useTranslation } from 'react-i18next'
+import { toast } from 'sonner'
+import {
+ createManagerResource,
+ discoverManagerAzureResources,
+ putManagerResourceSecret,
+ updateManagerResource,
+} from '@/lib/manager-resources'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
@@ -12,28 +19,21 @@ import {
SheetHeader,
SheetTitle,
} from '@/components/ui/sheet'
-import { toast } from 'sonner'
-import { createResource } from '@/lib/heicode-mcp'
-// M2 phase 1 — Azure cloud account binding via manual Service Principal
-// credentials. Phase 2 (real OAuth code flow) and phase 3 (ARM resource
-// auto-discovery) require Azure App Registration + OpenBao to be online
-// first; until then, operators paste SP creds manually here. The sheet
-// is deliberately explicit about the security caveat: when OpenBao
-// isn't configured, the client_secret would be stored alongside the
-// resource binding, NOT in a vault. Caller sees a yellow warning until
-// vault is wired up.
+// Azure cloud account binding via manual Service Principal credentials.
+// The Manager creates a local ResourceBinding first, writes the
+// client_secret to Azure Key Vault via /api/resources/:id/secret, stores
+// only azkv://... on the resource row, then asks Manager to enumerate ARM
+// resources and persist them as cloud_resource bindings.
//
-// Field map → mcp-server ResourceBinding:
-// type = 'cloud_account'
+// Field map → Manager ResourceBinding:
+// resource_type = 'cloud_account'
// name = display_name
-// external_ref = subscription_id
-// metadata = { provider, tenant_id, client_id }
-// secret_ref = vault:// (intentionally empty during phase 1;
-// server should refuse to materialise creds until
-// OpenBao is wired up)
-// constraints = {} (resource group filters land in phase 3)
-// permission_scope= ['azure:read'] (read-only ARM access scope)
+// external_id = subscription_id
+// metadata = { subscription_id, tenant_id, client_id }
+// secret_ref = azkv:///secrets/
+// constraints = {} (resource group filters can be added later)
+// permission_scope= { actions: ['azure:read'] }
interface Props {
open: boolean
@@ -41,7 +41,11 @@ interface Props {
vaultConfigured?: boolean
}
-export function AzureCloudBindingSheet({ open, onOpenChange, vaultConfigured }: Props) {
+export function AzureCloudBindingSheet({
+ open,
+ onOpenChange,
+ vaultConfigured,
+}: Props) {
const { t } = useTranslation()
const qc = useQueryClient()
@@ -54,28 +58,68 @@ export function AzureCloudBindingSheet({ open, onOpenChange, vaultConfigured }:
})
const mutation = useMutation({
- mutationFn: () =>
- createResource({
- type: 'cloud_account',
+ mutationFn: async () => {
+ if (!vaultConfigured) {
+ throw new Error(t('Azure Key Vault is not ready'))
+ }
+ const pending = await createManagerResource({
+ resource_type: 'cloud_account',
name: form.name.trim(),
- external_ref: form.subscription_id.trim(),
+ provider: 'azure',
+ external_id: form.subscription_id.trim(),
+ binding_scope: `azure:${form.subscription_id.trim()}`,
metadata: {
- provider: 'azure',
+ subscription_id: form.subscription_id.trim(),
tenant_id: form.tenant_id.trim(),
client_id: form.client_id.trim(),
- // NOTE: client_secret is NOT included here. We rely on the
- // server side to mint a secret_ref via Secret Broker when
- // vault is online; until then the binding is created with
- // empty secret_ref and a "needs vault" status. Phase 2
- // wires up the actual cred-write step.
},
- permission_scope: ['azure:read'],
+ permission_scope: { actions: ['azure:read'] },
constraints: {},
- status: vaultConfigured ? 'active' : 'pending',
- }),
- onSuccess: () => {
+ status: 'pending',
+ })
+ const secret = await putManagerResourceSecret(pending.id, {
+ subscription_id: form.subscription_id.trim(),
+ tenant_id: form.tenant_id.trim(),
+ client_id: form.client_id.trim(),
+ client_secret: form.client_secret,
+ })
+ const account = await updateManagerResource(pending.id, {
+ resource_type: 'cloud_account',
+ name: pending.name,
+ provider: pending.provider,
+ external_id: pending.external_id,
+ binding_scope: pending.binding_scope,
+ secret_ref: secret.secret_ref,
+ metadata: pending.metadata,
+ permission_scope: pending.permission_scope,
+ constraints: pending.constraints,
+ status: 'active',
+ })
+ try {
+ const discovery = await discoverManagerAzureResources(account.id)
+ return { account, discovery, discoveryError: '' }
+ } catch (err) {
+ return {
+ account,
+ discovery: null,
+ discoveryError:
+ err instanceof Error ? err.message : t('Azure discovery failed'),
+ }
+ }
+ },
+ onSuccess: (result) => {
void qc.invalidateQueries({ queryKey: ['heicode', 'resources'] })
+ void qc.invalidateQueries({ queryKey: ['manager', 'cloud-resources'] })
toast.success(t('Azure subscription bound'))
+ if (result.discoveryError) {
+ toast.warning(result.discoveryError)
+ } else if (result.discovery) {
+ toast.success(
+ t('{{n}} Azure resources discovered', {
+ n: result.discovery.discovered,
+ })
+ )
+ }
onOpenChange(false)
setForm({
name: '',
@@ -86,7 +130,9 @@ export function AzureCloudBindingSheet({ open, onOpenChange, vaultConfigured }:
})
},
onError: (err) => {
- toast.error(err instanceof Error ? err.message : t('Failed to bind Azure account'))
+ toast.error(
+ err instanceof Error ? err.message : t('Failed to bind Azure account')
+ )
},
})
@@ -95,19 +141,20 @@ export function AzureCloudBindingSheet({ open, onOpenChange, vaultConfigured }:
form.subscription_id.trim() !== '' &&
form.tenant_id.trim() !== '' &&
form.client_id.trim() !== '' &&
- form.client_secret.trim() !== ''
+ form.client_secret.trim() !== '' &&
+ Boolean(vaultConfigured)
return (
-
+
-
+
{t('Connect Azure subscription')}
{t(
- 'Paste a Service Principal that has at least Reader role on the subscription. Heicode uses it to enumerate cloud resources (VMs, databases, AKS, storage) once auto-discovery lands. AWS and GCP are coming soon.'
+ 'Paste a Service Principal that has at least Reader role on the subscription. Heicode uses it to enumerate cloud resources (VMs, databases, AKS, storage). AWS and GCP are coming soon.'
)}
@@ -118,9 +165,9 @@ export function AzureCloudBindingSheet({ open, onOpenChange, vaultConfigured }:
{t('Secret vault not yet configured')}
-
+
{t(
- 'OpenBao is not wired up to the Manager yet (see System settings → Secret vault status). The binding will be created in pending state — the client_secret will NOT be persisted server-side until vault is configured. Re-enter or rotate the secret once vault is online.'
+ 'Azure Key Vault is not ready yet (see System settings → Secret vault status). The subscription cannot be bound until Manager can write the client_secret to Key Vault.'
)}
@@ -165,7 +212,9 @@ export function AzureCloudBindingSheet({ open, onOpenChange, vaultConfigured }:
- {t('Client ID (Application ID)')}
+
+ {t('Client ID (Application ID)')}
+
-
-
-
+
+
+
{t('What Heicode will and will not do')}
· {t('Read-only ARM access (Reader role recommended)')}
- · {t('Auto-discover VM / database / AKS / storage — coming soon')}
- · {t('Never modify resources without explicit approval from the desktop client')}
- · {t('Never write the client_secret into Manager logs or audit payloads')}
+
+ ·{' '}
+ {t('Auto-discover VM / database / AKS / storage')}
+
+
+ ·{' '}
+ {t(
+ 'Never modify resources without explicit approval from the desktop client'
+ )}
+
+
+ ·{' '}
+ {t(
+ 'Never write the client_secret into Manager logs or audit payloads'
+ )}
+
diff --git a/heicode/web/default/src/features/agnet-console/pages.tsx b/heicode/web/default/src/features/agnet-console/pages.tsx
index 1255469c..5cf4c200 100644
--- a/heicode/web/default/src/features/agnet-console/pages.tsx
+++ b/heicode/web/default/src/features/agnet-console/pages.tsx
@@ -5,17 +5,13 @@ import {
Activity,
AlertOctagon,
ArrowUpRight,
- Bot,
Building2,
- Calendar,
CheckCircle2,
CircleDashed,
Coins,
FileSearch,
Filter,
- GitBranch,
GitCommit,
- Hash,
PlayCircle,
Plus,
Rocket,
@@ -23,31 +19,11 @@ import {
ShieldCheck,
Tag,
Trash2,
- User2,
XCircle,
} from 'lucide-react'
import { useTranslation } from 'react-i18next'
-import { cn } from '@/lib/utils'
-import { Input } from '@/components/ui/input'
-import { Button } from '@/components/ui/button'
-import { Skeleton } from '@/components/ui/skeleton'
-import { QueryState } from '@/components/query-state'
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from '@/components/ui/select'
-import {
- getAgnetDeploymentEvents,
- getAgnetSnapshots,
- listAgnetDeployments,
- listAgnetDeploymentsQuiet,
- type AgnetDeployment,
- type AgnetRuntimeExecution,
- type AgnetSKAccessPolicy,
-} from './api'
+import { toast } from 'sonner'
+import { api } from '@/lib/api'
// /audit pulls from mcp-server §5.10 stub now, not the Heicode-local
// controller — the contract doc names that endpoint as the canonical
// source. The shape of McpAuditEntry is wider than the legacy local one
@@ -62,8 +38,17 @@ import {
type ResourceBinding,
type ResourceType,
} from '@/lib/heicode-mcp'
-import { AzureCloudBindingSheet } from './azure-cloud-binding-sheet'
-import { CreateAgnetDeploymentSheet } from './create-agnet-deployment-sheet'
+import { listManagerResources } from '@/lib/manager-resources'
+import { cn } from '@/lib/utils'
+import { Button } from '@/components/ui/button'
+import { Input } from '@/components/ui/input'
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select'
import {
Sheet,
SheetContent,
@@ -71,10 +56,25 @@ import {
SheetHeader,
SheetTitle,
} from '@/components/ui/sheet'
-import { toast } from 'sonner'
+import { Skeleton } from '@/components/ui/skeleton'
+import { QueryState } from '@/components/query-state'
+import {
+ getAgnetDeploymentEvents,
+ listAgnetDeployments,
+ type AgnetDeployment,
+ type AgnetRuntimeExecution,
+ type AgnetSKAccessPolicy,
+} from './api'
+import { AzureCloudBindingSheet } from './azure-cloud-binding-sheet'
+import { CreateAgnetDeploymentSheet } from './create-agnet-deployment-sheet'
type StatusKey = 'running' | 'success' | 'failed' | 'pending'
+type SecretStoreStatus = {
+ configured: boolean
+ reachable: boolean
+}
+
const STATUS_TO_KEY: Record
= {
running: 'running',
active: 'running',
@@ -118,7 +118,7 @@ function StatusBadge({ phase }: { phase: string }) {
return (
@@ -138,12 +138,12 @@ function PageSurface(props: {
-
+
Heicode Manager
{props.title}
{props.subtitle && (
-
+
{props.subtitle}
)}
@@ -162,10 +162,10 @@ function PageSurface(props: {
function EmptySurface(props: { title: string; hint?: string }) {
return (
-
+
{props.title}
{props.hint && (
-
{props.hint}
+
{props.hint}
)}
)
@@ -197,10 +197,10 @@ function MetaPill({
value: string
}) {
return (
-
-
+
+
{label}
-
+
{value}
@@ -253,7 +253,9 @@ function describeScope(dep: AgnetDeployment): string {
)
}
-function collectResourceGrants(dep: AgnetDeployment): Record[] {
+function collectResourceGrants(
+ dep: AgnetDeployment
+): Record[] {
return (
dep.orchestration_plan?.agents
?.flatMap((agent) => agent.resource_grants || [])
@@ -290,7 +292,7 @@ function formatRelativeTime(value: string | undefined): string {
// maskSecretRef shows enough of a secret_ref to identify which vault
// entry it points at, but NEVER the full value. Product docs §10
// "高级展开" + §13.9 forbid leaking plaintext credentials anywhere in
-// the UI. Accepted shapes are vault://path/to/secret or opaque IDs.
+// the UI. Accepted shapes are azkv://... Key Vault refs or opaque IDs.
// We keep the scheme and the first 6 chars of the leaf, then ellipsis.
function maskSecretRef(ref: string | undefined): string {
if (!ref) return '—'
@@ -338,13 +340,13 @@ function RunDetailPanel({ dep }: { dep: AgnetDeployment }) {
-
+
{t('Run detail')}
-
+
{dep.deployment_id}
-
+
{dep.orchestration_plan?.objective ||
dep.orchestration_plan?.template_hint ||
t('No objective')}
@@ -356,7 +358,11 @@ function RunDetailPanel({ dep }: { dep: AgnetDeployment }) {
-
+
-
+
-
-
+
+
{t('Permission manifest')}
-
+
{t('Run manifest hint')}
-
+
{grants.length} {t('resource grants')}
@@ -405,18 +411,14 @@ function RunDetailPanel({ dep }: { dep: AgnetDeployment }) {
-
- {t('Resource')}
-
+ {t('Resource')}
{t('Allowed actions')}
{t('Constraints')}
-
- {t('secret_ref')}
-
+ {t('secret_ref')}
{t('Status')}
@@ -429,8 +431,13 @@ function RunDetailPanel({ dep }: { dep: AgnetDeployment }) {
: []
const constraints =
(g.constraints as Record) || {}
- const constraintEntries = Object.entries(constraints).slice(0, 6)
- const secret = maskSecretRef(g.secret_ref as string | undefined)
+ const constraintEntries = Object.entries(constraints).slice(
+ 0,
+ 6
+ )
+ const secret = maskSecretRef(
+ g.secret_ref as string | undefined
+ )
const status = String(g.status || 'active')
return (
-
+
{resType}
-
+
{resId}
@@ -455,7 +462,7 @@ function RunDetailPanel({ dep }: { dep: AgnetDeployment }) {
{actions.map((a) => (
{a}
@@ -471,10 +478,11 @@ function RunDetailPanel({ dep }: { dep: AgnetDeployment }) {
{constraintEntries.map(([k, v]) => (
- {k}={String(v).length > 24
+ {k}=
+ {String(v).length > 24
? String(v).slice(0, 24) + '…'
: v}
@@ -482,7 +490,7 @@ function RunDetailPanel({ dep }: { dep: AgnetDeployment }) {
)}
-
+
{secret}
@@ -500,7 +508,7 @@ function RunDetailPanel({ dep }: { dep: AgnetDeployment }) {
})}
-
+
{t(
'Plaintext credentials are never shown. The secret_ref column is a vault pointer, not the secret itself.'
)}
@@ -509,21 +517,21 @@ function RunDetailPanel({ dep }: { dep: AgnetDeployment }) {
)}
-
-
-
+
+
+
{t('Events usage')}
-
+
{t('Run events hint')}
-
-
-
+
+
+
{t('Audit usage')}
-
+
{t('Run audit hint')}
@@ -598,7 +606,7 @@ export function AgnetDeploymentsPage() {
{t('New run')}
-
+
setKeyword(e.target.value)}
@@ -632,7 +640,9 @@ export function AgnetDeploymentsPage() {
retry={() => void refetchDeployments()}
loadingFallback={
}
emptyTitle={t('No runs match the current filter')}
- emptyDescription={t('Adjust filters or trigger a new orchestration plan.')}
+ emptyDescription={t(
+ 'Adjust filters or trigger a new orchestration plan.'
+ )}
>
{filtered.map((dep) => {
@@ -656,7 +666,7 @@ export function AgnetDeploymentsPage() {
}
}}
className={cn(
- 'group flex cursor-pointer flex-col gap-3 rounded-2xl border bg-[color-mix(in_oklch,var(--card)_55%,transparent)] p-4 transition hover:border-primary/45 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
+ 'group hover:border-primary/45 focus-visible:ring-ring flex cursor-pointer flex-col gap-3 rounded-2xl border bg-[color-mix(in_oklch,var(--card)_55%,transparent)] p-4 transition focus-visible:ring-2 focus-visible:outline-none',
selected
? 'border-primary/55'
: 'border-[color-mix(in_oklch,var(--primary)_18%,var(--border))]'
@@ -670,14 +680,14 @@ export function AgnetDeploymentsPage() {
-
+
{formatRelativeTime(dep.updated_at || dep.created_at)}
{t('View activity')}
@@ -705,7 +715,7 @@ export function AgnetDeploymentsPage() {
if (!o) setSelectedRunId(undefined)
}}
>
-
+
{selectedRun && (
<>
@@ -754,16 +764,16 @@ function RunAuditTimeline({ deploymentId }: { deploymentId: string }) {
-
+
{t('Audit timeline')}
-
+
{t(
'Every observable transition for this deployment. Survives container restarts (stored in DB).'
)}
-
+
{data.length} {t('events')}
@@ -775,15 +785,13 @@ function RunAuditTimeline({ deploymentId }: { deploymentId: string }) {
) : data.length === 0 ? (
-
+
{t('No audit events yet for this deployment.')}
) : (
{data.map((entry, idx) => {
- const eventName = String(
- entry.event || entry.action || '(unknown)'
- )
+ const eventName = String(entry.event || entry.action || '(unknown)')
const occurred = String(entry.occurred_at || '')
const correlation = String(entry.correlation_id || '')
const level = classifyEventLevel(entry)
@@ -799,7 +807,7 @@ function RunAuditTimeline({ deploymentId }: { deploymentId: string }) {
(entry.event_id as string) ||
`${eventName}-${idx}-${occurred}`
}
- className='flex items-start gap-3 rounded-lg border border-dashed border-[color-mix(in_oklch,var(--primary)_14%,var(--border))] bg-background/40 p-2'
+ className='bg-background/40 flex items-start gap-3 rounded-lg border border-dashed border-[color-mix(in_oklch,var(--primary)_14%,var(--border))] p-2'
>
-
+
{eventName}
-
+
{occurred || '—'}
{correlation && (
<>
@@ -938,55 +946,55 @@ export function AgnetEventsPage() {
emptyTitle={t('No events for the current filter.')}
emptyDescription={t('Switch level or pick another deployment.')}
>
-
- {filteredEvents.map((entry, idx) => {
- const lv = classifyEventLevel(entry as Record)
- const action = String(
- (entry as Record).event ||
- (entry as Record).type ||
- 'event'
- )
- const occurred = String(
- (entry as Record).occurred_at ||
- (entry as Record).timestamp ||
- ''
- )
- const dot =
- lv === 'error'
- ? 'bg-rose-400 ring-rose-500/30'
- : lv === 'warn'
- ? 'bg-amber-400 ring-amber-500/30'
- : 'bg-primary ring-primary/30'
- const Icon =
- lv === 'error'
- ? AlertOctagon
- : lv === 'warn'
- ? Activity
- : GitCommit
- return (
-
-
-
-
-
- {action}
-
-
- {formatRelativeTime(occurred)}
-
-
-
- {JSON.stringify(entry, null, 2)}
-
-
- )
- })}
-
+
+ {filteredEvents.map((entry, idx) => {
+ const lv = classifyEventLevel(entry as Record)
+ const action = String(
+ (entry as Record).event ||
+ (entry as Record).type ||
+ 'event'
+ )
+ const occurred = String(
+ (entry as Record).occurred_at ||
+ (entry as Record).timestamp ||
+ ''
+ )
+ const dot =
+ lv === 'error'
+ ? 'bg-rose-400 ring-rose-500/30'
+ : lv === 'warn'
+ ? 'bg-amber-400 ring-amber-500/30'
+ : 'bg-primary ring-primary/30'
+ const Icon =
+ lv === 'error'
+ ? AlertOctagon
+ : lv === 'warn'
+ ? Activity
+ : GitCommit
+ return (
+
+
+
+
+
+ {action}
+
+
+ {formatRelativeTime(occurred)}
+
+
+
+ {JSON.stringify(entry, null, 2)}
+
+
+ )
+ })}
+
)}
@@ -1025,7 +1033,13 @@ function maskIfSecret(key: string, value: string): string {
const lk = key.toLowerCase()
if (SECRET_FIELD_HINTS.some((h) => lk.includes(h))) {
if (!value) return '—'
- if (value.startsWith('secret_ref:') || value.startsWith('vault:')) return value
+ if (
+ value.startsWith('secret_ref:') ||
+ value.startsWith('azkv:') ||
+ value.startsWith('vault:')
+ ) {
+ return value
+ }
return value.length > 8 ? `${value.slice(0, 4)}…${value.slice(-2)}` : '***'
}
return value
@@ -1064,7 +1078,9 @@ function AuditEntryCard({
}) {
const action = String(entry.action || entry.event || '—')
const actor = String(entry.actor || entry.user || '—')
- const scope = String(entry.binding_scope || entry.tenant || entry.tenant_id || '—')
+ const scope = String(
+ entry.binding_scope || entry.tenant || entry.tenant_id || '—'
+ )
const resourceId = String(entry.resource_id || '—')
const resourceType = String(entry.resource_type || '—')
const allowedActions = Array.isArray(entry.allowed_actions)
@@ -1094,18 +1110,18 @@ function AuditEntryCard({
>
@@ -1117,19 +1133,36 @@ function AuditEntryCard({
-
+
)
}
-function RedactedField({ k, v, mono }: { k: string; v: string; mono?: boolean }) {
+function RedactedField({
+ k,
+ v,
+ mono,
+}: {
+ k: string
+ v: string
+ mono?: boolean
+}) {
return (
-
+
{k}
-
+
{v}
@@ -1198,7 +1231,7 @@ export function AgnetAuditPage() {
>
}
>
-
+
{t(
'Per docs §6: plaintext credentials never appear here. Long-lived secrets live in the secret vault; only secret_ref and redacted previews are shown.'
)}
@@ -1214,7 +1247,11 @@ export function AgnetAuditPage() {
>
{filtered.map((entry, idx) => (
-
} t={t} />
+ }
+ t={t}
+ />
))}
@@ -1250,9 +1287,9 @@ export function AgnetAuditPage() {
// 'project_doc' for docs)
// tenant_id → (dropped — server uses auth.user_id)
// (none) → permission_scope ['repo:read']
-// (none) → secret_ref (left blank for now; server
-// will write vault://... once Secret
-// Broker lands per §2.1 TODO)
+// (none) → secret_ref (Azure Key Vault azkv://...
+// reference when the resource has
+// credential material)
export function AgnetSKSourcesPage() {
const { t } = useTranslation()
const queryClient = useQueryClient()
@@ -1285,6 +1322,35 @@ export function AgnetSKSourcesPage() {
retry: false,
})
const resources = resourcesQuery.data ?? []
+ const managerCloudResourcesQuery = useQuery({
+ queryKey: ['manager', 'cloud-resources'],
+ queryFn: () =>
+ listManagerResources({ status: 'active' }).then((items) =>
+ items.filter(
+ (r) =>
+ r.resource_type === 'cloud_account' ||
+ r.resource_type === 'cloud_resource'
+ )
+ ),
+ retry: false,
+ })
+ const secretStoreQuery = useQuery({
+ queryKey: ['secret-store', 'status'],
+ queryFn: async () => {
+ const res = await api.get<{
+ success: boolean
+ data?: SecretStoreStatus
+ }>('/api/secret-store/status', {
+ skipBusinessError: true,
+ skipErrorHandler: true,
+ } as Record
)
+ return res.data?.data ?? { configured: false, reachable: false }
+ },
+ retry: false,
+ })
+ const keyVaultReady = Boolean(
+ secretStoreQuery.data?.configured && secretStoreQuery.data?.reachable
+ )
const createResourceMutation = useMutation({
mutationFn: () => {
@@ -1336,9 +1402,19 @@ export function AgnetSKSourcesPage() {
// that line and made users wonder which one they were picking.
const docSources = resources.filter((r) => r.type === 'project_doc')
const skSources = resources.filter((r) => r.type === 'sk')
- const cloudSources = resources.filter(
+ const mcpCloudSources = resources.filter(
(r) => r.type === 'cloud_account' || r.type === 'cloud_resource'
)
+ const cloudSources = [
+ ...mcpCloudSources,
+ ...(managerCloudResourcesQuery.data ?? []),
+ ]
+ const managerCloudAccounts = (managerCloudResourcesQuery.data ?? []).filter(
+ (r) => r.resource_type === 'cloud_account'
+ )
+ const managerDiscoveredCloudResources = (
+ managerCloudResourcesQuery.data ?? []
+ ).filter((r) => r.resource_type === 'cloud_resource')
// skSources used to include project_doc rows for "do we have any
// doc-ish source" gating downstream — preserve that contract by
@@ -1381,14 +1457,18 @@ export function AgnetSKSourcesPage() {
key: 'cloud',
title: t('Connect cloud account'),
summary:
- cloudSources.length > 0
- ? t('{{n}} cloud resource connected', { n: cloudSources.length })
- : t('Authorize Azure (AWS / GCP coming soon). Resource auto-discovery later.'),
+ managerDiscoveredCloudResources.length > 0
+ ? t('{{n}} cloud resource connected', {
+ n: managerDiscoveredCloudResources.length,
+ })
+ : managerCloudAccounts.length > 0
+ ? t('{{n}} cloud account connected', {
+ n: managerCloudAccounts.length,
+ })
+ : t(
+ 'Authorize Azure (AWS / GCP coming soon). Heicode auto-discovers cloud resources.'
+ ),
done: cloudSources.length > 0,
- pendingHint:
- cloudSources.length === 0
- ? t('Resource auto-discovery — coming soon')
- : undefined,
},
{
key: 'review',
@@ -1403,8 +1483,7 @@ export function AgnetSKSourcesPage() {
const completed = steps.filter((s) => s.done).length
const total = steps.length
- const prereqsDone =
- projectSources.length > 0 || skOrDocSources.length > 0
+ const prereqsDone = projectSources.length > 0 || skOrDocSources.length > 0
return (
+
{t('{{done}}/{{total}} completed', { done: completed, total })}
@@ -1430,32 +1509,25 @@ export function AgnetSKSourcesPage() {
'mt-0.5 inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-xs font-semibold ring-1 ring-inset',
step.done
? 'bg-emerald-500/15 text-emerald-400 ring-emerald-500/30'
- : 'bg-[color-mix(in_oklch,var(--primary)_12%,transparent)] text-primary ring-primary/30'
+ : 'text-primary ring-primary/30 bg-[color-mix(in_oklch,var(--primary)_12%,transparent)]'
)}
>
{step.done ? : idx + 1}
-
+
{step.title}
{step.optional && (
-
+
{t('optional')}
)}
-
+
{step.summary}
- {step.pendingHint && (
-
- {step.pendingHint}
-
- )}
- {step.key === 'code' ||
- step.key === 'docs' ||
- step.key === 'sk' ? (
+ {step.key === 'code' || step.key === 'docs' || step.key === 'sk' ? (
) : step.key === 'cloud' ? (
- // M2 phase 1 — open the Azure-specific sheet. Auto-discovery
- // ('ARM enumerate') still pending vault wiring; users
- // manually paste SP creds for now.
+ // Open the Azure-specific sheet. Manager stores the secret in
+ // Azure Key Vault, then enumerates ARM resources through the
+ // backend discovery endpoint.
-
-
{t('How this works')}
+
+
{t('How this works')}
{t(
'Long-lived credentials are stored in the secret vault. Agnet only requests short-lived, scoped credentials at run time. Production deploys and other high-risk actions are approved from the desktop client.'
@@ -1524,17 +1596,17 @@ export function AgnetSKSourcesPage() {
{advancedOpen && (
-