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,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."""