python-codex 0.2.7__py3-none-any.whl → 0.3.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.
- pycodex/__init__.py +14 -14
- pycodex/agent.py +465 -499
- pycodex/bootstrap.py +417 -0
- pycodex/cli.py +236 -510
- pycodex/compat.py +19 -5
- pycodex/context.py +222 -212
- pycodex/doctor.py +52 -48
- pycodex/events.py +857 -0
- pycodex/feishu_card.py +217 -163
- pycodex/feishu_link.py +43 -83
- pycodex/model.py +324 -253
- pycodex/model_metadata.py +19 -7
- pycodex/portable.py +76 -45
- pycodex/portable_server.py +32 -24
- pycodex/prompts/models.json +245 -983
- pycodex/protocol.py +177 -137
- pycodex/runtime.py +579 -176
- pycodex/runtime_services.py +204 -157
- pycodex/tools/__init__.py +1 -1
- pycodex/tools/apply_patch_tool.py +69 -48
- pycodex/tools/base_tool.py +89 -42
- pycodex/tools/clock_tool.py +58 -25
- pycodex/tools/close_agent_tool.py +2 -2
- pycodex/tools/code_mode_manager.py +77 -64
- pycodex/tools/exec_command_tool.py +26 -11
- pycodex/tools/exec_tool.py +4 -4
- pycodex/tools/grep_files_tool.py +12 -10
- pycodex/tools/ipython_tool.py +10 -13
- pycodex/tools/list_dir_tool.py +13 -9
- pycodex/tools/read_file_tool.py +29 -17
- pycodex/tools/request_permissions_tool.py +15 -5
- pycodex/tools/request_user_input_tool.py +13 -104
- pycodex/tools/resume_agent_tool.py +2 -2
- pycodex/tools/send_input_tool.py +11 -8
- pycodex/tools/shell_command_tool.py +7 -5
- pycodex/tools/shell_tool.py +7 -5
- pycodex/tools/spawn_agent_tool.py +7 -4
- pycodex/tools/unified_exec_manager.py +102 -69
- pycodex/tools/update_plan_tool.py +8 -5
- pycodex/tools/view_image_tool.py +7 -5
- pycodex/tools/wait_agent_tool.py +27 -4
- pycodex/tools/wait_tool.py +5 -4
- pycodex/tools/web_search_tool.py +4 -2
- pycodex/tools/write_stdin_tool.py +12 -11
- pycodex/utils/__init__.py +2 -17
- pycodex/utils/compactor.py +41 -72
- pycodex/utils/debug.py +2 -2
- pycodex/utils/dotenv.py +6 -7
- pycodex/utils/event_helpers.py +190 -0
- pycodex/utils/get_env.py +27 -70
- pycodex/{image_utils.py → utils/image_utils.py} +8 -11
- pycodex/utils/random_ids.py +1 -2
- pycodex/utils/session_persist.py +217 -163
- pycodex/utils/truncation.py +21 -45
- python_codex-0.3.0.dist-info/METADATA +704 -0
- python_codex-0.3.0.dist-info/RECORD +90 -0
- responses_server/__init__.py +1 -5
- responses_server/__main__.py +0 -1
- responses_server/app.py +36 -31
- responses_server/config.py +23 -23
- responses_server/messages_api.py +51 -53
- responses_server/payload_processors.py +25 -20
- responses_server/server.py +11 -11
- responses_server/session_store.py +14 -11
- responses_server/stream_router.py +101 -98
- responses_server/tools/custom_adapter.py +17 -16
- responses_server/tools/web_search.py +39 -36
- responses_server/trajectory_dump.py +36 -14
- workspace_server/__main__.py +0 -1
- workspace_server/app.py +461 -375
- workspace_server/workspace.html +852 -228
- workspace_server/workspaces.html +94 -95
- workspace_server/workspaces.py +137 -79
- pycodex/collaboration.py +0 -20
- pycodex/interactive_session.py +0 -415
- pycodex/prompts/collaboration_default.md +0 -11
- pycodex/prompts/collaboration_plan.md +0 -128
- pycodex/utils/toolcall_visualize.py +0 -713
- pycodex/utils/visualize.py +0 -560
- python_codex-0.2.7.dist-info/METADATA +0 -455
- python_codex-0.2.7.dist-info/RECORD +0 -93
- {python_codex-0.2.7.dist-info → python_codex-0.3.0.dist-info}/WHEEL +0 -0
- {python_codex-0.2.7.dist-info → python_codex-0.3.0.dist-info}/entry_points.txt +0 -0
- {python_codex-0.2.7.dist-info → python_codex-0.3.0.dist-info}/licenses/LICENSE +0 -0
pycodex/feishu_card.py
CHANGED
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import os
|
|
3
|
-
from pathlib import Path
|
|
4
3
|
import time
|
|
5
4
|
import typing
|
|
5
|
+
from pathlib import Path
|
|
6
6
|
|
|
7
7
|
import requests
|
|
8
8
|
|
|
9
|
+
from .events import (
|
|
10
|
+
DEFAULT_MAIN_PROMPT,
|
|
11
|
+
Event,
|
|
12
|
+
EventDisplay,
|
|
13
|
+
InputQueuedEvent,
|
|
14
|
+
SessionStateEvent,
|
|
15
|
+
TurnCompletedEvent,
|
|
16
|
+
TurnStartedEvent,
|
|
17
|
+
)
|
|
18
|
+
from .utils.event_helpers import completed_history
|
|
19
|
+
|
|
9
20
|
FEISHU_API_BASE = "https://open.feishu.cn/open-apis"
|
|
10
21
|
FEISHU_DOMAIN = "https://open.feishu.cn"
|
|
11
22
|
FEISHU_REFRESH_TOKEN_FILE = "~/.codex/.feishu_refresh_token"
|
|
@@ -13,7 +24,6 @@ CARD_OUTPUT_LIMIT = 6500
|
|
|
13
24
|
CARD_OUTPUT_MODE_MARKDOWN = "markdown"
|
|
14
25
|
CARD_OUTPUT_MODE_CODE = "code"
|
|
15
26
|
CARD_CONTENT_ERROR_MARKER = "Failed to create card content"
|
|
16
|
-
LAST_TURN_PREFIX = "(*last turn)\n"
|
|
17
27
|
|
|
18
28
|
|
|
19
29
|
class PycodexCard:
|
|
@@ -25,7 +35,6 @@ class PycodexCard:
|
|
|
25
35
|
domain: str = FEISHU_DOMAIN,
|
|
26
36
|
verification_token: "typing.Union[str, None]" = None,
|
|
27
37
|
encrypt_key: "typing.Union[str, None]" = None,
|
|
28
|
-
refresh_token: "typing.Union[str, None]" = None,
|
|
29
38
|
session: "typing.Union[requests.Session, None]" = None,
|
|
30
39
|
) -> None:
|
|
31
40
|
self.app_id = app_id
|
|
@@ -34,22 +43,23 @@ class PycodexCard:
|
|
|
34
43
|
self.domain = domain
|
|
35
44
|
self.verification_token = verification_token
|
|
36
45
|
self.encrypt_key = encrypt_key
|
|
37
|
-
self.refresh_token = refresh_token
|
|
38
46
|
self.session = session or requests.Session()
|
|
39
47
|
self.message_id = None
|
|
40
48
|
self.callback_token = None
|
|
41
49
|
self.session_key = None
|
|
42
|
-
self.status =
|
|
43
|
-
self.
|
|
44
|
-
self.last_sender = "cli"
|
|
45
|
-
self.last_prompt = ""
|
|
50
|
+
self.status = None
|
|
51
|
+
self.prompt_text = DEFAULT_MAIN_PROMPT
|
|
46
52
|
self.model_name = "pycodex"
|
|
47
53
|
self.output_text = ""
|
|
48
|
-
self.
|
|
49
|
-
self.
|
|
50
|
-
self.
|
|
51
|
-
self.
|
|
54
|
+
self.activity_text = ""
|
|
55
|
+
self.user_prompt = ""
|
|
56
|
+
self.turn_history = []
|
|
57
|
+
self.history_index = None
|
|
58
|
+
self._active_turn_id = None
|
|
59
|
+
self._event_output = ""
|
|
60
|
+
self.display = EventDisplay(self._log, self._set_status, self._set_prompt)
|
|
52
61
|
self.detached = False
|
|
62
|
+
self.accepts_input = True
|
|
53
63
|
self._user_access_token = None
|
|
54
64
|
self._user_token_expires_at = 0.0
|
|
55
65
|
self._tenant_token = None
|
|
@@ -68,7 +78,6 @@ class PycodexCard:
|
|
|
68
78
|
"LARK_VERIFICATION_TOKEN",
|
|
69
79
|
),
|
|
70
80
|
encrypt_key=_env("FEISHU_ENCRYPT_KEY", "LARK_ENCRYPT_KEY"),
|
|
71
|
-
refresh_token=_read_refresh_token() or os.environ.get("FEISHU_REFRESH_TOKEN"),
|
|
72
81
|
)
|
|
73
82
|
|
|
74
83
|
def configured(self) -> bool:
|
|
@@ -146,145 +155,92 @@ class PycodexCard:
|
|
|
146
155
|
use_user_token=False,
|
|
147
156
|
)
|
|
148
157
|
|
|
149
|
-
def set_queued(self, prompt: str, sender: str = "cli") -> None:
|
|
150
|
-
self.status = "Queued"
|
|
151
|
-
self.status_detail = "Waiting for pycodex."
|
|
152
|
-
self.last_sender = sender or "cli"
|
|
153
|
-
self.last_prompt = prompt
|
|
154
|
-
self._mark_last_turn_output()
|
|
155
|
-
self._reset_working_output()
|
|
156
|
-
self.error = ""
|
|
157
|
-
self.running = True
|
|
158
|
-
|
|
159
|
-
def set_snapshot(self, prompt: str, output: str) -> None:
|
|
160
|
-
self.last_prompt = prompt or self.last_prompt
|
|
161
|
-
self.output_text = output or self.output_text
|
|
162
|
-
|
|
163
158
|
def detach(self) -> None:
|
|
164
|
-
self.status = "Detached"
|
|
165
|
-
self.status_detail = ""
|
|
166
|
-
self.error = ""
|
|
167
|
-
self.running = False
|
|
168
159
|
self.detached = True
|
|
169
160
|
|
|
170
|
-
def apply_event(self, event) ->
|
|
171
|
-
|
|
172
|
-
if
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
prompt_text =
|
|
191
|
-
if
|
|
192
|
-
self.
|
|
193
|
-
|
|
161
|
+
def apply_event(self, event: "Event") -> None:
|
|
162
|
+
self._event_output = ""
|
|
163
|
+
if isinstance(event, SessionStateEvent):
|
|
164
|
+
state = event.state
|
|
165
|
+
self.model_name = state["model"]
|
|
166
|
+
self.accepts_input = state["accepts_input"]
|
|
167
|
+
self.display.closed = state["closed"]
|
|
168
|
+
if event.reason in {"attach", "history"}:
|
|
169
|
+
self.output_text = ""
|
|
170
|
+
self.activity_text = ""
|
|
171
|
+
self.turn_history = list(completed_history(state))
|
|
172
|
+
self.history_index = None
|
|
173
|
+
active = state["active_turn"]
|
|
174
|
+
self._active_turn_id = active["turn_id"] if active is not None else None
|
|
175
|
+
self.user_prompt = (
|
|
176
|
+
"\n".join(active["user_texts"])
|
|
177
|
+
if active is not None
|
|
178
|
+
else self.turn_history[-1][0] if self.turn_history else ""
|
|
179
|
+
)
|
|
180
|
+
self.display.stream_buffer = ""
|
|
181
|
+
self.prompt_text = DEFAULT_MAIN_PROMPT
|
|
182
|
+
if state["busy"]:
|
|
183
|
+
self._set_status("working")
|
|
184
|
+
else:
|
|
185
|
+
self.display.set_idle_status(state["background_work_count"])
|
|
186
|
+
for _prompt, response in self.turn_history[-1:]:
|
|
187
|
+
self.output_text = response
|
|
188
|
+
if state["busy"]:
|
|
189
|
+
self._mark_last_turn_output()
|
|
190
|
+
elif isinstance(event, (InputQueuedEvent, TurnStartedEvent)):
|
|
194
191
|
self._mark_last_turn_output()
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
self.
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
if kind == "stream_error":
|
|
214
|
-
self.status = "Retrying"
|
|
215
|
-
self.status_detail = str(
|
|
216
|
-
payload.get("summary") or payload.get("message") or ""
|
|
217
|
-
)
|
|
218
|
-
return False
|
|
219
|
-
if kind == "turn_completed":
|
|
220
|
-
final_text = str(payload.get("output_text") or "")
|
|
221
|
-
if final_text:
|
|
222
|
-
self.output_text = final_text
|
|
223
|
-
self.status = "Idle"
|
|
224
|
-
self.status_detail = "Turn completed."
|
|
225
|
-
self.running = False
|
|
226
|
-
self._reset_working_output()
|
|
227
|
-
return True
|
|
228
|
-
if kind in {"turn_failed", "submission_failed"}:
|
|
229
|
-
self.status = "Error"
|
|
230
|
-
self.error = str(payload.get("error") or kind)
|
|
231
|
-
self.status_detail = self.error
|
|
232
|
-
self.running = False
|
|
233
|
-
self._finish_working_delta_segment()
|
|
234
|
-
return True
|
|
235
|
-
if kind in {"turn_interrupted", "submission_cancelled"}:
|
|
236
|
-
self.status = "Idle"
|
|
237
|
-
self.status_detail = kind.replace("_", " ")
|
|
238
|
-
self.running = False
|
|
239
|
-
self._reset_working_output()
|
|
240
|
-
return True
|
|
241
|
-
return False
|
|
192
|
+
if isinstance(event, TurnStartedEvent):
|
|
193
|
+
prompt = event.visualize()
|
|
194
|
+
if event.turn_id == self._active_turn_id and self.user_prompt:
|
|
195
|
+
self.user_prompt += "\n" + prompt
|
|
196
|
+
else:
|
|
197
|
+
self.user_prompt = prompt
|
|
198
|
+
self._active_turn_id = event.turn_id
|
|
199
|
+
event.render(self.display)
|
|
200
|
+
if isinstance(event, TurnCompletedEvent):
|
|
201
|
+
if event.output_text:
|
|
202
|
+
self.output_text = event.output_text
|
|
203
|
+
self.turn_history.append((self.user_prompt, event.output_text))
|
|
204
|
+
self.activity_text = ""
|
|
205
|
+
self._active_turn_id = None
|
|
206
|
+
elif isinstance(event, TurnStartedEvent):
|
|
207
|
+
self.activity_text = ""
|
|
208
|
+
elif self._event_output:
|
|
209
|
+
self.activity_text = self._event_output
|
|
242
210
|
|
|
243
211
|
def _mark_last_turn_output(self) -> None:
|
|
244
|
-
|
|
245
|
-
|
|
212
|
+
prefix = "(*last turn)\n"
|
|
213
|
+
if self.output_text and not self.output_text.startswith(prefix):
|
|
214
|
+
self.output_text = prefix + self.output_text
|
|
246
215
|
|
|
247
|
-
def
|
|
248
|
-
self.
|
|
249
|
-
|
|
216
|
+
def _log(self, text: str) -> None:
|
|
217
|
+
self._event_output = (
|
|
218
|
+
self._event_output + ("\n" if self._event_output else "") + text
|
|
219
|
+
)[-CARD_OUTPUT_LIMIT:]
|
|
250
220
|
|
|
251
|
-
def
|
|
252
|
-
self.
|
|
221
|
+
def _set_status(self, text: "typing.Union[str, None]") -> None:
|
|
222
|
+
self.status = text
|
|
253
223
|
|
|
254
|
-
def
|
|
255
|
-
if
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
self.working_output_text += delta
|
|
259
|
-
else:
|
|
260
|
-
self.working_output_text = delta
|
|
261
|
-
self._working_delta_active = True
|
|
224
|
+
def _set_prompt(self, text: str) -> None:
|
|
225
|
+
if text != self.prompt_text:
|
|
226
|
+
self.activity_text = ""
|
|
227
|
+
self.prompt_text = text
|
|
262
228
|
|
|
263
229
|
def render(
|
|
264
230
|
self, output_mode: str = CARD_OUTPUT_MODE_MARKDOWN
|
|
265
231
|
) -> "typing.Dict[str, object]":
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
if self.
|
|
271
|
-
status_line += " - " + _escape_markdown(self.error)
|
|
272
|
-
input_disabled = self.running or self.detached
|
|
273
|
-
sender = _escape_markdown(self.last_sender or "cli")
|
|
274
|
-
prompt = _truncate(self.last_prompt, 1200) or "-"
|
|
275
|
-
output = _truncate(self.output_text, CARD_OUTPUT_LIMIT) or (
|
|
276
|
-
"Waiting for output..." if self.running else "Ready."
|
|
277
|
-
)
|
|
278
|
-
output_content = _render_output_content(output, output_mode)
|
|
279
|
-
working_output = _truncate(self.working_output_text, CARD_OUTPUT_LIMIT)
|
|
280
|
-
color, title = _status_template(self.status)
|
|
232
|
+
status = "Closed" if self.display.closed else self.status
|
|
233
|
+
input_disabled = not self.accepts_input or self.detached or self.display.closed
|
|
234
|
+
output = _truncate(self.output_text, CARD_OUTPUT_LIMIT) or "Ready."
|
|
235
|
+
working_output = _truncate(self.display.stream_buffer, CARD_OUTPUT_LIMIT)
|
|
236
|
+
color = _status_color("Detached" if self.detached else status or "Idle")
|
|
281
237
|
body_elements = [
|
|
282
|
-
|
|
283
|
-
"
|
|
284
|
-
"
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
238
|
+
_output_box(
|
|
239
|
+
"answer_box",
|
|
240
|
+
"answer_md",
|
|
241
|
+
_render_output_content(output, output_mode),
|
|
242
|
+
"grey-50",
|
|
243
|
+
),
|
|
288
244
|
]
|
|
289
245
|
if working_output:
|
|
290
246
|
body_elements.append(
|
|
@@ -295,11 +251,49 @@ class PycodexCard:
|
|
|
295
251
|
"green-50",
|
|
296
252
|
)
|
|
297
253
|
)
|
|
254
|
+
if self.activity_text:
|
|
255
|
+
body_elements.append(
|
|
256
|
+
{
|
|
257
|
+
"tag": "markdown",
|
|
258
|
+
"element_id": "activity_md",
|
|
259
|
+
"content": _render_output_content(self.activity_text, output_mode),
|
|
260
|
+
}
|
|
261
|
+
)
|
|
262
|
+
if self.user_prompt:
|
|
263
|
+
body_elements.insert(
|
|
264
|
+
0,
|
|
265
|
+
{
|
|
266
|
+
"tag": "markdown",
|
|
267
|
+
"element_id": "user_prompt_md",
|
|
268
|
+
"content": _render_output_content(
|
|
269
|
+
"user> " + _truncate(self.user_prompt, CARD_OUTPUT_LIMIT),
|
|
270
|
+
output_mode,
|
|
271
|
+
),
|
|
272
|
+
},
|
|
273
|
+
)
|
|
274
|
+
history = self.earlier_turns()
|
|
275
|
+
if history:
|
|
276
|
+
index = (
|
|
277
|
+
len(history) - 1 if self.history_index is None else self.history_index
|
|
278
|
+
)
|
|
279
|
+
body_elements.insert(
|
|
280
|
+
0,
|
|
281
|
+
_history_panel(
|
|
282
|
+
history,
|
|
283
|
+
index,
|
|
284
|
+
self.history_index is not None,
|
|
285
|
+
output_mode,
|
|
286
|
+
not self.detached,
|
|
287
|
+
),
|
|
288
|
+
)
|
|
298
289
|
card = {
|
|
299
290
|
"schema": "2.0",
|
|
300
291
|
"config": {"update_multi": True},
|
|
301
292
|
"header": {
|
|
302
|
-
"title": {
|
|
293
|
+
"title": {
|
|
294
|
+
"tag": "plain_text",
|
|
295
|
+
"content": self.display.title or "pycodex",
|
|
296
|
+
},
|
|
303
297
|
"template": color,
|
|
304
298
|
},
|
|
305
299
|
"body": {
|
|
@@ -317,7 +311,7 @@ class PycodexCard:
|
|
|
317
311
|
"disabled": input_disabled,
|
|
318
312
|
"placeholder": {
|
|
319
313
|
"tag": "plain_text",
|
|
320
|
-
"content":
|
|
314
|
+
"content": self.prompt_text + (status or self.model_name),
|
|
321
315
|
},
|
|
322
316
|
"behaviors": [{"type": "callback", "value": {"action": "send"}}],
|
|
323
317
|
"value": {"action": "send"},
|
|
@@ -325,6 +319,16 @@ class PycodexCard:
|
|
|
325
319
|
)
|
|
326
320
|
return card
|
|
327
321
|
|
|
322
|
+
def earlier_turns(self):
|
|
323
|
+
if self._active_turn_id is not None:
|
|
324
|
+
return self.turn_history
|
|
325
|
+
return self.turn_history[:-1]
|
|
326
|
+
|
|
327
|
+
def show_history(self, index):
|
|
328
|
+
if type(index) is not int or not 0 <= index < len(self.earlier_turns()):
|
|
329
|
+
raise ValueError("This history turn is no longer available.")
|
|
330
|
+
self.history_index = index
|
|
331
|
+
|
|
328
332
|
def parse_action(self, sdk_event) -> "typing.Dict[str, object]":
|
|
329
333
|
event_data = getattr(sdk_event, "event", None)
|
|
330
334
|
action_payload = getattr(event_data, "action", None)
|
|
@@ -360,6 +364,7 @@ class PycodexCard:
|
|
|
360
364
|
self.session_key = _default_session_key(tenant_key, open_id, message_id)
|
|
361
365
|
return {
|
|
362
366
|
"action": action,
|
|
367
|
+
"history_index": value.get("history_index"),
|
|
363
368
|
"prompt": prompt,
|
|
364
369
|
"sender": self.resolve_operator_name(operator),
|
|
365
370
|
}
|
|
@@ -470,10 +475,9 @@ class PycodexCard:
|
|
|
470
475
|
)
|
|
471
476
|
|
|
472
477
|
def user_access_token(self) -> "typing.Union[str, None]":
|
|
473
|
-
refresh_token = _read_refresh_token()
|
|
478
|
+
refresh_token = _read_refresh_token()
|
|
474
479
|
if not refresh_token:
|
|
475
480
|
return None
|
|
476
|
-
self.refresh_token = refresh_token
|
|
477
481
|
now = time.time()
|
|
478
482
|
if self._user_access_token and now < self._user_token_expires_at:
|
|
479
483
|
return self._user_access_token
|
|
@@ -493,14 +497,12 @@ class PycodexCard:
|
|
|
493
497
|
token = payload.get("access_token")
|
|
494
498
|
if not token:
|
|
495
499
|
raise RuntimeError("user access_token missing from Feishu response")
|
|
500
|
+
refresh_token = payload.get("refresh_token")
|
|
501
|
+
if refresh_token:
|
|
502
|
+
_write_refresh_token(str(refresh_token))
|
|
496
503
|
self._user_access_token = str(token)
|
|
497
504
|
expires_in = payload.get("expires_in") or 0
|
|
498
505
|
self._user_token_expires_at = time.time() + max(60, int(expires_in) - 120)
|
|
499
|
-
refresh_token = payload.get("refresh_token")
|
|
500
|
-
if refresh_token:
|
|
501
|
-
self.refresh_token = str(refresh_token)
|
|
502
|
-
os.environ["FEISHU_REFRESH_TOKEN"] = self.refresh_token
|
|
503
|
-
_write_refresh_token(self.refresh_token)
|
|
504
506
|
return self._user_access_token
|
|
505
507
|
|
|
506
508
|
def tenant_access_token(self) -> str:
|
|
@@ -601,15 +603,13 @@ def _display_user_name(user: "typing.Any") -> "typing.Union[str, None]":
|
|
|
601
603
|
return None
|
|
602
604
|
|
|
603
605
|
|
|
604
|
-
def
|
|
606
|
+
def _status_color(status: str) -> str:
|
|
605
607
|
normalized = status.lower()
|
|
606
|
-
if normalized in {"
|
|
607
|
-
return "
|
|
608
|
-
if normalized in {"
|
|
609
|
-
return "
|
|
610
|
-
|
|
611
|
-
return "grey", "Session Detached"
|
|
612
|
-
return "green", "Session Connected"
|
|
608
|
+
if normalized in {"detached", "closed"}:
|
|
609
|
+
return "grey"
|
|
610
|
+
if normalized in {"idle", "idle: sleeping"}:
|
|
611
|
+
return "green"
|
|
612
|
+
return "blue"
|
|
613
613
|
|
|
614
614
|
|
|
615
615
|
def _truncate(text: str, limit: int) -> str:
|
|
@@ -629,6 +629,65 @@ def _render_output_content(output: str, output_mode: str) -> str:
|
|
|
629
629
|
return output
|
|
630
630
|
|
|
631
631
|
|
|
632
|
+
def _history_panel(history, index, expanded, output_mode, interactive):
|
|
633
|
+
prompt, response = history[index]
|
|
634
|
+
content = _truncate(
|
|
635
|
+
"user> {0}\n\nassistant> {1}".format(prompt, response), CARD_OUTPUT_LIMIT
|
|
636
|
+
)
|
|
637
|
+
columns = []
|
|
638
|
+
for label, target, disabled in (
|
|
639
|
+
("Previous", index - 1, index == 0),
|
|
640
|
+
("Next", index + 1, index == len(history) - 1),
|
|
641
|
+
):
|
|
642
|
+
columns.append(
|
|
643
|
+
{
|
|
644
|
+
"tag": "column",
|
|
645
|
+
"width": "auto",
|
|
646
|
+
"elements": [
|
|
647
|
+
{
|
|
648
|
+
"tag": "button",
|
|
649
|
+
"text": {"tag": "plain_text", "content": label},
|
|
650
|
+
"type": "default",
|
|
651
|
+
"disabled": disabled or not interactive,
|
|
652
|
+
"behaviors": [
|
|
653
|
+
{
|
|
654
|
+
"type": "callback",
|
|
655
|
+
"value": {
|
|
656
|
+
"action": "history",
|
|
657
|
+
"history_index": target,
|
|
658
|
+
},
|
|
659
|
+
}
|
|
660
|
+
],
|
|
661
|
+
}
|
|
662
|
+
],
|
|
663
|
+
}
|
|
664
|
+
)
|
|
665
|
+
return {
|
|
666
|
+
"tag": "collapsible_panel",
|
|
667
|
+
"element_id": "history_panel",
|
|
668
|
+
"expanded": expanded,
|
|
669
|
+
"header": {
|
|
670
|
+
"icon": {
|
|
671
|
+
"tag": "standard_icon",
|
|
672
|
+
"token": "down-small-ccm_outlined",
|
|
673
|
+
"size": "16px 16px",
|
|
674
|
+
},
|
|
675
|
+
"title": {
|
|
676
|
+
"tag": "plain_text",
|
|
677
|
+
"content": "History · {0} / {1}".format(index + 1, len(history)),
|
|
678
|
+
},
|
|
679
|
+
},
|
|
680
|
+
"elements": [
|
|
681
|
+
{
|
|
682
|
+
"tag": "markdown",
|
|
683
|
+
"element_id": "history_md",
|
|
684
|
+
"content": _render_output_content(content, output_mode),
|
|
685
|
+
},
|
|
686
|
+
{"tag": "column_set", "columns": columns},
|
|
687
|
+
],
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
|
|
632
691
|
def _output_box(
|
|
633
692
|
box_id: str,
|
|
634
693
|
markdown_id: str,
|
|
@@ -664,10 +723,6 @@ def _is_card_content_error(exc: "BaseException") -> bool:
|
|
|
664
723
|
return CARD_CONTENT_ERROR_MARKER in str(exc)
|
|
665
724
|
|
|
666
725
|
|
|
667
|
-
def _escape_markdown(text: str) -> str:
|
|
668
|
-
return str(text or "").replace("`", "'")
|
|
669
|
-
|
|
670
|
-
|
|
671
726
|
def _dig(value: "typing.Any", *path: str) -> "typing.Any":
|
|
672
727
|
current = value
|
|
673
728
|
for key in path:
|
|
@@ -700,11 +755,10 @@ def _read_refresh_token() -> "typing.Union[str, None]":
|
|
|
700
755
|
def _write_refresh_token(value: str) -> None:
|
|
701
756
|
path = _refresh_token_path()
|
|
702
757
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
703
|
-
|
|
704
|
-
|
|
758
|
+
descriptor = os.open(str(path), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
|
759
|
+
with os.fdopen(descriptor, "w", encoding="utf-8") as stream:
|
|
705
760
|
path.chmod(0o600)
|
|
706
|
-
|
|
707
|
-
pass
|
|
761
|
+
stream.write("{0}\n".format(value))
|
|
708
762
|
|
|
709
763
|
|
|
710
764
|
def _api_base_to_domain(api_base: str) -> str:
|