dsh-prompt-customizer 0.2.7 → 0.3.1
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.en.md +171 -72
- package/README.md +170 -72
- package/client/client.js +1753 -410
- package/client/client.js.map +1 -1
- package/lib/catalog.js +131 -0
- package/lib/effective.js +109 -0
- package/lib/index.js +712 -184
- package/lib/promotion.js +80 -0
- package/lib/schema.js +78 -33
- package/lib/sectionOps.d.mts +32 -0
- package/lib/sectionOps.mjs +134 -0
- package/lib/store.js +124 -0
- package/package.json +7 -5
- package/src/client/PresetsTab.tsx +141 -48
- package/src/client/PreviewTab.tsx +67 -41
- package/src/client/PreviewTools.tsx +6 -4
- package/src/client/SectionsTab.tsx +508 -156
- package/src/client/ToolsTab.tsx +253 -35
- package/src/client/index.ts +331 -46
- package/src/client/locales.ts +135 -7
- package/src/client/preset-io.ts +225 -0
- package/src/client/presets.ts +256 -116
- package/src/client/styles.ts +9 -1
- package/src/client/types.ts +89 -44
- package/lib/client.js +0 -344
package/lib/catalog.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 「本系统全部提示词 / 全部工具」的累积登记表 —— `catalog.yaml`。
|
|
3
|
+
*
|
|
4
|
+
* 段与工具都挂在各 agent 预设自己的 scope 里,要看到某个预设独有的名字必须先
|
|
5
|
+
* 挂载它(`standingKeyFor` 组装整棵插件树,单个可慢到十几秒)。清单面板要的却是
|
|
6
|
+
* 「不随编辑目标切换、只增不减」的全景,所以这里不做全量预热,而是懒式累积:
|
|
7
|
+
* 每次为任一 scope 构建清单 / 预览,就把这次真正看到的段与工具并进登记表 ——
|
|
8
|
+
* 同名后见到者赢,新名追加,绝不因为切换预设而变少。
|
|
9
|
+
*
|
|
10
|
+
* 文件只是派生缓存:删掉它会随浏览重新长出来,不含任何用户配置。因此登记表只存
|
|
11
|
+
* 身份与内容(段名 / 顺序 / 文本,工具名 / 描述),**不存**屏蔽、替换、隐藏这类
|
|
12
|
+
* 按作用域计算的标记 —— 那些永远在响应时按当前生效配置现算。
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import path from 'node:path'
|
|
16
|
+
import { createConfigStore } from './store.js'
|
|
17
|
+
|
|
18
|
+
/** 只取已定义字段,避免 `{...旧, ...新}` 用 undefined 把旧值抹成空洞。 */
|
|
19
|
+
function definedFields(item) {
|
|
20
|
+
const patch = {}
|
|
21
|
+
for (const [key, value] of Object.entries(item)) {
|
|
22
|
+
if (value !== undefined) patch[key] = value
|
|
23
|
+
}
|
|
24
|
+
return patch
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isName(value) {
|
|
28
|
+
return typeof value === 'string' && value.length > 0
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 把一次所见并进已有名单:同名覆盖(后见到者赢,保持原有位置),新名追加。
|
|
33
|
+
* `orderOf` 给缺少 order 的新条目补一个稳定下标(工具池不需要)。
|
|
34
|
+
* 返回 `{ list, changed }` —— changed 为 false 时调用方可以不落盘。
|
|
35
|
+
*/
|
|
36
|
+
export function mergeSighting(prev, sighting, orderOf = null) {
|
|
37
|
+
const list = (Array.isArray(prev) ? prev : []).map((item) => ({ ...item }))
|
|
38
|
+
const index = new Map(list.map((item, i) => [item?.name, i]))
|
|
39
|
+
let changed = false
|
|
40
|
+
for (const raw of Array.isArray(sighting) ? sighting : []) {
|
|
41
|
+
if (raw === null || typeof raw !== 'object' || !isName(raw.name)) continue
|
|
42
|
+
const at = index.get(raw.name)
|
|
43
|
+
if (at === undefined) {
|
|
44
|
+
const next = { ...raw }
|
|
45
|
+
if (orderOf !== null && next.order === undefined) next.order = orderOf(list, next)
|
|
46
|
+
index.set(raw.name, list.length)
|
|
47
|
+
list.push(next)
|
|
48
|
+
changed = true
|
|
49
|
+
continue
|
|
50
|
+
}
|
|
51
|
+
const patch = definedFields(raw)
|
|
52
|
+
const merged = { ...list[at], ...patch }
|
|
53
|
+
if (JSON.stringify(merged) === JSON.stringify(list[at])) continue
|
|
54
|
+
list[at] = merged
|
|
55
|
+
changed = true
|
|
56
|
+
}
|
|
57
|
+
return { list, changed }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** 创建一个累积登记表。`dir` 与配置文件同目录;读不到 / 坏文件一律从空开始。 */
|
|
61
|
+
export function createCatalog({ dir, file, warn } = {}) {
|
|
62
|
+
const target = file ?? path.join(dir ?? '.', 'catalog.yaml')
|
|
63
|
+
const store = createConfigStore({
|
|
64
|
+
file: target,
|
|
65
|
+
warn,
|
|
66
|
+
header: '# dsh-prompt-customizer 段 / 工具累积登记表(派生缓存,非配置)。\n'
|
|
67
|
+
+ '# 由面板浏览各 agent 预设时自动累积;删掉本文件会随下次浏览重新长出来。\n',
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
/** 归一化磁盘内容:只认数组里的具名条目,段保留 order / text。 */
|
|
71
|
+
const normalize = (raw, kind) => {
|
|
72
|
+
const list = Array.isArray(raw) ? raw : []
|
|
73
|
+
if (kind === 'sections') {
|
|
74
|
+
return list
|
|
75
|
+
.filter((x) => x !== null && typeof x === 'object' && isName(x.name))
|
|
76
|
+
.map((x) => ({ name: x.name, order: typeof x.order === 'number' ? x.order : undefined, text: typeof x.text === 'string' ? x.text : '' }))
|
|
77
|
+
.map((x) => (x.order === undefined ? { name: x.name, text: x.text } : { name: x.name, order: x.order, text: x.text }))
|
|
78
|
+
}
|
|
79
|
+
return list
|
|
80
|
+
.filter((x) => x !== null && typeof x === 'object' && isName(x.name))
|
|
81
|
+
.map((x) => ({ name: x.name, description: typeof x.description === 'string' ? x.description : '' }))
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const load = () => {
|
|
85
|
+
let doc
|
|
86
|
+
try { doc = store.readResolved() ?? {} } catch { doc = {} }
|
|
87
|
+
return {
|
|
88
|
+
sections: normalize(doc.sections, 'sections'),
|
|
89
|
+
tools: normalize(doc.tools, 'tools'),
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let state = load()
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
/** 当前并集(按登记顺序;调用方自己现算标记与排序)。 */
|
|
97
|
+
read() {
|
|
98
|
+
return { sections: state.sections.map((x) => ({ ...x })), tools: state.tools.map((x) => ({ ...x })) }
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 并进一次所见。段的 order 只在所见提供时才更新(预览路径没有 order,
|
|
103
|
+
* 不该把已知顺序打乱);新段用「当前最大 order + 1」落位,保持追加在尾部。
|
|
104
|
+
*/
|
|
105
|
+
observe({ sections = [], tools = [] } = {}) {
|
|
106
|
+
// 与 config.yaml 同构的语义:外部编辑 / 删除即时生效(store 是 mtime 懒
|
|
107
|
+
// 加载的,这里取一次最新状态再合并)。否则删掉本文件后,内存里的旧池子
|
|
108
|
+
// 会在下次请求被原样写回来,「删掉随浏览重建」就成了假话。
|
|
109
|
+
state = load()
|
|
110
|
+
let changed = false
|
|
111
|
+
const nextOrder = (list) => {
|
|
112
|
+
let max = -1
|
|
113
|
+
for (const item of list) if (typeof item.order === 'number' && item.order > max) max = item.order
|
|
114
|
+
return max + 1
|
|
115
|
+
}
|
|
116
|
+
const mergedSections = mergeSighting(state.sections, sections, (list) => nextOrder(list))
|
|
117
|
+
if (mergedSections.changed) { state.sections = mergedSections.list; changed = true }
|
|
118
|
+
const mergedTools = mergeSighting(state.tools, tools)
|
|
119
|
+
if (mergedTools.changed) { state.tools = mergedTools.list; changed = true }
|
|
120
|
+
if (!changed) return false
|
|
121
|
+
// 落盘失败只影响缓存持久性,绝不让清单 / 预览请求失败。
|
|
122
|
+
try { store.writeSection(state) } catch (error) {
|
|
123
|
+
try { warn?.(`prompt-customizer: catalog.yaml 写入失败(本次仅内存累积):${String(error && error.message ? error.message : error)}`) } catch { /* logger 缺席 */ }
|
|
124
|
+
}
|
|
125
|
+
return true
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
/** 测试 / 排查用:重新从磁盘读一次。 */
|
|
129
|
+
reload() { state = load(); return state },
|
|
130
|
+
}
|
|
131
|
+
}
|
package/lib/effective.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 字段级 override 合并(方案 A)+ 阶段过滤(方案 C)。
|
|
3
|
+
*
|
|
4
|
+
* 合并语义:override 中**存在的字段整体接管**全局默认;空缺的字段回落
|
|
5
|
+
* 全局。replace / inject / tools 沿用「非空才接管」(空对象 / 空列表视同
|
|
6
|
+
* 未设置);三份段屏蔽名单(sections / sectionsBootstrap / sectionsCompaction)
|
|
7
|
+
* 例外 —— **空数组 = 显式接管为空**(用户在该预设下解除全部屏蔽,绝不能
|
|
8
|
+
* 回落全局把屏蔽「复活」;客户端 editView 的 `??` 语义与此一致)。
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** 非空数组才视为有效覆盖值(inject 用;屏蔽名单不适用,见上)。 */
|
|
12
|
+
function hasList(value) {
|
|
13
|
+
return Array.isArray(value) && value.length > 0
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** 非空对象(至少一个键)才视为有效覆盖值。 */
|
|
17
|
+
function hasDict(value) {
|
|
18
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length > 0
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** 工具配置整体是否携带至少一条有效规则(含 bootstrap / compaction 目录,
|
|
22
|
+
* exclude 或 add 任一非空即算)。 */
|
|
23
|
+
function hasTools(tools) {
|
|
24
|
+
if (tools === null || typeof tools !== 'object') return false
|
|
25
|
+
return hasList(tools.exclude) || hasList(tools.add) || hasTools(tools.bootstrap) || hasTools(tools.compaction)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 字段级合并出一份生效配置。replace / inject / tools 看非空;三份段屏蔽
|
|
30
|
+
* 名单看「字段是否存在」(空数组 = 显式接管为空,不回落全局)。
|
|
31
|
+
*/
|
|
32
|
+
export function mergeConfig(globalCfg, ovr) {
|
|
33
|
+
const g = globalCfg ?? {}
|
|
34
|
+
return {
|
|
35
|
+
sections: Array.isArray(ovr?.sections) ? ovr.sections : (g.sections ?? []),
|
|
36
|
+
sectionsBootstrap: Array.isArray(ovr?.sectionsBootstrap) ? ovr.sectionsBootstrap : (g.sectionsBootstrap ?? []),
|
|
37
|
+
sectionsCompaction: Array.isArray(ovr?.sectionsCompaction) ? ovr.sectionsCompaction : (g.sectionsCompaction ?? []),
|
|
38
|
+
replace: hasDict(ovr?.replace) ? ovr.replace : (g.replace ?? {}),
|
|
39
|
+
inject: hasList(ovr?.inject) ? ovr.inject : (g.inject ?? []),
|
|
40
|
+
tools: hasTools(ovr?.tools) ? ovr.tools : (g.tools ?? {}),
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 选出当前阶段生效的注入段:常驻期注入(`always`)始终注入;其余注入按
|
|
46
|
+
* 阶段模式匹配 —— bootstrap(未晋级)→ `bootstrap`,active(已晋级)→
|
|
47
|
+
* `active`,compact(压缩后仍未晋级)→ `compaction`。每个阶段的注入集
|
|
48
|
+
* 互相独立(各自的 order 空间),用于「每阶段独立名单 + 虚拟 order」。
|
|
49
|
+
* 传入 `status`(promotion.status() 结果 `{ promoted, boundary }`),
|
|
50
|
+
* 不再沿用旧的布尔 promoted(旧的 compaction 窗口被硬塞进 bootstrap 期,
|
|
51
|
+
* 无法独立排序)。
|
|
52
|
+
*/
|
|
53
|
+
export function filterInjectByPhase(injectList, status) {
|
|
54
|
+
const mode = status.promoted ? 'active' : status.boundary >= 0 ? 'compaction' : 'bootstrap'
|
|
55
|
+
return (injectList ?? []).filter((item) => {
|
|
56
|
+
const phase = item.phase === 'bootstrap' || item.phase === 'active' || item.phase === 'compaction' ? item.phase : 'always'
|
|
57
|
+
return phase === 'always' || phase === mode
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 选出当前阶段生效的工具过滤配置(三态):三个阶段**各用自己那份名单,互不
|
|
63
|
+
* 继承** —— 已晋级 → 静态 `exclude`,未晋级 → `bootstrap.exclude`,压缩后仍未
|
|
64
|
+
* 晋级 → `compaction.exclude`;空名单就表示「该阶段什么都不隐藏」。
|
|
65
|
+
*
|
|
66
|
+
* 早期版本让阶段名单在为空时回落静态字段,看起来省了一份重复配置,实际是拿
|
|
67
|
+
* 走了阶段独立性:界面按三个阶段分别勾选,而引导期的勾选状态既反映不了静态
|
|
68
|
+
* 名单里已隐藏的工具(显示成"可见"、模型其实看不见),往引导期名单里加一项又
|
|
69
|
+
* 会让整个静态名单在该阶段失效(其它工具的隐藏一起蒸发)。
|
|
70
|
+
* `status` 是 promotion.status() 的结果 `{ promoted, boundary }`。
|
|
71
|
+
*/
|
|
72
|
+
export function pickToolsFilter(toolsCfg, status) {
|
|
73
|
+
if (!status.promoted) {
|
|
74
|
+
if (status.boundary >= 0) {
|
|
75
|
+
return { exclude: toolsCfg?.compaction?.exclude ?? [], add: toolsCfg?.compaction?.add ?? [] }
|
|
76
|
+
}
|
|
77
|
+
return { exclude: toolsCfg?.bootstrap?.exclude ?? [], add: toolsCfg?.bootstrap?.add ?? [] }
|
|
78
|
+
}
|
|
79
|
+
return { exclude: toolsCfg?.exclude ?? [], add: toolsCfg?.add ?? [] }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 黑名单过滤(装配钩子与 inventory 共用):把 `exclude` 里的工具从目录里拿掉。
|
|
84
|
+
*
|
|
85
|
+
* 刻意不支持「白名单 / 封闭式允许清单」:include 一旦生效就让 exclude 完全失效,
|
|
86
|
+
* 而且它会把目录冻结在点名的几个上 —— 别的插件以后新增的工具一律看不见。「只留
|
|
87
|
+
* 这几个」用 exclude 反选即可,无需第二套语义。
|
|
88
|
+
*
|
|
89
|
+
* 「把被裁掉但注册表里仍有的工具加回该阶段」是另一条独立通道(`add` 名单),由
|
|
90
|
+
* 装配钩子在过滤**之后**从注册表查回 schema 追加 —— 见 lib/index.js,不在这里。
|
|
91
|
+
*/
|
|
92
|
+
export function applyToolFilter(tools, { exclude }) {
|
|
93
|
+
const excludeSet = new Set(exclude ?? [])
|
|
94
|
+
if (excludeSet.size > 0) return tools.filter((tool) => !excludeSet.has(tool.name))
|
|
95
|
+
return tools
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 每阶段独立段屏蔽(与工具目录三态对称):压缩受控期在「压缩后仍未晋级」
|
|
100
|
+
* 时生效且优先于引导期;引导期在未晋级时生效;否则回落常驻期(全局
|
|
101
|
+
* `sections`,列表为空表示不启用阶段化)。返回该阶段额外屏蔽的段名。
|
|
102
|
+
*/
|
|
103
|
+
export function pickSectionsForStatus(config, status) {
|
|
104
|
+
const compaction = config?.sectionsCompaction
|
|
105
|
+
const bootstrap = config?.sectionsBootstrap
|
|
106
|
+
if (!status.promoted && status.boundary >= 0 && Array.isArray(compaction) && compaction.length > 0) return compaction
|
|
107
|
+
if (!status.promoted && Array.isArray(bootstrap) && bootstrap.length > 0) return bootstrap
|
|
108
|
+
return []
|
|
109
|
+
}
|