@ninkch/dsh-infinite-canvas 0.17.2 → 0.17.3

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/client.js CHANGED
@@ -333,6 +333,11 @@ window.__ModuleLoader__.load({
333
333
  'updateAvailable': '新版本可用',
334
334
  'updateNow': '一键更新',
335
335
  'updateUpToDate': '已是最新版本',
336
+ 'updateInstalling': '正在安装更新…',
337
+ 'updateInstalled': '已安装',
338
+ 'updateInstalledHint': '重启 Harness 后生效(网页部分刷新页面即可)',
339
+ 'updateFailGuide': '自动安装未成功,请在 DSH 插件管理 中升级',
340
+ 'updatePolicySkipped': '(已按你的点击跳过 24 小时新版本冷却期)',
336
341
  'updateCurrent': '当前',
337
342
  'updateGo': '去升级',
338
343
  'npmUpgradeHint': '请在 DSH 插件管理 中升级到最新版(本插件绝不自动安装或重启);安装完成后重启 Harness 生效',
@@ -590,6 +595,11 @@ window.__ModuleLoader__.load({
590
595
  'updateAvailable': 'New version available',
591
596
  'updateNow': 'Update now',
592
597
  'updateUpToDate': 'Up to date',
598
+ 'updateInstalling': 'Installing update…',
599
+ 'updateInstalled': 'Installed',
600
+ 'updateInstalledHint': 'restart Harness to take effect (refresh the page for the web part)',
601
+ 'updateFailGuide': 'Auto-install failed — please upgrade via the DSH Plugin Manager',
602
+ 'updatePolicySkipped': '(skipped the 24-hour release cool-down at your click)',
593
603
  'updateCurrent': 'current',
594
604
  'updateGo': 'Upgrade',
595
605
  'npmUpgradeHint': 'Please upgrade via the DSH Plugin Manager (this plugin never auto-installs or restarts); restart Harness after installing',
@@ -1834,12 +1844,16 @@ window.__ModuleLoader__.load({
1834
1844
 
1835
1845
  // ============================================================
1836
1846
  // 自动更新:当前副本版本 vs npm registry latest(与安装方式无关)
1837
- // 有新版本 → 横幅提示;安装一律引导到 DSH 插件管理,绝不自动安装/重启
1847
+ // 有新版本 → 横幅 + "去升级";点击后由 Host 在所属 profile 内
1848
+ // pnpm add 精确版本(绝不自动重启/刷新);旧 Host / 开发态 / 失败 → 回退为纯引导
1838
1849
  // ============================================================
1839
1850
  async function checkUpdate(silent) {
1840
1851
  update((s) => { s.updateInfo = null; s.updateChecking = true; s.updateMsg = null; });
1841
1852
  try {
1842
1853
  const info = await hostRpc('updateCheck');
1854
+ // 兼容旧 Host(SHA 比较语义会给出 upToDate=false 但 behind=0):
1855
+ // 只要有 behind 数值,就统一按"落后 N 版"判定最新与否
1856
+ if (info && typeof info.behind === 'number') info.upToDate = info.behind === 0;
1843
1857
  update((s) => { s.updateInfo = info; s.updateChecking = false; });
1844
1858
  return info;
1845
1859
  } catch (e) {
@@ -1848,10 +1862,23 @@ window.__ModuleLoader__.load({
1848
1862
  return null;
1849
1863
  }
1850
1864
  }
1851
- function applyUpdate() {
1852
- // 仅引导:插件不修改用户 profile、不执行任何安装动作、不刷新不重启
1853
- const target = S.updateInfo && S.updateInfo.remoteVersion ? S.updateInfo.remoteVersion : '';
1854
- update((s) => { s.updateBusy = false; s.updateMsg = T('npmUpgradeHint') + (target ? ' → v' + target : ''); });
1865
+ async function applyUpdate() {
1866
+ update((s) => { s.updateBusy = true; s.updateMsg = T('updateInstalling'); });
1867
+ try {
1868
+ const res = await hostRpc('updateInstall');
1869
+ if (res && res.ok && res.changed) {
1870
+ const relaxed = res.policyRelaxed ? ' ' + T('updatePolicySkipped') : '';
1871
+ update((s) => { s.updateBusy = false; s.updateMsg = T('updateInstalled') + ' v' + res.version + ' — ' + T('updateInstalledHint') + relaxed; });
1872
+ } else if (res && res.ok) {
1873
+ update((s) => { s.updateBusy = false; s.updateMsg = T('updateUpToDate') + (res.version ? ' v' + res.version : ''); });
1874
+ } else {
1875
+ const reason = res && res.output ? '(' + String(res.output).slice(-120) + ')' : '';
1876
+ update((s) => { s.updateBusy = false; s.updateMsg = T('updateFailGuide') + (res && res.version ? ' → v' + res.version : '') + reason; });
1877
+ }
1878
+ } catch (e) {
1879
+ // 旧 Host 没有 updateInstall RPC → 回退为纯引导(方案 A 兜底)
1880
+ update((s) => { s.updateBusy = false; s.updateMsg = T('npmUpgradeHint') + ((S.updateInfo && S.updateInfo.remoteVersion) ? ' → v' + S.updateInfo.remoteVersion : ''); });
1881
+ }
1855
1882
  }
1856
1883
 
1857
1884
  // ============================================================
@@ -4194,7 +4221,7 @@ window.__ModuleLoader__.load({
4194
4221
  h('button', {
4195
4222
  type: 'button', className: cls('dshic-btn'), style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)', gap: 4 },
4196
4223
  title: T('updateBtn') + ((S.updateInfo && S.updateInfo.runningVersion) ? '(当前 v' + S.updateInfo.runningVersion + ')' : ''), disabled: S.updateChecking,
4197
- onClick: () => { checkUpdate(false).then((info) => { if (info && info.upToDate) toast(T('updateUpToDate')); }); },
4224
+ onClick: () => { checkUpdate(false).then((info) => { if (info && info.upToDate) toast(T('updateUpToDate') + (info.runningVersion ? ' v' + info.runningVersion : '')); }); },
4198
4225
  }, T('updateBtn'), S.updateChecking ? h('span', { className: 'dshic-spin' }) : ((S.updateInfo && !S.updateInfo.upToDate) ? ' ⬆' : '')),
4199
4226
  h('div', { className: 'dshic-toolbar-sep' }),
4200
4227
  h(Btn, {
package/docs/release.md CHANGED
@@ -107,10 +107,11 @@ GET /infinite-canvas/api/rpc ping # 安装重启后 version 应为
107
107
  - **当前版本** = 当前加载副本的 `package.json`(Host 启动时读取)
108
108
  - **最新版本** = npm registry `…/@ninkch%2Fdsh-infinite-canvas/latest`(即 `latest` dist-tag,beta 不会出现)
109
109
  - 本地版本 ≥ 远端 → 视为最新;仅真正落后 → 画布顶部横幅"新版本可用 vX(当前 vY)"
110
- - 横幅"去升级"按钮 → **仅提示**:"请在 DSH 插件管理 中升级…重启 Harness 生效"
111
- - **绝不自动安装、绝不自动刷新、绝不自动重启**;网络失败静默(手动点击才提示错误)
110
+ - 横幅"去升级"按钮 → **真实安装**:Host 在所属 profile 内执行 `pnpm add <pkg>@<精确版本> --save-exact` 并同步 bundles 列表;完成后仅提示"已安装 vX — 重启 Harness 后生效"
111
+ - **降级为纯引导**的情形:开发态(link/.git)、找不到 pnpm、安装失败、旧 Host 无 `updateInstall` RPC
112
+ - **绝不自动刷新、绝不自动重启**;网络失败静默(手动点击才提示错误)
112
113
 
113
- 实现位置:`index.js` `updateCheck()`(RPC `updateCheck`);文案 key:`updateGo` / `npmUpgradeHint` / `updateCurrent`。
114
+ 实现位置:`index.js` `updateCheck()` / `updateInstall()`(RPC 同名);文案 key:`updateGo` / `updateInstalling` / `updateInstalled` / `updateInstalledHint` / `updateFailGuide` / `updateCurrent`。
114
115
 
115
116
  ## 7. 发布日检查清单
116
117
 
package/index.js CHANGED
@@ -6,7 +6,10 @@
6
6
  * through the Harness `llm` Service — the same providers/models the user has
7
7
  * configured for the Harness itself, no extra API key handling here.
8
8
  */
9
- import { readFileSync } from 'node:fs';
9
+ import { spawn } from 'node:child_process';
10
+ import fs from 'node:fs/promises';
11
+ import { readFileSync, existsSync, readdirSync } from 'node:fs';
12
+ import os from 'node:os';
10
13
  import path from 'node:path';
11
14
  import { fileURLToPath } from 'node:url';
12
15
 
@@ -22,6 +25,86 @@ try {
22
25
  // 更新检查的唯一信息源 = npm registry latest(与安装方式无关:
23
26
  // npm 在线安装 / 本地 link 开发态 / 其它方式,一律走同一条逻辑)。
24
27
  const NPM_LATEST_URL = 'https://registry.npmjs.org/' + encodeURIComponent('@ninkch/dsh-infinite-canvas') + '/latest';
28
+ const PKG_NAME = '@ninkch/dsh-infinite-canvas';
29
+
30
+ // ---------- 安装形态探测(用于"点击即更新") ----------
31
+ // 受管安装 = 本副本位于某个 DSH profile 的 node_modules 下(向上找带 dsh.profile.bundles 的 package.json)。
32
+ // 开发态(目录含 .git)不受管 → 点击更新时降级为纯引导。
33
+ const PROFILE_DIR = (() => {
34
+ if (existsSync(path.join(HERE, '.git'))) return null;
35
+ let dir = HERE;
36
+ for (let i = 0; i < 6; i++) {
37
+ dir = path.dirname(dir);
38
+ try {
39
+ const m = JSON.parse(readFileSync(path.join(dir, 'package.json'), 'utf8'));
40
+ if (m && m.dsh && m.dsh.profile && Array.isArray(m.dsh.profile.bundles)) return dir;
41
+ } catch { /* 继续向上 */ }
42
+ }
43
+ return null;
44
+ })();
45
+
46
+ /** 定位 pnpm:Desktop 安装布局 → 用户运行时目录 → PATH。返回 { execPath, args 前缀 } 或 null。 */
47
+ function findPnpm() {
48
+ const candidates = [];
49
+ try {
50
+ candidates.push(path.join(path.dirname(process.execPath), 'resources', 'runtime', 'pnpm', 'bin', 'pnpm.mjs'));
51
+ } catch { /* execPath 不可用 */ }
52
+ try {
53
+ const runtimes = path.join(os.homedir(), '.dsh', 'dsh-runtimes');
54
+ for (const entry of readdirSync(runtimes)) {
55
+ candidates.push(path.join(runtimes, entry, 'dependencies', 'pnpm', 'bin', 'pnpm.mjs'));
56
+ }
57
+ } catch { /* 无运行时目录 */ }
58
+ for (const c of candidates) {
59
+ try { if (existsSync(c)) return { kind: 'script', node: process.execPath, script: c }; } catch { /* 跳过 */ }
60
+ }
61
+ try { if (existsSync('C:\\Program Files\\nodejs\\pnpm.cmd')) return { kind: 'path', command: 'pnpm' }; } catch { /* 跳过 */ }
62
+ return { kind: 'path', command: 'pnpm' }; // 交给 spawn 兜底(PATH 上有就用)
63
+ }
64
+
65
+ /** 在 profile 目录执行 pnpm add <spec> --save-exact。opts.relaxReleaseAge → 跳过 24h 新版本冷却期。 */
66
+ function runPnpmAdd(pnpm, spec, opts = {}) {
67
+ return new Promise((resolve) => {
68
+ const started = Date.now();
69
+ const stdout = [];
70
+ const stderr = [];
71
+ let child;
72
+ const relax = opts.relaxReleaseAge ? ['--config.minimum-release-age=0'] : [];
73
+ const args = pnpm.kind === 'script'
74
+ ? ['--expose-internals', pnpm.script, 'add', spec, '--save-exact', ...relax]
75
+ : ['add', spec, '--save-exact', ...relax];
76
+ try {
77
+ child = pnpm.kind === 'script'
78
+ ? spawn(pnpm.node, args, { cwd: PROFILE_DIR, windowsHide: true, env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' } })
79
+ : spawn(pnpm.command, args, { cwd: PROFILE_DIR, windowsHide: true, shell: process.platform === 'win32' });
80
+ } catch (e) {
81
+ resolve({ ok: false, stdout: '', stderr: String(e?.message ?? e) });
82
+ return;
83
+ }
84
+ const timer = setTimeout(() => { try { child.kill(); } catch { /* 忽略 */ } }, 240000);
85
+ child.stdout?.setEncoding('utf8');
86
+ child.stderr?.setEncoding('utf8');
87
+ child.stdout?.on('data', (d) => stdout.push(String(d)));
88
+ child.stderr?.on('data', (d) => stderr.push(String(d)));
89
+ child.on('error', (e) => { clearTimeout(timer); resolve({ ok: false, stdout: stdout.join(''), stderr: String(e?.message ?? e) }); });
90
+ child.on('close', (code) => {
91
+ clearTimeout(timer);
92
+ resolve({ ok: code === 0, code, stdout: stdout.join(''), stderr: stderr.join(''), ms: Date.now() - started });
93
+ });
94
+ });
95
+ }
96
+
97
+ /** pnpm add 成功后同步 bundles 列表(等价 DSH reconcile 的 add 分支:新依赖必须成为 bundle 层)。 */
98
+ async function ensureBundleEntry(profileDir, pkg) {
99
+ const manifestPath = path.join(profileDir, 'package.json');
100
+ const m = JSON.parse(await fs.readFile(manifestPath, 'utf8'));
101
+ const bundles = (m.dsh && m.dsh.profile && m.dsh.profile.bundles) || [];
102
+ if (bundles.includes(pkg)) return false;
103
+ bundles.push(pkg);
104
+ m.dsh = { ...(m.dsh || {}), profile: { ...(m.dsh.profile || {}), bundles } };
105
+ await fs.writeFile(manifestPath, JSON.stringify(m, null, 2) + '\n', 'utf8');
106
+ return true;
107
+ }
25
108
 
26
109
  /** 读取 npm registry 上 latest dist-tag 的版本号。 */
27
110
  async function npmLatestVersion() {
@@ -62,7 +145,6 @@ function readLocalVersion() {
62
145
  /**
63
146
  * 更新检查(与安装方式无关):当前副本版本 vs npm registry latest。
64
147
  * 本地版本 ≥ 远端 → 最新;仅真正落后才提示。
65
- * 插件绝不自动安装、绝不自动重启/刷新 —— 安装一律引导到 DSH 插件管理。
66
148
  */
67
149
  async function updateCheck() {
68
150
  const runningVersion = readLocalVersion();
@@ -71,10 +153,49 @@ async function updateCheck() {
71
153
  return { source: 'npm', runningVersion, remoteVersion, behind, upToDate: behind === 0 };
72
154
  }
73
155
 
156
+ /**
157
+ * 点击即更新(仅在用户显式点击时由客户端调用,无 RPC 参数):
158
+ * 安装 registry latest 的精确版本到所属 profile,绝不自动重启/刷新。
159
+ * - 已是最新 → { ok:true, changed:false }
160
+ * - 受管安装 + pnpm 可用 → pnpm add <pkg>@<exact> --save-exact + 同步 bundles 列表
161
+ * - 开发态 / pnpm 不可用 / 安装失败 → { ok:false, guide:true }(客户端回退为纯引导文案)
162
+ */
163
+ async function updateInstall() {
164
+ const runningVersion = readLocalVersion();
165
+ const latest = await npmLatestVersion();
166
+ if (cmpVer(runningVersion, latest) >= 0) {
167
+ return { ok: true, changed: false, version: runningVersion };
168
+ }
169
+ if (!PROFILE_DIR) {
170
+ return { ok: false, guide: true, version: latest, output: '开发态安装,请自行更新工作区' };
171
+ }
172
+ const pnpm = findPnpm();
173
+ const spec = PKG_NAME + '@' + latest;
174
+ let res = await runPnpmAdd(pnpm, spec);
175
+ let policyRelaxed = false;
176
+ if (!res.ok && /MINIMUM_RELEASE_AGE_VIOLATION/.test((res.stdout || '') + (res.stderr || ''))) {
177
+ // pnpm 11 供应链保护对 24h 内新发布间歇性拦截;用户点击已是明确同意 → 放宽重试一次
178
+ policyRelaxed = true;
179
+ res = await runPnpmAdd(pnpm, spec, { relaxReleaseAge: true });
180
+ }
181
+ const output = ((res.stdout || '') + '\n' + (res.stderr || '')).trim().slice(-1500);
182
+ if (!res.ok) {
183
+ return { ok: false, guide: true, version: latest, output };
184
+ }
185
+ try {
186
+ await ensureBundleEntry(PROFILE_DIR, PKG_NAME);
187
+ } catch { /* bundles 同步失败不阻塞安装结果 */ }
188
+ let installed = latest;
189
+ try {
190
+ installed = JSON.parse(readFileSync(path.join(PROFILE_DIR, 'node_modules', ...PKG_NAME.split('/'), 'package.json'), 'utf8')).version || latest;
191
+ } catch { /* 保留 latest */ }
192
+ return { ok: true, changed: true, version: installed, output, policyRelaxed };
193
+ }
194
+
74
195
  export const inject = ['webServer'];
75
196
 
76
197
  // 仅供诊断 / 自动化测试(不属于 RPC 面,运行时行为不受影响)
77
- export const _internals = { updateCheck, npmLatestVersion, cmpVer, getVersion: () => VERSION };
198
+ export const _internals = { updateCheck, updateInstall, npmLatestVersion, cmpVer, findPnpm, getProfileDir: () => PROFILE_DIR, getVersion: () => VERSION };
78
199
 
79
200
  function resolveModel(providers, want) {
80
201
  const byProvider = want && want.provider
@@ -156,6 +277,7 @@ export function apply(ctx) {
156
277
  async llmOptions() { return listLlmOptions(); },
157
278
  async llmComplete(args) { return complete(args); },
158
279
  async updateCheck() { return updateCheck(); },
280
+ async updateInstall() { return updateInstall(); },
159
281
  async listSkills() {
160
282
  const skills = ctx.get('skills');
161
283
  if (!skills) return { skills: [], error: 'skills 服务不可用' };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ninkch/dsh-infinite-canvas",
3
- "version": "0.17.2",
3
+ "version": "0.17.3",
4
4
  "description": "Node-based short-drama studio for DeepSeek Harness: script → storyboard → assets → shots → voiced, subtitled final cut on an infinite canvas.",
5
5
  "keywords": [
6
6
  "dsh",