@co0ontty/wand 4.48.0 → 4.49.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.
- package/dist/build-info.json +3 -3
- package/dist/config.d.ts +6 -0
- package/dist/config.js +24 -1
- package/dist/distribution-manager.d.ts +21 -1
- package/dist/distribution-manager.js +58 -3
- package/dist/process-manager.js +6 -3
- package/dist/pty-shell-launch.js +15 -3
- package/dist/server-update-routes.d.ts +3 -0
- package/dist/server-update-routes.js +53 -1
- package/dist/server-workspace-routes.js +1 -0
- package/dist/session-topic.d.ts +5 -0
- package/dist/session-topic.js +7 -0
- package/dist/web-ui/content/scripts.js +52 -52
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/embedded-assets.d.ts +1 -1
- package/dist/web-ui/embedded-assets.js +3 -3
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"commit": "
|
|
3
|
-
"builtAt": "2026-08-
|
|
4
|
-
"version": "4.
|
|
2
|
+
"commit": "e521372a025630229f6976b2c6800084e851a8e7",
|
|
3
|
+
"builtAt": "2026-08-23T23:10:59.104Z",
|
|
4
|
+
"version": "4.49.0",
|
|
5
5
|
"channel": "stable"
|
|
6
6
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -10,6 +10,12 @@ import type { WandStorage } from "./storage.js";
|
|
|
10
10
|
export declare const PREFERENCE_KEYS: readonly ["defaultProvider", "defaultSessionKind", "defaultTaskWorktree", "defaultMode", "defaultCwd", "defaultModel", "defaultCodexModel", "defaultOpenCodeModel", "defaultGrokModel", "defaultQoderModel", "defaultPiModel", "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
|
+
/**
|
|
14
|
+
* The user's login shell from passwd, then $SHELL, then a platform default.
|
|
15
|
+
* Launchd / systemd usually omit $SHELL, so falling back to /bin/bash made PTY
|
|
16
|
+
* sessions look like a generic sh even when the account default is zsh.
|
|
17
|
+
*/
|
|
18
|
+
export declare function resolveDefaultShell(): string;
|
|
13
19
|
export declare const defaultConfig: () => WandConfig;
|
|
14
20
|
export declare function resolveConfigPath(inputPath?: string): string;
|
|
15
21
|
export declare function resolveConfigDir(configPath: string): string;
|
package/dist/config.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { chmod, mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
4
|
+
import os from "node:os";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
import process from "node:process";
|
|
6
7
|
import { isRunningAsRoot } from "./env-utils.js";
|
|
@@ -50,6 +51,28 @@ function preferenceStorageKey(key) {
|
|
|
50
51
|
export function isPreferenceKey(key) {
|
|
51
52
|
return PREFERENCE_KEY_SET.has(key);
|
|
52
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* The user's login shell from passwd, then $SHELL, then a platform default.
|
|
56
|
+
* Launchd / systemd usually omit $SHELL, so falling back to /bin/bash made PTY
|
|
57
|
+
* sessions look like a generic sh even when the account default is zsh.
|
|
58
|
+
*/
|
|
59
|
+
export function resolveDefaultShell() {
|
|
60
|
+
if (process.platform === "win32") {
|
|
61
|
+
return process.env.COMSPEC || "cmd.exe";
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
const loginShell = os.userInfo().shell?.trim();
|
|
65
|
+
if (loginShell)
|
|
66
|
+
return loginShell;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// userInfo() throws when the process has no passwd entry.
|
|
70
|
+
}
|
|
71
|
+
const envShell = process.env.SHELL?.trim();
|
|
72
|
+
if (envShell)
|
|
73
|
+
return envShell;
|
|
74
|
+
return process.platform === "darwin" ? "/bin/zsh" : "/bin/bash";
|
|
75
|
+
}
|
|
53
76
|
export const defaultConfig = () => ({
|
|
54
77
|
host: "127.0.0.1",
|
|
55
78
|
port: 8443,
|
|
@@ -64,7 +87,7 @@ export const defaultConfig = () => ({
|
|
|
64
87
|
// 注意:defaultMode 是偏好字段,只存 SQLite、不写 config.json(见 stripPreferenceFields),
|
|
65
88
|
// 这里仅作为「用户从未在设置里显式选过模式」时的回落值——显式选择始终优先。
|
|
66
89
|
defaultMode: isRunningAsRoot() ? "default" : "managed",
|
|
67
|
-
shell:
|
|
90
|
+
shell: resolveDefaultShell(),
|
|
68
91
|
defaultCwd: process.cwd(),
|
|
69
92
|
startupCommands: [],
|
|
70
93
|
allowedCommandPrefixes: [],
|
|
@@ -16,7 +16,13 @@ export interface ResolvedDistributionAsset {
|
|
|
16
16
|
size: number;
|
|
17
17
|
source: "local" | "github";
|
|
18
18
|
releaseNotes?: string;
|
|
19
|
-
/**
|
|
19
|
+
/** 本地文件哈希或 GitHub Release asset.digest;客户端下载后校验。 */
|
|
20
|
+
sha256?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface GitHubApkDownload {
|
|
23
|
+
remoteUrl: string;
|
|
24
|
+
fileName: string;
|
|
25
|
+
size: number;
|
|
20
26
|
sha256?: string;
|
|
21
27
|
}
|
|
22
28
|
export interface DistributionSettings {
|
|
@@ -37,6 +43,15 @@ export interface DistributionManagerOptions {
|
|
|
37
43
|
* 不该出现在 Android 的更新弹窗。保留分隔线前的变更摘要,并兼容旧版正文。
|
|
38
44
|
*/
|
|
39
45
|
export declare function extractUpdateSummary(releaseBody: string): string;
|
|
46
|
+
/**
|
|
47
|
+
* GitHub Release 文件名带 semver build metadata(`wand-v4.48.0+202608232136.apk`)。
|
|
48
|
+
* `+` 在 content URI / 部分 OEM 安装器里会被当成空格,下载成功后无法拉起安装。
|
|
49
|
+
* 落盘和对外 fileName 都改成 `-`,版本比较仍用原始 GitHub 文件名提取的 version。
|
|
50
|
+
*/
|
|
51
|
+
export declare function sanitizeApkFileName(fileName: string): string;
|
|
52
|
+
/** 解析 GitHub Release asset.digest(`sha256:<hex>`);格式不对返回 undefined。 */
|
|
53
|
+
export declare function parseGithubDigest(digest: string | undefined): string | undefined;
|
|
54
|
+
export declare function githubApkProxyPath(channel?: ApkUpdateChannel): string;
|
|
40
55
|
export declare class DistributionManager {
|
|
41
56
|
private readonly options;
|
|
42
57
|
private readonly fetchImpl;
|
|
@@ -44,6 +59,11 @@ export declare class DistributionManager {
|
|
|
44
59
|
private readonly githubCache;
|
|
45
60
|
constructor(options: DistributionManagerOptions);
|
|
46
61
|
resolveLatestApk(channel: ApkUpdateChannel): Promise<ResolvedDistributionAsset | null>;
|
|
62
|
+
/**
|
|
63
|
+
* 给 `/android/download?source=github` 用:原始 GitHub URL 只留在服务端,
|
|
64
|
+
* 客户端只拿到同源代理地址。fileName 已去掉 `+`。
|
|
65
|
+
*/
|
|
66
|
+
resolveGitHubApkDownload(): Promise<GitHubApkDownload | null>;
|
|
47
67
|
resolveAndroidDownload(channel?: ApkUpdateChannel): Promise<LocalDistributionAsset | null>;
|
|
48
68
|
/**
|
|
49
69
|
* 清理 Android 分发目录,只保留最新的一个 APK。在设备开始下载 Beta 包时
|
|
@@ -12,6 +12,30 @@ export function extractUpdateSummary(releaseBody) {
|
|
|
12
12
|
const summary = releaseBody.split(/\r?\n---\s*(?:\r?\n|$)/, 1)[0]?.trim() ?? "";
|
|
13
13
|
return summary.slice(0, 500);
|
|
14
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* GitHub Release 文件名带 semver build metadata(`wand-v4.48.0+202608232136.apk`)。
|
|
17
|
+
* `+` 在 content URI / 部分 OEM 安装器里会被当成空格,下载成功后无法拉起安装。
|
|
18
|
+
* 落盘和对外 fileName 都改成 `-`,版本比较仍用原始 GitHub 文件名提取的 version。
|
|
19
|
+
*/
|
|
20
|
+
export function sanitizeApkFileName(fileName) {
|
|
21
|
+
const base = fileName.replace(/^.*[/\\]/, "").trim() || "wand-update.apk";
|
|
22
|
+
const replaced = base.replaceAll("+", "-").replace(/[^A-Za-z0-9._-]+/g, "-");
|
|
23
|
+
if (!replaced.toLowerCase().endsWith(".apk"))
|
|
24
|
+
return `${replaced || "wand-update"}.apk`;
|
|
25
|
+
return replaced;
|
|
26
|
+
}
|
|
27
|
+
/** 解析 GitHub Release asset.digest(`sha256:<hex>`);格式不对返回 undefined。 */
|
|
28
|
+
export function parseGithubDigest(digest) {
|
|
29
|
+
if (!digest)
|
|
30
|
+
return undefined;
|
|
31
|
+
const match = /^sha256:([a-fA-F0-9]{64})$/.exec(digest.trim());
|
|
32
|
+
return match ? match[1].toLowerCase() : undefined;
|
|
33
|
+
}
|
|
34
|
+
export function githubApkProxyPath(channel) {
|
|
35
|
+
return channel
|
|
36
|
+
? `/android/download?channel=${channel}&source=github`
|
|
37
|
+
: "/android/download?source=github";
|
|
38
|
+
}
|
|
15
39
|
function asRecord(value) {
|
|
16
40
|
return value && typeof value === "object" ? value : null;
|
|
17
41
|
}
|
|
@@ -70,7 +94,14 @@ export class DistributionManager {
|
|
|
70
94
|
size: localApk.size,
|
|
71
95
|
source: "local",
|
|
72
96
|
} : null;
|
|
73
|
-
const github = githubApk ? {
|
|
97
|
+
const github = githubApk ? {
|
|
98
|
+
...githubApk,
|
|
99
|
+
source: "github",
|
|
100
|
+
// 手机不要直连 GitHub CDN:检查接口能通只说明 wand server 能访问 api.github.com,
|
|
101
|
+
// objects/release-assets.githubusercontent.com 在移动网上经常被重置。
|
|
102
|
+
fileName: sanitizeApkFileName(githubApk.fileName),
|
|
103
|
+
downloadUrl: githubApkProxyPath(channel),
|
|
104
|
+
} : null;
|
|
74
105
|
let winner;
|
|
75
106
|
if (local && github) {
|
|
76
107
|
winner = compareApkInstallOrder(github.version, local.version) > 0 ? github : local;
|
|
@@ -78,7 +109,7 @@ export class DistributionManager {
|
|
|
78
109
|
else {
|
|
79
110
|
winner = local ?? github;
|
|
80
111
|
}
|
|
81
|
-
//
|
|
112
|
+
// 本地分发现算 SHA-256;GitHub 来源用 Release digest,随 spread 带上。
|
|
82
113
|
if (winner?.source === "local" && localApk) {
|
|
83
114
|
try {
|
|
84
115
|
const fileStat = await stat(localApk.filePath);
|
|
@@ -93,6 +124,21 @@ export class DistributionManager {
|
|
|
93
124
|
}
|
|
94
125
|
return winner;
|
|
95
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* 给 `/android/download?source=github` 用:原始 GitHub URL 只留在服务端,
|
|
129
|
+
* 客户端只拿到同源代理地址。fileName 已去掉 `+`。
|
|
130
|
+
*/
|
|
131
|
+
async resolveGitHubApkDownload() {
|
|
132
|
+
const asset = await this.fetchGitHubAsset(".apk");
|
|
133
|
+
if (!asset)
|
|
134
|
+
return null;
|
|
135
|
+
return {
|
|
136
|
+
remoteUrl: asset.downloadUrl,
|
|
137
|
+
fileName: sanitizeApkFileName(asset.fileName),
|
|
138
|
+
size: asset.size,
|
|
139
|
+
sha256: asset.sha256,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
96
142
|
async resolveAndroidDownload(channel = "beta") {
|
|
97
143
|
await this.refreshConfig();
|
|
98
144
|
const { config, configDir } = this.options;
|
|
@@ -376,11 +422,13 @@ export class DistributionManager {
|
|
|
376
422
|
const version = extractArtifactVersion(hit.asset.name, extension)
|
|
377
423
|
?? extractArtifactVersion(hit.tagName, extension)
|
|
378
424
|
?? hit.tagName.replace(/^v/, "");
|
|
425
|
+
const sha256 = parseGithubDigest(hit.asset.digest);
|
|
379
426
|
const asset = {
|
|
380
427
|
version,
|
|
381
428
|
downloadUrl: hit.asset.browser_download_url,
|
|
382
429
|
fileName: hit.asset.name,
|
|
383
430
|
size: hit.asset.size,
|
|
431
|
+
...(sha256 ? { sha256 } : {}),
|
|
384
432
|
...(extension === ".apk" && hit.body ? { releaseNotes: extractUpdateSummary(hit.body) } : {}),
|
|
385
433
|
};
|
|
386
434
|
this.githubCache.set(extension, { asset, timestamp: this.now() });
|
|
@@ -409,11 +457,18 @@ export class DistributionManager {
|
|
|
409
457
|
return null;
|
|
410
458
|
}
|
|
411
459
|
buildSettings(kind, directory, enabled, local, github) {
|
|
412
|
-
|
|
460
|
+
let selected = local
|
|
413
461
|
? { ...local, source: "local" }
|
|
414
462
|
: github
|
|
415
463
|
? { ...github, updatedAt: null, source: "github" }
|
|
416
464
|
: null;
|
|
465
|
+
if (kind === "apk" && selected?.source === "github" && github) {
|
|
466
|
+
selected = {
|
|
467
|
+
...selected,
|
|
468
|
+
fileName: sanitizeApkFileName(github.fileName),
|
|
469
|
+
downloadUrl: githubApkProxyPath(),
|
|
470
|
+
};
|
|
471
|
+
}
|
|
417
472
|
const hasKey = kind === "apk" ? "hasApk" : kind === "dmg" ? "hasDmg" : "hasIpa";
|
|
418
473
|
const dirKey = kind === "apk" ? "apkDir" : kind === "dmg" ? "dmgDir" : "ipaDir";
|
|
419
474
|
return {
|
package/dist/process-manager.js
CHANGED
|
@@ -14,7 +14,7 @@ import { buildLanguageDirective, buildManagedAutonomyDirective } from "./languag
|
|
|
14
14
|
import { prepareSessionWorktree } from "./git-worktree.js";
|
|
15
15
|
import { getProviderCommandSessionId, getProviderResumeCommandSessionId } from "./resume-policy.js";
|
|
16
16
|
import { normalizeThinkingEffort, thinkingEffortToClaudeCliEffort, thinkingEffortToClaudeSlashEffort, thinkingEffortToCodexReasoningEffort, thinkingEffortToOpenCodeVariant, thinkingEffortToPiLevel } from "./structured-provider-common.js";
|
|
17
|
-
import { SessionTopicCoordinator } from "./session-topic.js";
|
|
17
|
+
import { SessionTopicCoordinator, shouldGenerateSessionTopicFromPtyInput } from "./session-topic.js";
|
|
18
18
|
import { getErrorMessage } from "./error-utils.js";
|
|
19
19
|
import { resolveSystemAiContext } from "./session-ai-context.js";
|
|
20
20
|
import { resolveSessionCwd } from "./session-cwd.js";
|
|
@@ -1116,7 +1116,10 @@ export class ProcessManager extends EventEmitter {
|
|
|
1116
1116
|
env: buildChildEnv(this.config.inheritEnv !== false, {
|
|
1117
1117
|
WAND_MODE: effectiveMode,
|
|
1118
1118
|
WAND_AUTO_CONFIRM: record.autoApprovePermissions ? "1" : "0",
|
|
1119
|
-
WAND_AUTO_EDIT: effectiveMode === "auto-edit" ? "1" : "0"
|
|
1119
|
+
WAND_AUTO_EDIT: effectiveMode === "auto-edit" ? "1" : "0",
|
|
1120
|
+
SHELL: this.config.shell,
|
|
1121
|
+
LANG: process.env.LANG || process.env.LC_ALL || process.env.LC_CTYPE || "C.UTF-8",
|
|
1122
|
+
LC_CTYPE: process.env.LC_CTYPE || process.env.LANG || process.env.LC_ALL || "C.UTF-8",
|
|
1120
1123
|
}),
|
|
1121
1124
|
name: "xterm-256color",
|
|
1122
1125
|
// 使用 record 上由前端协商好的真实尺寸,避免"先 120 列、几百毫秒后再 resize"
|
|
@@ -1494,7 +1497,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
1494
1497
|
console.error(`[ProcessManager] Rejecting input: session ${id} has no PTY`);
|
|
1495
1498
|
throw new SessionInputError("Session is not running.", "SESSION_NO_PTY", id, record.status);
|
|
1496
1499
|
}
|
|
1497
|
-
if (view
|
|
1500
|
+
if (shouldGenerateSessionTopicFromPtyInput(view, shortcutKey))
|
|
1498
1501
|
this.maybeGenerateSessionTopic(id, input);
|
|
1499
1502
|
// Log shortcut key interactions for auto-confirm and mode analysis
|
|
1500
1503
|
if (shortcutKey) {
|
package/dist/pty-shell-launch.js
CHANGED
|
@@ -91,6 +91,15 @@ export class PtyCliExitMarker {
|
|
|
91
91
|
return { data: combined, exitCode: null };
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
|
+
function buildTerminalRestoreCommand() {
|
|
95
|
+
// Provider TUIs often die on Ctrl+C without leaving the alternate screen or
|
|
96
|
+
// resetting mouse / bracketed-paste / application-keypad modes. Restore a
|
|
97
|
+
// normal interactive tty before handing the session to the user's shell.
|
|
98
|
+
return [
|
|
99
|
+
"printf '\\033[?1049l\\033[?25h\\033[m\\033[?1000l\\033[?1002l\\033[?1003l\\033[?1006l\\033[?2004l\\r\\n'",
|
|
100
|
+
"stty sane 2>/dev/null || :",
|
|
101
|
+
].join("; ");
|
|
102
|
+
}
|
|
94
103
|
function buildPosixProviderShellCommand(command, shell, marker) {
|
|
95
104
|
// The non-default SIGINT trap keeps the launcher shell alive when Ctrl+C
|
|
96
105
|
// terminates its foreground CLI. External commands reset caught traps to the
|
|
@@ -102,7 +111,10 @@ function buildPosixProviderShellCommand(command, shell, marker) {
|
|
|
102
111
|
"trap ':' INT",
|
|
103
112
|
`if ${command}; then __wand_cli_status=0; else __wand_cli_status=$?; fi`,
|
|
104
113
|
`printf '\\036WAND_CLI_EXIT:${marker.token}:%s\\037' "$__wand_cli_status"`,
|
|
105
|
-
|
|
114
|
+
buildTerminalRestoreCommand(),
|
|
115
|
+
// -i forces an interactive prompt even if tty detection is confused after a
|
|
116
|
+
// TUI teardown; -l matches Terminal.app / iTerm login-shell startup files.
|
|
117
|
+
`exec ${quotePosixShell(shell)} -il`,
|
|
106
118
|
].join("; ");
|
|
107
119
|
}
|
|
108
120
|
/**
|
|
@@ -114,7 +126,7 @@ export function buildPtyShellLaunchPlan(options) {
|
|
|
114
126
|
const platform = options.platform ?? os.platform();
|
|
115
127
|
if (options.bareShell) {
|
|
116
128
|
return {
|
|
117
|
-
shellArgs: platform === "win32" ? [] : ["-
|
|
129
|
+
shellArgs: platform === "win32" ? [] : ["-il"],
|
|
118
130
|
cliExitMarker: null,
|
|
119
131
|
};
|
|
120
132
|
}
|
|
@@ -130,7 +142,7 @@ export function buildPtyShellLaunchPlan(options) {
|
|
|
130
142
|
const marker = new PtyCliExitMarker(options.markerToken);
|
|
131
143
|
return {
|
|
132
144
|
// Interactive + login initialization matches a real terminal before the
|
|
133
|
-
// provider starts; `exec <shell> -
|
|
145
|
+
// provider starts; `exec <shell> -il` becomes the persistent prompt after it exits.
|
|
134
146
|
shellArgs: ["-lic", buildPosixProviderShellCommand(options.command, options.shell, marker)],
|
|
135
147
|
cliExitMarker: marker,
|
|
136
148
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Express, RequestHandler } from "express";
|
|
2
|
+
import type { GitHubApkDownload } from "./distribution-manager.js";
|
|
2
3
|
import type { ModelCatalogService } from "./models.js";
|
|
3
4
|
import type { PackageUpdateInfo, UpdateChannel } from "./npm-update-utils.js";
|
|
4
5
|
import { type ProviderCliUpdateStatus } from "./provider-cli-updater.js";
|
|
@@ -21,6 +22,8 @@ interface ResolvedUpdateAsset {
|
|
|
21
22
|
export interface PublicUpdateRoutesDependencies {
|
|
22
23
|
resolveLatestApk(channel: "stable" | "beta"): Promise<ResolvedUpdateAsset | null>;
|
|
23
24
|
resolveAndroidDownload(channel: "stable" | "beta"): Promise<DownloadAsset | null>;
|
|
25
|
+
/** GitHub 来源由服务端代拉,避免手机直连 CDN。测试 stub 可不实现。 */
|
|
26
|
+
resolveGitHubApkDownload?(): Promise<GitHubApkDownload | null>;
|
|
24
27
|
/** Beta 包下发时清理分发目录,只保留最新一个 APK(可选,测试 stub 可不实现)。 */
|
|
25
28
|
pruneAndroidApkDirectory?(keepFileName?: string): Promise<{
|
|
26
29
|
deleted: string[];
|
|
@@ -1,9 +1,52 @@
|
|
|
1
|
+
import { Readable } from "node:stream";
|
|
2
|
+
import { pipeline } from "node:stream/promises";
|
|
1
3
|
import { getErrorMessage } from "./error-utils.js";
|
|
2
4
|
import { asyncRoute } from "./express-async.js";
|
|
3
5
|
import { checkProviderCliUpdates, updateProviderClis, verifyProviderCliUpdateResults, } from "./provider-cli-updater.js";
|
|
4
6
|
import { streamFileWithRange } from "./server-file-routes.js";
|
|
5
7
|
import { compareApkInstallOrder, compareSemver } from "./version-utils.js";
|
|
6
8
|
import { canUseDetachedUpdateHelper, startDetachedUpdateHelper } from "./update-helper.js";
|
|
9
|
+
const GITHUB_PROXY_TIMEOUT_MS = 5 * 60 * 1000;
|
|
10
|
+
async function proxyGitHubApk(_req, res, asset, fetchImpl = fetch) {
|
|
11
|
+
let remote;
|
|
12
|
+
try {
|
|
13
|
+
remote = await fetchImpl(asset.remoteUrl, {
|
|
14
|
+
headers: {
|
|
15
|
+
"User-Agent": "wand-server",
|
|
16
|
+
Accept: "application/octet-stream",
|
|
17
|
+
},
|
|
18
|
+
redirect: "follow",
|
|
19
|
+
signal: AbortSignal.timeout(GITHUB_PROXY_TIMEOUT_MS),
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
res.status(502).json({ error: getErrorMessage(error, "从 GitHub 下载安装包失败。") });
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (!remote.ok || !remote.body) {
|
|
27
|
+
res.status(502).json({ error: `从 GitHub 下载安装包失败(HTTP ${remote.status})。` });
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const remoteLength = Number(remote.headers.get("content-length"));
|
|
31
|
+
const size = Number.isFinite(remoteLength) && remoteLength > 0 ? remoteLength : asset.size;
|
|
32
|
+
res.status(200);
|
|
33
|
+
res.setHeader("Content-Type", "application/vnd.android.package-archive");
|
|
34
|
+
res.setHeader("Content-Disposition", `attachment; filename="${encodeURIComponent(asset.fileName)}"`);
|
|
35
|
+
if (size > 0)
|
|
36
|
+
res.setHeader("Content-Length", String(size));
|
|
37
|
+
if (asset.sha256)
|
|
38
|
+
res.setHeader("X-APK-Sha256", asset.sha256);
|
|
39
|
+
try {
|
|
40
|
+
await pipeline(Readable.fromWeb(remote.body), res);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (!res.headersSent) {
|
|
44
|
+
res.status(502).json({ error: getErrorMessage(error, "从 GitHub 下载安装包失败。") });
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
res.destroy();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
7
50
|
export function registerPublicUpdateRoutes(app, deps) {
|
|
8
51
|
app.get("/api/android-apk-update", asyncRoute(async (req, res) => {
|
|
9
52
|
const currentVersion = typeof req.query.currentVersion === "string" ? req.query.currentVersion.trim() : "";
|
|
@@ -31,11 +74,20 @@ export function registerPublicUpdateRoutes(app, deps) {
|
|
|
31
74
|
source: latest.source,
|
|
32
75
|
channel,
|
|
33
76
|
releaseNotes: updateAvailable ? (latest.releaseNotes ?? null) : null,
|
|
34
|
-
//
|
|
77
|
+
// 本地文件哈希或 GitHub Release digest;客户端下载后校验。
|
|
35
78
|
sha256: updateAvailable ? (latest.sha256 ?? null) : null,
|
|
36
79
|
});
|
|
37
80
|
}));
|
|
38
81
|
app.get("/android/download", asyncRoute(async (req, res) => {
|
|
82
|
+
if (req.query.source === "github") {
|
|
83
|
+
const remote = deps.resolveGitHubApkDownload ? await deps.resolveGitHubApkDownload() : null;
|
|
84
|
+
if (!remote) {
|
|
85
|
+
res.status(404).json({ error: "当前没有可下载的 GitHub APK 文件。" });
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
await proxyGitHubApk(req, res, remote);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
39
91
|
const channel = req.query.channel === "beta" ? "beta" : "stable";
|
|
40
92
|
const asset = await deps.resolveAndroidDownload(channel);
|
|
41
93
|
if (!asset) {
|
|
@@ -285,6 +285,7 @@ export function registerWorkspaceRoutes(app, storage, sessions) {
|
|
|
285
285
|
provider: session.provider,
|
|
286
286
|
sessionKind: session.sessionKind,
|
|
287
287
|
runner: session.runner,
|
|
288
|
+
command: session.command,
|
|
288
289
|
title: resolveSessionDisplayTitle(session),
|
|
289
290
|
status: session.status,
|
|
290
291
|
cwd: session.cwd,
|
package/dist/session-topic.d.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { type QuickCommitAiOptions } from "./git-quick-commit.js";
|
|
2
2
|
import type { ConversationTurn } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* PTY 终端视图里,只有底部输入框整段提交(shortcutKey=enter_text)才总结标题。
|
|
5
|
+
* 逐键 pty_input / 方向键 / 单独回车不能触发,避免把按键当主题。
|
|
6
|
+
*/
|
|
7
|
+
export declare function shouldGenerateSessionTopicFromPtyInput(view?: "chat" | "terminal", shortcutKey?: string): boolean;
|
|
3
8
|
export interface SessionTopic {
|
|
4
9
|
title: string;
|
|
5
10
|
description: string;
|
package/dist/session-topic.js
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { callConfiguredAiText } from "./git-quick-commit.js";
|
|
2
2
|
const MAX_PROMPT_LENGTH = 12_000;
|
|
3
|
+
/**
|
|
4
|
+
* PTY 终端视图里,只有底部输入框整段提交(shortcutKey=enter_text)才总结标题。
|
|
5
|
+
* 逐键 pty_input / 方向键 / 单独回车不能触发,避免把按键当主题。
|
|
6
|
+
*/
|
|
7
|
+
export function shouldGenerateSessionTopicFromPtyInput(view, shortcutKey) {
|
|
8
|
+
return view !== "terminal" || shortcutKey === "enter_text";
|
|
9
|
+
}
|
|
3
10
|
function cleanTopicText(value, maxLength) {
|
|
4
11
|
if (typeof value !== "string")
|
|
5
12
|
return "";
|