soloforge 1.2.10 → 1.2.11
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/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 +27 -21
- package/dist/engine/intent_router.d.ts.map +1 -1
- package/dist/engine/intent_router.js +213 -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/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,124 @@ 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
|
+
return {
|
|
223
|
+
...mk("skip_chat", "none", false, [], [], undefined, undefined, 0.95),
|
|
224
|
+
skip_reason: signals.skip_reason,
|
|
225
|
+
decision_version: 2,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
if (signals.is_general_qa) {
|
|
294
229
|
console.error("[soloForge] 意图路由: 匹配通用问答模式,路由到 direct_answer");
|
|
295
|
-
return
|
|
230
|
+
return {
|
|
231
|
+
...mk("direct_answer", "none", false, [], [], undefined, undefined, 0.9),
|
|
232
|
+
decision_version: 2,
|
|
233
|
+
};
|
|
296
234
|
}
|
|
297
235
|
const artifactVerbPresent = hasArtifactVerb(intent);
|
|
298
236
|
const hasArtifactSignal = artifactVerbPresent && hasArtifactNoun(intent);
|
|
299
237
|
const artifact = detectArtifact(intent, taskId);
|
|
300
|
-
const inputPaths = extractInputPaths(intent);
|
|
301
|
-
const hasInputPaths = inputPaths.length > 0;
|
|
302
|
-
// 源码提取信号 = archive/log 显式模式 OR 提取类动词 + 输入路径/源码
|
|
303
238
|
const hasArchiveLogSignal = SOURCE_EXTRACTION_PATTERNS.some((s) => s.pattern.test(intent));
|
|
304
239
|
const hasExtractionVerb = EXTRACTION_VERB_PATTERNS.some((p) => p.test(intent));
|
|
305
240
|
const hasSourceExtraction = hasArchiveLogSignal
|
|
306
241
|
|| (hasExtractionVerb && (hasInputPaths || /源码/.test(intent)));
|
|
307
242
|
const isFullPipeline = FULL_PIPELINE_SIGNALS.some((p) => p.test(intent));
|
|
308
|
-
//
|
|
243
|
+
// ── 冲突仲裁:检查否定/约束信号 ──
|
|
244
|
+
const hasNoMutation = signals.negations.some((n) => n.negates === "mutation");
|
|
245
|
+
const hasNoPipeline = signals.negations.some((n) => n.negates === "pipeline");
|
|
246
|
+
// 层 1: 源码/材料提取
|
|
309
247
|
if (hasSourceExtraction && (hasArtifactSignal || artifact)) {
|
|
310
248
|
console.error("[soloForge] 意图路由: 路由到 source_extraction");
|
|
311
|
-
const materials =
|
|
249
|
+
const materials = hasInputPaths
|
|
312
250
|
? inputPaths.map((p) => ({ kind: p.kind, path: p.path, required: true, ingestion_status: p.ingestion_status }))
|
|
313
|
-
: inferMissingInputs(intent, false).materials;
|
|
314
|
-
const missing =
|
|
251
|
+
: inferMissingInputs(intent, false).materials.map((m) => ({ kind: m.kind, required: m.required }));
|
|
252
|
+
const missing = hasInputPaths
|
|
315
253
|
? []
|
|
316
254
|
: inferMissingInputs(intent, false).missing;
|
|
317
|
-
return
|
|
318
|
-
|
|
319
|
-
|
|
255
|
+
return {
|
|
256
|
+
...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),
|
|
257
|
+
needs_clarification: missing.length > 0,
|
|
258
|
+
route_reason: scoredResult.route_reason,
|
|
259
|
+
signal_evidence: scoredResult.signal_evidence,
|
|
260
|
+
decision_version: 2,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
// 层 2: 规划
|
|
320
264
|
if (PLANNING_PATTERNS.some((p) => p.test(normalized)) || isFullPipeline) {
|
|
321
265
|
console.error("[soloForge] 意图路由: 路由到 planning");
|
|
322
|
-
if (isFullPipeline) {
|
|
323
|
-
return
|
|
266
|
+
if (isFullPipeline && !hasNoPipeline) {
|
|
267
|
+
return {
|
|
268
|
+
...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),
|
|
269
|
+
route_reason: scoredResult.route_reason,
|
|
270
|
+
signal_evidence: scoredResult.signal_evidence,
|
|
271
|
+
decision_version: 2,
|
|
272
|
+
};
|
|
324
273
|
}
|
|
325
|
-
return
|
|
326
|
-
|
|
327
|
-
|
|
274
|
+
return {
|
|
275
|
+
...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),
|
|
276
|
+
route_reason: scoredResult.route_reason,
|
|
277
|
+
signal_evidence: scoredResult.signal_evidence,
|
|
278
|
+
decision_version: 2,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
// 层 3: 操作
|
|
328
282
|
if (OPERATION_PATTERNS.some((p) => p.test(normalized))) {
|
|
329
283
|
console.error("[soloForge] 意图路由: 路由到 operation");
|
|
330
|
-
return
|
|
284
|
+
return {
|
|
285
|
+
...mk("operation", "verification_only", false, [], [], undefined, undefined, 0.8),
|
|
286
|
+
route_reason: scoredResult.route_reason,
|
|
287
|
+
decision_version: 2,
|
|
288
|
+
};
|
|
331
289
|
}
|
|
332
|
-
// 层 3.5: 分析
|
|
290
|
+
// 层 3.5: 分析
|
|
333
291
|
if (ANALYSIS_PATTERNS.some((p) => p.test(normalized)) && !artifactVerbPresent) {
|
|
334
292
|
console.error("[soloForge] 意图路由: 路由到 analysis(分析优先)");
|
|
335
|
-
return
|
|
293
|
+
return {
|
|
294
|
+
...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),
|
|
295
|
+
route_reason: scoredResult.route_reason,
|
|
296
|
+
decision_version: 2,
|
|
297
|
+
};
|
|
336
298
|
}
|
|
337
|
-
// 层 4: 产物生成
|
|
299
|
+
// 层 4: 产物生成
|
|
338
300
|
if (hasArtifactSignal || (artifactVerbPresent && artifact && !PURE_EXPLAIN_PATTERNS.some((p) => p.test(intent)))) {
|
|
339
301
|
console.error("[soloForge] 意图路由: 路由到 artifact_generation");
|
|
340
302
|
const missing = [];
|
|
@@ -343,71 +305,96 @@ export function routeIntent(input) {
|
|
|
343
305
|
if (refMatch)
|
|
344
306
|
missing.push(`请提供 ${refMatch[1]} 文件路径或内容`);
|
|
345
307
|
}
|
|
346
|
-
return
|
|
308
|
+
return {
|
|
309
|
+
...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),
|
|
310
|
+
needs_clarification: missing.length > 0,
|
|
311
|
+
route_reason: scoredResult.route_reason,
|
|
312
|
+
signal_evidence: scoredResult.signal_evidence,
|
|
313
|
+
decision_version: 2,
|
|
314
|
+
};
|
|
347
315
|
}
|
|
348
316
|
// 层 5: 代码修改
|
|
349
317
|
if (CODE_CHANGE_PATTERNS.some((p) => p.test(normalized))) {
|
|
350
318
|
console.error("[soloForge] 意图路由: 路由到 code_change");
|
|
351
|
-
return
|
|
319
|
+
return {
|
|
320
|
+
...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),
|
|
321
|
+
route_reason: scoredResult.route_reason,
|
|
322
|
+
decision_version: 2,
|
|
323
|
+
};
|
|
352
324
|
}
|
|
353
325
|
// 层 6: 审查
|
|
354
326
|
if (REVIEW_PATTERNS.some((p) => p.test(normalized))) {
|
|
355
327
|
console.error("[soloForge] 意图路由: 路由到 review");
|
|
356
|
-
return
|
|
328
|
+
return {
|
|
329
|
+
...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),
|
|
330
|
+
route_reason: scoredResult.route_reason,
|
|
331
|
+
decision_version: 2,
|
|
332
|
+
};
|
|
357
333
|
}
|
|
358
|
-
// 层 7:
|
|
334
|
+
// 层 7: 通用分析
|
|
359
335
|
if (ANALYSIS_PATTERNS.some((p) => p.test(normalized))) {
|
|
360
336
|
console.error("[soloForge] 意图路由: 路由到 analysis(通用)");
|
|
361
|
-
return
|
|
337
|
+
return {
|
|
338
|
+
...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),
|
|
339
|
+
route_reason: scoredResult.route_reason,
|
|
340
|
+
decision_version: 2,
|
|
341
|
+
};
|
|
362
342
|
}
|
|
363
343
|
// 层 8: 灰区
|
|
364
344
|
if (/整理(?:一下|下)?/.test(intent)) {
|
|
365
345
|
if (!hasArtifactNoun(intent)) {
|
|
366
|
-
return
|
|
346
|
+
return {
|
|
347
|
+
...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),
|
|
348
|
+
route_reason: "gray_zone:organize",
|
|
349
|
+
decision_version: 2,
|
|
350
|
+
};
|
|
367
351
|
}
|
|
368
352
|
}
|
|
369
353
|
// 层 9: 纯解释
|
|
370
354
|
if (PURE_EXPLAIN_PATTERNS.some((p) => p.test(intent))) {
|
|
371
355
|
if (/(?:这个|这|项目里|项目中的|当前|现有)/.test(intent)) {
|
|
372
|
-
return
|
|
356
|
+
return {
|
|
357
|
+
...mk("analysis", "read_only_analysis", false, [], [], undefined, undefined, 0.7),
|
|
358
|
+
route_reason: "pure_explain+context",
|
|
359
|
+
decision_version: 2,
|
|
360
|
+
};
|
|
373
361
|
}
|
|
374
|
-
return
|
|
362
|
+
return {
|
|
363
|
+
...mk("direct_answer", "none", false, [], [], undefined, undefined, 0.85),
|
|
364
|
+
route_reason: "pure_explain",
|
|
365
|
+
decision_version: 2,
|
|
366
|
+
};
|
|
375
367
|
}
|
|
376
368
|
// 层 10: "看看" 灰区
|
|
377
369
|
if (/看看|看下/.test(intent)) {
|
|
378
370
|
if (/(?:风险|问题|有没有)/.test(intent)) {
|
|
379
|
-
return
|
|
371
|
+
return {
|
|
372
|
+
...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),
|
|
373
|
+
route_reason: "gray:review",
|
|
374
|
+
decision_version: 2,
|
|
375
|
+
};
|
|
380
376
|
}
|
|
381
|
-
return
|
|
377
|
+
return {
|
|
378
|
+
...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),
|
|
379
|
+
route_reason: "gray:look",
|
|
380
|
+
decision_version: 2,
|
|
381
|
+
};
|
|
382
382
|
}
|
|
383
383
|
// 兜底
|
|
384
384
|
console.error("[soloForge] 意图路由: 未匹配任何规则,兜底路由到 direct_answer");
|
|
385
|
-
return
|
|
385
|
+
return {
|
|
386
|
+
...mk("direct_answer", "none", false, [], [], undefined, undefined, 0.3),
|
|
387
|
+
decision_version: 2,
|
|
388
|
+
};
|
|
386
389
|
}
|
|
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
|
-
*/
|
|
390
|
+
// ── 构造函数 ──
|
|
399
391
|
function mk(route, execution_shape, mutation_allowed, input_materials, missing, output_artifact, pipeline_hint, confidence) {
|
|
400
392
|
return {
|
|
401
393
|
route, execution_shape, mutation_allowed, input_materials,
|
|
402
394
|
output_artifact, pipeline_hint, missing_required_inputs: missing, confidence,
|
|
403
395
|
};
|
|
404
396
|
}
|
|
405
|
-
|
|
406
|
-
* 推断流水线提示 — 根据意图文本和产物类型推断适用的流水线阶段。
|
|
407
|
-
* @param intent - 用户意图文本
|
|
408
|
-
* @param artifactKind - 可选的产物类型
|
|
409
|
-
* @returns 流水线提示字符串,无法推断时返回 undefined
|
|
410
|
-
*/
|
|
397
|
+
// ── 流水线提示 ──
|
|
411
398
|
function inferPipelineHint(intent, artifactKind) {
|
|
412
399
|
if (/源码.*原型|原型.*源码|源码包.*原型|prototype/i.test(intent)) {
|
|
413
400
|
return "source-prototype-to-delivery:step-1";
|
|
@@ -423,22 +410,11 @@ function inferPipelineHint(intent, artifactKind) {
|
|
|
423
410
|
default: return undefined;
|
|
424
411
|
}
|
|
425
412
|
}
|
|
426
|
-
|
|
427
|
-
* 判断是否应进入 SoloForge 流程。
|
|
428
|
-
* skip_chat 和 direct_answer 路由不进入 SoloForge。
|
|
429
|
-
*
|
|
430
|
-
* @param decision - 意图路由决策
|
|
431
|
-
* @returns 是否应进入 SoloForge
|
|
432
|
-
*/
|
|
413
|
+
// ── 外部 API(保持不变) ──
|
|
433
414
|
export function shouldEnterSoloForge(decision) {
|
|
434
415
|
console.error("[soloForge] 意图路由: 判断是否进入 SoloForge,当前路由: " + decision.route);
|
|
435
416
|
return decision.route !== "skip_chat" && decision.route !== "direct_answer";
|
|
436
417
|
}
|
|
437
|
-
/**
|
|
438
|
-
* 根据路由决策选择对应的 Prompt 模板。
|
|
439
|
-
* @param decision - 意图路由决策
|
|
440
|
-
* @returns Prompt 模板名称
|
|
441
|
-
*/
|
|
442
418
|
export function selectPromptTemplate(decision) {
|
|
443
419
|
console.error("[soloForge] 意图路由: 选择 Prompt 模板,路由: " + decision.route + ", 执行形态: " + decision.execution_shape);
|
|
444
420
|
if (decision.route === "source_extraction")
|
|
@@ -455,4 +431,24 @@ export function selectPromptTemplate(decision) {
|
|
|
455
431
|
default: return "code_execution_prompt";
|
|
456
432
|
}
|
|
457
433
|
}
|
|
434
|
+
export function debugRouteIntent(input) {
|
|
435
|
+
const { intent, task_id: taskId } = input;
|
|
436
|
+
const materialResult = extractInputMaterials(intent);
|
|
437
|
+
const signals = extractSignals(intent);
|
|
438
|
+
const scored = scoreRoute(intent, signals, materialResult, taskId);
|
|
439
|
+
const decision = routeIntent(input);
|
|
440
|
+
const whyNotSkip = signals.is_skip
|
|
441
|
+
? `skip detected: ${signals.skip_reason}`
|
|
442
|
+
: signals.is_general_qa
|
|
443
|
+
? "general QA pattern"
|
|
444
|
+
: "has SoloForge-relevant signals";
|
|
445
|
+
return {
|
|
446
|
+
intent,
|
|
447
|
+
extracted_materials: materialResult,
|
|
448
|
+
signals,
|
|
449
|
+
scored,
|
|
450
|
+
final_decision: decision,
|
|
451
|
+
why_not_skip: whyNotSkip,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
458
454
|
//# sourceMappingURL=intent_router.js.map
|