python-codex 0.1.12__py3-none-any.whl → 0.1.13__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/feishu_card.py ADDED
@@ -0,0 +1,693 @@
1
+ import json
2
+ import os
3
+ from pathlib import Path
4
+ import time
5
+ import typing
6
+
7
+ import requests
8
+
9
+ FEISHU_API_BASE = "https://open.feishu.cn/open-apis"
10
+ FEISHU_DOMAIN = "https://open.feishu.cn"
11
+ FEISHU_REFRESH_TOKEN_FILE = "~/.codex/.feishu_refresh_token"
12
+ CARD_OUTPUT_LIMIT = 6500
13
+ CARD_OUTPUT_MODE_MARKDOWN = "markdown"
14
+ CARD_OUTPUT_MODE_CODE = "code"
15
+ CARD_CONTENT_ERROR_MARKER = "Failed to create card content"
16
+ LAST_TURN_PREFIX = "(*last turn)\n"
17
+
18
+
19
+ class PycodexCard:
20
+ def __init__(
21
+ self,
22
+ app_id: "typing.Union[str, None]" = None,
23
+ app_secret: "typing.Union[str, None]" = None,
24
+ api_base: str = FEISHU_API_BASE,
25
+ domain: str = FEISHU_DOMAIN,
26
+ verification_token: "typing.Union[str, None]" = None,
27
+ encrypt_key: "typing.Union[str, None]" = None,
28
+ refresh_token: "typing.Union[str, None]" = None,
29
+ session: "typing.Union[requests.Session, None]" = None,
30
+ ) -> None:
31
+ self.app_id = app_id
32
+ self.app_secret = app_secret
33
+ self.api_base = api_base
34
+ self.domain = domain
35
+ self.verification_token = verification_token
36
+ self.encrypt_key = encrypt_key
37
+ self.refresh_token = refresh_token
38
+ self.session = session or requests.Session()
39
+ self.message_id = None
40
+ self.callback_token = None
41
+ self.session_key = None
42
+ self.status = "Idle"
43
+ self.status_detail = "Ready."
44
+ self.last_sender = "cli"
45
+ self.last_prompt = ""
46
+ self.model_name = "pycodex"
47
+ self.output_text = ""
48
+ self.error = ""
49
+ self.running = False
50
+ self.detached = False
51
+ self._user_access_token = None
52
+ self._user_token_expires_at = 0.0
53
+ self._tenant_token = None
54
+ self._tenant_token_expires_at = 0.0
55
+
56
+ @classmethod
57
+ def from_env(cls) -> "PycodexCard":
58
+ api_base = os.environ.get("FEISHU_API_BASE", FEISHU_API_BASE)
59
+ return cls(
60
+ app_id=_env("FEISHU_APP_ID", "LARK_APP_ID"),
61
+ app_secret=_env("FEISHU_APP_SECRET", "LARK_APP_SECRET"),
62
+ api_base=api_base,
63
+ domain=os.environ.get("FEISHU_DOMAIN", _api_base_to_domain(api_base)),
64
+ verification_token=_env(
65
+ "FEISHU_VERIFICATION_TOKEN",
66
+ "LARK_VERIFICATION_TOKEN",
67
+ ),
68
+ encrypt_key=_env("FEISHU_ENCRYPT_KEY", "LARK_ENCRYPT_KEY"),
69
+ refresh_token=_read_refresh_token() or os.environ.get("FEISHU_REFRESH_TOKEN"),
70
+ )
71
+
72
+ def configured(self) -> bool:
73
+ return bool(self.app_id and self.app_secret)
74
+
75
+ def send(self, target: str) -> None:
76
+ receive_id = str(target or "").strip()
77
+ if not receive_id:
78
+ raise ValueError("recipient target is required")
79
+ receive_id = self.resolve_name(receive_id)
80
+ if not receive_id:
81
+ raise ValueError(
82
+ "cannot resolve Feishu user from target: {0}".format(target)
83
+ )
84
+ if receive_id.startswith("oc_"):
85
+ resolved_type = "chat_id"
86
+ else:
87
+ resolved_type = "open_id"
88
+
89
+ self.session_key = "feishu:manual:{0}:{1}".format(resolved_type, receive_id)
90
+
91
+ def build_body(output_mode):
92
+ return {
93
+ "receive_id": receive_id,
94
+ "msg_type": "interactive",
95
+ "content": json.dumps(self.render(output_mode), ensure_ascii=False),
96
+ }
97
+
98
+ response = self._request_rendered_card(
99
+ "POST",
100
+ "/im/v1/messages",
101
+ {"receive_id_type": resolved_type},
102
+ build_body,
103
+ )
104
+ self.message_id = _extract_message_id(response)
105
+
106
+ def update(
107
+ self,
108
+ message_id: "typing.Union[str, None]" = None,
109
+ callback_token: "typing.Union[str, None]" = None,
110
+ ) -> None:
111
+ if message_id:
112
+ self.message_id = message_id
113
+ if callback_token:
114
+ self.callback_token = callback_token
115
+ if not self.configured():
116
+ return
117
+ if self.message_id:
118
+
119
+ def build_body(output_mode):
120
+ return {
121
+ "content": json.dumps(
122
+ self.render(output_mode),
123
+ ensure_ascii=False,
124
+ )
125
+ }
126
+
127
+ self._request_rendered_card(
128
+ "PATCH",
129
+ "/im/v1/messages/{0}".format(self.message_id),
130
+ None,
131
+ build_body,
132
+ )
133
+ return
134
+ if self.callback_token:
135
+
136
+ def build_body(output_mode):
137
+ return {"token": self.callback_token, "card": self.render(output_mode)}
138
+
139
+ self._request_rendered_card(
140
+ "POST",
141
+ "/interactive/v1/card/update",
142
+ None,
143
+ build_body,
144
+ use_user_token=False,
145
+ )
146
+
147
+ def set_queued(self, prompt: str, sender: str = "cli") -> None:
148
+ self.status = "Queued"
149
+ self.status_detail = "Waiting for pycodex."
150
+ self.last_sender = sender or "cli"
151
+ self.last_prompt = prompt
152
+ self._mark_last_turn_output()
153
+ self.error = ""
154
+ self.running = True
155
+
156
+ def set_snapshot(self, prompt: str, output: str) -> None:
157
+ self.last_prompt = prompt or self.last_prompt
158
+ self.output_text = output or self.output_text
159
+
160
+ def detach(self) -> None:
161
+ self.status = "Detached"
162
+ self.status_detail = ""
163
+ self.error = ""
164
+ self.running = False
165
+ self.detached = True
166
+
167
+ def apply_event(self, event) -> bool:
168
+ kind = getattr(event, "kind", None)
169
+ if kind is None and isinstance(event, dict):
170
+ kind = event.get("kind", "")
171
+ kind = str(kind or "")
172
+ payload = getattr(event, "payload", None)
173
+ if payload is None and isinstance(event, dict):
174
+ payload = event
175
+ if not isinstance(payload, dict):
176
+ payload = {}
177
+ if kind == "turn_started":
178
+ self.status = "Running"
179
+ self.status_detail = "Model request started."
180
+ was_running = self.running
181
+ self.running = True
182
+ prompt = payload.get("user_text") or "\n".join(
183
+ str(item) for item in payload.get("user_texts", []) or []
184
+ )
185
+ if prompt:
186
+ prompt_text = str(prompt)
187
+ if not (was_running and prompt_text == self.last_prompt):
188
+ self.last_sender = "cli"
189
+ self.last_prompt = prompt_text
190
+ self._mark_last_turn_output()
191
+ self.error = ""
192
+ return False
193
+ if kind == "assistant_delta":
194
+ self.status = "Responding"
195
+ self.status_detail = "Receiving assistant output."
196
+ # self.output_text += str(payload.get("delta", ""))
197
+ return False
198
+ if kind == "tool_started":
199
+ self.status = "Tool"
200
+ self.status_detail = str(payload.get("tool_name") or "tool")
201
+ return False
202
+ if kind == "tool_completed":
203
+ self.status_detail = str(
204
+ payload.get("summary") or payload.get("tool_name") or "tool completed"
205
+ )
206
+ return False
207
+ if kind == "stream_error":
208
+ self.status = "Retrying"
209
+ self.status_detail = str(
210
+ payload.get("summary") or payload.get("message") or ""
211
+ )
212
+ return False
213
+ if kind == "turn_completed":
214
+ final_text = str(payload.get("output_text") or "")
215
+ if final_text:
216
+ self.output_text = final_text
217
+ self.status = "Idle"
218
+ self.status_detail = "Turn completed."
219
+ self.running = False
220
+ return True
221
+ if kind in {"turn_failed", "submission_failed"}:
222
+ self.status = "Error"
223
+ self.error = str(payload.get("error") or kind)
224
+ self.status_detail = self.error
225
+ self.running = False
226
+ return True
227
+ if kind in {"turn_interrupted", "submission_cancelled"}:
228
+ self.status = "Idle"
229
+ self.status_detail = kind.replace("_", " ")
230
+ self.running = False
231
+ return True
232
+ return False
233
+
234
+ def _mark_last_turn_output(self) -> None:
235
+ if self.output_text and not self.output_text.startswith(LAST_TURN_PREFIX):
236
+ self.output_text = LAST_TURN_PREFIX + self.output_text
237
+
238
+ def render(
239
+ self, output_mode: str = CARD_OUTPUT_MODE_MARKDOWN
240
+ ) -> "typing.Dict[str, object]":
241
+ idle = str(self.status or "").lower() == "idle"
242
+ status_line = _escape_markdown(self.status)
243
+ if self.status_detail:
244
+ status_line += " - " + _escape_markdown(self.status_detail)
245
+ if self.error:
246
+ status_line += " - " + _escape_markdown(self.error)
247
+ input_disabled = self.running or self.detached
248
+ sender = _escape_markdown(self.last_sender or "cli")
249
+ prompt = _truncate(self.last_prompt, 1200) or "-"
250
+ output = _truncate(self.output_text, CARD_OUTPUT_LIMIT) or (
251
+ "Waiting for output..." if self.running else "Ready."
252
+ )
253
+ output_content = _render_output_content(output, output_mode)
254
+ color, title = _status_template(self.status)
255
+ card = {
256
+ "schema": "2.0",
257
+ "config": {"update_multi": True},
258
+ "header": {
259
+ "title": {"tag": "plain_text", "content": title},
260
+ "template": color,
261
+ },
262
+ "body": {
263
+ "elements": [
264
+ {
265
+ "tag": "markdown",
266
+ "element_id": "prompt_md",
267
+ "content": f"> {sender}: **{_escape_code_block(prompt)}**",
268
+ },
269
+ {
270
+ "tag": "column_set",
271
+ "background_style": "grey-50",
272
+ "horizontal_spacing": "8px",
273
+ "horizontal_align": "left",
274
+ "columns": [
275
+ {
276
+ "tag": "column",
277
+ "width": "auto",
278
+ "elements": [
279
+ {
280
+ "tag": "markdown",
281
+ "element_id": "answer_md",
282
+ "content": output_content,
283
+ },
284
+ ],
285
+ "vertical_spacing": "8px",
286
+ "horizontal_align": "left",
287
+ "vertical_align": "top",
288
+ }
289
+ ],
290
+ },
291
+ ]
292
+ },
293
+ }
294
+ if not self.detached:
295
+ card["body"]["elements"].append(
296
+ {
297
+ "tag": "input",
298
+ "element_id": "prompt_input",
299
+ "name": "prompt",
300
+ "input_type": "text",
301
+ "width": "fill",
302
+ "disabled": input_disabled,
303
+ "placeholder": {
304
+ "tag": "plain_text",
305
+ "content": f"Ask {self.model_name}..." if idle else status_line,
306
+ },
307
+ "behaviors": [{"type": "callback", "value": {"action": "send"}}],
308
+ "value": {"action": "send"},
309
+ },
310
+ )
311
+ return card
312
+
313
+ def parse_action(self, sdk_event) -> "typing.Dict[str, object]":
314
+ event_data = getattr(sdk_event, "event", None)
315
+ action_payload = getattr(event_data, "action", None)
316
+ operator = getattr(event_data, "operator", None)
317
+ context = getattr(event_data, "context", None)
318
+ form_values = _sdk_form_values(action_payload)
319
+ value = getattr(action_payload, "value", None) or {}
320
+ if not isinstance(value, dict):
321
+ value = {}
322
+ prompt = str(
323
+ form_values.get("prompt")
324
+ or form_values.get("prompt_input")
325
+ or getattr(action_payload, "input_value", None)
326
+ or value.get("input_value")
327
+ or ""
328
+ ).strip()
329
+ if not prompt and form_values:
330
+ prompt = str(next(iter(form_values.values()))).strip()
331
+ action = str(
332
+ value.get("action") or getattr(action_payload, "name", None) or "send"
333
+ ).strip()
334
+ if action in {"send_button", "prompt_form"}:
335
+ action = "send"
336
+ message_id = (
337
+ str(getattr(context, "open_message_id", None) or "").strip() or None
338
+ )
339
+ callback_token = str(getattr(event_data, "token", None) or "").strip() or None
340
+ self.message_id = message_id or self.message_id
341
+ self.callback_token = callback_token or self.callback_token
342
+ if not self.session_key:
343
+ tenant_key = str(getattr(operator, "tenant_key", None) or "").strip()
344
+ open_id = str(getattr(operator, "open_id", None) or "").strip()
345
+ self.session_key = _default_session_key(tenant_key, open_id, message_id)
346
+ return {
347
+ "action": action,
348
+ "prompt": prompt,
349
+ "sender": self.resolve_operator_name(operator),
350
+ }
351
+
352
+ def resolve_name(self, name: str) -> "typing.Union[str, None]":
353
+ normalized = str(name or "").strip()
354
+ if not normalized:
355
+ return None
356
+ aliases = _user_id_aliases()
357
+ if normalized in aliases:
358
+ return aliases[normalized]
359
+ if normalized.startswith("ou_"):
360
+ return normalized
361
+ if normalized.startswith("oc_"):
362
+ return normalized
363
+ if normalized.isnumeric():
364
+ return self._lookup_user({"mobiles": [normalized]})
365
+ if "@" not in normalized:
366
+ email_domain = _default_email_domain()
367
+ if not email_domain:
368
+ return None
369
+ normalized = normalized + "@" + email_domain
370
+ return self._lookup_user({"emails": [normalized]})
371
+
372
+ def _lookup_user(
373
+ self, body: "typing.Dict[str, object]"
374
+ ) -> "typing.Union[str, None]":
375
+ payload = dict(body)
376
+ payload["include_resigned"] = False
377
+ response = self._request(
378
+ "POST",
379
+ "/contact/v3/users/batch_get_id",
380
+ params={"user_id_type": "open_id"},
381
+ json_body=payload,
382
+ use_user_token=False,
383
+ )
384
+ user_list = _dig(response, "data", "user_list")
385
+ if not isinstance(user_list, list) or not user_list:
386
+ return None
387
+ item = user_list[0] if isinstance(user_list[0], dict) else {}
388
+ value = item.get("user_id") or item.get("open_id") or item.get("union_id")
389
+ return str(value).strip() if value else None
390
+
391
+ def resolve_operator_name(self, operator) -> str:
392
+ for user_id_type in ("user_id", "open_id", "union_id"):
393
+ user_id = str(getattr(operator, user_id_type, None) or "").strip()
394
+ if not user_id:
395
+ continue
396
+ try:
397
+ response = self._request(
398
+ "GET",
399
+ "/contact/v3/users/{0}".format(user_id),
400
+ params={"user_id_type": user_id_type},
401
+ use_user_token=False,
402
+ )
403
+ except Exception:
404
+ continue
405
+ name = _display_user_name(_dig(response, "data", "user") or response)
406
+ if name:
407
+ return name
408
+ return "cli"
409
+
410
+ def _request(
411
+ self,
412
+ method: str,
413
+ path: str,
414
+ params: "typing.Union[typing.Dict[str, str], None]" = None,
415
+ json_body: "typing.Union[typing.Dict[str, object], None]" = None,
416
+ use_user_token: bool = True,
417
+ ) -> "typing.Dict[str, object]":
418
+ user_token = self.user_access_token() if use_user_token else None
419
+ token = user_token or self.tenant_access_token()
420
+ response = self.session.request(
421
+ method,
422
+ self.api_base.rstrip("/") + path,
423
+ params=params,
424
+ json=json_body,
425
+ headers={"Authorization": "Bearer {0}".format(token)},
426
+ timeout=20,
427
+ )
428
+ return _checked_json_response(response)
429
+
430
+ def _request_rendered_card(
431
+ self,
432
+ method: str,
433
+ path: str,
434
+ params: "typing.Union[typing.Dict[str, str], None]",
435
+ json_body_builder: "typing.Callable[[str], typing.Dict[str, object]]",
436
+ use_user_token: bool = True,
437
+ ) -> "typing.Dict[str, object]":
438
+ try:
439
+ return self._request(
440
+ method,
441
+ path,
442
+ params=params,
443
+ json_body=json_body_builder(CARD_OUTPUT_MODE_MARKDOWN),
444
+ use_user_token=use_user_token,
445
+ )
446
+ except Exception as exc:
447
+ if not _is_card_content_error(exc):
448
+ raise
449
+ return self._request(
450
+ method,
451
+ path,
452
+ params=params,
453
+ json_body=json_body_builder(CARD_OUTPUT_MODE_CODE),
454
+ use_user_token=use_user_token,
455
+ )
456
+
457
+ def user_access_token(self) -> "typing.Union[str, None]":
458
+ refresh_token = _read_refresh_token() or self.refresh_token
459
+ if not refresh_token:
460
+ return None
461
+ self.refresh_token = refresh_token
462
+ now = time.time()
463
+ if self._user_access_token and now < self._user_token_expires_at:
464
+ return self._user_access_token
465
+ if not self.configured():
466
+ raise RuntimeError("FEISHU_APP_ID and FEISHU_APP_SECRET are required")
467
+ response = self.session.post(
468
+ self.api_base.rstrip("/") + "/authen/v2/oauth/token",
469
+ json={
470
+ "grant_type": "refresh_token",
471
+ "client_id": self.app_id,
472
+ "client_secret": self.app_secret,
473
+ "refresh_token": refresh_token,
474
+ },
475
+ timeout=20,
476
+ )
477
+ payload = _checked_json_response(response)
478
+ token = payload.get("access_token")
479
+ if not token:
480
+ raise RuntimeError("user access_token missing from Feishu response")
481
+ self._user_access_token = str(token)
482
+ expires_in = payload.get("expires_in") or 0
483
+ self._user_token_expires_at = time.time() + max(60, int(expires_in) - 120)
484
+ refresh_token = payload.get("refresh_token")
485
+ if refresh_token:
486
+ self.refresh_token = str(refresh_token)
487
+ os.environ["FEISHU_REFRESH_TOKEN"] = self.refresh_token
488
+ _write_refresh_token(self.refresh_token)
489
+ return self._user_access_token
490
+
491
+ def tenant_access_token(self) -> str:
492
+ now = time.time()
493
+ if self._tenant_token and now < self._tenant_token_expires_at:
494
+ return self._tenant_token
495
+ response = self.session.post(
496
+ self.api_base.rstrip("/") + "/auth/v3/tenant_access_token/internal",
497
+ json={"app_id": self.app_id, "app_secret": self.app_secret},
498
+ timeout=20,
499
+ )
500
+ payload = _checked_json_response(response)
501
+ token = _dig(payload, "tenant_access_token") or _dig(
502
+ payload,
503
+ "data",
504
+ "tenant_access_token",
505
+ )
506
+ if not token:
507
+ raise RuntimeError("tenant_access_token missing from Feishu response")
508
+ expires_in = _dig(payload, "expire") or _dig(payload, "data", "expire") or 3600
509
+ self._tenant_token = str(token)
510
+ self._tenant_token_expires_at = now + max(60, int(expires_in) - 120)
511
+ return self._tenant_token
512
+
513
+
514
+ def _sdk_form_values(action_payload: "typing.Any") -> "typing.Dict[str, object]":
515
+ for name in ("form_value", "form_values", "input_values"):
516
+ value = getattr(action_payload, name, None)
517
+ if isinstance(value, dict):
518
+ return value
519
+ value = getattr(action_payload, "value", None)
520
+ if isinstance(value, dict):
521
+ for name in ("form_value", "form_values", "input_values"):
522
+ nested = value.get(name)
523
+ if isinstance(nested, dict):
524
+ return nested
525
+ return {}
526
+
527
+
528
+ def _checked_json_response(response) -> "typing.Dict[str, object]":
529
+ try:
530
+ payload = response.json()
531
+ except Exception as exc:
532
+ raise RuntimeError(
533
+ "Feishu API returned non-JSON response: status={0} body={1}".format(
534
+ getattr(response, "status_code", "?"),
535
+ getattr(response, "text", ""),
536
+ )
537
+ ) from exc
538
+ if getattr(response, "status_code", 200) >= 400:
539
+ raise RuntimeError(
540
+ "Feishu API error: status={0} body={1}".format(
541
+ response.status_code,
542
+ json.dumps(payload, ensure_ascii=False),
543
+ )
544
+ )
545
+ code = payload.get("code")
546
+ if code not in (None, 0):
547
+ raise RuntimeError(
548
+ "Feishu API error: {0}".format(json.dumps(payload, ensure_ascii=False))
549
+ )
550
+ return payload
551
+
552
+
553
+ def _extract_message_id(
554
+ response: "typing.Dict[str, object]",
555
+ ) -> "typing.Union[str, None]":
556
+ for path in (
557
+ ("data", "message_id"),
558
+ ("data", "message", "message_id"),
559
+ ("message_id",),
560
+ ("open_message_id",),
561
+ ):
562
+ value = _dig(response, *path)
563
+ if value:
564
+ return str(value)
565
+ return None
566
+
567
+
568
+ def _default_session_key(
569
+ tenant_key: str,
570
+ open_id: str,
571
+ message_id: "typing.Union[str, None]",
572
+ ) -> str:
573
+ user_key = open_id or "unknown"
574
+ if message_id:
575
+ return "feishu:{0}:{1}:{2}".format(tenant_key or "tenant", user_key, message_id)
576
+ return "feishu:{0}:{1}:default".format(tenant_key or "tenant", user_key)
577
+
578
+
579
+ def _display_user_name(user: "typing.Any") -> "typing.Union[str, None]":
580
+ if not isinstance(user, dict):
581
+ return None
582
+ for key in ("name", "en_name", "nickname", "email", "enterprise_email"):
583
+ value = str(user.get(key) or "").strip()
584
+ if value:
585
+ return value
586
+ return None
587
+
588
+
589
+ def _status_template(status: str) -> "typing.Tuple[str, str]":
590
+ normalized = status.lower()
591
+ if normalized in {"error", "failed"}:
592
+ return "red", "Session Connected"
593
+ if normalized in {"running", "responding", "tool", "queued", "retrying"}:
594
+ return "blue", "Session Connected"
595
+ if normalized in {"detached"}:
596
+ return "grey", "Session Detached"
597
+ return "green", "Session Connected"
598
+
599
+
600
+ def _truncate(text: str, limit: int) -> str:
601
+ value = str(text or "")
602
+ if len(value) <= limit:
603
+ return value
604
+ return value[: max(0, limit - 32)] + "\n...[truncated]"
605
+
606
+
607
+ def _escape_code_block(text: str) -> str:
608
+ return str(text or "").replace("```", "'''")
609
+
610
+
611
+ def _render_output_content(output: str, output_mode: str) -> str:
612
+ if output_mode == CARD_OUTPUT_MODE_CODE:
613
+ return "```text\n{0}\n```".format(_escape_code_block(output))
614
+ return output
615
+
616
+
617
+ def _is_card_content_error(exc: "BaseException") -> bool:
618
+ return CARD_CONTENT_ERROR_MARKER in str(exc)
619
+
620
+
621
+ def _escape_markdown(text: str) -> str:
622
+ return str(text or "").replace("`", "'")
623
+
624
+
625
+ def _dig(value: "typing.Any", *path: str) -> "typing.Any":
626
+ current = value
627
+ for key in path:
628
+ if not isinstance(current, dict):
629
+ return None
630
+ current = current.get(key)
631
+ return current
632
+
633
+
634
+ def _env(*names: str) -> "typing.Union[str, None]":
635
+ for name in names:
636
+ value = os.environ.get(name)
637
+ if value:
638
+ return value
639
+ return None
640
+
641
+
642
+ def _refresh_token_path() -> Path:
643
+ return Path(FEISHU_REFRESH_TOKEN_FILE).expanduser()
644
+
645
+
646
+ def _read_refresh_token() -> "typing.Union[str, None]":
647
+ path = _refresh_token_path()
648
+ if not path.exists():
649
+ return None
650
+ value = path.read_text(encoding="utf-8", errors="replace").strip()
651
+ return value or None
652
+
653
+
654
+ def _write_refresh_token(value: str) -> None:
655
+ path = _refresh_token_path()
656
+ path.parent.mkdir(parents=True, exist_ok=True)
657
+ path.write_text("{0}\n".format(value), encoding="utf-8")
658
+ try:
659
+ path.chmod(0o600)
660
+ except OSError:
661
+ pass
662
+
663
+
664
+ def _api_base_to_domain(api_base: str) -> str:
665
+ base = str(api_base or FEISHU_API_BASE).rstrip("/")
666
+ marker = "/open-apis"
667
+ if base.endswith(marker):
668
+ return base[: -len(marker)]
669
+ return base
670
+
671
+
672
+ def _default_email_domain() -> "typing.Union[str, None]":
673
+ value = _env(
674
+ "PYCODEX_FEISHU_DEFAULT_EMAIL_DOMAIN",
675
+ "FEISHU_DEFAULT_EMAIL_DOMAIN",
676
+ "LARK_DEFAULT_EMAIL_DOMAIN",
677
+ )
678
+ if not value:
679
+ return None
680
+ return str(value).strip().lstrip("@") or None
681
+
682
+
683
+ def _user_id_aliases() -> "typing.Dict[str, str]":
684
+ raw = os.environ.get("PYCODEX_FEISHU_USER_IDS")
685
+ if not raw:
686
+ return {}
687
+ try:
688
+ payload = json.loads(raw)
689
+ except Exception:
690
+ return {}
691
+ if not isinstance(payload, dict):
692
+ return {}
693
+ return {str(key): str(value) for key, value in payload.items() if key and value}