@co0ontty/wand 1.6.1 → 1.6.2
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/dist/config.js +23 -0
- package/dist/server.js +137 -14
- package/dist/structured-session-manager.js +8 -6
- package/dist/types.d.ts +9 -0
- package/dist/web-ui/content/scripts.js +43 -5
- package/dist/web-ui/content/styles.css +7 -0
- package/package.json +1 -1
package/dist/config.js
CHANGED
|
@@ -79,6 +79,28 @@ export async function saveConfig(configPath, config) {
|
|
|
79
79
|
await mkdir(path.dirname(configPath), { recursive: true });
|
|
80
80
|
await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
81
81
|
}
|
|
82
|
+
function normalizeStructuredChatPersona(input) {
|
|
83
|
+
if (!input || typeof input !== "object")
|
|
84
|
+
return undefined;
|
|
85
|
+
const normalizeRole = (roleInput) => {
|
|
86
|
+
if (!roleInput || typeof roleInput !== "object")
|
|
87
|
+
return undefined;
|
|
88
|
+
const role = roleInput;
|
|
89
|
+
const normalized = {
|
|
90
|
+
name: typeof role.name === "string" ? role.name.trim() : undefined,
|
|
91
|
+
avatar: typeof role.avatar === "string" ? role.avatar.trim() : undefined,
|
|
92
|
+
};
|
|
93
|
+
if (!normalized.name && !normalized.avatar)
|
|
94
|
+
return undefined;
|
|
95
|
+
return normalized;
|
|
96
|
+
};
|
|
97
|
+
const personaInput = input;
|
|
98
|
+
const user = normalizeRole(personaInput.user);
|
|
99
|
+
const assistant = normalizeRole(personaInput.assistant);
|
|
100
|
+
if (!user && !assistant)
|
|
101
|
+
return undefined;
|
|
102
|
+
return { user, assistant };
|
|
103
|
+
}
|
|
82
104
|
function mergeWithDefaults(input) {
|
|
83
105
|
const defaults = defaultConfig();
|
|
84
106
|
return {
|
|
@@ -108,6 +130,7 @@ function mergeWithDefaults(input) {
|
|
|
108
130
|
mode: isExecutionMode(preset.mode) ? preset.mode : undefined
|
|
109
131
|
}))
|
|
110
132
|
: defaults.commandPresets,
|
|
133
|
+
structuredChatPersona: normalizeStructuredChatPersona(input.structuredChatPersona),
|
|
111
134
|
language: typeof input.language === "string" ? input.language.trim() : defaults.language,
|
|
112
135
|
};
|
|
113
136
|
}
|
package/dist/server.js
CHANGED
|
@@ -8,6 +8,19 @@ import { promisify } from "node:util";
|
|
|
8
8
|
import path from "node:path";
|
|
9
9
|
import process from "node:process";
|
|
10
10
|
import { WebSocketServer } from "ws";
|
|
11
|
+
import { ensureAvatarSeed, getAvatarSvg } from "./avatar.js";
|
|
12
|
+
import { createSession, revokeSession, setAuthStorage, validateSession } from "./auth.js";
|
|
13
|
+
import { ensureCertificates } from "./cert.js";
|
|
14
|
+
import { isExecutionMode, resolveConfigDir, saveConfig } from "./config.js";
|
|
15
|
+
import { ProcessManager } from "./process-manager.js";
|
|
16
|
+
import { StructuredSessionManager } from "./structured-session-manager.js";
|
|
17
|
+
import { generatePwaManifest, generateServiceWorker } from "./pwa.js";
|
|
18
|
+
import { getErrorMessage, registerClaudeHistoryRoutes, registerSessionRoutes } from "./server-session-routes.js";
|
|
19
|
+
import { resolveDatabasePath, WandStorage } from "./storage.js";
|
|
20
|
+
import { renderApp } from "./web-ui/index.js";
|
|
21
|
+
import { WsBroadcastManager } from "./ws-broadcast.js";
|
|
22
|
+
import { checkRateLimit, recordFailedLogin, resetRateLimit } from "./middleware/rate-limit.js";
|
|
23
|
+
import { isPathWithinBase, isBlockedFolderPath, normalizeFolderPath } from "./middleware/path-safety.js";
|
|
11
24
|
const execAsync = promisify(exec);
|
|
12
25
|
const SERVER_MODULE_DIR = path.dirname(new URL(import.meta.url).pathname);
|
|
13
26
|
const RUNTIME_ROOT_DIR = path.resolve(SERVER_MODULE_DIR, "..");
|
|
@@ -52,19 +65,83 @@ function compareSemver(a, b) {
|
|
|
52
65
|
}
|
|
53
66
|
return 0;
|
|
54
67
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
+
function isExternalAvatarSource(value) {
|
|
69
|
+
return /^(https?:|data:)/i.test(value);
|
|
70
|
+
}
|
|
71
|
+
function normalizePersonaName(value) {
|
|
72
|
+
if (typeof value !== "string")
|
|
73
|
+
return undefined;
|
|
74
|
+
const trimmed = value.trim();
|
|
75
|
+
return trimmed || undefined;
|
|
76
|
+
}
|
|
77
|
+
function normalizePersonaAvatar(value) {
|
|
78
|
+
if (typeof value !== "string")
|
|
79
|
+
return undefined;
|
|
80
|
+
const trimmed = value.trim();
|
|
81
|
+
return trimmed || undefined;
|
|
82
|
+
}
|
|
83
|
+
function resolveStructuredChatPersona(config) {
|
|
84
|
+
const persona = config.structuredChatPersona;
|
|
85
|
+
if (!persona)
|
|
86
|
+
return undefined;
|
|
87
|
+
const userName = normalizePersonaName(persona.user?.name);
|
|
88
|
+
const userAvatar = normalizePersonaAvatar(persona.user?.avatar);
|
|
89
|
+
const assistantName = normalizePersonaName(persona.assistant?.name);
|
|
90
|
+
const assistantAvatar = normalizePersonaAvatar(persona.assistant?.avatar);
|
|
91
|
+
if (!userName && !userAvatar && !assistantName && !assistantAvatar) {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
user: userName || userAvatar ? { name: userName, avatar: userAvatar } : undefined,
|
|
96
|
+
assistant: assistantName || assistantAvatar ? { name: assistantName, avatar: assistantAvatar } : undefined,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function resolveStructuredChatAvatarPath(configPath, config, role) {
|
|
100
|
+
const avatar = role === "user"
|
|
101
|
+
? config.structuredChatPersona?.user?.avatar
|
|
102
|
+
: config.structuredChatPersona?.assistant?.avatar;
|
|
103
|
+
if (!avatar || isExternalAvatarSource(avatar)) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
const configDir = resolveConfigDir(configPath);
|
|
107
|
+
return path.isAbsolute(avatar) ? avatar : path.resolve(configDir, avatar);
|
|
108
|
+
}
|
|
109
|
+
async function buildStructuredChatPersonaPayload(configPath, config) {
|
|
110
|
+
const persona = resolveStructuredChatPersona(config);
|
|
111
|
+
if (!persona)
|
|
112
|
+
return undefined;
|
|
113
|
+
const buildRole = async (role) => {
|
|
114
|
+
const roleConfig = role === "user" ? persona.user : persona.assistant;
|
|
115
|
+
if (!roleConfig)
|
|
116
|
+
return undefined;
|
|
117
|
+
let avatar = roleConfig.avatar;
|
|
118
|
+
if (avatar && !isExternalAvatarSource(avatar)) {
|
|
119
|
+
const resolvedPath = resolveStructuredChatAvatarPath(configPath, config, role);
|
|
120
|
+
if (!resolvedPath) {
|
|
121
|
+
avatar = undefined;
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
try {
|
|
125
|
+
const fileStat = await stat(resolvedPath);
|
|
126
|
+
avatar = fileStat.isFile() ? `/api/structured-chat-avatar/${role}` : undefined;
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
avatar = undefined;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (!roleConfig.name && !avatar)
|
|
134
|
+
return undefined;
|
|
135
|
+
return {
|
|
136
|
+
name: roleConfig.name,
|
|
137
|
+
avatar,
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
const [user, assistant] = await Promise.all([buildRole("user"), buildRole("assistant")]);
|
|
141
|
+
if (!user && !assistant)
|
|
142
|
+
return undefined;
|
|
143
|
+
return { user, assistant };
|
|
144
|
+
}
|
|
68
145
|
// ── Git helpers ──
|
|
69
146
|
async function isGitRepo(dirPath) {
|
|
70
147
|
try {
|
|
@@ -266,6 +343,50 @@ export async function startServer(config, configPath) {
|
|
|
266
343
|
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
|
267
344
|
res.type("html").send(renderApp(configPath));
|
|
268
345
|
});
|
|
346
|
+
app.get("/api/structured-chat-avatar/:role", async (req, res) => {
|
|
347
|
+
const role = req.params.role === "user" || req.params.role === "assistant"
|
|
348
|
+
? req.params.role
|
|
349
|
+
: null;
|
|
350
|
+
if (!role) {
|
|
351
|
+
res.status(404).end();
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
const resolvedPath = resolveStructuredChatAvatarPath(configPath, config, role);
|
|
355
|
+
if (!resolvedPath) {
|
|
356
|
+
res.status(404).end();
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
try {
|
|
360
|
+
const fileStat = await stat(resolvedPath);
|
|
361
|
+
if (!fileStat.isFile()) {
|
|
362
|
+
res.status(404).end();
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
const ext = path.extname(resolvedPath).toLowerCase();
|
|
366
|
+
const contentType = ext === ".svg"
|
|
367
|
+
? "image/svg+xml"
|
|
368
|
+
: ext === ".png"
|
|
369
|
+
? "image/png"
|
|
370
|
+
: ext === ".jpg" || ext === ".jpeg"
|
|
371
|
+
? "image/jpeg"
|
|
372
|
+
: ext === ".webp"
|
|
373
|
+
? "image/webp"
|
|
374
|
+
: ext === ".gif"
|
|
375
|
+
? "image/gif"
|
|
376
|
+
: ext === ".avif"
|
|
377
|
+
? "image/avif"
|
|
378
|
+
: null;
|
|
379
|
+
if (!contentType) {
|
|
380
|
+
res.status(415).json({ error: "不支持的头像格式。" });
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
384
|
+
res.type(contentType).sendFile(resolvedPath);
|
|
385
|
+
}
|
|
386
|
+
catch {
|
|
387
|
+
res.status(404).end();
|
|
388
|
+
}
|
|
389
|
+
});
|
|
269
390
|
app.get("/manifest.json", (_req, res) => {
|
|
270
391
|
res.setHeader("Content-Type", "application/manifest+json");
|
|
271
392
|
res.send(generatePwaManifest());
|
|
@@ -328,7 +449,8 @@ export async function startServer(config, configPath) {
|
|
|
328
449
|
});
|
|
329
450
|
app.use("/api", requireAuth);
|
|
330
451
|
// ── Config & Session info ──
|
|
331
|
-
app.get("/api/config", (_req, res) => {
|
|
452
|
+
app.get("/api/config", async (_req, res) => {
|
|
453
|
+
const structuredChatPersona = await buildStructuredChatPersonaPayload(configPath, config);
|
|
332
454
|
res.json({
|
|
333
455
|
host: config.host,
|
|
334
456
|
port: config.port,
|
|
@@ -336,6 +458,7 @@ export async function startServer(config, configPath) {
|
|
|
336
458
|
defaultCwd: config.defaultCwd,
|
|
337
459
|
commandPresets: config.commandPresets,
|
|
338
460
|
structuredRunners: [{ label: "Claude Structured", runner: "claude-cli-print" }],
|
|
461
|
+
structuredChatPersona,
|
|
339
462
|
updateAvailable: cachedUpdateInfo?.updateAvailable ?? false,
|
|
340
463
|
latestVersion: cachedUpdateInfo?.latest ?? null,
|
|
341
464
|
currentVersion: PKG_VERSION,
|
|
@@ -345,19 +345,21 @@ export class StructuredSessionManager {
|
|
|
345
345
|
// ---------------------------------------------------------------------------
|
|
346
346
|
// CLI argument construction
|
|
347
347
|
// ---------------------------------------------------------------------------
|
|
348
|
-
buildPermissionArgs(mode) {
|
|
348
|
+
buildPermissionArgs(mode, autoApprove) {
|
|
349
|
+
const shouldBypass = autoApprove || mode === "full-access" || mode === "managed";
|
|
350
|
+
const shouldAcceptEdits = mode === "auto-edit";
|
|
349
351
|
if (!isRunningAsRoot()) {
|
|
350
|
-
if (
|
|
352
|
+
if (shouldBypass) {
|
|
351
353
|
return ["--permission-mode", "bypassPermissions"];
|
|
352
354
|
}
|
|
353
|
-
if (
|
|
355
|
+
if (shouldAcceptEdits) {
|
|
354
356
|
return ["--permission-mode", "acceptEdits"];
|
|
355
357
|
}
|
|
356
358
|
return [];
|
|
357
359
|
}
|
|
358
360
|
// Root: Claude CLI refuses bypassPermissions.
|
|
359
361
|
// acceptEdits auto-approves within CWD; --allowedTools extends to all paths.
|
|
360
|
-
if (
|
|
362
|
+
if (shouldBypass || shouldAcceptEdits) {
|
|
361
363
|
return [
|
|
362
364
|
"--permission-mode", "acceptEdits",
|
|
363
365
|
"--allowedTools", "Bash", "Edit", "Write", "Read", "Glob", "Grep",
|
|
@@ -384,8 +386,8 @@ export class StructuredSessionManager {
|
|
|
384
386
|
return new Promise((resolve, reject) => {
|
|
385
387
|
const args = ["-p", "--verbose", "--output-format", "stream-json"];
|
|
386
388
|
console.log("[WAND] runClaudeStreaming sessionId:", sessionId, "mode:", session.mode, "claudeSessionId:", session.claudeSessionId);
|
|
387
|
-
// Add permission args based on mode
|
|
388
|
-
const permArgs = this.buildPermissionArgs(session.mode);
|
|
389
|
+
// Add permission args based on mode + autoApprovePermissions toggle
|
|
390
|
+
const permArgs = this.buildPermissionArgs(session.mode, session.autoApprovePermissions ?? false);
|
|
389
391
|
args.push(...permArgs);
|
|
390
392
|
// In managed mode, append autonomous system prompt
|
|
391
393
|
if (session.mode === "managed") {
|
package/dist/types.d.ts
CHANGED
|
@@ -38,6 +38,14 @@ export interface CommandPreset {
|
|
|
38
38
|
command: string;
|
|
39
39
|
mode?: ExecutionMode;
|
|
40
40
|
}
|
|
41
|
+
export interface StructuredChatPersonaRoleConfig {
|
|
42
|
+
name?: string;
|
|
43
|
+
avatar?: string;
|
|
44
|
+
}
|
|
45
|
+
export interface StructuredChatPersonaConfig {
|
|
46
|
+
user?: StructuredChatPersonaRoleConfig;
|
|
47
|
+
assistant?: StructuredChatPersonaRoleConfig;
|
|
48
|
+
}
|
|
41
49
|
export interface WandConfig {
|
|
42
50
|
host: string;
|
|
43
51
|
port: number;
|
|
@@ -50,6 +58,7 @@ export interface WandConfig {
|
|
|
50
58
|
startupCommands: string[];
|
|
51
59
|
allowedCommandPrefixes: string[];
|
|
52
60
|
commandPresets: CommandPreset[];
|
|
61
|
+
structuredChatPersona?: StructuredChatPersonaConfig;
|
|
53
62
|
/** Max total size (bytes) for shortcut interaction logs per session (default: 10 MB). Set 0 to disable logging. */
|
|
54
63
|
shortcutLogMaxBytes?: number;
|
|
55
64
|
/** Preferred response language for Claude (e.g. "中文", "English"). Empty string means no override. */
|
|
@@ -9318,13 +9318,51 @@
|
|
|
9318
9318
|
};
|
|
9319
9319
|
})();
|
|
9320
9320
|
|
|
9321
|
+
var DEFAULT_CHAT_PERSONA = {
|
|
9322
|
+
user: {
|
|
9323
|
+
name: "赛博虎妞",
|
|
9324
|
+
avatarSvg: PIXEL_AVATAR.user
|
|
9325
|
+
},
|
|
9326
|
+
assistant: {
|
|
9327
|
+
name: "勤劳初二",
|
|
9328
|
+
avatarSvg: PIXEL_AVATAR.assistant
|
|
9329
|
+
}
|
|
9330
|
+
};
|
|
9331
|
+
|
|
9332
|
+
function getStructuredChatPersona(role) {
|
|
9333
|
+
var configPersona = state.config && state.config.structuredChatPersona;
|
|
9334
|
+
var roleConfig = configPersona && configPersona[role] ? configPersona[role] : null;
|
|
9335
|
+
var defaults = DEFAULT_CHAT_PERSONA[role] || DEFAULT_CHAT_PERSONA.assistant;
|
|
9336
|
+
return {
|
|
9337
|
+
name: roleConfig && typeof roleConfig.name === "string" && roleConfig.name.trim()
|
|
9338
|
+
? roleConfig.name.trim()
|
|
9339
|
+
: defaults.name,
|
|
9340
|
+
avatar: roleConfig && typeof roleConfig.avatar === "string" && roleConfig.avatar.trim()
|
|
9341
|
+
? roleConfig.avatar.trim()
|
|
9342
|
+
: null,
|
|
9343
|
+
avatarSvg: defaults.avatarSvg
|
|
9344
|
+
};
|
|
9345
|
+
}
|
|
9346
|
+
|
|
9347
|
+
function renderAvatarFallback(svg) {
|
|
9348
|
+
return '<div class="pixel-avatar">' + svg + '</div>';
|
|
9349
|
+
}
|
|
9350
|
+
|
|
9351
|
+
function handleChatAvatarImageError(img, role) {
|
|
9352
|
+
if (!img || !img.parentNode) return;
|
|
9353
|
+
var persona = getStructuredChatPersona(role === "user" ? "user" : "assistant");
|
|
9354
|
+
img.outerHTML = renderAvatarFallback(persona.avatarSvg);
|
|
9355
|
+
}
|
|
9356
|
+
|
|
9321
9357
|
function chatAvatar(role) {
|
|
9322
|
-
var
|
|
9323
|
-
var
|
|
9324
|
-
var
|
|
9358
|
+
var personaRole = role === "user" ? "user" : "assistant";
|
|
9359
|
+
var persona = getStructuredChatPersona(personaRole);
|
|
9360
|
+
var avatarInner = persona.avatar
|
|
9361
|
+
? '<img class="pixel-avatar-image" src="' + escapeHtml(persona.avatar) + '" alt="' + escapeHtml(persona.name) + '" onerror="handleChatAvatarImageError(this, ' + JSON.stringify(personaRole) + ')" />'
|
|
9362
|
+
: renderAvatarFallback(persona.avatarSvg);
|
|
9325
9363
|
return '<div class="chat-message-avatar ' + role + '">' +
|
|
9326
|
-
|
|
9327
|
-
'<span class="avatar-name">' + name + '</span>' +
|
|
9364
|
+
avatarInner +
|
|
9365
|
+
'<span class="avatar-name">' + escapeHtml(persona.name) + '</span>' +
|
|
9328
9366
|
'</div>';
|
|
9329
9367
|
}
|
|
9330
9368
|
|
|
@@ -2328,6 +2328,13 @@
|
|
|
2328
2328
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
|
|
2329
2329
|
}
|
|
2330
2330
|
|
|
2331
|
+
.pixel-avatar-image {
|
|
2332
|
+
display: block;
|
|
2333
|
+
width: 100%;
|
|
2334
|
+
height: 100%;
|
|
2335
|
+
object-fit: cover;
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2331
2338
|
.pixel-avatar-svg {
|
|
2332
2339
|
display: block;
|
|
2333
2340
|
width: 100%;
|