@co0ontty/wand 1.21.5 → 1.21.7

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env -S node --disable-warning=ExperimentalWarning
2
2
  import process from "node:process";
3
- import { ensureConfig, hasConfigFile, isExecutionMode, resolveConfigPath, saveConfig } from "./config.js";
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
- const config = await ensureConfig(configPath);
54
- process.stdout.write(`${JSON.stringify(config, null, 2)}\n`);
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 config = await ensureConfig(configPath);
64
- const nextConfig = setConfigValue(config, key, value);
65
- await saveConfig(configPath, nextConfig);
66
- process.stdout.write(`[wand] Updated ${key} in ${configPath}\n`);
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
- const config = await ensureConfig(configPath);
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 ?? "sdk");
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, normalizeCardDefaults, resolveConfigDir, saveConfig } from "./config.js";
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";
@@ -724,7 +724,7 @@ export async function startServer(config, configPath) {
724
724
  defaultMode: config.defaultMode,
725
725
  defaultCwd: config.defaultCwd,
726
726
  commandPresets: config.commandPresets,
727
- structuredRunner: config.structuredRunner ?? "cli",
727
+ structuredRunner: config.structuredRunner ?? "sdk",
728
728
  structuredRunners: [
729
729
  { label: "Claude Structured", runner: "claude-cli-print" },
730
730
  { label: "Codex Structured", runner: "codex-cli-exec" },
@@ -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
- const allowedFields = ["host", "port", "https", "defaultMode", "defaultCwd", "shell", "language", "defaultModel", "structuredRunner"];
820
- let changed = false;
821
- for (const field of allowedFields) {
822
- if (field in body && body[field] !== undefined) {
823
- if (field === "port") {
824
- const p = Number(body.port);
825
- if (!Number.isInteger(p) || p < 1 || p > 65535) {
826
- res.status(400).json({ error: `无效端口号: ${body.port}` });
827
- return;
828
- }
829
- config.port = p;
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
- changed = true;
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
- // Handle cardDefaults separately (nested object, no restart needed)
863
- if (body.cardDefaults !== undefined) {
864
- config.cardDefaults = normalizeCardDefaults(body.cardDefaults);
865
- changed = true;
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
- if (!changed) {
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
- await saveConfig(configPath, config);
868
+ if (touchedDeployField) {
869
+ await saveConfig(configPath, config);
870
+ }
875
871
  const { password: _pw, ...safeConfig } = config;
876
- res.json({ ok: true, config: safeConfig, restartRequired });
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");