python-codex 0.3.3__py3-none-any.whl → 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. pycodex/__init__.py +3 -27
  2. pycodex/bootstrap.py +5 -45
  3. pycodex/cli.py +1 -14
  4. pycodex/context.py +1 -1
  5. pycodex/events.py +1 -1
  6. pycodex/model.py +2 -2
  7. pycodex/portable/__init__.py +45 -0
  8. pycodex/{portable.py → portable/client.py} +4 -7
  9. pycodex/{portable_server.py → portable/server.py} +3 -3
  10. pycodex/protocol.py +1 -1
  11. pycodex/runtime.py +1 -1
  12. pycodex/tools/base_tool.py +1 -1
  13. pycodex/tools/close_agent_tool.py +1 -1
  14. pycodex/tools/code_mode_manager.py +1 -1
  15. pycodex/tools/request_permissions_tool.py +1 -1
  16. pycodex/tools/resume_agent_tool.py +1 -1
  17. pycodex/{runtime_services.py → tools/runtime_services.py} +5 -5
  18. pycodex/tools/send_input_tool.py +1 -1
  19. pycodex/tools/spawn_agent_tool.py +1 -1
  20. pycodex/tools/unified_exec_manager.py +1 -1
  21. pycodex/tools/update_plan_tool.py +1 -1
  22. pycodex/tools/view_image_tool.py +5 -0
  23. pycodex/tools/wait_agent_tool.py +1 -1
  24. pycodex/utils/__init__.py +0 -4
  25. pycodex/utils/_aes_gcm/LICENSE +56 -0
  26. pycodex/utils/_aes_gcm/README.md +61 -0
  27. pycodex/utils/_aes_gcm/__init__.py +1 -0
  28. pycodex/utils/_aes_gcm/aes.py +175 -0
  29. pycodex/utils/_aes_gcm/gcm.py +73 -0
  30. pycodex/utils/_aes_gcm/source-sha256.json +4 -0
  31. pycodex/{compat.py → utils/compat.py} +2 -0
  32. pycodex/utils/crypto.py +35 -0
  33. pycodex/utils/debug.py +24 -0
  34. pycodex/{doctor.py → utils/doctor.py} +3 -3
  35. pycodex/utils/image_utils.py +3 -2
  36. pycodex/{model_metadata.py → utils/model_metadata.py} +2 -2
  37. {python_codex-0.3.3.dist-info → python_codex-0.4.0.dist-info}/METADATA +120 -42
  38. {python_codex-0.3.3.dist-info → python_codex-0.4.0.dist-info}/RECORD +42 -50
  39. {python_codex-0.3.3.dist-info → python_codex-0.4.0.dist-info}/entry_points.txt +0 -1
  40. responses_server/app.py +28 -15
  41. pycodex/feishu_card.py +0 -793
  42. pycodex/feishu_link.py +0 -440
  43. pycodex/tools/ipython_tool.py +0 -142
  44. workspace_server/__init__.py +0 -39
  45. workspace_server/__main__.py +0 -4
  46. workspace_server/app.py +0 -1906
  47. workspace_server/content_views.LICENSE.txt +0 -470
  48. workspace_server/content_views.css +0 -1
  49. workspace_server/content_views.mjs +0 -25
  50. workspace_server/editor.css +0 -1
  51. workspace_server/editor.mjs +0 -1260
  52. workspace_server/owned_process.py +0 -27
  53. workspace_server/workspace.html +0 -2267
  54. workspace_server/workspace_views.py +0 -533
  55. workspace_server/workspaces.html +0 -550
  56. workspace_server/workspaces.py +0 -632
  57. {python_codex-0.3.3.dist-info → python_codex-0.4.0.dist-info}/WHEEL +0 -0
  58. {python_codex-0.3.3.dist-info → python_codex-0.4.0.dist-info}/licenses/LICENSE +0 -0
pycodex/feishu_card.py DELETED
@@ -1,793 +0,0 @@
1
- import json
2
- import os
3
- import time
4
- import typing
5
- from pathlib import Path
6
-
7
- import requests
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
-
20
- FEISHU_API_BASE = "https://open.feishu.cn/open-apis"
21
- FEISHU_DOMAIN = "https://open.feishu.cn"
22
- FEISHU_REFRESH_TOKEN_FILE = "~/.codex/.feishu_refresh_token"
23
- CARD_OUTPUT_LIMIT = 6500
24
- CARD_OUTPUT_MODE_MARKDOWN = "markdown"
25
- CARD_OUTPUT_MODE_CODE = "code"
26
- CARD_CONTENT_ERROR_MARKER = "Failed to create card content"
27
-
28
-
29
- class PycodexCard:
30
- def __init__(
31
- self,
32
- app_id: "typing.Union[str, None]" = None,
33
- app_secret: "typing.Union[str, None]" = None,
34
- api_base: str = FEISHU_API_BASE,
35
- domain: str = FEISHU_DOMAIN,
36
- verification_token: "typing.Union[str, None]" = None,
37
- encrypt_key: "typing.Union[str, None]" = None,
38
- session: "typing.Union[requests.Session, None]" = None,
39
- ) -> None:
40
- self.app_id = app_id
41
- self.app_secret = app_secret
42
- self.api_base = api_base
43
- self.domain = domain
44
- self.verification_token = verification_token
45
- self.encrypt_key = encrypt_key
46
- self.session = session or requests.Session()
47
- self.message_id = None
48
- self.callback_token = None
49
- self.session_key = None
50
- self.status = None
51
- self.prompt_text = DEFAULT_MAIN_PROMPT
52
- self.model_name = "pycodex"
53
- self.output_text = ""
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)
61
- self.detached = False
62
- self.accepts_input = True
63
- self._user_access_token = None
64
- self._user_token_expires_at = 0.0
65
- self._tenant_token = None
66
- self._tenant_token_expires_at = 0.0
67
-
68
- @classmethod
69
- def from_env(cls) -> "PycodexCard":
70
- api_base = os.environ.get("FEISHU_API_BASE", FEISHU_API_BASE)
71
- return cls(
72
- app_id=_env("FEISHU_APP_ID", "LARK_APP_ID"),
73
- app_secret=_env("FEISHU_APP_SECRET", "LARK_APP_SECRET"),
74
- api_base=api_base,
75
- domain=os.environ.get("FEISHU_DOMAIN", _api_base_to_domain(api_base)),
76
- verification_token=_env(
77
- "FEISHU_VERIFICATION_TOKEN",
78
- "LARK_VERIFICATION_TOKEN",
79
- ),
80
- encrypt_key=_env("FEISHU_ENCRYPT_KEY", "LARK_ENCRYPT_KEY"),
81
- )
82
-
83
- def configured(self) -> bool:
84
- return bool(self.app_id and self.app_secret)
85
-
86
- def send(self, target: str) -> None:
87
- receive_id = str(target or "").strip()
88
- if not receive_id:
89
- raise ValueError("recipient target is required")
90
- receive_id = self.resolve_name(receive_id)
91
- if not receive_id:
92
- raise ValueError(
93
- "cannot resolve Feishu user from target: {0}".format(target)
94
- )
95
- if receive_id.startswith("oc_"):
96
- resolved_type = "chat_id"
97
- else:
98
- resolved_type = "open_id"
99
-
100
- self.session_key = "feishu:manual:{0}:{1}".format(resolved_type, receive_id)
101
-
102
- def build_body(output_mode):
103
- return {
104
- "receive_id": receive_id,
105
- "msg_type": "interactive",
106
- "content": json.dumps(self.render(output_mode), ensure_ascii=False),
107
- }
108
-
109
- response = self._request_rendered_card(
110
- "POST",
111
- "/im/v1/messages",
112
- {"receive_id_type": resolved_type},
113
- build_body,
114
- )
115
- self.message_id = _extract_message_id(response)
116
-
117
- def update(
118
- self,
119
- message_id: "typing.Union[str, None]" = None,
120
- callback_token: "typing.Union[str, None]" = None,
121
- ) -> None:
122
- if message_id:
123
- self.message_id = message_id
124
- if callback_token:
125
- self.callback_token = callback_token
126
- if not self.configured():
127
- return
128
- if self.message_id:
129
-
130
- def build_body(output_mode):
131
- return {
132
- "content": json.dumps(
133
- self.render(output_mode),
134
- ensure_ascii=False,
135
- )
136
- }
137
-
138
- self._request_rendered_card(
139
- "PATCH",
140
- "/im/v1/messages/{0}".format(self.message_id),
141
- None,
142
- build_body,
143
- )
144
- return
145
- if self.callback_token:
146
-
147
- def build_body(output_mode):
148
- return {"token": self.callback_token, "card": self.render(output_mode)}
149
-
150
- self._request_rendered_card(
151
- "POST",
152
- "/interactive/v1/card/update",
153
- None,
154
- build_body,
155
- use_user_token=False,
156
- )
157
-
158
- def detach(self) -> None:
159
- self.detached = True
160
-
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)):
191
- self._mark_last_turn_output()
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
210
-
211
- def _mark_last_turn_output(self) -> None:
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
215
-
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:]
220
-
221
- def _set_status(self, text: "typing.Union[str, None]") -> None:
222
- self.status = text
223
-
224
- def _set_prompt(self, text: str) -> None:
225
- if text != self.prompt_text:
226
- self.activity_text = ""
227
- self.prompt_text = text
228
-
229
- def render(
230
- self, output_mode: str = CARD_OUTPUT_MODE_MARKDOWN
231
- ) -> "typing.Dict[str, object]":
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")
237
- body_elements = [
238
- _output_box(
239
- "answer_box",
240
- "answer_md",
241
- _render_output_content(output, output_mode),
242
- "grey-50",
243
- ),
244
- ]
245
- if working_output:
246
- body_elements.append(
247
- _output_box(
248
- "working_output_box",
249
- "working_output_md",
250
- _render_output_content(working_output, output_mode),
251
- "green-50",
252
- )
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
- )
289
- card = {
290
- "schema": "2.0",
291
- "config": {"update_multi": True},
292
- "header": {
293
- "title": {
294
- "tag": "plain_text",
295
- "content": self.display.title or "pycodex",
296
- },
297
- "template": color,
298
- },
299
- "body": {
300
- "elements": body_elements,
301
- },
302
- }
303
- if not self.detached:
304
- card["body"]["elements"].append(
305
- {
306
- "tag": "input",
307
- "element_id": "prompt_input",
308
- "name": "prompt",
309
- "input_type": "text",
310
- "width": "fill",
311
- "disabled": input_disabled,
312
- "placeholder": {
313
- "tag": "plain_text",
314
- "content": self.prompt_text + (status or self.model_name),
315
- },
316
- "behaviors": [{"type": "callback", "value": {"action": "send"}}],
317
- "value": {"action": "send"},
318
- },
319
- )
320
- return card
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
-
332
- def parse_action(self, sdk_event) -> "typing.Dict[str, object]":
333
- event_data = getattr(sdk_event, "event", None)
334
- action_payload = getattr(event_data, "action", None)
335
- operator = getattr(event_data, "operator", None)
336
- context = getattr(event_data, "context", None)
337
- form_values = _sdk_form_values(action_payload)
338
- value = getattr(action_payload, "value", None) or {}
339
- if not isinstance(value, dict):
340
- value = {}
341
- prompt = str(
342
- form_values.get("prompt")
343
- or form_values.get("prompt_input")
344
- or getattr(action_payload, "input_value", None)
345
- or value.get("input_value")
346
- or ""
347
- ).strip()
348
- if not prompt and form_values:
349
- prompt = str(next(iter(form_values.values()))).strip()
350
- action = str(
351
- value.get("action") or getattr(action_payload, "name", None) or "send"
352
- ).strip()
353
- if action in {"send_button", "prompt_form"}:
354
- action = "send"
355
- message_id = (
356
- str(getattr(context, "open_message_id", None) or "").strip() or None
357
- )
358
- callback_token = str(getattr(event_data, "token", None) or "").strip() or None
359
- self.message_id = message_id or self.message_id
360
- self.callback_token = callback_token or self.callback_token
361
- if not self.session_key:
362
- tenant_key = str(getattr(operator, "tenant_key", None) or "").strip()
363
- open_id = str(getattr(operator, "open_id", None) or "").strip()
364
- self.session_key = _default_session_key(tenant_key, open_id, message_id)
365
- return {
366
- "action": action,
367
- "history_index": value.get("history_index"),
368
- "prompt": prompt,
369
- "sender": self.resolve_operator_name(operator),
370
- }
371
-
372
- def resolve_name(self, name: str) -> "typing.Union[str, None]":
373
- normalized = str(name or "").strip()
374
- if not normalized:
375
- return None
376
- aliases = _user_id_aliases()
377
- if normalized in aliases:
378
- return aliases[normalized]
379
- if normalized.startswith("ou_"):
380
- return normalized
381
- if normalized.startswith("oc_"):
382
- return normalized
383
- if normalized.isnumeric():
384
- return self._lookup_user({"mobiles": [normalized]})
385
- if "@" not in normalized:
386
- email_domain = _default_email_domain()
387
- if not email_domain:
388
- return None
389
- normalized = normalized + "@" + email_domain
390
- return self._lookup_user({"emails": [normalized]})
391
-
392
- def _lookup_user(
393
- self, body: "typing.Dict[str, object]"
394
- ) -> "typing.Union[str, None]":
395
- payload = dict(body)
396
- payload["include_resigned"] = False
397
- response = self._request(
398
- "POST",
399
- "/contact/v3/users/batch_get_id",
400
- params={"user_id_type": "open_id"},
401
- json_body=payload,
402
- use_user_token=False,
403
- )
404
- user_list = _dig(response, "data", "user_list")
405
- if not isinstance(user_list, list) or not user_list:
406
- return None
407
- item = user_list[0] if isinstance(user_list[0], dict) else {}
408
- value = item.get("user_id") or item.get("open_id") or item.get("union_id")
409
- return str(value).strip() if value else None
410
-
411
- def resolve_operator_name(self, operator) -> str:
412
- for user_id_type in ("user_id", "open_id", "union_id"):
413
- user_id = str(getattr(operator, user_id_type, None) or "").strip()
414
- if not user_id:
415
- continue
416
- try:
417
- response = self._request(
418
- "GET",
419
- "/contact/v3/users/{0}".format(user_id),
420
- params={"user_id_type": user_id_type},
421
- use_user_token=False,
422
- )
423
- except Exception:
424
- continue
425
- name = _display_user_name(_dig(response, "data", "user") or response)
426
- if name:
427
- return name
428
- return "cli"
429
-
430
- def _request(
431
- self,
432
- method: str,
433
- path: str,
434
- params: "typing.Union[typing.Dict[str, str], None]" = None,
435
- json_body: "typing.Union[typing.Dict[str, object], None]" = None,
436
- use_user_token: bool = True,
437
- ) -> "typing.Dict[str, object]":
438
- user_token = self.user_access_token() if use_user_token else None
439
- token = user_token or self.tenant_access_token()
440
- response = self.session.request(
441
- method,
442
- self.api_base.rstrip("/") + path,
443
- params=params,
444
- json=json_body,
445
- headers={"Authorization": "Bearer {0}".format(token)},
446
- timeout=20,
447
- )
448
- return _checked_json_response(response)
449
-
450
- def _request_rendered_card(
451
- self,
452
- method: str,
453
- path: str,
454
- params: "typing.Union[typing.Dict[str, str], None]",
455
- json_body_builder: "typing.Callable[[str], typing.Dict[str, object]]",
456
- use_user_token: bool = True,
457
- ) -> "typing.Dict[str, object]":
458
- try:
459
- return self._request(
460
- method,
461
- path,
462
- params=params,
463
- json_body=json_body_builder(CARD_OUTPUT_MODE_MARKDOWN),
464
- use_user_token=use_user_token,
465
- )
466
- except Exception as exc:
467
- if not _is_card_content_error(exc):
468
- raise
469
- return self._request(
470
- method,
471
- path,
472
- params=params,
473
- json_body=json_body_builder(CARD_OUTPUT_MODE_CODE),
474
- use_user_token=use_user_token,
475
- )
476
-
477
- def user_access_token(self) -> "typing.Union[str, None]":
478
- refresh_token = _read_refresh_token()
479
- if not refresh_token:
480
- return None
481
- now = time.time()
482
- if self._user_access_token and now < self._user_token_expires_at:
483
- return self._user_access_token
484
- if not self.configured():
485
- raise RuntimeError("FEISHU_APP_ID and FEISHU_APP_SECRET are required")
486
- response = self.session.post(
487
- self.api_base.rstrip("/") + "/authen/v2/oauth/token",
488
- json={
489
- "grant_type": "refresh_token",
490
- "client_id": self.app_id,
491
- "client_secret": self.app_secret,
492
- "refresh_token": refresh_token,
493
- },
494
- timeout=20,
495
- )
496
- payload = _checked_json_response(response)
497
- token = payload.get("access_token")
498
- if not token:
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))
503
- self._user_access_token = str(token)
504
- expires_in = payload.get("expires_in") or 0
505
- self._user_token_expires_at = time.time() + max(60, int(expires_in) - 120)
506
- return self._user_access_token
507
-
508
- def tenant_access_token(self) -> str:
509
- now = time.time()
510
- if self._tenant_token and now < self._tenant_token_expires_at:
511
- return self._tenant_token
512
- response = self.session.post(
513
- self.api_base.rstrip("/") + "/auth/v3/tenant_access_token/internal",
514
- json={"app_id": self.app_id, "app_secret": self.app_secret},
515
- timeout=20,
516
- )
517
- payload = _checked_json_response(response)
518
- token = _dig(payload, "tenant_access_token") or _dig(
519
- payload,
520
- "data",
521
- "tenant_access_token",
522
- )
523
- if not token:
524
- raise RuntimeError("tenant_access_token missing from Feishu response")
525
- expires_in = _dig(payload, "expire") or _dig(payload, "data", "expire") or 3600
526
- self._tenant_token = str(token)
527
- self._tenant_token_expires_at = now + max(60, int(expires_in) - 120)
528
- return self._tenant_token
529
-
530
-
531
- def _sdk_form_values(action_payload: "typing.Any") -> "typing.Dict[str, object]":
532
- for name in ("form_value", "form_values", "input_values"):
533
- value = getattr(action_payload, name, None)
534
- if isinstance(value, dict):
535
- return value
536
- value = getattr(action_payload, "value", None)
537
- if isinstance(value, dict):
538
- for name in ("form_value", "form_values", "input_values"):
539
- nested = value.get(name)
540
- if isinstance(nested, dict):
541
- return nested
542
- return {}
543
-
544
-
545
- def _checked_json_response(response) -> "typing.Dict[str, object]":
546
- try:
547
- payload = response.json()
548
- except Exception as exc:
549
- raise RuntimeError(
550
- "Feishu API returned non-JSON response: status={0} body={1}".format(
551
- getattr(response, "status_code", "?"),
552
- getattr(response, "text", ""),
553
- )
554
- ) from exc
555
- if getattr(response, "status_code", 200) >= 400:
556
- raise RuntimeError(
557
- "Feishu API error: status={0} body={1}".format(
558
- response.status_code,
559
- json.dumps(payload, ensure_ascii=False),
560
- )
561
- )
562
- code = payload.get("code")
563
- if code not in (None, 0):
564
- raise RuntimeError(
565
- "Feishu API error: {0}".format(json.dumps(payload, ensure_ascii=False))
566
- )
567
- return payload
568
-
569
-
570
- def _extract_message_id(
571
- response: "typing.Dict[str, object]",
572
- ) -> "typing.Union[str, None]":
573
- for path in (
574
- ("data", "message_id"),
575
- ("data", "message", "message_id"),
576
- ("message_id",),
577
- ("open_message_id",),
578
- ):
579
- value = _dig(response, *path)
580
- if value:
581
- return str(value)
582
- return None
583
-
584
-
585
- def _default_session_key(
586
- tenant_key: str,
587
- open_id: str,
588
- message_id: "typing.Union[str, None]",
589
- ) -> str:
590
- user_key = open_id or "unknown"
591
- if message_id:
592
- return "feishu:{0}:{1}:{2}".format(tenant_key or "tenant", user_key, message_id)
593
- return "feishu:{0}:{1}:default".format(tenant_key or "tenant", user_key)
594
-
595
-
596
- def _display_user_name(user: "typing.Any") -> "typing.Union[str, None]":
597
- if not isinstance(user, dict):
598
- return None
599
- for key in ("name", "en_name", "nickname", "email", "enterprise_email"):
600
- value = str(user.get(key) or "").strip()
601
- if value:
602
- return value
603
- return None
604
-
605
-
606
- def _status_color(status: str) -> str:
607
- normalized = status.lower()
608
- if normalized in {"detached", "closed"}:
609
- return "grey"
610
- if normalized in {"idle", "idle: sleeping"}:
611
- return "green"
612
- return "blue"
613
-
614
-
615
- def _truncate(text: str, limit: int) -> str:
616
- value = str(text or "")
617
- if len(value) <= limit:
618
- return value
619
- return value[: max(0, limit - 32)] + "\n...[truncated]"
620
-
621
-
622
- def _escape_code_block(text: str) -> str:
623
- return str(text or "").replace("```", "'''")
624
-
625
-
626
- def _render_output_content(output: str, output_mode: str) -> str:
627
- if output_mode == CARD_OUTPUT_MODE_CODE:
628
- return "```text\n{0}\n```".format(_escape_code_block(output))
629
- return output
630
-
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
-
691
- def _output_box(
692
- box_id: str,
693
- markdown_id: str,
694
- content: str,
695
- background_style: str,
696
- ) -> "typing.Dict[str, object]":
697
- return {
698
- "tag": "column_set",
699
- "element_id": box_id,
700
- "background_style": background_style,
701
- "horizontal_spacing": "8px",
702
- "horizontal_align": "left",
703
- "columns": [
704
- {
705
- "tag": "column",
706
- "width": "auto",
707
- "elements": [
708
- {
709
- "tag": "markdown",
710
- "element_id": markdown_id,
711
- "content": content,
712
- },
713
- ],
714
- "vertical_spacing": "8px",
715
- "horizontal_align": "left",
716
- "vertical_align": "top",
717
- }
718
- ],
719
- }
720
-
721
-
722
- def _is_card_content_error(exc: "BaseException") -> bool:
723
- return CARD_CONTENT_ERROR_MARKER in str(exc)
724
-
725
-
726
- def _dig(value: "typing.Any", *path: str) -> "typing.Any":
727
- current = value
728
- for key in path:
729
- if not isinstance(current, dict):
730
- return None
731
- current = current.get(key)
732
- return current
733
-
734
-
735
- def _env(*names: str) -> "typing.Union[str, None]":
736
- for name in names:
737
- value = os.environ.get(name)
738
- if value:
739
- return value
740
- return None
741
-
742
-
743
- def _refresh_token_path() -> Path:
744
- return Path(FEISHU_REFRESH_TOKEN_FILE).expanduser()
745
-
746
-
747
- def _read_refresh_token() -> "typing.Union[str, None]":
748
- path = _refresh_token_path()
749
- if not path.exists():
750
- return None
751
- value = path.read_text(encoding="utf-8", errors="replace").strip()
752
- return value or None
753
-
754
-
755
- def _write_refresh_token(value: str) -> None:
756
- path = _refresh_token_path()
757
- path.parent.mkdir(parents=True, exist_ok=True)
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:
760
- path.chmod(0o600)
761
- stream.write("{0}\n".format(value))
762
-
763
-
764
- def _api_base_to_domain(api_base: str) -> str:
765
- base = str(api_base or FEISHU_API_BASE).rstrip("/")
766
- marker = "/open-apis"
767
- if base.endswith(marker):
768
- return base[: -len(marker)]
769
- return base
770
-
771
-
772
- def _default_email_domain() -> "typing.Union[str, None]":
773
- value = _env(
774
- "PYCODEX_FEISHU_DEFAULT_EMAIL_DOMAIN",
775
- "FEISHU_DEFAULT_EMAIL_DOMAIN",
776
- "LARK_DEFAULT_EMAIL_DOMAIN",
777
- )
778
- if not value:
779
- return None
780
- return str(value).strip().lstrip("@") or None
781
-
782
-
783
- def _user_id_aliases() -> "typing.Dict[str, str]":
784
- raw = os.environ.get("PYCODEX_FEISHU_USER_IDS")
785
- if not raw:
786
- return {}
787
- try:
788
- payload = json.loads(raw)
789
- except Exception:
790
- return {}
791
- if not isinstance(payload, dict):
792
- return {}
793
- return {str(key): str(value) for key, value in payload.items() if key and value}