lansenger-sdk 1.0.0__tar.gz

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 (50) hide show
  1. lansenger_sdk-1.0.0/PKG-INFO +458 -0
  2. lansenger_sdk-1.0.0/README.md +435 -0
  3. lansenger_sdk-1.0.0/pyproject.toml +44 -0
  4. lansenger_sdk-1.0.0/setup.cfg +4 -0
  5. lansenger_sdk-1.0.0/src/lansenger_sdk/__init__.py +340 -0
  6. lansenger_sdk-1.0.0/src/lansenger_sdk/account_messages.py +121 -0
  7. lansenger_sdk-1.0.0/src/lansenger_sdk/auth.py +100 -0
  8. lansenger_sdk-1.0.0/src/lansenger_sdk/calendars.py +477 -0
  9. lansenger_sdk-1.0.0/src/lansenger_sdk/callbacks.py +500 -0
  10. lansenger_sdk-1.0.0/src/lansenger_sdk/client.py +2586 -0
  11. lansenger_sdk-1.0.0/src/lansenger_sdk/config.py +78 -0
  12. lansenger_sdk-1.0.0/src/lansenger_sdk/constants.py +105 -0
  13. lansenger_sdk-1.0.0/src/lansenger_sdk/contacts.py +455 -0
  14. lansenger_sdk-1.0.0/src/lansenger_sdk/departments.py +209 -0
  15. lansenger_sdk-1.0.0/src/lansenger_sdk/exceptions.py +45 -0
  16. lansenger_sdk-1.0.0/src/lansenger_sdk/group_messages.py +126 -0
  17. lansenger_sdk-1.0.0/src/lansenger_sdk/groups.py +529 -0
  18. lansenger_sdk-1.0.0/src/lansenger_sdk/media.py +161 -0
  19. lansenger_sdk-1.0.0/src/lansenger_sdk/models.py +878 -0
  20. lansenger_sdk-1.0.0/src/lansenger_sdk/oauth.py +312 -0
  21. lansenger_sdk-1.0.0/src/lansenger_sdk/persistence.py +149 -0
  22. lansenger_sdk-1.0.0/src/lansenger_sdk/py.typed +0 -0
  23. lansenger_sdk-1.0.0/src/lansenger_sdk/streaming.py +146 -0
  24. lansenger_sdk-1.0.0/src/lansenger_sdk/sync_client.py +1348 -0
  25. lansenger_sdk-1.0.0/src/lansenger_sdk/todos.py +577 -0
  26. lansenger_sdk-1.0.0/src/lansenger_sdk/user_messages.py +116 -0
  27. lansenger_sdk-1.0.0/src/lansenger_sdk/users.py +97 -0
  28. lansenger_sdk-1.0.0/src/lansenger_sdk.egg-info/PKG-INFO +458 -0
  29. lansenger_sdk-1.0.0/src/lansenger_sdk.egg-info/SOURCES.txt +48 -0
  30. lansenger_sdk-1.0.0/src/lansenger_sdk.egg-info/dependency_links.txt +1 -0
  31. lansenger_sdk-1.0.0/src/lansenger_sdk.egg-info/requires.txt +5 -0
  32. lansenger_sdk-1.0.0/src/lansenger_sdk.egg-info/top_level.txt +1 -0
  33. lansenger_sdk-1.0.0/tests/test_account_messages.py +154 -0
  34. lansenger_sdk-1.0.0/tests/test_bot_message.py +41 -0
  35. lansenger_sdk-1.0.0/tests/test_calendars.py +374 -0
  36. lansenger_sdk-1.0.0/tests/test_callbacks.py +439 -0
  37. lansenger_sdk-1.0.0/tests/test_client.py +282 -0
  38. lansenger_sdk-1.0.0/tests/test_config.py +66 -0
  39. lansenger_sdk-1.0.0/tests/test_constants.py +93 -0
  40. lansenger_sdk-1.0.0/tests/test_contacts.py +316 -0
  41. lansenger_sdk-1.0.0/tests/test_departments.py +255 -0
  42. lansenger_sdk-1.0.0/tests/test_exceptions.py +57 -0
  43. lansenger_sdk-1.0.0/tests/test_group_messages.py +233 -0
  44. lansenger_sdk-1.0.0/tests/test_groups.py +403 -0
  45. lansenger_sdk-1.0.0/tests/test_models.py +179 -0
  46. lansenger_sdk-1.0.0/tests/test_oauth.py +328 -0
  47. lansenger_sdk-1.0.0/tests/test_persistence.py +146 -0
  48. lansenger_sdk-1.0.0/tests/test_streaming.py +227 -0
  49. lansenger_sdk-1.0.0/tests/test_todos.py +340 -0
  50. lansenger_sdk-1.0.0/tests/test_user_messages.py +183 -0
@@ -0,0 +1,458 @@
1
+ Metadata-Version: 2.4
2
+ Name: lansenger-sdk
3
+ Version: 1.0.0
4
+ Summary: Framework-independent Python SDK for Lansenger (蓝信) Smart Bot API — send messages, files, images, cards, manage messages
5
+ Author: Lansenger PM Team
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/lansenger-pm/lansenger-sdk
8
+ Project-URL: Repository, https://github.com/lansenger-pm/lansenger-sdk
9
+ Keywords: lansenger,蓝信,chatbot,messaging,sdk,agent,enterprise-chat
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Communications :: Chat
17
+ Requires-Python: >=3.10
18
+ Description-Content-Type: text/markdown
19
+ Requires-Dist: httpx>=0.24
20
+ Provides-Extra: dev
21
+ Requires-Dist: pytest>=7; extra == "dev"
22
+ Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
23
+
24
+ [English](README.md) | [简体中文](README.zhHans.md) | [繁体中文](README.zhHant.md) | [繁体中文香港](README.zhHantHK.md) | [Français](README.fr.md)
25
+
26
+ # lansenger-sdk
27
+
28
+ Framework-independent Python SDK for the Lansenger (蓝信) platform — supports Lansenger apps, organization bots, and personal bots.
29
+
30
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
31
+ [![Python 3.10+](https://img.shields.io/badge/Python-3.10%2B-blue)](https://www.python.org/)
32
+ [![Tests: 296](https://img.shields.io/badge/Tests-296-green)](https://github.com/lansenger-pm/lansenger-skills-official)
33
+
34
+ > 💠 Zero framework dependencies — only `httpx`. Works with any async or sync Python codebase.
35
+
36
+ ## Supported Bot Types
37
+
38
+ | Bot Type | Auth | WebSocket Inbound | All APIs |
39
+ |----------|------|-------------------|----------|
40
+ | **Lansenger App** | appToken + userToken | ✗ (uses webhook) | ✓ |
41
+ | **Organization Bot** | appToken + userToken | ✗ (uses webhook) | ✓ |
42
+ | **Personal Bot** | appToken | ✓ (WebSocket) | ✓ (limited for non-bot APIs) |
43
+
44
+ All three bot types use the same auth mechanism: `appToken` is required for every API call; `userToken` is only needed for specific user-level operations (user info, staff search, calendar, etc.).
45
+
46
+ ## Features
47
+
48
+ - **Async & Sync clients** — `LansengerClient` (async) + `LansengerSyncClient` (blocking)
49
+ - **Credential & token persistence** — `CredentialStore` saves app_id, app_secret, URLs, appToken, userToken to file (survives restarts)
50
+ - **OAuth2 user authentication** — authorize URL, code exchange, token refresh
51
+ - **Organization & departments** — org info, department detail/children/staff
52
+ - **Staff & contacts** — basic/detailed info, ID mapping, department ancestors, search
53
+ - **Messaging** — 3 private chat channels (bot, official account, user impersonate) + group chat, all message types, @mention, human/bot sender identity
54
+ - **Rich cards** — appCard (with dynamic status updates), oacard, linkCard, verifyCard, appArticles
55
+ - **Streaming messages** — SSE-based real-time delivery for AI agents
56
+ - **Media upload/download** — files, images, videos with auto type detection
57
+ - **Message management** — revoke, dynamic card update
58
+ - **Groups** — create, info, members, list, membership check, update settings & members
59
+ - **Calendar & schedule** — primary calendar, schedule CRUD, attendee management
60
+ - **Unified todo** — create, update, delete, query, executor management, status counts
61
+ - **Callback events** — 25 event types, structured data parsing, signature verification
62
+
63
+ ## Quick Install
64
+
65
+ ```bash
66
+ pip install lansenger-sdk
67
+ ```
68
+
69
+ For development:
70
+
71
+ ```bash
72
+ pip install -e ".[dev]"
73
+ ```
74
+
75
+ ## 1. Authentication
76
+
77
+ ### appToken — Required for all API calls
78
+
79
+ Every SDK method requires `appToken`. The client automatically obtains and refreshes it using your `app_id` + `app_secret`. You never need to manage appToken manually — the `TokenManager` handles the lifecycle:
80
+
81
+ 1. **First call** → `GET /v1/apptoken/create` with app_id + app_secret → returns `appToken` (valid 2 hours)
82
+ 2. **Subsequent calls** → reuse cached appToken until expiry
83
+ 3. **Token expired** → automatically refresh via the same endpoint
84
+
85
+ ```python
86
+ # appToken is managed automatically — just configure app_id + app_secret
87
+ client = LansengerClient(app_id="your-appid", app_secret="your-secret")
88
+
89
+ # You can also get/invalidate token manually
90
+ token = await client.get_token()
91
+ client.invalidate_token() # force refresh on next call
92
+ ```
93
+
94
+ ### userToken — Only needed for specific endpoints
95
+
96
+ `userToken` represents a specific Lansenger user's authorization (obtained via OAuth2). It's only required for:
97
+ - User-level information (fetch_user_info, fetch_staff_detail, search_staff)
98
+ - Calendar & schedule operations (fetch_primary_calendar, create_schedule, etc.)
99
+ - Group operations as a human sender
100
+
101
+ ### Getting Credentials
102
+
103
+ | Bot Type | How to get app_id + app_secret |
104
+ |----------|--------------------------------|
105
+ | **Personal Bot** | Lansenger desktop → Contacts → Smart Bots → Personal Bots → click ℹ️ icon (mobile client does NOT show credentials) |
106
+ | **Lansenger App** | Create at [Lansenger Developer Center](https://dev.lanxin.cn) — may require organization admin approval |
107
+ | **Organization Bot** | Create at [Lansenger Developer Center](https://dev.lanxin.cn) — may require organization admin approval |
108
+
109
+ ### OAuth2 user-level auth
110
+
111
+ ```python
112
+ # Build authorize URL — redirect user to Lansenger passport
113
+ url = client.build_authorize_url(redirect_uri="https://myapp.com/callback")
114
+
115
+ # After user authorizes, exchange code for userToken + refreshToken
116
+ token_result = await client.exchange_code(code="auth_code_from_callback")
117
+
118
+ # Refresh expired userToken
119
+ new_token = await client.refresh_user_token(refresh_token=token_result.refresh_token)
120
+
121
+ # Fetch user profile
122
+ user_info = await client.fetch_user_info(user_token=token_result.user_token)
123
+ ```
124
+
125
+ ## 2. Organization & Departments
126
+
127
+ ```python
128
+ # Organization info
129
+ org = await client.fetch_org_info(org_id="orgId")
130
+
131
+ # Department hierarchy
132
+ detail = await client.fetch_department_detail(department_id="deptId")
133
+ children = await client.fetch_department_children(department_id="deptId")
134
+ staffs = await client.fetch_department_staffs(department_id="deptId")
135
+ ```
136
+
137
+ ## 3. Staff & Contacts
138
+
139
+ ```python
140
+ # Basic staff info
141
+ staff = await client.fetch_staff_basic_info(staff_id="staffOpenId")
142
+
143
+ # Detailed profile (userToken recommended)
144
+ detail = await client.fetch_staff_detail(staff_id="staffOpenId", user_token="ut")
145
+
146
+ # Map phone → staffId
147
+ mapping = await client.fetch_staff_id_mapping(
148
+ org_id="orgId", id_type="mobile", id_value="13800138000"
149
+ )
150
+
151
+ # Department ancestors for a staff member
152
+ ancestors = await client.fetch_department_ancestors(staff_id="staffOpenId")
153
+
154
+ # Search staff (requires userToken or userId)
155
+ results = await client.search_staff(keyword="Zhang San", user_token="ut")
156
+
157
+ # Org extra field IDs
158
+ fields = await client.fetch_org_extra_field_ids(org_id="orgId")
159
+ ```
160
+
161
+ ## 4. Messaging & Media
162
+
163
+ #### Bot private chat — most common
164
+
165
+ ```python
166
+ result = await client.send_text(chat_id="staff123", content="Hello!")
167
+ result = await client.send_markdown(chat_id="staff123", content="**Bold**")
168
+ result = await client.send_file(chat_id="staff123", file_path="/path/to/report.pdf")
169
+ ```
170
+
171
+ #### Public account channel
172
+
173
+ ```python
174
+ result = await client.send_account_message(
175
+ msg_type="text", msg_data={"text": {"content": "System notice"}},
176
+ chat_ids=["staff1", "staff2"], account_id="524288-xxxx",
177
+ )
178
+ ```
179
+
180
+ #### User impersonate channel (requires userToken)
181
+
182
+ ```python
183
+ result = await client.send_user_message(
184
+ receiver_id="staff456", msg_type="text",
185
+ msg_data={"text": {"content": "Hello"}},
186
+ user_token="ut", # required
187
+ )
188
+ ```
189
+
190
+ #### Group chat
191
+
192
+ ```python
193
+ # Bot → group
194
+ result = await client.send_text(chat_id="group123", content="Notice", is_group=True)
195
+
196
+ # Human → group (with userToken)
197
+ result = await client.send_group_message(
198
+ group_id="group123", msg_type="text",
199
+ msg_data={"text": {"content": "I'll handle it"}},
200
+ user_token="ut",
201
+ )
202
+
203
+ # Group chat supports ALL message types (text, formatText, oacard, appCard, linkCard, etc.)
204
+ result = await client.send_group_message(
205
+ group_id="group123", msg_type="appCard",
206
+ msg_data={"appCard": {"bodyTitle": "Approval", "isDynamic": True}},
207
+ user_token="ut",
208
+ )
209
+
210
+ # @mention in group
211
+ result = await client.send_text(
212
+ chat_id="group123", content="Important!", is_group=True, reminder_all=True,
213
+ )
214
+ ```
215
+
216
+ #### Rich cards
217
+
218
+ ```python
219
+ result = await client.send_app_card(chat_id="staff123", body_title="Approval", is_dynamic=True)
220
+ result = await client.send_link_card(chat_id="staff123", title="Article", link="https://...")
221
+ result = await client.send_app_articles(chat_id="staff123", articles=[...])
222
+
223
+ # Update dynamic card status
224
+ result = await client.update_dynamic_card(msg_id="msg123", is_last_update=True)
225
+ ```
226
+
227
+ #### Streaming messages (for AI agents)
228
+
229
+ ```python
230
+ result = await client.create_stream_message(receiver_id="staff1", receiver_type="staff", stream_id="s1")
231
+ result = await client.fetch_stream_message(msg_id="msg123")
232
+ ```
233
+
234
+ #### Media
235
+
236
+ ```python
237
+ # Upload
238
+ upload = await client.upload_media(file_path="/path/to/file.pdf")
239
+
240
+ # Download
241
+ download = await client.download_media(media_id="media123")
242
+
243
+ # Revoke messages
244
+ result = await client.revoke_message(message_ids=["msg1", "msg2"])
245
+ ```
246
+
247
+ ## 5. Groups
248
+
249
+ ```python
250
+ # Create group
251
+ group = await client.create_group(name="Project Chat", org_id="orgId", staff_id_list=["s1","s2","s3"])
252
+
253
+ # Fetch info & members
254
+ info = await client.fetch_group_info(group_id="groupOpenId")
255
+ members = await client.fetch_group_members(group_id="groupOpenId")
256
+ groups = await client.fetch_group_list()
257
+
258
+ # Check membership
259
+ result = await client.check_is_in_group(group_id="groupOpenId", staff_id="staff1")
260
+
261
+ # Update settings
262
+ await client.update_group_info(group_id="groupId", name="New Name", manage_mode=1)
263
+
264
+ # Add/remove members
265
+ await client.update_group_members(
266
+ group_id="groupId", add_user_list=["staff4"], del_user_list=["staff3"],
267
+ )
268
+ ```
269
+
270
+ ## 6. Calendar & Schedule
271
+
272
+ ```python
273
+ # Get primary calendar (requires userToken or userId)
274
+ cal = await client.fetch_primary_calendar(user_token="ut")
275
+
276
+ # Create schedule
277
+ schedule = await client.create_schedule(
278
+ calendar_id=cal.calendar_id, summary="Team Meeting",
279
+ start_time={"date": "2024-01-15", "time": "10:00", "timeZone": "Asia/Shanghai"},
280
+ end_time={"date": "2024-01-15", "time": "11:00", "timeZone": "Asia/Shanghai"},
281
+ attendees=[{"staffId": "staff1", "attendeeFlag": "required"}],
282
+ user_token="ut",
283
+ )
284
+
285
+ # Fetch/delete schedule
286
+ info = await client.fetch_schedule(calendar_id="cal1", schedule_id="sch1", user_token="ut")
287
+ await client.delete_schedule(calendar_id="cal1", schedule_id="sch1", user_token="ut")
288
+
289
+ # Schedule list in time range (max 42 days)
290
+ schedules = await client.fetch_schedule_list(
291
+ calendar_id="cal1", start_time=1705276800000, end_time=1707940800000, user_token="ut",
292
+ )
293
+
294
+ # Attendee management
295
+ attendees = await client.fetch_schedule_attendees(calendar_id="cal1", schedule_id="sch1", user_token="ut")
296
+ await client.add_schedule_attendees(calendar_id="cal1", schedule_id="sch1", attendees=["staff2"], user_token="ut")
297
+ await client.delete_schedule_attendees(calendar_id="cal1", schedule_id="sch1", attendees=["staff2"], user_token="ut")
298
+ ```
299
+
300
+ ## 7. Unified Todo
301
+
302
+ ```python
303
+ from lansenger_sdk import TODO_TYPE_APPROVAL, TODO_TODO_STATUS_DONE
304
+
305
+ # Create todo task
306
+ todo = await client.create_todo_task(
307
+ title="Approval Request", link="https://app.com/a/1", pc_link="https://pc.app.com/a/1",
308
+ executor_ids=["staff1"], org_id="org1", type=TODO_TYPE_APPROVAL,
309
+ )
310
+
311
+ # Update status (11=pending-read, 12=read, 21=pending-do, 22=done)
312
+ await client.update_todo_task_status(todotask_id="taskId", status=TODO_TODO_STATUS_DONE, org_id="org1")
313
+
314
+ # Update content
315
+ await client.update_todo_task(todotask_id="taskId", title="Updated", link="l", pc_link="p", org_id="org1")
316
+
317
+ # Delete (sender only)
318
+ await client.delete_todo_task(todotask_id="taskId", org_id="org1")
319
+
320
+ # Query
321
+ list_result = await client.fetch_todo_task_list(org_id="org1")
322
+ task = await client.fetch_todo_task_by_id(todotask_id="taskId", org_id="org1")
323
+ task = await client.fetch_todo_task_by_source_id(source_id="src1", org_id="org1")
324
+ counts = await client.fetch_todo_task_status_counts(staff_id="staff1", org_id="org1")
325
+
326
+ # Executor management
327
+ await client.add_executors(executor_ids=["staff2"], org_id="org1", todotask_id="taskId")
328
+ await client.delete_executors(executor_ids=["staff2"], org_id="org1", todotask_id="taskId")
329
+ executors = await client.fetch_executor_list(todotask_id="taskId", org_id="org1")
330
+ await client.update_executor_status(
331
+ executor_status_list=[{"executorId": "staff1", "todotaskId": "taskId", "status": "22"}],
332
+ org_id="org1",
333
+ )
334
+ ```
335
+
336
+ ## 8. Callback Events
337
+
338
+ ```python
339
+ from lansenger_sdk import parse_callback_payload, verify_callback_signature
340
+
341
+ # Parse webhook payload
342
+ events = parse_callback_payload(encrypted_data, encoding_key="your_key")
343
+
344
+ # Verify signature
345
+ is_valid = verify_callback_signature(timestamp, nonce, signature, encoding_key)
346
+
347
+ # Available event types
348
+ types = client.get_callback_event_types() # 26 event types across 14 categories
349
+ ```
350
+
351
+ ## Message Type Capability Matrix
352
+
353
+ | msgType | Markdown | @mention | Attachments | Private Channels | Group Chat | Notes |
354
+ |---------|----------|----------|-------------|------------------|------------|-------|
355
+ | `text` | ✗ | ✓ (group) | ✓ | Bot, Official Account, User Impersonate | ✓ | Up to 6000 bytes |
356
+ | `formatText` | ✓ | ✗ | ✗ | User Impersonate only | ✓ | Markdown via formatType=1 |
357
+ | `oacard` | ✗ | ✗ | ✗ | Bot, Official Account, User Impersonate | ✓ | Simple card with fields |
358
+ | `appCard` | ✓ (div tags) | ✗ | ✗ | Bot, Official Account, User Impersonate | ✓ | Rich card, dynamic updates |
359
+ | `linkCard` | ✗ | ✗ | ✗ | Bot, Official Account | ✓ | Link preview card |
360
+ | `appArticles` | ✗ | ✗ | ✗ | Bot private only | ✓ | Article list (1+ articles) |
361
+ | `verifyCard` | ✗ | ✗ | ✗ | Bot, Official Account | ✓ | Verification card with buttons |
362
+ | `i18nAppCard` | ✓ (div tags) | ✗ | ✗ | Bot, Official Account, User Impersonate | ✓ | Multilingual appCard (zhHans/zhHant/zhHantHK/en/fr) |
363
+ | `i18nSystemAction` | ✗ | ✗ | ✗ | Platform internal | ✓ | Multilingual systemAction |
364
+ | `i18nSystem` | ✗ | ✗ | ✗ | Platform internal | ✓ | Multilingual system message |
365
+
366
+ **Group chat** supports all message types. Only group chat supports @mention.
367
+
368
+ ## Configuration
369
+
370
+ ### Environment Variables
371
+
372
+ | Variable | Required | Description | Default |
373
+ |----------|----------|-------------|---------|
374
+ | `LANSENGER_APP_ID` | ✓ | App/Bot ID | — |
375
+ | `LANSENGER_APP_SECRET` | ✓ | App/Bot Secret | — |
376
+ | `LANSENGER_API_GATEWAY_URL` | ✗ | API Gateway URL | `https://open.e.lanxin.cn/open/apigw` |
377
+ | `LANSENGER_PASSPORT_URL` | ✗ | Passport URL (for OAuth2) | — |
378
+
379
+ ### Credential & Token Persistence
380
+
381
+ By default, credentials and tokens stay in memory only (lost on process exit). Enable file persistence with `store_path`:
382
+
383
+ ```python
384
+ from lansenger_sdk import LansengerClient, CredentialStore
385
+
386
+ # Auto-persist to ~/.lansenger/sdk_state.json (0600 permissions)
387
+ client = LansengerClient(app_id="...", app_secret="...", store_path="~/.lansenger/sdk_state.json")
388
+
389
+ # Or from env with persistence
390
+ client = LansengerClient.from_env(store_path="~/.lansenger/sdk_state.json")
391
+
392
+ # Manual store operations
393
+ store = CredentialStore(path="~/.lansenger/sdk_state.json")
394
+ store.save_credentials("app_id", "app_secret", api_gateway_url="...", passport_url="...")
395
+ store.save_user_token("user_token", refresh_token="refresh_token")
396
+ token = store.load_app_token() # None if expired
397
+ ```
398
+
399
+ When persistence is enabled:
400
+ - **appToken** is saved after each API fetch and restored on restart (skips redundant API calls)
401
+ - **userToken + refreshToken** are saved after OAuth2 exchange
402
+ - **Credentials + URLs** are saved together for full config recovery
403
+
404
+ ### Sync Client
405
+
406
+ All methods available on `LansengerSyncClient` with identical signatures (blocking):
407
+
408
+ ```python
409
+ from lansenger_sdk import LansengerSyncClient
410
+
411
+ client = LansengerSyncClient.from_env()
412
+ result = client.send_text(chat_id="staff123", content="Hello!")
413
+ org = client.fetch_org_info(org_id="orgId")
414
+ ```
415
+
416
+ ## Project Structure
417
+
418
+ ```
419
+ lansenger-skills-official/
420
+ ├── src/lansenger_sdk/
421
+ │ ├── __init__.py # All exports
422
+ │ ├── client.py # LansengerClient (async)
423
+ │ ├── sync_client.py # LansengerSyncClient (sync)
424
+ │ ├── config.py # LansengerConfig
425
+ │ ├── auth.py # TokenManager — appToken lifecycle
426
+ │ ├── oauth.py # OAuth2 helpers
427
+ │ ├── constants.py # API endpoints, media types, OAuth scopes
428
+ │ ├── exceptions.py # LansengerError hierarchy
429
+ │ ├── models.py # 35+ dataclass result types
430
+ │ ├── contacts.py # Staff & org info APIs
431
+ │ ├── departments.py # Department APIs
432
+ │ ├── account_messages.py # Public account channel
433
+ │ ├── user_messages.py # User impersonate channel
434
+ │ ├── group_messages.py # Group chat channel
435
+ │ ├── media.py # Upload/download
436
+ │ ├── streaming.py # SSE streaming
437
+ │ ├── persistence.py # CredentialStore — file-based token & credential persistence
438
+ │ ├── callbacks.py # Callback events
439
+ │ ├── groups.py # Group APIs
440
+ │ ├── todos.py # Unified Todo
441
+ │ ├── calendars.py # Calendar & Schedule
442
+ │ └── users.py # User info
443
+ ├── tests/ # 296 tests, all passing
444
+ ├── skills/ # 9 skill docs + manifest
445
+ ├── pyproject.toml
446
+ └── README*.md # 5-language READMEs
447
+ ```
448
+
449
+ ## Development
450
+
451
+ ```bash
452
+ pip install -e ".[dev]"
453
+ pytest tests/ -v
454
+ ```
455
+
456
+ ## License
457
+
458
+ MIT — see [LICENSE](LICENSE).