mirror of
https://github.com/Wx-2025/ST-Amily2-Chat-Optimisation.git
synced 2026-06-06 13:55:51 +00:00
ci: auto build & obfuscate [2026-05-16 19:16:28] (Jenkins #21)
This commit is contained in:
47
core/api.js
47
core/api.js
@@ -441,7 +441,7 @@ async function fetchSillyTavernPresetModels() {
|
||||
export async function getApiSettings(slot = 'main') {
|
||||
const s = extension_settings[extensionName] || {};
|
||||
|
||||
// 优先读取槽位分配的 Profile(仅接管连接参数)
|
||||
// 优先读取槽位分配的 Profile(profile 一旦分配即为权威,不再被主面板/模块独立设置压制)
|
||||
const profile = await getSlotProfile(slot);
|
||||
if (profile) {
|
||||
const resolvedProvider = profile.provider === 'sillytavern_backend'
|
||||
@@ -453,10 +453,10 @@ export async function getApiSettings(slot = 'main') {
|
||||
apiUrl: profile.apiUrl,
|
||||
apiKey: profile.apiKey ?? '',
|
||||
model: profile.model,
|
||||
// 温度 / MaxTokens 读面板值(profile-sync 保留了这些输入框)
|
||||
maxTokens: s.maxTokens ?? profile.maxTokens ?? 65500,
|
||||
temperature: s.temperature ?? profile.temperature ?? 1.0,
|
||||
maxTokens: profile.maxTokens ?? 65500,
|
||||
temperature: profile.temperature ?? 1.0,
|
||||
fakeStream: profile.fakeStream ?? false,
|
||||
customParams: profile.customParams ?? {},
|
||||
tavernProfile: '',
|
||||
};
|
||||
}
|
||||
@@ -588,7 +588,10 @@ export async function callAI(messages, options = {}) {
|
||||
apiUrl: apiSettings.apiUrl,
|
||||
apiKey: apiSettings.apiKey,
|
||||
apiProvider: apiSettings.apiProvider,
|
||||
...options
|
||||
customParams: apiSettings.customParams ?? {},
|
||||
...options,
|
||||
// options 可显式覆盖 customParams,体现"代码内显式 > profile 配置"
|
||||
customParams: { ...(apiSettings.customParams ?? {}), ...(options.customParams ?? {}) },
|
||||
};
|
||||
|
||||
if (finalOptions.apiProvider !== 'sillytavern_preset') {
|
||||
@@ -680,11 +683,14 @@ async function callOpenAICompatible(messages, options) {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
// 用户自定义参数(profile.customParams + 显式 options.customParams 已在 callAI 合并)
|
||||
...(options.customParams || {}),
|
||||
// 表单托管的核心字段总是覆盖 customParams
|
||||
model: options.model,
|
||||
messages: messages,
|
||||
max_tokens: options.maxTokens,
|
||||
temperature: options.temperature,
|
||||
stream: false
|
||||
stream: false,
|
||||
})
|
||||
});
|
||||
|
||||
@@ -699,6 +705,21 @@ async function callOpenAICompatible(messages, options) {
|
||||
|
||||
async function callOpenAITest(messages, options) {
|
||||
const body = {
|
||||
// 1. 可调默认值(用户 customParams 可覆盖)
|
||||
top_p: options.top_p || 1,
|
||||
frequency_penalty: 0,
|
||||
presence_penalty: 0.12,
|
||||
include_reasoning: false,
|
||||
reasoning_effort: 'medium',
|
||||
enable_web_search: false,
|
||||
request_images: false,
|
||||
custom_prompt_post_processing: 'strict',
|
||||
group_names: [],
|
||||
|
||||
// 2. 用户 customParams 覆盖上层默认值
|
||||
...(options.customParams || {}),
|
||||
|
||||
// 3. 表单托管的核心字段总是 win
|
||||
chat_completion_source: 'openai',
|
||||
messages: messages,
|
||||
model: options.model,
|
||||
@@ -707,15 +728,6 @@ async function callOpenAITest(messages, options) {
|
||||
stream: false,
|
||||
max_tokens: options.maxTokens || 30000,
|
||||
temperature: options.temperature || 1,
|
||||
top_p: options.top_p || 1,
|
||||
custom_prompt_post_processing: 'strict',
|
||||
enable_web_search: false,
|
||||
frequency_penalty: 0,
|
||||
group_names: [],
|
||||
include_reasoning: false,
|
||||
presence_penalty: 0.12,
|
||||
reasoning_effort: 'medium',
|
||||
request_images: false,
|
||||
};
|
||||
|
||||
const response = await fetch('/api/backends/chat-completions/generate', {
|
||||
@@ -816,6 +828,9 @@ async function callSillyTavernBackend(messages, options) {
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({
|
||||
// 用户 customParams(可被核心字段覆盖)
|
||||
...(options.customParams || {}),
|
||||
// 表单托管字段总是 win
|
||||
chat_completion_source: 'custom',
|
||||
custom_url: options.apiUrl,
|
||||
api_key: options.apiKey,
|
||||
@@ -823,7 +838,7 @@ async function callSillyTavernBackend(messages, options) {
|
||||
messages: messages,
|
||||
max_tokens: options.maxTokens,
|
||||
temperature: options.temperature,
|
||||
stream: false
|
||||
stream: false,
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
@@ -3,11 +3,12 @@ import { getRequestHeaders } from "/script.js";
|
||||
import { extensionName } from "../../utils/settings.js";
|
||||
import { getSlotProfile, providerToApiMode } from './api-resolver.js';
|
||||
import { configManager } from '../../utils/config/ConfigManager.js';
|
||||
import { detectVendor } from '../../utils/api-vendor.js';
|
||||
|
||||
async function getConcurrentApiSettings() {
|
||||
const s = extension_settings[extensionName] || {};
|
||||
|
||||
// 优先读取槽位分配的 Profile(仅接管连接参数)
|
||||
// 优先读取槽位分配的 Profile(profile 一旦分配即权威,slider 残值不再覆盖)
|
||||
const profile = await getSlotProfile('plotOptConc');
|
||||
if (profile) {
|
||||
return {
|
||||
@@ -15,8 +16,7 @@ async function getConcurrentApiSettings() {
|
||||
apiUrl: profile.apiUrl,
|
||||
apiKey: profile.apiKey ?? '',
|
||||
model: profile.model,
|
||||
// MaxTokens 读面板值
|
||||
maxTokens: s.plotOpt_concurrentMaxTokens ?? profile.maxTokens ?? 8100,
|
||||
maxTokens: profile.maxTokens ?? 8100,
|
||||
temperature: profile.temperature ?? 1,
|
||||
};
|
||||
}
|
||||
@@ -47,7 +47,7 @@ export async function callConcurrentAI(messages, options = {}) {
|
||||
|
||||
if (!finalOptions.apiUrl || !finalOptions.model || !finalOptions.apiKey) {
|
||||
console.warn("[Amily2-Concurrent外交部] API配置不完整,无法调用AI");
|
||||
toastr.error("并发API配置不完整,请检查URL、Key和模型配置。", "Concurrent-外交部");
|
||||
toastr.error("并发剧情优化(plotOptConc)未配置 API 连接配置,请前往 API 连接配置面板分配 profile 或填写并发优化独立设置。", "Amily2-并发优化未配置");
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ export async function callConcurrentAI(messages, options = {}) {
|
||||
}
|
||||
|
||||
async function callConcurrentOpenAITest(messages, options) {
|
||||
const isGoogleApi = options.apiUrl.includes('googleapis.com');
|
||||
const isGoogleApi = (await detectVendor(options.apiUrl)) === 'google';
|
||||
|
||||
const body = {
|
||||
chat_completion_source: 'openai',
|
||||
|
||||
@@ -4,6 +4,7 @@ import { extensionName } from "../../utils/settings.js";
|
||||
import { amilyHelper } from '../../core/tavern-helper/main.js';
|
||||
import { getSlotProfile, providerToApiMode } from './api-resolver.js';
|
||||
import { configManager } from '../../utils/config/ConfigManager.js';
|
||||
import { detectVendor } from '../../utils/api-vendor.js';
|
||||
|
||||
let ChatCompletionService = undefined;
|
||||
try {
|
||||
@@ -47,7 +48,7 @@ function normalizeApiResponse(responseData) {
|
||||
export async function getJqyhApiSettings() {
|
||||
const s = extension_settings[extensionName] || {};
|
||||
|
||||
// JQYH 与剧情优化互斥,共用 'plotOpt' 槽位
|
||||
// JQYH 与剧情优化互斥,共用 'plotOpt' 槽位(profile 一旦分配即权威,slider 残值不再覆盖)
|
||||
const profile = await getSlotProfile('plotOpt');
|
||||
if (profile) {
|
||||
return {
|
||||
@@ -55,9 +56,9 @@ export async function getJqyhApiSettings() {
|
||||
apiUrl: profile.apiUrl,
|
||||
apiKey: profile.apiKey ?? '',
|
||||
model: profile.model,
|
||||
// 温度 / MaxTokens 读面板值
|
||||
maxTokens: s.jqyhMaxTokens ?? profile.maxTokens ?? 65500,
|
||||
temperature: s.jqyhTemperature ?? profile.temperature ?? 1.0,
|
||||
maxTokens: profile.maxTokens ?? 65500,
|
||||
temperature: profile.temperature ?? 1.0,
|
||||
customParams: profile.customParams ?? {},
|
||||
tavernProfile: '',
|
||||
};
|
||||
}
|
||||
@@ -70,6 +71,7 @@ export async function getJqyhApiSettings() {
|
||||
model: s.jqyhModel || '',
|
||||
maxTokens: s.jqyhMaxTokens || 4000,
|
||||
temperature: s.jqyhTemperature || 0.7,
|
||||
customParams: {},
|
||||
tavernProfile: s.jqyhTavernProfile || '',
|
||||
};
|
||||
}
|
||||
@@ -96,7 +98,7 @@ export async function callJqyhAI(messages, options = {}) {
|
||||
if (finalOptions.apiMode !== 'sillytavern_preset') {
|
||||
if (!finalOptions.apiUrl || !finalOptions.model || !finalOptions.apiKey) {
|
||||
console.warn("[Amily2-Jqyh外交部] API配置不完整,无法调用AI");
|
||||
toastr.error("API配置不完整,请检查URL、Key和模型配置。", "Jqyh-外交部");
|
||||
toastr.error("剧情优化前置(JQYH)未配置 API 连接配置,请前往 API 连接配置面板分配 profile 或填写 JQYH 独立设置。", "Amily2-JQYH 未配置");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -160,9 +162,11 @@ export async function callJqyhAI(messages, options = {}) {
|
||||
}
|
||||
|
||||
async function callJqyhOpenAITest(messages, options) {
|
||||
const isGoogleApi = options.apiUrl.includes('googleapis.com');
|
||||
const isGoogleApi = (await detectVendor(options.apiUrl)) === 'google';
|
||||
|
||||
const body = {
|
||||
top_p: options.top_p || 1,
|
||||
...(options.customParams || {}),
|
||||
chat_completion_source: 'openai',
|
||||
messages: messages,
|
||||
model: options.model,
|
||||
@@ -171,7 +175,6 @@ async function callJqyhOpenAITest(messages, options) {
|
||||
stream: false,
|
||||
max_tokens: options.maxTokens || 30000,
|
||||
temperature: options.temperature || 1,
|
||||
top_p: options.top_p || 1,
|
||||
};
|
||||
|
||||
if (!isGoogleApi) {
|
||||
@@ -245,7 +248,8 @@ async function callJqyhSillyTavernPreset(messages, options) {
|
||||
responsePromise = context.ConnectionManagerRequestService.sendRequest(
|
||||
targetProfile.id,
|
||||
messages,
|
||||
options.maxTokens || 4000
|
||||
options.maxTokens || 4000,
|
||||
options.customParams || {}
|
||||
);
|
||||
|
||||
} finally {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { extensionName } from "../../utils/settings.js";
|
||||
import { amilyHelper } from '../../core/tavern-helper/main.js';
|
||||
import { getSlotProfile, providerToApiMode } from './api-resolver.js';
|
||||
import { configManager } from '../../utils/config/ConfigManager.js';
|
||||
import { detectVendor } from '../../utils/api-vendor.js';
|
||||
|
||||
let ChatCompletionService = undefined;
|
||||
try {
|
||||
@@ -41,7 +42,7 @@ if (window.Amily2Bus) {
|
||||
export async function getNccsApiSettings() {
|
||||
const s = extension_settings[extensionName] || {};
|
||||
|
||||
// 优先读取 'nccs' 槽位分配的 Profile(仅接管连接参数)
|
||||
// 优先读取 'nccs' 槽位分配的 Profile(profile 一旦分配即权威,旧 slider 残值不再覆盖)
|
||||
const profile = await getSlotProfile('nccs');
|
||||
if (profile) {
|
||||
return {
|
||||
@@ -50,9 +51,9 @@ export async function getNccsApiSettings() {
|
||||
apiUrl: profile.apiUrl,
|
||||
apiKey: profile.apiKey ?? '',
|
||||
model: profile.model,
|
||||
// 温度 / MaxTokens / FakeStream 读面板值(profile-sync 保留了这些输入框)
|
||||
maxTokens: s.nccsMaxTokens ?? profile.maxTokens ?? 65500,
|
||||
temperature: s.nccsTemperature ?? profile.temperature ?? 1.0,
|
||||
maxTokens: profile.maxTokens ?? 65500,
|
||||
temperature: profile.temperature ?? 1.0,
|
||||
customParams: profile.customParams ?? {},
|
||||
tavernProfile: '',
|
||||
useFakeStream: s.nccsFakeStreamEnabled ?? false,
|
||||
};
|
||||
@@ -67,6 +68,7 @@ export async function getNccsApiSettings() {
|
||||
model: s.nccsModel || '',
|
||||
maxTokens: s.nccsMaxTokens ?? 8192,
|
||||
temperature: s.nccsTemperature ?? 1,
|
||||
customParams: {},
|
||||
tavernProfile: s.nccsTavernProfile || '',
|
||||
useFakeStream: s.nccsFakeStreamEnabled || false,
|
||||
};
|
||||
@@ -94,7 +96,7 @@ export async function callNccsAI(messages, options = {}) {
|
||||
if (finalOptions.apiMode !== 'sillytavern_preset') {
|
||||
if (!finalOptions.apiUrl || !finalOptions.model || !finalOptions.apiKey) {
|
||||
console.warn("[Amily2-Nccs外交部] API配置不完整,无法调用AI");
|
||||
toastr.error("API配置不完整,请检查URL、Key和模型配置。", "Nccs-外交部");
|
||||
toastr.error("并发模块(NCCS)未配置 API 连接配置,请前往 API 连接配置面板分配 profile 或填写 NCCS 独立设置。", "Amily2-NCCS 未配置");
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
@@ -187,8 +189,10 @@ function normalizeApiResponse(responseData) {
|
||||
}
|
||||
|
||||
async function callNccsOpenAITest(messages, options) {
|
||||
const isGoogleApi = options.apiUrl.includes('googleapis.com');
|
||||
const isGoogleApi = (await detectVendor(options.apiUrl)) === 'google';
|
||||
const body = {
|
||||
top_p: options.top_p || 1,
|
||||
...(options.customParams || {}),
|
||||
chat_completion_source: 'openai',
|
||||
messages: messages,
|
||||
model: options.model,
|
||||
@@ -197,7 +201,6 @@ async function callNccsOpenAITest(messages, options) {
|
||||
stream: !!options.stream,
|
||||
max_tokens: 8192,
|
||||
temperature: 1,
|
||||
top_p: options.top_p || 1,
|
||||
};
|
||||
|
||||
if (!isGoogleApi) {
|
||||
@@ -244,7 +247,8 @@ async function callNccsSillyTavernPreset(messages, options) {
|
||||
const result = await context.ConnectionManagerRequestService.sendRequest(
|
||||
targetProfile.id,
|
||||
messages,
|
||||
8192
|
||||
8192,
|
||||
options.customParams || {}
|
||||
);
|
||||
|
||||
return normalizeApiResponse(result);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { extensionName } from "../../utils/settings.js";
|
||||
import { amilyHelper } from '../../core/tavern-helper/main.js';
|
||||
import { getSlotProfile, providerToApiMode } from './api-resolver.js';
|
||||
import { configManager } from '../../utils/config/ConfigManager.js';
|
||||
import { detectVendor } from '../../utils/api-vendor.js';
|
||||
|
||||
let ChatCompletionService = undefined;
|
||||
try {
|
||||
@@ -47,7 +48,7 @@ function normalizeApiResponse(responseData) {
|
||||
export async function getNgmsApiSettings() {
|
||||
const s = extension_settings[extensionName] || {};
|
||||
|
||||
// 优先读取 'ngms' 槽位分配的 Profile(仅接管连接参数)
|
||||
// 优先读取 'ngms' 槽位分配的 Profile(profile 一旦分配即权威,旧 slider 残值不再覆盖)
|
||||
const profile = await getSlotProfile('ngms');
|
||||
if (profile) {
|
||||
return {
|
||||
@@ -55,9 +56,9 @@ export async function getNgmsApiSettings() {
|
||||
apiUrl: profile.apiUrl,
|
||||
apiKey: profile.apiKey ?? '',
|
||||
model: profile.model,
|
||||
// 温度 / MaxTokens / FakeStream 读面板值
|
||||
maxTokens: s.ngmsMaxTokens ?? profile.maxTokens ?? 65500,
|
||||
temperature: s.ngmsTemperature ?? profile.temperature ?? 1.0,
|
||||
maxTokens: profile.maxTokens ?? 65500,
|
||||
temperature: profile.temperature ?? 1.0,
|
||||
customParams: profile.customParams ?? {},
|
||||
tavernProfile: '',
|
||||
useFakeStream: s.ngmsFakeStreamEnabled ?? false,
|
||||
};
|
||||
@@ -71,6 +72,7 @@ export async function getNgmsApiSettings() {
|
||||
model: s.ngmsModel || '',
|
||||
maxTokens: s.ngmsMaxTokens ?? 30000,
|
||||
temperature: s.ngmsTemperature ?? 1.0,
|
||||
customParams: {},
|
||||
tavernProfile: s.ngmsTavernProfile || '',
|
||||
useFakeStream: s.ngmsFakeStreamEnabled || false,
|
||||
};
|
||||
@@ -101,7 +103,7 @@ export async function callNgmsAI(messages, options = {}) {
|
||||
if (finalOptions.apiMode !== 'sillytavern_preset') {
|
||||
if (!finalOptions.apiUrl || !finalOptions.model || !finalOptions.apiKey) {
|
||||
console.warn("[Amily2-Ngms外交部] API配置不完整,无法调用AI");
|
||||
toastr.error("API配置不完整,请检查URL、Key和模型配置。", "Ngms-外交部");
|
||||
toastr.error("总结模块(NGMS)未配置 API 连接配置,请前往 API 连接配置面板分配 profile 或填写 NGMS 独立设置。", "Amily2-NGMS 未配置");
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
@@ -221,9 +223,11 @@ async function fetchFakeStream(url, opts) {
|
||||
}
|
||||
|
||||
async function callNgmsOpenAITest(messages, options) {
|
||||
const isGoogleApi = options.apiUrl.includes('googleapis.com');
|
||||
const isGoogleApi = (await detectVendor(options.apiUrl)) === 'google';
|
||||
|
||||
const body = {
|
||||
top_p: options.top_p || 1,
|
||||
...(options.customParams || {}),
|
||||
chat_completion_source: 'openai',
|
||||
messages: messages,
|
||||
model: options.model,
|
||||
@@ -232,7 +236,6 @@ async function callNgmsOpenAITest(messages, options) {
|
||||
stream: !!options.stream,
|
||||
max_tokens: options.maxTokens || 30000,
|
||||
temperature: options.temperature || 1,
|
||||
top_p: options.top_p || 1,
|
||||
};
|
||||
|
||||
if (!isGoogleApi) {
|
||||
@@ -312,7 +315,8 @@ async function callNgmsSillyTavernPreset(messages, options) {
|
||||
responsePromise = context.ConnectionManagerRequestService.sendRequest(
|
||||
targetProfile.id,
|
||||
messages,
|
||||
options.maxTokens || 4000
|
||||
options.maxTokens || 4000,
|
||||
options.customParams || {}
|
||||
);
|
||||
|
||||
} finally {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { extensionName } from "../../utils/settings.js";
|
||||
import { amilyHelper } from '../../core/tavern-helper/main.js';
|
||||
import { getSlotProfile, providerToApiMode } from './api-resolver.js';
|
||||
import { configManager } from '../../utils/config/ConfigManager.js';
|
||||
import { detectVendor } from '../../utils/api-vendor.js';
|
||||
|
||||
let ChatCompletionService = undefined;
|
||||
try {
|
||||
@@ -47,7 +48,7 @@ function normalizeApiResponse(responseData) {
|
||||
export async function getSybdApiSettings() {
|
||||
const s = extension_settings[extensionName] || {};
|
||||
|
||||
// 优先读取 'sybd' 槽位分配的 Profile
|
||||
// 优先读取 'sybd' 槽位分配的 Profile(profile 一旦分配即权威,slider 残值不再覆盖)
|
||||
const profile = await getSlotProfile('sybd');
|
||||
if (profile) {
|
||||
return {
|
||||
@@ -55,8 +56,9 @@ export async function getSybdApiSettings() {
|
||||
apiUrl: profile.apiUrl,
|
||||
apiKey: profile.apiKey ?? '',
|
||||
model: profile.model,
|
||||
maxTokens: s.sybdMaxTokens ?? profile.maxTokens ?? 4000,
|
||||
temperature: s.sybdTemperature ?? profile.temperature ?? 0.7,
|
||||
maxTokens: profile.maxTokens ?? 4000,
|
||||
temperature: profile.temperature ?? 0.7,
|
||||
customParams: profile.customParams ?? {},
|
||||
tavernProfile: '',
|
||||
};
|
||||
}
|
||||
@@ -69,6 +71,7 @@ export async function getSybdApiSettings() {
|
||||
model: s.sybdModel || '',
|
||||
maxTokens: s.sybdMaxTokens || 4000,
|
||||
temperature: s.sybdTemperature || 0.7,
|
||||
customParams: {},
|
||||
tavernProfile: s.sybdTavernProfile || '',
|
||||
};
|
||||
}
|
||||
@@ -95,7 +98,7 @@ export async function callSybdAI(messages, options = {}) {
|
||||
if (finalOptions.apiMode !== 'sillytavern_preset') {
|
||||
if (!finalOptions.apiUrl || !finalOptions.model || !finalOptions.apiKey) {
|
||||
console.warn("[Amily2-Sybd外交部] API配置不完整,无法调用AI");
|
||||
toastr.error("API配置不完整,请检查URL、Key和模型配置。", "Sybd-外交部");
|
||||
toastr.error("术语表填写(SYBD)未配置 API 连接配置,请前往 API 连接配置面板分配 profile 或填写 SYBD 独立设置。", "Amily2-SYBD 未配置");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -159,9 +162,11 @@ export async function callSybdAI(messages, options = {}) {
|
||||
}
|
||||
|
||||
async function callSybdOpenAITest(messages, options) {
|
||||
const isGoogleApi = options.apiUrl.includes('googleapis.com');
|
||||
const isGoogleApi = (await detectVendor(options.apiUrl)) === 'google';
|
||||
|
||||
const body = {
|
||||
top_p: options.top_p || 1,
|
||||
...(options.customParams || {}),
|
||||
chat_completion_source: 'openai',
|
||||
messages: messages,
|
||||
model: options.model,
|
||||
@@ -170,7 +175,6 @@ async function callSybdOpenAITest(messages, options) {
|
||||
stream: false,
|
||||
max_tokens: options.maxTokens || 30000,
|
||||
temperature: options.temperature || 1,
|
||||
top_p: options.top_p || 1,
|
||||
};
|
||||
|
||||
if (!isGoogleApi) {
|
||||
@@ -244,7 +248,8 @@ async function callSybdSillyTavernPreset(messages, options) {
|
||||
responsePromise = context.ConnectionManagerRequestService.sendRequest(
|
||||
targetProfile.id,
|
||||
messages,
|
||||
options.maxTokens || 4000
|
||||
options.maxTokens || 4000,
|
||||
options.customParams || {}
|
||||
);
|
||||
|
||||
} finally {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { characters, this_chid, saveSettingsDebounced, getCharacters } from "/sc
|
||||
import { world_names } from "/scripts/world-info.js";
|
||||
import { getApiConfig, setApiConfig, testConnection, fetchModels } from "./api.js";
|
||||
import { tools } from "./tools.js";
|
||||
import { syncSlot } from "../../ui/profile-sync.js";
|
||||
|
||||
const extensionFolderPath = `scripts/extensions/third-party/${extensionName}`;
|
||||
|
||||
@@ -42,6 +43,7 @@ export async function openAutoCharCardWindow() {
|
||||
try {
|
||||
populateDropdowns();
|
||||
loadApiSettings();
|
||||
await syncSlot('autoCharCard');
|
||||
renderRulesList();
|
||||
renderSessionsList();
|
||||
restoreChatHistory();
|
||||
|
||||
@@ -15,7 +15,7 @@ import { compatibleWriteToLorebook } from "./tavernhelper-compatibility.js";
|
||||
import { ingestTextToHanlinyuan } from "./rag-processor.js";
|
||||
import { showSummaryModal, showHtmlModal } from "../ui/page-window.js";
|
||||
import { getPresetPrompts, getMixedOrder } from '../PresetSettings/index.js';
|
||||
import { callAI, generateRandomSeed } from "./api.js";
|
||||
import { generateRandomSeed } from "./api.js";
|
||||
import { callNgmsAI } from "./api/Ngms_api.js";
|
||||
import { executeAutoHide } from "./autoHideManager.js";
|
||||
import { resolveHistoriographyRuleConfig } from "../utils/config/RuleProfileManager.js";
|
||||
@@ -384,7 +384,9 @@ async function getSummary(formattedHistory, toastTitle, retryCount = 0) {
|
||||
}
|
||||
}
|
||||
|
||||
const summary = settings.ngmsEnabled ? await callNgmsAI(messages) : await callAI(messages);
|
||||
// 历史总结统一走 NGMS slot;ngms 未配置时 callNgmsAI 自带模块名错误提示。
|
||||
// 旧 ngmsEnabled 三元式 fallback 到 main 的设计已在主 API 移除后失效。
|
||||
const summary = await callNgmsAI(messages);
|
||||
console.log('[大史官-微言录] AI回复的全部内容:', summary);
|
||||
|
||||
if (!summary || !summary.trim()) {
|
||||
@@ -603,7 +605,8 @@ export async function executeRefinement(worldbook, loreKey) {
|
||||
|
||||
const getRefinedContent = async (retryCount = 0) => {
|
||||
toastr.info("正在召唤模型进行内容精炼...", "宏史卷重铸");
|
||||
const content = settings.ngmsEnabled ? await callNgmsAI(messages) : await callAI(messages);
|
||||
// 历史总结统一走 NGMS slot;ngms 未配置时 callNgmsAI 自带错误提示。
|
||||
const content = await callNgmsAI(messages);
|
||||
|
||||
if (!content || !content.trim()) {
|
||||
const maxRetries = settings.historiographyMaxRetries ?? 2;
|
||||
|
||||
@@ -33,7 +33,7 @@ export async function getEmbedRetrievalSettings() {
|
||||
const profile = await getSlotProfile('ragEmbed');
|
||||
if (profile) {
|
||||
return {
|
||||
apiEndpoint: 'custom',
|
||||
apiEndpoint: profile.provider === 'google' ? 'google_direct' : 'custom',
|
||||
customApiUrl: profile.apiUrl,
|
||||
apiKey: profile.apiKey ?? '',
|
||||
embeddingModel: profile.model,
|
||||
|
||||
190
core/table-system/actions/applyOperations.js
Normal file
190
core/table-system/actions/applyOperations.js
Normal file
@@ -0,0 +1,190 @@
|
||||
/**
|
||||
* @file Action: applyOperations —— 表格操作推演核心。
|
||||
*
|
||||
* 输入:基准 state + Operation[]
|
||||
* 输出:新 state(深拷贝)+ Change[] 变更记录
|
||||
*
|
||||
* 不依赖任何 formatter / store / persistence —— 纯函数。
|
||||
* 所有 formatter (legacy / json / toolcall) 解析完都吐 Operation[] 给本函数。
|
||||
*
|
||||
* 历史来源:从 executor.js 中 insertRow / updateRow / deleteRow 三个内部函数
|
||||
* 抽出,行为完全等价。executeCommands 改造为:parse 文本 → ops → 调本函数。
|
||||
*
|
||||
* 关键行为约定(不要随便改,否则破坏老存档):
|
||||
* - 入参 state 不被修改;返回的 state 是 JSON 深拷贝
|
||||
* - updateRow 的 rowIndex 越界 → 自动转换为 insertRow(历史智能修正)
|
||||
* - deleteRow 是延迟删除:rowStatuses[rowIndex] = 'pending-deletion',行不实际从 rows 中移除
|
||||
* - insertRow 的 changes 用 type='update'(每个被填的单元格一条),不要发明 'insert'
|
||||
*
|
||||
* @typedef {import('../dto/Table.js').TableState} TableState
|
||||
* @typedef {import('../dto/Operation.js').Operation} Operation
|
||||
* @typedef {import('../dto/Operation.js').InsertRowOperation} InsertRowOperation
|
||||
* @typedef {import('../dto/Operation.js').UpdateRowOperation} UpdateRowOperation
|
||||
* @typedef {import('../dto/Operation.js').DeleteRowOperation} DeleteRowOperation
|
||||
* @typedef {import('../dto/Change.js').Change} Change
|
||||
*/
|
||||
|
||||
import { log } from '../logger.js';
|
||||
|
||||
/**
|
||||
* 在表格末尾插入一行。in-place mutation(调用方已 clone)。
|
||||
* @param {TableState} state
|
||||
* @param {number} tableIndex
|
||||
* @param {Object<string, string>} data
|
||||
* @returns {{ state: TableState, changes: Change[] }}
|
||||
*/
|
||||
function _insertRow(state, tableIndex, data) {
|
||||
if (!state[tableIndex]) {
|
||||
log(`AI指令错误:尝试在不存在的表格索引 ${tableIndex} 中插入行。`, 'error');
|
||||
return { state, changes: [] };
|
||||
}
|
||||
|
||||
if (typeof data !== 'object' || data === null) {
|
||||
log(`AI指令错误:insertRow 的 data 参数必须是对象,实际收到: ${typeof data} (${data})`, 'error');
|
||||
return { state, changes: [] };
|
||||
}
|
||||
|
||||
const table = state[tableIndex];
|
||||
const colCount = table.headers.length;
|
||||
const newRow = Array(colCount).fill('');
|
||||
/** @type {Change[]} */
|
||||
const changes = [];
|
||||
const newRowIndex = table.rows.length;
|
||||
|
||||
for (const colIndex in data) {
|
||||
const cIndex = parseInt(colIndex, 10);
|
||||
if (cIndex < colCount) {
|
||||
newRow[cIndex] = data[colIndex];
|
||||
changes.push({ type: 'update', tableIndex, rowIndex: newRowIndex, colIndex: cIndex });
|
||||
}
|
||||
}
|
||||
table.rows.push(newRow);
|
||||
|
||||
// 同步更新 rowStatuses
|
||||
if (!table.rowStatuses) {
|
||||
table.rowStatuses = Array(table.rows.length - 1).fill('normal');
|
||||
}
|
||||
table.rowStatuses.push('normal');
|
||||
|
||||
return { state, changes };
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新指定行。in-place mutation。
|
||||
* 历史智能修正:rowIndex 越界自动降级为 insertRow。
|
||||
* @param {TableState} state
|
||||
* @param {number} tableIndex
|
||||
* @param {number} rowIndex
|
||||
* @param {Object<string, string>} data
|
||||
* @returns {{ state: TableState, changes: Change[] }}
|
||||
*/
|
||||
function _updateRow(state, tableIndex, rowIndex, data) {
|
||||
if (!state[tableIndex]) {
|
||||
log(`AI指令错误:尝试更新不存在的表格 ${tableIndex}。`, 'error');
|
||||
return { state, changes: [] };
|
||||
}
|
||||
|
||||
if (typeof data !== 'object' || data === null) {
|
||||
log(`AI指令错误:updateRow 的 data 参数必须是对象,实际收到: ${typeof data} (${data})`, 'error');
|
||||
return { state, changes: [] };
|
||||
}
|
||||
|
||||
const table = state[tableIndex];
|
||||
|
||||
if (rowIndex >= table.rows.length) {
|
||||
log(`AI指令修正:updateRow 的行索引 ${rowIndex} 超出范围,自动转换为 insertRow。`, 'warn');
|
||||
return _insertRow(state, tableIndex, data);
|
||||
}
|
||||
|
||||
const row = table.rows[rowIndex];
|
||||
/** @type {Change[]} */
|
||||
const changes = [];
|
||||
for (const colIndex in data) {
|
||||
const cIndex = parseInt(colIndex, 10);
|
||||
if (cIndex < row.length) {
|
||||
row[cIndex] = data[colIndex];
|
||||
changes.push({ type: 'update', tableIndex, rowIndex, colIndex: cIndex });
|
||||
}
|
||||
}
|
||||
return { state, changes };
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记指定行为待删除(延迟删除)。in-place mutation。
|
||||
* 不从 rows 实际移除;commitPendingDeletions 才会真正 splice。
|
||||
* @param {TableState} state
|
||||
* @param {number} tableIndex
|
||||
* @param {number} rowIndex
|
||||
* @returns {{ state: TableState, changes: Change[] }}
|
||||
*/
|
||||
function _deleteRow(state, tableIndex, rowIndex) {
|
||||
const table = state[tableIndex];
|
||||
if (!table || !table.rows[rowIndex]) {
|
||||
log(`AI指令错误:尝试删除不存在的表格 ${tableIndex} 或行 ${rowIndex}。`, 'error');
|
||||
return { state, changes: [] };
|
||||
}
|
||||
|
||||
if (!table.rowStatuses) {
|
||||
table.rowStatuses = Array(table.rows.length).fill('normal');
|
||||
}
|
||||
|
||||
if (table.rowStatuses[rowIndex] !== 'pending-deletion') {
|
||||
table.rowStatuses[rowIndex] = 'pending-deletion';
|
||||
/** @type {Change[]} */
|
||||
const changes = [{ type: 'delete', tableIndex, rowIndex }];
|
||||
return { state, changes };
|
||||
}
|
||||
|
||||
return { state, changes: [] };
|
||||
}
|
||||
|
||||
/** @type {Object<string, (state: TableState, op: Operation) => { state: TableState, changes: Change[] }>} */
|
||||
const HANDLERS = {
|
||||
insertRow: (state, op) => _insertRow(state, op.tableIndex, /** @type {InsertRowOperation} */(op).data),
|
||||
updateRow: (state, op) => _updateRow(state, op.tableIndex, /** @type {UpdateRowOperation} */(op).rowIndex, /** @type {UpdateRowOperation} */(op).data),
|
||||
deleteRow: (state, op) => _deleteRow(state, op.tableIndex, /** @type {DeleteRowOperation} */(op).rowIndex),
|
||||
};
|
||||
|
||||
/**
|
||||
* 把一组操作推演到 state 上。
|
||||
*
|
||||
* @param {TableState} initialState
|
||||
* @param {Operation[]} operations
|
||||
* @returns {{ state: TableState, changes: Change[] }}
|
||||
*/
|
||||
export function applyOperations(initialState, operations) {
|
||||
if (!Array.isArray(operations) || operations.length === 0) {
|
||||
return { state: initialState, changes: [] };
|
||||
}
|
||||
|
||||
let state = JSON.parse(JSON.stringify(initialState));
|
||||
/** @type {Change[]} */
|
||||
let allChanges = [];
|
||||
|
||||
for (const op of operations) {
|
||||
if (!op || typeof op !== 'object' || typeof op.op !== 'string') {
|
||||
log(`跳过非法操作: ${JSON.stringify(op)}`, 'warn');
|
||||
continue;
|
||||
}
|
||||
const handler = HANDLERS[op.op];
|
||||
if (!handler) {
|
||||
log(`未知操作类型: ${op.op}`, 'error');
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const result = handler(state, op);
|
||||
state = result.state;
|
||||
if (result.changes && result.changes.length > 0) {
|
||||
allChanges = allChanges.concat(result.changes);
|
||||
}
|
||||
const opLabel = op.op + '(' + op.tableIndex
|
||||
+ (typeof (/** @type {any} */(op)).rowIndex === 'number' ? `, ${(/** @type {any} */(op)).rowIndex}` : '')
|
||||
+ ')';
|
||||
log(`成功推演操作: ${opLabel}`, 'success');
|
||||
} catch (e) {
|
||||
log(`推演操作 ${op.op} 时发生运行时错误: ${e.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
return { state, changes: allChanges };
|
||||
}
|
||||
@@ -125,8 +125,8 @@ async function callTableModel(messages) {
|
||||
}
|
||||
return result;
|
||||
} else {
|
||||
log('使用默认 API 进行表格填充...', 'info');
|
||||
const result = await callAI(messages);
|
||||
log('使用 tableFilling slot 进行表格填充...', 'info');
|
||||
const result = await callAI(messages, { slot: 'tableFilling' });
|
||||
if (!result) {
|
||||
throw new Error('API返回内容为空。');
|
||||
}
|
||||
|
||||
21
core/table-system/dto/Change.js
Normal file
21
core/table-system/dto/Change.js
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* applyOperations 推演完成后吐出的变更记录。供高亮、SuperMemory 同步、UI 刷新使用。
|
||||
*
|
||||
* 注意 type 只有 'update' 和 'delete' 两种 —— insertRow 在 executor.js 历史实现里
|
||||
* 也吐 type='update'(每个被填的单元格一条),不要发明 'insert' type。
|
||||
*
|
||||
* @typedef {Object} UpdateChange
|
||||
* @property {'update'} type
|
||||
* @property {number} tableIndex
|
||||
* @property {number} rowIndex
|
||||
* @property {number} colIndex
|
||||
*
|
||||
* @typedef {Object} DeleteChange
|
||||
* @property {'delete'} type
|
||||
* @property {number} tableIndex
|
||||
* @property {number} rowIndex
|
||||
*
|
||||
* @typedef {UpdateChange | DeleteChange} Change
|
||||
*/
|
||||
|
||||
export {};
|
||||
26
core/table-system/dto/Operation.js
Normal file
26
core/table-system/dto/Operation.js
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* LLM 输出的统一动作格式。无论 formatter 是 legacy / json / toolcall,
|
||||
* 解析完都吐 Operation[],下游 applyOperations 不关心来源。
|
||||
*
|
||||
* data 字段的 key 是列索引的字符串形式('0', '1', ...),与 executor.js 历史行为对齐。
|
||||
*
|
||||
* @typedef {Object} InsertRowOperation
|
||||
* @property {'insertRow'} op
|
||||
* @property {number} tableIndex
|
||||
* @property {Object<string, string>} data { [colIndex]: cellValue }
|
||||
*
|
||||
* @typedef {Object} UpdateRowOperation
|
||||
* @property {'updateRow'} op
|
||||
* @property {number} tableIndex
|
||||
* @property {number} rowIndex
|
||||
* @property {Object<string, string>} data
|
||||
*
|
||||
* @typedef {Object} DeleteRowOperation
|
||||
* @property {'deleteRow'} op
|
||||
* @property {number} tableIndex
|
||||
* @property {number} rowIndex
|
||||
*
|
||||
* @typedef {InsertRowOperation | UpdateRowOperation | DeleteRowOperation} Operation
|
||||
*/
|
||||
|
||||
export {};
|
||||
38
core/table-system/dto/Table.js
Normal file
38
core/table-system/dto/Table.js
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* @file 表格相关数据形状(DTO)
|
||||
* 对应运行时存于 message.extra.amily2_tables_data 的结构。
|
||||
*/
|
||||
|
||||
/**
|
||||
* 单元格内容;空值约定为空串而非 null/undefined。
|
||||
* @typedef {string} Cell
|
||||
*/
|
||||
|
||||
/**
|
||||
* 行状态。'pending-deletion' 表示已标记待删除(延迟删除机制)。
|
||||
* @typedef {'normal' | 'pending-deletion'} RowStatus
|
||||
*/
|
||||
|
||||
/**
|
||||
* 单张表格。
|
||||
* @typedef {Object} Table
|
||||
* @property {string} name 表格名(唯一标识 + UI 显示名)
|
||||
* @property {string[]} headers 列头数组,长度 = 列数
|
||||
* @property {Cell[][]} rows 行数据,二维数组,rows[i].length = headers.length
|
||||
* @property {RowStatus[]} [rowStatuses] 行状态数组,与 rows 等长
|
||||
* @property {(number|null)[]} [columnWidths] 列宽数组(UI 用),与 headers 等长,null 表示自适应
|
||||
* @property {string} [note] 表格说明
|
||||
* @property {string} [rule_add] 添加行规则(自然语言)
|
||||
* @property {string} [rule_delete] 删除行规则
|
||||
* @property {string} [rule_update] 更新行规则
|
||||
* @property {Object<string, number>} [charLimitRules] 多列字符限制:{ "colIndexStr": maxChars }
|
||||
* @property {number} [rowLimitRule] 行数上限,0 表示不限
|
||||
* @property {number} [simplifyRowThreshold] 历史行简化阈值,0 表示不简化
|
||||
*/
|
||||
|
||||
/**
|
||||
* 表格集合 = 全局状态。
|
||||
* @typedef {Table[]} TableState
|
||||
*/
|
||||
|
||||
export {};
|
||||
9
core/table-system/dto/TableState.js
Normal file
9
core/table-system/dto/TableState.js
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* @file TableState 的实际定义已合并至 ./Table.js(与 Table 共处一处便于阅读)。
|
||||
* 本文件保留为转发别名,供需要按 dto 名称单独导入的消费方使用:
|
||||
* /** @typedef {import('./TableState.js').TableState} TableState *\/
|
||||
*
|
||||
* @typedef {import('./Table.js').TableState} TableState
|
||||
*/
|
||||
|
||||
export {};
|
||||
@@ -1,100 +1,31 @@
|
||||
/**
|
||||
* @file 旧版 <Amily2Edit> 文本格式的解析器 + executeCommands 入口。
|
||||
*
|
||||
* Phase 0 重构后职责收窄:
|
||||
* - 仅负责把 LLM 返回的文本块解析成 Operation[](legacy formatter 角色)
|
||||
* - 推演下推到 actions/applyOperations.js,本文件不再持有 insertRow/updateRow/deleteRow 实现
|
||||
*
|
||||
* 对外 API:
|
||||
* - parseToOperations(text) : 纯解析,文本 → Op[](Phase B legacy formatter 直接复用)
|
||||
* - executeCommands(text, state) : 解析 + 推演,返回历史 shape { finalState, hasChanges, changes }
|
||||
*
|
||||
* 等 Phase B 引入 formatters/ 目录后,本文件改名为 formatters/legacy.js。
|
||||
*
|
||||
* @typedef {import('./dto/Operation.js').Operation} Operation
|
||||
* @typedef {import('./dto/Table.js').TableState} TableState
|
||||
*/
|
||||
|
||||
import { log } from './logger.js';
|
||||
import { applyOperations } from './actions/applyOperations.js';
|
||||
|
||||
function insertRow(state, tableIndex, data) {
|
||||
if (!state[tableIndex]) {
|
||||
log(`AI指令错误:尝试在不存在的表格索引 ${tableIndex} 中插入行。`, 'error');
|
||||
return { state, changes: [] };
|
||||
}
|
||||
|
||||
// 【安全检查】确保 data 是对象
|
||||
if (typeof data !== 'object' || data === null) {
|
||||
log(`AI指令错误:insertRow 的 data 参数必须是对象,实际收到: ${typeof data} (${data})`, 'error');
|
||||
return { state, changes: [] };
|
||||
}
|
||||
|
||||
const table = state[tableIndex];
|
||||
const colCount = table.headers.length;
|
||||
const newRow = Array(colCount).fill('');
|
||||
const changes = [];
|
||||
const newRowIndex = table.rows.length;
|
||||
|
||||
for (const colIndex in data) {
|
||||
const cIndex = parseInt(colIndex, 10);
|
||||
if (cIndex < colCount) {
|
||||
newRow[cIndex] = data[colIndex];
|
||||
changes.push({ type: 'update', tableIndex, rowIndex: newRowIndex, colIndex: cIndex });
|
||||
}
|
||||
}
|
||||
table.rows.push(newRow);
|
||||
|
||||
// 同步更新 rowStatuses
|
||||
if (!table.rowStatuses) {
|
||||
table.rowStatuses = Array(table.rows.length - 1).fill('normal');
|
||||
}
|
||||
table.rowStatuses.push('normal');
|
||||
|
||||
return { state, changes };
|
||||
}
|
||||
|
||||
function updateRow(state, tableIndex, rowIndex, data) {
|
||||
if (!state[tableIndex]) {
|
||||
log(`AI指令错误:尝试更新不存在的表格 ${tableIndex}。`, 'error');
|
||||
return { state, changes: [] };
|
||||
}
|
||||
|
||||
// 【安全检查】确保 data 是对象
|
||||
if (typeof data !== 'object' || data === null) {
|
||||
log(`AI指令错误:updateRow 的 data 参数必须是对象,实际收到: ${typeof data} (${data})`, 'error');
|
||||
return { state, changes: [] };
|
||||
}
|
||||
|
||||
const table = state[tableIndex];
|
||||
|
||||
if (rowIndex >= table.rows.length) {
|
||||
log(`AI指令修正:updateRow 的行索引 ${rowIndex} 超出范围,自动转换为 insertRow。`, 'warn');
|
||||
return insertRow(state, tableIndex, data);
|
||||
}
|
||||
|
||||
const row = table.rows[rowIndex];
|
||||
const changes = [];
|
||||
for (const colIndex in data) {
|
||||
const cIndex = parseInt(colIndex, 10);
|
||||
if (cIndex < row.length) {
|
||||
row[cIndex] = data[colIndex];
|
||||
changes.push({ type: 'update', tableIndex, rowIndex, colIndex: cIndex });
|
||||
}
|
||||
}
|
||||
return { state, changes };
|
||||
}
|
||||
|
||||
|
||||
function deleteRow(state, tableIndex, rowIndex) {
|
||||
const table = state[tableIndex];
|
||||
if (!table || !table.rows[rowIndex]) {
|
||||
log(`AI指令错误:尝试删除不存在的表格 ${tableIndex} 或行 ${rowIndex}。`, 'error');
|
||||
return { state, changes: [] };
|
||||
}
|
||||
|
||||
if (!table.rowStatuses) {
|
||||
table.rowStatuses = Array(table.rows.length).fill('normal');
|
||||
}
|
||||
|
||||
if (table.rowStatuses[rowIndex] !== 'pending-deletion') {
|
||||
table.rowStatuses[rowIndex] = 'pending-deletion';
|
||||
const changes = [{ type: 'delete', tableIndex, rowIndex }];
|
||||
return { state, changes };
|
||||
}
|
||||
|
||||
return { state, changes: [] };
|
||||
}
|
||||
|
||||
|
||||
const allowedFunctions = {
|
||||
insertRow,
|
||||
updateRow,
|
||||
deleteRow,
|
||||
};
|
||||
const ALLOWED_FN_NAMES = new Set(['insertRow', 'updateRow', 'deleteRow']);
|
||||
|
||||
/**
|
||||
* 把单行函数调用文本解析为 { name, args } 中间表示。
|
||||
* 内部用,不导出。args 是位置参数数组,待 _argsToOperation 转成 Operation 对象。
|
||||
* @param {string} callString
|
||||
* @returns {{ name: string, args: any[] } | null}
|
||||
*/
|
||||
function parseFunctionCall(callString) {
|
||||
const match = callString.trim().match(/(\w+)\((.*)\)/);
|
||||
if (!match) {
|
||||
@@ -105,7 +36,7 @@ function parseFunctionCall(callString) {
|
||||
const functionName = match[1];
|
||||
const argsString = match[2];
|
||||
|
||||
if (!allowedFunctions[functionName]) {
|
||||
if (!ALLOWED_FN_NAMES.has(functionName)) {
|
||||
log(`检测到非法函数调用: "${functionName}"。已阻止执行。`, 'error');
|
||||
return null;
|
||||
}
|
||||
@@ -116,11 +47,11 @@ function parseFunctionCall(callString) {
|
||||
let currentArg = '';
|
||||
let inQuote = false;
|
||||
let quoteChar = '';
|
||||
let braceDepth = 0;
|
||||
|
||||
let braceDepth = 0;
|
||||
|
||||
for (let i = 0; i < argsString.length; i++) {
|
||||
const char = argsString[i];
|
||||
|
||||
|
||||
if ((char === '"' || char === "'") && (i === 0 || argsString[i-1] !== '\\')) {
|
||||
if (!inQuote) {
|
||||
inQuote = true;
|
||||
@@ -164,7 +95,7 @@ function parseValue(val) {
|
||||
if (val === 'null') return null;
|
||||
if (val === 'undefined') return undefined;
|
||||
if (!isNaN(Number(val)) && val !== '') return Number(val);
|
||||
|
||||
|
||||
if (val.startsWith('"') && val.endsWith('"')) {
|
||||
try { return JSON.parse(val); } catch (e) { return val.slice(1, -1); }
|
||||
}
|
||||
@@ -203,14 +134,14 @@ function parseValue(val) {
|
||||
|
||||
function tryParseObject(str) {
|
||||
if (!str.startsWith('{') || !str.endsWith('}')) return null;
|
||||
|
||||
|
||||
let content = str.slice(1, -1);
|
||||
const result = {};
|
||||
let hasMatch = false;
|
||||
|
||||
|
||||
const strings = [];
|
||||
let placeholderIndex = 0;
|
||||
|
||||
|
||||
// 提取字符串并替换为占位符,避免正则在字符串内部匹配
|
||||
const stringRegex = /"([^"\\]|\\.)*"|'([^'\\]|\\.)*'/g;
|
||||
content = content.replace(stringRegex, (match) => {
|
||||
@@ -219,36 +150,36 @@ function tryParseObject(str) {
|
||||
placeholderIndex++;
|
||||
return placeholder;
|
||||
});
|
||||
|
||||
|
||||
// 匹配键:(开头或逗号/分号/冒号) + (数字 或 字母数字下划线 或 占位符) + 冒号
|
||||
const keyRegex = /(?:^|[,;:]+\s*)(?:(\d+)|([a-zA-Z0-9_]+)|(__STR_\d+__))\s*:/g;
|
||||
|
||||
|
||||
let match;
|
||||
let lastIndex = 0;
|
||||
let lastKey = null;
|
||||
|
||||
|
||||
while ((match = keyRegex.exec(content)) !== null) {
|
||||
hasMatch = true;
|
||||
if (lastKey !== null) {
|
||||
let valStr = content.slice(lastIndex, match.index).trim();
|
||||
valStr = valStr.replace(/[,;:]+$/, '').trim();
|
||||
|
||||
|
||||
let actualKey = restoreStrings(lastKey, strings);
|
||||
result[actualKey] = restoreStrings(valStr, strings);
|
||||
}
|
||||
|
||||
|
||||
lastKey = match[1] || match[2] || match[3];
|
||||
lastIndex = match.index + match[0].length;
|
||||
}
|
||||
|
||||
|
||||
if (lastKey !== null) {
|
||||
let valStr = content.slice(lastIndex).trim();
|
||||
valStr = valStr.replace(/[,;:]+$/, '').trim();
|
||||
|
||||
|
||||
let actualKey = restoreStrings(lastKey, strings);
|
||||
result[actualKey] = restoreStrings(valStr, strings);
|
||||
}
|
||||
|
||||
|
||||
return hasMatch ? result : null;
|
||||
}
|
||||
|
||||
@@ -269,51 +200,77 @@ function cleanValueStr(str) {
|
||||
return str;
|
||||
}
|
||||
|
||||
/**
|
||||
* 把 parseFunctionCall 返回的位置参数数组转成 Operation 对象。
|
||||
* @param {string} name
|
||||
* @param {any[]} args
|
||||
* @returns {Operation | null}
|
||||
*/
|
||||
function _argsToOperation(name, args) {
|
||||
if (name === 'insertRow') {
|
||||
return /** @type {Operation} */ ({ op: 'insertRow', tableIndex: args[0], data: args[1] });
|
||||
}
|
||||
if (name === 'updateRow') {
|
||||
return /** @type {Operation} */ ({ op: 'updateRow', tableIndex: args[0], rowIndex: args[1], data: args[2] });
|
||||
}
|
||||
if (name === 'deleteRow') {
|
||||
return /** @type {Operation} */ ({ op: 'deleteRow', tableIndex: args[0], rowIndex: args[1] });
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function executeCommands(aiResponseText, initialState) {
|
||||
/**
|
||||
* 把 LLM 返回的文本块解析为 Operation[]。
|
||||
* 不在文本中找到 <Amily2Edit> 块时返回空数组(不视为错误)。
|
||||
*
|
||||
* @param {string} aiResponseText
|
||||
* @returns {Operation[]}
|
||||
*/
|
||||
export function parseToOperations(aiResponseText) {
|
||||
const commandBlockRegex = /<Amily2Edit>([\s\S]*?)<\/Amily2Edit>/;
|
||||
const match = aiResponseText.match(commandBlockRegex);
|
||||
const match = (aiResponseText || '').match(commandBlockRegex);
|
||||
if (!match) return [];
|
||||
|
||||
if (!match) {
|
||||
return { finalState: initialState, hasChanges: false, changes: [] };
|
||||
}
|
||||
|
||||
log('检测到AI指令块,开始推演...', 'info');
|
||||
const commandBlock = match[1].replace(/<!--|-->/g, '').trim();
|
||||
if (!commandBlock) {
|
||||
return { finalState: initialState, hasChanges: false, changes: [] };
|
||||
}
|
||||
if (!commandBlock) return [];
|
||||
|
||||
const commands = commandBlock.split('\n').filter(line => line.trim() !== '');
|
||||
if (commands.length === 0) {
|
||||
if (commands.length === 0) return [];
|
||||
|
||||
/** @type {Operation[]} */
|
||||
const ops = [];
|
||||
for (const commandString of commands) {
|
||||
const trimmed = commandString.trim();
|
||||
if (!trimmed.startsWith('insertRow(') &&
|
||||
!trimmed.startsWith('updateRow(') &&
|
||||
!trimmed.startsWith('deleteRow(')) {
|
||||
continue;
|
||||
}
|
||||
const parsed = parseFunctionCall(trimmed);
|
||||
if (!parsed) continue;
|
||||
const op = _argsToOperation(parsed.name, parsed.args);
|
||||
if (op) ops.push(op);
|
||||
}
|
||||
return ops;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 LLM 文本指令并推演到 state 上。
|
||||
* 历史 API,调用方期望返回 { finalState, hasChanges, changes }。
|
||||
*
|
||||
* @param {string} aiResponseText
|
||||
* @param {TableState} initialState
|
||||
* @returns {{ finalState: TableState, hasChanges: boolean, changes: import('./dto/Change.js').Change[] }}
|
||||
*/
|
||||
export function executeCommands(aiResponseText, initialState) {
|
||||
const ops = parseToOperations(aiResponseText);
|
||||
|
||||
if (ops.length === 0) {
|
||||
return { finalState: initialState, hasChanges: false, changes: [] };
|
||||
}
|
||||
|
||||
let currentState = JSON.parse(JSON.stringify(initialState));
|
||||
let allChanges = [];
|
||||
log(`检测到 ${ops.length} 条 AI 指令,开始推演...`, 'info');
|
||||
|
||||
commands.forEach(commandString => {
|
||||
const trimmedCommand = commandString.trim();
|
||||
if (trimmedCommand.startsWith('insertRow(') ||
|
||||
trimmedCommand.startsWith('deleteRow(') ||
|
||||
trimmedCommand.startsWith('updateRow('))
|
||||
{
|
||||
const parsed = parseFunctionCall(trimmedCommand);
|
||||
if (parsed) {
|
||||
try {
|
||||
const result = allowedFunctions[parsed.name](currentState, ...parsed.args);
|
||||
currentState = result.state;
|
||||
if (result.changes && result.changes.length > 0) {
|
||||
allChanges = allChanges.concat(result.changes);
|
||||
}
|
||||
log(`成功推演指令: ${commandString}`, 'success');
|
||||
} catch (e) {
|
||||
log(`推演指令 "${commandString}" 时发生运行时错误: ${e.message}`, 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const hasChanges = allChanges.length > 0;
|
||||
return { finalState: currentState, hasChanges, changes: allChanges };
|
||||
const { state, changes } = applyOperations(initialState, ops);
|
||||
return { finalState: state, hasChanges: changes.length > 0, changes };
|
||||
}
|
||||
|
||||
98
core/table-system/infra/persistence.js
Normal file
98
core/table-system/infra/persistence.js
Normal file
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* @file ITablePersistence 实现 —— 表格状态的持久化层。
|
||||
*
|
||||
* 替代 manager.js 中:
|
||||
* - saveStateToMessage(state, targetMessage) → 写入指定消息的 extra
|
||||
* - 16 处复制样板(getContext + saveStateToMessage + saveChat / saveChatDebounced)
|
||||
* 被合并为 commitToLastMessage / commitToLastMessageAsync 两个函数
|
||||
*
|
||||
* 不读取 store;调用方显式传入要持久化的 state。这样:
|
||||
* - 测试容易(不依赖全局单例)
|
||||
* - 万一未来需要在事务边界提交"快照"而非当前 state,接口已就位
|
||||
*
|
||||
* @typedef {import('../dto/Table.js').TableState} TableState
|
||||
*/
|
||||
|
||||
import { saveChat } from '/script.js';
|
||||
import { getContext } from '/scripts/extensions.js';
|
||||
import { saveChatDebounced } from '../../../utils/utils.js';
|
||||
import { log } from '../logger.js';
|
||||
|
||||
/**
|
||||
* message.extra 中存储表格状态的 key。
|
||||
* 此值不能轻易改 —— 所有历史聊天的存档都用这个 key。
|
||||
*/
|
||||
export const TABLE_DATA_KEY = 'amily2_tables_data';
|
||||
|
||||
/**
|
||||
* 把状态深拷贝写入指定消息的 metadata。
|
||||
* 不主动调用 saveChat —— 写盘时机由调用方决定。
|
||||
*
|
||||
* @param {TableState | null} stateToSave
|
||||
* @param {Object} targetMessage
|
||||
* @returns {boolean} 是否写入成功
|
||||
*/
|
||||
export function saveStateToMessage(stateToSave, targetMessage) {
|
||||
if (!stateToSave || !targetMessage) {
|
||||
log('缺少状态或目标消息,无法保存。', 'error');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!targetMessage.extra) {
|
||||
targetMessage.extra = {};
|
||||
}
|
||||
|
||||
targetMessage.extra[TABLE_DATA_KEY] = JSON.parse(JSON.stringify(stateToSave));
|
||||
log(`表格状态已准备写入消息 [${targetMessage.mes.substring(0, 20)}...]`, 'info');
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 把 state 提交到 chat 最新一条消息并立即 saveChat。
|
||||
*
|
||||
* 该函数封装了 manager.js 中复制了 16 次的样板:
|
||||
* const context = getContext();
|
||||
* if (context.chat && context.chat.length > 0) {
|
||||
* const lastMessage = context.chat[context.chat.length - 1];
|
||||
* if (saveStateToMessage(state, lastMessage)) {
|
||||
* saveChat();
|
||||
* return;
|
||||
* }
|
||||
* }
|
||||
* saveChatDebounced();
|
||||
*
|
||||
* @param {TableState | null} state
|
||||
* @returns {boolean} true = 走 last-message commit 路径;false = 降级到 debounced
|
||||
*/
|
||||
export function commitToLastMessage(state) {
|
||||
const context = getContext();
|
||||
if (context.chat && context.chat.length > 0) {
|
||||
const lastMessage = context.chat[context.chat.length - 1];
|
||||
if (saveStateToMessage(state, lastMessage)) {
|
||||
saveChat();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
saveChatDebounced();
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* commitToLastMessage 的 async 变体。
|
||||
* deleteRow / restoreRow / rollbackState 等需要等 saveChat 完成后才做后续渲染的场景使用。
|
||||
*
|
||||
* @param {TableState | null} state
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
export async function commitToLastMessageAsync(state) {
|
||||
const context = getContext();
|
||||
if (context.chat && context.chat.length > 0) {
|
||||
const lastMessage = context.chat[context.chat.length - 1];
|
||||
if (saveStateToMessage(state, lastMessage)) {
|
||||
await saveChat();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
await saveChatDebounced();
|
||||
return false;
|
||||
}
|
||||
117
core/table-system/infra/store.js
Normal file
117
core/table-system/infra/store.js
Normal file
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* @file ITableStore 实现 —— 表格运行时状态的唯一所有者。
|
||||
*
|
||||
* 替代 manager.js 中三个 module-level 可变量:
|
||||
* currentTablesState → 通过 getState/setState 访问
|
||||
* highlightedCells → addHighlight/getHighlights/clearHighlights
|
||||
* updatedTables → markTableUpdated/getUpdatedTables/clearUpdatedTables
|
||||
*
|
||||
* 本模块只承担"存",不触发任何副作用(不保存、不渲染、不发事件总线消息)。
|
||||
* 副作用编排留给 Service 层 / Action 层。
|
||||
*
|
||||
* setState 会触发 subscribe 注册的回调,给 UI / SuperMemory 一个钩子,
|
||||
* 但不直接 import UI(保持 domain 纯度)。
|
||||
*
|
||||
* @typedef {import('../dto/Table.js').TableState} TableState
|
||||
*/
|
||||
|
||||
import { log } from '../logger.js';
|
||||
|
||||
/** @type {TableState | null} */
|
||||
let _state = null;
|
||||
|
||||
/** @type {Set<string>} 形如 "tableIndex-rowIndex-colIndex" */
|
||||
const _highlights = new Set();
|
||||
|
||||
/** @type {Set<number>} 标记本周期内被改过的表格索引 */
|
||||
const _updatedTables = new Set();
|
||||
|
||||
/** @type {Set<(state: TableState | null) => void>} */
|
||||
const _listeners = new Set();
|
||||
|
||||
// ── 主状态 ────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* @returns {TableState | null}
|
||||
*/
|
||||
export function getState() {
|
||||
return _state;
|
||||
}
|
||||
|
||||
/**
|
||||
* 直接替换全局状态。注意:不做深拷贝,调用方需自己负责传入的 state 不被外部 mutate。
|
||||
* @param {TableState | null} newState
|
||||
*/
|
||||
export function setState(newState) {
|
||||
_state = newState;
|
||||
_notify();
|
||||
}
|
||||
|
||||
/**
|
||||
* 订阅 setState 触发的变更通知。返回取消订阅函数。
|
||||
* 仅在 setState 被调用时触发;mutate 同一引用不会触发。
|
||||
* @param {(state: TableState | null) => void} listener
|
||||
* @returns {() => void}
|
||||
*/
|
||||
export function subscribe(listener) {
|
||||
_listeners.add(listener);
|
||||
return () => _listeners.delete(listener);
|
||||
}
|
||||
|
||||
function _notify() {
|
||||
for (const l of _listeners) {
|
||||
try {
|
||||
l(_state);
|
||||
} catch (e) {
|
||||
console.error('[TableStore] listener error:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 单元格高亮 ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* @param {number} tableIndex
|
||||
* @param {number} rowIndex
|
||||
* @param {number} colIndex
|
||||
*/
|
||||
export function addHighlight(tableIndex, rowIndex, colIndex) {
|
||||
_highlights.add(`${tableIndex}-${rowIndex}-${colIndex}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Set<string>}
|
||||
*/
|
||||
export function getHighlights() {
|
||||
return _highlights;
|
||||
}
|
||||
|
||||
export function clearHighlights() {
|
||||
if (_highlights.size > 0) {
|
||||
_highlights.clear();
|
||||
log('已清除所有单元格高亮标记。', 'info');
|
||||
}
|
||||
}
|
||||
|
||||
// ── 更新过的表格标记 ───────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* @param {number} tableIndex
|
||||
*/
|
||||
export function markTableUpdated(tableIndex) {
|
||||
_updatedTables.add(tableIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Set<number>}
|
||||
*/
|
||||
export function getUpdatedTables() {
|
||||
return _updatedTables;
|
||||
}
|
||||
|
||||
export function clearUpdatedTables() {
|
||||
if (_updatedTables.size > 0) {
|
||||
_updatedTables.clear();
|
||||
log('已清除所有表格的更新标记。', 'info');
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
329
core/table-system/preset.js
Normal file
329
core/table-system/preset.js
Normal file
@@ -0,0 +1,329 @@
|
||||
/**
|
||||
* @file 表格预设的导入 / 导出 / 全局预设管理。
|
||||
*
|
||||
* 历史来源:从 manager.js 抽出
|
||||
* - exportPreset / exportPresetFull → 调内部 exportPresetBase
|
||||
* - importPreset → 接受 hooks 注入 SuperMemory 同步等副作用
|
||||
* - clearGlobalPreset → 清除 extension_settings 中的全局预设
|
||||
* - importGlobalPreset → 写入全局预设
|
||||
*
|
||||
* 设计要点:
|
||||
* - 不内含 SuperMemory dispatch 逻辑(避免与 manager.js 循环依赖)
|
||||
* - importPreset 接受 hooks: { onAfterApply, onImported },调用方注入需要的副作用
|
||||
* - 所有持久化走 infra/persistence.js,不再复制 saveStateToMessage 样板
|
||||
*/
|
||||
|
||||
import { extension_settings, getContext } from '/scripts/extensions.js';
|
||||
import { saveSettingsDebounced } from '/script.js';
|
||||
import { extensionName } from '../../utils/settings.js';
|
||||
import { log } from './logger.js';
|
||||
import { getState, setState } from './infra/store.js';
|
||||
import { saveStateToMessage, commitToLastMessage } from './infra/persistence.js';
|
||||
import {
|
||||
getBatchFillerRuleTemplate,
|
||||
getBatchFillerFlowTemplate,
|
||||
saveBatchFillerRuleTemplate,
|
||||
saveBatchFillerFlowTemplate,
|
||||
saveAiTemplate,
|
||||
} from './templates.js';
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* onAfterApply?: () => void,
|
||||
* onImported?: () => void
|
||||
* }} ImportPresetHooks
|
||||
*/
|
||||
|
||||
// ── 导出 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* @param {boolean} includeData 是否包含 rows 实际数据
|
||||
*/
|
||||
function exportPresetBase(includeData = false) {
|
||||
const state = getState();
|
||||
if (!state) {
|
||||
log('无法导出:当前表格状态为空。', 'error');
|
||||
toastr.error('没有可导出的表格数据。');
|
||||
return;
|
||||
}
|
||||
|
||||
let tablesToExport;
|
||||
let fileNameSuffix;
|
||||
|
||||
if (includeData) {
|
||||
// 完整备份
|
||||
tablesToExport = JSON.parse(JSON.stringify(state));
|
||||
fileNameSuffix = '完整备份';
|
||||
} else {
|
||||
// 纯净预设:仅结构 + 规则,不带数据
|
||||
tablesToExport = state.map(table => ({
|
||||
name: table.name,
|
||||
headers: table.headers,
|
||||
columnWidths: table.columnWidths || [],
|
||||
note: table.note,
|
||||
rule_add: table.rule_add,
|
||||
rule_delete: table.rule_delete,
|
||||
rule_update: table.rule_update,
|
||||
charLimitRules: table.charLimitRules || {},
|
||||
rowLimitRule: table.rowLimitRule || 0,
|
||||
// simplifyRowThreshold 不导出:与当前聊天进度强绑定的临时设置
|
||||
rows: [],
|
||||
rowStatuses: [],
|
||||
}));
|
||||
fileNameSuffix = '纯净预设';
|
||||
}
|
||||
|
||||
const preset = {
|
||||
version: 'Amily2-Table-Preset-v3.0-separated_templates',
|
||||
batchFillerRuleTemplate: getBatchFillerRuleTemplate(),
|
||||
batchFillerFlowTemplate: getBatchFillerFlowTemplate(),
|
||||
tables: tablesToExport,
|
||||
};
|
||||
|
||||
const blob = new Blob([JSON.stringify(preset, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `Amily2-${fileNameSuffix}-${new Date().toISOString().slice(0, 10)}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
log(`【${fileNameSuffix}】已成功导出。`, 'success');
|
||||
toastr.success(`【${fileNameSuffix}】已开始下载。`, '导出成功');
|
||||
}
|
||||
|
||||
export function exportPreset() {
|
||||
exportPresetBase(false);
|
||||
}
|
||||
|
||||
export function exportPresetFull() {
|
||||
exportPresetBase(true);
|
||||
}
|
||||
|
||||
// ── 导入 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* 把导入的 tables 数组归一化(补字段 + 兼容旧版结构)。in-place mutation。
|
||||
*/
|
||||
function _normalizeImportedTables(importedTables) {
|
||||
importedTables.forEach(table => {
|
||||
if (table.name === undefined || table.headers === undefined || table.rows === undefined) {
|
||||
throw new Error(`导入的表格数据格式不正确: ${JSON.stringify(table)}`);
|
||||
}
|
||||
if (table.note === undefined) table.note = '无';
|
||||
if (table.rule_add === undefined) table.rule_add = '允许';
|
||||
if (table.rule_delete === undefined) table.rule_delete = '允许';
|
||||
if (table.rule_update === undefined) table.rule_update = '允许';
|
||||
|
||||
// 多列规则兼容:旧 charLimitRule 单列对象 → 新 charLimitRules 对象映射
|
||||
if (table.charLimitRule && !table.charLimitRules) {
|
||||
table.charLimitRules = {};
|
||||
if (table.charLimitRule.columnIndex !== -1 && table.charLimitRule.limit > 0) {
|
||||
table.charLimitRules[table.charLimitRule.columnIndex] = table.charLimitRule.limit;
|
||||
}
|
||||
} else if (table.charLimitRules === undefined) {
|
||||
table.charLimitRules = {};
|
||||
}
|
||||
delete table.charLimitRule;
|
||||
|
||||
// 延迟删除:rowStatuses 必须存在
|
||||
if (!table.rowStatuses) {
|
||||
table.rowStatuses = Array(table.rows.length).fill('normal');
|
||||
}
|
||||
if (table.rowLimitRule === undefined) table.rowLimitRule = 0;
|
||||
if (table.columnWidths === undefined) table.columnWidths = [];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 把导入的预设里的模板字段写回 extension_settings。版本兼容三档:
|
||||
* v3.0(separated) / v2.1(aiRule+aiFlow) / v2.0(aiTemplate)
|
||||
*/
|
||||
function _applyImportedTemplates(preset) {
|
||||
if (preset.version === 'Amily2-Table-Preset-v3.0-separated_templates') {
|
||||
saveBatchFillerRuleTemplate(preset.batchFillerRuleTemplate || '');
|
||||
saveBatchFillerFlowTemplate(preset.batchFillerFlowTemplate || '');
|
||||
saveAiTemplate(preset.injectionFlowTemplate || '');
|
||||
} else if (preset.aiRuleTemplate !== undefined && preset.aiFlowTemplate !== undefined) {
|
||||
saveBatchFillerRuleTemplate(preset.aiRuleTemplate || '');
|
||||
saveBatchFillerFlowTemplate(preset.aiFlowTemplate || '');
|
||||
saveAiTemplate(preset.aiFlowTemplate || '');
|
||||
} else if (preset.aiTemplate) {
|
||||
saveBatchFillerRuleTemplate('');
|
||||
saveBatchFillerFlowTemplate(preset.aiTemplate || '');
|
||||
saveAiTemplate(preset.aiTemplate || '');
|
||||
} else {
|
||||
log('导入的预设中缺少指令模板字段,模板将不会被更新。', 'warn');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 弹出文件选择 → 解析 JSON → 归一化 → 写入 store + 持久化。
|
||||
*
|
||||
* hooks.onAfterApply 在 setState 之后、saveChat 之前触发(用于注入 SuperMemory 同步等副作用)。
|
||||
* hooks.onImported 在全部完成后触发(UI 刷新)。
|
||||
*
|
||||
* @param {ImportPresetHooks | (() => void)} [hooksOrCallback] 兼容旧签名 importPreset(callback)
|
||||
*/
|
||||
export function importPreset(hooksOrCallback) {
|
||||
/** @type {ImportPresetHooks} */
|
||||
const hooks = typeof hooksOrCallback === 'function'
|
||||
? { onImported: hooksOrCallback }
|
||||
: (hooksOrCallback || {});
|
||||
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = '.json';
|
||||
|
||||
input.onchange = e => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = event => {
|
||||
try {
|
||||
const preset = JSON.parse(event.target.result);
|
||||
|
||||
if (!preset.version || !Array.isArray(preset.tables)) {
|
||||
throw new Error('文件格式无效或缺少版本号/表格数据。');
|
||||
}
|
||||
|
||||
const confirmation = window.confirm(
|
||||
'【警告】\n\n导入操作将完全覆盖您当前的AI指令模板和所有表格(包括结构和内容)。\n\n此操作不可逆,是否确定要继续?'
|
||||
);
|
||||
if (!confirmation) {
|
||||
log('用户取消了导入操作。', 'info');
|
||||
toastr.info('导入操作已取消。');
|
||||
return;
|
||||
}
|
||||
|
||||
_applyImportedTemplates(preset);
|
||||
|
||||
const importedTables = preset.tables;
|
||||
_normalizeImportedTables(importedTables);
|
||||
|
||||
setState(importedTables);
|
||||
|
||||
// 钩子:让调用方注入 SuperMemory 全量同步等副作用
|
||||
if (typeof hooks.onAfterApply === 'function') {
|
||||
try { hooks.onAfterApply(); } catch (e) {
|
||||
log(`importPreset onAfterApply 抛错: ${e.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
commitToLastMessage(getState());
|
||||
log('导入的预设已强制写入最新消息并立即保存。', 'success');
|
||||
log('预设已成功导入并应用。', 'success');
|
||||
toastr.success('预设已成功导入!', '导入成功');
|
||||
|
||||
if (typeof hooks.onImported === 'function') {
|
||||
try { hooks.onImported(); } catch (e) {
|
||||
log(`importPreset onImported 抛错: ${e.message}`, 'error');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
log(`导入预设失败: ${error.message}`, 'error');
|
||||
toastr.error(`导入失败:${error.message}`, '错误');
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
};
|
||||
|
||||
input.click();
|
||||
}
|
||||
|
||||
// ── 全局预设 ──────────────────────────────────────────────────────────────
|
||||
|
||||
export function clearGlobalPreset() {
|
||||
if (extension_settings[extensionName] && extension_settings[extensionName].global_table_preset) {
|
||||
const confirmation = window.confirm(
|
||||
'【清除全局预设】\n\n您确定要清除已设置的全局预设吗?\n\n清除后,新聊天将恢复使用扩展内置的默认表格模板。'
|
||||
);
|
||||
|
||||
if (confirmation) {
|
||||
delete extension_settings[extensionName].global_table_preset;
|
||||
saveSettingsDebounced();
|
||||
log('全局预设已被清除。', 'success');
|
||||
toastr.success('全局预设已清除,新聊天将使用默认模板。', '操作成功');
|
||||
} else {
|
||||
log('用户取消了清除全局预设的操作。', 'info');
|
||||
toastr.info('操作已取消。');
|
||||
}
|
||||
} else {
|
||||
log('无需清除,当前未设置任何全局预设。', 'info');
|
||||
toastr.info('当前没有设置全局预设。', '提示');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {(() => void) | undefined} onImported
|
||||
*/
|
||||
export function importGlobalPreset(onImported) {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = '.json';
|
||||
|
||||
input.onchange = e => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = event => {
|
||||
try {
|
||||
const preset = JSON.parse(event.target.result);
|
||||
|
||||
if (!preset.version || !Array.isArray(preset.tables)) {
|
||||
throw new Error('文件格式无效或缺少版本号/表格数据。');
|
||||
}
|
||||
|
||||
const confirmation = window.confirm(
|
||||
'【全局预设导入】\n\n这将把选定的预设设置为所有新聊天的默认表格。\n\n此操作将覆盖任何已存在的全局预设,是否确定?'
|
||||
);
|
||||
if (!confirmation) {
|
||||
log('用户取消了全局预设导入操作。', 'info');
|
||||
toastr.info('操作已取消。');
|
||||
return;
|
||||
}
|
||||
|
||||
// 纯净副本:仅结构,不含 rows
|
||||
const cleanTables = preset.tables.map(table => ({
|
||||
name: table.name,
|
||||
headers: table.headers,
|
||||
note: table.note,
|
||||
rule_add: table.rule_add,
|
||||
rule_delete: table.rule_delete,
|
||||
rule_update: table.rule_update,
|
||||
rows: [],
|
||||
}));
|
||||
|
||||
if (!extension_settings[extensionName]) extension_settings[extensionName] = {};
|
||||
extension_settings[extensionName].global_table_preset = {
|
||||
version: preset.version,
|
||||
tables: cleanTables,
|
||||
batchFillerRuleTemplate: preset.batchFillerRuleTemplate,
|
||||
batchFillerFlowTemplate: preset.batchFillerFlowTemplate,
|
||||
};
|
||||
saveSettingsDebounced();
|
||||
|
||||
_applyImportedTemplates(preset);
|
||||
|
||||
log('全局预设已成功导入并保存到扩展设置中。', 'success');
|
||||
toastr.success('全局预设已设置!新聊天将默认使用此预设。', '设置成功');
|
||||
|
||||
if (typeof onImported === 'function') {
|
||||
try { onImported(); } catch (e) {
|
||||
log(`importGlobalPreset onImported 抛错: ${e.message}`, 'error');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
log(`导入全局预设失败: ${error.message}`, 'error');
|
||||
toastr.error(`导入失败:${error.message}`, '错误');
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
};
|
||||
|
||||
input.click();
|
||||
}
|
||||
239
core/table-system/rendering.js
Normal file
239
core/table-system/rendering.js
Normal file
@@ -0,0 +1,239 @@
|
||||
/**
|
||||
* @file Markdown/CSV 渲染 —— 把 TableState 渲染为 prompt 可用的字符串。
|
||||
*
|
||||
* 纯函数:吃 state、吐字符串。不读 store、不写盘、不发事件。
|
||||
*
|
||||
* 历史来源:从 manager.js 抽出
|
||||
* - convertTablesToCsvString → tablesToCsv
|
||||
* - convertSelectedTablesToCsvString → tablesToCsvWithSelection
|
||||
* - convertTablesToCsvStringForContentOnly → tablesToCsvContentOnly
|
||||
* - checkTableRules (内部) → _checkTableRules (内部)
|
||||
*
|
||||
* manager.js 保留同名 export 作 wrapper(自动注入 getState()),所有外部调用点零改动。
|
||||
*
|
||||
* @typedef {import('./dto/Table.js').Table} Table
|
||||
* @typedef {import('./dto/Table.js').TableState} TableState
|
||||
*/
|
||||
|
||||
/**
|
||||
* 检查表格规则违规,返回聚合警告字符串(多行)。
|
||||
* 行数超限 + 多列字符限制超限。
|
||||
* @param {Table} table
|
||||
* @returns {string}
|
||||
*/
|
||||
function _checkTableRules(table) {
|
||||
const warnings = [];
|
||||
|
||||
// 行数限制
|
||||
if (table.rowLimitRule && table.rowLimitRule > 0 && table.rows.length > table.rowLimitRule) {
|
||||
warnings.push(`【当前(${table.name})超出规定(${table.rowLimitRule})行,请结合剧情缩减至(${table.rowLimitRule})行以下,但切莫完全删除。】`);
|
||||
}
|
||||
|
||||
// 多列字符限制
|
||||
const charLimitRules = table.charLimitRules || {};
|
||||
for (const colIndexStr in charLimitRules) {
|
||||
const colIndex = parseInt(colIndexStr, 10);
|
||||
const limit = charLimitRules[colIndex];
|
||||
if (limit > 0 && colIndex >= 0 && colIndex < table.headers.length) {
|
||||
const colName = table.headers[colIndex];
|
||||
const offendingRows = [];
|
||||
table.rows.forEach((row, rowIndex) => {
|
||||
if (table.rowStatuses && table.rowStatuses[rowIndex] === 'pending-deletion') return;
|
||||
const cellContent = row[colIndex] || '';
|
||||
if (cellContent.length > limit) offendingRows.push(rowIndex);
|
||||
});
|
||||
if (offendingRows.length > 0) {
|
||||
warnings.push(`【当前(${table.name})第(${offendingRows.join('、')})行(${colName})列,字符超出规定(${limit})字限制,请进行缩减。】`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return warnings.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* 把单个 table 的"内容主体"(含 simplify 处理 + warnings)写入到 fullString 末尾。
|
||||
* 提取自三个渲染函数中重复的内层逻辑。
|
||||
*
|
||||
* @param {Table} table
|
||||
* @param {string} tagName
|
||||
* @returns {string}
|
||||
*/
|
||||
function _renderTableBody(table, tagName) {
|
||||
let out = '';
|
||||
const activeRows = table.rows.filter((row, i) => !table.rowStatuses || table.rowStatuses[i] !== 'pending-deletion');
|
||||
|
||||
if (activeRows.length === 0) {
|
||||
out += '(该表当前内容为空)\n';
|
||||
} else {
|
||||
const simplifyThreshold = table.simplifyRowThreshold || 0;
|
||||
let simplifiedCount = 0;
|
||||
|
||||
table.rows.forEach((row, rowIndex) => {
|
||||
if (table.rowStatuses && table.rowStatuses[rowIndex] === 'pending-deletion') return;
|
||||
|
||||
// 历史内容简化:前 N 行用 ---已锁定--- 占位
|
||||
if (simplifyThreshold > 0 && rowIndex < simplifyThreshold) {
|
||||
if (simplifiedCount === 0) {
|
||||
const placeholderCells = row.map(() => '---已锁定---');
|
||||
out += `| ${rowIndex} | ${placeholderCells.join(' | ')} |\n`;
|
||||
out += `| ... | ${row.map(() => '...').join(' | ')} |\n`;
|
||||
}
|
||||
if (rowIndex === simplifyThreshold - 1) {
|
||||
const placeholderCells = row.map(() => '---已锁定---');
|
||||
out += `| ${rowIndex} | ${placeholderCells.join(' | ')} |\n`;
|
||||
}
|
||||
simplifiedCount++;
|
||||
return;
|
||||
}
|
||||
|
||||
if (Array.isArray(row)) {
|
||||
const rowCells = row.map(cell => {
|
||||
const cellContent = (cell === null || cell === undefined || cell === '') ? '未知' : String(cell);
|
||||
return cellContent.replace(/\|/g, '|');
|
||||
});
|
||||
out += `| ${rowIndex} | ${rowCells.join(' | ')} |\n`;
|
||||
}
|
||||
});
|
||||
|
||||
if (simplifiedCount > 0) {
|
||||
out += `\n【系统提示】:表格前 ${simplifiedCount} 行(索引 0 到 ${simplifiedCount - 1})的历史内容已简化并锁定,无需读取或修改。请专注于后续行的内容。\n`;
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 完整渲染:所有表格内容 + 规则 + 警告,注入到主流程 prompt。
|
||||
* 对应 manager.js#convertTablesToCsvString。
|
||||
*
|
||||
* @param {TableState | null} state
|
||||
* @returns {string}
|
||||
*/
|
||||
export function tablesToCsv(state) {
|
||||
if (!state || state.length === 0) return '';
|
||||
|
||||
let fullString = '';
|
||||
state.forEach((table, tableIndex) => {
|
||||
// 标题
|
||||
fullString += `\n* ${tableIndex}:${table.name}\n`;
|
||||
|
||||
// 说明
|
||||
fullString += `【说明】:\n${table.note || '无'}\n`;
|
||||
|
||||
// 内容(Markdown 表)
|
||||
const tagName = table.name.replace(/\s/g, '') + '内容';
|
||||
fullString += `<${tagName}>\n`;
|
||||
const headerWithIndex = ['rowIndex', ...table.headers.map((h, i) => `${i}:${h}`)];
|
||||
fullString += `| ${headerWithIndex.join(' | ')} |\n`;
|
||||
fullString += `|${headerWithIndex.map(() => '---').join('|')}|\n`;
|
||||
fullString += _renderTableBody(table, tagName);
|
||||
|
||||
// 警告
|
||||
const warnings = _checkTableRules(table);
|
||||
if (warnings) fullString += `${warnings}\n`;
|
||||
fullString += `</${tagName}>\n`;
|
||||
|
||||
// 规则
|
||||
fullString += `【增加】: ${table.rule_add || '允许'}\n`;
|
||||
fullString += `【删除】: ${table.rule_delete || '允许'}\n`;
|
||||
fullString += `【修改】: ${table.rule_update || '允许'}\n`;
|
||||
|
||||
if (tableIndex < state.length - 1) fullString += '\n---\n';
|
||||
});
|
||||
|
||||
return fullString;
|
||||
}
|
||||
|
||||
/**
|
||||
* 选中态渲染:未选中的表格只展示表头作为索引参考;选中的展示完整内容。
|
||||
* 对应 manager.js#convertSelectedTablesToCsvString。
|
||||
*
|
||||
* @param {TableState | null} state
|
||||
* @param {number[]} selectedIndices
|
||||
* @returns {string}
|
||||
*/
|
||||
export function tablesToCsvWithSelection(state, selectedIndices) {
|
||||
if (!state || state.length === 0) return '';
|
||||
const selected = Array.isArray(selectedIndices) ? selectedIndices : [];
|
||||
|
||||
let fullString = '';
|
||||
state.forEach((table, tableIndex) => {
|
||||
const isSelected = selected.includes(tableIndex);
|
||||
|
||||
// 标题
|
||||
fullString += `\n* ${tableIndex}:${table.name}`;
|
||||
if (!isSelected) fullString += ' (本表格无需重新整理,仅供参考)';
|
||||
fullString += '\n';
|
||||
|
||||
// 说明
|
||||
fullString += `【说明】:\n${table.note || '无'}\n`;
|
||||
|
||||
const tagName = table.name.replace(/\s/g, '') + '内容';
|
||||
fullString += `<${tagName}>\n`;
|
||||
const headerWithIndex = ['rowIndex', ...table.headers.map((h, i) => `${i}:${h}`)];
|
||||
fullString += `| ${headerWithIndex.join(' | ')} |\n`;
|
||||
fullString += `|${headerWithIndex.map(() => '---').join('|')}|\n`;
|
||||
|
||||
if (isSelected) {
|
||||
fullString += _renderTableBody(table, tagName);
|
||||
const warnings = _checkTableRules(table);
|
||||
if (warnings) fullString += `${warnings}\n`;
|
||||
} else {
|
||||
fullString += '(此处省略未选中的表格内容,仅提供表头供索引参考)\n';
|
||||
}
|
||||
fullString += `</${tagName}>\n`;
|
||||
|
||||
// 规则
|
||||
if (isSelected) {
|
||||
fullString += `【增加】: ${table.rule_add || '允许'}\n`;
|
||||
fullString += `【删除】: ${table.rule_delete || '允许'}\n`;
|
||||
fullString += `【修改】: ${table.rule_update || '允许'}\n`;
|
||||
} else {
|
||||
fullString += `【操作权限】: 禁止修改此表格\n`;
|
||||
}
|
||||
|
||||
if (tableIndex < state.length - 1) fullString += '\n---\n';
|
||||
});
|
||||
|
||||
return fullString;
|
||||
}
|
||||
|
||||
/**
|
||||
* 仅内容渲染:不带规则、不带 rowIndex 列、不带说明。
|
||||
* 用于"分步填表"和"优化中填表"模式下的 prompt 注入(只展示数据本身)。
|
||||
* 对应 manager.js#convertTablesToCsvStringForContentOnly。
|
||||
*
|
||||
* @param {TableState | null} state
|
||||
* @returns {string}
|
||||
*/
|
||||
export function tablesToCsvContentOnly(state) {
|
||||
if (!state || state.length === 0) return '';
|
||||
|
||||
let outputString = '';
|
||||
state.forEach(table => {
|
||||
outputString += `\n<${table.name}>\n`;
|
||||
|
||||
// Markdown 表头
|
||||
outputString += `| ${table.headers.join(' | ')} |\n`;
|
||||
outputString += `|${table.headers.map(() => '---').join('|')}|\n`;
|
||||
|
||||
// 数据
|
||||
const activeRows = table.rows.filter((row, i) => !table.rowStatuses || table.rowStatuses[i] !== 'pending-deletion');
|
||||
if (activeRows.length > 0) {
|
||||
activeRows.forEach(row => {
|
||||
if (Array.isArray(row)) {
|
||||
const rowContent = row.map(cell => (cell === null || cell === undefined || cell === '') ? ' ' : cell.toString());
|
||||
outputString += `| ${rowContent.join(' | ')} |\n`;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
outputString += '(该表当前内容为空)\n';
|
||||
}
|
||||
|
||||
outputString += `</${table.name}>\n`;
|
||||
});
|
||||
|
||||
return outputString.trim();
|
||||
}
|
||||
@@ -71,8 +71,8 @@ export async function reorganizeTableContent(selectedTableIndices) {
|
||||
console.log('[Amily2-重新整理] 使用 Nccs API 进行表格重整...');
|
||||
rawContent = await callNccsAI(messages);
|
||||
} else {
|
||||
console.log('[Amily2-重新整理] 使用默认 API 进行表格重整...');
|
||||
rawContent = await callAI(messages);
|
||||
console.log('[Amily2-重新整理] 使用 tableFilling slot 进行表格重整...');
|
||||
rawContent = await callAI(messages, { slot: 'tableFilling' });
|
||||
}
|
||||
|
||||
if (!rawContent) {
|
||||
|
||||
@@ -277,8 +277,8 @@ export async function fillWithSecondaryApi(latestMessage, forceRun = false) {
|
||||
console.log('[Amily2-副API] 使用 Nccs API 进行分步填表...');
|
||||
rawContent = await callNccsAI(messages);
|
||||
} else {
|
||||
console.log('[Amily2-副API] 使用默认 API 进行分步填表...');
|
||||
rawContent = await callAI(messages);
|
||||
console.log('[Amily2-副API] 使用 tableFilling slot 进行分步填表...');
|
||||
rawContent = await callAI(messages, { slot: 'tableFilling' });
|
||||
}
|
||||
|
||||
if (!rawContent) {
|
||||
|
||||
78
core/table-system/templates.js
Normal file
78
core/table-system/templates.js
Normal file
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* @file 表格 prompt 模板的 getter/setter 集中点。
|
||||
*
|
||||
* 三套模板:
|
||||
* - batch_filler_rule_template 规则模板(系统提示词部分)
|
||||
* - batch_filler_flow_template 流程模板(含 {{{Amily2TableData}}} 占位符)
|
||||
* - amily2_ai_template 注入模板(主 API 模式下走的注入)
|
||||
*
|
||||
* 所有读写都落到 extension_settings[extensionName],saveSettingsDebounced 触发持久化。
|
||||
*
|
||||
* 历史来源:从 manager.js 抽出
|
||||
* - getBatchFillerRuleTemplate / saveBatchFillerRuleTemplate
|
||||
* - getBatchFillerFlowTemplate / saveBatchFillerFlowTemplate
|
||||
* - getAiFlowTemplateForInjection
|
||||
* - saveAiTemplate / getAiTemplate
|
||||
*/
|
||||
|
||||
import { extension_settings } from '/scripts/extensions.js';
|
||||
import { saveSettingsDebounced } from '/script.js';
|
||||
import { extensionName } from '../../utils/settings.js';
|
||||
import { DEFAULT_AI_RULE_TEMPLATE, DEFAULT_AI_FLOW_TEMPLATE } from './settings.js';
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getBatchFillerRuleTemplate() {
|
||||
return extension_settings[extensionName]?.batch_filler_rule_template ?? DEFAULT_AI_RULE_TEMPLATE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} template
|
||||
*/
|
||||
export function saveBatchFillerRuleTemplate(template) {
|
||||
if (!extension_settings[extensionName]) extension_settings[extensionName] = {};
|
||||
extension_settings[extensionName].batch_filler_rule_template = template;
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getBatchFillerFlowTemplate() {
|
||||
return extension_settings[extensionName]?.batch_filler_flow_template ?? DEFAULT_AI_FLOW_TEMPLATE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} template
|
||||
*/
|
||||
export function saveBatchFillerFlowTemplate(template) {
|
||||
if (!extension_settings[extensionName]) extension_settings[extensionName] = {};
|
||||
extension_settings[extensionName].batch_filler_flow_template = template;
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
/**
|
||||
* 主 API 模式下注入用的流程模板。与 batch_filler_flow_template 是两套独立配置。
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getAiFlowTemplateForInjection() {
|
||||
return extension_settings[extensionName]?.amily2_ai_template ?? DEFAULT_AI_FLOW_TEMPLATE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} template
|
||||
*/
|
||||
export function saveAiTemplate(template) {
|
||||
if (!extension_settings[extensionName]) extension_settings[extensionName] = {};
|
||||
extension_settings[extensionName].amily2_ai_template = template;
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
/**
|
||||
* 别名 —— 历史 manager.js 同名函数,等价于 getAiFlowTemplateForInjection。
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getAiTemplate() {
|
||||
return getAiFlowTemplateForInjection();
|
||||
}
|
||||
Reference in New Issue
Block a user