dsh-prompt-customizer 0.4.2 → 0.5.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.md +41 -35
- package/client/client.js +1655 -781
- package/client/client.js.map +1 -1
- package/lib/effective.js +2 -2
- package/lib/index.js +139 -100
- package/lib/promotion.js +58 -62
- package/lib/sectionOps.d.mts +35 -0
- package/lib/sectionOps.mjs +140 -0
- package/lib/zh/cordis.mjs +110 -0
- package/lib/zh/core.mjs +47 -0
- package/lib/zh/index.d.mts +8 -0
- package/lib/zh/index.mjs +23 -0
- package/lib/zh/sdk.mjs +22 -0
- package/lib/zh/teams.mjs +19 -0
- package/lib/zh/undo.mjs +18 -0
- package/package.json +9 -10
- package/src/client/Panel.tsx +532 -0
- package/src/client/{PresetsTab.tsx → PresetsPane.tsx} +146 -129
- package/src/client/PreviewPane.tsx +73 -0
- package/src/client/SectionsPane.tsx +331 -0
- package/src/client/ToolsPane.tsx +230 -0
- package/src/client/error-boundary.tsx +48 -0
- package/src/client/index.ts +69 -406
- package/src/client/locales.ts +44 -8
- package/src/client/nav.tsx +284 -0
- package/src/client/shell.tsx +179 -0
- package/src/client/styles.ts +24 -14
- package/src/client/types.ts +1 -4
- package/src/client/PreviewTab.tsx +0 -92
- package/src/client/SectionsTab.tsx +0 -539
- package/src/client/ToolsTab.tsx +0 -293
package/lib/effective.js
CHANGED
|
@@ -46,7 +46,7 @@ export function mergeConfig(globalCfg, ovr) {
|
|
|
46
46
|
* 阶段模式匹配 —— bootstrap(未晋级)→ `bootstrap`,active(已晋级)→
|
|
47
47
|
* `active`,compact(压缩后仍未晋级)→ `compaction`。每个阶段的注入集
|
|
48
48
|
* 互相独立(各自的 order 空间),用于「每阶段独立名单 + 虚拟 order」。
|
|
49
|
-
* 传入 `status
|
|
49
|
+
* 传入 `status`(阶段投影 statusOf 的结果 `{ promoted, boundary }`),
|
|
50
50
|
* 不再沿用旧的布尔 promoted(旧的 compaction 窗口被硬塞进 bootstrap 期,
|
|
51
51
|
* 无法独立排序)。
|
|
52
52
|
*/
|
|
@@ -67,7 +67,7 @@ export function filterInjectByPhase(injectList, status) {
|
|
|
67
67
|
* 走了阶段独立性:界面按三个阶段分别勾选,而引导期的勾选状态既反映不了静态
|
|
68
68
|
* 名单里已隐藏的工具(显示成"可见"、模型其实看不见),往引导期名单里加一项又
|
|
69
69
|
* 会让整个静态名单在该阶段失效(其它工具的隐藏一起蒸发)。
|
|
70
|
-
* `status`
|
|
70
|
+
* `status` 是阶段投影 statusOf 的结果 `{ promoted, boundary }`。
|
|
71
71
|
*/
|
|
72
72
|
export function pickToolsFilter(toolsCfg, status) {
|
|
73
73
|
if (!status.promoted) {
|
package/lib/index.js
CHANGED
|
@@ -40,7 +40,7 @@ import os from 'node:os'
|
|
|
40
40
|
import path from 'node:path'
|
|
41
41
|
import { Config, NS } from './schema.js'
|
|
42
42
|
import { mergeConfig, pickToolsFilter, pickSectionsForStatus, applyToolFilter, applySectionPolicy } from './effective.js'
|
|
43
|
-
import {
|
|
43
|
+
import { createPhaseProjection, presetOfSession, statusOf } from './promotion.js'
|
|
44
44
|
import { createConfigStore, readLegacySection } from './store.js'
|
|
45
45
|
import { createCatalog } from './catalog.js'
|
|
46
46
|
import { registerVariables, listVariableNames } from './vars.js'
|
|
@@ -60,8 +60,7 @@ function dshHome() {
|
|
|
60
60
|
return process.env.DSH_HOME ?? path.join(os.homedir(), '.dsh')
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
/** 收集并解析一个请求体(JSON),超过上限直接拒绝——路由是本进程私有的,上限只是防御。 */
|
|
64
|
-
function readJsonBody(req, limit = 2 * 1024 * 1024) {
|
|
63
|
+
/** 收集并解析一个请求体(JSON),超过上限直接拒绝——路由是本进程私有的,上限只是防御。 */function readJsonBody(req, limit = 2 * 1024 * 1024) {
|
|
65
64
|
return new Promise((resolve, reject) => {
|
|
66
65
|
let size = 0
|
|
67
66
|
const chunks = []
|
|
@@ -159,11 +158,16 @@ export function apply(ctx, entry = {}) {
|
|
|
159
158
|
// 启动时剥掉并记一条日志(逐处列出被去掉的名字),不静默改用户文件。
|
|
160
159
|
stripLegacyInclude(store, (message) => { try { ctx.logger?.info?.(message) } catch { /* logger 缺席 */ } })
|
|
161
160
|
|
|
162
|
-
// 2)
|
|
163
|
-
//
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
161
|
+
// 2) 阶段投影:把 bootstrap/active 推导注册成宿主的会话投影 unit —— 宿主按会话
|
|
162
|
+
// 折叠(含 resume / reload 后从日志重建)并增量驱动,本插件不再自扫历史
|
|
163
|
+
// (`Session.events` 已在 0.1.2-alpha.4 移除,同步历史读在 0.1.6 已 deprecated;
|
|
164
|
+
// 见 promotion.js 顶部说明)。注册随本插件的 fiber 一起注销。
|
|
165
|
+
ctx.inject(['sessionProjections'], (projectionCtx) => {
|
|
166
|
+
try {
|
|
167
|
+
projectionCtx.sessionProjections.register(createPhaseProjection())
|
|
168
|
+
} catch (error) {
|
|
169
|
+
try { ctx.logger?.warn?.(`prompt-customizer: 阶段投影注册失败(阶段视图回落为已晋级):${String((error && error.message) || error)}`) } catch { /* logger 缺席 */ }
|
|
170
|
+
}
|
|
167
171
|
})
|
|
168
172
|
|
|
169
173
|
// 单次降级告警:过滤逻辑出错时退回未加工的装配,绝不拖垮会话。
|
|
@@ -196,20 +200,30 @@ export function apply(ctx, entry = {}) {
|
|
|
196
200
|
// 3) 装配目标解析(瀑布流与 assemble 强制覆盖共用):生效配置 = 显式预览
|
|
197
201
|
// 提示 > 会话所属 agent 预设(字段级覆盖)> 全局默认。预览 hint 优先于
|
|
198
202
|
// 会话事件推导的阶段,让同一份配置能渲染 bootstrap / compaction / active
|
|
199
|
-
// 三种阶段视图。boundary
|
|
203
|
+
// 三种阶段视图。boundary 语义与阶段投影 statusOf 一致(-1 = 从未压缩)。
|
|
204
|
+
// 预览装配可携带 promptCustomizerPatch(UI 未保存草稿):只在内存里叠加
|
|
205
|
+
// 到目标层(overlayPatch),磁盘不动 —— 实时预览「所见即保存后所得」。
|
|
200
206
|
const resolveTarget = (context) => {
|
|
201
207
|
const raw = read()
|
|
208
|
+
const patch = context?.promptCustomizerPatch
|
|
209
|
+
const base = patch !== null && typeof patch === 'object' && !Array.isArray(patch)
|
|
210
|
+
? overlayPatch(raw,
|
|
211
|
+
typeof context.promptCustomizerTarget === 'string' && context.promptCustomizerTarget.length > 0
|
|
212
|
+
? context.promptCustomizerTarget
|
|
213
|
+
: undefined,
|
|
214
|
+
patch)
|
|
215
|
+
: raw
|
|
202
216
|
const id = typeof context?.promptCustomizerPreset === 'string'
|
|
203
217
|
? context.promptCustomizerPreset
|
|
204
|
-
: presetOfSession(context?.agent?.session)
|
|
205
|
-
const cfg = mergeConfig(
|
|
218
|
+
: presetOfSession(ctx.get('sessionProjections'), context?.agent?.session)
|
|
219
|
+
const cfg = mergeConfig(base, base.overrides?.[id])
|
|
206
220
|
const phaseHint = typeof context?.promptCustomizerPhase === 'string'
|
|
207
221
|
? context.promptCustomizerPhase
|
|
208
222
|
: undefined
|
|
209
223
|
const status = phaseHint === 'bootstrap' ? { promoted: false, boundary: -1 }
|
|
210
224
|
: phaseHint === 'compaction' ? { promoted: false, boundary: 1 }
|
|
211
225
|
: phaseHint === 'active' ? { promoted: true, boundary: -1 }
|
|
212
|
-
:
|
|
226
|
+
: statusOf(ctx.get('sessionProjections'), context?.agent)
|
|
213
227
|
return { raw, cfg, status }
|
|
214
228
|
}
|
|
215
229
|
|
|
@@ -341,7 +355,38 @@ export function apply(ctx, entry = {}) {
|
|
|
341
355
|
// 会抛 "cannot get property without inject"。
|
|
342
356
|
const webserver = ctx.get('webServer')
|
|
343
357
|
if (webserver) {
|
|
344
|
-
|
|
358
|
+
// 连接信任闸(宿主 0.1.2-alpha.1 起):官方 /api 通道走 process 级 bootstrap
|
|
359
|
+
// token + 签名 Cookie,而直接注册在 webServer 上的自建路由不在其中 —— 宿主
|
|
360
|
+
// 契约要求先问过 connection.requestRejection:其 Host/Origin 围栏挡 DNS
|
|
361
|
+
// rebinding 与跨站调用,浏览器认证再挡未持 Cookie 的调用。本插件的路由会写盘
|
|
362
|
+
// (config/presets),必须过闸;闸本身不可用(connection 服务缺席)时保守
|
|
363
|
+
// 拒绝(fail-closed)——「没有认证面」不等于「无需认证」。
|
|
364
|
+
//
|
|
365
|
+
// 服务每次请求现读,绝不在 apply 时捕获:connection 由同 profile 的另一条
|
|
366
|
+
// entry 提供,entry 创建顺序不保证(实测冷启时本插件 apply 早于其提供,
|
|
367
|
+
// 捕获版会让整组路由永久 503);宿主自带路由(open-in-app 等)同款懒读。
|
|
368
|
+
const unauthenticated = (req, res) => {
|
|
369
|
+
const connection = ctx.get('connection')
|
|
370
|
+
if (typeof connection?.requestRejection !== 'function') {
|
|
371
|
+
writeJson(res, 503, { ok: false, error: 'connection service unavailable: prompt-customizer routes refuse to serve unauthenticated' })
|
|
372
|
+
return true
|
|
373
|
+
}
|
|
374
|
+
const rejection = connection.requestRejection(req)
|
|
375
|
+
if (rejection === undefined) return false
|
|
376
|
+
res.writeHead(rejection, { 'content-type': 'text/plain; charset=utf-8' })
|
|
377
|
+
res.end(rejection === 401 ? 'unauthorized' : 'forbidden')
|
|
378
|
+
return true
|
|
379
|
+
}
|
|
380
|
+
/** 与 webserver.register 同形,统一在 handler 前挂信任闸(注册顺序与 disposer 不变)。 */
|
|
381
|
+
const register = (route) => webserver.register({
|
|
382
|
+
kind: route.kind,
|
|
383
|
+
path: route.path,
|
|
384
|
+
handler: (req, res) => {
|
|
385
|
+
if (unauthenticated(req, res)) return undefined
|
|
386
|
+
return route.handler(req, res)
|
|
387
|
+
},
|
|
388
|
+
})
|
|
389
|
+
const dispose = register({
|
|
345
390
|
kind: 'exact',
|
|
346
391
|
path: '/api/prompt-customizer/inventory',
|
|
347
392
|
handler: async (req, res) => {
|
|
@@ -376,13 +421,24 @@ export function apply(ctx, entry = {}) {
|
|
|
376
421
|
// 使用 standard agent,与清单一致);可选 `?phase=<bootstrap|compaction|
|
|
377
422
|
// active>` 预览指定对话阶段的三态视图(省略时按晋级推导,只读路由下
|
|
378
423
|
// 即 active / 静态视图)。
|
|
379
|
-
|
|
424
|
+
// POST 形态(UI 实时预览):body { target?, patch?, phase? } —— patch 是
|
|
425
|
+
// UI 未保存的编辑草稿,只在内存里叠加(overlayPatch),绝不写盘。
|
|
426
|
+
const disposePreview = register({
|
|
380
427
|
kind: 'exact',
|
|
381
428
|
path: '/api/prompt-customizer/preview',
|
|
382
429
|
handler: async (req, res) => {
|
|
383
430
|
try {
|
|
384
|
-
|
|
385
|
-
|
|
431
|
+
let body = null
|
|
432
|
+
if (req.method === 'POST') {
|
|
433
|
+
try { body = await readJsonBody(req) } catch { body = null }
|
|
434
|
+
}
|
|
435
|
+
const { scopeKey, hintId, phase, scopeResolved } = await previewTarget(ctx, req, body)
|
|
436
|
+
const patch = body !== null && typeof body === 'object'
|
|
437
|
+
&& body.patch !== null && typeof body.patch === 'object' && !Array.isArray(body.patch)
|
|
438
|
+
? body.patch
|
|
439
|
+
: undefined
|
|
440
|
+
const patchTarget = typeof body?.target === 'string' && body.target.length > 0 ? body.target : undefined
|
|
441
|
+
const preview = await buildPreview(ctx, scopeKey, hintId, phase, catalog, patch, patchTarget)
|
|
386
442
|
writeJson(res, 200, { ...preview, scopeResolved, phase })
|
|
387
443
|
} catch (error) {
|
|
388
444
|
writeJson(res, 500, {
|
|
@@ -396,7 +452,7 @@ export function apply(ctx, entry = {}) {
|
|
|
396
452
|
|
|
397
453
|
// agent 预设枚举路由:直接读 roster 的 list()(未 memo 化,运行中新增
|
|
398
454
|
// 的预设立即可见),给 UI 的目标选择器与预览选择器供货。
|
|
399
|
-
const disposePresets =
|
|
455
|
+
const disposePresets = register({
|
|
400
456
|
kind: 'exact',
|
|
401
457
|
path: '/api/prompt-customizer/agent-presets',
|
|
402
458
|
handler: async (_req, res) => {
|
|
@@ -423,7 +479,7 @@ export function apply(ctx, entry = {}) {
|
|
|
423
479
|
// 来源预设的原样组成 + 我刚改好的定制,这才叫「保存的是我修改后的样子」。
|
|
424
480
|
// 我们不再自己生成骨架:骨架带着 persona complete: true,宿主会在水流之后把
|
|
425
481
|
// sections 还原成那一条段,段级定制在它上面根本不生效。
|
|
426
|
-
const disposePresetsCreate =
|
|
482
|
+
const disposePresetsCreate = register({
|
|
427
483
|
kind: 'exact',
|
|
428
484
|
path: '/api/prompt-customizer/presets',
|
|
429
485
|
handler: async (req, res) => {
|
|
@@ -486,7 +542,7 @@ export function apply(ctx, entry = {}) {
|
|
|
486
542
|
// GET /config → { ok, config }(解析后含默认值)
|
|
487
543
|
// POST /config/set → body { field, value };写入并返回最新 config
|
|
488
544
|
// POST /config/unset → body { field };删除字段并返回最新 config
|
|
489
|
-
const disposeConfig =
|
|
545
|
+
const disposeConfig = register({
|
|
490
546
|
kind: 'exact',
|
|
491
547
|
path: '/api/prompt-customizer/config',
|
|
492
548
|
handler: async (_req, res) => {
|
|
@@ -511,14 +567,14 @@ export function apply(ctx, entry = {}) {
|
|
|
511
567
|
})
|
|
512
568
|
}
|
|
513
569
|
}
|
|
514
|
-
const disposeConfigSet =
|
|
570
|
+
const disposeConfigSet = register({
|
|
515
571
|
kind: 'exact',
|
|
516
572
|
path: '/api/prompt-customizer/config/set',
|
|
517
573
|
handler: withFieldWrite(({ field, value }) => store.setField(field, value)),
|
|
518
574
|
})
|
|
519
575
|
ctx.effect(() => disposeConfigSet, 'prompt-customizer: config set route')
|
|
520
576
|
|
|
521
|
-
const disposeConfigUnset =
|
|
577
|
+
const disposeConfigUnset = register({
|
|
522
578
|
kind: 'exact',
|
|
523
579
|
path: '/api/prompt-customizer/config/unset',
|
|
524
580
|
handler: withFieldWrite(({ field }) => store.setField(field, undefined)),
|
|
@@ -529,7 +585,7 @@ export function apply(ctx, entry = {}) {
|
|
|
529
585
|
// 无需卸载 / 重启(forceSections: false 时 assemble 包装直接放行宿主原样,
|
|
530
586
|
// 瀑布流里空配置的过滤是无操作)。文件本身保留,避免卸载重装后迁移逻辑
|
|
531
587
|
// 再次触发;之后要重新启用定制,把 forceSections 改回 true 或删除该行即可。
|
|
532
|
-
const disposeConfigReset =
|
|
588
|
+
const disposeConfigReset = register({
|
|
533
589
|
kind: 'exact',
|
|
534
590
|
path: '/api/prompt-customizer/config/reset',
|
|
535
591
|
handler: async (_req, res) => {
|
|
@@ -547,11 +603,10 @@ export function apply(ctx, entry = {}) {
|
|
|
547
603
|
ctx.effect(() => disposeConfigReset, 'prompt-customizer: config reset route')
|
|
548
604
|
|
|
549
605
|
// 批量保存路由:UI「统一保存按钮」的唯一写盘通道。body { target?, patch },
|
|
550
|
-
// patch
|
|
551
|
-
//
|
|
552
|
-
//
|
|
553
|
-
const
|
|
554
|
-
const disposeConfigApply = webserver.register({
|
|
606
|
+
// patch 只认业务字段(含每阶段独立段名单),字段值为 null 表示删除(回退
|
|
607
|
+
// 默认/全局)。target 缺省写全局顶层;target 为 agent 预设 id 时写
|
|
608
|
+
// overrides[id]。叠加语义与 /preview 的草稿预览共用 overlayPatch(一份实现)。
|
|
609
|
+
const disposeConfigApply = register({
|
|
555
610
|
kind: 'exact',
|
|
556
611
|
path: '/api/prompt-customizer/config/apply',
|
|
557
612
|
handler: async (req, res) => {
|
|
@@ -563,26 +618,7 @@ export function apply(ctx, entry = {}) {
|
|
|
563
618
|
return
|
|
564
619
|
}
|
|
565
620
|
const target = typeof body.target === 'string' && body.target.length > 0 ? body.target : undefined
|
|
566
|
-
|
|
567
|
-
const applyPatch = (obj) => {
|
|
568
|
-
for (const field of APPLY_FIELDS) {
|
|
569
|
-
if (!Object.hasOwn(patch, field)) continue
|
|
570
|
-
if (patch[field] === null) delete obj[field]
|
|
571
|
-
else obj[field] = patch[field]
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
if (target === undefined) {
|
|
575
|
-
applyPatch(base)
|
|
576
|
-
} else {
|
|
577
|
-
const overrides = { ...(base.overrides ?? {}) }
|
|
578
|
-
const ovr = { ...(overrides[target] ?? {}) }
|
|
579
|
-
applyPatch(ovr)
|
|
580
|
-
if (Object.keys(ovr).length > 0) overrides[target] = ovr
|
|
581
|
-
else delete overrides[target]
|
|
582
|
-
if (Object.keys(overrides).length > 0) base.overrides = overrides
|
|
583
|
-
else delete base.overrides
|
|
584
|
-
}
|
|
585
|
-
store.writeSection(base)
|
|
621
|
+
store.writeSection(overlayPatch({ ...(store.raw() ?? {}) }, target, patch))
|
|
586
622
|
writeJson(res, 200, { ok: true, config: read() })
|
|
587
623
|
} catch (error) {
|
|
588
624
|
writeJson(res, 400, {
|
|
@@ -604,6 +640,39 @@ function withTimeout(value, ms, fallback) {
|
|
|
604
640
|
])
|
|
605
641
|
}
|
|
606
642
|
|
|
643
|
+
/** 配置补丁认得的业务字段(与 UI 编辑域一一对应;含每阶段独立名单)。 */
|
|
644
|
+
const APPLY_FIELDS = ['sections', 'sectionsBootstrap', 'sectionsCompaction', 'replace', 'inject', 'tools']
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* 把一份补丁叠加到 raw 配置的目标层,返回新对象(不改原对象、不碰磁盘)。
|
|
648
|
+
* target 缺省 = 全局顶层;字符串 = overrides[id]。字段值为 null = 删除该字段
|
|
649
|
+
*(回退默认 / 全局);全空的 override 连带删除,避免配置堆积空壳。
|
|
650
|
+
* /config/apply 落盘与 /preview 的草稿叠加(实时预览,只读内存)共用这一份
|
|
651
|
+
* 语义 —— 两处行为永远一致。
|
|
652
|
+
*/
|
|
653
|
+
function overlayPatch(raw, target, patch) {
|
|
654
|
+
const base = { ...(raw ?? {}) }
|
|
655
|
+
const applyPatch = (obj) => {
|
|
656
|
+
for (const field of APPLY_FIELDS) {
|
|
657
|
+
if (!Object.hasOwn(patch, field)) continue
|
|
658
|
+
if (patch[field] === null) delete obj[field]
|
|
659
|
+
else obj[field] = patch[field]
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
if (target === undefined) {
|
|
663
|
+
applyPatch(base)
|
|
664
|
+
} else {
|
|
665
|
+
const overrides = { ...(base.overrides ?? {}) }
|
|
666
|
+
const ovr = { ...(overrides[target] ?? {}) }
|
|
667
|
+
applyPatch(ovr)
|
|
668
|
+
if (Object.keys(ovr).length > 0) overrides[target] = ovr
|
|
669
|
+
else delete overrides[target]
|
|
670
|
+
if (Object.keys(overrides).length > 0) base.overrides = overrides
|
|
671
|
+
else delete base.overrides
|
|
672
|
+
}
|
|
673
|
+
return base
|
|
674
|
+
}
|
|
675
|
+
|
|
607
676
|
/**
|
|
608
677
|
* 宿主 authoring API(`agentPresets.copy`)抛错 → HTTP 状态码。
|
|
609
678
|
*
|
|
@@ -735,61 +804,34 @@ async function resolveScopeFor(ctx, id) {
|
|
|
735
804
|
* id 挂载失败时展示会回落 standard scope,但标记为 false,让 UI 明确警示
|
|
736
805
|
* 「预览的不是该预设的原生装配」。
|
|
737
806
|
*/
|
|
738
|
-
async function previewTarget(ctx, req) {
|
|
807
|
+
async function previewTarget(ctx, req, body = null) {
|
|
739
808
|
const id = queryOf(req).get('scope') ?? undefined
|
|
740
809
|
const resolved = id ? await resolveScopeFor(ctx, id) : await resolveStandardScope(ctx)
|
|
741
810
|
const scopeKey = resolved ?? (id ? await resolveStandardScope(ctx) : undefined)
|
|
742
|
-
// `?phase
|
|
743
|
-
|
|
811
|
+
// `?phase=`(或 POST body.phase):bootstrap / compaction / active 三态预览;
|
|
812
|
+
// 其它值一律回退实际推导。
|
|
813
|
+
const rawPhase = (body !== null && typeof body === 'object' ? body.phase : undefined)
|
|
814
|
+
?? queryOf(req).get('phase')
|
|
815
|
+
?? undefined
|
|
744
816
|
const phase = rawPhase === 'bootstrap' || rawPhase === 'compaction' || rawPhase === 'active'
|
|
745
817
|
? rawPhase
|
|
746
818
|
: undefined
|
|
747
819
|
return { scopeKey, hintId: id, phase, scopeResolved: resolved !== undefined }
|
|
748
820
|
}
|
|
749
821
|
|
|
750
|
-
/**
|
|
751
|
-
* 按预览阶段合成一个最小伪会话 agent。预设原生的阶段裁剪插件(zero-tool
|
|
752
|
-
* bootstrap / router-bootstrap / warmup 等)在 assemble 瀑布流里只读
|
|
753
|
-
* `session.id / session.events / session.header / agent.options.model` 推导
|
|
754
|
-
* 晋级状态并动态裁剪工具目录 —— 合成的 durable 事件让这些规则在只读预览
|
|
755
|
-
* 里按所选阶段真实运行(而非挤在常驻视图):
|
|
756
|
-
* - bootstrap:空事件(boundary -1,未晋级,首轮形态)
|
|
757
|
-
* - compaction:一条 compaction/end(压缩后未重新晋级)
|
|
758
|
-
* - active:一条晋级信号(assistant/message,event 形状见各预设的
|
|
759
|
-
* compaction-epoch.mjs —— 只认 type 与 seq)
|
|
760
|
-
*/
|
|
761
|
-
export function fakeAgentFor(phase, presetId) {
|
|
762
|
-
const events = phase === 'compaction'
|
|
763
|
-
? [{ type: 'compaction/end', seq: 1 }]
|
|
764
|
-
: phase === 'active'
|
|
765
|
-
? [{ type: 'assistant/message', seq: 1 }]
|
|
766
|
-
: []
|
|
767
|
-
return {
|
|
768
|
-
session: {
|
|
769
|
-
id: `prompt-customizer-preview-${phase}`,
|
|
770
|
-
events,
|
|
771
|
-
header: {
|
|
772
|
-
delegationDepth: 0,
|
|
773
|
-
agentPreset: presetId,
|
|
774
|
-
cwd: process.cwd(),
|
|
775
|
-
meta: {},
|
|
776
|
-
},
|
|
777
|
-
},
|
|
778
|
-
options: { provider: '', model: '' },
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
|
|
782
822
|
/**
|
|
783
823
|
* 渲染某个 scope 的最终系统提示词:跑完整的装配瀑布流(所有插件的过滤、
|
|
784
824
|
* 包括我们自己的都会生效),再把幸存的段拼成文本。这就是模型实际看到的内容。
|
|
785
825
|
*
|
|
786
|
-
*
|
|
787
|
-
*
|
|
788
|
-
*
|
|
789
|
-
*
|
|
790
|
-
*
|
|
826
|
+
* 装配是**纯 scope 的诊断装配(不带 agent)**:宿主的不变量契约明确「诊断
|
|
827
|
+
* 性装配不得携带 agent」—— agent-presets 的 roster 不变量见到未加入预设的
|
|
828
|
+
* agent 就 fail(抛异常),旧版伪会话每次都触发它,让每次阶段预览都降级。
|
|
829
|
+
* 当前 harness 的装配瀑布流里也没有读会话推导阶段的插件(阶段裁剪发生在
|
|
830
|
+
* agent-loop 层,不在 assemble 钩子),三个阶段视图完全由我们自己的
|
|
831
|
+
* promptCustomizerPhase hint 驱动。同时返回注册表原始目录总数 registryTotal,
|
|
832
|
+
* 与模型可见目录形成对照(如 PTC / Code Mode 把全目录包装成单一 run_code)。
|
|
791
833
|
*/
|
|
792
|
-
async function buildPreview(ctx, scopeKey, hintId, phase, catalog) {
|
|
834
|
+
async function buildPreview(ctx, scopeKey, hintId, phase, catalog, patch, patchTarget) {
|
|
793
835
|
// promptCustomizerPreset / promptCustomizerPhase 都是本插件的自定义装配
|
|
794
836
|
// 字段:让我们的过滤器在这次装配里按该 agent 预设的 override 与指定阶段
|
|
795
837
|
// 视图生效(AssembleContext 允许插件自定义字段)。
|
|
@@ -798,20 +840,18 @@ async function buildPreview(ctx, scopeKey, hintId, phase, catalog) {
|
|
|
798
840
|
promptCustomizerPreset: hintId,
|
|
799
841
|
promptCustomizerPhase: phase,
|
|
800
842
|
// 请求预过滤视图(base):本插件的过滤器把进入过滤前的段 / 工具原文
|
|
801
|
-
// 连同屏蔽 /
|
|
843
|
+
// 连同屏蔽 / 隐藏标记一起附在装配结果上,作为面板两个栏的统一数据源。
|
|
802
844
|
promptCustomizerBase: true,
|
|
803
845
|
}
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
)
|
|
810
|
-
} catch {
|
|
811
|
-
// 伪会话让某个预设插件抛错了:退回无会话装配,原生规则不参与本次预览。
|
|
812
|
-
degraded = phase !== undefined
|
|
813
|
-
assembled = await ctx.systemPrompt.assemble(hint)
|
|
846
|
+
// 实时预览(UI 未保存草稿):补丁挂进装配上下文,过滤器在 resolveTarget
|
|
847
|
+
// 里把它叠加到目标层 —— 只读内存,磁盘不动。
|
|
848
|
+
if (patch !== undefined) {
|
|
849
|
+
hint.promptCustomizerPatch = patch
|
|
850
|
+
hint.promptCustomizerTarget = patchTarget
|
|
814
851
|
}
|
|
852
|
+
// 纯 scope 诊断装配(不带 agent,见上方函数注释):让宿主 roster 不变量
|
|
853
|
+
// 按「诊断」放行,三个阶段视图由 promptCustomizerPhase hint 驱动。
|
|
854
|
+
const assembled = await ctx.systemPrompt.assemble(hint)
|
|
815
855
|
const sections = Array.isArray(assembled.sections)
|
|
816
856
|
? assembled.sections.map((section) => ({ name: section.name, text: String(section.text ?? '') }))
|
|
817
857
|
: []
|
|
@@ -840,7 +880,6 @@ async function buildPreview(ctx, scopeKey, hintId, phase, catalog) {
|
|
|
840
880
|
return {
|
|
841
881
|
ok: true,
|
|
842
882
|
scope: scopeKey,
|
|
843
|
-
degraded,
|
|
844
883
|
registryTotal,
|
|
845
884
|
/** 该 scope 注册表的工具名清单:供界面区分「在本预设里、只是被该阶段裁掉」
|
|
846
885
|
* 与「根本不属于本预设(无法加回)」。 */
|
package/lib/promotion.js
CHANGED
|
@@ -1,80 +1,76 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Epoch-aware
|
|
3
|
-
* compaction-epoch.mjs:晋级由 durable 会话事件推导(默认首个
|
|
4
|
-
* `tool/call` 或 `assistant/message`),compaction 之后复位(首个压缩后
|
|
5
|
-
* 请求回到 bootstrap 态,直到新的晋级信号),subagent 视为恒已晋级。
|
|
2
|
+
* Epoch-aware 晋级(bootstrap / active)阶段推导 + 会话预设解析。
|
|
6
3
|
*
|
|
7
|
-
*
|
|
8
|
-
* /
|
|
4
|
+
* 阶段语义(行为对标 agent-presets 实验预设里的 compaction-epoch.mjs):晋级由
|
|
5
|
+
* durable 会话事件推导(默认首个 `tool/call` 或 `assistant/message`),compaction
|
|
6
|
+
* 之后复位(首个压缩后请求回到 bootstrap 态,直到新的晋级信号),subagent 视为
|
|
7
|
+
* 恒已晋级。resume / reload 后由宿主投影的懒折叠从日志重建出相同的阶段。
|
|
8
|
+
*
|
|
9
|
+
* 读取方式:宿主 0.1.2-alpha.4 移除了 `Session.events`,0.1.6 又把同步历史读
|
|
10
|
+
* (`eventAt` / `snapshotEvents` / `ownEvents`)标记为 deprecated(见
|
|
11
|
+
* .agents/notes/implemented/architecture/2026-09-09-deprecate-synchronous-session-event-reads.md,
|
|
12
|
+
* 长期方向是「投影状态 + 增量维护」)。因此本插件不再自行扫历史:阶段推导注册成
|
|
13
|
+
* 宿主的会话投影 unit(宿主负责折叠与增量驱动),预设解析直接读宿主 agent-presets
|
|
14
|
+
* 注册的 `agentPreset` 投影。投影服务缺席(自组 profile 未挂载 session-projection)
|
|
15
|
+
* 时,读取端回落到「已晋级」——与清单读取的静态视图一致,绝不阻断装配。
|
|
9
16
|
*/
|
|
10
17
|
|
|
18
|
+
import { z } from 'zod'
|
|
19
|
+
|
|
11
20
|
/** 默认的晋级信号事件。 */
|
|
12
21
|
const DEFAULT_PROMOTE_EVENTS = ['tool/call', 'assistant/message']
|
|
13
22
|
|
|
14
|
-
/**
|
|
15
|
-
export
|
|
16
|
-
const promote = new Set(promoteEvents)
|
|
17
|
-
/** sessionId -> { boundary, promoted } */
|
|
18
|
-
const state = new Map()
|
|
23
|
+
/** 本插件注册的阶段投影 key(带命名空间,避免与宿主 / 其它插件撞名)。 */
|
|
24
|
+
export const PHASE_PROJECTION_KEY = 'prompt-customizer/phase'
|
|
19
25
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
let boundary = -1
|
|
23
|
-
let promoted = false
|
|
24
|
-
for (const event of session.events ?? []) {
|
|
25
|
-
const seq = event.seq ?? 0 // 没有 seq 的事件视为边界之后
|
|
26
|
-
if (event.type === 'compaction/end') {
|
|
27
|
-
boundary = seq
|
|
28
|
-
promoted = false
|
|
29
|
-
continue
|
|
30
|
-
}
|
|
31
|
-
if (promote.has(event.type) && seq > boundary) promoted = true
|
|
32
|
-
}
|
|
33
|
-
const entry = { boundary, promoted }
|
|
34
|
-
state.set(session.id, entry)
|
|
35
|
-
return entry
|
|
36
|
-
}
|
|
26
|
+
/** 无从推导时的阶段:视为已晋级(静态视图)。 */
|
|
27
|
+
export const PROMOTED_STATUS = Object.freeze({ boundary: -1, promoted: true })
|
|
37
28
|
|
|
29
|
+
/**
|
|
30
|
+
* 构建阶段投影 unit(`ctx.sessionProjections.register` 的入参)。
|
|
31
|
+
*
|
|
32
|
+
* state 是纯 JSON `{ boundary, promoted }`(投影契约要求);apply 是纯函数,
|
|
33
|
+
* 未命中事件必须返回原引用(宿主以 Object.is 抑制下游工作)。
|
|
34
|
+
* ponytail: 只覆盖 compaction 边界这一种 epoch,未跟踪 fork 语义。
|
|
35
|
+
* 升级路径:参照 anchored-standard 的 fork-aware 边界扩展 apply。
|
|
36
|
+
*/
|
|
37
|
+
export function createPhaseProjection(promoteEvents = DEFAULT_PROMOTE_EVENTS) {
|
|
38
|
+
const promote = new Set(promoteEvents)
|
|
38
39
|
return {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if ((session.header?.delegationDepth ?? 0) > 0) return { boundary: -1, promoted: true }
|
|
48
|
-
return state.get(session.id) ?? scan(session)
|
|
49
|
-
},
|
|
50
|
-
|
|
51
|
-
/** 增量喂入 `session/event`,避免每次装配全量扫描。 */
|
|
52
|
-
observe(session, event) {
|
|
53
|
-
const entry = state.get(session.id)
|
|
54
|
-
if (entry === undefined) return
|
|
55
|
-
const seq = event.seq ?? 0
|
|
56
|
-
if (event.type === 'compaction/end') {
|
|
57
|
-
state.set(session.id, { boundary: seq, promoted: false })
|
|
58
|
-
return
|
|
59
|
-
}
|
|
60
|
-
if (promote.has(event.type) && seq > entry.boundary && !entry.promoted) {
|
|
61
|
-
state.set(session.id, { ...entry, promoted: true })
|
|
40
|
+
key: PHASE_PROJECTION_KEY,
|
|
41
|
+
stateSchema: z.object({ boundary: z.number().int(), promoted: z.boolean() }),
|
|
42
|
+
init: () => ({ boundary: -1, promoted: false }),
|
|
43
|
+
apply: (state, event) => {
|
|
44
|
+
const seq = event.seq ?? 0 // 没有 seq 的事件视为边界之后
|
|
45
|
+
if (event.type === 'compaction/end') return { boundary: seq, promoted: false }
|
|
46
|
+
if (promote.has(event.type) && seq > state.boundary && !state.promoted) {
|
|
47
|
+
return { ...state, promoted: true }
|
|
62
48
|
}
|
|
49
|
+
return state
|
|
63
50
|
},
|
|
51
|
+
stateVersion: 1,
|
|
64
52
|
}
|
|
65
53
|
}
|
|
66
54
|
|
|
67
55
|
/**
|
|
68
|
-
*
|
|
69
|
-
*
|
|
56
|
+
* 当前 agent 的阶段。agent 缺席(纯清单读取等)或子代理一律已晋级;
|
|
57
|
+
* 投影 unit 未注册(服务缺席 / 尚未注册)时同样回落到已晋级。
|
|
70
58
|
*/
|
|
71
|
-
export function
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
preset = event.data.agentPreset
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
return preset
|
|
59
|
+
export function statusOf(projections, agent) {
|
|
60
|
+
const session = agent?.session
|
|
61
|
+
if (!session) return PROMOTED_STATUS
|
|
62
|
+
if ((session.header?.delegationDepth ?? 0) > 0) return PROMOTED_STATUS
|
|
63
|
+
return projections?.stateOf?.(session, PHASE_PROJECTION_KEY) ?? PROMOTED_STATUS
|
|
80
64
|
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 解析一个会话所属的 agent 预设 id:优先宿主 agent-presets 注册的 `agentPreset`
|
|
68
|
+
* 投影(header 记录创建时值,投影随 `agent-preset/selected` 事件推进),
|
|
69
|
+
* 投影缺席(无 roster 的组合)时回落到 header 的创建时值。
|
|
70
|
+
*/
|
|
71
|
+
export function presetOfSession(projections, session) {
|
|
72
|
+
if (!session) return undefined
|
|
73
|
+
const projected = projections?.stateOf?.(session, 'agentPreset')
|
|
74
|
+
if (typeof projected === 'string') return projected
|
|
75
|
+
return typeof session.header?.agentPreset === 'string' ? session.header.agentPreset : undefined
|
|
76
|
+
}
|
package/lib/sectionOps.d.mts
CHANGED
|
@@ -30,3 +30,38 @@ export function phaseInjectEntries<T extends object>(
|
|
|
30
30
|
key: PhaseKey,
|
|
31
31
|
rows: T[],
|
|
32
32
|
): Array<Record<string, unknown>>
|
|
33
|
+
/** 三态同步持久化:rowsByKey 给出三个阶段各自的有序行集合,生成完整 inject
|
|
34
|
+
* 列表(always 条目原样保留,三阶段各自连续虚拟 order)。 */
|
|
35
|
+
export function mergedPhaseInjectEntries<T extends object>(
|
|
36
|
+
cfg: unknown,
|
|
37
|
+
rowsByKey: Record<PhaseKey, T[]>,
|
|
38
|
+
): Array<Record<string, unknown>>
|
|
39
|
+
/** 提示词 Tab 一个阶段部分的行(含屏蔽行与注入行)。 */
|
|
40
|
+
export interface PhaseRow {
|
|
41
|
+
name: string
|
|
42
|
+
text: string
|
|
43
|
+
replaced: boolean
|
|
44
|
+
custom: boolean
|
|
45
|
+
override: string
|
|
46
|
+
blocked: boolean
|
|
47
|
+
}
|
|
48
|
+
/** 一个阶段部分的全部行(post ∪ 被屏蔽 ∪ 本阶段注入条目,草稿序叠加)。
|
|
49
|
+
* `view` = 三阶段预览装配里对应阶段的视图(可为 null = 视为空)。 */
|
|
50
|
+
export function phaseRows(cfg: unknown, view: unknown, key: PhaseKey): PhaseRow[]
|
|
51
|
+
/** 「应用中文」(开关开启)一次性覆盖:三阶段行里名字命中 zhMap 的非自定义
|
|
52
|
+
* 行覆盖为中文译本,生成完整 inject 列表;未命中保持原样。zhMap 条目为字符
|
|
53
|
+
* 串或函数(入参该行原文,返回空值 = 放弃替换)。 */
|
|
54
|
+
export function zhMergedInjectEntries(
|
|
55
|
+
cfg: unknown,
|
|
56
|
+
views: unknown,
|
|
57
|
+
zhMap: Record<string, string | ((original: string) => string | null | undefined)>,
|
|
58
|
+
): Array<Record<string, unknown>>
|
|
59
|
+
/** 「回归英文」(开关关闭):命中译本的非自定义行清空替换文本(服务端回落
|
|
60
|
+
* 注册表英文原文),生成完整 inject 列表;自定义段不碰。 */
|
|
61
|
+
export function zhRevertInjectEntries(
|
|
62
|
+
cfg: unknown,
|
|
63
|
+
views: unknown,
|
|
64
|
+
zhMap: Record<string, string | ((original: string) => string | null | undefined)>,
|
|
65
|
+
): Array<Record<string, unknown>>
|
|
66
|
+
/** 开关状态探测:任一注入条目文本等于字符串译本即视为已应用。 */
|
|
67
|
+
export function zhApplied(cfg: unknown, zhMap: Record<string, string | ((original: string) => string | null | undefined)>): boolean
|