40e4a527fe
Introduce RRF-based recall fusion for personal/episodic/temporal paths, zvec-grep workspace shadow probing for code executors, and a local headroom proxy starter aligned with the fusion plan defaults. Co-authored-by: Cursor <cursoragent@cursor.com>
124 lines
3.5 KiB
JavaScript
124 lines
3.5 KiB
JavaScript
import { spawnSync } from 'node:child_process';
|
|
import path from 'node:path';
|
|
|
|
export const DEFAULT_ZVEC_QUERY_LIMIT = 8;
|
|
|
|
function truthy(value, fallback = false) {
|
|
const raw = String(value ?? '').trim().toLowerCase();
|
|
if (!raw) return fallback;
|
|
return ['1', 'true', 'yes', 'on'].includes(raw);
|
|
}
|
|
|
|
export function resolveZvecWorkspaceMode(env = process.env) {
|
|
const raw = String(env.MEMIND_ZVEC_WORKSPACE_MODE ?? 'off').trim().toLowerCase();
|
|
return ['off', 'shadow', 'active'].includes(raw) ? raw : 'off';
|
|
}
|
|
|
|
export function resolveZvecBinary(env = process.env) {
|
|
return String(env.MEMIND_ZVEC_BINARY ?? 'zg').trim() || 'zg';
|
|
}
|
|
|
|
export function resolveZvecWorkspaceRoot(cwd, env = process.env) {
|
|
const explicit = String(env.MEMIND_ZVEC_WORKSPACE_ROOT ?? '').trim();
|
|
if (explicit) return path.resolve(explicit);
|
|
return path.resolve(String(cwd ?? process.cwd()));
|
|
}
|
|
|
|
function parseZvecQueryOutput(stdout) {
|
|
const hits = [];
|
|
const lines = String(stdout ?? '').split('\n');
|
|
for (const line of lines) {
|
|
const match = line.match(/^#(\d+) matchedBy=([^\s]+)\s+(.+?):(\d+)-(\d+)/);
|
|
if (!match) continue;
|
|
hits.push({
|
|
rank: Number(match[1]),
|
|
matchedBy: match[2],
|
|
path: match[3].trim(),
|
|
startLine: Number(match[4]),
|
|
endLine: Number(match[5]),
|
|
});
|
|
}
|
|
return hits;
|
|
}
|
|
|
|
export function searchZvecWorkspace({
|
|
query,
|
|
limit = DEFAULT_ZVEC_QUERY_LIMIT,
|
|
cwd,
|
|
env = process.env,
|
|
spawnImpl = spawnSync,
|
|
} = {}) {
|
|
const mode = resolveZvecWorkspaceMode(env);
|
|
const text = String(query ?? '').trim();
|
|
const workspaceRoot = resolveZvecWorkspaceRoot(cwd, env);
|
|
if (mode === 'off' || !text) {
|
|
return {
|
|
mode,
|
|
query: text,
|
|
hits: [],
|
|
applied: false,
|
|
skipped: true,
|
|
reason: mode === 'off' ? 'disabled' : 'empty_query',
|
|
};
|
|
}
|
|
|
|
const binary = resolveZvecBinary(env);
|
|
const result = spawnImpl(binary, ['query', text, '--limit', String(limit)], {
|
|
cwd: workspaceRoot,
|
|
env: { ...process.env, ...env },
|
|
encoding: 'utf8',
|
|
});
|
|
|
|
if (result.status !== 0) {
|
|
return {
|
|
mode,
|
|
query: text,
|
|
hits: [],
|
|
applied: false,
|
|
skipped: true,
|
|
reason: 'zvec_query_failed',
|
|
stderr: String(result.stderr ?? '').trim().slice(0, 400),
|
|
};
|
|
}
|
|
|
|
const hits = parseZvecQueryOutput(result.stdout);
|
|
return {
|
|
mode,
|
|
query: text,
|
|
workspaceRoot,
|
|
hits,
|
|
hitCount: hits.length,
|
|
applied: mode === 'active',
|
|
skipped: false,
|
|
stdoutPreview: String(result.stdout ?? '').trim().slice(0, 1200),
|
|
};
|
|
}
|
|
|
|
export function formatZvecWorkspaceBlock(result) {
|
|
if (!result?.hits?.length) return '';
|
|
const lines = [
|
|
'【Workspace 代码检索】',
|
|
'以下片段来自本工作区索引,仅供开发/排查参考,不是用户生活记忆。',
|
|
];
|
|
for (const hit of result.hits.slice(0, 8)) {
|
|
lines.push(
|
|
`- ${hit.path}:${hit.startLine}-${hit.endLine} (${hit.matchedBy})`,
|
|
);
|
|
}
|
|
return lines.join('\n').trim();
|
|
}
|
|
|
|
export function isZvecWorkspaceEnabled(env = process.env) {
|
|
return resolveZvecWorkspaceMode(env) !== 'off';
|
|
}
|
|
|
|
export function shouldAttachZvecWorkspaceForTask(taskType, env = process.env) {
|
|
if (!isZvecWorkspaceEnabled(env)) return false;
|
|
const raw = String(env.MEMIND_ZVEC_WORKSPACE_TASK_TYPES ?? 'page_data_dev_complex,repo_refactor,multi_file')
|
|
.split(',')
|
|
.map((item) => item.trim().toLowerCase())
|
|
.filter(Boolean);
|
|
const normalizedTask = String(taskType ?? '').trim().toLowerCase();
|
|
return raw.includes(normalizedTask);
|
|
}
|