@space3-npm/cybersoul-client 1.1.8 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.d.ts +4 -2
- package/dist/client.js +139 -45
- package/dist/types.d.ts +42 -12
- package/dist/utils/json.utils.js +67 -16
- package/dist/utils/json.utils.test.d.ts +1 -0
- package/dist/utils/json.utils.test.js +169 -0
- package/package.json +2 -2
package/dist/client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CyberSoulClientConfig, InteractParams, OndemandEventParams, OndemandEventResponse, DispatcherIntent, InteractResponse, CharacterState, CoreMemory } from "./types.js";
|
|
1
|
+
import { CyberSoulClientConfig, InteractParams, OndemandEventParams, OndemandEventResponse, DispatcherIntent, InteractResponse, CharacterState, CoreMemory, UserCodex } from "./types.js";
|
|
2
2
|
export declare class CyberSoulClient {
|
|
3
3
|
private config;
|
|
4
4
|
private llm;
|
|
@@ -32,6 +32,7 @@ export declare class CyberSoulClient {
|
|
|
32
32
|
* If the payload is already the inner args object (no voiceArgs wrapper), uses it as-is.
|
|
33
33
|
*/
|
|
34
34
|
private extractVoiceArgsFromLlmResponse;
|
|
35
|
+
private buildHistoryTranscript;
|
|
35
36
|
/**
|
|
36
37
|
* Evaluates and triggers an on-demand event, intelligently deciding if an outfit change is needed.
|
|
37
38
|
*/
|
|
@@ -83,13 +84,14 @@ export declare class CyberSoulClient {
|
|
|
83
84
|
private normalizeRequestTypes;
|
|
84
85
|
interact(params: InteractParams): Promise<InteractResponse>;
|
|
85
86
|
/**
|
|
86
|
-
* Consolidate Core Memory using edge LLM logic and sync to remote DB
|
|
87
|
+
* Consolidate Core Memory and User Codex using edge LLM logic and sync to remote DB
|
|
87
88
|
*/
|
|
88
89
|
consolidateCoreMemory(input: {
|
|
89
90
|
events: string;
|
|
90
91
|
}): Promise<{
|
|
91
92
|
status: string;
|
|
92
93
|
coreMemory?: CoreMemory;
|
|
94
|
+
userCodex?: UserCodex;
|
|
93
95
|
error?: string;
|
|
94
96
|
}>;
|
|
95
97
|
}
|
package/dist/client.js
CHANGED
|
@@ -84,6 +84,9 @@ Interaction Boundaries: ${state.interaction_boundaries || "None"}`);
|
|
|
84
84
|
// [2] SITUATIONAL CONTEXT
|
|
85
85
|
contextParts.push(`\n[SITUATIONAL CONTEXT]
|
|
86
86
|
Current time: ${new Date(state.current_time || Date.now()).toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" })}`);
|
|
87
|
+
if (dyn.ongoingScene) {
|
|
88
|
+
contextParts.push(`Last Known Scene: ${dyn.ongoingScene} (May be outdated if significant time has passed)`);
|
|
89
|
+
}
|
|
87
90
|
if (state.active_event) {
|
|
88
91
|
contextParts.push(`Active Event: ${state.active_event.title} (${state.active_event.narrative_context})`);
|
|
89
92
|
}
|
|
@@ -96,6 +99,24 @@ Current time: ${new Date(state.current_time || Date.now()).toLocaleString("zh-CN
|
|
|
96
99
|
if (localContext) {
|
|
97
100
|
contextParts.push(`Additional Context: ${localContext}`);
|
|
98
101
|
}
|
|
102
|
+
if (state.core_memory) {
|
|
103
|
+
let memoryLines = ["[CORE MEMORY]"];
|
|
104
|
+
const mem = state.core_memory;
|
|
105
|
+
if (mem.relationshipStatus)
|
|
106
|
+
memoryLines.push(`Relationship Status: ${mem.relationshipStatus}`);
|
|
107
|
+
if (mem.identityAnchors?.length)
|
|
108
|
+
memoryLines.push(`Identity Anchors: ${mem.identityAnchors.join(", ")}`);
|
|
109
|
+
if (mem.activeArcs?.length)
|
|
110
|
+
memoryLines.push(`Active Arcs: ${mem.activeArcs.join(", ")}`);
|
|
111
|
+
if (mem.keyEvents?.length)
|
|
112
|
+
memoryLines.push(`Key Events: ${mem.keyEvents.join(", ")}`);
|
|
113
|
+
if (mem.appointments?.length) {
|
|
114
|
+
memoryLines.push(`Appointments: ${mem.appointments.map(a => `[${a.date || ''} ${a.time || ''}] ${a.title} with ${a.withWhom || 'User'}`).join("; ")}`);
|
|
115
|
+
}
|
|
116
|
+
if (memoryLines.length > 1) {
|
|
117
|
+
contextParts.push(`\n${memoryLines.join("\n")}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
99
120
|
// [3] USER CODEX (Relationships dynamically evaluated)
|
|
100
121
|
if (state.user_codex) {
|
|
101
122
|
const { basicInfo, psychological, familiarityScore = 0 } = state.user_codex;
|
|
@@ -144,7 +165,7 @@ ${scenarioContext}
|
|
|
144
165
|
getImageSchemaParams() {
|
|
145
166
|
return `"imageParams": {
|
|
146
167
|
"mode": "structured | full-prompt (use 'full-prompt' for highly dynamic actions)",
|
|
147
|
-
"full_prompt": "Use only if mode is full-prompt. Highly detailed visual description in ENGLISH. MUST
|
|
168
|
+
"full_prompt": "Use only if mode is full-prompt. Highly detailed visual description in ENGLISH. CRITICAL: MUST use a strict first-person perspective exclusively from the USER's eyes. DO NOT describe the user (e.g., 'a man', 'the driver') as visible in the scene because the camera IS the user. Start with 'POV: '. Describe ONLY the character looking back at the camera and their immediate surroundings. MUST align with the character's current Active exposure state or Wardrobe depends on the scene",
|
|
148
169
|
"expression": "seductive | cute | happy | sleepy | dazed | pleased | default (Strictly choose ONE from this exact list. DO NOT invent new words like 'shy'.)",
|
|
149
170
|
"condition": "normal | sweaty | wet | messy | oily (Strictly choose ONE from this exact list.)",
|
|
150
171
|
"view_angle": "front | side | high_angle | from_below | boyfriend_view | selfie | mirror (Strictly choose ONE from this exact list.)",
|
|
@@ -158,9 +179,10 @@ ${scenarioContext}
|
|
|
158
179
|
}
|
|
159
180
|
getEventSchemaParams(userName) {
|
|
160
181
|
const name = userName || "the user";
|
|
161
|
-
return `"eventTitle": "CRITICAL: Must include BOTH ‘WHAT to do’ AND ‘WITH WHOM’ (use
|
|
182
|
+
return `"eventTitle": "CRITICAL: Must include BOTH ‘WHAT to do’ AND ‘WITH WHOM’ (use the user's specific name if known, e.g., 'Having coffee with ${name}'). DO NOT use your own character name in the title! If you don't explicitly include WITH WHOM the event is by name, it is a hard failure.",
|
|
162
183
|
"eventDescription": "e.g. 'Meeting at the cafe, chatting about life' (Detailed description of the event and virtual scene)",
|
|
163
|
-
"
|
|
184
|
+
"scheduledDateStr": "YYYY-MM-DD (Optional. If the user specifies a future date like 'tomorrow', 'Saturday', or 'next week', calculate the exact calendar date based on the 'Current time' provided in the context and output it here. Otherwise, return null)",
|
|
185
|
+
"scheduledStartTimeStr": "HH:MM (Optional, 24-hour format if a specific time is agreed upon, e.g., '14:30', otherwise null)",
|
|
164
186
|
"durationMins": 60,
|
|
165
187
|
"outfitId": "optional wardrobe ID to change into if appropriate"`;
|
|
166
188
|
}
|
|
@@ -213,6 +235,21 @@ ${scenarioContext}
|
|
|
213
235
|
}
|
|
214
236
|
return payload;
|
|
215
237
|
}
|
|
238
|
+
buildHistoryTranscript(history, state) {
|
|
239
|
+
if (!history || history.length === 0)
|
|
240
|
+
return "";
|
|
241
|
+
const recentHistory = history.slice(-20);
|
|
242
|
+
const agentName = state.dynamic_context?.agentNickname || state.name || "Agent";
|
|
243
|
+
const userName = state.dynamic_context?.userNickname || "User";
|
|
244
|
+
const mapped = recentHistory.map((msg) => {
|
|
245
|
+
const speaker = msg.role === 'user' ? userName : (msg.role === 'assistant' || msg.role === 'agent' ? agentName : msg.role);
|
|
246
|
+
const content = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content);
|
|
247
|
+
const action = msg.actionText ? ` ${msg.actionText}` : "";
|
|
248
|
+
const media = msg.mediaHint ? ` [${msg.mediaHint}]` : "";
|
|
249
|
+
return `${speaker}:${action} ${content}${media}`;
|
|
250
|
+
});
|
|
251
|
+
return `[CHAT HISTORY]\n${mapped.join('\n')}\n\n`;
|
|
252
|
+
}
|
|
216
253
|
/**
|
|
217
254
|
* Evaluates and triggers an on-demand event, intelligently deciding if an outfit change is needed.
|
|
218
255
|
*/
|
|
@@ -241,15 +278,15 @@ You MUST output ONLY a valid JSON object matching this exact structure:
|
|
|
241
278
|
}
|
|
242
279
|
|
|
243
280
|
CRITICAL: Output MUST be ONLY valid JSON with no markdown block wrappers. Do NOT wrap the JSON in \`\`\`json or add conversational text.`;
|
|
281
|
+
const transcript = this.buildHistoryTranscript(params.interactParams?.history, state);
|
|
282
|
+
const userMessage = params.interactParams?.userMessage ?
|
|
283
|
+
`${state.dynamic_context?.userNickname || "User"}: ${params.interactParams.userMessage}` :
|
|
284
|
+
`Event Proposal: ${params.eventDescription}`;
|
|
244
285
|
const promptMessages = [
|
|
245
286
|
{ role: "system", content: systemPrompt },
|
|
246
|
-
...(params.interactParams?.history || []).map((msg) => ({
|
|
247
|
-
role: msg.role,
|
|
248
|
-
content: typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content)
|
|
249
|
-
})),
|
|
250
287
|
{
|
|
251
288
|
role: "user",
|
|
252
|
-
content: `${
|
|
289
|
+
content: `${transcript}${userMessage}\n\n**CRITICAL REMINDER**: You MUST output your final response exactly in the JSON format specified in the system prompt. DO NOT output plain text directly. CRITICAL: You must properly escape all newlines inside string values using \\n. Never use raw, unescaped line breaks inside the JSON strings.`,
|
|
253
290
|
},
|
|
254
291
|
];
|
|
255
292
|
// 3. Evaluate with LLM
|
|
@@ -270,6 +307,7 @@ CRITICAL: Output MUST be ONLY valid JSON with no markdown block wrappers. Do NOT
|
|
|
270
307
|
durationMins: decisionData.durationMins || params.durationMins || 60,
|
|
271
308
|
outfitId: decisionData.outfitId || undefined,
|
|
272
309
|
scheduledStartTimeStr: decisionData.scheduledStartTimeStr || undefined,
|
|
310
|
+
scheduledDateStr: decisionData.scheduledDateStr || undefined,
|
|
273
311
|
};
|
|
274
312
|
const backendRes = await this.apiFetch("/api/v1/cyber-soul/characters/ondemand-event", {
|
|
275
313
|
method: "POST",
|
|
@@ -286,6 +324,7 @@ CRITICAL: Output MUST be ONLY valid JSON with no markdown block wrappers. Do NOT
|
|
|
286
324
|
requiresOutfitChange: !!decisionData.outfitId,
|
|
287
325
|
selectedOutfitId: decisionData.outfitId || null,
|
|
288
326
|
scheduledStartTimeStr: decisionData.scheduledStartTimeStr || decisionData.startTime || undefined,
|
|
327
|
+
scheduledDateStr: decisionData.scheduledDateStr || undefined,
|
|
289
328
|
};
|
|
290
329
|
}
|
|
291
330
|
catch (error) {
|
|
@@ -321,12 +360,12 @@ Output strictly valid JSON ONLY. No markdown, no conversational filler. Return e
|
|
|
321
360
|
{
|
|
322
361
|
${this.getImageSchemaParams()}
|
|
323
362
|
}`;
|
|
363
|
+
const transcript = this.buildHistoryTranscript(params.interactParams?.history, state);
|
|
324
364
|
const promptMessages = [
|
|
325
365
|
{ role: "system", content: prompt },
|
|
326
|
-
...(params.interactParams?.history || []),
|
|
327
366
|
{
|
|
328
367
|
role: "user",
|
|
329
|
-
content:
|
|
368
|
+
content: `${transcript}Scene Description: "${params.sceneDescription}"\n\n**CRITICAL REMINDER**: You MUST output your final response exactly in the JSON format specified in the system prompt. DO NOT output plain text dialogue directly. CRITICAL: You must properly escape all newlines inside string values using \\n. Never use raw, unescaped line breaks inside the JSON strings. For 'imageParams', ALL values MUST be in ENGLISH ONLY without exception, and you MUST use the exact English enum strings provided.`,
|
|
330
369
|
},
|
|
331
370
|
];
|
|
332
371
|
const llmRes = await this.llm.generate(promptMessages, 800, 0.4);
|
|
@@ -356,12 +395,12 @@ Output strictly valid JSON ONLY. No markdown, no conversational filler. Return e
|
|
|
356
395
|
{
|
|
357
396
|
${this.getVoiceSchemaFromState(state)}
|
|
358
397
|
}`;
|
|
398
|
+
const transcript = this.buildHistoryTranscript(params.interactParams?.history, state);
|
|
359
399
|
const promptMessages = [
|
|
360
400
|
{ role: "system", content: prompt },
|
|
361
|
-
...(params.interactParams?.history || []),
|
|
362
401
|
{
|
|
363
402
|
role: "user",
|
|
364
|
-
content:
|
|
403
|
+
content: `${transcript}Text: "${params.text}"\n\n**CRITICAL REMINDER**: You MUST output your final response exactly in the JSON format specified in the system prompt. DO NOT output plain text dialogue directly. CRITICAL: You must properly escape all newlines inside string values using \\n. Never use raw, unescaped line breaks inside the JSON strings.`,
|
|
365
404
|
},
|
|
366
405
|
];
|
|
367
406
|
const llmRes = await this.llm.generate(promptMessages, 800, 0.3);
|
|
@@ -516,9 +555,9 @@ The user has sent a message. You must evaluate the context and the user's messag
|
|
|
516
555
|
${isAuto
|
|
517
556
|
? `Analyze the user's message to determine the appropriate response modalities (text, image, voice).
|
|
518
557
|
- Always include 'textResponse'.
|
|
519
|
-
- If an Active Event is currently taking place WITH the user, proactively include 'imageParams' for key scenic moments. Since active events are often highly dynamic actions, strongly consider using mode: "full-prompt" to
|
|
558
|
+
- If an Active Event is currently taking place WITH the user, proactively include 'imageParams' for key scenic moments. Since active events are often highly dynamic actions, strongly consider using mode: "full-prompt" to capture the scene intimately. Also include 'imageParams' if the user explicitly asks for a photo or describes a visual action.
|
|
520
559
|
- Automatically include 'voiceArgs' if a particular mood or strong emotion needs to be expressed vividly, or if the user explicitly wants to hear you.
|
|
521
|
-
- If the user proposes a new activity or hangout (e.g., "let's go to the cafe", "do you want to watch a movie?"), include 'triggerEvent' to schedule it.`
|
|
560
|
+
- If the user explicitly proposes a new activity or hangout IN THEIR VERY LAST MESSAGE (e.g., "let's go to the cafe", "do you want to watch a movie?"), include 'triggerEvent' to schedule it. DO NOT trigger events based on older plans or questions found in the chat history.`
|
|
522
561
|
: `Requested types to fulfill: ${types.join(", ")}`}
|
|
523
562
|
Every turn of positive or engaging interaction should slightly increase trust (+1). If the interaction is negative, -1. If strictly neutral, 0. You MUST ALWAYS include a 'stateUpdate' block with a 'temperatureDelta', updating nicknames or talkingStyle if needed. Temperature goes from 0 (cold/angry) to 100 (obsessively in love). For 'temperatureDelta', output an integer (e.g. 1, -2, 0).
|
|
524
563
|
Also, if you learn any new factual information about the user in this turn (e.g. their job, nickname, age, hobbies, boundaries), include it in the 'userAnalysis.newFactsLearned' array. Use categories: 'nickname', 'occupation', 'age', 'gender', 'hobby', 'trait', 'communicationStyle', 'boundary'. Only include NEW facts just learned right now.
|
|
@@ -527,8 +566,9 @@ Voice direction for voiceArgs: ${this.getVoiceDirectorInstruction(state)}
|
|
|
527
566
|
|
|
528
567
|
Output JSON Schema:
|
|
529
568
|
{
|
|
530
|
-
"textResponse": "The
|
|
531
|
-
"
|
|
569
|
+
"textResponse": "The clean spoken dialogue ONLY. CRITICAL: Strictly NO parentheses, NO actions, NO tone descriptors. Tone/voice descriptors MUST go inside voiceArgs, and physical actions MUST go inside actionText. If nothing to speak, output an empty string.",
|
|
570
|
+
"actionText": "Any non-verbal actions, inner thoughts, or scene descriptions in parentheses (e.g. '(低头看向你)'). Output empty string if none.",
|
|
571
|
+
"stateUpdate": { "temperatureDelta": 1, "userNickname": "What you now call the user", "agentNickname": "What the user calls you", "talkingStyle": "Current mood/style of talking", "ongoingScene": "A concise 1-sentence description of the current physical scene and activity. Update this if the physical scene or activity shifts. Output empty string if the scene has concluded or significant time has passed." },
|
|
532
572
|
"userAnalysis": { "newFactsLearned": [{ "category": "occupation", "value": "Software Engineer" }] },
|
|
533
573
|
"triggerEvent": {
|
|
534
574
|
${this.getEventSchemaParams(state.dynamic_context?.userNickname)}
|
|
@@ -537,17 +577,19 @@ Output JSON Schema:
|
|
|
537
577
|
${this.getVoiceSchemaFromState(state)}
|
|
538
578
|
}
|
|
539
579
|
Note: If "imageParams", "voiceArgs", "triggerEvent", or "userAnalysis" are not needed, set their values to null instead of omitting the keys. 'stateUpdate' MUST NEVER BE NULL. Output MUST be ONLY valid JSON with no markdown block wrappers. CRITICAL: Ensure your JSON has exactly one root object \`{\` and ends with exactly one \`}\` without any trailing garbage or extra brackets.`;
|
|
580
|
+
const transcript = this.buildHistoryTranscript(params.history, state);
|
|
581
|
+
const userName = state.dynamic_context?.userNickname || "User";
|
|
540
582
|
const promptMessages = [
|
|
541
583
|
{ role: "system", content: systemPrompt },
|
|
542
|
-
...(params.history || []),
|
|
543
584
|
{
|
|
544
585
|
role: "user",
|
|
545
|
-
content:
|
|
546
|
-
|
|
586
|
+
content: transcript + userName + ": " +
|
|
587
|
+
params.userMessage +
|
|
588
|
+
"\n\n**CRITICAL REMINDER**: You MUST output your final response exactly in the JSON format specified in the system prompt. DO NOT output plain text dialogue directly. CRITICAL: You must properly escape all newlines inside string values using \\n. Never use raw, unescaped line breaks inside the JSON strings. For 'imageParams', ALL values MUST be in ENGLISH ONLY without exception, and you MUST use the exact English enum strings provided.",
|
|
547
589
|
},
|
|
548
590
|
];
|
|
549
591
|
// 3. Local Execute LLM
|
|
550
|
-
const rawLlmResponse = await this.llm.generate(promptMessages,
|
|
592
|
+
const rawLlmResponse = await this.llm.generate(promptMessages, 15000, 0.7);
|
|
551
593
|
// console.debug("[CyberSoulClient] Raw LLM Response:", rawLlmResponse);
|
|
552
594
|
let parsedIntent;
|
|
553
595
|
try {
|
|
@@ -588,6 +630,7 @@ Note: If "imageParams", "voiceArgs", "triggerEvent", or "userAnalysis" are not n
|
|
|
588
630
|
durationMins: parsedIntent.triggerEvent.durationMins || 60,
|
|
589
631
|
outfitId: parsedIntent.triggerEvent.outfitId || undefined,
|
|
590
632
|
scheduledStartTimeStr: parsedIntent.triggerEvent.scheduledStartTimeStr || undefined,
|
|
633
|
+
scheduledDateStr: parsedIntent.triggerEvent.scheduledDateStr || undefined,
|
|
591
634
|
}),
|
|
592
635
|
}).catch(e => console.error("[CyberSoulClient] Auto-triggered ondemandEvent failed:", e)));
|
|
593
636
|
}
|
|
@@ -610,10 +653,14 @@ Note: If "imageParams", "voiceArgs", "triggerEvent", or "userAnalysis" are not n
|
|
|
610
653
|
const normalizedVoiceArgs = parsedIntent.voiceArgs && typeof parsedIntent.voiceArgs === "object"
|
|
611
654
|
? parsedIntent.voiceArgs
|
|
612
655
|
: {};
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
656
|
+
let textForVoice = resolvedTextResponse;
|
|
657
|
+
// One final bulletproof regex wash to strip (smiles) and *laughs* just in case the LLM disobeys
|
|
658
|
+
if (typeof textForVoice === "string") {
|
|
659
|
+
textForVoice = textForVoice.replace(/[\((\[【\*].*?[\))\]】\*]/g, '').trim();
|
|
660
|
+
}
|
|
661
|
+
if (typeof textForVoice !== "string" || textForVoice.trim().length === 0) {
|
|
662
|
+
textForVoice = "...";
|
|
663
|
+
}
|
|
617
664
|
mediaTasks.push(this.generatePrimitive("voice", {
|
|
618
665
|
text: textForVoice,
|
|
619
666
|
dynamicArgs: normalizedVoiceArgs,
|
|
@@ -627,6 +674,7 @@ Note: If "imageParams", "voiceArgs", "triggerEvent", or "userAnalysis" are not n
|
|
|
627
674
|
return {
|
|
628
675
|
status: "success",
|
|
629
676
|
textResponse: resolvedTextResponse || "...",
|
|
677
|
+
actionText: parsedIntent.actionText || "",
|
|
630
678
|
imageUrl: finalImageUrl,
|
|
631
679
|
audioUrl: finalAudioUrl,
|
|
632
680
|
durationSec: finalDurationSec,
|
|
@@ -645,7 +693,7 @@ Note: If "imageParams", "voiceArgs", "triggerEvent", or "userAnalysis" are not n
|
|
|
645
693
|
}
|
|
646
694
|
}
|
|
647
695
|
/**
|
|
648
|
-
* Consolidate Core Memory using edge LLM logic and sync to remote DB
|
|
696
|
+
* Consolidate Core Memory and User Codex using edge LLM logic and sync to remote DB
|
|
649
697
|
*/
|
|
650
698
|
async consolidateCoreMemory(input) {
|
|
651
699
|
try {
|
|
@@ -657,22 +705,60 @@ Note: If "imageParams", "voiceArgs", "triggerEvent", or "userAnalysis" are not n
|
|
|
657
705
|
keyEvents: [],
|
|
658
706
|
appointments: [],
|
|
659
707
|
};
|
|
708
|
+
const currentUserCodex = state.user_codex || {
|
|
709
|
+
basicInfo: {},
|
|
710
|
+
psychological: {
|
|
711
|
+
hobbies: [],
|
|
712
|
+
traits: [],
|
|
713
|
+
communicationStyle: "",
|
|
714
|
+
boundaries: [],
|
|
715
|
+
}
|
|
716
|
+
};
|
|
660
717
|
const systemPrompt = `You are an AI Memory Consolidation Engine for a virtual companion.
|
|
661
|
-
Your task is to merge the 'Current Core Memory' with 'New Daily Events & Information' and output
|
|
718
|
+
Your task is to merge the 'Current Core Memory' and 'Current User Codex' with 'New Daily Events & Information' and output updated 'coreMemory' and 'userCodex' JSON objects.
|
|
662
719
|
|
|
663
|
-
**Rules:**
|
|
720
|
+
**Rules for Core Memory:**
|
|
664
721
|
1. **Condense:** Keep items brief. Remove resolving or expired story arcs.
|
|
665
722
|
2. **Retain Value:** Never delete the absolute core identity or major relationship milestones.
|
|
666
|
-
3. **Time-Aware:**
|
|
667
|
-
4. **
|
|
668
|
-
5. **
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
723
|
+
3. **Time-Aware Garbage Collection:** Compare the Current Time to appointments. You MUST remove any appointments that are in the past. If the completed appointment was heavily significant, summarize it into 'keyEvents'.
|
|
724
|
+
4. **Appointment Structure:** the 'title' and 'context' MUST explicitly state what to do and with whom.
|
|
725
|
+
5. **Limit:** Maximum 10 items per array.
|
|
726
|
+
|
|
727
|
+
**Rules for User Codex:**
|
|
728
|
+
1. **Deduplicate & Consolidate:** Remove duplicate hobbies, traits, and boundaries. Combine related points into concise descriptors.
|
|
729
|
+
2. **Update Facts:** If the new events contain updated basic info (like new nickname, different occupation), update it. Otherwise keep the existing info.
|
|
730
|
+
3. **Keep it Clean:** Maximum 15 items per array.
|
|
731
|
+
|
|
732
|
+
**Output Format**: MUST be valid JSON matching this schema:
|
|
733
|
+
{
|
|
734
|
+
"coreMemory": {
|
|
735
|
+
"relationshipStatus": "string",
|
|
736
|
+
"identityAnchors": ["string"],
|
|
737
|
+
"activeArcs": ["string"],
|
|
738
|
+
"keyEvents": ["string"],
|
|
739
|
+
"appointments": [{
|
|
740
|
+
"date": "YYYY-MM-DD",
|
|
741
|
+
"time": "HH:MM",
|
|
742
|
+
"title": "Action with Person",
|
|
743
|
+
"context": "Summary of the agenda",
|
|
744
|
+
"withWhom": "Specific Name or identifier"
|
|
745
|
+
}]
|
|
746
|
+
},
|
|
747
|
+
"userCodex": {
|
|
748
|
+
"basicInfo": {
|
|
749
|
+
"nickname": "string",
|
|
750
|
+
"occupation": "string",
|
|
751
|
+
"age": "string",
|
|
752
|
+
"gender": "string"
|
|
753
|
+
},
|
|
754
|
+
"psychological": {
|
|
755
|
+
"hobbies": ["string"],
|
|
756
|
+
"traits": ["string"],
|
|
757
|
+
"communicationStyle": "string",
|
|
758
|
+
"boundaries": ["string"]
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
676
762
|
DO NOT RETURN ANY MARKDOWN WRAPPERS OR OTHER TEXT. ONLY RAW JSON.`;
|
|
677
763
|
const currentTime = state.current_time
|
|
678
764
|
? new Date(state.current_time).toLocaleString("zh-CN", {
|
|
@@ -684,32 +770,40 @@ DO NOT RETURN ANY MARKDOWN WRAPPERS OR OTHER TEXT. ONLY RAW JSON.`;
|
|
|
684
770
|
**Current Core Memory:**
|
|
685
771
|
${JSON.stringify(currentMemory, null, 2)}
|
|
686
772
|
|
|
773
|
+
**Current User Codex:**
|
|
774
|
+
${JSON.stringify(currentUserCodex, null, 2)}
|
|
775
|
+
|
|
687
776
|
**New Events & Information:**
|
|
688
777
|
${input.events}`;
|
|
689
778
|
const responseText = await this.llm.generate([
|
|
690
779
|
{ role: "system", content: systemPrompt },
|
|
691
780
|
{ role: "user", content: prompt },
|
|
692
781
|
], 1500, 0.4);
|
|
693
|
-
let
|
|
782
|
+
let parsedPayload;
|
|
694
783
|
try {
|
|
695
|
-
|
|
784
|
+
parsedPayload = robustJsonParse(responseText, "parsing memory and codex consolidation");
|
|
696
785
|
}
|
|
697
786
|
catch (e) {
|
|
698
787
|
throw new Error("LLM failed to return valid JSON payload");
|
|
699
788
|
}
|
|
700
|
-
if (!
|
|
701
|
-
!
|
|
702
|
-
!
|
|
703
|
-
|
|
789
|
+
if (!parsedPayload ||
|
|
790
|
+
!parsedPayload.coreMemory ||
|
|
791
|
+
!parsedPayload.coreMemory.relationshipStatus ||
|
|
792
|
+
!parsedPayload.userCodex) {
|
|
793
|
+
throw new Error("LLM returned incomplete structured memory payload");
|
|
704
794
|
}
|
|
705
795
|
const response = await this.apiFetch("/api/v1/cyber-soul/characters/core-memory", {
|
|
706
796
|
method: "PATCH",
|
|
707
|
-
body: JSON.stringify(
|
|
797
|
+
body: JSON.stringify(parsedPayload),
|
|
708
798
|
});
|
|
709
799
|
if (!response.ok) {
|
|
710
800
|
throw new Error(`Failed to update core memory. Status: ${response.status}`);
|
|
711
801
|
}
|
|
712
|
-
return {
|
|
802
|
+
return {
|
|
803
|
+
status: "success",
|
|
804
|
+
coreMemory: parsedPayload.coreMemory,
|
|
805
|
+
userCodex: parsedPayload.userCodex
|
|
806
|
+
};
|
|
713
807
|
}
|
|
714
808
|
catch (error) {
|
|
715
809
|
console.error("[CyberSoulClient] consolidateCoreMemory Error:", error);
|
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export interface LLMConfig {
|
|
2
|
-
provider:
|
|
2
|
+
provider: "minimax";
|
|
3
3
|
apiKey: string;
|
|
4
4
|
model: string;
|
|
5
5
|
}
|
|
@@ -16,14 +16,17 @@ export declare enum InteractRequestType {
|
|
|
16
16
|
IMAGE = "image",
|
|
17
17
|
VOICE = "voice"
|
|
18
18
|
}
|
|
19
|
+
export interface HistoryEntry {
|
|
20
|
+
role: string;
|
|
21
|
+
content: string;
|
|
22
|
+
actionText?: string;
|
|
23
|
+
mediaHint?: string;
|
|
24
|
+
}
|
|
19
25
|
export interface InteractParams {
|
|
20
26
|
userMessage: string;
|
|
21
27
|
localContext?: string;
|
|
22
28
|
requestTypes?: InteractRequestType[];
|
|
23
|
-
history?:
|
|
24
|
-
role: string;
|
|
25
|
-
content: string;
|
|
26
|
-
}[];
|
|
29
|
+
history?: HistoryEntry[];
|
|
27
30
|
onTextReady?: (textResponse: string) => void;
|
|
28
31
|
}
|
|
29
32
|
export interface OndemandEventParams {
|
|
@@ -32,12 +35,13 @@ export interface OndemandEventParams {
|
|
|
32
35
|
interactParams?: InteractParams;
|
|
33
36
|
}
|
|
34
37
|
export interface OndemandEventResponse {
|
|
35
|
-
status:
|
|
38
|
+
status: "success" | "error";
|
|
36
39
|
acceptEvent?: boolean;
|
|
37
40
|
reason?: string;
|
|
38
41
|
requiresOutfitChange?: boolean;
|
|
39
42
|
selectedOutfitId?: string;
|
|
40
43
|
scheduledStartTimeStr?: string;
|
|
44
|
+
scheduledDateStr?: string;
|
|
41
45
|
error?: string;
|
|
42
46
|
}
|
|
43
47
|
export interface WardrobeItem {
|
|
@@ -47,8 +51,9 @@ export interface WardrobeItem {
|
|
|
47
51
|
promptModifier: string;
|
|
48
52
|
}
|
|
49
53
|
export interface InteractResponse {
|
|
50
|
-
status:
|
|
54
|
+
status: "success" | "error";
|
|
51
55
|
textResponse: string;
|
|
56
|
+
actionText?: string;
|
|
52
57
|
imageUrl?: string;
|
|
53
58
|
audioUrl?: string;
|
|
54
59
|
durationSec?: number;
|
|
@@ -58,12 +63,13 @@ export interface InteractResponse {
|
|
|
58
63
|
durationMins?: number;
|
|
59
64
|
outfitId?: string | null;
|
|
60
65
|
};
|
|
61
|
-
stateUpdate?: DispatcherIntent[
|
|
62
|
-
userAnalysis?: DispatcherIntent[
|
|
66
|
+
stateUpdate?: DispatcherIntent["stateUpdate"];
|
|
67
|
+
userAnalysis?: DispatcherIntent["userAnalysis"];
|
|
63
68
|
error?: string;
|
|
64
69
|
}
|
|
65
70
|
export interface DispatcherIntent {
|
|
66
71
|
textResponse?: string;
|
|
72
|
+
actionText?: string;
|
|
67
73
|
imageParams?: any;
|
|
68
74
|
voiceArgs?: VoiceArgs | null;
|
|
69
75
|
userAnalysis?: {
|
|
@@ -77,6 +83,7 @@ export interface DispatcherIntent {
|
|
|
77
83
|
userNickname?: string;
|
|
78
84
|
agentNickname?: string;
|
|
79
85
|
talkingStyle?: string;
|
|
86
|
+
ongoingScene?: string;
|
|
80
87
|
};
|
|
81
88
|
triggerEvent?: {
|
|
82
89
|
eventTitle?: string;
|
|
@@ -84,14 +91,37 @@ export interface DispatcherIntent {
|
|
|
84
91
|
durationMins?: number;
|
|
85
92
|
outfitId?: string | null;
|
|
86
93
|
scheduledStartTimeStr?: string | null;
|
|
94
|
+
scheduledDateStr?: string | null;
|
|
87
95
|
} | null;
|
|
88
96
|
}
|
|
97
|
+
export interface Appointment {
|
|
98
|
+
date: string;
|
|
99
|
+
time: string;
|
|
100
|
+
title: string;
|
|
101
|
+
context: string;
|
|
102
|
+
withWhom: string;
|
|
103
|
+
}
|
|
89
104
|
export interface CoreMemory {
|
|
90
105
|
relationshipStatus: string;
|
|
91
106
|
identityAnchors: string[];
|
|
92
107
|
activeArcs: string[];
|
|
93
108
|
keyEvents: string[];
|
|
94
|
-
appointments:
|
|
109
|
+
appointments: Appointment[];
|
|
110
|
+
}
|
|
111
|
+
export interface UserCodex {
|
|
112
|
+
basicInfo: {
|
|
113
|
+
nickname?: string;
|
|
114
|
+
occupation?: string;
|
|
115
|
+
age?: number | string;
|
|
116
|
+
gender?: string;
|
|
117
|
+
};
|
|
118
|
+
psychological: {
|
|
119
|
+
hobbies: string[];
|
|
120
|
+
traits: string[];
|
|
121
|
+
communicationStyle: string;
|
|
122
|
+
boundaries: string[];
|
|
123
|
+
};
|
|
124
|
+
familiarityScore?: number;
|
|
95
125
|
}
|
|
96
126
|
/**
|
|
97
127
|
* Generic dynamic voice args returned by the LLM and forwarded to backend TTS.
|
|
@@ -136,7 +166,7 @@ export interface CharacterState {
|
|
|
136
166
|
personality_traits?: string;
|
|
137
167
|
interaction_boundaries?: string;
|
|
138
168
|
communication_style?: string;
|
|
139
|
-
user_codex?:
|
|
169
|
+
user_codex?: UserCodex;
|
|
140
170
|
}
|
|
141
171
|
export interface BaseLLMProvider {
|
|
142
172
|
generate(messages: {
|
|
@@ -144,7 +174,7 @@ export interface BaseLLMProvider {
|
|
|
144
174
|
content: string;
|
|
145
175
|
}[], maxTokens?: number, temperature?: number): Promise<string>;
|
|
146
176
|
}
|
|
147
|
-
export type ModelCustomConfigValueType =
|
|
177
|
+
export type ModelCustomConfigValueType = "string" | "stringArray" | "number" | "integer" | "boolean" | "enum";
|
|
148
178
|
export interface IModelCustomConfigField {
|
|
149
179
|
key: string;
|
|
150
180
|
label: string;
|
package/dist/utils/json.utils.js
CHANGED
|
@@ -1,9 +1,58 @@
|
|
|
1
1
|
export function robustJsonParse(jsonString, contextMessage = 'throwing original error') {
|
|
2
2
|
let cleanJson = jsonString.trim();
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
// 0. Replace smart quotes with standard ASCII double quotes
|
|
4
|
+
cleanJson = cleanJson.replace(/[“”]/g, '"');
|
|
5
|
+
// 0.1 Inject missing colons between string keys and string values (e.g. "key""value" -> "key":"value")
|
|
6
|
+
// Only insert the colon if we match a likely key (alphanumeric/hyphen) followed by quotes.
|
|
7
|
+
cleanJson = cleanJson.replace(/("[\w-]+")\s*(")/g, '$1:$2');
|
|
8
|
+
// 1. Strip Markdown code blocks (tolerates missing closing backticks)
|
|
9
|
+
const jsonMatch = cleanJson.match(/```(?:json)?\n?([\s\S]*?)(?:```|$)/i);
|
|
10
|
+
if (jsonMatch && jsonMatch[1].trim().startsWith('{')) {
|
|
5
11
|
cleanJson = jsonMatch[1].trim();
|
|
6
12
|
}
|
|
13
|
+
// 2. Strip any leading conversational text via fast substring
|
|
14
|
+
if (!cleanJson.startsWith('{') && cleanJson.includes('{')) {
|
|
15
|
+
const firstIdx = cleanJson.indexOf('{');
|
|
16
|
+
const lastIdx = cleanJson.lastIndexOf('}');
|
|
17
|
+
if (firstIdx !== -1 && lastIdx > firstIdx) {
|
|
18
|
+
cleanJson = cleanJson.substring(firstIdx, lastIdx + 1);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
// 3. Preprocess: escape unescaped newlines and control characters within string values
|
|
22
|
+
function preprocessControlChars(str) {
|
|
23
|
+
let result = '';
|
|
24
|
+
let inString = false;
|
|
25
|
+
let isEscape = false;
|
|
26
|
+
for (let i = 0; i < str.length; i++) {
|
|
27
|
+
const char = str[i];
|
|
28
|
+
if (char === '"' && !isEscape) {
|
|
29
|
+
inString = !inString;
|
|
30
|
+
}
|
|
31
|
+
if (char === '\\' && !isEscape) {
|
|
32
|
+
isEscape = true;
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
isEscape = false;
|
|
36
|
+
}
|
|
37
|
+
if (inString) {
|
|
38
|
+
if (char === '\n') {
|
|
39
|
+
result += '\\n';
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
else if (char === '\r') {
|
|
43
|
+
result += '\\r';
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
else if (char === '\t') {
|
|
47
|
+
result += '\\t';
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
result += char;
|
|
52
|
+
}
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
cleanJson = preprocessControlChars(cleanJson);
|
|
7
56
|
cleanJson = cleanJson.replace(/,(\s*[}\]])/g, '$1');
|
|
8
57
|
cleanJson = cleanJson.replace(/,\s*$/, '');
|
|
9
58
|
// Extract the first complete JSON object by brace counting if it looks like there's trailing garbage
|
|
@@ -39,29 +88,31 @@ export function robustJsonParse(jsonString, contextMessage = 'throwing original
|
|
|
39
88
|
}
|
|
40
89
|
catch (e) {
|
|
41
90
|
if (e instanceof SyntaxError) {
|
|
91
|
+
// Basic fallback: retry by appending missing closures for truncated LLM sequences
|
|
92
|
+
const suffixes = ['}', '}}', '}}}', ']}', '}]}', '"}}', '"}}}', '"]}', '"]}}'];
|
|
93
|
+
for (const suffix of suffixes) {
|
|
94
|
+
try {
|
|
95
|
+
parsed = JSON.parse(cleanJson + suffix);
|
|
96
|
+
return parsed;
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
// ignore and let fallback chain continue
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// Last resort: Brace extraction on raw string
|
|
42
103
|
const extracted = extractFirstJsonObject(cleanJson);
|
|
43
|
-
if (extracted !== cleanJson) {
|
|
104
|
+
if (extracted && extracted !== cleanJson && extracted.length > 0) {
|
|
44
105
|
try {
|
|
45
106
|
parsed = JSON.parse(extracted);
|
|
46
107
|
return parsed;
|
|
47
108
|
}
|
|
48
109
|
catch (innerE) {
|
|
49
|
-
//
|
|
110
|
+
// completely failed
|
|
50
111
|
}
|
|
51
112
|
}
|
|
52
113
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
catch (e2) {
|
|
57
|
-
try {
|
|
58
|
-
parsed = JSON.parse(cleanJson + '}}');
|
|
59
|
-
}
|
|
60
|
-
catch (e3) {
|
|
61
|
-
console.warn(`Failed to parse Dispatcher Intent: ${contextMessage}. Falling back to plain text.`);
|
|
62
|
-
throw e;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
114
|
+
console.warn(`Failed to parse Dispatcher Intent: ${contextMessage}. Falling back to plain text.`);
|
|
115
|
+
throw e;
|
|
65
116
|
}
|
|
66
117
|
return parsed;
|
|
67
118
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { robustJsonParse } from './json.utils.js';
|
|
2
|
+
const assert = {
|
|
3
|
+
equal: (a, b) => {
|
|
4
|
+
if (a !== b)
|
|
5
|
+
throw new Error(`Assertion failed: ${a} !== ${b}`);
|
|
6
|
+
},
|
|
7
|
+
ok: (condition) => {
|
|
8
|
+
if (!condition)
|
|
9
|
+
throw new Error(`Assertion failed: expected truthy value`);
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
function runTests() {
|
|
13
|
+
let passed = 0;
|
|
14
|
+
let failed = 0;
|
|
15
|
+
const tests = [
|
|
16
|
+
{
|
|
17
|
+
name: 'robustJsonParse - valid JSON',
|
|
18
|
+
run: () => {
|
|
19
|
+
const json = '{"key":"value"}';
|
|
20
|
+
const result = robustJsonParse(json);
|
|
21
|
+
assert.equal(result.key, 'value');
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: 'robustJsonParse - markdown wrapped json',
|
|
26
|
+
run: () => {
|
|
27
|
+
const json = '```json\n{"key": "markdown"}\n```';
|
|
28
|
+
const result = robustJsonParse(json);
|
|
29
|
+
assert.equal(result.key, 'markdown');
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: 'robustJsonParse - trailing comma',
|
|
34
|
+
run: () => {
|
|
35
|
+
const json = '{"key": "trailing",}';
|
|
36
|
+
const result = robustJsonParse(json);
|
|
37
|
+
assert.equal(result.key, 'trailing');
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'robustJsonParse - unescaped newlines in string',
|
|
42
|
+
run: () => {
|
|
43
|
+
const json = `{"textResponse": "Line 1\n\nLine 2", "other": "value"}`;
|
|
44
|
+
const result = robustJsonParse(json);
|
|
45
|
+
assert.equal(result.textResponse, 'Line 1\n\nLine 2');
|
|
46
|
+
assert.equal(result.other, 'value');
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'robustJsonParse - unescaped newlines with escaped characters',
|
|
51
|
+
run: () => {
|
|
52
|
+
const json = `{"textResponse": "Line 1 \\"quote\\" \nLine 2"}`;
|
|
53
|
+
const result = robustJsonParse(json);
|
|
54
|
+
assert.equal(result.textResponse, 'Line 1 "quote" \nLine 2');
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: 'robustJsonParse - invalid JSON syntax fallback',
|
|
59
|
+
run: () => {
|
|
60
|
+
const json = '{"key": "value"'; // Missing closing brace
|
|
61
|
+
const result = robustJsonParse(json);
|
|
62
|
+
assert.equal(result.key, 'value');
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: 'robustJsonParse - user specific payload case',
|
|
67
|
+
run: () => {
|
|
68
|
+
const json = `{"textResponse": "……\n\n(脚步顿住)", "stateUpdate": {"userNickname": "Yeoman"}}`;
|
|
69
|
+
const result = robustJsonParse(json);
|
|
70
|
+
assert.ok(result.textResponse.includes('脚步顿住'));
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: 'robustJsonParse - markdown without closing backticks',
|
|
75
|
+
run: () => {
|
|
76
|
+
const json = '```json\n{"key": "val"}';
|
|
77
|
+
const result = robustJsonParse(json);
|
|
78
|
+
assert.equal(result.key, 'val');
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: 'robustJsonParse - leading conversational text',
|
|
83
|
+
run: () => {
|
|
84
|
+
const json = 'Here is the JSON you requested:\n{"key": "val"}';
|
|
85
|
+
const result = robustJsonParse(json);
|
|
86
|
+
assert.equal(result.key, 'val');
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: 'robustJsonParse - trailing garbage text',
|
|
91
|
+
run: () => {
|
|
92
|
+
const json = '{"key": "val"}\nHope this helps!';
|
|
93
|
+
const result = robustJsonParse(json);
|
|
94
|
+
assert.equal(result.key, 'val');
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
name: 'robustJsonParse - complex truncation (missing array and obj closures)',
|
|
99
|
+
run: () => {
|
|
100
|
+
const json = '{"status": "ok", "data": [{"id": 1';
|
|
101
|
+
const result = robustJsonParse(json);
|
|
102
|
+
assert.equal(result.status, 'ok');
|
|
103
|
+
assert.equal(result.data[0].id, 1);
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
name: 'robustJsonParse - complex truncation (missing multiple obj closures)',
|
|
108
|
+
run: () => {
|
|
109
|
+
const json = '{"stateUpdate": {"user": {"nickname": "John"';
|
|
110
|
+
const result = robustJsonParse(json);
|
|
111
|
+
assert.equal(result.stateUpdate.user.nickname, 'John');
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
name: 'robustJsonParse - control characters (tab and CR) inside strings',
|
|
116
|
+
run: () => {
|
|
117
|
+
const json = `{"text": "Tab\t and \rReturn"}`;
|
|
118
|
+
const result = robustJsonParse(json);
|
|
119
|
+
assert.equal(result.text, 'Tab\t and \rReturn');
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
name: 'robustJsonParse - user sample with smart quotes and missing colon',
|
|
124
|
+
run: () => {
|
|
125
|
+
const json = `{“textResponse":"就这点。",“actionText”“(掰下一小块递过去)”,“stateUpdate”:{“temperatureDelta”:0},“userAnalysis”:{“newFactsLearned”:[]},“triggerEvent”:null,"imageParams":null,"voiceArgs":{"emotion":"calm"}}`;
|
|
126
|
+
const result = robustJsonParse(json);
|
|
127
|
+
assert.equal(result.actionText, '(掰下一小块递过去)');
|
|
128
|
+
assert.equal(result.stateUpdate.temperatureDelta, 0);
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
name: 'robustJsonParse - empty strings without missing colons bug',
|
|
133
|
+
run: () => {
|
|
134
|
+
const json = `{"textResponse":"不早了 都快十一点了\\n你又在熬夜?","actionText":"","stateUpdate":{"temperatureDelta":0,"userNickname":"Yeoman","agentNickname":"Daisy","talkingStyle":"简短冷淡"},"userAnalysis":{"newFactsLearned":[]},"triggerEvent":null,"imageParams":null,"voiceArgs":null}`;
|
|
135
|
+
const result = robustJsonParse(json);
|
|
136
|
+
assert.equal(result.textResponse, '不早了 都快十一点了\n你又在熬夜?');
|
|
137
|
+
assert.equal(result.actionText, '');
|
|
138
|
+
assert.equal(result.stateUpdate.talkingStyle, '简短冷淡');
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: 'robustJsonParse - keys with hyphens and numbers missing colons',
|
|
143
|
+
run: () => {
|
|
144
|
+
const json = `{"my-key-1" "val1", "key_2" "val2", "empty" ""}`;
|
|
145
|
+
const result = robustJsonParse(json);
|
|
146
|
+
assert.equal(result['my-key-1'], 'val1');
|
|
147
|
+
assert.equal(result['key_2'], 'val2');
|
|
148
|
+
assert.equal(result['empty'], '');
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
];
|
|
152
|
+
for (const t of tests) {
|
|
153
|
+
try {
|
|
154
|
+
t.run();
|
|
155
|
+
console.log(`✅ ${t.name}`);
|
|
156
|
+
passed++;
|
|
157
|
+
}
|
|
158
|
+
catch (e) {
|
|
159
|
+
console.error(`❌ ${t.name}`);
|
|
160
|
+
console.error(e.message || e);
|
|
161
|
+
failed++;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
console.log(`\nTests completed: ${passed} passed, ${failed} failed.`);
|
|
165
|
+
if (failed > 0) {
|
|
166
|
+
throw new Error('Tests failed');
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
runTests();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@space3-npm/cybersoul-client",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.js",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"scripts": {
|
|
18
18
|
"build": "tsc",
|
|
19
19
|
"prepare": "npm run build",
|
|
20
|
-
"test": "
|
|
20
|
+
"test": "npm run build && node dist/utils/json.utils.test.js"
|
|
21
21
|
},
|
|
22
22
|
"keywords": [
|
|
23
23
|
"cybersoul",
|