diff --git a/src/web/chat/service.py b/src/web/chat/service.py index 612c66b..90ad8ec 100644 --- a/src/web/chat/service.py +++ b/src/web/chat/service.py @@ -586,8 +586,9 @@ def _build_turn_payload( "Let only characters who are currently present respond; do not force every participant to speak each turn." ) if normalized_message_kind == "narration" and mode == "act" and controlled_character_name: + response_lower_bound = min(response_limit_hint, max(1, min(2, len(active_participants)))) response_count_rule = ( - f"Return {max(2, min(response_limit_hint, len(active_participants)))}-{response_limit_hint} in-world replies " + f"Return {response_lower_bound}-{response_limit_hint} in-world replies " f"when multiple cast members are present. Other participants besides {controlled_character_name} must speak; " "do not return only the controlled character's line." ) @@ -1102,7 +1103,7 @@ def _choose_response_limit_hint(*, mode: str, active_count: int, turn_id: str, m lower = min(upper, 2 if active_count <= 2 else 3) return rng.randint(lower, upper) if message_kind == "narration" and mode in {"act", "insert"}: - upper = min(4, max(2, active_count)) + upper = min(4, max(1, active_count)) lower = 2 if active_count >= 2 else 1 return rng.randint(lower, upper) upper = min(3, max(1, active_count)) diff --git a/src/web/pipeline/automatic_steps.py b/src/web/pipeline/automatic_steps.py index efef73d..882d24d 100644 --- a/src/web/pipeline/automatic_steps.py +++ b/src/web/pipeline/automatic_steps.py @@ -126,6 +126,7 @@ def process_distill_character( payload=character_payload, chunk_count=int(chunk_meta.get("chunk_count", 1) or 1), ) + current_review_fields = read_persona_review_fields(load_profile_source(source_path)) assert_run_not_stopped(manifest_path, current_character=character) on_distill("materializing_character", {"character": character}) @@ -133,10 +134,6 @@ def process_distill_character( source_path, run_dir / "artifacts" / "characters" / novel_id / safe_filename(character), ) - try: - current_review_fields = read_persona_review_fields(load_persona_bundle(materialized["persona_dir"])) - except Exception: - current_review_fields = read_persona_review_fields(load_profile_source(source_path)) change_summary = summarize_redistill_character_change( character=character, previous_fields=previous_review_fields, diff --git a/src/web/static/js/main.js b/src/web/static/js/main.js index 94749a2..0981f2c 100644 --- a/src/web/static/js/main.js +++ b/src/web/static/js/main.js @@ -3184,6 +3184,17 @@ async function handleSendTurn(messageOverride = "", messageKindOverride = "", op window.clearTimeout(retryFeedbackTimer); const errorText = String(error?.message || "").trim() || "这句话暂时没有送达。"; if (sessionSnapshot && !silentOptimistic) { + if (typeof UI_BRIDGE_TOOLS?.syncLegacyUiState === "function") { + UI_BRIDGE_TOOLS.syncLegacyUiState("dialogue-session-restore", { + currentDialogueSessionId, + currentDialogueSession: sessionSnapshot, + }); + } else if (typeof publishLegacyUiState === "function") { + publishLegacyUiState("dialogue-session-restore", { + currentDialogueSessionId, + currentDialogueSession: sessionSnapshot, + }); + } const failedSession = { ...sessionSnapshot, transcript: window.buildFailedSendTranscript(sessionSnapshot, message, messageKind, errorText), diff --git a/tests/test_web_app.py b/tests/test_web_app.py index 747b1c8..d9ca68b 100644 --- a/tests/test_web_app.py +++ b/tests/test_web_app.py @@ -6401,6 +6401,60 @@ def test_prepare_turn_act_narration_prompt_prioritizes_other_cast_over_controlle self.assertEqual(hints[0]["priority"], "normal") self.assertEqual(hints[1]["priority"], "high") + def test_prepare_turn_act_narration_prompt_handles_single_responder(self): + with tempfile.TemporaryDirectory() as tmp: + service = WebRunService(tmp) + service.save_model_settings( + provider="openai-compatible", + model="deepseek-chat", + base_url="https://example.com/v1", + api_key="sk-test", + ) + payload = service.create_run( + novel_name="laoshe.txt", + novel_content_base64=base64.b64encode("祥子娶了虎妞。".encode("utf-8")).decode("ascii"), + characters=["祥子", "虎妞"], + ) + for name in ("祥子", "虎妞"): + service.ingest_character_result( + payload["run_id"], + character=name, + content_base64=base64.b64encode( + f"- name: {name}\n- novel_id: laoshe\n- core_identity: 人物\n".encode("utf-8") + ).decode("ascii"), + ) + + manifest = service._require_manifest(payload["run_id"]) + session = service.dialogue.create_session( + manifest, + mode="act", + participants=["祥子", "虎妞"], + controlled_character="祥子", + ) + raw_session = service.dialogue._read_json( + service.dialogue._session_file(payload["run_id"], session["session_id"]) + ) + service.dialogue._set_session_scene_progress( + raw_session, + { + "present_participants": ["虎妞"], + "offstage_participants": ["祥子"], + }, + ) + turn_payload = service.dialogue._build_turn_payload( + manifest, + raw_session, + turn_id="turn-act-narration-single", + message="第二天一早,虎妞催祥子出门办事。", + message_kind="narration", + speaker_override="场景提示", + ) + response_rule = turn_payload["instructions"]["response_count_rule"] + + self.assertEqual(turn_payload["host_action"]["response_limit_hint"], 1) + self.assertIn("Return 1-1 in-world replies", response_rule) + self.assertNotIn("Return 2-1", response_rule) + def test_reorder_plot_push_responses_moves_controlled_character_before_closing_line(self): payload = { "mode": "act",