@kk-irving/knowledge-mcp-server 1.0.0 → 1.0.1
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.
|
@@ -90,17 +90,41 @@ async function listAllSpaces() {
|
|
|
90
90
|
function sleep(ms) {
|
|
91
91
|
return new Promise((r) => setTimeout(r, ms));
|
|
92
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* 把 ISO 时间戳(含 T、Z)规范化为 CQL 可接受的 `YYYY-MM-DD HH:mm`。
|
|
95
|
+
* Atlassian CQL parser 要求 day-or-minute 精度,不支持秒、毫秒、Z 后缀。
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* "2026-06-22T10:30:45.123Z" → "2026-06-22 10:30"
|
|
99
|
+
* "2026-06-22" → "2026-06-22 00:00"
|
|
100
|
+
*/
|
|
101
|
+
function toCqlDateTime(s) {
|
|
102
|
+
if (!s)
|
|
103
|
+
return "";
|
|
104
|
+
const m = s.match(/^(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2}))?/);
|
|
105
|
+
if (!m)
|
|
106
|
+
return s;
|
|
107
|
+
const [, y, mo, d, hh = "00", mm = "00"] = m;
|
|
108
|
+
return `${y}-${mo}-${d} ${hh}:${mm}`;
|
|
109
|
+
}
|
|
93
110
|
/**
|
|
94
111
|
* 全量/增量同步 Confluence pages。
|
|
95
112
|
*
|
|
96
113
|
* @param args.space 仅同步指定空间(可重复传入多个空间用 CSV,如 "TVENG,DOC")
|
|
97
|
-
* @param args.limit 最大同步条数(防一次性拉爆)。默认 1000
|
|
98
|
-
* @param args.since 仅同步 lastmodified > since 的页面(YYYY-MM-DD HH:
|
|
114
|
+
* @param args.limit 最大同步条数(防一次性拉爆)。默认 1000;传 0 / 负数 = 不限
|
|
115
|
+
* @param args.since 仅同步 lastmodified > since 的页面(YYYY-MM-DD HH:mm)
|
|
99
116
|
*/
|
|
100
117
|
export async function syncConfluence(args = {}) {
|
|
101
118
|
const db = getDb();
|
|
102
|
-
|
|
103
|
-
const
|
|
119
|
+
// limit ≤ 0 → 无限拉
|
|
120
|
+
const userLimit = args.limit;
|
|
121
|
+
const limit = userLimit == null
|
|
122
|
+
? 1000
|
|
123
|
+
: userLimit <= 0
|
|
124
|
+
? Number.MAX_SAFE_INTEGER
|
|
125
|
+
: userLimit;
|
|
126
|
+
const stateSinceRaw = args.since ?? getSyncState("confluence", "last_full_sync") ?? "";
|
|
127
|
+
const stateSince = stateSinceRaw ? toCqlDateTime(stateSinceRaw) : "";
|
|
104
128
|
// 决定要同步的空间集合
|
|
105
129
|
let spaces;
|
|
106
130
|
if (args.space) {
|
|
@@ -118,15 +142,17 @@ export async function syncConfluence(args = {}) {
|
|
|
118
142
|
}
|
|
119
143
|
let fetched = 0;
|
|
120
144
|
let upserted = 0;
|
|
145
|
+
let maxUpdated = stateSinceRaw;
|
|
121
146
|
const pageSize = 100;
|
|
122
147
|
for (const spaceKey of spaces) {
|
|
123
148
|
let start = 0;
|
|
149
|
+
let pageInSpace = 0;
|
|
124
150
|
while (fetched < limit) {
|
|
125
151
|
const remaining = limit - fetched;
|
|
126
152
|
const n = Math.min(pageSize, remaining);
|
|
127
153
|
let resp;
|
|
128
154
|
if (stateSince) {
|
|
129
|
-
// 走 CQL
|
|
155
|
+
// 走 CQL 增量(lastmodified 用 day/minute 精度格式)
|
|
130
156
|
const cql = `space.key = "${spaceKey}" AND type = page AND lastmodified > "${stateSince}"`;
|
|
131
157
|
resp = await confluenceGet("/rest/api/content/search", {
|
|
132
158
|
cql,
|
|
@@ -161,6 +187,13 @@ export async function syncConfluence(args = {}) {
|
|
|
161
187
|
fetched += items.length;
|
|
162
188
|
upserted += items.length;
|
|
163
189
|
start += items.length;
|
|
190
|
+
pageInSpace++;
|
|
191
|
+
// 跟踪最大 updated(用于 watermark)
|
|
192
|
+
for (const r of rows) {
|
|
193
|
+
if (r.updated && r.updated > maxUpdated)
|
|
194
|
+
maxUpdated = r.updated;
|
|
195
|
+
}
|
|
196
|
+
process.stderr.write(`[confluence-sync] space=${spaceKey} page=${pageInSpace}, got=${items.length}, total=${fetched}\n`);
|
|
164
197
|
if (items.length < n)
|
|
165
198
|
break;
|
|
166
199
|
await sleep(CONFLUENCE_REQUEST_DELAY_MS);
|
|
@@ -168,7 +201,8 @@ export async function syncConfluence(args = {}) {
|
|
|
168
201
|
if (fetched >= limit)
|
|
169
202
|
break;
|
|
170
203
|
}
|
|
171
|
-
|
|
204
|
+
// watermark:用已拉取的最大 updated 时间的 CQL 格式(YYYY-MM-DD HH:mm)
|
|
205
|
+
const watermark = maxUpdated ? toCqlDateTime(maxUpdated) : stateSince || toCqlDateTime(new Date().toISOString());
|
|
172
206
|
setSyncState("confluence", "last_full_sync", watermark);
|
|
173
207
|
return {
|
|
174
208
|
source: "confluence",
|
|
@@ -106,22 +106,48 @@ ON CONFLICT(change_id) DO UPDATE SET
|
|
|
106
106
|
`;
|
|
107
107
|
function buildQuery(args) {
|
|
108
108
|
const parts = [];
|
|
109
|
-
if (args.query)
|
|
109
|
+
if (args.query) {
|
|
110
|
+
// 用户传了自定义 query,完全用它,不再叠加默认作用域
|
|
110
111
|
parts.push(args.query);
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
// 没传自定义 query → 总是加默认作用域过滤(避免拉到所有人的 changes)
|
|
115
|
+
// 首次(无 since)时叠加 -age:365d 限制时间窗口;增量(有 since)时不需要 -age
|
|
116
|
+
// 用户可通过 KNOWLEDGE_GERRIT_SYNC_QUERY 环境变量自定义默认作用域
|
|
117
|
+
const customDefault = (process.env.KNOWLEDGE_GERRIT_SYNC_QUERY ?? "").trim();
|
|
118
|
+
if (customDefault) {
|
|
119
|
+
parts.push(customDefault);
|
|
120
|
+
}
|
|
121
|
+
else if (args.since) {
|
|
122
|
+
parts.push("(owner:self OR reviewer:self)");
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
parts.push("(owner:self OR reviewer:self) -age:365d");
|
|
126
|
+
}
|
|
127
|
+
}
|
|
111
128
|
if (args.project)
|
|
112
129
|
parts.push(`project:${args.project}`);
|
|
113
130
|
if (args.since)
|
|
114
131
|
parts.push(`after:"${args.since}"`);
|
|
115
|
-
if (parts.length === 0)
|
|
116
|
-
parts.push("status:open OR -status:open"); // all
|
|
117
132
|
return parts.join(" ");
|
|
118
133
|
}
|
|
119
134
|
/**
|
|
120
135
|
* 全量/增量同步 Gerrit changes。
|
|
136
|
+
*
|
|
137
|
+
* @param args.query 自定义 query;缺省走 `KNOWLEDGE_GERRIT_SYNC_QUERY` 或 `(owner:self OR reviewer:self) -age:365d`
|
|
138
|
+
* @param args.project 仅同步特定 project
|
|
139
|
+
* @param args.since 仅同步 after:since 的 changes(YYYY-MM-DD)
|
|
140
|
+
* @param args.limit 最大同步条数(防一次性拉爆)。默认 1000;传 0 / 负数 = 不限
|
|
121
141
|
*/
|
|
122
142
|
export async function syncGerrit(args = {}) {
|
|
123
143
|
const db = getDb();
|
|
124
|
-
|
|
144
|
+
// limit ≤ 0 → 无限拉
|
|
145
|
+
const userLimit = args.limit;
|
|
146
|
+
const limit = userLimit == null
|
|
147
|
+
? 1000
|
|
148
|
+
: userLimit <= 0
|
|
149
|
+
? Number.MAX_SAFE_INTEGER
|
|
150
|
+
: userLimit;
|
|
125
151
|
const stateSince = args.since ?? getSyncState("gerrit", "last_full_sync_date") ?? "";
|
|
126
152
|
const finalQuery = buildQuery({
|
|
127
153
|
since: stateSince,
|
|
@@ -138,6 +164,7 @@ export async function syncGerrit(args = {}) {
|
|
|
138
164
|
let offset = 0;
|
|
139
165
|
let fetched = 0;
|
|
140
166
|
let upserted = 0;
|
|
167
|
+
let maxUpdated = stateSince;
|
|
141
168
|
const batchSize = 200;
|
|
142
169
|
while (fetched < limit) {
|
|
143
170
|
const remaining = limit - fetched;
|
|
@@ -169,12 +196,21 @@ export async function syncGerrit(args = {}) {
|
|
|
169
196
|
fetched += changes.length;
|
|
170
197
|
upserted += changes.length;
|
|
171
198
|
offset += changes.length;
|
|
199
|
+
// 跟踪最大 updated(Gerrit 返回 ISO 格式:"2026-06-22 10:30:45.123000000")
|
|
200
|
+
for (const r of rows) {
|
|
201
|
+
if (r.updated && r.updated > maxUpdated)
|
|
202
|
+
maxUpdated = r.updated;
|
|
203
|
+
}
|
|
204
|
+
process.stderr.write(`[gerrit-sync] page offset=${offset - changes.length}, got=${changes.length}, total=${fetched}\n`);
|
|
172
205
|
if (changes.length < n)
|
|
173
206
|
break;
|
|
174
207
|
if (!changes[changes.length - 1]?._more_changes)
|
|
175
208
|
break;
|
|
176
209
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
210
|
+
// watermark:用已拉取的最大 updated 的 day-only 形式(Gerrit `after:` 接受 YYYY-MM-DD)
|
|
211
|
+
const watermark = maxUpdated
|
|
212
|
+
? maxUpdated.slice(0, 10)
|
|
213
|
+
: stateSince || new Date().toISOString().slice(0, 10);
|
|
214
|
+
setSyncState("gerrit", "last_full_sync_date", watermark);
|
|
215
|
+
return { source: "gerrit", fetched, upserted, watermark, query: finalQuery };
|
|
180
216
|
}
|
|
@@ -48,12 +48,18 @@ ON CONFLICT(id) DO UPDATE SET
|
|
|
48
48
|
* 全量/增量同步 Zmind issues。
|
|
49
49
|
*
|
|
50
50
|
* @param args.since 仅同步 updated_on >= since 的 issues(YYYY-MM-DD)。未传则用 sync_state 水位。
|
|
51
|
-
* @param args.limit 最大同步条数(防一次性拉爆)。默认 1000
|
|
51
|
+
* @param args.limit 最大同步条数(防一次性拉爆)。默认 1000;传 0 / 负数 = 不限(拉到 API 返回空)
|
|
52
52
|
* @param args.statusId 状态过滤;默认 "*" 全部
|
|
53
53
|
*/
|
|
54
54
|
export async function syncZmind(args = {}) {
|
|
55
55
|
const db = getDb();
|
|
56
|
-
|
|
56
|
+
// limit ≤ 0 → 无限拉(直到 API 返回空)
|
|
57
|
+
const userLimit = args.limit;
|
|
58
|
+
const limit = userLimit == null
|
|
59
|
+
? 1000
|
|
60
|
+
: userLimit <= 0
|
|
61
|
+
? Number.MAX_SAFE_INTEGER
|
|
62
|
+
: userLimit;
|
|
57
63
|
const stateSince = args.since ?? getSyncState("zmind", "last_full_sync") ?? "";
|
|
58
64
|
const upsert = db.prepare(UPSERT_SQL);
|
|
59
65
|
function upsertMany(rows) {
|
|
@@ -65,6 +71,7 @@ export async function syncZmind(args = {}) {
|
|
|
65
71
|
let offset = 0;
|
|
66
72
|
let fetched = 0;
|
|
67
73
|
let upserted = 0;
|
|
74
|
+
let maxUpdatedOn = stateSince; // 用于设置 watermark = 已拉取的最大 updated_on
|
|
68
75
|
const pageSize = 100;
|
|
69
76
|
while (fetched < limit) {
|
|
70
77
|
const remaining = limit - fetched;
|
|
@@ -75,8 +82,9 @@ export async function syncZmind(args = {}) {
|
|
|
75
82
|
offset: String(offset),
|
|
76
83
|
limit: String(batch),
|
|
77
84
|
};
|
|
85
|
+
// Redmine 接受 `>=YYYY-MM-DD` 格式;ISO 时间串会解析失败 → 强制截到 day
|
|
78
86
|
if (stateSince)
|
|
79
|
-
params.updated_on = `>=${stateSince}`;
|
|
87
|
+
params.updated_on = `>=${stateSince.slice(0, 10)}`;
|
|
80
88
|
const data = await zmindGet("/issues.json", params);
|
|
81
89
|
const issues = data.issues ?? [];
|
|
82
90
|
if (issues.length === 0)
|
|
@@ -97,10 +105,21 @@ export async function syncZmind(args = {}) {
|
|
|
97
105
|
fetched += issues.length;
|
|
98
106
|
upserted += issues.length;
|
|
99
107
|
offset += issues.length;
|
|
108
|
+
// 跟踪最大 updated_on(Zmind 返回 ISO 格式,截到 day 即可比较)
|
|
109
|
+
for (const r of rows) {
|
|
110
|
+
if (r.updated_on && r.updated_on > maxUpdatedOn) {
|
|
111
|
+
maxUpdatedOn = r.updated_on;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
process.stderr.write(`[zmind-sync] page offset=${offset - issues.length}, got=${issues.length}, total=${fetched}\n`);
|
|
100
115
|
if (issues.length < batch)
|
|
101
116
|
break;
|
|
102
117
|
}
|
|
103
|
-
|
|
118
|
+
// watermark:用已拉取的最大 updated_on 的 day-only 形式(Redmine 接受 YYYY-MM-DD)
|
|
119
|
+
// 若没拉到任何数据,保留 stateSince(不前推)
|
|
120
|
+
const watermark = maxUpdatedOn
|
|
121
|
+
? maxUpdatedOn.slice(0, 10)
|
|
122
|
+
: stateSince || new Date().toISOString().slice(0, 10);
|
|
104
123
|
setSyncState("zmind", "last_full_sync", watermark);
|
|
105
124
|
return { source: "zmind", fetched, upserted, watermark };
|
|
106
125
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kk-irving/knowledge-mcp-server",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "Local knowledge base MCP Server: vector (BGE-small-zh ONNX) + FTS5 hybrid retrieval across Zmind PR / Gerrit changes / Confluence pages",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Local knowledge base MCP Server: vector (BGE-small-zh ONNX) + FTS5 hybrid retrieval across Zmind PR / Gerrit changes / Confluence pages; v1.0.1 fixes sync_gerrit default query (was 0-hit due to OR precedence)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"knowledge-mcp-server": "dist/index.js"
|