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/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
+ }