Files
ST-Amily2-Chat-Optimisation/core/auto-char-card/tools.js

681 lines
25 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { amilyHelper } from "../tavern-helper/main.js";
import * as charApi from "./char-api.js";
import { callAi } from "./api.js";
export const tools = {
read_world_info: async ({ book_name, return_full = false }) => {
const entries = await amilyHelper.getLorebookEntries(book_name);
if (return_full) {
return JSON.stringify(entries, null, 2);
}
const summary = entries.map(e => {
let keys = e.key;
if (Array.isArray(keys)) keys = keys.join(', ');
return {
uid: e.uid,
keys: keys,
comment: e.comment || keys || "未命名条目",
};
});
return JSON.stringify({
info: "世界书条目索引。请使用带有 'uid' 的 'read_world_entry' 来读取具体内容。",
total_entries: entries.length,
entries: summary
}, null, 2);
},
read_world_entry: async ({ book_name, uid }) => {
const entries = await amilyHelper.getLorebookEntries(book_name);
const entry = entries.find(e => String(e.uid) === String(uid));
if (!entry) {
return JSON.stringify({
status: "error",
code: "ENTRY_NOT_FOUND",
message: `在世界书 "${book_name}" 中未找到 UID 为 ${uid} 的条目。`,
suggestion: "请使用 'read_world_info' 查看可用的 UID。"
});
}
return JSON.stringify({
status: "success",
data: entry
}, null, 2);
},
write_world_info_entry: async ({ book_name, entries }) => {
if (typeof entries === 'string') {
try {
const cleanEntries = entries.replace(/```json/g, '').replace(/```/g, '').trim();
entries = JSON.parse(cleanEntries);
} catch (e) {
return JSON.stringify({
status: "error",
code: "INVALID_JSON",
message: `'entries' 参数必须是有效的 JSON 数组。解析错误: ${e.message}`
});
}
}
if (!Array.isArray(entries)) {
if (typeof entries === 'object' && entries !== null) {
entries = [entries];
} else {
return JSON.stringify({
status: "error",
code: "INVALID_TYPE",
message: "'entries' 参数必须是数组或对象。"
});
}
}
const updates = [];
const creates = [];
for (const entry of entries) {
if (entry.uid !== undefined) {
updates.push(entry);
} else {
creates.push(entry);
}
}
let updatedCount = 0;
let createdCount = 0;
let errors = [];
if (updates.length > 0) {
const success = await amilyHelper.setLorebookEntries(book_name, updates);
if (success) updatedCount = updates.length;
else errors.push("更新条目失败。");
}
if (creates.length > 0) {
const success = await amilyHelper.createLorebookEntries(book_name, creates);
if (success) createdCount = creates.length;
else errors.push("创建条目失败。");
}
if (errors.length > 0 && updatedCount === 0 && createdCount === 0) {
return JSON.stringify({
status: "error",
code: "WRITE_FAILED",
message: errors.join(" ")
});
}
return JSON.stringify({
status: "success",
message: `成功更新了 ${updatedCount} 个条目,创建了 ${createdCount} 个条目。`,
data: { updated: updatedCount, created: createdCount }
});
},
create_world_book: async ({ book_name }) => {
const success = await amilyHelper.createLorebook(book_name);
if (success) {
return JSON.stringify({
status: "success",
message: `世界书 "${book_name}" 创建成功。`
});
} else {
return JSON.stringify({
status: "error",
code: "CREATE_FAILED",
message: `创建世界书 "${book_name}" 失败。`
});
}
},
read_character_card: async ({ chid }) => {
const char = charApi.getCharacter(chid);
if (!char) {
return JSON.stringify({
status: "error",
code: "CHAR_NOT_FOUND",
message: "未找到角色。"
});
}
const safeChar = {
name: char.name,
description: char.description,
personality: char.personality,
scenario: char.scenario,
first_mes: char.first_mes,
mes_example: char.mes_example,
alternate_greetings: char.data?.alternate_greetings || []
};
return JSON.stringify({
status: "success",
data: safeChar
}, null, 2);
},
update_character_card: async (args) => {
const { chid, ...updates } = args;
const finalUpdates = args.updates || updates;
const success = await charApi.updateCharacter(chid, finalUpdates);
if (success) {
const updatedFields = Object.keys(finalUpdates).join(', ');
return JSON.stringify({
status: "success",
message: `角色卡更新成功 [ID: ${chid}]。`,
data: { updated_fields: updatedFields }
});
} else {
return JSON.stringify({
status: "error",
code: "UPDATE_FAILED",
message: "更新角色卡失败。请确保您正在编辑当前选中的角色(暂不支持后台编辑其他角色)。"
});
}
},
edit_character_text: async ({ chid, field, diff }) => {
const char = charApi.getCharacter(chid);
if (!char) {
return JSON.stringify({
status: "error",
code: "CHAR_NOT_FOUND",
message: "未找到角色。"
});
}
const allowedFields = ['description', 'personality', 'scenario', 'first_mes', 'mes_example'];
if (!allowedFields.includes(field)) {
return JSON.stringify({
status: "error",
code: "INVALID_FIELD",
message: `无效的字段。允许的字段: ${allowedFields.join(', ')}`
});
}
let content = char[field] || '';
const normalizedDiff = diff
.replace(/-------\s*SEARCH/g, '------- SEARCH')
.replace(/=======\s*/g, '=======')
.replace(/\+\+\+\+\+\+\+\s*REPLACE/g, '+++++++ REPLACE');
const changes = normalizedDiff.split('------- SEARCH');
if (changes[0].trim() === '') changes.shift();
for (const change of changes) {
const parts = change.split('=======');
if (parts.length < 2) continue;
const searchBlock = parts[0].trim();
const replaceBlock = parts[1].split('+++++++ REPLACE')[0].trim();
if (content.includes(searchBlock)) {
content = content.replace(searchBlock, replaceBlock);
continue;
}
const normalizedSearch = searchBlock.replace(/\r\n/g, '\n');
const lines = normalizedSearch.split('\n');
const regexPattern = lines.map(line => line.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('\\r?\\n');
const regex = new RegExp(regexPattern);
const match = content.match(regex);
if (match) {
content = content.replace(match[0], replaceBlock);
continue;
}
return JSON.stringify({
status: "error",
code: "SEARCH_NOT_FOUND",
message: `在字段 '${field}' 中未找到搜索块。`,
suggestion: "请确保 SEARCH 块与现有内容完全匹配(包括空格)。"
});
}
const success = await charApi.updateCharacter(chid, { [field]: content });
if (success) {
return JSON.stringify({
status: "success",
message: `字段 '${field}' 更新成功。`
});
} else {
return JSON.stringify({
status: "error",
code: "UPDATE_FAILED",
message: `更新字段 '${field}' 失败。请确保您正在编辑当前选中的角色(暂不支持后台编辑其他角色)。`
});
}
},
edit_world_info_entry: async ({ book_name, uid, diff }) => {
const entries = await amilyHelper.getLorebookEntries(book_name);
const entry = entries.find(e => String(e.uid) === String(uid));
if (!entry) {
return JSON.stringify({
status: "error",
code: "ENTRY_NOT_FOUND",
message: `在世界书 "${book_name}" 中未找到 UID 为 ${uid} 的条目。`
});
}
let content = entry.content || '';
const normalizedDiff = diff
.replace(/-------\s*SEARCH/g, '------- SEARCH')
.replace(/=======\s*/g, '=======')
.replace(/\+\+\+\+\+\+\+\s*REPLACE/g, '+++++++ REPLACE');
const changes = normalizedDiff.split('------- SEARCH');
if (changes[0].trim() === '') changes.shift();
for (const change of changes) {
const parts = change.split('=======');
if (parts.length < 2) continue;
const searchBlock = parts[0].trim();
const replaceBlock = parts[1].split('+++++++ REPLACE')[0].trim();
if (content.includes(searchBlock)) {
content = content.replace(searchBlock, replaceBlock);
continue;
}
const normalizedSearch = searchBlock.replace(/\r\n/g, '\n');
const lines = normalizedSearch.split('\n');
const regexPattern = lines.map(line => line.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('\\r?\\n');
const regex = new RegExp(regexPattern);
const match = content.match(regex);
if (match) {
content = content.replace(match[0], replaceBlock);
continue;
}
return JSON.stringify({
status: "error",
code: "SEARCH_NOT_FOUND",
message: `在条目内容中未找到搜索块。`,
suggestion: "请确保 SEARCH 块与现有内容完全匹配(包括空格)。"
});
}
const success = await amilyHelper.setLorebookEntries(book_name, [{ uid: entry.uid, content: content }]);
if (success) {
return JSON.stringify({
status: "success",
message: `条目 [${uid}] 更新成功。`
});
} else {
return JSON.stringify({
status: "error",
code: "UPDATE_FAILED",
message: `更新条目 [${uid}] 失败。`
});
}
},
manage_first_message: async ({ action, chid, index, message }) => {
let success = false;
switch (action) {
case 'add':
success = await charApi.addFirstMessage(chid, message);
break;
case 'update':
success = await charApi.updateFirstMessage(chid, index, message);
break;
case 'remove':
success = await charApi.removeFirstMessage(chid, index);
break;
default:
return JSON.stringify({
status: "error",
code: "INVALID_ACTION",
message: "无效的操作。"
});
}
if (success) {
return JSON.stringify({
status: "success",
message: `开场白 ${action} 成功。`
});
} else {
return JSON.stringify({
status: "error",
code: "ACTION_FAILED",
message: `开场白 ${action} 失败。`
});
}
},
create_character: async ({ name }) => {
const result = await charApi.createNewCharacter(name);
if (result === -1) {
return JSON.stringify({
status: "error",
code: "CREATE_FAILED",
message: "创建角色失败。"
});
}
if (result === -2) {
return JSON.stringify({
status: "warning",
code: "CREATE_PENDING",
message: "角色创建请求已发送。请手动刷新。"
});
}
return JSON.stringify({
status: "success",
message: `角色创建成功。`,
data: { id: result }
});
},
simulate_chat: async ({ chid, message }) => {
const char = charApi.getCharacter(chid);
if (!char) {
return JSON.stringify({
status: "error",
code: "CHAR_NOT_FOUND",
message: "未找到角色。"
});
}
const systemPrompt = `You are roleplaying as ${char.name}.
Description: ${char.description}
Personality: ${char.personality}
Scenario: ${char.scenario}
Reply to the user's message in character. Stay in character.`;
const messages = [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: message }
];
try {
const response = await callAi('executor', messages, { temperature: 0.9 });
return JSON.stringify({
status: "success",
data: {
character: char.name,
response: response
}
});
} catch (error) {
return JSON.stringify({
status: "error",
code: "SIMULATION_FAILED",
message: `模拟对话失败: ${error.message}`
});
}
},
set_style_reference: async ({ style }) => {
return JSON.stringify({
status: "success",
message: `样式参考已设置为: ${style}`,
_action: "update_task_state",
_updates: { style_reference: style }
});
},
analyze_entities: async ({ text }) => {
const systemPrompt = `You are an expert World Builder and Entity Extractor.
Analyze the provided text and identify key entities that should have their own World Info (Lorebook) entries.
Focus on:
- Proper Nouns (People, Places, Organizations, Artifacts)
- Unique Concepts (Magic systems, Historical events, Species)
Return a JSON object with a "entities" array. Each entity should have:
- "name": The name of the entity.
- "type": The type (Person, Location, Organization, etc.).
- "description": A brief summary based on the text (1-2 sentences).
- "confidence": A score (0-1) of how important this entity seems.
Output ONLY valid JSON.`;
const messages = [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: text }
];
try {
const response = await callAi('executor', messages, { temperature: 0.1 });
const cleanResponse = response.replace(/```json/g, '').replace(/```/g, '').trim();
return cleanResponse;
} catch (error) {
return JSON.stringify({
status: "error",
code: "ANALYSIS_FAILED",
message: `实体分析失败: ${error.message}`
});
}
},
ask_user: async ({ question }) => {
return JSON.stringify({
status: "success",
message: `已向用户提问: ${question}`,
_action: "stop_and_wait",
data: { question }
});
}
};
export function getToolDefinitions() {
return [
{
name: "read_world_info",
description: "读取世界书的索引(包含关键字和注释的条目列表)。不返回完整内容。",
parameters: {
type: "object",
properties: {
book_name: { type: "string", description: "世界书名称。" }
},
required: ["book_name"]
}
},
{
name: "read_world_entry",
description: "读取特定世界书条目的完整内容。",
parameters: {
type: "object",
properties: {
book_name: { type: "string", description: "世界书名称。" },
uid: { type: "number", description: "要读取的条目 UID。" }
},
required: ["book_name", "uid"]
}
},
{
name: "write_world_info_entry",
description: "创建或更新世界书中的条目。",
parameters: {
type: "object",
properties: {
book_name: { type: "string", description: "世界书名称。" },
entries: {
type: "array",
items: {
type: "object",
properties: {
uid: { type: "number", description: "条目 ID可选用于更新。" },
comment: { type: "string", description: "条目标题/注释。" },
content: { type: "string", description: "条目内容。" },
key: { type: "array", items: { type: "string" }, description: "关键字。" },
enabled: { type: "boolean", description: "是否启用。" },
constant: { type: "boolean", description: "常驻(蓝灯)。" },
position: { type: "string", enum: ["before_character_definition", "after_character_definition", "before_author_note", "after_author_note", "at_depth_as_system"], description: "插入位置。" },
depth: { type: "number", description: "插入深度。" },
scanDepth: { type: "number", description: "扫描深度。" },
exclude_recursion: { type: "boolean", description: "排除递归。" },
prevent_recursion: { type: "boolean", description: "防止递归。" }
}
}
}
},
required: ["book_name", "entries"]
}
},
{
name: "create_world_book",
description: "创建一个新的空世界书。",
parameters: {
type: "object",
properties: {
book_name: { type: "string", description: "新世界书的名称。" }
},
required: ["book_name"]
}
},
{
name: "read_character_card",
description: "读取角色卡数据。",
parameters: {
type: "object",
properties: {
chid: { type: "number", description: "角色 ID。" }
},
required: ["chid"]
}
},
{
name: "update_character_card",
description: "更新角色卡字段(覆盖)。",
parameters: {
type: "object",
properties: {
chid: { type: "number", description: "角色 ID。" },
name: { type: "string" },
description: { type: "string" },
personality: { type: "string" },
scenario: { type: "string" },
first_mes: { type: "string" },
mes_example: { type: "string" }
},
required: ["chid"]
}
},
{
name: "edit_character_text",
description: "使用 搜索/替换 块编辑角色的特定文本字段。",
parameters: {
type: "object",
properties: {
chid: { type: "number", description: "角色 ID。" },
field: { type: "string", enum: ["description", "personality", "scenario", "first_mes", "mes_example"], description: "要编辑的字段。" },
diff: {
type: "string",
description: "一个或多个遵循此确切格式的 搜索/替换 块:\n------- SEARCH\n[exact content to find]\n=======\n[new content to replace with]\n+++++++ REPLACE"
}
},
required: ["chid", "field", "diff"]
}
},
{
name: "edit_world_info_entry",
description: "使用 搜索/替换 块编辑世界书条目的内容。",
parameters: {
type: "object",
properties: {
book_name: { type: "string", description: "世界书名称。" },
uid: { type: "number", description: "条目 UID。" },
diff: {
type: "string",
description: "一个或多个遵循此确切格式的 搜索/替换 块:\n------- SEARCH\n[exact content to find]\n=======\n[new content to replace with]\n+++++++ REPLACE"
}
},
required: ["book_name", "uid", "diff"]
}
},
{
name: "manage_first_message",
description: "添加、更新或删除候补开场白。",
parameters: {
type: "object",
properties: {
action: { type: "string", enum: ["add", "update", "remove"] },
chid: { type: "number", description: "角色 ID。" },
index: { type: "number", description: "开场白索引(更新/删除时必需)。" },
message: { type: "string", description: "开场白内容(添加/更新时必需)。" }
},
required: ["action", "chid"]
}
},
{
name: "create_character",
description: "创建一个新角色卡。",
parameters: {
type: "object",
properties: {
name: { type: "string", description: "新角色的名字。" }
},
required: ["name"]
}
},
{
name: "simulate_chat",
description: "与角色模拟对话以测试其性格和设定。",
parameters: {
type: "object",
properties: {
chid: { type: "number", description: "角色 ID。" },
message: { type: "string", description: "发送给角色的消息。" }
},
required: ["chid", "message"]
}
},
{
name: "set_style_reference",
description: "设置生成内容的风格参考或模板(例如:'黑暗奇幻风格''莎士比亚风格''JSON格式模板')。",
parameters: {
type: "object",
properties: {
style: { type: "string", description: "风格描述或模板内容。" }
},
required: ["style"]
}
},
{
name: "analyze_entities",
description: "分析文本并提取潜在的世界书条目(实体)。",
parameters: {
type: "object",
properties: {
text: { type: "string", description: "要分析的文本。" }
},
required: ["text"]
}
},
{
name: "ask_user",
description: "向用户提问以获取更多信息或确认。这将暂停自动执行并等待用户回复。",
parameters: {
type: "object",
properties: {
question: { type: "string", description: "要问的问题。" }
},
required: ["question"]
}
}
];
}