@ninkch/dsh-artcraft 0.1.1 → 0.2.1
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 +5 -0
- package/README.zh-CN.md +4 -0
- package/client.js +189 -0
- package/index.js +155 -3
- package/package.json +17 -3
package/README.md
CHANGED
|
@@ -280,6 +280,11 @@ The three places that must agree on the package name are the profile `package.js
|
|
|
280
280
|
GitHub: this repository is self-contained; `README.md` is English-first with a
|
|
281
281
|
[简体中文](README.zh-CN.md) companion.
|
|
282
282
|
|
|
283
|
+
**Releases run through npm Trusted Publishing** (GitHub Actions OIDC — no tokens, no OTP):
|
|
284
|
+
`.github/workflows/publish.yml` fires on `v*` tags; the trusted publisher on the npm package
|
|
285
|
+
points at `NINKCH/dsh-artcraft` / `publish.yml`. To cut a release: bump `version`, commit,
|
|
286
|
+
`git tag v0.x.y && git push origin v0.x.y`.
|
|
287
|
+
|
|
283
288
|
## License
|
|
284
289
|
|
|
285
290
|
**This plugin (`@ninkch/dsh-artcraft`): Apache-2.0** — see [LICENSE](LICENSE). It ships none of the
|
package/README.zh-CN.md
CHANGED
|
@@ -263,6 +263,10 @@ HTTP 面无鉴权,挂在 Harness Web 服务之下(仅回环监听)——
|
|
|
263
263
|
GitHub 上传本仓库即可:`README.md` 英文优先,附
|
|
264
264
|
[简体中文](README.zh-CN.md) 对照。
|
|
265
265
|
|
|
266
|
+
**发版走 npm Trusted Publishing**(GitHub Actions OIDC —— 无令牌、无 OTP):
|
|
267
|
+
`.github/workflows/publish.yml` 在 `v*` 标签推送时触发;npm 包侧的可信发布指向
|
|
268
|
+
`NINKCH/dsh-artcraft` / `publish.yml`。发版流程:改 `version` → 提交 → `git tag v0.x.y && git push origin v0.x.y`。
|
|
269
|
+
|
|
266
270
|
## 许可
|
|
267
271
|
|
|
268
272
|
**本插件(`@ninkch/dsh-artcraft`):Apache-2.0**(见 [LICENSE](LICENSE))。不包含 ArtCraft 应用的任何代码 —— 只调用其公开自动化面
|
package/client.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
// @ninkch/dsh-artcraft — client half.
|
|
2
|
+
// 不占侧栏/主面板:状态与「检查更新」挂在 插件页 → 本插件详情卡 上
|
|
3
|
+
// (plugins.detail.badge / plugins.detail.section)。
|
|
4
|
+
window.__ModuleLoader__.load({
|
|
5
|
+
id: '@ninkch/dsh-artcraft',
|
|
6
|
+
factory(require) {
|
|
7
|
+
const React = require('react');
|
|
8
|
+
const h = React.createElement;
|
|
9
|
+
const { useState, useEffect, useCallback, useRef } = React;
|
|
10
|
+
|
|
11
|
+
const NS = 'artcraft';
|
|
12
|
+
const API = '/artcraft/api';
|
|
13
|
+
|
|
14
|
+
const CSS = `
|
|
15
|
+
.dshac-card { border: 1px solid var(--dsw-border-primary, rgba(128,128,128,.35)); border-radius: 10px; padding: 12px; font-size: 13px; line-height: 1.5; }
|
|
16
|
+
.dshac-title { font-weight: 600; margin-bottom: 8px; display: flex; justify-content: space-between; align-items: center; gap: 8px; }
|
|
17
|
+
.dshac-row { display: flex; align-items: center; gap: 8px; margin: 4px 0; flex-wrap: wrap; }
|
|
18
|
+
.dshac-dot { width: 9px; height: 9px; border-radius: 50%; flex: none; display: inline-block; }
|
|
19
|
+
.dshac-dot.ok { background: #34c759; } .dshac-dot.bad { background: #ff5f57; } .dshac-dot.unknown { background: #8e8e93; }
|
|
20
|
+
.dshac-mono { font-family: ui-monospace, Consolas, monospace; font-size: 12px; word-break: break-all; }
|
|
21
|
+
.dshac-muted { opacity: .65; font-size: 12px; }
|
|
22
|
+
.dshac-btn { border: 1px solid var(--dsw-border-primary, rgba(128,128,128,.4)); background: transparent; color: inherit;
|
|
23
|
+
border-radius: 7px; padding: 3px 10px; font-size: 12px; cursor: pointer; }
|
|
24
|
+
.dshac-btn:hover:not(:disabled) { background: rgba(128,128,128,.15); }
|
|
25
|
+
.dshac-btn:disabled { opacity: .5; cursor: default; }
|
|
26
|
+
.dshac-btn.primary { background: rgba(80,120,255,.25); }
|
|
27
|
+
.dshac-out { white-space: pre-wrap; font-family: ui-monospace, Consolas, monospace; font-size: 11px;
|
|
28
|
+
background: rgba(128,128,128,.12); border-radius: 7px; padding: 8px; margin-top: 8px; max-height: 160px; overflow: auto; }
|
|
29
|
+
.dshac-note { margin-top: 8px; color: var(--dsw-alias-state-warning-primary, #d69e2e); font-size: 12px; }
|
|
30
|
+
.dshac-ok { color: var(--dsw-alias-state-success-primary, #34c759); }
|
|
31
|
+
.dshac-chip { display: inline-flex; align-items: center; border: 1px solid rgba(80,120,255,.5); color: inherit;
|
|
32
|
+
background: rgba(80,120,255,.18); border-radius: 999px; padding: 1px 9px; font-size: 11px; }
|
|
33
|
+
`;
|
|
34
|
+
|
|
35
|
+
async function api(path, opts) {
|
|
36
|
+
const ctrl = typeof AbortController !== 'undefined' ? new AbortController() : null;
|
|
37
|
+
const timer = ctrl ? setTimeout(() => ctrl.abort(), 30000) : null;
|
|
38
|
+
try {
|
|
39
|
+
const res = await fetch(API + path, { ...(opts || {}), signal: ctrl ? ctrl.signal : undefined });
|
|
40
|
+
const j = await res.json().catch(() => ({}));
|
|
41
|
+
if (!res.ok) throw new Error(j.error || ('HTTP ' + res.status));
|
|
42
|
+
return j;
|
|
43
|
+
} finally { if (timer) clearTimeout(timer); }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** 详情页 subject 兼容提取:判断当前详情页是否本插件 */
|
|
47
|
+
function isOurPage(props) {
|
|
48
|
+
const sub = props && props.subject ? props.subject : props;
|
|
49
|
+
if (!sub) return false;
|
|
50
|
+
const name = sub.packageName ?? sub.name ?? sub.moduleName ?? sub.id ?? (typeof sub === 'string' ? sub : '');
|
|
51
|
+
return typeof name === 'string' && name.includes('dsh-artcraft');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function Dot({ ok }) {
|
|
55
|
+
return h('span', { className: 'dshac-dot ' + (ok === true ? 'ok' : ok === false ? 'bad' : 'unknown') });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** 详情页标题旁的徽标:有新版本时出现 */
|
|
59
|
+
function UpdateBadge(props) {
|
|
60
|
+
const [latest, setLatest] = useState(null);
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (!isOurPage(props)) return;
|
|
63
|
+
let ok = true;
|
|
64
|
+
api('/update/check').then((u) => { if (ok && u?.updateAvailable) setLatest(u.latest); }).catch(() => {});
|
|
65
|
+
return () => { ok = false; };
|
|
66
|
+
}, []);
|
|
67
|
+
if (!isOurPage(props) || !latest) return null;
|
|
68
|
+
return h('span', { className: 'dshac-chip', title: 'npm 上有新版本,到本页一键更新' }, '有新版本 v' + latest);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** 详情页内容区:工具链状态 + 更新 */
|
|
72
|
+
function StatusSection(props) {
|
|
73
|
+
const [status, setStatus] = useState(null);
|
|
74
|
+
const [statusErr, setStatusErr] = useState(null);
|
|
75
|
+
const [upd, setUpd] = useState(null);
|
|
76
|
+
const [busy, setBusy] = useState('');
|
|
77
|
+
const [out, setOut] = useState('');
|
|
78
|
+
const [needRestart, setNeedRestart] = useState(false);
|
|
79
|
+
const ours = isOurPage(props);
|
|
80
|
+
const alive = useRef(true);
|
|
81
|
+
useEffect(() => () => { alive.current = false; }, []);
|
|
82
|
+
|
|
83
|
+
const loadStatus = useCallback(async () => {
|
|
84
|
+
try {
|
|
85
|
+
const s = await api('/status');
|
|
86
|
+
if (!alive.current) return;
|
|
87
|
+
setStatus(s); setStatusErr(null);
|
|
88
|
+
} catch (e) { if (alive.current) setStatusErr(String(e.message || e)); }
|
|
89
|
+
}, []);
|
|
90
|
+
const checkUpdate = useCallback(async () => {
|
|
91
|
+
setBusy('check'); setOut('');
|
|
92
|
+
try {
|
|
93
|
+
const u = await api('/update/check');
|
|
94
|
+
if (!alive.current) return;
|
|
95
|
+
setUpd(u);
|
|
96
|
+
setOut(u.updateAvailable
|
|
97
|
+
? `有新版本:${u.latest}(当前 ${u.current}),安装方式:${u.mode === 'git' ? 'git 仓库' : 'npm 安装'}`
|
|
98
|
+
: `已是最新${u.latest ? '(npm latest ' + u.latest + ')' : ''}${u.note ? ' · ' + u.note : ''}`);
|
|
99
|
+
} catch (e) { if (alive.current) setOut('检查失败:' + String(e.message || e)); }
|
|
100
|
+
finally { if (alive.current) setBusy(''); }
|
|
101
|
+
}, []);
|
|
102
|
+
const applyUpdate = useCallback(async () => {
|
|
103
|
+
setBusy('apply'); setOut('');
|
|
104
|
+
try {
|
|
105
|
+
const r = await api('/update/apply', { method: 'POST', headers: { 'content-type': 'application/json' }, body: '{}' });
|
|
106
|
+
if (!alive.current) return;
|
|
107
|
+
setOut((r.output || String(r.message || '')) + (r.needRestart ? '\n\n✅ 更新完成 —— 请重启 DeepSeek Harness 后生效。' : ''));
|
|
108
|
+
if (r.needRestart) setNeedRestart(true);
|
|
109
|
+
} catch (e) { if (alive.current) setOut('更新失败:' + String(e.message || e)); }
|
|
110
|
+
finally { if (alive.current) setBusy(''); }
|
|
111
|
+
}, []);
|
|
112
|
+
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
if (!ours) return;
|
|
115
|
+
loadStatus(); checkUpdate();
|
|
116
|
+
}, [ours, loadStatus, checkUpdate]);
|
|
117
|
+
|
|
118
|
+
if (!ours) return null;
|
|
119
|
+
|
|
120
|
+
const bins = status?.bins ?? {};
|
|
121
|
+
const binRow = (key, label) => {
|
|
122
|
+
const b = bins[key];
|
|
123
|
+
return h('div', { className: 'dshac-row' },
|
|
124
|
+
h(Dot, { ok: b ? b.found : null }),
|
|
125
|
+
h('span', null, label),
|
|
126
|
+
b?.found ? null : h('span', { className: 'dshac-muted' }, b?.note || '未配置'),
|
|
127
|
+
);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
return h('div', { className: 'dshac-card' },
|
|
131
|
+
h('div', { className: 'dshac-title' }, 'ArtCraft 工具链与更新',
|
|
132
|
+
h('button', { className: 'dshac-btn', onClick: loadStatus, disabled: busy !== '' }, '刷新')),
|
|
133
|
+
statusErr
|
|
134
|
+
? h('div', { className: 'dshac-muted' }, '未连接:' + statusErr + '(插件可能未注册或 Harness 需要重启)')
|
|
135
|
+
: h('div', null,
|
|
136
|
+
binRow('photocraft', 'PhotoCraft (CLI)'),
|
|
137
|
+
binRow('filmcraft', 'FilmCraft (CLI)'),
|
|
138
|
+
binRow('effectcraft', 'EffectCraft (CLI)'),
|
|
139
|
+
status?.dataRoot ? h('div', { className: 'dshac-row dshac-muted' },
|
|
140
|
+
'数据目录:', h('span', { className: 'dshac-mono' }, status.dataRoot.path)) : null),
|
|
141
|
+
h('div', { className: 'dshac-row', style: { marginTop: '10px' } },
|
|
142
|
+
h('span', { className: 'dshac-muted' },
|
|
143
|
+
'当前 v' + (status?.version ?? upd?.current ?? '?') +
|
|
144
|
+
(upd ? ' · npm latest v' + (upd.latest ?? '—') : '') +
|
|
145
|
+
(upd?.mode ? '(' + (upd.mode === 'git' ? 'git 仓库' + (upd.gitClean === false ? ' · 有本地改动' : '') : 'npm 安装') + ')' : '')),
|
|
146
|
+
h('button', { className: 'dshac-btn primary', onClick: checkUpdate, disabled: busy !== '' },
|
|
147
|
+
busy === 'check' ? '检查中…' : '检查更新'),
|
|
148
|
+
h('button', {
|
|
149
|
+
className: 'dshac-btn',
|
|
150
|
+
onClick: applyUpdate,
|
|
151
|
+
disabled: busy !== '' || !upd?.updateAvailable,
|
|
152
|
+
title: upd?.updateAvailable ? '' : '有新版本时才可应用',
|
|
153
|
+
}, busy === 'apply' ? '更新中…' : '一键更新')),
|
|
154
|
+
needRestart ? h('div', { className: 'dshac-note' }, '更新已写入 —— 重启 DeepSeek Harness 后生效。') : null,
|
|
155
|
+
out ? h('div', { className: 'dshac-out' }, out) : null,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
inject: ['slots', 'locale'],
|
|
161
|
+
apply(ctx) {
|
|
162
|
+
ctx.effect(() => ctx.locale.register(NS, { zh: { artcraft: 'ArtCraft' }, en: { artcraft: 'ArtCraft' } }), 'artcraft: dictionaries');
|
|
163
|
+
ctx.effect(() => {
|
|
164
|
+
const el = document.createElement('style');
|
|
165
|
+
el.id = 'dshac-styles';
|
|
166
|
+
el.textContent = CSS;
|
|
167
|
+
document.head.appendChild(el);
|
|
168
|
+
return () => el.remove();
|
|
169
|
+
}, 'artcraft: styles');
|
|
170
|
+
// 插件页 → 本插件详情卡:标题旁徽标(有新版本时)
|
|
171
|
+
ctx.slots.inject('plugins.detail.badge', () => ctx.slots.register({
|
|
172
|
+
name: 'plugins.detail.badge',
|
|
173
|
+
id: 'artcraft-update-badge',
|
|
174
|
+
order: 50,
|
|
175
|
+
label: () => 'ArtCraft 更新',
|
|
176
|
+
locale: NS,
|
|
177
|
+
}, UpdateBadge));
|
|
178
|
+
// 插件页 → 本插件详情卡:内容区(工具链状态 + 检测更新 + 一键更新)
|
|
179
|
+
ctx.slots.inject('plugins.detail.section', () => ctx.slots.register({
|
|
180
|
+
name: 'plugins.detail.section',
|
|
181
|
+
id: 'artcraft-status',
|
|
182
|
+
order: 50,
|
|
183
|
+
label: () => 'ArtCraft 工具链与更新',
|
|
184
|
+
locale: NS,
|
|
185
|
+
}, StatusSection));
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
},
|
|
189
|
+
});
|
package/index.js
CHANGED
|
@@ -39,10 +39,11 @@ import crypto from 'node:crypto';
|
|
|
39
39
|
import { fileURLToPath } from 'node:url';
|
|
40
40
|
|
|
41
41
|
const NAME = 'artcraft';
|
|
42
|
-
const VERSION = '0.
|
|
42
|
+
const VERSION = '0.2.0';
|
|
43
43
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
44
44
|
|
|
45
45
|
const BINS = ['photocraft', 'filmcraft', 'effectcraft'];
|
|
46
|
+
const PKG_NAME = '@ninkch/dsh-artcraft';
|
|
46
47
|
const DEFAULT_PORTS = { filmcraft: 9876, effectcraft: 9877 };
|
|
47
48
|
|
|
48
49
|
const BODY_CAP = 2 * 1024 * 1024; // 普通 JSON 请求体上限
|
|
@@ -152,14 +153,14 @@ function pushTail(arr, chunk) {
|
|
|
152
153
|
}
|
|
153
154
|
|
|
154
155
|
/** 运行子进程到结束;返回 {ok, code, signal, stdout, stderr, ms} */
|
|
155
|
-
function runProcess(exe, args, { cwd, timeoutMs = 15 * 60_000 } = {}) {
|
|
156
|
+
function runProcess(exe, args, { cwd, timeoutMs = 15 * 60_000, shell = false } = {}) {
|
|
156
157
|
return new Promise((resolve) => {
|
|
157
158
|
const started = Date.now();
|
|
158
159
|
const stdout = [];
|
|
159
160
|
const stderr = [];
|
|
160
161
|
let child;
|
|
161
162
|
try {
|
|
162
|
-
child = spawn(String(exe), (args ?? []).map(String), { cwd, windowsHide: true });
|
|
163
|
+
child = spawn(String(exe), (args ?? []).map(String), { cwd, windowsHide: true, shell });
|
|
163
164
|
} catch (e) {
|
|
164
165
|
resolve({ ok: false, code: -1, signal: null, stdout: '', stderr: String(e?.message ?? e), ms: 0 });
|
|
165
166
|
return;
|
|
@@ -442,6 +443,131 @@ function makeRoute(path, handler) {
|
|
|
442
443
|
};
|
|
443
444
|
}
|
|
444
445
|
|
|
446
|
+
/* ============================ 自更新(检查 / 应用) ============================ */
|
|
447
|
+
|
|
448
|
+
function semverGt(a, b) {
|
|
449
|
+
const p = (s) => String(s ?? '').trim().replace(/^v/, '').split('.').map((n) => parseInt(n, 10) || 0);
|
|
450
|
+
const [a1, a2, a3] = p(a), [b1, b2, b3] = p(b);
|
|
451
|
+
return a1 !== b1 ? a1 > b1 : a2 !== b2 ? a2 > b2 : a3 > b3;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/** 探测本插件的安装形态:git 仓库(link 开发)还是 npm 安装 */
|
|
455
|
+
async function detectInstall() {
|
|
456
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
457
|
+
const real = await fs.realpath(here);
|
|
458
|
+
let dir = real;
|
|
459
|
+
let repoDir = null;
|
|
460
|
+
while (true) {
|
|
461
|
+
try {
|
|
462
|
+
const st = await fs.stat(path.join(dir, '.git'));
|
|
463
|
+
if (st.isDirectory() || st.isFile()) { repoDir = dir; break; }
|
|
464
|
+
} catch { /* 继续 */ }
|
|
465
|
+
const parent = path.dirname(dir);
|
|
466
|
+
if (parent === dir) break;
|
|
467
|
+
dir = parent;
|
|
468
|
+
}
|
|
469
|
+
let profileDir = null;
|
|
470
|
+
if (!repoDir) {
|
|
471
|
+
dir = path.dirname(here);
|
|
472
|
+
while (true) {
|
|
473
|
+
try {
|
|
474
|
+
const txt = await fs.readFile(path.join(dir, 'package.json'), 'utf8');
|
|
475
|
+
if (txt.includes('"dsh"')) { profileDir = dir; break; }
|
|
476
|
+
} catch { /* 继续 */ }
|
|
477
|
+
const parent = path.dirname(dir);
|
|
478
|
+
if (parent === dir) break;
|
|
479
|
+
dir = parent;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
let gitClean = null;
|
|
483
|
+
let branch = null;
|
|
484
|
+
if (repoDir) {
|
|
485
|
+
const st = await runProcess('git', ['status', '--porcelain'], { cwd: repoDir, timeoutMs: 20000 });
|
|
486
|
+
gitClean = st.ok && st.stdout.trim() === '';
|
|
487
|
+
const b = await runProcess('git', ['branch', '--show-current'], { cwd: repoDir, timeoutMs: 20000 });
|
|
488
|
+
branch = (b.stdout || '').trim() || null;
|
|
489
|
+
}
|
|
490
|
+
return { mode: repoDir ? 'git' : 'npm', here, real, repoDir, profileDir, gitClean, branch };
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
async function fetchLatestVersion() {
|
|
494
|
+
const url = `https://registry.npmjs.org/${PKG_NAME.replace('/', '%2f')}/latest`;
|
|
495
|
+
try {
|
|
496
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(12000) });
|
|
497
|
+
if (res.status === 404) return { latest: null, note: 'npm 上还没有这个包' };
|
|
498
|
+
if (!res.ok) throw new Error('registry 返回 ' + res.status);
|
|
499
|
+
const j = await res.json();
|
|
500
|
+
return { latest: j.version ?? null };
|
|
501
|
+
} catch (e) {
|
|
502
|
+
return { latest: null, note: '查询 npm 失败: ' + String(e?.cause?.message || e?.message || e) };
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
async function updateCheck() {
|
|
507
|
+
const current = VERSION;
|
|
508
|
+
const { latest, note } = await fetchLatestVersion();
|
|
509
|
+
const info = await detectInstall();
|
|
510
|
+
return {
|
|
511
|
+
current,
|
|
512
|
+
latest,
|
|
513
|
+
updateAvailable: !!(latest && semverGt(latest, current)),
|
|
514
|
+
mode: info.mode,
|
|
515
|
+
repoDir: info.repoDir,
|
|
516
|
+
gitClean: info.gitClean,
|
|
517
|
+
branch: info.branch,
|
|
518
|
+
note,
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
async function findRuntimeTools() {
|
|
523
|
+
const isWin = process.platform === 'win32';
|
|
524
|
+
const pnCmd = isWin ? 'pnpm.cmd' : 'pnpm';
|
|
525
|
+
const probe = await runProcess(pnCmd, ['--version'], { timeoutMs: 20000 });
|
|
526
|
+
if (probe.ok) return { pnpmCmd: pnCmd };
|
|
527
|
+
try {
|
|
528
|
+
const rtRoot = path.join(os.homedir(), '.dsh', 'dsh-runtimes');
|
|
529
|
+
const entries = await fs.readdir(rtRoot, { withFileTypes: true });
|
|
530
|
+
for (const e of entries) {
|
|
531
|
+
if (!e.isDirectory()) continue;
|
|
532
|
+
const pnpmMjs = path.join(rtRoot, e.name, 'dependencies', 'pnpm', 'bin', 'pnpm.mjs');
|
|
533
|
+
const nodeExe = path.join(rtRoot, e.name, 'dependencies', 'node', 'bin', isWin ? 'node.exe' : 'node');
|
|
534
|
+
if ((await fileExists(pnpmMjs)) && (await fileExists(nodeExe))) {
|
|
535
|
+
return { nodeExe, pnpmMjs };
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
} catch { /* ignore */ }
|
|
539
|
+
return {};
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
async function updateApply() {
|
|
543
|
+
const info = await detectInstall();
|
|
544
|
+
const { latest } = await fetchLatestVersion();
|
|
545
|
+
if (!latest) throw new Error('npm 上没有可用的发布版本');
|
|
546
|
+
if (!semverGt(latest, VERSION)) return { ok: true, message: `已是最新(${VERSION})`, needRestart: false };
|
|
547
|
+
|
|
548
|
+
if (info.mode === 'git') {
|
|
549
|
+
if (info.gitClean === false) {
|
|
550
|
+
throw new Error('插件目录有未提交的本地改动,为避免覆盖请先 git commit / stash,再执行更新');
|
|
551
|
+
}
|
|
552
|
+
const r = await runProcess('git', ['pull', '--ff-only'], { cwd: info.repoDir, timeoutMs: 120000 });
|
|
553
|
+
return { ok: r.ok, output: (r.stdout + '\n' + r.stderr).trim(), needRestart: r.ok, latest };
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// npm 安装模式
|
|
557
|
+
if (!info.profileDir) throw new Error('未定位到 Harness profile 目录,无法自动更新');
|
|
558
|
+
const tools = await findRuntimeTools();
|
|
559
|
+
let cmd, args;
|
|
560
|
+
if (tools.pnpmCmd) {
|
|
561
|
+
cmd = tools.pnpmCmd; args = ['add', `${PKG_NAME}@latest`, '--dir', info.profileDir];
|
|
562
|
+
} else if (tools.pnpmMjs) {
|
|
563
|
+
cmd = tools.nodeExe; args = [tools.pnpmMjs, 'add', `${PKG_NAME}@latest`, '--dir', info.profileDir];
|
|
564
|
+
} else {
|
|
565
|
+
throw new Error('未找到 pnpm(PATH 与 ~/.dsh/dsh-runtimes 均未命中),请手动在 profile 目录执行 pnpm add');
|
|
566
|
+
}
|
|
567
|
+
const r = await runProcess(cmd, args, { cwd: info.profileDir, timeoutMs: 300000, shell: !!tools.pnpmCmd });
|
|
568
|
+
return { ok: r.ok, output: (r.stdout + '\n' + r.stderr).trim(), needRestart: r.ok, latest };
|
|
569
|
+
}
|
|
570
|
+
|
|
445
571
|
/* ============================ 插件入口 ============================ */
|
|
446
572
|
|
|
447
573
|
const toolNames = [];
|
|
@@ -584,6 +710,23 @@ export function apply(ctx) {
|
|
|
584
710
|
},
|
|
585
711
|
});
|
|
586
712
|
|
|
713
|
+
attempt({
|
|
714
|
+
name: 'artcraft_update',
|
|
715
|
+
description: '检查/应用 @ninkch/dsh-artcraft 插件自身的更新:action=check 对比本地与 npm latest;action=apply 执行更新(git pull 或 pnpm add,按安装形态自动选择),完成后需重启 Harness 生效。',
|
|
716
|
+
schema: {
|
|
717
|
+
type: 'object',
|
|
718
|
+
properties: {
|
|
719
|
+
action: { type: 'string', enum: ['check', 'apply'] },
|
|
720
|
+
},
|
|
721
|
+
required: ['action'],
|
|
722
|
+
additionalProperties: false,
|
|
723
|
+
},
|
|
724
|
+
async execute(args) {
|
|
725
|
+
if (String(args.action) === 'apply') return updateApply();
|
|
726
|
+
return updateCheck();
|
|
727
|
+
},
|
|
728
|
+
});
|
|
729
|
+
|
|
587
730
|
return () => {
|
|
588
731
|
for (const d of disposers) { try { d(); } catch { /* ignore */ } }
|
|
589
732
|
};
|
|
@@ -739,6 +882,15 @@ export function apply(ctx) {
|
|
|
739
882
|
}
|
|
740
883
|
}),
|
|
741
884
|
|
|
885
|
+
makeRoute(`/${NAME}/api/update/check`, async (req, res) => {
|
|
886
|
+
sendJson(res, 200, await updateCheck());
|
|
887
|
+
}),
|
|
888
|
+
|
|
889
|
+
makeRoute(`/${NAME}/api/update/apply`, async (req, res) => {
|
|
890
|
+
if (req.method !== 'POST') throw new Error('仅支持 POST');
|
|
891
|
+
sendJson(res, 200, await updateApply());
|
|
892
|
+
}),
|
|
893
|
+
|
|
742
894
|
makeRoute(`/${NAME}/api/control`, async (req, res) => {
|
|
743
895
|
if (req.method !== 'POST') throw new Error('仅支持 POST');
|
|
744
896
|
const cfg = await loadConfig();
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
{
|
|
1
|
+
{
|
|
2
2
|
"name": "@ninkch/dsh-artcraft",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Drive the open-source ArtCraft studio apps (PhotoCraft / FilmCraft / EffectCraft) from DeepSeek Harness — agent tools
|
|
5
|
+
"description": "Drive the open-source ArtCraft studio apps (PhotoCraft / FilmCraft / EffectCraft) from DeepSeek Harness — agent tools, a same-origin HTTP API, and a sidebar panel with toolchain status and self-update.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"deepseek-harness",
|
|
8
8
|
"dsh-plugin",
|
|
@@ -14,15 +14,25 @@
|
|
|
14
14
|
"ai-agents"
|
|
15
15
|
],
|
|
16
16
|
"license": "Apache-2.0",
|
|
17
|
+
"author": "NINKCH",
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"registry": "https://registry.npmjs.org/"
|
|
20
|
+
},
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/NINKCH/dsh-artcraft.git"
|
|
24
|
+
},
|
|
17
25
|
"engines": {
|
|
18
26
|
"node": ">=18"
|
|
19
27
|
},
|
|
20
28
|
"exports": {
|
|
21
29
|
".": "./index.js",
|
|
30
|
+
"./client": "./client.js",
|
|
22
31
|
"./package.json": "./package.json"
|
|
23
32
|
},
|
|
24
33
|
"files": [
|
|
25
34
|
"index.js",
|
|
35
|
+
"client.js",
|
|
26
36
|
"cordis.patch.yml",
|
|
27
37
|
"artcraft.config.json.example",
|
|
28
38
|
"README.md",
|
|
@@ -32,6 +42,10 @@
|
|
|
32
42
|
"dsh": {
|
|
33
43
|
"bundle": {
|
|
34
44
|
"patch": "./cordis.patch.yml"
|
|
45
|
+
},
|
|
46
|
+
"client": {
|
|
47
|
+
"platform": "web",
|
|
48
|
+
"immediately": true
|
|
35
49
|
}
|
|
36
50
|
}
|
|
37
51
|
}
|