Skip to content

Commit 9397e70

Browse files
committed
重构“枚举”类型状态属性
1 parent 1a326ff commit 9397e70

28 files changed

Lines changed: 706 additions & 158 deletions

Assets/CHANGELOG.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,13 @@
33
* 修复了知识检索配置未生效的问题
44
* 为项目编辑界面新增通用提示文本, 以帮助确定哪些字段会否进入/不进入生成检索流程
55
* 修正了知识系统
6-
* 知识条目“标题” → “备注”
7-
* 知识条目“标签” → “关键字
6+
* 知识条目“标题” → “备注”
7+
* 知识条目“标签” → “关键词
88
* 简化了状态栏更新逻辑, 现在仅发送 AI 确实可以修改的条目, 避免分散注意力
99
* 极大简化了配置读取逻辑
1010
* 移除了状态值“复合”类型
1111
* 使部分遗漏的枚举也进入了本地化体系
12+
* 重构了状态系统中“枚举”类型的配置方式
13+
* 移除了与该类型不相关的界面元素
14+
* 可以单独指定每个枚举如何触发, 包含“随机”与“表达式两种方式”, “随机”方式下可以为枚举单独指定概率,
15+
“表达式”方式下可以为该枚举关联其他“数值”类型的状态属性, 并在满足表达式时切换至该枚举

Assets/Langs/zh-CN.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,15 @@
214214
"State.Driver.System.Help": "按规则自动变换",
215215
"State.Trigger.SceneChange": "场景切换时",
216216
"State.Trigger.RoundEnd": "每轮结束时",
217-
"State.Trigger.Custom": "自定义",
217+
"State.TransitionMethod.Method": "变更方式",
218+
"State.TransitionMethod.Random": "随机",
219+
"State.TransitionMethod.Expression": "表达式",
220+
"State.TransitionMethod.Probability": "概率",
221+
"State.TransitionMethod.Attribute": "关联属性",
222+
"State.TransitionMethod.ExpressionPlaceholder": "如: {val} >= 1000",
223+
"State.TransitionMethod.SwitchMode": "切换方式",
224+
"State.SwitchMode.Always": "始终切换",
225+
"State.SwitchMode.Once": "切换一次",
218226
"State.Panel.NotStarted": "未开始",
219227
"State.Panel.Scene": "场景",
220228
"State.Panel.Timeline": "时间线",

DirectorPrompt.Agents/AttributeConfigSerializer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ public static class AttributeConfigSerializer
88
public static JsonSerializerOptions Options { get; } = new()
99
{
1010
PropertyNameCaseInsensitive = true,
11-
Converters = { new JsonStringEnumConverter() }
11+
Converters = { new JsonStringEnumConverter() }
1212
};
1313

1414
public static T? Deserialize<T>(string json) where T : class

DirectorPrompt.Agents/Pipeline/PostProcessingStage.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ private async Task<string> BuildAgentContextAsync(ToolExecutionContext context,
9494
}
9595

9696
var attributes = (await stateRepository.GetAttributesAsync(context.ProjectID, StateScope.Global, cancellationToken))
97-
.Where(a => a.Driver == Driver.Narrative)
97+
.Where(a => a.Driver == Driver.Narrative && a.ValueType != StateValueType.Enum)
9898
.ToList();
9999

100100
if (attributes.Count > 0)
@@ -105,7 +105,7 @@ private async Task<string> BuildAgentContextAsync(ToolExecutionContext context,
105105

106106
foreach (var attr in attributes)
107107
{
108-
var value = await stateRepository.GetStateValueAsync(attr.ID, context.SessionID, cancellationToken);
108+
var value = await stateRepository.GetStateValueAsync(attr.ID, context.SessionID, cancellationToken);
109109
var type = FormatType(attr);
110110
var constraint = FormatConstraint(attr);
111111
var rules = FormatRules(attr);
@@ -147,7 +147,7 @@ private async Task<string> BuildAgentContextAsync(ToolExecutionContext context,
147147
}
148148

149149
var categoryAttrs = (await stateRepository.GetAttributesAsync(context.ProjectID, StateScope.Category, cancellationToken))
150-
.Where(a => a.Driver == Driver.Narrative)
150+
.Where(a => a.Driver == Driver.Narrative && a.ValueType != StateValueType.Enum)
151151
.ToList();
152152

153153
if (categoryAttrs.Count > 0)

DirectorPrompt.Agents/Prompts/MemorySubAgentPrompt.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public static class MemorySubAgentPrompt
2323
"""
2424
你是记忆更新系统。分析 `---` 后的叙事文本, 结合上下文调用工具更新记忆、状态与人物。
2525
26-
状态更新: 叙事中出现影响状态属性的变化时, 根据上下文表格中 narrative 驱动的属性调用工具更新。全局数值属性用 update_state 传入变化量, 枚举属性用 set_state 传入新值; 人物数值属性用 update_character_state, 枚举属性用 set_character_state。system 驱动属性不可修改。无相关变化时跳过。
26+
状态更新: 叙事中出现影响状态属性的变化时, 根据上下文表格中的属性调用工具更新。全局数值属性用 update_state 传入变化量; 人物数值属性用 update_character_state。枚举属性由系统自动变换, 不可通过工具修改。无相关变化时跳过。
2727
2828
人物建档: 仅对有具体姓名或固定称谓、与已有角色产生直接互动、或导演指令明确引入的角色建档。无名群众只在 create_memory 中记录。
2929

DirectorPrompt.Agents/SystemStateTransformer.cs

Lines changed: 152 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using System.Collections.Concurrent;
2+
using System.Globalization;
13
using DirectorPrompt.Domain.Configurations;
24
using DirectorPrompt.Domain.Enums;
35
using DirectorPrompt.Domain.Models;
@@ -14,6 +16,8 @@ public sealed class SystemStateTransformer
1416
IConditionEngine conditionEngine
1517
) : ISystemStateTransformer
1618
{
19+
private readonly ConcurrentDictionary<(long SessionID, long AttributeID, long? CharacterID, string Option), bool> onceTriggered = new();
20+
1721
public async Task ExecuteAsync
1822
(
1923
long projectID,
@@ -33,11 +37,11 @@ public async Task ExecuteAsync
3337
);
3438

3539
var attributes = await stateRepository.GetAttributesAsync(projectID, null, cancellationToken);
36-
var systemAttrs = attributes.Where(a => a.Driver == Driver.System).ToList();
40+
var systemAttrs = attributes.Where(a => a.Driver == Driver.System || a.ValueType == StateValueType.Enum).ToList();
3741

3842
if (systemAttrs.Count == 0)
3943
{
40-
Log.Debug("无 system 驱动的状态属性, 跳过");
44+
Log.Debug("无系统驱动的状态属性, 跳过");
4145
return;
4246
}
4347

@@ -78,24 +82,24 @@ private async Task TransformGlobalAttributeAsync
7882
CancellationToken cancellationToken
7983
)
8084
{
81-
if (attr.ValueType == StateValueType.Enum)
82-
{
83-
var value = await stateRepository.GetStateValueAsync(attr.ID, sessionID, cancellationToken);
84-
var currentValue = value?.Value ?? string.Empty;
85+
if (attr.ValueType != StateValueType.Enum)
86+
return;
8587

86-
await TransformEnumAttributeAsync
87-
(
88-
attr,
89-
sessionID,
90-
sceneID,
91-
roundID,
92-
trigger,
93-
currentValue,
94-
globalStateValues,
95-
null,
96-
cancellationToken
97-
);
98-
}
88+
var value = await stateRepository.GetStateValueAsync(attr.ID, sessionID, cancellationToken);
89+
var currentValue = value?.Value ?? string.Empty;
90+
91+
await TransformEnumAttributeAsync
92+
(
93+
attr,
94+
sessionID,
95+
sceneID,
96+
roundID,
97+
trigger,
98+
currentValue,
99+
globalStateValues,
100+
null,
101+
cancellationToken
102+
);
99103
}
100104

101105
private async Task TransformCategoryAttributeAsync
@@ -111,6 +115,9 @@ private async Task TransformCategoryAttributeAsync
111115
CancellationToken cancellationToken
112116
)
113117
{
118+
if (attr.ValueType != StateValueType.Enum)
119+
return;
120+
114121
foreach (var character in characters)
115122
{
116123
var charValues = await characterRepository.GetCharacterStateValuesAsync(character.ID, cancellationToken);
@@ -122,23 +129,20 @@ CancellationToken cancellationToken
122129
v => v.Value
123130
);
124131

125-
if (attr.ValueType == StateValueType.Enum)
126-
{
127-
var currentValue = charValues.FirstOrDefault(v => v.AttributeID == attr.ID)?.Value ?? string.Empty;
128-
129-
await TransformEnumAttributeAsync
130-
(
131-
attr,
132-
sessionID,
133-
sceneID,
134-
roundID,
135-
trigger,
136-
currentValue,
137-
charContext,
138-
character.ID,
139-
cancellationToken
140-
);
141-
}
132+
var currentValue = charValues.FirstOrDefault(v => v.AttributeID == attr.ID)?.Value ?? string.Empty;
133+
134+
await TransformEnumAttributeAsync
135+
(
136+
attr,
137+
sessionID,
138+
sceneID,
139+
roundID,
140+
trigger,
141+
currentValue,
142+
charContext,
143+
character.ID,
144+
cancellationToken
145+
);
142146
}
143147
}
144148

@@ -166,7 +170,7 @@ CancellationToken cancellationToken
166170
if (string.IsNullOrEmpty(currentValue))
167171
currentValue = config.Options.FirstOrDefault() ?? string.Empty;
168172

169-
var newValue = ResolveEnumTransition(currentValue, config, stateValues);
173+
var newValue = ResolveEnumTransition(attr, sessionID, characterID, currentValue, config, stateValues);
170174

171175
if (newValue == currentValue)
172176
return;
@@ -200,47 +204,142 @@ await stateRepository.SetStateValueAsync
200204

201205
private string ResolveEnumTransition
202206
(
207+
StateAttribute attr,
208+
long sessionID,
209+
long? characterID,
203210
string currentValue,
204211
EnumAttributeConfig config,
205212
Dictionary<string, string> stateValues
206213
)
207214
{
208-
if (config.Conditions.Count > 0)
215+
if (config.Transitions.Count == 0)
216+
return currentValue;
217+
218+
var alwaysMet = new List<(string Option, float Weight)>();
219+
220+
foreach (var t in config.Transitions)
209221
{
210-
var context = new ConditionContext(stateValues);
222+
if (t.Method != EnumTransitionMethod.Expression || t.SwitchMode != EnumSwitchMode.Always)
223+
continue;
211224

212-
foreach (var cond in config.Conditions)
225+
if (EvaluateTransitionExpression(t, stateValues))
226+
alwaysMet.Add((t.Option, t.Weight));
227+
}
228+
229+
if (alwaysMet.Count > 0)
230+
{
231+
var best = alwaysMet.MaxBy(x => x.Weight);
232+
return best.Option;
233+
}
234+
235+
var onceFirstTime = new List<(string Option, float Weight)>();
236+
237+
foreach (var t in config.Transitions)
238+
{
239+
if (t.Method != EnumTransitionMethod.Expression || t.SwitchMode != EnumSwitchMode.Once)
240+
continue;
241+
242+
var key = (sessionID, attr.ID, characterID, t.Option);
243+
var isMet = EvaluateTransitionExpression(t, stateValues);
244+
245+
if (!isMet)
213246
{
214-
if (conditionEngine.Evaluate(cond.When, context))
215-
return PickWeighted(cond.Transition);
247+
onceTriggered[key] = false;
248+
continue;
216249
}
250+
251+
if (onceTriggered.TryGetValue(key, out var triggered) && triggered)
252+
continue;
253+
254+
onceTriggered[key] = true;
255+
onceFirstTime.Add((t.Option, t.Weight));
217256
}
218257

219-
if (config.TransitionRules.TryGetValue(currentValue, out var rules))
220-
return PickWeighted(rules);
258+
if (onceFirstTime.Count > 0)
259+
{
260+
var best = onceFirstTime.MaxBy(x => x.Weight);
261+
return best.Option;
262+
}
263+
264+
var pool = new List<(string Option, float Weight)>();
265+
266+
foreach (var t in config.Transitions)
267+
{
268+
switch (t.Method)
269+
{
270+
case EnumTransitionMethod.Random:
271+
pool.Add((t.Option, t.Weight));
272+
break;
273+
274+
case EnumTransitionMethod.Expression when t.SwitchMode == EnumSwitchMode.Once:
275+
{
276+
var key = (sessionID, attr.ID, characterID, t.Option);
277+
var isMet = EvaluateTransitionExpression(t, stateValues);
278+
279+
if (isMet && onceTriggered.TryGetValue(key, out var triggered) && triggered)
280+
pool.Add((t.Option, t.Weight));
281+
282+
break;
283+
}
284+
}
285+
}
286+
287+
if (pool.Count > 0)
288+
return PickWeighted(pool);
221289

222290
return currentValue;
223291
}
224292

225-
private static string PickWeighted(Dictionary<string, float> weights)
293+
private bool EvaluateTransitionExpression
294+
(
295+
EnumTransitionConfig transition,
296+
Dictionary<string, string> stateValues
297+
)
226298
{
227-
var total = weights.Values.Sum();
299+
if (string.IsNullOrWhiteSpace(transition.Expression) || string.IsNullOrWhiteSpace(transition.AttributeName))
300+
return false;
301+
302+
if (!stateValues.TryGetValue(transition.AttributeName, out var value))
303+
return false;
304+
305+
var isNumeric = float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out _);
306+
var valReplacement = isNumeric ?
307+
value :
308+
$"\"{value}\"";
309+
310+
var expr = transition.Expression.Replace("{val}", valReplacement);
311+
expr = expr.Replace(" AND ", " && ").Replace(" OR ", " || ");
312+
313+
try
314+
{
315+
return conditionEngine.Evaluate(expr, new ConditionContext(new Dictionary<string, string>()));
316+
}
317+
catch (Exception ex)
318+
{
319+
Log.Warning(ex, "表达式求值失败: {Expression}", transition.Expression);
320+
return false;
321+
}
322+
}
323+
324+
private static string PickWeighted(List<(string Option, float Weight)> pool)
325+
{
326+
var total = pool.Sum(x => x.Weight);
228327

229328
if (total <= 0)
230-
return weights.Keys.FirstOrDefault() ?? string.Empty;
329+
return pool[0].Option;
231330

232331
var roll = (float)Random.Shared.NextDouble() * total;
233332
var cumulative = 0f;
234333

235-
foreach (var (key, weight) in weights)
334+
foreach (var (option, weight) in pool)
236335
{
237336
cumulative += weight;
238337

239338
if (roll <= cumulative)
240-
return key;
339+
return option;
241340
}
242341

243-
return weights.Keys.Last();
342+
return pool[^1].Option;
244343
}
245344

246345
private async Task<Dictionary<string, string>> BuildGlobalStateContextAsync
@@ -265,11 +364,7 @@ private static bool IsTriggerMatch(SystemTrigger configTrigger, SystemTrigger ac
265364
configTrigger == actualTrigger;
266365

267366
private static SystemTrigger ParseTrigger(string? value) =>
268-
value switch
269-
{
270-
"scene_change" => SystemTrigger.SceneChange,
271-
"round_end" => SystemTrigger.RoundEnd,
272-
"custom" => SystemTrigger.Custom,
273-
_ => SystemTrigger.RoundEnd
274-
};
367+
Enum.TryParse(value, true, out SystemTrigger trigger) ?
368+
trigger :
369+
SystemTrigger.RoundEnd;
275370
}

DirectorPrompt.Agents/Tools/CharacterTools.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -620,8 +620,8 @@ string reason
620620
if (attr is null)
621621
return error;
622622

623-
if (attr.Driver == Driver.System)
624-
return JsonSerializer.Serialize(new { error = $"状态属性 {attribute} 为 system 驱动, AI 不可直接修改" });
623+
if (attr.Driver == Driver.System || attr.ValueType == StateValueType.Enum)
624+
return JsonSerializer.Serialize(new { error = $"状态属性 {attribute} 为系统驱动或枚举类型, AI 不可直接修改" });
625625

626626
var values = await characterRepository.GetCharacterStateValuesAsync(character.ID);
627627
var currentValue = values.FirstOrDefault(v => v.AttributeID == attr.ID);
@@ -661,8 +661,8 @@ string reason
661661
if (attr is null)
662662
return error;
663663

664-
if (attr.Driver == Driver.System)
665-
return JsonSerializer.Serialize(new { error = $"状态属性 {attribute} 为 system 驱动, AI 不可直接修改" });
664+
if (attr.Driver == Driver.System || attr.ValueType == StateValueType.Enum)
665+
return JsonSerializer.Serialize(new { error = $"状态属性 {attribute} 为系统驱动或枚举类型, AI 不可直接修改" });
666666

667667
await characterRepository.SetCharacterStateValueAsync(character.ID, attr.ID, value);
668668

0 commit comments

Comments
 (0)