dsh-instruction-memory 1.5.3 → 1.5.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/CHANGELOG.md +84 -0
- package/README.md +151 -14
- package/docs/demo.html +671 -0
- package/lib/client.js +170 -14
- package/lib/index.js +87 -11
- package/memory.schema.json +2 -2
- package/package.json +3 -2
package/docs/demo.html
ADDED
|
@@ -0,0 +1,671 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>dsh-instruction-memory 设置页演示</title>
|
|
7
|
+
<script src="vendor/react.production.min.js"></script>
|
|
8
|
+
<script src="vendor/react-dom.production.min.js"></script>
|
|
9
|
+
<style>
|
|
10
|
+
body { margin:0; background:#f5f6f8; font-family:system-ui,-apple-system,'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif; }
|
|
11
|
+
#root { max-width:920px; margin:28px auto 0; padding:0 16px; }
|
|
12
|
+
</style>
|
|
13
|
+
</head>
|
|
14
|
+
<body>
|
|
15
|
+
<div id="root"></div>
|
|
16
|
+
|
|
17
|
+
<script>
|
|
18
|
+
// Minimal stand-ins for the DSH web shell: module loader + require.
|
|
19
|
+
window.__ModuleLoader__ = { load: (spec) => { window.__dshSpec = spec } }
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<script>
|
|
23
|
+
// In-memory host that speaks the exact route envelope of lib/index.js, so
|
|
24
|
+
// this page exercises the REAL client module (loaded below) against
|
|
25
|
+
// realistic data — no backend needed.
|
|
26
|
+
var DEMO_STORE = {
|
|
27
|
+
version: 1,
|
|
28
|
+
enabled: true,
|
|
29
|
+
// Deliberately tight so the budget meter and the "本轮不注入" marks are
|
|
30
|
+
// visible immediately; raising it in the input clears them live.
|
|
31
|
+
budgetChars: 800,
|
|
32
|
+
entries: [
|
|
33
|
+
{
|
|
34
|
+
id: 'im-demo-lang',
|
|
35
|
+
title: '回答语言',
|
|
36
|
+
content: '所有回答默认使用简体中文;技术名词首次出现时在括号里附英文原文。',
|
|
37
|
+
mode: 'always', when: '', priority: 2, enabled: true, tags: ['沟通'],
|
|
38
|
+
updatedAt: Date.now() - 86400000 * 3,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: 'im-demo-confirm',
|
|
42
|
+
title: '先确认再动手',
|
|
43
|
+
content: '需求不明确或存在多种合理实现时,先列出候选方案向用户确认,再开始修改代码。',
|
|
44
|
+
mode: 'always', when: '', priority: 1, enabled: true, tags: ['沟通', '工作流'],
|
|
45
|
+
updatedAt: Date.now() - 86400000 * 2,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: 'im-demo-style',
|
|
49
|
+
title: '代码风格',
|
|
50
|
+
content: '修改代码时遵循该文件既有的缩进、命名密度与注释风格,不顺手重构无关代码;\n变量命名优先使用业务领域词汇而不是数据结构词汇,例如用「订单金额」而不是「data1」;\n新增依赖前先检查 package.json 中是否已有能力等价的依赖,避免重复引入;\n公共函数需要补充输入输出说明注释,但不为改动本身添加解释性注释;\n改动完成后运行项目现有的测试与 lint 检查,若有失败先修复再交付,不留下只改了一半的状态;\n涉及对外接口或数据结构变更时,先在回复中列出受影响的调用方,确认迁移路径后再提交。\n日志打印需带关键上下文(对象标识、入参摘要),不要只输出 "error" 这类无信息内容;\n异步任务必须设置超时与重试上限,不允许无限等待外部资源;\n对外错误信息要区分用户可操作的提示与内部诊断细节,不把堆栈直接暴露给调用方;\n代码评审先核对行为变化与测试覆盖,再讨论风格偏好,意见必须可执行、可验证;\n以上要求同样适用于纯重构类提交,重构前后行为必须完全一致。',
|
|
51
|
+
mode: 'auto', when: '编写或修改代码时', priority: 1, enabled: true, tags: ['代码规范'],
|
|
52
|
+
updatedAt: Date.now() - 86400000,
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: 'im-demo-reply',
|
|
56
|
+
title: '回复结构',
|
|
57
|
+
content: '较长的回答先给一句结论,再展开细节;方案对比先列差异再给推荐。',
|
|
58
|
+
mode: 'auto', when: '解释性问题与方案对比', priority: 0, enabled: false, tags: ['沟通'],
|
|
59
|
+
updatedAt: Date.now() - 3600000,
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
profiles: [
|
|
63
|
+
{
|
|
64
|
+
id: 'pf-demo-bg',
|
|
65
|
+
name: '职业背景',
|
|
66
|
+
content: '有 8 年后端开发经验,熟悉 Java 与分布式系统;技术问题可以跳过入门概念,直接讲权衡与实现细节。',
|
|
67
|
+
enabled: true,
|
|
68
|
+
updatedAt: Date.now() - 86400000,
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: 'pf-demo-style',
|
|
72
|
+
name: '沟通偏好',
|
|
73
|
+
content: '喜欢先给结论、再给可执行步骤,少寒暄少铺陈。',
|
|
74
|
+
enabled: true,
|
|
75
|
+
updatedAt: Date.now() - 7200000,
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: 'pf-demo-goal',
|
|
79
|
+
name: '长期追求(停用示例)',
|
|
80
|
+
content: '希望代码评审意见都具体可执行,不要泛泛而谈。',
|
|
81
|
+
enabled: false,
|
|
82
|
+
updatedAt: Date.now() - 3600000,
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// F13 配置集:演示数据里放两套,好让「切换」真的改变注入内容。所有既有条目
|
|
88
|
+
// 归入「默认」,另加一条只属于「工作(示例)」的条目。
|
|
89
|
+
;(function seedSets() {
|
|
90
|
+
DEMO_STORE.version = 2
|
|
91
|
+
DEMO_STORE.$schema = './memory.schema.json'
|
|
92
|
+
DEMO_STORE.backupDir = ''
|
|
93
|
+
DEMO_STORE.disabledUntil = 0
|
|
94
|
+
DEMO_STORE.activeSetId = 'st-demo-default'
|
|
95
|
+
DEMO_STORE.sets = [
|
|
96
|
+
{ id: 'st-demo-default', name: '默认' },
|
|
97
|
+
{ id: 'st-demo-work', name: '工作(示例)' },
|
|
98
|
+
]
|
|
99
|
+
DEMO_STORE.entries.forEach(function (e) { e.setId = 'st-demo-default' })
|
|
100
|
+
;(DEMO_STORE.profiles || []).forEach(function (p) { p.setId = 'st-demo-default' })
|
|
101
|
+
DEMO_STORE.entries.push({
|
|
102
|
+
id: 'im-demo-work',
|
|
103
|
+
title: '工作集专属',
|
|
104
|
+
content: '这条只属于「工作(示例)」配置集。切换到它时,上面几条指令与用户形象都不会被注入——和真实宿主一样,注入只渲染当前激活的那一套。',
|
|
105
|
+
mode: 'always', when: '', priority: 1, enabled: true, tags: ['演示'],
|
|
106
|
+
setId: 'st-demo-work', updatedAt: Date.now() - 3600000,
|
|
107
|
+
})
|
|
108
|
+
})()
|
|
109
|
+
|
|
110
|
+
/** 与宿主 activeScope/inScope 同义:'' 归属当前集合。 */
|
|
111
|
+
function demoInScope(item) {
|
|
112
|
+
var own = item && typeof item.setId === 'string' && item.setId !== '' ? item.setId : DEMO_STORE.activeSetId
|
|
113
|
+
return own === DEMO_STORE.activeSetId
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function demoScopedEntries() {
|
|
117
|
+
return DEMO_STORE.entries.filter(demoInScope)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function demoScopedProfiles() {
|
|
121
|
+
return (DEMO_STORE.profiles || []).filter(demoInScope)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// A compact, faithful copy of the Host's injection planning (see
|
|
125
|
+
// planInjection in lib/index.js): it generates both the preview text and
|
|
126
|
+
// the per-entry included/omitted plan the Client's F1 budget meter renders,
|
|
127
|
+
// so changing the budget input updates the marks live in this demo.
|
|
128
|
+
var DEMO_HEADER = [
|
|
129
|
+
'## 用户长期指令记忆(Instruction Memory)',
|
|
130
|
+
'以下条目由用户在 DSH 设置 →「指令记忆」中长期保存,属于本次及后续所有对话的既有要求,请在回答、推理与执行任务时遵守;它们不覆盖系统与安全规则。',
|
|
131
|
+
'· [始终] 条目必须无条件遵守。',
|
|
132
|
+
'· [按需] 条目仅在与当前问题或任务相关时生效;不相关时直接忽略,不要强行套用。',
|
|
133
|
+
'· 条目之间冲突时,优先遵守优先级更高或更具体的一条;无法判断时向用户确认。',
|
|
134
|
+
'',
|
|
135
|
+
].join('\n')
|
|
136
|
+
|
|
137
|
+
function demoFooter(count) {
|
|
138
|
+
return count === 0
|
|
139
|
+
? ''
|
|
140
|
+
: '\n\n(另有 ' + count + ' 条指令因注入字数上限未包含,可在「设置 → 指令记忆」中提高上限或调低其优先级。)'
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Faithful copy of the Host's persona header (lib/index.js PERSONA_HEADER).
|
|
144
|
+
var DEMO_PERSONA_HEADER = [
|
|
145
|
+
'## 关于用户(用户形象)',
|
|
146
|
+
'以下是用户本人提供的个人形象信息,描述其身份背景、喜好、性格与追求,不是要求你扮演的角色。请在保证回答正确、完整、客观且紧扣问题本身的前提下,结合这些信息调整解释的深浅、详略、举例与建议的侧重;不得为了迎合这些信息而改变事实、省略必要的风险提示或偏离问题核心;与当前问题无关时直接忽略。',
|
|
147
|
+
'',
|
|
148
|
+
].join('\n')
|
|
149
|
+
|
|
150
|
+
function demoProfilesBlock() {
|
|
151
|
+
var active = demoScopedProfiles()
|
|
152
|
+
.filter(function (p) { return p && p.enabled === true && p.content })
|
|
153
|
+
if (active.length === 0) return { text: '', profiles: [], chars: 0 }
|
|
154
|
+
var blocks = active.map(function (p) {
|
|
155
|
+
var name = p.name || (p.content || '').slice(0, 20)
|
|
156
|
+
return { id: p.id, name: name, block: '### ' + name + '\n' + p.content }
|
|
157
|
+
})
|
|
158
|
+
var text = DEMO_PERSONA_HEADER + '\n' + blocks.map(function (b) { return b.block }).join('\n\n')
|
|
159
|
+
return {
|
|
160
|
+
text: text,
|
|
161
|
+
chars: text.length,
|
|
162
|
+
profiles: blocks.map(function (b) { return { id: b.id, name: b.name, chars: b.block.length } }),
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function demoFormatEntry(entry, tag) {
|
|
167
|
+
var prio = entry.priority === 2 ? '高' : (entry.priority === 0 ? '低' : '普通')
|
|
168
|
+
var lines = ['[' + tag + '|优先级 ' + prio + '] ' + entry.title]
|
|
169
|
+
if (entry.mode === 'auto' && entry.when) lines.push('适用场景:' + entry.when)
|
|
170
|
+
lines.push(entry.content)
|
|
171
|
+
return lines.join('\n')
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function demoPlan() {
|
|
175
|
+
var budget = DEMO_STORE.budgetChars
|
|
176
|
+
if (typeof budget !== 'number' || !isFinite(budget)) budget = 4000
|
|
177
|
+
if (budget < 800) budget = 800
|
|
178
|
+
if (budget > 40000) budget = 40000
|
|
179
|
+
var blank = function () {
|
|
180
|
+
return { text: '', budget: budget, activeCount: 0, included: [], omitted: [], profiles: [], profileChars: 0, entryChars: 0 }
|
|
181
|
+
}
|
|
182
|
+
if (DEMO_STORE.enabled !== true) return blank()
|
|
183
|
+
// F7 临时静音:与真实宿主同义——暂停中的计划与 enabled:false 逐字节相同。
|
|
184
|
+
if (typeof DEMO_STORE.disabledUntil === 'number' && DEMO_STORE.disabledUntil > Date.now()) return blank()
|
|
185
|
+
var persona = demoProfilesBlock()
|
|
186
|
+
var active = demoScopedEntries().filter(function (e) { return e.enabled && e.content !== '' })
|
|
187
|
+
|
|
188
|
+
var blocks = []
|
|
189
|
+
var included = []
|
|
190
|
+
var omittedPlan = []
|
|
191
|
+
var used = DEMO_HEADER.length
|
|
192
|
+
var omitted = 0
|
|
193
|
+
|
|
194
|
+
function consider(entry, tag) {
|
|
195
|
+
var block = demoFormatEntry(entry, tag)
|
|
196
|
+
var reserve = demoFooter(omitted + 1).length
|
|
197
|
+
if (used + block.length + 2 + reserve <= budget) {
|
|
198
|
+
used += block.length + 2
|
|
199
|
+
blocks.push(block)
|
|
200
|
+
included.push({ id: entry.id, chars: block.length, truncated: false })
|
|
201
|
+
return
|
|
202
|
+
}
|
|
203
|
+
if (blocks.length === 0) {
|
|
204
|
+
var room = budget - used - 2 - reserve
|
|
205
|
+
var note = '\n…(本条因注入字数上限被截断)'
|
|
206
|
+
if (room > note.length + 20) {
|
|
207
|
+
var head = block.slice(0, room - note.length)
|
|
208
|
+
var body = head + note
|
|
209
|
+
blocks.push(body)
|
|
210
|
+
used += body.length + 2
|
|
211
|
+
included.push({ id: entry.id, chars: body.length, truncated: true })
|
|
212
|
+
return
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
omitted += 1
|
|
216
|
+
omittedPlan.push({ id: entry.id, chars: block.length })
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
active.forEach(function (entry) { if (entry.mode === 'always') consider(entry, '始终') })
|
|
220
|
+
active.forEach(function (entry) { if (entry.mode !== 'always') consider(entry, '按需') })
|
|
221
|
+
|
|
222
|
+
var entryText = blocks.length === 0
|
|
223
|
+
? ''
|
|
224
|
+
: DEMO_HEADER + blocks.join('\n\n') + demoFooter(omitted)
|
|
225
|
+
var parts = []
|
|
226
|
+
if (persona.text !== '') parts.push(persona.text)
|
|
227
|
+
if (entryText !== '') parts.push(entryText)
|
|
228
|
+
return {
|
|
229
|
+
text: parts.join('\n\n'),
|
|
230
|
+
budget: budget,
|
|
231
|
+
activeCount: active.length,
|
|
232
|
+
included: included,
|
|
233
|
+
omitted: omittedPlan,
|
|
234
|
+
profiles: persona.profiles,
|
|
235
|
+
profileChars: persona.chars,
|
|
236
|
+
entryChars: entryText.length,
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Where a fresh set id comes from (mirrors the Host's st- prefix). */
|
|
241
|
+
function demoSetId() {
|
|
242
|
+
return 'st-demo-' + Math.random().toString(36).slice(2, 8)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function demoSnapshot() {
|
|
246
|
+
var plan = demoPlan()
|
|
247
|
+
// The panel edits and lists ONLY the active set (exactly like the real
|
|
248
|
+
// Host's snapshot), so a demo switch really empties the list.
|
|
249
|
+
// sets.total is the WHOLE-STORE count (same rule as the real host), so it
|
|
250
|
+
// is captured from DEMO_STORE BEFORE the scoped arrays replace these
|
|
251
|
+
// fields below — `data` aliases `full`, and counting after the reassign
|
|
252
|
+
// used to report the current-set numbers as the store-wide totals.
|
|
253
|
+
var totalEntries = (DEMO_STORE.entries || []).length
|
|
254
|
+
var totalProfiles = (DEMO_STORE.profiles || []).length
|
|
255
|
+
var full = JSON.parse(JSON.stringify(DEMO_STORE))
|
|
256
|
+
var data = full
|
|
257
|
+
data.entries = demoScopedEntries().map(function (e) { return JSON.parse(JSON.stringify(e)) })
|
|
258
|
+
data.profiles = demoScopedProfiles().map(function (p) { return JSON.parse(JSON.stringify(p)) })
|
|
259
|
+
var belongsTo = function (item, id) {
|
|
260
|
+
var own = item && typeof item.setId === 'string' && item.setId !== '' ? item.setId : DEMO_STORE.activeSetId
|
|
261
|
+
return own === id
|
|
262
|
+
}
|
|
263
|
+
var setList = (DEMO_STORE.sets || []).map(function (s) {
|
|
264
|
+
return {
|
|
265
|
+
id: s.id,
|
|
266
|
+
name: s.name,
|
|
267
|
+
active: s.id === DEMO_STORE.activeSetId,
|
|
268
|
+
entries: (DEMO_STORE.entries || []).filter(function (e) { return belongsTo(e, s.id) }).length,
|
|
269
|
+
profiles: (DEMO_STORE.profiles || []).filter(function (p) { return belongsTo(p, s.id) }).length,
|
|
270
|
+
}
|
|
271
|
+
})
|
|
272
|
+
return {
|
|
273
|
+
data: data,
|
|
274
|
+
sets: {
|
|
275
|
+
activeId: DEMO_STORE.activeSetId,
|
|
276
|
+
scoped: true,
|
|
277
|
+
total: { entries: totalEntries, profiles: totalProfiles },
|
|
278
|
+
list: setList,
|
|
279
|
+
},
|
|
280
|
+
storage: {
|
|
281
|
+
// The store file NEVER moves with the backup location — only the
|
|
282
|
+
// backup copies do (same rule as the real host).
|
|
283
|
+
path: 'C:\\Users\\demo\\.dsh\\instruction-memory\\memory.json',
|
|
284
|
+
error: null,
|
|
285
|
+
warning: null,
|
|
286
|
+
savedAt: Date.now() - 5 * 60000,
|
|
287
|
+
migratedFrom: null,
|
|
288
|
+
onDisk: true,
|
|
289
|
+
backupDir: DEMO_STORE.backupDir || '',
|
|
290
|
+
isDefaultBackupDir: !DEMO_STORE.backupDir,
|
|
291
|
+
backupPath: (DEMO_STORE.backupDir || 'C:\\Users\\demo\\.dsh\\instruction-memory') + '\\memory.json.bak',
|
|
292
|
+
historyDir: (DEMO_STORE.backupDir || 'C:\\Users\\demo\\.dsh\\instruction-memory') + '\\history',
|
|
293
|
+
historyCount: 0,
|
|
294
|
+
historyError: null,
|
|
295
|
+
schemaFile: 'C:\\Users\\demo\\.dsh\\instruction-memory\\memory.schema.json',
|
|
296
|
+
},
|
|
297
|
+
injection: {
|
|
298
|
+
registered: plan.text !== '',
|
|
299
|
+
available: true,
|
|
300
|
+
error: null,
|
|
301
|
+
muted: DEMO_STORE.enabled === true && typeof DEMO_STORE.disabledUntil === 'number' && DEMO_STORE.disabledUntil > Date.now(),
|
|
302
|
+
chars: plan.text.length,
|
|
303
|
+
budget: plan.budget,
|
|
304
|
+
activeCount: plan.activeCount,
|
|
305
|
+
included: plan.included,
|
|
306
|
+
omitted: plan.omitted,
|
|
307
|
+
profiles: plan.profiles,
|
|
308
|
+
profileChars: plan.profileChars,
|
|
309
|
+
entryChars: plan.entryChars,
|
|
310
|
+
pulls: 2,
|
|
311
|
+
},
|
|
312
|
+
// F12:演示桩里路由绑定在回环地址,安全面板不告警。
|
|
313
|
+
security: { hostGuarded: true, boundHost: '127.0.0.1' },
|
|
314
|
+
diagnostics: { loadedFrom: 'demo(内存桩宿主)', rev: data.rev || 0 },
|
|
315
|
+
preview: plan.text,
|
|
316
|
+
limits: {
|
|
317
|
+
minBudget: 800, maxBudget: 40000, maxEntries: 200,
|
|
318
|
+
maxProfiles: 50, maxProfileName: 40, maxProfileContent: 4000,
|
|
319
|
+
maxSets: 20, maxSetName: 30, maxHistory: 20, maxBackupDir: 512,
|
|
320
|
+
maxBodyChars: 3218000,
|
|
321
|
+
},
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** A no-op answer: ok, nothing written (mirrors the real envelope). */
|
|
326
|
+
function demoNoop() {
|
|
327
|
+
return {
|
|
328
|
+
ok: true,
|
|
329
|
+
json: async function () {
|
|
330
|
+
return { ok: true, saved: false, message: null, snapshot: demoSnapshot() }
|
|
331
|
+
},
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** Uniform route envelope for the demo host (the real one answers the same shape). */
|
|
336
|
+
function demoReply(ok, message) {
|
|
337
|
+
return {
|
|
338
|
+
ok: true,
|
|
339
|
+
json: async function () {
|
|
340
|
+
return {
|
|
341
|
+
ok: ok === true,
|
|
342
|
+
saved: ok === true,
|
|
343
|
+
message: message === undefined ? null : message,
|
|
344
|
+
snapshot: demoSnapshot(),
|
|
345
|
+
}
|
|
346
|
+
},
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Adopt a whole store object (源码模式 / 整体替换). Mirrors the parts of the
|
|
352
|
+
* Host's loader a demo needs to stay believable: sets are kept, every item is
|
|
353
|
+
* stamped onto an existing set, missing ids are minted.
|
|
354
|
+
*/
|
|
355
|
+
function demoAdoptStore(parsed) {
|
|
356
|
+
DEMO_STORE.version = 2
|
|
357
|
+
DEMO_STORE.enabled = parsed.enabled !== false
|
|
358
|
+
DEMO_STORE.disabledUntil = typeof parsed.disabledUntil === 'number' && isFinite(parsed.disabledUntil) && parsed.disabledUntil > 0
|
|
359
|
+
? parsed.disabledUntil
|
|
360
|
+
: 0
|
|
361
|
+
DEMO_STORE.budgetChars = typeof parsed.budgetChars === 'number' ? parsed.budgetChars : 4000
|
|
362
|
+
DEMO_STORE.sets = Array.isArray(parsed.sets) && parsed.sets.length > 0
|
|
363
|
+
? parsed.sets.map(function (s) { return { id: String(s.id), name: String(s.name).slice(0, 30) } })
|
|
364
|
+
: [{ id: 'st-demo-default', name: '默认' }]
|
|
365
|
+
DEMO_STORE.activeSetId = DEMO_STORE.sets.some(function (s) { return s.id === parsed.activeSetId })
|
|
366
|
+
? parsed.activeSetId
|
|
367
|
+
: DEMO_STORE.sets[0].id
|
|
368
|
+
DEMO_STORE.entries = (Array.isArray(parsed.entries) ? parsed.entries : []).map(function (e) {
|
|
369
|
+
var entry = JSON.parse(JSON.stringify(e))
|
|
370
|
+
entry.id = entry.id || ('im-demo-' + Math.random().toString(36).slice(2, 8))
|
|
371
|
+
entry.setId = DEMO_STORE.sets.some(function (s) { return s.id === entry.setId })
|
|
372
|
+
? entry.setId
|
|
373
|
+
: DEMO_STORE.activeSetId
|
|
374
|
+
entry.tags = Array.isArray(entry.tags) ? entry.tags : []
|
|
375
|
+
return entry
|
|
376
|
+
})
|
|
377
|
+
DEMO_STORE.profiles = (Array.isArray(parsed.profiles) ? parsed.profiles : []).map(function (p) {
|
|
378
|
+
var profile = JSON.parse(JSON.stringify(p))
|
|
379
|
+
profile.id = profile.id || ('pf-demo-' + Math.random().toString(36).slice(2, 8))
|
|
380
|
+
profile.setId = DEMO_STORE.sets.some(function (s) { return s.id === profile.setId })
|
|
381
|
+
? profile.setId
|
|
382
|
+
: DEMO_STORE.activeSetId
|
|
383
|
+
return profile
|
|
384
|
+
})
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
window.fetch = async function (url, options) {
|
|
388
|
+
var payload = JSON.parse(options.body)
|
|
389
|
+
switch (payload.method) {
|
|
390
|
+
case 'save-entry': {
|
|
391
|
+
var entry = Object.assign({}, payload.args.entry)
|
|
392
|
+
if (!entry.id) entry.id = 'im-demo-' + Math.random().toString(36).slice(2, 8)
|
|
393
|
+
entry.updatedAt = Date.now()
|
|
394
|
+
// The interactive path always files into the ACTIVE set (same rule as
|
|
395
|
+
// the real Host — an edit can never move an item between sets), and an
|
|
396
|
+
// id owned by another set is re-minted rather than replaced.
|
|
397
|
+
entry.setId = DEMO_STORE.activeSetId
|
|
398
|
+
var foreignId = DEMO_STORE.entries.some(function (e) {
|
|
399
|
+
return e.id === entry.id && e.setId !== DEMO_STORE.activeSetId
|
|
400
|
+
})
|
|
401
|
+
if (foreignId) entry.id = 'im-demo-' + Math.random().toString(36).slice(2, 8)
|
|
402
|
+
var list = DEMO_STORE.entries
|
|
403
|
+
var index = -1
|
|
404
|
+
for (var i = 0; i < list.length; i += 1) if (list[i].id === entry.id) { index = i; break }
|
|
405
|
+
if (index >= 0) list[index] = entry
|
|
406
|
+
else list.push(entry)
|
|
407
|
+
break
|
|
408
|
+
}
|
|
409
|
+
case 'delete-entry': {
|
|
410
|
+
var doomed = payload.args.id
|
|
411
|
+
var owned = DEMO_STORE.entries.some(function (e) {
|
|
412
|
+
return e.id === doomed && demoInScope(e)
|
|
413
|
+
})
|
|
414
|
+
if (!owned) return demoNoop()
|
|
415
|
+
DEMO_STORE.entries = DEMO_STORE.entries.filter(function (e) { return e.id !== doomed })
|
|
416
|
+
break
|
|
417
|
+
}
|
|
418
|
+
case 'set-options': {
|
|
419
|
+
if (typeof payload.args.enabled === 'boolean') {
|
|
420
|
+
DEMO_STORE.enabled = payload.args.enabled
|
|
421
|
+
DEMO_STORE.disabledUntil = 0
|
|
422
|
+
}
|
|
423
|
+
if (typeof payload.args.disabledUntil === 'number' && isFinite(payload.args.disabledUntil)
|
|
424
|
+
&& payload.args.disabledUntil >= 0 && typeof payload.args.enabled !== 'boolean') {
|
|
425
|
+
var wantedMute = Math.round(payload.args.disabledUntil)
|
|
426
|
+
var muteClamped = false
|
|
427
|
+
if (wantedMute <= Date.now()) wantedMute = 0
|
|
428
|
+
else if (wantedMute > Date.now() + 7 * 86400000) { wantedMute = Date.now() + 7 * 86400000; muteClamped = true }
|
|
429
|
+
DEMO_STORE.disabledUntil = wantedMute
|
|
430
|
+
if (wantedMute > 0) {
|
|
431
|
+
// 演示桩也到期自动恢复:真实宿主由插件内的定时器负责,这里用一个页内
|
|
432
|
+
// 定时器模拟同一行为(下一个快照即恢复正常态)。
|
|
433
|
+
setTimeout(function () { DEMO_STORE.disabledUntil = 0 }, wantedMute - Date.now() + 50)
|
|
434
|
+
}
|
|
435
|
+
return demoReply(true, wantedMute === 0
|
|
436
|
+
? '已恢复注入。'
|
|
437
|
+
: '已暂停注入至 ' + new Date(wantedMute).toLocaleString() + '(到期自动恢复;暂停期间不注入任何指令或用户形象)'
|
|
438
|
+
+ (muteClamped ? ';暂停时长上限为 7 天,已按上限记录' : ''))
|
|
439
|
+
}
|
|
440
|
+
if (typeof payload.args.budgetChars === 'number') DEMO_STORE.budgetChars = payload.args.budgetChars
|
|
441
|
+
if (typeof payload.args.backupDir === 'string') {
|
|
442
|
+
DEMO_STORE.backupDir = payload.args.backupDir.trim()
|
|
443
|
+
return demoReply(true, DEMO_STORE.backupDir === ''
|
|
444
|
+
? '已恢复默认备份位置(版本历史与导出与存储文件同目录)'
|
|
445
|
+
: '备份位置已更新:' + DEMO_STORE.backupDir + '(版本历史与「导出到备份目录」随之改变;存储文件本身不移动)')
|
|
446
|
+
}
|
|
447
|
+
break
|
|
448
|
+
}
|
|
449
|
+
case 'batch': {
|
|
450
|
+
// Demo stand-in for the Host's single-transaction batch route.
|
|
451
|
+
var b = payload.args || {}
|
|
452
|
+
var batchIds = Array.isArray(b.ids) ? b.ids : []
|
|
453
|
+
if (b.op === 'delete') {
|
|
454
|
+
DEMO_STORE.entries = DEMO_STORE.entries.filter(function (e) {
|
|
455
|
+
return batchIds.indexOf(e.id) < 0
|
|
456
|
+
})
|
|
457
|
+
} else {
|
|
458
|
+
DEMO_STORE.entries.forEach(function (e) {
|
|
459
|
+
if (batchIds.indexOf(e.id) < 0) return
|
|
460
|
+
if (b.op === 'set-enabled') {
|
|
461
|
+
e.enabled = b.value
|
|
462
|
+
} else if (b.op === 'set-priority') {
|
|
463
|
+
e.priority = b.value
|
|
464
|
+
} else if (b.op === 'add-tags') {
|
|
465
|
+
var cur = Array.isArray(e.tags) ? e.tags.slice() : []
|
|
466
|
+
;(b.value || []).forEach(function (tg) { if (cur.indexOf(tg) < 0) cur.push(tg) })
|
|
467
|
+
e.tags = cur.slice(0, 8)
|
|
468
|
+
} else if (b.op === 'remove-tags') {
|
|
469
|
+
e.tags = (e.tags || []).filter(function (tg) { return (b.value || []).indexOf(tg) < 0 })
|
|
470
|
+
}
|
|
471
|
+
})
|
|
472
|
+
}
|
|
473
|
+
break
|
|
474
|
+
}
|
|
475
|
+
case 'save-profile': {
|
|
476
|
+
// Demo stand-in for the Host's save-profile route. Mirror
|
|
477
|
+
// sanitizeProfile: a blank name falls back to the first 20 single-line
|
|
478
|
+
// characters of content, so the card never shows an empty name (the
|
|
479
|
+
// real Host stores this fallback; it is not a client-only label).
|
|
480
|
+
var p = Object.assign({}, payload.args.profile)
|
|
481
|
+
if (!p.id) p.id = 'pf-demo-' + Math.random().toString(36).slice(2, 8)
|
|
482
|
+
if (typeof p.enabled !== 'boolean') p.enabled = true
|
|
483
|
+
if (typeof p.content !== 'string') p.content = ''
|
|
484
|
+
var oneLine = function (s, max) {
|
|
485
|
+
return String(s).split(/\r?\n/).map(function (x) { return x.trim() })
|
|
486
|
+
.filter(Boolean).join(' ').slice(0, max)
|
|
487
|
+
}
|
|
488
|
+
var trimmedName = oneLine(p.name || '', 40)
|
|
489
|
+
p.name = trimmedName !== '' ? trimmedName : oneLine(p.content, 20)
|
|
490
|
+
p.updatedAt = Date.now()
|
|
491
|
+
if (!Array.isArray(DEMO_STORE.profiles)) DEMO_STORE.profiles = []
|
|
492
|
+
var pIndex = -1
|
|
493
|
+
for (var pi = 0; pi < DEMO_STORE.profiles.length; pi += 1) {
|
|
494
|
+
if (DEMO_STORE.profiles[pi].id === p.id) { pIndex = pi; break }
|
|
495
|
+
}
|
|
496
|
+
if (pIndex >= 0) DEMO_STORE.profiles[pIndex] = p
|
|
497
|
+
else DEMO_STORE.profiles.push(p)
|
|
498
|
+
break
|
|
499
|
+
}
|
|
500
|
+
case 'delete-profile':
|
|
501
|
+
DEMO_STORE.profiles = (DEMO_STORE.profiles || [])
|
|
502
|
+
.filter(function (p) { return p.id !== payload.args.id })
|
|
503
|
+
break
|
|
504
|
+
// ---- F13 配置集 ----
|
|
505
|
+
case 'save-set': {
|
|
506
|
+
var wanted = Object.assign({}, payload.args.set)
|
|
507
|
+
var wantedName = typeof wanted.name === 'string'
|
|
508
|
+
? wanted.name.split(/\r?\n/).join(' ').trim().slice(0, 30)
|
|
509
|
+
: ''
|
|
510
|
+
if (wantedName === '') return demoReply(false, '配置集名称不能为空(单行,最多 30 字符)')
|
|
511
|
+
var sameName = function (s) { return s.name === wantedName && s.id !== wanted.id }
|
|
512
|
+
if (DEMO_STORE.sets.some(sameName)) return demoReply(false, '已存在同名配置集「' + wantedName + '」')
|
|
513
|
+
if (!wanted.id) {
|
|
514
|
+
if (DEMO_STORE.sets.length >= 20) return demoReply(false, '最多创建 20 个配置集')
|
|
515
|
+
DEMO_STORE.sets.push({ id: demoSetId(), name: wantedName })
|
|
516
|
+
return demoReply(true, '已创建配置集「' + wantedName + '」;它是空的,点击「切换」后才会注入它')
|
|
517
|
+
}
|
|
518
|
+
var renameAt = -1
|
|
519
|
+
for (var si = 0; si < DEMO_STORE.sets.length; si += 1) {
|
|
520
|
+
if (DEMO_STORE.sets[si].id === wanted.id) { renameAt = si; break }
|
|
521
|
+
}
|
|
522
|
+
if (renameAt < 0) return demoReply(false, '没有该配置集(可能已被其他窗口删除)')
|
|
523
|
+
DEMO_STORE.sets[renameAt].name = wantedName
|
|
524
|
+
return demoReply(true, '已重命名为「' + wantedName + '」')
|
|
525
|
+
}
|
|
526
|
+
case 'delete-set': {
|
|
527
|
+
var deleteId = payload.args.id
|
|
528
|
+
var targetSet = null
|
|
529
|
+
DEMO_STORE.sets.forEach(function (s) { if (s.id === deleteId) targetSet = s })
|
|
530
|
+
if (targetSet === null) return demoReply(false, '没有该配置集')
|
|
531
|
+
if (DEMO_STORE.sets.length <= 1) return demoReply(false, '至少要保留一个配置集')
|
|
532
|
+
if (deleteId === DEMO_STORE.activeSetId) return demoReply(false, '不能删除当前激活的配置集;请先切换到其他配置集')
|
|
533
|
+
DEMO_STORE.sets = DEMO_STORE.sets.filter(function (s) { return s.id !== deleteId })
|
|
534
|
+
DEMO_STORE.entries = DEMO_STORE.entries.filter(function (e) { return e.setId !== deleteId })
|
|
535
|
+
DEMO_STORE.profiles = (DEMO_STORE.profiles || []).filter(function (p) { return p.setId !== deleteId })
|
|
536
|
+
return demoReply(true, '已删除配置集「' + targetSet.name + '」及其中的条目与形象')
|
|
537
|
+
}
|
|
538
|
+
case 'set-active-set': {
|
|
539
|
+
var activeId = payload.args.id
|
|
540
|
+
var found = DEMO_STORE.sets.some(function (s) { return s.id === activeId })
|
|
541
|
+
if (!found) return demoReply(false, '没有该配置集(可能已被其他窗口删除)')
|
|
542
|
+
if (activeId === DEMO_STORE.activeSetId) return demoNoop()
|
|
543
|
+
DEMO_STORE.activeSetId = activeId
|
|
544
|
+
var activeName = ''
|
|
545
|
+
DEMO_STORE.sets.forEach(function (s) { if (s.id === activeId) activeName = s.name })
|
|
546
|
+
return demoReply(true, '已切换到配置集「' + activeName + '」,注入内容已随之更新')
|
|
547
|
+
}
|
|
548
|
+
case 'duplicate-set': {
|
|
549
|
+
var copyFrom = null
|
|
550
|
+
DEMO_STORE.sets.forEach(function (s) { if (s.id === payload.args.id) copyFrom = s })
|
|
551
|
+
if (copyFrom === null) return demoReply(false, '没有该配置集')
|
|
552
|
+
if (DEMO_STORE.sets.length >= 20) return demoReply(false, '最多 20 个配置集')
|
|
553
|
+
var copyName = (payload.args.name || copyFrom.name + ' 副本').trim().slice(0, 30)
|
|
554
|
+
if (DEMO_STORE.sets.some(function (s) { return s.name === copyName })) return demoReply(false, '已存在同名配置集「' + copyName + '」')
|
|
555
|
+
var copyId = demoSetId()
|
|
556
|
+
var stamp = Date.now()
|
|
557
|
+
var copiedEntries = DEMO_STORE.entries.filter(function (e) { return e.setId === copyFrom.id })
|
|
558
|
+
.map(function (e) {
|
|
559
|
+
var copy = JSON.parse(JSON.stringify(e))
|
|
560
|
+
copy.id = 'im-demo-' + Math.random().toString(36).slice(2, 8)
|
|
561
|
+
copy.setId = copyId
|
|
562
|
+
copy.updatedAt = stamp
|
|
563
|
+
return copy
|
|
564
|
+
})
|
|
565
|
+
DEMO_STORE.sets.push({ id: copyId, name: copyName })
|
|
566
|
+
Array.prototype.push.apply(DEMO_STORE.entries, copiedEntries)
|
|
567
|
+
return demoReply(true, '已复制为「' + copyName + '」(' + copiedEntries.length + ' 条指令);点击「切换」后才会注入它')
|
|
568
|
+
}
|
|
569
|
+
// ---- F14 版本历史(演示页面不落盘,因此没有快照可列) ----
|
|
570
|
+
case 'history-list':
|
|
571
|
+
return {
|
|
572
|
+
ok: true,
|
|
573
|
+
json: async function () {
|
|
574
|
+
return {
|
|
575
|
+
ok: true, saved: false, message: null, snapshot: demoSnapshot(),
|
|
576
|
+
history: { dir: '(演示页面不写历史快照;真实宿主会在存储目录的 history/ 子目录里保留最近 20 份)', items: [] },
|
|
577
|
+
}
|
|
578
|
+
},
|
|
579
|
+
}
|
|
580
|
+
case 'history-restore':
|
|
581
|
+
case 'history-delete':
|
|
582
|
+
return demoReply(false, '演示页面没有历史快照')
|
|
583
|
+
// ---- F16 源码模式 ----
|
|
584
|
+
case 'read-source':
|
|
585
|
+
return {
|
|
586
|
+
ok: true,
|
|
587
|
+
json: async function () {
|
|
588
|
+
return { ok: true, saved: false, message: null, source: JSON.stringify(DEMO_STORE, null, 2) + '\n', snapshot: demoSnapshot() }
|
|
589
|
+
},
|
|
590
|
+
}
|
|
591
|
+
case 'write-source': {
|
|
592
|
+
var text = payload.args.text
|
|
593
|
+
if (typeof text !== 'string' || text.trim() === '') return demoReply(false, '源码不能为空')
|
|
594
|
+
var parsedSource = null
|
|
595
|
+
try {
|
|
596
|
+
parsedSource = JSON.parse(text)
|
|
597
|
+
} catch (err) {
|
|
598
|
+
return demoReply(false, '源码不是有效的 JSON:' + (err && err.message ? err.message : err))
|
|
599
|
+
}
|
|
600
|
+
if (parsedSource === null || typeof parsedSource !== 'object' || Array.isArray(parsedSource)) {
|
|
601
|
+
return demoReply(false, '顶层必须是一个 JSON 对象')
|
|
602
|
+
}
|
|
603
|
+
if (typeof parsedSource.version === 'number' && parsedSource.version !== 2) {
|
|
604
|
+
return demoReply(false, '内容声明为格式版本 ' + parsedSource.version + ',本插件支持的格式版本为 1 / 2')
|
|
605
|
+
}
|
|
606
|
+
demoAdoptStore(parsedSource)
|
|
607
|
+
return demoReply(true, '已按源码保存(演示页面只改内存,不写文件):'
|
|
608
|
+
+ DEMO_STORE.entries.length + ' 条指令 / ' + (DEMO_STORE.profiles || []).length + ' 个用户形象 / '
|
|
609
|
+
+ DEMO_STORE.sets.length + ' 个配置集')
|
|
610
|
+
}
|
|
611
|
+
// ---- F14 整体替换恢复 ----
|
|
612
|
+
case 'restore-data': {
|
|
613
|
+
var restorePayload = payload.args.payload
|
|
614
|
+
if (restorePayload === null || typeof restorePayload !== 'object') return demoReply(false, '恢复内容不是有效的 JSON 对象')
|
|
615
|
+
var restoreInner = restorePayload.data !== null && typeof restorePayload.data === 'object' ? restorePayload.data : restorePayload
|
|
616
|
+
var looksLikeStore = ['version', 'enabled', 'budgetChars', 'activeSetId', 'sets', 'entries', 'profiles']
|
|
617
|
+
.some(function (key) { return key in restoreInner })
|
|
618
|
+
if (!looksLikeStore) return demoReply(false, '内容不像本插件的存储文件')
|
|
619
|
+
demoAdoptStore(restoreInner)
|
|
620
|
+
return demoReply(true, '已用备份整体替换当前记忆(演示页面只改内存,不写文件)')
|
|
621
|
+
}
|
|
622
|
+
// ---- F18 备份位置 ----
|
|
623
|
+
case 'export-to-dir':
|
|
624
|
+
return demoReply(true, '演示页面不会真的写文件;真实宿主会把导出写入「'
|
|
625
|
+
+ (DEMO_STORE.backupDir || '存储目录') + '」,文件名形如 dsh-instruction-memory-20261015-152355Z.json')
|
|
626
|
+
case 'export-data':
|
|
627
|
+
return {
|
|
628
|
+
ok: true,
|
|
629
|
+
json: async function () {
|
|
630
|
+
return {
|
|
631
|
+
ok: true, saved: false, message: null,
|
|
632
|
+
// 导出的是整份存储(所有配置集),不是当前激活的那一套。
|
|
633
|
+
export: { app: 'dsh-instruction-memory', version: 2, exportedAt: new Date().toISOString(), data: JSON.parse(JSON.stringify(DEMO_STORE)) },
|
|
634
|
+
snapshot: demoSnapshot(),
|
|
635
|
+
}
|
|
636
|
+
},
|
|
637
|
+
}
|
|
638
|
+
default:
|
|
639
|
+
break
|
|
640
|
+
}
|
|
641
|
+
return {
|
|
642
|
+
ok: true,
|
|
643
|
+
json: async function () {
|
|
644
|
+
return { ok: true, saved: true, message: null, snapshot: demoSnapshot() }
|
|
645
|
+
},
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
</script>
|
|
649
|
+
|
|
650
|
+
<!-- The real client module, unmodified. -->
|
|
651
|
+
<script src="../lib/client.js"></script>
|
|
652
|
+
|
|
653
|
+
<script>
|
|
654
|
+
var plugin = window.__dshSpec.factory(function (id) {
|
|
655
|
+
if (id === 'react') return window.React
|
|
656
|
+
throw new Error('unexpected require: ' + id)
|
|
657
|
+
})
|
|
658
|
+
|
|
659
|
+
var registered = []
|
|
660
|
+
plugin.apply({
|
|
661
|
+
slots: {
|
|
662
|
+
inject: function (key, callback) { callback(); return function () {} },
|
|
663
|
+
register: function (options, render) { registered.push(render); return function () {} },
|
|
664
|
+
},
|
|
665
|
+
effect: function (fn) { fn(); return function () {} },
|
|
666
|
+
})
|
|
667
|
+
|
|
668
|
+
ReactDOM.createRoot(document.getElementById('root')).render(registered[0]())
|
|
669
|
+
</script>
|
|
670
|
+
</body>
|
|
671
|
+
</html>
|