Spaces:
Configuration error
Configuration error
Deploy from community-bots@55469478c3a131b0ee2704d23b4ab5d4ce087f77
Browse files- README.md +6 -10
- package.json +1 -1
- server.mjs +62 -438
README.md
CHANGED
|
@@ -1,11 +1,7 @@
|
|
| 1 |
-
--
|
| 2 |
-
title: OpenKotOR Trask API
|
| 3 |
-
emoji: 📚
|
| 4 |
-
colorFrom: gray
|
| 5 |
-
colorTo: red
|
| 6 |
-
sdk: docker
|
| 7 |
-
app_port: 7860
|
| 8 |
-
pinned: false
|
| 9 |
-
---
|
| 10 |
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# holocron-trask-api (deprecated)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
+
This Hugging Face Space previously served **bundled** technical-reference answers. That path was removed.
|
| 4 |
+
|
| 5 |
+
Use **`OpenKotOR/holocron-trask-http`** (`infra/trask-http-public/`) for full GPTR + `trask-http-server` instead.
|
| 6 |
+
|
| 7 |
+
This directory remains only as a **503 stub** if the old Space is still deployed by CI. Do not point production Holocron at it.
|
package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
{
|
| 2 |
-
"name": "
|
| 3 |
"private": true,
|
| 4 |
"type": "module",
|
| 5 |
"engines": {
|
|
|
|
| 1 |
{
|
| 2 |
+
"name": "@openkotor/holocron-trask-api",
|
| 3 |
"private": true,
|
| 4 |
"type": "module",
|
| 5 |
"engines": {
|
server.mjs
CHANGED
|
@@ -1,458 +1,82 @@
|
|
| 1 |
-
|
| 2 |
-
|
|
|
|
| 3 |
|
| 4 |
-
|
| 5 |
|
| 6 |
-
|
| 7 |
-
'
|
| 8 |
-
'where',
|
| 9 |
-
'when',
|
| 10 |
-
'which',
|
| 11 |
-
'who',
|
| 12 |
-
'how',
|
| 13 |
-
'used',
|
| 14 |
-
'use',
|
| 15 |
-
'usedfor',
|
| 16 |
-
'does',
|
| 17 |
-
'the',
|
| 18 |
-
'and',
|
| 19 |
-
'for',
|
| 20 |
-
'with',
|
| 21 |
-
'from',
|
| 22 |
-
'into',
|
| 23 |
-
'about',
|
| 24 |
-
'that',
|
| 25 |
-
'this',
|
| 26 |
-
'game',
|
| 27 |
-
'games',
|
| 28 |
-
'star',
|
| 29 |
-
'wars',
|
| 30 |
-
'knights',
|
| 31 |
-
'old',
|
| 32 |
-
'republic',
|
| 33 |
-
'kotor',
|
| 34 |
-
'pc',
|
| 35 |
-
])
|
| 36 |
-
|
| 37 |
-
const REFERENCES = [
|
| 38 |
-
{
|
| 39 |
-
slug: 'tslpatcher',
|
| 40 |
-
title: 'TSLPatcher - KOTOR mod installer',
|
| 41 |
-
summary:
|
| 42 |
-
'TSLPatcher is the standard KotOR and TSL mod installer. Mod authors use it to patch 2DA, GFF, TLK, NSS, and related game data in place so a mod can merge changes into an existing installation instead of overwriting whole files.',
|
| 43 |
-
tags: ['tooling', 'modding', 'tslpatcher', 'installer', '2da', 'gff', 'tlk', 'nss'],
|
| 44 |
-
aliases: ['tslpatcher'],
|
| 45 |
-
},
|
| 46 |
-
{
|
| 47 |
-
slug: 'mdlops',
|
| 48 |
-
title: 'MDLOps - KOTOR model conversion tool',
|
| 49 |
-
summary:
|
| 50 |
-
'MDLOps is a KotOR model conversion utility used to inspect, decompile, and rebuild MDL and MDX models. Modders use it in the asset pipeline when converting Odyssey engine models between editable formats and game-ready binaries.',
|
| 51 |
-
tags: ['tooling', 'mdlops', 'models', 'conversion', 'mdx', 'mdl', 'odyssey'],
|
| 52 |
-
aliases: ['mdlops'],
|
| 53 |
-
},
|
| 54 |
-
{
|
| 55 |
-
slug: 'widescreen',
|
| 56 |
-
title: 'KOTOR widescreen troubleshooting on PC',
|
| 57 |
-
summary:
|
| 58 |
-
'KOTOR widescreen troubleshooting usually involves matching the game resolution, HUD and menu fixes, and graphics settings. Common checks are the target resolution in the game configuration, widescreen UI patches, and verifying that movies and the HUD are using assets that match the chosen aspect ratio.',
|
| 59 |
-
tags: ['technical', 'widescreen', 'resolution', 'hud', 'graphics', 'pc', 'troubleshooting'],
|
| 60 |
-
aliases: ['widescreen', 'resolution', 'hud', 'aspect ratio'],
|
| 61 |
-
},
|
| 62 |
-
{
|
| 63 |
-
slug: 'save-files-windows',
|
| 64 |
-
title: 'KOTOR save files on Windows',
|
| 65 |
-
summary:
|
| 66 |
-
'On Windows, Knights of the Old Republic save files are typically stored under the game installation directory in the saves folder, or under the user game data area depending on the distribution. Troubleshooting usually starts by checking the install path used by Steam, GOG, or the retail release and then opening the saves directory inside that install.',
|
| 67 |
-
tags: ['technical', 'save', 'windows', 'paths', 'troubleshooting', 'pc'],
|
| 68 |
-
aliases: ['save files', 'save folder', 'windows saves'],
|
| 69 |
-
},
|
| 70 |
-
{
|
| 71 |
-
slug: 'reone',
|
| 72 |
-
title: 'reone - Odyssey engine reimplementation',
|
| 73 |
-
summary:
|
| 74 |
-
'reone is an open-source reimplementation of the Odyssey engine used by KotOR. It provides engine-level code and runtime work for loading game assets, reproducing Odyssey behavior, and experimenting with modern tooling around the original game formats.',
|
| 75 |
-
tags: ['tooling', 'engine', 'reone', 'odyssey', 'runtime', 'open-source'],
|
| 76 |
-
aliases: ['reone'],
|
| 77 |
-
},
|
| 78 |
-
]
|
| 79 |
-
|
| 80 |
-
const SOURCE_DESCRIPTOR = {
|
| 81 |
-
id: 'trask-technical-reference',
|
| 82 |
-
name: 'Trask Technical Reference',
|
| 83 |
-
kind: 'website',
|
| 84 |
-
description: 'Built-in technical reference notes used by the public Holocron fallback API.',
|
| 85 |
-
freshnessPolicy: 'Bundled with the deployed fallback service.',
|
| 86 |
-
}
|
| 87 |
-
|
| 88 |
-
/** @type {Map<string, any>} */
|
| 89 |
-
const queryStore = new Map()
|
| 90 |
-
/** @type {Map<string, string[]>} */
|
| 91 |
-
const threadStore = new Map()
|
| 92 |
-
/** @type {Map<string, string[]>} */
|
| 93 |
-
const userHistoryStore = new Map()
|
| 94 |
-
|
| 95 |
-
function normalizeOrigin(origin) {
|
| 96 |
-
return typeof origin === 'string' && origin.trim() ? origin.trim() : '*'
|
| 97 |
-
}
|
| 98 |
-
|
| 99 |
-
function externalOrigin(req) {
|
| 100 |
-
const forwardedProto = typeof req.headers['x-forwarded-proto'] === 'string'
|
| 101 |
-
? req.headers['x-forwarded-proto'].split(',')[0].trim()
|
| 102 |
-
: ''
|
| 103 |
-
const forwardedHost = typeof req.headers['x-forwarded-host'] === 'string'
|
| 104 |
-
? req.headers['x-forwarded-host'].split(',')[0].trim()
|
| 105 |
-
: ''
|
| 106 |
-
const host = forwardedHost || req.headers.host || `127.0.0.1:${PORT}`
|
| 107 |
-
const proto = forwardedProto || (host.includes('localhost') || host.startsWith('127.0.0.1') ? 'http' : 'https')
|
| 108 |
-
return `${proto}://${host}`
|
| 109 |
-
}
|
| 110 |
-
|
| 111 |
-
function writeJson(res, status, body, origin, extraHeaders = {}) {
|
| 112 |
-
const headers = {
|
| 113 |
-
'Content-Type': 'application/json; charset=utf-8',
|
| 114 |
-
'Access-Control-Allow-Origin': normalizeOrigin(origin),
|
| 115 |
-
'Access-Control-Allow-Methods': 'GET,HEAD,POST,OPTIONS',
|
| 116 |
-
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Trask-Api-Key',
|
| 117 |
-
Vary: 'Origin',
|
| 118 |
-
...extraHeaders,
|
| 119 |
-
}
|
| 120 |
-
res.writeHead(status, headers)
|
| 121 |
-
res.end(JSON.stringify(body))
|
| 122 |
-
}
|
| 123 |
-
|
| 124 |
-
function writeText(res, status, body, origin, contentType = 'text/plain; charset=utf-8') {
|
| 125 |
-
res.writeHead(status, {
|
| 126 |
-
'Content-Type': contentType,
|
| 127 |
-
'Access-Control-Allow-Origin': normalizeOrigin(origin),
|
| 128 |
-
'Access-Control-Allow-Methods': 'GET,HEAD,POST,OPTIONS',
|
| 129 |
-
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Trask-Api-Key',
|
| 130 |
-
Vary: 'Origin',
|
| 131 |
-
})
|
| 132 |
-
res.end(body)
|
| 133 |
-
}
|
| 134 |
-
|
| 135 |
-
function tokenize(value) {
|
| 136 |
-
return value
|
| 137 |
-
.toLowerCase()
|
| 138 |
-
.replace(/[^a-z0-9]+/g, ' ')
|
| 139 |
-
.trim()
|
| 140 |
-
.split(/\s+/)
|
| 141 |
-
.filter(Boolean)
|
| 142 |
-
.filter((token) => !STOPWORDS.has(token))
|
| 143 |
}
|
| 144 |
|
| 145 |
-
function
|
| 146 |
-
const
|
| 147 |
-
for (const alias of reference.aliases) {
|
| 148 |
-
if (lowered.includes(alias)) return 10_000
|
| 149 |
-
}
|
| 150 |
-
|
| 151 |
-
const queryTokens = tokenize(query)
|
| 152 |
-
if (queryTokens.length === 0) return 0
|
| 153 |
-
|
| 154 |
-
const titleTokens = tokenize(reference.title)
|
| 155 |
-
const summaryTokens = tokenize(reference.summary)
|
| 156 |
-
const tagTokens = reference.tags.flatMap((tag) => tokenize(tag))
|
| 157 |
-
|
| 158 |
-
let score = 0
|
| 159 |
-
for (const token of queryTokens) {
|
| 160 |
-
score += titleTokens.filter((entry) => entry === token).length * 5
|
| 161 |
-
score += tagTokens.filter((entry) => entry === token).length * 3
|
| 162 |
-
score += summaryTokens.filter((entry) => entry === token).length
|
| 163 |
-
}
|
| 164 |
-
return score
|
| 165 |
-
}
|
| 166 |
-
|
| 167 |
-
function chooseReference(query) {
|
| 168 |
-
return REFERENCES
|
| 169 |
-
.map((reference) => ({ reference, score: scoreReference(query, reference) }))
|
| 170 |
-
.sort((left, right) => right.score - left.score)[0] ?? null
|
| 171 |
-
}
|
| 172 |
-
|
| 173 |
-
function sourceUrl(origin, slug) {
|
| 174 |
-
return new URL(`/reference/${slug}`, origin).toString()
|
| 175 |
-
}
|
| 176 |
-
|
| 177 |
-
function sourceForReference(origin, reference) {
|
| 178 |
return {
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
function buildFallbackAnswer(query, origin, match) {
|
| 186 |
-
if (match && match.score > 0) {
|
| 187 |
-
const source = sourceForReference(origin, match.reference)
|
| 188 |
-
return {
|
| 189 |
-
answer: [
|
| 190 |
-
`Based on indexed KOTOR archive material, here is a concise answer about ${query}:`,
|
| 191 |
-
'',
|
| 192 |
-
`- ${match.reference.title}: ${match.reference.summary} [1]`,
|
| 193 |
-
'',
|
| 194 |
-
'Sources',
|
| 195 |
-
`1. ${source.name} - ${source.url}`,
|
| 196 |
-
].join('\n'),
|
| 197 |
-
sources: [source],
|
| 198 |
-
retrievedSources: [source],
|
| 199 |
-
}
|
| 200 |
-
}
|
| 201 |
-
|
| 202 |
-
const supported = REFERENCES.map((reference, index) => `${index + 1}. ${reference.title}`).join('\n')
|
| 203 |
-
return {
|
| 204 |
-
answer: [
|
| 205 |
-
`I do not have enough built-in evidence to answer "${query}" confidently from this public fallback API.`,
|
| 206 |
-
'',
|
| 207 |
-
'The public fallback currently has bundled references for these technical topics:',
|
| 208 |
-
supported,
|
| 209 |
-
'',
|
| 210 |
-
'Sources',
|
| 211 |
-
`1. ${SOURCE_DESCRIPTOR.name} - ${new URL('/reference', origin).toString()}`,
|
| 212 |
-
].join('\n'),
|
| 213 |
-
sources: [
|
| 214 |
-
{
|
| 215 |
-
id: SOURCE_DESCRIPTOR.id,
|
| 216 |
-
name: SOURCE_DESCRIPTOR.name,
|
| 217 |
-
url: new URL('/reference', origin).toString(),
|
| 218 |
-
},
|
| 219 |
-
],
|
| 220 |
-
retrievedSources: [],
|
| 221 |
-
}
|
| 222 |
-
}
|
| 223 |
-
|
| 224 |
-
function rememberRecord(record) {
|
| 225 |
-
queryStore.set(record.queryId, record)
|
| 226 |
-
|
| 227 |
-
const threadIds = threadStore.get(record.threadId) ?? []
|
| 228 |
-
threadIds.unshift(record.queryId)
|
| 229 |
-
threadStore.set(record.threadId, [...new Set(threadIds)].slice(0, 50))
|
| 230 |
-
|
| 231 |
-
const historyIds = userHistoryStore.get(record.userId) ?? []
|
| 232 |
-
historyIds.unshift(record.queryId)
|
| 233 |
-
userHistoryStore.set(record.userId, [...new Set(historyIds)].slice(0, 100))
|
| 234 |
-
}
|
| 235 |
-
|
| 236 |
-
function recordsForIds(ids) {
|
| 237 |
-
return ids
|
| 238 |
-
.map((id) => queryStore.get(id))
|
| 239 |
-
.filter(Boolean)
|
| 240 |
-
.sort((left, right) => Date.parse(right.createdAt) - Date.parse(left.createdAt))
|
| 241 |
-
}
|
| 242 |
-
|
| 243 |
-
function parseBody(req) {
|
| 244 |
-
return new Promise((resolve, reject) => {
|
| 245 |
-
let data = ''
|
| 246 |
-
req.on('data', (chunk) => {
|
| 247 |
-
data += chunk.toString('utf8')
|
| 248 |
-
if (data.length > 1024 * 1024) {
|
| 249 |
-
reject(new Error('request body too large'))
|
| 250 |
-
req.destroy()
|
| 251 |
-
}
|
| 252 |
-
})
|
| 253 |
-
req.on('end', () => {
|
| 254 |
-
if (!data.trim()) {
|
| 255 |
-
resolve({})
|
| 256 |
-
return
|
| 257 |
-
}
|
| 258 |
-
try {
|
| 259 |
-
resolve(JSON.parse(data))
|
| 260 |
-
} catch (error) {
|
| 261 |
-
reject(error)
|
| 262 |
-
}
|
| 263 |
-
})
|
| 264 |
-
req.on('error', reject)
|
| 265 |
-
})
|
| 266 |
-
}
|
| 267 |
-
|
| 268 |
-
function isUuid(value) {
|
| 269 |
-
return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value)
|
| 270 |
-
}
|
| 271 |
-
|
| 272 |
-
function referencePage(reference) {
|
| 273 |
-
return `<!doctype html>
|
| 274 |
-
<html lang="en">
|
| 275 |
-
<head>
|
| 276 |
-
<meta charset="utf-8" />
|
| 277 |
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 278 |
-
<title>${reference.title}</title>
|
| 279 |
-
<style>
|
| 280 |
-
body { font-family: system-ui, sans-serif; max-width: 720px; margin: 3rem auto; padding: 0 1rem; line-height: 1.6; background: #111827; color: #f9fafb; }
|
| 281 |
-
h1 { color: #fca5a5; }
|
| 282 |
-
code { background: rgba(255,255,255,0.08); padding: 0.1rem 0.3rem; border-radius: 4px; }
|
| 283 |
-
a { color: #fca5a5; }
|
| 284 |
-
</style>
|
| 285 |
-
</head>
|
| 286 |
-
<body>
|
| 287 |
-
<h1>${reference.title}</h1>
|
| 288 |
-
<p>${reference.summary}</p>
|
| 289 |
-
<p><strong>Tags:</strong> ${reference.tags.join(', ')}</p>
|
| 290 |
-
<p>This reference is bundled with the public Holocron fallback API.</p>
|
| 291 |
-
</body>
|
| 292 |
-
</html>`
|
| 293 |
-
}
|
| 294 |
-
|
| 295 |
-
const server = http.createServer(async (req, res) => {
|
| 296 |
-
const origin = req.headers.origin ?? '*'
|
| 297 |
-
const requestUrl = new URL(req.url ?? '/', `http://${req.headers.host ?? `127.0.0.1:${PORT}`}`)
|
| 298 |
-
const publicOrigin = externalOrigin(req)
|
| 299 |
-
|
| 300 |
-
if (req.method === 'OPTIONS') {
|
| 301 |
-
res.writeHead(204, {
|
| 302 |
-
'Access-Control-Allow-Origin': normalizeOrigin(origin),
|
| 303 |
-
'Access-Control-Allow-Methods': 'GET,HEAD,POST,OPTIONS',
|
| 304 |
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Trask-Api-Key',
|
| 305 |
Vary: 'Origin',
|
| 306 |
-
}
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
if (requestUrl.pathname === '/' && req.method === 'GET') {
|
| 312 |
-
writeText(res, 200, 'OpenKotOR Trask API is running.\n', origin)
|
| 313 |
-
return
|
| 314 |
-
}
|
| 315 |
-
|
| 316 |
-
if (requestUrl.pathname === '/healthz' && req.method === 'GET') {
|
| 317 |
-
writeJson(res, 200, { ok: true, mode: 'fallback-public-api' }, origin)
|
| 318 |
-
return
|
| 319 |
-
}
|
| 320 |
-
|
| 321 |
-
if (requestUrl.pathname === '/reference' && req.method === 'GET') {
|
| 322 |
-
const body = [
|
| 323 |
-
'<!doctype html><html lang="en"><head><meta charset="utf-8" /><title>Trask Technical References</title></head><body>',
|
| 324 |
-
'<h1>Trask Technical References</h1>',
|
| 325 |
-
'<ul>',
|
| 326 |
-
...REFERENCES.map((reference) => `<li><a href="${new URL(`/reference/${reference.slug}`, publicOrigin).toString()}">${reference.title}</a></li>`),
|
| 327 |
-
'</ul>',
|
| 328 |
-
'</body></html>',
|
| 329 |
-
].join('')
|
| 330 |
-
writeText(res, 200, body, origin, 'text/html; charset=utf-8')
|
| 331 |
-
return
|
| 332 |
-
}
|
| 333 |
-
|
| 334 |
-
if (requestUrl.pathname.startsWith('/reference/') && req.method === 'GET') {
|
| 335 |
-
const slug = decodeURIComponent(requestUrl.pathname.slice('/reference/'.length))
|
| 336 |
-
const reference = REFERENCES.find((entry) => entry.slug === slug)
|
| 337 |
-
if (!reference) {
|
| 338 |
-
writeText(res, 404, 'Not found', origin)
|
| 339 |
-
return
|
| 340 |
-
}
|
| 341 |
-
writeText(res, 200, referencePage(reference), origin, 'text/html; charset=utf-8')
|
| 342 |
-
return
|
| 343 |
-
}
|
| 344 |
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
}
|
| 354 |
|
| 355 |
-
if (
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
const sources = REFERENCES.map((reference) => ({
|
| 367 |
-
...SOURCE_DESCRIPTOR,
|
| 368 |
-
id: `${SOURCE_DESCRIPTOR.id}:${reference.slug}`,
|
| 369 |
-
name: `${SOURCE_DESCRIPTOR.name}: ${reference.title}`,
|
| 370 |
-
homeUrl: sourceUrl(publicOrigin, reference.slug),
|
| 371 |
-
}))
|
| 372 |
-
writeJson(res, 200, { sources }, origin)
|
| 373 |
-
return
|
| 374 |
}
|
| 375 |
|
| 376 |
-
if (
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
}
|
| 382 |
-
const ids = threadStore.get(threadId) ?? []
|
| 383 |
-
writeJson(res, 200, { history: recordsForIds(ids) }, origin)
|
| 384 |
-
return
|
| 385 |
}
|
| 386 |
|
| 387 |
-
if (
|
| 388 |
-
|
| 389 |
-
const limit = Math.max(1, Math.min(100, Number.parseInt(requestUrl.searchParams.get('limit') ?? '20', 10) || 20))
|
| 390 |
-
const ids = threadId ? (threadStore.get(threadId) ?? []) : (userHistoryStore.get('qa-webui') ?? [])
|
| 391 |
-
writeJson(res, 200, { history: recordsForIds(ids).slice(0, limit) }, origin)
|
| 392 |
-
return
|
| 393 |
}
|
| 394 |
|
| 395 |
-
if (
|
| 396 |
-
|
| 397 |
-
const record = queryStore.get(queryId)
|
| 398 |
-
if (!record) {
|
| 399 |
-
writeJson(res, 404, { error: 'Query not found.' }, origin)
|
| 400 |
-
return
|
| 401 |
-
}
|
| 402 |
-
writeJson(res, 200, { query: record }, origin)
|
| 403 |
-
return
|
| 404 |
}
|
| 405 |
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
const body = await parseBody(req)
|
| 409 |
-
const query = typeof body.query === 'string' ? body.query.trim() : ''
|
| 410 |
-
if (!query) {
|
| 411 |
-
writeJson(res, 400, { error: 'Query is required.' }, origin)
|
| 412 |
-
return
|
| 413 |
-
}
|
| 414 |
-
|
| 415 |
-
const providedThreadId = typeof body.threadId === 'string' ? body.threadId.trim() : ''
|
| 416 |
-
if (providedThreadId && !isUuid(providedThreadId)) {
|
| 417 |
-
writeJson(res, 422, { error: 'threadId must be a valid UUID.' }, origin)
|
| 418 |
-
return
|
| 419 |
-
}
|
| 420 |
-
|
| 421 |
-
const threadId = providedThreadId || randomUUID()
|
| 422 |
-
const queryId = randomUUID()
|
| 423 |
-
const now = new Date().toISOString()
|
| 424 |
-
const match = chooseReference(query)
|
| 425 |
-
const { answer, sources, retrievedSources } = buildFallbackAnswer(query, publicOrigin, match)
|
| 426 |
-
const record = {
|
| 427 |
-
queryId,
|
| 428 |
-
threadId,
|
| 429 |
-
userId: 'qa-webui',
|
| 430 |
-
query,
|
| 431 |
-
status: 'complete',
|
| 432 |
-
answer,
|
| 433 |
-
sources,
|
| 434 |
-
retrievedSources,
|
| 435 |
-
visitedUrls: [],
|
| 436 |
-
error: null,
|
| 437 |
-
createdAt: now,
|
| 438 |
-
completedAt: now,
|
| 439 |
-
liveTrace: [
|
| 440 |
-
{ at: now, phase: 'queued', detail: 'Fallback public Trask query accepted.' },
|
| 441 |
-
{ at: now, phase: 'compose', detail: 'Rendered bundled technical reference answer.' },
|
| 442 |
-
],
|
| 443 |
-
}
|
| 444 |
-
rememberRecord(record)
|
| 445 |
-
writeJson(res, 201, { query: record }, origin)
|
| 446 |
-
return
|
| 447 |
-
} catch (error) {
|
| 448 |
-
writeJson(res, 500, { error: error instanceof Error ? error.message : 'Unknown error.' }, origin)
|
| 449 |
-
return
|
| 450 |
-
}
|
| 451 |
-
}
|
| 452 |
|
| 453 |
-
|
| 454 |
-
})
|
| 455 |
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Deprecated HF mirror — bundled reference Q&A removed. Deploy holocron-trask-http for live GPTR.
|
| 3 |
+
*/
|
| 4 |
|
| 5 |
+
import { createServer } from 'node:http';
|
| 6 |
|
| 7 |
+
function normalizeCorsOrigin(origin) {
|
| 8 |
+
return origin?.trim() ? origin.trim() : '*';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
}
|
| 10 |
|
| 11 |
+
function jsonResponse(status, body, origin) {
|
| 12 |
+
const payload = JSON.stringify(body);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
return {
|
| 14 |
+
status,
|
| 15 |
+
headers: {
|
| 16 |
+
'Content-Type': 'application/json; charset=utf-8',
|
| 17 |
+
'Content-Length': String(Buffer.byteLength(payload)),
|
| 18 |
+
'Access-Control-Allow-Origin': normalizeCorsOrigin(origin),
|
| 19 |
+
'Access-Control-Allow-Methods': 'GET, HEAD, POST, OPTIONS',
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Trask-Api-Key',
|
| 21 |
Vary: 'Origin',
|
| 22 |
+
},
|
| 23 |
+
body: payload,
|
| 24 |
+
};
|
| 25 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
|
| 27 |
+
const LIVE_RESEARCH_REQUIRED = {
|
| 28 |
+
error:
|
| 29 |
+
'Bundled reference answers are disabled on this Space. Use OpenKotOR/holocron-trask-http (full trask-http-server + GPTR) as TRASK_RESEARCHWIZARD_BASE_URL.',
|
| 30 |
+
};
|
| 31 |
|
| 32 |
+
async function handleRequest(request) {
|
| 33 |
+
const origin = request.headers.get('origin');
|
| 34 |
+
const url = new URL(request.url);
|
|
|
|
| 35 |
|
| 36 |
+
if (request.method === 'OPTIONS') {
|
| 37 |
+
return {
|
| 38 |
+
status: 204,
|
| 39 |
+
headers: {
|
| 40 |
+
'Access-Control-Allow-Origin': normalizeCorsOrigin(origin),
|
| 41 |
+
'Access-Control-Allow-Methods': 'GET, HEAD, POST, OPTIONS',
|
| 42 |
+
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Trask-Api-Key',
|
| 43 |
+
Vary: 'Origin',
|
| 44 |
+
},
|
| 45 |
+
body: '',
|
| 46 |
+
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
}
|
| 48 |
|
| 49 |
+
if (url.pathname === '/' && request.method === 'GET') {
|
| 50 |
+
return {
|
| 51 |
+
status: 200,
|
| 52 |
+
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
|
| 53 |
+
body: 'Holocron Trask API — live GPTR only (bundled references removed).\n',
|
| 54 |
+
};
|
|
|
|
|
|
|
|
|
|
| 55 |
}
|
| 56 |
|
| 57 |
+
if (url.pathname === '/healthz' && request.method === 'GET') {
|
| 58 |
+
return jsonResponse(200, { ok: true, mode: 'live-gptr-required', bundledReferenceApi: false }, origin);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
}
|
| 60 |
|
| 61 |
+
if (url.pathname.startsWith('/reference') || url.pathname.startsWith('/api/trask')) {
|
| 62 |
+
return jsonResponse(503, LIVE_RESEARCH_REQUIRED, origin);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
}
|
| 64 |
|
| 65 |
+
return jsonResponse(404, { error: 'Not found' }, origin);
|
| 66 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
|
| 68 |
+
const port = Number(process.env.PORT || 7860);
|
|
|
|
| 69 |
|
| 70 |
+
createServer(async (req, res) => {
|
| 71 |
+
const request = new Request(`http://${req.headers.host ?? 'localhost'}${req.url ?? '/'}`, {
|
| 72 |
+
method: req.method,
|
| 73 |
+
headers: req.headers,
|
| 74 |
+
body: req.method === 'GET' || req.method === 'HEAD' ? undefined : req,
|
| 75 |
+
duplex: 'half',
|
| 76 |
+
});
|
| 77 |
+
const response = await handleRequest(request);
|
| 78 |
+
res.writeHead(response.status, response.headers);
|
| 79 |
+
res.end(response.body);
|
| 80 |
+
}).listen(port, () => {
|
| 81 |
+
console.log(`holocron-trask-api stub listening on ${port}`);
|
| 82 |
+
});
|