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
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
<title>AutoSnippet Dashboard</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-Duc8Qk-c.js"></script>
|
|
9
9
|
<link rel="modulepreload" crossorigin href="/assets/yaml-qRaU8Ldn.js">
|
|
10
10
|
<link rel="modulepreload" crossorigin href="/assets/vendor-BotF760a.js">
|
|
11
11
|
<link rel="modulepreload" crossorigin href="/assets/axios-C0Zqfgkc.js">
|
|
12
|
-
<link rel="modulepreload" crossorigin href="/assets/icons-
|
|
12
|
+
<link rel="modulepreload" crossorigin href="/assets/icons-B_Xg4B-s.js">
|
|
13
13
|
<link rel="modulepreload" crossorigin href="/assets/syntax-highlighter-CVLHn9O5.js">
|
|
14
14
|
<link rel="modulepreload" crossorigin href="/assets/react-markdown-BA6FB2NP.js">
|
|
15
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
15
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CkIih2CC.css">
|
|
16
16
|
</head>
|
|
17
17
|
<body>
|
|
18
18
|
<div id="root"></div>
|
package/lib/bootstrap.js
CHANGED
|
@@ -11,6 +11,7 @@ import Gateway from './core/gateway/Gateway.js';
|
|
|
11
11
|
import AuditLogger from './infrastructure/audit/AuditLogger.js';
|
|
12
12
|
import AuditStore from './infrastructure/audit/AuditStore.js';
|
|
13
13
|
import { SkillHooks } from './service/skills/SkillHooks.js';
|
|
14
|
+
import pathGuard from './shared/PathGuard.js';
|
|
14
15
|
|
|
15
16
|
const __filename = fileURLToPath(import.meta.url);
|
|
16
17
|
const __dirname = path.dirname(__filename);
|
|
@@ -24,6 +25,22 @@ export class Bootstrap {
|
|
|
24
25
|
this.components = {};
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
/**
|
|
29
|
+
* 配置 PathGuard 路径安全守卫
|
|
30
|
+
* 必须在任何文件写操作前调用
|
|
31
|
+
* @param {string} projectRoot - 用户项目的绝对路径
|
|
32
|
+
* @param {string} [knowledgeBaseDir] - 知识库目录名(如 'AutoSnippet')
|
|
33
|
+
*/
|
|
34
|
+
static configurePathGuard(projectRoot, knowledgeBaseDir) {
|
|
35
|
+
if (!pathGuard.configured && projectRoot) {
|
|
36
|
+
const packageRoot = path.resolve(__dirname, '..');
|
|
37
|
+
pathGuard.configure({ projectRoot, packageRoot, knowledgeBaseDir });
|
|
38
|
+
} else if (knowledgeBaseDir) {
|
|
39
|
+
// 已配置但知识库目录名可能后续才知道
|
|
40
|
+
pathGuard.setKnowledgeBaseDir(knowledgeBaseDir);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
27
44
|
/**
|
|
28
45
|
* 初始化应用程序
|
|
29
46
|
*/
|
package/lib/cli/SetupService.js
CHANGED
|
@@ -188,6 +188,9 @@ export class SetupService {
|
|
|
188
188
|
// 确保 .autosnippet/ 在主仓库 .gitignore 中
|
|
189
189
|
this._ensureGitignore();
|
|
190
190
|
|
|
191
|
+
// .env — AI 配置模板
|
|
192
|
+
this._ensureEnvFile();
|
|
193
|
+
|
|
191
194
|
return { created: 'runtime' };
|
|
192
195
|
}
|
|
193
196
|
|
|
@@ -640,6 +643,35 @@ export class SetupService {
|
|
|
640
643
|
|
|
641
644
|
/* ═══ Helpers ════════════════════════════════════════ */
|
|
642
645
|
|
|
646
|
+
/**
|
|
647
|
+
* @private 在项目根目录创建 .env 文件(从 .env.example 复制)
|
|
648
|
+
* 如果 .env 已存在则跳过并提示用户手动配置。
|
|
649
|
+
*/
|
|
650
|
+
_ensureEnvFile() {
|
|
651
|
+
const envPath = join(this.projectRoot, '.env');
|
|
652
|
+
if (existsSync(envPath)) {
|
|
653
|
+
console.log(' ℹ️ .env 已存在,跳过写入。如需配置 AI,请手动编辑或通过 Dashboard 设置');
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const examplePath = join(REPO_ROOT, '.env.example');
|
|
658
|
+
if (existsSync(examplePath)) {
|
|
659
|
+
copyFileSync(examplePath, envPath);
|
|
660
|
+
} else {
|
|
661
|
+
// fallback: .env.example 缺失时写入最小模板
|
|
662
|
+
writeFileSync(envPath, [
|
|
663
|
+
'# AutoSnippet AI 配置(由 asd setup 自动生成)',
|
|
664
|
+
'# 完整配置说明见 .env.example',
|
|
665
|
+
'',
|
|
666
|
+
'ASD_AI_PROVIDER=google',
|
|
667
|
+
'ASD_AI_MODEL=gemini-2.0-flash',
|
|
668
|
+
'# ASD_GOOGLE_API_KEY=',
|
|
669
|
+
'',
|
|
670
|
+
].join('\n'));
|
|
671
|
+
}
|
|
672
|
+
console.log(' ✅ .env(已从 .env.example 复制,请填写 API Key 后使用)');
|
|
673
|
+
}
|
|
674
|
+
|
|
643
675
|
/** @private 确保项目 .gitignore 正确配置 AutoSnippet 相关规则 */
|
|
644
676
|
_ensureGitignore() {
|
|
645
677
|
const giPath = join(this.projectRoot, '.gitignore');
|
|
@@ -672,6 +704,27 @@ export class SetupService {
|
|
|
672
704
|
console.log(' ✅ .gitignore += !.autosnippet/config.json');
|
|
673
705
|
}
|
|
674
706
|
|
|
707
|
+
// ── 必须忽略:.env(包含 API Key 等敏感信息) ──
|
|
708
|
+
if (!content.includes('.env') || (!content.match(/^\.env$/m) && !content.match(/^\.env\s/m))) {
|
|
709
|
+
content += `\n# AutoSnippet 环境变量(含 API Key,不入库)\n.env\n`;
|
|
710
|
+
changed = true;
|
|
711
|
+
console.log(' ✅ .gitignore += .env');
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// ── 必须忽略:logs/(winston 运行日志,可达数十 MB) ──
|
|
715
|
+
if (!content.match(/^logs\/?$/m)) {
|
|
716
|
+
content += `\n# AutoSnippet 运行日志\nlogs/\n`;
|
|
717
|
+
changed = true;
|
|
718
|
+
console.log(' ✅ .gitignore += logs/');
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// ── 必须忽略:.autosnippet-drafts/(AI 草稿临时目录) ──
|
|
722
|
+
if (!content.includes('.autosnippet-drafts')) {
|
|
723
|
+
content += `\n# AutoSnippet AI 草稿(临时)\n.autosnippet-drafts/\n`;
|
|
724
|
+
changed = true;
|
|
725
|
+
console.log(' ✅ .gitignore += .autosnippet-drafts/');
|
|
726
|
+
}
|
|
727
|
+
|
|
675
728
|
// Skills 已迁移到 AutoSnippet/skills/(知识库目录内),自动跟随 Git
|
|
676
729
|
|
|
677
730
|
// ── 清理旧版本的 .autosnippet/skills/ negation(已迁移,不再需要)──
|
|
@@ -203,8 +203,17 @@ export class ClaudeProvider extends AiProvider {
|
|
|
203
203
|
* stop_reason: 'end_turn' | 'tool_use' | 'max_tokens'
|
|
204
204
|
*/
|
|
205
205
|
#parseToolResponse(data) {
|
|
206
|
+
// 提取 token 用量 (Claude usage)
|
|
207
|
+
const usage = data?.usage
|
|
208
|
+
? {
|
|
209
|
+
inputTokens: data.usage.input_tokens || 0,
|
|
210
|
+
outputTokens: data.usage.output_tokens || 0,
|
|
211
|
+
totalTokens: (data.usage.input_tokens || 0) + (data.usage.output_tokens || 0),
|
|
212
|
+
}
|
|
213
|
+
: null;
|
|
214
|
+
|
|
206
215
|
if (!data?.content?.length) {
|
|
207
|
-
return { text: '', functionCalls: null };
|
|
216
|
+
return { text: '', functionCalls: null, usage };
|
|
208
217
|
}
|
|
209
218
|
|
|
210
219
|
const functionCalls = [];
|
|
@@ -227,12 +236,14 @@ export class ClaudeProvider extends AiProvider {
|
|
|
227
236
|
return {
|
|
228
237
|
text: textParts.length > 0 ? textParts.join('\n') : null,
|
|
229
238
|
functionCalls,
|
|
239
|
+
usage,
|
|
230
240
|
};
|
|
231
241
|
}
|
|
232
242
|
|
|
233
243
|
return {
|
|
234
244
|
text: textParts.join('\n'),
|
|
235
245
|
functionCalls: null,
|
|
246
|
+
usage,
|
|
236
247
|
};
|
|
237
248
|
}
|
|
238
249
|
|
|
@@ -242,8 +242,18 @@ export class GoogleGeminiProvider extends AiProvider {
|
|
|
242
242
|
*/
|
|
243
243
|
#parseToolResponse(data) {
|
|
244
244
|
const content = data?.candidates?.[0]?.content;
|
|
245
|
+
|
|
246
|
+
// 提取 token 用量 (Gemini usageMetadata)
|
|
247
|
+
const usage = data?.usageMetadata
|
|
248
|
+
? {
|
|
249
|
+
inputTokens: data.usageMetadata.promptTokenCount || 0,
|
|
250
|
+
outputTokens: data.usageMetadata.candidatesTokenCount || 0,
|
|
251
|
+
totalTokens: data.usageMetadata.totalTokenCount || 0,
|
|
252
|
+
}
|
|
253
|
+
: null;
|
|
254
|
+
|
|
245
255
|
if (!content || !content.parts || content.parts.length === 0) {
|
|
246
|
-
return { text: '', functionCalls: null };
|
|
256
|
+
return { text: '', functionCalls: null, usage };
|
|
247
257
|
}
|
|
248
258
|
|
|
249
259
|
const functionCalls = [];
|
|
@@ -267,12 +277,14 @@ export class GoogleGeminiProvider extends AiProvider {
|
|
|
267
277
|
return {
|
|
268
278
|
text: textParts.length > 0 ? textParts.join('\n') : null,
|
|
269
279
|
functionCalls,
|
|
280
|
+
usage,
|
|
270
281
|
};
|
|
271
282
|
}
|
|
272
283
|
|
|
273
284
|
return {
|
|
274
285
|
text: textParts.join('\n'),
|
|
275
286
|
functionCalls: null,
|
|
287
|
+
usage,
|
|
276
288
|
};
|
|
277
289
|
}
|
|
278
290
|
|
|
@@ -146,7 +146,17 @@ export class OpenAiProvider extends AiProvider {
|
|
|
146
146
|
*/
|
|
147
147
|
#parseToolResponse(data) {
|
|
148
148
|
const choice = data?.choices?.[0];
|
|
149
|
-
|
|
149
|
+
|
|
150
|
+
// 提取 token 用量 (OpenAI usage)
|
|
151
|
+
const usage = data?.usage
|
|
152
|
+
? {
|
|
153
|
+
inputTokens: data.usage.prompt_tokens || 0,
|
|
154
|
+
outputTokens: data.usage.completion_tokens || 0,
|
|
155
|
+
totalTokens: data.usage.total_tokens || 0,
|
|
156
|
+
}
|
|
157
|
+
: null;
|
|
158
|
+
|
|
159
|
+
if (!choice) return { text: '', functionCalls: null, usage };
|
|
150
160
|
|
|
151
161
|
const message = choice.message;
|
|
152
162
|
const text = message?.content || null;
|
|
@@ -165,11 +175,11 @@ export class OpenAiProvider extends AiProvider {
|
|
|
165
175
|
|
|
166
176
|
if (functionCalls.length > 0) {
|
|
167
177
|
this.logger.debug(`[OpenAI] native function calls: ${functionCalls.map(fc => fc.name).join(', ')}`);
|
|
168
|
-
return { text, functionCalls };
|
|
178
|
+
return { text, functionCalls, usage };
|
|
169
179
|
}
|
|
170
180
|
}
|
|
171
181
|
|
|
172
|
-
return { text, functionCalls: null };
|
|
182
|
+
return { text, functionCalls: null, usage };
|
|
173
183
|
}
|
|
174
184
|
|
|
175
185
|
async summarize(code) {
|
|
@@ -51,6 +51,11 @@ export class McpServer {
|
|
|
51
51
|
async initialize() {
|
|
52
52
|
if (!this.container) {
|
|
53
53
|
const { default: Bootstrap } = await import('../../bootstrap.js');
|
|
54
|
+
|
|
55
|
+
// 路径安全守卫 — 在任何写操作前配置
|
|
56
|
+
const projectRoot = process.env.ASD_PROJECT_DIR || process.cwd();
|
|
57
|
+
Bootstrap.configurePathGuard(projectRoot);
|
|
58
|
+
|
|
54
59
|
this.bootstrap = new Bootstrap();
|
|
55
60
|
const components = await this.bootstrap.initialize();
|
|
56
61
|
|
|
@@ -62,7 +67,7 @@ export class McpServer {
|
|
|
62
67
|
auditLogger: components.auditLogger,
|
|
63
68
|
gateway: components.gateway,
|
|
64
69
|
constitution: components.constitution,
|
|
65
|
-
projectRoot
|
|
70
|
+
projectRoot,
|
|
66
71
|
});
|
|
67
72
|
|
|
68
73
|
// 注册 Gateway action handlers
|
|
@@ -143,10 +148,12 @@ export class McpServer {
|
|
|
143
148
|
// Bootstrap 冷启动
|
|
144
149
|
case 'autosnippet_bootstrap_knowledge': return bootstrapHandlers.bootstrapKnowledge(ctx, args);
|
|
145
150
|
case 'autosnippet_bootstrap_refine': return bootstrapHandlers.bootstrapRefine(ctx, args);
|
|
146
|
-
// Skills 加载 & 创建 & 推荐
|
|
151
|
+
// Skills 加载 & 创建 & 管理 & 推荐
|
|
147
152
|
case 'autosnippet_list_skills': return skillHandlers.listSkills();
|
|
148
153
|
case 'autosnippet_load_skill': return skillHandlers.loadSkill(ctx, args);
|
|
149
154
|
case 'autosnippet_create_skill': return skillHandlers.createSkill(ctx, args);
|
|
155
|
+
case 'autosnippet_delete_skill': return skillHandlers.deleteSkill(ctx, args);
|
|
156
|
+
case 'autosnippet_update_skill': return skillHandlers.updateSkill(ctx, args);
|
|
150
157
|
case 'autosnippet_suggest_skills': return skillHandlers.suggestSkills(ctx);
|
|
151
158
|
default: throw new Error(`Unknown tool: ${name}`);
|
|
152
159
|
}
|
|
@@ -195,9 +202,9 @@ export class McpServer {
|
|
|
195
202
|
await this.initialize();
|
|
196
203
|
const transport = new StdioServerTransport();
|
|
197
204
|
await this.server.connect(transport);
|
|
198
|
-
this.logger.info('MCP Server started (stdio) —
|
|
205
|
+
this.logger.info('MCP Server started (stdio) — 38 tools');
|
|
199
206
|
// 在 stderr 写一行简洁的就绪通知(不使用 winston,仅用于 Cursor 日志面板 & 调试)
|
|
200
|
-
process.stderr.write('AutoSnippet MCP ready —
|
|
207
|
+
process.stderr.write('AutoSnippet MCP ready — 38 tools\n');
|
|
201
208
|
}
|
|
202
209
|
|
|
203
210
|
async shutdown() {
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import path from 'node:path';
|
|
15
|
+
import fs from 'node:fs/promises';
|
|
15
16
|
import { AnalystAgent } from '../../../../../service/chat/AnalystAgent.js';
|
|
16
17
|
import { ProducerAgent } from '../../../../../service/chat/ProducerAgent.js';
|
|
17
18
|
import { TierScheduler } from './tier-scheduler.js';
|
|
@@ -20,6 +21,69 @@ import Logger from '../../../../../infrastructure/logging/Logger.js';
|
|
|
20
21
|
|
|
21
22
|
const logger = Logger.getInstance();
|
|
22
23
|
|
|
24
|
+
// ──────────────────────────────────────────────────────────────────
|
|
25
|
+
// P3: 断点续传 — Checkpoint 存储/恢复
|
|
26
|
+
// ──────────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
const CHECKPOINT_TTL_MS = 3600_000; // 1小时内有效
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 保存维度级 checkpoint
|
|
32
|
+
* @param {string} projectRoot
|
|
33
|
+
* @param {string} sessionId
|
|
34
|
+
* @param {string} dimId
|
|
35
|
+
* @param {object} result — 维度执行结果
|
|
36
|
+
* @param {object} [digest] — DimensionDigest
|
|
37
|
+
*/
|
|
38
|
+
async function saveDimensionCheckpoint(projectRoot, sessionId, dimId, result, digest = null) {
|
|
39
|
+
try {
|
|
40
|
+
const checkpointDir = path.join(projectRoot, '.autosnippet', 'bootstrap-checkpoint');
|
|
41
|
+
await fs.mkdir(checkpointDir, { recursive: true });
|
|
42
|
+
await fs.writeFile(
|
|
43
|
+
path.join(checkpointDir, `${dimId}.json`),
|
|
44
|
+
JSON.stringify({ dimId, sessionId, ...result, digest, completedAt: Date.now() }),
|
|
45
|
+
);
|
|
46
|
+
} catch (err) {
|
|
47
|
+
logger.warn(`[Bootstrap-v3] checkpoint save failed for "${dimId}": ${err.message}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 加载有效的 checkpoints
|
|
53
|
+
* @param {string} projectRoot
|
|
54
|
+
* @returns {Promise<Map<string, object>>} dimId → checkpoint data
|
|
55
|
+
*/
|
|
56
|
+
async function loadCheckpoints(projectRoot) {
|
|
57
|
+
const checkpoints = new Map();
|
|
58
|
+
try {
|
|
59
|
+
const checkpointDir = path.join(projectRoot, '.autosnippet', 'bootstrap-checkpoint');
|
|
60
|
+
const files = await fs.readdir(checkpointDir).catch(() => []);
|
|
61
|
+
const now = Date.now();
|
|
62
|
+
for (const file of files) {
|
|
63
|
+
if (!file.endsWith('.json')) continue;
|
|
64
|
+
try {
|
|
65
|
+
const content = await fs.readFile(path.join(checkpointDir, file), 'utf-8');
|
|
66
|
+
const data = JSON.parse(content);
|
|
67
|
+
if (data.completedAt && (now - data.completedAt) < CHECKPOINT_TTL_MS) {
|
|
68
|
+
checkpoints.set(data.dimId, data);
|
|
69
|
+
}
|
|
70
|
+
} catch { /* skip corrupt checkpoint */ }
|
|
71
|
+
}
|
|
72
|
+
} catch { /* checkpoint dir doesn't exist */ }
|
|
73
|
+
return checkpoints;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 清理 checkpoint 目录
|
|
78
|
+
* @param {string} projectRoot
|
|
79
|
+
*/
|
|
80
|
+
async function clearCheckpoints(projectRoot) {
|
|
81
|
+
try {
|
|
82
|
+
const checkpointDir = path.join(projectRoot, '.autosnippet', 'bootstrap-checkpoint');
|
|
83
|
+
await fs.rm(checkpointDir, { recursive: true, force: true });
|
|
84
|
+
} catch { /* ignore */ }
|
|
85
|
+
}
|
|
86
|
+
|
|
23
87
|
// ──────────────────────────────────────────────────────────────────
|
|
24
88
|
// v3.0 维度配置 (增加 focusAreas 用于 Analyst prompt)
|
|
25
89
|
// ──────────────────────────────────────────────────────────────────
|
|
@@ -222,7 +286,7 @@ export async function fillDimensionsV3(fillContext) {
|
|
|
222
286
|
// ═══════════════════════════════════════════════════════════
|
|
223
287
|
// Step 2: 按维度分层执行 (Analyst → Gate → Producer)
|
|
224
288
|
// ═══════════════════════════════════════════════════════════
|
|
225
|
-
const concurrency = parseInt(process.env.ASD_PARALLEL_CONCURRENCY || '
|
|
289
|
+
const concurrency = parseInt(process.env.ASD_PARALLEL_CONCURRENCY || '3', 10);
|
|
226
290
|
const enableParallel = process.env.ASD_PARALLEL_BOOTSTRAP !== 'false';
|
|
227
291
|
const scheduler = new TierScheduler();
|
|
228
292
|
|
|
@@ -233,13 +297,52 @@ export async function fillDimensionsV3(fillContext) {
|
|
|
233
297
|
|
|
234
298
|
logger.info(`[Bootstrap-v3] Active dimensions: [${activeDimIds.join(', ')}], concurrency=${enableParallel ? concurrency : 1}`);
|
|
235
299
|
|
|
300
|
+
// ── P3: 断点续传 — 加载有效 checkpoints ──
|
|
301
|
+
const completedCheckpoints = await loadCheckpoints(projectRoot);
|
|
302
|
+
const skippedDims = [];
|
|
303
|
+
for (const [dimId, checkpoint] of completedCheckpoints) {
|
|
304
|
+
if (activeDimIds.includes(dimId)) {
|
|
305
|
+
// 恢复 DimensionContext 中的 digest
|
|
306
|
+
if (checkpoint.digest) {
|
|
307
|
+
dimContext.addDimensionDigest(dimId, checkpoint.digest);
|
|
308
|
+
}
|
|
309
|
+
taskManager?.markTaskCompleted(dimId, {
|
|
310
|
+
type: 'checkpoint-restored',
|
|
311
|
+
...checkpoint,
|
|
312
|
+
});
|
|
313
|
+
skippedDims.push(dimId);
|
|
314
|
+
logger.info(`[Bootstrap-v3] ⏩ 跳过已完成维度 (checkpoint): "${dimId}"`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
236
318
|
const candidateResults = { created: 0, failed: 0, errors: [] };
|
|
237
319
|
const dimensionCandidates = {};
|
|
320
|
+
const dimensionStats = {}; // P4.2: 维度级统计
|
|
238
321
|
|
|
239
322
|
/**
|
|
240
323
|
* 执行单个维度: Analyst → Gate → Producer
|
|
241
324
|
*/
|
|
242
325
|
async function executeDimension(dimId) {
|
|
326
|
+
// P3: 跳过已有 checkpoint 的维度
|
|
327
|
+
if (skippedDims.includes(dimId)) {
|
|
328
|
+
const cp = completedCheckpoints.get(dimId);
|
|
329
|
+
const cpResult = {
|
|
330
|
+
candidateCount: cp?.candidateCount || 0,
|
|
331
|
+
rejectedCount: cp?.rejectedCount || 0,
|
|
332
|
+
analysisChars: cp?.analysisChars || 0,
|
|
333
|
+
referencedFiles: cp?.referencedFiles || 0,
|
|
334
|
+
durationMs: cp?.durationMs || 0,
|
|
335
|
+
toolCallCount: cp?.toolCallCount || 0,
|
|
336
|
+
tokenUsage: cp?.tokenUsage || { input: 0, output: 0 },
|
|
337
|
+
skipped: true,
|
|
338
|
+
restoredFromCheckpoint: true,
|
|
339
|
+
};
|
|
340
|
+
// P4.2: 将恢复的维度也记入统计
|
|
341
|
+
dimensionStats[dimId] = cpResult;
|
|
342
|
+
candidateResults.created += cpResult.candidateCount;
|
|
343
|
+
return cpResult;
|
|
344
|
+
}
|
|
345
|
+
|
|
243
346
|
const dim = dimensions.find(d => d.id === dimId);
|
|
244
347
|
const v3Config = DIMENSION_CONFIGS_V3[dimId];
|
|
245
348
|
if (!dim || !v3Config) {
|
|
@@ -270,7 +373,7 @@ export async function fillDimensionsV3(fillContext) {
|
|
|
270
373
|
try {
|
|
271
374
|
// ── Phase 1: Analyst ──
|
|
272
375
|
const analysisReport = await Promise.race([
|
|
273
|
-
analystAgent.analyze(dimConfig, projectInfo, { sessionId }),
|
|
376
|
+
analystAgent.analyze(dimConfig, projectInfo, { sessionId, dimensionContext: dimContext }),
|
|
274
377
|
new Promise((_, reject) =>
|
|
275
378
|
setTimeout(() => reject(new Error(`Analyst timeout for "${dimId}"`)), 180_000)),
|
|
276
379
|
]);
|
|
@@ -333,13 +436,32 @@ export async function fillDimensionsV3(fillContext) {
|
|
|
333
436
|
toolCallCount: (analysisReport.metadata?.toolCallCount || 0) + (producerResult.toolCalls?.length || 0),
|
|
334
437
|
});
|
|
335
438
|
|
|
336
|
-
|
|
439
|
+
// P4.1: 聚合 token 用量
|
|
440
|
+
const analystTokens = analysisReport.metadata?.tokenUsage || { input: 0, output: 0 };
|
|
441
|
+
const producerTokens = producerResult.tokenUsage || { input: 0, output: 0 };
|
|
442
|
+
const dimTokenUsage = {
|
|
443
|
+
input: (analystTokens.input || 0) + (producerTokens.input || 0),
|
|
444
|
+
output: (analystTokens.output || 0) + (producerTokens.output || 0),
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
const dimResult = {
|
|
337
448
|
candidateCount: producerResult.candidateCount,
|
|
449
|
+
rejectedCount: producerResult.rejectedCount || 0,
|
|
338
450
|
analysisChars: analysisReport.analysisText.length,
|
|
339
451
|
referencedFiles: analysisReport.referencedFiles.length,
|
|
340
452
|
durationMs: Date.now() - dimStartTime,
|
|
453
|
+
toolCallCount: (analysisReport.metadata?.toolCallCount || 0) + (producerResult.toolCalls?.length || 0),
|
|
454
|
+
tokenUsage: dimTokenUsage,
|
|
341
455
|
};
|
|
342
456
|
|
|
457
|
+
// P4.2: 记录维度统计
|
|
458
|
+
dimensionStats[dimId] = dimResult;
|
|
459
|
+
|
|
460
|
+
// P3: 保存 checkpoint
|
|
461
|
+
await saveDimensionCheckpoint(projectRoot, sessionId, dimId, dimResult, digest);
|
|
462
|
+
|
|
463
|
+
return dimResult;
|
|
464
|
+
|
|
343
465
|
} catch (err) {
|
|
344
466
|
logger.error(`[Bootstrap-v3] Dimension "${dimId}" failed: ${err.message}`);
|
|
345
467
|
candidateResults.errors.push({ dimId, error: err.message });
|
|
@@ -449,16 +571,83 @@ export async function fillDimensionsV3(fillContext) {
|
|
|
449
571
|
}
|
|
450
572
|
|
|
451
573
|
// ═══════════════════════════════════════════════════════════
|
|
452
|
-
// Summary
|
|
574
|
+
// Summary + P4.2: Bootstrap Report
|
|
453
575
|
// ═══════════════════════════════════════════════════════════
|
|
454
576
|
const totalTimeMs = Date.now() - t0;
|
|
577
|
+
|
|
578
|
+
// P4.1: 汇总所有维度 token 用量
|
|
579
|
+
const totalTokenUsage = { input: 0, output: 0 };
|
|
580
|
+
const totalToolCalls = Object.values(dimensionStats).reduce((sum, s) => sum + (s.toolCallCount || 0), 0);
|
|
581
|
+
for (const stat of Object.values(dimensionStats)) {
|
|
582
|
+
if (stat.tokenUsage) {
|
|
583
|
+
totalTokenUsage.input += stat.tokenUsage.input || 0;
|
|
584
|
+
totalTokenUsage.output += stat.tokenUsage.output || 0;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
455
588
|
logger.info([
|
|
456
589
|
`[Bootstrap-v3] ═══ Pipeline complete ═══`,
|
|
457
590
|
` Candidates: ${candidateResults.created} created, ${candidateResults.errors.length} errors`,
|
|
458
591
|
` Skills: ${skillResults.created} created, ${skillResults.failed} failed`,
|
|
459
592
|
` Time: ${totalTimeMs}ms (${(totalTimeMs / 1000).toFixed(1)}s)`,
|
|
460
593
|
` Mode: ${enableParallel ? `parallel (concurrency=${concurrency})` : 'serial'}`,
|
|
461
|
-
|
|
594
|
+
` Tokens: input=${totalTokenUsage.input}, output=${totalTokenUsage.output}`,
|
|
595
|
+
` Tool calls: ${totalToolCalls}`,
|
|
596
|
+
skippedDims.length > 0 ? ` Checkpoints restored: [${skippedDims.join(', ')}]` : '',
|
|
597
|
+
].filter(Boolean).join('\n'));
|
|
598
|
+
|
|
599
|
+
// P4.2: 生成冷启动报告
|
|
600
|
+
try {
|
|
601
|
+
const report = {
|
|
602
|
+
version: '2.7.0',
|
|
603
|
+
timestamp: new Date().toISOString(),
|
|
604
|
+
project: {
|
|
605
|
+
name: projectInfo.name,
|
|
606
|
+
files: projectInfo.fileCount,
|
|
607
|
+
lang: projectInfo.lang,
|
|
608
|
+
},
|
|
609
|
+
duration: {
|
|
610
|
+
totalMs: totalTimeMs,
|
|
611
|
+
totalSec: Math.round(totalTimeMs / 1000),
|
|
612
|
+
},
|
|
613
|
+
dimensions: {},
|
|
614
|
+
totals: {
|
|
615
|
+
candidates: candidateResults.created,
|
|
616
|
+
skills: skillResults.created,
|
|
617
|
+
toolCalls: totalToolCalls,
|
|
618
|
+
tokenUsage: totalTokenUsage,
|
|
619
|
+
errors: candidateResults.errors.length,
|
|
620
|
+
},
|
|
621
|
+
checkpoints: {
|
|
622
|
+
restored: skippedDims,
|
|
623
|
+
},
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
for (const [dimId, stat] of Object.entries(dimensionStats)) {
|
|
627
|
+
report.dimensions[dimId] = {
|
|
628
|
+
candidatesSubmitted: stat.candidateCount || 0,
|
|
629
|
+
candidatesRejected: stat.rejectedCount || 0,
|
|
630
|
+
analysisChars: stat.analysisChars || 0,
|
|
631
|
+
referencedFiles: stat.referencedFiles || 0,
|
|
632
|
+
durationMs: stat.durationMs || 0,
|
|
633
|
+
toolCallCount: stat.toolCallCount || 0,
|
|
634
|
+
tokenUsage: stat.tokenUsage || { input: 0, output: 0 },
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
const reportDir = path.join(projectRoot, '.autosnippet');
|
|
639
|
+
await fs.mkdir(reportDir, { recursive: true });
|
|
640
|
+
await fs.writeFile(
|
|
641
|
+
path.join(reportDir, 'bootstrap-report.json'),
|
|
642
|
+
JSON.stringify(report, null, 2),
|
|
643
|
+
);
|
|
644
|
+
logger.info(`[Bootstrap-v3] 📊 Bootstrap report saved to .autosnippet/bootstrap-report.json`);
|
|
645
|
+
} catch (reportErr) {
|
|
646
|
+
logger.warn(`[Bootstrap-v3] Bootstrap report generation failed: ${reportErr.message}`);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// P3: 成功完成后清理 checkpoints
|
|
650
|
+
await clearCheckpoints(projectRoot);
|
|
462
651
|
|
|
463
652
|
// 释放文件缓存
|
|
464
653
|
allFiles = null;
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* TierScheduler.js — 维度分层并行调度器
|
|
3
3
|
*
|
|
4
|
-
* 按维度间信息依赖关系分
|
|
4
|
+
* 按维度间信息依赖关系分 3 层执行:
|
|
5
5
|
* - Tier 1: 基础数据层 (project-profile, objc-deep-scan, category-scan) — 可并行
|
|
6
|
-
* - Tier 2:
|
|
7
|
-
* - Tier 3:
|
|
8
|
-
* - Tier 4: 总结层 (best-practice, agent-guidelines) — 依赖全部
|
|
6
|
+
* - Tier 2: 规范+架构+模式 (code-standard, architecture, code-pattern) — 依赖 Tier 1
|
|
7
|
+
* - Tier 3: 流转+实践+总结 (event-and-data-flow, best-practice, agent-guidelines) — 依赖 Tier 2
|
|
9
8
|
*
|
|
10
9
|
* 每层内部可并行 (受 concurrency 限制),层间串行。
|
|
11
10
|
*
|
|
@@ -21,10 +20,9 @@ const logger = Logger.getInstance();
|
|
|
21
20
|
// ──────────────────────────────────────────────────────────────────
|
|
22
21
|
|
|
23
22
|
const DEFAULT_TIERS = [
|
|
24
|
-
['project-profile', 'objc-deep-scan', 'category-scan'],
|
|
25
|
-
['code-standard', 'architecture'],
|
|
26
|
-
['
|
|
27
|
-
['best-practice', 'agent-guidelines'], // Tier 4: 总结
|
|
23
|
+
['project-profile', 'objc-deep-scan', 'category-scan'], // Tier 1: 基础数据
|
|
24
|
+
['code-standard', 'architecture', 'code-pattern'], // Tier 2: 规范+架构+模式
|
|
25
|
+
['event-and-data-flow', 'best-practice', 'agent-guidelines'], // Tier 3: 流转+实践+总结
|
|
28
26
|
];
|
|
29
27
|
|
|
30
28
|
// ──────────────────────────────────────────────────────────────────
|
|
@@ -79,13 +77,13 @@ export class TierScheduler {
|
|
|
79
77
|
*
|
|
80
78
|
* @param {Function} executeDimension — async (dimId) => DimensionResult
|
|
81
79
|
* @param {object} [options]
|
|
82
|
-
* @param {number} [options.concurrency=
|
|
80
|
+
* @param {number} [options.concurrency=3] — Tier 内最大并行数
|
|
83
81
|
* @param {Function} [options.onTierComplete] — (tierIndex, tierResults) => void
|
|
84
82
|
* @param {Function} [options.shouldAbort] — () => boolean — 外部中止信号
|
|
85
83
|
* @returns {Promise<Map<string, any>>} — dimId → result
|
|
86
84
|
*/
|
|
87
85
|
async execute(executeDimension, options = {}) {
|
|
88
|
-
const { concurrency =
|
|
86
|
+
const { concurrency = 3, onTierComplete, shouldAbort } = options;
|
|
89
87
|
const results = new Map();
|
|
90
88
|
|
|
91
89
|
for (let tierIndex = 0; tierIndex < this.#tiers.length; tierIndex++) {
|
|
@@ -41,6 +41,7 @@ import { envelope } from '../envelope.js';
|
|
|
41
41
|
import { inferLang, detectPrimaryLanguage, buildLanguageExtension } from './LanguageExtensions.js';
|
|
42
42
|
import { inferTargetRole, inferFilePriority } from './TargetClassifier.js';
|
|
43
43
|
import { analyzeProject, generateContextForAgent, isAvailable as astIsAvailable } from '../../../core/AstAnalyzer.js';
|
|
44
|
+
import pathGuard from '../../../shared/PathGuard.js';
|
|
44
45
|
|
|
45
46
|
// ── Sub-modules ──
|
|
46
47
|
import { loadBootstrapSkills, extractSkillDimensionGuides, enhanceDimensions } from './bootstrap/skills.js';
|
|
@@ -67,6 +68,13 @@ export { loadBootstrapSkills };
|
|
|
67
68
|
export async function bootstrapKnowledge(ctx, args) {
|
|
68
69
|
const t0 = Date.now();
|
|
69
70
|
const projectRoot = process.env.ASD_PROJECT_DIR || process.cwd();
|
|
71
|
+
|
|
72
|
+
// 路径安全守卫 — 确保所有写操作限制在项目目录内
|
|
73
|
+
if (!pathGuard.configured) {
|
|
74
|
+
const { default: Bootstrap } = await import('../../../bootstrap.js');
|
|
75
|
+
Bootstrap.configurePathGuard(projectRoot);
|
|
76
|
+
}
|
|
77
|
+
|
|
70
78
|
const maxFiles = args.maxFiles || 500;
|
|
71
79
|
const skipGuard = args.skipGuard || false;
|
|
72
80
|
const contentMaxLines = args.contentMaxLines || 120;
|