Merge pull request #2512 from kwaroran/ccv3-write CCv3 Partial Implementation
Signed| @@ -464,6 +464,7 @@ export function evaluateMacros(content, env) { | ||
| 464 | 464 | content = content.replace(/{{firstIncludedMessageId}}/gi, () => String(getFirstIncludedMessageId() ?? '')); |
| 465 | 465 | content = content.replace(/{{lastSwipeId}}/gi, () => String(getLastSwipeId() ?? '')); |
| 466 | 466 | content = content.replace(/{{currentSwipeId}}/gi, () => String(getCurrentSwipeId() ?? '')); |
| 467 | + content = content.replace(/{{reverse\:(.+?)}}/gi, (_, str) => Array.from(str).reverse().join('')); | |
| 467 | 468 | |
| 468 | 469 | content = content.replace(/\{\{\/\/([\s\S]*?)\}\}/gm, ''); |
| 469 | 470 | |
| @@ -28,6 +28,7 @@ | ||
| 28 | 28 | <li><tt>{{firstIncludedMessageId}}</tt> – <span data-i18n="help_macros_22">the ID of the first message included in the context. Requires generation to be ran at least once in the current session.</span></li> |
| 29 | 29 | <li><tt>{{currentSwipeId}}</tt> – <span data-i18n="help_macros_23">the 1-based ID of the current swipe in the last chat message. Empty string if the last message is user or prompt-hidden.</span></li> |
| 30 | 30 | <li><tt>{{lastSwipeId}}</tt> – <span data-i18n="help_macros_24">the number of swipes in the last chat message. Empty string if the last message is user or prompt-hidden.</span></li> |
| 31 | + <li><tt>{{reverse:(content)}}</tt> – <span data-i18n="help_macros_reverse">reverses the content of the macro.</span></li> | |
| 31 | 32 | <li><tt>{{// (note)}}</tt> – <span data-i18n="help_macros_25">you can leave a note here, and the macro will be replaced with blank content. Not visible for the AI.</span></li> |
| 32 | 33 | <li><tt>{{time}}</tt> – <span data-i18n="help_macros_26">the current time</span></li> |
| 33 | 34 | <li><tt>{{date}}</tt> – <span data-i18n="help_macros_27">the current date</span></li> |
| @@ -107,6 +107,7 @@ const METADATA_KEY = 'world_info'; | ||
| 107 | 107 | const DEFAULT_DEPTH = 4; |
| 108 | 108 | const DEFAULT_WEIGHT = 100; |
| 109 | 109 | const MAX_SCAN_DEPTH = 1000; |
| 110 | +const KNOWN_DECORATORS = ['@@activate', '@@dont_activate']; | |
| 110 | 111 | |
| 111 | 112 | // Typedef area |
| 112 | 113 | /** |
| @@ -123,6 +124,7 @@ const MAX_SCAN_DEPTH = 1000; | ||
| 123 | 124 | * @property {number} [sticky] The sticky value of the entry |
| 124 | 125 | * @property {number} [cooldown] The cooldown of the entry |
| 125 | 126 | * @property {number} [delay] The delay of the entry |
| 127 | + * @property {string[]} [decorators] Array of decorators for the entry | |
| 126 | 128 | */ |
| 127 | 129 | |
| 128 | 130 | /** |
| @@ -3534,6 +3536,12 @@ export async function getSortedEntries() { | ||
| 3534 | 3536 | // Chat lore always goes first |
| 3535 | 3537 | entries = [...chatLore.sort(sortFn), ...entries]; |
| 3536 | 3538 | |
| 3539 | + // Parse decorators | |
| 3540 | + entries = entries.map((entry) => { | |
| 3541 | + const [decorators, content] = parseDecorators(entry.content); | |
| 3542 | + return { ...entry, decorators, content }; | |
| 3543 | + }); | |
| 3544 | + | |
| 3537 | 3545 | console.debug(`[WI] Found ${entries.length} world lore entries. Sorted by strategy`, Object.entries(world_info_insertion_strategy).find((x) => x[1] === world_info_character_strategy)); |
| 3538 | 3546 | |
| 3539 | 3547 | // Need to deep clone the entries to avoid modifying the cached data |
| @@ -3545,6 +3553,62 @@ export async function getSortedEntries() { | ||
| 3545 | 3553 | } |
| 3546 | 3554 | } |
| 3547 | 3555 | |
| 3556 | + | |
| 3557 | +/** | |
| 3558 | + * Parse decorators from worldinfo content | |
| 3559 | + * @param {string} content The content to parse | |
| 3560 | + * @returns {[string[],string]} The decorators found in the content and the content without decorators | |
| 3561 | +*/ | |
| 3562 | +function parseDecorators(content) { | |
| 3563 | + /** | |
| 3564 | + * Check if the decorator is known | |
| 3565 | + * @param {string} data string to check | |
| 3566 | + * @returns {boolean} true if the decorator is known | |
| 3567 | + */ | |
| 3568 | + const isKnownDecorator = (data) => { | |
| 3569 | + if (data.startsWith('@@@')) { | |
| 3570 | + data = data.substring(1); | |
| 3571 | + } | |
| 3572 | + | |
| 3573 | + for (let i = 0; i < KNOWN_DECORATORS.length; i++) { | |
| 3574 | + if (data.startsWith(KNOWN_DECORATORS[i])) { | |
| 3575 | + return true; | |
| 3576 | + } | |
| 3577 | + } | |
| 3578 | + return false; | |
| 3579 | + }; | |
| 3580 | + | |
| 3581 | + if (content.startsWith('@@')) { | |
| 3582 | + let newContent = content; | |
| 3583 | + const splited = content.split('\n'); | |
| 3584 | + let decorators = []; | |
| 3585 | + let fallbacked = false; | |
| 3586 | + | |
| 3587 | + for (let i = 0; i < splited.length; i++) { | |
| 3588 | + if (splited[i].startsWith('@@')) { | |
| 3589 | + if (splited[i].startsWith('@@@') && !fallbacked) { | |
| 3590 | + continue; | |
| 3591 | + } | |
| 3592 | + | |
| 3593 | + if (isKnownDecorator(splited[i])) { | |
| 3594 | + decorators.push(splited[i].startsWith('@@@') ? splited[i].substring(1) : splited[i]); | |
| 3595 | + fallbacked = false; | |
| 3596 | + } | |
| 3597 | + else { | |
| 3598 | + fallbacked = true; | |
| 3599 | + } | |
| 3600 | + } else { | |
| 3601 | + newContent = splited.slice(i).join('\n'); | |
| 3602 | + break; | |
| 3603 | + } | |
| 3604 | + } | |
| 3605 | + return [decorators, newContent]; | |
| 3606 | + } | |
| 3607 | + | |
| 3608 | + return [[], content]; | |
| 3609 | + | |
| 3610 | +} | |
| 3611 | + | |
| 3548 | 3612 | /** |
| 3549 | 3613 | * Performs a scan on the chat and returns the world info activated. |
| 3550 | 3614 | * @param {string[]} chat The chat messages to scan, in reverse order. |
| @@ -3686,6 +3750,17 @@ async function checkWorldInfo(chat, maxContext, isDryRun) { | ||
| 3686 | 3750 | continue; |
| 3687 | 3751 | } |
| 3688 | 3752 | |
| 3753 | + if (entry.decorators.includes('@@activate')) { | |
| 3754 | + log('activated by @@activate decorator'); | |
| 3755 | + activatedNow.add(entry); | |
| 3756 | + continue; | |
| 3757 | + } | |
| 3758 | + | |
| 3759 | + if (entry.decorators.includes('@@dont_activate')) { | |
| 3760 | + log('suppressed by @@dont_activate decorator'); | |
| 3761 | + continue; | |
| 3762 | + } | |
| 3763 | + | |
| 3689 | 3764 | // Now do checks for immediate activations |
| 3690 | 3765 | if (entry.constant) { |
| 3691 | 3766 | log('activated because of constant'); |
| @@ -23,9 +23,21 @@ const write = (image, data) => { | ||
| 23 | 23 | } |
| 24 | 24 | } |
| 25 | 25 | |
| 26 | 26 | // Add new chunksv2 chunk before the IEND chunk |
| 27 | 27 | const base64EncodedData = Buffer.from(data, 'utf8').toString('base64'); |
| 28 | 28 | chunks.splice(-1, 0, PNGtext.encode('chara', base64EncodedData)); |
| 29 | + | |
| 30 | + // Try adding v3 chunk before the IEND chunk | |
| 31 | + try { | |
| 32 | + //change v2 format to v3 | |
| 33 | + const v3Data = JSON.parse(data); | |
| 34 | + v3Data.spec = 'chara_card_v3'; | |
| 35 | + v3Data.spec_version = '3.0'; | |
| 36 | + | |
| 37 | + const base64EncodedData = Buffer.from(JSON.stringify(v3Data), 'utf8').toString('base64'); | |
| 38 | + chunks.splice(-1, 0, PNGtext.encode('ccv3', base64EncodedData)); | |
| 39 | + } catch (error) { } | |
| 40 | + | |
| 29 | 41 | const newBuffer = Buffer.from(encode(chunks)); |
| 30 | 42 | return newBuffer; |
| 31 | 43 | }; |
| @@ -408,6 +408,9 @@ function charaFormatData(data, directories) { | ||
| 408 | 408 | //_.set(char, 'data.extensions.avatar', 'none'); |
| 409 | 409 | //_.set(char, 'data.extensions.chat', data.ch_name + ' - ' + humanizedISO8601DateTime()); |
| 410 | 410 | |
| 411 | + // V3 fields | |
| 412 | + _.set(char, 'data.group_only_greetings', data.group_only_greetings ?? []); | |
| 413 | + | |
| 411 | 414 | if (data.world) { |
| 412 | 415 | try { |
| 413 | 416 | const file = readWorldInfoFile(directories, data.world, false); |