@yeaft/webchat-agent 0.1.76 → 0.1.78
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/cli.js +1 -0
- package/connection/upgrade-worker-template.js +2 -2
- package/connection/upgrade.js +5 -4
- package/crew/control.js +31 -6
- package/crew/human-interaction.js +25 -4
- package/crew/role-query.js +38 -0
- package/crew/routing.js +36 -5
- package/package.json +1 -1
package/cli.js
CHANGED
|
@@ -76,7 +76,7 @@ try {
|
|
|
76
76
|
log('Temp dir: ' + tmpDir);
|
|
77
77
|
|
|
78
78
|
const packOutput = execFileSync('npm', ['pack', PKG, '--pack-destination', tmpDir], {
|
|
79
|
-
shell:
|
|
79
|
+
shell: process.platform === 'win32', encoding: 'utf8', cwd: tmpDir, timeout: 120000
|
|
80
80
|
}).trim();
|
|
81
81
|
const tgzName = packOutput.split('\n').pop().trim();
|
|
82
82
|
const tgzPath = path.join(tmpDir, tgzName);
|
|
@@ -100,7 +100,7 @@ try {
|
|
|
100
100
|
log('Installing dependencies...');
|
|
101
101
|
try {
|
|
102
102
|
execFileSync('npm', ['install', '--omit=dev'], {
|
|
103
|
-
shell:
|
|
103
|
+
shell: process.platform === 'win32', cwd: TARGET, encoding: 'utf8', timeout: 120000
|
|
104
104
|
});
|
|
105
105
|
log('Dependencies installed');
|
|
106
106
|
} catch (depErr) {
|
package/connection/upgrade.js
CHANGED
|
@@ -46,7 +46,7 @@ export async function handleUpgradeAgent() {
|
|
|
46
46
|
const pkgName = ctx.pkgName || '@yeaft/webchat-agent';
|
|
47
47
|
// Check latest version (async to avoid blocking heartbeat)
|
|
48
48
|
const latestVersion = await new Promise((resolve, reject) => {
|
|
49
|
-
execFile('npm', ['view', pkgName, 'version'], { stdio: 'pipe', shell:
|
|
49
|
+
execFile('npm', ['view', pkgName, 'version'], { stdio: 'pipe', shell: process.platform === 'win32' }, (err, stdout) => {
|
|
50
50
|
if (err) reject(err); else resolve(stdout.toString().trim());
|
|
51
51
|
});
|
|
52
52
|
});
|
|
@@ -74,7 +74,7 @@ export async function handleUpgradeAgent() {
|
|
|
74
74
|
|
|
75
75
|
// 判断全局安装 vs 局部安装
|
|
76
76
|
const isGlobalInstall = await new Promise((resolve) => {
|
|
77
|
-
execFile('npm', ['prefix', '-g'], { shell:
|
|
77
|
+
execFile('npm', ['prefix', '-g'], { shell: process.platform === 'win32' }, (err, stdout) => {
|
|
78
78
|
if (err) { resolve(false); return; }
|
|
79
79
|
const globalPrefix = stdout.toString().trim().replace(/\\/g, '/');
|
|
80
80
|
resolve(installDir === globalPrefix || installDir === globalPrefix + '/lib');
|
|
@@ -94,7 +94,7 @@ export async function handleUpgradeAgent() {
|
|
|
94
94
|
const isPm2 = !!process.env.pm_id;
|
|
95
95
|
if (isPm2) {
|
|
96
96
|
try {
|
|
97
|
-
execFileSync('pm2', ['delete', PM2_APP_NAME], { shell:
|
|
97
|
+
execFileSync('pm2', ['delete', PM2_APP_NAME], { shell: process.platform === 'win32', stdio: 'pipe' });
|
|
98
98
|
console.log(`[Agent] PM2 app deleted to prevent auto-restart during upgrade`);
|
|
99
99
|
} catch {
|
|
100
100
|
console.log(`[Agent] PM2 delete skipped (app may not be registered)`);
|
|
@@ -201,8 +201,9 @@ function spawnWindowsUpgradeScript(pkgName, installDir, isGlobalInstall, latestV
|
|
|
201
201
|
|
|
202
202
|
writeFileSync(batPath, batLines.join('\r\n'));
|
|
203
203
|
const child = spawn('cmd.exe', ['/c', batPath], {
|
|
204
|
+
detached: true,
|
|
204
205
|
stdio: 'ignore',
|
|
205
|
-
windowsHide: true
|
|
206
|
+
windowsHide: true,
|
|
206
207
|
});
|
|
207
208
|
child.unref();
|
|
208
209
|
console.log(`[Agent] Spawned upgrade script (PID wait for ${pid}, pm2=${isPm2}, dir=${installDir}): ${batPath}`);
|
package/crew/control.js
CHANGED
|
@@ -66,6 +66,11 @@ export async function handleCrewControl(msg) {
|
|
|
66
66
|
async function clearSingleRole(session, roleName) {
|
|
67
67
|
const roleState = session.roleStates.get(roleName);
|
|
68
68
|
|
|
69
|
+
// P0-1: 清除该角色在 humanMessageQueue 中的待处理消息,防止幽灵 ROUTE 执行
|
|
70
|
+
if (session.humanMessageQueue.length > 0) {
|
|
71
|
+
session.humanMessageQueue = session.humanMessageQueue.filter(m => m.target !== roleName);
|
|
72
|
+
}
|
|
73
|
+
|
|
69
74
|
if (roleState) {
|
|
70
75
|
// 保存工作摘要到 task file(与 context_exceeded clear 一致)
|
|
71
76
|
if (roleState.accumulatedText) {
|
|
@@ -73,9 +78,21 @@ async function clearSingleRole(session, roleName) {
|
|
|
73
78
|
console.warn(`[Crew] Failed to save work summary for ${roleName}:`, e.message));
|
|
74
79
|
}
|
|
75
80
|
|
|
81
|
+
// P1-3: abort 并等待 query iterator 退出,避免 abort+dispatch 竞态
|
|
76
82
|
if (roleState.abortController) {
|
|
77
83
|
roleState.abortController.abort();
|
|
78
84
|
}
|
|
85
|
+
// 等待 query 完成(iterator 会因 AbortError 退出)
|
|
86
|
+
if (roleState.query) {
|
|
87
|
+
try {
|
|
88
|
+
// Drain the iterator — it will throw AbortError and exit
|
|
89
|
+
// eslint-disable-next-line no-empty
|
|
90
|
+
for await (const _ of roleState.query) {}
|
|
91
|
+
} catch {
|
|
92
|
+
// Expected: AbortError or other cleanup errors
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
79
96
|
roleState.query = null;
|
|
80
97
|
roleState.inputStream = null;
|
|
81
98
|
roleState.turnActive = false;
|
|
@@ -86,6 +103,10 @@ async function clearSingleRole(session, roleName) {
|
|
|
86
103
|
roleState.lastDispatchFrom = null;
|
|
87
104
|
roleState.lastDispatchTaskId = null;
|
|
88
105
|
roleState.lastDispatchTaskTitle = null;
|
|
106
|
+
// P0-3: 重置 UI 相关状态,防止显示过时信息
|
|
107
|
+
roleState.currentTask = null;
|
|
108
|
+
roleState.currentTool = null;
|
|
109
|
+
roleState.lastTurnText = '';
|
|
89
110
|
}
|
|
90
111
|
|
|
91
112
|
await clearRoleSessionId(session.sharedDir, roleName);
|
|
@@ -168,12 +189,12 @@ async function resumeSession(session) {
|
|
|
168
189
|
const pending = session.pendingRoutes.slice();
|
|
169
190
|
session.pendingRoutes = [];
|
|
170
191
|
console.log(`[Crew] Replaying ${pending.length} pending route(s)`);
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
console.warn(`[Crew] Pending route replay failed:`,
|
|
192
|
+
// P1-5: 串行执行 pending routes,防止多个 route 指向同一角色时并发 dispatch
|
|
193
|
+
for (const { fromRole, route } of pending) {
|
|
194
|
+
try {
|
|
195
|
+
await executeRoute(session, fromRole, route);
|
|
196
|
+
} catch (err) {
|
|
197
|
+
console.warn(`[Crew] Pending route replay failed:`, err);
|
|
177
198
|
}
|
|
178
199
|
}
|
|
179
200
|
return;
|
|
@@ -338,6 +359,9 @@ async function stopAll(session) {
|
|
|
338
359
|
* 清空 session
|
|
339
360
|
*/
|
|
340
361
|
async function clearSession(session) {
|
|
362
|
+
// P1-2: 先重置 _processingHumanQueue,防止后续消息处理被阻塞
|
|
363
|
+
session._processingHumanQueue = false;
|
|
364
|
+
|
|
341
365
|
for (const [roleName, roleState] of session.roleStates) {
|
|
342
366
|
if (roleState.abortController) {
|
|
343
367
|
roleState.abortController.abort();
|
|
@@ -350,6 +374,7 @@ async function clearSession(session) {
|
|
|
350
374
|
await clearRoleSessionId(session.sharedDir, roleName);
|
|
351
375
|
}
|
|
352
376
|
|
|
377
|
+
// P1-1: humanMessageQueue 在这里清空,确保不会有幽灵消息在 clear 后被处理
|
|
353
378
|
session.messageHistory = [];
|
|
354
379
|
session.uiMessages = [];
|
|
355
380
|
session.humanMessageQueue = [];
|
|
@@ -86,10 +86,31 @@ export async function handleCrewHumanInput(msg) {
|
|
|
86
86
|
const { createRoleQuery } = await import('./role-query.js');
|
|
87
87
|
roleState = await createRoleQuery(session, target);
|
|
88
88
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
89
|
+
// P1-4: 守卫 stream.enqueue
|
|
90
|
+
try {
|
|
91
|
+
if (roleState.inputStream && !roleState.inputStream.isDone) {
|
|
92
|
+
roleState.inputStream.enqueue({
|
|
93
|
+
type: 'user',
|
|
94
|
+
message: { role: 'user', content: message }
|
|
95
|
+
});
|
|
96
|
+
} else {
|
|
97
|
+
console.warn(`[Crew] Skill dispatch: stream closed for ${target}, recreating`);
|
|
98
|
+
const { createRoleQuery } = await import('./role-query.js');
|
|
99
|
+
roleState = await createRoleQuery(session, target);
|
|
100
|
+
roleState.inputStream.enqueue({
|
|
101
|
+
type: 'user',
|
|
102
|
+
message: { role: 'user', content: message }
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
} catch (enqueueErr) {
|
|
106
|
+
console.error(`[Crew] Skill dispatch enqueue failed for ${target}:`, enqueueErr.message);
|
|
107
|
+
const { createRoleQuery } = await import('./role-query.js');
|
|
108
|
+
roleState = await createRoleQuery(session, target);
|
|
109
|
+
roleState.inputStream.enqueue({
|
|
110
|
+
type: 'user',
|
|
111
|
+
message: { role: 'user', content: message }
|
|
112
|
+
});
|
|
113
|
+
}
|
|
93
114
|
sendStatusUpdate(session);
|
|
94
115
|
console.log(`[Crew] Skill command dispatched to ${target}: ${message}`);
|
|
95
116
|
return;
|
package/crew/role-query.js
CHANGED
|
@@ -84,10 +84,48 @@ export function classifyRoleError(error) {
|
|
|
84
84
|
// Role Query Management
|
|
85
85
|
// =====================================================================
|
|
86
86
|
|
|
87
|
+
// P1-6: Per-role mutex to prevent concurrent createRoleQuery calls creating orphan processes
|
|
88
|
+
const _roleQueryLocks = new Map();
|
|
89
|
+
|
|
87
90
|
/**
|
|
88
91
|
* 为角色创建持久 query 实例
|
|
89
92
|
*/
|
|
90
93
|
export async function createRoleQuery(session, roleName) {
|
|
94
|
+
const lockKey = `${session.id}:${roleName}`;
|
|
95
|
+
|
|
96
|
+
// P1-6: 如果该角色已有 pending 的 createRoleQuery,等待它完成
|
|
97
|
+
if (_roleQueryLocks.has(lockKey)) {
|
|
98
|
+
console.log(`[Crew] Waiting for existing createRoleQuery lock on ${roleName}`);
|
|
99
|
+
try {
|
|
100
|
+
await _roleQueryLocks.get(lockKey);
|
|
101
|
+
} catch {
|
|
102
|
+
// Previous attempt failed, proceed with new creation
|
|
103
|
+
}
|
|
104
|
+
// 锁释放后,检查是否已经有可用的 query
|
|
105
|
+
const existing = session.roleStates.get(roleName);
|
|
106
|
+
if (existing?.query && existing?.inputStream && !existing.inputStream.isDone) {
|
|
107
|
+
console.log(`[Crew] Reusing existing query for ${roleName} after lock release`);
|
|
108
|
+
return existing;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const promise = _createRoleQueryInner(session, roleName);
|
|
113
|
+
_roleQueryLocks.set(lockKey, promise);
|
|
114
|
+
try {
|
|
115
|
+
const result = await promise;
|
|
116
|
+
return result;
|
|
117
|
+
} finally {
|
|
118
|
+
// 只删除自己的 lock(如果后续调用覆盖了,不误删)
|
|
119
|
+
if (_roleQueryLocks.get(lockKey) === promise) {
|
|
120
|
+
_roleQueryLocks.delete(lockKey);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* createRoleQuery 内部实现
|
|
127
|
+
*/
|
|
128
|
+
async function _createRoleQueryInner(session, roleName) {
|
|
91
129
|
const role = session.roles.get(roleName);
|
|
92
130
|
if (!role) throw new Error(`Role not found: ${roleName}`);
|
|
93
131
|
|
package/crew/routing.js
CHANGED
|
@@ -227,17 +227,48 @@ export async function dispatchToRole(session, roleName, content, fromSource, tas
|
|
|
227
227
|
roleState = await createRoleQuery(session, roleName);
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
-
//
|
|
230
|
+
// P1-4: 守卫 stream.enqueue — stream 可能已被 abort 关闭
|
|
231
231
|
roleState.lastDispatchContent = content;
|
|
232
232
|
roleState.lastDispatchFrom = fromSource;
|
|
233
233
|
roleState.lastDispatchTaskId = taskId || null;
|
|
234
234
|
roleState.lastDispatchTaskTitle = taskTitle || null;
|
|
235
235
|
roleState.turnActive = true;
|
|
236
236
|
roleState.accumulatedText = '';
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
237
|
+
try {
|
|
238
|
+
if (roleState.inputStream && !roleState.inputStream.isDone) {
|
|
239
|
+
roleState.inputStream.enqueue({
|
|
240
|
+
type: 'user',
|
|
241
|
+
message: { role: 'user', content }
|
|
242
|
+
});
|
|
243
|
+
} else {
|
|
244
|
+
console.warn(`[Crew] Cannot enqueue to ${roleName}: stream closed or missing, recreating`);
|
|
245
|
+
roleState = await createRoleQuery(session, roleName);
|
|
246
|
+
roleState.lastDispatchContent = content;
|
|
247
|
+
roleState.lastDispatchFrom = fromSource;
|
|
248
|
+
roleState.lastDispatchTaskId = taskId || null;
|
|
249
|
+
roleState.lastDispatchTaskTitle = taskTitle || null;
|
|
250
|
+
roleState.turnActive = true;
|
|
251
|
+
roleState.accumulatedText = '';
|
|
252
|
+
roleState.inputStream.enqueue({
|
|
253
|
+
type: 'user',
|
|
254
|
+
message: { role: 'user', content }
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
} catch (enqueueErr) {
|
|
258
|
+
console.error(`[Crew] Failed to enqueue to ${roleName}:`, enqueueErr.message);
|
|
259
|
+
// Recreate query and retry once
|
|
260
|
+
roleState = await createRoleQuery(session, roleName);
|
|
261
|
+
roleState.lastDispatchContent = content;
|
|
262
|
+
roleState.lastDispatchFrom = fromSource;
|
|
263
|
+
roleState.lastDispatchTaskId = taskId || null;
|
|
264
|
+
roleState.lastDispatchTaskTitle = taskTitle || null;
|
|
265
|
+
roleState.turnActive = true;
|
|
266
|
+
roleState.accumulatedText = '';
|
|
267
|
+
roleState.inputStream.enqueue({
|
|
268
|
+
type: 'user',
|
|
269
|
+
message: { role: 'user', content }
|
|
270
|
+
});
|
|
271
|
+
}
|
|
241
272
|
|
|
242
273
|
sendStatusUpdate(session);
|
|
243
274
|
console.log(`[Crew] Dispatched to ${roleName} from ${fromSource}${taskId ? ` (task: ${taskId})` : ''}`);
|