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.
Files changed (112) hide show
  1. netizen_cli/__init__.py +3 -0
  2. netizen_cli/__main__.py +4 -0
  3. netizen_cli/admin/__init__.py +1 -0
  4. netizen_cli/admin/auth.py +928 -0
  5. netizen_cli/admin/errors.py +9 -0
  6. netizen_cli/admin/port_config.py +115 -0
  7. netizen_cli/admin/presentation.py +257 -0
  8. netizen_cli/admin/queries.py +337 -0
  9. netizen_cli/admin/static/admin.css +260 -0
  10. netizen_cli/admin/static/admin.js +2898 -0
  11. netizen_cli/admin/static/index.html +327 -0
  12. netizen_cli/admin/transport.py +935 -0
  13. netizen_cli/admin/web.py +2717 -0
  14. netizen_cli/bindings.py +3215 -0
  15. netizen_cli/builtin_skills.py +93 -0
  16. netizen_cli/cards/__init__.py +105 -0
  17. netizen_cli/cards/callbacks.py +565 -0
  18. netizen_cli/cards/controls.py +2273 -0
  19. netizen_cli/cards/defaults.py +213 -0
  20. netizen_cli/cards/model_info.py +80 -0
  21. netizen_cli/cards/questions.py +220 -0
  22. netizen_cli/cards/reply.py +2247 -0
  23. netizen_cli/cards/scheduled.py +836 -0
  24. netizen_cli/channel/__init__.py +1 -0
  25. netizen_cli/channel/completion_mentions.py +60 -0
  26. netizen_cli/channel/input_preparation.py +644 -0
  27. netizen_cli/channel/messages.py +57 -0
  28. netizen_cli/channel/ports.py +52 -0
  29. netizen_cli/channel/question_inputs.py +51 -0
  30. netizen_cli/channel/reactions.py +293 -0
  31. netizen_cli/channel/reply_presenter.py +1505 -0
  32. netizen_cli/channel/topics.py +70 -0
  33. netizen_cli/channel_app.py +6593 -0
  34. netizen_cli/cli.py +287 -0
  35. netizen_cli/cli_data.py +536 -0
  36. netizen_cli/cli_packages.py +526 -0
  37. netizen_cli/cli_services.py +651 -0
  38. netizen_cli/cli_setup.py +242 -0
  39. netizen_cli/cli_update.py +303 -0
  40. netizen_cli/cli_update_restore.py +53 -0
  41. netizen_cli/cli_update_worker.py +333 -0
  42. netizen_cli/codex_runtime.py +7125 -0
  43. netizen_cli/completion_mention.py +16 -0
  44. netizen_cli/database_migrations.py +218 -0
  45. netizen_cli/defaults/__init__.py +5 -0
  46. netizen_cli/defaults/models.py +39 -0
  47. netizen_cli/defaults/service.py +232 -0
  48. netizen_cli/defaults/store.py +260 -0
  49. netizen_cli/deployment/__init__.py +1 -0
  50. netizen_cli/deployment/restart_worker.py +134 -0
  51. netizen_cli/deployment/update_executor.py +258 -0
  52. netizen_cli/deployment/update_protocol.py +281 -0
  53. netizen_cli/domain.py +416 -0
  54. netizen_cli/error_messages.py +124 -0
  55. netizen_cli/experience.py +531 -0
  56. netizen_cli/feishu_app_onboarding.py +187 -0
  57. netizen_cli/feishu_app_permissions.py +123 -0
  58. netizen_cli/git_status.py +63 -0
  59. netizen_cli/image_inputs.py +579 -0
  60. netizen_cli/instance.py +84 -0
  61. netizen_cli/lark_app.py +125 -0
  62. netizen_cli/main.py +903 -0
  63. netizen_cli/management/__init__.py +83 -0
  64. netizen_cli/management/blocking_io.py +352 -0
  65. netizen_cli/management/chat_labels.py +266 -0
  66. netizen_cli/management/coordination.py +32 -0
  67. netizen_cli/management/service.py +2187 -0
  68. netizen_cli/management/updates.py +214 -0
  69. netizen_cli/markdown_images.py +78 -0
  70. netizen_cli/message_content.py +786 -0
  71. netizen_cli/message_history.py +643 -0
  72. netizen_cli/message_preparation.py +60 -0
  73. netizen_cli/message_projection.py +923 -0
  74. netizen_cli/migrations/__init__.py +1 -0
  75. netizen_cli/migrations/schema.py +103 -0
  76. netizen_cli/migrations/v14.py +438 -0
  77. netizen_cli/model_settings.py +269 -0
  78. netizen_cli/package_resources.py +22 -0
  79. netizen_cli/projects.py +327 -0
  80. netizen_cli/prompt_projection.py +327 -0
  81. netizen_cli/quoted_context.py +312 -0
  82. netizen_cli/resources/config.example.yaml +35 -0
  83. netizen_cli/resources/skills/netizen-lark/SKILL.md +64 -0
  84. netizen_cli/resources/skills/netizen-user-guide/SKILL.md +37 -0
  85. netizen_cli/resources/skills/netizen-user-guide/references/user-guide.md +842 -0
  86. netizen_cli/result_images.py +123 -0
  87. netizen_cli/runtime/__init__.py +1 -0
  88. netizen_cli/runtime/contracts.py +792 -0
  89. netizen_cli/runtime/name_writes.py +67 -0
  90. netizen_cli/runtime/thread_naming.py +451 -0
  91. netizen_cli/schedules/__init__.py +1 -0
  92. netizen_cli/schedules/mcp.py +535 -0
  93. netizen_cli/schedules/models.py +394 -0
  94. netizen_cli/schedules/scheduler.py +374 -0
  95. netizen_cli/schedules/service.py +766 -0
  96. netizen_cli/schedules/store.py +771 -0
  97. netizen_cli/sdk_gap_adapter.py +1151 -0
  98. netizen_cli/service_launcher.py +583 -0
  99. netizen_cli/session_settings.py +126 -0
  100. netizen_cli/settings.py +216 -0
  101. netizen_cli/skill_references.py +40 -0
  102. netizen_cli/terminal_cleanup.py +155 -0
  103. netizen_cli/turn_activity.py +688 -0
  104. netizen_cli/turn_files.py +812 -0
  105. netizen_cli/turn_patch_children.py +254 -0
  106. netizen_cli/turn_plan_observer.py +315 -0
  107. netizen_cli/user_questions.py +106 -0
  108. netizen_cli-0.10.0.dist-info/METADATA +18 -0
  109. netizen_cli-0.10.0.dist-info/RECORD +112 -0
  110. netizen_cli-0.10.0.dist-info/WHEEL +5 -0
  111. netizen_cli-0.10.0.dist-info/entry_points.txt +2 -0
  112. 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("&", "&amp;"))
66
+ if (title := token.attrGet("title")) is not None:
67
+ title = title.replace("&", "&amp;").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