@yeaft/webchat-agent 0.1.447 → 0.1.449

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.
@@ -12,6 +12,34 @@ import ctx from '../context.js';
12
12
  // Context 使用率常量(运行时从 ctx.CONFIG 读取)
13
13
  const getMaxContext = () => ctx.CONFIG?.maxContextTokens || 128000;
14
14
 
15
+ /**
16
+ * Detect routing intent in text that lacks a proper ROUTE block.
17
+ * Returns true if keywords suggest the role intended to route to someone.
18
+ * @param {string} text
19
+ * @returns {boolean}
20
+ */
21
+ export function _detectRouteIntent(text) {
22
+ if (!text || text.length < 10) return false;
23
+ // Check only the last 1000 chars (routing intent is usually at the end)
24
+ const tail = text.slice(-1000);
25
+ // Chinese patterns: 提交给/交给/请.*审查/转给/发给/route to
26
+ // English patterns: route to/submit to/forward to/hand off to/pass to
27
+ const intentPatterns = [
28
+ /提交给\s*\S+/,
29
+ /交给\s*\S+/,
30
+ /请\s*\S+\s*审查/,
31
+ /转给\s*\S+/,
32
+ /发给\s*\S+/,
33
+ /route\s+to\s+\S+/i,
34
+ /submit\s+to\s+\S+/i,
35
+ /forward\s+to\s+\S+/i,
36
+ /hand\s*off\s+to\s+\S+/i,
37
+ /pass\s+to\s+\S+/i,
38
+ /ROUTE[→:]\s*\S+/, // shorthand that parseRoutes might have already caught, but as safety net
39
+ ];
40
+ return intentPatterns.some(p => p.test(tail));
41
+ }
42
+
15
43
  /**
16
44
  * 处理角色的流式输出
17
45
  */
@@ -224,8 +252,21 @@ export async function processRoleOutput(session, roleName, roleQuery, roleState)
224
252
  });
225
253
  sendStatusUpdate(session);
226
254
  } else {
227
- const { processHumanQueue } = await import('./human-interaction.js');
228
- await processHumanQueue(session);
255
+ // Route intent detection: if no ROUTE block but text suggests routing intent,
256
+ // auto-forward to PM so the message doesn't get lost
257
+ if (_detectRouteIntent(roleState.lastTurnText) && roleName !== session.decisionMaker) {
258
+ console.log(`[Crew] ${roleName} turn ended without ROUTE but has routing intent — auto-forwarding to PM`);
259
+ const autoSummary = `[auto-forward: ${roleName} 的输出包含路由意图但缺少 ROUTE 块]\n${(roleState.lastTurnText || '').slice(-500).trim()}`;
260
+ await executeRoute(session, roleName, {
261
+ to: session.decisionMaker,
262
+ summary: autoSummary,
263
+ taskId: roleState.currentTask?.taskId || null,
264
+ taskTitle: roleState.currentTask?.taskTitle || null,
265
+ });
266
+ } else {
267
+ const { processHumanQueue } = await import('./human-interaction.js');
268
+ await processHumanQueue(session);
269
+ }
229
270
  }
230
271
  }
231
272
  }
package/crew/routing.js CHANGED
@@ -38,42 +38,98 @@ function _appendTextToContent(content, text) {
38
38
  */
39
39
  export function parseRoutes(text) {
40
40
  const routes = [];
41
+
42
+ // ─── Phase 1: Standard ROUTE blocks (with END_ROUTE) ──────────
41
43
  // ★ Tolerate both underscore and space variants: ---END_ROUTE--- or ---END ROUTE---
42
- const regex = /---ROUTE---\s*\n([\s\S]*?)---END[_ ]ROUTE---/g;
44
+ // Use negative lookahead to not cross another ---ROUTE--- boundary
45
+ const regex = /---ROUTE---\s*\n((?:(?!---ROUTE---)[\s\S])*?)---END[_ ]ROUTE---/g;
43
46
  let match;
47
+ const matchedRanges = []; // track matched ranges to avoid double-parsing
44
48
 
45
49
  while ((match = regex.exec(text)) !== null) {
46
- const block = match[1];
47
- const toMatch = block.match(/to:\s*(.+)/i);
48
- if (!toMatch) continue;
49
-
50
- // ★ Clean `to` value: take only the first word (strip parenthetical notes, extra text)
51
- // e.g. "pm (决策者)" → "pm", "dev-1 // main dev" → "dev-1"
52
- const toRaw = toMatch[1].trim().toLowerCase();
53
- // Strip trailing punctuation (commas, semicolons, colons, etc.)
54
- const toClean = toRaw.split(/[\s(]/)[0].replace(/[,;:!?。,;:!?]+$/, '');
55
-
56
- // ★ summary: match until next known field (task:/taskTitle:) or end of block
57
- const summaryMatch = block.match(/summary:\s*([\s\S]+?)(?=\n\s*(?:task|taskTitle)\s*:|$)/i);
58
- const taskMatch = block.match(/^task:\s*(.+)/im);
59
- const taskTitleMatch = block.match(/^taskTitle:\s*(.+)/im);
60
-
61
- let summary = summaryMatch ? summaryMatch[1].trim() : '';
62
- if (!summary) {
63
- summary = '[该角色未提供消息摘要]';
64
- }
50
+ matchedRanges.push({ start: match.index, end: match.index + match[0].length });
51
+ const parsed = _parseRouteBlock(match[1]);
52
+ if (parsed) routes.push(parsed);
53
+ }
65
54
 
66
- routes.push({
67
- to: toClean,
68
- summary,
69
- taskId: taskMatch ? taskMatch[1].trim() : null,
70
- taskTitle: taskTitleMatch ? taskTitleMatch[1].trim() : null
71
- });
55
+ // ─── Phase 2: Fallback — ROUTE block missing END_ROUTE ────────
56
+ // Match ---ROUTE--- without a closing ---END_ROUTE---
57
+ // Take content until next ---ROUTE--- or EOF
58
+ const openRegex = /---ROUTE---\s*\n/g;
59
+ while ((match = openRegex.exec(text)) !== null) {
60
+ // Skip if this range was already captured by Phase 1
61
+ const pos = match.index;
62
+ if (matchedRanges.some(r => pos >= r.start && pos < r.end)) continue;
63
+
64
+ const blockStart = pos + match[0].length;
65
+ // End at next ---ROUTE--- or EOF
66
+ const nextRoute = text.indexOf('---ROUTE---', blockStart);
67
+ const blockEnd = nextRoute !== -1 ? nextRoute : text.length;
68
+ const block = text.slice(blockStart, blockEnd);
69
+
70
+ const parsed = _parseRouteBlock(block);
71
+ if (parsed) routes.push(parsed);
72
+ }
73
+
74
+ // ─── Phase 3: Shorthand — "ROUTE → target" / "ROUTE: target" ─
75
+ // Matches single-line shorthands like: ROUTE → dev-1: summary here
76
+ // or: ROUTE: dev-1, summary here
77
+ const shorthandRegex = /^ROUTE\s*[→:]\s*(\S+)[,:\s]*(.*)$/gm;
78
+ while ((match = shorthandRegex.exec(text)) !== null) {
79
+ // Skip if inside an already-matched ROUTE block range
80
+ const pos = match.index;
81
+ if (matchedRanges.some(r => pos >= r.start && pos < r.end)) continue;
82
+ // Also skip if the line is inside a ---ROUTE--- block (even unclosed)
83
+ const precedingText = text.slice(0, pos);
84
+ const lastRouteOpen = precedingText.lastIndexOf('---ROUTE---');
85
+ const lastRouteClose = Math.max(
86
+ precedingText.lastIndexOf('---END_ROUTE---'),
87
+ precedingText.lastIndexOf('---END ROUTE---')
88
+ );
89
+ if (lastRouteOpen > lastRouteClose) continue; // inside an open block
90
+
91
+ const toRaw = match[1].trim().toLowerCase().replace(/[,;:!?。,;:!?]+$/, '');
92
+ const summary = match[2] ? match[2].trim() : '[该角色未提供消息摘要]';
93
+
94
+ routes.push({ to: toRaw, summary, taskId: null, taskTitle: null });
72
95
  }
73
96
 
74
97
  return routes;
75
98
  }
76
99
 
100
+ /**
101
+ * Parse fields from a ROUTE block body (the content between ---ROUTE--- and ---END_ROUTE---).
102
+ * @param {string} block — raw block content
103
+ * @returns {{ to: string, summary: string, taskId: string|null, taskTitle: string|null } | null}
104
+ */
105
+ function _parseRouteBlock(block) {
106
+ const toMatch = block.match(/to:\s*(.+)/i);
107
+ if (!toMatch) return null;
108
+
109
+ // ★ Clean `to` value: take only the first word (strip parenthetical notes, extra text)
110
+ // e.g. "pm (决策者)" → "pm", "dev-1 // main dev" → "dev-1"
111
+ const toRaw = toMatch[1].trim().toLowerCase();
112
+ // Strip trailing punctuation (commas, semicolons, colons, etc.)
113
+ const toClean = toRaw.split(/[\s(]/)[0].replace(/[,;:!?。,;:!?]+$/, '');
114
+
115
+ // ★ summary: match until next known field (task:/taskTitle:) or end of block
116
+ const summaryMatch = block.match(/summary:\s*([\s\S]+?)(?=\n\s*(?:task|taskTitle)\s*:|$)/i);
117
+ const taskMatch = block.match(/^task:\s*(.+)/im);
118
+ const taskTitleMatch = block.match(/^taskTitle:\s*(.+)/im);
119
+
120
+ let summary = summaryMatch ? summaryMatch[1].trim() : '';
121
+ if (!summary) {
122
+ summary = '[该角色未提供消息摘要]';
123
+ }
124
+
125
+ return {
126
+ to: toClean,
127
+ summary,
128
+ taskId: taskMatch ? taskMatch[1].trim() : null,
129
+ taskTitle: taskTitleMatch ? taskTitleMatch[1].trim() : null
130
+ };
131
+ }
132
+
77
133
  /**
78
134
  * Resolve a ROUTE `to` value to an actual role name in the session.
79
135
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.1.447",
3
+ "version": "0.1.449",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",