@capyclaw/ppt-workbench 2.4.0 → 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/core/ops/dispatcher.js +2 -2
- package/core/ops/index.js +39 -39
- package/editor/app.js +5366 -5366
- package/package.json +53 -53
- package/server/http-utils.mjs +2 -2
- package/server/infra/tuning.mjs +82 -82
- package/server/routes/bridge.mjs +47 -47
- package/server/runtime/watch.mjs +17 -17
- package/server/shared/editor-host.mjs +125 -125
- package/server/shared/kit/tuning.mjs +82 -82
- package/server/single-instance.mjs +2 -2
- package/server/store/checkpoints.mjs +28 -28
- package/server/store/doc.mjs +52 -52
- package/server/store/index.mjs +34 -34
- package/server/store/lease.mjs +2 -2
- package/server/store/sanitize.mjs +32 -32
- package/server/validate.mjs +1 -1
package/package.json
CHANGED
|
@@ -1,53 +1,53 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@capyclaw/ppt-workbench",
|
|
3
|
-
"version": "2.4.
|
|
4
|
-
"description": "PPT 工作台运行时:27 个 ppt_* MCP 工具(导入/导出 PPTX + 图表/表格/公式/形状/线条/备注 + 契约探测)+ 本机实时 PPTist 编辑器。",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "./lib/host.mjs",
|
|
7
|
-
"exports": {
|
|
8
|
-
".": "./lib/host.mjs",
|
|
9
|
-
"./package.json": "./package.json"
|
|
10
|
-
},
|
|
11
|
-
"dsh": {
|
|
12
|
-
"bundle": {
|
|
13
|
-
"patch": "./cordis.patch.yml"
|
|
14
|
-
},
|
|
15
|
-
"capabilities": {
|
|
16
|
-
"source": "server/tools/capabilities",
|
|
17
|
-
"manifest": "capabilities.json"
|
|
18
|
-
}
|
|
19
|
-
},
|
|
20
|
-
"meta": {
|
|
21
|
-
"title": "PPT 工作台运行时",
|
|
22
|
-
"description": "AI 生成/编辑 PPT:27 个 ppt_* 工具 + 浏览器实时编辑器(http://127.0.0.1:39311/)"
|
|
23
|
-
},
|
|
24
|
-
"files": [
|
|
25
|
-
"lib/",
|
|
26
|
-
"server/",
|
|
27
|
-
"core/",
|
|
28
|
-
"!server/.relay-instance.lock",
|
|
29
|
-
"editor/",
|
|
30
|
-
"contract/",
|
|
31
|
-
"cordis.patch.yml",
|
|
32
|
-
"README.md",
|
|
33
|
-
"THIRD_PARTY_NOTICES.md"
|
|
34
|
-
],
|
|
35
|
-
"license": "AGPL-3.0-or-later",
|
|
36
|
-
"dependencies": {
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
},
|
|
40
|
-
"author": {
|
|
41
|
-
"name": "CapyClaw",
|
|
42
|
-
"url": "https://github.com/capyclaw",
|
|
43
|
-
"email": "support@capyclaw.dev"
|
|
44
|
-
},
|
|
45
|
-
"repository": {
|
|
46
|
-
"type": "git",
|
|
47
|
-
"url": "https://github.com/capyclaw/dsh-plugins"
|
|
48
|
-
},
|
|
49
|
-
"bugs": {
|
|
50
|
-
"url": "https://github.com/capyclaw/dsh-plugins/issues"
|
|
51
|
-
},
|
|
52
|
-
"homepage": "https://github.com/capyclaw/dsh-plugins"
|
|
53
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@capyclaw/ppt-workbench",
|
|
3
|
+
"version": "2.4.1",
|
|
4
|
+
"description": "PPT 工作台运行时:27 个 ppt_* MCP 工具(导入/导出 PPTX + 图表/表格/公式/形状/线条/备注 + 契约探测)+ 本机实时 PPTist 编辑器。",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./lib/host.mjs",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./lib/host.mjs",
|
|
9
|
+
"./package.json": "./package.json"
|
|
10
|
+
},
|
|
11
|
+
"dsh": {
|
|
12
|
+
"bundle": {
|
|
13
|
+
"patch": "./cordis.patch.yml"
|
|
14
|
+
},
|
|
15
|
+
"capabilities": {
|
|
16
|
+
"source": "server/tools/capabilities",
|
|
17
|
+
"manifest": "capabilities.json"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"meta": {
|
|
21
|
+
"title": "PPT 工作台运行时",
|
|
22
|
+
"description": "AI 生成/编辑 PPT:27 个 ppt_* 工具 + 浏览器实时编辑器(http://127.0.0.1:39311/)"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"lib/",
|
|
26
|
+
"server/",
|
|
27
|
+
"core/",
|
|
28
|
+
"!server/.relay-instance.lock",
|
|
29
|
+
"editor/",
|
|
30
|
+
"contract/",
|
|
31
|
+
"cordis.patch.yml",
|
|
32
|
+
"README.md",
|
|
33
|
+
"THIRD_PARTY_NOTICES.md"
|
|
34
|
+
],
|
|
35
|
+
"license": "AGPL-3.0-or-later",
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"pptxgenjs": "^3.12.0",
|
|
38
|
+
"@capyclaw/document-workbench": "^0.1.0"
|
|
39
|
+
},
|
|
40
|
+
"author": {
|
|
41
|
+
"name": "CapyClaw",
|
|
42
|
+
"url": "https://github.com/capyclaw",
|
|
43
|
+
"email": "support@capyclaw.dev"
|
|
44
|
+
},
|
|
45
|
+
"repository": {
|
|
46
|
+
"type": "git",
|
|
47
|
+
"url": "https://github.com/capyclaw/dsh-plugins"
|
|
48
|
+
},
|
|
49
|
+
"bugs": {
|
|
50
|
+
"url": "https://github.com/capyclaw/dsh-plugins/issues"
|
|
51
|
+
},
|
|
52
|
+
"homepage": "https://github.com/capyclaw/dsh-plugins"
|
|
53
|
+
}
|
package/server/http-utils.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
/** 兼容入口:HTTP 传输实现统一来自
|
|
2
|
-
export * from './infra/http.mjs'
|
|
1
|
+
/** 兼容入口:HTTP 传输实现统一来自 @capyclaw/plugin-kit 的构建快照。 */
|
|
2
|
+
export * from './infra/http.mjs'
|
package/server/infra/tuning.mjs
CHANGED
|
@@ -1,82 +1,82 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* 默认值与进化搜索(evolution/ NSGA-II)的 founder 基线保持一致;
|
|
5
|
-
* 生产环境可用 DSH_TUNING_JSON 覆盖(按包名分组),测试/评估用 setTuning 注入。
|
|
6
|
-
*
|
|
7
|
-
* DSH_TUNING_JSON 形如:
|
|
8
|
-
* {"
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
const DEFAULTS = {
|
|
12
|
-
/* fs-lock:元数据文件锁
|
|
13
|
-
* pollMs=10 由 NSGA-II 进化搜索固化(evolution/,2026-11-01):
|
|
14
|
-
* 24 路竞争延迟 2202→1400ms;10ms 为延迟与轮询 IO 的膝点。 */
|
|
15
|
-
fileLockSyncTimeoutMs: 10000,
|
|
16
|
-
fileLockSyncStaleMs: 30000,
|
|
17
|
-
fileLockAsyncTimeoutMs: 30000,
|
|
18
|
-
fileLockPollMs: 10,
|
|
19
|
-
fileLockFreshGraceMs: 2000,
|
|
20
|
-
|
|
21
|
-
/* 编辑器租约:wait() 轮询节奏 */
|
|
22
|
-
leaseWaitPollMs: 200,
|
|
23
|
-
leaseWaitTimeoutMs: 4000,
|
|
24
|
-
|
|
25
|
-
/* JournalStore:journal 轮转阈值与尾读默认行数
|
|
26
|
-
* 512KB 由进化搜索固化:尾读 p95 4.3→1.9ms、磁盘 -70%、追加吞吐 +8%。 */
|
|
27
|
-
journalMaxBytes: 512 * 1024,
|
|
28
|
-
journalTailLimit: 8,
|
|
29
|
-
|
|
30
|
-
/* WorkspaceManager catalog 写节流(降低全局 catalog 锁竞争) */
|
|
31
|
-
catalogThrottleMs: 5000,
|
|
32
|
-
|
|
33
|
-
/* WorkspaceClient token 校验缓存 */
|
|
34
|
-
tokenVerifyCacheMs: 30000,
|
|
35
|
-
|
|
36
|
-
/* HTTP 服务三级超时(keepAliveTimeoutMs 必须小于 headersTimeoutMs) */
|
|
37
|
-
requestTimeoutMs: 30000,
|
|
38
|
-
headersTimeoutMs: 10000,
|
|
39
|
-
keepAliveTimeoutMs: 5000,
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
let overrides = {}
|
|
43
|
-
let envLoaded = false
|
|
44
|
-
|
|
45
|
-
function loadEnv() {
|
|
46
|
-
if (envLoaded) return
|
|
47
|
-
envLoaded = true
|
|
48
|
-
try {
|
|
49
|
-
const raw = process.env.DSH_TUNING_JSON
|
|
50
|
-
if (!raw) return
|
|
51
|
-
const parsed = JSON.parse(raw)
|
|
52
|
-
if (!parsed || typeof parsed !== 'object') return
|
|
53
|
-
const own = parsed['
|
|
54
|
-
? parsed['
|
|
55
|
-
: parsed
|
|
56
|
-
for (const key of Object.keys(DEFAULTS)) {
|
|
57
|
-
if (own[key] !== undefined) overrides[key] = own[key]
|
|
58
|
-
}
|
|
59
|
-
} catch { /* 非法 JSON 静默忽略:调优失败不能阻塞启动 */ }
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** 当前生效参数(默认值 + env + setTuning 覆盖)。 */
|
|
63
|
-
export function tuning() {
|
|
64
|
-
loadEnv()
|
|
65
|
-
return { ...DEFAULTS, ...overrides }
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** 测试/评估注入。只接受已知键,避免拼写错误静默生效。 */
|
|
69
|
-
export function setTuning(patch = {}) {
|
|
70
|
-
loadEnv()
|
|
71
|
-
for (const [key, value] of Object.entries(patch)) {
|
|
72
|
-
if (key in DEFAULTS) overrides[key] = value
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/** 恢复到默认值 + env 覆盖。 */
|
|
77
|
-
export function resetTuning() {
|
|
78
|
-
overrides = {}
|
|
79
|
-
envLoaded = false
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export const TUNING_DEFAULTS = { ...DEFAULTS }
|
|
1
|
+
/**
|
|
2
|
+
* @capyclaw/plugin-kit 运行时调优层。
|
|
3
|
+
*
|
|
4
|
+
* 默认值与进化搜索(evolution/ NSGA-II)的 founder 基线保持一致;
|
|
5
|
+
* 生产环境可用 DSH_TUNING_JSON 覆盖(按包名分组),测试/评估用 setTuning 注入。
|
|
6
|
+
*
|
|
7
|
+
* DSH_TUNING_JSON 形如:
|
|
8
|
+
* {"@capyclaw/plugin-kit": {"fileLockPollMs": 25, "journalMaxBytes": 2097152}}
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const DEFAULTS = {
|
|
12
|
+
/* fs-lock:元数据文件锁
|
|
13
|
+
* pollMs=10 由 NSGA-II 进化搜索固化(evolution/,2026-11-01):
|
|
14
|
+
* 24 路竞争延迟 2202→1400ms;10ms 为延迟与轮询 IO 的膝点。 */
|
|
15
|
+
fileLockSyncTimeoutMs: 10000,
|
|
16
|
+
fileLockSyncStaleMs: 30000,
|
|
17
|
+
fileLockAsyncTimeoutMs: 30000,
|
|
18
|
+
fileLockPollMs: 10,
|
|
19
|
+
fileLockFreshGraceMs: 2000,
|
|
20
|
+
|
|
21
|
+
/* 编辑器租约:wait() 轮询节奏 */
|
|
22
|
+
leaseWaitPollMs: 200,
|
|
23
|
+
leaseWaitTimeoutMs: 4000,
|
|
24
|
+
|
|
25
|
+
/* JournalStore:journal 轮转阈值与尾读默认行数
|
|
26
|
+
* 512KB 由进化搜索固化:尾读 p95 4.3→1.9ms、磁盘 -70%、追加吞吐 +8%。 */
|
|
27
|
+
journalMaxBytes: 512 * 1024,
|
|
28
|
+
journalTailLimit: 8,
|
|
29
|
+
|
|
30
|
+
/* WorkspaceManager catalog 写节流(降低全局 catalog 锁竞争) */
|
|
31
|
+
catalogThrottleMs: 5000,
|
|
32
|
+
|
|
33
|
+
/* WorkspaceClient token 校验缓存 */
|
|
34
|
+
tokenVerifyCacheMs: 30000,
|
|
35
|
+
|
|
36
|
+
/* HTTP 服务三级超时(keepAliveTimeoutMs 必须小于 headersTimeoutMs) */
|
|
37
|
+
requestTimeoutMs: 30000,
|
|
38
|
+
headersTimeoutMs: 10000,
|
|
39
|
+
keepAliveTimeoutMs: 5000,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let overrides = {}
|
|
43
|
+
let envLoaded = false
|
|
44
|
+
|
|
45
|
+
function loadEnv() {
|
|
46
|
+
if (envLoaded) return
|
|
47
|
+
envLoaded = true
|
|
48
|
+
try {
|
|
49
|
+
const raw = process.env.DSH_TUNING_JSON
|
|
50
|
+
if (!raw) return
|
|
51
|
+
const parsed = JSON.parse(raw)
|
|
52
|
+
if (!parsed || typeof parsed !== 'object') return
|
|
53
|
+
const own = parsed['@capyclaw/plugin-kit'] && typeof parsed['@capyclaw/plugin-kit'] === 'object'
|
|
54
|
+
? parsed['@capyclaw/plugin-kit']
|
|
55
|
+
: parsed
|
|
56
|
+
for (const key of Object.keys(DEFAULTS)) {
|
|
57
|
+
if (own[key] !== undefined) overrides[key] = own[key]
|
|
58
|
+
}
|
|
59
|
+
} catch { /* 非法 JSON 静默忽略:调优失败不能阻塞启动 */ }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** 当前生效参数(默认值 + env + setTuning 覆盖)。 */
|
|
63
|
+
export function tuning() {
|
|
64
|
+
loadEnv()
|
|
65
|
+
return { ...DEFAULTS, ...overrides }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** 测试/评估注入。只接受已知键,避免拼写错误静默生效。 */
|
|
69
|
+
export function setTuning(patch = {}) {
|
|
70
|
+
loadEnv()
|
|
71
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
72
|
+
if (key in DEFAULTS) overrides[key] = value
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** 恢复到默认值 + env 覆盖。 */
|
|
77
|
+
export function resetTuning() {
|
|
78
|
+
overrides = {}
|
|
79
|
+
envLoaded = false
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export const TUNING_DEFAULTS = { ...DEFAULTS }
|
package/server/routes/bridge.mjs
CHANGED
|
@@ -1,47 +1,47 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* PPT 桥端点:协议与路由由
|
|
3
|
-
* PPT 的 deck 投影、VIEW_OPS 与 applyOp。
|
|
4
|
-
*/
|
|
5
|
-
import { createDocumentBridge } from '
|
|
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
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* PPT 桥端点:协议与路由由 @capyclaw/document-workbench 提供,本文件只注入
|
|
3
|
+
* PPT 的 deck 投影、VIEW_OPS 与 applyOp。
|
|
4
|
+
*/
|
|
5
|
+
import { createDocumentBridge } from '@capyclaw/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
|
+
}
|
package/server/runtime/watch.mjs
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
/** 兼容层:长轮询的通用实现位于
|
|
2
|
-
import { watchSession } from '
|
|
3
|
-
import { getTuning } from '../tuning.mjs'
|
|
4
|
-
|
|
5
|
-
export function watchDoc(store, seq0, shot0, ui0, timeoutS, aborted) {
|
|
6
|
-
const tuning = getTuning()
|
|
7
|
-
return watchSession(store, {
|
|
8
|
-
seq: seq0,
|
|
9
|
-
shot: shot0,
|
|
10
|
-
ui: ui0,
|
|
11
|
-
timeoutS,
|
|
12
|
-
isAborted: aborted,
|
|
13
|
-
pollMs: tuning.watchPollMs,
|
|
14
|
-
timeoutSec: tuning.watchTimeoutSec,
|
|
15
|
-
maxTimeoutSec: tuning.watchTimeoutMaxSec,
|
|
16
|
-
})
|
|
17
|
-
}
|
|
1
|
+
/** 兼容层:长轮询的通用实现位于 @capyclaw/document-workbench/session。 */
|
|
2
|
+
import { watchSession } from '@capyclaw/document-workbench/session'
|
|
3
|
+
import { getTuning } from '../tuning.mjs'
|
|
4
|
+
|
|
5
|
+
export function watchDoc(store, seq0, shot0, ui0, timeoutS, aborted) {
|
|
6
|
+
const tuning = getTuning()
|
|
7
|
+
return watchSession(store, {
|
|
8
|
+
seq: seq0,
|
|
9
|
+
shot: shot0,
|
|
10
|
+
ui: ui0,
|
|
11
|
+
timeoutS,
|
|
12
|
+
isAborted: aborted,
|
|
13
|
+
pollMs: tuning.watchPollMs,
|
|
14
|
+
timeoutSec: tuning.watchTimeoutSec,
|
|
15
|
+
maxTimeoutSec: tuning.watchTimeoutMaxSec,
|
|
16
|
+
})
|
|
17
|
+
}
|
|
@@ -1,125 +1,125 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* dsh-editor-host —— 通用浏览器编辑器宿主。
|
|
3
|
-
*
|
|
4
|
-
* 功能插件只提供 editor/ 静态目录,不再自己写:
|
|
5
|
-
* - 静态文件路由 / content-type / 404
|
|
6
|
-
* - “显示即连接”的租约仲裁(后打开者抢占,旧窗口静默待命)
|
|
7
|
-
* - claim / release / status / heartbeat 端点
|
|
8
|
-
* - 连接状态供 /healthz 使用
|
|
9
|
-
*
|
|
10
|
-
* 浏览器端可直接使用
|
|
11
|
-
*/
|
|
12
|
-
import fs from 'node:fs'
|
|
13
|
-
import path from 'node:path'
|
|
14
|
-
import { createEditorLease } from './kit/editor/lease.mjs'
|
|
15
|
-
import { send, readBody } from './kit/service/http.mjs'
|
|
16
|
-
import { createServiceApi } from './kit/service/api.mjs'
|
|
17
|
-
|
|
18
|
-
const MIME = {
|
|
19
|
-
'.html': 'text/html; charset=utf-8',
|
|
20
|
-
'.js': 'text/javascript; charset=utf-8',
|
|
21
|
-
'.mjs': 'text/javascript; charset=utf-8',
|
|
22
|
-
'.css': 'text/css; charset=utf-8',
|
|
23
|
-
'.json': 'application/json; charset=utf-8',
|
|
24
|
-
'.svg': 'image/svg+xml',
|
|
25
|
-
'.png': 'image/png',
|
|
26
|
-
'.jpg': 'image/jpeg',
|
|
27
|
-
'.jpeg': 'image/jpeg',
|
|
28
|
-
'.webp': 'image/webp',
|
|
29
|
-
'.woff2': 'font/woff2',
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function safeJoin(root, rel) {
|
|
33
|
-
const target = path.resolve(root, `.${path.sep}${rel}`)
|
|
34
|
-
const base = path.resolve(root)
|
|
35
|
-
return target === base || target.startsWith(base + path.sep) ? target : null
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function contentType(file) {
|
|
39
|
-
return MIME[path.extname(file).toLowerCase()] || 'application/octet-stream'
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* @param {{ id: string, dir: string, basePath?: string, index?: string, files?: Record<string,string|string[]> }} options
|
|
44
|
-
*/
|
|
45
|
-
export function createEditorHost({ id, dir, basePath = '', index = 'index.html', files = null } = {}) {
|
|
46
|
-
if (!id) throw new Error('createEditorHost: id 必填')
|
|
47
|
-
const prefix = basePath === '/' ? '' : String(basePath || '').replace(/\/+$/, '')
|
|
48
|
-
const lease = createEditorLease()
|
|
49
|
-
|
|
50
|
-
const staticRoutes = async function editorStatic(req, res, u) {
|
|
51
|
-
if (req.method !== 'GET' && req.method !== 'HEAD') return false
|
|
52
|
-
if (prefix && u.pathname !== prefix && !u.pathname.startsWith(prefix + '/')) return false
|
|
53
|
-
const relRaw = prefix ? u.pathname.slice(prefix.length) : u.pathname
|
|
54
|
-
const rel = relRaw === '' || relRaw === '/' ? index : decodeURIComponent(relRaw.replace(/^\/+/, ''))
|
|
55
|
-
let file = null
|
|
56
|
-
if (files) {
|
|
57
|
-
const hit = files[relRaw === '' ? '/' : relRaw] || files[relRaw]
|
|
58
|
-
if (!hit) return false
|
|
59
|
-
const name = Array.isArray(hit) ? hit[0] : hit
|
|
60
|
-
file = safeJoin(dir, name)
|
|
61
|
-
} else {
|
|
62
|
-
file = safeJoin(dir, rel)
|
|
63
|
-
}
|
|
64
|
-
if (!file) return false
|
|
65
|
-
try {
|
|
66
|
-
const data = fs.readFileSync(file)
|
|
67
|
-
res.writeHead(200, {
|
|
68
|
-
'content-type': contentType(file),
|
|
69
|
-
'content-length': data.length,
|
|
70
|
-
'x-content-type-options': 'nosniff',
|
|
71
|
-
'referrer-policy': 'no-referrer',
|
|
72
|
-
'cache-control': 'no-cache',
|
|
73
|
-
})
|
|
74
|
-
res.end(req.method === 'HEAD' ? undefined : data)
|
|
75
|
-
return true
|
|
76
|
-
} catch {
|
|
77
|
-
return false
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const leaseRoutes = async function editorLeaseRoutes(req, res, u) {
|
|
82
|
-
const claimPath = `${prefix}/claim`
|
|
83
|
-
const releasePath = `${prefix}/release`
|
|
84
|
-
const statusPath = `${prefix}/status`
|
|
85
|
-
if (u.pathname === claimPath && req.method === 'POST') {
|
|
86
|
-
const body = await readBody(req)
|
|
87
|
-
const result = lease.claim(String(body.id || ''), !!body.force)
|
|
88
|
-
send(req, res, result.ok ? 200 : 409, result)
|
|
89
|
-
return true
|
|
90
|
-
}
|
|
91
|
-
if (u.pathname === releasePath && req.method === 'POST') {
|
|
92
|
-
const body = await readBody(req)
|
|
93
|
-
send(req, res, 200, lease.release(String(body.id || '')))
|
|
94
|
-
return true
|
|
95
|
-
}
|
|
96
|
-
if (u.pathname === statusPath && req.method === 'GET') {
|
|
97
|
-
send(req, res, 200, { ok: true, connected: lease.isLive(), lease: lease.status() })
|
|
98
|
-
return true
|
|
99
|
-
}
|
|
100
|
-
return false
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
return {
|
|
104
|
-
id,
|
|
105
|
-
lease,
|
|
106
|
-
routes() {
|
|
107
|
-
return [staticRoutes, leaseRoutes]
|
|
108
|
-
},
|
|
109
|
-
staticRoutes,
|
|
110
|
-
leaseRoutes,
|
|
111
|
-
status() {
|
|
112
|
-
return { connected: lease.isLive(), lease: lease.status() }
|
|
113
|
-
},
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
export function createEditorHostService(config = {}) {
|
|
118
|
-
return {
|
|
119
|
-
api: createServiceApi('editorHost', '1.0.0', ['static', 'lease', 'heartbeat', 'status']),
|
|
120
|
-
version: '0.1.0',
|
|
121
|
-
createEditorHost,
|
|
122
|
-
createLease: createEditorLease,
|
|
123
|
-
defaultHost: config.dir ? createEditorHost({ id: config.id || 'editor', ...config }) : null,
|
|
124
|
-
}
|
|
125
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* dsh-editor-host —— 通用浏览器编辑器宿主。
|
|
3
|
+
*
|
|
4
|
+
* 功能插件只提供 editor/ 静态目录,不再自己写:
|
|
5
|
+
* - 静态文件路由 / content-type / 404
|
|
6
|
+
* - “显示即连接”的租约仲裁(后打开者抢占,旧窗口静默待命)
|
|
7
|
+
* - claim / release / status / heartbeat 端点
|
|
8
|
+
* - 连接状态供 /healthz 使用
|
|
9
|
+
*
|
|
10
|
+
* 浏览器端可直接使用 @capyclaw/plugin-kit 的 editor/lease.mjs(无 Node 依赖)。
|
|
11
|
+
*/
|
|
12
|
+
import fs from 'node:fs'
|
|
13
|
+
import path from 'node:path'
|
|
14
|
+
import { createEditorLease } from './kit/editor/lease.mjs'
|
|
15
|
+
import { send, readBody } from './kit/service/http.mjs'
|
|
16
|
+
import { createServiceApi } from './kit/service/api.mjs'
|
|
17
|
+
|
|
18
|
+
const MIME = {
|
|
19
|
+
'.html': 'text/html; charset=utf-8',
|
|
20
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
21
|
+
'.mjs': 'text/javascript; charset=utf-8',
|
|
22
|
+
'.css': 'text/css; charset=utf-8',
|
|
23
|
+
'.json': 'application/json; charset=utf-8',
|
|
24
|
+
'.svg': 'image/svg+xml',
|
|
25
|
+
'.png': 'image/png',
|
|
26
|
+
'.jpg': 'image/jpeg',
|
|
27
|
+
'.jpeg': 'image/jpeg',
|
|
28
|
+
'.webp': 'image/webp',
|
|
29
|
+
'.woff2': 'font/woff2',
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function safeJoin(root, rel) {
|
|
33
|
+
const target = path.resolve(root, `.${path.sep}${rel}`)
|
|
34
|
+
const base = path.resolve(root)
|
|
35
|
+
return target === base || target.startsWith(base + path.sep) ? target : null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function contentType(file) {
|
|
39
|
+
return MIME[path.extname(file).toLowerCase()] || 'application/octet-stream'
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param {{ id: string, dir: string, basePath?: string, index?: string, files?: Record<string,string|string[]> }} options
|
|
44
|
+
*/
|
|
45
|
+
export function createEditorHost({ id, dir, basePath = '', index = 'index.html', files = null } = {}) {
|
|
46
|
+
if (!id) throw new Error('createEditorHost: id 必填')
|
|
47
|
+
const prefix = basePath === '/' ? '' : String(basePath || '').replace(/\/+$/, '')
|
|
48
|
+
const lease = createEditorLease()
|
|
49
|
+
|
|
50
|
+
const staticRoutes = async function editorStatic(req, res, u) {
|
|
51
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') return false
|
|
52
|
+
if (prefix && u.pathname !== prefix && !u.pathname.startsWith(prefix + '/')) return false
|
|
53
|
+
const relRaw = prefix ? u.pathname.slice(prefix.length) : u.pathname
|
|
54
|
+
const rel = relRaw === '' || relRaw === '/' ? index : decodeURIComponent(relRaw.replace(/^\/+/, ''))
|
|
55
|
+
let file = null
|
|
56
|
+
if (files) {
|
|
57
|
+
const hit = files[relRaw === '' ? '/' : relRaw] || files[relRaw]
|
|
58
|
+
if (!hit) return false
|
|
59
|
+
const name = Array.isArray(hit) ? hit[0] : hit
|
|
60
|
+
file = safeJoin(dir, name)
|
|
61
|
+
} else {
|
|
62
|
+
file = safeJoin(dir, rel)
|
|
63
|
+
}
|
|
64
|
+
if (!file) return false
|
|
65
|
+
try {
|
|
66
|
+
const data = fs.readFileSync(file)
|
|
67
|
+
res.writeHead(200, {
|
|
68
|
+
'content-type': contentType(file),
|
|
69
|
+
'content-length': data.length,
|
|
70
|
+
'x-content-type-options': 'nosniff',
|
|
71
|
+
'referrer-policy': 'no-referrer',
|
|
72
|
+
'cache-control': 'no-cache',
|
|
73
|
+
})
|
|
74
|
+
res.end(req.method === 'HEAD' ? undefined : data)
|
|
75
|
+
return true
|
|
76
|
+
} catch {
|
|
77
|
+
return false
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const leaseRoutes = async function editorLeaseRoutes(req, res, u) {
|
|
82
|
+
const claimPath = `${prefix}/claim`
|
|
83
|
+
const releasePath = `${prefix}/release`
|
|
84
|
+
const statusPath = `${prefix}/status`
|
|
85
|
+
if (u.pathname === claimPath && req.method === 'POST') {
|
|
86
|
+
const body = await readBody(req)
|
|
87
|
+
const result = lease.claim(String(body.id || ''), !!body.force)
|
|
88
|
+
send(req, res, result.ok ? 200 : 409, result)
|
|
89
|
+
return true
|
|
90
|
+
}
|
|
91
|
+
if (u.pathname === releasePath && req.method === 'POST') {
|
|
92
|
+
const body = await readBody(req)
|
|
93
|
+
send(req, res, 200, lease.release(String(body.id || '')))
|
|
94
|
+
return true
|
|
95
|
+
}
|
|
96
|
+
if (u.pathname === statusPath && req.method === 'GET') {
|
|
97
|
+
send(req, res, 200, { ok: true, connected: lease.isLive(), lease: lease.status() })
|
|
98
|
+
return true
|
|
99
|
+
}
|
|
100
|
+
return false
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
id,
|
|
105
|
+
lease,
|
|
106
|
+
routes() {
|
|
107
|
+
return [staticRoutes, leaseRoutes]
|
|
108
|
+
},
|
|
109
|
+
staticRoutes,
|
|
110
|
+
leaseRoutes,
|
|
111
|
+
status() {
|
|
112
|
+
return { connected: lease.isLive(), lease: lease.status() }
|
|
113
|
+
},
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function createEditorHostService(config = {}) {
|
|
118
|
+
return {
|
|
119
|
+
api: createServiceApi('editorHost', '1.0.0', ['static', 'lease', 'heartbeat', 'status']),
|
|
120
|
+
version: '0.1.0',
|
|
121
|
+
createEditorHost,
|
|
122
|
+
createLease: createEditorLease,
|
|
123
|
+
defaultHost: config.dir ? createEditorHost({ id: config.id || 'editor', ...config }) : null,
|
|
124
|
+
}
|
|
125
|
+
}
|