dsh-custom-mode 1.1.0 → 1.2.0
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/client.js +79 -0
- package/index.mjs +21 -0
- package/journal.mjs +157 -0
- package/locales.mjs +14 -0
- package/package.json +2 -1
package/client.js
CHANGED
|
@@ -92,6 +92,13 @@ try {
|
|
|
92
92
|
"msg.reorderFailed": "调整顺序失败",
|
|
93
93
|
"msg.imported": "已导入到编辑器(还没有保存):检查后点「保存」。",
|
|
94
94
|
"msg.importFailed": "导入失败",
|
|
95
|
+
"history.label": "改动历史",
|
|
96
|
+
"history.pick": "选择要载入的版本…",
|
|
97
|
+
"history.load": "载入这一版",
|
|
98
|
+
"history.hint": "每次保存、以及会话内工具或手工改动,都会在这里留一版;载入只改草稿,保存前不落盘。",
|
|
99
|
+
"history.by.settings": "设置页保存",
|
|
100
|
+
"history.by.external": "会话内/手工改动",
|
|
101
|
+
"msg.loadFailed": "载入这一版失败",
|
|
95
102
|
"msg.importEmpty": "这个文件是空的。",
|
|
96
103
|
"msg.importTooLarge": "文件太大(上限 1MB)。",
|
|
97
104
|
"msg.exported": "已导出为文件。",
|
|
@@ -224,6 +231,13 @@ try {
|
|
|
224
231
|
"msg.reorderFailed": "Could not reorder",
|
|
225
232
|
"msg.imported": "Imported into the editor (not saved yet) — review it, then click Save.",
|
|
226
233
|
"msg.importFailed": "Import failed",
|
|
234
|
+
"history.label": "Change history",
|
|
235
|
+
"history.pick": "Pick a version to load…",
|
|
236
|
+
"history.load": "Load this version",
|
|
237
|
+
"history.hint": "Every save — plus changes made in a session or by hand — leaves a version here. Loading one only edits the draft; nothing is written until you save.",
|
|
238
|
+
"history.by.settings": "saved from this page",
|
|
239
|
+
"history.by.external": "changed in a session / by hand",
|
|
240
|
+
"msg.loadFailed": "Could not load that version",
|
|
227
241
|
"msg.importEmpty": "That file is empty.",
|
|
228
242
|
"msg.importTooLarge": "That file is too large (1 MB limit).",
|
|
229
243
|
"msg.exported": "Exported to a file.",
|
|
@@ -535,6 +549,7 @@ try {
|
|
|
535
549
|
const ROUTES = {
|
|
536
550
|
list: ROUTE,
|
|
537
551
|
state: ROUTE + "/state",
|
|
552
|
+
history: ROUTE + "/history",
|
|
538
553
|
create: ROUTE + "/create",
|
|
539
554
|
delete: ROUTE + "/delete",
|
|
540
555
|
reorder: ROUTE + "/reorder",
|
|
@@ -576,6 +591,13 @@ try {
|
|
|
576
591
|
}
|
|
577
592
|
|
|
578
593
|
/** The editable draft for one assistant, derived from its loaded state. */
|
|
594
|
+
/** 时间戳给人看:转本地时间;解析不了就原样显示(历史文件是纯文本,什么都有可能)。 */
|
|
595
|
+
function formatWhen(at) {
|
|
596
|
+
const parsed = new Date(at)
|
|
597
|
+
if (Number.isNaN(parsed.getTime())) return String(at).slice(0, 16)
|
|
598
|
+
return parsed.toLocaleString()
|
|
599
|
+
}
|
|
600
|
+
|
|
579
601
|
function draftOf(state) {
|
|
580
602
|
return {
|
|
581
603
|
id: state.id,
|
|
@@ -587,6 +609,9 @@ try {
|
|
|
587
609
|
// 「恢复出厂提示词」要用它。它不是草稿的一部分,`sameDraft` 不比较它 ——
|
|
588
610
|
// 漏掉这一行按钮会一直置灰(实测:真点了没反应,浏览器验收抓到)。
|
|
589
611
|
factoryPrompt: typeof state.factoryPrompt === "string" ? state.factoryPrompt : null,
|
|
612
|
+
// 改动历史:列表来自 state(只有元数据),正文点「载入这一版」时按需取。
|
|
613
|
+
history: Array.isArray(state.history) ? state.history : [],
|
|
614
|
+
historyPick: "",
|
|
590
615
|
}
|
|
591
616
|
}
|
|
592
617
|
|
|
@@ -915,6 +940,24 @@ try {
|
|
|
915
940
|
update({ prompt: factory })
|
|
916
941
|
}
|
|
917
942
|
|
|
943
|
+
/** 把某个历史版本载入编辑器。与「恢复出厂」同一条纪律:只改草稿,保存前不落盘。 */
|
|
944
|
+
const loadVersion = async () => {
|
|
945
|
+
const picked = typeof draft.historyPick === "string" ? draft.historyPick : ""
|
|
946
|
+
if (picked === "" || draft.id === undefined) return
|
|
947
|
+
try {
|
|
948
|
+
// 版本键是序号(时间戳会在同一毫秒内撞车 —— 单测就是那样抓到它的)。
|
|
949
|
+
const response = await fetch(ROUTES.history + "?id=" + encodeURIComponent(draft.id) + "&n=" + encodeURIComponent(picked))
|
|
950
|
+
const payload = await asJson(response)
|
|
951
|
+
if (payload === null || payload.ok !== true) {
|
|
952
|
+
setStatus(t("msg.loadFailed"))
|
|
953
|
+
return
|
|
954
|
+
}
|
|
955
|
+
update({ prompt: payload.text })
|
|
956
|
+
} catch (error) {
|
|
957
|
+
setStatus(t("msg.loadFailed") + ":" + describeError(error))
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
918
961
|
const exportPrompt = () => {
|
|
919
962
|
try {
|
|
920
963
|
const blob = new Blob([draft.prompt], { type: "text/markdown;charset=utf-8" })
|
|
@@ -1314,6 +1357,42 @@ try {
|
|
|
1314
1357
|
"aria-label": t("prompt.heading"),
|
|
1315
1358
|
onChange: (event) => update({ prompt: event.target.value }),
|
|
1316
1359
|
}),
|
|
1360
|
+
// 改动历史:谁在什么时候改过。之前这里只有"当前文本",所以会话内的工具
|
|
1361
|
+
// (或手工编辑)改掉提示词时,用户既看不见也回不去。
|
|
1362
|
+
draft.history.length === 0
|
|
1363
|
+
? null
|
|
1364
|
+
: react.createElement(
|
|
1365
|
+
"div",
|
|
1366
|
+
{ className: "cpfe-history" },
|
|
1367
|
+
react.createElement("span", { className: "cpfe-history-label" }, t("history.label")),
|
|
1368
|
+
react.createElement(
|
|
1369
|
+
"select",
|
|
1370
|
+
{
|
|
1371
|
+
value: draft.historyPick,
|
|
1372
|
+
"aria-label": t("history.label"),
|
|
1373
|
+
onChange: (event) => update({ historyPick: event.target.value }),
|
|
1374
|
+
},
|
|
1375
|
+
react.createElement("option", { value: "" }, t("history.pick")),
|
|
1376
|
+
...draft.history.map((entry) =>
|
|
1377
|
+
react.createElement(
|
|
1378
|
+
"option",
|
|
1379
|
+
{ key: String(entry.n), value: String(entry.n) },
|
|
1380
|
+
formatWhen(entry.at) + " · " + t("history.by." + entry.by) + " · " + String(entry.bytes) + " B",
|
|
1381
|
+
),
|
|
1382
|
+
),
|
|
1383
|
+
),
|
|
1384
|
+
react.createElement(
|
|
1385
|
+
A.Button,
|
|
1386
|
+
{
|
|
1387
|
+
variant: "outline",
|
|
1388
|
+
size: "sm",
|
|
1389
|
+
disabled: busy || draft.historyPick === "",
|
|
1390
|
+
onClick: loadVersion,
|
|
1391
|
+
},
|
|
1392
|
+
t("history.load"),
|
|
1393
|
+
),
|
|
1394
|
+
react.createElement("span", { className: "cpfe-history-hint" }, t("history.hint")),
|
|
1395
|
+
),
|
|
1317
1396
|
),
|
|
1318
1397
|
]
|
|
1319
1398
|
: []
|
package/index.mjs
CHANGED
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
setShippedPresetsDir,
|
|
52
52
|
} from './composition.mjs'
|
|
53
53
|
import { readPresetMeta, writePresetMeta, presetMetaPath, PRESET_META_PATH } from './meta.mjs'
|
|
54
|
+
import { listHistory, readVersion, recordExternalChange, recordPrompt, HISTORY_SOURCE } from './journal.mjs'
|
|
54
55
|
import { packagedPresetDir } from './seed.mjs'
|
|
55
56
|
import {
|
|
56
57
|
allocateId,
|
|
@@ -70,11 +71,13 @@ const STATE_PATH = ROUTE_PATH + '/state'
|
|
|
70
71
|
const CREATE_PATH = ROUTE_PATH + '/create'
|
|
71
72
|
const DELETE_PATH = ROUTE_PATH + '/delete'
|
|
72
73
|
const REORDER_PATH = ROUTE_PATH + '/reorder'
|
|
74
|
+
const HISTORY_PATH = ROUTE_PATH + '/history'
|
|
73
75
|
|
|
74
76
|
/** Which verbs each endpoint answers. `undefined` for a path means 404. */
|
|
75
77
|
const METHODS = {
|
|
76
78
|
[ROUTE_PATH]: ['GET'],
|
|
77
79
|
[STATE_PATH]: ['GET', 'POST'],
|
|
80
|
+
[HISTORY_PATH]: ['GET'],
|
|
78
81
|
[CREATE_PATH]: ['POST'],
|
|
79
82
|
[DELETE_PATH]: ['POST'],
|
|
80
83
|
[REORDER_PATH]: ['POST'],
|
|
@@ -217,6 +220,9 @@ export function readState(rows, id, options = {}) {
|
|
|
217
220
|
const mode = modeOf(text)
|
|
218
221
|
const prompt = readPrompt(directory)
|
|
219
222
|
const meta = readPresetMeta(directory)
|
|
223
|
+
// 页面要打开时顺便对账:磁盘上的文本若与日志末条不同,说明它在设置页之外被改过
|
|
224
|
+
// (会话内的 custom_prompt 工具、手工编辑、别处同步)—— 补记一条 external,于是"被改过"看得见。
|
|
225
|
+
if (prompt.ok === true) recordExternalChange(directory, prompt.text)
|
|
220
226
|
return {
|
|
221
227
|
ok: true,
|
|
222
228
|
id,
|
|
@@ -234,6 +240,8 @@ export function readState(rows, id, options = {}) {
|
|
|
234
240
|
// 回退用的出厂文本。它不是"当前值",页面只把它填进编辑器,保存前不落盘 —— 所以
|
|
235
241
|
// 一次误点不会破坏任何东西(重新读取即可丢弃)。
|
|
236
242
|
factoryPrompt: typeof options.factoryPrompt === 'string' ? options.factoryPrompt : null,
|
|
243
|
+
// 改动历史(只有元数据,正文按需取:见 GET /custom-mode/history)。
|
|
244
|
+
history: listHistory(directory),
|
|
237
245
|
}
|
|
238
246
|
}
|
|
239
247
|
|
|
@@ -311,6 +319,9 @@ export function saveState(rows, input) {
|
|
|
311
319
|
try {
|
|
312
320
|
writeAtomic(compositionFile(directory), composition)
|
|
313
321
|
writeAtomic(promptFile(directory), prompt)
|
|
322
|
+
// 改动留痕:三个改动路径(设置页 / 会话内工具 / 手工编辑)里,只有设置页是"当场知道"的。
|
|
323
|
+
// 另外两条由 readState 对比补记(见 journal.mjs 的单写者说明)。
|
|
324
|
+
recordPrompt(directory, prompt, HISTORY_SOURCE.settings)
|
|
314
325
|
} catch (error) {
|
|
315
326
|
return { ok: false, error: '写入失败:' + describe(error) }
|
|
316
327
|
}
|
|
@@ -601,6 +612,16 @@ export function apply(ctx) {
|
|
|
601
612
|
return json(readState(await roster(), url.searchParams.get('id') ?? '', { factoryPrompt: packagedPrompt() }))
|
|
602
613
|
}
|
|
603
614
|
|
|
615
|
+
if (request.method === 'GET' && pathname === HISTORY_PATH) {
|
|
616
|
+
await ensureShipped()
|
|
617
|
+
const id = url.searchParams.get('id') ?? ''
|
|
618
|
+
const directory = assistantDir(await roster(), id)
|
|
619
|
+
if (directory === undefined) return json(unknownAssistant(id), 404)
|
|
620
|
+
const text = readVersion(directory, url.searchParams.get('n') ?? '')
|
|
621
|
+
if (text === null) return json({ ok: false, error: '找不到这个版本(历史可能已被上限裁剪)。' }, 404)
|
|
622
|
+
return json({ ok: true, id, n: url.searchParams.get('n'), text })
|
|
623
|
+
}
|
|
624
|
+
|
|
604
625
|
// Backstop on request size. `requestBody: 'buffered'` means the platform applies its own
|
|
605
626
|
// JSON cap, but that cap is the host's configuration, not a contract — measured on Windows,
|
|
606
627
|
// a 5 MB body reached us happily. This keeps one authenticated request from making us buffer
|
package/journal.mjs
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 提示词改动日志:**只追加**、单写者、可审计。
|
|
3
|
+
*
|
|
4
|
+
* 为什么需要它:一个助手的 `prompt.md` 有三条改动路径 —— 设置页保存、会话内的 `custom_prompt`
|
|
5
|
+
* 工具、以及手工编辑文件。后两条对用户是**不可见**的:提示词变了,页面只会显示"当前文本",
|
|
6
|
+
* 没有任何地方能看出"谁在什么时候改的、上一版是什么"。这个模块把每一次变化留成一行,于是
|
|
7
|
+
* "恢复出厂"之外还能"回到上一版",会话内被改动也能被看见。
|
|
8
|
+
*
|
|
9
|
+
* 设计取舍(都写在这里,因为它们看起来可以更简单):
|
|
10
|
+
*
|
|
11
|
+
* 1. **单写者**:只有宿主半写这个文件。会话内的工具**不**直接写日志 —— 否则文件格式就得在
|
|
12
|
+
* preset 副本里再实现一遍(那些文件是独立分发的)。改为在读状态时对比"当前文本 vs 日志末条",
|
|
13
|
+
* 不一致就补记一条 `external`:工具改的、手工改的、甚至别的机器同步过来的,都会以同一种方式
|
|
14
|
+
* 留痕。代价是同一段文本的**中间若干次**改动看不到(我们只能看到"离开时的样子"),这一点
|
|
15
|
+
* 在文档里写明,不假装是版本控制。
|
|
16
|
+
* 2. **只追加 + 上限**:追加而不是覆盖,坏一行不牵连其余(解析时跳过坏行)。超过上限就整文件
|
|
17
|
+
* 重写成最后 N 条 —— 这是唯一的"重写",且只发生在追加之后,用原子写完成。
|
|
18
|
+
* 3. **相同内容不重复记**:保存两次而文本没变,不该产生两条历史。
|
|
19
|
+
* 4. **版本键是自增序号,不是时间戳**:同一毫秒内落两次记录完全可能(实测:连续两次保存),
|
|
20
|
+
* 时间戳做键会让"取回某一版"取到隔壁那条。时间戳只用于显示。
|
|
21
|
+
*
|
|
22
|
+
* 这个方向借鉴了同生态里 whale-persona 的"收件箱"设计(只追加、坏行不牵连、状态显式),
|
|
23
|
+
* 但这里记录的是**已发生的事实**而不是待确认的提议 —— 我们的场景是"改了什么要看得见、回得去",
|
|
24
|
+
* 不需要它的确认闸门。
|
|
25
|
+
*/
|
|
26
|
+
import { existsSync, readFileSync, renameSync, writeFileSync } from 'node:fs'
|
|
27
|
+
import { join } from 'node:path'
|
|
28
|
+
|
|
29
|
+
/** 每个助手目录下的日志文件名。 */
|
|
30
|
+
export const HISTORY_NAME = 'prompt-history.jsonl'
|
|
31
|
+
|
|
32
|
+
/** 保留的最大版本数。超出后整文件重写为最后这么多条。 */
|
|
33
|
+
export const HISTORY_MAX = 30
|
|
34
|
+
|
|
35
|
+
/** 改动来源。`settings` 是设置页保存;`external` 是日志末条之后发生的任何改动。 */
|
|
36
|
+
export const HISTORY_SOURCE = { settings: 'settings', external: 'external' }
|
|
37
|
+
|
|
38
|
+
/** @param {string} directory - one assistant's preset directory. */
|
|
39
|
+
export function historyFile(directory) {
|
|
40
|
+
return join(directory, HISTORY_NAME)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Parse the log, skipping unreadable lines.
|
|
45
|
+
*
|
|
46
|
+
* A line that is not valid JSON, or lacks a text, is ignored rather than throwing: this file is
|
|
47
|
+
* written across process restarts and may be edited by hand, and one bad line must not cost the
|
|
48
|
+
* user every version (the same reason the inbox design in this ecosystem replays line by line).
|
|
49
|
+
*
|
|
50
|
+
* @returns {Array<{ at: string, by: string, text: string }>} oldest first.
|
|
51
|
+
*/
|
|
52
|
+
export function readEntries(directory) {
|
|
53
|
+
const file = historyFile(directory)
|
|
54
|
+
if (!existsSync(file)) return []
|
|
55
|
+
const entries = []
|
|
56
|
+
for (const line of readFileSync(file, 'utf8').split('\n')) {
|
|
57
|
+
if (line.trim() === '') continue
|
|
58
|
+
try {
|
|
59
|
+
const parsed = JSON.parse(line)
|
|
60
|
+
if (parsed === null || typeof parsed !== 'object') continue
|
|
61
|
+
if (typeof parsed.text !== 'string') continue
|
|
62
|
+
entries.push({
|
|
63
|
+
// 老日志没有 n 时按行序补一个,于是"序号"这个概念对任何日志都成立。
|
|
64
|
+
n: Number.isInteger(parsed.n) && parsed.n > 0 ? parsed.n : entries.length + 1,
|
|
65
|
+
at: typeof parsed.at === 'string' ? parsed.at : '',
|
|
66
|
+
by: typeof parsed.by === 'string' ? parsed.by : HISTORY_SOURCE.external,
|
|
67
|
+
text: parsed.text,
|
|
68
|
+
})
|
|
69
|
+
} catch {
|
|
70
|
+
continue
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return entries
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Append one revision, unless it repeats the newest one.
|
|
78
|
+
*
|
|
79
|
+
* @param {string} directory - one assistant's preset directory.
|
|
80
|
+
* @param {string} text - the prompt content **after** the change.
|
|
81
|
+
* @param {string} by - {@link HISTORY_SOURCE}.
|
|
82
|
+
* @returns {{ recorded: boolean, at: string, n: number }} whether a line was appended.
|
|
83
|
+
*/
|
|
84
|
+
export function recordPrompt(directory, text, by = HISTORY_SOURCE.settings) {
|
|
85
|
+
if (typeof text !== 'string') return { recorded: false, at: '' }
|
|
86
|
+
const entries = readEntries(directory)
|
|
87
|
+
const newest = entries[entries.length - 1]
|
|
88
|
+
if (newest !== undefined && newest.text === text) return { recorded: false, at: newest.at }
|
|
89
|
+
|
|
90
|
+
const at = new Date().toISOString()
|
|
91
|
+
const n = entries.length === 0 ? 1 : entries[entries.length - 1].n + 1
|
|
92
|
+
const lines = entries.slice(-(HISTORY_MAX - 1)).map((entry) =>
|
|
93
|
+
JSON.stringify({ n: entry.n, at: entry.at, by: entry.by, bytes: entry.text.length, text: entry.text }),
|
|
94
|
+
)
|
|
95
|
+
lines.push(JSON.stringify({ n, at, by, bytes: text.length, text }))
|
|
96
|
+
|
|
97
|
+
// 唯一的整文件重写:顺手把上限外的旧条目丢掉,用原子写落盘(读取器不会看到写了一半的文件)。
|
|
98
|
+
const file = historyFile(directory)
|
|
99
|
+
const temporary = `${file}.tmp-${String(process.pid)}`
|
|
100
|
+
writeFileSync(temporary, lines.join('\n') + '\n', 'utf8')
|
|
101
|
+
renameSync(temporary, file)
|
|
102
|
+
return { recorded: true, at, n }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The log as the page needs it: newest first, **without** the texts.
|
|
107
|
+
*
|
|
108
|
+
* Text is fetched per revision on demand (`readVersion`) — a page that ships 30 prompts on every
|
|
109
|
+
* load would be slower than the thing it is documenting.
|
|
110
|
+
*
|
|
111
|
+
* @returns {Array<{ at: string, by: string, bytes: number, preview: string }>}
|
|
112
|
+
*/
|
|
113
|
+
export function listHistory(directory, limit = HISTORY_MAX) {
|
|
114
|
+
const entries = readEntries(directory)
|
|
115
|
+
const out = []
|
|
116
|
+
for (const entry of entries.slice(-limit).reverse()) {
|
|
117
|
+
const firstLine = entry.text.split('\n').find((line) => line.trim() !== '') ?? ''
|
|
118
|
+
out.push({
|
|
119
|
+
n: entry.n,
|
|
120
|
+
at: entry.at,
|
|
121
|
+
by: entry.by,
|
|
122
|
+
bytes: entry.text.length,
|
|
123
|
+
preview: firstLine.trim().slice(0, 80),
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
return out
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* One revision's full text, by its sequence number.
|
|
131
|
+
*
|
|
132
|
+
* The key is `n`, not `at`: two revisions can share a timestamp (measured — two records inside the
|
|
133
|
+
* same millisecond), and keying on it silently returns the neighbour.
|
|
134
|
+
*
|
|
135
|
+
* @param {number|string} n - the revision number shown in {@link listHistory}.
|
|
136
|
+
* @returns {string|null}
|
|
137
|
+
*/
|
|
138
|
+
export function readVersion(directory, n) {
|
|
139
|
+
const wanted = typeof n === 'string' ? Number(n) : n
|
|
140
|
+
if (!Number.isInteger(wanted)) return null
|
|
141
|
+
for (const entry of readEntries(directory)) {
|
|
142
|
+
if (entry.n === wanted) return entry.text
|
|
143
|
+
}
|
|
144
|
+
return null
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Record `text` as an `external` revision when it is not what the log already ends with.
|
|
149
|
+
*
|
|
150
|
+
* This is how changes made outside the settings page become visible: the page asks for state, and
|
|
151
|
+
* the host notices that what is on disk is not what it last recorded.
|
|
152
|
+
*
|
|
153
|
+
* @returns {boolean} whether something was recorded.
|
|
154
|
+
*/
|
|
155
|
+
export function recordExternalChange(directory, text) {
|
|
156
|
+
return recordPrompt(directory, text, HISTORY_SOURCE.external).recorded
|
|
157
|
+
}
|
package/locales.mjs
CHANGED
|
@@ -52,6 +52,13 @@ export const zh = {
|
|
|
52
52
|
'msg.reorderFailed': '调整顺序失败',
|
|
53
53
|
'msg.imported': '已导入到编辑器(还没有保存):检查后点「保存」。',
|
|
54
54
|
'msg.importFailed': '导入失败',
|
|
55
|
+
'history.label': '改动历史',
|
|
56
|
+
'history.pick': '选择要载入的版本…',
|
|
57
|
+
'history.load': '载入这一版',
|
|
58
|
+
'history.hint': '每次保存、以及会话内工具或手工改动,都会在这里留一版;载入只改草稿,保存前不落盘。',
|
|
59
|
+
'history.by.settings': '设置页保存',
|
|
60
|
+
'history.by.external': '会话内/手工改动',
|
|
61
|
+
'msg.loadFailed': '载入这一版失败',
|
|
55
62
|
'msg.importEmpty': '这个文件是空的。',
|
|
56
63
|
'msg.importTooLarge': '文件太大(上限 1MB)。',
|
|
57
64
|
'msg.exported': '已导出为文件。',
|
|
@@ -201,6 +208,13 @@ export const en = {
|
|
|
201
208
|
'msg.reorderFailed': 'Could not reorder',
|
|
202
209
|
'msg.imported': 'Imported into the editor (not saved yet) — review it, then click Save.',
|
|
203
210
|
'msg.importFailed': 'Import failed',
|
|
211
|
+
'history.label': 'Change history',
|
|
212
|
+
'history.pick': 'Pick a version to load…',
|
|
213
|
+
'history.load': 'Load this version',
|
|
214
|
+
'history.hint': 'Every save — plus changes made in a session or by hand — leaves a version here. Loading one only edits the draft; nothing is written until you save.',
|
|
215
|
+
'history.by.settings': 'saved from this page',
|
|
216
|
+
'history.by.external': 'changed in a session / by hand',
|
|
217
|
+
'msg.loadFailed': 'Could not load that version',
|
|
204
218
|
'msg.importEmpty': 'That file is empty.',
|
|
205
219
|
'msg.importTooLarge': 'That file is too large (1 MB limit).',
|
|
206
220
|
'msg.exported': 'Exported to a file.',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-custom-mode",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Custom modes and custom prompts for DeepSeek Harness (dsh): edit a mode's system prompt on the settings page (it takes effect on the next model step), choose its base mode, switch plugins row by row, and keep several assistants side by side.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
"meta.mjs",
|
|
53
53
|
"paths.mjs",
|
|
54
54
|
"locales.mjs",
|
|
55
|
+
"journal.mjs",
|
|
55
56
|
"cordis.patch.yml",
|
|
56
57
|
"preset/"
|
|
57
58
|
],
|