@yeaft/webchat-agent 0.1.107 → 0.1.114
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/connection/message-router.js +2 -2
- package/conversation.js +2 -2
- package/crew/persistence.js +38 -0
- package/crew/routing.js +67 -5
- package/crew/session.js +12 -3
- package/crew.js +1 -0
- package/index.js +1 -1
- package/package.json +1 -1
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
import {
|
|
18
18
|
createCrewSession, handleCrewHumanInput, handleCrewControl,
|
|
19
19
|
addRoleToSession, removeRoleFromSession,
|
|
20
|
-
handleListCrewSessions, handleCheckCrewExists, handleDeleteCrewDir, resumeCrewSession, removeFromCrewIndex,
|
|
20
|
+
handleListCrewSessions, handleCheckCrewExists, handleDeleteCrewDir, resumeCrewSession, removeFromCrewIndex, hideCrewSession,
|
|
21
21
|
handleLoadCrewHistory
|
|
22
22
|
} from '../crew.js';
|
|
23
23
|
import { sendToServer, flushMessageBuffer } from './buffer.js';
|
|
@@ -240,7 +240,7 @@ export async function handleMessage(msg) {
|
|
|
240
240
|
break;
|
|
241
241
|
|
|
242
242
|
case 'delete_crew_session':
|
|
243
|
-
await
|
|
243
|
+
await hideCrewSession(msg.sessionId);
|
|
244
244
|
(await import('../conversation.js')).sendConversationList();
|
|
245
245
|
break;
|
|
246
246
|
|
package/conversation.js
CHANGED
|
@@ -85,11 +85,11 @@ export async function sendConversationList() {
|
|
|
85
85
|
type: 'crew',
|
|
86
86
|
});
|
|
87
87
|
}
|
|
88
|
-
// 追加索引中已停止的 crew sessions
|
|
88
|
+
// 追加索引中已停止的 crew sessions(不重复,跳过 hidden)
|
|
89
89
|
try {
|
|
90
90
|
const index = await loadCrewIndex();
|
|
91
91
|
for (const entry of index) {
|
|
92
|
-
if (!activeCrewIds.has(entry.sessionId)) {
|
|
92
|
+
if (!activeCrewIds.has(entry.sessionId) && !entry.hidden) {
|
|
93
93
|
list.push({
|
|
94
94
|
id: entry.sessionId,
|
|
95
95
|
workDir: entry.projectDir,
|
package/crew/persistence.js
CHANGED
|
@@ -76,6 +76,44 @@ export async function removeFromCrewIndex(sessionId) {
|
|
|
76
76
|
// 这些文件在 recreate 时会被复用(合并统计数据 + 恢复消息历史)
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
/**
|
|
80
|
+
* 隐藏 crew session(UI 删除 = 隐藏,不是真删)
|
|
81
|
+
* 在 crew-index 条目上标记 hidden: true + hiddenAt 时间戳,
|
|
82
|
+
* 同时从内存 crewSessions Map 中移除(停止运行态)。
|
|
83
|
+
* 不修改 session.json,不从 index 中移除条目。
|
|
84
|
+
*/
|
|
85
|
+
export async function hideCrewSession(sessionId) {
|
|
86
|
+
const { crewSessions } = await import('./session.js');
|
|
87
|
+
|
|
88
|
+
const index = await loadCrewIndex();
|
|
89
|
+
const entry = index.find(e => e.sessionId === sessionId);
|
|
90
|
+
if (entry) {
|
|
91
|
+
entry.hidden = true;
|
|
92
|
+
entry.hiddenAt = Date.now();
|
|
93
|
+
await saveCrewIndex(index);
|
|
94
|
+
console.log(`[Crew] Hidden session ${sessionId} in index`);
|
|
95
|
+
}
|
|
96
|
+
// 从内存中移除(停止运行态,防止 sendConversationList 加入)
|
|
97
|
+
if (crewSessions.has(sessionId)) {
|
|
98
|
+
crewSessions.delete(sessionId);
|
|
99
|
+
console.log(`[Crew] Removed session ${sessionId} from active sessions`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 取消隐藏 crew session(用户主动恢复时调用)
|
|
105
|
+
*/
|
|
106
|
+
export async function unhideCrewSession(sessionId) {
|
|
107
|
+
const index = await loadCrewIndex();
|
|
108
|
+
const entry = index.find(e => e.sessionId === sessionId);
|
|
109
|
+
if (entry && entry.hidden) {
|
|
110
|
+
delete entry.hidden;
|
|
111
|
+
delete entry.hiddenAt;
|
|
112
|
+
await saveCrewIndex(index);
|
|
113
|
+
console.log(`[Crew] Unhidden session ${sessionId} in index`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
79
117
|
// =====================================================================
|
|
80
118
|
// Session Metadata (.crew/session.json)
|
|
81
119
|
// =====================================================================
|
package/crew/routing.js
CHANGED
|
@@ -27,12 +27,18 @@ export function parseRoutes(text) {
|
|
|
27
27
|
const toMatch = block.match(/to:\s*(.+)/i);
|
|
28
28
|
if (!toMatch) continue;
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
// ★ Clean `to` value: take only the first word (strip parenthetical notes, extra text)
|
|
31
|
+
// e.g. "pm (决策者)" → "pm", "dev-1 // main dev" → "dev-1"
|
|
32
|
+
const toRaw = toMatch[1].trim().toLowerCase();
|
|
33
|
+
const toClean = toRaw.split(/[\s(]/)[0];
|
|
34
|
+
|
|
35
|
+
// ★ summary: match until next known field (task:/taskTitle:) or end of block
|
|
36
|
+
const summaryMatch = block.match(/summary:\s*([\s\S]+?)(?=\n\s*(?:task|taskTitle)\s*:|$)/i);
|
|
31
37
|
const taskMatch = block.match(/^task:\s*(.+)/im);
|
|
32
38
|
const taskTitleMatch = block.match(/^taskTitle:\s*(.+)/im);
|
|
33
39
|
|
|
34
40
|
routes.push({
|
|
35
|
-
to:
|
|
41
|
+
to: toClean,
|
|
36
42
|
summary: summaryMatch ? summaryMatch[1].trim() : '',
|
|
37
43
|
taskId: taskMatch ? taskMatch[1].trim() : null,
|
|
38
44
|
taskTitle: taskTitleMatch ? taskTitleMatch[1].trim() : null
|
|
@@ -42,6 +48,59 @@ export function parseRoutes(text) {
|
|
|
42
48
|
return routes;
|
|
43
49
|
}
|
|
44
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Resolve a ROUTE `to` value to an actual role name in the session.
|
|
53
|
+
*
|
|
54
|
+
* Resolution order:
|
|
55
|
+
* 1. Exact match: `to` matches a role name directly (e.g. "dev-1")
|
|
56
|
+
* 2. roleType match: `to` matches a role's roleType (e.g. "developer" → "dev-1")
|
|
57
|
+
* 3. Short prefix match: `to` matches the SHORT_PREFIX of a roleType (e.g. "dev" → "dev-1")
|
|
58
|
+
* 4. Same-group dispatch: if sender is in a multi-instance group (e.g. dev-1),
|
|
59
|
+
* and `to` matches the roleType/prefix of another group (e.g. "reviewer"),
|
|
60
|
+
* route to the instance with matching groupIndex (e.g. rev-1)
|
|
61
|
+
*
|
|
62
|
+
* For multi-instance matches (2/3), prefer the instance with the same groupIndex
|
|
63
|
+
* as the sender. Falls back to the first instance if no groupIndex match.
|
|
64
|
+
*
|
|
65
|
+
* @param {string} to - raw route target from ROUTE block
|
|
66
|
+
* @param {object} session - crew session
|
|
67
|
+
* @param {string} [fromRole] - sending role name (for groupIndex matching)
|
|
68
|
+
* @returns {string|null} resolved role name, or null if unresolvable
|
|
69
|
+
*/
|
|
70
|
+
export function resolveRoleName(to, session, fromRole) {
|
|
71
|
+
// 1. Exact match
|
|
72
|
+
if (session.roles.has(to)) return to;
|
|
73
|
+
|
|
74
|
+
// Build candidate list by roleType and short prefix
|
|
75
|
+
const fromRoleConfig = fromRole ? session.roles.get(fromRole) : null;
|
|
76
|
+
const fromGroupIndex = fromRoleConfig?.groupIndex || 0;
|
|
77
|
+
|
|
78
|
+
let candidates = [];
|
|
79
|
+
|
|
80
|
+
for (const [name, config] of session.roles) {
|
|
81
|
+
// 2. roleType match (e.g. "developer" → dev-1, dev-2, dev-3)
|
|
82
|
+
if (config.roleType === to) {
|
|
83
|
+
candidates.push({ name, groupIndex: config.groupIndex || 0 });
|
|
84
|
+
}
|
|
85
|
+
// 3. Short prefix match (e.g. "dev" → developer roleType → dev-1)
|
|
86
|
+
// Match if the role name starts with `to-` (e.g. "dev" matches "dev-1", "dev-2")
|
|
87
|
+
else if (name.startsWith(to + '-') && /^\d+$/.test(name.slice(to.length + 1))) {
|
|
88
|
+
candidates.push({ name, groupIndex: config.groupIndex || 0 });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (candidates.length === 0) return null;
|
|
93
|
+
|
|
94
|
+
// 4. Prefer same groupIndex as sender
|
|
95
|
+
if (fromGroupIndex > 0) {
|
|
96
|
+
const sameGroup = candidates.find(c => c.groupIndex === fromGroupIndex);
|
|
97
|
+
if (sameGroup) return sameGroup.name;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Fall back to first candidate
|
|
101
|
+
return candidates[0].name;
|
|
102
|
+
}
|
|
103
|
+
|
|
45
104
|
/**
|
|
46
105
|
* 执行路由
|
|
47
106
|
*/
|
|
@@ -68,7 +127,9 @@ export async function executeRoute(session, fromRole, route) {
|
|
|
68
127
|
// 更新工作看板:推断状态
|
|
69
128
|
const { getMessages } = await import('../crew-i18n.js');
|
|
70
129
|
const m = getMessages(session.language || 'zh-CN');
|
|
71
|
-
|
|
130
|
+
// ★ Use resolveRoleName for kanban status lookup too
|
|
131
|
+
const resolvedKanbanTo = resolveRoleName(to, session, fromRole);
|
|
132
|
+
const toRoleConfig = session.roles.get(resolvedKanbanTo || to);
|
|
72
133
|
let status = m.kanbanStatusDev;
|
|
73
134
|
if (toRoleConfig) {
|
|
74
135
|
switch (toRoleConfig.roleType) {
|
|
@@ -111,13 +172,14 @@ export async function executeRoute(session, fromRole, route) {
|
|
|
111
172
|
}
|
|
112
173
|
|
|
113
174
|
// 路由到指定角色
|
|
114
|
-
|
|
175
|
+
const resolvedTo = resolveRoleName(to, session, fromRole);
|
|
176
|
+
if (resolvedTo) {
|
|
115
177
|
if (session.humanMessageQueue.length > 0) {
|
|
116
178
|
const { processHumanQueue } = await import('./human-interaction.js');
|
|
117
179
|
await processHumanQueue(session);
|
|
118
180
|
} else {
|
|
119
181
|
const taskPrompt = buildRoutePrompt(fromRole, summary, session);
|
|
120
|
-
await dispatchToRole(session,
|
|
182
|
+
await dispatchToRole(session, resolvedTo, taskPrompt, fromRole, taskId, taskTitle);
|
|
121
183
|
}
|
|
122
184
|
} else {
|
|
123
185
|
console.warn(`[Crew] Unknown route target: ${to}`);
|
package/crew/session.js
CHANGED
|
@@ -8,7 +8,7 @@ import { getMessages } from '../crew-i18n.js';
|
|
|
8
8
|
import { initWorktrees } from './worktree.js';
|
|
9
9
|
import { initSharedDir, writeRoleClaudeMd, updateSharedClaudeMd, backupMemoryContent } from './shared-dir.js';
|
|
10
10
|
import {
|
|
11
|
-
loadCrewIndex, upsertCrewIndex, removeFromCrewIndex,
|
|
11
|
+
loadCrewIndex, upsertCrewIndex, removeFromCrewIndex, unhideCrewSession,
|
|
12
12
|
loadSessionMeta, saveSessionMeta, loadSessionMessages, getMaxShardIndex
|
|
13
13
|
} from './persistence.js';
|
|
14
14
|
import { sendCrewMessage, sendCrewOutput, sendStatusUpdate } from './ui-messages.js';
|
|
@@ -121,6 +121,7 @@ async function findExistingSessionByProjectDir(projectDir) {
|
|
|
121
121
|
e.projectDir.replace(/\/+$/, '') === normalizedDir
|
|
122
122
|
&& (!agentId || !e.agentId || e.agentId === agentId)
|
|
123
123
|
&& e.status !== 'completed'
|
|
124
|
+
&& !e.hidden
|
|
124
125
|
);
|
|
125
126
|
|
|
126
127
|
if (match) {
|
|
@@ -428,6 +429,9 @@ export async function handleDeleteCrewDir(msg) {
|
|
|
428
429
|
export async function resumeCrewSession(msg) {
|
|
429
430
|
const { sessionId, userId, username } = msg;
|
|
430
431
|
|
|
432
|
+
// 用户主动恢复 → 清除 hidden 标记(如果有的话)
|
|
433
|
+
await unhideCrewSession(sessionId);
|
|
434
|
+
|
|
431
435
|
if (crewSessions.has(sessionId)) {
|
|
432
436
|
const session = crewSessions.get(sessionId);
|
|
433
437
|
const roles = Array.from(session.roles.values());
|
|
@@ -465,17 +469,22 @@ export async function resumeCrewSession(msg) {
|
|
|
465
469
|
const index = await loadCrewIndex();
|
|
466
470
|
const indexEntry = index.find(e => e.sessionId === sessionId);
|
|
467
471
|
if (!indexEntry) {
|
|
468
|
-
|
|
472
|
+
console.warn(`[Crew] resumeCrewSession: session ${sessionId} not found in index`);
|
|
473
|
+
sendCrewMessage({ type: 'crew_session_restore_failed', sessionId, message: 'Crew session not found in index' });
|
|
469
474
|
return;
|
|
470
475
|
}
|
|
471
476
|
|
|
472
477
|
const meta = await loadSessionMeta(indexEntry.sharedDir);
|
|
473
478
|
if (!meta) {
|
|
474
|
-
|
|
479
|
+
console.warn(`[Crew] resumeCrewSession: session.json not found at ${indexEntry.sharedDir}`);
|
|
480
|
+
sendCrewMessage({ type: 'crew_session_restore_failed', sessionId, message: 'Crew session metadata not found' });
|
|
475
481
|
return;
|
|
476
482
|
}
|
|
477
483
|
|
|
478
484
|
const roles = meta.roles || [];
|
|
485
|
+
if (roles.length === 0) {
|
|
486
|
+
console.warn(`[Crew] resumeCrewSession: session ${sessionId} has empty roles in session.json`);
|
|
487
|
+
}
|
|
479
488
|
const decisionMaker = meta.decisionMaker || roles[0]?.name || null;
|
|
480
489
|
const session = {
|
|
481
490
|
id: sessionId,
|
package/crew.js
CHANGED
package/index.js
CHANGED
|
@@ -78,7 +78,7 @@ const CONFIG = {
|
|
|
78
78
|
// 最大上下文 tokens(用于百分比计算的分母)
|
|
79
79
|
maxContextTokens: parseInt(process.env.MAX_CONTEXT_TOKENS || fileConfig.maxContextTokens, 10) || 128000,
|
|
80
80
|
// Auto-compact 阈值(tokens):context 超过此值时自动触发 compact
|
|
81
|
-
autoCompactThreshold: parseInt(process.env.AUTO_COMPACT_THRESHOLD || fileConfig.autoCompactThreshold, 10) ||
|
|
81
|
+
autoCompactThreshold: parseInt(process.env.AUTO_COMPACT_THRESHOLD || fileConfig.autoCompactThreshold, 10) || 110000
|
|
82
82
|
};
|
|
83
83
|
|
|
84
84
|
// 初始化共享上下文
|