aisc-cli 0.1.1__tar.gz → 0.1.3__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 (101) hide show
  1. {aisc_cli-0.1.1/src/aisc_cli.egg-info → aisc_cli-0.1.3}/PKG-INFO +12 -11
  2. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/README.md +11 -10
  3. aisc_cli-0.1.3/src/aisc/VERSION +1 -0
  4. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/application/cc_switch_provider.py +20 -1
  5. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/application/conversation.py +290 -0
  6. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/application/docker_lifecycle.py +1249 -659
  7. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/application/session.py +28 -0
  8. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/application/update.py +18 -2
  9. aisc_cli-0.1.3/src/aisc/application/worklog.py +258 -0
  10. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/cli/commands/build.py +55 -4
  11. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/cli/commands/cc_switch.py +23 -7
  12. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/cli/commands/conversation.py +23 -0
  13. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/cli/commands/run.py +39 -37
  14. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/cli/commands/serve.py +18 -2
  15. aisc_cli-0.1.3/src/aisc/cli/commands/worklog.py +74 -0
  16. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/cli/main.py +173 -6
  17. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/domain/models.py +5 -0
  18. {aisc_cli-0.1.1 → aisc_cli-0.1.3/src/aisc_cli.egg-info}/PKG-INFO +12 -11
  19. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc_cli.egg-info/SOURCES.txt +2 -0
  20. aisc_cli-0.1.1/src/aisc/VERSION +0 -1
  21. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/LICENSE +0 -0
  22. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/MANIFEST.in +0 -0
  23. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/pyproject.toml +0 -0
  24. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/setup.cfg +0 -0
  25. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/__init__.py +0 -0
  26. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/__main__.py +0 -0
  27. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/adapters/__init__.py +0 -0
  28. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/adapters/config_reader.py +0 -0
  29. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/adapters/container_registry.py +0 -0
  30. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/adapters/data_root_store.py +0 -0
  31. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/adapters/docker_.py +0 -0
  32. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/adapters/docker_gateway.py +0 -0
  33. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/adapters/docker_sdk_backed.py +0 -0
  34. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/adapters/maintenance_lock.py +0 -0
  35. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/adapters/state_file.py +0 -0
  36. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/adapters/system.py +0 -0
  37. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/adapters/windows_config_reader.py +0 -0
  38. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/adapters/workspace_lease_store.py +0 -0
  39. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/application/__init__.py +0 -0
  40. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/application/artifact.py +0 -0
  41. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/application/bundle_fetch.py +0 -0
  42. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/application/cc_switch_resolver.py +0 -0
  43. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/application/config_service.py +0 -0
  44. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/application/data_migration.py +0 -0
  45. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/application/data_root.py +0 -0
  46. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/application/doctor.py +0 -0
  47. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/application/legacy_scan.py +0 -0
  48. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/application/network_subscription.py +0 -0
  49. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/application/profile_service.py +0 -0
  50. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/application/provider.py +0 -0
  51. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/application/repair.py +0 -0
  52. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/application/resources.py +0 -0
  53. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/application/runtime.py +0 -0
  54. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/application/tool_versions.py +0 -0
  55. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/application/toolchain.py +0 -0
  56. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/application/usage.py +0 -0
  57. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/application/version.py +0 -0
  58. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/application/web_gateway.py +0 -0
  59. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/application/workspace_reconcile.py +0 -0
  60. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/applog.py +0 -0
  61. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/cli/__init__.py +0 -0
  62. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/cli/build_progress_parser.py +0 -0
  63. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/cli/commands/__init__.py +0 -0
  64. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/cli/commands/agents.py +0 -0
  65. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/cli/commands/artifact.py +0 -0
  66. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/cli/commands/bundle.py +0 -0
  67. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/cli/commands/config.py +0 -0
  68. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/cli/commands/container.py +0 -0
  69. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/cli/commands/data_root.py +0 -0
  70. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/cli/commands/logs.py +0 -0
  71. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/cli/commands/network.py +0 -0
  72. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/cli/commands/profile.py +0 -0
  73. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/cli/commands/provider.py +0 -0
  74. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/cli/commands/runs.py +0 -0
  75. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/cli/commands/runtime.py +0 -0
  76. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/cli/commands/serve_fs.py +0 -0
  77. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/cli/commands/session.py +0 -0
  78. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/cli/commands/update.py +0 -0
  79. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/cli/commands/usage.py +0 -0
  80. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/cli/commands/wizard.py +0 -0
  81. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/cli/commands/workspaces.py +0 -0
  82. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/cli/help_i18n.py +0 -0
  83. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/cli/output.py +0 -0
  84. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/cli/tutorial.py +0 -0
  85. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/domain/__init__.py +0 -0
  86. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/domain/artifacts.py +0 -0
  87. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/domain/cc_switch_release.py +0 -0
  88. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/domain/config.py +0 -0
  89. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/domain/data_migration.py +0 -0
  90. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/domain/data_root.py +0 -0
  91. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/domain/docker_ownership.py +0 -0
  92. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/domain/gateway.py +0 -0
  93. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/domain/toolchain.py +0 -0
  94. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/domain/web_services.py +0 -0
  95. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/domain/workspace_lease.py +0 -0
  96. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/schemas/__init__.py +0 -0
  97. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/schemas/config_schema.py +0 -0
  98. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc_cli.egg-info/dependency_links.txt +0 -0
  99. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc_cli.egg-info/entry_points.txt +0 -0
  100. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc_cli.egg-info/requires.txt +0 -0
  101. {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc_cli.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aisc-cli
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary: AISC CLI — Super Claude workstation management
5
5
  Author: AISC contributors
6
6
  License-Expression: MIT
@@ -122,9 +122,10 @@ AISC 尚未代码签名。SmartScreen 提示时,请先确认文件来自项目
122
122
  | **火山引擎 Ark** | 兼容 OpenAI 格式;需配置接入点 ID |
123
123
  | **智谱 GLM** | GLM-5.2 旗舰模型 |
124
124
  | **Kimi** | 月之暗面 Kimi K3 |
125
- | **Codesome-Group** | 二合一中转;Codex 默认模型 `gpt-5.6-sol`,Claude 侧 `https://v5.codesome.cn/api` |
125
+ | **Codesome V3** | 月卡/按量中转(`sk-` Key,后台建 Key 选分组) |
126
+ | **Codesome 二合一** | V5 月卡中转(`cr-` 卡密即 Key,Claude/GPT 双端通用) |
126
127
 
127
- 所有预置供应商的 Codex 上游格式统一为 **OpenAI Responses API(原生直连)**,存量容器在下次启动时自动刷新(用户自定义值不受影响)。
128
+ 2.1.12 起(D-6.7)镜像**不再预置**任何 provider 行——以上为「添加 provider」的模板清单(Codesome 置顶默认)。所有模板的 Codex 上游走 S9a 形态(本地路由把 Responses 翻译为 Anthropic Messages,直连模板声明的 Anthropic 端点);Codesome 两模板的 Claude 侧不含模型键(官方要求,模型由 Key 的服务端分组路由),并预填官方推荐的自动压缩水位(claude 150k / codex 900k)。
128
129
 
129
130
  ## CLI 使用指南
130
131
 
@@ -390,18 +391,18 @@ MIT License,详见 [LICENSE](https://github.com/wangyuncepu/AISC/blob/main/LIC
390
391
 
391
392
  ### Codesome|Codex 与 Claude Code 二合一服务
392
393
 
393
- Codesome 提供 API 调用形式的二合一月卡:一张 `cr-...` API Key 可分别接入 Claude Code 和 Codex。这是 API 服务,不是 Claude 或 Codex 成品账号。
394
+ Codesome 提供 API 调用形式的接入服务,有两条产品线:**V3**(Claude/GPT 月卡 + 按量,`sk-...` Key,在后台建 Key 并选分组)与**二合一/V5**(月卡,订单里的 `cr-...` 卡密直接就是 Key,Claude/GPT 双端通用,无分组)。这是 API 服务,不是 Claude 或 Codex 成品账号。
394
395
 
395
- **v2.1.7 起 AISC 镜像已内置 Codesome-Group 预置供应商**——Workbench 的 Providers 页或容器内 cc-switch 选择 Codesome-Group、填入 API Key 即可使用,无需手工配置端点。
396
+ **2.1.12 起内置两个 Codesome 模板**(`codesome-v3` / `codesome-2in1`)——Workbench Providers 页「添加 provider」默认选中 Codesome,选对产品线、填入 Key 即可(表单下方「获取服务」直达注册/购买页)。两条线的地址不可混用,V3 的 `sk-` Key 不能配二合一,反之亦然。
396
397
 
397
- 手动配置时的端点(二合一 Key 两端通用,URL 不同不可混用):
398
+ 手动配置时的端点(两线各自两端通用,URL 不同不可混用):
398
399
 
399
- | 使用端 | API URL |
400
- | --- | --- |
401
- | Claude Code / Claude | `https://v5.codesome.cn/api` |
402
- | Codex / OpenAI 格式客户端 | `https://v5.codesome.cn/openai` |
400
+ | 产品线 | Claude 端 | Codex / OpenAI 格式端 |
401
+ | --- | --- | --- |
402
+ | V3(sk-) | `https://cc.codesome.ai` | `https://cc.codesome.ai/v1` |
403
+ | 二合一(cr-) | `https://v5.codesome.cn/api` | `https://v5.codesome.cn/openai` |
403
404
 
404
- Claude Code 通过 `ANTHROPIC_BASE_URL` 和 `ANTHROPIC_AUTH_TOKEN` 配置;Codex 使用 OpenAI 格式配置。完整教程见 [Codesome 文档](https://doc.codesome.ai/)。
405
+ Claude Code 通过 `ANTHROPIC_BASE_URL` 和 `ANTHROPIC_AUTH_TOKEN` 配置(加 `CLAUDE_CODE_ATTRIBUTION_HEADER=0`,官方核心三行之一);Codex 使用 OpenAI 格式配置(模型统一 `gpt-5.6-terra`)。可选:自动压缩(claude `CLAUDE_AUTO_COMPACT_WINDOW`,codex `model_auto_compact_token_limit`)到值自动压缩历史。完整教程见 [Codesome 文档](https://doc.codesome.ai/)。
405
406
 
406
407
  购买与开通(注册/下单入口,非服务使用地址):
407
408
 
@@ -91,9 +91,10 @@ AISC 尚未代码签名。SmartScreen 提示时,请先确认文件来自项目
91
91
  | **火山引擎 Ark** | 兼容 OpenAI 格式;需配置接入点 ID |
92
92
  | **智谱 GLM** | GLM-5.2 旗舰模型 |
93
93
  | **Kimi** | 月之暗面 Kimi K3 |
94
- | **Codesome-Group** | 二合一中转;Codex 默认模型 `gpt-5.6-sol`,Claude 侧 `https://v5.codesome.cn/api` |
94
+ | **Codesome V3** | 月卡/按量中转(`sk-` Key,后台建 Key 选分组) |
95
+ | **Codesome 二合一** | V5 月卡中转(`cr-` 卡密即 Key,Claude/GPT 双端通用) |
95
96
 
96
- 所有预置供应商的 Codex 上游格式统一为 **OpenAI Responses API(原生直连)**,存量容器在下次启动时自动刷新(用户自定义值不受影响)。
97
+ 2.1.12 起(D-6.7)镜像**不再预置**任何 provider 行——以上为「添加 provider」的模板清单(Codesome 置顶默认)。所有模板的 Codex 上游走 S9a 形态(本地路由把 Responses 翻译为 Anthropic Messages,直连模板声明的 Anthropic 端点);Codesome 两模板的 Claude 侧不含模型键(官方要求,模型由 Key 的服务端分组路由),并预填官方推荐的自动压缩水位(claude 150k / codex 900k)。
97
98
 
98
99
  ## CLI 使用指南
99
100
 
@@ -359,18 +360,18 @@ MIT License,详见 [LICENSE](https://github.com/wangyuncepu/AISC/blob/main/LIC
359
360
 
360
361
  ### Codesome|Codex 与 Claude Code 二合一服务
361
362
 
362
- Codesome 提供 API 调用形式的二合一月卡:一张 `cr-...` API Key 可分别接入 Claude Code 和 Codex。这是 API 服务,不是 Claude 或 Codex 成品账号。
363
+ Codesome 提供 API 调用形式的接入服务,有两条产品线:**V3**(Claude/GPT 月卡 + 按量,`sk-...` Key,在后台建 Key 并选分组)与**二合一/V5**(月卡,订单里的 `cr-...` 卡密直接就是 Key,Claude/GPT 双端通用,无分组)。这是 API 服务,不是 Claude 或 Codex 成品账号。
363
364
 
364
- **v2.1.7 起 AISC 镜像已内置 Codesome-Group 预置供应商**——Workbench 的 Providers 页或容器内 cc-switch 选择 Codesome-Group、填入 API Key 即可使用,无需手工配置端点。
365
+ **2.1.12 起内置两个 Codesome 模板**(`codesome-v3` / `codesome-2in1`)——Workbench Providers 页「添加 provider」默认选中 Codesome,选对产品线、填入 Key 即可(表单下方「获取服务」直达注册/购买页)。两条线的地址不可混用,V3 的 `sk-` Key 不能配二合一,反之亦然。
365
366
 
366
- 手动配置时的端点(二合一 Key 两端通用,URL 不同不可混用):
367
+ 手动配置时的端点(两线各自两端通用,URL 不同不可混用):
367
368
 
368
- | 使用端 | API URL |
369
- | --- | --- |
370
- | Claude Code / Claude | `https://v5.codesome.cn/api` |
371
- | Codex / OpenAI 格式客户端 | `https://v5.codesome.cn/openai` |
369
+ | 产品线 | Claude 端 | Codex / OpenAI 格式端 |
370
+ | --- | --- | --- |
371
+ | V3(sk-) | `https://cc.codesome.ai` | `https://cc.codesome.ai/v1` |
372
+ | 二合一(cr-) | `https://v5.codesome.cn/api` | `https://v5.codesome.cn/openai` |
372
373
 
373
- Claude Code 通过 `ANTHROPIC_BASE_URL` 和 `ANTHROPIC_AUTH_TOKEN` 配置;Codex 使用 OpenAI 格式配置。完整教程见 [Codesome 文档](https://doc.codesome.ai/)。
374
+ Claude Code 通过 `ANTHROPIC_BASE_URL` 和 `ANTHROPIC_AUTH_TOKEN` 配置(加 `CLAUDE_CODE_ATTRIBUTION_HEADER=0`,官方核心三行之一);Codex 使用 OpenAI 格式配置(模型统一 `gpt-5.6-terra`)。可选:自动压缩(claude `CLAUDE_AUTO_COMPACT_WINDOW`,codex `model_auto_compact_token_limit`)到值自动压缩历史。完整教程见 [Codesome 文档](https://doc.codesome.ai/)。
374
375
 
375
376
  购买与开通(注册/下单入口,非服务使用地址):
376
377
 
@@ -0,0 +1 @@
1
+ 0.1.3
@@ -21,7 +21,7 @@ from aisc.domain.models import CliError, RuntimeExitCode
21
21
  PROTOCOL = "aisc.cc-switch-provider/v1"
22
22
  _ADAPTER_PATH = "/usr/local/bin/aisc-cc-provider"
23
23
  _AGENTS = ("claude", "codex")
24
- _OPS = ("list", "add", "edit", "switch", "delete", "fetch-models")
24
+ _OPS = ("list", "add", "edit", "switch", "delete", "fetch-models", "templates")
25
25
 
26
26
 
27
27
  def _validate(runtime_id: str, agent: str, op: str) -> None:
@@ -161,6 +161,25 @@ def list_providers(runtime_id: str, agent: str, workspace: Optional[str], execut
161
161
  "operation_id": envelope.get("operation_id")}
162
162
 
163
163
 
164
+ def list_templates(runtime_id: str, agent: str, workspace: Optional[str],
165
+ executor: Any) -> Dict[str, Any]:
166
+ """D-6.8: the add-provider template manifest (adapter `templates` op).
167
+
168
+ The op itself is agent-less; the transport keeps the uniform --agent
169
+ argument (validated, then ignored inside the adapter)."""
170
+ from aisc.application.data_root import workspace_state_dir
171
+ from pathlib import Path
172
+
173
+ ws_path = Path(workspace).resolve() if workspace else Path.cwd()
174
+ _validate(runtime_id, agent, "templates")
175
+ envelope = _exec_adapter(
176
+ runtime_id, workspace_state_dir(ws_path), executor, "templates", agent,
177
+ None, None,
178
+ )
179
+ return {"agent": agent, "templates": envelope.get("templates") or [],
180
+ "operation_id": envelope.get("operation_id")}
181
+
182
+
164
183
  def add_provider(runtime_id: str, agent: str, request: Dict[str, Any],
165
184
  workspace: Optional[str], executor: Any) -> Dict[str, Any]:
166
185
  from aisc.application.data_root import workspace_state_dir
@@ -514,3 +514,293 @@ def rename_conversation(workspace: str, conversation_id: str, agent: str,
514
514
  "agent": agent,
515
515
  "title": clean,
516
516
  }
517
+
518
+
519
+ # ---------------------------------------------------------------------------
520
+ # Conversation read (批 2 聊天式 UI, 2026-09-21): normalized read-only stream.
521
+ # Provider transcripts stay strictly read-only (module-head rule); this layer
522
+ # only PROJECTS them into a stable rendering schema — no writes anywhere.
523
+ # ---------------------------------------------------------------------------
524
+
525
+ CONVERSATION_READ_SCHEMA = "aisc.conversation-read/v1"
526
+ _READ_TEXT_CAP = 8000 # per user/assistant message (chars)
527
+ _READ_REASONING_CAP = 4000
528
+ _READ_TOOL_CAP = 4000 # per tool_use input / tool_result output
529
+ _READ_MAX_MESSAGES = 4000 # hard bound on the rendered stream
530
+
531
+
532
+ def _read_join_text(content: Any, block_type: str) -> Optional[str]:
533
+ """Join EVERY ``block_type`` text block (unlike _text_from_content, which
534
+ returns the first). A chat bubble must not silently drop the rest of a
535
+ multi-block message."""
536
+ if isinstance(content, str):
537
+ return content
538
+ if isinstance(content, list):
539
+ parts = []
540
+ for block in content:
541
+ if isinstance(block, dict) and block.get("type") == block_type:
542
+ text = block.get("text")
543
+ if isinstance(text, str) and text.strip():
544
+ parts.append(text)
545
+ if parts:
546
+ return "\n\n".join(parts)
547
+ return None
548
+
549
+
550
+ def _read_as_text(value: Any) -> Optional[str]:
551
+ """Best-effort text for tool arguments/outputs: strings pass through,
552
+ structured payloads serialize (bounded by the caller's cap)."""
553
+ if value is None:
554
+ return None
555
+ if isinstance(value, str):
556
+ return value
557
+ try:
558
+ return json.dumps(value, ensure_ascii=False, indent=1)
559
+ except (TypeError, ValueError):
560
+ return str(value)
561
+
562
+
563
+ def _read_cap(text: Any, cap: int) -> str:
564
+ """Coerce to str and hard-cap, marking the cut in the text itself (the
565
+ chat pane renders plain text — a side flag would be invisible there)."""
566
+ out = text if isinstance(text, str) else ("" if text is None else str(text))
567
+ if len(out) > cap:
568
+ return out[:cap] + f"\n…[截断,共 {len(out)} 字符]"
569
+ return out
570
+
571
+
572
+ def _read_summary_text(summary: Any) -> Optional[str]:
573
+ """Codex reasoning payload: ``summary`` is a list of summary blocks."""
574
+ if isinstance(summary, list):
575
+ parts = []
576
+ for block in summary:
577
+ if isinstance(block, dict) and isinstance(block.get("text"), str) \
578
+ and block["text"].strip():
579
+ parts.append(block["text"])
580
+ if parts:
581
+ return "\n\n".join(parts)
582
+ if isinstance(summary, str) and summary.strip():
583
+ return summary
584
+ return None
585
+
586
+
587
+ def _read_entries_codex(obj: Dict[str, Any]) -> List[Dict[str, Any]]:
588
+ """One codex rollout line → zero or more rendered entries. Rendered:
589
+ user/assistant messages, reasoning summaries, function calls/results.
590
+ Dropped: session_meta, event_msg, world_state, turn_context, developer
591
+ messages, injected-context user blocks."""
592
+ if obj.get("type") != "response_item":
593
+ return []
594
+ payload = obj.get("payload")
595
+ if not isinstance(payload, dict):
596
+ return []
597
+ ts = obj.get("timestamp") if isinstance(obj.get("timestamp"), str) else None
598
+ kind = payload.get("type")
599
+ if kind == "message":
600
+ role = payload.get("role")
601
+ if role == "user":
602
+ text = _read_join_text(payload.get("content"), "input_text")
603
+ if not text or not text.strip():
604
+ return []
605
+ if _CONTEXT_TITLE_RE.match(text.lstrip()):
606
+ return []
607
+ return [{"role": "user", "kind": "message", "text": text, "ts": ts}]
608
+ if role == "assistant":
609
+ text = _read_join_text(payload.get("content"), "output_text")
610
+ if not text or not text.strip():
611
+ return []
612
+ return [{"role": "assistant", "kind": "message", "text": text, "ts": ts}]
613
+ return []
614
+ if kind == "reasoning":
615
+ text = _read_summary_text(payload.get("summary"))
616
+ if not text:
617
+ return []
618
+ return [{"role": "assistant", "kind": "reasoning", "text": text, "ts": ts}]
619
+ if kind == "function_call":
620
+ name = payload.get("name")
621
+ call_id = payload.get("call_id")
622
+ return [{
623
+ "role": "assistant", "kind": "tool_use",
624
+ "name": name if isinstance(name, str) else "tool",
625
+ "text": _read_as_text(payload.get("arguments")),
626
+ "call_id": call_id if isinstance(call_id, str) else None,
627
+ "ts": ts,
628
+ }]
629
+ if kind == "function_call_output":
630
+ call_id = payload.get("call_id")
631
+ return [{
632
+ "role": "tool", "kind": "tool_result",
633
+ "text": _read_as_text(payload.get("output")),
634
+ "call_id": call_id if isinstance(call_id, str) else None,
635
+ "ts": ts,
636
+ }]
637
+ return []
638
+
639
+
640
+ def _read_entries_claude(obj: Dict[str, Any]) -> List[Dict[str, Any]]:
641
+ """One Claude session line → zero or more rendered entries. Rendered:
642
+ user/assistant text, thinking, tool_use, tool_result. Dropped:
643
+ queue-operation/attachment/last-prompt/atis-latch and sidechain
644
+ (subagent) transcripts — the main thread only."""
645
+ if obj.get("type") not in ("user", "assistant"):
646
+ return []
647
+ if obj.get("isSidechain") is True:
648
+ return []
649
+ msg = obj.get("message")
650
+ if not isinstance(msg, dict):
651
+ return []
652
+ ts = obj.get("timestamp") if isinstance(obj.get("timestamp"), str) else None
653
+ role = "user" if obj.get("type") == "user" else "assistant"
654
+ content = msg.get("content")
655
+ out: List[Dict[str, Any]] = []
656
+
657
+ def _push_text(text: Any) -> None:
658
+ if isinstance(text, str) and text.strip():
659
+ if role == "user" and _CONTEXT_TITLE_RE.match(text.lstrip()):
660
+ return
661
+ out.append({"role": role, "kind": "message", "text": text, "ts": ts})
662
+
663
+ if isinstance(content, str):
664
+ _push_text(content)
665
+ return out
666
+ if isinstance(content, list):
667
+ for block in content:
668
+ if not isinstance(block, dict):
669
+ continue
670
+ bt = block.get("type")
671
+ if bt == "text":
672
+ _push_text(block.get("text"))
673
+ elif bt == "thinking":
674
+ text = block.get("thinking")
675
+ if isinstance(text, str) and text.strip():
676
+ out.append({"role": "assistant", "kind": "reasoning",
677
+ "text": text, "ts": ts})
678
+ elif bt == "tool_use":
679
+ call_id = block.get("id")
680
+ out.append({
681
+ "role": "assistant", "kind": "tool_use",
682
+ "name": block.get("name") if isinstance(block.get("name"), str) else "tool",
683
+ "text": _read_as_text(block.get("input")),
684
+ "call_id": call_id if isinstance(call_id, str) else None,
685
+ "ts": ts,
686
+ })
687
+ elif bt == "tool_result":
688
+ inner = block.get("content")
689
+ text = inner if isinstance(inner, str) else _read_join_text(inner, "text")
690
+ call_id = block.get("tool_use_id")
691
+ out.append({
692
+ "role": "tool", "kind": "tool_result",
693
+ "text": text,
694
+ "call_id": call_id if isinstance(call_id, str) else None,
695
+ "ts": ts,
696
+ })
697
+ return out
698
+
699
+
700
+ _READ_EXTRACTORS = {
701
+ "codex": _read_entries_codex,
702
+ "claude": _read_entries_claude,
703
+ }
704
+
705
+
706
+ def read_conversation(workspace: str, conversation_id: str, agent: str,
707
+ tail: Optional[int] = None,
708
+ before: Optional[int] = None) -> Dict[str, Any]:
709
+ """Project a provider transcript into the chat rendering stream.
710
+
711
+ Pagination walks the RENDERED stream backwards: ``tail`` returns the
712
+ last ``tail`` entries; ``before`` (0-based exclusive ordinal) returns
713
+ the window immediately preceding it. Ordinals are stable per file
714
+ version — the UI pages with ``before=start`` until start reaches 0.
715
+ Malformed lines are skipped and counted, never fatal (list semantics)."""
716
+ if not is_conversation_uuid(conversation_id):
717
+ raise CliError(
718
+ message=f"Invalid conversation ID: {conversation_id}",
719
+ exit_code=2,
720
+ error_code=ERROR_INVALID_ID,
721
+ )
722
+ if agent not in _AGENTS:
723
+ raise CliError(
724
+ message=f"Unsupported agent: {agent}",
725
+ exit_code=2,
726
+ error_code=ERROR_INVALID_AGENT,
727
+ )
728
+ ws_dir = _workspace_dir(workspace)
729
+ path = _find_conversation_file(ws_dir, agent, conversation_id)
730
+ if path is None:
731
+ raise CliError(
732
+ message=f"Conversation {conversation_id} not found for agent {agent}",
733
+ exit_code=3,
734
+ error_code=ERROR_UNRESUMABLE,
735
+ )
736
+ extract = _READ_EXTRACTORS[agent]
737
+ entries: List[Dict[str, Any]] = []
738
+ malformed_lines = 0
739
+ try:
740
+ fh = path.open("r", encoding="utf-8", errors="replace")
741
+ except OSError as e:
742
+ raise CliError(
743
+ message=f"Conversation file unreadable: {e}",
744
+ exit_code=3,
745
+ error_code=ERROR_UNRESUMABLE,
746
+ )
747
+ with fh:
748
+ for line in fh:
749
+ line = line.strip()
750
+ if not line:
751
+ continue
752
+ try:
753
+ obj = json.loads(line)
754
+ except (ValueError, TypeError):
755
+ malformed_lines += 1
756
+ continue
757
+ if not isinstance(obj, dict):
758
+ continue
759
+ for entry in extract(obj):
760
+ if len(entries) >= _READ_MAX_MESSAGES:
761
+ break # stream bound; the tail window still pages correctly
762
+ entries.append(entry)
763
+ # Pair tool results into their calls (nearest preceding unmatched match);
764
+ # unpaired results stay standalone so nothing is silently dropped.
765
+ open_calls: Dict[str, Dict[str, Any]] = {}
766
+ for entry in entries:
767
+ if entry["kind"] == "tool_use" and entry.get("call_id"):
768
+ open_calls[entry["call_id"]] = entry
769
+ elif entry["kind"] == "tool_result" and entry.get("call_id"):
770
+ call = open_calls.get(entry["call_id"])
771
+ if call is not None and "result" not in call:
772
+ call["result"] = entry.pop("text")
773
+ entry["consumed"] = True
774
+ rendered = [e for e in entries if not e.get("consumed")]
775
+ for i, entry in enumerate(rendered):
776
+ entry["ordinal"] = i
777
+ cap = _READ_TEXT_CAP
778
+ if entry["kind"] == "reasoning":
779
+ cap = _READ_REASONING_CAP
780
+ elif entry["kind"] == "tool_use":
781
+ entry["text"] = _read_cap(entry.get("text"), _READ_TOOL_CAP)
782
+ if "result" in entry:
783
+ entry["result"] = _read_cap(entry["result"], _READ_TOOL_CAP)
784
+ continue
785
+ elif entry["kind"] == "tool_result":
786
+ cap = _READ_TOOL_CAP
787
+ entry["text"] = _read_cap(entry.get("text"), cap)
788
+
789
+ total = len(rendered)
790
+ start = 0
791
+ end = total
792
+ if before is not None:
793
+ end = max(0, min(int(before), total))
794
+ if tail is not None and tail > 0:
795
+ start = max(0, end - int(tail))
796
+ return {
797
+ "schema": CONVERSATION_READ_SCHEMA,
798
+ "conversation_id": conversation_id,
799
+ "agent": agent,
800
+ "file_size": path.stat().st_size,
801
+ "total": total,
802
+ "start": start,
803
+ "messages": rendered[start:end],
804
+ "malformed_lines": malformed_lines,
805
+ "degraded_reason": "malformed" if malformed_lines else None,
806
+ }