feat(platform): add platform blueprint scaffolding

This commit is contained in:
deploy-promoter[bot]
2026-03-07 08:24:54 +01:00
parent a679f654ed
commit eb221450ca
35 changed files with 1909 additions and 0 deletions
@@ -0,0 +1,50 @@
---
name: Platform Execution
about: Track platform delivery work with explicit planning, evidence, and rollback fields
title: "[Platform] "
labels: platform
assignees: ""
---
## Summary
<!-- Describe the scope, expected outcome, and impacted behavior. -->
## Type
- [ ] Epic
- [ ] Issue
- [ ] Follow-up
## Priority
- [ ] P0
- [ ] P1
- [ ] P2
- [ ] P3
## Week
<!-- Use YYYY-Www, for example 2026-W10. -->
## Repos touched
- `ljwx-deploy`
## Files / Paths expected to change
- `platform/...`
## Dependencies
- None
## Acceptance Criteria
- [ ]
## Evidence Required
- [ ] PR link
- [ ] Manifest or config diff
- [ ] Runtime verification output
- [ ] Docs updated if contract or behavior changed
## Rollback Plan
- Revert target:
- Re-apply or resync step:
- Verification after rollback:
## Links
<!-- Optional milestone, epic, dashboard, ArgoCD, or runbook links. -->
+10
View File
@@ -7,6 +7,14 @@ GitOps 配置仓库,使用 ArgoCD 管理 Kubernetes 集群。
- 新进程/新项目/老项目统一接入入口:`docs/START-HERE-GITOPS-ONBOARDING.md`
- 自动扫描当前仓库是否已接入:`bash scripts/ops/scan-gitops-context.sh --repo "$PWD"`
## Platform Blueprint
- 平台装配蓝图入口:`docs/platform/README.md`
- 平台配置与契约:`platform/`
- 集成版本清单:`release/platform-version.yaml`
- 平台 runbooks:`runbooks/`
- 新集群初始化脚本骨架:`scripts/bootstrap/cluster-init.sh`
## Bid-MVP Factory Control Plane
- 运行验证:`bash scripts/verify.sh`
@@ -30,6 +38,8 @@ GitOps 配置仓库,使用 ArgoCD 管理 Kubernetes 集群。
- `apps/` - 应用配置
- `cluster/` - 本地/dev 集群级别配置
- `cluster-prod/` - 生产集群级别配置(prod app + prod promoter + registry secret sync)
- `platform/` - 平台装配、契约、路由、知识治理配置
- `runbooks/` - 平台级操作手册
## 生产发布闭环(当前)
+22
View File
@@ -0,0 +1,22 @@
# Platform Blueprint
本目录用于沉淀 `ljwx-deploy` 作为平台集成仓时的装配规则、边界和执行方式。
原则:
1. `ljwx-deploy` 仍然是唯一部署真相源。
2. `apps/`、`argocd-apps/`、`cluster/` 负责实际交付对象。
3. `platform/` 负责平台级配置、契约、路由、知识治理和环境矩阵。
4. 运行时行为必须能回溯到 Git 中的版本、策略、知识和能力契约。
建议阅读顺序:
1. `architecture-overview.md`
2. `component-responsibilities.md`
3. `platform-repo-design.md`
4. `routing-strategy.md`
5. `knowledge-pipeline.md`
6. `deployment-environments.md`
7. `bootstrap-and-migration.md`
8. `engineering-execution-guide.md`
9. `github-project-plan.md`
+40
View File
@@ -0,0 +1,40 @@
# Architecture Overview
## Goal
建立一个以客户产品为中心、可版本化、可回滚、可迁移的 AI 平台装配结构。
## Layers
| Layer | Components | Responsibility |
| --- | --- | --- |
| Product entry | `ljwx-chat`, website embeds | Conversation UI, customer-facing experience, tenant-aware entry |
| Orchestration | `OpenClaw`, routing policy | Request classification, model selection, tool orchestration, execution audit |
| Capability | `ljwx-core-api`, `n8n`, adapters | Deterministic business actions, authz, audit, external integrations |
| Knowledge | knowledge pipeline, retrieval, Dify datasets | Normalize, classify, publish, invalidate, retrieve |
| Platform | `ljwx-deploy`, ArgoCD, k3s, GitHub Actions | GitOps, overlays, release evidence, bootstrap, rollback |
## Source-of-Truth Rules
1. Deployment truth lives in `apps/`, `cluster/`, `argocd-apps/`, and `release/`.
2. Platform behavior truth lives in `platform/`.
3. Dify consumes processed knowledge; it is not the raw knowledge source.
4. `n8n` executes workflows; it is not the business system of record.
## Request Flows
### General chat
`ljwx-chat -> OpenClaw -> route policy -> GPT -> response`
### Knowledge Q&A
`ljwx-chat -> OpenClaw -> retrieval/Dify dataset -> GPT/Claude -> cited response`
### Deterministic business action
`ljwx-chat -> OpenClaw -> capability registry -> core-api/n8n -> audited result -> response`
### Multi-step request
`ljwx-chat -> OpenClaw -> plan -> retrieval + tools + business action -> summarized response`
+27
View File
@@ -0,0 +1,27 @@
# Bootstrap And Migration
## Bootstrap Goal
Bring up a new server or cluster by replaying Git-managed baselines instead of rebuilding state manually.
## Bootstrap Sequence
1. prepare host and k3s
2. prepare registry pull and secret management prerequisites
3. run `scripts/bootstrap/cluster-init.sh`
4. apply Argo bootstrap apps
5. sync app overlays
6. run smoke and evidence checks
## Migration Goal
Migrate workloads or contracts from legacy directories into Git-managed platform structure without changing the runtime truth boundary.
## Migration Rule
When moving legacy assets:
1. export contracts and examples first
2. add platform config references
3. validate Argo and smoke targets
4. only then retire legacy entrypoints
@@ -0,0 +1,20 @@
# Component Responsibilities
| Component | Owns | Must Not Own |
| --- | --- | --- |
| `ljwx-chat` | UI, session presentation, channel entry, user-facing configuration | Orchestration policy, business workflows, knowledge source-of-truth |
| `OpenClaw` | Routing, tool orchestration, execution traces, fallback policy | Persistent business state, raw knowledge governance, product backend ownership |
| `GPT` | Default generation, tool-capable responses, fast general tasks | Workflow state, knowledge publishing, business truth |
| `Claude` | Long-form analysis, design reasoning, complementary fallback | Default full traffic routing, workflow state |
| `Dify` | Dataset-backed RAG consumption, agent UX where appropriate | Raw content source, core business logic, system-wide orchestration |
| `n8n` | Deterministic workflow execution, retries, notifications, approvals | Product backend, complex reasoning, knowledge governance |
| `ljwx-core-api` | Authn/authz, audit, capability gateway, business APIs | Frontend rendering, workflow editing |
| `knowledge-processor` | Collection, normalization, classification, publish/invalidate | User-facing conversation UI |
| `ljwx-deploy` | Integrated assembly, deployment structure, env overlays, runbooks | Product feature logic |
## Guardrails
1. Core business rules go to `ljwx-core-api`, not Dify or `n8n`.
2. Routing logic goes to `platform/routing/`, not `ljwx-chat`.
3. Capability ownership is explicit in `platform/assembly/capabilities.yaml`.
4. Knowledge publication is reversible and auditable.
+22
View File
@@ -0,0 +1,22 @@
# Deployment Environments
## Current Profiles
`platform/assembly/env-matrix.yaml` is the platform-level matrix for environment and cluster behavior.
Initial profiles:
1. `local-k3s`
2. `orbstack-k3s-cn`
3. `prod-planned`
## Rules
1. Environment differences belong in profile files, not duplicated scripts.
2. Dev can auto-sync; production should remain review-gated.
3. Service map, smoke targets, public ingress, and release policy must be profile-driven.
## Secrets
Secrets are still externalized and must not be stored in this repository.
This directory only records references and expected runtime contracts.
@@ -0,0 +1,26 @@
# Engineering Execution Guide
## Change Types
| Change type | Primary files |
| --- | --- |
| routing behavior | `platform/routing/`, `platform/contracts/`, `release/platform-version.yaml` |
| knowledge publication | `platform/knowledge/`, knowledge source repos, publish workflow |
| capability ownership | `platform/assembly/capabilities.yaml`, `platform/contracts/service-capability-map.yaml` |
| deployment topology | `apps/`, `cluster/`, `argocd-apps/`, `release/` |
## Required PR Contents
Every platform PR should include:
1. scope and impacted environments
2. changed source-of-truth files
3. rollback method
4. verification evidence
5. follow-up tasks if the change is staged
## Minimum Review Standard
1. platform config and runtime behavior must match
2. no component should silently gain a new responsibility boundary
3. route and capability changes must remain explainable from Git
+68
View File
@@ -0,0 +1,68 @@
# GitHub Project Plan
## Purpose
Use GitHub Milestones, Epics, and Issues to plan platform work with clear ownership, scheduling, evidence, and rollback.
## Milestones
- Define a dated delivery checkpoint or platform outcome.
- Group epics that must be completed together.
- Carry exit criteria that can be verified from GitHub, manifests, and runtime evidence.
## Epics
- Represent one cross-repo capability slice under a milestone.
- Link the affected repos, contracts, and operating procedures.
- Decompose into issues that can be reviewed and rolled back independently.
## Issues
- Represent the smallest reviewable execution unit.
- Change one coherent behavior, contract, or operational path.
- Always include acceptance, evidence, and rollback before moving to done.
## Priority
- `P0`: production blocker, security issue, or same-day delivery requirement.
- `P1`: current-week delivery target with direct dependency on active milestone work.
- `P2`: planned follow-up work that should land in the next scheduled wave.
- `P3`: backlog or optimization work without immediate release pressure.
## Week
- Use `YYYY-Www` format, for example `2026-W10`.
- Re-evaluate the week whenever dependencies or scope change.
## Dependencies
- List blocking issues, PRs, repos, manifests, or environment gates.
- Make blockers explicit enough that another engineer can trace them without oral context.
## Acceptance
- Define observable done conditions, not implementation intent.
- Prefer checks such as manifest diff merged, Argo sync healthy, smoke test pass, or contract updated.
## Evidence
- Attach the proof required to close the item.
- Typical evidence includes PR links, screenshots, command output, ArgoCD state, and smoke-test results.
## Rollback
- Document the exact revert path before execution starts.
- Include the repo or manifest to revert and the verification step after rollback.
## Recommended Planning Table
| Milestone | Epic | Issue | Priority | Week | Dependencies | Acceptance | Evidence | Rollback |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| Platform routing baseline | Shared ingress behavior | Add platform redirect middleware to affected app overlay | P1 | 2026-W10 | ingress profile update, service PR | dev ingress smoke passes and docs updated | PR link, sync result, curl output | revert PR, resync app, rerun smoke |
## Operating Rules
1. Every execution issue should belong to one epic and one milestone.
2. Priority and week should be reviewed together; stale scheduling is a planning bug.
3. Acceptance and evidence should be specific enough for async review.
4. Rollback should be executable by an on-call engineer without extra explanation.
+37
View File
@@ -0,0 +1,37 @@
# Knowledge Pipeline
## Positioning
The raw source of truth stays in Git-managed repositories and approved source systems.
Dify only consumes processed outputs.
## Pipeline
1. collect
2. normalize
3. classify
4. deduplicate
5. chunk
6. tag
7. apply ACL
8. review
9. publish
10. invalidate or unpublish
## Publish Targets
- Dify datasets for product-facing retrieval
- internal retrieval collections for platform and support use cases
- customer-scoped retrieval collections when tenant isolation is required
## Invalidate Triggers
Publishing is not append-only. A document or chunk must be invalidated when:
1. source content is deleted
2. ACL changes
3. `review_status` moves out of `approved`
4. `expires_at` is reached
5. customer scope changes
The target configuration lives in `platform/knowledge/publish-targets.yaml`.
+31
View File
@@ -0,0 +1,31 @@
# Platform Repo Design
## Purpose
`ljwx-deploy` is both:
1. the only deployment source of truth
2. the integrated platform assembly repository
## Directory Roles
| Directory | Role |
| --- | --- |
| `apps/` | deployable application manifests and overlays |
| `cluster/` | namespace and cluster baselines |
| `argocd-apps/` | ArgoCD bootstrap and application ownership |
| `release/` | queue, service maps, integrated platform versions |
| `platform/` | contracts, routing, knowledge governance, env matrix, capability registry |
| `runbooks/` | operational procedures for rollback, rebuild, republish |
| `scripts/bootstrap/` | cluster bootstrap entrypoints |
| `scripts/migrate/` | migration/export helpers |
## Change Rule
If a change affects runtime behavior but not image code, it should still be represented in this repository through:
- route config
- capability registry
- knowledge publish config
- env profile
- platform version
+38
View File
@@ -0,0 +1,38 @@
# Routing Strategy
## Design Split
Routing must be split into three layers:
1. `platform/routing/routes.*.yaml`
Business-intent to route-policy mapping.
2. `platform/routing/model-catalog.yaml`
Provider and model capability metadata.
3. `platform/assembly/capabilities.yaml`
Tool and service dispatch registry.
This keeps strategy, model facts, and execution targets separate.
## Target Categories
| Category | Primary | Fallback | Tool orchestration |
| --- | --- | --- | --- |
| `general_chat` | GPT | Claude | no |
| `long_analysis` | Claude | GPT | no |
| `solution_design` | Claude | GPT | optional |
| `tool_execution` | GPT | Claude | yes |
| `knowledge_qa` | GPT | Claude | yes |
| `multi_step` | GPT | Claude | yes |
## Decision Evidence
Every route decision must record:
1. matched rule IDs
2. chosen provider and model
3. enabled or skipped tools
4. selected dataset scope
5. fallback reason when triggered
6. final cited documents or chunks for retrieval-backed answers
These fields are represented by `platform/contracts/router-request.schema.json` and `platform/contracts/router-decision.schema.json`.
+10
View File
@@ -0,0 +1,10 @@
# Platform Config
This directory stores platform-level assembly artifacts that are not application source code:
- `assembly/`: environment matrix and capability registry
- `contracts/`: request, decision, and knowledge contracts
- `routing/`: route policies and model catalog
- `knowledge/`: source registry, taxonomy, ACL, publish targets
Deployment truth is still executed from `apps/`, `cluster/`, `argocd-apps/`, and `release/`.
+33
View File
@@ -0,0 +1,33 @@
version: 0.1.0
capabilities:
kb.search:
executor: retrieval
owner_service: knowledge-processor
auth: scoped
audit: summary
timeout_ms: 3000
customer.lookup:
executor: core-api
owner_service: ljwx-core-api
auth: required
audit: full
timeout_ms: 5000
ticket.create:
executor: n8n
owner_service: n8n
auth: required
audit: full
timeout_ms: 8000
conversation.audit.write:
executor: core-api
owner_service: ljwx-core-api
auth: internal
audit: full
timeout_ms: 2000
route.execute:
executor: openclaw
owner_service: openclaw
auth: internal
audit: full
timeout_ms: 15000
+34
View File
@@ -0,0 +1,34 @@
version: 0.1.0
profiles:
local-k3s:
cluster_class: k3s
service_map_path: release/services.local-k3s.yaml
smoke_targets_path: scripts/smoke/targets.local-k3s.json
argocd_auto_sync: true
public_https: true
release_policy: auto-promote
orbstack-k3s-cn:
cluster_class: k3s
service_map_path: release/services.orbstack-k3s-cn.yaml
smoke_targets_path: scripts/smoke/targets.orbstack-k3s-cn.json
argocd_auto_sync: true
public_https: true
release_policy: auto-promote
prod-planned:
cluster_class: k3s
service_map_path: TBD
smoke_targets_path: TBD
argocd_auto_sync: false
public_https: true
release_policy: review-gated
environments:
dev:
default_profile: local-k3s
sync_policy: automated
approval_required: false
prod:
default_profile: prod-planned
sync_policy: manual
approval_required: true
@@ -0,0 +1,50 @@
$schema: https://json-schema.org/draft/2020-12/schema
$id: https://lingjingwanxiang.cn/schemas/knowledge-chunk.schema.yaml
title: KnowledgeChunk
type: object
additionalProperties: false
required:
- chunk_id
- document_id
- chunk_index
- visibility
- token_count
- text_hash
properties:
chunk_id:
type: string
minLength: 1
document_id:
type: string
minLength: 1
chunk_index:
type: integer
minimum: 0
heading_path:
type: array
items:
type: string
visibility:
type: string
enum:
- public
- internal
- customer
customer_id:
type:
- string
- "null"
token_count:
type: integer
minimum: 1
embedding_model:
type:
- string
- "null"
text_hash:
type: string
minLength: 1
published_targets:
type: array
items:
type: string
@@ -0,0 +1,79 @@
$schema: https://json-schema.org/draft/2020-12/schema
$id: https://lingjingwanxiang.cn/schemas/knowledge-document.schema.yaml
title: KnowledgeDocument
type: object
additionalProperties: false
required:
- document_id
- title
- source_type
- source_ref
- visibility
- doc_type
- owner
- version
- review_status
properties:
document_id:
type: string
minLength: 1
title:
type: string
minLength: 1
source_type:
type: string
enum:
- repo_doc
- faq
- delivery_doc
- deployment_doc
- support_record
- partner_reference
source_ref:
type: string
minLength: 1
visibility:
type: string
enum:
- public
- internal
- customer
customer_id:
type:
- string
- "null"
doc_type:
type: string
owner:
type: string
version:
type: string
checksum:
type:
- string
- "null"
review_status:
type: string
enum:
- draft
- reviewing
- approved
- rejected
approved_by:
type:
- string
- "null"
effective_from:
type:
- string
- "null"
format: date-time
expires_at:
type:
- string
- "null"
format: date-time
tags:
type: array
items:
type: string
@@ -0,0 +1,81 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://lingjingwanxiang.cn/schemas/router-decision.schema.json",
"title": "RouterDecision",
"type": "object",
"additionalProperties": false,
"required": [
"route_id",
"selected_provider",
"selected_model",
"tool_plan",
"decision_basis"
],
"properties": {
"route_id": {
"type": "string",
"minLength": 1
},
"selected_provider": {
"type": "string",
"minLength": 1
},
"selected_model": {
"type": "string",
"minLength": 1
},
"tool_plan": {
"type": "array",
"items": {
"type": "string"
}
},
"dataset_scopes": {
"type": "array",
"items": {
"type": "string"
}
},
"fallback": {
"type": ["object", "null"],
"additionalProperties": false,
"required": ["provider", "model", "condition"],
"properties": {
"provider": {
"type": "string"
},
"model": {
"type": "string"
},
"condition": {
"type": "string"
}
}
},
"decision_basis": {
"type": "object",
"additionalProperties": false,
"required": ["matched_rules", "reasons"],
"properties": {
"matched_rules": {
"type": "array",
"items": {
"type": "string"
}
},
"reasons": {
"type": "array",
"items": {
"type": "string"
}
},
"skipped_tools": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
}
}
@@ -0,0 +1,72 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://lingjingwanxiang.cn/schemas/router-request.schema.json",
"title": "RouterRequest",
"type": "object",
"additionalProperties": false,
"required": [
"trace_id",
"channel",
"task_type",
"requires_tools",
"requires_retrieval",
"messages"
],
"properties": {
"trace_id": {
"type": "string",
"minLength": 1
},
"tenant_id": {
"type": ["string", "null"]
},
"channel": {
"type": "string",
"enum": ["web", "api", "im", "system"]
},
"task_type": {
"type": "string",
"enum": [
"general_chat",
"long_analysis",
"solution_design",
"tool_execution",
"knowledge_qa",
"multi_step"
]
},
"requires_tools": {
"type": "boolean"
},
"requires_retrieval": {
"type": "boolean"
},
"latency_budget_ms": {
"type": ["integer", "null"],
"minimum": 1
},
"messages": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"additionalProperties": false,
"required": ["role", "content"],
"properties": {
"role": {
"type": "string",
"enum": ["system", "user", "assistant", "tool"]
},
"content": {
"type": "string",
"minLength": 1
}
}
}
},
"context_meta": {
"type": "object",
"additionalProperties": true
}
}
}
@@ -0,0 +1,45 @@
version: 0.1.0
services:
ljwx-chat:
provides:
- conversation.ui
- session.preferences
consumes:
- route.execute
openclaw:
provides:
- route.execute
- response.stream
- tool.orchestrate
consumes:
- kb.search
- customer.lookup
- ticket.create
- conversation.audit.write
ljwx-core-api:
provides:
- customer.lookup
- conversation.audit.write
- auth.check
- tool.gateway
consumes: []
n8n:
provides:
- ticket.create
- notification.dispatch
- approval.flow
consumes:
- auth.check
ljwx-dify:
provides:
- rag.answer
- dataset.retrieve
consumes:
- kb.search
knowledge-processor:
provides:
- kb.search
- knowledge.publish
- knowledge.invalidate
consumes: []
@@ -0,0 +1,38 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://lingjingwanxiang.cn/schemas/tool-result.schema.json",
"title": "ToolResult",
"type": "object",
"additionalProperties": false,
"required": ["tool_name", "success"],
"properties": {
"tool_name": {
"type": "string",
"minLength": 1
},
"success": {
"type": "boolean"
},
"data": {
"type": ["object", "array", "string", "number", "boolean", "null"]
},
"error": {
"type": ["string", "null"]
},
"meta": {
"type": "object",
"additionalProperties": true,
"properties": {
"mock": {
"type": "boolean"
},
"reason": {
"type": "string"
},
"audit_event_id": {
"type": "string"
}
}
}
}
}
+18
View File
@@ -0,0 +1,18 @@
version: 0.1.0
policies:
public:
allowed_targets:
- dify-public
- retrieval-public
review_required: true
internal:
allowed_targets:
- dify-internal
- retrieval-internal
review_required: true
customer:
allowed_targets:
- retrieval-customer-template
review_required: true
requires_customer_id: true
+49
View File
@@ -0,0 +1,49 @@
version: 0.1.0
targets:
dify-public:
type: dify_dataset
visibility: public
dataset_name: public-knowledge
invalidate_on:
- source_deleted
- acl_changed
- review_revoked
- expired
dify-internal:
type: dify_dataset
visibility: internal
dataset_name: internal-knowledge
invalidate_on:
- source_deleted
- acl_changed
- review_revoked
- expired
retrieval-public:
type: retrieval_collection
visibility: public
collection_name: public-knowledge
invalidate_on:
- source_deleted
- acl_changed
- review_revoked
- expired
retrieval-internal:
type: retrieval_collection
visibility: internal
collection_name: internal-knowledge
invalidate_on:
- source_deleted
- acl_changed
- review_revoked
- expired
retrieval-customer-template:
type: retrieval_collection
visibility: customer
collection_name_template: customer-{customer_id}
invalidate_on:
- source_deleted
- acl_changed
- review_revoked
- expired
- customer_scope_changed
+34
View File
@@ -0,0 +1,34 @@
version: 0.1.0
sources:
- id: ljwx-chat-docs
type: git_repo
repo: ljwx-chat
include:
- README.md
- docs/**
visibility: internal
owner: team-ai
- id: ljwx-deploy-docs
type: git_repo
repo: ljwx-deploy
include:
- docs/**
- runbooks/**
visibility: internal
owner: team-platform
- id: website-public-faq
type: git_repo
repo: ljwx-website
include:
- docs/**
- content/faq/**
visibility: public
owner: team-product
- id: customer-delivery-docs
type: managed_upload
repo: ljwx-knowledge
include:
- content/customers/**
visibility: customer
owner: team-delivery
+25
View File
@@ -0,0 +1,25 @@
version: 0.1.0
doc_types:
- project_doc
- solution_doc
- delivery_doc
- deployment_doc
- readme
- faq
- support_record
- ops_doc
- partner_reference
product_areas:
- chat
- openclaw
- dify
- n8n
- platform
- knowledge
visibility_scopes:
- public
- internal
- customer
+25
View File
@@ -0,0 +1,25 @@
version: 0.1.0
providers:
openai:
auth_secret_ref: runtime-llm/openai
supports_streaming: true
anthropic:
auth_secret_ref: runtime-llm/anthropic
supports_streaming: true
models:
gpt-5.4:
provider: openai
tier: primary
supports_tools: true
supports_json_mode: true
max_context_tokens: 200000
cost_profile: standard
claude-sonnet-4.6:
provider: anthropic
tier: complementary
supports_tools: true
supports_json_mode: true
max_context_tokens: 200000
cost_profile: analysis
+66
View File
@@ -0,0 +1,66 @@
version: 0.1.0
default_route: general_chat
routes:
- id: general_chat
match:
task_type: general_chat
primary:
provider: openai
model: gpt-5.4
fallback:
provider: anthropic
model: claude-sonnet-4.6
condition: provider_error_or_timeout
tool_policy: none
- id: long_analysis
match:
task_type: long_analysis
primary:
provider: anthropic
model: claude-sonnet-4.6
fallback:
provider: openai
model: gpt-5.4
condition: provider_error_or_timeout
tool_policy: none
- id: knowledge_qa
match:
task_type: knowledge_qa
primary:
provider: openai
model: gpt-5.4
fallback:
provider: anthropic
model: claude-sonnet-4.6
condition: provider_error_or_timeout
tool_policy: retrieval_only
dataset_scopes:
- public
- internal
- id: tool_execution
match:
task_type: tool_execution
primary:
provider: openai
model: gpt-5.4
fallback:
provider: anthropic
model: claude-sonnet-4.6
condition: provider_error_or_timeout
tool_policy: capability_registry
- id: multi_step
match:
task_type: multi_step
primary:
provider: openai
model: gpt-5.4
fallback:
provider: anthropic
model: claude-sonnet-4.6
condition: provider_error_or_timeout
tool_policy: capability_registry
+54
View File
@@ -0,0 +1,54 @@
version: 0.1.0
default_route: general_chat
routes:
- id: general_chat
match:
task_type: general_chat
primary:
provider: openai
model: gpt-5.4
fallback:
provider: anthropic
model: claude-sonnet-4.6
condition: provider_error_or_timeout
tool_policy: none
- id: long_analysis
match:
task_type: long_analysis
primary:
provider: anthropic
model: claude-sonnet-4.6
fallback:
provider: openai
model: gpt-5.4
condition: provider_error_or_timeout
tool_policy: none
- id: knowledge_qa
match:
task_type: knowledge_qa
primary:
provider: openai
model: gpt-5.4
fallback:
provider: anthropic
model: claude-sonnet-4.6
condition: provider_error_or_timeout
tool_policy: retrieval_only
dataset_scopes:
- public
- id: tool_execution
match:
task_type: tool_execution
primary:
provider: openai
model: gpt-5.4
fallback:
provider: anthropic
model: claude-sonnet-4.6
condition: provider_error_or_timeout
tool_policy: capability_registry
approval_gate: required_for_high_risk_actions
+44
View File
@@ -0,0 +1,44 @@
platform_version: 0.1.0-draft
release_channel: dev
released_at: null
components:
ljwx-chat:
delivery_ref: release/services.yaml
config_ref: apps/ljwx-chat/overlays/ljwx-chat-dev/kustomization.yaml
image: TBD
openclaw:
delivery_ref: planned-import
config_ref: planned-import
image: TBD
ljwx-core-api:
delivery_ref: planned
config_ref: planned
image: TBD
ljwx-dify:
delivery_ref: release/services.yaml
config_ref: apps/ljwx-dify
image: TBD
n8n:
delivery_ref: planned-import
config_ref: planned-import
image: TBD
knowledge-processor:
delivery_ref: planned
config_ref: planned
image: TBD
config_versions:
routing_rules: platform/routing/routes.dev.yaml@0.1.0
model_catalog: platform/routing/model-catalog.yaml@0.1.0
capability_registry: platform/assembly/capabilities.yaml@0.1.0
service_capability_map: platform/contracts/service-capability-map.yaml@0.1.0
knowledge_document_schema: platform/contracts/knowledge-document.schema.yaml@0.1.0
knowledge_chunk_schema: platform/contracts/knowledge-chunk.schema.yaml@0.1.0
workflow_template_version: tracked-in-ljwx-workflow-templates
core_contract_version: 0.1.0
env_profile: platform/assembly/env-matrix.yaml#local-k3s
notes:
- This file versions integrated behavior, not only image references.
- Update this file whenever routing, capability, or knowledge behavior changes.
+27
View File
@@ -0,0 +1,27 @@
# Knowledge Republish
## Use Cases
Use this runbook when processed knowledge must be republished or invalidated without changing product code.
## Triggers
1. source file deleted
2. ACL changed
3. review revoked
4. document expired
5. customer scope changed
## Steps
1. confirm source registry and publish target entries
2. rebuild normalized and chunk manifests
3. invalidate target datasets or collections
4. republish approved content
5. verify retrieval scope and sample queries
## Evidence
- publish job run ID
- invalidated dataset or collection IDs
- sample retrieval verification
+20
View File
@@ -0,0 +1,20 @@
# Node Rebuild
## Goal
Rebuild a cluster node or a small cluster using Git-managed bootstrap and application baselines.
## Steps
1. prepare host
2. install k3s
3. restore pull secrets and external secrets prerequisites
4. run `scripts/bootstrap/cluster-init.sh --env dev --profile <profile> --apply`
5. verify Argo apps
6. verify ingress and smoke targets
## Exit Criteria
- Argo apps are healthy
- promoted images match overlays
- smoke checks pass
+28
View File
@@ -0,0 +1,28 @@
# Platform Rollback
## Scope
Use this runbook when a platform release changes runtime behavior and must be reverted.
## Inputs
1. target `platform_version`
2. affected services
3. reason for rollback
4. verification owner
## Steps
1. identify the last known-good `release/platform-version.yaml`
2. re-enqueue last known-good image digests where required
3. restore route, capability, and knowledge config refs
4. merge or revert via PR
5. wait for Argo `Synced/Healthy`
6. run smoke and record evidence
## Evidence
- rollback PR
- Argo health snapshot
- smoke result
- updated evidence record
+111
View File
@@ -0,0 +1,111 @@
#!/usr/bin/env bash
set -euo pipefail
# This script bootstraps a cluster using Git-managed Argo applications.
ENV_NAME=""
PROFILE_NAME=""
APPLY_MODE="false"
SKIP_TLS="false"
usage() {
cat <<'EOF'
用法:
bash scripts/bootstrap/cluster-init.sh --env <env> --profile <profile> [--apply] [--skip-tls]
示例:
bash scripts/bootstrap/cluster-init.sh --env dev --profile local-k3s
bash scripts/bootstrap/cluster-init.sh --env dev --profile local-k3s --apply
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--env)
ENV_NAME="${2:-}"
shift 2
;;
--profile)
PROFILE_NAME="${2:-}"
shift 2
;;
--apply)
APPLY_MODE="true"
shift
;;
--skip-tls)
SKIP_TLS="true"
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "未知参数: $1" >&2
usage
exit 1
;;
esac
done
if [[ -z "$ENV_NAME" || -z "$PROFILE_NAME" ]]; then
echo "必须同时提供 --env 和 --profile。" >&2
usage
exit 1
fi
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
ENV_MATRIX="$REPO_ROOT/platform/assembly/env-matrix.yaml"
ARGO_CLUSTER_BOOTSTRAP="$REPO_ROOT/argocd-apps/00-cluster-bootstrap.yaml"
ARGO_APPS_BOOTSTRAP="$REPO_ROOT/argocd-apps/01-apps-bootstrap.yaml"
TLS_APP="$REPO_ROOT/argocd-apps/03-cert-manager-dev.yaml"
TLS_CONFIG_APP="$REPO_ROOT/argocd-apps/04-cert-manager-config-dev.yaml"
for required_file in \
"$ENV_MATRIX" \
"$ARGO_CLUSTER_BOOTSTRAP" \
"$ARGO_APPS_BOOTSTRAP"; do
if [[ ! -f "$required_file" ]]; then
echo "缺少必需文件: $required_file" >&2
exit 1
fi
done
echo "准备初始化集群。"
echo "环境: $ENV_NAME"
echo "Profile: $PROFILE_NAME"
echo "应用模式: $APPLY_MODE"
echo "跳过 TLS: $SKIP_TLS"
echo "校验完成,计划执行以下步骤:"
echo "1. 应用 cluster bootstrap Argo app"
echo "2. 应用 apps bootstrap Argo app"
if [[ "$SKIP_TLS" != "true" ]]; then
echo "3. 应用 cert-manager 与 TLS 配置"
fi
echo "4. 等待 Argo 接管后续资源"
echo "5. 运行 smoke 和证据校验"
if [[ "$APPLY_MODE" != "true" ]]; then
echo "当前为预览模式,未执行 kubectl apply。"
exit 0
fi
if ! command -v kubectl >/dev/null 2>&1; then
echo "未找到 kubectl,无法执行 apply。" >&2
exit 1
fi
echo "开始应用 Argo bootstrap 资源。"
kubectl apply -f "$ARGO_CLUSTER_BOOTSTRAP"
kubectl apply -f "$ARGO_APPS_BOOTSTRAP"
if [[ "$SKIP_TLS" != "true" ]]; then
kubectl apply -f "$TLS_APP"
kubectl apply -f "$TLS_CONFIG_APP"
fi
echo "初始化命令已执行。请随后检查 Argo、Smoke 和 Evidence 状态。"
+56
View File
@@ -0,0 +1,56 @@
#!/usr/bin/env bash
set -euo pipefail
# This script exports runtime-contract directories for migration review.
OUT_DIR=""
usage() {
cat <<'EOF'
用法:
bash scripts/migrate/export-runtime-contract.sh --out <output-dir>
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--out)
OUT_DIR="${2:-}"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "未知参数: $1" >&2
usage
exit 1
;;
esac
done
if [[ -z "$OUT_DIR" ]]; then
echo "必须提供导出目录。" >&2
usage
exit 1
fi
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
DEST_DIR="$(cd "$REPO_ROOT" && mkdir -p "$OUT_DIR" && cd "$OUT_DIR" && pwd)"
MANIFEST_FILE="$DEST_DIR/exported-runtime-contracts.txt"
echo "开始导出 runtime-contract 目录到: $DEST_DIR"
: > "$MANIFEST_FILE"
while IFS= read -r contract_dir; do
rel_path="${contract_dir#"$REPO_ROOT"/}"
target_dir="$DEST_DIR/$rel_path"
mkdir -p "$(dirname "$target_dir")"
cp -R "$contract_dir" "$target_dir"
echo "$rel_path" >> "$MANIFEST_FILE"
done < <(find "$REPO_ROOT/apps" -type d -name runtime-contract | sort)
echo "导出完成,清单文件: $MANIFEST_FILE"
+519
View File
@@ -0,0 +1,519 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
cd "$ROOT_DIR"
ENV_NAME="dev"
SERVICES_CSV="ljwx-website,ljwx-dify,ljwx-chat"
TARGETS_FILE="scripts/smoke/targets.local-k3s.json"
WORKFLOW_TIMEOUT_SEC=3600
QUEUE_TIMEOUT_SEC=1800
PROMOTE_TIMEOUT_SEC=1800
POLL_INTERVAL_SEC=20
SKIP_TRIGGER="false"
SKIP_SMOKE="false"
SMOKE_DRY_RUN="false"
ALLOW_SMOKE_FAILURES="false"
ALLOW_PROMOTE_PENDING="false"
log() {
echo "[smart-cs-e2e] $*"
}
err() {
echo "[smart-cs-e2e][错误] $*" >&2
}
usage() {
cat <<'USAGE'
用途:
一键执行 Smart CS 端到端验证:
Gate -> 三仓 build-and-enqueue -> queue/promote -> ArgoCD -> smoke -> evidence
用法:
bash scripts/ops/e2e-smart-cs.sh [options]
选项:
--env <dev|demo|prod> 目标环境(默认: dev)
--services <csv> 服务列表(默认: ljwx-website,ljwx-dify,ljwx-chat)
--targets <path> smoke targets 文件(默认: scripts/smoke/targets.local-k3s.json)
--workflow-timeout-sec <n> 每个 workflow 等待秒数(默认: 3600)
--queue-timeout-sec <n> 等待 queue 入列秒数(默认: 1800)
--promote-timeout-sec <n> 等待 promoted 秒数(默认: 1800)
--poll-interval-sec <n> 轮询间隔秒数(默认: 20)
--skip-trigger 跳过触发三仓 workflow(只做后半段验证)
--skip-smoke 跳过 smoke 执行
--smoke-dry-run smoke 使用 --dry-run
--allow-smoke-failures smoke 使用 --allow-failures
--allow-promote-pending promoted 超时不失败(仅告警)
-h, --help 显示帮助
USAGE
}
while [[ $# -gt 0 ]]; do
case "$1" in
--env)
ENV_NAME="${2:-}"
shift 2
;;
--services)
SERVICES_CSV="${2:-}"
shift 2
;;
--targets)
TARGETS_FILE="${2:-}"
shift 2
;;
--workflow-timeout-sec)
WORKFLOW_TIMEOUT_SEC="${2:-}"
shift 2
;;
--queue-timeout-sec)
QUEUE_TIMEOUT_SEC="${2:-}"
shift 2
;;
--promote-timeout-sec)
PROMOTE_TIMEOUT_SEC="${2:-}"
shift 2
;;
--poll-interval-sec)
POLL_INTERVAL_SEC="${2:-}"
shift 2
;;
--skip-trigger)
SKIP_TRIGGER="true"
shift
;;
--skip-smoke)
SKIP_SMOKE="true"
shift
;;
--smoke-dry-run)
SMOKE_DRY_RUN="true"
shift
;;
--allow-smoke-failures)
ALLOW_SMOKE_FAILURES="true"
shift
;;
--allow-promote-pending)
ALLOW_PROMOTE_PENDING="true"
shift
;;
-h|--help)
usage
exit 0
;;
*)
err "未知参数: $1"
usage
exit 2
;;
esac
done
required_cmds=(jq kubectl python3 uvx)
if [[ "$SKIP_TRIGGER" != "true" ]]; then
required_cmds+=(gh)
fi
for cmd in "${required_cmds[@]}"; do
if ! command -v "$cmd" >/dev/null 2>&1; then
err "缺少命令: $cmd"
exit 2
fi
done
if [[ "$SKIP_TRIGGER" != "true" ]]; then
if ! gh auth status >/dev/null 2>&1; then
err "gh 未登录,请先执行: gh auth login"
exit 2
fi
fi
if [[ ! -f "release/queue.yaml" ]]; then
err "当前目录不是 ljwx-deploy 仓库根目录"
exit 2
fi
if [[ ! -f "$TARGETS_FILE" ]]; then
err "smoke targets 文件不存在: $TARGETS_FILE"
exit 2
fi
declare -A REPO_BY_SERVICE=(
["ljwx-website"]="BrunoGao/ljwx-website"
["ljwx-dify"]="BrunoGao/ljwx-dify"
["ljwx-chat"]="BrunoGao/ljwx-chat"
)
IFS=',' read -r -a SERVICES <<< "$SERVICES_CSV"
if [[ "${#SERVICES[@]}" -eq 0 ]]; then
err "--services 不能为空"
exit 2
fi
for service in "${SERVICES[@]}"; do
if [[ -z "${REPO_BY_SERVICE[$service]:-}" ]]; then
err "未配置仓库映射的服务: $service"
exit 2
fi
done
resolve_run_id() {
local repo="$1"
local since_ts="$2"
local run_id=""
local deadline=$((SECONDS + 180))
while (( SECONDS < deadline )); do
run_id="$(gh run list -R "$repo" -w build-and-enqueue --event workflow_dispatch --json databaseId,createdAt -L 30 \
| jq -r --arg since "$since_ts" '[.[] | select(.createdAt >= $since)] | sort_by(.createdAt) | reverse | .[0].databaseId // empty')"
if [[ -n "$run_id" ]]; then
echo "$run_id"
return 0
fi
sleep 3
done
return 1
}
wait_run_success() {
local repo="$1"
local run_id="$2"
local deadline=$((SECONDS + WORKFLOW_TIMEOUT_SEC))
while (( SECONDS < deadline )); do
local info
info="$(gh run view "$run_id" -R "$repo" --json status,conclusion,url 2>/dev/null || true)"
local status conclusion url
status="$(jq -r '.status // empty' <<<"$info")"
conclusion="$(jq -r '.conclusion // empty' <<<"$info")"
url="$(jq -r '.url // empty' <<<"$info")"
if [[ "$status" == "completed" ]]; then
if [[ "$conclusion" == "success" ]]; then
log "workflow 成功: $repo run_id=$run_id"
return 0
fi
err "workflow 失败: $repo run_id=$run_id conclusion=$conclusion url=$url"
return 1
fi
log "等待 workflow 完成: $repo run_id=$run_id status=${status:-unknown}"
sleep "$POLL_INTERVAL_SEC"
done
err "workflow 等待超时: $repo run_id=$run_id"
return 1
}
wait_queue_new_entries() {
local since_ts="$1"
local deadline=$((SECONDS + QUEUE_TIMEOUT_SEC))
while (( SECONDS < deadline )); do
if uvx --with pyyaml python - "$since_ts" "$ENV_NAME" "$SERVICES_CSV" <<'PY'
import sys
from datetime import datetime, timezone
import yaml
since = datetime.fromisoformat(sys.argv[1].replace("Z", "+00:00")).astimezone(timezone.utc)
env = sys.argv[2]
services = [x for x in sys.argv[3].split(",") if x]
with open("release/queue.yaml", "r", encoding="utf-8") as f:
q = yaml.safe_load(f) or {}
states = ["pending", "promoted", "failed", "superseded"]
missing = []
for svc in services:
latest = None
for st in states:
for item in q.get(st, []) or []:
if not isinstance(item, dict):
continue
if item.get("service") != svc or item.get("env") != env:
continue
created_at = item.get("createdAt") or ""
try:
ts = datetime.fromisoformat(str(created_at).replace("Z", "+00:00")).astimezone(timezone.utc)
except ValueError:
continue
if ts < since:
continue
if latest is None or ts > latest[0]:
latest = (ts, st, item.get("id", ""))
if latest is None:
missing.append(svc)
else:
print(f"QUEUE_OK {svc} state={latest[1]} id={latest[2]}")
if missing:
print("QUEUE_MISSING " + ",".join(missing), file=sys.stderr)
raise SystemExit(1)
PY
then
return 0
fi
log "等待 queue 入列..."
sleep "$POLL_INTERVAL_SEC"
done
err "queue 入列等待超时"
return 1
}
wait_promoted_entries() {
local since_ts="$1"
local deadline=$((SECONDS + PROMOTE_TIMEOUT_SEC))
while (( SECONDS < deadline )); do
if uvx --with pyyaml python - "$since_ts" "$ENV_NAME" "$SERVICES_CSV" <<'PY'
import sys
from datetime import datetime, timezone
import yaml
since = datetime.fromisoformat(sys.argv[1].replace("Z", "+00:00")).astimezone(timezone.utc)
env = sys.argv[2]
services = [x for x in sys.argv[3].split(",") if x]
with open("release/queue.yaml", "r", encoding="utf-8") as f:
q = yaml.safe_load(f) or {}
missing = []
for svc in services:
latest = None
for item in q.get("promoted", []) or []:
if not isinstance(item, dict):
continue
if item.get("service") != svc or item.get("env") != env:
continue
created_at = item.get("createdAt") or ""
try:
ts = datetime.fromisoformat(str(created_at).replace("Z", "+00:00")).astimezone(timezone.utc)
except ValueError:
continue
if ts < since:
continue
if latest is None or ts > latest[0]:
latest = (ts, item.get("id", ""), item.get("promotedAt", ""))
if latest is None:
missing.append(svc)
else:
print(f"PROMOTED_OK {svc} id={latest[1]} promotedAt={latest[2]}")
if missing:
print("PROMOTED_MISSING " + ",".join(missing), file=sys.stderr)
raise SystemExit(1)
PY
then
return 0
fi
log "等待 promoter 出列..."
sleep "$POLL_INTERVAL_SEC"
done
return 1
}
check_argocd_apps() {
log "检查 ArgoCD 应用健康状态"
local app_json
app_json="$(uvx --with pyyaml python - "$ENV_NAME" "$SERVICES_CSV" <<'PY'
import json
import sys
import yaml
env = sys.argv[1]
services = [x for x in sys.argv[2].split(',') if x]
with open("release/services.yaml", "r", encoding="utf-8") as f:
data = yaml.safe_load(f) or {}
apps = []
for svc in services:
app = (
data.get("services", {})
.get(svc, {})
.get("envs", {})
.get(env, {})
.get("argocdApp")
)
if not app:
raise SystemExit(f"missing argocdApp mapping for service={svc} env={env}")
apps.append(app)
print(json.dumps(apps))
PY
)"
mapfile -t app_names < <(jq -r '.[]' <<<"$app_json")
for app in "${app_names[@]}"; do
local sync health
sync="$(kubectl -n argocd get applications.argoproj.io "$app" -o jsonpath='{.status.sync.status}')"
health="$(kubectl -n argocd get applications.argoproj.io "$app" -o jsonpath='{.status.health.status}')"
log "argocd app=$app sync=$sync health=$health"
if [[ "$sync" != "Synced" || "$health" != "Healthy" ]]; then
err "ArgoCD 状态异常: app=$app sync=$sync health=$health"
return 1
fi
done
return 0
}
run_smoke_subset() {
local tmp_targets
tmp_targets="$(mktemp /tmp/smart-cs-smoke-targets.XXXXXX.json)"
local services_json
services_json="$(printf '%s\n' "${SERVICES[@]}" | jq -R . | jq -s .)"
jq --argjson services "$services_json" '{profile: .profile, targets: [ .targets[] | select(.service as $s | $services | index($s)) ]}' "$TARGETS_FILE" > "$tmp_targets"
local target_count
target_count="$(jq '.targets | length' "$tmp_targets")"
if [[ "$target_count" -lt 1 ]]; then
err "过滤后的 smoke targets 为空: $tmp_targets"
rm -f "$tmp_targets"
return 1
fi
for service in "${SERVICES[@]}"; do
if ! jq -e --arg svc "$service" '.targets | any(.service == $svc)' "$tmp_targets" >/dev/null; then
err "smoke targets 缺少服务: $service"
rm -f "$tmp_targets"
return 1
fi
done
local args=(python3 scripts/smoke/run_smoke.py --targets "$tmp_targets" --queue release/queue.yaml)
if [[ "$SMOKE_DRY_RUN" == "true" ]]; then
args+=(--dry-run)
fi
if [[ "$ALLOW_SMOKE_FAILURES" == "true" ]]; then
args+=(--allow-failures)
fi
log "执行 smoke: ${args[*]}"
"${args[@]}"
rm -f "$tmp_targets"
return 0
}
print_evidence_summary() {
uvx --with pyyaml python - "$ENV_NAME" "$SERVICES_CSV" <<'PY'
import glob
import os
import sys
from datetime import datetime, timezone
import yaml
env = sys.argv[1]
services = [x for x in sys.argv[2].split(',') if x]
records = []
for path in glob.glob("evidence/records/*.yaml"):
with open(path, "r", encoding="utf-8") as f:
data = yaml.safe_load(f) or {}
if data.get("service") in services and data.get("env") == env:
ts_text = (data.get("tests", {}).get("smoke", {}).get("checkedAt")
or data.get("deploy", {}).get("syncedAt")
or data.get("promotedAt")
or "1970-01-01T00:00:00Z")
try:
ts = datetime.fromisoformat(str(ts_text).replace("Z", "+00:00")).astimezone(timezone.utc)
except ValueError:
ts = datetime(1970, 1, 1, tzinfo=timezone.utc)
records.append((data.get("service"), ts, data))
print("EVIDENCE_SUMMARY")
for svc in services:
cand = [item for item in records if item[0] == svc]
if not cand:
print(f"- {svc}: NO_RECORD")
continue
_, _, latest = sorted(cand, key=lambda x: x[1], reverse=True)[0]
smoke = (((latest.get("tests") or {}).get("smoke") or {}).get("status") or "unknown")
queue_id = (((latest.get("deploy") or {}).get("queueId")) or "")
checked = (((latest.get("tests") or {}).get("smoke") or {}).get("checkedAt") or "")
print(f"- {svc}: smoke={smoke} queue_id={queue_id} checked_at={checked}")
PY
}
START_TS="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
QUEUE_REFERENCE_TS="$START_TS"
log "开始执行 E2E,env=$ENV_NAME services=$SERVICES_CSV start=$START_TS"
log "步骤 1/7: 执行 deploy gate 本地校验"
bash scripts/verify.sh
log "步骤 2/7: 校验三仓 n8n 模板已回写"
for service in "${SERVICES[@]}"; do
local_repo="/root/codes/$service"
manifest_path="$local_repo/config/n8n/workflows/smart-cs/manifest.yaml"
if [[ ! -f "$manifest_path" ]]; then
err "缺少模板清单: $manifest_path"
exit 1
fi
log "模板存在: $manifest_path"
done
if [[ "$SKIP_TRIGGER" != "true" ]]; then
log "步骤 3/7: 触发三仓 build-and-enqueue"
for service in "${SERVICES[@]}"; do
repo="${REPO_BY_SERVICE[$service]}"
trigger_ts="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
log "触发 workflow: repo=$repo env=$ENV_NAME"
gh workflow run build-and-enqueue.yml -R "$repo" --ref main -f target_env="$ENV_NAME"
run_id="$(resolve_run_id "$repo" "$trigger_ts" || true)"
if [[ -z "$run_id" ]]; then
err "无法解析 workflow run id: repo=$repo"
exit 1
fi
log "捕获 run_id: repo=$repo run_id=$run_id"
wait_run_success "$repo" "$run_id"
done
else
log "步骤 3/7: 已跳过 workflow 触发(--skip-trigger)"
QUEUE_REFERENCE_TS="1970-01-01T00:00:00Z"
log "跳过触发模式下,queue/promoted 校验将使用全量历史记录"
fi
log "步骤 4/7: 等待 queue 入列"
wait_queue_new_entries "$QUEUE_REFERENCE_TS"
log "步骤 5/7: 等待 promoter 出列"
if ! wait_promoted_entries "$QUEUE_REFERENCE_TS"; then
if [[ "$ALLOW_PROMOTE_PENDING" == "true" ]]; then
log "告警: promoter 出列超时,但已允许继续(--allow-promote-pending)"
else
err "promoter 出列超时"
exit 1
fi
fi
log "步骤 6/7: 检查 ArgoCD 健康状态"
check_argocd_apps
if [[ "$SKIP_SMOKE" != "true" ]]; then
log "步骤 7/7: 执行 smoke"
run_smoke_subset
else
log "步骤 7/7: 已跳过 smoke(--skip-smoke)"
fi
log "输出 evidence 摘要"
print_evidence_summary
log "E2E 验证完成"