@ninkch/dsh-artcraft 0.0.0-stage → 0.2.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.
@@ -0,0 +1,284 @@
1
+ <div align="center">
2
+
3
+ # ArtCraft Bridge
4
+
5
+ **用 DeepSeek Harness 在对话里、或在你的前端界面中,驱动开源创作套件 ArtCraft —— PhotoCraft · FilmCraft · EffectCraft**
6
+
7
+ [English](README.md) | [简体中文](README.zh-CN.md)
8
+
9
+ `@ninkch/dsh-artcraft` · DeepSeek Harness 插件 · Apache-2.0
10
+
11
+ </div>
12
+
13
+ ---
14
+
15
+ ## 为什么做这个
16
+
17
+ [ArtCraft](https://getartcraft.com/apps) 是一套用纯 Rust 从零重写的免费公平源代码(fair source)
18
+ Adobe 对标工具:
19
+ **PhotoCraft**(对标 Photoshop)、**FilmCraft**(对标 Premiere Pro)、**EffectCraft**(对标 After
20
+ Effects)等。它们有一个大多数桌面软件都不具备的共同基因:**每个菜单项、滑块、对话框都是一条
21
+ 命令**,而同一套命令既能从无头 CLI 调用,也能走 JSON 控制通道或 MCP——这些 App 从第一天起
22
+ 就是为 agent 设计的。
23
+
24
+ 本插件把这套自动化面接进 DeepSeek Harness,让 AI 不只是"谈论"这些软件,而是真的**操作**它们:
25
+
26
+ - **对话里** —— 四个 Agent 工具,任何 Harness 会话都能检查工具链、执行 CLI 命令、向运行中的
27
+ App 发控制通道命令、在专属数据目录里读写文件。
28
+ - **浏览器里** —— 同源 HTTP API(`/artcraft/api/*`),任何 Harness 前端都能直接调用:无 CORS、
29
+ 无密钥。它为 [无限画布 / infinite-canvas](../dsh-infinite_canvas/README.md)(节点式 AI 短剧
30
+ 分镜画布)而建,但任何前端都可用。
31
+ - **磁盘上** —— 每台机器一个 `dataRoot`,素材、工程文件、渲染产物按项目分区
32
+ (`media/ · craft/ · out/`),生成式流水线与桌面应用在同一片地基上会合。
33
+
34
+ 典型的 AI 短剧闭环:模型生成分镜图 → PhotoCraft 批量调色锐化 → EffectCraft 渲染动态片头 →
35
+ FilmCraft 剪辑、调色、混音、烧字幕 → 导出 H.264。"生成"之后的每一步都由本插件无头驱动。
36
+
37
+ ## 整体结构
38
+
39
+ ```
40
+ ┌──────────────── DeepSeek Harness 网页端(浏览器)────────────────┐
41
+ │ 以无限画布为例:① 上传资产 ② 发起任务 ③ 取回产物 │
42
+ └─────────────────────────────┬───────────────────────────────────┘
43
+ ▼ 同源 fetch,无 CORS
44
+ ┌──────────────── DSH Host · @ninkch/dsh-artcraft ────────────────┐
45
+ │ HTTP 路由 /artcraft/api/* + Agent 工具 artcraft_* │
46
+ │ → 子进程(各 CLI) → TCP 控制通道 │
47
+ └──────┬───────────────────────┬───────────────────────┬───────────┘
48
+ ▼ ▼ ▼
49
+ photocraft-cli filmcraft-cli effectcraft-cli
50
+ 批量调色、PSD 编辑 EDL/XML 进出、剪辑调色 .ecproj(JSON) 渲染、
51
+ (无头) 混音导出 Lottie 导出
52
+ └───────────────────────┴───────────────────────┘
53
+ ▼
54
+ <dataRoot>/<项目>/{media, craft, out}
55
+ ```
56
+
57
+ 全流程本地运行:不联网、无账号、无遥测。
58
+
59
+ ## 环境要求
60
+
61
+ - **DeepSeek Harness** 桌面版(Harness 支持的任意平台)。
62
+ - **ArtCraft 应用** —— 从 [getartcraft.com](https://getartcraft.com/apps) 或
63
+ [storytold GitHub Releases](https://github.com/storytold) 按需下载。Windows 与 Linux 的
64
+ portable/安装包内置 CLI(`photocraft-cli.exe` 等);macOS 的 CLI 是单独的公证 zip。
65
+ 已验证版本:PhotoCraft 0.6、FilmCraft 0.5、EffectCraft(早期构建)。
66
+ - 其他什么都不需要:无 Python、无 FFmpeg —— ArtCraft 自带编解码器。
67
+
68
+ ## 安装
69
+
70
+ **1. 把本仓库放到磁盘任意位置**(放进 Harness 工作区、与其他本地插件平级是惯例)。
71
+
72
+ **2. 在 Harness profile 中注册。** profile 是一个 pnpm 包目录:
73
+ `C:\Users\<你>\.dsh\profiles\<profile>\`(桌面版默认 `desktop`)。改两处:
74
+
75
+ ```jsonc
76
+ // profile 下的 package.json
77
+ {
78
+ "dependencies": {
79
+ // ...已有依赖...
80
+ "@ninkch/dsh-artcraft": "^0.1.1", // ← 来自 npm
81
+ // 或者,本地开发时用 link 指向本仓库:
82
+ // "@ninkch/dsh-artcraft": "link:D:/code/deepseek-plugin/dsh-artcraft"
83
+ },
84
+ "dsh": { "profile": { "bundles": [
85
+ // ...已有 bundles...
86
+ "@ninkch/dsh-artcraft" // ← 加入白名单
87
+ ] } }
88
+ }
89
+ ```
90
+
91
+ 然后建立链接并重启一次 Harness(Host 侧代码只在启动时加载):
92
+
93
+ ```sh
94
+ pnpm install --dir C:/Users/<你>/.dsh/profiles/desktop
95
+ ```
96
+
97
+ > 若你的 Harness 版本带有"从文件夹安装插件"的管理界面,用它也行 —— 最终落盘的就是上面两行。
98
+
99
+ **3. 配置** `index.js` 旁边的 `artcraft.config.json`(从 `artcraft.config.json.example` 复制;
100
+ 该文件已 gitignore,且每次请求都会重读 —— 改路径无需重启):
101
+
102
+ ```jsonc
103
+ {
104
+ "dataRoot": "D:/craft-workspace", // 素材与产物的根目录
105
+ "bins": {
106
+ // 三种写法:目录(自动拼接 <名>-cli 可执行文件)、完整 exe 路径、或走 PATH 的裸命令名
107
+ "photocraft": "C:/craft/photocraft-0.6.0-windows-x64-portable",
108
+ "filmcraft": "C:/craft/filmcraft-0.5.0-windows-x64-portable",
109
+ "effectcraft": "C:/craft/effectcraft-windows-x64-portable"
110
+ },
111
+ "control": { "filmcraft": 9876, "effectcraft": 9877 }
112
+ }
113
+ ```
114
+
115
+ **4. 验证。** 在任意 Harness 对话里让 AI 执行 `artcraft_status`,`bins.*.found` 全绿即就绪。
116
+
117
+ ## 在对话里驱动
118
+
119
+ 插件注册四个工具,agent 自行编排,所以自然语言直接可用:
120
+
121
+ > *"把 `demo/media` 里的 PNG 都用柔和 S 曲线调色、智能锐化 80,结果放进 `demo/out/graded`。"*
122
+
123
+ 这句话背后通常是:
124
+
125
+ ```
126
+ artcraft_cli { bin:"photocraft", args:["batch","--actions","demo/craft/grade.json",
127
+ "--in","demo/media","--out","demo/out/graded"] }
128
+ ```
129
+
130
+ | 工具 | 用途 |
131
+ |---|---|
132
+ | `artcraft_status` | 工具体检:CLI 是否找到、数据目录、控制端口、进行中的任务 |
133
+ | `artcraft_cli` | 同步执行任意 CLI 子命令(`photocraft-cli run/batch`、`filmcraft-cli commands` 等) |
134
+ | `artcraft_control` | 向**运行中**的 FilmCraft / EffectCraft 控制通道发一条命令 |
135
+ | `artcraft_fs` | 在 `dataRoot` 内 list / stat / 读 / 写 / 建目录(写 `grade.json`、取回 `.ecproj` 等) |
136
+
137
+ ## 在浏览器里驱动
138
+
139
+ 同源 HTTP,JSON 进出。核心模式是**任务生命周期**——发起、轮询、取回,长渲染不占用请求周期:
140
+
141
+ ```
142
+ POST /artcraft/api/job { "bin":"effectcraft",
143
+ "args":["render","--comp","Main",
144
+ "--out","demo/out/intro.mp4","demo/craft/intro.ecproj"],
145
+ "outputs":["demo/out/intro.mp4"] }
146
+ → { "id":"7f2c…" }
147
+
148
+ GET /artcraft/api/job?id=7f2c…&tail=30
149
+ → { "status":"running|done|failed|timeout",
150
+ "stdout":"…尾部日志…", "stderr":"…",
151
+ "outputs":[{"rel":"demo/out/intro.mp4","exists":true,"size":812345}] }
152
+
153
+ GET /artcraft/api/file?rel=demo/out/intro.mp4 → 原始字节(流式)
154
+ POST /artcraft/api/job/cancel { "id":"7f2c…" }
155
+ ```
156
+
157
+ 全部路由:
158
+
159
+ | 路由 | 用途 |
160
+ |---|---|
161
+ | `GET /artcraft/api/status` | 工具体检(与 `artcraft_status` 同数据) |
162
+ | `GET /artcraft/api/projects` | `dataRoot` 下的项目目录 |
163
+ | `POST /artcraft/api/fs?action=write&rel=…` | 写文件 —— 原始字节流(Blob 直传)或 JSON `{rel, text\|base64}` |
164
+ | `GET /artcraft/api/fs?op=list\|stat\|readtext&rel=…` | 读目录、元数据、小文本 |
165
+ | `GET /artcraft/api/file?rel=…` | 下载文件 —— 流式,上限 2 GB |
166
+ | `POST /artcraft/api/job` · `GET ?id=…` · `POST /artcraft/api/job/cancel` | 任务生命周期,含产物探测与日志尾部 |
167
+ | `POST /artcraft/api/control` | 控制通道同步调用 |
168
+
169
+ 上传单次上限 320 MB(原始路径 Blob 直传,无需 base64);下载流式上限 2 GB。
170
+ 所有路径参数相对 `dataRoot` 且被限制在其中。
171
+
172
+ ## 控制通道:驱动正在运行的 App
173
+
174
+ CLI 是无头、一次性的。想边看边调 —— 调色、扒片 —— 就带控制通道启动桌面 App,实时对话:
175
+
176
+ ```sh
177
+ filmcraft --control 9876 # 或设 FILMCRAFT_CONTROL_PORT;仅回环监听
178
+ ```
179
+
180
+ ```jsonc
181
+ POST /artcraft/api/control
182
+ { "app": "filmcraft", "method": "engine.execute",
183
+ "params": { "command": "timeline.place",
184
+ "params": { "item": 1, "track": "V1", "seconds": 0, "duration": 1270080000000 } } }
185
+
186
+ // 给 87 号片段上一条 Lumetri 曲线……
187
+ { "command": "effects.setParam",
188
+ "params": { "clip": 87, "effect": "lumetri", "param": "curve_luma",
189
+ "value": [[0,0],[0.25,0.21],[0.75,0.82],[1,1]] } }
190
+
191
+ // ……再看一眼 App 里长什么样
192
+ { "method": "ui.screenshot", "params": { "path": "frame.png" } }
193
+ ```
194
+
195
+ 时间可传 `seconds` / `frame` / `timecode`(FilmCraft 内部以每秒 254,016,000,000 tick 计)。
196
+ `filmcraft-cli commands` 列出全部 650+ 引擎命令;协议详见
197
+ [filmcraft/docs/control-protocol.md](https://github.com/storytold/filmcraft/blob/main/docs/control-protocol.md)。
198
+ EffectCraft 同族协议,端口默认 9877。
199
+
200
+ > PhotoCraft 的控制通道是带鉴权的变体,本插件**有意不代理** —— PhotoCraft 走无头 CLI 足够。
201
+
202
+ ## 与无限画布(infinite-canvas)协作
203
+
204
+ 两个插件零代码耦合,只在 HTTP 和磁盘布局上会合。画布侧:
205
+
206
+ 1. 把生成的资产(IndexedDB Blob)同步进 `<项目>/media/`;
207
+ 2. 以 `<项目>/craft/` 里的工程文件发起 PhotoCraft / FilmCraft / EffectCraft 任务;
208
+ 3. 从 `<项目>/out/` 取回成片入库。
209
+
210
+ 如果你要做类似的前端,照此契约即可:项目名作文件夹名、保持 `media · craft · out` 三件套,
211
+ 一切自然组合。
212
+
213
+ ## 常用配方
214
+
215
+ **分镜图统一调色** —— `demo/craft/grade.json`:
216
+
217
+ ```json
218
+ { "actions": [
219
+ { "cmd": "layer.newAdjustmentLayer.curves", "params": { "points": [[0,0],[64,48],[192,212],[255,255]] } },
220
+ { "cmd": "filter.sharpen.smartSharpen", "params": { "amount": 80 } }
221
+ ] }
222
+ ```
223
+
224
+ ```sh
225
+ photocraft-cli batch --actions demo/craft/grade.json --in demo/media --out demo/out/graded
226
+ ```
227
+
228
+ **告别浏览器录屏的成片** —— 画布把分镜导出为 FCP7 XML(`xmeml`)或 CMX3600 EDL 写入
229
+ `demo/craft/timeline.xml`;FilmCraft 导入后自动关联 `demo/media/`,导出 H.264(Windows 走
230
+ NVENC 硬编)、ProRes、DNxHR,附 EBU R128 响度归一与字幕烧录。交换格式是 FilmCraft 自动化
231
+ 最划算的地方 —— EDL/XML 是纯文本,你的流水线本来就会写。
232
+
233
+ **动画字幕即代码** —— EffectCraft 工程(`.ecproj`)是版本化 JSON,模型可以直接写、也可以填
234
+ 模板。渲染成 MP4/ProRes —— 带 alpha 的 ProRes 4444 可直接叠进 FilmCraft 当图形层 —— 或导出
235
+ **Lottie**,在网页里内联播放。
236
+
237
+ ## 排错
238
+
239
+ - **`bins.*.found: false`** —— 核对上面的路径写法;CLI 必须在 App 目录旁(或在 `PATH`)。
240
+ 改 `artcraft.config.json` 下一次请求即生效。
241
+ - **`/artcraft/api/*` 返回 404** —— bundle 没在 profile 注册(见"安装"),或注册后没重启。
242
+ 对话里调 `artcraft_status` 是最快探针。
243
+ - **控制通道连接失败** —— App 必须以 `--control <端口>` 启动;通道仅回环且无鉴权,端口上
244
+ 不会有别的应答者。
245
+ - **任务卡住** —— `GET /artcraft/api/job` 带日志列出全部任务,按 id 取消;插件重载会按设计
246
+ 终止所有子任务。
247
+ - **路径越界报错** —— 设计如此:所有文件参数必须落在 `dataRoot` 之内。
248
+
249
+ ## 安全与信任模型
250
+
251
+ HTTP 面无鉴权,挂在 Harness Web 服务之下(仅回环监听)—— 把端口当本机可信处理,**不要暴露
252
+ 到网络**;确需远程请加带鉴权的反向代理。子进程锁定三个白名单可执行文件;文件访问锁定
253
+ `dataRoot`;控制通道只会拨 `127.0.0.1`。
254
+
255
+ ## 发布
256
+
257
+ 本插件已发布到 npm:**[`@ninkch/dsh-artcraft`](https://www.npmjs.com/package/@ninkch/dsh-artcraft)**
258
+ (带 scope 的包发布时需要 `npm publish --access public`)。使用时把包名加进 Harness profile 的
259
+ `dependencies` 与 `dsh.profile.bundles`(见"安装"),profile 里跑一次 `pnpm install` 即可。
260
+ 本地开发则把依赖换成 `"link:<本仓库路径>"`。包名必须一致的三处:profile 的 `package.json`
261
+ (依赖 + bundles)、本仓库的 `cordis.patch.yml`。
262
+
263
+ GitHub 上传本仓库即可:`README.md` 英文优先,附
264
+ [简体中文](README.zh-CN.md) 对照。
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
+
270
+ ## 许可
271
+
272
+ **本插件(`@ninkch/dsh-artcraft`):Apache-2.0**(见 [LICENSE](LICENSE))。不包含 ArtCraft 应用的任何代码 —— 只调用其公开自动化面
273
+ (无头 CLI + JSON 控制通道)。
274
+
275
+ **ArtCraft 各应用本身并非 OSI 开源**:它们采用 **ArtCraft License (WIP)** —— 一种 "fair source"
276
+ (公平源代码)许可([全文见 NOTICE.md](NOTICE.md))。给构建在其上的使用者划重点:
277
+
278
+ - ✅ 免费使用;可为私人目的备份、复制、修改、编译
279
+ - ✅ **用这些应用创作的所有媒体完全归你** —— 渲染出的短剧成片就是你的商业资产,没有附加条件
280
+ - ❌ 不得把应用当作自己的软件出售;不得用其代码开发竞品
281
+ - ❌ 不得在 fork 中移除社区/捐赠链接;未经许可不得用 ArtCraft 名称、Logo 或吉祥物做商业宣传
282
+ - 🤝 团队公开承诺:若业务终结,软件将转为 OSI 认可的开源许可
283
+
284
+ 对 AI 短剧工作室的务实解读:本地随便用,别动应用里的社区链接,卖你的片子——而不是卖应用。
@@ -0,0 +1,12 @@
1
+ {
2
+ "dataRoot": "D:/craft-workspace",
3
+ "bins": {
4
+ "photocraft": "C:/craft/photocraft-0.6.0-windows-x64-portable",
5
+ "filmcraft": "C:/craft/filmcraft-0.5.0-windows-x64-portable",
6
+ "effectcraft": "C:/craft/effectcraft-0.1.0-windows-x64-portable"
7
+ },
8
+ "control": {
9
+ "filmcraft": 9876,
10
+ "effectcraft": 9877
11
+ }
12
+ }
package/client.js ADDED
@@ -0,0 +1,181 @@
1
+ // @ninkch/dsh-artcraft — client half.
2
+ // 侧栏 "ArtCraft" 按钮 + 面板:工具链状态 / 自更新(检查 + 一键应用)。
3
+ window.__ModuleLoader__.load({
4
+ id: '@ninkch/dsh-artcraft',
5
+ factory(require) {
6
+ const React = require('react');
7
+ const h = React.createElement;
8
+ const { useState, useEffect, useCallback, useRef } = React;
9
+
10
+ const NS = 'artcraft';
11
+ const PANEL_ID = 'artcraft';
12
+ const API = '/artcraft/api';
13
+
14
+ const CSS = `
15
+ .dshac-panel { padding: 14px; font-size: 13px; line-height: 1.5; overflow: auto; height: 100%; box-sizing: border-box; }
16
+ .dshac-h { font-size: 14px; font-weight: 600; margin: 0 0 10px; display: flex; align-items: center; gap: 8px; }
17
+ .dshac-card { border: 1px solid var(--dsw-border-primary, rgba(128,128,128,.35)); border-radius: 10px; padding: 12px; margin-bottom: 12px; }
18
+ .dshac-title { font-weight: 600; margin-bottom: 8px; display: flex; justify-content: space-between; align-items: center; }
19
+ .dshac-row { display: flex; align-items: center; gap: 8px; margin: 4px 0; }
20
+ .dshac-dot { width: 9px; height: 9px; border-radius: 50%; flex: none; }
21
+ .dshac-dot.ok { background: #34c759; } .dshac-dot.bad { background: #ff5f57; } .dshac-dot.unknown { background: #8e8e93; }
22
+ .dshac-mono { font-family: ui-monospace, Consolas, monospace; font-size: 12px; word-break: break-all; }
23
+ .dshac-muted { opacity: .65; font-size: 12px; }
24
+ .dshac-btn { border: 1px solid var(--dsw-border-primary, rgba(128,128,128,.4)); background: transparent; color: inherit;
25
+ border-radius: 7px; padding: 4px 12px; font-size: 12px; cursor: pointer; }
26
+ .dshac-btn:hover:not(:disabled) { background: rgba(128,128,128,.15); }
27
+ .dshac-btn:disabled { opacity: .5; cursor: default; }
28
+ .dshac-btn.primary { background: rgba(80,120,255,.25); }
29
+ .dshac-out { white-space: pre-wrap; font-family: ui-monospace, Consolas, monospace; font-size: 11px;
30
+ background: rgba(128,128,128,.12); border-radius: 7px; padding: 8px; margin-top: 8px; max-height: 180px; overflow: auto; }
31
+ .dshac-note { margin-top: 8px; color: var(--dsw-alias-state-warning-primary, #d69e2e); font-size: 12px; }
32
+ .dshac-ok { color: var(--dsw-alias-state-success-primary, #34c759); }
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
+ function Dot({ ok }) {
47
+ return h('span', { className: 'dshac-dot ' + (ok === true ? 'ok' : ok === false ? 'bad' : 'unknown') });
48
+ }
49
+
50
+ function PanelIcon() {
51
+ return h('svg', { width: 18, height: 18, viewBox: '0 0 20 20', fill: 'none' },
52
+ h('rect', { x: 3, y: 3, width: 14, height: 14, rx: 3, stroke: 'currentColor', 'stroke-width': 1.5 }),
53
+ h('path', { d: 'M6.5 10h4M8.5 8v4', stroke: 'currentColor', 'stroke-width': 1.5, 'stroke-linecap': 'round' }),
54
+ h('circle', { cx: 13.5, cy: 13, r: 1.6, fill: 'currentColor' }),
55
+ );
56
+ }
57
+
58
+ function ArtCraftPage() {
59
+ const [status, setStatus] = useState(null);
60
+ const [statusErr, setStatusErr] = useState(null);
61
+ const [upd, setUpd] = useState(null);
62
+ const [busy, setBusy] = useState('');
63
+ const [out, setOut] = useState('');
64
+ const [needRestart, setNeedRestart] = useState(false);
65
+ const alive = useRef(true);
66
+ useEffect(() => () => { alive.current = false; }, []);
67
+
68
+ const loadStatus = useCallback(async () => {
69
+ try {
70
+ const s = await api('/status');
71
+ if (!alive.current) return;
72
+ setStatus(s); setStatusErr(null);
73
+ } catch (e) {
74
+ if (!alive.current) return;
75
+ setStatusErr(String(e.message || e));
76
+ }
77
+ }, []);
78
+ const checkUpdate = useCallback(async () => {
79
+ setBusy('check'); setOut('');
80
+ try {
81
+ const u = await api('/update/check');
82
+ if (!alive.current) return;
83
+ setUpd(u);
84
+ setOut(u.updateAvailable
85
+ ? `有新版本:${u.latest}(当前 ${u.current}),安装方式:${u.mode === 'git' ? 'git 仓库' : 'npm 安装'}`
86
+ : `已是最新${u.latest ? '(npm latest ' + u.latest + ')' : ''}${u.note ? ' · ' + u.note : ''}`);
87
+ } catch (e) { if (alive.current) setOut('检查失败:' + String(e.message || e)); }
88
+ finally { if (alive.current) setBusy(''); }
89
+ }, []);
90
+ const applyUpdate = useCallback(async () => {
91
+ setBusy('apply'); setOut('');
92
+ try {
93
+ const r = await api('/update/apply', { method: 'POST', headers: { 'content-type': 'application/json' }, body: '{}' });
94
+ if (!alive.current) return;
95
+ setOut((r.output || String(r.message || '')) + (r.needRestart ? '\n\n✅ 更新完成 —— 请重启 DeepSeek Harness 后生效。' : ''));
96
+ if (r.needRestart) setNeedRestart(true);
97
+ } catch (e) { if (alive.current) setOut('更新失败:' + String(e.message || e)); }
98
+ finally { if (alive.current) setBusy(''); }
99
+ }, []);
100
+
101
+ useEffect(() => { loadStatus(); checkUpdate(); }, [loadStatus, checkUpdate]);
102
+
103
+ const bins = status?.bins ?? {};
104
+ const binRow = (key, label) => {
105
+ const b = bins[key];
106
+ return h('div', { className: 'dshac-row' },
107
+ h(Dot, { ok: b ? b.found : null }),
108
+ h('span', null, label),
109
+ b?.found ? null : h('span', { className: 'dshac-muted' }, b?.note || '未配置'),
110
+ );
111
+ };
112
+
113
+ return h('div', { className: 'dshac-panel' },
114
+ h('div', { className: 'dshac-h' }, 'ArtCraft Bridge',
115
+ status ? h('span', { className: 'dshac-muted' }, 'v' + status.version) : null),
116
+
117
+ statusErr
118
+ ? h('div', { className: 'dshac-card' },
119
+ h('div', { className: 'dshac-title' }, '未连接'),
120
+ h('div', { className: 'dshac-muted' }, statusErr + ' —— 插件可能未注册或 Harness 需要重启。'))
121
+ : h('div', { className: 'dshac-card' },
122
+ h('div', { className: 'dshac-title' }, '工具链',
123
+ h('button', { className: 'dshac-btn', onClick: loadStatus, disabled: busy !== '' }, '刷新')),
124
+ binRow('photocraft', 'PhotoCraft (CLI)'),
125
+ binRow('filmcraft', 'FilmCraft (CLI)'),
126
+ binRow('effectcraft', 'EffectCraft (CLI)'),
127
+ status?.dataRoot ? h('div', { className: 'dshac-row dshac-muted' },
128
+ '数据目录:', h('span', { className: 'dshac-mono' }, status.dataRoot.path)) : null),
129
+
130
+ h('div', { className: 'dshac-card' },
131
+ h('div', { className: 'dshac-title' }, '插件更新',
132
+ h('button', { className: 'dshac-btn primary', onClick: checkUpdate, disabled: busy !== '' },
133
+ busy === 'check' ? '检查中…' : '检查更新')),
134
+ upd
135
+ ? h('div', { className: 'dshac-row' },
136
+ h('span', null, '当前 v' + (upd.current ?? '?')),
137
+ h('span', { className: 'dshac-muted' }, '→'),
138
+ h('span', { className: upd.updateAvailable ? 'dshac-ok' : 'dshac-muted' },
139
+ 'npm latest v' + (upd.latest ?? '—')),
140
+ upd.mode ? h('span', { className: 'dshac-muted' },
141
+ '(' + (upd.mode === 'git' ? 'git 仓库 · ' + (upd.branch || '?') + (upd.gitClean === false ? ' · 有本地改动' : ' · 工作区干净') : 'npm 安装') + ')') : null)
142
+ : h('div', { className: 'dshac-muted' }, '尚未检查'),
143
+ h('div', { style: { marginTop: '8px' } },
144
+ h('button', {
145
+ className: 'dshac-btn primary',
146
+ onClick: applyUpdate,
147
+ disabled: busy !== '' || !upd?.updateAvailable,
148
+ title: upd?.updateAvailable ? '' : '先检查更新,有新版本才可应用',
149
+ }, busy === 'apply' ? '更新中…' : '一键更新')),
150
+ needRestart ? h('div', { className: 'dshac-note' }, '更新已写入 —— 重启 DeepSeek Harness 后生效。') : null,
151
+ out ? h('div', { className: 'dshac-out' }, out) : null),
152
+ );
153
+ }
154
+
155
+ return {
156
+ inject: ['slots', 'locale'],
157
+ apply(ctx) {
158
+ ctx.effect(() => ctx.locale.register(NS, { zh: { panel: 'ArtCraft' }, en: { panel: 'ArtCraft' } }), 'artcraft: dictionaries');
159
+ ctx.effect(() => {
160
+ const el = document.createElement('style');
161
+ el.id = 'dshac-styles';
162
+ el.textContent = CSS;
163
+ document.head.appendChild(el);
164
+ return () => el.remove();
165
+ }, 'artcraft: styles');
166
+ ctx.slots.inject('sidebar.panellist', () => ctx.slots.register({
167
+ name: 'sidebar.panellist',
168
+ id: PANEL_ID,
169
+ order: 101,
170
+ label: () => 'ArtCraft',
171
+ locale: NS,
172
+ }, PanelIcon));
173
+ ctx.slots.inject('main', () => ctx.slots.register({
174
+ name: 'main',
175
+ key: PANEL_ID,
176
+ locale: NS,
177
+ }, ArtCraftPage));
178
+ },
179
+ };
180
+ },
181
+ });
@@ -0,0 +1,4 @@
1
+ - insert:
2
+ - id: artcraft
3
+ name: '@ninkch/dsh-artcraft'
4
+ config: {}