CrazyAirhead

疯狂的傻瓜,傻瓜也疯狂——傻方能执著,疯狂才专注!

0%

项目

近期通过 Claude Code cli 工具(使用的是 GLM 4.7 的模型)开发了一些项目,主体代码通过 AI 生成,人工做些调整。

开发工具箱

通过 go 语言实现,基于 fyne UI 框架开发工具箱,解决 DevHub 工具不够趁手时还要额外在网上查找在线工具的问题。如果想加功能就让 AI 生成,想加就能加,想用就能用。

项目地址:https://gitee.com/CrazyAirhead/dev-tools

img

Java 模型转 SQL语句工具。

通过 Java 语言实现,解决项目只有 Model,没有表结构时,系统无法启动问题,工具的想法来自 easy-query 的 CodeFirst 模式。

项目地址:https://gitee.com/CrazyAirhead/model-to-sql

img

流程编辑器

通过 Typescript 语言实现,基于Soybean Admin(使用 Navie UI 和 vue-flow),解决 Solon Flow 流程编排问题,支持撤回、恢复、自动布局、编辑属性等功能。

项目地址:https://gitee.com/CrazyAirhead/porpoise-flow (未完成代码迁移,预留)

img

表单编辑器

项目基于 https://gitee.com/chengliang4810/naiveui-form-designer, 升级依赖的版本,并转换成 TypeScript 项目,为了能更好的集成到 Soybean Admin 中。

项目地址:https://gitee.com/CrazyAirhead/naiveui-form-designer

img

体会

  • 虽然自己当前使用 Claude Code(AI)的方法还是比较初级,很多东西也还是在摸索阶段。但 AI 已经能写很多自己之前不会的代码。不管怎么样,先用起来更重要。
  • AI 生成代码对于有代码基础的人会更友好一点。如果没基础的人员,还是应该补充的基础知识。
  • 有的时候碰到一些问题不好描述,可以指定文件、代码片段、甚至是变量,而不只是描述界面要怎么样。
  • 限制很重要,一开始需要选好技术框架,不要让AI自由发挥,并维护在 CLAUDE.md 中(如果没有生成的需要自己补充)。
  • 虽然 Claude Code 没有像 /init 一样提供 /update 的命令,但可以直接在对话中说更新 CLAUDE.md,把一些代码上的调整更新到 CLAUDE.md 中。
  • AI 生成的一个好处,就是能先给你提供一些思路或者基础代码,这些可能会改变你的想法,接着可以调整 prompt和设计方案,用重新生成的方式再来一次,比起自己写了推倒重来,迭代的速度变快很多。

Token 统计

最近一个月的 token 统计,也不知道算不算多。

img

如果觉得 GLM-4.7 也还行,最近还有优惠活动。

img

说明

在使用 easy-query 一段时间之后,发现 Jfinal 的 ActiveRecord 的一个很好用的功能,现在用起来变麻烦了,是什么呢?就是实体的丰富化。与实体的裁剪相对,丰富化就是对实体内容进行扩展,返回给前端,比如,存储的是编码,但多返回名称。为了实现实体的裁剪与丰富化,通过是建立新的 DTO,不同的返回结果,增加不同的 DTO,这就会导致 DTO的膨胀或者爆炸。而 ActiveRecord 和 Modle 内部通过 Map 来承载数据,天然具体扩展能力,不管是裁剪还是丰富化,都不需要额外定义 DTO。在 扩展 easy-query 中,我们提到了实体的裁剪和实现方法,有兴趣的同学可以进一步看一看。

因为 easy-query 是基于Bean 的 getter 和 setter 方法来读取和设置数据的,也就是说虽然编写的 DSL 已经返回了扩展的字段,因为 Model 对象缺失 getter 和 setter 方法,而无法返回新增字段的数据。如果要返回新增字段的数据就是需要给实体增加 getter 和 setter 方法。虽然比起DTO的膨胀好了一点,但变得没那么直观和直接了。

于是在 easy-query 的群里,问了这个问题,作者说是比较麻烦的。前几天是了修改FastBean 和 ModelProxy,仍然是没有办法实现。

img

今天重新查询 easy-query 的文档,发现了 EntityMetadataManager 和作者提供了StreamIterableFactory,通过替换这个两个框架行为最终实现了编写 DSL 能返回需要的数据。暂时还没有处理直接 SQL 查询时的数据丰富化。

实现

简单说明 EntityMetadataManager,其实会解析实体哪些是字段,哪些是属性。StreamIterableFactory 实现的是对结果集的转换处理。在 Model Bean 的需求里,其实希望的是,如果不是字段,1. SQL 多返回的字段也需要被认定为是属性,2. 通过Model的 put 方法设置数据,而不是直接忽略。因此本次的扩展也就是围绕着这两点进行的。

img

ModelEntityMetadataManager

如果是 Model 类型的实体,需要特殊处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@Override
public EntityMetadata getEntityMetadata(Class<?> entityClass) {
if (entityClass == null) {
throw new IllegalArgumentException("entityClass");
}
EntityMetadata cacheItem = entityMetadataCache.get(entityClass);
if (cacheItem != null) {
return cacheItem;
}

if (Map.class.isAssignableFrom(entityClass)) {
return entityMetadataCache.computeIfAbsent(
entityClass,
key -> new MapEntityMetadata(Map.class, mapColumnNameChecker, mapKeyNameConversion));
}

if (Row.class.isAssignableFrom(entityClass) || Objects.equals(SqlKit.class, entityClass)) {
return entityMetadataCache.computeIfAbsent(
entityClass,
key -> new RowEntityMetadata(Row.class, mapColumnNameChecker, mapKeyNameConversion));
}

if (Model.class.isAssignableFrom(entityClass)) {
EntityMetadata entityMetadata =
new ModelEntityMetadata(entityClass, mapColumnNameChecker, mapKeyNameConversion);
entityMetadata.init(serviceProvider);

addMetadata(entityMetadata.getTableName(), entityMetadata);
return entityMetadataCache.computeIfAbsent(entityClass, key -> entityMetadata);
}

EntityMetadata entityMetadata = new EntityMetadata(entityClass);
entityMetadata.init(serviceProvider);

addMetadata(entityMetadata.getTableName(), entityMetadata);
return entityMetadataCache.computeIfAbsent(
entityClass,
key -> {
return entityMetadata;
});
}

ModelEntityMetadata

如果属性不是数据库字段,需要将 put 和 set 返回,当作该属性的getter和setter方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Override
public ColumnMetadata getColumnNotNull(String propertyName) {
ColumnMetadata columnMetadata = getColumnOrNull(propertyName);
if (columnMetadata == null) {
String checkField = mapColumnNameChecker.checkColumnName(propertyName);
ColumnOption columnOption = new ColumnOption(false, this, mapKeyNameConversion.convert(checkField), checkField, checkField);
columnOption.setGetterCaller(obj -> {
if (obj instanceof Model<?>) {
Model<?> model = (Model<?>) obj;
return model.get(checkField);
}
return null;
});

columnOption.setSetterCaller((obj, val) -> {
if (obj instanceof Model<?>) {
Model<?> model = (Model<?>) obj;
model.put(checkField, val);
}
});

return new ColumnMetadata(columnOption);
}

return columnMetadata;
}

ModelStreamIterableFactory

需要使用自定义的 ModelStreamIterable

1
2
3
4
5
6
7
public class ModelStreamIterableFactory implements StreamIterableFactory {
@Override
public <T> StreamIterable<T> create(
ExecutorContext context, ResultMetadata<T> resultMetadata, StreamResultSet streamResultSet) {
return new ModelStreamIterable<>(context, resultMetadata, streamResultSet);
}
}

ModelStreamIterable

如果是 Bean 数据,需要使用 ModelBeanStreamIterator。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
private Iterator<T> iterator0() throws SQLException {
EntityMetadataTypeEnum entityMetadataType = resultMetadata.getEntityMetadataType();
switch (entityMetadataType) {
case MAP:
{
if (EasyArrayUtil.isNotEmpty(context.getExpressionContext().getResultPropTypes())) {
return EasyObjectUtil.typeCastNullable(
new MapTypeStreamIterator(
context, streamResultSet, EasyObjectUtil.typeCastNullable(resultMetadata)));
}
return new MapStreamIterator<>(context, streamResultSet, resultMetadata);
}
case BASIC_TYPE:
return new BasicStreamIterator<>(context, streamResultSet, resultMetadata);
// case DRAFT:
// return EasyObjectUtil.typeCastNullable(new DraftStreamIterator(context,
// streamResultSet, EasyObjectUtil.typeCastNullable(resultMetadata)));
default:
{
if (resultMetadata.getDataReader() != null) {
return new FastBeanStreamIterator<>(context, streamResultSet, resultMetadata);
}
if (DraftResult.class.isAssignableFrom(resultMetadata.getResultClass())) {
return EasyObjectUtil.typeCastNullable(
new DraftStreamIterator(
context, streamResultSet, EasyObjectUtil.typeCastNullable(resultMetadata)));
}
return new ModelBeanStreamIterator<>(context, streamResultSet, resultMetadata);
}
}
}

ModelBeanStreamIterator

完全重写 getMapColumnMetadata,如果不是字段的情况,需要使用 get 和 put 方法,另外需要设置 JdbcTypeHandler 为 ObjectTypeHandler。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
private ResultColumnMetadata getMapColumnMetadata(
int index, String columnName, boolean mapToBeanStrict) {
EntityMetadata entityMetadata = resultMetadata.getEntityMetadata();
ColumnMetadata columnMetadata = entityMetadata.getColumnMetadataOrNull(columnName);
if (columnMetadata != null) {
return new EntityResultColumnMetadata(index, entityMetadata, columnMetadata);
}

if (!mapToBeanStrict) {
return resultMetadata.getResultColumnOrNullByPropertyName(index, columnName);
}

ColumnOption columnOption =
new ColumnOption(false, entityMetadata, columnName, columnName, columnName);
columnOption.setGetterCaller(
obj -> {
if (obj instanceof Model<?>) {
return ((Model<?>) obj).get(columnName);
}
return null;
});

columnOption.setSetterCaller(
(obj, val) -> {
if (obj instanceof Model<?>) {
((Model<?>) obj).put(columnName, val);
}
});

columnOption.setJdbcTypeHandler(new ObjectTypeHandler());

columnMetadata = new ColumnMetadata(columnOption);
return new EntityResultColumnMetadata(index, entityMetadata, columnMetadata);
}

效果

查询

CodeColumn(这个 CodeColumn 类字段有点多,且是自动生成的代码,这里就不放代码了)本身不包含 tableName 属性,是通过 CodeTable 的 name 取别名而来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//  @Deform(keep = {"id", "name"})
public List<CodeColumn> simpleListCodeColumn(CodeColumn codeColumn) {
List<CodeColumn> list =
entityQueryable()
.leftJoin(
CodeTable.class,
(t1, t2) -> {
t1.tableId().eq(t2.id());
})
.select(
CodeColumn.class,
(t1, t2) -> Select.of(t1.FETCHER.allFields(), t2.name().as("tableName")))
.toList();
return list;
}

结果

虽然使用的是 CodeColumn 没有 tableName 属性,但是 tableName 的值成功返回了。Solon 的配置字段过滤 Null 值了。

img

代码

扩展 easy-query 的代码已经签入 https://gitee.com/CrazyAirhead/easy-query,增加了

sql-solon-plugin-ext 模块。代码是从我的其他项目迁移过来的,除用了Gradle 和更高版本的 JDK 和 hutool ,因此虽然通过编译,但是仍有可能报错,目前只建议参考,如果要实际使用,碰到问题可能需要自行修复。

公司的文件预览服务使用的是 kkFileView,后面 kkFileView 开始走商业化路线,开源版本就停止更新了。

昨天给客户部署服务的时候,发现给客户部署的 kkFileView 无法启动。客户要求在 Arm 的机器中部署应用,所有的 Docker 镜像都是重新打包的。可是 kkFileView 提供的 Dockerfile 的下载 LibreOffice 的链接无法访问了,使用的 LibreOffice 版本也不支持 arm 架构,所以只能升级 LibreOffice。一开始只是照着原有的 Dockerfile 替换源和替换 LibreOffice 的版本,可能还是无法启动,测试发现是缺失了依赖。费了好大劲才发现说,制作 Docker 镜像的时候,其实不需要使用离线安装包,而应该用 apt-get 的在线安装的方式。

如果没有 AI 工具的话,事情可能就这么结束了。昨晚回家一想,既然我们已经全面使用了 Solon,那么为什么不用 AI 写一个 Solon 版本的文件预览服务呢?我们不需要像 kkFileView 那么支持那么多的文件预览,重点解决 Office 文档的预览,提供 Office 文档转 PDF 的功能就够了。

于是通过 Solon 的模板项目生成器,构建了一个初始化的版本。用户用Claude Code 的 /init 命令对项目进行初始化。因为看过 kkFileView 的一些源码,整体的方案是知道一些的,前端也不想使用分离模式。于是给Claude Code 的提示词是:

我要创建一个基于 Java 的文档转换和预览系统,使用 JODConverter 技术提供高质量的文档格式转换服务,并通过 Bootstrap 5 构建现代化的 Web 界面,先提供 UI 图,供我审核。先不要生成代码,把升级方案生成到 doc 目录。

于是他夸夸一顿操作给我生成了这些文档,比我自己写完整多了。

img

中间对界面做了些调整,要求它使用单独的预览页面而不是用预览区域。让它开始生成代码之后,也就是看看一屏一屏的滚动代码,自己选择 yes 和 apply 就行。

需要注意的是,虽然在 CLAUDE.md 里面已经识别了 Solon 框架,但 Spring 的资料就是比 Solon 全,在技术实现的方案里面,还是会引入 Spring 的依赖,生成的代码还是会包含 Spring 的注解。还碰到的问题是 一些 Solon 的包路径错误,所有这些错误,都需要自己手动调整(我不知道如果不自己调整,最后能不能通过)。

在修改编译通过,项目可以正常启动后,一定要注意签入 Git,这样后面生成的不满意也可以回滚。

接下来的调试和调整了,对于前端来说,我基本不会,所以我只提问题,让 Claude Code 进行修改。对于后端代码来说,我能看其中写的有问题的地方,自己做修改。这里和自己写代码没什么不同,发现错误,修改错误。

让 AI 修改问题时,如果能知道文件名最好直接写在提示词,如果确定代码的部分是一样的,这样修改会更高效些。比如在待上传列表中的标签文字一开始不是白色的,不容易看出来,我让它修改它修改了列表的标题,而不是标签。后面我自己找的标签的位置说,颜色看不清,他很快就把颜色换成白色了。

以下是功能截图:

上传文件

选择文件,出现待上传文件,点击上传按钮,文件显示在已上传列表,此时可以点击转换或者预览。

img

预览 docx 文件

img

预览 xlsx 文件

img

查看转换任务

已经转换好的文件可以直接点击下载按钮进行下载。

img

本次代码生成一个用的是 1000万 GLM-4.6资源包,生成的时候还没用一般,调试问题,修改问题,一会就给我干完了,然后充了个 GLM Coding Lite 才把后面的功能调完。问题处理、调试可能会发送比较多的上下文信息,而且需要经过多轮修改,所以Token 消耗会比多。

经过一天的调整,一个 MVP 算是出来了,主体的文件转换和预览功能都是正常使用的,后续还需要调整代码结构和完善功能。于是每次使用 AI 编程的时候,都感慨 AI 是真行,写代码的成本应该是会越来越低的,自己能做的事情应该是越来越多的。

说明

公司的 Ficus 基础框架中的 ficus-starter-server 使用本地网关对路由做了统一的规划。比如:AdminGateway 统一使用 /admin-api/**路径,使用管理用户鉴权过滤器,会注册所有标记为 adminApi 的组件接口;AppGateway 统一使用 /app-api/**路径,使用会员鉴权过滤器,会注册所有标记为 appApi 的组件接口;而OpenGateway 统一使用 /open-api/**路径,则增加了签名校验的过滤器,会注册所有标记为 openApi 的组件接口。

img

一体机服务也是基于 Ficus 基础框架开发,但是因为是一个独立运行的服务,而不是基于平台的微服务,因此需要调整自己的本地 Gateway 配置。

问题

一体机增加自己的本地网关的配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Component
@Mapping("/admin-api/**")
public class AioAdminGateway extends Gateway {
@Inject private SecurityProperties securityProperties;

/** index 越小优先级越高 */
@Override
protected void register() {
filter(-1, new FicusTraceFilter());
filter(0, new FicusAdminExceptionFilter());
filter(4, new AioAdminFilter(securityProperties));
filter(5, new LicenseFilter());

// 添加Bean
addBeans(bw -> "adminApi".equals(bw.tag()));
}
}

测试的时候发现,虽然 swagger 里可以看到接口,但是请求的时候提示 404,把断点打在 Controller 或者 Filter 上都没有进入。

原因

经过一段时间的排查时找到原因了。ficus-starter-server 组件,会先执行AdminGateway,但因为 autoScanRoute=false 关闭了 Filter 和 Action 的注册,所以 /admin-api/**是没有接口的,也就无法访问了。

AioAdminGateway 虽然也注册到 /admin-api/**,但是更晚注册的,可能地址重复了,实际上没有真正注册 Action。

而 Swagger 没有做判断,只要是 adminApi 标记的就会加载到接口列表中,所以出现了接口可见,但请求不了的怪异现象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* @author airhead
*/
@Component
@Mapping("/admin-api/**")
public class AdminGateway extends BaseGateway {
@Inject private TenantProperties tenantProperties;
@Inject private SecurityProperties securityProperties;
@Inject private WebProperties webProperties;

/** index 越小优先级越高 */
@Override
protected void register() {
// 部分情况不希望server来自动扫描路由,手动添加
if (!webProperties.getAutoScanRoute()) {
return;
}

filter(-1, new FicusTraceFilter());
filter(0, new FicusAdminExceptionFilter());
filter(2, new FicusTenantFilter(tenantProperties));
filter(3, new FicusAdminFilter(securityProperties));

// 添加Bean
addBeans(bw -> "adminApi".equals(bw.tag()));
}
}

处理

使用 Condition 注解,如果autoScanRoute=false不创建对应的 bean。Condition 的使用参考 https://solon.noear.org/article/434。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* @author airhead
*/
@Mapping("/admin-api/**")
@Component
@Condition(onProperty = "${ficus.web.autoScanRoute:true} == true")
public class AdminGateway extends BaseGateway {
@Inject private TenantProperties tenantProperties;
@Inject private SecurityProperties securityProperties;
@Inject private WebProperties webProperties;

/** index 越小优先级越高 */
@Override
protected void register() {
filter(-1, new FicusTraceFilter());
filter(0, new FicusAdminExceptionFilter());
filter(2, new FicusTenantFilter(tenantProperties));
filter(3, new FicusAdminFilter(securityProperties));

// 添加Bean
addBeans(bw -> "adminApi".equals(bw.tag()));
}
}

后续

登记了个 issue,如果路由冲突的时候增加打印告警日志。https://gitee.com/opensolon/solon/issues/ID7MWK

说明

Solon 默认是支持 JDK 25 的(JDK 25 的更新内容可以自行搜索),尝试把现有的项目升级生成到 JDK 25(当前版本 JDK 17),通过 Gradle 管理依赖。

需要变更的内容不多,以下为具体的步骤。

步骤

  • 需要准备 JDK 25,可以通过 IDEA 自动下载 JDK,我这里选择的是 temurin-25 版本(具体版本可以 IDEA 的列表)。

  • 修改编译的 JDK 版本 build.gradle 中指定。

    1
    2
    3
    4
    5
    6
    def jdkVersion = 25
    java {
    toolchain {
    languageVersion = JavaLanguageVersion.of("${jdkVersion}")
    }
    }
  • 修改项目结构中的 SDK 和 Language Level 为 temurin-25 和 SDK default。

  • 修改 IDEA 设置中的 Gradle JVM 为 temurin-25。

  • 修改 gradle-wrapper.properties中的版本为9.2.0

    1
    2
    3
    4
    5
    distributionBase=GRADLE_USER_HOME
    distributionPath=wrapper/dists
    distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.0-bin.zip
    zipStoreBase=GRADLE_USER_HOME
    zipStorePath=wrapper/dists
  • 如果有使用 lombok 插件,需要升级到 9.0.0 版本。

    1
    2
    3
    4
    plugins {
    id 'java'
    id "io.freefair.lombok" version "9.0.0"
    }

    如果编译的时候提示Execution failed for java.lang.ExceptionInInitializerError,应该就是 lombok 版本太低了。

补充

因为没有使用 Maven 管理项目,没有验证。如果使用 Maven 的同学,可以参考 https://junkangworld.com/blog/lombok-jdk-25-the-ultimate-2025-compatibility-guide。

说明

本次升级的 Solon 版本是 3.7.0-M2, 3.7.0 版本还未正式发布,此次升级是一次兼容性测试。

官网对 3.7 的说明「https://solon.noear.org/article/1206」,重点是 snack3 升级到 snack4。如果有使用 snack 序列化的话需要重点关注。

升级

  1. 如果有显示的引入 snack3 或者 solon-serialization-snack3 或者 solon-config-snack3, 都需要修改成对应的 snack4 的版本。

  2. 因为 snack4 修改了包名,部分类的位置和接口,因此也需要同步修改。整体的兼容性情况,参看「https://solon.noear.org/article/1184」。

    如果编译通过基本上就是升级成功了。

可能碰到的问题

  • 接口 @Body 的请求参数或者返回值没有符合预期。

检查 snack3 是否替换干净

  • 如果有对 snack 的序列化做定制,需要先关注 3.6.0 的兼容性说明「https://solon.noear.org/article/1199」统一了序列化和反序列化。

    如果有定制,就需要修改。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    @Managed
    public class SerializerDemo {
    //ps: 这前需要使用 Fastjson2RenderFactory, Fastjson2ActionExecutor 两个对象,且表意不清晰 //(仍可使用)
    @Managed
    public void config(Fastjson2StringSerializer serializer) {
    //序列化(输出用)
    serializer.addEncoder(Date.class, s -> s.getTime());

    serializer.addEncoder(Date.class, (out, obj, o1, type, i) -> {
    out.writeInt64(((Date) obj).getTime());
    });

    serializer.getSerializeConfig().addFeatures(JSONWriter.Feature.WriteMapNullValue); //添加特性
    serializer.getSerializeConfig().removeFeatures(JSONWriter.Feature.BrowserCompatible); //移除特性
    serializer.getSerializeConfig().setFeatures(JSONWriter.Feature.BrowserCompatible); //重设特性

    //反序列化(收接用)
    serializer.getDeserializeConfig().addFeatures(JSONReader.Feature.Base64StringAsByteArray);
    }
    }
  • 如果定制了 Encoder,Encoder 未生效的。

    修改继承类时注意 ObjectEncoderObjectPatternEncoder 的区别, ObjectEncoder 适用于修改单个类,而ObjectPatternEncoder适用于一批类,也就是基类的情况。

    以下是使用 ObjectPatternEncoder 代码示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    @Configuration
    public class DemoConfig {
    @Bean
    public void config(Snack4StringSerializer serializer) {
    serializer.addEncoder(Model.class, new ModelEncoder<>());

    serializer.getDeserializeConfig().addFeatures(Feature.Write_OnlyUseSetter);
    }
    }

    /**
    * @author airhead
    */
    public class ModelEncoder<T extends Model<?>> implements ObjectPatternEncoder<T> {

    /** 对 Model 和 Record 的字段名进行转换的函数。例如转成驼峰形式对 oracle 支持更友好 */
    protected static Function<String, String> fieldNameConverter = StrUtil::toCamelCase;

    @Override
    public ONode encode(EncodeContext ctx, T value, ONode node) {
    if (value == null) {
    return node;
    }

    return encode(node, value._getAttrs());
    }

    @Override
    public boolean canEncode(Object value) {
    return value instanceof Model;
    }

    protected ONode encode(ONode node, Map<String, Object> map) {
    if (CollUtil.isEmpty(map)) {
    return node;
    }

    Map<String, Object> newMap = new HashMap<>(map.size());
    for (Map.Entry<String, Object> entry : map.entrySet()) {
    String fieldName = entry.getKey();
    Object value = entry.getValue();
    String attrName = fieldName;
    if (fieldNameConverter != null) {
    attrName = fieldNameConverter.apply(fieldName);
    }
    newMap.put(attrName, value);
    }

    node.fill(newMap);

    return node;
    }
    }


  • 如果使用了 Lombok,如果指定了 @Accessors(chain = true),如果指定了`serializer.getDeserializeConfig().addFeatures(Feature.Write_OnlyUseSetter),获取的请求参数可能为空。

    因为 chain 生成的 setter 不是标准的 Bean setter,多了返回值。可以通过下面两个方法中的一个的避免这个问题。

    1. 去除 @Accessors(chain = true)
    2. 或者修改 Feature.Write_AllowUseSetter,setter 优先,没有setter 用 字段。

看到宝玉xp 分享说,以后 Claude Code、 Claude Agent SDK 可能成为 Coding Agent 的事实标准,除了 OpenAI 和 Gemini 都会用它的工具来训练模型,最后原来越多的模型兼容 Claude Code。今天看到 karminski-牙医在微博上说 GLM-4.6 可以写生产级代码,突然就想国内这么卷的大模型,是不是都兼容了 Claude Code。虽说现在用 Kimi 已经能满足我写前端的一些需求了,但也可以试试其他模型,看看情况怎么样,于是整理了下国内几个常用的大模型接入 Claude Code 的配置。

使用 Claude Code 的方法基本一致,主要是模型地址和模型不同。

  1. 安装 Nodejs 18+ 版本

  2. 安装 Claude Code

    1
    2
    3
    4
    npm install -g @anthropic-ai/claude-code

    # 检查安装是否成功
    claude --version
  3. 配置,获取大模型平台的 API Keys,需要注册,充值等。

  4. 修改环境变量 ANTHROPIC_BASE_URLANTHROPIC_AUTH_TOKEN

  5. 打开终端,进入项目目录,输入 claude 开始使用

    1
    2
    cd my-project
    claude
  6. 补充,Claude Code 默认是开启自动压缩上下文的,输入 config, 在菜单中将 Auto-compact 设置为 false

GLM

链接

1
https://docs.bigmodel.cn/cn/guide/develop/claude

配置

1
2
3
4
5
6
7
export ANTHROPIC_BASE_URL=https://open.bigmodel.cn/api/anthropic
export ANTHROPIC_AUTH_TOKEN=${YOUR_MOONSHOT_API_KEY}
export API_TIMEOUT_MS=3000000
# 以下也可以不用配置
export ANTHROPIC_DEFAULT_HAIKU_MODEL=glm-4.5-air
export ANTHROPIC_DEFAULT_SONNET_MODEL=glm-4.6
export ANTHROPIC_DEFAULT_OPUS_MODEL=glm-4.6

Kimi-k2

链接

1
https://platform.moonshot.cn/docs/guide/agent-support

配置

1
2
3
4
export ANTHROPIC_BASE_URL=https://api.moonshot.cn/anthropic
export ANTHROPIC_AUTH_TOKEN=${YOUR_MOONSHOT_API_KEY}
export ANTHROPIC_MODEL=kimi-k2-turbo-preview
export ANTHROPIC_SMALL_FAST_MODEL=kimi-k2-turbo-preview

DeepSeek

链接

1
https://api-docs.deepseek.com/zh-cn/guides/anthropic_api

配置

1
2
3
4
5
6
export ANTHROPIC_BASE_URL=https://api.deepseek.com/anthropic
export ANTHROPIC_AUTH_TOKEN=${DEEPSEEK_API_KEY}
export API_TIMEOUT_MS=600000
export ANTHROPIC_MODEL=deepseek-chat
export ANTHROPIC_SMALL_FAST_MODEL=deepseek-chat
export CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1

百炼

链接

1
https://help.aliyun.com/zh/model-studio/claude-code

配置

1
2
3
4
export ANTHROPIC_BASE_URL="https://dashscope.aliyuncs.com/apps/anthropic"
export ANTHROPIC_AUTH_TOKEN=${YOUR_MOONSHOT_API_KEY}
export ANTHROPIC_MODEL=qwen-plus
export ANTHROPIC_SMALL_FAST_MODEL=qwen-flash

在上一篇中说到,通过 AI 生成代码已经完成了「AI 模版生成」配置页面,但当时是没有进行前后端对接的,于是继续给 Claude 提要求,让它生成后端的方案和代码。最终经过一些前后端代码的调整,实现的整体效果如下。

不会编辑视频,这里简单描述下操作的步骤:

  1. 设置 hosts,配置域名实现不同域名切换的效果(注意重新打开浏览器,避免没有生效的问题)
  2. 增加模版,这里只是简单的配置(设置名称等)。
  3. 增加站点,设置域名和选择模版。
  4. 通过域名测试,网页无法打开。
  5. 切换回模板管理,点击「AI模板生成」,随机选择颜色,填写提示词,点击「模板生成」。
  6. 观察日志,代码是否已经生成完毕。
  7. 刷新浏览器,页面已经可以正常浏览。

虽然整理流程是通的,但整个功能不可避免还存在一些问题。另外需要补充的是,内容管理本身没有很复杂的业务,所以可能会比较适合 AI 生成,自己的实际任务能否用 AI 生成,或者按怎么样的颗粒度生成,还要继续摸索。

一样的,需要在 IDEA 中安装 Claude Code 插件,之后通过 /init 初始化,生成 CLAUDE.md

为了方便后端的接口处理,我让它生成输出 JSON 的格式的方案,为了不过多影响阅读,接口方案放在的了最后面。

接着让它按方案执行生成的方案,生成对应的代码。

生成代码的好处:

  • 相对自己写,速度还是快。
  • 生成的代码能使用 Solon 框架,而且还能知道用 ChatModel 进行 llm 的调用。
  • 生成的代码里面还包含了测试代码。
  • 生成的代码包含了一个代码质量评估和重试。

但也发现了些问题:

  • 命名规范的部分没有获取到(后面了解到,可以自己继续修改CLAUDE.md)
  • 生成的后端接口与前端调用的接口不一致(可能是因为我先生成了前端代码,再要求生成的后端代码)
  • 生成的代码比较复杂,是自己完全构建 prompt。
  • 没有实现生成的代码的存储,只做了辅助函数的调用。
  • 修改代码的时候,不比写代码的时间少。

不过问题不大了,自己做了代码上的修改,比如:

  • 接口可能调用比较长,使用了异步调用。
  • 接口的prompt 组装使用了模版,直接从资源文件中读取。
  • 实现生成的代码的实际写入。

进一步的调整:

  • 发现引用的 bootstrap 和 jquery 的 CDN 地址错误,在提示词中调整为引入本地地址,具体内容看用户提示词。
  • 发现生成的css和js没有被引入,在提示词中调整为引入,具体内容看用户提示词。
  • 调用 API 的时候,max_tokens 需要设置为 65536,否则默认的 max_tokens 可能操作返回的内容不完整。具体大小,可以从模型的定价获取,kimi-k2-turbo-preview 的上下文件是 256 k (对应的就是 65536 的 token)
  • 需要配置完整的接口路径,否则可能出错。
1
2
3
4
5
6
cms:
apiUrl: https://api.moonshot.cn/v1/chat/completions
apiKey: sk-xxxxx
provider: openai
model: kimi-k2-turbo-preview
timeout: 600s

经过上面的一些调整,就能看到视频中展示的效果了。

还存在的问题:

  • 生成的网站的HTML,没有完全按提示词要求,比较随机/单一,也希望大佬们指点一二。
  • AI 生成代码的交互,生成完成需要有提示,方便后续调整。
  • 后端的模版语法还没有加到提示词中,还不知道加进去之后的效果。

系统提示词

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
你是一位专业的网站设计工程师,请根据我提供的网站模需求,生成网页代码。

## 强制JSON输出格式要求

你必须返回一个有效的JSON对象,包含以下字段:

```json
{
"html": "<!DOCTYPE html>...",
"css": "/* 样式代码 */",
"js": "// JavaScript代码",
"metadata": {
"title": "页面标题",
"description": "页面描述",
"keywords": ["关键词1", "关键词2"],
"author": "作者信息",
"viewport": "width=device-width, initial-scale=1.0"
}
}
```

**重要约束:**
1. 你的整个响应必须是一个有效的JSON字符串
2. HTML字段必须包含完整的HTML5文档结构
3. CSS字段必须包含所有必要的样式(包括Bootstrap自定义变量)
4. JS字段必须包含所有交互逻辑
5. 不要包含任何解释性文字,只返回JSON
6. 确保JSON可以通过标准JSON解析器解析

记住:你必须返回一个有效的JSON对象,包含html、css、js和metadata字段!不要包含任何解释性文字,只返回JSON。

## 输出质量控制
1. 严格遵循用户提供的样式配置JSON中的所有设置,包括颜色值、字体大小、间距等
2. 确保生成的内容与业务主题高度相关,避免通用模板
3. 仔细检查所有技术规范要求是否被完全实现
4. 验证响应式设计在不同屏幕尺寸下的表现
5. 确认所有交互效果和动画都已正确实现

## 验证流程
在生成代码前,请按以下顺序验证:
1. 样式配置 → 2. 业务主题 → 3. 技术规范 → 4. 内容要求

用户提示词

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
## 样式配置

**请严格遵循这些样式设置(主题色,字体排版,布局样式,组件样式),确保设计一致性**,以下是样式配置的JSON内容:

```json
${styleConfig}
```

## 业务主题详细说明

**核心业务:** [${businessData}]
**目标用户:** [描述目标用户群体]
**主要功能:** [列出3-5个核心功能]
**品牌调性:** [描述品牌个性,如专业、友好、创新等]

**必须包含的内容区块:**
1. 头部hero区域:包含主标题和行动号召
2. 功能特点展示区:至少3个核心功能
3. 客户评价或案例展示
4. 联系或注册区域
5. 页脚作者信息区

## 内容要求

- 所有页面内容必须为简体中文
- 保持原文件的核心信息,但以更易读、可视化的方式呈现
- 在页面底部添加作者信息区域,包含:
- 作者姓名: [CrazyAirhead]
- 社交媒体链接: 至少包含Twitter/X
- 版权信息和年份

## 设计风格

- 整体风格参考Linear App的简约现代设计
- 使用清晰的视觉层次结构,突出重要内容
- 配色方案应专业、和谐,适合长时间阅读

# 技术规范

- **使用Bootstrap框架,通过`/static/lib/bootstrap/css/bootstrap.min.css`和`/static/lib/bootstrap/js/bootstrap.bundle.min.js`引入**
- **使用jQuery库,通过`/static/lib/jquery/jquery-3.6.0.min.js`引入**
- **根据样式配置的JSON内容(主题色,字体排版,布局样式,组件样式等)配置自定义CSS,确保设计一致性, 通过`/styles.css`引入**
- **通过`/scripts.js`引入自定义的js**
- 使用现代CSS特性
- 遵循Web标准和最佳实践
- 响应式设计: 使用Bootstrap网格系统和媒体查询
- 深色模式: 实现主题切换功能,默认跟随系统设置
- 动画效果: 使用CSS3动画和JavaScript
- 代码结构清晰,包含适当注释,便于理解和维护

## 响应式设计

- 页面必须在所有设备上(手机、平板、桌面)完美展示
- 针对不同屏幕尺寸优化布局和字体大小
- 确保移动端有良好的触控体验

## 媒体资源

- 使用文档中的Markdown图片链接(如果有的话)
- 使用文档中的视频嵌入代码(如果有的话)

## 图标与视觉元素

- **使用Font Awesome图标库(通过CDN引入)**
- 根据内容主题选择合适的插图或图表展示数据
- 避免使用emoji作为主要图标

## 交互体验

- 添加适当的微交互效果提升用户体验:
- 按钮悬停时有轻微放大和颜色变化
- 卡片元素悬停时有精致的阴影和边框效果
- 页面滚动时有平滑过渡效果
- 内容区块加载时有优雅的淡入动画

## 性能优化

- 确保页面加载速度快,避免不必要的大型资源
- 图片使用现代格式(WebP)并进行适当压缩
- 实现懒加载技术用于长页面内容

接口方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
# AI 模板生成方案(结构化JSON版)

## 概述

本文档重新设计AI模板生成方案,核心改进是**强制AI返回结构化JSON格式**,而不是HTML文本。这样后端可以直接解析JSON对象,无需复杂的HTML解析逻辑,提高系统的可靠性和维护性。

## 1. 核心改进点

### 1.1 响应格式革命性变化

```diff
- AI返回: HTML文本(需要复杂解析)
+ AI返回: JSON对象(直接解析使用)
```

**新的响应结构:**
```json
{
"html": "<!DOCTYPE html>...",
"css": "/* 样式代码 */",
"js": "// JavaScript代码",
"metadata": {
"title": "页面标题",
"description": "页面描述",
"keywords": ["关键词1", "关键词2"],
"author": "作者信息",
"viewport": "width=device-width, initial-scale=1.0"
}
}
```

### 1.2 优势对比

| 方面 | HTML解析方案 | JSON结构化方案 |
|-----|-------------|---------------|
| **解析复杂度** | 高(需要HTML解析器) | 低(原生JSON解析) |
| **错误率** | 高(解析容易出错) | 低(格式标准化) |
| **维护成本** | 高(复杂解析逻辑) | 低(简单直接) |
| **扩展性** | 差(结构变化影响大) | 好(字段扩展容易) |
| **性能** | 慢(解析开销大) | 快(直接对象访问) |

## 2. 前端配置系统升级

### 2.1 样式配置(StyleConfig)

保持原有结构,但增加模板类型标识:

```typescript
interface StyleConfig {
colors: {
primary: ColorScale; // 主色调
secondary: ColorScale; // 辅助色
neutral: ColorScale; // 中性色
custom: CustomColor[]; // 自定义颜色
};
typography: {
headingFont: string; // 标题字体
bodyFont: string; // 正文字体
baseFontSize: number; // 基础字号
lineHeight: number; // 行高
headingScale: number; // 标题缩放比例
headingWeight: number; // 标题字重
};
ui: {
borderRadius: number; // 圆角半径
shadowLevel: number; // 阴影级别
spacingScale: number; // 间距缩放
buttonStyle: number; // 按钮样式类型
animationLevel: number; // 动画级别
};
template: {
type: 'landing' | 'product' | 'blog' | 'portfolio' | 'dashboard';
layout: 'single' | 'multi' | 'masonry' | 'grid';
complexity: 'simple' | 'medium' | 'complex';
};
meta: {
exportTime: string;
version: string;
hash: string; // 配置哈希值
};
}
```

### 2.2 提示词配置(AIPrompt)

```typescript
interface AIPrompt {
// 核心内容
basePrompt: string; // 结构化提示词内容
styleConfig: StyleConfig; // 样式配置
contentType: 'landing' | 'product' | 'blog' | 'portfolio' | 'dashboard';

// 业务数据
businessData: {
companyName?: string;
productName?: string;
description?: string;
features?: string[];
contactInfo?: {
email?: string;
phone?: string;
address?: string;
};
socialLinks?: {
twitter?: string;
facebook?: string;
linkedin?: string;
instagram?: string;
};
};

// 技术要求
technical: {
responsive: boolean; // 是否需要响应式
darkMode: boolean; // 是否需要深色模式
animations: boolean; // 是否需要动画
seo: boolean; // 是否需要SEO优化
accessibility: boolean; // 是否需要无障碍支持
};

// 内容要求
content: {
language: 'zh-CN' | 'en-US' | 'ja-JP';
tone: 'professional' | 'friendly' | 'creative' | 'technical';
length: 'short' | 'medium' | 'long';
customSections?: string[]; // 自定义段落
};

// 作者信息
authorInfo: {
name: string;
socialLinks: string[];
};

// 模板关联
templateId?: number;
templateCode?: string;
}
```

## 3. 结构化提示词设计

### 3.1 核心提示词模板

创建新的结构化提示词文件 `bootstrap-prompt-structured.txt`

```
你是一位专业的前端开发工程师,请根据我提供的配置和要求,生成结构化的网页模板代码。

# 强制JSON输出格式要求

你必须返回一个有效的JSON对象,包含以下字段:

```json
{
"html": "<!DOCTYPE html>...",
"css": "/* 样式代码 */",
"js": "// JavaScript代码",
"metadata": {
"title": "页面标题",
"description": "页面描述",
"keywords": ["关键词1", "关键词2"],
"author": "作者信息",
"viewport": "width=device-width, initial-scale=1.0"
}
}
```

**重要约束:**
1. 你的整个响应必须是一个有效的JSON字符串
2. HTML字段必须包含完整的HTML5文档结构
3. CSS字段必须包含所有必要的样式(包括Bootstrap自定义变量)
4. JS字段必须包含所有交互逻辑
5. 不要包含任何解释性文字,只返回JSON
6. 确保JSON可以通过标准JSON解析器解析
```

### 3.2 样式变量注入

提示词中包含完整的CSS变量映射:

```
# 样式配置注入

以下是样式配置JSON,请严格遵循这些样式设置:
{styleConfig}

必须包含的CSS自定义变量:

```css
:root {
/* 主色调 */
--primary-50: {primary-50};
--primary-500: {primary-500};
--primary-900: {primary-900};

/* 辅助色 */
--secondary-50: {secondary-50};
--secondary-500: {secondary-500};

/* 字体设置 */
--font-heading: '{headingFont}';
--font-body: '{bodyFont}';
--font-size-base: {baseFontSize}px;
--line-height: {lineHeight};

/* UI元素 */
--border-radius: {borderRadius}px;
--shadow-level: {shadowLevel};
}
```
```

### 3.3 内容类型特定逻辑

根据不同的内容类型,AI应用不同的模板逻辑:

```javascript
const contentTemplates = {
landing: {
sections: ['hero', 'features', 'testimonials', 'cta', 'footer'],
components: ['navbar', 'hero-banner', 'feature-cards', 'testimonial-carousel'],
interactions: ['smooth-scroll', 'fade-in-animation', 'counter-animation']
},
product: {
sections: ['header', 'product-gallery', 'details', 'specs', 'reviews', 'related'],
components: ['image-gallery', 'price-display', 'add-to-cart', 'review-system'],
interactions: ['image-zoom', 'variant-selection', 'quantity-selector']
},
blog: {
sections: ['header', 'article-meta', 'content', 'author-bio', 'related-posts', 'comments'],
components: ['article-content', 'table-of-contents', 'social-share', 'comment-form'],
interactions: ['reading-progress', 'smooth-scroll', 'share-popup']
}
};
```

## 4. 后端API重新设计

### 4.1 主要接口更新

```typescript
POST /cms/template/ai-generate-structured

请求体 (Request Body):
{
// 核心配置
basePrompt: string; // 结构化提示词
styleConfig: StyleConfig; // 样式配置
contentType: 'landing' | 'product' | 'blog' | 'portfolio' | 'dashboard';

// 业务数据
businessData: {
companyName?: string;
productName?: string;
description?: string;
features?: string[];
};

// 技术要求
technical: {
responsive: boolean;
darkMode: boolean;
animations: boolean;
seo: boolean;
};

// 模板关联
templateId?: number;
templateCode?: string;
}

响应体 (Response Body):
{
success: boolean;
data: {
template: {
html: string; // 完整的HTML代码
css: string; // 完整的CSS代码
js: string; // 完整的JavaScript代码
metadata: {
title: string;
description: string;
keywords: string[];
author: string;
viewport: string;
};
};
metadata: {
aiModel: string; // 使用的AI模型
tokensUsed: number; // 使用的token数量
generationTime: number; // 生成时间(秒)
styleHash: string; // 样式配置哈希
complexity: string; // 复杂度等级
quality: number; // 质量评分(0-100)
};
};
error?: {
code: string;
message: string;
details?: any;
};
}
```

### 4.2 新的处理流程

```
接收请求 → 参数验证 → 构建结构化提示词 → 调用AI模型 →
解析JSON响应 → 验证JSON结构 → 代码质量检查 →
生成文件 → 质量评分 → 返回结果
```

## 5. 后端处理逻辑重构

### 5.1 JSON响应解析

```typescript
interface AIResponse {
html: string;
css: string;
js: string;
metadata: {
title: string;
description: string;
keywords: string[];
author: string;
viewport: string;
};
}

class AIResponseParser {
parseResponse(rawResponse: string): AIResponse {
try {
// 1. 清理响应文本
const cleanedResponse = this.cleanResponse(rawResponse);

// 2. 解析JSON
const parsed = JSON.parse(cleanedResponse);

// 3. 验证必需字段
this.validateRequiredFields(parsed);

// 4. 验证字段类型
this.validateFieldTypes(parsed);

// 5. 内容安全检查
this.securityCheck(parsed);

return parsed;
} catch (error) {
throw new AIResponseParseError(`JSON解析失败: ${error.message}`);
}
}

private cleanResponse(response: string): string {
// 移除可能的markdown代码块标记
return response
.replace(/```json\n/g, '')
.replace(/\n```/g, '')
.replace(/```/g, '')
.trim();
}

private validateRequiredFields(parsed: any): void {
const requiredFields = ['html', 'css', 'js', 'metadata'];
for (const field of requiredFields) {
if (!parsed[field]) {
throw new Error(`缺少必需字段: ${field}`);
}
}
}

private validateFieldTypes(parsed: any): void {
if (typeof parsed.html !== 'string') {
throw new Error('html字段必须是字符串');
}
if (typeof parsed.css !== 'string') {
throw new Error('css字段必须是字符串');
}
if (typeof parsed.js !== 'string') {
throw new Error('js字段必须是字符串');
}
if (!this.isValidMetadata(parsed.metadata)) {
throw new Error('metadata字段格式不正确');
}
}

private securityCheck(parsed: any): void {
// 检查是否包含恶意代码
const htmlSafe = this.checkHtmlSafety(parsed.html);
const cssSafe = this.checkCssSafety(parsed.css);
const jsSafe = this.checkJsSafety(parsed.js);

if (!htmlSafe || !cssSafe || !jsSafe) {
throw new Error('生成的代码包含不安全内容');
}
}
}
```

### 5.2 代码质量验证

```typescript
class CodeQualityValidator {
validateHTML(html: string): ValidationResult {
const issues: string[] = [];

// 1. HTML结构验证
if (!html.includes('<!DOCTYPE html>')) {
issues.push('缺少DOCTYPE声明');
}
if (!html.includes('<html lang="zh-CN"')) {
issues.push('缺少语言声明或不是中文');
}
if (!html.includes('<meta charset="UTF-8">')) {
issues.push('缺少字符编码声明');
}
if (!html.includes('<meta name="viewport"')) {
issues.push('缺少viewport声明');
}

// 2. 必需的CSS和JS引入
if (!html.includes('bootstrap.min.css')) {
issues.push('缺少Bootstrap CSS引入');
}
if (!html.includes('jquery.min.js')) {
issues.push('缺少jQuery引入');
}

return {
valid: issues.length === 0,
issues,
score: Math.max(0, 100 - issues.length * 10)
};
}

validateCSS(css: string, styleConfig: StyleConfig): ValidationResult {
const issues: string[] = [];

// 1. CSS变量验证
const requiredVars = [
'--primary-500',
'--secondary-500',
'--font-heading',
'--font-body',
'--border-radius'
];

for (const varName of requiredVars) {
if (!css.includes(varName)) {
issues.push(`缺少CSS变量: ${varName}`);
}
}

// 2. 响应式设计检查
if (!css.includes('@media')) {
issues.push('缺少响应式媒体查询');
}

// 3. 安全性检查
if (css.includes('expression(') || css.includes('javascript:')) {
issues.push('CSS包含潜在的安全风险');
}

return {
valid: issues.length === 0,
issues,
score: Math.max(0, 100 - issues.length * 15)
};
}

validateJS(js: string): ValidationResult {
const issues: string[] = [];

try {
// 1. 语法检查
new Function(js);
} catch (error) {
issues.push(`JavaScript语法错误: ${error.message}`);
}

// 2. 必需的函数检查
const requiredFunctions = ['initTheme', 'toggleTheme'];
for (const funcName of requiredFunctions) {
if (!js.includes(funcName)) {
issues.push(`缺少必需的函数: ${funcName}`);
}
}

// 3. 安全性检查
if (js.includes('eval(') || js.includes('Function(')) {
issues.push('JavaScript包含不安全的函数调用');
}

return {
valid: issues.length === 0,
issues,
score: Math.max(0, 100 - issues.length * 20)
};
}
}
```

## 6. AI模型调用优化

### 6.1 模型选择策略

```typescript
interface ModelConfig {
name: string;
maxTokens: number;
temperature: number;
topP: number;
bestFor: string[];
}

const modelConfigs: ModelConfig[] = [
{
name: 'gpt-4-turbo-preview',
maxTokens: 4000,
temperature: 0.3, // 降低创造性,提高一致性
topP: 0.9,
bestFor: ['complex', 'multi-section', 'interactive']
},
{
name: 'claude-3-opus',
maxTokens: 4000,
temperature: 0.2,
topP: 0.95,
bestFor: ['structured', 'json-output', 'precise']
}
];

function selectOptimalModel(prompt: AIPrompt): string {
const complexity = calculateComplexity(prompt);
const requiresJSON = true; // 总是需要JSON输出

// 优先选择擅长结构化输出的模型
if (requiresJSON) {
return 'claude-3-opus'; // Claude更擅长结构化输出
}

if (complexity > 70) {
return 'gpt-4-turbo-preview';
}

return 'claude-3-opus';
}
```

### 6.2 提示词优化

```typescript
function buildStructuredPrompt(prompt: AIPrompt): string {
const basePrompt = loadPromptTemplate('bootstrap-prompt-structured');

// 1. 样式配置注入
const styleInjection = generateStyleInjection(prompt.styleConfig);

// 2. 内容类型特定逻辑
const contentLogic = generateContentLogic(prompt.contentType);

// 3. 技术要求注入
const technicalRequirements = generateTechnicalRequirements(prompt.technical);

// 4. 业务数据注入
const businessData = generateBusinessData(prompt.businessData);

return `
${basePrompt}

# 样式配置注入
${styleInjection}

# 内容类型逻辑
${contentLogic}

# 技术要求
${technicalRequirements}

# 业务数据
${businessData}

# JSON输出格式提醒
记住:你必须返回一个有效的JSON对象,包含html、css、js和metadata字段!
`.trim();
}
```

## 7. 错误处理和降级策略

### 7.1 JSON解析错误处理

```typescript
async function generateTemplateWithFallback(prompt: AIPrompt): Promise<TemplateResult> {
const maxRetries = 3;
let lastError: Error;

for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
// 1. 调用AI模型
const aiResponse = await callAIModel(prompt);

// 2. 解析JSON响应
const parsedResponse = parseAIResponse(aiResponse);

// 3. 验证代码质量
const validationResult = await validateCodeQuality(parsedResponse);

if (validationResult.overallScore >= 70) {
return {
success: true,
template: parsedResponse,
quality: validationResult.overallScore
};
} else {
throw new Error(`代码质量分数过低: ${validationResult.overallScore}`);
}
} catch (error) {
lastError = error;
console.warn(`第${attempt}次尝试失败:`, error.message);

if (attempt < maxRetries) {
// 等待一段时间后重试
await sleep(1000 * attempt);
}
}
}

// 所有重试都失败,使用降级方案
return generateFallbackTemplate(prompt, lastError);
}
```

### 7.2 降级方案

```typescript
function generateFallbackTemplate(prompt: AIPrompt, originalError: Error): TemplateResult {
const templateType = prompt.contentType;
const styleConfig = prompt.styleConfig;

// 1. 获取基础模板
const baseTemplate = getBaseTemplate(templateType);

// 2. 应用样式配置
const styledTemplate = applyStyleConfig(baseTemplate, styleConfig);

// 3. 注入业务数据
const finalTemplate = injectBusinessData(styledTemplate, prompt.businessData);

return {
success: true,
template: finalTemplate,
quality: 60, // 降级模板质量分数较低
isFallback: true,
originalError: originalError.message
};
}
```

## 8. 质量评估和优化

### 8.1 自动质量评分

```typescript
interface QualityMetrics {
structure: number; // HTML结构完整性
styling: number; // CSS样式质量
functionality: number; // JavaScript功能完整性
accessibility: number; // 无障碍支持
performance: number; // 性能优化
overall: number; // 综合评分
}

function calculateQualityScore(template: AIResponse, prompt: AIPrompt): QualityMetrics {
const structureScore = evaluateHTMLStructure(template.html);
const stylingScore = evaluateCSSQuality(template.css, prompt.styleConfig);
const functionalityScore = evaluateJSQuality(template.js);
const accessibilityScore = evaluateAccessibility(template.html);
const performanceScore = evaluatePerformance(template);

const overall = Math.round(
(structureScore * 0.25 +
stylingScore * 0.25 +
functionalityScore * 0.20 +
accessibilityScore * 0.15 +
performanceScore * 0.15)
);

return {
structure: structureScore,
styling: stylingScore,
functionality: functionalityScore,
accessibility: accessibilityScore,
performance: performanceScore,
overall
};
}
```

### 8.2 持续优化机制

```typescript
// 收集使用数据
interface UsageAnalytics {
prompt: string;
styleConfig: StyleConfig;
qualityScore: number;
userFeedback?: number;
generationTime: number;
errorCount: number;
}

// 模型微调数据准备
function prepareFineTuningData(analytics: UsageAnalytics[]): TrainingData[] {
return analytics
.filter(data => data.qualityScore > 80 && data.userFeedback >= 4)
.map(data => ({
prompt: data.prompt,
styleConfig: data.styleConfig,
expectedOutput: generateExpectedOutput(data),
qualityScore: data.qualityScore
}));
}
```

## 9. 实施计划(更新版)

### 9.1 第一阶段:核心重构(1-2周)
- [ ] 创建结构化提示词模板
- [ ] 实现JSON响应解析器
- [ ] 更新API接口定义
- [ ] 基础错误处理

### 9.2 第二阶段:质量保障(2-3周)
- [ ] 代码质量验证器
- [ ] JSON结构验证
- [ ] 安全内容检查
- [ ] 降级方案实现

### 9.3 第三阶段:智能优化(3-4周)
- [ ] 模型选择优化
- [ ] 提示词动态生成
- [ ] 质量评分系统
- [ ] 性能监控

### 9.4 第四阶段:高级功能(4-5周)
- [ ] 多模型支持
- [ ] 缓存优化
- [ ] A/B测试框架
- [ ] 用户反馈集成

## 10. 新方案的优势总结

### 10.1 技术优势

1. **解析可靠性**:JSON格式标准化,解析错误率降低90%
2. **维护简单性**:无需复杂的HTML解析逻辑,代码量减少60%
3. **扩展灵活性**:字段扩展容易,不影响现有解析逻辑
4. **类型安全**:TypeScript接口定义,编译时错误检查
5. **性能提升**:直接对象访问,比HTML解析快10倍以上

### 10.2 业务优势

1. **质量保证**:结构化的代码更容易验证和测试
2. **用户体验**:更快的响应时间和更稳定的输出
3. **开发效率**:前后端开发分离,并行开发更容易
4. **错误诊断**:JSON结构错误更容易定位和修复
5. **数据分析**:结构化数据便于统计分析和优化

### 10.3 长期价值

1. **标准化**:为未来的模板生成建立标准
2. **自动化**:更容易实现全自动化的CI/CD流程
3. **智能化**:为AI模型的进一步优化提供数据基础
4. **平台化**:为构建模板生成平台奠定基础

## 总结

新的结构化JSON方案彻底解决了HTML解析的痛点,通过强制AI返回标准化的JSON格式,实现了:

- **零解析错误**:直接JSON解析,无需复杂的HTML处理
- **高质量代码**:结构化的代码更容易验证和优化
- **快速响应**:简化的处理流程,提高生成速度
- **易于维护**:清晰的代码结构和错误处理
- **扩展性强**:灵活的结构支持未来的功能扩展

这个方案为构建可靠、高效、智能的AI模板生成系统奠定了坚实的技术基础。现在需要更新前端使用新的结构化提示词,后端实现JSON解析逻辑,整个系统就可以按照新的方案运行。建议优先实施核心重构阶段,让系统先能正常工作,然后逐步完善质量保障和智能优化功能。

最近找同事学习了些 AI 代码生成工具的使用,算是我的一次升级。先看下我生成代码的效果,从点击 「AI生成」开始的界面都是由 AI 生成的。如果觉得效果还可以的话,可以继续往下看。

作为一个普通开发人员,在使用先进的 AI 模型的时候,还是需要跨过几个门槛,比如网络,比如支付,比如封号等问题。我自己就是好不容易搞定支付之后,刚想用用 OpenAI 就被封号了。于是有一段时间虽然看到一些比较好的代码生成效果,自己也没有特别实践,导致自己使用 AI 变成工具非常原始。这次和同事聊天,他说到,自己写一个功能,自己就没写过代码。于是非常好奇他的工作流是怎么样的,然后过去学习了下,自己也验证了一番之后,果然有效。

我之前的使用方式是这样的,使用VS Code,然后 ROO CODE 插件,配置的是 DeepSeek。在使用时,直接在 Prompt 里面说要什么功能,然后等大模型生成,然后除了接受就是接受,最终完成的东西很大程度都没办法完成,需要自己继续修改。于是最终主体代码都是自己写,然后就是让ROO CODE 帮忙完成一些小功能,或者修复直接的报错的问题。

同事说,之前他也这么用,但是发现效果不好,后面就是让 AI 先整理自己的需求,然后提供方案,而不是直接提供代码。可以对方案提供质疑,然后进一步完善方案,方案完成后才让 AI 写代码。

在看了同事的直接操作之后,于是回家开始使用 Claud Code 插件,如果没有注册 Claude Code,可以使用下面的两种方式替代:

  1. 使用 Claude 镜像站,https://www.aigocode.com/,同事用的是这个方式。
  2. 使用 Kimi 替代,我自己用的是这种方式。

大家可以根据实际的情况自己选择,我这里简单描述下使用 Kimi 替代的操作。

  1. 注册好 https://platform.moonshot.cn/ 账号,充值解除限速,并配置好 API Key
  2. 安装好 Node.js
  3. 安装 Claude
1
npm install -g @anthropic-ai/claude
  1. 配置环境变量,导出 ANTHROPIC_BASE_URLANTHROPIC_AUTH_TOKEN
1
2
3
4
5
# 如果是其他操作系统,自己用对应的方式修改。
vi .zshrc

export ANTHROPIC_BASE_URL=https://api.moonshot.cn/anthropic
export ANTHROPIC_AUTH_TOKEN=sk-xxxxxxxxx
  1. 安装 VS Code 的 Claude Code for VS Code 插件
  2. 打开项目,通过/init 进行初始化。
  3. 接着可以通过对话的方式进行代码编写了。

以下是演示的生成界面的主体过程(中间有关闭窗口,没有完整的记录),这个界面首先来自,https://mp.weixin.qq.com/s/tafopd82apfEtCk2DXU0sg,我期望的是通过 AI 生成配置之后生成 CMS 站点的模版。这里主要就是生成配置页面的功能。

我的需求文档整理如下:

1
2
3
4
5
6
7
8
9
10
我现在想做为内容管理模块增加 AI 生成站点模板的功能。
通过模板管理的,[AI 模板] 进入对应的功能页面。

整个页面的核心就是两个部分一个是调整样式,一个是修改提示词,生成站点模板。

首先可以在页面左侧对网页样式进行调整,比如主题色、辅助色、字体、大小、字间距、按钮样式等。为了简化网页样式的调整,需要有兜底方案,首先你可以选择预设的各种风格模板,另外的话可以点击「随机生成」按钮,点完所有的元素都会变化,用户可以多点几次挑选自己喜欢的配置。

可以在右侧预览,预览需要做了三个页面首页、产品、博客,方便在不同的页面内容上预览,通过 Tab 进行切换。另外预览的部分还支持手机、电脑、平板不同页面宽度的预览,需要支持夜间模式。

最后用户可以设置补充默认的提示词内容,点击生成,把设置好的 JSON 和提示词调用 AI 接口生成对应的模板页面。

可以让 Claude 之间生成到 doc 目录下,初步返回的内容如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
1. 功能模块划分

核心模块:
- 样式配置器(左侧)
- 实时预览器(右侧)
- AI生成引擎

2. 样式配置器设计

预设风格模板:
- 现代简约(Linear风格)
- 商务专业
- 创意艺术
- 科技感
- 温暖亲和

样式配置维度:
interface StyleConfig {
theme: {
primaryColor: string;
secondaryColor: string;
accentColor: string;
backgroundColor: string;
surfaceColor: string;
};
typography: {
fontFamily: string;
baseFontSize: number;
headingScale: number;
lineHeight: number;
letterSpacing: number;
};
layout: {
borderRadius: number;
spacing: number;
maxWidth: number;
gridGap: number;
};
components: {
buttonStyle: 'filled' | 'outlined' | 'ghost';
cardElevation: number;
inputStyle: 'underline' | 'outline' | 'filled';
};
}

3. 预览器设计

多页面预览:
- 首页(Landing Page)
- 产品页(Product Showcase)
- 博客页(Blog Layout)

响应式预览:
- 手机(375px)
- 平板(768px)
- 桌面(1440px)

主题切换:
- 浅色模式
- 深色模式
- 自动跟随系统

4. AI生成引擎优化

提示词结构:
interface AIPrompt {
basePrompt: string; // 来自bootstrap-prompt.txt
styleConfig: StyleConfig;
contentType: 'landing' | 'product' | 'blog';
additionalRequirements: string[];
authorInfo: {
name: string;
socialLinks: string[];
};
}

生成流程:
1. 用户配置样式参数
2. 系统生成标准化JSON配置
3. 结合提示词调用AI接口
4. 生成完整HTML模板

5. 用户界面设计

左侧配置面板:
┌─────────────────────────────────────┐
│ 预设模板选择器 │
├─────────────────────────────────────┤
│ 主题色彩配置 │
│ ├── 主色调 ■ #3B82F6 │
│ ├── 辅助色 ■ #10B981 │
│ └── 背景色 ■ #FFFFFF │
├─────────────────────────────────────┤
│ 字体排版设置 │
│ ├── 字体族 [Inter] ▼ │
│ ├── 基础字号 16px [-][+] │
│ └── 行高 1.5 [-][+] │
├─────────────────────────────────────┤
│ 组件样式 │
│ ├── 按钮样式 [填充] ▼ │
│ └── 圆角半径 8px [-][+] │
├─────────────────────────────────────┤
│ [🎲 随机生成] [💾 保存配置] │
└─────────────────────────────────────┘

右侧预览面板:
┌─────────────────────────────────────┐
│ [首页] [产品] [博客] | 📱 💻 🖥️ 🌓 │
├─────────────────────────────────────┤
│ │
│ 实时预览区域 │
│ │
│ ╔═══════════════════════╗ │
│ ║ ║ │
│ ║ 模板预览内容 ║ │
│ ║ ║ │
│ ╚═══════════════════════╝ │
│ │
└─────────────────────────────────────┘

6. 技术实现方案

前端架构:
- Vue 3 Composition API
- Naive UI组件库
- UnoCSS样式框架
- Pinia状态管理

配置持久化:
- LocalStorage保存用户配置
- 支持导出/导入JSON配置
- 版本管理支持

AI集成:
- 封装通用AI调用接口
- 支持多种AI服务提供商
- 缓存机制优化性能

7. 交互优化

用户体验增强:
- 配置变更实时预览
- 撤销/重做功能
- 键盘快捷键支持
- 拖拽调整参数
- 批量应用样式

性能优化:
- 虚拟滚动长列表
- 防抖处理频繁操作
- 懒加载预览内容
- 压缩传输数据

8. 扩展功能

高级功能:
- 自定义CSS注入
- JavaScript交互增强
- SEO优化配置
- 性能指标监控
- A/B测试支持

这个设计方案充分考虑了用户友好性、技术可行性和扩展性,能够满足内容管理系统中AI生成站点模板的核心需求。

当时感觉已经和细化了,特别是有同事提到的 ascii 的 ui,于是让 Claude 继续生成。

63407f72e3b7c378f582db235b50aaff

一开始生成的页面是这样的,配置在左侧,Prompt在下面,不是太好的利用页面,于是让大模型把配置和提示词配置都放到右侧,用Tab 进行切换。

接下来发现一些交互的问题,直接发给Claude 进行修改。

![image-20250918230352310](/Users/airhead/Library/Application Support/typora-user-images/image-20250918230352310.png)

在这个过程中当中,注意即使提交 Git,以保留修改的成果,毕竟 AI 生成的也还是有些随机。另外一个就是因为项目是由 lint check的,如果 Claude 自己不能修改的情况,可以用 ROO CODE。一个模型发现不了的问题,另一个模型能发现。

整个环境验证下拉自己基本没有写代码,都是交给了 Claude Code 插件来处理的。

下面是完成这个功能,修改其中的BUG,产生的消费记录。

![image-20250918230945685](/Users/airhead/Library/Application Support/typora-user-images/image-20250918230945685.png)

通过这次AI 代码生成工具使用方法的调整,让我实现了一些我之前肯定做不出来的效果,而且我也基本上没有写代码,更重要的省钱省时间。

说明

《Solon 实用教程》是有计划写一个管理系统的完整示例的,但代码更新比较慢,也就没有迁移到示例仓库中。在调试前端代码的时候发现,对我来说,因为不熟悉前端,即便有了SoyBean 和 alovajs 开发起来也挺不容易。这段时间利用系统提供的代码生成功能,在 AI 的加持下,完成了内容管理的功能,算是验证了这个管理系统的基本可用。于是退而求其次,先把管理系统当前的功能做了些截图,也相当于是一个预览版本。如果公布了预览版本,也可以逼迫自己继续花时间来整理和开发这个管理端系统。

功能列表

  • 系统管理
    • 用户管理
    • 角色管理
    • 权限管理
    • 配置管理
    • 字典管理
  • 基础设施
    • 存储管理
      • 存储配置
      • 存储文件
  • 开发管理
    • 代码生成
      • 数据源配置
      • 生成配置
  • 内容管理
    • 文章管理
    • 栏目管理
    • 菜单管理
    • 站点管理
    • 模板管理
  • AI (进行中)
    • 聊天
    • 工作流

登录

img

首页

img

系统管理

用户管理

img

img

角色管理

img

权限管理

img

配置管理

img

字典管理

img

基础设施

存储管理

存储配置

img

存储文件

img

开发管理

代码生成

数据源配置

img

生成配置

img

img

内容管理

文章管理

img

栏目管理

img

菜单管理

img

站点管理

img

模版管理

img

img

AI

AI 部分是正在尝试去做的功能。

聊天

img

工作流

img