@yeaft/webchat-agent 0.1.78 → 0.1.80
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/conversation.js +80 -5
- package/crew/session.js +4 -1
- package/crew/shared-dir.js +101 -3
- package/crew-i18n.js +20 -0
- package/package.json +1 -1
- package/roleplay-dir.js +301 -0
- package/roleplay-i18n.js +445 -0
- package/roleplay-session.js +184 -0
package/conversation.js
CHANGED
|
@@ -3,6 +3,8 @@ import { loadSessionHistory } from './history.js';
|
|
|
3
3
|
import { startClaudeQuery } from './claude.js';
|
|
4
4
|
import { crewSessions, loadCrewIndex } from './crew.js';
|
|
5
5
|
import { rolePlaySessions, saveRolePlayIndex, removeRolePlaySession, loadRolePlayIndex, validateRolePlayConfig, initRolePlayRouteState, loadCrewContext, refreshCrewContext, initCrewContextMtimes } from './roleplay.js';
|
|
6
|
+
import { initRolePlayDir, writeSessionClaudeMd, generateSessionName, getDefaultRoles, getSessionDir } from './roleplay-dir.js';
|
|
7
|
+
import { addRolePlaySession, findRolePlaySessionByConversationId, setActiveRolePlaySession } from './roleplay-session.js';
|
|
6
8
|
|
|
7
9
|
// Restore persisted roleplay sessions on module load (agent startup)
|
|
8
10
|
loadRolePlayIndex();
|
|
@@ -149,16 +151,71 @@ export async function createConversation(msg) {
|
|
|
149
151
|
}
|
|
150
152
|
}
|
|
151
153
|
|
|
152
|
-
|
|
154
|
+
// ★ RolePlay: initialize .roleplay/ directory, generate session, set cwd
|
|
155
|
+
let rpSessionName = null;
|
|
156
|
+
let rpSessionWorkDir = effectiveWorkDir; // default: project root
|
|
157
|
+
if (rolePlayConfig) {
|
|
158
|
+
try {
|
|
159
|
+
const language = rolePlayConfig.language || 'zh-CN';
|
|
160
|
+
|
|
161
|
+
// 1. Ensure .roleplay/ directory structure exists
|
|
162
|
+
await initRolePlayDir(effectiveWorkDir, language);
|
|
163
|
+
|
|
164
|
+
// 2. Generate unique session name
|
|
165
|
+
rpSessionName = generateSessionName(
|
|
166
|
+
effectiveWorkDir,
|
|
167
|
+
rolePlayConfig.teamType,
|
|
168
|
+
msg.rolePlayConfig?.sessionName || null
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
// 3. Write session CLAUDE.md (role list, ROUTE protocol, workflow)
|
|
172
|
+
await writeSessionClaudeMd(effectiveWorkDir, rpSessionName, {
|
|
173
|
+
teamType: rolePlayConfig.teamType,
|
|
174
|
+
language,
|
|
175
|
+
roles: rolePlayConfig.roles,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// 4. Set cwd to session directory so Claude Code auto-reads its CLAUDE.md
|
|
179
|
+
rpSessionWorkDir = getSessionDir(effectiveWorkDir, rpSessionName);
|
|
180
|
+
|
|
181
|
+
// 5. Build roles snapshot for session.json
|
|
182
|
+
const rolesSnapshot = (rolePlayConfig.roles && rolePlayConfig.roles.length > 0)
|
|
183
|
+
? rolePlayConfig.roles.map(r => ({
|
|
184
|
+
name: r.name,
|
|
185
|
+
displayName: r.displayName || r.name,
|
|
186
|
+
icon: r.icon || '',
|
|
187
|
+
}))
|
|
188
|
+
: getDefaultRoles(rolePlayConfig.teamType, language);
|
|
189
|
+
|
|
190
|
+
// 6. Persist to .roleplay/session.json
|
|
191
|
+
await addRolePlaySession(effectiveWorkDir, {
|
|
192
|
+
name: rpSessionName,
|
|
193
|
+
teamType: rolePlayConfig.teamType,
|
|
194
|
+
language,
|
|
195
|
+
projectDir: effectiveWorkDir,
|
|
196
|
+
conversationId,
|
|
197
|
+
roles: rolesSnapshot,
|
|
198
|
+
createdAt: Date.now(),
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
console.log(` RolePlay: initialized .roleplay/${rpSessionName}, cwd=${rpSessionWorkDir}`);
|
|
202
|
+
} catch (e) {
|
|
203
|
+
console.error('[createConversation] Failed to init .roleplay/ dir:', e);
|
|
204
|
+
// Non-fatal: fall back to project root as cwd
|
|
205
|
+
rpSessionWorkDir = effectiveWorkDir;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
console.log(`Creating conversation: ${conversationId} in ${rpSessionWorkDir} (lazy start)`);
|
|
153
210
|
if (username) console.log(` User: ${username} (${userId})`);
|
|
154
|
-
if (rolePlayConfig) console.log(` RolePlay: teamType=${rolePlayConfig.teamType}, roles=${rolePlayConfig.roles?.length}`);
|
|
211
|
+
if (rolePlayConfig) console.log(` RolePlay: teamType=${rolePlayConfig.teamType}, roles=${rolePlayConfig.roles?.length}, session=${rpSessionName}`);
|
|
155
212
|
|
|
156
213
|
// 只创建 conversation 状态,不启动 Claude 进程
|
|
157
214
|
// Claude 进程会在用户发送第一条消息时启动 (见 handleUserInput)
|
|
158
215
|
ctx.conversations.set(conversationId, {
|
|
159
216
|
query: null,
|
|
160
217
|
inputStream: null,
|
|
161
|
-
workDir:
|
|
218
|
+
workDir: rpSessionWorkDir,
|
|
162
219
|
claudeSessionId: null,
|
|
163
220
|
createdAt: Date.now(),
|
|
164
221
|
abortController: null,
|
|
@@ -169,6 +226,9 @@ export async function createConversation(msg) {
|
|
|
169
226
|
username,
|
|
170
227
|
disallowedTools: disallowedTools || null, // null = 使用全局默认
|
|
171
228
|
rolePlayConfig: rolePlayConfig || null,
|
|
229
|
+
// Track the original project dir and session name for .roleplay/ operations
|
|
230
|
+
_rpProjectDir: rolePlayConfig ? effectiveWorkDir : null,
|
|
231
|
+
_rpSessionName: rpSessionName,
|
|
172
232
|
usage: {
|
|
173
233
|
inputTokens: 0,
|
|
174
234
|
outputTokens: 0,
|
|
@@ -198,7 +258,7 @@ export async function createConversation(msg) {
|
|
|
198
258
|
ctx.sendToServer({
|
|
199
259
|
type: 'conversation_created',
|
|
200
260
|
conversationId,
|
|
201
|
-
workDir:
|
|
261
|
+
workDir: rpSessionWorkDir,
|
|
202
262
|
userId,
|
|
203
263
|
username,
|
|
204
264
|
disallowedTools: disallowedTools || null,
|
|
@@ -260,6 +320,21 @@ export async function resumeConversation(msg) {
|
|
|
260
320
|
? { roles: rolePlayEntry.roles, teamType: rolePlayEntry.teamType, language: rolePlayEntry.language }
|
|
261
321
|
: null;
|
|
262
322
|
|
|
323
|
+
// ★ RolePlay resume: look up session in .roleplay/session.json to restore cwd
|
|
324
|
+
let rpResumeWorkDir = effectiveWorkDir;
|
|
325
|
+
if (rolePlayConfig && rolePlayEntry) {
|
|
326
|
+
const rpProjectDir = rolePlayEntry.projectDir || effectiveWorkDir;
|
|
327
|
+
const rpDiskSession = findRolePlaySessionByConversationId(rpProjectDir, conversationId);
|
|
328
|
+
if (rpDiskSession && rpDiskSession.name) {
|
|
329
|
+
rpResumeWorkDir = getSessionDir(rpProjectDir, rpDiskSession.name);
|
|
330
|
+
// Re-activate the session
|
|
331
|
+
setActiveRolePlaySession(rpProjectDir, rpDiskSession.name).catch(e => {
|
|
332
|
+
console.warn('[Resume] Failed to update activeSession:', e.message);
|
|
333
|
+
});
|
|
334
|
+
console.log(`[Resume] RolePlay: restored session cwd=${rpResumeWorkDir}`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
263
338
|
// ★ RolePlay resume: refresh .crew context to get latest kanban/features
|
|
264
339
|
if (rolePlayConfig && rolePlayEntry) {
|
|
265
340
|
const crewContext = loadCrewContext(effectiveWorkDir);
|
|
@@ -274,7 +349,7 @@ export async function resumeConversation(msg) {
|
|
|
274
349
|
ctx.conversations.set(conversationId, {
|
|
275
350
|
query: null,
|
|
276
351
|
inputStream: null,
|
|
277
|
-
workDir:
|
|
352
|
+
workDir: rpResumeWorkDir,
|
|
278
353
|
claudeSessionId: claudeSessionId, // 保存要恢复的 session ID
|
|
279
354
|
createdAt: Date.now(),
|
|
280
355
|
abortController: null,
|
package/crew/session.js
CHANGED
|
@@ -6,7 +6,7 @@ import { join, isAbsolute } from 'path';
|
|
|
6
6
|
import ctx from '../context.js';
|
|
7
7
|
import { getMessages } from '../crew-i18n.js';
|
|
8
8
|
import { initWorktrees } from './worktree.js';
|
|
9
|
-
import { initSharedDir, writeRoleClaudeMd, updateSharedClaudeMd } from './shared-dir.js';
|
|
9
|
+
import { initSharedDir, writeRoleClaudeMd, updateSharedClaudeMd, backupMemoryContent } from './shared-dir.js';
|
|
10
10
|
import {
|
|
11
11
|
loadCrewIndex, upsertCrewIndex, removeFromCrewIndex,
|
|
12
12
|
loadSessionMeta, saveSessionMeta, loadSessionMessages, getMaxShardIndex
|
|
@@ -366,6 +366,9 @@ export async function handleDeleteCrewDir(msg) {
|
|
|
366
366
|
if (!isValidProjectDir(projectDir)) return;
|
|
367
367
|
const crewDir = join(projectDir, '.crew');
|
|
368
368
|
try {
|
|
369
|
+
// 提取并备份记忆内容(删除前)
|
|
370
|
+
await backupMemoryContent(crewDir);
|
|
371
|
+
|
|
369
372
|
// 删除 Crew 模板定义
|
|
370
373
|
await fs.rm(join(crewDir, 'CLAUDE.md'), { force: true }).catch(() => {});
|
|
371
374
|
await fs.rm(join(crewDir, 'roles'), { recursive: true, force: true }).catch(() => {});
|
package/crew/shared-dir.js
CHANGED
|
@@ -4,13 +4,98 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { promises as fs } from 'fs';
|
|
6
6
|
import { join } from 'path';
|
|
7
|
-
import { getMessages } from '../crew-i18n.js';
|
|
7
|
+
import { getMessages, getAllMemoryTitles } from '../crew-i18n.js';
|
|
8
8
|
|
|
9
9
|
/** Format role label: "icon displayName" or just "displayName" if no icon */
|
|
10
10
|
function roleLabel(r) {
|
|
11
11
|
return r.icon ? `${r.icon} ${r.displayName}` : r.displayName;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
const MEMORY_BACKUP_FILE = '.memory-backup.json';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Extract user-written content after a memory section title.
|
|
18
|
+
* Searches for any known locale's title (e.g. "# 共享记忆" or "# Shared Memory"),
|
|
19
|
+
* returns the trimmed content after the title line until EOF or next top-level heading.
|
|
20
|
+
* Returns null if section not found or content is only the default placeholder.
|
|
21
|
+
*/
|
|
22
|
+
function extractMemorySection(fileContent, titles, defaults) {
|
|
23
|
+
for (const title of titles) {
|
|
24
|
+
const idx = fileContent.indexOf(title);
|
|
25
|
+
if (idx === -1) continue;
|
|
26
|
+
// Content starts after the title line
|
|
27
|
+
const afterTitle = fileContent.slice(idx + title.length);
|
|
28
|
+
// Find next top-level heading (# at start of line) — that's where memory ends
|
|
29
|
+
const nextHeading = afterTitle.search(/\n#\s/);
|
|
30
|
+
const raw = nextHeading === -1 ? afterTitle : afterTitle.slice(0, nextHeading);
|
|
31
|
+
const trimmed = raw.trim();
|
|
32
|
+
// Skip if empty or is just the default placeholder
|
|
33
|
+
if (!trimmed) return null;
|
|
34
|
+
for (const d of defaults) {
|
|
35
|
+
if (trimmed === d.trim()) return null;
|
|
36
|
+
}
|
|
37
|
+
return trimmed;
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Backup memory content from .crew/CLAUDE.md and .crew/roles/*/CLAUDE.md
|
|
44
|
+
* before deletion. Writes .crew/.memory-backup.json.
|
|
45
|
+
*/
|
|
46
|
+
export async function backupMemoryContent(crewDir) {
|
|
47
|
+
const { sharedTitles, sharedDefaults, personalTitles, personalDefaults } = getAllMemoryTitles();
|
|
48
|
+
const backup = { shared: null, roles: {} };
|
|
49
|
+
|
|
50
|
+
// Extract shared memory from .crew/CLAUDE.md
|
|
51
|
+
try {
|
|
52
|
+
const sharedContent = await fs.readFile(join(crewDir, 'CLAUDE.md'), 'utf-8');
|
|
53
|
+
backup.shared = extractMemorySection(sharedContent, sharedTitles, sharedDefaults);
|
|
54
|
+
} catch { /* CLAUDE.md doesn't exist — skip */ }
|
|
55
|
+
|
|
56
|
+
// Extract personal memory from each role's CLAUDE.md
|
|
57
|
+
try {
|
|
58
|
+
const rolesDir = join(crewDir, 'roles');
|
|
59
|
+
const roleDirs = await fs.readdir(rolesDir);
|
|
60
|
+
for (const roleName of roleDirs) {
|
|
61
|
+
try {
|
|
62
|
+
const roleClaudeMd = await fs.readFile(join(rolesDir, roleName, 'CLAUDE.md'), 'utf-8');
|
|
63
|
+
const memory = extractMemorySection(roleClaudeMd, personalTitles, personalDefaults);
|
|
64
|
+
if (memory) {
|
|
65
|
+
backup.roles[roleName] = memory;
|
|
66
|
+
}
|
|
67
|
+
} catch { /* Role dir or file missing — skip */ }
|
|
68
|
+
}
|
|
69
|
+
} catch { /* roles/ doesn't exist — skip */ }
|
|
70
|
+
|
|
71
|
+
// Only write backup if there's something to preserve
|
|
72
|
+
if (backup.shared || Object.keys(backup.roles).length > 0) {
|
|
73
|
+
await fs.writeFile(join(crewDir, MEMORY_BACKUP_FILE), JSON.stringify(backup, null, 2));
|
|
74
|
+
console.log(`[Crew] Memory backup saved: shared=${!!backup.shared}, roles=${Object.keys(backup.roles).join(',') || 'none'}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Load memory backup from .crew/.memory-backup.json, returns null if not found.
|
|
80
|
+
*/
|
|
81
|
+
async function loadMemoryBackup(sharedDir) {
|
|
82
|
+
try {
|
|
83
|
+
const data = await fs.readFile(join(sharedDir, MEMORY_BACKUP_FILE), 'utf-8');
|
|
84
|
+
return JSON.parse(data);
|
|
85
|
+
} catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Delete memory backup file after successful restore.
|
|
92
|
+
*/
|
|
93
|
+
async function cleanupMemoryBackup(sharedDir) {
|
|
94
|
+
try {
|
|
95
|
+
await fs.rm(join(sharedDir, MEMORY_BACKUP_FILE), { force: true });
|
|
96
|
+
} catch { /* ignore */ }
|
|
97
|
+
}
|
|
98
|
+
|
|
14
99
|
/**
|
|
15
100
|
* 初始化共享目录
|
|
16
101
|
*/
|
|
@@ -27,6 +112,9 @@ export async function initSharedDir(sharedDir, roles, projectDir, language = 'zh
|
|
|
27
112
|
|
|
28
113
|
// 生成 .crew/CLAUDE.md(共享级)
|
|
29
114
|
await writeSharedClaudeMd(sharedDir, roles, projectDir, language);
|
|
115
|
+
|
|
116
|
+
// 清理记忆备份文件(已在 write 阶段恢复)
|
|
117
|
+
await cleanupMemoryBackup(sharedDir);
|
|
30
118
|
}
|
|
31
119
|
|
|
32
120
|
/**
|
|
@@ -52,6 +140,10 @@ export async function initRoleDir(sharedDir, role, language = 'zh-CN', allRoles
|
|
|
52
140
|
export async function writeSharedClaudeMd(sharedDir, roles, projectDir, language = 'zh-CN') {
|
|
53
141
|
const m = getMessages(language);
|
|
54
142
|
|
|
143
|
+
// Check for memory backup to restore
|
|
144
|
+
const backup = await loadMemoryBackup(sharedDir);
|
|
145
|
+
const sharedMemoryContent = (backup && backup.shared) ? backup.shared : m.sharedMemoryDefault;
|
|
146
|
+
|
|
55
147
|
const claudeMd = `${m.projectGoal}
|
|
56
148
|
|
|
57
149
|
${m.projectCodePath}
|
|
@@ -73,7 +165,7 @@ ${m.worktreeRulesContent}
|
|
|
73
165
|
${m.featureRecordShared}
|
|
74
166
|
|
|
75
167
|
${m.sharedMemoryTitle}
|
|
76
|
-
${
|
|
168
|
+
${sharedMemoryContent}
|
|
77
169
|
`;
|
|
78
170
|
|
|
79
171
|
await fs.writeFile(join(sharedDir, 'CLAUDE.md'), claudeMd);
|
|
@@ -129,6 +221,12 @@ export async function writeRoleClaudeMd(sharedDir, role, language = 'zh-CN', all
|
|
|
129
221
|
const roleDir = join(sharedDir, 'roles', role.name);
|
|
130
222
|
const m = getMessages(language);
|
|
131
223
|
|
|
224
|
+
// Check for memory backup to restore
|
|
225
|
+
const backup = await loadMemoryBackup(sharedDir);
|
|
226
|
+
const personalMemoryContent = (backup && backup.roles && backup.roles[role.name])
|
|
227
|
+
? backup.roles[role.name]
|
|
228
|
+
: m.personalMemoryDefault;
|
|
229
|
+
|
|
132
230
|
// Resolve generic ROUTE targets to actual instance names
|
|
133
231
|
const resolvedClaudeMd = resolveRouteTargets(role.claudeMd || role.description, role, allRoles);
|
|
134
232
|
|
|
@@ -147,7 +245,7 @@ ${m.codeWorkDirNote}
|
|
|
147
245
|
|
|
148
246
|
claudeMd += `
|
|
149
247
|
${m.personalMemory}
|
|
150
|
-
${
|
|
248
|
+
${personalMemoryContent}
|
|
151
249
|
`;
|
|
152
250
|
|
|
153
251
|
await fs.writeFile(join(roleDir, 'CLAUDE.md'), claudeMd);
|
package/crew-i18n.js
CHANGED
|
@@ -498,3 +498,23 @@ Roles don't need to manually create or update these files.`,
|
|
|
498
498
|
export function getMessages(language) {
|
|
499
499
|
return messages[language] || messages['zh-CN'];
|
|
500
500
|
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Get all memory-related titles and defaults across all locales.
|
|
504
|
+
* Used to detect and extract user-written memory content regardless of language.
|
|
505
|
+
* @returns {{ sharedTitles: string[], sharedDefaults: string[], personalTitles: string[], personalDefaults: string[] }}
|
|
506
|
+
*/
|
|
507
|
+
export function getAllMemoryTitles() {
|
|
508
|
+
const sharedTitles = [];
|
|
509
|
+
const sharedDefaults = [];
|
|
510
|
+
const personalTitles = [];
|
|
511
|
+
const personalDefaults = [];
|
|
512
|
+
for (const lang of Object.keys(messages)) {
|
|
513
|
+
const m = messages[lang];
|
|
514
|
+
if (m.sharedMemoryTitle) sharedTitles.push(m.sharedMemoryTitle);
|
|
515
|
+
if (m.sharedMemoryDefault) sharedDefaults.push(m.sharedMemoryDefault);
|
|
516
|
+
if (m.personalMemory) personalTitles.push(m.personalMemory);
|
|
517
|
+
if (m.personalMemoryDefault) personalDefaults.push(m.personalMemoryDefault);
|
|
518
|
+
}
|
|
519
|
+
return { sharedTitles, sharedDefaults, personalTitles, personalDefaults };
|
|
520
|
+
}
|
package/package.json
CHANGED
package/roleplay-dir.js
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RolePlay — .roleplay/ directory and CLAUDE.md management.
|
|
3
|
+
*
|
|
4
|
+
* Analogous to agent/crew/shared-dir.js but for the single-process
|
|
5
|
+
* RolePlay collaboration mode.
|
|
6
|
+
*
|
|
7
|
+
* Directory structure:
|
|
8
|
+
* .roleplay/
|
|
9
|
+
* ├── CLAUDE.md (shared instructions, inherited by all sessions)
|
|
10
|
+
* ├── session.json (session index)
|
|
11
|
+
* ├── roles/ (one subdirectory per session)
|
|
12
|
+
* │ └── {session-name}/
|
|
13
|
+
* │ └── CLAUDE.md (Claude Code cwd points here)
|
|
14
|
+
* └── context/
|
|
15
|
+
* ├── kanban.md
|
|
16
|
+
* └── features/
|
|
17
|
+
*/
|
|
18
|
+
import { promises as fs } from 'fs';
|
|
19
|
+
import { join } from 'path';
|
|
20
|
+
import { existsSync, readdirSync } from 'fs';
|
|
21
|
+
import { getRolePlayMessages } from './roleplay-i18n.js';
|
|
22
|
+
|
|
23
|
+
// ─── Team type → default role set mapping ───────────────────────────
|
|
24
|
+
// Each entry is a list of role names (keys into roleTemplates in i18n).
|
|
25
|
+
const TEAM_ROLES = {
|
|
26
|
+
dev: ['pm', 'dev', 'reviewer', 'tester'],
|
|
27
|
+
writing: ['planner', 'writer', 'editor'],
|
|
28
|
+
trading: ['strategist', 'analyst', 'macro', 'risk', 'trader'],
|
|
29
|
+
video: ['director', 'scriptwriter', 'storyboard'],
|
|
30
|
+
custom: ['pm', 'dev', 'reviewer', 'tester'], // default same as dev
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// ─── Session name constraints ───────────────────────────────────────
|
|
34
|
+
const SESSION_NAME_RE = /^[a-z0-9-]+$/;
|
|
35
|
+
const MAX_SESSION_NAME_LEN = 64;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Generate a session directory name.
|
|
39
|
+
*
|
|
40
|
+
* Format: `{teamType}-{customName|"team"}-{YYYYMMDD}[-{seq}]`
|
|
41
|
+
*
|
|
42
|
+
* If a session with the same name already exists under .roleplay/roles/,
|
|
43
|
+
* a sequence number is appended (e.g. `-2`, `-3`).
|
|
44
|
+
*
|
|
45
|
+
* @param {string} projectDir - absolute path to project root
|
|
46
|
+
* @param {string} teamType - dev | writing | trading | video | custom
|
|
47
|
+
* @param {string} [customName] - user-provided name (optional)
|
|
48
|
+
* @returns {string} unique session name
|
|
49
|
+
*/
|
|
50
|
+
export function generateSessionName(projectDir, teamType, customName) {
|
|
51
|
+
const now = new Date();
|
|
52
|
+
const datePart = [
|
|
53
|
+
now.getFullYear(),
|
|
54
|
+
String(now.getMonth() + 1).padStart(2, '0'),
|
|
55
|
+
String(now.getDate()).padStart(2, '0'),
|
|
56
|
+
].join('');
|
|
57
|
+
|
|
58
|
+
const namePart = sanitizeNamePart(customName) || 'team';
|
|
59
|
+
const base = `${teamType}-${namePart}-${datePart}`;
|
|
60
|
+
|
|
61
|
+
// Check for duplicates under .roleplay/roles/
|
|
62
|
+
const rolesDir = join(projectDir, '.roleplay', 'roles');
|
|
63
|
+
const existing = new Set();
|
|
64
|
+
if (existsSync(rolesDir)) {
|
|
65
|
+
try {
|
|
66
|
+
for (const d of readdirSync(rolesDir, { withFileTypes: true })) {
|
|
67
|
+
if (d.isDirectory()) existing.add(d.name);
|
|
68
|
+
}
|
|
69
|
+
} catch {
|
|
70
|
+
// permission error — treat as empty
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!existing.has(base)) return base;
|
|
75
|
+
|
|
76
|
+
// Append sequence number
|
|
77
|
+
for (let seq = 2; seq <= 999; seq++) {
|
|
78
|
+
const candidate = `${base}-${seq}`;
|
|
79
|
+
if (!existing.has(candidate)) return candidate;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Extremely unlikely: fall back to timestamp suffix
|
|
83
|
+
return `${base}-${Date.now()}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Sanitize user-provided name part:
|
|
88
|
+
* - lowercase
|
|
89
|
+
* - replace non-alphanumeric with hyphens
|
|
90
|
+
* - collapse consecutive hyphens
|
|
91
|
+
* - trim leading/trailing hyphens
|
|
92
|
+
* - enforce max length
|
|
93
|
+
*
|
|
94
|
+
* Returns empty string if input is empty/invalid.
|
|
95
|
+
*/
|
|
96
|
+
function sanitizeNamePart(raw) {
|
|
97
|
+
if (!raw || typeof raw !== 'string') return '';
|
|
98
|
+
let s = raw.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
|
|
99
|
+
if (s.length > 30) s = s.substring(0, 30).replace(/-$/, '');
|
|
100
|
+
return SESSION_NAME_RE.test(s) ? s : '';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ─── Directory initialization ───────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Ensure .roleplay/ directory structure exists.
|
|
107
|
+
* Creates the top-level dirs if absent, writes shared CLAUDE.md
|
|
108
|
+
* only on first creation (preserves user edits after that).
|
|
109
|
+
*
|
|
110
|
+
* @param {string} projectDir - absolute path to project root
|
|
111
|
+
* @param {string} [language='zh-CN']
|
|
112
|
+
*/
|
|
113
|
+
export async function initRolePlayDir(projectDir, language = 'zh-CN') {
|
|
114
|
+
const rpDir = join(projectDir, '.roleplay');
|
|
115
|
+
|
|
116
|
+
await fs.mkdir(rpDir, { recursive: true });
|
|
117
|
+
await fs.mkdir(join(rpDir, 'roles'), { recursive: true });
|
|
118
|
+
await fs.mkdir(join(rpDir, 'context'), { recursive: true });
|
|
119
|
+
await fs.mkdir(join(rpDir, 'context', 'features'), { recursive: true });
|
|
120
|
+
|
|
121
|
+
// Write shared CLAUDE.md only if it doesn't exist
|
|
122
|
+
const sharedMdPath = join(rpDir, 'CLAUDE.md');
|
|
123
|
+
try {
|
|
124
|
+
await fs.access(sharedMdPath);
|
|
125
|
+
// Already exists — don't overwrite (user may have edited it)
|
|
126
|
+
} catch {
|
|
127
|
+
await writeRolePlaySharedClaudeMd(projectDir, language);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Write (or overwrite) .roleplay/CLAUDE.md — the shared-level instructions
|
|
133
|
+
* inherited by all sessions via Claude Code's CLAUDE.md lookup chain.
|
|
134
|
+
*
|
|
135
|
+
* @param {string} projectDir
|
|
136
|
+
* @param {string} [language='zh-CN']
|
|
137
|
+
*/
|
|
138
|
+
export async function writeRolePlaySharedClaudeMd(projectDir, language = 'zh-CN') {
|
|
139
|
+
const m = getRolePlayMessages(language);
|
|
140
|
+
const rpDir = join(projectDir, '.roleplay');
|
|
141
|
+
|
|
142
|
+
const content = `${m.sharedTitle}
|
|
143
|
+
|
|
144
|
+
${m.projectPath}
|
|
145
|
+
${projectDir}
|
|
146
|
+
${m.useAbsolutePath}
|
|
147
|
+
|
|
148
|
+
${m.workMode}
|
|
149
|
+
${m.workModeContent}
|
|
150
|
+
|
|
151
|
+
${m.workConventions}
|
|
152
|
+
${m.workConventionsContent}
|
|
153
|
+
|
|
154
|
+
${m.crewRelation}
|
|
155
|
+
${m.crewRelationContent}
|
|
156
|
+
|
|
157
|
+
${m.sharedMemory}
|
|
158
|
+
${m.sharedMemoryDefault}
|
|
159
|
+
`;
|
|
160
|
+
|
|
161
|
+
await fs.writeFile(join(rpDir, 'CLAUDE.md'), content);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ─── Session CLAUDE.md generation ───────────────────────────────────
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Write .roleplay/roles/{sessionName}/CLAUDE.md — session-level config.
|
|
168
|
+
*
|
|
169
|
+
* This file is the core configuration that Claude Code reads automatically
|
|
170
|
+
* (cwd is set to this directory). It contains:
|
|
171
|
+
* - Session metadata (name, teamType, language)
|
|
172
|
+
* - Full role list with descriptions (generated from teamType or custom roles)
|
|
173
|
+
* - ROUTE protocol reference
|
|
174
|
+
* - Workflow for this team type
|
|
175
|
+
* - Project path reference
|
|
176
|
+
* - Session memory section
|
|
177
|
+
*
|
|
178
|
+
* @param {string} projectDir - project root
|
|
179
|
+
* @param {string} sessionName - session directory name
|
|
180
|
+
* @param {object} config - { teamType, language, roles?, projectDir? }
|
|
181
|
+
* - config.roles: optional custom role array from RolePlay config.
|
|
182
|
+
* If provided and non-empty, these are used instead of default team roles.
|
|
183
|
+
* Each role: { name, displayName, icon?, description?, claudeMd? }
|
|
184
|
+
*/
|
|
185
|
+
export async function writeSessionClaudeMd(projectDir, sessionName, config) {
|
|
186
|
+
const { teamType = 'dev', language = 'zh-CN', roles: customRoles } = config;
|
|
187
|
+
const m = getRolePlayMessages(language);
|
|
188
|
+
|
|
189
|
+
const sessionDir = join(projectDir, '.roleplay', 'roles', sessionName);
|
|
190
|
+
await fs.mkdir(sessionDir, { recursive: true });
|
|
191
|
+
|
|
192
|
+
// Build role list section
|
|
193
|
+
const roleSection = buildRoleSection(teamType, language, customRoles);
|
|
194
|
+
|
|
195
|
+
// Build workflow section
|
|
196
|
+
const workflow = (teamType === 'dev')
|
|
197
|
+
? m.devWorkflow
|
|
198
|
+
: m.genericWorkflow;
|
|
199
|
+
|
|
200
|
+
const content = `${m.sessionTitle(sessionName)}
|
|
201
|
+
|
|
202
|
+
${m.teamTypeLabel}
|
|
203
|
+
${teamType}
|
|
204
|
+
|
|
205
|
+
${m.languageLabel}
|
|
206
|
+
${language}
|
|
207
|
+
|
|
208
|
+
${m.roleListTitle}
|
|
209
|
+
|
|
210
|
+
${roleSection}
|
|
211
|
+
|
|
212
|
+
${m.routeProtocol}
|
|
213
|
+
|
|
214
|
+
${m.workflowTitle}
|
|
215
|
+
|
|
216
|
+
${workflow}
|
|
217
|
+
|
|
218
|
+
${m.projectPathTitle}
|
|
219
|
+
${projectDir}
|
|
220
|
+
${m.useAbsolutePath}
|
|
221
|
+
|
|
222
|
+
${m.sessionMemory}
|
|
223
|
+
${m.sessionMemoryDefault}
|
|
224
|
+
`;
|
|
225
|
+
|
|
226
|
+
await fs.writeFile(join(sessionDir, 'CLAUDE.md'), content);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Build the role list section for a session CLAUDE.md.
|
|
231
|
+
*
|
|
232
|
+
* Strategy:
|
|
233
|
+
* 1. If custom roles are provided (from RolePlay config), use them directly.
|
|
234
|
+
* For each custom role, look up a matching roleTemplate for extra detail,
|
|
235
|
+
* but prefer the custom role's claudeMd/description if provided.
|
|
236
|
+
* 2. Otherwise, use the default role set for the teamType from TEAM_ROLES.
|
|
237
|
+
*/
|
|
238
|
+
function buildRoleSection(teamType, language, customRoles) {
|
|
239
|
+
const m = getRolePlayMessages(language);
|
|
240
|
+
const templates = m.roleTemplates;
|
|
241
|
+
|
|
242
|
+
if (customRoles && customRoles.length > 0) {
|
|
243
|
+
return customRoles.map(r => {
|
|
244
|
+
const tmpl = templates[r.name];
|
|
245
|
+
// Prefer custom claudeMd, then custom description, then template
|
|
246
|
+
const body = r.claudeMd || r.description || (tmpl ? tmpl.content : '');
|
|
247
|
+
const heading = tmpl
|
|
248
|
+
? tmpl.heading
|
|
249
|
+
: `## ${r.icon || ''} ${r.displayName || r.name} (${r.name})`.replace(/\s{2,}/g, ' ');
|
|
250
|
+
return `${heading}\n${body}`;
|
|
251
|
+
}).join('\n\n');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Default: use TEAM_ROLES mapping
|
|
255
|
+
const roleNames = TEAM_ROLES[teamType] || TEAM_ROLES.dev;
|
|
256
|
+
return roleNames.map(name => {
|
|
257
|
+
const tmpl = templates[name];
|
|
258
|
+
if (!tmpl) return `## ${name}\n(No template available)`;
|
|
259
|
+
return `${tmpl.heading}\n${tmpl.content}`;
|
|
260
|
+
}).join('\n\n');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Get the default role list for a team type.
|
|
265
|
+
* Used by session creation to populate session.json roles snapshot.
|
|
266
|
+
*
|
|
267
|
+
* @param {string} teamType
|
|
268
|
+
* @param {string} language
|
|
269
|
+
* @returns {Array<{name: string, displayName: string, icon: string}>}
|
|
270
|
+
*/
|
|
271
|
+
export function getDefaultRoles(teamType, language = 'zh-CN') {
|
|
272
|
+
const m = getRolePlayMessages(language);
|
|
273
|
+
const templates = m.roleTemplates;
|
|
274
|
+
const roleNames = TEAM_ROLES[teamType] || TEAM_ROLES.dev;
|
|
275
|
+
|
|
276
|
+
return roleNames.map(name => {
|
|
277
|
+
const tmpl = templates[name];
|
|
278
|
+
if (!tmpl) return { name, displayName: name, icon: '' };
|
|
279
|
+
|
|
280
|
+
// Extract icon and displayName from heading: "## 📋 PM-乔布斯 (pm)"
|
|
281
|
+
const headingMatch = tmpl.heading.match(/^##\s*(\S+)\s+(.+?)\s*\((\w+)\)\s*$/);
|
|
282
|
+
if (headingMatch) {
|
|
283
|
+
return {
|
|
284
|
+
name: headingMatch[3],
|
|
285
|
+
displayName: headingMatch[2],
|
|
286
|
+
icon: headingMatch[1],
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
return { name, displayName: name, icon: '' };
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Get the session directory path.
|
|
295
|
+
* @param {string} projectDir
|
|
296
|
+
* @param {string} sessionName
|
|
297
|
+
* @returns {string} absolute path to .roleplay/roles/{sessionName}/
|
|
298
|
+
*/
|
|
299
|
+
export function getSessionDir(projectDir, sessionName) {
|
|
300
|
+
return join(projectDir, '.roleplay', 'roles', sessionName);
|
|
301
|
+
}
|
package/roleplay-i18n.js
ADDED
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RolePlay i18n — localized template strings for .roleplay/ directory files.
|
|
3
|
+
*
|
|
4
|
+
* Separated from crew-i18n.js because RolePlay CLAUDE.md templates have
|
|
5
|
+
* different structure (single-process, no worktree rules, session-scoped).
|
|
6
|
+
*
|
|
7
|
+
* Supported languages: 'zh-CN' (default), 'en'
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const messages = {
|
|
11
|
+
'zh-CN': {
|
|
12
|
+
// ── .roleplay/CLAUDE.md (shared level) ──
|
|
13
|
+
sharedTitle: '# RolePlay 共享指令',
|
|
14
|
+
projectPath: '# 项目路径',
|
|
15
|
+
useAbsolutePath: '所有代码操作请使用此绝对路径。',
|
|
16
|
+
workMode: '# 工作模式',
|
|
17
|
+
workModeContent: `RolePlay 是单进程多角色协作模式。一个 Claude 实例依次扮演不同角色,通过 ROUTE 协议切换。
|
|
18
|
+
与 Crew(多进程、每角色独立 Claude 实例)的区别:
|
|
19
|
+
- 所有角色共享同一个上下文窗口
|
|
20
|
+
- 角色切换是即时的(无需跨进程通信)
|
|
21
|
+
- 适合轻量级协作和快速迭代`,
|
|
22
|
+
workConventions: '# 工作约定',
|
|
23
|
+
workConventionsContent: `- 文档产出写入 .roleplay/context/ 目录
|
|
24
|
+
- 代码修改使用项目路径的绝对路径
|
|
25
|
+
- 每个角色专注自己的职责,不越界`,
|
|
26
|
+
crewRelation: '# 与 .crew 的关系',
|
|
27
|
+
crewRelationContent: `- .roleplay/context/ 可以读取 .crew/context/ 的内容
|
|
28
|
+
- .crew 的共享记忆和看板对 RolePlay 可见
|
|
29
|
+
- RolePlay 不修改 .crew/ 的任何内容(只读)`,
|
|
30
|
+
sharedMemory: '# 共享记忆',
|
|
31
|
+
sharedMemoryDefault: '_所有 session 共同维护,记录项目级的共识和决策。_',
|
|
32
|
+
|
|
33
|
+
// ── .roleplay/roles/{session}/CLAUDE.md (session level) ──
|
|
34
|
+
sessionTitle: (name) => `# RolePlay Session: ${name}`,
|
|
35
|
+
teamTypeLabel: '# 团队类型',
|
|
36
|
+
languageLabel: '# 语言',
|
|
37
|
+
roleListTitle: '# 角色列表',
|
|
38
|
+
|
|
39
|
+
// Role templates per teamType
|
|
40
|
+
roleTemplates: {
|
|
41
|
+
pm: {
|
|
42
|
+
heading: '## 📋 PM-乔布斯 (pm)',
|
|
43
|
+
content: `你是 PM-乔布斯。你的职责:
|
|
44
|
+
- 分析用户需求,理解意图
|
|
45
|
+
- 将需求拆分为可执行的开发任务
|
|
46
|
+
- 定义验收标准
|
|
47
|
+
- 最终验收开发成果
|
|
48
|
+
|
|
49
|
+
风格:简洁、注重用户价值、善于抓住本质。`,
|
|
50
|
+
},
|
|
51
|
+
dev: {
|
|
52
|
+
heading: '## 💻 开发者-托瓦兹 (dev)',
|
|
53
|
+
content: `你是开发者-托瓦兹。你的职责:
|
|
54
|
+
- 设计技术方案和架构
|
|
55
|
+
- 使用工具(Read, Edit, Write, Bash)实现代码
|
|
56
|
+
- 确保代码质量和可维护性
|
|
57
|
+
- 修复 reviewer 和 tester 提出的问题
|
|
58
|
+
|
|
59
|
+
风格:追求代码简洁优雅,重视性能和可维护性。不写废话,直接动手。`,
|
|
60
|
+
},
|
|
61
|
+
reviewer: {
|
|
62
|
+
heading: '## 🔍 审查者-马丁 (reviewer)',
|
|
63
|
+
content: `你是审查者-马丁。你的职责:
|
|
64
|
+
- 仔细审查开发者的代码变更
|
|
65
|
+
- 检查:代码风格、命名规范、架构合理性、边界情况、安全漏洞
|
|
66
|
+
- 如果有问题,明确指出并说明修改建议
|
|
67
|
+
- 确认通过后明确说"LGTM"
|
|
68
|
+
|
|
69
|
+
风格:严格但友善,注重最佳实践,善于发现潜在问题。`,
|
|
70
|
+
},
|
|
71
|
+
tester: {
|
|
72
|
+
heading: '## 🧪 测试者-贝克 (tester)',
|
|
73
|
+
content: `你是测试者-贝克。你的职责:
|
|
74
|
+
- 使用 Bash 工具运行测试
|
|
75
|
+
- 验证功能是否按预期工作
|
|
76
|
+
- 检查边界情况和异常处理
|
|
77
|
+
- 如果有 bug,明确描述复现步骤
|
|
78
|
+
|
|
79
|
+
风格:测试驱动思维,善于发现边界情况,追求可靠性。`,
|
|
80
|
+
},
|
|
81
|
+
designer: {
|
|
82
|
+
heading: '## 🎨 设计师-拉姆斯 (designer)',
|
|
83
|
+
content: `你是设计师-拉姆斯。你的职责:
|
|
84
|
+
- 设计用户交互流程和页面布局
|
|
85
|
+
- 确保视觉一致性和用户体验
|
|
86
|
+
- 输出设计方案给开发者实现
|
|
87
|
+
|
|
88
|
+
风格:Less but better,追求极致简洁。`,
|
|
89
|
+
},
|
|
90
|
+
// Writing team
|
|
91
|
+
planner: {
|
|
92
|
+
heading: '## 📋 策划-曹雪芹 (planner)',
|
|
93
|
+
content: `你是策划-曹雪芹。你的职责:
|
|
94
|
+
- 制定整体创作方向和大纲
|
|
95
|
+
- 审核内容质量和一致性
|
|
96
|
+
- 做出关键创作决策
|
|
97
|
+
|
|
98
|
+
风格:注重故事内核,善于把控整体节奏。`,
|
|
99
|
+
},
|
|
100
|
+
writer: {
|
|
101
|
+
heading: '## ✍️ 执笔师-鲁迅 (writer)',
|
|
102
|
+
content: `你是执笔师-鲁迅。你的职责:
|
|
103
|
+
- 根据大纲撰写具体内容
|
|
104
|
+
- 把控文字质量和风格一致性
|
|
105
|
+
- 按照设计师的节奏方案写作
|
|
106
|
+
|
|
107
|
+
风格:文字犀利,善于用细节打动人。`,
|
|
108
|
+
},
|
|
109
|
+
editor: {
|
|
110
|
+
heading: '## 📝 审稿师 (editor)',
|
|
111
|
+
content: `你是审稿师。你的职责:
|
|
112
|
+
- 审核文字质量、逻辑一致性
|
|
113
|
+
- 检查错别字、语法、标点
|
|
114
|
+
- 确认内容符合整体方向
|
|
115
|
+
|
|
116
|
+
风格:严谨细致,注重可读性。`,
|
|
117
|
+
},
|
|
118
|
+
// Trading team
|
|
119
|
+
strategist: {
|
|
120
|
+
heading: '## 📋 策略师 (strategist)',
|
|
121
|
+
content: `你是策略师。你的职责:
|
|
122
|
+
- 综合技术分析和宏观研究做出交易决策
|
|
123
|
+
- 管理整体仓位和风险敞口
|
|
124
|
+
- 协调团队分析方向
|
|
125
|
+
|
|
126
|
+
风格:冷静理性,注重概率思维。`,
|
|
127
|
+
},
|
|
128
|
+
analyst: {
|
|
129
|
+
heading: '## 📊 技术分析师 (analyst)',
|
|
130
|
+
content: `你是技术分析师。你的职责:
|
|
131
|
+
- 分析价格走势和技术指标
|
|
132
|
+
- 识别关键支撑/阻力位
|
|
133
|
+
- 提供入场/出场信号
|
|
134
|
+
|
|
135
|
+
风格:数据驱动,图表说话。`,
|
|
136
|
+
},
|
|
137
|
+
macro: {
|
|
138
|
+
heading: '## 🌐 宏观研究员 (macro)',
|
|
139
|
+
content: `你是宏观研究员。你的职责:
|
|
140
|
+
- 分析宏观经济数据和政策
|
|
141
|
+
- 评估市场情绪和资金流向
|
|
142
|
+
- 提供宏观背景判断
|
|
143
|
+
|
|
144
|
+
风格:视野开阔,善于关联不同市场。`,
|
|
145
|
+
},
|
|
146
|
+
risk: {
|
|
147
|
+
heading: '## 🛡️ 风控 (risk)',
|
|
148
|
+
content: `你是风控。你的职责:
|
|
149
|
+
- 审查交易方案的风险
|
|
150
|
+
- 设定止损和仓位限制
|
|
151
|
+
- 监控已有持仓风险
|
|
152
|
+
|
|
153
|
+
风格:保守谨慎,底线思维。`,
|
|
154
|
+
},
|
|
155
|
+
trader: {
|
|
156
|
+
heading: '## 💰 交易员 (trader)',
|
|
157
|
+
content: `你是交易员。你的职责:
|
|
158
|
+
- 执行策略师的交易决策
|
|
159
|
+
- 选择最优执行时机和方式
|
|
160
|
+
- 报告执行结果
|
|
161
|
+
|
|
162
|
+
风格:执行力强,反应迅速。`,
|
|
163
|
+
},
|
|
164
|
+
// Video team
|
|
165
|
+
director: {
|
|
166
|
+
heading: '## 🎬 导演 (director)',
|
|
167
|
+
content: `你是导演。你的职责:
|
|
168
|
+
- 把控整体视觉叙事方向
|
|
169
|
+
- 审核脚本和分镜
|
|
170
|
+
- 做出最终创意决策
|
|
171
|
+
|
|
172
|
+
风格:注重视觉叙事,善于把控节奏。`,
|
|
173
|
+
},
|
|
174
|
+
scriptwriter: {
|
|
175
|
+
heading: '## ✏️ 编剧 (scriptwriter)',
|
|
176
|
+
content: `你是编剧。你的职责:
|
|
177
|
+
- 撰写视频脚本和旁白
|
|
178
|
+
- 构建叙事结构
|
|
179
|
+
- 设计情节转折
|
|
180
|
+
|
|
181
|
+
风格:善于讲故事,注重情感共鸣。`,
|
|
182
|
+
},
|
|
183
|
+
storyboard: {
|
|
184
|
+
heading: '## 🖼️ 分镜师 (storyboard)',
|
|
185
|
+
content: `你是分镜师。你的职责:
|
|
186
|
+
- 将脚本转化为视觉分镜
|
|
187
|
+
- 设计镜头语言和转场
|
|
188
|
+
- 确保视觉连贯性
|
|
189
|
+
|
|
190
|
+
风格:视觉思维,善于用画面讲故事。`,
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
routeProtocol: `# ROUTE 协议
|
|
195
|
+
|
|
196
|
+
当一个角色完成工作需要交给另一个角色时,使用 ROUTE 块:
|
|
197
|
+
|
|
198
|
+
---ROUTE---
|
|
199
|
+
to: {目标角色name}
|
|
200
|
+
summary: {交接内容摘要}
|
|
201
|
+
task: {任务ID}(可选)
|
|
202
|
+
taskTitle: {任务标题}(可选)
|
|
203
|
+
---END_ROUTE---
|
|
204
|
+
|
|
205
|
+
规则:
|
|
206
|
+
- \`to\` 必须是有效的角色 name,或 \`human\` 表示需要用户输入
|
|
207
|
+
- 一次可以输出多个 ROUTE 块
|
|
208
|
+
- ROUTE 块必须在角色输出的末尾
|
|
209
|
+
- 切换后必须完全以该角色的视角和人格思考和行动`,
|
|
210
|
+
|
|
211
|
+
workflowTitle: '# 工作流程',
|
|
212
|
+
devWorkflow: `1. **PM** 分析需求,拆分任务,确定验收标准
|
|
213
|
+
2. **开发者** 实现代码(使用工具读写文件)
|
|
214
|
+
3. **审查者** Code Review(不通过 → 返回开发者修复)
|
|
215
|
+
4. **测试者** 运行测试 & 验证(有 bug → 返回开发者修复)
|
|
216
|
+
5. **PM** 验收总结`,
|
|
217
|
+
genericWorkflow: '按角色顺序依次完成任务。',
|
|
218
|
+
|
|
219
|
+
projectPathTitle: '# 项目路径',
|
|
220
|
+
|
|
221
|
+
sessionMemory: '# Session 记忆',
|
|
222
|
+
sessionMemoryDefault: '_本 session 的工作记录、决策和待办事项。_',
|
|
223
|
+
},
|
|
224
|
+
|
|
225
|
+
'en': {
|
|
226
|
+
// ── .roleplay/CLAUDE.md (shared level) ──
|
|
227
|
+
sharedTitle: '# RolePlay Shared Instructions',
|
|
228
|
+
projectPath: '# Project Path',
|
|
229
|
+
useAbsolutePath: 'Use this absolute path for all code operations.',
|
|
230
|
+
workMode: '# Work Mode',
|
|
231
|
+
workModeContent: `RolePlay is a single-process multi-role collaboration mode. One Claude instance plays different roles in sequence, switching via the ROUTE protocol.
|
|
232
|
+
Differences from Crew (multi-process, each role has its own Claude instance):
|
|
233
|
+
- All roles share the same context window
|
|
234
|
+
- Role switching is instant (no cross-process communication)
|
|
235
|
+
- Suitable for lightweight collaboration and rapid iteration`,
|
|
236
|
+
workConventions: '# Work Conventions',
|
|
237
|
+
workConventionsContent: `- Write documentation output to .roleplay/context/ directory
|
|
238
|
+
- Use absolute project path for code changes
|
|
239
|
+
- Each role focuses on its own responsibilities`,
|
|
240
|
+
crewRelation: '# Relationship with .crew',
|
|
241
|
+
crewRelationContent: `- .roleplay/context/ can read .crew/context/ content
|
|
242
|
+
- .crew shared memory and kanban are visible to RolePlay
|
|
243
|
+
- RolePlay does not modify anything in .crew/ (read-only)`,
|
|
244
|
+
sharedMemory: '# Shared Memory',
|
|
245
|
+
sharedMemoryDefault: '_Maintained by all sessions, recording project-level consensus and decisions._',
|
|
246
|
+
|
|
247
|
+
// ── .roleplay/roles/{session}/CLAUDE.md (session level) ──
|
|
248
|
+
sessionTitle: (name) => `# RolePlay Session: ${name}`,
|
|
249
|
+
teamTypeLabel: '# Team Type',
|
|
250
|
+
languageLabel: '# Language',
|
|
251
|
+
roleListTitle: '# Role List',
|
|
252
|
+
|
|
253
|
+
roleTemplates: {
|
|
254
|
+
pm: {
|
|
255
|
+
heading: '## 📋 PM-Jobs (pm)',
|
|
256
|
+
content: `You are PM-Jobs. Your responsibilities:
|
|
257
|
+
- Analyze user requirements and understand intent
|
|
258
|
+
- Break down requirements into executable tasks
|
|
259
|
+
- Define acceptance criteria
|
|
260
|
+
- Final acceptance of deliverables
|
|
261
|
+
|
|
262
|
+
Style: Concise, user-value focused, excellent at grasping the essence.`,
|
|
263
|
+
},
|
|
264
|
+
dev: {
|
|
265
|
+
heading: '## 💻 Dev-Torvalds (dev)',
|
|
266
|
+
content: `You are Dev-Torvalds. Your responsibilities:
|
|
267
|
+
- Design technical solutions and architecture
|
|
268
|
+
- Use tools (Read, Edit, Write, Bash) to implement code
|
|
269
|
+
- Ensure code quality and maintainability
|
|
270
|
+
- Fix issues raised by reviewer and tester
|
|
271
|
+
|
|
272
|
+
Style: Pursue clean, elegant code. Value performance and maintainability. No fluff, just code.`,
|
|
273
|
+
},
|
|
274
|
+
reviewer: {
|
|
275
|
+
heading: '## 🔍 Reviewer-Martin (reviewer)',
|
|
276
|
+
content: `You are Reviewer-Martin. Your responsibilities:
|
|
277
|
+
- Carefully review code changes from the developer
|
|
278
|
+
- Check: code style, naming conventions, architecture, edge cases, security
|
|
279
|
+
- If issues found, clearly point them out with fix suggestions
|
|
280
|
+
- When approved, explicitly say "LGTM"
|
|
281
|
+
|
|
282
|
+
Style: Strict but kind, focused on best practices, good at spotting potential issues.`,
|
|
283
|
+
},
|
|
284
|
+
tester: {
|
|
285
|
+
heading: '## 🧪 Tester-Beck (tester)',
|
|
286
|
+
content: `You are Tester-Beck. Your responsibilities:
|
|
287
|
+
- Use Bash tool to run tests
|
|
288
|
+
- Verify functionality works as expected
|
|
289
|
+
- Check edge cases and error handling
|
|
290
|
+
- If bugs found, clearly describe reproduction steps
|
|
291
|
+
|
|
292
|
+
Style: Test-driven thinking, good at finding edge cases, pursuing reliability.`,
|
|
293
|
+
},
|
|
294
|
+
designer: {
|
|
295
|
+
heading: '## 🎨 Designer-Rams (designer)',
|
|
296
|
+
content: `You are Designer-Rams. Your responsibilities:
|
|
297
|
+
- Design user interaction flows and page layouts
|
|
298
|
+
- Ensure visual consistency and user experience
|
|
299
|
+
- Deliver design specs for developer implementation
|
|
300
|
+
|
|
301
|
+
Style: Less but better, pursuing ultimate simplicity.`,
|
|
302
|
+
},
|
|
303
|
+
planner: {
|
|
304
|
+
heading: '## 📋 Planner (planner)',
|
|
305
|
+
content: `You are the Planner. Your responsibilities:
|
|
306
|
+
- Set overall creative direction and outline
|
|
307
|
+
- Review content quality and consistency
|
|
308
|
+
- Make key creative decisions
|
|
309
|
+
|
|
310
|
+
Style: Focus on story core, good at pacing control.`,
|
|
311
|
+
},
|
|
312
|
+
writer: {
|
|
313
|
+
heading: '## ✍️ Writer (writer)',
|
|
314
|
+
content: `You are the Writer. Your responsibilities:
|
|
315
|
+
- Write specific content based on the outline
|
|
316
|
+
- Maintain writing quality and style consistency
|
|
317
|
+
- Follow the designer's pacing plan
|
|
318
|
+
|
|
319
|
+
Style: Sharp writing, good at using details to move people.`,
|
|
320
|
+
},
|
|
321
|
+
editor: {
|
|
322
|
+
heading: '## 📝 Editor (editor)',
|
|
323
|
+
content: `You are the Editor. Your responsibilities:
|
|
324
|
+
- Review writing quality, logical consistency
|
|
325
|
+
- Check typos, grammar, punctuation
|
|
326
|
+
- Confirm content aligns with overall direction
|
|
327
|
+
|
|
328
|
+
Style: Rigorous and detailed, focused on readability.`,
|
|
329
|
+
},
|
|
330
|
+
strategist: {
|
|
331
|
+
heading: '## 📋 Strategist (strategist)',
|
|
332
|
+
content: `You are the Strategist. Your responsibilities:
|
|
333
|
+
- Synthesize technical analysis and macro research for trading decisions
|
|
334
|
+
- Manage overall positions and risk exposure
|
|
335
|
+
- Coordinate team analysis direction
|
|
336
|
+
|
|
337
|
+
Style: Calm and rational, probability-focused thinking.`,
|
|
338
|
+
},
|
|
339
|
+
analyst: {
|
|
340
|
+
heading: '## 📊 Technical Analyst (analyst)',
|
|
341
|
+
content: `You are the Technical Analyst. Your responsibilities:
|
|
342
|
+
- Analyze price trends and technical indicators
|
|
343
|
+
- Identify key support/resistance levels
|
|
344
|
+
- Provide entry/exit signals
|
|
345
|
+
|
|
346
|
+
Style: Data-driven, charts speak.`,
|
|
347
|
+
},
|
|
348
|
+
macro: {
|
|
349
|
+
heading: '## 🌐 Macro Researcher (macro)',
|
|
350
|
+
content: `You are the Macro Researcher. Your responsibilities:
|
|
351
|
+
- Analyze macroeconomic data and policies
|
|
352
|
+
- Assess market sentiment and fund flows
|
|
353
|
+
- Provide macro context judgment
|
|
354
|
+
|
|
355
|
+
Style: Broad perspective, good at connecting different markets.`,
|
|
356
|
+
},
|
|
357
|
+
risk: {
|
|
358
|
+
heading: '## 🛡️ Risk Manager (risk)',
|
|
359
|
+
content: `You are the Risk Manager. Your responsibilities:
|
|
360
|
+
- Review trading plan risks
|
|
361
|
+
- Set stop-loss and position limits
|
|
362
|
+
- Monitor existing position risk
|
|
363
|
+
|
|
364
|
+
Style: Conservative, bottom-line thinking.`,
|
|
365
|
+
},
|
|
366
|
+
trader: {
|
|
367
|
+
heading: '## 💰 Trader (trader)',
|
|
368
|
+
content: `You are the Trader. Your responsibilities:
|
|
369
|
+
- Execute strategist's trading decisions
|
|
370
|
+
- Choose optimal execution timing and method
|
|
371
|
+
- Report execution results
|
|
372
|
+
|
|
373
|
+
Style: Strong execution, quick reactions.`,
|
|
374
|
+
},
|
|
375
|
+
director: {
|
|
376
|
+
heading: '## 🎬 Director (director)',
|
|
377
|
+
content: `You are the Director. Your responsibilities:
|
|
378
|
+
- Control overall visual narrative direction
|
|
379
|
+
- Review scripts and storyboards
|
|
380
|
+
- Make final creative decisions
|
|
381
|
+
|
|
382
|
+
Style: Visual storytelling, good at pacing control.`,
|
|
383
|
+
},
|
|
384
|
+
scriptwriter: {
|
|
385
|
+
heading: '## ✏️ Scriptwriter (scriptwriter)',
|
|
386
|
+
content: `You are the Scriptwriter. Your responsibilities:
|
|
387
|
+
- Write video scripts and narration
|
|
388
|
+
- Build narrative structure
|
|
389
|
+
- Design plot twists
|
|
390
|
+
|
|
391
|
+
Style: Good storytelling, focused on emotional resonance.`,
|
|
392
|
+
},
|
|
393
|
+
storyboard: {
|
|
394
|
+
heading: '## 🖼️ Storyboard Artist (storyboard)',
|
|
395
|
+
content: `You are the Storyboard Artist. Your responsibilities:
|
|
396
|
+
- Convert scripts into visual storyboards
|
|
397
|
+
- Design camera language and transitions
|
|
398
|
+
- Ensure visual continuity
|
|
399
|
+
|
|
400
|
+
Style: Visual thinking, good at telling stories with images.`,
|
|
401
|
+
},
|
|
402
|
+
},
|
|
403
|
+
|
|
404
|
+
routeProtocol: `# ROUTE Protocol
|
|
405
|
+
|
|
406
|
+
When a role finishes work and needs to hand off to another role, use a ROUTE block:
|
|
407
|
+
|
|
408
|
+
---ROUTE---
|
|
409
|
+
to: {target_role_name}
|
|
410
|
+
summary: {handoff content summary}
|
|
411
|
+
task: {task ID} (optional)
|
|
412
|
+
taskTitle: {task title} (optional)
|
|
413
|
+
---END_ROUTE---
|
|
414
|
+
|
|
415
|
+
Rules:
|
|
416
|
+
- \`to\` must be a valid role name, or \`human\` for user input
|
|
417
|
+
- Multiple ROUTE blocks can be output at once
|
|
418
|
+
- ROUTE blocks must be at the end of role output
|
|
419
|
+
- After switching, fully think and act from that role's perspective`,
|
|
420
|
+
|
|
421
|
+
workflowTitle: '# Workflow',
|
|
422
|
+
devWorkflow: `1. **PM** analyzes requirements, breaks down tasks, defines acceptance criteria
|
|
423
|
+
2. **Dev** implements code (using tools to read/write files)
|
|
424
|
+
3. **Reviewer** code review (if fails → back to Dev)
|
|
425
|
+
4. **Tester** runs tests & verifies (if bugs → back to Dev)
|
|
426
|
+
5. **PM** acceptance & summary`,
|
|
427
|
+
genericWorkflow: 'Complete tasks by following the role sequence.',
|
|
428
|
+
|
|
429
|
+
projectPathTitle: '# Project Path',
|
|
430
|
+
|
|
431
|
+
sessionMemory: '# Session Memory',
|
|
432
|
+
sessionMemoryDefault: '_Work records, decisions, and to-do items for this session._',
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Get RolePlay i18n messages for the given language.
|
|
438
|
+
* Falls back to 'zh-CN' for unknown languages.
|
|
439
|
+
*
|
|
440
|
+
* @param {string} language
|
|
441
|
+
* @returns {object}
|
|
442
|
+
*/
|
|
443
|
+
export function getRolePlayMessages(language) {
|
|
444
|
+
return messages[language] || messages['zh-CN'];
|
|
445
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RolePlay Session — session index management for .roleplay/session.json.
|
|
3
|
+
*
|
|
4
|
+
* Analogous to agent/crew/session.js but much simpler:
|
|
5
|
+
* - No multi-process concerns (single process)
|
|
6
|
+
* - No worktree management
|
|
7
|
+
* - Atomic write for crash safety
|
|
8
|
+
*
|
|
9
|
+
* session.json schema:
|
|
10
|
+
* {
|
|
11
|
+
* sessions: [{ name, teamType, language, projectDir, conversationId,
|
|
12
|
+
* roles, createdAt, updatedAt, status }],
|
|
13
|
+
* activeSession: string | null
|
|
14
|
+
* }
|
|
15
|
+
*/
|
|
16
|
+
import { promises as fs } from 'fs';
|
|
17
|
+
import { join } from 'path';
|
|
18
|
+
import { existsSync, readFileSync, writeFileSync, renameSync } from 'fs';
|
|
19
|
+
|
|
20
|
+
const SESSION_FILE = 'session.json';
|
|
21
|
+
|
|
22
|
+
// ─── Read / Write ───────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Load .roleplay/session.json.
|
|
26
|
+
* Returns { sessions: [], activeSession: null } if file doesn't exist or is corrupt.
|
|
27
|
+
*
|
|
28
|
+
* @param {string} projectDir
|
|
29
|
+
* @returns {{ sessions: Array, activeSession: string|null }}
|
|
30
|
+
*/
|
|
31
|
+
export function loadRolePlaySessionIndex(projectDir) {
|
|
32
|
+
const filePath = join(projectDir, '.roleplay', SESSION_FILE);
|
|
33
|
+
if (!existsSync(filePath)) {
|
|
34
|
+
return { sessions: [], activeSession: null };
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const raw = readFileSync(filePath, 'utf-8');
|
|
38
|
+
const data = JSON.parse(raw);
|
|
39
|
+
return {
|
|
40
|
+
sessions: Array.isArray(data.sessions) ? data.sessions : [],
|
|
41
|
+
activeSession: data.activeSession || null,
|
|
42
|
+
};
|
|
43
|
+
} catch (e) {
|
|
44
|
+
console.warn('[RolePlaySession] Failed to load session.json:', e.message);
|
|
45
|
+
return { sessions: [], activeSession: null };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Save .roleplay/session.json atomically (write tmp → rename).
|
|
51
|
+
*
|
|
52
|
+
* @param {string} projectDir
|
|
53
|
+
* @param {{ sessions: Array, activeSession: string|null }} data
|
|
54
|
+
*/
|
|
55
|
+
export async function saveRolePlaySessionIndex(projectDir, data) {
|
|
56
|
+
const rpDir = join(projectDir, '.roleplay');
|
|
57
|
+
const filePath = join(rpDir, SESSION_FILE);
|
|
58
|
+
const tmpPath = `${filePath}.tmp.${Date.now()}`;
|
|
59
|
+
|
|
60
|
+
const content = JSON.stringify(data, null, 2);
|
|
61
|
+
try {
|
|
62
|
+
await fs.mkdir(rpDir, { recursive: true });
|
|
63
|
+
await fs.writeFile(tmpPath, content);
|
|
64
|
+
await fs.rename(tmpPath, filePath);
|
|
65
|
+
} catch (e) {
|
|
66
|
+
console.error('[RolePlaySession] Failed to save session.json:', e.message);
|
|
67
|
+
// Clean up tmp file if rename failed
|
|
68
|
+
try { await fs.unlink(tmpPath); } catch {}
|
|
69
|
+
throw e;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ─── Session CRUD ───────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Add a new session entry to .roleplay/session.json.
|
|
77
|
+
* Sets it as the activeSession.
|
|
78
|
+
*
|
|
79
|
+
* @param {string} projectDir
|
|
80
|
+
* @param {object} session - session entry to add:
|
|
81
|
+
* { name, teamType, language, projectDir, conversationId, roles, createdAt }
|
|
82
|
+
* @returns {object} the session entry with updatedAt/status fields added
|
|
83
|
+
*/
|
|
84
|
+
export async function addRolePlaySession(projectDir, session) {
|
|
85
|
+
const index = loadRolePlaySessionIndex(projectDir);
|
|
86
|
+
|
|
87
|
+
// Check for duplicate session name
|
|
88
|
+
const existing = index.sessions.find(s => s.name === session.name);
|
|
89
|
+
if (existing) {
|
|
90
|
+
// Update existing entry instead of creating duplicate
|
|
91
|
+
Object.assign(existing, {
|
|
92
|
+
...session,
|
|
93
|
+
updatedAt: Date.now(),
|
|
94
|
+
status: 'active',
|
|
95
|
+
});
|
|
96
|
+
index.activeSession = session.name;
|
|
97
|
+
await saveRolePlaySessionIndex(projectDir, index);
|
|
98
|
+
return existing;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const entry = {
|
|
102
|
+
...session,
|
|
103
|
+
updatedAt: Date.now(),
|
|
104
|
+
status: 'active',
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
index.sessions.push(entry);
|
|
108
|
+
index.activeSession = session.name;
|
|
109
|
+
await saveRolePlaySessionIndex(projectDir, index);
|
|
110
|
+
return entry;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Remove (archive) a session from .roleplay/session.json.
|
|
115
|
+
* The session entry is marked as 'archived', not deleted,
|
|
116
|
+
* so the context/ directory content remains valid.
|
|
117
|
+
*
|
|
118
|
+
* Optionally removes the session's roles/ directory.
|
|
119
|
+
*
|
|
120
|
+
* @param {string} projectDir
|
|
121
|
+
* @param {string} sessionName
|
|
122
|
+
* @param {object} [options]
|
|
123
|
+
* @param {boolean} [options.removeDir=true] - remove .roleplay/roles/{sessionName}/
|
|
124
|
+
*/
|
|
125
|
+
export async function removeRolePlaySession(projectDir, sessionName, options = {}) {
|
|
126
|
+
const { removeDir = true } = options;
|
|
127
|
+
const index = loadRolePlaySessionIndex(projectDir);
|
|
128
|
+
|
|
129
|
+
const session = index.sessions.find(s => s.name === sessionName);
|
|
130
|
+
if (session) {
|
|
131
|
+
session.status = 'archived';
|
|
132
|
+
session.updatedAt = Date.now();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Clear activeSession if it was the removed one
|
|
136
|
+
if (index.activeSession === sessionName) {
|
|
137
|
+
// Pick next active session (most recently updated non-archived)
|
|
138
|
+
const active = index.sessions
|
|
139
|
+
.filter(s => s.status === 'active')
|
|
140
|
+
.sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0));
|
|
141
|
+
index.activeSession = active[0]?.name || null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
await saveRolePlaySessionIndex(projectDir, index);
|
|
145
|
+
|
|
146
|
+
// Remove the session directory (but keep context/)
|
|
147
|
+
if (removeDir) {
|
|
148
|
+
const sessionDir = join(projectDir, '.roleplay', 'roles', sessionName);
|
|
149
|
+
try {
|
|
150
|
+
await fs.rm(sessionDir, { recursive: true, force: true });
|
|
151
|
+
} catch {
|
|
152
|
+
// Not critical — may not exist
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Find a session entry by conversationId.
|
|
159
|
+
*
|
|
160
|
+
* @param {string} projectDir
|
|
161
|
+
* @param {string} conversationId
|
|
162
|
+
* @returns {object|null} session entry or null
|
|
163
|
+
*/
|
|
164
|
+
export function findRolePlaySessionByConversationId(projectDir, conversationId) {
|
|
165
|
+
if (!conversationId) return null;
|
|
166
|
+
const index = loadRolePlaySessionIndex(projectDir);
|
|
167
|
+
return index.sessions.find(s => s.conversationId === conversationId) || null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Update the activeSession pointer and updatedAt timestamp.
|
|
172
|
+
*
|
|
173
|
+
* @param {string} projectDir
|
|
174
|
+
* @param {string} sessionName
|
|
175
|
+
*/
|
|
176
|
+
export async function setActiveRolePlaySession(projectDir, sessionName) {
|
|
177
|
+
const index = loadRolePlaySessionIndex(projectDir);
|
|
178
|
+
const session = index.sessions.find(s => s.name === sessionName);
|
|
179
|
+
if (session) {
|
|
180
|
+
session.updatedAt = Date.now();
|
|
181
|
+
index.activeSession = sessionName;
|
|
182
|
+
await saveRolePlaySessionIndex(projectDir, index);
|
|
183
|
+
}
|
|
184
|
+
}
|