@yeaft/webchat-agent 0.1.63 → 0.1.64
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 +5 -1
- package/conversation.js +45 -1
- package/package.json +1 -1
- package/roleplay.js +188 -3
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
createConversation, resumeConversation, deleteConversation,
|
|
13
13
|
handleRefreshConversation, handleCancelExecution,
|
|
14
14
|
handleUserInput, handleUpdateConversationSettings, handleAskUserAnswer,
|
|
15
|
-
sendConversationList
|
|
15
|
+
sendConversationList, handleCheckCrewContext
|
|
16
16
|
} from '../conversation.js';
|
|
17
17
|
import {
|
|
18
18
|
createCrewSession, handleCrewHumanInput, handleCrewControl,
|
|
@@ -66,6 +66,10 @@ export async function handleMessage(msg) {
|
|
|
66
66
|
await createConversation(msg);
|
|
67
67
|
break;
|
|
68
68
|
|
|
69
|
+
case 'check_crew_context':
|
|
70
|
+
handleCheckCrewContext(msg);
|
|
71
|
+
break;
|
|
72
|
+
|
|
69
73
|
case 'resume_conversation':
|
|
70
74
|
await resumeConversation(msg);
|
|
71
75
|
break;
|
package/conversation.js
CHANGED
|
@@ -2,7 +2,7 @@ import ctx from './context.js';
|
|
|
2
2
|
import { loadSessionHistory } from './history.js';
|
|
3
3
|
import { startClaudeQuery } from './claude.js';
|
|
4
4
|
import { crewSessions, loadCrewIndex } from './crew.js';
|
|
5
|
-
import { rolePlaySessions, saveRolePlayIndex, removeRolePlaySession, loadRolePlayIndex, validateRolePlayConfig, initRolePlayRouteState } from './roleplay.js';
|
|
5
|
+
import { rolePlaySessions, saveRolePlayIndex, removeRolePlaySession, loadRolePlayIndex, validateRolePlayConfig, initRolePlayRouteState, loadCrewContext } from './roleplay.js';
|
|
6
6
|
|
|
7
7
|
// Restore persisted roleplay sessions on module load (agent startup)
|
|
8
8
|
loadRolePlayIndex();
|
|
@@ -140,6 +140,15 @@ export async function createConversation(msg) {
|
|
|
140
140
|
rolePlayConfig = result.config;
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
// Load .crew context if rolePlay and projectDir contains .crew directory
|
|
144
|
+
if (rolePlayConfig) {
|
|
145
|
+
const crewContext = loadCrewContext(effectiveWorkDir);
|
|
146
|
+
if (crewContext) {
|
|
147
|
+
rolePlayConfig.crewContext = crewContext;
|
|
148
|
+
console.log(` RolePlay: loaded .crew context (${crewContext.roles.length} roles, ${crewContext.features.length} features)`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
143
152
|
console.log(`Creating conversation: ${conversationId} in ${effectiveWorkDir} (lazy start)`);
|
|
144
153
|
if (username) console.log(` User: ${username} (${userId})`);
|
|
145
154
|
if (rolePlayConfig) console.log(` RolePlay: teamType=${rolePlayConfig.teamType}, roles=${rolePlayConfig.roles?.length}`);
|
|
@@ -589,3 +598,38 @@ export function handleAskUserAnswer(msg) {
|
|
|
589
598
|
console.log(`[AskUser] No pending question for requestId: ${msg.requestId}`);
|
|
590
599
|
}
|
|
591
600
|
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Handle check_crew_context request — check if a directory has .crew context
|
|
604
|
+
* and return role/context info for RolePlay auto-import.
|
|
605
|
+
*/
|
|
606
|
+
export function handleCheckCrewContext(msg) {
|
|
607
|
+
const { projectDir, requestId } = msg;
|
|
608
|
+
if (!projectDir) {
|
|
609
|
+
ctx.sendToServer({ type: 'crew_context_result', requestId, found: false });
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
const crewContext = loadCrewContext(projectDir);
|
|
613
|
+
if (!crewContext) {
|
|
614
|
+
ctx.sendToServer({ type: 'crew_context_result', requestId, found: false });
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
// Return a safe subset for the frontend (no full claudeMd content, just metadata)
|
|
618
|
+
ctx.sendToServer({
|
|
619
|
+
type: 'crew_context_result',
|
|
620
|
+
requestId,
|
|
621
|
+
found: true,
|
|
622
|
+
roles: crewContext.roles.map(r => ({
|
|
623
|
+
name: r.name,
|
|
624
|
+
displayName: r.displayName,
|
|
625
|
+
icon: r.icon,
|
|
626
|
+
description: r.description,
|
|
627
|
+
roleType: r.roleType,
|
|
628
|
+
isDecisionMaker: r.isDecisionMaker,
|
|
629
|
+
hasClaudeMd: !!(r.claudeMd && r.claudeMd.length > 0),
|
|
630
|
+
})),
|
|
631
|
+
teamType: crewContext.teamType,
|
|
632
|
+
language: crewContext.language,
|
|
633
|
+
featureCount: crewContext.features.length,
|
|
634
|
+
});
|
|
635
|
+
}
|
package/package.json
CHANGED
package/roleplay.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { join } from 'path';
|
|
11
11
|
import { homedir } from 'os';
|
|
12
|
-
import { readFileSync, writeFileSync, existsSync, renameSync } from 'fs';
|
|
12
|
+
import { readFileSync, writeFileSync, existsSync, renameSync, readdirSync } from 'fs';
|
|
13
13
|
import { parseRoutes } from './crew/routing.js';
|
|
14
14
|
|
|
15
15
|
const ROLEPLAY_INDEX_PATH = join(homedir(), '.claude', 'roleplay-sessions.json');
|
|
@@ -70,6 +70,147 @@ export function removeRolePlaySession(conversationId) {
|
|
|
70
70
|
saveRolePlayIndex();
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// .crew context import
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Read a file if it exists, otherwise return null.
|
|
79
|
+
* @param {string} filePath
|
|
80
|
+
* @returns {string|null}
|
|
81
|
+
*/
|
|
82
|
+
function readFileOrNull(filePath) {
|
|
83
|
+
try {
|
|
84
|
+
if (!existsSync(filePath)) return null;
|
|
85
|
+
return readFileSync(filePath, 'utf-8');
|
|
86
|
+
} catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Load .crew context from a project directory.
|
|
93
|
+
* Returns null if .crew/ doesn't exist.
|
|
94
|
+
*
|
|
95
|
+
* @param {string} projectDir - absolute path to project root
|
|
96
|
+
* @returns {{ sharedClaudeMd: string, roles: Array, kanban: string, features: Array, teamType: string, language: string } | null}
|
|
97
|
+
*/
|
|
98
|
+
export function loadCrewContext(projectDir) {
|
|
99
|
+
const crewDir = join(projectDir, '.crew');
|
|
100
|
+
if (!existsSync(crewDir)) return null;
|
|
101
|
+
|
|
102
|
+
// 1. Shared CLAUDE.md
|
|
103
|
+
const sharedClaudeMd = readFileOrNull(join(crewDir, 'CLAUDE.md')) || '';
|
|
104
|
+
|
|
105
|
+
// 2. session.json → roles, teamType, language, features
|
|
106
|
+
let sessionRoles = [];
|
|
107
|
+
let teamType = 'dev';
|
|
108
|
+
let language = 'zh-CN';
|
|
109
|
+
let sessionFeatures = [];
|
|
110
|
+
const sessionPath = join(crewDir, 'session.json');
|
|
111
|
+
const sessionJson = readFileOrNull(sessionPath);
|
|
112
|
+
if (sessionJson) {
|
|
113
|
+
try {
|
|
114
|
+
const session = JSON.parse(sessionJson);
|
|
115
|
+
if (Array.isArray(session.roles)) {
|
|
116
|
+
sessionRoles = session.roles;
|
|
117
|
+
}
|
|
118
|
+
if (session.teamType) teamType = session.teamType;
|
|
119
|
+
if (session.language) language = session.language;
|
|
120
|
+
if (Array.isArray(session.features)) {
|
|
121
|
+
sessionFeatures = session.features;
|
|
122
|
+
}
|
|
123
|
+
} catch {
|
|
124
|
+
// Invalid JSON — ignore
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 3. Per-role CLAUDE.md from .crew/roles/*/CLAUDE.md
|
|
129
|
+
const roleClaudes = {};
|
|
130
|
+
const rolesDir = join(crewDir, 'roles');
|
|
131
|
+
if (existsSync(rolesDir)) {
|
|
132
|
+
try {
|
|
133
|
+
const roleDirs = readdirSync(rolesDir, { withFileTypes: true })
|
|
134
|
+
.filter(d => d.isDirectory())
|
|
135
|
+
.map(d => d.name);
|
|
136
|
+
for (const dirName of roleDirs) {
|
|
137
|
+
const md = readFileOrNull(join(rolesDir, dirName, 'CLAUDE.md'));
|
|
138
|
+
if (md) roleClaudes[dirName] = md;
|
|
139
|
+
}
|
|
140
|
+
} catch {
|
|
141
|
+
// Permission error or similar — ignore
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// 4. Merge roles: deduplicate by roleType, attach claudeMd
|
|
146
|
+
const roles = deduplicateRoles(sessionRoles, roleClaudes);
|
|
147
|
+
|
|
148
|
+
// 5. Kanban
|
|
149
|
+
const kanban = readFileOrNull(join(crewDir, 'context', 'kanban.md')) || '';
|
|
150
|
+
|
|
151
|
+
// 6. Feature files from context/features/*.md
|
|
152
|
+
const features = [];
|
|
153
|
+
const featuresDir = join(crewDir, 'context', 'features');
|
|
154
|
+
if (existsSync(featuresDir)) {
|
|
155
|
+
try {
|
|
156
|
+
const files = readdirSync(featuresDir)
|
|
157
|
+
.filter(f => f.endsWith('.md') && f !== 'index.md')
|
|
158
|
+
.sort();
|
|
159
|
+
for (const f of files) {
|
|
160
|
+
const content = readFileOrNull(join(featuresDir, f));
|
|
161
|
+
if (content) {
|
|
162
|
+
features.push({ name: f.replace('.md', ''), content });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
} catch {
|
|
166
|
+
// ignore
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return { sharedClaudeMd, roles, kanban, features, teamType, language, sessionFeatures };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Deduplicate Crew roles by roleType.
|
|
175
|
+
* Crew may have dev-1, dev-2, dev-3 — collapse to a single "dev" for RolePlay.
|
|
176
|
+
* Attaches per-role CLAUDE.md content.
|
|
177
|
+
*/
|
|
178
|
+
function deduplicateRoles(sessionRoles, roleClaudes) {
|
|
179
|
+
const byType = new Map(); // roleType -> first role seen
|
|
180
|
+
const merged = [];
|
|
181
|
+
|
|
182
|
+
for (const r of sessionRoles) {
|
|
183
|
+
const type = r.roleType || r.name;
|
|
184
|
+
const claudeMd = roleClaudes[r.name] || '';
|
|
185
|
+
|
|
186
|
+
if (byType.has(type)) {
|
|
187
|
+
// Already have this roleType — skip duplicate instance
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
byType.set(type, true);
|
|
192
|
+
|
|
193
|
+
// Use roleType as the RolePlay name (e.g. "developer" instead of "dev-1")
|
|
194
|
+
// But keep "pm" and "designer" as-is since they're typically single-instance
|
|
195
|
+
const name = type;
|
|
196
|
+
// Strip instance suffix from displayName (e.g. "开发者-托瓦兹-1" → "开发者-托瓦兹")
|
|
197
|
+
let displayName = r.displayName || name;
|
|
198
|
+
displayName = displayName.replace(/-\d+$/, '');
|
|
199
|
+
|
|
200
|
+
merged.push({
|
|
201
|
+
name,
|
|
202
|
+
displayName,
|
|
203
|
+
icon: r.icon || '',
|
|
204
|
+
description: r.description || '',
|
|
205
|
+
claudeMd: claudeMd.substring(0, MAX_CLAUDE_MD_LEN),
|
|
206
|
+
roleType: type,
|
|
207
|
+
isDecisionMaker: !!r.isDecisionMaker,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return merged;
|
|
212
|
+
}
|
|
213
|
+
|
|
73
214
|
// ---------------------------------------------------------------------------
|
|
74
215
|
// Input validation
|
|
75
216
|
// ---------------------------------------------------------------------------
|
|
@@ -165,11 +306,11 @@ export function validateRolePlayConfig(config) {
|
|
|
165
306
|
* Build the appendSystemPrompt that tells Claude about the role play roles
|
|
166
307
|
* and how to switch between them.
|
|
167
308
|
*
|
|
168
|
-
* @param {{ roles: Array, teamType: string, language: string }} config
|
|
309
|
+
* @param {{ roles: Array, teamType: string, language: string, crewContext?: object }} config
|
|
169
310
|
* @returns {string}
|
|
170
311
|
*/
|
|
171
312
|
export function buildRolePlaySystemPrompt(config) {
|
|
172
|
-
const { roles, teamType, language } = config;
|
|
313
|
+
const { roles, teamType, language, crewContext } = config;
|
|
173
314
|
const isZh = language === 'zh-CN';
|
|
174
315
|
|
|
175
316
|
// Build role list
|
|
@@ -186,6 +327,14 @@ export function buildRolePlaySystemPrompt(config) {
|
|
|
186
327
|
? buildZhPrompt(roleList, workflow)
|
|
187
328
|
: buildEnPrompt(roleList, workflow);
|
|
188
329
|
|
|
330
|
+
// Append .crew context if available
|
|
331
|
+
if (crewContext) {
|
|
332
|
+
const contextBlock = buildCrewContextBlock(crewContext, isZh);
|
|
333
|
+
if (contextBlock) {
|
|
334
|
+
return (prompt + '\n\n' + contextBlock).trim();
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
189
338
|
return prompt.trim();
|
|
190
339
|
}
|
|
191
340
|
|
|
@@ -531,3 +680,39 @@ export function getRolePlayRouteState(conversationId) {
|
|
|
531
680
|
waitingHumanContext: session.waitingHumanContext || null
|
|
532
681
|
};
|
|
533
682
|
}
|
|
683
|
+
|
|
684
|
+
// ---------------------------------------------------------------------------
|
|
685
|
+
// .crew context block for system prompt
|
|
686
|
+
// ---------------------------------------------------------------------------
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* Build the .crew context block to append to the system prompt.
|
|
690
|
+
* Includes shared instructions, kanban, and feature history.
|
|
691
|
+
*/
|
|
692
|
+
function buildCrewContextBlock(crewContext, isZh) {
|
|
693
|
+
const sections = [];
|
|
694
|
+
|
|
695
|
+
if (crewContext.sharedClaudeMd) {
|
|
696
|
+
const header = isZh ? '## 项目共享指令(来自 .crew)' : '## Shared Project Instructions (from .crew)';
|
|
697
|
+
sections.push(`${header}\n\n${crewContext.sharedClaudeMd}`);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
if (crewContext.kanban) {
|
|
701
|
+
const header = isZh ? '## 当前任务看板' : '## Current Task Board';
|
|
702
|
+
sections.push(`${header}\n\n${crewContext.kanban}`);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
if (crewContext.features && crewContext.features.length > 0) {
|
|
706
|
+
const header = isZh ? '## 历史工作记录' : '## Work History';
|
|
707
|
+
// Only include the last few features to avoid blowing up context
|
|
708
|
+
const recentFeatures = crewContext.features.slice(-5);
|
|
709
|
+
const featureTexts = recentFeatures.map(f => {
|
|
710
|
+
// Truncate each feature to keep total size reasonable
|
|
711
|
+
const content = f.content.length > 2000 ? f.content.substring(0, 2000) + '\n...(truncated)' : f.content;
|
|
712
|
+
return `### ${f.name}\n${content}`;
|
|
713
|
+
}).join('\n\n');
|
|
714
|
+
sections.push(`${header}\n\n${featureTexts}`);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
return sections.join('\n\n');
|
|
718
|
+
}
|