📋 CGA.js 知识库总览
本文档包含 CGA.js 引擎的技术架构、项目统计、测试机制、AI 学习闭环以及详细的技术实现方案。点击左侧目录或下方翻页按钮切换章节。
📑 章节目录
一、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_ERROR | ANTLR4 解析失败,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 > 5且status = 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_cases和cga_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 / APScheduler | Python 异步任务队列 |
| 数据存储 | PostgreSQL + 本地文件 | 复用现有数据库 |
| CI/CD | GitHub 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 + Vite | CGA 语法解析、几何生成、GLB 导出 |
| 主站前端 | www.cgajs.com/ | 纯 HTML/CSS/JS | IDE、文档、用户系统、管理后台 |
| 资源市场 | marketplace.cgajs.com/ | 纯 HTML/CSS/JS | CGA 文件上传、销售、下载 |
| 后端 API | cgajs-api | Python + FastAPI + SQLAlchemy + PostgreSQL | 认证、支付、编译 API、数据管理 |
| 插件生态 | plugins/ | Ruby (SketchUp) | SketchUp 插件 (.rbz) |
2.2 子站点统计
目前共 4 个子站点/域名:
| # | 子站点 | 说明 |
|---|---|---|
| 1 | www.cgajs.com | 主站(IDE + 文档 + 用户系统) |
| 2 | marketplace.cgajs.com | CGA 资源市场(文件交易) |
| 3 | beta.cgajs.com | 引擎 Beta 测试环境(CI/CD 部署目标) |
| 4 | vip.cgajs.com | VIP 会员站(已上线) |
2.3 页面统计
主站 www.cgajs.com(8 个独立页面)
| # | 页面文件 | 功能说明 | 代码行数 |
|---|---|---|---|
| 1 | index.html | IDE 主页:CGA 代码编辑器 + Three.js 3D 实时预览 + Inspector/AST/Diagnostics 面板 | 59 |
| 2 | study.html | 知识库/文档中心:运营文档 + 完整技术架构与开发完成度报告 | 1,576 |
| 3 | roadmap.html | 开发路线图:可交互的功能进度看板,追踪 307 项功能状态 | 831 |
| 4 | plugin.html | 插件中心:SketchUp 插件下载(免费版/Pro版)、安装教程 | 381 |
| 5 | admin.html | 管理后台:仪表盘、系统配置、用户/订单/大模型/自学习/版本管理 | 1,170 |
| 6 | auth.html | 登录/注册:邮箱+密码认证,JWT Token 机制 | 126 |
| 7 | billing.html | 充值与购买:微信支付扫码、VIP 订阅(199元/年)、积分充值 | 365 |
| 8 | profile.html | 用户中心:个人资料、API Key 管理、套餐状态、余额、调用统计 | 213 |
市场站 marketplace.cgajs.com(1 个页面)
| # | 页面文件 | 功能说明 | 代码行数 |
|---|---|---|---|
| 9 | index.html | 市场主页:CGA 文件列表、搜索筛选、文件上传、预览弹窗、分页 | 459 |
辅助 JS 文件(5 个)
| 文件 | 功能 | 行数 |
|---|---|---|
enhance.js | IDE 核心增强(编辑器、3D 预览、编译交互) | 1,115 |
cga-autocomplete.js | CGA 代码自动补全 | 178 |
inspector-enhance.js | Inspector 面板增强 | 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 总结
三、函数深度测试与验证机制
当前引擎存在大量"伪实现"函数:语法解析通过、执行不报错,但无任何几何效果(如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 步)
问题定位(自动)
分析失败日志,定位到具体函数和源码位置。例如:roofGable 在 src/geometry/operations/create/roof.ts:88 返回空数组。
上下文收集(自动)
收集:失败 CGA 代码片段 + 当前引擎该函数的实现代码 + CityEngine 官方文档描述 + 参考 GLB 输出(如有)。
LLM 诊断与修复(AI)
调用大模型生成修复方案。Prompt 必须包含:失败现象、根因分析、修复后的 TypeScript 代码、单元测试用例。
隔离验证(自动)
在临时分支应用修复,运行:a) 原失败用例 b) 函数契约测试 c) 全量回归测试(Marketplace 147 个文件)。通过率 > 95% 才允许进入下一步。
生成 Beta 版本(自动)
自动递增 patch 版本号(如 0.1.3 → 0.1.4),构建产物,部署到 beta.cgajs.com,并在 admin 后台创建 "待审核发布" 记录。
管理员审核与发布(人工)
管理员在后台查看:修改了哪些函数、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.ts 的 compile() 方法返回 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 2 | Trace 日志包含每个操作 | compile() 返回 traceLog 数组 |
| 失败函数自动定位准确 | 对 10 个已知失败文件,定位正确率 ≥80% | |
| cga_file_checks 表有数据 | 每次检测写入一行记录 | |
| Phase 3 | Prompt 模板可渲染 | 传入变量后生成有效 Prompt |
| LLM 返回可解析为 JSON | 含 root_cause / fixed_code / test_cases | |
| 隔离验证流程跑通 | 从生成建议到验证报告 ≤5 分钟 | |
| Phase 4 | Beta 自动部署 | 接受建议后 3 分钟内 beta.cgajs.com 生效 |
| 生产发布手动确认 | admin 点击发布后 www.cgajs.com 更新 | |
| 回滚可用 | 紧急回滚 ≤2 分钟恢复上一个生产版本 | |
| Phase 5 | 定时任务稳定运行 | 连续 7 天无异常中断 |
| 管理员可感知闭环 | 每天上班看到昨天的 AI 建议列表,可一键处理 |
六、CGA 函数解析实现
基于 CGA.js 引擎源码(cgaprocessor.js)中核心函数的实现原理分析,详细解读几何生成算法的每一步。
1. 概述
本节收录了对 cgaprocessor.js 中 6 个核心函数的源码实现详解文档,涵盖屋顶生成类函数与周长分割退缩函数。每篇文档均包含:
- 函数定位与作用说明
- 完整源码摘录(带语法高亮)
- 参数解析与算法步骤详解
- 关键数学原理与几何结构示意图
- 使用示例与同类函数对比
2. 函数文档列表
📐 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 子属性。
3. 核心算法共性
上述 roof 类函数共享以下几何处理流程:
- 法线提取:取 footprint 第一个面的法线作为基准平面
- 2D 投影:建立局部坐标系(xaxis, yaxis),将 3D 顶点投影到 2D 平面
- 极角排序:按 atan2 角度围绕质心逆时针排序,获得有序多边形环
- 屋脊/方向计算:根据最长边或指定边确定屋顶走向
- 高度映射:按坡度角或指定高度计算每个顶点的屋顶高度
- 3D 逆变换:将 2D 局部坐标映射回 3D 世界坐标
- 三角网格构建:生成底部面与侧面坡面的三角面片
4. 纹理系统专题
CGA.js 纹理系统完整实现了 CityEngine 的 UV 投影与 PBR 材质管线,包含以下核心能力:
4.1 UV 投影流程
- setupProjection:定义 UV 坐标系(uvw 系统),支持
scope.xy/xz/yz和world.xy/xz/yz两种坐标系 - projectUV:沿 w 轴将几何体顶点投影到 uv 平面,生成纹理坐标(存储在 BufferGeometry 的 uv 属性中)
- UV 变换:scaleUV、rotateUV、translateUV、tileUV 对已生成坐标进行二次变换
4.2 PBR 材质属性支持矩阵
| 属性 | 类型 | 说明 |
|---|---|---|
material.colormap | string | 基础颜色贴图路径 |
material.normalmap | string | 法线贴图路径 |
material.roughnessmap | string | 粗糙度贴图路径 |
material.metallicmap | string | 金属度贴图路径 |
material.bumpmap | string | 凹凸贴图路径 |
material.emissivemap | string | 自发光贴图路径 |
material.occlusionmap | string | 环境光遮蔽贴图路径 |
material.opacitymap | string | 透明度贴图路径 |
material.colormap.su | number | 颜色贴图 U 方向缩放 |
material.colormap.sv | number | 颜色贴图 V 方向缩放 |
material.colormap.rw | number | 颜色贴图 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)
- Basic Texture Mapping — Brick Facade:基础纹理 + setupProjection + projectUV
- PBR Material — Wood and Metal:colormap / normalmap / roughnessmap / metallicmap 综合应用
- UV Transformations:scaleUV / rotateUV / translateUV / tileUV 对比
- Global Texture Projection:world.xz 坐标系全局投影
- Material Query — getMaterial:材质查询与复用
- Multi-UV Set Projection:多 UV 通道(基础纹理 + 脏迹贴图)
七、CGA 解析器完整审计与修复路线图
基于 CityEngine CGA 官方参考文档(195 个条目)对 cgajs-engine 进行全面审计,识别已完整实现、部分实现/Stub、以及完全缺失的功能。本页为实时追踪页,随引擎迭代更新。
1. 审计概览
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 | ✅ | 完整实现 | |
| 布尔3D | union | ✅ | 完整实现 |
| subtract | ✅ | 完整实现 | |
| intersect | ✅ | 完整实现 | |
| 纹理 | texture | ✅ | 完整实现 |
| 变换 | alignScopeToAxes | ✅ | 完整实现 |
| center | ✅ | 完整实现 | |
| color | ✅ | 完整实现 | |
| deleteHoles | ✅ | 完整实现 | |
| i / insert | ⚠️ | stub:只生成 placeholder box,无法加载真实资产 | |
| ✅ | 完整实现(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 组
| 属性组 | 具体属性 | 状态 | 说明 |
|---|---|---|---|
| Material | material.color / colormap / opacitymap / ... | ✅ | 通过 set() 和 MemberExpr 支持 |
| comp | comp.sel | ❌ | 未实现 |
| comp.index | ❌ | 未实现 | |
| comp.total | ❌ | 未实现 | |
| initialShape | initialShape.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 需单独支持 | |
| pivot | pivot.p{x|y|z} | ❌ | 未实现 |
| pivot.o{x|y|z} | ❌ | 未实现 | |
| seedian | seedian | ❌ | 未实现 |
| split | split.index | ❌ | 未实现 |
| split.total | ❌ | 未实现 | |
| trim | trim.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 — 紧急修复(运行时错误 / 语法不兼容)
| # | 问题 | 影响 | 修复方案 |
|---|---|---|---|
| 1 | rotate(mode, coordSystem, ...) 操作完全缺失 | 任何使用 rotate() 的 CGA 都会解析失败或静默忽略 | 在 evaluator.ts 的 evalSimpleOperation 中新增 rotate 分支,支持绝对/相对模式和 scope/world 坐标系 |
| 2 | mirror(xFlip, yFlip, zFlip) 签名不匹配 | CityEngine 标准写法 mirror(1,0,0) 会导致错误行为 | 向后兼容:检测参数数量和类型,字符串参数走旧逻辑,三个数字参数走新逻辑 |
| 3 | setPivot(axisMap, cornerIndex) 空实现 | 调用后 pivot 未变化,后续 transform 结果错误 | 根据 axisMap 选择坐标轴,cornerIndex 选择 scope 角点,更新 shape.pivot |
| 4 | split(axis, adjustMode) 不支持 adjustMode | 某些 CGA 文件解析错误或行为不一致 | 扩展 parser AST 支持 adjustMode;evaluator 中根据 adjustMode 调整浮点分支计算 |
| 5 | comp 函数版本完全缺失 | 任何在表达式中使用 comp() 的 CGA 编译失败 | 在 builtins.ts 中新增 comp 函数实现,返回组件数组 |
| 6 | Shape 属性(comp.index/total, split.index/total, initialShape.* 等)未解析 | 表达式中使用这些属性时返回 undefined 或错误值 | 在 expression-evaluator.ts 的 IdentifierExpr 分支中增加对这些特殊标识符的处理 |
P1 — 高优先级(常用功能缺失 / Stub 过多)
| # | 问题 | 影响 | 修复方案 |
|---|---|---|---|
| 7 | 数组重载数学函数缺失(abs/ceil/floor/rint/isinf/isnan) | 向量化计算无法使用 | 在 builtins.ts 中检测参数类型,数组参数返回映射后的新数组 |
| 8 | print(value) 函数版本缺失 | 调试时无法链式调用 print | 新增 print 函数:打印参数并返回值 |
| 9 | colorHexToO 缺失 | 处理带透明度的十六进制颜色时出错 | 实现:提取 hex 字符串的最后两位并转换为 0-1 范围 |
| 10 | getMaterial ✅ / readMaterial 缺失 | getMaterial 已实现,返回 CSV 格式材质属性 | readMaterial 解析 .cgamat 材质文件格式(待实现) |
| 11 | cleanupGeometry(components, tolerance) 参数缺失 | 精细控制几何清理不可行 | 扩展参数支持,根据 components 选择清理目标 |
| 12 | normalizeUV(uvSet, mode, type) ✅ | 已实现 normalizeMode 支持 | — |
| 13 | insert / insertAlongUV 为 placeholder | 无法加载真实资产,模型细节缺失 | 建立资产预加载映射表;无法加载时生成更合理的 placeholder(如根据文件名推断类型) |
| 14 | offset(distance, keepFaces) 缺少 keepFaces | offset 后拓扑控制不足 | 扩展参数,根据 keepFaces 决定是否保留原始面 |
P2 — 中优先级(上下文 / 资产 / 文件 I/O)
| # | 问题 | 影响 | 修复方案 |
|---|---|---|---|
| 15 | Context 查询函数全 stub(inside/overlaps/touches/minimumDistance/contextCompare/contextCount) | 无法做空间上下文判断(如建筑物间距、遮挡关系) | 实现 AABB-based 上下文查询引擎;在 EvalContext 中维护 shape 列表用于查询 |
| 16 | edgeAttr.getFloat/String/Bool 全 stub | 无法读取边属性(如街道宽度、建筑退界) | 在 shape 上增加 edgeAttributes Map;实现读取逻辑 |
| 17 | Asset 函数全 stub(assetInfo/assetMetadata/imageInfo/assetsSortRatio/imagesSortRatio 等) | 无法查询资产元数据 | 建立资产元数据缓存;实现基础查询 |
| 18 | File I/O 函数全 stub(fileExists/fileSearch/readTextFile/readTable 等) | 无法读取外部数据(CSV、文本) | 实现基于 fetch 的文件读取;在浏览器环境中使用预加载文件映射 |
| 19 | getGeoCoord / convert 为 stub | 地理坐标转换不可用 | getGeoCoord 实现简单的局部坐标到经纬度映射;convert 实现 scope/world 旋转平移转换 |
| 20 | comp(h) holes 返回空数组 | 无法处理带孔洞的多边形 | 改进 face 检测算法,识别孔洞边界并返回 hole shapes |
P3 — 低优先级(增强与完善)
| # | 问题 | 影响 | 修复方案 |
|---|---|---|---|
| 21 | inline geometryMergeStrategy 扩展不足 | 复杂合并策略不可用 | 支持更多合并策略参数 |
| 22 | setbackToArea 缺少 min/max distances | 面积退缩的边界控制不足 | 扩展参数解析和计算逻辑 |
| 23 | scatter gaussian 分布未完整支持 | 高斯散布模式不可用 | 实现 gaussian 随机分布 |
| 24 | findFirst utility string 版本与 builtin array 版本冲突 | 同名函数不同签名调用歧义 | 运行时根据第一个参数类型分发(string→utility, array→builtin) |
| 25 | 数组索引多维支持 [row, col] | 二维数组访问写法不支持 | 扩展 IndexExpr 支持多个索引参数 |
| 26 | scope.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) | lodash、minimatch、ajv、esbuild 等存在已知 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_files 用 ilike,feed_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.py 中 CompileRequest 与 main.py 重复定义 |
存在歧义,但不影响运行 | cgajs-api |
| 22 | .venv 缺少 pip 命令 |
后续通过 pip 安装依赖会失败 | cgajs-api |
🚨 问题自检 — 2026-05-30
以下问题由 2026-05-30 全面项目检查产出,已全部修复。
🔴 P0 — 严重(安全与核心功能)
| # | 问题 | 影响 | 位置 |
|---|---|---|---|
| 1 | allow_origins=["*"] + allow_credentials=True |
已收紧为白名单(cgajs.com 子域 + localhost),evil.com 等外部 Origin 不再获得 Access-Control-Allow-Origin | cgajs-api/main.py |
| 2 | 已添加 X-Frame-Options(SAMEORIGIN/DENY)、X-Content-Type-Options(nosniff)、X-XSS-Protection、Referrer-Policy、Permissions-Policy、HSTS(max-age=63072000) | nginx | |
| 3 | nginx.conf 中已移除 TLSv1/TLSv1.1,仅保留 TLSv1.2/TLSv1.3(Certbot options-ssl-nginx.conf 原本已正确配置) | nginx | |
| 4 | 已添加内存级速率限制中间件(60 RPM / IP),响应头包含 X-RateLimit-Limit / X-RateLimit-Remaining | cgajs-api |
🟡 P1 — 中优先级(代码质量、运维、依赖)
| # | 问题 | 影响 | 位置 |
|---|---|---|---|
| 5 | 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 | 升级 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 | 已清理 132 个孤儿文件,磁盘文件数从 320 降至 188,与 DB 记录一致 | marketplace.cgajs.com | |
| 10 | index.html 与主站内容不一致 |
已同步 VIP 站点的 index.html、CSS、JS 与主站完全一致 | vip.cgajs.com |
| 11 | 已清理 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 | 新增 8 个测试文件、76 个测试用例,覆盖几何创建、几何操作、变换、纹理材质、内置函数、流程控制、高级 split、高级 parser。总计 95 个测试,覆盖率从 33% 提升至 45% | cgajs-engine | |
| 14 | Parser 中 TODO 未处理:typed parameters in CI |
CI 测试参数化策略待确定,长期技术债 | cgajs-engine |
| 15 | openapi_url="/openapi.json" 配置存在 |
已在 FastAPI 构造函数中设置 openapi_url=None,/openapi.json 现返回 404 |
cgajs-api |