devconfig-gen-singbox 2.0.0__py3-none-any.whl
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.
- devconfig_gen/__init__.py +67 -0
- devconfig_gen/cli.py +163 -0
- devconfig_gen/engine.py +217 -0
- devconfig_gen/formats.py +708 -0
- devconfig_gen/models.py +135 -0
- devconfig_gen/providers/__init__.py +8 -0
- devconfig_gen/providers/custom.py +92 -0
- devconfig_gen/providers/env_provider.py +184 -0
- devconfig_gen/providers/json_provider.py +84 -0
- devconfig_gen/providers/singbox/__init__.py +9 -0
- devconfig_gen/providers/singbox/data/rules.json +97 -0
- devconfig_gen/providers/singbox/links.py +24 -0
- devconfig_gen/providers/singbox/models.py +76 -0
- devconfig_gen/providers/singbox/plugins/__init__.py +33 -0
- devconfig_gen/providers/singbox/plugins/anytls.py +129 -0
- devconfig_gen/providers/singbox/plugins/base.py +117 -0
- devconfig_gen/providers/singbox/plugins/hysteria2.py +110 -0
- devconfig_gen/providers/singbox/plugins/tuic.py +90 -0
- devconfig_gen/providers/singbox/provider.py +362 -0
- devconfig_gen/providers/singbox/route.py +369 -0
- devconfig_gen/providers/singbox/schema.py +362 -0
- devconfig_gen/py.typed +0 -0
- devconfig_gen/registry.py +39 -0
- devconfig_gen/validation.py +148 -0
- devconfig_gen_singbox-2.0.0.dist-info/METADATA +343 -0
- devconfig_gen_singbox-2.0.0.dist-info/RECORD +30 -0
- devconfig_gen_singbox-2.0.0.dist-info/WHEEL +5 -0
- devconfig_gen_singbox-2.0.0.dist-info/entry_points.txt +3 -0
- devconfig_gen_singbox-2.0.0.dist-info/licenses/LICENSE +201 -0
- devconfig_gen_singbox-2.0.0.dist-info/top_level.txt +1 -0
devconfig_gen/models.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Stable public data structures for configuration providers."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any, Mapping, Optional, Protocol, Sequence
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(frozen=True)
|
|
8
|
+
class GenerationRequest:
|
|
9
|
+
"""Input to a provider; mappings are treated as read-only."""
|
|
10
|
+
|
|
11
|
+
context: Mapping[str, Any] = field(default_factory=dict)
|
|
12
|
+
options: Mapping[str, Any] = field(default_factory=dict)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class Diagnostic:
|
|
17
|
+
"""A single, machine-readable validation message.
|
|
18
|
+
|
|
19
|
+
``field`` is the dotted path to the offending value (for example
|
|
20
|
+
``app.port``). ``message`` is the fully rendered, user-facing text
|
|
21
|
+
(for example ``port must be between 1 and 65535, got 99999``).
|
|
22
|
+
``severity`` is ``"error"`` or ``"warning"``.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
field: str
|
|
26
|
+
message: str
|
|
27
|
+
severity: str = "error"
|
|
28
|
+
|
|
29
|
+
def __str__(self) -> str:
|
|
30
|
+
return self.message
|
|
31
|
+
|
|
32
|
+
def as_dict(self) -> dict:
|
|
33
|
+
return {"field": self.field, "message": self.message, "severity": self.severity}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class GeneratedArtifact:
|
|
38
|
+
name: str
|
|
39
|
+
content: Any
|
|
40
|
+
media_type: str = "application/json"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True)
|
|
44
|
+
class GenerationResult:
|
|
45
|
+
provider: str
|
|
46
|
+
artifacts: Sequence[GeneratedArtifact]
|
|
47
|
+
diagnostics: Sequence[Diagnostic] = field(default_factory=tuple)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True)
|
|
51
|
+
class ProviderField:
|
|
52
|
+
"""Declarative description of one provider input field.
|
|
53
|
+
|
|
54
|
+
``name`` is the dotted field path, e.g. ``app.port``. The remaining
|
|
55
|
+
attributes describe the value for documentation and future UI generation.
|
|
56
|
+
|
|
57
|
+
``i18n`` optionally maps a locale (e.g. ``"zh"``) to translated
|
|
58
|
+
``title``/``description`` strings, letting clients render localized
|
|
59
|
+
metadata without changing the canonical English defaults.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
name: str
|
|
63
|
+
type: str = "string"
|
|
64
|
+
required: bool = False
|
|
65
|
+
default: Any = None
|
|
66
|
+
description: str = ""
|
|
67
|
+
choices: Sequence[str] = ()
|
|
68
|
+
minimum: Optional[float] = None
|
|
69
|
+
maximum: Optional[float] = None
|
|
70
|
+
title: str = ""
|
|
71
|
+
i18n: Mapping[str, Mapping[str, str]] = field(default_factory=dict)
|
|
72
|
+
|
|
73
|
+
def as_dict(self) -> dict:
|
|
74
|
+
data = {
|
|
75
|
+
"name": self.name,
|
|
76
|
+
"title": self.title or self.name,
|
|
77
|
+
"type": self.type,
|
|
78
|
+
"required": self.required,
|
|
79
|
+
"default": self.default,
|
|
80
|
+
"description": self.description,
|
|
81
|
+
}
|
|
82
|
+
if self.choices:
|
|
83
|
+
data["choices"] = list(self.choices)
|
|
84
|
+
if self.minimum is not None:
|
|
85
|
+
data["minimum"] = self.minimum
|
|
86
|
+
if self.maximum is not None:
|
|
87
|
+
data["maximum"] = self.maximum
|
|
88
|
+
if self.i18n:
|
|
89
|
+
data["i18n"] = {locale: dict(values) for locale, values in self.i18n.items()}
|
|
90
|
+
return data
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass(frozen=True)
|
|
94
|
+
class ProviderStep:
|
|
95
|
+
"""A declarative step grouping fields for a guided input flow.
|
|
96
|
+
|
|
97
|
+
``i18n`` optionally maps a locale (e.g. ``"zh"``) to translated
|
|
98
|
+
``title``/``description`` strings for localized clients.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
id: str
|
|
102
|
+
title: str
|
|
103
|
+
description: str = ""
|
|
104
|
+
fields: Sequence[ProviderField] = ()
|
|
105
|
+
i18n: Mapping[str, Mapping[str, str]] = field(default_factory=dict)
|
|
106
|
+
|
|
107
|
+
def as_dict(self) -> dict:
|
|
108
|
+
data = {
|
|
109
|
+
"id": self.id,
|
|
110
|
+
"title": self.title,
|
|
111
|
+
"description": self.description,
|
|
112
|
+
"fields": [item.as_dict() for item in self.fields],
|
|
113
|
+
}
|
|
114
|
+
if self.i18n:
|
|
115
|
+
data["i18n"] = {locale: dict(values) for locale, values in self.i18n.items()}
|
|
116
|
+
return data
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class ConfigProvider(Protocol):
|
|
120
|
+
"""Structural contract every provider implements.
|
|
121
|
+
|
|
122
|
+
``validate`` returns rendered messages. Providers may additionally
|
|
123
|
+
implement ``diagnose`` (structured ``Diagnostic`` values) and
|
|
124
|
+
``describe_schema`` / ``steps`` (declarative field metadata). Metadata
|
|
125
|
+
methods are optional, so a minimal provider only needs ``name``,
|
|
126
|
+
``validate``, and ``generate``.
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
name: str
|
|
130
|
+
|
|
131
|
+
def generate(self, request: GenerationRequest) -> GenerationResult:
|
|
132
|
+
...
|
|
133
|
+
|
|
134
|
+
def validate(self, request: GenerationRequest) -> Sequence[str]:
|
|
135
|
+
...
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Built-in configuration providers."""
|
|
2
|
+
|
|
3
|
+
from .custom import CustomProvider
|
|
4
|
+
from .env_provider import EnvProvider
|
|
5
|
+
from .json_provider import JsonProvider
|
|
6
|
+
from .singbox import SingBoxProvider
|
|
7
|
+
|
|
8
|
+
__all__ = ["CustomProvider", "EnvProvider", "JsonProvider", "SingBoxProvider"]
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""A generic, schema-free provider for arbitrary JSON/YAML documents.
|
|
2
|
+
|
|
3
|
+
Unlike schema-bound providers that validate a fixed set of domain fields,
|
|
4
|
+
``custom`` imposes no schema at all: any nesting of mappings, sequences and
|
|
5
|
+
scalars is accepted, and callers can add, remove, or clear nodes at any level.
|
|
6
|
+
It exists so users are not forced into a particular domain model.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any, Mapping, Sequence
|
|
12
|
+
|
|
13
|
+
from .. import formats
|
|
14
|
+
from ..models import (
|
|
15
|
+
GeneratedArtifact,
|
|
16
|
+
GenerationRequest,
|
|
17
|
+
GenerationResult,
|
|
18
|
+
ProviderField,
|
|
19
|
+
ProviderStep,
|
|
20
|
+
)
|
|
21
|
+
from ..validation import ValidationError
|
|
22
|
+
|
|
23
|
+
DEFAULT_ARTIFACT_STEM = "custom"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class CustomProvider:
|
|
27
|
+
"""Pass an arbitrary, freely-edited document through the pipeline."""
|
|
28
|
+
|
|
29
|
+
name = "custom"
|
|
30
|
+
|
|
31
|
+
steps: Sequence[ProviderStep] = (
|
|
32
|
+
ProviderStep(
|
|
33
|
+
id="document",
|
|
34
|
+
title="Custom document",
|
|
35
|
+
description="Build any nested JSON/YAML structure; add, remove, or clear nodes at any level.",
|
|
36
|
+
i18n={
|
|
37
|
+
"zh": {
|
|
38
|
+
"title": "自定义文档",
|
|
39
|
+
"description": "自由构建任意嵌套的 JSON/YAML 结构;任意层级都可增删或清空。",
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
fields=(
|
|
43
|
+
ProviderField(
|
|
44
|
+
"document",
|
|
45
|
+
type="tree",
|
|
46
|
+
required=False,
|
|
47
|
+
title="Document tree",
|
|
48
|
+
description="Arbitrary nested document; keys and values are fully editable.",
|
|
49
|
+
i18n={
|
|
50
|
+
"zh": {
|
|
51
|
+
"title": "文档结构",
|
|
52
|
+
"description": "任意嵌套文档;键与值均可自由编辑。",
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
),
|
|
56
|
+
),
|
|
57
|
+
),
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
def describe_schema(self) -> Sequence[ProviderStep]:
|
|
61
|
+
return self.steps
|
|
62
|
+
|
|
63
|
+
def _unwrap_context(self, context: Any) -> Any:
|
|
64
|
+
if isinstance(context, Mapping) and set(context.keys()) == {"document"}:
|
|
65
|
+
return context["document"]
|
|
66
|
+
return context
|
|
67
|
+
|
|
68
|
+
def validate(self, request: GenerationRequest):
|
|
69
|
+
return ()
|
|
70
|
+
|
|
71
|
+
def generate(self, request: GenerationRequest) -> GenerationResult:
|
|
72
|
+
errors = self.validate(request)
|
|
73
|
+
if errors:
|
|
74
|
+
raise ValidationError(errors)
|
|
75
|
+
fmt = formats.resolve_format(
|
|
76
|
+
request.options.get("format"),
|
|
77
|
+
name=request.options.get("name"),
|
|
78
|
+
default=formats.JSON,
|
|
79
|
+
)
|
|
80
|
+
suffix = "yaml" if fmt == formats.YAML else "json"
|
|
81
|
+
filename = request.options.get("name") or f"{DEFAULT_ARTIFACT_STEM}.{suffix}"
|
|
82
|
+
context = self._unwrap_context(request.context)
|
|
83
|
+
return GenerationResult(
|
|
84
|
+
provider=self.name,
|
|
85
|
+
artifacts=(
|
|
86
|
+
GeneratedArtifact(
|
|
87
|
+
name=str(filename),
|
|
88
|
+
content=context,
|
|
89
|
+
media_type=formats.media_type_for(fmt),
|
|
90
|
+
),
|
|
91
|
+
),
|
|
92
|
+
)
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""A second provider with real transformation: structured data -> ``.env``.
|
|
2
|
+
|
|
3
|
+
Nested mappings are flattened into ``UPPER_SNAKE_CASE`` variables. This is a
|
|
4
|
+
domain-neutral, non-JSON output format, which makes the provider contract's
|
|
5
|
+
``media_type`` handling and multi-source merging easy to demonstrate.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
from typing import Any, Mapping, Optional, Sequence, Tuple
|
|
12
|
+
|
|
13
|
+
from ..models import (
|
|
14
|
+
Diagnostic,
|
|
15
|
+
GeneratedArtifact,
|
|
16
|
+
GenerationRequest,
|
|
17
|
+
GenerationResult,
|
|
18
|
+
ProviderField,
|
|
19
|
+
ProviderStep,
|
|
20
|
+
)
|
|
21
|
+
from ..validation import ValidationError, is_mapping
|
|
22
|
+
|
|
23
|
+
_SEGMENT_RE = re.compile(r"[^A-Za-z0-9]+")
|
|
24
|
+
|
|
25
|
+
DEFAULT_ARTIFACT_NAME = ".env"
|
|
26
|
+
ENV_MEDIA_TYPE = "text/plain"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class EnvProvider:
|
|
30
|
+
"""Flatten a structured mapping into a ``.env`` document."""
|
|
31
|
+
|
|
32
|
+
name = "env"
|
|
33
|
+
|
|
34
|
+
steps: Sequence[ProviderStep] = (
|
|
35
|
+
ProviderStep(
|
|
36
|
+
id="variables",
|
|
37
|
+
title="Environment variables",
|
|
38
|
+
description="Nested keys are joined with '_' and upper-cased.",
|
|
39
|
+
i18n={"zh": {"title": "环境变量", "description": "嵌套键用 '_' 连接并转为大写。"}},
|
|
40
|
+
fields=(
|
|
41
|
+
ProviderField(
|
|
42
|
+
"variables",
|
|
43
|
+
type="mapping",
|
|
44
|
+
required=True,
|
|
45
|
+
title="Variables",
|
|
46
|
+
description="Variables; nested keys become UPPER_SNAKE_CASE names.",
|
|
47
|
+
i18n={"zh": {"title": "变量", "description": "变量;嵌套键会转为 UPPER_SNAKE_CASE 名称。"}},
|
|
48
|
+
),
|
|
49
|
+
),
|
|
50
|
+
),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def describe_schema(self) -> Sequence[ProviderStep]:
|
|
54
|
+
return self.steps
|
|
55
|
+
|
|
56
|
+
def diagnose(self, request: GenerationRequest) -> Sequence[Diagnostic]:
|
|
57
|
+
_, diagnostics = self._prepare(request)
|
|
58
|
+
return tuple(diagnostics)
|
|
59
|
+
|
|
60
|
+
def validate(self, request: GenerationRequest):
|
|
61
|
+
return tuple(item.message for item in self.diagnose(request))
|
|
62
|
+
|
|
63
|
+
def generate(self, request: GenerationRequest) -> GenerationResult:
|
|
64
|
+
pairs, diagnostics = self._prepare(request)
|
|
65
|
+
if diagnostics:
|
|
66
|
+
raise ValidationError(diagnostics)
|
|
67
|
+
text = "\n".join(f"{key}={value}" for key, value in pairs.items()) + "\n"
|
|
68
|
+
name = request.options.get("name") or DEFAULT_ARTIFACT_NAME
|
|
69
|
+
return GenerationResult(
|
|
70
|
+
provider=self.name,
|
|
71
|
+
artifacts=(
|
|
72
|
+
GeneratedArtifact(
|
|
73
|
+
name=str(name),
|
|
74
|
+
content=text,
|
|
75
|
+
media_type=ENV_MEDIA_TYPE,
|
|
76
|
+
),
|
|
77
|
+
),
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# -- normalization + validation -----------------------------------------
|
|
81
|
+
|
|
82
|
+
def _prepare(self, request: GenerationRequest) -> Tuple[dict, list]:
|
|
83
|
+
diagnostics: list = []
|
|
84
|
+
context = request.context
|
|
85
|
+
if not is_mapping(context):
|
|
86
|
+
return {}, [Diagnostic("variables", "variables must be a mapping at the document root")]
|
|
87
|
+
|
|
88
|
+
raw: Mapping = context
|
|
89
|
+
if set(context.keys()) == {"variables"} and is_mapping(context["variables"]):
|
|
90
|
+
raw = context["variables"]
|
|
91
|
+
|
|
92
|
+
if not raw:
|
|
93
|
+
return {}, [Diagnostic("variables", "variables must not be empty")]
|
|
94
|
+
|
|
95
|
+
pairs: dict = {}
|
|
96
|
+
origins: dict = {}
|
|
97
|
+
self._flatten(raw, (), pairs, origins, diagnostics)
|
|
98
|
+
return pairs, diagnostics
|
|
99
|
+
|
|
100
|
+
def _flatten(
|
|
101
|
+
self,
|
|
102
|
+
source: Mapping,
|
|
103
|
+
prefix: Tuple[str, ...],
|
|
104
|
+
pairs: dict,
|
|
105
|
+
origins: dict,
|
|
106
|
+
diagnostics: list,
|
|
107
|
+
) -> None:
|
|
108
|
+
for key, value in source.items():
|
|
109
|
+
segment = self._segment(key, prefix, diagnostics)
|
|
110
|
+
if segment is None:
|
|
111
|
+
continue
|
|
112
|
+
path = prefix + (segment,)
|
|
113
|
+
|
|
114
|
+
if is_mapping(value):
|
|
115
|
+
if not value:
|
|
116
|
+
env_key = "_".join(path)
|
|
117
|
+
self._assign(env_key, "", path, pairs, origins, diagnostics)
|
|
118
|
+
else:
|
|
119
|
+
self._flatten(value, path, pairs, origins, diagnostics)
|
|
120
|
+
elif isinstance(value, (list, tuple)):
|
|
121
|
+
if any(is_mapping(item) for item in value):
|
|
122
|
+
diagnostics.append(
|
|
123
|
+
Diagnostic(
|
|
124
|
+
".".join(path).lower(),
|
|
125
|
+
f"{'.'.join(path).lower()} must not contain mappings",
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
continue
|
|
129
|
+
rendered = ",".join(self._scalar_to_env(item) for item in value)
|
|
130
|
+
self._assign("_".join(path), rendered, path, pairs, origins, diagnostics)
|
|
131
|
+
elif isinstance(value, (dict, set)):
|
|
132
|
+
diagnostics.append(
|
|
133
|
+
Diagnostic(
|
|
134
|
+
".".join(path).lower(),
|
|
135
|
+
f"{'.'.join(path).lower()} has an unsupported value type",
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
else:
|
|
139
|
+
self._assign(
|
|
140
|
+
"_".join(path), self._scalar_to_env(value), path, pairs, origins, diagnostics
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
def _assign(
|
|
144
|
+
self,
|
|
145
|
+
env_key: str,
|
|
146
|
+
value: str,
|
|
147
|
+
path: Tuple[str, ...],
|
|
148
|
+
pairs: dict,
|
|
149
|
+
origins: dict,
|
|
150
|
+
diagnostics: list,
|
|
151
|
+
) -> None:
|
|
152
|
+
origin = ".".join(path).lower()
|
|
153
|
+
if env_key in pairs and origins.get(env_key) != origin:
|
|
154
|
+
diagnostics.append(
|
|
155
|
+
Diagnostic(
|
|
156
|
+
origin,
|
|
157
|
+
f"'{env_key}' collides with '{origins[env_key]}' after normalization",
|
|
158
|
+
)
|
|
159
|
+
)
|
|
160
|
+
return
|
|
161
|
+
pairs[env_key] = value
|
|
162
|
+
origins[env_key] = origin
|
|
163
|
+
|
|
164
|
+
@staticmethod
|
|
165
|
+
def _segment(key: Any, prefix: Tuple[str, ...], diagnostics: list) -> Optional[str]:
|
|
166
|
+
segment = _SEGMENT_RE.sub("_", str(key)).strip("_").upper()
|
|
167
|
+
path = ".".join(prefix + (str(key),)).lower()
|
|
168
|
+
if not segment:
|
|
169
|
+
label = path or "<root>"
|
|
170
|
+
diagnostics.append(Diagnostic(path, f"{label} is not a valid variable name"))
|
|
171
|
+
return None
|
|
172
|
+
return segment
|
|
173
|
+
|
|
174
|
+
@staticmethod
|
|
175
|
+
def _scalar_to_env(value: Any) -> str:
|
|
176
|
+
if value is None:
|
|
177
|
+
return ""
|
|
178
|
+
if value is True:
|
|
179
|
+
return "true"
|
|
180
|
+
if value is False:
|
|
181
|
+
return "false"
|
|
182
|
+
if isinstance(value, (int, float)):
|
|
183
|
+
return str(value)
|
|
184
|
+
return str(value)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Pass-through JSON/YAML provider.
|
|
2
|
+
|
|
3
|
+
This provider performs no field-level transformation; it exists so callers can
|
|
4
|
+
normalize or re-serialize an arbitrary structured document through the same
|
|
5
|
+
pipeline as richer providers.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any, Mapping, Sequence
|
|
11
|
+
|
|
12
|
+
from .. import formats
|
|
13
|
+
from ..models import (
|
|
14
|
+
GeneratedArtifact,
|
|
15
|
+
GenerationRequest,
|
|
16
|
+
GenerationResult,
|
|
17
|
+
ProviderField,
|
|
18
|
+
ProviderStep,
|
|
19
|
+
)
|
|
20
|
+
from ..validation import ValidationError
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class JsonProvider:
|
|
24
|
+
name = "json"
|
|
25
|
+
|
|
26
|
+
steps: Sequence[ProviderStep] = (
|
|
27
|
+
ProviderStep(
|
|
28
|
+
id="document",
|
|
29
|
+
title="Document",
|
|
30
|
+
description="Upload, paste, or edit an arbitrary JSON/YAML document to pass through unchanged.",
|
|
31
|
+
i18n={"zh": {"title": "文档", "description": "支持上传文件或直接粘贴/编辑任意 JSON/YAML 文档,原样透传与格式转换。"}},
|
|
32
|
+
fields=(
|
|
33
|
+
ProviderField(
|
|
34
|
+
"document",
|
|
35
|
+
type="document",
|
|
36
|
+
required=True,
|
|
37
|
+
title="Document Content",
|
|
38
|
+
description="Upload a JSON/YAML file or paste/edit document content directly.",
|
|
39
|
+
i18n={"zh": {"title": "文档内容", "description": "支持直接上传 JSON/YAML 文件,或直接粘贴/编辑文档内容。"}},
|
|
40
|
+
),
|
|
41
|
+
),
|
|
42
|
+
),
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
def describe_schema(self) -> Sequence[ProviderStep]:
|
|
46
|
+
return self.steps
|
|
47
|
+
|
|
48
|
+
def _unwrap_context(self, context: Any) -> Any:
|
|
49
|
+
if (
|
|
50
|
+
isinstance(context, Mapping)
|
|
51
|
+
and set(context.keys()) == {"document"}
|
|
52
|
+
and isinstance(context["document"], Mapping)
|
|
53
|
+
):
|
|
54
|
+
return context["document"]
|
|
55
|
+
return context
|
|
56
|
+
|
|
57
|
+
def validate(self, request: GenerationRequest):
|
|
58
|
+
context = self._unwrap_context(request.context)
|
|
59
|
+
if not isinstance(context, Mapping):
|
|
60
|
+
return ("document: expected a mapping at the root",)
|
|
61
|
+
return ()
|
|
62
|
+
|
|
63
|
+
def generate(self, request: GenerationRequest) -> GenerationResult:
|
|
64
|
+
errors = self.validate(request)
|
|
65
|
+
if errors:
|
|
66
|
+
raise ValidationError(errors)
|
|
67
|
+
fmt = formats.resolve_format(
|
|
68
|
+
request.options.get("format"),
|
|
69
|
+
name=request.options.get("name"),
|
|
70
|
+
default=formats.JSON,
|
|
71
|
+
)
|
|
72
|
+
suffix = "yaml" if fmt == formats.YAML else "json"
|
|
73
|
+
filename = request.options.get("name") or f"config.{suffix}"
|
|
74
|
+
context = self._unwrap_context(request.context)
|
|
75
|
+
return GenerationResult(
|
|
76
|
+
provider=self.name,
|
|
77
|
+
artifacts=(
|
|
78
|
+
GeneratedArtifact(
|
|
79
|
+
name=str(filename),
|
|
80
|
+
content=dict(context),
|
|
81
|
+
media_type=formats.media_type_for(fmt),
|
|
82
|
+
),
|
|
83
|
+
),
|
|
84
|
+
)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""The sing-box domain provider (child-repo integration fork).
|
|
2
|
+
|
|
3
|
+
The neutral engine lives in the parent repository; this package owns all
|
|
4
|
+
sing-box domain detail and follows ``PROVIDER_STANDARD.md``.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .provider import SingBoxProvider
|
|
8
|
+
|
|
9
|
+
__all__ = ["SingBoxProvider"]
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
{
|
|
2
|
+
"direct_exact": [
|
|
3
|
+
"blzddist1-a.akamaihd.net",
|
|
4
|
+
"download.jetbrains.com",
|
|
5
|
+
"file-igamecj.akamaized.net",
|
|
6
|
+
"identity.apple.com",
|
|
7
|
+
"images-cn.ssl-images-amazon.com",
|
|
8
|
+
"officecdn-microsoft-com.akamaized.net",
|
|
9
|
+
"speedtest.macpaw.com",
|
|
10
|
+
"www-cdn.icloud.com.akadns.net"
|
|
11
|
+
],
|
|
12
|
+
"proxy_exact": [
|
|
13
|
+
"copilot.microsoft.com",
|
|
14
|
+
"copilot.bing.com",
|
|
15
|
+
"cdn.angruo.com",
|
|
16
|
+
"api.statsig.com",
|
|
17
|
+
"browser-intake-datadoghq.com",
|
|
18
|
+
"chat.openai.com.cdn.cloudflare.net",
|
|
19
|
+
"o33249.ingest.sentry.io",
|
|
20
|
+
"openai-api.arkoselabs.com",
|
|
21
|
+
"openaicom-api-bdcpf8c6d2e9atf6.z01.azurefd.net",
|
|
22
|
+
"openaicomproductionae4b.blob.core.windows.net",
|
|
23
|
+
"production-openaicom-storage.azureedge.net",
|
|
24
|
+
"static.cloudflareinsights.com",
|
|
25
|
+
"chatgpt.com",
|
|
26
|
+
"generativelanguage.googleapis.com",
|
|
27
|
+
"ai.google.dev"
|
|
28
|
+
],
|
|
29
|
+
"direct_suffix": [
|
|
30
|
+
"aaplimg.com",
|
|
31
|
+
"apple-cloudkit.com",
|
|
32
|
+
"apple.co",
|
|
33
|
+
"apple.news",
|
|
34
|
+
"apple.com.cn",
|
|
35
|
+
"appstore.com",
|
|
36
|
+
"cdn-apple.com",
|
|
37
|
+
"icloud-content.com",
|
|
38
|
+
"icloud.com",
|
|
39
|
+
"icloud.com.cn",
|
|
40
|
+
"me.com",
|
|
41
|
+
"mzstatic.com",
|
|
42
|
+
"apple.com",
|
|
43
|
+
"itunes.com"
|
|
44
|
+
],
|
|
45
|
+
"proxy_suffix": [
|
|
46
|
+
"apple-relay.akamaized.net",
|
|
47
|
+
"apple-relay.apple.com",
|
|
48
|
+
"apple-relay.cloudflare.com",
|
|
49
|
+
"apple-relay.fastly-edge.com",
|
|
50
|
+
"apple-relay.mask.apple-dns.net",
|
|
51
|
+
"m-team.cc",
|
|
52
|
+
"sciencemag.org",
|
|
53
|
+
"teamviewer.com",
|
|
54
|
+
"v2ex.com"
|
|
55
|
+
],
|
|
56
|
+
"direct_keyword": [
|
|
57
|
+
"apple.com.edgekey.net"
|
|
58
|
+
],
|
|
59
|
+
"proxy_keyword": [
|
|
60
|
+
"blogspot",
|
|
61
|
+
"pixiv",
|
|
62
|
+
"openaicom-api"
|
|
63
|
+
],
|
|
64
|
+
"direct_cidr": [
|
|
65
|
+
"192.168.0.0/16",
|
|
66
|
+
"10.0.0.0/8",
|
|
67
|
+
"172.16.0.0/12",
|
|
68
|
+
"127.0.0.0/8",
|
|
69
|
+
"2403:300:a42::/48",
|
|
70
|
+
"2403:300:a51::/48",
|
|
71
|
+
"2620:149:a44::/48",
|
|
72
|
+
"2a01:b740:a42::/48"
|
|
73
|
+
],
|
|
74
|
+
"proxy_cidr": [
|
|
75
|
+
"17.0.0.0/8",
|
|
76
|
+
"142.250.0.0/15",
|
|
77
|
+
"142.251.0.0/16",
|
|
78
|
+
"172.217.0.0/16",
|
|
79
|
+
"172.253.0.0/16",
|
|
80
|
+
"216.239.32.0/19",
|
|
81
|
+
"24.199.123.28/32",
|
|
82
|
+
"45.76.214.191/32",
|
|
83
|
+
"64.23.132.171/32",
|
|
84
|
+
"143.198.200.27/32",
|
|
85
|
+
"159.89.204.203/32",
|
|
86
|
+
"91.108.4.0/22",
|
|
87
|
+
"91.108.8.0/22",
|
|
88
|
+
"91.108.12.0/22",
|
|
89
|
+
"91.108.16.0/22",
|
|
90
|
+
"91.108.56.0/22",
|
|
91
|
+
"109.239.140.0/24",
|
|
92
|
+
"149.154.160.0/20",
|
|
93
|
+
"2001:B28:F23D::/48",
|
|
94
|
+
"2001:B28:F23F::/48",
|
|
95
|
+
"2001:67C:4E8::/48"
|
|
96
|
+
]
|
|
97
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Share-link aggregation.
|
|
2
|
+
|
|
3
|
+
Links are derived output: one line per enabled protocol, in input order, so the
|
|
4
|
+
result is deterministic and stable.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Sequence
|
|
10
|
+
|
|
11
|
+
from .models import BuildContext, ProtocolConfig
|
|
12
|
+
from .plugins import get_plugin
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def build_links(protocols: Sequence[ProtocolConfig], ctx: BuildContext) -> str:
|
|
16
|
+
lines = []
|
|
17
|
+
for cfg in protocols:
|
|
18
|
+
plugin = get_plugin(cfg.type)
|
|
19
|
+
if plugin is None:
|
|
20
|
+
continue
|
|
21
|
+
lines.append(plugin.build_share_link(cfg, ctx))
|
|
22
|
+
if not lines:
|
|
23
|
+
return ""
|
|
24
|
+
return "\n".join(lines) + "\n"
|