pinokiod 7.3.0 → 7.3.3
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/kernel/api/github/index.js +444 -0
- package/kernel/api/index.js +199 -11
- package/kernel/api/process/index.js +124 -44
- package/kernel/api/shell_run_template.js +273 -0
- package/kernel/api/uri/index.js +51 -0
- package/kernel/bin/{conda-python.js → conda-pins.js} +23 -0
- package/kernel/bin/conda.js +15 -5
- package/kernel/bin/git.js +9 -10
- package/kernel/bin/huggingface.js +1 -1
- package/kernel/bin/index.js +5 -2
- package/kernel/bin/zip.js +9 -1
- package/kernel/connect/providers/github/README.md +5 -4
- package/kernel/environment.js +195 -92
- package/kernel/git.js +98 -19
- package/kernel/gitconfig_template +7 -0
- package/kernel/gpu/amd.js +72 -0
- package/kernel/gpu/apple.js +8 -0
- package/kernel/gpu/common.js +12 -0
- package/kernel/gpu/intel.js +47 -0
- package/kernel/gpu/nvidia.js +8 -0
- package/kernel/index.js +11 -1
- package/kernel/managed_skills.js +871 -0
- package/kernel/plugin.js +6 -58
- package/kernel/plugin_sources.js +316 -0
- package/kernel/resource_usage/gpu.js +349 -0
- package/kernel/resource_usage/index.js +322 -0
- package/kernel/resource_usage/macos_footprint.js +197 -0
- package/kernel/resource_usage/preferences.js +92 -0
- package/kernel/resource_usage/process_tree.js +303 -0
- package/kernel/scripts/git/create +4 -4
- package/kernel/scripts/git/fork +7 -8
- package/kernel/shell.js +23 -2
- package/kernel/shells.js +41 -0
- package/kernel/sysinfo.js +62 -9
- package/kernel/util.js +60 -0
- package/package.json +1 -1
- package/server/index.js +984 -156
- package/server/lib/app_log_report.js +543 -0
- package/server/lib/content_validation.js +55 -33
- package/server/lib/launcher_instruction_bootstrap.js +4 -96
- package/server/lib/terminal_session_helpers.js +0 -3
- package/server/public/common.js +77 -31
- package/server/public/create-launcher.js +4 -32
- package/server/public/logs.js +1428 -0
- package/server/public/nav.js +7 -0
- package/server/public/plugin-detail.js +93 -10
- package/server/public/privacy_filter_worker.js +391 -0
- package/server/public/style.css +1104 -154
- package/server/public/task-launcher.js +8 -29
- package/server/public/universal-launcher.css +8 -6
- package/server/public/universal-launcher.js +3 -27
- package/server/routes/apps.js +195 -1
- package/server/views/app.ejs +3041 -717
- package/server/views/autolaunch.ejs +917 -0
- package/server/views/bootstrap.ejs +7 -1
- package/server/views/d.ejs +408 -65
- package/server/views/editor.ejs +85 -19
- package/server/views/index.ejs +661 -111
- package/server/views/init/index.ejs +1 -1
- package/server/views/install.ejs +1 -1
- package/server/views/logs.ejs +164 -86
- package/server/views/net.ejs +7 -1
- package/server/views/partials/d_terminal_column.ejs +2 -2
- package/server/views/partials/d_terminal_options.ejs +0 -8
- package/server/views/partials/fs_status.ejs +47 -0
- package/server/views/partials/home_action_modal.ejs +86 -0
- package/server/views/partials/home_run_menu.ejs +87 -0
- package/server/views/partials/main_sidebar.ejs +2 -0
- package/server/views/partials/menu.ejs +1 -1
- package/server/views/plugin_detail.ejs +19 -4
- package/server/views/plugins.ejs +201 -3
- package/server/views/pre.ejs +1 -1
- package/server/views/pro.ejs +1 -1
- package/server/views/shell.ejs +40 -18
- package/server/views/skills.ejs +506 -0
- package/server/views/terminal.ejs +45 -19
- package/spec/INSTRUCTION_SYNC.md +20 -10
- package/system/plugin/antigravity-cli/antigravity.png +0 -0
- package/system/plugin/antigravity-cli/common.js +155 -0
- package/system/plugin/antigravity-cli/install.js +272 -0
- package/system/plugin/antigravity-cli/pinokio.js +13 -0
- package/system/plugin/antigravity-cli-auto/antigravity.png +0 -0
- package/system/plugin/antigravity-cli-auto/pinokio.js +13 -0
- package/system/plugin/claude/claude.png +0 -0
- package/system/plugin/claude/pinokio.js +47 -0
- package/system/plugin/claude-auto/claude.png +0 -0
- package/system/plugin/claude-auto/pinokio.js +58 -0
- package/system/plugin/claude-desktop/icon.jpeg +0 -0
- package/system/plugin/claude-desktop/pinokio.js +23 -0
- package/system/plugin/codex/openai.webp +0 -0
- package/system/plugin/codex/pinokio.js +42 -0
- package/system/plugin/codex-auto/openai.webp +0 -0
- package/system/plugin/codex-auto/pinokio.js +49 -0
- package/system/plugin/codex-desktop/icon.png +0 -0
- package/system/plugin/codex-desktop/pinokio.js +23 -0
- package/system/plugin/crush/crush.png +0 -0
- package/system/plugin/crush/pinokio.js +15 -0
- package/system/plugin/cursor/cursor.jpeg +0 -0
- package/system/plugin/cursor/pinokio.js +23 -0
- package/system/plugin/qwen/pinokio.js +34 -0
- package/system/plugin/qwen/qwen.png +0 -0
- package/system/plugin/vscode/pinokio.js +20 -0
- package/system/plugin/vscode/vscode.png +0 -0
- package/system/plugin/windsurf/pinokio.js +23 -0
- package/system/plugin/windsurf/windsurf.png +0 -0
- package/test/antigravity-cli-plugin.test.js +185 -0
- package/test/app-api.test.js +239 -0
- package/test/app-log-report.test.js +67 -0
- package/test/environment-cache-preflight.test.js +98 -0
- package/test/git-bin.test.js +59 -0
- package/test/git-defaults.test.js +97 -0
- package/test/github-api.test.js +158 -0
- package/test/github-connection.test.js +117 -0
- package/test/huggingface-bin.test.js +25 -0
- package/test/managed-skills.test.js +351 -0
- package/test/plugin-action-functions.test.js +337 -0
- package/test/plugin-dev-iframe.test.js +17 -0
- package/test/plugin-sources.test.js +203 -0
- package/test/privacy-filter-worker-heuristics.test.js +69 -0
- package/test/process-wait.test.js +169 -0
- package/test/script-api.test.js +97 -0
- package/test/shell-api.test.js +134 -0
- package/test/shell-run-template.test.js +209 -0
- package/test/storage-api.test.js +137 -0
- package/test/uri-api.test.js +100 -0
package/server/public/nav.js
CHANGED
|
@@ -25,6 +25,12 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
25
25
|
return;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
const headerMinimizeEnabled = !!minimizeButton;
|
|
29
|
+
if (!headerMinimizeEnabled) {
|
|
30
|
+
header.classList.remove("minimized", "transitioning");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (headerMinimizeEnabled) {
|
|
28
34
|
const homeIcon = homeLink ? homeLink.querySelector("img.icon") : null;
|
|
29
35
|
const ensureHomeExpandIcon = () => {
|
|
30
36
|
if (!homeLink || !homeIcon) {
|
|
@@ -492,6 +498,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
492
498
|
log("resize:clamp", { before, after: { left, top } });
|
|
493
499
|
});
|
|
494
500
|
|
|
501
|
+
}
|
|
495
502
|
|
|
496
503
|
// Inspector handling
|
|
497
504
|
const inspectorButton = document.querySelector('#inspector');
|
|
@@ -15,6 +15,14 @@
|
|
|
15
15
|
let share = bootstrap.share || {};
|
|
16
16
|
const apps = Array.isArray(bootstrap.apps) ? bootstrap.apps : [];
|
|
17
17
|
const stateUrl = typeof bootstrap.stateUrl === "string" ? bootstrap.stateUrl : "";
|
|
18
|
+
const cwdFromUrl = (() => {
|
|
19
|
+
try {
|
|
20
|
+
return new URLSearchParams(window.location.search).get("cwd") || "";
|
|
21
|
+
} catch (_) {
|
|
22
|
+
return "";
|
|
23
|
+
}
|
|
24
|
+
})();
|
|
25
|
+
plugin.cwd = typeof plugin.cwd === "string" && plugin.cwd ? plugin.cwd : cwdFromUrl;
|
|
18
26
|
|
|
19
27
|
const ACTION_LABELS = {
|
|
20
28
|
install: "Install",
|
|
@@ -47,9 +55,14 @@
|
|
|
47
55
|
const overlayRefresh = document.querySelector("[data-plugin-share-overlay-refresh]");
|
|
48
56
|
const actionsSection = document.querySelector("[data-plugin-actions-section]");
|
|
49
57
|
const nextStepBanner = document.querySelector("[data-plugin-next-step-banner]");
|
|
58
|
+
const aiPermissionCard = document.querySelector("[data-plugin-ai-permissions]");
|
|
59
|
+
const aiPermissionClear = document.querySelector("[data-plugin-ai-permission-clear]");
|
|
60
|
+
const aiPermissionPath = document.querySelector("[data-plugin-ai-permission-path]");
|
|
61
|
+
const aiPermissionStatus = document.querySelector("[data-plugin-ai-permission-status]");
|
|
50
62
|
|
|
51
63
|
let refreshInFlight = false;
|
|
52
64
|
let createFormOpen = false;
|
|
65
|
+
const AI_CONSENT_PREFIX = "pinokio:ai-consent:";
|
|
53
66
|
|
|
54
67
|
function readDownloadState() {
|
|
55
68
|
try {
|
|
@@ -116,6 +129,59 @@
|
|
|
116
129
|
}, 2200);
|
|
117
130
|
}
|
|
118
131
|
|
|
132
|
+
function normalizeConsentSource(value) {
|
|
133
|
+
if (!value || typeof value !== "string") return "";
|
|
134
|
+
return value.trim().replace(/\\/g, "/").replace(/\/+$/, "");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function aiPermissionKey() {
|
|
138
|
+
const source = normalizeConsentSource(plugin.cwd || "");
|
|
139
|
+
return source ? `${AI_CONSENT_PREFIX}${encodeURIComponent(source)}` : "";
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function updateAiPermissionStatus() {
|
|
143
|
+
const cwd = normalizeConsentSource(plugin.cwd || "");
|
|
144
|
+
if (aiPermissionCard) {
|
|
145
|
+
aiPermissionCard.classList.toggle("task-hidden", !cwd);
|
|
146
|
+
}
|
|
147
|
+
if (aiPermissionPath) {
|
|
148
|
+
aiPermissionPath.textContent = cwd;
|
|
149
|
+
}
|
|
150
|
+
if (!aiPermissionStatus) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const key = aiPermissionKey();
|
|
154
|
+
let status = "No project permission context.";
|
|
155
|
+
try {
|
|
156
|
+
if (!key || !window.localStorage) {
|
|
157
|
+
status = "Browser storage is unavailable.";
|
|
158
|
+
} else if (window.localStorage.getItem(key) !== null) {
|
|
159
|
+
status = "Saved permission exists for this project.";
|
|
160
|
+
} else {
|
|
161
|
+
status = "No saved permission for this project.";
|
|
162
|
+
}
|
|
163
|
+
} catch (_) {
|
|
164
|
+
status = "Browser storage is unavailable.";
|
|
165
|
+
}
|
|
166
|
+
aiPermissionStatus.textContent = status;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function clearAiPermission() {
|
|
170
|
+
const key = aiPermissionKey();
|
|
171
|
+
if (!key) {
|
|
172
|
+
updateAiPermissionStatus();
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
try {
|
|
176
|
+
window.localStorage.removeItem(key);
|
|
177
|
+
updateAiPermissionStatus();
|
|
178
|
+
showFeedback("AI permission cleared for this project.", "success");
|
|
179
|
+
} catch (_) {
|
|
180
|
+
updateAiPermissionStatus();
|
|
181
|
+
showFeedback("Unable to clear AI permission.", "error");
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
119
185
|
function clearNode(node) {
|
|
120
186
|
while (node && node.firstChild) {
|
|
121
187
|
node.removeChild(node.firstChild);
|
|
@@ -225,15 +291,15 @@
|
|
|
225
291
|
return "";
|
|
226
292
|
}
|
|
227
293
|
const queryPairs = [];
|
|
228
|
-
const pushPair = (key, value
|
|
294
|
+
const pushPair = (key, value) => {
|
|
229
295
|
if (value === undefined || value === null) {
|
|
230
296
|
return;
|
|
231
297
|
}
|
|
232
298
|
const encodedKey = encodeURIComponent(key);
|
|
233
|
-
const encodedValue =
|
|
299
|
+
const encodedValue = encodeURIComponent(String(value));
|
|
234
300
|
queryPairs.push(`${encodedKey}=${encodedValue}`);
|
|
235
301
|
};
|
|
236
|
-
pushPair("plugin", plugin.pluginPath
|
|
302
|
+
pushPair("plugin", plugin.pluginPath);
|
|
237
303
|
if (Array.isArray(plugin.extraParams)) {
|
|
238
304
|
plugin.extraParams.forEach(([key, value]) => {
|
|
239
305
|
pushPair(key, value);
|
|
@@ -749,23 +815,32 @@
|
|
|
749
815
|
|
|
750
816
|
function applyDownloadedState() {
|
|
751
817
|
const downloadState = readDownloadState();
|
|
752
|
-
if (!downloadState.downloaded) {
|
|
818
|
+
if (!downloadState.downloaded && !downloadState.next) {
|
|
753
819
|
return;
|
|
754
820
|
}
|
|
755
821
|
|
|
756
822
|
const installCard = document.querySelector('[data-plugin-action-card="install"]');
|
|
757
823
|
const openCard = document.querySelector('[data-plugin-action-card="open"]');
|
|
758
|
-
|
|
824
|
+
let nextAction = downloadState.next || (installCard ? "install" : (openCard ? "open" : ""));
|
|
825
|
+
if (nextAction === "install" && !installCard && openCard && plugin.installed === true) {
|
|
826
|
+
nextAction = "open";
|
|
827
|
+
}
|
|
759
828
|
const targetCard = nextAction ? document.querySelector(`[data-plugin-action-card="${nextAction}"]`) : null;
|
|
760
829
|
|
|
761
830
|
if (nextStepBanner) {
|
|
762
831
|
nextStepBanner.hidden = false;
|
|
763
832
|
nextStepBanner.classList.remove("task-hidden");
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
833
|
+
if (downloadState.downloaded) {
|
|
834
|
+
nextStepBanner.textContent = nextAction === "install"
|
|
835
|
+
? "Downloaded successfully. Next step: install this plugin."
|
|
836
|
+
: nextAction === "open"
|
|
837
|
+
? (plugin.installed === true ? "Installed. You can open this plugin in a project." : "Downloaded successfully. Next step: open this plugin in a project.")
|
|
838
|
+
: "Downloaded successfully.";
|
|
839
|
+
} else {
|
|
840
|
+
nextStepBanner.textContent = nextAction === "install"
|
|
841
|
+
? "This plugin needs to be installed before it can run."
|
|
842
|
+
: "Next step: open this plugin in a project.";
|
|
843
|
+
}
|
|
769
844
|
}
|
|
770
845
|
|
|
771
846
|
if (targetCard) {
|
|
@@ -915,6 +990,14 @@
|
|
|
915
990
|
});
|
|
916
991
|
}
|
|
917
992
|
|
|
993
|
+
if (aiPermissionClear) {
|
|
994
|
+
aiPermissionClear.addEventListener("click", (event) => {
|
|
995
|
+
event.preventDefault();
|
|
996
|
+
clearAiPermission();
|
|
997
|
+
});
|
|
998
|
+
updateAiPermissionStatus();
|
|
999
|
+
}
|
|
1000
|
+
|
|
918
1001
|
render();
|
|
919
1002
|
applyDownloadedState();
|
|
920
1003
|
})();
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
const TRANSFORMERS_URL = 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@4.2.0/+esm'
|
|
2
|
+
const MODEL_ID = 'openai/privacy-filter'
|
|
3
|
+
const MAX_CHUNK_CHARS = 1800
|
|
4
|
+
const CHUNK_OVERLAP_CHARS = 120
|
|
5
|
+
const URL_COMPONENT_LABELS = new Set([
|
|
6
|
+
'private_url',
|
|
7
|
+
'private_person',
|
|
8
|
+
'private_organization',
|
|
9
|
+
'private_username',
|
|
10
|
+
'private_name'
|
|
11
|
+
])
|
|
12
|
+
|
|
13
|
+
let transformersPromise = null
|
|
14
|
+
let pipelinePromise = null
|
|
15
|
+
let pipelineKey = ''
|
|
16
|
+
let activeDevice = 'webgpu'
|
|
17
|
+
let activeDtype = 'q4f16'
|
|
18
|
+
|
|
19
|
+
const post = (message) => {
|
|
20
|
+
self.postMessage(message)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const normalizeLabel = (value) => {
|
|
24
|
+
const label = String(value || 'private')
|
|
25
|
+
.replace(/^[bieos]-/i, '')
|
|
26
|
+
.replace(/[^a-z0-9_-]+/gi, '_')
|
|
27
|
+
.replace(/^_+|_+$/g, '')
|
|
28
|
+
.toLowerCase()
|
|
29
|
+
return label || 'private'
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const trimUrlCandidate = (value) => {
|
|
33
|
+
return String(value || '')
|
|
34
|
+
.replace(/^[([{<]+/, '')
|
|
35
|
+
.replace(/[)\]}>.,;:]+$/g, '')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const tokenAround = (text, start, end) => {
|
|
39
|
+
const value = String(text || '')
|
|
40
|
+
let left = Math.max(0, start)
|
|
41
|
+
let right = Math.min(value.length, end)
|
|
42
|
+
while (left > 0 && !/[\s"'`<>]/.test(value[left - 1])) {
|
|
43
|
+
left -= 1
|
|
44
|
+
}
|
|
45
|
+
while (right < value.length && !/[\s"'`<>]/.test(value[right])) {
|
|
46
|
+
right += 1
|
|
47
|
+
}
|
|
48
|
+
let raw = value.slice(left, right)
|
|
49
|
+
const trimmed = trimUrlCandidate(raw)
|
|
50
|
+
const leadingTrim = raw.length - raw.replace(/^[([{<]+/, '').length
|
|
51
|
+
left += leadingTrim
|
|
52
|
+
right = left + trimmed.length
|
|
53
|
+
return {
|
|
54
|
+
start: left,
|
|
55
|
+
end: right,
|
|
56
|
+
value: trimmed
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const parseUrlCandidate = (value) => {
|
|
61
|
+
let candidate = trimUrlCandidate(value)
|
|
62
|
+
candidate = candidate.replace(/^git\+/i, '')
|
|
63
|
+
const sshMatch = candidate.match(/^git@([^:\s]+):(.+)$/i)
|
|
64
|
+
if (sshMatch) {
|
|
65
|
+
return {
|
|
66
|
+
protocol: 'ssh:',
|
|
67
|
+
username: 'git',
|
|
68
|
+
password: '',
|
|
69
|
+
hostname: sshMatch[1],
|
|
70
|
+
search: '',
|
|
71
|
+
hash: ''
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (/^[A-Za-z0-9.-]+\.[A-Za-z]{2,}(?:[/:?#]|$)/.test(candidate)) {
|
|
75
|
+
candidate = `https://${candidate}`
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
return new URL(candidate)
|
|
79
|
+
} catch (_) {
|
|
80
|
+
return null
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const isExposableUrl = (value) => {
|
|
85
|
+
const url = parseUrlCandidate(value)
|
|
86
|
+
if (!url) {
|
|
87
|
+
return false
|
|
88
|
+
}
|
|
89
|
+
if (url.protocol === 'ssh:') {
|
|
90
|
+
return Boolean(url.hostname)
|
|
91
|
+
}
|
|
92
|
+
return /^(?:https?|git|ssh):$/i.test(url.protocol) && Boolean(url.hostname)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const shouldKeepEntityUnmasked = (text, entity) => {
|
|
96
|
+
if (!URL_COMPONENT_LABELS.has(entity.label)) {
|
|
97
|
+
return false
|
|
98
|
+
}
|
|
99
|
+
const candidate = tokenAround(text, entity.start, entity.end)
|
|
100
|
+
return Boolean(candidate.value && isExposableUrl(candidate.value))
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const detectUrlSecretEntities = (text) => {
|
|
104
|
+
const value = String(text || '')
|
|
105
|
+
const entities = []
|
|
106
|
+
const urlPattern = /(?:git\+)?https?:\/\/[^\s"'`<>]+|git@[^\s"'`<>]+:[^\s"'`<>]+/gi
|
|
107
|
+
let match = urlPattern.exec(value)
|
|
108
|
+
while (match) {
|
|
109
|
+
const token = tokenAround(value, match.index, match.index + match[0].length)
|
|
110
|
+
const candidate = token.value
|
|
111
|
+
const parsed = parseUrlCandidate(candidate)
|
|
112
|
+
if (parsed) {
|
|
113
|
+
if (parsed.protocol !== 'ssh:' && (parsed.username || parsed.password)) {
|
|
114
|
+
const userInfoMatch = candidate.match(/^[a-z][a-z0-9+.-]*:\/\/([^/@]+)@/i)
|
|
115
|
+
if (userInfoMatch) {
|
|
116
|
+
const start = token.start + candidate.indexOf(userInfoMatch[1])
|
|
117
|
+
entities.push({
|
|
118
|
+
label: 'url_credential',
|
|
119
|
+
start,
|
|
120
|
+
end: start + userInfoMatch[1].length
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const sensitiveValuePattern = /([?&#;](?:access[_-]?token|refresh[_-]?token|token|secret|password|passwd|api[_-]?key|apikey|auth|authorization|session|cookie|key|signature|sig|jwt)=)([^&#;\s"'`<>]+)/gi
|
|
125
|
+
let sensitiveMatch = sensitiveValuePattern.exec(candidate)
|
|
126
|
+
while (sensitiveMatch) {
|
|
127
|
+
const start = token.start + sensitiveMatch.index + sensitiveMatch[1].length
|
|
128
|
+
entities.push({
|
|
129
|
+
label: 'url_secret',
|
|
130
|
+
start,
|
|
131
|
+
end: start + sensitiveMatch[2].length
|
|
132
|
+
})
|
|
133
|
+
sensitiveMatch = sensitiveValuePattern.exec(candidate)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
match = urlPattern.exec(value)
|
|
137
|
+
}
|
|
138
|
+
return entities
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const prepareMaskEntities = (text, entities) => {
|
|
142
|
+
return [
|
|
143
|
+
...detectUrlSecretEntities(text),
|
|
144
|
+
...(entities || []).filter((entity) => !shouldKeepEntityUnmasked(text, entity))
|
|
145
|
+
]
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const attachOffsets = (text, raw, absoluteStart) => {
|
|
149
|
+
const entities = []
|
|
150
|
+
let cursor = 0
|
|
151
|
+
for (const entry of raw || []) {
|
|
152
|
+
const label = normalizeLabel(entry.entity_group || entry.entity || entry.label)
|
|
153
|
+
const score = Number(entry.score)
|
|
154
|
+
if (typeof entry.start === 'number' && typeof entry.end === 'number' && entry.end > entry.start) {
|
|
155
|
+
entities.push({
|
|
156
|
+
label,
|
|
157
|
+
score: Number.isFinite(score) ? score : 0,
|
|
158
|
+
start: absoluteStart + entry.start,
|
|
159
|
+
end: absoluteStart + entry.end
|
|
160
|
+
})
|
|
161
|
+
cursor = Math.max(cursor, entry.end)
|
|
162
|
+
continue
|
|
163
|
+
}
|
|
164
|
+
const word = String(entry.word || '').replace(/^\s+/, '')
|
|
165
|
+
const found = word ? text.indexOf(word, cursor) : -1
|
|
166
|
+
if (found >= 0) {
|
|
167
|
+
entities.push({
|
|
168
|
+
label,
|
|
169
|
+
score: Number.isFinite(score) ? score : 0,
|
|
170
|
+
start: absoluteStart + found,
|
|
171
|
+
end: absoluteStart + found + word.length
|
|
172
|
+
})
|
|
173
|
+
cursor = found + word.length
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return entities
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const chooseChunkEnd = (text, start, hardEnd) => {
|
|
180
|
+
if (hardEnd >= text.length) {
|
|
181
|
+
return text.length
|
|
182
|
+
}
|
|
183
|
+
const slice = text.slice(start, hardEnd)
|
|
184
|
+
const candidates = [
|
|
185
|
+
slice.lastIndexOf('\n\n'),
|
|
186
|
+
slice.lastIndexOf('\n'),
|
|
187
|
+
slice.lastIndexOf(' ')
|
|
188
|
+
].filter((index) => index > Math.floor(MAX_CHUNK_CHARS * 0.55))
|
|
189
|
+
if (candidates.length > 0) {
|
|
190
|
+
return start + Math.max(...candidates) + 1
|
|
191
|
+
}
|
|
192
|
+
return hardEnd
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const buildChunks = (text) => {
|
|
196
|
+
const chunks = []
|
|
197
|
+
let start = 0
|
|
198
|
+
while (start < text.length) {
|
|
199
|
+
const hardEnd = Math.min(start + MAX_CHUNK_CHARS, text.length)
|
|
200
|
+
const end = chooseChunkEnd(text, start, hardEnd)
|
|
201
|
+
chunks.push({
|
|
202
|
+
start,
|
|
203
|
+
text: text.slice(start, end)
|
|
204
|
+
})
|
|
205
|
+
if (end >= text.length) {
|
|
206
|
+
break
|
|
207
|
+
}
|
|
208
|
+
start = Math.max(end - CHUNK_OVERLAP_CHARS, start + 1)
|
|
209
|
+
}
|
|
210
|
+
return chunks
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const mergeEntities = (entities) => {
|
|
214
|
+
const sorted = entities
|
|
215
|
+
.filter((entity) => Number.isFinite(entity.start) && Number.isFinite(entity.end) && entity.end > entity.start)
|
|
216
|
+
.sort((a, b) => a.start - b.start || b.end - a.end || b.score - a.score)
|
|
217
|
+
const merged = []
|
|
218
|
+
for (const entity of sorted) {
|
|
219
|
+
const last = merged[merged.length - 1]
|
|
220
|
+
if (!last || entity.start >= last.end) {
|
|
221
|
+
merged.push(entity)
|
|
222
|
+
continue
|
|
223
|
+
}
|
|
224
|
+
const entityLength = entity.end - entity.start
|
|
225
|
+
const lastLength = last.end - last.start
|
|
226
|
+
if (entityLength > lastLength || (entityLength === lastLength && entity.score > last.score)) {
|
|
227
|
+
merged[merged.length - 1] = entity
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return merged.sort((a, b) => a.start - b.start || a.end - b.end)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const maskText = (text, entities) => {
|
|
234
|
+
let cursor = 0
|
|
235
|
+
let masked = ''
|
|
236
|
+
const counts = {}
|
|
237
|
+
const items = []
|
|
238
|
+
for (const entity of entities) {
|
|
239
|
+
if (entity.start < cursor) {
|
|
240
|
+
continue
|
|
241
|
+
}
|
|
242
|
+
masked += text.slice(cursor, entity.start)
|
|
243
|
+
const replacement = `[${entity.label}]`
|
|
244
|
+
const maskedStart = masked.length
|
|
245
|
+
masked += replacement
|
|
246
|
+
const maskedEnd = masked.length
|
|
247
|
+
counts[entity.label] = (counts[entity.label] || 0) + 1
|
|
248
|
+
items.push({
|
|
249
|
+
id: items.length,
|
|
250
|
+
label: entity.label,
|
|
251
|
+
score: entity.score,
|
|
252
|
+
sourceStart: entity.start,
|
|
253
|
+
sourceEnd: entity.end,
|
|
254
|
+
maskedStart,
|
|
255
|
+
maskedEnd,
|
|
256
|
+
replacement
|
|
257
|
+
})
|
|
258
|
+
cursor = entity.end
|
|
259
|
+
}
|
|
260
|
+
masked += text.slice(cursor)
|
|
261
|
+
for (const item of items) {
|
|
262
|
+
const lineStart = masked.lastIndexOf('\n', item.maskedStart - 1) + 1
|
|
263
|
+
const nextLine = masked.indexOf('\n', item.maskedEnd)
|
|
264
|
+
const lineEnd = nextLine >= 0 ? nextLine : masked.length
|
|
265
|
+
item.line = masked.slice(0, item.maskedStart).split('\n').length
|
|
266
|
+
item.context = masked.slice(lineStart, lineEnd).trim()
|
|
267
|
+
}
|
|
268
|
+
return { masked, counts, items }
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const loadTransformers = async () => {
|
|
272
|
+
if (!transformersPromise) {
|
|
273
|
+
transformersPromise = import(TRANSFORMERS_URL).then((mod) => {
|
|
274
|
+
if (mod.env) {
|
|
275
|
+
mod.env.allowLocalModels = false
|
|
276
|
+
mod.env.allowRemoteModels = true
|
|
277
|
+
mod.env.useBrowserCache = true
|
|
278
|
+
mod.env.useWasmCache = true
|
|
279
|
+
mod.env.cacheKey = 'pinokio-privacy-filter-cache'
|
|
280
|
+
}
|
|
281
|
+
return mod
|
|
282
|
+
})
|
|
283
|
+
}
|
|
284
|
+
return transformersPromise
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const getPipeline = async (device, dtype) => {
|
|
288
|
+
const nextDevice = device || activeDevice
|
|
289
|
+
const nextDtype = dtype || activeDtype
|
|
290
|
+
const nextKey = `${nextDevice}:${nextDtype}`
|
|
291
|
+
if (pipelinePromise && pipelineKey === nextKey) {
|
|
292
|
+
return pipelinePromise
|
|
293
|
+
}
|
|
294
|
+
activeDevice = nextDevice
|
|
295
|
+
activeDtype = nextDtype
|
|
296
|
+
pipelineKey = nextKey
|
|
297
|
+
pipelinePromise = loadTransformers().then(({ pipeline }) => {
|
|
298
|
+
return pipeline('token-classification', MODEL_ID, {
|
|
299
|
+
device: activeDevice,
|
|
300
|
+
dtype: activeDtype,
|
|
301
|
+
progress_callback: (progress) => {
|
|
302
|
+
if (progress && progress.status === 'progress') {
|
|
303
|
+
post({
|
|
304
|
+
type: 'download',
|
|
305
|
+
file: progress.file || '',
|
|
306
|
+
loaded: progress.loaded || 0,
|
|
307
|
+
total: progress.total || 0,
|
|
308
|
+
progress: progress.progress || 0
|
|
309
|
+
})
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
})
|
|
313
|
+
})
|
|
314
|
+
pipelinePromise.catch(() => {
|
|
315
|
+
if (pipelineKey === nextKey) {
|
|
316
|
+
pipelinePromise = null
|
|
317
|
+
pipelineKey = ''
|
|
318
|
+
}
|
|
319
|
+
})
|
|
320
|
+
return pipelinePromise
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const classifyChunks = async ({ classifier, chunks, id }) => {
|
|
324
|
+
const entities = []
|
|
325
|
+
for (let index = 0; index < chunks.length; index += 1) {
|
|
326
|
+
const chunk = chunks[index]
|
|
327
|
+
post({ type: 'chunk', id, done: index, total: chunks.length })
|
|
328
|
+
const output = await classifier(chunk.text, { aggregation_strategy: 'simple' })
|
|
329
|
+
entities.push(...attachOffsets(chunk.text, output, chunk.start))
|
|
330
|
+
}
|
|
331
|
+
return entities
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const filterText = async ({ id, text, device, dtype }) => {
|
|
335
|
+
const reportText = String(text || '')
|
|
336
|
+
const chunks = buildChunks(reportText)
|
|
337
|
+
const startedAt = performance.now()
|
|
338
|
+
let requestedDevice = device || activeDevice
|
|
339
|
+
let requestedDtype = dtype || activeDtype
|
|
340
|
+
let classifier = null
|
|
341
|
+
let entities = []
|
|
342
|
+
try {
|
|
343
|
+
classifier = await getPipeline(requestedDevice, requestedDtype)
|
|
344
|
+
entities = await classifyChunks({ classifier, chunks, id })
|
|
345
|
+
} catch (error) {
|
|
346
|
+
if (requestedDevice === 'wasm') {
|
|
347
|
+
throw error
|
|
348
|
+
}
|
|
349
|
+
pipelinePromise = null
|
|
350
|
+
pipelineKey = ''
|
|
351
|
+
requestedDevice = 'wasm'
|
|
352
|
+
requestedDtype = 'q8'
|
|
353
|
+
post({
|
|
354
|
+
type: 'fallback',
|
|
355
|
+
id,
|
|
356
|
+
device: requestedDevice,
|
|
357
|
+
dtype: requestedDtype,
|
|
358
|
+
message: 'WebGPU privacy filtering failed. Retrying locally with WASM.'
|
|
359
|
+
})
|
|
360
|
+
classifier = await getPipeline(requestedDevice, requestedDtype)
|
|
361
|
+
entities = await classifyChunks({ classifier, chunks, id })
|
|
362
|
+
}
|
|
363
|
+
const merged = mergeEntities(prepareMaskEntities(reportText, entities))
|
|
364
|
+
const result = maskText(reportText, merged)
|
|
365
|
+
post({
|
|
366
|
+
type: 'result',
|
|
367
|
+
id,
|
|
368
|
+
text: result.masked,
|
|
369
|
+
entities: merged,
|
|
370
|
+
items: result.items,
|
|
371
|
+
counts: result.counts,
|
|
372
|
+
latencyMs: performance.now() - startedAt,
|
|
373
|
+
device: activeDevice,
|
|
374
|
+
dtype: activeDtype,
|
|
375
|
+
chunks: chunks.length
|
|
376
|
+
})
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
self.addEventListener('message', (event) => {
|
|
380
|
+
const message = event.data || {}
|
|
381
|
+
if (message.type !== 'filter') {
|
|
382
|
+
return
|
|
383
|
+
}
|
|
384
|
+
filterText(message).catch((error) => {
|
|
385
|
+
post({
|
|
386
|
+
type: 'error',
|
|
387
|
+
id: message.id,
|
|
388
|
+
message: error && error.message ? error.message : String(error || 'Privacy filter failed')
|
|
389
|
+
})
|
|
390
|
+
})
|
|
391
|
+
})
|