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,9 @@
1
+ """HTTP errors shared by Admin request parsing and routing."""
2
+
3
+
4
+ class AdminWebError(RuntimeError):
5
+ def __init__(self, status: int, code: str, message: str) -> None:
6
+ super().__init__(message)
7
+ self.status = status
8
+ self.code = code
9
+ self.message = message
@@ -0,0 +1,115 @@
1
+ """Narrow atomic port writes for explicit installation and first startup."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import contextlib
6
+ import os
7
+ import stat
8
+ import tempfile
9
+ from dataclasses import dataclass, field
10
+ from pathlib import Path
11
+
12
+ import yaml
13
+
14
+
15
+ class AdminPortConfigurationError(ValueError):
16
+ pass
17
+
18
+
19
+ @dataclass(frozen=True, slots=True)
20
+ class ConfigFileSnapshot:
21
+ path: Path
22
+ content: bytes = field(repr=False)
23
+ identity: tuple[int, ...]
24
+
25
+ @classmethod
26
+ def read(cls, path: Path) -> "ConfigFileSnapshot":
27
+ if not stat.S_ISREG(path.lstat().st_mode):
28
+ raise AdminPortConfigurationError(f"configuration must be a regular file: {path}")
29
+ descriptor = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_CLOEXEC)
30
+ with os.fdopen(descriptor, "rb") as source:
31
+ before = os.fstat(source.fileno())
32
+ if not stat.S_ISREG(before.st_mode):
33
+ raise AdminPortConfigurationError(f"configuration must be a regular file: {path}")
34
+ content = source.read()
35
+ after = os.fstat(source.fileno())
36
+ identity = _identity(before)
37
+ if identity != _identity(after) or identity != _identity(path.lstat()):
38
+ raise AdminPortConfigurationError(f"configuration changed while reading: {path}")
39
+ return cls(path, content, identity)
40
+
41
+ def require_unchanged(self) -> None:
42
+ if self != self.read(self.path):
43
+ raise AdminPortConfigurationError(
44
+ f"configuration changed since startup; retry with the current configuration: {self.path}"
45
+ )
46
+
47
+
48
+ def _identity(metadata: os.stat_result) -> tuple[int, ...]:
49
+ return (
50
+ metadata.st_dev, metadata.st_ino, metadata.st_mode, metadata.st_size,
51
+ metadata.st_mtime_ns, metadata.st_ctime_ns,
52
+ )
53
+
54
+
55
+ def persist_admin_port(snapshot: ConfigFileSnapshot, port: int) -> None:
56
+ """Preserve all YAML values and refuse a detected concurrent human edit.
57
+
58
+ The caller retains its listening socket and instance lifetime lock. This
59
+ deliberately does not acquire the installer lock (the installer may still
60
+ be waiting for this startup). Only the previously absent port is added.
61
+ """
62
+
63
+ _validate_port(port)
64
+ loaded = yaml.safe_load(snapshot.content)
65
+ if not isinstance(loaded, dict):
66
+ raise AdminPortConfigurationError("configuration root must be a mapping")
67
+ values = loaded.setdefault("adminWeb", {})
68
+ if not isinstance(values, dict) or "port" in values or values.get("enabled", True) is not True:
69
+ raise AdminPortConfigurationError("Admin port allocation requires enabled Admin and absent port")
70
+ # YAML aliases may share this mapping with another key. Updating Admin
71
+ # must not mutate those unrelated values through Python object identity.
72
+ values = dict(values)
73
+ loaded["adminWeb"] = values
74
+ values["port"] = port
75
+ _write_configuration(snapshot, loaded)
76
+
77
+
78
+ def set_admin_port(path: Path, port: int) -> None:
79
+ """Apply an explicit installer choice, even if Admin is currently disabled."""
80
+
81
+ _validate_port(port)
82
+ snapshot = ConfigFileSnapshot.read(path)
83
+ loaded = yaml.safe_load(snapshot.content)
84
+ if not isinstance(loaded, dict):
85
+ raise AdminPortConfigurationError("configuration root must be a mapping")
86
+ values = loaded.setdefault("adminWeb", {})
87
+ if not isinstance(values, dict):
88
+ raise AdminPortConfigurationError("adminWeb must be a mapping")
89
+ values = dict(values)
90
+ loaded["adminWeb"] = values
91
+ values["port"] = port
92
+ _write_configuration(snapshot, loaded)
93
+
94
+
95
+ def _validate_port(port: int) -> None:
96
+ if isinstance(port, bool) or not isinstance(port, int) or not 1 <= port <= 65535:
97
+ raise AdminPortConfigurationError("Admin port must be from 1 to 65535")
98
+
99
+
100
+ def _write_configuration(snapshot: ConfigFileSnapshot, loaded: dict) -> None:
101
+ content = yaml.safe_dump(loaded, allow_unicode=True, sort_keys=False).encode("utf-8")
102
+ snapshot.require_unchanged()
103
+ descriptor, temporary_name = tempfile.mkstemp(prefix=".admin-port-", dir=snapshot.path.parent)
104
+ temporary = Path(temporary_name)
105
+ try:
106
+ with os.fdopen(descriptor, "wb") as output:
107
+ os.fchmod(output.fileno(), 0o600)
108
+ output.write(content)
109
+ output.flush()
110
+ os.fsync(output.fileno())
111
+ snapshot.require_unchanged()
112
+ os.replace(temporary, snapshot.path)
113
+ finally:
114
+ with contextlib.suppress(FileNotFoundError):
115
+ temporary.unlink()
@@ -0,0 +1,257 @@
1
+ """Pure Admin response projections over shared management facts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import asdict, is_dataclass
6
+ from datetime import datetime
7
+ from enum import Enum
8
+ from typing import Any
9
+ from urllib.parse import urlencode
10
+ from zoneinfo import ZoneInfo
11
+
12
+ from ..bindings import BindingTurnSettings
13
+ from ..runtime.contracts import (
14
+ ReleaseDisposition,
15
+ StopDisposition,
16
+ )
17
+ from ..management import BindingStatusProjection, ChatLabel
18
+
19
+
20
+ def _release_check_message(code: str | None) -> str | None:
21
+ if code is None:
22
+ return None
23
+ return {
24
+ "release_rate_limited": "GitHub 更新查询已触发限流,请稍后重试。",
25
+ "release_access_denied": (
26
+ "更新查询被拒绝(HTTP 403)。请检查服务所在机器的网络访问策略或代理配置;"
27
+ "若持续出现,请联系维护者。"
28
+ ),
29
+ "release_not_found": (
30
+ "未找到可访问的官方发布版本(HTTP 404)。请稍后重试;"
31
+ "若持续出现,请联系维护者检查仓库与 Release 状态。"
32
+ ),
33
+ "release_service_unavailable": (
34
+ "更新服务暂时异常(HTTP 5xx)。请稍后重新检查。"
35
+ ),
36
+ "release_http_error": (
37
+ "更新查询收到异常 HTTP 响应。请稍后重试;"
38
+ "若持续出现,请联系维护者检查更新接口或网络代理。"
39
+ ),
40
+ "release_check_timeout": (
41
+ "查询 GitHub 更新服务超时。请稍后重试;"
42
+ "若持续出现,请检查服务所在机器的网络或代理配置。"
43
+ ),
44
+ "release_network_error": (
45
+ "访问 GitHub 更新服务时发生网络错误。请检查服务所在机器的网络、代理和 HTTPS 证书配置后再试。"
46
+ ),
47
+ "release_invalid_response": (
48
+ "官方更新信息格式异常,或发布资产不完整、未通过校验。"
49
+ "请稍后重试;若持续出现,请联系维护者检查 Release。"
50
+ ),
51
+ }.get(code, "检查更新失败,原因暂未识别。请稍后重试;若持续出现,请联系维护者。")
52
+
53
+
54
+ def _chat_open_url(chat: ChatLabel) -> str:
55
+ if chat.chat_mode == "p2p" and chat.p2p_target_open_id is not None:
56
+ query = urlencode({"openId": chat.p2p_target_open_id})
57
+ else:
58
+ query = urlencode({"openChatId": chat.chat_id})
59
+ return f"https://applink.feishu.cn/client/chat/open?{query}"
60
+
61
+
62
+ def _topic_open_url(chat_id: str, topic_id: str) -> str:
63
+ # Match the official Lark CLI's desktop/mobile ID spellings and root position.
64
+ query = urlencode({
65
+ "open_chat_id": chat_id,
66
+ "open_thread_id": topic_id,
67
+ "openchatid": chat_id,
68
+ "openthreadid": topic_id,
69
+ "thread_position": -1,
70
+ })
71
+ return f"https://applink.feishu.cn/client/thread/open?{query}"
72
+
73
+
74
+ def _schedule_plan_json(plan: dict[str, Any], chat: ChatLabel) -> dict[str, Any]:
75
+ rule = plan.get("schedule")
76
+ local_at = None
77
+ local_end_at = None
78
+ if rule and rule["kind"] == "once":
79
+ local_at = datetime.fromisoformat(rule["at"]).astimezone(
80
+ ZoneInfo(rule["timezone"]),
81
+ ).replace(tzinfo=None).isoformat(timespec="minutes")
82
+ if rule and rule.get("end_at"):
83
+ local_end_at = datetime.fromisoformat(rule["end_at"]).astimezone(
84
+ ZoneInfo(rule["timezone"]),
85
+ ).replace(tzinfo=None).isoformat(timespec="minutes")
86
+ return {**plan, "once_local_at": local_at, "end_local_at": local_end_at, "chat": {
87
+ "chatId": chat.chat_id,
88
+ "chatLabel": chat.display_name,
89
+ "chatLabelResolved": chat.resolved,
90
+ "chatMode": chat.chat_mode,
91
+ "chatType": chat.chat_type,
92
+ "chatOpenUrl": _chat_open_url(chat),
93
+ }}
94
+
95
+
96
+ def _jsonable(value: object) -> object:
97
+ if value is None or isinstance(value, (str, int, float, bool)):
98
+ return value
99
+ if isinstance(value, Enum):
100
+ return value.value
101
+ if is_dataclass(value):
102
+ return {
103
+ key: _jsonable(item)
104
+ for key, item in asdict(value).items()
105
+ }
106
+ if isinstance(value, dict):
107
+ return {str(key): _jsonable(item) for key, item in value.items()}
108
+ if isinstance(value, (tuple, list)):
109
+ return [_jsonable(item) for item in value]
110
+ raise TypeError(f"unsupported JSON projection: {type(value).__name__}")
111
+
112
+
113
+ def _settings_json(settings: BindingTurnSettings | None) -> dict[str, str] | None:
114
+ if settings is None:
115
+ return None
116
+ return {
117
+ "modelId": settings.model_id,
118
+ "effortId": settings.effort_id,
119
+ "serviceTierId": settings.service_tier_id,
120
+ }
121
+
122
+
123
+ def _runtime_binding_json(status: BindingStatusProjection) -> dict[str, object]:
124
+ snapshot = status.snapshot
125
+ turn = snapshot.turn
126
+ goal = snapshot.goal
127
+ lifecycle = snapshot.lifecycle
128
+ subscription = snapshot.subscription
129
+ usage = snapshot.context_window_usage
130
+ return {
131
+ "bindingId": snapshot.binding_id,
132
+ "activityRevision": snapshot.activity_revision,
133
+ "primaryStatus": status.primary_status,
134
+ "primaryStatusResolution": status.primary_status_resolution.value,
135
+ "subscriptionState": (
136
+ status.subscription_state.value
137
+ if status.subscription_state is not None
138
+ else None
139
+ ),
140
+ "turn": (
141
+ {
142
+ "threadId": turn.thread_id,
143
+ "turnId": turn.turn_id,
144
+ "state": turn.state.value,
145
+ }
146
+ if turn is not None
147
+ else None
148
+ ),
149
+ "goal": (
150
+ {
151
+ "threadId": goal.thread_id,
152
+ "logicalTurnId": goal.logical_turn_id,
153
+ "state": goal.state.value,
154
+ }
155
+ if goal is not None
156
+ else None
157
+ ),
158
+ "compacting": snapshot.compacting,
159
+ "lifecycle": (
160
+ {
161
+ "threadId": lifecycle.thread_id,
162
+ "state": lifecycle.state.value,
163
+ }
164
+ if lifecycle is not None
165
+ else None
166
+ ),
167
+ "subscription": (
168
+ {
169
+ "threadId": subscription.thread_id,
170
+ "state": subscription.state.value,
171
+ "releaseInSeconds": subscription.release_in_seconds,
172
+ }
173
+ if subscription is not None
174
+ else None
175
+ ),
176
+ "contextWindow": (
177
+ {
178
+ "usedTokens": usage.used_tokens,
179
+ "contextWindowTokens": usage.context_window_tokens,
180
+ }
181
+ if usage is not None
182
+ else None
183
+ ),
184
+ }
185
+
186
+
187
+ def _stop_disposition_message(disposition: StopDisposition) -> str:
188
+ return {
189
+ StopDisposition.NOT_RUNNING: "该会话当前没有运行任务。",
190
+ StopDisposition.REQUESTED: (
191
+ "已请求中断 exact Codex Turn;确认终态前仍会显示为停止中。"
192
+ ),
193
+ StopDisposition.STOPPING: (
194
+ "该会话正在停止;已再次尝试完成中断与终端清理。"
195
+ ),
196
+ StopDisposition.COMPACTING: (
197
+ "该会话正在压缩,当前没有已验证的安全取消能力。"
198
+ ),
199
+ StopDisposition.GOAL_REQUESTED: (
200
+ "已请求暂停 Goal 并中断当前物理 Turn。"
201
+ ),
202
+ StopDisposition.GOAL_STOPPING: "该 Goal 正在暂停。",
203
+ StopDisposition.EXTERNAL_GOAL: (
204
+ "这是外部 active Goal,当前无法安全重挂并暂停。"
205
+ ),
206
+ }[disposition]
207
+
208
+
209
+ def _release_disposition_message(disposition: ReleaseDisposition) -> str:
210
+ return {
211
+ ReleaseDisposition.NOT_MATERIALIZED: (
212
+ "该会话尚未物化,没有原生 Thread 订阅可释放。"
213
+ ),
214
+ ReleaseDisposition.NOT_SUBSCRIBED: (
215
+ "本进程当前没有该 Thread 的订阅;Binding 与原生历史均保留。"
216
+ ),
217
+ ReleaseDisposition.RELEASED: (
218
+ "已取消本进程对该 Thread 的订阅;Binding 与原生历史均保留,"
219
+ "下次消息仍会 resume 同一 Thread。"
220
+ ),
221
+ }[disposition]
222
+
223
+
224
+ def _runtime_side_json(snapshot: Any) -> dict[str, object]:
225
+ return {
226
+ "sideId": snapshot.side_id,
227
+ "parentBindingId": snapshot.parent_binding_id,
228
+ "threadId": snapshot.thread_id,
229
+ "state": snapshot.state.value,
230
+ "turnId": snapshot.turn_id,
231
+ "turnState": (
232
+ snapshot.turn_state.value if snapshot.turn_state is not None else None
233
+ ),
234
+ }
235
+
236
+
237
+ def _project_result(request_id: str, project: Any) -> dict[str, object]:
238
+ return {
239
+ "requestId": request_id,
240
+ "alias": project.alias,
241
+ "cwd": str(project.cwd),
242
+ "enabled": project.enabled,
243
+ "revision": project.revision,
244
+ }
245
+
246
+
247
+ def _binding_result(request_id: str, binding: Any) -> dict[str, object]:
248
+ return {
249
+ "requestId": request_id,
250
+ "bindingId": binding.id,
251
+ "scopeKey": binding.scope_key,
252
+ "current": binding.active,
253
+ "nativeThreadId": binding.native_thread_id,
254
+ "settingsRevision": binding.settings_revision,
255
+ "messageContextMode": binding.message_context_mode.value,
256
+ "contextRevision": binding.context_revision,
257
+ }