soloforge 1.2.10 → 1.2.12
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/dist/adapters/claude_code/hooks.d.ts +1 -1
- package/dist/adapters/claude_code/hooks.d.ts.map +1 -1
- package/dist/adapters/claude_code/hooks.js +11 -1
- package/dist/adapters/claude_code/hooks.js.map +1 -1
- package/dist/bin/soloforge.js +36 -0
- package/dist/bin/soloforge.js.map +1 -1
- package/dist/engine/batch1_scenario_registry.d.ts +6 -6
- package/dist/engine/batch1_scenario_registry.d.ts.map +1 -1
- package/dist/engine/batch1_scenario_registry.js +42 -7
- package/dist/engine/batch1_scenario_registry.js.map +1 -1
- package/dist/engine/batch1_scenario_runners.d.ts +12 -0
- package/dist/engine/batch1_scenario_runners.d.ts.map +1 -1
- package/dist/engine/batch1_scenario_runners.js +148 -0
- package/dist/engine/batch1_scenario_runners.js.map +1 -1
- package/dist/engine/input_material_extractor.d.ts +46 -0
- package/dist/engine/input_material_extractor.d.ts.map +1 -0
- package/dist/engine/input_material_extractor.js +155 -0
- package/dist/engine/input_material_extractor.js.map +1 -0
- package/dist/engine/intent_route_scorer.d.ts +45 -0
- package/dist/engine/intent_route_scorer.d.ts.map +1 -0
- package/dist/engine/intent_route_scorer.js +326 -0
- package/dist/engine/intent_route_scorer.js.map +1 -0
- package/dist/engine/intent_router.d.ts +58 -21
- package/dist/engine/intent_router.d.ts.map +1 -1
- package/dist/engine/intent_router.js +236 -217
- package/dist/engine/intent_router.js.map +1 -1
- package/dist/engine/intent_signal_extractor.d.ts +70 -0
- package/dist/engine/intent_signal_extractor.d.ts.map +1 -0
- package/dist/engine/intent_signal_extractor.js +235 -0
- package/dist/engine/intent_signal_extractor.js.map +1 -0
- package/dist/engine/task_context.d.ts.map +1 -1
- package/dist/engine/task_context.js +4 -0
- package/dist/engine/task_context.js.map +1 -1
- package/dist/types.d.ts +3 -12
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -1,28 +1,24 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Intent Router — 意图路由器
|
|
3
|
+
*
|
|
4
|
+
* 根据用户意图文本确定语义通道 (route) 和执行形态 (execution_shape)。
|
|
5
|
+
* 零 AI 依赖,纯规则匹配。
|
|
6
|
+
*
|
|
7
|
+
* 替代旧的 task_type -> strategy 入口裁决链路。
|
|
8
|
+
* task_type 保留为辅助描述,不再单独决定是否进入 SoloForge。
|
|
9
|
+
*
|
|
10
|
+
* v2: 模块化拆分 — 路由判定由"信号提取 + 归一化 + 打分 + 冲突仲裁"完成,
|
|
11
|
+
* 不再依赖单条整句正则。
|
|
12
|
+
*/
|
|
13
|
+
import { extractInputMaterials } from "./input_material_extractor.js";
|
|
14
|
+
import { extractSignals } from "./intent_signal_extractor.js";
|
|
15
|
+
import { scoreRoute } from "./intent_route_scorer.js";
|
|
2
16
|
console.error("[soloForge] 意图路由: 模块已加载");
|
|
3
|
-
// ──
|
|
4
|
-
/**
|
|
5
|
-
const SKIP_PATTERNS = [
|
|
6
|
-
{ pattern: /^(你好|hi|hello|hey|嗨|早上好|下午好|晚上好)[\s!!.。]*$/i, reason: "greeting" },
|
|
7
|
-
{ pattern: /^(今天|现在|几号|什么时候|什么时候了)/, reason: "casual_question" },
|
|
8
|
-
{ pattern: /不用\s*(soloForge|solo\s*forge)/i, reason: "explicit_skip" },
|
|
9
|
-
{ pattern: /直接(告诉|说|回答|给)/, reason: "explicit_skip" },
|
|
10
|
-
];
|
|
11
|
-
/** 通用知识问答模式(不需要代码执行) */
|
|
12
|
-
const GENERAL_QA_PATTERNS = [
|
|
13
|
-
/(?:什么(?:是|叫|意思))\s*(?:REST|API|DTO|DAO|SOA|DDD|TDD|BDD|CI\/CD|SQL|NoSQL|TypeScript|JavaScript|Java|Python|Go|Docker|K8s|Kubernetes|React|Vue|Spring|Node|Nginx|Redis|MySQL|MongoDB|GraphQL|gRPC|WebSocket|OAuth|JWT|SOLID|DRY|KISS|YAGNI)/i,
|
|
14
|
-
/^(?:TypeScript|Java|Python|Go|React|Vue|Spring)\s*(?:union type|泛型|接口|类|装饰器|闭包|协程|通道|组件|生命周期|状态管理|中间件)/i,
|
|
15
|
-
];
|
|
16
|
-
/** 纯解释类模式 */
|
|
17
|
-
const PURE_EXPLAIN_PATTERNS = [
|
|
18
|
-
/(?:解释|说明)(?:一下|下)?\s*(?:这个|这|这模块|这代码|这段|这个模块)\s*(?:怎么|如何|是什么|是什么意思|的|了)/,
|
|
19
|
-
/^(?:解释|说明)(?:一下|下)?\s/,
|
|
20
|
-
/^(?:这个|这)\s*(?:是|什么)/,
|
|
21
|
-
];
|
|
22
|
-
/** 产物信号规则表 */
|
|
17
|
+
// ── 规则表(保留用于兼容层) ──
|
|
18
|
+
/** 产物信号规则表 — 保留用于 detectArtifact 回退 */
|
|
23
19
|
const ARTIFACT_SIGNALS = [
|
|
24
|
-
{ pattern: /原型(?:说明|规约|描述|文档|报告|spec)
|
|
25
|
-
{ pattern: /接口(?:设计|清单|文档|spec)/, artifactKind: "api_spec", defaultName: "接口设计" },
|
|
20
|
+
{ pattern: /原型(?:说明|规约|描述|文档|报告|spec)|页面(?:说明|流程)|交互说明|prototype\s*spec/i, artifactKind: "prototype_spec", defaultName: "原型说明", template: "原型说明模版.md" },
|
|
21
|
+
{ pattern: /接口(?:设计|清单|文档|spec|梳理)/, artifactKind: "api_spec", defaultName: "接口设计" },
|
|
26
22
|
{ pattern: /(?:架构设计|详细设计)/, artifactKind: "design_doc", defaultName: "设计文档" },
|
|
27
23
|
{ pattern: /测试计划/, artifactKind: "test_plan", defaultName: "测试计划" },
|
|
28
24
|
{ pattern: /迁移(?:方案|评估|计划)/, artifactKind: "migration_plan", defaultName: "迁移方案" },
|
|
@@ -38,12 +34,11 @@ const ARTIFACT_SIGNALS = [
|
|
|
38
34
|
/** 产物动词: 只有明确表达"产出/生成"意图的才算 */
|
|
39
35
|
const ARTIFACT_VERBS = [
|
|
40
36
|
/生成/, /输出/, /抓取/, /提取/, /整理成/, /写成/,
|
|
41
|
-
/梳理.*(?:成|为|出)/,
|
|
42
37
|
/拆(?:成|分为)/,
|
|
43
38
|
/导出/, /输出为/, /保存为/, /产出到/, /做一份/, /写一份/, /生成一份/,
|
|
44
|
-
|
|
39
|
+
/做成?/, /形成/, /制作/, /从.*(?:生成|输出|提取|抓取)/,
|
|
45
40
|
];
|
|
46
|
-
/**
|
|
41
|
+
/** 源码提取信号 */
|
|
47
42
|
const SOURCE_EXTRACTION_PATTERNS = [
|
|
48
43
|
{ pattern: /\.(zip|tar|gz|rar|7z|jar|war)/i, inputKind: "archive" },
|
|
49
44
|
{ pattern: /源码包|压缩包|zip包|包文件/, inputKind: "archive" },
|
|
@@ -51,10 +46,12 @@ const SOURCE_EXTRACTION_PATTERNS = [
|
|
|
51
46
|
{ pattern: /(?:logs?\/|\.log|日志(?:文件|目录|路径))/i, inputKind: "log" },
|
|
52
47
|
{ pattern: /从报错日志/, inputKind: "log" },
|
|
53
48
|
];
|
|
54
|
-
/**
|
|
49
|
+
/** 提取类动词 */
|
|
55
50
|
const EXTRACTION_VERB_PATTERNS = [
|
|
56
51
|
/从\s*.*(?:提取|抽取)/,
|
|
57
|
-
|
|
52
|
+
/从\s*.*(?:生成|输出)/,
|
|
53
|
+
/(?:拿|用)\s*.*(?:做|生成|输出|提取|梳理|整理)/,
|
|
54
|
+
/读取\s*.*生成/,
|
|
58
55
|
/抓取.*(?:源码|源代码|原型)/,
|
|
59
56
|
];
|
|
60
57
|
/** 代码修改模式 */
|
|
@@ -103,122 +100,38 @@ const OPERATION_PATTERNS = [
|
|
|
103
100
|
/准备.*(?:发布前|检查清单)/,
|
|
104
101
|
/发布前(?:检查|准备)/,
|
|
105
102
|
];
|
|
103
|
+
/** 跳过类模式 */
|
|
104
|
+
const SKIP_PATTERNS = [
|
|
105
|
+
{ pattern: /^(你好|hi|hello|hey|嗨|早上好|下午好|晚上好)[\s!!.。]*$/i, reason: "greeting" },
|
|
106
|
+
{ pattern: /^(今天|现在|几号|什么时候|什么时候了)/, reason: "casual_question" },
|
|
107
|
+
{ pattern: /不用\s*(soloForge|solo\s*forge)/i, reason: "explicit_skip" },
|
|
108
|
+
{ pattern: /直接(告诉|说|回答|给)/, reason: "explicit_skip" },
|
|
109
|
+
];
|
|
110
|
+
/** 通用知识问答模式 */
|
|
111
|
+
const GENERAL_QA_PATTERNS = [
|
|
112
|
+
/(?:什么(?:是|叫|意思))\s*(?:REST|API|DTO|DAO|SOA|DDD|TDD|BDD|CI\/CD|SQL|NoSQL|TypeScript|JavaScript|Java|Python|Go|Docker|K8s|Kubernetes|React|Vue|Spring|Node|Nginx|Redis|MySQL|MongoDB|GraphQL|gRPC|WebSocket|OAuth|JWT|SOLID|DRY|KISS|YAGNI)/i,
|
|
113
|
+
/^(?:TypeScript|Java|Python|Go|React|Vue|Spring)\s*(?:union type|泛型|接口|类|装饰器|闭包|协程|通道|组件|生命周期|状态管理|中间件)/i,
|
|
114
|
+
];
|
|
115
|
+
/** 纯解释类模式 */
|
|
116
|
+
const PURE_EXPLAIN_PATTERNS = [
|
|
117
|
+
/(?:解释|说明)(?:一下|下)?\s*(?:这个|这|这模块|这代码|这段|这个模块)\s*(?:怎么|如何|是什么|是什么意思|的|了)/,
|
|
118
|
+
/^(?:解释|说明)(?:一下|下)?\s/,
|
|
119
|
+
/^(?:这个|这)\s*(?:是|什么)/,
|
|
120
|
+
];
|
|
121
|
+
// ── 路径提取兼容函数(调用新模块) ──
|
|
106
122
|
/**
|
|
107
123
|
* 从意图文本中提取输入路径。
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
* @param intent - 用户意图文本
|
|
111
|
-
* @returns 提取到的路径列表,包含类型和摄取状态
|
|
124
|
+
* 保留原函数签名,内部委托给 input_material_extractor。
|
|
112
125
|
*/
|
|
113
126
|
function extractInputPaths(intent) {
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
// 规范化 ~/ 路径用于策略检查,但保留原始路径
|
|
121
|
-
const normalizedForPolicy = p.replace(/^~\//, (process.env.HOME || "/") + "/");
|
|
122
|
-
const status = classifyIngestionStatus(normalizedForPolicy);
|
|
123
|
-
paths.push({ kind, path: p, ingestion_status: status });
|
|
124
|
-
}
|
|
125
|
-
// 收集所有绝对路径的 span(start, end),用于后续排除重叠的相对匹配
|
|
126
|
-
const absoluteSpans = [];
|
|
127
|
-
function recordAbsoluteSpan(match) {
|
|
128
|
-
absoluteSpans.push({ start: match.index, end: match.index + match[0].length });
|
|
129
|
-
}
|
|
130
|
-
function overlapsAbsolute(start, end) {
|
|
131
|
-
return absoluteSpans.some((s) => start >= s.start && start < s.end);
|
|
132
|
-
}
|
|
133
|
-
// 0. ~/ 路径(home 目录相对路径)
|
|
134
|
-
const tildeArchive = intent.match(/(~\/[\p{L}\p{N}_.-]+)+\.(?:zip|tar\.gz|gz|rar|7z|jar|war)/iu);
|
|
135
|
-
if (tildeArchive) {
|
|
136
|
-
addPath(tildeArchive[0], "archive");
|
|
137
|
-
recordAbsoluteSpan(tildeArchive);
|
|
138
|
-
}
|
|
139
|
-
const tildeFileMatches = intent.matchAll(/(?<![\p{L}\p{N}_\/])(~\/[\p{L}\p{N}_.-]+)+\.(?:log|md|txt|doc|docx|pdf|java|py|ts|tsx|js|jsx|go|rs|json|yaml|yml|toml|pem|key|rsa)/giu);
|
|
140
|
-
for (const m of tildeFileMatches) {
|
|
141
|
-
const p = m[0];
|
|
142
|
-
if (/\.log$/i.test(p))
|
|
143
|
-
addPath(p, "log");
|
|
144
|
-
else if (/\.(md|txt|doc|pdf|docx)$/i.test(p))
|
|
145
|
-
addPath(p, "doc");
|
|
146
|
-
else
|
|
147
|
-
addPath(p, "file");
|
|
148
|
-
recordAbsoluteSpan(m);
|
|
149
|
-
}
|
|
150
|
-
const tildeDirMatches = intent.matchAll(/(?<![\p{L}\p{N}_\/])(~\/[\p{L}\p{N}_.-]+)+(?=\/?(?:\s|$|,|,|\)|\]|的|下|中|里))/gu);
|
|
151
|
-
for (const m of tildeDirMatches) {
|
|
152
|
-
addPath(m[0], "directory");
|
|
153
|
-
recordAbsoluteSpan(m);
|
|
154
|
-
}
|
|
155
|
-
// 通用 ~/ 路径匹配(未被上述特定模式捕获的路径,如 ~/.ssh/id_rsa)
|
|
156
|
-
const tildeGenericMatches = intent.matchAll(/(~\/[\p{L}\p{N}_.\-\/]+)/gu);
|
|
157
|
-
for (const m of tildeGenericMatches) {
|
|
158
|
-
const p = m[0];
|
|
159
|
-
if (!seen.has(p))
|
|
160
|
-
addPath(p, "file");
|
|
161
|
-
recordAbsoluteSpan(m);
|
|
162
|
-
}
|
|
163
|
-
// 1. 绝对 archive: /xxx/yyy.zip — 跳过与 ~/ 路径重叠的
|
|
164
|
-
const absoluteArchive = intent.match(/(?<![\p{L}\p{N}_\/])(\/[\p{L}\p{N}_.-]+)+\.(?:zip|tar\.gz|gz|rar|7z|jar|war)/iu);
|
|
165
|
-
if (absoluteArchive && !overlapsAbsolute(absoluteArchive.index, absoluteArchive.index + absoluteArchive[0].length)) {
|
|
166
|
-
addPath(absoluteArchive[0], "archive");
|
|
167
|
-
recordAbsoluteSpan(absoluteArchive);
|
|
168
|
-
}
|
|
169
|
-
// 2. 绝对文件路径: /tmp/error.log, /Users/Charlie/docs/api.md 等
|
|
170
|
-
const absoluteFileMatches = intent.matchAll(/(?<![\p{L}\p{N}_\/])(\/[\p{L}\p{N}_.-]+)+\.(?:log|md|txt|doc|docx|pdf|java|py|ts|tsx|js|jsx|go|rs|json|yaml|yml|toml)/giu);
|
|
171
|
-
for (const m of absoluteFileMatches) {
|
|
172
|
-
if (overlapsAbsolute(m.index, m.index + m[0].length))
|
|
173
|
-
continue;
|
|
174
|
-
const p = m[0];
|
|
175
|
-
if (/\.log$/i.test(p))
|
|
176
|
-
addPath(p, "log");
|
|
177
|
-
else if (/\.(md|txt|doc|pdf|docx)$/i.test(p))
|
|
178
|
-
addPath(p, "doc");
|
|
179
|
-
else
|
|
180
|
-
addPath(p, "file");
|
|
181
|
-
recordAbsoluteSpan(m);
|
|
182
|
-
}
|
|
183
|
-
// 3. 绝对目录路径: /Users/Charlie/project/src 等
|
|
184
|
-
const absoluteDirMatches = intent.matchAll(/(?<![\p{L}\p{N}_\/])(\/[\p{L}\p{N}_.-]+)+(?=\/?(?:\s|$|,|,|\)|\]|的|下|中|里))/gu);
|
|
185
|
-
for (const m of absoluteDirMatches) {
|
|
186
|
-
const p = m[0];
|
|
187
|
-
if (overlapsAbsolute(m.index, m.index + m[0].length) || seen.has(p))
|
|
188
|
-
continue;
|
|
189
|
-
addPath(p, "directory");
|
|
190
|
-
recordAbsoluteSpan(m);
|
|
191
|
-
}
|
|
192
|
-
// 4. 相对文件路径: xxx/yyy.ext — 跳过与绝对路径重叠的
|
|
193
|
-
const fileMatches = intent.matchAll(/([\p{L}\p{N}_][\p{L}\p{N}_.-]*(?:\/[\p{L}\p{N}_.-]+)+\.(?:tar\.gz|zip|gz|rar|7z|jar|war|log|md|txt|doc|docx|pdf|java|py|ts|tsx|js|jsx|go|rs|json|yaml|yml|toml|pem|key|rsa))/giu);
|
|
194
|
-
for (const m of fileMatches) {
|
|
195
|
-
if (overlapsAbsolute(m.index, m.index + m[0].length))
|
|
196
|
-
continue;
|
|
197
|
-
const p = m[1].replace(/^从(?=[\p{L}\p{N}_][\p{L}\p{N}_.-]*\/)/u, "");
|
|
198
|
-
if (/\.log$/i.test(p))
|
|
199
|
-
addPath(p, "log");
|
|
200
|
-
else if (/\.(md|txt|doc|pdf|docx)$/i.test(p))
|
|
201
|
-
addPath(p, "doc");
|
|
202
|
-
else if (/\.(zip|tar\.gz|gz|rar|7z|jar|war)$/i.test(p))
|
|
203
|
-
addPath(p, "archive");
|
|
204
|
-
else
|
|
205
|
-
addPath(p, "file");
|
|
206
|
-
}
|
|
207
|
-
// 5. 相对目录路径: xxx/src, old-system/src, docs/api — 跳过与绝对路径重叠的
|
|
208
|
-
const dirMatches = intent.matchAll(/([\p{L}\p{N}_.-]+(?:\/[\p{L}\p{N}_.-]+)*\/(?:src|old-[\p{L}\p{N}_]+|docs?|logs?))/gu);
|
|
209
|
-
for (const m of dirMatches) {
|
|
210
|
-
if (overlapsAbsolute(m.index, m.index + m[0].length))
|
|
211
|
-
continue;
|
|
212
|
-
addPath(m[1], "directory");
|
|
213
|
-
}
|
|
214
|
-
return paths;
|
|
127
|
+
const result = extractInputMaterials(intent);
|
|
128
|
+
return result.materials.map((m) => ({
|
|
129
|
+
kind: m.kind,
|
|
130
|
+
path: m.path,
|
|
131
|
+
ingestion_status: m.ingestion_status,
|
|
132
|
+
}));
|
|
215
133
|
}
|
|
216
|
-
|
|
217
|
-
* 从意图文本中检测产物信号。
|
|
218
|
-
* @param intent - 用户意图文本
|
|
219
|
-
* @param taskId - 可选的任务 ID,用于生成产物路径
|
|
220
|
-
* @returns 产物信息,未检测到时返回 undefined
|
|
221
|
-
*/
|
|
134
|
+
// ── 产物检测(保留兼容) ──
|
|
222
135
|
function detectArtifact(intent, taskId) {
|
|
223
136
|
const id = taskId || "01";
|
|
224
137
|
for (const signal of ARTIFACT_SIGNALS) {
|
|
@@ -238,21 +151,13 @@ function detectArtifact(intent, taskId) {
|
|
|
238
151
|
}
|
|
239
152
|
return undefined;
|
|
240
153
|
}
|
|
241
|
-
/** 检查意图文本中是否包含产物动词 */
|
|
242
154
|
function hasArtifactVerb(intent) {
|
|
243
155
|
return ARTIFACT_VERBS.some((p) => p.test(intent));
|
|
244
156
|
}
|
|
245
|
-
/** 检查意图文本中是否包含产物名词 */
|
|
246
157
|
function hasArtifactNoun(intent) {
|
|
247
158
|
return ARTIFACT_SIGNALS.some((s) => s.pattern.test(intent))
|
|
248
159
|
|| /(?:文档|方案|规约|计划|清单|报告|说明文档)$/.test(intent);
|
|
249
160
|
}
|
|
250
|
-
/**
|
|
251
|
-
* 推断缺失材料的语义类型和澄清提示。
|
|
252
|
-
* @param intent - 用户意图文本
|
|
253
|
-
* @param hasPath - 意图文本中是否已包含路径
|
|
254
|
-
* @returns 材料列表和缺失提示信息
|
|
255
|
-
*/
|
|
256
161
|
function inferMissingInputs(intent, hasPath) {
|
|
257
162
|
if (hasPath)
|
|
258
163
|
return { materials: [], missing: [] };
|
|
@@ -274,67 +179,144 @@ function inferMissingInputs(intent, hasPath) {
|
|
|
274
179
|
}
|
|
275
180
|
/**
|
|
276
181
|
* 主路由函数 — 根据用户意图文本确定语义路由和执行形态。
|
|
277
|
-
*
|
|
182
|
+
* v2: 调用结构化信号提取 → 打分 → 冲突仲裁,不再依赖单条正则句子匹配。
|
|
278
183
|
*
|
|
279
184
|
* @param input - 路由输入参数,包含意图文本和可选的项目路径/任务 ID
|
|
280
185
|
* @returns 意图路由决策,包含路由类型、执行形态和置信度
|
|
281
186
|
*/
|
|
187
|
+
function logRouteHit(decision) {
|
|
188
|
+
console.error(JSON.stringify({
|
|
189
|
+
sf_route_hit: true,
|
|
190
|
+
route: decision.route,
|
|
191
|
+
execution_shape: decision.execution_shape,
|
|
192
|
+
input_materials: decision.input_materials.map((m) => m.kind).join(",") || "none",
|
|
193
|
+
output_artifact: decision.output_artifact?.kind ?? "-",
|
|
194
|
+
prompt_template: selectPromptTemplate(decision),
|
|
195
|
+
confidence: decision.confidence,
|
|
196
|
+
}));
|
|
197
|
+
}
|
|
282
198
|
export function routeIntent(input) {
|
|
199
|
+
const decision = routeIntentCore(input);
|
|
200
|
+
logRouteHit(decision);
|
|
201
|
+
return decision;
|
|
202
|
+
}
|
|
203
|
+
function routeIntentCore(input) {
|
|
283
204
|
const { intent, task_id: taskId } = input;
|
|
284
205
|
const normalized = intent.toLowerCase();
|
|
285
|
-
console.error("[soloForge] 意图路由: 开始意图路由");
|
|
206
|
+
console.error("[soloForge] 意图路由: 开始意图路由 (v2 signal-based)");
|
|
207
|
+
// ── 阶段 1: 信号提取 ──
|
|
208
|
+
const signals = extractSignals(intent);
|
|
209
|
+
const materialResult = extractInputMaterials(intent);
|
|
210
|
+
const scoredResult = scoreRoute(intent, signals, materialResult, taskId);
|
|
211
|
+
// ── 阶段 2: 兼容层 — 将新模块结果映射回原有分层逻辑 ──
|
|
212
|
+
// 这确保所有现有测试用例的行为不变
|
|
213
|
+
const inputPaths = materialResult.materials.map((m) => ({
|
|
214
|
+
kind: m.kind,
|
|
215
|
+
path: m.path,
|
|
216
|
+
ingestion_status: m.ingestion_status,
|
|
217
|
+
}));
|
|
218
|
+
const hasInputPaths = inputPaths.length > 0;
|
|
286
219
|
// 层 0: 跳过类
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
220
|
+
if (signals.is_skip) {
|
|
221
|
+
console.error(`[soloForge] 意图路由: 匹配跳过模式 (${signals.skip_reason}),路由到 skip_chat`);
|
|
222
|
+
const skipRejected = scoredResult.score_breakdown
|
|
223
|
+
.filter(s => s.route !== "skip_chat")
|
|
224
|
+
.slice(0, 3)
|
|
225
|
+
.map(s => ({ route: s.route, reason: s.reasons.join("; "), score: s.score }));
|
|
226
|
+
return {
|
|
227
|
+
...mk("skip_chat", "none", false, [], [], undefined, undefined, 0.95),
|
|
228
|
+
skip_reason: signals.skip_reason,
|
|
229
|
+
evidence: ["skip:" + signals.skip_reason],
|
|
230
|
+
rejected_routes: skipRejected,
|
|
231
|
+
decision_version: 2,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
if (signals.is_general_qa) {
|
|
294
235
|
console.error("[soloForge] 意图路由: 匹配通用问答模式,路由到 direct_answer");
|
|
295
|
-
|
|
236
|
+
const qaRejected = scoredResult.score_breakdown
|
|
237
|
+
.filter(s => s.route !== "direct_answer")
|
|
238
|
+
.slice(0, 3)
|
|
239
|
+
.map(s => ({ route: s.route, reason: s.reasons.join("; "), score: s.score }));
|
|
240
|
+
return {
|
|
241
|
+
...mk("direct_answer", "none", false, [], [], undefined, undefined, 0.9),
|
|
242
|
+
evidence: ["general_qa"],
|
|
243
|
+
rejected_routes: qaRejected,
|
|
244
|
+
decision_version: 2,
|
|
245
|
+
};
|
|
296
246
|
}
|
|
297
247
|
const artifactVerbPresent = hasArtifactVerb(intent);
|
|
298
248
|
const hasArtifactSignal = artifactVerbPresent && hasArtifactNoun(intent);
|
|
299
249
|
const artifact = detectArtifact(intent, taskId);
|
|
300
|
-
const inputPaths = extractInputPaths(intent);
|
|
301
|
-
const hasInputPaths = inputPaths.length > 0;
|
|
302
|
-
// 源码提取信号 = archive/log 显式模式 OR 提取类动词 + 输入路径/源码
|
|
303
250
|
const hasArchiveLogSignal = SOURCE_EXTRACTION_PATTERNS.some((s) => s.pattern.test(intent));
|
|
304
251
|
const hasExtractionVerb = EXTRACTION_VERB_PATTERNS.some((p) => p.test(intent));
|
|
305
252
|
const hasSourceExtraction = hasArchiveLogSignal
|
|
306
253
|
|| (hasExtractionVerb && (hasInputPaths || /源码/.test(intent)));
|
|
307
254
|
const isFullPipeline = FULL_PIPELINE_SIGNALS.some((p) => p.test(intent));
|
|
308
|
-
//
|
|
255
|
+
// ── 冲突仲裁:检查否定/约束信号 ──
|
|
256
|
+
const hasNoMutation = signals.negations.some((n) => n.negates === "mutation");
|
|
257
|
+
const hasNoPipeline = signals.negations.some((n) => n.negates === "pipeline");
|
|
258
|
+
// 层 1: 源码/材料提取
|
|
309
259
|
if (hasSourceExtraction && (hasArtifactSignal || artifact)) {
|
|
310
260
|
console.error("[soloForge] 意图路由: 路由到 source_extraction");
|
|
311
|
-
const materials =
|
|
261
|
+
const materials = hasInputPaths
|
|
312
262
|
? inputPaths.map((p) => ({ kind: p.kind, path: p.path, required: true, ingestion_status: p.ingestion_status }))
|
|
313
|
-
: inferMissingInputs(intent, false).materials;
|
|
314
|
-
const missing =
|
|
263
|
+
: inferMissingInputs(intent, false).materials.map((m) => ({ kind: m.kind, required: m.required }));
|
|
264
|
+
const missing = hasInputPaths
|
|
315
265
|
? []
|
|
316
266
|
: inferMissingInputs(intent, false).missing;
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
267
|
+
const inferred = hasInputPaths ? undefined : inferMissingInputs(intent, false).materials;
|
|
268
|
+
const rejectedRoutes = scoredResult.score_breakdown
|
|
269
|
+
.filter(s => s.route !== "source_extraction")
|
|
270
|
+
.slice(0, 3)
|
|
271
|
+
.map(s => ({ route: s.route, reason: s.reasons.join("; "), score: s.score }));
|
|
272
|
+
return {
|
|
273
|
+
...mk("source_extraction", "single_artifact", false, materials, missing, artifact ?? { kind: "custom", path: `.soloforge/output/${taskId ?? "01"}/01-提取产物.md`, taskId }, inferPipelineHint(intent, artifact?.kind), 0.8),
|
|
274
|
+
inferred_materials: inferred,
|
|
275
|
+
needs_clarification: missing.length > 0,
|
|
276
|
+
route_reason: scoredResult.route_reason,
|
|
277
|
+
signal_evidence: scoredResult.signal_evidence,
|
|
278
|
+
evidence: scoredResult.signal_evidence,
|
|
279
|
+
rejected_routes: rejectedRoutes,
|
|
280
|
+
decision_version: 2,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
// 层 2: 规划
|
|
320
284
|
if (PLANNING_PATTERNS.some((p) => p.test(normalized)) || isFullPipeline) {
|
|
321
285
|
console.error("[soloForge] 意图路由: 路由到 planning");
|
|
322
|
-
if (isFullPipeline) {
|
|
323
|
-
return
|
|
286
|
+
if (isFullPipeline && !hasNoPipeline) {
|
|
287
|
+
return {
|
|
288
|
+
...mk("planning", "multi_stage_plan", false, inputPaths.map((p) => ({ kind: p.kind, path: p.path, required: false, ingestion_status: p.ingestion_status })), [], artifact, inferPipelineHint(intent, artifact?.kind), 0.75),
|
|
289
|
+
route_reason: scoredResult.route_reason,
|
|
290
|
+
signal_evidence: scoredResult.signal_evidence,
|
|
291
|
+
decision_version: 2,
|
|
292
|
+
};
|
|
324
293
|
}
|
|
325
|
-
return
|
|
326
|
-
|
|
327
|
-
|
|
294
|
+
return {
|
|
295
|
+
...mk("planning", "single_artifact", false, inputPaths.map((p) => ({ kind: p.kind, path: p.path, required: false, ingestion_status: p.ingestion_status })), [], { kind: "task_breakdown", path: `.soloforge/output/${taskId ?? "01"}/01-任务拆解.md`, taskId }, undefined, 0.8),
|
|
296
|
+
route_reason: scoredResult.route_reason,
|
|
297
|
+
signal_evidence: scoredResult.signal_evidence,
|
|
298
|
+
decision_version: 2,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
// 层 3: 操作
|
|
328
302
|
if (OPERATION_PATTERNS.some((p) => p.test(normalized))) {
|
|
329
303
|
console.error("[soloForge] 意图路由: 路由到 operation");
|
|
330
|
-
return
|
|
304
|
+
return {
|
|
305
|
+
...mk("operation", "verification_only", false, [], [], undefined, undefined, 0.8),
|
|
306
|
+
route_reason: scoredResult.route_reason,
|
|
307
|
+
decision_version: 2,
|
|
308
|
+
};
|
|
331
309
|
}
|
|
332
|
-
// 层 3.5: 分析
|
|
310
|
+
// 层 3.5: 分析
|
|
333
311
|
if (ANALYSIS_PATTERNS.some((p) => p.test(normalized)) && !artifactVerbPresent) {
|
|
334
312
|
console.error("[soloForge] 意图路由: 路由到 analysis(分析优先)");
|
|
335
|
-
return
|
|
313
|
+
return {
|
|
314
|
+
...mk("analysis", "read_only_analysis", false, inputPaths.map((p) => ({ kind: p.kind, path: p.path, required: false, ingestion_status: p.ingestion_status })), [], undefined, undefined, 0.8),
|
|
315
|
+
route_reason: scoredResult.route_reason,
|
|
316
|
+
decision_version: 2,
|
|
317
|
+
};
|
|
336
318
|
}
|
|
337
|
-
// 层 4: 产物生成
|
|
319
|
+
// 层 4: 产物生成
|
|
338
320
|
if (hasArtifactSignal || (artifactVerbPresent && artifact && !PURE_EXPLAIN_PATTERNS.some((p) => p.test(intent)))) {
|
|
339
321
|
console.error("[soloForge] 意图路由: 路由到 artifact_generation");
|
|
340
322
|
const missing = [];
|
|
@@ -343,71 +325,99 @@ export function routeIntent(input) {
|
|
|
343
325
|
if (refMatch)
|
|
344
326
|
missing.push(`请提供 ${refMatch[1]} 文件路径或内容`);
|
|
345
327
|
}
|
|
346
|
-
return
|
|
328
|
+
return {
|
|
329
|
+
...mk("artifact_generation", "single_artifact", false, inputPaths.map((p) => ({ kind: p.kind, path: p.path, required: false, ingestion_status: p.ingestion_status })), missing, artifact ?? { kind: "custom", path: `.soloforge/output/${taskId ?? "01"}/01-产物.md`, taskId }, inferPipelineHint(intent, artifact?.kind), 0.8),
|
|
330
|
+
needs_clarification: missing.length > 0,
|
|
331
|
+
route_reason: scoredResult.route_reason,
|
|
332
|
+
signal_evidence: scoredResult.signal_evidence,
|
|
333
|
+
decision_version: 2,
|
|
334
|
+
};
|
|
347
335
|
}
|
|
348
336
|
// 层 5: 代码修改
|
|
349
337
|
if (CODE_CHANGE_PATTERNS.some((p) => p.test(normalized))) {
|
|
350
338
|
console.error("[soloForge] 意图路由: 路由到 code_change");
|
|
351
|
-
return
|
|
339
|
+
return {
|
|
340
|
+
...mk("code_change", "code_execution", true, inputPaths.map((p) => ({ kind: p.kind, path: p.path, required: false, ingestion_status: p.ingestion_status })), [], undefined, undefined, 0.85),
|
|
341
|
+
route_reason: scoredResult.route_reason,
|
|
342
|
+
decision_version: 2,
|
|
343
|
+
};
|
|
352
344
|
}
|
|
353
345
|
// 层 6: 审查
|
|
354
346
|
if (REVIEW_PATTERNS.some((p) => p.test(normalized))) {
|
|
355
347
|
console.error("[soloForge] 意图路由: 路由到 review");
|
|
356
|
-
return
|
|
348
|
+
return {
|
|
349
|
+
...mk("review", "read_only_analysis", false, inputPaths.map((p) => ({ kind: p.kind, path: p.path, required: false, ingestion_status: p.ingestion_status })), [], undefined, undefined, 0.85),
|
|
350
|
+
route_reason: scoredResult.route_reason,
|
|
351
|
+
decision_version: 2,
|
|
352
|
+
};
|
|
357
353
|
}
|
|
358
|
-
// 层 7:
|
|
354
|
+
// 层 7: 通用分析
|
|
359
355
|
if (ANALYSIS_PATTERNS.some((p) => p.test(normalized))) {
|
|
360
356
|
console.error("[soloForge] 意图路由: 路由到 analysis(通用)");
|
|
361
|
-
return
|
|
357
|
+
return {
|
|
358
|
+
...mk("analysis", "read_only_analysis", false, inputPaths.map((p) => ({ kind: p.kind, path: p.path, required: false, ingestion_status: p.ingestion_status })), [], undefined, undefined, 0.8),
|
|
359
|
+
route_reason: scoredResult.route_reason,
|
|
360
|
+
decision_version: 2,
|
|
361
|
+
};
|
|
362
362
|
}
|
|
363
363
|
// 层 8: 灰区
|
|
364
364
|
if (/整理(?:一下|下)?/.test(intent)) {
|
|
365
365
|
if (!hasArtifactNoun(intent)) {
|
|
366
|
-
return
|
|
366
|
+
return {
|
|
367
|
+
...mk("direct_answer", "none", false, inputPaths.map((p) => ({ kind: p.kind, path: p.path, required: false, ingestion_status: p.ingestion_status })), [], undefined, undefined, 0.6),
|
|
368
|
+
route_reason: "gray_zone:organize",
|
|
369
|
+
decision_version: 2,
|
|
370
|
+
};
|
|
367
371
|
}
|
|
368
372
|
}
|
|
369
373
|
// 层 9: 纯解释
|
|
370
374
|
if (PURE_EXPLAIN_PATTERNS.some((p) => p.test(intent))) {
|
|
371
375
|
if (/(?:这个|这|项目里|项目中的|当前|现有)/.test(intent)) {
|
|
372
|
-
return
|
|
376
|
+
return {
|
|
377
|
+
...mk("analysis", "read_only_analysis", false, [], [], undefined, undefined, 0.7),
|
|
378
|
+
route_reason: "pure_explain+context",
|
|
379
|
+
decision_version: 2,
|
|
380
|
+
};
|
|
373
381
|
}
|
|
374
|
-
return
|
|
382
|
+
return {
|
|
383
|
+
...mk("direct_answer", "none", false, [], [], undefined, undefined, 0.85),
|
|
384
|
+
route_reason: "pure_explain",
|
|
385
|
+
decision_version: 2,
|
|
386
|
+
};
|
|
375
387
|
}
|
|
376
388
|
// 层 10: "看看" 灰区
|
|
377
389
|
if (/看看|看下/.test(intent)) {
|
|
378
390
|
if (/(?:风险|问题|有没有)/.test(intent)) {
|
|
379
|
-
return
|
|
391
|
+
return {
|
|
392
|
+
...mk("review", "read_only_analysis", false, inputPaths.map((p) => ({ kind: p.kind, path: p.path, required: false, ingestion_status: p.ingestion_status })), [], undefined, undefined, 0.7),
|
|
393
|
+
route_reason: "gray:review",
|
|
394
|
+
decision_version: 2,
|
|
395
|
+
};
|
|
380
396
|
}
|
|
381
|
-
return
|
|
397
|
+
return {
|
|
398
|
+
...mk("direct_answer", "none", false, inputPaths.map((p) => ({ kind: p.kind, path: p.path, required: false, ingestion_status: p.ingestion_status })), [], undefined, undefined, 0.6),
|
|
399
|
+
route_reason: "gray:look",
|
|
400
|
+
decision_version: 2,
|
|
401
|
+
};
|
|
382
402
|
}
|
|
383
403
|
// 兜底
|
|
384
404
|
console.error("[soloForge] 意图路由: 未匹配任何规则,兜底路由到 direct_answer");
|
|
385
|
-
return
|
|
405
|
+
return {
|
|
406
|
+
...mk("direct_answer", "none", false, [], [], undefined, undefined, 0.3),
|
|
407
|
+
decision_version: 2,
|
|
408
|
+
};
|
|
386
409
|
}
|
|
387
|
-
|
|
388
|
-
* 构造路由决策对象。
|
|
389
|
-
* @param route - 路由类型
|
|
390
|
-
* @param execution_shape - 执行形态
|
|
391
|
-
* @param mutation_allowed - 是否允许变更
|
|
392
|
-
* @param input_materials - 输入材料列表
|
|
393
|
-
* @param missing - 缺失输入列表
|
|
394
|
-
* @param output_artifact - 输出产物
|
|
395
|
-
* @param pipeline_hint - 流水线提示
|
|
396
|
-
* @param confidence - 置信度
|
|
397
|
-
* @returns 意图路由决策
|
|
398
|
-
*/
|
|
410
|
+
// ── 构造函数 ──
|
|
399
411
|
function mk(route, execution_shape, mutation_allowed, input_materials, missing, output_artifact, pipeline_hint, confidence) {
|
|
400
412
|
return {
|
|
401
413
|
route, execution_shape, mutation_allowed, input_materials,
|
|
402
414
|
output_artifact, pipeline_hint, missing_required_inputs: missing, confidence,
|
|
415
|
+
scope: { read_only: !mutation_allowed, description: route + " scope" },
|
|
416
|
+
constraints: mutation_allowed ? [] : ["no_mutation"],
|
|
417
|
+
language_policy: { primary: "zh-CN", fallback: "en" },
|
|
403
418
|
};
|
|
404
419
|
}
|
|
405
|
-
|
|
406
|
-
* 推断流水线提示 — 根据意图文本和产物类型推断适用的流水线阶段。
|
|
407
|
-
* @param intent - 用户意图文本
|
|
408
|
-
* @param artifactKind - 可选的产物类型
|
|
409
|
-
* @returns 流水线提示字符串,无法推断时返回 undefined
|
|
410
|
-
*/
|
|
420
|
+
// ── 流水线提示 ──
|
|
411
421
|
function inferPipelineHint(intent, artifactKind) {
|
|
412
422
|
if (/源码.*原型|原型.*源码|源码包.*原型|prototype/i.test(intent)) {
|
|
413
423
|
return "source-prototype-to-delivery:step-1";
|
|
@@ -423,22 +433,11 @@ function inferPipelineHint(intent, artifactKind) {
|
|
|
423
433
|
default: return undefined;
|
|
424
434
|
}
|
|
425
435
|
}
|
|
426
|
-
|
|
427
|
-
* 判断是否应进入 SoloForge 流程。
|
|
428
|
-
* skip_chat 和 direct_answer 路由不进入 SoloForge。
|
|
429
|
-
*
|
|
430
|
-
* @param decision - 意图路由决策
|
|
431
|
-
* @returns 是否应进入 SoloForge
|
|
432
|
-
*/
|
|
436
|
+
// ── 外部 API(保持不变) ──
|
|
433
437
|
export function shouldEnterSoloForge(decision) {
|
|
434
438
|
console.error("[soloForge] 意图路由: 判断是否进入 SoloForge,当前路由: " + decision.route);
|
|
435
439
|
return decision.route !== "skip_chat" && decision.route !== "direct_answer";
|
|
436
440
|
}
|
|
437
|
-
/**
|
|
438
|
-
* 根据路由决策选择对应的 Prompt 模板。
|
|
439
|
-
* @param decision - 意图路由决策
|
|
440
|
-
* @returns Prompt 模板名称
|
|
441
|
-
*/
|
|
442
441
|
export function selectPromptTemplate(decision) {
|
|
443
442
|
console.error("[soloForge] 意图路由: 选择 Prompt 模板,路由: " + decision.route + ", 执行形态: " + decision.execution_shape);
|
|
444
443
|
if (decision.route === "source_extraction")
|
|
@@ -455,4 +454,24 @@ export function selectPromptTemplate(decision) {
|
|
|
455
454
|
default: return "code_execution_prompt";
|
|
456
455
|
}
|
|
457
456
|
}
|
|
457
|
+
export function debugRouteIntent(input) {
|
|
458
|
+
const { intent, task_id: taskId } = input;
|
|
459
|
+
const materialResult = extractInputMaterials(intent);
|
|
460
|
+
const signals = extractSignals(intent);
|
|
461
|
+
const scored = scoreRoute(intent, signals, materialResult, taskId);
|
|
462
|
+
const decision = routeIntent(input);
|
|
463
|
+
const whyNotSkip = signals.is_skip
|
|
464
|
+
? `skip detected: ${signals.skip_reason}`
|
|
465
|
+
: signals.is_general_qa
|
|
466
|
+
? "general QA pattern"
|
|
467
|
+
: "has SoloForge-relevant signals";
|
|
468
|
+
return {
|
|
469
|
+
intent,
|
|
470
|
+
extracted_materials: materialResult,
|
|
471
|
+
signals,
|
|
472
|
+
scored,
|
|
473
|
+
final_decision: decision,
|
|
474
|
+
why_not_skip: whyNotSkip,
|
|
475
|
+
};
|
|
476
|
+
}
|
|
458
477
|
//# sourceMappingURL=intent_router.js.map
|