dsh-instruction-memory 1.0.5 → 1.0.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 +12 -2
- package/docs/screenshot.png +0 -0
- package/lib/client.js +109 -4
- package/lib/index.js +104 -4
- package/package.json +11 -5
package/README.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
在 DSH 设置界面中维护**长期指令记忆**:保存后自动注入此后每一轮对话的系统提示词。
|
|
4
4
|
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
> 想先看看界面?在 [GitHub 仓库](https://github.com/HERO476/dsh-instruction-memory)中打开 `docs/demo.html`,无需安装 DSH,浏览器直接体验真实设置页(内置演示数据)。
|
|
8
|
+
|
|
5
9
|
## 安装
|
|
6
10
|
|
|
7
11
|
```sh
|
|
@@ -32,6 +36,7 @@ dsh plugin --profile web remove dsh-instruction-memory
|
|
|
32
36
|
| `contract-test.mjs` | 契约测试:驱动真实路由,验证两半之间的响应格式 |
|
|
33
37
|
| `verify.mjs` | 装载验证:按 DSH 的方式加载两半 |
|
|
34
38
|
| `smoke-test.mjs` | 注入渲染逻辑的单元测试 |
|
|
39
|
+
| `docs/demo.html` | 离线演示页:桩掉 DSH 外壳、加载真实 Client 半渲染设置页(不随 npm 包发布) |
|
|
35
40
|
|
|
36
41
|
## 生效方式
|
|
37
42
|
|
|
@@ -58,8 +63,13 @@ DSH 就会得到第二份空记忆,看起来像"记忆消失"。
|
|
|
58
63
|
> 首次启动新版本时会自动迁移到上述位置,并把原文件改名为 `*.migrated` 保留。
|
|
59
64
|
|
|
60
65
|
该文件可手工编辑,改完点设置页的「重新载入」即可读回。每次保存先写临时文件再原子替换,
|
|
61
|
-
进程崩溃不会留下损坏的半截 `memory.json
|
|
62
|
-
|
|
66
|
+
进程崩溃不会留下损坏的半截 `memory.json`;保存时上一版会保留为 `memory.json.bak`,
|
|
67
|
+
若 `memory.json` 解析失败会自动临时回退到该备份并明确提示。若手工编辑后的文件有条目为空
|
|
68
|
+
或超过 200 条上限,设置页会明确提示哪些条目未能载入(它们将在下次保存时从文件中移除),
|
|
69
|
+
不会静默丢弃。
|
|
70
|
+
|
|
71
|
+
设置页支持**导出 / 导入**:导出为自带说明的 JSON 文件;导入采用**合并语义**——按 id 与
|
|
72
|
+
「标题+内容」去重,只新增、绝不覆盖或清空现有条目。
|
|
63
73
|
|
|
64
74
|
## 兼容性
|
|
65
75
|
|
|
Binary file
|
package/lib/client.js
CHANGED
|
@@ -71,6 +71,9 @@ window.__ModuleLoader__.load({
|
|
|
71
71
|
.im-dot-on { background:var(--dsw-alias-state-success-primary,#18a058); }
|
|
72
72
|
.im-dot-off { background:var(--dsw-alias-state-warn-primary,#d97706); }
|
|
73
73
|
.im-dot-err { background:var(--dsw-alias-state-error-primary,#d94838); }
|
|
74
|
+
.im-body-open { max-height:none; }
|
|
75
|
+
.im-more { margin-top:4px; font-size:11.5px; color:var(--dsw-alias-brand-primary,#3b6cff); cursor:pointer; user-select:none; }
|
|
76
|
+
.im-search-meta { margin-top:5px; }
|
|
74
77
|
@media (max-width:640px) { .im-grid2 { grid-template-columns:1fr; } }
|
|
75
78
|
`
|
|
76
79
|
|
|
@@ -193,6 +196,24 @@ window.__ModuleLoader__.load({
|
|
|
193
196
|
return h('div', { className: 'im-card' }, children)
|
|
194
197
|
}
|
|
195
198
|
|
|
199
|
+
/**
|
|
200
|
+
* Entry body with an explicit 展开/收起 toggle. The collapsed cap lives in
|
|
201
|
+
* CSS (max-height + overflow:hidden), so whether overflow actually happens
|
|
202
|
+
* is measured from the DOM — a short body renders no toggle at all.
|
|
203
|
+
*/
|
|
204
|
+
function EntryBody(props) {
|
|
205
|
+
const bodyRef = React.useRef(null)
|
|
206
|
+
const [overflowing, setOverflowing] = React.useState(false)
|
|
207
|
+
const [open, setOpen] = React.useState(false)
|
|
208
|
+
React.useEffect(() => {
|
|
209
|
+
const el = bodyRef.current
|
|
210
|
+
if (el !== null && el !== undefined) setOverflowing(el.scrollHeight > el.clientHeight + 1)
|
|
211
|
+
}, [props.text])
|
|
212
|
+
return h('div', null,
|
|
213
|
+
h('div', { className: 'im-body' + (open ? ' im-body-open' : ''), ref: bodyRef }, props.text),
|
|
214
|
+
overflowing ? h('div', { className: 'im-more', onClick: () => setOpen(!open) }, open ? '收起' : '展开全部') : null)
|
|
215
|
+
}
|
|
216
|
+
|
|
196
217
|
function InstructionPanel() {
|
|
197
218
|
const [view, setView] = React.useState(null)
|
|
198
219
|
const [error, setError] = React.useState('')
|
|
@@ -201,6 +222,8 @@ window.__ModuleLoader__.load({
|
|
|
201
222
|
const [draft, setDraft] = React.useState(null)
|
|
202
223
|
const [budgetText, setBudgetText] = React.useState('')
|
|
203
224
|
const [showPreview, setShowPreview] = React.useState(false)
|
|
225
|
+
const [query, setQuery] = React.useState('')
|
|
226
|
+
const importInputRef = React.useRef(null)
|
|
204
227
|
|
|
205
228
|
function absorb(result) {
|
|
206
229
|
const normalized = normalizeResult(result)
|
|
@@ -235,6 +258,50 @@ window.__ModuleLoader__.load({
|
|
|
235
258
|
}
|
|
236
259
|
}
|
|
237
260
|
|
|
261
|
+
async function doExport() {
|
|
262
|
+
try {
|
|
263
|
+
const result = await callHost('export-data', null)
|
|
264
|
+
if (result === null || typeof result !== 'object' || result.export === undefined || result.export === null) {
|
|
265
|
+
setError('导出失败:宿主返回了无法识别的数据')
|
|
266
|
+
return
|
|
267
|
+
}
|
|
268
|
+
const blob = new Blob([JSON.stringify(result.export, null, 2) + '\n'], { type: 'application/json' })
|
|
269
|
+
const url = URL.createObjectURL(blob)
|
|
270
|
+
const link = document.createElement('a')
|
|
271
|
+
link.href = url
|
|
272
|
+
link.download = 'dsh-instruction-memory-' + new Date().toISOString().slice(0, 10) + '.json'
|
|
273
|
+
document.body.appendChild(link)
|
|
274
|
+
link.click()
|
|
275
|
+
if (link.parentNode !== null) link.parentNode.removeChild(link)
|
|
276
|
+
setTimeout(() => { URL.revokeObjectURL(url) }, 1000)
|
|
277
|
+
} catch (err) {
|
|
278
|
+
setError(String(err && err.message ? err.message : err))
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async function handleImportFile(file) {
|
|
283
|
+
try {
|
|
284
|
+
const text = typeof file.text === 'function'
|
|
285
|
+
? await file.text()
|
|
286
|
+
: await new Promise((resolve, reject) => {
|
|
287
|
+
const reader = new FileReader()
|
|
288
|
+
reader.onload = () => resolve(String(reader.result))
|
|
289
|
+
reader.onerror = () => reject(reader.error)
|
|
290
|
+
reader.readAsText(file)
|
|
291
|
+
})
|
|
292
|
+
let payload
|
|
293
|
+
try {
|
|
294
|
+
payload = JSON.parse(text)
|
|
295
|
+
} catch (err) {
|
|
296
|
+
setError('导入文件不是有效的 JSON(' + String(err && err.message ? err.message : err) + ')')
|
|
297
|
+
return
|
|
298
|
+
}
|
|
299
|
+
await send('import-data', { payload })
|
|
300
|
+
} catch (err) {
|
|
301
|
+
setError(String(err && err.message ? err.message : err))
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
238
305
|
React.useEffect(() => {
|
|
239
306
|
let alive = true
|
|
240
307
|
callHost('state', null).then(
|
|
@@ -276,6 +343,13 @@ window.__ModuleLoader__.load({
|
|
|
276
343
|
const injection = view.injection || { registered: false, chars: 0, error: null }
|
|
277
344
|
const entries = data.entries.slice().sort((a, b) => (b.priority - a.priority) || (b.updatedAt - a.updatedAt))
|
|
278
345
|
const activeCount = entries.filter((e) => e.enabled && e.content !== '').length
|
|
346
|
+
const keyword = query.trim().toLowerCase()
|
|
347
|
+
const visible = keyword === ''
|
|
348
|
+
? entries
|
|
349
|
+
: entries.filter((entry) =>
|
|
350
|
+
entry.title.toLowerCase().indexOf(keyword) !== -1
|
|
351
|
+
|| entry.content.toLowerCase().indexOf(keyword) !== -1
|
|
352
|
+
|| entry.when.toLowerCase().indexOf(keyword) !== -1)
|
|
279
353
|
|
|
280
354
|
const dotClass = injection.error
|
|
281
355
|
? 'im-dot im-dot-err'
|
|
@@ -328,6 +402,14 @@ window.__ModuleLoader__.load({
|
|
|
328
402
|
h('label', { className: 'im-label' }, '生效规则'),
|
|
329
403
|
h('div', { className: 'im-meta' },
|
|
330
404
|
'「始终」无条件生效;「按需」由模型按相关性自动判断。超出上限时按优先级保留,其余暂不注入。'))),
|
|
405
|
+
entries.length > 0 ? h('div', { key: 'search' },
|
|
406
|
+
h('input', {
|
|
407
|
+
className: 'im-input',
|
|
408
|
+
value: query,
|
|
409
|
+
placeholder: '搜索指令:标题 / 内容 / 适用场景(共 ' + entries.length + ' 条)',
|
|
410
|
+
onChange: (ev) => setQuery(ev.target.value),
|
|
411
|
+
}),
|
|
412
|
+
query.trim() !== '' ? h('div', { className: 'im-meta im-search-meta' }, '匹配 ' + visible.length + ' / ' + entries.length + ' 条') : null) : null,
|
|
331
413
|
h('div', { className: 'im-row-wrap' },
|
|
332
414
|
h('button', {
|
|
333
415
|
className: 'im-btn im-btn-primary',
|
|
@@ -335,6 +417,15 @@ window.__ModuleLoader__.load({
|
|
|
335
417
|
onClick: () => { setDraft(blankDraft()); setError('') },
|
|
336
418
|
}, '+ 新增指令'),
|
|
337
419
|
h('button', { className: 'im-btn', disabled: busy, onClick: () => send('reload', null) }, '重新载入'),
|
|
420
|
+
h('button', { className: 'im-btn', disabled: busy, onClick: doExport }, '导出'),
|
|
421
|
+
h('button', {
|
|
422
|
+
className: 'im-btn',
|
|
423
|
+
disabled: busy,
|
|
424
|
+
onClick: () => {
|
|
425
|
+
const el = importInputRef.current
|
|
426
|
+
if (el !== null && el !== undefined) el.click()
|
|
427
|
+
},
|
|
428
|
+
}, '导入'),
|
|
338
429
|
h('button', {
|
|
339
430
|
className: 'im-btn',
|
|
340
431
|
disabled: busy,
|
|
@@ -355,9 +446,21 @@ window.__ModuleLoader__.load({
|
|
|
355
446
|
error === '' && notice === '' && storage.error ? h('div', { className: 'im-err' }, storage.error) : null,
|
|
356
447
|
showPreview ? h('div', null,
|
|
357
448
|
h('label', { className: 'im-label' }, '当前注入到每轮系统提示词的内容'),
|
|
358
|
-
h('pre', { className: 'im-pre' }, view.preview === '' ? '(当前没有生效中的指令)' : view.preview)) : null
|
|
449
|
+
h('pre', { className: 'im-pre' }, view.preview === '' ? '(当前没有生效中的指令)' : view.preview)) : null,
|
|
450
|
+
h('input', {
|
|
451
|
+
key: 'import-input',
|
|
452
|
+
type: 'file',
|
|
453
|
+
accept: '.json,application/json',
|
|
454
|
+
style: { display: 'none' },
|
|
455
|
+
ref: importInputRef,
|
|
456
|
+
onChange: (ev) => {
|
|
457
|
+
const file = ev.target.files && ev.target.files.length > 0 ? ev.target.files[0] : null
|
|
458
|
+
ev.target.value = ''
|
|
459
|
+
if (file !== null) void handleImportFile(file)
|
|
460
|
+
},
|
|
461
|
+
}))
|
|
359
462
|
|
|
360
|
-
const items =
|
|
463
|
+
const items = visible.map((entry) => {
|
|
361
464
|
const tags = [
|
|
362
465
|
h('span', {
|
|
363
466
|
className: 'im-tag ' + (entry.mode === 'always' ? 'im-tag-always' : 'im-tag-auto'),
|
|
@@ -402,7 +505,7 @@ window.__ModuleLoader__.load({
|
|
|
402
505
|
send('delete-entry', { id: entry.id })
|
|
403
506
|
},
|
|
404
507
|
}, '删除')),
|
|
405
|
-
h(
|
|
508
|
+
h(EntryBody, { text: entry.content, key: 'body' }),
|
|
406
509
|
entry.mode === 'auto' && entry.when !== ''
|
|
407
510
|
? h('div', { className: 'im-meta' }, '适用场景:' + entry.when)
|
|
408
511
|
: null)
|
|
@@ -411,7 +514,9 @@ window.__ModuleLoader__.load({
|
|
|
411
514
|
const list = entries.length === 0
|
|
412
515
|
? h('div', { className: 'im-empty', key: 'empty' },
|
|
413
516
|
'还没有指令记忆。点击「+ 新增指令」添加第一条,例如“所有回答使用简体中文”。')
|
|
414
|
-
:
|
|
517
|
+
: visible.length === 0
|
|
518
|
+
? h('div', { className: 'im-empty', key: 'no-match' }, '没有匹配“' + query.trim() + '”的指令。')
|
|
519
|
+
: h('div', { className: 'im-list', key: 'list' }, items)
|
|
415
520
|
|
|
416
521
|
return h('div', { className: 'im-root' }, header, list)
|
|
417
522
|
}
|
package/lib/index.js
CHANGED
|
@@ -346,14 +346,21 @@ export function apply(ctx) {
|
|
|
346
346
|
const serialize = () => JSON.stringify(state.data, null, 2) + '\n'
|
|
347
347
|
|
|
348
348
|
const writeToDisk = async () => {
|
|
349
|
-
// Atomic replace (write a sibling temp file, then rename)
|
|
350
|
-
// mid-write must never leave a truncated
|
|
351
|
-
//
|
|
352
|
-
//
|
|
349
|
+
// Atomic replace (write a sibling temp file, then rename) with a rolling
|
|
350
|
+
// one-version backup: a crash mid-write must never leave a truncated
|
|
351
|
+
// memory.json, and an edit the user regrets must still be recoverable
|
|
352
|
+
// from memory.json.bak. The .bak holds the previous good version.
|
|
353
353
|
const tmpPath = storePath + '.tmp'
|
|
354
|
+
const bakPath = storePath + '.bak'
|
|
354
355
|
try {
|
|
355
356
|
await mkdir(dirname(storePath), { recursive: true })
|
|
356
357
|
await writeFile(tmpPath, serialize(), 'utf8')
|
|
358
|
+
try {
|
|
359
|
+
await rename(storePath, bakPath)
|
|
360
|
+
} catch (err) {
|
|
361
|
+
// No previous version on the very first save; anything else is real.
|
|
362
|
+
if (err.code !== 'ENOENT') throw err
|
|
363
|
+
}
|
|
357
364
|
try {
|
|
358
365
|
await rename(tmpPath, storePath)
|
|
359
366
|
} catch (err) {
|
|
@@ -393,6 +400,26 @@ export function apply(ctx) {
|
|
|
393
400
|
return
|
|
394
401
|
} catch (err) {
|
|
395
402
|
if (err === null || err === undefined || err.code !== 'ENOENT') {
|
|
403
|
+
// The main file is unreadable or corrupt. A previous good version
|
|
404
|
+
// may survive in the rolling backup — recover from it rather than
|
|
405
|
+
// presenting an empty memory, but say so loudly. The corrupt file
|
|
406
|
+
// itself stays untouched for hand repair; it gets overwritten on the
|
|
407
|
+
// next save (the pre-existing and disclosed behaviour).
|
|
408
|
+
if (err instanceof SyntaxError) {
|
|
409
|
+
try {
|
|
410
|
+
const backup = await readStoreFile(storePath + '.bak')
|
|
411
|
+
if (backup !== null) {
|
|
412
|
+
const loaded = normalizeData(backup)
|
|
413
|
+
state.data = loaded.data
|
|
414
|
+
state.storageWarning = (droppedEntriesMessage(loaded.dropped) || '')
|
|
415
|
+
+ (loaded.dropped > 0 ? ';' : '')
|
|
416
|
+
+ 'memory.json 解析失败(' + describeError(err) + '),已临时回退到最近备份 memory.json.bak;损坏文件已保留,下次保存时会覆盖它'
|
|
417
|
+
return
|
|
418
|
+
}
|
|
419
|
+
} catch {
|
|
420
|
+
// No usable backup either; fall through to the error below.
|
|
421
|
+
}
|
|
422
|
+
}
|
|
396
423
|
// A file we cannot understand must never be silently overwritten.
|
|
397
424
|
state.storageError = err instanceof SyntaxError
|
|
398
425
|
? '配置文件解析失败(' + describeError(err) + '),原文件已保留;你下次保存时会覆盖它'
|
|
@@ -542,6 +569,63 @@ export function apply(ctx) {
|
|
|
542
569
|
return { ok: state.storageError === null, saved: true, message: state.storageError, snapshot: snapshot() }
|
|
543
570
|
})
|
|
544
571
|
|
|
572
|
+
const exportData = () => enqueue(async () => ({
|
|
573
|
+
ok: true,
|
|
574
|
+
saved: true,
|
|
575
|
+
message: null,
|
|
576
|
+
// A self-describing envelope: re-importable by this plugin, and readable
|
|
577
|
+
// as plain JSON by anything else.
|
|
578
|
+
export: { app: 'dsh-instruction-memory', version: 1, exportedAt: new Date().toISOString(), data: snapshot().data },
|
|
579
|
+
snapshot: snapshot(),
|
|
580
|
+
}))
|
|
581
|
+
|
|
582
|
+
const importData = (payload) => enqueue(async () => {
|
|
583
|
+
if (payload === null || typeof payload !== 'object') {
|
|
584
|
+
return { ok: false, saved: false, message: '导入内容不是有效的 JSON 对象', snapshot: snapshot() }
|
|
585
|
+
}
|
|
586
|
+
// Accept both the plugin's own export envelope ({app, data:{entries}}) and
|
|
587
|
+
// a bare {entries} array (e.g. a hand-assembled file).
|
|
588
|
+
const rawEntries = Array.isArray(payload.entries)
|
|
589
|
+
? payload.entries
|
|
590
|
+
: (payload.data !== null && typeof payload.data === 'object' && Array.isArray(payload.data.entries)
|
|
591
|
+
? payload.data.entries
|
|
592
|
+
: null)
|
|
593
|
+
if (rawEntries === null || rawEntries.length === 0) {
|
|
594
|
+
return { ok: false, saved: false, message: '导入文件中没有找到条目', snapshot: snapshot() }
|
|
595
|
+
}
|
|
596
|
+
// Merge, never replace: importing must not be able to wipe what is here.
|
|
597
|
+
// Duplicates (same id, or same title+content) and invalid items are
|
|
598
|
+
// skipped and counted, not silently dropped.
|
|
599
|
+
const existing = state.data.entries
|
|
600
|
+
const seenIds = new Set(existing.map((entry) => entry.id))
|
|
601
|
+
const seenBodies = new Set(existing.map((entry) => entry.title + '\u0000' + entry.content))
|
|
602
|
+
let imported = 0
|
|
603
|
+
let skipped = 0
|
|
604
|
+
for (const candidate of rawEntries) {
|
|
605
|
+
const entry = sanitizeEntry(candidate)
|
|
606
|
+
if (entry === null || seenIds.has(entry.id) || seenBodies.has(entry.title + '\u0000' + entry.content)) {
|
|
607
|
+
skipped += 1
|
|
608
|
+
continue
|
|
609
|
+
}
|
|
610
|
+
if (existing.length >= MAX_ENTRIES) {
|
|
611
|
+
skipped += 1
|
|
612
|
+
continue
|
|
613
|
+
}
|
|
614
|
+
entry.updatedAt = Date.now()
|
|
615
|
+
existing.push(entry)
|
|
616
|
+
seenIds.add(entry.id)
|
|
617
|
+
seenBodies.add(entry.title + '\u0000' + entry.content)
|
|
618
|
+
imported += 1
|
|
619
|
+
}
|
|
620
|
+
if (imported === 0) {
|
|
621
|
+
return { ok: true, saved: true, message: '没有新增条目:' + skipped + ' 条已存在或无效', snapshot: snapshot() }
|
|
622
|
+
}
|
|
623
|
+
syncSection()
|
|
624
|
+
const saved = await writeToDisk()
|
|
625
|
+
if (saved) state.storageWarning = null
|
|
626
|
+
return settle('导入完成:新增 ' + imported + ' 条,跳过 ' + skipped + ' 条(已存在或无效)', saved)
|
|
627
|
+
})
|
|
628
|
+
|
|
545
629
|
const dispatch = async (method, args) => {
|
|
546
630
|
await boot
|
|
547
631
|
switch (method) {
|
|
@@ -557,6 +641,10 @@ export function apply(ctx) {
|
|
|
557
641
|
return setOptions(args)
|
|
558
642
|
case 'reload':
|
|
559
643
|
return reload()
|
|
644
|
+
case 'export-data':
|
|
645
|
+
return exportData()
|
|
646
|
+
case 'import-data':
|
|
647
|
+
return importData(args !== null && typeof args === 'object' ? args.payload : null)
|
|
560
648
|
default:
|
|
561
649
|
return { ok: false, saved: false, message: '未知方法:' + String(method), snapshot: snapshot() }
|
|
562
650
|
}
|
|
@@ -572,6 +660,18 @@ export function apply(ctx) {
|
|
|
572
660
|
res.end(JSON.stringify({ ok: false, message: 'only POST is supported' }))
|
|
573
661
|
return
|
|
574
662
|
}
|
|
663
|
+
// The settings page always posts application/json. Enforcing it turns
|
|
664
|
+
// the anti-CSRF coincidence (a plain HTML form cannot fabricate a JSON
|
|
665
|
+
// body) into a hard rule: a cross-site or malformed post now gets a
|
|
666
|
+
// readable 415 instead of a confusing parse failure.
|
|
667
|
+
const contentType = String(
|
|
668
|
+
(req.headers !== null && typeof req.headers === 'object') ? (req.headers['content-type'] || '') : '',
|
|
669
|
+
)
|
|
670
|
+
if (contentType.indexOf('application/json') === -1) {
|
|
671
|
+
res.writeHead(415, { 'content-type': 'application/json; charset=utf-8' })
|
|
672
|
+
res.end(JSON.stringify({ ok: false, message: 'content-type 必须是 application/json' }))
|
|
673
|
+
return
|
|
674
|
+
}
|
|
575
675
|
// A call aborted mid-flight (settings page closed, tab navigated away)
|
|
576
676
|
// makes these streams emit 'error'. A server stream with no error
|
|
577
677
|
// listener turns that into an uncaught exception — i.e. it would take
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-instruction-memory",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "DSH
|
|
3
|
+
"version": "1.0.7",
|
|
4
|
+
"description": "User-only instruction memory for DSH: long-term instructions maintained in the settings UI and auto-injected into every conversation — the model has no write access. 指令记忆:只由用户在设置页维护的长期指令,自动注入此后每轮对话;模型没有任何写入口。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"repository": {
|
|
@@ -27,16 +27,22 @@
|
|
|
27
27
|
"lib",
|
|
28
28
|
"cordis.patch.yml",
|
|
29
29
|
"README.md",
|
|
30
|
-
"LICENSE"
|
|
30
|
+
"LICENSE",
|
|
31
|
+
"docs/screenshot.png"
|
|
31
32
|
],
|
|
32
33
|
"keywords": [
|
|
33
34
|
"dsh",
|
|
34
35
|
"dsh-plugin",
|
|
35
36
|
"deepseek-harness",
|
|
36
|
-
"cordis",
|
|
37
37
|
"instruction-memory",
|
|
38
|
+
"memory",
|
|
39
|
+
"记忆",
|
|
40
|
+
"instructions",
|
|
41
|
+
"custom-instructions",
|
|
42
|
+
"persistent-instructions",
|
|
38
43
|
"system-prompt",
|
|
39
|
-
"settings"
|
|
44
|
+
"settings",
|
|
45
|
+
"cordis"
|
|
40
46
|
],
|
|
41
47
|
"engines": {
|
|
42
48
|
"node": ">=20"
|