netizen-cli 0.10.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.
- netizen_cli/__init__.py +3 -0
- netizen_cli/__main__.py +4 -0
- netizen_cli/admin/__init__.py +1 -0
- netizen_cli/admin/auth.py +928 -0
- netizen_cli/admin/errors.py +9 -0
- netizen_cli/admin/port_config.py +115 -0
- netizen_cli/admin/presentation.py +257 -0
- netizen_cli/admin/queries.py +337 -0
- netizen_cli/admin/static/admin.css +260 -0
- netizen_cli/admin/static/admin.js +2898 -0
- netizen_cli/admin/static/index.html +327 -0
- netizen_cli/admin/transport.py +935 -0
- netizen_cli/admin/web.py +2717 -0
- netizen_cli/bindings.py +3215 -0
- netizen_cli/builtin_skills.py +93 -0
- netizen_cli/cards/__init__.py +105 -0
- netizen_cli/cards/callbacks.py +565 -0
- netizen_cli/cards/controls.py +2273 -0
- netizen_cli/cards/defaults.py +213 -0
- netizen_cli/cards/model_info.py +80 -0
- netizen_cli/cards/questions.py +220 -0
- netizen_cli/cards/reply.py +2247 -0
- netizen_cli/cards/scheduled.py +836 -0
- netizen_cli/channel/__init__.py +1 -0
- netizen_cli/channel/completion_mentions.py +60 -0
- netizen_cli/channel/input_preparation.py +644 -0
- netizen_cli/channel/messages.py +57 -0
- netizen_cli/channel/ports.py +52 -0
- netizen_cli/channel/question_inputs.py +51 -0
- netizen_cli/channel/reactions.py +293 -0
- netizen_cli/channel/reply_presenter.py +1505 -0
- netizen_cli/channel/topics.py +70 -0
- netizen_cli/channel_app.py +6593 -0
- netizen_cli/cli.py +287 -0
- netizen_cli/cli_data.py +536 -0
- netizen_cli/cli_packages.py +526 -0
- netizen_cli/cli_services.py +651 -0
- netizen_cli/cli_setup.py +242 -0
- netizen_cli/cli_update.py +303 -0
- netizen_cli/cli_update_restore.py +53 -0
- netizen_cli/cli_update_worker.py +333 -0
- netizen_cli/codex_runtime.py +7125 -0
- netizen_cli/completion_mention.py +16 -0
- netizen_cli/database_migrations.py +218 -0
- netizen_cli/defaults/__init__.py +5 -0
- netizen_cli/defaults/models.py +39 -0
- netizen_cli/defaults/service.py +232 -0
- netizen_cli/defaults/store.py +260 -0
- netizen_cli/deployment/__init__.py +1 -0
- netizen_cli/deployment/restart_worker.py +134 -0
- netizen_cli/deployment/update_executor.py +258 -0
- netizen_cli/deployment/update_protocol.py +281 -0
- netizen_cli/domain.py +416 -0
- netizen_cli/error_messages.py +124 -0
- netizen_cli/experience.py +531 -0
- netizen_cli/feishu_app_onboarding.py +187 -0
- netizen_cli/feishu_app_permissions.py +123 -0
- netizen_cli/git_status.py +63 -0
- netizen_cli/image_inputs.py +579 -0
- netizen_cli/instance.py +84 -0
- netizen_cli/lark_app.py +125 -0
- netizen_cli/main.py +903 -0
- netizen_cli/management/__init__.py +83 -0
- netizen_cli/management/blocking_io.py +352 -0
- netizen_cli/management/chat_labels.py +266 -0
- netizen_cli/management/coordination.py +32 -0
- netizen_cli/management/service.py +2187 -0
- netizen_cli/management/updates.py +214 -0
- netizen_cli/markdown_images.py +78 -0
- netizen_cli/message_content.py +786 -0
- netizen_cli/message_history.py +643 -0
- netizen_cli/message_preparation.py +60 -0
- netizen_cli/message_projection.py +923 -0
- netizen_cli/migrations/__init__.py +1 -0
- netizen_cli/migrations/schema.py +103 -0
- netizen_cli/migrations/v14.py +438 -0
- netizen_cli/model_settings.py +269 -0
- netizen_cli/package_resources.py +22 -0
- netizen_cli/projects.py +327 -0
- netizen_cli/prompt_projection.py +327 -0
- netizen_cli/quoted_context.py +312 -0
- netizen_cli/resources/config.example.yaml +35 -0
- netizen_cli/resources/skills/netizen-lark/SKILL.md +64 -0
- netizen_cli/resources/skills/netizen-user-guide/SKILL.md +37 -0
- netizen_cli/resources/skills/netizen-user-guide/references/user-guide.md +842 -0
- netizen_cli/result_images.py +123 -0
- netizen_cli/runtime/__init__.py +1 -0
- netizen_cli/runtime/contracts.py +792 -0
- netizen_cli/runtime/name_writes.py +67 -0
- netizen_cli/runtime/thread_naming.py +451 -0
- netizen_cli/schedules/__init__.py +1 -0
- netizen_cli/schedules/mcp.py +535 -0
- netizen_cli/schedules/models.py +394 -0
- netizen_cli/schedules/scheduler.py +374 -0
- netizen_cli/schedules/service.py +766 -0
- netizen_cli/schedules/store.py +771 -0
- netizen_cli/sdk_gap_adapter.py +1151 -0
- netizen_cli/service_launcher.py +583 -0
- netizen_cli/session_settings.py +126 -0
- netizen_cli/settings.py +216 -0
- netizen_cli/skill_references.py +40 -0
- netizen_cli/terminal_cleanup.py +155 -0
- netizen_cli/turn_activity.py +688 -0
- netizen_cli/turn_files.py +812 -0
- netizen_cli/turn_patch_children.py +254 -0
- netizen_cli/turn_plan_observer.py +315 -0
- netizen_cli/user_questions.py +106 -0
- netizen_cli-0.10.0.dist-info/METADATA +18 -0
- netizen_cli-0.10.0.dist-info/RECORD +112 -0
- netizen_cli-0.10.0.dist-info/WHEEL +5 -0
- netizen_cli-0.10.0.dist-info/entry_points.txt +2 -0
- netizen_cli-0.10.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""Instance-only Admin maintenance; package updates belong to the CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
import hashlib
|
|
7
|
+
import importlib.metadata
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
import pwd
|
|
12
|
+
import sys
|
|
13
|
+
import time
|
|
14
|
+
from typing import Any, Callable
|
|
15
|
+
|
|
16
|
+
from .blocking_io import BoundedBlockingIOExecutor
|
|
17
|
+
from ..instance import resolve_instance_root
|
|
18
|
+
from ..deployment.update_executor import UpdateExecutor, UpdateDispatchUnknown, UpdateExecutorError
|
|
19
|
+
from ..deployment.update_protocol import (
|
|
20
|
+
UpdateProtocolError, acquire_install_lock, advance_operation,
|
|
21
|
+
new_cli_restart_operation, read_operation, terminal_phase, write_operation,
|
|
22
|
+
)
|
|
23
|
+
from ..deployment.restart_worker import START_HANDOFF_SECONDS
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class UpdateError(RuntimeError):
|
|
27
|
+
"""Stable application failure; entry points own presentation."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, code: str) -> None:
|
|
30
|
+
self.code = code
|
|
31
|
+
super().__init__(code)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class InstalledPackage:
|
|
36
|
+
version: str
|
|
37
|
+
python: Path
|
|
38
|
+
prefix: Path
|
|
39
|
+
package: Path
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def identity(self) -> str:
|
|
43
|
+
# Resolving Python's symlink would merge independent virtual environments.
|
|
44
|
+
payload = [self.version, str(self.python), str(self.prefix), str(self.package)]
|
|
45
|
+
return hashlib.sha256(json.dumps(payload, separators=(",", ":")).encode()).hexdigest()
|
|
46
|
+
|
|
47
|
+
def as_dict(self) -> dict[str, Any]:
|
|
48
|
+
return {"version": self.version, "source": "python",
|
|
49
|
+
"installationId": self.identity, "python": str(self.python)}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def installed_package() -> InstalledPackage:
|
|
53
|
+
try:
|
|
54
|
+
version = importlib.metadata.version("netizen-cli")
|
|
55
|
+
except importlib.metadata.PackageNotFoundError:
|
|
56
|
+
version = "unknown"
|
|
57
|
+
return InstalledPackage(version, Path(os.path.abspath(sys.executable)),
|
|
58
|
+
Path(sys.prefix).resolve(), Path(__file__).resolve().parents[1])
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class UpdateService:
|
|
62
|
+
"""One bounded management worker, with no package-manager authority."""
|
|
63
|
+
|
|
64
|
+
def __init__(self, *, root: Path | None = None, home: Path | None = None,
|
|
65
|
+
current: InstalledPackage | None = None,
|
|
66
|
+
executor: UpdateExecutor | None = None,
|
|
67
|
+
binding_matches: Callable[[Path, InstalledPackage], bool] | None = None,
|
|
68
|
+
clock: Callable[[], float] = time.time) -> None:
|
|
69
|
+
self.home = (home or Path(pwd.getpwuid(os.geteuid()).pw_dir)).resolve()
|
|
70
|
+
self.product_root = resolve_instance_root(root, account_home=self.home)
|
|
71
|
+
self._current = current or installed_package()
|
|
72
|
+
self._executor = executor
|
|
73
|
+
self._binding_matches = binding_matches or self._matches_bound_environment
|
|
74
|
+
self._clock = clock
|
|
75
|
+
self._service_ready = False
|
|
76
|
+
self._startup_restart_id: str | None = None
|
|
77
|
+
try:
|
|
78
|
+
operation = read_operation(self.product_root)
|
|
79
|
+
if (operation is not None and operation.get("schema") == 3
|
|
80
|
+
and operation["phase"] in {"restarting", "recovery_required"}):
|
|
81
|
+
self._startup_restart_id = operation["operationId"]
|
|
82
|
+
except (UpdateProtocolError, OSError):
|
|
83
|
+
pass # Maintenance diagnostics must not prevent normal startup.
|
|
84
|
+
self._io = BoundedBlockingIOExecutor(max_workers=1, capacity=2,
|
|
85
|
+
thread_name_prefix="netizen-maintenance-io")
|
|
86
|
+
|
|
87
|
+
@staticmethod
|
|
88
|
+
def _matches_bound_environment(root: Path, current: InstalledPackage) -> bool:
|
|
89
|
+
from ..cli_services import ServiceError, ServiceManager
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
status = ServiceManager().inspect(root)
|
|
93
|
+
return bool(status is not None
|
|
94
|
+
and status.binding.prefix == current.prefix
|
|
95
|
+
and status.binding.python == current.python)
|
|
96
|
+
except (ServiceError, OSError, ValueError):
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
async def close(self, *, deadline: float | None = None) -> None:
|
|
100
|
+
self.set_service_ready(False)
|
|
101
|
+
await self._io.aclose(deadline=deadline)
|
|
102
|
+
|
|
103
|
+
def set_service_ready(self, ready: bool) -> None:
|
|
104
|
+
self._service_ready = ready
|
|
105
|
+
|
|
106
|
+
async def status(self) -> dict[str, Any]:
|
|
107
|
+
return await self._io.submit(self._status)
|
|
108
|
+
|
|
109
|
+
async def restart(self, *, installation_id: str) -> dict[str, Any]:
|
|
110
|
+
return await self._io.submit(self._restart, installation_id=installation_id)
|
|
111
|
+
|
|
112
|
+
def _manager(self) -> UpdateExecutor:
|
|
113
|
+
if self._executor is None:
|
|
114
|
+
self._executor = UpdateExecutor(self.home, root=self.product_root)
|
|
115
|
+
return self._executor
|
|
116
|
+
|
|
117
|
+
def _restart_supported(self) -> bool:
|
|
118
|
+
return (self._current.version != "unknown"
|
|
119
|
+
and self._binding_matches(self.product_root, self._current))
|
|
120
|
+
|
|
121
|
+
def _operation(self) -> dict[str, Any] | None:
|
|
122
|
+
try:
|
|
123
|
+
operation = read_operation(self.product_root)
|
|
124
|
+
if operation is None:
|
|
125
|
+
return None
|
|
126
|
+
try:
|
|
127
|
+
descriptor = acquire_install_lock(self.product_root)
|
|
128
|
+
except BlockingIOError:
|
|
129
|
+
return operation
|
|
130
|
+
try:
|
|
131
|
+
operation = read_operation(self.product_root)
|
|
132
|
+
if operation is None:
|
|
133
|
+
return None
|
|
134
|
+
if terminal_phase(operation["phase"]):
|
|
135
|
+
try:
|
|
136
|
+
self._manager().cleanup(operation["operationId"])
|
|
137
|
+
except UpdateExecutorError:
|
|
138
|
+
return operation
|
|
139
|
+
return self._recover_ready_restart(operation)
|
|
140
|
+
if operation["phase"] == "accepted":
|
|
141
|
+
if (self._manager().is_active(operation["operationId"])
|
|
142
|
+
and self._clock() - operation["createdAt"] < START_HANDOFF_SECONDS):
|
|
143
|
+
return operation
|
|
144
|
+
operation = advance_operation(self.product_root, operation["operationId"],
|
|
145
|
+
"recovery_required", "worker_lost")
|
|
146
|
+
try:
|
|
147
|
+
self._manager().cleanup(operation["operationId"])
|
|
148
|
+
except UpdateExecutorError:
|
|
149
|
+
pass
|
|
150
|
+
return operation
|
|
151
|
+
finally:
|
|
152
|
+
os.close(descriptor)
|
|
153
|
+
except (UpdateProtocolError, OSError, UpdateExecutorError) as error:
|
|
154
|
+
raise UpdateError("update_state_unavailable") from error
|
|
155
|
+
|
|
156
|
+
def _recover_ready_restart(self, operation: dict[str, Any]) -> dict[str, Any]:
|
|
157
|
+
if (operation.get("schema") == 3
|
|
158
|
+
and operation["phase"] == "recovery_required"
|
|
159
|
+
and operation["code"] in {"restart_failed", "worker_lost"}
|
|
160
|
+
and operation["operationId"] == self._startup_restart_id
|
|
161
|
+
and self._service_ready and self._restart_supported()
|
|
162
|
+
and operation["target"]["installationId"] == self._current.identity):
|
|
163
|
+
return advance_operation(self.product_root, operation["operationId"],
|
|
164
|
+
"recovered", "service_ready")
|
|
165
|
+
return operation
|
|
166
|
+
|
|
167
|
+
def _status(self) -> dict[str, Any]:
|
|
168
|
+
operation = self._operation()
|
|
169
|
+
busy = operation is not None and (not terminal_phase(operation["phase"])
|
|
170
|
+
or operation["phase"] == "recovery_required")
|
|
171
|
+
supported = self._restart_supported()
|
|
172
|
+
return {"current": self._current.as_dict(), "supported": False,
|
|
173
|
+
"restartSupported": supported, "restartAvailable": supported and not busy,
|
|
174
|
+
"latest": None, "available": False, "operation": operation,
|
|
175
|
+
"checkingErrorCode": None, "checkedAt": None}
|
|
176
|
+
|
|
177
|
+
def _restart(self, *, installation_id: str) -> dict[str, Any]:
|
|
178
|
+
if not self._restart_supported():
|
|
179
|
+
raise UpdateError("restart_unsupported")
|
|
180
|
+
if installation_id != self._current.identity:
|
|
181
|
+
raise UpdateError("update_installation_changed")
|
|
182
|
+
try:
|
|
183
|
+
descriptor = acquire_install_lock(self.product_root)
|
|
184
|
+
except BlockingIOError as error:
|
|
185
|
+
raise UpdateError("update_busy") from error
|
|
186
|
+
except (OSError, UpdateProtocolError) as error:
|
|
187
|
+
raise UpdateError("update_lock_unavailable") from error
|
|
188
|
+
try:
|
|
189
|
+
if not self._restart_supported():
|
|
190
|
+
raise UpdateError("update_installation_changed")
|
|
191
|
+
previous = read_operation(self.product_root)
|
|
192
|
+
if previous is not None:
|
|
193
|
+
if not terminal_phase(previous["phase"]):
|
|
194
|
+
raise UpdateError("update_already_submitted")
|
|
195
|
+
if previous["phase"] == "recovery_required":
|
|
196
|
+
raise UpdateError("update_recovery_required")
|
|
197
|
+
try:
|
|
198
|
+
self._manager().cleanup(previous["operationId"])
|
|
199
|
+
except UpdateExecutorError as error:
|
|
200
|
+
raise UpdateError("update_cleanup_unavailable") from error
|
|
201
|
+
operation = new_cli_restart_operation(self._current.version, self._current.identity)
|
|
202
|
+
write_operation(self.product_root, operation)
|
|
203
|
+
try:
|
|
204
|
+
self._manager().launch(operation["operationId"], self._current.python)
|
|
205
|
+
except UpdateDispatchUnknown:
|
|
206
|
+
return operation # May have launched; never submit twice.
|
|
207
|
+
except (OSError, RuntimeError):
|
|
208
|
+
return advance_operation(self.product_root, operation["operationId"],
|
|
209
|
+
"failed", "dispatch_failed")
|
|
210
|
+
return operation
|
|
211
|
+
except (OSError, UpdateProtocolError) as error:
|
|
212
|
+
raise UpdateError("update_submission_unknown") from error
|
|
213
|
+
finally:
|
|
214
|
+
os.close(descriptor)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Image-token editing through the standard Markdown parser and renderer."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterator, Sequence
|
|
4
|
+
from typing import Any
|
|
5
|
+
from urllib.parse import urlsplit
|
|
6
|
+
|
|
7
|
+
from markdown_it import MarkdownIt
|
|
8
|
+
from markdown_it.token import Token
|
|
9
|
+
from mdformat.plugins import PARSER_EXTENSIONS
|
|
10
|
+
from mdformat.renderer import MDRenderer
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
_GFM = PARSER_EXTENSIONS["gfm"]
|
|
14
|
+
_MARKDOWN = MarkdownIt("commonmark", {
|
|
15
|
+
"mdformat": {"wrap": "keep", "number": True},
|
|
16
|
+
"parser_extension": [_GFM],
|
|
17
|
+
}, renderer_cls=MDRenderer)
|
|
18
|
+
_GFM.update_mdit(_MARKDOWN)
|
|
19
|
+
_DEFAULT_VALIDATE_LINK = _MARKDOWN.validateLink
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _validate_local_file_link(target: str) -> bool:
|
|
23
|
+
# The default rejects file: links. Permit local file URLs for image reads,
|
|
24
|
+
# but never network shares or the other schemes rejected by CommonMark.
|
|
25
|
+
try:
|
|
26
|
+
parts = urlsplit(target)
|
|
27
|
+
except ValueError:
|
|
28
|
+
return False
|
|
29
|
+
if parts.scheme == "file":
|
|
30
|
+
return not (parts.netloc or parts.query or parts.fragment)
|
|
31
|
+
return _DEFAULT_VALIDATE_LINK(target)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
_MARKDOWN.validateLink = _validate_local_file_link
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def parse_markdown(content: str) -> tuple[list[Token], dict[str, Any]]:
|
|
38
|
+
env: dict[str, Any] = {}
|
|
39
|
+
return _MARKDOWN.parse(content, env), env
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _walk_tokens(tokens: Sequence[Token]) -> Iterator[Token]:
|
|
43
|
+
for token in tokens:
|
|
44
|
+
yield token
|
|
45
|
+
# Alt text is not a separate image or link to be rewritten.
|
|
46
|
+
if token.type != "image" and token.children:
|
|
47
|
+
yield from _walk_tokens(token.children)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def image_tokens(tokens: Sequence[Token]) -> Iterator[Token]:
|
|
51
|
+
return (token for token in _walk_tokens(tokens) if token.type == "image")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def render_markdown(tokens: Sequence[Token], env: dict[str, Any]) -> str:
|
|
55
|
+
for token in _walk_tokens(tokens):
|
|
56
|
+
if token.type not in {"image", "link_open"}:
|
|
57
|
+
continue
|
|
58
|
+
if token.type == "image":
|
|
59
|
+
# mdformat 1.0 otherwise drops escaped/entity text in image alt.
|
|
60
|
+
token.children = [Token("text", "", 0, content=token.content)]
|
|
61
|
+
# Attributes are already decoded. Escape literal entities/backslashes
|
|
62
|
+
# for Markdown output; autolinks do not interpret character references.
|
|
63
|
+
uri_attr = "src" if token.type == "image" else "href"
|
|
64
|
+
if token.info != "auto" and (uri := token.attrGet(uri_attr)) is not None:
|
|
65
|
+
token.attrSet(uri_attr, uri.replace("&", "&"))
|
|
66
|
+
if (title := token.attrGet("title")) is not None:
|
|
67
|
+
title = title.replace("&", "&").replace("\\", "\\\\")
|
|
68
|
+
if token.type == "image": # Link rendering already escapes quotes.
|
|
69
|
+
title = title.replace('"', '\\"')
|
|
70
|
+
token.attrSet("title", title)
|
|
71
|
+
return _MARKDOWN.renderer.render(tokens, _MARKDOWN.options, env).rstrip("\n")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def replace_image_with_text(token: Token, detail: str) -> None:
|
|
75
|
+
label = " ".join(token.content.split())[:120] or "图片"
|
|
76
|
+
token.type, token.tag = "text", ""
|
|
77
|
+
token.content = f"〔图片:{label};{detail}〕"
|
|
78
|
+
token.attrs, token.children = {}, None
|