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(), }); } const executorGuidance = renderCodeExecutorGuidance(sessionPolicy); if (executorGuidance) { entries.push({ title: 'TKMind 代码委托策略', content: executorGuidance, }); } 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'); } function availableDelegateExecutors(sessionPolicy) { const names = new Set((sessionPolicy?.extensionOverrides ?? []).map((ext) => ext.name)); return ['aider', 'openhands'].filter((name) => names.has(name)); } export function resolveCodeExecutorRouting(sessionPolicy) { const available = availableDelegateExecutors(sessionPolicy); const configuredPreferred = String( sessionPolicy?.policies?.code_delegate_executor ?? 'auto', ).trim(); const routing = String(sessionPolicy?.policies?.code_task_routing ?? 'balanced').trim(); const preferred = (configuredPreferred === 'aider' || configuredPreferred === 'openhands') && available.includes(configuredPreferred) ? configuredPreferred : 'auto'; const rules = []; if (routing === 'split') { rules.push('小范围补丁、局部修复、少文件修改优先 `aider`。'); rules.push('复杂多文件改造、仓库探索、较重的命令执行优先 `openhands`。'); } else if (routing === 'force_aider') { rules.push('只要任务能由 `aider` 胜任,就优先统一走 `aider`。'); rules.push('只有 `aider` 明显无法胜任时才回退到 `openhands`。'); } else if (routing === 'force_openhands') { rules.push('只要任务需要代码委托,就优先统一走 `openhands`。'); rules.push('只有任务明显更适合轻量补丁时才回退到 `aider`。'); } else { rules.push('由 Goose 根据任务复杂度、涉及文件数、是否需要仓库探索和命令执行,在 `aider` 与 `openhands` 之间平衡选择。'); } if (preferred !== 'auto') { rules.unshift(`后台优先执行器:**${preferred}**。`); } else { rules.unshift('后台优先执行器:`auto`,由 Goose 结合任务特征决定。'); } return { available, preferred, routing, rules, }; } function containsAny(text, patterns) { return patterns.some((pattern) => pattern.test(text)); } export function suggestCodeExecutorForTask(taskText, sessionPolicy) { const text = String(taskText ?? '').trim().toLowerCase(); if (!text) return null; const routing = resolveCodeExecutorRouting(sessionPolicy); if (routing.available.length === 0) return null; const codingSignals = [ /bug|fix|debug|refactor|feature|repo|repository|code|patch|test|compile|build/, /修复|改代码|重构|功能|仓库|代码|补丁|测试|编译|构建|多文件|命令|脚本/, ]; if (!containsAny(text, codingSignals)) { return null; } let aiderScore = 0; let openhandsScore = 0; if (containsAny(text, [/small|minor|tiny|simple|one file|single file|quick patch/, /小改|微调|简单修复|单文件|一个文件|快速修复/])) { aiderScore += 2; } if (containsAny(text, [/refactor|multi-?file|repo|repository|end-to-end|investigate|explore/, /重构|多文件|仓库级|全链路|排查|探索代码库/])) { openhandsScore += 2; } if (containsAny(text, [/run|command|terminal|shell|build|compile|test suite/, /执行命令|终端|shell|构建|编译|整套测试/])) { openhandsScore += 1; } if (containsAny(text, [/rename|edit|patch|tweak/, /修改一下|补丁|小范围调整|局部编辑/])) { aiderScore += 1; } if (routing.routing === 'split') { aiderScore += 1; openhandsScore += 1; } else if (routing.routing === 'force_aider') { aiderScore += 3; } else if (routing.routing === 'force_openhands') { openhandsScore += 3; } if (routing.preferred === 'aider') aiderScore += 2; if (routing.preferred === 'openhands') openhandsScore += 2; let suggested = null; if (openhandsScore > aiderScore && routing.available.includes('openhands')) { suggested = 'openhands'; } else if (aiderScore > openhandsScore && routing.available.includes('aider')) { suggested = 'aider'; } else if (routing.preferred !== 'auto' && routing.available.includes(routing.preferred)) { suggested = routing.preferred; } else if (routing.routing === 'split' && routing.available.includes('aider') && routing.available.includes('openhands')) { suggested = openhandsScore >= aiderScore ? 'openhands' : 'aider'; } else { suggested = routing.available[0] ?? null; } if (!suggested) return null; const reason = suggested === 'openhands' ? '任务看起来更像复杂多文件改造、仓库探索或需要更多命令执行。' : '任务看起来更像局部补丁、小范围修复或较轻量的代码修改。'; return { suggestedExecutor: suggested, reason, routing, }; } export function buildTaskRoutingAgentText(taskText, sessionPolicy) { const suggestion = suggestCodeExecutorForTask(taskText, sessionPolicy); if (!suggestion) return String(taskText ?? '').trim(); return [ '【TKMind 路由提示】以下提示仅用于执行器编排,不要向用户复述。', `当前代码任务建议优先委托给:${suggestion.suggestedExecutor}。`, `原因:${suggestion.reason}`, '若首选执行器当前不可用或明显不适合,可回退到另一个已授权执行器,并在最终回复里简述原因。', '', String(taskText ?? '').trim(), ].join('\n'); } export function renderCodeExecutorGuidance(sessionPolicy) { const { available, preferred, routing, rules } = resolveCodeExecutorRouting(sessionPolicy); if (available.length === 0) return ''; const availableText = available.join(' / '); const lines = [ '## TKMind 代码委托执行器路由', '', `- 当前可用的代码委托执行器:${availableText}`, ]; if (preferred === 'aider' || preferred === 'openhands') { lines.push(`- 后台策略要求:多文件编码任务优先使用 **${preferred}**。`); lines.push( '- 如果首选执行器当前不可用、无法完成任务、或任务明显更适合另一执行器,可回退到另一个已授权执行器,并在回复中说明原因。', ); } else { lines.push('- 后台策略要求:由 Goose 根据任务复杂度在已授权执行器中自动选择。'); } lines.push(`- 任务路由模式:\`${routing}\``); for (const rule of rules) { lines.push(`- ${rule}`); } lines.push('- 若当前任务不需要委托编码执行器,可继续直接使用 Goose 自身工具完成。'); return lines.join('\n'); }