autosnippet 3.2.8 → 3.2.10
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/bin/cli.js +6 -5
- package/dashboard/dist/assets/index-BTAsOZv2.js +128 -0
- package/dashboard/dist/assets/index-C_72Ct98.css +1 -0
- package/dashboard/dist/index.html +2 -2
- package/lib/cli/AiScanService.js +23 -26
- package/lib/cli/SetupService.js +1 -1
- package/lib/cli/deploy/FileManifest.js +1 -1
- package/lib/core/AstAnalyzer.js +1 -1
- package/lib/core/discovery/index.js +2 -2
- package/lib/external/ai/AiProvider.js +66 -172
- package/lib/external/ai/providers/GoogleGeminiProvider.js +29 -5
- package/lib/external/mcp/handlers/bootstrap/BootstrapSession.js +1 -1
- package/lib/external/mcp/handlers/bootstrap/ExternalSubmissionTracker.js +3 -3
- package/lib/external/mcp/handlers/bootstrap/MissionBriefingBuilder.js +1 -1
- package/lib/external/mcp/handlers/bootstrap/pipeline/IncrementalBootstrap.js +1 -1
- package/lib/external/mcp/handlers/bootstrap/pipeline/dimension-context.js +8 -8
- package/lib/external/mcp/handlers/bootstrap/pipeline/noAiFallback.js +1 -1
- package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +291 -204
- package/lib/external/mcp/handlers/bootstrap/shared/bootstrap-phases.js +7 -6
- package/lib/external/mcp/handlers/bootstrap/shared/dimension-sop.js +1 -1
- package/lib/external/mcp/handlers/bootstrap-internal.js +2 -2
- package/lib/external/mcp/handlers/dimension-complete-external.js +6 -6
- package/lib/http/HttpServer.js +1 -1
- package/lib/http/middleware/requestLogger.js +1 -0
- package/lib/http/routes/ai.js +240 -35
- package/lib/http/routes/candidates.js +2 -3
- package/lib/http/routes/extract.js +13 -11
- package/lib/http/routes/modules.js +2 -2
- package/lib/http/routes/recipes.js +5 -5
- package/lib/http/routes/remote.js +134 -255
- package/lib/http/routes/violations.js +0 -54
- package/lib/http/utils/sse-sessions.js +1 -1
- package/lib/infrastructure/logging/Logger.js +5 -4
- package/lib/infrastructure/monitoring/PerformanceMonitor.js +3 -2
- package/lib/injection/ServiceContainer.js +64 -17
- package/lib/platform/ScreenCaptureService.js +177 -0
- package/lib/platform/ios/routes/spm.js +2 -2
- package/lib/service/agent/AgentEventBus.js +207 -0
- package/lib/service/agent/AgentFactory.js +535 -0
- package/lib/service/agent/AgentMessage.js +240 -0
- package/lib/service/agent/AgentRouter.js +228 -0
- package/lib/service/agent/AgentRuntime.js +1056 -0
- package/lib/service/agent/AgentState.js +217 -0
- package/lib/service/agent/IntentClassifier.js +331 -0
- package/lib/service/agent/LarkTransport.js +389 -0
- package/lib/service/agent/capabilities.js +409 -0
- package/lib/service/{chat → agent/context}/ContextWindow.js +37 -12
- package/lib/service/{chat → agent/context}/ExplorationTracker.js +112 -33
- package/lib/service/{chat → agent/core}/ChatAgentPrompts.js +5 -3
- package/lib/service/agent/core/LoopContext.js +170 -0
- package/lib/service/agent/core/MessageAdapter.js +223 -0
- package/lib/service/agent/core/ToolExecutionPipeline.js +376 -0
- package/lib/service/{chat → agent/domain}/ChatAgentTasks.js +15 -98
- package/lib/service/{chat → agent/domain}/EpisodicConsolidator.js +7 -7
- package/lib/service/{chat → agent/domain}/EvidenceCollector.js +4 -2
- package/lib/service/{chat/AnalystAgent.js → agent/domain/insight-analyst.js} +37 -172
- package/lib/service/{chat/HandoffProtocol.js → agent/domain/insight-gate.js} +85 -135
- package/lib/service/agent/domain/insight-producer.js +270 -0
- package/lib/service/agent/domain/scan-prompts.js +444 -0
- package/lib/service/agent/forced-summary.js +266 -0
- package/lib/service/agent/index.js +91 -0
- package/lib/service/{chat → agent}/memory/ActiveContext.js +29 -1
- package/lib/service/{chat → agent}/memory/MemoryCoordinator.js +7 -7
- package/lib/service/{chat/ProjectSemanticMemory.js → agent/memory/PersistentMemory.js} +359 -89
- package/lib/service/{chat → agent}/memory/SessionStore.js +1 -1
- package/lib/service/{chat → agent}/memory/index.js +1 -1
- package/lib/service/agent/policies.js +442 -0
- package/lib/service/agent/presets.js +305 -0
- package/lib/service/agent/strategies.js +756 -0
- package/lib/service/{chat → agent/tools}/ToolRegistry.js +3 -3
- package/lib/service/agent/tools/ai-analysis.js +75 -0
- package/lib/service/{chat → agent}/tools/composite.js +2 -1
- package/lib/service/{chat → agent}/tools/guard.js +1 -121
- package/lib/service/{chat → agent}/tools/index.js +27 -21
- package/lib/service/{chat → agent}/tools/infrastructure.js +1 -1
- package/lib/service/agent/tools/knowledge-graph.js +112 -0
- package/lib/service/agent/tools/scan-recipe.js +189 -0
- package/lib/service/agent/tools/system-interaction.js +476 -0
- package/lib/service/automation/DirectiveDetector.js +0 -1
- package/lib/service/automation/FileWatcher.js +0 -8
- package/lib/service/automation/handlers/CreateHandler.js +7 -3
- package/lib/service/automation/handlers/DraftHandler.js +7 -6
- package/lib/service/module/ModuleService.js +40 -73
- package/lib/service/skills/SignalCollector.js +26 -19
- package/lib/service/snippet/codecs/VSCodeCodec.js +1 -1
- package/lib/shared/FieldSpec.js +1 -1
- package/lib/shared/StyleGuide.js +1 -1
- package/package.json +4 -1
- package/resources/native-ui/screenshot.swift +228 -0
- package/dashboard/dist/assets/index-D5jiDBQG.css +0 -1
- package/dashboard/dist/assets/index-e5OKj-Ni.js +0 -128
- package/lib/core/discovery/SpmDiscoverer.js +0 -5
- package/lib/external/mcp/handlers/bootstrap/pipeline/EpisodicMemory.js +0 -750
- package/lib/external/mcp/handlers/bootstrap/pipeline/ToolResultCache.js +0 -277
- package/lib/http/routes/spm.js +0 -5
- package/lib/infrastructure/external/XcodeAutomation.js +0 -15
- package/lib/service/chat/ChatAgent.js +0 -1602
- package/lib/service/chat/Memory.js +0 -161
- package/lib/service/chat/ProducerAgent.js +0 -431
- package/lib/service/chat/ReasoningTrace.js +0 -523
- package/lib/service/chat/TaskPipeline.js +0 -357
- package/lib/service/chat/WorkingMemory.js +0 -359
- package/lib/service/chat/memory/PersistentMemory.js +0 -450
- package/lib/service/chat/tools/ai-analysis.js +0 -267
- package/lib/service/chat/tools/knowledge-graph.js +0 -234
- package/lib/service/chat/tools.js +0 -18
- package/lib/service/snippet/PlaceholderConverter.js +0 -5
- package/lib/service/snippet/codecs/XcodeCodec.js +0 -5
- /package/lib/service/{chat → agent}/ConversationStore.js +0 -0
- /package/lib/service/{chat → agent}/tools/_shared.js +0 -0
- /package/lib/service/{chat → agent}/tools/ast-graph.js +0 -0
- /package/lib/service/{chat → agent}/tools/lifecycle.js +0 -0
- /package/lib/service/{chat → agent}/tools/project-access.js +0 -0
- /package/lib/service/{chat → agent}/tools/query.js +0 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scan-recipe.js — 扫描专用 Recipe 收集工具
|
|
3
|
+
*
|
|
4
|
+
* 与冷启动 submit_knowledge 使用完全相同的字段 schema,
|
|
5
|
+
* 但不入库 — 仅做本地验证 + 内存收集。
|
|
6
|
+
*
|
|
7
|
+
* 扫描 Produce 阶段 LLM 调用此工具逐个提交 Recipe,
|
|
8
|
+
* 执行完成后由 AgentFactory.scanKnowledge() 从 toolCalls 中提取。
|
|
9
|
+
*
|
|
10
|
+
* 设计原因:
|
|
11
|
+
* - 冷启动 Producer 通过 submit_knowledge 工具逐个提交候选(工具驱动)
|
|
12
|
+
* - 扫描 Produce 之前是纯 JSON 文本输出,LLM 容易 hallucinate 错误工具调用
|
|
13
|
+
* - 统一为工具驱动模式:相同 schema → 相同字段质量 → 相同下游消费
|
|
14
|
+
*
|
|
15
|
+
* @module scan-recipe
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
// ── collect_scan_recipe ──────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
export const collectScanRecipe = {
|
|
21
|
+
name: 'collect_scan_recipe',
|
|
22
|
+
description:
|
|
23
|
+
'提交一条扫描发现的知识候选(Recipe)。每个独立的代码模式/设计模式/最佳实践应单独调用此工具提交。\n' +
|
|
24
|
+
'所有必填字段必须在单次调用中一次性提供。\n' +
|
|
25
|
+
'⚠️ content 必须是对象: { "pattern": "代码片段", "markdown": "项目特写正文≥200字", "rationale": "设计原理" }\n' +
|
|
26
|
+
'⚠️ reasoning 必须是对象: { "whyStandard": "原因", "sources": ["file.ts"], "confidence": 0.85 }',
|
|
27
|
+
parameters: {
|
|
28
|
+
type: 'object',
|
|
29
|
+
properties: {
|
|
30
|
+
// ── 基本信息 ──
|
|
31
|
+
title: { type: 'string', description: '中文标题(≤20字,使用项目真实类名)' },
|
|
32
|
+
language: { type: 'string', description: '编程语言(小写)' },
|
|
33
|
+
description: { type: 'string', description: '中文简述 ≤80 字,引用真实类名' },
|
|
34
|
+
tags: { type: 'array', items: { type: 'string' }, description: '标签列表' },
|
|
35
|
+
|
|
36
|
+
// ── 内容(V3 content 子对象) ──
|
|
37
|
+
content: {
|
|
38
|
+
type: 'object',
|
|
39
|
+
description:
|
|
40
|
+
'{ markdown: "项目特写 Markdown(≥200字)", pattern: "核心代码 3-8 行", rationale: "设计原理(必填)" }',
|
|
41
|
+
properties: {
|
|
42
|
+
pattern: { type: 'string', description: '核心代码片段' },
|
|
43
|
+
markdown: { type: 'string', description: 'Markdown 正文(≥200字符)' },
|
|
44
|
+
rationale: { type: 'string', description: '设计原理说明(必填)' },
|
|
45
|
+
},
|
|
46
|
+
required: ['rationale'],
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
// ── Cursor 交付(必填)──
|
|
50
|
+
kind: {
|
|
51
|
+
type: 'string',
|
|
52
|
+
enum: ['rule', 'pattern', 'fact'],
|
|
53
|
+
description: 'rule=规则 | pattern=模板 | fact=参考',
|
|
54
|
+
},
|
|
55
|
+
category: {
|
|
56
|
+
type: 'string',
|
|
57
|
+
description: '分类: View / Service / Tool / Model / Network / Storage / UI / Utility',
|
|
58
|
+
},
|
|
59
|
+
trigger: { type: 'string', description: '触发关键词(@前缀,kebab-case)' },
|
|
60
|
+
doClause: { type: 'string', description: '正向指令(英文祈使句 ≤60 tokens)' },
|
|
61
|
+
dontClause: { type: 'string', description: '反向约束(描述禁止的做法)' },
|
|
62
|
+
whenClause: { type: 'string', description: '触发场景(描述何时适用)' },
|
|
63
|
+
coreCode: { type: 'string', description: '精华代码骨架(3-8行,语法完整)' },
|
|
64
|
+
|
|
65
|
+
// ── 结构化字段 ──
|
|
66
|
+
headers: {
|
|
67
|
+
type: 'array',
|
|
68
|
+
items: { type: 'string' },
|
|
69
|
+
description: '完整 import/include 语句数组',
|
|
70
|
+
},
|
|
71
|
+
usageGuide: { type: 'string', description: '使用指南(何时/如何使用)' },
|
|
72
|
+
knowledgeType: {
|
|
73
|
+
type: 'string',
|
|
74
|
+
description: 'code-pattern / architecture / best-practice / code-standard / data-flow / solution 等',
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
// ── 推理 ──
|
|
78
|
+
reasoning: {
|
|
79
|
+
type: 'object',
|
|
80
|
+
description: '{ whyStandard: "原因", sources: ["file.ts"], confidence: 0.85 }',
|
|
81
|
+
properties: {
|
|
82
|
+
whyStandard: { type: 'string', description: '为什么这是标准做法(必填)' },
|
|
83
|
+
sources: { type: 'array', items: { type: 'string' }, description: '参考的文件路径数组(必填)' },
|
|
84
|
+
confidence: { type: 'number', description: '置信度 0.0-1.0' },
|
|
85
|
+
},
|
|
86
|
+
required: ['whyStandard', 'sources'],
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
// ── 可选 ──
|
|
90
|
+
complexity: { type: 'string', enum: ['beginner', 'intermediate', 'advanced'] },
|
|
91
|
+
scope: { type: 'string', enum: ['universal', 'project-specific', 'target-specific'] },
|
|
92
|
+
},
|
|
93
|
+
required: [
|
|
94
|
+
'title', 'language', 'content', 'kind',
|
|
95
|
+
'doClause', 'dontClause', 'whenClause', 'coreCode',
|
|
96
|
+
'category', 'trigger', 'description',
|
|
97
|
+
'headers', 'usageGuide', 'knowledgeType', 'reasoning',
|
|
98
|
+
],
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Handler — 本地验证 + 内存收集(不入库)
|
|
103
|
+
*
|
|
104
|
+
* 验证通过后返回 { status: 'collected', recipe: {...} },
|
|
105
|
+
* AgentFactory.scanKnowledge() 从 toolCalls 结果中提取 recipes。
|
|
106
|
+
*/
|
|
107
|
+
handler: async (params, _ctx) => {
|
|
108
|
+
// ── 基本验证 ──
|
|
109
|
+
const errors = [];
|
|
110
|
+
|
|
111
|
+
if (!params.title || params.title.trim().length === 0) {
|
|
112
|
+
errors.push('title 不能为空');
|
|
113
|
+
}
|
|
114
|
+
if (!params.content || typeof params.content !== 'object') {
|
|
115
|
+
errors.push('content 必须是对象');
|
|
116
|
+
} else if (!params.content.rationale) {
|
|
117
|
+
errors.push('content.rationale (设计原理) 是必填字段');
|
|
118
|
+
}
|
|
119
|
+
if (!params.reasoning || typeof params.reasoning !== 'object') {
|
|
120
|
+
errors.push('reasoning 必须是对象');
|
|
121
|
+
} else {
|
|
122
|
+
if (!params.reasoning.whyStandard) errors.push('reasoning.whyStandard 是必填字段');
|
|
123
|
+
if (!Array.isArray(params.reasoning.sources) || params.reasoning.sources.length === 0) {
|
|
124
|
+
errors.push('reasoning.sources 必须是非空数组');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (!params.kind || !['rule', 'pattern', 'fact'].includes(params.kind)) {
|
|
128
|
+
errors.push('kind 必须是 rule / pattern / fact 之一');
|
|
129
|
+
}
|
|
130
|
+
if (!params.trigger || !params.trigger.startsWith('@')) {
|
|
131
|
+
errors.push('trigger 必须以 @ 开头');
|
|
132
|
+
}
|
|
133
|
+
if (!params.coreCode || params.coreCode.trim().length < 10) {
|
|
134
|
+
errors.push('coreCode 必须提供有意义的代码骨架(≥10字符)');
|
|
135
|
+
}
|
|
136
|
+
if (!params.doClause) {
|
|
137
|
+
errors.push('doClause (正向指令) 是必填字段');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (errors.length > 0) {
|
|
141
|
+
return {
|
|
142
|
+
status: 'rejected',
|
|
143
|
+
error: errors.join('\n'),
|
|
144
|
+
hint: '请根据错误信息调整内容后重新提交。',
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ── 构建标准化 Recipe 对象 ──
|
|
149
|
+
const contentObj = params.content || {};
|
|
150
|
+
const reasoning = params.reasoning || {};
|
|
151
|
+
|
|
152
|
+
const recipe = {
|
|
153
|
+
title: params.title.trim(),
|
|
154
|
+
language: params.language || '',
|
|
155
|
+
description: params.description || '',
|
|
156
|
+
tags: params.tags || [],
|
|
157
|
+
content: {
|
|
158
|
+
pattern: contentObj.pattern || '',
|
|
159
|
+
markdown: contentObj.markdown || '',
|
|
160
|
+
rationale: contentObj.rationale || '',
|
|
161
|
+
},
|
|
162
|
+
kind: params.kind,
|
|
163
|
+
category: params.category || 'Utility',
|
|
164
|
+
trigger: params.trigger,
|
|
165
|
+
doClause: params.doClause || '',
|
|
166
|
+
dontClause: params.dontClause || '',
|
|
167
|
+
whenClause: params.whenClause || '',
|
|
168
|
+
coreCode: params.coreCode || '',
|
|
169
|
+
headers: params.headers || [],
|
|
170
|
+
usageGuide: params.usageGuide || '',
|
|
171
|
+
knowledgeType: params.knowledgeType || 'code-pattern',
|
|
172
|
+
reasoning: {
|
|
173
|
+
whyStandard: reasoning.whyStandard || '',
|
|
174
|
+
sources: reasoning.sources || [],
|
|
175
|
+
confidence: reasoning.confidence ?? 0.8,
|
|
176
|
+
},
|
|
177
|
+
complexity: params.complexity || 'intermediate',
|
|
178
|
+
scope: params.scope || 'project-specific',
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
status: 'collected',
|
|
183
|
+
title: recipe.title,
|
|
184
|
+
recipe,
|
|
185
|
+
};
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
export default collectScanRecipe;
|
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* system-interaction.js — 系统交互工具 (3)
|
|
3
|
+
*
|
|
4
|
+
* 为 Agent 提供与本地操作系统交互的能力:
|
|
5
|
+
*
|
|
6
|
+
* 1. run_safe_command 安全执行终端命令 (受 SafetyPolicy 约束)
|
|
7
|
+
* 2. write_project_file 写入/创建项目文件 (受文件范围约束)
|
|
8
|
+
* 3. get_environment_info 获取运行环境信息
|
|
9
|
+
*
|
|
10
|
+
* ⚠️ 安全设计:
|
|
11
|
+
* - run_safe_command 在工具层即执行命令黑名单/白名单检查
|
|
12
|
+
* - write_project_file 在工具层即执行文件路径范围检查
|
|
13
|
+
* - 两者均依赖 AgentRuntime 注入的 safetyPolicy 上下文
|
|
14
|
+
* - 即使 safetyPolicy 未注入,工具自身也有基础安全兜底
|
|
15
|
+
*
|
|
16
|
+
* @module system-interaction
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { execFile } from 'node:child_process';
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
import os from 'node:os';
|
|
22
|
+
import path from 'node:path';
|
|
23
|
+
import { promisify } from 'node:util';
|
|
24
|
+
|
|
25
|
+
const execFileAsync = promisify(execFile);
|
|
26
|
+
|
|
27
|
+
// ─── 常量 ────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
/** 工具层兜底: 始终拒绝的危险命令模式 (无论 SafetyPolicy 是否注入) */
|
|
30
|
+
const HARDCODED_BLACKLIST = [
|
|
31
|
+
/\brm\s+-rf\s+[\/~]/,
|
|
32
|
+
/\bsudo\b/,
|
|
33
|
+
/\bmkfs\b/,
|
|
34
|
+
/\bdd\s+if=/,
|
|
35
|
+
/\b(shutdown|reboot|halt)\b/,
|
|
36
|
+
/>\s*\/dev\//,
|
|
37
|
+
/\bcurl\b.*\|\s*(bash|sh)/,
|
|
38
|
+
/\bchmod\s+777/,
|
|
39
|
+
/\bpasswd\b/,
|
|
40
|
+
/\bkillall\b/,
|
|
41
|
+
/\bfork\s*bomb/i,
|
|
42
|
+
/:\(\)\s*\{\s*:\|:\s*&\s*\}\s*;/, // fork bomb pattern
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
/** 工具层兜底: 无 SafetyPolicy 时仅允许的安全命令前缀 */
|
|
46
|
+
const FALLBACK_SAFE_PREFIXES = [
|
|
47
|
+
'ls', 'cat', 'head', 'tail', 'grep', 'find', 'wc',
|
|
48
|
+
'echo', 'pwd', 'date', 'which', 'file', 'stat',
|
|
49
|
+
'git log', 'git status', 'git diff', 'git branch', 'git show',
|
|
50
|
+
'npm list', 'npm outdated', 'node -v', 'npm -v',
|
|
51
|
+
'python --version', 'python3 --version',
|
|
52
|
+
'env', 'printenv',
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
/** 命令执行超时 (ms) */
|
|
56
|
+
const COMMAND_TIMEOUT = 30_000;
|
|
57
|
+
|
|
58
|
+
/** 输出截断长度 (bytes) */
|
|
59
|
+
const MAX_OUTPUT_LENGTH = 16_000;
|
|
60
|
+
|
|
61
|
+
/** 文件写入最大尺寸 (bytes) */
|
|
62
|
+
const MAX_WRITE_SIZE = 512 * 1024;
|
|
63
|
+
|
|
64
|
+
// ─── 内部工具函数 ────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 硬编码黑名单检查 — 工具层兜底, 无论是否有 SafetyPolicy 都生效
|
|
68
|
+
*/
|
|
69
|
+
function _isHardBlacklisted(command) {
|
|
70
|
+
for (const pattern of HARDCODED_BLACKLIST) {
|
|
71
|
+
if (pattern.test(command)) return true;
|
|
72
|
+
}
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 无 SafetyPolicy 时的白名单兜底
|
|
78
|
+
*/
|
|
79
|
+
function _isFallbackSafe(command) {
|
|
80
|
+
const trimmed = command.trim();
|
|
81
|
+
return FALLBACK_SAFE_PREFIXES.some(prefix => trimmed.startsWith(prefix));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 截断过长输出
|
|
86
|
+
*/
|
|
87
|
+
function _truncate(text, max = MAX_OUTPUT_LENGTH) {
|
|
88
|
+
if (!text || text.length <= max) return text;
|
|
89
|
+
return `${text.slice(0, max)}\n\n... [输出已截断, 共 ${text.length} 字符]`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 获取 projectRoot — 优先从 context 获取, 兜底用 cwd
|
|
94
|
+
*/
|
|
95
|
+
function _getProjectRoot(ctx) {
|
|
96
|
+
return ctx.projectRoot || ctx.container?.get?.('projectRoot') || process.cwd();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ═══════════════════════════════════════════════════════
|
|
100
|
+
// 1. run_safe_command — 安全执行终端命令
|
|
101
|
+
// ═══════════════════════════════════════════════════════
|
|
102
|
+
|
|
103
|
+
export const runSafeCommand = {
|
|
104
|
+
name: 'run_safe_command',
|
|
105
|
+
description:
|
|
106
|
+
'在项目目录下安全执行终端命令。' +
|
|
107
|
+
'命令受安全策略约束: 危险命令(sudo/rm -rf/shutdown 等)被自动拦截。' +
|
|
108
|
+
'适用于: 查看 git 状态、运行测试、检查依赖版本、执行构建等。' +
|
|
109
|
+
'超时 30 秒, 输出超过 16KB 会被截断。' +
|
|
110
|
+
'如果需要管道或重定向, 请用 sh -c "..." 包装。',
|
|
111
|
+
parameters: {
|
|
112
|
+
type: 'object',
|
|
113
|
+
properties: {
|
|
114
|
+
command: {
|
|
115
|
+
type: 'string',
|
|
116
|
+
description: '要执行的终端命令, 如 "git status" 或 "npm test"',
|
|
117
|
+
},
|
|
118
|
+
cwd: {
|
|
119
|
+
type: 'string',
|
|
120
|
+
description: '工作目录 (相对于项目根目录), 缺省为项目根目录',
|
|
121
|
+
},
|
|
122
|
+
timeout: {
|
|
123
|
+
type: 'number',
|
|
124
|
+
description: '超时时间(毫秒), 默认 30000',
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
required: ['command'],
|
|
128
|
+
},
|
|
129
|
+
handler: async (params, ctx) => {
|
|
130
|
+
const { command, cwd, timeout } = params;
|
|
131
|
+
const projectRoot = _getProjectRoot(ctx);
|
|
132
|
+
|
|
133
|
+
if (!command || typeof command !== 'string' || command.trim().length === 0) {
|
|
134
|
+
return { error: '命令不能为空' };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ── 安全检查 Layer 1: 硬编码黑名单 (无条件拦截) ──
|
|
138
|
+
if (_isHardBlacklisted(command)) {
|
|
139
|
+
return { error: `安全拦截: 命令 "${command}" 匹配危险模式, 已被阻止执行` };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ── 安全检查 Layer 2: SafetyPolicy (如果注入) ──
|
|
143
|
+
const safetyPolicy = ctx.safetyPolicy || null;
|
|
144
|
+
if (safetyPolicy) {
|
|
145
|
+
const check = safetyPolicy.checkCommand(command);
|
|
146
|
+
if (!check.safe) {
|
|
147
|
+
return { error: `SafetyPolicy 拦截: ${check.reason}` };
|
|
148
|
+
}
|
|
149
|
+
} else {
|
|
150
|
+
// 无 SafetyPolicy 时使用白名单兜底
|
|
151
|
+
if (!_isFallbackSafe(command)) {
|
|
152
|
+
return {
|
|
153
|
+
error: `无安全策略: 命令 "${command}" 不在安全白名单中。` +
|
|
154
|
+
`允许的命令前缀: ${FALLBACK_SAFE_PREFIXES.join(', ')}`,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ── 解析工作目录 ──
|
|
160
|
+
let workDir = projectRoot;
|
|
161
|
+
if (cwd) {
|
|
162
|
+
workDir = path.isAbsolute(cwd) ? cwd : path.resolve(projectRoot, cwd);
|
|
163
|
+
// 范围检查
|
|
164
|
+
if (!workDir.startsWith(path.resolve(projectRoot))) {
|
|
165
|
+
return { error: `工作目录 "${cwd}" 超出项目范围 "${projectRoot}"` };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (!fs.existsSync(workDir)) {
|
|
170
|
+
return { error: `工作目录 "${workDir}" 不存在` };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ── 执行命令 ──
|
|
174
|
+
const effectiveTimeout = timeout || COMMAND_TIMEOUT;
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
const { stdout, stderr } = await execFileAsync('sh', ['-c', command], {
|
|
178
|
+
cwd: workDir,
|
|
179
|
+
timeout: effectiveTimeout,
|
|
180
|
+
maxBuffer: 1024 * 1024, // 1MB 缓冲
|
|
181
|
+
env: {
|
|
182
|
+
...process.env,
|
|
183
|
+
// 禁用交互式 pager
|
|
184
|
+
GIT_PAGER: 'cat',
|
|
185
|
+
PAGER: 'cat',
|
|
186
|
+
LESS: '-FRX',
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
exitCode: 0,
|
|
192
|
+
stdout: _truncate(stdout),
|
|
193
|
+
stderr: _truncate(stderr),
|
|
194
|
+
command,
|
|
195
|
+
cwd: workDir,
|
|
196
|
+
};
|
|
197
|
+
} catch (err) {
|
|
198
|
+
// 超时
|
|
199
|
+
if (err.killed) {
|
|
200
|
+
return {
|
|
201
|
+
error: `命令执行超时 (${effectiveTimeout}ms)`,
|
|
202
|
+
command,
|
|
203
|
+
stdout: _truncate(err.stdout || ''),
|
|
204
|
+
stderr: _truncate(err.stderr || ''),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// 非零退出
|
|
209
|
+
return {
|
|
210
|
+
exitCode: err.code ?? 1,
|
|
211
|
+
stdout: _truncate(err.stdout || ''),
|
|
212
|
+
stderr: _truncate(err.stderr || err.message || ''),
|
|
213
|
+
command,
|
|
214
|
+
cwd: workDir,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
// ═══════════════════════════════════════════════════════
|
|
221
|
+
// 2. write_project_file — 写入项目文件
|
|
222
|
+
// ═══════════════════════════════════════════════════════
|
|
223
|
+
|
|
224
|
+
export const writeProjectFile = {
|
|
225
|
+
name: 'write_project_file',
|
|
226
|
+
description:
|
|
227
|
+
'在项目目录内创建或覆盖写入文件。' +
|
|
228
|
+
'自动创建不存在的中间目录。文件路径必须在项目范围内。' +
|
|
229
|
+
'适用于: 生成配置文件、创建代码文件、写入分析报告等。' +
|
|
230
|
+
'最大写入 512KB。',
|
|
231
|
+
parameters: {
|
|
232
|
+
type: 'object',
|
|
233
|
+
properties: {
|
|
234
|
+
filePath: {
|
|
235
|
+
type: 'string',
|
|
236
|
+
description: '目标文件路径 (相对于项目根目录或绝对路径)',
|
|
237
|
+
},
|
|
238
|
+
content: {
|
|
239
|
+
type: 'string',
|
|
240
|
+
description: '要写入的文件内容',
|
|
241
|
+
},
|
|
242
|
+
append: {
|
|
243
|
+
type: 'boolean',
|
|
244
|
+
description: '是否追加模式 (默认 false = 覆盖写入)',
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
required: ['filePath', 'content'],
|
|
248
|
+
},
|
|
249
|
+
handler: async (params, ctx) => {
|
|
250
|
+
const { filePath, content, append } = params;
|
|
251
|
+
const projectRoot = _getProjectRoot(ctx);
|
|
252
|
+
|
|
253
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
254
|
+
return { error: '文件路径不能为空' };
|
|
255
|
+
}
|
|
256
|
+
if (typeof content !== 'string') {
|
|
257
|
+
return { error: '文件内容必须为字符串' };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ── 大小限制 ──
|
|
261
|
+
if (Buffer.byteLength(content, 'utf-8') > MAX_WRITE_SIZE) {
|
|
262
|
+
return { error: `文件内容超过大小限制 (${MAX_WRITE_SIZE / 1024}KB)` };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ── 路径解析与安全检查 ──
|
|
266
|
+
const resolved = path.isAbsolute(filePath)
|
|
267
|
+
? path.resolve(filePath)
|
|
268
|
+
: path.resolve(projectRoot, filePath);
|
|
269
|
+
|
|
270
|
+
const scopeRoot = path.resolve(projectRoot);
|
|
271
|
+
if (!resolved.startsWith(scopeRoot + path.sep) && resolved !== scopeRoot) {
|
|
272
|
+
return { error: `文件路径 "${filePath}" 超出项目范围 "${projectRoot}"` };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// SafetyPolicy 路径检查
|
|
276
|
+
const safetyPolicy = ctx.safetyPolicy || null;
|
|
277
|
+
if (safetyPolicy) {
|
|
278
|
+
const check = safetyPolicy.checkFilePath(resolved);
|
|
279
|
+
if (!check.safe) {
|
|
280
|
+
return { error: `SafetyPolicy 拦截: ${check.reason}` };
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ── 危险路径兜底 ──
|
|
285
|
+
const dangerousPatterns = [
|
|
286
|
+
/node_modules\//,
|
|
287
|
+
/\.git\//,
|
|
288
|
+
/\.env$/,
|
|
289
|
+
/\.env\.local$/,
|
|
290
|
+
/package-lock\.json$/,
|
|
291
|
+
/yarn\.lock$/,
|
|
292
|
+
/pnpm-lock\.yaml$/,
|
|
293
|
+
];
|
|
294
|
+
const relPath = path.relative(scopeRoot, resolved);
|
|
295
|
+
for (const p of dangerousPatterns) {
|
|
296
|
+
if (p.test(relPath)) {
|
|
297
|
+
return { error: `安全拦截: 不允许写入 "${relPath}" (匹配受保护路径模式)` };
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ── 写入文件 ──
|
|
302
|
+
try {
|
|
303
|
+
// 确保目录存在
|
|
304
|
+
const dir = path.dirname(resolved);
|
|
305
|
+
if (!fs.existsSync(dir)) {
|
|
306
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (append) {
|
|
310
|
+
fs.appendFileSync(resolved, content, 'utf-8');
|
|
311
|
+
} else {
|
|
312
|
+
fs.writeFileSync(resolved, content, 'utf-8');
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const stat = fs.statSync(resolved);
|
|
316
|
+
return {
|
|
317
|
+
success: true,
|
|
318
|
+
filePath: relPath,
|
|
319
|
+
absolutePath: resolved,
|
|
320
|
+
size: stat.size,
|
|
321
|
+
mode: append ? 'append' : 'overwrite',
|
|
322
|
+
};
|
|
323
|
+
} catch (err) {
|
|
324
|
+
return { error: `写入文件失败: ${err.message}` };
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
// ═══════════════════════════════════════════════════════
|
|
330
|
+
// 3. get_environment_info — 获取运行环境信息
|
|
331
|
+
// ═══════════════════════════════════════════════════════
|
|
332
|
+
|
|
333
|
+
export const getEnvironmentInfo = {
|
|
334
|
+
name: 'get_environment_info',
|
|
335
|
+
description:
|
|
336
|
+
'获取当前运行环境的系统信息。' +
|
|
337
|
+
'包括: 操作系统、Node.js 版本、项目路径、Git 分支、依赖管理器等。' +
|
|
338
|
+
'适用于: 环境诊断、构建问题排查、项目状态检查。',
|
|
339
|
+
parameters: {
|
|
340
|
+
type: 'object',
|
|
341
|
+
properties: {
|
|
342
|
+
sections: {
|
|
343
|
+
type: 'array',
|
|
344
|
+
items: {
|
|
345
|
+
type: 'string',
|
|
346
|
+
enum: ['os', 'node', 'git', 'project', 'all'],
|
|
347
|
+
},
|
|
348
|
+
description: '要获取的信息部分, 默认 ["all"]',
|
|
349
|
+
},
|
|
350
|
+
},
|
|
351
|
+
required: [],
|
|
352
|
+
},
|
|
353
|
+
handler: async (params, ctx) => {
|
|
354
|
+
const sections = params.sections || ['all'];
|
|
355
|
+
const all = sections.includes('all');
|
|
356
|
+
const projectRoot = _getProjectRoot(ctx);
|
|
357
|
+
const info = {};
|
|
358
|
+
|
|
359
|
+
// ── OS 信息 ──
|
|
360
|
+
if (all || sections.includes('os')) {
|
|
361
|
+
info.os = {
|
|
362
|
+
platform: os.platform(),
|
|
363
|
+
arch: os.arch(),
|
|
364
|
+
release: os.release(),
|
|
365
|
+
hostname: os.hostname(),
|
|
366
|
+
uptime: `${Math.floor(os.uptime() / 3600)}h ${Math.floor((os.uptime() % 3600) / 60)}m`,
|
|
367
|
+
memory: {
|
|
368
|
+
total: `${Math.round(os.totalmem() / (1024 * 1024 * 1024))}GB`,
|
|
369
|
+
free: `${Math.round(os.freemem() / (1024 * 1024 * 1024))}GB`,
|
|
370
|
+
},
|
|
371
|
+
cpus: os.cpus().length,
|
|
372
|
+
shell: process.env.SHELL || process.env.COMSPEC || 'unknown',
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// ── Node 信息 ──
|
|
377
|
+
if (all || sections.includes('node')) {
|
|
378
|
+
info.node = {
|
|
379
|
+
version: process.version,
|
|
380
|
+
execPath: process.execPath,
|
|
381
|
+
pid: process.pid,
|
|
382
|
+
env: {
|
|
383
|
+
NODE_ENV: process.env.NODE_ENV || 'unset',
|
|
384
|
+
npm_package_version: process.env.npm_package_version || 'N/A',
|
|
385
|
+
},
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
// npm/pnpm/yarn 版本
|
|
389
|
+
for (const pm of ['npm', 'pnpm', 'yarn']) {
|
|
390
|
+
try {
|
|
391
|
+
const { stdout } = await execFileAsync(pm, ['--version'], {
|
|
392
|
+
timeout: 5000,
|
|
393
|
+
});
|
|
394
|
+
info.node[`${pm}_version`] = stdout.trim();
|
|
395
|
+
} catch {
|
|
396
|
+
// 未安装, 跳过
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// ── Git 信息 ──
|
|
402
|
+
if (all || sections.includes('git')) {
|
|
403
|
+
info.git = {};
|
|
404
|
+
try {
|
|
405
|
+
const { stdout: branch } = await execFileAsync(
|
|
406
|
+
'git', ['branch', '--show-current'],
|
|
407
|
+
{ cwd: projectRoot, timeout: 5000 },
|
|
408
|
+
);
|
|
409
|
+
info.git.branch = branch.trim();
|
|
410
|
+
|
|
411
|
+
const { stdout: status } = await execFileAsync(
|
|
412
|
+
'git', ['status', '--porcelain'],
|
|
413
|
+
{ cwd: projectRoot, timeout: 5000 },
|
|
414
|
+
);
|
|
415
|
+
const lines = status.trim().split('\n').filter(Boolean);
|
|
416
|
+
info.git.dirty = lines.length > 0;
|
|
417
|
+
info.git.changedFiles = lines.length;
|
|
418
|
+
|
|
419
|
+
const { stdout: lastCommit } = await execFileAsync(
|
|
420
|
+
'git', ['log', '-1', '--format=%h %s (%cr)'],
|
|
421
|
+
{ cwd: projectRoot, timeout: 5000 },
|
|
422
|
+
);
|
|
423
|
+
info.git.lastCommit = lastCommit.trim();
|
|
424
|
+
|
|
425
|
+
const { stdout: remoteUrl } = await execFileAsync(
|
|
426
|
+
'git', ['remote', 'get-url', 'origin'],
|
|
427
|
+
{ cwd: projectRoot, timeout: 5000 },
|
|
428
|
+
);
|
|
429
|
+
info.git.remote = remoteUrl.trim();
|
|
430
|
+
} catch {
|
|
431
|
+
info.git.error = '非 Git 仓库或 Git 未安装';
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// ── 项目信息 ──
|
|
436
|
+
if (all || sections.includes('project')) {
|
|
437
|
+
info.project = {
|
|
438
|
+
root: projectRoot,
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
// package.json
|
|
442
|
+
const pkgPath = path.join(projectRoot, 'package.json');
|
|
443
|
+
if (fs.existsSync(pkgPath)) {
|
|
444
|
+
try {
|
|
445
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
446
|
+
info.project.name = pkg.name;
|
|
447
|
+
info.project.version = pkg.version;
|
|
448
|
+
info.project.type = pkg.type || 'commonjs';
|
|
449
|
+
info.project.dependencies = Object.keys(pkg.dependencies || {}).length;
|
|
450
|
+
info.project.devDependencies = Object.keys(pkg.devDependencies || {}).length;
|
|
451
|
+
} catch { /* invalid package.json */ }
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Podfile / Cartfile / build.gradle / CMakeLists / Makefile 检测
|
|
455
|
+
const projectIndicators = [
|
|
456
|
+
{ file: 'Podfile', type: 'CocoaPods (iOS)' },
|
|
457
|
+
{ file: 'Cartfile', type: 'Carthage (iOS)' },
|
|
458
|
+
{ file: 'Package.swift', type: 'Swift Package Manager' },
|
|
459
|
+
{ file: 'build.gradle', type: 'Gradle (Android/Java)' },
|
|
460
|
+
{ file: 'pom.xml', type: 'Maven (Java)' },
|
|
461
|
+
{ file: 'CMakeLists.txt', type: 'CMake (C/C++)' },
|
|
462
|
+
{ file: 'Makefile', type: 'Make' },
|
|
463
|
+
{ file: 'Cargo.toml', type: 'Cargo (Rust)' },
|
|
464
|
+
{ file: 'go.mod', type: 'Go Modules' },
|
|
465
|
+
{ file: 'requirements.txt', type: 'pip (Python)' },
|
|
466
|
+
{ file: 'pyproject.toml', type: 'Python project' },
|
|
467
|
+
{ file: 'Gemfile', type: 'Bundler (Ruby)' },
|
|
468
|
+
];
|
|
469
|
+
info.project.buildSystems = projectIndicators
|
|
470
|
+
.filter(({ file }) => fs.existsSync(path.join(projectRoot, file)))
|
|
471
|
+
.map(({ type }) => type);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return info;
|
|
475
|
+
},
|
|
476
|
+
};
|