dsh-read-animation 0.1.0 → 0.1.2

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/CHANGELOG.md CHANGED
@@ -1,103 +1,169 @@
1
- # Changelog
2
-
3
- ## 0.1.0 — unreleased
4
-
5
- Initial version.
6
-
7
- ### Fixed after an adversarial audit
8
-
9
- Four independent reviews (blast radius, plugin code, packaging and lifecycle, then
10
- an adversarial pass over every high-severity finding) ran before this was
11
- published. What they found, and what changed:
12
-
13
- - **The frame sheet's `variantId` was not a content digest.** It was a hand-rolled
14
- `sha256:filmanim…` string, and the DeepSeek adapter validates persisted ids
15
- against `/^sha256:[0-9a-f]{64}$/` when it loads its upload index — then swallows
16
- the failure into an *empty* index. The effect was not a visible error: it
17
- discarded every other session's cached file id and re-uploaded every image.
18
- Reproduced against the real exported index class (plugin id: written 1, reloaded
19
- 0; conforming id: 1/1). It is now the SHA-256 of the bytes being returned, which
20
- also stops one on-disk cache entry from aliasing two different byte streams.
21
- - **The degraded path was worse than the missing feature.** The store reaches the
22
- frame it re-encodes through its own `readImage` call, which this plugin wraps, so
23
- falling back to the store's request read could hand the model the *whole
24
- animation* where it expects one frame. The request path now suspends the
25
- animation substitution around that call, and reads its own tiles through the
26
- unwrapped read. Pinned by an integration check that breaks a stored frame and
27
- asserts the fallback is exactly one frame.
28
- - **No decoded-pixel ceiling on the `read_animation` path.** `limitInputPixels`
29
- was disabled with nothing in its place: a 60 KB file declaring a 9000x9000
30
- canvas was refused by the deployment's own attachment store and decoded here
31
- anyway, at about 310 MiB. Both the probe and the frame extraction now carry the
32
- deployment's `imageLimits.maxImagePixels`.
33
- - **The sheet cache ignored the size the caller asked for**, so a caller with an
34
- 8 KiB budget could be served a 28 KB sheet built for a 4 MiB one. The target is
35
- now part of the cache key, and a tile is never larger than a single frame would
36
- have been for that caller.
37
- - **The measured header wrap could still be defeated.** A floor of 16 characters
38
- kept the loop from reaching a budget that fit, so on a narrow canvas it gave up
39
- and returned ink wider than the sheet. The floor is now low enough to be
40
- satisfiable, and a canvas too narrow for legible text gets no header at all.
41
- - **The canvas was composed before it was shrunk**, building a 6156x6306 (about
42
- 111 MiB) image to throw away. Tile size and grid are settled first.
43
- - **Torn state files could poison the next record.** A crash part way through an
44
- append left a line with no newline, so the following record concatenated onto it
45
- and became unreadable too. A torn tail is now detected and compacted away.
46
- - **The store's bound applied to memory but not to what was read back**, and the
47
- log was never compacted. Both fixed; the file is rewritten when it grows past
48
- twice the entries worth keeping.
49
- - **Teardown was last-writer-wins.** Each install captured whatever was on the
50
- method, so disposing one left another's patch removed and could reinstall a
51
- disposed closure as a permanent own property. Three wrappers now share one
52
- installer that resolves its delegate per call, removes cleanly (deleting an
53
- inherited property rather than leaving a shadow behind), and reports whether it
54
- actually took effect — the previous identity check compared a value that proxy
55
- reads always re-wrap, so it could never detect a failed install.
56
- - **`npm test` did not run the invariant checks.** The no-`@deepseek-ai`-import
57
- rule and the bundle/manifest consistency check lived only in the integration
58
- harness, which prints SKIP and exits 0 without `DSH_HARNESS_MODULES`. Both now
59
- run in `npm test`, which is also the `prepublishOnly` gate.
60
- - **`files[]` omitted `scripts/`** while `package.json` advertised two npm scripts
61
- that live there, so the published package's own scripts were broken.
62
-
63
- - **A pasted animation keeps playing in the UI, and the message is left alone.**
64
- The browser already uploads an animated GIF/WebP byte-for-byte and labelled as
65
- such; the Host flattens it to a single
66
- frame during prompt admission and keeps no copy of the source. The plugin wraps
67
- `ctx.attachments.admitPromptContent` to capture the frames and the original file
68
- at that moment, and `ctx.attachments.readImage` so the UI is served the animation
69
- itself (the browser animates whatever bytes that one read returns). What you send
70
- is what you send: the chat message is unchanged.
71
- - **The model now receives a labelled sheet of the frames.** The delivery step is
72
- `ctx.attachments.readImageRequest(ref, target, signal)`, the method each adapter
73
- calls to build its own copy of one image. It answers with exactly one image, so
74
- the frames are composed into a grid chosen to be as square as the sheet budget
75
- allows — every tile at its native size, with the frame index, duration and start
76
- time printed under each, and the animation's own facts in a wrapped header. The
77
- sheet is built once per animation and cached.
78
- - Rewriting the request from an `llm/stream` listener is **not** used and would not
79
- work: a loop-built request arrives deep-frozen ("listeners read it, never rewrite
80
- it"), so that earlier attempt silently did nothing.
81
- - Captured frames and the original are persisted per `$DSH_HOME` in
82
- `read-animation/v1/pasted.jsonl`, keyed by the message's attachment id, so a
83
- session resumed in a new process still displays — and still delivers — an
84
- animation pasted before the restart.
85
- - A frame's index and start time ride in its attachment display name
86
- (`x.gif#frame16@1960ms`), which the harness renders beside the picture once the
87
- frames are delivered — so the timing never has to be repeated as message text.
88
- (An earlier version of this feature put a header plus one caption per frame into
89
- the user's own message; consecutive text parts are concatenated by the UI with
90
- no separator, so a one-image message became an unreadable wall.)
91
- - `read_animation` tool: decodes an animated GIF or animated WebP, samples frames
92
- across the timeline (or by explicit index / instant), commits each frame to the
93
- attachment store, and returns a header plus one captioned image per frame.
94
- - Refuses animated PNG by name instead of silently returning its first frame,
95
- which is what the host image library does with one.
96
- - Refuses static images and points at `read_image`; answers a single-frame GIF or
97
- WebP with a note that it is not animated.
98
- - Discloses decode-time substitutions in the model-facing text: zero frame
99
- delays treated as 100 ms, missing delays, transparency flattened onto white.
100
- - The package imports no `@deepseek-ai/*` module at runtime, so it loads both as
101
- an installed package and as a `link:`ed checkout. The compiled schemas ship as
102
- literals beside their authoring specs, and the integration harness keeps the
103
- two in step.
1
+ # Changelog
2
+
3
+ ## 0.1.2 — 2026-11-08
4
+
5
+ - Fix [#7](https://github.com/NeoHotaru/dsh-read-animation/issues/7). Capture
6
+ uploaded GIF and animated WebP bytes at `attachments.saveImages`. DSH 0.2.2
7
+ normalizes uploads before prompt admission, so its stored references previously
8
+ bypassed capture. Original animation display, model frame sheets, and
9
+ normalized-copy reads now use the captured source.
10
+ - Add raw-upload regressions through the real attachment store and, on DSH
11
+ 0.2.2, its upload service and receipt resolution. Update the integration fixture
12
+ for the separate native tool executor and batch execution API.
13
+
14
+
15
+ - **The supported DSH range is measured rather than assumed.** The plugin wraps
16
+ internal service methods, and `engines.dsh` claimed `>=0.1.7-alpha.1` on the
17
+ strength of one working install. The integration suite now runs against
18
+ `0.1.3-alpha.2` (the oldest release exposing every member), `0.1.7-alpha.2`,
19
+ `0.2.0-rc.2` — which is `latest` — and `0.2.1-alpha.3`, all 38/38, and
20
+ `scripts/check-dsh-compat.py` reads the type declarations of all 32 published
21
+ releases to find the floor: **0.1.3-alpha.2**. See
22
+ [COMPATIBILITY.md](COMPATIBILITY.md), including what is *not* claimed.
23
+ - **A host missing a member now says so.** Nothing checked the members the plugin
24
+ calls before calling them: a host with `readImage` but no `saveImages` mounted
25
+ cleanly and then threw a `TypeError` from inside the tool. Mount reports every
26
+ missing member with the release it arrived in, the tool names the reason in its
27
+ own error, and a test covers the check itself.
28
+ - **`preview-sheet` printed a path that does not exist.** It spelled the store
29
+ layout as `files/<name>` where the store uses `files/<2>/<digest>/<name>`. The
30
+ plugin asks `fileHostPath` and was always right; only the script's second copy of
31
+ the layout could drift.
32
+ - **The integration harness needed `maxPixels` in a request target.** DSH
33
+ 0.1.3-alpha.2 rejects a target without it; later releases default it. A test
34
+ artefact, not a compatibility problem, but it meant the suite could not run on
35
+ the floor version at all.
36
+
37
+ ## 0.1.1 — 2026-10-14
38
+
39
+ Fixed after the first release, from what a live session reported back.
40
+
41
+ - **`read_animation` follows the path it advertises.** Every image a model receives
42
+ is shown as a normalized copy at `attachments/v1/objects/<2>/<digest>`, and that
43
+ is the path a model reaches for when it wants to look at the picture again. For a
44
+ pasted animation that copy is a single frame, so the tool answered "this picture
45
+ has one frame": true of that file, and useless as an answer. The tool now resolves
46
+ that path to the animation this plugin kept, and reports the file the bytes
47
+ actually came from. A bare `sha256:<digest>` is accepted too.
48
+ - **The sheet says where the animation lives.** Its header ends with the path the
49
+ store keeps the original at, so a model that wants more than the eight samples can
50
+ read that path with explicit frame indices instead of searching the attachment
51
+ store for the file behind the picture it was sent.
52
+
53
+ ## 0.1.0 — 2026-10-14
54
+
55
+ First release.
56
+
57
+ Note for anyone publishing a later version, because both releases so far looked like
58
+ failures while they were simply still in flight:
59
+
60
+ - `npm publish` answers `PUT … 202` and prints "Your package is being processed and
61
+ may take a few minutes to become available", then exits 0. For a brand-new package
62
+ name the registry also carries a public `0.0.0-stage` placeholder alongside it.
63
+ Nothing needs approving in either case.
64
+ - Measured on 0.1.1: the version appeared in the packument **2m41s** after the PUT,
65
+ and its tarball became downloadable **7m05s** after it. In between, the packument
66
+ either 404s or lists the version while its tarball still 404s.
67
+ - Check the tarball, not just `npm view versions`, and check it with a GET: a HEAD
68
+ request to the tarball URL answers 404 even once the tarball is live.
69
+ - A mirror lags further. `registry.npmmirror.com` still served 0.1.0 well after
70
+ 0.1.1 was live on npmjs.org, and a profile configured against it keeps installing
71
+ the previous version until it syncs.
72
+
73
+ ### Fixed after an adversarial audit
74
+
75
+ Four independent reviews (blast radius, plugin code, packaging and lifecycle, then
76
+ an adversarial pass over every high-severity finding) ran before this was
77
+ published. What they found, and what changed:
78
+
79
+ - **The frame sheet's `variantId` was not a content digest.** It was a hand-rolled
80
+ `sha256:filmanim…` string, and the DeepSeek adapter validates persisted ids
81
+ against `/^sha256:[0-9a-f]{64}$/` when it loads its upload index — then swallows
82
+ the failure into an *empty* index. The effect was not a visible error: it
83
+ discarded every other session's cached file id and re-uploaded every image.
84
+ Reproduced against the real exported index class (plugin id: written 1, reloaded
85
+ 0; conforming id: 1/1). It is now the SHA-256 of the bytes being returned, which
86
+ also stops one on-disk cache entry from aliasing two different byte streams.
87
+ - **The degraded path was worse than the missing feature.** The store reaches the
88
+ frame it re-encodes through its own `readImage` call, which this plugin wraps, so
89
+ falling back to the store's request read could hand the model the *whole
90
+ animation* where it expects one frame. The request path now suspends the
91
+ animation substitution around that call, and reads its own tiles through the
92
+ unwrapped read. Pinned by an integration check that breaks a stored frame and
93
+ asserts the fallback is exactly one frame.
94
+ - **No decoded-pixel ceiling on the `read_animation` path.** `limitInputPixels`
95
+ was disabled with nothing in its place: a 60 KB file declaring a 9000x9000
96
+ canvas was refused by the deployment's own attachment store and decoded here
97
+ anyway, at about 310 MiB. Both the probe and the frame extraction now carry the
98
+ deployment's `imageLimits.maxImagePixels`.
99
+ - **The sheet cache ignored the size the caller asked for**, so a caller with an
100
+ 8 KiB budget could be served a 28 KB sheet built for a 4 MiB one. The target is
101
+ now part of the cache key, and a tile is never larger than a single frame would
102
+ have been for that caller.
103
+ - **The measured header wrap could still be defeated.** A floor of 16 characters
104
+ kept the loop from reaching a budget that fit, so on a narrow canvas it gave up
105
+ and returned ink wider than the sheet. The floor is now low enough to be
106
+ satisfiable, and a canvas too narrow for legible text gets no header at all.
107
+ - **The canvas was composed before it was shrunk**, building a 6156x6306 (about
108
+ 111 MiB) image to throw away. Tile size and grid are settled first.
109
+ - **Torn state files could poison the next record.** A crash part way through an
110
+ append left a line with no newline, so the following record concatenated onto it
111
+ and became unreadable too. A torn tail is now detected and compacted away.
112
+ - **The store's bound applied to memory but not to what was read back**, and the
113
+ log was never compacted. Both fixed; the file is rewritten when it grows past
114
+ twice the entries worth keeping.
115
+ - **Teardown was last-writer-wins.** Each install captured whatever was on the
116
+ method, so disposing one left another's patch removed and could reinstall a
117
+ disposed closure as a permanent own property. Three wrappers now share one
118
+ installer that resolves its delegate per call, removes cleanly (deleting an
119
+ inherited property rather than leaving a shadow behind), and reports whether it
120
+ actually took effect — the previous identity check compared a value that proxy
121
+ reads always re-wrap, so it could never detect a failed install.
122
+ - **`npm test` did not run the invariant checks.** The no-`@deepseek-ai`-import
123
+ rule and the bundle/manifest consistency check lived only in the integration
124
+ harness, which prints SKIP and exits 0 without `DSH_HARNESS_MODULES`. Both now
125
+ run in `npm test`, which is also the `prepublishOnly` gate.
126
+ - **`files[]` omitted `scripts/`** while `package.json` advertised two npm scripts
127
+ that live there, so the published package's own scripts were broken.
128
+
129
+ - **A pasted animation keeps playing in the UI, and the message is left alone.**
130
+ The browser already uploads an animated GIF/WebP byte-for-byte and labelled as
131
+ such; the Host flattens it to a single
132
+ frame during prompt admission and keeps no copy of the source. The plugin wraps
133
+ `ctx.attachments.admitPromptContent` to capture the frames and the original file
134
+ at that moment, and `ctx.attachments.readImage` so the UI is served the animation
135
+ itself (the browser animates whatever bytes that one read returns). What you send
136
+ is what you send: the chat message is unchanged.
137
+ - **The model now receives a labelled sheet of the frames.** The delivery step is
138
+ `ctx.attachments.readImageRequest(ref, target, signal)`, the method each adapter
139
+ calls to build its own copy of one image. It answers with exactly one image, so
140
+ the frames are composed into a grid chosen to be as square as the sheet budget
141
+ allows — every tile at its native size, with the frame index, duration and start
142
+ time printed under each, and the animation's own facts in a wrapped header. The
143
+ sheet is built once per animation and cached.
144
+ - Rewriting the request from an `llm/stream` listener is **not** used and would not
145
+ work: a loop-built request arrives deep-frozen ("listeners read it, never rewrite
146
+ it"), so that earlier attempt silently did nothing.
147
+ - Captured frames and the original are persisted per `$DSH_HOME` in
148
+ `read-animation/v1/pasted.jsonl`, keyed by the message's attachment id, so a
149
+ session resumed in a new process still displays — and still delivers — an
150
+ animation pasted before the restart.
151
+ - A frame's index and start time ride in its attachment display name
152
+ (`x.gif#frame16@1960ms`), which the harness renders beside the picture once the
153
+ frames are delivered — so the timing never has to be repeated as message text.
154
+ (An earlier version of this feature put a header plus one caption per frame into
155
+ the user's own message; consecutive text parts are concatenated by the UI with
156
+ no separator, so a one-image message became an unreadable wall.)
157
+ - `read_animation` tool: decodes an animated GIF or animated WebP, samples frames
158
+ across the timeline (or by explicit index / instant), commits each frame to the
159
+ attachment store, and returns a header plus one captioned image per frame.
160
+ - Refuses animated PNG by name instead of silently returning its first frame,
161
+ which is what the host image library does with one.
162
+ - Refuses static images and points at `read_image`; answers a single-frame GIF or
163
+ WebP with a note that it is not animated.
164
+ - Discloses decode-time substitutions in the model-facing text: zero frame
165
+ delays treated as 100 ms, missing delays, transparency flattened onto white.
166
+ - The package imports no `@deepseek-ai/*` module at runtime, so it loads both as
167
+ an installed package and as a `link:`ed checkout. The compiled schemas ship as
168
+ literals beside their authoring specs, and the integration harness keeps the
169
+ two in step.
@@ -0,0 +1,100 @@
1
+ # Compatibility
2
+
3
+ ## Supported range
4
+
5
+ **DSH 0.1.3-alpha.2 and later.** Upload compatibility rechecked on
6
+ 2026-11-08 against DSH 0.2.2-rc.1 and 0.1.7-alpha.2.
7
+
8
+ ## What was measured
9
+
10
+ ### The integration suite, actually run
11
+
12
+ `test/integration/harness.mjs` boots the real `dsh-fs-local`,
13
+ `dsh-attachment-local`, `dsh-system-prompt` and `dsh-tools` plugins in-process,
14
+ registers the tool, and exercises the paste path through a real `llm.stream`
15
+ dispatch. Pointed at four versions spanning the range:
16
+
17
+ | DSH | checks |
18
+ | --- | --- |
19
+ | `0.1.3-alpha.2` — the oldest release with the full API | 38/38 |
20
+ | `0.1.7-alpha.2` — the version this was developed against | 38/38 |
21
+ | `0.2.0-rc.2` — `latest` on that date | 38/38 |
22
+ | `0.2.1-alpha.3` — `alpha` on that date, newest | 38/38 |
23
+
24
+ The table above records the 2026-10-14 baseline. The upload regression checks
25
+ added on 2026-11-08 produced the following results:
26
+
27
+ | DSH | checks |
28
+ | --- | --- |
29
+ | `0.1.7-alpha.2` — encoded-prompt and raw image-storage paths | 40/40 |
30
+ | `0.2.2-rc.1` — includes the raw upload service and stored-reference admission | 41/41 |
31
+
32
+ DSH 0.2.2 normalizes uploaded images before prompt admission. Capture now wraps
33
+ `saveImages`, which receives raw bytes for both upload and encoded-prompt paths.
34
+ The regressions check GIF and animated WebP display, frame sheets, and reads of
35
+ normalized-copy paths. The 0.2.2 case also calls the real
36
+ `FileUploads.uploadImageStream` and resolves its receipt into a stored image
37
+ reference. The test harness mounts the native tool executor on hosts that expose
38
+ that separate service.
39
+
40
+ ```sh
41
+ DSH_HARNESS_MODULES=/path/to/dsh/node_modules node test/integration/harness.mjs
42
+ ```
43
+
44
+ ### The API surface, read from every published release
45
+
46
+ `scripts/check-dsh-compat.py` downloads the type declarations of
47
+ `@deepseek-ai/dsh-attachment` and `@deepseek-ai/dsh-attachment-local` for every
48
+ published DSH version and reports which of the members this plugin calls are
49
+ present. DSH publishes its whole monorepo at one version, so `dsh@X` pins
50
+ `dsh-attachment-local@X`.
51
+
52
+ ```sh
53
+ python scripts/check-dsh-compat.py
54
+ ```
55
+
56
+ The result over all 32 published releases: every member is present from
57
+ **0.1.3-alpha.2** onward and absent before it. The members, and the release each
58
+ first appears in:
59
+
60
+ | member | since |
61
+ | --- | --- |
62
+ | `saveImages` | `0.1.0-rc.7` |
63
+ | `readImageRequest` | `0.1.2-alpha.3` |
64
+ | `readFileStream` | `0.1.2-alpha.3` |
65
+ | `saveFile` | `0.1.3-alpha.2` |
66
+ | `fileHostPath` | `0.1.3-alpha.2` — optional; the plugin works without it |
67
+
68
+ So the floor is set by `saveFile`, which is what keeps the original animation's
69
+ bytes for the sheet, and by `fileHostPath`. Below `0.1.3-alpha.2` the plugin still
70
+ mounts and still reads animations from files; pasted animations degrade to a
71
+ single frame, and it says which member is missing and when that member arrived.
72
+
73
+ ## What is not claimed
74
+
75
+ - **Four of the seventeen releases in the range were executed.** Every release in
76
+ the range was checked to expose the full member list, but a release whose types
77
+ are complete can still behave differently. The four points span floor to ceiling.
78
+ - **The upper bound is a date, not a guarantee.** This plugin wraps
79
+ `saveImages`, `readImage`, and `readImageRequest` — internal service
80
+ methods. A DSH release that changes their signatures or the shape of a request
81
+ target will break it, and nothing here can detect that in advance.
82
+ - **The `engines.dsh` field is documentation.** npm does not enforce a
83
+ non-standard `engines` key, and DSH does not read it. It records the measured
84
+ floor; it does not stop an install.
85
+
86
+ ## Re-measuring after a DSH release
87
+
88
+ 1. `python scripts/check-dsh-compat.py` — does the new release still expose
89
+ everything?
90
+ 2. Install it somewhere and run the suite against it:
91
+
92
+ ```sh
93
+ mkdir /tmp/dsh-probe && cd /tmp/dsh-probe
94
+ npm init -y && npm i @deepseek-ai/dsh@<version>
95
+ cd <this repo>
96
+ DSH_HARNESS_MODULES=/tmp/dsh-probe/node_modules node test/integration/harness.mjs
97
+ ```
98
+
99
+ 3. If it fails, the check that fails names the method. The messages raised at mount
100
+ and from the tool also name the missing member and the release it arrived in.
package/README.md CHANGED
@@ -52,13 +52,13 @@ the chunk stream and refuses it by name instead of returning frame 0.
52
52
  paste handler hands the clipboard's own `File` straight to the upload path, and
53
53
  that path encodes `image/gif` and `image/webp` unchanged (`imageMediaType()`
54
54
  accepts both and returns them as-is) — no canvas, no `drawImage`, no `toBlob`.
55
- **The bytes reach the Host whole.** It is the Host that destroys them:
56
- `admitPromptContent` runs every image through the single-frame normalizer and
57
- keeps no copy of the source.
55
+ **The bytes reach the Host whole.** The Host normalizes them through
56
+ `attachments.saveImages`. In DSH 0.2.2 this happens during upload, so prompt
57
+ admission receives an already-stored image reference.
58
58
 
59
- `admitPromptContent` is therefore the last moment the original bytes exist. The
60
- plugin wraps it, decodes the animation and stores the frames — then **returns the
61
- original result untouched**:
59
+ The plugin wraps `saveImages`, captures the animation from its raw inputs, and
60
+ stores the original and decoded frames against the returned image reference.
61
+ This covers raw uploads and older encoded-prompt admission:
62
62
 
63
63
  ```
64
64
  you paste 1 animation
@@ -143,22 +143,26 @@ the gaps are gaps.
143
143
 
144
144
  ## Install
145
145
 
146
+ Needs **DSH 0.1.3-alpha.2 or later** — see [COMPATIBILITY.md](COMPATIBILITY.md) for
147
+ the measured range, how it was measured, and what is not claimed.
148
+
146
149
  ```sh
147
150
  dsh plugin --profile web add dsh-read-animation
148
151
  ```
149
152
 
150
- Replace `web` with your profile name. **Not on npm yet** — until it is, install
151
- straight from the repository:
153
+ Replace `web` with your profile name. Both routes are verified in a real profile:
154
+ the install lands, `dsh plugin add` registers the bundle, and the `sharp` native
155
+ binary resolves from the package's own `node_modules`.
152
156
 
153
157
  ```sh
154
- dsh plugin --profile web add github:NeoHotaru/dsh-read-animation
158
+ dsh plugin --profile web add dsh-read-animation # npm
159
+ dsh plugin --profile web add github:NeoHotaru/dsh-read-animation#v0.1.0 # a tagged commit
155
160
  ```
156
161
 
157
- That route is verified end to end: it installs, `dsh plugin add` registers the
158
- bundle, and the `sharp` native binary resolves from the package's own
159
- `node_modules`. (A git spec copies the whole repository, tests and fixtures
160
- included, which is why the npm package above is the nicer install once it
161
- exists.)
162
+ The git route copies the whole repository, tests and fixtures included, which is
163
+ why npm is the nicer install. (A brand-new package name goes through npm's staged
164
+ publishing on its first release, so the version you install may briefly not exist
165
+ yet while the registry processes it.)
162
166
 
163
167
  Or from a checkout:
164
168
 
@@ -238,8 +242,8 @@ tool refuses to run when the resolved model route does not declare image input,
238
242
  for the same reason `read_image` does.
239
243
 
240
244
  **A pasted animation** is handled in three steps, none of which touch your
241
- message: **capture** at admission (`ctx.attachments.admitPromptContent` is the last
242
- moment the original bytes exist), **display** at `ctx.attachments.readImage` (the
245
+ message: **capture** at image storage (`ctx.attachments.saveImages` receives the
246
+ original bytes), **display** at `ctx.attachments.readImage` (the
243
247
  one read the UI makes, served the original so it plays), and **delivery** at
244
248
  `ctx.attachments.readImageRequest` (the model's copy, composed into one labelled
245
249
  sheet). All three are **monkey-patches of core service methods**, so they do
package/README.zh.md CHANGED
@@ -47,10 +47,11 @@ APNG 得的是同一种病,而且更隐蔽:libvips 8.18.7 根本不认 `acTL
47
47
  **入口一 · 贴进聊天框。** 浏览器那段路本身是干净的——粘贴处理器把 `clipboardData` 里的原始
48
48
  `File` 直接交出去,上传路径对 `image/gif` / `image/webp` 原样编码(`imageMediaType()` 明确
49
49
  接受这两种并原样返回),没有 canvas、没有 `drawImage`、没有 `toBlob`。**字节完好地到达宿主。**
50
- 丢掉它的是宿主自己:`admitPromptContent` 把每张图都过一遍「单帧」归一化,源字节不保留。
50
+ 宿主通过 `attachments.saveImages` 将图片归一化。DSH 0.2.2 在上传阶段就完成这一步,
51
+ 之后消息接收入口拿到的是已存储的图片引用。
51
52
 
52
- `admitPromptContent` 是**原件还存在的最后一刻**,所以本插件在这里把它解码、把帧存下来——
53
- 然后**把原样的结果还回去**:
53
+ 本插件在 `saveImages` 保存图片时,用仍可访问的原始字节解码动画,并将原件和帧关联到
54
+ 返回的图片引用。这同时覆盖原始文件上传和旧版编码图片入口:
54
55
 
55
56
  ```
56
57
  你贴 1 张动图
@@ -121,15 +122,16 @@ frame 9 of 24 (sample 3) · starts at 450 ms · displayed for 50 ms
121
122
  dsh plugin --profile web add dsh-read-animation
122
123
  ```
123
124
 
124
- `web` 换成你自己的 profile 名。**目前还没发到 npm** —— 在那之前可以直接从仓库装:
125
+ `web` 换成你自己的 profile 名。**两条路都在真实 profile 里验证过**:装得上、`dsh plugin add` 会把它
126
+ 注册成 bundle、`sharp` 的原生二进制也能从包自己的 `node_modules` 里解析出来。
125
127
 
126
128
  ```sh
127
- dsh plugin --profile web add github:NeoHotaru/dsh-read-animation
129
+ dsh plugin --profile web add dsh-read-animation # npm
130
+ dsh plugin --profile web add github:NeoHotaru/dsh-read-animation#v0.1.0 # 钉住某个 tag
128
131
  ```
129
132
 
130
- 这条路是**实测过的**:能装上、`dsh plugin add` 会把它注册成 bundle、`sharp` 的原生二进制也能
131
- 从包自己的 `node_modules` 里解析出来。(git spec 会把整个仓库连测试和 fixture 一起拷下来,
132
- 所以等上面那个 npm 包存在之后,npm 那条路更干净。)
133
+ git 那条会把整个仓库(含测试和 fixture)拷下来,所以 npm 更干净。(全新包名的**首次发布**会走
134
+ npm 的 staged publishing,所以刚发完那一小段时间里,registry 上可能还查不到那个版本号。)
133
135
 
134
136
  或者从本地检出:
135
137
 
@@ -200,8 +202,8 @@ PNG,所以 harness 的归一化管线对它无事可做。
200
202
  `read_image` 完全一致。当解析出的模型路由未声明图片输入时,工具会拒绝执行——理由和
201
203
  `read_image` 相同。
202
204
 
203
- **贴进聊天框的动图**分三步,都不碰你的消息:**捕获**在准入层
204
- (`ctx.attachments.admitPromptContent` 是原件还在的最后一刻),**显示**在
205
+ **贴进聊天框的动图**分三步,都不碰你的消息:**捕获**在图片存储层
206
+ (`ctx.attachments.saveImages` 仍能访问上传原件),**显示**在
205
207
  `ctx.attachments.readImage`(界面取图的那一次读,返回原件所以会动),**交付**在
206
208
  `ctx.attachments.readImageRequest`(模型那份,拼成一张带标注的胶片条)。三步都是
207
209
  **猴补核心服务的方法**,所以写得很保守——
@@ -0,0 +1,32 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96" width="96" height="96" role="img" aria-label="Four frames of one animation, with the subject growing across them">
2
+ <!--
3
+ The plugin's whole idea in one mark: one animation shown as a grid of frames
4
+ over time, with the subject changing across them.
5
+
6
+ Drawn for the size it is actually rendered at, roughly 40px on a bundle card:
7
+ four bold shapes, no gradients, no text, nothing that turns to mush. The
8
+ background is a mid-tone tile so the mark reads on both a light and a dark
9
+ card, and the frames inside carry the contrast.
10
+ -->
11
+ <rect width="96" height="96" rx="21" fill="#26333F"/>
12
+ <g fill="#EDF2F8">
13
+ <rect x="10" y="10" width="36" height="36" rx="7"/>
14
+ <rect x="50" y="10" width="36" height="36" rx="7"/>
15
+ <rect x="10" y="50" width="36" height="36" rx="7"/>
16
+ <rect x="50" y="50" width="36" height="36" rx="7"/>
17
+ </g>
18
+ <!--
19
+ The subject grows across the four moments. Size is a property the eye reads
20
+ even when it scans a grid out of order, which a position change is not: in a
21
+ 2x2 layout, reading order jumps from the top right back to the bottom left, so
22
+ four dots each moving inside their own frame read as four unrelated things.
23
+ -->
24
+ <g fill="#26333F">
25
+ <circle cx="28" cy="28" r="5"/>
26
+ <circle cx="68" cy="28" r="7"/>
27
+ <circle cx="28" cy="68" r="9"/>
28
+ </g>
29
+ <!-- The last sampled moment is the one the sheet calls out, so it is the one
30
+ shape in the mark that is not greyscale. -->
31
+ <circle cx="68" cy="68" r="11" fill="#C9761B"/>
32
+ </svg>