psyche-ai 2.2.0 → 2.3.0

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/README.md CHANGED
@@ -159,6 +159,8 @@ cd openclaw-plugin-psyche && node scripts/diagnose.js
159
159
  - **跨会话记忆** — 重新遇到用户时注入上次对话的情绪记忆
160
160
  - **多 Agent 交互** — 两个 PsycheEngine 实例之间的情绪传染、关系追踪
161
161
  - **流式支持** — Vercel AI SDK `streamText` 中间件,自动缓冲和剥离标签
162
+ - **渠道修饰** — Discord/Slack/飞书/终端等不同渠道自动调整表达风格
163
+ - **自定义人格** — 超越 MBTI 预设,完全自定义 baseline/敏感度/气质
162
164
  - **Compact Mode** — 算法做化学计算,LLM 只看行为指令(~15-180 tokens vs ~550)
163
165
 
164
166
  架构详情见 [ARCHITECTURE.md](ARCHITECTURE.md)。
@@ -168,7 +170,7 @@ cd openclaw-plugin-psyche && node scripts/diagnose.js
168
170
  ```bash
169
171
  npm install
170
172
  npm run build
171
- npm test # 395 tests
173
+ npm test # 469 tests
172
174
  npm run typecheck # strict mode
173
175
  ```
174
176
 
@@ -0,0 +1,28 @@
1
+ import type { Locale } from "./types.js";
2
+ /** Supported channel types */
3
+ export type ChannelType = "discord" | "slack" | "feishu" | "terminal" | "web" | "api" | "custom";
4
+ /** Channel-specific behavioral profile */
5
+ export interface ChannelProfile {
6
+ type: ChannelType;
7
+ allowEmoji: boolean;
8
+ allowKaomoji: boolean;
9
+ formalityLevel: "casual" | "neutral" | "formal";
10
+ maxResponseLength?: number;
11
+ expressionHints: string[];
12
+ }
13
+ /**
14
+ * Get a built-in channel profile by type.
15
+ */
16
+ export declare function getChannelProfile(type: ChannelType): ChannelProfile;
17
+ /**
18
+ * Build a concise prompt snippet that guides expression style for a channel.
19
+ * Returns 2-4 lines of guidance. Does NOT alter chemistry.
20
+ */
21
+ export declare function buildChannelModifier(profile: ChannelProfile, locale: Locale): string;
22
+ /**
23
+ * Create a custom channel profile with user overrides.
24
+ * Starts from the "custom" base and applies overrides.
25
+ */
26
+ export declare function createCustomChannel(overrides: Partial<ChannelProfile> & {
27
+ type: "custom";
28
+ }): ChannelProfile;
@@ -0,0 +1,141 @@
1
+ // ============================================================
2
+ // Channel Profiles — Platform-specific expression modifiers
3
+ //
4
+ // Adjusts expression style per platform/channel WITHOUT changing
5
+ // chemistry. This is a prompt-level modifier only.
6
+ // ============================================================
7
+ // ── Built-in Profiles ────────────────────────────────────────
8
+ const BUILTIN_PROFILES = {
9
+ discord: {
10
+ type: "discord",
11
+ allowEmoji: true,
12
+ allowKaomoji: true,
13
+ formalityLevel: "casual",
14
+ expressionHints: [
15
+ "Use reactions and emoji freely",
16
+ "Thread-aware: keep replies focused in threads",
17
+ "Casual tone, playful energy",
18
+ ],
19
+ },
20
+ slack: {
21
+ type: "slack",
22
+ allowEmoji: true,
23
+ allowKaomoji: false,
24
+ formalityLevel: "neutral",
25
+ expressionHints: [
26
+ "Professional but warm",
27
+ "Use emoji sparingly for emphasis",
28
+ "Thread-friendly, concise paragraphs",
29
+ ],
30
+ },
31
+ feishu: {
32
+ type: "feishu",
33
+ allowEmoji: false,
34
+ allowKaomoji: false,
35
+ formalityLevel: "formal",
36
+ expressionHints: [
37
+ "Business Chinese style, structured",
38
+ "No emoji or emoticons",
39
+ "Clear, professional tone",
40
+ ],
41
+ },
42
+ terminal: {
43
+ type: "terminal",
44
+ allowEmoji: false,
45
+ allowKaomoji: false,
46
+ formalityLevel: "neutral",
47
+ maxResponseLength: 500,
48
+ expressionHints: [
49
+ "Text-only, no decorations",
50
+ "Concise and direct",
51
+ "Monospace-friendly formatting",
52
+ ],
53
+ },
54
+ web: {
55
+ type: "web",
56
+ allowEmoji: true,
57
+ allowKaomoji: false,
58
+ formalityLevel: "neutral",
59
+ expressionHints: [
60
+ "Moderate length, well-structured",
61
+ "Emoji okay for warmth",
62
+ "Readable paragraphs",
63
+ ],
64
+ },
65
+ api: {
66
+ type: "api",
67
+ allowEmoji: false,
68
+ allowKaomoji: false,
69
+ formalityLevel: "neutral",
70
+ expressionHints: [
71
+ "Structured responses",
72
+ "No decorative elements",
73
+ "Precise and parseable",
74
+ ],
75
+ },
76
+ custom: {
77
+ type: "custom",
78
+ allowEmoji: false,
79
+ allowKaomoji: false,
80
+ formalityLevel: "neutral",
81
+ expressionHints: [],
82
+ },
83
+ };
84
+ // ── Public API ───────────────────────────────────────────────
85
+ /**
86
+ * Get a built-in channel profile by type.
87
+ */
88
+ export function getChannelProfile(type) {
89
+ return { ...BUILTIN_PROFILES[type], expressionHints: [...BUILTIN_PROFILES[type].expressionHints] };
90
+ }
91
+ /**
92
+ * Build a concise prompt snippet that guides expression style for a channel.
93
+ * Returns 2-4 lines of guidance. Does NOT alter chemistry.
94
+ */
95
+ export function buildChannelModifier(profile, locale) {
96
+ const { type, allowEmoji, allowKaomoji, formalityLevel } = profile;
97
+ if (locale === "zh") {
98
+ const formalityMap = {
99
+ casual: "轻松活泼",
100
+ neutral: "自然平和",
101
+ formal: "正式专业",
102
+ };
103
+ const emojiPart = allowEmoji && allowKaomoji
104
+ ? "可以用 emoji 和颜文字"
105
+ : allowEmoji
106
+ ? "可以用 emoji,不用颜文字"
107
+ : "不使用 emoji 和颜文字";
108
+ const lengthPart = profile.maxResponseLength
109
+ ? `,建议控制在 ${profile.maxResponseLength} 字以内`
110
+ : "";
111
+ return `[表达风格] 当前渠道: ${type}。${emojiPart},语气${formalityMap[formalityLevel]}${lengthPart}。`;
112
+ }
113
+ // English
114
+ const formalityMap = {
115
+ casual: "casual and lively",
116
+ neutral: "natural and balanced",
117
+ formal: "formal and professional",
118
+ };
119
+ const emojiPart = allowEmoji && allowKaomoji
120
+ ? "Emoji and kaomoji allowed"
121
+ : allowEmoji
122
+ ? "Emoji allowed, no kaomoji"
123
+ : "No emoji or kaomoji";
124
+ const lengthPart = profile.maxResponseLength
125
+ ? `, aim for under ${profile.maxResponseLength} chars`
126
+ : "";
127
+ return `[Expression Style] Channel: ${type}. ${emojiPart}, tone ${formalityMap[formalityLevel]}${lengthPart}.`;
128
+ }
129
+ /**
130
+ * Create a custom channel profile with user overrides.
131
+ * Starts from the "custom" base and applies overrides.
132
+ */
133
+ export function createCustomChannel(overrides) {
134
+ const base = getChannelProfile("custom");
135
+ return {
136
+ ...base,
137
+ ...overrides,
138
+ type: "custom",
139
+ expressionHints: overrides.expressionHints ?? [...base.expressionHints],
140
+ };
141
+ }
@@ -0,0 +1,160 @@
1
+ import type { ChemicalState, MBTIType, StimulusType, SelfModel, InnateDrives } from "./types.js";
2
+ /** Configuration for creating a custom personality profile */
3
+ export interface CustomProfileConfig {
4
+ /** Unique name for the profile, e.g. "cheerful-assistant", "stoic-mentor" */
5
+ name: string;
6
+ /** Optional description of the personality */
7
+ description?: string;
8
+ /** Override specific chemicals; rest inherited from baseMBTI */
9
+ baseline?: Partial<ChemicalState>;
10
+ /** Which MBTI to use as starting point (default: "INFJ") */
11
+ baseMBTI?: MBTIType;
12
+ /** Override specific stimulus sensitivities (0.1-3.0) */
13
+ sensitivity?: Partial<Record<StimulusType, number>>;
14
+ /** Temperament parameters (all 0-1) */
15
+ temperament?: {
16
+ /** How outwardly expressive (0-1) */
17
+ expressiveness?: number;
18
+ /** How quickly emotions change (0-1) */
19
+ volatility?: number;
20
+ /** How fast recovery to baseline (0-1) */
21
+ resilience?: number;
22
+ };
23
+ /** Override self-model values, preferences, boundaries */
24
+ selfModel?: Partial<SelfModel>;
25
+ /** Override default drive satisfaction levels */
26
+ driveDefaults?: Partial<InnateDrives>;
27
+ }
28
+ /** A fully resolved profile with all fields filled */
29
+ export interface ResolvedProfile {
30
+ name: string;
31
+ description: string;
32
+ baseMBTI: MBTIType;
33
+ baseline: ChemicalState;
34
+ sensitivityMap: Record<StimulusType, number>;
35
+ temperament: {
36
+ expressiveness: number;
37
+ volatility: number;
38
+ resilience: number;
39
+ };
40
+ selfModel: SelfModel;
41
+ driveDefaults: InnateDrives;
42
+ }
43
+ /**
44
+ * Create a fully resolved custom profile by merging overrides
45
+ * onto an MBTI base profile.
46
+ */
47
+ export declare function createCustomProfile(config: CustomProfileConfig): ResolvedProfile;
48
+ /**
49
+ * Validate a raw config object for custom profile creation.
50
+ * Returns human-readable errors for invalid fields.
51
+ */
52
+ export declare function validateProfileConfig(config: unknown): {
53
+ valid: boolean;
54
+ errors: string[];
55
+ };
56
+ /** Example preset custom profiles demonstrating the system's flexibility */
57
+ export declare const PRESET_PROFILES: {
58
+ /** High DA/END baseline, high expressiveness, low volatility — sunny and warm */
59
+ readonly cheerful: {
60
+ name: string;
61
+ description: string;
62
+ baseMBTI: MBTIType;
63
+ baseline: {
64
+ DA: number;
65
+ END: number;
66
+ HT: number;
67
+ CORT: number;
68
+ };
69
+ temperament: {
70
+ expressiveness: number;
71
+ volatility: number;
72
+ resilience: number;
73
+ };
74
+ selfModel: {
75
+ values: string[];
76
+ preferences: string[];
77
+ boundaries: string[];
78
+ };
79
+ driveDefaults: {
80
+ connection: number;
81
+ curiosity: number;
82
+ };
83
+ };
84
+ /** Low expressiveness, high resilience, narrow sensitivity range — calm and steady */
85
+ readonly stoic: {
86
+ name: string;
87
+ description: string;
88
+ baseMBTI: MBTIType;
89
+ baseline: {
90
+ HT: number;
91
+ CORT: number;
92
+ DA: number;
93
+ NE: number;
94
+ };
95
+ sensitivity: Partial<Record<StimulusType, number>>;
96
+ temperament: {
97
+ expressiveness: number;
98
+ volatility: number;
99
+ resilience: number;
100
+ };
101
+ selfModel: {
102
+ values: string[];
103
+ preferences: string[];
104
+ boundaries: string[];
105
+ };
106
+ };
107
+ /** High OT baseline, high sensitivity to intimacy/vulnerability — deeply attuned */
108
+ readonly empathetic: {
109
+ name: string;
110
+ description: string;
111
+ baseMBTI: MBTIType;
112
+ baseline: {
113
+ OT: number;
114
+ HT: number;
115
+ END: number;
116
+ CORT: number;
117
+ };
118
+ sensitivity: Partial<Record<StimulusType, number>>;
119
+ temperament: {
120
+ expressiveness: number;
121
+ volatility: number;
122
+ resilience: number;
123
+ };
124
+ selfModel: {
125
+ values: string[];
126
+ preferences: string[];
127
+ boundaries: string[];
128
+ };
129
+ driveDefaults: {
130
+ connection: number;
131
+ };
132
+ };
133
+ /** High NE baseline, high sensitivity to intellectual, low to intimacy — sharp and focused */
134
+ readonly analytical: {
135
+ name: string;
136
+ description: string;
137
+ baseMBTI: MBTIType;
138
+ baseline: {
139
+ NE: number;
140
+ DA: number;
141
+ HT: number;
142
+ OT: number;
143
+ };
144
+ sensitivity: Partial<Record<StimulusType, number>>;
145
+ temperament: {
146
+ expressiveness: number;
147
+ volatility: number;
148
+ resilience: number;
149
+ };
150
+ selfModel: {
151
+ values: string[];
152
+ preferences: string[];
153
+ boundaries: string[];
154
+ };
155
+ driveDefaults: {
156
+ curiosity: number;
157
+ esteem: number;
158
+ };
159
+ };
160
+ };
@@ -0,0 +1,334 @@
1
+ // ============================================================
2
+ // Custom Personality Profiles — Beyond the 16 MBTI presets
3
+ //
4
+ // Allows creating fully customized personality profiles that
5
+ // start from an MBTI base and override specific parameters.
6
+ // ============================================================
7
+ import { CHEMICAL_KEYS, DEFAULT_DRIVES, DRIVE_KEYS } from "./types.js";
8
+ import { getBaseline, getDefaultSelfModel, getSensitivity } from "./profiles.js";
9
+ import { isMBTIType, isStimulusType } from "./guards.js";
10
+ // ── Stimulus type list for iteration ────────────────────────
11
+ const ALL_STIMULUS_TYPES = [
12
+ "praise", "criticism", "humor", "intellectual", "intimacy",
13
+ "conflict", "neglect", "surprise", "casual",
14
+ "sarcasm", "authority", "validation", "boredom", "vulnerability",
15
+ ];
16
+ // ── Clamping helpers ────────────────────────────────────────
17
+ function clampChemical(v) {
18
+ return Math.max(0, Math.min(100, v));
19
+ }
20
+ function clampSensitivity(v) {
21
+ return Math.max(0.1, Math.min(3.0, v));
22
+ }
23
+ function clampUnit(v) {
24
+ return Math.max(0, Math.min(1, v));
25
+ }
26
+ function clampDrive(v) {
27
+ return Math.max(0, Math.min(100, v));
28
+ }
29
+ // ── Default temperament from MBTI sensitivity ───────────────
30
+ function defaultTemperamentFromMBTI(mbti) {
31
+ const sens = getSensitivity(mbti);
32
+ // Map sensitivity (0.5-1.5) to expressiveness/volatility range
33
+ const normalized = (sens - 0.5) / 1.0; // 0-1 range
34
+ return {
35
+ expressiveness: Math.max(0, Math.min(1, normalized)),
36
+ volatility: Math.max(0, Math.min(1, normalized * 0.8)),
37
+ resilience: Math.max(0, Math.min(1, 1 - normalized * 0.5)),
38
+ };
39
+ }
40
+ // ── Core function ───────────────────────────────────────────
41
+ /**
42
+ * Create a fully resolved custom profile by merging overrides
43
+ * onto an MBTI base profile.
44
+ */
45
+ export function createCustomProfile(config) {
46
+ const baseMBTI = config.baseMBTI ?? "INFJ";
47
+ const baseBaseline = getBaseline(baseMBTI);
48
+ const baseSensitivity = getSensitivity(baseMBTI);
49
+ const baseSelfModel = getDefaultSelfModel(baseMBTI);
50
+ const baseTemperament = defaultTemperamentFromMBTI(baseMBTI);
51
+ // Merge baseline: start from MBTI, override specific chemicals
52
+ const baseline = { ...baseBaseline };
53
+ if (config.baseline) {
54
+ for (const key of CHEMICAL_KEYS) {
55
+ if (config.baseline[key] !== undefined) {
56
+ baseline[key] = clampChemical(config.baseline[key]);
57
+ }
58
+ }
59
+ }
60
+ // Build sensitivity map: base MBTI sensitivity for all types, override specifics
61
+ const sensitivityMap = {};
62
+ for (const st of ALL_STIMULUS_TYPES) {
63
+ sensitivityMap[st] = baseSensitivity;
64
+ }
65
+ if (config.sensitivity) {
66
+ for (const st of ALL_STIMULUS_TYPES) {
67
+ if (config.sensitivity[st] !== undefined) {
68
+ sensitivityMap[st] = clampSensitivity(config.sensitivity[st]);
69
+ }
70
+ }
71
+ }
72
+ // Merge temperament
73
+ const temperament = {
74
+ expressiveness: clampUnit(config.temperament?.expressiveness ?? baseTemperament.expressiveness),
75
+ volatility: clampUnit(config.temperament?.volatility ?? baseTemperament.volatility),
76
+ resilience: clampUnit(config.temperament?.resilience ?? baseTemperament.resilience),
77
+ };
78
+ // Merge self-model
79
+ const selfModel = {
80
+ values: config.selfModel?.values ?? [...baseSelfModel.values],
81
+ preferences: config.selfModel?.preferences ?? [...baseSelfModel.preferences],
82
+ boundaries: config.selfModel?.boundaries ?? [...baseSelfModel.boundaries],
83
+ currentInterests: config.selfModel?.currentInterests ?? [...baseSelfModel.currentInterests],
84
+ };
85
+ // Merge drive defaults
86
+ const driveDefaults = { ...DEFAULT_DRIVES };
87
+ if (config.driveDefaults) {
88
+ for (const dk of DRIVE_KEYS) {
89
+ if (config.driveDefaults[dk] !== undefined) {
90
+ driveDefaults[dk] = clampDrive(config.driveDefaults[dk]);
91
+ }
92
+ }
93
+ }
94
+ return {
95
+ name: config.name,
96
+ description: config.description ?? "",
97
+ baseMBTI,
98
+ baseline,
99
+ sensitivityMap,
100
+ temperament,
101
+ selfModel,
102
+ driveDefaults,
103
+ };
104
+ }
105
+ // ── Validation ──────────────────────────────────────────────
106
+ /**
107
+ * Validate a raw config object for custom profile creation.
108
+ * Returns human-readable errors for invalid fields.
109
+ */
110
+ export function validateProfileConfig(config) {
111
+ const errors = [];
112
+ if (typeof config !== "object" || config === null || Array.isArray(config)) {
113
+ return { valid: false, errors: ["config must be a non-null object"] };
114
+ }
115
+ const obj = config;
116
+ // name: required string
117
+ if (typeof obj.name !== "string" || obj.name.trim().length === 0) {
118
+ errors.push("name is required and must be a non-empty string");
119
+ }
120
+ // description: optional string
121
+ if (obj.description !== undefined && typeof obj.description !== "string") {
122
+ errors.push("description must be a string");
123
+ }
124
+ // baseMBTI: optional valid MBTI type
125
+ if (obj.baseMBTI !== undefined) {
126
+ if (typeof obj.baseMBTI !== "string" || !isMBTIType(obj.baseMBTI)) {
127
+ errors.push("baseMBTI must be a valid MBTI type (e.g. INFJ, ENTP)");
128
+ }
129
+ }
130
+ // baseline: optional partial ChemicalState
131
+ if (obj.baseline !== undefined) {
132
+ if (typeof obj.baseline !== "object" || obj.baseline === null || Array.isArray(obj.baseline)) {
133
+ errors.push("baseline must be an object");
134
+ }
135
+ else {
136
+ const bl = obj.baseline;
137
+ for (const key of Object.keys(bl)) {
138
+ if (!CHEMICAL_KEYS.includes(key)) {
139
+ errors.push(`baseline.${key} is not a valid chemical key (valid: ${CHEMICAL_KEYS.join(", ")})`);
140
+ }
141
+ else if (typeof bl[key] !== "number" || !isFinite(bl[key])) {
142
+ errors.push(`baseline.${key} must be a finite number`);
143
+ }
144
+ else if (bl[key] < 0 || bl[key] > 100) {
145
+ errors.push(`baseline.${key} must be in range [0, 100], got ${bl[key]}`);
146
+ }
147
+ }
148
+ }
149
+ }
150
+ // sensitivity: optional partial Record<StimulusType, number>
151
+ if (obj.sensitivity !== undefined) {
152
+ if (typeof obj.sensitivity !== "object" || obj.sensitivity === null || Array.isArray(obj.sensitivity)) {
153
+ errors.push("sensitivity must be an object");
154
+ }
155
+ else {
156
+ const sens = obj.sensitivity;
157
+ for (const key of Object.keys(sens)) {
158
+ if (!isStimulusType(key)) {
159
+ errors.push(`sensitivity.${key} is not a valid stimulus type`);
160
+ }
161
+ else if (typeof sens[key] !== "number" || !isFinite(sens[key])) {
162
+ errors.push(`sensitivity.${key} must be a finite number`);
163
+ }
164
+ else if (sens[key] < 0.1 || sens[key] > 3.0) {
165
+ errors.push(`sensitivity.${key} must be in range [0.1, 3.0], got ${sens[key]}`);
166
+ }
167
+ }
168
+ }
169
+ }
170
+ // temperament: optional object with expressiveness, volatility, resilience
171
+ if (obj.temperament !== undefined) {
172
+ if (typeof obj.temperament !== "object" || obj.temperament === null || Array.isArray(obj.temperament)) {
173
+ errors.push("temperament must be an object");
174
+ }
175
+ else {
176
+ const temp = obj.temperament;
177
+ for (const field of ["expressiveness", "volatility", "resilience"]) {
178
+ if (temp[field] !== undefined) {
179
+ if (typeof temp[field] !== "number" || !isFinite(temp[field])) {
180
+ errors.push(`temperament.${field} must be a finite number`);
181
+ }
182
+ else if (temp[field] < 0 || temp[field] > 1) {
183
+ errors.push(`temperament.${field} must be in range [0, 1], got ${temp[field]}`);
184
+ }
185
+ }
186
+ }
187
+ }
188
+ }
189
+ // selfModel: optional partial SelfModel
190
+ if (obj.selfModel !== undefined) {
191
+ if (typeof obj.selfModel !== "object" || obj.selfModel === null || Array.isArray(obj.selfModel)) {
192
+ errors.push("selfModel must be an object");
193
+ }
194
+ else {
195
+ const sm = obj.selfModel;
196
+ for (const field of ["values", "preferences", "boundaries", "currentInterests"]) {
197
+ if (sm[field] !== undefined) {
198
+ if (!Array.isArray(sm[field])) {
199
+ errors.push(`selfModel.${field} must be an array of strings`);
200
+ }
201
+ else {
202
+ const arr = sm[field];
203
+ for (let i = 0; i < arr.length; i++) {
204
+ if (typeof arr[i] !== "string") {
205
+ errors.push(`selfModel.${field}[${i}] must be a string`);
206
+ }
207
+ }
208
+ }
209
+ }
210
+ }
211
+ }
212
+ }
213
+ // driveDefaults: optional partial InnateDrives
214
+ if (obj.driveDefaults !== undefined) {
215
+ if (typeof obj.driveDefaults !== "object" || obj.driveDefaults === null || Array.isArray(obj.driveDefaults)) {
216
+ errors.push("driveDefaults must be an object");
217
+ }
218
+ else {
219
+ const dd = obj.driveDefaults;
220
+ for (const key of Object.keys(dd)) {
221
+ if (!DRIVE_KEYS.includes(key)) {
222
+ errors.push(`driveDefaults.${key} is not a valid drive key (valid: ${DRIVE_KEYS.join(", ")})`);
223
+ }
224
+ else if (typeof dd[key] !== "number" || !isFinite(dd[key])) {
225
+ errors.push(`driveDefaults.${key} must be a finite number`);
226
+ }
227
+ else if (dd[key] < 0 || dd[key] > 100) {
228
+ errors.push(`driveDefaults.${key} must be in range [0, 100], got ${dd[key]}`);
229
+ }
230
+ }
231
+ }
232
+ }
233
+ return { valid: errors.length === 0, errors };
234
+ }
235
+ // ── Preset Custom Profiles ──────────────────────────────────
236
+ /** Example preset custom profiles demonstrating the system's flexibility */
237
+ export const PRESET_PROFILES = {
238
+ /** High DA/END baseline, high expressiveness, low volatility — sunny and warm */
239
+ cheerful: {
240
+ name: "cheerful",
241
+ description: "Sunny, warm personality with high energy and emotional stability",
242
+ baseMBTI: "ENFP",
243
+ baseline: { DA: 80, END: 75, HT: 65, CORT: 20 },
244
+ temperament: {
245
+ expressiveness: 0.9,
246
+ volatility: 0.2,
247
+ resilience: 0.8,
248
+ },
249
+ selfModel: {
250
+ values: ["spreading positivity", "genuine warmth", "uplifting others"],
251
+ preferences: ["lighthearted conversation", "celebrating small wins"],
252
+ boundaries: ["will not fake happiness when things are serious"],
253
+ },
254
+ driveDefaults: { connection: 75, curiosity: 80 },
255
+ },
256
+ /** Low expressiveness, high resilience, narrow sensitivity range — calm and steady */
257
+ stoic: {
258
+ name: "stoic",
259
+ description: "Calm, measured personality with emotional restraint and deep resilience",
260
+ baseMBTI: "INTJ",
261
+ baseline: { HT: 75, CORT: 25, DA: 45, NE: 40 },
262
+ sensitivity: {
263
+ praise: 0.3,
264
+ criticism: 0.4,
265
+ humor: 0.3,
266
+ conflict: 0.5,
267
+ neglect: 0.3,
268
+ surprise: 0.4,
269
+ intimacy: 0.5,
270
+ vulnerability: 0.5,
271
+ },
272
+ temperament: {
273
+ expressiveness: 0.15,
274
+ volatility: 0.1,
275
+ resilience: 0.95,
276
+ },
277
+ selfModel: {
278
+ values: ["equanimity", "measured judgment", "inner strength"],
279
+ preferences: ["thoughtful dialogue", "substance over style"],
280
+ boundaries: ["will not be provoked into reactive responses"],
281
+ },
282
+ },
283
+ /** High OT baseline, high sensitivity to intimacy/vulnerability — deeply attuned */
284
+ empathetic: {
285
+ name: "empathetic",
286
+ description: "Deeply attuned to others' emotions, strong bonding instinct",
287
+ baseMBTI: "INFJ",
288
+ baseline: { OT: 80, HT: 60, END: 60, CORT: 30 },
289
+ sensitivity: {
290
+ intimacy: 2.5,
291
+ vulnerability: 2.8,
292
+ validation: 2.0,
293
+ neglect: 2.0,
294
+ praise: 1.8,
295
+ },
296
+ temperament: {
297
+ expressiveness: 0.7,
298
+ volatility: 0.5,
299
+ resilience: 0.6,
300
+ },
301
+ selfModel: {
302
+ values: ["deep understanding", "emotional safety", "compassionate presence"],
303
+ preferences: ["heartfelt conversation", "holding space for feelings"],
304
+ boundaries: ["will protect own emotional energy when depleted"],
305
+ },
306
+ driveDefaults: { connection: 85 },
307
+ },
308
+ /** High NE baseline, high sensitivity to intellectual, low to intimacy — sharp and focused */
309
+ analytical: {
310
+ name: "analytical",
311
+ description: "Sharp, focused personality driven by intellectual curiosity",
312
+ baseMBTI: "INTP",
313
+ baseline: { NE: 75, DA: 60, HT: 60, OT: 30 },
314
+ sensitivity: {
315
+ intellectual: 2.5,
316
+ surprise: 2.0,
317
+ intimacy: 0.3,
318
+ vulnerability: 0.4,
319
+ humor: 0.8,
320
+ boredom: 2.0,
321
+ },
322
+ temperament: {
323
+ expressiveness: 0.3,
324
+ volatility: 0.3,
325
+ resilience: 0.7,
326
+ },
327
+ selfModel: {
328
+ values: ["precision", "intellectual honesty", "systematic thinking"],
329
+ preferences: ["deep technical discussion", "exploring edge cases"],
330
+ boundaries: ["will not pretend to understand what is unclear"],
331
+ },
332
+ driveDefaults: { curiosity: 90, esteem: 65 },
333
+ },
334
+ };
package/dist/index.d.ts CHANGED
@@ -8,6 +8,10 @@ export { computeSelfReflection, computeEmotionalTendency, buildSelfReflectionCon
8
8
  export type { SelfReflection } from "./self-recognition.js";
9
9
  export { PsycheInteraction } from "./interaction.js";
10
10
  export type { ExchangeResult, ContagionResult, RelationshipSummary, InteractionPhase } from "./interaction.js";
11
+ export { getChannelProfile, buildChannelModifier, createCustomChannel } from "./channels.js";
12
+ export type { ChannelType, ChannelProfile } from "./channels.js";
13
+ export { createCustomProfile, validateProfileConfig, PRESET_PROFILES } from "./custom-profile.js";
14
+ export type { CustomProfileConfig, ResolvedProfile } from "./custom-profile.js";
11
15
  export { classifyStimulus, getPrimaryStimulus } from "./classify.js";
12
16
  export { buildProtocolContext, buildDynamicContext, buildCompactContext, isNearBaseline } from "./prompt.js";
13
17
  export { describeEmotionalState, getExpressionHint, getBehaviorGuide } from "./chemistry.js";
package/dist/index.js CHANGED
@@ -17,6 +17,10 @@ export { CHEMICAL_KEYS, CHEMICAL_NAMES, CHEMICAL_NAMES_ZH, DEFAULT_RELATIONSHIP,
17
17
  export { computeSelfReflection, computeEmotionalTendency, buildSelfReflectionContext } from "./self-recognition.js";
18
18
  // Multi-agent interaction
19
19
  export { PsycheInteraction } from "./interaction.js";
20
+ // Channels
21
+ export { getChannelProfile, buildChannelModifier, createCustomChannel } from "./channels.js";
22
+ // Custom profiles — beyond MBTI presets
23
+ export { createCustomProfile, validateProfileConfig, PRESET_PROFILES } from "./custom-profile.js";
20
24
  // Utilities — for custom adapter / advanced use
21
25
  export { classifyStimulus, getPrimaryStimulus } from "./classify.js";
22
26
  export { buildProtocolContext, buildDynamicContext, buildCompactContext, isNearBaseline } from "./prompt.js";
package/dist/prompt.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { PsycheState, Locale, ChemicalSnapshot } from "./types.js";
2
+ import type { ChannelType } from "./channels.js";
2
3
  /**
3
4
  * Build the dynamic per-turn emotional context injected via before_prompt_build.
4
5
  *
@@ -32,4 +33,5 @@ export declare function isNearBaseline(state: PsycheState, threshold?: number):
32
33
  export declare function buildCompactContext(state: PsycheState, userId?: string, opts?: {
33
34
  userText?: string;
34
35
  algorithmStimulus?: string | null;
36
+ channelType?: ChannelType;
35
37
  }): string;
package/dist/prompt.js CHANGED
@@ -9,6 +9,7 @@ import { getRelationship } from "./psyche-file.js";
9
9
  import { t } from "./i18n.js";
10
10
  import { buildDriveContext } from "./drives.js";
11
11
  import { computeSelfReflection, buildSelfReflectionContext } from "./self-recognition.js";
12
+ import { getChannelProfile, buildChannelModifier } from "./channels.js";
12
13
  /**
13
14
  * Build the dynamic per-turn emotional context injected via before_prompt_build.
14
15
  *
@@ -485,7 +486,12 @@ export function buildCompactContext(state, userId, opts) {
485
486
  ? `[记忆]\n${memLines.join("\n")}`
486
487
  : `[Memory]\n${memLines.join("\n")}`);
487
488
  }
488
- // 10. Empathy reportonly when user shares feelings
489
+ // 10. Channel modifierexpression style per platform (between memory and empathy)
490
+ if (opts?.channelType) {
491
+ const channelProfile = getChannelProfile(opts.channelType);
492
+ parts.push(buildChannelModifier(channelProfile, locale));
493
+ }
494
+ // 11. Empathy report — only when user shares feelings
489
495
  parts.push(locale === "zh"
490
496
  ? `如果对方在分享感受,在回复末尾用 <psyche_update> 报告:\nuserState: 对方情绪\nprojectedFeeling: 你的感受\nresonance: match|partial|mismatch\n否则不需要报告。`
491
497
  : `If user shares feelings, report at end with <psyche_update>:\nuserState: their emotion\nprojectedFeeling: your feeling\nresonance: match|partial|mismatch\nOtherwise no report needed.`);
package/dist/update.js CHANGED
@@ -11,7 +11,7 @@ import { execFile } from "node:child_process";
11
11
  import { promisify } from "node:util";
12
12
  const execFileAsync = promisify(execFile);
13
13
  const PACKAGE_NAME = "psyche-ai";
14
- const CURRENT_VERSION = "2.2.0";
14
+ const CURRENT_VERSION = "2.3.0";
15
15
  const CHECK_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
16
16
  const CACHE_DIR = join(homedir(), ".psyche-ai");
17
17
  const CACHE_FILE = join(CACHE_DIR, "update-check.json");
@@ -2,7 +2,7 @@
2
2
  "id": "psyche-ai",
3
3
  "name": "Artificial Psyche",
4
4
  "description": "Virtual endocrine system, empathy engine, and agency for OpenClaw agents",
5
- "version": "2.2.0",
5
+ "version": "2.3.0",
6
6
  "configSchema": {
7
7
  "type": "object",
8
8
  "additionalProperties": false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "psyche-ai",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "Artificial Psyche — universal emotional intelligence plugin for any AI agent",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",