Files
memind/user-memory-profile.mjs
T

179 lines
5.6 KiB
JavaScript
Raw Normal View History

import fs from 'node:fs';
import path from 'node:path';
import { resolveUserAddressName } from './user-publish.mjs';
export const USER_MEMORY_PROFILE_FILENAME = '.tkmind-profile.json';
export const USER_MEMORY_PROFILE_VERSION = 1;
export function buildInitialUserMemoryProfile({
userId,
displayName,
username,
slug,
now = Date.now(),
}) {
const addressName = resolveUserAddressName({ displayName, username, slug });
return {
version: USER_MEMORY_PROFILE_VERSION,
userId,
displayName: addressName,
language: 'zh-CN',
responseStyle: 'balanced',
preferences: [],
createdAt: now,
updatedAt: now,
};
}
export function resolveUserMemoryProfilePath(workspaceRoot) {
return path.join(workspaceRoot, USER_MEMORY_PROFILE_FILENAME);
}
export function loadUserMemoryProfile(workspaceRoot) {
const profilePath = resolveUserMemoryProfilePath(workspaceRoot);
if (!fs.existsSync(profilePath)) return null;
try {
const raw = fs.readFileSync(profilePath, 'utf8');
const parsed = JSON.parse(raw);
if (!parsed || typeof parsed !== 'object') return null;
return parsed;
} catch {
return null;
}
}
export function ensureUserMemoryProfile(workspaceRoot, context) {
const profilePath = resolveUserMemoryProfilePath(workspaceRoot);
const existing = loadUserMemoryProfile(workspaceRoot);
if (existing) {
const addressName = resolveUserAddressName(context);
const nextDisplayName = addressName || existing.displayName;
if (nextDisplayName && nextDisplayName !== existing.displayName) {
const updated = {
...existing,
displayName: nextDisplayName,
updatedAt: Date.now(),
};
fs.writeFileSync(profilePath, `${JSON.stringify(updated, null, 2)}\n`, 'utf8');
return updated;
}
return existing;
}
const profile = buildInitialUserMemoryProfile({
userId: context.userId,
displayName: context.displayName,
username: context.username,
slug: context.slug,
});
fs.writeFileSync(profilePath, `${JSON.stringify(profile, null, 2)}\n`, 'utf8');
return profile;
}
export function renderMemoryStoreGuidance({ addressName }) {
const name = addressName || '用户';
return [
'## TKMind 长期记忆(L3 / memory 扩展)',
'',
`- 当前服务对象:**${name}**(仅此用户,不得与其他用户混淆)`,
'- 使用 memory 工具记录用户**明确表达**的偏好、习惯与稳定事实',
'- 记忆分类建议:',
' - `preference`:回复风格、格式、语言、称呼习惯',
' - `project`:当前项目目标、约定、技术选型(与 L2 项目记忆互补)',
' - `fact`:长期稳定、可复用的事实(如部门、常用工具)',
'- 用户说「忘记…」「不要再…」「取消偏好…」时,必须更新或删除对应记忆',
'- **禁止**记录其他用户的信息;**禁止**猜测未明确表达的偏好',
'- 与 L2 项目记忆(harness)分工:L2 记近期工作与决策摘要;L3 记可跨会话复用的个人偏好',
].join('\n');
}
function renderPreferenceLines(profile) {
const items = Array.isArray(profile?.preferences) ? profile.preferences : [];
if (items.length === 0) {
return ['- (暂无结构化偏好 — 可在对话中用 memory 工具补充)'];
}
return items.map((item) => {
if (typeof item === 'string') return `- ${item}`;
const category = item?.category ? `[${item.category}] ` : '';
const label = item?.label ?? item?.key ?? 'preference';
const value = item?.value ?? item?.content ?? '';
return `- ${category}${label}:${value}`;
});
}
export function renderUserMemoryProfileForHarness(profile, context = {}) {
const addressName = resolveUserAddressName({
displayName: profile?.displayName ?? context.displayName,
username: context.username,
slug: context.slug,
});
const language = profile?.language === 'zh-CN' ? '简体中文' : (profile?.language ?? '简体中文');
const styleMap = {
concise: '简洁直接,少废话',
detailed: '详细完整,必要时展开',
balanced: '简洁务实,必要时补充细节',
};
const responseStyle = styleMap[profile?.responseStyle] ?? styleMap.balanced;
return [
'## TKMind 用户偏好画像(L3)',
'',
`- 用户称呼:**${addressName}**`,
`- 界面语言:${language}`,
`- 默认回复风格:${responseStyle}`,
'',
'### 已记录的结构化偏好',
...renderPreferenceLines(profile),
].join('\n');
}
export function buildSessionMemoryEntries({
workingDir,
sessionPolicy,
sandboxConstraints = null,
userContext = null,
}) {
const entries = [];
if (sandboxConstraints?.trim()) {
entries.push({
title: 'TKMind 用户空间沙箱',
content: sandboxConstraints.trim(),
});
}
if (!hasMemoryStore(sessionPolicy)) {
return entries;
}
const profile =
loadUserMemoryProfile(workingDir) ??
(userContext?.userId
? ensureUserMemoryProfile(workingDir, userContext)
: null);
const addressName = resolveUserAddressName({
displayName: profile?.displayName ?? userContext?.displayName,
username: userContext?.username,
slug: userContext?.slug,
});
entries.push({
title: 'TKMind 长期记忆规则',
content: renderMemoryStoreGuidance({ addressName }),
});
if (profile) {
entries.push({
title: 'TKMind 用户偏好画像',
content: renderUserMemoryProfileForHarness(profile, userContext ?? {}),
});
}
return entries;
}
export function hasMemoryStore(sessionPolicy) {
return (sessionPolicy?.extensionOverrides ?? []).some((ext) => ext.name === 'memory');
}