hap-cli 0.6.7__tar.gz → 0.6.8__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 (98) hide show
  1. {hap_cli-0.6.7/hap_cli.egg-info → hap_cli-0.6.8}/PKG-INFO +17 -10
  2. hap_cli-0.6.7/PKG-INFO → hap_cli-0.6.8/hap_cli/README.md +14 -35
  3. {hap_cli-0.6.7 → hap_cli-0.6.8}/hap_cli/README_CN.md +14 -0
  4. {hap_cli-0.6.7 → hap_cli-0.6.8}/hap_cli/commands/app_cmd.py +51 -5
  5. {hap_cli-0.6.7 → hap_cli-0.6.8}/hap_cli/commands/auth_cmd.py +80 -15
  6. {hap_cli-0.6.7 → hap_cli-0.6.8}/hap_cli/commands/calendar_cmd.py +3 -3
  7. {hap_cli-0.6.7 → hap_cli-0.6.8}/hap_cli/commands/chat_cmd.py +18 -9
  8. hap_cli-0.6.8/hap_cli/commands/config_cmd.py +293 -0
  9. {hap_cli-0.6.7 → hap_cli-0.6.8}/hap_cli/commands/contact_cmd.py +3 -3
  10. {hap_cli-0.6.7 → hap_cli-0.6.8}/hap_cli/commands/department_cmd.py +3 -3
  11. {hap_cli-0.6.7 → hap_cli-0.6.8}/hap_cli/commands/instance_cmd.py +3 -3
  12. {hap_cli-0.6.7 → hap_cli-0.6.8}/hap_cli/commands/node_cmd.py +3 -3
  13. {hap_cli-0.6.7 → hap_cli-0.6.8}/hap_cli/commands/optionset_cmd.py +3 -3
  14. {hap_cli-0.6.7 → hap_cli-0.6.8}/hap_cli/commands/page_cmd.py +3 -3
  15. {hap_cli-0.6.7 → hap_cli-0.6.8}/hap_cli/commands/post_cmd.py +3 -3
  16. {hap_cli-0.6.7 → hap_cli-0.6.8}/hap_cli/commands/record_cmd.py +3 -3
  17. hap_cli-0.6.8/hap_cli/commands/region_cmd.py +19 -0
  18. {hap_cli-0.6.7 → hap_cli-0.6.8}/hap_cli/commands/role_cmd.py +3 -3
  19. hap_cli-0.6.8/hap_cli/commands/v3_registry.py +244 -0
  20. {hap_cli-0.6.7 → hap_cli-0.6.8}/hap_cli/commands/workflow_cmd.py +3 -3
  21. {hap_cli-0.6.7 → hap_cli-0.6.8}/hap_cli/commands/worksheet_cmd.py +3 -3
  22. {hap_cli-0.6.7 → hap_cli-0.6.8}/hap_cli/core/auth.py +267 -36
  23. hap_cli-0.6.8/hap_cli/core/chat.py +1186 -0
  24. hap_cli-0.6.8/hap_cli/core/global_meta.py +151 -0
  25. hap_cli-0.6.8/hap_cli/core/logger.py +180 -0
  26. hap_cli-0.6.8/hap_cli/core/session.py +886 -0
  27. hap_cli-0.6.8/hap_cli/core/token_crypto.py +80 -0
  28. hap_cli-0.6.8/hap_cli/core/v3/__init__.py +19 -0
  29. hap_cli-0.6.8/hap_cli/core/v3/dispatcher.py +410 -0
  30. hap_cli-0.6.8/hap_cli/core/v3/render.py +116 -0
  31. hap_cli-0.6.8/hap_cli/core/v3/schema.py +94 -0
  32. hap_cli-0.6.8/hap_cli/hap_cli.py +174 -0
  33. hap_cli-0.6.8/hap_cli/i18n.py +245 -0
  34. hap_cli-0.6.8/hap_cli/locale/__init__.py +1 -0
  35. hap_cli-0.6.8/hap_cli/locale/messages.json +3280 -0
  36. hap_cli-0.6.8/hap_cli/locale/messages.schema.json +54 -0
  37. {hap_cli-0.6.7 → hap_cli-0.6.8}/hap_cli/tests/conftest.py +20 -0
  38. hap_cli-0.6.8/hap_cli/tests/test_auth_prerelease.py +410 -0
  39. hap_cli-0.6.8/hap_cli/tests/test_config_cmd.py +191 -0
  40. {hap_cli-0.6.7 → hap_cli-0.6.8}/hap_cli/tests/test_core.py +743 -132
  41. {hap_cli-0.6.7 → hap_cli-0.6.8}/hap_cli/tests/test_full_e2e.py +4 -2
  42. hap_cli-0.6.8/hap_cli/tests/test_global_meta.py +309 -0
  43. hap_cli-0.6.8/hap_cli/tests/test_i18n.py +306 -0
  44. hap_cli-0.6.8/hap_cli/tests/test_integration_v3.py +177 -0
  45. {hap_cli-0.6.7 → hap_cli-0.6.8}/hap_cli/tests/test_org_id_cli.py +27 -15
  46. hap_cli-0.6.8/hap_cli/tests/test_v3_dispatcher.py +349 -0
  47. hap_cli-0.6.8/hap_cli/tests/test_v3_session.py +307 -0
  48. hap_cli-0.6.7/hap_cli/README.md → hap_cli-0.6.8/hap_cli.egg-info/PKG-INFO +42 -6
  49. {hap_cli-0.6.7 → hap_cli-0.6.8}/hap_cli.egg-info/SOURCES.txt +20 -0
  50. {hap_cli-0.6.7 → hap_cli-0.6.8}/hap_cli.egg-info/requires.txt +1 -2
  51. hap_cli-0.6.8/setup.py +67 -0
  52. hap_cli-0.6.7/hap_cli/core/chat.py +0 -672
  53. hap_cli-0.6.7/hap_cli/core/session.py +0 -493
  54. hap_cli-0.6.7/hap_cli/core/token_crypto.py +0 -128
  55. hap_cli-0.6.7/hap_cli/hap_cli.py +0 -230
  56. hap_cli-0.6.7/setup.py +0 -39
  57. {hap_cli-0.6.7 → hap_cli-0.6.8}/hap_cli/__init__.py +0 -0
  58. {hap_cli-0.6.7 → hap_cli-0.6.8}/hap_cli/commands/__init__.py +0 -0
  59. {hap_cli-0.6.7 → hap_cli-0.6.8}/hap_cli/context.py +0 -0
  60. {hap_cli-0.6.7 → hap_cli-0.6.8}/hap_cli/core/__init__.py +0 -0
  61. {hap_cli-0.6.7 → hap_cli-0.6.8}/hap_cli/core/app.py +0 -0
  62. {hap_cli-0.6.7 → hap_cli-0.6.8}/hap_cli/core/calendar_mod.py +0 -0
  63. {hap_cli-0.6.7 → hap_cli-0.6.8}/hap_cli/core/contact.py +0 -0
  64. {hap_cli-0.6.7 → hap_cli-0.6.8}/hap_cli/core/department.py +0 -0
  65. {hap_cli-0.6.7 → hap_cli-0.6.8}/hap_cli/core/flow_node.py +0 -0
  66. {hap_cli-0.6.7 → hap_cli-0.6.8}/hap_cli/core/group.py +0 -0
  67. {hap_cli-0.6.7 → hap_cli-0.6.8}/hap_cli/core/instance.py +0 -0
  68. {hap_cli-0.6.7 → hap_cli-0.6.8}/hap_cli/core/optionset.py +0 -0
  69. {hap_cli-0.6.7 → hap_cli-0.6.8}/hap_cli/core/page.py +0 -0
  70. {hap_cli-0.6.7 → hap_cli-0.6.8}/hap_cli/core/post.py +0 -0
  71. {hap_cli-0.6.7 → hap_cli-0.6.8}/hap_cli/core/record.py +0 -0
  72. {hap_cli-0.6.7 → hap_cli-0.6.8}/hap_cli/core/response_crypto.py +0 -0
  73. {hap_cli-0.6.7 → hap_cli-0.6.8}/hap_cli/core/role.py +0 -0
  74. {hap_cli-0.6.7 → hap_cli-0.6.8}/hap_cli/core/workflow.py +0 -0
  75. {hap_cli-0.6.7 → hap_cli-0.6.8}/hap_cli/core/worksheet.py +0 -0
  76. {hap_cli-0.6.7 → hap_cli-0.6.8}/hap_cli/skills/SKILL.md +0 -0
  77. {hap_cli-0.6.7 → hap_cli-0.6.8}/hap_cli/skills/__init__.py +0 -0
  78. {hap_cli-0.6.7 → hap_cli-0.6.8}/hap_cli/tests/__init__.py +0 -0
  79. {hap_cli-0.6.7 → hap_cli-0.6.8}/hap_cli/tests/test_integration.py +0 -0
  80. {hap_cli-0.6.7 → hap_cli-0.6.8}/hap_cli/tests/test_integration_approval.py +0 -0
  81. {hap_cli-0.6.7 → hap_cli-0.6.8}/hap_cli/tests/test_integration_calendar.py +0 -0
  82. {hap_cli-0.6.7 → hap_cli-0.6.8}/hap_cli/tests/test_integration_destructive.py +0 -0
  83. {hap_cli-0.6.7 → hap_cli-0.6.8}/hap_cli/tests/test_integration_misc.py +0 -0
  84. {hap_cli-0.6.7 → hap_cli-0.6.8}/hap_cli/tests/test_integration_post.py +0 -0
  85. {hap_cli-0.6.7 → hap_cli-0.6.8}/hap_cli/tests/test_integration_social.py +0 -0
  86. {hap_cli-0.6.7 → hap_cli-0.6.8}/hap_cli/tests/test_integration_workflow.py +0 -0
  87. {hap_cli-0.6.7 → hap_cli-0.6.8}/hap_cli/tests/test_integration_worksheet_extra.py +0 -0
  88. {hap_cli-0.6.7 → hap_cli-0.6.8}/hap_cli/tests/test_org_id_docs.py +0 -0
  89. {hap_cli-0.6.7 → hap_cli-0.6.8}/hap_cli/tests/test_parameter_conventions.py +0 -0
  90. {hap_cli-0.6.7 → hap_cli-0.6.8}/hap_cli/tests/test_parameter_mapping_registry.py +0 -0
  91. {hap_cli-0.6.7 → hap_cli-0.6.8}/hap_cli/utils/__init__.py +0 -0
  92. {hap_cli-0.6.7 → hap_cli-0.6.8}/hap_cli/utils/formatting.py +0 -0
  93. {hap_cli-0.6.7 → hap_cli-0.6.8}/hap_cli/utils/options.py +0 -0
  94. {hap_cli-0.6.7 → hap_cli-0.6.8}/hap_cli/utils/parameter_mapping.py +0 -0
  95. {hap_cli-0.6.7 → hap_cli-0.6.8}/hap_cli.egg-info/dependency_links.txt +0 -0
  96. {hap_cli-0.6.7 → hap_cli-0.6.8}/hap_cli.egg-info/entry_points.txt +0 -0
  97. {hap_cli-0.6.7 → hap_cli-0.6.8}/hap_cli.egg-info/top_level.txt +0 -0
  98. {hap_cli-0.6.7 → hap_cli-0.6.8}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hap-cli
3
- Version: 0.6.7
3
+ Version: 0.6.8
4
4
  Summary: CLI harness for MingDAO HAP - Enterprise no-code platform
5
5
  Author: hap-cli
6
6
  License: Apache-2.0
@@ -15,14 +15,13 @@ Requires-Python: >=3.10
15
15
  Description-Content-Type: text/markdown
16
16
  Requires-Dist: click>=8.0
17
17
  Requires-Dist: requests>=2.28
18
- Provides-Extra: crypto
19
- Requires-Dist: pycryptodome>=3.15; extra == "crypto"
18
+ Requires-Dist: pycryptodome>=3.15
19
+ Requires-Dist: python-socketio[client]>=5.10
20
20
  Dynamic: author
21
21
  Dynamic: classifier
22
22
  Dynamic: description
23
23
  Dynamic: description-content-type
24
24
  Dynamic: license
25
- Dynamic: provides-extra
26
25
  Dynamic: requires-dist
27
26
  Dynamic: requires-python
28
27
  Dynamic: summary
@@ -374,6 +373,20 @@ hap --json worksheet record list WORKSHEET_ID
374
373
  | `chat group-info` | Get information about a chat group |
375
374
  | `chat card-detail` | Resolve card references embedded in chat messages |
376
375
 
376
+ ### config — Harness Settings
377
+
378
+ | Command | Description |
379
+ |---|---|
380
+ | `config show` | Show current harness configuration |
381
+ | `config completion [bash\|zsh\|fish] [--install]` | Enable `<Tab>` shell completion (old `hap completion` kept as hidden alias) |
382
+ | `config log on` | Enable API request/response logging |
383
+ | `config log off` | Disable logging |
384
+ | `config log level <LEVEL>` | Set log level (default `WARNING`; choices: `DEBUG/INFO/WARNING/ERROR/CRITICAL`) |
385
+ | `config log status` | Show current logging on/off, level, and file path |
386
+ | `config log view [--lines N] [--no-follow]` | Follow the log file `tail -f` style |
387
+
388
+ Logs are written to `~/.hap-cli/hap-cli.log` (rotated at 10 MB × 3). When enabled, every HAP API call is logged at `INFO` with URL, request body, response body, and a redacted `Authorization` header (first 16 characters only). Sensitive keys like `password` in request bodies are masked automatically. Set `HAP_LOG_LEVEL=DEBUG` to temporarily override the stored level without editing config.
389
+
377
390
  ## Workflow Management
378
391
 
379
392
  ```bash
@@ -458,12 +471,6 @@ non-interactively (e.g. on a headless server).
458
471
 
459
472
  Commands that expose `--org-id` will use the saved current org when you omit the option.
460
473
 
461
- For private deployments with encrypted API responses, install crypto support:
462
-
463
- ```bash
464
- pip install hap-cli[crypto]
465
- ```
466
-
467
474
  ## More Examples
468
475
 
469
476
  ```bash
@@ -1,32 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: hap-cli
3
- Version: 0.6.7
4
- Summary: CLI harness for MingDAO HAP - Enterprise no-code platform
5
- Author: hap-cli
6
- License: Apache-2.0
7
- Classifier: Development Status :: 3 - Alpha
8
- Classifier: Intended Audience :: Developers
9
- Classifier: License :: OSI Approved :: Apache Software License
10
- Classifier: Programming Language :: Python :: 3
11
- Classifier: Programming Language :: Python :: 3.10
12
- Classifier: Programming Language :: Python :: 3.11
13
- Classifier: Programming Language :: Python :: 3.12
14
- Requires-Python: >=3.10
15
- Description-Content-Type: text/markdown
16
- Requires-Dist: click>=8.0
17
- Requires-Dist: requests>=2.28
18
- Provides-Extra: crypto
19
- Requires-Dist: pycryptodome>=3.15; extra == "crypto"
20
- Dynamic: author
21
- Dynamic: classifier
22
- Dynamic: description
23
- Dynamic: description-content-type
24
- Dynamic: license
25
- Dynamic: provides-extra
26
- Dynamic: requires-dist
27
- Dynamic: requires-python
28
- Dynamic: summary
29
-
30
1
  # hap-cli
31
2
 
32
3
  CLI harness for **MingDAO HAP** (明道云) - an enterprise no-code platform (hap).
@@ -374,6 +345,20 @@ hap --json worksheet record list WORKSHEET_ID
374
345
  | `chat group-info` | Get information about a chat group |
375
346
  | `chat card-detail` | Resolve card references embedded in chat messages |
376
347
 
348
+ ### config — Harness Settings
349
+
350
+ | Command | Description |
351
+ |---|---|
352
+ | `config show` | Show current harness configuration |
353
+ | `config completion [bash\|zsh\|fish] [--install]` | Enable `<Tab>` shell completion (old `hap completion` kept as hidden alias) |
354
+ | `config log on` | Enable API request/response logging |
355
+ | `config log off` | Disable logging |
356
+ | `config log level <LEVEL>` | Set log level (default `WARNING`; choices: `DEBUG/INFO/WARNING/ERROR/CRITICAL`) |
357
+ | `config log status` | Show current logging on/off, level, and file path |
358
+ | `config log view [--lines N] [--no-follow]` | Follow the log file `tail -f` style |
359
+
360
+ Logs are written to `~/.hap-cli/hap-cli.log` (rotated at 10 MB × 3). When enabled, every HAP API call is logged at `INFO` with URL, request body, response body, and a redacted `Authorization` header (first 16 characters only). Sensitive keys like `password` in request bodies are masked automatically. Set `HAP_LOG_LEVEL=DEBUG` to temporarily override the stored level without editing config.
361
+
377
362
  ## Workflow Management
378
363
 
379
364
  ```bash
@@ -458,12 +443,6 @@ non-interactively (e.g. on a headless server).
458
443
 
459
444
  Commands that expose `--org-id` will use the saved current org when you omit the option.
460
445
 
461
- For private deployments with encrypted API responses, install crypto support:
462
-
463
- ```bash
464
- pip install hap-cli[crypto]
465
- ```
466
-
467
446
  ## More Examples
468
447
 
469
448
  ```bash
@@ -352,6 +352,20 @@ hap --json worksheet record list 工作表ID
352
352
  | `chat group-info` | 获取群组信息 |
353
353
  | `chat card-detail` | 解析聊天消息中嵌入的卡片引用 |
354
354
 
355
+ ### config — 本地 CLI 配置
356
+
357
+ | 命令 | 说明 |
358
+ |---|---|
359
+ | `config show` | 显示当前 CLI 配置摘要 |
360
+ | `config completion [bash\|zsh\|fish] [--install]` | 启用 `<Tab>` 命令补全(原 `hap completion`,保留隐藏别名) |
361
+ | `config log on` | 打开 API 请求/响应日志记录 |
362
+ | `config log off` | 关闭日志记录 |
363
+ | `config log level <LEVEL>` | 设置日志级别(默认 `WARNING`) |
364
+ | `config log status` | 查看当前日志开关、级别及文件路径 |
365
+ | `config log view [--lines N] [--no-follow]` | 以 `tail -f` 模式跟踪日志文件 |
366
+
367
+ 日志默认写入 `~/.hap-cli/hap-cli.log`,自动按 10MB × 3 份滚动。开启后每次 API 调用以 `INFO` 级别记录 URL、请求体、响应体及脱敏后的 `Authorization`(仅保留前 16 字符)。请求体中的 `password` 等敏感字段会被自动打码。临时调试可设 `HAP_LOG_LEVEL=DEBUG` 覆盖本地配置。
368
+
355
369
  ## 工作流管理
356
370
 
357
371
  ### 创建与编辑流程
@@ -6,14 +6,25 @@ from hap_cli.context import pass_context
6
6
  from hap_cli.core import app as app_mod
7
7
  from hap_cli.utils.formatting import output_json, output_table, output_kv
8
8
  from hap_cli.utils.options import org_id_option, require_api_project_id
9
+ from hap_cli.i18n import t
9
10
 
10
11
 
11
- @click.group()
12
+ @click.group(help=t('Application management.'))
12
13
  def app():
13
- """Application management."""
14
14
  pass
15
15
 
16
16
 
17
+ def _print_apps_with_default(rows, default_app_id, columns, headers):
18
+ marked = []
19
+ for row in rows:
20
+ row = dict(row)
21
+ row["_mark"] = "*" if default_app_id and row.get("appId") == default_app_id else ""
22
+ marked.append(row)
23
+ output_table(marked, ["_mark", *columns], [" ", *headers])
24
+ if default_app_id and any(r.get("appId") == default_app_id for r in rows):
25
+ click.echo(t("* = current default app (set via 'hap app select')."))
26
+
27
+
17
28
  @app.command("list")
18
29
  @org_id_option()
19
30
  @pass_context
@@ -24,8 +35,9 @@ def app_list(ctx, org_id):
24
35
  apps = app_mod.get_all_home_apps(session, require_api_project_id(session, org_id))
25
36
  ctx.output(
26
37
  apps,
27
- lambda d: output_table(
38
+ lambda d: _print_apps_with_default(
28
39
  d,
40
+ session.default_app_id,
29
41
  ["appId", "name", "orgName"],
30
42
  ["App ID", "Name", "Org Name"],
31
43
  ),
@@ -44,8 +56,9 @@ def app_list_managed(ctx, org_id):
44
56
  apps = app_mod.get_managed_apps(session, require_api_project_id(session, org_id))
45
57
  ctx.output(
46
58
  apps,
47
- lambda d: output_table(
59
+ lambda d: _print_apps_with_default(
48
60
  d,
61
+ session.default_app_id,
49
62
  ["appId", "name", "worksheetCount"],
50
63
  ["App ID", "Name", "Worksheets"],
51
64
  ),
@@ -93,6 +106,39 @@ def app_worksheets(ctx, app_id):
93
106
  ctx.handle_error(e)
94
107
 
95
108
 
109
+ @app.command("select")
110
+ @click.argument("app_id")
111
+ @pass_context
112
+ def app_select(ctx, app_id):
113
+ """Set the default application used when --app-id is omitted."""
114
+ try:
115
+ session = ctx.get_session()
116
+ session.default_app_id = app_id
117
+ session.save()
118
+ ctx.output(
119
+ {"status": "ok", "default_app_id": app_id},
120
+ lambda d: click.echo(t("Default app set to: {app_id}").format(app_id=app_id)),
121
+ )
122
+ except Exception as e:
123
+ ctx.handle_error(e)
124
+
125
+
126
+ @app.command("unselect")
127
+ @pass_context
128
+ def app_unselect(ctx):
129
+ """Clear the saved default application."""
130
+ try:
131
+ session = ctx.get_session()
132
+ session.default_app_id = ""
133
+ session.save()
134
+ ctx.output(
135
+ {"status": "ok", "default_app_id": ""},
136
+ lambda d: click.echo(t("Default app cleared.")),
137
+ )
138
+ except Exception as e:
139
+ ctx.handle_error(e)
140
+
141
+
96
142
  # ── App Lifecycle ────────────────────────────────────────────────────────
97
143
 
98
144
 
@@ -360,4 +406,4 @@ def app_logs(ctx, app_id, org_id, page_size, page):
360
406
  )
361
407
  ctx.output(data, lambda d: output_json(d))
362
408
  except Exception as e:
363
- ctx.handle_error(e)
409
+ ctx.handle_error(e)
@@ -7,7 +7,9 @@ import click
7
7
  from hap_cli.context import pass_context
8
8
  from hap_cli.core.session import Session, SessionError
9
9
  from hap_cli.core import auth as auth_mod
10
+ from hap_cli.core import global_meta as gm
10
11
  from hap_cli.utils.formatting import output_json, output_kv
12
+ from hap_cli.i18n import t
11
13
 
12
14
 
13
15
  SERVER_HELP = (
@@ -16,9 +18,8 @@ SERVER_HELP = (
16
18
  )
17
19
 
18
20
 
19
- @click.group()
21
+ @click.group(help=t('Manage authentication and session identity.'))
20
22
  def auth():
21
- """Manage authentication and session identity."""
22
23
  pass
23
24
 
24
25
 
@@ -46,16 +47,44 @@ def _paste_flow(ctx, server):
46
47
  return auth_mod.verify_token(server, token)
47
48
 
48
49
 
49
- def _save_and_report(ctx, token, webui, user_info):
50
+ def _auto_set_language_from_account(user_info: dict) -> None:
51
+ """Persist a CLI language picked from the user's HAP account.
52
+
53
+ No-op when the user has already pinned a language via
54
+ ``hap config language`` (``language_source == "manual"``).
55
+ """
56
+ from hap_cli.core.session import load_config, save_config
57
+ from hap_cli.i18n import lang_from_hap_account, reset_language_cache
58
+
59
+ cfg = load_config() or {}
60
+ if cfg.get("language_source") == "manual":
61
+ return
62
+ lang = lang_from_hap_account(str(user_info.get("lang") or ""))
63
+ save_config({"language": lang, "language_source": "auto"})
64
+ reset_language_cache()
65
+
66
+
67
+ def _save_and_report(ctx, token, webui, user_info, meta):
50
68
  session = ctx.get_session()
51
69
  session.server_url = webui
52
70
  session.auth_token = token
53
71
  session.default_app_id = ""
54
72
  session.default_project_id = ""
55
- orgs = auth_mod.list_my_orgs(session)
73
+ session.default_project_name = ""
74
+ # Harvest routing table + org list from the meta we already fetched
75
+ # during login. No extra round-trip. The meta is fetched from the
76
+ # *exact* login host (see _main_api_base_for_url), so meihua
77
+ # sessions get meihua-specific URLs (api2.mingdao.com etc.) without
78
+ # a synthesised override.
79
+ session.service_endpoints = gm.extract_service_endpoints(meta)
80
+ # Auto-detect UI language from the user's HAP account preference,
81
+ # but only if the user has not pinned a language manually.
82
+ _auto_set_language_from_account(user_info)
83
+ orgs = gm.extract_orgs(meta)
56
84
  current_org = orgs[0] if orgs else {}
57
85
  if current_org.get("projectId"):
58
86
  session.default_project_id = current_org["projectId"]
87
+ session.default_project_name = str(current_org.get("companyName") or "")
59
88
  session.save()
60
89
 
61
90
  data = {
@@ -90,19 +119,51 @@ def auth_login(ctx, server, timeout, token_opt):
90
119
  If the browser flow can't complete (no GUI, callback never fires),
91
120
  interrupt the command to fall back to pasting the token manually.
92
121
  Use --token to supply a token non-interactively for scripts.
122
+
123
+ Shared-credential shortcut: production (``mingdao`` /
124
+ ``https://www.mingdao.com``) and pre-release
125
+ (``https://meihua.mingdao.com``) share user accounts and data —
126
+ only the API routing differs. When you are already logged into one
127
+ and run ``hap auth login`` for the other, the existing token is
128
+ reused and the browser is not opened. If no session exists yet,
129
+ falls back to the regular browser flow.
93
130
  """
94
131
  try:
132
+ # Shared-credential warm switch: skip the browser when an
133
+ # existing session on a sibling host can be reused.
134
+ if not token_opt and auth_mod.can_warm_switch_to(server):
135
+ target_host = auth_mod._resolve_target_host(server)
136
+ if not ctx.json_mode:
137
+ click.echo(f"Switching to {target_host} using existing credentials...")
138
+ token, webui, user_info, meta, reused = (
139
+ auth_mod.shortcut_switch_shared_credential(server)
140
+ )
141
+ _save_and_report(ctx, token, webui, user_info, meta)
142
+ if not ctx.json_mode and reused:
143
+ click.echo("(reused existing token — no browser opened)")
144
+ return
145
+
146
+ # Cold pre-release: no existing session, but target is meihua.
147
+ # The shortcut handles browser-to-prod-then-swap so users still
148
+ # land on meihua at the end.
149
+ if not token_opt and auth_mod.is_prerelease_login(server):
150
+ token, webui, user_info, meta, reused = (
151
+ auth_mod.shortcut_switch_shared_credential(server)
152
+ )
153
+ _save_and_report(ctx, token, webui, user_info, meta)
154
+ return
155
+
95
156
  if token_opt:
96
- token, webui, user_info = auth_mod.verify_token(server, token_opt)
157
+ token, webui, user_info, meta = auth_mod.verify_token(server, token_opt)
97
158
  else:
98
159
  try:
99
160
  _print_browser_hint(ctx, server, timeout)
100
- token, webui, user_info = auth_mod.login(server, timeout=timeout)
161
+ token, webui, user_info, meta = auth_mod.login(server, timeout=timeout)
101
162
  except (TimeoutError, KeyboardInterrupt):
102
163
  if not ctx.json_mode:
103
164
  click.echo("\nSwitching to manual paste mode...")
104
- token, webui, user_info = _paste_flow(ctx, server)
105
- _save_and_report(ctx, token, webui, user_info)
165
+ token, webui, user_info, meta = _paste_flow(ctx, server)
166
+ _save_and_report(ctx, token, webui, user_info, meta)
106
167
  except ImportError as e:
107
168
  ctx.handle_error(SessionError(str(e)))
108
169
  except Exception as e:
@@ -118,6 +179,8 @@ def auth_logout(ctx):
118
179
  session.auth_token = ""
119
180
  session.default_app_id = ""
120
181
  session.default_project_id = ""
182
+ session.default_project_name = ""
183
+ session.service_endpoints = {}
121
184
  session.save()
122
185
  ctx.output(
123
186
  {"status": "ok", "server_url": old_server},
@@ -177,22 +240,24 @@ def auth_whoami(ctx):
177
240
 
178
241
  user_info = auth_mod.get_user_info(session.server_url, session.auth_token)
179
242
 
180
- current_org = {}
243
+ # Prefer the org name cached at login/switch-org time so whoami
244
+ # runs without an extra network round-trip. Fall back to the
245
+ # projects list if the cache is empty (older configs).
181
246
  current_org_id = session.default_project_id
182
- if current_org_id:
247
+ current_org_name = session.default_project_name
248
+ if current_org_id and not current_org_name:
183
249
  try:
184
250
  for org in auth_mod.list_my_orgs(session):
185
251
  if org.get("projectId") == current_org_id:
186
- current_org = org
252
+ current_org_name = str(org.get("companyName") or "")
187
253
  break
188
254
  except Exception:
189
- # Fetching orgs is best-effort; whoami should still work
190
- current_org = {"projectId": current_org_id}
255
+ pass
191
256
 
192
257
  data = {
193
258
  **user_info,
194
- "current_org_id": current_org.get("projectId", ""),
195
- "current_org_name": current_org.get("companyName", ""),
259
+ "current_org_id": current_org_id,
260
+ "current_org_name": current_org_name,
196
261
  }
197
262
  ctx.output(
198
263
  data,
@@ -7,15 +7,15 @@ import click
7
7
  from hap_cli.context import pass_context
8
8
  from hap_cli.core import auth as auth_mod
9
9
  from hap_cli.core import calendar_mod as cal_mod
10
+ from hap_cli.i18n import t
10
11
 
11
12
 
12
13
  FREQUENCY_CHOICES = ("none", "day", "week", "month", "year")
13
14
  REMIND_TYPE_CHOICES = ("none", "minute", "hour", "day")
14
15
 
15
16
 
16
- @click.group()
17
+ @click.group(help=t('Calendar and schedule management.'))
17
18
  def calendar():
18
- """Calendar and schedule management."""
19
19
  pass
20
20
 
21
21
 
@@ -543,4 +543,4 @@ def calendar_categories(ctx):
543
543
  summary = _simplify_categories_result(raw)
544
544
  ctx.output(summary, _render_categories)
545
545
  except Exception as e:
546
- ctx.handle_error(e)
546
+ ctx.handle_error(e)
@@ -8,6 +8,7 @@ from hap_cli.context import pass_context
8
8
  from hap_cli.core import chat as chat_mod
9
9
  from hap_cli.core import group as group_mod
10
10
  from hap_cli.utils.formatting import output_json, output_kv, output_table
11
+ from hap_cli.i18n import t
11
12
 
12
13
 
13
14
  CHAT_SESSION_TYPE_LABELS = {
@@ -35,9 +36,9 @@ def _chat_message_preview(session_item):
35
36
  message = ""
36
37
 
37
38
  prefixes = []
38
- count = session_item.get("count")
39
- if isinstance(count, int) and count > 0:
40
- prefixes.append(f"[{count}条未读]")
39
+ unread = session_item.get("unread")
40
+ if isinstance(unread, int) and unread > 0:
41
+ prefixes.append(f"[{unread}条未读]")
41
42
 
42
43
  if session_item.get("type") == 2:
43
44
  sender_name = ((session_item.get("from") or {}).get("name") or "").strip()
@@ -72,9 +73,8 @@ def _output_chat_list_table(rows):
72
73
  )
73
74
 
74
75
 
75
- @click.group()
76
+ @click.group(help=t('Chat and messaging management.'))
76
77
  def chat():
77
- """Chat and messaging management."""
78
78
  pass
79
79
 
80
80
 
@@ -174,7 +174,6 @@ def chat_files(ctx, with_user, group_id, page_size, page, keywords, file_type, s
174
174
  "--inbox-type", "inbox_type", default=None,
175
175
  help="Raw inbox type code (overrides --category for advanced filters)",
176
176
  )
177
- @click.option("--unread", is_flag=True, help="Only unread messages (inbox mode)")
178
177
  @click.option("--page", "-p", default=1, help="Page number")
179
178
  @click.option("--num", "-n", default=20, help="Messages per page")
180
179
  @click.option(
@@ -187,6 +186,14 @@ def chat_files(ctx, with_user, group_id, page_size, page, keywords, file_type, s
187
186
  type=click.Choice(["", "up", "down"]),
188
187
  help="'up' loads older messages, 'down' loads newer; empty = default",
189
188
  )
189
+ @click.option(
190
+ "--mark-read", "mark_read", is_flag=True, default=False,
191
+ help="After fetching, clear the unread badge for this chat. "
192
+ "Opens a brief socket.io session to emit the same event the "
193
+ "web UI uses when the panel is opened. For --inbox-type "
194
+ "(raw code) this only works when the code maps back to a "
195
+ "known category.",
196
+ )
190
197
  @pass_context
191
198
  def chat_messages(
192
199
  ctx,
@@ -194,12 +201,12 @@ def chat_messages(
194
201
  group_id,
195
202
  category,
196
203
  inbox_type,
197
- unread,
198
204
  page,
199
205
  num,
200
206
  sincetime,
201
207
  keyword,
202
208
  direction,
209
+ mark_read,
203
210
  ):
204
211
  """Fetch chat message history.
205
212
 
@@ -227,6 +234,7 @@ def chat_messages(
227
234
  sincetime=sincetime,
228
235
  keyword=keyword,
229
236
  direction=direction,
237
+ mark_read=mark_read,
230
238
  )
231
239
  elif group_id:
232
240
  data = chat_mod.get_group_messages(
@@ -237,6 +245,7 @@ def chat_messages(
237
245
  sincetime=sincetime,
238
246
  keyword=keyword,
239
247
  direction=direction,
248
+ mark_read=mark_read,
240
249
  )
241
250
  else:
242
251
  type_code = inbox_type or chat_mod.INBOX_CATEGORY_TYPES[category]
@@ -245,9 +254,10 @@ def chat_messages(
245
254
  type_=type_code,
246
255
  page_index=page,
247
256
  page_size=num,
248
- load_type=1 if unread else 2,
249
257
  category=category,
250
258
  )
259
+ if mark_read and category:
260
+ chat_mod.mark_inbox_read(session, category)
251
261
  ctx.output(data, lambda d: output_json(d))
252
262
  except Exception as e:
253
263
  ctx.handle_error(e)
@@ -309,4 +319,3 @@ def chat_card_detail(ctx, task, post, calendar, kcfile, worksheet, worksheetrow)
309
319
  except Exception as e:
310
320
  ctx.handle_error(e)
311
321
 
312
-