opencode-translate 1.0.6 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +77 -6
- package/dist/index.js +873 -0
- package/index.d.ts +5 -0
- package/package.json +15 -16
- package/src/activation/chat-message.ts +0 -189
- package/src/activation/index.ts +0 -38
- package/src/activation/logging.ts +0 -11
- package/src/activation/messages-transform.ts +0 -44
- package/src/activation/metadata.ts +0 -41
- package/src/activation/parts.ts +0 -46
- package/src/activation/question-hooks.ts +0 -126
- package/src/activation/state.ts +0 -97
- package/src/activation/text-complete.ts +0 -50
- package/src/activation/trigger.ts +0 -57
- package/src/activation/types.ts +0 -47
- package/src/activation.ts +0 -1
- package/src/anthropic-oauth.ts +0 -148
- package/src/auth/codex-request.ts +0 -108
- package/src/auth/codex-response.ts +0 -78
- package/src/auth/codex-shared.ts +0 -3
- package/src/auth/headers.ts +0 -18
- package/src/auth/index.ts +0 -177
- package/src/auth/oauth-fetch.ts +0 -100
- package/src/auth/refresh.ts +0 -102
- package/src/auth/retry.ts +0 -70
- package/src/auth/store.ts +0 -98
- package/src/auth/types.ts +0 -27
- package/src/auth.ts +0 -1
- package/src/constants/errors.ts +0 -24
- package/src/constants/guards.ts +0 -33
- package/src/constants/options.ts +0 -55
- package/src/constants/plugin.ts +0 -9
- package/src/constants/types.ts +0 -159
- package/src/constants.ts +0 -5
- package/src/formatting.ts +0 -157
- package/src/index.ts +0 -7
- package/src/labels.ts +0 -3
- package/src/prompts.ts +0 -123
- package/src/question-tool.ts +0 -234
- package/src/translator/index.ts +0 -172
- package/src/translator/part-id.ts +0 -43
- package/src/translator/provider.ts +0 -411
- package/src/translator/retry.ts +0 -62
- package/src/translator/types.ts +0 -24
- package/src/translator.ts +0 -1
package/src/question-tool.ts
DELETED
|
@@ -1,234 +0,0 @@
|
|
|
1
|
-
// Translation layer for OpenCode's built-in `question` tool.
|
|
2
|
-
//
|
|
3
|
-
// Flow:
|
|
4
|
-
// 1. Agent (main LLM, English-only) invokes the `question` tool with an
|
|
5
|
-
// `args.questions[]` payload in English.
|
|
6
|
-
// 2. `tool.execute.before` hook translates all question text, headers, and
|
|
7
|
-
// option labels + descriptions into the configured `lang` so the question
|
|
8
|
-
// prompt renders in the user's language.
|
|
9
|
-
// 3. OpenCode publishes `question.asked`; the TUI shows the translated
|
|
10
|
-
// dialog and the user picks an option (or types a custom answer).
|
|
11
|
-
// 4. `tool.execute.after` hook reverses the substitution using the
|
|
12
|
-
// snapshot we captured in step 2. Selected options are restored by
|
|
13
|
-
// label mapping; non-empty custom answers are translated like normal
|
|
14
|
-
// user messages, so the output delivered back to the LLM stays in
|
|
15
|
-
// English.
|
|
16
|
-
//
|
|
17
|
-
// A per-callID snapshot is kept so mapping a user-selected translated label
|
|
18
|
-
// back to its original English label is deterministic.
|
|
19
|
-
|
|
20
|
-
import { unwrapEchoedTextEnvelope } from "./prompts"
|
|
21
|
-
|
|
22
|
-
type TextRecord = { question: string; header: string; options: OptionRecord[]; multiple?: boolean; custom?: boolean }
|
|
23
|
-
type OptionRecord = { label: string; description: string }
|
|
24
|
-
|
|
25
|
-
export interface QuestionArgs {
|
|
26
|
-
questions: TextRecord[]
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface QuestionSnapshot {
|
|
30
|
-
original: TextRecord[]
|
|
31
|
-
translated: TextRecord[]
|
|
32
|
-
userLanguage: string
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export interface QuestionToolOutput {
|
|
36
|
-
title?: string
|
|
37
|
-
output?: string
|
|
38
|
-
metadata?: { answers?: readonly (readonly string[])[] } | Record<string, unknown>
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export interface RestoreQuestionOutputOptions {
|
|
42
|
-
translateCustomAnswers?: (texts: readonly string[]) => Promise<readonly string[]>
|
|
43
|
-
onTranslationError?: (error: unknown) => Promise<void> | void
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
interface TranslatableField {
|
|
47
|
-
text: string
|
|
48
|
-
set(value: string): void
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
interface CustomAnswerSlot {
|
|
52
|
-
questionIndex: number
|
|
53
|
-
answerIndex: number
|
|
54
|
-
text: string
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function cloneQuestion(q: TextRecord): TextRecord {
|
|
58
|
-
return {
|
|
59
|
-
question: q.question,
|
|
60
|
-
header: q.header,
|
|
61
|
-
options: q.options.map((option) => ({ label: option.label, description: option.description })),
|
|
62
|
-
...(q.multiple !== undefined ? { multiple: q.multiple } : {}),
|
|
63
|
-
...(q.custom !== undefined ? { custom: q.custom } : {}),
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function snapshotQuestions(args: QuestionArgs): TextRecord[] {
|
|
68
|
-
return args.questions.map(cloneQuestion)
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function restoreQuestionArgs(args: QuestionArgs, original: readonly TextRecord[]): void {
|
|
72
|
-
args.questions.splice(0, args.questions.length, ...original.map(cloneQuestion))
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export function isQuestionArgs(value: unknown): value is QuestionArgs {
|
|
76
|
-
if (!value || typeof value !== "object") return false
|
|
77
|
-
const questions = (value as Record<string, unknown>).questions
|
|
78
|
-
if (!Array.isArray(questions)) return false
|
|
79
|
-
for (const q of questions) {
|
|
80
|
-
if (!q || typeof q !== "object") return false
|
|
81
|
-
const record = q as Record<string, unknown>
|
|
82
|
-
if (typeof record.question !== "string") return false
|
|
83
|
-
if (typeof record.header !== "string") return false
|
|
84
|
-
if (!Array.isArray(record.options)) return false
|
|
85
|
-
for (const opt of record.options) {
|
|
86
|
-
if (!opt || typeof opt !== "object") return false
|
|
87
|
-
const optRecord = opt as Record<string, unknown>
|
|
88
|
-
if (typeof optRecord.label !== "string") return false
|
|
89
|
-
if (typeof optRecord.description !== "string") return false
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
return true
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Translate every display-facing string in one batch, then commit the
|
|
96
|
-
// translated clone only after the batch succeeds.
|
|
97
|
-
export async function translateQuestionArgs(
|
|
98
|
-
args: QuestionArgs,
|
|
99
|
-
translate: (texts: readonly string[]) => Promise<readonly string[]>,
|
|
100
|
-
): Promise<void> {
|
|
101
|
-
const translatedQuestions = snapshotQuestions(args)
|
|
102
|
-
const fields: TranslatableField[] = []
|
|
103
|
-
|
|
104
|
-
function addField(text: string, set: (value: string) => void) {
|
|
105
|
-
if (text.length === 0) return
|
|
106
|
-
fields.push({ text, set })
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
for (const q of translatedQuestions) {
|
|
110
|
-
addField(q.question, (value) => {
|
|
111
|
-
q.question = value
|
|
112
|
-
})
|
|
113
|
-
addField(q.header, (value) => {
|
|
114
|
-
q.header = value
|
|
115
|
-
})
|
|
116
|
-
for (const option of q.options) {
|
|
117
|
-
addField(option.label, (value) => {
|
|
118
|
-
option.label = value
|
|
119
|
-
})
|
|
120
|
-
addField(option.description, (value) => {
|
|
121
|
-
option.description = value
|
|
122
|
-
})
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (fields.length === 0) return
|
|
127
|
-
|
|
128
|
-
const translated = await translate(fields.map((field) => field.text))
|
|
129
|
-
if (translated.length !== fields.length) {
|
|
130
|
-
throw new Error(`Question translator returned ${translated.length} translations for ${fields.length} fields`)
|
|
131
|
-
}
|
|
132
|
-
for (const [index, field] of fields.entries()) {
|
|
133
|
-
field.set(unwrapEchoedTextEnvelope(translated[index]))
|
|
134
|
-
}
|
|
135
|
-
args.questions.splice(0, args.questions.length, ...translatedQuestions)
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function restoreOptionLabel(
|
|
139
|
-
selectedLabel: string,
|
|
140
|
-
translatedOptions: readonly OptionRecord[],
|
|
141
|
-
originalOptions: readonly OptionRecord[],
|
|
142
|
-
): string | undefined {
|
|
143
|
-
const idx = translatedOptions.findIndex((option) => option.label === selectedLabel)
|
|
144
|
-
if (idx < 0) return undefined
|
|
145
|
-
return originalOptions[idx]?.label ?? selectedLabel
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
async function restoreQuestionAnswers(
|
|
149
|
-
original: readonly TextRecord[],
|
|
150
|
-
translated: readonly TextRecord[],
|
|
151
|
-
answers: readonly (readonly string[])[],
|
|
152
|
-
options: RestoreQuestionOutputOptions = {},
|
|
153
|
-
): Promise<string[][]> {
|
|
154
|
-
const translateCustomAnswers = options.translateCustomAnswers
|
|
155
|
-
const customSlots: CustomAnswerSlot[] = []
|
|
156
|
-
const restored = original.map((q, questionIndex) => {
|
|
157
|
-
const selected = answers[questionIndex] ?? []
|
|
158
|
-
const translatedOptions = translated[questionIndex]?.options ?? []
|
|
159
|
-
const originalOptions = q.options
|
|
160
|
-
|
|
161
|
-
return selected.map((label, answerIndex) => {
|
|
162
|
-
const restoredLabel = restoreOptionLabel(label, translatedOptions, originalOptions)
|
|
163
|
-
if (restoredLabel !== undefined) return restoredLabel
|
|
164
|
-
if (!translateCustomAnswers || label.trim().length === 0) return label
|
|
165
|
-
|
|
166
|
-
customSlots.push({ questionIndex, answerIndex, text: label })
|
|
167
|
-
return label
|
|
168
|
-
})
|
|
169
|
-
})
|
|
170
|
-
|
|
171
|
-
if (!translateCustomAnswers || customSlots.length === 0) return restored
|
|
172
|
-
|
|
173
|
-
try {
|
|
174
|
-
const translatedCustomAnswers = await translateCustomAnswers(customSlots.map((slot) => slot.text))
|
|
175
|
-
if (translatedCustomAnswers.length !== customSlots.length) {
|
|
176
|
-
throw new Error(
|
|
177
|
-
`Question custom-answer translator returned ${translatedCustomAnswers.length} translations for ${customSlots.length} answers`,
|
|
178
|
-
)
|
|
179
|
-
}
|
|
180
|
-
for (const [index, slot] of customSlots.entries()) {
|
|
181
|
-
restored[slot.questionIndex][slot.answerIndex] = unwrapEchoedTextEnvelope(translatedCustomAnswers[index])
|
|
182
|
-
}
|
|
183
|
-
} catch (error) {
|
|
184
|
-
await options.onTranslationError?.(error)
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
return restored
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function formatRestoredOutput(original: readonly TextRecord[], answers: readonly (readonly string[])[]): string {
|
|
191
|
-
const formattedParts = original.map((q, i) => {
|
|
192
|
-
const restored = answers[i] ?? []
|
|
193
|
-
const rendered = restored.length > 0 ? restored.join(", ") : "Unanswered"
|
|
194
|
-
return `"${q.question}"="${rendered}"`
|
|
195
|
-
})
|
|
196
|
-
const formatted = formattedParts.join(", ")
|
|
197
|
-
return `User has answered your questions: ${formatted}. You can now continue with the user's answers in mind.`
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// Reconstruct the exact output string the question tool would have produced
|
|
201
|
-
// if it had been called with the original English args. Mirrors the format
|
|
202
|
-
// in `packages/opencode/src/tool/question.ts` (as of opencode 1.14.x).
|
|
203
|
-
export async function buildRestoredOutput(
|
|
204
|
-
original: readonly TextRecord[],
|
|
205
|
-
translated: readonly TextRecord[],
|
|
206
|
-
answers: readonly (readonly string[])[],
|
|
207
|
-
options: RestoreQuestionOutputOptions = {},
|
|
208
|
-
): Promise<string> {
|
|
209
|
-
const restoredAnswers = await restoreQuestionAnswers(original, translated, answers, options)
|
|
210
|
-
return formatRestoredOutput(original, restoredAnswers)
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
function mutableMetadata(output: QuestionToolOutput): Record<string, unknown> {
|
|
214
|
-
if (output.metadata && typeof output.metadata === "object" && !Array.isArray(output.metadata)) {
|
|
215
|
-
return output.metadata as Record<string, unknown>
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const metadata: Record<string, unknown> = {}
|
|
219
|
-
output.metadata = metadata
|
|
220
|
-
return metadata
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
export async function restoreQuestionOutput(
|
|
224
|
-
output: QuestionToolOutput,
|
|
225
|
-
snapshot: QuestionSnapshot,
|
|
226
|
-
options: RestoreQuestionOutputOptions = {},
|
|
227
|
-
): Promise<void> {
|
|
228
|
-
if (typeof output.output !== "string") return
|
|
229
|
-
const answersRaw = (output.metadata as { answers?: readonly (readonly string[])[] } | undefined)?.answers
|
|
230
|
-
const answers = Array.isArray(answersRaw) ? answersRaw : []
|
|
231
|
-
const restoredAnswers = await restoreQuestionAnswers(snapshot.original, snapshot.translated, answers, options)
|
|
232
|
-
output.output = formatRestoredOutput(snapshot.original, restoredAnswers)
|
|
233
|
-
mutableMetadata(output).answers = restoredAnswers
|
|
234
|
-
}
|
package/src/translator/index.ts
DELETED
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
import { setTimeout as sleep } from "node:timers/promises"
|
|
2
|
-
import { generateText, type LanguageModel } from "ai"
|
|
3
|
-
import { createCredentialResolver } from "../auth"
|
|
4
|
-
import {
|
|
5
|
-
buildAuthUnavailableError,
|
|
6
|
-
PLUGIN_NAME,
|
|
7
|
-
type PluginClientLike,
|
|
8
|
-
type ProviderInfo,
|
|
9
|
-
parseTranslatorModel,
|
|
10
|
-
type ResolvedTranslateOptions,
|
|
11
|
-
} from "../constants"
|
|
12
|
-
import {
|
|
13
|
-
buildBatchSystemPrompt,
|
|
14
|
-
buildBatchUserPrompt,
|
|
15
|
-
buildSystemPrompt,
|
|
16
|
-
buildUserPrompt,
|
|
17
|
-
parseBatchSegments,
|
|
18
|
-
unwrapEchoedTextEnvelope,
|
|
19
|
-
} from "../prompts"
|
|
20
|
-
import { __resetSyntheticPartIDForTest } from "./part-id"
|
|
21
|
-
import {
|
|
22
|
-
__resetProviderFactoryCacheForTest,
|
|
23
|
-
buildVariantProviderOptions,
|
|
24
|
-
instantiateModel,
|
|
25
|
-
instantiateProvider,
|
|
26
|
-
loadFactory,
|
|
27
|
-
resolveModelInfo,
|
|
28
|
-
supportsTemperature,
|
|
29
|
-
} from "./provider"
|
|
30
|
-
import { withRetry } from "./retry"
|
|
31
|
-
import type { TranslateTextInput, TranslateTextsInput, TranslatorDependencies } from "./types"
|
|
32
|
-
|
|
33
|
-
const DEFAULT_TRANSLATE_TIMEOUT_MS = 180_000
|
|
34
|
-
|
|
35
|
-
function withTimeout<T>(promise: Promise<T>, timeoutMs: number, label: string): Promise<T> {
|
|
36
|
-
return new Promise<T>((resolve, reject) => {
|
|
37
|
-
const timer = setTimeout(() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)), timeoutMs)
|
|
38
|
-
promise.then(
|
|
39
|
-
(value) => {
|
|
40
|
-
clearTimeout(timer)
|
|
41
|
-
resolve(value)
|
|
42
|
-
},
|
|
43
|
-
(error) => {
|
|
44
|
-
clearTimeout(timer)
|
|
45
|
-
reject(error)
|
|
46
|
-
},
|
|
47
|
-
)
|
|
48
|
-
})
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function isAuthMessage(error: unknown): boolean {
|
|
52
|
-
if (!(error instanceof Error)) return false
|
|
53
|
-
return error.message.includes(":AUTH_UNAVAILABLE]") || error.message.includes(":OAUTH_REFRESH_FAILED]")
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function modelProviderHint(providerID: string, provider?: ProviderInfo): Error {
|
|
57
|
-
return buildAuthUnavailableError(providerID, provider?.env[0] || "the provider's API key env var")
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export function __resetTranslatorCachesForTest() {
|
|
61
|
-
__resetProviderFactoryCacheForTest()
|
|
62
|
-
__resetSyntheticPartIDForTest()
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function createTranslator(
|
|
66
|
-
client: PluginClientLike,
|
|
67
|
-
options: ResolvedTranslateOptions,
|
|
68
|
-
deps: TranslatorDependencies = {},
|
|
69
|
-
) {
|
|
70
|
-
const sleepImpl = deps.sleep ?? ((ms: number) => sleep(ms))
|
|
71
|
-
const now = deps.now ?? (() => Date.now())
|
|
72
|
-
const generateTextImpl = deps.generateTextImpl ?? generateText
|
|
73
|
-
const credentialResolver = deps.credentialResolver ?? createCredentialResolver(client)
|
|
74
|
-
const timeoutMs = deps.timeoutMs ?? DEFAULT_TRANSLATE_TIMEOUT_MS
|
|
75
|
-
|
|
76
|
-
async function generateFromPrompts(system: string, prompt: string): Promise<string> {
|
|
77
|
-
const { providerID, modelID } = parseTranslatorModel(options.model)
|
|
78
|
-
const credentials = await credentialResolver.resolve(options.model)
|
|
79
|
-
const modelInfo = resolveModelInfo(credentials.provider, modelID)
|
|
80
|
-
const variantProviderOptions = buildVariantProviderOptions(providerID, modelID, modelInfo, options.variant)
|
|
81
|
-
const factory = await loadFactory(providerID, modelInfo)
|
|
82
|
-
const provider = instantiateProvider(factory, providerID, credentials, modelInfo)
|
|
83
|
-
const providerOptions = { ...(credentials.provider?.options ?? {}), ...(modelInfo.options ?? {}) }
|
|
84
|
-
const model = instantiateModel(provider, modelID, providerID, modelInfo, providerOptions) as LanguageModel
|
|
85
|
-
|
|
86
|
-
return withRetry(async () => {
|
|
87
|
-
try {
|
|
88
|
-
const result = await withTimeout(
|
|
89
|
-
generateTextImpl({
|
|
90
|
-
model,
|
|
91
|
-
system,
|
|
92
|
-
...(supportsTemperature(providerID, modelID, modelInfo) ? { temperature: 0 } : {}),
|
|
93
|
-
...(variantProviderOptions ? { providerOptions: variantProviderOptions } : {}),
|
|
94
|
-
prompt,
|
|
95
|
-
}),
|
|
96
|
-
timeoutMs,
|
|
97
|
-
"Translator generateText",
|
|
98
|
-
)
|
|
99
|
-
return result.text
|
|
100
|
-
} catch (error) {
|
|
101
|
-
if (isAuthMessage(error)) throw error
|
|
102
|
-
if (credentials.mode === "default" && credentialResolver.isMissingCredentialError(error)) {
|
|
103
|
-
throw modelProviderHint(providerID, credentials.provider)
|
|
104
|
-
}
|
|
105
|
-
throw error
|
|
106
|
-
}
|
|
107
|
-
}, sleepImpl)
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
async function translateText(input: TranslateTextInput): Promise<string> {
|
|
111
|
-
if (!input.text) return input.text
|
|
112
|
-
if (input.sourceLanguage === input.targetLanguage) return input.text
|
|
113
|
-
|
|
114
|
-
const startedAt = now()
|
|
115
|
-
const rawTranslated = await generateFromPrompts(buildSystemPrompt(input), buildUserPrompt(input))
|
|
116
|
-
const translated = unwrapEchoedTextEnvelope(rawTranslated)
|
|
117
|
-
|
|
118
|
-
if (options.verbose) {
|
|
119
|
-
await client.app.log({
|
|
120
|
-
body: {
|
|
121
|
-
service: PLUGIN_NAME,
|
|
122
|
-
level: "info",
|
|
123
|
-
message: "translated",
|
|
124
|
-
extra: {
|
|
125
|
-
direction: input.direction,
|
|
126
|
-
chars_in: input.text.length,
|
|
127
|
-
chars_out: translated.length,
|
|
128
|
-
ms: now() - startedAt,
|
|
129
|
-
cached: false,
|
|
130
|
-
model: options.model,
|
|
131
|
-
},
|
|
132
|
-
},
|
|
133
|
-
})
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
return translated
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
async function translateTexts(input: TranslateTextsInput): Promise<string[]> {
|
|
140
|
-
if (input.texts.length === 0) return []
|
|
141
|
-
if (input.sourceLanguage === input.targetLanguage) return [...input.texts]
|
|
142
|
-
|
|
143
|
-
const startedAt = now()
|
|
144
|
-
const rawTranslated = await generateFromPrompts(buildBatchSystemPrompt(input), buildBatchUserPrompt(input))
|
|
145
|
-
const translated = parseBatchSegments(rawTranslated, input.texts.length).map(unwrapEchoedTextEnvelope)
|
|
146
|
-
|
|
147
|
-
if (options.verbose) {
|
|
148
|
-
await client.app.log({
|
|
149
|
-
body: {
|
|
150
|
-
service: PLUGIN_NAME,
|
|
151
|
-
level: "info",
|
|
152
|
-
message: "translated",
|
|
153
|
-
extra: {
|
|
154
|
-
direction: input.direction,
|
|
155
|
-
chars_in: input.texts.reduce((total, text) => total + text.length, 0),
|
|
156
|
-
chars_out: translated.reduce((total, text) => total + text.length, 0),
|
|
157
|
-
segments: input.texts.length,
|
|
158
|
-
ms: now() - startedAt,
|
|
159
|
-
cached: false,
|
|
160
|
-
model: options.model,
|
|
161
|
-
},
|
|
162
|
-
},
|
|
163
|
-
})
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return translated
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
return { translateText, translateTexts }
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
export { __resetSyntheticPartIDForTest, createSyntheticPartID, hashText } from "./part-id"
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { createHash, randomBytes } from "node:crypto"
|
|
2
|
-
|
|
3
|
-
const PART_ID_LENGTH = 26
|
|
4
|
-
const PART_ID_PREFIX = "prt"
|
|
5
|
-
const BASE62_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
|
6
|
-
|
|
7
|
-
let partLastTimestamp = 0
|
|
8
|
-
let partCounter = 0
|
|
9
|
-
|
|
10
|
-
export function __resetSyntheticPartIDForTest() {
|
|
11
|
-
partLastTimestamp = 0
|
|
12
|
-
partCounter = 0
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function randomBase62(length: number): string {
|
|
16
|
-
const bytes = randomBytes(length)
|
|
17
|
-
let result = ""
|
|
18
|
-
for (let index = 0; index < length; index += 1) {
|
|
19
|
-
result += BASE62_CHARS[bytes[index] % BASE62_CHARS.length]
|
|
20
|
-
}
|
|
21
|
-
return result
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function hashText(text: string): string {
|
|
25
|
-
return createHash("sha256").update(text, "utf8").digest("hex").slice(0, 16)
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function createSyntheticPartID(): string {
|
|
29
|
-
const currentTimestamp = Date.now()
|
|
30
|
-
if (currentTimestamp !== partLastTimestamp) {
|
|
31
|
-
partLastTimestamp = currentTimestamp
|
|
32
|
-
partCounter = 0
|
|
33
|
-
}
|
|
34
|
-
partCounter += 1
|
|
35
|
-
|
|
36
|
-
const encoded = BigInt(currentTimestamp) * BigInt(0x1000) + BigInt(partCounter)
|
|
37
|
-
const timeBytes = Buffer.alloc(6)
|
|
38
|
-
for (let index = 0; index < 6; index += 1) {
|
|
39
|
-
timeBytes[index] = Number((encoded >> BigInt(40 - 8 * index)) & BigInt(0xff))
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return `${PART_ID_PREFIX}_${timeBytes.toString("hex")}${randomBase62(PART_ID_LENGTH - 12)}`
|
|
43
|
-
}
|