@ninkch/dsh-artcraft 0.2.6 → 0.2.7

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
@@ -25,13 +25,13 @@ This plugin brings that surface into DeepSeek Harness, so an AI agent can *opera
25
25
  not just talk about them:
26
26
 
27
27
  - **In chat** — four agent tools let any Harness session check the toolchain, run CLI commands,
28
- send control-channel commands to a running app, and read/write files in a dedicated data root.
28
+ send control-channel commands to a running app, and do bridged file operations.
29
29
  - **In the browser** — a same-origin HTTP API (`/artcraft/api/*`) that any Harness web UI can call,
30
30
  no CORS, no keys. It was built for [无限画布 / infinite-canvas](../dsh-infinite_canvas/README.md),
31
31
  a node-based AI short-drama storyboard canvas, but any front end can use it.
32
- - **On disk** — one `dataRoot` per machine where assets, project files and rendered output live,
33
- laid out per project (`media/ · craft/ · out/`), so generative pipelines and the desktop apps
34
- meet on common ground.
32
+ - **Paths belong to the caller** — this plugin is a bridge and owns no data directory. Files read
33
+ or written go wherever the caller points: chat invocations default to the current conversation
34
+ workspace (or any path named in the prompt); the canvas writes under its own project folder.
35
35
 
36
36
  A typical AI short-drama loop looks like this: generate storyboard frames with a model →
37
37
  batch color-grade and sharpen them in PhotoCraft → render an animated title in EffectCraft →
@@ -55,7 +55,8 @@ Every step after "generate" runs through this plugin, headless.
55
55
  headless edits cut · grade · mix Lottie export
56
56
  └───────────────────────┴───────────────────────┘
57
57
  ▼
58
- <dataRoot>/<project>/{media, craft, out}
58
+ caller-decided locations (chat → conversation
59
+ workspace · canvas → its project folder)
59
60
  ```
60
61
 
61
62
  Everything runs locally. No cloud round-trips, no accounts, no telemetry.
@@ -102,19 +103,19 @@ pnpm install --dir C:/Users/<you>/.dsh/profiles/desktop
102
103
  > If your Harness build ships a plugin manager UI that installs from a folder, that works too —
103
104
  > it ends up writing exactly the two lines above.
104
105
 
105
- **3. Configure** `artcraft.config.json` next to `index.js` (copy from
106
- `artcraft.config.json.example`; the file is git-ignored and re-read on every request — no restart
107
- needed for path changes):
106
+ **3. Configure** `artcraft.config.json` (copy from
107
+ `artcraft.config.json.example`). Recommended location: `C:\Users\<you>\.dsh\artcraft.config.json`
108
+ (persists across plugin updates); placing it next to `index.js` also works (legacy). The file is
109
+ re-read on every request — no restart needed for path changes:
108
110
 
109
111
  ```jsonc
110
112
  {
111
- "dataRoot": "D:/craft-workspace", // where assets & renders live
112
113
  "bins": {
113
114
  // three accepted forms: a directory (the <name>-cli binary is appended),
114
115
  // a full path to the executable, or a bare name looked up on PATH
115
116
  "photocraft": "C:/craft/photocraft-0.6.0-windows-x64-portable",
116
117
  "filmcraft": "C:/craft/filmcraft-0.5.0-windows-x64-portable",
117
- "effectcraft": "C:/craft/effectcraft-windows-x64-portable"
118
+ "effectcraft": "C:/Program Files/EffectCraft"
118
119
  },
119
120
  "control": { "filmcraft": 9876, "effectcraft": 9877 }
120
121
  }
@@ -139,10 +140,10 @@ artcraft_cli { bin:"photocraft", args:["batch","--actions","demo/craft/grade.js
139
140
 
140
141
  | Tool | What it does |
141
142
  |---|---|
142
- | `artcraft_status` | Toolchain health check: which CLIs resolved, data root, control ports, running jobs |
143
+ | `artcraft_status` | Toolchain health check: which CLIs resolved, config location, control ports, running jobs |
143
144
  | `artcraft_cli` | Run any CLI subcommand synchronously (`photocraft-cli run/batch`, `filmcraft-cli commands`, …) |
144
145
  | `artcraft_control` | One command over the JSON control channel of a *running* FilmCraft / EffectCraft |
145
- | `artcraft_fs` | list / stat / read / write / mkdir inside `dataRoot` (writing `grade.json`, fetching a rendered `.ecproj`, …) |
146
+ | `artcraft_fs` | Bridged file ops — list / stat / read / write / mkdir at caller-decided absolute paths |
146
147
 
147
148
  ## Driving it from the browser
148
149
 
@@ -170,15 +171,16 @@ Full route list:
170
171
  | Route | Purpose |
171
172
  |---|---|
172
173
  | `GET /artcraft/api/status` | Toolchain health (same payload as `artcraft_status`) |
173
- | `GET /artcraft/api/projects` | Project directories under `dataRoot` |
174
- | `POST /artcraft/api/fs?action=write&rel=…` | Write a file — raw body (Blob upload) or JSON `{rel, text\|base64}` |
175
- | `GET /artcraft/api/fs?op=list\|stat\|readtext&rel=…` | Read directories, metadata, small text files |
176
- | `GET /artcraft/api/file?rel=…` | Download a file — streamed, up to 2 GB |
174
+ | `POST /artcraft/api/fs?action=write&path=…` | Write a file — absolute `path`, raw body (Blob upload) or JSON `{base, rel, text\|base64}` |
175
+ | `GET /artcraft/api/fs?op=list\|stat\|readtext&path=…` | Read directories, metadata, small text files |
176
+ | `GET /artcraft/api/file?path=…` | Download a file — streamed, up to 2 GB |
177
177
  | `POST /artcraft/api/job` · `GET ?id=…` · `POST /artcraft/api/job/cancel` | Job lifecycle with output statting and tail logs |
178
178
  | `POST /artcraft/api/control` | Synchronous control-channel call |
179
179
 
180
180
  Uploads are capped at 320 MB per request (Blob direct, no base64 needed for the raw path);
181
- downloads stream up to 2 GB. Path arguments are always relative to `dataRoot` and confined to it.
181
+ downloads stream up to 2 GB. All path arguments are **absolute** (or `base` absolute directory +
182
+ relative `rel`) — the caller decides where files live: chat calls point at the conversation
183
+ workspace, the canvas points at its project folder.
182
184
 
183
185
  ## The control channel: drive a running app
184
186
 
@@ -259,14 +261,15 @@ a web UI.
259
261
  loopback-only and unauthenticated, so nothing else will answer on that port.
260
262
  - **A job hangs** — `GET /artcraft/api/job` lists everything with logs; cancel by id. Plugin
261
263
  reloads kill all child jobs by design.
262
- - **Path escape errors** — by design: every filesystem argument must resolve inside `dataRoot`.
264
+ - **"需要绝对路径" errors** — pass absolute paths (chat: the conversation workspace; canvas: the
265
+ project folder), or a `base` absolute directory + relative `rel`.
263
266
 
264
267
  ## Security & trust model
265
268
 
266
269
  The HTTP surface has **no authentication** and rides on the Harness web server, which listens on
267
270
  loopback only — treat the port as trusted-local and don't expose it to a network; put a reverse
268
- proxy with auth in front if you must. Child processes are locked to the three allow-listed
269
- binaries; file access is locked to `dataRoot`; the control channel only ever dials `127.0.0.1`.
271
+ proxy with auth in front if you must. Child processes are locked to the seven allow-listed
272
+ family CLIs; the control channel only ever dials `127.0.0.1`.
270
273
 
271
274
  ## Publishing
272
275
 
@@ -281,10 +284,10 @@ GitHub: this repository is self-contained; `README.md` is English-first with a
281
284
  [简体中文](README.zh-CN.md) companion.
282
285
 
283
286
  **Releases run through npm Trusted Publishing** (GitHub Actions OIDC — no tokens, no OTP):
284
- `.github/workflows/publish.yml` fires on `v*` tags **on `main` only** (a guard step rejects tags
285
- from other branches); the trusted publisher on the npm package points at `NINKCH/dsh-artcraft` /
286
- `publish.yml`. Release flow and rules live in [RELEASING.md](RELEASING.md) — in short: changes
287
- land on `dev`, get tested there, and are only merged to `main` and tagged with the maintainer's
287
+ publishing is triggered by **creating a GitHub Release** (tag `vX.Y.Z` matching `package.json`);
288
+ `.github/workflows/publish.yml` verifies the release commit is on `main` and the versions align,
289
+ then publishes. Release flow and rules live in [RELEASING.md](RELEASING.md) — in short: changes
290
+ land on `dev`, get tested there, and are only merged to `main` and released with the maintainer's
288
291
  explicit approval.
289
292
 
290
293
  ## License
package/README.zh-CN.md CHANGED
@@ -24,12 +24,12 @@ Effects)等。它们有一个大多数桌面软件都不具备的共同基因
24
24
  本插件把这套自动化面接进 DeepSeek Harness,让 AI 不只是"谈论"这些软件,而是真的**操作**它们:
25
25
 
26
26
  - **对话里** —— 四个 Agent 工具,任何 Harness 会话都能检查工具链、执行 CLI 命令、向运行中的
27
- App 发控制通道命令、在专属数据目录里读写文件。
27
+ App 发控制通道命令、做桥接文件操作。
28
28
  - **浏览器里** —— 同源 HTTP API(`/artcraft/api/*`),任何 Harness 前端都能直接调用:无 CORS、
29
29
  无密钥。它为 [无限画布 / infinite-canvas](../dsh-infinite_canvas/README.md)(节点式 AI 短剧
30
30
  分镜画布)而建,但任何前端都可用。
31
- - **磁盘上** —— 每台机器一个 `dataRoot`,素材、工程文件、渲染产物按项目分区
32
- (`media/ · craft/ · out/`),生成式流水线与桌面应用在同一片地基上会合。
31
+ - **路径由调用方决定** —— 本插件只是桥接,不持有数据目录:对话调用默认落在当前对话工作空间
32
+ (或提示词指定的任意位置);画布调用则写在自己项目目录下。
33
33
 
34
34
  典型的 AI 短剧闭环:模型生成分镜图 → PhotoCraft 批量调色锐化 → EffectCraft 渲染动态片头 →
35
35
  FilmCraft 剪辑、调色、混音、烧字幕 → 导出 H.264。"生成"之后的每一步都由本插件无头驱动。
@@ -51,7 +51,7 @@ FilmCraft 剪辑、调色、混音、烧字幕 → 导出 H.264。"生成"之后
51
51
  (无头) 混音导出 Lottie 导出
52
52
  └───────────────────────┴───────────────────────┘
53
53
  ▼
54
- <dataRoot>/<项目>/{media, craft, out}
54
+ 产物位置由调用方决定(对话 → 当前工作空间 · 画布 → 项目目录)
55
55
  ```
56
56
 
57
57
  全流程本地运行:不联网、无账号、无遥测。
@@ -96,17 +96,17 @@ pnpm install --dir C:/Users/<你>/.dsh/profiles/desktop
96
96
 
97
97
  > 若你的 Harness 版本带有"从文件夹安装插件"的管理界面,用它也行 —— 最终落盘的就是上面两行。
98
98
 
99
- **3. 配置** `index.js` 旁边的 `artcraft.config.json`(从 `artcraft.config.json.example` 复制;
100
- 该文件已 gitignore,且每次请求都会重读 —— 改路径无需重启):
99
+ **3. 配置** `artcraft.config.json`(从 `artcraft.config.json.example` 复制)。推荐放
100
+ `C:\Users\<你>\.dsh\artcraft.config.json`(持久,更新插件不丢);放在 `index.js` 旁也可(旧版兼容)。
101
+ 文件已 gitignore,且每次请求都会重读 —— 改路径无需重启:
101
102
 
102
103
  ```jsonc
103
104
  {
104
- "dataRoot": "D:/craft-workspace", // 素材与产物的根目录
105
105
  "bins": {
106
106
  // 三种写法:目录(自动拼接 <名>-cli 可执行文件)、完整 exe 路径、或走 PATH 的裸命令名
107
107
  "photocraft": "C:/craft/photocraft-0.6.0-windows-x64-portable",
108
108
  "filmcraft": "C:/craft/filmcraft-0.5.0-windows-x64-portable",
109
- "effectcraft": "C:/craft/effectcraft-windows-x64-portable"
109
+ "effectcraft": "C:/Program Files/EffectCraft"
110
110
  },
111
111
  "control": { "filmcraft": 9876, "effectcraft": 9877 }
112
112
  }
@@ -129,10 +129,10 @@ artcraft_cli { bin:"photocraft", args:["batch","--actions","demo/craft/grade.js
129
129
 
130
130
  | 工具 | 用途 |
131
131
  |---|---|
132
- | `artcraft_status` | 工具体检:CLI 是否找到、数据目录、控制端口、进行中的任务 |
132
+ | `artcraft_status` | 工具体检:CLI 是否找到、配置目录、控制端口、进行中的任务 |
133
133
  | `artcraft_cli` | 同步执行任意 CLI 子命令(`photocraft-cli run/batch`、`filmcraft-cli commands` 等) |
134
134
  | `artcraft_control` | 向**运行中**的 FilmCraft / EffectCraft 控制通道发一条命令 |
135
- | `artcraft_fs` | 在 `dataRoot` 内 list / stat / 读 / 写 / 建目录(写 `grade.json`、取回 `.ecproj` 等) |
135
+ | `artcraft_fs` | 桥接文件操作 —— list / stat / 读 / 写 / 建目录,路径由调用方绝对指定 |
136
136
 
137
137
  ## 在浏览器里驱动
138
138
 
@@ -159,15 +159,15 @@ POST /artcraft/api/job/cancel { "id":"7f2c…" }
159
159
  | 路由 | 用途 |
160
160
  |---|---|
161
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 |
162
+ | `POST /artcraft/api/fs?action=write&path=…` | 写文件 —— 绝对 `path`,原始字节流(Blob 直传)或 JSON `{base, rel, text\|base64}` |
163
+ | `GET /artcraft/api/fs?op=list\|stat\|readtext&path=…` | 读目录、元数据、小文本 |
164
+ | `GET /artcraft/api/file?path=…` | 下载文件 —— 流式,上限 2 GB |
166
165
  | `POST /artcraft/api/job` · `GET ?id=…` · `POST /artcraft/api/job/cancel` | 任务生命周期,含产物探测与日志尾部 |
167
166
  | `POST /artcraft/api/control` | 控制通道同步调用 |
168
167
 
169
168
  上传单次上限 320 MB(原始路径 Blob 直传,无需 base64);下载流式上限 2 GB。
170
- 所有路径参数相对 `dataRoot` 且被限制在其中。
169
+ 所有路径参数为**绝对路径**(或 `base` 绝对目录 + 相对 `rel`)—— 位置由调用方决定:
170
+ 对话调用指向当前工作空间,画布调用指向其项目目录。
171
171
 
172
172
  ## 控制通道:驱动正在运行的 App
173
173
 
@@ -244,13 +244,14 @@ NVENC 硬编)、ProRes、DNxHR,附 EBU R128 响度归一与字幕烧录。
244
244
  不会有别的应答者。
245
245
  - **任务卡住** —— `GET /artcraft/api/job` 带日志列出全部任务,按 id 取消;插件重载会按设计
246
246
  终止所有子任务。
247
- - **路径越界报错** —— 设计如此:所有文件参数必须落在 `dataRoot` 之内。
247
+ - **"需要绝对路径"报错** —— 传绝对路径即可:对话用当前工作空间下的路径,画布用项目目录下的
248
+ 路径(或 `base` 绝对目录 + 相对 `rel`)。
248
249
 
249
250
  ## 安全与信任模型
250
251
 
251
252
  HTTP 面无鉴权,挂在 Harness Web 服务之下(仅回环监听)—— 把端口当本机可信处理,**不要暴露
252
- 到网络**;确需远程请加带鉴权的反向代理。子进程锁定三个白名单可执行文件;文件访问锁定
253
- `dataRoot`;控制通道只会拨 `127.0.0.1`。
253
+ 到网络**;确需远程请加带鉴权的反向代理。子进程锁定 7 个白名单家族 CLI;控制通道只会拨
254
+ `127.0.0.1`。
254
255
 
255
256
  ## 发布
256
257
 
@@ -264,9 +265,9 @@ GitHub 上传本仓库即可:`README.md` 英文优先,附
264
265
  [简体中文](README.zh-CN.md) 对照。
265
266
 
266
267
  **发版走 npm Trusted Publishing**(GitHub Actions OIDC —— 无令牌、无 OTP):
267
- `.github/workflows/publish.yml` 仅在 **main 分支**的 `v*` 标签推送时触发(守卫步骤会拒绝其他分支
268
- 的标签);npm 包侧的可信发布指向 `NINKCH/dsh-artcraft` / `publish.yml`。发版流程与红线见
269
- [RELEASING.md](RELEASING.md):改动先进 `dev` 测试,维护者明确批准后才合并 main 并打标签。
268
+ 发布由 **GitHub Release** 触发(tag `vX.Y.Z` 与 package.json version 一致);`.github/workflows/publish.yml`
269
+ 会校验发布提交在 main 上且版本对齐,然后自动发布。发版流程与红线见
270
+ [RELEASING.md](RELEASING.md):改动先进 `dev` 测试,维护者明确批准后才合并 main 并发 Release。
270
271
 
271
272
  ## 许可
272
273
 
@@ -1,9 +1,12 @@
1
1
  {
2
- "dataRoot": "D:/craft-workspace",
3
2
  "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"
3
+ "photocraft": "D:/craft/photocraft-0.6.0/photocraft-0.6.0-windows-x64-portable",
4
+ "vectorcraft": "C:/Program Files/VectorCraft",
5
+ "filmcraft": "D:/craft/filmcraft-0.5.0/filmcraft-0.5.0-windows-x64-portable",
6
+ "lightcraft": "C:/Program Files/LightCraft",
7
+ "pdfcraft": "C:/Program Files/PdfCraft",
8
+ "effectcraft": "C:/Program Files/EffectCraft",
9
+ "designcraft": "C:/Program Files/DesignCraft"
7
10
  },
8
11
  "control": {
9
12
  "filmcraft": 9876,
package/client.js CHANGED
@@ -120,8 +120,8 @@ window.__ModuleLoader__.load({
120
120
  statusErr
121
121
  ? h('div', { className: 'dshac-muted' }, '未连接:' + statusErr + '(插件可能未注册或 Harness 需要重启)')
122
122
  : h('div', null, Object.keys(LABELS).map(binRow),
123
- status?.dataRoot ? h('div', { className: 'dshac-row dshac-muted' },
124
- '数据目录:', h('span', { className: 'dshac-mono' }, status.dataRoot.path)) : null),
123
+ status?.config?.path ? h('div', { className: 'dshac-row dshac-muted' },
124
+ '配置目录:', h('span', { className: 'dshac-mono' }, status.config.path)) : null),
125
125
  h('div', { className: 'dshac-row', style: { marginTop: '10px' } },
126
126
  h('span', { className: 'dshac-muted' },
127
127
  '当前 v' + (status?.version ?? upd?.current ?? '?') +
package/index.js CHANGED
@@ -1,15 +1,20 @@
1
1
  /**
2
2
  * @ninkch/dsh-artcraft — DeepSeek Harness 插件(Host 半侧)
3
3
  *
4
- * 把 ArtCraft 家族的自动化接口封装成两种形态:
4
+ * 本插件只是【桥接】:把 ArtCraft 家族软件的自动化接口转接给 Harness,不持有任何数据目录。
5
+ * 结果文件的位置由调用方决定 ——
6
+ * 对话调用:当前对话工作空间下的绝对路径(或提示词指定的任意位置)
7
+ * 画布调用:画布项目目录下的绝对路径(哪个项目使用就放哪个项目下)
8
+ *
9
+ * 两种形态:
5
10
  * 1) Agent Tools(对话内直接驱动):
6
- * artcraft_status —— 检查三个 CLI 是否可用、数据目录、控制通道配置
7
- * artcraft_cli —— 运行 photocraft-cli / filmcraft-cli / effectcraft-cli 任意命令
11
+ * artcraft_status —— 检查 7 个家族 CLI 是否可用、配置目录、控制通道配置
12
+ * artcraft_cli —— 运行 7 个家族 CLI 任意命令(cwd / 文件参数由调用方给绝对路径)
8
13
  * artcraft_control —— 向运行中的 FilmCraft/EffectCraft JSON 控制通道发一条命令
9
- * artcraft_fs —— 在 dataRoot 内读写文件(准备 actions.json / 取回产物)
14
+ * artcraft_fs —— 桥接文件操作(list/stat/readtext/write/mkdir,绝对路径)
15
+ * artcraft_update —— 插件自身更新(检查/应用)
10
16
  * 2) HTTP API(同源 /artcraft/api/*,供无限画布等前端调用,无 CORS 问题):
11
17
  * GET /artcraft/api/status 环境自检
12
- * GET /artcraft/api/projects 列出 dataRoot 下的项目目录
13
18
  * GET /artcraft/api/fs list / stat / readtext
14
19
  * POST /artcraft/api/fs write(JSON base64/text 或原始字节流)
15
20
  * GET /artcraft/api/file 下载产物(流式,raw 或 text)
@@ -19,12 +24,10 @@
19
24
  * POST /artcraft/api/control 控制通道同步调用(engine.execute / ui.*)
20
25
  *
21
26
  * 安全约定:
22
- * - 所有文件操作限制在 dataRoot 内(拒绝 .. 越界与绝对路径外逃);
23
- * - 子进程只允许三个白名单键(photocraft/filmcraft/effectcraft),
24
- * 具体可执行文件由 artcraft.config.json 指定;
27
+ * - 子进程只允许 7 个白名单键(家族 CLI),具体可执行文件由 artcraft.config.json 指定;
25
28
  * - 控制通道只连 127.0.0.1,与 ArtCraft 官方协议一致(App 需以 --control 启动;
26
29
  * PhotoCraft 的控制通道为鉴权模式,本插件不代理,故仅支持 filmcraft/effectcraft);
27
- * - 本插件与 infinite-canvas 零代码耦合,仅通过 HTTP + 磁盘目录约定协作。
30
+ * - 本插件与 infinite-canvas 零代码耦合,仅通过 HTTP + 调用方给定路径协作。
28
31
  *
29
32
  * 信任模型:HTTP API 无鉴权,随 DSH Web GUI 仅监听回环;不要把 GUI 端口暴露到局域网。
30
33
  */
@@ -39,7 +42,7 @@ import crypto from 'node:crypto';
39
42
  import { fileURLToPath } from 'node:url';
40
43
 
41
44
  const NAME = 'artcraft';
42
- const VERSION = '0.2.6';
45
+ const VERSION = '0.2.7';
43
46
  const HERE = path.dirname(fileURLToPath(import.meta.url));
44
47
 
45
48
  const BINS = ['photocraft', 'vectorcraft', 'filmcraft', 'lightcraft', 'pdfcraft', 'effectcraft', 'designcraft'];
@@ -55,7 +58,6 @@ const LINE_CAP = 4000; // 单行截断
55
58
  const KEEP_JOBS = 64; // 已结束任务保留条数
56
59
 
57
60
  const DEFAULT_CONFIG = {
58
- dataRoot: path.join(os.homedir(), 'craft-workspace'),
59
61
  bins: {
60
62
  photocraft: 'photocraft-cli',
61
63
  vectorcraft: 'vectorcraft-cli',
@@ -72,9 +74,9 @@ const DEFAULT_CONFIG = {
72
74
 
73
75
  async function loadConfig() {
74
76
  const cfg = {
75
- dataRoot: DEFAULT_CONFIG.dataRoot,
76
77
  bins: { ...DEFAULT_CONFIG.bins },
77
78
  control: { ...DEFAULT_PORTS },
79
+ configPath: null, // 实际生效的配置文件(供界面显示"配置目录")
78
80
  };
79
81
  // 配置查找顺序:~/.dsh(持久,推荐 —— 更新替换包目录也不丢)→ 包目录(旧版兼容)
80
82
  const candidates = [
@@ -84,7 +86,6 @@ async function loadConfig() {
84
86
  for (const candidate of candidates) {
85
87
  try {
86
88
  const user = JSON.parse(await fs.readFile(candidate, 'utf8'));
87
- if (user && typeof user.dataRoot === 'string' && user.dataRoot) cfg.dataRoot = user.dataRoot;
88
89
  if (user && user.bins) {
89
90
  for (const k of BINS) {
90
91
  if (typeof user.bins[k] === 'string' && user.bins[k]) cfg.bins[k] = user.bins[k];
@@ -96,6 +97,7 @@ async function loadConfig() {
96
97
  if (Number.isInteger(p) && p > 0 && p < 65536) cfg.control[k] = p;
97
98
  }
98
99
  }
100
+ cfg.configPath = candidate;
99
101
  break; // 第一个可解析的配置生效
100
102
  } catch { /* 该候选不存在/非法 → 试下一个 */ }
101
103
  }
@@ -104,20 +106,36 @@ async function loadConfig() {
104
106
 
105
107
  /* ============================ 路径与进程 ============================ */
106
108
 
107
- /** 把 rel 解析为 dataRoot 内的绝对路径,拒绝越界 */
108
- function resolveSafeRel(dataRoot, rel) {
109
- const root = path.resolve(String(dataRoot));
110
- const p = path.resolve(root, String(rel ?? '.'));
111
- if (p !== root && !p.startsWith(root + path.sep)) {
112
- throw new Error(`路径越界(必须在数据目录内): ${rel}`);
109
+ /**
110
+ * 桥接语义:路径由调用方决定(本插件只是桥梁,不设数据目录)——
111
+ * 对话调用:当前对话工作空间下的绝对路径(或提示词指定的任意位置)
112
+ * 画布调用:画布项目目录下的绝对路径
113
+ * 支持 ~ 展开;也接受 base(绝对目录) + rel(相对) 组合,方便批量调用。
114
+ */
115
+ function resolveCallerPath(p) {
116
+ let s = String(p ?? '').trim();
117
+ if (!s) throw new Error('缺少路径参数');
118
+ if (s === '~' || s.startsWith('~/') || s.startsWith('~\\')) s = path.join(os.homedir(), s.slice(1));
119
+ const abs = path.resolve(s);
120
+ if (!path.isAbsolute(abs)) {
121
+ throw new Error('需要绝对路径 —— 对话调用传当前工作空间下的绝对路径,画布传项目目录下的绝对路径(或用 base + 相对路径组合)');
122
+ }
123
+ return abs;
124
+ }
125
+
126
+ function resolveBaseRel(base, rel) {
127
+ if (base) {
128
+ const b = resolveCallerPath(base);
129
+ if (rel == null || String(rel) === '') return b;
130
+ return path.resolve(b, String(rel));
113
131
  }
114
- return p;
132
+ return resolveCallerPath(rel);
115
133
  }
116
134
 
117
- async function ensureDirSafe(cfg, rel) {
118
- const p = resolveSafeRel(cfg.dataRoot, rel ?? '.');
119
- await fs.mkdir(p, { recursive: true });
120
- return p;
135
+ async function ensureDir(p) {
136
+ const abs = resolveCallerPath(p);
137
+ await fs.mkdir(abs, { recursive: true });
138
+ return abs;
121
139
  }
122
140
 
123
141
  function cliNameFor(key) {
@@ -266,15 +284,27 @@ function controlCall(port, method, params, { timeoutMs = 30_000 } = {}) {
266
284
 
267
285
  const jobs = new Map(); // id -> job
268
286
 
269
- async function statOutputs(cfg, outputs) {
287
+ /**
288
+ * 桥接语义:产物路径由调用方给定 ——
289
+ * 绝对路径直接使用;也可传 cwd(绝对目录)让 outputs 以它为基准。
290
+ */
291
+ async function statOutputs(outputs, cwd) {
270
292
  const out = [];
293
+ const base = cwd ? resolveCallerPath(cwd) : null;
271
294
  for (const item of outputs ?? []) {
272
- const rel = String(item.rel ?? item);
295
+ const spec = String(item.rel ?? item.path ?? item);
296
+ let abs;
297
+ try {
298
+ abs = base ? path.resolve(base, spec) : resolveCallerPath(spec);
299
+ } catch {
300
+ out.push({ rel: spec, exists: false, dir: false, size: null, mtime: null });
301
+ continue;
302
+ }
273
303
  try {
274
- const st = await fs.stat(resolveSafeRel(cfg.dataRoot, rel));
275
- out.push({ rel, exists: true, dir: st.isDirectory(), size: st.size, mtime: st.mtimeMs });
304
+ const st = await fs.stat(abs);
305
+ out.push({ rel: spec, path: abs, exists: true, dir: st.isDirectory(), size: st.size, mtime: st.mtimeMs });
276
306
  } catch {
277
- out.push({ rel, exists: false, dir: false, size: null, mtime: null });
307
+ out.push({ rel: spec, path: abs, exists: false, dir: false, size: null, mtime: null });
278
308
  }
279
309
  }
280
310
  return out;
@@ -309,18 +339,19 @@ function pruneJobs() {
309
339
  }
310
340
  }
311
341
 
312
- async function startJob(cfg, { bin, args, cwdRel, outputs, timeoutMs }) {
342
+ async function startJob(cfg, { bin, args, cwd, outputs, timeoutMs }) {
313
343
  if (!BINS.includes(bin)) throw new Error(`bin 必须是 ${BINS.join(' | ')} 之一`);
314
344
  if (args != null && !Array.isArray(args)) throw new Error('args 必须是字符串数组');
315
345
  const exe = await resolveBin(cfg, bin);
316
- const cwd = await ensureDirSafe(cfg, cwdRel ?? '.');
346
+ // 桥接语义:cwd 由调用方给绝对路径(对话=工作空间下的目录,画布=项目目录);不传则继承 Host 进程
347
+ const cwdAbs = cwd ? await ensureDir(cwd) : null;
317
348
  const id = crypto.randomUUID();
318
349
  const job = {
319
350
  id,
320
351
  bin,
321
352
  exe,
322
353
  args: (args ?? []).map(String),
323
- cwd,
354
+ cwd: cwdAbs,
324
355
  status: 'running',
325
356
  code: null,
326
357
  signal: null,
@@ -328,11 +359,11 @@ async function startJob(cfg, { bin, args, cwdRel, outputs, timeoutMs }) {
328
359
  endedAt: null,
329
360
  stdout: [],
330
361
  stderr: [],
331
- outputs: (outputs ?? []).map((rel) => ({ rel: String(rel) })),
362
+ outputs: (outputs ?? []).map((o) => ({ rel: String(o.rel ?? o.path ?? o) })),
332
363
  child: null,
333
364
  timer: null,
334
365
  };
335
- const child = spawn(job.exe, job.args, { cwd, windowsHide: true });
366
+ const child = spawn(job.exe, job.args, { cwd: cwdAbs ?? undefined, windowsHide: true });
336
367
  job.child = child;
337
368
  child.stdout?.setEncoding('utf8');
338
369
  child.stderr?.setEncoding('utf8');
@@ -384,13 +415,13 @@ async function statusInfo(cfg, meta) {
384
415
  }
385
416
  bins[key] = { configured: cfg.bins[key], exe, found, note };
386
417
  }
387
- let dataRoot = { path: cfg.dataRoot, exists: false };
388
- try { dataRoot.exists = (await fs.stat(cfg.dataRoot)).isDirectory(); } catch { /* 不存在 */ }
418
+ let configPath = null;
419
+ try { configPath = cfg.configPath && (await fs.stat(cfg.configPath)).isFile() ? cfg.configPath : null; } catch { /* 不存在 */ }
389
420
  return {
390
421
  plugin: NAME,
391
422
  version: VERSION,
392
423
  platform: process.platform,
393
- dataRoot,
424
+ config: { path: configPath, found: !!configPath },
394
425
  bins,
395
426
  control: cfg.control,
396
427
  jobs: {
@@ -632,7 +663,7 @@ export function apply(ctx) {
632
663
 
633
664
  attempt({
634
665
  name: 'artcraft_status',
635
- description: '检查 ArtCraft 工具链(photocraft/filmcraft/effectcraft CLI)是否可用,报告数据目录、控制通道端口与任务状态。',
666
+ description: '检查 ArtCraft 家族 7 个 CLI 是否可用,报告配置目录、控制通道端口与任务状态。本插件只是桥接,产物路径由调用方决定。',
636
667
  schema: { type: 'object', properties: {}, additionalProperties: false },
637
668
  async execute() {
638
669
  const cfg = await loadConfig();
@@ -648,7 +679,7 @@ export function apply(ctx) {
648
679
  properties: {
649
680
  bin: { type: 'string', enum: [...BINS], description: '工具键' },
650
681
  args: { type: 'array', items: { type: 'string' }, description: '命令行参数(不含程序名)' },
651
- cwdRel: { type: 'string', description: '工作目录,相对 dataRoot' },
682
+ cwd: { type: 'string', description: '工作目录(绝对路径,可选):对话=当前工作空间下的目录,画布=项目目录;不传则继承 Host 进程' },
652
683
  timeoutMs: { type: 'number', description: '超时毫秒,默认 900000' },
653
684
  },
654
685
  required: ['bin', 'args'],
@@ -658,7 +689,7 @@ export function apply(ctx) {
658
689
  const cfg = await loadConfig();
659
690
  if (args.args != null && !Array.isArray(args.args)) throw new Error('args 必须是字符串数组');
660
691
  const exe = await resolveBin(cfg, String(args.bin));
661
- const cwd = await ensureDirSafe(cfg, args.cwdRel ?? '.');
692
+ const cwd = args.cwd ? await ensureDir(args.cwd) : null;
662
693
  const r = await runProcess(exe, args.args ?? [], { cwd, timeoutMs: args.timeoutMs });
663
694
  return { bin: args.bin, exe, cwd, ...r };
664
695
  },
@@ -689,56 +720,57 @@ export function apply(ctx) {
689
720
 
690
721
  attempt({
691
722
  name: 'artcraft_fs',
692
- description: '在 craft 数据目录(dataRoot)内进行文件操作:list / stat / readtext / write / mkdir。所有路径相对 dataRoot,禁止越界。',
723
+ description: '桥接文件操作:list / stat / readtext / write / mkdir。路径由调用方决定 —— 对话调用传当前工作空间下的绝对路径,画布调用传项目目录下的绝对路径(或 base + rel 组合)。',
693
724
  schema: {
694
725
  type: 'object',
695
726
  properties: {
696
727
  op: { type: 'string', enum: ['list', 'stat', 'readtext', 'write', 'mkdir'] },
697
- rel: { type: 'string', description: '相对 dataRoot 的路径' },
728
+ path: { type: 'string', description: '绝对路径(或 ~/ 展开)' },
729
+ base: { type: 'string', description: '可选:绝对基准目录,rel 相对它解析' },
730
+ rel: { type: 'string', description: '可选:相对 base 的路径(需同时给 base)' },
698
731
  text: { type: 'string', description: 'op=write 时的文本内容' },
699
732
  base64: { type: 'string', description: 'op=write 时的二进制内容(base64)' },
700
733
  },
701
- required: ['op', 'rel'],
734
+ required: ['op'],
702
735
  additionalProperties: false,
703
736
  },
704
737
  async execute(args) {
705
- const cfg = await loadConfig();
706
- const rel = String(args.rel ?? '');
707
738
  const op = String(args.op);
739
+ const P = () => resolveBaseRel(args.base, args.path ?? args.rel);
708
740
  if (op === 'list') {
709
- const dir = resolveSafeRel(cfg.dataRoot, rel || '.');
741
+ const dir = P();
710
742
  const entries = await fs.readdir(dir, { withFileTypes: true });
711
743
  const out = [];
712
744
  for (const ent of entries) {
713
745
  const st = await fs.stat(path.join(dir, ent.name)).catch(() => null);
714
746
  out.push({ name: ent.name, dir: ent.isDirectory(), size: st?.size ?? null, mtime: st?.mtimeMs ?? null });
715
747
  }
716
- return { rel, entries: out };
748
+ return { path: dir, entries: out };
717
749
  }
718
750
  if (op === 'stat') {
719
- const st = await fs.stat(resolveSafeRel(cfg.dataRoot, rel));
720
- return { rel, size: st.size, mtime: st.mtimeMs, dir: st.isDirectory() };
751
+ const st = await fs.stat(P());
752
+ return { path: P(), size: st.size, mtime: st.mtimeMs, dir: st.isDirectory() };
721
753
  }
722
754
  if (op === 'readtext') {
723
- const p = resolveSafeRel(cfg.dataRoot, rel);
755
+ const p = P();
724
756
  const st = await fs.stat(p);
725
757
  if (st.size > TEXT_CAP) throw new Error('文件超过 16MB,请用 GET /artcraft/api/file 取回');
726
758
  const buf = await fs.readFile(p);
727
- return { rel, text: buf.toString('utf8') };
759
+ return { path: p, text: buf.toString('utf8') };
728
760
  }
729
761
  if (op === 'mkdir') {
730
- const p = await ensureDirSafe(cfg, rel);
731
- return { rel, path: p };
762
+ const p = await ensureDir(P());
763
+ return { path: p };
732
764
  }
733
765
  if (op === 'write') {
734
- const p = resolveSafeRel(cfg.dataRoot, rel);
766
+ const p = P();
735
767
  await fs.mkdir(path.dirname(p), { recursive: true });
736
768
  let data;
737
769
  if (typeof args.base64 === 'string') data = Buffer.from(args.base64, 'base64');
738
770
  else if (typeof args.text === 'string') data = Buffer.from(args.text, 'utf8');
739
771
  else throw new Error('write 需要 text 或 base64');
740
772
  await fs.writeFile(p, data);
741
- return { rel, size: data.length };
773
+ return { path: p, size: data.length };
742
774
  }
743
775
  throw new Error(`未知 op: ${op}`);
744
776
  },
@@ -746,7 +778,7 @@ export function apply(ctx) {
746
778
 
747
779
  attempt({
748
780
  name: 'artcraft_update',
749
- description: '检查/应用 @ninkch/dsh-artcraft 插件自身的更新:action=check 对比本地与 npm latest;action=apply 执行更新(git pull 或 pnpm add,按安装形态自动选择),完成后需重启 Harness 生效。',
781
+ description: '检查/应用 @ninkch/dsh-artcraft 插件自身的更新:action=check 对比本地与 npm latest;action=apply 执行更新(git pull 或官方 tarball 直装 + sha512 校验,按安装形态自动选择),完成后需重启 Harness 生效。',
750
782
  schema: {
751
783
  type: 'object',
752
784
  properties: {
@@ -774,48 +806,34 @@ export function apply(ctx) {
774
806
  sendJson(res, 200, await statusInfo(cfg, { registered: [...toolNames], errors: [...toolErrors] }));
775
807
  }),
776
808
 
777
- makeRoute(`/${NAME}/api/projects`, async (req, res) => {
778
- const cfg = await loadConfig();
779
- await fs.mkdir(cfg.dataRoot, { recursive: true });
780
- const entries = await fs.readdir(cfg.dataRoot, { withFileTypes: true });
781
- const projects = [];
782
- for (const ent of entries) {
783
- if (!ent.isDirectory()) continue;
784
- const st = await fs.stat(path.join(cfg.dataRoot, ent.name)).catch(() => null);
785
- projects.push({ name: ent.name, mtime: st?.mtimeMs ?? null });
786
- }
787
- projects.sort((a, b) => (b.mtime ?? 0) - (a.mtime ?? 0));
788
- sendJson(res, 200, { dataRoot: cfg.dataRoot, projects });
789
- }),
790
-
791
809
  makeRoute(`/${NAME}/api/fs`, async (req, res) => {
792
- const cfg = await loadConfig();
793
810
  const url = new URL(req.url, 'http://x');
811
+ const base = url.searchParams.get('base'); // 可选:绝对基准目录(画布=项目目录,对话=工作空间下目录)
812
+ const P = () => resolveBaseRel(base, url.searchParams.get('path') ?? url.searchParams.get('rel'));
794
813
  if (req.method === 'GET') {
795
814
  const op = url.searchParams.get('op') ?? 'list';
796
- const rel = url.searchParams.get('rel') ?? '.';
797
815
  if (op === 'list') {
798
- const dir = resolveSafeRel(cfg.dataRoot, rel);
816
+ const dir = P();
799
817
  const entries = await fs.readdir(dir, { withFileTypes: true });
800
818
  const out = [];
801
819
  for (const ent of entries) {
802
820
  const st = await fs.stat(path.join(dir, ent.name)).catch(() => null);
803
821
  out.push({ name: ent.name, dir: ent.isDirectory(), size: st?.size ?? null, mtime: st?.mtimeMs ?? null });
804
822
  }
805
- sendJson(res, 200, { rel, entries: out });
823
+ sendJson(res, 200, { path: dir, entries: out });
806
824
  return;
807
825
  }
808
826
  if (op === 'stat') {
809
- const st = await fs.stat(resolveSafeRel(cfg.dataRoot, rel));
810
- sendJson(res, 200, { rel, size: st.size, mtime: st.mtimeMs, dir: st.isDirectory() });
827
+ const st = await fs.stat(P());
828
+ sendJson(res, 200, { path: P(), size: st.size, mtime: st.mtimeMs, dir: st.isDirectory() });
811
829
  return;
812
830
  }
813
831
  if (op === 'readtext') {
814
- const p = resolveSafeRel(cfg.dataRoot, rel);
832
+ const p = P();
815
833
  const st = await fs.stat(p);
816
834
  if (st.size > TEXT_CAP) throw new Error('文件超过 16MB,请改用 GET /artcraft/api/file');
817
835
  const buf = await fs.readFile(p);
818
- sendJson(res, 200, { rel, text: buf.toString('utf8') });
836
+ sendJson(res, 200, { path: p, text: buf.toString('utf8') });
819
837
  return;
820
838
  }
821
839
  throw new Error(`未知 op: ${op}`);
@@ -824,47 +842,43 @@ export function apply(ctx) {
824
842
  const action = url.searchParams.get('action') ?? 'write';
825
843
  if (action === 'mkdir') {
826
844
  const body = await readJson(req);
827
- const p = await ensureDirSafe(cfg, body.rel ?? url.searchParams.get('rel'));
845
+ const p = await ensureDir(resolveBaseRel(body.base ?? url.searchParams.get('base'), body.path ?? body.rel ?? url.searchParams.get('rel')));
828
846
  sendJson(res, 200, { ok: true, path: p });
829
847
  return;
830
848
  }
831
- // write:JSON {rel, base64|text} 或原始字节流(rel 走查询串)
849
+ // write:JSON {path 或 base+rel, base64|text} 或原始字节流(路径走查询串)
832
850
  const isJson = String(req.headers['content-type'] ?? '').includes('json');
833
- let rel;
851
+ let target;
834
852
  let data;
835
853
  if (isJson) {
836
854
  const body = await readJson(req, WRITE_CAP);
837
- rel = String(body.rel ?? url.searchParams.get('rel') ?? '');
838
855
  if (typeof body.base64 === 'string') data = Buffer.from(body.base64.replace(/\s+/g, ''), 'base64');
839
856
  else if (typeof body.text === 'string') data = Buffer.from(body.text, 'utf8');
840
857
  else throw new Error('write 需要 base64 或 text 字段');
858
+ target = resolveBaseRel(body.base ?? url.searchParams.get('base'), body.path ?? body.rel ?? url.searchParams.get('rel'));
841
859
  } else {
842
- rel = String(url.searchParams.get('rel') ?? '');
843
860
  data = await readBody(req, WRITE_CAP);
861
+ target = resolveBaseRel(url.searchParams.get('base'), url.searchParams.get('path') ?? url.searchParams.get('rel'));
844
862
  }
845
- if (!rel) throw new Error('缺少 rel');
846
- const p = resolveSafeRel(cfg.dataRoot, rel);
847
- await fs.mkdir(path.dirname(p), { recursive: true });
848
- await fs.writeFile(p, data);
849
- sendJson(res, 200, { ok: true, rel, size: data.length });
863
+ await fs.mkdir(path.dirname(target), { recursive: true });
864
+ await fs.writeFile(target, data);
865
+ sendJson(res, 200, { ok: true, path: target, size: data.length });
850
866
  return;
851
867
  }
852
868
  throw new Error('仅支持 GET/POST');
853
869
  }),
854
870
 
855
871
  makeRoute(`/${NAME}/api/file`, async (req, res) => {
856
- const cfg = await loadConfig();
857
872
  if (req.method !== 'GET') throw new Error('仅支持 GET');
858
873
  const url = new URL(req.url, 'http://x');
859
- const rel = String(url.searchParams.get('rel') ?? '');
860
- const p = resolveSafeRel(cfg.dataRoot, rel);
874
+ const p = resolveBaseRel(url.searchParams.get('base'), url.searchParams.get('path') ?? url.searchParams.get('rel'));
861
875
  const st = await fs.stat(p);
862
876
  if (!st.isFile()) throw new Error('不是常规文件');
863
877
  if (st.size > READ_CAP) throw new Error(`文件超过读取上限(${Math.round(READ_CAP / 1048576)}MB)`);
864
878
  if (url.searchParams.get('as') === 'text') {
865
879
  if (st.size > TEXT_CAP) throw new Error('文本模式上限 16MB,请直接下载原始文件');
866
880
  const buf = await fs.readFile(p);
867
- sendJson(res, 200, { rel, text: buf.toString('utf8') });
881
+ sendJson(res, 200, { path: p, text: buf.toString('utf8') });
868
882
  return;
869
883
  }
870
884
  // 流式传输:产物可能是几百 MB 的视频,不整读进内存
@@ -882,13 +896,13 @@ export function apply(ctx) {
882
896
  if (id) {
883
897
  const job = jobs.get(id);
884
898
  if (!job) throw new Error('任务不存在或已被回收');
885
- job.outputs = await statOutputs(cfg, job.outputs);
899
+ job.outputs = await statOutputs(job.outputs, job.cwd);
886
900
  sendJson(res, 200, jobSnapshot(job, tail));
887
901
  return;
888
902
  }
889
903
  const list = [];
890
904
  for (const job of [...jobs.values()].reverse()) {
891
- job.outputs = await statOutputs(cfg, job.outputs);
905
+ job.outputs = await statOutputs(job.outputs, job.cwd);
892
906
  list.push(jobSnapshot(job, 5));
893
907
  }
894
908
  sendJson(res, 200, { jobs: list });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ninkch/dsh-artcraft",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "type": "module",
5
5
  "description": "Drive the open-source ArtCraft studio apps (PhotoCraft / FilmCraft / EffectCraft) from DeepSeek Harness ? agent tools, a same-origin HTTP API, and plugins-page status with self-update.",
6
6
  "keywords": [