@musnows/scriverse 0.4.11 → 0.4.12
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/dist/ai.js +209 -17
- package/dist/ai.js.map +1 -1
- package/dist/app.js +39 -1
- package/dist/app.js.map +1 -1
- package/dist/public/ai-message-meta.js +7 -2
- package/dist/public/app.js +181 -13
- package/dist/public/index.html +3 -3
- package/dist/public/styles.css +38 -0
- package/dist/store.js +21 -6
- package/dist/store.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
export function formatAiMessageMeta(modelDisplayName, outputTokens, suffix = "") {
|
|
1
|
+
export function formatAiMessageMeta(modelDisplayName, outputTokens, cacheHitPercent, suffix = "") {
|
|
2
2
|
const modelName = String(modelDisplayName || "模型").trim();
|
|
3
3
|
const tokenCount = Math.max(0, Math.round(Number(outputTokens) || 0)).toLocaleString("zh-CN");
|
|
4
|
-
|
|
4
|
+
const cachePercent = Number(cacheHitPercent);
|
|
5
|
+
const roundedCachePercent = Math.round((cachePercent + Number.EPSILON) * 10) / 10;
|
|
6
|
+
const cacheLabel = Number.isFinite(cachePercent)
|
|
7
|
+
? `缓存命中 ${Math.max(0, Math.min(100, roundedCachePercent)).toLocaleString("zh-CN")}%`
|
|
8
|
+
: "";
|
|
9
|
+
return [modelName, `${tokenCount} tok`, cacheLabel, String(suffix || "").trim()].filter(Boolean).join(" · ");
|
|
5
10
|
}
|
|
6
11
|
|
|
7
12
|
export function estimateAiMessageTokens(value) {
|
package/dist/public/app.js
CHANGED
|
@@ -6,7 +6,7 @@ import { shouldShowAiQuickActions } from "/ai-conversation.js?v=20260713-quick-a
|
|
|
6
6
|
import { calculateLineNumberRowHeight, calculateLineNumberRowTop, calculateLineNumberTextOffset, calculateLineNumberTop } from "/line-number-layout.js?v=20260713-row-box-alignment";
|
|
7
7
|
import { MODEL_PURPOSE_OPTIONS, isKimiModelId, modelFormValues, modelOptionLabel, modelPayload } from "/model-config.js?v=20260723-kimi-temperature";
|
|
8
8
|
import { shouldSendAiPrompt } from "/ai-prompt-keyboard.js?v=20260713-enter-to-send";
|
|
9
|
-
import { estimateAiMessageTokens, formatAiMessageMeta } from "/ai-message-meta.js?v=
|
|
9
|
+
import { estimateAiMessageTokens, formatAiMessageMeta } from "/ai-message-meta.js?v=20260726-cache-hit-percent";
|
|
10
10
|
import { formatAiMessageTime } from "/ai-message-time.js?v=20260713-cross-day-time";
|
|
11
11
|
import { formatAiContextUsageTooltip } from "/ai-context-meter.js?v=20260718-layered-context";
|
|
12
12
|
import { copyAiRawMarkdown } from "/ai-message-actions.js?v=20260713-copy-raw-markdown";
|
|
@@ -104,6 +104,8 @@ let peerPageStale = false;
|
|
|
104
104
|
let collaborationAutoSaveDisabled = false;
|
|
105
105
|
|
|
106
106
|
let timelineMultiSelectEnabled = false;
|
|
107
|
+
let taskProgressRefreshTimer = null;
|
|
108
|
+
const taskProgressRefreshInterval = 2_500;
|
|
107
109
|
|
|
108
110
|
const chapterTypes = ["正文", "设定", "作者的话", "其他"];
|
|
109
111
|
|
|
@@ -2989,6 +2991,7 @@ async function showModule(module) {
|
|
|
2989
2991
|
if (module !== "editor" && state.module === "editor" && !(await confirmDiscardChanges())) return;
|
|
2990
2992
|
if (module !== "editor" && state.module === "editor" && state.dirty) setSaveState("已放弃修改");
|
|
2991
2993
|
state.module = module;
|
|
2994
|
+
if (module !== "tasks") stopTaskProgressRefresh();
|
|
2992
2995
|
applyWorkAccessMode();
|
|
2993
2996
|
markActiveModule(module);
|
|
2994
2997
|
if (module === "editor") {
|
|
@@ -3868,6 +3871,7 @@ async function renderReviews() {
|
|
|
3868
3871
|
}
|
|
3869
3872
|
|
|
3870
3873
|
async function renderTasks() {
|
|
3874
|
+
stopTaskProgressRefresh();
|
|
3871
3875
|
const [tasks, settings] = await Promise.all([
|
|
3872
3876
|
apiAllPages(`/api/works/${state.work.id}/tasks?view=summary`),
|
|
3873
3877
|
canReadModule("ai-settings")
|
|
@@ -3877,7 +3881,12 @@ async function renderTasks() {
|
|
|
3877
3881
|
mountModuleCount(tasks.length);
|
|
3878
3882
|
const canConfigureAutoRun = canEditModule("tasks") && canEditModule("ai-settings");
|
|
3879
3883
|
const pendingCount = tasks.filter((item) => item.status === "pending").length;
|
|
3880
|
-
const
|
|
3884
|
+
const runningTasks = tasks.filter((item) => item.status === "running");
|
|
3885
|
+
const runningCount = runningTasks.length;
|
|
3886
|
+
const activeTaskCount = pendingCount + runningCount;
|
|
3887
|
+
const runningProgress = runningCount
|
|
3888
|
+
? Math.round(runningTasks.reduce((total, item) => total + Math.min(100, Math.max(0, Number(item.progress) || 0)), 0) / runningCount)
|
|
3889
|
+
: 0;
|
|
3881
3890
|
$("#module-content").innerHTML = `
|
|
3882
3891
|
<section class="task-auto-run-panel ${canConfigureAutoRun ? "" : "hidden"}" aria-labelledby="task-auto-run-title">
|
|
3883
3892
|
<div class="task-auto-run-copy">
|
|
@@ -3893,6 +3902,10 @@ async function renderTasks() {
|
|
|
3893
3902
|
<button id="task-auto-run-continue" class="ghost-button" type="button" ${settings.autoRunEnabled ? "" : "disabled"}>开始下一轮</button>
|
|
3894
3903
|
</div>
|
|
3895
3904
|
<p class="task-auto-run-meta">待执行队列 ${pendingCount} 个 · 正在运行 ${runningCount} 个</p>
|
|
3905
|
+
<div class="task-auto-run-progress ${activeTaskCount ? "" : "hidden"}" aria-live="polite">
|
|
3906
|
+
<div class="task-auto-run-progress-label"><span>${runningCount ? "运行中任务平均进度" : "等待任务开始"}</span><strong>${runningProgress}%</strong></div>
|
|
3907
|
+
<progress class="task-auto-run-progress-bar" max="100" value="${runningProgress}" aria-label="${runningCount ? "运行中任务平均进度" : "待执行任务进度"}">${runningProgress}%</progress>
|
|
3908
|
+
</div>
|
|
3896
3909
|
</section>
|
|
3897
3910
|
${tasks.length ? `<table class="table-list task-table"><thead><tr><th>分析类型</th><th>范围</th><th>状态</th><th>进度</th><th>操作</th></tr></thead><tbody>${tasks.map((item) => `
|
|
3898
3911
|
<tr>
|
|
@@ -3956,6 +3969,7 @@ async function renderTasks() {
|
|
|
3956
3969
|
button.textContent = "运行中";
|
|
3957
3970
|
const cancel = button.parentElement.querySelector("[data-cancel-task]");
|
|
3958
3971
|
if (cancel) cancel.textContent = "取消运行";
|
|
3972
|
+
scheduleTaskProgressRefresh(workId, 1);
|
|
3959
3973
|
const completed = await api(`/api/tasks/${button.dataset.runTask}/run`, { method: "POST", body: { modelId: $("#ai-model").value || undefined } });
|
|
3960
3974
|
toast(completed.status === "cancelled" ? "分析任务已取消" : completed.status === "expired" ? "正文已变化,本次分析已过期" : "分析已完成");
|
|
3961
3975
|
if (state.module === "tasks" && state.work?.id === workId) await renderTasks();
|
|
@@ -3975,6 +3989,32 @@ async function renderTasks() {
|
|
|
3975
3989
|
button.disabled = false;
|
|
3976
3990
|
}
|
|
3977
3991
|
}));
|
|
3992
|
+
scheduleTaskProgressRefresh(state.work.id, runningCount);
|
|
3993
|
+
}
|
|
3994
|
+
|
|
3995
|
+
function stopTaskProgressRefresh() {
|
|
3996
|
+
if (taskProgressRefreshTimer === null) return;
|
|
3997
|
+
window.clearTimeout(taskProgressRefreshTimer);
|
|
3998
|
+
taskProgressRefreshTimer = null;
|
|
3999
|
+
}
|
|
4000
|
+
|
|
4001
|
+
function scheduleTaskProgressRefresh(workId, runningCount) {
|
|
4002
|
+
stopTaskProgressRefresh();
|
|
4003
|
+
if (runningCount === 0) return;
|
|
4004
|
+
taskProgressRefreshTimer = window.setTimeout(async () => {
|
|
4005
|
+
taskProgressRefreshTimer = null;
|
|
4006
|
+
if (state.module !== "tasks" || state.work?.id !== workId) return;
|
|
4007
|
+
if ($(".task-auto-run-controls")?.contains(document.activeElement)) {
|
|
4008
|
+
scheduleTaskProgressRefresh(workId, runningCount);
|
|
4009
|
+
return;
|
|
4010
|
+
}
|
|
4011
|
+
try {
|
|
4012
|
+
await renderTasks();
|
|
4013
|
+
} catch (error) {
|
|
4014
|
+
console.error("Failed to refresh task progress", error);
|
|
4015
|
+
scheduleTaskProgressRefresh(workId, runningCount);
|
|
4016
|
+
}
|
|
4017
|
+
}, taskProgressRefreshInterval);
|
|
3978
4018
|
}
|
|
3979
4019
|
|
|
3980
4020
|
function openTaskDetailDialog(task) {
|
|
@@ -4530,6 +4570,8 @@ function openDialog(title, fields, onSubmit, eyebrow = "新增", options = {}) {
|
|
|
4530
4570
|
bindRelationshipKeywordControls($("#dialog-fields"));
|
|
4531
4571
|
bindVditorEditors($("#dialog-fields"));
|
|
4532
4572
|
const form = $("#dynamic-form");
|
|
4573
|
+
form.onclick = null;
|
|
4574
|
+
form.onkeydown = null;
|
|
4533
4575
|
form.onsubmit = async (event) => {
|
|
4534
4576
|
if (event.submitter?.value === "cancel") {
|
|
4535
4577
|
void discardPendingMarkdownAttachments();
|
|
@@ -5922,14 +5964,58 @@ function openReviewDialog() {
|
|
|
5922
5964
|
});
|
|
5923
5965
|
}
|
|
5924
5966
|
|
|
5925
|
-
function openTaskDialog() {
|
|
5967
|
+
async function openTaskDialog() {
|
|
5926
5968
|
const chapterOptions = state.work.volumes.flatMap((volume) => volume.chapters.map((chapter) => [chapter.id, `${volume.title} / ${chapter.title}`]));
|
|
5969
|
+
let relationshipCharacters = [];
|
|
5970
|
+
try {
|
|
5971
|
+
relationshipCharacters = canReadModule("characters")
|
|
5972
|
+
? await apiAllPages(`/api/works/${state.work.id}/characters`)
|
|
5973
|
+
: [];
|
|
5974
|
+
} catch (error) {
|
|
5975
|
+
toast(`角色列表加载失败:${error.message}`, "error");
|
|
5976
|
+
return;
|
|
5977
|
+
}
|
|
5978
|
+
const characterOptions = relationshipCharacters.map((character) => [character.id, character.name]);
|
|
5979
|
+
const relationshipCharacterPicker = `<div class="form-field relationship-character-field">
|
|
5980
|
+
<span id="relationship-character-label">被分析角色(可多选)</span>
|
|
5981
|
+
<div class="relationship-character-picker">
|
|
5982
|
+
<button class="relationship-character-trigger" type="button" aria-expanded="false" aria-controls="relationship-character-bubble" aria-labelledby="relationship-character-label relationship-character-summary">
|
|
5983
|
+
<span id="relationship-character-summary">选择需要定向分析的角色</span>
|
|
5984
|
+
<span class="relationship-character-trigger-meta"><span data-relationship-character-count>未选择</span><span class="relationship-character-chevron" aria-hidden="true">⌄</span></span>
|
|
5985
|
+
</button>
|
|
5986
|
+
<div id="relationship-character-bubble" class="relationship-character-bubble hidden">
|
|
5987
|
+
<label class="relationship-character-search">筛选角色<input type="search" data-relationship-character-search placeholder="输入角色名" autocomplete="off"></label>
|
|
5988
|
+
<div class="relationship-character-bubble-meta"><span>共 ${characterOptions.length} 个角色</span><button type="button" data-relationship-character-clear disabled>清空选择</button></div>
|
|
5989
|
+
<div class="relationship-character-options" role="group" aria-labelledby="relationship-character-label">
|
|
5990
|
+
${characterOptions.map(([characterId, characterName]) => `<label class="relationship-character-chip" data-character-search-name="${esc(String(characterName).toLocaleLowerCase())}"><input type="checkbox" name="characterIds" value="${esc(characterId)}" data-character-name="${esc(characterName)}"><span>${esc(characterName)}</span></label>`).join("")}
|
|
5991
|
+
</div>
|
|
5992
|
+
<p class="relationship-character-empty hidden" data-relationship-character-empty>没有匹配的角色</p>
|
|
5993
|
+
</div>
|
|
5994
|
+
</div>
|
|
5995
|
+
</div>`;
|
|
5927
5996
|
const defaultTaskType = ANALYSIS_TYPES[0].value;
|
|
5928
5997
|
const taskTypeField = `<div class="form-field analysis-type-field"><label>分析类型<select name="taskType" aria-describedby="analysis-type-description">${ANALYSIS_TYPES.map(({ value, label }) => `<option value="${esc(value)}" ${value === defaultTaskType ? "selected" : ""}>${esc(label)}</option>`).join("")}</select></label><p id="analysis-type-description" class="analysis-type-description" aria-live="polite">${esc(analysisTypeDescription(defaultTaskType))}</p></div>`;
|
|
5929
5998
|
const chapterField = `<label class="task-chapter-field">章节<select name="chapterId">${chapterOptions.map(([key, text], index) => `<option value="${esc(key)}" ${index === 0 ? "selected" : ""}>${esc(text)}</option>`).join("")}</select></label>`;
|
|
5930
|
-
|
|
5931
|
-
|
|
5932
|
-
|
|
5999
|
+
const relationshipFields = `<div class="relationship-analysis-options hidden">
|
|
6000
|
+
${relationshipCharacterPicker}
|
|
6001
|
+
<p class="relationship-analysis-helper"><span aria-hidden="true">i</span><span>留空时使用基础关系抽取;选中角色后,将汇总其跨章节证据再进行全局关系归纳。</span></p>
|
|
6002
|
+
<div class="relationship-overwrite-card hidden">
|
|
6003
|
+
<label class="checkbox-field"><input name="replaceExistingRelationships" type="checkbox" disabled><span>用本次结果覆盖所选角色的已有关系</span></label>
|
|
6004
|
+
<p>任务成功后,会先删除所有涉及所选角色的旧关系,再写入本次分析结果。</p>
|
|
6005
|
+
</div>
|
|
6006
|
+
<label>额外分析提示<textarea name="additionalPrompt" maxlength="10000" placeholder="例如:重点识别权力继承、师承变化或隐秘亲缘关系"></textarea><small>将同时追加到证据收集和全局关系归纳提示词,仅影响本次任务。</small></label>
|
|
6007
|
+
</div>`;
|
|
6008
|
+
openDialog("开始 AI 分析", taskTypeField + field("scopeType", "分析范围", "select", "chapter", [["chapter", "指定章节"], ["book", "全书"]]) + chapterField + relationshipFields, async (form) => {
|
|
6009
|
+
const taskType = String(form.get("taskType"));
|
|
6010
|
+
const scopeType = String(form.get("scopeType"));
|
|
6011
|
+
const includeAllSettings = taskType === "relationship-analysis" && scopeType === "book-with-settings";
|
|
6012
|
+
const additionalPrompt = taskType === "relationship-analysis" ? String(form.get("additionalPrompt") ?? "").trim() : "";
|
|
6013
|
+
const characterIds = taskType === "relationship-analysis" ? form.getAll("characterIds").map(String).filter(Boolean) : [];
|
|
6014
|
+
const replaceExistingRelationships = characterIds.length > 0 && form.get("replaceExistingRelationships") === "on";
|
|
6015
|
+
const scope = taskType === "character-identity-audit" || scopeType === "book" || includeAllSettings
|
|
6016
|
+
? { type: "book", ...(includeAllSettings ? { includeAllSettings: true } : {}), ...(additionalPrompt ? { additionalPrompt } : {}), ...(characterIds.length ? { characterIds } : {}), ...(replaceExistingRelationships ? { replaceExistingRelationships: true } : {}) }
|
|
6017
|
+
: { type: "chapter", chapterId: form.get("chapterId"), ...(additionalPrompt ? { additionalPrompt } : {}), ...(characterIds.length ? { characterIds } : {}), ...(replaceExistingRelationships ? { replaceExistingRelationships: true } : {}) };
|
|
6018
|
+
await api(`/api/works/${state.work.id}/tasks`, { method: "POST", body: { taskType, scope } });
|
|
5933
6019
|
await renderTasks();
|
|
5934
6020
|
});
|
|
5935
6021
|
const taskTypeSelect = $("#dialog-fields").querySelector('select[name="taskType"]');
|
|
@@ -5937,17 +6023,99 @@ function openTaskDialog() {
|
|
|
5937
6023
|
const chapterSelect = $("#dialog-fields").querySelector('select[name="chapterId"]');
|
|
5938
6024
|
const chapterFieldElement = chapterSelect.closest(".task-chapter-field");
|
|
5939
6025
|
const description = $("#analysis-type-description");
|
|
6026
|
+
const relationshipOptions = $("#dialog-fields").querySelector(".relationship-analysis-options");
|
|
6027
|
+
const relationshipPrompt = relationshipOptions.querySelector('textarea[name="additionalPrompt"]');
|
|
6028
|
+
const relationshipCharacterPickerElement = relationshipOptions.querySelector(".relationship-character-picker");
|
|
6029
|
+
const relationshipCharacterTrigger = relationshipOptions.querySelector(".relationship-character-trigger");
|
|
6030
|
+
const relationshipCharacterBubble = relationshipOptions.querySelector(".relationship-character-bubble");
|
|
6031
|
+
const relationshipCharacterSearch = relationshipOptions.querySelector("[data-relationship-character-search]");
|
|
6032
|
+
const relationshipCharacterInputs = [...relationshipOptions.querySelectorAll('input[name="characterIds"]')];
|
|
6033
|
+
const relationshipCharacterSummary = relationshipOptions.querySelector("#relationship-character-summary");
|
|
6034
|
+
const relationshipCharacterCount = relationshipOptions.querySelector("[data-relationship-character-count]");
|
|
6035
|
+
const relationshipCharacterClear = relationshipOptions.querySelector("[data-relationship-character-clear]");
|
|
6036
|
+
const relationshipCharacterEmpty = relationshipOptions.querySelector("[data-relationship-character-empty]");
|
|
6037
|
+
const replaceRelationships = relationshipOptions.querySelector('input[name="replaceExistingRelationships"]');
|
|
6038
|
+
const relationshipOverwriteCard = relationshipOptions.querySelector(".relationship-overwrite-card");
|
|
6039
|
+
const allSettingsOption = document.createElement("option");
|
|
6040
|
+
allSettingsOption.value = "book-with-settings";
|
|
6041
|
+
allSettingsOption.textContent = "全书 + 所有设定";
|
|
5940
6042
|
const syncChapterField = () => {
|
|
5941
|
-
const disabled = scopeTypeSelect.value
|
|
6043
|
+
const disabled = scopeTypeSelect.value !== "chapter";
|
|
5942
6044
|
chapterSelect.disabled = disabled;
|
|
5943
6045
|
chapterFieldElement.classList.toggle("is-disabled", disabled);
|
|
5944
6046
|
chapterFieldElement.setAttribute("aria-disabled", String(disabled));
|
|
5945
6047
|
};
|
|
6048
|
+
const setRelationshipCharacterBubbleOpen = (open) => {
|
|
6049
|
+
relationshipCharacterBubble.classList.toggle("hidden", !open);
|
|
6050
|
+
relationshipCharacterTrigger.setAttribute("aria-expanded", String(open));
|
|
6051
|
+
if (open) relationshipCharacterSearch.focus();
|
|
6052
|
+
};
|
|
6053
|
+
const syncRelationshipCharacterPicker = () => {
|
|
6054
|
+
const selected = relationshipCharacterInputs.filter((input) => input.checked);
|
|
6055
|
+
const selectedNames = selected.map((input) => input.dataset.characterName);
|
|
6056
|
+
relationshipCharacterSummary.textContent = selectedNames.length
|
|
6057
|
+
? `${selectedNames.slice(0, 2).join("、")}${selectedNames.length > 2 ? ` 等 ${selectedNames.length} 人` : ""}`
|
|
6058
|
+
: "选择需要定向分析的角色";
|
|
6059
|
+
relationshipCharacterCount.textContent = selected.length ? `已选 ${selected.length}` : "未选择";
|
|
6060
|
+
relationshipCharacterClear.disabled = selected.length === 0;
|
|
6061
|
+
relationshipCharacterTrigger.setAttribute("aria-label", `筛选被分析角色,已选择 ${selected.length} 个`);
|
|
6062
|
+
};
|
|
6063
|
+
const filterRelationshipCharacters = () => {
|
|
6064
|
+
const query = relationshipCharacterSearch.value.trim().toLocaleLowerCase();
|
|
6065
|
+
let visibleCount = 0;
|
|
6066
|
+
for (const input of relationshipCharacterInputs) {
|
|
6067
|
+
const chip = input.closest(".relationship-character-chip");
|
|
6068
|
+
const visible = !query || chip.dataset.characterSearchName.includes(query);
|
|
6069
|
+
chip.classList.toggle("hidden", !visible);
|
|
6070
|
+
if (visible) visibleCount += 1;
|
|
6071
|
+
}
|
|
6072
|
+
relationshipCharacterEmpty.classList.toggle("hidden", visibleCount > 0);
|
|
6073
|
+
};
|
|
6074
|
+
const syncRelationshipOptions = () => {
|
|
6075
|
+
const enabled = taskTypeSelect.value === "relationship-analysis";
|
|
6076
|
+
if (enabled && !allSettingsOption.isConnected) scopeTypeSelect.append(allSettingsOption);
|
|
6077
|
+
if (!enabled && allSettingsOption.isConnected) {
|
|
6078
|
+
if (scopeTypeSelect.value === allSettingsOption.value) scopeTypeSelect.value = "book";
|
|
6079
|
+
allSettingsOption.remove();
|
|
6080
|
+
}
|
|
6081
|
+
relationshipOptions.classList.toggle("hidden", !enabled);
|
|
6082
|
+
relationshipPrompt.disabled = !enabled;
|
|
6083
|
+
relationshipCharacterTrigger.disabled = !enabled;
|
|
6084
|
+
relationshipCharacterSearch.disabled = !enabled;
|
|
6085
|
+
for (const input of relationshipCharacterInputs) input.disabled = !enabled;
|
|
6086
|
+
if (!enabled) setRelationshipCharacterBubbleOpen(false);
|
|
6087
|
+
const hasSelectedCharacters = enabled && relationshipCharacterInputs.some((input) => input.checked);
|
|
6088
|
+
replaceRelationships.disabled = !hasSelectedCharacters;
|
|
6089
|
+
relationshipOverwriteCard.classList.toggle("hidden", !hasSelectedCharacters);
|
|
6090
|
+
if (!hasSelectedCharacters) replaceRelationships.checked = false;
|
|
6091
|
+
syncRelationshipCharacterPicker();
|
|
6092
|
+
filterRelationshipCharacters();
|
|
6093
|
+
syncChapterField();
|
|
6094
|
+
};
|
|
5946
6095
|
taskTypeSelect.addEventListener("change", () => {
|
|
5947
6096
|
description.textContent = analysisTypeDescription(taskTypeSelect.value);
|
|
6097
|
+
syncRelationshipOptions();
|
|
6098
|
+
});
|
|
6099
|
+
relationshipCharacterTrigger.addEventListener("click", () => {
|
|
6100
|
+
setRelationshipCharacterBubbleOpen(relationshipCharacterTrigger.getAttribute("aria-expanded") !== "true");
|
|
5948
6101
|
});
|
|
6102
|
+
relationshipCharacterSearch.addEventListener("input", filterRelationshipCharacters);
|
|
6103
|
+
for (const input of relationshipCharacterInputs) input.addEventListener("change", syncRelationshipOptions);
|
|
6104
|
+
relationshipCharacterClear.addEventListener("click", () => {
|
|
6105
|
+
for (const input of relationshipCharacterInputs) input.checked = false;
|
|
6106
|
+
syncRelationshipOptions();
|
|
6107
|
+
});
|
|
6108
|
+
$("#dynamic-form").onclick = (event) => {
|
|
6109
|
+
if (!relationshipCharacterPickerElement.contains(event.target)) setRelationshipCharacterBubbleOpen(false);
|
|
6110
|
+
};
|
|
6111
|
+
$("#dynamic-form").onkeydown = (event) => {
|
|
6112
|
+
if (event.key !== "Escape" || relationshipCharacterBubble.classList.contains("hidden")) return;
|
|
6113
|
+
event.preventDefault();
|
|
6114
|
+
setRelationshipCharacterBubbleOpen(false);
|
|
6115
|
+
relationshipCharacterTrigger.focus();
|
|
6116
|
+
};
|
|
5949
6117
|
scopeTypeSelect.addEventListener("change", syncChapterField);
|
|
5950
|
-
|
|
6118
|
+
syncRelationshipOptions();
|
|
5951
6119
|
}
|
|
5952
6120
|
|
|
5953
6121
|
function openProviderDialog(item) {
|
|
@@ -6038,7 +6206,7 @@ async function sendAi() {
|
|
|
6038
6206
|
} else {
|
|
6039
6207
|
suggestion = await api(`/api/works/${state.work.id}/suggestions`, { method: "POST", body: { taskType, instruction, scope, modelId, citations } });
|
|
6040
6208
|
assistantContent = suggestion.content;
|
|
6041
|
-
assistantMetadata = { modelDisplayName: suggestion.model?.displayName, outputTokens: suggestion.outputTokens };
|
|
6209
|
+
assistantMetadata = { modelDisplayName: suggestion.model?.displayName, outputTokens: suggestion.outputTokens, cacheHitPercent: suggestion.cacheHitPercent };
|
|
6042
6210
|
}
|
|
6043
6211
|
try {
|
|
6044
6212
|
const persistedAssistantMessage = await persistAiConversationMessage("assistant", assistantContent, [], assistantMetadata);
|
|
@@ -6134,9 +6302,9 @@ async function streamChat(body) {
|
|
|
6134
6302
|
toolCalls = Array.isArray(payload.toolCalls) ? payload.toolCalls : toolCalls;
|
|
6135
6303
|
processSteps = Array.isArray(payload.processSteps) ? payload.processSteps : processSteps;
|
|
6136
6304
|
const processDurationMs = elapsedProcessTime();
|
|
6137
|
-
generatedMetadata = { modelDisplayName: payload.model?.displayName, outputTokens: payload.outputTokens, toolCalls, processSteps, processDurationMs };
|
|
6305
|
+
generatedMetadata = { modelDisplayName: payload.model?.displayName, outputTokens: payload.outputTokens, cacheHitPercent: payload.cacheHitPercent, toolCalls, processSteps, processDurationMs };
|
|
6138
6306
|
renderAiProcessSteps(message, processSteps, true, processDurationMs);
|
|
6139
|
-
meta.textContent = formatAiMessageMeta(payload.model?.displayName, payload.outputTokens);
|
|
6307
|
+
meta.textContent = formatAiMessageMeta(payload.model?.displayName, payload.outputTokens, payload.cacheHitPercent);
|
|
6140
6308
|
attachAssistantCopyAction(message, streamedText);
|
|
6141
6309
|
scrollAiFeedToBottom();
|
|
6142
6310
|
} else if (eventName === "error") {
|
|
@@ -6191,7 +6359,7 @@ function appendMessage(role, text, citations = [], createdAt = null, metadata =
|
|
|
6191
6359
|
const outputTokens = Number.isFinite(metadata?.outputTokens) ? metadata.outputTokens : estimateAiMessageTokens(text);
|
|
6192
6360
|
const meta = document.createElement("div");
|
|
6193
6361
|
meta.className = "message-meta";
|
|
6194
|
-
meta.textContent = formatAiMessageMeta(modelDisplayName, outputTokens);
|
|
6362
|
+
meta.textContent = formatAiMessageMeta(modelDisplayName, outputTokens, metadata?.cacheHitPercent);
|
|
6195
6363
|
message.append(meta);
|
|
6196
6364
|
attachAssistantCopyAction(message, text);
|
|
6197
6365
|
}
|
|
@@ -6206,7 +6374,7 @@ function appendSuggestion(suggestion, createdAt = null, messageId = null) {
|
|
|
6206
6374
|
const applicable = suggestion.action !== "note";
|
|
6207
6375
|
const guard = suggestion.guard;
|
|
6208
6376
|
const guardHtml = guard ? `<section class="guard-card ${esc(guard.status)}" data-testid="continuation-guard"><strong>${guard.status === "clear" ? "一致性守卫:未发现冲突" : guard.status === "warning" ? `一致性守卫:发现 ${guard.issues.length} 项风险` : "一致性守卫:检查失败"}</strong>${guard.status === "failed" ? `<p>${esc(guard.failure || "无法完成检查,请谨慎采纳")}</p>` : guard.issues.map((issue) => `<p><b>${esc(levelLabel(issue.severity))} · ${esc(reviewItemTypeLabel(issue.type))}</b> ${esc(issue.title)}${issue.description ? `:${esc(issue.description)}` : ""}</p>`).join("")}</section>` : "";
|
|
6209
|
-
message.innerHTML = `<div class="message-body">${renderMarkdown(suggestion.content)}</div><div class="message-meta">${esc(formatAiMessageMeta(suggestion.model?.displayName, suggestion.outputTokens, `基于 v${suggestion.chapterVersion ?? "-"}`))}</div>${guardHtml}${applicable ? '<div class="message-actions"><button data-action="accept">采纳到正文</button><button data-action="reject">拒绝</button></div>' : ""}`;
|
|
6377
|
+
message.innerHTML = `<div class="message-body">${renderMarkdown(suggestion.content)}</div><div class="message-meta">${esc(formatAiMessageMeta(suggestion.model?.displayName, suggestion.outputTokens, suggestion.cacheHitPercent, `基于 v${suggestion.chapterVersion ?? "-"}`))}</div>${guardHtml}${applicable ? '<div class="message-actions"><button data-action="accept">采纳到正文</button><button data-action="reject">拒绝</button></div>' : ""}`;
|
|
6210
6378
|
attachMessageHeading(message, "助手建议", createdAt ?? undefined);
|
|
6211
6379
|
attachAssistantCopyAction(message, suggestion.content);
|
|
6212
6380
|
attachMessageIdentity(message, messageId);
|
package/dist/public/index.html
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
<link rel="icon" href="/icon.svg?v=20260712" type="image/svg+xml">
|
|
11
11
|
<link rel="manifest" href="/site.webmanifest">
|
|
12
12
|
<link rel="stylesheet" href="/vendor/vditor/dist/index.css?v=3.11.2">
|
|
13
|
-
<link rel="stylesheet" href="/styles.css?v=20260726-ai-
|
|
13
|
+
<link rel="stylesheet" href="/styles.css?v=20260726-ai-history-cache-analysis-progress">
|
|
14
14
|
</head>
|
|
15
15
|
<body class="auth-pending">
|
|
16
16
|
<section id="auth-view" class="auth-view hidden" aria-labelledby="auth-title">
|
|
@@ -242,8 +242,8 @@
|
|
|
242
242
|
<div class="ai-heading-title"><span class="status-dot"></span><strong>创作助手</strong></div>
|
|
243
243
|
<div class="ai-heading-actions">
|
|
244
244
|
<span id="ai-conversation-title" title="当前对话">新对话</span>
|
|
245
|
+
<button id="ai-history-toggle" type="button" aria-label="历史记录" aria-controls="ai-history-dialog" aria-expanded="false" aria-haspopup="dialog" title="历史记录"><svg class="ai-heading-action-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M3 12a9 9 0 1 0 3-6.7L3 8"></path><path d="M3 3v5h5M12 7v5l3.5 2"></path></svg></button>
|
|
245
246
|
<button id="ai-new-conversation" type="button" aria-label="新建对话" title="新建对话">+</button>
|
|
246
|
-
<button id="ai-history-toggle" type="button" aria-controls="ai-history-dialog" aria-expanded="false" aria-haspopup="dialog" title="历史记录">历史</button>
|
|
247
247
|
</div>
|
|
248
248
|
</div>
|
|
249
249
|
<div class="quick-actions" aria-label="快捷指令">
|
|
@@ -732,6 +732,6 @@
|
|
|
732
732
|
<div id="auth-loading" class="auth-loading" role="status" aria-label="正在载入工作台"></div>
|
|
733
733
|
<script id="vditorIconScript" src="/vendor/vditor/dist/js/icons/ant.js?v=3.11.2"></script>
|
|
734
734
|
<script src="/vendor/vditor/dist/index.min.js?v=3.11.2"></script>
|
|
735
|
-
<script type="module" src="/app.js?v=20260726-
|
|
735
|
+
<script type="module" src="/app.js?v=20260726-ai-history-cache-analysis-progress"></script>
|
|
736
736
|
</body>
|
|
737
737
|
</html>
|
package/dist/public/styles.css
CHANGED
|
@@ -1180,6 +1180,13 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
|
|
|
1180
1180
|
margin: 0;
|
|
1181
1181
|
}
|
|
1182
1182
|
.task-auto-run-meta { margin: 0; color: var(--muted); font-size: 10px; }
|
|
1183
|
+
.task-auto-run-progress { display: grid; gap: 6px; width: min(100%, 420px); }
|
|
1184
|
+
.task-auto-run-progress-label { display: flex; align-items: center; justify-content: space-between; gap: 12px; color: var(--muted); font-size: 10px; }
|
|
1185
|
+
.task-auto-run-progress-label strong { color: var(--ink); font-family: var(--font-latin), monospace; font-size: 10px; font-weight: 600; }
|
|
1186
|
+
.task-auto-run-progress-bar { width: 100%; height: 6px; overflow: hidden; border: 0; border-radius: 999px; appearance: none; background: color-mix(in srgb, var(--line) 72%, transparent); }
|
|
1187
|
+
.task-auto-run-progress-bar::-webkit-progress-bar { border-radius: inherit; background: color-mix(in srgb, var(--line) 72%, transparent); }
|
|
1188
|
+
.task-auto-run-progress-bar::-webkit-progress-value { border-radius: inherit; background: var(--accent); transition: width .25s ease; }
|
|
1189
|
+
.task-auto-run-progress-bar::-moz-progress-bar { border-radius: inherit; background: var(--accent); transition: width .25s ease; }
|
|
1183
1190
|
.task-row-actions { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
1184
1191
|
.task-detail { display: grid; gap: 12px; }
|
|
1185
1192
|
.task-detail p, .task-detail div { margin: 0; font-size: 12px; line-height: 1.55; }
|
|
@@ -1481,6 +1488,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
|
|
|
1481
1488
|
.ai-heading-actions { display: flex; align-items: center; min-width: 0; margin-left: auto; gap: 5px; }
|
|
1482
1489
|
.ai-heading-actions > span { min-width: 0; max-width: 88px; overflow: hidden; color: var(--muted); font-size: 9px; text-overflow: ellipsis; white-space: nowrap; }
|
|
1483
1490
|
.ai-heading-actions button { min-width: 28px; height: 28px; padding: 0 7px; border: 1px solid var(--line); border-radius: 4px; background: transparent; color: var(--muted); font-size: 9px; }
|
|
1491
|
+
.ai-heading-action-icon { display: block; width: 15px; height: 15px; margin: auto; fill: none; stroke: currentColor; stroke-linecap: round; stroke-linejoin: round; stroke-width: 1.8; }
|
|
1484
1492
|
.ai-heading-actions #ai-new-conversation { padding: 0; font: 17px/1 var(--font-latin), monospace; }
|
|
1485
1493
|
.ai-heading-actions button:hover, .ai-heading-actions button[aria-expanded="true"] { background: var(--paper-deep); color: var(--ink); }
|
|
1486
1494
|
.status-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--green); box-shadow: 0 0 0 3px rgba(94,118,92,.14); }
|
|
@@ -1740,6 +1748,36 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
|
|
|
1740
1748
|
.task-chapter-field { transition: opacity .15s ease; }
|
|
1741
1749
|
.task-chapter-field.is-disabled { opacity: .48; }
|
|
1742
1750
|
.task-chapter-field select:disabled { cursor: not-allowed; }
|
|
1751
|
+
.relationship-analysis-options { display: grid; gap: 14px; }
|
|
1752
|
+
.relationship-character-field { min-width: 0; }
|
|
1753
|
+
.relationship-character-picker { display: grid; gap: 8px; min-width: 0; }
|
|
1754
|
+
.dialog-fields .relationship-character-trigger { display: flex; width: 100%; min-height: 42px; align-items: center; justify-content: space-between; gap: 12px; padding: 9px 11px; border: 1px solid var(--line); border-radius: 4px; background: var(--surface); color: var(--ink); font-size: 12px; text-align: left; }
|
|
1755
|
+
.relationship-character-trigger:hover, .relationship-character-trigger:focus-visible, .relationship-character-trigger[aria-expanded="true"] { border-color: var(--accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 12%, transparent); outline: none; }
|
|
1756
|
+
.relationship-character-trigger:disabled { cursor: not-allowed; opacity: .48; }
|
|
1757
|
+
.relationship-character-trigger > span:first-child { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1758
|
+
.relationship-character-trigger-meta { display: inline-flex; flex: 0 0 auto; align-items: center; gap: 8px; color: var(--muted); font-family: var(--font-latin), monospace; font-size: 10px; }
|
|
1759
|
+
.relationship-character-chevron { font-size: 15px; line-height: 1; transition: transform .15s ease; }
|
|
1760
|
+
.relationship-character-trigger[aria-expanded="true"] .relationship-character-chevron { transform: rotate(180deg); }
|
|
1761
|
+
.relationship-character-bubble { position: relative; display: grid; gap: 9px; padding: 12px; border: 1px solid color-mix(in srgb, var(--accent) 36%, var(--line)); border-radius: 8px; background: var(--surface-soft); box-shadow: 0 10px 30px rgba(48,39,31,.12); }
|
|
1762
|
+
.relationship-character-bubble::before { position: absolute; top: -6px; right: 20px; width: 10px; height: 10px; border-top: 1px solid color-mix(in srgb, var(--accent) 36%, var(--line)); border-left: 1px solid color-mix(in srgb, var(--accent) 36%, var(--line)); background: var(--surface-soft); content: ""; transform: rotate(45deg); }
|
|
1763
|
+
.dialog-fields .relationship-character-search { gap: 5px; }
|
|
1764
|
+
.dialog-fields .relationship-character-search input { min-height: 36px; padding: 7px 9px; background: var(--surface); }
|
|
1765
|
+
.relationship-character-bubble-meta { display: flex; align-items: center; justify-content: space-between; gap: 10px; color: var(--muted); font-size: 9px; }
|
|
1766
|
+
.relationship-character-bubble-meta button { padding: 2px 0; border: 0; background: transparent; color: var(--accent-dark); font-size: 9px; }
|
|
1767
|
+
.relationship-character-bubble-meta button:disabled { color: var(--muted); cursor: default; opacity: .45; }
|
|
1768
|
+
.relationship-character-options { display: flex; flex-wrap: wrap; gap: 7px; max-height: 220px; overflow-y: auto; padding: 2px 2px 2px 0; }
|
|
1769
|
+
.dialog-fields .relationship-character-chip { position: relative; display: inline-flex; width: auto; cursor: pointer; }
|
|
1770
|
+
.relationship-character-chip input { position: absolute; width: 1px !important; min-width: 1px; height: 1px; opacity: 0; }
|
|
1771
|
+
.relationship-character-chip span { display: inline-flex; align-items: center; min-height: 30px; padding: 5px 10px; border: 1px solid var(--line); border-radius: 16px; background: var(--panel); color: var(--muted); font-size: 10px; transition: background .14s ease, border-color .14s ease, color .14s ease; }
|
|
1772
|
+
.relationship-character-chip input:checked + span { border-color: var(--accent); background: var(--accent); color: #fff; }
|
|
1773
|
+
.relationship-character-chip input:focus-visible + span { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
1774
|
+
.relationship-character-empty { margin: 0; padding: 18px 8px; color: var(--muted); font-size: 10px; text-align: center; }
|
|
1775
|
+
.relationship-analysis-helper { display: flex; gap: 7px; align-items: flex-start; margin: -7px 2px 0; color: var(--muted); font-size: 10px; line-height: 1.55; }
|
|
1776
|
+
.relationship-analysis-helper > span:first-child { display: inline-grid; flex: 0 0 16px; width: 16px; height: 16px; margin-top: 1px; place-items: center; border: 1px solid var(--line); border-radius: 50%; color: var(--accent-dark); font-family: var(--font-latin), monospace; font-size: 9px; font-style: normal; font-weight: 700; line-height: 1; }
|
|
1777
|
+
.relationship-overwrite-card { display: grid; gap: 5px; padding: 11px 12px; border: 1px solid var(--line); border-radius: 5px; background: var(--surface-soft); transition: border-color .15s ease, background .15s ease, opacity .15s ease; }
|
|
1778
|
+
.relationship-overwrite-card .checkbox-field { color: var(--ink); font-size: 11px; font-weight: 600; }
|
|
1779
|
+
.relationship-overwrite-card > p { margin: 0 0 0 25px; color: var(--muted); font-size: 10px; line-height: 1.55; }
|
|
1780
|
+
.relationship-overwrite-card:has(input:checked) { border-color: color-mix(in srgb, var(--accent) 62%, var(--line)); background: color-mix(in srgb, var(--accent) 7%, var(--surface)); }
|
|
1743
1781
|
.model-temperature-hint { margin: 0; color: var(--accent-dark); font-size: 10px; line-height: 1.55; }
|
|
1744
1782
|
.item-list-rows { display: grid; gap: 7px; }
|
|
1745
1783
|
.item-list-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 7px; }
|
package/dist/store.js
CHANGED
|
@@ -3607,6 +3607,15 @@ export class Store {
|
|
|
3607
3607
|
const timestamp = now();
|
|
3608
3608
|
const scope = input.scope ?? { type: "book" };
|
|
3609
3609
|
const sourceVersions = {};
|
|
3610
|
+
if (Array.isArray(scope.characterIds)) {
|
|
3611
|
+
for (const characterId of scope.characterIds) {
|
|
3612
|
+
if (typeof characterId !== "string")
|
|
3613
|
+
throw new AppError(400, "CHARACTER_REQUIRED", "被分析角色标识无效");
|
|
3614
|
+
const character = this.getCharacter(characterId);
|
|
3615
|
+
if (character.workId !== workId)
|
|
3616
|
+
throw new AppError(400, "CHARACTER_WORK_MISMATCH", "被分析角色不属于当前作品");
|
|
3617
|
+
}
|
|
3618
|
+
}
|
|
3610
3619
|
if (typeof scope.chapterId === "string") {
|
|
3611
3620
|
const chapter = this.getChapter(scope.chapterId);
|
|
3612
3621
|
if (chapter.workId !== workId)
|
|
@@ -3759,17 +3768,23 @@ export class Store {
|
|
|
3759
3768
|
};
|
|
3760
3769
|
}
|
|
3761
3770
|
taskScopeSummaryFromMaps(scope, chapterSummaries, volumeTitles) {
|
|
3771
|
+
const targetedSuffix = Array.isArray(scope.characterIds) && scope.characterIds.length
|
|
3772
|
+
? ` · 定向 ${scope.characterIds.length} 人${scope.replaceExistingRelationships === true ? " · 覆盖已有关系" : ""}`
|
|
3773
|
+
: "";
|
|
3762
3774
|
if (typeof scope.chapterId === "string")
|
|
3763
|
-
return chapterSummaries.get(scope.chapterId) ?? "章节已删除"
|
|
3775
|
+
return `${chapterSummaries.get(scope.chapterId) ?? "章节已删除"}${targetedSuffix}`;
|
|
3764
3776
|
if (scope.type === "volume" && typeof scope.volumeId === "string") {
|
|
3765
3777
|
const title = volumeTitles.get(scope.volumeId);
|
|
3766
|
-
return title ? `分卷 · ${title}` : "分卷已删除";
|
|
3778
|
+
return title ? `分卷 · ${title}${targetedSuffix}` : "分卷已删除";
|
|
3767
3779
|
}
|
|
3768
3780
|
if (scope.type === "book" || Object.keys(scope).length === 0)
|
|
3769
|
-
return "全书"
|
|
3781
|
+
return `${scope.includeAllSettings === true ? "全书 + 所有设定" : "全书"}${targetedSuffix}`;
|
|
3770
3782
|
return "未指定范围";
|
|
3771
3783
|
}
|
|
3772
3784
|
taskScopeSummary(workId, scope) {
|
|
3785
|
+
const targetedSuffix = Array.isArray(scope.characterIds) && scope.characterIds.length
|
|
3786
|
+
? ` · 定向 ${scope.characterIds.length} 人${scope.replaceExistingRelationships === true ? " · 覆盖已有关系" : ""}`
|
|
3787
|
+
: "";
|
|
3773
3788
|
if (typeof scope.chapterId === "string") {
|
|
3774
3789
|
const chapter = this.db.get(`SELECT chapter.title AS title, volume.title AS volume_title
|
|
3775
3790
|
FROM chapters chapter
|
|
@@ -3779,14 +3794,14 @@ export class Store {
|
|
|
3779
3794
|
return "章节已删除";
|
|
3780
3795
|
const title = requiredString(chapter, "title");
|
|
3781
3796
|
const volumeTitle = requiredString(chapter, "volume_title");
|
|
3782
|
-
return `${volumeTitle} · ${title}`;
|
|
3797
|
+
return `${volumeTitle} · ${title}${targetedSuffix}`;
|
|
3783
3798
|
}
|
|
3784
3799
|
if (scope.type === "volume" && typeof scope.volumeId === "string") {
|
|
3785
3800
|
const volume = this.db.get("SELECT title FROM volumes WHERE id = ? AND work_id = ?", scope.volumeId, workId);
|
|
3786
|
-
return volume ? `分卷 · ${requiredString(volume, "title")}` : "分卷已删除";
|
|
3801
|
+
return volume ? `分卷 · ${requiredString(volume, "title")}${targetedSuffix}` : "分卷已删除";
|
|
3787
3802
|
}
|
|
3788
3803
|
if (scope.type === "book" || Object.keys(scope).length === 0)
|
|
3789
|
-
return "全书"
|
|
3804
|
+
return `${scope.includeAllSettings === true ? "全书 + 所有设定" : "全书"}${targetedSuffix}`;
|
|
3790
3805
|
return "未指定范围";
|
|
3791
3806
|
}
|
|
3792
3807
|
taskScopeDetails(workId, scope) {
|