dsh-cait 1.0.3 → 1.0.4
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/CHANGELOG.md +367 -352
- package/LICENSE +201 -201
- package/README.ja.md +4 -3
- package/README.md +424 -423
- package/README.zh-CN.md +391 -390
- package/client/components.js +2377 -2377
- package/client/helpers.js +103 -103
- package/client/locales.js +730 -730
- package/client/rpc.js +93 -93
- package/client/styles.js +258 -258
- package/client.js +3556 -3556
- package/icon.svg +8 -8
- package/index.js +45 -45
- package/lib/api.js +223 -223
- package/lib/docx.js +389 -389
- package/lib/errors.js +8 -8
- package/lib/formats.js +917 -917
- package/lib/glossary.js +178 -178
- package/lib/import-export.js +605 -605
- package/lib/libraries.js +183 -183
- package/lib/markdown.js +28 -28
- package/lib/mt.js +152 -152
- package/lib/persistence.js +321 -321
- package/lib/pptx.js +89 -89
- package/lib/segmenter.js +84 -84
- package/lib/similarity.js +113 -113
- package/lib/store-utils.js +339 -339
- package/lib/store.js +734 -734
- package/lib/tm.js +160 -160
- package/lib/tmx.js +85 -85
- package/lib/tools.js +472 -472
- package/lib/xlsx.js +218 -218
- package/lib/xml.js +31 -31
- package/locale/en.json +11 -11
- package/locale/zh.json +11 -11
- package/package.json +83 -83
- package/scripts/build-client.mjs +13 -13
package/icon.svg
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none" role="img" aria-label="CAIT">
|
|
2
|
-
<title>CAIT</title>
|
|
3
|
-
<path d="M6.3 5.8H15.2C17 5.8 18.4 7.2 18.4 9V14.8C18.4 16.6 17 18 15.2 18H9.3L5.9 21.6V18H6.3C4.5 18 3.1 16.6 3.1 14.8V9C3.1 7.2 4.5 5.8 6.3 5.8Z" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
|
4
|
-
<path d="M5.4 16.4 9.2 8.5 13 16.4M6.8 13.4h4.8" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"/>
|
|
5
|
-
<path d="M17.4 14.2H27.4C29 14.2 30.2 15.4 30.2 17V22.2C30.2 23.8 29 25 27.4 25H27.2L27 28.8 23.2 25H17.4C15.8 25 14.6 23.8 14.6 22.2V17C14.6 15.4 15.8 14.2 17.4 14.2Z" fill="var(--dsw-alias-bg-base, #ffffff)" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
|
6
|
-
<circle cx="23" cy="17.2" r="1.4" fill="currentColor"/>
|
|
7
|
-
<path d="M18.6 20.6H27.4M23 20.6l-4.2 4.2M23 20.6l4.2 4.2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
8
|
-
</svg>
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none" role="img" aria-label="CAIT">
|
|
2
|
+
<title>CAIT</title>
|
|
3
|
+
<path d="M6.3 5.8H15.2C17 5.8 18.4 7.2 18.4 9V14.8C18.4 16.6 17 18 15.2 18H9.3L5.9 21.6V18H6.3C4.5 18 3.1 16.6 3.1 14.8V9C3.1 7.2 4.5 5.8 6.3 5.8Z" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
|
4
|
+
<path d="M5.4 16.4 9.2 8.5 13 16.4M6.8 13.4h4.8" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"/>
|
|
5
|
+
<path d="M17.4 14.2H27.4C29 14.2 30.2 15.4 30.2 17V22.2C30.2 23.8 29 25 27.4 25H27.2L27 28.8 23.2 25H17.4C15.8 25 14.6 23.8 14.6 22.2V17C14.6 15.4 15.8 14.2 17.4 14.2Z" fill="var(--dsw-alias-bg-base, #ffffff)" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
|
6
|
+
<circle cx="23" cy="17.2" r="1.4" fill="currentColor"/>
|
|
7
|
+
<path d="M18.6 20.6H27.4M23 20.6l-4.2 4.2M23 20.6l4.2 4.2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
8
|
+
</svg>
|
package/index.js
CHANGED
|
@@ -1,45 +1,45 @@
|
|
|
1
|
-
import { homedir } from 'node:os'
|
|
2
|
-
import path from 'node:path'
|
|
3
|
-
import Schema from '@deepseek-ai/schemastery'
|
|
4
|
-
import { CatStore } from './lib/store.js'
|
|
5
|
-
import { registerCatTools } from './lib/tools.js'
|
|
6
|
-
import { registerCatApi } from './lib/api.js'
|
|
7
|
-
import { createMtRuntime } from './lib/mt.js'
|
|
8
|
-
|
|
9
|
-
export const name = 'dsh-cait'
|
|
10
|
-
|
|
11
|
-
// Cordis has no optional injection: every entry in this array is a required
|
|
12
|
-
// service. CAIT targets the Web/Desktop composition, where all three exist.
|
|
13
|
-
export const inject = ['tools', 'webServer', 'connection']
|
|
14
|
-
|
|
15
|
-
export const Config = Schema.object({
|
|
16
|
-
dataDir: Schema.string()
|
|
17
|
-
.default('')
|
|
18
|
-
.description(
|
|
19
|
-
'CAIT 数据目录。留空时使用 DSH 用户数据目录下的 cait 子目录($DSH_HOME/cait)。支持绝对路径、相对路径和 ~。',
|
|
20
|
-
),
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
function expandHome(value) {
|
|
24
|
-
if (value === '~') return homedir()
|
|
25
|
-
if (value.startsWith('~/') || value.startsWith('~\\')) return path.join(homedir(), value.slice(2))
|
|
26
|
-
return value
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function resolveDataDir(configured) {
|
|
30
|
-
const custom = String(configured ?? '').trim()
|
|
31
|
-
if (custom) return path.resolve(expandHome(custom))
|
|
32
|
-
|
|
33
|
-
const dshHome = String(process.env.DSH_HOME ?? '').trim()
|
|
34
|
-
return path.join(path.resolve(dshHome || path.join(homedir(), '.dsh')), 'cait')
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function apply(ctx, config = {}) {
|
|
38
|
-
const store = new CatStore({
|
|
39
|
-
dataDir: resolveDataDir(config?.dataDir),
|
|
40
|
-
mtRuntime: createMtRuntime(ctx),
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
ctx.effect(() => registerCatTools(ctx, store))
|
|
44
|
-
ctx.effect(() => registerCatApi(ctx, store))
|
|
45
|
-
}
|
|
1
|
+
import { homedir } from 'node:os'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import Schema from '@deepseek-ai/schemastery'
|
|
4
|
+
import { CatStore } from './lib/store.js'
|
|
5
|
+
import { registerCatTools } from './lib/tools.js'
|
|
6
|
+
import { registerCatApi } from './lib/api.js'
|
|
7
|
+
import { createMtRuntime } from './lib/mt.js'
|
|
8
|
+
|
|
9
|
+
export const name = 'dsh-cait'
|
|
10
|
+
|
|
11
|
+
// Cordis has no optional injection: every entry in this array is a required
|
|
12
|
+
// service. CAIT targets the Web/Desktop composition, where all three exist.
|
|
13
|
+
export const inject = ['tools', 'webServer', 'connection']
|
|
14
|
+
|
|
15
|
+
export const Config = Schema.object({
|
|
16
|
+
dataDir: Schema.string()
|
|
17
|
+
.default('')
|
|
18
|
+
.description(
|
|
19
|
+
'CAIT 数据目录。留空时使用 DSH 用户数据目录下的 cait 子目录($DSH_HOME/cait)。支持绝对路径、相对路径和 ~。',
|
|
20
|
+
),
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
function expandHome(value) {
|
|
24
|
+
if (value === '~') return homedir()
|
|
25
|
+
if (value.startsWith('~/') || value.startsWith('~\\')) return path.join(homedir(), value.slice(2))
|
|
26
|
+
return value
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function resolveDataDir(configured) {
|
|
30
|
+
const custom = String(configured ?? '').trim()
|
|
31
|
+
if (custom) return path.resolve(expandHome(custom))
|
|
32
|
+
|
|
33
|
+
const dshHome = String(process.env.DSH_HOME ?? '').trim()
|
|
34
|
+
return path.join(path.resolve(dshHome || path.join(homedir(), '.dsh')), 'cait')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function apply(ctx, config = {}) {
|
|
38
|
+
const store = new CatStore({
|
|
39
|
+
dataDir: resolveDataDir(config?.dataDir),
|
|
40
|
+
mtRuntime: createMtRuntime(ctx),
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
ctx.effect(() => registerCatTools(ctx, store))
|
|
44
|
+
ctx.effect(() => registerCatApi(ctx, store))
|
|
45
|
+
}
|
package/lib/api.js
CHANGED
|
@@ -1,223 +1,223 @@
|
|
|
1
|
-
import { CatError } from './store.js'
|
|
2
|
-
import { CAIT_VERSION } from './store-utils.js'
|
|
3
|
-
|
|
4
|
-
const API_PREFIX = '/cait/api'
|
|
5
|
-
const MAX_BODY = 32 * 1024 * 1024
|
|
6
|
-
|
|
7
|
-
const MUTATIONS = new Set([
|
|
8
|
-
'createDocument',
|
|
9
|
-
'deleteDocument',
|
|
10
|
-
'openDocument',
|
|
11
|
-
'closeDocument',
|
|
12
|
-
'importFile',
|
|
13
|
-
'importTmx',
|
|
14
|
-
'loadDemo',
|
|
15
|
-
'selectSegment',
|
|
16
|
-
'saveDraft',
|
|
17
|
-
'confirmSegment',
|
|
18
|
-
'moveSegment',
|
|
19
|
-
'addTM',
|
|
20
|
-
'importTM',
|
|
21
|
-
'clearTM',
|
|
22
|
-
'addGlossary',
|
|
23
|
-
'updateGlossary',
|
|
24
|
-
'deleteGlossary',
|
|
25
|
-
'importGlossary',
|
|
26
|
-
'clearGlossary',
|
|
27
|
-
'createLibrary',
|
|
28
|
-
'setMounts',
|
|
29
|
-
'saveSettings',
|
|
30
|
-
])
|
|
31
|
-
|
|
32
|
-
function sendJson(res, status, payload) {
|
|
33
|
-
if (res.headersSent) return
|
|
34
|
-
const body = JSON.stringify(payload)
|
|
35
|
-
res.writeHead(status, {
|
|
36
|
-
'Content-Type': 'application/json; charset=utf-8',
|
|
37
|
-
'Content-Length': Buffer.byteLength(body),
|
|
38
|
-
'Cache-Control': 'no-store',
|
|
39
|
-
'X-Content-Type-Options': 'nosniff',
|
|
40
|
-
})
|
|
41
|
-
res.end(body)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function readBody(req) {
|
|
45
|
-
return new Promise((resolve, reject) => {
|
|
46
|
-
const chunks = []
|
|
47
|
-
let size = 0
|
|
48
|
-
let settled = false
|
|
49
|
-
|
|
50
|
-
const fail = (error) => {
|
|
51
|
-
if (settled) return
|
|
52
|
-
settled = true
|
|
53
|
-
req.removeListener('data', onData)
|
|
54
|
-
req.removeListener('end', onEnd)
|
|
55
|
-
req.removeListener('error', onError)
|
|
56
|
-
req.resume()
|
|
57
|
-
reject(error)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const onData = (chunk) => {
|
|
61
|
-
size += chunk.length
|
|
62
|
-
if (size > MAX_BODY) {
|
|
63
|
-
fail(new CatError('BODY_TOO_LARGE', '请求体过大。', 413))
|
|
64
|
-
return
|
|
65
|
-
}
|
|
66
|
-
chunks.push(chunk)
|
|
67
|
-
}
|
|
68
|
-
const onEnd = () => {
|
|
69
|
-
if (settled) return
|
|
70
|
-
settled = true
|
|
71
|
-
resolve(Buffer.concat(chunks).toString('utf8'))
|
|
72
|
-
}
|
|
73
|
-
const onError = (error) => {
|
|
74
|
-
if (settled) return
|
|
75
|
-
settled = true
|
|
76
|
-
reject(error)
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
req.on('data', onData)
|
|
80
|
-
req.on('end', onEnd)
|
|
81
|
-
req.on('error', onError)
|
|
82
|
-
})
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function confirmDangerous(fn) {
|
|
86
|
-
return (params) => {
|
|
87
|
-
if (params?.confirm !== true) {
|
|
88
|
-
throw new CatError('CONFIRMATION_REQUIRED', '该操作需要二次确认。', 409)
|
|
89
|
-
}
|
|
90
|
-
return fn(params)
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function createHandlers(store) {
|
|
95
|
-
return {
|
|
96
|
-
bootstrap: () => store.bootstrap(),
|
|
97
|
-
listMtRoutes: (params) => store.listMtRoutes(params),
|
|
98
|
-
createDocument: (params) => store.createDocument(params),
|
|
99
|
-
deleteDocument: (params) => store.deleteDocument(params),
|
|
100
|
-
openDocument: (params) => store.openDocument(params),
|
|
101
|
-
closeDocument: (params) => store.closeDocument(params),
|
|
102
|
-
importFile: (params) => store.importFile(params),
|
|
103
|
-
importTmx: (params) => store.importTmx(params),
|
|
104
|
-
loadDemo: () => store.loadDemo(),
|
|
105
|
-
selectSegment: (params) => store.selectSegment(params),
|
|
106
|
-
saveDraft: (params) => store.saveDraft(params),
|
|
107
|
-
confirmSegment: (params) => store.confirmSegment(params),
|
|
108
|
-
moveSegment: (params) => store.moveSegment(params),
|
|
109
|
-
|
|
110
|
-
lookupTM: (params) => {
|
|
111
|
-
const result = store.lookupTM(params)
|
|
112
|
-
return { ...result, count: result.matches.length }
|
|
113
|
-
},
|
|
114
|
-
addTM: (params) => store.addTM(params),
|
|
115
|
-
importTM: (params) => store.importTM(params),
|
|
116
|
-
clearTM: confirmDangerous((params) => store.clearTM(params)),
|
|
117
|
-
|
|
118
|
-
addGlossary: (params) => store.addGlossary(params),
|
|
119
|
-
updateGlossary: (params) => store.updateGlossary(params),
|
|
120
|
-
deleteGlossary: (params) => store.deleteGlossary(params),
|
|
121
|
-
importGlossary: (params) => store.importGlossary(params),
|
|
122
|
-
clearGlossary: confirmDangerous((params) => store.clearGlossary(params)),
|
|
123
|
-
|
|
124
|
-
createLibrary: (params) => store.createLibrary(params),
|
|
125
|
-
setMounts: (params) => store.setMounts(params),
|
|
126
|
-
saveSettings: (params) => store.saveSettings(params),
|
|
127
|
-
mtSuggest: (params) => store.mtSuggest(params),
|
|
128
|
-
exportDocument: (params) => {
|
|
129
|
-
const result = store.exportDocument(params)
|
|
130
|
-
if (result && Buffer.isBuffer(result.content)) {
|
|
131
|
-
return { ...result, content: result.content.toString('base64'), encoding: 'base64' }
|
|
132
|
-
}
|
|
133
|
-
return result
|
|
134
|
-
},
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
export function registerCatApi(ctx, store) {
|
|
139
|
-
let webServer
|
|
140
|
-
let connection
|
|
141
|
-
try {
|
|
142
|
-
webServer = ctx.webServer
|
|
143
|
-
connection = ctx.connection
|
|
144
|
-
} catch {
|
|
145
|
-
return () => {}
|
|
146
|
-
}
|
|
147
|
-
if (!webServer || !connection) return () => {}
|
|
148
|
-
|
|
149
|
-
const handlers = createHandlers(store)
|
|
150
|
-
|
|
151
|
-
const handler = async (req, res) => {
|
|
152
|
-
try {
|
|
153
|
-
const rejection = connection.requestRejection(req)
|
|
154
|
-
if (rejection) {
|
|
155
|
-
sendJson(res, rejection, {
|
|
156
|
-
ok: false,
|
|
157
|
-
error: { code: 'UNAUTHORIZED', message: 'CAIT API 仅接受本机同源请求。' },
|
|
158
|
-
})
|
|
159
|
-
return
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const url = new URL(req.url || '/', 'http://cait.local')
|
|
163
|
-
if (url.pathname === `${API_PREFIX}/health`) {
|
|
164
|
-
if (req.method !== 'GET') {
|
|
165
|
-
sendJson(res, 405, { ok: false, error: { code: 'METHOD_NOT_ALLOWED', message: '只支持 GET。' } })
|
|
166
|
-
return
|
|
167
|
-
}
|
|
168
|
-
sendJson(res, 200, { ok: true, data: { name: 'cait', version: CAIT_VERSION } })
|
|
169
|
-
return
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (url.pathname !== `${API_PREFIX}/rpc`) {
|
|
173
|
-
sendJson(res, 404, { ok: false, error: { code: 'NOT_FOUND', message: '接口不存在。' } })
|
|
174
|
-
return
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
if (req.method !== 'POST') {
|
|
178
|
-
sendJson(res, 405, { ok: false, error: { code: 'METHOD_NOT_ALLOWED', message: '只支持 POST。' } })
|
|
179
|
-
return
|
|
180
|
-
}
|
|
181
|
-
if (!String(req.headers['content-type'] || '').toLowerCase().includes('application/json')) {
|
|
182
|
-
sendJson(res, 415, { ok: false, error: { code: 'UNSUPPORTED_MEDIA_TYPE', message: '请求必须为 JSON。' } })
|
|
183
|
-
return
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const raw = await readBody(req)
|
|
187
|
-
let payload
|
|
188
|
-
try {
|
|
189
|
-
payload = JSON.parse(raw || '{}')
|
|
190
|
-
} catch {
|
|
191
|
-
throw new CatError('INVALID_JSON', '请求 JSON 解析失败。')
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const method = String(payload?.method ?? '')
|
|
195
|
-
if (!Object.hasOwn(handlers, method)) {
|
|
196
|
-
throw new CatError('UNKNOWN_METHOD', `未知的 CAIT 方法: ${method || '(empty)'}`, 404)
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const params = payload.params && typeof payload.params === 'object' ? payload.params : {}
|
|
200
|
-
const result = await handlers[method](params)
|
|
201
|
-
const data = MUTATIONS.has(method)
|
|
202
|
-
? { ...(result ?? {}), state: store.bootstrap() }
|
|
203
|
-
: result
|
|
204
|
-
sendJson(res, 200, { ok: true, data })
|
|
205
|
-
} catch (error) {
|
|
206
|
-
const status = error instanceof CatError ? error.status : 500
|
|
207
|
-
const code = error instanceof CatError ? error.code : 'INTERNAL_ERROR'
|
|
208
|
-
const message = error instanceof CatError
|
|
209
|
-
? error.message
|
|
210
|
-
: 'CAIT 服务发生内部错误。'
|
|
211
|
-
if (!(error instanceof CatError)) {
|
|
212
|
-
try {
|
|
213
|
-
ctx.logger?.('cait')?.error?.(error)
|
|
214
|
-
} catch {
|
|
215
|
-
// logging must never mask the response
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
sendJson(res, status, { ok: false, error: { code, message } })
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
return webServer.register({ kind: 'prefix', path: API_PREFIX, handler })
|
|
223
|
-
}
|
|
1
|
+
import { CatError } from './store.js'
|
|
2
|
+
import { CAIT_VERSION } from './store-utils.js'
|
|
3
|
+
|
|
4
|
+
const API_PREFIX = '/cait/api'
|
|
5
|
+
const MAX_BODY = 32 * 1024 * 1024
|
|
6
|
+
|
|
7
|
+
const MUTATIONS = new Set([
|
|
8
|
+
'createDocument',
|
|
9
|
+
'deleteDocument',
|
|
10
|
+
'openDocument',
|
|
11
|
+
'closeDocument',
|
|
12
|
+
'importFile',
|
|
13
|
+
'importTmx',
|
|
14
|
+
'loadDemo',
|
|
15
|
+
'selectSegment',
|
|
16
|
+
'saveDraft',
|
|
17
|
+
'confirmSegment',
|
|
18
|
+
'moveSegment',
|
|
19
|
+
'addTM',
|
|
20
|
+
'importTM',
|
|
21
|
+
'clearTM',
|
|
22
|
+
'addGlossary',
|
|
23
|
+
'updateGlossary',
|
|
24
|
+
'deleteGlossary',
|
|
25
|
+
'importGlossary',
|
|
26
|
+
'clearGlossary',
|
|
27
|
+
'createLibrary',
|
|
28
|
+
'setMounts',
|
|
29
|
+
'saveSettings',
|
|
30
|
+
])
|
|
31
|
+
|
|
32
|
+
function sendJson(res, status, payload) {
|
|
33
|
+
if (res.headersSent) return
|
|
34
|
+
const body = JSON.stringify(payload)
|
|
35
|
+
res.writeHead(status, {
|
|
36
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
37
|
+
'Content-Length': Buffer.byteLength(body),
|
|
38
|
+
'Cache-Control': 'no-store',
|
|
39
|
+
'X-Content-Type-Options': 'nosniff',
|
|
40
|
+
})
|
|
41
|
+
res.end(body)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function readBody(req) {
|
|
45
|
+
return new Promise((resolve, reject) => {
|
|
46
|
+
const chunks = []
|
|
47
|
+
let size = 0
|
|
48
|
+
let settled = false
|
|
49
|
+
|
|
50
|
+
const fail = (error) => {
|
|
51
|
+
if (settled) return
|
|
52
|
+
settled = true
|
|
53
|
+
req.removeListener('data', onData)
|
|
54
|
+
req.removeListener('end', onEnd)
|
|
55
|
+
req.removeListener('error', onError)
|
|
56
|
+
req.resume()
|
|
57
|
+
reject(error)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const onData = (chunk) => {
|
|
61
|
+
size += chunk.length
|
|
62
|
+
if (size > MAX_BODY) {
|
|
63
|
+
fail(new CatError('BODY_TOO_LARGE', '请求体过大。', 413))
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
chunks.push(chunk)
|
|
67
|
+
}
|
|
68
|
+
const onEnd = () => {
|
|
69
|
+
if (settled) return
|
|
70
|
+
settled = true
|
|
71
|
+
resolve(Buffer.concat(chunks).toString('utf8'))
|
|
72
|
+
}
|
|
73
|
+
const onError = (error) => {
|
|
74
|
+
if (settled) return
|
|
75
|
+
settled = true
|
|
76
|
+
reject(error)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
req.on('data', onData)
|
|
80
|
+
req.on('end', onEnd)
|
|
81
|
+
req.on('error', onError)
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function confirmDangerous(fn) {
|
|
86
|
+
return (params) => {
|
|
87
|
+
if (params?.confirm !== true) {
|
|
88
|
+
throw new CatError('CONFIRMATION_REQUIRED', '该操作需要二次确认。', 409)
|
|
89
|
+
}
|
|
90
|
+
return fn(params)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function createHandlers(store) {
|
|
95
|
+
return {
|
|
96
|
+
bootstrap: () => store.bootstrap(),
|
|
97
|
+
listMtRoutes: (params) => store.listMtRoutes(params),
|
|
98
|
+
createDocument: (params) => store.createDocument(params),
|
|
99
|
+
deleteDocument: (params) => store.deleteDocument(params),
|
|
100
|
+
openDocument: (params) => store.openDocument(params),
|
|
101
|
+
closeDocument: (params) => store.closeDocument(params),
|
|
102
|
+
importFile: (params) => store.importFile(params),
|
|
103
|
+
importTmx: (params) => store.importTmx(params),
|
|
104
|
+
loadDemo: () => store.loadDemo(),
|
|
105
|
+
selectSegment: (params) => store.selectSegment(params),
|
|
106
|
+
saveDraft: (params) => store.saveDraft(params),
|
|
107
|
+
confirmSegment: (params) => store.confirmSegment(params),
|
|
108
|
+
moveSegment: (params) => store.moveSegment(params),
|
|
109
|
+
|
|
110
|
+
lookupTM: (params) => {
|
|
111
|
+
const result = store.lookupTM(params)
|
|
112
|
+
return { ...result, count: result.matches.length }
|
|
113
|
+
},
|
|
114
|
+
addTM: (params) => store.addTM(params),
|
|
115
|
+
importTM: (params) => store.importTM(params),
|
|
116
|
+
clearTM: confirmDangerous((params) => store.clearTM(params)),
|
|
117
|
+
|
|
118
|
+
addGlossary: (params) => store.addGlossary(params),
|
|
119
|
+
updateGlossary: (params) => store.updateGlossary(params),
|
|
120
|
+
deleteGlossary: (params) => store.deleteGlossary(params),
|
|
121
|
+
importGlossary: (params) => store.importGlossary(params),
|
|
122
|
+
clearGlossary: confirmDangerous((params) => store.clearGlossary(params)),
|
|
123
|
+
|
|
124
|
+
createLibrary: (params) => store.createLibrary(params),
|
|
125
|
+
setMounts: (params) => store.setMounts(params),
|
|
126
|
+
saveSettings: (params) => store.saveSettings(params),
|
|
127
|
+
mtSuggest: (params) => store.mtSuggest(params),
|
|
128
|
+
exportDocument: (params) => {
|
|
129
|
+
const result = store.exportDocument(params)
|
|
130
|
+
if (result && Buffer.isBuffer(result.content)) {
|
|
131
|
+
return { ...result, content: result.content.toString('base64'), encoding: 'base64' }
|
|
132
|
+
}
|
|
133
|
+
return result
|
|
134
|
+
},
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function registerCatApi(ctx, store) {
|
|
139
|
+
let webServer
|
|
140
|
+
let connection
|
|
141
|
+
try {
|
|
142
|
+
webServer = ctx.webServer
|
|
143
|
+
connection = ctx.connection
|
|
144
|
+
} catch {
|
|
145
|
+
return () => {}
|
|
146
|
+
}
|
|
147
|
+
if (!webServer || !connection) return () => {}
|
|
148
|
+
|
|
149
|
+
const handlers = createHandlers(store)
|
|
150
|
+
|
|
151
|
+
const handler = async (req, res) => {
|
|
152
|
+
try {
|
|
153
|
+
const rejection = connection.requestRejection(req)
|
|
154
|
+
if (rejection) {
|
|
155
|
+
sendJson(res, rejection, {
|
|
156
|
+
ok: false,
|
|
157
|
+
error: { code: 'UNAUTHORIZED', message: 'CAIT API 仅接受本机同源请求。' },
|
|
158
|
+
})
|
|
159
|
+
return
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const url = new URL(req.url || '/', 'http://cait.local')
|
|
163
|
+
if (url.pathname === `${API_PREFIX}/health`) {
|
|
164
|
+
if (req.method !== 'GET') {
|
|
165
|
+
sendJson(res, 405, { ok: false, error: { code: 'METHOD_NOT_ALLOWED', message: '只支持 GET。' } })
|
|
166
|
+
return
|
|
167
|
+
}
|
|
168
|
+
sendJson(res, 200, { ok: true, data: { name: 'cait', version: CAIT_VERSION } })
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (url.pathname !== `${API_PREFIX}/rpc`) {
|
|
173
|
+
sendJson(res, 404, { ok: false, error: { code: 'NOT_FOUND', message: '接口不存在。' } })
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (req.method !== 'POST') {
|
|
178
|
+
sendJson(res, 405, { ok: false, error: { code: 'METHOD_NOT_ALLOWED', message: '只支持 POST。' } })
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
if (!String(req.headers['content-type'] || '').toLowerCase().includes('application/json')) {
|
|
182
|
+
sendJson(res, 415, { ok: false, error: { code: 'UNSUPPORTED_MEDIA_TYPE', message: '请求必须为 JSON。' } })
|
|
183
|
+
return
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const raw = await readBody(req)
|
|
187
|
+
let payload
|
|
188
|
+
try {
|
|
189
|
+
payload = JSON.parse(raw || '{}')
|
|
190
|
+
} catch {
|
|
191
|
+
throw new CatError('INVALID_JSON', '请求 JSON 解析失败。')
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const method = String(payload?.method ?? '')
|
|
195
|
+
if (!Object.hasOwn(handlers, method)) {
|
|
196
|
+
throw new CatError('UNKNOWN_METHOD', `未知的 CAIT 方法: ${method || '(empty)'}`, 404)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const params = payload.params && typeof payload.params === 'object' ? payload.params : {}
|
|
200
|
+
const result = await handlers[method](params)
|
|
201
|
+
const data = MUTATIONS.has(method)
|
|
202
|
+
? { ...(result ?? {}), state: store.bootstrap() }
|
|
203
|
+
: result
|
|
204
|
+
sendJson(res, 200, { ok: true, data })
|
|
205
|
+
} catch (error) {
|
|
206
|
+
const status = error instanceof CatError ? error.status : 500
|
|
207
|
+
const code = error instanceof CatError ? error.code : 'INTERNAL_ERROR'
|
|
208
|
+
const message = error instanceof CatError
|
|
209
|
+
? error.message
|
|
210
|
+
: 'CAIT 服务发生内部错误。'
|
|
211
|
+
if (!(error instanceof CatError)) {
|
|
212
|
+
try {
|
|
213
|
+
ctx.logger?.('cait')?.error?.(error)
|
|
214
|
+
} catch {
|
|
215
|
+
// logging must never mask the response
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
sendJson(res, status, { ok: false, error: { code, message } })
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return webServer.register({ kind: 'prefix', path: API_PREFIX, handler })
|
|
223
|
+
}
|