Plan Checker (neues Studenten-Werkzeug, Seitenleiste + Admin → Plan-Checker- Merkmale): lädt einen Bauplan als PDF hoch und lässt ChatGPT oder Claude ihn gegen eine gepflegte Merkmalsliste prüfen. PDF-Rendering (pdf.js + @napi-rs/canvas) berücksichtigt jetzt Ebenen (Optional Content Groups) und WASM-Decoder korrekt, sendet eine Übersicht plus mehrere hochaufgelöste Kachel-Ausschnitte statt eines einzelnen, zu groben Bilds, und beide Anbieter bekommen dasselbe Bildmaterial. Jedes Prüfergebnis kann auf eines der gesendeten Bilder als Beleg verweisen (bildIndex) — als Screenshot im Ergebnis anklickbar, öffnet vergrößert in einem Modal statt eines von Chrome blockierten target=_blank-Links auf eine data:-URL. KI-Modellname ist jetzt pro Konto überschreibbar (Einstellungen → Mein Konto) für Drittanbieter mit abweichendem Modell-Katalog. Wochenbericht: dichte Kartenansicht bekommt einen Fokus-Modus — nur der Kommentar liegt offen, Leistungsphasen und Beteiligte stecken in aufklappbaren Abschnitten statt dauerhaft nebeneinander zu stehen. Projektbereich: Terminplan-Auslastung rechnet jetzt exakt wie der Auslastungs-Tab/die Wandtafel (statt einer unabhängigen, tageweisen Schätzung); die LPH-Fortschrittsliste im Dashboard zeigt nur noch die beauftragten Leistungsphasen. Wandtafel: Dashboard-Kopf mit Kennzahlen-Reihe und Panel-Reihe neu strukturiert, in den Blautönen des Firmenlogos; dieselbe Palette wurde anschließend auf den gesamten App-weiten Dark Mode übernommen. Dateibereinigung: nicht mehr benötigte Test-Uploads/Backups entfernt. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
191 lines
6.6 KiB
JavaScript
191 lines
6.6 KiB
JavaScript
'use strict'
|
|
|
|
const DispatcherBase = require('./dispatcher-base')
|
|
const { kClose, kDestroy, kClosed, kDestroyed, kDispatch, kNoProxyAgent, kHttpProxyAgent, kHttpsProxyAgent } = require('../core/symbols')
|
|
const ProxyAgent = require('./proxy-agent')
|
|
const Agent = require('./agent')
|
|
|
|
const DEFAULT_PORTS = {
|
|
'http:': 80,
|
|
'https:': 443
|
|
}
|
|
|
|
class EnvHttpProxyAgent extends DispatcherBase {
|
|
#noProxyValue = null
|
|
#noProxyEntries = null
|
|
#opts = null
|
|
|
|
constructor (opts = {}) {
|
|
super(opts)
|
|
this.#opts = opts
|
|
|
|
const { httpProxy, httpsProxy, noProxy, ...agentOpts } = opts
|
|
|
|
this[kNoProxyAgent] = new Agent(agentOpts)
|
|
|
|
const HTTP_PROXY = httpProxy ?? process.env.http_proxy ?? process.env.HTTP_PROXY
|
|
if (HTTP_PROXY) {
|
|
this[kHttpProxyAgent] = new ProxyAgent({ ...agentOpts, uri: HTTP_PROXY })
|
|
} else {
|
|
this[kHttpProxyAgent] = this[kNoProxyAgent]
|
|
}
|
|
|
|
const HTTPS_PROXY = httpsProxy ?? process.env.https_proxy ?? process.env.HTTPS_PROXY
|
|
if (HTTPS_PROXY) {
|
|
this[kHttpsProxyAgent] = new ProxyAgent({ ...agentOpts, uri: HTTPS_PROXY })
|
|
} else {
|
|
this[kHttpsProxyAgent] = this[kHttpProxyAgent]
|
|
}
|
|
|
|
this.#parseNoProxy()
|
|
}
|
|
|
|
[kDispatch] (opts, handler) {
|
|
const url = new URL(opts.origin)
|
|
const agent = this.#getProxyAgentForUrl(url)
|
|
return agent.dispatch(opts, handler)
|
|
}
|
|
|
|
[kClose] () {
|
|
return Promise.all([
|
|
this[kNoProxyAgent].close(),
|
|
!this[kHttpProxyAgent][kClosed] && this[kHttpProxyAgent].close(),
|
|
!this[kHttpsProxyAgent][kClosed] && this[kHttpsProxyAgent].close()
|
|
])
|
|
}
|
|
|
|
[kDestroy] (err) {
|
|
return Promise.all([
|
|
this[kNoProxyAgent].destroy(err),
|
|
!this[kHttpProxyAgent][kDestroyed] && this[kHttpProxyAgent].destroy(err),
|
|
!this[kHttpsProxyAgent][kDestroyed] && this[kHttpsProxyAgent].destroy(err)
|
|
])
|
|
}
|
|
|
|
#getProxyAgentForUrl (url) {
|
|
let { protocol, host: hostname, port } = url
|
|
|
|
// Remove the port suffix (e.g. ":8080") and then strip surrounding
|
|
// brackets from IPv6 literals (e.g. "[::1]" -> "::1") so that the
|
|
// result matches the unbracketed form stored by #parseNoProxy.
|
|
hostname = hostname.replace(/:\d*$/, '').replace(/^\[(.+)\]$/, '$1').toLowerCase()
|
|
// Drop a trailing dot: it only marks the fully qualified form of a domain
|
|
// name ("example.com." and "example.com" are the same name, RFC 1034 root
|
|
// label). This runs on every dispatch, so it is a charCode check rather
|
|
// than a third regex. `length > 1` leaves the degenerate host "." alone.
|
|
if (hostname.length > 1 && hostname.charCodeAt(hostname.length - 1) === 46) {
|
|
hostname = hostname.slice(0, -1)
|
|
}
|
|
port = Number.parseInt(port, 10) || DEFAULT_PORTS[protocol] || 0
|
|
if (!this.#shouldProxy(hostname, port)) {
|
|
return this[kNoProxyAgent]
|
|
}
|
|
if (protocol === 'https:') {
|
|
return this[kHttpsProxyAgent]
|
|
}
|
|
return this[kHttpProxyAgent]
|
|
}
|
|
|
|
#shouldProxy (hostname, port) {
|
|
if (this.#noProxyChanged) {
|
|
this.#parseNoProxy()
|
|
}
|
|
|
|
if (this.#noProxyEntries.length === 0) {
|
|
return true // Always proxy if NO_PROXY is not set or empty.
|
|
}
|
|
|
|
for (let i = 0; i < this.#noProxyEntries.length; i++) {
|
|
const entry = this.#noProxyEntries[i]
|
|
// A bare `*` entry matches all hosts regardless of its position or the
|
|
// surrounding whitespace (e.g. ` * ` or `none.invalid,*`). If a port is
|
|
// attached (`*:80`) it only bypasses that port.
|
|
if (entry.hostname === '*') {
|
|
if (entry.port && entry.port !== port) {
|
|
continue
|
|
}
|
|
return false // Never proxy if a wildcard entry is present.
|
|
}
|
|
if (entry.port && entry.port !== port) {
|
|
continue // Skip if ports don't match.
|
|
}
|
|
// Don't proxy if the hostname is equal with the no_proxy host. A
|
|
// `*.example.com` wildcard matches subdomains only, not the apex
|
|
// `example.com`, so exact matches are skipped for wildcard entries.
|
|
if (!entry.wildcard && hostname === entry.hostname) {
|
|
return false
|
|
}
|
|
// Don't proxy if the hostname is the subdomain of the no_proxy host.
|
|
// Reference - https://github.com/denoland/deno/blob/6fbce91e40cc07fc6da74068e5cc56fdd40f7b4c/ext/fetch/proxy.rs#L485
|
|
if (hostname.slice(-(entry.hostname.length + 1)) === `.${entry.hostname}`) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
#parseNoProxy () {
|
|
const noProxyValue = this.#opts.noProxy ?? this.#noProxyEnv
|
|
const noProxySplit = noProxyValue.split(/[,\s]/)
|
|
const noProxyEntries = []
|
|
|
|
for (let i = 0; i < noProxySplit.length; i++) {
|
|
const entry = noProxySplit[i]
|
|
if (!entry) {
|
|
continue
|
|
}
|
|
|
|
// An IPv6 entry with a port must be bracketed: [::1]:443.
|
|
// A bare IPv6 address like ::1 contains colons that must not be
|
|
// confused with a host:port separator, so we handle it separately.
|
|
let hostname, port
|
|
const ipv6WithPort = entry.match(/^\[(.+)\]:(\d+)$/)
|
|
if (ipv6WithPort) {
|
|
hostname = ipv6WithPort[1]
|
|
port = Number.parseInt(ipv6WithPort[2], 10)
|
|
} else {
|
|
// Bracketed IPv6 without port, or plain hostname[:port], or bare IPv6.
|
|
// Strip optional brackets first.
|
|
const unbracketed = entry.replace(/^\[(.+)\]$/, '$1')
|
|
// A bare IPv6 address contains multiple colons; a hostname:port entry
|
|
// has exactly one colon followed by digits. Only attempt host:port
|
|
// splitting when that is unambiguously the case.
|
|
const colonCount = (unbracketed.match(/:/g) || []).length
|
|
const parsed = colonCount === 1 && unbracketed.match(/^(.+):(\d+)$/)
|
|
hostname = parsed ? parsed[1] : unbracketed
|
|
port = parsed ? Number.parseInt(parsed[2], 10) : 0
|
|
}
|
|
|
|
// A leading `*` marks a subdomain wildcard (`*.example.com`), distinct
|
|
// from a plain or leading-dot suffix (`example.com` / `.example.com`)
|
|
// which also matches the apex. `*.example.com` must only match
|
|
// subdomains, never the apex `example.com` itself.
|
|
const wildcard = entry.charCodeAt(0) === 42 /* '*' */
|
|
|
|
noProxyEntries.push({
|
|
// strip leading dot or asterisk with dot, and any trailing dot
|
|
hostname: hostname.replace(/^\*?\./, '').replace(/^(.+)\.$/, '$1').toLowerCase(),
|
|
port,
|
|
wildcard
|
|
})
|
|
}
|
|
|
|
this.#noProxyValue = noProxyValue
|
|
this.#noProxyEntries = noProxyEntries
|
|
}
|
|
|
|
get #noProxyChanged () {
|
|
if (this.#opts.noProxy !== undefined) {
|
|
return false
|
|
}
|
|
return this.#noProxyValue !== this.#noProxyEnv
|
|
}
|
|
|
|
get #noProxyEnv () {
|
|
return process.env.no_proxy ?? process.env.NO_PROXY ?? ''
|
|
}
|
|
}
|
|
|
|
module.exports = EnvHttpProxyAgent
|