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,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
|
+
}
|