dsh-plugin-image-tools 0.6.1 → 0.6.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/README.md
CHANGED
|
@@ -175,7 +175,13 @@ npm install
|
|
|
175
175
|
## 安全与限制
|
|
176
176
|
|
|
177
177
|
- 图片字节仅存于进程内存:选择卡图片随问题回答/取消立即释放;
|
|
178
|
-
|
|
178
|
+
内嵌图片与附件回显依赖 30 分钟 TTL 清理(需存活到回复渲染完成)。
|
|
179
|
+
- **浏览器缓存兜底**:图片路由的 URL 是内容寻址的(pickId/showId 为 UUID、
|
|
180
|
+
attachmentId 为内容哈希),字节永不改变,因此响应头下发
|
|
181
|
+
`Cache-Control: private, max-age=2592000, immutable`(30 天)。图片一旦在
|
|
182
|
+
浏览器里加载成功,字节就留在浏览器本地缓存——之后源文件被删除/覆盖、
|
|
183
|
+
服务端 TTL 清理、甚至 dsh 重启,刷新页面 / 回看历史消息时浏览器直接命中
|
|
184
|
+
本地缓存,图片依然可见(不再向服务端发请求)。
|
|
179
185
|
- 单张图片上限 20 MiB;仅支持 PNG / JPEG / WebP / GIF(按魔数校验,声明不符会报错)。
|
|
180
186
|
- 图片路由为同源普通 HTTP 路由(与 GUI 同信任级别),未加额外鉴权。
|
|
181
187
|
- 内嵌图片的 markdown URL 是绝对地址(`http://host:port`,由服务端监听配置推导);
|
package/lib/index.js
CHANGED
|
@@ -47,6 +47,15 @@ export const ROUTE_PREFIX = '/dsh-plugin-image-tools'
|
|
|
47
47
|
export const MAX_IMAGE_BYTES = 20 * 1024 * 1024
|
|
48
48
|
/** 未答/未展示图片注册条目的存活时间(被放弃的提问与历史回复最终被清理)。 */
|
|
49
49
|
export const IMAGE_TTL_MS = 30 * 60 * 1000
|
|
50
|
+
/**
|
|
51
|
+
* 图片字节路由的浏览器缓存策略。
|
|
52
|
+
* 三个路由的 URL 都是内容寻址的(pickId/showId 为 UUID、attachmentId 为内容哈希),
|
|
53
|
+
* 同一 URL 的字节永远不会变,因此可以放心下发 immutable 长缓存:图片一旦加载
|
|
54
|
+
* 成功就留在浏览器本地,源文件被删除/覆盖、服务端内存注册表被 TTL 清理、甚至
|
|
55
|
+
* dsh 进程重启后,刷新页面/回看历史消息时浏览器直接命中本地缓存,不再向服务端
|
|
56
|
+
* 发请求(避免 404:max-age=300 的旧策略下,图片见过一次后 5 分钟就"消失")。
|
|
57
|
+
*/
|
|
58
|
+
export const IMAGE_CACHE_CONTROL = 'private, max-age=2592000, immutable'
|
|
50
59
|
/** 拉取远程图片的超时(ms)。 */
|
|
51
60
|
export const FETCH_TIMEOUT_MS = 30 * 1000
|
|
52
61
|
/** detail 里不可见标记的前缀/后缀。 */
|
|
@@ -804,7 +813,7 @@ export function apply(ctx) {
|
|
|
804
813
|
res.writeHead(200, {
|
|
805
814
|
'content-type': ref.mediaType,
|
|
806
815
|
'content-length': stored.data.byteLength,
|
|
807
|
-
'cache-control':
|
|
816
|
+
'cache-control': IMAGE_CACHE_CONTROL,
|
|
808
817
|
'x-content-type-options': 'nosniff'
|
|
809
818
|
})
|
|
810
819
|
res.end(Buffer.from(stored.data))
|
|
@@ -830,7 +839,7 @@ export function apply(ctx) {
|
|
|
830
839
|
res.writeHead(200, {
|
|
831
840
|
'content-type': image.mediaType,
|
|
832
841
|
'content-length': image.bytes.byteLength,
|
|
833
|
-
'cache-control':
|
|
842
|
+
'cache-control': IMAGE_CACHE_CONTROL,
|
|
834
843
|
'x-content-type-options': 'nosniff'
|
|
835
844
|
})
|
|
836
845
|
res.end(image.bytes)
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-image-tools",
|
|
3
3
|
"description": "DSH 图片插件:ask_user_choice 图片/图文混合选项(Web GUI 渲染图片选择卡,可放大查看)+ show_images 在回复中内嵌图片(图片与文字混排)+ 聊天栏所有图片点击放大,放大层支持滚轮/按钮缩放与拖拽平移。图片来源支持本地路径 / http(s) URL / base64 data URI。纯插件实现,不改核心包。",
|
|
4
|
-
"version": "0.6.
|
|
4
|
+
"version": "0.6.2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|
package/scripts/smoke-server.mjs
CHANGED
|
@@ -16,7 +16,7 @@ import { strict as assert } from 'node:assert'
|
|
|
16
16
|
import { mkdtempSync, readFileSync, existsSync } from 'node:fs'
|
|
17
17
|
import { tmpdir } from 'node:os'
|
|
18
18
|
import { join } from 'node:path'
|
|
19
|
-
import { apply, ROUTE_PREFIX } from '../lib/index.js'
|
|
19
|
+
import { apply, IMAGE_CACHE_CONTROL, ROUTE_PREFIX } from '../lib/index.js'
|
|
20
20
|
|
|
21
21
|
const pngBytes = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 1, 2, 3, 4])
|
|
22
22
|
|
|
@@ -139,6 +139,7 @@ route.handler(
|
|
|
139
139
|
)
|
|
140
140
|
assert.equal(served.status, 200)
|
|
141
141
|
assert.equal(served.headers['content-type'], 'image/png')
|
|
142
|
+
assert.equal(served.headers['cache-control'], IMAGE_CACHE_CONTROL, '选择卡图片应下发长缓存')
|
|
142
143
|
assert.deepEqual(served.body, pngBytes)
|
|
143
144
|
// 越界下标 → 404
|
|
144
145
|
let notFound = null
|
|
@@ -201,6 +202,7 @@ for (const index of [0, 1]) {
|
|
|
201
202
|
}
|
|
202
203
|
assert.equal(showServed[0].status, 200)
|
|
203
204
|
assert.equal(showServed[0].headers['content-type'], 'image/png')
|
|
205
|
+
assert.equal(showServed[0].headers['cache-control'], IMAGE_CACHE_CONTROL, '内嵌图片应下发长缓存')
|
|
204
206
|
assert.deepEqual(showServed[0].body, pngBytes)
|
|
205
207
|
assert.equal(showServed[1].status, 200)
|
|
206
208
|
// 越界 → 404
|
|
@@ -250,6 +252,7 @@ await route.handler(
|
|
|
250
252
|
)
|
|
251
253
|
assert.equal(attServed.status, 200)
|
|
252
254
|
assert.equal(attServed.headers['content-type'], 'image/png')
|
|
255
|
+
assert.equal(attServed.headers['cache-control'], IMAGE_CACHE_CONTROL, '附件回显应下发长缓存')
|
|
253
256
|
assert.deepEqual(attServed.body, pngBytes)
|
|
254
257
|
// 未知附件 → 404
|
|
255
258
|
let attNotFound = null
|
|
@@ -164,12 +164,19 @@
|
|
|
164
164
|
## 边界与取舍
|
|
165
165
|
|
|
166
166
|
- 图片字节在内存(v1 简单可靠);20 MiB × 张数有总量风险,后续可落盘/走附件。
|
|
167
|
+
- **浏览器缓存兜底**:三个图片路由(pick/show/attachment)的 URL 均为内容寻址
|
|
168
|
+
(UUID / 内容哈希),字节不可变,故下发 `Cache-Control: private,
|
|
169
|
+
max-age=2592000, immutable`(30 天)。图片一旦在浏览器加载成功,就进入浏览器
|
|
170
|
+
本地缓存:源文件被删/覆盖、服务端 TTL 清理、进程重启,浏览器直接命中缓存
|
|
171
|
+
不再请求 → 历史消息图片"见过就不消失"。代价是服务端更新同 URL 字节不可见
|
|
172
|
+
——但内容寻址语义下同 URL 永远不会换字节,无此风险。
|
|
167
173
|
- 图片路由与 GUI 同信任级别,未加额外鉴权(与整站一致)。
|
|
168
174
|
- detail 以纯文本展示(未引 MarkdownText,避免对 primitives 的依赖面)。
|
|
169
175
|
- URL 图片由服务端拉取(浏览器跨域/CSP 不受影响)。
|
|
170
176
|
- 内嵌图片 URL 是绝对地址:GUI 经反向代理/换端口访问时历史消息图片可能失效
|
|
171
177
|
(可后续用 `x-forwarded-*` / Host 头改进)。
|
|
172
|
-
- `shows` 无「展示完成」事件,采用 TTL 清理(30
|
|
178
|
+
- `shows` 无「展示完成」事件,采用 TTL 清理(30 分钟),重启即失效(与选择卡一致);
|
|
179
|
+
失效前已被浏览器缓存过的图片不受影响(见上一条)。
|
|
173
180
|
|
|
174
181
|
## 验证
|
|
175
182
|
|