Files

87 lines
3.2 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'json_util.dart';
/// 一套 AI 人设(system prompt)
///
/// 与 [AiConfigItem](模型配置)是**平行的两套配置**:一个管「用哪个模型」,
/// 一个管「用什么人设」。两者正交 —— App 里切模型不会换人设,切人设也不会换模型。
///
/// ⚠️ [systemPrompt] 里**不含日期等环境信息**。那些是运行时事实,
/// 由服务端在每次对话时追加(今天是几号、「这个月」指哪个月)。
/// 所以「新建时预填」拿到的是可编辑的那部分,用户不用管日期。
class AiPersonaItem {
const AiPersonaItem({
required this.id,
required this.name,
required this.systemPrompt,
required this.promptLength,
required this.isDefault,
required this.editable,
required this.defaultPrompt,
this.avatar,
});
/// null = 虚拟的「默认」项(内容是服务端代码内置的)
final int? id;
final String name;
/// 人设 + 运行规则。默认项上是内置默认文本,用户项上是自己配的
final String systemPrompt;
/// 服务端数好的字数,前端直接显示
final int promptLength;
/// 是否是「默认」那一项
final bool isDefault;
/// 是否可编辑/删除。默认项为 false
final bool editable;
/// 内置默认文本。**只有默认项上才有值**,供「恢复默认」按钮使用。
///
/// 放服务端下发而不是 App 里硬编码,是为了两边永远一致 ——
/// 以后改了默认人设,App 不用跟着改版本。
final String? defaultPrompt;
/// 头像**相对路径**(如 `persona/2026/09/29/xxx.jpg`)。
/// 用 [Avatar] 组件显示,它会自己拼 host。null = 用默认头像。
final String? avatar;
/// 用于「显示已选中的是哪一项」的稳定标识。
///
/// 默认项没有 id,用 -1 代替 —— 不能直接用 null 当 map key 去比较。
int get selectionKey => id ?? -1;
factory AiPersonaItem.fromJson(Map<String, dynamic> json) {
return AiPersonaItem(
id: asNullableInt(json['id']),
name: asString(json['name'], fallback: '未命名'),
systemPrompt: asString(json['systemPrompt']),
promptLength: asInt(json['promptLength']),
isDefault: json['isDefault'] == true,
editable: json['editable'] != false,
// 用户项上这个字段是 null —— 不能直接用 asNonEmpty(它要 fallback 参数,
// 而我们恰恰需要保留「null = 没有」这个语义,供「恢复默认」按钮判断)
defaultPrompt: _nullableNonEmpty(json['defaultPrompt']),
avatar: _nullableNonEmpty(json['avatar']),
);
}
/// 列表里显示的两行预览:把换行压平,太长的截断
String get preview {
final flat = systemPrompt.replaceAll(RegExp(r'\s+'), ' ').trim();
if (flat.length <= 50) return flat;
return '${flat.substring(0, 50)}…';
}
}
/// 空串/null 都返回 null,否则原样返回。
///
/// 不用 `asNonEmpty` 是因为它需要一个 fallback 字符串,而这里
/// 「有没有值」本身是有意义的(defaultPrompt 只有默认项才有)。
String? _nullableNonEmpty(Object? value) {
if (value == null) return null;
final s = value.toString();
return s.isEmpty ? null : s;
}