@co0ontty/wand 4.6.1 → 4.7.0-beta.gd325de7

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.
@@ -1,6 +1,6 @@
1
1
  {
2
- "commit": "c86da1ca2559537e44e57d7a085250bae857a3cc",
3
- "builtAt": "2026-07-16T00:54:24.041Z",
4
- "version": "4.6.1",
5
- "channel": "stable"
2
+ "commit": "d325de70c28d8eda24f741294c3de61e96ee27e7",
3
+ "builtAt": "2026-07-16T11:22:44.578Z",
4
+ "version": "4.7.0-beta.gd325de7",
5
+ "channel": "beta"
6
6
  }
package/dist/cli.js CHANGED
@@ -96,9 +96,7 @@ async function main() {
96
96
  break;
97
97
  }
98
98
  case "config:show": {
99
- // 展示合并后的视图(JSON 部署字段 + DB 偏好字段)。password 脱敏:
100
- // 显示是否已自定义("<set>" / "change-me"),避免误把真密码截图分享出去;
101
- // 想看真值就直接读 DB(sqlite3 wand.db "SELECT * FROM app_config WHERE key='password'")。
99
+ // 展示合并后的视图(JSON 部署字段 + DB 偏好字段),但绝不打印运行时密钥。
102
100
  const { ensureDatabaseFile, resolveDatabasePath, WandStorage } = await import("./storage.js");
103
101
  const dbPath = resolveDatabasePath(configPath);
104
102
  ensureDatabaseFile(dbPath);
@@ -108,6 +106,11 @@ async function main() {
108
106
  const display = {
109
107
  ...config,
110
108
  password: config.password === "change-me" ? "change-me" : "<set>",
109
+ appSecret: config.appSecret ? "<set>" : "",
110
+ systemAi: config.systemAi ? {
111
+ ...config.systemAi,
112
+ apiKey: config.systemAi.apiKey ? "<set>" : "",
113
+ } : undefined,
111
114
  };
112
115
  process.stdout.write(`${JSON.stringify(display, null, 2)}\n`);
113
116
  }
package/dist/config.d.ts CHANGED
@@ -7,7 +7,7 @@ import type { WandStorage } from "./storage.js";
7
7
  * 升级路径:老 JSON 里仍存有这些字段时,首次启动会被搬到 DB(见 migrateLegacyPreferencesToDb),
8
8
  * 然后下一次 saveConfig 写回 JSON 时它们会被剥离(见 stripPreferenceFields)。
9
9
  */
10
- export declare const PREFERENCE_KEYS: readonly ["defaultProvider", "defaultSessionKind", "defaultMode", "defaultCwd", "defaultModel", "defaultCodexModel", "defaultOpenCodeModel", "commitCli", "commitModel", "defaultThinkingEffort", "structuredRunner", "language", "cardDefaults", "inheritEnv"];
10
+ export declare const PREFERENCE_KEYS: readonly ["defaultProvider", "defaultSessionKind", "defaultMode", "defaultCwd", "defaultModel", "defaultCodexModel", "defaultOpenCodeModel", "commitCli", "commitModel", "commitAiSource", "systemAi", "defaultThinkingEffort", "structuredRunner", "language", "cardDefaults", "inheritEnv"];
11
11
  export type PreferenceKey = (typeof PREFERENCE_KEYS)[number];
12
12
  export declare function isPreferenceKey(key: string): key is PreferenceKey;
13
13
  export declare const defaultConfig: () => WandConfig;
@@ -37,7 +37,11 @@ export declare function migrateLegacyPreferencesToDb(rawJsonInput: Partial<WandC
37
37
  */
38
38
  export declare function applyStoragePreferences(config: WandConfig, storage: WandStorage): WandConfig;
39
39
  /** Write a single preference value to DB and (in-place) update the live config object. */
40
- export declare function writePreferenceToStorage(config: WandConfig, storage: WandStorage, key: PreferenceKey, value: unknown): void;
40
+ export declare function writePreferenceToStorage(config: WandConfig, storage: WandStorage, key: PreferenceKey, value: unknown, options?: {
41
+ deferCommitAiValidation?: boolean;
42
+ }): void;
43
+ /** Validate the cross-field contract for Commit's direct-API source. */
44
+ export declare function validateCommitAiConfig(config: Pick<WandConfig, "commitAiSource" | "systemAi">): void;
41
45
  export declare function normalizeCardDefaults(input: unknown): CardExpandDefaults;
42
46
  export declare function isExecutionMode(value: unknown): value is ExecutionMode;
43
47
  export declare function getProviderDefaultModels(config: Pick<WandConfig, "defaultModel" | "defaultCodexModel" | "defaultOpenCodeModel">): {
package/dist/config.js CHANGED
@@ -4,6 +4,7 @@ import { chmod, mkdir, readFile, rename, unlink, writeFile } from "node:fs/promi
4
4
  import path from "node:path";
5
5
  import process from "node:process";
6
6
  import { isRunningAsRoot } from "./env-utils.js";
7
+ import { normalizeSystemAiConfig } from "./system-ai.js";
7
8
  function isThinkingEffort(value) {
8
9
  return value === "off"
9
10
  || value === "standard"
@@ -30,6 +31,8 @@ export const PREFERENCE_KEYS = [
30
31
  "defaultOpenCodeModel",
31
32
  "commitCli",
32
33
  "commitModel",
34
+ "commitAiSource",
35
+ "systemAi",
33
36
  "defaultThinkingEffort",
34
37
  "structuredRunner",
35
38
  "language",
@@ -70,6 +73,16 @@ export const defaultConfig = () => ({
70
73
  defaultOpenCodeModel: "",
71
74
  commitCli: "claude",
72
75
  commitModel: "",
76
+ commitAiSource: "cli",
77
+ systemAi: {
78
+ enabled: false,
79
+ protocol: "openai",
80
+ baseUrl: "",
81
+ apiKey: "",
82
+ model: "",
83
+ authHeader: "bearer",
84
+ source: "custom",
85
+ },
73
86
  defaultThinkingEffort: "off",
74
87
  structuredRunner: "cli",
75
88
  inheritEnv: true,
@@ -310,6 +323,15 @@ export function applyStoragePreferences(config, storage) {
310
323
  if (typeof v === "string")
311
324
  config.commitModel = v.trim();
312
325
  }
326
+ if (storage.hasPreference(preferenceStorageKey("commitAiSource"))) {
327
+ const v = storage.getPreference(preferenceStorageKey("commitAiSource"), defaults.commitAiSource ?? "cli");
328
+ if (v === "cli" || v === "api")
329
+ config.commitAiSource = v;
330
+ }
331
+ if (storage.hasPreference(preferenceStorageKey("systemAi"))) {
332
+ const v = storage.getPreference(preferenceStorageKey("systemAi"), defaults.systemAi);
333
+ config.systemAi = normalizeSystemAiConfig(v, defaults.systemAi);
334
+ }
313
335
  if (storage.hasPreference(preferenceStorageKey("defaultThinkingEffort"))) {
314
336
  const v = storage.getPreference(preferenceStorageKey("defaultThinkingEffort"), defaults.defaultThinkingEffort ?? "off");
315
337
  if (isThinkingEffort(v))
@@ -333,10 +355,11 @@ export function applyStoragePreferences(config, storage) {
333
355
  const v = storage.getPreference(preferenceStorageKey("inheritEnv"), defaults.inheritEnv ?? true);
334
356
  config.inheritEnv = v === false ? false : true;
335
357
  }
358
+ validateCommitAiConfig(config);
336
359
  return config;
337
360
  }
338
361
  /** Write a single preference value to DB and (in-place) update the live config object. */
339
- export function writePreferenceToStorage(config, storage, key, value) {
362
+ export function writePreferenceToStorage(config, storage, key, value, options = {}) {
340
363
  const dbKey = preferenceStorageKey(key);
341
364
  switch (key) {
342
365
  case "defaultProvider": {
@@ -397,6 +420,31 @@ export function writePreferenceToStorage(config, storage, key, value) {
397
420
  config.commitModel = v;
398
421
  break;
399
422
  }
423
+ case "commitAiSource": {
424
+ if (value !== "cli" && value !== "api")
425
+ throw new Error(`无效 commit AI 来源: ${String(value)}`);
426
+ if (!options.deferCommitAiValidation) {
427
+ validateCommitAiConfig({ ...config, commitAiSource: value });
428
+ }
429
+ storage.setPreference(dbKey, value);
430
+ config.commitAiSource = value;
431
+ break;
432
+ }
433
+ case "systemAi": {
434
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
435
+ throw new Error("systemAi 必须是对象。");
436
+ }
437
+ const normalized = normalizeSystemAiConfig(value, config.systemAi ?? defaultConfig().systemAi);
438
+ if (normalized.enabled && (!normalized.baseUrl || !normalized.apiKey || !normalized.model)) {
439
+ throw new Error("启用系统 AI API 时,地址、API Key 和模型不能为空。");
440
+ }
441
+ if (!options.deferCommitAiValidation) {
442
+ validateCommitAiConfig({ ...config, systemAi: normalized });
443
+ }
444
+ storage.setPreference(dbKey, normalized);
445
+ config.systemAi = normalized;
446
+ break;
447
+ }
400
448
  case "defaultThinkingEffort": {
401
449
  if (!isThinkingEffort(value))
402
450
  throw new Error(`无效思考深度: ${String(value)}`);
@@ -435,6 +483,15 @@ export function writePreferenceToStorage(config, storage, key, value) {
435
483
  }
436
484
  }
437
485
  }
486
+ /** Validate the cross-field contract for Commit's direct-API source. */
487
+ export function validateCommitAiConfig(config) {
488
+ if (config.commitAiSource !== "api")
489
+ return;
490
+ const directApi = config.systemAi;
491
+ if (!directApi?.baseUrl || !directApi.apiKey || !directApi.model) {
492
+ throw new Error("选择直连 API 生成 Commit 时,必须先填写 API 地址、API Key 和模型。");
493
+ }
494
+ }
438
495
  function defaultCardExpandDefaults() {
439
496
  return {
440
497
  editCards: false,
@@ -578,6 +635,7 @@ function mergeWithDefaults(input) {
578
635
  defaultOpenCodeModel: typeof input.defaultOpenCodeModel === "string" ? input.defaultOpenCodeModel.trim() : defaults.defaultOpenCodeModel,
579
636
  commitCli: input.commitCli === "codex" || input.commitCli === "opencode" ? input.commitCli : "claude",
580
637
  commitModel: typeof input.commitModel === "string" ? input.commitModel.trim() : defaults.commitModel,
638
+ commitAiSource: input.commitAiSource === "api" ? "api" : "cli",
581
639
  defaultThinkingEffort: isThinkingEffort(input.defaultThinkingEffort) ? input.defaultThinkingEffort : "off",
582
640
  structuredRunner: (input.structuredRunner === "sdk" || input.structuredRunner === "cli") ? input.structuredRunner : defaults.structuredRunner,
583
641
  inheritEnv: typeof input.inheritEnv === "boolean" ? input.inheritEnv : (defaults.inheritEnv ?? true),
@@ -29,6 +29,11 @@ export interface DistributionManagerOptions {
29
29
  fetch?: typeof fetch;
30
30
  now?: () => number;
31
31
  }
32
+ /**
33
+ * GitHub Release 正文还包含 Android/macOS/iOS 的安装指引;它们属于发布页,
34
+ * 不该出现在 Android 的更新弹窗。保留分隔线前的变更摘要,并兼容旧版正文。
35
+ */
36
+ export declare function extractUpdateSummary(releaseBody: string): string;
32
37
  export declare class DistributionManager {
33
38
  private readonly options;
34
39
  private readonly fetchImpl;
@@ -2,6 +2,14 @@ import { mkdir, readdir, readFile, stat } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { compareApkInstallOrder, compareSemver, extractSemver } from "./version-utils.js";
4
4
  const GITHUB_CACHE_TTL_MS = 10 * 60 * 1000;
5
+ /**
6
+ * GitHub Release 正文还包含 Android/macOS/iOS 的安装指引;它们属于发布页,
7
+ * 不该出现在 Android 的更新弹窗。保留分隔线前的变更摘要,并兼容旧版正文。
8
+ */
9
+ export function extractUpdateSummary(releaseBody) {
10
+ const summary = releaseBody.split(/\r?\n---\s*(?:\r?\n|$)/, 1)[0]?.trim() ?? "";
11
+ return summary.slice(0, 500);
12
+ }
5
13
  function asRecord(value) {
6
14
  return value && typeof value === "object" ? value : null;
7
15
  }
@@ -215,7 +223,7 @@ export class DistributionManager {
215
223
  downloadUrl: hit.asset.browser_download_url,
216
224
  fileName: hit.asset.name,
217
225
  size: hit.asset.size,
218
- ...(extension === ".apk" && hit.body ? { releaseNotes: hit.body.trim().slice(0, 500) } : {}),
226
+ ...(extension === ".apk" && hit.body ? { releaseNotes: extractUpdateSummary(hit.body) } : {}),
219
227
  };
220
228
  this.githubCache.set(extension, { asset, timestamp: this.now() });
221
229
  return asset;
@@ -10,6 +10,7 @@ interface QuickCommitOptions {
10
10
  model?: string | null;
11
11
  thinkingEffort?: SessionSnapshot["thinkingEffort"];
12
12
  inheritEnv?: boolean;
13
+ systemAi?: import("./types.js").SystemAiConfig;
13
14
  autoMessage: boolean;
14
15
  customMessage?: string;
15
16
  tag?: string;
@@ -27,6 +28,7 @@ interface QuickCommitAiOptions {
27
28
  model?: string | null;
28
29
  thinkingEffort?: SessionSnapshot["thinkingEffort"];
29
30
  inheritEnv?: boolean;
31
+ systemAi?: import("./types.js").SystemAiConfig;
30
32
  }
31
33
  export declare class QuickCommitError extends Error {
32
34
  readonly code: QuickCommitErrorCode;
@@ -1,6 +1,7 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { spawn } from "node:child_process";
3
3
  import { ClaudeRunError, runClaudePrint } from "./claude-sdk-runner.js";
4
+ import { callSystemAiText } from "./system-ai.js";
4
5
  import { buildChildEnv } from "./env-utils.js";
5
6
  import { runGit as runGitBase, runGitAsync as runGitAsyncBase, runGitRaw as runGitRawBase, runGitRawAsync as runGitRawAsyncBase, getGitErrorMessage, } from "./git-utils.js";
6
7
  import { thinkingEffortToClaudeCliEffort, thinkingEffortToCodexReasoningEffort, thinkingEffortToOpenCodeVariant } from "./structured-provider-common.js";
@@ -81,6 +82,58 @@ async function resolvePushRemoteAsync(cwd) {
81
82
  catch { /* use origin */ }
82
83
  return "origin";
83
84
  }
85
+ function githubSshPushUrl(remoteUrl) {
86
+ try {
87
+ const parsed = new URL(remoteUrl);
88
+ if (parsed.protocol !== "https:" || parsed.hostname.toLowerCase() !== "github.com")
89
+ return undefined;
90
+ const path = decodeURIComponent(parsed.pathname).replace(/^\/+|\/+$/g, "");
91
+ const parts = path.split("/");
92
+ if (parts.length !== 2 || parts.some((part) => !part))
93
+ return undefined;
94
+ return `git@github.com:${parts[0]}/${parts[1]}`;
95
+ }
96
+ catch {
97
+ return undefined;
98
+ }
99
+ }
100
+ function isHttpsCredentialError(error) {
101
+ return /could not read (?:User|Pass)name|authentication failed|terminal prompts disabled|credential[^\n]*(?:failed|missing)|http basic: access denied/i
102
+ .test(getGitErrorMessage(error));
103
+ }
104
+ async function resolveRemotePushUrl(cwd, remote) {
105
+ try {
106
+ return await runGitAsync(["remote", "get-url", "--push", remote], cwd);
107
+ }
108
+ catch {
109
+ return remote;
110
+ }
111
+ }
112
+ /**
113
+ * Push explicit refs through one transport seam. A Wand service deliberately disables interactive
114
+ * credential prompts, so a GitHub HTTPS remote that has no non-interactive credential would
115
+ * otherwise fail even when the same machine is already configured for GitHub SSH.
116
+ */
117
+ async function pushRemoteRefs(cwd, remote, options = [], refs = []) {
118
+ try {
119
+ await runGitAsync(["push", ...options, remote, ...refs], cwd, GIT_PUSH_TIMEOUT_MS);
120
+ return;
121
+ }
122
+ catch (error) {
123
+ if (!isHttpsCredentialError(error))
124
+ throw error;
125
+ const configuredUrl = await resolveRemotePushUrl(cwd, remote);
126
+ const sshUrl = githubSshPushUrl(configuredUrl);
127
+ if (!sshUrl)
128
+ throw error;
129
+ try {
130
+ await runGitAsync(["push", ...options, sshUrl, ...refs], cwd, GIT_PUSH_TIMEOUT_MS);
131
+ }
132
+ catch (sshError) {
133
+ throw new Error(`${getGitErrorMessage(error)}\nGitHub SSH 回退也失败:${getGitErrorMessage(sshError)}`, { cause: sshError });
134
+ }
135
+ }
136
+ }
84
137
  function unquotePath(raw) {
85
138
  if (raw.startsWith("\"") && raw.endsWith("\"")) {
86
139
  return raw.slice(1, -1).replace(/\\"/g, "\"").replace(/\\\\/g, "\\");
@@ -553,6 +606,8 @@ async function callOpenCodeText(prompt, cwd, opts) {
553
606
  return text;
554
607
  }
555
608
  async function callAiText(prompt, cwd, language, opts) {
609
+ if (opts.systemAi?.enabled)
610
+ return callSystemAiText(prompt, opts.systemAi);
556
611
  const provider = normalizeProvider(opts.provider);
557
612
  if (provider === "codex") {
558
613
  return callCodexText(prompt, cwd, opts);
@@ -751,21 +806,21 @@ async function doPush(opts) {
751
806
  try {
752
807
  if (pushCommits) {
753
808
  if (hasUpstream) {
754
- await runGitAsync(["push", recurseFlag], cwd, GIT_PUSH_TIMEOUT_MS);
809
+ await pushRemoteRefs(cwd, pushRemote, [recurseFlag]);
755
810
  }
756
811
  else {
757
- await runGitAsync(["push", "-u", recurseFlag, pushRemote, "HEAD"], cwd, GIT_PUSH_TIMEOUT_MS);
812
+ await pushRemoteRefs(cwd, pushRemote, ["-u", recurseFlag], ["HEAD"]);
758
813
  }
759
814
  pushedCommits = true;
760
815
  }
761
816
  if (pushTags) {
762
817
  if (Array.isArray(pushTags)) {
763
818
  for (const name of pushTags) {
764
- await runGitAsync(["push", pushRemote, `refs/tags/${name}`], cwd, GIT_PUSH_TIMEOUT_MS);
819
+ await pushRemoteRefs(cwd, pushRemote, [], [`refs/tags/${name}`]);
765
820
  }
766
821
  }
767
822
  else {
768
- await runGitAsync(["push", pushRemote, "--tags"], cwd, GIT_PUSH_TIMEOUT_MS);
823
+ await pushRemoteRefs(cwd, pushRemote, ["--tags"]);
769
824
  }
770
825
  pushedTags = true;
771
826
  }
@@ -1012,7 +1067,7 @@ async function pushSubmodules(parentCwd, subInfos, opts) {
1012
1067
  continue;
1013
1068
  }
1014
1069
  try {
1015
- await runGitAsync(["push", info.remote, `HEAD:refs/heads/${info.branch}`], subCwd, GIT_PUSH_TIMEOUT_MS);
1070
+ await pushRemoteRefs(subCwd, info.remote, [], [`HEAD:refs/heads/${info.branch}`]);
1016
1071
  }
1017
1072
  catch (error) {
1018
1073
  errors.push(`submodule ${info.path} 推送失败:${getGitErrorMessage(error)}`);
@@ -1020,7 +1075,7 @@ async function pushSubmodules(parentCwd, subInfos, opts) {
1020
1075
  }
1021
1076
  if (opts.pushTags && opts.tagName) {
1022
1077
  try {
1023
- await runGitAsync(["push", info.remote, `refs/tags/${opts.tagName}`], subCwd, GIT_PUSH_TIMEOUT_MS);
1078
+ await pushRemoteRefs(subCwd, info.remote, [], [`refs/tags/${opts.tagName}`]);
1024
1079
  }
1025
1080
  catch (error) {
1026
1081
  errors.push(`submodule ${info.path} 推送 tag 失败:${getGitErrorMessage(error)}`);
@@ -1191,7 +1246,11 @@ async function runQuickCommitFallbackCli(opts, priorError) {
1191
1246
  pushed: false,
1192
1247
  };
1193
1248
  }
1194
- function shouldFallbackToCli(error) {
1249
+ function shouldFallbackToCli(error, opts) {
1250
+ // An explicit direct-API selection is a hard boundary: never execute a local
1251
+ // provider CLI (especially the permission-bypassing fallback) behind it.
1252
+ if (opts.systemAi?.enabled)
1253
+ return false;
1195
1254
  return ![
1196
1255
  "CWD_MISSING",
1197
1256
  "NO_CWD",
@@ -1209,7 +1268,7 @@ export async function runQuickCommitWithFallback(opts) {
1209
1268
  return await runQuickCommit(opts);
1210
1269
  }
1211
1270
  catch (error) {
1212
- if (error instanceof QuickCommitError && shouldFallbackToCli(error)) {
1271
+ if (error instanceof QuickCommitError && shouldFallbackToCli(error, opts)) {
1213
1272
  return runQuickCommitFallbackCli(opts, error.message);
1214
1273
  }
1215
1274
  throw error;
@@ -1222,6 +1281,7 @@ export async function runQuickCommit(opts) {
1222
1281
  model: opts.model,
1223
1282
  thinkingEffort: opts.thinkingEffort,
1224
1283
  inheritEnv: opts.inheritEnv,
1284
+ systemAi: opts.systemAi,
1225
1285
  };
1226
1286
  await assertGitWorkTreeAsync(cwd);
1227
1287
  // 先 add 一次让我们能在 collectStagedDiff 看到完整改动(包含 submodule 指针),
@@ -1312,6 +1372,13 @@ export async function runQuickCommit(opts) {
1312
1372
  if (!tagName && autoTag) {
1313
1373
  tagName = await generateTagAfterCommit(cwd, language, message, ai);
1314
1374
  }
1375
+ let submodulePushInfos = submoduleOutcome.commits;
1376
+ if (submodule) {
1377
+ // The selected scope is the declared submodule set, not merely the submodules dirtied by this
1378
+ // request. A clean submodule can still have an unpushed HEAD or a pre-existing pointer change.
1379
+ const collected = await collectSubmodulesForPush(cwd);
1380
+ submodulePushInfos = collected.infos;
1381
+ }
1315
1382
  if (tagName) {
1316
1383
  try {
1317
1384
  await runGitAsync(["tag", tagName], cwd);
@@ -1319,9 +1386,10 @@ export async function runQuickCommit(opts) {
1319
1386
  catch (error) {
1320
1387
  throw new QuickCommitError(`git tag 失败:${getGitErrorMessage(error)}`, "GIT_TAG_FAILED");
1321
1388
  }
1322
- // 纳入 submodule 时给刚提交的 submodule 打同名 tag(非致命,失败不阻断父仓库流程)。
1323
- if (submodule && submoduleOutcome.commits.length > 0) {
1324
- await tagSubmodules(cwd, submoduleOutcome.commits, tagName);
1389
+ // 纳入 submodule 时给全部声明的 submodule 当前 HEAD 打同名 tag。这样纯指针变化或
1390
+ // 已提前提交但尚未推送的 submodule 也不会漏掉发布 tag。
1391
+ if (submodule && submodulePushInfos.length > 0) {
1392
+ await tagSubmodules(cwd, submodulePushInfos, tagName);
1325
1393
  }
1326
1394
  }
1327
1395
  let pushed = false;
@@ -1330,10 +1398,10 @@ export async function runQuickCommit(opts) {
1330
1398
  // 纳入 submodule:先把各 submodule 的 HEAD(+ 同名 tag)分别推到各自远端分支,
1331
1399
  // 解决 detached HEAD 无法被父仓库 on-demand 递归推送的问题;父仓库随后用
1332
1400
  // recurse=no 单独推(submodule 已就绪)。否则父仓库用 recurse=check 做安全校验。
1333
- const includeSub = !!submodule && submoduleOutcome.commits.length > 0;
1401
+ const includeSub = !!submodule && submodulePushInfos.length > 0;
1334
1402
  let subPushErrors = [];
1335
1403
  if (includeSub) {
1336
- const subPush = await pushSubmodules(cwd, submoduleOutcome.commits, { pushTags: !!tagName, tagName });
1404
+ const subPush = await pushSubmodules(cwd, submodulePushInfos, { pushTags: !!tagName, tagName });
1337
1405
  subPushErrors = subPush.errors;
1338
1406
  }
1339
1407
  if (subPushErrors.length > 0) {
@@ -19,6 +19,7 @@ import process from "node:process";
19
19
  import { promisify } from "node:util";
20
20
  import { whichSync } from "./path-repair.js";
21
21
  import { getErrorMessage } from "./error-utils.js";
22
+ import { compareWandInstallOrder, extractSemver } from "./version-utils.js";
22
23
  const execFileAsync = promisify(execFile);
23
24
  export const PACKAGE_NAME = "@co0ontty/wand";
24
25
  const PACKAGE_SCOPE = "@co0ontty";
@@ -53,10 +54,13 @@ function computeUpdateAvailable(currentVersion, latestVersion, channel) {
53
54
  const target = channel === "stable"
54
55
  ? getStableTagVersion(latestVersion)
55
56
  : cleanVersion(latestVersion);
56
- // npm's selected dist-tag is authoritative. Manual/local builds can have a
57
- // numerically higher, lower, invalid, or suffixed version; any mismatch must
58
- // still allow switching to the exact package selected by @latest or @beta.
59
- return current !== target;
57
+ // Invalid/manual labels cannot be ordered safely, so offer the selected
58
+ // dist-tag. Valid versions only update forward. In particular, Wand's local
59
+ // X.Y.Z-debug.* build represents code after X.Y.Z and must not be replaced
60
+ // by the same stable release as an apparent "upgrade".
61
+ if (!extractSemver(current) || !extractSemver(target))
62
+ return current !== target;
63
+ return compareWandInstallOrder(target, current) > 0;
60
64
  }
61
65
  export function buildPackageUpdateInfo(currentVersion, channel, latestVersion) {
62
66
  const latest = latestVersion?.trim() || null;
@@ -1564,7 +1564,7 @@ export class ProcessManager extends EventEmitter {
1564
1564
  if (this.disposed || !prompt || !record || record.title || this.topicRequests.has(id))
1565
1565
  return;
1566
1566
  this.topicRequests.add(id);
1567
- void generateSessionTopic(prompt, record.cwd, this.config.language)
1567
+ void generateSessionTopic(prompt, record.cwd, this.config.language, this.config.systemAi)
1568
1568
  .then(({ title, description }) => {
1569
1569
  if (!this.disposed && this.sessions.has(id))
1570
1570
  this.setSessionTopic(id, title, description);
@@ -1,5 +1,6 @@
1
+ import type { SystemAiConfig } from "./types.js";
1
2
  export declare class PromptOptimizeError extends Error {
2
3
  readonly code: string;
3
4
  constructor(message: string, code: string);
4
5
  }
5
- export declare function optimizePrompt(rawText: string, language: string, cwd?: string): Promise<string>;
6
+ export declare function optimizePrompt(rawText: string, language: string, cwd?: string, systemAi?: SystemAiConfig): Promise<string>;
@@ -1,4 +1,5 @@
1
1
  import { ClaudeRunError, runClaudePrint } from "./claude-sdk-runner.js";
2
+ import { callSystemAiText } from "./system-ai.js";
2
3
  const CLAUDE_TIMEOUT_MS = 60_000;
3
4
  const MAX_INPUT_LENGTH = 8000;
4
5
  export class PromptOptimizeError extends Error {
@@ -41,7 +42,7 @@ function buildOptimizePrompt(userInput, language) {
41
42
  userInput,
42
43
  ].join("\n");
43
44
  }
44
- export async function optimizePrompt(rawText, language, cwd) {
45
+ export async function optimizePrompt(rawText, language, cwd, systemAi) {
45
46
  const text = (rawText || "").trim();
46
47
  if (!text) {
47
48
  throw new PromptOptimizeError("请先输入要优化的内容。", "EMPTY_INPUT");
@@ -50,7 +51,7 @@ export async function optimizePrompt(rawText, language, cwd) {
50
51
  throw new PromptOptimizeError(`输入过长(${text.length} 字符),请缩短到 ${MAX_INPUT_LENGTH} 以内。`, "INPUT_TOO_LONG");
51
52
  }
52
53
  const prompt = buildOptimizePrompt(text, language);
53
- const raw = await callClaudeText(prompt, cwd, language);
54
+ const raw = systemAi?.enabled ? await callSystemAiText(prompt, systemAi) : await callClaudeText(prompt, cwd, language);
54
55
  const cleaned = raw
55
56
  .replace(/^```[a-zA-Z]*\n?/, "")
56
57
  .replace(/\n?```$/, "")
@@ -3,14 +3,20 @@ import path from "node:path";
3
3
  import { buildChildEnv } from "./env-utils.js";
4
4
  import { getErrorMessage } from "./error-utils.js";
5
5
  import { asyncRoute } from "./express-async.js";
6
- import { getProviderDefaultModels, PREFERENCE_KEYS, saveConfig, writePreferenceToStorage, } from "./config.js";
6
+ import { getProviderDefaultModels, PREFERENCE_KEYS, saveConfig, validateCommitAiConfig, writePreferenceToStorage, } from "./config.js";
7
7
  import { getCachedModels, refreshModels } from "./models.js";
8
8
  import { DEPLOYMENT_CONFIG_KEYS } from "./runtime-config.js";
9
+ import { discoverCliSystemAiConfig, normalizeSystemAiConfig } from "./system-ai.js";
9
10
  function publicConfig(config) {
10
11
  const { password: _password, appSecret: _appSecret, ...safe } = config;
11
12
  const defaultModels = getProviderDefaultModels(config);
12
13
  return {
13
14
  ...safe,
15
+ systemAi: safe.systemAi ? {
16
+ ...safe.systemAi,
17
+ apiKey: "",
18
+ hasApiKey: Boolean(safe.systemAi.apiKey),
19
+ } : undefined,
14
20
  defaultModel: defaultModels.claude,
15
21
  defaultCodexModel: defaultModels.codex,
16
22
  defaultOpenCodeModel: defaultModels.opencode,
@@ -104,6 +110,24 @@ export function registerSettingsRoutes(app, deps) {
104
110
  });
105
111
  res.json({ inheritEnv, total: entries.length, reveal, entries });
106
112
  });
113
+ app.post("/api/settings/system-ai/import", requireAdmin, (req, res) => {
114
+ const body = (req.body ?? {});
115
+ const source = body.source === "codex" || body.source === "opencode" || body.source === "claude"
116
+ ? body.source
117
+ : config.commitCli;
118
+ const imported = discoverCliSystemAiConfig(source);
119
+ if (!imported) {
120
+ res.status(404).json({ error: "没有在已配置的 CLI 文件中找到可直连的 API 地址、密钥和模型。" });
121
+ return;
122
+ }
123
+ const candidate = runtimeConfig.createCandidate();
124
+ writePreferenceToStorage(candidate, storage, "systemAi", {
125
+ ...imported,
126
+ enabled: candidate.systemAi?.enabled === true,
127
+ });
128
+ runtimeConfig.commit(candidate, new Set(["systemAi"]));
129
+ res.json({ ok: true, systemAi: (publicConfig(candidate).systemAi) });
130
+ });
107
131
  app.get("/api/app-connect-code", requireAdmin, (req, res) => {
108
132
  res.json(deps.resolveAppConnectCode(req));
109
133
  });
@@ -117,7 +141,9 @@ export function registerSettingsRoutes(app, deps) {
117
141
  setPreference(key, value) { stagedPreferences.push({ key, value }); },
118
142
  };
119
143
  const stagePreference = (field, value) => {
120
- writePreferenceToStorage(candidateConfig, stagingStorage, field, value);
144
+ writePreferenceToStorage(candidateConfig, stagingStorage, field, value, {
145
+ deferCommitAiValidation: true,
146
+ });
121
147
  stagedPreferenceFields.add(field);
122
148
  };
123
149
  let touchedDeployField = false;
@@ -159,12 +185,25 @@ export function registerSettingsRoutes(app, deps) {
159
185
  if (Object.hasOwn(body.defaultModels, "opencode"))
160
186
  stagePreference("defaultOpenCodeModel", body.defaultModels.opencode);
161
187
  }
188
+ if (body.systemAi !== undefined) {
189
+ if (!body.systemAi || typeof body.systemAi !== "object" || Array.isArray(body.systemAi)) {
190
+ throw new Error("systemAi 必须是对象。");
191
+ }
192
+ const previous = candidateConfig.systemAi;
193
+ const apiKey = typeof body.systemAi.apiKey === "string" && body.systemAi.apiKey.trim()
194
+ ? body.systemAi.apiKey.trim()
195
+ : previous?.apiKey ?? "";
196
+ stagePreference("systemAi", normalizeSystemAiConfig({ ...previous, ...body.systemAi, apiKey }, previous));
197
+ }
162
198
  for (const field of PREFERENCE_KEYS) {
199
+ if (field === "systemAi")
200
+ continue;
163
201
  const value = body[field];
164
202
  if (!(field in body) || value === undefined)
165
203
  continue;
166
204
  stagePreference(field, value);
167
205
  }
206
+ validateCommitAiConfig(candidateConfig);
168
207
  }
169
208
  catch (error) {
170
209
  res.status(400).json({ error: getErrorMessage(error, "配置校验失败。") });
package/dist/server.js CHANGED
@@ -59,18 +59,12 @@ async function checkLatestPackageVersion(channel, forceRefresh = false) {
59
59
  const now = Date.now();
60
60
  const cached = packageUpdateCache.get(channel);
61
61
  if (!forceRefresh && cached && now - cached.timestamp < CACHE_TTL_MS) {
62
- return applyLocalBuildUpdateOverride(cached.info);
62
+ return cached.info;
63
63
  }
64
64
  const info = await checkPackageUpdateAsync(PKG_VERSION, channel);
65
65
  if (info.latest) {
66
66
  packageUpdateCache.set(channel, { info, timestamp: now });
67
67
  }
68
- return applyLocalBuildUpdateOverride(info);
69
- }
70
- function applyLocalBuildUpdateOverride(info) {
71
- if (info.channel === "stable" && BUILD_INFO.channel === "beta" && info.latest) {
72
- return { ...info, updateAvailable: true };
73
- }
74
68
  return info;
75
69
  }
76
70
  /** 读取 dist/build-info.json(由 scripts/stamp-build-info.js 在 build 时生成)。 */
@@ -924,7 +918,7 @@ export async function startServer(config, configPath, options = {}) {
924
918
  cwd = snap.cwd;
925
919
  }
926
920
  try {
927
- const optimized = await optimizePrompt(text, config.language ?? "", cwd);
921
+ const optimized = await optimizePrompt(text, config.language ?? "", cwd, config.systemAi);
928
922
  res.json({ optimized });
929
923
  }
930
924
  catch (error) {
@@ -1,9 +1,10 @@
1
- import type { SessionProvider, SessionSnapshot, WandConfig } from "./types.js";
1
+ import type { SessionProvider, SessionSnapshot, SystemAiConfig, WandConfig } from "./types.js";
2
2
  export interface SessionAiContext {
3
3
  provider: SessionProvider;
4
4
  model?: string;
5
5
  thinkingEffort: SessionSnapshot["thinkingEffort"];
6
6
  inheritEnv?: boolean;
7
+ systemAi?: SystemAiConfig;
7
8
  }
8
9
  /**
9
10
  * Resolve the provider from every representation used by current and legacy
@@ -14,4 +15,4 @@ export declare function resolveSessionProvider(snapshot: Pick<SessionSnapshot, "
14
15
  /** Build the provider-specific settings used by session-adjacent AI actions. */
15
16
  export declare function resolveSessionAiContext(snapshot: Pick<SessionSnapshot, "provider" | "structuredState" | "runner" | "command" | "selectedModel" | "thinkingEffort">, config: Pick<WandConfig, "defaultModel" | "defaultCodexModel" | "defaultOpenCodeModel" | "defaultThinkingEffort" | "inheritEnv">): SessionAiContext;
16
17
  /** Build the AI context for quick-commit actions from their global preferences. */
17
- export declare function resolveCommitAiContext(snapshot: Pick<SessionSnapshot, "provider" | "structuredState" | "runner" | "command" | "selectedModel" | "thinkingEffort">, config: Pick<WandConfig, "defaultModel" | "defaultCodexModel" | "defaultOpenCodeModel" | "defaultThinkingEffort" | "inheritEnv" | "commitCli" | "commitModel">): SessionAiContext;
18
+ export declare function resolveCommitAiContext(snapshot: Pick<SessionSnapshot, "provider" | "structuredState" | "runner" | "command" | "selectedModel" | "thinkingEffort">, config: Pick<WandConfig, "defaultModel" | "defaultCodexModel" | "defaultOpenCodeModel" | "defaultThinkingEffort" | "inheritEnv" | "commitCli" | "commitModel" | "commitAiSource" | "systemAi">): SessionAiContext;
@@ -41,9 +41,17 @@ export function resolveSessionAiContext(snapshot, config) {
41
41
  /** Build the AI context for quick-commit actions from their global preferences. */
42
42
  export function resolveCommitAiContext(snapshot, config) {
43
43
  const sessionContext = resolveSessionAiContext(snapshot, config);
44
+ const directApi = config.systemAi ?? {
45
+ enabled: true,
46
+ protocol: "openai",
47
+ baseUrl: "",
48
+ apiKey: "",
49
+ model: "",
50
+ };
44
51
  return {
45
52
  ...sessionContext,
46
53
  provider: config.commitCli === "codex" || config.commitCli === "opencode" ? config.commitCli : "claude",
47
54
  model: normalizeModel(config.commitModel),
55
+ ...(config.commitAiSource === "api" ? { systemAi: { ...directApi, enabled: true } } : {}),
48
56
  };
49
57
  }
@@ -1,5 +1,6 @@
1
+ import type { SystemAiConfig } from "./types.js";
1
2
  export interface SessionTopic {
2
3
  title: string;
3
4
  description: string;
4
5
  }
5
- export declare function generateSessionTopic(userMessage: string, cwd?: string, language?: string): Promise<SessionTopic>;
6
+ export declare function generateSessionTopic(userMessage: string, cwd?: string, language?: string, systemAi?: SystemAiConfig): Promise<SessionTopic>;