@yeaft/webchat-agent 0.1.247 → 0.1.249

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.
@@ -20,11 +20,6 @@ import {
20
20
  handleListCrewSessions, handleCheckCrewExists, handleDeleteCrewDir, resumeCrewSession, removeFromCrewIndex, hideCrewSession,
21
21
  handleLoadCrewHistory, handleCheckCrewContext
22
22
  } from '../crew.js';
23
- import {
24
- initConductor, handleConductorUserInput,
25
- stopConductor, clearConductor,
26
- handleConductorHistory
27
- } from '../conductor.js';
28
23
  import { sendToServer, flushMessageBuffer } from './buffer.js';
29
24
  import { handleRestartAgent, handleUpgradeAgent } from './upgrade.js';
30
25
  import { loadMcpServers, updateMcpConfig } from '../mcp.js';
@@ -260,27 +255,6 @@ export async function handleMessage(msg) {
260
255
  await handleLoadCrewHistory(msg);
261
256
  break;
262
257
 
263
- // Conductor (V2) messages
264
- case 'open_conductor':
265
- await initConductor(msg);
266
- break;
267
-
268
- case 'conductor_user_input':
269
- await handleConductorUserInput(msg);
270
- break;
271
-
272
- case 'stop_conductor':
273
- await stopConductor();
274
- break;
275
-
276
- case 'clear_conductor':
277
- await clearConductor();
278
- break;
279
-
280
- case 'conductor_load_history':
281
- await handleConductorHistory(msg);
282
- break;
283
-
284
258
  // Port proxy
285
259
  case 'proxy_request':
286
260
  handleProxyHttpRequest(msg);
package/conversation.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import ctx from './context.js';
2
- import { query, Stream } from './sdk/index.js';
2
+ import { query } from './sdk/index.js';
3
3
  import { loadSessionHistory } from './history.js';
4
4
  import { startClaudeQuery } from './claude.js';
5
5
  import { crewSessions, loadCrewIndex } from './crew.js';
@@ -23,8 +23,9 @@ function prestartClaude(conversationId, workDir, resumeSessionId) {
23
23
 
24
24
  /**
25
25
  * Preload slash commands from Claude CLI (fire-and-forget).
26
- * Spawns a minimal CLI process to capture the system init message,
27
- * then aborts immediately (no API tokens consumed — init is local).
26
+ * Spawns a minimal CLI process with a cheap built-in command (/cost)
27
+ * to trigger the system init message, captures slash_commands,
28
+ * then aborts immediately (no API tokens consumed).
28
29
  *
29
30
  * @param {string} [workDir] - Project directory (default: agent workDir)
30
31
  * @param {string} [targetId] - conversationId to key the update to
@@ -34,9 +35,11 @@ export async function preloadSlashCommands(workDir, targetId = '__preload__') {
34
35
  const effectiveWorkDir = workDir || ctx.CONFIG.workDir;
35
36
  try {
36
37
  const abortController = new AbortController();
37
- const inputStream = new Stream();
38
+ // Use --print with a cheap built-in command to trigger system init.
39
+ // In stream-json mode, Claude CLI won't emit init until a user message arrives,
40
+ // so we use string prompt mode instead which triggers init immediately.
38
41
  const claudeQuery = query({
39
- prompt: inputStream,
42
+ prompt: '/cost',
40
43
  options: {
41
44
  cwd: effectiveWorkDir,
42
45
  permissionMode: 'bypassPermissions',
package/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import 'dotenv/config';
2
2
  import { platform, homedir } from 'os';
3
- import { existsSync, readFileSync, writeFileSync, mkdirSync, cpSync } from 'fs';
3
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, cpSync, chmodSync, readdirSync } from 'fs';
4
4
  import { join, dirname } from 'path';
5
5
  import { exec } from 'child_process';
6
6
  import { promisify } from 'util';
@@ -99,8 +99,6 @@ async function detectCapabilities() {
99
99
  const { getDefaultClaudeCodePath } = await import('./sdk/utils.js');
100
100
  const claudePath = getDefaultClaudeCodePath();
101
101
  if (claudePath) capabilities.push('crew');
102
- // Conductor V2 reuses the same Claude CLI
103
- if (claudePath) capabilities.push('conductor');
104
102
  } catch {}
105
103
 
106
104
  console.log(`[Capabilities] Detected: ${capabilities.join(', ')}`);
@@ -217,10 +215,22 @@ async function ensureYeaftSkills() {
217
215
  writeFileSync(knownFile, JSON.stringify(known, null, 2));
218
216
  }
219
217
 
218
+ // --- Layer 2.5: Ensure hook scripts are executable (git clone may lose +x on some OS) ---
219
+ const fixHookPermissions = (dir) => {
220
+ const hooksDir = join(dir, 'hooks');
221
+ if (existsSync(hooksDir)) {
222
+ for (const f of readdirSync(hooksDir)) {
223
+ try { chmodSync(join(hooksDir, f), 0o755); } catch { /* ignore */ }
224
+ }
225
+ }
226
+ };
227
+ fixHookPermissions(installDir);
228
+
220
229
  // --- Layer 3: Copy to plugin cache (on first install or after update) ---
221
230
  if (!existsSync(cacheDir) || needsCacheUpdate) {
222
231
  mkdirSync(cacheDir, { recursive: true });
223
232
  cpSync(installDir, cacheDir, { recursive: true });
233
+ fixHookPermissions(cacheDir);
224
234
  console.log(`[Startup] yeaft-skills ${needsCacheUpdate ? 'updated in' : 'copied to'} plugin cache`);
225
235
  }
226
236
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.1.247",
3
+ "version": "0.1.249",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -1,478 +0,0 @@
1
- /**
2
- * Conductor — Actor Claude 实例管理
3
- *
4
- * Actor 是最底层的执行单元:
5
- * - 创建 actor session(写 CLAUDE.md → 启动 Claude)
6
- * - 发送任务 → 收集结果 → 回调 Orchestrator
7
- * - 释放 actor(清理 session)
8
- *
9
- * 复用 V1 的 createRoleQuery 模式,适配 Conductor 三层架构
10
- */
11
- import { query, Stream } from '../sdk/index.js';
12
- import { join } from 'path';
13
- import { mkdirSync } from 'fs';
14
- import ctx from '../context.js';
15
- import { generateActorCLAUDEmd, generateActorDirName, generateActorInstanceId } from './claudemd-generator.js';
16
- import { getPersonaById } from './personas/index.js';
17
- import { getSpecialty, getThinkingMode } from './specialties/index.js';
18
-
19
- // =====================================================================
20
- // Actor Instance Registry
21
- // =====================================================================
22
-
23
- /**
24
- * Per-task actor registry: taskId → Map<instanceId, ActorInstance>
25
- * @type {Map<string, Map<string, ActorInstance>>}
26
- */
27
- const actorRegistry = new Map();
28
-
29
- /**
30
- * Per-actor mutex to prevent concurrent createActorQuery calls
31
- * @type {Map<string, Promise>}
32
- */
33
- const _actorLocks = new Map();
34
-
35
- // =====================================================================
36
- // Actor Lifecycle
37
- // =====================================================================
38
-
39
- /**
40
- * 创建 Actor 实例
41
- *
42
- * 流程:生成 CLAUDE.md → 创建目录 → 启动 Claude query(appendSystemPrompt 注入)→ 注册
43
- *
44
- * @param {object} params
45
- * @param {string} params.personaId - persona ID
46
- * @param {string} params.specialtyId - specialty ID
47
- * @param {string} params.taskId - 所属 task ID
48
- * @param {string} params.taskDir - task 目录路径 (.conductor/tasks/task-N)
49
- * @param {object} params.taskContext - 传递给 CLAUDE.md 生成器的上下文
50
- * @param {string} [params.threadId] - 并行线程 ID(coding actor 用)
51
- * @param {string} [params.worktreePath] - worktree 路径
52
- * @param {string} [params.model] - 指定模型
53
- * @param {function} [params.onOutput] - 输出回调 (instanceId, message) => void
54
- * @param {function} [params.onComplete] - 完成回调 (instanceId, result) => void
55
- * @param {function} [params.onError] - 错误回调 (instanceId, error) => void
56
- * @returns {Promise<ActorInstance>}
57
- */
58
- export async function createActor(params) {
59
- const {
60
- personaId,
61
- specialtyId,
62
- taskId,
63
- taskDir,
64
- taskContext = {},
65
- threadId,
66
- worktreePath,
67
- model,
68
- onOutput,
69
- onComplete,
70
- onError
71
- } = params;
72
-
73
- const instanceId = generateActorInstanceId(specialtyId, personaId, taskId, threadId);
74
- const lockKey = instanceId;
75
-
76
- // Mutex: 防止并发创建同一个 actor
77
- if (_actorLocks.has(lockKey)) {
78
- console.log(`[Actor] Waiting for existing lock on ${instanceId}`);
79
- try { await _actorLocks.get(lockKey); } catch { /* 前一次失败了,继续 */ }
80
- const existing = getActor(taskId, instanceId);
81
- if (existing && existing.status === 'working') {
82
- console.log(`[Actor] Reusing existing actor ${instanceId}`);
83
- return existing;
84
- }
85
- }
86
-
87
- const promise = _createActorInner(params, instanceId);
88
- _actorLocks.set(lockKey, promise);
89
-
90
- try {
91
- return await promise;
92
- } finally {
93
- if (_actorLocks.get(lockKey) === promise) {
94
- _actorLocks.delete(lockKey);
95
- }
96
- }
97
- }
98
-
99
- /**
100
- * createActor 内部实现
101
- */
102
- async function _createActorInner(params, instanceId) {
103
- const {
104
- personaId,
105
- specialtyId,
106
- taskId,
107
- taskDir,
108
- taskContext = {},
109
- threadId,
110
- worktreePath,
111
- model,
112
- onOutput,
113
- onComplete,
114
- onError
115
- } = params;
116
-
117
- const persona = getPersonaById(personaId);
118
- if (!persona) throw new Error(`Persona not found: ${personaId}`);
119
-
120
- const specialty = getSpecialty(specialtyId);
121
- if (!specialty) throw new Error(`Specialty not found: ${specialtyId}`);
122
-
123
- // 1. 生成 CLAUDE.md
124
- const claudemdContent = generateActorCLAUDEmd(persona, specialtyId, {
125
- ...taskContext,
126
- worktreePath
127
- });
128
-
129
- // 2. 创建 actor 目录
130
- const actorDirName = generateActorDirName(specialtyId, personaId, threadId);
131
- const actorDir = join(taskDir, 'actors', actorDirName);
132
- mkdirSync(actorDir, { recursive: true });
133
-
134
- // 3. 确定 cwd(coding actor 用 worktree,其他用 actor 目录)
135
- // CLAUDE.md 通过 appendSystemPrompt 注入 SDK,不写入文件(避免双重注入)
136
- const actorCwd = worktreePath || actorDir;
137
- mkdirSync(actorCwd, { recursive: true });
138
-
139
- // 5. 创建 Claude query
140
- const inputStream = new Stream();
141
- const abortController = new AbortController();
142
-
143
- // 继承全局 disallowedTools
144
- const globalDisallowed = ctx.CONFIG?.disallowedTools || [];
145
- const actorDisallowed = ['Agent']; // Actor 不允许使用 Agent 工具
146
- const effectiveDisallowed = [...globalDisallowed, ...actorDisallowed];
147
-
148
- const queryOptions = {
149
- cwd: actorCwd,
150
- permissionMode: 'bypassPermissions',
151
- abort: abortController.signal,
152
- model: model || undefined,
153
- appendSystemPrompt: claudemdContent,
154
- ...(effectiveDisallowed.length > 0 && { disallowedTools: effectiveDisallowed })
155
- };
156
-
157
- const roleQuery = query({
158
- prompt: inputStream,
159
- options: queryOptions
160
- });
161
-
162
- // 6. 创建 ActorInstance
163
- const thinkingMode = getThinkingMode(specialtyId);
164
-
165
- /** @type {ActorInstance} */
166
- const instance = {
167
- instanceId,
168
- personaId,
169
- personaName: persona.name,
170
- specialtyId,
171
- specialtyDisplayName: specialty.displayName,
172
- thinkingMode,
173
- taskId,
174
- threadId: threadId || null,
175
- worktreePath: worktreePath || null,
176
- actorDir,
177
- claudeSessionId: null,
178
- status: 'working',
179
- activity: `${persona.name} 正在执行 ${specialty.displayName}`,
180
- startedAt: Date.now(),
181
- // 内部管理
182
- _query: roleQuery,
183
- _inputStream: inputStream,
184
- _abortController: abortController,
185
- _accumulatedText: '',
186
- _onOutput: onOutput || null,
187
- _onComplete: onComplete || null,
188
- _onError: onError || null
189
- };
190
-
191
- // 7. 注册到 registry
192
- if (!actorRegistry.has(taskId)) {
193
- actorRegistry.set(taskId, new Map());
194
- }
195
- actorRegistry.get(taskId).set(instanceId, instance);
196
-
197
- // 8. 异步处理 Actor 输出
198
- processActorOutput(instance);
199
-
200
- console.log(`[Actor] Created ${instanceId}: ${persona.name} × ${specialtyId} (${thinkingMode})`);
201
- return instance;
202
- }
203
-
204
- // =====================================================================
205
- // Actor Communication
206
- // =====================================================================
207
-
208
- /**
209
- * 向 Actor 发送任务消息
210
- *
211
- * @param {string} taskId - task ID
212
- * @param {string} instanceId - actor instance ID
213
- * @param {string} message - 要发送的消息
214
- */
215
- export function sendToActor(taskId, instanceId, message) {
216
- const instance = getActor(taskId, instanceId);
217
- if (!instance) {
218
- console.error(`[Actor] Cannot send to unknown actor: ${instanceId}`);
219
- return;
220
- }
221
-
222
- if (instance._inputStream.isDone) {
223
- console.error(`[Actor] Cannot send to completed actor: ${instanceId}`);
224
- return;
225
- }
226
-
227
- console.log(`[Actor] Sending message to ${instanceId} (${message.length} chars)`);
228
- instance._inputStream.enqueue(message);
229
- }
230
-
231
- /**
232
- * 异步处理 Actor 输出流
233
- */
234
- async function processActorOutput(instance) {
235
- const { instanceId, _query: roleQuery } = instance;
236
-
237
- try {
238
- for await (const message of roleQuery) {
239
- // 提取 sessionId
240
- if (message.sessionId && !instance.claudeSessionId) {
241
- instance.claudeSessionId = message.sessionId;
242
- }
243
-
244
- // 处理助手消息
245
- if (message.type === 'assistant') {
246
- const text = extractText(message);
247
- if (text) {
248
- instance._accumulatedText += text;
249
-
250
- // 实时输出回调
251
- if (instance._onOutput) {
252
- try {
253
- instance._onOutput(instanceId, message);
254
- } catch (e) {
255
- console.error(`[Actor] onOutput callback error for ${instanceId}:`, e.message);
256
- }
257
- }
258
- }
259
- }
260
-
261
- // turn 结束的标志
262
- if (message.type === 'result') {
263
- instance.status = 'done';
264
- instance.activity = '已完成';
265
-
266
- const result = {
267
- instanceId,
268
- personaId: instance.personaId,
269
- personaName: instance.personaName,
270
- specialtyId: instance.specialtyId,
271
- thinkingMode: instance.thinkingMode,
272
- text: instance._accumulatedText,
273
- costUsd: message.costUSD || 0,
274
- inputTokens: message.inputTokens || 0,
275
- outputTokens: message.outputTokens || 0,
276
- duration: Date.now() - instance.startedAt
277
- };
278
-
279
- console.log(`[Actor] ${instanceId} completed (${result.duration}ms, $${result.costUsd.toFixed(4)})`);
280
-
281
- if (instance._onComplete) {
282
- try {
283
- instance._onComplete(instanceId, result);
284
- } catch (e) {
285
- console.error(`[Actor] onComplete callback error for ${instanceId}:`, e.message);
286
- }
287
- }
288
- }
289
- }
290
- } catch (error) {
291
- instance.status = 'failed';
292
- instance.activity = `失败: ${error.message}`;
293
- console.error(`[Actor] ${instanceId} error:`, error.message);
294
-
295
- if (instance._onError) {
296
- try {
297
- instance._onError(instanceId, error);
298
- } catch (e) {
299
- console.error(`[Actor] onError callback error for ${instanceId}:`, e.message);
300
- }
301
- }
302
- }
303
- }
304
-
305
- /**
306
- * 从消息中提取文本内容
307
- */
308
- function extractText(message) {
309
- if (!message.message?.content) return '';
310
- const content = message.message.content;
311
- if (typeof content === 'string') return content;
312
- if (Array.isArray(content)) {
313
- return content
314
- .filter(c => c.type === 'text')
315
- .map(c => c.text)
316
- .join('');
317
- }
318
- return '';
319
- }
320
-
321
- // =====================================================================
322
- // Actor Release & Cleanup
323
- // =====================================================================
324
-
325
- /**
326
- * 释放 Actor 实例(清理 session)
327
- *
328
- * @param {string} taskId
329
- * @param {string} instanceId
330
- * @param {object} [options]
331
- * @param {boolean} [options.force=false] - 强制终止(abort)
332
- */
333
- export async function releaseActor(taskId, instanceId, options = {}) {
334
- const instance = getActor(taskId, instanceId);
335
- if (!instance) {
336
- console.warn(`[Actor] Cannot release unknown actor: ${instanceId}`);
337
- return;
338
- }
339
-
340
- const { force = false } = options;
341
-
342
- // 关闭 input stream
343
- if (!instance._inputStream.isDone) {
344
- instance._inputStream.done();
345
- }
346
-
347
- // 强制终止
348
- if (force && instance._abortController) {
349
- instance._abortController.abort();
350
- }
351
-
352
- instance.status = 'done';
353
- instance.activity = '已释放';
354
-
355
- // 从 registry 移除,防止内存泄漏
356
- const taskActors = actorRegistry.get(taskId);
357
- if (taskActors) {
358
- taskActors.delete(instanceId);
359
- if (taskActors.size === 0) actorRegistry.delete(taskId);
360
- }
361
-
362
- console.log(`[Actor] Released ${instanceId}${force ? ' (forced)' : ''}`);
363
- }
364
-
365
- /**
366
- * 释放指定 task 的全部 actor
367
- *
368
- * @param {string} taskId
369
- * @param {object} [options]
370
- * @param {boolean} [options.force=false]
371
- */
372
- export async function releaseAllActors(taskId, options = {}) {
373
- const taskActors = actorRegistry.get(taskId);
374
- if (!taskActors) return;
375
-
376
- const promises = [];
377
- for (const [instanceId] of taskActors) {
378
- promises.push(releaseActor(taskId, instanceId, options));
379
- }
380
- await Promise.allSettled(promises);
381
-
382
- actorRegistry.delete(taskId);
383
- console.log(`[Actor] Released all actors for task ${taskId}`);
384
- }
385
-
386
- // =====================================================================
387
- // Actor Query
388
- // =====================================================================
389
-
390
- /**
391
- * 获取单个 actor 实例
392
- * @param {string} taskId
393
- * @param {string} instanceId
394
- * @returns {ActorInstance|null}
395
- */
396
- export function getActor(taskId, instanceId) {
397
- return actorRegistry.get(taskId)?.get(instanceId) || null;
398
- }
399
-
400
- /**
401
- * 获取指定 task 的全部活跃 actor
402
- * @param {string} taskId
403
- * @returns {ActorInstance[]}
404
- */
405
- export function getActiveActors(taskId) {
406
- const taskActors = actorRegistry.get(taskId);
407
- if (!taskActors) return [];
408
- return Array.from(taskActors.values()).filter(a => a.status === 'working');
409
- }
410
-
411
- /**
412
- * 获取指定 task 的全部 actor(含已完成)
413
- * @param {string} taskId
414
- * @returns {ActorInstance[]}
415
- */
416
- export function getAllActors(taskId) {
417
- const taskActors = actorRegistry.get(taskId);
418
- if (!taskActors) return [];
419
- return Array.from(taskActors.values());
420
- }
421
-
422
- /**
423
- * 获取 actor 实例的公开状态(用于前端展示)
424
- * @param {ActorInstance} instance
425
- * @returns {object}
426
- */
427
- export function getActorPublicState(instance) {
428
- return {
429
- instanceId: instance.instanceId,
430
- personaId: instance.personaId,
431
- personaName: instance.personaName,
432
- specialtyId: instance.specialtyId,
433
- specialtyDisplayName: instance.specialtyDisplayName,
434
- thinkingMode: instance.thinkingMode,
435
- taskId: instance.taskId,
436
- threadId: instance.threadId,
437
- status: instance.status,
438
- activity: instance.activity,
439
- startedAt: instance.startedAt
440
- };
441
- }
442
-
443
- /**
444
- * 获取指定 task 全部 actor 的公开状态列表
445
- * @param {string} taskId
446
- * @returns {object[]}
447
- */
448
- export function getActorStates(taskId) {
449
- return getAllActors(taskId).map(getActorPublicState);
450
- }
451
-
452
- // =====================================================================
453
- // Error Classification (复用 V1 模式)
454
- // =====================================================================
455
-
456
- /**
457
- * 判断 actor 错误是否可恢复
458
- * @param {Error} error
459
- * @returns {{ recoverable: boolean, reason: string }}
460
- */
461
- export function classifyActorError(error) {
462
- const msg = error.message || '';
463
- if (/context.*(window|limit|exceeded)|token.*limit|too.*(long|large)|max.*token/i.test(msg)) {
464
- return { recoverable: true, reason: 'context_exceeded' };
465
- }
466
- if (/rate.?limit|429|overloaded|503|502|timeout|ECONNRESET|ETIMEDOUT/i.test(msg)) {
467
- return { recoverable: true, reason: 'transient_api_error' };
468
- }
469
- if (/exited with code [1-9]/i.test(msg) && msg.length < 100) {
470
- return { recoverable: true, reason: 'process_crashed' };
471
- }
472
- if (/spawn|ENOENT|not found/i.test(msg)) {
473
- return { recoverable: false, reason: 'spawn_failed' };
474
- }
475
- return { recoverable: true, reason: 'unknown' };
476
- }
477
-
478
- export { actorRegistry };