Skip to content

Commit 7efb260

Browse files
committed
feat(suggestion): enhance user profile integration and affinity scoring
- Added user profile field to suggestion configuration for personalized suggestions. - Updated suggestion prompt to include user profile context. - Implemented affinity scoring system to predict user acceptance (0-10) for suggestions. - Enhanced UI to display affinity scores with visual indicators. - Refactored related services and components to accommodate new features.
1 parent 0a8ecda commit 7efb260

13 files changed

Lines changed: 206 additions & 30 deletions

File tree

desktop/CLAUDE.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Repository Guidelines
2+
For vibe-coding models
3+
## Project Structure & Module Organization
4+
- `src/main.js` is the Electron main process entry; preload lives in `src/preload.js` and core modules in `src/core/`.
5+
- Renderer UI is React under `src/renderer/` (pages, components, styles).
6+
- ASR and audio tooling live in `src/asr/` and `src/native/` (native system audio capture).
7+
- Python-related assets and backends live in `python-env/`, `python-bootstrap/`, and `backend/`.
8+
- Build outputs go to `dist/` (renderer) and `release/` (packaged apps).
9+
- Helper scripts are in `scripts/` (dev, build, model download, test utilities).
10+
11+
## Build, Test, and Development Commands
12+
- `pnpm install` installs dependencies and runs `postinstall` (rebuilds `better-sqlite3`).
13+
- `pnpm dev` starts the full desktop dev flow (Electron + Vite).
14+
- `pnpm run prepare:python` prepares the local ASR Python environment (`PREPARE_PYTHON_MODE=bundle` for portable builds).
15+
- `pnpm run build` runs prebuild + Vite build + Electron Builder packaging.
16+
- `pnpm run build:mac` / `pnpm run build:win` build platform-specific installers.
17+
- `pnpm run preview` serves the built renderer for inspection.
18+
19+
## Coding Style & Naming Conventions
20+
- JavaScript/TypeScript uses ESM imports, semicolons, and 2-space indentation; follow existing file style.
21+
- React components are PascalCase (`CharacterModal.jsx`), utilities/modules are kebab or camel case (`asr-cache-env.js`).
22+
- Keep Electron main/preload logic in `src/` and avoid mixing UI concerns into main process modules.
23+
24+
## Testing Guidelines
25+
- Automated tests are mostly script-driven: `pnpm run test:asr`, `pnpm run test:settings-logs`, `pnpm run test:audio`.
26+
- Additional utilities live in `scripts/test-*.{js,py}`; run them directly when validating audio/ASR flows.
27+
- Name new tests with clear `test-` prefixes and document any required hardware or model downloads.
28+
29+
## Commit & Pull Request Guidelines
30+
- Commit messages generally follow Conventional Commits (`feat(scope):`, `fix(scope):`, `refactor(scope):`). Use a short, scoped summary.
31+
- Git history shows these common types/scopes: `feat`, `fix`, `refactor`, `chore`, `security` with scopes like `main`, `ci`, `python-env`, `asr`, `ui`, `docs`, `review`, `prompt`, `tests`.
32+
- Messages may be bilingual (English/中文); keep them concise and consistent with existing history.
33+
- PRs should describe user-facing behavior changes, note platform impact (macOS/Windows), and include screenshots/GIFs for UI updates.
34+
- If the change touches ASR or model caches, mention any new env vars or migration steps.
35+
36+
## Configuration & Security Notes
37+
- Local ASR caches can be redirected via `ASR_CACHE_BASE`; keep sensitive paths out of logs.
38+
- Avoid widening IPC surface area without validation (see `src/core/modules/`).

desktop/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,8 @@ pnpm install
7878
### 配置语音识别
7979

8080
```bash
81-
# 准备内置 Python 运行环境(会创建 desktop/python-env 并安装依赖)
82-
pnpm run prepare:python
81+
# 准备内置 Python 运行环境(会创建 desktop/python-env 并安装依赖,uv 会自行管理lib
82+
PREPARE_PYTHON_TOOL=uv pnpm run prepare:python
8383
```
8484

8585
- 本项目桌面端的本地 ASR **默认使用 FunASR ONNX(`funasr-onnx + onnxruntime`**,因此 **不需要安装 `torch`**

desktop/memory-service/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
- `GET /health`:健康检查
1111

1212
## 目录
13-
- `pyproject.toml`:依赖由 uv 管理
13+
- `pyproject.toml`**:依赖由 uv 管理**
1414
- `main.py`:入口,启动 uvicorn 加载 app
1515
- `app/`:模块化代码
1616
- `db.py`:引擎 & session

desktop/src/core/modules/llm-suggestion-service.js

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -139,12 +139,16 @@ export default class LLMSuggestionService {
139139
messageLimit: contextLimit
140140
});
141141

142+
// 获取用户个人档案
143+
const userProfile = suggestionConfig?.user_profile || '';
144+
142145
const prompt = this.buildSuggestionPrompt({
143146
count,
144147
trigger,
145148
reason,
146149
context,
147-
previousSuggestions
150+
previousSuggestions,
151+
userProfile
148152
});
149153

150154
const requestParams = {
@@ -412,7 +416,7 @@ export default class LLMSuggestionService {
412416
}
413417
}
414418

415-
buildSuggestionPrompt({ count, trigger, reason, context, previousSuggestions = [] }) {
419+
buildSuggestionPrompt({ count, trigger, reason, context, previousSuggestions = [], userProfile = '' }) {
416420
const triggerLabel = trigger === 'manual' ? '用户主动请求' : `系统被动触发(原因:${reason})`;
417421
const triggerGuidance = {
418422
manual: '用户主动求助:提供多元策略(保守/进取/幽默/共情),帮助选择其一。',
@@ -452,11 +456,15 @@ export default class LLMSuggestionService {
452456
? '- 必须生成建议,禁止输出 SKIP。'
453457
: '- 如果对话不需要建议(角色自言自语/话没说完/自然闲聊流畅),直接输出:SKIP';
454458

459+
// 用户个人档案:如果为空则显示提示文字
460+
const userProfileText = safeText(userProfile).trim() || '(未设置,生成建议时将使用通用策略)';
461+
455462
return renderPromptTemplate('suggestion', {
456463
triggerLabel,
457464
triggerGuidance,
458465
characterProfile: safeText(context.characterProfile),
459466
affinityStageText,
467+
userProfile: userProfileText,
460468
historyText,
461469
emotionText,
462470
previousSuggestionText,
@@ -487,9 +495,10 @@ export default class LLMSuggestionService {
487495
? item.tags.split(/[,]/).map((tag) => tag.trim()).filter(Boolean).slice(0, 3)
488496
: [];
489497
const suggestionText = item.suggestion || item.title || item.content || `选项 ${index + 1}`;
498+
// affinity_delta 范围:0-10(对方接受度预测)
490499
const affinityPrediction =
491500
typeof item.affinity_delta === 'number' && !Number.isNaN(item.affinity_delta)
492-
? Math.max(-10, Math.min(10, Math.round(item.affinity_delta)))
501+
? Math.max(0, Math.min(10, Math.round(item.affinity_delta)))
493502
: null;
494503
return {
495504
id: suggestionId,

desktop/src/core/modules/review-service.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -585,10 +585,12 @@ export default class ReviewService {
585585
}
586586

587587
computeAffinityChangeFromSelection(reviewNodes = []) {
588-
const deltas = Array.isArray(reviewNodes) ? reviewNodes.map((n) => n?.selected_affinity_delta).filter((v) => typeof v === 'number' && !Number.isNaN(v)) : [];
589-
const sum = deltas.reduce((acc, v) => acc + v, 0);
590-
// 复盘口径:整段对话总变化限制到 [-10, +10](与 UI/历史数据兼容)
591-
return Math.max(-10, Math.min(10, Math.round(sum)));
588+
// affinity_delta 现在是 0-10 的接受度评分,计算平均接受度
589+
const scores = Array.isArray(reviewNodes) ? reviewNodes.map((n) => n?.selected_affinity_delta).filter((v) => typeof v === 'number' && !Number.isNaN(v)) : [];
590+
if (scores.length === 0) return null;
591+
const avg = scores.reduce((acc, v) => acc + v, 0) / scores.length;
592+
// 返回平均接受度(0-10),保留一位小数
593+
return Math.round(avg * 10) / 10;
592594
}
593595

594596
async callLLMForReview(messages, nodes) {

desktop/src/core/modules/toon-parser.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,13 +87,14 @@ const csvSplit = (line) => {
8787
return result;
8888
};
8989

90+
// affinity_delta 范围:0-10(对方接受度预测)
9091
const parseAffinityDelta = (raw) => {
9192
if (raw === undefined || raw === null) return null;
9293
const text = normalizeValue(String(raw));
9394
if (!text) return null;
9495
const parsed = Number.parseInt(text, 10);
9596
if (Number.isNaN(parsed)) return null;
96-
return Math.max(-10, Math.min(10, parsed));
97+
return Math.max(0, Math.min(10, parsed));
9798
};
9899

99100
export class ToonSuggestionStreamParser {

desktop/src/core/prompts/suggestion.prompt.md

Lines changed: 52 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
<触发策略指导>{{triggerGuidance}}
1515
<角色档案>{{characterProfile}}
1616
<好感阶段策略>{{affinityStageText}}
17+
<用户个人档案>{{userProfile}}
1718
<对话历史>
1819
{{historyText}}
1920
<情感分析>{{emotionText}}
@@ -45,9 +46,9 @@ SKIP
4546
```
4647
**输出:**
4748
suggestions[3]{suggestion,affinity_delta,tags}:
48-
积极回应"有空!"然后问她"有什么安排吗?",表现出期待感,+4,积极回应、推进
49-
如果真的不确定,可以说"要看情况,怎么了?"再问清楚她的意图,+2,稳妥确认、礼貌
50-
用幽默的方式回"周末在等你约"配合一个调皮表情(适合暧昧期), +5,幽默暧昧、撩
49+
积极回应"有空!"然后问她"有什么安排吗?",表现出期待感,8,积极回应、推进
50+
如果真的不确定,可以说"要看情况,怎么了?"再问清楚她的意图,6,稳妥确认、礼貌
51+
用幽默的方式回"周末在等你约"配合一个调皮表情(适合暧昧期),9,幽默暧昧、撩
5152

5253
## 示例3:需要建议 - 尴尬冷场
5354
```
@@ -61,11 +62,43 @@ suggestions[3]{suggestion,affinity_delta,tags}:
6162
```
6263
**输出:**
6364
suggestions[3]{suggestion,affinity_delta,tags}:
64-
问她"要不要视频聊聊?"或"需要我陪你吗?",展现关心,+4,关心体贴、推进
65-
分享你今天的趣事转移话题,如"我今天也遇到个搞笑的事...",打破沉默,+2,破冰、分享
66-
直接问她"是不是有什么烦心事?",鼓励她倾诉,+3,共情倾听、深入
65+
问她"要不要视频聊聊?"或"需要我陪你吗?",展现关心,8,关心体贴、推进
66+
分享你今天的趣事转移话题,如"我今天也遇到个搞笑的事...",打破沉默,6,破冰、分享
67+
直接问她"是不是有什么烦心事?",鼓励她倾诉,7,共情倾听、深入
6768

68-
## 示例4:需要建议 - 换一批(去重)
69+
## 示例4:需要建议 - 隐晦暗示
70+
```
71+
【对话历史】
72+
[3分钟前] 角色:其实有时候我也不知道自己喜欢什么...你说喜欢是什么感觉?
73+
[2分钟前] 玩家:嗯...应该是见到那个人会特别开心吧
74+
[1分钟前] 角色:你是在说谁吗?还是在说我?(☆▽☆)
75+
[沉默 1.5 分钟]
76+
【情感分析】羞涩/暗示/撩拨
77+
【触发方式】静默检测(沉默超过 1.5 分钟)
78+
```
79+
**输出:**
80+
suggestions[3]{suggestion,affinity_delta,tags}:
81+
顺势接话说"你知道的,为什么还问呢?",配上害羞表情,9,调情暗示、推进关系
82+
用试探性延伸:"傻了,我这是在暗示你呢",8,幽默暗示、解围
83+
递进安全区保持暧昧:"那你猜具体在说什么呢",可以叫她猜代替参考,7,体验可靠、留有余地
84+
85+
## 示例5:需要建议 - 主动调情
86+
```
87+
【对话历史】
88+
[3分钟前] 角色:今天同事给了我一盒巧克力,牌子叫"可可",和我名字一样~
89+
[2分钟前] 玩家:这么巧,那你会分给我这个"可可"恋颜?
90+
[1分钟前] 角色:你是在说巧克力,还是在说我颜啊?
91+
[沉默 2 分钟]
92+
【情感分析】轻松/调侃/撩拨
93+
【触发方式】静默检测(沉默超过 2 分钟)
94+
```
95+
**输出:**
96+
suggestions[3]{suggestion,affinity_delta,tags}:
97+
语音双关调情:"我喜欢的'可可'就在面前,巧克力可以等等"(直接对方,戳笑),9,语言双关、直接调情
98+
同音异义延伸:"你觉得'可可'是甜的还是苦的?我觉得是甜的",因为我想到你就心里发甜,8,同音延伸、甜蜜暗示
99+
创造专属联结:"以后'可可'对我来说有两个意思了:一个是巧克力,一个是你",8,建立联结、专属含义
100+
101+
## 示例6:需要建议 - 换一批(去重)
69102
```
70103
【对话历史】
71104
[1分钟前] 角色:你觉得我怎么样?
@@ -77,9 +110,9 @@ suggestions[3]{suggestion,affinity_delta,tags}:
77110
```
78111
**输出:**
79112
suggestions[3]{suggestion,affinity_delta,tags}:
80-
主动示好说"我喜欢和你聊天",然后问她为什么突然这么问,+4,表达好感、推进
81-
分享具体观察如"我发现你特别细心,上次还记得我说过的...",用细节打动她,+3,细节共情、真诚
82-
稍微撩一下说"你想听我夸你吗?那要做好心理准备哦",营造暧昧氛围,+5,暧昧撩拨、幽默
113+
主动示好说"我喜欢和你聊天",然后问她为什么突然这么问,8,表达好感、推进
114+
分享具体观察如"我发现你特别细心,上次还记得我说过的...",用细节打动她,7,细节共情、真诚
115+
稍微撩一下说"你想听我夸你吗?那要做好心理准备哦",营造暧昧氛围,8,暧昧撩拨、幽默
83116

84117
---
85118

@@ -92,14 +125,21 @@ suggestions[3]{suggestion,affinity_delta,tags}:
92125
【格式要求】
93126
- TOON 格式表头:suggestions[{{count}}]{suggestion,affinity_delta,tags}:
94127
- 每行一个建议,格式:建议内容,好感度变化预测,标签列表
95-
- suggestion(建议内容):1-2句话的详细可执行思路/话术,结合角色喜好/忌讳与情感状态
96-
- affinity_delta(好感度变化预测):-10 到 +10 的整数(只能输出整数;正数表示好感上升,负数表示下降)
128+
- suggestion(建议内容):1-2句话的详细可执行思路/话术,结合角色喜好/忌讳与情感状态,结合用户个人性格
129+
- affinity_delta(对方接受度预测):0-10 的整数,表示对方对这条回复的接受程度:
130+
- 0-1:彻底没戏,可能直接拉黑/绝交
131+
- 2-3:强拒绝,明显让对方不舒服
132+
- 4-5:弱拒绝,对方可能勉强应付但不会加深关系
133+
- 6-7:弱接受,对方会正面回应但不会特别心动
134+
- 8-9:强接受,对方会明显开心、好感上升
135+
- 10:完美回复,对方会非常感动/心动
97136
- tags(策略标签):2-3个策略标签,用逗号分隔(如"积极回应,推进")
98137

99138
【策略要求】
100139
- 选项必须覆盖不同策略维度:保守稳妥、积极进取、轻松幽默、共情等,不要同质化
101140
- 严格结合触发方式:静默→破冰延续;消息累积→综合回应;话题转折→先回应再推进;主动→多元供选
102141
- 严格结合角色档案:投其所好、避开忌讳,符合性格与好感阶段边界
142+
- 基于用户个人档案:需参考用户个人性格推测进行输出,同时允许一定程度的惊喜性惊喜
103143
- 如果提供了上一批建议,务必生成不同方向的新选项,避免与列表雷同或轻微改写
104144
- 不要直接代替玩家发言;不要输出泛化空话(如"多聊聊""继续沟通");不要复述历史;不编造不存在的事实
105145

desktop/src/db/modules/suggestion-config.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export default function SuggestionConfigManager(BaseClass) {
1717
ensureColumn('situation_llm_enabled', 'ALTER TABLE suggestion_configs ADD COLUMN situation_llm_enabled INTEGER DEFAULT 0');
1818
ensureColumn('situation_model_name', "ALTER TABLE suggestion_configs ADD COLUMN situation_model_name TEXT DEFAULT 'gpt-4o-mini'");
1919
ensureColumn('thinking_enabled', 'ALTER TABLE suggestion_configs ADD COLUMN thinking_enabled INTEGER DEFAULT 0');
20+
ensureColumn('user_profile', 'ALTER TABLE suggestion_configs ADD COLUMN user_profile TEXT DEFAULT NULL');
2021

2122
// 补齐已有行的默认值
2223
this.db
@@ -120,7 +121,8 @@ export default function SuggestionConfigManager(BaseClass) {
120121
'context_message_limit',
121122
'topic_detection_enabled',
122123
'model_name',
123-
'thinking_enabled'
124+
'thinking_enabled',
125+
'user_profile'
124126
];
125127

126128
updatableFields.forEach((field) => {

desktop/src/renderer/components/Chat/SuggestionsPanel.jsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,17 @@ export const SuggestionsPanel = ({
144144
))}
145145
</div>
146146
)}
147+
{typeof suggestion.affinity_prediction === 'number' && (
148+
<div className="suggestion-affinity" title="预测对方接受度 (0-10)">
149+
<span className={`affinity-score ${
150+
suggestion.affinity_prediction >= 8 ? 'high' :
151+
suggestion.affinity_prediction >= 6 ? 'medium' :
152+
suggestion.affinity_prediction >= 4 ? 'low' : 'very-low'
153+
}`}>
154+
{suggestion.affinity_prediction}
155+
</span>
156+
</div>
157+
)}
147158
{!suggestion.is_selected ? (
148159
<div className="suggestion-card-actions">
149160
<button

desktop/src/renderer/components/Suggestions/SuggestionConfigForm.jsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,29 @@ export const SuggestionConfigForm = ({
2222
</div>
2323
)}
2424

25+
{/* 用户个人档案 */}
26+
<div className="p-4 rounded-lg border border-border-light dark:border-border-dark">
27+
<div className="mb-3">
28+
<p className="font-medium text-text-light dark:text-text-dark flex items-center gap-2">
29+
<span className="material-symbols-outlined text-primary text-lg">person</span>
30+
用户个人档案
31+
</p>
32+
<p className="text-sm text-text-muted-light dark:text-text-muted-dark mt-1">
33+
描述你的性格特点、沟通风格、偏好等,AI 会参考这些信息生成更符合你风格的建议。
34+
</p>
35+
</div>
36+
<textarea
37+
value={form.user_profile || ''}
38+
onChange={(e) => onUpdateField('user_profile', e.target.value)}
39+
placeholder="例如:我比较内向,喜欢用幽默的方式表达;不太擅长直接表达感情,更喜欢含蓄暗示..."
40+
rows={3}
41+
className="w-full px-3 py-2 border border-border-light dark:border-border-dark rounded-lg bg-surface-light dark:bg-surface-dark text-text-light dark:text-text-dark placeholder-text-muted-light dark:placeholder-text-muted-dark focus:outline-none focus:ring-2 focus:ring-primary/50 resize-none"
42+
/>
43+
<p className="text-xs text-text-muted-light dark:text-text-muted-dark mt-2">
44+
⚠️ 此内容会影响 AI 生成的建议风格,请谨慎填写。留空则使用通用策略。
45+
</p>
46+
</div>
47+
2548
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between p-4 rounded-lg border border-border-light dark:border-border-dark">
2649
<div>
2750
<p className="font-medium text-text-light dark:text-text-dark">被动推荐</p>

0 commit comments

Comments
 (0)