@lorrylurui/code-intelligence-mcp 2.0.4 → 2.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -0
- package/dist/config/env.js +9 -0
- package/dist/config/tuning.js +114 -0
- package/dist/db/schema.js +37 -0
- package/dist/index.js +1 -0
- package/dist/indexer/babelParser.js +2 -1
- package/dist/indexer/chunkText.js +164 -0
- package/dist/indexer/embedText.js +2 -2
- package/dist/indexer/indexProject.js +193 -22
- package/dist/indexer/jsAstNormalizer.js +36 -6
- package/dist/prompts/reusableCodeAdvisorPrompt.js +63 -34
- package/dist/repositories/chunkRepository.js +181 -0
- package/dist/repositories/symbolRepository.js +108 -15
- package/dist/server/createServer.js +16 -0
- package/dist/services/contextAssembler.js +150 -0
- package/dist/services/ranking.js +109 -58
- package/dist/services/recommendationService.js +515 -46
- package/dist/services/reindex.js +25 -0
- package/dist/tools/getSymbolDetail.js +2 -1
- package/dist/tools/queryDocs.js +113 -0
- package/dist/tools/recommendComponent.js +86 -10
- package/dist/tools/searchByStructure.js +2 -1
- package/dist/tools/searchSymbols.js +57 -21
- package/dist/types/chunk.js +1 -0
- package/dist/workers/embeddingWorker.js +0 -1
- package/package.json +1 -1
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* query_docs MCP 工具:完整的 RAG 检索 + 上下文组装入口。
|
|
3
|
+
*
|
|
4
|
+
* 调用链:
|
|
5
|
+
* query
|
|
6
|
+
* → ChunkRepository.searchSemantic() 向量检索 topK chunk
|
|
7
|
+
* → ContextAssembler.assemble() 邻块扩展 → 去重 → 字符预算截断 → 文本渲染
|
|
8
|
+
* → 返回 contextText + sources 供 LLM 合成最终回答
|
|
9
|
+
*
|
|
10
|
+
* 为什么工具只返回 contextText 而不直接生成回答?
|
|
11
|
+
* 本 MCP server 没有内置 LLM,"合成回答"由调用方(Claude/GPT 等)完成。
|
|
12
|
+
* 工具负责"检索 + 组装",调用方负责"理解 + 生成",职责清晰、可独立测试。
|
|
13
|
+
*/
|
|
14
|
+
import { z } from 'zod';
|
|
15
|
+
import { ChunkRepository } from '../repositories/chunkRepository.js';
|
|
16
|
+
import { ContextAssembler } from '../services/contextAssembler.js';
|
|
17
|
+
import { CHUNK_TOP_K, CONTEXT_ADJACENT_RADIUS, CONTEXT_MAX_CHARS, CONTEXT_MAX_CHUNKS, } from '../config/tuning.js';
|
|
18
|
+
export const queryDocsInput = z.object({
|
|
19
|
+
/** 自然语言查询,将被向量化后用于语义检索 */
|
|
20
|
+
query: z.string().min(1),
|
|
21
|
+
/** 语义检索拉取的候选 chunk 数,最终受字符预算限制 */
|
|
22
|
+
limit: z.number().int().min(1).max(50).optional().default(CHUNK_TOP_K),
|
|
23
|
+
/**
|
|
24
|
+
* 每个命中 chunk 向前后各扩展的邻块数。
|
|
25
|
+
* 0 = 不扩展(纯向量检索结果);1 = 各取一块(推荐);2 = 各取两块(长文档)
|
|
26
|
+
*/
|
|
27
|
+
adjacentRadius: z
|
|
28
|
+
.number()
|
|
29
|
+
.int()
|
|
30
|
+
.min(0)
|
|
31
|
+
.max(3)
|
|
32
|
+
.optional()
|
|
33
|
+
.default(CONTEXT_ADJACENT_RADIUS),
|
|
34
|
+
/** 上下文总字符数预算,超出时截断末尾 chunk */
|
|
35
|
+
maxChars: z.number().int().min(500).optional().default(CONTEXT_MAX_CHARS),
|
|
36
|
+
/** 扩展后保留的最大 chunk 数量 */
|
|
37
|
+
maxChunks: z
|
|
38
|
+
.number()
|
|
39
|
+
.int()
|
|
40
|
+
.min(1)
|
|
41
|
+
.max(30)
|
|
42
|
+
.optional()
|
|
43
|
+
.default(CONTEXT_MAX_CHUNKS),
|
|
44
|
+
/** 仅检索指定文档路径下的 chunk(精确路径过滤) */
|
|
45
|
+
path: z.string().optional(),
|
|
46
|
+
});
|
|
47
|
+
export function createQueryDocsTool() {
|
|
48
|
+
const repo = new ChunkRepository();
|
|
49
|
+
const assembler = new ContextAssembler(repo);
|
|
50
|
+
return {
|
|
51
|
+
name: 'query_docs',
|
|
52
|
+
description: '对文档知识库进行语义检索,返回与查询最相关的文档片段(已组装为可直接注入 prompt 的上下文文本)。\n' +
|
|
53
|
+
'使用场景:\n' +
|
|
54
|
+
'- 查找 QA 文档、架构说明、设计决策等非代码知识\n' +
|
|
55
|
+
'- 需要引用具体文档原文回答时\n' +
|
|
56
|
+
'- 回答后请基于返回的 contextText 中的原文进行陈述,不要凭空补充\n' +
|
|
57
|
+
'注意:本工具检索文档 chunk,代码符号请使用 search_symbols / recommend_component。',
|
|
58
|
+
inputSchema: queryDocsInput.shape,
|
|
59
|
+
handler: async (input) => {
|
|
60
|
+
// ── 阶段1:语义检索 ─────────────────────────────────────────────
|
|
61
|
+
const hits = await repo.searchSemantic(input.query, {
|
|
62
|
+
limit: input.limit,
|
|
63
|
+
path: input.path,
|
|
64
|
+
});
|
|
65
|
+
if (hits.length === 0) {
|
|
66
|
+
return {
|
|
67
|
+
content: [
|
|
68
|
+
{
|
|
69
|
+
type: 'text',
|
|
70
|
+
text: JSON.stringify({
|
|
71
|
+
contextText: '',
|
|
72
|
+
sources: [],
|
|
73
|
+
hitCount: 0,
|
|
74
|
+
totalChunks: 0,
|
|
75
|
+
truncated: false,
|
|
76
|
+
message: '未找到相关文档片段,请尝试调整查询或确认文档已建立索引。',
|
|
77
|
+
}),
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
// ── 阶段2:邻块扩展 + 去重 + 预算截断 + 文本渲染 ───────────────
|
|
83
|
+
const assembled = await assembler.assemble(hits, {
|
|
84
|
+
maxChars: input.maxChars,
|
|
85
|
+
adjacentRadius: input.adjacentRadius,
|
|
86
|
+
maxChunks: input.maxChunks,
|
|
87
|
+
});
|
|
88
|
+
// sources 供调用方引用来源,避免 LLM 伪造引用。
|
|
89
|
+
const sources = assembled.chunks.map((chunk) => ({
|
|
90
|
+
path: chunk.path,
|
|
91
|
+
title: chunk.title,
|
|
92
|
+
chunkIndex: chunk.chunkIndex,
|
|
93
|
+
chunkCount: chunk.chunkCount,
|
|
94
|
+
similarity: chunk.similarity ?? null,
|
|
95
|
+
summary: chunk.summary ?? null,
|
|
96
|
+
}));
|
|
97
|
+
return {
|
|
98
|
+
content: [
|
|
99
|
+
{
|
|
100
|
+
type: 'text',
|
|
101
|
+
text: JSON.stringify({
|
|
102
|
+
contextText: assembled.contextText,
|
|
103
|
+
sources,
|
|
104
|
+
hitCount: assembled.hitCount,
|
|
105
|
+
totalChunks: assembled.totalChunks,
|
|
106
|
+
truncated: assembled.truncated,
|
|
107
|
+
}),
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
};
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
@@ -1,4 +1,66 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
function formatCallers(callers) {
|
|
3
|
+
if (!callers.length)
|
|
4
|
+
return '新增';
|
|
5
|
+
return callers.map((caller) => `${caller.name}(${caller.path})`).join('; ');
|
|
6
|
+
}
|
|
7
|
+
function formatSideEffects(sideEffects) {
|
|
8
|
+
return sideEffects.length ? sideEffects.join('/') : '无';
|
|
9
|
+
}
|
|
10
|
+
function formatHasResult(result) {
|
|
11
|
+
const recommended = result.recommended;
|
|
12
|
+
if (!recommended)
|
|
13
|
+
return '';
|
|
14
|
+
const reasons = [];
|
|
15
|
+
if (recommended.matchedProps.length) {
|
|
16
|
+
reasons.push(`匹配到必需 props:${recommended.matchedProps.join(', ')}`);
|
|
17
|
+
}
|
|
18
|
+
if (recommended.matchedHooks.length) {
|
|
19
|
+
reasons.push(`匹配到必需 hooks:${recommended.matchedHooks.join(', ')}`);
|
|
20
|
+
}
|
|
21
|
+
if (reasons.length < 2) {
|
|
22
|
+
reasons.push('综合语义、结构字段和可复用性排序后为首选。');
|
|
23
|
+
}
|
|
24
|
+
const alternatives = result.alternatives.length
|
|
25
|
+
? result.alternatives
|
|
26
|
+
.map((item) => `${item.name}(${formatSideEffects(item.sideEffects)})`)
|
|
27
|
+
.join('; ')
|
|
28
|
+
: '无';
|
|
29
|
+
return `首选:${recommended.name} — ${recommended.path}
|
|
30
|
+
symbolId:${recommended.id}
|
|
31
|
+
使用范围:${formatCallers(recommended.callers)}
|
|
32
|
+
副作用:${formatSideEffects(recommended.sideEffects)}
|
|
33
|
+
理由:
|
|
34
|
+
1. ${reasons[0] ?? '匹配度最高,适合直接复用。'}
|
|
35
|
+
2. ${reasons[1] ?? 'API 形态与需求一致,接入成本低。'}
|
|
36
|
+
其他候选:${alternatives}
|
|
37
|
+
用法提示:
|
|
38
|
+
\`\`\`tsx
|
|
39
|
+
<${recommended.name} value={value} onChange={handleChange} />
|
|
40
|
+
\`\`\`
|
|
41
|
+
是否采纳(**请在聊天框输入 1 或 2**):
|
|
42
|
+
1️⃣ 采纳推荐 — 自动调用 inc_usage 记录使用
|
|
43
|
+
2️⃣ 取消`;
|
|
44
|
+
}
|
|
45
|
+
function formatNoResult() {
|
|
46
|
+
return `首选:未找到已有实现
|
|
47
|
+
使用范围:无
|
|
48
|
+
副作用:无
|
|
49
|
+
理由:
|
|
50
|
+
1. 当前索引中没有满足条件的组件(例如必须包含 onChange)
|
|
51
|
+
2. 已尝试可用检索方式,仍无可用候选
|
|
52
|
+
其他候选:无
|
|
53
|
+
用法提示:
|
|
54
|
+
\`\`\`tsx
|
|
55
|
+
// 可先创建一个受控 Input 组件,至少暴露 value + onChange
|
|
56
|
+
\`\`\`
|
|
57
|
+
是否采纳(**请在聊天框输入 1 或 2**):
|
|
58
|
+
1️⃣ 新建最小可复用组件
|
|
59
|
+
2️⃣ 取消`;
|
|
60
|
+
}
|
|
61
|
+
function formatReply(result) {
|
|
62
|
+
return result.recommended ? formatHasResult(result) : formatNoResult();
|
|
63
|
+
}
|
|
2
64
|
export const recommendComponentInput = z.object({
|
|
3
65
|
query: z.string().min(1),
|
|
4
66
|
requiredProps: z.array(z.string().min(1)).optional(),
|
|
@@ -10,18 +72,32 @@ export const recommendComponentInput = z.object({
|
|
|
10
72
|
export function createRecommendComponentTool(recommendationService) {
|
|
11
73
|
return {
|
|
12
74
|
name: 'recommend_component',
|
|
13
|
-
description: '
|
|
75
|
+
description: '【降级链第一步,唯一首选工具】当用户询问有没有可复用的组件/函数/util,或需要找仓库中现有实现时,必须先调用本工具。\n' +
|
|
76
|
+
'本工具会自动完成候选搜索、结构过滤和首选推荐,无需再调其他搜索工具。\n' +
|
|
77
|
+
'⚠️ 输出约束(必须严格遵守):\n' +
|
|
78
|
+
'- recommended != null:立即将工具返回的文本原样输出给用户,完全停止,不得调用任何其他工具,不得改写为散文或追加说明。\n' +
|
|
79
|
+
'- recommended = null:进入降级链第二步,调用 search_symbols(semantic=true)。\n' +
|
|
80
|
+
'- 任何情况下禁止调用 grep/read file/file search 作为本工具的后续动作。',
|
|
14
81
|
inputSchema: recommendComponentInput.shape,
|
|
15
82
|
handler: async (input) => {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
83
|
+
console.error('[code-intelligence-mcp] tool.recommend_component.called query=%s requiredProps=%s requiredHooks=%s category=%s semantic=%s limit=%s', input.query, JSON.stringify(input.requiredProps ?? []), JSON.stringify(input.requiredHooks ?? []), input.category ?? '', String(input.semantic ?? true), String(input.limit ?? 5));
|
|
84
|
+
try {
|
|
85
|
+
const result = await recommendationService.recommendComponent(input);
|
|
86
|
+
console.error('[code-intelligence-mcp] tool.recommend_component.done recommended=%s alternatives=%s queriedBy=%s', result.recommended ? 'yes' : 'no', String(result.alternatives.length), result.queriedBy);
|
|
87
|
+
const formattedReply = formatReply(result);
|
|
88
|
+
return {
|
|
89
|
+
content: [
|
|
90
|
+
{
|
|
91
|
+
type: 'text',
|
|
92
|
+
text: formattedReply,
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
console.error('[code-intelligence-mcp] tool.recommend_component.error', error);
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
25
101
|
},
|
|
26
102
|
};
|
|
27
103
|
}
|
|
@@ -15,7 +15,8 @@ export function createSearchByStructureTool(repository) {
|
|
|
15
15
|
name: 'search_by_structure',
|
|
16
16
|
description: '按代码块的结构字段(props/params/hooks)检索,适合已知接口形态时使用。\n' +
|
|
17
17
|
'示例:需要一个接受 value、onChange、error 三个 prop 的输入组件 → fields: ["value", "onChange", "error"], type: "component"\n' +
|
|
18
|
-
'与 search_symbols 配合:先语义检索候选,再用本工具做 API
|
|
18
|
+
'与 search_symbols 配合:先语义检索候选,再用本工具做 API 结构过滤以精确匹配。\n' +
|
|
19
|
+
'约束:当用户是在问“有没有可复用组件/帮我找组件”时,默认不要调用本工具;仅在用户明确要求二次验证时使用。',
|
|
19
20
|
inputSchema: searchByStructureInput.shape,
|
|
20
21
|
handler: async (input) => {
|
|
21
22
|
const rows = await repository.searchByStructure(input.fields, {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { rankSemanticHits, rankSymbols } from '../services/ranking.js';
|
|
3
|
+
import { isReusableCandidate } from '../services/recommendationService.js';
|
|
4
|
+
import { SEARCH_SCORE_THRESHOLD, SEARCH_TOP_K } from '../config/tuning.js';
|
|
3
5
|
export const searchSymbolsInput = z.object({
|
|
4
6
|
query: z.string().min(1),
|
|
5
7
|
type: z
|
|
@@ -10,16 +12,35 @@ export const searchSymbolsInput = z.object({
|
|
|
10
12
|
semantic: z.boolean().optional().default(false),
|
|
11
13
|
limit: z.number().int().min(1).max(100).optional().default(20),
|
|
12
14
|
});
|
|
13
|
-
const SCORE_THRESHOLD_FOR_FINAL =
|
|
14
|
-
const TOP_K_FOR_FINAL_RESULTS =
|
|
15
|
+
const SCORE_THRESHOLD_FOR_FINAL = SEARCH_SCORE_THRESHOLD; // 综合排序分阈値(语义相似度占50%权重,原始0.5相似度≈综合60.35起)
|
|
16
|
+
const TOP_K_FOR_FINAL_RESULTS = SEARCH_TOP_K; // 结果上限,返回相似度高的,保证数据质量
|
|
17
|
+
function toRankedResult(item) {
|
|
18
|
+
return {
|
|
19
|
+
id: item.symbol.id,
|
|
20
|
+
name: item.symbol.name,
|
|
21
|
+
type: item.symbol.type,
|
|
22
|
+
path: item.symbol.path,
|
|
23
|
+
description: item.symbol.description,
|
|
24
|
+
usageCount: item.symbol.usageCount,
|
|
25
|
+
score: item.score,
|
|
26
|
+
reason: item.reason.summary,
|
|
27
|
+
reasonDetail: item.reason,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function filterReusableSymbols(items) {
|
|
31
|
+
return items.filter((item) => isReusableCandidate(item.symbol));
|
|
32
|
+
}
|
|
15
33
|
export function createSearchSymbolsTool(repository) {
|
|
16
34
|
return {
|
|
17
35
|
name: 'search_symbols',
|
|
18
|
-
description: '
|
|
36
|
+
description: '【降级链第二步,非首选工具】搜索项目中已有的可复用代码块(函数、组件、Hook、类型等)。\n' +
|
|
37
|
+
'⚠️ 调用顺序约束:对于"帮我找 X 组件/util/函数"类问题,第一步必须是 recommend_component,不得跳过直接调用本工具。\n' +
|
|
38
|
+
'本工具仅在 recommend_component 返回 null(无推荐)后才允许调用,作为第二级降级检索。\n' +
|
|
19
39
|
'- 有明确名称时(如 "useDebounce"):semantic=false(默认),直接关键词检索\n' +
|
|
20
|
-
'- 描述功能意图时(如 "防抖"、"处理表单提交"):semantic=true
|
|
21
|
-
'- 不确定 type
|
|
22
|
-
'-
|
|
40
|
+
'- 描述功能意图时(如 "防抖"、"处理表单提交"):semantic=true,进行语义检索\n' +
|
|
41
|
+
'- 不确定 type 时省略该参数\n' +
|
|
42
|
+
'- 本工具返回结果后,必须立即按固定模板输出,停止所有后续工具调用。\n' +
|
|
43
|
+
'- 本工具返回空结果后,输出无结果模板,完全停止,禁止 grep/read file/文件系统兜底。',
|
|
23
44
|
inputSchema: searchSymbolsInput.shape,
|
|
24
45
|
handler: async (input) => {
|
|
25
46
|
if (input.semantic) {
|
|
@@ -27,9 +48,34 @@ export function createSearchSymbolsTool(repository) {
|
|
|
27
48
|
type: input.type,
|
|
28
49
|
limit: input.limit,
|
|
29
50
|
});
|
|
30
|
-
const
|
|
51
|
+
const reusableHits = filterReusableSymbols(hits);
|
|
52
|
+
if (reusableHits.length === 0) {
|
|
53
|
+
const keywordRows = (await repository.search(input.query, input.type)).filter((row) => isReusableCandidate(row));
|
|
54
|
+
const fallbackRows = input.ranked
|
|
55
|
+
? rankSymbols(input.query, keywordRows)
|
|
56
|
+
.map(toRankedResult)
|
|
57
|
+
.filter((x) => x.score >= SCORE_THRESHOLD_FOR_FINAL)
|
|
58
|
+
.slice(0, TOP_K_FOR_FINAL_RESULTS)
|
|
59
|
+
: keywordRows.map((r) => ({
|
|
60
|
+
id: r.id,
|
|
61
|
+
name: r.name,
|
|
62
|
+
type: r.type,
|
|
63
|
+
path: r.path,
|
|
64
|
+
description: r.description,
|
|
65
|
+
usageCount: r.usageCount,
|
|
66
|
+
}));
|
|
67
|
+
return {
|
|
68
|
+
content: [
|
|
69
|
+
{
|
|
70
|
+
type: 'text',
|
|
71
|
+
text: JSON.stringify(fallbackRows, null, 2),
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
const simById = new Map(reusableHits.map((h) => [h.symbol.id, h.similarity]));
|
|
31
77
|
const resultRows = input.ranked
|
|
32
|
-
? rankSemanticHits(
|
|
78
|
+
? rankSemanticHits(reusableHits, input.query)
|
|
33
79
|
.map((item) => ({
|
|
34
80
|
id: item.symbol.id,
|
|
35
81
|
name: item.symbol.name,
|
|
@@ -44,7 +90,7 @@ export function createSearchSymbolsTool(repository) {
|
|
|
44
90
|
}))
|
|
45
91
|
.filter((x) => x.score >= SCORE_THRESHOLD_FOR_FINAL) // 基于综合排序分过滤,保留 usage/recency 高的结果
|
|
46
92
|
.slice(0, TOP_K_FOR_FINAL_RESULTS)
|
|
47
|
-
:
|
|
93
|
+
: reusableHits.map((h) => ({
|
|
48
94
|
id: h.symbol.id,
|
|
49
95
|
name: h.symbol.name,
|
|
50
96
|
type: h.symbol.type,
|
|
@@ -62,20 +108,10 @@ export function createSearchSymbolsTool(repository) {
|
|
|
62
108
|
],
|
|
63
109
|
};
|
|
64
110
|
}
|
|
65
|
-
const rows = await repository.search(input.query, input.type);
|
|
111
|
+
const rows = (await repository.search(input.query, input.type)).filter((row) => isReusableCandidate(row));
|
|
66
112
|
const resultRows = input.ranked
|
|
67
113
|
? rankSymbols(input.query, rows)
|
|
68
|
-
.map(
|
|
69
|
-
id: item.symbol.id,
|
|
70
|
-
name: item.symbol.name,
|
|
71
|
-
type: item.symbol.type,
|
|
72
|
-
path: item.symbol.path,
|
|
73
|
-
description: item.symbol.description,
|
|
74
|
-
usageCount: item.symbol.usageCount,
|
|
75
|
-
score: item.score,
|
|
76
|
-
reason: item.reason.summary,
|
|
77
|
-
reasonDetail: item.reason,
|
|
78
|
-
}))
|
|
114
|
+
.map(toRankedResult)
|
|
79
115
|
.filter((x) => x.score >= SCORE_THRESHOLD_FOR_FINAL) // 基于综合排序分过滤
|
|
80
116
|
.slice(0, TOP_K_FOR_FINAL_RESULTS)
|
|
81
117
|
: rows.map((r) => ({
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -67,7 +67,6 @@ async function processEmbedJob(job, pool) {
|
|
|
67
67
|
const meta = typeof row.meta === 'string' ? JSON.parse(row.meta) : (row.meta ?? {});
|
|
68
68
|
const rowObj = { ...row, meta };
|
|
69
69
|
console.error(`[worker] 🔄 embedding [${ts()}] table=${table} hash=${shortHash}… ${row.path}:${row.name}`);
|
|
70
|
-
// 与 reindex 保持一致:优先用 content(语义模板),降级用 indexedRowToEmbedText
|
|
71
70
|
const doc = row.content ?? indexedRowToEmbedText(rowObj);
|
|
72
71
|
const vectors = await embedClient.embed([doc]);
|
|
73
72
|
vector = vectors[0];
|