autosnippet 2.6.0 → 2.7.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/README.md +137 -65
- package/bin/api-server.js +5 -0
- package/bin/cli.js +6 -1
- package/dashboard/dist/assets/{icons-rnn04CvH.js → icons-B_Xg4B-s.js} +148 -88
- package/dashboard/dist/assets/index-BjfUm8p9.js +197 -0
- package/dashboard/dist/assets/index-CkIih2CC.css +1 -0
- package/dashboard/dist/assets/{react-markdown-CWxUbOf4.js → react-markdown-BA6FB2NP.js} +1 -1
- package/dashboard/dist/assets/{syntax-highlighter-CJ2drQQb.js → syntax-highlighter-CVLHn9O5.js} +1 -1
- package/dashboard/dist/assets/{vendor-f83ah6cm.js → vendor-BotF760a.js} +61 -61
- package/dashboard/dist/index.html +6 -6
- package/lib/bootstrap.js +18 -1
- package/lib/cli/SetupService.js +86 -8
- package/lib/cli/UpgradeService.js +139 -2
- package/lib/core/ast/ProjectGraph.js +599 -0
- package/lib/core/gateway/GatewayActionRegistry.js +2 -2
- package/lib/domain/recipe/Recipe.js +3 -0
- package/lib/external/ai/AiProvider.js +83 -20
- package/lib/external/ai/providers/ClaudeProvider.js +208 -0
- package/lib/external/ai/providers/GoogleGeminiProvider.js +247 -1
- package/lib/external/ai/providers/OpenAiProvider.js +141 -0
- package/lib/external/mcp/McpServer.js +6 -1
- package/lib/external/mcp/handlers/bootstrap/pipeline/dimension-context.js +216 -0
- package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +657 -0
- package/lib/external/mcp/handlers/bootstrap/pipeline/tier-scheduler.js +160 -0
- package/lib/external/mcp/handlers/bootstrap/skills.js +225 -0
- package/lib/external/mcp/handlers/bootstrap.js +159 -1634
- package/lib/external/mcp/handlers/browse.js +1 -1
- package/lib/external/mcp/handlers/candidate.js +1 -33
- package/lib/external/mcp/handlers/skill.js +58 -17
- package/lib/external/mcp/tools.js +4 -3
- package/lib/http/middleware/requestLogger.js +23 -4
- package/lib/http/routes/ai.js +158 -2
- package/lib/http/routes/auth.js +3 -2
- package/lib/http/routes/candidates.js +49 -25
- package/lib/http/routes/commands.js +0 -8
- package/lib/http/routes/guardRules.js +1 -16
- package/lib/http/routes/recipes.js +4 -17
- package/lib/http/routes/search.js +11 -19
- package/lib/http/routes/skills.js +2 -0
- package/lib/http/routes/snippets.js +0 -33
- package/lib/http/routes/spm.js +37 -63
- package/lib/http/utils/routeHelpers.js +31 -0
- package/lib/infrastructure/config/Paths.js +12 -0
- package/lib/infrastructure/database/DatabaseConnection.js +6 -1
- package/lib/infrastructure/logging/Logger.js +86 -3
- package/lib/infrastructure/realtime/RealtimeService.js +2 -5
- package/lib/infrastructure/vector/JsonVectorAdapter.js +26 -1
- package/lib/injection/ServiceContainer.js +55 -2
- package/lib/service/bootstrap/BootstrapTaskManager.js +400 -0
- package/lib/service/candidate/CandidateFileWriter.js +72 -27
- package/lib/service/candidate/CandidateService.js +156 -10
- package/lib/service/chat/AnalystAgent.js +245 -0
- package/lib/service/chat/CandidateGuardrail.js +134 -0
- package/lib/service/chat/ChatAgent.js +1055 -167
- package/lib/service/chat/ContextWindow.js +730 -0
- package/lib/service/chat/ConversationStore.js +3 -0
- package/lib/service/chat/HandoffProtocol.js +181 -0
- package/lib/service/chat/Memory.js +3 -0
- package/lib/service/chat/ProducerAgent.js +293 -0
- package/lib/service/chat/ToolRegistry.js +149 -5
- package/lib/service/chat/tools.js +1404 -61
- 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 +16 -1
- package/lib/service/recipe/RecipeStatsTracker.js +2 -0
- package/lib/service/skills/SignalCollector.js +33 -6
- package/lib/service/skills/SkillAdvisor.js +2 -1
- package/lib/service/skills/SkillHooks.js +13 -5
- package/lib/service/spm/SpmService.js +2 -2
- package/lib/shared/PathGuard.js +314 -0
- package/package.json +1 -1
- package/resources/native-ui/combined-window.swift +494 -0
- package/templates/copilot-instructions.md +20 -3
- package/templates/cursor-rules/autosnippet-conventions.mdc +21 -4
- package/templates/cursor-rules/autosnippet-skills.mdc +45 -0
- package/dashboard/dist/assets/index-BBKa3Dgi.js +0 -195
- package/dashboard/dist/assets/index-DLsECfzW.css +0 -1
|
@@ -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
|
}
|
|
@@ -300,7 +304,18 @@ export class RecipeFileWriter {
|
|
|
300
304
|
}
|
|
301
305
|
if (recipe.content?.steps?.length > 0) {
|
|
302
306
|
parts.push('### 使用步骤\n');
|
|
303
|
-
recipe.content.steps.forEach((step, i) => {
|
|
307
|
+
recipe.content.steps.forEach((step, i) => {
|
|
308
|
+
if (typeof step === 'string') {
|
|
309
|
+
parts.push(`${i + 1}. ${step}`);
|
|
310
|
+
} else {
|
|
311
|
+
const title = step.title || '';
|
|
312
|
+
const desc = step.description || '';
|
|
313
|
+
const code = step.code || '';
|
|
314
|
+
const text = [title, desc].filter(Boolean).join(': ');
|
|
315
|
+
parts.push(`${i + 1}. ${text || '(步骤)'}`);
|
|
316
|
+
if (code) { parts.push(''); parts.push(`\`\`\`\n${code}\n\`\`\``); }
|
|
317
|
+
}
|
|
318
|
+
});
|
|
304
319
|
parts.push('');
|
|
305
320
|
}
|
|
306
321
|
if (recipe.constraints?.boundaries?.length > 0) {
|
|
@@ -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 分钟
|
|
@@ -76,14 +77,14 @@ export class SignalCollector {
|
|
|
76
77
|
projectRoot,
|
|
77
78
|
database = null,
|
|
78
79
|
chatAgent = null,
|
|
79
|
-
mode = '
|
|
80
|
+
mode = 'auto',
|
|
80
81
|
intervalMs = DEFAULT_INTERVAL_MS,
|
|
81
82
|
onSuggestions = null,
|
|
82
83
|
}) {
|
|
83
84
|
this.#projectRoot = projectRoot;
|
|
84
85
|
this.#db = database;
|
|
85
86
|
this.#chatAgent = chatAgent;
|
|
86
|
-
this.#mode = ['off', 'suggest', 'auto'].includes(mode) ? mode : '
|
|
87
|
+
this.#mode = ['off', 'suggest', 'auto'].includes(mode) ? mode : 'auto';
|
|
87
88
|
this.#intervalMs = Math.max(Math.min(intervalMs, MAX_INTERVAL_MS), MIN_INTERVAL_MS);
|
|
88
89
|
this.#logger = Logger.getInstance();
|
|
89
90
|
this.#onSuggestions = onSuggestions;
|
|
@@ -160,6 +161,16 @@ export class SignalCollector {
|
|
|
160
161
|
getSnapshot() { return { ...this.#snapshot }; }
|
|
161
162
|
getMode() { return this.#mode; }
|
|
162
163
|
|
|
164
|
+
/** 从 pendingSuggestions 中移除已创建的 Skill */
|
|
165
|
+
removePendingSuggestion(name) {
|
|
166
|
+
if (!this.#snapshot.pendingSuggestions?.length) return;
|
|
167
|
+
this.#snapshot.pendingSuggestions = this.#snapshot.pendingSuggestions.filter(s => s.name !== name);
|
|
168
|
+
if (this.#snapshot.lastResult) {
|
|
169
|
+
this.#snapshot.lastResult.newSuggestions = this.#snapshot.pendingSuggestions.length;
|
|
170
|
+
}
|
|
171
|
+
this.#saveSnapshot();
|
|
172
|
+
}
|
|
173
|
+
|
|
163
174
|
setMode(mode) {
|
|
164
175
|
if (!['off', 'suggest', 'auto'].includes(mode)) return;
|
|
165
176
|
this.#mode = mode;
|
|
@@ -211,6 +222,17 @@ export class SignalCollector {
|
|
|
211
222
|
newSuggestions: newSuggestions.length,
|
|
212
223
|
aiToolCalls: toolCalls?.length || 0,
|
|
213
224
|
};
|
|
225
|
+
// 持久化 AI 生成的建议,供前端直接读取
|
|
226
|
+
if (newSuggestions.length > 0) {
|
|
227
|
+
this.#snapshot.pendingSuggestions = newSuggestions.map(s => ({
|
|
228
|
+
name: s.name,
|
|
229
|
+
description: s.description || s.reason || '',
|
|
230
|
+
rationale: s.rationale || s.reason || '',
|
|
231
|
+
body: s.body || '',
|
|
232
|
+
source: s.source || 'signal-collector',
|
|
233
|
+
priority: s.priority || 'medium',
|
|
234
|
+
}));
|
|
235
|
+
}
|
|
214
236
|
|
|
215
237
|
if (newSuggestions.length > 0) {
|
|
216
238
|
for (const s of newSuggestions) {
|
|
@@ -403,14 +425,16 @@ ${JSON.stringify(signals.codeChanges, null, 2)}
|
|
|
403
425
|
|
|
404
426
|
1. 综合分析以上 6 个维度的信号
|
|
405
427
|
2. 识别重复模式、高频错误、未覆盖的操作
|
|
406
|
-
3.
|
|
407
|
-
4.
|
|
408
|
-
5.
|
|
428
|
+
3. **只推荐项目特有的知识模式**,不要推荐通用编程知识(如 Git 基础、语言语法等)
|
|
429
|
+
4. 推荐的 Skill 应该能固化团队/项目的独有约定、架构决策或反复出现的问题解决方案
|
|
430
|
+
5. 根据信号密度判断下次分析应间隔多久(5-1440 分钟)
|
|
431
|
+
6. 给出简要分析摘要
|
|
432
|
+
7. 如果没有发现值得推荐的项目特有模式,返回空的 suggestions 数组
|
|
409
433
|
|
|
410
434
|
## 输出格式
|
|
411
435
|
|
|
412
436
|
在你的回复最后一行,输出一个 JSON 对象(不要包在 markdown code block 中):
|
|
413
|
-
{"suggestions":[{"name":"skill-name","reason":"推荐原因","priority":"high|medium|low","body":"推荐的 Skill 内容"}],"nextIntervalMinutes":60,"summary":"一句话分析摘要"}`;
|
|
437
|
+
{"suggestions":[{"name":"skill-name","description":"一句话中文描述","reason":"推荐原因","priority":"high|medium|low","body":"推荐的 Skill 内容"}],"nextIntervalMinutes":60,"summary":"一句话分析摘要"}`;
|
|
414
438
|
}
|
|
415
439
|
|
|
416
440
|
// ═══════════════════════════════════════════════════════
|
|
@@ -473,6 +497,7 @@ ${JSON.stringify(signals.codeChanges, null, 2)}
|
|
|
473
497
|
lastResult: data.lastResult || null,
|
|
474
498
|
lastAiSummary: data.lastAiSummary || '',
|
|
475
499
|
autoCreated: Array.isArray(data.autoCreated) ? data.autoCreated : [],
|
|
500
|
+
pendingSuggestions: Array.isArray(data.pendingSuggestions) ? data.pendingSuggestions : [],
|
|
476
501
|
};
|
|
477
502
|
}
|
|
478
503
|
} catch { /* corrupt — reset */ }
|
|
@@ -484,6 +509,7 @@ ${JSON.stringify(signals.codeChanges, null, 2)}
|
|
|
484
509
|
lastResult: null,
|
|
485
510
|
lastAiSummary: '',
|
|
486
511
|
autoCreated: [],
|
|
512
|
+
pendingSuggestions: [],
|
|
487
513
|
};
|
|
488
514
|
}
|
|
489
515
|
|
|
@@ -500,6 +526,7 @@ ${JSON.stringify(signals.codeChanges, null, 2)}
|
|
|
500
526
|
}
|
|
501
527
|
|
|
502
528
|
const dir = path.dirname(this.#snapshotPath);
|
|
529
|
+
pathGuard.assertProjectWriteSafe(dir);
|
|
503
530
|
if (!fs.existsSync(dir)) {
|
|
504
531
|
fs.mkdirSync(dir, { recursive: true });
|
|
505
532
|
}
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
import fs from 'node:fs';
|
|
19
19
|
import path from 'node:path';
|
|
20
|
+
import { getProjectSkillsPath } from '../../infrastructure/config/Paths.js';
|
|
20
21
|
|
|
21
22
|
export class SkillAdvisor {
|
|
22
23
|
#projectRoot;
|
|
@@ -299,7 +300,7 @@ export class SkillAdvisor {
|
|
|
299
300
|
*/
|
|
300
301
|
#listExistingProjectSkills() {
|
|
301
302
|
const names = new Set();
|
|
302
|
-
const dir =
|
|
303
|
+
const dir = getProjectSkillsPath(this.#projectRoot);
|
|
303
304
|
try {
|
|
304
305
|
fs.readdirSync(dir, { withFileTypes: true })
|
|
305
306
|
.filter(d => d.isDirectory())
|
|
@@ -10,18 +10,26 @@
|
|
|
10
10
|
* - onGuardCheck(violation, ctx) → violation (可修改)
|
|
11
11
|
* - onBootstrapComplete(stats, ctx) → void
|
|
12
12
|
*
|
|
13
|
-
* 加载顺序: 内置 skills/ → 项目级
|
|
13
|
+
* 加载顺序: 内置 skills/ → 项目级 AutoSnippet/skills/(同名覆盖)
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import fs from 'node:fs';
|
|
17
17
|
import path from 'node:path';
|
|
18
18
|
import { fileURLToPath } from 'node:url';
|
|
19
19
|
import Logger from '../../infrastructure/logging/Logger.js';
|
|
20
|
+
import { getProjectSkillsPath } from '../../infrastructure/config/Paths.js';
|
|
20
21
|
|
|
21
22
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
const SKILLS_DIR = path.resolve(__dirname, '../../../skills');
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 获取项目级 Skills 目录(运行时动态解析)
|
|
27
|
+
* 路径: {projectRoot}/AutoSnippet/skills/
|
|
28
|
+
*/
|
|
29
|
+
function _getProjectSkillsDir() {
|
|
30
|
+
const projectRoot = process.env.ASD_PROJECT_DIR || process.cwd();
|
|
31
|
+
return getProjectSkillsPath(projectRoot);
|
|
32
|
+
}
|
|
25
33
|
|
|
26
34
|
const HOOK_NAMES = [
|
|
27
35
|
'onCandidateSubmit',
|
|
@@ -48,7 +56,7 @@ export class SkillHooks {
|
|
|
48
56
|
await this.#loadFromDir(SKILLS_DIR, loaded);
|
|
49
57
|
|
|
50
58
|
// 2. 项目级 skills(覆盖同名)
|
|
51
|
-
await this.#loadFromDir(
|
|
59
|
+
await this.#loadFromDir(_getProjectSkillsDir(), loaded);
|
|
52
60
|
|
|
53
61
|
// 3. 注册所有钩子
|
|
54
62
|
for (const [skillName, mod] of loaded) {
|
|
@@ -650,7 +650,7 @@ export class SpmService {
|
|
|
650
650
|
const CODE_EXTS = isDirectoryTarget
|
|
651
651
|
? new Set(['.swift', '.m', '.mm', '.h', '.c', '.cpp', '.js', '.ts', '.tsx', '.jsx', '.py', '.java', '.kt', '.go', '.rs', '.rb', '.vue', '.mjs', '.cjs'])
|
|
652
652
|
: new Set(['.swift', '.m', '.h', '.c', '.cpp', '.mm']);
|
|
653
|
-
const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.next', 'Pods', '.build', 'DerivedData', 'vendor', '__pycache__', '.venv', 'target']);
|
|
653
|
+
const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.next', 'Pods', 'Carthage', '.build', 'DerivedData', 'vendor', '__pycache__', '.venv', 'target']);
|
|
654
654
|
const MAX_FILES = 300;
|
|
655
655
|
|
|
656
656
|
const files = [];
|
|
@@ -889,7 +889,7 @@ export class SpmService {
|
|
|
889
889
|
// 非 SPM 项目:直接扫描常见源码目录(fallback)
|
|
890
890
|
this.#logger.info('[SpmService] scanProject: No SPM targets, falling back to directory scan');
|
|
891
891
|
const CODE_EXTS = new Set(['.swift', '.m', '.mm', '.h', '.js', '.ts', '.tsx', '.jsx', '.py', '.java', '.kt', '.go', '.rs', '.rb', '.vue', '.mjs', '.cjs']);
|
|
892
|
-
const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.next', 'Pods', '.build', 'DerivedData', 'vendor', '__pycache__', '.venv', 'target']);
|
|
892
|
+
const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.next', 'Pods', 'Carthage', '.build', 'DerivedData', 'vendor', '__pycache__', '.venv', 'target']);
|
|
893
893
|
const srcDirs = ['Sources', 'src', 'lib', 'app', 'pages', 'components', 'modules', 'packages'];
|
|
894
894
|
|
|
895
895
|
const walkDir = (dir, targetName) => {
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PathGuard — 文件写入路径安全守卫(双层防护)
|
|
3
|
+
*
|
|
4
|
+
* 防止 AutoSnippet 在项目目录之外 或 项目内非法位置 创建文件。
|
|
5
|
+
* BiliDemo/data 事件的根因:process.cwd() 解析到非预期目录,DB/日志等写操作
|
|
6
|
+
* 逃逸到用户项目外,创建了脏数据。
|
|
7
|
+
*
|
|
8
|
+
* 双层防护:
|
|
9
|
+
* Layer 1 — assertSafe(path):
|
|
10
|
+
* 边界检查,拦截写到 projectRoot 外的操作
|
|
11
|
+
* Layer 2 — assertProjectWriteSafe(path):
|
|
12
|
+
* 项目内作用域检查,仅允许写入以下前缀:
|
|
13
|
+
* .autosnippet/ — 运行时 DB、记忆、对话、信号快照
|
|
14
|
+
* {kbDir}/ — 知识库(recipes、candidates、skills、guard 文件)
|
|
15
|
+
* .cursor/ — Cursor IDE 集成
|
|
16
|
+
* .vscode/ — VSCode 集成
|
|
17
|
+
* .github/ — Copilot instructions
|
|
18
|
+
* .gitignore — 追加忽略规则
|
|
19
|
+
* 项目内其他位置(如 data/、src/ 等)一律拦截
|
|
20
|
+
*
|
|
21
|
+
* 设计:
|
|
22
|
+
* - 单例模式,通过 configure() 绑定 projectRoot
|
|
23
|
+
* - 新建文件/目录前调用 assertProjectWriteSafe() 校验
|
|
24
|
+
* - 修改已有文件前调用 assertSafe() 校验(不限制项目内位置)
|
|
25
|
+
* - 允许白名单目录(Xcode snippets、全局缓存等)
|
|
26
|
+
* - 错误不静默:越界写操作抛出 PathGuardError
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import path from 'node:path';
|
|
30
|
+
import fs from 'node:fs';
|
|
31
|
+
|
|
32
|
+
export class PathGuardError extends Error {
|
|
33
|
+
/**
|
|
34
|
+
* @param {string} targetPath - 被拦截的目标路径
|
|
35
|
+
* @param {string} projectRoot - 当前项目根目录
|
|
36
|
+
* @param {string} [reason] - 拦截原因
|
|
37
|
+
*/
|
|
38
|
+
constructor(targetPath, projectRoot, reason) {
|
|
39
|
+
const msg = reason
|
|
40
|
+
? `[PathGuard] ${reason}: "${targetPath}"`
|
|
41
|
+
: `[PathGuard] 写入路径越界: "${targetPath}" 不在允许范围内。`;
|
|
42
|
+
super(
|
|
43
|
+
msg +
|
|
44
|
+
`\n projectRoot: ${projectRoot}` +
|
|
45
|
+
`\n 提示: 检查 process.cwd() 或 projectRoot 配置是否正确`
|
|
46
|
+
);
|
|
47
|
+
this.name = 'PathGuardError';
|
|
48
|
+
this.targetPath = targetPath;
|
|
49
|
+
this.projectRoot = projectRoot;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 项目内允许 AutoSnippet 创建新文件/目录的前缀
|
|
55
|
+
* 注意:这是相对于 projectRoot 的前缀列表
|
|
56
|
+
*/
|
|
57
|
+
const PROJECT_WRITE_SCOPE_PREFIXES = [
|
|
58
|
+
'.autosnippet', // 运行时 DB、记忆、对话、信号快照
|
|
59
|
+
'.cursor', // Cursor IDE 集成
|
|
60
|
+
'.vscode', // VSCode 集成
|
|
61
|
+
'.github', // Copilot instructions
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 项目根目录下允许直接写入的文件(非目录前缀匹配)
|
|
66
|
+
*/
|
|
67
|
+
const PROJECT_ROOT_WRITABLE_FILES = [
|
|
68
|
+
'.gitignore',
|
|
69
|
+
'.env',
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
class PathGuard {
|
|
73
|
+
/** @type {string|null} 项目根目录(绝对路径) */
|
|
74
|
+
#projectRoot = null;
|
|
75
|
+
|
|
76
|
+
/** @type {string|null} AutoSnippet 包自身根目录 */
|
|
77
|
+
#packageRoot = null;
|
|
78
|
+
|
|
79
|
+
/** @type {Set<string>} 额外允许的绝对路径前缀 */
|
|
80
|
+
#allowList = new Set();
|
|
81
|
+
|
|
82
|
+
/** @type {string|null} 知识库目录名(如 'AutoSnippet') */
|
|
83
|
+
#knowledgeBaseDir = null;
|
|
84
|
+
|
|
85
|
+
/** @type {boolean} 是否已配置 */
|
|
86
|
+
#configured = false;
|
|
87
|
+
|
|
88
|
+
constructor() {}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 配置 PathGuard(每个进程执行一次)
|
|
92
|
+
* @param {object} opts
|
|
93
|
+
* @param {string} opts.projectRoot - 用户项目根目录(绝对路径)
|
|
94
|
+
* @param {string} [opts.packageRoot] - AutoSnippet 包自身根目录
|
|
95
|
+
* @param {string} [opts.knowledgeBaseDir='AutoSnippet'] - 知识库目录名
|
|
96
|
+
* @param {string[]} [opts.extraAllowPaths] - 额外允许的路径前缀
|
|
97
|
+
*/
|
|
98
|
+
configure({ projectRoot, packageRoot, knowledgeBaseDir, extraAllowPaths = [] }) {
|
|
99
|
+
if (!projectRoot || !path.isAbsolute(projectRoot)) {
|
|
100
|
+
throw new Error(`[PathGuard] projectRoot 必须是绝对路径,收到: "${projectRoot}"`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
this.#projectRoot = path.resolve(projectRoot);
|
|
104
|
+
this.#packageRoot = packageRoot ? path.resolve(packageRoot) : null;
|
|
105
|
+
this.#knowledgeBaseDir = knowledgeBaseDir || null; // 延迟解析
|
|
106
|
+
|
|
107
|
+
// 默认白名单:Xcode snippets 目录、全局缓存
|
|
108
|
+
const HOME = process.env.HOME || process.env.USERPROFILE || '';
|
|
109
|
+
if (HOME) {
|
|
110
|
+
this.#allowList.add(path.join(HOME, 'Library/Developer/Xcode/UserData/CodeSnippets'));
|
|
111
|
+
this.#allowList.add(path.join(HOME, '.autosnippet'));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 用户自定义白名单
|
|
115
|
+
for (const p of extraAllowPaths) {
|
|
116
|
+
if (path.isAbsolute(p)) {
|
|
117
|
+
this.#allowList.add(path.resolve(p));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
this.#configured = true;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** 是否已配置 */
|
|
125
|
+
get configured() {
|
|
126
|
+
return this.#configured;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** 当前 projectRoot */
|
|
130
|
+
get projectRoot() {
|
|
131
|
+
return this.#projectRoot;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* 设置知识库目录名(可在 configure 之后延迟设置)
|
|
136
|
+
* @param {string} dirName - 如 'AutoSnippet'、'Knowledge' 等
|
|
137
|
+
*/
|
|
138
|
+
setKnowledgeBaseDir(dirName) {
|
|
139
|
+
if (dirName && typeof dirName === 'string') {
|
|
140
|
+
this.#knowledgeBaseDir = dirName;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Layer 1: 断言路径在允许的边界范围内
|
|
146
|
+
* 用于修改已有文件的场景(如 XcodeIntegration 插入 header、SpmService 修改 Package.swift)
|
|
147
|
+
* @param {string} targetPath - 要写入的绝对路径
|
|
148
|
+
* @throws {PathGuardError}
|
|
149
|
+
*/
|
|
150
|
+
assertSafe(targetPath) {
|
|
151
|
+
if (!this.#configured) return;
|
|
152
|
+
|
|
153
|
+
if (!targetPath || typeof targetPath !== 'string') {
|
|
154
|
+
throw new PathGuardError(String(targetPath), this.#projectRoot);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const resolved = path.resolve(targetPath);
|
|
158
|
+
|
|
159
|
+
// 1. 项目目录内 — 允许
|
|
160
|
+
if (this.#isUnder(resolved, this.#projectRoot)) return;
|
|
161
|
+
|
|
162
|
+
// 2. AutoSnippet 包自身目录内(logs/ 等)— 允许
|
|
163
|
+
if (this.#packageRoot && this.#isUnder(resolved, this.#packageRoot)) return;
|
|
164
|
+
|
|
165
|
+
// 3. 白名单目录 — 允许
|
|
166
|
+
for (const allowed of this.#allowList) {
|
|
167
|
+
if (this.#isUnder(resolved, allowed)) return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// 越界
|
|
171
|
+
throw new PathGuardError(resolved, this.#projectRoot);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Layer 2: 断言路径在项目内允许的写入作用域中
|
|
176
|
+
* 用于创建新目录/新文件的场景(如 mkdirSync、writeFileSync 创建新文件)
|
|
177
|
+
* 比 assertSafe() 更严格:即使在 projectRoot 内,也只允许写入特定前缀
|
|
178
|
+
* @param {string} targetPath - 要创建的绝对路径
|
|
179
|
+
* @throws {PathGuardError}
|
|
180
|
+
*/
|
|
181
|
+
assertProjectWriteSafe(targetPath) {
|
|
182
|
+
if (!this.#configured) return;
|
|
183
|
+
|
|
184
|
+
// 先做边界检查
|
|
185
|
+
this.assertSafe(targetPath);
|
|
186
|
+
|
|
187
|
+
const resolved = path.resolve(targetPath);
|
|
188
|
+
|
|
189
|
+
// 如果不在 projectRoot 内(在白名单/packageRoot 中),跳过项目内检查
|
|
190
|
+
if (!this.#isUnder(resolved, this.#projectRoot)) return;
|
|
191
|
+
|
|
192
|
+
// 计算相对于 projectRoot 的路径
|
|
193
|
+
const relative = path.relative(this.#projectRoot, resolved);
|
|
194
|
+
const firstSegment = relative.split(path.sep)[0];
|
|
195
|
+
|
|
196
|
+
// 检查是否在允许的前缀中
|
|
197
|
+
for (const prefix of PROJECT_WRITE_SCOPE_PREFIXES) {
|
|
198
|
+
if (firstSegment === prefix) return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// 检查知识库目录(动态解析)
|
|
202
|
+
const kbDir = this.#resolveKnowledgeBaseDir();
|
|
203
|
+
if (kbDir && firstSegment === kbDir) return;
|
|
204
|
+
|
|
205
|
+
// 检查根目录可写文件(如 .gitignore)
|
|
206
|
+
if (PROJECT_ROOT_WRITABLE_FILES.includes(relative)) return;
|
|
207
|
+
|
|
208
|
+
// 不在允许的写入范围内
|
|
209
|
+
throw new PathGuardError(
|
|
210
|
+
resolved,
|
|
211
|
+
this.#projectRoot,
|
|
212
|
+
`项目内写入范围受限: "${relative}" 不在允许的目录中(允许: ${[...PROJECT_WRITE_SCOPE_PREFIXES, kbDir || 'AutoSnippet'].join(', ')})`,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* 安全检查(不抛错,返回 boolean)
|
|
218
|
+
* @param {string} targetPath
|
|
219
|
+
* @returns {boolean}
|
|
220
|
+
*/
|
|
221
|
+
isSafe(targetPath) {
|
|
222
|
+
try {
|
|
223
|
+
this.assertSafe(targetPath);
|
|
224
|
+
return true;
|
|
225
|
+
} catch {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* 项目内写入范围检查(不抛错,返回 boolean)
|
|
232
|
+
* @param {string} targetPath
|
|
233
|
+
* @returns {boolean}
|
|
234
|
+
*/
|
|
235
|
+
isProjectWriteSafe(targetPath) {
|
|
236
|
+
try {
|
|
237
|
+
this.assertProjectWriteSafe(targetPath);
|
|
238
|
+
return true;
|
|
239
|
+
} catch {
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* 将相对路径安全地解析到 projectRoot 下
|
|
246
|
+
* 替代 path.resolve(relativePath)(后者基于 cwd,不安全)
|
|
247
|
+
* @param {string} relativePath
|
|
248
|
+
* @returns {string} 绝对路径
|
|
249
|
+
*/
|
|
250
|
+
resolveProjectPath(relativePath) {
|
|
251
|
+
if (!this.#configured || !this.#projectRoot) {
|
|
252
|
+
// 未配置时 fallback 到 cwd(向后兼容)
|
|
253
|
+
return path.resolve(relativePath);
|
|
254
|
+
}
|
|
255
|
+
const resolved = path.resolve(this.#projectRoot, relativePath);
|
|
256
|
+
this.assertSafe(resolved);
|
|
257
|
+
return resolved;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* 重置状态(仅用于测试)
|
|
262
|
+
*/
|
|
263
|
+
_reset() {
|
|
264
|
+
this.#projectRoot = null;
|
|
265
|
+
this.#packageRoot = null;
|
|
266
|
+
this.#allowList.clear();
|
|
267
|
+
this.#knowledgeBaseDir = null;
|
|
268
|
+
this.#configured = false;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* resolved 是否在 base 目录下
|
|
273
|
+
*/
|
|
274
|
+
#isUnder(resolved, base) {
|
|
275
|
+
return resolved === base || resolved.startsWith(base + path.sep);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* 解析知识库目录名
|
|
280
|
+
* 优先使用 configure 阶段传入的值,否则尝试运行时探测
|
|
281
|
+
*/
|
|
282
|
+
#resolveKnowledgeBaseDir() {
|
|
283
|
+
if (this.#knowledgeBaseDir) return this.#knowledgeBaseDir;
|
|
284
|
+
|
|
285
|
+
// 运行时探测: 查找包含 AutoSnippet.boxspec.json 的子目录
|
|
286
|
+
try {
|
|
287
|
+
const entries = fs.readdirSync(this.#projectRoot, { withFileTypes: true });
|
|
288
|
+
for (const e of entries) {
|
|
289
|
+
if (e.isDirectory() && !e.name.startsWith('.')) {
|
|
290
|
+
if (fs.existsSync(path.join(this.#projectRoot, e.name, 'AutoSnippet.boxspec.json'))) {
|
|
291
|
+
this.#knowledgeBaseDir = e.name;
|
|
292
|
+
return e.name;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
} catch { /* ignore */ }
|
|
297
|
+
|
|
298
|
+
// 默认
|
|
299
|
+
return 'AutoSnippet';
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* 延迟加载 fs(避免循环依赖)
|
|
305
|
+
*/
|
|
306
|
+
function await_fs() {
|
|
307
|
+
// eslint-disable-next-line no-eval
|
|
308
|
+
return eval("require('fs')");
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// 单例 — 整个进程共享
|
|
312
|
+
const pathGuard = new PathGuard();
|
|
313
|
+
|
|
314
|
+
export default pathGuard;
|