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,2273 @@
|
|
|
1
|
+
"""Management cards, forms, and strict Channel control decoding."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from collections.abc import Callable, Mapping
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from lark_channel import OutboundCard
|
|
11
|
+
|
|
12
|
+
from ..bindings import (
|
|
13
|
+
BindingTaskFeedback,
|
|
14
|
+
BindingTurnSettings,
|
|
15
|
+
SideTopicState,
|
|
16
|
+
)
|
|
17
|
+
from ..domain import (
|
|
18
|
+
ActiveState,
|
|
19
|
+
CardControlIntent,
|
|
20
|
+
CardControlName,
|
|
21
|
+
FeishuScope,
|
|
22
|
+
GoalStatus,
|
|
23
|
+
MentionContextMode,
|
|
24
|
+
ReplyCardGoalModule,
|
|
25
|
+
ReplyCardProjection,
|
|
26
|
+
SettingsSection,
|
|
27
|
+
SESSION_IDLE_STATE,
|
|
28
|
+
ScopeKind,
|
|
29
|
+
session_stop_available,
|
|
30
|
+
)
|
|
31
|
+
from ..model_settings import ModelCatalog, TurnModelSettings
|
|
32
|
+
from ..projects import Project
|
|
33
|
+
from ..session_settings import SessionSettings
|
|
34
|
+
from ..sdk_gap_adapter import GoalSnapshot
|
|
35
|
+
from .callbacks import (
|
|
36
|
+
ACTION_VERSION,
|
|
37
|
+
CardActionError,
|
|
38
|
+
SettingsCardActionError,
|
|
39
|
+
_binding_reference,
|
|
40
|
+
_bounded_string,
|
|
41
|
+
_builder,
|
|
42
|
+
_button_row,
|
|
43
|
+
_callback_button,
|
|
44
|
+
_config_model_reference,
|
|
45
|
+
_context_mode_reference,
|
|
46
|
+
_decode_binding_reference,
|
|
47
|
+
_decode_config_model_reference,
|
|
48
|
+
_decode_context_mode_reference,
|
|
49
|
+
_decode_goal_generation,
|
|
50
|
+
_decode_native_thread_reference,
|
|
51
|
+
_decode_new_model_reference,
|
|
52
|
+
_decode_project_mode_reference,
|
|
53
|
+
_decode_project_reference,
|
|
54
|
+
_decode_side_reference,
|
|
55
|
+
_decode_task_feedback_reference,
|
|
56
|
+
_decode_turn_reference,
|
|
57
|
+
_envelope,
|
|
58
|
+
_goal_generation,
|
|
59
|
+
_md_code,
|
|
60
|
+
_native_thread_reference,
|
|
61
|
+
_new_callback_nonce,
|
|
62
|
+
_new_model_reference,
|
|
63
|
+
_notice,
|
|
64
|
+
_plain,
|
|
65
|
+
_plain_text,
|
|
66
|
+
_project_mode_reference,
|
|
67
|
+
_project_reference,
|
|
68
|
+
_rename_name_field,
|
|
69
|
+
_repeatable_callback_button,
|
|
70
|
+
_required_string,
|
|
71
|
+
_scope_from_envelope,
|
|
72
|
+
_side_reference,
|
|
73
|
+
_task_feedback_reference,
|
|
74
|
+
_turn_reference,
|
|
75
|
+
)
|
|
76
|
+
from .reply import reply_card
|
|
77
|
+
from .model_info import with_model_details
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
SESSIONS_PAGE_SIZE = 10
|
|
81
|
+
MAX_THREAD_NAME_CHARS = 120
|
|
82
|
+
MAX_SETTING_ID_CHARS = 128
|
|
83
|
+
_RENAME_NAME_FIELD = re.compile(
|
|
84
|
+
r"rename_name_v1__([A-Za-z0-9][A-Za-z0-9-]{0,127})"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass(frozen=True, slots=True)
|
|
89
|
+
class ArchivedSessionCardItem:
|
|
90
|
+
binding_id: str
|
|
91
|
+
short_id: str
|
|
92
|
+
project_alias: str
|
|
93
|
+
native_thread_id: str
|
|
94
|
+
title: str
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@dataclass(frozen=True, slots=True)
|
|
98
|
+
class SessionCardItem:
|
|
99
|
+
binding_id: str
|
|
100
|
+
short_id: str
|
|
101
|
+
project_alias: str
|
|
102
|
+
native_thread_id: str | None
|
|
103
|
+
title: str
|
|
104
|
+
state: str
|
|
105
|
+
active: bool
|
|
106
|
+
activity_revision: int = 0
|
|
107
|
+
turn_id: str | None = None
|
|
108
|
+
catalog_unconfirmed: bool = False
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def settings_card(
|
|
112
|
+
*,
|
|
113
|
+
scope: FeishuScope,
|
|
114
|
+
projects: tuple[Project, ...],
|
|
115
|
+
project_root: str,
|
|
116
|
+
section: SettingsSection = SettingsSection.PROJECTS,
|
|
117
|
+
notice: str | None = None,
|
|
118
|
+
notice_is_error: bool = False,
|
|
119
|
+
) -> OutboundCard:
|
|
120
|
+
builder = _builder("Netizen 设置", _settings_section_label(section))
|
|
121
|
+
builder.raw(_settings_navigation(scope=scope, active=section))
|
|
122
|
+
builder.markdown(
|
|
123
|
+
"当前聊天的参与者均可管理项目;修改会影响使用这个机器人的其他聊天。"
|
|
124
|
+
)
|
|
125
|
+
if notice:
|
|
126
|
+
builder.raw(_notice(notice, error=notice_is_error))
|
|
127
|
+
|
|
128
|
+
if section is SettingsSection.PROJECTS:
|
|
129
|
+
_render_projects_settings(
|
|
130
|
+
builder,
|
|
131
|
+
scope=scope,
|
|
132
|
+
projects=projects,
|
|
133
|
+
project_root=project_root,
|
|
134
|
+
)
|
|
135
|
+
else: # pragma: no cover - enum and renderer must evolve together
|
|
136
|
+
raise ValueError(f"unsupported settings section: {section}")
|
|
137
|
+
|
|
138
|
+
return OutboundCard(card=builder.to_dict())
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _render_projects_settings(
|
|
142
|
+
builder: Any,
|
|
143
|
+
*,
|
|
144
|
+
scope: FeishuScope,
|
|
145
|
+
projects: tuple[Project, ...],
|
|
146
|
+
project_root: str,
|
|
147
|
+
) -> None:
|
|
148
|
+
builder.markdown(
|
|
149
|
+
"**项目工作目录**\n"
|
|
150
|
+
"项目是 Codex 实际工作的目录,使用这个机器人的会话共享这些项目。"
|
|
151
|
+
"可创建空目录,或登记机器人所在机器上的已有目录。"
|
|
152
|
+
)
|
|
153
|
+
builder.markdown("**管理项目**")
|
|
154
|
+
if projects:
|
|
155
|
+
builder.raw(_project_management_form(projects))
|
|
156
|
+
else:
|
|
157
|
+
builder.markdown("还没有项目,请在下方新增,然后发送 `/new` 创建会话。")
|
|
158
|
+
|
|
159
|
+
builder.raw(
|
|
160
|
+
_repeatable_callback_button(
|
|
161
|
+
label="刷新当前配置",
|
|
162
|
+
value=_envelope(
|
|
163
|
+
scope,
|
|
164
|
+
CardControlName.REFRESH_SETTINGS,
|
|
165
|
+
settings_section=SettingsSection.PROJECTS.value,
|
|
166
|
+
),
|
|
167
|
+
)
|
|
168
|
+
)
|
|
169
|
+
builder.divider()
|
|
170
|
+
builder.markdown("**新增项目**")
|
|
171
|
+
builder.raw(_project_create_form(project_root))
|
|
172
|
+
builder.markdown(
|
|
173
|
+
f"<font color='grey'>空项目默认创建在 `{_md_code(project_root)}`;"
|
|
174
|
+
"Netizen 从不删除项目目录。</font>"
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _settings_navigation(
|
|
179
|
+
*,
|
|
180
|
+
scope: FeishuScope,
|
|
181
|
+
active: SettingsSection,
|
|
182
|
+
) -> dict[str, Any]:
|
|
183
|
+
return _button_row(
|
|
184
|
+
*(
|
|
185
|
+
_repeatable_callback_button(
|
|
186
|
+
label=_settings_section_label(section),
|
|
187
|
+
value=_envelope(
|
|
188
|
+
scope,
|
|
189
|
+
CardControlName.OPEN_SETTINGS_SECTION,
|
|
190
|
+
settings_section=section.value,
|
|
191
|
+
),
|
|
192
|
+
style="primary_filled" if section is active else "default",
|
|
193
|
+
)
|
|
194
|
+
for section in SettingsSection
|
|
195
|
+
)
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _settings_section_label(section: SettingsSection) -> str:
|
|
200
|
+
if section is SettingsSection.PROJECTS:
|
|
201
|
+
return "项目"
|
|
202
|
+
raise ValueError(f"unsupported settings section: {section}")
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _project_management_form(projects: tuple[Project, ...]) -> dict[str, Any]:
|
|
206
|
+
return {
|
|
207
|
+
"tag": "form",
|
|
208
|
+
"name": "project_manage_v1",
|
|
209
|
+
"elements": [
|
|
210
|
+
{
|
|
211
|
+
"tag": "select_static",
|
|
212
|
+
"name": "project_manage_target",
|
|
213
|
+
"required": True,
|
|
214
|
+
"placeholder": _plain_text("选择项目"),
|
|
215
|
+
"options": [
|
|
216
|
+
{
|
|
217
|
+
"text": _plain_text(
|
|
218
|
+
f"{project.alias} · "
|
|
219
|
+
f"{'已启用' if project.enabled else '已停用'}"
|
|
220
|
+
),
|
|
221
|
+
"value": _project_reference(project),
|
|
222
|
+
}
|
|
223
|
+
for project in projects
|
|
224
|
+
],
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
"tag": "select_static",
|
|
228
|
+
"name": "project_manage_operation",
|
|
229
|
+
"required": True,
|
|
230
|
+
"placeholder": _plain_text("选择操作"),
|
|
231
|
+
"options": [
|
|
232
|
+
{"text": _plain_text("启用"), "value": "enable"},
|
|
233
|
+
{"text": _plain_text("停用"), "value": "disable"},
|
|
234
|
+
],
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
"tag": "button",
|
|
238
|
+
"name": "project_manage_submit_v1",
|
|
239
|
+
"text": _plain_text("应用到所选项目"),
|
|
240
|
+
"type": "primary",
|
|
241
|
+
"width": "fill",
|
|
242
|
+
"form_action_type": "submit",
|
|
243
|
+
"confirm": {
|
|
244
|
+
"title": _plain_text("确认修改项目?"),
|
|
245
|
+
"text": _plain_text(
|
|
246
|
+
"状态会立即更新;停用只阻止创建新会话,已有会话仍可继续。"
|
|
247
|
+
),
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
],
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _project_create_form(project_root: str) -> dict[str, Any]:
|
|
255
|
+
callback_nonce = _new_callback_nonce()
|
|
256
|
+
return {
|
|
257
|
+
"tag": "form",
|
|
258
|
+
"name": "project_create_v1",
|
|
259
|
+
"elements": [
|
|
260
|
+
{
|
|
261
|
+
"tag": "input",
|
|
262
|
+
"name": "project_alias",
|
|
263
|
+
"required": True,
|
|
264
|
+
"label": _plain_text("项目标识(小写字母或数字开头,可含 _ 和 -)"),
|
|
265
|
+
"placeholder": _plain_text("例如:demo_project"),
|
|
266
|
+
"max_length": 64,
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
"tag": "div",
|
|
270
|
+
"text": _plain_text("模式"),
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
"tag": "select_static",
|
|
274
|
+
"name": "project_mode",
|
|
275
|
+
"required": True,
|
|
276
|
+
"initial_option": _project_mode_reference(
|
|
277
|
+
"create",
|
|
278
|
+
callback_nonce,
|
|
279
|
+
),
|
|
280
|
+
"options": [
|
|
281
|
+
{
|
|
282
|
+
"text": _plain_text("创建空目录"),
|
|
283
|
+
"value": _project_mode_reference(
|
|
284
|
+
"create",
|
|
285
|
+
callback_nonce,
|
|
286
|
+
),
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
"text": _plain_text("登记已有目录"),
|
|
290
|
+
"value": _project_mode_reference(
|
|
291
|
+
"existing",
|
|
292
|
+
callback_nonce,
|
|
293
|
+
),
|
|
294
|
+
},
|
|
295
|
+
],
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
"tag": "input",
|
|
299
|
+
"name": "project_path",
|
|
300
|
+
"label": _plain_text("绝对路径(创建模式可留空)"),
|
|
301
|
+
"placeholder": _plain_text(str(project_root)),
|
|
302
|
+
"max_length": 1000,
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
"tag": "button",
|
|
306
|
+
"name": "project_submit_v1",
|
|
307
|
+
"text": _plain_text("保存项目"),
|
|
308
|
+
"type": "primary_filled",
|
|
309
|
+
"width": "fill",
|
|
310
|
+
"form_action_type": "submit",
|
|
311
|
+
},
|
|
312
|
+
],
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _new_binding_form(
|
|
317
|
+
projects: tuple[Project, ...],
|
|
318
|
+
catalog: ModelCatalog | None,
|
|
319
|
+
*,
|
|
320
|
+
initial_project_alias: str | None,
|
|
321
|
+
allow_context_mode: bool,
|
|
322
|
+
message_context_mode: MentionContextMode,
|
|
323
|
+
task_feedback: BindingTaskFeedback,
|
|
324
|
+
) -> dict[str, Any]:
|
|
325
|
+
initial_project = next(
|
|
326
|
+
(
|
|
327
|
+
project
|
|
328
|
+
for project in projects
|
|
329
|
+
if project.alias == initial_project_alias
|
|
330
|
+
),
|
|
331
|
+
None,
|
|
332
|
+
)
|
|
333
|
+
elements = [
|
|
334
|
+
_form_label("项目工作目录"),
|
|
335
|
+
_static_select(
|
|
336
|
+
name="new_project",
|
|
337
|
+
placeholder="选择项目",
|
|
338
|
+
options=tuple(
|
|
339
|
+
(
|
|
340
|
+
f"{project.alias} · {project.cwd}",
|
|
341
|
+
_project_reference(project),
|
|
342
|
+
)
|
|
343
|
+
for project in projects
|
|
344
|
+
),
|
|
345
|
+
initial_option=(
|
|
346
|
+
_project_reference(initial_project)
|
|
347
|
+
if initial_project is not None
|
|
348
|
+
else None
|
|
349
|
+
),
|
|
350
|
+
),
|
|
351
|
+
]
|
|
352
|
+
if catalog is None:
|
|
353
|
+
elements.extend(
|
|
354
|
+
[
|
|
355
|
+
_form_label("Model"),
|
|
356
|
+
_static_select(
|
|
357
|
+
name="new_model",
|
|
358
|
+
placeholder="选择 Model 来源",
|
|
359
|
+
options=(("继承 Codex", _new_model_reference(None)),),
|
|
360
|
+
initial_option=_new_model_reference(None),
|
|
361
|
+
),
|
|
362
|
+
]
|
|
363
|
+
)
|
|
364
|
+
else:
|
|
365
|
+
elements.extend(
|
|
366
|
+
_model_settings_form_elements(
|
|
367
|
+
prefix="new",
|
|
368
|
+
catalog=catalog,
|
|
369
|
+
model_value_encoder=_new_model_reference,
|
|
370
|
+
inherit_initial_when_unset=False,
|
|
371
|
+
)
|
|
372
|
+
)
|
|
373
|
+
if allow_context_mode:
|
|
374
|
+
elements.extend(
|
|
375
|
+
_context_mode_form_elements(
|
|
376
|
+
prefix="new",
|
|
377
|
+
initial_mode=message_context_mode,
|
|
378
|
+
)
|
|
379
|
+
)
|
|
380
|
+
elements.extend(
|
|
381
|
+
_task_feedback_form_elements(prefix="new", initial=task_feedback)
|
|
382
|
+
)
|
|
383
|
+
elements.append(
|
|
384
|
+
_form_submit_button(name="new_binding_submit_v6", label="新建会话")
|
|
385
|
+
)
|
|
386
|
+
return {
|
|
387
|
+
"tag": "form",
|
|
388
|
+
"name": "new_binding_v6",
|
|
389
|
+
"elements": elements,
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _binding_config_form(
|
|
394
|
+
*,
|
|
395
|
+
binding_id: str,
|
|
396
|
+
settings_revision: int,
|
|
397
|
+
context_revision: int,
|
|
398
|
+
feedback_revision: int,
|
|
399
|
+
turn_settings: BindingTurnSettings | None,
|
|
400
|
+
message_context_mode: MentionContextMode,
|
|
401
|
+
task_feedback: BindingTaskFeedback,
|
|
402
|
+
allow_context_mode: bool,
|
|
403
|
+
catalog: ModelCatalog | None,
|
|
404
|
+
) -> dict[str, Any]:
|
|
405
|
+
def model_reference(model_id: str | None) -> str:
|
|
406
|
+
return _config_model_reference(
|
|
407
|
+
binding_id=binding_id,
|
|
408
|
+
settings_revision=settings_revision,
|
|
409
|
+
context_revision=context_revision,
|
|
410
|
+
feedback_revision=feedback_revision,
|
|
411
|
+
model_id=model_id,
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
elements: list[dict[str, Any]] = []
|
|
415
|
+
if catalog is None:
|
|
416
|
+
elements.extend(
|
|
417
|
+
[
|
|
418
|
+
_form_label("Model"),
|
|
419
|
+
_static_select(
|
|
420
|
+
name="config_model",
|
|
421
|
+
placeholder="选择 Model 来源",
|
|
422
|
+
options=(("继承 Codex", model_reference(None)),),
|
|
423
|
+
initial_option=model_reference(None),
|
|
424
|
+
),
|
|
425
|
+
]
|
|
426
|
+
)
|
|
427
|
+
else:
|
|
428
|
+
elements.extend(
|
|
429
|
+
_model_settings_form_elements(
|
|
430
|
+
prefix="config",
|
|
431
|
+
catalog=catalog,
|
|
432
|
+
turn_settings=turn_settings,
|
|
433
|
+
model_value_encoder=model_reference,
|
|
434
|
+
inherit_initial_when_unset=True,
|
|
435
|
+
)
|
|
436
|
+
)
|
|
437
|
+
if allow_context_mode:
|
|
438
|
+
elements.extend(
|
|
439
|
+
_context_mode_form_elements(
|
|
440
|
+
prefix="config",
|
|
441
|
+
initial_mode=message_context_mode,
|
|
442
|
+
)
|
|
443
|
+
)
|
|
444
|
+
elements.extend(
|
|
445
|
+
_task_feedback_form_elements(prefix="config", initial=task_feedback)
|
|
446
|
+
)
|
|
447
|
+
elements.append(
|
|
448
|
+
_form_submit_button(
|
|
449
|
+
name="binding_config_submit_v6",
|
|
450
|
+
label="保存会话配置",
|
|
451
|
+
)
|
|
452
|
+
)
|
|
453
|
+
return {
|
|
454
|
+
"tag": "form",
|
|
455
|
+
"name": "binding_config_v6",
|
|
456
|
+
"elements": elements,
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def _model_settings_form_elements(
|
|
461
|
+
*,
|
|
462
|
+
prefix: str,
|
|
463
|
+
catalog: ModelCatalog,
|
|
464
|
+
turn_settings: BindingTurnSettings | None = None,
|
|
465
|
+
model_value_encoder: Callable[[str | None], str],
|
|
466
|
+
inherit_initial_when_unset: bool,
|
|
467
|
+
model_label: str = "Model",
|
|
468
|
+
) -> list[dict[str, Any]]:
|
|
469
|
+
default = catalog.default_model
|
|
470
|
+
model_id: str | None = (
|
|
471
|
+
None if inherit_initial_when_unset else default.id
|
|
472
|
+
)
|
|
473
|
+
effort_id = default.default_effort_id
|
|
474
|
+
service_tier_id = default.default_service_tier_id
|
|
475
|
+
if turn_settings is not None:
|
|
476
|
+
try:
|
|
477
|
+
catalog.resolve(
|
|
478
|
+
model_id=turn_settings.model_id,
|
|
479
|
+
effort_id=turn_settings.effort_id,
|
|
480
|
+
service_tier_id=turn_settings.service_tier_id,
|
|
481
|
+
)
|
|
482
|
+
except ValueError:
|
|
483
|
+
pass
|
|
484
|
+
else:
|
|
485
|
+
model_id = turn_settings.model_id
|
|
486
|
+
effort_id = turn_settings.effort_id
|
|
487
|
+
service_tier_id = turn_settings.service_tier_id
|
|
488
|
+
|
|
489
|
+
return [
|
|
490
|
+
_form_label(model_label),
|
|
491
|
+
_static_select(
|
|
492
|
+
name=f"{prefix}_model",
|
|
493
|
+
placeholder="选择 Model",
|
|
494
|
+
options=tuple(
|
|
495
|
+
[("继承 Codex", model_value_encoder(None))]
|
|
496
|
+
+ [
|
|
497
|
+
(
|
|
498
|
+
model.display_name
|
|
499
|
+
+ (" · 默认" if model.is_default else ""),
|
|
500
|
+
model_value_encoder(model.id),
|
|
501
|
+
)
|
|
502
|
+
for model in catalog.models
|
|
503
|
+
]
|
|
504
|
+
),
|
|
505
|
+
initial_option=model_value_encoder(model_id),
|
|
506
|
+
),
|
|
507
|
+
_form_label("Effort"),
|
|
508
|
+
_static_select(
|
|
509
|
+
name=f"{prefix}_effort",
|
|
510
|
+
placeholder="选择 Effort",
|
|
511
|
+
options=tuple(
|
|
512
|
+
(option.id, option.id) for option in catalog.effort_options
|
|
513
|
+
),
|
|
514
|
+
initial_option=effort_id,
|
|
515
|
+
),
|
|
516
|
+
_form_label("Speed"),
|
|
517
|
+
_static_select(
|
|
518
|
+
name=f"{prefix}_speed",
|
|
519
|
+
placeholder="选择 Speed",
|
|
520
|
+
options=tuple(
|
|
521
|
+
(option.name, option.id)
|
|
522
|
+
for option in catalog.service_tier_options
|
|
523
|
+
),
|
|
524
|
+
initial_option=service_tier_id,
|
|
525
|
+
),
|
|
526
|
+
]
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def context_mode_display(mode: MentionContextMode) -> str:
|
|
530
|
+
if mode is MentionContextMode.CATCH_UP:
|
|
531
|
+
return "自动带上期间的群聊讨论"
|
|
532
|
+
return "仅这条 @ 消息"
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def _context_mode_form_elements(
|
|
536
|
+
*,
|
|
537
|
+
prefix: str,
|
|
538
|
+
initial_mode: MentionContextMode,
|
|
539
|
+
) -> list[dict[str, Any]]:
|
|
540
|
+
return [
|
|
541
|
+
_form_label("@ 时读取的消息范围"),
|
|
542
|
+
_static_select(
|
|
543
|
+
name=f"{prefix}_context_mode",
|
|
544
|
+
placeholder="选择 @ 时读取的消息范围",
|
|
545
|
+
options=(
|
|
546
|
+
(
|
|
547
|
+
"仅这条 @ 消息(默认)",
|
|
548
|
+
_context_mode_reference(MentionContextMode.CURRENT_ONLY),
|
|
549
|
+
),
|
|
550
|
+
(
|
|
551
|
+
"自动带上期间的群聊讨论",
|
|
552
|
+
_context_mode_reference(MentionContextMode.CATCH_UP),
|
|
553
|
+
),
|
|
554
|
+
),
|
|
555
|
+
initial_option=_context_mode_reference(initial_mode),
|
|
556
|
+
),
|
|
557
|
+
_form_hint(
|
|
558
|
+
"机器人始终只响应 @ 它的消息。选择“自动带上”后,"
|
|
559
|
+
"两次 @ 之间群里其他成员未 @ 机器人的消息,"
|
|
560
|
+
"也会在下一次 @ 时被读取,作为背景交给 Codex。"
|
|
561
|
+
),
|
|
562
|
+
]
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def _task_feedback_form_elements(
|
|
566
|
+
*,
|
|
567
|
+
prefix: str,
|
|
568
|
+
initial: BindingTaskFeedback,
|
|
569
|
+
) -> list[dict[str, Any]]:
|
|
570
|
+
return [
|
|
571
|
+
_form_label("执行中表情闪烁"),
|
|
572
|
+
_static_select(
|
|
573
|
+
name=f"{prefix}_task_reactions",
|
|
574
|
+
placeholder="选择是否显示执行中表情闪烁",
|
|
575
|
+
options=(
|
|
576
|
+
("关闭(默认)", _task_feedback_reference(False)),
|
|
577
|
+
("开启", _task_feedback_reference(True)),
|
|
578
|
+
),
|
|
579
|
+
initial_option=_task_feedback_reference(
|
|
580
|
+
initial.reaction_pulse_enabled
|
|
581
|
+
),
|
|
582
|
+
),
|
|
583
|
+
_form_hint(
|
|
584
|
+
"任务接收、成功调整和结束时始终显示表情。"
|
|
585
|
+
"开启后,执行中还会间歇显示动态表情;"
|
|
586
|
+
"部分移动端可能将其显示为单独消息。"
|
|
587
|
+
),
|
|
588
|
+
_form_label("进度卡"),
|
|
589
|
+
_static_select(
|
|
590
|
+
name=f"{prefix}_progress_card",
|
|
591
|
+
placeholder="选择是否使用进度卡",
|
|
592
|
+
options=(
|
|
593
|
+
("关闭", _task_feedback_reference(False)),
|
|
594
|
+
("开启(默认)", _task_feedback_reference(True)),
|
|
595
|
+
),
|
|
596
|
+
initial_option=_task_feedback_reference(
|
|
597
|
+
initial.progress_card_enabled
|
|
598
|
+
),
|
|
599
|
+
),
|
|
600
|
+
_form_hint(
|
|
601
|
+
"开启后会逐步更新任务状态与清单;"
|
|
602
|
+
"完成后执行过程自动折叠。"
|
|
603
|
+
),
|
|
604
|
+
_form_label("结束时 @ 提醒"),
|
|
605
|
+
_static_select(
|
|
606
|
+
name=f"{prefix}_completion_mention",
|
|
607
|
+
placeholder="选择是否在任务结束时 @ 提醒",
|
|
608
|
+
options=(
|
|
609
|
+
("关闭", _task_feedback_reference(False)),
|
|
610
|
+
("开启(默认)", _task_feedback_reference(True)),
|
|
611
|
+
),
|
|
612
|
+
initial_option=_task_feedback_reference(initial.completion_mention_enabled),
|
|
613
|
+
),
|
|
614
|
+
_form_hint("任务完成或失败时 @ 本轮发起人;主动停止不提醒。定时任务自动首轮不提醒。"),
|
|
615
|
+
]
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
def session_settings_form_elements(
|
|
619
|
+
*,
|
|
620
|
+
prefix: str,
|
|
621
|
+
settings: SessionSettings,
|
|
622
|
+
catalog: ModelCatalog | None,
|
|
623
|
+
catalog_error: str | None = None,
|
|
624
|
+
allow_context_mode: bool,
|
|
625
|
+
) -> list[dict[str, Any]]:
|
|
626
|
+
"""Use ordinary controls while preserving unavailable explicit intent."""
|
|
627
|
+
usable_catalog = catalog
|
|
628
|
+
if catalog is not None:
|
|
629
|
+
try:
|
|
630
|
+
settings.validate_catalog(catalog)
|
|
631
|
+
except ValueError:
|
|
632
|
+
usable_catalog = None
|
|
633
|
+
catalog_error = "已有 Model / Effort / Speed 暂不可用,当前选择完整保留。"
|
|
634
|
+
elements: list[dict[str, Any]] = []
|
|
635
|
+
if usable_catalog is not None:
|
|
636
|
+
elements.extend(_model_settings_form_elements(
|
|
637
|
+
prefix=prefix, catalog=usable_catalog, turn_settings=settings.turn_settings,
|
|
638
|
+
model_value_encoder=_new_model_reference, inherit_initial_when_unset=True,
|
|
639
|
+
))
|
|
640
|
+
else:
|
|
641
|
+
elements.append(_notice(catalog_error or "模型目录暂不可用;可以保留原配置或明确选择继承 Codex。"))
|
|
642
|
+
options = [("继承 Codex", _new_model_reference(None))]
|
|
643
|
+
turn = settings.turn_settings
|
|
644
|
+
if turn is not None:
|
|
645
|
+
options.append(("保留已有显式配置", _new_model_reference(turn.model_id)))
|
|
646
|
+
elements.extend([
|
|
647
|
+
_form_label("Model"),
|
|
648
|
+
_static_select(name=f"{prefix}_model", placeholder="选择 Model 来源", options=tuple(options),
|
|
649
|
+
initial_option=_new_model_reference(turn.model_id if turn else None)),
|
|
650
|
+
])
|
|
651
|
+
if turn is not None:
|
|
652
|
+
for field, label, value in (("effort", "Effort", turn.effort_id), ("speed", "Speed", turn.service_tier_id)):
|
|
653
|
+
elements.extend([
|
|
654
|
+
_form_label(label),
|
|
655
|
+
_static_select(name=f"{prefix}_{field}", placeholder=label,
|
|
656
|
+
options=((value, value),), initial_option=value),
|
|
657
|
+
])
|
|
658
|
+
if allow_context_mode:
|
|
659
|
+
elements.extend(_context_mode_form_elements(prefix=prefix, initial_mode=settings.message_context_mode))
|
|
660
|
+
elements.extend(_task_feedback_form_elements(prefix=prefix, initial=settings.task_feedback))
|
|
661
|
+
return elements
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
def session_settings_summary(settings: SessionSettings, *, allow_context_mode: bool = True) -> str:
|
|
665
|
+
turn = settings.turn_settings
|
|
666
|
+
model = "Model:继承 Codex" if turn is None else (
|
|
667
|
+
f"Model:{turn.model_id}\nEffort:{turn.effort_id}\nSpeed:{turn.service_tier_id}"
|
|
668
|
+
)
|
|
669
|
+
context = "\n" + _context_mode_summary(settings.message_context_mode) if allow_context_mode else ""
|
|
670
|
+
return model + context + "\n" + _task_feedback_summary(settings.task_feedback)
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
def decode_session_settings_fields(
|
|
674
|
+
payload: Mapping[str, Any], *, prefix: str, model_id: str | None,
|
|
675
|
+
) -> SessionSettings:
|
|
676
|
+
"""Shared value validation for ordinary and scheduled session forms."""
|
|
677
|
+
context_field = f"{prefix}_context_mode"
|
|
678
|
+
context_mode = _decode_context_mode_reference(_required_string(
|
|
679
|
+
payload.get(context_field, _context_mode_reference(MentionContextMode.CURRENT_ONLY)), context_field,
|
|
680
|
+
))
|
|
681
|
+
feedback = BindingTaskFeedback(
|
|
682
|
+
_decode_task_feedback_reference(payload[f"{prefix}_task_reactions"], f"{prefix}_task_reactions"),
|
|
683
|
+
_decode_task_feedback_reference(payload[f"{prefix}_progress_card"], f"{prefix}_progress_card"),
|
|
684
|
+
_decode_task_feedback_reference(payload[f"{prefix}_completion_mention"], f"{prefix}_completion_mention"),
|
|
685
|
+
)
|
|
686
|
+
catalog_fields = {f"{prefix}_effort", f"{prefix}_speed"}
|
|
687
|
+
has_catalog_fields = catalog_fields.issubset(payload)
|
|
688
|
+
turn = None
|
|
689
|
+
if model_id is not None and not has_catalog_fields:
|
|
690
|
+
raise CardActionError("显式 Model 必须同时选择 Effort 与 Speed,请重新打开卡片。")
|
|
691
|
+
if has_catalog_fields:
|
|
692
|
+
effort_id = _bounded_string(payload[f"{prefix}_effort"], f"{prefix}_effort", max_chars=MAX_SETTING_ID_CHARS)
|
|
693
|
+
speed_id = _bounded_string(payload[f"{prefix}_speed"], f"{prefix}_speed", max_chars=MAX_SETTING_ID_CHARS)
|
|
694
|
+
if model_id is not None:
|
|
695
|
+
turn = BindingTurnSettings(model_id, effort_id, speed_id)
|
|
696
|
+
return SessionSettings(turn, feedback, context_mode)
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
def _require_completion_mention_field(payload: Mapping[str, Any], *, prefix: str) -> None:
|
|
700
|
+
if f"{prefix}_completion_mention" not in payload:
|
|
701
|
+
raise CardActionError("会话配置卡片已过期,请重新打开配置卡片。")
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
def decode_session_settings_form(payload: Mapping[str, Any], *, prefix: str) -> SessionSettings:
|
|
705
|
+
_require_completion_mention_field(payload, prefix=prefix)
|
|
706
|
+
fields = {f"{prefix}_{name}" for name in ("model", "task_reactions", "progress_card", "completion_mention")}
|
|
707
|
+
context = {f"{prefix}_context_mode"} if f"{prefix}_context_mode" in payload else set()
|
|
708
|
+
catalog = {f"{prefix}_effort", f"{prefix}_speed"}
|
|
709
|
+
if frozenset(payload) not in {frozenset(fields | context), frozenset(fields | context | catalog)}:
|
|
710
|
+
raise CardActionError("会话配置表单字段不完整或包含未知字段。")
|
|
711
|
+
model_id = _decode_new_model_reference(_required_string(payload[f"{prefix}_model"], f"{prefix}_model"))
|
|
712
|
+
return decode_session_settings_fields(payload, prefix=prefix, model_id=model_id)
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
def _form_label(label: str) -> dict[str, Any]:
|
|
716
|
+
return {"tag": "div", "text": _plain_text(label)}
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
def _form_hint(text: str) -> dict[str, Any]:
|
|
720
|
+
return {
|
|
721
|
+
"tag": "markdown",
|
|
722
|
+
"content": f"<font color='grey'>{text}</font>",
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
def _static_select(
|
|
727
|
+
*,
|
|
728
|
+
name: str,
|
|
729
|
+
placeholder: str,
|
|
730
|
+
options: tuple[tuple[str, str], ...],
|
|
731
|
+
initial_option: str | None,
|
|
732
|
+
) -> dict[str, Any]:
|
|
733
|
+
select = {
|
|
734
|
+
"tag": "select_static",
|
|
735
|
+
"name": name,
|
|
736
|
+
"required": True,
|
|
737
|
+
"placeholder": _plain_text(placeholder),
|
|
738
|
+
"options": [
|
|
739
|
+
{"text": _plain_text(label), "value": value}
|
|
740
|
+
for label, value in options
|
|
741
|
+
],
|
|
742
|
+
}
|
|
743
|
+
if initial_option is not None:
|
|
744
|
+
select["initial_option"] = initial_option
|
|
745
|
+
return select
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
def _form_submit_button(*, name: str, label: str) -> dict[str, Any]:
|
|
749
|
+
return {
|
|
750
|
+
"tag": "button",
|
|
751
|
+
"name": name,
|
|
752
|
+
"text": _plain_text(label),
|
|
753
|
+
"type": "primary_filled",
|
|
754
|
+
"width": "fill",
|
|
755
|
+
"form_action_type": "submit",
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
def new_binding_card(
|
|
760
|
+
*,
|
|
761
|
+
scope: FeishuScope,
|
|
762
|
+
projects: tuple[Project, ...],
|
|
763
|
+
initial_project_alias: str | None = None,
|
|
764
|
+
catalog: ModelCatalog | None = None,
|
|
765
|
+
catalog_error: str | None = None,
|
|
766
|
+
allow_context_mode: bool = True,
|
|
767
|
+
message_context_mode: MentionContextMode = MentionContextMode.CURRENT_ONLY,
|
|
768
|
+
task_feedback: BindingTaskFeedback | None = None,
|
|
769
|
+
) -> OutboundCard:
|
|
770
|
+
builder = _builder("新建会话", "选择项目,开始一次对话")
|
|
771
|
+
builder.markdown(
|
|
772
|
+
"项目是 Codex 实际工作的目录。选择项目并创建会话后,直接发送任务即可开始。"
|
|
773
|
+
"创建会话本身不会启动任务。"
|
|
774
|
+
)
|
|
775
|
+
if not projects:
|
|
776
|
+
builder.markdown(
|
|
777
|
+
"还没有已启用的项目。请先发送 `/settings` 创建、登记或启用项目,"
|
|
778
|
+
"再发送 `/new`。发送 `/help` 查看快速开始。"
|
|
779
|
+
)
|
|
780
|
+
else:
|
|
781
|
+
builder.markdown(
|
|
782
|
+
"不确定模型配置时,可选择“继承 Codex”;"
|
|
783
|
+
"也可选择 Model(模型)、Effort(思考强度)和 Speed(速度),"
|
|
784
|
+
"应用于这个会话后续的新任务。空闲时可通过 `/config` 调整。"
|
|
785
|
+
)
|
|
786
|
+
if catalog is None:
|
|
787
|
+
builder.raw(
|
|
788
|
+
_notice(
|
|
789
|
+
(catalog_error or "Model / Effort / Speed 暂不可用。")
|
|
790
|
+
+ " 当前仍可选择项目并继承 Codex 创建会话。",
|
|
791
|
+
error=True,
|
|
792
|
+
)
|
|
793
|
+
)
|
|
794
|
+
builder.raw(
|
|
795
|
+
_new_binding_form(
|
|
796
|
+
projects,
|
|
797
|
+
catalog,
|
|
798
|
+
initial_project_alias=initial_project_alias,
|
|
799
|
+
allow_context_mode=allow_context_mode,
|
|
800
|
+
message_context_mode=message_context_mode,
|
|
801
|
+
task_feedback=(
|
|
802
|
+
task_feedback or SessionSettings.new_defaults(catalog).task_feedback
|
|
803
|
+
),
|
|
804
|
+
)
|
|
805
|
+
)
|
|
806
|
+
return OutboundCard(card=with_model_details(builder.to_dict(), catalog if projects else None))
|
|
807
|
+
|
|
808
|
+
|
|
809
|
+
def config_card(
|
|
810
|
+
*,
|
|
811
|
+
scope: FeishuScope,
|
|
812
|
+
binding_id: str,
|
|
813
|
+
short_id: str,
|
|
814
|
+
project_alias: str,
|
|
815
|
+
settings_revision: int,
|
|
816
|
+
turn_settings: BindingTurnSettings | None,
|
|
817
|
+
catalog: ModelCatalog | None,
|
|
818
|
+
context_revision: int = 1,
|
|
819
|
+
feedback_revision: int = 1,
|
|
820
|
+
message_context_mode: MentionContextMode = MentionContextMode.CURRENT_ONLY,
|
|
821
|
+
task_feedback: BindingTaskFeedback | None = None,
|
|
822
|
+
allow_context_mode: bool = True,
|
|
823
|
+
catalog_error: str | None = None,
|
|
824
|
+
) -> OutboundCard:
|
|
825
|
+
builder = _builder("当前会话配置", f"{short_id} · {project_alias}")
|
|
826
|
+
builder.markdown(
|
|
827
|
+
"Model 可继承 Codex,也可显式选择 Model / Effort / Speed;"
|
|
828
|
+
"保存不会启动任务。Model 是模型,Effort 是思考强度,Speed 是速度。"
|
|
829
|
+
)
|
|
830
|
+
if catalog is None:
|
|
831
|
+
builder.raw(
|
|
832
|
+
_notice(
|
|
833
|
+
(catalog_error or "Model / Effort / Speed 暂不可用。")
|
|
834
|
+
+ " 当前仍可清除显式配置并改为继承 Codex。",
|
|
835
|
+
error=True,
|
|
836
|
+
)
|
|
837
|
+
)
|
|
838
|
+
elif turn_settings is not None:
|
|
839
|
+
try:
|
|
840
|
+
catalog.resolve(
|
|
841
|
+
model_id=turn_settings.model_id,
|
|
842
|
+
effort_id=turn_settings.effort_id,
|
|
843
|
+
service_tier_id=turn_settings.service_tier_id,
|
|
844
|
+
)
|
|
845
|
+
except ValueError:
|
|
846
|
+
builder.raw(
|
|
847
|
+
_notice(
|
|
848
|
+
"已保存的会话配置不再出现在当前模型目录中;"
|
|
849
|
+
"发送任务会明确失败并保留配置。请在下方重新选择三项配置。",
|
|
850
|
+
error=True,
|
|
851
|
+
)
|
|
852
|
+
)
|
|
853
|
+
builder.raw(
|
|
854
|
+
_binding_config_form(
|
|
855
|
+
binding_id=binding_id,
|
|
856
|
+
settings_revision=settings_revision,
|
|
857
|
+
context_revision=context_revision,
|
|
858
|
+
feedback_revision=feedback_revision,
|
|
859
|
+
turn_settings=turn_settings,
|
|
860
|
+
message_context_mode=message_context_mode,
|
|
861
|
+
task_feedback=task_feedback or BindingTaskFeedback(),
|
|
862
|
+
allow_context_mode=allow_context_mode,
|
|
863
|
+
catalog=catalog,
|
|
864
|
+
)
|
|
865
|
+
)
|
|
866
|
+
return OutboundCard(card=with_model_details(builder.to_dict(), catalog))
|
|
867
|
+
|
|
868
|
+
|
|
869
|
+
def rename_binding_card(
|
|
870
|
+
*,
|
|
871
|
+
scope: FeishuScope,
|
|
872
|
+
binding_id: str,
|
|
873
|
+
short_id: str,
|
|
874
|
+
project_alias: str,
|
|
875
|
+
current_title: str,
|
|
876
|
+
) -> OutboundCard:
|
|
877
|
+
builder = _builder("重命名当前会话", f"{short_id} · {project_alias}")
|
|
878
|
+
builder.markdown(
|
|
879
|
+
f"当前名称:`{_md_code(current_title)}`\n"
|
|
880
|
+
"名称直接写入原生 Codex Thread,并同步显示在 Codex App/CLI。"
|
|
881
|
+
)
|
|
882
|
+
builder.raw(
|
|
883
|
+
{
|
|
884
|
+
"tag": "form",
|
|
885
|
+
"name": "binding_rename_v1",
|
|
886
|
+
"elements": [
|
|
887
|
+
{
|
|
888
|
+
"tag": "input",
|
|
889
|
+
"name": _rename_name_field(binding_id),
|
|
890
|
+
"required": True,
|
|
891
|
+
"label": _plain_text("新名称"),
|
|
892
|
+
"placeholder": _plain_text("输入新的会话名称"),
|
|
893
|
+
"max_length": MAX_THREAD_NAME_CHARS,
|
|
894
|
+
},
|
|
895
|
+
_form_submit_button(
|
|
896
|
+
name="binding_rename_submit_v1",
|
|
897
|
+
label="保存新名称",
|
|
898
|
+
),
|
|
899
|
+
],
|
|
900
|
+
}
|
|
901
|
+
)
|
|
902
|
+
return OutboundCard(card=builder.to_dict())
|
|
903
|
+
|
|
904
|
+
|
|
905
|
+
def archive_binding_card(
|
|
906
|
+
*,
|
|
907
|
+
scope: FeishuScope,
|
|
908
|
+
binding_id: str,
|
|
909
|
+
short_id: str,
|
|
910
|
+
project_alias: str,
|
|
911
|
+
title: str,
|
|
912
|
+
) -> OutboundCard:
|
|
913
|
+
builder = _builder("归档当前会话", f"{short_id} · {project_alias}")
|
|
914
|
+
builder.markdown(
|
|
915
|
+
f"即将归档:`{_md_code(title)}`\n"
|
|
916
|
+
"Codex 会处理该 Thread 当前的原生活动后执行归档。"
|
|
917
|
+
"归档会同步影响 Codex App/CLI;历史不会删除,之后可以恢复。"
|
|
918
|
+
)
|
|
919
|
+
builder.raw(
|
|
920
|
+
_callback_button(
|
|
921
|
+
label="确认归档当前会话",
|
|
922
|
+
value=_envelope(
|
|
923
|
+
scope,
|
|
924
|
+
CardControlName.ARCHIVE_BINDING,
|
|
925
|
+
binding_id=_binding_reference(binding_id),
|
|
926
|
+
),
|
|
927
|
+
style="primary_filled",
|
|
928
|
+
confirm=(
|
|
929
|
+
"确认归档当前会话?",
|
|
930
|
+
"成功后当前会话会清空,后续消息不会自动切换到其他会话。",
|
|
931
|
+
),
|
|
932
|
+
)
|
|
933
|
+
)
|
|
934
|
+
return OutboundCard(card=builder.to_dict())
|
|
935
|
+
|
|
936
|
+
|
|
937
|
+
def delete_binding_card(
|
|
938
|
+
*,
|
|
939
|
+
scope: FeishuScope,
|
|
940
|
+
binding_id: str,
|
|
941
|
+
short_id: str,
|
|
942
|
+
project_alias: str,
|
|
943
|
+
title: str,
|
|
944
|
+
native_thread_id: str | None = None,
|
|
945
|
+
) -> OutboundCard:
|
|
946
|
+
builder = _builder(
|
|
947
|
+
"永久删除当前会话",
|
|
948
|
+
f"{short_id} · {project_alias}",
|
|
949
|
+
template="red",
|
|
950
|
+
)
|
|
951
|
+
if native_thread_id is None:
|
|
952
|
+
builder.markdown(
|
|
953
|
+
f"即将删除:`{_md_code(title)}`\n"
|
|
954
|
+
"该 Lazy 会话尚无原生历史;确认后只删除本地 Binding。"
|
|
955
|
+
)
|
|
956
|
+
confirm_body = "请再次确认:该本地会话映射将被永久删除。"
|
|
957
|
+
callback_extra: dict[str, object] = {}
|
|
958
|
+
else:
|
|
959
|
+
builder.markdown(
|
|
960
|
+
f"即将删除:`{_md_code(title)}`\n"
|
|
961
|
+
"Codex 会处理该 Thread 当前的原生活动。确认后将永久删除原生 "
|
|
962
|
+
"Codex Thread、其 spawned descendants 与本地 "
|
|
963
|
+
"Binding;Codex App/CLI 中的对应历史也会消失。"
|
|
964
|
+
)
|
|
965
|
+
confirm_body = (
|
|
966
|
+
"请再次确认:原生会话、派生会话与本地 Binding 都将永久删除,无法恢复。"
|
|
967
|
+
)
|
|
968
|
+
callback_extra = {
|
|
969
|
+
"expected_native_thread_id": _native_thread_reference(
|
|
970
|
+
native_thread_id
|
|
971
|
+
)
|
|
972
|
+
}
|
|
973
|
+
builder.raw(
|
|
974
|
+
_callback_button(
|
|
975
|
+
label="永久删除当前会话",
|
|
976
|
+
value=_envelope(
|
|
977
|
+
scope,
|
|
978
|
+
CardControlName.DELETE_BINDING,
|
|
979
|
+
binding_id=_binding_reference(binding_id),
|
|
980
|
+
**callback_extra,
|
|
981
|
+
),
|
|
982
|
+
confirm=(
|
|
983
|
+
"永久删除且无法恢复",
|
|
984
|
+
confirm_body,
|
|
985
|
+
),
|
|
986
|
+
)
|
|
987
|
+
)
|
|
988
|
+
return OutboundCard(card=builder.to_dict())
|
|
989
|
+
|
|
990
|
+
|
|
991
|
+
def sessions_delete_binding_card(
|
|
992
|
+
*,
|
|
993
|
+
scope: FeishuScope,
|
|
994
|
+
binding_id: str,
|
|
995
|
+
short_id: str,
|
|
996
|
+
project_alias: str,
|
|
997
|
+
title: str,
|
|
998
|
+
native_thread_id: str | None,
|
|
999
|
+
page: int,
|
|
1000
|
+
) -> OutboundCard:
|
|
1001
|
+
builder = _builder(
|
|
1002
|
+
"永久删除会话",
|
|
1003
|
+
f"{short_id} · {project_alias}",
|
|
1004
|
+
template="red",
|
|
1005
|
+
)
|
|
1006
|
+
if native_thread_id is None:
|
|
1007
|
+
builder.markdown(
|
|
1008
|
+
f"即将删除:`{_md_code(title)}`\n"
|
|
1009
|
+
"该 Lazy 会话尚无原生历史;确认后只永久删除本地 Binding。"
|
|
1010
|
+
)
|
|
1011
|
+
confirm_body = "请再次确认:该本地会话映射将被永久删除。"
|
|
1012
|
+
else:
|
|
1013
|
+
builder.markdown(
|
|
1014
|
+
f"即将删除:`{_md_code(title)}`\n"
|
|
1015
|
+
"确认后将永久删除原生 Codex Thread、其 spawned descendants 与本地 "
|
|
1016
|
+
"Binding;Codex App/CLI 中的对应历史也会消失。"
|
|
1017
|
+
)
|
|
1018
|
+
confirm_body = (
|
|
1019
|
+
"请再次确认:原生会话、派生会话与本地 Binding 都将永久删除,无法恢复。"
|
|
1020
|
+
)
|
|
1021
|
+
expected_native_thread_id = (
|
|
1022
|
+
_native_thread_reference(native_thread_id)
|
|
1023
|
+
if native_thread_id is not None
|
|
1024
|
+
else None
|
|
1025
|
+
)
|
|
1026
|
+
builder.raw(
|
|
1027
|
+
_button_row(
|
|
1028
|
+
_callback_button(
|
|
1029
|
+
label="永久删除此会话",
|
|
1030
|
+
value=_envelope(
|
|
1031
|
+
scope,
|
|
1032
|
+
CardControlName.DELETE_EXACT_BINDING,
|
|
1033
|
+
binding_id=_binding_reference(binding_id),
|
|
1034
|
+
expected_native_thread_id=expected_native_thread_id,
|
|
1035
|
+
page=page,
|
|
1036
|
+
),
|
|
1037
|
+
style="danger",
|
|
1038
|
+
confirm=("永久删除且无法恢复", confirm_body),
|
|
1039
|
+
),
|
|
1040
|
+
_repeatable_callback_button(
|
|
1041
|
+
label="返回会话列表",
|
|
1042
|
+
value=_sessions_page_envelope(scope=scope, target=page),
|
|
1043
|
+
),
|
|
1044
|
+
)
|
|
1045
|
+
)
|
|
1046
|
+
return OutboundCard(card=builder.to_dict())
|
|
1047
|
+
|
|
1048
|
+
|
|
1049
|
+
def archived_sessions_card(
|
|
1050
|
+
*,
|
|
1051
|
+
scope: FeishuScope,
|
|
1052
|
+
sessions: tuple[ArchivedSessionCardItem, ...],
|
|
1053
|
+
native_delete_available: bool,
|
|
1054
|
+
notice: str | None = None,
|
|
1055
|
+
notice_is_error: bool = False,
|
|
1056
|
+
unconfirmed_count: int = 0,
|
|
1057
|
+
) -> OutboundCard:
|
|
1058
|
+
builder = _builder("已归档会话", "恢复后自动切换")
|
|
1059
|
+
if notice:
|
|
1060
|
+
builder.raw(_notice(notice, error=notice_is_error))
|
|
1061
|
+
builder.markdown(
|
|
1062
|
+
"已归档会话不会出现在普通 `/sessions` 中。恢复不会修改历史或会话配置;"
|
|
1063
|
+
"删除会永久移除原生 Thread、其 spawned descendants 与本地 Binding。"
|
|
1064
|
+
)
|
|
1065
|
+
if not sessions:
|
|
1066
|
+
builder.markdown(
|
|
1067
|
+
("当前未确认到已归档会话。" if unconfirmed_count else "当前聊天或话题没有已归档会话。")
|
|
1068
|
+
+ "发送 `/sessions` 查看普通会话,"
|
|
1069
|
+
"或发送 `/new` 新建会话。"
|
|
1070
|
+
)
|
|
1071
|
+
return OutboundCard(card=builder.to_dict())
|
|
1072
|
+
for session in sessions:
|
|
1073
|
+
builder.raw(
|
|
1074
|
+
_archived_session_row(
|
|
1075
|
+
scope=scope,
|
|
1076
|
+
session=session,
|
|
1077
|
+
native_delete_available=native_delete_available,
|
|
1078
|
+
)
|
|
1079
|
+
)
|
|
1080
|
+
return OutboundCard(card=builder.to_dict())
|
|
1081
|
+
|
|
1082
|
+
|
|
1083
|
+
def archived_sessions_delete_binding_card(
|
|
1084
|
+
*,
|
|
1085
|
+
scope: FeishuScope,
|
|
1086
|
+
binding_id: str,
|
|
1087
|
+
short_id: str,
|
|
1088
|
+
project_alias: str,
|
|
1089
|
+
title: str,
|
|
1090
|
+
native_thread_id: str,
|
|
1091
|
+
) -> OutboundCard:
|
|
1092
|
+
builder = _builder(
|
|
1093
|
+
"永久删除已归档会话",
|
|
1094
|
+
f"{short_id} · {project_alias}",
|
|
1095
|
+
template="red",
|
|
1096
|
+
)
|
|
1097
|
+
builder.markdown(
|
|
1098
|
+
f"即将删除:`{_md_code(title)}`\n"
|
|
1099
|
+
"确认后将永久删除原生 Codex Thread、其 spawned descendants 与本地 "
|
|
1100
|
+
"Binding;Codex App/CLI 中的对应历史也会消失。"
|
|
1101
|
+
)
|
|
1102
|
+
builder.raw(
|
|
1103
|
+
_button_row(
|
|
1104
|
+
_callback_button(
|
|
1105
|
+
label="永久删除已归档会话",
|
|
1106
|
+
value=_envelope(
|
|
1107
|
+
scope,
|
|
1108
|
+
CardControlName.DELETE_ARCHIVED_BINDING,
|
|
1109
|
+
binding_id=_binding_reference(binding_id),
|
|
1110
|
+
expected_native_thread_id=_native_thread_reference(
|
|
1111
|
+
native_thread_id
|
|
1112
|
+
),
|
|
1113
|
+
),
|
|
1114
|
+
style="danger",
|
|
1115
|
+
confirm=(
|
|
1116
|
+
"永久删除且无法恢复",
|
|
1117
|
+
"原生会话、派生会话与本地 Binding 都将永久删除。",
|
|
1118
|
+
),
|
|
1119
|
+
),
|
|
1120
|
+
_repeatable_callback_button(
|
|
1121
|
+
label="返回归档列表",
|
|
1122
|
+
value=_envelope(
|
|
1123
|
+
scope,
|
|
1124
|
+
CardControlName.REFRESH_ARCHIVED_SESSIONS,
|
|
1125
|
+
),
|
|
1126
|
+
),
|
|
1127
|
+
)
|
|
1128
|
+
)
|
|
1129
|
+
return OutboundCard(card=builder.to_dict())
|
|
1130
|
+
|
|
1131
|
+
|
|
1132
|
+
def sessions_card(
|
|
1133
|
+
*,
|
|
1134
|
+
scope: FeishuScope,
|
|
1135
|
+
sessions: tuple[SessionCardItem, ...],
|
|
1136
|
+
native_delete_available: bool,
|
|
1137
|
+
total_count: int,
|
|
1138
|
+
active_binding_id: str | None,
|
|
1139
|
+
page: int = 0,
|
|
1140
|
+
unconfirmed_count: int = 0,
|
|
1141
|
+
notice: str | None = None,
|
|
1142
|
+
notice_is_error: bool = False,
|
|
1143
|
+
) -> OutboundCard:
|
|
1144
|
+
subtitle = (
|
|
1145
|
+
f"{total_count} 个会话(含 {unconfirmed_count} 个归档状态未确认)"
|
|
1146
|
+
if unconfirmed_count else f"{total_count} 个普通会话"
|
|
1147
|
+
)
|
|
1148
|
+
builder = _builder("会话", subtitle)
|
|
1149
|
+
if notice:
|
|
1150
|
+
builder.raw(_notice(notice, error=notice_is_error))
|
|
1151
|
+
builder.markdown(
|
|
1152
|
+
"切换不会停止其他会话正在运行的任务。"
|
|
1153
|
+
"归档与删除会直接委托 Codex 处理当前原生活动。归档保留历史,可从 "
|
|
1154
|
+
"`/sessions archived` 恢复;删除会级联移除派生会话,需进入红色确认卡。"
|
|
1155
|
+
)
|
|
1156
|
+
if not sessions:
|
|
1157
|
+
builder.markdown(
|
|
1158
|
+
"当前聊天或话题没有普通会话。发送 `/new` 新建会话,"
|
|
1159
|
+
"或发送 `/sessions archived` 查找已归档会话。"
|
|
1160
|
+
)
|
|
1161
|
+
return OutboundCard(card=builder.to_dict())
|
|
1162
|
+
|
|
1163
|
+
total_pages = max(
|
|
1164
|
+
1,
|
|
1165
|
+
(total_count + SESSIONS_PAGE_SIZE - 1) // SESSIONS_PAGE_SIZE,
|
|
1166
|
+
)
|
|
1167
|
+
|
|
1168
|
+
for session in sessions:
|
|
1169
|
+
builder.raw(
|
|
1170
|
+
_session_row(
|
|
1171
|
+
scope=scope,
|
|
1172
|
+
session=session,
|
|
1173
|
+
page=page,
|
|
1174
|
+
expected_active_binding_id=active_binding_id,
|
|
1175
|
+
native_delete_available=native_delete_available,
|
|
1176
|
+
)
|
|
1177
|
+
)
|
|
1178
|
+
|
|
1179
|
+
if total_pages > 1:
|
|
1180
|
+
builder.raw(
|
|
1181
|
+
_sessions_pagination(
|
|
1182
|
+
scope=scope,
|
|
1183
|
+
page=page,
|
|
1184
|
+
total_pages=total_pages,
|
|
1185
|
+
)
|
|
1186
|
+
)
|
|
1187
|
+
return OutboundCard(card=builder.to_dict())
|
|
1188
|
+
|
|
1189
|
+
|
|
1190
|
+
def _session_row(
|
|
1191
|
+
*,
|
|
1192
|
+
scope: FeishuScope,
|
|
1193
|
+
session: SessionCardItem,
|
|
1194
|
+
page: int,
|
|
1195
|
+
expected_active_binding_id: str | None,
|
|
1196
|
+
native_delete_available: bool,
|
|
1197
|
+
) -> dict[str, Any]:
|
|
1198
|
+
expected_turn_id = (
|
|
1199
|
+
_turn_reference(session.turn_id) if session.turn_id is not None else None
|
|
1200
|
+
)
|
|
1201
|
+
native = (
|
|
1202
|
+
session.native_thread_id[:8]
|
|
1203
|
+
if session.native_thread_id is not None
|
|
1204
|
+
else "pending"
|
|
1205
|
+
)
|
|
1206
|
+
marker_text = "● 当前" if session.active else "○"
|
|
1207
|
+
text = (
|
|
1208
|
+
f"{marker_text} {session.title}\n"
|
|
1209
|
+
f"会话:{session.short_id} · Project:{session.project_alias} · "
|
|
1210
|
+
f"Native:{native} · 状态:{session.state}"
|
|
1211
|
+
)
|
|
1212
|
+
if session.catalog_unconfirmed:
|
|
1213
|
+
text += " · 归档状态:未确认"
|
|
1214
|
+
controls: list[dict[str, Any]] = []
|
|
1215
|
+
if not session.active:
|
|
1216
|
+
controls.append(
|
|
1217
|
+
_repeatable_callback_button(
|
|
1218
|
+
label="设为当前",
|
|
1219
|
+
value=_envelope(
|
|
1220
|
+
scope,
|
|
1221
|
+
CardControlName.ACTIVATE_BINDING,
|
|
1222
|
+
binding_id=_binding_reference(session.binding_id),
|
|
1223
|
+
),
|
|
1224
|
+
style="primary_filled",
|
|
1225
|
+
)
|
|
1226
|
+
)
|
|
1227
|
+
if session.native_thread_id is not None:
|
|
1228
|
+
controls.append(
|
|
1229
|
+
_callback_button(
|
|
1230
|
+
label="归档",
|
|
1231
|
+
value=_envelope(
|
|
1232
|
+
scope,
|
|
1233
|
+
CardControlName.ARCHIVE_EXACT_BINDING,
|
|
1234
|
+
binding_id=_binding_reference(session.binding_id),
|
|
1235
|
+
page=page,
|
|
1236
|
+
),
|
|
1237
|
+
confirm=(
|
|
1238
|
+
"确认归档此会话?",
|
|
1239
|
+
(
|
|
1240
|
+
f"将归档会话 {session.short_id}({session.title})。"
|
|
1241
|
+
"历史不会删除,之后可以恢复。"
|
|
1242
|
+
),
|
|
1243
|
+
),
|
|
1244
|
+
)
|
|
1245
|
+
)
|
|
1246
|
+
if (
|
|
1247
|
+
(
|
|
1248
|
+
session.native_thread_id is None
|
|
1249
|
+
and session.state == SESSION_IDLE_STATE
|
|
1250
|
+
)
|
|
1251
|
+
or (
|
|
1252
|
+
session.native_thread_id is not None
|
|
1253
|
+
and native_delete_available
|
|
1254
|
+
)
|
|
1255
|
+
):
|
|
1256
|
+
controls.append(
|
|
1257
|
+
_repeatable_callback_button(
|
|
1258
|
+
label="删除",
|
|
1259
|
+
value=_envelope(
|
|
1260
|
+
scope,
|
|
1261
|
+
CardControlName.PREPARE_EXACT_DELETE_BINDING,
|
|
1262
|
+
binding_id=_binding_reference(session.binding_id),
|
|
1263
|
+
expected_native_thread_id=(
|
|
1264
|
+
_native_thread_reference(session.native_thread_id)
|
|
1265
|
+
if session.native_thread_id is not None
|
|
1266
|
+
else None
|
|
1267
|
+
),
|
|
1268
|
+
page=page,
|
|
1269
|
+
),
|
|
1270
|
+
style="danger",
|
|
1271
|
+
)
|
|
1272
|
+
)
|
|
1273
|
+
if session_stop_available(session.state):
|
|
1274
|
+
controls.append(
|
|
1275
|
+
_callback_button(
|
|
1276
|
+
label="停止",
|
|
1277
|
+
value=_envelope(
|
|
1278
|
+
scope,
|
|
1279
|
+
CardControlName.STOP_EXACT_BINDING,
|
|
1280
|
+
binding_id=_binding_reference(session.binding_id),
|
|
1281
|
+
expected_active_binding_id=(
|
|
1282
|
+
_binding_reference(expected_active_binding_id)
|
|
1283
|
+
if expected_active_binding_id is not None
|
|
1284
|
+
else None
|
|
1285
|
+
),
|
|
1286
|
+
expected_activity_revision=session.activity_revision,
|
|
1287
|
+
expected_turn_id=expected_turn_id,
|
|
1288
|
+
page=page,
|
|
1289
|
+
),
|
|
1290
|
+
)
|
|
1291
|
+
)
|
|
1292
|
+
if (
|
|
1293
|
+
session.state == ActiveState.OBSERVATION_UNAVAILABLE.value
|
|
1294
|
+
and session.turn_id is not None
|
|
1295
|
+
):
|
|
1296
|
+
controls.append(
|
|
1297
|
+
_repeatable_callback_button(
|
|
1298
|
+
label="重新检查",
|
|
1299
|
+
value=_envelope(
|
|
1300
|
+
scope,
|
|
1301
|
+
CardControlName.RECHECK_EXACT_TURN,
|
|
1302
|
+
binding_id=_binding_reference(session.binding_id),
|
|
1303
|
+
expected_active_binding_id=(
|
|
1304
|
+
_binding_reference(expected_active_binding_id)
|
|
1305
|
+
if expected_active_binding_id is not None
|
|
1306
|
+
else None
|
|
1307
|
+
),
|
|
1308
|
+
expected_activity_revision=session.activity_revision,
|
|
1309
|
+
expected_turn_id=expected_turn_id,
|
|
1310
|
+
page=page,
|
|
1311
|
+
),
|
|
1312
|
+
)
|
|
1313
|
+
)
|
|
1314
|
+
columns = [
|
|
1315
|
+
{
|
|
1316
|
+
"tag": "column",
|
|
1317
|
+
"width": "weighted",
|
|
1318
|
+
"weight": 5,
|
|
1319
|
+
"padding": "8px",
|
|
1320
|
+
"elements": [_plain(text)],
|
|
1321
|
+
},
|
|
1322
|
+
]
|
|
1323
|
+
if controls:
|
|
1324
|
+
columns.extend(
|
|
1325
|
+
{
|
|
1326
|
+
"tag": "column",
|
|
1327
|
+
"width": "auto",
|
|
1328
|
+
"vertical_align": "center",
|
|
1329
|
+
"padding": "8px",
|
|
1330
|
+
"elements": [control],
|
|
1331
|
+
}
|
|
1332
|
+
for control in controls
|
|
1333
|
+
)
|
|
1334
|
+
background = "blue-50" if session.active else "grey-50"
|
|
1335
|
+
return {
|
|
1336
|
+
"tag": "column_set",
|
|
1337
|
+
"flex_mode": "none",
|
|
1338
|
+
"background_style": background,
|
|
1339
|
+
"margin": "0 0 8px 0",
|
|
1340
|
+
"columns": columns,
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
|
|
1344
|
+
def _sessions_pagination(
|
|
1345
|
+
*,
|
|
1346
|
+
scope: FeishuScope,
|
|
1347
|
+
page: int,
|
|
1348
|
+
total_pages: int,
|
|
1349
|
+
) -> dict[str, Any]:
|
|
1350
|
+
columns: list[dict[str, Any]] = [
|
|
1351
|
+
{
|
|
1352
|
+
"tag": "column",
|
|
1353
|
+
"width": "weighted",
|
|
1354
|
+
"weight": 1,
|
|
1355
|
+
"vertical_align": "center",
|
|
1356
|
+
"elements": [
|
|
1357
|
+
{
|
|
1358
|
+
"tag": "markdown",
|
|
1359
|
+
"content": f"<font color='grey'>第 {page + 1}/{total_pages} 页</font>",
|
|
1360
|
+
}
|
|
1361
|
+
],
|
|
1362
|
+
}
|
|
1363
|
+
]
|
|
1364
|
+
if page > 0:
|
|
1365
|
+
columns.append(
|
|
1366
|
+
{
|
|
1367
|
+
"tag": "column",
|
|
1368
|
+
"width": "auto",
|
|
1369
|
+
"elements": [
|
|
1370
|
+
_repeatable_callback_button(
|
|
1371
|
+
label="上一页",
|
|
1372
|
+
value=_sessions_page_envelope(
|
|
1373
|
+
scope=scope,
|
|
1374
|
+
target=page - 1,
|
|
1375
|
+
),
|
|
1376
|
+
)
|
|
1377
|
+
],
|
|
1378
|
+
}
|
|
1379
|
+
)
|
|
1380
|
+
if page + 1 < total_pages:
|
|
1381
|
+
columns.append(
|
|
1382
|
+
{
|
|
1383
|
+
"tag": "column",
|
|
1384
|
+
"width": "auto",
|
|
1385
|
+
"elements": [
|
|
1386
|
+
_repeatable_callback_button(
|
|
1387
|
+
label="下一页",
|
|
1388
|
+
value=_sessions_page_envelope(
|
|
1389
|
+
scope=scope,
|
|
1390
|
+
target=page + 1,
|
|
1391
|
+
),
|
|
1392
|
+
)
|
|
1393
|
+
],
|
|
1394
|
+
}
|
|
1395
|
+
)
|
|
1396
|
+
return {"tag": "column_set", "flex_mode": "none", "columns": columns}
|
|
1397
|
+
|
|
1398
|
+
|
|
1399
|
+
def _sessions_page_envelope(*, scope: FeishuScope, target: int) -> dict[str, Any]:
|
|
1400
|
+
return _envelope(
|
|
1401
|
+
scope,
|
|
1402
|
+
CardControlName.SESSIONS_PAGE,
|
|
1403
|
+
page=target,
|
|
1404
|
+
)
|
|
1405
|
+
|
|
1406
|
+
|
|
1407
|
+
def binding_lifecycle_result_card(
|
|
1408
|
+
*,
|
|
1409
|
+
title: str,
|
|
1410
|
+
short_id: str,
|
|
1411
|
+
project_alias: str,
|
|
1412
|
+
message: str,
|
|
1413
|
+
) -> OutboundCard:
|
|
1414
|
+
builder = _builder(
|
|
1415
|
+
title,
|
|
1416
|
+
f"{short_id} · {project_alias}",
|
|
1417
|
+
template="green",
|
|
1418
|
+
)
|
|
1419
|
+
builder.markdown(message)
|
|
1420
|
+
return OutboundCard(card=builder.to_dict())
|
|
1421
|
+
|
|
1422
|
+
|
|
1423
|
+
def goal_card(
|
|
1424
|
+
*,
|
|
1425
|
+
scope: FeishuScope,
|
|
1426
|
+
binding_id: str,
|
|
1427
|
+
short_id: str,
|
|
1428
|
+
project_alias: str,
|
|
1429
|
+
goal: GoalSnapshot | None,
|
|
1430
|
+
runtime_state: str | None = None,
|
|
1431
|
+
notice: str | None = None,
|
|
1432
|
+
notice_is_error: bool = False,
|
|
1433
|
+
goal_generation: str | None = None,
|
|
1434
|
+
) -> OutboundCard:
|
|
1435
|
+
generation = (
|
|
1436
|
+
goal_generation
|
|
1437
|
+
if goal_generation is not None
|
|
1438
|
+
else (None if goal is None else _goal_generation(goal))
|
|
1439
|
+
)
|
|
1440
|
+
return reply_card(
|
|
1441
|
+
ReplyCardProjection(
|
|
1442
|
+
scope=scope,
|
|
1443
|
+
goal=ReplyCardGoalModule(
|
|
1444
|
+
binding_id=binding_id,
|
|
1445
|
+
short_id=short_id,
|
|
1446
|
+
project_alias=project_alias,
|
|
1447
|
+
goal_generation=generation,
|
|
1448
|
+
status=None if goal is None else goal.status.value,
|
|
1449
|
+
runtime_state=runtime_state,
|
|
1450
|
+
objective=None if goal is None else goal.objective,
|
|
1451
|
+
token_budget=None if goal is None else goal.token_budget,
|
|
1452
|
+
tokens_used=0 if goal is None else goal.tokens_used,
|
|
1453
|
+
notice=notice,
|
|
1454
|
+
notice_is_error=notice_is_error,
|
|
1455
|
+
),
|
|
1456
|
+
)
|
|
1457
|
+
)
|
|
1458
|
+
|
|
1459
|
+
|
|
1460
|
+
def side_topic_card(
|
|
1461
|
+
*,
|
|
1462
|
+
scope: FeishuScope,
|
|
1463
|
+
side_id: str,
|
|
1464
|
+
parent_short_id: str,
|
|
1465
|
+
creator_id: str,
|
|
1466
|
+
created_at: str,
|
|
1467
|
+
state: SideTopicState,
|
|
1468
|
+
notice: str | None = None,
|
|
1469
|
+
notice_is_error: bool = False,
|
|
1470
|
+
) -> OutboundCard:
|
|
1471
|
+
labels = {
|
|
1472
|
+
SideTopicState.CREATING: "创建中",
|
|
1473
|
+
SideTopicState.OPEN: "可用",
|
|
1474
|
+
SideTopicState.CLOSED: "已结束",
|
|
1475
|
+
SideTopicState.EXPIRED: "已过期",
|
|
1476
|
+
SideTopicState.FAILED: "创建或清理失败",
|
|
1477
|
+
}
|
|
1478
|
+
templates = {
|
|
1479
|
+
SideTopicState.CREATING: "blue",
|
|
1480
|
+
SideTopicState.OPEN: "green",
|
|
1481
|
+
SideTopicState.CLOSED: "grey",
|
|
1482
|
+
SideTopicState.EXPIRED: "grey",
|
|
1483
|
+
SideTopicState.FAILED: "red",
|
|
1484
|
+
}
|
|
1485
|
+
builder = _builder(
|
|
1486
|
+
"Codex Side",
|
|
1487
|
+
f"{labels[state]} · Parent {parent_short_id}",
|
|
1488
|
+
template=templates[state],
|
|
1489
|
+
)
|
|
1490
|
+
if notice:
|
|
1491
|
+
builder.raw(_notice(notice, error=notice_is_error))
|
|
1492
|
+
builder.markdown(
|
|
1493
|
+
f"**状态**:`{state.value}`\n"
|
|
1494
|
+
f"**Parent 会话**:`{_md_code(parent_short_id)}`\n"
|
|
1495
|
+
f"**创建者**:`{_md_code(creator_id)}`\n"
|
|
1496
|
+
f"**创建时间**:`{_md_code(created_at)}`\n\n"
|
|
1497
|
+
"Side 使用独立的 ephemeral Codex Thread,并与 Parent 共享 Project cwd;"
|
|
1498
|
+
"文件改动彼此可见。服务重启后本 Side 会过期,历史消息仅保留在飞书中。"
|
|
1499
|
+
)
|
|
1500
|
+
if state is SideTopicState.OPEN and scope.kind is not ScopeKind.TOPIC:
|
|
1501
|
+
raise ValueError("an open Side card requires its exact Topic scope")
|
|
1502
|
+
if (
|
|
1503
|
+
state in {SideTopicState.CREATING, SideTopicState.OPEN}
|
|
1504
|
+
and scope.kind is ScopeKind.TOPIC
|
|
1505
|
+
):
|
|
1506
|
+
creating = state is SideTopicState.CREATING
|
|
1507
|
+
builder.raw(
|
|
1508
|
+
_button_row(
|
|
1509
|
+
_repeatable_callback_button(
|
|
1510
|
+
label="取消 Side" if creating else "结束 Side",
|
|
1511
|
+
value=_envelope(
|
|
1512
|
+
scope,
|
|
1513
|
+
CardControlName.SIDE_CLOSE,
|
|
1514
|
+
side_id=_side_reference(side_id),
|
|
1515
|
+
),
|
|
1516
|
+
style="danger",
|
|
1517
|
+
confirm=(
|
|
1518
|
+
"取消 Side" if creating else "结束 Side",
|
|
1519
|
+
(
|
|
1520
|
+
"将重试清理未完成的 Side,并取消原生订阅。"
|
|
1521
|
+
if creating
|
|
1522
|
+
else "将中断当前 Side Turn、清理后台终端并取消原生订阅。"
|
|
1523
|
+
),
|
|
1524
|
+
),
|
|
1525
|
+
)
|
|
1526
|
+
)
|
|
1527
|
+
)
|
|
1528
|
+
return OutboundCard(card=builder.to_dict())
|
|
1529
|
+
|
|
1530
|
+
|
|
1531
|
+
def binding_created_card(
|
|
1532
|
+
*,
|
|
1533
|
+
short_id: str,
|
|
1534
|
+
project_alias: str,
|
|
1535
|
+
settings: TurnModelSettings | None = None,
|
|
1536
|
+
message_context_mode: MentionContextMode = MentionContextMode.CURRENT_ONLY,
|
|
1537
|
+
task_feedback: BindingTaskFeedback | None = None,
|
|
1538
|
+
) -> OutboundCard:
|
|
1539
|
+
builder = _builder(
|
|
1540
|
+
"会话创建成功",
|
|
1541
|
+
f"{project_alias} · {short_id}",
|
|
1542
|
+
template="green",
|
|
1543
|
+
)
|
|
1544
|
+
builder.markdown(
|
|
1545
|
+
f"✅ 已选择项目 `{_md_code(project_alias)}`,"
|
|
1546
|
+
f"并创建、切换到会话 **{short_id}**。"
|
|
1547
|
+
)
|
|
1548
|
+
builder.markdown(
|
|
1549
|
+
"现在可以直接发送任务,例如:**梳理这个项目的结构。**\n"
|
|
1550
|
+
"如果刚才的任务因没有会话而未执行,请重新发送。\n"
|
|
1551
|
+
"在群聊和群话题中,每条消息都需要 @机器人。"
|
|
1552
|
+
)
|
|
1553
|
+
builder.markdown(_model_source_summary(settings))
|
|
1554
|
+
builder.markdown(_context_mode_summary(message_context_mode))
|
|
1555
|
+
builder.markdown(
|
|
1556
|
+
_task_feedback_summary(task_feedback or BindingTaskFeedback())
|
|
1557
|
+
)
|
|
1558
|
+
return OutboundCard(card=builder.to_dict())
|
|
1559
|
+
|
|
1560
|
+
|
|
1561
|
+
def binding_configured_card(
|
|
1562
|
+
*,
|
|
1563
|
+
short_id: str,
|
|
1564
|
+
project_alias: str,
|
|
1565
|
+
settings: TurnModelSettings | None,
|
|
1566
|
+
message_context_mode: MentionContextMode = MentionContextMode.CURRENT_ONLY,
|
|
1567
|
+
task_feedback: BindingTaskFeedback | None = None,
|
|
1568
|
+
) -> OutboundCard:
|
|
1569
|
+
builder = _builder(
|
|
1570
|
+
"会话配置已保存",
|
|
1571
|
+
f"{project_alias} · {short_id}",
|
|
1572
|
+
template="green",
|
|
1573
|
+
)
|
|
1574
|
+
builder.markdown(_model_source_summary(settings))
|
|
1575
|
+
builder.markdown(_context_mode_summary(message_context_mode))
|
|
1576
|
+
builder.markdown(
|
|
1577
|
+
_task_feedback_summary(task_feedback or BindingTaskFeedback())
|
|
1578
|
+
)
|
|
1579
|
+
return OutboundCard(card=builder.to_dict())
|
|
1580
|
+
|
|
1581
|
+
|
|
1582
|
+
def _model_source_summary(settings: TurnModelSettings | None) -> str:
|
|
1583
|
+
if settings is None:
|
|
1584
|
+
return "Model 来源:继承 Codex,使用 Codex 自身的模型配置。"
|
|
1585
|
+
return (
|
|
1586
|
+
"Model 来源:Netizen 会话显式配置。后续新任务将使用:"
|
|
1587
|
+
f"Model=`{_md_code(settings.model)}` · "
|
|
1588
|
+
f"Effort=`{_md_code(settings.effort_id)}` · "
|
|
1589
|
+
f"Speed=`{_md_code(settings.service_tier_name)}`"
|
|
1590
|
+
)
|
|
1591
|
+
|
|
1592
|
+
|
|
1593
|
+
def _context_mode_summary(mode: MentionContextMode) -> str:
|
|
1594
|
+
if mode is MentionContextMode.CATCH_UP:
|
|
1595
|
+
return (
|
|
1596
|
+
"@ 时读取的消息范围:自动带上期间的群聊讨论。"
|
|
1597
|
+
"两次 @ 之间群里未 @ 机器人的成员消息,"
|
|
1598
|
+
"也会在下一次 @ 时作为背景交给 Codex。"
|
|
1599
|
+
)
|
|
1600
|
+
return "@ 时读取的消息范围:仅这条 @ 消息。"
|
|
1601
|
+
|
|
1602
|
+
|
|
1603
|
+
def _task_feedback_summary(feedback: BindingTaskFeedback) -> str:
|
|
1604
|
+
pulse = "开启" if feedback.reaction_pulse_enabled else "关闭"
|
|
1605
|
+
progress = "开启" if feedback.progress_card_enabled else "关闭"
|
|
1606
|
+
mention = "开启" if feedback.completion_mention_enabled else "关闭"
|
|
1607
|
+
return f"执行中表情闪烁:{pulse}\n进度卡:{progress}\n结束时 @ 提醒:{mention}"
|
|
1608
|
+
|
|
1609
|
+
|
|
1610
|
+
def error_card(message: str, *, scope: FeishuScope | None = None) -> OutboundCard:
|
|
1611
|
+
builder = _builder("操作失败", "请修正后重试", template="red")
|
|
1612
|
+
builder.raw(_notice(message, error=True))
|
|
1613
|
+
if scope is not None:
|
|
1614
|
+
builder.raw(
|
|
1615
|
+
_repeatable_callback_button(
|
|
1616
|
+
label="刷新设置",
|
|
1617
|
+
value=_envelope(
|
|
1618
|
+
scope,
|
|
1619
|
+
CardControlName.REFRESH_SETTINGS,
|
|
1620
|
+
settings_section=SettingsSection.PROJECTS.value,
|
|
1621
|
+
),
|
|
1622
|
+
style="primary_filled",
|
|
1623
|
+
)
|
|
1624
|
+
)
|
|
1625
|
+
return OutboundCard(card=builder.to_dict())
|
|
1626
|
+
|
|
1627
|
+
|
|
1628
|
+
def decode_button_action(
|
|
1629
|
+
*,
|
|
1630
|
+
app_id: str,
|
|
1631
|
+
message_id: str,
|
|
1632
|
+
callback_chat_id: str,
|
|
1633
|
+
sender_id: str,
|
|
1634
|
+
tag: str,
|
|
1635
|
+
value: Any,
|
|
1636
|
+
) -> CardControlIntent:
|
|
1637
|
+
if tag != "button":
|
|
1638
|
+
raise CardActionError(f"不支持的卡片组件:{tag or 'unknown'}")
|
|
1639
|
+
if not message_id or not callback_chat_id or not sender_id:
|
|
1640
|
+
raise CardActionError("卡片回调缺少消息、聊天或操作者标识。")
|
|
1641
|
+
if not isinstance(value, Mapping):
|
|
1642
|
+
raise CardActionError("卡片动作 value 必须是对象。")
|
|
1643
|
+
payload = dict(value)
|
|
1644
|
+
payload.pop("nonce", None)
|
|
1645
|
+
raw_intent = payload.get("intent")
|
|
1646
|
+
try:
|
|
1647
|
+
name = CardControlName(raw_intent)
|
|
1648
|
+
except (TypeError, ValueError) as error:
|
|
1649
|
+
raise CardActionError("未知卡片动作。") from error
|
|
1650
|
+
if payload.get("v") != ACTION_VERSION or isinstance(payload.get("v"), bool):
|
|
1651
|
+
raise CardActionError("卡片版本已过期,请重新发送原命令打开新卡片。")
|
|
1652
|
+
form_only = {
|
|
1653
|
+
CardControlName.REGISTER_PROJECT,
|
|
1654
|
+
CardControlName.CREATE_BINDING,
|
|
1655
|
+
CardControlName.CONFIGURE_BINDING,
|
|
1656
|
+
CardControlName.RENAME_BINDING,
|
|
1657
|
+
}
|
|
1658
|
+
if name in form_only:
|
|
1659
|
+
raise CardActionError("该操作只能通过对应表单提交。")
|
|
1660
|
+
common = {"v", "intent", "chat_id", "scope_kind"}
|
|
1661
|
+
extra_by_name = {
|
|
1662
|
+
CardControlName.OPEN_SETTINGS_SECTION: {"settings_section"},
|
|
1663
|
+
CardControlName.REFRESH_SETTINGS: {"settings_section"},
|
|
1664
|
+
CardControlName.SET_PROJECT_ENABLED: {
|
|
1665
|
+
"project_alias",
|
|
1666
|
+
"enabled",
|
|
1667
|
+
"expected_revision",
|
|
1668
|
+
},
|
|
1669
|
+
CardControlName.ARCHIVE_BINDING: {"binding_id"},
|
|
1670
|
+
CardControlName.ARCHIVE_EXACT_BINDING: {
|
|
1671
|
+
"binding_id",
|
|
1672
|
+
"page",
|
|
1673
|
+
},
|
|
1674
|
+
CardControlName.DELETE_BINDING: {
|
|
1675
|
+
"binding_id",
|
|
1676
|
+
},
|
|
1677
|
+
CardControlName.PREPARE_EXACT_DELETE_BINDING: {
|
|
1678
|
+
"binding_id",
|
|
1679
|
+
"expected_native_thread_id",
|
|
1680
|
+
"page",
|
|
1681
|
+
},
|
|
1682
|
+
CardControlName.DELETE_EXACT_BINDING: {
|
|
1683
|
+
"binding_id",
|
|
1684
|
+
"expected_native_thread_id",
|
|
1685
|
+
"page",
|
|
1686
|
+
},
|
|
1687
|
+
CardControlName.PREPARE_ARCHIVED_DELETE_BINDING: {
|
|
1688
|
+
"binding_id",
|
|
1689
|
+
"expected_native_thread_id",
|
|
1690
|
+
},
|
|
1691
|
+
CardControlName.DELETE_ARCHIVED_BINDING: {
|
|
1692
|
+
"binding_id",
|
|
1693
|
+
"expected_native_thread_id",
|
|
1694
|
+
},
|
|
1695
|
+
CardControlName.UNARCHIVE_BINDING: {"binding_id"},
|
|
1696
|
+
CardControlName.ACTIVATE_BINDING: {"binding_id"},
|
|
1697
|
+
CardControlName.STOP_EXACT_BINDING: {
|
|
1698
|
+
"binding_id",
|
|
1699
|
+
"expected_active_binding_id",
|
|
1700
|
+
"expected_activity_revision",
|
|
1701
|
+
"expected_turn_id",
|
|
1702
|
+
"page",
|
|
1703
|
+
},
|
|
1704
|
+
CardControlName.RECHECK_EXACT_TURN: {
|
|
1705
|
+
"binding_id",
|
|
1706
|
+
"expected_active_binding_id",
|
|
1707
|
+
"expected_activity_revision",
|
|
1708
|
+
"expected_turn_id",
|
|
1709
|
+
"page",
|
|
1710
|
+
},
|
|
1711
|
+
CardControlName.SESSIONS_PAGE: {"page"},
|
|
1712
|
+
CardControlName.REFRESH_ARCHIVED_SESSIONS: set(),
|
|
1713
|
+
CardControlName.GOAL_PAUSE: {
|
|
1714
|
+
"binding_id",
|
|
1715
|
+
"goal_generation",
|
|
1716
|
+
"expected_goal_status",
|
|
1717
|
+
},
|
|
1718
|
+
CardControlName.GOAL_RESUME: {
|
|
1719
|
+
"binding_id",
|
|
1720
|
+
"goal_generation",
|
|
1721
|
+
"expected_goal_status",
|
|
1722
|
+
},
|
|
1723
|
+
CardControlName.GOAL_CLEAR: {
|
|
1724
|
+
"binding_id",
|
|
1725
|
+
"goal_generation",
|
|
1726
|
+
"expected_goal_status",
|
|
1727
|
+
},
|
|
1728
|
+
CardControlName.SIDE_CLOSE: {"side_id"},
|
|
1729
|
+
}
|
|
1730
|
+
try:
|
|
1731
|
+
scope_kind = ScopeKind(payload.get("scope_kind"))
|
|
1732
|
+
except (TypeError, ValueError) as error:
|
|
1733
|
+
raise CardActionError("未知 Scope kind。") from error
|
|
1734
|
+
scope_fields = {"topic_id"} if scope_kind is ScopeKind.TOPIC else set()
|
|
1735
|
+
expected = common | scope_fields | extra_by_name[name]
|
|
1736
|
+
if (
|
|
1737
|
+
name is CardControlName.DELETE_BINDING
|
|
1738
|
+
and "expected_native_thread_id" in payload
|
|
1739
|
+
):
|
|
1740
|
+
expected.add("expected_native_thread_id")
|
|
1741
|
+
if set(payload) != expected:
|
|
1742
|
+
raise CardActionError("卡片动作字段不完整或包含未知字段。")
|
|
1743
|
+
if payload["chat_id"] != callback_chat_id:
|
|
1744
|
+
raise CardActionError("卡片 Scope 与当前聊天不一致。")
|
|
1745
|
+
scope = _scope_from_envelope(
|
|
1746
|
+
app_id=app_id,
|
|
1747
|
+
chat_id=callback_chat_id,
|
|
1748
|
+
kind=scope_kind,
|
|
1749
|
+
topic_id=payload.get("topic_id"),
|
|
1750
|
+
)
|
|
1751
|
+
|
|
1752
|
+
alias = None
|
|
1753
|
+
binding_id = None
|
|
1754
|
+
expected_active_binding_id = None
|
|
1755
|
+
expected_native_thread_id = None
|
|
1756
|
+
side_id = None
|
|
1757
|
+
revision = None
|
|
1758
|
+
enabled = None
|
|
1759
|
+
section = None
|
|
1760
|
+
page = None
|
|
1761
|
+
expected_activity_revision = None
|
|
1762
|
+
expected_turn_id = None
|
|
1763
|
+
goal_generation_value = None
|
|
1764
|
+
expected_goal_status = None
|
|
1765
|
+
if "settings_section" in payload:
|
|
1766
|
+
try:
|
|
1767
|
+
section = SettingsSection(payload["settings_section"])
|
|
1768
|
+
except (TypeError, ValueError) as error:
|
|
1769
|
+
raise CardActionError("未知 Settings 分区。") from error
|
|
1770
|
+
if "project_alias" in payload:
|
|
1771
|
+
alias = _required_string(payload["project_alias"], "project_alias")
|
|
1772
|
+
if "binding_id" in payload:
|
|
1773
|
+
binding_id = _decode_binding_reference(
|
|
1774
|
+
_required_string(payload["binding_id"], "binding_id")
|
|
1775
|
+
)
|
|
1776
|
+
if "expected_active_binding_id" in payload:
|
|
1777
|
+
raw_active_binding_id = payload["expected_active_binding_id"]
|
|
1778
|
+
if raw_active_binding_id is not None:
|
|
1779
|
+
expected_active_binding_id = _decode_binding_reference(
|
|
1780
|
+
_required_string(
|
|
1781
|
+
raw_active_binding_id,
|
|
1782
|
+
"expected_active_binding_id",
|
|
1783
|
+
)
|
|
1784
|
+
)
|
|
1785
|
+
if "expected_native_thread_id" in payload:
|
|
1786
|
+
raw_native_thread_id = payload["expected_native_thread_id"]
|
|
1787
|
+
if raw_native_thread_id is not None:
|
|
1788
|
+
expected_native_thread_id = _decode_native_thread_reference(
|
|
1789
|
+
_required_string(
|
|
1790
|
+
raw_native_thread_id,
|
|
1791
|
+
"expected_native_thread_id",
|
|
1792
|
+
)
|
|
1793
|
+
)
|
|
1794
|
+
if "expected_activity_revision" in payload:
|
|
1795
|
+
raw_activity_revision = payload["expected_activity_revision"]
|
|
1796
|
+
if (
|
|
1797
|
+
isinstance(raw_activity_revision, bool)
|
|
1798
|
+
or not isinstance(raw_activity_revision, int)
|
|
1799
|
+
or raw_activity_revision < 0
|
|
1800
|
+
):
|
|
1801
|
+
raise CardActionError(
|
|
1802
|
+
"expected_activity_revision 必须是非负整数。"
|
|
1803
|
+
)
|
|
1804
|
+
expected_activity_revision = raw_activity_revision
|
|
1805
|
+
if "expected_turn_id" in payload:
|
|
1806
|
+
raw_turn_id = payload["expected_turn_id"]
|
|
1807
|
+
if raw_turn_id is not None:
|
|
1808
|
+
expected_turn_id = _decode_turn_reference(
|
|
1809
|
+
_required_string(raw_turn_id, "expected_turn_id")
|
|
1810
|
+
)
|
|
1811
|
+
if "side_id" in payload:
|
|
1812
|
+
side_id = _decode_side_reference(
|
|
1813
|
+
_required_string(payload["side_id"], "side_id")
|
|
1814
|
+
)
|
|
1815
|
+
if "expected_revision" in payload:
|
|
1816
|
+
revision = payload["expected_revision"]
|
|
1817
|
+
if isinstance(revision, bool) or not isinstance(revision, int) or revision < 1:
|
|
1818
|
+
raise CardActionError("expected_revision 必须是正整数。")
|
|
1819
|
+
if "enabled" in payload:
|
|
1820
|
+
enabled = payload["enabled"]
|
|
1821
|
+
if not isinstance(enabled, bool):
|
|
1822
|
+
raise CardActionError("enabled 必须是布尔值。")
|
|
1823
|
+
if "page" in payload:
|
|
1824
|
+
raw_page = payload["page"]
|
|
1825
|
+
if (
|
|
1826
|
+
isinstance(raw_page, bool)
|
|
1827
|
+
or not isinstance(raw_page, int)
|
|
1828
|
+
or raw_page < 0
|
|
1829
|
+
):
|
|
1830
|
+
raise CardActionError("页码必须是非负整数。")
|
|
1831
|
+
page = raw_page
|
|
1832
|
+
if "goal_generation" in payload:
|
|
1833
|
+
goal_generation_value = _decode_goal_generation(
|
|
1834
|
+
payload["goal_generation"]
|
|
1835
|
+
)
|
|
1836
|
+
if "expected_goal_status" in payload:
|
|
1837
|
+
expected_goal_status = _required_string(
|
|
1838
|
+
payload["expected_goal_status"],
|
|
1839
|
+
"expected_goal_status",
|
|
1840
|
+
)
|
|
1841
|
+
valid_goal_statuses = {item.value for item in GoalStatus}
|
|
1842
|
+
if expected_goal_status not in valid_goal_statuses:
|
|
1843
|
+
raise CardActionError("Goal 预期状态无效。")
|
|
1844
|
+
expected_by_action = {
|
|
1845
|
+
CardControlName.GOAL_PAUSE: {GoalStatus.ACTIVE.value},
|
|
1846
|
+
CardControlName.GOAL_RESUME: {GoalStatus.PAUSED.value},
|
|
1847
|
+
CardControlName.GOAL_CLEAR: valid_goal_statuses
|
|
1848
|
+
- {GoalStatus.ACTIVE.value},
|
|
1849
|
+
}
|
|
1850
|
+
if expected_goal_status not in expected_by_action[name]:
|
|
1851
|
+
raise CardActionError("Goal 动作与预期状态不一致。")
|
|
1852
|
+
if name is CardControlName.SET_PROJECT_ENABLED:
|
|
1853
|
+
section = SettingsSection.PROJECTS
|
|
1854
|
+
if name is CardControlName.SIDE_CLOSE and scope.kind is not ScopeKind.TOPIC:
|
|
1855
|
+
raise CardActionError("Side 结束动作必须来自原 Side 话题。")
|
|
1856
|
+
if (
|
|
1857
|
+
name is CardControlName.RECHECK_EXACT_TURN
|
|
1858
|
+
and expected_turn_id is None
|
|
1859
|
+
):
|
|
1860
|
+
raise CardActionError("Turn 重新检查动作缺少 exact Turn。")
|
|
1861
|
+
return CardControlIntent(
|
|
1862
|
+
scope=scope,
|
|
1863
|
+
source_id=message_id,
|
|
1864
|
+
sender_id=sender_id,
|
|
1865
|
+
name=name,
|
|
1866
|
+
settings_section=section,
|
|
1867
|
+
project_alias=alias,
|
|
1868
|
+
expected_revision=revision,
|
|
1869
|
+
enabled=enabled,
|
|
1870
|
+
binding_id=binding_id,
|
|
1871
|
+
expected_active_binding_id=expected_active_binding_id,
|
|
1872
|
+
expected_native_thread_id=expected_native_thread_id,
|
|
1873
|
+
expected_activity_revision=expected_activity_revision,
|
|
1874
|
+
expected_turn_id=expected_turn_id,
|
|
1875
|
+
side_id=side_id,
|
|
1876
|
+
page=page,
|
|
1877
|
+
goal_generation=goal_generation_value,
|
|
1878
|
+
expected_goal_status=expected_goal_status,
|
|
1879
|
+
)
|
|
1880
|
+
|
|
1881
|
+
|
|
1882
|
+
def decode_card_form(
|
|
1883
|
+
*,
|
|
1884
|
+
scope: FeishuScope,
|
|
1885
|
+
message_id: str,
|
|
1886
|
+
sender_id: str,
|
|
1887
|
+
tag: str,
|
|
1888
|
+
form_value: Any,
|
|
1889
|
+
) -> CardControlIntent:
|
|
1890
|
+
if not isinstance(form_value, Mapping):
|
|
1891
|
+
raise CardActionError("卡片表单值必须是对象。")
|
|
1892
|
+
fields = set(form_value)
|
|
1893
|
+
has_new = any(field.startswith("new_") for field in fields)
|
|
1894
|
+
has_config = any(field.startswith("config_") for field in fields)
|
|
1895
|
+
has_rename = any(field.startswith("rename_name_v1__") for field in fields)
|
|
1896
|
+
if sum((has_new, has_config, has_rename)) > 1:
|
|
1897
|
+
raise CardActionError("卡片表单混合了不同操作的字段。")
|
|
1898
|
+
if has_new:
|
|
1899
|
+
return _decode_new_binding_form(
|
|
1900
|
+
scope=scope,
|
|
1901
|
+
message_id=message_id,
|
|
1902
|
+
sender_id=sender_id,
|
|
1903
|
+
tag=tag,
|
|
1904
|
+
form_value=form_value,
|
|
1905
|
+
)
|
|
1906
|
+
if has_config:
|
|
1907
|
+
return _decode_config_form(
|
|
1908
|
+
scope=scope,
|
|
1909
|
+
message_id=message_id,
|
|
1910
|
+
sender_id=sender_id,
|
|
1911
|
+
tag=tag,
|
|
1912
|
+
form_value=form_value,
|
|
1913
|
+
)
|
|
1914
|
+
if has_rename:
|
|
1915
|
+
return _decode_rename_form(
|
|
1916
|
+
scope=scope,
|
|
1917
|
+
message_id=message_id,
|
|
1918
|
+
sender_id=sender_id,
|
|
1919
|
+
tag=tag,
|
|
1920
|
+
form_value=form_value,
|
|
1921
|
+
)
|
|
1922
|
+
return decode_settings_form(
|
|
1923
|
+
scope=scope,
|
|
1924
|
+
message_id=message_id,
|
|
1925
|
+
sender_id=sender_id,
|
|
1926
|
+
tag=tag,
|
|
1927
|
+
form_value=form_value,
|
|
1928
|
+
)
|
|
1929
|
+
|
|
1930
|
+
|
|
1931
|
+
def _decode_new_binding_form(
|
|
1932
|
+
*,
|
|
1933
|
+
scope: FeishuScope,
|
|
1934
|
+
message_id: str,
|
|
1935
|
+
sender_id: str,
|
|
1936
|
+
tag: str,
|
|
1937
|
+
form_value: Mapping[str, Any],
|
|
1938
|
+
) -> CardControlIntent:
|
|
1939
|
+
if tag != "button" or not message_id or not sender_id:
|
|
1940
|
+
raise CardActionError("会话配置表单回调不完整。")
|
|
1941
|
+
payload = dict(form_value)
|
|
1942
|
+
_require_completion_mention_field(payload, prefix="new")
|
|
1943
|
+
base_fields = {
|
|
1944
|
+
"new_project",
|
|
1945
|
+
"new_model",
|
|
1946
|
+
"new_task_reactions",
|
|
1947
|
+
"new_progress_card",
|
|
1948
|
+
"new_completion_mention",
|
|
1949
|
+
}
|
|
1950
|
+
context_fields = (
|
|
1951
|
+
{"new_context_mode"} if "new_context_mode" in payload else set()
|
|
1952
|
+
)
|
|
1953
|
+
catalog_fields = {"new_effort", "new_speed"}
|
|
1954
|
+
if frozenset(payload) not in {
|
|
1955
|
+
frozenset(base_fields | context_fields),
|
|
1956
|
+
frozenset(base_fields | context_fields | catalog_fields),
|
|
1957
|
+
}:
|
|
1958
|
+
raise CardActionError("会话配置表单字段不完整或包含未知字段。")
|
|
1959
|
+
|
|
1960
|
+
project_alias, expected_revision = _decode_project_reference(
|
|
1961
|
+
_required_string(payload["new_project"], "new_project")
|
|
1962
|
+
)
|
|
1963
|
+
model_id = _decode_new_model_reference(
|
|
1964
|
+
_required_string(payload["new_model"], "new_model")
|
|
1965
|
+
)
|
|
1966
|
+
settings = decode_session_settings_fields(payload, prefix="new", model_id=model_id)
|
|
1967
|
+
turn = settings.turn_settings
|
|
1968
|
+
return CardControlIntent(
|
|
1969
|
+
scope=scope,
|
|
1970
|
+
source_id=message_id,
|
|
1971
|
+
sender_id=sender_id,
|
|
1972
|
+
name=CardControlName.CREATE_BINDING,
|
|
1973
|
+
project_alias=project_alias,
|
|
1974
|
+
expected_revision=expected_revision,
|
|
1975
|
+
model_id=model_id,
|
|
1976
|
+
effort_id=turn.effort_id if turn else None,
|
|
1977
|
+
service_tier_id=turn.service_tier_id if turn else None,
|
|
1978
|
+
message_context_mode=settings.message_context_mode,
|
|
1979
|
+
reaction_pulse_enabled=settings.task_feedback.reaction_pulse_enabled,
|
|
1980
|
+
progress_card_enabled=settings.task_feedback.progress_card_enabled,
|
|
1981
|
+
completion_mention_enabled=settings.task_feedback.completion_mention_enabled,
|
|
1982
|
+
)
|
|
1983
|
+
|
|
1984
|
+
|
|
1985
|
+
def _decode_config_form(
|
|
1986
|
+
*,
|
|
1987
|
+
scope: FeishuScope,
|
|
1988
|
+
message_id: str,
|
|
1989
|
+
sender_id: str,
|
|
1990
|
+
tag: str,
|
|
1991
|
+
form_value: Mapping[str, Any],
|
|
1992
|
+
) -> CardControlIntent:
|
|
1993
|
+
if tag != "button" or not message_id or not sender_id:
|
|
1994
|
+
raise CardActionError("会话配置表单回调不完整。")
|
|
1995
|
+
payload = dict(form_value)
|
|
1996
|
+
_require_completion_mention_field(payload, prefix="config")
|
|
1997
|
+
base_fields = {
|
|
1998
|
+
"config_model",
|
|
1999
|
+
"config_task_reactions",
|
|
2000
|
+
"config_progress_card",
|
|
2001
|
+
"config_completion_mention",
|
|
2002
|
+
}
|
|
2003
|
+
context_fields = (
|
|
2004
|
+
{"config_context_mode"} if "config_context_mode" in payload else set()
|
|
2005
|
+
)
|
|
2006
|
+
catalog_fields = {"config_effort", "config_speed"}
|
|
2007
|
+
if frozenset(payload) not in {
|
|
2008
|
+
frozenset(base_fields | context_fields),
|
|
2009
|
+
frozenset(base_fields | context_fields | catalog_fields),
|
|
2010
|
+
}:
|
|
2011
|
+
raise CardActionError("会话配置表单字段不完整或包含未知字段。")
|
|
2012
|
+
(
|
|
2013
|
+
binding_id,
|
|
2014
|
+
expected_settings_revision,
|
|
2015
|
+
expected_context_revision,
|
|
2016
|
+
feedback_revision,
|
|
2017
|
+
model_id,
|
|
2018
|
+
) = _decode_config_model_reference(
|
|
2019
|
+
_required_string(payload["config_model"], "config_model")
|
|
2020
|
+
)
|
|
2021
|
+
settings = decode_session_settings_fields(payload, prefix="config", model_id=model_id)
|
|
2022
|
+
turn = settings.turn_settings
|
|
2023
|
+
return CardControlIntent(
|
|
2024
|
+
scope=scope,
|
|
2025
|
+
source_id=message_id,
|
|
2026
|
+
sender_id=sender_id,
|
|
2027
|
+
name=CardControlName.CONFIGURE_BINDING,
|
|
2028
|
+
expected_settings_revision=expected_settings_revision,
|
|
2029
|
+
expected_context_revision=expected_context_revision,
|
|
2030
|
+
feedback_revision=feedback_revision,
|
|
2031
|
+
binding_id=binding_id,
|
|
2032
|
+
model_id=model_id,
|
|
2033
|
+
effort_id=turn.effort_id if turn else None,
|
|
2034
|
+
service_tier_id=turn.service_tier_id if turn else None,
|
|
2035
|
+
message_context_mode=settings.message_context_mode,
|
|
2036
|
+
reaction_pulse_enabled=settings.task_feedback.reaction_pulse_enabled,
|
|
2037
|
+
progress_card_enabled=settings.task_feedback.progress_card_enabled,
|
|
2038
|
+
completion_mention_enabled=settings.task_feedback.completion_mention_enabled,
|
|
2039
|
+
)
|
|
2040
|
+
|
|
2041
|
+
|
|
2042
|
+
def _decode_rename_form(
|
|
2043
|
+
*,
|
|
2044
|
+
scope: FeishuScope,
|
|
2045
|
+
message_id: str,
|
|
2046
|
+
sender_id: str,
|
|
2047
|
+
tag: str,
|
|
2048
|
+
form_value: Mapping[str, Any],
|
|
2049
|
+
) -> CardControlIntent:
|
|
2050
|
+
if tag != "button" or not message_id or not sender_id:
|
|
2051
|
+
raise CardActionError("会话重命名表单回调不完整。")
|
|
2052
|
+
payload = dict(form_value)
|
|
2053
|
+
if len(payload) != 1:
|
|
2054
|
+
raise CardActionError("会话重命名表单字段不完整或包含未知字段。")
|
|
2055
|
+
field, raw_name = next(iter(payload.items()))
|
|
2056
|
+
match = _RENAME_NAME_FIELD.fullmatch(field)
|
|
2057
|
+
if match is None:
|
|
2058
|
+
raise CardActionError("会话重命名卡片已过期,请重新发送 /rename。")
|
|
2059
|
+
name = " ".join(_required_string(raw_name, "thread_name").split())
|
|
2060
|
+
if not name:
|
|
2061
|
+
raise CardActionError("会话名称不能为空。")
|
|
2062
|
+
if len(name) > MAX_THREAD_NAME_CHARS:
|
|
2063
|
+
raise CardActionError(
|
|
2064
|
+
f"会话名称不能超过 {MAX_THREAD_NAME_CHARS} 个字符。"
|
|
2065
|
+
)
|
|
2066
|
+
return CardControlIntent(
|
|
2067
|
+
scope=scope,
|
|
2068
|
+
source_id=message_id,
|
|
2069
|
+
sender_id=sender_id,
|
|
2070
|
+
name=CardControlName.RENAME_BINDING,
|
|
2071
|
+
binding_id=match.group(1),
|
|
2072
|
+
thread_name=name,
|
|
2073
|
+
)
|
|
2074
|
+
|
|
2075
|
+
|
|
2076
|
+
def decode_settings_form(
|
|
2077
|
+
*,
|
|
2078
|
+
scope: FeishuScope,
|
|
2079
|
+
message_id: str,
|
|
2080
|
+
sender_id: str,
|
|
2081
|
+
tag: str,
|
|
2082
|
+
form_value: Any,
|
|
2083
|
+
) -> CardControlIntent:
|
|
2084
|
+
try:
|
|
2085
|
+
return _decode_settings_form(
|
|
2086
|
+
scope=scope,
|
|
2087
|
+
message_id=message_id,
|
|
2088
|
+
sender_id=sender_id,
|
|
2089
|
+
tag=tag,
|
|
2090
|
+
form_value=form_value,
|
|
2091
|
+
)
|
|
2092
|
+
except SettingsCardActionError:
|
|
2093
|
+
raise
|
|
2094
|
+
except CardActionError as error:
|
|
2095
|
+
raise SettingsCardActionError(
|
|
2096
|
+
str(error),
|
|
2097
|
+
scope=scope,
|
|
2098
|
+
section=SettingsSection.PROJECTS,
|
|
2099
|
+
) from error
|
|
2100
|
+
|
|
2101
|
+
|
|
2102
|
+
def _decode_settings_form(
|
|
2103
|
+
*,
|
|
2104
|
+
scope: FeishuScope,
|
|
2105
|
+
message_id: str,
|
|
2106
|
+
sender_id: str,
|
|
2107
|
+
tag: str,
|
|
2108
|
+
form_value: Any,
|
|
2109
|
+
) -> CardControlIntent:
|
|
2110
|
+
if tag != "button" or not message_id or not sender_id:
|
|
2111
|
+
raise CardActionError("Settings 表单回调不完整。")
|
|
2112
|
+
if not isinstance(form_value, Mapping):
|
|
2113
|
+
raise CardActionError("Settings 表单值必须是对象。")
|
|
2114
|
+
payload = dict(form_value)
|
|
2115
|
+
if set(payload) == {"project_manage_target", "project_manage_operation"}:
|
|
2116
|
+
return _decode_project_management_form(
|
|
2117
|
+
scope=scope,
|
|
2118
|
+
message_id=message_id,
|
|
2119
|
+
sender_id=sender_id,
|
|
2120
|
+
payload=payload,
|
|
2121
|
+
)
|
|
2122
|
+
|
|
2123
|
+
required = {"project_alias", "project_mode"}
|
|
2124
|
+
allowed = required | {"project_path"}
|
|
2125
|
+
if not required.issubset(payload) or not set(payload).issubset(allowed):
|
|
2126
|
+
raise CardActionError("未知、字段不完整或包含额外字段的 Settings 表单。")
|
|
2127
|
+
alias = _required_string(payload["project_alias"], "project_alias").strip()
|
|
2128
|
+
mode = _decode_project_mode_reference(
|
|
2129
|
+
_required_string(payload["project_mode"], "project_mode")
|
|
2130
|
+
)
|
|
2131
|
+
raw_path = payload.get("project_path", "")
|
|
2132
|
+
if not isinstance(raw_path, str):
|
|
2133
|
+
raise CardActionError("project_path 必须是字符串。")
|
|
2134
|
+
path = raw_path.strip() or None
|
|
2135
|
+
if mode == "existing" and path is None:
|
|
2136
|
+
raise CardActionError("登记已有目录时必须填写绝对路径。")
|
|
2137
|
+
return CardControlIntent(
|
|
2138
|
+
scope=scope,
|
|
2139
|
+
source_id=message_id,
|
|
2140
|
+
sender_id=sender_id,
|
|
2141
|
+
name=CardControlName.REGISTER_PROJECT,
|
|
2142
|
+
settings_section=SettingsSection.PROJECTS,
|
|
2143
|
+
project_alias=alias,
|
|
2144
|
+
project_path=path,
|
|
2145
|
+
create_directory=mode == "create",
|
|
2146
|
+
)
|
|
2147
|
+
|
|
2148
|
+
|
|
2149
|
+
def _decode_project_management_form(
|
|
2150
|
+
*,
|
|
2151
|
+
scope: FeishuScope,
|
|
2152
|
+
message_id: str,
|
|
2153
|
+
sender_id: str,
|
|
2154
|
+
payload: Mapping[str, Any],
|
|
2155
|
+
) -> CardControlIntent:
|
|
2156
|
+
alias, revision = _decode_project_reference(
|
|
2157
|
+
_required_string(payload["project_manage_target"], "project_manage_target")
|
|
2158
|
+
)
|
|
2159
|
+
operation = _required_string(
|
|
2160
|
+
payload["project_manage_operation"],
|
|
2161
|
+
"project_manage_operation",
|
|
2162
|
+
)
|
|
2163
|
+
if operation == "enable":
|
|
2164
|
+
enabled = True
|
|
2165
|
+
elif operation == "disable":
|
|
2166
|
+
enabled = False
|
|
2167
|
+
else:
|
|
2168
|
+
raise CardActionError("未知 Project 操作。")
|
|
2169
|
+
return CardControlIntent(
|
|
2170
|
+
scope=scope,
|
|
2171
|
+
source_id=message_id,
|
|
2172
|
+
sender_id=sender_id,
|
|
2173
|
+
name=CardControlName.SET_PROJECT_ENABLED,
|
|
2174
|
+
settings_section=SettingsSection.PROJECTS,
|
|
2175
|
+
project_alias=alias,
|
|
2176
|
+
expected_revision=revision,
|
|
2177
|
+
enabled=enabled,
|
|
2178
|
+
)
|
|
2179
|
+
|
|
2180
|
+
|
|
2181
|
+
def _project_row(
|
|
2182
|
+
project: Project,
|
|
2183
|
+
controls: list[dict[str, Any]],
|
|
2184
|
+
*,
|
|
2185
|
+
status: str,
|
|
2186
|
+
) -> dict[str, Any]:
|
|
2187
|
+
return {
|
|
2188
|
+
"tag": "column_set",
|
|
2189
|
+
"flex_mode": "none",
|
|
2190
|
+
"background_style": "grey-50",
|
|
2191
|
+
"margin": "0 0 8px 0",
|
|
2192
|
+
"columns": [
|
|
2193
|
+
{
|
|
2194
|
+
"tag": "column",
|
|
2195
|
+
"width": "weighted",
|
|
2196
|
+
"weight": 5,
|
|
2197
|
+
"padding": "8px",
|
|
2198
|
+
"elements": [
|
|
2199
|
+
_plain(f"{project.alias} · {status}\n{project.cwd}"),
|
|
2200
|
+
],
|
|
2201
|
+
},
|
|
2202
|
+
{
|
|
2203
|
+
"tag": "column",
|
|
2204
|
+
"width": "auto",
|
|
2205
|
+
"vertical_align": "center",
|
|
2206
|
+
"padding": "8px",
|
|
2207
|
+
"elements": controls,
|
|
2208
|
+
},
|
|
2209
|
+
],
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
|
|
2213
|
+
def _archived_session_row(
|
|
2214
|
+
*,
|
|
2215
|
+
scope: FeishuScope,
|
|
2216
|
+
session: ArchivedSessionCardItem,
|
|
2217
|
+
native_delete_available: bool,
|
|
2218
|
+
) -> dict[str, Any]:
|
|
2219
|
+
controls = [
|
|
2220
|
+
_callback_button(
|
|
2221
|
+
label="恢复并切换",
|
|
2222
|
+
value=_envelope(
|
|
2223
|
+
scope,
|
|
2224
|
+
CardControlName.UNARCHIVE_BINDING,
|
|
2225
|
+
binding_id=_binding_reference(session.binding_id),
|
|
2226
|
+
),
|
|
2227
|
+
style="primary_filled",
|
|
2228
|
+
)
|
|
2229
|
+
]
|
|
2230
|
+
if native_delete_available:
|
|
2231
|
+
controls.append(
|
|
2232
|
+
_repeatable_callback_button(
|
|
2233
|
+
label="删除",
|
|
2234
|
+
value=_envelope(
|
|
2235
|
+
scope,
|
|
2236
|
+
CardControlName.PREPARE_ARCHIVED_DELETE_BINDING,
|
|
2237
|
+
binding_id=_binding_reference(session.binding_id),
|
|
2238
|
+
expected_native_thread_id=_native_thread_reference(
|
|
2239
|
+
session.native_thread_id
|
|
2240
|
+
),
|
|
2241
|
+
),
|
|
2242
|
+
style="danger",
|
|
2243
|
+
)
|
|
2244
|
+
)
|
|
2245
|
+
return {
|
|
2246
|
+
"tag": "column_set",
|
|
2247
|
+
"flex_mode": "none",
|
|
2248
|
+
"background_style": "grey-50",
|
|
2249
|
+
"margin": "0 0 8px 0",
|
|
2250
|
+
"columns": [
|
|
2251
|
+
{
|
|
2252
|
+
"tag": "column",
|
|
2253
|
+
"width": "weighted",
|
|
2254
|
+
"weight": 5,
|
|
2255
|
+
"padding": "8px",
|
|
2256
|
+
"elements": [
|
|
2257
|
+
_plain(
|
|
2258
|
+
f"{session.title}\n"
|
|
2259
|
+
f"会话:{session.short_id} · "
|
|
2260
|
+
f"Project:{session.project_alias} · "
|
|
2261
|
+
f"Native:{session.native_thread_id[:8]}"
|
|
2262
|
+
)
|
|
2263
|
+
],
|
|
2264
|
+
},
|
|
2265
|
+
{
|
|
2266
|
+
"tag": "column",
|
|
2267
|
+
"width": "auto",
|
|
2268
|
+
"vertical_align": "center",
|
|
2269
|
+
"padding": "8px",
|
|
2270
|
+
"elements": controls,
|
|
2271
|
+
},
|
|
2272
|
+
],
|
|
2273
|
+
}
|