dsh-ppt-studio 1.1.2 → 1.1.4
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 +38 -18
- package/agent-presets/ppt/agent.cordis.yml +22 -15
- package/cordis.patch.yml +19 -14
- package/docs/01-/351/234/200/346/261/202/344/270/216/347/233/256/346/240/207.md +1 -1
- package/docs/02-/346/212/200/346/234/257/346/212/245/345/221/212.md +5 -5
- package/docs/03-/346/233/264/346/226/260/346/227/245/345/277/227.md +105 -0
- package/docs/05-/350/277/255/344/273/243/346/265/201/347/250/213.md +34 -3
- package/docs/06-/350/257/204/345/256/241/344/270/216/346/265/213/350/257/225.md +5 -3
- package/examples/fx/fx-pro/preview/04_pages_04_chart.html +1 -1
- package/examples/fx/fx-pro/preview/deck.html +1 -1
- package/examples/fx/fx-pro/preview/measured.json +1 -1
- package/examples/fx/fx-pro.pptx +0 -0
- package/examples/fx/seed.pptx +0 -0
- package/examples/fx/single.pptx +0 -0
- package/examples/fx/spliced.pptx +0 -0
- package/examples/smoke/imported/deck.yaml +6 -6
- package/examples/smoke/imported/import-styles.json +1 -33
- package/examples/smoke/imported/loss-audit.json +99 -0
- package/examples/smoke/imported/pages/slide_03.yaml +5 -36
- package/examples/smoke/imported/preview-rt/03_pages_slide_03.html +1 -1
- package/examples/smoke/imported/preview-rt/deck.html +1 -1
- package/examples/smoke/imported/preview-rt/layout.json +26 -78
- package/examples/smoke/imported/reference/previews/03.png +0 -0
- package/examples/smoke/imported/source.pptx +0 -0
- package/examples/smoke/out-smoke.pptx +0 -0
- package/examples/smoke/preview/03_pages_03_chart.html +1 -1
- package/examples/smoke/preview/deck.html +1 -1
- package/lib/capabilities.js +189 -0
- package/lib/home.js +32 -0
- package/lib/index.js +278 -112
- package/lib/office-qa.js +111 -0
- package/lib/pptd/export-pptx.js +159 -10
- package/lib/pptd/import-pptx.js +12 -0
- package/lib/pptd/loss-audit.js +363 -0
- package/lib/pptd/nativeChart.js +454 -0
- package/lib/pptd/schema.js +48 -1
- package/lib/pptd/svgCharts.js +180 -37
- package/lib/pptxPy.js +96 -11
- package/lib/preset-delivery.js +107 -9
- package/lib/preview-server.js +7 -5
- package/lib/router.js +3 -2
- package/lib/state.js +8 -6
- package/lib/templates.js +4 -4
- package/lib/tools.js +81 -21
- package/package.json +2 -1
- package/scripts/audit-manual-facts.mjs +4 -2
- package/scripts/e2e-1.0.mjs +2 -2
- package/scripts/fixtures/workflow-baseline.json +10 -10
- package/scripts/install.mjs +13 -11
- package/scripts/smoke.mjs +390 -66
- package/scripts/verify-loss-audit.mjs +245 -0
- package/scripts/verify-native-charts.mjs +529 -0
- package/scripts/verify-preset-scope.mjs +593 -0
- package/skills/ppt-studio-data/SKILL.md +5 -7
- package/skills/ppt-studio-manual/SKILL.md +2 -2
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* verify-preset-scope.mjs —— **预设隔离矩阵**的真机自证(装配层改动的验收门禁,第一轮)。
|
|
4
|
+
*
|
|
5
|
+
* 它回答一个只有真宿主能回答的问题:把本插件的能力面注册到「PPT 工作室」预设的**作用域**里之后,
|
|
6
|
+
* ① 工具可见性、② 技能可见性、③ **系统提示段可见性** 是否都只对 ppt 预设的会话生效,
|
|
7
|
+
* 其它预设(standard)与无预设的全局平面是否**完全看不到**。
|
|
8
|
+
*
|
|
9
|
+
* ── 为什么要"真机 + 隔离"─────────────────────────────────────────────────────────
|
|
10
|
+
* spike(task-1)已经证明:绝对 `file://` 预设行能被挂载、预设不 broken、工具与技能都落预设层、
|
|
11
|
+
* 会话内切预设也拿得到。**但提示段没验证过**——如果把 `system-prompt/assemble` 监听者搬到预设
|
|
12
|
+
* 作用域,它的 waterfall 只按 scope 链投递(`scopeTarget`),是否真的"只对该预设的 agent 生效"
|
|
13
|
+
* 必须实测,不能推断。本脚本用宿主自己的装配入口 `systemPrompt.assemble({agent, scope: agent})`
|
|
14
|
+
* 取 `sections` 做**双向**判定:standard 里没有 `ppt-workflow`,ppt 里**有**。
|
|
15
|
+
*
|
|
16
|
+
* ── 怎么做到"不改仓库、不改产物"──────────────────────────────────────────────────
|
|
17
|
+
* 本脚本只:① 复制**本仓库当前工作区**到 temp、`npm pack`、装进隔离 profile;② 生成一个**探针
|
|
18
|
+
* 插件**(temp 里的独立 .mjs)与一份 **--patch overlay**(`- insert:` 一行,绝对 file:// 名),
|
|
19
|
+
* 由宿主把探针挂到 profile 平面。
|
|
20
|
+
* · 探针负责建真会话(`agents.create` + `presets.mount`,与宿主
|
|
21
|
+
* `dsh-api-session-controller/lib/types/agent.js` 的 `composeAgent()` 同形,GUI 走同一条路)、
|
|
22
|
+
* 写会话状态文件(让提示段判据可判)、枚举可见性、把结果写一个 JSON 文件;
|
|
23
|
+
* · 脚本负责拉起宿主、等结果、断言、杀进程、清理。
|
|
24
|
+
* ⇒ 仓库既有文件(src/lib/docs/smoke)**零改动**;也不依赖任何模型/凭据(不花 token)。
|
|
25
|
+
*
|
|
26
|
+
* ── 用法 ─────────────────────────────────────────────────────────────────────
|
|
27
|
+
* node scripts/verify-preset-scope.mjs # 一键:隔离安装 + 真宿主 + 断言
|
|
28
|
+
* node scripts/verify-preset-scope.mjs --keep # 保留隔离目录(事后翻证据)
|
|
29
|
+
* node scripts/verify-preset-scope.mjs --strict # "未落地"也算失败(改动落地后设为门禁用)
|
|
30
|
+
* node scripts/verify-preset-scope.mjs --negative # 额外跑负面对照(故意写错的 file:// 行)
|
|
31
|
+
* node scripts/verify-preset-scope.mjs --work <dir> # 指定隔离根目录
|
|
32
|
+
* node scripts/verify-preset-scope.mjs --dsh <bin.js> # 指定宿主入口(缺省自动探测)
|
|
33
|
+
* node scripts/verify-preset-scope.mjs --timeout 240 # 等探针结果的秒数(缺省 180)
|
|
34
|
+
* node scripts/verify-preset-scope.mjs --expect-tools 22 --expect-skills 4
|
|
35
|
+
*
|
|
36
|
+
* 退出码:0 = 全部断言通过 **或** 判定为"隔离未生效(改动未落地)"(打印醒目标记,不算 FAIL);
|
|
37
|
+
* 1 = 至少一条断言失败(含"预设 broken"这种真失败);
|
|
38
|
+
* 2 = 仅 --strict 下的"未生效"。缺 pnpm / 缺 dsh / 缺 node 时打印 ⚠ 跳过并退出 0。
|
|
39
|
+
*
|
|
40
|
+
* ── 判定口径(每条断言各自写明"判据来源")────────────────────────────────────────
|
|
41
|
+
* · 工具可见性:`ctx.get('tools').schemas(agent)` —— 即宿主 `systemPrompt.tools(context => wireSchemas(context.scope))`
|
|
42
|
+
* 用的**同一个** `view(scope)`(dsh-tools/lib/index.js:2707 / :3023 / :2959-2985);
|
|
43
|
+
* agent 本身即 scope key(dsh-agent-loop/lib/index.js:778 `createScope(loopCtx, this)`)。
|
|
44
|
+
* · 技能可见性:`ctx.get('skills').list({ scope: agent })`(dsh-skill 按 scope 链合并,:224-240)。
|
|
45
|
+
* · 提示段可见性:`ctx.get('systemPrompt').assemble({ agent, scope: agent })` 的 `sections`——
|
|
46
|
+
* `assembleContextFor(agent) = { agent, scope: agent }`(dsh-agent/lib/types/dispatch.js:92-94,
|
|
47
|
+
* 宿主 agent-loop 在 :907 就是这么调的);waterfall 的投递按 scope(dsh-system-prompt/lib/index.js:355
|
|
48
|
+
* `ctx.waterfall(scopeTarget(this, scope), 'system-prompt/assemble', …)`)。
|
|
49
|
+
* · 预设健康度:`ctx.get('agentPresets').list()` 行里有无 `broken` 字段
|
|
50
|
+
* (dsh-agent-preset-registry/lib/index.js:578-587;前端按 `broken === void 0` 过滤,dsh-client-ui-agent-preset/lib/client.js:1147)。
|
|
51
|
+
* · 全局残留:`ctx.get('tools').schemas(undefined)` = 无预设的全局视图。
|
|
52
|
+
*/
|
|
53
|
+
import { spawn, spawnSync } from 'node:child_process'
|
|
54
|
+
import { cpSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs'
|
|
55
|
+
import { dirname, join, resolve } from 'node:path'
|
|
56
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
57
|
+
import { tmpdir, homedir } from 'node:os'
|
|
58
|
+
|
|
59
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
60
|
+
const args = process.argv.slice(2)
|
|
61
|
+
const flag = (name) => args.includes(name)
|
|
62
|
+
const opt = (name, dflt) => {
|
|
63
|
+
const i = args.indexOf(name)
|
|
64
|
+
return i >= 0 && args[i + 1] !== undefined && !args[i + 1].startsWith('--') ? args[i + 1] : dflt
|
|
65
|
+
}
|
|
66
|
+
const KEEP = flag('--keep')
|
|
67
|
+
const STRICT = flag('--strict')
|
|
68
|
+
const NEGATIVE = flag('--negative')
|
|
69
|
+
const TIMEOUT_MS = Number(opt('--timeout', '180')) * 1000
|
|
70
|
+
const EXPECT_TOOLS = Number(opt('--expect-tools', '22'))
|
|
71
|
+
const EXPECT_SKILLS = Number(opt('--expect-skills', '4'))
|
|
72
|
+
const PRESET = opt('--preset', 'ppt')
|
|
73
|
+
const OTHER = opt('--other', 'standard')
|
|
74
|
+
const WORK_ARG = opt('--work', '')
|
|
75
|
+
|
|
76
|
+
let pass = 0
|
|
77
|
+
let fail = 0
|
|
78
|
+
let pending = false
|
|
79
|
+
function check(label, ok, detail = '') {
|
|
80
|
+
console.log(`${ok ? '✓' : '✗'} ${label}${detail ? ` — ${detail}` : ''}`)
|
|
81
|
+
ok ? pass++ : fail++
|
|
82
|
+
}
|
|
83
|
+
/** 只打印证据行,**不计入**通过/失败(用于"改动未落地"时的判定依据,避免把它误判成门禁失败)。 */
|
|
84
|
+
function evidence(label, ok, detail = '') {
|
|
85
|
+
console.log(`${ok ? '✓' : '✗'}(证据)${label}${detail ? ` — ${detail}` : ''}`)
|
|
86
|
+
}
|
|
87
|
+
function info(label, detail = '') { console.log(`· ${label}${detail ? ` — ${detail}` : ''}`) }
|
|
88
|
+
function head(title) { console.log(`\n=== ${title} ===`) }
|
|
89
|
+
|
|
90
|
+
function run(cmd, cmdArgs, opts = {}) {
|
|
91
|
+
const r = spawnSync(cmd, cmdArgs, { encoding: 'utf8', shell: process.platform === 'win32', ...opts })
|
|
92
|
+
return r
|
|
93
|
+
}
|
|
94
|
+
function mergeOut(r) { return `${r.stdout ?? ''}\n${r.stderr ?? ''}`.trim() }
|
|
95
|
+
function tail(s, n = 240) { return String(s).replace(/\s+/g, ' ').trim().slice(-n) }
|
|
96
|
+
|
|
97
|
+
// ── 前置:node / dsh / pnpm ────────────────────────────────────────────────────
|
|
98
|
+
console.log(`verify-preset-scope:仓库 ${root}`)
|
|
99
|
+
console.log(` 预设 ${PRESET}(期望 ppt_*=${EXPECT_TOOLS}、技能=${EXPECT_SKILLS})|对照预设 ${OTHER}`
|
|
100
|
+
+ `|负面对照 ${NEGATIVE ? '开' : '关'}|未落地判定 ${STRICT ? 'FAIL(--strict)' : 'PENDING(不算失败)'}`)
|
|
101
|
+
|
|
102
|
+
const dshBin = findDshBin()
|
|
103
|
+
if (dshBin === null) {
|
|
104
|
+
console.log('⚠ 跳过:找不到宿主入口 `@deepseek-ai/dsh/lib/bin.js`(用 --dsh <path> 指定,或 npm i -g @deepseek-ai/dsh)')
|
|
105
|
+
process.exit(0)
|
|
106
|
+
}
|
|
107
|
+
const pnpm = run('pnpm', ['--version'])
|
|
108
|
+
if (pnpm.status !== 0) {
|
|
109
|
+
console.log('⚠ 跳过:PATH 上没有 pnpm(`dsh plugin` 是 pnpm 转发器)')
|
|
110
|
+
process.exit(0)
|
|
111
|
+
}
|
|
112
|
+
info('宿主入口', dshBin)
|
|
113
|
+
info('pnpm', mergeOut(pnpm).split(/\r?\n/)[0])
|
|
114
|
+
|
|
115
|
+
// ── 隔离环境 ───────────────────────────────────────────────────────────────────
|
|
116
|
+
const work = WORK_ARG ? resolve(WORK_ARG) : mkdtempSync(join(tmpdir(), 'dsh-ppt-scope-verify-'))
|
|
117
|
+
const home = join(work, 'home')
|
|
118
|
+
const cwd = join(work, 'cwd')
|
|
119
|
+
const packDir = join(work, 'pack')
|
|
120
|
+
for (const d of [home, cwd, packDir]) mkdirSync(d, { recursive: true })
|
|
121
|
+
info('隔离根目录', work)
|
|
122
|
+
info('隔离 DSH_HOME', home)
|
|
123
|
+
|
|
124
|
+
let host = null
|
|
125
|
+
let exitCode = 0
|
|
126
|
+
try {
|
|
127
|
+
// ── 0) 把**本仓库当前工作区**复制到 temp 并在副本里构建(src → lib)───────────────
|
|
128
|
+
// 为什么必须先复制 + 构建:`lib/` 是**提交的构建产物**,而本轮改动在 `src/`。直接 pack 仓库里的
|
|
129
|
+
// 旧 `lib/` 会把"改动未落地"误报成结论。复制到 temp 后跑 `scripts/build.mjs`(纯 src→lib 拷贝)
|
|
130
|
+
// ⇒ 被测对象 = 你此刻的 src,且**仓库零写入**。
|
|
131
|
+
const scratch = join(work, 'repo')
|
|
132
|
+
mkdirSync(scratch, { recursive: true })
|
|
133
|
+
cpSync(root, scratch, {
|
|
134
|
+
recursive: true,
|
|
135
|
+
filter: (src) => !/[\\/](\.git|node_modules)([\\/]|$)/.test(src),
|
|
136
|
+
})
|
|
137
|
+
const build = run('node', [join(scratch, 'scripts', 'build.mjs')], { cwd: scratch })
|
|
138
|
+
check('副本里构建成功(src → lib,测的是你此刻的 src)', build.status === 0, tail(mergeOut(build)))
|
|
139
|
+
|
|
140
|
+
// ── 1) 打包(npm pack 只写 packDir,不写仓库)─────────────────────────────────
|
|
141
|
+
const pack = run('npm', ['pack', '--pack-destination', packDir], { cwd: scratch })
|
|
142
|
+
// 文件名只信 **stdout**(npm 把 notice 写 stderr、文件名写 stdout;合并输出后"最后一行"会是 notice)
|
|
143
|
+
// 再兜一层:直接扫 packDir 里的 tgz。
|
|
144
|
+
const fromStdout = (pack.stdout ?? '').split(/\r?\n/).map((s) => s.trim()).filter((s) => s.endsWith('.tgz')).pop()
|
|
145
|
+
const fromDir = readdirSync(packDir).filter((f) => f.endsWith('.tgz')).sort().pop()
|
|
146
|
+
const tgzName = fromStdout ?? fromDir ?? ''
|
|
147
|
+
const tgz = join(packDir, tgzName)
|
|
148
|
+
check('npm pack 出当前工作区的 tgz', existsSync(tgz), tgzName || tail(mergeOut(pack)))
|
|
149
|
+
|
|
150
|
+
// ── 2) 装进隔离 profile ───────────────────────────────────────────────────────
|
|
151
|
+
const env = { ...process.env, DSH_HOME: home }
|
|
152
|
+
const add = run('node', [dshBin, 'plugin', '--profile', 'web', 'add', tgz], { env, cwd: work })
|
|
153
|
+
check('`dsh plugin --profile web add <tgz>` 退出码 0(隔离 profile)', add.status === 0, tail(mergeOut(add)))
|
|
154
|
+
const installed = join(home, 'profiles', 'web', 'node_modules', 'dsh-ppt-studio')
|
|
155
|
+
check('插件已物化到隔离 profile', existsSync(join(installed, 'lib', 'index.js')), installed)
|
|
156
|
+
|
|
157
|
+
// ── 3) 探针(temp 里的独立模块)+ overlay patch(绝对 file:// 行)──────────────
|
|
158
|
+
const probeFile = join(work, 'probe.mjs')
|
|
159
|
+
writeFileSync(probeFile, probeSource(), 'utf8')
|
|
160
|
+
const patchFile = join(work, 'probe.patch.yml')
|
|
161
|
+
writeFileSync(patchFile, `# verify-preset-scope:把探针挂到 profile 平面(绝对 file:// 名,不经 baseUrl 解析)\n`
|
|
162
|
+
+ `- insert:\n - id: ppt-scope-probe\n name: '${pathToFileURL(probeFile).href}'\n`, 'utf8')
|
|
163
|
+
const resultFile = join(work, 'result.json')
|
|
164
|
+
const hostLog = join(work, 'host.log')
|
|
165
|
+
|
|
166
|
+
// ── 4) 拉起宿主 ───────────────────────────────────────────────────────────────
|
|
167
|
+
console.log('\n拉起隔离宿主(不打开浏览器,端口交给 OS)…')
|
|
168
|
+
host = spawn(process.execPath, [dshBin, '--profile', 'web', '--patch', patchFile, '--no-open', '--port', '0'], {
|
|
169
|
+
cwd, env: {
|
|
170
|
+
...env,
|
|
171
|
+
PPT_SCOPE_VERIFY: '1',
|
|
172
|
+
PPT_SCOPE_RESULT: resultFile,
|
|
173
|
+
PPT_SCOPE_PRESET: PRESET,
|
|
174
|
+
PPT_SCOPE_OTHER: OTHER,
|
|
175
|
+
PPT_SCOPE_NEGATIVE: NEGATIVE ? '1' : '0',
|
|
176
|
+
PPT_SCOPE_CWD: cwd,
|
|
177
|
+
PPT_STUDIO_DEBUG: '1',
|
|
178
|
+
},
|
|
179
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
180
|
+
windowsHide: true,
|
|
181
|
+
})
|
|
182
|
+
let hostOut = ''
|
|
183
|
+
host.stdout.on('data', (b) => { hostOut += String(b) })
|
|
184
|
+
host.stderr.on('data', (b) => { hostOut += String(b) })
|
|
185
|
+
host.on('error', (e) => { hostOut += `\n[spawn error] ${String(e?.message ?? e)}\n` })
|
|
186
|
+
|
|
187
|
+
const result = await waitForResult(resultFile, host, TIMEOUT_MS)
|
|
188
|
+
writeFileSync(hostLog, hostOut, 'utf8')
|
|
189
|
+
if (result !== null) {
|
|
190
|
+
const url = (hostOut.match(/https?:\/\/127\.0\.0\.1:\d+\S*/) ?? ['(未见 URL)'])[0]
|
|
191
|
+
info('宿主已就绪', url)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (result === null) {
|
|
195
|
+
check(`探针在 ${TIMEOUT_MS / 1000}s 内产出结果(${resultFile})`, false,
|
|
196
|
+
host === null || host.exitCode === null ? '宿主仍在跑但探针未落盘' : `宿主已退出 exit=${host.exitCode}`)
|
|
197
|
+
console.log(' —— 宿主输出尾部 ——')
|
|
198
|
+
for (const l of hostOut.split(/\r?\n/).slice(-12)) console.log(` ${l}`)
|
|
199
|
+
} else {
|
|
200
|
+
exitCode = report(result)
|
|
201
|
+
}
|
|
202
|
+
} finally {
|
|
203
|
+
await killHost(host)
|
|
204
|
+
if (KEEP) console.log(`\n(--keep)隔离目录保留在:${work}`)
|
|
205
|
+
else rmSync(work, { recursive: true, force: true })
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const verdict = fail > 0 ? 'FAIL' : (pending ? 'PENDING(隔离未生效—核心改动未落地)' : 'PASS')
|
|
209
|
+
console.log(`\n==== verify-preset-scope 结果:${verdict}:${pass} 通过 / ${fail} 失败 ====`)
|
|
210
|
+
process.exit(fail > 0 ? 1 : exitCode)
|
|
211
|
+
|
|
212
|
+
// ────────────────────────────────────────────────────────────────────────────────
|
|
213
|
+
/** 断言矩阵。返回进程退出码(0 通过 / PENDING;1 失败;STRICT 下 PENDING=2)。 */
|
|
214
|
+
function report(result) {
|
|
215
|
+
const matrix = result.matrix ?? {}
|
|
216
|
+
const presets = matrix.presets ?? []
|
|
217
|
+
const ppt = matrix[PRESET] ?? {}
|
|
218
|
+
const other = matrix[OTHER] ?? {}
|
|
219
|
+
const switched = matrix.switched ?? {}
|
|
220
|
+
const globalView = matrix.global ?? {}
|
|
221
|
+
const rowOf = (id) => presets.find((p) => p.id === id)
|
|
222
|
+
|
|
223
|
+
if (result.fatal) {
|
|
224
|
+
check('探针跑完(无致命错误)', false, tail(result.fatal, 400))
|
|
225
|
+
return 1
|
|
226
|
+
}
|
|
227
|
+
check('探针跑完(无致命错误)', true, `stages=${(result.stages ?? []).join(' → ')}`)
|
|
228
|
+
|
|
229
|
+
// ── A. 预设健康度 ─────────────────────────────────────────────────────────────
|
|
230
|
+
head('A. 预设健康度(agentPresets.list() 有无 broken 字段)')
|
|
231
|
+
const pptRow = rowOf(PRESET)
|
|
232
|
+
check(`presets 列表里存在 ${PRESET} 预设`, pptRow !== undefined,
|
|
233
|
+
presets.map((p) => p.id).join(', '))
|
|
234
|
+
const healthy = pptRow !== undefined && pptRow.broken === undefined
|
|
235
|
+
check(`${PRESET} 预设无 broken 字段(= 选择器可见)`, healthy, pptRow?.broken ?? '(无 broken)')
|
|
236
|
+
|
|
237
|
+
// ── "隔离是否生效"的判定(先判,再决定后面是断言还是 PENDING)────────────────────
|
|
238
|
+
const isolatedTools = (ppt.pptTools ?? []).length === EXPECT_TOOLS && (other.pptTools ?? []).length === 0
|
|
239
|
+
const isolatedSkills = (ppt.pptSkills ?? []).length === EXPECT_SKILLS && (other.pptSkills ?? []).length === 0
|
|
240
|
+
const isolatedPrompt = ppt.hasWorkflowSection === true && other.hasWorkflowSection === false
|
|
241
|
+
const isolated = isolatedTools && isolatedSkills && isolatedPrompt
|
|
242
|
+
|
|
243
|
+
head('B. 隔离判定(三条同时成立才算"改动已落地")')
|
|
244
|
+
// 已落地 ⇒ 这三条是门禁断言(计入通过/失败);未落地 ⇒ 它们只是"为什么判未落地"的证据行。
|
|
245
|
+
const marks = isolated ? check : evidence
|
|
246
|
+
marks(`工具隔离:${PRESET} ppt_*=${(ppt.pptTools ?? []).length}(期望 ${EXPECT_TOOLS})且 ${OTHER} ppt_*=0`,
|
|
247
|
+
isolatedTools, `${PRESET}=${(ppt.pptTools ?? []).length}|${OTHER}=${(other.pptTools ?? []).length}`)
|
|
248
|
+
marks(`技能隔离:${PRESET} 本插件技能=${(ppt.pptSkills ?? []).length}(期望 ${EXPECT_SKILLS})且 ${OTHER}=0`,
|
|
249
|
+
isolatedSkills, `${PRESET}=${(ppt.pptSkills ?? []).length}|${OTHER}=${(other.pptSkills ?? []).length}`)
|
|
250
|
+
marks(`提示段隔离:${PRESET} sections 含 ppt-workflow 且 ${OTHER} 不含`,
|
|
251
|
+
isolatedPrompt, `${PRESET}=${ppt.hasWorkflowSection}|${OTHER}=${other.hasWorkflowSection}`)
|
|
252
|
+
|
|
253
|
+
if (!isolated) {
|
|
254
|
+
if (!healthy) {
|
|
255
|
+
// 预设不健康是**真失败**(不是"未落地"):插件声明了一条注定挂不上的行。
|
|
256
|
+
head('结论:FAIL —— 预设不健康(不是"未落地")')
|
|
257
|
+
check(`${PRESET} 预设必须健康`, false, String(pptRow?.broken ?? '(缺失)'))
|
|
258
|
+
return 1
|
|
259
|
+
}
|
|
260
|
+
head('结论:隔离未生效(核心改动未落地,或走了全局回落)')
|
|
261
|
+
console.log(`⚠ PENDING(隔离未生效—核心改动未落地):${PRESET} 预设健康,但隔离矩阵未成立:`)
|
|
262
|
+
info(`工具:${OTHER} 可见 ppt_*`, String((other.pptTools ?? []).length))
|
|
263
|
+
info(`工具:${PRESET} 可见 ppt_*`, String((ppt.pptTools ?? []).length))
|
|
264
|
+
info('技能', `${OTHER}=${(other.pptSkills ?? []).length}|${PRESET}=${(ppt.pptSkills ?? []).length}`)
|
|
265
|
+
info('提示段', `${OTHER}=${other.hasWorkflowSection}|${PRESET}=${ppt.hasWorkflowSection}`)
|
|
266
|
+
info('全局视图 ppt_*', String((globalView.pptTools ?? []).length))
|
|
267
|
+
// 口径要说清是"哪一条没成立",不要一律归因于"未落地"(避免用推断代替证据)
|
|
268
|
+
if (isolatedPrompt && !isolatedTools) {
|
|
269
|
+
info('归因提示', '提示段已隔离而工具未隔离 ⇒ 可能"部分落地",或 --expect-tools 与插件实际工具数不符(先核对期望值)')
|
|
270
|
+
} else if (!isolatedPrompt && isolatedTools && isolatedSkills) {
|
|
271
|
+
info('归因提示', '工具/技能已隔离而提示段未隔离 ⇒ 提示段与语义路由可能仍注册在 profile 平面(本轮最需要补的就是这条)')
|
|
272
|
+
} else {
|
|
273
|
+
info('归因提示', '三条均未隔离 ⇒ 更像核心改动未落地;若确信已落地,请查宿主日志里的 [ppt-studio] 回落告警')
|
|
274
|
+
}
|
|
275
|
+
console.log('(这不是宿主问题:请确认 src/preset-delivery.js 的自定位行与 src/index.js 的 scope 分档已落地;'
|
|
276
|
+
+ '本脚本测的是 src(副本里现场 build),与仓库提交的 lib/ 是否同步无关)')
|
|
277
|
+
pending = true
|
|
278
|
+
return STRICT ? 2 : 0
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ── C. 非 PPT 预设:零影响(三条口径分开断言)────────────────────────────────────
|
|
282
|
+
head(`C. 非 PPT 预设(${OTHER})零影响`)
|
|
283
|
+
check('工具:ppt_* = 0', (other.pptTools ?? []).length === 0, `可见工具 ${other.toolCount ?? '?'} 个:${(other.tools ?? []).join(',')}`)
|
|
284
|
+
check(`技能:本插件 ${EXPECT_SKILLS} 本 = 0`, (other.pptSkills ?? []).length === 0,
|
|
285
|
+
`可见技能 ${(other.skills ?? []).length} 个${(other.skills ?? []).length ? `:${(other.skills ?? []).join(',')}` : ''}`)
|
|
286
|
+
check('提示段:sections 里没有 ppt-workflow', other.hasWorkflowSection === false,
|
|
287
|
+
`sections = [${(other.sections ?? []).join(', ')}]`)
|
|
288
|
+
|
|
289
|
+
// ── D. PPT 预设:全能力面(含提示段"有"这一向)────────────────────────────────
|
|
290
|
+
head(`D. PPT 预设(${PRESET})全能力面`)
|
|
291
|
+
check(`工具:ppt_* = ${EXPECT_TOOLS}`, (ppt.pptTools ?? []).length === EXPECT_TOOLS, `ppt_* = [${(ppt.pptTools ?? []).join(',')}]`)
|
|
292
|
+
check(`技能:= ${EXPECT_SKILLS}`, (ppt.pptSkills ?? []).length === EXPECT_SKILLS, `[${(ppt.pptSkills ?? []).join(',')}]`)
|
|
293
|
+
check('提示段:sections 里有 ppt-workflow', ppt.hasWorkflowSection === true,
|
|
294
|
+
`sections = [${(ppt.sections ?? []).join(', ')}]`)
|
|
295
|
+
check('模型面目录交叉核对:assembly.tools 里 ppt_* 数 = tools.schemas 里 ppt_* 数',
|
|
296
|
+
(ppt.assemblyPptTools ?? []).length === (ppt.pptTools ?? []).length,
|
|
297
|
+
`assembly=${(ppt.assemblyPptTools ?? []).length}|registry=${(ppt.pptTools ?? []).length}`)
|
|
298
|
+
|
|
299
|
+
// ── E. 会话内切换 ─────────────────────────────────────────────────────────────
|
|
300
|
+
head('E. 会话内切换(standard → ppt,同一 agent)')
|
|
301
|
+
check('切换成功且 composedPreset 变成 ppt', switched.composedPreset === PRESET, String(switched.composedPreset ?? '(未取到)'))
|
|
302
|
+
check(`切换后拿到 ppt_* = ${EXPECT_TOOLS}`, (switched.pptTools ?? []).length === EXPECT_TOOLS, `= ${(switched.pptTools ?? []).length}`)
|
|
303
|
+
check(`切换后拿到技能 = ${EXPECT_SKILLS}`, (switched.pptSkills ?? []).length === EXPECT_SKILLS, `= ${(switched.pptSkills ?? []).length}`)
|
|
304
|
+
check('切换后提示段也出现 ppt-workflow(提示段判据同样跟随 scope 链)', switched.hasWorkflowSection === true,
|
|
305
|
+
`sections = [${(switched.sections ?? []).join(', ')}]`)
|
|
306
|
+
|
|
307
|
+
// ── F. 零全局残留 ─────────────────────────────────────────────────────────────
|
|
308
|
+
head('F. 零全局残留(无预设的全局视图)')
|
|
309
|
+
check('全局视图 ppt_* = 0', (globalView.pptTools ?? []).length === 0,
|
|
310
|
+
`全局工具 ${globalView.toolCount ?? '?'} 个,ppt_* = [${(globalView.pptTools ?? []).join(',')}]`)
|
|
311
|
+
|
|
312
|
+
// ── G. 负面对照(可选)────────────────────────────────────────────────────────
|
|
313
|
+
if (NEGATIVE) {
|
|
314
|
+
head('G. 负面对照:故意写错的 file:// 行')
|
|
315
|
+
const neg = matrix.negative ?? {}
|
|
316
|
+
check('宿主报告 broken(诊断含 "never started")', typeof neg.brokenRow === 'string' && /never started/.test(neg.brokenRow),
|
|
317
|
+
tail(String(neg.brokenRow ?? '(未取到)'), 300))
|
|
318
|
+
check('坏预设不影响好预设(ppt 仍健康)', neg.pptStillHealthy !== false, String(neg.pptStillHealthy))
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// ── 判据来源(口径标注,避免"用推断代替")──────────────────────────────────────
|
|
322
|
+
head('判据来源(每个口径的 API 与宿主代码)')
|
|
323
|
+
info('工具可见性', "ctx.get('tools').schemas(agent) ← systemPrompt.tools(ctx => wireSchemas(ctx.scope)),dsh-tools/lib/index.js:2707/:3023/:2959-2985")
|
|
324
|
+
info('技能可见性', "ctx.get('skills').list({ scope: agent }),dsh-skill/lib/index.js:224-240(按 scope 链合并,:111-117)")
|
|
325
|
+
info('提示段可见性', "ctx.get('systemPrompt').assemble({ agent, scope: agent }) 的 sections ← dsh-agent/lib/types/dispatch.js:92-94 + dsh-agent-loop/lib/index.js:907;waterfall 按 scope 投递 dsh-system-prompt/lib/index.js:355")
|
|
326
|
+
info('预设健康度', 'ctx.get(\'agentPresets\').list() 的 broken 字段,dsh-agent-preset-registry/lib/index.js:578-587')
|
|
327
|
+
info('全局残留', "ctx.get('tools').schemas(undefined)(无预设的全局视图)")
|
|
328
|
+
info('跨平面注记', '工具/技能三条断言读的是"注册表层"(模型目录同一来源);提示段读的是"装配入口返回的 sections"。三者都不是推断。')
|
|
329
|
+
return 0
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** 轮询结果文件(探针原子写:先 .tmp 再 rename)。 */
|
|
333
|
+
async function waitForResult(file, child, timeoutMs) {
|
|
334
|
+
const deadline = Date.now() + timeoutMs
|
|
335
|
+
while (Date.now() < deadline) {
|
|
336
|
+
if (existsSync(file)) {
|
|
337
|
+
try { return JSON.parse(readFileSync(file, 'utf8')) } catch { /* 半成品,继续等 */ }
|
|
338
|
+
}
|
|
339
|
+
if (child !== null && child.exitCode !== null) {
|
|
340
|
+
// 宿主退出前可能刚写完,再给一次机会
|
|
341
|
+
await sleep(300)
|
|
342
|
+
if (existsSync(file)) { try { return JSON.parse(readFileSync(file, 'utf8')) } catch { /* 忽略 */ } }
|
|
343
|
+
return null
|
|
344
|
+
}
|
|
345
|
+
await sleep(400)
|
|
346
|
+
}
|
|
347
|
+
return null
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function sleep(ms) { return new Promise((r) => setTimeout(r, ms)) }
|
|
351
|
+
|
|
352
|
+
/** 结束宿主(Windows 下要连子进程一起杀)。 */
|
|
353
|
+
async function killHost(child) {
|
|
354
|
+
if (child === null || child.exitCode !== null) return
|
|
355
|
+
try {
|
|
356
|
+
if (process.platform === 'win32') run('taskkill', ['/PID', String(child.pid), '/T', '/F'])
|
|
357
|
+
else child.kill('SIGKILL')
|
|
358
|
+
} catch { /* 忽略 */ }
|
|
359
|
+
await sleep(500)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** 探测宿主入口:$DSH_BIN → `npm root -g` → APPDATA/LOCALAPPDATA → homedir。 */
|
|
363
|
+
function findDshBin() {
|
|
364
|
+
const rel = join('@deepseek-ai', 'dsh', 'lib', 'bin.js')
|
|
365
|
+
const candidates = []
|
|
366
|
+
if (process.env.DSH_BIN) candidates.push(process.env.DSH_BIN)
|
|
367
|
+
try {
|
|
368
|
+
const r = run('npm', ['root', '-g'])
|
|
369
|
+
if (r.status === 0) candidates.push(join(mergeOut(r), rel))
|
|
370
|
+
} catch { /* npm 不可用 */ }
|
|
371
|
+
for (const k of ['APPDATA', 'LOCALAPPDATA']) if (process.env[k]) candidates.push(join(process.env[k], 'npm', 'node_modules', rel))
|
|
372
|
+
candidates.push(join(homedir(), 'AppData', 'Roaming', 'npm', 'node_modules', rel))
|
|
373
|
+
candidates.push('/usr/lib/node_modules/@deepseek-ai/dsh/lib/bin.js', '/usr/local/lib/node_modules/@deepseek-ai/dsh/lib/bin.js')
|
|
374
|
+
for (const c of candidates) if (c && existsSync(c)) return c
|
|
375
|
+
return null
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// ────────────────────────────────────────────────────────────────────────────────
|
|
379
|
+
/**
|
|
380
|
+
* 探针插件源码(写到 temp 的 probe.mjs,由 --patch overlay 以绝对 file:// 名挂载)。
|
|
381
|
+
* 它**只读**宿主状态 + 建会话 + 枚举,不改仓库、不改插件。
|
|
382
|
+
*
|
|
383
|
+
* 写成**函数声明**而不是模块级 const:下面的顶层代码在模块求值到 `const` 之前就会调用它
|
|
384
|
+
* (TDZ 会抛 "Cannot access 'PROBE_SOURCE' before initialization"——仓库自己的
|
|
385
|
+
* scripts/verify-fresh-install.mjs 顶部注释记过同一个坑)。
|
|
386
|
+
* @returns {string} 探针模块源码
|
|
387
|
+
*/
|
|
388
|
+
function probeSource() {
|
|
389
|
+
return String.raw`
|
|
390
|
+
import { appendFileSync, existsSync, mkdirSync, renameSync, writeFileSync } from 'node:fs'
|
|
391
|
+
import { join } from 'node:path'
|
|
392
|
+
|
|
393
|
+
const OUT = process.env.PPT_SCOPE_RESULT
|
|
394
|
+
const LOG = String(OUT) + '.log'
|
|
395
|
+
const PRESET = process.env.PPT_SCOPE_PRESET || 'ppt'
|
|
396
|
+
const OTHER = process.env.PPT_SCOPE_OTHER || 'standard'
|
|
397
|
+
const NEG = process.env.PPT_SCOPE_NEGATIVE === '1'
|
|
398
|
+
const CWD = process.env.PPT_SCOPE_CWD || process.cwd()
|
|
399
|
+
const HOME = process.env.DSH_HOME
|
|
400
|
+
|
|
401
|
+
const RESULT = { startedAt: new Date().toISOString(), stages: [], errors: [], matrix: {} }
|
|
402
|
+
const log = (m) => { try { appendFileSync(LOG, new Date().toISOString() + ' ' + m + '\n') } catch {} }
|
|
403
|
+
const flush = () => {
|
|
404
|
+
try { writeFileSync(OUT + '.tmp', JSON.stringify(RESULT, null, 2)); renameSync(OUT + '.tmp', OUT) } catch {}
|
|
405
|
+
}
|
|
406
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
407
|
+
async function waitFor(ctx, name, ms = 60000) {
|
|
408
|
+
const end = Date.now() + ms
|
|
409
|
+
while (Date.now() < end) {
|
|
410
|
+
try { const s = ctx.get(name); if (s !== undefined) return s } catch { /* 继续等 */ }
|
|
411
|
+
await sleep(300)
|
|
412
|
+
}
|
|
413
|
+
return undefined
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export const name = 'ppt-scope-probe'
|
|
417
|
+
|
|
418
|
+
export function apply(ctx) {
|
|
419
|
+
if (process.env.PPT_SCOPE_VERIFY !== '1') return
|
|
420
|
+
main(ctx).catch((e) => { RESULT.fatal = String(e?.stack ?? e); flush(); log('FATAL ' + e) })
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
async function main(ctx) {
|
|
424
|
+
const systemPrompt = await waitFor(ctx, 'systemPrompt')
|
|
425
|
+
const tools = await waitFor(ctx, 'tools')
|
|
426
|
+
const agents = await waitFor(ctx, 'agents')
|
|
427
|
+
const presets = await waitFor(ctx, 'agentPresets')
|
|
428
|
+
const skills = ctx.get('skills')
|
|
429
|
+
if (!systemPrompt || !tools || !agents || !presets) {
|
|
430
|
+
RESULT.fatal = '必需服务缺失:' + JSON.stringify({ systemPrompt: !!systemPrompt, tools: !!tools, agents: !!agents, presets: !!presets })
|
|
431
|
+
flush(); return
|
|
432
|
+
}
|
|
433
|
+
RESULT.stages.push('services')
|
|
434
|
+
log('服务就绪')
|
|
435
|
+
|
|
436
|
+
// 等到本插件把预设声明落定(list() 会跑注册表的 diagnostic,broken 判据就在这里)
|
|
437
|
+
let list = []
|
|
438
|
+
for (let i = 0; i < 100; i++) {
|
|
439
|
+
list = await presets.list().catch((e) => [{ id: '(list 抛错)', broken: String(e?.message ?? e) }])
|
|
440
|
+
if (list.some((p) => p.id === PRESET)) break
|
|
441
|
+
await sleep(300)
|
|
442
|
+
}
|
|
443
|
+
RESULT.matrix.presets = list.map((p) => ({ id: p.id, name: p.name, order: p.order, broken: p.broken }))
|
|
444
|
+
RESULT.stages.push('preset-declared')
|
|
445
|
+
log('presets=' + JSON.stringify(list.map((p) => p.id + (p.broken ? ':broken' : ''))))
|
|
446
|
+
|
|
447
|
+
const presetBroken = (list.find((p) => p.id === PRESET) || {}).broken
|
|
448
|
+
if (presetBroken !== undefined) {
|
|
449
|
+
// 预设不健康:仍然把健康度证据写出来,让脚本判 FAIL(不继续建会话)
|
|
450
|
+
RESULT.stages.push('preset-broken')
|
|
451
|
+
flush(); return
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
let agentOptions
|
|
455
|
+
try {
|
|
456
|
+
const sel = ctx.get('agentDefaultModel')?.currentSelection()
|
|
457
|
+
if (sel?.provider && sel?.model) agentOptions = { provider: sel.provider, model: sel.model }
|
|
458
|
+
} catch { /* 缺默认模型不影响建会话 */ }
|
|
459
|
+
|
|
460
|
+
async function makeAgent(presetId, sessionId) {
|
|
461
|
+
// 先写会话状态(提示段判据需要 workflowActive=true):字段照 lib/state.js 的 DEFAULT_SESSION
|
|
462
|
+
mkdirSync(join(HOME, 'ppt-studio'), { recursive: true })
|
|
463
|
+
writeFileSync(join(HOME, 'ppt-studio', 'session-' + String(sessionId).replace(/[^\w-]+/g, '_') + '.json'),
|
|
464
|
+
JSON.stringify({
|
|
465
|
+
routing: 'on', mode: 'auto', fidelity: 'auto', review: 'points', quality: 'standard',
|
|
466
|
+
engine: 'auto', quick: false, template: null, pauseAfter: [],
|
|
467
|
+
workflowActive: true, taskType: 'from-scratch', deckDir: null,
|
|
468
|
+
}, null, 2), 'utf8')
|
|
469
|
+
const resolved = await presets.resolve(presetId)
|
|
470
|
+
if (resolved.broken !== undefined) throw new Error('预设 ' + presetId + ' 不健康:' + resolved.broken)
|
|
471
|
+
const handle = await agents.create({
|
|
472
|
+
sessionId,
|
|
473
|
+
...(agentOptions === undefined ? {} : { agentOptions }),
|
|
474
|
+
meta: { cwd: CWD, agentPreset: resolved.id },
|
|
475
|
+
setup: async (agentCtx) => { await presets.mount(agentCtx, resolved.id) },
|
|
476
|
+
})
|
|
477
|
+
return handle.agent
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
async function measure(agent) {
|
|
481
|
+
const out = { sessionId: agent.id, composedPreset: null, tools: [], toolCount: 0, pptTools: [],
|
|
482
|
+
skills: [], pptSkills: [], sections: [], hasWorkflowSection: false, assemblyPptTools: [], errors: [] }
|
|
483
|
+
try { out.composedPreset = presets.composedPreset(agent.ctx) ?? null } catch (e) { out.errors.push('composedPreset: ' + e.message) }
|
|
484
|
+
try {
|
|
485
|
+
const names = tools.schemas(agent).map((s) => s.name).sort()
|
|
486
|
+
out.tools = names; out.toolCount = names.length; out.pptTools = names.filter((n) => n.startsWith('ppt_'))
|
|
487
|
+
} catch (e) { out.errors.push('schemas: ' + String(e?.message ?? e)) }
|
|
488
|
+
try {
|
|
489
|
+
if (skills === undefined) out.errors.push('skills 服务缺失')
|
|
490
|
+
else {
|
|
491
|
+
const rows = await skills.list({ scope: agent })
|
|
492
|
+
out.skills = rows.map((s) => s.name).sort()
|
|
493
|
+
out.pptSkills = out.skills.filter((n) => n.startsWith('ppt-studio-'))
|
|
494
|
+
}
|
|
495
|
+
} catch (e) { out.errors.push('skills.list: ' + String(e?.message ?? e)) }
|
|
496
|
+
try {
|
|
497
|
+
// 与宿主 agent-loop 同一入口:assembleContextFor(agent) = { agent, scope: agent }
|
|
498
|
+
const assembly = await systemPrompt.assemble({ agent, scope: agent })
|
|
499
|
+
out.sections = (assembly.sections ?? []).map((s) => s.name)
|
|
500
|
+
out.hasWorkflowSection = out.sections.includes('ppt-workflow')
|
|
501
|
+
out.assemblyPptTools = (assembly.tools ?? []).map((t) => t.name).filter((n) => n.startsWith('ppt_')).sort()
|
|
502
|
+
} catch (e) { out.errors.push('assemble: ' + String(e?.message ?? e)) }
|
|
503
|
+
return out
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// 对照预设(standard)
|
|
507
|
+
try {
|
|
508
|
+
const a = await makeAgent(OTHER, 'scope-verify-' + OTHER)
|
|
509
|
+
RESULT.matrix[OTHER] = await measure(a)
|
|
510
|
+
RESULT.stages.push('agent:' + OTHER)
|
|
511
|
+
log(OTHER + ' tools=' + RESULT.matrix[OTHER].toolCount + ' ppt=' + RESULT.matrix[OTHER].pptTools.length
|
|
512
|
+
+ ' skills=' + RESULT.matrix[OTHER].pptSkills.length + ' workflowSection=' + RESULT.matrix[OTHER].hasWorkflowSection)
|
|
513
|
+
} catch (e) { RESULT.errors.push(OTHER + ': ' + String(e?.message ?? e)); log('ERR ' + OTHER + ' ' + e) }
|
|
514
|
+
|
|
515
|
+
// ppt 预设
|
|
516
|
+
let pptAgent
|
|
517
|
+
try {
|
|
518
|
+
pptAgent = await makeAgent(PRESET, 'scope-verify-' + PRESET)
|
|
519
|
+
RESULT.matrix[PRESET] = await measure(pptAgent)
|
|
520
|
+
RESULT.stages.push('agent:' + PRESET)
|
|
521
|
+
log(PRESET + ' tools=' + RESULT.matrix[PRESET].toolCount + ' ppt=' + RESULT.matrix[PRESET].pptTools.length
|
|
522
|
+
+ ' skills=' + RESULT.matrix[PRESET].pptSkills.length + ' workflowSection=' + RESULT.matrix[PRESET].hasWorkflowSection)
|
|
523
|
+
} catch (e) { RESULT.errors.push(PRESET + ': ' + String(e?.message ?? e)); log('ERR ' + PRESET + ' ' + e) }
|
|
524
|
+
|
|
525
|
+
// 会话内切换:在对照预设的 agent 上调注册表自己的 select(= agent-presets.swap 的源头函数)
|
|
526
|
+
try {
|
|
527
|
+
if (RESULT.matrix[OTHER] !== undefined) {
|
|
528
|
+
const again = await makeAgentAfterSwitch(presets, agents, agentOptions)
|
|
529
|
+
RESULT.matrix.switched = again
|
|
530
|
+
RESULT.stages.push('switched')
|
|
531
|
+
log('switched composedPreset=' + again.composedPreset + ' ppt=' + again.pptTools.length
|
|
532
|
+
+ ' skills=' + again.pptSkills.length + ' workflowSection=' + again.hasWorkflowSection)
|
|
533
|
+
}
|
|
534
|
+
} catch (e) { RESULT.errors.push('switch: ' + String(e?.message ?? e)); log('ERR switch ' + e) }
|
|
535
|
+
|
|
536
|
+
async function makeAgentAfterSwitch(presets2, agents2, agentOptions2) {
|
|
537
|
+
// 重新走一遍"先 standard 再切"的完整路径,避免复用已被测过的 agent 造成歧义
|
|
538
|
+
const sid = 'scope-verify-switch'
|
|
539
|
+
mkdirSync(join(HOME, 'ppt-studio'), { recursive: true })
|
|
540
|
+
writeFileSync(join(HOME, 'ppt-studio', 'session-' + sid + '.json'),
|
|
541
|
+
JSON.stringify({
|
|
542
|
+
routing: 'on', mode: 'auto', fidelity: 'auto', review: 'points', quality: 'standard',
|
|
543
|
+
engine: 'auto', quick: false, template: null, pauseAfter: [],
|
|
544
|
+
workflowActive: true, taskType: 'from-scratch', deckDir: null,
|
|
545
|
+
}, null, 2), 'utf8')
|
|
546
|
+
const first = await presets2.resolve(OTHER)
|
|
547
|
+
const handle = await agents2.create({
|
|
548
|
+
sessionId: sid,
|
|
549
|
+
...(agentOptions2 === undefined ? {} : { agentOptions: agentOptions2 }),
|
|
550
|
+
meta: { cwd: CWD, agentPreset: first.id },
|
|
551
|
+
setup: async (agentCtx) => { await presets2.mount(agentCtx, first.id) },
|
|
552
|
+
})
|
|
553
|
+
const agent = handle.agent
|
|
554
|
+
await presets2.select(agent, PRESET)
|
|
555
|
+
await sleep(500)
|
|
556
|
+
return measure(agent)
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// 全局视图(无预设)
|
|
560
|
+
try {
|
|
561
|
+
const names = tools.schemas(undefined).map((s) => s.name).sort()
|
|
562
|
+
RESULT.matrix.global = { toolCount: names.length, pptTools: names.filter((n) => n.startsWith('ppt_')) }
|
|
563
|
+
RESULT.stages.push('global-view')
|
|
564
|
+
log('global tools=' + names.length + ' ppt=' + RESULT.matrix.global.pptTools.length)
|
|
565
|
+
} catch (e) { RESULT.errors.push('global: ' + String(e?.message ?? e)) }
|
|
566
|
+
|
|
567
|
+
// 负面对照:自己注册一条故意写错的 file:// 行,看宿主的失败形状
|
|
568
|
+
if (NEG) {
|
|
569
|
+
try {
|
|
570
|
+
const bogus = new URL('nope-missing.js', import.meta.url).href
|
|
571
|
+
const unregister = await presets.register({
|
|
572
|
+
id: 'scope-verify-broken', name: 'scope verify broken (预期 broken)',
|
|
573
|
+
plugins: [{ id: 'bogus-row', name: bogus }],
|
|
574
|
+
})
|
|
575
|
+
const after = await presets.list()
|
|
576
|
+
const row = after.find((p) => p.id === 'scope-verify-broken') || {}
|
|
577
|
+
RESULT.matrix.negative = {
|
|
578
|
+
bogusName: bogus,
|
|
579
|
+
brokenRow: row.broken,
|
|
580
|
+
pptStillHealthy: (after.find((p) => p.id === PRESET) || {}).broken === undefined,
|
|
581
|
+
}
|
|
582
|
+
RESULT.stages.push('negative')
|
|
583
|
+
log('negative broken=' + JSON.stringify(row.broken))
|
|
584
|
+
await unregister()
|
|
585
|
+
} catch (e) { RESULT.errors.push('negative: ' + String(e?.message ?? e)) }
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
RESULT.finishedAt = new Date().toISOString()
|
|
589
|
+
flush()
|
|
590
|
+
log('DONE')
|
|
591
|
+
}
|
|
592
|
+
`
|
|
593
|
+
}
|
|
@@ -22,16 +22,14 @@ whenToUse: 正在给页面安排图表、表格、关键数字;或用户反馈
|
|
|
22
22
|
| **图表作装饰 / 示意 / 版式衬底**(不承载结论) | **不必** | 让它纯几何即可;**别让它假装承载数据**(要么标注"示意",要么干脆用形状) |
|
|
23
23
|
| 介于两者之间 | 按"读者是否需要读出值"判 | 拿不准就至少给单位与一句"读图结论"——结论句写在**图外**(§2 路径 A),不是往图里塞标签 |
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
**成品层(交付物,2026-09-26 第三轮起)**:`chart` 元素**默认产出原生可编辑图表**——包里是 `ppt/charts/chartN.xml` + **内嵌数据工作簿** `ppt/embeddings/*.xlsx`:PowerPoint 里能"**编辑数据**"、能换图表类型,坐标轴/网格线/图例/数据标签**由图表自己算**(分类名与数值都在图表里,不再需要你手写文本框)。
|
|
26
|
+
`chart.render: vector` 才回到旧行为(矢量拼绘:只画柱/折线/饼形,**没有**轴/图例/标签,标签全靠你补)。`chart.legend / labels / axes` 是可选开关:默认多系列或饼图显示图例、饼图显示百分比标签、柱/线显示数值标签。
|
|
27
27
|
|
|
28
|
-
**预览层(`ppt_render` 的 HTML
|
|
29
|
-
**pie 不画分类名,却照样画那套 y 轴刻度**(对饼图毫无意义);**每个数据点的具体值只在悬停提示里**。
|
|
30
|
-
所以"预览里看着能懂"对饼图根本不成立,对 bar/line 也仅是分类名与相对刻度可读。
|
|
28
|
+
**预览层(`ppt_render` 的 HTML)与成品同源**:`svgCharts.js` 与导出共用同一个图表模型,预览里能看到的轴/刻度/网格/图例/数据标签与成品一致(旧版"预览画了刻度、成品却没有"的错位已消除)。分类名超过 8 字截断(预览与成品同一处理)。
|
|
31
29
|
|
|
32
30
|
→ **预览是定版参考,交付物是 pptx**:只按预览确认"图表能看懂",交付出去可能就是一张没有标注的图形。
|
|
33
31
|
|
|
34
|
-
→ **承载数据的图表** =
|
|
32
|
+
→ **承载数据的图表** = 图表元素 + 你补的**单位与一句结论句**(native 默认已带分类名/数值/图例;`render: vector` 才需要自己补标签)。
|
|
35
33
|
→ **装饰性图表** 可以不带标签,但交付说明里加一句"该图仅作示意"(本手册的补充建议,不在铁律 7 的必含项里),并且不要用它去支撑页面结论。
|
|
36
34
|
|
|
37
35
|
## 1. 选型:先问"读者要读到什么"
|
|
@@ -74,7 +72,7 @@ whenToUse: 正在给页面安排图表、表格、关键数字;或用户反馈
|
|
|
74
72
|
|
|
75
73
|
| 做法 | 怎么写 | 代价 |
|
|
76
74
|
|---|---|---|
|
|
77
|
-
| **A.
|
|
75
|
+
| **A. 图内标签 / 图外标签**(默认) | native(默认):`chart.labels: true` 把数值画在图上、图例自动生成、轴与网格自带;vector:图表 bounds 缩小,分类名/数值/图例排在图框外空白区 | native 不需预留空间;vector 需要(图表别占满整页) |
|
|
78
76
|
| **B. 图当衬底**(仅衬底类图) | 给 chart 加 `role: background`,再把这组重叠写进该页 `expectedOverlaps` | 声明的是"这张图是承载文字的底"——语义变了,且**文字压在图形上仍然难读**,声明不会替你解决可读性 |
|
|
79
77
|
| **C. 图纯装饰**(仅纯装饰图) | 给 chart 加 `role: decoration`,**完全退出重叠报告**(不用写声明) | `decoration` 的语义是"此元素不承载信息";只豁免**重叠**,不豁免超页面/超安全区/文本溢出 |
|
|
80
78
|
|
|
@@ -34,7 +34,7 @@ DSH 上做 PPT 的工作区:说需求 → 四类任务工作流 → 「数字
|
|
|
34
34
|
| "字号可以 11pt 吗" | **可以**(下限 = 用户指令):用户给了下限(如"不得小于14号")→ 写入 theme.minFontSize 并严格遵守;用户没给 → 不设任何强制下限(插件无默认)。 |
|
|
35
35
|
| "为什么导出会缩字" | verify 已报溢出 → 先清零(扩容器/精简文案/显式 `\n`);verify 通过 ⇒ 导出不缩字 |
|
|
36
36
|
| "预览链接 404" | 路由在 PPT 工作室会话挂载时注册——确认当前是 PPT 工作室会话(不是默认会话) |
|
|
37
|
-
| "图表能画什么" | bar/line/pie
|
|
37
|
+
| "图表能画什么" | bar/line/pie,默认**原生可编辑图表**(自带轴/网格/图例/数据标签,可"编辑数据",数据在内嵌工作簿);`chart.render: vector` 回到矢量拼绘(标签自己补) |
|
|
38
38
|
| "用别人的模板做" | `ppt_import`(带参考层:source.pptx + 真渲染整页 + 全量色板)→ 先 read_image 看 `reference/previews/*.png` 真身再动手 |
|
|
39
39
|
| "要 100% 像模板" | `ppt_patch`(手术模式:只换文字/表格内容,XML 原样) |
|
|
40
40
|
| "网页预览对,打开 pptx 线条不对" | 旧引擎三个连线编码 bug(四个症状:斜线镜像 / × 少一笔 / 箭头消失 / 水平线变斜),**均已修复**——看 `ppt_export` 报告的"线方向 N/N"自证;修复前导出的产物重新 `ppt_export` |
|
|
@@ -97,7 +97,7 @@ DSH 上做 PPT 的工作区:说需求 → 四类任务工作流 → 「数字
|
|
|
97
97
|
## 7. 开发维护(仅改插件时用)
|
|
98
98
|
|
|
99
99
|
- 改码:`src/` → `node scripts/build.mjs` → **重启 host**(插件经 agent preset 会话装配;`dev_reload_package` 只覆盖注入器装配包,且注入器 junction 已存在时会指向旧安装根)。
|
|
100
|
-
- 回归:`node scripts/smoke.mjs`(
|
|
100
|
+
- 回归:`node scripts/smoke.mjs`(303 断言)→ `node scripts/preflight-1.0.mjs`(发布预检)→ `node scripts/regression-real.mjs`。
|
|
101
101
|
- 跨层验证纪律:**预览层与成品层必须互相验证**——`ppt_render`+`ppt_verify` 只管 HTML/估算层,OOXML 层靠 `ppt_export` 的 parity 自证(表/图/线方向)+ `ppt_visual` 真渲染抽检;只跑单层会漏掉"预览对、成品错"(2026-09-14 连线方向事故)。
|
|
102
102
|
- 文档链:改需求/决策 → docs/01;改机制 → docs/02;每次 → docs/03;验收 → docs/04;发布前 → docs/06。
|
|
103
103
|
- 装配:**默认装法是 npm 包名**(`dsh plugin --profile <p> add dsh-ppt-studio`,profile 级、随 profile 启动装配;**桌面端**在插件管理里填同一个包名 `dsh-ppt-studio`;GitHub Release 的资产 URL 与 `scripts/install*.mjs` 是离线/归档备选)。装完**必须重启 host**。排查:`dsh plugin --profile web list` → `dsh --profile web --dump-config` 找 `ppt-studio` 行。preset 插件行只在"离线 junction"场景用,与 bundle 装法**互斥**。
|