dsh-convert-core 0.1.0-alpha.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.md +149 -0
- package/bin/media-to-md.mjs +168 -0
- package/bin/tita-convert.mjs +85 -0
- package/docs/media-to-md.md +143 -0
- package/lib/adapters/anydoc.d.ts +73 -0
- package/lib/adapters/anydoc.d.ts.map +1 -0
- package/lib/adapters/anydoc.js +295 -0
- package/lib/adapters/anydoc.js.map +1 -0
- package/lib/adapters/iddoc.d.ts +28 -0
- package/lib/adapters/iddoc.d.ts.map +1 -0
- package/lib/adapters/iddoc.js +153 -0
- package/lib/adapters/iddoc.js.map +1 -0
- package/lib/adapters/index.d.ts +41 -0
- package/lib/adapters/index.d.ts.map +1 -0
- package/lib/adapters/index.js +115 -0
- package/lib/adapters/index.js.map +1 -0
- package/lib/adapters/scanned.d.ts +28 -0
- package/lib/adapters/scanned.d.ts.map +1 -0
- package/lib/adapters/scanned.js +121 -0
- package/lib/adapters/scanned.js.map +1 -0
- package/lib/adapters/text.d.ts +29 -0
- package/lib/adapters/text.d.ts.map +1 -0
- package/lib/adapters/text.js +135 -0
- package/lib/adapters/text.js.map +1 -0
- package/lib/adapters/types.d.ts +65 -0
- package/lib/adapters/types.d.ts.map +1 -0
- package/lib/adapters/types.js +11 -0
- package/lib/adapters/types.js.map +1 -0
- package/lib/contract.d.ts +185 -0
- package/lib/contract.d.ts.map +1 -0
- package/lib/contract.js +59 -0
- package/lib/contract.js.map +1 -0
- package/lib/delivery.d.ts +36 -0
- package/lib/delivery.d.ts.map +1 -0
- package/lib/delivery.js +97 -0
- package/lib/delivery.js.map +1 -0
- package/lib/http.d.ts +34 -0
- package/lib/http.d.ts.map +1 -0
- package/lib/http.js +372 -0
- package/lib/http.js.map +1 -0
- package/lib/markdown.d.ts +32 -0
- package/lib/markdown.d.ts.map +1 -0
- package/lib/markdown.js +145 -0
- package/lib/markdown.js.map +1 -0
- package/lib/media/binaries.d.ts +26 -0
- package/lib/media/binaries.d.ts.map +1 -0
- package/lib/media/binaries.js +60 -0
- package/lib/media/binaries.js.map +1 -0
- package/lib/media/convert.d.ts +19 -0
- package/lib/media/convert.d.ts.map +1 -0
- package/lib/media/convert.js +210 -0
- package/lib/media/convert.js.map +1 -0
- package/lib/media/index.d.ts +27 -0
- package/lib/media/index.d.ts.map +1 -0
- package/lib/media/index.js +27 -0
- package/lib/media/index.js.map +1 -0
- package/lib/media/markdown.d.ts +40 -0
- package/lib/media/markdown.d.ts.map +1 -0
- package/lib/media/markdown.js +89 -0
- package/lib/media/markdown.js.map +1 -0
- package/lib/media/media.d.ts +45 -0
- package/lib/media/media.d.ts.map +1 -0
- package/lib/media/media.js +167 -0
- package/lib/media/media.js.map +1 -0
- package/lib/media/models.d.ts +31 -0
- package/lib/media/models.d.ts.map +1 -0
- package/lib/media/models.js +87 -0
- package/lib/media/models.js.map +1 -0
- package/lib/media/scanned.d.ts +18 -0
- package/lib/media/scanned.d.ts.map +1 -0
- package/lib/media/scanned.js +179 -0
- package/lib/media/scanned.js.map +1 -0
- package/lib/media/types.d.ts +154 -0
- package/lib/media/types.d.ts.map +1 -0
- package/lib/media/types.js +23 -0
- package/lib/media/types.js.map +1 -0
- package/lib/server.d.ts +35 -0
- package/lib/server.d.ts.map +1 -0
- package/lib/server.js +64 -0
- package/lib/server.js.map +1 -0
- package/lib/service.d.ts +170 -0
- package/lib/service.d.ts.map +1 -0
- package/lib/service.js +562 -0
- package/lib/service.js.map +1 -0
- package/package.json +59 -0
- package/scripts/asr.py +55 -0
- package/scripts/ocr.py +40 -0
- package/scripts/pdf_pages.py +49 -0
- package/scripts/vendor.mjs +76 -0
- package/src/adapters/anydoc.ts +356 -0
- package/src/adapters/iddoc.ts +171 -0
- package/src/adapters/index.ts +147 -0
- package/src/adapters/scanned.ts +139 -0
- package/src/adapters/text.ts +146 -0
- package/src/adapters/types.ts +76 -0
- package/src/contract.ts +230 -0
- package/src/delivery.ts +124 -0
- package/src/http.ts +394 -0
- package/src/markdown.ts +141 -0
- package/src/media/binaries.ts +67 -0
- package/src/media/convert.ts +232 -0
- package/src/media/index.ts +43 -0
- package/src/media/markdown.ts +105 -0
- package/src/media/media.ts +204 -0
- package/src/media/models.ts +124 -0
- package/src/media/scanned.ts +200 -0
- package/src/media/types.ts +171 -0
- package/src/server.ts +85 -0
- package/src/service.ts +639 -0
- package/web/app.js +382 -0
- package/web/index.html +217 -0
- package/web/styles.css +263 -0
- package/web/vendor/icons.js +25 -0
- package/web/vendor/vue.global.prod.js +14 -0
package/web/app.js
ADDED
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Tita 转换内核 · 控制台
|
|
3
|
+
*
|
|
4
|
+
* Vue 3 Options API, deliberately buildless: this file is served as-is and
|
|
5
|
+
* talks to the kernel API under the same base path. The DSH plugin embeds this
|
|
6
|
+
* exact page instead of reimplementing it.
|
|
7
|
+
*/
|
|
8
|
+
(function () {
|
|
9
|
+
'use strict'
|
|
10
|
+
|
|
11
|
+
var meta = document.querySelector('meta[name="convert-base"]')
|
|
12
|
+
var BASE = String((meta && meta.content) || '').replace(/\/+$/, '')
|
|
13
|
+
var ICONS = window.__CONVERT_ICONS__ || {}
|
|
14
|
+
|
|
15
|
+
var KIND_ICONS = { file: 'file-text', text: 'clipboard-type' }
|
|
16
|
+
|
|
17
|
+
function encodePath(path) {
|
|
18
|
+
return String(path).split('/').map(encodeURIComponent).join('/')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function escapeHtml(value) {
|
|
22
|
+
return String(value).replace(/[&<>"]/g, function (character) {
|
|
23
|
+
return { '&': '&', '<': '<', '>': '>', '"': '"' }[character]
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Small Markdown renderer for the delivery preview. Input is our own output. */
|
|
28
|
+
function renderMarkdown(source, taskId) {
|
|
29
|
+
var lines = String(source || '').split('\n')
|
|
30
|
+
var out = []
|
|
31
|
+
var listStack = []
|
|
32
|
+
var inCode = false
|
|
33
|
+
var codeBuffer = []
|
|
34
|
+
var paragraph = []
|
|
35
|
+
|
|
36
|
+
function flushParagraph() {
|
|
37
|
+
if (paragraph.length) {
|
|
38
|
+
out.push('<p>' + inline(paragraph.join(' ')) + '</p>')
|
|
39
|
+
paragraph = []
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function closeLists(toDepth) {
|
|
43
|
+
while (listStack.length > toDepth) out.push('</' + listStack.pop() + '>')
|
|
44
|
+
}
|
|
45
|
+
function imageSrc(raw) {
|
|
46
|
+
if (/^https?:/i.test(raw) || raw.indexOf('data:') === 0) return raw
|
|
47
|
+
return BASE + '/api/tasks/' + encodeURIComponent(taskId) + '/files/' + encodePath(decodeURIComponent(raw))
|
|
48
|
+
}
|
|
49
|
+
function inline(text) {
|
|
50
|
+
var escaped = escapeHtml(text)
|
|
51
|
+
escaped = escaped.replace(/!\[([^\]]*)\]\(([^)\s]+)\)/g, function (_all, alt, url) {
|
|
52
|
+
return '<img alt="' + alt + '" src="' + imageSrc(url) + '" loading="lazy" />'
|
|
53
|
+
})
|
|
54
|
+
escaped = escaped.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, '<a href="$2" target="_blank" rel="noreferrer">$1</a>')
|
|
55
|
+
escaped = escaped.replace(/`([^`]+)`/g, '<code>$1</code>')
|
|
56
|
+
escaped = escaped.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
|
|
57
|
+
escaped = escaped.replace(/(^|[\s(])\*([^*]+)\*/g, '$1<em>$2</em>')
|
|
58
|
+
escaped = escaped.replace(/~~([^~]+)~~/g, '<del>$1</del>')
|
|
59
|
+
return escaped
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
for (var index = 0; index < lines.length; index += 1) {
|
|
63
|
+
var line = lines[index]
|
|
64
|
+
var fence = /^```(.*)$/.exec(line)
|
|
65
|
+
if (fence) {
|
|
66
|
+
if (inCode) {
|
|
67
|
+
out.push('<pre><code>' + escapeHtml(codeBuffer.join('\n')) + '</code></pre>')
|
|
68
|
+
codeBuffer = []
|
|
69
|
+
inCode = false
|
|
70
|
+
} else {
|
|
71
|
+
flushParagraph()
|
|
72
|
+
closeLists(0)
|
|
73
|
+
inCode = true
|
|
74
|
+
}
|
|
75
|
+
continue
|
|
76
|
+
}
|
|
77
|
+
if (inCode) {
|
|
78
|
+
codeBuffer.push(line)
|
|
79
|
+
continue
|
|
80
|
+
}
|
|
81
|
+
if (!line.trim()) {
|
|
82
|
+
flushParagraph()
|
|
83
|
+
closeLists(0)
|
|
84
|
+
continue
|
|
85
|
+
}
|
|
86
|
+
var heading = /^(#{1,6})\s+(.*)$/.exec(line)
|
|
87
|
+
if (heading) {
|
|
88
|
+
flushParagraph()
|
|
89
|
+
closeLists(0)
|
|
90
|
+
var level = heading[1].length
|
|
91
|
+
out.push('<h' + level + '>' + inline(heading[2]) + '</h' + level + '>')
|
|
92
|
+
continue
|
|
93
|
+
}
|
|
94
|
+
if (/^(-{3,}|\*{3,})$/.test(line.trim())) {
|
|
95
|
+
flushParagraph()
|
|
96
|
+
closeLists(0)
|
|
97
|
+
out.push('<hr />')
|
|
98
|
+
continue
|
|
99
|
+
}
|
|
100
|
+
var quote = /^>\s?(.*)$/.exec(line)
|
|
101
|
+
if (quote) {
|
|
102
|
+
flushParagraph()
|
|
103
|
+
out.push('<blockquote>' + inline(quote[1]) + '</blockquote>')
|
|
104
|
+
continue
|
|
105
|
+
}
|
|
106
|
+
var bullet = /^(\s*)([-*+]|\d+\.)\s+(.*)$/.exec(line)
|
|
107
|
+
if (bullet) {
|
|
108
|
+
flushParagraph()
|
|
109
|
+
var depth = Math.floor(bullet[1].length / 2) + 1
|
|
110
|
+
var type = /\d/.test(bullet[2]) ? 'ol' : 'ul'
|
|
111
|
+
closeLists(depth - 1)
|
|
112
|
+
while (listStack.length < depth) {
|
|
113
|
+
out.push('<' + type + '>')
|
|
114
|
+
listStack.push(type)
|
|
115
|
+
}
|
|
116
|
+
out.push('<li>' + inline(bullet[3]) + '</li>')
|
|
117
|
+
continue
|
|
118
|
+
}
|
|
119
|
+
if (/^\|.*\|$/.test(line.trim())) {
|
|
120
|
+
var next = lines[index + 1] || ''
|
|
121
|
+
if (/^\|[\s:|-]+\|$/.test(next.trim())) {
|
|
122
|
+
flushParagraph()
|
|
123
|
+
closeLists(0)
|
|
124
|
+
var header = line.trim().slice(1, -1).split('|').map(function (cell) { return inline(cell.trim()) })
|
|
125
|
+
index += 1
|
|
126
|
+
var rows = []
|
|
127
|
+
while (index + 1 < lines.length && /^\|.*\|$/.test((lines[index + 1] || '').trim())) {
|
|
128
|
+
index += 1
|
|
129
|
+
rows.push(lines[index].trim().slice(1, -1).split('|').map(function (cell) { return inline(cell.trim()) }))
|
|
130
|
+
}
|
|
131
|
+
out.push('<table><thead><tr>' + header.map(function (cell) { return '<th>' + cell + '</th>' }).join('') + '</tr></thead><tbody>' +
|
|
132
|
+
rows.map(function (row) { return '<tr>' + row.map(function (cell) { return '<td>' + cell + '</td>' }).join('') + '</tr>' }).join('') +
|
|
133
|
+
'</tbody></table>')
|
|
134
|
+
continue
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
paragraph.push(line.trim())
|
|
138
|
+
}
|
|
139
|
+
if (inCode && codeBuffer.length) out.push('<pre><code>' + escapeHtml(codeBuffer.join('\n')) + '</code></pre>')
|
|
140
|
+
flushParagraph()
|
|
141
|
+
closeLists(0)
|
|
142
|
+
return out.join('\n')
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
var app = Vue.createApp({
|
|
146
|
+
data: function () {
|
|
147
|
+
return {
|
|
148
|
+
base: BASE,
|
|
149
|
+
tab: 'convert',
|
|
150
|
+
tabs: [
|
|
151
|
+
{ id: 'convert', label: '转换', icon: 'file-up' },
|
|
152
|
+
{ id: 'tasks', label: '任务', icon: 'inbox' },
|
|
153
|
+
{ id: 'delivery', label: '交付', icon: 'package' },
|
|
154
|
+
],
|
|
155
|
+
kinds: [
|
|
156
|
+
{ id: 'file', label: '文件', icon: 'file-text' },
|
|
157
|
+
{ id: 'text', label: '文本', icon: 'clipboard-type' },
|
|
158
|
+
],
|
|
159
|
+
form: { kind: 'file', text: '', title: '', sourceType: '', adapter: '', file: null },
|
|
160
|
+
health: {},
|
|
161
|
+
capabilities: null,
|
|
162
|
+
tasks: [],
|
|
163
|
+
selectedId: '',
|
|
164
|
+
detail: null,
|
|
165
|
+
ingest: null,
|
|
166
|
+
preview: '',
|
|
167
|
+
error: null,
|
|
168
|
+
notice: null,
|
|
169
|
+
loading: false,
|
|
170
|
+
submitting: false,
|
|
171
|
+
stream: null,
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
computed: {
|
|
175
|
+
adapters: function () { return (this.capabilities && this.capabilities.adapters) || [] },
|
|
176
|
+
runtime: function () { return (this.capabilities && this.capabilities.runtime) || {} },
|
|
177
|
+
extensions: function () { return ((this.capabilities && this.capabilities.extensions) || []).map(function (item) { return item.replace('.', '') }) },
|
|
178
|
+
taskCount: function () { return this.tasks.length },
|
|
179
|
+
succeeded: function () { return this.tasks.filter(function (task) { return task.state === 'succeeded' }) },
|
|
180
|
+
instruction: function () {
|
|
181
|
+
if (!this.ingest || !this.detail) return ''
|
|
182
|
+
var ingested = this.ingest
|
|
183
|
+
return [
|
|
184
|
+
'把这次转换的结果录入知识库:',
|
|
185
|
+
'1) 交付目录:' + (this.detail.deliveryDir || ''),
|
|
186
|
+
'2) 图片资产:' + ingested.assets.map(function (asset) { return asset.rel }).join(', '),
|
|
187
|
+
'3) external_id:' + ingested.externalId + '(幂等键,重复录入应更新同一条来源)',
|
|
188
|
+
'4) source_type 候选:' + ingested.sourceTypeCandidates.join(' / ') + '(按 0013:分类由录入段判定)',
|
|
189
|
+
'5) 请调用知识插件 add_source,并在卡片中引用图片,否则图片拿不到多模态向量。',
|
|
190
|
+
].join('\n')
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
methods: {
|
|
194
|
+
ic: function (name) { return ICONS[name] || ICONS['layers'] || '' },
|
|
195
|
+
|
|
196
|
+
api: function (path, options) {
|
|
197
|
+
return fetch(BASE + path, options).then(function (response) {
|
|
198
|
+
return response.text().then(function (text) {
|
|
199
|
+
var payload
|
|
200
|
+
try { payload = text ? JSON.parse(text) : {} } catch (cause) { payload = { ok: false, error: { code: 'BAD_RESPONSE', message: text.slice(0, 300) } } }
|
|
201
|
+
if (!response.ok || payload.ok === false) {
|
|
202
|
+
var detail = payload.error || { code: 'HTTP_' + response.status, message: '请求失败' }
|
|
203
|
+
var error = new Error(detail.message)
|
|
204
|
+
error.code = detail.code
|
|
205
|
+
error.suggestion = detail.suggestion
|
|
206
|
+
throw error
|
|
207
|
+
}
|
|
208
|
+
return payload.data
|
|
209
|
+
})
|
|
210
|
+
})
|
|
211
|
+
},
|
|
212
|
+
|
|
213
|
+
describeError: function (cause) {
|
|
214
|
+
if (!cause) return '未知错误'
|
|
215
|
+
return [cause.code, cause.message].filter(Boolean).join(' · ') + (cause.suggestion ? ' — 建议:' + cause.suggestion : '')
|
|
216
|
+
},
|
|
217
|
+
|
|
218
|
+
refresh: async function () {
|
|
219
|
+
this.loading = true
|
|
220
|
+
try {
|
|
221
|
+
var results = await Promise.all([
|
|
222
|
+
this.api('/api/tasks'),
|
|
223
|
+
this.capabilities ? Promise.resolve(null) : this.api('/api/capabilities'),
|
|
224
|
+
this.api('/api/health'),
|
|
225
|
+
])
|
|
226
|
+
this.tasks = results[0].tasks || []
|
|
227
|
+
if (results[1]) this.capabilities = results[1]
|
|
228
|
+
this.health = results[2] || {}
|
|
229
|
+
if (this.selectedId && !this.tasks.some(function (task) { return task.id === this.selectedId }.bind(this))) {
|
|
230
|
+
this.selectedId = ''
|
|
231
|
+
this.detail = null
|
|
232
|
+
this.ingest = null
|
|
233
|
+
}
|
|
234
|
+
} catch (cause) {
|
|
235
|
+
this.error = this.describeError(cause)
|
|
236
|
+
} finally {
|
|
237
|
+
this.loading = false
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
onFile: function (event) {
|
|
242
|
+
this.form.file = (event.target.files && event.target.files[0]) || null
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
resetForm: function () {
|
|
246
|
+
this.form.text = ''
|
|
247
|
+
this.form.title = ''
|
|
248
|
+
this.form.sourceType = ''
|
|
249
|
+
this.form.adapter = ''
|
|
250
|
+
this.form.file = null
|
|
251
|
+
},
|
|
252
|
+
|
|
253
|
+
submit: async function () {
|
|
254
|
+
this.error = null
|
|
255
|
+
this.notice = null
|
|
256
|
+
this.submitting = true
|
|
257
|
+
try {
|
|
258
|
+
var kind = this.form.kind
|
|
259
|
+
var task
|
|
260
|
+
var common = {
|
|
261
|
+
title: this.form.title || undefined,
|
|
262
|
+
source_type: this.form.sourceType || undefined,
|
|
263
|
+
adapter: this.form.adapter || undefined,
|
|
264
|
+
}
|
|
265
|
+
if (kind === 'text') {
|
|
266
|
+
if (!this.form.text.trim()) throw new Error('请输入要转换的文本')
|
|
267
|
+
task = await this.api('/api/tasks', {
|
|
268
|
+
method: 'POST',
|
|
269
|
+
headers: { 'content-type': 'application/json' },
|
|
270
|
+
body: JSON.stringify(Object.assign({ kind: 'text', text: this.form.text }, common)),
|
|
271
|
+
})
|
|
272
|
+
} else {
|
|
273
|
+
if (!this.form.file) throw new Error('请选择文件')
|
|
274
|
+
var body = new FormData()
|
|
275
|
+
body.append('kind', kind)
|
|
276
|
+
if (common.title) body.append('title', common.title)
|
|
277
|
+
if (common.source_type) body.append('source_type', common.source_type)
|
|
278
|
+
if (common.adapter) body.append('adapter', common.adapter)
|
|
279
|
+
body.append('file', this.form.file)
|
|
280
|
+
task = await this.api('/api/tasks', { method: 'POST', body: body })
|
|
281
|
+
}
|
|
282
|
+
this.notice = '任务已受理:' + task.id
|
|
283
|
+
this.selectedId = task.id
|
|
284
|
+
this.tab = 'tasks'
|
|
285
|
+
this.resetForm()
|
|
286
|
+
await this.refresh()
|
|
287
|
+
} catch (cause) {
|
|
288
|
+
this.error = this.describeError(cause)
|
|
289
|
+
} finally {
|
|
290
|
+
this.submitting = false
|
|
291
|
+
}
|
|
292
|
+
},
|
|
293
|
+
|
|
294
|
+
cancel: async function (id) {
|
|
295
|
+
try {
|
|
296
|
+
await this.api('/api/tasks/' + encodeURIComponent(id) + '/cancel', { method: 'POST' })
|
|
297
|
+
await this.refresh()
|
|
298
|
+
} catch (cause) { this.error = this.describeError(cause) }
|
|
299
|
+
},
|
|
300
|
+
|
|
301
|
+
retry: async function (id) {
|
|
302
|
+
try {
|
|
303
|
+
await this.api('/api/tasks/' + encodeURIComponent(id) + '/retry', { method: 'POST' })
|
|
304
|
+
await this.refresh()
|
|
305
|
+
} catch (cause) { this.error = this.describeError(cause) }
|
|
306
|
+
},
|
|
307
|
+
|
|
308
|
+
openTask: async function (id) {
|
|
309
|
+
if (!id) { this.selectedId = ''; this.detail = null; this.ingest = null; return }
|
|
310
|
+
this.selectedId = id
|
|
311
|
+
try {
|
|
312
|
+
var detail = await this.api('/api/tasks/' + encodeURIComponent(id))
|
|
313
|
+
this.detail = detail
|
|
314
|
+
this.ingest = detail.ingest || null
|
|
315
|
+
this.preview = detail.ingest && detail.ingest.content ? renderMarkdown(detail.ingest.content, id) : ''
|
|
316
|
+
} catch (cause) { this.error = this.describeError(cause) }
|
|
317
|
+
},
|
|
318
|
+
|
|
319
|
+
fileUrl: function (rel) {
|
|
320
|
+
return BASE + '/api/tasks/' + encodeURIComponent(this.selectedId) + '/files/' + encodePath(rel)
|
|
321
|
+
},
|
|
322
|
+
|
|
323
|
+
copy: function (text) {
|
|
324
|
+
var self = this
|
|
325
|
+
var value = String(text || '')
|
|
326
|
+
var done = function () { self.notice = '已复制到剪贴板' }
|
|
327
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
328
|
+
navigator.clipboard.writeText(value).then(done).catch(function () { self.error = '复制失败,请手动选择文本' })
|
|
329
|
+
return
|
|
330
|
+
}
|
|
331
|
+
var area = document.createElement('textarea')
|
|
332
|
+
area.value = value
|
|
333
|
+
document.body.appendChild(area)
|
|
334
|
+
area.select()
|
|
335
|
+
try { document.execCommand('copy'); done() } catch (cause) { self.error = '复制失败' }
|
|
336
|
+
document.body.removeChild(area)
|
|
337
|
+
},
|
|
338
|
+
|
|
339
|
+
stateLabel: function (state) {
|
|
340
|
+
return { queued: '排队', running: '进行中', succeeded: '已交付', failed: '失败', cancelled: '已取消' }[state] || state
|
|
341
|
+
},
|
|
342
|
+
|
|
343
|
+
formatBytes: function (bytes) {
|
|
344
|
+
if (!bytes && bytes !== 0) return ''
|
|
345
|
+
var units = ['B', 'KB', 'MB', 'GB']
|
|
346
|
+
var value = Number(bytes)
|
|
347
|
+
var unit = 0
|
|
348
|
+
while (value >= 1024 && unit < units.length - 1) { value /= 1024; unit += 1 }
|
|
349
|
+
return value.toFixed(value >= 10 || unit === 0 ? 0 : 1) + ' ' + units[unit]
|
|
350
|
+
},
|
|
351
|
+
|
|
352
|
+
connect: function () {
|
|
353
|
+
var self = this
|
|
354
|
+
if (typeof EventSource === 'undefined') return
|
|
355
|
+
try {
|
|
356
|
+
this.stream = new EventSource(BASE + '/api/events')
|
|
357
|
+
this.stream.addEventListener('task', function (event) {
|
|
358
|
+
try {
|
|
359
|
+
var incoming = JSON.parse(event.data)
|
|
360
|
+
var index = self.tasks.findIndex(function (task) { return task.id === incoming.id })
|
|
361
|
+
if (index >= 0) self.tasks.splice(index, 1, incoming)
|
|
362
|
+
else self.tasks.unshift(incoming)
|
|
363
|
+
if (self.selectedId === incoming.id && (incoming.state === 'succeeded' || incoming.state === 'failed')) {
|
|
364
|
+
self.openTask(incoming.id)
|
|
365
|
+
}
|
|
366
|
+
} catch (cause) { /* ignore malformed frame */ }
|
|
367
|
+
})
|
|
368
|
+
this.stream.addEventListener('error', function () { /* EventSource retries by itself */ })
|
|
369
|
+
} catch (cause) { /* SSE is an optimisation, not a requirement */ }
|
|
370
|
+
},
|
|
371
|
+
},
|
|
372
|
+
mounted: function () {
|
|
373
|
+
this.refresh()
|
|
374
|
+
this.connect()
|
|
375
|
+
},
|
|
376
|
+
beforeUnmount: function () {
|
|
377
|
+
if (this.stream) this.stream.close()
|
|
378
|
+
},
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
app.mount('#app')
|
|
382
|
+
})()
|
package/web/index.html
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<meta name="convert-base" content="__CONVERT_BASE__" />
|
|
7
|
+
<title>Tita 转换内核 · 多格式 → 知识就绪 Markdown</title>
|
|
8
|
+
<link rel="stylesheet" href="__CONVERT_BASE__/styles.css" />
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<div id="app">
|
|
12
|
+
<header class="topbar">
|
|
13
|
+
<div class="brand">
|
|
14
|
+
<span class="brand-mark"><span class="ico" v-html="ic('layers')"></span></span>
|
|
15
|
+
<div>
|
|
16
|
+
<h1>Tita 转换内核</h1>
|
|
17
|
+
<p>Word / PowerPoint / Excel / OpenDocument / RTF / EPUB / CSV / 文本层 PDF → 知识就绪 Markdown + 本地图片</p>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
<nav class="tabs">
|
|
21
|
+
<button
|
|
22
|
+
v-for="item in tabs"
|
|
23
|
+
:key="item.id"
|
|
24
|
+
type="button"
|
|
25
|
+
:class="['tab', { active: tab === item.id }]"
|
|
26
|
+
@click="tab = item.id"
|
|
27
|
+
>
|
|
28
|
+
<span class="ico" v-html="ic(item.icon)"></span><span>{{ item.label }}</span>
|
|
29
|
+
</button>
|
|
30
|
+
</nav>
|
|
31
|
+
<div class="topbar-actions">
|
|
32
|
+
<span class="pill" :title="health.rootDir">
|
|
33
|
+
<span class="ico" v-html="ic('hard-drive')"></span>{{ taskCount }} 个任务
|
|
34
|
+
</span>
|
|
35
|
+
<button type="button" class="ghost" :disabled="loading" @click="refresh">
|
|
36
|
+
<span class="ico" v-html="ic('refresh-cw')"></span>刷新
|
|
37
|
+
</button>
|
|
38
|
+
</div>
|
|
39
|
+
</header>
|
|
40
|
+
|
|
41
|
+
<main>
|
|
42
|
+
<p v-if="error" class="banner error"><span class="ico" v-html="ic('alert-triangle')"></span>{{ error }}</p>
|
|
43
|
+
<p v-if="notice" class="banner ok"><span class="ico" v-html="ic('check-circle')"></span>{{ notice }}</p>
|
|
44
|
+
|
|
45
|
+
<!-- ============ 转换 ============ -->
|
|
46
|
+
<section v-show="tab === 'convert'" class="split">
|
|
47
|
+
<form class="card" @submit.prevent="submit">
|
|
48
|
+
<h2><span class="ico" v-html="ic('file-up')"></span>新转换</h2>
|
|
49
|
+
|
|
50
|
+
<div class="segmented">
|
|
51
|
+
<button
|
|
52
|
+
v-for="kind in kinds"
|
|
53
|
+
:key="kind.id"
|
|
54
|
+
type="button"
|
|
55
|
+
:class="['segment', { active: form.kind === kind.id }]"
|
|
56
|
+
@click="form.kind = kind.id"
|
|
57
|
+
>
|
|
58
|
+
<span class="ico" v-html="ic(kind.icon)"></span>{{ kind.label }}
|
|
59
|
+
</button>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<label v-if="form.kind === 'file'" class="field">
|
|
63
|
+
<span>输入文件</span>
|
|
64
|
+
<input type="file" @change="onFile" />
|
|
65
|
+
<small v-if="form.file">{{ form.file.name }} · {{ formatBytes(form.file.size) }}</small>
|
|
66
|
+
<small v-else>{{ extensions.join(' ') }}(单文件 ≤ 256 MB)</small>
|
|
67
|
+
</label>
|
|
68
|
+
|
|
69
|
+
<label v-if="form.kind === 'text'" class="field">
|
|
70
|
+
<span>文本</span>
|
|
71
|
+
<textarea v-model="form.text" rows="8" placeholder="粘贴 Markdown / 纯文本"></textarea>
|
|
72
|
+
</label>
|
|
73
|
+
|
|
74
|
+
<div class="grid-2">
|
|
75
|
+
<label class="field">
|
|
76
|
+
<span>标题(可选)</span>
|
|
77
|
+
<input v-model.trim="form.title" type="text" placeholder="留空则自动推断" />
|
|
78
|
+
</label>
|
|
79
|
+
<label class="field">
|
|
80
|
+
<span>adapter(可选)</span>
|
|
81
|
+
<select v-model="form.adapter">
|
|
82
|
+
<option value="">自动选择</option>
|
|
83
|
+
<option v-for="adapter in adapters" :key="adapter.id" :value="adapter.id">
|
|
84
|
+
{{ adapter.id }}{{ adapter.availability.available ? '' : '(当前不可用)' }}
|
|
85
|
+
</option>
|
|
86
|
+
</select>
|
|
87
|
+
</label>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<label class="field">
|
|
91
|
+
<span>source_type(业务分类,可选)</span>
|
|
92
|
+
<input v-model.trim="form.sourceType" type="text" placeholder="article / book / prd / document …" />
|
|
93
|
+
<small>按 0013 的边界:转换内核不替你决定分类,这里留空就只写候选,交由录入段判定。</small>
|
|
94
|
+
</label>
|
|
95
|
+
|
|
96
|
+
<button type="submit" class="primary" :disabled="submitting">
|
|
97
|
+
<span class="ico" v-html="ic(submitting ? 'loader' : 'play')"></span>{{ submitting ? '提交中…' : '开始转换' }}
|
|
98
|
+
</button>
|
|
99
|
+
</form>
|
|
100
|
+
|
|
101
|
+
<section class="card">
|
|
102
|
+
<h2><span class="ico" v-html="ic('activity')"></span>能力自检</h2>
|
|
103
|
+
<p class="muted">缺失的依赖不会静默降级,这里如实列出与安装命令。</p>
|
|
104
|
+
<ul class="cap-list">
|
|
105
|
+
<li v-for="adapter in adapters" :key="adapter.id">
|
|
106
|
+
<span :class="['dot', adapter.availability.available ? 'on' : 'off']"></span>
|
|
107
|
+
<div>
|
|
108
|
+
<strong>{{ adapter.id }}</strong>
|
|
109
|
+
<p>{{ adapter.description }}</p>
|
|
110
|
+
<p v-if="!adapter.availability.available" class="muted">{{ adapter.availability.reason }}</p>
|
|
111
|
+
<p v-if="adapter.availability.suggestion" class="hint">{{ adapter.availability.suggestion }}</p>
|
|
112
|
+
</div>
|
|
113
|
+
</li>
|
|
114
|
+
</ul>
|
|
115
|
+
<h3>支持的输入格式</h3>
|
|
116
|
+
<p class="muted small">{{ extensions.join(' ') }}</p>
|
|
117
|
+
<h3>引擎</h3>
|
|
118
|
+
<ul class="bin-list">
|
|
119
|
+
<li v-for="(probe, name) in runtime" :key="name">
|
|
120
|
+
<code>{{ name }}</code>
|
|
121
|
+
<span :class="['dot', probe.available ? 'on' : 'off']"></span>
|
|
122
|
+
<span v-if="probe.available" class="muted">{{ probe.version }}</span>
|
|
123
|
+
<span v-else class="hint">{{ probe.error }}</span>
|
|
124
|
+
</li>
|
|
125
|
+
</ul>
|
|
126
|
+
</section>
|
|
127
|
+
</section>
|
|
128
|
+
|
|
129
|
+
<!-- ============ 任务 ============ -->
|
|
130
|
+
<section v-show="tab === 'tasks'" class="card">
|
|
131
|
+
<h2><span class="ico" v-html="ic('inbox')"></span>转换任务</h2>
|
|
132
|
+
<p v-if="!tasks.length" class="muted">还没有任务。切到「转换」页提交一个输入。</p>
|
|
133
|
+
<table v-else class="tasks">
|
|
134
|
+
<thead>
|
|
135
|
+
<tr><th>标题</th><th>状态</th><th>进度</th><th>产物</th><th></th></tr>
|
|
136
|
+
</thead>
|
|
137
|
+
<tbody>
|
|
138
|
+
<tr v-for="task in tasks" :key="task.id" :class="{ selected: selectedId === task.id }">
|
|
139
|
+
<td>
|
|
140
|
+
<strong>{{ task.title }}</strong>
|
|
141
|
+
<div class="muted small">{{ task.id }} · {{ task.parser || task.inputKind }}</div>
|
|
142
|
+
<div v-if="task.error" class="error-text small">{{ task.error.code }} · {{ task.error.message }}</div>
|
|
143
|
+
</td>
|
|
144
|
+
<td><span :class="['state', task.state]">{{ stateLabel(task.state) }}</span></td>
|
|
145
|
+
<td>
|
|
146
|
+
<div class="progress">
|
|
147
|
+
<div class="bar" :class="{ indeterminate: task.state === 'running' && task.progress === null }" :style="{ width: task.progress === null ? '100%' : Math.round(task.progress * 100) + '%' }"></div>
|
|
148
|
+
</div>
|
|
149
|
+
<div class="muted small">{{ task.stage }}{{ task.message ? ' · ' + task.message : '' }}</div>
|
|
150
|
+
</td>
|
|
151
|
+
<td class="muted small">
|
|
152
|
+
{{ task.assetCount }} 图 / {{ task.wordCount }} 字
|
|
153
|
+
<div v-if="task.warnings && task.warnings.length">{{ task.warnings.length }} 条 warning</div>
|
|
154
|
+
</td>
|
|
155
|
+
<td class="row-actions">
|
|
156
|
+
<button type="button" class="ghost" @click="openTask(task.id)"><span class="ico" v-html="ic('scan-eye')"></span></button>
|
|
157
|
+
<button v-if="task.state === 'running' || task.state === 'queued'" type="button" class="ghost" @click="cancel(task.id)"><span class="ico" v-html="ic('x')"></span></button>
|
|
158
|
+
<button v-else type="button" class="ghost" @click="retry(task.id)"><span class="ico" v-html="ic('rotate-ccw')"></span></button>
|
|
159
|
+
</td>
|
|
160
|
+
</tr>
|
|
161
|
+
</tbody>
|
|
162
|
+
</table>
|
|
163
|
+
</section>
|
|
164
|
+
|
|
165
|
+
<!-- ============ 交付 ============ -->
|
|
166
|
+
<section v-show="tab === 'delivery'" class="card">
|
|
167
|
+
<h2><span class="ico" v-html="ic('package')"></span>交付目录与录入负载</h2>
|
|
168
|
+
<p class="muted">产物写入 <code>{{ health.outDir }}/<taskId>/</code>;下面的 ingest 负载就是交给知识录入的那一份。</p>
|
|
169
|
+
<p v-if="!succeeded.length" class="muted">还没有成功交付的任务。</p>
|
|
170
|
+
<template v-else>
|
|
171
|
+
<label class="field">
|
|
172
|
+
<span>选择任务</span>
|
|
173
|
+
<select :value="selectedId" @change="openTask($event.target.value)">
|
|
174
|
+
<option value="">—</option>
|
|
175
|
+
<option v-for="task in succeeded" :key="task.id" :value="task.id">{{ task.title }} ({{ task.id }})</option>
|
|
176
|
+
</select>
|
|
177
|
+
</label>
|
|
178
|
+
<div v-if="ingest" class="delivery">
|
|
179
|
+
<dl class="kv">
|
|
180
|
+
<div><dt>deliveryDir</dt><dd><code>{{ ingest.deliveryDir || detail.deliveryDir }}</code></dd></div>
|
|
181
|
+
<div><dt>external_id</dt><dd><code>{{ ingest.externalId }}</code></dd></div>
|
|
182
|
+
<div><dt>content_hash</dt><dd><code>{{ ingest.contentHash }}</code></dd></div>
|
|
183
|
+
<div><dt>source_format</dt><dd>{{ ingest.sourceFormat }} · parser {{ ingest.parser }}</dd></div>
|
|
184
|
+
<div><dt>source_type 候选</dt><dd>{{ ingest.sourceTypeCandidates.join(', ') }}</dd></div>
|
|
185
|
+
<div><dt>图片资产</dt><dd>{{ ingest.assets.length }} 个(引用基准:{{ ingest.assetRefBase }})</dd></div>
|
|
186
|
+
</dl>
|
|
187
|
+
<div class="row-actions">
|
|
188
|
+
<button type="button" class="ghost" @click="copy(ingest.markdown)"><span class="ico" v-html="ic('copy')"></span>复制 Markdown</button>
|
|
189
|
+
<a class="ghost" :href="fileUrl('index.md')" target="_blank" rel="noreferrer"><span class="ico" v-html="ic('download')"></span>index.md</a>
|
|
190
|
+
<a class="ghost" :href="fileUrl('manifest.json')" target="_blank" rel="noreferrer"><span class="ico" v-html="ic('download')"></span>manifest.json</a>
|
|
191
|
+
</div>
|
|
192
|
+
<h3>预览</h3>
|
|
193
|
+
<article class="preview" v-html="preview"></article>
|
|
194
|
+
<h3>assets/</h3>
|
|
195
|
+
<div class="assets">
|
|
196
|
+
<figure v-for="asset in ingest.assets" :key="asset.rel">
|
|
197
|
+
<img :src="fileUrl(asset.rel)" :alt="asset.rel" loading="lazy" />
|
|
198
|
+
<figcaption>{{ asset.rel }}<br /><span class="muted">{{ formatBytes(asset.bytes) }}</span></figcaption>
|
|
199
|
+
</figure>
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
<p v-else class="muted">选择一个已交付的任务查看产物。</p>
|
|
203
|
+
</template>
|
|
204
|
+
</section>
|
|
205
|
+
</main>
|
|
206
|
+
|
|
207
|
+
<footer class="footer">
|
|
208
|
+
<span>内核:<code>dsh-convert-core</code></span>
|
|
209
|
+
<span>交付根目录:<code>{{ health.rootDir }}</code></span>
|
|
210
|
+
</footer>
|
|
211
|
+
</div>
|
|
212
|
+
|
|
213
|
+
<script src="__CONVERT_BASE__/vendor/vue.global.prod.js"></script>
|
|
214
|
+
<script src="__CONVERT_BASE__/vendor/icons.js"></script>
|
|
215
|
+
<script src="__CONVERT_BASE__/app.js"></script>
|
|
216
|
+
</body>
|
|
217
|
+
</html>
|