dsh-her 0.2.0 → 0.3.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/README.md CHANGED
@@ -14,7 +14,7 @@
14
14
  | M3 外形包与姿势图 | ✅ mac 通过([记录](docs/M3记录.md));用合成测试包验证,"想看 JK" 全流程跑通;待真实素材校准 |
15
15
  | M4 内置记忆 | ✅ mac 通过([记录](docs/M4记录.md));换全新会话仍记得,删掉的事不再提 |
16
16
  | M5 向导与管理页 | ✅ mac 通过([记录](docs/M5记录.md));dsh 侧栏"她":7 步向导 + 管理页。身体的下载安装挪到 M6 |
17
- | M6 打磨与发布 | 进行中([记录](docs/M6记录.md)):mac 身体打包、一键下载安装和自动更新、Edge 声音不再依赖系统 Python;签名公证、Windows、省电待做 |
17
+ | M6 打磨与发布 | 进行中([记录](docs/M6记录.md)):mac 身体打包、一键下载安装和自动更新、Edge 声音不再依赖系统 Python;Windows 身体打包和安装已写好、等实测([Windows适配](docs/Windows适配.md));签名公证、省电待做 |
18
18
 
19
19
  ## 组成
20
20
 
@@ -6,6 +6,8 @@
6
6
  * { "format": "dsh-her-body@1", "version": "0.1.0",
7
7
  * "assets": { "darwin-arm64": { "file": "dsh-her-body-0.1.0-darwin-arm64.zip", "size": …, "sha256": "…" } } }
8
8
  *
9
+ * The app is `dsh-her.app` on macOS and a `dsh-her` folder with
10
+ * `dsh-her.exe` on Windows (body/scripts/package.mjs makes both).
9
11
  * `<home>/body/current.json` says which version is installed. A download
10
12
  * that does not match its SHA-256 is thrown away; the previous version stays
11
13
  * until the new one is in place, and one older version is kept to go back to.
@@ -17,9 +19,13 @@ import { join } from 'node:path';
17
19
  import { Readable } from 'node:stream';
18
20
  import { pipeline } from 'node:stream/promises';
19
21
  import { promisify } from 'node:util';
22
+ import { systemTar } from "../platform.js";
20
23
  const run = promisify(execFile);
21
24
  export const MANIFEST = 'body-latest.json';
22
- const APP = 'dsh-her.app';
25
+ /** The app inside a release zip (and in `<home>/body/<version>/`). */
26
+ export const appName = (platform = process.platform) => (platform === 'win32' ? 'dsh-her' : 'dsh-her.app');
27
+ /** Platforms with a body in the release. */
28
+ const PACKAGED = new Set(['darwin', 'win32']);
23
29
  /** This computer's asset key in the manifest. */
24
30
  export const platformKey = (platform = process.platform, arch = process.arch) => `${platform}-${arch}`;
25
31
  /** -1, 0, 1: "0.1.0-dev.0" < "0.1.0" < "0.1.1" < "0.2.0". */
@@ -72,6 +78,8 @@ export function installedPlan(home) {
72
78
  return undefined;
73
79
  if (process.platform === 'darwin')
74
80
  return { binary: join(installed.app, 'Contents', 'MacOS', 'Electron'), args: [], cwd: join(installed.app, '..') };
81
+ if (process.platform === 'win32')
82
+ return { binary: join(installed.app, 'dsh-her.exe'), args: [], cwd: installed.app };
75
83
  return undefined;
76
84
  }
77
85
  /** `base` + file: the release's download address ends with a slash, or the file is appended after one. */
@@ -87,8 +95,8 @@ export async function fetchManifest(base) {
87
95
  * the installed version and make it current. Progress in bytes.
88
96
  */
89
97
  export async function install(home, base, manifest, onProgress) {
90
- if (process.platform !== 'darwin')
91
- throw new Error('这个系统的身体程序还没有发布(现在只有 macOS 版)');
98
+ if (!PACKAGED.has(process.platform))
99
+ throw new Error('这个系统的身体程序还没有发布(现在有 macOS 和 Windows 版)');
92
100
  const asset = manifest.assets[platformKey()];
93
101
  if (asset === undefined)
94
102
  throw new Error(`发布里没有这台电脑(${platformKey()})的身体程序`);
@@ -116,22 +124,31 @@ export async function install(home, base, manifest, onProgress) {
116
124
  const unpacking = `${target}.unpacking`;
117
125
  rmSync(unpacking, { recursive: true, force: true });
118
126
  mkdirSync(unpacking, { recursive: true });
119
- // ditto keeps the app's symlinks, permissions and signature.
120
- await run('ditto', ['-x', '-k', zip, unpacking], { timeout: 300_000 });
121
- if (!existsSync(join(unpacking, APP)))
127
+ // macOS: ditto keeps the app's symlinks, permissions and signature. Windows: the system tar reads zip.
128
+ if (process.platform === 'darwin')
129
+ await run('ditto', ['-x', '-k', zip, unpacking], { timeout: 300_000 });
130
+ else
131
+ await run(systemTar(), ['-xf', zip, '-C', unpacking], { timeout: 300_000 });
132
+ if (!existsSync(join(unpacking, appName())))
122
133
  throw new Error('下载的包里没有身体程序');
123
134
  rmSync(target, { recursive: true, force: true });
124
135
  renameSync(unpacking, target);
125
136
  rmSync(zip, { force: true });
126
137
  const previous = installedBody(home);
127
- const installed = { version: manifest.version, app: join(target, APP), installedAt: new Date().toISOString() };
138
+ const installed = { version: manifest.version, app: join(target, appName()), installedAt: new Date().toISOString() };
128
139
  writeFileSync(join(root, 'current.json.tmp'), `${JSON.stringify(installed, null, 2)}\n`);
129
140
  renameSync(join(root, 'current.json.tmp'), join(root, 'current.json'));
130
141
  // Keep this version and the one before it; older ones go.
131
142
  for (const entry of readdirSync(root)) {
132
143
  const keep = entry === manifest.version || entry === previous?.version || entry === 'download' || entry.endsWith('.json');
133
- if (!keep)
144
+ if (keep)
145
+ continue;
146
+ try {
134
147
  rmSync(join(root, entry), { recursive: true, force: true });
148
+ }
149
+ catch {
150
+ // Windows keeps a running program's files locked: that old version goes next time.
151
+ }
135
152
  }
136
153
  return installed;
137
154
  }
@@ -42,7 +42,7 @@ export function launchBody(plan, env) {
42
42
  const childEnv = { ...process.env, DSH_HER_URL: env.url, DSH_HER_TOKEN: env.token, DSH_HER_HOME: env.home };
43
43
  delete childEnv.ELECTRON_RUN_AS_NODE;
44
44
  const args = env.pack === '' ? plan.args : [...plan.args, '--pack', env.pack];
45
- const child = spawn(plan.binary, args, { cwd: plan.cwd, env: childEnv, detached: true, stdio: 'ignore' });
45
+ const child = spawn(plan.binary, args, { cwd: plan.cwd, env: childEnv, detached: true, stdio: 'ignore', windowsHide: true });
46
46
  child.unref();
47
47
  return child.pid;
48
48
  }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Small differences between operating systems the plugin runs on.
3
+ */
4
+ import { join } from 'node:path';
5
+ /**
6
+ * The system's own bsdtar (macOS, Windows 10+). On Windows it is named by its
7
+ * full path: a `tar` earlier on PATH (Git for Windows ships GNU tar) reads
8
+ * `C:\…` as a remote host and fails.
9
+ */
10
+ export function systemTar(platform = process.platform) {
11
+ return platform === 'win32' ? join(process.env.SystemRoot ?? 'C:\\Windows', 'System32', 'tar.exe') : 'tar';
12
+ }
@@ -6,12 +6,16 @@
6
6
  * { "format": "dsh-her-body@1", "version": "0.1.0",
7
7
  * "assets": { "darwin-arm64": { "file": "dsh-her-body-0.1.0-darwin-arm64.zip", "size": …, "sha256": "…" } } }
8
8
  *
9
+ * The app is `dsh-her.app` on macOS and a `dsh-her` folder with
10
+ * `dsh-her.exe` on Windows (body/scripts/package.mjs makes both).
9
11
  * `<home>/body/current.json` says which version is installed. A download
10
12
  * that does not match its SHA-256 is thrown away; the previous version stays
11
13
  * until the new one is in place, and one older version is kept to go back to.
12
14
  */
13
15
  import type { LaunchPlan } from './body-launcher.ts';
14
16
  export declare const MANIFEST = "body-latest.json";
17
+ /** The app inside a release zip (and in `<home>/body/<version>/`). */
18
+ export declare const appName: (platform?: string) => "dsh-her" | "dsh-her.app";
15
19
  export interface BodyAsset {
16
20
  file: string;
17
21
  size: number;
@@ -24,7 +28,7 @@ export interface BodyManifest {
24
28
  }
25
29
  export interface Installed {
26
30
  version: string;
27
- /** The app (macOS: dsh-her.app) inside `<home>/body/<version>/`. */
31
+ /** The app (macOS: dsh-her.app; Windows: the dsh-her folder) inside `<home>/body/<version>/`. */
28
32
  app: string;
29
33
  installedAt: string;
30
34
  }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Small differences between operating systems the plugin runs on.
3
+ */
4
+ /**
5
+ * The system's own bsdtar (macOS, Windows 10+). On Windows it is named by its
6
+ * full path: a `tar` earlier on PATH (Git for Windows ships GNU tar) reads
7
+ * `C:\…` as a remote host and fails.
8
+ */
9
+ export declare function systemTar(platform?: string): string;
@@ -4,6 +4,7 @@
4
4
  * the cloud (cosyvoice.ts). Design: docs/语音方案.md.
5
5
  */
6
6
  import { execFile } from 'node:child_process';
7
+ import { systemTar } from "../platform.js";
7
8
  import { createWriteStream, existsSync, mkdirSync, renameSync, rmSync } from 'node:fs';
8
9
  import { join } from 'node:path';
9
10
  import { Readable } from 'node:stream';
@@ -59,7 +60,7 @@ export async function fetchModel(modelsDir, spec, onProgress) {
59
60
  mkdirSync(staging, { recursive: true });
60
61
  try {
61
62
  // bsdtar ships with macOS and Windows 10+, and reads bzip2 itself.
62
- await promisify(execFile)('tar', ['-xjf', partial, '-C', staging]);
63
+ await promisify(execFile)(systemTar(), ['-xjf', partial, '-C', staging]);
63
64
  renameSync(join(staging, spec.name), modelPath(modelsDir, spec));
64
65
  }
65
66
  finally {
package/lib/voice/uv.js CHANGED
@@ -10,6 +10,7 @@ import { createHash } from 'node:crypto';
10
10
  import { existsSync } from 'node:fs';
11
11
  import { mkdir, rm, writeFile } from 'node:fs/promises';
12
12
  import { join } from 'node:path';
13
+ import { systemTar } from "../platform.js";
13
14
  export function run(cmd, args, timeout, env) {
14
15
  return new Promise((resolve, reject) => {
15
16
  execFile(cmd, args, { timeout, maxBuffer: 8 << 20, ...(env === undefined ? {} : { env: { ...process.env, ...env } }) }, (error, stdout, stderr) => {
@@ -51,7 +52,7 @@ export async function installUv(uvHome) {
51
52
  const archive = join(dir, asset.file);
52
53
  await writeFile(archive, bytes);
53
54
  // tar reads both the .tar.gz and (Windows 10+) the .zip.
54
- await run('tar', ['-xf', archive, '-C', dir], 120_000);
55
+ await run(systemTar(), ['-xf', archive, '-C', dir], 120_000);
55
56
  await rm(archive, { force: true });
56
57
  const uv = [join(dir, 'uv'), join(dir, 'uv.exe'), join(dir, asset.file.replace(/\.(tar\.gz|zip)$/, ''), 'uv')].find(p => existsSync(p));
57
58
  if (uv === undefined)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-her",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "A desktop companion for DeepSeek Harness (dsh): she lives on your real desktop (on top of your windows or inside the wallpaper), talks with you by voice and remembers you. · DSH 桌面女友:住在你真正的桌面上,能语音聊天、记得你。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",