@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.
- package/LICENSE +21 -0
- package/README.md +126 -3
- package/THIRD_PARTY_NOTICES.md +21 -0
- package/contract/ppt-contract.json +1529 -0
- package/cordis.patch.yml +25 -0
- package/core/audit.js +500 -0
- package/core/chart.js +199 -0
- package/core/color.js +80 -0
- package/core/deck-layout.js +195 -0
- package/core/formula.js +80 -0
- package/core/hfmath/hershey.js +1763 -0
- package/core/hfmath/index.js +1176 -0
- package/core/hfmath/stretch.js +41 -0
- package/core/hfmath/symbols.js +408 -0
- package/core/ops/browser.js +28 -0
- package/core/ops/chart.js +64 -0
- package/core/ops/deck.js +21 -0
- package/core/ops/dispatcher.js +2 -0
- package/core/ops/element.js +55 -0
- package/core/ops/history.js +11 -0
- package/core/ops/import.js +95 -0
- package/core/ops/index.js +39 -0
- package/core/ops/playback.js +9 -0
- package/core/ops/recolor.js +33 -0
- package/core/ops/slide.js +66 -0
- package/core/ops/table.js +85 -0
- package/core/ops/text.js +35 -0
- package/core/ops/view.js +13 -0
- package/core/ops.js +12 -0
- package/core/quick.js +52 -0
- package/core/shape-library.js +122 -0
- package/core/table.js +178 -0
- package/core/text.js +200 -0
- package/editor/app.js +5366 -0
- package/editor/heartbeat-worker.js +116 -0
- package/editor/index.html +89 -0
- package/editor/ppt-full-app.js +376 -0
- package/lib/host.mjs +98 -0
- package/package.json +53 -6
- package/server/audit.mjs +2 -0
- package/server/bridge-client.mjs +174 -0
- package/server/constants.mjs +20 -0
- package/server/contract-snapshots.mjs +62 -0
- package/server/contract.mjs +24 -0
- package/server/deck-layout.mjs +2 -0
- package/server/http-utils.mjs +2 -0
- package/server/infra/http.mjs +113 -0
- package/server/infra/runtime/context.mjs +128 -0
- package/server/infra/store/blob-store.mjs +109 -0
- package/server/infra/tuning.mjs +82 -0
- package/server/infra/util/fs-lock.mjs +132 -0
- package/server/infra/workspace/manager.mjs +657 -0
- package/server/mcp/dispatch.mjs +2 -0
- package/server/mcp/handler.mjs +2 -0
- package/server/mcp/instructions.mjs +40 -0
- package/server/mcp-http.mjs +3 -0
- package/server/ppt-shim.mjs +156 -0
- package/server/relay.mjs +245 -0
- package/server/routes/assets.mjs +2 -0
- package/server/routes/bridge.mjs +47 -0
- package/server/routes/editor.mjs +9 -0
- package/server/routes/import.mjs +258 -0
- package/server/routes/mcp.mjs +17 -0
- package/server/runtime/recovery.mjs +9 -0
- package/server/runtime/watch.mjs +17 -0
- package/server/shared/SHARED_MANIFEST.json +47 -0
- package/server/shared/asset-service.mjs +217 -0
- package/server/shared/editor-host.mjs +125 -0
- package/server/shared/kit/editor/lease.mjs +139 -0
- package/server/shared/kit/service/api.mjs +45 -0
- package/server/shared/kit/service/http.mjs +113 -0
- package/server/shared/kit/service/mcp-http.mjs +142 -0
- package/server/shared/kit/service/single-instance.mjs +72 -0
- package/server/shared/kit/tools/validate.mjs +115 -0
- package/server/shared/kit/tuning.mjs +82 -0
- package/server/shared/kit/util/fs-lock.mjs +132 -0
- package/server/shared/service-host.mjs +511 -0
- package/server/shared/state-service.mjs +249 -0
- package/server/shared/tool-host.mjs +341 -0
- package/server/shared/tuning.mjs +63 -0
- package/server/single-instance.mjs +2 -0
- package/server/store/backends/file-backend.mjs +13 -0
- package/server/store/checkpoints.mjs +28 -0
- package/server/store/doc.mjs +52 -0
- package/server/store/index.mjs +34 -0
- package/server/store/journal.mjs +9 -0
- package/server/store/lease.mjs +2 -0
- package/server/store/sanitize.mjs +32 -0
- package/server/store.mjs +2 -0
- package/server/tools/capabilities/apply.mjs +33 -0
- package/server/tools/capabilities/audit.mjs +24 -0
- package/server/tools/capabilities/chart.mjs +83 -0
- package/server/tools/capabilities/compose.mjs +97 -0
- package/server/tools/capabilities/contract.mjs +34 -0
- package/server/tools/capabilities/deck.mjs +27 -0
- package/server/tools/capabilities/element.mjs +67 -0
- package/server/tools/capabilities/export.mjs +95 -0
- package/server/tools/capabilities/formula.mjs +72 -0
- package/server/tools/capabilities/history.mjs +29 -0
- package/server/tools/capabilities/image.mjs +152 -0
- package/server/tools/capabilities/imagesearch.mjs +152 -0
- package/server/tools/capabilities/import.mjs +180 -0
- package/server/tools/capabilities/index.mjs +58 -0
- package/server/tools/capabilities/line.mjs +58 -0
- package/server/tools/capabilities/notes.mjs +33 -0
- package/server/tools/capabilities/present.mjs +34 -0
- package/server/tools/capabilities/recolor.mjs +60 -0
- package/server/tools/capabilities/screenshot.mjs +86 -0
- package/server/tools/capabilities/shape.mjs +64 -0
- package/server/tools/capabilities/slide.mjs +81 -0
- package/server/tools/capabilities/style.mjs +48 -0
- package/server/tools/capabilities/table.mjs +127 -0
- package/server/tools/capabilities/task.mjs +52 -0
- package/server/tools/capabilities/text.mjs +82 -0
- package/server/tools/domains/color.mjs +4 -0
- package/server/tools/domains/image.mjs +49 -0
- package/server/tools/domains/index.mjs +7 -0
- package/server/tools/domains/pptx-export.mjs +566 -0
- package/server/tools/domains/recolor.mjs +25 -0
- package/server/tools/domains/remote-image.mjs +291 -0
- package/server/tools/domains/slide.mjs +21 -0
- package/server/tools/domains/text-style.mjs +11 -0
- package/server/tools/domains/tooling.mjs +13 -0
- package/server/tools/index.mjs +14 -0
- package/server/tools/kernel/define.mjs +5 -0
- package/server/tools/kernel/ports.mjs +42 -0
- package/server/tools/kernel/registry.mjs +17 -0
- package/server/tools.mjs +18 -0
- package/server/tuning.mjs +86 -0
- package/server/validate.mjs +2 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** PPT 兼容层:标准会话信封 + PPT 的 deck 初始文档与标题同步。 */
|
|
2
|
+
import {
|
|
3
|
+
applyAck as baseAck,
|
|
4
|
+
applyPush as basePush,
|
|
5
|
+
applyServer as baseServer,
|
|
6
|
+
applyShotRequest as baseShotRequest,
|
|
7
|
+
applyShotResult as baseShotResult,
|
|
8
|
+
applyState as baseState,
|
|
9
|
+
applyUi as baseUi,
|
|
10
|
+
createSessionState,
|
|
11
|
+
pendingOf,
|
|
12
|
+
} from '@capyclaw/document-workbench/session'
|
|
13
|
+
|
|
14
|
+
export const EMPTY_DECK = () => ({ title: '', slides: [] })
|
|
15
|
+
|
|
16
|
+
export const EMPTY_DOC = () => createSessionState({ title: '' })
|
|
17
|
+
|
|
18
|
+
export function applyPush(doc, cmds, title, now) {
|
|
19
|
+
return basePush(doc, cmds, title, now)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function applyServer(doc, seq, applied, error, now) {
|
|
23
|
+
return baseServer(doc, seq, applied, error, {
|
|
24
|
+
now,
|
|
25
|
+
stateCompact: true,
|
|
26
|
+
onApplied: (next) => {
|
|
27
|
+
if (next.title && next.deck) next.deck.title = next.title
|
|
28
|
+
},
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function applyAck(doc, seq, applied, error, now) {
|
|
33
|
+
return baseAck(doc, seq, applied, error, now)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function applyState(doc, deck, seq, now) {
|
|
37
|
+
return baseState(doc, deck, seq, { now, documentKey: 'deck' })
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function applyUi(doc, cmds, now) {
|
|
41
|
+
return baseUi(doc, cmds, now)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function applyShotRequest(doc, now) {
|
|
45
|
+
return baseShotRequest(doc, now)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function applyShotResult(doc, req, data, now) {
|
|
49
|
+
return baseShotResult(doc, req, data, now)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export { pendingOf }
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PPT 桥存储 facade:通用版本化会话来自 @capyclaw/document-workbench,
|
|
3
|
+
* 本文件只保留 deck 的初始文档、summary 与标题同步。
|
|
4
|
+
*/
|
|
5
|
+
import { DocumentSession, OWNER_TTL } from '@capyclaw/document-workbench/session'
|
|
6
|
+
import { FileBridgeBackend } from './backends/file-backend.mjs'
|
|
7
|
+
import { EMPTY_DOC } from './doc.mjs'
|
|
8
|
+
|
|
9
|
+
export * from './sanitize.mjs'
|
|
10
|
+
export { OWNER_TTL, FileBridgeBackend }
|
|
11
|
+
|
|
12
|
+
export class BridgeStore extends DocumentSession {
|
|
13
|
+
constructor(fileOrOptions) {
|
|
14
|
+
let backend
|
|
15
|
+
if (typeof fileOrOptions === 'string') backend = new FileBridgeBackend(fileOrOptions)
|
|
16
|
+
else if (fileOrOptions && fileOrOptions.backend) backend = fileOrOptions.backend
|
|
17
|
+
else throw new Error('BridgeStore 需要 file 路径或 backend')
|
|
18
|
+
|
|
19
|
+
super({
|
|
20
|
+
backend,
|
|
21
|
+
initialDoc: EMPTY_DOC(),
|
|
22
|
+
documentKey: 'deck',
|
|
23
|
+
stateCompact: true,
|
|
24
|
+
summary: (doc) => ({
|
|
25
|
+
title: doc.deck && doc.deck.title ? doc.deck.title : '未命名',
|
|
26
|
+
slides: doc.deck && Array.isArray(doc.deck.slides) ? doc.deck.slides.length : 0,
|
|
27
|
+
}),
|
|
28
|
+
checkpointDocument: () => ({ title: '未命名', slides: [] }),
|
|
29
|
+
onApplied: (doc) => {
|
|
30
|
+
if (doc.title && doc.deck) doc.deck.title = doc.title
|
|
31
|
+
},
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** 桥协议白名单:通用信封来自 @capyclaw/document-workbench,PPT 只需声明 deck 字段与上限。 */
|
|
2
|
+
import { createSessionSanitizers } from '@capyclaw/document-workbench/session'
|
|
3
|
+
|
|
4
|
+
export const MAX_CMDS = 100 // 单队指令条数上限
|
|
5
|
+
export const MAX_TOTAL = 8 * 1024 * 1024 // 单队总量上限(字节)
|
|
6
|
+
export const MAX_SHOT = 4 * 1024 * 1024 // 截图 dataURL 上限
|
|
7
|
+
|
|
8
|
+
const sanitizers = createSessionSanitizers({
|
|
9
|
+
documentKey: 'deck',
|
|
10
|
+
maxCmds: MAX_CMDS,
|
|
11
|
+
maxTotal: MAX_TOTAL,
|
|
12
|
+
maxShot: MAX_SHOT,
|
|
13
|
+
maxTitle: 120,
|
|
14
|
+
maxError: 2000,
|
|
15
|
+
maxId: 64,
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
const {
|
|
19
|
+
sanitizeAck,
|
|
20
|
+
sanitizeClaim,
|
|
21
|
+
sanitizePush,
|
|
22
|
+
sanitizeShotResult,
|
|
23
|
+
sanitizeState: baseSanitizeState,
|
|
24
|
+
} = sanitizers
|
|
25
|
+
|
|
26
|
+
export { sanitizeAck, sanitizeClaim, sanitizePush, sanitizeShotResult }
|
|
27
|
+
|
|
28
|
+
/** 兼容旧 API:返回 { deck, seq };通用 bridge 会把它归一化为 value。 */
|
|
29
|
+
export function sanitizeState(body) {
|
|
30
|
+
const data = baseSanitizeState(body)
|
|
31
|
+
return data ? { deck: data.value, seq: data.seq } : null
|
|
32
|
+
}
|
package/server/store.mjs
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** 底层指令逃生口:一次提交任意 op 数组。 */
|
|
2
|
+
import { okText } from '../domains/tooling.mjs'
|
|
3
|
+
import { defineCapability, defineTool } from '../kernel/define.mjs'
|
|
4
|
+
|
|
5
|
+
export const applyCapability = defineCapability({
|
|
6
|
+
id: 'apply',
|
|
7
|
+
title: '底层指令',
|
|
8
|
+
tags: ['advanced'],
|
|
9
|
+
requires: ['deck'],
|
|
10
|
+
features: ['deck.write'],
|
|
11
|
+
tools: [
|
|
12
|
+
defineTool({
|
|
13
|
+
name: 'ppt_apply',
|
|
14
|
+
description: '通用逃生口:一次提交任意底层指令数组(load / update-slide / add-element / set-background / theme / …),复杂组合动作用。',
|
|
15
|
+
inputSchema: {
|
|
16
|
+
type: 'object',
|
|
17
|
+
properties: {
|
|
18
|
+
ops: { type: 'array', maxItems: 100, items: { type: 'object' }, description: '底层 op 数组(≤100)' },
|
|
19
|
+
title: { type: 'string', maxLength: 120, description: 'deck 标题(可选)' },
|
|
20
|
+
},
|
|
21
|
+
additionalProperties: false,
|
|
22
|
+
},
|
|
23
|
+
kind: 'write',
|
|
24
|
+
since: '1.0.0',
|
|
25
|
+
handler: async ({ ops, title }, ctx) => {
|
|
26
|
+
const list = Array.isArray(ops) ? ops : []
|
|
27
|
+
if (!list.length && !title) throw new Error('ops 为空且未提供 title:请提供操作或标题')
|
|
28
|
+
const r = await ctx.deck.write(list, title)
|
|
29
|
+
return okText(r, list.length ? `已应用 ${list.length} 条指令` : '已更新标题')
|
|
30
|
+
},
|
|
31
|
+
}),
|
|
32
|
+
],
|
|
33
|
+
})
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** 确定性 deck 审计能力。 */
|
|
2
|
+
import { auditDeck } from '../../../core/audit.js'
|
|
3
|
+
import { defineCapability, defineTool } from '../kernel/define.mjs'
|
|
4
|
+
|
|
5
|
+
export const auditCapability = defineCapability({
|
|
6
|
+
id: 'audit',
|
|
7
|
+
title: '确定性审计',
|
|
8
|
+
tags: ['read', 'quality'],
|
|
9
|
+
requires: ['deck'],
|
|
10
|
+
features: ['deck.audit'],
|
|
11
|
+
tools: [
|
|
12
|
+
defineTool({
|
|
13
|
+
name: 'ppt_audit',
|
|
14
|
+
description: '确定性审计当前 deck:文字越界/溢出/重叠、同文案重复、文字隐形/被遮挡、有效背景对比度、重复版式、配色漂移、重复图片、页面密度。返回 pass/blocking;交付前或截图前调用,pass=false 不得说审计通过。',
|
|
15
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
16
|
+
kind: 'read',
|
|
17
|
+
since: '2.1.0',
|
|
18
|
+
handler: async (args, ctx) => {
|
|
19
|
+
const doc = await ctx.deck.read()
|
|
20
|
+
return { text: JSON.stringify(auditDeck(doc.deck), null, 2) }
|
|
21
|
+
},
|
|
22
|
+
}),
|
|
23
|
+
],
|
|
24
|
+
})
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/** 图表能力:把数据直接变成 PPTist 原生图表(echarts 渲染,不再手绘形状拼图)。 */
|
|
2
|
+
import { CHART_TYPES } from '../../../core/chart.js'
|
|
3
|
+
import { clamp, okText } from '../domains/tooling.mjs'
|
|
4
|
+
import { defineCapability, defineTool } from '../kernel/define.mjs'
|
|
5
|
+
|
|
6
|
+
const chartDataDesc =
|
|
7
|
+
'数据,支持多种格式:{labels:[...], series:[[...]]} / {labels:[...], values:[...]} 单系列 / ' +
|
|
8
|
+
'{headers:[...], rows:[[...]]} 表格式(首列类目,其余列系列) / {items:[[name,value]...]} 饼图式'
|
|
9
|
+
|
|
10
|
+
export const chartCapability = defineCapability({
|
|
11
|
+
id: 'chart',
|
|
12
|
+
title: '图表',
|
|
13
|
+
tags: ['write', 'data'],
|
|
14
|
+
requires: ['deck'],
|
|
15
|
+
features: ['deck.chart'],
|
|
16
|
+
tools: [
|
|
17
|
+
defineTool({
|
|
18
|
+
name: 'ppt_chart',
|
|
19
|
+
description:
|
|
20
|
+
'插入或更新图表(PPTist 原生 echarts 图表)。给定数据即可生成柱/条/折线/面积/饼/环/雷达/散点图,' +
|
|
21
|
+
'支持多种数据格式(labels+series、表格 rows、键值对 items)。带 element_id 时为更新已有图表。',
|
|
22
|
+
inputSchema: {
|
|
23
|
+
type: 'object',
|
|
24
|
+
properties: {
|
|
25
|
+
slide: { type: 'integer', description: '0 起页号,默认 0' },
|
|
26
|
+
chart_type: { type: 'string', enum: CHART_TYPES, description: '图表类型,新增必填' },
|
|
27
|
+
data: { type: 'object', description: chartDataDesc },
|
|
28
|
+
element_id: { type: 'string', description: '更新模式:要更新的图表元素 id' },
|
|
29
|
+
options: {
|
|
30
|
+
type: 'object',
|
|
31
|
+
description: '渲染选项 {lineSmooth?:折线平滑, stack?:堆叠}',
|
|
32
|
+
properties: {
|
|
33
|
+
lineSmooth: { type: 'boolean' },
|
|
34
|
+
stack: { type: 'boolean' },
|
|
35
|
+
},
|
|
36
|
+
additionalProperties: false,
|
|
37
|
+
},
|
|
38
|
+
colors: { type: 'array', items: { type: 'string' }, description: '系列配色(缺省用幻灯片主题色)' },
|
|
39
|
+
text_color: { type: 'string', description: '图表文字颜色' },
|
|
40
|
+
left: { type: 'number' },
|
|
41
|
+
top: { type: 'number' },
|
|
42
|
+
width: { type: 'number' },
|
|
43
|
+
height: { type: 'number' },
|
|
44
|
+
},
|
|
45
|
+
required: ['slide'],
|
|
46
|
+
additionalProperties: false,
|
|
47
|
+
},
|
|
48
|
+
kind: 'write',
|
|
49
|
+
since: '2.2.0',
|
|
50
|
+
handler: async (args, ctx) => {
|
|
51
|
+
const i = clamp(Number(args.slide) | 0, 0, 999)
|
|
52
|
+
const p = args || {}
|
|
53
|
+
if (p.element_id) {
|
|
54
|
+
const props = {}
|
|
55
|
+
if (p.chart_type) props.chartType = p.chart_type
|
|
56
|
+
if (p.data != null) props.data = p.data
|
|
57
|
+
if (p.options != null) props.options = p.options
|
|
58
|
+
if (Array.isArray(p.colors)) props.colors = p.colors
|
|
59
|
+
if (p.text_color) props.textColor = p.text_color
|
|
60
|
+
for (const k of ['left', 'top', 'width', 'height']) {
|
|
61
|
+
if (p[k] != null) props[k] = p[k]
|
|
62
|
+
}
|
|
63
|
+
const r = await ctx.deck.write([{ op: 'update-chart', slide: i, elementId: String(p.element_id), props }])
|
|
64
|
+
return okText(r, `已更新图表 ${p.element_id}`)
|
|
65
|
+
}
|
|
66
|
+
if (!p.chart_type) throw new Error('新增图表需要 chart_type(' + CHART_TYPES.join('/') + ')')
|
|
67
|
+
if (p.data == null) throw new Error('新增图表需要 data(' + chartDataDesc + ')')
|
|
68
|
+
const element = {
|
|
69
|
+
chartType: p.chart_type,
|
|
70
|
+
data: p.data,
|
|
71
|
+
...(p.options != null ? { options: p.options } : {}),
|
|
72
|
+
...(Array.isArray(p.colors) ? { colors: p.colors } : {}),
|
|
73
|
+
...(p.text_color ? { textColor: p.text_color } : {}),
|
|
74
|
+
}
|
|
75
|
+
for (const k of ['left', 'top', 'width', 'height']) {
|
|
76
|
+
if (p[k] != null) element[k] = p[k]
|
|
77
|
+
}
|
|
78
|
+
const r = await ctx.deck.write([{ op: 'add-chart', slide: i, element }])
|
|
79
|
+
return okText(r, `已在第 ${i} 页插入 ${p.chart_type} 图表`)
|
|
80
|
+
},
|
|
81
|
+
}),
|
|
82
|
+
],
|
|
83
|
+
})
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/** 成稿编排能力:内置版式成稿(组合 core 引擎与 deck 端口)。 */
|
|
2
|
+
import { buildDeck, buildSlide, PRESETS } from '../../../core/deck-layout.js'
|
|
3
|
+
import { okText } from '../domains/tooling.mjs'
|
|
4
|
+
import { defineCapability, defineTool } from '../kernel/define.mjs'
|
|
5
|
+
|
|
6
|
+
export const composeCapability = defineCapability({
|
|
7
|
+
id: 'compose',
|
|
8
|
+
title: '成稿编排',
|
|
9
|
+
tags: ['recipe', 'layout'],
|
|
10
|
+
requires: ['deck'],
|
|
11
|
+
features: ['deck.compose'],
|
|
12
|
+
tools: [
|
|
13
|
+
defineTool({
|
|
14
|
+
name: 'ppt_deck',
|
|
15
|
+
description: '从大纲一次成稿:按版式(cover/section/bullets/two/stat/quote/end)+ 设计预设(clean/business/warm/tech)生成整套专业排版 deck,替换当前内容。默认简洁成稿首选(preset: clean)。',
|
|
16
|
+
inputSchema: {
|
|
17
|
+
type: 'object',
|
|
18
|
+
properties: {
|
|
19
|
+
title: { type: 'string', maxLength: 40 },
|
|
20
|
+
preset: { type: 'string', enum: ['clean', 'business', 'warm', 'tech'] },
|
|
21
|
+
slides: {
|
|
22
|
+
type: 'array',
|
|
23
|
+
maxItems: 30,
|
|
24
|
+
items: {
|
|
25
|
+
type: 'object',
|
|
26
|
+
properties: {
|
|
27
|
+
layout: { type: 'string', enum: ['cover', 'section', 'bullets', 'two', 'stat', 'quote', 'end'] },
|
|
28
|
+
title: { type: 'string', maxLength: 40 },
|
|
29
|
+
subtitle: { type: 'string', maxLength: 80 },
|
|
30
|
+
kicker: { type: 'string', maxLength: 40 },
|
|
31
|
+
index: { type: 'integer' },
|
|
32
|
+
bullets: { type: 'array', maxItems: 8 },
|
|
33
|
+
left: { type: 'object' }, right: { type: 'object' },
|
|
34
|
+
stats: { type: 'array', maxItems: 4 },
|
|
35
|
+
quote: { type: 'string', maxLength: 300 }, author: { type: 'string', maxLength: 60 },
|
|
36
|
+
note: { type: 'string', maxLength: 300 }, foot: { type: 'string', maxLength: 100 },
|
|
37
|
+
},
|
|
38
|
+
required: ['layout'],
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
required: ['slides'],
|
|
43
|
+
additionalProperties: false,
|
|
44
|
+
},
|
|
45
|
+
kind: 'write',
|
|
46
|
+
since: '1.0.0',
|
|
47
|
+
handler: async ({ title, preset, slides }, ctx) => {
|
|
48
|
+
const deck = buildDeck({ title, preset, slides })
|
|
49
|
+
const r = await ctx.deck.write([{ op: 'load', deck }])
|
|
50
|
+
return okText(r, `已生成 ${deck.slides.length} 页「${deck.title}」(预设 ${preset || 'clean'})`)
|
|
51
|
+
},
|
|
52
|
+
}),
|
|
53
|
+
defineTool({
|
|
54
|
+
name: 'ppt_page',
|
|
55
|
+
description: '逐页成稿:一次只追加/替换当前一页(同一套 clean 版式),适合长 deck 分批落盘;首屏用 replace:true,后续逐页追加。',
|
|
56
|
+
inputSchema: {
|
|
57
|
+
type: 'object',
|
|
58
|
+
properties: {
|
|
59
|
+
layout: { type: 'string', enum: ['cover', 'section', 'bullets', 'two', 'stat', 'quote', 'end'] },
|
|
60
|
+
preset: { type: 'string', enum: ['clean', 'business', 'warm', 'tech'] },
|
|
61
|
+
deck_title: { type: 'string', maxLength: 40 },
|
|
62
|
+
position: { type: 'integer', description: '插入位置(0 起,缺省末尾)' },
|
|
63
|
+
replace: { type: 'boolean', description: 'true 时用这一页替换整个 deck(首屏用)' },
|
|
64
|
+
title: { type: 'string', maxLength: 40 },
|
|
65
|
+
subtitle: { type: 'string', maxLength: 80 },
|
|
66
|
+
kicker: { type: 'string', maxLength: 40 },
|
|
67
|
+
section_index: { type: 'integer', description: 'section 版式的章节序号' },
|
|
68
|
+
bullets: { type: 'array', maxItems: 8 },
|
|
69
|
+
left: { type: 'object' }, right: { type: 'object' },
|
|
70
|
+
stats: { type: 'array', maxItems: 4 },
|
|
71
|
+
quote: { type: 'string', maxLength: 300 }, author: { type: 'string', maxLength: 60 },
|
|
72
|
+
note: { type: 'string', maxLength: 300 }, foot: { type: 'string', maxLength: 100 },
|
|
73
|
+
},
|
|
74
|
+
required: ['layout'],
|
|
75
|
+
additionalProperties: false,
|
|
76
|
+
},
|
|
77
|
+
kind: 'write',
|
|
78
|
+
since: '2.3.0',
|
|
79
|
+
handler: async ({ layout, preset, deck_title, position, replace, section_index, ...page }, ctx) => {
|
|
80
|
+
const spec = { layout, ...page }
|
|
81
|
+
if (section_index != null) spec.index = section_index
|
|
82
|
+
const presetName = PRESETS[preset] ? preset : 'clean'
|
|
83
|
+
const P = PRESETS[presetName]
|
|
84
|
+
const slide = buildSlide(P, spec, 0)
|
|
85
|
+
const theme = { themeColors: [P.accent, P.accent2, P.fg], fontColor: P.fg, backgroundColor: P.bg }
|
|
86
|
+
if (replace) {
|
|
87
|
+
const r = await ctx.deck.write([{ op: 'load', deck: { title: deck_title || page.title || '演示文稿', slides: [slide], theme } }])
|
|
88
|
+
return okText(r, `已用 1 页替换 deck「${deck_title || page.title || '演示文稿'}」`)
|
|
89
|
+
}
|
|
90
|
+
const cmd = { op: 'add-slide', slide }
|
|
91
|
+
if (position != null) cmd.index = Number(position) | 0
|
|
92
|
+
const r = await ctx.deck.write([cmd])
|
|
93
|
+
return okText(r, `已追加 1 页「${page.title || layout}」`)
|
|
94
|
+
},
|
|
95
|
+
}),
|
|
96
|
+
],
|
|
97
|
+
})
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** 契约探测能力:版本、工具清单、能力清单与运行时状态。 */
|
|
2
|
+
import { defineCapability, defineTool } from '../kernel/define.mjs'
|
|
3
|
+
|
|
4
|
+
export const contractCapability = defineCapability({
|
|
5
|
+
id: 'contract',
|
|
6
|
+
title: '契约探测',
|
|
7
|
+
tags: ['meta'],
|
|
8
|
+
requires: ['status', 'contract'],
|
|
9
|
+
features: ['deck.contract'],
|
|
10
|
+
tools: [
|
|
11
|
+
defineTool({
|
|
12
|
+
name: 'ppt_capabilities',
|
|
13
|
+
description: '返回工作台契约版本、工具清单、能力清单、能力标签与编辑器地址。skill 加载后先调用一次;工具缺失或 major 版本不一致时停止并提示升级。',
|
|
14
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
15
|
+
kind: 'read',
|
|
16
|
+
since: '2.0.0',
|
|
17
|
+
handler: async (args, ctx) => {
|
|
18
|
+
let editorUrl = ''
|
|
19
|
+
try { editorUrl = new URL(ctx.status.baseUrl).origin + '/' } catch { /* 保持空字符串 */ }
|
|
20
|
+
let runtime = { editorConnected: false, hasDeck: false, slides: 0, seq: 0 }
|
|
21
|
+
try {
|
|
22
|
+
const status = await ctx.status.get()
|
|
23
|
+
runtime = {
|
|
24
|
+
editorConnected: !!status.owner,
|
|
25
|
+
hasDeck: !!status.has_deck,
|
|
26
|
+
slides: Number(status.slides || 0),
|
|
27
|
+
seq: Number(status.seq || 0),
|
|
28
|
+
}
|
|
29
|
+
} catch { /* relay 不可用时仍返回静态契约 */ }
|
|
30
|
+
return { text: JSON.stringify(ctx.contract.report(editorUrl, runtime), null, 2) }
|
|
31
|
+
},
|
|
32
|
+
}),
|
|
33
|
+
],
|
|
34
|
+
})
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** 读 deck 能力。 */
|
|
2
|
+
import { defineCapability, defineTool } from '../kernel/define.mjs'
|
|
3
|
+
|
|
4
|
+
export const deckCapability = defineCapability({
|
|
5
|
+
id: 'deck',
|
|
6
|
+
title: '读取演示文稿',
|
|
7
|
+
tags: ['read'],
|
|
8
|
+
requires: ['deck'],
|
|
9
|
+
features: ['deck.read'],
|
|
10
|
+
tools: [
|
|
11
|
+
defineTool({
|
|
12
|
+
name: 'ppt_get_deck',
|
|
13
|
+
description: '读当前演示文稿:标题、每页背景/元素。需要精确定位(页号/元素 id)时调用;改背景/加页/改首个文本不要先读。',
|
|
14
|
+
inputSchema: {
|
|
15
|
+
type: 'object',
|
|
16
|
+
properties: { detailed: { type: 'boolean', description: '元素几何/颜色等细节(缺省只给摘要)' } },
|
|
17
|
+
additionalProperties: false,
|
|
18
|
+
},
|
|
19
|
+
kind: 'read',
|
|
20
|
+
since: '1.0.0',
|
|
21
|
+
handler: async ({ detailed }, ctx) => {
|
|
22
|
+
const doc = await ctx.deck.read()
|
|
23
|
+
return { text: ctx.deck.summarize(doc.deck, !!detailed) }
|
|
24
|
+
},
|
|
25
|
+
}),
|
|
26
|
+
],
|
|
27
|
+
})
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/** 元素能力:加/改/删元素;图片插入委托给 media.insertImage。 */
|
|
2
|
+
import { resolveColor } from '../domains/color.mjs'
|
|
3
|
+
import { clamp, okText } from '../domains/tooling.mjs'
|
|
4
|
+
import { defineCapability, defineTool } from '../kernel/define.mjs'
|
|
5
|
+
|
|
6
|
+
export const elementCapability = defineCapability({
|
|
7
|
+
id: 'element',
|
|
8
|
+
title: '元素编辑',
|
|
9
|
+
tags: ['write', 'layout'],
|
|
10
|
+
requires: ['deck', 'media'],
|
|
11
|
+
dependsOn: ['image'],
|
|
12
|
+
features: ['deck.write'],
|
|
13
|
+
tools: [
|
|
14
|
+
defineTool({
|
|
15
|
+
name: 'ppt_element',
|
|
16
|
+
description: '元素级编辑:add 加文本/形状/图片,update 改位置/尺寸/旋转/填充/文字样式,remove 删除。插入图片走 ppt_image 的同一实现。',
|
|
17
|
+
inputSchema: {
|
|
18
|
+
type: 'object',
|
|
19
|
+
properties: {
|
|
20
|
+
action: { type: 'string', enum: ['add', 'update', 'remove'] },
|
|
21
|
+
slide: { type: 'integer', description: '0 起页号' },
|
|
22
|
+
element_id: { type: 'string' },
|
|
23
|
+
element: {
|
|
24
|
+
type: 'object',
|
|
25
|
+
description: "add 用:{type:'text'|'shape'|'image', text?/content?, src?(image), fill?, left?, top?, width?, height?, fontSize?, color?}",
|
|
26
|
+
},
|
|
27
|
+
props: {
|
|
28
|
+
type: 'object',
|
|
29
|
+
description: 'update 用:{left?,top?,width?,height?,rotate?,opacity?,fill?,src?,text?,fontSize?,color?,bold?,align?}',
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
required: ['action', 'slide'],
|
|
33
|
+
additionalProperties: false,
|
|
34
|
+
},
|
|
35
|
+
kind: 'write',
|
|
36
|
+
since: '1.0.0',
|
|
37
|
+
handler: async ({ action, slide, element_id, element, props }, ctx) => {
|
|
38
|
+
const i = clamp(Number(slide) | 0, 0, 999)
|
|
39
|
+
if (action === 'add') {
|
|
40
|
+
const el = element || {}
|
|
41
|
+
if (el.type === 'image') {
|
|
42
|
+
if (!ctx.media) throw new Error('插入图片需要启用 image 能力')
|
|
43
|
+
return ctx.media.insertImage({ slide: i, ...el })
|
|
44
|
+
}
|
|
45
|
+
const r = await ctx.deck.write([{ op: 'add-element', slide: i, element: el }])
|
|
46
|
+
return okText(r, `已在第 ${i} 页加元素`)
|
|
47
|
+
}
|
|
48
|
+
if (!element_id) throw new Error('update/remove 需要 element_id')
|
|
49
|
+
if (action === 'remove') {
|
|
50
|
+
const r = await ctx.deck.write([{ op: 'remove-element', slide: i, elementId: String(element_id) }])
|
|
51
|
+
return okText(r, `已删除元素 ${element_id}`)
|
|
52
|
+
}
|
|
53
|
+
if (action === 'update') {
|
|
54
|
+
const cleaned = {}
|
|
55
|
+
const p = props || {}
|
|
56
|
+
;['left', 'top', 'width', 'height', 'rotate', 'opacity', 'fill', 'src', 'text', 'html', 'fontSize', 'color', 'bold', 'align', 'defaultColor'].forEach(k => {
|
|
57
|
+
if (p[k] != null) cleaned[k] = p[k]
|
|
58
|
+
})
|
|
59
|
+
if (cleaned.color) cleaned.color = resolveColor(cleaned.color)
|
|
60
|
+
const r = await ctx.deck.write([{ op: 'set-element', slide: i, elementId: String(element_id), props: cleaned }])
|
|
61
|
+
return okText(r, `已更新元素 ${element_id}`)
|
|
62
|
+
}
|
|
63
|
+
throw new Error('未知 action: ' + action)
|
|
64
|
+
},
|
|
65
|
+
}),
|
|
66
|
+
],
|
|
67
|
+
})
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/** PPTX 导出:把当前 deck 渲染成本机 .pptx 文件。 */
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { defineCapability, defineTool } from '../kernel/define.mjs'
|
|
6
|
+
import { renderDeckToPptx, safeFileName } from '../domains/pptx-export.mjs'
|
|
7
|
+
|
|
8
|
+
function outputPath({ filePath, fileName, title }) {
|
|
9
|
+
const stamp = new Date().toISOString().replace(/[-:]/g, '').replace(/\..+$/, '')
|
|
10
|
+
const base = safeFileName(fileName || title || 'presentation')
|
|
11
|
+
const defaultName = `${base}${stamp ? '-' + stamp : ''}.pptx`
|
|
12
|
+
if (!filePath) return path.join(os.tmpdir(), 'ppt-exports', defaultName)
|
|
13
|
+
|
|
14
|
+
const resolved = path.resolve(String(filePath))
|
|
15
|
+
if (/\.pptx$/i.test(resolved)) return resolved
|
|
16
|
+
return path.join(resolved, defaultName)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const exportCapability = defineCapability({
|
|
20
|
+
id: 'export',
|
|
21
|
+
title: '导出 PPTX',
|
|
22
|
+
tags: ['write', 'file'],
|
|
23
|
+
requires: ['deck', 'status'],
|
|
24
|
+
features: ['deck.export'],
|
|
25
|
+
tools: [
|
|
26
|
+
defineTool({
|
|
27
|
+
name: 'ppt_export_pptx',
|
|
28
|
+
description: '把当前 deck 导出为本机 .pptx 文件(pptxgenjs)。支持文字/形状/图片/线条/表格/图表/背景/备注;自定义 SVG 路径用矩形近似,LaTeX/音视频跳过。返回文件绝对路径与降级项。',
|
|
29
|
+
inputSchema: {
|
|
30
|
+
type: 'object',
|
|
31
|
+
properties: {
|
|
32
|
+
path: { type: 'string', description: '输出 .pptx 路径,或已存在的目录;缺省写到系统临时目录 ppt-exports/' },
|
|
33
|
+
file_name: { type: 'string', maxLength: 160, description: '文件名(可省略 .pptx);path 为目录/缺省时使用' },
|
|
34
|
+
slide_indices: {
|
|
35
|
+
type: 'array',
|
|
36
|
+
items: { type: 'integer', minimum: 0 },
|
|
37
|
+
maxItems: 200,
|
|
38
|
+
description: '只导出指定页(0 基);缺省导出全部',
|
|
39
|
+
},
|
|
40
|
+
include_notes: { type: 'boolean', description: '是否写入演讲者备注(默认 true)' },
|
|
41
|
+
overwrite: { type: 'boolean', description: '目标已存在时是否覆盖;保存回原文件必须显式 true' },
|
|
42
|
+
backup: { type: 'boolean', description: '覆盖前是否备份为 <name>.bak-<时间>.pptx(默认 true)' },
|
|
43
|
+
},
|
|
44
|
+
additionalProperties: false,
|
|
45
|
+
},
|
|
46
|
+
kind: 'write',
|
|
47
|
+
since: '2.4.0',
|
|
48
|
+
handler: async ({ path: filePath, file_name, slide_indices, include_notes, overwrite, backup }, ctx) => {
|
|
49
|
+
const doc = await ctx.deck.read()
|
|
50
|
+
const deck = doc && doc.deck
|
|
51
|
+
if (!deck || !Array.isArray(deck.slides) || !deck.slides.length) {
|
|
52
|
+
throw new Error('当前 deck 没有幻灯片,无法导出 PPTX')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let exportDeck = deck
|
|
56
|
+
if (Array.isArray(slide_indices) && slide_indices.length) {
|
|
57
|
+
const indices = [...new Set(slide_indices.map((value) => Number(value)))]
|
|
58
|
+
const bad = indices.find((index) => !Number.isInteger(index) || index < 0 || index >= deck.slides.length)
|
|
59
|
+
if (bad !== undefined) throw new Error(`页号越界:${bad}(共 ${deck.slides.length} 页)`)
|
|
60
|
+
exportDeck = { ...deck, slides: indices.map((index) => deck.slides[index]) }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const target = outputPath({ filePath, fileName: file_name, title: deck.title })
|
|
64
|
+
if (fs.existsSync(target) && overwrite !== true) {
|
|
65
|
+
throw new Error('目标文件已存在:保存回原文件请显式传 overwrite:true;否则请换 path 或 file_name 另存')
|
|
66
|
+
}
|
|
67
|
+
const { buffer, stats } = await renderDeckToPptx(exportDeck, {
|
|
68
|
+
assetBase: ctx.status.baseUrl,
|
|
69
|
+
includeNotes: include_notes !== false,
|
|
70
|
+
})
|
|
71
|
+
fs.mkdirSync(path.dirname(target), { recursive: true })
|
|
72
|
+
let backupPath = ''
|
|
73
|
+
if (fs.existsSync(target) && backup !== false) {
|
|
74
|
+
const stamp = new Date().toISOString().replace(/[-:]/g, '').replace(/\..+$/, '')
|
|
75
|
+
backupPath = target.replace(/\.pptx$/i, `.bak-${stamp}.pptx`)
|
|
76
|
+
fs.copyFileSync(target, backupPath)
|
|
77
|
+
}
|
|
78
|
+
fs.writeFileSync(target, buffer)
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
text: JSON.stringify({
|
|
82
|
+
ok: true,
|
|
83
|
+
path: target,
|
|
84
|
+
backup: backupPath,
|
|
85
|
+
bytes: buffer.length,
|
|
86
|
+
title: deck.title || '',
|
|
87
|
+
slides: exportDeck.slides.length,
|
|
88
|
+
stats,
|
|
89
|
+
note: '导出完成。若 stats.skipped/approximated 非 0,说明有元素被跳过或近似,详见 stats.warnings。',
|
|
90
|
+
}, null, 2),
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
}),
|
|
94
|
+
],
|
|
95
|
+
})
|