@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,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 轻量 JSON Schema 参数校验(覆盖本工具面用到的子集)。
|
|
3
|
+
* MCP server 不会替我们执行 inputSchema,必须在调用实现前自己挡住坏参数。
|
|
4
|
+
*
|
|
5
|
+
* 编译缓存:schema 对象通常随工具定义长期存在,按 schema 对象(WeakMap)编译成
|
|
6
|
+
* 闭包树,重复调用不再逐层遍历 schema/创建临时数组。语义与解释执行逐行等价
|
|
7
|
+
* (anyOf / type / enum / maxLength / array / object 的顺序与错误文案不变)。
|
|
8
|
+
* 基准:50 元素数组参数校验从 ~0.029ms 降到 ~0.01ms(evolution/hotpath-bench.mjs H11)。
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const TYPE_CHECKS = {
|
|
12
|
+
string: (v) => typeof v === 'string',
|
|
13
|
+
number: (v) => typeof v === 'number' && Number.isFinite(v),
|
|
14
|
+
integer: (v) => typeof v === 'number' && Number.isInteger(v),
|
|
15
|
+
boolean: (v) => typeof v === 'boolean',
|
|
16
|
+
array: (v) => Array.isArray(v),
|
|
17
|
+
object: (v) => v !== null && typeof v === 'object' && !Array.isArray(v),
|
|
18
|
+
null: (v) => v === null,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** schema 对象 → 编译后的校验闭包。 */
|
|
22
|
+
const compiledCache = new WeakMap()
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 把 schema 编译成 `(value, path, errors) => void` 闭包树。
|
|
26
|
+
* 纯结构性编译,不依赖运行时数据;同一 schema 对象只编译一次。
|
|
27
|
+
*/
|
|
28
|
+
function compileSchema(schema) {
|
|
29
|
+
if (!schema || typeof schema !== 'object') return () => {}
|
|
30
|
+
|
|
31
|
+
/* anyOf:与解释版一致——任一支路无错误即通过,且通过后不再执行其余规则。 */
|
|
32
|
+
if (schema.anyOf) {
|
|
33
|
+
const branches = schema.anyOf.map(compileSchema)
|
|
34
|
+
return (value, path, errors) => {
|
|
35
|
+
const matched = branches.some((branch) => {
|
|
36
|
+
const local = []
|
|
37
|
+
branch(value, path, local)
|
|
38
|
+
return local.length === 0
|
|
39
|
+
})
|
|
40
|
+
if (!matched) errors.push(`${path} 不符合 anyOf`)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const typeCheck = schema.type ? TYPE_CHECKS[schema.type] : null
|
|
45
|
+
const enumValues = schema.enum ? schema.enum : null
|
|
46
|
+
const maxLength = schema.maxLength != null ? schema.maxLength : null
|
|
47
|
+
const maxItems = schema.type === 'array' && schema.maxItems != null ? schema.maxItems : null
|
|
48
|
+
const validateItem = schema.type === 'array' && schema.items ? compileSchema(schema.items) : null
|
|
49
|
+
const properties = schema.type === 'object' ? (schema.properties || {}) : null
|
|
50
|
+
const propValidators = properties
|
|
51
|
+
? Object.entries(properties).map(([key, child]) => [key, compileSchema(child)])
|
|
52
|
+
: null
|
|
53
|
+
const required = properties && Array.isArray(schema.required) ? schema.required : null
|
|
54
|
+
const noAdditional = properties && schema.additionalProperties === false
|
|
55
|
+
|
|
56
|
+
return (value, path, errors) => {
|
|
57
|
+
/* 类型不符:解释版立即返回,不再做深层校验(保持该短路语义)。 */
|
|
58
|
+
if (typeCheck && !typeCheck(value)) {
|
|
59
|
+
errors.push(`${path} 应为 ${schema.type}`)
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
if (enumValues && !enumValues.includes(value)) {
|
|
63
|
+
errors.push(`${path} 必须是 ${enumValues.join(' / ')}`)
|
|
64
|
+
}
|
|
65
|
+
if (maxLength != null && typeof value === 'string' && value.length > maxLength) {
|
|
66
|
+
errors.push(`${path} 超过最大长度 ${maxLength}`)
|
|
67
|
+
}
|
|
68
|
+
if (maxItems != null && Array.isArray(value) && value.length > maxItems) {
|
|
69
|
+
errors.push(`${path} 最多 ${maxItems} 项`)
|
|
70
|
+
}
|
|
71
|
+
if (validateItem && Array.isArray(value)) {
|
|
72
|
+
value.forEach((item, i) => validateItem(item, `${path}[${i}]`, errors))
|
|
73
|
+
}
|
|
74
|
+
if (properties && value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
75
|
+
if (required) {
|
|
76
|
+
for (const key of required) {
|
|
77
|
+
if (value[key] === undefined || value[key] === null) errors.push(`${path}.${key} 必填`)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (noAdditional) {
|
|
81
|
+
for (const key of Object.keys(value)) {
|
|
82
|
+
if (!properties[key]) errors.push(`${path}.${key} 不是允许的参数`)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
for (const [key, validateChild] of propValidators) {
|
|
86
|
+
if (value[key] === undefined) continue
|
|
87
|
+
validateChild(value[key], `${path}.${key}`, errors)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** 取(或编译)schema 的校验闭包。 */
|
|
94
|
+
function validatorOf(schema) {
|
|
95
|
+
if (!schema || typeof schema !== 'object') return compileSchema(schema)
|
|
96
|
+
const cached = compiledCache.get(schema)
|
|
97
|
+
if (cached) return cached
|
|
98
|
+
const compiled = compileSchema(schema)
|
|
99
|
+
compiledCache.set(schema, compiled)
|
|
100
|
+
return compiled
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* @returns {{ok: boolean, errors: string[]}}
|
|
105
|
+
*/
|
|
106
|
+
export function validateToolArguments(tool, args) {
|
|
107
|
+
const errors = []
|
|
108
|
+
if (!tool || !tool.inputSchema) {
|
|
109
|
+
if (args != null && (typeof args !== 'object' || Array.isArray(args))) errors.push('arguments 必须是 object')
|
|
110
|
+
return { ok: errors.length === 0, errors }
|
|
111
|
+
}
|
|
112
|
+
if (args == null) args = {}
|
|
113
|
+
validatorOf(tool.inputSchema)(args, 'arguments', errors)
|
|
114
|
+
return { ok: errors.length === 0, errors }
|
|
115
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-plugin-kit 运行时调优层。
|
|
3
|
+
*
|
|
4
|
+
* 默认值与进化搜索(evolution/ NSGA-II)的 founder 基线保持一致;
|
|
5
|
+
* 生产环境可用 DSH_TUNING_JSON 覆盖(按包名分组),测试/评估用 setTuning 注入。
|
|
6
|
+
*
|
|
7
|
+
* DSH_TUNING_JSON 形如:
|
|
8
|
+
* {"dsh-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['dsh-plugin-kit'] && typeof parsed['dsh-plugin-kit'] === 'object'
|
|
54
|
+
? parsed['dsh-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 }
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/** 同步文件锁:给 registry / workspace manifest 这类元数据用。 */
|
|
2
|
+
import crypto from 'node:crypto'
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { tuning } from '../tuning.mjs'
|
|
6
|
+
|
|
7
|
+
const sleepCell = new Int32Array(new SharedArrayBuffer(4))
|
|
8
|
+
|
|
9
|
+
export function sleepSync(ms) {
|
|
10
|
+
Atomics.wait(sleepCell, 0, 0, ms)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function pidDead(pid) {
|
|
14
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) return false
|
|
15
|
+
try { process.kill(pid, 0); return false } catch (error) { return error.code === 'ESRCH' }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function newToken() {
|
|
19
|
+
return crypto.randomBytes(12).toString('hex')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function tryCreate(lockFile, token) {
|
|
23
|
+
fs.mkdirSync(path.dirname(lockFile), { recursive: true })
|
|
24
|
+
let fd
|
|
25
|
+
try {
|
|
26
|
+
fd = fs.openSync(lockFile, 'wx')
|
|
27
|
+
} catch (error) {
|
|
28
|
+
if (error.code === 'EEXIST') return false
|
|
29
|
+
throw error
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
fs.writeFileSync(fd, JSON.stringify({ pid: process.pid, token, ts: Date.now() }))
|
|
33
|
+
try { fs.fsyncSync(fd) } catch { /* 部分文件系统不支持 */ }
|
|
34
|
+
} finally {
|
|
35
|
+
fs.closeSync(fd)
|
|
36
|
+
}
|
|
37
|
+
return true
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Only a complete record whose owner is provably dead can be reclaimed. A
|
|
41
|
+
// record-specific claim serializes competing reclaimers, including across
|
|
42
|
+
// successive lock owners. An abandoned claim is intentionally not reclaimed.
|
|
43
|
+
function reclaimDead(lockFile, staleMs) {
|
|
44
|
+
let raw, info, stat
|
|
45
|
+
try {
|
|
46
|
+
raw = fs.readFileSync(lockFile, 'utf8')
|
|
47
|
+
info = JSON.parse(raw)
|
|
48
|
+
stat = fs.statSync(lockFile)
|
|
49
|
+
} catch { return false }
|
|
50
|
+
if (!/^[a-f0-9]{24}$/.test(info?.token || '') || !pidDead(info.pid) || Date.now() - stat.mtimeMs <= staleMs) return false
|
|
51
|
+
const claim = `${lockFile}.reclaim-${crypto.createHash('sha256').update(raw).digest('hex')}`
|
|
52
|
+
let fd
|
|
53
|
+
try { fd = fs.openSync(claim, 'wx') } catch { return false }
|
|
54
|
+
try {
|
|
55
|
+
try {
|
|
56
|
+
if (fs.readFileSync(lockFile, 'utf8') !== raw || !pidDead(info.pid)) return false
|
|
57
|
+
fs.rmSync(lockFile)
|
|
58
|
+
return true
|
|
59
|
+
} catch { return false }
|
|
60
|
+
} finally {
|
|
61
|
+
fs.closeSync(fd)
|
|
62
|
+
try { fs.rmSync(claim, { force: true }) } catch { /* 保留未知状态 */ }
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function releaseOwned(lockFile, token) {
|
|
67
|
+
try {
|
|
68
|
+
const info = JSON.parse(fs.readFileSync(lockFile, 'utf8'))
|
|
69
|
+
if (info.token === token) fs.rmSync(lockFile, { force: true })
|
|
70
|
+
} catch { /* 已被删除或被接管 */ }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function acquireSync(lockFile, { timeoutMs, staleMs }) {
|
|
74
|
+
const token = newToken()
|
|
75
|
+
const deadline = Date.now() + timeoutMs
|
|
76
|
+
const { fileLockPollMs: pollMs } = tuning()
|
|
77
|
+
while (true) {
|
|
78
|
+
if (tryCreate(lockFile, token)) return token
|
|
79
|
+
if (reclaimDead(lockFile, staleMs)) continue
|
|
80
|
+
if (Date.now() >= deadline) throw new Error(`等待文件锁超时: ${lockFile}`)
|
|
81
|
+
sleepSync(Math.min(pollMs, Math.max(1, deadline - Date.now())))
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function acquireAsync(lockFile, { timeoutMs, staleMs }) {
|
|
86
|
+
const token = newToken()
|
|
87
|
+
const deadline = Date.now() + timeoutMs
|
|
88
|
+
const { fileLockPollMs: pollMs } = tuning()
|
|
89
|
+
while (true) {
|
|
90
|
+
if (tryCreate(lockFile, token)) return token
|
|
91
|
+
if (reclaimDead(lockFile, staleMs)) continue
|
|
92
|
+
if (Date.now() >= deadline) throw new Error(`等待文件锁超时: ${lockFile}`)
|
|
93
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(pollMs, Math.max(1, deadline - Date.now()))))
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function withFileLock(lockFile, fn, opts = {}) {
|
|
98
|
+
const defaults = tuning()
|
|
99
|
+
const { timeoutMs = defaults.fileLockSyncTimeoutMs, staleMs = defaults.fileLockSyncStaleMs } = opts
|
|
100
|
+
const token = acquireSync(lockFile, { timeoutMs, staleMs })
|
|
101
|
+
try {
|
|
102
|
+
return fn()
|
|
103
|
+
} finally {
|
|
104
|
+
releaseOwned(lockFile, token)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** 异步版本:用于分配端口这类需要 await 探测的场景。 */
|
|
109
|
+
export async function withAsyncFileLock(lockFile, fn, opts = {}) {
|
|
110
|
+
const defaults = tuning()
|
|
111
|
+
const { timeoutMs = defaults.fileLockAsyncTimeoutMs, staleMs = defaults.fileLockSyncStaleMs } = opts
|
|
112
|
+
const token = await acquireAsync(lockFile, { timeoutMs, staleMs })
|
|
113
|
+
try {
|
|
114
|
+
return await fn()
|
|
115
|
+
} finally {
|
|
116
|
+
releaseOwned(lockFile, token)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function writeJsonAtomic(file, value) {
|
|
121
|
+
fs.mkdirSync(path.dirname(file), { recursive: true })
|
|
122
|
+
const tmp = `${file}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`
|
|
123
|
+
fs.writeFileSync(tmp, JSON.stringify(value, null, 2) + '\n')
|
|
124
|
+
try { fs.renameSync(tmp, file) } catch {
|
|
125
|
+
try { fs.rmSync(file, { force: true }) } catch { /* 忽略 */ }
|
|
126
|
+
fs.renameSync(tmp, file)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function readJson(file, fallback = null) {
|
|
131
|
+
try { return JSON.parse(fs.readFileSync(file, 'utf8')) } catch { return fallback }
|
|
132
|
+
}
|