@co0ontty/wand 1.21.5 → 1.21.8
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/cli.js +44 -18
- package/dist/config.d.ts +34 -0
- package/dist/config.js +163 -1
- package/dist/server.js +47 -50
- package/dist/storage.d.ts +6 -0
- package/dist/storage.js +29 -0
- package/dist/structured-session-manager.js +69 -16
- package/dist/web-ui/content/scripts.js +126 -51
- package/dist/web-ui/content/styles.css +1471 -254
- package/package.json +1 -3
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env -S node --disable-warning=ExperimentalWarning
|
|
2
2
|
import process from "node:process";
|
|
3
|
-
import {
|
|
3
|
+
import { hasConfigFile, isPreferenceKey, loadConfigWithStorage, resolveConfigPath, saveConfig, writePreferenceToStorage, } from "./config.js";
|
|
4
4
|
async function main() {
|
|
5
5
|
const args = process.argv.slice(2);
|
|
6
6
|
const command = args[0] || "help";
|
|
@@ -50,8 +50,18 @@ async function main() {
|
|
|
50
50
|
break;
|
|
51
51
|
}
|
|
52
52
|
case "config:show": {
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
// 展示合并后的视图(JSON 部署字段 + DB 偏好字段)。
|
|
54
|
+
const { ensureDatabaseFile, resolveDatabasePath, WandStorage } = await import("./storage.js");
|
|
55
|
+
const dbPath = resolveDatabasePath(configPath);
|
|
56
|
+
ensureDatabaseFile(dbPath);
|
|
57
|
+
const storage = new WandStorage(dbPath);
|
|
58
|
+
try {
|
|
59
|
+
const config = await loadConfigWithStorage(configPath, storage);
|
|
60
|
+
process.stdout.write(`${JSON.stringify(config, null, 2)}\n`);
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
storage.close();
|
|
64
|
+
}
|
|
55
65
|
break;
|
|
56
66
|
}
|
|
57
67
|
case "config:set": {
|
|
@@ -60,10 +70,26 @@ async function main() {
|
|
|
60
70
|
if (!key || typeof value === "undefined") {
|
|
61
71
|
throw new Error("Usage: wand config:set <key> <value>");
|
|
62
72
|
}
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
73
|
+
const { ensureDatabaseFile, resolveDatabasePath, WandStorage } = await import("./storage.js");
|
|
74
|
+
const dbPath = resolveDatabasePath(configPath);
|
|
75
|
+
ensureDatabaseFile(dbPath);
|
|
76
|
+
const storage = new WandStorage(dbPath);
|
|
77
|
+
try {
|
|
78
|
+
const config = await loadConfigWithStorage(configPath, storage);
|
|
79
|
+
if (isPreferenceKey(key)) {
|
|
80
|
+
// 偏好字段写 DB,无需重启
|
|
81
|
+
writePreferenceToStorage(config, storage, key, value);
|
|
82
|
+
process.stdout.write(`[wand] Updated preference ${key} in ${dbPath}\n`);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
const nextConfig = setConfigValue(config, key, value);
|
|
86
|
+
await saveConfig(configPath, nextConfig);
|
|
87
|
+
process.stdout.write(`[wand] Updated ${key} in ${configPath}\n`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
storage.close();
|
|
92
|
+
}
|
|
67
93
|
break;
|
|
68
94
|
}
|
|
69
95
|
case "help":
|
|
@@ -95,11 +121,19 @@ Options:
|
|
|
95
121
|
`);
|
|
96
122
|
}
|
|
97
123
|
async function ensureRequiredFiles(configPath, opts = {}) {
|
|
98
|
-
const { ensureDatabaseFile, resolveDatabasePath } = await import("./storage.js");
|
|
124
|
+
const { ensureDatabaseFile, resolveDatabasePath, WandStorage } = await import("./storage.js");
|
|
99
125
|
const dbPath = resolveDatabasePath(configPath);
|
|
100
126
|
const hadConfig = hasConfigFile(configPath);
|
|
101
|
-
|
|
127
|
+
// 先建 DB 文件,再加载 config(loadConfigWithStorage 需要 storage 来迁移老 JSON 偏好字段并应用 DB 覆盖)。
|
|
102
128
|
const createdDb = ensureDatabaseFile(dbPath);
|
|
129
|
+
const storage = new WandStorage(dbPath);
|
|
130
|
+
let config;
|
|
131
|
+
try {
|
|
132
|
+
config = await loadConfigWithStorage(configPath, storage);
|
|
133
|
+
}
|
|
134
|
+
finally {
|
|
135
|
+
storage.close();
|
|
136
|
+
}
|
|
103
137
|
// 已存在的 ready 信息在 TUI 模式下由启动 banner 统一展示,此处静默;
|
|
104
138
|
// 但 created 是首次创建事件,无论 TUI 与否都值得提示。
|
|
105
139
|
if (!hadConfig) {
|
|
@@ -153,11 +187,11 @@ function printStartupBanner(handle) {
|
|
|
153
187
|
process.stdout.write(lines.join("\n") + "\n");
|
|
154
188
|
}
|
|
155
189
|
function setConfigValue(config, key, value) {
|
|
190
|
+
// 偏好字段(defaultMode/defaultCwd/...)由调用方分流到 storage,这里只处理 JSON 字段。
|
|
156
191
|
switch (key) {
|
|
157
192
|
case "host":
|
|
158
193
|
case "password":
|
|
159
194
|
case "shell":
|
|
160
|
-
case "defaultCwd":
|
|
161
195
|
return {
|
|
162
196
|
...config,
|
|
163
197
|
[key]: value
|
|
@@ -170,14 +204,6 @@ function setConfigValue(config, key, value) {
|
|
|
170
204
|
...config,
|
|
171
205
|
port: Number(value)
|
|
172
206
|
};
|
|
173
|
-
case "defaultMode":
|
|
174
|
-
if (!isExecutionMode(value)) {
|
|
175
|
-
throw new Error(`defaultMode must be one of: assist, agent, agent-max, auto-edit, default, full-access, managed, native`);
|
|
176
|
-
}
|
|
177
|
-
return {
|
|
178
|
-
...config,
|
|
179
|
-
defaultMode: value
|
|
180
|
-
};
|
|
181
207
|
case "https":
|
|
182
208
|
if (value !== "true" && value !== "false") {
|
|
183
209
|
throw new Error("https must be 'true' or 'false'");
|
package/dist/config.d.ts
CHANGED
|
@@ -1,9 +1,43 @@
|
|
|
1
1
|
import { CardExpandDefaults, ExecutionMode, WandConfig } from "./types.js";
|
|
2
|
+
import type { WandStorage } from "./storage.js";
|
|
3
|
+
/**
|
|
4
|
+
* 通过 UI 设置面板可改的"用户偏好"字段。这些字段从 SQLite app_config 表读取,
|
|
5
|
+
* 不再写入 ~/.wand/config.json。JSON 只保留服务部署/启动期参数(host/port/shell 等)。
|
|
6
|
+
*
|
|
7
|
+
* 升级路径:老 JSON 里仍存有这些字段时,首次启动会被搬到 DB(见 migrateLegacyPreferencesToDb),
|
|
8
|
+
* 然后下一次 saveConfig 写回 JSON 时它们会被剥离(见 stripPreferenceFields)。
|
|
9
|
+
*/
|
|
10
|
+
export declare const PREFERENCE_KEYS: readonly ["defaultMode", "defaultCwd", "defaultModel", "structuredRunner", "language", "cardDefaults"];
|
|
11
|
+
export type PreferenceKey = (typeof PREFERENCE_KEYS)[number];
|
|
12
|
+
export declare function isPreferenceKey(key: string): key is PreferenceKey;
|
|
2
13
|
export declare const defaultConfig: () => WandConfig;
|
|
3
14
|
export declare function resolveConfigPath(inputPath?: string): string;
|
|
4
15
|
export declare function resolveConfigDir(configPath: string): string;
|
|
5
16
|
export declare function hasConfigFile(configPath: string): boolean;
|
|
6
17
|
export declare function ensureConfig(configPath: string): Promise<WandConfig>;
|
|
18
|
+
/** saveConfig 写出时去掉偏好字段——这些已经移到 SQLite。 */
|
|
7
19
|
export declare function saveConfig(configPath: string, config: WandConfig): Promise<void>;
|
|
20
|
+
/**
|
|
21
|
+
* 启动期合并 JSON + DB 偏好。语义:
|
|
22
|
+
* 1. 读 raw JSON
|
|
23
|
+
* 2. 把 JSON 中残留的偏好字段搬到 DB(只在 DB 没值时迁移)
|
|
24
|
+
* 3. mergeWithDefaults(raw) 得到 baseline
|
|
25
|
+
* 4. applyStoragePreferences 用 DB 覆盖 baseline 的偏好字段
|
|
26
|
+
* 5. 如果 JSON 里仍含偏好字段(来自 1),用 saveConfig 重写一次 JSON(剥离后)
|
|
27
|
+
*/
|
|
28
|
+
export declare function loadConfigWithStorage(configPath: string, storage: WandStorage): Promise<WandConfig>;
|
|
29
|
+
/**
|
|
30
|
+
* 老版本 JSON 里如果还存有偏好字段,且 DB 里对应 key 没有写过,
|
|
31
|
+
* 把 JSON 的值搬到 DB。注意:必须在 mergeWithDefaults 之前操作 raw input,
|
|
32
|
+
* 这样能区分"用户显式写过 X" 和 "X 是 mergeWithDefaults 注入的默认值"。
|
|
33
|
+
*/
|
|
34
|
+
export declare function migrateLegacyPreferencesToDb(rawJsonInput: Partial<WandConfig> | null | undefined, storage: WandStorage): void;
|
|
35
|
+
/**
|
|
36
|
+
* 用 DB 里的偏好覆盖 config 对象(in-place),返回同一个引用,
|
|
37
|
+
* 方便各 manager 持有引用后继续工作。DB 里没有的字段不动,保留 JSON 默认值。
|
|
38
|
+
*/
|
|
39
|
+
export declare function applyStoragePreferences(config: WandConfig, storage: WandStorage): WandConfig;
|
|
40
|
+
/** Write a single preference value to DB and (in-place) update the live config object. */
|
|
41
|
+
export declare function writePreferenceToStorage(config: WandConfig, storage: WandStorage, key: PreferenceKey, value: unknown): void;
|
|
8
42
|
export declare function normalizeCardDefaults(input: unknown): CardExpandDefaults;
|
|
9
43
|
export declare function isExecutionMode(value: unknown): value is ExecutionMode;
|
package/dist/config.js
CHANGED
|
@@ -5,6 +5,28 @@ import path from "node:path";
|
|
|
5
5
|
import process from "node:process";
|
|
6
6
|
const DEFAULT_CONFIG_DIR = ".wand";
|
|
7
7
|
const DEFAULT_CONFIG_FILE = "config.json";
|
|
8
|
+
/**
|
|
9
|
+
* 通过 UI 设置面板可改的"用户偏好"字段。这些字段从 SQLite app_config 表读取,
|
|
10
|
+
* 不再写入 ~/.wand/config.json。JSON 只保留服务部署/启动期参数(host/port/shell 等)。
|
|
11
|
+
*
|
|
12
|
+
* 升级路径:老 JSON 里仍存有这些字段时,首次启动会被搬到 DB(见 migrateLegacyPreferencesToDb),
|
|
13
|
+
* 然后下一次 saveConfig 写回 JSON 时它们会被剥离(见 stripPreferenceFields)。
|
|
14
|
+
*/
|
|
15
|
+
export const PREFERENCE_KEYS = [
|
|
16
|
+
"defaultMode",
|
|
17
|
+
"defaultCwd",
|
|
18
|
+
"defaultModel",
|
|
19
|
+
"structuredRunner",
|
|
20
|
+
"language",
|
|
21
|
+
"cardDefaults",
|
|
22
|
+
];
|
|
23
|
+
const PREFERENCE_KEY_SET = new Set(PREFERENCE_KEYS);
|
|
24
|
+
function preferenceStorageKey(key) {
|
|
25
|
+
return `pref:${key}`;
|
|
26
|
+
}
|
|
27
|
+
export function isPreferenceKey(key) {
|
|
28
|
+
return PREFERENCE_KEY_SET.has(key);
|
|
29
|
+
}
|
|
8
30
|
export const defaultConfig = () => ({
|
|
9
31
|
host: "127.0.0.1",
|
|
10
32
|
port: 8443,
|
|
@@ -80,9 +102,149 @@ export async function ensureConfig(configPath) {
|
|
|
80
102
|
return config;
|
|
81
103
|
}
|
|
82
104
|
}
|
|
105
|
+
/** saveConfig 写出时去掉偏好字段——这些已经移到 SQLite。 */
|
|
83
106
|
export async function saveConfig(configPath, config) {
|
|
84
107
|
await mkdir(path.dirname(configPath), { recursive: true });
|
|
85
|
-
await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
108
|
+
await writeFile(configPath, `${JSON.stringify(stripPreferenceFields(config), null, 2)}\n`, "utf8");
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* 启动期合并 JSON + DB 偏好。语义:
|
|
112
|
+
* 1. 读 raw JSON
|
|
113
|
+
* 2. 把 JSON 中残留的偏好字段搬到 DB(只在 DB 没值时迁移)
|
|
114
|
+
* 3. mergeWithDefaults(raw) 得到 baseline
|
|
115
|
+
* 4. applyStoragePreferences 用 DB 覆盖 baseline 的偏好字段
|
|
116
|
+
* 5. 如果 JSON 里仍含偏好字段(来自 1),用 saveConfig 重写一次 JSON(剥离后)
|
|
117
|
+
*/
|
|
118
|
+
export async function loadConfigWithStorage(configPath, storage) {
|
|
119
|
+
const dir = path.dirname(configPath);
|
|
120
|
+
await mkdir(dir, { recursive: true });
|
|
121
|
+
let rawInput = {};
|
|
122
|
+
let hadFile = false;
|
|
123
|
+
try {
|
|
124
|
+
const raw = await readFile(configPath, "utf8");
|
|
125
|
+
rawInput = JSON.parse(raw);
|
|
126
|
+
hadFile = true;
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
rawInput = {};
|
|
130
|
+
}
|
|
131
|
+
migrateLegacyPreferencesToDb(rawInput, storage);
|
|
132
|
+
const config = mergeWithDefaults(rawInput);
|
|
133
|
+
applyStoragePreferences(config, storage);
|
|
134
|
+
// 如果 JSON 里有偏好字段(说明是老版本配置或刚迁移),重写一次干净版本
|
|
135
|
+
const hasLegacyPrefs = PREFERENCE_KEYS.some((key) => key in rawInput);
|
|
136
|
+
if (!hadFile || hasLegacyPrefs) {
|
|
137
|
+
await saveConfig(configPath, config);
|
|
138
|
+
}
|
|
139
|
+
return config;
|
|
140
|
+
}
|
|
141
|
+
/** Build a JSON-safe view of WandConfig that excludes preference fields (which live in DB). */
|
|
142
|
+
function stripPreferenceFields(config) {
|
|
143
|
+
const out = { ...config };
|
|
144
|
+
for (const key of PREFERENCE_KEYS) {
|
|
145
|
+
delete out[key];
|
|
146
|
+
}
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* 老版本 JSON 里如果还存有偏好字段,且 DB 里对应 key 没有写过,
|
|
151
|
+
* 把 JSON 的值搬到 DB。注意:必须在 mergeWithDefaults 之前操作 raw input,
|
|
152
|
+
* 这样能区分"用户显式写过 X" 和 "X 是 mergeWithDefaults 注入的默认值"。
|
|
153
|
+
*/
|
|
154
|
+
export function migrateLegacyPreferencesToDb(rawJsonInput, storage) {
|
|
155
|
+
if (!rawJsonInput || typeof rawJsonInput !== "object")
|
|
156
|
+
return;
|
|
157
|
+
for (const key of PREFERENCE_KEYS) {
|
|
158
|
+
if (!(key in rawJsonInput))
|
|
159
|
+
continue;
|
|
160
|
+
const value = rawJsonInput[key];
|
|
161
|
+
if (value === undefined)
|
|
162
|
+
continue;
|
|
163
|
+
const dbKey = preferenceStorageKey(key);
|
|
164
|
+
if (storage.hasPreference(dbKey))
|
|
165
|
+
continue;
|
|
166
|
+
storage.setPreference(dbKey, value);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* 用 DB 里的偏好覆盖 config 对象(in-place),返回同一个引用,
|
|
171
|
+
* 方便各 manager 持有引用后继续工作。DB 里没有的字段不动,保留 JSON 默认值。
|
|
172
|
+
*/
|
|
173
|
+
export function applyStoragePreferences(config, storage) {
|
|
174
|
+
const defaults = defaultConfig();
|
|
175
|
+
if (storage.hasPreference(preferenceStorageKey("defaultMode"))) {
|
|
176
|
+
const v = storage.getPreference(preferenceStorageKey("defaultMode"), defaults.defaultMode);
|
|
177
|
+
if (isExecutionMode(v))
|
|
178
|
+
config.defaultMode = v;
|
|
179
|
+
}
|
|
180
|
+
if (storage.hasPreference(preferenceStorageKey("defaultCwd"))) {
|
|
181
|
+
const v = storage.getPreference(preferenceStorageKey("defaultCwd"), defaults.defaultCwd);
|
|
182
|
+
if (typeof v === "string" && v.trim())
|
|
183
|
+
config.defaultCwd = v;
|
|
184
|
+
}
|
|
185
|
+
if (storage.hasPreference(preferenceStorageKey("defaultModel"))) {
|
|
186
|
+
const v = storage.getPreference(preferenceStorageKey("defaultModel"), defaults.defaultModel ?? "");
|
|
187
|
+
if (typeof v === "string")
|
|
188
|
+
config.defaultModel = v.trim();
|
|
189
|
+
}
|
|
190
|
+
if (storage.hasPreference(preferenceStorageKey("structuredRunner"))) {
|
|
191
|
+
const v = storage.getPreference(preferenceStorageKey("structuredRunner"), defaults.structuredRunner ?? "cli");
|
|
192
|
+
if (v === "cli" || v === "sdk")
|
|
193
|
+
config.structuredRunner = v;
|
|
194
|
+
}
|
|
195
|
+
if (storage.hasPreference(preferenceStorageKey("language"))) {
|
|
196
|
+
const v = storage.getPreference(preferenceStorageKey("language"), defaults.language ?? "");
|
|
197
|
+
if (typeof v === "string")
|
|
198
|
+
config.language = v.trim();
|
|
199
|
+
}
|
|
200
|
+
if (storage.hasPreference(preferenceStorageKey("cardDefaults"))) {
|
|
201
|
+
const v = storage.getPreference(preferenceStorageKey("cardDefaults"), defaults.cardDefaults);
|
|
202
|
+
config.cardDefaults = normalizeCardDefaults(v);
|
|
203
|
+
}
|
|
204
|
+
return config;
|
|
205
|
+
}
|
|
206
|
+
/** Write a single preference value to DB and (in-place) update the live config object. */
|
|
207
|
+
export function writePreferenceToStorage(config, storage, key, value) {
|
|
208
|
+
const dbKey = preferenceStorageKey(key);
|
|
209
|
+
switch (key) {
|
|
210
|
+
case "defaultMode": {
|
|
211
|
+
if (!isExecutionMode(value))
|
|
212
|
+
throw new Error(`无效执行模式: ${value}`);
|
|
213
|
+
storage.setPreference(dbKey, value);
|
|
214
|
+
config.defaultMode = value;
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
case "defaultCwd": {
|
|
218
|
+
const v = typeof value === "string" ? value : "";
|
|
219
|
+
storage.setPreference(dbKey, v);
|
|
220
|
+
config.defaultCwd = v || defaultConfig().defaultCwd;
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
case "defaultModel": {
|
|
224
|
+
const v = typeof value === "string" ? value.trim() : "";
|
|
225
|
+
storage.setPreference(dbKey, v);
|
|
226
|
+
config.defaultModel = v;
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
case "structuredRunner": {
|
|
230
|
+
const v = value === "cli" ? "cli" : "sdk";
|
|
231
|
+
storage.setPreference(dbKey, v);
|
|
232
|
+
config.structuredRunner = v;
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
case "language": {
|
|
236
|
+
const v = typeof value === "string" ? value.trim() : "";
|
|
237
|
+
storage.setPreference(dbKey, v);
|
|
238
|
+
config.language = v;
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
case "cardDefaults": {
|
|
242
|
+
const normalized = normalizeCardDefaults(value);
|
|
243
|
+
storage.setPreference(dbKey, normalized);
|
|
244
|
+
config.cardDefaults = normalized;
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
86
248
|
}
|
|
87
249
|
function defaultCardExpandDefaults() {
|
|
88
250
|
return {
|
package/dist/server.js
CHANGED
|
@@ -13,7 +13,7 @@ import { WebSocketServer } from "ws";
|
|
|
13
13
|
import { ensureAvatarSeed, getAvatarSvg } from "./avatar.js";
|
|
14
14
|
import { createSession, revokeSession, setAuthStorage, validateSession } from "./auth.js";
|
|
15
15
|
import { ensureCertificates } from "./cert.js";
|
|
16
|
-
import { isExecutionMode,
|
|
16
|
+
import { isExecutionMode, PREFERENCE_KEYS, resolveConfigDir, saveConfig, writePreferenceToStorage, } from "./config.js";
|
|
17
17
|
import { getCachedModels, refreshModels } from "./models.js";
|
|
18
18
|
import { ProcessManager } from "./process-manager.js";
|
|
19
19
|
import { SessionLogger } from "./session-logger.js";
|
|
@@ -816,64 +816,61 @@ export async function startServer(config, configPath) {
|
|
|
816
816
|
});
|
|
817
817
|
app.post("/api/settings/config", async (req, res) => {
|
|
818
818
|
const body = req.body;
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
else if (field === "https") {
|
|
832
|
-
config.https = body.https === true;
|
|
833
|
-
}
|
|
834
|
-
else if (field === "defaultMode") {
|
|
835
|
-
if (!isExecutionMode(body.defaultMode)) {
|
|
836
|
-
res.status(400).json({ error: `无效执行模式: ${body.defaultMode}` });
|
|
837
|
-
return;
|
|
838
|
-
}
|
|
839
|
-
config.defaultMode = body.defaultMode;
|
|
840
|
-
}
|
|
841
|
-
else if (field === "host") {
|
|
842
|
-
config.host = String(body.host);
|
|
843
|
-
}
|
|
844
|
-
else if (field === "defaultCwd") {
|
|
845
|
-
config.defaultCwd = String(body.defaultCwd);
|
|
846
|
-
}
|
|
847
|
-
else if (field === "shell") {
|
|
848
|
-
config.shell = String(body.shell);
|
|
849
|
-
}
|
|
850
|
-
else if (field === "language") {
|
|
851
|
-
config.language = typeof body.language === "string" ? body.language.trim() : "";
|
|
852
|
-
}
|
|
853
|
-
else if (field === "defaultModel") {
|
|
854
|
-
config.defaultModel = typeof body.defaultModel === "string" ? body.defaultModel.trim() : "";
|
|
855
|
-
}
|
|
856
|
-
else if (field === "structuredRunner") {
|
|
857
|
-
config.structuredRunner = body.structuredRunner === "sdk" ? "sdk" : "cli";
|
|
819
|
+
// 部署字段:写 JSON,需要重启服务才生效(host/port/https 影响监听,shell 影响新 PTY)
|
|
820
|
+
const deployFields = ["host", "port", "https", "shell"];
|
|
821
|
+
let touchedDeployField = false;
|
|
822
|
+
let touchedPreferenceField = false;
|
|
823
|
+
for (const field of deployFields) {
|
|
824
|
+
if (!(field in body) || body[field] === undefined)
|
|
825
|
+
continue;
|
|
826
|
+
if (field === "port") {
|
|
827
|
+
const p = Number(body.port);
|
|
828
|
+
if (!Number.isInteger(p) || p < 1 || p > 65535) {
|
|
829
|
+
res.status(400).json({ error: `无效端口号: ${body.port}` });
|
|
830
|
+
return;
|
|
858
831
|
}
|
|
859
|
-
|
|
832
|
+
config.port = p;
|
|
860
833
|
}
|
|
834
|
+
else if (field === "https") {
|
|
835
|
+
config.https = body.https === true;
|
|
836
|
+
}
|
|
837
|
+
else if (field === "host") {
|
|
838
|
+
config.host = String(body.host);
|
|
839
|
+
}
|
|
840
|
+
else if (field === "shell") {
|
|
841
|
+
config.shell = String(body.shell);
|
|
842
|
+
}
|
|
843
|
+
touchedDeployField = true;
|
|
861
844
|
}
|
|
862
|
-
//
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
845
|
+
// 偏好字段:写 SQLite app_config,立即热生效(manager 持有 config 同一引用)。
|
|
846
|
+
// defaultMode 单独做严格校验以保留 400 错误响应,其余字段走 writePreferenceToStorage 的统一类型化处理。
|
|
847
|
+
if (body.defaultMode !== undefined && !isExecutionMode(body.defaultMode)) {
|
|
848
|
+
res.status(400).json({ error: `无效执行模式: ${body.defaultMode}` });
|
|
849
|
+
return;
|
|
866
850
|
}
|
|
867
|
-
|
|
851
|
+
for (const field of PREFERENCE_KEYS) {
|
|
852
|
+
if (!(field in body) || body[field] === undefined)
|
|
853
|
+
continue;
|
|
854
|
+
try {
|
|
855
|
+
writePreferenceToStorage(config, storage, field, body[field]);
|
|
856
|
+
}
|
|
857
|
+
catch (err) {
|
|
858
|
+
res.status(400).json({ error: getErrorMessage(err, `字段 ${field} 校验失败`) });
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
touchedPreferenceField = true;
|
|
862
|
+
}
|
|
863
|
+
if (!touchedDeployField && !touchedPreferenceField) {
|
|
868
864
|
res.status(400).json({ error: "没有可更新的配置字段。" });
|
|
869
865
|
return;
|
|
870
866
|
}
|
|
871
|
-
// cardDefaults-only changes don't need restart
|
|
872
|
-
const restartRequired = allowedFields.some((f) => f in body && body[f] !== undefined);
|
|
873
867
|
try {
|
|
874
|
-
|
|
868
|
+
if (touchedDeployField) {
|
|
869
|
+
await saveConfig(configPath, config);
|
|
870
|
+
}
|
|
875
871
|
const { password: _pw, ...safeConfig } = config;
|
|
876
|
-
|
|
872
|
+
// 只有部署字段才需要重启;偏好字段已经热生效。
|
|
873
|
+
res.json({ ok: true, config: safeConfig, restartRequired: touchedDeployField });
|
|
877
874
|
}
|
|
878
875
|
catch (error) {
|
|
879
876
|
res.status(500).json({ error: getErrorMessage(error, "保存配置失败。") });
|
package/dist/storage.d.ts
CHANGED
|
@@ -16,6 +16,12 @@ export declare class WandStorage {
|
|
|
16
16
|
setConfigValue(key: string, value: string): void;
|
|
17
17
|
/** Delete a config value */
|
|
18
18
|
deleteConfigValue(key: string): void;
|
|
19
|
+
/** 读取偏好。未设置或 JSON 解析失败时返回 fallback。 */
|
|
20
|
+
getPreference<T>(key: string, fallback: T): T;
|
|
21
|
+
/** 写入偏好。undefined / null 视为删除。 */
|
|
22
|
+
setPreference<T>(key: string, value: T | null | undefined): void;
|
|
23
|
+
/** 判断偏好是否在 DB 中存在(区别于值为 null/false/"")。 */
|
|
24
|
+
hasPreference(key: string): boolean;
|
|
19
25
|
/** Get password from database */
|
|
20
26
|
getPassword(): string | null;
|
|
21
27
|
/** Set password in database */
|
package/dist/storage.js
CHANGED
|
@@ -261,6 +261,35 @@ export class WandStorage {
|
|
|
261
261
|
deleteConfigValue(key) {
|
|
262
262
|
this.db.prepare("DELETE FROM app_config WHERE key = ?").run(key);
|
|
263
263
|
}
|
|
264
|
+
// ============ Preference Methods ============
|
|
265
|
+
// Preferences 与 getConfigValue/setConfigValue 共用 app_config 表,
|
|
266
|
+
// 区别在于:preference 自动 JSON 序列化/反序列化,并按"未设置时返回 fallback"语义返回。
|
|
267
|
+
// 用于存放 UI 设置面板可改的用户偏好(defaultMode/defaultModel/cardDefaults 等),
|
|
268
|
+
// 与 JSON 配置中的部署期参数(host/port/shell 等)分开。
|
|
269
|
+
/** 读取偏好。未设置或 JSON 解析失败时返回 fallback。 */
|
|
270
|
+
getPreference(key, fallback) {
|
|
271
|
+
const raw = this.getConfigValue(key);
|
|
272
|
+
if (raw === null)
|
|
273
|
+
return fallback;
|
|
274
|
+
try {
|
|
275
|
+
return JSON.parse(raw);
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
return fallback;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
/** 写入偏好。undefined / null 视为删除。 */
|
|
282
|
+
setPreference(key, value) {
|
|
283
|
+
if (value === undefined || value === null) {
|
|
284
|
+
this.deleteConfigValue(key);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
this.setConfigValue(key, JSON.stringify(value));
|
|
288
|
+
}
|
|
289
|
+
/** 判断偏好是否在 DB 中存在(区别于值为 null/false/"")。 */
|
|
290
|
+
hasPreference(key) {
|
|
291
|
+
return this.getConfigValue(key) !== null;
|
|
292
|
+
}
|
|
264
293
|
/** Get password from database */
|
|
265
294
|
getPassword() {
|
|
266
295
|
return this.getConfigValue("password");
|