@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,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* server instructions:agent 拿到的「怎么用这套工具」说明书(≤32KB 硬限)。
|
|
3
|
+
* 排版由 dsh-tool-host 的 buildInstructions 统一生成,本文件只放 PPT 文案。
|
|
4
|
+
*/
|
|
5
|
+
import { buildInstructions } from '../shared/tool-host.mjs'
|
|
6
|
+
import { CONTRACT_VERSION } from '../contract.mjs'
|
|
7
|
+
|
|
8
|
+
export function serverInstructions(editorUrl, { tools = [], capabilities = [] } = {}) {
|
|
9
|
+
const major = String(CONTRACT_VERSION).split('.')[0]
|
|
10
|
+
const caps = capabilities.length
|
|
11
|
+
? capabilities
|
|
12
|
+
: [{ title: '工具', description: '', tools: tools.map((tool) => tool.name) }]
|
|
13
|
+
|
|
14
|
+
return buildInstructions({
|
|
15
|
+
title: 'PPTist 工作台',
|
|
16
|
+
intro: '你负责通过 `mcp__ppt__*` 工具编辑右侧实时 PPTist 编辑器。',
|
|
17
|
+
startup: [
|
|
18
|
+
`首次操作前调用 \`ppt_capabilities\`,确认 \`contractVersion\` 为 ${major}.x;工具缺失或 major 不一致时停止,提示用户安装/升级 \`dsh-ppt-suite\`。`,
|
|
19
|
+
'若本会话提供对应 skill,先加载:`pptmaster-narrative`、`pptmaster-design`、`pptist-api`、`pptist-authoring`、`pptist-editing`、`pptist-visual-check`、`pptist-research`;没有 skill 时按本页规则执行。',
|
|
20
|
+
],
|
|
21
|
+
capabilities: caps,
|
|
22
|
+
rules: [
|
|
23
|
+
'用户说“背景/页面/幻灯片/标题”指右侧编辑器,不是网页或文件。',
|
|
24
|
+
'新建 deck 默认用 `ppt_deck` + `preset:"clean"` 出简洁稿,不叠装饰形状。',
|
|
25
|
+
'改现有 deck 只执行用户点名的修改;不主动审美、审计、截图,不扩大范围。',
|
|
26
|
+
'改整体背景/配色只用 `ppt_recolor` 一次完成;禁止逐页 `ppt_element` 改色。',
|
|
27
|
+
'改文字/加页/改背景不要先读整个 deck;只有定位具体元素时才 `ppt_get_deck({detailed:true})`,一次即可。',
|
|
28
|
+
'图片用 `ppt_image`(attachment_id/path/src),不要用 shell 找图,不要用 `ppt_element` 传本机路径。',
|
|
29
|
+
'单次工具参数不要过大:`ppt_deck` 一次不超过约 20 页;超过时先成稿骨架,再用 `ppt_fill` / `ppt_text` 分批补充,避免 tool input JSON 解析失败。',
|
|
30
|
+
'不主动截图;用户要求看效果或确实不确定版面时才 `ppt_screenshot`。',
|
|
31
|
+
'工具返回已含回执;改完一句话回复,不要重复验证。',
|
|
32
|
+
'长任务每阶段用 `ppt_task` checkpoint;新会话先 status,必要时 restore。',
|
|
33
|
+
`编辑器未连接时,提示用户打开 ${editorUrl}(页面加载后自动连接),不要循环重试。`,
|
|
34
|
+
],
|
|
35
|
+
output: [
|
|
36
|
+
'交付时说明页数、每章要点、是否已截图确认。',
|
|
37
|
+
'不贴 JSON、不列工具调用日志。',
|
|
38
|
+
],
|
|
39
|
+
})
|
|
40
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ppt-shim.mjs — 服务端 PptApp 数据执行器(纯 JSON,无 DOM)。
|
|
3
|
+
*
|
|
4
|
+
* 架构定位(2026-10-30 解耦批):
|
|
5
|
+
* deck 数据的权威保存在 relay 进程(store.doc.deck)。
|
|
6
|
+
* MCP 工具的 ops 在服务端由本 shim 直接执行——**不依赖任何编辑器页面在线**。
|
|
7
|
+
* 编辑器页面(无论侧边栏还是独立标签)只是视图:watch 到指令版本变化后
|
|
8
|
+
* 镜像 load 最新 deck;用户手动编辑通过 pushState 镜像回服务端。
|
|
9
|
+
*
|
|
10
|
+
* 实现 core/ops.js 定义的 P 接口子集(数据类):
|
|
11
|
+
* isMounted / getSlides / updateSlide / load / setTitle / setTheme / getTheme
|
|
12
|
+
* addSlide / removeSlide / moveSlide / duplicateSlide / selectSlide
|
|
13
|
+
* snapshot / undo / redo / present / exitPresent
|
|
14
|
+
* undo/redo 在服务端维护 50 步 deck 历史;present/select 仍是页面概念。
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const emptyDeck = () => ({
|
|
18
|
+
title: '未命名演示文稿',
|
|
19
|
+
slides: [],
|
|
20
|
+
theme: {
|
|
21
|
+
themeColors: ['#2f6feb', '#f0a030', '#1f2d3d'],
|
|
22
|
+
fontColor: '#1f2d3d',
|
|
23
|
+
fontName: '',
|
|
24
|
+
backgroundColor: '#ffffff',
|
|
25
|
+
},
|
|
26
|
+
viewportSize: 1000,
|
|
27
|
+
viewportRatio: 0.5625,
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
let dupCounter = 0
|
|
31
|
+
|
|
32
|
+
export function createJsonPptApp(store) {
|
|
33
|
+
const deckOf = () => {
|
|
34
|
+
if (!store.doc.deck || !Array.isArray(store.doc.deck.slides)) {
|
|
35
|
+
store.doc.deck = emptyDeck()
|
|
36
|
+
}
|
|
37
|
+
return store.doc.deck
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const past = []
|
|
41
|
+
const future = []
|
|
42
|
+
const cloneDeck = () => JSON.parse(JSON.stringify(deckOf()))
|
|
43
|
+
const remember = () => {
|
|
44
|
+
past.push(cloneDeck())
|
|
45
|
+
if (past.length > 50) past.shift()
|
|
46
|
+
future.length = 0
|
|
47
|
+
}
|
|
48
|
+
const restore = (deck) => {
|
|
49
|
+
store.doc.deck = deck
|
|
50
|
+
store.doc.updated = Date.now() / 1000
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const P = {
|
|
54
|
+
isMounted() { return true },
|
|
55
|
+
|
|
56
|
+
getSlides() { return deckOf().slides },
|
|
57
|
+
|
|
58
|
+
/** PPTist updateSlide(index, slide) → bool */
|
|
59
|
+
updateSlide(i, slide) {
|
|
60
|
+
const slides = deckOf().slides
|
|
61
|
+
const k = Number(i) | 0
|
|
62
|
+
if (k < 0 || k >= slides.length) return false
|
|
63
|
+
slides[k] = slide
|
|
64
|
+
return true
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
/** 替换 title + slides + 画布设定(checkpoint 恢复必须连 theme 一起还原)。 */
|
|
68
|
+
load(deck) {
|
|
69
|
+
const d = deckOf()
|
|
70
|
+
if (deck && Array.isArray(deck.slides)) d.slides = JSON.parse(JSON.stringify(deck.slides))
|
|
71
|
+
if (deck && deck.title != null) d.title = String(deck.title)
|
|
72
|
+
for (const key of ['theme', 'viewportSize', 'viewportRatio']) {
|
|
73
|
+
if (deck && deck[key] != null) d[key] = JSON.parse(JSON.stringify(deck[key]))
|
|
74
|
+
}
|
|
75
|
+
return true
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
setTitle(t) { deckOf().title = String(t || '') },
|
|
79
|
+
|
|
80
|
+
getTheme() { return deckOf().theme || {} },
|
|
81
|
+
|
|
82
|
+
setTheme(t) {
|
|
83
|
+
const d = deckOf()
|
|
84
|
+
d.theme = { ...(d.theme || {}), ...(t || {}) }
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
addSlide(slide, index) {
|
|
88
|
+
const d = deckOf()
|
|
89
|
+
const s = (slide && (slide.elements || slide.background || slide.id))
|
|
90
|
+
? JSON.parse(JSON.stringify(slide))
|
|
91
|
+
: { id: 'slide_' + Date.now() + '_' + Math.floor(Math.random() * 1e3), elements: [] }
|
|
92
|
+
if (!Array.isArray(s.elements)) s.elements = []
|
|
93
|
+
if (index != null) {
|
|
94
|
+
const k = Math.min(Math.max(Number(index) | 0, 0), d.slides.length)
|
|
95
|
+
d.slides.splice(k, 0, s)
|
|
96
|
+
} else {
|
|
97
|
+
d.slides.push(s)
|
|
98
|
+
}
|
|
99
|
+
return true
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
removeSlide(i) {
|
|
103
|
+
const slides = deckOf().slides
|
|
104
|
+
const k = Number(i) | 0
|
|
105
|
+
if (k < 0 || k >= slides.length) return false
|
|
106
|
+
slides.splice(k, 1)
|
|
107
|
+
return true
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
moveSlide(from, to) {
|
|
111
|
+
const slides = deckOf().slides
|
|
112
|
+
const a = Number(from) | 0
|
|
113
|
+
const b = Number(to) | 0
|
|
114
|
+
if (a < 0 || a >= slides.length || b < 0 || b > slides.length) return false
|
|
115
|
+
const [m] = slides.splice(a, 1)
|
|
116
|
+
slides.splice(b > a ? b - 1 : b, 0, m)
|
|
117
|
+
return true
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
duplicateSlide(i) {
|
|
121
|
+
const slides = deckOf().slides
|
|
122
|
+
const k = Number(i) | 0
|
|
123
|
+
if (k < 0 || k >= slides.length) return false
|
|
124
|
+
dupCounter += 1
|
|
125
|
+
const copy = JSON.parse(JSON.stringify(slides[k]))
|
|
126
|
+
copy.id = (copy.id || 'slide') + '_d' + dupCounter
|
|
127
|
+
;(copy.elements || []).forEach(e => { e.id = (e.id || 'el') + '_d' + dupCounter })
|
|
128
|
+
slides.splice(k + 1, 0, copy)
|
|
129
|
+
return true
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
/** 每个会改 deck 的 op 执行前调用。 */
|
|
133
|
+
snapshot: remember,
|
|
134
|
+
|
|
135
|
+
undo() {
|
|
136
|
+
if (!past.length) return false
|
|
137
|
+
future.push(cloneDeck())
|
|
138
|
+
restore(past.pop())
|
|
139
|
+
return true
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
redo() {
|
|
143
|
+
if (!future.length) return false
|
|
144
|
+
past.push(cloneDeck())
|
|
145
|
+
restore(future.pop())
|
|
146
|
+
return true
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
/* 页面概念:服务端 no-op */
|
|
150
|
+
selectSlide() { return true },
|
|
151
|
+
present() { return true },
|
|
152
|
+
exitPresent() { return true },
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return P
|
|
156
|
+
}
|
package/server/relay.mjs
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 桥 relay 组合根:
|
|
3
|
+
* 1) 桥端点(/api/desktop/works/ppt-bridge…)→ routes/bridge.mjs
|
|
4
|
+
* 2) MCP Streamable HTTP(/mcp)→ mcp/handler.mjs
|
|
5
|
+
* 3) 编辑器静态页(/、/app.js、/ppt-full-app.js)→ routes/editor.mjs
|
|
6
|
+
*
|
|
7
|
+
* 通用宿主(端口/锁/复用/健康/指标/静态/关闭)由 dsh-service-host 提供;
|
|
8
|
+
* 本文件只保留 PPT 专属装配。
|
|
9
|
+
*
|
|
10
|
+
* 也可脱离 dsh 独立运行:node server/relay.mjs [--port 39311] [--dir <hostDir>]
|
|
11
|
+
*/
|
|
12
|
+
import fs from 'node:fs'
|
|
13
|
+
import path from 'node:path'
|
|
14
|
+
import { fileURLToPath } from 'node:url'
|
|
15
|
+
import { BridgeStore } from './store.mjs'
|
|
16
|
+
import { createMcpHandler, serverInstructions } from './mcp-http.mjs'
|
|
17
|
+
import { applyOp } from '../core/ops.js'
|
|
18
|
+
import { createJsonPptApp } from './ppt-shim.mjs'
|
|
19
|
+
import { CONTRACT_VERSION, PACKAGE_VERSION } from './contract.mjs'
|
|
20
|
+
import { BASE_PATH, DEFAULT_PORT } from './constants.mjs'
|
|
21
|
+
import { createAssetRoutes } from './routes/assets.mjs'
|
|
22
|
+
import { createFileAssetStore } from './shared/asset-service.mjs'
|
|
23
|
+
import { createEditorRoutes } from './routes/editor.mjs'
|
|
24
|
+
import { createMcpRoutes } from './routes/mcp.mjs'
|
|
25
|
+
import { createImportRoutes } from './routes/import.mjs'
|
|
26
|
+
import { createBridgeRoutes } from './routes/bridge.mjs'
|
|
27
|
+
import { replayPending } from './runtime/recovery.mjs'
|
|
28
|
+
import { createServiceApp } from './shared/service-host.mjs'
|
|
29
|
+
import { inspectContractSnapshots, describeMismatch } from './contract-snapshots.mjs'
|
|
30
|
+
import { getTuning } from './tuning.mjs'
|
|
31
|
+
import { createRuntimeStoreClient, loadRuntimeContext } from './infra/runtime/context.mjs'
|
|
32
|
+
import { WorkspaceBackend } from './infra/workspace/manager.mjs'
|
|
33
|
+
|
|
34
|
+
export { DEFAULT_PORT, BASE_PATH }
|
|
35
|
+
|
|
36
|
+
const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
37
|
+
|
|
38
|
+
export async function startRelay({
|
|
39
|
+
port = DEFAULT_PORT,
|
|
40
|
+
hostDir = process.env.PPT_BRIDGE_HOME
|
|
41
|
+
|| path.join(process.env.USERPROFILE || process.env.HOME || '.', '.dsh-ppt-bridge'),
|
|
42
|
+
packageDir = PKG_ROOT,
|
|
43
|
+
scanLimit = 50,
|
|
44
|
+
lockFile,
|
|
45
|
+
strictPort = false,
|
|
46
|
+
allowReuse = true,
|
|
47
|
+
backend = null,
|
|
48
|
+
assetStore = null,
|
|
49
|
+
assetService = null,
|
|
50
|
+
capabilities = null,
|
|
51
|
+
serviceHost = null,
|
|
52
|
+
toolHost = null,
|
|
53
|
+
editorHost = null,
|
|
54
|
+
} = {}) {
|
|
55
|
+
const editorDir = path.join(packageDir, 'editor')
|
|
56
|
+
const contractSnapshots = inspectContractSnapshots(packageDir, CONTRACT_VERSION)
|
|
57
|
+
if (!contractSnapshots.ok) {
|
|
58
|
+
console.error('[ppt-workbench] 契约快照不一致,工具调用将被拒绝:', describeMismatch(contractSnapshots))
|
|
59
|
+
}
|
|
60
|
+
const store = backend ? new BridgeStore({ backend }) : new BridgeStore(path.join(hostDir, 'state.json'))
|
|
61
|
+
/* 独立运行(无 dsh)也要有对象仓库:图片落 hostDir/assets,
|
|
62
|
+
* 否则 /assets 上传 404,ppt_images / ppt_image 的入库分支不可用。 */
|
|
63
|
+
if (!assetStore && !assetService) {
|
|
64
|
+
try {
|
|
65
|
+
assetStore = createFileAssetStore({ dir: path.join(hostDir, 'assets') })
|
|
66
|
+
} catch { /* 对象仓库不可用不影响纯 deck 功能 */ }
|
|
67
|
+
}
|
|
68
|
+
if (assetStore) process.env.PPT_ASSET_STORE = '1'
|
|
69
|
+
|
|
70
|
+
const createApp = serviceHost?.createApp ? (options) => serviceHost.createApp(options) : createServiceApp
|
|
71
|
+
const app = createApp({
|
|
72
|
+
id: 'ppt-workbench',
|
|
73
|
+
title: 'PPT Workbench',
|
|
74
|
+
version: PACKAGE_VERSION,
|
|
75
|
+
contractVersion: CONTRACT_VERSION,
|
|
76
|
+
health: () => {
|
|
77
|
+
const doc = store.get()
|
|
78
|
+
const owner = doc.owner && doc.owner.id && (Date.now() / 1000 - (doc.owner.ts || 0) <= 10)
|
|
79
|
+
return {
|
|
80
|
+
bridge: 'ppt-workbench',
|
|
81
|
+
editor: `http://127.0.0.1:${app.port}/`,
|
|
82
|
+
editorConnected: !!owner,
|
|
83
|
+
hasDeck: !!(doc.deck && Array.isArray(doc.deck.slides) && doc.deck.slides.length),
|
|
84
|
+
slides: doc.deck && Array.isArray(doc.deck.slides) ? doc.deck.slides.length : 0,
|
|
85
|
+
seq: doc.seq | 0,
|
|
86
|
+
appliedSeq: doc.applied_seq | 0,
|
|
87
|
+
stateSeq: doc.state_seq | 0,
|
|
88
|
+
uiSeq: doc.ui_seq | 0,
|
|
89
|
+
lastError: doc.error || '',
|
|
90
|
+
contractsOk: contractSnapshots.ok,
|
|
91
|
+
contractsChecked: contractSnapshots.checked,
|
|
92
|
+
injected: {
|
|
93
|
+
serviceHost: !!serviceHost,
|
|
94
|
+
toolHost: !!toolHost,
|
|
95
|
+
editorHost: !!editorHost,
|
|
96
|
+
assets: !!assetStore,
|
|
97
|
+
},
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
metrics: () => {
|
|
101
|
+
const doc = store.get()
|
|
102
|
+
const slides = doc.deck && Array.isArray(doc.deck.slides) ? doc.deck.slides.length : 0
|
|
103
|
+
const owner = doc.owner && doc.owner.id && (Date.now() / 1000 - (doc.owner.ts || 0) <= 10)
|
|
104
|
+
return [
|
|
105
|
+
['ppt_workbench_slides', slides],
|
|
106
|
+
['ppt_workbench_seq', doc.seq | 0],
|
|
107
|
+
['ppt_workbench_applied_seq', doc.applied_seq | 0],
|
|
108
|
+
['ppt_workbench_state_seq', doc.state_seq | 0],
|
|
109
|
+
['ppt_workbench_editor_connected', owner ? 1 : 0],
|
|
110
|
+
['ppt_workbench_shot_req', doc.shot_req | 0],
|
|
111
|
+
['ppt_workbench_memory_rss_bytes', process.memoryUsage().rss],
|
|
112
|
+
]
|
|
113
|
+
},
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
const jsonP = createJsonPptApp(store)
|
|
117
|
+
let mcpHandler = null
|
|
118
|
+
for (const route of createMcpRoutes({ getMcpHandler: () => mcpHandler })) app.use(route)
|
|
119
|
+
if (assetService?.mount) assetService.mount(app, { store: assetStore, basePath: BASE_PATH })
|
|
120
|
+
else app.use(createAssetRoutes({ assetStore, basePath: BASE_PATH }))
|
|
121
|
+
for (const route of createEditorRoutes({ editorDir, editorHost })) app.use(route)
|
|
122
|
+
/* 必须先于 bridge 路由:bridge 的主端点会把任意 POST 当 JSON 读取,
|
|
123
|
+
* 原始 pptx 字节不能被它先消费。 */
|
|
124
|
+
app.use(createImportRoutes({ basePath: BASE_PATH, importDir: path.join(hostDir, 'imports'), store }))
|
|
125
|
+
for (const route of createBridgeRoutes({ store, jsonP, applyOp })) app.use(route)
|
|
126
|
+
|
|
127
|
+
const tuning = getTuning()
|
|
128
|
+
const info = await app.listen({
|
|
129
|
+
port,
|
|
130
|
+
hostDir,
|
|
131
|
+
scanLimit,
|
|
132
|
+
lockFile: lockFile || path.join(hostDir, '.relay-instance.lock'),
|
|
133
|
+
strictPort,
|
|
134
|
+
allowReuse,
|
|
135
|
+
discoveryFile: '',
|
|
136
|
+
requestTimeoutMs: Number(tuning.httpRequestTimeoutMs) || 30000,
|
|
137
|
+
headersTimeoutMs: Number(tuning.httpHeadersTimeoutMs) || 10000,
|
|
138
|
+
keepAliveTimeoutMs: Number(tuning.httpKeepAliveTimeoutMs) || 5000,
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
if (info.reused) {
|
|
142
|
+
return { url: `${info.url}${BASE_PATH}`, editorUrl: `${info.url}/`, port: info.port, reused: true, stop: async () => {} }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
/* bridge-client 在 import 时读 PPT_BRIDGE_URL,必须先设 env 再 import tools。 */
|
|
147
|
+
process.env.PPT_BRIDGE_URL = `${info.url}${BASE_PATH}`
|
|
148
|
+
|
|
149
|
+
/* 崩溃恢复:state.json 里 seq > applied_seq 说明上一批指令未完整落盘。
|
|
150
|
+
* 用 store 的串行队列占住写锁,恢复完成前的 push 会排队而不是交叉执行。 */
|
|
151
|
+
await store.enqueue(() => replayPending({ store, jsonP, applyOp }))
|
|
152
|
+
|
|
153
|
+
let registry = null
|
|
154
|
+
try {
|
|
155
|
+
const tools = await import('./tools.mjs')
|
|
156
|
+
registry = tools.createRegistry(tools.CAPABILITIES, { enabled: capabilities, toolHost })
|
|
157
|
+
} catch (e) {
|
|
158
|
+
console.error('[ppt-workbench] 工具未装载(构建产物缺失,先跑 scripts/build-plugin.mjs):', e.message)
|
|
159
|
+
}
|
|
160
|
+
const TOOLS = registry?.TOOLS || []
|
|
161
|
+
const TOOL_IMPL = registry?.TOOL_IMPL || {}
|
|
162
|
+
const CAPABILITIES = registry?.CAPABILITIES || []
|
|
163
|
+
mcpHandler = createMcpHandler({
|
|
164
|
+
tools: TOOLS,
|
|
165
|
+
callTool: (name, args) => {
|
|
166
|
+
if (!contractSnapshots.ok && name !== 'ppt_capabilities') {
|
|
167
|
+
throw new Error(
|
|
168
|
+
`契约快照与运行时 major 不一致,已拒绝 ${name}:${describeMismatch(contractSnapshots)}。` +
|
|
169
|
+
'请重装/升级 dsh-ppt-suite 后重试。',
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
const impl = TOOL_IMPL[name]
|
|
173
|
+
if (!impl) throw new Error('未知工具: ' + name)
|
|
174
|
+
return impl(args)
|
|
175
|
+
},
|
|
176
|
+
serverInfo: { name: 'ppt-workbench', version: PACKAGE_VERSION },
|
|
177
|
+
instructions: serverInstructions(`http://127.0.0.1:${info.port}/`, { tools: TOOLS, capabilities: CAPABILITIES }),
|
|
178
|
+
maxBodyBytes: Number(tuning.bodyLimitBytes) || 10 * 1024 * 1024,
|
|
179
|
+
maxBatch: Number(tuning.mcpBatchLimit) || 100,
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
writeDiscovery(hostDir, `${info.url}${BASE_PATH}`, info.port)
|
|
183
|
+
return {
|
|
184
|
+
url: `${info.url}${BASE_PATH}`,
|
|
185
|
+
editorUrl: `${info.url}/`,
|
|
186
|
+
port: info.port,
|
|
187
|
+
reused: false,
|
|
188
|
+
stop: async () => {
|
|
189
|
+
removeDiscovery(hostDir)
|
|
190
|
+
await info.stop()
|
|
191
|
+
},
|
|
192
|
+
}
|
|
193
|
+
} catch (error) {
|
|
194
|
+
/* 启动失败不能留下僵尸端口/锁/发现文件。 */
|
|
195
|
+
try { removeDiscovery(hostDir) } catch { /* 忽略 */ }
|
|
196
|
+
try { await info.stop() } catch { /* 忽略 */ }
|
|
197
|
+
throw error
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function writeDiscovery(hostDir, url, port) {
|
|
202
|
+
try {
|
|
203
|
+
fs.mkdirSync(hostDir, { recursive: true })
|
|
204
|
+
fs.writeFileSync(path.join(hostDir, 'bridge.json'), JSON.stringify({
|
|
205
|
+
bridge: 'ppt-workbench', url, port, pid: process.pid, updated: new Date().toISOString(),
|
|
206
|
+
}, null, 2))
|
|
207
|
+
} catch { /* 发现文件写失败不影响服务 */ }
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function removeDiscovery(hostDir) {
|
|
211
|
+
const file = path.join(hostDir, 'bridge.json')
|
|
212
|
+
try {
|
|
213
|
+
const data = JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
214
|
+
if (data.pid === process.pid) fs.rmSync(file, { force: true })
|
|
215
|
+
} catch { /* 不存在或不是本进程写的 */ }
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/* ---- 独立运行入口(无 dsh 也可手动拉起,供调试/演示)---- */
|
|
219
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
220
|
+
const arg = (name, dflt) => {
|
|
221
|
+
const i = process.argv.indexOf(name)
|
|
222
|
+
return i >= 0 && process.argv[i + 1] ? process.argv[i + 1] : dflt
|
|
223
|
+
}
|
|
224
|
+
/* 独立服务形态也要接上 runtime 的 workspace:broker 拉起时继承了
|
|
225
|
+
* DSH_RUNTIME_FILE / DSH_STORE_*,relay 据此使用当前 runtime 的 store,
|
|
226
|
+
* 而不是退化成全局 ~/.dsh-ppt-bridge 文件库。 */
|
|
227
|
+
const runtime = loadRuntimeContext()
|
|
228
|
+
const workId = runtime?.workId || 'main'
|
|
229
|
+
const client = runtime ? createRuntimeStoreClient(runtime, { workId }) : null
|
|
230
|
+
const backend = client
|
|
231
|
+
? new WorkspaceBackend({
|
|
232
|
+
client,
|
|
233
|
+
workId,
|
|
234
|
+
journalPath: runtime.paths?.logs ? path.join(runtime.paths.logs, 'ppt-journal.jsonl') : null,
|
|
235
|
+
})
|
|
236
|
+
: null
|
|
237
|
+
const relay = await startRelay({
|
|
238
|
+
port: Number(arg('--port', DEFAULT_PORT)) || DEFAULT_PORT,
|
|
239
|
+
hostDir: arg('--dir', undefined) || runtime?.paths?.contentState || undefined,
|
|
240
|
+
backend,
|
|
241
|
+
})
|
|
242
|
+
console.log('[ppt-workbench] 桥:', relay.url)
|
|
243
|
+
console.log('[ppt-workbench] 编辑器:', relay.editorUrl, '(浏览器打开即用)')
|
|
244
|
+
console.log('[ppt-workbench] MCP 端点:', relay.url.replace(BASE_PATH, '/mcp'))
|
|
245
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PPT 桥端点:协议与路由由 dsh-document-workbench 提供,本文件只注入
|
|
3
|
+
* PPT 的 deck 投影、VIEW_OPS 与 applyOp。
|
|
4
|
+
*/
|
|
5
|
+
import { createDocumentBridge } from 'dsh-document-workbench/bridge'
|
|
6
|
+
import { BASE_PATH, VIEW_OPS } from '../constants.mjs'
|
|
7
|
+
import { getTuning } from '../tuning.mjs'
|
|
8
|
+
import {
|
|
9
|
+
sanitizeAck, sanitizeClaim, sanitizePush, sanitizeShotResult, sanitizeState,
|
|
10
|
+
} from '../store.mjs'
|
|
11
|
+
|
|
12
|
+
export function createBridgeRoutes({ store, jsonP, applyOp }) {
|
|
13
|
+
const tuning = getTuning()
|
|
14
|
+
return createDocumentBridge({
|
|
15
|
+
session: store,
|
|
16
|
+
basePath: BASE_PATH,
|
|
17
|
+
bridgeName: 'ppt-bridge',
|
|
18
|
+
documentKey: 'deck',
|
|
19
|
+
documentPath: '/deck',
|
|
20
|
+
authority: 'server',
|
|
21
|
+
apply: (cmd) => applyOp(jsonP, cmd),
|
|
22
|
+
isViewOp: (cmd) => VIEW_OPS.has(cmd.op),
|
|
23
|
+
sanitizers: {
|
|
24
|
+
sanitizeAck,
|
|
25
|
+
sanitizeClaim,
|
|
26
|
+
sanitizePush,
|
|
27
|
+
sanitizeShotResult,
|
|
28
|
+
sanitizeState,
|
|
29
|
+
},
|
|
30
|
+
bodyLimitBytes: Number(tuning.bodyLimitBytes) || 10 * 1024 * 1024,
|
|
31
|
+
watch: {
|
|
32
|
+
pollMs: tuning.watchPollMs,
|
|
33
|
+
timeoutSec: tuning.watchTimeoutSec,
|
|
34
|
+
maxTimeoutSec: tuning.watchTimeoutMaxSec,
|
|
35
|
+
},
|
|
36
|
+
projectStatus: (doc) => ({
|
|
37
|
+
bridge: 'ppt-workbench',
|
|
38
|
+
has_deck: !!(doc.deck && Array.isArray(doc.deck.slides) && doc.deck.slides.length),
|
|
39
|
+
slides: doc.deck && Array.isArray(doc.deck.slides) ? doc.deck.slides.length : 0,
|
|
40
|
+
}),
|
|
41
|
+
projectDocument: (doc) => ({
|
|
42
|
+
deck: doc.deck || {},
|
|
43
|
+
state_seq: doc.state_seq | 0,
|
|
44
|
+
applied_seq: doc.applied_seq | 0,
|
|
45
|
+
}),
|
|
46
|
+
}).routes
|
|
47
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** 静态编辑器路由统一来自 dsh-editor-host 公共插件。 */
|
|
2
|
+
import { createEditorHost as createSharedEditorHost } from '../shared/editor-host.mjs'
|
|
3
|
+
import { EDITOR_FILES } from '../constants.mjs'
|
|
4
|
+
|
|
5
|
+
export function createEditorRoutes({ editorDir, editorHost = null }) {
|
|
6
|
+
const factory = editorHost?.createEditorHost || createSharedEditorHost
|
|
7
|
+
const host = factory({ id: 'ppt-editor', dir: editorDir, files: EDITOR_FILES })
|
|
8
|
+
return [host.staticRoutes]
|
|
9
|
+
}
|