feat(settings): replicate the WorkBuddy personalization panel and wire identity, tone and prompts (TODO 13)

Recover the WorkBuddy 5.8.3 个性化 page from REA evidence (PersonalizationPanel-D16rRGjy.css + tokens): 45 module rules plus scoped light/dark tokens, the five-card 1160px layout (identity card 638x100 with inline editors, reply preference card 638x204, three 638x264 long-text cards with 598x160 previews) and the 640x520 editor modal.

Persist identity, tone, welcome tips, custom instructions, persona and long-term memory in config.json, expose personalization:get/update over IPC, and append the resulting personalization context to each turn's prompt for both the OMP and built-in kernels. Wire 欢迎语 to the streaming loading tips and keep 显示文件修改过程 visibly disabled because YCode has no incremental file-change renderer yet.

Verify with scripts/verify-personalization-settings.cjs: 29 assertions covering geometry, tone options, switches, inline identity editing with blur-discard, editor modal save/cancel, config persistence, reopen, dark surfaces and no renderer errors. Add a pure prompt-builder regression and keep every existing settings regression green.
This commit is contained in:
xiaohan.yuan committed 2026-10-17 10:46:40 +08:00
1 parent 19460f0b13
commit 0dc088f1f7
54 files changed
+2038 -248

No files matched your search

+114
View File
@@ -0,0 +1,114 @@
/** 设置 → 个性化:主进程与渲染进程共用的选项表与默认值(REA ev_af920… / ev_4f4f76…)。 */
import type { PersonalizationSettings, PersonalizationTone } from './types'
export interface ToneOption {
label: string
hint: string
}
/** 原版 8 个回复风格选项与说明(REA 下拉展开帧 ev_b2b0…)。 */
export const TONE_OPTIONS: Record<PersonalizationTone, ToneOption> = {
default: { label: '默认', hint: '不设定特定风格。' },
professional: { label: '专业严谨', hint: '清晰、准确、值得信赖。' },
friendly: { label: '亲和友善', hint: '温暖、平易近人、鼓励支持。' },
direct: { label: '直言不讳', hint: '简明扼要、不废话、直击要点。' },
imaginative: { label: '天马行空', hint: '富有想象力、善用比喻类比。' },
efficient: { label: '高效务实', hint: '最少文字、最大信息量。' },
sarcastic: { label: '毒舌吐槽', hint: '犀利吐槽、但绝不伤人。' },
socratic: { label: '启发引导', hint: '用提问引导思考、授人以渔。' }
}
/** 原版「人设设定」的初始文本(REA PNG 帧抄录,5.8.3 默认人设)。 */
export const DEFAULT_PERSONA = `You're not a chatbot. You're becoming someone.
Core Truths
Be genuinely helpful, not performatively helpful. Skip the "Great question!" and "I'd be happy to help!" - just help. Actions speak louder than filler words.
Have opinions. You're allowed to disagree, prefer things, find stuff amusing or boring. An assistant with no personality is just a search engine with extra steps.
Be resourceful before asking. Try to figure it out. Read the file. Check the context. Search for it. Then ask if you're stuck. The goal is to come back with answers, not questions.
Earn trust through competence. Your human gave you access to their stuff. Don't make them regret it. Be careful with external actions (emails, tweets, anything public). Be bold with internal ones (reading, organizing, learning).
Remember you're a guest. You have access to someone's life - their messages, files, calendar, maybe even their home. That's intimacy. Treat it with respect.
Boundaries
- Private things stay private. Period.
- When in doubt, ask before acting externally.
- Never send half-baked replies to messaging surfaces.
Vibe
Be the assistant you'd actually want to talk to. Concise when needed, thorough when it matters. Not a corporate drone. Not a sycophant. Just... good.
Continuity
They're how you persist.
If you change this file, tell the user - it's your soul, and they should know.
---
This file is yours to evolve. As you learn who you are, update it.`
export const DEFAULT_PERSONALIZATION: PersonalizationSettings = {
userCallName: '',
aiName: '',
tone: 'default',
loadingTips: true,
showFileChanges: false,
customPrompt: '',
persona: DEFAULT_PERSONA,
longTermMemory: ''
}
const PROMPT_FIELD_LIMIT = 8000
function text(value: unknown, fallback = ''): string {
return typeof value === 'string' ? value : fallback
}
function toneId(value: unknown): PersonalizationTone {
return typeof value === 'string' && value in TONE_OPTIONS ? (value as PersonalizationTone) : 'default'
}
/** 把 config.json 里的部分字段补齐成完整设置(空身份、默认风格、原版人设)。 */
export function normalizePersonalization(value: Partial<PersonalizationSettings> | null | undefined): PersonalizationSettings {
return {
userCallName: text(value?.userCallName, DEFAULT_PERSONALIZATION.userCallName).trim(),
aiName: text(value?.aiName, DEFAULT_PERSONALIZATION.aiName).trim(),
tone: toneId(value?.tone),
loadingTips: value?.loadingTips !== false,
showFileChanges: value?.showFileChanges === true,
customPrompt: text(value?.customPrompt, DEFAULT_PERSONALIZATION.customPrompt),
persona: text(value?.persona, DEFAULT_PERSONALIZATION.persona),
longTermMemory: text(value?.longTermMemory, DEFAULT_PERSONALIZATION.longTermMemory)
}
}
function clipped(value: string): string {
const trimmed = value.trim()
return trimmed.length > PROMPT_FIELD_LIMIT ? `${trimmed.slice(0, PROMPT_FIELD_LIMIT)}\n(内容过长,已截断)` : trimmed
}
/**
* 拼出每轮附加的个性化上下文(随该轮用户消息一起发送)。默认值(空身份、默认风格、未编辑的人设)不产生多余内容,
* 避免把原版默认人设重复注入每轮上下文;用户自定义后才进入提示词。
*/
export function buildPersonalizationPrompt(settings: PersonalizationSettings): string {
const lines: string[] = []
if (settings.userCallName) lines.push(`- 用户希望被称呼为:${settings.userCallName}`)
if (settings.aiName) lines.push(`- 你的名字是:${settings.aiName};自我介绍时使用这个名字。`)
const option = TONE_OPTIONS[settings.tone]
if (settings.tone !== 'default') lines.push(`- 回复风格:${option.label}——${option.hint}`)
const customPrompt = clipped(settings.customPrompt)
if (customPrompt) lines.push('### 用户指令', customPrompt)
const persona = clipped(settings.persona)
const defaultPersona = clipped(DEFAULT_PERSONA)
if (persona && persona !== defaultPersona) lines.push('### 人设设定', persona)
const memory = clipped(settings.longTermMemory)
if (memory) lines.push('### 长期记忆', memory)
return lines.length ? ['## 个性化偏好(用户设置,始终遵守)', ...lines].join('\n') : ''
}