@co0ontty/wand 1.15.0 → 1.17.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/claude-pty-bridge.d.ts +3 -0
- package/dist/claude-pty-bridge.js +35 -1
- package/dist/config.js +2 -0
- package/dist/models.d.ts +13 -0
- package/dist/models.js +54 -0
- package/dist/process-manager.d.ts +7 -0
- package/dist/process-manager.js +67 -19
- package/dist/pty-text-utils.d.ts +7 -0
- package/dist/pty-text-utils.js +14 -0
- package/dist/pwa.js +11 -6
- package/dist/server-session-routes.js +25 -1
- package/dist/server.js +125 -16
- package/dist/structured-session-manager.d.ts +4 -0
- package/dist/structured-session-manager.js +51 -5
- package/dist/types.d.ts +16 -0
- package/dist/upload-routes.d.ts +3 -0
- package/dist/upload-routes.js +53 -0
- package/dist/web-ui/content/scripts.js +828 -516
- package/dist/web-ui/content/styles.css +293 -119
- package/dist/web-ui/content/vendor/wterm/terminal.css +162 -0
- package/dist/web-ui/content/vendor/wterm/wterm.bundle.js +1 -0
- package/dist/web-ui/index.js +2 -4
- package/dist/ws-broadcast.js +12 -7
- package/package.json +8 -5
package/dist/server.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
|
+
import compression from "compression";
|
|
2
3
|
import express from "express";
|
|
3
4
|
import { createReadStream, existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
5
|
import { mkdir, readdir, readFile, stat } from "node:fs/promises";
|
|
@@ -13,10 +14,12 @@ import { ensureAvatarSeed, getAvatarSvg } from "./avatar.js";
|
|
|
13
14
|
import { createSession, revokeSession, setAuthStorage, validateSession } from "./auth.js";
|
|
14
15
|
import { ensureCertificates } from "./cert.js";
|
|
15
16
|
import { isExecutionMode, normalizeCardDefaults, resolveConfigDir, saveConfig } from "./config.js";
|
|
17
|
+
import { getCachedModels, refreshModels } from "./models.js";
|
|
16
18
|
import { ProcessManager } from "./process-manager.js";
|
|
17
19
|
import { StructuredSessionManager } from "./structured-session-manager.js";
|
|
18
20
|
import { generatePwaManifest, generateServiceWorker } from "./pwa.js";
|
|
19
21
|
import { getErrorMessage, registerClaudeHistoryRoutes, registerSessionRoutes } from "./server-session-routes.js";
|
|
22
|
+
import { registerUploadRoutes } from "./upload-routes.js";
|
|
20
23
|
import { resolveDatabasePath, WandStorage } from "./storage.js";
|
|
21
24
|
import { renderApp } from "./web-ui/index.js";
|
|
22
25
|
import { WsBroadcastManager } from "./ws-broadcast.js";
|
|
@@ -485,9 +488,12 @@ export async function startServer(config, configPath) {
|
|
|
485
488
|
const protocol = useHttps ? "https" : "http";
|
|
486
489
|
const nodeModulesDir = path.join(RUNTIME_ROOT_DIR, "node_modules");
|
|
487
490
|
app.use(express.json({ limit: "1mb" }));
|
|
488
|
-
app.use(
|
|
489
|
-
|
|
490
|
-
|
|
491
|
+
app.use(compression({ threshold: 1024 }));
|
|
492
|
+
const vendorCacheOpts = { maxAge: "7d", immutable: true };
|
|
493
|
+
const contentDir = existsSync(path.join(SERVER_MODULE_DIR, "web-ui", "content"))
|
|
494
|
+
? path.join(SERVER_MODULE_DIR, "web-ui", "content")
|
|
495
|
+
: path.join(RUNTIME_ROOT_DIR, "src", "web-ui", "content");
|
|
496
|
+
app.use("/vendor/wterm", express.static(path.join(contentDir, "vendor", "wterm"), vendorCacheOpts));
|
|
491
497
|
// ── Web UI and PWA endpoints ──
|
|
492
498
|
app.get("/", (_req, res) => {
|
|
493
499
|
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
|
@@ -691,6 +697,10 @@ export async function startServer(config, configPath) {
|
|
|
691
697
|
hasCert: existsSync(certPaths.keyPath) && existsSync(certPaths.certPath),
|
|
692
698
|
updateAvailable: cachedUpdateInfo?.updateAvailable ?? false,
|
|
693
699
|
latestVersion: cachedUpdateInfo?.latest ?? null,
|
|
700
|
+
autoUpdate: {
|
|
701
|
+
web: storage.getConfigValue("autoUpdateWeb") === "true",
|
|
702
|
+
apk: storage.getConfigValue("autoUpdateApk") === "true",
|
|
703
|
+
},
|
|
694
704
|
androidApk: {
|
|
695
705
|
enabled: config.android?.enabled === true,
|
|
696
706
|
apkDir,
|
|
@@ -742,7 +752,7 @@ export async function startServer(config, configPath) {
|
|
|
742
752
|
});
|
|
743
753
|
app.post("/api/settings/config", async (req, res) => {
|
|
744
754
|
const body = req.body;
|
|
745
|
-
const allowedFields = ["host", "port", "https", "defaultMode", "defaultCwd", "shell", "language"];
|
|
755
|
+
const allowedFields = ["host", "port", "https", "defaultMode", "defaultCwd", "shell", "language", "defaultModel"];
|
|
746
756
|
let changed = false;
|
|
747
757
|
for (const field of allowedFields) {
|
|
748
758
|
if (field in body && body[field] !== undefined) {
|
|
@@ -776,6 +786,9 @@ export async function startServer(config, configPath) {
|
|
|
776
786
|
else if (field === "language") {
|
|
777
787
|
config.language = typeof body.language === "string" ? body.language.trim() : "";
|
|
778
788
|
}
|
|
789
|
+
else if (field === "defaultModel") {
|
|
790
|
+
config.defaultModel = typeof body.defaultModel === "string" ? body.defaultModel.trim() : "";
|
|
791
|
+
}
|
|
779
792
|
changed = true;
|
|
780
793
|
}
|
|
781
794
|
}
|
|
@@ -799,6 +812,29 @@ export async function startServer(config, configPath) {
|
|
|
799
812
|
res.status(500).json({ error: getErrorMessage(error, "保存配置失败。") });
|
|
800
813
|
}
|
|
801
814
|
});
|
|
815
|
+
app.get("/api/models", (_req, res) => {
|
|
816
|
+
const cached = getCachedModels();
|
|
817
|
+
res.json({
|
|
818
|
+
models: cached.models,
|
|
819
|
+
claudeVersion: cached.claudeVersion,
|
|
820
|
+
refreshedAt: cached.refreshedAt,
|
|
821
|
+
defaultModel: config.defaultModel ?? "",
|
|
822
|
+
});
|
|
823
|
+
});
|
|
824
|
+
app.post("/api/models/refresh", async (_req, res) => {
|
|
825
|
+
try {
|
|
826
|
+
const refreshed = await refreshModels();
|
|
827
|
+
res.json({
|
|
828
|
+
models: refreshed.models,
|
|
829
|
+
claudeVersion: refreshed.claudeVersion,
|
|
830
|
+
refreshedAt: refreshed.refreshedAt,
|
|
831
|
+
defaultModel: config.defaultModel ?? "",
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
catch (error) {
|
|
835
|
+
res.status(500).json({ error: getErrorMessage(error, "刷新模型列表失败。") });
|
|
836
|
+
}
|
|
837
|
+
});
|
|
802
838
|
app.post("/api/settings/upload-cert", async (req, res) => {
|
|
803
839
|
const { key, cert } = req.body;
|
|
804
840
|
if (!key || !cert) {
|
|
@@ -854,6 +890,7 @@ export async function startServer(config, configPath) {
|
|
|
854
890
|
});
|
|
855
891
|
registerSessionRoutes(app, processes, structuredSessions, storage, config.defaultMode);
|
|
856
892
|
registerClaudeHistoryRoutes(app, processes, storage);
|
|
893
|
+
registerUploadRoutes(app, processes);
|
|
857
894
|
// ── Path suggestion ──
|
|
858
895
|
app.get("/api/path-suggestions", async (req, res) => {
|
|
859
896
|
const query = typeof req.query.q === "string" ? req.query.q : "";
|
|
@@ -869,10 +906,9 @@ export async function startServer(config, configPath) {
|
|
|
869
906
|
app.get("/api/directory", async (req, res) => {
|
|
870
907
|
const q = typeof req.query.q === "string" ? req.query.q : "";
|
|
871
908
|
const includeGitStatus = req.query.gitStatus === "true";
|
|
872
|
-
const targetPath = path.resolve(
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
res.status(403).json({ error: "访问被拒绝:路径必须在项目目录内。" });
|
|
909
|
+
const targetPath = path.resolve(q || config.defaultCwd);
|
|
910
|
+
if (isBlockedFolderPath(targetPath)) {
|
|
911
|
+
res.status(403).json({ error: "访问被拒绝:无法访问系统敏感目录。" });
|
|
876
912
|
return;
|
|
877
913
|
}
|
|
878
914
|
try {
|
|
@@ -908,8 +944,7 @@ export async function startServer(config, configPath) {
|
|
|
908
944
|
return;
|
|
909
945
|
}
|
|
910
946
|
const resolvedPath = path.resolve(filePath);
|
|
911
|
-
|
|
912
|
-
if (!isPathWithinBase(resolvedPath, allowedBase)) {
|
|
947
|
+
if (isBlockedFolderPath(resolvedPath)) {
|
|
913
948
|
res.status(403).json({ error: "Access denied" });
|
|
914
949
|
return;
|
|
915
950
|
}
|
|
@@ -1125,9 +1160,12 @@ export async function startServer(config, configPath) {
|
|
|
1125
1160
|
}
|
|
1126
1161
|
const initialInput = body.initialInput?.trim();
|
|
1127
1162
|
try {
|
|
1163
|
+
const rawModel = typeof body.model === "string" ? body.model.trim() : "";
|
|
1164
|
+
const effectiveModel = rawModel || (config.defaultModel ?? "").trim() || undefined;
|
|
1128
1165
|
const snapshot = processes.start(body.command, body.cwd, normalizeMode(body.mode, config.defaultMode), initialInput || undefined, {
|
|
1129
1166
|
worktreeEnabled: body.worktreeEnabled === true,
|
|
1130
1167
|
provider: body.provider,
|
|
1168
|
+
model: effectiveModel,
|
|
1131
1169
|
});
|
|
1132
1170
|
res.status(201).json(snapshot);
|
|
1133
1171
|
}
|
|
@@ -1142,7 +1180,15 @@ export async function startServer(config, configPath) {
|
|
|
1142
1180
|
return createHttpsServer({ key: ssl.key, cert: ssl.cert }, app);
|
|
1143
1181
|
})()
|
|
1144
1182
|
: createHttpServer(app);
|
|
1145
|
-
const wss = new WebSocketServer({
|
|
1183
|
+
const wss = new WebSocketServer({
|
|
1184
|
+
server,
|
|
1185
|
+
path: "/ws",
|
|
1186
|
+
perMessageDeflate: {
|
|
1187
|
+
zlibDeflateOptions: { level: 1 },
|
|
1188
|
+
threshold: 512,
|
|
1189
|
+
concurrencyLimit: 10,
|
|
1190
|
+
},
|
|
1191
|
+
});
|
|
1146
1192
|
const wsManager = new WsBroadcastManager(wss, () => config.cardDefaults ?? {});
|
|
1147
1193
|
wsManager.setup((id) => structuredSessions.get(id) ?? processes.get(id));
|
|
1148
1194
|
// Wire process events to WebSocket broadcast
|
|
@@ -1196,17 +1242,80 @@ export async function startServer(config, configPath) {
|
|
|
1196
1242
|
}
|
|
1197
1243
|
// Start configured background sessions after the server is already reachable.
|
|
1198
1244
|
processes.runStartupCommands();
|
|
1199
|
-
//
|
|
1200
|
-
|
|
1245
|
+
// ── Auto-update endpoints ──
|
|
1246
|
+
app.get("/api/auto-update", (_req, res) => {
|
|
1247
|
+
const web = storage.getConfigValue("autoUpdateWeb") === "true";
|
|
1248
|
+
const apk = storage.getConfigValue("autoUpdateApk") === "true";
|
|
1249
|
+
res.json({ web, apk });
|
|
1250
|
+
});
|
|
1251
|
+
app.post("/api/auto-update", express.json(), (req, res) => {
|
|
1252
|
+
const { web, apk } = req.body;
|
|
1253
|
+
if (typeof web === "boolean") {
|
|
1254
|
+
storage.setConfigValue("autoUpdateWeb", String(web));
|
|
1255
|
+
}
|
|
1256
|
+
if (typeof apk === "boolean") {
|
|
1257
|
+
storage.setConfigValue("autoUpdateApk", String(apk));
|
|
1258
|
+
}
|
|
1259
|
+
res.json({
|
|
1260
|
+
web: storage.getConfigValue("autoUpdateWeb") === "true",
|
|
1261
|
+
apk: storage.getConfigValue("autoUpdateApk") === "true",
|
|
1262
|
+
});
|
|
1263
|
+
});
|
|
1264
|
+
// ── Auto-update logic ──
|
|
1265
|
+
async function performAutoUpdate() {
|
|
1266
|
+
const info = await checkNpmLatestVersion(true);
|
|
1201
1267
|
cachedUpdateInfo = info;
|
|
1202
|
-
if (info.updateAvailable)
|
|
1268
|
+
if (!info.updateAvailable)
|
|
1269
|
+
return;
|
|
1270
|
+
const autoEnabled = storage.getConfigValue("autoUpdateWeb") === "true";
|
|
1271
|
+
if (!autoEnabled) {
|
|
1272
|
+
// Not auto-updating, just notify
|
|
1203
1273
|
process.stdout.write(`[wand] 发现新版本 ${info.latest}(当前 ${info.current})。运行 npm install -g ${PKG_NAME}@latest 进行更新。\n`);
|
|
1204
|
-
// Broadcast update notification to all connected WS clients
|
|
1205
1274
|
wsManager.emitEvent({
|
|
1206
1275
|
type: "notification",
|
|
1207
1276
|
sessionId: "__system__",
|
|
1208
1277
|
data: { kind: "update", current: info.current, latest: info.latest },
|
|
1209
1278
|
});
|
|
1279
|
+
return;
|
|
1210
1280
|
}
|
|
1211
|
-
|
|
1281
|
+
// Auto-update: install and restart
|
|
1282
|
+
process.stdout.write(`[wand] 自动更新:正在从 ${info.current} 更新到 ${info.latest}...\n`);
|
|
1283
|
+
wsManager.emitEvent({
|
|
1284
|
+
type: "notification",
|
|
1285
|
+
sessionId: "__system__",
|
|
1286
|
+
data: { kind: "auto-update-start", current: info.current, latest: info.latest },
|
|
1287
|
+
});
|
|
1288
|
+
try {
|
|
1289
|
+
await execAsync(`npm install -g ${PKG_NAME}@latest`, { timeout: 120000 });
|
|
1290
|
+
process.stdout.write(`[wand] 自动更新完成,正在重启...\n`);
|
|
1291
|
+
wsManager.emitEvent({
|
|
1292
|
+
type: "notification",
|
|
1293
|
+
sessionId: "__system__",
|
|
1294
|
+
data: { kind: "auto-update-restart", current: info.current, latest: info.latest },
|
|
1295
|
+
});
|
|
1296
|
+
// Restart after a brief delay
|
|
1297
|
+
setTimeout(() => {
|
|
1298
|
+
wss.clients.forEach((client) => client.close());
|
|
1299
|
+
server.close(() => {
|
|
1300
|
+
spawn(process.execPath, process.argv.slice(1), {
|
|
1301
|
+
detached: true,
|
|
1302
|
+
stdio: "inherit",
|
|
1303
|
+
cwd: process.cwd(),
|
|
1304
|
+
env: process.env,
|
|
1305
|
+
}).unref();
|
|
1306
|
+
process.exit(0);
|
|
1307
|
+
});
|
|
1308
|
+
setTimeout(() => process.exit(0), 5000);
|
|
1309
|
+
}, 1000);
|
|
1310
|
+
}
|
|
1311
|
+
catch (error) {
|
|
1312
|
+
process.stdout.write(`[wand] 自动更新失败: ${getErrorMessage(error, "未知错误")}\n`);
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
// Background update check on startup
|
|
1316
|
+
performAutoUpdate().catch(() => { });
|
|
1317
|
+
// Periodic update check (every 30 minutes)
|
|
1318
|
+
setInterval(() => {
|
|
1319
|
+
performAutoUpdate().catch(() => { });
|
|
1320
|
+
}, 30 * 60 * 1000);
|
|
1212
1321
|
}
|
|
@@ -6,6 +6,8 @@ interface CreateStructuredSessionOptions {
|
|
|
6
6
|
prompt?: string;
|
|
7
7
|
runner?: SessionRunner;
|
|
8
8
|
worktreeEnabled?: boolean;
|
|
9
|
+
/** 用户指定的 Claude 模型(别名或完整 ID)。留空则 spawn 时不加 --model。 */
|
|
10
|
+
model?: string;
|
|
9
11
|
}
|
|
10
12
|
export declare class StructuredSessionManager {
|
|
11
13
|
private readonly storage;
|
|
@@ -25,6 +27,8 @@ export declare class StructuredSessionManager {
|
|
|
25
27
|
approvePermission(sessionId: string): SessionSnapshot;
|
|
26
28
|
/** Deny a pending permission request. */
|
|
27
29
|
denyPermission(sessionId: string): SessionSnapshot;
|
|
30
|
+
/** Update the selected model for a structured session. Takes effect on the next spawn. */
|
|
31
|
+
setSessionModel(sessionId: string, model: string | null): SessionSnapshot;
|
|
28
32
|
/** Toggle auto-approve for the session. */
|
|
29
33
|
toggleAutoApprove(sessionId: string): SessionSnapshot;
|
|
30
34
|
/** Resolve a specific escalation by requestId. */
|
|
@@ -35,6 +35,17 @@ function buildStructuredOutputPayload(snapshot) {
|
|
|
35
35
|
structuredState: snapshot.structuredState,
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
|
+
function buildIncrementalStructuredPayload(snapshot) {
|
|
39
|
+
const messages = snapshot.messages ?? [];
|
|
40
|
+
return {
|
|
41
|
+
incremental: true,
|
|
42
|
+
queuedMessages: snapshot.queuedMessages,
|
|
43
|
+
sessionKind: "structured",
|
|
44
|
+
structuredState: snapshot.structuredState,
|
|
45
|
+
lastMessage: messages.length > 0 ? messages[messages.length - 1] : undefined,
|
|
46
|
+
messageCount: messages.length,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
38
49
|
export class StructuredSessionManager {
|
|
39
50
|
storage;
|
|
40
51
|
config;
|
|
@@ -62,11 +73,12 @@ export class StructuredSessionManager {
|
|
|
62
73
|
structuredState: {
|
|
63
74
|
provider: snapshot.structuredState?.provider ?? snapshot.provider ?? "claude",
|
|
64
75
|
runner: snapshot.runner ?? "claude-cli-print",
|
|
65
|
-
model: snapshot.structuredState?.model,
|
|
76
|
+
model: snapshot.structuredState?.model ?? snapshot.selectedModel ?? undefined,
|
|
66
77
|
lastError: snapshot.structuredState?.lastError ?? null,
|
|
67
78
|
inFlight: false,
|
|
68
79
|
activeRequestId: null,
|
|
69
80
|
},
|
|
81
|
+
selectedModel: snapshot.selectedModel ?? null,
|
|
70
82
|
};
|
|
71
83
|
this.sessions.set(restored.id, restored);
|
|
72
84
|
this.storage.saveSession(restored);
|
|
@@ -101,6 +113,7 @@ export class StructuredSessionManager {
|
|
|
101
113
|
const worktreeSetup = options.worktreeEnabled
|
|
102
114
|
? prepareSessionWorktree({ cwd: options.cwd, sessionId: id })
|
|
103
115
|
: null;
|
|
116
|
+
const selectedModel = options.model?.trim() || null;
|
|
104
117
|
const snapshot = {
|
|
105
118
|
id,
|
|
106
119
|
sessionKind: "structured",
|
|
@@ -124,6 +137,7 @@ export class StructuredSessionManager {
|
|
|
124
137
|
structuredState: {
|
|
125
138
|
provider: "claude",
|
|
126
139
|
runner: options.runner ?? "claude-cli-print",
|
|
140
|
+
model: selectedModel ?? undefined,
|
|
127
141
|
inFlight: false,
|
|
128
142
|
activeRequestId: null,
|
|
129
143
|
lastError: null,
|
|
@@ -131,6 +145,7 @@ export class StructuredSessionManager {
|
|
|
131
145
|
autoRecovered: false,
|
|
132
146
|
autoApprovePermissions: shouldAutoApproveForMode(options.mode),
|
|
133
147
|
approvalStats: { tool: 0, command: 0, file: 0, total: 0 },
|
|
148
|
+
selectedModel,
|
|
134
149
|
};
|
|
135
150
|
this.sessions.set(id, snapshot);
|
|
136
151
|
this.storage.saveSession(snapshot);
|
|
@@ -251,6 +266,27 @@ export class StructuredSessionManager {
|
|
|
251
266
|
denyPermission(sessionId) {
|
|
252
267
|
return this.resolvePermission(sessionId, false);
|
|
253
268
|
}
|
|
269
|
+
/** Update the selected model for a structured session. Takes effect on the next spawn. */
|
|
270
|
+
setSessionModel(sessionId, model) {
|
|
271
|
+
const session = this.requireSession(sessionId);
|
|
272
|
+
const normalized = model?.trim() || null;
|
|
273
|
+
const updated = {
|
|
274
|
+
...session,
|
|
275
|
+
selectedModel: normalized,
|
|
276
|
+
structuredState: {
|
|
277
|
+
...(session.structuredState ?? { provider: "claude", runner: "claude-cli-print", inFlight: false, activeRequestId: null, lastError: null }),
|
|
278
|
+
model: normalized ?? undefined,
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
this.sessions.set(sessionId, updated);
|
|
282
|
+
this.storage.saveSession(updated);
|
|
283
|
+
this.emit({
|
|
284
|
+
type: "status",
|
|
285
|
+
sessionId,
|
|
286
|
+
data: { sessionKind: "structured", selectedModel: normalized, structuredState: updated.structuredState },
|
|
287
|
+
});
|
|
288
|
+
return updated;
|
|
289
|
+
}
|
|
254
290
|
/** Toggle auto-approve for the session. */
|
|
255
291
|
toggleAutoApprove(sessionId) {
|
|
256
292
|
const session = this.requireSession(sessionId);
|
|
@@ -474,14 +510,24 @@ export class StructuredSessionManager {
|
|
|
474
510
|
// Add permission args based on mode + autoApprovePermissions toggle
|
|
475
511
|
const permArgs = this.buildPermissionArgs(session.mode, session.autoApprovePermissions ?? false);
|
|
476
512
|
args.push(...permArgs);
|
|
513
|
+
// Append language-aware system prompts
|
|
514
|
+
const language = this.config.language?.trim();
|
|
515
|
+
const isChinese = language === "中文";
|
|
477
516
|
// In managed mode, append autonomous system prompt
|
|
478
517
|
if (session.mode === "managed") {
|
|
479
|
-
args.push("--append-system-prompt",
|
|
518
|
+
args.push("--append-system-prompt", isChinese
|
|
519
|
+
? "你正在完全托管的自主模式下运行。用户可能无法及时回复问题或确认。你必须独立做出所有决策——自行选择最佳方案,而不是向用户询问偏好、确认或澄清。如果有多种可行方案,选择你认为最合适的并继续执行。除非任务本身存在根本性的歧义且无法合理推断,否则不要等待用户输入。果断行动,自主决策。"
|
|
520
|
+
: "You are running in a fully managed, autonomous mode. The user may not be available to respond to questions or confirmations in a timely manner. You MUST make all decisions independently — choose the best approach yourself instead of asking the user for preferences, confirmations, or clarifications. If multiple approaches are viable, pick the one you judge most appropriate and proceed. Never block on user input unless the task is fundamentally ambiguous and cannot be reasonably inferred. Be decisive and self-directed.");
|
|
480
521
|
}
|
|
481
522
|
// Append language preference if configured
|
|
482
|
-
const language = this.config.language?.trim();
|
|
483
523
|
if (language) {
|
|
484
|
-
args.push("--append-system-prompt",
|
|
524
|
+
args.push("--append-system-prompt", isChinese
|
|
525
|
+
? "请使用中文回复。所有解释、注释和对话文本都使用中文。"
|
|
526
|
+
: `Please respond in ${language}. Use ${language} for all your explanations, comments, and conversational text.`);
|
|
527
|
+
}
|
|
528
|
+
const modelChoice = session.selectedModel?.trim();
|
|
529
|
+
if (modelChoice && modelChoice !== "default") {
|
|
530
|
+
args.push("--model", modelChoice);
|
|
485
531
|
}
|
|
486
532
|
if (session.claudeSessionId) {
|
|
487
533
|
args.push("--resume", session.claudeSessionId);
|
|
@@ -516,7 +562,7 @@ export class StructuredSessionManager {
|
|
|
516
562
|
this.emit({
|
|
517
563
|
type: "output",
|
|
518
564
|
sessionId,
|
|
519
|
-
data:
|
|
565
|
+
data: buildIncrementalStructuredPayload(current),
|
|
520
566
|
});
|
|
521
567
|
};
|
|
522
568
|
const scheduleEmit = () => {
|
package/dist/types.d.ts
CHANGED
|
@@ -86,6 +86,18 @@ export interface WandConfig {
|
|
|
86
86
|
android?: AndroidApkConfig;
|
|
87
87
|
/** Default expand/collapse state for card types in structured chat view */
|
|
88
88
|
cardDefaults?: CardExpandDefaults;
|
|
89
|
+
/** 新建会话时默认使用的 Claude 模型(别名或完整 ID)。留空则不传 --model,由 claude 自行决定。 */
|
|
90
|
+
defaultModel?: string;
|
|
91
|
+
}
|
|
92
|
+
export interface ClaudeModelInfo {
|
|
93
|
+
/** 传给 --model 的值(别名或完整模型 ID) */
|
|
94
|
+
id: string;
|
|
95
|
+
/** UI 显示的友好标签 */
|
|
96
|
+
label: string;
|
|
97
|
+
/** 可选备注:例如 "当前默认"、"最新" */
|
|
98
|
+
note?: string;
|
|
99
|
+
/** 是否为别名(opus/sonnet 等);完整 ID 为 false */
|
|
100
|
+
alias?: boolean;
|
|
89
101
|
}
|
|
90
102
|
interface WorktreeInfo {
|
|
91
103
|
branch: string;
|
|
@@ -130,6 +142,8 @@ export interface CommandRequest {
|
|
|
130
142
|
mode?: ExecutionMode;
|
|
131
143
|
initialInput?: string;
|
|
132
144
|
worktreeEnabled?: boolean;
|
|
145
|
+
/** Claude 模型(别名或完整 ID)。仅对 claude provider 生效。留空则回落到 config.defaultModel。 */
|
|
146
|
+
model?: string;
|
|
133
147
|
}
|
|
134
148
|
export interface InputRequest {
|
|
135
149
|
input?: string;
|
|
@@ -273,6 +287,8 @@ export interface SessionSnapshot {
|
|
|
273
287
|
summary?: string;
|
|
274
288
|
/** 当前正在执行的任务标题(用于会话列表展示) */
|
|
275
289
|
currentTaskTitle?: string;
|
|
290
|
+
/** 用户为此会话选定的 Claude 模型(别名或完整 ID)。结构化会话下次 spawn 时使用;PTY 会话仅用于展示。 */
|
|
291
|
+
selectedModel?: string | null;
|
|
276
292
|
}
|
|
277
293
|
export type SessionLifecycleState = "initializing" | "running" | "idle" | "thinking" | "waiting-input" | "archived";
|
|
278
294
|
export interface SessionLifecycle {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { mkdirSync, existsSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { randomBytes } from "node:crypto";
|
|
4
|
+
import multer from "multer";
|
|
5
|
+
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB
|
|
6
|
+
const MAX_FILES = 5;
|
|
7
|
+
function sanitizeFilename(name) {
|
|
8
|
+
return name.replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 200);
|
|
9
|
+
}
|
|
10
|
+
export function registerUploadRoutes(app, processes) {
|
|
11
|
+
const storage = multer.diskStorage({
|
|
12
|
+
destination(_req, _file, cb) {
|
|
13
|
+
const sessionId = _req.params?.id;
|
|
14
|
+
const session = sessionId ? processes.get(sessionId) : null;
|
|
15
|
+
const cwd = session?.cwd || "/tmp";
|
|
16
|
+
const uploadDir = path.join(cwd, ".wand-uploads");
|
|
17
|
+
if (!existsSync(uploadDir)) {
|
|
18
|
+
mkdirSync(uploadDir, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
cb(null, uploadDir);
|
|
21
|
+
},
|
|
22
|
+
filename(_req, file, cb) {
|
|
23
|
+
const ts = Date.now();
|
|
24
|
+
const rand = randomBytes(4).toString("hex");
|
|
25
|
+
const safe = sanitizeFilename(file.originalname);
|
|
26
|
+
cb(null, `${ts}-${rand}-${safe}`);
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
const upload = multer({
|
|
30
|
+
storage,
|
|
31
|
+
limits: { fileSize: MAX_FILE_SIZE, files: MAX_FILES },
|
|
32
|
+
});
|
|
33
|
+
app.post("/api/sessions/:id/upload", upload.array("files", MAX_FILES), (req, res) => {
|
|
34
|
+
const sessionId = req.params.id;
|
|
35
|
+
const session = processes.get(sessionId);
|
|
36
|
+
if (!session) {
|
|
37
|
+
res.status(404).json({ error: "会话不存在。" });
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const files = req.files || [];
|
|
41
|
+
if (files.length === 0) {
|
|
42
|
+
res.status(400).json({ error: "未收到文件。" });
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const result = files.map((f) => ({
|
|
46
|
+
originalName: f.originalname,
|
|
47
|
+
savedPath: f.path,
|
|
48
|
+
size: f.size,
|
|
49
|
+
mimeType: f.mimetype,
|
|
50
|
+
}));
|
|
51
|
+
res.json({ files: result });
|
|
52
|
+
});
|
|
53
|
+
}
|