dsh-preset-workbench 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +206 -206
- package/lib/client.js +792 -792
- package/lib/index.js +1234 -1234
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -1,792 +1,792 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* preset-workbench (browser) - settings section "预设工作台".
|
|
3
|
-
* Registers one settings.section page: pick a user preset, edit its prompt
|
|
4
|
-
* sections (each with an enabled flag), save, create, delete, validate, or
|
|
5
|
-
* open its directory. All host work goes through the POST bridge
|
|
6
|
-
* /api/preset-workbench (loopback).
|
|
7
|
-
*
|
|
8
|
-
* Layout note: every control lives in the pinned top area; only the section
|
|
9
|
-
* cards scroll. Theme follows the shell via --dsw-alias-* tokens.
|
|
10
|
-
*/
|
|
11
|
-
(function () {
|
|
12
|
-
'use strict'
|
|
13
|
-
|
|
14
|
-
window.__ModuleLoader__.load({
|
|
15
|
-
id: 'dsh-preset-workbench',
|
|
16
|
-
factory: function (require) {
|
|
17
|
-
var module = { exports: {} }
|
|
18
|
-
var exports = module.exports
|
|
19
|
-
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
|
|
20
|
-
|
|
21
|
-
var react = require('react')
|
|
22
|
-
var h = react.createElement
|
|
23
|
-
|
|
24
|
-
var cssText = [
|
|
25
|
-
'.pwt-root{display:flex;flex-direction:column;gap:10px;font-size:13px;line-height:1.6;height:100%;min-height:0;color:var(--dsw-alias-label-primary)}',
|
|
26
|
-
'.pwt-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}',
|
|
27
|
-
'.pwt-select,.pwt-input{background:var(--dsw-specific-input-major,var(--dsw-alias-bg-layer-2));border:1px solid var(--dsw-alias-border-l2);border-radius:6px;color:var(--dsw-alias-label-primary);padding:4px 8px;font-size:13px}',
|
|
28
|
-
'.pwt-select option,.pwt-select optgroup{background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary)}',
|
|
29
|
-
'.pwt-btn{background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l2);border-radius:6px;color:var(--dsw-alias-label-primary);padding:4px 10px;cursor:pointer;font-size:13px}',
|
|
30
|
-
'.pwt-btn:hover:not([disabled]){background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}',
|
|
31
|
-
'.pwt-btnDanger{color:var(--dsw-alias-state-error-primary)}',
|
|
32
|
-
'.pwt-btn[disabled]{opacity:.45;cursor:default}',
|
|
33
|
-
'.pwt-status{color:var(--dsw-alias-label-secondary);font-size:12px;white-space:pre-wrap;word-break:break-all}',
|
|
34
|
-
'.pwt-statusError{color:var(--dsw-alias-state-error-primary)}',
|
|
35
|
-
'.pwt-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:8px;padding:8px;display:flex;flex-direction:column;gap:6px}',
|
|
36
|
-
'.pwt-sections{display:flex;flex-direction:column;gap:10px;overflow:auto;min-height:0;flex:1;padding-right:4px}',
|
|
37
|
-
'.pwt-ta{width:100%;box-sizing:border-box;background:var(--dsw-specific-input-major,var(--dsw-alias-bg-layer-2));border:1px solid var(--dsw-alias-border-l2);border-radius:6px;color:var(--dsw-alias-label-primary);font-family:Consolas,Menlo,monospace;font-size:12.5px;line-height:1.55;padding:8px;resize:vertical}',
|
|
38
|
-
'.pwt-ta:focus-visible,.pwt-input:focus-visible,.pwt-select:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-1px}',
|
|
39
|
-
'.pwt-hint{color:var(--dsw-alias-label-tertiary);font-size:12px}',
|
|
40
|
-
'.pwt-kind{display:inline-block;margin-right:8px;padding:1px 7px;border-radius:9px;font-size:11px;line-height:17px;white-space:nowrap;background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary);border:1px solid var(--dsw-alias-border-l1)}',
|
|
41
|
-
'.pwt-kindWarn{background:var(--dsw-alias-state-warn-primary);color:var(--dsw-alias-label-primary);border-color:transparent}',
|
|
42
|
-
'.pwt-file{color:var(--dsw-alias-label-tertiary);font-size:11px}',
|
|
43
|
-
'.pwt-grow{flex:1;min-width:160px}',
|
|
44
|
-
'.pwt-tpl{min-width:150px;max-width:220px;flex:0 0 auto}',
|
|
45
|
-
'.pwt-dd{position:relative;display:inline-block}',
|
|
46
|
-
'.pwt-dd-btn{display:flex;align-items:center;gap:8px;width:100%;background:var(--dsw-specific-menu,var(--dsw-alias-bg-layer-3))}',
|
|
47
|
-
'.pwt-dd-btn:hover{background:var(--dsw-specific-menu,var(--dsw-alias-bg-layer-3));border-color:var(--dsw-alias-label-dimmed)}',
|
|
48
|
-
'.pwt-dd-label{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1;text-align:left}',
|
|
49
|
-
'.pwt-dd-caret{color:var(--dsw-alias-label-tertiary);font-size:11px}',
|
|
50
|
-
'.pwt-dd-menu{position:absolute;top:calc(100% + 4px);left:0;min-width:230px;max-width:340px;max-height:280px;overflow-y:auto;background:var(--dsw-specific-menu,var(--dsw-alias-bg-layer-3));border:1px solid var(--dsw-alias-border-l2);border-radius:8px;box-shadow:0 6px 20px rgba(0,0,0,.28);z-index:999;padding:4px}',
|
|
51
|
-
'.pwt-dd-group{display:flex;flex-direction:column}',
|
|
52
|
-
'.pwt-dd-glabel{font-size:11px;color:var(--dsw-alias-label-tertiary);padding:3px 8px 2px}',
|
|
53
|
-
'.pwt-dd-opt{display:block;width:100%;text-align:left;background:none;border:none;color:var(--dsw-alias-label-primary);padding:5px 8px;border-radius:5px;cursor:pointer;font-size:13px}',
|
|
54
|
-
'.pwt-dd-opt:hover{background:var(--dsw-alias-button-floating-hover,var(--dsw-alias-interactive-bg-hover,var(--dsw-alias-bg-layer-2)));color:var(--dsw-alias-label-primary)}',
|
|
55
|
-
'.pwt-dd-sel{color:var(--dsw-alias-brand-primary);font-weight:600}',
|
|
56
|
-
'.pwt-dd-opt.pwt-dd-sel{background:var(--dsw-alias-interactive-bg-active,var(--dsw-alias-bg-layer-2))}',
|
|
57
|
-
'.pwt-title{font-weight:600}',
|
|
58
|
-
'details.pwt-help{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:6px 10px;background:var(--dsw-alias-bg-layer-3)}',
|
|
59
|
-
'details.pwt-help summary{cursor:pointer;color:var(--dsw-alias-label-secondary);font-size:12px;user-select:none}',
|
|
60
|
-
'details.pwt-help[open] summary{margin-bottom:6px}',
|
|
61
|
-
'details.pwt-help ul{margin:4px 0 4px 18px;padding:0}'
|
|
62
|
-
].join('\n')
|
|
63
|
-
|
|
64
|
-
function rpc(op, payload) {
|
|
65
|
-
var body = Object.assign({ op: op, lang: localStorage.getItem('pwt-lang') || 'zh' }, payload || {})
|
|
66
|
-
return fetch('/api/preset-workbench', {
|
|
67
|
-
method: 'POST',
|
|
68
|
-
headers: { 'content-type': 'application/json' },
|
|
69
|
-
body: JSON.stringify(body),
|
|
70
|
-
}).then(function (res) { return res.json() })
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// ── 字符串表(中/英)──「中英切换键」的文案仓库 ──
|
|
74
|
-
// t(key, ...args):按当前语言取 {0}/{1} 占位替换。
|
|
75
|
-
var DICT = {
|
|
76
|
-
zh: {
|
|
77
|
-
'sectionTitle': '预设工作台',
|
|
78
|
-
'lang': 'English',
|
|
79
|
-
'whatIs': '❓ 预设是什么?怎么用本页?点开看 30 秒说明',
|
|
80
|
-
'whatIsP1': '预设 = 新会话的「开局配置包」。你在新会话前选择哪个预设,那个会话里的 AI 就是什么人设、什么能力。',
|
|
81
|
-
'whatIsP2': '一个预设包含三样:① 人设提示词(本页编辑的主体,可分多段)② 能力配置(下方「能力开关」里打勾即可加减工具)③ 名字与简介',
|
|
82
|
-
'whatIsP3a': '新建自己的预设:',
|
|
83
|
-
'whatIsP3b': '点下方「新建预设」→ 起个 id(英文小写,如 my-rp)和显示名 → 它会复制鲸鱼娘的全部内容 → 在编辑区改成你的设定 → 点「保存全部」→ 新开会话,在选择器里选它。完事。',
|
|
84
|
-
'whatIsP4a': '改名/改简介:',
|
|
85
|
-
'whatIsP4b': '选中预设 → 点「打开目录」→ 用记事本改 preset.yml 里的 name 和 description 两行。',
|
|
86
|
-
'whatIsP5a': '勾选框:',
|
|
87
|
-
'whatIsP5b': '取消勾选的段保留文字但不进入系统提示词(不给 AI 看)。',
|
|
88
|
-
'whatIsP6a': '保存后:',
|
|
89
|
-
'whatIsP6b': '只对之后新开的会话生效,当前正在聊的会话不变。',
|
|
90
|
-
'currentPreset': '当前预设:',
|
|
91
|
-
'openDir': '打开目录',
|
|
92
|
-
'openDirTip': '在资源管理器中打开这个预设的文件夹',
|
|
93
|
-
'validate': '校验挂载',
|
|
94
|
-
'validateTip': '模拟一次会话加载,检查预设能否正常使用',
|
|
95
|
-
'bindTitle': '工作区默认预设',
|
|
96
|
-
'bindDesc': '给每个工作项目(工作区目录)绑定一个预设。之后在这个工作区新开的会话会自动用它,不用每次手动选;没绑定的目录沿用全局默认。对已经在聊的会话无影响。',
|
|
97
|
-
'bindEmpty': '还没有工作区。先在左侧栏添加工作项目,再回来绑定。',
|
|
98
|
-
'bindNone': '(不绑定,用全局默认)',
|
|
99
|
-
'bindOrphan': '此目录已不在工作区列表,但绑定仍在——新会话若从这里启动仍会生效。',
|
|
100
|
-
'bindHint': '子目录里的新会话也会继承绑定。手动为新会话选过预设时,以手动选择为准。',
|
|
101
|
-
'bindOk': '已绑定:新会话将使用「{0}」。',
|
|
102
|
-
'bindCleared': '已解除绑定,恢复全局默认。',
|
|
103
|
-
'bindFail': '绑定失败: ',
|
|
104
|
-
'newPreset': '新建预设',
|
|
105
|
-
'collapse': '收起新建',
|
|
106
|
-
'deletePreset': '删除预设',
|
|
107
|
-
'deleteTip': '删除整个预设文件夹(两击确认防手滑)',
|
|
108
|
-
'confirmDel': '⚠ 确认删除 {0} ?再点一次',
|
|
109
|
-
'selectUnit': '(选择)',
|
|
110
|
-
'brokenTag': '(损坏)',
|
|
111
|
-
'template': '模板:',
|
|
112
|
-
'idLabel': 'id:',
|
|
113
|
-
'nameLabel': '显示名:',
|
|
114
|
-
'idPlaceholder': '英文小写,如 my-rp',
|
|
115
|
-
'namePlaceholder': '如 我的角色扮演',
|
|
116
|
-
'createFrom': '从模板创建',
|
|
117
|
-
'noTemplateSel': '请先选择模板',
|
|
118
|
-
'creating': '创建中…',
|
|
119
|
-
'created': '已创建: {0}。现在编辑它的提示词段落,改完记得点「保存全部」,然后在新会话的选择器里选它。',
|
|
120
|
-
'createFail': '创建失败: ',
|
|
121
|
-
'deleteFail': '删除失败: ',
|
|
122
|
-
'deleted': '已删除预设: {0}',
|
|
123
|
-
'addSection': '+添加段落',
|
|
124
|
-
'saveAll': '💾 保存全部',
|
|
125
|
-
'saveHintNone': '(先在上面选择一个预设)',
|
|
126
|
-
'saveHintLoading': '(读取中…)',
|
|
127
|
-
'saving': '保存中…',
|
|
128
|
-
'saved': '已保存 {0} 段。新开的会话生效;当前会话不变。',
|
|
129
|
-
'saveFail': '保存失败: ',
|
|
130
|
-
'validating': '校验中…',
|
|
131
|
-
'validOk': '挂载校验通过 ✔ 下次新开会话可以正常加载这个预设。',
|
|
132
|
-
'validFail': '挂载校验失败:{0}',
|
|
133
|
-
'validReqFail': '校验请求失败: ',
|
|
134
|
-
'openDirFail': '打开目录失败: ',
|
|
135
|
-
'openDirOk': '已打开目录: {0}(可在此用记事本改 preset.yml 的名字和简介)',
|
|
136
|
-
'loadFail': '读取失败: ',
|
|
137
|
-
'capsTitle': '🧰 能力开关:给这个预设加/减工具(打勾立即生效,自动校验)',
|
|
138
|
-
'capsDesc': '下面是可选能力。勾上 → 把对应工具装进这个预设;取消 → 移除。每次切换会自动校验并提示结果。只对之后新开的会话生效。',
|
|
139
|
-
'capsRo': '官方预设只读——不可修改。想改成自己的,复制一份再编辑。',
|
|
140
|
-
'capsHint': '选中预设后这里会列出可开关的能力',
|
|
141
|
-
'capOn': '开启',
|
|
142
|
-
'capOff': '关闭',
|
|
143
|
-
'capToggle': '能力「{0}」{1}。新会话生效;挂载校验{2}',
|
|
144
|
-
'capOk': '通过 ✔',
|
|
145
|
-
'capFail': '失败:{0}',
|
|
146
|
-
'capSwitchFail': '能力切换失败: ',
|
|
147
|
-
'badgeWired': '🔌 已接线(未启用)',
|
|
148
|
-
'badgeOn': '✓ 已启用',
|
|
149
|
-
'grpUser': '📁 用户模板',
|
|
150
|
-
'grpSystem': '🏁 官方预设',
|
|
151
|
-
'unitNone': '(未选择)',
|
|
152
|
-
'nameWrap': '({0})',
|
|
153
|
-
'curUserGrp': '用户预设',
|
|
154
|
-
'curSystemGrp': '官方预设(只读)',
|
|
155
|
-
'systemBadge': '官方预设',
|
|
156
|
-
'systemReadonly': '这是官方预设,只读——不可修改。想改成自己的,复制一份再编辑。',
|
|
157
|
-
'systemComp': '它带的能力组合:',
|
|
158
|
-
'compEmpty': '(没有可展示的组合行)',
|
|
159
|
-
'compOn': '启用',
|
|
160
|
-
'compOff': '关闭',
|
|
161
|
-
'compCore': '预设地基(不可关闭)',
|
|
162
|
-
'enabled': '启用',
|
|
163
|
-
'enabledTip': '勾选 = 这段生效;取消 = 保留文字但不给 AI 看',
|
|
164
|
-
'secTitlePh': '段落标题',
|
|
165
|
-
'delSec': '删除段',
|
|
166
|
-
'secTextPh': '这一段提示词内容…',
|
|
167
|
-
'loading': '读取中…',
|
|
168
|
-
'pickFirst': '先在上面选择一个预设',
|
|
169
|
-
'noSections': '该预设还没有分段提示词',
|
|
170
|
-
'metaName': '名称:{0} ',
|
|
171
|
-
'kindBuiltin': '📦 内置模板',
|
|
172
|
-
'kindBuiltinTip': '这个 id 与插件随包分发的模板同名,且内容与模板一致——该内容已随插件公开。',
|
|
173
|
-
'kindBuiltinModified': '📦 内置模板 · ✏️ 已改动',
|
|
174
|
-
'kindBuiltinModifiedTip': 'id 与随包模板同名,但内容已被改动——里面可能有你自己加的内容,不要假定它和模板一样可以公开。',
|
|
175
|
-
'kindCustom': '👤 自定义',
|
|
176
|
-
'kindCustomTip': '不在插件内置模板之列,是你自己创作的预设。',
|
|
177
|
-
'kindOfficial': '官方预设',
|
|
178
|
-
'kindOfficialTip': '由 DSH 官方或第三方插件声明,磁盘上没有目录,本工作台只能查看。',
|
|
179
|
-
},
|
|
180
|
-
en: {
|
|
181
|
-
'sectionTitle': 'Preset Workbench',
|
|
182
|
-
'lang': '中文',
|
|
183
|
-
'whatIs': '❓ What is a preset & how to use this page? 30-sec intro',
|
|
184
|
-
'whatIsP1': 'A preset is a "starter config pack" for a new session. Whichever preset you pick before a session sets that session\u2019s persona and capabilities.',
|
|
185
|
-
'whatIsP2': 'A preset has three parts: ① persona prompt (edited here, can be multi-section) ② capability config (toggle tools below in "Capabilities") ③ name & description',
|
|
186
|
-
'whatIsP3a': 'Create your own: ',
|
|
187
|
-
'whatIsP3b': 'click "New preset" → give an id (lowercase, e.g. my-rp) and display name → it copies whale-mom\u2019s full content → edit in the area → "Save all" → open a new session and pick it. Done.',
|
|
188
|
-
'whatIsP4a': 'Rename / edit description: ',
|
|
189
|
-
'whatIsP4b': 'select the preset → "Open folder" → edit name and description in preset.yml with a text editor.',
|
|
190
|
-
'whatIsP5a': 'Checkboxes: ',
|
|
191
|
-
'whatIsP5b': 'unchecked sections keep their text but are excluded from the system prompt (hidden from the AI).',
|
|
192
|
-
'whatIsP6a': 'After saving: ',
|
|
193
|
-
'whatIsP6b': 'only affects sessions opened afterwards; the current conversation stays unchanged.',
|
|
194
|
-
'currentPreset': 'Current preset:',
|
|
195
|
-
'openDir': 'Open folder',
|
|
196
|
-
'openDirTip': 'Open this preset\u2019s folder in the file explorer',
|
|
197
|
-
'validate': 'Validate load',
|
|
198
|
-
'validateTip': 'Simulate a session load to check the preset works',
|
|
199
|
-
'bindTitle': 'Workspace default presets',
|
|
200
|
-
'bindDesc': 'Bind a preset to each work project (workspace directory). New sessions started in that workspace automatically use it — no manual picking; unbound directories keep the global default. Running conversations are unaffected.',
|
|
201
|
-
'bindEmpty': 'No workspaces yet. Add a work project in the sidebar first, then come back to bind.',
|
|
202
|
-
'bindNone': '(none — use global default)',
|
|
203
|
-
'bindOrphan': 'This directory left the workspace list, but the binding remains — new sessions started here still apply it.',
|
|
204
|
-
'bindHint': 'Sessions started in subdirectories inherit the binding too. An explicit preset picked for a new session always wins.',
|
|
205
|
-
'bindOk': 'Bound: new sessions will use “{0}”.',
|
|
206
|
-
'bindCleared': 'Binding removed; the global default applies again.',
|
|
207
|
-
'bindFail': 'Binding failed: ',
|
|
208
|
-
'newPreset': 'New preset',
|
|
209
|
-
'collapse': 'Collapse',
|
|
210
|
-
'deletePreset': 'Delete preset',
|
|
211
|
-
'deleteTip': 'Delete the whole preset folder (two clicks to confirm)',
|
|
212
|
-
'confirmDel': '⚠ Confirm delete {0}? Click again',
|
|
213
|
-
'selectUnit': '(select)',
|
|
214
|
-
'brokenTag': '(broken)',
|
|
215
|
-
'template': 'Template:',
|
|
216
|
-
'idLabel': 'id:',
|
|
217
|
-
'nameLabel': 'Name:',
|
|
218
|
-
'idPlaceholder': 'lowercase, e.g. my-rp',
|
|
219
|
-
'namePlaceholder': 'e.g. My Roleplay',
|
|
220
|
-
'createFrom': 'Create from template',
|
|
221
|
-
'noTemplateSel': 'Please select a template first',
|
|
222
|
-
'creating': 'Creating…',
|
|
223
|
-
'created': 'Created {0}. Now edit its prompt sections, then click "Save all", and pick it in a new session\u2019s selector.',
|
|
224
|
-
'createFail': 'Create failed: ',
|
|
225
|
-
'deleteFail': 'Delete failed: ',
|
|
226
|
-
'deleted': 'Deleted preset: {0}',
|
|
227
|
-
'addSection': '+ Add section',
|
|
228
|
-
'saveAll': '💾 Save all',
|
|
229
|
-
'saveHintNone': '(pick a preset above first)',
|
|
230
|
-
'saveHintLoading': '(loading…)',
|
|
231
|
-
'saving': 'Saving…',
|
|
232
|
-
'saved': 'Saved {0} section(s). Affects only new sessions; the current conversation is unchanged.',
|
|
233
|
-
'saveFail': 'Save failed: ',
|
|
234
|
-
'validating': 'Validating…',
|
|
235
|
-
'validOk': 'Mount check passed ✔ New sessions can load this preset normally.',
|
|
236
|
-
'validFail': 'Mount check failed: {0}',
|
|
237
|
-
'validReqFail': 'Validate request failed: ',
|
|
238
|
-
'openDirFail': 'Open folder failed: ',
|
|
239
|
-
'openDirOk': 'Opened folder: {0} (edit name & description in preset.yml here)',
|
|
240
|
-
'loadFail': 'Load failed: ',
|
|
241
|
-
'capsTitle': '🧰 Capabilities: add/remove tools for this preset (toggle applies immediately, auto-validated)',
|
|
242
|
-
'capsDesc': 'The optional capabilities below. Check → install the tool into this preset; uncheck → remove it. Each toggle auto-validates. Affects only new sessions.',
|
|
243
|
-
'capsRo': 'Official presets are read-only — not editable. Duplicate one to make your own.',
|
|
244
|
-
'capsHint': 'Capabilities will appear here after picking a preset',
|
|
245
|
-
'capOn': 'on',
|
|
246
|
-
'capOff': 'off',
|
|
247
|
-
'capToggle': 'Capability "{0}" {1}. New sessions; mount check {2}',
|
|
248
|
-
'capOk': 'passed ✔',
|
|
249
|
-
'capFail': 'failed: {0}',
|
|
250
|
-
'capSwitchFail': 'Capability toggle failed: ',
|
|
251
|
-
'badgeWired': '🔌 wired (inactive)',
|
|
252
|
-
'badgeOn': '✓ active',
|
|
253
|
-
'grpUser': '📁 User templates',
|
|
254
|
-
'grpSystem': '🏁 Official presets',
|
|
255
|
-
'unitNone': '(not selected)',
|
|
256
|
-
'nameWrap': '({0})',
|
|
257
|
-
'curUserGrp': 'User presets',
|
|
258
|
-
'curSystemGrp': 'Official presets (read-only)',
|
|
259
|
-
'systemBadge': 'Official preset',
|
|
260
|
-
'systemReadonly': 'This is an official preset — read-only. Not editable. Duplicate it to make your own.',
|
|
261
|
-
'systemComp': 'Its capability composition:',
|
|
262
|
-
'compEmpty': '(no rows to show)',
|
|
263
|
-
'compOn': 'on',
|
|
264
|
-
'compOff': 'off',
|
|
265
|
-
'compCore': 'preset core (cannot disable)',
|
|
266
|
-
'enabled': 'Enabled',
|
|
267
|
-
'enabledTip': 'checked = this section is active; unchecked = kept but hidden from the AI',
|
|
268
|
-
'secTitlePh': 'Section title',
|
|
269
|
-
'delSec': 'Delete section',
|
|
270
|
-
'secTextPh': 'Prompt content for this section…',
|
|
271
|
-
'loading': 'Loading…',
|
|
272
|
-
'pickFirst': 'Start by picking a preset above',
|
|
273
|
-
'noSections': 'This preset has no prompt sections yet',
|
|
274
|
-
'metaName': 'Name: {0} ',
|
|
275
|
-
'kindBuiltin': '📦 Built-in template',
|
|
276
|
-
'kindBuiltinTip': 'This id matches a template shipped with the plugin, and the content matches it byte for byte — that content is already public with the plugin.',
|
|
277
|
-
'kindBuiltinModified': '📦 Built-in template · ✏️ modified',
|
|
278
|
-
'kindBuiltinModifiedTip': 'The id matches a shipped template, but the content has been changed — it may contain your own additions, so do not assume it is as public as the template.',
|
|
279
|
-
'kindCustom': '👤 Custom',
|
|
280
|
-
'kindCustomTip': 'Not one of the plugin’s built-in templates; this is a preset you authored.',
|
|
281
|
-
'kindOfficial': 'Official preset',
|
|
282
|
-
'kindOfficialTip': 'Declared by DSH or a third-party plugin, with no directory on disk — this workbench can only view it.',
|
|
283
|
-
},
|
|
284
|
-
}
|
|
285
|
-
function t(orig) {
|
|
286
|
-
var lang = localStorage.getItem('pwt-lang') || 'zh'
|
|
287
|
-
var key = orig
|
|
288
|
-
var args = Array.prototype.slice.call(arguments, 1)
|
|
289
|
-
if (lang === 'zh' && DICT.zh[key]) return substitute(DICT.zh[key], args)
|
|
290
|
-
if (lang === 'en' && DICT.en[key]) return substitute(DICT.en[key], args)
|
|
291
|
-
// 未收录:回退原文
|
|
292
|
-
return orig
|
|
293
|
-
}
|
|
294
|
-
function substitute(tpl, args) {
|
|
295
|
-
var s = String(tpl)
|
|
296
|
-
for (var i = 0; i < args.length; i++) s = s.replace(new RegExp('\\{' + i + '\\}', 'g'), argStr(args[i]))
|
|
297
|
-
return s
|
|
298
|
-
}
|
|
299
|
-
function argStr(a) { return (a === undefined || a === null) ? '' : String(a) }
|
|
300
|
-
function langNow() { return localStorage.getItem('pwt-lang') || 'zh' }
|
|
301
|
-
function setLang(l) { localStorage.setItem('pwt-lang', l) }
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
// 渲染错误边界:一个子视图崩溃时显示可读的错误,而不是把整个设置页拖成空白。
|
|
305
|
-
// 参考 dsh-doctor 的 DoctorErrorBoundary 写法。
|
|
306
|
-
var PwtErrorBoundary = (function () {
|
|
307
|
-
var B = function (props) {
|
|
308
|
-
react.Component.call(this, props)
|
|
309
|
-
this.state = { failed: false, message: '' }
|
|
310
|
-
}
|
|
311
|
-
B.prototype = Object.create(react.Component.prototype)
|
|
312
|
-
B.prototype.constructor = B
|
|
313
|
-
B.getDerivedStateFromError = function () { return { failed: true } }
|
|
314
|
-
B.prototype.componentDidCatch = function (err) {
|
|
315
|
-
try { this.setState({ message: err && err.message ? err.message : String(err) }) } catch (_) {}
|
|
316
|
-
}
|
|
317
|
-
B.prototype.render = function () {
|
|
318
|
-
if (!this.state.failed) return this.props.children
|
|
319
|
-
return h('div', { className: 'pwt-status pwt-statusError', style: { padding: '8px', border: '1px solid var(--dsw-alias-state-error-primary)', borderRadius: '8px' } },
|
|
320
|
-
'◆ 预设工作台渲染出错(已捕获,不影响其他设置页): ' + (this.state.message || 'unknown'))
|
|
321
|
-
}
|
|
322
|
-
return B
|
|
323
|
-
})()
|
|
324
|
-
|
|
325
|
-
function TemplatePicker(props) {
|
|
326
|
-
var val = props.value || ''
|
|
327
|
-
var items = props.items || []
|
|
328
|
-
var onChange = props.onChange
|
|
329
|
-
var opn = react.useState(false); var open = opn[0]; var setOpen = opn[1]
|
|
330
|
-
var refBox = react.useRef(null)
|
|
331
|
-
var label = t('unitNone')
|
|
332
|
-
for (var i = 0; i < items.length; i++) {
|
|
333
|
-
if (items[i].source + ':' + items[i].id === val) {
|
|
334
|
-
label = items[i].id + (items[i].name ? t('nameWrap', items[i].name) : '')
|
|
335
|
-
break
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
react.useEffect(function () {
|
|
339
|
-
if (!open) return undefined
|
|
340
|
-
function onDown(e) {
|
|
341
|
-
var el = refBox.current
|
|
342
|
-
if (el && (el === e.target || el.contains(e.target))) return
|
|
343
|
-
setOpen(false)
|
|
344
|
-
}
|
|
345
|
-
function onKey(e) { if (e.key === 'Escape') setOpen(false) }
|
|
346
|
-
document.addEventListener('mousedown', onDown)
|
|
347
|
-
document.addEventListener('keydown', onKey)
|
|
348
|
-
return function () {
|
|
349
|
-
document.removeEventListener('mousedown', onDown)
|
|
350
|
-
document.removeEventListener('keydown', onKey)
|
|
351
|
-
}
|
|
352
|
-
}, [open])
|
|
353
|
-
var groups = [
|
|
354
|
-
{ label: t('grpUser'), source: 'user' },
|
|
355
|
-
{ label: t('grpSystem'), source: 'system' }
|
|
356
|
-
]
|
|
357
|
-
return h('div', { ref: refBox, className: 'pwt-dd' },
|
|
358
|
-
h('button', { type: 'button', className: 'pwt-btn pwt-tpl pwt-dd-btn', onClick: function () { setOpen(!open) } },
|
|
359
|
-
h('span', { className: 'pwt-dd-label' }, label),
|
|
360
|
-
h('span', { className: 'pwt-dd-caret' }, open ? '▲' : '▼')
|
|
361
|
-
),
|
|
362
|
-
open ? h('div', { className: 'pwt-dd-menu' },
|
|
363
|
-
groups.map(function (g) {
|
|
364
|
-
var arr = items.filter(function (t) { return t.source === g.source })
|
|
365
|
-
if (!arr.length) return null
|
|
366
|
-
return h('div', { key: g.source, className: 'pwt-dd-group' },
|
|
367
|
-
h('div', { className: 'pwt-dd-glabel' }, g.label),
|
|
368
|
-
arr.map(function (
|
|
369
|
-
var v =
|
|
370
|
-
return h('button', { key: v, type: 'button', className: 'pwt-dd-opt' + (v === val ? ' pwt-dd-sel' : ''), onClick: function () { onChange(v); setOpen(false) } },
|
|
371
|
-
|
|
372
|
-
})
|
|
373
|
-
)
|
|
374
|
-
})
|
|
375
|
-
) : null
|
|
376
|
-
)
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
function WorkbenchPage() {
|
|
380
|
-
var presetsL = react.useState(null); var list = presetsL[0]; var setList = presetsL[1]
|
|
381
|
-
var cur = react.useState(''); var current = cur[0]; var setCurrent = cur[1]
|
|
382
|
-
var dta = react.useState(null); var data = dta[0]; var setData = dta[1]
|
|
383
|
-
var stt = react.useState(''); var status = stt[0]; var setStatus = stt[1]
|
|
384
|
-
var bsy = react.useState(false); var busy = bsy[0]; var setBusy = bsy[1]
|
|
385
|
-
var shw = react.useState(false); var showNew = shw[0]; var setShowNew = shw[1]
|
|
386
|
-
var nid = react.useState(''); var newId = nid[0]; var setNewId = nid[1]
|
|
387
|
-
var nnm = react.useState(''); var newName = nnm[0]; var setNewName = nnm[1]
|
|
388
|
-
var tpl = react.useState(null); var templates = tpl[0]; var setTemplates = tpl[1]
|
|
389
|
-
var tplSel = react.useState(''); var template = tplSel[0]; var setTemplate = tplSel[1]
|
|
390
|
-
var cfd = react.useState(false); var confirmDel = cfd[0]; var setConfirmDel = cfd[1]
|
|
391
|
-
var lng = react.useState(langNow()); var lang = lng[0]; var setLangState = lng[1]
|
|
392
|
-
var curRo = react.useState(false); var currentReadonly = curRo[0]; var setCurrentReadonly = curRo[1]
|
|
393
|
-
var bnd = react.useState(null); var bindings = bnd[0]; var setBindings = bnd[1]
|
|
394
|
-
|
|
395
|
-
var loadBindings = function () {
|
|
396
|
-
return rpc('bindings').then(function (r) {
|
|
397
|
-
if (r && r.ok) setBindings(r.rows || [])
|
|
398
|
-
}).catch(function () { setBindings([]) })
|
|
399
|
-
}
|
|
400
|
-
react.useEffect(function () { loadBindings() }, [])
|
|
401
|
-
|
|
402
|
-
var setWorkspaceBinding = function (path, presetId) {
|
|
403
|
-
setBusy(true)
|
|
404
|
-
var call = presetId ? rpc('setBinding', { path: path, preset: presetId }) : rpc('clearBinding', { path: path })
|
|
405
|
-
return call.then(function (r) {
|
|
406
|
-
if (!r || !r.ok) throw new Error(r && r.error || 'binding failed')
|
|
407
|
-
setStatus(presetId ? t('bindOk', presetId) : t('bindCleared'))
|
|
408
|
-
return loadBindings()
|
|
409
|
-
}).catch(function (e) { setStatus(t('bindFail') + e.message) }).finally(function () { setBusy(false) })
|
|
410
|
-
}
|
|
411
|
-
var toggleLang = function () {
|
|
412
|
-
var next = (langNow() === 'en') ? 'zh' : 'en'
|
|
413
|
-
setLang(next)
|
|
414
|
-
setLangState(next)
|
|
415
|
-
// 语言变了 → 重新拉取能力开关(title/desc 由宿主按 lang 给),保持整页文案一致
|
|
416
|
-
if (current) capLoad(current)
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
var refresh = function () {
|
|
420
|
-
var tarr = []
|
|
421
|
-
return rpc('list').then(function (r) {
|
|
422
|
-
if (!r || !r.ok) throw new Error(r && r.error || 'load failed')
|
|
423
|
-
var arr = r.presets || []
|
|
424
|
-
tarr = arr
|
|
425
|
-
setList(arr)
|
|
426
|
-
setCurrent(function (prev) {
|
|
427
|
-
return prev || (arr[0] ? arr[0].id : '')
|
|
428
|
-
})
|
|
429
|
-
return rpc('templates')
|
|
430
|
-
}).then(function (r) {
|
|
431
|
-
if (!r || !r.ok) throw new Error(r && r.error || 'templates failed')
|
|
432
|
-
setTemplates(r.templates || [])
|
|
433
|
-
setTemplate(function (prev) {
|
|
434
|
-
if (prev) return prev
|
|
435
|
-
var ts = r.templates || []
|
|
436
|
-
return ts.length ? (ts[0].source + ':' + ts[0].id) : ''
|
|
437
|
-
})
|
|
438
|
-
}).catch(function (e) {
|
|
439
|
-
// 兼容兜底:模板接口不可用(如宿主未重启/旧版本)时,
|
|
440
|
-
// 用用户预设 + 官方四个当模板即可(内置模板已并入用户组,不单独出现)。
|
|
441
|
-
var ts = []
|
|
442
|
-
for (var i = 0; i < tarr.length; i++) ts.push({ id: tarr[i].id, name: tarr[i].name, source: 'user' })
|
|
443
|
-
var official = ['standard', 'code', 'minimal', 'cordis']
|
|
444
|
-
for (var o = 0; o < official.length; o++) ts.push({ id: official[o], name: undefined, source: 'system' })
|
|
445
|
-
setTemplates(ts)
|
|
446
|
-
setTemplate(function (prev) { return prev || (ts[0] ? ts[0].source + ':' + ts[0].id : '') })
|
|
447
|
-
})
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
var roOf = function (id, arr) {
|
|
451
|
-
if (!arr) return false
|
|
452
|
-
for (var i = 0; i < arr.length; i++) if (arr[i].id === id) return !!arr[i].readonly
|
|
453
|
-
return false
|
|
454
|
-
}
|
|
455
|
-
react.useEffect(function () { setCurrentReadonly(roOf(current, list)) }, [current, list])
|
|
456
|
-
|
|
457
|
-
var load = function (id) {
|
|
458
|
-
if (!id) { setData(null); return }
|
|
459
|
-
setBusy(true)
|
|
460
|
-
rpc('load', { id: id }).then(function (r) {
|
|
461
|
-
if (!r || !r.ok) throw new Error(r && r.error || 'load failed')
|
|
462
|
-
setData(r)
|
|
463
|
-
setStatus('')
|
|
464
|
-
}).catch(function (e) {
|
|
465
|
-
setData(null)
|
|
466
|
-
setStatus(t('loadFail') + e.message)
|
|
467
|
-
}).finally(function () { setBusy(false) })
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
react.useEffect(function () { refresh() }, [])
|
|
471
|
-
react.useEffect(function () { load(current) }, [current])
|
|
472
|
-
|
|
473
|
-
var save = function () {
|
|
474
|
-
if (!data || !current) return
|
|
475
|
-
setBusy(true)
|
|
476
|
-
setStatus(t('saving'))
|
|
477
|
-
rpc('save', { id: current, sections: data.sections }).then(function (r) {
|
|
478
|
-
if (!r || !r.ok) throw new Error(r && r.error || 'save failed')
|
|
479
|
-
setStatus(t('saved', (r.wrote || 0)))
|
|
480
|
-
}).catch(function (e) { setStatus(t('saveFail') + e.message) }).finally(function () { setBusy(false) })
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
var doValidate = function () {
|
|
484
|
-
if (!current) return
|
|
485
|
-
setStatus(t('validating'))
|
|
486
|
-
rpc('validate', { id: current }).then(function (r) {
|
|
487
|
-
setStatus(r && r.ok ? t('validOk') : t('validFail', (r && r.error)))
|
|
488
|
-
}).catch(function (e) { setStatus(t('validReqFail') + e.message) })
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
var doReveal = function () {
|
|
492
|
-
if (!current) return
|
|
493
|
-
rpc('reveal', { id: current }).then(function (r) {
|
|
494
|
-
if (!r || !r.ok) throw new Error(r && r.error || 'reveal failed')
|
|
495
|
-
setStatus(t('openDirOk', r.dir))
|
|
496
|
-
}).catch(function (e) { setStatus(t('openDirFail') + e.message) })
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
var doCreate = function () {
|
|
500
|
-
if (!template) { setStatus(t('noTemplateSel')); return }
|
|
501
|
-
setBusy(true)
|
|
502
|
-
setStatus(t('creating'))
|
|
503
|
-
rpc('create', { id: newId, name: newName, from: template }).then(function (r) {
|
|
504
|
-
if (!r || !r.ok) throw new Error(r && r.error || 'create failed')
|
|
505
|
-
setShowNew(false)
|
|
506
|
-
setNewId('')
|
|
507
|
-
setNewName('')
|
|
508
|
-
setStatus(t('created', r.id))
|
|
509
|
-
return refresh().then(function () { setCurrent(r.id) })
|
|
510
|
-
}).catch(function (e) { setStatus(t('createFail') + e.message) }).finally(function () { setBusy(false) })
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
var doDelete = function () {
|
|
514
|
-
if (!current) return
|
|
515
|
-
setBusy(true)
|
|
516
|
-
rpc('delete', { id: current }).then(function (r) {
|
|
517
|
-
if (!r || !r.ok) throw new Error(r && r.error || 'delete failed')
|
|
518
|
-
setStatus(t('deleted', current))
|
|
519
|
-
setConfirmDel(false)
|
|
520
|
-
setData(null)
|
|
521
|
-
setCurrent('')
|
|
522
|
-
return refresh()
|
|
523
|
-
}).catch(function (e) {
|
|
524
|
-
setStatus(t('deleteFail') + e.message)
|
|
525
|
-
setConfirmDel(false)
|
|
526
|
-
}).finally(function () { setBusy(false) })
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
var patchSection = function (i, patch) {
|
|
530
|
-
setData(function (d) {
|
|
531
|
-
if (!d) return d
|
|
532
|
-
var arr = d.sections.slice()
|
|
533
|
-
arr[i] = Object.assign({}, arr[i], patch)
|
|
534
|
-
return Object.assign({}, d, { sections: arr })
|
|
535
|
-
})
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
var addSection = function () {
|
|
539
|
-
setData(function (d) {
|
|
540
|
-
if (!d) return d
|
|
541
|
-
var arr = d.sections.slice()
|
|
542
|
-
arr.push({ file: 'section.' + Date.now() + '.md', title: '新段落', enabled: true, text: '' })
|
|
543
|
-
return Object.assign({}, d, { sections: arr })
|
|
544
|
-
})
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
var removeSection = function (i) {
|
|
548
|
-
setData(function (d) {
|
|
549
|
-
if (!d) return d
|
|
550
|
-
var arr = d.sections.slice()
|
|
551
|
-
arr.splice(i, 1)
|
|
552
|
-
return Object.assign({}, d, { sections: arr })
|
|
553
|
-
})
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
// ── 能力开关 ──
|
|
557
|
-
var capS = react.useState(null); var caps = capS[0]; var setCaps = capS[1]
|
|
558
|
-
|
|
559
|
-
var capLoad = function (id) {
|
|
560
|
-
if (!id) { setCaps(null); return }
|
|
561
|
-
rpc('caps', { id: id }).then(function (r) {
|
|
562
|
-
if (r && r.ok) setCaps(r.caps || [])
|
|
563
|
-
}).catch(function () { setCaps(null) })
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
react.useEffect(function () { capLoad(current) }, [current])
|
|
567
|
-
|
|
568
|
-
var toggleCap = function (c) {
|
|
569
|
-
if (!current) return
|
|
570
|
-
setBusy(true)
|
|
571
|
-
rpc('setCap', { id: current, capId: c.id, enabled: !c.enabled }).then(function (r) {
|
|
572
|
-
if (!r || !r.ok) throw new Error(r && r.error || 'setCap failed')
|
|
573
|
-
var verb = t(!c.enabled ? 'capOn' : 'capOff')
|
|
574
|
-
return rpc('caps', { id: current }).then(function (r2) {
|
|
575
|
-
if (r2 && r2.ok) setCaps(r2.caps)
|
|
576
|
-
return rpc('validate', { id: current }).then(function (v) {
|
|
577
|
-
setStatus(t('capToggle', c.title, verb, (v && v.ok ? t('capOk') : t('capFail', (v && v.error)))))
|
|
578
|
-
})
|
|
579
|
-
})
|
|
580
|
-
}).catch(function (e) { setStatus(t('capSwitchFail') + e.message) }).finally(function () { setBusy(false) })
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
var children = []
|
|
584
|
-
|
|
585
|
-
children.push(h('style', { key: 'pwt-style' }, cssText))
|
|
586
|
-
|
|
587
|
-
// ── 内置说明 ──
|
|
588
|
-
children.push(h('details', { key: 'help', className: 'pwt-help' },
|
|
589
|
-
h('summary', null, t('whatIs')),
|
|
590
|
-
h('div', null,
|
|
591
|
-
h('p', { style: { margin: '4px 0' } }, t('whatIsP1')),
|
|
592
|
-
h('ul', null,
|
|
593
|
-
h('li', null, t('whatIsP2'))),
|
|
594
|
-
h('p', { style: { margin: '4px 0' } },
|
|
595
|
-
h('b', null, t('whatIsP3a')), t('whatIsP3b')),
|
|
596
|
-
h('p', { style: { margin: '4px 0' } },
|
|
597
|
-
h('b', null, t('whatIsP4a')), t('whatIsP4b')),
|
|
598
|
-
h('p', { style: { margin: '4px 0' } },
|
|
599
|
-
h('b', null, t('whatIsP5a')), t('whatIsP5b')),
|
|
600
|
-
h('p', { style: { margin: '4px 0' } },
|
|
601
|
-
h('b', null, t('whatIsP6a')), t('whatIsP6b')))
|
|
602
|
-
))
|
|
603
|
-
|
|
604
|
-
// ── 选择与操作(钉在顶部,不随内容滚动)──
|
|
605
|
-
// 按只读性分组:用户预设可编辑,官方预设(system)只读。
|
|
606
|
-
var userOpts = []
|
|
607
|
-
var systemOpts = []
|
|
608
|
-
;(list || []).forEach(function (p) {
|
|
609
|
-
// 可编辑预设加来源标记:📦 = 与随包模板同名,👤 = 你自己创作的。
|
|
610
|
-
var mark = p.readonly ? '' : (p.kind === 'builtin' ? '📦 ' : '👤 ')
|
|
611
|
-
var label = mark + (p.name ? p.name + ' · ' : '') + p.id + (p.broken ? t('brokenTag') : '')
|
|
612
|
-
var opt = h('option', { key: p.id, value: p.id }, label)
|
|
613
|
-
if (p.readonly) systemOpts.push(opt)
|
|
614
|
-
else userOpts.push(opt)
|
|
615
|
-
})
|
|
616
|
-
var opts = [
|
|
617
|
-
userOpts.length ? h('optgroup', { key: 'user', label: t('curUserGrp') }, userOpts) : null,
|
|
618
|
-
systemOpts.length ? h('optgroup', { key: 'system', label: t('curSystemGrp') }, systemOpts) : null,
|
|
619
|
-
].filter(Boolean)
|
|
620
|
-
var currentMeta = (list || []).filter(function (p) { return p.id === current })[0]
|
|
621
|
-
var isRo = currentReadonly
|
|
622
|
-
|
|
623
|
-
children.push(h('div', { key: 'row1', className: 'pwt-row' },
|
|
624
|
-
h('span', null, t('currentPreset')),
|
|
625
|
-
h('select', { className: 'pwt-select', value: current, onChange: function (e) { setCurrent(e.target.value); setConfirmDel(false) }, disabled: busy },
|
|
626
|
-
h('option', { key: '', value: '' }, t('selectUnit')),
|
|
627
|
-
opts
|
|
628
|
-
),
|
|
629
|
-
h('button', { className: 'pwt-btn', onClick: doReveal, disabled: !current || isRo, title: t('openDirTip') }, t('openDir')),
|
|
630
|
-
h('button', { className: 'pwt-btn', onClick: doValidate, disabled: busy || !current, title: t('validateTip') }, t('validate')),
|
|
631
|
-
h('button', { className: 'pwt-btn', onClick: function () { setShowNew(!showNew); setConfirmDel(false) } }, showNew ? t('collapse') : t('newPreset')),
|
|
632
|
-
h('button', {
|
|
633
|
-
className: 'pwt-btn pwt-btnDanger',
|
|
634
|
-
onClick: function () { if (confirmDel) { doDelete() } else { setConfirmDel(true) } },
|
|
635
|
-
disabled: busy || !current || isRo,
|
|
636
|
-
title: t('deleteTip')
|
|
637
|
-
}, confirmDel ? t('confirmDel', current) : t('deletePreset')),
|
|
638
|
-
h('span', { className: 'pwt-grow' }),
|
|
639
|
-
h('button', { className: 'pwt-btn', onClick: toggleLang, title: lang === 'en' ? '切换到中文' : 'Switch to English' }, t('lang'))
|
|
640
|
-
))
|
|
641
|
-
|
|
642
|
-
if (currentMeta) {
|
|
643
|
-
// 来源徽章:📦 内置模板 / 📦·✏️ 已改动 / 👤 自定义 / 官方预设
|
|
644
|
-
var badge = (function (p) {
|
|
645
|
-
if (p.readonly) return { text: t('kindOfficial'), tip: t('kindOfficialTip'), warn: false }
|
|
646
|
-
if (p.kind === 'builtin') {
|
|
647
|
-
return p.modified
|
|
648
|
-
? { text: t('kindBuiltinModified'), tip: t('kindBuiltinModifiedTip'), warn: true }
|
|
649
|
-
: { text: t('kindBuiltin'), tip: t('kindBuiltinTip'), warn: false }
|
|
650
|
-
}
|
|
651
|
-
return { text: t('kindCustom'), tip: t('kindCustomTip'), warn: false }
|
|
652
|
-
})(currentMeta)
|
|
653
|
-
children.push(h('div', { key: 'meta', className: 'pwt-hint' },
|
|
654
|
-
h('span', { className: 'pwt-kind' + (badge.warn ? ' pwt-kindWarn' : ''), title: badge.tip }, badge.text),
|
|
655
|
-
(currentMeta.name ? t('metaName', currentMeta.name) : '') + (currentMeta.description || '')))
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
if (showNew) {
|
|
659
|
-
children.push(h('div', { key: 'rowNew', className: 'pwt-row' },
|
|
660
|
-
h('span', null, t('template')),
|
|
661
|
-
h(TemplatePicker, { value: template, items: templates || [], onChange: setTemplate }),
|
|
662
|
-
h('span', null, t('idLabel')),
|
|
663
|
-
h('input', { className: 'pwt-input', value: newId, placeholder: t('idPlaceholder'), onChange: function (e) { setNewId(e.target.value) } }),
|
|
664
|
-
h('span', null, t('nameLabel')),
|
|
665
|
-
h('input', { className: 'pwt-input', value: newName, placeholder: t('namePlaceholder'), onChange: function (e) { setNewName(e.target.value) } }),
|
|
666
|
-
h('button', { className: 'pwt-btn', onClick: doCreate, disabled: busy || !newId }, t('createFrom'))
|
|
667
|
-
))
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
// ── 编辑动作行 ──
|
|
671
|
-
var saveHint = ''
|
|
672
|
-
if (isRo) saveHint = t('systemBadge')
|
|
673
|
-
else if (!current) saveHint = t('saveHintNone')
|
|
674
|
-
else if (!data) saveHint = t('saveHintLoading')
|
|
675
|
-
children.push(h('div', { key: 'rowAct', className: 'pwt-row' },
|
|
676
|
-
h('button', { className: 'pwt-btn', onClick: addSection, disabled: busy || !data || isRo }, t('addSection')),
|
|
677
|
-
h('button', { className: 'pwt-btn', onClick: save, disabled: busy || !data || !current || isRo }, t('saveAll')),
|
|
678
|
-
h('span', { className: 'pwt-hint' }, saveHint)
|
|
679
|
-
))
|
|
680
|
-
|
|
681
|
-
children.push(h('div', { key: 'status', className: (/失败|failed/i.test(status)) ? 'pwt-status pwt-statusError' : 'pwt-status' }, status))
|
|
682
|
-
|
|
683
|
-
// ── 段落卡片(唯一滚动区)──
|
|
684
|
-
var cards = (data && data.sections ? data.sections : []).map(function (s, i) {
|
|
685
|
-
return h('div', { key: s.file + '-' + i, className: 'pwt-card' },
|
|
686
|
-
h('div', { className: 'pwt-row' },
|
|
687
|
-
h('label', { className: 'pwt-row', title: t('enabledTip') },
|
|
688
|
-
h('input', { type: 'checkbox', checked: s.enabled !== false, onChange: function (e) { patchSection(i, { enabled: e.target.checked }) } }),
|
|
689
|
-
h('span', { className: 'pwt-title' }, t('enabled'))
|
|
690
|
-
),
|
|
691
|
-
h('input', {
|
|
692
|
-
className: 'pwt-input pwt-grow', value: s.title || '', placeholder: t('secTitlePh'),
|
|
693
|
-
onChange: function (e) { patchSection(i, { title: e.target.value }) },
|
|
694
|
-
}),
|
|
695
|
-
h('span', { className: 'pwt-file' }, s.file),
|
|
696
|
-
h('button', { className: 'pwt-btn', onClick: function () { removeSection(i) } }, t('delSec'))
|
|
697
|
-
),
|
|
698
|
-
h('textarea', {
|
|
699
|
-
className: 'pwt-ta', rows: Math.min(20, Math.max(6, (s.text || '').split('\n').length + 1)),
|
|
700
|
-
value: s.text || '', placeholder: t('secTextPh'),
|
|
701
|
-
onChange: function (e) { patchSection(i, { text: e.target.value }) },
|
|
702
|
-
})
|
|
703
|
-
)
|
|
704
|
-
})
|
|
705
|
-
|
|
706
|
-
children.push(h('div', { key: 'sections', className: 'pwt-sections' },
|
|
707
|
-
// ── 能力开关(与段落卡片同属一个滚动区:展开后能一路滚到底)──
|
|
708
|
-
// 系统/官方预设只读:能力开关仅供查看,勾选禁用。
|
|
709
|
-
h('details', { key: 'caps', className: 'pwt-help' },
|
|
710
|
-
h('summary', null, t('capsTitle')),
|
|
711
|
-
h('div', null,
|
|
712
|
-
h('p', { style: { margin: '4px 0' } }, t('capsDesc')),
|
|
713
|
-
isRo ? h('div', { className: 'pwt-status' }, t('capsRo')) : null,
|
|
714
|
-
(caps === null
|
|
715
|
-
? h('div', { className: 'pwt-hint' }, t('capsHint'))
|
|
716
|
-
: h('div', { style: { display: 'flex', flexDirection: 'column', gap: 6 } },
|
|
717
|
-
caps.map(function (c) {
|
|
718
|
-
var badge = c.enabled ? (c.dormant ? t('badgeWired') : t('badgeOn')) : ''
|
|
719
|
-
var lock = c.locked
|
|
720
|
-
return h('label', { key: c.id, className: 'pwt-row', title: c.desc, style: { cursor: (isRo || lock) ? 'default' : 'pointer' } },
|
|
721
|
-
h('input', {
|
|
722
|
-
type: 'checkbox',
|
|
723
|
-
checked: lock ? true : !!c.enabled,
|
|
724
|
-
disabled: busy || !current || isRo || lock,
|
|
725
|
-
onChange: function () { if (lock) return; toggleCap(c) },
|
|
726
|
-
}),
|
|
727
|
-
h('span', { className: 'pwt-title' }, lock ? ('🔒 ' + c.title) : c.title),
|
|
728
|
-
badge ? h('span', { className: 'pwt-hint' }, badge) : null,
|
|
729
|
-
h('span', { className: 'pwt-hint' }, (lock ? t('compCore') + ' · ' : '') + c.desc)
|
|
730
|
-
)
|
|
731
|
-
})
|
|
732
|
-
))
|
|
733
|
-
)
|
|
734
|
-
),
|
|
735
|
-
// ── 每工作区默认预设:给每个工作区目录绑一个预设,新会话自动用 ──
|
|
736
|
-
h('details', { key: 'bindings', className: 'pwt-help' },
|
|
737
|
-
h('summary', null, t('bindTitle')),
|
|
738
|
-
h('div', null,
|
|
739
|
-
h('p', { style: { margin: '4px 0' } }, t('bindDesc')),
|
|
740
|
-
(bindings === null
|
|
741
|
-
? h('div', { className: 'pwt-hint' }, t('loading'))
|
|
742
|
-
: (bindings.length === 0
|
|
743
|
-
? h('div', { className: 'pwt-hint' }, t('bindEmpty'))
|
|
744
|
-
: h('div', { style: { display: 'flex', flexDirection: 'column', gap: 6 } },
|
|
745
|
-
bindings.map(function (b) {
|
|
746
|
-
var opts = [h('option', { key: '', value: '' }, t('bindNone'))]
|
|
747
|
-
;(list || []).forEach(function (p) {
|
|
748
|
-
opts.push(h('option', { key: p.id, value: p.id }, (p.name ? p.name + ' · ' : '') + p.id))
|
|
749
|
-
})
|
|
750
|
-
return h('div', { key: b.path, className: 'pwt-row' },
|
|
751
|
-
h('span', { className: 'pwt-file', title: b.path }, (b.orphan ? '⚠ ' : '') + (b.title || b.path)),
|
|
752
|
-
h('select', {
|
|
753
|
-
className: 'pwt-select',
|
|
754
|
-
value: b.preset || '',
|
|
755
|
-
disabled: busy,
|
|
756
|
-
onChange: function (e) { setWorkspaceBinding(b.path, e.target.value) },
|
|
757
|
-
}, opts),
|
|
758
|
-
b.orphan ? h('span', { className: 'pwt-hint' }, t('bindOrphan')) : null
|
|
759
|
-
)
|
|
760
|
-
})
|
|
761
|
-
))),
|
|
762
|
-
h('div', { className: 'pwt-hint' }, t('bindHint'))
|
|
763
|
-
)
|
|
764
|
-
),
|
|
765
|
-
// 段落卡片区(能力开关已并入,不再单独列官方组合块)
|
|
766
|
-
data === null
|
|
767
|
-
? h('div', { className: 'pwt-hint' }, current ? t('loading') : t('pickFirst'))
|
|
768
|
-
: (data.sections.length === 0 ? h('div', { className: 'pwt-hint' }, t('noSections')) : cards)
|
|
769
|
-
))
|
|
770
|
-
|
|
771
|
-
return h('div', { className: 'pwt-root' }, children)
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
function apply(ctx) {
|
|
775
|
-
var slots = ctx.get('slots')
|
|
776
|
-
if (slots === undefined) return
|
|
777
|
-
slots.inject('settings.section', function () {
|
|
778
|
-
slots.register(
|
|
779
|
-
{ name: 'settings.section', id: 'preset-workbench', order: 60, label: (langNow() === 'en' ? 'Preset Workbench' : '预设工作台') },
|
|
780
|
-
function (props) { return h(PwtErrorBoundary, null, h(WorkbenchPage, props || {})) }
|
|
781
|
-
)
|
|
782
|
-
})
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
exports.apply = apply
|
|
786
|
-
exports.inject = ['slots']
|
|
787
|
-
exports.default = { apply: apply, inject: ['slots'] }
|
|
788
|
-
|
|
789
|
-
return module.exports
|
|
790
|
-
},
|
|
791
|
-
})
|
|
792
|
-
})()
|
|
1
|
+
/**
|
|
2
|
+
* preset-workbench (browser) - settings section "预设工作台".
|
|
3
|
+
* Registers one settings.section page: pick a user preset, edit its prompt
|
|
4
|
+
* sections (each with an enabled flag), save, create, delete, validate, or
|
|
5
|
+
* open its directory. All host work goes through the POST bridge
|
|
6
|
+
* /api/preset-workbench (loopback).
|
|
7
|
+
*
|
|
8
|
+
* Layout note: every control lives in the pinned top area; only the section
|
|
9
|
+
* cards scroll. Theme follows the shell via --dsw-alias-* tokens.
|
|
10
|
+
*/
|
|
11
|
+
(function () {
|
|
12
|
+
'use strict'
|
|
13
|
+
|
|
14
|
+
window.__ModuleLoader__.load({
|
|
15
|
+
id: 'dsh-preset-workbench',
|
|
16
|
+
factory: function (require) {
|
|
17
|
+
var module = { exports: {} }
|
|
18
|
+
var exports = module.exports
|
|
19
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
|
|
20
|
+
|
|
21
|
+
var react = require('react')
|
|
22
|
+
var h = react.createElement
|
|
23
|
+
|
|
24
|
+
var cssText = [
|
|
25
|
+
'.pwt-root{display:flex;flex-direction:column;gap:10px;font-size:13px;line-height:1.6;height:100%;min-height:0;color:var(--dsw-alias-label-primary)}',
|
|
26
|
+
'.pwt-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}',
|
|
27
|
+
'.pwt-select,.pwt-input{background:var(--dsw-specific-input-major,var(--dsw-alias-bg-layer-2));border:1px solid var(--dsw-alias-border-l2);border-radius:6px;color:var(--dsw-alias-label-primary);padding:4px 8px;font-size:13px}',
|
|
28
|
+
'.pwt-select option,.pwt-select optgroup{background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary)}',
|
|
29
|
+
'.pwt-btn{background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l2);border-radius:6px;color:var(--dsw-alias-label-primary);padding:4px 10px;cursor:pointer;font-size:13px}',
|
|
30
|
+
'.pwt-btn:hover:not([disabled]){background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}',
|
|
31
|
+
'.pwt-btnDanger{color:var(--dsw-alias-state-error-primary)}',
|
|
32
|
+
'.pwt-btn[disabled]{opacity:.45;cursor:default}',
|
|
33
|
+
'.pwt-status{color:var(--dsw-alias-label-secondary);font-size:12px;white-space:pre-wrap;word-break:break-all}',
|
|
34
|
+
'.pwt-statusError{color:var(--dsw-alias-state-error-primary)}',
|
|
35
|
+
'.pwt-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:8px;padding:8px;display:flex;flex-direction:column;gap:6px}',
|
|
36
|
+
'.pwt-sections{display:flex;flex-direction:column;gap:10px;overflow:auto;min-height:0;flex:1;padding-right:4px}',
|
|
37
|
+
'.pwt-ta{width:100%;box-sizing:border-box;background:var(--dsw-specific-input-major,var(--dsw-alias-bg-layer-2));border:1px solid var(--dsw-alias-border-l2);border-radius:6px;color:var(--dsw-alias-label-primary);font-family:Consolas,Menlo,monospace;font-size:12.5px;line-height:1.55;padding:8px;resize:vertical}',
|
|
38
|
+
'.pwt-ta:focus-visible,.pwt-input:focus-visible,.pwt-select:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-1px}',
|
|
39
|
+
'.pwt-hint{color:var(--dsw-alias-label-tertiary);font-size:12px}',
|
|
40
|
+
'.pwt-kind{display:inline-block;margin-right:8px;padding:1px 7px;border-radius:9px;font-size:11px;line-height:17px;white-space:nowrap;background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary);border:1px solid var(--dsw-alias-border-l1)}',
|
|
41
|
+
'.pwt-kindWarn{background:var(--dsw-alias-state-warn-primary);color:var(--dsw-alias-label-primary);border-color:transparent}',
|
|
42
|
+
'.pwt-file{color:var(--dsw-alias-label-tertiary);font-size:11px}',
|
|
43
|
+
'.pwt-grow{flex:1;min-width:160px}',
|
|
44
|
+
'.pwt-tpl{min-width:150px;max-width:220px;flex:0 0 auto}',
|
|
45
|
+
'.pwt-dd{position:relative;display:inline-block}',
|
|
46
|
+
'.pwt-dd-btn{display:flex;align-items:center;gap:8px;width:100%;background:var(--dsw-specific-menu,var(--dsw-alias-bg-layer-3))}',
|
|
47
|
+
'.pwt-dd-btn:hover{background:var(--dsw-specific-menu,var(--dsw-alias-bg-layer-3));border-color:var(--dsw-alias-label-dimmed)}',
|
|
48
|
+
'.pwt-dd-label{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1;text-align:left}',
|
|
49
|
+
'.pwt-dd-caret{color:var(--dsw-alias-label-tertiary);font-size:11px}',
|
|
50
|
+
'.pwt-dd-menu{position:absolute;top:calc(100% + 4px);left:0;min-width:230px;max-width:340px;max-height:280px;overflow-y:auto;background:var(--dsw-specific-menu,var(--dsw-alias-bg-layer-3));border:1px solid var(--dsw-alias-border-l2);border-radius:8px;box-shadow:0 6px 20px rgba(0,0,0,.28);z-index:999;padding:4px}',
|
|
51
|
+
'.pwt-dd-group{display:flex;flex-direction:column}',
|
|
52
|
+
'.pwt-dd-glabel{font-size:11px;color:var(--dsw-alias-label-tertiary);padding:3px 8px 2px}',
|
|
53
|
+
'.pwt-dd-opt{display:block;width:100%;text-align:left;background:none;border:none;color:var(--dsw-alias-label-primary);padding:5px 8px;border-radius:5px;cursor:pointer;font-size:13px}',
|
|
54
|
+
'.pwt-dd-opt:hover{background:var(--dsw-alias-button-floating-hover,var(--dsw-alias-interactive-bg-hover,var(--dsw-alias-bg-layer-2)));color:var(--dsw-alias-label-primary)}',
|
|
55
|
+
'.pwt-dd-sel{color:var(--dsw-alias-brand-primary);font-weight:600}',
|
|
56
|
+
'.pwt-dd-opt.pwt-dd-sel{background:var(--dsw-alias-interactive-bg-active,var(--dsw-alias-bg-layer-2))}',
|
|
57
|
+
'.pwt-title{font-weight:600}',
|
|
58
|
+
'details.pwt-help{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:6px 10px;background:var(--dsw-alias-bg-layer-3)}',
|
|
59
|
+
'details.pwt-help summary{cursor:pointer;color:var(--dsw-alias-label-secondary);font-size:12px;user-select:none}',
|
|
60
|
+
'details.pwt-help[open] summary{margin-bottom:6px}',
|
|
61
|
+
'details.pwt-help ul{margin:4px 0 4px 18px;padding:0}'
|
|
62
|
+
].join('\n')
|
|
63
|
+
|
|
64
|
+
function rpc(op, payload) {
|
|
65
|
+
var body = Object.assign({ op: op, lang: localStorage.getItem('pwt-lang') || 'zh' }, payload || {})
|
|
66
|
+
return fetch('/api/preset-workbench', {
|
|
67
|
+
method: 'POST',
|
|
68
|
+
headers: { 'content-type': 'application/json' },
|
|
69
|
+
body: JSON.stringify(body),
|
|
70
|
+
}).then(function (res) { return res.json() })
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ── 字符串表(中/英)──「中英切换键」的文案仓库 ──
|
|
74
|
+
// t(key, ...args):按当前语言取 {0}/{1} 占位替换。
|
|
75
|
+
var DICT = {
|
|
76
|
+
zh: {
|
|
77
|
+
'sectionTitle': '预设工作台',
|
|
78
|
+
'lang': 'English',
|
|
79
|
+
'whatIs': '❓ 预设是什么?怎么用本页?点开看 30 秒说明',
|
|
80
|
+
'whatIsP1': '预设 = 新会话的「开局配置包」。你在新会话前选择哪个预设,那个会话里的 AI 就是什么人设、什么能力。',
|
|
81
|
+
'whatIsP2': '一个预设包含三样:① 人设提示词(本页编辑的主体,可分多段)② 能力配置(下方「能力开关」里打勾即可加减工具)③ 名字与简介',
|
|
82
|
+
'whatIsP3a': '新建自己的预设:',
|
|
83
|
+
'whatIsP3b': '点下方「新建预设」→ 起个 id(英文小写,如 my-rp)和显示名 → 它会复制鲸鱼娘的全部内容 → 在编辑区改成你的设定 → 点「保存全部」→ 新开会话,在选择器里选它。完事。',
|
|
84
|
+
'whatIsP4a': '改名/改简介:',
|
|
85
|
+
'whatIsP4b': '选中预设 → 点「打开目录」→ 用记事本改 preset.yml 里的 name 和 description 两行。',
|
|
86
|
+
'whatIsP5a': '勾选框:',
|
|
87
|
+
'whatIsP5b': '取消勾选的段保留文字但不进入系统提示词(不给 AI 看)。',
|
|
88
|
+
'whatIsP6a': '保存后:',
|
|
89
|
+
'whatIsP6b': '只对之后新开的会话生效,当前正在聊的会话不变。',
|
|
90
|
+
'currentPreset': '当前预设:',
|
|
91
|
+
'openDir': '打开目录',
|
|
92
|
+
'openDirTip': '在资源管理器中打开这个预设的文件夹',
|
|
93
|
+
'validate': '校验挂载',
|
|
94
|
+
'validateTip': '模拟一次会话加载,检查预设能否正常使用',
|
|
95
|
+
'bindTitle': '工作区默认预设',
|
|
96
|
+
'bindDesc': '给每个工作项目(工作区目录)绑定一个预设。之后在这个工作区新开的会话会自动用它,不用每次手动选;没绑定的目录沿用全局默认。对已经在聊的会话无影响。',
|
|
97
|
+
'bindEmpty': '还没有工作区。先在左侧栏添加工作项目,再回来绑定。',
|
|
98
|
+
'bindNone': '(不绑定,用全局默认)',
|
|
99
|
+
'bindOrphan': '此目录已不在工作区列表,但绑定仍在——新会话若从这里启动仍会生效。',
|
|
100
|
+
'bindHint': '子目录里的新会话也会继承绑定。手动为新会话选过预设时,以手动选择为准。',
|
|
101
|
+
'bindOk': '已绑定:新会话将使用「{0}」。',
|
|
102
|
+
'bindCleared': '已解除绑定,恢复全局默认。',
|
|
103
|
+
'bindFail': '绑定失败: ',
|
|
104
|
+
'newPreset': '新建预设',
|
|
105
|
+
'collapse': '收起新建',
|
|
106
|
+
'deletePreset': '删除预设',
|
|
107
|
+
'deleteTip': '删除整个预设文件夹(两击确认防手滑)',
|
|
108
|
+
'confirmDel': '⚠ 确认删除 {0} ?再点一次',
|
|
109
|
+
'selectUnit': '(选择)',
|
|
110
|
+
'brokenTag': '(损坏)',
|
|
111
|
+
'template': '模板:',
|
|
112
|
+
'idLabel': 'id:',
|
|
113
|
+
'nameLabel': '显示名:',
|
|
114
|
+
'idPlaceholder': '英文小写,如 my-rp',
|
|
115
|
+
'namePlaceholder': '如 我的角色扮演',
|
|
116
|
+
'createFrom': '从模板创建',
|
|
117
|
+
'noTemplateSel': '请先选择模板',
|
|
118
|
+
'creating': '创建中…',
|
|
119
|
+
'created': '已创建: {0}。现在编辑它的提示词段落,改完记得点「保存全部」,然后在新会话的选择器里选它。',
|
|
120
|
+
'createFail': '创建失败: ',
|
|
121
|
+
'deleteFail': '删除失败: ',
|
|
122
|
+
'deleted': '已删除预设: {0}',
|
|
123
|
+
'addSection': '+添加段落',
|
|
124
|
+
'saveAll': '💾 保存全部',
|
|
125
|
+
'saveHintNone': '(先在上面选择一个预设)',
|
|
126
|
+
'saveHintLoading': '(读取中…)',
|
|
127
|
+
'saving': '保存中…',
|
|
128
|
+
'saved': '已保存 {0} 段。新开的会话生效;当前会话不变。',
|
|
129
|
+
'saveFail': '保存失败: ',
|
|
130
|
+
'validating': '校验中…',
|
|
131
|
+
'validOk': '挂载校验通过 ✔ 下次新开会话可以正常加载这个预设。',
|
|
132
|
+
'validFail': '挂载校验失败:{0}',
|
|
133
|
+
'validReqFail': '校验请求失败: ',
|
|
134
|
+
'openDirFail': '打开目录失败: ',
|
|
135
|
+
'openDirOk': '已打开目录: {0}(可在此用记事本改 preset.yml 的名字和简介)',
|
|
136
|
+
'loadFail': '读取失败: ',
|
|
137
|
+
'capsTitle': '🧰 能力开关:给这个预设加/减工具(打勾立即生效,自动校验)',
|
|
138
|
+
'capsDesc': '下面是可选能力。勾上 → 把对应工具装进这个预设;取消 → 移除。每次切换会自动校验并提示结果。只对之后新开的会话生效。',
|
|
139
|
+
'capsRo': '官方预设只读——不可修改。想改成自己的,复制一份再编辑。',
|
|
140
|
+
'capsHint': '选中预设后这里会列出可开关的能力',
|
|
141
|
+
'capOn': '开启',
|
|
142
|
+
'capOff': '关闭',
|
|
143
|
+
'capToggle': '能力「{0}」{1}。新会话生效;挂载校验{2}',
|
|
144
|
+
'capOk': '通过 ✔',
|
|
145
|
+
'capFail': '失败:{0}',
|
|
146
|
+
'capSwitchFail': '能力切换失败: ',
|
|
147
|
+
'badgeWired': '🔌 已接线(未启用)',
|
|
148
|
+
'badgeOn': '✓ 已启用',
|
|
149
|
+
'grpUser': '📁 用户模板',
|
|
150
|
+
'grpSystem': '🏁 官方预设',
|
|
151
|
+
'unitNone': '(未选择)',
|
|
152
|
+
'nameWrap': '({0})',
|
|
153
|
+
'curUserGrp': '用户预设',
|
|
154
|
+
'curSystemGrp': '官方预设(只读)',
|
|
155
|
+
'systemBadge': '官方预设',
|
|
156
|
+
'systemReadonly': '这是官方预设,只读——不可修改。想改成自己的,复制一份再编辑。',
|
|
157
|
+
'systemComp': '它带的能力组合:',
|
|
158
|
+
'compEmpty': '(没有可展示的组合行)',
|
|
159
|
+
'compOn': '启用',
|
|
160
|
+
'compOff': '关闭',
|
|
161
|
+
'compCore': '预设地基(不可关闭)',
|
|
162
|
+
'enabled': '启用',
|
|
163
|
+
'enabledTip': '勾选 = 这段生效;取消 = 保留文字但不给 AI 看',
|
|
164
|
+
'secTitlePh': '段落标题',
|
|
165
|
+
'delSec': '删除段',
|
|
166
|
+
'secTextPh': '这一段提示词内容…',
|
|
167
|
+
'loading': '读取中…',
|
|
168
|
+
'pickFirst': '先在上面选择一个预设',
|
|
169
|
+
'noSections': '该预设还没有分段提示词',
|
|
170
|
+
'metaName': '名称:{0} ',
|
|
171
|
+
'kindBuiltin': '📦 内置模板',
|
|
172
|
+
'kindBuiltinTip': '这个 id 与插件随包分发的模板同名,且内容与模板一致——该内容已随插件公开。',
|
|
173
|
+
'kindBuiltinModified': '📦 内置模板 · ✏️ 已改动',
|
|
174
|
+
'kindBuiltinModifiedTip': 'id 与随包模板同名,但内容已被改动——里面可能有你自己加的内容,不要假定它和模板一样可以公开。',
|
|
175
|
+
'kindCustom': '👤 自定义',
|
|
176
|
+
'kindCustomTip': '不在插件内置模板之列,是你自己创作的预设。',
|
|
177
|
+
'kindOfficial': '官方预设',
|
|
178
|
+
'kindOfficialTip': '由 DSH 官方或第三方插件声明,磁盘上没有目录,本工作台只能查看。',
|
|
179
|
+
},
|
|
180
|
+
en: {
|
|
181
|
+
'sectionTitle': 'Preset Workbench',
|
|
182
|
+
'lang': '中文',
|
|
183
|
+
'whatIs': '❓ What is a preset & how to use this page? 30-sec intro',
|
|
184
|
+
'whatIsP1': 'A preset is a "starter config pack" for a new session. Whichever preset you pick before a session sets that session\u2019s persona and capabilities.',
|
|
185
|
+
'whatIsP2': 'A preset has three parts: ① persona prompt (edited here, can be multi-section) ② capability config (toggle tools below in "Capabilities") ③ name & description',
|
|
186
|
+
'whatIsP3a': 'Create your own: ',
|
|
187
|
+
'whatIsP3b': 'click "New preset" → give an id (lowercase, e.g. my-rp) and display name → it copies whale-mom\u2019s full content → edit in the area → "Save all" → open a new session and pick it. Done.',
|
|
188
|
+
'whatIsP4a': 'Rename / edit description: ',
|
|
189
|
+
'whatIsP4b': 'select the preset → "Open folder" → edit name and description in preset.yml with a text editor.',
|
|
190
|
+
'whatIsP5a': 'Checkboxes: ',
|
|
191
|
+
'whatIsP5b': 'unchecked sections keep their text but are excluded from the system prompt (hidden from the AI).',
|
|
192
|
+
'whatIsP6a': 'After saving: ',
|
|
193
|
+
'whatIsP6b': 'only affects sessions opened afterwards; the current conversation stays unchanged.',
|
|
194
|
+
'currentPreset': 'Current preset:',
|
|
195
|
+
'openDir': 'Open folder',
|
|
196
|
+
'openDirTip': 'Open this preset\u2019s folder in the file explorer',
|
|
197
|
+
'validate': 'Validate load',
|
|
198
|
+
'validateTip': 'Simulate a session load to check the preset works',
|
|
199
|
+
'bindTitle': 'Workspace default presets',
|
|
200
|
+
'bindDesc': 'Bind a preset to each work project (workspace directory). New sessions started in that workspace automatically use it — no manual picking; unbound directories keep the global default. Running conversations are unaffected.',
|
|
201
|
+
'bindEmpty': 'No workspaces yet. Add a work project in the sidebar first, then come back to bind.',
|
|
202
|
+
'bindNone': '(none — use global default)',
|
|
203
|
+
'bindOrphan': 'This directory left the workspace list, but the binding remains — new sessions started here still apply it.',
|
|
204
|
+
'bindHint': 'Sessions started in subdirectories inherit the binding too. An explicit preset picked for a new session always wins.',
|
|
205
|
+
'bindOk': 'Bound: new sessions will use “{0}”.',
|
|
206
|
+
'bindCleared': 'Binding removed; the global default applies again.',
|
|
207
|
+
'bindFail': 'Binding failed: ',
|
|
208
|
+
'newPreset': 'New preset',
|
|
209
|
+
'collapse': 'Collapse',
|
|
210
|
+
'deletePreset': 'Delete preset',
|
|
211
|
+
'deleteTip': 'Delete the whole preset folder (two clicks to confirm)',
|
|
212
|
+
'confirmDel': '⚠ Confirm delete {0}? Click again',
|
|
213
|
+
'selectUnit': '(select)',
|
|
214
|
+
'brokenTag': '(broken)',
|
|
215
|
+
'template': 'Template:',
|
|
216
|
+
'idLabel': 'id:',
|
|
217
|
+
'nameLabel': 'Name:',
|
|
218
|
+
'idPlaceholder': 'lowercase, e.g. my-rp',
|
|
219
|
+
'namePlaceholder': 'e.g. My Roleplay',
|
|
220
|
+
'createFrom': 'Create from template',
|
|
221
|
+
'noTemplateSel': 'Please select a template first',
|
|
222
|
+
'creating': 'Creating…',
|
|
223
|
+
'created': 'Created {0}. Now edit its prompt sections, then click "Save all", and pick it in a new session\u2019s selector.',
|
|
224
|
+
'createFail': 'Create failed: ',
|
|
225
|
+
'deleteFail': 'Delete failed: ',
|
|
226
|
+
'deleted': 'Deleted preset: {0}',
|
|
227
|
+
'addSection': '+ Add section',
|
|
228
|
+
'saveAll': '💾 Save all',
|
|
229
|
+
'saveHintNone': '(pick a preset above first)',
|
|
230
|
+
'saveHintLoading': '(loading…)',
|
|
231
|
+
'saving': 'Saving…',
|
|
232
|
+
'saved': 'Saved {0} section(s). Affects only new sessions; the current conversation is unchanged.',
|
|
233
|
+
'saveFail': 'Save failed: ',
|
|
234
|
+
'validating': 'Validating…',
|
|
235
|
+
'validOk': 'Mount check passed ✔ New sessions can load this preset normally.',
|
|
236
|
+
'validFail': 'Mount check failed: {0}',
|
|
237
|
+
'validReqFail': 'Validate request failed: ',
|
|
238
|
+
'openDirFail': 'Open folder failed: ',
|
|
239
|
+
'openDirOk': 'Opened folder: {0} (edit name & description in preset.yml here)',
|
|
240
|
+
'loadFail': 'Load failed: ',
|
|
241
|
+
'capsTitle': '🧰 Capabilities: add/remove tools for this preset (toggle applies immediately, auto-validated)',
|
|
242
|
+
'capsDesc': 'The optional capabilities below. Check → install the tool into this preset; uncheck → remove it. Each toggle auto-validates. Affects only new sessions.',
|
|
243
|
+
'capsRo': 'Official presets are read-only — not editable. Duplicate one to make your own.',
|
|
244
|
+
'capsHint': 'Capabilities will appear here after picking a preset',
|
|
245
|
+
'capOn': 'on',
|
|
246
|
+
'capOff': 'off',
|
|
247
|
+
'capToggle': 'Capability "{0}" {1}. New sessions; mount check {2}',
|
|
248
|
+
'capOk': 'passed ✔',
|
|
249
|
+
'capFail': 'failed: {0}',
|
|
250
|
+
'capSwitchFail': 'Capability toggle failed: ',
|
|
251
|
+
'badgeWired': '🔌 wired (inactive)',
|
|
252
|
+
'badgeOn': '✓ active',
|
|
253
|
+
'grpUser': '📁 User templates',
|
|
254
|
+
'grpSystem': '🏁 Official presets',
|
|
255
|
+
'unitNone': '(not selected)',
|
|
256
|
+
'nameWrap': '({0})',
|
|
257
|
+
'curUserGrp': 'User presets',
|
|
258
|
+
'curSystemGrp': 'Official presets (read-only)',
|
|
259
|
+
'systemBadge': 'Official preset',
|
|
260
|
+
'systemReadonly': 'This is an official preset — read-only. Not editable. Duplicate it to make your own.',
|
|
261
|
+
'systemComp': 'Its capability composition:',
|
|
262
|
+
'compEmpty': '(no rows to show)',
|
|
263
|
+
'compOn': 'on',
|
|
264
|
+
'compOff': 'off',
|
|
265
|
+
'compCore': 'preset core (cannot disable)',
|
|
266
|
+
'enabled': 'Enabled',
|
|
267
|
+
'enabledTip': 'checked = this section is active; unchecked = kept but hidden from the AI',
|
|
268
|
+
'secTitlePh': 'Section title',
|
|
269
|
+
'delSec': 'Delete section',
|
|
270
|
+
'secTextPh': 'Prompt content for this section…',
|
|
271
|
+
'loading': 'Loading…',
|
|
272
|
+
'pickFirst': 'Start by picking a preset above',
|
|
273
|
+
'noSections': 'This preset has no prompt sections yet',
|
|
274
|
+
'metaName': 'Name: {0} ',
|
|
275
|
+
'kindBuiltin': '📦 Built-in template',
|
|
276
|
+
'kindBuiltinTip': 'This id matches a template shipped with the plugin, and the content matches it byte for byte — that content is already public with the plugin.',
|
|
277
|
+
'kindBuiltinModified': '📦 Built-in template · ✏️ modified',
|
|
278
|
+
'kindBuiltinModifiedTip': 'The id matches a shipped template, but the content has been changed — it may contain your own additions, so do not assume it is as public as the template.',
|
|
279
|
+
'kindCustom': '👤 Custom',
|
|
280
|
+
'kindCustomTip': 'Not one of the plugin’s built-in templates; this is a preset you authored.',
|
|
281
|
+
'kindOfficial': 'Official preset',
|
|
282
|
+
'kindOfficialTip': 'Declared by DSH or a third-party plugin, with no directory on disk — this workbench can only view it.',
|
|
283
|
+
},
|
|
284
|
+
}
|
|
285
|
+
function t(orig) {
|
|
286
|
+
var lang = localStorage.getItem('pwt-lang') || 'zh'
|
|
287
|
+
var key = orig
|
|
288
|
+
var args = Array.prototype.slice.call(arguments, 1)
|
|
289
|
+
if (lang === 'zh' && DICT.zh[key]) return substitute(DICT.zh[key], args)
|
|
290
|
+
if (lang === 'en' && DICT.en[key]) return substitute(DICT.en[key], args)
|
|
291
|
+
// 未收录:回退原文
|
|
292
|
+
return orig
|
|
293
|
+
}
|
|
294
|
+
function substitute(tpl, args) {
|
|
295
|
+
var s = String(tpl)
|
|
296
|
+
for (var i = 0; i < args.length; i++) s = s.replace(new RegExp('\\{' + i + '\\}', 'g'), argStr(args[i]))
|
|
297
|
+
return s
|
|
298
|
+
}
|
|
299
|
+
function argStr(a) { return (a === undefined || a === null) ? '' : String(a) }
|
|
300
|
+
function langNow() { return localStorage.getItem('pwt-lang') || 'zh' }
|
|
301
|
+
function setLang(l) { localStorage.setItem('pwt-lang', l) }
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
// 渲染错误边界:一个子视图崩溃时显示可读的错误,而不是把整个设置页拖成空白。
|
|
305
|
+
// 参考 dsh-doctor 的 DoctorErrorBoundary 写法。
|
|
306
|
+
var PwtErrorBoundary = (function () {
|
|
307
|
+
var B = function (props) {
|
|
308
|
+
react.Component.call(this, props)
|
|
309
|
+
this.state = { failed: false, message: '' }
|
|
310
|
+
}
|
|
311
|
+
B.prototype = Object.create(react.Component.prototype)
|
|
312
|
+
B.prototype.constructor = B
|
|
313
|
+
B.getDerivedStateFromError = function () { return { failed: true } }
|
|
314
|
+
B.prototype.componentDidCatch = function (err) {
|
|
315
|
+
try { this.setState({ message: err && err.message ? err.message : String(err) }) } catch (_) {}
|
|
316
|
+
}
|
|
317
|
+
B.prototype.render = function () {
|
|
318
|
+
if (!this.state.failed) return this.props.children
|
|
319
|
+
return h('div', { className: 'pwt-status pwt-statusError', style: { padding: '8px', border: '1px solid var(--dsw-alias-state-error-primary)', borderRadius: '8px' } },
|
|
320
|
+
'◆ 预设工作台渲染出错(已捕获,不影响其他设置页): ' + (this.state.message || 'unknown'))
|
|
321
|
+
}
|
|
322
|
+
return B
|
|
323
|
+
})()
|
|
324
|
+
|
|
325
|
+
function TemplatePicker(props) {
|
|
326
|
+
var val = props.value || ''
|
|
327
|
+
var items = props.items || []
|
|
328
|
+
var onChange = props.onChange
|
|
329
|
+
var opn = react.useState(false); var open = opn[0]; var setOpen = opn[1]
|
|
330
|
+
var refBox = react.useRef(null)
|
|
331
|
+
var label = t('unitNone')
|
|
332
|
+
for (var i = 0; i < items.length; i++) {
|
|
333
|
+
if (items[i].source + ':' + items[i].id === val) {
|
|
334
|
+
label = items[i].id + (items[i].name ? t('nameWrap', items[i].name) : '')
|
|
335
|
+
break
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
react.useEffect(function () {
|
|
339
|
+
if (!open) return undefined
|
|
340
|
+
function onDown(e) {
|
|
341
|
+
var el = refBox.current
|
|
342
|
+
if (el && (el === e.target || el.contains(e.target))) return
|
|
343
|
+
setOpen(false)
|
|
344
|
+
}
|
|
345
|
+
function onKey(e) { if (e.key === 'Escape') setOpen(false) }
|
|
346
|
+
document.addEventListener('mousedown', onDown)
|
|
347
|
+
document.addEventListener('keydown', onKey)
|
|
348
|
+
return function () {
|
|
349
|
+
document.removeEventListener('mousedown', onDown)
|
|
350
|
+
document.removeEventListener('keydown', onKey)
|
|
351
|
+
}
|
|
352
|
+
}, [open])
|
|
353
|
+
var groups = [
|
|
354
|
+
{ label: t('grpUser'), source: 'user' },
|
|
355
|
+
{ label: t('grpSystem'), source: 'system' }
|
|
356
|
+
]
|
|
357
|
+
return h('div', { ref: refBox, className: 'pwt-dd' },
|
|
358
|
+
h('button', { type: 'button', className: 'pwt-btn pwt-tpl pwt-dd-btn', onClick: function () { setOpen(!open) } },
|
|
359
|
+
h('span', { className: 'pwt-dd-label' }, label),
|
|
360
|
+
h('span', { className: 'pwt-dd-caret' }, open ? '▲' : '▼')
|
|
361
|
+
),
|
|
362
|
+
open ? h('div', { className: 'pwt-dd-menu' },
|
|
363
|
+
groups.map(function (g) {
|
|
364
|
+
var arr = items.filter(function (t) { return t.source === g.source })
|
|
365
|
+
if (!arr.length) return null
|
|
366
|
+
return h('div', { key: g.source, className: 'pwt-dd-group' },
|
|
367
|
+
h('div', { className: 'pwt-dd-glabel' }, g.label),
|
|
368
|
+
arr.map(function (it) {
|
|
369
|
+
var v = it.source + ':' + it.id
|
|
370
|
+
return h('button', { key: v, type: 'button', className: 'pwt-dd-opt' + (v === val ? ' pwt-dd-sel' : ''), onClick: function () { onChange(v); setOpen(false) } },
|
|
371
|
+
it.id + (it.name ? t('nameWrap', it.name) : ''))
|
|
372
|
+
})
|
|
373
|
+
)
|
|
374
|
+
})
|
|
375
|
+
) : null
|
|
376
|
+
)
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function WorkbenchPage() {
|
|
380
|
+
var presetsL = react.useState(null); var list = presetsL[0]; var setList = presetsL[1]
|
|
381
|
+
var cur = react.useState(''); var current = cur[0]; var setCurrent = cur[1]
|
|
382
|
+
var dta = react.useState(null); var data = dta[0]; var setData = dta[1]
|
|
383
|
+
var stt = react.useState(''); var status = stt[0]; var setStatus = stt[1]
|
|
384
|
+
var bsy = react.useState(false); var busy = bsy[0]; var setBusy = bsy[1]
|
|
385
|
+
var shw = react.useState(false); var showNew = shw[0]; var setShowNew = shw[1]
|
|
386
|
+
var nid = react.useState(''); var newId = nid[0]; var setNewId = nid[1]
|
|
387
|
+
var nnm = react.useState(''); var newName = nnm[0]; var setNewName = nnm[1]
|
|
388
|
+
var tpl = react.useState(null); var templates = tpl[0]; var setTemplates = tpl[1]
|
|
389
|
+
var tplSel = react.useState(''); var template = tplSel[0]; var setTemplate = tplSel[1]
|
|
390
|
+
var cfd = react.useState(false); var confirmDel = cfd[0]; var setConfirmDel = cfd[1]
|
|
391
|
+
var lng = react.useState(langNow()); var lang = lng[0]; var setLangState = lng[1]
|
|
392
|
+
var curRo = react.useState(false); var currentReadonly = curRo[0]; var setCurrentReadonly = curRo[1]
|
|
393
|
+
var bnd = react.useState(null); var bindings = bnd[0]; var setBindings = bnd[1]
|
|
394
|
+
|
|
395
|
+
var loadBindings = function () {
|
|
396
|
+
return rpc('bindings').then(function (r) {
|
|
397
|
+
if (r && r.ok) setBindings(r.rows || [])
|
|
398
|
+
}).catch(function () { setBindings([]) })
|
|
399
|
+
}
|
|
400
|
+
react.useEffect(function () { loadBindings() }, [])
|
|
401
|
+
|
|
402
|
+
var setWorkspaceBinding = function (path, presetId) {
|
|
403
|
+
setBusy(true)
|
|
404
|
+
var call = presetId ? rpc('setBinding', { path: path, preset: presetId }) : rpc('clearBinding', { path: path })
|
|
405
|
+
return call.then(function (r) {
|
|
406
|
+
if (!r || !r.ok) throw new Error(r && r.error || 'binding failed')
|
|
407
|
+
setStatus(presetId ? t('bindOk', presetId) : t('bindCleared'))
|
|
408
|
+
return loadBindings()
|
|
409
|
+
}).catch(function (e) { setStatus(t('bindFail') + e.message) }).finally(function () { setBusy(false) })
|
|
410
|
+
}
|
|
411
|
+
var toggleLang = function () {
|
|
412
|
+
var next = (langNow() === 'en') ? 'zh' : 'en'
|
|
413
|
+
setLang(next)
|
|
414
|
+
setLangState(next)
|
|
415
|
+
// 语言变了 → 重新拉取能力开关(title/desc 由宿主按 lang 给),保持整页文案一致
|
|
416
|
+
if (current) capLoad(current)
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
var refresh = function () {
|
|
420
|
+
var tarr = []
|
|
421
|
+
return rpc('list').then(function (r) {
|
|
422
|
+
if (!r || !r.ok) throw new Error(r && r.error || 'load failed')
|
|
423
|
+
var arr = r.presets || []
|
|
424
|
+
tarr = arr
|
|
425
|
+
setList(arr)
|
|
426
|
+
setCurrent(function (prev) {
|
|
427
|
+
return prev || (arr[0] ? arr[0].id : '')
|
|
428
|
+
})
|
|
429
|
+
return rpc('templates')
|
|
430
|
+
}).then(function (r) {
|
|
431
|
+
if (!r || !r.ok) throw new Error(r && r.error || 'templates failed')
|
|
432
|
+
setTemplates(r.templates || [])
|
|
433
|
+
setTemplate(function (prev) {
|
|
434
|
+
if (prev) return prev
|
|
435
|
+
var ts = r.templates || []
|
|
436
|
+
return ts.length ? (ts[0].source + ':' + ts[0].id) : ''
|
|
437
|
+
})
|
|
438
|
+
}).catch(function (e) {
|
|
439
|
+
// 兼容兜底:模板接口不可用(如宿主未重启/旧版本)时,
|
|
440
|
+
// 用用户预设 + 官方四个当模板即可(内置模板已并入用户组,不单独出现)。
|
|
441
|
+
var ts = []
|
|
442
|
+
for (var i = 0; i < tarr.length; i++) ts.push({ id: tarr[i].id, name: tarr[i].name, source: 'user' })
|
|
443
|
+
var official = ['standard', 'code', 'minimal', 'cordis']
|
|
444
|
+
for (var o = 0; o < official.length; o++) ts.push({ id: official[o], name: undefined, source: 'system' })
|
|
445
|
+
setTemplates(ts)
|
|
446
|
+
setTemplate(function (prev) { return prev || (ts[0] ? ts[0].source + ':' + ts[0].id : '') })
|
|
447
|
+
})
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
var roOf = function (id, arr) {
|
|
451
|
+
if (!arr) return false
|
|
452
|
+
for (var i = 0; i < arr.length; i++) if (arr[i].id === id) return !!arr[i].readonly
|
|
453
|
+
return false
|
|
454
|
+
}
|
|
455
|
+
react.useEffect(function () { setCurrentReadonly(roOf(current, list)) }, [current, list])
|
|
456
|
+
|
|
457
|
+
var load = function (id) {
|
|
458
|
+
if (!id) { setData(null); return }
|
|
459
|
+
setBusy(true)
|
|
460
|
+
rpc('load', { id: id }).then(function (r) {
|
|
461
|
+
if (!r || !r.ok) throw new Error(r && r.error || 'load failed')
|
|
462
|
+
setData(r)
|
|
463
|
+
setStatus('')
|
|
464
|
+
}).catch(function (e) {
|
|
465
|
+
setData(null)
|
|
466
|
+
setStatus(t('loadFail') + e.message)
|
|
467
|
+
}).finally(function () { setBusy(false) })
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
react.useEffect(function () { refresh() }, [])
|
|
471
|
+
react.useEffect(function () { load(current) }, [current])
|
|
472
|
+
|
|
473
|
+
var save = function () {
|
|
474
|
+
if (!data || !current) return
|
|
475
|
+
setBusy(true)
|
|
476
|
+
setStatus(t('saving'))
|
|
477
|
+
rpc('save', { id: current, sections: data.sections }).then(function (r) {
|
|
478
|
+
if (!r || !r.ok) throw new Error(r && r.error || 'save failed')
|
|
479
|
+
setStatus(t('saved', (r.wrote || 0)))
|
|
480
|
+
}).catch(function (e) { setStatus(t('saveFail') + e.message) }).finally(function () { setBusy(false) })
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
var doValidate = function () {
|
|
484
|
+
if (!current) return
|
|
485
|
+
setStatus(t('validating'))
|
|
486
|
+
rpc('validate', { id: current }).then(function (r) {
|
|
487
|
+
setStatus(r && r.ok ? t('validOk') : t('validFail', (r && r.error)))
|
|
488
|
+
}).catch(function (e) { setStatus(t('validReqFail') + e.message) })
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
var doReveal = function () {
|
|
492
|
+
if (!current) return
|
|
493
|
+
rpc('reveal', { id: current }).then(function (r) {
|
|
494
|
+
if (!r || !r.ok) throw new Error(r && r.error || 'reveal failed')
|
|
495
|
+
setStatus(t('openDirOk', r.dir))
|
|
496
|
+
}).catch(function (e) { setStatus(t('openDirFail') + e.message) })
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
var doCreate = function () {
|
|
500
|
+
if (!template) { setStatus(t('noTemplateSel')); return }
|
|
501
|
+
setBusy(true)
|
|
502
|
+
setStatus(t('creating'))
|
|
503
|
+
rpc('create', { id: newId, name: newName, from: template }).then(function (r) {
|
|
504
|
+
if (!r || !r.ok) throw new Error(r && r.error || 'create failed')
|
|
505
|
+
setShowNew(false)
|
|
506
|
+
setNewId('')
|
|
507
|
+
setNewName('')
|
|
508
|
+
setStatus(t('created', r.id))
|
|
509
|
+
return refresh().then(function () { setCurrent(r.id) })
|
|
510
|
+
}).catch(function (e) { setStatus(t('createFail') + e.message) }).finally(function () { setBusy(false) })
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
var doDelete = function () {
|
|
514
|
+
if (!current) return
|
|
515
|
+
setBusy(true)
|
|
516
|
+
rpc('delete', { id: current }).then(function (r) {
|
|
517
|
+
if (!r || !r.ok) throw new Error(r && r.error || 'delete failed')
|
|
518
|
+
setStatus(t('deleted', current))
|
|
519
|
+
setConfirmDel(false)
|
|
520
|
+
setData(null)
|
|
521
|
+
setCurrent('')
|
|
522
|
+
return refresh()
|
|
523
|
+
}).catch(function (e) {
|
|
524
|
+
setStatus(t('deleteFail') + e.message)
|
|
525
|
+
setConfirmDel(false)
|
|
526
|
+
}).finally(function () { setBusy(false) })
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
var patchSection = function (i, patch) {
|
|
530
|
+
setData(function (d) {
|
|
531
|
+
if (!d) return d
|
|
532
|
+
var arr = d.sections.slice()
|
|
533
|
+
arr[i] = Object.assign({}, arr[i], patch)
|
|
534
|
+
return Object.assign({}, d, { sections: arr })
|
|
535
|
+
})
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
var addSection = function () {
|
|
539
|
+
setData(function (d) {
|
|
540
|
+
if (!d) return d
|
|
541
|
+
var arr = d.sections.slice()
|
|
542
|
+
arr.push({ file: 'section.' + Date.now() + '.md', title: '新段落', enabled: true, text: '' })
|
|
543
|
+
return Object.assign({}, d, { sections: arr })
|
|
544
|
+
})
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
var removeSection = function (i) {
|
|
548
|
+
setData(function (d) {
|
|
549
|
+
if (!d) return d
|
|
550
|
+
var arr = d.sections.slice()
|
|
551
|
+
arr.splice(i, 1)
|
|
552
|
+
return Object.assign({}, d, { sections: arr })
|
|
553
|
+
})
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// ── 能力开关 ──
|
|
557
|
+
var capS = react.useState(null); var caps = capS[0]; var setCaps = capS[1]
|
|
558
|
+
|
|
559
|
+
var capLoad = function (id) {
|
|
560
|
+
if (!id) { setCaps(null); return }
|
|
561
|
+
rpc('caps', { id: id }).then(function (r) {
|
|
562
|
+
if (r && r.ok) setCaps(r.caps || [])
|
|
563
|
+
}).catch(function () { setCaps(null) })
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
react.useEffect(function () { capLoad(current) }, [current])
|
|
567
|
+
|
|
568
|
+
var toggleCap = function (c) {
|
|
569
|
+
if (!current) return
|
|
570
|
+
setBusy(true)
|
|
571
|
+
rpc('setCap', { id: current, capId: c.id, enabled: !c.enabled }).then(function (r) {
|
|
572
|
+
if (!r || !r.ok) throw new Error(r && r.error || 'setCap failed')
|
|
573
|
+
var verb = t(!c.enabled ? 'capOn' : 'capOff')
|
|
574
|
+
return rpc('caps', { id: current }).then(function (r2) {
|
|
575
|
+
if (r2 && r2.ok) setCaps(r2.caps)
|
|
576
|
+
return rpc('validate', { id: current }).then(function (v) {
|
|
577
|
+
setStatus(t('capToggle', c.title, verb, (v && v.ok ? t('capOk') : t('capFail', (v && v.error)))))
|
|
578
|
+
})
|
|
579
|
+
})
|
|
580
|
+
}).catch(function (e) { setStatus(t('capSwitchFail') + e.message) }).finally(function () { setBusy(false) })
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
var children = []
|
|
584
|
+
|
|
585
|
+
children.push(h('style', { key: 'pwt-style' }, cssText))
|
|
586
|
+
|
|
587
|
+
// ── 内置说明 ──
|
|
588
|
+
children.push(h('details', { key: 'help', className: 'pwt-help' },
|
|
589
|
+
h('summary', null, t('whatIs')),
|
|
590
|
+
h('div', null,
|
|
591
|
+
h('p', { style: { margin: '4px 0' } }, t('whatIsP1')),
|
|
592
|
+
h('ul', null,
|
|
593
|
+
h('li', null, t('whatIsP2'))),
|
|
594
|
+
h('p', { style: { margin: '4px 0' } },
|
|
595
|
+
h('b', null, t('whatIsP3a')), t('whatIsP3b')),
|
|
596
|
+
h('p', { style: { margin: '4px 0' } },
|
|
597
|
+
h('b', null, t('whatIsP4a')), t('whatIsP4b')),
|
|
598
|
+
h('p', { style: { margin: '4px 0' } },
|
|
599
|
+
h('b', null, t('whatIsP5a')), t('whatIsP5b')),
|
|
600
|
+
h('p', { style: { margin: '4px 0' } },
|
|
601
|
+
h('b', null, t('whatIsP6a')), t('whatIsP6b')))
|
|
602
|
+
))
|
|
603
|
+
|
|
604
|
+
// ── 选择与操作(钉在顶部,不随内容滚动)──
|
|
605
|
+
// 按只读性分组:用户预设可编辑,官方预设(system)只读。
|
|
606
|
+
var userOpts = []
|
|
607
|
+
var systemOpts = []
|
|
608
|
+
;(list || []).forEach(function (p) {
|
|
609
|
+
// 可编辑预设加来源标记:📦 = 与随包模板同名,👤 = 你自己创作的。
|
|
610
|
+
var mark = p.readonly ? '' : (p.kind === 'builtin' ? '📦 ' : '👤 ')
|
|
611
|
+
var label = mark + (p.name ? p.name + ' · ' : '') + p.id + (p.broken ? t('brokenTag') : '')
|
|
612
|
+
var opt = h('option', { key: p.id, value: p.id }, label)
|
|
613
|
+
if (p.readonly) systemOpts.push(opt)
|
|
614
|
+
else userOpts.push(opt)
|
|
615
|
+
})
|
|
616
|
+
var opts = [
|
|
617
|
+
userOpts.length ? h('optgroup', { key: 'user', label: t('curUserGrp') }, userOpts) : null,
|
|
618
|
+
systemOpts.length ? h('optgroup', { key: 'system', label: t('curSystemGrp') }, systemOpts) : null,
|
|
619
|
+
].filter(Boolean)
|
|
620
|
+
var currentMeta = (list || []).filter(function (p) { return p.id === current })[0]
|
|
621
|
+
var isRo = currentReadonly
|
|
622
|
+
|
|
623
|
+
children.push(h('div', { key: 'row1', className: 'pwt-row' },
|
|
624
|
+
h('span', null, t('currentPreset')),
|
|
625
|
+
h('select', { className: 'pwt-select', value: current, onChange: function (e) { setCurrent(e.target.value); setConfirmDel(false) }, disabled: busy },
|
|
626
|
+
h('option', { key: '', value: '' }, t('selectUnit')),
|
|
627
|
+
opts
|
|
628
|
+
),
|
|
629
|
+
h('button', { className: 'pwt-btn', onClick: doReveal, disabled: !current || isRo, title: t('openDirTip') }, t('openDir')),
|
|
630
|
+
h('button', { className: 'pwt-btn', onClick: doValidate, disabled: busy || !current, title: t('validateTip') }, t('validate')),
|
|
631
|
+
h('button', { className: 'pwt-btn', onClick: function () { setShowNew(!showNew); setConfirmDel(false) } }, showNew ? t('collapse') : t('newPreset')),
|
|
632
|
+
h('button', {
|
|
633
|
+
className: 'pwt-btn pwt-btnDanger',
|
|
634
|
+
onClick: function () { if (confirmDel) { doDelete() } else { setConfirmDel(true) } },
|
|
635
|
+
disabled: busy || !current || isRo,
|
|
636
|
+
title: t('deleteTip')
|
|
637
|
+
}, confirmDel ? t('confirmDel', current) : t('deletePreset')),
|
|
638
|
+
h('span', { className: 'pwt-grow' }),
|
|
639
|
+
h('button', { className: 'pwt-btn', onClick: toggleLang, title: lang === 'en' ? '切换到中文' : 'Switch to English' }, t('lang'))
|
|
640
|
+
))
|
|
641
|
+
|
|
642
|
+
if (currentMeta) {
|
|
643
|
+
// 来源徽章:📦 内置模板 / 📦·✏️ 已改动 / 👤 自定义 / 官方预设
|
|
644
|
+
var badge = (function (p) {
|
|
645
|
+
if (p.readonly) return { text: t('kindOfficial'), tip: t('kindOfficialTip'), warn: false }
|
|
646
|
+
if (p.kind === 'builtin') {
|
|
647
|
+
return p.modified
|
|
648
|
+
? { text: t('kindBuiltinModified'), tip: t('kindBuiltinModifiedTip'), warn: true }
|
|
649
|
+
: { text: t('kindBuiltin'), tip: t('kindBuiltinTip'), warn: false }
|
|
650
|
+
}
|
|
651
|
+
return { text: t('kindCustom'), tip: t('kindCustomTip'), warn: false }
|
|
652
|
+
})(currentMeta)
|
|
653
|
+
children.push(h('div', { key: 'meta', className: 'pwt-hint' },
|
|
654
|
+
h('span', { className: 'pwt-kind' + (badge.warn ? ' pwt-kindWarn' : ''), title: badge.tip }, badge.text),
|
|
655
|
+
(currentMeta.name ? t('metaName', currentMeta.name) : '') + (currentMeta.description || '')))
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
if (showNew) {
|
|
659
|
+
children.push(h('div', { key: 'rowNew', className: 'pwt-row' },
|
|
660
|
+
h('span', null, t('template')),
|
|
661
|
+
h(TemplatePicker, { value: template, items: templates || [], onChange: setTemplate }),
|
|
662
|
+
h('span', null, t('idLabel')),
|
|
663
|
+
h('input', { className: 'pwt-input', value: newId, placeholder: t('idPlaceholder'), onChange: function (e) { setNewId(e.target.value) } }),
|
|
664
|
+
h('span', null, t('nameLabel')),
|
|
665
|
+
h('input', { className: 'pwt-input', value: newName, placeholder: t('namePlaceholder'), onChange: function (e) { setNewName(e.target.value) } }),
|
|
666
|
+
h('button', { className: 'pwt-btn', onClick: doCreate, disabled: busy || !newId }, t('createFrom'))
|
|
667
|
+
))
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// ── 编辑动作行 ──
|
|
671
|
+
var saveHint = ''
|
|
672
|
+
if (isRo) saveHint = t('systemBadge')
|
|
673
|
+
else if (!current) saveHint = t('saveHintNone')
|
|
674
|
+
else if (!data) saveHint = t('saveHintLoading')
|
|
675
|
+
children.push(h('div', { key: 'rowAct', className: 'pwt-row' },
|
|
676
|
+
h('button', { className: 'pwt-btn', onClick: addSection, disabled: busy || !data || isRo }, t('addSection')),
|
|
677
|
+
h('button', { className: 'pwt-btn', onClick: save, disabled: busy || !data || !current || isRo }, t('saveAll')),
|
|
678
|
+
h('span', { className: 'pwt-hint' }, saveHint)
|
|
679
|
+
))
|
|
680
|
+
|
|
681
|
+
children.push(h('div', { key: 'status', className: (/失败|failed/i.test(status)) ? 'pwt-status pwt-statusError' : 'pwt-status' }, status))
|
|
682
|
+
|
|
683
|
+
// ── 段落卡片(唯一滚动区)──
|
|
684
|
+
var cards = (data && data.sections ? data.sections : []).map(function (s, i) {
|
|
685
|
+
return h('div', { key: s.file + '-' + i, className: 'pwt-card' },
|
|
686
|
+
h('div', { className: 'pwt-row' },
|
|
687
|
+
h('label', { className: 'pwt-row', title: t('enabledTip') },
|
|
688
|
+
h('input', { type: 'checkbox', checked: s.enabled !== false, onChange: function (e) { patchSection(i, { enabled: e.target.checked }) } }),
|
|
689
|
+
h('span', { className: 'pwt-title' }, t('enabled'))
|
|
690
|
+
),
|
|
691
|
+
h('input', {
|
|
692
|
+
className: 'pwt-input pwt-grow', value: s.title || '', placeholder: t('secTitlePh'),
|
|
693
|
+
onChange: function (e) { patchSection(i, { title: e.target.value }) },
|
|
694
|
+
}),
|
|
695
|
+
h('span', { className: 'pwt-file' }, s.file),
|
|
696
|
+
h('button', { className: 'pwt-btn', onClick: function () { removeSection(i) } }, t('delSec'))
|
|
697
|
+
),
|
|
698
|
+
h('textarea', {
|
|
699
|
+
className: 'pwt-ta', rows: Math.min(20, Math.max(6, (s.text || '').split('\n').length + 1)),
|
|
700
|
+
value: s.text || '', placeholder: t('secTextPh'),
|
|
701
|
+
onChange: function (e) { patchSection(i, { text: e.target.value }) },
|
|
702
|
+
})
|
|
703
|
+
)
|
|
704
|
+
})
|
|
705
|
+
|
|
706
|
+
children.push(h('div', { key: 'sections', className: 'pwt-sections' },
|
|
707
|
+
// ── 能力开关(与段落卡片同属一个滚动区:展开后能一路滚到底)──
|
|
708
|
+
// 系统/官方预设只读:能力开关仅供查看,勾选禁用。
|
|
709
|
+
h('details', { key: 'caps', className: 'pwt-help' },
|
|
710
|
+
h('summary', null, t('capsTitle')),
|
|
711
|
+
h('div', null,
|
|
712
|
+
h('p', { style: { margin: '4px 0' } }, t('capsDesc')),
|
|
713
|
+
isRo ? h('div', { className: 'pwt-status' }, t('capsRo')) : null,
|
|
714
|
+
(caps === null
|
|
715
|
+
? h('div', { className: 'pwt-hint' }, t('capsHint'))
|
|
716
|
+
: h('div', { style: { display: 'flex', flexDirection: 'column', gap: 6 } },
|
|
717
|
+
caps.map(function (c) {
|
|
718
|
+
var badge = c.enabled ? (c.dormant ? t('badgeWired') : t('badgeOn')) : ''
|
|
719
|
+
var lock = c.locked
|
|
720
|
+
return h('label', { key: c.id, className: 'pwt-row', title: c.desc, style: { cursor: (isRo || lock) ? 'default' : 'pointer' } },
|
|
721
|
+
h('input', {
|
|
722
|
+
type: 'checkbox',
|
|
723
|
+
checked: lock ? true : !!c.enabled,
|
|
724
|
+
disabled: busy || !current || isRo || lock,
|
|
725
|
+
onChange: function () { if (lock) return; toggleCap(c) },
|
|
726
|
+
}),
|
|
727
|
+
h('span', { className: 'pwt-title' }, lock ? ('🔒 ' + c.title) : c.title),
|
|
728
|
+
badge ? h('span', { className: 'pwt-hint' }, badge) : null,
|
|
729
|
+
h('span', { className: 'pwt-hint' }, (lock ? t('compCore') + ' · ' : '') + c.desc)
|
|
730
|
+
)
|
|
731
|
+
})
|
|
732
|
+
))
|
|
733
|
+
)
|
|
734
|
+
),
|
|
735
|
+
// ── 每工作区默认预设:给每个工作区目录绑一个预设,新会话自动用 ──
|
|
736
|
+
h('details', { key: 'bindings', className: 'pwt-help' },
|
|
737
|
+
h('summary', null, t('bindTitle')),
|
|
738
|
+
h('div', null,
|
|
739
|
+
h('p', { style: { margin: '4px 0' } }, t('bindDesc')),
|
|
740
|
+
(bindings === null
|
|
741
|
+
? h('div', { className: 'pwt-hint' }, t('loading'))
|
|
742
|
+
: (bindings.length === 0
|
|
743
|
+
? h('div', { className: 'pwt-hint' }, t('bindEmpty'))
|
|
744
|
+
: h('div', { style: { display: 'flex', flexDirection: 'column', gap: 6 } },
|
|
745
|
+
bindings.map(function (b) {
|
|
746
|
+
var opts = [h('option', { key: '', value: '' }, t('bindNone'))]
|
|
747
|
+
;(list || []).forEach(function (p) {
|
|
748
|
+
opts.push(h('option', { key: p.id, value: p.id }, (p.name ? p.name + ' · ' : '') + p.id))
|
|
749
|
+
})
|
|
750
|
+
return h('div', { key: b.path, className: 'pwt-row' },
|
|
751
|
+
h('span', { className: 'pwt-file', title: b.path }, (b.orphan ? '⚠ ' : '') + (b.title || b.path)),
|
|
752
|
+
h('select', {
|
|
753
|
+
className: 'pwt-select',
|
|
754
|
+
value: b.preset || '',
|
|
755
|
+
disabled: busy,
|
|
756
|
+
onChange: function (e) { setWorkspaceBinding(b.path, e.target.value) },
|
|
757
|
+
}, opts),
|
|
758
|
+
b.orphan ? h('span', { className: 'pwt-hint' }, t('bindOrphan')) : null
|
|
759
|
+
)
|
|
760
|
+
})
|
|
761
|
+
))),
|
|
762
|
+
h('div', { className: 'pwt-hint' }, t('bindHint'))
|
|
763
|
+
)
|
|
764
|
+
),
|
|
765
|
+
// 段落卡片区(能力开关已并入,不再单独列官方组合块)
|
|
766
|
+
data === null
|
|
767
|
+
? h('div', { className: 'pwt-hint' }, current ? t('loading') : t('pickFirst'))
|
|
768
|
+
: (data.sections.length === 0 ? h('div', { className: 'pwt-hint' }, t('noSections')) : cards)
|
|
769
|
+
))
|
|
770
|
+
|
|
771
|
+
return h('div', { className: 'pwt-root' }, children)
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function apply(ctx) {
|
|
775
|
+
var slots = ctx.get('slots')
|
|
776
|
+
if (slots === undefined) return
|
|
777
|
+
slots.inject('settings.section', function () {
|
|
778
|
+
slots.register(
|
|
779
|
+
{ name: 'settings.section', id: 'preset-workbench', order: 60, label: (langNow() === 'en' ? 'Preset Workbench' : '预设工作台') },
|
|
780
|
+
function (props) { return h(PwtErrorBoundary, null, h(WorkbenchPage, props || {})) }
|
|
781
|
+
)
|
|
782
|
+
})
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
exports.apply = apply
|
|
786
|
+
exports.inject = ['slots']
|
|
787
|
+
exports.default = { apply: apply, inject: ['slots'] }
|
|
788
|
+
|
|
789
|
+
return module.exports
|
|
790
|
+
},
|
|
791
|
+
})
|
|
792
|
+
})()
|