@co0ontty/wand 3.1.1 → 4.0.0

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.
Files changed (62) hide show
  1. package/dist/auth.d.ts +19 -5
  2. package/dist/auth.js +83 -45
  3. package/dist/build-info.json +3 -3
  4. package/dist/cert.d.ts +1 -1
  5. package/dist/cert.js +124 -74
  6. package/dist/config.js +25 -8
  7. package/dist/express-async.d.ts +6 -0
  8. package/dist/express-async.js +28 -0
  9. package/dist/git-quick-commit.d.ts +2 -0
  10. package/dist/git-quick-commit.js +215 -76
  11. package/dist/git-utils.d.ts +4 -0
  12. package/dist/git-utils.js +60 -11
  13. package/dist/git-worktree.d.ts +8 -1
  14. package/dist/git-worktree.js +406 -41
  15. package/dist/models.d.ts +34 -4
  16. package/dist/models.js +334 -48
  17. package/dist/process-manager.d.ts +22 -30
  18. package/dist/process-manager.js +374 -441
  19. package/dist/provider-history-scanner.d.ts +54 -0
  20. package/dist/provider-history-scanner.js +354 -0
  21. package/dist/request-limits.d.ts +1 -0
  22. package/dist/request-limits.js +8 -0
  23. package/dist/resume-policy.d.ts +2 -0
  24. package/dist/resume-policy.js +5 -0
  25. package/dist/runtime-config.d.ts +16 -0
  26. package/dist/runtime-config.js +49 -0
  27. package/dist/server-file-routes.d.ts +17 -0
  28. package/dist/server-file-routes.js +653 -0
  29. package/dist/server-session-routes.d.ts +16 -3
  30. package/dist/server-session-routes.js +170 -149
  31. package/dist/server-settings-routes.d.ts +43 -0
  32. package/dist/server-settings-routes.js +225 -0
  33. package/dist/server-update-routes.d.ts +61 -0
  34. package/dist/server-update-routes.js +215 -0
  35. package/dist/server.d.ts +6 -4
  36. package/dist/server.js +350 -1313
  37. package/dist/session-logger.d.ts +32 -2
  38. package/dist/session-logger.js +145 -15
  39. package/dist/session-registry.d.ts +27 -0
  40. package/dist/session-registry.js +153 -0
  41. package/dist/session-transport.d.ts +31 -0
  42. package/dist/session-transport.js +82 -0
  43. package/dist/storage.d.ts +24 -6
  44. package/dist/storage.js +291 -44
  45. package/dist/structured-claude-adapter.d.ts +19 -0
  46. package/dist/structured-claude-adapter.js +117 -0
  47. package/dist/structured-codex-adapter.d.ts +3 -0
  48. package/dist/structured-codex-adapter.js +29 -0
  49. package/dist/structured-opencode-adapter.d.ts +11 -0
  50. package/dist/structured-opencode-adapter.js +115 -0
  51. package/dist/structured-provider-common.d.ts +11 -0
  52. package/dist/structured-provider-common.js +77 -0
  53. package/dist/structured-session-manager.d.ts +32 -35
  54. package/dist/structured-session-manager.js +551 -605
  55. package/dist/types.d.ts +10 -0
  56. package/dist/update-helper.js +5 -1
  57. package/dist/web-ui/content/scripts.js +32 -32
  58. package/dist/web-ui/embedded-assets.d.ts +1 -1
  59. package/dist/web-ui/embedded-assets.js +2 -2
  60. package/dist/ws-broadcast.d.ts +16 -1
  61. package/dist/ws-broadcast.js +124 -58
  62. package/package.json +2 -1
@@ -0,0 +1,43 @@
1
+ import type { Express, Request, RequestHandler } from "express";
2
+ import { type ModelRefreshOptions } from "./models.js";
3
+ import { type RuntimeConfigState } from "./runtime-config.js";
4
+ import type { WandStorage } from "./storage.js";
5
+ import type { WandConfig } from "./types.js";
6
+ interface SettingsDistributionPayload {
7
+ androidApk: Record<string, unknown>;
8
+ macosDmg: Record<string, unknown>;
9
+ }
10
+ interface SettingsBuildInfo {
11
+ commit: string | null;
12
+ builtAt: string | null;
13
+ channel: string | null;
14
+ }
15
+ export interface ServerSettingsRoutesDependencies {
16
+ storage: WandStorage;
17
+ config: WandConfig;
18
+ runtimeConfig: RuntimeConfigState;
19
+ configPath: string;
20
+ configDir: string;
21
+ requireAdmin: RequestHandler;
22
+ requireAdminOrSessionPreferences: RequestHandler;
23
+ packageInfo: {
24
+ version: string;
25
+ name: string;
26
+ nodeVersion: string;
27
+ repoUrl: string;
28
+ };
29
+ buildInfo: SettingsBuildInfo;
30
+ getCachedUpdateInfo(): {
31
+ updateAvailable: boolean;
32
+ latest: string | null;
33
+ } | null;
34
+ getUpdateChannel(): "stable" | "beta";
35
+ getDistributionSettings(): Promise<SettingsDistributionPayload>;
36
+ getModelRefreshOptions(): ModelRefreshOptions;
37
+ resolveAppConnectCode(req: Request): {
38
+ code: string;
39
+ url: string;
40
+ };
41
+ }
42
+ export declare function registerSettingsRoutes(app: Express, deps: ServerSettingsRoutesDependencies): void;
43
+ export {};
@@ -0,0 +1,225 @@
1
+ import { existsSync, writeFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { buildChildEnv } from "./env-utils.js";
4
+ import { getErrorMessage } from "./error-utils.js";
5
+ import { asyncRoute } from "./express-async.js";
6
+ import { getProviderDefaultModels, PREFERENCE_KEYS, saveConfig, writePreferenceToStorage, } from "./config.js";
7
+ import { getCachedModels, refreshModels } from "./models.js";
8
+ import { DEPLOYMENT_CONFIG_KEYS } from "./runtime-config.js";
9
+ function publicConfig(config) {
10
+ const { password: _password, appSecret: _appSecret, ...safe } = config;
11
+ const defaultModels = getProviderDefaultModels(config);
12
+ return {
13
+ ...safe,
14
+ defaultModel: defaultModels.claude,
15
+ defaultCodexModel: defaultModels.codex,
16
+ defaultOpenCodeModel: defaultModels.opencode,
17
+ defaultModels,
18
+ };
19
+ }
20
+ export function registerSettingsRoutes(app, deps) {
21
+ const { storage, config, runtimeConfig, configPath, configDir, requireAdmin, requireAdminOrSessionPreferences, } = deps;
22
+ app.get("/api/settings", requireAdmin, asyncRoute(async (_req, res) => {
23
+ const desiredConfig = runtimeConfig.desiredSnapshot();
24
+ const distribution = await deps.getDistributionSettings();
25
+ const cachedUpdate = deps.getCachedUpdateInfo();
26
+ res.json({
27
+ version: deps.packageInfo.version,
28
+ packageName: deps.packageInfo.name,
29
+ nodeVersion: deps.packageInfo.nodeVersion,
30
+ repoUrl: deps.packageInfo.repoUrl,
31
+ config: publicConfig(desiredConfig),
32
+ desiredConfig: publicConfig(desiredConfig),
33
+ activeConfig: publicConfig(config),
34
+ restartRequired: runtimeConfig.hasPendingRestart(),
35
+ hasCert: existsSync(path.join(configDir, "server.key")) && existsSync(path.join(configDir, "server.crt")),
36
+ updateAvailable: cachedUpdate?.updateAvailable ?? false,
37
+ latestVersion: cachedUpdate?.latest ?? null,
38
+ updateChannel: deps.getUpdateChannel(),
39
+ build: {
40
+ ...deps.buildInfo,
41
+ shortCommit: deps.buildInfo.commit ? deps.buildInfo.commit.slice(0, 7) : null,
42
+ },
43
+ autoUpdate: {
44
+ web: storage.getConfigValue("autoUpdateWeb") === "true",
45
+ apk: storage.getConfigValue("autoUpdateApk") === "true",
46
+ dmg: storage.getConfigValue("autoUpdateDmg") === "true",
47
+ cli: storage.getConfigValue("autoUpdateProviderClis") === "true",
48
+ },
49
+ ...distribution,
50
+ });
51
+ }));
52
+ app.get("/api/settings/env-preview", (req, res, next) => {
53
+ if (req.query.reveal === "1")
54
+ return requireAdmin(req, res, next);
55
+ next();
56
+ }, (req, res) => {
57
+ const inheritEnv = config.inheritEnv !== false;
58
+ const env = buildChildEnv(inheritEnv, {
59
+ WAND_MODE: "<runtime>",
60
+ WAND_AUTO_CONFIRM: "<runtime>",
61
+ WAND_AUTO_EDIT: "<runtime>",
62
+ });
63
+ const reveal = req.query.reveal === "1" || req.query.reveal === "true";
64
+ const sensitivePattern = /(KEY|TOKEN|SECRET|PASSWORD|AUTH|CREDENTIAL|COOKIE|SESSION)/i;
65
+ const entries = Object.keys(env).sort().map((name) => {
66
+ const raw = env[name] ?? "";
67
+ const sensitive = sensitivePattern.test(name);
68
+ const placeholder = raw.startsWith("<") && raw.endsWith(">");
69
+ return {
70
+ name,
71
+ value: sensitive && !reveal && !placeholder ? "***" : raw,
72
+ length: raw.length,
73
+ sensitive,
74
+ };
75
+ });
76
+ res.json({ inheritEnv, total: entries.length, reveal, entries });
77
+ });
78
+ app.get("/api/app-connect-code", requireAdmin, (req, res) => {
79
+ res.json(deps.resolveAppConnectCode(req));
80
+ });
81
+ app.post("/api/settings/config", requireAdminOrSessionPreferences, asyncRoute(async (req, res) => {
82
+ const body = req.body;
83
+ const previousDesiredConfig = runtimeConfig.desiredSnapshot();
84
+ const candidateConfig = runtimeConfig.createCandidate();
85
+ const stagedPreferences = [];
86
+ const stagedPreferenceFields = new Set();
87
+ const stagingStorage = {
88
+ setPreference(key, value) { stagedPreferences.push({ key, value }); },
89
+ };
90
+ const stagePreference = (field, value) => {
91
+ writePreferenceToStorage(candidateConfig, stagingStorage, field, value);
92
+ stagedPreferenceFields.add(field);
93
+ };
94
+ let touchedDeployField = false;
95
+ try {
96
+ for (const field of DEPLOYMENT_CONFIG_KEYS) {
97
+ if (!(field in body) || body[field] === undefined)
98
+ continue;
99
+ if (field === "port") {
100
+ const port = Number(body.port);
101
+ if (!Number.isInteger(port) || port < 1 || port > 65535)
102
+ throw new Error(`无效端口号: ${body.port}`);
103
+ candidateConfig.port = port;
104
+ }
105
+ else if (field === "https") {
106
+ if (typeof body.https !== "boolean")
107
+ throw new Error("https 必须是布尔值。");
108
+ candidateConfig.https = body.https;
109
+ }
110
+ else if (field === "host") {
111
+ if (typeof body.host !== "string" || !body.host.trim())
112
+ throw new Error("host 不能为空。");
113
+ candidateConfig.host = body.host.trim();
114
+ }
115
+ else if (field === "shell") {
116
+ if (typeof body.shell !== "string" || !body.shell.trim())
117
+ throw new Error("shell 不能为空。");
118
+ candidateConfig.shell = body.shell.trim();
119
+ }
120
+ touchedDeployField = true;
121
+ }
122
+ if (body.defaultModels !== undefined) {
123
+ if (!body.defaultModels || typeof body.defaultModels !== "object" || Array.isArray(body.defaultModels)) {
124
+ throw new Error("defaultModels 必须是对象。");
125
+ }
126
+ if (Object.hasOwn(body.defaultModels, "claude"))
127
+ stagePreference("defaultModel", body.defaultModels.claude);
128
+ if (Object.hasOwn(body.defaultModels, "codex"))
129
+ stagePreference("defaultCodexModel", body.defaultModels.codex);
130
+ if (Object.hasOwn(body.defaultModels, "opencode"))
131
+ stagePreference("defaultOpenCodeModel", body.defaultModels.opencode);
132
+ }
133
+ for (const field of PREFERENCE_KEYS) {
134
+ const value = body[field];
135
+ if (!(field in body) || value === undefined)
136
+ continue;
137
+ stagePreference(field, value);
138
+ }
139
+ }
140
+ catch (error) {
141
+ res.status(400).json({ error: getErrorMessage(error, "配置校验失败。") });
142
+ return;
143
+ }
144
+ if (!touchedDeployField && stagedPreferences.length === 0) {
145
+ res.status(400).json({ error: "没有可更新的配置字段。" });
146
+ return;
147
+ }
148
+ let deployConfigWritten = false;
149
+ try {
150
+ if (touchedDeployField) {
151
+ await saveConfig(configPath, candidateConfig);
152
+ deployConfigWritten = true;
153
+ }
154
+ if (stagedPreferences.length > 0) {
155
+ storage.transaction(() => {
156
+ for (const mutation of stagedPreferences)
157
+ storage.setPreference(mutation.key, mutation.value);
158
+ });
159
+ }
160
+ runtimeConfig.commit(candidateConfig, stagedPreferenceFields);
161
+ res.json({
162
+ ok: true,
163
+ config: publicConfig(candidateConfig),
164
+ desiredConfig: publicConfig(candidateConfig),
165
+ activeConfig: publicConfig(config),
166
+ restartRequired: runtimeConfig.hasPendingRestart(),
167
+ });
168
+ }
169
+ catch (error) {
170
+ if (deployConfigWritten) {
171
+ try {
172
+ await saveConfig(configPath, previousDesiredConfig);
173
+ }
174
+ catch { /* preserve original */ }
175
+ }
176
+ res.status(500).json({ error: getErrorMessage(error, "保存配置失败。") });
177
+ }
178
+ }));
179
+ app.get("/api/models", (_req, res) => {
180
+ const cached = getCachedModels(deps.getModelRefreshOptions());
181
+ const defaults = getProviderDefaultModels(config);
182
+ res.json({
183
+ ...cached,
184
+ defaultModel: defaults.claude,
185
+ defaultCodexModel: defaults.codex,
186
+ defaultOpenCodeModel: defaults.opencode,
187
+ defaultModels: defaults,
188
+ });
189
+ });
190
+ app.post("/api/models/refresh", asyncRoute(async (_req, res) => {
191
+ try {
192
+ const refreshed = await refreshModels({ ...deps.getModelRefreshOptions(), verifyClaudeCandidates: true });
193
+ const defaults = getProviderDefaultModels(config);
194
+ res.json({
195
+ ...refreshed,
196
+ defaultModel: defaults.claude,
197
+ defaultCodexModel: defaults.codex,
198
+ defaultOpenCodeModel: defaults.opencode,
199
+ defaultModels: defaults,
200
+ });
201
+ }
202
+ catch (error) {
203
+ res.status(500).json({ error: getErrorMessage(error, "刷新模型列表失败。") });
204
+ }
205
+ }));
206
+ app.post("/api/settings/upload-cert", requireAdmin, asyncRoute(async (req, res) => {
207
+ const { key, cert } = req.body;
208
+ if (!key || !cert) {
209
+ res.status(400).json({ error: "请提供 key 和 cert 内容。" });
210
+ return;
211
+ }
212
+ if (!key.includes("-----BEGIN") || !cert.includes("-----BEGIN")) {
213
+ res.status(400).json({ error: "证书内容格式无效,请上传 PEM 格式的文件。" });
214
+ return;
215
+ }
216
+ try {
217
+ writeFileSync(path.join(configDir, "server.key"), key, { mode: 0o600 });
218
+ writeFileSync(path.join(configDir, "server.crt"), cert, { mode: 0o600 });
219
+ res.json({ ok: true, restartRequired: true });
220
+ }
221
+ catch (error) {
222
+ res.status(500).json({ error: getErrorMessage(error, "保存证书失败。") });
223
+ }
224
+ }));
225
+ }
@@ -0,0 +1,61 @@
1
+ import type { Express, RequestHandler } from "express";
2
+ import { type ModelRefreshOptions } from "./models.js";
3
+ import type { PackageUpdateInfo, UpdateChannel } from "./npm-update-utils.js";
4
+ import { type ProviderCliUpdateStatus } from "./provider-cli-updater.js";
5
+ import type { WandStorage } from "./storage.js";
6
+ import type { WandConfig } from "./types.js";
7
+ interface DownloadAsset {
8
+ fileName: string;
9
+ filePath: string;
10
+ size: number;
11
+ }
12
+ interface ResolvedUpdateAsset {
13
+ version: string;
14
+ downloadUrl: string;
15
+ fileName: string;
16
+ size: number;
17
+ source: "local" | "github";
18
+ releaseNotes?: string;
19
+ }
20
+ export interface PublicUpdateRoutesDependencies {
21
+ resolveLatestApk(channel: "stable" | "beta"): Promise<ResolvedUpdateAsset | null>;
22
+ resolveAndroidDownload(channel: "stable" | "beta"): Promise<DownloadAsset | null>;
23
+ resolveLatestDmg(): Promise<ResolvedUpdateAsset | null>;
24
+ resolveMacosDownload(): Promise<DownloadAsset | null>;
25
+ }
26
+ export declare function registerPublicUpdateRoutes(app: Express, deps: PublicUpdateRoutesDependencies): void;
27
+ export declare class ServerUpdateState {
28
+ providerCliUpdateCache: {
29
+ items: ProviderCliUpdateStatus[];
30
+ checkedAt: string;
31
+ } | null;
32
+ providerCliUpdateInFlight: boolean;
33
+ updateInFlight: boolean;
34
+ }
35
+ export declare function refreshProviderCliUpdateState(state: ServerUpdateState, config: WandConfig): Promise<{
36
+ items: ProviderCliUpdateStatus[];
37
+ checkedAt: string;
38
+ }>;
39
+ export interface AdminUpdateRoutesDependencies {
40
+ storage: WandStorage;
41
+ config: WandConfig;
42
+ configPath: string;
43
+ requireAdmin: RequestHandler;
44
+ state: ServerUpdateState;
45
+ getDistributionSettings(): Promise<{
46
+ androidApk: Record<string, unknown>;
47
+ macosDmg: Record<string, unknown>;
48
+ }>;
49
+ getModelRefreshOptions(): ModelRefreshOptions;
50
+ getUpdateChannel(): UpdateChannel;
51
+ checkLatestPackageVersion(channel: UpdateChannel, forceRefresh?: boolean): Promise<PackageUpdateInfo>;
52
+ buildInfo: {
53
+ commit: string | null;
54
+ builtAt: string | null;
55
+ channel: string | null;
56
+ };
57
+ serverInstanceId: string;
58
+ emitSystemNotification(data: Record<string, unknown>): void;
59
+ }
60
+ export declare function registerAdminUpdateRoutes(app: Express, deps: AdminUpdateRoutesDependencies): void;
61
+ export {};
@@ -0,0 +1,215 @@
1
+ import { getErrorMessage } from "./error-utils.js";
2
+ import { asyncRoute } from "./express-async.js";
3
+ import { refreshModels } from "./models.js";
4
+ import { checkProviderCliUpdates, updateProviderClis, verifyProviderCliUpdateResults, } from "./provider-cli-updater.js";
5
+ import { streamFileWithRange } from "./server-file-routes.js";
6
+ import { compareSemver } from "./version-utils.js";
7
+ import { canUseDetachedUpdateHelper, startDetachedUpdateHelper } from "./update-helper.js";
8
+ export function registerPublicUpdateRoutes(app, deps) {
9
+ app.get("/api/android-apk-update", asyncRoute(async (req, res) => {
10
+ const currentVersion = typeof req.query.currentVersion === "string" ? req.query.currentVersion.trim() : "";
11
+ if (!currentVersion) {
12
+ res.status(400).json({ error: "Missing currentVersion query parameter." });
13
+ return;
14
+ }
15
+ const channel = req.query.channel === "beta" ? "beta" : "stable";
16
+ const latest = await deps.resolveLatestApk(channel);
17
+ if (!latest) {
18
+ res.json({ updateAvailable: false, currentVersion, latestVersion: null, downloadUrl: null, source: null, channel });
19
+ return;
20
+ }
21
+ const updateAvailable = compareSemver(latest.version, currentVersion) > 0;
22
+ res.json({
23
+ updateAvailable,
24
+ currentVersion,
25
+ latestVersion: latest.version,
26
+ downloadUrl: updateAvailable ? latest.downloadUrl : null,
27
+ fileName: updateAvailable ? latest.fileName : null,
28
+ size: updateAvailable ? latest.size : null,
29
+ source: latest.source,
30
+ channel,
31
+ releaseNotes: updateAvailable ? (latest.releaseNotes ?? null) : null,
32
+ });
33
+ }));
34
+ app.get("/android/download", asyncRoute(async (req, res) => {
35
+ const channel = req.query.channel === "stable" ? "stable" : "beta";
36
+ const asset = await deps.resolveAndroidDownload(channel);
37
+ if (!asset) {
38
+ res.status(404).json({ error: "当前没有可下载的 APK 文件。" });
39
+ return;
40
+ }
41
+ streamFileWithRange(req, res, {
42
+ filePath: asset.filePath,
43
+ size: asset.size,
44
+ contentType: "application/vnd.android.package-archive",
45
+ disposition: `attachment; filename="${encodeURIComponent(asset.fileName)}"`,
46
+ readErrorMessage: "读取 APK 文件失败。",
47
+ });
48
+ }));
49
+ app.get("/api/macos-dmg-update", asyncRoute(async (req, res) => {
50
+ const currentVersion = typeof req.query.currentVersion === "string" ? req.query.currentVersion.trim() : "";
51
+ if (!currentVersion) {
52
+ res.status(400).json({ error: "Missing currentVersion query parameter." });
53
+ return;
54
+ }
55
+ const latest = await deps.resolveLatestDmg();
56
+ if (!latest) {
57
+ res.json({ updateAvailable: false, currentVersion, latestVersion: null, downloadUrl: null, source: null });
58
+ return;
59
+ }
60
+ const updateAvailable = compareSemver(latest.version, currentVersion) > 0;
61
+ res.json({
62
+ updateAvailable,
63
+ currentVersion,
64
+ latestVersion: latest.version,
65
+ downloadUrl: updateAvailable ? latest.downloadUrl : null,
66
+ fileName: updateAvailable ? latest.fileName : null,
67
+ size: updateAvailable ? latest.size : null,
68
+ source: latest.source,
69
+ });
70
+ }));
71
+ app.get("/macos/download", asyncRoute(async (req, res) => {
72
+ const asset = await deps.resolveMacosDownload();
73
+ if (!asset) {
74
+ res.status(404).json({ error: "当前没有可下载的 DMG 文件。" });
75
+ return;
76
+ }
77
+ streamFileWithRange(req, res, {
78
+ filePath: asset.filePath,
79
+ size: asset.size,
80
+ contentType: "application/x-apple-diskimage",
81
+ disposition: `attachment; filename="${encodeURIComponent(asset.fileName)}"`,
82
+ readErrorMessage: "读取 DMG 文件失败。",
83
+ });
84
+ }));
85
+ }
86
+ export class ServerUpdateState {
87
+ providerCliUpdateCache = null;
88
+ providerCliUpdateInFlight = false;
89
+ updateInFlight = false;
90
+ }
91
+ export async function refreshProviderCliUpdateState(state, config) {
92
+ const items = await checkProviderCliUpdates({ inheritEnv: config.inheritEnv !== false });
93
+ const result = { items, checkedAt: new Date().toISOString() };
94
+ state.providerCliUpdateCache = result;
95
+ return result;
96
+ }
97
+ export function registerAdminUpdateRoutes(app, deps) {
98
+ const { state, config, storage, requireAdmin } = deps;
99
+ app.get("/api/android-apk", requireAdmin, asyncRoute(async (_req, res) => {
100
+ res.json((await deps.getDistributionSettings()).androidApk);
101
+ }));
102
+ app.get("/api/macos-dmg", requireAdmin, asyncRoute(async (_req, res) => {
103
+ res.json((await deps.getDistributionSettings()).macosDmg);
104
+ }));
105
+ app.get("/api/provider-cli-updates", requireAdmin, asyncRoute(async (req, res) => {
106
+ try {
107
+ const force = req.query.refresh === "1" || !state.providerCliUpdateCache;
108
+ const data = force
109
+ ? await refreshProviderCliUpdateState(state, config)
110
+ : state.providerCliUpdateCache;
111
+ res.json({
112
+ ...data,
113
+ updating: state.providerCliUpdateInFlight,
114
+ autoUpdate: storage.getConfigValue("autoUpdateProviderClis") === "true",
115
+ });
116
+ }
117
+ catch (error) {
118
+ res.status(500).json({ error: getErrorMessage(error, "检查 CLI 更新失败。") });
119
+ }
120
+ }));
121
+ app.post("/api/provider-cli-updates", requireAdmin, asyncRoute(async (req, res) => {
122
+ if (state.providerCliUpdateInFlight || state.updateInFlight) {
123
+ res.status(409).json({ error: "CLI 更新正在进行中,请稍候。" });
124
+ return;
125
+ }
126
+ const rawIds = Array.isArray(req.body?.ids) ? req.body.ids : [];
127
+ const ids = rawIds.filter((value) => value === "claude" || value === "codex" || value === "opencode");
128
+ state.providerCliUpdateInFlight = true;
129
+ try {
130
+ const before = await refreshProviderCliUpdateState(state, config);
131
+ const commandResults = await updateProviderClis(before.items, ids.length ? ids : undefined, {
132
+ inheritEnv: config.inheritEnv !== false,
133
+ onLog: (line) => process.stdout.write(`[wand] ${line}\n`),
134
+ });
135
+ const after = await refreshProviderCliUpdateState(state, config);
136
+ const results = verifyProviderCliUpdateResults(commandResults, after.items);
137
+ void refreshModels(deps.getModelRefreshOptions()).catch(() => { });
138
+ res.json({ ok: results.every((item) => item.ok), results, ...after, autoUpdate: storage.getConfigValue("autoUpdateProviderClis") === "true" });
139
+ }
140
+ catch (error) {
141
+ res.status(500).json({ error: getErrorMessage(error, "更新 CLI 失败。") });
142
+ }
143
+ finally {
144
+ state.providerCliUpdateInFlight = false;
145
+ }
146
+ }));
147
+ app.get("/api/check-update", requireAdmin, asyncRoute(async (_req, res) => {
148
+ try {
149
+ const info = await deps.checkLatestPackageVersion(deps.getUpdateChannel(), true);
150
+ res.json({
151
+ ...info,
152
+ build: {
153
+ ...deps.buildInfo,
154
+ shortCommit: deps.buildInfo.commit ? deps.buildInfo.commit.slice(0, 7) : null,
155
+ },
156
+ });
157
+ }
158
+ catch (error) {
159
+ res.status(500).json({ error: getErrorMessage(error, "检查更新失败。") });
160
+ }
161
+ }));
162
+ app.post("/api/update", requireAdmin, asyncRoute(async (_req, res) => {
163
+ if (state.updateInFlight || state.providerCliUpdateInFlight) {
164
+ res.status(409).json({ error: "更新正在进行中,请稍候。" });
165
+ return;
166
+ }
167
+ state.updateInFlight = true;
168
+ try {
169
+ const info = await deps.checkLatestPackageVersion(deps.getUpdateChannel(), true);
170
+ if (!info.latest) {
171
+ res.status(502).json({ error: "无法连接到 npm registry。" });
172
+ return;
173
+ }
174
+ if (!canUseDetachedUpdateHelper()) {
175
+ res.status(500).json({ error: "当前平台暂不支持 Web 异步更新,请在终端运行 install.sh 更新。" });
176
+ return;
177
+ }
178
+ const helper = startDetachedUpdateHelper({
179
+ installSpec: info.installSpec,
180
+ configPath: deps.configPath,
181
+ parentPid: process.pid,
182
+ cliArgs: process.argv.slice(2),
183
+ cwd: process.cwd(),
184
+ env: process.env,
185
+ timeoutMs: 300000,
186
+ });
187
+ if (!helper.started) {
188
+ res.status(500).json({ error: helper.message, detail: `script=${helper.scriptPath}\nlog=${helper.logPath}` });
189
+ return;
190
+ }
191
+ process.stdout.write(`[wand] ${helper.message}\n`);
192
+ deps.emitSystemNotification({
193
+ kind: "auto-update-restart",
194
+ current: info.current,
195
+ latest: info.latest,
196
+ previousInstanceId: deps.serverInstanceId,
197
+ });
198
+ res.json({
199
+ ok: true,
200
+ message: info.updateAvailable ? `已开始更新到 ${info.latest}` : `已开始重新安装 ${info.latest}`,
201
+ restartRequired: false,
202
+ detachedUpdate: true,
203
+ version: info.latest,
204
+ previousInstanceId: deps.serverInstanceId,
205
+ logPath: helper.logPath,
206
+ });
207
+ }
208
+ catch (error) {
209
+ res.status(500).json({ error: getErrorMessage(error, "更新失败。") });
210
+ }
211
+ finally {
212
+ state.updateInFlight = false;
213
+ }
214
+ }));
215
+ }
package/dist/server.d.ts CHANGED
@@ -1,10 +1,9 @@
1
+ import { AuthService } from "./auth.js";
2
+ import { ModelRefreshOptions } from "./models.js";
1
3
  import { ProcessManager } from "./process-manager.js";
2
4
  import { StructuredSessionManager } from "./structured-session-manager.js";
3
- import { WandStorage } from "./storage.js";
4
5
  import { type PathRepairResult } from "./path-repair.js";
5
6
  import { WandConfig } from "./types.js";
6
- /** Persist a cwd to recent paths. Used by both REST and session creation hooks. */
7
- export declare function recordRecentPath(storage: WandStorage, cwd: string | undefined | null): void;
8
7
  export interface ServerUrl {
9
8
  url: string;
10
9
  scheme: "HTTP" | "HTTPS";
@@ -12,6 +11,7 @@ export interface ServerUrl {
12
11
  export interface ServerHandle {
13
12
  processManager: ProcessManager;
14
13
  structuredSessions: StructuredSessionManager;
14
+ authService: AuthService;
15
15
  configPath: string;
16
16
  dbPath: string;
17
17
  urls: ServerUrl[];
@@ -29,4 +29,6 @@ export declare class PortInUseError extends Error {
29
29
  constructor(port: number, host: string);
30
30
  }
31
31
  export declare function isPortInUseError(error: unknown): error is PortInUseError;
32
- export declare function startServer(config: WandConfig, configPath: string): Promise<ServerHandle>;
32
+ export declare function startServer(config: WandConfig, configPath: string, options?: {
33
+ modelRefreshOptions?: () => Partial<ModelRefreshOptions>;
34
+ }): Promise<ServerHandle>;