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,123 @@
|
|
|
1
|
+
"""Best-effort previews of the local images explicitly referenced in a Result."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
import stat
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Literal, Protocol
|
|
12
|
+
from urllib.parse import unquote, urlsplit
|
|
13
|
+
|
|
14
|
+
from lark_channel import MediaSource
|
|
15
|
+
|
|
16
|
+
from .markdown_images import image_tokens, parse_markdown, render_markdown, replace_image_with_text
|
|
17
|
+
from .turn_files import is_supported_image_header
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
_MAX_IMAGES = 20
|
|
21
|
+
_MAX_IMAGE_BYTES = 10 * 1024 * 1024
|
|
22
|
+
_MAX_TOTAL_BYTES = 50 * 1024 * 1024
|
|
23
|
+
_PREVIEW_TIMEOUT_SECONDS = 8.0
|
|
24
|
+
_UPLOAD_TIMEOUT_SECONDS = 4.0
|
|
25
|
+
_IMAGE_KEY = re.compile(r"img_[A-Za-z0-9_-]{1,256}\Z")
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ImageUploadChannel(Protocol):
|
|
30
|
+
async def upload_media(self, source: MediaSource, *, kind: Literal["image"]) -> str: ...
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
async def prepare_result_images(
|
|
34
|
+
channel: ImageUploadChannel,
|
|
35
|
+
content: str,
|
|
36
|
+
*,
|
|
37
|
+
cwd: Path | None,
|
|
38
|
+
) -> str:
|
|
39
|
+
"""Read the referenced images independently of the Files module.
|
|
40
|
+
|
|
41
|
+
Uploaded keys live only in the returned Result string and its existing
|
|
42
|
+
self-contained page callbacks. Neither rendering nor pagination uploads.
|
|
43
|
+
"""
|
|
44
|
+
if "![" not in content:
|
|
45
|
+
return content
|
|
46
|
+
tokens, env = parse_markdown(content)
|
|
47
|
+
uploaded: dict[Path, str | None] = {}
|
|
48
|
+
total_bytes = 0
|
|
49
|
+
deadline = asyncio.get_running_loop().time() + _PREVIEW_TIMEOUT_SECONDS
|
|
50
|
+
changed = False
|
|
51
|
+
for token in image_tokens(tokens):
|
|
52
|
+
target = token.attrGet("src") or ""
|
|
53
|
+
if _IMAGE_KEY.fullmatch(target):
|
|
54
|
+
continue
|
|
55
|
+
parts = urlsplit(target)
|
|
56
|
+
if parts.scheme not in {"", "file"} or parts.netloc:
|
|
57
|
+
continue # Remote/data images are not fetched or rewritten.
|
|
58
|
+
changed = True
|
|
59
|
+
path = _local_path(target, cwd)
|
|
60
|
+
if path is not None:
|
|
61
|
+
if path not in uploaded and len(uploaded) < _MAX_IMAGES:
|
|
62
|
+
uploaded[path] = None
|
|
63
|
+
remaining = deadline - asyncio.get_running_loop().time()
|
|
64
|
+
if remaining > 0 and total_bytes < _MAX_TOTAL_BYTES:
|
|
65
|
+
read_limit = min(_MAX_IMAGE_BYTES, _MAX_TOTAL_BYTES - total_bytes)
|
|
66
|
+
# Reserve before starting the worker: failed or timed-out reads
|
|
67
|
+
# may have consumed bytes even when no body reaches this task.
|
|
68
|
+
total_bytes += read_limit
|
|
69
|
+
try:
|
|
70
|
+
async with asyncio.timeout(min(remaining, _UPLOAD_TIMEOUT_SECONDS)):
|
|
71
|
+
body = await asyncio.to_thread(
|
|
72
|
+
_read_image, path, read_limit,
|
|
73
|
+
)
|
|
74
|
+
total_bytes -= read_limit - len(body)
|
|
75
|
+
key = await channel.upload_media(
|
|
76
|
+
MediaSource(kind="buffer", buffer=body), kind="image",
|
|
77
|
+
)
|
|
78
|
+
if not isinstance(key, str) or not _IMAGE_KEY.fullmatch(key):
|
|
79
|
+
raise ValueError("invalid uploaded image key")
|
|
80
|
+
uploaded[path] = key
|
|
81
|
+
except asyncio.CancelledError:
|
|
82
|
+
raise
|
|
83
|
+
except Exception:
|
|
84
|
+
# Do not expose local paths or transport/credential details.
|
|
85
|
+
logger.warning("inline result image preview unavailable")
|
|
86
|
+
key = uploaded.get(path)
|
|
87
|
+
if key is not None:
|
|
88
|
+
token.attrSet("src", key)
|
|
89
|
+
continue
|
|
90
|
+
replace_image_with_text(token, "预览暂不可用:图片不存在、无法读取或上传失败")
|
|
91
|
+
return render_markdown(tokens, env) if changed else content
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _local_path(target: str, cwd: Path | None) -> Path | None:
|
|
95
|
+
try:
|
|
96
|
+
parsed = urlsplit(target)
|
|
97
|
+
if parsed.scheme == "file" and not parsed.netloc and not parsed.query and not parsed.fragment:
|
|
98
|
+
target = parsed.path
|
|
99
|
+
elif parsed.scheme or parsed.netloc:
|
|
100
|
+
return None
|
|
101
|
+
path = Path(unquote(target, errors="strict")).expanduser()
|
|
102
|
+
if not path.is_absolute():
|
|
103
|
+
if cwd is None:
|
|
104
|
+
return None
|
|
105
|
+
path = cwd / path
|
|
106
|
+
return path.resolve()
|
|
107
|
+
except (OSError, RuntimeError, ValueError, UnicodeError):
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _read_image(path: Path, limit: int) -> bytes:
|
|
112
|
+
# Nonblocking open rejects a file replaced by a FIFO without hanging a worker.
|
|
113
|
+
fd = os.open(path, os.O_RDONLY | os.O_NONBLOCK | os.O_NOFOLLOW)
|
|
114
|
+
with os.fdopen(fd, "rb", buffering=0) as stream:
|
|
115
|
+
metadata = os.fstat(stream.fileno())
|
|
116
|
+
if not stat.S_ISREG(metadata.st_mode) or not 0 < metadata.st_size <= limit:
|
|
117
|
+
raise ValueError("image is unavailable or exceeds preview limits")
|
|
118
|
+
body = stream.read(limit)
|
|
119
|
+
if len(body) != metadata.st_size or os.fstat(stream.fileno()).st_size != metadata.st_size:
|
|
120
|
+
raise ValueError("image changed while being read")
|
|
121
|
+
if not is_supported_image_header(body[:12]):
|
|
122
|
+
raise ValueError("unsupported or oversized image")
|
|
123
|
+
return body
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Public Runtime contracts; execution remains owned by CodexRuntime."""
|