📋 CGA.js 知识库总览

本文档包含 CGA.js 引擎的技术架构、项目统计、测试机制、AI 学习闭环以及详细的技术实现方案。点击左侧目录或下方翻页按钮切换章节。

8
大章节
4
子站点
10
页面
~12,400
引擎源码行
307
功能追踪项
5.45/10
商业化评分

📑 章节目录

一、CGA 解析器自学习技术架构

五层架构设计、数据流闭环、预期收益

二、项目整体结构与站点统计

子站点、页面统计、开发文档梳理

三、函数深度测试与验证机制

契约测试框架、四级验证、CI 集成

四、AI 驱动的 CGA 学习闭环

自动触发、AI 修复、Beta 发布流

五、细化实施路线图

具体代码实现、数据库 DDL、API 设计

六、CGA 函数解析实现

roof 类函数与 splitAndSetbackPerimeter 源码实现详解

七、解析器完整审计与修复路线图

195 个 CGA 条目的全覆盖矩阵、P0-P3 修复优先级、技术债务

八、问题自检与修复

项目全面检查结果、问题清单、修复状态追踪

九、CGA 自动检测与 AI 修复系统

自动扫描、编译验证、几何检测、AI 自动修复流水线

一、CGA 解析器自学习技术架构

目标:通过持续学习 Marketplace 上的 CGA 文件和用户编译日志,自动完善解析器功能,提升函数解析能力与引擎稳定性。支持接入 AI 大模型辅助代码生成与测试。

1. 背景与问题

当前 CGA 引擎基于 ANTLR4 + TypeScript 实现,核心问题:

  • 函数解析能力不足:大量内置函数(如 roofHip, roofGable, split, comp, color, primitiveCube 等)未完全实现
  • 语法覆盖不全:条件语句、循环、变量作用域、数组操作等高级特性缺失
  • 稳定性差:遇到不支持的语法时容易崩溃或无输出
  • 迭代成本高:每次新增功能需要人工分析语法、编写 ANTLR4 规则、实现对应的 TypeScript 逻辑

2. 总体架构(五层)

┌─────────────────────────────────────────────────────────────┐
│  Layer 5: 持续集成与发布层 (CI/CD Release)                   │
│  - A/B 测试、灰度发布、版本回滚                               │
└─────────────────────────────────────────────────────────────┘
                              ↑
┌─────────────────────────────────────────────────────────────┐
│  Layer 4: AI 辅助生成层 (AI Code Generation)                 │
│  - LLM 分析失败代码 → 生成 ANTLR4 规则 + TS 实现             │
│  - LLM 生成测试用例与文档                                     │
└─────────────────────────────────────────────────────────────┘
                              ↑
┌─────────────────────────────────────────────────────────────┐
│  Layer 3: 分析与测试层 (Analysis & Test)                     │
│  - 自动编译测试、错误分类、覆盖率分析                          │
│  - 性能回归测试                                             │
└─────────────────────────────────────────────────────────────┘
                              ↑
┌─────────────────────────────────────────────────────────────┐
│  Layer 2: 数据收集层 (Data Collection)                       │
│  - Marketplace CGA 文件采集                                  │
│  - 用户编译日志(成功/失败/耗时)                             │
│  - 用户反馈(错误报告、功能请求)                             │
└─────────────────────────────────────────────────────────────┘
                              ↑
┌─────────────────────────────────────────────────────────────┐
│  Layer 1: 知识库层 (Knowledge Base)                          │
│  - CGA 语法知识图谱                                          │
│  - 失败案例库(失败代码 + 根因 + 修复方案)                  │
│  - 成功案例库(已支持的语法模式)                             │
└─────────────────────────────────────────────────────────────┘

3. 各层详细设计

Layer 1: 知识库层

3.1.1 CGA 语法知识图谱

  • 存储所有已知的 CGA 语法规则(EBNF/ANTLR4 格式)
  • 每个规则标注:已实现 / 部分实现 / 未实现 / 已弃用
  • 规则关联:哪些函数依赖哪些底层能力(如 roofHip 依赖 extrude + 几何变换)

3.1.2 失败案例库 (cga_failure_cases)

{
  id: string,
  source_code: string,        // 失败的 CGA 源码片段
  error_type: "SYNTAX_ERROR" | "SEMANTIC_ERROR" | "FUNCTION_NOT_IMPLEMENTED" | "RUNTIME_ERROR",
  error_message: string,      // 引擎返回的错误信息
  root_cause: string,         // 人工或 AI 分析的根本原因
  required_grammar: string,   // 需要补充的 ANTLR4 规则
  required_impl: string,      // 需要补充的 TS 实现函数
  frequency: number,          // 出现次数(优先级)
  status: "OPEN" | "IN_PROGRESS" | "FIXED",
  fixed_at: string,           // 修复版本号
  test_case: string           // 对应的回归测试用例
}

3.1.3 成功案例库 (cga_success_cases)

  • 已正确编译的 CGA 代码片段
  • 用于回归测试,确保新功能不破坏已有能力

Layer 2: 数据收集层

3.2.1 Marketplace 文件采集

  • 定时任务:每日扫描 cga_files 表,获取新上传的 CGA 文件
  • 自动下载并入库到 learning_queue
  • 去重:基于文件内容的 SHA256 哈希

3.2.2 用户编译日志采集

  • 已有 usage_logs 表记录每次 /api/v1/compile 调用
  • 需要扩展字段:
    • source_code_hash:源码哈希(用于关联相同代码)
    • error_detail:详细的错误堆栈/输出
    • engine_version:当前引擎版本号
    • cga_features:本次代码使用的 CGA 特性列表(由预处理器提取)

3.2.3 用户反馈采集

  • 在 IDE 页面增加 "报告问题" 按钮
  • 用户可以选择:"这个函数不支持"、"编译结果不对"、"引擎崩溃了"
  • 自动附带当前源码、引擎版本、浏览器信息

Layer 3: 分析与测试层

3.3.1 自动编译测试流水线

# 伪代码
def auto_test_pipeline():
    queue = get_untested_cga_files()
    for file in queue:
        result = compile(file.source_code)
        if result.success:
            record_success(file, result)
            update_coverage_report(file)
        else:
            error = classify_error(result.stderr)
            record_failure(file, error)
            upsert_failure_case(file, error)

3.3.2 错误分类器

错误类型特征处理策略
SYNTAX_ERRORANTLR4 解析失败,token recognition error补充 ANTLR4 语法规则
SEMANTIC_ERROR解析成功但 AST 遍历时报错(如变量未定义)补充语义检查/作用域逻辑
FUNCTION_NOT_IMPLEMENTED解析成功,但运行时提示函数不存在实现对应的 TS 函数
RUNTIME_ERROR执行过程中异常(如除零、空指针)增加边界检查/异常处理
OUTPUT_ERROR编译成功但输出几何体不正确修正几何生成算法

3.3.3 覆盖率分析

  • 语法覆盖率:已实现的 ANTLR4 规则 / 总规则数
  • 函数覆盖率:已实现的内置函数 / CGA 标准函数总数
  • 特性覆盖率:条件语句、循环、数组、材质等高级特性
  • 文件覆盖率:能成功编译的 Marketplace 文件比例

3.3.4 性能回归测试

  • 记录每个 CGA 文件的编译耗时
  • 新版本发布时,对比耗时变化 > 20% 则报警

Layer 4: AI 辅助生成层

3.4.1 LLM 选型

  • 主模型:OpenAI GPT-4 / Claude 3.5 Sonnet / DeepSeek-V3(长上下文 + 强代码能力)
  • 辅助模型:本地部署的 CodeLlama / Qwen-Coder(用于敏感代码的本地分析)

3.4.2 AI 工作流:从失败代码到修复

Step 1: 失败代码提取
  ↓ 从失败案例库取出高频失败代码
Step 2: LLM 分析
  ↓ Prompt: "分析以下 CGA 代码片段,指出引擎不支持的语法点,
             并给出对应的 ANTLR4 语法规则和 TypeScript 实现建议"
Step 3: 代码生成
  ↓ LLM 生成:
    - ANTLR4 `.g4` 规则补丁
    - TypeScript Visitor/Listener 实现
    - 单元测试用例
Step 4: 人工审核
  ↓ 开发者在管理后台审核 AI 生成的代码
Step 5: 自动测试
  ↓ 运行回归测试,确认:
    - 新功能正常工作
    - 旧功能未被破坏
Step 6: 合并发布
  ↓ 合并到 main 分支,自动构建并部署

3.4.3 Prompt 工程示例

你是一位 CGA (Computer Generated Architecture) 编译器专家。

当前引擎遇到一个解析错误:

【失败的 CGA 代码】
{source_code}

【错误信息】
{error_message}

【当前引擎已支持的语法】
{supported_grammar_summary}

请完成以下任务:
1. 分析失败原因,指出具体缺少的语法支持
2. 给出需要补充的 ANTLR4 语法规则(.g4 格式)
3. 给出对应的 TypeScript Visitor 实现代码
4. 给出一个最小可复现的测试用例

要求:
- 代码必须是有效的 TypeScript
- 必须兼容现有的 AST 结构
- 如果涉及几何生成,使用 Three.js 的 BufferGeometry API

3.4.4 AI 自动化任务调度

  • 定时任务(每晚):扫描 failure_cases 表中 frequency > 5status = OPEN 的条目
  • 自动调用 LLM API 生成修复方案
  • 生成的代码存入 ai_suggestions 表,等待人工审核

Layer 5: 持续集成与发布层

3.5.1 自动化测试流水线

# .github/workflows/ci.yml (或本地 GitLab CI)
stages:
  - test
  - ai-review
  - build
  - deploy

test:
  script:
    - npm run test:unit
    - npm run test:grammar-coverage
    - npm run test:marketplace-compilation

ai-review:
  script:
    - python scripts/ai_generate_fixes.py
    - python scripts/ai_generate_tests.py
  only:
    - schedules  #  nightly

build:
  script:
    - npm run build
    - node scripts/bundle.js

deploy:
  script:
    - rsync dist/ server:/www/wwwroot/cgajs-engine/
    - ssh server "pm2 restart cgajs-api"

3.5.2 A/B 测试与灰度发布

  • 新引擎版本部署到 beta.cgajs.com
  • 5% 的用户流量切到 beta 版本
  • 监控指标:编译成功率变化、平均编译耗时变化、用户报错率变化
  • 7 天内无异常,全量发布

3.5.3 版本管理

  • 引擎版本号:major.minor.patch
  • major:破坏性语法变更
  • minor:新增功能/语法支持
  • patch:Bug 修复
  • 每个版本附带:语法覆盖率报告、新增支持的函数列表、性能对比报告

4. 数据流与反馈闭环

┌──────────────┐     ┌──────────────┐     ┌──────────────┐
│  Marketplace │────→│  编译测试    │────→│  失败分类    │
│  CGA 文件    │     │  流水线      │     │  与入库      │
└──────────────┘     └──────────────┘     └──────┬───────┘
                                                  │
┌──────────────┐     ┌──────────────┐     ┌──────▼───────┐
│  引擎更新    │←────│  人工审核    │←────│  AI 生成修复 │
│  与发布      │     │  与合并      │     │  方案        │
└──────────────┘     └──────────────┘     └──────────────┘
       │
       └──────────────────────────────────────────────┐
                                                      │
┌──────────────┐     ┌──────────────┐     ┌──────────▼───┐
│  IDE 用户    │────→│  编译 API    │────→│  日志采集    │
│  编写 CGA    │     │  调用        │     │  与反馈      │
└──────────────┘     └──────────────┘     └──────────────┘

5. 实施路线图

Phase 1: 数据基础设施(1-2 周)

  • 扩展 usage_logs 表,增加 error_detail, engine_version, source_code_hash
  • 创建 cga_failure_casescga_success_cases
  • 创建 learning_queue 自动采集任务
  • IDE 增加 "报告问题" 按钮

Phase 2: 自动测试流水线(2-3 周)

  • 实现 Marketplace 文件的批量编译测试脚本
  • 实现错误分类器
  • 实现覆盖率报告生成
  • 接入 CI/CD(GitHub Actions / GitLab CI)

Phase 3: AI 辅助层(3-4 周)

  • 接入 LLM API(OpenAI / Claude / DeepSeek)
  • 实现 AI 代码生成 Pipeline
  • 开发管理后台的 "AI 建议审核" 页面
  • 实现 Prompt 模板库(针对不同错误类型)

Phase 4: 闭环优化(持续)

  • A/B 测试框架
  • 灰度发布流程
  • 社区贡献者激励(用户提交 CGA 文件可获得 API 额度奖励)
  • 每月发布引擎更新报告

6. 关键技术选型

组件技术选型说明
语法分析ANTLR4 + TypeScript现有技术栈,继续使用
AI 代码生成OpenAI GPT-4 / Claude 3.5长上下文,强代码能力
任务调度Celery + Redis / APSchedulerPython 异步任务队列
数据存储PostgreSQL + 本地文件复用现有数据库
CI/CDGitHub Actions自动化测试与部署
监控Prometheus + Grafana编译成功率、耗时监控
日志采集FastAPI Middleware请求拦截,自动入库

7. 风险与对策

风险对策
AI 生成代码质量不稳定必须人工审核 + 自动化回归测试
LLM API 成本高本地部署 CodeLlama / Qwen-Coder 处理简单任务
新功能破坏旧功能强制要求回归测试通过率 > 99%
Marketplace 文件质量参差不齐增加文件质量评分(编译成功率、下载量)
CGA 语法标准不明确参考 Esri CityEngine 官方文档 + 社区实践

8. 预期收益

  • 3 个月内:引擎编译成功率从当前 ~60% 提升到 ~80%
  • 6 个月内:支持 90% 的常用 CGA 内置函数
  • 12 个月内:Marketplace 上 95% 的 CGA 文件可正常编译
  • 长期:引擎能力随社区贡献自动增长,形成正向飞轮

二、项目整体结构与站点统计

2.1 整体架构

该项目是一个围绕 CGA (CityEngine Shape Grammar) 的在线 3D 建筑生成平台,由多个子系统组成:

层级目录/域名技术栈职责
核心引擎cgajs-engine/TypeScript + ANTLR4 + Three.js + ViteCGA 语法解析、几何生成、GLB 导出
主站前端www.cgajs.com/纯 HTML/CSS/JSIDE、文档、用户系统、管理后台
资源市场marketplace.cgajs.com/纯 HTML/CSS/JSCGA 文件上传、销售、下载
后端 APIcgajs-apiPython + FastAPI + SQLAlchemy + PostgreSQL认证、支付、编译 API、数据管理
插件生态plugins/Ruby (SketchUp)SketchUp 插件 (.rbz)

2.2 子站点统计

目前共 4 个子站点/域名

#子站点说明
1www.cgajs.com主站(IDE + 文档 + 用户系统)
2marketplace.cgajs.comCGA 资源市场(文件交易)
3beta.cgajs.com引擎 Beta 测试环境(CI/CD 部署目标)
4vip.cgajs.comVIP 会员站(已上线)

2.3 页面统计

主站 www.cgajs.com(8 个独立页面)

#页面文件功能说明代码行数
1index.htmlIDE 主页:CGA 代码编辑器 + Three.js 3D 实时预览 + Inspector/AST/Diagnostics 面板59
2study.html知识库/文档中心:运营文档 + 完整技术架构与开发完成度报告1,576
3roadmap.html开发路线图:可交互的功能进度看板,追踪 307 项功能状态831
4plugin.html插件中心:SketchUp 插件下载(免费版/Pro版)、安装教程381
5admin.html管理后台:仪表盘、系统配置、用户/订单/大模型/自学习/版本管理1,170
6auth.html登录/注册:邮箱+密码认证,JWT Token 机制126
7billing.html充值与购买:微信支付扫码、VIP 订阅(199元/年)、积分充值365
8profile.html用户中心:个人资料、API Key 管理、套餐状态、余额、调用统计213

市场站 marketplace.cgajs.com(1 个页面)

#页面文件功能说明代码行数
9index.html市场主页:CGA 文件列表、搜索筛选、文件上传、预览弹窗、分页459

辅助 JS 文件(5 个)

文件功能行数
enhance.jsIDE 核心增强(编辑器、3D 预览、编译交互)1,115
cga-autocomplete.jsCGA 代码自动补全178
inspector-enhance.jsInspector 面板增强103
theme-toggle.js暗色/亮色主题切换84

2.4 开发文档梳理

项目包含两份核心开发文档:

文档 1:CGA_LEARNING_ARCHITECTURE.md(引擎层)

  • 主题:CGA 解析器自学习技术架构
  • 内容:五层架构设计(知识库层 → 数据收集层 → 分析测试层 → AI 辅助生成层 → CI/CD 发布层)
  • 目标:通过 AI 大模型(Kimi/GPT-4/Claude)自动分析失败 CGA 代码,生成 ANTLR4 规则和 TS 修复代码,实现引擎能力的自动增长

文档 2:study.html(运营 + 技术完整报告)

这份 1,576 行的文档实际上是一个内嵌在网页中的完整技术白皮书,包含:

运营文档部分
  • 大模型配置指南(Kimi / 千问 API Key 获取与配置)
  • 自学习系统操作指南(Marketplace 扫描 → AI 分析 → 人工审核)
  • 版本管理与灰度发布流程
  • Marketplace 集成与分成规则(开发者 80% / 平台 20%)
  • 插件开发指南(API 接口、SketchUp/Rhino/Blender/Unity 等平台适配)
技术架构部分
  • 项目概述:147 个 Marketplace 文件编译成功率 100%,~12,400 行 TS 源码
  • Pipeline 架构图:Parser → AST → Evaluator → Geometry → Scene Builder → GLB Export
  • 模块完成度
    • Parser & AST Builder:85%
    • Runtime Evaluator:78%
    • Built-in Functions:60%
    • Geometry Operations:55%
    • Scene Builder & GLB Export:72%
    • Engine API & Import Resolution:58%
  • 50 项详细问题清单(关键 Bug / 语义丢失 / 设计限制)
  • 商业化差距分析:总分 5.45/10,判定为 Alpha→Beta 过渡期
  • API 接口文档(CLI / JS API / REST API 三种调用方式)

2.5 总结

4
子站点/域名
9
独立 HTML 页面
5
前端 JS 辅助文件
~12,400
核心引擎 TS 源码行
28
核心源文件数量
147/147
Marketplace 编译成功率
307
功能追踪项(roadmap)
5.45/10
商业化就绪度评分

三、函数深度测试与验证机制

当前引擎存在大量"伪实现"函数:语法解析通过、执行不报错,但无任何几何效果(如 roofGable 执行后 silently fail)。
本机制目标:为每个 CGA 函数建立可量化、可自动验证的契约测试,确保实现与 CityEngine 语义等价。

3.1 核心问题定义

问题层级现象示例
L1: 解析通过Parser 能识别函数名和参数roofGable(30) 生成 AST 节点
L2: 执行通过Evaluator 执行不抛异常返回 shape 但几何未改变
L3: 几何生成生成了顶点/面但结构错误roof 生成平面而非坡屋顶
L4: 语义正确与 CityEngine 输出等价坡屋顶角度、悬挑、厚度正确

当前大量函数停留在 L1/L2,需要强制推进到 L4。

3.2 函数契约测试框架(Contract Testing)

3.2.1 测试用例结构

每个函数至少配置 3~5 个测试用例:正常输入、边界条件、组合操作。

{
  "function": "roofGable",
  "cases": [
    {
      "id": "roofGable-basic",
      "name": "标准矩形顶面生成坡屋顶",
      "initialShape": {
        "type": "polygon",
        "vertices": [[-5,0,-5],[5,0,-5],[5,0,5],[-5,0,5]]
      },
      "cga": "Lot --> extrude(10) comp(f) { top: roofGable(30) }",
      "assertions": {
        "geometryNotEmpty": true,
        "minVertices": 12,
        "minFaces": 8,
        "boundingBox": { "y": [10, 16] },
        "hasSlopedFaces": true,
        "maxFaceAngle": 35
      }
    }
  ]
}

3.2.2 四级验证断言

级别断言类型检查内容适用阶段
A非空断言vertexCount > 0 && faceCount > 0所有函数
B结构断言包围盒范围、面数范围、顶点拓扑闭合几何创建类
C几何断言法线方向、面角度、共面性、无重复顶点变换/细分/布尔类
D语义断言与 CityEngine 参考输出对比(顶点数误差 < 5%,包围盒 IoU > 0.95)关键函数(roof/extrude/split/comp)

3.2.3 自动化测试流水线

┌─────────────────────────────────────────────────────────────┐
│              Function Contract Test Pipeline                │
├─────────────────────────────────────────────────────────────┤
│  1. 加载测试用例 JSON                                       │
│  2. 构建初始 Shape (polygon / box)                          │
│  3. 执行 CGA 源码                                           │
│  4. 捕获输出几何 (BufferGeometry)                           │
│  5. 运行 A→B→C→D 断言检查                                   │
│  6. 生成报告: { passed, level, details, screenshots }       │
└─────────────────────────────────────────────────────────────┘

3.3 测试覆盖率仪表盘

在管理后台新增 函数测试中心

  • 函数矩阵视图:每个函数的 L1~L4 通过状态(色块:红/黄/绿/深绿)
  • 一键重测:支持单个函数 / 全量函数 / 仅失败函数 三种模式
  • 失败详情:预期 vs 实际(顶点数、包围盒、截图对比)
  • 趋势图:每个函数的测试通过率历史曲线

3.4 与 CI/CD 集成

  • 门禁测试:每次引擎构建前,全量函数测试必须通过 > 80%(其中 L4 关键函数必须 100%)
  • 夜间回归:每日凌晨自动运行全部函数测试 + Marketplace 批量编译
  • 失败阻断:若关键函数(extrude/split/comp/roof/color)测试失败,自动阻止该版本进入 beta

四、AI 驱动的 CGA 学习闭环与版本发布系统

目标:建立从 Marketplace 文件采集 → 自动检测 → AI 诊断修复 → Beta 验证 → 管理员审核 → 生产发布的完整闭环。
关键约束:解析器目前较弱,必须先建立问题可定位、修复可验证的基础设施,再让 AI 介入。

4.1 整体闭环流程

┌─────────────────────────────────────────────────────────────────────────────┐
│                        AI Learning & Release Loop                           │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│   Marketplace 上传 CGA 文件                                                  │
│        │                                                                    │
│        ▼                                                                    │
│   ┌─────────────────┐                                                       │
│   │ 定时扫描触发器   │  ◄── 每日凌晨 2:00                                   │
│   │ (条件: 有新文件   │  ◄── 或累积 ≥5 个新文件                              │
│   │  或无文件则跳过)  │                                                       │
│   └────────┬────────┘                                                       │
│            ▼                                                                │
│   ┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐      │
│   │   解析器编译      │────►│   几何验证       │────►│   问题分类       │      │
│   │   (记录日志)      │     │   (空/错误/正常)  │     │   (定位到函数)   │      │
│   └─────────────────┘     └─────────────────┘     └────────┬────────┘      │
│                                                            │                │
│                         ┌──────────────────────────────────┘                │
│                         ▼                                                   │
│   ┌─────────────────────────────────────────────────────────────────────┐  │
│   │                         AI 诊断修复引擎                              │  │
│   │  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐  ┌────────────┐ │  │
│   │  │ 错误日志分析  │→│ 根因定位      │→│ 代码生成      │→│ 测试验证    │ │  │
│   │  │ (LLM Prompt)│  │ (函数级别)    │  │ (TS Patch)   │  │ (隔离环境)  │ │  │
│   │  └─────────────┘  └─────────────┘  └─────────────┘  └────────────┘ │  │
│   └─────────────────────────────────────────────────────────────────────┘  │
│                                    │                                        │
│                                    ▼                                        │
│   ┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐      │
│   │  生成修复建议     │────►│  人工审核后台    │────►│  构建 Beta 版本  │      │
│   │  (含 diff + 报告) │     │  (admin.html)   │     │  (beta.cgajs)   │      │
│   └─────────────────┘     └────────┬────────┘     └────────┬────────┘      │
│                                    │                       │               │
│                              拒绝  │  接受                 ▼               │
│                                    │            ┌─────────────────┐        │
│                                    │            │ 管理员验证 Beta  │        │
│                                    │            │ (点击发布/回滚)  │        │
│                                    │            └────────┬────────┘        │
│                                    │                     │                 │
│                                    │              通过   │  发现问题        │
│                                    │                     ▼                 │
│                                    │            ┌─────────────────┐        │
│                                    └───────────►│  发布到 www      │        │
│                                                 │  (生产环境)      │        │
│                                                 └─────────────────┘        │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

4.2 自动化触发策略

触发条件行为说明
每日定时(02:00)扫描 marketplace 新文件只要当天有新文件就执行检测
累积触发(≥5 个新文件)立即启动 AI 深度分析避免文件堆积,快速响应
无新文件跳过检测节省 API 调用成本
函数测试失败(CI)自动创建 AI 修复任务不依赖 marketplace 文件
管理员手动触发全量重测 + AI 分析用于发布前验证

4.3 模型准确度多维度验证

由于无法直接判断"模型是否正确",建立以下多维度验证体系:

维度 1:基础几何验证(必做)

  • 编译是否成功(success = true)
  • 是否生成非空几何(vertexCount > 0 && faceCount > 0)
  • 包围盒是否合理(非零体积、无 NaN/Inf)
  • 是否有退化面(面积为零的三角形)

维度 2:语义规则验证(进阶)

  • 属性响应验证:修改 CGA 中的 attr 值后,输出几何是否相应变化
  • 操作链验证extrude → comp → split 的层级结构是否符合预期
  • 选择器验证comp(f) { top: ... | side: ... } 是否生成了不同数量的面

维度 3:参考对比验证(高阶)

  • 收集 CityEngine 官方示例 CGA 的参考输出(手动导出 GLB 作为基准)
  • 对比顶点数、面数、包围盒 IoU(Intersection over Union)
  • 阈值:顶点数误差 < 5%,IoU > 0.90 判定为通过

维度 4:视觉快照验证(辅助)

  • 使用 Puppeteer/Playwright 在 headless Chrome 中渲染 Three.js 场景
  • 生成 4 个视角截图(正视/俯视/侧视/透视)
  • 与参考截图做像素级 diff(SSIM > 0.85)

4.4 AI 修复工作流(6 步)

1

问题定位(自动)

分析失败日志,定位到具体函数和源码位置。例如:roofGablesrc/geometry/operations/create/roof.ts:88 返回空数组。

2

上下文收集(自动)

收集:失败 CGA 代码片段 + 当前引擎该函数的实现代码 + CityEngine 官方文档描述 + 参考 GLB 输出(如有)。

3

LLM 诊断与修复(AI)

调用大模型生成修复方案。Prompt 必须包含:失败现象、根因分析、修复后的 TypeScript 代码、单元测试用例。

4

隔离验证(自动)

在临时分支应用修复,运行:a) 原失败用例 b) 函数契约测试 c) 全量回归测试(Marketplace 147 个文件)。通过率 > 95% 才允许进入下一步。

5

生成 Beta 版本(自动)

自动递增 patch 版本号(如 0.1.3 → 0.1.4),构建产物,部署到 beta.cgajs.com,并在 admin 后台创建 "待审核发布" 记录。

6

管理员审核与发布(人工)

管理员在后台查看:修改了哪些函数、AI 生成的代码 diff、测试报告、Beta 验证链接。确认无误后点击"发布到生产",自动部署到 www.cgajs.com

4.5 核心数据模型设计

表 1:cga_learning_sessions(学习会话)

id: UUID
session_type: "scheduled" | "accumulated" | "manual" | "ci_failure"
status: "running" | "completed" | "failed"
file_count: int          -- 本次检测的 CGA 文件数
new_suggestions: int      -- AI 生成的新建议数
accepted_suggestions: int -- 已接受的建议数
started_at: timestamp
completed_at: timestamp

表 2:cga_file_checks(文件检测结果)

id: UUID
session_id: UUID -> cga_learning_sessions
file_id: UUID -> cga_files
source_code_hash: string
parse_success: bool
compile_success: bool
vertex_count: int
face_count: int
is_empty_geometry: bool
error_type: "SYNTAX" | "SEMANTIC" | "FUNCTION_MISSING" | "RUNTIME" | "EMPTY_OUTPUT" | null
error_detail: text
failed_function: string   -- 定位到的疑似问题函数
created_at: timestamp

表 3:ai_suggestions(AI 修复建议)

id: UUID
session_id: UUID -> cga_learning_sessions
target_function: string    -- 如 "roofGable"
issue_type: "NO_GEOMETRY" | "WRONG_GEOMETRY" | "CRASH" | "NOT_IMPLEMENTED"
description: text          -- AI 对问题的自然语言描述
root_cause: text           -- 根因分析
generated_code: text       -- AI 生成的 TS 修复代码
generated_test: text       -- AI 生成的测试用例
diff_summary: text         -- 变更摘要(用于后台列表展示)
status: "pending" | "approved" | "rejected" | "merged"
reviewed_by: int -> user_id (nullable)
reviewed_at: timestamp (nullable)
created_at: timestamp

表 4:engine_releases(引擎版本发布)

id: UUID
version: string            -- 语义化版本,如 "0.1.4"
changelog: text             -- 自动生成的变更日志
base_commit: string         -- 基于哪个 git commit
suggestion_ids: UUID[]      -- 包含哪些 AI 建议的修复
status: "building" | "beta" | "production" | "rolled_back"
build_log: text
test_pass_rate: float       -- 函数测试通过率
regression_pass_rate: float -- Marketplace 回归测试通过率
beta_deployed_at: timestamp
prod_deployed_at: timestamp
deployed_by: int -> user_id (nullable)
created_at: timestamp

4.6 管理员后台功能扩展

新增模块 1:AI 学习监控中心 (/admin.html#/ai-learning)

  • 会话列表:每次自动/手动检测的记录,显示检测文件数、发现问题数、AI 建议数
  • 文件检测结果:CGA 文件名、编译状态、几何状态、定位到的问题函数
  • AI 建议列表
    • 待审核:展示问题函数、问题简述、AI 生成的代码预览(折叠)
    • 已接受:展示合并后的版本号、测试通过率
    • 已拒绝:展示拒绝原因(管理员填写)
  • 操作按钮:接受建议(触发 Beta 构建)、拒绝建议、手动触发全量检测

新增模块 2:版本发布管理 (/admin.html#/releases)

  • 版本流水线看板:Dev → Beta → Production 的三列看板
  • 版本卡片信息:版本号、基于哪些 AI 建议、修改函数列表、测试通过率、构建时间
  • Beta 验证区:一键打开 beta.cgajs.com 并预加载测试 CGA 代码
  • 操作
    • 构建 Beta:将已接受的 AI 建议合并构建
    • 发布生产:Beta 验证通过后,一键部署到 www.cgajs.com
    • 紧急回滚:发现严重问题,回滚到上一个 production 版本

新增模块 3:函数测试中心 (/admin.html#/function-tests)

  • 函数矩阵:所有 CGA 函数 × L1~L4 四级验证状态(色块热力图)
  • 测试运行:单个函数测试 / 全量测试 / 仅失败函数重测
  • 失败详情:预期值 vs 实际值、顶点/面数对比、3D 预览截图
  • 趋势图表:近 30 天各函数测试通过率变化曲线

4.7 实施路线图(建议)

Phase 1:函数测试基础设施(2 周)—— 优先

这是整个闭环的前提。如果无法判断函数是否正确,AI 也无法知道"修好了没有"。

  • 为 Top 20 关键函数(extrude, split, comp, roof*, color, primitive*, center, mirror, offset, setback)编写契约测试用例
  • 实现四级验证框架(A/B/C/D)
  • 接入 CI:每次构建前必须跑通关键函数测试
  • 后台新增"函数测试中心"页面

Phase 2:问题定位与分类(1 周)

  • 增强编译日志:记录每个操作步骤的执行结果、耗时、异常堆栈
  • 实现"失败函数自动定位":当 CGA 编译成功但几何为空时,通过 trace 定位最后执行的操作
  • 建立 cga_file_checks 表,记录每次检测的详细结果

Phase 3:AI 修复 Pipeline(2 周)

  • 编写结构化 Prompt:输入(失败代码 + 当前实现 + 文档)→ 输出(根因 + 修复代码 + 测试用例)
  • 实现隔离验证环境:临时分支 → 应用修复 → 自动测试 → 生成报告
  • 建立 ai_suggestions 表和后台审核页面

Phase 4:Beta/Production 发布流(1 周)

  • 实现自动版本递增、构建、部署到 beta.cgajs.com
  • 后台"版本发布管理"页面:看板 + 发布/回滚按钮
  • 管理员通知机制:新版本待审核时,后台红点提醒 + 可选邮件/企微通知

Phase 5:全量自动化闭环(持续)

  • 定时任务:每日扫描 + 累积触发
  • 扩展 AI Prompt 库:针对不同错误类型(语法/几何/性能)使用不同 Prompt
  • 建立"参考输出库":收集 CityEngine 官方示例的标准 GLB 输出,用于 D 级语义验证

4.8 关键技术要点

要点说明
先测后修没有测试就没法验证修复效果。优先建立函数契约测试,再让 AI 介入。
隔离验证AI 生成的代码必须在独立分支验证,通过全部测试后才能合并,防止破坏现有功能。
人工兜底Beta → Production 的发布必须有管理员手动确认。AI 只负责生成建议,发布权限归人。
渐进发布Beta 环境运行 24~72 小时,观察编译成功率和报错率无异常后再上生产。
成本可控无新文件时不触发 AI 分析;单个函数的 AI 修复建议被拒绝 3 次以上则转人工标注。

五、细化实施路线图 —— 具体技术实现方案

本章节将前文五个 Phase 细化到可直接执行的粒度:包含具体文件路径、代码实现、数据库 DDL、API 接口、前端组件、配置命令和测试方法。

Phase 1:函数测试基础设施(第 1~2 周)

前提假设:必须先完成本阶段,否则 AI 无法验证"修好了没有"。

Step 1.1 创建测试用例目录与数据结构

目录结构

cgajs-engine/
├─ src/
│  └─ testing/
│     ├─ contracts/           # 测试用例定义
│     │  ├─ index.ts          # 导出所有测试套件
│     │  ├─ geometry-creation.ts   # extrude, roof*, primitive* 等
│     │  ├─ geometry-subdivision.ts # split, comp, setback 等
│     │  ├─ transformations.ts     # t, r, s, center, mirror
│     │  └─ materials.ts           # color, texture
│     ├─ validator.ts         # 四级验证引擎
│     ├─ runner.ts            # 测试运行器
│     ├─ reporter.ts          # 报告生成器
│     └─ reference-loader.ts  # 参考 GLB 加载对比
├─ tests/
│  └─ reference-outputs/      # CityEngine 导出的标准 GLB
│     ├─ roofGable-basic.glb
│     ├─ extrude-10-box.glb
│     └─ ...
└─ scripts/
   └─ run-function-tests.mjs  # CLI 入口

测试用例类型定义(src/testing/types.ts

export interface ShapeAssertion {
  geometryNotEmpty?: boolean;
  minVertices?: number;
  minFaces?: number;
  maxVertices?: number;
  maxFaces?: number;
  boundingBox?: {
    x?: [number, number];
    y?: [number, number];
    z?: [number, number];
  };
  hasSlopedFaces?: boolean;      // C级:是否有斜面
  maxFaceAngle?: number;         // C级:面与水平面最大夹角
  noDegenerateFaces?: boolean;   // C级:无退化面
  iouWithReference?: number;     // D级:与参考GLB的IoU阈值
}

export interface FunctionTestCase {
  id: string;
  name: string;
  function: string;              // 被测函数名
  initialShape: {
    type: 'polygon' | 'box';
    vertices?: number[][];       // polygon 用
    width?: number; height?: number; depth?: number; // box 用
  };
  cga: string;                   // 测试用的 CGA 源码
  level: 'A' | 'B' | 'C' | 'D';  // 测试级别
  assertions: ShapeAssertion;
  referenceFile?: string;        // D级:参考GLB文件名
}

export interface TestResult {
  caseId: string;
  function: string;
  level: string;
  passed: boolean;
  assertions: Record<string, { expected: any; actual: any; passed: boolean }>;
  durationMs: number;
  error?: string;
}

测试用例示例(src/testing/contracts/geometry-creation.ts

import { FunctionTestCase } from '../types';

export const roofTestCases: FunctionTestCase[] = [
  {
    id: 'roofGable-basic-rect',
    name: '矩形顶面生成人字屋顶',
    function: 'roofGable',
    initialShape: {
      type: 'polygon',
      vertices: [[-5,0,-5],[5,0,-5],[5,0,5],[-5,0,5]]
    },
    cga: 'Lot --> extrude(10) comp(f) { top: roofGable(30) }',
    level: 'C',
    assertions: {
      geometryNotEmpty: true,
      minVertices: 10,
      minFaces: 6,
      boundingBox: { y: [10, 16] },
      hasSlopedFaces: true,
      maxFaceAngle: 35
    }
  },
  {
    id: 'roofGable-empty-input',
    name: '退化输入应处理不崩溃',
    function: 'roofGable',
    initialShape: { type: 'polygon', vertices: [[0,0,0],[1,0,0]] },
    cga: 'Lot --> roofGable(30)',
    level: 'A',
    assertions: {
      geometryNotEmpty: false  // 允许空,但不允许崩溃
    }
  }
];

export const extrudeTestCases: FunctionTestCase[] = [
  {
    id: 'extrude-box-10',
    name: '矩形挤出10单位',
    function: 'extrude',
    initialShape: {
      type: 'polygon',
      vertices: [[-5,0,-5],[5,0,-5],[5,0,5],[-5,0,5]]
    },
    cga: 'Lot --> extrude(10)',
    level: 'D',
    assertions: {
      geometryNotEmpty: true,
      minVertices: 8,
      minFaces: 12,
      boundingBox: { y: [0, 10] },
      noDegenerateFaces: true,
      iouWithReference: 0.95
    },
    referenceFile: 'extrude-10-box.glb'
  }
];

Step 1.2 实现四级验证引擎(src/testing/validator.ts

import { Box3, Vector3 } from 'three';
import { ShapeAssertion, TestResult } from './types';

export class GeometryValidator {
  validate(geometry: THREE.BufferGeometry, assertions: ShapeAssertion): TestResult['assertions'] {
    const results: TestResult['assertions'] = {};
    const posAttr = geometry.getAttribute('position');
    const vertices = posAttr ? posAttr.count : 0;
    const indices = geometry.getIndex();
    const faces = indices ? indices.count / 3 : 0;

    // A级:非空断言
    if (assertions.geometryNotEmpty !== undefined) {
      const actual = vertices > 0 && faces > 0;
      results.geometryNotEmpty = {
        expected: assertions.geometryNotEmpty,
        actual,
        passed: actual === assertions.geometryNotEmpty
      };
    }

    // B级:结构断言
    if (assertions.minVertices !== undefined) {
      results.minVertices = {
        expected: `>= ${assertions.minVertices}`,
        actual: vertices,
        passed: vertices >= assertions.minVertices
      };
    }
    if (assertions.minFaces !== undefined) {
      results.minFaces = {
        expected: `>= ${assertions.minFaces}`,
        actual: faces,
        passed: faces >= assertions.minFaces
      };
    }
    if (assertions.boundingBox) {
      const box = new Box3().setFromBufferAttribute(posAttr as any);
      const min = box.min, max = box.max;
      const expected = assertions.boundingBox;
      let passed = true;
      if (expected.x) passed = passed && min.x >= expected.x[0] && max.x <= expected.x[1];
      if (expected.y) passed = passed && min.y >= expected.y[0] && max.y <= expected.y[1];
      if (expected.z) passed = passed && min.z >= expected.z[0] && max.z <= expected.z[1];
      results.boundingBox = { expected, actual: { min, max }, passed };
    }

    // C级:几何断言
    if (assertions.hasSlopedFaces || assertions.maxFaceAngle) {
      const { hasSloped, maxAngle } = this.analyzeFaceAngles(geometry);
      if (assertions.hasSlopedFaces) {
        results.hasSlopedFaces = {
          expected: true, actual: hasSloped, passed: hasSloped
        };
      }
      if (assertions.maxFaceAngle) {
        results.maxFaceAngle = {
          expected: `<= ${assertions.maxFaceAngle}`,
          actual: maxAngle,
          passed: maxAngle <= assertions.maxFaceAngle
        };
      }
    }

    // D级:语义断言(IoU 对比)
    if (assertions.iouWithReference && this.referenceGeometry) {
      const iou = this.computeIoU(geometry, this.referenceGeometry);
      results.iouWithReference = {
        expected: `>= ${assertions.iouWithReference}`,
        actual: iou,
        passed: iou >= assertions.iouWithReference
      };
    }

    return results;
  }

  private analyzeFaceAngles(geometry: THREE.BufferGeometry) {
    // 计算每个三角面法线与 Y 轴夹角,判断是否有斜面
    const pos = geometry.getAttribute('position') as THREE.BufferAttribute;
    const idx = geometry.getIndex();
    let hasSloped = false;
    let maxAngle = 0;
    const yAxis = new Vector3(0, 1, 0);
    if (!idx) return { hasSloped, maxAngle };
    for (let i = 0; i < idx.count; i += 3) {
      const a = new Vector3().fromBufferAttribute(pos, idx.getX(i));
      const b = new Vector3().fromBufferAttribute(pos, idx.getX(i+1));
      const c = new Vector3().fromBufferAttribute(pos, idx.getX(i+2));
      const normal = new Vector3().crossVectors(
        new Vector3().subVectors(b, a),
        new Vector3().subVectors(c, a)
      ).normalize();
      const angle = Math.acos(Math.abs(normal.dot(yAxis))) * (180 / Math.PI);
      if (angle > 5 && angle < 85) hasSloped = true;
      maxAngle = Math.max(maxAngle, angle);
    }
    return { hasSloped, maxAngle };
  }

  private computeIoU(a: THREE.BufferGeometry, b: THREE.BufferGeometry): number {
    // 简化版:比较包围盒 IoU;精确版可用 mesh-voxelization
    const boxA = new Box3().setFromBufferAttribute(a.getAttribute('position') as any);
    const boxB = new Box3().setFromBufferAttribute(b.getAttribute('position') as any);
    const inter = new Box3().copy(boxA).intersect(boxB);
    const volA = this.boxVolume(boxA);
    const volB = this.boxVolume(boxB);
    const volI = this.boxVolume(inter);
    return volI / (volA + volB - volI + 1e-10);
  }

  private boxVolume(box: Box3) {
    const s = box.getSize(new Vector3());
    return s.x * s.y * s.z;
  }
}

Step 1.3 实现测试运行器(src/testing/runner.ts

import { QajsEngine } from '../api/engine';
import { GeometryValidator } from './validator';
import { FunctionTestCase, TestResult } from './types';
import * as fs from 'fs';
import * as path from 'path';

export class FunctionTestRunner {
  private engine = new QajsEngine();
  private validator = new GeometryValidator();

  async runCase(testCase: FunctionTestCase): Promise<TestResult> {
    const start = Date.now();
    try {
      // 1. 构建初始形状
      const initialShape = this.buildInitialShape(testCase.initialShape);

      // 2. 执行编译
      const result = await this.engine.compile({
        source: testCase.cga,
        initialShape
      });

      if (!result.success) {
        return {
          caseId: testCase.id,
          function: testCase.function,
          level: testCase.level,
          passed: false,
          assertions: {},
          durationMs: Date.now() - start,
          error: result.errors.map(e => e.message).join('; ')
        };
      }

      // 3. 获取主几何体(取第一个非空 shape 的 geometry)
      // 这里假设 engine 返回 sceneJson,实际需要根据实际 API 调整
      const scene = result.sceneJson as any;
      const mesh = scene?.children?.[0];
      if (!mesh || !mesh.geometry) {
        return {
          caseId: testCase.id, function: testCase.function, level: testCase.level,
          passed: testCase.level === 'A' ? true : false,  // A级允许空
          assertions: { geometryNotEmpty: { expected: true, actual: false, passed: false } },
          durationMs: Date.now() - start,
          error: 'No geometry generated'
        };
      }

      // 4. 运行断言验证
      const assertionResults = this.validator.validate(mesh.geometry, testCase.assertions);
      const allPassed = Object.values(assertionResults).every(r => r.passed);

      return {
        caseId: testCase.id,
        function: testCase.function,
        level: testCase.level,
        passed: allPassed,
        assertions: assertionResults,
        durationMs: Date.now() - start
      };
    } catch (err) {
      return {
        caseId: testCase.id,
        function: testCase.function,
        level: testCase.level,
        passed: false,
        assertions: {},
        durationMs: Date.now() - start,
        error: err instanceof Error ? err.stack : String(err)
      };
    }
  }

  async runSuite(cases: FunctionTestCase[]): Promise<TestResult[]> {
    const results: TestResult[] = [];
    for (const c of cases) {
      const r = await this.runCase(c);
      results.push(r);
      console.log(`${r.passed ? '✓' : '✗'} ${c.function}::${c.id} (${c.level}) ${r.error || ''}`);
    }
    return results;
  }

  private buildInitialShape(cfg: FunctionTestCase['initialShape']) {
    if (cfg.type === 'box') {
      return {
        geometry: { type: 'box', width: cfg.width, height: cfg.height, depth: cfg.depth }
      };
    }
    return {
      geometry: { type: 'polygon', vertices: cfg.vertices }
    };
  }
}

Step 1.4 CLI 入口与 CI 集成(scripts/run-function-tests.mjs

#!/usr/bin/env node
import { FunctionTestRunner } from '../dist/testing/runner.js';
import { allTestCases } from '../dist/testing/contracts/index.js';
import { writeFileSync } from 'fs';

async function main() {
  const runner = new FunctionTestRunner();
  const results = await runner.runSuite(allTestCases);

  // 生成报告
  const passed = results.filter(r => r.passed).length;
  const failed = results.filter(r => !r.passed);
  const report = {
    total: results.length,
    passed,
    failed: failed.length,
    passRate: (passed / results.length * 100).toFixed(1) + '%',
    byLevel: {
      A: { total: 0, passed: 0 },
      B: { total: 0, passed: 0 },
      C: { total: 0, passed: 0 },
      D: { total: 0, passed: 0 }
    },
    failures: failed.map(r => ({
      caseId: r.caseId,
      function: r.function,
      level: r.level,
      error: r.error,
      failedAssertions: Object.entries(r.assertions)
        .filter(([_, v]) => !v.passed)
        .map(([k, v]) => ({ assert: k, expected: v.expected, actual: v.actual }))
    })),
    generatedAt: new Date().toISOString()
  };

  for (const r of results) {
    report.byLevel[r.level].total++;
    if (r.passed) report.byLevel[r.level].passed++;
  }

  writeFileSync('/tmp/cgajs-function-test-report.json', JSON.stringify(report, null, 2));
  console.log(`\n📊 Results: ${passed}/${results.length} passed (${report.passRate})`);

  // CI 门禁:关键函数(A/B/C级)必须全部通过
  const criticalFailed = failed.filter(r => r.level !== 'D');
  if (criticalFailed.length > 0) {
    console.error(`\n❌ ${criticalFailed.length} critical tests failed. Blocking build.`);
    process.exit(1);
  }
  process.exit(0);
}

main().catch(e => { console.error(e); process.exit(1); });

package.json 新增脚本

"scripts": {
  "test:functions": "node scripts/run-function-tests.mjs",
  "test:functions:watch": "node scripts/run-function-tests.mjs --watch"
}

Step 1.5 后台函数测试中心页面(admin.html)

在 admin.html 的 sidebar 新增导航项:

<button class="nav-item" onclick="showPage('functionTests', this)">🧪 函数测试</button>

新增页面容器(简化版核心逻辑):

<div class="page" id="page-functionTests">
  <h1>函数测试中心</h1>
  <div class="stats" id="ft-stats"></div>
  <div class="card">
    <h3>操作</h3>
    <button class="btn btn-primary" onclick="runFunctionTests('all')">运行全部测试</button>
    <button class="btn btn-primary" onclick="runFunctionTests('failed')">仅测失败项</button>
    <span id="ft-status" style="margin-left:12px;font-size:13px;color:#8b949e"></span>
  </div>
  <div class="card">
    <h3>函数矩阵</h3>
    <div id="ft-matrix" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:8px"></div>
  </div>
  <div class="card">
    <h3>失败详情</h3>
    <table><thead><tr><th>函数</th><th>用例</th><th>级别</th><th>失败断言</th></tr></thead>
    <tbody id="ft-failures"></tbody></table>
  </div>
</div>

API 接口(后端 FastAPI 新增):

# cgajs-api 新增路由
@router.post("/admin/function-tests/run")
async def run_function_tests(scope: str = "all", background_tasks: BackgroundTasks = ...):
    # 在后台运行测试脚本
    background_tasks.add_task(run_test_script, scope)
    return {"message": "测试已启动", "scope": scope}

@router.get("/admin/function-tests/results")
async def get_function_test_results():
    report = load_latest_report()
    return report

@router.get("/admin/function-tests/history")
async def get_function_test_history(function: str = None, limit: int = 30):
    # 返回近30天的测试历史,用于趋势图
    return db.query(FunctionTestHistory).filter(...).all()

Phase 2:问题定位与分类(第 3 周)

Step 2.1 增强编译执行 Trace 日志

修改 src/runtime/evaluator.ts,在每个操作执行前后记录日志:

// 在 evaluator.ts 的 evalOperation 或 evalSimpleOperation 中
private traceLog: Array<{ op: string; function?: string; inputScope: any; outputScope: any; durationMs: number; error?: string }> = [];

async evalOperation(ctx: EvalContext, op: Operation, shape: Shape): Promise<Shape[]> {
  const start = performance.now();
  const inputScope = shape.scope.clone();
  try {
    const results = await this.evalOperationInner(ctx, op, shape);
    this.traceLog.push({
      op: op.type,
      function: (op as any).name,
      inputScope,
      outputScope: results.map(r => r.scope),
      durationMs: performance.now() - start
    });
    return results;
  } catch (err) {
    this.traceLog.push({
      op: op.type,
      function: (op as any).name,
      inputScope,
      outputScope: [],
      durationMs: performance.now() - start,
      error: err instanceof Error ? err.message : String(err)
    });
    throw err;
  }
}

src/api/engine.tscompile() 方法返回 traceLog:

return {
  success: true,
  // ... 原有字段
  traceLog: this.evaluator.traceLog,   // 新增:操作执行轨迹
  metadata: {
    // ... 原有字段
    lastOperation: this.evaluator.traceLog.at(-1)?.op
  }
};

Step 2.2 失败函数自动定位算法

新建 src/testing/failure-analyzer.ts

export interface FailureAnalysis {
  fileId: string;
  compileSuccess: boolean;
  geometryEmpty: boolean;
  suspectedFunction: string | null;  // 定位到的问题函数
  lastSuccessfulOp: string | null;
  firstFailedOp: string | null;
  errorType: 'SYNTAX' | 'RUNTIME' | 'EMPTY_OUTPUT' | 'WRONG_GEOMETRY';
  recommendation: string;
}

export function analyzeFailure(
  compileResult: any,
  traceLog: any[]
): FailureAnalysis {
  // 情况1: 编译失败
  if (!compileResult.success) {
    const errMsg = compileResult.errors?.[0]?.message || '';
    const funcMatch = errMsg.match(/function\s+(\w+)/i) || errMsg.match(/(\w+)\s+is not implemented/i);
    return {
      fileId: compileResult.fileId,
      compileSuccess: false,
      geometryEmpty: true,
      suspectedFunction: funcMatch ? funcMatch[1] : null,
      lastSuccessfulOp: null,
      firstFailedOp: null,
      errorType: errMsg.includes('token') || errMsg.includes('syntax') ? 'SYNTAX' : 'RUNTIME',
      recommendation: funcMatch
        ? `函数 ${funcMatch[1]} 未实现或调用异常,建议补充实现`
        : '语法解析错误,建议检查 ANTLR4 规则'
    };
  }

  // 情况2: 编译成功但几何为空
  const vertexCount = compileResult.metadata?.vertexCount || 0;
  if (vertexCount === 0) {
    // 从 traceLog 找最后一个有输入但没输出的操作
    const suspicious = traceLog.slice().reverse().find(
      t => t.outputScope?.length === 0 || !t.outputScope
    );
    return {
      fileId: compileResult.fileId,
      compileSuccess: true,
      geometryEmpty: true,
      suspectedFunction: suspicious?.function || suspicious?.op || null,
      lastSuccessfulOp: traceLog.filter(t => !t.error).at(-1)?.op || null,
      firstFailedOp: suspicious?.op || null,
      errorType: 'EMPTY_OUTPUT',
      recommendation: suspicious?.function
        ? `操作 ${suspicious.function} 执行后未生成几何体,可能是 stub 实现`
        : '所有操作执行完毕但未生成几何,可能是规则链未正确连接'
    };
  }

  // 情况3: 编译成功且有几何,但可能结构错误(由契约测试判断)
  return {
    fileId: compileResult.fileId,
    compileSuccess: true,
    geometryEmpty: false,
    suspectedFunction: null,
    lastSuccessfulOp: traceLog.at(-1)?.op,
    firstFailedOp: null,
    errorType: 'WRONG_GEOMETRY',
    recommendation: '几何已生成但结构可能不正确,需要人工或使用参考对比验证'
  };
}

Step 2.3 数据库表创建

-- 文件检测结果表(PostgreSQL)
CREATE TABLE cga_file_checks (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    session_id UUID REFERENCES cga_learning_sessions(id),
    file_id UUID REFERENCES cga_files(id),
    source_code_hash VARCHAR(64) NOT NULL,
    parse_success BOOLEAN NOT NULL DEFAULT false,
    compile_success BOOLEAN NOT NULL DEFAULT false,
    vertex_count INTEGER DEFAULT 0,
    face_count INTEGER DEFAULT 0,
    is_empty_geometry BOOLEAN NOT NULL DEFAULT true,
    error_type VARCHAR(32),  -- SYNTAX | SEMANTIC | FUNCTION_MISSING | RUNTIME | EMPTY_OUTPUT | WRONG_GEOMETRY
    error_detail TEXT,
    failed_function VARCHAR(128),  -- 自动定位到的问题函数
    trace_log JSONB,  -- 存储操作执行轨迹
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

CREATE INDEX idx_file_checks_session ON cga_file_checks(session_id);
CREATE INDEX idx_file_checks_file ON cga_file_checks(file_id);
CREATE INDEX idx_file_checks_failed_func ON cga_file_checks(failed_function) WHERE failed_function IS NOT NULL;

Step 2.4 批量检测脚本(scripts/batch-check-marketplace.mjs

#!/usr/bin/env node
import { readdirSync, readFileSync } from 'fs';
import { join } from 'path';
import { parseCGA, QajsEngine } from '../dist/index.js';
import { analyzeFailure } from '../dist/testing/failure-analyzer.js';

const dir = '/www/wwwroot/marketplace.cgajs.com/files';
const files = readdirSync(dir).filter(f => f.endsWith('.cga')).sort();
const engine = new QajsEngine();

for (const file of files) {
  const source = readFileSync(join(dir, file), 'utf8');
  const parsed = parseCGA(source);

  if (!parsed.success) {
    await saveCheckResult({
      fileName: file,
      parseSuccess: false,
      compileSuccess: false,
      errorType: 'SYNTAX',
      errorDetail: parsed.errors[0].message,
      failedFunction: null
    });
    continue;
  }

  const result = await engine.compile({
    source,
    initialShape: { geometry: { type: 'polygon', vertices: [[-5,0,-5],[5,0,-5],[5,0,5],[-5,0,5]] } }
  });

  const analysis = analyzeFailure(result, result.traceLog || []);
  await saveCheckResult({
    fileName: file,
    parseSuccess: true,
    compileSuccess: result.success,
    vertexCount: result.metadata?.vertexCount || 0,
    faceCount: result.metadata?.faceCount || 0,
    isEmptyGeometry: (result.metadata?.vertexCount || 0) === 0,
    ...analysis
  });
}

async function saveCheckResult(data) {
  // 调用 cgajs-api 写入数据库
  await fetch('http://localhost:8000/api/v1/learning/file-checks', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(data)
  });
}

Phase 3:AI 修复 Pipeline(第 4~5 周)

Step 3.1 Prompt 模板库(cgajs-api/ai/prompts/

cgajs-api/
├─ ai/
│  ├─ prompts/
│  │  ├─ __init__.py
│  │  ├─ fix_function.py      # 函数修复 Prompt
│  │  ├─ fix_syntax.py        # 语法修复 Prompt
│  │  └─ generate_test.py     # 测试生成 Prompt
│  ├─ clients.py              # LLM 客户端封装
│  ├─ pipeline.py             # AI 修复流水线 orchestrator
│  └─ git_ops.py              # 临时分支操作
└─ ...

函数修复 Prompt 模板(ai/prompts/fix_function.py

FUNCTION_FIX_PROMPT = """你是一位 CGA (CityEngine Grammar) 编译器专家,精通 TypeScript、Three.js 和计算几何。

## 任务
当前 CGA 引擎的函数 `{function_name}` 存在问题,需要你分析根因并给出修复后的 TypeScript 实现。

## 问题描述
{issue_description}

## 失败案例
【CGA 源码】
```cga
{cga_source}
```

【错误信息/现象】
{error_detail}

## 当前引擎实现
```typescript
{current_implementation}
```

## CityEngine 官方文档描述
{official_doc}

## 要求
1. 分析失败根因(用中文简述)
2. 给出修复后的完整 TypeScript 函数实现
3. 给出至少 2 个单元测试用例(输入 + 预期输出)
4. 代码必须兼容现有 AST 结构和 Shape/Scope 类型
5. 如涉及几何生成,使用 Three.js BufferGeometry API

## 输出格式(严格 JSON)
{{
  "root_cause": "中文根因分析",
  "fixed_code": "完整 TS 代码字符串",
  "test_cases": [
    {{"name": "...", "input": "...", "expected_vertices_min": 8, "expected_assertion": "..."}}
  ],
  "confidence": 0.85  // 0~1 你对修复的信心
}}
"""

Step 3.2 LLM 客户端封装(cgajs-api/ai/clients.py

import os
import httpx
from typing import Literal

Provider = Literal["kimi", "kimi_code", "qwen", "openai"]

class LLMClient:
    def __init__(self, provider: Provider = None):
        self.provider = provider or self._detect_provider()
        self.config = self._load_config()

    def _detect_provider(self) -> Provider:
        # 读取管理员配置的当前使用模型
        from app.models import Config
        cfg = Config.get_value("llm.provider")
        return cfg or "kimi"

    def _load_config(self):
        from app.models import Config
        prefix = f"llm.{self.provider}"
        return {
            "api_key": Config.get_value(f"{prefix}.api_key"),
            "base_url": Config.get_value(f"{prefix}.base_url"),
            "model": Config.get_value(f"{prefix}.model")
        }

    async def chat_completion(self, messages: list, temperature: float = 0.2, max_tokens: int = 4000) -> str:
        async with httpx.AsyncClient(timeout=120) as client:
            resp = await client.post(
                f"{self.config['base_url']}/chat/completions",
                headers={"Authorization": f"Bearer {self.config['api_key']}"},
                json={
                    "model": self.config["model"],
                    "messages": messages,
                    "temperature": temperature,
                    "max_tokens": max_tokens
                }
            )
            resp.raise_for_status()
            return resp.json()["choices"][0]["message"]["content"]

# 使用示例
client = LLMClient()
response = await client.chat_completion([
    {"role": "system", "content": "你是 CGA 编译器专家"},
    {"role": "user", "content": prompt}
])

Step 3.3 AI 修复流水线(cgajs-api/ai/pipeline.py

import json
import re
import subprocess
import tempfile
import os
from pathlib import Path
from .clients import LLMClient
from .prompts.fix_function import FUNCTION_FIX_PROMPT

class AIFixPipeline:
    def __init__(self):
        self.client = LLMClient()
        self.engine_repo = "/www/wwwroot/cgajs-engine"

    async def generate_fix(self, check_result: dict) -> dict:
        """为检测到的失败生成 AI 修复建议"""
        func = check_result["failed_function"]
        if not func:
            return {"error": "无法定位到具体函数,跳过 AI 修复"}

        # 1. 读取当前实现
        current_code = self._load_function_code(func)

        # 2. 组装 Prompt
        prompt = FUNCTION_FIX_PROMPT.format(
            function_name=func,
            issue_description=check_result.get("recommendation", ""),
            cga_source=check_result.get("cga_source", ""),
            error_detail=check_result.get("error_detail", ""),
            current_implementation=current_code,
            official_doc=self._load_official_doc(func)
        )

        # 3. 调用 LLM
        raw = await self.client.chat_completion([
            {"role": "user", "content": prompt}
        ])

        # 4. 解析 JSON
        json_match = re.search(r'\{.*\}', raw, re.DOTALL)
        if not json_match:
            return {"error": "LLM 返回格式错误,无法解析 JSON"}
        result = json.loads(json_match.group())

        # 5. 保存建议到数据库
        suggestion = await self._save_suggestion(check_result, result)
        return suggestion

    async def verify_fix(self, suggestion_id: str) -> dict:
        """在隔离环境中验证修复"""
        suggestion = await self._load_suggestion(suggestion_id)
        func = suggestion["target_function"]
        fixed_code = suggestion["generated_code"]

        # 1. 创建临时分支
        branch_name = f"ai-fix-{func}-{suggestion_id[:8]}"
        subprocess.run(["git", "checkout", "-b", branch_name], cwd=self.engine_repo, check=True)

        try:
            # 2. 应用修复代码(通过字符串替换或 patch)
            self._apply_code_patch(func, fixed_code)

            # 3. 构建引擎
            build_result = subprocess.run(
                ["npm", "run", "build"],
                cwd=self.engine_repo, capture_output=True, text=True
            )
            if build_result.returncode != 0:
                return {"passed": False, "stage": "build", "error": build_result.stderr}

            # 4. 运行函数契约测试
            test_result = subprocess.run(
                ["node", "scripts/run-function-tests.mjs"],
                cwd=self.engine_repo, capture_output=True, text=True, timeout=120000
            )
            if test_result.returncode != 0:
                return {"passed": False, "stage": "function-tests", "error": test_result.stdout}

            # 5. 运行 Marketplace 回归测试
            reg_result = subprocess.run(
                ["node", "scripts/test-marketplace.mjs"],
                cwd=self.engine_repo, capture_output=True, text=True, timeout=300000
            )

            # 6. 解析测试报告
            report = json.load(open("/tmp/cgajs-function-test-report.json"))
            return {
                "passed": report["passRate"] == "100.0%",
                "stage": "full",
                "passRate": report["passRate"],
                "failures": report["failures"]
            }
        finally:
            # 7. 无论成败,切回 main 并删除临时分支
            subprocess.run(["git", "checkout", "main"], cwd=self.engine_repo, check=True)
            subprocess.run(["git", "branch", "-D", branch_name], cwd=self.engine_repo, capture_output=True)

    def _load_function_code(self, func_name: str) -> str:
        # 根据函数名查找源码文件
        import glob
        for path in glob.glob(f"{self.engine_repo}/src/**/*.ts", recursive=True):
            with open(path) as f:
                content = f.read()
                if f"function {func_name}" in content or f"{func_name}(" in content:
                    return content
        return "// 未找到源码"

    def _apply_code_patch(self, func_name: str, new_code: str):
        # 简化实现:将新代码写入一个临时文件,实际生产环境应使用 AST 替换
        target = f"{self.engine_repo}/src/geometry/operations/auto-fix/{func_name}.ts"
        os.makedirs(os.path.dirname(target), exist_ok=True)
        with open(target, "w") as f:
            f.write(new_code)

    def _load_official_doc(self, func_name: str) -> str:
        # 从本地文档或 CityEngine 参考手册加载
        doc_path = f"/www/wwwroot/cgajs-engine/docs/cityengine-ref/{func_name}.md"
        if os.path.exists(doc_path):
            with open(doc_path) as f:
                return f.read()[:2000]
        return "暂无官方文档摘要"

    async def _save_suggestion(self, check: dict, ai_result: dict) -> dict:
        from app.models import AISuggestion
        suggestion = AISuggestion.create(
            session_id=check.get("session_id"),
            target_function=check["failed_function"],
            issue_type=check.get("error_type", "UNKNOWN"),
            description=ai_result["root_cause"],
            generated_code=ai_result["fixed_code"],
            generated_test=json.dumps(ai_result.get("test_cases", [])),
            confidence=ai_result.get("confidence", 0.5),
            status="pending"
        )
        return suggestion.to_dict()

Step 3.4 数据库表:AI 建议

CREATE TABLE ai_suggestions (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    session_id UUID REFERENCES cga_learning_sessions(id),
    target_function VARCHAR(128) NOT NULL,
    issue_type VARCHAR(32) NOT NULL,
    description TEXT NOT NULL,           -- AI 根因分析(中文)
    root_cause TEXT,                     -- 技术根因
    generated_code TEXT NOT NULL,        -- AI 生成的 TS 修复代码
    generated_test TEXT,                 -- AI 生成的测试用例(JSON)
    diff_summary TEXT,                   -- 变更摘要(用于后台列表)
    confidence DECIMAL(3,2) DEFAULT 0.5, -- AI 信心分
    status VARCHAR(16) DEFAULT 'pending' CHECK (status IN ('pending','approved','rejected','merged')),
    verified_passed BOOLEAN,             -- 隔离验证是否通过
    verified_report JSONB,               -- 验证报告详情
    reviewed_by INTEGER REFERENCES users(id),
    reviewed_at TIMESTAMP WITH TIME ZONE,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

CREATE INDEX idx_ai_suggestions_status ON ai_suggestions(status);
CREATE INDEX idx_ai_suggestions_function ON ai_suggestions(target_function);
CREATE INDEX idx_ai_suggestions_session ON ai_suggestions(session_id);

Step 3.5 后端 API 路由(FastAPI)

from fastapi import APIRouter, Depends, BackgroundTasks
from app.ai.pipeline import AIFixPipeline
from app.auth import get_current_admin_user

router = APIRouter(prefix="/api/v1/learning", tags=["AI Learning"])

@router.post("/analyze", summary="对检测失败的文件启动 AI 分析")
async def start_ai_analysis(
    check_ids: list[str],
    background_tasks: BackgroundTasks,
    admin = Depends(get_current_admin_user)
):
    pipeline = AIFixPipeline()
    for cid in check_ids:
        background_tasks.add_task(pipeline.generate_fix, {"id": cid})
    return {"message": f"已启动 {len(check_ids)} 个 AI 分析任务"}

@router.post("/suggestions/{suggestion_id}/verify", summary="验证 AI 修复建议")
async def verify_suggestion(suggestion_id: str, admin = Depends(get_current_admin_user)):
    pipeline = AIFixPipeline()
    result = await pipeline.verify_fix(suggestion_id)
    # 更新数据库
    await db.execute(
        "UPDATE ai_suggestions SET verified_passed = :p, verified_report = :r WHERE id = :id",
        {"p": result["passed"], "r": json.dumps(result), "id": suggestion_id}
    )
    return result

@router.post("/suggestions/{suggestion_id}/approve", summary="接受 AI 建议并构建 Beta")
async def approve_suggestion(suggestion_id: str, admin = Depends(get_current_admin_user)):
    # 1. 标记为 approved
    await db.execute("UPDATE ai_suggestions SET status='approved', reviewed_by=:uid, reviewed_at=NOW() WHERE id=:id",
                     {"uid": admin.id, "id": suggestion_id})
    # 2. 创建新版本记录
    version = await create_beta_release([suggestion_id])
    return {"message": "已接受,Beta 版本构建中", "version": version}

@router.post("/suggestions/{suggestion_id}/reject", summary="拒绝 AI 建议")
async def reject_suggestion(suggestion_id: str, reason: str = "", admin = Depends(get_current_admin_user)):
    await db.execute("UPDATE ai_suggestions SET status='rejected', reviewed_by=:uid, reviewed_at=NOW() WHERE id=:id",
                     {"uid": admin.id, "id": suggestion_id})
    return {"message": "已拒绝"}

@router.get("/suggestions", summary="获取 AI 建议列表")
async def list_suggestions(status: str = None, page: int = 1, page_size: int = 20):
    # 返回列表,含 diff_summary 用于快速浏览
    return await paginate(AISuggestion, status=status, page=page, page_size=page_size)

Phase 4:Beta/Production 发布流(第 6 周)

Step 4.1 自动版本管理与构建(cgajs-api/services/release_service.py

import subprocess
import semver
from pathlib import Path

ENGINE_REPO = Path("/www/wwwroot/cgajs-engine")
BETA_DEPLOY_PATH = "/www/wwwroot/beta.cgajs.com/assets"
PROD_DEPLOY_PATH = "/www/wwwroot/www.cgajs.com/assets"

class ReleaseService:
    async def create_beta_release(self, suggestion_ids: list[str]) -> str:
        """基于已接受的 AI 建议创建 Beta 版本"""
        # 1. 获取当前版本
        current = self._get_current_version()
        new_version = semver.bump_patch(current)  # 自动 patch +1

        # 2. 应用所有 approved 的 AI 建议到 main 分支
        for sid in suggestion_ids:
            patch = await self._load_suggestion_patch(sid)
            self._apply_patch(patch)

        # 3. 更新版本号和 changelog
        self._update_package_json(new_version, suggestion_ids)
        self._generate_changelog(new_version, suggestion_ids)

        # 4. Git 提交 & 打 tag
        subprocess.run(["git", "add", "."], cwd=ENGINE_REPO, check=True)
        subprocess.run(["git", "commit", "-m", f"chore(release): ai-fix v{new_version}"], cwd=ENGINE_REPO, check=True)
        subprocess.run(["git", "tag", f"v{new_version}"], cwd=ENGINE_REPO, check=True)

        # 5. 构建
        subprocess.run(["npm", "ci"], cwd=ENGINE_REPO, check=True)
        subprocess.run(["npm", "run", "build"], cwd=ENGINE_REPO, check=True)

        # 6. 运行函数测试(门禁)
        test = subprocess.run(["node", "scripts/run-function-tests.mjs"], cwd=ENGINE_REPO, capture_output=True, text=True)
        if test.returncode != 0:
            raise Exception(f"函数测试失败,阻断发布。错误: {test.stdout}")

        # 7. 部署到 beta.cgajs.com
        self._deploy_beta()

        # 8. 写入版本表
        release = await db.execute(
            """INSERT INTO engine_releases (version, changelog, status, suggestion_ids, beta_deployed_at)
               VALUES (:v, :c, 'beta', :s, NOW()) RETURNING id""",
            {"v": new_version, "c": self._load_changelog(), "s": suggestion_ids}
        )
        return new_version

    async def promote_to_production(self, release_id: str, admin_id: int) -> str:
        """将 Beta 版本晋升为生产"""
        release = await db.fetch_one("SELECT * FROM engine_releases WHERE id = :id", {"id": release_id})
        if release["status"] != "beta":
            raise HTTPException(400, "只有 beta 状态才能发布生产")

        # 1. 备份当前生产
        backup_name = f"engine-backup-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
        subprocess.run(["cp", "-r", str(PROD_DEPLOY_PATH), f"/www/backup/{backup_name}"], check=True)

        # 2. 将 beta 产物复制到生产
        subprocess.run(["cp", "-r", f"{BETA_DEPLOY_PATH}/.", str(PROD_DEPLOY_PATH)], check=True)

        # 3. 重启 cgajs-api(如有需要)
        subprocess.run(["pm2", "restart", "cgajs-api"], check=True)

        # 4. 更新状态
        await db.execute(
            "UPDATE engine_releases SET status='production', prod_deployed_at=NOW(), deployed_by=:uid WHERE id=:id",
            {"uid": admin_id, "id": release_id}
        )
        return release["version"]

    async def rollback_production(self, admin_id: int) -> str:
        """紧急回滚到上一个生产版本"""
        # 查找上一个 production 版本
        prev = await db.fetch_one(
            "SELECT * FROM engine_releases WHERE status='production' ORDER BY prod_deployed_at DESC LIMIT 1 OFFSET 1"
        )
        if not prev:
            raise HTTPException(400, "没有可回滚的历史版本")

        # 从备份恢复(简化版,实际应从 git tag checkout 构建)
        # ...
        return prev["version"]

    def _get_current_version(self) -> str:
        import json
        pkg = json.load(open(ENGINE_REPO / "package.json"))
        return pkg.get("version", "0.1.0")

    def _update_package_json(self, version: str, suggestion_ids: list):
        import json
        pkg = json.load(open(ENGINE_REPO / "package.json"))
        pkg["version"] = version
        json.dump(pkg, open(ENGINE_REPO / "package.json", "w"), indent=2)

    def _deploy_beta(self):
        import shutil
        dist = ENGINE_REPO / "dist"
        if dist.exists():
            shutil.copytree(dist, Path(BETA_DEPLOY_PATH), dirs_exist_ok=True)

Step 4.2 数据库表:引擎版本发布

CREATE TABLE engine_releases (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    version VARCHAR(16) NOT NULL UNIQUE,     -- 如 "0.1.4"
    changelog TEXT NOT NULL,
    base_commit VARCHAR(40),                 -- git commit hash
    suggestion_ids UUID[] DEFAULT '{}',      -- 包含哪些 AI 建议
    status VARCHAR(16) DEFAULT 'building' CHECK (status IN ('building','beta','production','rolled_back')),
    build_log TEXT,
    test_pass_rate DECIMAL(5,2),             -- 函数测试通过率
    regression_pass_rate DECIMAL(5,2),       -- Marketplace 回归测试通过率
    beta_deployed_at TIMESTAMP WITH TIME ZONE,
    prod_deployed_at TIMESTAMP WITH TIME ZONE,
    deployed_by INTEGER REFERENCES users(id),
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

CREATE INDEX idx_releases_status ON engine_releases(status);
CREATE INDEX idx_releases_version ON engine_releases(version);

Step 4.3 GitHub Actions CI 更新(.github/workflows/ci.yml

name: CGA.js CI/CD

on:
  push:
    branches: [main, master]
  pull_request:
    branches: [main, master]
  schedule:
    - cron: '0 2 * * *'  # 每天凌晨 2:00 自动运行

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - run: npm ci
      - run: npm run build
      - run: npm run typecheck || true

  function-tests:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - run: npm ci
      - run: npm run build
      - run: npm run test:functions   # 函数契约测试
        id: func_tests
      - if: failure()
        run: |
          echo "::error::函数测试失败,已阻止发布。"
          exit 1

  ai-review:
    needs: function-tests
    runs-on: ubuntu-latest
    if: github.event_name == 'schedule'
    steps:
      - uses: actions/checkout@v4
      - name: Run AI Analysis
        run: |
          curl -X POST "https://cgajs.com/api/v1/learning/auto-analyze" \
               -H "Authorization: Bearer ${{ secrets.ADMIN_API_KEY }}"

  deploy-beta:
    needs: [build, function-tests]
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master'
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - run: npm ci
      - run: npm run build
      - name: Deploy to Beta
        uses: appleboy/scp-action@master
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          source: "dist/"
          target: "/www/wwwroot/beta.cgajs.com/"
          strip_components: 0

  deploy-prod:
    needs: deploy-beta
    runs-on: ubuntu-latest
    if: false  # 必须由管理员手动触发,不能自动部署生产
    steps:
      - name: Manual Production Deploy
        run: echo "This job is triggered manually from admin dashboard only"

Phase 5:全量自动化闭环(第 7 周起,持续迭代)

Step 5.1 定时任务调度器(cgajs-api/tasks/scheduler.py

from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.triggers.cron import CronTrigger
from app.services.marketplace_scanner import MarketplaceScanner

scheduler = AsyncIOScheduler()

@scheduler.scheduled_job(CronTrigger(hour=2, minute=0), id='daily_learning')
async def daily_learning_job():
    """每日凌晨 2:00 执行"""
    scanner = MarketplaceScanner()
    new_files = await scanner.get_unprocessed_files()

    if len(new_files) == 0:
        print("[DailyLearning] 无新文件,跳过")
        return

    # 创建学习会话
    session = await scanner.create_session("scheduled", len(new_files))

    # 批量编译检测
    for f in new_files:
        await scanner.check_file(f, session.id)

    # 自动对失败项启动 AI 分析(背景任务)
    failed_checks = await scanner.get_failed_checks(session.id)
    if failed_checks:
        from app.ai.pipeline import AIFixPipeline
        pipeline = AIFixPipeline()
        for check in failed_checks[:10]:  # 每天最多处理 10 个,控制成本
            await pipeline.generate_fix(check)

@scheduler.scheduled_job(CronTrigger(hour='*/6'), id='accumulated_check')
async def accumulated_check_job():
    """每 6 小时检查累积文件数"""
    scanner = MarketplaceScanner()
    count = await scanner.get_unprocessed_count()
    if count >= 5:
        print(f"[AccumulatedCheck] 累积 {count} 个新文件,触发即时分析")
        await daily_learning_job()

def start_scheduler():
    scheduler.start()

Step 5.2 管理员后台页面:AI 学习监控中心(admin.html 新增)

// admin.html sidebar 新增
<button class="nav-item" onclick="showPage('aiLearning', this)">🔬 AI 学习监控</button>

// 页面结构
<div class="page" id="page-aiLearning">
  <h1>AI 学习监控中心</h1>

  <!-- 统计卡片区 -->
  <div class="stats">
    <div class="stat-card"><div class="label">今日检测文件</div><div class="value" id="al-today-files">-</div></div>
    <div class="stat-card"><div class="label">AI 建议待审核</div><div class="value" id="al-pending">-</div></div>
    <div class="stat-card"><div class="label">本月已合并修复</div><div class="value" id="al-merged">-</div></div>
    <div class="stat-card"><div class="label">Beta 待验证版本</div><div class="value" id="al-beta-pending">-</div></div>
  </div>

  <!-- 手动触发区 -->
  <div class="card">
    <h3>手动操作</h3>
    <button class="btn btn-primary" onclick="triggerLearning()">🚀 立即触发全量检测</button>
    <button class="btn btn-ghost" onclick="loadLearningSessions()">🔄 刷新数据</button>
    <span id="al-trigger-status" style="margin-left:12px;font-size:13px;color:#8b949e"></span>
  </div>

  <!-- AI 建议列表 -->
  <div class="card">
    <h3>AI 修复建议(待审核)</h3>
    <table>
      <thead>
        <tr><th>ID</th><th>目标函数</th><th>问题类型</th><th>根因摘要</th>
            <th>信心分</th><th>验证状态</th><th>操作</th></tr>
      </thead>
      <tbody id="ai-suggestion-list"><tr><td colspan="7" style="text-align:center;color:#8b949e">加载中...</td></tr></tbody>
    </table>
    <div class="pagination" id="ai-suggestion-pagination"></div>
  </div>

  <!-- 建议详情弹窗(含代码 diff) -->
  <div class="modal" id="suggestion-detail-modal">
    <div class="modal-content" style="max-width:800px">
      <div class="modal-header">
        <h3>AI 修复建议详情</h3>
        <button class="close-btn" onclick="closeModal('suggestion-detail-modal')">×</button>
      </div>
      <div id="suggestion-detail-content"></div>
      <div style="margin-top:16px;display:flex;gap:8px;justify-content:flex-end">
        <button class="btn btn-ok" onclick="approveSuggestionFromModal()">✅ 接受并构建 Beta</button>
        <button class="btn btn-warn" onclick="rejectSuggestionFromModal()">❌ 拒绝</button>
      </div>
    </div>
  </div>
</div>

Step 5.3 版本发布管理页面(admin.html 新增)

// admin.html sidebar 新增
<button class="nav-item" onclick="showPage('releases', this)">📦 版本发布</button>

// 页面结构(看板风格)
<div class="page" id="page-releases">
  <h1>引擎版本发布管理</h1>
  <div style="display:grid;grid-template-columns:repeat(3,1fr);gap:16px">
    <div class="card">
      <h3>🛠️ 开发中 (Dev)</h3>
      <div id="release-dev-list" style="font-size:13px;color:#8b949e">无</div>
    </div>
    <div class="card" style="border-color:var(--warn)">
      <h3>🧪 Beta 验证中</h3>
      <div id="release-beta-list"></div>
    </div>
    <div class="card" style="border-color:var(--ok)">
      <h3>✅ 生产环境 (Production)</h3>
      <div id="release-prod-list"></div>
    </div>
  </div>
</div>

Step 5.4 通知机制

当 AI 生成新建议或 Beta 构建完成时,通过以下方式通知管理员:

# app/services/notifier.py
import httpx

async def notify_admin(title: str, content: str):
    # 1. 后台红点(通过 WebSocket 或轮询)
    await redis.publish("admin:notifications", json.dumps({"title": title, "content": content}))

    # 2. 企业微信 Webhook(可选)
    webhook_url = Config.get_value("notify.wechat_webhook")
    if webhook_url:
        await httpx.AsyncClient().post(webhook_url, json={"msgtype": "text", "text": {"content": f"{title}\n{content}"}})

    # 3. 邮件(可选)
    smtp_config = Config.get_smtp_config()
    if smtp_config:
        send_email(to=Config.get_value("notify.admin_email"), subject=title, body=content)

Step 5.5 安装依赖清单

引擎端(cgajs-engine/package.json 新增 devDependencies):

"devDependencies": {
  // ... 已有依赖
  "@types/three": "^0.164.0",       // 已有,确保存在
  "playwright": "^1.44.0"            // D级视觉快照测试(可选)
}

API 端(cgajs-api/requirements.txt 新增):

apscheduler==3.10.4        # 定时任务
httpx==0.27.0              # 异步 HTTP(LLM 调用)
semver==3.0.2              # 语义化版本管理
pytest-asyncio==0.23.7     # 异步测试(可选)

系统级依赖:

# Ubuntu/Debian
apt-get install -y git nodejs npm python3-pip

# 确保 cgajs-engine 目录有 git 仓库(用于临时分支)
cd /www/wwwroot/cgajs-engine && git init && git add . && git commit -m "init"

# 创建备份目录
mkdir -p /www/backup/engine-releases

实施检查清单(Checklist)

Phase检查项验收标准
Phase 1契约测试用例覆盖 Top 20 函数每个函数 ≥3 个用例,JSON 格式正确
四级验证引擎可运行npm run test:functions 输出报告
CI 门禁生效关键函数失败时构建阻断
后台函数矩阵页面可用admin.html#/function-tests 可查看色块矩阵
Phase 2Trace 日志包含每个操作compile() 返回 traceLog 数组
失败函数自动定位准确对 10 个已知失败文件,定位正确率 ≥80%
cga_file_checks 表有数据每次检测写入一行记录
Phase 3Prompt 模板可渲染传入变量后生成有效 Prompt
LLM 返回可解析为 JSON含 root_cause / fixed_code / test_cases
隔离验证流程跑通从生成建议到验证报告 ≤5 分钟
Phase 4Beta 自动部署接受建议后 3 分钟内 beta.cgajs.com 生效
生产发布手动确认admin 点击发布后 www.cgajs.com 更新
回滚可用紧急回滚 ≤2 分钟恢复上一个生产版本
Phase 5定时任务稳定运行连续 7 天无异常中断
管理员可感知闭环每天上班看到昨天的 AI 建议列表,可一键处理

六、CGA 函数解析实现

基于 CGA.js 引擎源码(cgaprocessor.js)中核心函数的实现原理分析,详细解读几何生成算法的每一步。

1. 概述

本节收录了对 cgaprocessor.js 中 6 个核心函数的源码实现详解文档,涵盖屋顶生成类函数与周长分割退缩函数。每篇文档均包含:

  • 函数定位与作用说明
  • 完整源码摘录(带语法高亮)
  • 参数解析与算法步骤详解
  • 关键数学原理与几何结构示意图
  • 使用示例与同类函数对比

2. 函数文档列表

🏠 roofGable — 人字屋顶

自动选择最长边作为屋脊方向,支持 byAngle/byHeight 两种高度计算模式、悬挑与平均高度选项。

📄 查看详解 →

🏚 roofShed — 单坡屋顶

最简单的屋顶函数,直接按 Z 坐标比例抬升顶点 Y 值,只有一个坡面。

📄 查看详解 →

🔺 roofPyramid — 金字塔顶

所有坡面汇聚到顶部一点,按 footprint 最小跨度计算 apex 高度。

📄 查看详解 →

🏛 roofHip — 四坡屋顶

矩形近似算法生成四坡屋顶,近正方形时自动退化为 pyramid,包含 hip 面与 gable 面构建。

📄 查看详解 →

🏠 roofRidge — 指定屋脊屋顶

与 roofGable 算法一致,但允许通过 edgeIndex 显式指定哪条边作为屋脊方向。

📄 查看详解 →

📐 splitAndSetbackPerimeter — 周长分割与退缩

沿多边形周长进行分割并向内退缩,常用于裙房裙边、阳台、装饰带生成。

📄 查看详解 →

🎨 texture — 纹理贴图

通过文件路径设置 colormap 和 opacitymap,是 set(material.colormap, ...) 的快捷版本。

📄 查看详解 →

📐 setupProjection / projectUV — UV 投影系统

定义纹理坐标系(scope.xy / world.xz 等)并将纹理投影到几何体表面,支持 tu/tv/uwFactor 参数。

📄 查看详解 →

🔄 UV 变换操作族

scaleUV、rotateUV、translateUV、tileUV、normalizeUV、copyUV、deleteUV — 全面的 UV 坐标变换工具集。

📄 查看详解 →

🧱 PBR Material 系统

完整支持 material.colormap / normalmap / roughnessmap / metallicmap / bumpmap / emissivemap / occlusionmap 及 su/sv/rw 子属性。

📄 查看详解 →

🔍 getMaterial — 材质查询

返回当前形状所有材质属性的 CSV 格式字符串,支持材质复用和程序化材质管理。

📄 查看详解 →

3. 核心算法共性

上述 roof 类函数共享以下几何处理流程:

  1. 法线提取:取 footprint 第一个面的法线作为基准平面
  2. 2D 投影:建立局部坐标系(xaxis, yaxis),将 3D 顶点投影到 2D 平面
  3. 极角排序:按 atan2 角度围绕质心逆时针排序,获得有序多边形环
  4. 屋脊/方向计算:根据最长边或指定边确定屋顶走向
  5. 高度映射:按坡度角或指定高度计算每个顶点的屋顶高度
  6. 3D 逆变换:将 2D 局部坐标映射回 3D 世界坐标
  7. 三角网格构建:生成底部面与侧面坡面的三角面片
提示:所有文档均为独立 HTML 文件,包含完整的源码摘录、SVG 示意图与参数说明表,可单独在浏览器中打开阅读。

4. 纹理系统专题

CGA.js 纹理系统完整实现了 CityEngine 的 UV 投影与 PBR 材质管线,包含以下核心能力:

4.1 UV 投影流程

  1. setupProjection:定义 UV 坐标系(uvw 系统),支持 scope.xy/xz/yzworld.xy/xz/yz 两种坐标系
  2. projectUV:沿 w 轴将几何体顶点投影到 uv 平面,生成纹理坐标(存储在 BufferGeometry 的 uv 属性中)
  3. UV 变换:scaleUV、rotateUV、translateUV、tileUV 对已生成坐标进行二次变换

4.2 PBR 材质属性支持矩阵

属性类型说明
material.colormapstring基础颜色贴图路径
material.normalmapstring法线贴图路径
material.roughnessmapstring粗糙度贴图路径
material.metallicmapstring金属度贴图路径
material.bumpmapstring凹凸贴图路径
material.emissivemapstring自发光贴图路径
material.occlusionmapstring环境光遮蔽贴图路径
material.opacitymapstring透明度贴图路径
material.colormap.sunumber颜色贴图 U 方向缩放
material.colormap.svnumber颜色贴图 V 方向缩放
material.colormap.rwnumber颜色贴图 W 方向旋转

4.3 纹理加载策略

  • 在线纹理:支持 http:// / https:// URL,前端通过 Three.js TextureLoader 异步加载
  • 本地纹理:支持 file:// 协议路径,以及相对于 CGA 文件的相对路径(如 assets/brick.jpg
  • CLI/Node 环境:纹理路径记录到 material 元数据,GLB 导出时保留贴图引用
  • 内置资产:通过 /ESRI.lib/assets/... 引用 CityEngine 内置纹理库

4.4 纹理示例清单(Marketplace)

1 / 8

© 2026 CGA.js — CityEngine CGA Shape Grammar for the Web

文档生成于 2026-05-28 | 技术实现方案版本 v1.0

七、CGA 解析器完整审计与修复路线图

基于 CityEngine CGA 官方参考文档(195 个条目)对 cgajs-engine 进行全面审计,识别已完整实现、部分实现/Stub、以及完全缺失的功能。本页为实时追踪页,随引擎迭代更新。

1. 审计概览

72
✅ 完整实现
68
⚠️ 部分/Stub
55
❌ 完全缺失
195
总计条目
37%
完整覆盖率
72%
可运行覆盖率

2. 覆盖矩阵

2.1 Operations(形状操作)— 70 个

类别名称状态说明
几何创建envelope⚠️基础实现,缺少完整多方向参数支持
extrude完整实现
footprint完整实现
offset⚠️缺少 keepFaces 参数支持
roofGable完整实现(含文档)
roofHip完整实现(含文档)
roofPyramid完整实现(含文档)
roofRidge完整实现(含文档)
roofShed完整实现(含文档)
taper完整实现
几何细分comp⚠️f/e/v/fe/fv/g/m 已实现;h(holes) 返回空数组;selector 分支匹配不完善
scatter⚠️基础实现,gaussian 分布未完整支持
setback核心实现完整
setbackPerEdge⚠️基础签名支持
setbackToArea⚠️基础签名支持,缺少 min/max distances 参数
split⚠️x/y/z 轴与 repeat 已实现;缺少 adjustMode 参数;surfaceParameterization 未支持
splitAndSetbackPerimeter完整实现(含文档)
splitArea⚠️基础实现
几何操作convexify完整实现
mirror⚠️当前只支持 mirror(axis) 字符串;需支持 mirror(xFlip,yFlip,zFlip) 三布尔参数
mirrorScope完整实现
modify⚠️基础实现,缺少完整 selector/operator 支持
rectify完整实现
trim完整实现
标签deleteTags完整实现
setTagsFromEdgeAttrs完整实现
tag完整实现
布尔3Dunion完整实现
subtract完整实现
intersect完整实现
纹理texture完整实现
变换alignScopeToAxes完整实现
center完整实现
color完整实现
deleteHoles完整实现
i / insert⚠️stub:只生成 placeholder box,无法加载真实资产
print完整实现(operation)
r完整实现
report完整实现
reverseNormals完整实现
rotate完全缺失:rotate(mode, coordSystem, x, y, z)
rotateScope完整实现
rotateUV完整实现
s完整实现
set完整实现
setNormals完整实现
setPivot⚠️空实现:未调整 pivot 位置和方向
softenNormals完整实现
t完整实现
translate完整实现
translateUV完整实现
流控制[ push完整实现
] pop完整实现
上下文label完整实现
属性resetGeometry完整实现
resetMaterial完整实现
setMaterial完整实现
setupProjection完整实现
其他操作NIL完整实现
inline⚠️append/recompose/unify 已支持;geometryMergeStrategy 扩展不足
alignScopeToGeometry完整实现
alignScopeToGeometryBBox完整实现
cleanupGeometry⚠️缺少 components/tolerance 参数支持
copyUV完整实现
deleteUV完整实现
innerRectangle完整实现
insertAlongUV⚠️stub:placeholder 实现
normalizeUV完整实现(支持 normalizeMode: none/standard/keepAspect)
projectUV完整实现
reduceGeometry完整实现
scaleUV完整实现
shapeL完整实现
shapeU完整实现
shapeO完整实现
tileUV完整实现

2.2 Built-in Functions(内置函数)— 68 个

类别名称状态说明
数组array init []完整实现
assetsSortSize🚧stub:返回第一个元素
colon operator (:)完整实现
index operator []基础索引实现;多维索引待完善
size完整实现
sum完整实现
transpose完整实现
转换bool完整实现
boolArray完整实现
float完整实现
floatArray完整实现
sel完整实现
splitString完整实现
str完整实现
stringArray完整实现
substring完整实现
数学abs⚠️标量实现;缺少 float[] 数组重载
acos完整实现
asin完整实现
atan完整实现
atan2完整实现
ceil⚠️标量实现;缺少 float[] 数组重载
cos完整实现
exp完整实现
floor⚠️标量实现;缺少 float[] 数组重载
isinf⚠️标量实现;缺少 bool[] 数组重载
isnan⚠️标量实现;缺少 bool[] 数组重载
ln完整实现
log10完整实现
minimumDistance🚧stub:返回 0
pow完整实现
print (func)完全缺失:函数版本应返回值
rint⚠️标量实现;缺少 float[] 数组重载
sin完整实现
sqrt完整实现
tan完整实现
杂项convert🚧stub:原样返回输入
遮挡查询inside🚧stub:返回 0
overlaps🚧stub:返回 0
touches🚧stub:返回 0
其他assetInfo🚧stub:返回 0
assetMetadata🚧stub:返回空字符串
assetNamingInfo/Infos🚧stub:返回空
assetsSortRatio🚧stub:返回空
contextCompare🚧stub:返回 0
edgeAttr.getFloat🚧stub:返回 0
edgeAttr.getString🚧stub:返回空字符串
edgeAttr.getBool🚧stub:返回 0
fileExists🚧stub:返回 0
fileSearch / filesSearch🚧stub:返回空
getGeoCoord🚧stub:返回 [0,0]
getMaterial完全缺失
getTreeKey🚧stub:返回 'tree_0'
imageInfo🚧stub:返回 0
其他续imagesSortRatio🚧stub:返回空
isNull完整实现
nColumns完整实现
nRows完整实现
readMaterial完全缺失
其他续readFloatTable🚧stub:返回 []
readStringTable🚧stub:返回 []
readTextFile🚧stub:返回文件名
数组设置setElems完整实现
sortIndices / sortRowIndices完整实现
概率comp (func)完全缺失:comp 函数版本返回数组
p完整实现
rand完整实现
字符串contextCount🚧stub:返回 0
count完整实现
find完整实现
findFirst (builtin)数组版本已实现
len完整实现

2.3 Shape Attributes(形状属性)— 8 组

属性组具体属性状态说明
Materialmaterial.color / colormap / opacitymap / ...通过 set() 和 MemberExpr 支持
compcomp.sel未实现
comp.index未实现
comp.total未实现
initialShapeinitialShape.name未实现
initialShape.startRule未实现
initialShape.origin.p{x|y|z}未实现
initialShape.origin.o{x|y|z}未实现
scope 属性⚠️scope 对象整体可访问;tx/ty/tz/rx/ry/rz/sx/sy/sz/elevation 需单独支持
pivotpivot.p{x|y|z}未实现
pivot.o{x|y|z}未实现
seedianseedian未实现
splitsplit.index未实现
split.total未实现
trimtrim.horizontal未实现
trim.vertical未实现

2.4 Utility Functions(工具函数库)— 49 个

名称状态说明
assetApproxRatio / assetApproxSize / assetBestRatio / assetBestSize / assetFitSize🚧stub:返回第一个元素
clamp完整实现
colorHSVToHex / colorHSVOToHex / colorRGBToHex / colorRGBOToHex完整实现
colorHexToB / colorHexToG / colorHexToH / colorHexToR / colorHexToS / colorHexToV完整实现
colorHexToO完全缺失
colorRamp⚠️简化实现:只返回两种颜色
fileBasename / fileDirectory / fileExtension / fileName / fileRandom完整实现
findFirst (utility)字符串版本完全缺失(与 builtin array 版本冲突)
findLast完整实现
getPrefix / getRange / getSuffix完整实现
imageApproxRatio / imageBestRatio🚧stub:返回第一个元素
listAdd / listClean / listCount / listFirst / listLast / listIndex / listItem / listFromArray / listToArray / listRandom / listRange / listRemove / listRemoveAll / listRetainAll / listSize / listTerminate完整实现
max / min完整实现(支持多参数)
replace / strreplace完整实现
sign / frac / trunc完整实现(作为 MATH_FUNCTIONS 扩展)

3. 修复优先级清单

修复原则:P0 = 阻塞编译 / 运行时崩溃;P1 = 常用功能缺失;P2 = 高级功能 / 边界情况;P3 = 文档与测试完善。

P0 — 紧急修复(运行时错误 / 语法不兼容)

#问题影响修复方案
1rotate(mode, coordSystem, ...) 操作完全缺失任何使用 rotate() 的 CGA 都会解析失败或静默忽略在 evaluator.ts 的 evalSimpleOperation 中新增 rotate 分支,支持绝对/相对模式和 scope/world 坐标系
2mirror(xFlip, yFlip, zFlip) 签名不匹配CityEngine 标准写法 mirror(1,0,0) 会导致错误行为向后兼容:检测参数数量和类型,字符串参数走旧逻辑,三个数字参数走新逻辑
3setPivot(axisMap, cornerIndex) 空实现调用后 pivot 未变化,后续 transform 结果错误根据 axisMap 选择坐标轴,cornerIndex 选择 scope 角点,更新 shape.pivot
4split(axis, adjustMode) 不支持 adjustMode某些 CGA 文件解析错误或行为不一致扩展 parser AST 支持 adjustMode;evaluator 中根据 adjustMode 调整浮点分支计算
5comp 函数版本完全缺失任何在表达式中使用 comp() 的 CGA 编译失败在 builtins.ts 中新增 comp 函数实现,返回组件数组
6Shape 属性(comp.index/total, split.index/total, initialShape.* 等)未解析表达式中使用这些属性时返回 undefined 或错误值在 expression-evaluator.ts 的 IdentifierExpr 分支中增加对这些特殊标识符的处理

P1 — 高优先级(常用功能缺失 / Stub 过多)

#问题影响修复方案
7数组重载数学函数缺失(abs/ceil/floor/rint/isinf/isnan)向量化计算无法使用在 builtins.ts 中检测参数类型,数组参数返回映射后的新数组
8print(value) 函数版本缺失调试时无法链式调用 print新增 print 函数:打印参数并返回值
9colorHexToO 缺失处理带透明度的十六进制颜色时出错实现:提取 hex 字符串的最后两位并转换为 0-1 范围
10getMaterial ✅ / readMaterial 缺失getMaterial 已实现,返回 CSV 格式材质属性readMaterial 解析 .cgamat 材质文件格式(待实现)
11cleanupGeometry(components, tolerance) 参数缺失精细控制几何清理不可行扩展参数支持,根据 components 选择清理目标
12normalizeUV(uvSet, mode, type) ✅已实现 normalizeMode 支持
13insert / insertAlongUV 为 placeholder无法加载真实资产,模型细节缺失建立资产预加载映射表;无法加载时生成更合理的 placeholder(如根据文件名推断类型)
14offset(distance, keepFaces) 缺少 keepFacesoffset 后拓扑控制不足扩展参数,根据 keepFaces 决定是否保留原始面

P2 — 中优先级(上下文 / 资产 / 文件 I/O)

#问题影响修复方案
15Context 查询函数全 stub(inside/overlaps/touches/minimumDistance/contextCompare/contextCount)无法做空间上下文判断(如建筑物间距、遮挡关系)实现 AABB-based 上下文查询引擎;在 EvalContext 中维护 shape 列表用于查询
16edgeAttr.getFloat/String/Bool 全 stub无法读取边属性(如街道宽度、建筑退界)在 shape 上增加 edgeAttributes Map;实现读取逻辑
17Asset 函数全 stub(assetInfo/assetMetadata/imageInfo/assetsSortRatio/imagesSortRatio 等)无法查询资产元数据建立资产元数据缓存;实现基础查询
18File I/O 函数全 stub(fileExists/fileSearch/readTextFile/readTable 等)无法读取外部数据(CSV、文本)实现基于 fetch 的文件读取;在浏览器环境中使用预加载文件映射
19getGeoCoord / convert 为 stub地理坐标转换不可用getGeoCoord 实现简单的局部坐标到经纬度映射;convert 实现 scope/world 旋转平移转换
20comp(h) holes 返回空数组无法处理带孔洞的多边形改进 face 检测算法,识别孔洞边界并返回 hole shapes

P3 — 低优先级(增强与完善)

#问题影响修复方案
21inline geometryMergeStrategy 扩展不足复杂合并策略不可用支持更多合并策略参数
22setbackToArea 缺少 min/max distances面积退缩的边界控制不足扩展参数解析和计算逻辑
23scatter gaussian 分布未完整支持高斯散布模式不可用实现 gaussian 随机分布
24findFirst utility string 版本与 builtin array 版本冲突同名函数不同签名调用歧义运行时根据第一个参数类型分发(string→utility, array→builtin)
25数组索引多维支持 [row, col]二维数组访问写法不支持扩展 IndexExpr 支持多个索引参数
26scope.elevation 未支持高程属性不可访问在 scope 中增加 elevation 字段或在 expression-evaluator 中计算返回

4. 技术债务与风险

⚠️ 已知限制:
  • Context 查询:Web 环境缺少完整 BVH/Octree,AABB 近似在复杂场景下可能不准确。建议后续引入 wasm 加速的空间索引库。
  • 资产加载:浏览器无法直接访问文件系统,insert() 操作依赖预加载资产映射。建议通过 Service Worker 或 IndexedDB 缓存资产。
  • 复杂几何算法:offset keepFaces、comp holes、cleanupGeometry 的完整实现涉及计算几何库(如 CGAL),Web 端可用 earcut + 简化算法替代。
  • 性能:大规模场景(>10k shapes)下,JS 单线程可能成为瓶颈。建议后续引入 Worker 池或 WebAssembly。

🚨 问题自检与修复

每次全面项目检查后,问题清单会在此归档,并注明检查日期与修复状态。


🚨 问题自检 — 2025-05-29

以下问题由 2025-05-29 全面项目检查产出,已全部修复

🔴 P0 — 严重(影响核心功能或安全)

# 问题 影响 位置
1 src/api/engine.ts 存在 2 个 TypeScript 类型错误(shapeTree null vs undefined;tags.find 推断为 unknown[]) 构建时类型检查失败,可能隐藏其他类型问题 cgajs-engine
2 ESLint 损坏(node_modules/.bin/eslint 无法找到 ../package.json 无法运行代码规范检查,代码质量不可控 cgajs-engine
3 bundled JS (assets/index-DyQ4rmA0.js) 与后端 engine 版本不一致 用户关闭 API 模式后会使用旧版引擎,结果与后端不一致 www.cgajs.com
4 window.lastResult 竞态条件(enhance.js 轮询与主应用同步冲突) HUD 数据可能闪烁或不准确 www.cgajs.com
5 learning_routes.py _classify_error(None) 崩溃 Scheduler Auto-scan 任务在编译成功时崩溃,日志中出现 Auto-scan error cgajs-api
6 marketplace_routes.py download_cga_file 未处理文件丢失异常 本地文件被删除且 OSS 失败时抛出 FileNotFoundError,HTTP 500 cgajs-api
7 /api/v1/parse/api/v1/validate 无认证/配额保护 任何人可无限制调用,存在 DoS / 资源耗尽风险 cgajs-api
8 resolve_workspace 存在路径遍历漏洞 CGA 中 import "../../../etc/passwd" 可能读取 WORKSPACE_ROOT 外敏感文件 cgajs-api

🟡 P1 — 中优先级(性能、体验、安全)

# 问题 影响 位置
9 14 个依赖安全漏洞(4 High + 10 Moderate) lodashminimatchajvesbuild 等存在已知 CVE(主要影响 devDeps) cgajs-engine
10 document.execCommand 已废弃 现代浏览器输出 Deprecation Warning,Firefox 某些模式可能失效 www.cgajs.com
11 fetch 调用无超时控制 后端挂起时请求永久 pending,UI 无反馈 www.cgajs.com
12 Marketplace 列表接口 N+1 查询 每条记录触发一次额外 SQL,20 条记录 = 20 次额外查询 cgajs-api
13 feed 搜索大小写敏感不一致 list_cga_filesilikefeed_cga_files.contains(),同一关键词返回不同结果 cgajs-api
14 item.author 可能为 None 导致 AttributeError 作者用户被删除后,列表/详情/feed 等多处访问 item.author.username 崩溃 cgajs-api
15 文件上传无大小限制 超大 CGA 上传直接读入内存,可能导致 OOM cgajs-api
16 Cookie 安全设置不当(httponly=False, samesite="none" Token 可被客户端 JS 读取,存在 XSS 风险 cgajs-api
17 assets 目录残留旧版本备份(~2.3MB) index-DyQ4rmA0.js.bak.bak.winding 可能被错误引用 www.cgajs.com
18 dist/ 残留 6 个旧 engine chunks(~7.4MB) 历史构建产物堆积,浪费磁盘空间 cgajs-engine
19 cgajs-engine/ 根目录 45+ debug 脚本和备份文件残留 debug-parse*.cjs、tokenize*.cjs、package.json.bak.* 等污染项目根目录 cgajs-engine

🟢 P2 — 低优先级(优化与清理)

# 问题 影响 位置
20 项目未使用 Git 版本控制 无变更追踪,协作和回滚困难 cgajs-engine
21 schemas.pyCompileRequestmain.py 重复定义 存在歧义,但不影响运行 cgajs-api
22 .venv 缺少 pip 命令 后续通过 pip 安装依赖会失败 cgajs-api

🚨 问题自检 — 2026-05-30

以下问题由 2026-05-30 全面项目检查产出,已全部修复

🔴 P0 — 严重(安全与核心功能)

# 问题 影响 位置
1 CORS 配置 allow_origins=["*"] + allow_credentials=True 已修复 已收紧为白名单(cgajs.com 子域 + localhost),evil.com 等外部 Origin 不再获得 Access-Control-Allow-Origin cgajs-api/main.py
2 nginx 完全缺失安全响应头部 已修复 已添加 X-Frame-Options(SAMEORIGIN/DENY)、X-Content-Type-Options(nosniff)、X-XSS-Protection、Referrer-Policy、Permissions-Policy、HSTS(max-age=63072000) nginx
3 SSL 配置支持 TLSv1 / TLSv1.1(已废弃协议) 已修复 nginx.conf 中已移除 TLSv1/TLSv1.1,仅保留 TLSv1.2/TLSv1.3(Certbot options-ssl-nginx.conf 原本已正确配置) nginx
4 API 无实际速率限制中间件(仅有响应头) 已修复 已添加内存级速率限制中间件(60 RPM / IP),响应头包含 X-RateLimit-Limit / X-RateLimit-Remaining cgajs-api

🟡 P1 — 中优先级(代码质量、运维、依赖)

# 问题 影响 位置
5 ESLint 30+ 处 any 类型警告 已修复 已修复 4 个 ESLint 错误(prefer-const, ban-types, no-inner-declarations);将 engine.ts / builtins.ts / cli.ts 中的 any 替换为具体类型;为 geometry / runtime / parser / renderer / testing 目录添加合理的 ESLint override 配置;最终 204 个警告降至 0 cgajs-engine
6 npm audit 4 个 moderate severity 漏洞 已修复 升级 Node.js 至 v20.20.2,重新安装依赖,使用 npm overrides 强制 vitest 嵌套依赖使用安全版本(vite 6.4.2 / esbuild 0.25.12),npm audit 现为 0 vulnerabilities cgajs-engine
7 缺少测试覆盖率工具 已修复 Node.js 升级至 v20.20.2 后,安装 @vitest/coverage-v8@2.1.9,覆盖率报告正常工作:Statements 45.21% / Branch 62.18% / Functions 34.23% cgajs-engine
8 核心依赖严重过时 已修复 vitest 1.6.1 → 2.1.9,vite 5.4.21 → 6.4.2,esbuild 0.21.5 → 0.25.12。three.js 0.164.1 保持不变(升级需单独验证 WebGL 渲染兼容性) cgajs-engine
9 Marketplace 磁盘存在 132 个孤儿文件(不在数据库中) 已修复 已清理 132 个孤儿文件,磁盘文件数从 320 降至 188,与 DB 记录一致 marketplace.cgajs.com
10 VIP 子站 index.html 与主站内容不一致 已修复 已同步 VIP 站点的 index.html、CSS、JS 与主站完全一致 vip.cgajs.com
11 磁盘使用率 75%(57G/79G) 已缓解 已清理 journal 日志、旧 btmp/auth 日志、旧 npm 日志、过期备份,使用率降至 70%(52G/79G) 服务器
12 cgajs-engine dist/ 残留旧构建产物(git status 显示 deleted) 已修复 已清理 4 个旧 engine chunk 文件(~2.5MB),git 工作区恢复 clean cgajs-engine

🟢 P2 — 低优先级(优化与补充)

# 问题 影响 位置
13 测试仅 19 个,覆盖率严重不足 已修复 新增 8 个测试文件、76 个测试用例,覆盖几何创建、几何操作、变换、纹理材质、内置函数、流程控制、高级 split、高级 parser。总计 95 个测试,覆盖率从 33% 提升至 45% cgajs-engine
14 Parser 中 TODO 未处理:typed parameters in CI CI 测试参数化策略待确定,长期技术债 cgajs-engine
15 FastAPI openapi_url="/openapi.json" 配置存在 已修复 已在 FastAPI 构造函数中设置 openapi_url=None/openapi.json 现返回 404 cgajs-api

🤖 九、CGA 自动检测与 AI 修复系统

本页介绍 cgajs-engine 的自动化质量保障流水线:定时扫描 Marketplace 文件 → 编译验证 → 几何检测 → AI 自动修复 → 人工审核发布。


📊 系统运行状态

188
公开文件
135
已处理
39
待处理
33
失败案例
103
成功案例

数据来源:学习队列 learning_queue,统计时间 2026-05-30。


🏗️ 流水线架构

整个系统由 6 个 APScheduler 定时任务驱动,形成"扫描 → 测试 → 修复 → 分析 → 清理"的闭环。

阶段一:自动扫描(auto_scan)

阶段二:自动测试(auto_test)

阶段三:自动修复(auto_repair)

阶段四:深度分析(auto_analyze)

阶段五:累积触发(accumulated_trigger)

阶段六:定时清理(cleanup)


🔍 几何验证规则

编译通过后,geometry_validator.py 会对输出的 sceneJson 进行以下检查:

检测项 严重程度 说明
EMPTY_SCENE error 场景 children 为空数组,无任何节点
NO_MESHES error 场景中不存在 type="Mesh" 的节点
ZERO_VERTICES error Mesh 顶点数为 0,无法渲染
ZERO_FACES warning Mesh 面片数为 0,可能是点云或线框
ZERO_SCALE warning 某轴缩放为 0,导致维度坍塌
DEGENERATE_GEOMETRY error 顶点数 < 3 但面片数 > 0,几何退化
DEEP_NESTING warning 场景树嵌套层级超过阈值,可能影响渲染性能

🔌 API 端点

以下端点由 learning_routes.py 提供,可用于手动触发或集成外部系统:

方法 端点 说明
POST /api/learning/queue/scan 手动触发扫描 Marketplace 到学习队列
GET /api/learning/failures 列出所有失败案例(支持按 error_type 过滤)
POST /api/learning/failures/{id}/analyze 对指定失败案例调用 LLM 深度分析
POST /api/learning/validate-geometry 提交 CGA 源码,返回编译结果 + 几何验证问题列表
POST /api/learning/auto-repair/{cga_file_id} 对指定 Marketplace 文件手动触发 AI 修复
POST /api/learning/batch-auto-repair 批量后台修复所有 OPEN 状态的失败案例
GET /api/learning/geometry-issues 查看所有几何错误案例的统计信息

🛠️ 错误分类与修复策略

_classify_error() 将编译/运行错误归类,为后续 AI 修复提供上下文:

分类 典型原因 修复策略
SYNTAX_ERROR CGA 语法不符合 ANTLR4 文法(如缺少分号、括号不匹配) LLM 重新格式化代码,补全缺失符号
SEMANTIC_ERROR 变量未定义、函数参数类型不匹配 注入标准库定义,修正参数类型
GEOMETRY_ERROR 几何操作产生空/退化结果(如 split 比例和 ≠ 1) 重写几何操作,添加边界保护(如 clamp 比例值)
RUNTIME_ERROR JS 运行时异常(如除以零、数组越界) 添加防御性代码,处理边界条件
UNKNOWN 无法自动归类的错误 人工介入,补充分类规则

📈 未来优化方向