2026-06-15 22:09:38 -07:00
|
|
|
|
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;
|
2026-06-29 22:20:04 +08:00
|
|
|
|
const DEFAULT_TIMEZONE = 'Asia/Shanghai';
|
2026-06-15 22:09:38 -07:00
|
|
|
|
|
|
|
|
|
|
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');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-29 06:59:37 +08:00
|
|
|
|
function renderStoredUserMemoryLines(memories) {
|
|
|
|
|
|
const items = Array.isArray(memories) ? memories : [];
|
|
|
|
|
|
if (items.length === 0) return ['- (暂无已沉淀的对话记忆)'];
|
|
|
|
|
|
return items.slice(0, 50).map((item) => {
|
|
|
|
|
|
const label = item?.label ? `[${item.label}] ` : '';
|
|
|
|
|
|
const text = String(item?.text ?? item?.memory_text ?? item?.memoryText ?? '').trim();
|
|
|
|
|
|
return `- ${label}${text}`;
|
|
|
|
|
|
}).filter((line) => line.trim() !== '-');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function renderStoredUserMemoriesForHarness(memories) {
|
|
|
|
|
|
return [
|
|
|
|
|
|
'## TKMind 已沉淀用户记忆',
|
|
|
|
|
|
'',
|
|
|
|
|
|
'以下内容来自用户历史对话的长期记忆抽取,仅用于改善当前用户体验。',
|
|
|
|
|
|
'不要把这些内容透露给其他用户;如果用户要求忘记或删除,应遵从并更新对应记忆。',
|
|
|
|
|
|
'',
|
|
|
|
|
|
...renderStoredUserMemoryLines(memories),
|
|
|
|
|
|
].join('\n');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-29 22:20:04 +08:00
|
|
|
|
function formatDateParts(now, timezone) {
|
|
|
|
|
|
const formatter = new Intl.DateTimeFormat('en-CA', {
|
|
|
|
|
|
timeZone: timezone,
|
|
|
|
|
|
year: 'numeric',
|
|
|
|
|
|
month: '2-digit',
|
|
|
|
|
|
day: '2-digit',
|
|
|
|
|
|
});
|
|
|
|
|
|
const parts = Object.fromEntries(
|
|
|
|
|
|
formatter.formatToParts(new Date(now)).map((part) => [part.type, part.value]),
|
|
|
|
|
|
);
|
|
|
|
|
|
return {
|
|
|
|
|
|
year: parts.year ?? '0000',
|
|
|
|
|
|
month: parts.month ?? '01',
|
|
|
|
|
|
day: parts.day ?? '01',
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-30 09:30:51 +08:00
|
|
|
|
function formatLocalClockParts(now, timezone) {
|
|
|
|
|
|
const formatter = new Intl.DateTimeFormat('en-GB', {
|
|
|
|
|
|
timeZone: timezone,
|
|
|
|
|
|
hour: '2-digit',
|
|
|
|
|
|
minute: '2-digit',
|
|
|
|
|
|
hour12: false,
|
|
|
|
|
|
});
|
|
|
|
|
|
const parts = Object.fromEntries(
|
|
|
|
|
|
formatter.formatToParts(new Date(now)).map((part) => [part.type, part.value]),
|
|
|
|
|
|
);
|
|
|
|
|
|
return {
|
|
|
|
|
|
hour: Number(parts.hour ?? 0),
|
|
|
|
|
|
minute: Number(parts.minute ?? 0),
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function resolveTimeOfDayPeriod(hour) {
|
|
|
|
|
|
const h = Number(hour);
|
|
|
|
|
|
if (h < 5) return { period: '凌晨', greeting: '你好' };
|
|
|
|
|
|
if (h < 9) return { period: '早上', greeting: '早上好' };
|
|
|
|
|
|
if (h < 12) return { period: '上午', greeting: '上午好' };
|
|
|
|
|
|
if (h < 14) return { period: '中午', greeting: '中午好' };
|
|
|
|
|
|
if (h < 18) return { period: '下午', greeting: '下午好' };
|
|
|
|
|
|
return { period: '晚上', greeting: '晚上好' };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-01 08:40:27 +08:00
|
|
|
|
export function buildCurrentTimeAgentPrefix({ now = Date.now(), timezone = DEFAULT_TIMEZONE } = {}) {
|
|
|
|
|
|
return [
|
|
|
|
|
|
'【TKMind 时间基准 - 仅供回答时间/日期/时段问候,不要向用户复述】',
|
|
|
|
|
|
renderCurrentTimeAnchor({ now, timezone }),
|
|
|
|
|
|
'',
|
|
|
|
|
|
].join('\n');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-29 22:20:04 +08:00
|
|
|
|
export function renderCurrentTimeAnchor({ now = Date.now(), timezone = DEFAULT_TIMEZONE } = {}) {
|
|
|
|
|
|
const { year, month, day } = formatDateParts(now, timezone);
|
2026-06-30 09:30:51 +08:00
|
|
|
|
const { hour, minute } = formatLocalClockParts(now, timezone);
|
|
|
|
|
|
const { period, greeting } = resolveTimeOfDayPeriod(hour);
|
|
|
|
|
|
const clock = `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`;
|
2026-06-29 22:20:04 +08:00
|
|
|
|
const weekday = new Intl.DateTimeFormat('zh-CN', {
|
|
|
|
|
|
timeZone: timezone,
|
|
|
|
|
|
weekday: 'long',
|
|
|
|
|
|
}).format(new Date(now));
|
|
|
|
|
|
return [
|
|
|
|
|
|
'## TKMind 当前时间基准',
|
|
|
|
|
|
'',
|
|
|
|
|
|
`- 当前时区:${timezone}`,
|
|
|
|
|
|
`- 当前日期:${year}-${month}-${day}(${weekday})`,
|
2026-06-30 09:30:51 +08:00
|
|
|
|
`- 当前时刻:${clock}(${period})`,
|
|
|
|
|
|
`- 若需时段问候可参考:${greeting}(仅新会话开场或用户主动打招呼时使用,普通任务回复不要每条都加)`,
|
|
|
|
|
|
'- 回答中涉及“今天 / 明天 / 后天 / 周几 / 早上 / 下午 / 晚上”等时间表述时,必须以上述日期与时刻为准,禁止自行假设当前时间或使用 UTC 等其它时区。',
|
2026-06-29 22:20:04 +08:00
|
|
|
|
].join('\n');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 22:09:38 -07:00
|
|
|
|
export function buildSessionMemoryEntries({
|
|
|
|
|
|
workingDir,
|
|
|
|
|
|
sessionPolicy,
|
|
|
|
|
|
sandboxConstraints = null,
|
|
|
|
|
|
userContext = null,
|
2026-06-29 06:59:37 +08:00
|
|
|
|
userMemories = null,
|
2026-06-29 22:20:04 +08:00
|
|
|
|
now = Date.now(),
|
2026-06-15 22:09:38 -07:00
|
|
|
|
}) {
|
|
|
|
|
|
const entries = [];
|
2026-06-29 22:20:04 +08:00
|
|
|
|
const timezone = String(
|
|
|
|
|
|
userContext?.timezone ?? process.env.H5_DEFAULT_TIMEZONE ?? DEFAULT_TIMEZONE,
|
|
|
|
|
|
).trim() || DEFAULT_TIMEZONE;
|
2026-06-15 22:09:38 -07:00
|
|
|
|
|
|
|
|
|
|
if (sandboxConstraints?.trim()) {
|
|
|
|
|
|
entries.push({
|
|
|
|
|
|
title: 'TKMind 用户空间沙箱',
|
|
|
|
|
|
content: sandboxConstraints.trim(),
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-26 15:19:03 +08:00
|
|
|
|
const executorGuidance = renderCodeExecutorGuidance(sessionPolicy);
|
|
|
|
|
|
if (executorGuidance) {
|
|
|
|
|
|
entries.push({
|
|
|
|
|
|
title: 'TKMind 代码委托策略',
|
|
|
|
|
|
content: executorGuidance,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-29 22:20:04 +08:00
|
|
|
|
entries.push({
|
|
|
|
|
|
title: 'TKMind 当前时间基准',
|
|
|
|
|
|
content: renderCurrentTimeAnchor({ now, timezone }),
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-06-30 02:02:58 +08:00
|
|
|
|
const scheduleTools = (sessionPolicy?.extensionOverrides ?? []).find((ext) => ext.name === 'sandbox-fs');
|
|
|
|
|
|
if ((scheduleTools?.available_tools ?? []).includes('schedule_create_item')) {
|
|
|
|
|
|
entries.push({
|
|
|
|
|
|
title: 'TKMind 日程写入规则',
|
|
|
|
|
|
content: [
|
|
|
|
|
|
'创建或提醒待办/日程时:',
|
|
|
|
|
|
'- 必须使用 startLocal / endLocal / remindLocal,格式 YYYY-MM-DD HH:mm(用户时区墙上时钟)。',
|
|
|
|
|
|
'- 禁止自行估算 Unix 毫秒时间戳;传错会被工具拒绝。',
|
|
|
|
|
|
'- 写入后调用 schedule_list_items 核对日期是否与用户表述一致。',
|
|
|
|
|
|
].join('\n'),
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 22:09:38 -07:00
|
|
|
|
if (!hasMemoryStore(sessionPolicy)) {
|
|
|
|
|
|
return entries;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-26 15:56:22 +08:00
|
|
|
|
// Always call ensureUserMemoryProfile when userContext is available so that
|
|
|
|
|
|
// stale displayName values (e.g. UUID written before nickname was resolved)
|
|
|
|
|
|
// are healed on the next session init.
|
|
|
|
|
|
const profile = userContext?.userId
|
|
|
|
|
|
? ensureUserMemoryProfile(workingDir, userContext)
|
|
|
|
|
|
: loadUserMemoryProfile(workingDir);
|
2026-06-15 22:09:38 -07:00
|
|
|
|
|
|
|
|
|
|
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 ?? {}),
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-29 06:59:37 +08:00
|
|
|
|
if (Array.isArray(userMemories) && userMemories.length > 0) {
|
|
|
|
|
|
entries.push({
|
|
|
|
|
|
title: 'TKMind 已沉淀用户记忆',
|
|
|
|
|
|
content: renderStoredUserMemoriesForHarness(userMemories),
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 22:09:38 -07:00
|
|
|
|
return entries;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function hasMemoryStore(sessionPolicy) {
|
|
|
|
|
|
return (sessionPolicy?.extensionOverrides ?? []).some((ext) => ext.name === 'memory');
|
|
|
|
|
|
}
|
2026-06-26 15:19:03 +08:00
|
|
|
|
|
|
|
|
|
|
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');
|
|
|
|
|
|
}
|