pinokiod 7.5.41 → 7.5.43
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/package.json +1 -1
- package/server/index.js +45 -7
- package/server/lib/log_redaction.js +163 -5
- package/server/lib/privacy_filter_cache.js +286 -0
- package/server/public/logs-top-redaction.css +85 -37
- package/server/public/logs-top-redaction.js +299 -105
- package/server/public/logs.js +65 -9
- package/server/public/privacy_filter_worker.js +149 -7
- package/server/public/style.css +82 -0
- package/server/views/logs.ejs +36 -25
- package/server/views/partials/logs_top_redaction_actions.ejs +1 -2
- package/server/views/partials/logs_top_redaction_pane.ejs +16 -6
- package/test/log-redaction-overrides.test.js +85 -3
- package/test/logs-ask-ai.test.js +213 -18
- package/test/privacy-filter-cache.test.js +242 -0
package/package.json
CHANGED
package/server/index.js
CHANGED
|
@@ -85,6 +85,7 @@ const { createTaskWorkspaceLinkService } = require("./lib/task_workspace_links")
|
|
|
85
85
|
const { createContentValidationService } = require("./lib/content_validation")
|
|
86
86
|
const { buildSecureRouterDebugSnapshot, createSecureRouterDebugStore } = require("./lib/secure_router_debug")
|
|
87
87
|
const logRedaction = require("./lib/log_redaction")
|
|
88
|
+
const privacyFilterCache = require("./lib/privacy_filter_cache")
|
|
88
89
|
const AppRegistryService = require("./lib/app_registry")
|
|
89
90
|
const AppLogService = require("./lib/app_logs")
|
|
90
91
|
const AppLogReportService = require("./lib/app_log_report")
|
|
@@ -6401,6 +6402,12 @@ class Server {
|
|
|
6401
6402
|
}
|
|
6402
6403
|
});
|
|
6403
6404
|
this.app.use('/files', serve2, serveIndex(this.kernel.homedir, {icons: true, hidden: true, theme: this.theme }))
|
|
6405
|
+
this.app.use('/pinokio/privacy-filter/models', express.static(privacyFilterCache.cacheRoot(this.kernel), {
|
|
6406
|
+
fallthrough: true,
|
|
6407
|
+
index: false,
|
|
6408
|
+
immutable: true,
|
|
6409
|
+
maxAge: '1y'
|
|
6410
|
+
}))
|
|
6404
6411
|
} else {
|
|
6405
6412
|
this.app.set("views", [
|
|
6406
6413
|
path.resolve(__dirname, "views")
|
|
@@ -8415,6 +8422,24 @@ class Server {
|
|
|
8415
8422
|
entries
|
|
8416
8423
|
})
|
|
8417
8424
|
}))
|
|
8425
|
+
this.app.get("/pinokio/privacy-filter/status", ex(async (req, res) => {
|
|
8426
|
+
const payload = await privacyFilterCache.status(this.kernel, {
|
|
8427
|
+
dtype: req.query.dtype
|
|
8428
|
+
})
|
|
8429
|
+
res.set("Cache-Control", "no-store")
|
|
8430
|
+
res.json(payload)
|
|
8431
|
+
}))
|
|
8432
|
+
this.app.post("/pinokio/privacy-filter/ensure", ex(async (req, res) => {
|
|
8433
|
+
if (!privacyFilterCache.isInstallRequestAllowed(req)) {
|
|
8434
|
+
res.status(403).json({ error: "Privacy filter cache install requires a same-origin request" })
|
|
8435
|
+
return
|
|
8436
|
+
}
|
|
8437
|
+
const payload = await privacyFilterCache.ensure(this.kernel, {
|
|
8438
|
+
dtype: req.query.dtype
|
|
8439
|
+
})
|
|
8440
|
+
res.set("Cache-Control", "no-store")
|
|
8441
|
+
res.json(payload)
|
|
8442
|
+
}))
|
|
8418
8443
|
this.app.get("/pinokio/logs/file", ex(logRedaction.createTopLevelLogFileHandler(this)))
|
|
8419
8444
|
this.app.get("/api/logs/stream", ex(async (req, res) => {
|
|
8420
8445
|
const workspace = typeof req.query.workspace === 'string' ? req.query.workspace.trim() : ''
|
|
@@ -15991,12 +16016,21 @@ class Server {
|
|
|
15991
16016
|
}
|
|
15992
16017
|
return
|
|
15993
16018
|
}
|
|
15994
|
-
const
|
|
16019
|
+
const reviewedArchiveRequested = Boolean(req.body && (
|
|
16020
|
+
Object.prototype.hasOwnProperty.call(req.body, 'redacted_overrides') ||
|
|
16021
|
+
Object.prototype.hasOwnProperty.call(req.body, 'excluded_paths')
|
|
16022
|
+
))
|
|
16023
|
+
const allowPartialOverrides = Boolean(req.body && (
|
|
16024
|
+
req.body.allow_partial_overrides === true ||
|
|
16025
|
+
req.body.allow_partial_overrides === 'true'
|
|
16026
|
+
))
|
|
15995
16027
|
let redactionOverrides = []
|
|
16028
|
+
let redactionExclusions = []
|
|
15996
16029
|
try {
|
|
15997
16030
|
redactionOverrides = logRedaction.normalizeLogRedactionOverrides(req.body || {})
|
|
16031
|
+
redactionExclusions = logRedaction.normalizeLogRedactionExclusions(req.body || {})
|
|
15998
16032
|
} catch (error) {
|
|
15999
|
-
res.status(400).json({ error: error && error.message ? error.message : 'Invalid redaction
|
|
16033
|
+
res.status(400).json({ error: error && error.message ? error.message : 'Invalid redaction request' })
|
|
16000
16034
|
return
|
|
16001
16035
|
}
|
|
16002
16036
|
|
|
@@ -16014,22 +16048,26 @@ class Server {
|
|
|
16014
16048
|
|
|
16015
16049
|
let folder = this.kernel.path("exported_logs")
|
|
16016
16050
|
let zipPath = this.kernel.path("logs.zip")
|
|
16017
|
-
if (
|
|
16051
|
+
if (reviewedArchiveRequested) {
|
|
16018
16052
|
await fs.promises.rm(zipPath, { force: true }).catch(() => {})
|
|
16019
16053
|
}
|
|
16020
16054
|
let appliedRedactionOverrides = 0
|
|
16055
|
+
let appliedRedactionExclusions = 0
|
|
16021
16056
|
try {
|
|
16022
|
-
if (
|
|
16023
|
-
await logRedaction.assertCompleteLogRedactionOverrides(folder, redactionOverrides
|
|
16057
|
+
if (reviewedArchiveRequested) {
|
|
16058
|
+
await logRedaction.assertCompleteLogRedactionOverrides(folder, redactionOverrides, redactionExclusions, {
|
|
16059
|
+
requireComplete: !allowPartialOverrides
|
|
16060
|
+
})
|
|
16024
16061
|
}
|
|
16062
|
+
appliedRedactionExclusions = await logRedaction.applyLogRedactionExclusions(folder, redactionExclusions)
|
|
16025
16063
|
appliedRedactionOverrides = await logRedaction.applyLogRedactionOverrides(folder, redactionOverrides)
|
|
16026
16064
|
} catch (error) {
|
|
16027
|
-
res.status(400).json({ error: error && error.message ? error.message : 'Failed to apply redaction
|
|
16065
|
+
res.status(400).json({ error: error && error.message ? error.message : 'Failed to apply redaction request' })
|
|
16028
16066
|
return
|
|
16029
16067
|
}
|
|
16030
16068
|
await fs.promises.rm(zipPath, { force: true }).catch(() => {})
|
|
16031
16069
|
await compressing.zip.compressDir(folder, zipPath)
|
|
16032
|
-
res.json({ success: true, download: '/pinokio/logs.zip', redacted_overrides: appliedRedactionOverrides })
|
|
16070
|
+
res.json({ success: true, download: '/pinokio/logs.zip', redacted_overrides: appliedRedactionOverrides, excluded_paths: appliedRedactionExclusions })
|
|
16033
16071
|
}))
|
|
16034
16072
|
this.app.get("/pinokio/version", ex(async (req, res) => {
|
|
16035
16073
|
let version = this.version
|
|
@@ -5,6 +5,7 @@ const { isBinaryFile } = require('isbinaryfile')
|
|
|
5
5
|
const LOG_REDACTION_FILE_MAX_BYTES = 2 * 1024 * 1024
|
|
6
6
|
const LOG_REDACTION_OVERRIDE_MAX_BYTES = 8 * 1024 * 1024
|
|
7
7
|
const LOG_REDACTION_TEXT_EXTENSIONS = new Set(['.json', '.log', '.txt'])
|
|
8
|
+
const LOG_REDACTION_TAIL_LINE_COUNTS = new Set([500, 1000, 2000])
|
|
8
9
|
const CADDY_LOG_PATTERN = /^caddy(?:-.+)?\.log$/i
|
|
9
10
|
|
|
10
11
|
function isTopLevelRedactableLogPath(relativePath = '') {
|
|
@@ -21,8 +22,84 @@ function isTopLevelRedactableLogPath(relativePath = '') {
|
|
|
21
22
|
return LOG_REDACTION_TEXT_EXTENSIONS.has(path.extname(value).toLowerCase())
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
function normalizeTailLineCount(value) {
|
|
26
|
+
const parsed = Number.parseInt(String(value || ''), 10)
|
|
27
|
+
return LOG_REDACTION_TAIL_LINE_COUNTS.has(parsed) ? parsed : 0
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function readTailTextFile(filePath, stats, tailLines) {
|
|
31
|
+
const readBytes = Math.min(stats.size, LOG_REDACTION_FILE_MAX_BYTES - 2048)
|
|
32
|
+
const start = Math.max(0, stats.size - readBytes)
|
|
33
|
+
const buffer = Buffer.alloc(readBytes)
|
|
34
|
+
const handle = await fs.promises.open(filePath, 'r')
|
|
35
|
+
let bytesRead = 0
|
|
36
|
+
try {
|
|
37
|
+
const result = await handle.read(buffer, 0, readBytes, start)
|
|
38
|
+
bytesRead = result.bytesRead
|
|
39
|
+
} finally {
|
|
40
|
+
await handle.close()
|
|
41
|
+
}
|
|
42
|
+
let text = buffer.slice(0, bytesRead).toString('utf8')
|
|
43
|
+
if (start > 0) {
|
|
44
|
+
const firstNewline = text.indexOf('\n')
|
|
45
|
+
if (firstNewline >= 0) {
|
|
46
|
+
text = text.slice(firstNewline + 1)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const lines = text.split(/\r?\n/)
|
|
50
|
+
const selected = lines.length > tailLines ? lines.slice(-tailLines) : lines
|
|
51
|
+
const omittedLines = Math.max(0, lines.length - selected.length)
|
|
52
|
+
const prefix = start > 0 || omittedLines > 0
|
|
53
|
+
? `[Older log content omitted by user. Showing the last ${tailLines.toLocaleString()} lines.]\n`
|
|
54
|
+
: ''
|
|
55
|
+
return {
|
|
56
|
+
text: `${prefix}${selected.join('\n')}`,
|
|
57
|
+
truncated: start > 0 || omittedLines > 0,
|
|
58
|
+
included_lines: selected.length
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function clonePlainObject(value) {
|
|
63
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
64
|
+
? { ...value }
|
|
65
|
+
: {}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function createRuntimeEnvSnapshot(kernel) {
|
|
69
|
+
const baseEnv = {
|
|
70
|
+
...clonePlainObject(process.env),
|
|
71
|
+
...clonePlainObject(kernel && kernel.envs)
|
|
72
|
+
}
|
|
73
|
+
if (kernel && kernel.bin && typeof kernel.bin.envs === 'function') {
|
|
74
|
+
try {
|
|
75
|
+
return clonePlainObject(kernel.bin.envs(baseEnv))
|
|
76
|
+
} catch (_) {}
|
|
77
|
+
}
|
|
78
|
+
return baseEnv
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function createFallbackStateSnapshot(kernel) {
|
|
82
|
+
const env = createRuntimeEnvSnapshot(kernel)
|
|
83
|
+
if (Object.keys(env).length === 0) {
|
|
84
|
+
return null
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
state: 'snapshot',
|
|
88
|
+
id: 'runtime-environment',
|
|
89
|
+
group: 'system',
|
|
90
|
+
env,
|
|
91
|
+
path: kernel && kernel.homedir,
|
|
92
|
+
cmd: 'pinokio environment snapshot',
|
|
93
|
+
done: true,
|
|
94
|
+
ready: true
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
24
98
|
function createCurrentLogSnapshot(kernel, version) {
|
|
25
|
-
const
|
|
99
|
+
const liveShells = kernel && kernel.shell && Array.isArray(kernel.shell.shells)
|
|
100
|
+
? kernel.shell.shells
|
|
101
|
+
: []
|
|
102
|
+
const states = liveShells.map((s) => {
|
|
26
103
|
return {
|
|
27
104
|
state: s.state,
|
|
28
105
|
id: s.id,
|
|
@@ -34,6 +111,12 @@ function createCurrentLogSnapshot(kernel, version) {
|
|
|
34
111
|
ready: s.ready,
|
|
35
112
|
}
|
|
36
113
|
})
|
|
114
|
+
if (states.length === 0) {
|
|
115
|
+
const fallbackState = createFallbackStateSnapshot(kernel)
|
|
116
|
+
if (fallbackState) {
|
|
117
|
+
states.push(fallbackState)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
37
120
|
|
|
38
121
|
const info = {
|
|
39
122
|
platform: kernel.platform,
|
|
@@ -104,12 +187,45 @@ function normalizeLogRedactionOverrides(body = {}) {
|
|
|
104
187
|
return overrides
|
|
105
188
|
}
|
|
106
189
|
|
|
107
|
-
|
|
190
|
+
function normalizeLogRedactionExclusions(body = {}) {
|
|
191
|
+
if (!body || !Object.prototype.hasOwnProperty.call(body, 'excluded_paths')) {
|
|
192
|
+
return []
|
|
193
|
+
}
|
|
194
|
+
const source = body.excluded_paths
|
|
195
|
+
if (!Array.isArray(source)) {
|
|
196
|
+
throw new Error('excluded_paths must be an array')
|
|
197
|
+
}
|
|
198
|
+
const exclusions = []
|
|
199
|
+
const seen = new Set()
|
|
200
|
+
for (const candidate of source) {
|
|
201
|
+
const relativePath = typeof candidate === 'string' ? candidate.trim() : ''
|
|
202
|
+
if (!isTopLevelRedactableLogPath(relativePath)) {
|
|
203
|
+
throw new Error(`Invalid excluded path: ${relativePath || '(empty)'}`)
|
|
204
|
+
}
|
|
205
|
+
if (seen.has(relativePath)) {
|
|
206
|
+
throw new Error(`Duplicate excluded path: ${relativePath}`)
|
|
207
|
+
}
|
|
208
|
+
seen.add(relativePath)
|
|
209
|
+
exclusions.push(relativePath)
|
|
210
|
+
}
|
|
211
|
+
return exclusions
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function assertCompleteLogRedactionOverrides(exportRoot, overrides = [], exclusions = [], options = {}) {
|
|
108
215
|
const overridePaths = new Set((Array.isArray(overrides) ? overrides : []).map((override) => override && override.path).filter(Boolean))
|
|
216
|
+
const excludedPaths = new Set(Array.isArray(exclusions) ? exclusions.filter(Boolean) : [])
|
|
217
|
+
for (const excludedPath of excludedPaths) {
|
|
218
|
+
if (overridePaths.has(excludedPath)) {
|
|
219
|
+
throw new Error(`File cannot be both redacted and excluded: ${excludedPath}`)
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (options && options.requireComplete === false) {
|
|
223
|
+
return
|
|
224
|
+
}
|
|
109
225
|
const missing = []
|
|
110
226
|
const entries = await fs.promises.readdir(exportRoot, { withFileTypes: true })
|
|
111
227
|
for (const entry of entries) {
|
|
112
|
-
if (entry.isFile() && isTopLevelRedactableLogPath(entry.name) && !overridePaths.has(entry.name)) {
|
|
228
|
+
if (entry.isFile() && isTopLevelRedactableLogPath(entry.name) && !overridePaths.has(entry.name) && !excludedPaths.has(entry.name)) {
|
|
113
229
|
missing.push(entry.name)
|
|
114
230
|
}
|
|
115
231
|
}
|
|
@@ -119,6 +235,35 @@ async function assertCompleteLogRedactionOverrides(exportRoot, overrides = []) {
|
|
|
119
235
|
}
|
|
120
236
|
}
|
|
121
237
|
|
|
238
|
+
async function applyLogRedactionExclusions(exportRoot, exclusions = []) {
|
|
239
|
+
if (!Array.isArray(exclusions) || exclusions.length === 0) {
|
|
240
|
+
return 0
|
|
241
|
+
}
|
|
242
|
+
let removed = 0
|
|
243
|
+
for (const relativePath of exclusions) {
|
|
244
|
+
if (!isTopLevelRedactableLogPath(relativePath)) {
|
|
245
|
+
throw new Error(`Invalid excluded path: ${relativePath || '(empty)'}`)
|
|
246
|
+
}
|
|
247
|
+
const target = path.resolve(exportRoot, relativePath)
|
|
248
|
+
const relative = path.relative(exportRoot, target)
|
|
249
|
+
if (!relative || relative.startsWith('..') || path.isAbsolute(relative) || relative.includes(path.sep)) {
|
|
250
|
+
throw new Error(`Invalid excluded path: ${relativePath}`)
|
|
251
|
+
}
|
|
252
|
+
let stats
|
|
253
|
+
try {
|
|
254
|
+
stats = await fs.promises.stat(target)
|
|
255
|
+
} catch (_) {
|
|
256
|
+
throw new Error(`Excluded path target not found: ${relativePath}`)
|
|
257
|
+
}
|
|
258
|
+
if (!stats.isFile()) {
|
|
259
|
+
throw new Error(`Excluded path target is not a file: ${relativePath}`)
|
|
260
|
+
}
|
|
261
|
+
await fs.promises.rm(target, { force: true })
|
|
262
|
+
removed += 1
|
|
263
|
+
}
|
|
264
|
+
return removed
|
|
265
|
+
}
|
|
266
|
+
|
|
122
267
|
async function applyLogRedactionOverrides(exportRoot, overrides = []) {
|
|
123
268
|
if (!Array.isArray(overrides) || overrides.length === 0) {
|
|
124
269
|
return 0
|
|
@@ -215,6 +360,7 @@ function createTopLevelLogFileHandler(server) {
|
|
|
215
360
|
res.status(400).json({ error: 'Only top-level text log files can be read for redaction' })
|
|
216
361
|
return
|
|
217
362
|
}
|
|
363
|
+
const tailLines = normalizeTailLineCount(req.query.tail_lines || req.query.lines)
|
|
218
364
|
let stats
|
|
219
365
|
try {
|
|
220
366
|
stats = await fs.promises.stat(descriptor.absolutePath)
|
|
@@ -226,7 +372,7 @@ function createTopLevelLogFileHandler(server) {
|
|
|
226
372
|
res.status(400).json({ error: 'Path is not a file' })
|
|
227
373
|
return
|
|
228
374
|
}
|
|
229
|
-
if (stats.size > LOG_REDACTION_FILE_MAX_BYTES) {
|
|
375
|
+
if (stats.size > LOG_REDACTION_FILE_MAX_BYTES && !tailLines) {
|
|
230
376
|
res.status(413).json({
|
|
231
377
|
error: 'File is too large to redact in the browser',
|
|
232
378
|
size: stats.size,
|
|
@@ -242,8 +388,14 @@ function createTopLevelLogFileHandler(server) {
|
|
|
242
388
|
}
|
|
243
389
|
} catch (_) {}
|
|
244
390
|
let text
|
|
391
|
+
let tail = null
|
|
245
392
|
try {
|
|
246
|
-
|
|
393
|
+
if (tailLines) {
|
|
394
|
+
tail = await readTailTextFile(descriptor.absolutePath, stats, tailLines)
|
|
395
|
+
text = tail.text
|
|
396
|
+
} else {
|
|
397
|
+
text = await fs.promises.readFile(descriptor.absolutePath, 'utf8')
|
|
398
|
+
}
|
|
247
399
|
} catch (error) {
|
|
248
400
|
res.status(500).json({ error: 'Failed to read file', detail: error.message })
|
|
249
401
|
return
|
|
@@ -254,6 +406,9 @@ function createTopLevelLogFileHandler(server) {
|
|
|
254
406
|
name: path.basename(relativePath),
|
|
255
407
|
size: stats.size,
|
|
256
408
|
modified: stats.mtime,
|
|
409
|
+
tail_lines: tailLines || null,
|
|
410
|
+
truncated: tail ? tail.truncated : false,
|
|
411
|
+
included_lines: tail ? tail.included_lines : null,
|
|
257
412
|
text
|
|
258
413
|
})
|
|
259
414
|
}
|
|
@@ -262,10 +417,13 @@ function createTopLevelLogFileHandler(server) {
|
|
|
262
417
|
module.exports = {
|
|
263
418
|
LOG_REDACTION_FILE_MAX_BYTES,
|
|
264
419
|
isTopLevelRedactableLogPath,
|
|
420
|
+
normalizeTailLineCount,
|
|
265
421
|
createCurrentLogSnapshot,
|
|
266
422
|
writeCurrentLogSnapshot,
|
|
267
423
|
normalizeLogRedactionOverrides,
|
|
424
|
+
normalizeLogRedactionExclusions,
|
|
268
425
|
assertCompleteLogRedactionOverrides,
|
|
426
|
+
applyLogRedactionExclusions,
|
|
269
427
|
applyLogRedactionOverrides,
|
|
270
428
|
createLogRedactionBodyParser,
|
|
271
429
|
createLogRedactionBodyParsers,
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const path = require('path')
|
|
3
|
+
const { pipeline } = require('stream/promises')
|
|
4
|
+
const axios = require('axios')
|
|
5
|
+
|
|
6
|
+
const PRIVACY_FILTER_MODEL_ID = 'openai/privacy-filter'
|
|
7
|
+
const PRIVACY_FILTER_REVISION = '7ffa9a043d54d1be65afb281eddf0ffbe629385b'
|
|
8
|
+
const PRIVACY_FILTER_LOCAL_MODEL_PATH = `/pinokio/privacy-filter/models/${PRIVACY_FILTER_REVISION}/`
|
|
9
|
+
const PRIVACY_FILTER_SHARED_FILES = [
|
|
10
|
+
'config.json',
|
|
11
|
+
'tokenizer_config.json',
|
|
12
|
+
'tokenizer.json'
|
|
13
|
+
]
|
|
14
|
+
const PRIVACY_FILTER_DTYPE_FILES = {
|
|
15
|
+
q4f16: [
|
|
16
|
+
'onnx/model_q4f16.onnx',
|
|
17
|
+
'onnx/model_q4f16.onnx_data'
|
|
18
|
+
],
|
|
19
|
+
q4: [
|
|
20
|
+
'onnx/model_q4.onnx',
|
|
21
|
+
'onnx/model_q4.onnx_data'
|
|
22
|
+
],
|
|
23
|
+
q8: [
|
|
24
|
+
'onnx/model_quantized.onnx',
|
|
25
|
+
'onnx/model_quantized.onnx_data'
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
const PRIVACY_FILTER_ASSET_SET = new Set([
|
|
29
|
+
...PRIVACY_FILTER_SHARED_FILES,
|
|
30
|
+
...Object.values(PRIVACY_FILTER_DTYPE_FILES).flat()
|
|
31
|
+
])
|
|
32
|
+
|
|
33
|
+
const ensurePromises = new Map()
|
|
34
|
+
const installStates = new Map()
|
|
35
|
+
|
|
36
|
+
function normalizeDtype(value) {
|
|
37
|
+
const dtype = String(value || '').trim().toLowerCase()
|
|
38
|
+
return Object.prototype.hasOwnProperty.call(PRIVACY_FILTER_DTYPE_FILES, dtype) ? dtype : 'q8'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function cacheRoot(kernel) {
|
|
42
|
+
const home = kernel && typeof kernel.homedir === 'string' ? kernel.homedir : ''
|
|
43
|
+
if (!home.trim()) {
|
|
44
|
+
throw new Error('Pinokio home directory is required for privacy filter cache.')
|
|
45
|
+
}
|
|
46
|
+
return path.resolve(home, 'cache', 'privacy-filter', 'models')
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function modelRoot(kernel) {
|
|
50
|
+
return path.resolve(cacheRoot(kernel), PRIVACY_FILTER_REVISION, 'openai', 'privacy-filter')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function assetFilesForDtype(dtype) {
|
|
54
|
+
const normalized = normalizeDtype(dtype)
|
|
55
|
+
return [
|
|
56
|
+
...PRIVACY_FILTER_SHARED_FILES,
|
|
57
|
+
...PRIVACY_FILTER_DTYPE_FILES[normalized]
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function assertKnownAsset(relativePath) {
|
|
62
|
+
const normalized = String(relativePath || '').replace(/\\/g, '/')
|
|
63
|
+
if (!PRIVACY_FILTER_ASSET_SET.has(normalized)) {
|
|
64
|
+
throw new Error(`Unknown privacy filter asset: ${relativePath}`)
|
|
65
|
+
}
|
|
66
|
+
return normalized
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isInstallRequestAllowed(req) {
|
|
70
|
+
const fetchSite = String(req && typeof req.get === 'function' ? req.get('Sec-Fetch-Site') || '' : '').trim().toLowerCase()
|
|
71
|
+
if (fetchSite && fetchSite !== 'same-origin' && fetchSite !== 'none') {
|
|
72
|
+
return false
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const origin = String(req && typeof req.get === 'function' ? req.get('Origin') || '' : '').trim()
|
|
76
|
+
if (!origin) {
|
|
77
|
+
return true
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const host = String(req && typeof req.get === 'function' ? req.get('Host') || '' : '').trim().toLowerCase()
|
|
81
|
+
if (!host) {
|
|
82
|
+
return false
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const parsed = new URL(origin)
|
|
87
|
+
return (parsed.protocol === 'http:' || parsed.protocol === 'https:') && parsed.host.toLowerCase() === host
|
|
88
|
+
} catch (_) {
|
|
89
|
+
return false
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function assetPath(kernel, relativePath) {
|
|
94
|
+
const normalized = assertKnownAsset(relativePath)
|
|
95
|
+
return path.resolve(modelRoot(kernel), ...normalized.split('/'))
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function ensureKey(kernel, dtype) {
|
|
99
|
+
return `${modelRoot(kernel)}:${normalizeDtype(dtype)}`
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function remoteAssetUrl(relativePath) {
|
|
103
|
+
const normalized = assertKnownAsset(relativePath)
|
|
104
|
+
return `https://huggingface.co/${PRIVACY_FILTER_MODEL_ID}/resolve/${PRIVACY_FILTER_REVISION}/${normalized}`
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function copyInstallState(install) {
|
|
108
|
+
return install ? { ...install } : null
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function cleanupStaleAssetTemps(target) {
|
|
112
|
+
const dir = path.dirname(target)
|
|
113
|
+
const base = path.basename(target)
|
|
114
|
+
const currentProcessPrefix = `${base}.${process.pid}.`
|
|
115
|
+
let entries
|
|
116
|
+
try {
|
|
117
|
+
entries = await fs.promises.readdir(dir, { withFileTypes: true })
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if (error && error.code === 'ENOENT') {
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
throw error
|
|
123
|
+
}
|
|
124
|
+
await Promise.all(entries
|
|
125
|
+
.filter((entry) => {
|
|
126
|
+
return entry.isFile() &&
|
|
127
|
+
entry.name.startsWith(`${base}.`) &&
|
|
128
|
+
entry.name.endsWith('.tmp') &&
|
|
129
|
+
!entry.name.startsWith(currentProcessPrefix)
|
|
130
|
+
})
|
|
131
|
+
.map((entry) => fs.promises.rm(path.resolve(dir, entry.name), { force: true })))
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function fileStatus(kernel, relativePath) {
|
|
135
|
+
const target = assetPath(kernel, relativePath)
|
|
136
|
+
try {
|
|
137
|
+
const stats = await fs.promises.stat(target)
|
|
138
|
+
return {
|
|
139
|
+
path: relativePath,
|
|
140
|
+
exists: stats.isFile() && stats.size > 0,
|
|
141
|
+
size: stats.isFile() ? stats.size : 0
|
|
142
|
+
}
|
|
143
|
+
} catch (_) {
|
|
144
|
+
return {
|
|
145
|
+
path: relativePath,
|
|
146
|
+
exists: false,
|
|
147
|
+
size: 0
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function status(kernel, options = {}) {
|
|
153
|
+
const dtype = normalizeDtype(options.dtype)
|
|
154
|
+
const install = copyInstallState(installStates.get(ensureKey(kernel, dtype)))
|
|
155
|
+
const files = await Promise.all(assetFilesForDtype(dtype).map((file) => fileStatus(kernel, file)))
|
|
156
|
+
const missing = files.filter((file) => !file.exists).map((file) => file.path)
|
|
157
|
+
return {
|
|
158
|
+
model_id: PRIVACY_FILTER_MODEL_ID,
|
|
159
|
+
revision: PRIVACY_FILTER_REVISION,
|
|
160
|
+
dtype,
|
|
161
|
+
local_model_path: PRIVACY_FILTER_LOCAL_MODEL_PATH,
|
|
162
|
+
ready: missing.length === 0,
|
|
163
|
+
files,
|
|
164
|
+
missing,
|
|
165
|
+
missing_count: missing.length,
|
|
166
|
+
installing: Boolean(install),
|
|
167
|
+
install
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function downloadAsset(kernel, relativePath, onProgress) {
|
|
172
|
+
const target = assetPath(kernel, relativePath)
|
|
173
|
+
const existing = await fileStatus(kernel, relativePath)
|
|
174
|
+
if (existing.exists) {
|
|
175
|
+
return { path: relativePath, status: 'cached', size: existing.size }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
await fs.promises.mkdir(path.dirname(target), { recursive: true })
|
|
179
|
+
await cleanupStaleAssetTemps(target)
|
|
180
|
+
const tmp = `${target}.${process.pid}.${Date.now()}.tmp`
|
|
181
|
+
try {
|
|
182
|
+
const response = await axios.get(remoteAssetUrl(relativePath), {
|
|
183
|
+
responseType: 'stream',
|
|
184
|
+
maxRedirects: 5,
|
|
185
|
+
timeout: 0,
|
|
186
|
+
maxContentLength: Infinity,
|
|
187
|
+
maxBodyLength: Infinity,
|
|
188
|
+
validateStatus: (statusCode) => statusCode >= 200 && statusCode < 300
|
|
189
|
+
})
|
|
190
|
+
let loaded = 0
|
|
191
|
+
const total = Number(response.headers && response.headers['content-length']) || 0
|
|
192
|
+
const encoded = Boolean(response.headers && response.headers['content-encoding'])
|
|
193
|
+
if (typeof onProgress === 'function') {
|
|
194
|
+
onProgress({ loaded, total })
|
|
195
|
+
}
|
|
196
|
+
response.data.on('data', (chunk) => {
|
|
197
|
+
loaded += chunk && chunk.length ? chunk.length : 0
|
|
198
|
+
if (typeof onProgress === 'function') {
|
|
199
|
+
onProgress({ loaded, total })
|
|
200
|
+
}
|
|
201
|
+
})
|
|
202
|
+
await pipeline(response.data, fs.createWriteStream(tmp))
|
|
203
|
+
const stats = await fs.promises.stat(tmp)
|
|
204
|
+
if (!stats.isFile() || stats.size <= 0) {
|
|
205
|
+
throw new Error(`Downloaded empty privacy filter asset: ${relativePath}`)
|
|
206
|
+
}
|
|
207
|
+
if (total > 0 && !encoded && stats.size !== total) {
|
|
208
|
+
throw new Error(`Incomplete privacy filter asset: ${relativePath} (${stats.size}/${total} bytes)`)
|
|
209
|
+
}
|
|
210
|
+
await fs.promises.rename(tmp, target)
|
|
211
|
+
return { path: relativePath, status: 'downloaded', size: stats.size }
|
|
212
|
+
} catch (error) {
|
|
213
|
+
await fs.promises.rm(tmp, { force: true }).catch(() => {})
|
|
214
|
+
throw error
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function ensure(kernel, options = {}) {
|
|
219
|
+
const dtype = normalizeDtype(options.dtype)
|
|
220
|
+
const key = ensureKey(kernel, dtype)
|
|
221
|
+
if (ensurePromises.has(key)) {
|
|
222
|
+
return ensurePromises.get(key)
|
|
223
|
+
}
|
|
224
|
+
const promise = (async () => {
|
|
225
|
+
const files = assetFilesForDtype(dtype)
|
|
226
|
+
const install = {
|
|
227
|
+
dtype,
|
|
228
|
+
total_files: files.length,
|
|
229
|
+
completed_files: 0,
|
|
230
|
+
cached_files: 0,
|
|
231
|
+
downloaded_files: 0,
|
|
232
|
+
current_file: '',
|
|
233
|
+
current_loaded: 0,
|
|
234
|
+
current_total: 0,
|
|
235
|
+
started_at: new Date().toISOString(),
|
|
236
|
+
updated_at: new Date().toISOString()
|
|
237
|
+
}
|
|
238
|
+
installStates.set(key, install)
|
|
239
|
+
const results = []
|
|
240
|
+
for (let index = 0; index < files.length; index += 1) {
|
|
241
|
+
const file = files[index]
|
|
242
|
+
install.current_file = file
|
|
243
|
+
install.current_index = index + 1
|
|
244
|
+
install.current_loaded = 0
|
|
245
|
+
install.current_total = 0
|
|
246
|
+
install.updated_at = new Date().toISOString()
|
|
247
|
+
const result = await downloadAsset(kernel, file, ({ loaded, total }) => {
|
|
248
|
+
install.current_loaded = loaded
|
|
249
|
+
install.current_total = total
|
|
250
|
+
install.updated_at = new Date().toISOString()
|
|
251
|
+
})
|
|
252
|
+
results.push(result)
|
|
253
|
+
install.completed_files += 1
|
|
254
|
+
if (result.status === 'cached') {
|
|
255
|
+
install.cached_files += 1
|
|
256
|
+
} else if (result.status === 'downloaded') {
|
|
257
|
+
install.downloaded_files += 1
|
|
258
|
+
}
|
|
259
|
+
install.updated_at = new Date().toISOString()
|
|
260
|
+
}
|
|
261
|
+
const current = await status(kernel, { dtype })
|
|
262
|
+
return {
|
|
263
|
+
...current,
|
|
264
|
+
results,
|
|
265
|
+
downloaded: results.filter((result) => result.status === 'downloaded').length,
|
|
266
|
+
cached: results.filter((result) => result.status === 'cached').length
|
|
267
|
+
}
|
|
268
|
+
})().finally(() => {
|
|
269
|
+
ensurePromises.delete(key)
|
|
270
|
+
installStates.delete(key)
|
|
271
|
+
})
|
|
272
|
+
ensurePromises.set(key, promise)
|
|
273
|
+
return promise
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
module.exports = {
|
|
277
|
+
PRIVACY_FILTER_MODEL_ID,
|
|
278
|
+
PRIVACY_FILTER_REVISION,
|
|
279
|
+
PRIVACY_FILTER_LOCAL_MODEL_PATH,
|
|
280
|
+
normalizeDtype,
|
|
281
|
+
cacheRoot,
|
|
282
|
+
modelRoot,
|
|
283
|
+
isInstallRequestAllowed,
|
|
284
|
+
status,
|
|
285
|
+
ensure
|
|
286
|
+
}
|