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.
- {aisc_cli-0.1.1/src/aisc_cli.egg-info → aisc_cli-0.1.3}/PKG-INFO +12 -11
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/README.md +11 -10
- aisc_cli-0.1.3/src/aisc/VERSION +1 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/application/cc_switch_provider.py +20 -1
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/application/conversation.py +290 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/application/docker_lifecycle.py +1249 -659
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/application/session.py +28 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/application/update.py +18 -2
- aisc_cli-0.1.3/src/aisc/application/worklog.py +258 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/cli/commands/build.py +55 -4
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/cli/commands/cc_switch.py +23 -7
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/cli/commands/conversation.py +23 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/cli/commands/run.py +39 -37
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/cli/commands/serve.py +18 -2
- aisc_cli-0.1.3/src/aisc/cli/commands/worklog.py +74 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/cli/main.py +173 -6
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/domain/models.py +5 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3/src/aisc_cli.egg-info}/PKG-INFO +12 -11
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc_cli.egg-info/SOURCES.txt +2 -0
- aisc_cli-0.1.1/src/aisc/VERSION +0 -1
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/LICENSE +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/MANIFEST.in +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/pyproject.toml +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/setup.cfg +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/__init__.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/__main__.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/adapters/__init__.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/adapters/config_reader.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/adapters/container_registry.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/adapters/data_root_store.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/adapters/docker_.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/adapters/docker_gateway.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/adapters/docker_sdk_backed.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/adapters/maintenance_lock.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/adapters/state_file.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/adapters/system.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/adapters/windows_config_reader.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/adapters/workspace_lease_store.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/application/__init__.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/application/artifact.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/application/bundle_fetch.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/application/cc_switch_resolver.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/application/config_service.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/application/data_migration.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/application/data_root.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/application/doctor.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/application/legacy_scan.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/application/network_subscription.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/application/profile_service.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/application/provider.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/application/repair.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/application/resources.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/application/runtime.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/application/tool_versions.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/application/toolchain.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/application/usage.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/application/version.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/application/web_gateway.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/application/workspace_reconcile.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/applog.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/cli/__init__.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/cli/build_progress_parser.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/cli/commands/__init__.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/cli/commands/agents.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/cli/commands/artifact.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/cli/commands/bundle.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/cli/commands/config.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/cli/commands/container.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/cli/commands/data_root.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/cli/commands/logs.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/cli/commands/network.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/cli/commands/profile.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/cli/commands/provider.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/cli/commands/runs.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/cli/commands/runtime.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/cli/commands/serve_fs.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/cli/commands/session.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/cli/commands/update.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/cli/commands/usage.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/cli/commands/wizard.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/cli/commands/workspaces.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/cli/help_i18n.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/cli/output.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/cli/tutorial.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/domain/__init__.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/domain/artifacts.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/domain/cc_switch_release.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/domain/config.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/domain/data_migration.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/domain/data_root.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/domain/docker_ownership.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/domain/gateway.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/domain/toolchain.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/domain/web_services.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/domain/workspace_lease.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/schemas/__init__.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc/schemas/config_schema.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc_cli.egg-info/dependency_links.txt +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc_cli.egg-info/entry_points.txt +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.3}/src/aisc_cli.egg-info/requires.txt +0 -0
- {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.
|
|
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
|
|
125
|
+
| **Codesome V3** | 月卡/按量中转(`sk-` Key,后台建 Key 选分组) |
|
|
126
|
+
| **Codesome 二合一** | V5 月卡中转(`cr-` 卡密即 Key,Claude/GPT 双端通用) |
|
|
126
127
|
|
|
127
|
-
|
|
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
|
|
394
|
+
Codesome 提供 API 调用形式的接入服务,有两条产品线:**V3**(Claude/GPT 月卡 + 按量,`sk-...` Key,在后台建 Key 并选分组)与**二合一/V5**(月卡,订单里的 `cr-...` 卡密直接就是 Key,Claude/GPT 双端通用,无分组)。这是 API 服务,不是 Claude 或 Codex 成品账号。
|
|
394
395
|
|
|
395
|
-
**
|
|
396
|
+
**2.1.12 起内置两个 Codesome 模板**(`codesome-v3` / `codesome-2in1`)——Workbench Providers 页「添加 provider」默认选中 Codesome,选对产品线、填入 Key 即可(表单下方「获取服务」直达注册/购买页)。两条线的地址不可混用,V3 的 `sk-` Key 不能配二合一,反之亦然。
|
|
396
397
|
|
|
397
|
-
|
|
398
|
+
手动配置时的端点(两线各自两端通用,URL 不同不可混用):
|
|
398
399
|
|
|
399
|
-
|
|
|
400
|
-
| --- | --- |
|
|
401
|
-
|
|
|
402
|
-
|
|
|
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`
|
|
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
|
|
94
|
+
| **Codesome V3** | 月卡/按量中转(`sk-` Key,后台建 Key 选分组) |
|
|
95
|
+
| **Codesome 二合一** | V5 月卡中转(`cr-` 卡密即 Key,Claude/GPT 双端通用) |
|
|
95
96
|
|
|
96
|
-
|
|
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
|
|
363
|
+
Codesome 提供 API 调用形式的接入服务,有两条产品线:**V3**(Claude/GPT 月卡 + 按量,`sk-...` Key,在后台建 Key 并选分组)与**二合一/V5**(月卡,订单里的 `cr-...` 卡密直接就是 Key,Claude/GPT 双端通用,无分组)。这是 API 服务,不是 Claude 或 Codex 成品账号。
|
|
363
364
|
|
|
364
|
-
**
|
|
365
|
+
**2.1.12 起内置两个 Codesome 模板**(`codesome-v3` / `codesome-2in1`)——Workbench Providers 页「添加 provider」默认选中 Codesome,选对产品线、填入 Key 即可(表单下方「获取服务」直达注册/购买页)。两条线的地址不可混用,V3 的 `sk-` Key 不能配二合一,反之亦然。
|
|
365
366
|
|
|
366
|
-
|
|
367
|
+
手动配置时的端点(两线各自两端通用,URL 不同不可混用):
|
|
367
368
|
|
|
368
|
-
|
|
|
369
|
-
| --- | --- |
|
|
370
|
-
|
|
|
371
|
-
|
|
|
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`
|
|
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
|
+
}
|