0.10.1: 安全与业务逻辑加固、新品牌与部署加固
三轮审查修复(60+ 项),相对远端 main(b536672)的关键变更:
- 安全: 数据面 SSRF 拨号防护(防 DNS rebinding)/上游凭据剥离/登录防枚举
与锁定态统一/可信代理(X-Forwarded-For)限流加固/会话版本失效机制/
撤销即时传播/弱密钥拒绝启动/脱敏字节级重写(保签名契约)
- 业务逻辑: 裸 body 上传 panic/bootstrap 审计管线卡死/定价通配符优先级/
全局工具可见性/调度器停机补跑/TOTP 挑战令牌消费顺序/熔断探针语义/
>4MB 响应 token 计量/管理员重置密码作废会话 等
- 前端: 新 logo(语枢 AI 网关主题)/Provider 凭据异常警示/删除入口/
后端错误消息透传/localStorage 敏感数据收敛
- 部署: CREDENTIAL_MASTER_KEY 持久化与弱值拒绝/Provider DELETE 接口/
nginx 安全头/worker 内存限制
- 新增迁移 000029(key_hash 索引)/000030(usage_daily 币种维度)
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# 重构进度
|
||||
|
||||
更新时间:2026-08-11
|
||||
更新时间:2026-08-12
|
||||
|
||||
## 已完成:M0 工程基线
|
||||
|
||||
@@ -133,3 +133,53 @@
|
||||
- 知识库列表新增 `vectorized_chunk_count`(`count(c.embedding)`),管理端展示"已向量化切片/总切片"覆盖率;创建/编辑知识库可三态选择检索模式,未向量化分块需重新处理才被语义召回。
|
||||
- `EMBEDDINGS_*` 配置(`OLLAMA_BASE_URL` 默认 `http://ollama:11434`、`EMBEDDING_MODEL` bge-m3、`EMBEDDING_DIM` 须与 `vector(1024)` 一致、批大小、超时)写入两个 `.env.example` 与 compose anchor。
|
||||
- 单测 `embedder_test.go`/`retrievers_test.go`(httptest 假 Ollama:批量切分、维度不符、404→pull→重试、降级分发)与集成 `TestKnowledgeVectorLifecycle`(真 pgvector+Ollama:导入即向量化、语义命中、embedder 失败时入库 + `embedding_failed` 事件)全部通过;`go build ./...`、`go vet ./...` 通过。
|
||||
|
||||
## 已完成:M8 基础设施层(P3 定时任务调度器)
|
||||
|
||||
- 迁移 `000026` 建 `gateway.scheduled_tasks` 与 `gateway.scheduled_task_runs`:任务配置和执行历史以 PostgreSQL 为权威源;计划触发使用唯一键防重复入队,pending/running/due 部分索引支撑大队列扫描。
|
||||
- 独立 `gateway-scheduler` worker 解析标准五字段 Cron 与 IANA 时区;到期任务和执行队列均使用 `FOR UPDATE SKIP LOCKED`,支持多副本并发、过期计划合并、执行租约超时回收、有限重试和最终失败事件。
|
||||
- 任务可调用已发布应用或数字员工;数字员工任务可在员工已有绑定内进一步限制 Skills/MCP 子集。执行 API Key 使用 `scheduled-task-api-key` 独立 AEAD purpose 加密,明文不回显。
|
||||
- 支持提示词、变量、会话 ID(保留最近五次成功回答作为上下文)和指定通知通道;完成/最终失败写入 outbox,通知 worker 可只投递指定 Webhook,同时物化为创建者站内信。
|
||||
- 管理 API/页面提供创建、编辑、启停、立即执行、删除和执行历史;RBAC 新增 `scheduled_task:read/manage`,菜单由服务端动态下发。
|
||||
- `TestSchedulerPostgreSQLLifecycle` 连真实 PostgreSQL + 模拟网关验证:加密凭据解密、手动与计划入队、只执行一次、响应落库、完成事件和下一运行时间推进;Cron 与数字员工绑定子集另有单元测试。
|
||||
|
||||
## 已完成:M8 基础设施层(P4 站内消息)
|
||||
|
||||
- 迁移 `000025` 建 `gateway.inbox_messages`:一条消息一个收件人(admin/portal 跨身份表、不设 FK),sender 广播逐收件人落库;部分唯一索引 `(source_event_id, recipient_kind, recipient_user_id) WHERE source_event_id IS NOT NULL`(NULLS NOT DISTINCT)实现事件重放幂等;`(recipient_kind, recipient_user_id, created_at DESC)` 收件箱索引 + 未读部分索引。
|
||||
- `InboxService`:`inboxPlan` 纯函数把 outbox 事件类型映射为站内信草稿(`model_access.requested`→通知全部启用管理员、`model_access.decided`→回执申请用户、`marketplace.installed`→通知安装用户、`knowledge_document.ready/reprocessed/embedding_failed`→通知执行管理员、`scheduled_task.completed/failed`→通知创建者);收件人解析区分直取 payload / 查 `model_access_requests` / 全部启用管理员。未读数以 PostgreSQL 为权威源,Redis 仅 PUBLISH 提示(为未来 SSE 预留)。
|
||||
- 通知 worker 在同一消费循环内物化站内消息(`dispatcher.SetInbox`,为 nil 时不落库、不影响 webhook);物化失败与 webhook 同语义留在 pending 由 reclaim 重试,幂等键保证重放不重复。
|
||||
- admin HTTP:消息中心(`scope=mine/broadcasts`)、未读数、向 portal(可按部门过滤)或全部 admin 广播、单条已读、全部已读;RBAC 新增 `inbox:read`/`inbox:manage`(auditor 仅读),动态菜单下发"站内消息"入口。portal HTTP:个人收件箱、未读徽标(header bar 30s 轮询)、已读/全部已读。
|
||||
- 前端:管理端 `gateway/inbox`(我的消息/已发送广播双 Tab + 广播对话框)、门户端 `portal/inbox`;门户顶栏 mail 铃铛 + 未读角标。
|
||||
- 集成测试 `TestInboxMaterializeAndBroadcast` 连真实库验证:事件物化→未读计数→重放幂等→已读回执→管理员广播→broadcasts 全局列表→全部已读。
|
||||
- 踩坑并修复:集成测试清理 DELETE 中同一参数同时比较 uuid 列与 jsonb text 提取,PG 无法推断类型报 `text = uuid`(SQLSTATE 42883)且 `_, _` 吞错导致重跑残留累加——改为显式 `::uuid`/`::text` 并补全按收件人删除。
|
||||
|
||||
## 已完成:M9 智能体与可观测(P1 LLM Trace)
|
||||
|
||||
- 迁移 `000027` 建 `gateway.agent_traces` 与 `gateway.agent_trace_spans`,并把 `trace_id` 关联到应用/数字员工运行记录;Trace 类型区分 application/digital_employee,span 类型覆盖 model/retrieval/tool。
|
||||
- 应用和数字员工运行链路接入 Trace:每轮受治理模型调用记录 Provider、模型、HTTP 状态、Token、轮次和耗时;知识库检索记录命中数;工具/MCP 执行记录名称、轮次、调用 ID、状态、耗时和错误。调用正文、工具参数和模型回答不写入 Trace。
|
||||
- Trace 写入失败是 best-effort,不改变模型调用响应;Trace 完成时同步回填运行计数、最终状态和错误,保留现有 `application_runs`/`digital_employee_runs` 查询兼容性。
|
||||
- admin API:`/api/v1/admin/traces` 列表过滤(时间、目标、状态、Request ID)与 `/traces/{id}` span 详情;新增 `trace:read` 权限,operator/auditor 可读,动态菜单下发 LLM Trace 页面。
|
||||
- 管理端新增 LLM Trace 时间线页面,展示模型/检索/工具链路、耗时、Token、Provider 和失败原因,明确提示不保存正文。
|
||||
- 验证:`TestTracePostgreSQLLifecycle` 验证存储生命周期;`TestWorkbenchPostgreSQLLifecycle` 验证真实应用运行自动产生模型/检索 Trace。
|
||||
|
||||
## 已完成:M9 智能体与可观测(P2 智能体会话)
|
||||
|
||||
- 复用 Trace 元数据聚合统一智能体会话中心:有 `X-Gateway-Conversation-ID` 的请求按会话聚合,无状态请求按 `request:<request_id>` 回退,区分 application/digital_employee。
|
||||
- admin API:`/api/v1/admin/agent-sessions` 支持时间、类型、目标编码、会话 ID 和数量过滤,返回 Trace 数、最近 Trace、状态、模型/工具/检索累计计数与最近活动时间;不读取或返回对话正文。
|
||||
- 管理端新增“智能体会话”页面,可查看会话聚合指标并打开最近 Trace 时间线;复用 `trace:read` 权限和动态菜单。
|
||||
- 验证:`TestTracePostgreSQLLifecycle` 增加会话聚合查询断言,admin 前端生产构建通过。
|
||||
|
||||
## 已完成:M9 智能体与可观测(P3 节点注册与心跳)
|
||||
|
||||
- 迁移 `000028` 新增 `gateway.agent_nodes`:节点编码、节点类型、Endpoint、公共/私有节点池、能力/元数据、令牌哈希和最后心跳信息;令牌只在登记或轮换响应中返回一次。
|
||||
- admin API:节点列表、登记、编辑、删除和令牌轮换;`agent_node:read/manage` 权限分别授予 auditor/operator,管理端新增节点监控页面。
|
||||
- 节点 API:`POST /api/v1/agent/nodes/{code}/heartbeat` 校验 `X-Agent-Token`,刷新版本、能力、元数据、错误和来源 IP;根据 90 秒心跳窗口派生 pending/online/offline/disabled 状态。
|
||||
- 当前 P3 明确不假装覆盖远程裸机安装、节点任务下发和真实执行路由;公共/私有池字段与能力元数据为下一阶段路由实现保留契约。
|
||||
- 验证:`TestAgentNodePostgreSQLLifecycle` 覆盖登记、编辑、心跳、令牌轮换和旧令牌失效。
|
||||
|
||||
## 已完成:M9 智能体与可观测(P4 节点池路由预览)
|
||||
|
||||
- admin API:`POST /api/v1/admin/agent-nodes/route-preview` 按公有/私有池、池编码、能力标签和 90 秒在线窗口筛选候选节点。
|
||||
- 选路使用 `sha256(request_key + "\x00" + node_id)` 的稳定排序,同一个 request key 在候选集合不变时会得到同一首选节点;响应同时返回候选列表、选路策略和无节点/无能力原因。
|
||||
- 管理端节点页面新增“路由预览”对话框,明确展示首选节点和候选顺序;该接口只读,不访问节点 Endpoint、不创建任务,也不代表远程执行路由已经完成。
|
||||
- 增加纯函数筛选/稳定性测试,并在 `TestAgentNodePostgreSQLLifecycle` 中覆盖真实在线节点的能力路由预览。
|
||||
|
||||
@@ -2,212 +2,11 @@
|
||||
|
||||
> 由 `scripts/compare_route_contracts.py` 通过 AST 与 OpenAPI 自动生成;`replaced` 表示能力有新契约承接,仍需兼容层或客户端迁移,不能视为原路由原样可用。
|
||||
|
||||
- 旧源码路由装饰器:201
|
||||
- 同方法同规范路径:85
|
||||
- 新契约替代:112
|
||||
- 明确退役:4
|
||||
- 旧源码路由装饰器:0
|
||||
- 同方法同规范路径:0
|
||||
- 新契约替代:0
|
||||
- 明确退役:0
|
||||
- 尚无契约:0
|
||||
|
||||
| 状态 | 方法 | 旧路径 | Go 契约/预期目标 | 来源 |
|
||||
|---|---|---|---|---|
|
||||
| 新契约替代 | `GET` | `/admin/api/admins` | `/api/v1/admin/identities/admins` | `admin.py:290` |
|
||||
| 新契约替代 | `POST` | `/admin/api/admins` | `/api/v1/admin/identities/admins` | `admin.py:306` |
|
||||
| 新契约替代 | `POST` | `/admin/api/admins/change_password` | `/api/v1/admin/password` | `admin.py:361` |
|
||||
| 新契约替代 | `DELETE` | `/admin/api/admins/{admin_id}` | `/api/v1/admin/identities/*/{identity_id} active=false` | `admin.py:381` |
|
||||
| 新契约替代 | `PATCH` | `/admin/api/admins/{admin_id}` | `/api/v1/admin/identities/admins/{identity_id} (PUT with full revision payload)` | `admin.py:329` |
|
||||
| 已覆盖 | `GET` | `/admin/api/applications` | `/api/v1/admin/applications` | `applications.py:158` |
|
||||
| 已覆盖 | `POST` | `/admin/api/applications` | `/api/v1/admin/applications` | `applications.py:287` |
|
||||
| 已覆盖 | `GET` | `/admin/api/applications/asset-dependencies` | `/api/v1/admin/applications/asset-dependencies` | `applications.py:172` |
|
||||
| 已覆盖 | `GET` | `/admin/api/applications/catalog` | `/api/v1/admin/applications/catalog` | `applications.py:165` |
|
||||
| 已覆盖 | `DELETE` | `/admin/api/applications/{app_id}` | `/api/v1/admin/applications/{application_id}` | `applications.py:370` |
|
||||
| 新契约替代 | `PATCH` | `/admin/api/applications/{app_id}` | `/api/v1/admin/applications/{application_id} (PUT with full revision payload)` | `applications.py:312` |
|
||||
| 新契约替代 | `GET` | `/admin/api/applications/{app_id}/composition` | `/api/v1/admin/applications/{application_id}` | `applications.py:190` |
|
||||
| 新契约替代 | `PUT` | `/admin/api/applications/{app_id}/composition` | `/api/v1/admin/applications/{application_id}` | `applications.py:202` |
|
||||
| 新契约替代 | `POST` | `/admin/api/applications/{app_id}/keys` | `/api/v1/admin/api-keys (scoped credentials)` | `applications.py:384` |
|
||||
| 已覆盖 | `POST` | `/admin/api/applications/{app_id}/publish` | `/api/v1/admin/applications/{application_id}/publish` | `applications.py:214` |
|
||||
| 已覆盖 | `POST` | `/admin/api/applications/{app_id}/rollback/{version}` | `/api/v1/admin/applications/{application_id}/rollback/{version}` | `applications.py:233` |
|
||||
| 已覆盖 | `GET` | `/admin/api/applications/{app_id}/runs` | `/api/v1/admin/applications/{application_id}/runs` | `applications.py:254` |
|
||||
| 新契约替代 | `POST` | `/admin/api/applications/{app_id}/test` | `/v1/applications/{application_code}/chat/completions` | `applications.py:272` |
|
||||
| 新契约替代 | `GET` | `/admin/api/audit_settings` | `/api/v1/admin/content-policies and /api/v1/admin/notification-channels` | `admin.py:2297` |
|
||||
| 新契约替代 | `PUT` | `/admin/api/audit_settings` | `/api/v1/admin/content-policies and /api/v1/admin/notification-channels` | `admin.py:2340` |
|
||||
| 新契约替代 | `POST` | `/admin/api/audit_settings/test_image_model` | `/api/v1/admin/content-policies and /api/v1/admin/notification-channels` | `admin.py:2379` |
|
||||
| 新契约替代 | `GET` | `/admin/api/content_audit_scopes` | `/api/v1/admin/content-policies` | `admin.py:2413` |
|
||||
| 新契约替代 | `POST` | `/admin/api/content_audit_scopes` | `/api/v1/admin/content-policies` | `admin.py:2436` |
|
||||
| 新契约替代 | `DELETE` | `/admin/api/content_audit_scopes/{scope_id}` | `/api/v1/admin/content-policies` | `admin.py:2470` |
|
||||
| 新契约替代 | `GET` | `/admin/api/cost_report` | `/api/v1/admin/usage/daily and /api/v1/admin/model-prices` | `admin.py:3936` |
|
||||
| 新契约替代 | `GET` | `/admin/api/cost_report/export` | `/api/v1/admin/usage/daily and /api/v1/admin/model-prices` | `admin.py:3967` |
|
||||
| 已覆盖 | `GET` | `/admin/api/departments` | `/api/v1/admin/departments` | `admin.py:550` |
|
||||
| 已覆盖 | `POST` | `/admin/api/departments` | `/api/v1/admin/departments` | `admin.py:565` |
|
||||
| 新契约替代 | `DELETE` | `/admin/api/departments/{dept_id}` | `/api/v1/admin/departments/{department_id} active=false` | `admin.py:615` |
|
||||
| 新契约替代 | `PATCH` | `/admin/api/departments/{dept_id}` | `/api/v1/admin/departments/{department_id} (PUT with full revision payload)` | `admin.py:584` |
|
||||
| 新契约替代 | `GET` | `/admin/api/dept_budgets` | `/api/v1/admin/usage/daily and /api/v1/admin/model-prices` | `admin.py:3781` |
|
||||
| 新契约替代 | `POST` | `/admin/api/dept_budgets` | `/api/v1/admin/usage/daily and /api/v1/admin/model-prices` | `admin.py:3807` |
|
||||
| 新契约替代 | `DELETE` | `/admin/api/dept_budgets/{budget_id}` | `/api/v1/admin/usage/daily and /api/v1/admin/model-prices` | `admin.py:3839` |
|
||||
| 新契约替代 | `GET` | `/admin/api/dept_stats` | `/api/v1/admin/usage/daily and /api/v1/admin/model-prices` | `admin.py:2934` |
|
||||
| 已覆盖 | `GET` | `/admin/api/fact-check/events` | `/api/v1/admin/fact-check/events` | `hallucination.py:390` |
|
||||
| 已覆盖 | `GET` | `/admin/api/fact-check/events/{event_id}` | `/api/v1/admin/fact-check/events/{event_id}` | `hallucination.py:405` |
|
||||
| 已覆盖 | `GET` | `/admin/api/fact-check/policies` | `/api/v1/admin/fact-check/policies` | `hallucination.py:348` |
|
||||
| 已覆盖 | `POST` | `/admin/api/fact-check/policies` | `/api/v1/admin/fact-check/policies` | `hallucination.py:355` |
|
||||
| 已覆盖 | `DELETE` | `/admin/api/fact-check/policies/{policy_id}` | `/api/v1/admin/fact-check/policies/{policy_id}` | `hallucination.py:378` |
|
||||
| 已覆盖 | `GET` | `/admin/api/fact-check/settings` | `/api/v1/admin/fact-check/settings` | `hallucination.py:312` |
|
||||
| 已覆盖 | `PUT` | `/admin/api/fact-check/settings` | `/api/v1/admin/fact-check/settings` | `hallucination.py:323` |
|
||||
| 新契约替代 | `GET` | `/admin/api/feishu_auth` | `/api/v1/admin/identity-providers (generic OIDC/SAML)` | `admin.py:3314` |
|
||||
| 新契约替代 | `POST` | `/admin/api/feishu_auth` | `/api/v1/admin/identity-providers (generic OIDC/SAML)` | `admin.py:3335` |
|
||||
| 新契约替代 | `POST` | `/admin/api/feishu_auth/test` | `/api/v1/admin/identity-providers (generic OIDC/SAML)` | `admin.py:3364` |
|
||||
| 新契约替代 | `GET` | `/admin/api/kb` | `/api/v1/admin/knowledge-bases` | `knowledge.py:395` |
|
||||
| 新契约替代 | `POST` | `/admin/api/kb` | `/api/v1/admin/knowledge-bases` | `knowledge.py:410` |
|
||||
| 新契约替代 | `DELETE` | `/admin/api/kb/{kb_id}` | `/api/v1/admin/knowledge-bases/{knowledge_base_id}` | `knowledge.py:475` |
|
||||
| 新契约替代 | `PATCH` | `/admin/api/kb/{kb_id}` | `/api/v1/admin/knowledge-bases/{knowledge_base_id} (PUT with full revision payload)` | `knowledge.py:439` |
|
||||
| 新契约替代 | `GET` | `/admin/api/kb/{kb_id}/documents` | `/api/v1/admin/knowledge-bases/{knowledge_base_id}/documents` | `knowledge.py:494` |
|
||||
| 新契约替代 | `POST` | `/admin/api/kb/{kb_id}/documents/paste` | `/api/v1/admin/knowledge-bases/{knowledge_base_id}/documents` | `knowledge.py:550` |
|
||||
| 新契约替代 | `POST` | `/admin/api/kb/{kb_id}/documents/upload` | `/api/v1/admin/knowledge-bases/{knowledge_base_id}/documents` | `knowledge.py:508` |
|
||||
| 新契约替代 | `DELETE` | `/admin/api/kb/{kb_id}/documents/{doc_id}` | `/api/v1/admin/knowledge-bases/{knowledge_base_id}/documents/{document_id}` | `knowledge.py:590` |
|
||||
| 新契约替代 | `POST` | `/admin/api/kb/{kb_id}/documents/{doc_id}/reprocess` | `/api/v1/admin/knowledge-bases/{knowledge_base_id}/documents/{document_id}/reprocess` | `knowledge.py:574` |
|
||||
| 新契约替代 | `POST` | `/admin/api/kb/{kb_id}/test_search` | `/api/v1/admin/knowledge-bases/{knowledge_base_id}/search` | `knowledge.py:613` |
|
||||
| 新契约替代 | `GET` | `/admin/api/keys` | `/api/v1/admin/api-keys` | `admin.py:912` |
|
||||
| 新契约替代 | `POST` | `/admin/api/keys` | `/api/v1/admin/api-keys` | `admin.py:943` |
|
||||
| 新契约替代 | `DELETE` | `/admin/api/keys/{key_id}` | `/api/v1/admin/api-keys/{api_key_id}` | `admin.py:1017` |
|
||||
| 新契约替代 | `PATCH` | `/admin/api/keys/{key_id}` | `/api/v1/admin/api-keys/{api_key_id}/limits` | `admin.py:977` |
|
||||
| 新契约替代 | `GET` | `/admin/api/log_search` | `/api/v1/admin/audit-events` | `admin.py:2817` |
|
||||
| 新契约替代 | `GET` | `/admin/api/log_views` | `/api/v1/admin/audit-events` | `admin.py:2879` |
|
||||
| 已覆盖 | `POST` | `/admin/api/login` | `/api/v1/admin/login` | `admin.py:151` |
|
||||
| 已覆盖 | `POST` | `/admin/api/login/totp` | `/api/v1/admin/login/totp` | `admin.py:193` |
|
||||
| 新契约替代 | `DELETE` | `/admin/api/logs` | `/api/v1/admin/audit-events (bounded immutable audit query)` | `admin.py:2633` |
|
||||
| 新契约替代 | `GET` | `/admin/api/logs` | `/api/v1/admin/audit-events` | `admin.py:2540` |
|
||||
| 新契约替代 | `GET` | `/admin/api/logs/search` | `/api/v1/admin/audit-events (bounded immutable audit query)` | `admin.py:2689` |
|
||||
| 新契约替代 | `GET` | `/admin/api/logs/{log_id}/view_history` | `/api/v1/admin/audit-events (bounded immutable audit query)` | `admin.py:2862` |
|
||||
| 明确退役 | `POST` | `/admin/api/logs/{log_id}/view_raw` | `retired: unbounded raw-body access is excluded by storage/compliance ADR` | `admin.py:2653` |
|
||||
| 新契约替代 | `GET` | `/admin/api/malicious_domains` | `/api/v1/admin/content-policies` | `admin.py:2501` |
|
||||
| 新契约替代 | `POST` | `/admin/api/malicious_domains` | `/api/v1/admin/content-policies` | `admin.py:2512` |
|
||||
| 新契约替代 | `DELETE` | `/admin/api/malicious_domains/{domain}` | `/api/v1/admin/content-policies` | `admin.py:2528` |
|
||||
| 已覆盖 | `GET` | `/admin/api/model-requests` | `/api/v1/admin/model-requests` | `admin.py:3506` |
|
||||
| 已覆盖 | `POST` | `/admin/api/model-requests/{req_id}/approve` | `/api/v1/admin/model-requests/{request_id}/approve` | `admin.py:3522` |
|
||||
| 已覆盖 | `POST` | `/admin/api/model-requests/{req_id}/reject` | `/api/v1/admin/model-requests/{request_id}/reject` | `admin.py:3569` |
|
||||
| 新契约替代 | `GET` | `/admin/api/model_aliases` | `/api/v1/admin/model-routes` | `admin.py:2204` |
|
||||
| 新契约替代 | `POST` | `/admin/api/model_aliases` | `/api/v1/admin/model-routes` | `admin.py:2224` |
|
||||
| 新契约替代 | `DELETE` | `/admin/api/model_aliases/{alias_id}` | `/api/v1/admin/model-routes` | `admin.py:2262` |
|
||||
| 新契约替代 | `PUT` | `/admin/api/model_aliases/{alias_id}` | `/api/v1/admin/model-routes` | `admin.py:2240` |
|
||||
| 新契约替代 | `GET` | `/admin/api/model_pricing` | `/api/v1/admin/model-prices` | `admin.py:3615` |
|
||||
| 新契约替代 | `POST` | `/admin/api/model_pricing` | `/api/v1/admin/model-prices` | `admin.py:3646` |
|
||||
| 新契约替代 | `DELETE` | `/admin/api/model_pricing/{pricing_id}` | `/api/v1/admin/model-prices/{price_id}` | `admin.py:3677` |
|
||||
| 新契约替代 | `GET` | `/admin/api/model_qps` | `/api/v1/admin/api-keys and /api/v1/admin/model-routes` | `admin.py:2114` |
|
||||
| 新契约替代 | `POST` | `/admin/api/model_qps` | `/api/v1/admin/api-keys and /api/v1/admin/model-routes` | `admin.py:2125` |
|
||||
| 新契约替代 | `DELETE` | `/admin/api/model_qps/{config_id}` | `/api/v1/admin/api-keys and /api/v1/admin/model-routes` | `admin.py:2147` |
|
||||
| 已覆盖 | `GET` | `/admin/api/models` | `/api/v1/admin/models` | `admin.py:1093` |
|
||||
| 已覆盖 | `GET` | `/admin/api/monitoring/overview` | `/api/v1/admin/monitoring/overview` | `admin.py:3153` |
|
||||
| 新契约替代 | `GET` | `/admin/api/notify_config` | `/api/v1/admin/notification-channels` | `admin.py:3392` |
|
||||
| 新契约替代 | `POST` | `/admin/api/notify_config` | `/api/v1/admin/notification-channels` | `admin.py:3415` |
|
||||
| 新契约替代 | `POST` | `/admin/api/notify_config/test` | `/api/v1/admin/notification-channels` | `admin.py:3447` |
|
||||
| 新契约替代 | `GET` | `/admin/api/pricing_config` | `/api/v1/admin/usage/daily and /api/v1/admin/model-prices` | `admin.py:3702` |
|
||||
| 新契约替代 | `PUT` | `/admin/api/pricing_config` | `/api/v1/admin/usage/daily and /api/v1/admin/model-prices` | `admin.py:3717` |
|
||||
| 已覆盖 | `GET` | `/admin/api/prompts` | `/api/v1/admin/prompts` | `prompts.py:227` |
|
||||
| 已覆盖 | `POST` | `/admin/api/prompts` | `/api/v1/admin/prompts` | `prompts.py:246` |
|
||||
| 新契约替代 | `GET` | `/admin/api/prompts/categories` | `/api/v1/admin/prompt-categories` | `prompts.py:135` |
|
||||
| 新契约替代 | `POST` | `/admin/api/prompts/categories` | `/api/v1/admin/prompt-categories` | `prompts.py:150` |
|
||||
| 新契约替代 | `DELETE` | `/admin/api/prompts/categories/{cat_id}` | `/api/v1/admin/prompt-categories` | `prompts.py:168` |
|
||||
| 已覆盖 | `DELETE` | `/admin/api/prompts/{tpl_id}` | `/api/v1/admin/prompts/{prompt_id}` | `prompts.py:323` |
|
||||
| 已覆盖 | `GET` | `/admin/api/prompts/{tpl_id}` | `/api/v1/admin/prompts/{prompt_id}` | `prompts.py:279` |
|
||||
| 新契约替代 | `PATCH` | `/admin/api/prompts/{tpl_id}` | `/api/v1/admin/prompts/{prompt_id} (PUT with full revision payload)` | `prompts.py:292` |
|
||||
| 新契约替代 | `POST` | `/admin/api/prompts/{tpl_id}/preview` | `/api/v1/admin/prompts/{prompt_id}/render` | `prompts.py:392` |
|
||||
| 已覆盖 | `POST` | `/admin/api/prompts/{tpl_id}/versions` | `/api/v1/admin/prompts/{prompt_id}/versions` | `prompts.py:344` |
|
||||
| 已覆盖 | `POST` | `/admin/api/prompts/{tpl_id}/versions/{version_id}/activate` | `/api/v1/admin/prompts/{prompt_id}/versions/{version}/activate` | `prompts.py:371` |
|
||||
| 已覆盖 | `GET` | `/admin/api/providers` | `/api/v1/admin/providers` | `admin.py:1083` |
|
||||
| 已覆盖 | `POST` | `/admin/api/providers` | `/api/v1/admin/providers` | `admin.py:1106` |
|
||||
| 新契约替代 | `DELETE` | `/admin/api/providers/{provider_id}` | `/api/v1/admin/providers/{provider_id} enabled=false` | `admin.py:1152` |
|
||||
| 新契约替代 | `PATCH` | `/admin/api/providers/{provider_id}` | `/api/v1/admin/providers/{provider_id} (PUT with full revision payload)` | `admin.py:1169` |
|
||||
| 已覆盖 | `PUT` | `/admin/api/providers/{provider_id}` | `/api/v1/admin/providers/{provider_id}` | `admin.py:1128` |
|
||||
| 新契约替代 | `GET` | `/admin/api/providers/{provider_id}/backends` | `/api/v1/admin/providers/{provider_id}/models and /api/v1/admin/model-routes` | `admin.py:1333` |
|
||||
| 新契约替代 | `PUT` | `/admin/api/providers/{provider_id}/backends` | `/api/v1/admin/providers/{provider_id}/models and /api/v1/admin/model-routes` | `admin.py:1358` |
|
||||
| 新契约替代 | `DELETE` | `/admin/api/providers/{provider_id}/backends/{backend_name}` | `/api/v1/admin/providers/{provider_id}/models and /api/v1/admin/model-routes` | `admin.py:1455` |
|
||||
| 新契约替代 | `PATCH` | `/admin/api/providers/{provider_id}/backends/{backend_name}` | `/api/v1/admin/providers/{provider_id}/models and /api/v1/admin/model-routes` | `admin.py:1414` |
|
||||
| 新契约替代 | `POST` | `/admin/api/providers/{provider_id}/fetch_models` | `/api/v1/admin/providers/{provider_id}/models/sync` | `admin.py:1255` |
|
||||
| 已覆盖 | `POST` | `/admin/api/providers/{provider_id}/test` | `/api/v1/admin/providers/{provider_id}/test` | `admin.py:1231` |
|
||||
| 新契约替代 | `GET` | `/admin/api/rate_limit_rules` | `/api/v1/admin/api-keys and /api/v1/admin/model-routes` | `admin.py:2033` |
|
||||
| 新契约替代 | `POST` | `/admin/api/rate_limit_rules` | `/api/v1/admin/api-keys and /api/v1/admin/model-routes` | `admin.py:2044` |
|
||||
| 新契约替代 | `DELETE` | `/admin/api/rate_limit_rules/{rule_id}` | `/api/v1/admin/api-keys and /api/v1/admin/model-routes` | `admin.py:2090` |
|
||||
| 新契约替代 | `PATCH` | `/admin/api/rate_limit_rules/{rule_id}` | `/api/v1/admin/api-keys and /api/v1/admin/model-routes` | `admin.py:2064` |
|
||||
| 已覆盖 | `POST` | `/admin/api/reload` | `/api/v1/admin/reload` | `admin.py:3292` |
|
||||
| 新契约替代 | `GET` | `/admin/api/routing_groups` | `/api/v1/admin/model-routes` | `smart_router.py:309` |
|
||||
| 新契约替代 | `POST` | `/admin/api/routing_groups` | `/api/v1/admin/model-routes` | `smart_router.py:326` |
|
||||
| 新契约替代 | `DELETE` | `/admin/api/routing_groups/{group_id}` | `/api/v1/admin/model-routes` | `smart_router.py:372` |
|
||||
| 新契约替代 | `PATCH` | `/admin/api/routing_groups/{group_id}` | `/api/v1/admin/model-routes` | `smart_router.py:346` |
|
||||
| 新契约替代 | `POST` | `/admin/api/routing_groups/{group_id}/preview` | `/api/v1/admin/model-routes` | `smart_router.py:468` |
|
||||
| 新契约替代 | `POST` | `/admin/api/routing_groups/{group_id}/targets` | `/api/v1/admin/model-routes` | `smart_router.py:390` |
|
||||
| 新契约替代 | `DELETE` | `/admin/api/routing_groups/{group_id}/targets/{target_id}` | `/api/v1/admin/model-routes` | `smart_router.py:448` |
|
||||
| 新契约替代 | `PATCH` | `/admin/api/routing_groups/{group_id}/targets/{target_id}` | `/api/v1/admin/model-routes` | `smart_router.py:422` |
|
||||
| 新契约替代 | `GET` | `/admin/api/rule_dimensions` | `/api/v1/admin/content-policies` | `admin.py:1854` |
|
||||
| 新契约替代 | `GET` | `/admin/api/rules` | `/api/v1/admin/content-policies` | `admin.py:1717` |
|
||||
| 新契约替代 | `POST` | `/admin/api/rules` | `/api/v1/admin/content-policies` | `admin.py:1753` |
|
||||
| 新契约替代 | `POST` | `/admin/api/rules/batch` | `/api/v1/admin/content-policies` | `admin.py:1775` |
|
||||
| 新契约替代 | `DELETE` | `/admin/api/rules/{rule_id}` | `/api/v1/admin/content-policies` | `admin.py:1835` |
|
||||
| 新契约替代 | `PATCH` | `/admin/api/rules/{rule_id}` | `/api/v1/admin/content-policies` | `admin.py:1808` |
|
||||
| 新契约替代 | `GET` | `/admin/api/stats` | `/api/v1/admin/usage/daily and /api/v1/admin/model-prices` | `admin.py:3022` |
|
||||
| 新契约替代 | `GET` | `/admin/api/system_info` | `/api/v1/admin/system-info` | `admin.py:2488` |
|
||||
| 已覆盖 | `GET` | `/admin/api/tools` | `/api/v1/admin/tools` | `tools.py:218` |
|
||||
| 已覆盖 | `POST` | `/admin/api/tools` | `/api/v1/admin/tools` | `tools.py:229` |
|
||||
| 已覆盖 | `DELETE` | `/admin/api/tools/{tool_id}` | `/api/v1/admin/tools/{tool_id}` | `tools.py:305` |
|
||||
| 新契约替代 | `PATCH` | `/admin/api/tools/{tool_id}` | `/api/v1/admin/tools/{tool_id} (PUT with full revision payload)` | `tools.py:260` |
|
||||
| 明确退役 | `GET` | `/admin/api/tools/{tool_id}/headers` | `retired: decrypted secret headers are intentionally write-only` | `tools.py:320` |
|
||||
| 新契约替代 | `POST` | `/admin/api/tools/{tool_id}/test_invoke` | `/api/v1/admin/tools/{tool_id}/test` | `tools.py:338` |
|
||||
| 新契约替代 | `POST` | `/admin/api/totp/backup_codes/regenerate` | `/api/v1/admin/totp/backup-codes/regenerate` | `admin.py:521` |
|
||||
| 已覆盖 | `POST` | `/admin/api/totp/confirm` | `/api/v1/admin/totp/confirm` | `admin.py:464` |
|
||||
| 已覆盖 | `POST` | `/admin/api/totp/disable` | `/api/v1/admin/totp/disable` | `admin.py:487` |
|
||||
| 已覆盖 | `POST` | `/admin/api/totp/setup` | `/api/v1/admin/totp/setup` | `admin.py:439` |
|
||||
| 已覆盖 | `GET` | `/admin/api/totp/status` | `/api/v1/admin/totp/status` | `admin.py:422` |
|
||||
| 新契约替代 | `GET` | `/admin/api/users` | `/api/v1/admin/identities/portal-users` | `admin.py:683` |
|
||||
| 新契约替代 | `POST` | `/admin/api/users` | `/api/v1/admin/identities/portal-users` | `admin.py:721` |
|
||||
| 新契约替代 | `DELETE` | `/admin/api/users/{user_id}` | `/api/v1/admin/identities/*/{identity_id} active=false` | `admin.py:826` |
|
||||
| 新契约替代 | `PATCH` | `/admin/api/users/{user_id}` | `/api/v1/admin/identities/portal-users/{identity_id} (PUT with full revision payload)` | `admin.py:745` |
|
||||
| 新契约替代 | `POST` | `/admin/api/users/{user_id}/approve` | `/api/v1/admin/identities/portal-users/{identity_id} active=true` | `admin.py:788` |
|
||||
| 新契约替代 | `DELETE` | `/admin/api/users/{user_id}/feishu` | `/api/v1/admin/identity-providers (generic identity link lifecycle)` | `admin.py:806` |
|
||||
| 新契约替代 | `POST` | `/admin/api/users/{user_id}/reset_password` | `/api/v1/admin/identities/portal-users/{identity_id} password field` | `admin.py:771` |
|
||||
| 已覆盖 | `GET` | `/admin/api/whoami` | `/api/v1/admin/whoami` | `admin.py:256` |
|
||||
| 明确退役 | `GET` | `/admin/ui` | `Art Design Pro SPA deployment` | `main.py:3838` |
|
||||
| 已覆盖 | `GET` | `/healthz` | `/healthz` | `main.py:3756` |
|
||||
| 已覆盖 | `GET` | `/metrics` | `/metrics` | `main.py:3793` |
|
||||
| 已覆盖 | `GET` | `/portal/api/applications` | `/api/v1/portal/applications` | `applications.py:408` |
|
||||
| 已覆盖 | `POST` | `/portal/api/apps/{app_code}/chat` | `/api/v1/portal/apps/{application_code}/chat` | `orchestration.py:227` |
|
||||
| 已覆盖 | `POST` | `/portal/api/apps/{app_code}/conversations` | `/api/v1/portal/apps/{application_code}/conversations` | `orchestration.py:245` |
|
||||
| 已覆盖 | `GET` | `/portal/api/apps/{app_code}/conversations/{conversation_id}` | `/api/v1/portal/apps/{application_code}/conversations/{conversation_id}` | `orchestration.py:260` |
|
||||
| 已覆盖 | `POST` | `/portal/api/apps/{app_code}/conversations/{conversation_id}/messages` | `/api/v1/portal/apps/{application_code}/conversations/{conversation_id}/messages` | `orchestration.py:275` |
|
||||
| 已覆盖 | `GET` | `/portal/api/catalog` | `/api/v1/portal/catalog` | `portal.py:938` |
|
||||
| 已覆盖 | `GET` | `/portal/api/cost` | `/api/v1/portal/cost` | `portal.py:550` |
|
||||
| 已覆盖 | `GET` | `/portal/api/docs-info` | `/api/v1/portal/docs-info` | `portal.py:690` |
|
||||
| 新契约替代 | `GET` | `/portal/api/feishu/callback` | `/api/v1/portal/sso (generic OIDC/SAML)` | `portal.py:214` |
|
||||
| 新契约替代 | `GET` | `/portal/api/feishu/enabled` | `/api/v1/portal/sso (generic OIDC/SAML)` | `portal.py:189` |
|
||||
| 新契约替代 | `GET` | `/portal/api/feishu/login` | `/api/v1/portal/sso (generic OIDC/SAML)` | `portal.py:195` |
|
||||
| 已覆盖 | `GET` | `/portal/api/knowledge` | `/api/v1/portal/knowledge` | `portal.py:830` |
|
||||
| 已覆盖 | `POST` | `/portal/api/login` | `/api/v1/portal/login` | `portal.py:76` |
|
||||
| 已覆盖 | `POST` | `/portal/api/login/totp` | `/api/v1/portal/login/totp` | `portal.py:123` |
|
||||
| 已覆盖 | `POST` | `/portal/api/logout` | `/api/v1/portal/logout` | `portal.py:181` |
|
||||
| 已覆盖 | `GET` | `/portal/api/logs` | `/api/v1/portal/logs` | `portal.py:597` |
|
||||
| 已覆盖 | `GET` | `/portal/api/logs/{log_id}` | `/api/v1/portal/logs/{audit_event_id}` | `portal.py:657` |
|
||||
| 已覆盖 | `GET` | `/portal/api/me` | `/api/v1/portal/me` | `portal.py:432` |
|
||||
| 已覆盖 | `GET` | `/portal/api/model-requests` | `/api/v1/portal/model-requests` | `portal.py:803` |
|
||||
| 已覆盖 | `POST` | `/portal/api/model-requests` | `/api/v1/portal/model-requests` | `portal.py:754` |
|
||||
| 已覆盖 | `GET` | `/portal/api/model-requests/available` | `/api/v1/portal/model-requests/available` | `portal.py:716` |
|
||||
| 已覆盖 | `POST` | `/portal/api/password` | `/api/v1/portal/password` | `portal.py:273` |
|
||||
| 已覆盖 | `GET` | `/portal/api/prompts` | `/api/v1/portal/prompts` | `portal.py:855` |
|
||||
| 已覆盖 | `GET` | `/portal/api/prompts/{tpl_id}` | `/api/v1/portal/prompts/{prompt_id}` | `portal.py:887` |
|
||||
| 已覆盖 | `DELETE` | `/portal/api/prompts/{tpl_id}/favorite` | `/api/v1/portal/prompts/{prompt_id}/favorite` | `portal.py:926` |
|
||||
| 已覆盖 | `POST` | `/portal/api/prompts/{tpl_id}/favorite` | `/api/v1/portal/prompts/{prompt_id}/favorite` | `portal.py:910` |
|
||||
| 已覆盖 | `GET` | `/portal/api/stats` | `/api/v1/portal/stats` | `portal.py:457` |
|
||||
| 已覆盖 | `GET` | `/portal/api/tools` | `/api/v1/portal/tools` | `portal.py:838` |
|
||||
| 新契约替代 | `POST` | `/portal/api/totp/backup_codes/regenerate` | `/api/v1/portal/totp/backup-codes/regenerate` | `portal.py:409` |
|
||||
| 已覆盖 | `POST` | `/portal/api/totp/confirm` | `/api/v1/portal/totp/confirm` | `portal.py:354` |
|
||||
| 已覆盖 | `POST` | `/portal/api/totp/disable` | `/api/v1/portal/totp/disable` | `portal.py:377` |
|
||||
| 已覆盖 | `POST` | `/portal/api/totp/setup` | `/api/v1/portal/totp/setup` | `portal.py:328` |
|
||||
| 已覆盖 | `GET` | `/portal/api/totp/status` | `/api/v1/portal/totp/status` | `portal.py:310` |
|
||||
| 明确退役 | `GET` | `/portal/ui` | `Art Design Pro SPA deployment` | `main.py:3846` |
|
||||
| 已覆盖 | `GET` | `/readyz` | `/readyz` | `main.py:3762` |
|
||||
| 新契约替代 | `POST` | `/v1/apps/{app_code}/chat/completions` | `/v1/applications/{application_code}/chat/completions` | `orchestration.py:213` |
|
||||
| 已覆盖 | `POST` | `/v1/chat/completions` | `/v1/chat/completions` | `main.py:2502` |
|
||||
| 已覆盖 | `POST` | `/v1/embeddings` | `/v1/embeddings` | `main.py:2541` |
|
||||
| 已覆盖 | `POST` | `/v1/knowledge/search` | `/v1/knowledge/search` | `main.py:3543` |
|
||||
| 已覆盖 | `POST` | `/v1/messages` | `/v1/messages` | `main.py:2515` |
|
||||
| 已覆盖 | `GET` | `/v1/models` | `/v1/models` | `main.py:3512` |
|
||||
| 已覆盖 | `GET` | `/v1/prompts` | `/v1/prompts` | `main.py:3657` |
|
||||
| 已覆盖 | `POST` | `/v1/prompts/{prompt_name}/render` | `/v1/prompts/{prompt_name}/render` | `main.py:3680` |
|
||||
| 已覆盖 | `POST` | `/v1/responses` | `/v1/responses` | `main.py:2530` |
|
||||
| 已覆盖 | `GET` | `/v1/tools` | `/v1/tools` | `main.py:3601` |
|
||||
| 已覆盖 | `POST` | `/v1/tools/{tool_name}/invoke` | `/v1/tools/{tool_code}/invoke` | `main.py:3610` |
|
||||
|
||||
@@ -0,0 +1,364 @@
|
||||
# 安全审查与修复报告(0.10.1 修订)
|
||||
|
||||
审查范围:`internal/*`(29k 行 Go)、`migrations/`、`deploy/`、`api/openapi/`。
|
||||
审查方式:4 路并行深度代码审查 + 关键发现人工复核 + 全量单元测试/`go vet`。
|
||||
日期:2026-08-12。以下条目均已在代码中修复并验证;未修复项作为已知限制列出。
|
||||
|
||||
## 高危(已修复)
|
||||
|
||||
### 1. 数据平面 SSRF:DNS rebinding 可把代理流量引向内网
|
||||
`internal/gateway/proxy.go` 的 `http.Transport` 原先使用普通 `net.Dialer`,
|
||||
Provider 主机名被 DNS 重绑到 169.254.169.254 / 10.x 后,网关会把带完整
|
||||
Prompt 的请求转发到内网并回传响应。控制面有 `safeDialContext`,数据面没有。
|
||||
**修复**:`provider.SafeDialContext`(拨号前对所有解析地址做公网校验、直连
|
||||
已校验 IP 防止二次解析)统一供控制面与数据面使用;`Proxy` 新增
|
||||
`SetAllowPrivateProviderURLs`,由 `ALLOW_PRIVATE_PROVIDER_URLS` 控制。
|
||||
|
||||
### 2. 网关 API Key 泄露给上游
|
||||
`internal/provider/openai/adapter.go` 的 `Prepare` 只在配置了上游凭据时
|
||||
`Set Authorization`,从不剥离客户端头——Provider 无 API Key 时,客户端
|
||||
用于认证网关的完整 Bearer Key 被原样转发给上游。
|
||||
**修复**:无条件 `Header.Del("Authorization")` 后再按需注入。
|
||||
|
||||
### 3. SSRF 判定网段不完整(CGNAT/6to4/NAT64 可绕过)
|
||||
`isPublicAddress` 只用了 Go 内建分类,漏掉 100.64.0.0/10、198.18.0.0/15、
|
||||
192.0.2.0/24、192.88.99.0/24、240.0.0.0/4、2002::/16(6to4 内嵌 IPv4)、
|
||||
64:ff9b::/96(NAT64)等;且存在两份重复实现(provider 与 controlplane)。
|
||||
**修复**:导出 `provider.IsPublicAddress` 单点实现,补齐全部特殊用途网段,
|
||||
v4-mapped v6 先 `Unmap()` 再判定;两处调用统一。
|
||||
|
||||
### 4. 应用/数字员工运行把所有网关错误伪装成 200
|
||||
`internal/workbench/runtime_http.go` 的 `boundedRecorder` 初始 `code=200`,
|
||||
`WriteHeader(4xx/5xx)` 变成 no-op:鉴权 401、限流 429、上游 502 全部被
|
||||
记录为成功,`callGateway` 的错误分支是死代码,审计/trace/事实核验数据被
|
||||
污染。
|
||||
**修复**:`code` 初始化为 0,`Write` 兜底 200;错误分支恢复生效。
|
||||
|
||||
### 5. 登录枚举 + 锁定状态泄露(响应差异 + 计时侧信道)
|
||||
`internal/identity`:未知账号 401 / 停用 403 / 锁定 429(精确倒计时)三态
|
||||
可枚举全部管理员/门户账号及其安全状态;停用/锁定账号不执行 dummy 哈希,
|
||||
响应时间还泄露"账号是否存在"。
|
||||
**修复**:登录路径四种失败(未知/停用/锁定/口令错)统一返回 401
|
||||
"账号或口令错误";停用/锁定账号同样执行 600k 次 dummy PBKDF2,计时一致。
|
||||
|
||||
### 6. 登录限流可被伪造 X-Forwarded-For 绕过
|
||||
`ClientIP` 无条件采信 XFF 首值;dev compose 把网关端口暴露在 0.0.0.0,
|
||||
直连时每请求换一个 XFF 值即可旋转 `gateway:login-limit:<ip>` 键。
|
||||
**修复**:新增 `TRUSTED_PROXIES`(默认环回+RFC1918+ULA,覆盖 compose 的
|
||||
nginx 同网段场景),仅可信对端携带的 XFF 被采信;dev compose 网关端口
|
||||
改绑 `127.0.0.1:8080`。
|
||||
|
||||
### 7. 凭据变更后旧会话继续有效
|
||||
会话是独立 Redis 键,无账号级版本:改密/停用 2FA 后,被盗会话在 TTL
|
||||
(12h)内一直有效,强制改密形同虚设。
|
||||
**修复**:`Principal.AuthVersion` + Redis 计数键(TTL 7 天 > 会话 12h,
|
||||
保证版本键过期时旧会话已自然消亡)。改密、TOTP 启用/停用、备用码重生成
|
||||
均 `BumpAuthVersion`;会话与 TOTP 挑战校验版本。
|
||||
|
||||
### 8. Webhook 投递失败被吞掉(无重试、无死信)
|
||||
`notifications.go` `deliver` 失败只记日志,事件照常 ACK——与注释声称的
|
||||
"reclaim 重试"相悖,通知实际是 at-most-once。
|
||||
**修复**:投递失败让事件留在 pending 由 reclaim 重试;单条投递记录
|
||||
`attempts >= 10` 后放弃自动重试(保留 failed 状态供管理端人工重试),
|
||||
防止毒 Webhook 永久卡死事件队列。
|
||||
|
||||
## 中危(已修复)
|
||||
|
||||
### 9. API Key 认证热路径全表扫描
|
||||
`api_keys` 只有 `(key_prefix, key_hash)` 复合索引,`WHERE key_hash=$1`
|
||||
无法使用 → 每个代理请求全表扫描。
|
||||
**修复**:迁移 `000029` 新增 `UNIQUE (key_hash)` 索引。
|
||||
|
||||
### 10. 撤销/改限流的缓存传播窗口与误报
|
||||
缓存 TTL 30s + `Invalidate` 失败时 DB 已生效却向管理员报错(重试会造成
|
||||
混乱)。
|
||||
**修复**:`Invalidate` 失败降级为 Warn 日志(DB 是权威源);缓存 TTL 降至
|
||||
10s 作纵深防御。
|
||||
|
||||
### 11. Token 配额提交脚本可重建无 TTL 残留键
|
||||
`SET ... KEEPTTL` 在键已过期(跨月)时创建永不过期的新键。
|
||||
**修复**:`EXISTS` 为 0 时放弃回写。
|
||||
|
||||
### 12. 熔断器忽略 HTTP 500
|
||||
只有 502/503/504 计入熔断失败,持续 500 的上游永远不熔断且 `success()`
|
||||
不断清零失败计数。
|
||||
**修复**:`failureResult` 将所有 5xx 计入熔断;重试仍只针对 502/503/504
|
||||
与可重放请求。
|
||||
|
||||
### 13. Outbox 退避在 attempt≥35 溢出为负延迟
|
||||
`2^(attempt-1)` 秒转 `time.Duration` 溢出 → `MarkFailed` 把 available_at
|
||||
设到过去 → 事件被立即重新认领,形成热循环烧掉重试预算(`MAX_ATTEMPTS`
|
||||
允许到 100)。
|
||||
**修复**:`attempt > 30` 直接返回上限。
|
||||
|
||||
### 14. 调度器停机期间漏掉所有中间执行
|
||||
`scheduleDue` 只补最新一次,`* * * * *` 任务宕 30 分钟丢 29 次执行。
|
||||
**修复**:从旧 `next_run_at` 起逐个 occurrence 补跑(单任务单轮上限
|
||||
`catchUpLimit=100`,防长期停机瞬间灌入海量记录)。
|
||||
|
||||
### 15. 多字节截断产生无效 UTF-8,事件永久卡死
|
||||
scheduler/trace/notifications 按字节截断 `[:1000]`,切半多字节 rune 后
|
||||
PostgreSQL `text` 列拒绝写入 → 重试循环永远失败;inbox 的 Go `len()`
|
||||
(字节)与 DB `length()`(字符)口径不一致,4000 字节中文正文超 4000
|
||||
字符 CHECK。
|
||||
**修复**:全部改为 rune 边界安全截断;inbox 落库前按 rune 数截断
|
||||
(title 256 / body 4000 / link 512)。
|
||||
|
||||
### 16. 管理端部分更新静默重置角色/状态
|
||||
只改显示名的 PUT 把 superadmin 降为 operator、把停用部门重新激活(绕过
|
||||
停用保护)。
|
||||
**修复**:`role` 留空 = 不修改(仅创建时默认 operator/member);`active`
|
||||
省略时保留当前值(部门与账号一致)。
|
||||
|
||||
### 17. Agent 节点心跳可枚举 + 令牌非恒定时间比较
|
||||
`WHERE code AND token_hash` 单条 UPDATE:404 vs 401 区分有效 code。
|
||||
**修复**:先按 code 取哈希、`subtle.ConstantTimeCompare`,未知 code 与
|
||||
错误令牌统一 `ErrInvalidToken`;`Endpoint` 增加 http(s) 绝对 URL 格式校验。
|
||||
|
||||
### 18. MCP 服务器内部端点泄露给门户用户
|
||||
`marketplace.Detail` 原样返回 `endpoint_url`/`has_secret_headers`,任何
|
||||
登录门户用户可读取内网服务拓扑。
|
||||
**修复**:门户详情脱敏这两个字段(管理端与运行时列表不受影响)。
|
||||
|
||||
### 19. usage_daily 多币种成本混算
|
||||
价格支持任意货币,但日聚合无币种维度,USD 与其它币种相加成单一数字。
|
||||
**修复**:迁移 `000030` 将 `currency char(3)` 并入 PK;recorder 按币种
|
||||
聚合,查询视图返回 `currency`。
|
||||
|
||||
### 20. Content policy 脱敏整包重编码
|
||||
`json.Marshal` 重编码改变键序/数字格式/HTML 转义,破坏上游签名与字节
|
||||
级契约。
|
||||
**修复**:`redactBytes` 状态机在原始字节上仅对 textual 字段值字符串做
|
||||
替换,其余字节零改动(语义与 walkText 一致,含嵌套/数组上下文传播)。
|
||||
|
||||
### 21. 事实核验不作用于数字员工
|
||||
`runApplication` 有 `applyFactCheck`,`runDigitalEmployee` 没有——`block`
|
||||
策略可被数字员工入口绕过。
|
||||
**修复**:数字员工响应同样执行事实核验管线。
|
||||
|
||||
### 22. 其它(批量)
|
||||
- `shadow` 中间件:handler panic(如 `http.ErrAbortHandler`)泄漏并发槽位
|
||||
→ 新增 completed 标志在 panic 路径归还。
|
||||
- 审计关闭 drain 只试一次 → 3 次有限重试 + 失败明确日志。
|
||||
- Trace 表无保留 → `TRACE_RETENTION`(默认 90 天)由 maintenance worker
|
||||
清理(spans 靠 ON DELETE CASCADE)。
|
||||
- 广播(inbox)非事务逐行插入 → 单事务包裹,中途失败不留半套消息。
|
||||
- `visible()` 空 APIKeyID fail-open → fail-closed。
|
||||
- 文件下载缺 `X-Content-Type-Options: nosniff`(用户可控 Content-Type)。
|
||||
- Provider 管理错误原样返回内网解析地址 → `ErrBlockedAddress` 哨兵,
|
||||
客户端只收通用提示,细节进服务端日志。
|
||||
- 备用码生成 `%31` 取模偏差 → 拒绝采样(丢弃 ≥248)。
|
||||
- OIDC 每次登录拉 discovery+JWKS → JWKS 按 provider 缓存 5 分钟。
|
||||
- MCP 客户端缓存不随服务器编辑失效 → 缓存键含 `revision`。
|
||||
- 凭据轮换单条损坏阻塞全部 → 跳过并计数。
|
||||
- `auditSpan.capture` 无锁写 → 纳入锁。
|
||||
- `proxyFor` 并发 cache-miss 重复构建 → `LoadOrStore` 单飞。
|
||||
- `max_tool_rounds=0`(未配置)导致应用首个工具调用即失败 → 默认 5。
|
||||
|
||||
## 部署配置(已修复)
|
||||
|
||||
- **`CREDENTIAL_MASTER_KEY` 全零默认值**(compose):任何拿到仓库的人可
|
||||
解密全部 Provider 凭据/TOTP 密钥/Webhook 签名。compose 改为
|
||||
`${CREDENTIAL_MASTER_KEY:?...}` 强制设置;`ValidateRuntime` 拒绝已知弱
|
||||
密钥(所有环境)。
|
||||
- 网关端口 8080 暴露 0.0.0.0 → 绑定 `127.0.0.1`(nginx 是唯一外部入口)。
|
||||
- nginx:`server_tokens off` + `X-Content-Type-Options`/`X-Frame-Options`/
|
||||
`Referrer-Policy` 安全头。
|
||||
- worker 服务加内存限制(`deploy.resources.limits`)。
|
||||
- 本地 compose 的 PostgreSQL/MinIO 弱口令为本地开发默认,README 已注明
|
||||
生产必须覆盖(生产 compose 已强制要求)。
|
||||
|
||||
## 已知限制(未修复,建议后续处理)
|
||||
|
||||
1. **审计维护大锁**(`audit/maintenance.go`):分区 drop/move 全程持
|
||||
`ACCESS EXCLUSIVE`,flush 会阻塞。建议把空分区创建移出锁区间、仅
|
||||
detach/move/attach 留在锁内,并分批删除。
|
||||
2. **内容策略单条规则失败冻结全部策略**:`Reload` 时任一规则编译失败会
|
||||
中止整个快照刷新(保存时 `Validate` 已缓解)。建议 per-policy skip。
|
||||
3. **无定价模型静默按 0 计费**:`pricing.Calculate` 未命中返回零成本且无
|
||||
信号。建议未定价模型加日志/指标或可配置默认价。
|
||||
4. **outbox 事件丢失预算**:Redis Stream `MAXLEN ~100k` 裁剪 + worker 宕机
|
||||
超预算时事件永久丢失(无回放)。建议 worker 启动时按
|
||||
`published_stream_id` 从 outbox 表补发。
|
||||
5. **audit 幂等**:`audit_events` 无唯一键,响应丢失重试会重复落审计行。
|
||||
可用 `request_id + recorded_at` 建唯一约束。
|
||||
6. **Outbox marker 30 天 Redis 内存税**:高吞吐下 `published:*` 键累积。
|
||||
7. **多实例 JWKS/会话版本键无 TTL 清理**:auth-version 键 7 天 TTL 已缓解。
|
||||
8. **`UPSTREAM_FALLBACK_ENABLED` 是空转**:fallback adapter 与主 adapter
|
||||
相同,没有真正的第二个上游。建议支持第二上游 URL/凭据或移除该开关。
|
||||
9. **OpenAPI 漂移**:`scripts/check_openapi_routes.py` 报 52 条已实现路由
|
||||
未收录 OpenAPI(admin files/inbox/marketplace/skills、portal marketplace、
|
||||
`/v1/mcp-servers` 等)。建议把检查脚本接入 CI 并补齐文档。
|
||||
10. **`/metrics` 未认证**:仅靠网络位置保护(dev compose 已回环绑定),
|
||||
生产建议放内网或加访问控制。
|
||||
11. **scheduled_task_runs.response 无保留**:每次执行持久化最多 2 MiB
|
||||
响应。建议只存截断预览或加清理任务。
|
||||
12. **agent_traces 会话聚合无预聚合表**:大表上 GROUP BY 查询慢。
|
||||
|
||||
## 验证
|
||||
|
||||
- `go build ./...`、`go vet ./...` 通过。
|
||||
- `go test ./...` 全量单元测试通过(含 gateway/identity/scheduler/
|
||||
workbench/contentpolicy/audit/outbox/provider/shadow)。
|
||||
- 新增测试:XFF 可信代理/不可信对端、无可信代理时的回退行为。
|
||||
- 迁移 `000029`(key_hash 索引)、`000030`(usage_daily currency)在
|
||||
真实 PostgreSQL 上执行通过。
|
||||
- Docker Compose 全栈(API + 5 workers + admin/portal 前端 + 依赖)启动
|
||||
健康,`/healthz` `/readyz` 正常。
|
||||
|
||||
---
|
||||
|
||||
# 追加:业务逻辑正确性专项审查(第二轮,2026-08-13)
|
||||
|
||||
以功能正确性视角(算法边界/算术/并发语义/协议语义/事务一致性)对全模块复核,
|
||||
在安全审查基础上新增修复:
|
||||
|
||||
## 已修复
|
||||
|
||||
1. **[高] 裸 body 上传 nil reader panic**(`files_admin_http.go`/`portal_files_http.go`):
|
||||
非 multipart Content-Type 上传时 body 保持 nil,`io.LimitReader(nil,...)` 在
|
||||
`Upload` 中 panic(net/http 恢复后每个此类请求 500 + panic 日志)。
|
||||
修复:非 multipart 时回退 `r.Body`。
|
||||
2. **[高] Bootstrap key 流量永久卡死审计管线**(`audit/recorder.go`):
|
||||
bootstrap 身份 `APIKeyID="bootstrap"` 非 UUID,写入
|
||||
`usage_daily.api_key_id uuid NOT NULL REFERENCES api_keys` 使整批事务失败、
|
||||
pending 永不清理、审计队列填满后所有事件被丢弃。
|
||||
修复:`uuidPointer` 对非 UUID 返回 NULL;日聚合跳过非 UUID 身份(审计
|
||||
事件本身仍落 audit_events)。
|
||||
3. **[高] 定价通配符优先级错误**(`pricing/service.go`):通配符只按生效日期
|
||||
排序,`gpt-4o-mini` 可能命中更宽泛的 `gpt-4*` 价格。
|
||||
修复:通配符按前缀长度降序(更具体优先)再按生效日期。
|
||||
4. **[高] 全局工具对所有人生效不可见**(`runtime_http.go visible()`):
|
||||
`len(departments)==0` 对 secure 资源返回 false——无部门限定的工具/MCP
|
||||
全部消失,绑定它们的应用运行时失败且无配置期报错。
|
||||
修复:无部门限定=全局资源,所有已认证主体可见;部门匹配只约束有部门
|
||||
限定的资源。
|
||||
5. **[高] 调度任务可配置永远无法执行的目标**(`scheduler/service.go`):
|
||||
管理员创建的 API Key 无 tenant,对部门限定应用/员工运行时一律 404/403,
|
||||
但保存时无任何校验。
|
||||
修复:`validateTarget` 增加 `requireKeyTenant` 预检——目标部门限定时,
|
||||
任务 API Key 必须存在、启用且 tenant 匹配,否则保存即报错。
|
||||
6. **[中] 路由权重合计 0 → 整数除零 panic**(`runtime/resolver.go`):weight≤0
|
||||
的脏数据行(绕过管理端校验)使 `Sum64()%0` panic。
|
||||
修复:权重池过滤非正权重;全非正时视为无匹配。
|
||||
7. **[中] 重试路径 nil `GetBody()` panic**(`gateway/resilience.go`):GET/HEAD
|
||||
携带未设置 GetBody 的 body 时重试调用 nil 方法。
|
||||
修复:GetBody 缺失时放弃重试(避免空体重发)。
|
||||
8. **[中] TOTP 挑战令牌双重使用竞态**(`identity/session.go`):两个并发请求
|
||||
用同一 pending 令牌 + 各自有效的不同因子可铸出两个会话。
|
||||
修复:`AuthenticatePending` 改 `GetDel` 原子消费。
|
||||
9. **[中] 事实核验策略作用域失效**(`factcheck/engine.go` + workbench):
|
||||
`ORDER BY scope LIMIT 1` 只执行字典序第一条策略,其余部门策略被静默忽略
|
||||
或张冠李戴。
|
||||
修复:`Check` 增加 scope 参数,`enabledPolicy` 优先精确匹配
|
||||
`department:<uuid>`、回退 global;workbench 从应用/员工部门推导 scope。
|
||||
10. **[低] 熔断器陈旧成功关闭刚打开的电路**(`gateway/resilience.go`):
|
||||
打开前通过 allow() 的在途请求成功会清零失败计数并清除 openUntil。
|
||||
修复:仅半开探针成功可关闭电路;关闭态成功只清零计数。
|
||||
11. **[低] MCP 工具缓存读竞态**(`mcp_client.go`):tools/toolsAt 无锁读。
|
||||
修复:读写统一持锁。
|
||||
12. **[低] 心跳 UPDATE 不复查 token_hash**(`agentnode/store.go`):轮换瞬间
|
||||
旧令牌仍可更新一次节点元数据。
|
||||
修复:UPDATE 条件恢复 `token_hash=$2`。
|
||||
13. **[低] 卸载已停用/归档资源失败**(`marketplace.go`):publishedResourceID
|
||||
要求 enabled+published,管理员停用后用户无法卸载,安装行永久卡死。
|
||||
修复:Uninstall 改用不限制状态的 `resourceIDByCode`。
|
||||
|
||||
## 复核为正确的关键逻辑(节选)
|
||||
|
||||
- 限流 Lua:无 off-by-one(限额 N 精确放行 N 次);月配额键含月份自动翻转。
|
||||
- Token 配额 reserve/commit/refund 对账数学正确、并发安全;跨月提交安全跳过。
|
||||
- `usageSession.finish` sync.Once 保证恰好执行一次;SSE/JSON usage 提取路径
|
||||
无双重计数。
|
||||
- 审计 recorder 队列/pending 无丢失无重复(事务内 CopyFrom+聚合原子提交)。
|
||||
- 分区维护 SQL 边界([start,end)、drop 判定)正确。
|
||||
- PBKDF2/TOTP/备用码密码学实现正确(±1 窗口、原子防重放、拒绝采样)。
|
||||
- 调度器补跑循环无 off-by-one;outbox claim/发布/标记与去重正确。
|
||||
- 会话 AuthVersion 语义正确:bump 后新登录签发新版本、旧会话 fail-closed。
|
||||
|
||||
---
|
||||
|
||||
# 追加:网关接入报错修复与部署密钥持久化(2026-08-13)
|
||||
|
||||
## 用户报告的问题
|
||||
管理端「网关接入」页面提示"服务器内部错误,请稍后重试"。
|
||||
|
||||
## 根因
|
||||
`GET /api/v1/admin/providers` 对每条记录解密凭据,**任何一条解密失败
|
||||
(AES-GCM tag 校验失败)都会让整个列表返回 400**。本环境每次重建部署都
|
||||
重新生成随机 `CREDENTIAL_MASTER_KEY`,旧 key 加密的 Provider 记录全部
|
||||
无法解密 → 列表接口 400 → 前端显示"服务器内部错误"。
|
||||
|
||||
## 修复
|
||||
1. **[bug] Provider 列表降级展示**(`provider/admin_http.go view()`):
|
||||
单条凭据解密失败不再使整个列表报错,改为 `credential_error` 警示字段
|
||||
返回 200,管理端仍可编辑/删除该记录恢复。
|
||||
2. **[功能缺口] Provider 缺少删除接口**:新增
|
||||
`DELETE /api/v1/admin/providers/{id}`(`repository.Delete` 事务 + outbox
|
||||
`provider.deleted` 事件;provider_models/model_routes 由 FK 级联清理)。
|
||||
3. **[部署] master key 持久化**:本地 compose 首次启动生成
|
||||
`deploy/.env`(compose 自动读取)并复用,重启/重建不再换 key;
|
||||
`.gitignore` 排除 `deploy/.env`;`deploy/PRODUCTION.md` 增加持久化与
|
||||
轮换说明。
|
||||
|
||||
## 验证
|
||||
- 列表接口对损坏记录返回 200 + `credential_error` 警示(旧代码返回 400)。
|
||||
- DELETE 接口删除成功,级联清理正常。
|
||||
- 用持久 key 重建 Provider 后**容器重启凭据仍可解密**
|
||||
(`key_configured: true, credential_error: ''`),resolver 快照刷新无错误。
|
||||
- 全量单测 24 包通过。
|
||||
|
||||
---
|
||||
|
||||
# 追加:第三轮审查(前端 + 回归,2026-08-13)
|
||||
|
||||
## 前端审查发现并修复(web/apps/admin + portal + api-client)
|
||||
|
||||
1. **[高] 凭据异常不可见**:后端新增的 `credential_error` 字段前端未展示,
|
||||
运维无法发现 KEK 失配/损坏的 Provider。修复:`ProviderRecord` 增加字段,
|
||||
凭据列对异常记录显示红色警示 Tag(tooltip 展示原因)。
|
||||
2. **[高] localStorage 持久化敏感数据**:Pinia persist 全量落盘
|
||||
(accessToken/refreshToken/账户信息/锁屏密码密文),任何 XSS 直接窃取。
|
||||
修复:改为 sessionStorage + `pick` 白名单(仅 isLogin/token/language),
|
||||
账户信息与锁屏密码不再持久化。
|
||||
3. **[中] 后端错误消息被吞**:`handleError` 优先用状态码通用文案,400/429
|
||||
显示"服务器内部错误",校验失败原因不可见。修复:后端 `msg` 优先;
|
||||
补齐 400/429 文案与枚举。
|
||||
4. **[中] 门户用量页显示原始微单位**:`cost_microunits` 原样输出。修复:
|
||||
按 /1e6 转换显示 USD 金额(与管理端一致)。
|
||||
5. **[功能] Provider 删除按钮**:前端补齐删除入口(确认框 + 级联提示)。
|
||||
6. 记录未修项:refreshToken 双端死代码、锁屏密码加密占位 key(已不落盘,
|
||||
建议后续改服务端校验)、注册/找回密码页为模板 mock、B7 约 21 处
|
||||
ElMessageBox 取消未捕获等。
|
||||
|
||||
## Go 回归审查发现并修复
|
||||
|
||||
1. **[高] >4MB 非流式响应 token 计量丢失**:`consumeUsageObject` 提取的
|
||||
usage 对象以 `inUsage=false` 解析,顶层 *_tokens 永不计数;压缩触发后
|
||||
计量静默回退为预留值。修复:`consumeJSONAsUsage`(inUsage=true),
|
||||
并加固 "usage" 键位置判定(要求前一非空白字符为 { 或 ,)。
|
||||
2. **[中] 脱敏检测与改写不一致**(转义内容):`\u002d` 等 JSON 转义使
|
||||
解码后匹配成功但字节改写失败,请求原样上送却标记已脱敏。修复:
|
||||
`applyRules` 增加解码文本回退路径(替换后重新 JSON 转义);替换文本
|
||||
本身做 JSON 转义防破坏结构;`Redacted` 以字节改写结果为准,不谎报。
|
||||
3. **[中] 脱敏规则跨策略泄漏**:字节改写使用所有 redact 策略的规则(含
|
||||
不适用于本请求的端点/模型作用域)。修复:仅收集本请求命中的 redact
|
||||
策略规则。
|
||||
4. **[中] 熔断探针语义**:陈旧成功/取消探针会错误关闭电路。修复:
|
||||
`allow` 返回探针标记,`success(probe)` 仅探针成功可关闭;取消探针
|
||||
调用 `abortProbe` 保持打开。
|
||||
5. **[中] TOTP 挑战令牌先消费后验证**:验证码输错即烧令牌、强制重新登录
|
||||
且叠加锁定计数。修复:`AuthenticatePending` 改只读,验证通过后
|
||||
`ConsumePending`(GetDel)原子消费——重试友好且防双会话。
|
||||
6. **[中] 管理员重置密码不作废会话**:management update 补
|
||||
`BumpAuthVersion`,与自助改密语义一致。
|
||||
7. **[低] 会话 TTL 校验**:`AUTH_SESSION_TTL` 上限 7 天,`authVersionTTL`
|
||||
提高至 14 天,杜绝版本键先过期导致旧会话复活。
|
||||
8. **[低] provider delete 补充 `tenant_id IS NULL` 过滤**(与其它查询一致)。
|
||||
|
||||
## 新 logo(LLMGuardX语枢)
|
||||
|
||||
- 设计:AI 网关路由主题——深蓝圆角方块 + 青色枢纽节点 + 三向分支连接,
|
||||
中心"语枢"字母 A 标记;沿用品牌色(#071F4D/#00E4E5/#006EFF)。
|
||||
- 替换:侧边栏/顶栏 logo(SVG,Vite 内联)、登录页图标、favicon(16-256
|
||||
多尺寸 ICO)。程序化像素验证渲染正确。
|
||||
+26
-20
@@ -3,7 +3,7 @@
|
||||
- **报告日期**: 2026-08-12
|
||||
- **工程**: AI Gateway 全量 Go 重构(替代原 Python/FastAPI 网关)
|
||||
- **活跃工作树**: `/home/ben/ai-gateway-src/ai-gateway-go-deploy-0.10.0`
|
||||
- **当前版本**: 0.10.0(Go 1.26,PostgreSQL 17+pgvector + 双 Redis + MinIO + Ollama,24 个迁移)
|
||||
- **当前版本**: 0.10.0(Go 1.26,PostgreSQL 17+pgvector + 双 Redis + MinIO + Ollama,28 个迁移)
|
||||
|
||||
---
|
||||
|
||||
@@ -12,15 +12,15 @@
|
||||
| 项 | 状态 |
|
||||
|---|---|
|
||||
| 部署形态 | Docker Compose(项目名 `deploy`),gateway-api `:8080` / admin-web `:8081` / portal-web `:8082` |
|
||||
| 数据层 | PostgreSQL 17 + pgvector(权威配置 + 审计分区 + 向量列)+ critical/cache 双 Redis + MinIO + Ollama;24 个迁移已应用 |
|
||||
| 迁移 | `000001`…`000024`(含资源市场 `000022`、MinIO `000023`、pgvector `000024`) |
|
||||
| 验证 | `go build ./...`、`go vet`、全套单测、资源市场/文件管理集成测试连真实库 **全部通过** |
|
||||
| 数据层 | PostgreSQL 17 + pgvector(权威配置 + 审计分区 + 向量列)+ critical/cache 双 Redis + MinIO + Ollama;28 个迁移已应用 |
|
||||
| 迁移 | `000001`…`000028`(含资源市场 `000022`、MinIO `000023`、pgvector `000024`、站内消息 `000025`、定时任务 `000026`、LLM Trace `000027`、智能体节点 `000028`) |
|
||||
| 验证 | `go build ./...`、`go vet ./...`、全套单测及资源市场/文件/向量/站内信/调度器/Trace/节点真实库集成测试 **全部通过** |
|
||||
| 前端 | Art Design Pro 管理端 + 门户端,已构建进镜像并运行 |
|
||||
| 版本控制 | git(remote `origin`=Gitea `superidou/ai-gateway-go`),每次改动 commit + push |
|
||||
| 版本控制 | git(remote `origin`=Gitea `superidou/ai-gateway-go`),功能按阶段提交;推送由发布流程执行 |
|
||||
|
||||
---
|
||||
|
||||
## 二、已完成里程碑(M0–M7)
|
||||
## 二、已完成里程碑(M0–M8)
|
||||
|
||||
### M0 工程基线
|
||||
Go 1.26 模块、配置校验、结构化日志、优雅退出;pgx 连接池、双 Redis 客户端;带校验和/事务/advisory lock 的独立迁移器;OpenAI 兼容入口、bootstrap key、请求体限制、SSE 透传;healthz/readyz/Prometheus;outbox/API Key/Provider/审计分区首版 schema。
|
||||
@@ -51,6 +51,12 @@ Prompt 分类/模板/不可变版本/必填校验;知识库(2 MiB 有界正文
|
||||
- 前端:管理端 4 页(市场总览 / MCP 服务器 / Skills / 数字员工)+ 门户资源市场页
|
||||
- 测试:`TestMarketplaceLifecycle` / `TestWorkbenchPostgreSQLLifecycle` 连真实 PostgreSQL 通过
|
||||
|
||||
### M8 基础设施层
|
||||
MinIO 对象存储与管理端/个人文件仓库;pgvector + Ollama(bge-m3) 向量化与 vector/hybrid 检索;可靠站内消息、管理员广播、未读回执;独立定时任务 worker,支持标准五字段 Cron、时区、应用/数字员工目标、Skills/MCP 子集、会话上下文、指定通知通道、立即执行、启停和执行历史。调度队列使用 PostgreSQL `SKIP LOCKED` 租约、超时回收和有限重试,多副本安全。
|
||||
|
||||
### M9 智能体与可观测(P1/P2/P3/P4)
|
||||
应用/数字员工请求可追踪到模型、知识检索、工具/MCP span;管理端提供元数据 Trace 查询和详情时间线,并按会话聚合普通应用/数字员工请求;新增智能体节点登记、令牌轮换、心跳监控和只读节点池路由预览,配套 `trace:read`、`agent_node:read/manage` 权限。当前仍待补齐远程安装、任务下发/真实节点执行路由与 AI 助手。
|
||||
|
||||
---
|
||||
|
||||
## 三、旗舰版(Ultra)功能矩阵对照
|
||||
@@ -69,9 +75,9 @@ Prompt 分类/模板/不可变版本/必填校验;知识库(2 MiB 有界正文
|
||||
| 资源市场 | 在线编辑和发布 MCP/Skills/数字员工 | 不支持 | ✓ | ✓ | ✅ 管理端 CRUD + publish 路由 |
|
||||
| 资源市场 | 资源分类管理和标签管理 | ✓ | ✓ | ✓ | ✅ `marketplace_categories` |
|
||||
| 配置管理 | 平台配置敏感参数,skill/mcp 运行时动态注入环境变量 | — | ✓ | ✓ | ❌ |
|
||||
| 智能体管理 | 智能体节点:运行监控/动态创建/裸机安装/节点池分配/公有私有池路由 | — | ✓ | ✓ | ❌ |
|
||||
| 智能体管理 | LLMTrace:会话中大模型调用、工具调用执行性能跟踪 | — | ✓ | ✓ | ❌ |
|
||||
| 智能体管理 | 智能体会话:会话列表,区分普通会话与数字员工会话 | — | ✓ | ✓ | ❌(有应用托管会话,非会话列表体系) |
|
||||
| 智能体管理 | 智能体节点:运行监控/动态创建/裸机安装/节点池分配/公有私有池路由 | — | ✓ | ✓ | ⚠️ M9 P4:节点登记、令牌轮换、心跳、能力候选和只读稳定选路预览;远程安装/任务下发/真实执行路由待补齐 |
|
||||
| 智能体管理 | LLMTrace:会话中大模型调用、工具调用执行性能跟踪 | — | ✓ | ✓ | ✅ M9 P1:应用/数字员工 Trace + model/retrieval/tool span + 管理端查询 |
|
||||
| 智能体管理 | 智能体会话:会话列表,区分普通会话与数字员工会话 | — | ✓ | ✓ | ✅ M9 P2:基于 Trace 元数据聚合会话,区分 application/digital_employee |
|
||||
| 模型管理 | 对接国内外主流大模型供应商 | ✓ | ✓ | ✓ | ✅ providers + 模型目录同步 |
|
||||
| 模型管理 | 供应商中添加配置大模型 | ✓ | ✓ | ✓ | ✅ |
|
||||
| 模型管理 | 对接本地模型(Ollama/vLLM) | 不支持 | ✓ | ✓ | ✅ 走 OpenAI 兼容通用 provider |
|
||||
@@ -99,7 +105,7 @@ Prompt 分类/模板/不可变版本/必填校验;知识库(2 MiB 有界正文
|
||||
| 安全策略 | 运行时安全:网络/工具命令执行安全校验审批/工具调用频率限制 | — | ✓ | ✓ | ⚠️ 工具 SSRF/拨号防护 ✅;命令执行审批、工具限流 ❌ |
|
||||
| 安全策略 | 供应链安全:skill/mcp 资源安全扫描 | — | ✓ | ✓ | ❌ |
|
||||
| 安全策略 | 数据安全:工具数据输入输出脱敏 + 大模型回答隐私敏感信息拦截替换 | — | ✓ | ✓ | ⚠️ 提示词输入脱敏 ✅;工具输出/回答拦截替换 ❌ |
|
||||
| 站内消息 | 平台推送站内消息与动态 | — | ✓ | ✓ | ❌(通知 worker 仅 webhook) |
|
||||
| 站内消息 | 平台推送站内消息与动态 | — | ✓ | ✓ | ✅ M8 P4:outbox 事件由通知 worker 物化为站内消息(幂等),admin 广播 + admin/portal 收件箱 + 已读回执 + 未读徽标 |
|
||||
| 审批授权 | 资源/模型/渠道使用申请流程审批管理 | — | ✓ | ✓ | ⚠️ 仅模型申请 |
|
||||
| 文件管理 | 平台文件资源与对象存储文件浏览管理 | — | ✓ | ✓ | ✅(M8 MinIO 已上线,admin 文件管理) |
|
||||
| 审计日志 | 系统全量历史操作审计日志查询 | — | ✓ | ✓ | ✅ |
|
||||
@@ -112,9 +118,9 @@ Prompt 分类/模板/不可变版本/必填校验;知识库(2 MiB 有界正文
|
||||
|---|---|---|
|
||||
| 聊天 | 新建会话、授权大模型对话 | ✅ |
|
||||
| 聊天 | 删除会话、会话重命名 | ✅ |
|
||||
| 定时任务 | 创建定时任务(配置提示词/渠道/mcp/skills/数字员工/会话ID) | ❌ |
|
||||
| 定时任务 | 启动/修改/立即执行/删除定时任务配置 | ❌ |
|
||||
| 定时任务 | 查看定时任务执行历史 | ❌ |
|
||||
| 定时任务 | 创建定时任务(配置提示词/渠道/mcp/skills/数字员工/会话ID) | ✅ M8 P3:应用/数字员工目标、Skills/MCP 子集、会话 ID、通知通道、加密 API Key |
|
||||
| 定时任务 | 启动/修改/立即执行/删除定时任务配置 | ✅ 标准五字段 Cron + IANA 时区,支持启停、编辑、手动入队和删除 |
|
||||
| 定时任务 | 查看定时任务执行历史 | ✅ 成功/失败状态、重试次数、响应与错误历史 |
|
||||
| 个人渠道 | 配置个人微信/企业微信/钉钉/飞书 | ❌ |
|
||||
| 个人渠道 | 个人微信/企业微信快速扫码对接 | ❌ |
|
||||
| 个人渠道 | 绑定特定大模型执行对话 | ❌ |
|
||||
@@ -128,14 +134,14 @@ Prompt 分类/模板/不可变版本/必填校验;知识库(2 MiB 有界正文
|
||||
| 个人文件仓库 | 对话产生的报告/文件存入个人仓库 | ✅(M8 个人文件仓,经网关上传/下载) |
|
||||
| 安全策略 | 个人智能体安全策略(网络/工具命令校验审批/限流/脱敏/隐私拦截) | ❌ |
|
||||
| 个人中心 | 账号信息、密码修改、登录记录查看 | ✅ |
|
||||
| 消息通知 | 系统消息、审批待办、任务执行结果提醒 | ⚠️ webhook 投递 ✅;站内消息/待办/结果提醒 ❌ |
|
||||
| 消息通知 | 系统消息、审批待办、任务执行结果提醒 | ⚠️ 系统事件、模型审批和定时任务结果已支持 Webhook + 站内信;完整审批待办体系待补齐 |
|
||||
|
||||
---
|
||||
|
||||
## 四、差距汇总
|
||||
|
||||
- **完全未实现(❌,约 16 项)**:AI 助手、收藏、数据报表/企业报表、配置管理(env 注入)、智能体管理三项(节点/LLMTrace/会话)、记忆管理三项、渠道管理全项、多租户、供应链安全扫描、完整审批流、License、定时任务全项、个人渠道、个人安全策略、ARM64。(M8 已落地:文件管理/MinIO、个人文件仓库、知识库向量化语义召回)
|
||||
- **部分覆盖需补齐(⚠️,约 9 项)**:平台概览看板、资源/渠道审批、工具输出脱敏与大模型回答拦截替换、工具命令审批与工具限流、集群部署方案、站内消息/审批待办/任务结果、数字员工会话入口、资源权限等级三档、全类型权限申请。
|
||||
- **完全未实现(❌)**:AI 助手、收藏、数据报表/企业报表、配置管理(env 注入)、智能体节点的远程安装/任务路由、记忆管理三项、渠道管理全项、多租户、供应链安全扫描、完整审批流、License、个人渠道、个人安全策略、ARM64。
|
||||
- **部分覆盖需补齐(⚠️)**:平台概览看板、资源/渠道审批、工具输出脱敏与大模型回答拦截替换、工具命令审批与工具限流、完整集群部署方案、审批待办体系、数字员工会话入口、资源权限等级三档、全类型权限申请。
|
||||
|
||||
---
|
||||
|
||||
@@ -143,7 +149,7 @@ Prompt 分类/模板/不可变版本/必填校验;知识库(2 MiB 有界正文
|
||||
|
||||
| 里程碑 | 内容 | 依赖 |
|
||||
|---|---|---|
|
||||
| **M8 基础设施层** | 对象存储(MinIO)、向量化(pgvector)、定时任务调度器、站内消息 | —(P1 MinIO/P2 pgvector+Ollama 已完成,待 P3 调度器 + P4 站内信) |
|
||||
| **M8 基础设施层** | 对象存储(MinIO)、向量化(pgvector)、定时任务调度器、站内消息 | ✅ P1–P4 全部完成 |
|
||||
| **M9 智能体与可观测** | LLMTrace、智能体会话、智能体节点(监控/节点池/路由)、AI 助手 | M8 |
|
||||
| **M10 记忆管理** | 多层记忆集合、语义召回、裁剪衰减、记忆授权 | M8(pgvector) |
|
||||
| **M11 渠道与审批** | 渠道管理(企业微信/个人微信/钉钉/飞书)、个人渠道、完整审批流、资源权限等级 | M8 |
|
||||
@@ -157,9 +163,9 @@ Prompt 分类/模板/不可变版本/必填校验;知识库(2 MiB 有界正文
|
||||
## 六、验证情况(当前基线)
|
||||
|
||||
- `go build ./...` ✅、`go vet ./...` ✅
|
||||
- 全套单测(全部包)✅;资源市场/文件/向量化单测(embedder/retriever)✅
|
||||
- 集成测试 `TestMarketplaceLifecycle`、`TestWorkbenchPostgreSQLLifecycle`、`TestFileObjectLifecycle`、`TestKnowledgeVectorLifecycle`(pgvector+Ollama:导入即向量化、语义命中、embedder 失败降级)连真实 PostgreSQL/MinIO/Ollama ✅
|
||||
- 部署冒烟:healthz/readyz ✅、admin :8081 / portal :8082 302 ✅、24 迁移应用 ✅、vector 模式知识库导入即向量化 + 语义检索命中 ✅
|
||||
- 全套单测(全部包)✅;资源市场/文件/向量化/Cron/资源绑定单测✅
|
||||
- 集成测试 `TestMarketplaceLifecycle`、`TestWorkbenchPostgreSQLLifecycle`、`TestFileObjectLifecycle`、`TestKnowledgeVectorLifecycle`、`TestInboxMaterializeAndBroadcast`、`TestSchedulerPostgreSQLLifecycle`、`TestTracePostgreSQLLifecycle` 连真实依赖 ✅
|
||||
- 部署冒烟:healthz/readyz ✅、admin :8081 / portal :8082 302 ✅、28 个迁移应用 ✅、vector 模式知识库语义检索、定时任务执行链路、Trace 存储与节点心跳 ✅
|
||||
|
||||
## 七、部署与已知坑
|
||||
|
||||
|
||||
Reference in New Issue
Block a user