@andyqiu/teamkit 0.1.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.
- package/LICENSE +21 -0
- package/README.md +73 -0
- package/agents/teamkit-git.md +42 -0
- package/bin/.gitkeep +0 -0
- package/bin/teamkit-doctor.mjs +320 -0
- package/bin/teamkit-upgrade.mjs +93 -0
- package/bin/teamkit.mjs +115 -0
- package/context-templates/.gitkeep +0 -0
- package/context-templates/git-instructions.md +69 -0
- package/context-templates/kh-instructions.md +109 -0
- package/dist/index.cjs +1773 -0
- package/install.sh +302 -0
- package/package.json +45 -0
- package/scripts/adr-check.mjs +368 -0
- package/scripts/adr-index-sync.mjs +133 -0
- package/scripts/capability-detect.mjs +231 -0
- package/scripts/merge-agents-md.mjs +201 -0
- package/scripts/publish.sh +184 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1773 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
function __accessProp(key) {
|
|
8
|
+
return this[key];
|
|
9
|
+
}
|
|
10
|
+
var __toESMCache_node;
|
|
11
|
+
var __toESMCache_esm;
|
|
12
|
+
var __toESM = (mod, isNodeMode, target) => {
|
|
13
|
+
var canCache = mod != null && typeof mod === "object";
|
|
14
|
+
if (canCache) {
|
|
15
|
+
var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
|
|
16
|
+
var cached = cache.get(mod);
|
|
17
|
+
if (cached)
|
|
18
|
+
return cached;
|
|
19
|
+
}
|
|
20
|
+
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
21
|
+
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
22
|
+
for (let key of __getOwnPropNames(mod))
|
|
23
|
+
if (!__hasOwnProp.call(to, key))
|
|
24
|
+
__defProp(to, key, {
|
|
25
|
+
get: __accessProp.bind(mod, key),
|
|
26
|
+
enumerable: true
|
|
27
|
+
});
|
|
28
|
+
if (canCache)
|
|
29
|
+
cache.set(mod, to);
|
|
30
|
+
return to;
|
|
31
|
+
};
|
|
32
|
+
var __toCommonJS = (from) => {
|
|
33
|
+
var entry = (__moduleCache ??= new WeakMap).get(from), desc;
|
|
34
|
+
if (entry)
|
|
35
|
+
return entry;
|
|
36
|
+
entry = __defProp({}, "__esModule", { value: true });
|
|
37
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
38
|
+
for (var key of __getOwnPropNames(from))
|
|
39
|
+
if (!__hasOwnProp.call(entry, key))
|
|
40
|
+
__defProp(entry, key, {
|
|
41
|
+
get: __accessProp.bind(from, key),
|
|
42
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
__moduleCache.set(from, entry);
|
|
46
|
+
return entry;
|
|
47
|
+
};
|
|
48
|
+
var __moduleCache;
|
|
49
|
+
var __returnValue = (v) => v;
|
|
50
|
+
function __exportSetter(name, newValue) {
|
|
51
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
52
|
+
}
|
|
53
|
+
var __export = (target, all) => {
|
|
54
|
+
for (var name in all)
|
|
55
|
+
__defProp(target, name, {
|
|
56
|
+
get: all[name],
|
|
57
|
+
enumerable: true,
|
|
58
|
+
configurable: true,
|
|
59
|
+
set: __exportSetter.bind(all, name)
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// src/index.ts
|
|
64
|
+
var exports_src = {};
|
|
65
|
+
__export(exports_src, {
|
|
66
|
+
teamkitServer: () => teamkitServer,
|
|
67
|
+
teamkitDevServer: () => teamkitDevServer,
|
|
68
|
+
createTeamkitServer: () => createTeamkitServer
|
|
69
|
+
});
|
|
70
|
+
module.exports = __toCommonJS(exports_src);
|
|
71
|
+
|
|
72
|
+
// lib/dev-isolation.ts
|
|
73
|
+
var fs = __toESM(require("fs"));
|
|
74
|
+
var path = __toESM(require("path"));
|
|
75
|
+
var os = __toESM(require("os"));
|
|
76
|
+
function shouldYieldToLocalPlugin() {
|
|
77
|
+
try {
|
|
78
|
+
const env = process.env.TEAMKIT_DEV;
|
|
79
|
+
if (env === "1" || env === "true" || env === "yes")
|
|
80
|
+
return true;
|
|
81
|
+
let dir = process.cwd();
|
|
82
|
+
for (let i = 0;i < 20; i++) {
|
|
83
|
+
const marker = path.join(dir, ".teamkit", ".dev-marker");
|
|
84
|
+
if (fs.existsSync(marker))
|
|
85
|
+
return true;
|
|
86
|
+
const parent = path.dirname(dir);
|
|
87
|
+
if (parent === dir)
|
|
88
|
+
break;
|
|
89
|
+
dir = parent;
|
|
90
|
+
}
|
|
91
|
+
return false;
|
|
92
|
+
} catch {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function logLifecycle(msg) {
|
|
97
|
+
try {
|
|
98
|
+
const logDir = path.join(os.homedir(), ".cache", "teamkit");
|
|
99
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
100
|
+
const logFile = path.join(logDir, "plugins.log");
|
|
101
|
+
const timestamp = new Date().toISOString();
|
|
102
|
+
fs.appendFileSync(logFile, `[${timestamp}] [teamkit] ${msg}
|
|
103
|
+
`, "utf-8");
|
|
104
|
+
} catch {}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// plugins/compaction-guard.ts
|
|
108
|
+
var KH_INSTRUCTIONS = `# KH 行为约束(teamkit M2 注入)
|
|
109
|
+
|
|
110
|
+
当对话出现以下触发词时,请优先调用 smart_search 查询团队知识库:
|
|
111
|
+
- 以前、之前、历史、为什么、怎么部署、怎么配置
|
|
112
|
+
- 架构、设计、决策、规范、约定
|
|
113
|
+
|
|
114
|
+
## KH 工具使用规范
|
|
115
|
+
- smart_search:查询团队历史经验(bug、架构决策、踩坑记录)
|
|
116
|
+
- save_chat_insight:当前对话有 bug 修复/架构决策/踩坑时自动沉淀
|
|
117
|
+
`;
|
|
118
|
+
async function handleCompaction(ctx) {
|
|
119
|
+
let snapshot = "";
|
|
120
|
+
try {
|
|
121
|
+
snapshot = await ctx.workingMemory?.get?.("teamkit:kh-context") ?? "";
|
|
122
|
+
} catch {}
|
|
123
|
+
const khInstructions = KH_INSTRUCTIONS;
|
|
124
|
+
ctx.output.context.push({
|
|
125
|
+
type: "text",
|
|
126
|
+
text: `<!-- teamkit:kh-context -->
|
|
127
|
+
${khInstructions}
|
|
128
|
+
|
|
129
|
+
## 当前 KH 上下文快照
|
|
130
|
+
${snapshot}`.trim()
|
|
131
|
+
});
|
|
132
|
+
try {
|
|
133
|
+
await ctx.workingMemory?.set?.("teamkit:compaction-guard", JSON.stringify({
|
|
134
|
+
compacted: true,
|
|
135
|
+
timestamp: new Date().toISOString()
|
|
136
|
+
}));
|
|
137
|
+
} catch {}
|
|
138
|
+
}
|
|
139
|
+
function handler() {
|
|
140
|
+
return {
|
|
141
|
+
hooks: {
|
|
142
|
+
"experimental.session.compacting": async (ctx) => {
|
|
143
|
+
try {
|
|
144
|
+
await handleCompaction(ctx);
|
|
145
|
+
} catch (err) {
|
|
146
|
+
console.error("[teamkit:compaction-guard] hook error:", err);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// lib/opencode-plugin-helpers.ts
|
|
154
|
+
var import_node_fs = require("node:fs");
|
|
155
|
+
var import_node_path = require("node:path");
|
|
156
|
+
var import_node_os = require("node:os");
|
|
157
|
+
var LOG_DIR = process.env["TEAMKIT_LOG_DIR"] ?? import_node_path.join(import_node_os.homedir(), ".cache", "teamkit");
|
|
158
|
+
var LOG_FILE = import_node_path.join(LOG_DIR, "plugins.log");
|
|
159
|
+
function safeWriteLog(plugin, payload) {
|
|
160
|
+
try {
|
|
161
|
+
import_node_fs.mkdirSync(LOG_DIR, { recursive: true });
|
|
162
|
+
const line = JSON.stringify({
|
|
163
|
+
ts: new Date().toISOString(),
|
|
164
|
+
plugin,
|
|
165
|
+
...payload
|
|
166
|
+
});
|
|
167
|
+
import_node_fs.appendFileSync(LOG_FILE, line + `
|
|
168
|
+
`, "utf8");
|
|
169
|
+
} catch {}
|
|
170
|
+
}
|
|
171
|
+
function makePluginLogger(plugin) {
|
|
172
|
+
return {
|
|
173
|
+
info: (msg, data) => safeWriteLog(plugin, { level: "info", msg, data }),
|
|
174
|
+
warn: (msg, data) => safeWriteLog(plugin, { level: "warn", msg, data }),
|
|
175
|
+
error: (msg, data) => safeWriteLog(plugin, { level: "error", msg, data }),
|
|
176
|
+
debug: (msg, data) => safeWriteLog(plugin, { level: "debug", msg, data })
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
function extractUserText(output) {
|
|
180
|
+
if (!output || typeof output !== "object")
|
|
181
|
+
return "";
|
|
182
|
+
const parts = output.parts;
|
|
183
|
+
if (!Array.isArray(parts))
|
|
184
|
+
return "";
|
|
185
|
+
const chunks = [];
|
|
186
|
+
for (const p of parts) {
|
|
187
|
+
if (p && typeof p === "object") {
|
|
188
|
+
const part = p;
|
|
189
|
+
if (part.type === "text" && typeof part.text === "string") {
|
|
190
|
+
chunks.push(part.text);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return chunks.join(`
|
|
195
|
+
`).trim();
|
|
196
|
+
}
|
|
197
|
+
async function safeAsync(plugin, hook, fn) {
|
|
198
|
+
try {
|
|
199
|
+
return await fn();
|
|
200
|
+
} catch (err) {
|
|
201
|
+
safeWriteLog(plugin, {
|
|
202
|
+
level: "error",
|
|
203
|
+
hook,
|
|
204
|
+
msg: "hook 异常(已隔离)",
|
|
205
|
+
error: err instanceof Error ? err.message : String(err),
|
|
206
|
+
stack: err instanceof Error ? err.stack?.split(`
|
|
207
|
+
`).slice(0, 5) : undefined
|
|
208
|
+
});
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function logLifecycle2(plugin, phase, extra) {
|
|
213
|
+
safeWriteLog(plugin, {
|
|
214
|
+
lifecycle: phase,
|
|
215
|
+
pid: typeof process !== "undefined" ? process.pid : -1,
|
|
216
|
+
bun: typeof process !== "undefined" && process.versions ? process.versions["bun"] ?? null : null,
|
|
217
|
+
node: typeof process !== "undefined" && process.versions ? process.versions.node : null,
|
|
218
|
+
...extra
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// lib/condenser.ts
|
|
223
|
+
var DEFAULT_CONDENSE = {
|
|
224
|
+
budget: 128000,
|
|
225
|
+
threshold: 0.7,
|
|
226
|
+
target: 0.4,
|
|
227
|
+
keepRecent: 6,
|
|
228
|
+
keepSystem: true,
|
|
229
|
+
charsPerToken: 4
|
|
230
|
+
};
|
|
231
|
+
function estimateTokens(text, charsPerToken = 4) {
|
|
232
|
+
if (!text)
|
|
233
|
+
return 0;
|
|
234
|
+
return Math.max(1, Math.ceil(text.length / charsPerToken));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// lib/trigger-words.ts
|
|
238
|
+
var KH_TRIGGER_WORDS_ZH = [
|
|
239
|
+
"怎么",
|
|
240
|
+
"怎样",
|
|
241
|
+
"如何",
|
|
242
|
+
"为什么",
|
|
243
|
+
"为啥",
|
|
244
|
+
"之前",
|
|
245
|
+
"历史",
|
|
246
|
+
"以前",
|
|
247
|
+
"部署",
|
|
248
|
+
"上线",
|
|
249
|
+
"发布",
|
|
250
|
+
"发版",
|
|
251
|
+
"环境",
|
|
252
|
+
"测试服",
|
|
253
|
+
"正式服",
|
|
254
|
+
"配置",
|
|
255
|
+
"地址",
|
|
256
|
+
"端口",
|
|
257
|
+
"凭证",
|
|
258
|
+
"接入",
|
|
259
|
+
"集成",
|
|
260
|
+
"谁负责",
|
|
261
|
+
"谁写的",
|
|
262
|
+
"报错",
|
|
263
|
+
"报什么错",
|
|
264
|
+
"跑不起来",
|
|
265
|
+
"起不来",
|
|
266
|
+
"挂了",
|
|
267
|
+
"崩了",
|
|
268
|
+
"规范",
|
|
269
|
+
"约定",
|
|
270
|
+
"风格"
|
|
271
|
+
];
|
|
272
|
+
var KH_TRIGGER_WORDS_EN = [
|
|
273
|
+
"how to",
|
|
274
|
+
"how do",
|
|
275
|
+
"how does",
|
|
276
|
+
"why",
|
|
277
|
+
"previously",
|
|
278
|
+
"historically",
|
|
279
|
+
"deploy",
|
|
280
|
+
"release",
|
|
281
|
+
"config",
|
|
282
|
+
"URL",
|
|
283
|
+
"credential",
|
|
284
|
+
"token",
|
|
285
|
+
"API key",
|
|
286
|
+
"api[_-]?key",
|
|
287
|
+
"integrate",
|
|
288
|
+
"integration",
|
|
289
|
+
"who owns",
|
|
290
|
+
"error",
|
|
291
|
+
"failed",
|
|
292
|
+
"convention",
|
|
293
|
+
"standard",
|
|
294
|
+
"style guide"
|
|
295
|
+
];
|
|
296
|
+
function escapeRegex(s) {
|
|
297
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
298
|
+
}
|
|
299
|
+
function buildKhTriggerRegex() {
|
|
300
|
+
const escapedZh = KH_TRIGGER_WORDS_ZH.map(escapeRegex).join("|");
|
|
301
|
+
const enParts = [];
|
|
302
|
+
for (const w of KH_TRIGGER_WORDS_EN) {
|
|
303
|
+
if (w === "api[_-]?key") {
|
|
304
|
+
enParts.push(w);
|
|
305
|
+
} else {
|
|
306
|
+
enParts.push(escapeRegex(w));
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
const escapedEn = enParts.join("|");
|
|
310
|
+
const pattern = `(${escapedZh})|\\b(${escapedEn})\\b`;
|
|
311
|
+
return new RegExp(pattern, "i");
|
|
312
|
+
}
|
|
313
|
+
var KH_TRIGGER_REGEX = buildKhTriggerRegex();
|
|
314
|
+
|
|
315
|
+
// plugins/kh-reminder.ts
|
|
316
|
+
var PLUGIN_NAME = "kh-reminder";
|
|
317
|
+
logLifecycle2(PLUGIN_NAME, "import", {});
|
|
318
|
+
var COOLDOWN_MS = 5 * 60 * 1000;
|
|
319
|
+
var lastTriggerAt = new Map;
|
|
320
|
+
var DEFAULT_THRESHOLD = 0.5;
|
|
321
|
+
var DEFAULT_RECENT_ROUNDS = 5;
|
|
322
|
+
function computeUsageRatio(messages) {
|
|
323
|
+
const cpt = DEFAULT_CONDENSE.charsPerToken;
|
|
324
|
+
const budget = DEFAULT_CONDENSE.budget;
|
|
325
|
+
if (budget <= 0)
|
|
326
|
+
return 0;
|
|
327
|
+
let total = 0;
|
|
328
|
+
for (const m of messages) {
|
|
329
|
+
total += m.tokens ?? estimateTokens(m.content, cpt);
|
|
330
|
+
}
|
|
331
|
+
return total / budget;
|
|
332
|
+
}
|
|
333
|
+
function findTriggerWord(messages) {
|
|
334
|
+
for (let i = messages.length - 1;i >= 0; i--) {
|
|
335
|
+
const m = messages[i];
|
|
336
|
+
if (!m || m.role !== "user")
|
|
337
|
+
continue;
|
|
338
|
+
const text = m.content ?? "";
|
|
339
|
+
const match = KH_TRIGGER_REGEX.exec(text);
|
|
340
|
+
if (match)
|
|
341
|
+
return match[0] ?? null;
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
function hasRecentSmartSearch(messages, recentRounds) {
|
|
347
|
+
const slice = messages.slice(-recentRounds);
|
|
348
|
+
for (const m of slice) {
|
|
349
|
+
const c = m.content ?? "";
|
|
350
|
+
if (/smart_search/i.test(c))
|
|
351
|
+
return true;
|
|
352
|
+
if (m.tag && /smart_search/i.test(m.tag))
|
|
353
|
+
return true;
|
|
354
|
+
}
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
function evaluate(input) {
|
|
358
|
+
const messages = Array.isArray(input.messages) ? input.messages : [];
|
|
359
|
+
const sessionId = input.sessionId ?? "unknown";
|
|
360
|
+
const threshold = input.threshold ?? DEFAULT_THRESHOLD;
|
|
361
|
+
const recentRounds = input.recentRounds ?? DEFAULT_RECENT_ROUNDS;
|
|
362
|
+
const now = (input.nowFn ?? Date.now)();
|
|
363
|
+
if (messages.length === 0) {
|
|
364
|
+
return { triggered: false, sessionId };
|
|
365
|
+
}
|
|
366
|
+
let reason = null;
|
|
367
|
+
const ratio = computeUsageRatio(messages);
|
|
368
|
+
if (ratio >= threshold) {
|
|
369
|
+
reason = { kind: "token-usage", ratio, threshold };
|
|
370
|
+
}
|
|
371
|
+
if (!reason) {
|
|
372
|
+
const word = findTriggerWord(messages);
|
|
373
|
+
if (word) {
|
|
374
|
+
reason = { kind: "trigger-word", word };
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
if (!reason) {
|
|
378
|
+
if (messages.length >= 6 && !hasRecentSmartSearch(messages, recentRounds)) {
|
|
379
|
+
reason = { kind: "no-search-in-recent-rounds", rounds: recentRounds };
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
if (!reason) {
|
|
383
|
+
return { triggered: false, sessionId };
|
|
384
|
+
}
|
|
385
|
+
const last = lastTriggerAt.get(sessionId);
|
|
386
|
+
if (last !== undefined && now - last < COOLDOWN_MS) {
|
|
387
|
+
return { triggered: false, cooldown_skipped: true, reason, sessionId };
|
|
388
|
+
}
|
|
389
|
+
lastTriggerAt.set(sessionId, now);
|
|
390
|
+
return { triggered: true, reason, sessionId };
|
|
391
|
+
}
|
|
392
|
+
function handleObserve(raw, log) {
|
|
393
|
+
const ctx = raw ?? {};
|
|
394
|
+
if (!Array.isArray(ctx.messages))
|
|
395
|
+
return null;
|
|
396
|
+
try {
|
|
397
|
+
const result = evaluate(ctx);
|
|
398
|
+
if (result.triggered && result.reason) {
|
|
399
|
+
const reasonStr = formatReason(result.reason);
|
|
400
|
+
log?.info(`[${PLUGIN_NAME}] ⚡ KH 提醒(observe-only) · session=${result.sessionId} · ${reasonStr}`, {
|
|
401
|
+
sessionId: result.sessionId,
|
|
402
|
+
reason: result.reason,
|
|
403
|
+
suggestion: "调用 smart_search 查项目历史;完成后用 save_chat_insight 沉淀"
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
return result;
|
|
407
|
+
} catch (err) {
|
|
408
|
+
log?.warn(`[${PLUGIN_NAME}] evaluate 异常(已隔离)`, {
|
|
409
|
+
error: err instanceof Error ? err.message : String(err)
|
|
410
|
+
});
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
function formatReason(r) {
|
|
415
|
+
switch (r.kind) {
|
|
416
|
+
case "token-usage":
|
|
417
|
+
return `token-usage ${(r.ratio * 100).toFixed(1)}% ≥ ${(r.threshold * 100).toFixed(0)}%`;
|
|
418
|
+
case "trigger-word":
|
|
419
|
+
return `trigger-word: "${r.word}"`;
|
|
420
|
+
case "no-search-in-recent-rounds":
|
|
421
|
+
return `no-search-in-recent-rounds (last ${r.rounds})`;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
var log = makePluginLogger(PLUGIN_NAME);
|
|
425
|
+
var khReminderServer = async (ctx) => {
|
|
426
|
+
logLifecycle2(PLUGIN_NAME, "activate", {
|
|
427
|
+
directory: ctx.directory,
|
|
428
|
+
threshold: DEFAULT_THRESHOLD,
|
|
429
|
+
cooldown_ms: COOLDOWN_MS
|
|
430
|
+
});
|
|
431
|
+
return {
|
|
432
|
+
"experimental.chat.messages.transform": async (_input, output) => {
|
|
433
|
+
await safeAsync(PLUGIN_NAME, "experimental.chat.messages.transform", async () => {
|
|
434
|
+
const list = output.messages;
|
|
435
|
+
if (!Array.isArray(list) || list.length === 0)
|
|
436
|
+
return;
|
|
437
|
+
let sessionId = "unknown";
|
|
438
|
+
const flat = list.map((m) => {
|
|
439
|
+
const info = m.info;
|
|
440
|
+
if (info && typeof info.sessionID === "string" && sessionId === "unknown") {
|
|
441
|
+
sessionId = info.sessionID;
|
|
442
|
+
}
|
|
443
|
+
const role = info?.role ?? "user";
|
|
444
|
+
const parts = m.parts ?? [];
|
|
445
|
+
const content = parts.filter((p) => p.type === "text").map((p) => p.text ?? "").join(`
|
|
446
|
+
`);
|
|
447
|
+
return { role, content };
|
|
448
|
+
});
|
|
449
|
+
const result = handleObserve({ messages: flat, sessionId }, log);
|
|
450
|
+
if (!result)
|
|
451
|
+
return;
|
|
452
|
+
safeWriteLog(PLUGIN_NAME, {
|
|
453
|
+
hook: "experimental.chat.messages.transform",
|
|
454
|
+
mode: "observe-only",
|
|
455
|
+
sessionId: result.sessionId,
|
|
456
|
+
triggered: result.triggered,
|
|
457
|
+
cooldown_skipped: result.cooldown_skipped ?? false,
|
|
458
|
+
reason: result.reason ?? null,
|
|
459
|
+
msgs: flat.length
|
|
460
|
+
});
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
};
|
|
465
|
+
var handler2 = khReminderServer;
|
|
466
|
+
|
|
467
|
+
// lib/global-config.ts
|
|
468
|
+
var import_node_fs2 = require("node:fs");
|
|
469
|
+
var path2 = __toESM(require("node:path"));
|
|
470
|
+
var os2 = __toESM(require("node:os"));
|
|
471
|
+
var DEFAULT_KH_URL = "http://10.5.60.26:8900/mcp";
|
|
472
|
+
var DEFAULT_KH_TIMEOUT_MS = 5000;
|
|
473
|
+
var DEFAULT_KH_MAX_RETRIES = 1;
|
|
474
|
+
var CACHE_TTL_MS = 5000;
|
|
475
|
+
var cache = new Map;
|
|
476
|
+
var warnedTokenInFile = new Set;
|
|
477
|
+
function cacheGet(key) {
|
|
478
|
+
const entry = cache.get(key);
|
|
479
|
+
if (!entry)
|
|
480
|
+
return;
|
|
481
|
+
if (Date.now() > entry.expireAt) {
|
|
482
|
+
cache.delete(key);
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
return entry.value;
|
|
486
|
+
}
|
|
487
|
+
function cacheSet(key, value) {
|
|
488
|
+
cache.set(key, { expireAt: Date.now() + CACHE_TTL_MS, value });
|
|
489
|
+
}
|
|
490
|
+
function loadJsonIfExists(filePath) {
|
|
491
|
+
const cacheKey = `file:${filePath}`;
|
|
492
|
+
const cached = cacheGet(cacheKey);
|
|
493
|
+
if (cached !== undefined)
|
|
494
|
+
return cached;
|
|
495
|
+
if (!import_node_fs2.existsSync(filePath)) {
|
|
496
|
+
cacheSet(cacheKey, null);
|
|
497
|
+
return null;
|
|
498
|
+
}
|
|
499
|
+
let raw;
|
|
500
|
+
try {
|
|
501
|
+
raw = import_node_fs2.readFileSync(filePath, "utf8");
|
|
502
|
+
} catch (err) {
|
|
503
|
+
warnOnce(`load-fail:${filePath}`, `[teamkit] 读取 ${filePath} 失败:${errorMessage(err)}(已忽略)`);
|
|
504
|
+
cacheSet(cacheKey, null);
|
|
505
|
+
return null;
|
|
506
|
+
}
|
|
507
|
+
if (raw.charCodeAt(0) === 65279)
|
|
508
|
+
raw = raw.slice(1);
|
|
509
|
+
try {
|
|
510
|
+
const parsed = JSON.parse(raw);
|
|
511
|
+
cacheSet(cacheKey, parsed);
|
|
512
|
+
return parsed;
|
|
513
|
+
} catch (err) {
|
|
514
|
+
warnOnce(`parse-fail:${filePath}`, `[teamkit] JSON 解析失败 ${filePath}:${errorMessage(err)}(已忽略)`);
|
|
515
|
+
cacheSet(cacheKey, null);
|
|
516
|
+
return null;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
function globalConfigDir(env = process.env) {
|
|
520
|
+
const xdg = env["XDG_CONFIG_HOME"];
|
|
521
|
+
const base = xdg && xdg.length > 0 ? xdg : path2.join(os2.homedir(), ".config");
|
|
522
|
+
return path2.join(base, "teamkit");
|
|
523
|
+
}
|
|
524
|
+
function projectConfigDir(root = process.cwd()) {
|
|
525
|
+
return path2.join(root, ".teamkit");
|
|
526
|
+
}
|
|
527
|
+
function getKhConfig(opts = {}) {
|
|
528
|
+
const root = opts.root ?? process.cwd();
|
|
529
|
+
const env = opts.env ?? process.env;
|
|
530
|
+
const cacheKey = `kh:${root}:${env["KNOWLEDGE_HUB_URL"] ?? ""}:${env["KNOWLEDGE_API_KEY"] ? "1" : "0"}`;
|
|
531
|
+
const cached = cacheGet(cacheKey);
|
|
532
|
+
if (cached !== undefined)
|
|
533
|
+
return cached;
|
|
534
|
+
const globalKh = readJsonObject(path2.join(globalConfigDir(env), "kh.json"));
|
|
535
|
+
const projectKh = readJsonObject(path2.join(projectConfigDir(root), "kh.json"));
|
|
536
|
+
for (const [label, src] of [
|
|
537
|
+
["全局 kh.json", globalKh],
|
|
538
|
+
["项目 kh.json", projectKh]
|
|
539
|
+
]) {
|
|
540
|
+
if (containsTokenField(src)) {
|
|
541
|
+
warnOnce(`token-in-file:${label}`, `[teamkit] 检测到 ${label} 写了 token / apiKey / secret 等敏感字段,已忽略。` + ` API key 必须通过环境变量 KNOWLEDGE_API_KEY 提供。`);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
const url = pickString(env["KNOWLEDGE_HUB_URL"]) ?? pickString(projectKh?.["url"]) ?? pickString(globalKh?.["url"]) ?? DEFAULT_KH_URL;
|
|
545
|
+
const timeoutMs = pickPositiveInt(projectKh?.["timeoutMs"]) ?? pickPositiveInt(globalKh?.["timeoutMs"]) ?? DEFAULT_KH_TIMEOUT_MS;
|
|
546
|
+
const maxRetries = pickNonNegativeInt(projectKh?.["maxRetries"]) ?? pickNonNegativeInt(globalKh?.["maxRetries"]) ?? DEFAULT_KH_MAX_RETRIES;
|
|
547
|
+
const apiKey = pickString(env["KNOWLEDGE_API_KEY"]);
|
|
548
|
+
const cfg = apiKey ? { url, apiKey, timeoutMs, maxRetries } : { url, timeoutMs, maxRetries };
|
|
549
|
+
cacheSet(cacheKey, cfg);
|
|
550
|
+
return cfg;
|
|
551
|
+
}
|
|
552
|
+
function readJsonObject(filePath) {
|
|
553
|
+
const raw = loadJsonIfExists(filePath);
|
|
554
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
555
|
+
return null;
|
|
556
|
+
return raw;
|
|
557
|
+
}
|
|
558
|
+
function containsTokenField(obj) {
|
|
559
|
+
if (!obj)
|
|
560
|
+
return false;
|
|
561
|
+
const banned = [
|
|
562
|
+
"token",
|
|
563
|
+
"apikey",
|
|
564
|
+
"api_key",
|
|
565
|
+
"apiKey",
|
|
566
|
+
"authorization",
|
|
567
|
+
"auth",
|
|
568
|
+
"bearer",
|
|
569
|
+
"secret",
|
|
570
|
+
"access_token",
|
|
571
|
+
"refresh_token",
|
|
572
|
+
"client_secret",
|
|
573
|
+
"password",
|
|
574
|
+
"passwd",
|
|
575
|
+
"credential",
|
|
576
|
+
"credentials"
|
|
577
|
+
];
|
|
578
|
+
const bannedLower = new Set(banned.map((b) => b.toLowerCase()));
|
|
579
|
+
for (const key of Object.keys(obj)) {
|
|
580
|
+
if (bannedLower.has(key.toLowerCase()))
|
|
581
|
+
return true;
|
|
582
|
+
}
|
|
583
|
+
return false;
|
|
584
|
+
}
|
|
585
|
+
function pickString(v) {
|
|
586
|
+
if (typeof v !== "string")
|
|
587
|
+
return;
|
|
588
|
+
const s = v.trim();
|
|
589
|
+
return s.length > 0 ? s : undefined;
|
|
590
|
+
}
|
|
591
|
+
function pickPositiveInt(v) {
|
|
592
|
+
if (typeof v !== "number" || !Number.isFinite(v))
|
|
593
|
+
return;
|
|
594
|
+
const n = Math.floor(v);
|
|
595
|
+
return n > 0 ? n : undefined;
|
|
596
|
+
}
|
|
597
|
+
function pickNonNegativeInt(v) {
|
|
598
|
+
if (typeof v !== "number" || !Number.isFinite(v))
|
|
599
|
+
return;
|
|
600
|
+
const n = Math.floor(v);
|
|
601
|
+
return n >= 0 ? n : undefined;
|
|
602
|
+
}
|
|
603
|
+
function errorMessage(err) {
|
|
604
|
+
return err instanceof Error ? err.message : String(err);
|
|
605
|
+
}
|
|
606
|
+
function warnOnce(key, msg) {
|
|
607
|
+
if (warnedTokenInFile.has(key))
|
|
608
|
+
return;
|
|
609
|
+
warnedTokenInFile.add(key);
|
|
610
|
+
console.warn(msg);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// lib/kh-client-stdio.ts
|
|
614
|
+
var DEFAULT_TIMEOUT_MS = 5000;
|
|
615
|
+
var DEFAULT_MAX_RETRIES = 1;
|
|
616
|
+
var RETRY_BASE_DELAY_MS = 200;
|
|
617
|
+
var nextRpcId = 1;
|
|
618
|
+
function makeRpcId() {
|
|
619
|
+
const n = nextRpcId++;
|
|
620
|
+
const r = Math.floor(Math.random() * 65535).toString(16);
|
|
621
|
+
return `tk-${n}-${r}`;
|
|
622
|
+
}
|
|
623
|
+
function createHttpMcpTransport(opts) {
|
|
624
|
+
const apiKey = opts.apiKey?.trim();
|
|
625
|
+
if (!apiKey)
|
|
626
|
+
return null;
|
|
627
|
+
const fetchImpl = opts.fetchImpl ?? (typeof globalThis.fetch === "function" ? globalThis.fetch.bind(globalThis) : null);
|
|
628
|
+
if (!fetchImpl)
|
|
629
|
+
return null;
|
|
630
|
+
const url = opts.url;
|
|
631
|
+
if (!url || typeof url !== "string")
|
|
632
|
+
return null;
|
|
633
|
+
const timeoutMs = opts.timeoutMs && opts.timeoutMs > 0 ? opts.timeoutMs : DEFAULT_TIMEOUT_MS;
|
|
634
|
+
const maxRetries = typeof opts.maxRetries === "number" && opts.maxRetries >= 0 ? Math.floor(opts.maxRetries) : DEFAULT_MAX_RETRIES;
|
|
635
|
+
const sleep = opts.sleep ?? defaultSleep;
|
|
636
|
+
return async (call) => {
|
|
637
|
+
const body = JSON.stringify({
|
|
638
|
+
jsonrpc: "2.0",
|
|
639
|
+
id: makeRpcId(),
|
|
640
|
+
method: "tools/call",
|
|
641
|
+
params: {
|
|
642
|
+
name: call.tool,
|
|
643
|
+
arguments: call.args
|
|
644
|
+
}
|
|
645
|
+
});
|
|
646
|
+
let lastErr;
|
|
647
|
+
for (let attempt = 0;attempt <= maxRetries; attempt++) {
|
|
648
|
+
try {
|
|
649
|
+
const result = await sendOnce({ url, apiKey, body, timeoutMs, fetchImpl });
|
|
650
|
+
return result;
|
|
651
|
+
} catch (err) {
|
|
652
|
+
lastErr = err;
|
|
653
|
+
if (err instanceof HttpStatusError && err.status >= 400 && err.status < 500) {
|
|
654
|
+
throw err;
|
|
655
|
+
}
|
|
656
|
+
if (attempt < maxRetries) {
|
|
657
|
+
const delay = RETRY_BASE_DELAY_MS * Math.pow(2, attempt);
|
|
658
|
+
await sleep(delay);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
throw lastErr ?? new Error("teamkit kh transport: unknown failure");
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
async function sendOnce(ctx) {
|
|
666
|
+
const controller = new AbortController;
|
|
667
|
+
const timer = setTimeout(() => controller.abort(), ctx.timeoutMs);
|
|
668
|
+
let resp;
|
|
669
|
+
try {
|
|
670
|
+
resp = await ctx.fetchImpl(ctx.url, {
|
|
671
|
+
method: "POST",
|
|
672
|
+
headers: {
|
|
673
|
+
"Content-Type": "application/json",
|
|
674
|
+
Accept: "application/json, text/event-stream",
|
|
675
|
+
Authorization: `Bearer ${ctx.apiKey}`
|
|
676
|
+
},
|
|
677
|
+
body: ctx.body,
|
|
678
|
+
signal: controller.signal
|
|
679
|
+
});
|
|
680
|
+
} catch (err) {
|
|
681
|
+
if (isAbortError(err)) {
|
|
682
|
+
throw new Error(`teamkit kh transport: request timeout after ${ctx.timeoutMs}ms`);
|
|
683
|
+
}
|
|
684
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
685
|
+
} finally {
|
|
686
|
+
clearTimeout(timer);
|
|
687
|
+
}
|
|
688
|
+
if (!resp.ok) {
|
|
689
|
+
throw new HttpStatusError(resp.status, `teamkit kh transport: HTTP ${resp.status}`);
|
|
690
|
+
}
|
|
691
|
+
let raw;
|
|
692
|
+
try {
|
|
693
|
+
raw = await resp.text();
|
|
694
|
+
} catch (err) {
|
|
695
|
+
throw new Error(`teamkit kh transport: read body failed — ${errorMessage2(err)}`);
|
|
696
|
+
}
|
|
697
|
+
let unwrapped;
|
|
698
|
+
try {
|
|
699
|
+
unwrapped = unwrapSseIfNeeded(raw, resp.headers.get("content-type"));
|
|
700
|
+
} catch (err) {
|
|
701
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
702
|
+
}
|
|
703
|
+
let json;
|
|
704
|
+
try {
|
|
705
|
+
json = JSON.parse(unwrapped);
|
|
706
|
+
} catch (err) {
|
|
707
|
+
throw new Error(`teamkit kh transport: invalid JSON response — ${errorMessage2(err)}`);
|
|
708
|
+
}
|
|
709
|
+
return parseRpcEnvelope(json);
|
|
710
|
+
}
|
|
711
|
+
function unwrapSseIfNeeded(rawBody, contentType) {
|
|
712
|
+
const ct = (contentType ?? "").toLowerCase();
|
|
713
|
+
if (!ct.includes("text/event-stream")) {
|
|
714
|
+
return rawBody;
|
|
715
|
+
}
|
|
716
|
+
const normalized = rawBody.replace(/\r\n/g, `
|
|
717
|
+
`);
|
|
718
|
+
const firstBlankLine = normalized.indexOf(`
|
|
719
|
+
|
|
720
|
+
`);
|
|
721
|
+
const firstEvent = firstBlankLine >= 0 ? normalized.slice(0, firstBlankLine) : normalized;
|
|
722
|
+
const dataLines = [];
|
|
723
|
+
for (const line of firstEvent.split(`
|
|
724
|
+
`)) {
|
|
725
|
+
if (line.startsWith("data:")) {
|
|
726
|
+
const value = line.slice(5).startsWith(" ") ? line.slice(6) : line.slice(5);
|
|
727
|
+
dataLines.push(value);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
if (dataLines.length === 0) {
|
|
731
|
+
throw new Error("teamkit kh transport: SSE response missing data");
|
|
732
|
+
}
|
|
733
|
+
return dataLines.join(`
|
|
734
|
+
`);
|
|
735
|
+
}
|
|
736
|
+
function parseRpcEnvelope(json) {
|
|
737
|
+
if (!json || typeof json !== "object") {
|
|
738
|
+
throw new Error("teamkit kh transport: response is not an object");
|
|
739
|
+
}
|
|
740
|
+
const obj = json;
|
|
741
|
+
if (obj["error"]) {
|
|
742
|
+
const e = obj["error"];
|
|
743
|
+
throw new Error(`teamkit kh transport: rpc error ${e.code ?? "?"} — ${e.message ?? "unknown"}`);
|
|
744
|
+
}
|
|
745
|
+
const result = obj["result"];
|
|
746
|
+
if (result === undefined || result === null) {
|
|
747
|
+
throw new Error("teamkit kh transport: response missing result");
|
|
748
|
+
}
|
|
749
|
+
if (typeof result === "object") {
|
|
750
|
+
const r = result;
|
|
751
|
+
const content = r["content"];
|
|
752
|
+
if (Array.isArray(content) && content.length > 0) {
|
|
753
|
+
const first = content[0];
|
|
754
|
+
if (first && typeof first["text"] === "string") {
|
|
755
|
+
const text = first["text"];
|
|
756
|
+
try {
|
|
757
|
+
return JSON.parse(text);
|
|
758
|
+
} catch {
|
|
759
|
+
return text;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
return result;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
class HttpStatusError extends Error {
|
|
768
|
+
status;
|
|
769
|
+
constructor(status, msg) {
|
|
770
|
+
super(msg);
|
|
771
|
+
this.status = status;
|
|
772
|
+
this.name = "HttpStatusError";
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
function isAbortError(err) {
|
|
776
|
+
if (!err || typeof err !== "object")
|
|
777
|
+
return false;
|
|
778
|
+
const name = err.name;
|
|
779
|
+
return name === "AbortError" || name === "TimeoutError";
|
|
780
|
+
}
|
|
781
|
+
function defaultSleep(ms) {
|
|
782
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
783
|
+
}
|
|
784
|
+
function errorMessage2(err) {
|
|
785
|
+
return err instanceof Error ? err.message : String(err);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// lib/kh-client.ts
|
|
789
|
+
var KH_CATEGORIES = new Set([
|
|
790
|
+
"convention",
|
|
791
|
+
"anti_pattern",
|
|
792
|
+
"architecture",
|
|
793
|
+
"reference",
|
|
794
|
+
"workflow",
|
|
795
|
+
"gotcha",
|
|
796
|
+
"tooling",
|
|
797
|
+
"decision"
|
|
798
|
+
]);
|
|
799
|
+
function defaultTransport() {
|
|
800
|
+
const g = globalThis;
|
|
801
|
+
if (typeof g.__teamkitMcpCall === "function")
|
|
802
|
+
return g.__teamkitMcpCall;
|
|
803
|
+
return resolveStdioTransport();
|
|
804
|
+
}
|
|
805
|
+
function resolveStdioTransport() {
|
|
806
|
+
try {
|
|
807
|
+
const cfg = getKhConfig();
|
|
808
|
+
return createHttpMcpTransport({
|
|
809
|
+
url: cfg.url,
|
|
810
|
+
apiKey: cfg.apiKey,
|
|
811
|
+
timeoutMs: cfg.timeoutMs,
|
|
812
|
+
maxRetries: cfg.maxRetries
|
|
813
|
+
});
|
|
814
|
+
} catch (err) {
|
|
815
|
+
warnOnce2(`stdio-transport-init`, `[teamkit] KH HTTP transport 初始化失败:${errorMessage3(err)}(已降级)`);
|
|
816
|
+
return null;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
function detectProjectContext() {
|
|
820
|
+
return {
|
|
821
|
+
projectName: process.env["TEAMKIT_PROJECT"] ?? guessProjectName(),
|
|
822
|
+
currentTask: process.env["TEAMKIT_TASK"],
|
|
823
|
+
caller: process.env["TEAMKIT_CALLER"] ?? "tool"
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
function guessProjectName() {
|
|
827
|
+
try {
|
|
828
|
+
const cwd = process.cwd();
|
|
829
|
+
return cwd.split(/[\\/]/).pop() ?? "unknown";
|
|
830
|
+
} catch {
|
|
831
|
+
return "unknown";
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
class KhClient {
|
|
836
|
+
server;
|
|
837
|
+
transport;
|
|
838
|
+
context;
|
|
839
|
+
maxRetries;
|
|
840
|
+
constructor(opts = {}) {
|
|
841
|
+
this.server = opts.serverIdentifier ?? process.env["TEAMKIT_KH_SERVER"] ?? "knowledge-hub";
|
|
842
|
+
this.transport = opts.transport ?? defaultTransport();
|
|
843
|
+
this.context = opts.context ?? detectProjectContext();
|
|
844
|
+
this.maxRetries = opts.maxRetries ?? 0;
|
|
845
|
+
}
|
|
846
|
+
async search(input) {
|
|
847
|
+
const query = (input.query ?? "").trim();
|
|
848
|
+
if (!query) {
|
|
849
|
+
return { ok: false, reason: "invalid_input", message: "query 不能为空" };
|
|
850
|
+
}
|
|
851
|
+
if (!this.transport) {
|
|
852
|
+
return {
|
|
853
|
+
ok: false,
|
|
854
|
+
reason: "transport_unavailable",
|
|
855
|
+
message: "MCP transport 未注入;KH 调用降级(不阻塞主流程)"
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
const args = {
|
|
859
|
+
query,
|
|
860
|
+
limit: input.limit ?? 5,
|
|
861
|
+
_context: {
|
|
862
|
+
project: this.context.projectName,
|
|
863
|
+
caller: this.context.caller,
|
|
864
|
+
prefer: input.prefer ?? ["bugfix", "architecture", "convention"]
|
|
865
|
+
}
|
|
866
|
+
};
|
|
867
|
+
try {
|
|
868
|
+
const raw = await this.callWithRetry({ server: this.server, tool: "smart_search", args });
|
|
869
|
+
return normalizeSearchResult(raw, query);
|
|
870
|
+
} catch (err) {
|
|
871
|
+
return {
|
|
872
|
+
ok: false,
|
|
873
|
+
reason: "kh_returned_error",
|
|
874
|
+
message: err instanceof Error ? err.message : String(err)
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
async save(input) {
|
|
879
|
+
const insight = (input.insight ?? "").trim();
|
|
880
|
+
if (!insight) {
|
|
881
|
+
return { ok: false, reason: "invalid_input", message: "insight 不能为空" };
|
|
882
|
+
}
|
|
883
|
+
if (insight.length > 5000) {
|
|
884
|
+
return {
|
|
885
|
+
ok: false,
|
|
886
|
+
reason: "invalid_input",
|
|
887
|
+
message: `insight 过长 (${insight.length} > 5000 字),建议分多条沉淀`
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
const category = input.category;
|
|
891
|
+
if (!this.transport) {
|
|
892
|
+
return {
|
|
893
|
+
ok: false,
|
|
894
|
+
reason: "transport_unavailable",
|
|
895
|
+
message: "MCP transport 未注入;KH 沉淀降级(不阻塞主流程)"
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
const tags = uniq([
|
|
899
|
+
...input.tags ?? [],
|
|
900
|
+
`project:${this.context.projectName}`,
|
|
901
|
+
`caller:${this.context.caller}`
|
|
902
|
+
]);
|
|
903
|
+
try {
|
|
904
|
+
const raw = await this.callWithRetry({
|
|
905
|
+
server: this.server,
|
|
906
|
+
tool: "save_chat_insight",
|
|
907
|
+
args: { insight, category, tags }
|
|
908
|
+
});
|
|
909
|
+
return normalizeSaveResult(raw);
|
|
910
|
+
} catch (err) {
|
|
911
|
+
return {
|
|
912
|
+
ok: false,
|
|
913
|
+
reason: "kh_returned_error",
|
|
914
|
+
message: err instanceof Error ? err.message : String(err)
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
async callWithRetry(call) {
|
|
919
|
+
if (!this.transport)
|
|
920
|
+
throw new Error("transport not available");
|
|
921
|
+
const full = {
|
|
922
|
+
server: "server" in call && call.server || this.server,
|
|
923
|
+
tool: call.tool,
|
|
924
|
+
args: call.args
|
|
925
|
+
};
|
|
926
|
+
let lastErr;
|
|
927
|
+
for (let attempt = 0;attempt <= this.maxRetries; attempt++) {
|
|
928
|
+
try {
|
|
929
|
+
return await this.transport(full);
|
|
930
|
+
} catch (err) {
|
|
931
|
+
lastErr = err;
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
throw lastErr;
|
|
935
|
+
}
|
|
936
|
+
hasTransport() {
|
|
937
|
+
return this.transport !== null;
|
|
938
|
+
}
|
|
939
|
+
async addKnowledge(args) {
|
|
940
|
+
return this.callKhTool("add_knowledge", args);
|
|
941
|
+
}
|
|
942
|
+
async uploadDocument(args) {
|
|
943
|
+
return this.callKhTool("upload_document", args);
|
|
944
|
+
}
|
|
945
|
+
async uploadDocumentFromUrl(args) {
|
|
946
|
+
return this.callKhTool("upload_document_from_url", args);
|
|
947
|
+
}
|
|
948
|
+
async updateKnowledge(args) {
|
|
949
|
+
return this.callKhTool("update_knowledge", args);
|
|
950
|
+
}
|
|
951
|
+
async flagOutdated(args) {
|
|
952
|
+
return this.callKhTool("flag_outdated", args);
|
|
953
|
+
}
|
|
954
|
+
async deleteKnowledge(args) {
|
|
955
|
+
return this.callKhTool("delete_knowledge", args);
|
|
956
|
+
}
|
|
957
|
+
async listRecent(args = {}) {
|
|
958
|
+
return this.callKhTool("list_recent", args);
|
|
959
|
+
}
|
|
960
|
+
async updateWorkingMemory(args) {
|
|
961
|
+
return this.callKhTool("update_working_memory", args);
|
|
962
|
+
}
|
|
963
|
+
async getWorkingMemory(args = {}) {
|
|
964
|
+
return this.callKhTool("get_working_memory", args);
|
|
965
|
+
}
|
|
966
|
+
async updateUserPreference(args) {
|
|
967
|
+
return this.callKhTool("update_user_preference", args);
|
|
968
|
+
}
|
|
969
|
+
async webSearch(args) {
|
|
970
|
+
return this.callKhTool("web_search", args);
|
|
971
|
+
}
|
|
972
|
+
async generateImage(args) {
|
|
973
|
+
return this.callKhTool("generate_image", args);
|
|
974
|
+
}
|
|
975
|
+
async callKhTool(tool, args) {
|
|
976
|
+
if (!this.transport) {
|
|
977
|
+
return {
|
|
978
|
+
ok: false,
|
|
979
|
+
reason: "transport_unavailable",
|
|
980
|
+
message: `MCP transport 未注入;KH ${tool} 调用降级(不阻塞主流程)`
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
try {
|
|
984
|
+
return await this.callWithRetry({ tool, args });
|
|
985
|
+
} catch (err) {
|
|
986
|
+
return {
|
|
987
|
+
ok: false,
|
|
988
|
+
reason: "kh_returned_error",
|
|
989
|
+
message: err instanceof Error ? err.message : String(err)
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
function normalizeSearchResult(raw, query) {
|
|
995
|
+
const obj = raw ?? {};
|
|
996
|
+
const list = obj["insights"] ?? obj["matches"] ?? obj["results"] ?? [];
|
|
997
|
+
const insights = list.map((item) => normalizeInsight(item)).filter((x) => x !== null);
|
|
998
|
+
return {
|
|
999
|
+
ok: true,
|
|
1000
|
+
query,
|
|
1001
|
+
insights,
|
|
1002
|
+
hint: typeof obj["hint"] === "string" ? obj["hint"] : undefined
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
function normalizeInsight(raw) {
|
|
1006
|
+
if (!raw || typeof raw !== "object")
|
|
1007
|
+
return null;
|
|
1008
|
+
const r = raw;
|
|
1009
|
+
const id = pickString2(r, ["id", "_id", "uuid"]);
|
|
1010
|
+
const title = pickString2(r, ["title", "name"]) ?? "(untitled)";
|
|
1011
|
+
const content = pickString2(r, ["content", "body", "text"]) ?? "";
|
|
1012
|
+
if (!id)
|
|
1013
|
+
return null;
|
|
1014
|
+
return {
|
|
1015
|
+
id,
|
|
1016
|
+
title,
|
|
1017
|
+
content,
|
|
1018
|
+
category: pickString2(r, ["category", "type"]) ?? "reference",
|
|
1019
|
+
scope: pickString2(r, ["scope", "namespace"]) ?? "unknown",
|
|
1020
|
+
confidence: typeof r["confidence"] === "number" ? r["confidence"] : 0,
|
|
1021
|
+
tags: Array.isArray(r["tags"]) ? r["tags"] : undefined,
|
|
1022
|
+
source: pickString2(r, ["source", "origin"])
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
function normalizeSaveResult(raw) {
|
|
1026
|
+
const r = raw ?? {};
|
|
1027
|
+
return {
|
|
1028
|
+
ok: true,
|
|
1029
|
+
id: pickString2(r, ["id", "_id"]) ?? "unknown",
|
|
1030
|
+
title: pickString2(r, ["title"]) ?? "(no title)",
|
|
1031
|
+
category: pickString2(r, ["category"]) ?? "unknown",
|
|
1032
|
+
scope: pickString2(r, ["scope"]) ?? "unknown"
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
function pickString2(obj, keys) {
|
|
1036
|
+
for (const k of keys) {
|
|
1037
|
+
const v = obj[k];
|
|
1038
|
+
if (typeof v === "string" && v.length > 0)
|
|
1039
|
+
return v;
|
|
1040
|
+
}
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
function uniq(arr) {
|
|
1044
|
+
return Array.from(new Set(arr));
|
|
1045
|
+
}
|
|
1046
|
+
function errorMessage3(err) {
|
|
1047
|
+
return err instanceof Error ? err.message : String(err);
|
|
1048
|
+
}
|
|
1049
|
+
var _warnedKeys = new Set;
|
|
1050
|
+
function warnOnce2(key, msg) {
|
|
1051
|
+
if (_warnedKeys.has(key))
|
|
1052
|
+
return;
|
|
1053
|
+
_warnedKeys.add(key);
|
|
1054
|
+
console.warn(msg);
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
// lib/kh-shared-context.ts
|
|
1058
|
+
var DEFAULT_MAX_PER_SESSION = 20;
|
|
1059
|
+
var DEFAULT_TTL_MS = 5 * 60 * 1000;
|
|
1060
|
+
|
|
1061
|
+
class SessionScopedCache {
|
|
1062
|
+
cache = new Map;
|
|
1063
|
+
inflight = new Map;
|
|
1064
|
+
generation = new Map;
|
|
1065
|
+
ttlMs;
|
|
1066
|
+
maxPerSession;
|
|
1067
|
+
now;
|
|
1068
|
+
constructor(opts = {}) {
|
|
1069
|
+
this.ttlMs = opts.ttlMs ?? DEFAULT_TTL_MS;
|
|
1070
|
+
this.maxPerSession = opts.maxPerSession ?? DEFAULT_MAX_PER_SESSION;
|
|
1071
|
+
this.now = opts.now ?? (() => Date.now());
|
|
1072
|
+
}
|
|
1073
|
+
async searchOrGet(args) {
|
|
1074
|
+
const { client, sessionId, query, limit, timeoutMs } = args;
|
|
1075
|
+
const queryHash = this.hashQuery(sessionId, query);
|
|
1076
|
+
const inflightKey = `${sessionId}::${queryHash}`;
|
|
1077
|
+
const sessionMap = this.cache.get(sessionId);
|
|
1078
|
+
if (sessionMap) {
|
|
1079
|
+
const entry = sessionMap.get(queryHash);
|
|
1080
|
+
if (entry && this.now() - entry.cachedAt < this.ttlMs) {
|
|
1081
|
+
sessionMap.delete(queryHash);
|
|
1082
|
+
sessionMap.set(queryHash, entry);
|
|
1083
|
+
return entry.result;
|
|
1084
|
+
}
|
|
1085
|
+
if (entry)
|
|
1086
|
+
sessionMap.delete(queryHash);
|
|
1087
|
+
}
|
|
1088
|
+
const pending = this.inflight.get(inflightKey);
|
|
1089
|
+
if (pending)
|
|
1090
|
+
return pending;
|
|
1091
|
+
const startGen = this.generation.get(sessionId) ?? 0;
|
|
1092
|
+
const promise = this.doSearchAndMaybeCache(client, sessionId, queryHash, query, limit, timeoutMs, startGen).finally(() => {
|
|
1093
|
+
this.inflight.delete(inflightKey);
|
|
1094
|
+
});
|
|
1095
|
+
this.inflight.set(inflightKey, promise);
|
|
1096
|
+
return promise;
|
|
1097
|
+
}
|
|
1098
|
+
onSessionEnd(sessionId) {
|
|
1099
|
+
this.generation.set(sessionId, (this.generation.get(sessionId) ?? 0) + 1);
|
|
1100
|
+
this.cache.delete(sessionId);
|
|
1101
|
+
const prefix = `${sessionId}::`;
|
|
1102
|
+
for (const key of this.inflight.keys()) {
|
|
1103
|
+
if (key.startsWith(prefix))
|
|
1104
|
+
this.inflight.delete(key);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
_snapshot() {
|
|
1108
|
+
const perSession = {};
|
|
1109
|
+
let total = 0;
|
|
1110
|
+
for (const [sid, map] of this.cache.entries()) {
|
|
1111
|
+
perSession[sid] = map.size;
|
|
1112
|
+
total += map.size;
|
|
1113
|
+
}
|
|
1114
|
+
const generations = {};
|
|
1115
|
+
for (const [sid, g] of this.generation.entries()) {
|
|
1116
|
+
generations[sid] = g;
|
|
1117
|
+
}
|
|
1118
|
+
return {
|
|
1119
|
+
cacheSize: total,
|
|
1120
|
+
inflightSize: this.inflight.size,
|
|
1121
|
+
sessions: Array.from(this.cache.keys()),
|
|
1122
|
+
perSession,
|
|
1123
|
+
generations
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
_reset() {
|
|
1127
|
+
this.cache.clear();
|
|
1128
|
+
this.inflight.clear();
|
|
1129
|
+
this.generation.clear();
|
|
1130
|
+
}
|
|
1131
|
+
hashQuery(sessionId, query) {
|
|
1132
|
+
return `${sessionId}::${query}`;
|
|
1133
|
+
}
|
|
1134
|
+
async doSearchAndMaybeCache(client, sessionId, queryHash, query, limit, timeoutMs, startGen) {
|
|
1135
|
+
const result = await this.doSearch(client, query, limit, timeoutMs);
|
|
1136
|
+
const currentGen = this.generation.get(sessionId) ?? 0;
|
|
1137
|
+
if (currentGen === startGen && result.ok) {
|
|
1138
|
+
this.writeCache(sessionId, queryHash, query, result);
|
|
1139
|
+
}
|
|
1140
|
+
return result;
|
|
1141
|
+
}
|
|
1142
|
+
async doSearch(client, query, limit, timeoutMs) {
|
|
1143
|
+
const callPromise = (async () => {
|
|
1144
|
+
try {
|
|
1145
|
+
return await client.search({ query, limit });
|
|
1146
|
+
} catch (err) {
|
|
1147
|
+
return {
|
|
1148
|
+
ok: false,
|
|
1149
|
+
reason: "kh_returned_error",
|
|
1150
|
+
message: err instanceof Error ? err.message : String(err)
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
})();
|
|
1154
|
+
if (timeoutMs === undefined || timeoutMs <= 0) {
|
|
1155
|
+
return callPromise;
|
|
1156
|
+
}
|
|
1157
|
+
let timer = null;
|
|
1158
|
+
try {
|
|
1159
|
+
const racer = new Promise((res) => {
|
|
1160
|
+
timer = setTimeout(() => res({
|
|
1161
|
+
ok: false,
|
|
1162
|
+
reason: "kh_returned_error",
|
|
1163
|
+
message: `kh search timeout after ${timeoutMs}ms`
|
|
1164
|
+
}), timeoutMs);
|
|
1165
|
+
});
|
|
1166
|
+
return await Promise.race([callPromise, racer]);
|
|
1167
|
+
} finally {
|
|
1168
|
+
if (timer)
|
|
1169
|
+
clearTimeout(timer);
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
writeCache(sessionId, queryHash, query, result) {
|
|
1173
|
+
let sessionMap = this.cache.get(sessionId);
|
|
1174
|
+
if (!sessionMap) {
|
|
1175
|
+
sessionMap = new Map;
|
|
1176
|
+
this.cache.set(sessionId, sessionMap);
|
|
1177
|
+
}
|
|
1178
|
+
sessionMap.delete(queryHash);
|
|
1179
|
+
sessionMap.set(queryHash, { query, result, cachedAt: this.now() });
|
|
1180
|
+
while (sessionMap.size > this.maxPerSession) {
|
|
1181
|
+
const oldest = sessionMap.keys().next().value;
|
|
1182
|
+
if (oldest === undefined)
|
|
1183
|
+
break;
|
|
1184
|
+
sessionMap.delete(oldest);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
var sharedKhCache = new SessionScopedCache;
|
|
1189
|
+
|
|
1190
|
+
// plugins/kh-auto-context.ts
|
|
1191
|
+
var PLUGIN_NAME2 = "kh-auto-context";
|
|
1192
|
+
var INJECTION_MODE = "observe-only";
|
|
1193
|
+
function resolveInjectionMode(client) {
|
|
1194
|
+
return client.hasTransport() ? "system-injected" : "observe-only";
|
|
1195
|
+
}
|
|
1196
|
+
var DEFAULT_CONFIG = {
|
|
1197
|
+
minConfidence: 0.55,
|
|
1198
|
+
limit: 3,
|
|
1199
|
+
minTextLength: 8,
|
|
1200
|
+
cacheTtlMs: 5 * 60 * 1000,
|
|
1201
|
+
timeoutMs: 1000,
|
|
1202
|
+
skipPrefixes: ["/", "!", "@"],
|
|
1203
|
+
skipKeywords: ["你好", "hello", "hi", "thanks", "谢谢"]
|
|
1204
|
+
};
|
|
1205
|
+
|
|
1206
|
+
class QueryCache {
|
|
1207
|
+
ttlMs;
|
|
1208
|
+
now;
|
|
1209
|
+
map = new Map;
|
|
1210
|
+
constructor(ttlMs, now = () => Date.now()) {
|
|
1211
|
+
this.ttlMs = ttlMs;
|
|
1212
|
+
this.now = now;
|
|
1213
|
+
}
|
|
1214
|
+
shouldSkip(query) {
|
|
1215
|
+
const entry = this.map.get(query);
|
|
1216
|
+
if (!entry)
|
|
1217
|
+
return false;
|
|
1218
|
+
return this.now() - entry.injectedAt < this.ttlMs;
|
|
1219
|
+
}
|
|
1220
|
+
record(query, insights) {
|
|
1221
|
+
this.map.set(query, { query, injectedAt: this.now(), insights });
|
|
1222
|
+
}
|
|
1223
|
+
size() {
|
|
1224
|
+
return this.map.size;
|
|
1225
|
+
}
|
|
1226
|
+
clear() {
|
|
1227
|
+
this.map.clear();
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
function extractQuery(text) {
|
|
1231
|
+
if (!text)
|
|
1232
|
+
return "";
|
|
1233
|
+
const norm = text.trim().replace(/\s+/g, " ");
|
|
1234
|
+
const m = /^[^.。?!?!;;\n]+/.exec(norm);
|
|
1235
|
+
let head = (m ? m[0] : norm).trim();
|
|
1236
|
+
const fillerRe = /^(请|帮我|麻烦|我想|你能|你可以|看一?下|查一?下|how (do|can) (i|you)|can you|could you|please|help me)\s*/i;
|
|
1237
|
+
let prev = "";
|
|
1238
|
+
while (prev !== head) {
|
|
1239
|
+
prev = head;
|
|
1240
|
+
head = head.replace(fillerRe, "").trim();
|
|
1241
|
+
}
|
|
1242
|
+
return head.slice(0, 80);
|
|
1243
|
+
}
|
|
1244
|
+
function shouldInject(text, cfg = DEFAULT_CONFIG) {
|
|
1245
|
+
if (!text)
|
|
1246
|
+
return false;
|
|
1247
|
+
const t = text.trim();
|
|
1248
|
+
if (t.length < cfg.minTextLength)
|
|
1249
|
+
return false;
|
|
1250
|
+
for (const p of cfg.skipPrefixes) {
|
|
1251
|
+
if (t.startsWith(p))
|
|
1252
|
+
return false;
|
|
1253
|
+
}
|
|
1254
|
+
const lower = t.toLowerCase();
|
|
1255
|
+
for (const kw of cfg.skipKeywords) {
|
|
1256
|
+
if (lower.includes(kw.toLowerCase()) && t.length <= kw.length + 5)
|
|
1257
|
+
return false;
|
|
1258
|
+
}
|
|
1259
|
+
return true;
|
|
1260
|
+
}
|
|
1261
|
+
function formatInjection(query, insights, mode = INJECTION_MODE) {
|
|
1262
|
+
const lines = [];
|
|
1263
|
+
lines.push(`> \uD83E\uDDE0 [${mode}] 候选 ${insights.length} 条团队经验(query: "${query}")`);
|
|
1264
|
+
lines.push("");
|
|
1265
|
+
for (const ins of insights) {
|
|
1266
|
+
lines.push(`**${ins.title}** \`${ins.category}\` _(${(ins.confidence * 100).toFixed(0)}%)_`);
|
|
1267
|
+
const content = ins.content.length > 240 ? ins.content.slice(0, 240) + "…" : ins.content;
|
|
1268
|
+
lines.push(content.replace(/\n+/g, " "));
|
|
1269
|
+
lines.push("");
|
|
1270
|
+
}
|
|
1271
|
+
return { query, insights, markdown: lines.join(`
|
|
1272
|
+
`), mode };
|
|
1273
|
+
}
|
|
1274
|
+
var TEAMKIT_CONSTRAINTS = [
|
|
1275
|
+
{
|
|
1276
|
+
content: "本会话安装了 teamkit:触发词命中(部署/怎么/为什么/之前/历史等)必须先 smart_search;" + "对话结束后用 save_chat_insight 沉淀经验",
|
|
1277
|
+
priority: 9
|
|
1278
|
+
},
|
|
1279
|
+
{
|
|
1280
|
+
content: "API key 仅读环境变量 KNOWLEDGE_API_KEY,禁止写入任何配置文件",
|
|
1281
|
+
priority: 9
|
|
1282
|
+
},
|
|
1283
|
+
{
|
|
1284
|
+
content: "沉淀经验优先用 save_chat_insight(自动分类、去重、合并),不要直接 add_knowledge",
|
|
1285
|
+
priority: 8
|
|
1286
|
+
}
|
|
1287
|
+
];
|
|
1288
|
+
async function seedConstraints(client, log2) {
|
|
1289
|
+
if (!client.hasTransport()) {
|
|
1290
|
+
log2?.debug?.(`[${PLUGIN_NAME2}] seedConstraints: transport 不可用,跳过`, {});
|
|
1291
|
+
return { ok: false, reason: "transport_unavailable" };
|
|
1292
|
+
}
|
|
1293
|
+
try {
|
|
1294
|
+
const result = await client.updateWorkingMemory({
|
|
1295
|
+
section: "constraints",
|
|
1296
|
+
replace: false,
|
|
1297
|
+
items: TEAMKIT_CONSTRAINTS.map((c) => ({
|
|
1298
|
+
content: `teamkit: ${c.content}`,
|
|
1299
|
+
priority: c.priority
|
|
1300
|
+
}))
|
|
1301
|
+
});
|
|
1302
|
+
if (result && typeof result === "object" && "ok" in result && result.ok === false) {
|
|
1303
|
+
const r = result;
|
|
1304
|
+
log2?.warn(`[${PLUGIN_NAME2}] seedConstraints 降级`, { reason: r.reason, message: r.message });
|
|
1305
|
+
return {
|
|
1306
|
+
ok: false,
|
|
1307
|
+
reason: r.reason === "transport_unavailable" ? "transport_unavailable" : "kh_call_failed",
|
|
1308
|
+
message: r.message
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1311
|
+
log2?.info(`[${PLUGIN_NAME2}] seedConstraints: 已写入 ${TEAMKIT_CONSTRAINTS.length} 条 constraints`, {
|
|
1312
|
+
count: TEAMKIT_CONSTRAINTS.length
|
|
1313
|
+
});
|
|
1314
|
+
return { ok: true, itemsWritten: TEAMKIT_CONSTRAINTS.length };
|
|
1315
|
+
} catch (err) {
|
|
1316
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1317
|
+
log2?.warn(`[${PLUGIN_NAME2}] seedConstraints 失败(已静默)`, { error: message });
|
|
1318
|
+
return { ok: false, reason: "exception", message };
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
function createSystemInjectedHook(client, sessionId, log2) {
|
|
1322
|
+
const section = `teamkit:auto-context-${sessionId ?? "global"}`;
|
|
1323
|
+
return async (markdown) => {
|
|
1324
|
+
try {
|
|
1325
|
+
const result = await client.updateWorkingMemory({
|
|
1326
|
+
section,
|
|
1327
|
+
replace: true,
|
|
1328
|
+
items: [{ content: markdown, priority: 7 }]
|
|
1329
|
+
});
|
|
1330
|
+
if (result && typeof result === "object" && "ok" in result && result.ok === false) {
|
|
1331
|
+
const r = result;
|
|
1332
|
+
log2?.warn(`[${PLUGIN_NAME2}] system-injected 降级到 observe-only:${r.reason ?? "unknown"}`, {
|
|
1333
|
+
sessionId,
|
|
1334
|
+
section,
|
|
1335
|
+
preview: markdown.slice(0, 200),
|
|
1336
|
+
reason: r.reason,
|
|
1337
|
+
message: r.message
|
|
1338
|
+
});
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
log2?.info(`[${PLUGIN_NAME2}] system-injected: 写入 KH working_memory 成功 (${markdown.length} chars)`, {
|
|
1342
|
+
sessionId,
|
|
1343
|
+
section
|
|
1344
|
+
});
|
|
1345
|
+
} catch (err) {
|
|
1346
|
+
log2?.warn(`[${PLUGIN_NAME2}] system-injected 抛异常,降级到 observe-only`, {
|
|
1347
|
+
sessionId,
|
|
1348
|
+
section,
|
|
1349
|
+
preview: markdown.slice(0, 200),
|
|
1350
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1351
|
+
});
|
|
1352
|
+
}
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
var inflight = new Set;
|
|
1356
|
+
var INFLIGHT_CAP = 5;
|
|
1357
|
+
function inflightKey(sessionId, query) {
|
|
1358
|
+
return `${sessionId ?? "global"}:${Date.now()}:${Math.random().toString(36).slice(2, 10)}`;
|
|
1359
|
+
}
|
|
1360
|
+
async function runKhSearchAndInject(args) {
|
|
1361
|
+
const { query, ctx, opts, mode } = args;
|
|
1362
|
+
const cfg = opts.config ?? DEFAULT_CONFIG;
|
|
1363
|
+
const log2 = ctx.log;
|
|
1364
|
+
const startedAt = Date.now();
|
|
1365
|
+
const racer = opts.scheduler?.raceTimeout ?? (async (p, ms) => {
|
|
1366
|
+
let timer = null;
|
|
1367
|
+
try {
|
|
1368
|
+
return await Promise.race([
|
|
1369
|
+
p,
|
|
1370
|
+
new Promise((res) => {
|
|
1371
|
+
timer = setTimeout(() => res("__timeout__"), ms);
|
|
1372
|
+
})
|
|
1373
|
+
]);
|
|
1374
|
+
} finally {
|
|
1375
|
+
if (timer)
|
|
1376
|
+
clearTimeout(timer);
|
|
1377
|
+
}
|
|
1378
|
+
});
|
|
1379
|
+
let result;
|
|
1380
|
+
try {
|
|
1381
|
+
const searchPromise = sharedKhCache.searchOrGet({
|
|
1382
|
+
client: opts.client,
|
|
1383
|
+
sessionId: ctx.sessionId ?? "global",
|
|
1384
|
+
query,
|
|
1385
|
+
limit: cfg.limit
|
|
1386
|
+
});
|
|
1387
|
+
result = await racer(searchPromise, cfg.timeoutMs);
|
|
1388
|
+
} catch (err) {
|
|
1389
|
+
log2?.warn(`[${PLUGIN_NAME2}] client.search threw`, {
|
|
1390
|
+
query,
|
|
1391
|
+
elapsedMs: Date.now() - startedAt,
|
|
1392
|
+
sessionId: ctx.sessionId,
|
|
1393
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1394
|
+
});
|
|
1395
|
+
opts.cache.record(query, []);
|
|
1396
|
+
return null;
|
|
1397
|
+
}
|
|
1398
|
+
if (result === "__timeout__") {
|
|
1399
|
+
log2?.warn(`[${PLUGIN_NAME2}] timeout`, { query, ms: cfg.timeoutMs, elapsedMs: Date.now() - startedAt, sessionId: ctx.sessionId });
|
|
1400
|
+
opts.cache.record(query, []);
|
|
1401
|
+
return null;
|
|
1402
|
+
}
|
|
1403
|
+
if (!result.ok) {
|
|
1404
|
+
log2?.warn(`[${PLUGIN_NAME2}] kh degraded`, { reason: result.reason, query, elapsedMs: Date.now() - startedAt, sessionId: ctx.sessionId });
|
|
1405
|
+
opts.cache.record(query, []);
|
|
1406
|
+
return null;
|
|
1407
|
+
}
|
|
1408
|
+
const filtered = result.insights.filter((i) => (i.confidence ?? 0) >= cfg.minConfidence);
|
|
1409
|
+
if (filtered.length === 0) {
|
|
1410
|
+
opts.cache.record(query, []);
|
|
1411
|
+
return null;
|
|
1412
|
+
}
|
|
1413
|
+
const payload = formatInjection(query, filtered, mode);
|
|
1414
|
+
if (typeof ctx.injectContext === "function") {
|
|
1415
|
+
try {
|
|
1416
|
+
await ctx.injectContext(payload.markdown);
|
|
1417
|
+
} catch (err) {
|
|
1418
|
+
log2?.warn(`[${PLUGIN_NAME2}] injectContext threw`, {
|
|
1419
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1420
|
+
query,
|
|
1421
|
+
sessionId: ctx.sessionId
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
opts.cache.record(query, filtered);
|
|
1426
|
+
log2?.info(`[${PLUGIN_NAME2}] inject complete (${mode})`, {
|
|
1427
|
+
query,
|
|
1428
|
+
mode,
|
|
1429
|
+
candidateCount: filtered.length,
|
|
1430
|
+
elapsedMs: Date.now() - startedAt,
|
|
1431
|
+
sessionId: ctx.sessionId
|
|
1432
|
+
});
|
|
1433
|
+
return payload;
|
|
1434
|
+
}
|
|
1435
|
+
async function handleMessage(raw, opts) {
|
|
1436
|
+
const cfg = opts.config ?? DEFAULT_CONFIG;
|
|
1437
|
+
const mode = opts.mode ?? INJECTION_MODE;
|
|
1438
|
+
const ctx = raw ?? {};
|
|
1439
|
+
const log2 = ctx.log;
|
|
1440
|
+
const text = (ctx.content ?? "").trim();
|
|
1441
|
+
if (!shouldInject(text, cfg)) {
|
|
1442
|
+
log2?.debug?.(`[${PLUGIN_NAME2}] skip (filter)`, { textLen: text.length });
|
|
1443
|
+
return;
|
|
1444
|
+
}
|
|
1445
|
+
const query = extractQuery(text);
|
|
1446
|
+
if (!query)
|
|
1447
|
+
return;
|
|
1448
|
+
if (opts.cache.shouldSkip(query)) {
|
|
1449
|
+
log2?.debug?.(`[${PLUGIN_NAME2}] cache hit, skip`, { query });
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
1452
|
+
if (inflight.size >= INFLIGHT_CAP) {
|
|
1453
|
+
log2?.warn(`[${PLUGIN_NAME2}] inflight cap reached, skip`, { query, inflightSize: inflight.size, cap: INFLIGHT_CAP, sessionId: ctx.sessionId });
|
|
1454
|
+
return;
|
|
1455
|
+
}
|
|
1456
|
+
const key = inflightKey(ctx.sessionId, query);
|
|
1457
|
+
inflight.add(key);
|
|
1458
|
+
runKhSearchAndInject({ query, ctx, opts, mode }).catch((err) => {
|
|
1459
|
+
log2?.warn(`[${PLUGIN_NAME2}] runKhSearchAndInject 顶层兜底捕获`, {
|
|
1460
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1461
|
+
query,
|
|
1462
|
+
sessionId: ctx.sessionId
|
|
1463
|
+
});
|
|
1464
|
+
}).finally(() => {
|
|
1465
|
+
inflight.delete(key);
|
|
1466
|
+
});
|
|
1467
|
+
}
|
|
1468
|
+
logLifecycle2(PLUGIN_NAME2, "import");
|
|
1469
|
+
var sharedClient = new KhClient;
|
|
1470
|
+
var sharedCache = new QueryCache(DEFAULT_CONFIG.cacheTtlMs);
|
|
1471
|
+
var khAutoContextServer = async (ctx) => {
|
|
1472
|
+
const log2 = makePluginLogger(PLUGIN_NAME2);
|
|
1473
|
+
const runtimeMode = resolveInjectionMode(sharedClient);
|
|
1474
|
+
logLifecycle2(PLUGIN_NAME2, "activate", {
|
|
1475
|
+
directory: ctx.directory,
|
|
1476
|
+
minConfidence: DEFAULT_CONFIG.minConfidence,
|
|
1477
|
+
timeoutMs: DEFAULT_CONFIG.timeoutMs,
|
|
1478
|
+
mode: runtimeMode,
|
|
1479
|
+
transport_available: sharedClient.hasTransport()
|
|
1480
|
+
});
|
|
1481
|
+
seedConstraints(sharedClient, log2).catch((err) => {
|
|
1482
|
+
log2.warn("seedConstraints 顶层兜底捕获", { error: err instanceof Error ? err.message : String(err) });
|
|
1483
|
+
});
|
|
1484
|
+
return {
|
|
1485
|
+
"chat.message": async (input, output) => {
|
|
1486
|
+
await safeAsync(PLUGIN_NAME2, "chat.message", async () => {
|
|
1487
|
+
const text = extractUserText(output);
|
|
1488
|
+
if (!text)
|
|
1489
|
+
return;
|
|
1490
|
+
const injectContext = runtimeMode === "system-injected" ? createSystemInjectedHook(sharedClient, input.sessionID, log2) : async (markdown) => {
|
|
1491
|
+
log2.info(`KH context candidate (${markdown.length} chars)`, {
|
|
1492
|
+
sessionID: input.sessionID,
|
|
1493
|
+
mode: runtimeMode,
|
|
1494
|
+
preview: markdown.slice(0, 200)
|
|
1495
|
+
});
|
|
1496
|
+
};
|
|
1497
|
+
await handleMessage({ content: text, sessionId: input.sessionID, injectContext, log: log2 }, { client: sharedClient, cache: sharedCache, mode: runtimeMode });
|
|
1498
|
+
});
|
|
1499
|
+
},
|
|
1500
|
+
event: async ({ event }) => {
|
|
1501
|
+
await safeAsync(PLUGIN_NAME2, "event", async () => {
|
|
1502
|
+
const e = event;
|
|
1503
|
+
if (e.type !== "session.idle")
|
|
1504
|
+
return;
|
|
1505
|
+
const props = e.properties;
|
|
1506
|
+
const sid = props?.sessionID;
|
|
1507
|
+
if (typeof sid !== "string" || !sid)
|
|
1508
|
+
return;
|
|
1509
|
+
sharedKhCache.onSessionEnd(sid);
|
|
1510
|
+
log2.debug?.(`[${PLUGIN_NAME2}] session.idle: cleared shared cache`, { sessionID: sid });
|
|
1511
|
+
});
|
|
1512
|
+
}
|
|
1513
|
+
};
|
|
1514
|
+
};
|
|
1515
|
+
var handler3 = khAutoContextServer;
|
|
1516
|
+
|
|
1517
|
+
// plugins/auto-learning.ts
|
|
1518
|
+
var crypto = __toESM(require("node:crypto"));
|
|
1519
|
+
var PLUGIN_NAME3 = "auto-learning";
|
|
1520
|
+
var DEFAULT_CONFIG2 = {
|
|
1521
|
+
minScore: 0.5,
|
|
1522
|
+
cooldownMs: 24 * 60 * 60 * 1000,
|
|
1523
|
+
fingerprintLimit: 256,
|
|
1524
|
+
events: ["bug.fixed", "workflow.completed", "session.ended"]
|
|
1525
|
+
};
|
|
1526
|
+
var NOISE_TITLE_RE = /^\s*(test|wip|tmp|fix typo|update|chore|debug|todo|hello)\b/i;
|
|
1527
|
+
var TRIVIAL_KEYWORDS = ["typo", "格式化", "format", "rename", "重命名"];
|
|
1528
|
+
function extractInsights(ctx) {
|
|
1529
|
+
if (!ctx || typeof ctx !== "object")
|
|
1530
|
+
return [];
|
|
1531
|
+
const out = [];
|
|
1532
|
+
if (ctx.bug && ctx.bug.symptom && ctx.bug.fix) {
|
|
1533
|
+
const rootCause = ctx.bug.rootCause?.trim() ?? "";
|
|
1534
|
+
const baseScore = rootCause ? 0.85 : 0.55;
|
|
1535
|
+
const score = adjustScore(baseScore, ctx);
|
|
1536
|
+
const title = clip(`Bug 修复:${ctx.bug.symptom}`, 80);
|
|
1537
|
+
if (!isTrivial(title)) {
|
|
1538
|
+
out.push({
|
|
1539
|
+
title,
|
|
1540
|
+
content: clip([
|
|
1541
|
+
`**症状**:${ctx.bug.symptom}`,
|
|
1542
|
+
rootCause ? `**根因**:${rootCause}` : "",
|
|
1543
|
+
`**修复**:${ctx.bug.fix}`,
|
|
1544
|
+
ctx.bug.files?.length ? `**涉及**:${ctx.bug.files.slice(0, 5).join(", ")}` : ""
|
|
1545
|
+
].filter(Boolean).join(`
|
|
1546
|
+
|
|
1547
|
+
`), 500),
|
|
1548
|
+
kind: "bug-fix",
|
|
1549
|
+
category: "踩坑记录",
|
|
1550
|
+
tags: ctx.bug.files?.slice(0, 5),
|
|
1551
|
+
score
|
|
1552
|
+
});
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
if (ctx.workflow?.learnings?.length) {
|
|
1556
|
+
for (const lesson of ctx.workflow.learnings) {
|
|
1557
|
+
const text = lesson.trim();
|
|
1558
|
+
if (!text || isTrivial(text))
|
|
1559
|
+
continue;
|
|
1560
|
+
const score = adjustScore(0.7, ctx);
|
|
1561
|
+
out.push({
|
|
1562
|
+
title: clip(`${ctx.workflow.name ?? "workflow"}:${text}`, 80),
|
|
1563
|
+
content: clip(text, 500),
|
|
1564
|
+
kind: "decision",
|
|
1565
|
+
category: "操作指南",
|
|
1566
|
+
tags: ctx.workflow.name ? [ctx.workflow.name] : undefined,
|
|
1567
|
+
score
|
|
1568
|
+
});
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
if (ctx.summary && ctx.positiveFeedback && !ctx.rolledBack) {
|
|
1572
|
+
const text = ctx.summary.trim();
|
|
1573
|
+
if (text && !isTrivial(text)) {
|
|
1574
|
+
out.push({
|
|
1575
|
+
title: clip(text.split(/[。.\n]/)[0] || "会话洞察", 80),
|
|
1576
|
+
content: clip(text, 500),
|
|
1577
|
+
kind: "decision",
|
|
1578
|
+
category: "其它",
|
|
1579
|
+
score: adjustScore(0.55, ctx)
|
|
1580
|
+
});
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
for (const i of out)
|
|
1584
|
+
i.fingerprint = fingerprint(i);
|
|
1585
|
+
return out.filter((i) => i.score > 0);
|
|
1586
|
+
}
|
|
1587
|
+
function adjustScore(base, ctx) {
|
|
1588
|
+
let s = base;
|
|
1589
|
+
if (ctx.positiveFeedback)
|
|
1590
|
+
s += 0.1;
|
|
1591
|
+
if (ctx.rolledBack)
|
|
1592
|
+
s -= 0.5;
|
|
1593
|
+
return Math.max(-1, Math.min(1, s));
|
|
1594
|
+
}
|
|
1595
|
+
function isTrivial(text) {
|
|
1596
|
+
if (!text)
|
|
1597
|
+
return true;
|
|
1598
|
+
if (NOISE_TITLE_RE.test(text))
|
|
1599
|
+
return true;
|
|
1600
|
+
const lower = text.toLowerCase();
|
|
1601
|
+
return TRIVIAL_KEYWORDS.some((k) => lower.includes(k));
|
|
1602
|
+
}
|
|
1603
|
+
function clip(s, max) {
|
|
1604
|
+
if (!s)
|
|
1605
|
+
return "";
|
|
1606
|
+
if (s.length <= max)
|
|
1607
|
+
return s;
|
|
1608
|
+
return s.slice(0, max - 1) + "…";
|
|
1609
|
+
}
|
|
1610
|
+
function fingerprint(i) {
|
|
1611
|
+
const seed = `${i.kind}|${i.title}|${i.content}`;
|
|
1612
|
+
return crypto.createHash("sha1").update(seed).digest("hex").slice(0, 16);
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
class FingerprintLRU {
|
|
1616
|
+
limit;
|
|
1617
|
+
list = [];
|
|
1618
|
+
constructor(limit = 256) {
|
|
1619
|
+
this.limit = limit;
|
|
1620
|
+
}
|
|
1621
|
+
has(fp, withinMs, now = Date.now()) {
|
|
1622
|
+
const e = this.list.find((x) => x.fp === fp);
|
|
1623
|
+
if (!e)
|
|
1624
|
+
return false;
|
|
1625
|
+
return now - e.ts < withinMs;
|
|
1626
|
+
}
|
|
1627
|
+
add(fp, now = Date.now()) {
|
|
1628
|
+
this.list = this.list.filter((x) => x.fp !== fp);
|
|
1629
|
+
this.list.unshift({ fp, ts: now });
|
|
1630
|
+
if (this.list.length > this.limit)
|
|
1631
|
+
this.list.length = this.limit;
|
|
1632
|
+
}
|
|
1633
|
+
size() {
|
|
1634
|
+
return this.list.length;
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
function shouldSave(insight, lru, cfg, now = Date.now()) {
|
|
1638
|
+
if (insight.score < cfg.minScore) {
|
|
1639
|
+
return { decision: "skip", reason: `score ${insight.score.toFixed(2)} < ${cfg.minScore}`, insight };
|
|
1640
|
+
}
|
|
1641
|
+
if (insight.fingerprint && lru.has(insight.fingerprint, cfg.cooldownMs, now)) {
|
|
1642
|
+
return { decision: "skip", reason: "duplicate within cooldown", insight };
|
|
1643
|
+
}
|
|
1644
|
+
return { decision: "save", reason: "ok", insight };
|
|
1645
|
+
}
|
|
1646
|
+
var moduleLRU = new FingerprintLRU(DEFAULT_CONFIG2.fingerprintLimit);
|
|
1647
|
+
async function processEvent(raw, opts = {}) {
|
|
1648
|
+
const cfg = opts.config ?? DEFAULT_CONFIG2;
|
|
1649
|
+
const lru = opts.lru ?? moduleLRU;
|
|
1650
|
+
const now = opts.now ?? Date.now();
|
|
1651
|
+
const ctx = raw ?? {};
|
|
1652
|
+
const insights = extractInsights(ctx);
|
|
1653
|
+
const decisions = insights.map((i) => shouldSave(i, lru, cfg, now));
|
|
1654
|
+
let saved = 0;
|
|
1655
|
+
let skipped = 0;
|
|
1656
|
+
for (const d of decisions) {
|
|
1657
|
+
if (d.decision === "skip") {
|
|
1658
|
+
skipped++;
|
|
1659
|
+
continue;
|
|
1660
|
+
}
|
|
1661
|
+
if (typeof opts.saveInsight !== "function") {
|
|
1662
|
+
if (d.insight.fingerprint)
|
|
1663
|
+
lru.add(d.insight.fingerprint, now);
|
|
1664
|
+
skipped++;
|
|
1665
|
+
continue;
|
|
1666
|
+
}
|
|
1667
|
+
try {
|
|
1668
|
+
const res = await opts.saveInsight({
|
|
1669
|
+
title: d.insight.title,
|
|
1670
|
+
content: d.insight.content,
|
|
1671
|
+
category: d.insight.category,
|
|
1672
|
+
tags: d.insight.tags
|
|
1673
|
+
});
|
|
1674
|
+
if (res?.ok) {
|
|
1675
|
+
if (d.insight.fingerprint)
|
|
1676
|
+
lru.add(d.insight.fingerprint, now);
|
|
1677
|
+
saved++;
|
|
1678
|
+
opts.log?.info(`[${PLUGIN_NAME3}] saved insight: ${d.insight.title}`, { id: res.id });
|
|
1679
|
+
if (typeof opts.onSaved === "function") {
|
|
1680
|
+
try {
|
|
1681
|
+
await opts.onSaved(d.insight, { id: res.id });
|
|
1682
|
+
} catch (err) {
|
|
1683
|
+
opts.log?.warn(`[${PLUGIN_NAME3}] onSaved hook 抛错(已隔离)`, { error: err instanceof Error ? err.message : String(err) });
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
} else {
|
|
1687
|
+
skipped++;
|
|
1688
|
+
opts.log?.warn(`[${PLUGIN_NAME3}] save failed: ${res?.error ?? "unknown"}`);
|
|
1689
|
+
}
|
|
1690
|
+
} catch (err) {
|
|
1691
|
+
skipped++;
|
|
1692
|
+
opts.log?.warn(`[${PLUGIN_NAME3}] save threw(已隔离)`, { error: err instanceof Error ? err.message : String(err) });
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
return { saved, skipped, decisions };
|
|
1696
|
+
}
|
|
1697
|
+
function createKhSaveInsightHook(client, log2) {
|
|
1698
|
+
if (!client.hasTransport()) {
|
|
1699
|
+
log2?.debug?.(`[${PLUGIN_NAME3}] createKhSaveInsightHook: transport 不可用,返 undefined(走 noop)`, {});
|
|
1700
|
+
return;
|
|
1701
|
+
}
|
|
1702
|
+
return async ({ title, content, category, tags }) => {
|
|
1703
|
+
const insight = `**${title}**
|
|
1704
|
+
|
|
1705
|
+
${content}`;
|
|
1706
|
+
try {
|
|
1707
|
+
const r = await client.save({ insight, category, tags });
|
|
1708
|
+
if (r.ok)
|
|
1709
|
+
return { ok: true, id: r.id };
|
|
1710
|
+
return { ok: false, error: `${r.reason}: ${r.message}` };
|
|
1711
|
+
} catch (err) {
|
|
1712
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1713
|
+
log2?.warn(`[${PLUGIN_NAME3}] saveInsight hook 抛异常(已隔离)`, { error: message });
|
|
1714
|
+
return { ok: false, error: message };
|
|
1715
|
+
}
|
|
1716
|
+
};
|
|
1717
|
+
}
|
|
1718
|
+
logLifecycle2(PLUGIN_NAME3, "import");
|
|
1719
|
+
var TRIGGER_EVENT_TYPES = new Set(["session.idle", "session.error"]);
|
|
1720
|
+
var sharedClient2 = new KhClient;
|
|
1721
|
+
var autoLearningServer = async (ctx) => {
|
|
1722
|
+
const log2 = makePluginLogger(PLUGIN_NAME3);
|
|
1723
|
+
const saveInsight = createKhSaveInsightHook(sharedClient2, log2);
|
|
1724
|
+
logLifecycle2(PLUGIN_NAME3, "activate", {
|
|
1725
|
+
directory: ctx.directory,
|
|
1726
|
+
triggerEventTypes: [...TRIGGER_EVENT_TYPES],
|
|
1727
|
+
minScore: DEFAULT_CONFIG2.minScore,
|
|
1728
|
+
transport_available: sharedClient2.hasTransport(),
|
|
1729
|
+
save_insight_wired: typeof saveInsight === "function"
|
|
1730
|
+
});
|
|
1731
|
+
return {
|
|
1732
|
+
event: async ({ event }) => {
|
|
1733
|
+
await safeAsync(PLUGIN_NAME3, "event", async () => {
|
|
1734
|
+
const e = event;
|
|
1735
|
+
if (!e.type || !TRIGGER_EVENT_TYPES.has(e.type))
|
|
1736
|
+
return;
|
|
1737
|
+
const r = await processEvent({
|
|
1738
|
+
type: e.type,
|
|
1739
|
+
...e.properties && typeof e.properties === "object" ? e.properties : {}
|
|
1740
|
+
}, { log: log2, saveInsight });
|
|
1741
|
+
if (r.saved > 0 || r.skipped > 0) {
|
|
1742
|
+
log2.info(`event ${e.type}: saved=${r.saved} skipped=${r.skipped}`);
|
|
1743
|
+
}
|
|
1744
|
+
});
|
|
1745
|
+
}
|
|
1746
|
+
};
|
|
1747
|
+
};
|
|
1748
|
+
var handler4 = autoLearningServer;
|
|
1749
|
+
|
|
1750
|
+
// src/index.ts
|
|
1751
|
+
function createTeamkitServer(opts) {
|
|
1752
|
+
if (opts.enableDevIsolation && shouldYieldToLocalPlugin()) {
|
|
1753
|
+
logLifecycle("yield_to_local: true, handlers_active: 0");
|
|
1754
|
+
return {};
|
|
1755
|
+
}
|
|
1756
|
+
logLifecycle("handlers_active: 4 (kh-reminder + kh-auto-context + auto-learning + compaction-guard)");
|
|
1757
|
+
const compactionPlugin = handler();
|
|
1758
|
+
return async (ctx) => {
|
|
1759
|
+
const [khReminderHooks, khAutoContextHooks, autoLearningHooks] = await Promise.all([
|
|
1760
|
+
handler2(ctx),
|
|
1761
|
+
handler3(ctx),
|
|
1762
|
+
handler4(ctx)
|
|
1763
|
+
]);
|
|
1764
|
+
return {
|
|
1765
|
+
...compactionPlugin,
|
|
1766
|
+
...khReminderHooks ?? {},
|
|
1767
|
+
...khAutoContextHooks ?? {},
|
|
1768
|
+
...autoLearningHooks ?? {}
|
|
1769
|
+
};
|
|
1770
|
+
};
|
|
1771
|
+
}
|
|
1772
|
+
var teamkitServer = createTeamkitServer({ enableDevIsolation: true });
|
|
1773
|
+
var teamkitDevServer = createTeamkitServer({ enableDevIsolation: false });
|