(params: Record<string, unknown>, context: ToolExecutionContext)
| 336 | } |
| 337 | |
| 338 | async function handler(params: Record<string, unknown>, context: ToolExecutionContext): Promise<ToolResult> { |
| 339 | const locale = context.locale |
| 340 | const query = typeof params.query === 'string' ? params.query.trim() : '' |
| 341 | const criteria = typeof params.criteria === 'string' && params.criteria.trim() ? params.criteria.trim() : undefined |
| 342 | const keywords = Array.isArray(params.keywords) |
| 343 | ? (params.keywords.filter((k) => typeof k === 'string' && k.trim()) as string[]) |
| 344 | : [] |
| 345 | const requestedMode: EvidenceRetrievalMode = |
| 346 | params.mode === 'hybrid' || params.mode === 'semantic' || params.mode === 'keyword' || params.mode === 'auto' |
| 347 | ? params.mode |
| 348 | : 'auto' |
| 349 | |
| 350 | const service = context.semanticIndexService |
| 351 | const semanticAvailable = !!service && (await service.canSearch(context.sessionId)) |
| 352 | const rangeMs = parseEvidenceTimeRange(params, context) |
| 353 | |
| 354 | const { mode, warnings } = resolveMode(requestedMode, semanticAvailable, keywords.length > 0) |
| 355 | if (!criteria) warnings.add('criteria_missing') |
| 356 | |
| 357 | const runSemantic = mode === 'semantic' || mode === 'hybrid' |
| 358 | const runKeyword = mode === 'keyword' || mode === 'hybrid' |
| 359 | |
| 360 | let semanticRan = false |
| 361 | let keywordRan = false |
| 362 | const semanticCandidates: EvidenceCandidate[] = [] |
| 363 | let keywordCandidates: EvidenceCandidate[] = [] |
| 364 | |
| 365 | // 语义路径:service 内按 chunk 时间范围过滤;工具层再按 startTime/endTime 兜底过滤 |
| 366 | if (runSemantic) { |
| 367 | if (!service || !semanticAvailable) { |
| 368 | warnings.add('semantic_unavailable') |
| 369 | } else { |
| 370 | const result = await service.searchForTool(context.sessionId, query, { |
| 371 | maxResults: clampMaxResults(params.max_results), |
| 372 | preprocessConfig: context.preprocessConfig, |
| 373 | ownerPlatformId: context.ownerPlatformId, |
| 374 | locale, |
| 375 | maxResultTokens: context.maxToolResultTokens, |
| 376 | timeFilter: rangeMs, |
| 377 | }) |
| 378 | semanticRan = true |
| 379 | if (!result.available) { |
| 380 | warnings.add('semantic_unavailable') |
| 381 | } else { |
| 382 | if (result.partial) warnings.add('semantic_partial') |
| 383 | for (const s of result.sources) { |
| 384 | const startMs = parseIsoMs(s.startTime) |
| 385 | const endMs = parseIsoMs(s.endTime) ?? startMs |
| 386 | if (rangeMs && startMs != null && endMs != null && !overlapsTimeRangeMs(startMs, endMs, rangeMs)) continue |
| 387 | const evidenceText = stripLineTimestamps(s.text || s.snippet) |
| 388 | semanticCandidates.push({ |
| 389 | messageId: s.startMessageId, |
| 390 | startMessageId: s.startMessageId, |
| 391 | endMessageId: s.endMessageId, |
| 392 | timestamp: startMs ?? 0, |
| 393 | rangeStartMs: startMs ?? 0, |
| 394 | rangeEndMs: endMs ?? startMs ?? 0, |
| 395 | snippet: evidenceText, |
nothing calls this directly
no test coverage detected