dsh-instruction-memory 1.0.3 → 1.0.5
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 +8 -1
- package/lib/client.js +39 -13
- package/lib/index.js +120 -26
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -57,7 +57,9 @@ DSH 就会得到第二份空记忆,看起来像"记忆消失"。
|
|
|
57
57
|
> 1.0.0 之前的构建曾把数据写在宿主进程 cwd 下(`.dsh-instruction-memory.json`)。
|
|
58
58
|
> 首次启动新版本时会自动迁移到上述位置,并把原文件改名为 `*.migrated` 保留。
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
该文件可手工编辑,改完点设置页的「重新载入」即可读回。每次保存先写临时文件再原子替换,
|
|
61
|
+
进程崩溃不会留下损坏的半截 `memory.json`。若手工编辑后的文件有条目为空或超过 200 条上限,
|
|
62
|
+
设置页会明确提示哪些条目未能载入(它们将在下次保存时从文件中移除),不会静默丢弃。
|
|
61
63
|
|
|
62
64
|
## 兼容性
|
|
63
65
|
|
|
@@ -83,6 +85,11 @@ major.minor.patch 且自身带预发布标签的比较符"。因此看起来很
|
|
|
83
85
|
|
|
84
86
|
**未验证范围**:macOS / Linux、`headless` 与 `tui` profile、以及上表之外的其他 DSH 版本均未实测。
|
|
85
87
|
|
|
88
|
+
**同一串范围写在两处**:`dsh.compatibility.dsh`(插件自己的字段)与 `dsh.engines.dsh`(插件市场**实际读取**的位置)。
|
|
89
|
+
市场只读 `engines.dsh` / `dsh.engines.dsh` 以及 `@deepseek-ai/dsh*` 的 `peerDependencies`,并不读 `dsh.compatibility.dsh`。
|
|
90
|
+
从 1.0.4 起宿主要求会真实出现在市场卡片上,并生效于安装前的校验:**范围之外的宿主上,市场会把本插件从列表中隐藏并拒绝安装/更新**
|
|
91
|
+
(市场只隐藏"确认不兼容"的条目;"未声明"或"无法确认"的条目照常显示)。范围之外的宿主仍可自行用 `dsh plugin add` 安装,只是不再被市场担保。
|
|
92
|
+
|
|
86
93
|
## 发布(维护者)
|
|
87
94
|
|
|
88
95
|
```sh
|
package/lib/client.js
CHANGED
|
@@ -57,6 +57,7 @@ window.__ModuleLoader__.load({
|
|
|
57
57
|
border:1px dashed var(--dsw-alias-border-l2,rgba(0,0,0,.18)); color:var(--dsw-alias-label-secondary,#8a9099); }
|
|
58
58
|
.im-err { font-size:12px; color:var(--dsw-alias-state-error-primary,#d94838); }
|
|
59
59
|
.im-ok { font-size:12px; color:var(--dsw-alias-state-success-primary,#18a058); }
|
|
60
|
+
.im-warn { font-size:12px; color:var(--dsw-alias-state-warn-primary,#d97706); }
|
|
60
61
|
.im-meta { font-size:11.5px; color:var(--dsw-alias-label-secondary,#8a9099); word-break:break-all; }
|
|
61
62
|
.im-pre { margin:0; padding:12px 14px; border-radius:8px; font-size:11.5px; line-height:1.65;
|
|
62
63
|
background:var(--dsw-alias-bg-layer-2,rgba(0,0,0,.04)); color:var(--dsw-alias-label-secondary,#4b5563);
|
|
@@ -107,13 +108,25 @@ window.__ModuleLoader__.load({
|
|
|
107
108
|
}
|
|
108
109
|
|
|
109
110
|
async function callHost(method, args) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
111
|
+
// A hung host used to pin the panel on 保存中… forever; fail visibly
|
|
112
|
+
// after a generous timeout instead.
|
|
113
|
+
const controller = typeof AbortController === 'function' ? new AbortController() : null
|
|
114
|
+
const timer = controller !== null ? setTimeout(() => controller.abort(), 15000) : null
|
|
115
|
+
try {
|
|
116
|
+
const response = await fetch(API, {
|
|
117
|
+
method: 'POST',
|
|
118
|
+
headers: { 'content-type': 'application/json' },
|
|
119
|
+
body: JSON.stringify({ method, args: args === undefined ? null : args }),
|
|
120
|
+
signal: controller !== null ? controller.signal : undefined,
|
|
121
|
+
})
|
|
122
|
+
if (!response.ok) throw new Error('宿主返回 HTTP ' + response.status)
|
|
123
|
+
return await response.json()
|
|
124
|
+
} catch (err) {
|
|
125
|
+
if (controller !== null && controller.signal.aborted) throw new Error('请求宿主超时(15 秒无响应)')
|
|
126
|
+
throw err
|
|
127
|
+
} finally {
|
|
128
|
+
if (timer !== null) clearTimeout(timer)
|
|
129
|
+
}
|
|
117
130
|
}
|
|
118
131
|
|
|
119
132
|
function EntryEditor(props) {
|
|
@@ -126,6 +139,7 @@ window.__ModuleLoader__.load({
|
|
|
126
139
|
h('input', {
|
|
127
140
|
className: 'im-input',
|
|
128
141
|
value: draft.title,
|
|
142
|
+
maxLength: 120,
|
|
129
143
|
placeholder: '例如:回答语言',
|
|
130
144
|
onChange: (ev) => set({ title: ev.target.value }),
|
|
131
145
|
})),
|
|
@@ -134,6 +148,7 @@ window.__ModuleLoader__.load({
|
|
|
134
148
|
h('textarea', {
|
|
135
149
|
className: 'im-textarea',
|
|
136
150
|
value: draft.content,
|
|
151
|
+
maxLength: 6000,
|
|
137
152
|
placeholder: '例如:所有回答使用简体中文;不确定需求时先提问再动手。',
|
|
138
153
|
onChange: (ev) => set({ content: ev.target.value }),
|
|
139
154
|
})),
|
|
@@ -164,6 +179,7 @@ window.__ModuleLoader__.load({
|
|
|
164
179
|
h('input', {
|
|
165
180
|
className: 'im-input',
|
|
166
181
|
value: draft.when,
|
|
182
|
+
maxLength: 200,
|
|
167
183
|
placeholder: '例如:编写或修改代码时',
|
|
168
184
|
onChange: (ev) => set({ when: ev.target.value }),
|
|
169
185
|
})))
|
|
@@ -253,7 +269,10 @@ window.__ModuleLoader__.load({
|
|
|
253
269
|
}
|
|
254
270
|
|
|
255
271
|
const data = view.data
|
|
256
|
-
|
|
272
|
+
// Older hosts can hand over snapshots without storage/limits; fall back
|
|
273
|
+
// instead of throwing mid-render.
|
|
274
|
+
const storage = view.storage || {}
|
|
275
|
+
const limits = view.limits || { minBudget: 800, maxBudget: 40000, maxEntries: 200 }
|
|
257
276
|
const injection = view.injection || { registered: false, chars: 0, error: null }
|
|
258
277
|
const entries = data.entries.slice().sort((a, b) => (b.priority - a.priority) || (b.updatedAt - a.updatedAt))
|
|
259
278
|
const activeCount = entries.filter((e) => e.enabled && e.content !== '').length
|
|
@@ -289,17 +308,17 @@ window.__ModuleLoader__.load({
|
|
|
289
308
|
h('input', {
|
|
290
309
|
className: 'im-input',
|
|
291
310
|
type: 'number',
|
|
292
|
-
min:
|
|
293
|
-
max:
|
|
311
|
+
min: limits.minBudget,
|
|
312
|
+
max: limits.maxBudget,
|
|
294
313
|
step: 100,
|
|
295
314
|
value: budgetText,
|
|
296
315
|
disabled: busy,
|
|
297
316
|
onChange: (ev) => setBudgetText(ev.target.value),
|
|
298
317
|
onBlur: () => {
|
|
299
318
|
const parsed = Number(budgetText)
|
|
300
|
-
if (!Number.isFinite(parsed) || parsed <
|
|
319
|
+
if (!Number.isFinite(parsed) || parsed < limits.minBudget || parsed > limits.maxBudget) {
|
|
301
320
|
setBudgetText(String(data.budgetChars))
|
|
302
|
-
setError('上限需在 ' +
|
|
321
|
+
setError('上限需在 ' + limits.minBudget + ' ~ ' + limits.maxBudget + ' 之间')
|
|
303
322
|
return
|
|
304
323
|
}
|
|
305
324
|
send('set-options', { budgetChars: parsed })
|
|
@@ -330,6 +349,7 @@ window.__ModuleLoader__.load({
|
|
|
330
349
|
storage.migratedFrom
|
|
331
350
|
? h('div', { className: 'im-ok' }, '已从旧位置迁移:' + storage.migratedFrom + '(原文件保留为 *.migrated)')
|
|
332
351
|
: null,
|
|
352
|
+
storage.warning ? h('div', { className: 'im-warn' }, storage.warning) : null,
|
|
333
353
|
error !== '' ? h('div', { className: 'im-err' }, error) : null,
|
|
334
354
|
error === '' && notice !== '' ? h('div', { className: 'im-ok' }, notice) : null,
|
|
335
355
|
error === '' && notice === '' && storage.error ? h('div', { className: 'im-err' }, storage.error) : null,
|
|
@@ -374,7 +394,13 @@ window.__ModuleLoader__.load({
|
|
|
374
394
|
h('button', {
|
|
375
395
|
className: 'im-btn im-btn-danger',
|
|
376
396
|
disabled: busy,
|
|
377
|
-
onClick: () =>
|
|
397
|
+
onClick: () => {
|
|
398
|
+
// confirm() is standard DOM, guarded anyway: if the embedded
|
|
399
|
+
// shell omits it, deletion proceeds rather than breaking.
|
|
400
|
+
if (typeof window.confirm === 'function'
|
|
401
|
+
&& !window.confirm('确定删除「' + (entry.title !== '' ? entry.title : '未命名指令') + '」?删除后无法恢复。')) return
|
|
402
|
+
send('delete-entry', { id: entry.id })
|
|
403
|
+
},
|
|
378
404
|
}, '删除')),
|
|
379
405
|
h('div', { className: 'im-body' }, entry.content),
|
|
380
406
|
entry.mode === 'auto' && entry.when !== ''
|
package/lib/index.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* plugin's fiber, so unmounting the row removes all of them.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises'
|
|
13
|
+
import { mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises'
|
|
14
14
|
import { homedir } from 'node:os'
|
|
15
15
|
import { dirname, join } from 'node:path'
|
|
16
16
|
|
|
@@ -38,6 +38,8 @@ const LEGACY_FILE_NAME = '.dsh-instruction-memory.json'
|
|
|
38
38
|
const SECTION_NAME = 'instruction-memory'
|
|
39
39
|
const SECTION_ORDER = 900
|
|
40
40
|
const API_PREFIX = '/instruction-memory/api'
|
|
41
|
+
/** Request body ceiling for the settings-page route; anything larger is rejected. */
|
|
42
|
+
const MAX_BODY_CHARS = 1_000_000
|
|
41
43
|
|
|
42
44
|
/**
|
|
43
45
|
* Resolve this plugin's data file under the harness home.
|
|
@@ -120,24 +122,43 @@ export function sanitizeEntry(raw) {
|
|
|
120
122
|
}
|
|
121
123
|
}
|
|
122
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Load a parsed memory file. Returns the sanitized data plus how many array
|
|
127
|
+
* items were discarded (blank entries, or valid ones past the cap). The count
|
|
128
|
+
* is surfaced rather than swallowed because the next save rewrites the file
|
|
129
|
+
* without those items — silently destroying user data is exactly what this
|
|
130
|
+
* plugin must never do.
|
|
131
|
+
*/
|
|
123
132
|
function normalizeData(raw) {
|
|
124
|
-
const
|
|
125
|
-
if (raw === null || typeof raw !== 'object') return
|
|
126
|
-
if (typeof raw.enabled === 'boolean')
|
|
127
|
-
|
|
133
|
+
const data = { version: 1, enabled: true, budgetChars: DEFAULT_BUDGET, entries: [] }
|
|
134
|
+
if (raw === null || typeof raw !== 'object') return { data, dropped: 0 }
|
|
135
|
+
if (typeof raw.enabled === 'boolean') data.enabled = raw.enabled
|
|
136
|
+
data.budgetChars = clampBudget(raw.budgetChars)
|
|
128
137
|
const list = Array.isArray(raw.entries) ? raw.entries : []
|
|
129
138
|
const seen = new Set()
|
|
139
|
+
let dropped = 0
|
|
130
140
|
for (const candidate of list) {
|
|
131
|
-
if (out.entries.length >= MAX_ENTRIES) break
|
|
132
141
|
const entry = sanitizeEntry(candidate)
|
|
133
|
-
if (entry === null)
|
|
142
|
+
if (entry === null) {
|
|
143
|
+
dropped += 1
|
|
144
|
+
continue
|
|
145
|
+
}
|
|
146
|
+
if (data.entries.length >= MAX_ENTRIES) {
|
|
147
|
+
dropped += 1
|
|
148
|
+
continue
|
|
149
|
+
}
|
|
134
150
|
if (seen.has(entry.id)) entry.id = makeId()
|
|
135
151
|
seen.add(entry.id)
|
|
136
|
-
|
|
152
|
+
data.entries.push(entry)
|
|
137
153
|
}
|
|
138
|
-
return
|
|
154
|
+
return { data, dropped }
|
|
139
155
|
}
|
|
140
156
|
|
|
157
|
+
const droppedEntriesMessage = (count) =>
|
|
158
|
+
count > 0
|
|
159
|
+
? '存储文件中有 ' + count + ' 条条目未能载入(内容为空或超过 ' + MAX_ENTRIES + ' 条上限);下次保存时它们将从文件中移除'
|
|
160
|
+
: null
|
|
161
|
+
|
|
141
162
|
function formatEntry(entry, tag) {
|
|
142
163
|
const lines = ['[' + tag + '|优先级 ' + priorityLabel(entry.priority) + '] ' + entry.title]
|
|
143
164
|
if (entry.mode === 'auto' && entry.when !== '') lines.push('适用场景:' + entry.when)
|
|
@@ -212,7 +233,13 @@ export function buildBlock(data) {
|
|
|
212
233
|
const room = budget - used - 2 - reserve
|
|
213
234
|
const note = '\n…(本条因注入字数上限被截断)'
|
|
214
235
|
if (room > note.length + 20) {
|
|
215
|
-
|
|
236
|
+
// Cut in UTF-16 units but back off one char when that lands between a
|
|
237
|
+
// surrogate pair, so the truncation point can never end on the broken
|
|
238
|
+
// half of an emoji or CJK ext-B character.
|
|
239
|
+
let head = block.slice(0, room - note.length)
|
|
240
|
+
const last = head.charCodeAt(head.length - 1)
|
|
241
|
+
if (last >= 0xd800 && last <= 0xdbff) head = head.slice(0, -1)
|
|
242
|
+
const body = head + note
|
|
216
243
|
blocks.push(body)
|
|
217
244
|
used += body.length + 2
|
|
218
245
|
return
|
|
@@ -241,6 +268,7 @@ export function apply(ctx) {
|
|
|
241
268
|
storagePath: null,
|
|
242
269
|
storageDisplay: null,
|
|
243
270
|
storageError: null,
|
|
271
|
+
storageWarning: null,
|
|
244
272
|
savedAt: null,
|
|
245
273
|
migratedFrom: null,
|
|
246
274
|
sectionRegistered: false,
|
|
@@ -318,9 +346,20 @@ export function apply(ctx) {
|
|
|
318
346
|
const serialize = () => JSON.stringify(state.data, null, 2) + '\n'
|
|
319
347
|
|
|
320
348
|
const writeToDisk = async () => {
|
|
349
|
+
// Atomic replace (write a sibling temp file, then rename): a crash
|
|
350
|
+
// mid-write must never leave a truncated memory.json behind, because the
|
|
351
|
+
// reader refuses to touch a file it cannot parse — half-written JSON
|
|
352
|
+
// would wedge every future boot until hand-repaired.
|
|
353
|
+
const tmpPath = storePath + '.tmp'
|
|
321
354
|
try {
|
|
322
355
|
await mkdir(dirname(storePath), { recursive: true })
|
|
323
|
-
await writeFile(
|
|
356
|
+
await writeFile(tmpPath, serialize(), 'utf8')
|
|
357
|
+
try {
|
|
358
|
+
await rename(tmpPath, storePath)
|
|
359
|
+
} catch (err) {
|
|
360
|
+
await unlink(tmpPath).catch(() => {})
|
|
361
|
+
throw err
|
|
362
|
+
}
|
|
324
363
|
state.storagePath = storePath
|
|
325
364
|
state.storageDisplay = storePath
|
|
326
365
|
state.storageError = null
|
|
@@ -340,11 +379,16 @@ export function apply(ctx) {
|
|
|
340
379
|
const readFromDisk = async () => {
|
|
341
380
|
state.storagePath = storePath
|
|
342
381
|
state.storageDisplay = storePath
|
|
382
|
+
state.storageWarning = null
|
|
343
383
|
|
|
344
384
|
// 1. The canonical location under the harness home.
|
|
345
385
|
try {
|
|
346
386
|
const parsed = await readStoreFile(storePath)
|
|
347
|
-
if (parsed !== null)
|
|
387
|
+
if (parsed !== null) {
|
|
388
|
+
const loaded = normalizeData(parsed)
|
|
389
|
+
state.data = loaded.data
|
|
390
|
+
state.storageWarning = droppedEntriesMessage(loaded.dropped)
|
|
391
|
+
}
|
|
348
392
|
state.storageError = null
|
|
349
393
|
return
|
|
350
394
|
} catch (err) {
|
|
@@ -363,7 +407,9 @@ export function apply(ctx) {
|
|
|
363
407
|
try {
|
|
364
408
|
const parsed = await readStoreFile(legacyPath)
|
|
365
409
|
if (parsed !== null) {
|
|
366
|
-
|
|
410
|
+
const loaded = normalizeData(parsed)
|
|
411
|
+
state.data = loaded.data
|
|
412
|
+
state.storageWarning = droppedEntriesMessage(loaded.dropped)
|
|
367
413
|
if (await writeToDisk()) {
|
|
368
414
|
state.migratedFrom = legacyPath
|
|
369
415
|
// Keep the original as evidence instead of deleting user data.
|
|
@@ -394,6 +440,21 @@ export function apply(ctx) {
|
|
|
394
440
|
syncSection()
|
|
395
441
|
})
|
|
396
442
|
|
|
443
|
+
/**
|
|
444
|
+
* All state-mutating work (edits and 重新载入) runs through this chain, so a
|
|
445
|
+
* save that is still flushing to disk can never interleave with a reload
|
|
446
|
+
* reading the file back — the old lost-update window in which the panel
|
|
447
|
+
* briefly resurrected data the user had just replaced. Each operation
|
|
448
|
+
* returns its full response payload, computed in the same tick as the
|
|
449
|
+
* mutation, so a snapshot can never straddle the next queued operation.
|
|
450
|
+
*/
|
|
451
|
+
let diskQueue = Promise.resolve()
|
|
452
|
+
const enqueue = (op) => {
|
|
453
|
+
const run = diskQueue.then(op, op)
|
|
454
|
+
diskQueue = run.then(() => undefined, () => undefined)
|
|
455
|
+
return run
|
|
456
|
+
}
|
|
457
|
+
|
|
397
458
|
/* ---------------- snapshots ---------------- */
|
|
398
459
|
|
|
399
460
|
const snapshot = () => {
|
|
@@ -408,6 +469,7 @@ export function apply(ctx) {
|
|
|
408
469
|
storage: {
|
|
409
470
|
path: state.storageDisplay !== null ? state.storageDisplay : state.storagePath,
|
|
410
471
|
error: state.storageError,
|
|
472
|
+
warning: state.storageWarning,
|
|
411
473
|
savedAt: state.savedAt,
|
|
412
474
|
migratedFrom: state.migratedFrom,
|
|
413
475
|
},
|
|
@@ -431,7 +493,7 @@ export function apply(ctx) {
|
|
|
431
493
|
|
|
432
494
|
/* ---------------- commands (shared by route and callers) ---------------- */
|
|
433
495
|
|
|
434
|
-
const saveEntry = async (
|
|
496
|
+
const saveEntry = (raw) => enqueue(async () => {
|
|
435
497
|
const entry = sanitizeEntry(raw)
|
|
436
498
|
if (entry === null) return { ok: false, saved: false, message: '标题与指令内容不能同时为空', snapshot: snapshot() }
|
|
437
499
|
entry.updatedAt = Date.now()
|
|
@@ -445,16 +507,23 @@ export function apply(ctx) {
|
|
|
445
507
|
list.push(entry)
|
|
446
508
|
}
|
|
447
509
|
syncSection()
|
|
448
|
-
|
|
449
|
-
|
|
510
|
+
const saved = await writeToDisk()
|
|
511
|
+
// A successful save has rewritten the file to match memory, so a
|
|
512
|
+
// dropped-entries warning from load time ("they will be removed on the
|
|
513
|
+
// next save") has played out and must not keep showing.
|
|
514
|
+
if (saved) state.storageWarning = null
|
|
515
|
+
return settle(null, saved)
|
|
516
|
+
})
|
|
450
517
|
|
|
451
|
-
const deleteEntry = async (
|
|
518
|
+
const deleteEntry = (id) => enqueue(async () => {
|
|
452
519
|
state.data.entries = state.data.entries.filter((entry) => entry.id !== id)
|
|
453
520
|
syncSection()
|
|
454
|
-
|
|
455
|
-
|
|
521
|
+
const saved = await writeToDisk()
|
|
522
|
+
if (saved) state.storageWarning = null
|
|
523
|
+
return settle(null, saved)
|
|
524
|
+
})
|
|
456
525
|
|
|
457
|
-
const setOptions = async (
|
|
526
|
+
const setOptions = (patch) => enqueue(async () => {
|
|
458
527
|
if (patch !== null && typeof patch === 'object') {
|
|
459
528
|
if (typeof patch.enabled === 'boolean') state.data.enabled = patch.enabled
|
|
460
529
|
if (typeof patch.budgetChars === 'number' && Number.isFinite(patch.budgetChars)) {
|
|
@@ -462,8 +531,16 @@ export function apply(ctx) {
|
|
|
462
531
|
}
|
|
463
532
|
}
|
|
464
533
|
syncSection()
|
|
465
|
-
|
|
466
|
-
|
|
534
|
+
const saved = await writeToDisk()
|
|
535
|
+
if (saved) state.storageWarning = null
|
|
536
|
+
return settle(null, saved)
|
|
537
|
+
})
|
|
538
|
+
|
|
539
|
+
const reload = () => enqueue(async () => {
|
|
540
|
+
await readFromDisk()
|
|
541
|
+
syncSection()
|
|
542
|
+
return { ok: state.storageError === null, saved: true, message: state.storageError, snapshot: snapshot() }
|
|
543
|
+
})
|
|
467
544
|
|
|
468
545
|
const dispatch = async (method, args) => {
|
|
469
546
|
await boot
|
|
@@ -479,9 +556,7 @@ export function apply(ctx) {
|
|
|
479
556
|
case 'set-options':
|
|
480
557
|
return setOptions(args)
|
|
481
558
|
case 'reload':
|
|
482
|
-
|
|
483
|
-
syncSection()
|
|
484
|
-
return { ok: state.storageError === null, saved: true, message: state.storageError, snapshot: snapshot() }
|
|
559
|
+
return reload()
|
|
485
560
|
default:
|
|
486
561
|
return { ok: false, saved: false, message: '未知方法:' + String(method), snapshot: snapshot() }
|
|
487
562
|
}
|
|
@@ -497,14 +572,33 @@ export function apply(ctx) {
|
|
|
497
572
|
res.end(JSON.stringify({ ok: false, message: 'only POST is supported' }))
|
|
498
573
|
return
|
|
499
574
|
}
|
|
575
|
+
// A call aborted mid-flight (settings page closed, tab navigated away)
|
|
576
|
+
// makes these streams emit 'error'. A server stream with no error
|
|
577
|
+
// listener turns that into an uncaught exception — i.e. it would take
|
|
578
|
+
// the whole host process down over one dead HTTP request.
|
|
579
|
+
req.on('error', () => {})
|
|
580
|
+
res.on('error', () => {})
|
|
500
581
|
let body = ''
|
|
582
|
+
// Over the ceiling we stop accumulating but keep draining, so the
|
|
583
|
+
// response can go out as a clean 413 instead of a destroyed socket the
|
|
584
|
+
// Client can only report as a generic network failure.
|
|
585
|
+
let overflow = false
|
|
501
586
|
req.setEncoding('utf8')
|
|
502
587
|
req.on('data', (chunk) => {
|
|
588
|
+
if (overflow) return
|
|
503
589
|
body += chunk
|
|
504
|
-
if (body.length >
|
|
590
|
+
if (body.length > MAX_BODY_CHARS) {
|
|
591
|
+
overflow = true
|
|
592
|
+
body = ''
|
|
593
|
+
}
|
|
505
594
|
})
|
|
506
595
|
req.on('end', () => {
|
|
507
596
|
void (async () => {
|
|
597
|
+
if (overflow) {
|
|
598
|
+
res.writeHead(413, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' })
|
|
599
|
+
res.end(JSON.stringify({ ok: false, message: '请求体超过 ' + MAX_BODY_CHARS + ' 字符上限' }))
|
|
600
|
+
return
|
|
601
|
+
}
|
|
508
602
|
let result
|
|
509
603
|
try {
|
|
510
604
|
const payload = body === '' ? {} : JSON.parse(body)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-instruction-memory",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "DSH 指令记忆:在设置界面中维护可增删改的长期指令,并自动注入此后每一轮对话的系统提示词。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -60,6 +60,9 @@
|
|
|
60
60
|
],
|
|
61
61
|
"platform": "web"
|
|
62
62
|
},
|
|
63
|
+
"engines": {
|
|
64
|
+
"dsh": ">=0.1.3-alpha.2 <0.1.4 || >=0.1.4-0 <0.1.5-0 || >=0.1.5-alpha.1"
|
|
65
|
+
},
|
|
63
66
|
"compatibility": {
|
|
64
67
|
"dsh": ">=0.1.3-alpha.2 <0.1.4 || >=0.1.4-0 <0.1.5-0 || >=0.1.5-alpha.1"
|
|
65
68
|
}
|