@capyclaw/ppt-workbench 0.0.0-stage → 2.4.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.
Files changed (130) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +126 -3
  3. package/THIRD_PARTY_NOTICES.md +21 -0
  4. package/contract/ppt-contract.json +1529 -0
  5. package/cordis.patch.yml +25 -0
  6. package/core/audit.js +500 -0
  7. package/core/chart.js +199 -0
  8. package/core/color.js +80 -0
  9. package/core/deck-layout.js +195 -0
  10. package/core/formula.js +80 -0
  11. package/core/hfmath/hershey.js +1763 -0
  12. package/core/hfmath/index.js +1176 -0
  13. package/core/hfmath/stretch.js +41 -0
  14. package/core/hfmath/symbols.js +408 -0
  15. package/core/ops/browser.js +28 -0
  16. package/core/ops/chart.js +64 -0
  17. package/core/ops/deck.js +21 -0
  18. package/core/ops/dispatcher.js +2 -0
  19. package/core/ops/element.js +55 -0
  20. package/core/ops/history.js +11 -0
  21. package/core/ops/import.js +95 -0
  22. package/core/ops/index.js +39 -0
  23. package/core/ops/playback.js +9 -0
  24. package/core/ops/recolor.js +33 -0
  25. package/core/ops/slide.js +66 -0
  26. package/core/ops/table.js +85 -0
  27. package/core/ops/text.js +35 -0
  28. package/core/ops/view.js +13 -0
  29. package/core/ops.js +12 -0
  30. package/core/quick.js +52 -0
  31. package/core/shape-library.js +122 -0
  32. package/core/table.js +178 -0
  33. package/core/text.js +200 -0
  34. package/editor/app.js +5366 -0
  35. package/editor/heartbeat-worker.js +116 -0
  36. package/editor/index.html +89 -0
  37. package/editor/ppt-full-app.js +376 -0
  38. package/lib/host.mjs +98 -0
  39. package/package.json +53 -6
  40. package/server/audit.mjs +2 -0
  41. package/server/bridge-client.mjs +174 -0
  42. package/server/constants.mjs +20 -0
  43. package/server/contract-snapshots.mjs +62 -0
  44. package/server/contract.mjs +24 -0
  45. package/server/deck-layout.mjs +2 -0
  46. package/server/http-utils.mjs +2 -0
  47. package/server/infra/http.mjs +113 -0
  48. package/server/infra/runtime/context.mjs +128 -0
  49. package/server/infra/store/blob-store.mjs +109 -0
  50. package/server/infra/tuning.mjs +82 -0
  51. package/server/infra/util/fs-lock.mjs +132 -0
  52. package/server/infra/workspace/manager.mjs +657 -0
  53. package/server/mcp/dispatch.mjs +2 -0
  54. package/server/mcp/handler.mjs +2 -0
  55. package/server/mcp/instructions.mjs +40 -0
  56. package/server/mcp-http.mjs +3 -0
  57. package/server/ppt-shim.mjs +156 -0
  58. package/server/relay.mjs +245 -0
  59. package/server/routes/assets.mjs +2 -0
  60. package/server/routes/bridge.mjs +47 -0
  61. package/server/routes/editor.mjs +9 -0
  62. package/server/routes/import.mjs +258 -0
  63. package/server/routes/mcp.mjs +17 -0
  64. package/server/runtime/recovery.mjs +9 -0
  65. package/server/runtime/watch.mjs +17 -0
  66. package/server/shared/SHARED_MANIFEST.json +47 -0
  67. package/server/shared/asset-service.mjs +217 -0
  68. package/server/shared/editor-host.mjs +125 -0
  69. package/server/shared/kit/editor/lease.mjs +139 -0
  70. package/server/shared/kit/service/api.mjs +45 -0
  71. package/server/shared/kit/service/http.mjs +113 -0
  72. package/server/shared/kit/service/mcp-http.mjs +142 -0
  73. package/server/shared/kit/service/single-instance.mjs +72 -0
  74. package/server/shared/kit/tools/validate.mjs +115 -0
  75. package/server/shared/kit/tuning.mjs +82 -0
  76. package/server/shared/kit/util/fs-lock.mjs +132 -0
  77. package/server/shared/service-host.mjs +511 -0
  78. package/server/shared/state-service.mjs +249 -0
  79. package/server/shared/tool-host.mjs +341 -0
  80. package/server/shared/tuning.mjs +63 -0
  81. package/server/single-instance.mjs +2 -0
  82. package/server/store/backends/file-backend.mjs +13 -0
  83. package/server/store/checkpoints.mjs +28 -0
  84. package/server/store/doc.mjs +52 -0
  85. package/server/store/index.mjs +34 -0
  86. package/server/store/journal.mjs +9 -0
  87. package/server/store/lease.mjs +2 -0
  88. package/server/store/sanitize.mjs +32 -0
  89. package/server/store.mjs +2 -0
  90. package/server/tools/capabilities/apply.mjs +33 -0
  91. package/server/tools/capabilities/audit.mjs +24 -0
  92. package/server/tools/capabilities/chart.mjs +83 -0
  93. package/server/tools/capabilities/compose.mjs +97 -0
  94. package/server/tools/capabilities/contract.mjs +34 -0
  95. package/server/tools/capabilities/deck.mjs +27 -0
  96. package/server/tools/capabilities/element.mjs +67 -0
  97. package/server/tools/capabilities/export.mjs +95 -0
  98. package/server/tools/capabilities/formula.mjs +72 -0
  99. package/server/tools/capabilities/history.mjs +29 -0
  100. package/server/tools/capabilities/image.mjs +152 -0
  101. package/server/tools/capabilities/imagesearch.mjs +152 -0
  102. package/server/tools/capabilities/import.mjs +180 -0
  103. package/server/tools/capabilities/index.mjs +58 -0
  104. package/server/tools/capabilities/line.mjs +58 -0
  105. package/server/tools/capabilities/notes.mjs +33 -0
  106. package/server/tools/capabilities/present.mjs +34 -0
  107. package/server/tools/capabilities/recolor.mjs +60 -0
  108. package/server/tools/capabilities/screenshot.mjs +86 -0
  109. package/server/tools/capabilities/shape.mjs +64 -0
  110. package/server/tools/capabilities/slide.mjs +81 -0
  111. package/server/tools/capabilities/style.mjs +48 -0
  112. package/server/tools/capabilities/table.mjs +127 -0
  113. package/server/tools/capabilities/task.mjs +52 -0
  114. package/server/tools/capabilities/text.mjs +82 -0
  115. package/server/tools/domains/color.mjs +4 -0
  116. package/server/tools/domains/image.mjs +49 -0
  117. package/server/tools/domains/index.mjs +7 -0
  118. package/server/tools/domains/pptx-export.mjs +566 -0
  119. package/server/tools/domains/recolor.mjs +25 -0
  120. package/server/tools/domains/remote-image.mjs +291 -0
  121. package/server/tools/domains/slide.mjs +21 -0
  122. package/server/tools/domains/text-style.mjs +11 -0
  123. package/server/tools/domains/tooling.mjs +13 -0
  124. package/server/tools/index.mjs +14 -0
  125. package/server/tools/kernel/define.mjs +5 -0
  126. package/server/tools/kernel/ports.mjs +42 -0
  127. package/server/tools/kernel/registry.mjs +17 -0
  128. package/server/tools.mjs +18 -0
  129. package/server/tuning.mjs +86 -0
  130. package/server/validate.mjs +2 -0
@@ -0,0 +1,291 @@
1
+ /**
2
+ * 远程图片抓取:浏览器头 + 分级错误 + 主机熔断 + 候选预检。
3
+ *
4
+ * 2026-11-02 事故复盘(用户侧现象:一直报"远程图片下载/校验失败"):
5
+ * 1. 请求不带 User-Agent,图床把裸 Node 请求当爬虫直接 403;
6
+ * 2. 10s 硬超时覆盖了整个响应体,3.6MB 的原图在并发下必然中断;
7
+ * 3. 403 / 超时 / 超大 都吐同一句"请改用可访问的图片 URL",
8
+ * 模型读不出"这个域名没救",于是换一个 URL 再撞一次。
9
+ *
10
+ * 所以判定逻辑收在这里:每个失败都带 code 和 permanent 标记,调用方
11
+ * 据此告诉模型下一步该做什么;同一主机确认不可用后 10 分钟内短路,
12
+ * 不再重复付超时的代价。
13
+ */
14
+ export const IMAGE_MAX_BYTES = 6 * 1024 * 1024
15
+
16
+ export const IMAGE_UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 '
17
+ + '(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36'
18
+
19
+ const DEFAULT_TIMEOUT_MS = 25000
20
+ const DEFAULT_PROBE_TIMEOUT_MS = 4500
21
+ const DEFAULT_RETRIES = 1
22
+ const HOST_BLOCK_TTL_MS = 10 * 60 * 1000
23
+ const RETRY_BACKOFF_MS = 400
24
+ /* 同一主机连续多少个"永久失败"才整体熔断。
25
+ * 单条 403 可能只是某张图防盗链,一刀切会误伤同一 CDN 上的好图;
26
+ * 连续两条都废,才说明这个域名对本机不是可用来源。 */
27
+ const HOST_BLOCK_THRESHOLD = 2
28
+
29
+ /** host -> { until, code, reason } */
30
+ const blockedHosts = new Map()
31
+ /** host -> 连续永久失败次数(成功一次清零) */
32
+ const hostFailures = new Map()
33
+
34
+ export class ImageFetchError extends Error {
35
+ constructor(code, message, options = {}) {
36
+ super(message)
37
+ this.name = 'ImageFetchError'
38
+ this.code = code
39
+ this.host = options.host || ''
40
+ this.status = options.status || 0
41
+ this.permanent = options.permanent === true
42
+ this.retryable = options.retryable === true
43
+ }
44
+ }
45
+
46
+ export function hostOf(url) {
47
+ try { return new URL(String(url)).hostname.toLowerCase() } catch { return '' }
48
+ }
49
+
50
+ export function blockedHost(host) {
51
+ const entry = blockedHosts.get(host)
52
+ if (!entry) return null
53
+ if (entry.until <= Date.now()) { blockedHosts.delete(host); return null }
54
+ return entry
55
+ }
56
+
57
+ export function blockHost(host, code, reason) {
58
+ if (!host) return
59
+ blockedHosts.set(host, { until: Date.now() + HOST_BLOCK_TTL_MS, code, reason })
60
+ }
61
+
62
+ /** 记一次永久失败;达到阈值才真正熔断整个主机。 */
63
+ export function noteHostFailure(host, code, reason) {
64
+ if (!host) return false
65
+ const count = (hostFailures.get(host) || 0) + 1
66
+ hostFailures.set(host, count)
67
+ if (count < HOST_BLOCK_THRESHOLD) return false
68
+ blockHost(host, code, reason)
69
+ return true
70
+ }
71
+
72
+ export function noteHostSuccess(host) {
73
+ if (host) hostFailures.delete(host)
74
+ }
75
+
76
+ /** 测试用:清空熔断表。 */
77
+ export function clearBlockedHosts() {
78
+ blockedHosts.clear()
79
+ hostFailures.clear()
80
+ }
81
+
82
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
83
+
84
+ function isTimeout(error) {
85
+ const name = String(error?.name || '')
86
+ return name === 'TimeoutError' || name === 'AbortError'
87
+ }
88
+
89
+ function networkError(error, host, timeoutMs) {
90
+ if (isTimeout(error)) {
91
+ return new ImageFetchError('TIMEOUT', `下载超时(${Math.round(timeoutMs / 1000)}s 内没有收完)`, {
92
+ host, retryable: true,
93
+ })
94
+ }
95
+ return new ImageFetchError('NETWORK', `网络错误:${error?.message || error}`, { host, retryable: true })
96
+ }
97
+
98
+ function httpError(status, host) {
99
+ if (status === 401 || status === 403 || status === 451) {
100
+ noteHostFailure(host, 'HTTP_' + status, '拒绝直连(防盗链/爬虫拦截)')
101
+ return new ImageFetchError('HTTP_' + status, `HTTP ${status}:该站拒绝程序直连(防盗链)`, {
102
+ host, status, permanent: true,
103
+ })
104
+ }
105
+ if (status === 404 || status === 410) {
106
+ return new ImageFetchError('HTTP_' + status, `HTTP ${status}:链接已失效`, { host, status, permanent: true })
107
+ }
108
+ if (status === 408 || status === 425 || status === 429 || status >= 500) {
109
+ return new ImageFetchError('HTTP_' + status, `HTTP ${status}:对方暂时不可用`, { host, status, retryable: true })
110
+ }
111
+ return new ImageFetchError('HTTP_' + status, `HTTP ${status}`, { host, status, permanent: true })
112
+ }
113
+
114
+ const FETCH_HEADERS = {
115
+ 'user-agent': IMAGE_UA,
116
+ accept: 'image/avif,image/webp,image/png,image/jpeg,image/*;q=0.8,*/*;q=0.5',
117
+ 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8',
118
+ referer: 'https://www.bing.com/',
119
+ }
120
+
121
+ async function cancelBody(res) {
122
+ try { await res.body?.cancel() } catch { /* 已关闭 */ }
123
+ }
124
+
125
+ async function fetchBinary(url, { timeoutMs, maxBytes }) {
126
+ const host = hostOf(url)
127
+ let res
128
+ try {
129
+ res = await fetch(url, {
130
+ signal: AbortSignal.timeout(timeoutMs),
131
+ redirect: 'follow',
132
+ headers: FETCH_HEADERS,
133
+ })
134
+ } catch (error) {
135
+ throw networkError(error, host, timeoutMs)
136
+ }
137
+
138
+ if (!res.ok) {
139
+ await cancelBody(res)
140
+ throw httpError(res.status, host)
141
+ }
142
+
143
+ const type = String(res.headers.get('content-type') || '').toLowerCase()
144
+ if (type && !type.startsWith('image/') && !type.startsWith('application/octet-stream')) {
145
+ await cancelBody(res)
146
+ throw new ImageFetchError('NOT_IMAGE', `返回的不是图片(content-type=${type})`, {
147
+ host, status: res.status, permanent: true,
148
+ })
149
+ }
150
+
151
+ const declared = Number(res.headers.get('content-length') || 0)
152
+ if (declared > maxBytes) {
153
+ await cancelBody(res)
154
+ throw new ImageFetchError('TOO_LARGE', `图片 ${(declared / 1024 / 1024).toFixed(1)}MB 超过 ${Math.round(maxBytes / 1024 / 1024)}MB 上限`, {
155
+ host, status: res.status, permanent: true,
156
+ })
157
+ }
158
+
159
+ if (!res.body || typeof res.body.getReader !== 'function') {
160
+ let buf
161
+ try { buf = Buffer.from(await res.arrayBuffer()) } catch (error) {
162
+ throw networkError(error, host, timeoutMs)
163
+ }
164
+ if (buf.length > maxBytes) {
165
+ throw new ImageFetchError('TOO_LARGE', `图片 ${(buf.length / 1024 / 1024).toFixed(1)}MB 超过上限`, {
166
+ host, permanent: true,
167
+ })
168
+ }
169
+ noteHostSuccess(host)
170
+ return { buf, contentType: type, host }
171
+ }
172
+
173
+ const reader = res.body.getReader()
174
+ const chunks = []
175
+ let size = 0
176
+ try {
177
+ while (true) {
178
+ const { done, value } = await reader.read()
179
+ if (done) break
180
+ size += value.byteLength
181
+ if (size > maxBytes) {
182
+ try { await reader.cancel() } catch { /* 忽略 */ }
183
+ throw new ImageFetchError('TOO_LARGE', `图片超过 ${Math.round(maxBytes / 1024 / 1024)}MB 上限`, {
184
+ host, permanent: true,
185
+ })
186
+ }
187
+ chunks.push(Buffer.from(value))
188
+ }
189
+ } catch (error) {
190
+ if (error instanceof ImageFetchError) throw error
191
+ throw networkError(error, host, timeoutMs)
192
+ }
193
+ noteHostSuccess(host)
194
+ return { buf: Buffer.concat(chunks), contentType: type, host }
195
+ }
196
+
197
+ /**
198
+ * 下载远程图片为 Buffer。
199
+ * @returns {Promise<{buf: Buffer, contentType: string, host: string}>}
200
+ */
201
+ export async function downloadImage(url, options = {}) {
202
+ const host = hostOf(url)
203
+ const blocked = blockedHost(host)
204
+ if (blocked) {
205
+ throw new ImageFetchError('HOST_BLOCKED', `${host} 近期已确认不可用(${blocked.reason}),本次直接跳过`, {
206
+ host, permanent: true,
207
+ })
208
+ }
209
+ const timeoutMs = Math.max(1000, Number(options.timeoutMs) || DEFAULT_TIMEOUT_MS)
210
+ const maxBytes = Number(options.maxBytes) || IMAGE_MAX_BYTES
211
+ const retries = options.retries == null ? DEFAULT_RETRIES : Math.max(0, Number(options.retries) || 0)
212
+
213
+ let last = null
214
+ for (let attempt = 0; attempt <= retries; attempt++) {
215
+ try {
216
+ return await fetchBinary(url, { timeoutMs, maxBytes })
217
+ } catch (error) {
218
+ last = error
219
+ const retryable = error instanceof ImageFetchError && error.retryable
220
+ if (!retryable || attempt === retries) break
221
+ await sleep(RETRY_BACKOFF_MS * (attempt + 1))
222
+ }
223
+ }
224
+ throw last
225
+ }
226
+
227
+ /**
228
+ * 预检候选图:只读响应头,判定能否被 ppt_image 下载。
229
+ * 用于 ppt_images 在把直链交给模型之前先过滤死链,避免"选一个、失败、
230
+ * 再选一个、再失败"的循环。拿到头就断开,不读正文。
231
+ * @returns {Promise<{ok: true, size: number, type: string} | {ok: false, code: string, reason: string, permanent: boolean}>}
232
+ */
233
+ export async function probeImage(url, options = {}) {
234
+ const host = hostOf(url)
235
+ const blocked = blockedHost(host)
236
+ if (blocked) return { ok: false, code: 'HOST_BLOCKED', reason: blocked.reason, permanent: true }
237
+
238
+ const budget = Math.max(1000, Number(options.timeoutMs) || DEFAULT_PROBE_TIMEOUT_MS)
239
+ const remaining = options.deadline ? Number(options.deadline) - Date.now() : budget
240
+ if (remaining <= 0) return { ok: false, code: 'TIMEOUT', reason: '预检预算用尽', permanent: false }
241
+ const timeoutMs = Math.min(budget, remaining)
242
+ let res
243
+ try {
244
+ /* 不用 Range 探体积:部分图床对 bytes=0-0 回 416/400,
245
+ * 会把好图误判成死链。这里发普通 GET,拿到响应头就立刻 cancel,
246
+ * 正文不会真的下载完。 */
247
+ res = await fetch(url, {
248
+ signal: AbortSignal.timeout(timeoutMs),
249
+ redirect: 'follow',
250
+ headers: FETCH_HEADERS,
251
+ })
252
+ } catch (error) {
253
+ /* 预检超时不等于不可用:下载阶段给了更长预算,保留候选交给 ppt_image 判定。 */
254
+ return { ok: false, code: isTimeout(error) ? 'TIMEOUT' : 'NETWORK', reason: error?.message || String(error), permanent: false }
255
+ }
256
+
257
+ const type = String(res.headers.get('content-type') || '').toLowerCase()
258
+ if (!res.ok && res.status !== 206) {
259
+ await cancelBody(res)
260
+ const mapped = httpError(res.status, host)
261
+ return { ok: false, code: mapped.code, reason: mapped.message, permanent: mapped.permanent }
262
+ }
263
+ await cancelBody(res)
264
+ if (type && !type.startsWith('image/') && !type.startsWith('application/octet-stream')) {
265
+ return { ok: false, code: 'NOT_IMAGE', reason: `content-type=${type}`, permanent: true }
266
+ }
267
+ let size = Number(res.headers.get('content-length') || 0)
268
+ const range = String(res.headers.get('content-range') || '')
269
+ const total = Number(range.split('/').pop())
270
+ if (Number.isFinite(total) && total > 0) size = total
271
+ if (size > IMAGE_MAX_BYTES) {
272
+ return {
273
+ ok: false,
274
+ code: 'TOO_LARGE',
275
+ reason: `${(size / 1024 / 1024).toFixed(1)}MB 超过上限`,
276
+ permanent: true,
277
+ }
278
+ }
279
+ return { ok: true, size, type }
280
+ }
281
+
282
+ /** 把 ImageFetchError 翻译成模型能直接照着做的下一步。 */
283
+ export function describeImageFailure(error, url) {
284
+ const code = error instanceof ImageFetchError ? error.code : 'UNKNOWN'
285
+ const message = String(error?.message || error)
286
+ const permanent = error instanceof ImageFetchError ? error.permanent : false
287
+ const action = permanent
288
+ ? '这是永久性失败,不要重试同一链接或同一域名:改用 ppt_images 换来源,或本地下载后用 path 传入'
289
+ : '可以重试一次;仍失败就换来源(ppt_images)或改用本地 path'
290
+ return `远程图片不可用[${code}]:${message}。${action}。url=${String(url).slice(0, 160)}`
291
+ }
@@ -0,0 +1,21 @@
1
+ /** 幻灯片工厂:加页工具用的最小页面。 */
2
+ import { escapeHtml } from '../../../core/text.js'
3
+
4
+ export function simpleSlide(title, bullets, background) {
5
+ const W = 1000, pad = 60, ts = Date.now()
6
+ const els = [{
7
+ type: 'text', id: `mcp_t_${ts}`,
8
+ left: pad, top: 50, width: W - pad * 2, height: 70,
9
+ content: `<p style="font-size: 34px;"><strong>${escapeHtml(title)}</strong></p>`,
10
+ defaultColor: '#333333', defaultFontName: '', fixedRatio: false, rotate: 0,
11
+ }]
12
+ ;(bullets || []).slice(0, 8).forEach((b, i) => {
13
+ els.push({
14
+ type: 'text', id: `mcp_b_${ts}_${i}`,
15
+ left: pad, top: 150 + i * 46, width: W - pad * 2, height: 42,
16
+ content: `<p style="font-size: 20px; color: #555555;">• ${escapeHtml(String(b))}</p>`,
17
+ defaultColor: '#333333', defaultFontName: '', fixedRatio: false, rotate: 0,
18
+ })
19
+ })
20
+ return { id: `mcp_slide_${ts}`, background: { type: 'solid', color: background || '#ffffff' }, elements: els }
21
+ }
@@ -0,0 +1,11 @@
1
+ /** 文本样式参数归一化:ppt_text / ppt_fill / ppt_element 共用。 */
2
+ import { resolveColor } from './color.mjs'
3
+
4
+ export function normalizeTextStyle({ font_size, color, bold, align } = {}) {
5
+ const out = {}
6
+ if (font_size != null) out.fontSize = Math.min(200, Math.max(8, Number(font_size) | 0))
7
+ if (color) out.color = resolveColor(color)
8
+ if (bold != null) out.bold = !!bold
9
+ if (align) out.align = align
10
+ return out
11
+ }
@@ -0,0 +1,13 @@
1
+ /** 工具层通用小函数:参数钳制、等待、回执文案。 */
2
+
3
+ export const clamp = (n, lo, hi) => Math.min(hi, Math.max(lo, n))
4
+
5
+ export const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
6
+
7
+ export const okText = (r, msg) => ({
8
+ text: r.ok ? `${msg}(seq ${r.seq},${r.applied} 条)` : `应用失败:${r.error}`,
9
+ })
10
+
11
+ export const normalizeSlides = (slides) => (Array.isArray(slides) && slides.length
12
+ ? slides.map((n) => clamp(Number(n) | 0, 0, 999))
13
+ : null)
@@ -0,0 +1,14 @@
1
+ /** 工具面组合根:能力声明 → 注册表 → 对外导出的唯一入口。 */
2
+ import { CAPABILITIES } from './capabilities/index.mjs'
3
+ import { createRegistry } from './kernel/registry.mjs'
4
+
5
+ export { CAPABILITIES, createRegistry }
6
+
7
+ export const registry = createRegistry(CAPABILITIES)
8
+ export const {
9
+ TOOLS,
10
+ TOOL_IMPL,
11
+ TOOL_META,
12
+ FEATURES,
13
+ CAPABILITIES: CAPABILITY_LIST,
14
+ } = registry
@@ -0,0 +1,5 @@
1
+ /**
2
+ * 能力声明 DSL:统一来自 dsh-tool-host 公共插件。
3
+ * 保留此文件作为稳定 import 路径,capabilities/* 不需要改。
4
+ */
5
+ export { defineTool, defineCapability } from '../../shared/tool-host.mjs'
@@ -0,0 +1,42 @@
1
+ /**
2
+ * 内核端口:能力模块唯一允许接触的 I/O 面。
3
+ * 每个端口对应一类职责,能力按 requires 声明,registry 负责注入。
4
+ */
5
+ import {
6
+ BASE, get, getDeckOnly, getImportResult, getShot, getStatus, post, push, putAsset,
7
+ requireEditor, summarize, uploadImport,
8
+ } from '../../bridge-client.mjs'
9
+
10
+ export function createKernelPorts() {
11
+ return {
12
+ deck: {
13
+ read: getDeckOnly,
14
+ write: push,
15
+ summarize,
16
+ },
17
+ status: {
18
+ get: getStatus,
19
+ baseUrl: BASE,
20
+ },
21
+ editor: {
22
+ require: requireEditor,
23
+ getShot,
24
+ requestShot: () => post('/shot-request', {}),
25
+ },
26
+ assets: {
27
+ put: putAsset,
28
+ },
29
+ task: {
30
+ status: () => get('/task/status'),
31
+ list: () => get('/task/checkpoints'),
32
+ checkpoint: (name, meta) => post('/task/checkpoint', { name, meta: meta || {} }),
33
+ read: (name) => get('/task/checkpoint?name=' + encodeURIComponent(name)),
34
+ },
35
+ importer: {
36
+ upload: uploadImport,
37
+ result: getImportResult,
38
+ },
39
+ }
40
+ }
41
+
42
+ export const KERNEL_PORT_NAMES = Object.freeze(['deck', 'status', 'editor', 'assets', 'task', 'importer', 'contract'])
@@ -0,0 +1,17 @@
1
+ /**
2
+ * 能力注册表:通用实现来自 dsh-tool-host;本文件只注入 PPT 的内核端口与契约报告。
3
+ */
4
+ import { createRegistry as createBaseRegistry } from '../../shared/tool-host.mjs'
5
+ import { createKernelPorts, KERNEL_PORT_NAMES } from './ports.mjs'
6
+ import { capabilityReport } from '../../contract.mjs'
7
+
8
+ export function createRegistry(capabilities, options = {}) {
9
+ const { toolHost = null, ...rest } = options
10
+ const factory = toolHost?.createRegistry || createBaseRegistry
11
+ return factory(capabilities, {
12
+ ports: createKernelPorts(),
13
+ reserved: KERNEL_PORT_NAMES,
14
+ report: ({ editorUrl = '', runtime = {}, ...args }) => capabilityReport(args, editorUrl, runtime),
15
+ ...rest,
16
+ })
17
+ }
@@ -0,0 +1,18 @@
1
+ /** 兼容入口:保持 relay / stdio / tests 的旧 import 路径不变。 */
2
+ export {
3
+ TOOLS,
4
+ TOOL_IMPL,
5
+ TOOL_META,
6
+ FEATURES,
7
+ CAPABILITY_LIST,
8
+ CAPABILITIES,
9
+ createRegistry,
10
+ registry,
11
+ } from './tools/index.mjs'
12
+
13
+ /* 旧 tools.mjs 直接 re-export 的工具辅助;能力模块内部不再走这里。 */
14
+ export {
15
+ expandRecolorOps,
16
+ resolveColor,
17
+ simpleSlide,
18
+ } from './tools/domains/index.mjs'
@@ -0,0 +1,86 @@
1
+ /**
2
+ * 运行时调优参数(单一真源,构建时复制到 server/tuning.mjs)。
3
+ *
4
+ * 这些值不是拍脑袋的常量:由 bench/evolution 的 NSGA-II 进化搜索在
5
+ * 延迟 / 吞吐 / 空闲 CPU / 稳定性四个目标上跑出的 Pareto 解固化而来。
6
+ * 生产可以通过 PPT_TUNING_JSON 环境变量覆盖(见 applyTuningFromEnv)。
7
+ *
8
+ * 注意:本模块同时被 src/mcp 与构建后的 server/ 使用,禁止依赖插件内其它模块。
9
+ */
10
+
11
+ export const DEFAULT_TUNING = Object.freeze({
12
+ /* 编辑器长轮询:越小延迟越低,越大空闲 CPU 越低。
13
+ * 进化搜索的膝点解:75ms 只比 50ms 多 ~5ms 更新延迟,但轮询唤醒减少 35%。 */
14
+ watchPollMs: 75,
15
+ watchTimeoutSec: 20,
16
+ watchTimeoutMaxSec: 30,
17
+
18
+ /* MCP 客户端等待回执的节奏。 */
19
+ statusPollMs: 100,
20
+ pushBaseDeadlineMs: 12000,
21
+ pushPerOpMs: 50,
22
+
23
+ /* HTTP 宿主超时。 */
24
+ httpRequestTimeoutMs: 30000,
25
+ httpHeadersTimeoutMs: 10000,
26
+ httpKeepAliveTimeoutMs: 5000,
27
+
28
+ /* 状态持久化。 */
29
+ journalMaxBytes: 2 * 1024 * 1024,
30
+ stateCompact: true,
31
+
32
+ /* 请求体与 MCP 批量上限。 */
33
+ bodyLimitBytes: 10 * 1024 * 1024,
34
+ mcpBatchLimit: 100,
35
+ })
36
+
37
+ const NUMERIC_KEYS = new Set([
38
+ 'watchPollMs', 'watchTimeoutSec', 'watchTimeoutMaxSec', 'statusPollMs',
39
+ 'pushBaseDeadlineMs', 'pushPerOpMs', 'httpRequestTimeoutMs', 'httpHeadersTimeoutMs',
40
+ 'httpKeepAliveTimeoutMs', 'journalMaxBytes', 'bodyLimitBytes', 'mcpBatchLimit',
41
+ ])
42
+
43
+ let current = { ...DEFAULT_TUNING }
44
+
45
+ function sanitize(patch) {
46
+ const next = {}
47
+ for (const [key, raw] of Object.entries(patch || {})) {
48
+ if (!(key in DEFAULT_TUNING)) continue
49
+ if (key === 'stateCompact') {
50
+ if (typeof raw === 'boolean') next[key] = raw
51
+ continue
52
+ }
53
+ if (!NUMERIC_KEYS.has(key)) continue
54
+ const value = Number(raw)
55
+ if (Number.isFinite(value) && value >= 0) next[key] = value
56
+ }
57
+ return next
58
+ }
59
+
60
+ export function getTuning() {
61
+ return current
62
+ }
63
+
64
+ /** 仅供测试/进化搜索使用:设置当前进程内的调优参数。 */
65
+ export function setTuning(patch) {
66
+ current = { ...current, ...sanitize(patch) }
67
+ return current
68
+ }
69
+
70
+ export function resetTuning() {
71
+ current = { ...DEFAULT_TUNING }
72
+ return current
73
+ }
74
+
75
+ /** 生产覆盖:PPT_TUNING_JSON='{"watchPollMs":100}'。非法值静默忽略。 */
76
+ export function applyTuningFromEnv(env = process.env) {
77
+ const raw = env && env.PPT_TUNING_JSON
78
+ if (!raw) return current
79
+ try {
80
+ return setTuning(JSON.parse(raw))
81
+ } catch {
82
+ return current
83
+ }
84
+ }
85
+
86
+ applyTuningFromEnv()
@@ -0,0 +1,2 @@
1
+ /** 参数校验统一来自 dsh-tool-host 公共插件(底层为 @capyclaw/plugin-kit)。 */
2
+ export { validateToolArguments } from './shared/tool-host.mjs'