@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:MM
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
- const limit = Math.max(1, Math.min(50000, args.limit ?? 1000));
103
- const stateSince = args.since ?? getSyncState("confluence", "last_full_sync") ?? "";
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
- const watermark = new Date().toISOString();
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
- const limit = Math.max(1, Math.min(50000, args.limit ?? 1000));
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
- const today = new Date().toISOString().slice(0, 10);
178
- setSyncState("gerrit", "last_full_sync_date", today);
179
- return { source: "gerrit", fetched, upserted, watermark: today, query: finalQuery };
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
- const limit = Math.max(1, Math.min(50000, args.limit ?? 1000));
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
- const watermark = new Date().toISOString();
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.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"