@capyclaw/ppt-workbench 0.0.0-stage → 2.4.0
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 +51 -4
- 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,58 @@
|
|
|
1
|
+
/** 线条能力:直线/虚线/折线连接(含端点箭头),坐标用画布绝对值,方向免算。 */
|
|
2
|
+
import { clamp, okText } from '../domains/tooling.mjs'
|
|
3
|
+
import { defineCapability, defineTool } from '../kernel/define.mjs'
|
|
4
|
+
|
|
5
|
+
const POINT_STYLES = ['none', 'arrow', 'triangle', 'dot', 'square']
|
|
6
|
+
|
|
7
|
+
const pointOf = (v) => (v && v !== 'none' ? String(v) : '')
|
|
8
|
+
|
|
9
|
+
export const lineCapability = defineCapability({
|
|
10
|
+
id: 'line',
|
|
11
|
+
title: '线条',
|
|
12
|
+
tags: ['write', 'layout'],
|
|
13
|
+
requires: ['deck'],
|
|
14
|
+
features: ['deck.line'],
|
|
15
|
+
tools: [
|
|
16
|
+
defineTool({
|
|
17
|
+
name: 'ppt_line',
|
|
18
|
+
description:
|
|
19
|
+
'画一条线(直线 / 虚线 / 折线)。给起点终点画布坐标即可,包围盒自动计算;' +
|
|
20
|
+
'端点箭头可选 arrow / triangle / dot / square;broken 给拐点画布坐标可画折线。',
|
|
21
|
+
inputSchema: {
|
|
22
|
+
type: 'object',
|
|
23
|
+
properties: {
|
|
24
|
+
slide: { type: 'integer', description: '0 起页号,默认 0' },
|
|
25
|
+
x1: { type: 'number', description: '起点 x(画布坐标)' },
|
|
26
|
+
y1: { type: 'number', description: '起点 y' },
|
|
27
|
+
x2: { type: 'number', description: '终点 x' },
|
|
28
|
+
y2: { type: 'number', description: '终点 y' },
|
|
29
|
+
color: { type: 'string', description: '线色,默认 #5b9bd5' },
|
|
30
|
+
style: { type: 'string', enum: ['solid', 'dashed'], description: '默认 solid' },
|
|
31
|
+
width: { type: 'number', description: '线宽,默认 2' },
|
|
32
|
+
start_point: { type: 'string', enum: POINT_STYLES, description: '起点箭头,默认 none' },
|
|
33
|
+
end_point: { type: 'string', enum: POINT_STYLES, description: '终点箭头,默认 none' },
|
|
34
|
+
broken: { type: 'array', items: { type: 'number' }, description: '折线拐点 [x, y](画布坐标,产生 ┳ 型连接)' },
|
|
35
|
+
},
|
|
36
|
+
required: ['slide', 'x1', 'y1', 'x2', 'y2'],
|
|
37
|
+
additionalProperties: false,
|
|
38
|
+
},
|
|
39
|
+
kind: 'write',
|
|
40
|
+
since: '2.2.0',
|
|
41
|
+
handler: async (args, ctx) => {
|
|
42
|
+
const i = clamp(Number(args.slide) | 0, 0, 999)
|
|
43
|
+
const p = args || {}
|
|
44
|
+
const element = {
|
|
45
|
+
type: 'line',
|
|
46
|
+
x1: p.x1, y1: p.y1, x2: p.x2, y2: p.y2,
|
|
47
|
+
color: p.color || '#5b9bd5',
|
|
48
|
+
style: p.style || 'solid',
|
|
49
|
+
width: p.width != null ? p.width : 2,
|
|
50
|
+
points: [pointOf(p.start_point), pointOf(p.end_point)],
|
|
51
|
+
...(Array.isArray(p.broken) && p.broken.length === 2 ? { broken: p.broken } : {}),
|
|
52
|
+
}
|
|
53
|
+
const r = await ctx.deck.write([{ op: 'add-element', slide: i, element }])
|
|
54
|
+
return okText(r, `已在第 ${i} 页画线`)
|
|
55
|
+
},
|
|
56
|
+
}),
|
|
57
|
+
],
|
|
58
|
+
})
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** 备注能力:演讲者备注(编辑器备注面板直接显示)。 */
|
|
2
|
+
import { clamp, okText } from '../domains/tooling.mjs'
|
|
3
|
+
import { defineCapability, defineTool } from '../kernel/define.mjs'
|
|
4
|
+
|
|
5
|
+
export const notesCapability = defineCapability({
|
|
6
|
+
id: 'notes',
|
|
7
|
+
title: '演讲备注',
|
|
8
|
+
tags: ['write', 'content'],
|
|
9
|
+
requires: ['deck'],
|
|
10
|
+
features: ['deck.notes'],
|
|
11
|
+
tools: [
|
|
12
|
+
defineTool({
|
|
13
|
+
name: 'ppt_notes',
|
|
14
|
+
description: '设置指定页的演讲者备注(显示在编辑器备注面板,导出 PPTX 一并保留)。传空字符串清空。',
|
|
15
|
+
inputSchema: {
|
|
16
|
+
type: 'object',
|
|
17
|
+
properties: {
|
|
18
|
+
slide: { type: 'integer', description: '0 起页号' },
|
|
19
|
+
notes: { type: 'string', maxLength: 2000, description: '备注内容;空字符串清空' },
|
|
20
|
+
},
|
|
21
|
+
required: ['slide', 'notes'],
|
|
22
|
+
additionalProperties: false,
|
|
23
|
+
},
|
|
24
|
+
kind: 'write',
|
|
25
|
+
since: '2.2.0',
|
|
26
|
+
handler: async ({ slide, notes }, ctx) => {
|
|
27
|
+
const i = clamp(Number(slide) | 0, 0, 999)
|
|
28
|
+
const r = await ctx.deck.write([{ op: 'set-notes', slide: i, notes: String(notes || '') }])
|
|
29
|
+
return okText(r, notes ? `已设置第 ${i} 页备注` : `已清空第 ${i} 页备注`)
|
|
30
|
+
},
|
|
31
|
+
}),
|
|
32
|
+
],
|
|
33
|
+
})
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** 放映控制能力。 */
|
|
2
|
+
import { clamp, okText } from '../domains/tooling.mjs'
|
|
3
|
+
import { defineCapability, defineTool } from '../kernel/define.mjs'
|
|
4
|
+
|
|
5
|
+
export const presentCapability = defineCapability({
|
|
6
|
+
id: 'present',
|
|
7
|
+
title: '放映',
|
|
8
|
+
tags: ['control'],
|
|
9
|
+
requires: ['deck'],
|
|
10
|
+
features: ['deck.present'],
|
|
11
|
+
tools: [
|
|
12
|
+
defineTool({
|
|
13
|
+
name: 'ppt_present',
|
|
14
|
+
description: '放映控制:start(可指定起始页)/ exit。',
|
|
15
|
+
inputSchema: {
|
|
16
|
+
type: 'object',
|
|
17
|
+
properties: {
|
|
18
|
+
action: { type: 'string', enum: ['start', 'exit'] },
|
|
19
|
+
slide: { type: 'integer', description: 'start 的起始页(0 起)' },
|
|
20
|
+
},
|
|
21
|
+
required: ['action'],
|
|
22
|
+
additionalProperties: false,
|
|
23
|
+
},
|
|
24
|
+
kind: 'control',
|
|
25
|
+
since: '1.0.0',
|
|
26
|
+
handler: async ({ action, slide }, ctx) => {
|
|
27
|
+
const r = action === 'exit'
|
|
28
|
+
? await ctx.deck.write([{ op: 'exit-present' }])
|
|
29
|
+
: await ctx.deck.write([{ op: 'present', index: slide != null ? clamp(Number(slide) | 0, 0, 999) : undefined }])
|
|
30
|
+
return okText(r, action === 'exit' ? '已退出放映' : '已开始放映')
|
|
31
|
+
},
|
|
32
|
+
}),
|
|
33
|
+
],
|
|
34
|
+
})
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/** 整体换色能力:新编辑器单条 recolor,旧编辑器回退为批量队列。 */
|
|
2
|
+
import { resolveColor } from '../domains/color.mjs'
|
|
3
|
+
import { expandRecolorOps, resolveForeground } from '../domains/recolor.mjs'
|
|
4
|
+
import { normalizeSlides, okText } from '../domains/tooling.mjs'
|
|
5
|
+
import { defineCapability, defineTool } from '../kernel/define.mjs'
|
|
6
|
+
|
|
7
|
+
export const recolorCapability = defineCapability({
|
|
8
|
+
id: 'recolor',
|
|
9
|
+
title: '整体换色',
|
|
10
|
+
tags: ['recipe', 'style'],
|
|
11
|
+
requires: ['deck'],
|
|
12
|
+
features: ['deck.recolor'],
|
|
13
|
+
tools: [
|
|
14
|
+
defineTool({
|
|
15
|
+
name: 'ppt_recolor',
|
|
16
|
+
description: '一键整体换色(一次调用完成):背景 + 全部文字对比色 + 装饰形状(线条/圆点/色条)统一色。改整体背景/配色时优先用它,禁止逐页 ppt_element 改色。',
|
|
17
|
+
inputSchema: {
|
|
18
|
+
type: 'object',
|
|
19
|
+
properties: {
|
|
20
|
+
background: { type: 'string', description: '新背景色(CSS 颜色或中文色名)' },
|
|
21
|
+
text: { type: 'string', description: '文字颜色;缺省按背景自动对比(浅底深字/深底白字)' },
|
|
22
|
+
shapes: { type: 'string', description: '装饰形状填充色;缺省跟随文字色(传 null 表示不改装饰)' },
|
|
23
|
+
slides: { type: 'array', items: { type: 'integer' }, description: '只改这些页;缺省全部' },
|
|
24
|
+
},
|
|
25
|
+
required: ['background'],
|
|
26
|
+
additionalProperties: false,
|
|
27
|
+
},
|
|
28
|
+
kind: 'write',
|
|
29
|
+
since: '1.0.0',
|
|
30
|
+
handler: async ({ background, text, shapes, slides }, ctx) => {
|
|
31
|
+
const bg = resolveColor(background)
|
|
32
|
+
const textColor = text ? resolveColor(text) : ''
|
|
33
|
+
const shapeColor = shapes === undefined ? undefined : (shapes === null ? null : resolveColor(shapes))
|
|
34
|
+
const targets = normalizeSlides(slides)
|
|
35
|
+
|
|
36
|
+
// 1) 新编辑器:单条 recolor 指令(背景+文字+装饰一次完成)
|
|
37
|
+
const cmd = { op: 'recolor', background: bg }
|
|
38
|
+
if (textColor) cmd.text = textColor
|
|
39
|
+
if (shapeColor !== undefined) cmd.shapes = shapeColor
|
|
40
|
+
if (targets) cmd.indices = targets
|
|
41
|
+
const r = await ctx.deck.write([cmd])
|
|
42
|
+
if (r.ok) return okText(r, '整体配色已更新')
|
|
43
|
+
if (!/未知指令/.test(String(r.error || ''))) return okText(r, '整体配色失败')
|
|
44
|
+
|
|
45
|
+
// 2) 旧编辑器兼容:展开为 set-background + set-element 批量队列
|
|
46
|
+
const fg = resolveForeground(bg, textColor)
|
|
47
|
+
const sf = shapeColor === undefined ? fg : shapeColor
|
|
48
|
+
const doc = await ctx.deck.read()
|
|
49
|
+
const ops = expandRecolorOps(doc.deck, { bg, fg, sf, targets })
|
|
50
|
+
let applied = 0
|
|
51
|
+
for (let i = 0; i < ops.length; i += 90) {
|
|
52
|
+
const rr = await ctx.deck.write(ops.slice(i, i + 90))
|
|
53
|
+
if (!rr.ok) return okText(rr, '整体配色失败(兼容模式)')
|
|
54
|
+
applied += rr.applied
|
|
55
|
+
}
|
|
56
|
+
return { text: `整体配色已更新(兼容模式,${applied} 条)` }
|
|
57
|
+
},
|
|
58
|
+
}),
|
|
59
|
+
],
|
|
60
|
+
})
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/** 实时画面能力:唯一依赖编辑器页面在线的能力。 */
|
|
2
|
+
import { sleep } from '../domains/tooling.mjs'
|
|
3
|
+
import { defineCapability, defineTool } from '../kernel/define.mjs'
|
|
4
|
+
|
|
5
|
+
let screenshotChain = Promise.resolve()
|
|
6
|
+
|
|
7
|
+
/* 每次尝试等编辑器回图的时长:两次合计 ~18s + settle 3s ≈ 21s 封顶。
|
|
8
|
+
曾经 45s×2 ≈ 94s——客户端(60s 超时)早已断连,响应写回已销毁 socket
|
|
9
|
+
曾引发进程崩溃(HTTP 层已加断连防护,这里同时改为快速失败)。 */
|
|
10
|
+
export const SHOT_WAIT_DEADLINES_MS = [12000, 6000]
|
|
11
|
+
|
|
12
|
+
async function captureScreenshot(ctx, { deadlines = SHOT_WAIT_DEADLINES_MS } = {}) {
|
|
13
|
+
/* 截图必须在页面内渲染——这是唯一真正依赖面板在线的操作 */
|
|
14
|
+
await ctx.editor.require()
|
|
15
|
+
/* 空 deck 直接拒绝:曾经空 deck 让编辑器渲染空图被 sanitize 拒收,
|
|
16
|
+
页面又已标记 lastShotReq 不再重试 → 服务端 shot_req 永久悬挂。 */
|
|
17
|
+
const deckStatus = await ctx.status.get()
|
|
18
|
+
if (deckStatus.has_deck === false || Number(deckStatus.slides || 0) === 0) {
|
|
19
|
+
throw new Error('当前演示文稿为空(0 页),没有可截取的画面。请先用 ppt_deck / ppt_slide 添加页面。')
|
|
20
|
+
}
|
|
21
|
+
/* 先等编辑器应用完所有已排队指令,避免截到旧状态 */
|
|
22
|
+
const settleDeadline = Date.now() + 3000
|
|
23
|
+
while (Date.now() < settleDeadline) {
|
|
24
|
+
const st = await ctx.status.get()
|
|
25
|
+
if (Number(st.applied_seq || 0) >= Number(st.seq || 0)) break
|
|
26
|
+
await sleep(120)
|
|
27
|
+
}
|
|
28
|
+
let lastError = ''
|
|
29
|
+
for (const waitMs of deadlines) {
|
|
30
|
+
const reqDoc = await ctx.editor.requestShot()
|
|
31
|
+
const req = reqDoc.shot_req
|
|
32
|
+
const deadline = Date.now() + waitMs
|
|
33
|
+
let pending = true
|
|
34
|
+
while (Date.now() < deadline) {
|
|
35
|
+
const st = await ctx.status.get()
|
|
36
|
+
if (st.shot && st.shot.req === req) {
|
|
37
|
+
const shot = await ctx.editor.getShot()
|
|
38
|
+
if (shot.data) {
|
|
39
|
+
const base64 = shot.data.slice(shot.data.indexOf(',') + 1)
|
|
40
|
+
return {
|
|
41
|
+
content: [
|
|
42
|
+
{ type: 'text', text: `以下是右侧编辑器的实时画面(全部页拼图,帧 ${req}):` },
|
|
43
|
+
{ type: 'image', data: base64, mimeType: 'image/jpeg' },
|
|
44
|
+
],
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
lastError = '编辑器渲染失败'
|
|
48
|
+
pending = false
|
|
49
|
+
break
|
|
50
|
+
}
|
|
51
|
+
await sleep(150)
|
|
52
|
+
}
|
|
53
|
+
if (pending) lastError = '等待编辑器渲染超时'
|
|
54
|
+
await sleep(600)
|
|
55
|
+
}
|
|
56
|
+
throw new Error(`截图失败(${lastError}):编辑器面板未打开、在后台被浏览器节流,或页面已关闭。请把编辑器标签页切到前台再重试。`)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const screenshotCapability = defineCapability({
|
|
60
|
+
id: 'screenshot',
|
|
61
|
+
title: '实时画面',
|
|
62
|
+
tags: ['read', 'visual'],
|
|
63
|
+
requires: ['editor', 'status'],
|
|
64
|
+
features: ['deck.screenshot'],
|
|
65
|
+
tools: [
|
|
66
|
+
defineTool({
|
|
67
|
+
name: 'ppt_screenshot',
|
|
68
|
+
description: '取右侧编辑器实时画面(全部页拼图)。确认视觉效果时调用。',
|
|
69
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
70
|
+
kind: 'read',
|
|
71
|
+
since: '1.0.0',
|
|
72
|
+
handler: async (args, ctx) => {
|
|
73
|
+
/* 并发截图串行化:多个工具调用不能互相 reset shot_req。 */
|
|
74
|
+
const previous = screenshotChain
|
|
75
|
+
let release
|
|
76
|
+
screenshotChain = new Promise((resolve) => { release = resolve })
|
|
77
|
+
await previous
|
|
78
|
+
try {
|
|
79
|
+
return await captureScreenshot(ctx)
|
|
80
|
+
} finally {
|
|
81
|
+
release()
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
}),
|
|
85
|
+
],
|
|
86
|
+
})
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/** 形状能力:内置形状库一键插入(矩形/圆/三角/星形/箭头/气泡…),也接受自定义 path。 */
|
|
2
|
+
import { SHAPE_KEYS, getShape } from '../../../core/shape-library.js'
|
|
3
|
+
import { clamp, okText } from '../domains/tooling.mjs'
|
|
4
|
+
import { defineCapability, defineTool } from '../kernel/define.mjs'
|
|
5
|
+
|
|
6
|
+
export const shapeCapability = defineCapability({
|
|
7
|
+
id: 'shape',
|
|
8
|
+
title: '形状库',
|
|
9
|
+
tags: ['write', 'layout'],
|
|
10
|
+
requires: ['deck'],
|
|
11
|
+
features: ['deck.shape'],
|
|
12
|
+
tools: [
|
|
13
|
+
defineTool({
|
|
14
|
+
name: 'ppt_shape',
|
|
15
|
+
description:
|
|
16
|
+
'插入形状。shape 从内置库选(' + SHAPE_KEYS.join(' / ') + '),' +
|
|
17
|
+
'也可传自定义 SVG path + viewBox。装饰、流程节点、强调块都走这里。',
|
|
18
|
+
inputSchema: {
|
|
19
|
+
type: 'object',
|
|
20
|
+
properties: {
|
|
21
|
+
slide: { type: 'integer', description: '0 起页号,默认 0' },
|
|
22
|
+
shape: { type: 'string', description: '内置形状 key,如 circle / arrow-right / star-5 / round-rect' },
|
|
23
|
+
path: { type: 'string', description: '自定义 SVG path(与 shape 二选一,优先生效)' },
|
|
24
|
+
viewBox: { type: 'array', items: { type: 'number' }, description: '自定义 path 的 viewBox [w, h],默认 [200, 200]' },
|
|
25
|
+
fill: { type: 'string', description: '填充色,默认 #5b9bd5' },
|
|
26
|
+
left: { type: 'number' },
|
|
27
|
+
top: { type: 'number' },
|
|
28
|
+
width: { type: 'number', description: '默认 200' },
|
|
29
|
+
height: { type: 'number', description: '默认 200' },
|
|
30
|
+
rotate: { type: 'number' },
|
|
31
|
+
},
|
|
32
|
+
required: ['slide'],
|
|
33
|
+
additionalProperties: false,
|
|
34
|
+
},
|
|
35
|
+
kind: 'write',
|
|
36
|
+
since: '2.2.0',
|
|
37
|
+
handler: async (args, ctx) => {
|
|
38
|
+
const i = clamp(Number(args.slide) | 0, 0, 999)
|
|
39
|
+
const p = args || {}
|
|
40
|
+
let path = p.path
|
|
41
|
+
let viewBox = Array.isArray(p.viewBox) && p.viewBox.length === 2 ? p.viewBox : [200, 200]
|
|
42
|
+
if (!path) {
|
|
43
|
+
if (!p.shape) throw new Error('需要 shape(内置形状 key)或 path(自定义 SVG)')
|
|
44
|
+
const s = getShape(p.shape)
|
|
45
|
+
path = s.path
|
|
46
|
+
viewBox = s.viewBox
|
|
47
|
+
}
|
|
48
|
+
const element = {
|
|
49
|
+
type: 'shape',
|
|
50
|
+
path,
|
|
51
|
+
viewBox,
|
|
52
|
+
fill: p.fill || '#5b9bd5',
|
|
53
|
+
left: p.left != null ? p.left : 60,
|
|
54
|
+
top: p.top != null ? p.top : 120,
|
|
55
|
+
width: p.width != null ? p.width : 200,
|
|
56
|
+
height: p.height != null ? p.height : 200,
|
|
57
|
+
...(p.rotate != null ? { rotate: p.rotate } : {}),
|
|
58
|
+
}
|
|
59
|
+
const r = await ctx.deck.write([{ op: 'add-element', slide: i, element }])
|
|
60
|
+
return okText(r, `已在第 ${i} 页插入形状${p.shape ? ' ' + p.shape : ''}`)
|
|
61
|
+
},
|
|
62
|
+
}),
|
|
63
|
+
],
|
|
64
|
+
})
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/** 页面管理能力。 */
|
|
2
|
+
import { simpleSlide } from '../domains/slide.mjs'
|
|
3
|
+
import { okText } from '../domains/tooling.mjs'
|
|
4
|
+
import { defineCapability, defineTool } from '../kernel/define.mjs'
|
|
5
|
+
|
|
6
|
+
export const slideCapability = defineCapability({
|
|
7
|
+
id: 'slide',
|
|
8
|
+
title: '页面管理',
|
|
9
|
+
tags: ['write', 'structure'],
|
|
10
|
+
requires: ['deck'],
|
|
11
|
+
features: ['deck.write'],
|
|
12
|
+
tools: [
|
|
13
|
+
defineTool({
|
|
14
|
+
name: 'ppt_slide',
|
|
15
|
+
description: '页管理:add(标题+要点)/remove/move/duplicate/select。',
|
|
16
|
+
inputSchema: {
|
|
17
|
+
type: 'object',
|
|
18
|
+
properties: {
|
|
19
|
+
action: { type: 'string', enum: ['add', 'remove', 'move', 'duplicate', 'select'] },
|
|
20
|
+
index: { type: 'integer', description: '目标页号(0 起);add 时为插入位置,缺省末尾' },
|
|
21
|
+
to: { type: 'integer', description: 'move 的目标位置' },
|
|
22
|
+
title: { type: 'string', maxLength: 40, description: 'add 的标题' },
|
|
23
|
+
bullets: { type: 'array', maxItems: 8, items: { type: 'string', maxLength: 80 } },
|
|
24
|
+
background: { type: 'string' },
|
|
25
|
+
},
|
|
26
|
+
required: ['action'],
|
|
27
|
+
additionalProperties: false,
|
|
28
|
+
},
|
|
29
|
+
kind: 'write',
|
|
30
|
+
since: '1.0.0',
|
|
31
|
+
handler: async ({ action, index, to, title, bullets, background }, ctx) => {
|
|
32
|
+
const slideCount = async () => {
|
|
33
|
+
const doc = await ctx.deck.read()
|
|
34
|
+
return doc.deck && Array.isArray(doc.deck.slides) ? doc.deck.slides.length : 0
|
|
35
|
+
}
|
|
36
|
+
const requirePage = (value, label = 'index') => {
|
|
37
|
+
const n = Number(value)
|
|
38
|
+
if (!Number.isInteger(n) || n < 0) throw new Error(`ppt_slide ${action} 需要 ${label}(0 起整数)`)
|
|
39
|
+
return n
|
|
40
|
+
}
|
|
41
|
+
if (action === 'add') {
|
|
42
|
+
const count = await slideCount()
|
|
43
|
+
let at = index != null ? Math.min(requirePage(index), count) : count
|
|
44
|
+
const r = await ctx.deck.write([{ op: 'add-slide', slide: simpleSlide(title || '新一页', bullets, background), index: at }])
|
|
45
|
+
return okText(r, `已加一页「${title || '新一页'}」`)
|
|
46
|
+
}
|
|
47
|
+
if (action === 'remove') {
|
|
48
|
+
const count = await slideCount()
|
|
49
|
+
const i = requirePage(index)
|
|
50
|
+
if (i >= count) throw new Error(`页号越界:${i}(共 ${count} 页)`)
|
|
51
|
+
const r = await ctx.deck.write([{ op: 'remove-slide', id: i }])
|
|
52
|
+
return okText(r, `已删除第 ${i} 页`)
|
|
53
|
+
}
|
|
54
|
+
if (action === 'move') {
|
|
55
|
+
const count = await slideCount()
|
|
56
|
+
const from = requirePage(index)
|
|
57
|
+
const target = requirePage(to, 'to')
|
|
58
|
+
if (from >= count) throw new Error(`页号越界:${from}(共 ${count} 页)`)
|
|
59
|
+
if (target >= count) throw new Error(`目标页号越界:${target}(共 ${count} 页)`)
|
|
60
|
+
const r = await ctx.deck.write([{ op: 'move-slide', from, to: target }])
|
|
61
|
+
return okText(r, `已移动第 ${from} 页到 ${target}`)
|
|
62
|
+
}
|
|
63
|
+
if (action === 'duplicate') {
|
|
64
|
+
const count = await slideCount()
|
|
65
|
+
const i = requirePage(index)
|
|
66
|
+
if (i >= count) throw new Error(`页号越界:${i}(共 ${count} 页)`)
|
|
67
|
+
const r = await ctx.deck.write([{ op: 'duplicate-slide', id: i }])
|
|
68
|
+
return okText(r, `已复制第 ${i} 页`)
|
|
69
|
+
}
|
|
70
|
+
if (action === 'select') {
|
|
71
|
+
const count = await slideCount()
|
|
72
|
+
const i = requirePage(index)
|
|
73
|
+
if (i >= count) throw new Error(`页号越界:${i}(共 ${count} 页)`)
|
|
74
|
+
const r = await ctx.deck.write([{ op: 'select', id: i }])
|
|
75
|
+
return okText(r, `已选中第 ${i} 页`)
|
|
76
|
+
}
|
|
77
|
+
throw new Error('未知 action: ' + action)
|
|
78
|
+
},
|
|
79
|
+
}),
|
|
80
|
+
],
|
|
81
|
+
})
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/** 样式能力:背景色、文字颜色、字体名、主题色。 */
|
|
2
|
+
import { resolveColor } from '../domains/color.mjs'
|
|
3
|
+
import { clamp, normalizeSlides, okText } from '../domains/tooling.mjs'
|
|
4
|
+
import { defineCapability, defineTool } from '../kernel/define.mjs'
|
|
5
|
+
|
|
6
|
+
export const styleCapability = defineCapability({
|
|
7
|
+
id: 'style',
|
|
8
|
+
title: '主题与背景',
|
|
9
|
+
tags: ['write', 'style'],
|
|
10
|
+
requires: ['deck'],
|
|
11
|
+
features: ['deck.write'],
|
|
12
|
+
tools: [
|
|
13
|
+
defineTool({
|
|
14
|
+
name: 'ppt_style',
|
|
15
|
+
description: '整体样式:背景色(可指定页),文字颜色,字体名,主题色数组。浅底会自动配深字、深底配白字。',
|
|
16
|
+
inputSchema: {
|
|
17
|
+
type: 'object',
|
|
18
|
+
properties: {
|
|
19
|
+
background: { type: 'string', description: '背景色(CSS 或中文色名)' },
|
|
20
|
+
slides: { type: 'array', items: { type: 'integer' }, description: '只改这些页的背景;缺省全部' },
|
|
21
|
+
contrast: { type: 'boolean', description: '自动对比度,缺省 true' },
|
|
22
|
+
text_color: { type: 'string', description: '全局正文颜色' },
|
|
23
|
+
font_name: { type: 'string' },
|
|
24
|
+
theme_colors: { type: 'array', items: { type: 'string' }, description: '主题色数组' },
|
|
25
|
+
},
|
|
26
|
+
additionalProperties: false,
|
|
27
|
+
},
|
|
28
|
+
kind: 'write',
|
|
29
|
+
since: '1.0.0',
|
|
30
|
+
handler: async ({ background, slides, contrast, text_color, font_name, theme_colors }, ctx) => {
|
|
31
|
+
const ops = []
|
|
32
|
+
if (background) {
|
|
33
|
+
const cmd = { op: 'set-background', color: resolveColor(background), contrast: contrast !== false }
|
|
34
|
+
const targets = normalizeSlides(slides)
|
|
35
|
+
if (targets) cmd.indices = targets
|
|
36
|
+
ops.push(cmd)
|
|
37
|
+
}
|
|
38
|
+
const theme = {}
|
|
39
|
+
if (text_color) theme.fontColor = resolveColor(text_color)
|
|
40
|
+
if (font_name) theme.fontName = String(font_name)
|
|
41
|
+
if (Array.isArray(theme_colors) && theme_colors.length) theme.themeColors = theme_colors.map(resolveColor)
|
|
42
|
+
if (Object.keys(theme).length) ops.push({ op: 'theme', theme })
|
|
43
|
+
const r = await ctx.deck.write(ops)
|
|
44
|
+
return okText(r, '样式已更新')
|
|
45
|
+
},
|
|
46
|
+
}),
|
|
47
|
+
],
|
|
48
|
+
})
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/** 表格能力:把二维数据直接变成 PPTist 原生表格,支持表头样式与单元格精修。 */
|
|
2
|
+
import { clamp, okText } from '../domains/tooling.mjs'
|
|
3
|
+
import { defineCapability, defineTool } from '../kernel/define.mjs'
|
|
4
|
+
|
|
5
|
+
export const tableCapability = defineCapability({
|
|
6
|
+
id: 'table',
|
|
7
|
+
title: '表格',
|
|
8
|
+
tags: ['write', 'data'],
|
|
9
|
+
requires: ['deck'],
|
|
10
|
+
features: ['deck.table'],
|
|
11
|
+
tools: [
|
|
12
|
+
defineTool({
|
|
13
|
+
name: 'ppt_table',
|
|
14
|
+
description:
|
|
15
|
+
'插入或整体更新表格(PPTist 原生表格)。rows 为二维数组,字符串/数字直接作单元格;' +
|
|
16
|
+
'header: true 时首行渲染为加粗底色表头。带 element_id 时整体替换该表格数据。',
|
|
17
|
+
inputSchema: {
|
|
18
|
+
type: 'object',
|
|
19
|
+
properties: {
|
|
20
|
+
slide: { type: 'integer', description: '0 起页号,默认 0' },
|
|
21
|
+
rows: {
|
|
22
|
+
type: 'array',
|
|
23
|
+
description: '二维数组,如 [["区域","目标"],["华东",70],["华南",66]];单元格也可用对象 {text, bold, color, backcolor, align, fontsize}',
|
|
24
|
+
items: { type: 'array' },
|
|
25
|
+
},
|
|
26
|
+
header: { type: 'boolean', description: '首行作为表头(加粗 + 主题色浅底)' },
|
|
27
|
+
theme_color: { type: 'string', description: '表格主题色(表头底色由此淡化生成),默认 #5b9bd5' },
|
|
28
|
+
element_id: { type: 'string', description: '更新模式:整体替换该表格的数据/列宽' },
|
|
29
|
+
col_widths: { type: 'array', items: { type: 'number' }, description: '列宽比例或像素(自动归一)' },
|
|
30
|
+
cell_min_height: { type: 'number', description: '单元格最小高度,默认 36' },
|
|
31
|
+
left: { type: 'number' },
|
|
32
|
+
top: { type: 'number' },
|
|
33
|
+
width: { type: 'number' },
|
|
34
|
+
height: { type: 'number' },
|
|
35
|
+
},
|
|
36
|
+
required: ['slide', 'rows'],
|
|
37
|
+
additionalProperties: false,
|
|
38
|
+
},
|
|
39
|
+
kind: 'write',
|
|
40
|
+
since: '2.2.0',
|
|
41
|
+
handler: async (args, ctx) => {
|
|
42
|
+
const i = clamp(Number(args.slide) | 0, 0, 999)
|
|
43
|
+
const p = args || {}
|
|
44
|
+
if (p.element_id) {
|
|
45
|
+
const props = {}
|
|
46
|
+
if (p.rows != null) {
|
|
47
|
+
// 整体替换:重建 data,保留原主题色
|
|
48
|
+
const element = { rows: p.rows, header: Boolean(p.header), themeColor: p.theme_color }
|
|
49
|
+
const r0 = await ctx.deck.write([{ op: 'remove-element', slide: i, elementId: String(p.element_id) }])
|
|
50
|
+
if (!r0.ok) return { text: `替换失败:${r0.error}` }
|
|
51
|
+
const r = await ctx.deck.write([{ op: 'add-table', slide: i, element }])
|
|
52
|
+
return okText(r, `已重建表格 ${p.element_id}`)
|
|
53
|
+
}
|
|
54
|
+
if (p.theme_color) props.themeColor = p.theme_color
|
|
55
|
+
if (p.header != null) props.rowHeader = Boolean(p.header)
|
|
56
|
+
if (Array.isArray(p.col_widths)) props.colWidths = p.col_widths
|
|
57
|
+
for (const k of ['left', 'top', 'width', 'height']) {
|
|
58
|
+
if (p[k] != null) props[k] = p[k]
|
|
59
|
+
}
|
|
60
|
+
const r = await ctx.deck.write([{ op: 'update-table', slide: i, elementId: String(p.element_id), props }])
|
|
61
|
+
return okText(r, `已更新表格 ${p.element_id}`)
|
|
62
|
+
}
|
|
63
|
+
const element = {
|
|
64
|
+
rows: p.rows,
|
|
65
|
+
header: Boolean(p.header),
|
|
66
|
+
...(p.theme_color ? { themeColor: p.theme_color } : {}),
|
|
67
|
+
...(Array.isArray(p.col_widths) ? { colWidths: p.col_widths } : {}),
|
|
68
|
+
...(p.cell_min_height != null ? { cellMinHeight: p.cell_min_height } : {}),
|
|
69
|
+
}
|
|
70
|
+
for (const k of ['left', 'top', 'width', 'height']) {
|
|
71
|
+
if (p[k] != null) element[k] = p[k]
|
|
72
|
+
}
|
|
73
|
+
const r = await ctx.deck.write([{ op: 'add-table', slide: i, element }])
|
|
74
|
+
return okText(r, `已在第 ${i} 页插入表格(${p.rows.length} 行)`)
|
|
75
|
+
},
|
|
76
|
+
}),
|
|
77
|
+
|
|
78
|
+
defineTool({
|
|
79
|
+
name: 'ppt_table_cells',
|
|
80
|
+
description:
|
|
81
|
+
'精修已有表格的单元格:改文字 / 加粗 / 颜色 / 底色 / 对齐 / 字号,也可换表格主题色。' +
|
|
82
|
+
'行号列号从 0 起,元素 id 可用 ppt_get_deck 查询。',
|
|
83
|
+
inputSchema: {
|
|
84
|
+
type: 'object',
|
|
85
|
+
properties: {
|
|
86
|
+
slide: { type: 'integer', description: '0 起页号,默认 0' },
|
|
87
|
+
element_id: { type: 'string', description: '表格元素 id' },
|
|
88
|
+
cells: {
|
|
89
|
+
type: 'array',
|
|
90
|
+
description: '单元格补丁列表',
|
|
91
|
+
items: {
|
|
92
|
+
type: 'object',
|
|
93
|
+
properties: {
|
|
94
|
+
row: { type: 'integer', description: '0 起行号' },
|
|
95
|
+
col: { type: 'integer', description: '0 起列号' },
|
|
96
|
+
text: { type: 'string' },
|
|
97
|
+
bold: { type: 'boolean' },
|
|
98
|
+
color: { type: 'string', description: '文字颜色' },
|
|
99
|
+
backcolor: { type: 'string', description: '单元格底色' },
|
|
100
|
+
align: { type: 'string', enum: ['left', 'center', 'right'] },
|
|
101
|
+
fontsize: { type: 'number' },
|
|
102
|
+
},
|
|
103
|
+
required: ['row', 'col'],
|
|
104
|
+
additionalProperties: false,
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
theme_color: { type: 'string', description: '顺带更换表格主题色' },
|
|
108
|
+
},
|
|
109
|
+
required: ['slide', 'element_id', 'cells'],
|
|
110
|
+
additionalProperties: false,
|
|
111
|
+
},
|
|
112
|
+
kind: 'write',
|
|
113
|
+
since: '2.2.0',
|
|
114
|
+
handler: async ({ slide, element_id, cells, theme_color }, ctx) => {
|
|
115
|
+
const i = clamp(Number(slide) | 0, 0, 999)
|
|
116
|
+
const r = await ctx.deck.write([{
|
|
117
|
+
op: 'set-table-cells',
|
|
118
|
+
slide: i,
|
|
119
|
+
elementId: String(element_id),
|
|
120
|
+
cells,
|
|
121
|
+
...(theme_color ? { themeColor: theme_color } : {}),
|
|
122
|
+
}])
|
|
123
|
+
return okText(r, `已更新表格 ${element_id} 的 ${Array.isArray(cells) ? cells.length : 0} 个单元格`)
|
|
124
|
+
},
|
|
125
|
+
}),
|
|
126
|
+
],
|
|
127
|
+
})
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** 任务恢复能力:status / list / checkpoint / restore。 */
|
|
2
|
+
import { okText } from '../domains/tooling.mjs'
|
|
3
|
+
import { defineCapability, defineTool } from '../kernel/define.mjs'
|
|
4
|
+
|
|
5
|
+
export const taskCapability = defineCapability({
|
|
6
|
+
id: 'task',
|
|
7
|
+
title: '任务恢复',
|
|
8
|
+
tags: ['state'],
|
|
9
|
+
requires: ['deck', 'task'],
|
|
10
|
+
features: ['deck.task'],
|
|
11
|
+
tools: [
|
|
12
|
+
defineTool({
|
|
13
|
+
name: 'ppt_task',
|
|
14
|
+
description: '任务恢复:status 查当前进度/待处理批次/最近 checkpoint;list 列 checkpoint;checkpoint 保存命名快照;restore 恢复快照。长任务每阶段保存一次。',
|
|
15
|
+
inputSchema: {
|
|
16
|
+
type: 'object',
|
|
17
|
+
properties: {
|
|
18
|
+
action: { type: 'string', enum: ['status', 'list', 'checkpoint', 'restore'] },
|
|
19
|
+
name: { type: 'string', maxLength: 64, description: 'checkpoint 名称(字母/数字/._-)' },
|
|
20
|
+
meta: { type: 'object', description: 'checkpoint 附带的 goal/plan/next 等元数据' },
|
|
21
|
+
},
|
|
22
|
+
required: ['action'],
|
|
23
|
+
additionalProperties: false,
|
|
24
|
+
},
|
|
25
|
+
kind: 'read',
|
|
26
|
+
since: '2.2.0',
|
|
27
|
+
handler: async ({ action, name, meta }, ctx) => {
|
|
28
|
+
if (action === 'status') {
|
|
29
|
+
const status = await ctx.task.status()
|
|
30
|
+
return { text: JSON.stringify(status, null, 2) }
|
|
31
|
+
}
|
|
32
|
+
if (action === 'list') {
|
|
33
|
+
const data = await ctx.task.list()
|
|
34
|
+
return { text: JSON.stringify(data, null, 2) }
|
|
35
|
+
}
|
|
36
|
+
if (action === 'checkpoint') {
|
|
37
|
+
if (!name) throw new Error('checkpoint 需要 name')
|
|
38
|
+
const saved = await ctx.task.checkpoint(name, meta || {})
|
|
39
|
+
return { text: `已保存 checkpoint ${saved.name}(seq ${saved.seq},${saved.slides} 页)` }
|
|
40
|
+
}
|
|
41
|
+
if (action === 'restore') {
|
|
42
|
+
if (!name) throw new Error('restore 需要 name')
|
|
43
|
+
const saved = await ctx.task.read(name)
|
|
44
|
+
if (!saved || !saved.deck) throw new Error('checkpoint 不存在或内容损坏: ' + name)
|
|
45
|
+
const result = await ctx.deck.write([{ op: 'load', deck: saved.deck }])
|
|
46
|
+
return okText(result, `已恢复 checkpoint ${name}(${saved.slides} 页)`)
|
|
47
|
+
}
|
|
48
|
+
throw new Error('未知 action: ' + action)
|
|
49
|
+
},
|
|
50
|
+
}),
|
|
51
|
+
],
|
|
52
|
+
})
|