@civilization/dsh-drawio 0.1.0 → 0.2.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/README.md +7 -4
- package/lib/client.js +124 -30
- package/package.json +10 -8
- package/src/client/index.tsx +135 -30
- package/src/index.js +85 -4
- package/vendor/drawio/js/PreConfig.js +8 -2
- package/vendor/drawio/js/plantuml/drawio-plantuml.min.js +353 -0
- package/vendor/drawio/service-worker.js +7 -947
package/src/index.js
CHANGED
|
@@ -6,9 +6,11 @@ import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
|
6
6
|
import { editDrawio, inspectDrawio, normalizeDrawio } from './xml.js'
|
|
7
7
|
|
|
8
8
|
export const name = 'dsh-drawio'
|
|
9
|
-
export const inject = ['tools', 'fs', '
|
|
9
|
+
export const inject = ['tools', 'fs', 'webServer']
|
|
10
10
|
|
|
11
11
|
const runtimeRoot = fileURLToPath(new URL('../vendor/drawio/', import.meta.url))
|
|
12
|
+
const MAX_DIAGRAM_BYTES = 10 * 1024 * 1024
|
|
13
|
+
const MAX_REQUEST_BYTES = MAX_DIAGRAM_BYTES + 64 * 1024
|
|
12
14
|
const output = {
|
|
13
15
|
schema: { type: 'object', additionalProperties: true, properties: {} },
|
|
14
16
|
render: (_args, value) => [{ type: 'text', text: JSON.stringify(value, null, 2) }],
|
|
@@ -17,6 +19,7 @@ const pathParameter = { type: 'string', required: true, description: 'Path to a
|
|
|
17
19
|
|
|
18
20
|
export function apply(ctx) {
|
|
19
21
|
ctx.effect(() => ctx.webServer.register({ kind: 'prefix', path: '/dsh-drawio/runtime', handler: serveRuntime }))
|
|
22
|
+
ctx.effect(() => ctx.webServer.register({ kind: 'prefix', path: '/dsh-drawio/api', handler: (req, res) => serveCanvasApi(ctx, req, res) }))
|
|
20
23
|
register(ctx, defineTool({
|
|
21
24
|
name: 'drawio_inspect',
|
|
22
25
|
description: 'Read a Draw.io file as structured pages, nodes, edges and geometry. Use this before editing an existing diagram. Set include_xml only when raw XML details are required.',
|
|
@@ -51,7 +54,8 @@ export function apply(ctx) {
|
|
|
51
54
|
async execute(args, exec) {
|
|
52
55
|
const { text, target } = await readDiagram(ctx, args.path, exec)
|
|
53
56
|
const xml = editDrawio(text, args.operations, args.page)
|
|
54
|
-
const
|
|
57
|
+
const policy = await resolvePolicy(ctx, 'drawio_edit', args, exec)
|
|
58
|
+
const outcome = await writeDiagram(ctx, target, xml, args, exec, policy)
|
|
55
59
|
return { path: target.displayPath, operation: outcome.operation, operationsApplied: args.operations.length, ...inspectDrawio(xml, false) }
|
|
56
60
|
},
|
|
57
61
|
}))
|
|
@@ -62,7 +66,7 @@ export function apply(ctx) {
|
|
|
62
66
|
output,
|
|
63
67
|
async execute(args, exec) {
|
|
64
68
|
assertDrawioPath(args.path)
|
|
65
|
-
const policy = await
|
|
69
|
+
const policy = await resolvePolicy(ctx, 'drawio_write', args, exec)
|
|
66
70
|
const target = await ctx.fs.resolve(args.path, resolveOptions(exec, args.path, policy?.workspaceRoot))
|
|
67
71
|
const xml = normalizeDrawio(args.xml)
|
|
68
72
|
const outcome = await writeDiagram(ctx, target, xml, args, exec, policy)
|
|
@@ -71,6 +75,67 @@ export function apply(ctx) {
|
|
|
71
75
|
}))
|
|
72
76
|
}
|
|
73
77
|
|
|
78
|
+
async function serveCanvasApi(ctx, req, res) {
|
|
79
|
+
if (req.method === 'OPTIONS') {
|
|
80
|
+
res.statusCode = 204
|
|
81
|
+
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS')
|
|
82
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type')
|
|
83
|
+
res.end()
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
if (req.method !== 'POST') return respondJson(res, 405, { ok: false, error: { message: 'Method Not Allowed' } })
|
|
87
|
+
try {
|
|
88
|
+
const pathname = new URL(req.url || '/', 'http://dsh.internal').pathname
|
|
89
|
+
const method = pathname.slice('/dsh-drawio/api/'.length)
|
|
90
|
+
if (!['read', 'write'].includes(method)) return respondJson(res, 404, { ok: false, error: { message: 'Unknown Draw.io operation.' } })
|
|
91
|
+
const payload = await readJsonBody(req)
|
|
92
|
+
const target = await resolveCanvasTarget(ctx, payload.cwd, payload.path)
|
|
93
|
+
const info = await ctx.fs.stat(target)
|
|
94
|
+
if (!info || info.type !== 'file') throw new Error('Draw.io file was not found.')
|
|
95
|
+
if (Number(info.size) > MAX_DIAGRAM_BYTES) throw new Error('Draw.io file is too large to open safely.')
|
|
96
|
+
if (method === 'read') return respondJson(res, 200, { ok: true, value: { content: await ctx.fs.readText(target) } })
|
|
97
|
+
if (typeof payload.content !== 'string' || Buffer.byteLength(payload.content) > MAX_DIAGRAM_BYTES) throw new Error('Draw.io content is too large to save safely.')
|
|
98
|
+
const xml = normalizeDrawio(payload.content)
|
|
99
|
+
const sandboxPolicy = { mode: 'workspace-write', workspaceRoot: payload.cwd }
|
|
100
|
+
await ctx.fs.writeText(target, xml, { kind: 'replaceIfVersion', version: info.version }, undefined, sandboxPolicy)
|
|
101
|
+
return respondJson(res, 200, { ok: true, value: { ok: true } })
|
|
102
|
+
} catch (error) {
|
|
103
|
+
return respondJson(res, 400, { ok: false, error: { message: String(error?.message || error).slice(0, 500) } })
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function resolveCanvasTarget(ctx, cwd, path) {
|
|
108
|
+
if (typeof cwd !== 'string' || !cwd.trim()) throw new Error('A workspace is required.')
|
|
109
|
+
assertDrawioPath(path)
|
|
110
|
+
const [root, target] = await Promise.all([ctx.fs.resolve(cwd), ctx.fs.resolve(path, { cwd })])
|
|
111
|
+
if (!ctx.fs.contains(root, target)) throw new Error('Draw.io file must stay inside the current workspace.')
|
|
112
|
+
return target
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function readJsonBody(req) {
|
|
116
|
+
const declared = Number(req.headers?.['content-length'])
|
|
117
|
+
if (Number.isFinite(declared) && declared > MAX_REQUEST_BYTES) throw new Error('Request body is too large.')
|
|
118
|
+
const chunks = []; let size = 0
|
|
119
|
+
for await (const chunk of req) {
|
|
120
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
|
|
121
|
+
size += buffer.length
|
|
122
|
+
if (size > MAX_REQUEST_BYTES) throw new Error('Request body is too large.')
|
|
123
|
+
chunks.push(buffer)
|
|
124
|
+
}
|
|
125
|
+
const text = Buffer.concat(chunks).toString('utf8')
|
|
126
|
+
const value = text ? JSON.parse(text) : {}
|
|
127
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Request body must be a JSON object.')
|
|
128
|
+
return value
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function respondJson(res, status, value) {
|
|
132
|
+
const body = JSON.stringify(value)
|
|
133
|
+
res.statusCode = status
|
|
134
|
+
res.setHeader('Content-Type', 'application/json; charset=utf-8')
|
|
135
|
+
res.setHeader('Content-Length', Buffer.byteLength(body))
|
|
136
|
+
res.end(body)
|
|
137
|
+
}
|
|
138
|
+
|
|
74
139
|
function register(ctx, tool) {
|
|
75
140
|
ctx.effect(() => ctx.tools.register(tool))
|
|
76
141
|
}
|
|
@@ -86,8 +151,24 @@ async function readDiagram(ctx, requestedPath, exec) {
|
|
|
86
151
|
return { target, text }
|
|
87
152
|
}
|
|
88
153
|
|
|
154
|
+
async function resolvePolicy(ctx, toolName, args, exec) {
|
|
155
|
+
const sandbox = ctx.get?.('sandbox')
|
|
156
|
+
if (sandbox?.resolvePolicy) {
|
|
157
|
+
try {
|
|
158
|
+
return await sandbox.resolvePolicy(toolName, args, exec)
|
|
159
|
+
} catch {}
|
|
160
|
+
}
|
|
161
|
+
const policyService = ctx.get?.('sandboxPolicy')
|
|
162
|
+
if (policyService?.resolve) {
|
|
163
|
+
try {
|
|
164
|
+
return policyService.resolve({ ...(exec?.agent ? { session: exec.agent.session } : {}) })
|
|
165
|
+
} catch {}
|
|
166
|
+
}
|
|
167
|
+
return undefined
|
|
168
|
+
}
|
|
169
|
+
|
|
89
170
|
async function writeDiagram(ctx, target, xml, args, exec, resolvedPolicy) {
|
|
90
|
-
const policy = resolvedPolicy ?? await
|
|
171
|
+
const policy = resolvedPolicy ?? await resolvePolicy(ctx, 'write', args, exec)
|
|
91
172
|
const intent = await ctx.waterfall('fs/write-intent', target, exec, () => undefined)
|
|
92
173
|
const outcome = await ctx.fs.writeText(target, xml, intent, exec.signal, policy)
|
|
93
174
|
ctx.emit('fs/observed', target, { kind: 'present', version: outcome.version }, exec)
|
|
@@ -8,10 +8,16 @@ window.DRAW_MATH_URL = null;
|
|
|
8
8
|
window.DRAWIO_CONFIG = {
|
|
9
9
|
compressXml: false,
|
|
10
10
|
enableCssDarkMode: true,
|
|
11
|
-
defaultLibraries: 'general;flowchart;basic;arrows2',
|
|
12
|
-
enabledLibraries:
|
|
11
|
+
defaultLibraries: 'general;uml;er;bpmn;flowchart;basic;arrows2',
|
|
12
|
+
enabledLibraries: null
|
|
13
13
|
};
|
|
14
14
|
urlParams['sync'] = 'manual';
|
|
15
15
|
urlParams['offline'] = '1';
|
|
16
16
|
urlParams['local'] = '1';
|
|
17
17
|
urlParams['plugins'] = '0';
|
|
18
|
+
|
|
19
|
+
if (typeof navigator !== 'undefined' && 'serviceWorker' in navigator) {
|
|
20
|
+
navigator.serviceWorker.getRegistrations().then(function(regs) {
|
|
21
|
+
for (var i = 0; i < regs.length; i++) regs[i].unregister();
|
|
22
|
+
}).catch(function() {});
|
|
23
|
+
}
|