dsh-plugin-office-markdown 1.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/LICENSE +21 -0
- package/README.en.md +156 -0
- package/README.md +144 -0
- package/cordis.patch.yml +35 -0
- package/lib/client.js +589 -0
- package/lib/convert.js +796 -0
- package/lib/env.js +602 -0
- package/lib/fallback-node.js +334 -0
- package/lib/fallback.py +579 -0
- package/lib/index.js +1443 -0
- package/lib/paths.js +76 -0
- package/lib/removal-watchdog.js +359 -0
- package/lib/settings-api.js +381 -0
- package/package.json +55 -0
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* dsh-plugin-office-markdown — settings-page HTTP API (host half)
|
|
3
|
+
*
|
|
4
|
+
* The settings page lives in the client half, which cannot touch the
|
|
5
|
+
* filesystem, spawn pip, or probe interpreters. This module exposes the small
|
|
6
|
+
* JSON surface it needs over the host `webServer` service under the single
|
|
7
|
+
* prefix `/office-markdown`:
|
|
8
|
+
*
|
|
9
|
+
* GET /office-markdown/api/status fast — current converter + snapshot + job
|
|
10
|
+
* GET /office-markdown/api/probe slow — full per-interpreter inspection;
|
|
11
|
+
* `?force=1` bypasses the probeTtlMs cache
|
|
12
|
+
* GET /office-markdown/api/job progress of a running install/uninstall
|
|
13
|
+
* POST /office-markdown/api/convert-test { path } — run the real converter chain on
|
|
14
|
+
* one absolute path, for the settings page's
|
|
15
|
+
* "试转一个文件" card (never touches the cache)
|
|
16
|
+
* POST /office-markdown/api/env/install { target? } — pip install "markitdown[all]",
|
|
17
|
+
* or record an existing install so the
|
|
18
|
+
* uninstaller knows what to remove
|
|
19
|
+
* POST /office-markdown/api/env/uninstall pip uninstall exactly what we installed
|
|
20
|
+
*
|
|
21
|
+
* Install and uninstall run as a background job: the HTTP call returns at once
|
|
22
|
+
* and the page polls `/api/job`, so a multi-minute pip run cannot time out.
|
|
23
|
+
*/
|
|
24
|
+
import fs from 'node:fs'
|
|
25
|
+
import path from 'node:path'
|
|
26
|
+
import {
|
|
27
|
+
analyzeMarkdown,
|
|
28
|
+
classify,
|
|
29
|
+
converterLabelFor,
|
|
30
|
+
convertFile,
|
|
31
|
+
probeConverters,
|
|
32
|
+
pythonCandidates,
|
|
33
|
+
readFidelityMarker,
|
|
34
|
+
safeBaseName
|
|
35
|
+
} from './convert.js'
|
|
36
|
+
import {
|
|
37
|
+
adoptMarkitdown,
|
|
38
|
+
commandOf,
|
|
39
|
+
inspectEnvironments,
|
|
40
|
+
installMarkitdown,
|
|
41
|
+
invalidateProbeCache,
|
|
42
|
+
readSnapshot,
|
|
43
|
+
snapshotSummary,
|
|
44
|
+
uninstallMarkitdown
|
|
45
|
+
} from './env.js'
|
|
46
|
+
|
|
47
|
+
export const API_BASE = '/office-markdown'
|
|
48
|
+
const JOB_MAX_LOG = 300
|
|
49
|
+
|
|
50
|
+
let currentJob = null
|
|
51
|
+
|
|
52
|
+
function jobView() {
|
|
53
|
+
if (!currentJob) return null
|
|
54
|
+
return {
|
|
55
|
+
id: currentJob.id,
|
|
56
|
+
kind: currentJob.kind,
|
|
57
|
+
state: currentJob.state,
|
|
58
|
+
startedAt: currentJob.startedAt,
|
|
59
|
+
finishedAt: currentJob.finishedAt,
|
|
60
|
+
log: currentJob.log.slice(-JOB_MAX_LOG),
|
|
61
|
+
result: currentJob.result,
|
|
62
|
+
error: currentJob.error
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function startJob(kind, runner) {
|
|
67
|
+
if (currentJob && currentJob.state === 'running') {
|
|
68
|
+
return { ok: false, error: '已有任务正在运行(' + currentJob.kind + '),请等它结束。' }
|
|
69
|
+
}
|
|
70
|
+
const job = {
|
|
71
|
+
id: kind + '-' + Date.now().toString(36),
|
|
72
|
+
kind,
|
|
73
|
+
state: 'running',
|
|
74
|
+
startedAt: new Date().toISOString(),
|
|
75
|
+
finishedAt: '',
|
|
76
|
+
log: [],
|
|
77
|
+
result: null,
|
|
78
|
+
error: ''
|
|
79
|
+
}
|
|
80
|
+
currentJob = job
|
|
81
|
+
const onLog = (line) => job.log.push(new Date().toLocaleTimeString('zh-CN') + ' ' + String(line))
|
|
82
|
+
Promise.resolve()
|
|
83
|
+
.then(() => runner(onLog))
|
|
84
|
+
.then((result) => {
|
|
85
|
+
job.state = 'done'
|
|
86
|
+
job.result = result
|
|
87
|
+
job.finishedAt = new Date().toISOString()
|
|
88
|
+
})
|
|
89
|
+
.catch((error) => {
|
|
90
|
+
job.state = 'failed'
|
|
91
|
+
job.error = String((error && error.message) || error)
|
|
92
|
+
job.finishedAt = new Date().toISOString()
|
|
93
|
+
})
|
|
94
|
+
// Installing or removing markitdown changes what a probe would find, so the
|
|
95
|
+
// cached sweep is dropped rather than shown for up to probeTtlMs.
|
|
96
|
+
.then(() => invalidateProbeCache())
|
|
97
|
+
return { ok: true, job: { id: job.id, kind, state: 'running' } }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function reply(res, status, payload) {
|
|
101
|
+
let body
|
|
102
|
+
try {
|
|
103
|
+
body = JSON.stringify(payload)
|
|
104
|
+
} catch (error) {
|
|
105
|
+
status = 500
|
|
106
|
+
body = JSON.stringify({ ok: false, error: '结果无法序列化:' + String((error && error.message) || error) })
|
|
107
|
+
}
|
|
108
|
+
res.writeHead(status, {
|
|
109
|
+
'content-type': 'application/json; charset=utf-8',
|
|
110
|
+
'cache-control': 'no-store',
|
|
111
|
+
'content-length': Buffer.byteLength(body)
|
|
112
|
+
})
|
|
113
|
+
res.end(body)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function readBody(req) {
|
|
117
|
+
const chunks = []
|
|
118
|
+
let size = 0
|
|
119
|
+
try {
|
|
120
|
+
for await (const chunk of req) {
|
|
121
|
+
size += chunk.length
|
|
122
|
+
if (size > 64 * 1024) break
|
|
123
|
+
chunks.push(chunk)
|
|
124
|
+
}
|
|
125
|
+
} catch {
|
|
126
|
+
return {}
|
|
127
|
+
}
|
|
128
|
+
if (!chunks.length) return {}
|
|
129
|
+
try {
|
|
130
|
+
const parsed = JSON.parse(Buffer.concat(chunks).toString('utf8'))
|
|
131
|
+
return parsed && typeof parsed === 'object' ? parsed : {}
|
|
132
|
+
} catch {
|
|
133
|
+
return {}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Where a试转 artifact goes: the configured tmpDir, or right beside the source.
|
|
139
|
+
*
|
|
140
|
+
* The name is fixed (`<stem>-test.md`) on purpose. It deliberately does NOT
|
|
141
|
+
* match the tool's `<stem>-<8hex>.md` cache pattern, so a试转 can never be
|
|
142
|
+
* mistaken for a cached conversion, never shows up in `action:"clean"`, and
|
|
143
|
+
* never accumulates: each试转 overwrites the previous one.
|
|
144
|
+
*/
|
|
145
|
+
function testTarget(cfg, srcAbs) {
|
|
146
|
+
const configured = String(cfg.tmpDir || '').trim()
|
|
147
|
+
const dir = configured ? path.resolve(configured) : path.dirname(srcAbs)
|
|
148
|
+
return path.join(dir, safeBaseName(srcAbs) + '-test.md')
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Convert one file on demand, straight from the settings page.
|
|
153
|
+
*
|
|
154
|
+
* This is the "does it actually work on this machine?" button: it runs exactly
|
|
155
|
+
* the converter chain the tool uses, reports which converter won and whether it
|
|
156
|
+
* was MarkItDown or the built-in fallback, and returns a short preview — all
|
|
157
|
+
* without a model turn and without touching the tool's cache.
|
|
158
|
+
*/
|
|
159
|
+
async function runConvertTest(cfg, srcAbs) {
|
|
160
|
+
const startedAt = Date.now()
|
|
161
|
+
const cls = classify(srcAbs)
|
|
162
|
+
let st = null
|
|
163
|
+
try { st = fs.statSync(srcAbs) } catch { st = null }
|
|
164
|
+
if (!st || !st.isFile()) return { ok: false, error: '文件不存在或不是普通文件:' + srcAbs }
|
|
165
|
+
if (cls.kind === 'plain') {
|
|
166
|
+
return {
|
|
167
|
+
ok: true,
|
|
168
|
+
skipped: true,
|
|
169
|
+
sourcePath: srcAbs,
|
|
170
|
+
sourceKind: cls.label,
|
|
171
|
+
elapsedMs: Date.now() - startedAt,
|
|
172
|
+
message: '「' + cls.label + '」直接用 read 读取即可,不需要转换。'
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const dst = testTarget(cfg, srcAbs)
|
|
177
|
+
try {
|
|
178
|
+
fs.mkdirSync(path.dirname(dst), { recursive: true })
|
|
179
|
+
} catch (error) {
|
|
180
|
+
return { ok: false, error: '无法创建输出目录:' + String((error && error.message) || error) }
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const result = await convertFile({ srcPath: srcAbs, dstPath: dst, cfg, force: true })
|
|
184
|
+
if (!result.ok) {
|
|
185
|
+
return {
|
|
186
|
+
ok: false,
|
|
187
|
+
sourcePath: srcAbs,
|
|
188
|
+
sourceKind: cls.label,
|
|
189
|
+
converterAttempts: (result.attempts || []).map((a) => String((a && a.reason) || (a && a.id) || a)),
|
|
190
|
+
notices: result.probeNotes || [],
|
|
191
|
+
elapsedMs: Date.now() - startedAt,
|
|
192
|
+
error: String(result.error || '转换失败')
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const info = analyzeMarkdown(dst, { headChars: 501 })
|
|
197
|
+
const stamp = readFidelityMarker(dst)
|
|
198
|
+
return {
|
|
199
|
+
ok: true,
|
|
200
|
+
sourcePath: srcAbs,
|
|
201
|
+
sourceKind: cls.label,
|
|
202
|
+
sourceBytes: st.size,
|
|
203
|
+
markdownPath: dst,
|
|
204
|
+
markdownBytes: info.bytes,
|
|
205
|
+
lineCount: info.lines,
|
|
206
|
+
estimatedTokens: info.tokens,
|
|
207
|
+
converter: stamp ? stamp.converter : result.converter,
|
|
208
|
+
converterLabel: stamp
|
|
209
|
+
? converterLabelFor(stamp.converter, result.converterLabel || '')
|
|
210
|
+
: (result.converterLabel || ''),
|
|
211
|
+
fidelity: stamp && stamp.fidelity ? stamp.fidelity : result.fidelity,
|
|
212
|
+
preview: info.head.length > 500 ? info.head.slice(0, 500) : info.head,
|
|
213
|
+
previewTruncated: info.head.length > 500,
|
|
214
|
+
notices: result.probeNotes || [],
|
|
215
|
+
elapsedMs: Date.now() - startedAt
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function buildStatus(ctx, cfg, meta) {
|
|
220
|
+
const probe = await probeConverters(cfg, { force: false })
|
|
221
|
+
const active = probe.chain.length ? probe.chain[0] : null
|
|
222
|
+
const usingFallback = !active || active.fidelity === 'limited'
|
|
223
|
+
return {
|
|
224
|
+
ok: true,
|
|
225
|
+
plugin: {
|
|
226
|
+
name: meta.name,
|
|
227
|
+
version: meta.version,
|
|
228
|
+
provider: meta.provider,
|
|
229
|
+
tool: meta.toolName,
|
|
230
|
+
tmpDir: cfg.tmpDir || '(源文件同目录)'
|
|
231
|
+
},
|
|
232
|
+
converter: {
|
|
233
|
+
id: active ? active.id : '',
|
|
234
|
+
label: active ? active.label : '无可用转换器',
|
|
235
|
+
fidelity: active ? active.fidelity : 'none',
|
|
236
|
+
usingFallback,
|
|
237
|
+
summary: usingFallback
|
|
238
|
+
? '当前使用插件内置兜底转换(未使用 MarkItDown,保真度有限)'
|
|
239
|
+
: '当前使用本机 MarkItDown 环境(保真度高)',
|
|
240
|
+
chain: probe.chain.map((c) => ({ id: c.id, label: c.label, fidelity: c.fidelity })),
|
|
241
|
+
pythonPrefer: probe.pythonPrefer || String(cfg.pythonPrefer || 'auto'),
|
|
242
|
+
notices: probe.notes || []
|
|
243
|
+
},
|
|
244
|
+
snapshot: snapshotSummary(),
|
|
245
|
+
job: jobView()
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function pickCandidate(cfg, wanted) {
|
|
250
|
+
const list = pythonCandidates(cfg)
|
|
251
|
+
if (wanted) {
|
|
252
|
+
const hit = list.find((c) => commandOf(c) === wanted)
|
|
253
|
+
if (hit) return hit
|
|
254
|
+
}
|
|
255
|
+
return list.length ? list[0] : null
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function createSettingsApi(ctx, cfg, meta) {
|
|
259
|
+
async function handle(req, res) {
|
|
260
|
+
let pathname = '/'
|
|
261
|
+
try {
|
|
262
|
+
const url = new URL(String(req.url || '/'), 'http://localhost')
|
|
263
|
+
pathname = url.pathname
|
|
264
|
+
} catch {
|
|
265
|
+
return reply(res, 400, { ok: false, error: '无法解析请求 URL' })
|
|
266
|
+
}
|
|
267
|
+
const route = pathname.startsWith(API_BASE) ? pathname.slice(API_BASE.length) : pathname
|
|
268
|
+
const method = String(req.method || 'GET').toUpperCase()
|
|
269
|
+
|
|
270
|
+
try {
|
|
271
|
+
if (method === 'GET' && (route === '/api/status' || route === '/api/status/')) {
|
|
272
|
+
return reply(res, 200, await buildStatus(ctx, cfg, meta))
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (method === 'GET' && (route === '/api/job' || route === '/api/job/')) {
|
|
276
|
+
return reply(res, 200, { ok: true, job: jobView() })
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (method === 'GET' && (route === '/api/probe' || route === '/api/probe/')) {
|
|
280
|
+
/* Probing every interpreter is slow (several process spawns each), so
|
|
281
|
+
* the result is cached for cfg.probeTtlMs and the page only forces a
|
|
282
|
+
* fresh sweep when the user explicitly asks for one. */
|
|
283
|
+
const force = /[?&]force=1/.test(String(req.url || ''))
|
|
284
|
+
const envs = await inspectEnvironments(cfg, { force })
|
|
285
|
+
return reply(res, 200, {
|
|
286
|
+
ok: true,
|
|
287
|
+
cached: envs.cached === true,
|
|
288
|
+
budgetMs: envs.budgetMs || 0,
|
|
289
|
+
python: {
|
|
290
|
+
prefer: envs.pythonPrefer,
|
|
291
|
+
active: envs.active,
|
|
292
|
+
installTarget: envs.installTarget,
|
|
293
|
+
candidates: envs.candidates
|
|
294
|
+
},
|
|
295
|
+
snapshot: envs.snapshot
|
|
296
|
+
})
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (method === 'POST' && route === '/api/convert-test') {
|
|
300
|
+
const body = await readBody(req)
|
|
301
|
+
const raw = typeof body.path === 'string' ? body.path.trim() : ''
|
|
302
|
+
if (!raw) {
|
|
303
|
+
return reply(res, 400, { ok: false, error: '请填写要试转的文件路径(绝对路径)。' })
|
|
304
|
+
}
|
|
305
|
+
if (!path.isAbsolute(raw)) {
|
|
306
|
+
return reply(res, 400, { ok: false, error: '请填写绝对路径,例如 D:\\work\\报表.xlsx。' })
|
|
307
|
+
}
|
|
308
|
+
return reply(res, 200, await runConvertTest(cfg, path.normalize(raw)))
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (method === 'POST' && route === '/api/env/install') {
|
|
312
|
+
const body = await readBody(req)
|
|
313
|
+
const wanted = typeof body.target === 'string' ? body.target : ''
|
|
314
|
+
const started = startJob('install', async (onLog) => {
|
|
315
|
+
const candidate = pickCandidate(cfg, wanted)
|
|
316
|
+
if (!candidate) throw new Error('没有找到任何 Python 解释器,无法安装 markitdown。')
|
|
317
|
+
const target = await inspectEnvironments(cfg, {})
|
|
318
|
+
const usable = target.candidates.find((c) => c.command === commandOf(candidate))
|
|
319
|
+
if (usable && !usable.usable) throw new Error('该解释器无法运行:' + (usable.error || '未知原因'))
|
|
320
|
+
if (usable && !usable.hasPip) throw new Error('该解释器没有 pip,无法安装。请换一个(如 DSH 自带运行时)。')
|
|
321
|
+
if (usable && usable.hasMarkitdown) {
|
|
322
|
+
const version = usable.markitdownVersion || ''
|
|
323
|
+
const targetCmd = commandOf(candidate)
|
|
324
|
+
const snap = readSnapshot()
|
|
325
|
+
const same = !!snap && (snap.pythonCommand === targetCmd || (usable.executable && snap.python === usable.executable))
|
|
326
|
+
if (same) {
|
|
327
|
+
onLog('该解释器已经有 markitdown ' + version + ',并且已经登记在插件名下。')
|
|
328
|
+
return { ok: true, already: true, recorded: true, target: targetCmd }
|
|
329
|
+
}
|
|
330
|
+
if (snap) {
|
|
331
|
+
// Never overwrite a record pointing at another interpreter: doing so
|
|
332
|
+
// would orphan the packages we installed there.
|
|
333
|
+
onLog('环境记录目前指向另一个解释器:' + (snap.python || snap.pythonCommand || '未知')
|
|
334
|
+
+ '。为避免留下无人认领的包,本次不覆盖。若要改用当前解释器,请先点「卸载环境」。')
|
|
335
|
+
return { ok: true, already: true, recorded: true, target: targetCmd, conflict: true }
|
|
336
|
+
}
|
|
337
|
+
onLog('该解释器已经有 markitdown ' + version + ',不重复安装;改为登记到插件名下,卸载插件时会一并移除。')
|
|
338
|
+
const adopted = await adoptMarkitdown(candidate, { onLog })
|
|
339
|
+
if (!adopted || !adopted.ok) {
|
|
340
|
+
onLog('登记失败:' + ((adopted && adopted.error) || '未知原因') + '(markitdown 可以继续使用,只是卸载插件时不会动它。)')
|
|
341
|
+
return { ok: true, already: true, recorded: false, target: targetCmd }
|
|
342
|
+
}
|
|
343
|
+
return {
|
|
344
|
+
ok: true,
|
|
345
|
+
already: true,
|
|
346
|
+
recorded: true,
|
|
347
|
+
adopted: true,
|
|
348
|
+
target: targetCmd,
|
|
349
|
+
packages: (adopted.added || []).length,
|
|
350
|
+
kept: (adopted.keep || []).length
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return installMarkitdown(cfg, candidate, { onLog })
|
|
354
|
+
})
|
|
355
|
+
return reply(res, started.ok ? 202 : 409, started)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (method === 'POST' && route === '/api/env/uninstall') {
|
|
359
|
+
const started = startJob('uninstall', async (onLog) => uninstallMarkitdown({ onLog }))
|
|
360
|
+
return reply(res, started.ok ? 202 : 409, started)
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return reply(res, 404, { ok: false, error: '未知接口:' + method + ' ' + pathname })
|
|
364
|
+
} catch (error) {
|
|
365
|
+
return reply(res, 500, { ok: false, error: String((error && error.message) || error) })
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return handle
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Register the settings API on the host web server.
|
|
374
|
+
* Returns a disposer, or `null` when the host has no web server (CLI profiles).
|
|
375
|
+
*/
|
|
376
|
+
export function registerSettingsApi(ctx, cfg, meta) {
|
|
377
|
+
const webServer = ctx.get('webServer')
|
|
378
|
+
if (!webServer || typeof webServer.register !== 'function') return null
|
|
379
|
+
const handle = createSettingsApi(ctx, cfg, meta)
|
|
380
|
+
return webServer.register({ kind: 'prefix', path: API_BASE, handler: handle })
|
|
381
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-plugin-office-markdown",
|
|
3
|
+
"version": "1.2.1",
|
|
4
|
+
"description": "DeepSeek Harness(DSH)bundle 插件:在模型读取工作区里的 Office / PDF 文件之前,用 Microsoft MarkItDown 自动把它们转成 Markdown,转换结果写在源文件旁边,因此进入上下文的只是一个 .md 路径。产物完整保留、绝不裁剪,同时返回字节数、行数、token 估算与标题结构索引,并给出「先看结构、再用 grep 定位」的读取策略;支持一次调用批量转换多个文件或整个目录。内置设置页可以检查本机的 Python 环境、按需一键配置或卸载 MarkItDown,也可以直接试转一个文件确认转换链真的可用。在 DSH 里卸载本插件时,会连同它自己记录过的 Python 环境一起清理干净。",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Z8906",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/Z8906/dsh-plugin-office-markdown.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/Z8906/dsh-plugin-office-markdown#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/Z8906/dsh-plugin-office-markdown/issues"
|
|
14
|
+
},
|
|
15
|
+
"type": "module",
|
|
16
|
+
"main": "./lib/index.js",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": "./lib/index.js",
|
|
19
|
+
"./client": "./lib/client.js",
|
|
20
|
+
"./package.json": "./package.json"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"lib",
|
|
24
|
+
"cordis.patch.yml",
|
|
25
|
+
"README.md"
|
|
26
|
+
],
|
|
27
|
+
"dsh": {
|
|
28
|
+
"bundle": {
|
|
29
|
+
"patch": "./cordis.patch.yml"
|
|
30
|
+
},
|
|
31
|
+
"client": {
|
|
32
|
+
"platform": "web"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"deepseek-harness",
|
|
37
|
+
"dsh",
|
|
38
|
+
"dsh-plugin",
|
|
39
|
+
"dsh-bundle",
|
|
40
|
+
"cordis",
|
|
41
|
+
"markitdown",
|
|
42
|
+
"office",
|
|
43
|
+
"docx",
|
|
44
|
+
"xlsx",
|
|
45
|
+
"pptx",
|
|
46
|
+
"pdf",
|
|
47
|
+
"markdown",
|
|
48
|
+
"batch-conversion",
|
|
49
|
+
"token-saving",
|
|
50
|
+
"context-optimization"
|
|
51
|
+
],
|
|
52
|
+
"engines": {
|
|
53
|
+
"node": ">=18"
|
|
54
|
+
}
|
|
55
|
+
}
|