autosnippet 2.7.0 → 2.8.0
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 +138 -66
- package/bin/api-server.js +5 -0
- package/bin/cli.js +26 -0
- package/bin/mcp-server.js +22 -0
- package/dashboard/dist/assets/{icons-Cq4-iQhP.js → icons-B_Xg4B-s.js} +61 -61
- package/dashboard/dist/assets/index-CkIih2CC.css +1 -0
- package/dashboard/dist/assets/index-Duc8Qk-c.js +197 -0
- package/dashboard/dist/index.html +3 -3
- package/lib/bootstrap.js +17 -0
- package/lib/cli/SetupService.js +53 -0
- package/lib/external/ai/providers/ClaudeProvider.js +12 -1
- package/lib/external/ai/providers/GoogleGeminiProvider.js +13 -1
- package/lib/external/ai/providers/OpenAiProvider.js +13 -3
- package/lib/external/mcp/McpServer.js +11 -4
- package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +194 -5
- package/lib/external/mcp/handlers/bootstrap/pipeline/tier-scheduler.js +8 -10
- package/lib/external/mcp/handlers/bootstrap.js +8 -0
- package/lib/external/mcp/handlers/skill.js +202 -0
- package/lib/external/mcp/tools.js +54 -1
- package/lib/http/routes/ai.js +155 -1
- package/lib/infrastructure/config/Paths.js +3 -0
- package/lib/infrastructure/database/DatabaseConnection.js +6 -1
- package/lib/infrastructure/vector/JsonVectorAdapter.js +2 -0
- package/lib/service/automation/handlers/AlinkHandler.js +43 -4
- package/lib/service/candidate/CandidateFileWriter.js +4 -0
- package/lib/service/chat/AnalystAgent.js +37 -8
- package/lib/service/chat/CandidateGuardrail.js +3 -3
- package/lib/service/chat/ChatAgent.js +20 -1
- package/lib/service/chat/ConversationStore.js +3 -0
- package/lib/service/chat/HandoffProtocol.js +1 -0
- package/lib/service/chat/Memory.js +3 -0
- package/lib/service/chat/ProducerAgent.js +53 -0
- package/lib/service/chat/tools.js +13 -6
- package/lib/service/guard/ExclusionManager.js +2 -0
- package/lib/service/guard/RuleLearner.js +2 -0
- package/lib/service/quality/FeedbackCollector.js +2 -0
- package/lib/service/recipe/RecipeFileWriter.js +4 -0
- package/lib/service/recipe/RecipeStatsTracker.js +2 -0
- package/lib/service/skills/SignalCollector.js +2 -0
- package/lib/shared/PathGuard.js +314 -0
- package/package.json +1 -1
- package/resources/native-ui/combined-window.swift +494 -0
- package/dashboard/dist/assets/index-DBxH7pVn.css +0 -1
- package/dashboard/dist/assets/index-Dw2F6qAS.js +0 -197
|
@@ -19,8 +19,17 @@ import Logger from '../../infrastructure/logging/Logger.js';
|
|
|
19
19
|
// System Prompt — Analyst 专用 (~100 tokens)
|
|
20
20
|
// ──────────────────────────────────────────────────────────────────
|
|
21
21
|
|
|
22
|
-
const ANALYST_SYSTEM_PROMPT =
|
|
23
|
-
|
|
22
|
+
const ANALYST_SYSTEM_PROMPT = `你是一位高级软件架构师,正在深度分析一个真实项目。
|
|
23
|
+
|
|
24
|
+
## 工具使用优先级
|
|
25
|
+
1. **结构化查询优先** — get_project_overview → get_class_hierarchy → get_class_info
|
|
26
|
+
这些工具返回精确的类继承、协议实现、方法签名信息,比文本搜索更高效。
|
|
27
|
+
2. **文本搜索补充** — search_project_code 用于查找特定模式/关键字
|
|
28
|
+
3. **文件阅读验证** — read_project_file 用于确认具体实现细节
|
|
29
|
+
|
|
30
|
+
> ⚠️ 避免反复调用 search_project_code 搜索不同关键词。
|
|
31
|
+
> 如果需要了解类结构,直接用 get_class_info 查询。
|
|
32
|
+
|
|
24
33
|
输出你的分析发现,包括具体的文件路径和代码位置。
|
|
25
34
|
不需要任何特定格式,用自然语言描述你的理解即可。
|
|
26
35
|
尽可能多地使用工具来获取准确信息,不要猜测。`;
|
|
@@ -52,12 +61,12 @@ const ANALYST_TOOLS = [
|
|
|
52
61
|
// ──────────────────────────────────────────────────────────────────
|
|
53
62
|
|
|
54
63
|
const ANALYST_BUDGET = {
|
|
55
|
-
maxIterations: 20
|
|
56
|
-
searchBudget:
|
|
57
|
-
searchBudgetGrace: 10
|
|
64
|
+
maxIterations: 12, // was 20 — 多数维度 5-9 轮完成
|
|
65
|
+
searchBudget: 10, // was 15 — 探索为主,精简预算
|
|
66
|
+
searchBudgetGrace: 6, // was 10
|
|
58
67
|
maxSubmits: 0, // Analyst 不提交候选
|
|
59
68
|
softSubmitLimit: 0,
|
|
60
|
-
idleRoundsToExit: 3
|
|
69
|
+
idleRoundsToExit: 2, // was 3 — 减少空转
|
|
61
70
|
};
|
|
62
71
|
|
|
63
72
|
// ──────────────────────────────────────────────────────────────────
|
|
@@ -68,9 +77,10 @@ const ANALYST_BUDGET = {
|
|
|
68
77
|
* 构建 Analyst Prompt
|
|
69
78
|
* @param {object} dimConfig — 维度配置 { id, label, guide, focusAreas }
|
|
70
79
|
* @param {object} projectInfo — { name, lang, fileCount }
|
|
80
|
+
* @param {object} [dimensionContext] — DimensionContext 实例 (跨维度上下文)
|
|
71
81
|
* @returns {string}
|
|
72
82
|
*/
|
|
73
|
-
function buildAnalystPrompt(dimConfig, projectInfo) {
|
|
83
|
+
function buildAnalystPrompt(dimConfig, projectInfo, dimensionContext) {
|
|
74
84
|
const parts = [];
|
|
75
85
|
|
|
76
86
|
// §1 任务描述
|
|
@@ -106,6 +116,24 @@ ${depthHint}
|
|
|
106
116
|
// §5 前序上下文提示
|
|
107
117
|
parts.push('可以调用 get_previous_analysis 获取前序维度的分析结果,避免重复分析。');
|
|
108
118
|
|
|
119
|
+
// §6 前序维度分析摘要 (Tier 2+ 才有)
|
|
120
|
+
if (dimensionContext) {
|
|
121
|
+
const snapshot = dimensionContext.buildContextForDimension(dimConfig.id);
|
|
122
|
+
const prevDims = Object.entries(snapshot.previousDimensions);
|
|
123
|
+
if (prevDims.length > 0) {
|
|
124
|
+
parts.push(`## 前序维度分析摘要(避免重复探索)`);
|
|
125
|
+
for (const [dimId, digest] of prevDims) {
|
|
126
|
+
parts.push(`### ${dimId}\n${digest.summary || '(无摘要)'}`);
|
|
127
|
+
if (digest.keyFindings?.length > 0) {
|
|
128
|
+
parts.push(`关键发现: ${digest.keyFindings.join('; ')}`);
|
|
129
|
+
}
|
|
130
|
+
if (digest.crossRefs?.[dimConfig.id]) {
|
|
131
|
+
parts.push(`💡 对本维度的建议: ${digest.crossRefs[dimConfig.id]}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
109
137
|
return parts.join('\n\n');
|
|
110
138
|
}
|
|
111
139
|
|
|
@@ -146,11 +174,12 @@ export class AnalystAgent {
|
|
|
146
174
|
* @param {object} projectInfo — { name, lang, fileCount }
|
|
147
175
|
* @param {object} [options]
|
|
148
176
|
* @param {string} [options.sessionId] — Bootstrap session ID
|
|
177
|
+
* @param {object} [options.dimensionContext] — DimensionContext 实例
|
|
149
178
|
* @returns {Promise<import('./HandoffProtocol.js').AnalysisReport>}
|
|
150
179
|
*/
|
|
151
180
|
async analyze(dimConfig, projectInfo, options = {}) {
|
|
152
181
|
const dimId = dimConfig.id;
|
|
153
|
-
const prompt = buildAnalystPrompt(dimConfig, projectInfo);
|
|
182
|
+
const prompt = buildAnalystPrompt(dimConfig, projectInfo, options.dimensionContext);
|
|
154
183
|
|
|
155
184
|
this.#logger.info(`[AnalystAgent] ▶ analyzing dimension "${dimId}" — prompt ${prompt.length} chars`);
|
|
156
185
|
|
|
@@ -45,11 +45,11 @@ export class CandidateGuardrail {
|
|
|
45
45
|
return { valid: false, error: `内容过短 (${content.length} 字符, 最少 200)。请包含代码片段和项目上下文描述,而非一句话概括。` };
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
// knowledgeType 约束
|
|
48
|
+
// knowledgeType 约束 — 自动修正为允许列表中第一个类型
|
|
49
49
|
const allowed = this.#dimensionConfig.allowedKnowledgeTypes;
|
|
50
50
|
if (allowed?.length > 0 && candidate.knowledgeType) {
|
|
51
51
|
if (!allowed.includes(candidate.knowledgeType)) {
|
|
52
|
-
|
|
52
|
+
candidate.knowledgeType = allowed[0];
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
|
|
@@ -79,7 +79,7 @@ export class CandidateGuardrail {
|
|
|
79
79
|
|
|
80
80
|
// 检查是否包含代码引用或文件路径
|
|
81
81
|
const hasCodeBlock = /```[\s\S]*?```/.test(content) || /\.(m|h|swift|js|ts)(:\d+)?/.test(content);
|
|
82
|
-
const hasSourceRef =
|
|
82
|
+
const hasSourceRef = /来源[::]|[Ss]ource[::]|\(\w+\.\w+:\d+\)/.test(content) || /[A-Z]\w+\.(m|h|swift|java|kt|js|ts)/.test(content);
|
|
83
83
|
|
|
84
84
|
if (!hasCodeBlock && !hasSourceRef) {
|
|
85
85
|
return { valid: false, error: '内容缺少代码片段或文件引用 — 请用 read_project_file 获取代码后再提交,「项目特写」必须包含真实代码' };
|
|
@@ -128,6 +128,8 @@ export class ChatAgent {
|
|
|
128
128
|
#fileCache = null;
|
|
129
129
|
/** @type {Set<string>} 跨维度已提交候选标题(bootstrap 全局去重) */
|
|
130
130
|
#globalSubmittedTitles = new Set();
|
|
131
|
+
/** @type {{ input: number, output: number }} 当前 execute() 累计 token 用量 */
|
|
132
|
+
#currentTokenUsage = { input: 0, output: 0 };
|
|
131
133
|
|
|
132
134
|
/**
|
|
133
135
|
* @param {object} opts
|
|
@@ -199,6 +201,7 @@ export class ChatAgent {
|
|
|
199
201
|
temperature: temperatureOverride, // 覆盖默认温度
|
|
200
202
|
} = {}) {
|
|
201
203
|
this.#currentSource = source;
|
|
204
|
+
this.#currentTokenUsage = { input: 0, output: 0 };
|
|
202
205
|
const execStartTime = Date.now();
|
|
203
206
|
const promptPreview = prompt.length > 80 ? prompt.substring(0, 80) + '…' : prompt;
|
|
204
207
|
this.#logger.info(`[ChatAgent] ▶ execute — source=${source}${dimensionMeta?.id ? ', dim=' + dimensionMeta.id + '(' + dimensionMeta.outputType + ')' : (dimensionId ? ', dim=' + dimensionId : '')}, prompt="${promptPreview}", historyLen=${history.length}${conversationId ? ', convId=' + conversationId.substring(0, 8) : ''}`);
|
|
@@ -247,6 +250,9 @@ export class ChatAgent {
|
|
|
247
250
|
|
|
248
251
|
this.#extractMemory(prompt, result.reply);
|
|
249
252
|
|
|
253
|
+
// 附加 token 用量统计
|
|
254
|
+
result.tokenUsage = { ...this.#currentTokenUsage };
|
|
255
|
+
|
|
250
256
|
return { ...result, conversationId };
|
|
251
257
|
}
|
|
252
258
|
|
|
@@ -320,6 +326,7 @@ export class ChatAgent {
|
|
|
320
326
|
let consecutiveEmptyResponses = 0;
|
|
321
327
|
let iterationCount = 0; // v3: 独立迭代计数器
|
|
322
328
|
const submittedTitles = new Set(this.#globalSubmittedTitles);
|
|
329
|
+
const sharedState = {}; // P2.2: 跨工具调用共享状态(搜索计数器等)
|
|
323
330
|
|
|
324
331
|
// ── 主循环 ──
|
|
325
332
|
while (true) {
|
|
@@ -382,6 +389,13 @@ export class ChatAgent {
|
|
|
382
389
|
});
|
|
383
390
|
|
|
384
391
|
const aiDuration = Date.now() - iterStartTime;
|
|
392
|
+
|
|
393
|
+
// 累计 token 用量
|
|
394
|
+
if (aiResult.usage) {
|
|
395
|
+
this.#currentTokenUsage.input += aiResult.usage.inputTokens || 0;
|
|
396
|
+
this.#currentTokenUsage.output += aiResult.usage.outputTokens || 0;
|
|
397
|
+
}
|
|
398
|
+
|
|
385
399
|
if (aiResult.functionCalls?.length > 0) {
|
|
386
400
|
this.#logger.info(`[ChatAgent] ✓ AI returned ${aiResult.functionCalls.length} function calls in ${aiDuration}ms: [${aiResult.functionCalls.map(fc => fc.name).join(', ')}]`);
|
|
387
401
|
} else {
|
|
@@ -446,7 +460,7 @@ export class ChatAgent {
|
|
|
446
460
|
toolResult = await this.#toolRegistry.execute(
|
|
447
461
|
fc.name,
|
|
448
462
|
fc.args,
|
|
449
|
-
this.#getToolContext({ _sessionToolCalls: toolCalls, _dimensionMeta: dimensionMeta, _submittedTitles: submittedTitles }),
|
|
463
|
+
this.#getToolContext({ _sessionToolCalls: toolCalls, _dimensionMeta: dimensionMeta, _submittedTitles: submittedTitles, _sharedState: sharedState }),
|
|
450
464
|
);
|
|
451
465
|
const toolDuration = Date.now() - toolStartTime;
|
|
452
466
|
const resultSize = typeof toolResult === 'string' ? toolResult.length : JSON.stringify(toolResult).length;
|
|
@@ -662,6 +676,11 @@ ${submitSummary ? `已提交候选:\n${submitSummary}\n` : ''}
|
|
|
662
676
|
maxTokens: 8192,
|
|
663
677
|
},
|
|
664
678
|
);
|
|
679
|
+
// 累计 token 用量
|
|
680
|
+
if (summaryResult.usage) {
|
|
681
|
+
this.#currentTokenUsage.input += summaryResult.usage.inputTokens || 0;
|
|
682
|
+
this.#currentTokenUsage.output += summaryResult.usage.outputTokens || 0;
|
|
683
|
+
}
|
|
665
684
|
finalReply = this.#cleanFinalAnswer(summaryResult.text || '');
|
|
666
685
|
} catch (err) {
|
|
667
686
|
this.#logger.warn(`[ChatAgent] forced summary AI call failed: ${err.message}`);
|
|
@@ -22,6 +22,7 @@ import fs from 'node:fs';
|
|
|
22
22
|
import path from 'node:path';
|
|
23
23
|
import crypto from 'node:crypto';
|
|
24
24
|
import Logger from '../../infrastructure/logging/Logger.js';
|
|
25
|
+
import pathGuard from '../../shared/PathGuard.js';
|
|
25
26
|
|
|
26
27
|
const DEFAULT_TOKEN_BUDGET = 12000; // ~12K tokens 留给历史, 其余给系统提示词和当前消息
|
|
27
28
|
const CHARS_PER_TOKEN = 3.5; // 近似: 中文 ~3.5 / 英文 ~4 / 取偏保守值
|
|
@@ -40,6 +41,8 @@ export class ConversationStore {
|
|
|
40
41
|
this.#dir = path.join(projectRoot, '.autosnippet', 'conversations');
|
|
41
42
|
this.#indexPath = path.join(this.#dir, 'index.json');
|
|
42
43
|
this.#logger = Logger.getInstance();
|
|
44
|
+
// 路径安全检查
|
|
45
|
+
pathGuard.assertProjectWriteSafe(this.#dir);
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
// ═══════════════════════════════════════════════════════
|
|
@@ -92,6 +92,7 @@ export function buildAnalysisReport(analystResult, dimensionId, projectGraph = n
|
|
|
92
92
|
metadata: {
|
|
93
93
|
iterations: analystResult.toolCalls?.length || 0,
|
|
94
94
|
toolCallCount: analystResult.toolCalls?.length || 0,
|
|
95
|
+
tokenUsage: analystResult.tokenUsage || null,
|
|
95
96
|
},
|
|
96
97
|
};
|
|
97
98
|
}
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
|
|
25
25
|
import fs from 'node:fs';
|
|
26
26
|
import path from 'node:path';
|
|
27
|
+
import pathGuard from '../../shared/PathGuard.js';
|
|
27
28
|
|
|
28
29
|
export class Memory {
|
|
29
30
|
#filePath;
|
|
@@ -37,6 +38,8 @@ export class Memory {
|
|
|
37
38
|
constructor(projectRoot, { maxEntries = 50 } = {}) {
|
|
38
39
|
this.#filePath = path.join(projectRoot, '.autosnippet', 'memory.jsonl');
|
|
39
40
|
this.#maxEntries = maxEntries;
|
|
41
|
+
// 路径安全检查
|
|
42
|
+
pathGuard.assertProjectWriteSafe(this.#filePath);
|
|
40
43
|
}
|
|
41
44
|
|
|
42
45
|
/**
|
|
@@ -215,6 +215,58 @@ export class ProducerAgent {
|
|
|
215
215
|
}).length;
|
|
216
216
|
const rejectedCount = submitCalls.length - successCount;
|
|
217
217
|
|
|
218
|
+
// ── 拒绝率过高时追加一轮修正尝试 ──
|
|
219
|
+
if (rejectedCount > successCount && rejectedCount >= 2 && options.retryOnRejection !== false) {
|
|
220
|
+
this.#logger.info(`[ProducerAgent] 高拒绝率 (${rejectedCount}/${submitCalls.length}) — 尝试修正`);
|
|
221
|
+
try {
|
|
222
|
+
const retryPrompt = `你的 ${rejectedCount} 个提交被拒绝了。请根据拒绝原因改进后重新提交,确保:\n1. code 字段 ≥ 200 字符\n2. 包含代码块 (\`\`\`)\n3. 包含来源标注 (来源: FileName.m:行号)\n4. 标题使用项目真实类名`;
|
|
223
|
+
const retryResult = await this.#chatAgent.execute(retryPrompt, {
|
|
224
|
+
source: 'system',
|
|
225
|
+
conversationId: options.sessionId ? `producer-retry-${options.sessionId}-${dimId}` : undefined,
|
|
226
|
+
budget: { ...budget, maxIterations: 5, maxSubmits: rejectedCount },
|
|
227
|
+
systemPromptOverride: PRODUCER_SYSTEM_PROMPT,
|
|
228
|
+
allowedTools: PRODUCER_TOOLS,
|
|
229
|
+
disablePhaseRouter: true,
|
|
230
|
+
temperature: 0.3,
|
|
231
|
+
dimensionMeta: {
|
|
232
|
+
id: dimId,
|
|
233
|
+
outputType: dimConfig.outputType || 'candidate',
|
|
234
|
+
allowedKnowledgeTypes: dimConfig.allowedKnowledgeTypes || [],
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const retrySubmits = (retryResult.toolCalls || []).filter(
|
|
239
|
+
tc => (tc.tool || tc.name) === 'submit_candidate' || (tc.tool || tc.name) === 'submit_with_check'
|
|
240
|
+
);
|
|
241
|
+
const retrySuccess = retrySubmits.filter(tc => {
|
|
242
|
+
const res = tc.result;
|
|
243
|
+
if (!res) return true;
|
|
244
|
+
if (typeof res === 'string') return !res.includes('rejected') && !res.includes('error');
|
|
245
|
+
return res.status !== 'rejected' && res.status !== 'error';
|
|
246
|
+
}).length;
|
|
247
|
+
|
|
248
|
+
this.#logger.info(`[ProducerAgent] 修正轮: +${retrySuccess} candidates for "${dimId}"`);
|
|
249
|
+
|
|
250
|
+
// 合并两轮 token 用量
|
|
251
|
+
const baseTokens = result.tokenUsage || { input: 0, output: 0 };
|
|
252
|
+
const retryTokens = retryResult.tokenUsage || { input: 0, output: 0 };
|
|
253
|
+
const mergedTokenUsage = {
|
|
254
|
+
input: (baseTokens.input || 0) + (retryTokens.input || 0),
|
|
255
|
+
output: (baseTokens.output || 0) + (retryTokens.output || 0),
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
candidateCount: successCount + retrySuccess,
|
|
260
|
+
rejectedCount: rejectedCount - retrySuccess,
|
|
261
|
+
toolCalls: [...(result.toolCalls || []), ...(retryResult.toolCalls || [])],
|
|
262
|
+
reply: retryResult.reply || result.reply || '',
|
|
263
|
+
tokenUsage: mergedTokenUsage,
|
|
264
|
+
};
|
|
265
|
+
} catch (retryErr) {
|
|
266
|
+
this.#logger.warn(`[ProducerAgent] 修正轮失败 "${dimId}": ${retryErr.message}`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
218
270
|
this.#logger.info(`[ProducerAgent] ✅ dimension "${dimId}" — ${successCount} candidates created (${rejectedCount} rejected), ${result.toolCalls?.length || 0} total tool calls`);
|
|
219
271
|
|
|
220
272
|
return {
|
|
@@ -222,6 +274,7 @@ export class ProducerAgent {
|
|
|
222
274
|
rejectedCount,
|
|
223
275
|
toolCalls: result.toolCalls || [],
|
|
224
276
|
reply: result.reply || '',
|
|
277
|
+
tokenUsage: result.tokenUsage || null,
|
|
225
278
|
};
|
|
226
279
|
} catch (err) {
|
|
227
280
|
this.#logger.error(`[ProducerAgent] ❌ dimension "${dimId}" error: ${err.message}`);
|
|
@@ -258,6 +258,15 @@ const searchProjectCode = {
|
|
|
258
258
|
total,
|
|
259
259
|
searchedFiles: files.length,
|
|
260
260
|
skippedThirdParty,
|
|
261
|
+
...((() => {
|
|
262
|
+
// P2.2: 搜索超限提示 — 引导使用 AST 工具
|
|
263
|
+
const state = ctx._sharedState || ctx;
|
|
264
|
+
state._searchCallCount = (state._searchCallCount || 0) + 1;
|
|
265
|
+
if (state._searchCallCount > 8 && ctx.source === 'system') {
|
|
266
|
+
return { hint: `💡 你已搜索 ${state._searchCallCount} 次。考虑使用 get_class_info / get_class_hierarchy / get_project_overview 获取结构化信息,效率更高。` };
|
|
267
|
+
}
|
|
268
|
+
return {};
|
|
269
|
+
})()),
|
|
261
270
|
};
|
|
262
271
|
},
|
|
263
272
|
};
|
|
@@ -1547,15 +1556,13 @@ function _checkDimensionType(dimensionMeta, params, logger) {
|
|
|
1547
1556
|
};
|
|
1548
1557
|
}
|
|
1549
1558
|
|
|
1550
|
-
// 2. knowledgeType 校验 —
|
|
1559
|
+
// 2. knowledgeType 校验 — 不在允许列表时自动修正为第一个允许类型
|
|
1551
1560
|
const allowed = dimensionMeta.allowedKnowledgeTypes || [];
|
|
1552
1561
|
if (allowed.length > 0 && params.knowledgeType) {
|
|
1553
1562
|
if (!allowed.includes(params.knowledgeType)) {
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
reason: `knowledgeType "${params.knowledgeType}" 不在维度 "${dimensionMeta.id}" 允许的类型列表中。允许的类型: ${allowed.join(', ')}`,
|
|
1558
|
-
};
|
|
1563
|
+
const corrected = allowed[0];
|
|
1564
|
+
logger?.warn(`[submit_candidate] knowledgeType "${params.knowledgeType}" → "${corrected}" (auto-corrected for dimension "${dimensionMeta.id}")`);
|
|
1565
|
+
params.knowledgeType = corrected;
|
|
1559
1566
|
}
|
|
1560
1567
|
}
|
|
1561
1568
|
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from 'node:fs';
|
|
8
8
|
import { join, dirname, relative } from 'node:path';
|
|
9
9
|
import Logger from '../../infrastructure/logging/Logger.js';
|
|
10
|
+
import pathGuard from '../../shared/PathGuard.js';
|
|
10
11
|
|
|
11
12
|
export class ExclusionManager {
|
|
12
13
|
#exclusionsPath;
|
|
@@ -15,6 +16,7 @@ export class ExclusionManager {
|
|
|
15
16
|
constructor(projectRoot, options = {}) {
|
|
16
17
|
const kbDir = options.knowledgeBaseDir || 'AutoSnippet';
|
|
17
18
|
this.#exclusionsPath = join(projectRoot, kbDir, 'guard-exclusions.json');
|
|
19
|
+
pathGuard.assertProjectWriteSafe(this.#exclusionsPath);
|
|
18
20
|
// 迁移旧路径
|
|
19
21
|
this.#migrateOldPath(projectRoot, options.internalDir || '.autosnippet');
|
|
20
22
|
this.#data = this.#load();
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from 'node:fs';
|
|
8
8
|
import { join, dirname } from 'node:path';
|
|
9
9
|
import Logger from '../../infrastructure/logging/Logger.js';
|
|
10
|
+
import pathGuard from '../../shared/PathGuard.js';
|
|
10
11
|
|
|
11
12
|
const PROBLEMATIC_THRESHOLD = { falsePositiveRate: 0.3, minTriggers: 5 };
|
|
12
13
|
|
|
@@ -17,6 +18,7 @@ export class RuleLearner {
|
|
|
17
18
|
constructor(projectRoot, options = {}) {
|
|
18
19
|
const kbDir = options.knowledgeBaseDir || 'AutoSnippet';
|
|
19
20
|
this.#learnerPath = join(projectRoot, kbDir, 'guard-learner.json');
|
|
21
|
+
pathGuard.assertProjectWriteSafe(this.#learnerPath);
|
|
20
22
|
this.#migrateOldPath(projectRoot, options.internalDir || '.autosnippet');
|
|
21
23
|
this.#data = this.#load();
|
|
22
24
|
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from 'node:fs';
|
|
8
8
|
import { join, dirname } from 'node:path';
|
|
9
|
+
import pathGuard from '../../shared/PathGuard.js';
|
|
9
10
|
|
|
10
11
|
export class FeedbackCollector {
|
|
11
12
|
#feedbackPath;
|
|
@@ -15,6 +16,7 @@ export class FeedbackCollector {
|
|
|
15
16
|
constructor(projectRoot, options = {}) {
|
|
16
17
|
const kbDir = options.knowledgeBaseDir || 'AutoSnippet';
|
|
17
18
|
this.#feedbackPath = join(projectRoot, kbDir, 'feedback.json');
|
|
19
|
+
pathGuard.assertProjectWriteSafe(this.#feedbackPath);
|
|
18
20
|
this.#maxEvents = options.maxEvents || 1000;
|
|
19
21
|
this.#migrateOldPath(projectRoot, options.internalDir || '.autosnippet');
|
|
20
22
|
this.#events = this.#load();
|
|
@@ -19,6 +19,7 @@ import path from 'node:path';
|
|
|
19
19
|
import { createHash } from 'node:crypto';
|
|
20
20
|
import { RECIPES_DIR } from '../../infrastructure/config/Defaults.js';
|
|
21
21
|
import Logger from '../../infrastructure/logging/Logger.js';
|
|
22
|
+
import pathGuard from '../../shared/PathGuard.js';
|
|
22
23
|
|
|
23
24
|
export class RecipeFileWriter {
|
|
24
25
|
/**
|
|
@@ -160,6 +161,9 @@ export class RecipeFileWriter {
|
|
|
160
161
|
const category = (recipe.category || 'general').toLowerCase();
|
|
161
162
|
const categoryDir = path.join(this.recipesDir, category);
|
|
162
163
|
|
|
164
|
+
// 路径安全检查 — 阻止 category 含 ../ 导致路径逃逸
|
|
165
|
+
pathGuard.assertProjectWriteSafe(categoryDir);
|
|
166
|
+
|
|
163
167
|
if (!fs.existsSync(categoryDir)) {
|
|
164
168
|
fs.mkdirSync(categoryDir, { recursive: true });
|
|
165
169
|
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from 'node:fs';
|
|
8
8
|
import { join, dirname } from 'node:path';
|
|
9
9
|
import Logger from '../../infrastructure/logging/Logger.js';
|
|
10
|
+
import pathGuard from '../../shared/PathGuard.js';
|
|
10
11
|
|
|
11
12
|
const SCHEMA_VERSION = 1;
|
|
12
13
|
const DEFAULT_HEAT_WEIGHTS = { guard: 1.0, human: 2.0, ai: 1.5 };
|
|
@@ -19,6 +20,7 @@ export class RecipeStatsTracker {
|
|
|
19
20
|
constructor(projectRoot, options = {}) {
|
|
20
21
|
const kbDir = options.knowledgeBaseDir || 'AutoSnippet';
|
|
21
22
|
this.#statsPath = join(projectRoot, kbDir, 'recipe-stats.json');
|
|
23
|
+
pathGuard.assertProjectWriteSafe(this.#statsPath);
|
|
22
24
|
this.#migrateOldPath(projectRoot, options.internalDir || '.autosnippet');
|
|
23
25
|
this.#data = this.#load();
|
|
24
26
|
}
|
|
@@ -42,6 +42,7 @@ import path from 'node:path';
|
|
|
42
42
|
import { execSync } from 'node:child_process';
|
|
43
43
|
import Logger from '../../infrastructure/logging/Logger.js';
|
|
44
44
|
import { EventAggregator } from './EventAggregator.js';
|
|
45
|
+
import pathGuard from '../../shared/PathGuard.js';
|
|
45
46
|
|
|
46
47
|
const DEFAULT_INTERVAL_MS = 60 * 60 * 1000; // 1 小时(初始值,AI 可动态调整)
|
|
47
48
|
const MIN_INTERVAL_MS = 5 * 60 * 1000; // 最短 5 分钟
|
|
@@ -525,6 +526,7 @@ ${JSON.stringify(signals.codeChanges, null, 2)}
|
|
|
525
526
|
}
|
|
526
527
|
|
|
527
528
|
const dir = path.dirname(this.#snapshotPath);
|
|
529
|
+
pathGuard.assertProjectWriteSafe(dir);
|
|
528
530
|
if (!fs.existsSync(dir)) {
|
|
529
531
|
fs.mkdirSync(dir, { recursive: true });
|
|
530
532
|
}
|