@musnows/scriverse 1.0.12 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +3 -0
- package/README.md +3 -0
- package/dist/ai-stream-steer.js +96 -0
- package/dist/ai-stream-steer.js.map +1 -0
- package/dist/ai-write-plans.js +44 -38
- package/dist/ai-write-plans.js.map +1 -1
- package/dist/ai.js +241 -128
- package/dist/ai.js.map +1 -1
- package/dist/app.js +29 -5
- package/dist/app.js.map +1 -1
- package/dist/attachment-storage.js +3 -0
- package/dist/attachment-storage.js.map +1 -1
- package/dist/chapter-annotation-note.js +14 -0
- package/dist/chapter-annotation-note.js.map +1 -0
- package/dist/character-structured-fields.js +7 -0
- package/dist/character-structured-fields.js.map +1 -0
- package/dist/cli-contract.js +1 -1
- package/dist/cli-contract.js.map +1 -1
- package/dist/im.js +2 -1
- package/dist/im.js.map +1 -1
- package/dist/public/ai-prompt-keyboard.js +5 -1
- package/dist/public/ai-prompt-queue.js +226 -0
- package/dist/public/ai-send-mode.js +31 -0
- package/dist/public/app.js +564 -37
- package/dist/public/character-profile.d.ts +7 -0
- package/dist/public/character-profile.js +123 -5
- package/dist/public/index.html +11 -4
- package/dist/public/styles.css +28 -1
- package/dist/server-runtime.js +2 -0
- package/dist/server-runtime.js.map +1 -1
- package/dist/store.js +7 -6
- package/dist/store.js.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +1 -1
package/dist/public/app.js
CHANGED
|
@@ -39,7 +39,18 @@ import {
|
|
|
39
39
|
import { buildVditorLineNumberRows } from "/vditor-line-number-layout.js?v=20260729-vditor-line-numbers-v3";
|
|
40
40
|
import { MIN_MODEL_CONTEXT_WINDOW, MODEL_PURPOSE_OPTIONS, MODEL_THINKING_EFFORT_OPTIONS, isKimiModelId, modelContextWindowGuidance, modelFormValues, modelOptionLabel, modelPayload, modelThinkingEffortLabel, supportsMultimodalModelProtocol } from "/model-config.js?v=20260822-ai-model-thinking-label-v3&feature=ai-provider-responses-v1&feature=semantic-search-v6";
|
|
41
41
|
import { connectivityConfigurationSavedToast, connectivityTestErrorToast, connectivityTestResultToast } from "/ai-connectivity-test.js?v=20260822-private-ai-endpoint-hint-v1";
|
|
42
|
-
import { shouldSendAiPrompt } from "/ai-prompt-keyboard.js?v=
|
|
42
|
+
import { shouldActivateAiSendControl, shouldSendAiPrompt } from "/ai-prompt-keyboard.js?v=20260919-ai-send-mode-v2";
|
|
43
|
+
import { aiSendModeAction, normalizeAiSendMode, readStoredAiSendMode, writeStoredAiSendMode } from "/ai-send-mode.js?v=20260919-ai-send-mode-v2";
|
|
44
|
+
import {
|
|
45
|
+
aiPromptQueueDragPayload,
|
|
46
|
+
aiPromptQueuePreview,
|
|
47
|
+
canSendQueuedPromptAsSteer,
|
|
48
|
+
createAiPromptQueue,
|
|
49
|
+
parseAiPromptQueueDragPayload,
|
|
50
|
+
queuedPromptEditContent,
|
|
51
|
+
queuedPromptEditPatch,
|
|
52
|
+
queuedPromptSteerContent
|
|
53
|
+
} from "/ai-prompt-queue.js?v=20260919-ai-prompt-queue-v2";
|
|
43
54
|
import { estimateAiMessageTokens, formatAiMessageMeta } from "/ai-message-meta.js?v=20260814-ai-model-lock-v1";
|
|
44
55
|
import { createStreamTypewriter, createStreamTypewriterSpeedController } from "/stream-typewriter.js?v=20260912-stream-render-v2";
|
|
45
56
|
import { assertAiStreamCompleted, readAiEventStream } from "/ai-stream-protocol.js?v=20260812-ai-stream-complete-v1";
|
|
@@ -69,7 +80,7 @@ import { AI_CHAT_IMAGE_ATTACHMENT_MAX_COUNT, aiChatImageAttachmentIds, isAiChatI
|
|
|
69
80
|
import { findTextMatches, replaceTextMatches } from "/chapter-search.js?v=20260818-chapter-search-replace-v1";
|
|
70
81
|
import { MAX_CHAPTER_LINE_IDS, normalizeChapterLineIdDraft, reconcileChapterLineIdDraft, remapChapterLineCounts } from "/chapter-line-id-tracker.js?v=20260829-live-annotation-anchors-v1";
|
|
71
82
|
import { THEME_STORAGE_KEY, nextTheme, normalizeTheme, themeToggleLabel } from "/theme.js?v=20260713-dark-mode";
|
|
72
|
-
import { buildCharacterDetails, buildCharacterState, characterStateEntries, normalizeCharacterDetails, normalizeCharacterSections } from "/character-profile.js?v=
|
|
83
|
+
import { buildCharacterDetails, buildCharacterState, characterStateEntries, normalizeCharacterAttributes, normalizeCharacterDetails, normalizeCharacterSections } from "/character-profile.js?v=20260919-character-attributes-prune-v1";
|
|
73
84
|
import { characterVersionSourceLabel, describeCharacterVersionChanges } from "/character-version.js?v=20260816-character-gender-v1";
|
|
74
85
|
import { chapterDiffSummary, diffChapterLines } from "/chapter-version-diff.js?v=20260812-chapter-version-diff-v1";
|
|
75
86
|
import { VERSIONED_ENTITY_LABELS, entityVersionSnapshotSummary, entityVersionSourceLabel } from "/entity-version.js?v=20260809-global-replace-v1";
|
|
@@ -251,6 +262,11 @@ const state = {
|
|
|
251
262
|
let platformAiProtocolOptions = [];
|
|
252
263
|
|
|
253
264
|
const aiRequestManager = createAiRequestManager();
|
|
265
|
+
const aiPromptQueue = createAiPromptQueue();
|
|
266
|
+
let pendingQueuedComposer = null;
|
|
267
|
+
let aiPromptQueueDidDrag = false;
|
|
268
|
+
let aiPromptQueueEditorState = null;
|
|
269
|
+
let aiPromptQueueEditorBlurTimer = 0;
|
|
254
270
|
const aiChatTabManager = createAiChatTabManager(() => createAiIdempotencyKey());
|
|
255
271
|
const moduleRequestCache = createModuleRequestCache();
|
|
256
272
|
const cachedWorkModules = new Set([
|
|
@@ -533,6 +549,7 @@ function applyWorkAccessMode() {
|
|
|
533
549
|
state.dirty = false;
|
|
534
550
|
}
|
|
535
551
|
applyChapterEditorMode();
|
|
552
|
+
syncAiSendModeControl();
|
|
536
553
|
}
|
|
537
554
|
|
|
538
555
|
const $ = (selector) => document.querySelector(selector);
|
|
@@ -674,28 +691,77 @@ function aiInteractionBusy() {
|
|
|
674
691
|
}
|
|
675
692
|
|
|
676
693
|
function aiSendButtonIconMarkup(stateName) {
|
|
694
|
+
if (stateName === "queue") {
|
|
695
|
+
return '<svg class="ai-send-button-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M5 7h11M5 12h8M5 17h11"></path><path d="M16 10v7M12.5 13.5H19.5"></path></svg>';
|
|
696
|
+
}
|
|
677
697
|
return stateName === "stop"
|
|
678
698
|
? '<svg class="ai-send-button-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><rect x="6" y="6" width="12" height="12" rx="1.5"></rect></svg>'
|
|
679
699
|
: '<svg class="ai-send-button-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M21 3 9.5 14.5M21 3l-7 18-4.5-6.5L3 10l18-7Z"></path></svg>';
|
|
680
700
|
}
|
|
681
701
|
|
|
702
|
+
function applyAiSendModeFromStorage() {
|
|
703
|
+
const select = $("#ai-send-mode");
|
|
704
|
+
if (!select) return;
|
|
705
|
+
select.value = readStoredAiSendMode(localStorage);
|
|
706
|
+
syncAiSendModeControl();
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function aiComposerSendMode() {
|
|
710
|
+
return normalizeAiSendMode($("#ai-send-mode")?.value);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function syncAiSendModeControl() {
|
|
714
|
+
const select = $("#ai-send-mode");
|
|
715
|
+
if (!select) return;
|
|
716
|
+
const mode = normalizeAiSendMode(select.value);
|
|
717
|
+
if (select.value !== mode) select.value = mode;
|
|
718
|
+
const aiReadOnly = Boolean(state.work) && !canWritePermissionModule(state.work, "ai-chat");
|
|
719
|
+
select.disabled = aiReadOnly;
|
|
720
|
+
select.title = mode === "steer"
|
|
721
|
+
? "引导发送:生成中再发送会立即作用于当前执行流,不会打断本轮。空输入时发送键用于终止"
|
|
722
|
+
: "排队发送:生成中再发送会加入队列,当前回复结束后按顺序发送,不会打断正在执行的回复。空输入时发送键用于终止";
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function aiComposerHasSendablePrompt() {
|
|
726
|
+
return Boolean(queuedPromptSteerContent(captureAiPromptComposer()));
|
|
727
|
+
}
|
|
728
|
+
|
|
682
729
|
function syncAiRequestControls() {
|
|
683
730
|
const activeTabId = activeAiChatTab()?.id;
|
|
684
731
|
const continuingQuestion = activeTabId ? aiQuestionContinuationTabIds.has(activeTabId) : false;
|
|
685
732
|
const sending = aiRequestManager.hasActive(activeTabId) && !continuingQuestion;
|
|
686
733
|
const switching = aiConversationNavigationPending !== null;
|
|
687
734
|
const button = $("#ai-send");
|
|
688
|
-
const
|
|
689
|
-
const
|
|
735
|
+
const sendAction = aiSendModeAction(aiComposerSendMode(), sending, aiComposerHasSendablePrompt());
|
|
736
|
+
const stateName = sendAction === "queue" || sendAction === "steer" || sendAction === "stop"
|
|
737
|
+
? sendAction
|
|
738
|
+
: (switching || continuingQuestion) ? "switching" : "send";
|
|
739
|
+
const label = sendAction === "stop"
|
|
740
|
+
? "终止当前回复"
|
|
741
|
+
: sendAction === "queue"
|
|
742
|
+
? "排队发送"
|
|
743
|
+
: sendAction === "steer" ? "引导发送"
|
|
744
|
+
: continuingQuestion ? "AI 正在根据回答继续处理"
|
|
745
|
+
: switching ? "正在切换对话" : "发送消息";
|
|
690
746
|
button.disabled = switching || continuingQuestion;
|
|
691
747
|
button.dataset.state = stateName;
|
|
692
|
-
button.classList.toggle("is-stop",
|
|
748
|
+
button.classList.toggle("is-stop", sendAction === "stop");
|
|
749
|
+
button.classList.toggle("is-queue", sendAction === "queue");
|
|
693
750
|
button.setAttribute("aria-label", label);
|
|
694
|
-
button.title =
|
|
695
|
-
|
|
751
|
+
button.title = sendAction === "stop"
|
|
752
|
+
? "终止当前回复,排队 Prompt 仍会保留"
|
|
753
|
+
: sendAction === "queue"
|
|
754
|
+
? "排队发送,当前回复结束后按顺序发送,不会打断正在执行的回复"
|
|
755
|
+
: sendAction === "steer"
|
|
756
|
+
? "引导发送,立即作用于当前执行流,不会打断本轮"
|
|
757
|
+
: label;
|
|
758
|
+
button.innerHTML = aiSendButtonIconMarkup(stateName === "steer" ? "send" : stateName);
|
|
759
|
+
$(".prompt-composer")?.classList.toggle("is-streaming", sending);
|
|
760
|
+
syncAiSendModeControl();
|
|
696
761
|
syncAiTaskOptions();
|
|
697
762
|
renderAiRoleplayCharacterSelect();
|
|
698
763
|
syncAiImageAttachmentControl();
|
|
764
|
+
renderAiPromptQueue();
|
|
699
765
|
}
|
|
700
766
|
|
|
701
767
|
function cancelActiveAiRequest(reason) {
|
|
@@ -704,18 +770,388 @@ function cancelActiveAiRequest(reason) {
|
|
|
704
770
|
return cancelled;
|
|
705
771
|
}
|
|
706
772
|
|
|
707
|
-
function
|
|
773
|
+
function submitAiComposerPrompt() {
|
|
708
774
|
const tab = activeAiChatTab();
|
|
709
775
|
if (aiRequestManager.hasActive(tab?.id)) {
|
|
710
|
-
if (
|
|
711
|
-
|
|
712
|
-
|
|
776
|
+
if (aiComposerSendMode() === "steer") {
|
|
777
|
+
void sendActiveComposerAsSteer();
|
|
778
|
+
return;
|
|
713
779
|
}
|
|
780
|
+
queueActiveComposerPrompt();
|
|
714
781
|
return;
|
|
715
782
|
}
|
|
716
783
|
void sendAi();
|
|
717
784
|
}
|
|
718
785
|
|
|
786
|
+
function activateAiSendControl() {
|
|
787
|
+
const tab = activeAiChatTab();
|
|
788
|
+
if (aiRequestManager.hasActive(tab?.id) && !aiComposerHasSendablePrompt()) {
|
|
789
|
+
activateAiStopControl();
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
submitAiComposerPrompt();
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function activateAiStopControl() {
|
|
796
|
+
if (cancelActiveAiRequest("用户已终止当前回复")) {
|
|
797
|
+
toast("已终止当前回复,排队 Prompt 仍会保留");
|
|
798
|
+
$("#ai-prompt").focus();
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function activeAiConversationId(tab = activeAiChatTab()) {
|
|
803
|
+
return String(tab?.conversationId ?? state.aiConversationId ?? "").trim();
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function queueActiveComposerPrompt() {
|
|
807
|
+
const tab = activeAiChatTab();
|
|
808
|
+
if (!tab) return toast("Agent 对话页签尚未就绪", "error");
|
|
809
|
+
const snapshot = captureAiPromptComposer();
|
|
810
|
+
try {
|
|
811
|
+
aiPromptQueue.enqueue(tab.id, snapshot);
|
|
812
|
+
} catch (error) {
|
|
813
|
+
return toast(error.message, "error");
|
|
814
|
+
}
|
|
815
|
+
clearAiPromptComposer({ collapseScenePanel: Boolean(snapshot.sceneDirection) });
|
|
816
|
+
persistActiveAiChatTab();
|
|
817
|
+
syncAiRequestControls();
|
|
818
|
+
toast("已加入排队,当前回复结束后发送");
|
|
819
|
+
$("#ai-prompt").focus();
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
async function postAiConversationSteer(conversationId, content) {
|
|
823
|
+
const id = String(conversationId ?? "").trim();
|
|
824
|
+
if (!id) throw new Error("当前对话尚未开始,无法发送引导");
|
|
825
|
+
return api(`/api/ai-conversations/${encodeURIComponent(id)}/steer`, {
|
|
826
|
+
method: "POST",
|
|
827
|
+
body: { content }
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
async function sendActiveComposerAsSteer() {
|
|
832
|
+
const tab = activeAiChatTab();
|
|
833
|
+
if (!tab) return toast("Agent 对话页签尚未就绪", "error");
|
|
834
|
+
if (!aiRequestManager.hasActive(tab.id)) return toast("当前没有正在执行的回复,无法发送引导", "error");
|
|
835
|
+
const snapshot = captureAiPromptComposer();
|
|
836
|
+
const content = queuedPromptSteerContent(snapshot);
|
|
837
|
+
if (!content) {
|
|
838
|
+
return toast($("#ai-task").value === "roleplay" ? "请输入台词或场景旁白" : "请输入引导内容", "error");
|
|
839
|
+
}
|
|
840
|
+
try {
|
|
841
|
+
await postAiConversationSteer(activeAiConversationId(tab), content);
|
|
842
|
+
} catch (error) {
|
|
843
|
+
return toast(error.message, "error");
|
|
844
|
+
}
|
|
845
|
+
clearAiPromptComposer({ collapseScenePanel: Boolean(snapshot.sceneDirection) });
|
|
846
|
+
syncAiRequestControls();
|
|
847
|
+
toast("已发送执行流引导");
|
|
848
|
+
$("#ai-prompt").focus();
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
async function sendQueuedPromptAsSteer(itemId) {
|
|
852
|
+
const tab = activeAiChatTab();
|
|
853
|
+
if (!tab) return;
|
|
854
|
+
const item = aiPromptQueue.list(tab.id).find((entry) => entry.id === itemId);
|
|
855
|
+
if (!canSendQueuedPromptAsSteer(item, aiRequestManager.hasActive(tab.id))) {
|
|
856
|
+
return toast("当前没有正在执行的回复,无法发送引导", "error");
|
|
857
|
+
}
|
|
858
|
+
try {
|
|
859
|
+
await postAiConversationSteer(activeAiConversationId(tab), queuedPromptSteerContent(item));
|
|
860
|
+
} catch (error) {
|
|
861
|
+
return toast(error.message, "error");
|
|
862
|
+
}
|
|
863
|
+
aiPromptQueue.remove(tab.id, itemId);
|
|
864
|
+
renderAiPromptQueue();
|
|
865
|
+
toast("已将排队 Prompt 立即引导");
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
function sendQueuedPromptNow(itemId) {
|
|
869
|
+
const tab = activeAiChatTab();
|
|
870
|
+
if (!tab || aiRequestManager.hasActive(tab.id)) return;
|
|
871
|
+
const item = aiPromptQueue.list(tab.id).find((entry) => entry.id === itemId);
|
|
872
|
+
if (!item) return;
|
|
873
|
+
pendingQueuedComposer = item;
|
|
874
|
+
void sendAiWithOptions();
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
function flushNextQueuedPrompt(tab) {
|
|
878
|
+
if (!tab || !isActiveAiChatTab(tab) || aiRequestManager.hasActive(tab.id) || aiQuestionContinuationTabIds.has(tab.id)) {
|
|
879
|
+
renderAiPromptQueue();
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
const next = aiPromptQueue.list(tab.id)[0];
|
|
883
|
+
renderAiPromptQueue();
|
|
884
|
+
if (!next) return;
|
|
885
|
+
pendingQueuedComposer = next;
|
|
886
|
+
void sendAiWithOptions();
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
function clearAiPromptQueueEditorState(itemId = null) {
|
|
890
|
+
if (!itemId || aiPromptQueueEditorState?.itemId === itemId) aiPromptQueueEditorState = null;
|
|
891
|
+
if (aiPromptQueueEditorBlurTimer) {
|
|
892
|
+
window.clearTimeout(aiPromptQueueEditorBlurTimer);
|
|
893
|
+
aiPromptQueueEditorBlurTimer = 0;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function captureAiPromptQueueEditorState() {
|
|
898
|
+
if (!aiPromptQueueEditorState) return null;
|
|
899
|
+
const editor = document.querySelector("#ai-prompt-queue-list .ai-prompt-queue-editor");
|
|
900
|
+
if (!(editor instanceof HTMLTextAreaElement)) return aiPromptQueueEditorState;
|
|
901
|
+
const itemId = editor.closest("[data-queue-id]")?.dataset.queueId;
|
|
902
|
+
if (!itemId || itemId !== aiPromptQueueEditorState.itemId) return aiPromptQueueEditorState;
|
|
903
|
+
return {
|
|
904
|
+
itemId,
|
|
905
|
+
value: editor.value,
|
|
906
|
+
selectionStart: editor.selectionStart,
|
|
907
|
+
selectionEnd: editor.selectionEnd
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
function commitAiPromptQueueEdit(itemId, nextValue = null, { render = true } = {}) {
|
|
912
|
+
const tab = activeAiChatTab();
|
|
913
|
+
if (!tab || !itemId) return false;
|
|
914
|
+
const item = aiPromptQueue.list(tab.id).find((entry) => entry.id === itemId);
|
|
915
|
+
if (!item) {
|
|
916
|
+
clearAiPromptQueueEditorState(itemId);
|
|
917
|
+
return false;
|
|
918
|
+
}
|
|
919
|
+
const value = nextValue == null ? queuedPromptEditContent(item) : nextValue;
|
|
920
|
+
if (value === queuedPromptEditContent(item)) {
|
|
921
|
+
clearAiPromptQueueEditorState(itemId);
|
|
922
|
+
if (render) renderAiPromptQueue();
|
|
923
|
+
return true;
|
|
924
|
+
}
|
|
925
|
+
try {
|
|
926
|
+
aiPromptQueue.update(tab.id, itemId, queuedPromptEditPatch(item, value));
|
|
927
|
+
} catch (error) {
|
|
928
|
+
toast(error.message, "error");
|
|
929
|
+
return false;
|
|
930
|
+
}
|
|
931
|
+
clearAiPromptQueueEditorState(itemId);
|
|
932
|
+
if (render) renderAiPromptQueue();
|
|
933
|
+
return true;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
function beginAiPromptQueueEdit(item) {
|
|
937
|
+
const tab = activeAiChatTab();
|
|
938
|
+
if (!tab || !item || (state.work && !canWritePermissionModule(state.work, "ai-chat"))) return;
|
|
939
|
+
if (aiPromptQueueEditorState && aiPromptQueueEditorState.itemId !== item.id) {
|
|
940
|
+
const previous = document.querySelector("#ai-prompt-queue-list .ai-prompt-queue-editor");
|
|
941
|
+
const previousValue = previous instanceof HTMLTextAreaElement ? previous.value : aiPromptQueueEditorState.value;
|
|
942
|
+
if (!commitAiPromptQueueEdit(aiPromptQueueEditorState.itemId, previousValue)) return;
|
|
943
|
+
}
|
|
944
|
+
aiPromptQueueEditorState = {
|
|
945
|
+
itemId: item.id,
|
|
946
|
+
value: queuedPromptEditContent(item),
|
|
947
|
+
selectionStart: 0,
|
|
948
|
+
selectionEnd: queuedPromptEditContent(item).length
|
|
949
|
+
};
|
|
950
|
+
renderAiPromptQueue();
|
|
951
|
+
const editor = document.querySelector(`#ai-prompt-queue-list [data-queue-id="${CSS.escape(item.id)}"] .ai-prompt-queue-editor`);
|
|
952
|
+
if (editor instanceof HTMLTextAreaElement) {
|
|
953
|
+
editor.focus();
|
|
954
|
+
editor.setSelectionRange(0, editor.value.length);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
function moveAiPromptQueueItem(sourceId, targetId, placeAfter) {
|
|
959
|
+
const tab = activeAiChatTab();
|
|
960
|
+
if (!tab || !sourceId || !targetId) return;
|
|
961
|
+
aiPromptQueue.move(tab.id, sourceId, targetId, placeAfter);
|
|
962
|
+
renderAiPromptQueue();
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
function moveAiPromptQueueItemByOffset(itemId, offset) {
|
|
966
|
+
const tab = activeAiChatTab();
|
|
967
|
+
if (!tab || !itemId) return;
|
|
968
|
+
aiPromptQueue.moveByOffset(tab.id, itemId, offset);
|
|
969
|
+
renderAiPromptQueue();
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
function bindAiPromptQueueItemDrag(row, item, aiReadOnly, editing) {
|
|
973
|
+
if (aiReadOnly || editing) {
|
|
974
|
+
row.draggable = false;
|
|
975
|
+
row.removeAttribute("aria-grabbed");
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
row.draggable = true;
|
|
979
|
+
row.setAttribute("aria-grabbed", "false");
|
|
980
|
+
row.addEventListener("dragstart", (event) => {
|
|
981
|
+
if (event.target.closest("button, textarea, .ai-prompt-queue-editor")) {
|
|
982
|
+
event.preventDefault();
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
aiPromptQueueDidDrag = true;
|
|
986
|
+
event.dataTransfer?.setData("text/plain", aiPromptQueueDragPayload(item.id));
|
|
987
|
+
if (event.dataTransfer) event.dataTransfer.effectAllowed = "move";
|
|
988
|
+
row.classList.add("is-dragging");
|
|
989
|
+
row.setAttribute("aria-grabbed", "true");
|
|
990
|
+
});
|
|
991
|
+
row.addEventListener("dragend", () => {
|
|
992
|
+
row.classList.remove("is-dragging");
|
|
993
|
+
row.setAttribute("aria-grabbed", "false");
|
|
994
|
+
listAiPromptQueueDropTargets().forEach((node) => node.classList.remove("is-drag-over", "drop-after"));
|
|
995
|
+
window.setTimeout(() => { aiPromptQueueDidDrag = false; }, 0);
|
|
996
|
+
});
|
|
997
|
+
row.addEventListener("dragover", (event) => {
|
|
998
|
+
if (!event.dataTransfer?.types.includes("text/plain")) return;
|
|
999
|
+
event.preventDefault();
|
|
1000
|
+
if (event.dataTransfer) event.dataTransfer.dropEffect = "move";
|
|
1001
|
+
const after = event.clientY >= row.getBoundingClientRect().top + row.offsetHeight / 2;
|
|
1002
|
+
listAiPromptQueueDropTargets().forEach((node) => {
|
|
1003
|
+
if (node !== row) node.classList.remove("is-drag-over", "drop-after");
|
|
1004
|
+
});
|
|
1005
|
+
row.classList.toggle("drop-after", after);
|
|
1006
|
+
row.classList.add("is-drag-over");
|
|
1007
|
+
});
|
|
1008
|
+
row.addEventListener("dragleave", (event) => {
|
|
1009
|
+
if (row.contains(event.relatedTarget)) return;
|
|
1010
|
+
row.classList.remove("is-drag-over", "drop-after");
|
|
1011
|
+
});
|
|
1012
|
+
row.addEventListener("drop", (event) => {
|
|
1013
|
+
event.preventDefault();
|
|
1014
|
+
event.stopPropagation();
|
|
1015
|
+
const sourceId = parseAiPromptQueueDragPayload(event.dataTransfer?.getData("text/plain"));
|
|
1016
|
+
const after = row.classList.contains("drop-after");
|
|
1017
|
+
row.classList.remove("is-drag-over", "drop-after");
|
|
1018
|
+
if (!sourceId || sourceId === item.id) return;
|
|
1019
|
+
moveAiPromptQueueItem(sourceId, item.id, after);
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
function listAiPromptQueueDropTargets() {
|
|
1024
|
+
return [...document.querySelectorAll("#ai-prompt-queue-list .ai-prompt-queue-item")];
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
function renderAiPromptQueue() {
|
|
1028
|
+
const tab = activeAiChatTab();
|
|
1029
|
+
const panel = $("#ai-prompt-queue");
|
|
1030
|
+
const list = $("#ai-prompt-queue-list");
|
|
1031
|
+
if (!panel || !list) return;
|
|
1032
|
+
const items = tab ? aiPromptQueue.list(tab.id) : [];
|
|
1033
|
+
const streaming = Boolean(tab && aiRequestManager.hasActive(tab.id));
|
|
1034
|
+
const aiReadOnly = Boolean(state.work) && !canWritePermissionModule(state.work, "ai-chat");
|
|
1035
|
+
const editorState = captureAiPromptQueueEditorState();
|
|
1036
|
+
if (editorState && !items.some((item) => item.id === editorState.itemId)) clearAiPromptQueueEditorState(editorState.itemId);
|
|
1037
|
+
const activeEditor = items.some((item) => item.id === (editorState?.itemId ?? aiPromptQueueEditorState?.itemId))
|
|
1038
|
+
? editorState ?? aiPromptQueueEditorState
|
|
1039
|
+
: null;
|
|
1040
|
+
if (activeEditor) aiPromptQueueEditorState = activeEditor;
|
|
1041
|
+
else if (!items.length) clearAiPromptQueueEditorState();
|
|
1042
|
+
panel.classList.toggle("hidden", items.length === 0);
|
|
1043
|
+
list.replaceChildren(...items.map((item) => {
|
|
1044
|
+
const editing = Boolean(!aiReadOnly && activeEditor?.itemId === item.id);
|
|
1045
|
+
const row = document.createElement("li");
|
|
1046
|
+
row.className = `ai-prompt-queue-item${editing ? " is-editing" : ""}`;
|
|
1047
|
+
row.dataset.queueId = item.id;
|
|
1048
|
+
row.title = aiReadOnly ? "" : "拖拽调整顺序;点击文字可编辑";
|
|
1049
|
+
const previewText = aiPromptQueuePreview(queuedPromptSteerContent(item) || item.text);
|
|
1050
|
+
const fullText = queuedPromptSteerContent(item) || item.text;
|
|
1051
|
+
let preview;
|
|
1052
|
+
if (editing) {
|
|
1053
|
+
preview = document.createElement("textarea");
|
|
1054
|
+
preview.className = "ai-prompt-queue-editor";
|
|
1055
|
+
preview.value = activeEditor.value ?? queuedPromptEditContent(item);
|
|
1056
|
+
preview.rows = Math.min(6, Math.max(1, preview.value.split("\n").length));
|
|
1057
|
+
preview.setAttribute("aria-label", `编辑排队 Prompt:${previewText}`);
|
|
1058
|
+
preview.addEventListener("click", (event) => event.stopPropagation());
|
|
1059
|
+
preview.addEventListener("pointerdown", (event) => event.stopPropagation());
|
|
1060
|
+
preview.addEventListener("keydown", (event) => {
|
|
1061
|
+
event.stopPropagation();
|
|
1062
|
+
if (event.key === "Escape") {
|
|
1063
|
+
event.preventDefault();
|
|
1064
|
+
clearAiPromptQueueEditorState(item.id);
|
|
1065
|
+
renderAiPromptQueue();
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
if (shouldSendAiPrompt(event)) {
|
|
1069
|
+
event.preventDefault();
|
|
1070
|
+
commitAiPromptQueueEdit(item.id, preview.value);
|
|
1071
|
+
}
|
|
1072
|
+
});
|
|
1073
|
+
preview.addEventListener("blur", () => {
|
|
1074
|
+
aiPromptQueueEditorBlurTimer = window.setTimeout(() => {
|
|
1075
|
+
aiPromptQueueEditorBlurTimer = 0;
|
|
1076
|
+
if (aiPromptQueueEditorState?.itemId !== item.id) return;
|
|
1077
|
+
if (document.activeElement?.classList.contains("ai-prompt-queue-editor")) return;
|
|
1078
|
+
commitAiPromptQueueEdit(item.id, preview.value);
|
|
1079
|
+
}, 0);
|
|
1080
|
+
});
|
|
1081
|
+
} else {
|
|
1082
|
+
preview = document.createElement("p");
|
|
1083
|
+
preview.className = "ai-prompt-queue-preview";
|
|
1084
|
+
preview.textContent = previewText;
|
|
1085
|
+
preview.title = aiReadOnly ? fullText : `${fullText}\n点击编辑`;
|
|
1086
|
+
preview.tabIndex = aiReadOnly ? -1 : 0;
|
|
1087
|
+
preview.setAttribute("role", "button");
|
|
1088
|
+
preview.setAttribute("aria-label", `编辑排队 Prompt:${previewText}`);
|
|
1089
|
+
const startEdit = (event) => {
|
|
1090
|
+
if (aiReadOnly || aiPromptQueueDidDrag) return;
|
|
1091
|
+
event.preventDefault();
|
|
1092
|
+
beginAiPromptQueueEdit(item);
|
|
1093
|
+
};
|
|
1094
|
+
preview.addEventListener("click", startEdit);
|
|
1095
|
+
preview.addEventListener("keydown", (event) => {
|
|
1096
|
+
if (event.altKey && ["ArrowUp", "ArrowDown"].includes(event.key)) {
|
|
1097
|
+
event.preventDefault();
|
|
1098
|
+
moveAiPromptQueueItemByOffset(item.id, event.key === "ArrowDown" ? 1 : -1);
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
1102
|
+
startEdit(event);
|
|
1103
|
+
}
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
const actions = document.createElement("div");
|
|
1107
|
+
actions.className = "ai-prompt-queue-actions";
|
|
1108
|
+
const primary = document.createElement("button");
|
|
1109
|
+
primary.type = "button";
|
|
1110
|
+
const flushEditorBeforeAction = () => {
|
|
1111
|
+
if (!(preview instanceof HTMLTextAreaElement)) return;
|
|
1112
|
+
commitAiPromptQueueEdit(item.id, preview.value, { render: false });
|
|
1113
|
+
};
|
|
1114
|
+
if (streaming) {
|
|
1115
|
+
primary.className = "ai-prompt-queue-steer";
|
|
1116
|
+
primary.textContent = "立即引导";
|
|
1117
|
+
primary.disabled = aiReadOnly || !canSendQueuedPromptAsSteer(item, true);
|
|
1118
|
+
primary.setAttribute("aria-label", `立即引导排队 Prompt:${previewText}`);
|
|
1119
|
+
primary.addEventListener("pointerdown", flushEditorBeforeAction);
|
|
1120
|
+
primary.addEventListener("click", () => { void sendQueuedPromptAsSteer(item.id); });
|
|
1121
|
+
} else {
|
|
1122
|
+
primary.className = "ai-prompt-queue-send";
|
|
1123
|
+
primary.textContent = "现在发送";
|
|
1124
|
+
primary.disabled = aiReadOnly;
|
|
1125
|
+
primary.setAttribute("aria-label", `现在发送排队 Prompt:${previewText}`);
|
|
1126
|
+
primary.addEventListener("pointerdown", flushEditorBeforeAction);
|
|
1127
|
+
primary.addEventListener("click", () => sendQueuedPromptNow(item.id));
|
|
1128
|
+
}
|
|
1129
|
+
const remove = document.createElement("button");
|
|
1130
|
+
remove.type = "button";
|
|
1131
|
+
remove.className = "ai-prompt-queue-remove";
|
|
1132
|
+
remove.setAttribute("aria-label", `移除排队 Prompt:${previewText}`);
|
|
1133
|
+
remove.textContent = "×";
|
|
1134
|
+
remove.addEventListener("click", () => {
|
|
1135
|
+
aiPromptQueue.remove(tab.id, item.id);
|
|
1136
|
+
clearAiPromptQueueEditorState(item.id);
|
|
1137
|
+
renderAiPromptQueue();
|
|
1138
|
+
});
|
|
1139
|
+
actions.append(primary, remove);
|
|
1140
|
+
row.append(preview, actions);
|
|
1141
|
+
bindAiPromptQueueItemDrag(row, item, aiReadOnly, editing);
|
|
1142
|
+
return row;
|
|
1143
|
+
}));
|
|
1144
|
+
if (activeEditor) {
|
|
1145
|
+
const editor = list.querySelector(`[data-queue-id="${CSS.escape(activeEditor.itemId)}"] .ai-prompt-queue-editor`);
|
|
1146
|
+
if (editor instanceof HTMLTextAreaElement) {
|
|
1147
|
+
const start = Number.isInteger(activeEditor.selectionStart) ? activeEditor.selectionStart : editor.value.length;
|
|
1148
|
+
const end = Number.isInteger(activeEditor.selectionEnd) ? activeEditor.selectionEnd : editor.value.length;
|
|
1149
|
+
editor.focus();
|
|
1150
|
+
editor.setSelectionRange(start, end);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
|
|
719
1155
|
function beginAiConversationNavigation(reason, action = "切换会话") {
|
|
720
1156
|
aiConversationNavigationGeneration += 1;
|
|
721
1157
|
aiConversationNavigationPending = aiConversationNavigationGeneration;
|
|
@@ -836,6 +1272,7 @@ let chapterSelectionRequestGeneration = 0;
|
|
|
836
1272
|
let aiConversationNavigationGeneration = 0;
|
|
837
1273
|
let aiConversationNavigationPending = null;
|
|
838
1274
|
let aiChatTabLimit = 5;
|
|
1275
|
+
let chapterAnnotationNoteMaxLength = 6000;
|
|
839
1276
|
let aiConversationWorkspaceOpen = false;
|
|
840
1277
|
const loadedVolumeChapterIds = new Set();
|
|
841
1278
|
const volumeChapterLoadingIds = new Set();
|
|
@@ -2792,6 +3229,12 @@ function applyAiChatTabLimit(value) {
|
|
|
2792
3229
|
renderAiChatTabs();
|
|
2793
3230
|
}
|
|
2794
3231
|
|
|
3232
|
+
function applyChapterAnnotationNoteMaxLength(value) {
|
|
3233
|
+
const parsed = Number(value);
|
|
3234
|
+
if (!Number.isSafeInteger(parsed)) return;
|
|
3235
|
+
chapterAnnotationNoteMaxLength = Math.min(20000, Math.max(2000, parsed));
|
|
3236
|
+
}
|
|
3237
|
+
|
|
2795
3238
|
function renderAiChatTabs() {
|
|
2796
3239
|
const host = $("#ai-chat-tabs");
|
|
2797
3240
|
if (!host) return;
|
|
@@ -5647,7 +6090,7 @@ async function createSelectedLineAnnotation(kind) {
|
|
|
5647
6090
|
title: kind === "todo" ? "添加正文待办" : "添加正文评论",
|
|
5648
6091
|
inputLabel: kind === "todo" ? "待办内容" : "评论内容",
|
|
5649
6092
|
confirmLabel: "添加",
|
|
5650
|
-
maxLength:
|
|
6093
|
+
maxLength: chapterAnnotationNoteMaxLength
|
|
5651
6094
|
});
|
|
5652
6095
|
if (!note) return;
|
|
5653
6096
|
try {
|
|
@@ -5716,7 +6159,7 @@ function bindChapterAnnotationCards(host, annotations, { refresh, locate, overla
|
|
|
5716
6159
|
inputLabel: annotation.kind === "todo" ? "待办内容" : "评论内容",
|
|
5717
6160
|
value: annotation.note,
|
|
5718
6161
|
confirmLabel: "保存",
|
|
5719
|
-
maxLength:
|
|
6162
|
+
maxLength: chapterAnnotationNoteMaxLength
|
|
5720
6163
|
});
|
|
5721
6164
|
if (!note) return resumeOverlay();
|
|
5722
6165
|
try {
|
|
@@ -6084,6 +6527,7 @@ async function api(path, options = {}) {
|
|
|
6084
6527
|
if (path === "/api/health") {
|
|
6085
6528
|
applyImageUploadLimits(payload.data?.uploadLimits);
|
|
6086
6529
|
applyAiChatTabLimit(payload.data?.aiChatTabLimit);
|
|
6530
|
+
applyChapterAnnotationNoteMaxLength(payload.data?.chapterAnnotationNoteMaxLength);
|
|
6087
6531
|
updateSystemHealth({
|
|
6088
6532
|
status: payload.data?.status === "ok" ? "ready" : "degraded",
|
|
6089
6533
|
version: payload.data?.version
|
|
@@ -11352,11 +11796,12 @@ async function renderCharacters(page = characterListPage) {
|
|
|
11352
11796
|
? `<span class="character-lock-badge" aria-label="${item.lockedFields.length} 个锁定字段" title="锁定字段:${esc(item.lockedFields.join("、"))}"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="5" y="10" width="14" height="10" rx="2"></rect><path d="M8 10V7a4 4 0 0 1 8 0v3"></path></svg><span>${item.lockedFields.length}</span></span>`
|
|
11353
11797
|
: "";
|
|
11354
11798
|
const characterCards = () => `<div class="card-grid">${pageCharacters.map((item) => {
|
|
11355
|
-
const
|
|
11799
|
+
const attributes = normalizeCharacterAttributes(item.attributes);
|
|
11800
|
+
const details = attributes.details ?? [];
|
|
11356
11801
|
return `
|
|
11357
11802
|
<article class="record-card character-card preview-record-card has-card-edit has-pin-control" data-open-character="${esc(item.id)}" role="button" tabindex="0" aria-label="查看角色 ${esc(item.name)}">${characterPinButton(item)}${characterFavoriteButton(item)}${recordCardEditButton("edit-character", item.id, `角色“${item.name}”`)}
|
|
11358
11803
|
<div class="character-card-heading">${characterAvatarHtml(item)}<h3>${esc(item.name)}</h3>${entityLifecycleBadge(item.isDead, "已死亡")}${characterLockBadge(item)}</div>
|
|
11359
|
-
${
|
|
11804
|
+
${attributes.identity ? `<p class="character-identity">${esc(attributes.identity)}</p>` : ""}
|
|
11360
11805
|
<div class="character-gender"><b>性别</b><span class="pill">${esc(characterGenderLabel(item.gender))}</span></div>
|
|
11361
11806
|
${item.aliases.length ? `<div class="character-aliases"><b>别名</b>${item.aliases.map((alias) => `<span class="pill">${esc(alias)}</span>`).join("")}</div>` : ""}
|
|
11362
11807
|
${item.code ? `<div class="character-code"><b>编号</b><span class="pill">${esc(item.code)}</span></div>` : ""}
|
|
@@ -11368,7 +11813,7 @@ async function renderCharacters(page = characterListPage) {
|
|
|
11368
11813
|
</article>`;
|
|
11369
11814
|
}).join("")}</div>`;
|
|
11370
11815
|
const characterRows = () => `<div class="module-row-list">${pageCharacters.map((item) => {
|
|
11371
|
-
const preview = moduleRowPreview(item.profile?.summary || item.attributes
|
|
11816
|
+
const preview = moduleRowPreview(item.profile?.summary || normalizeCharacterAttributes(item.attributes).identity || Object.entries(item.currentState).map(([key, value]) => `${characterStateFieldLabel(key)}:${value}`).join(" ") || "尚未记录当前状态");
|
|
11372
11817
|
const meta = [
|
|
11373
11818
|
item.code ? `编号 ${item.code}` : "",
|
|
11374
11819
|
`性别 ${characterGenderLabel(item.gender)}`,
|
|
@@ -13227,7 +13672,7 @@ async function openRelationshipIdentityRepairDialog(task, failure) {
|
|
|
13227
13672
|
const character = await api(`/api/characters/${encodeURIComponent(details.characterId)}`);
|
|
13228
13673
|
const anchors = [
|
|
13229
13674
|
character.code,
|
|
13230
|
-
character.attributes
|
|
13675
|
+
normalizeCharacterAttributes(character.attributes).identity,
|
|
13231
13676
|
character.race?.name || character.species,
|
|
13232
13677
|
...(Array.isArray(character.organizations) ? character.organizations.map((organization) => organization.name) : [])
|
|
13233
13678
|
].map((value) => String(value ?? "").trim()).filter(Boolean);
|
|
@@ -13246,7 +13691,7 @@ async function openRelationshipIdentityRepairDialog(task, failure) {
|
|
|
13246
13691
|
<label>角色标准名<input type="text" value="${esc(character.name)}" readonly></label>
|
|
13247
13692
|
${field("aliases", "确认别名", "item-list", character.aliases ?? [])}
|
|
13248
13693
|
${field("code", "人物编号或代号", "text", character.code)}
|
|
13249
|
-
${field("identity", "身份与定位", "text", character.attributes
|
|
13694
|
+
${field("identity", "身份与定位", "text", normalizeCharacterAttributes(character.attributes).identity)}
|
|
13250
13695
|
<button class="ghost-button relationship-identity-full-profile" type="button" data-open-identity-character-profile>打开完整人物档案</button>`,
|
|
13251
13696
|
async (form) => {
|
|
13252
13697
|
const aliases = form.getAll("aliases").map((value) => String(value).trim()).filter(Boolean);
|
|
@@ -13257,7 +13702,7 @@ async function openRelationshipIdentityRepairDialog(task, failure) {
|
|
|
13257
13702
|
body: {
|
|
13258
13703
|
aliases,
|
|
13259
13704
|
code,
|
|
13260
|
-
attributes: { ...(character.attributes ?? {}), identity },
|
|
13705
|
+
attributes: normalizeCharacterAttributes({ ...(character.attributes ?? {}), identity }),
|
|
13261
13706
|
expectedVersionNo: character.versionNo,
|
|
13262
13707
|
changeNote: `修复人物关系来源匹配:${failure.message}`.slice(0, 500)
|
|
13263
13708
|
}
|
|
@@ -16925,6 +17370,7 @@ function renderCharacterEditorFields(item) {
|
|
|
16925
17370
|
const organizationOptions = state.organizations.map((organization) => [organization.id, organization.name]);
|
|
16926
17371
|
const chapterOptions = [["", "未指定"], ...(state.work?.volumes ?? []).flatMap((volume) => volume.chapters.map((chapter) => [chapter.id, `${volume.title} / ${chapter.title}`]))];
|
|
16927
17372
|
const stateEntries = characterStateEntries(item?.currentState ?? {});
|
|
17373
|
+
const attributes = normalizeCharacterAttributes(item?.attributes);
|
|
16928
17374
|
const raceField = !canReadModule("races")
|
|
16929
17375
|
? '<div class="character-editor-empty-field"><b>种族</b><span>当前账户没有种族模块读取权限,原有绑定不会被修改。</span></div>'
|
|
16930
17376
|
: state.races.length
|
|
@@ -16946,12 +17392,12 @@ function renderCharacterEditorFields(item) {
|
|
|
16946
17392
|
: '<div class="character-editor-empty-field"><b>首次登场章节</b><span>当前账户没有正文读取权限,原有绑定不会被修改。</span></div>')),
|
|
16947
17393
|
characterEditorSection("profile", "人物档案", "记录人物定位、行为动力、公开人设和便于创作时快速理解的简介。",
|
|
16948
17394
|
field("code", "编号", "text", item?.code) +
|
|
16949
|
-
field("identity", "身份与定位", "text",
|
|
17395
|
+
field("identity", "身份与定位", "text", attributes.identity) +
|
|
16950
17396
|
field("motivation", "核心动机", "textarea", item?.profile?.motivation) +
|
|
16951
17397
|
field("summary", "人物简介", "textarea", item?.profile?.summary) +
|
|
16952
17398
|
'<div class="form-field"><span>人设摘要</span><small>关系扮演时作为公开人设注入对方可见的角色卡,不会包含私密档案或 Markdown 章节。</small><textarea name="personaSummary" maxlength="20000" aria-label="人设摘要">' + esc(item?.profile?.personaSummary ?? "") + "</textarea></div>"),
|
|
16953
17399
|
characterEditorSection("settings", "扩展设定", "可用短属性和 Markdown 长章节承载形态、能力、生态、经历与研究记录。",
|
|
16954
|
-
field("details", "扩展属性", "key-value-list",
|
|
17400
|
+
field("details", "扩展属性", "key-value-list", attributes.details, { multilineValue: true }) +
|
|
16955
17401
|
'<div id="character-markdown-sections" class="character-markdown-sections"></div>'),
|
|
16956
17402
|
characterEditorSection("state", "状态与约束", "维护任意当前状态,并明确禁止 AI 自行覆盖的字段。",
|
|
16957
17403
|
field("isDead", "标记为已死亡", "checkbox", item?.isDead ?? false) +
|
|
@@ -16994,11 +17440,11 @@ function collectCharacterBody(form) {
|
|
|
16994
17440
|
isDead: form.has("isDead"),
|
|
16995
17441
|
code: String(form.get("code") ?? "").trim(),
|
|
16996
17442
|
aliases: form.getAll("aliases").map((value) => String(value).trim()).filter(Boolean),
|
|
16997
|
-
attributes: {
|
|
16998
|
-
|
|
17443
|
+
attributes: normalizeCharacterAttributes({
|
|
17444
|
+
species: typeof item?.attributes?.species === "string" ? item.attributes.species : "",
|
|
16999
17445
|
identity: String(form.get("identity") ?? "").trim(),
|
|
17000
17446
|
details: buildCharacterDetails(form.getAll("detailLabel"), form.getAll("detailValue"))
|
|
17001
|
-
},
|
|
17447
|
+
}),
|
|
17002
17448
|
profile: {
|
|
17003
17449
|
...profile,
|
|
17004
17450
|
motivation: String(form.get("motivation") ?? "").trim(),
|
|
@@ -18505,6 +18951,8 @@ async function sendAiWithOptions({ ignoreContextWarning = false, retry = null }
|
|
|
18505
18951
|
if (!tab) return toast("Agent 对话页签尚未就绪", "error");
|
|
18506
18952
|
if (aiRequestManager.hasActive(tab.id)) return;
|
|
18507
18953
|
if (aiQuestionContinuationTabIds.has(tab.id)) return toast("AI 正在根据你的回答继续处理,请稍候");
|
|
18954
|
+
const queuedComposer = pendingQueuedComposer;
|
|
18955
|
+
pendingQueuedComposer = null;
|
|
18508
18956
|
const composerSnapshot = captureAiPromptComposer();
|
|
18509
18957
|
const requestComposerSnapshot = retry
|
|
18510
18958
|
? {
|
|
@@ -18516,7 +18964,7 @@ async function sendAiWithOptions({ ignoreContextWarning = false, retry = null }
|
|
|
18516
18964
|
sceneDirection: retry.sceneDirection ?? "",
|
|
18517
18965
|
scenePin: captureAiScenePin()
|
|
18518
18966
|
}
|
|
18519
|
-
: composerSnapshot;
|
|
18967
|
+
: queuedComposer ?? composerSnapshot;
|
|
18520
18968
|
const instruction = String(requestComposerSnapshot.markup ?? requestComposerSnapshot.text).trim();
|
|
18521
18969
|
const instructionText = String(requestComposerSnapshot.text ?? "").trim();
|
|
18522
18970
|
const sceneDirection = $("#ai-task").value === "roleplay"
|
|
@@ -18531,6 +18979,10 @@ async function sendAiWithOptions({ ignoreContextWarning = false, retry = null }
|
|
|
18531
18979
|
if ($("#ai-task").value === "roleplay" && !state.aiRoleplayCharacter) return toast("请先选择角色卡", "error");
|
|
18532
18980
|
const requestScope = currentAiRequestScope();
|
|
18533
18981
|
if (!requestScope) return toast("请先选择章节", "error");
|
|
18982
|
+
if (queuedComposer) {
|
|
18983
|
+
aiPromptQueue.remove(tab.id, queuedComposer.id);
|
|
18984
|
+
renderAiPromptQueue();
|
|
18985
|
+
}
|
|
18534
18986
|
const { scope } = requestScope;
|
|
18535
18987
|
const citations = requestComposerSnapshot.citations.map(({ chapterId, chapterTitle, startLine, endLine, text }) => ({ chapterId, chapterTitle, startLine, endLine, text }));
|
|
18536
18988
|
const selectedTaskType = $("#ai-task").value;
|
|
@@ -18541,7 +18993,8 @@ async function sendAiWithOptions({ ignoreContextWarning = false, retry = null }
|
|
|
18541
18993
|
tabId: tab.id,
|
|
18542
18994
|
workId: state.work.id,
|
|
18543
18995
|
conversationId: state.aiConversationId
|
|
18544
|
-
})
|
|
18996
|
+
}),
|
|
18997
|
+
preserveComposer: Boolean(queuedComposer)
|
|
18545
18998
|
};
|
|
18546
18999
|
if (retry?.userMessageId) {
|
|
18547
19000
|
requestHolder.snapshot = aiRequestManager.bind(requestHolder.snapshot, { userMessageId: retry.userMessageId });
|
|
@@ -18549,6 +19002,7 @@ async function sendAiWithOptions({ ignoreContextWarning = false, retry = null }
|
|
|
18549
19002
|
setAiChatTabStatus(tab, "streaming");
|
|
18550
19003
|
syncAiRequestControls();
|
|
18551
19004
|
scrollAiFeedToBottom(tab.feed, { force: true });
|
|
19005
|
+
let shouldFlushQueuedPrompt = false;
|
|
18552
19006
|
try {
|
|
18553
19007
|
try {
|
|
18554
19008
|
await ensureAiModelsLoaded();
|
|
@@ -18593,6 +19047,7 @@ async function sendAiWithOptions({ ignoreContextWarning = false, retry = null }
|
|
|
18593
19047
|
const streamedRequest = assertAiRequestCurrent(requestHolder.snapshot);
|
|
18594
19048
|
if (streamed.action === "warn") return;
|
|
18595
19049
|
assistantContent = streamed.content;
|
|
19050
|
+
shouldFlushQueuedPrompt = true;
|
|
18596
19051
|
assistantMessage = streamed.message;
|
|
18597
19052
|
assistantMetadata = streamed.metadata;
|
|
18598
19053
|
persistedStreamMessage = streamed.messageId ? { id: streamed.messageId, createdAt: streamed.createdAt } : null;
|
|
@@ -18723,6 +19178,18 @@ async function sendAiWithOptions({ ignoreContextWarning = false, retry = null }
|
|
|
18723
19178
|
if (tab.status === "streaming") setAiChatTabStatus(tab, "ready");
|
|
18724
19179
|
else renderAiChatTabs();
|
|
18725
19180
|
if (isActiveAiChatTab(tab)) syncAiRequestControls();
|
|
19181
|
+
if (queuedComposer && !shouldFlushQueuedPrompt) {
|
|
19182
|
+
const stillQueued = aiPromptQueue.list(tab.id).some((item) => item.id === queuedComposer.id);
|
|
19183
|
+
if (!stillQueued) {
|
|
19184
|
+
try {
|
|
19185
|
+
aiPromptQueue.restore(tab.id, queuedComposer, 0);
|
|
19186
|
+
} catch {
|
|
19187
|
+
/* 失败时尽量把未发出的排队 Prompt 放回队首,超限则保留当前队列。 */
|
|
19188
|
+
}
|
|
19189
|
+
renderAiPromptQueue();
|
|
19190
|
+
}
|
|
19191
|
+
}
|
|
19192
|
+
if (shouldFlushQueuedPrompt) flushNextQueuedPrompt(tab);
|
|
18726
19193
|
}
|
|
18727
19194
|
}
|
|
18728
19195
|
|
|
@@ -18742,13 +19209,13 @@ async function streamChat(requestHolder, body, idempotencyKey, { endpoint = null
|
|
|
18742
19209
|
const tab = aiChatTabForRequest(requestHolder.snapshot);
|
|
18743
19210
|
if (!tab) throw createAiRequestAbortError("Agent 对话页签已关闭");
|
|
18744
19211
|
const feed = tab.feed;
|
|
18745
|
-
|
|
19212
|
+
let message = document.createElement("div");
|
|
18746
19213
|
message.className = "assistant-message is-streaming";
|
|
18747
19214
|
message.dataset.testid = "ai-stream-message";
|
|
18748
19215
|
const streamConnectionStartedAt = Date.now();
|
|
18749
19216
|
message.innerHTML = '<div class="message-body" data-testid="ai-stream-content" aria-live="polite" aria-busy="true"></div><div class="message-meta">正在连接模型流…… <span class="ai-stream-connection-seconds" data-testid="ai-stream-connection-seconds"></span> 秒</div>';
|
|
18750
|
-
|
|
18751
|
-
|
|
19217
|
+
let content = message.querySelector(".message-body");
|
|
19218
|
+
let meta = message.querySelector(".message-meta");
|
|
18752
19219
|
const connectionSeconds = message.querySelector(".ai-stream-connection-seconds");
|
|
18753
19220
|
const renderStreamConnectionElapsed = () => {
|
|
18754
19221
|
const elapsedSeconds = Math.max(0, Math.floor((Date.now() - streamConnectionStartedAt) / 1000));
|
|
@@ -18773,7 +19240,7 @@ async function streamChat(requestHolder, body, idempotencyKey, { endpoint = null
|
|
|
18773
19240
|
scrollAiFeedToBottom(feed);
|
|
18774
19241
|
return true;
|
|
18775
19242
|
};
|
|
18776
|
-
const
|
|
19243
|
+
const createAssistantTypewriter = () => createStreamTypewriter({
|
|
18777
19244
|
speedController: streamSpeedController,
|
|
18778
19245
|
shouldAnimate: () => aiStreamTargetVisible(feed),
|
|
18779
19246
|
onRender: (text, progress) => {
|
|
@@ -18786,6 +19253,7 @@ async function streamChat(requestHolder, body, idempotencyKey, { endpoint = null
|
|
|
18786
19253
|
});
|
|
18787
19254
|
}
|
|
18788
19255
|
});
|
|
19256
|
+
let typewriter = createAssistantTypewriter();
|
|
18789
19257
|
let streamedText = "";
|
|
18790
19258
|
let streamedPendingText = "";
|
|
18791
19259
|
let generatedMetadata = {};
|
|
@@ -18806,8 +19274,9 @@ async function streamChat(requestHolder, body, idempotencyKey, { endpoint = null
|
|
|
18806
19274
|
const processStepVisibleContents = new Map();
|
|
18807
19275
|
const renderStreamingProcessSteps = (completed, durationMs = elapsedProcessTime()) => {
|
|
18808
19276
|
if (!aiRequestTargetsCurrentState(requestHolder.snapshot)) return;
|
|
18809
|
-
|
|
18810
|
-
|
|
19277
|
+
const target = message;
|
|
19278
|
+
aiStreamRenders.enqueue(target, () => {
|
|
19279
|
+
renderAiProcessSteps(target, processSteps, completed, durationMs, processStepVisibleContents);
|
|
18811
19280
|
scrollAiFeedToBottom(feed);
|
|
18812
19281
|
});
|
|
18813
19282
|
};
|
|
@@ -18832,6 +19301,45 @@ async function streamChat(requestHolder, body, idempotencyKey, { endpoint = null
|
|
|
18832
19301
|
const revealProcessStepTypewriters = () => {
|
|
18833
19302
|
for (const typewriter of processStepTypewriters.values()) typewriter.reveal();
|
|
18834
19303
|
};
|
|
19304
|
+
const beginSteeredAssistantSegment = async (steerPayload) => {
|
|
19305
|
+
await Promise.all([typewriter.finish(), finishProcessStepTypewriters()]);
|
|
19306
|
+
if (messageMounted) {
|
|
19307
|
+
message.classList.remove("is-streaming");
|
|
19308
|
+
content.setAttribute("aria-busy", "false");
|
|
19309
|
+
const headingRole = message.querySelector(".message-heading > span");
|
|
19310
|
+
if (headingRole) headingRole.textContent = aiAssistantLabel("", tab.roleplayCharacter);
|
|
19311
|
+
renderStreamingProcessSteps(true);
|
|
19312
|
+
if (streamedText) attachAssistantCopyAction(message, streamedText);
|
|
19313
|
+
}
|
|
19314
|
+
const steerMessage = steerPayload?.message;
|
|
19315
|
+
if (steerMessage?.id) {
|
|
19316
|
+
updateAiConversationSummaryFromMessage(steerMessage);
|
|
19317
|
+
const existingSteerMessage = [...tab.feed.querySelectorAll(".user-message[data-message-id]")]
|
|
19318
|
+
.find((candidate) => candidate.dataset.messageId === String(steerMessage.id));
|
|
19319
|
+
if (!existingSteerMessage) {
|
|
19320
|
+
appendMessage("user", steerMessage.content, steerMessage.citations, steerMessage.createdAt, {
|
|
19321
|
+
...steerMessage.metadata,
|
|
19322
|
+
kind: "steer"
|
|
19323
|
+
}, steerMessage.id, { tab });
|
|
19324
|
+
}
|
|
19325
|
+
}
|
|
19326
|
+
streamedText = "";
|
|
19327
|
+
streamedPendingText = "";
|
|
19328
|
+
toolCalls = [];
|
|
19329
|
+
processSteps = [];
|
|
19330
|
+
processStepTypewriters.clear();
|
|
19331
|
+
processStepVisibleContents.clear();
|
|
19332
|
+
message = document.createElement("div");
|
|
19333
|
+
message.className = "assistant-message is-streaming";
|
|
19334
|
+
message.dataset.testid = "ai-stream-message";
|
|
19335
|
+
message.innerHTML = '<div class="message-body" data-testid="ai-stream-content" aria-live="polite" aria-busy="true"></div><div class="message-meta">已收到执行流引导,正在继续生成</div>';
|
|
19336
|
+
content = message.querySelector(".message-body");
|
|
19337
|
+
meta = message.querySelector(".message-meta");
|
|
19338
|
+
messageMounted = false;
|
|
19339
|
+
typewriter = createAssistantTypewriter();
|
|
19340
|
+
mountAssistantMessage();
|
|
19341
|
+
scrollAiFeedToBottom(feed);
|
|
19342
|
+
};
|
|
18835
19343
|
try {
|
|
18836
19344
|
const request = assertAiRequestCurrent(requestHolder.snapshot);
|
|
18837
19345
|
const response = await fetch(endpoint ?? `/api/works/${encodeURIComponent(request.workId)}/chat/stream`, {
|
|
@@ -18848,7 +19356,7 @@ async function streamChat(requestHolder, body, idempotencyKey, { endpoint = null
|
|
|
18848
19356
|
let streamError = null;
|
|
18849
19357
|
const consume = async (eventName, payload) => {
|
|
18850
19358
|
assertAiRequestCurrent(requestHolder.snapshot);
|
|
18851
|
-
if (streamConnectionEstablishedEvents.has(eventName)) stopStreamConnectionTimer();
|
|
19359
|
+
if (streamConnectionEstablishedEvents.has(eventName) || eventName === "steer") stopStreamConnectionTimer();
|
|
18852
19360
|
if (eventName === "continuation") {
|
|
18853
19361
|
if (String(payload.conversationId ?? "") !== requestHolder.snapshot.conversationId) throw new Error("流式续接返回了其他对话");
|
|
18854
19362
|
toolCalls = Array.isArray(payload.toolCalls) ? payload.toolCalls : [];
|
|
@@ -18898,7 +19406,7 @@ async function streamChat(requestHolder, body, idempotencyKey, { endpoint = null
|
|
|
18898
19406
|
tab.hasImageAttachments = tab.hasImageAttachments === true || aiConversationMessageHasImages(persistedUserMessage);
|
|
18899
19407
|
tab.selectedModelId = lockedModelId;
|
|
18900
19408
|
tab.promptSent = true;
|
|
18901
|
-
clearAiChatTabComposer(tab);
|
|
19409
|
+
if (!requestHolder.preserveComposer) clearAiChatTabComposer(tab);
|
|
18902
19410
|
const existingUserMessage = [...tab.feed.querySelectorAll(".user-message[data-message-id]")]
|
|
18903
19411
|
.find((candidate) => candidate.dataset.messageId === String(persistedUserMessage.id));
|
|
18904
19412
|
if (!existingUserMessage) {
|
|
@@ -18911,10 +19419,14 @@ async function streamChat(requestHolder, body, idempotencyKey, { endpoint = null
|
|
|
18911
19419
|
syncAiTaskOptions();
|
|
18912
19420
|
renderAiRoleplayCharacterSelect();
|
|
18913
19421
|
renderAiQuickActions();
|
|
18914
|
-
|
|
19422
|
+
if (!requestHolder.preserveComposer) {
|
|
19423
|
+
clearAiPromptComposer({ collapseScenePanel: Boolean(String(body.sceneDirection ?? "").trim()) });
|
|
19424
|
+
}
|
|
18915
19425
|
}
|
|
18916
19426
|
mountAssistantMessage();
|
|
18917
19427
|
}
|
|
19428
|
+
} else if (eventName === "steer") {
|
|
19429
|
+
await beginSteeredAssistantSegment(payload);
|
|
18918
19430
|
} else if (eventName === "delta") {
|
|
18919
19431
|
mountAssistantMessage();
|
|
18920
19432
|
const delta = typeof payload.delta === "string" ? payload.delta : "";
|
|
@@ -19126,6 +19638,14 @@ function appendMessage(role, text, citations = [], createdAt = null, metadata =
|
|
|
19126
19638
|
failureBadge.setAttribute("aria-label", `消息状态:${isInterrupted ? aiStreamInterruptionLabel(interruptionCode) : "失败"}`);
|
|
19127
19639
|
heading.firstElementChild?.append(failureBadge);
|
|
19128
19640
|
}
|
|
19641
|
+
if (role === "user" && metadata?.kind === "steer") {
|
|
19642
|
+
message.classList.add("is-steer");
|
|
19643
|
+
const steerBadge = document.createElement("strong");
|
|
19644
|
+
steerBadge.className = "ai-message-status is-steer";
|
|
19645
|
+
steerBadge.textContent = "引导";
|
|
19646
|
+
steerBadge.setAttribute("aria-label", "消息类型:执行流引导");
|
|
19647
|
+
heading.firstElementChild?.append(steerBadge);
|
|
19648
|
+
}
|
|
19129
19649
|
if (parsedUserTurn?.hasMarkup && parsedUserTurn.sceneDirection) {
|
|
19130
19650
|
const scene = document.createElement("div");
|
|
19131
19651
|
scene.className = "user-message-scene";
|
|
@@ -21009,6 +21529,7 @@ $("#module-create-button").addEventListener("click", () => ({ drafts: openDraftD
|
|
|
21009
21529
|
$(field)?.addEventListener("input", () => {
|
|
21010
21530
|
syncAiSceneComposer();
|
|
21011
21531
|
persistActiveAiChatTab();
|
|
21532
|
+
syncAiRequestControls();
|
|
21012
21533
|
});
|
|
21013
21534
|
}
|
|
21014
21535
|
$("#ai-attachment-input").addEventListener("change", (event) => {
|
|
@@ -21018,6 +21539,7 @@ $("#module-create-button").addEventListener("click", () => ({ drafts: openDraftD
|
|
|
21018
21539
|
$("#ai-prompt").addEventListener("input", async () => {
|
|
21019
21540
|
updateAiMentionMenu();
|
|
21020
21541
|
setAiContextMeter(null);
|
|
21542
|
+
syncAiRequestControls();
|
|
21021
21543
|
const textBeforeCursor = aiPromptTextBeforeCursor();
|
|
21022
21544
|
if ($("#ai-task").value !== "roleplay" && findAiSkillCommand(textBeforeCursor)) return;
|
|
21023
21545
|
if (!findAiMention(textBeforeCursor)) return;
|
|
@@ -21426,6 +21948,11 @@ $("#ai-context-meter").addEventListener("click", () => {
|
|
|
21426
21948
|
$("#ai-context-popover-close").addEventListener("click", () => setAiContextDistributionVisible(false));
|
|
21427
21949
|
$("#ai-citation-popover-close").addEventListener("click", () => closeAiCitationPopover({ restoreFocus: true }));
|
|
21428
21950
|
$("#ai-send").addEventListener("click", activateAiSendControl);
|
|
21951
|
+
$("#ai-send-mode").addEventListener("change", (event) => {
|
|
21952
|
+
writeStoredAiSendMode(localStorage, event.currentTarget.value);
|
|
21953
|
+
syncAiRequestControls();
|
|
21954
|
+
});
|
|
21955
|
+
applyAiSendModeFromStorage();
|
|
21429
21956
|
$("#ai-conversation-switcher").addEventListener("click", () => {
|
|
21430
21957
|
setAiConversationSwitcherVisible($("#ai-conversation-switcher-menu").classList.contains("hidden"));
|
|
21431
21958
|
});
|
|
@@ -21769,9 +22296,9 @@ $("#ai-prompt").addEventListener("keydown", (event) => {
|
|
|
21769
22296
|
return;
|
|
21770
22297
|
}
|
|
21771
22298
|
}
|
|
21772
|
-
if (
|
|
22299
|
+
if (shouldActivateAiSendControl(event)) {
|
|
21773
22300
|
event.preventDefault();
|
|
21774
|
-
if (!$("#ai-send").disabled
|
|
22301
|
+
if (!$("#ai-send").disabled) submitAiComposerPrompt();
|
|
21775
22302
|
}
|
|
21776
22303
|
});
|
|
21777
22304
|
$(".quick-actions").addEventListener("click", (event) => {
|