@yeaft/webchat-agent 0.1.60 → 0.1.61
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/claude.js +9 -9
- package/conversation.js +33 -33
- package/package.json +1 -1
- package/{vcrew.js → roleplay.js} +40 -24
package/claude.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { query, Stream } from './sdk/index.js';
|
|
2
2
|
import ctx from './context.js';
|
|
3
3
|
import { sendConversationList, sendOutput, sendError, handleAskUserQuestion } from './conversation.js';
|
|
4
|
-
import {
|
|
4
|
+
import { buildRolePlaySystemPrompt } from './roleplay.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Start a Claude SDK query for a conversation
|
|
@@ -10,13 +10,13 @@ import { buildVCrewSystemPrompt } from './vcrew.js';
|
|
|
10
10
|
export async function startClaudeQuery(conversationId, workDir, resumeSessionId) {
|
|
11
11
|
// 如果已存在,先保存 per-session 设置,再关闭
|
|
12
12
|
let savedDisallowedTools = null;
|
|
13
|
-
let
|
|
13
|
+
let savedRolePlayConfig = null;
|
|
14
14
|
let savedUserId = undefined;
|
|
15
15
|
let savedUsername = undefined;
|
|
16
16
|
if (ctx.conversations.has(conversationId)) {
|
|
17
17
|
const existing = ctx.conversations.get(conversationId);
|
|
18
18
|
savedDisallowedTools = existing.disallowedTools ?? null;
|
|
19
|
-
|
|
19
|
+
savedRolePlayConfig = existing.rolePlayConfig ?? null;
|
|
20
20
|
savedUserId = existing.userId;
|
|
21
21
|
savedUsername = existing.username;
|
|
22
22
|
if (existing.abortController) {
|
|
@@ -54,8 +54,8 @@ export async function startClaudeQuery(conversationId, workDir, resumeSessionId)
|
|
|
54
54
|
backgroundTasks: new Map(),
|
|
55
55
|
// Per-session 工具禁用设置
|
|
56
56
|
disallowedTools: savedDisallowedTools,
|
|
57
|
-
//
|
|
58
|
-
|
|
57
|
+
// Role Play config (for appendSystemPrompt injection)
|
|
58
|
+
rolePlayConfig: savedRolePlayConfig,
|
|
59
59
|
// 保留用户信息(从旧 state 恢复)
|
|
60
60
|
userId: savedUserId,
|
|
61
61
|
username: savedUsername,
|
|
@@ -85,10 +85,10 @@ export async function startClaudeQuery(conversationId, workDir, resumeSessionId)
|
|
|
85
85
|
console.log(`[SDK] Disallowed tools: ${effectiveDisallowedTools.join(', ')}`);
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
//
|
|
89
|
-
if (
|
|
90
|
-
options.appendSystemPrompt =
|
|
91
|
-
console.log(`[SDK]
|
|
88
|
+
// Role Play: inject appendSystemPrompt with role descriptions and workflow
|
|
89
|
+
if (savedRolePlayConfig) {
|
|
90
|
+
options.appendSystemPrompt = buildRolePlaySystemPrompt(savedRolePlayConfig);
|
|
91
|
+
console.log(`[SDK] RolePlay appendSystemPrompt injected (teamType: ${savedRolePlayConfig.teamType})`);
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
// Validate session ID is a valid UUID before using it
|
package/conversation.js
CHANGED
|
@@ -2,10 +2,10 @@ 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 {
|
|
5
|
+
import { rolePlaySessions, saveRolePlayIndex, removeRolePlaySession, loadRolePlayIndex, validateRolePlayConfig } from './roleplay.js';
|
|
6
6
|
|
|
7
|
-
// Restore persisted
|
|
8
|
-
|
|
7
|
+
// Restore persisted roleplay sessions on module load (agent startup)
|
|
8
|
+
loadRolePlayIndex();
|
|
9
9
|
|
|
10
10
|
// 不支持的斜杠命令(真正需要交互式 CLI 的命令)
|
|
11
11
|
const UNSUPPORTED_SLASH_COMMANDS = ['/help', '/bug', '/login', '/logout', '/terminal-setup', '/vim', '/config'];
|
|
@@ -38,7 +38,7 @@ export function parseSlashCommand(message) {
|
|
|
38
38
|
return { type: null, message };
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
// 发送 conversation 列表(含活跃 crew sessions + 索引中已停止的 crew sessions +
|
|
41
|
+
// 发送 conversation 列表(含活跃 crew sessions + 索引中已停止的 crew sessions + roleplay sessions)
|
|
42
42
|
export async function sendConversationList() {
|
|
43
43
|
const list = [];
|
|
44
44
|
for (const [id, state] of ctx.conversations) {
|
|
@@ -51,10 +51,10 @@ export async function sendConversationList() {
|
|
|
51
51
|
userId: state.userId,
|
|
52
52
|
username: state.username
|
|
53
53
|
};
|
|
54
|
-
//
|
|
55
|
-
if (
|
|
56
|
-
entry.type = '
|
|
57
|
-
entry.
|
|
54
|
+
// roleplay conversations are stored in ctx.conversations but also tracked in rolePlaySessions
|
|
55
|
+
if (rolePlaySessions.has(id)) {
|
|
56
|
+
entry.type = 'rolePlay';
|
|
57
|
+
entry.rolePlayRoles = rolePlaySessions.get(id).roles;
|
|
58
58
|
}
|
|
59
59
|
list.push(entry);
|
|
60
60
|
}
|
|
@@ -118,21 +118,21 @@ export async function createConversation(msg) {
|
|
|
118
118
|
const { conversationId, workDir, userId, username, disallowedTools } = msg;
|
|
119
119
|
const effectiveWorkDir = workDir || ctx.CONFIG.workDir;
|
|
120
120
|
|
|
121
|
-
// Validate and sanitize
|
|
122
|
-
let
|
|
123
|
-
if (msg.
|
|
124
|
-
const result =
|
|
121
|
+
// Validate and sanitize rolePlayConfig if provided
|
|
122
|
+
let rolePlayConfig = null;
|
|
123
|
+
if (msg.rolePlayConfig) {
|
|
124
|
+
const result = validateRolePlayConfig(msg.rolePlayConfig);
|
|
125
125
|
if (!result.valid) {
|
|
126
|
-
console.warn(`[createConversation] Invalid
|
|
127
|
-
sendError(conversationId, `Invalid
|
|
126
|
+
console.warn(`[createConversation] Invalid rolePlayConfig: ${result.error}`);
|
|
127
|
+
sendError(conversationId, `Invalid rolePlayConfig: ${result.error}`);
|
|
128
128
|
return;
|
|
129
129
|
}
|
|
130
|
-
|
|
130
|
+
rolePlayConfig = result.config;
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
console.log(`Creating conversation: ${conversationId} in ${effectiveWorkDir} (lazy start)`);
|
|
134
134
|
if (username) console.log(` User: ${username} (${userId})`);
|
|
135
|
-
if (
|
|
135
|
+
if (rolePlayConfig) console.log(` RolePlay: teamType=${rolePlayConfig.teamType}, roles=${rolePlayConfig.roles?.length}`);
|
|
136
136
|
|
|
137
137
|
// 只创建 conversation 状态,不启动 Claude 进程
|
|
138
138
|
// Claude 进程会在用户发送第一条消息时启动 (见 handleUserInput)
|
|
@@ -149,7 +149,7 @@ export async function createConversation(msg) {
|
|
|
149
149
|
userId,
|
|
150
150
|
username,
|
|
151
151
|
disallowedTools: disallowedTools || null, // null = 使用全局默认
|
|
152
|
-
|
|
152
|
+
rolePlayConfig: rolePlayConfig || null,
|
|
153
153
|
usage: {
|
|
154
154
|
inputTokens: 0,
|
|
155
155
|
outputTokens: 0,
|
|
@@ -159,18 +159,18 @@ export async function createConversation(msg) {
|
|
|
159
159
|
}
|
|
160
160
|
});
|
|
161
161
|
|
|
162
|
-
// Register in
|
|
163
|
-
if (
|
|
164
|
-
|
|
165
|
-
roles:
|
|
166
|
-
teamType:
|
|
167
|
-
language:
|
|
162
|
+
// Register in rolePlaySessions for type inference in sendConversationList
|
|
163
|
+
if (rolePlayConfig) {
|
|
164
|
+
rolePlaySessions.set(conversationId, {
|
|
165
|
+
roles: rolePlayConfig.roles,
|
|
166
|
+
teamType: rolePlayConfig.teamType,
|
|
167
|
+
language: rolePlayConfig.language,
|
|
168
168
|
projectDir: effectiveWorkDir,
|
|
169
169
|
createdAt: Date.now(),
|
|
170
170
|
userId,
|
|
171
171
|
username,
|
|
172
172
|
});
|
|
173
|
-
|
|
173
|
+
saveRolePlayIndex();
|
|
174
174
|
}
|
|
175
175
|
|
|
176
176
|
ctx.sendToServer({
|
|
@@ -180,7 +180,7 @@ export async function createConversation(msg) {
|
|
|
180
180
|
userId,
|
|
181
181
|
username,
|
|
182
182
|
disallowedTools: disallowedTools || null,
|
|
183
|
-
|
|
183
|
+
rolePlayConfig: rolePlayConfig || null
|
|
184
184
|
});
|
|
185
185
|
|
|
186
186
|
// 立即发送 agent 级别的 MCP servers 列表(从 ~/.claude.json 读取的)
|
|
@@ -232,10 +232,10 @@ export async function resumeConversation(msg) {
|
|
|
232
232
|
|
|
233
233
|
// 只创建 conversation 状态并保存 claudeSessionId,不启动 Claude 进程
|
|
234
234
|
// Claude 进程会在用户发送第一条消息时启动 (见 handleUserInput)
|
|
235
|
-
// Restore
|
|
236
|
-
const
|
|
237
|
-
const
|
|
238
|
-
? { roles:
|
|
235
|
+
// Restore rolePlayConfig from persisted rolePlaySessions if available
|
|
236
|
+
const rolePlayEntry = rolePlaySessions.get(conversationId);
|
|
237
|
+
const rolePlayConfig = rolePlayEntry
|
|
238
|
+
? { roles: rolePlayEntry.roles, teamType: rolePlayEntry.teamType, language: rolePlayEntry.language }
|
|
239
239
|
: null;
|
|
240
240
|
|
|
241
241
|
ctx.conversations.set(conversationId, {
|
|
@@ -251,7 +251,7 @@ export async function resumeConversation(msg) {
|
|
|
251
251
|
userId,
|
|
252
252
|
username,
|
|
253
253
|
disallowedTools: disallowedTools || null, // null = 使用全局默认
|
|
254
|
-
|
|
254
|
+
rolePlayConfig,
|
|
255
255
|
usage: {
|
|
256
256
|
inputTokens: 0,
|
|
257
257
|
outputTokens: 0,
|
|
@@ -317,9 +317,9 @@ export function deleteConversation(msg) {
|
|
|
317
317
|
ctx.conversations.delete(conversationId);
|
|
318
318
|
}
|
|
319
319
|
|
|
320
|
-
// Clean up
|
|
321
|
-
if (
|
|
322
|
-
|
|
320
|
+
// Clean up roleplay session if applicable
|
|
321
|
+
if (rolePlaySessions.has(conversationId)) {
|
|
322
|
+
removeRolePlaySession(conversationId);
|
|
323
323
|
}
|
|
324
324
|
|
|
325
325
|
ctx.sendToServer({
|
package/package.json
CHANGED
package/{vcrew.js → roleplay.js}
RENAMED
|
@@ -1,52 +1,68 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Role Play — lightweight multi-role collaboration within a single conversation.
|
|
3
3
|
*
|
|
4
|
-
* Manages
|
|
4
|
+
* Manages rolePlaySessions (in-memory + persisted to disk) and builds the
|
|
5
5
|
* appendSystemPrompt that instructs Claude to role-play multiple characters.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { join } from 'path';
|
|
9
9
|
import { homedir } from 'os';
|
|
10
|
-
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
10
|
+
import { readFileSync, writeFileSync, existsSync, renameSync } from 'fs';
|
|
11
11
|
|
|
12
|
-
const
|
|
12
|
+
const ROLEPLAY_INDEX_PATH = join(homedir(), '.claude', 'roleplay-sessions.json');
|
|
13
|
+
// ★ backward compat: old filename before rename
|
|
14
|
+
const LEGACY_INDEX_PATH = join(homedir(), '.claude', 'vcrew-sessions.json');
|
|
13
15
|
|
|
14
16
|
// In-memory map: conversationId -> { roles, teamType, language, projectDir, createdAt, userId, username }
|
|
15
|
-
export const
|
|
17
|
+
export const rolePlaySessions = new Map();
|
|
16
18
|
|
|
17
19
|
// ---------------------------------------------------------------------------
|
|
18
20
|
// Persistence
|
|
19
21
|
// ---------------------------------------------------------------------------
|
|
20
22
|
|
|
21
|
-
export function
|
|
23
|
+
export function saveRolePlayIndex() {
|
|
22
24
|
const data = [];
|
|
23
|
-
for (const [id, session] of
|
|
25
|
+
for (const [id, session] of rolePlaySessions) {
|
|
24
26
|
data.push({ id, ...session });
|
|
25
27
|
}
|
|
26
28
|
try {
|
|
27
|
-
writeFileSync(
|
|
29
|
+
writeFileSync(ROLEPLAY_INDEX_PATH, JSON.stringify(data, null, 2));
|
|
28
30
|
} catch (e) {
|
|
29
|
-
console.warn('[
|
|
31
|
+
console.warn('[roleplay] Failed to save index:', e.message);
|
|
30
32
|
}
|
|
31
33
|
}
|
|
32
34
|
|
|
33
|
-
export function
|
|
34
|
-
|
|
35
|
+
export function loadRolePlayIndex() {
|
|
36
|
+
let indexPath = ROLEPLAY_INDEX_PATH;
|
|
37
|
+
|
|
38
|
+
// ★ backward compat: migrate old vcrew-sessions.json → roleplay-sessions.json
|
|
39
|
+
if (!existsSync(indexPath) && existsSync(LEGACY_INDEX_PATH)) {
|
|
40
|
+
try {
|
|
41
|
+
renameSync(LEGACY_INDEX_PATH, indexPath);
|
|
42
|
+
console.log('[roleplay] Migrated vcrew-sessions.json → roleplay-sessions.json');
|
|
43
|
+
} catch (e) {
|
|
44
|
+
// rename failed (e.g. permissions), fall back to reading old file directly
|
|
45
|
+
console.warn('[roleplay] Could not rename legacy index, reading in-place:', e.message);
|
|
46
|
+
indexPath = LEGACY_INDEX_PATH;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!existsSync(indexPath)) return;
|
|
35
51
|
try {
|
|
36
|
-
const data = JSON.parse(readFileSync(
|
|
52
|
+
const data = JSON.parse(readFileSync(indexPath, 'utf-8'));
|
|
37
53
|
for (const entry of data) {
|
|
38
54
|
const { id, ...session } = entry;
|
|
39
|
-
|
|
55
|
+
rolePlaySessions.set(id, session);
|
|
40
56
|
}
|
|
41
|
-
console.log(`[
|
|
57
|
+
console.log(`[roleplay] Loaded ${rolePlaySessions.size} sessions from index`);
|
|
42
58
|
} catch (e) {
|
|
43
|
-
console.warn('[
|
|
59
|
+
console.warn('[roleplay] Failed to load index:', e.message);
|
|
44
60
|
}
|
|
45
61
|
}
|
|
46
62
|
|
|
47
|
-
export function
|
|
48
|
-
|
|
49
|
-
|
|
63
|
+
export function removeRolePlaySession(conversationId) {
|
|
64
|
+
rolePlaySessions.delete(conversationId);
|
|
65
|
+
saveRolePlayIndex();
|
|
50
66
|
}
|
|
51
67
|
|
|
52
68
|
// ---------------------------------------------------------------------------
|
|
@@ -60,15 +76,15 @@ const MAX_CLAUDE_MD_LEN = 4096;
|
|
|
60
76
|
const MAX_ROLES = 10;
|
|
61
77
|
|
|
62
78
|
/**
|
|
63
|
-
* Validate and sanitize
|
|
79
|
+
* Validate and sanitize rolePlayConfig from the client.
|
|
64
80
|
* Returns { valid: true, config: sanitizedConfig } or { valid: false, error: string }.
|
|
65
81
|
*
|
|
66
|
-
* @param {*} config - raw
|
|
82
|
+
* @param {*} config - raw rolePlayConfig from client message
|
|
67
83
|
* @returns {{ valid: boolean, config?: object, error?: string }}
|
|
68
84
|
*/
|
|
69
|
-
export function
|
|
85
|
+
export function validateRolePlayConfig(config) {
|
|
70
86
|
if (!config || typeof config !== 'object') {
|
|
71
|
-
return { valid: false, error: '
|
|
87
|
+
return { valid: false, error: 'rolePlayConfig must be an object' };
|
|
72
88
|
}
|
|
73
89
|
|
|
74
90
|
// teamType
|
|
@@ -141,13 +157,13 @@ export function validateVCrewConfig(config) {
|
|
|
141
157
|
// ---------------------------------------------------------------------------
|
|
142
158
|
|
|
143
159
|
/**
|
|
144
|
-
* Build the appendSystemPrompt that tells Claude about the
|
|
160
|
+
* Build the appendSystemPrompt that tells Claude about the role play roles
|
|
145
161
|
* and how to switch between them.
|
|
146
162
|
*
|
|
147
163
|
* @param {{ roles: Array, teamType: string, language: string }} config
|
|
148
164
|
* @returns {string}
|
|
149
165
|
*/
|
|
150
|
-
export function
|
|
166
|
+
export function buildRolePlaySystemPrompt(config) {
|
|
151
167
|
const { roles, teamType, language } = config;
|
|
152
168
|
const isZh = language === 'zh-CN';
|
|
153
169
|
|