deepflow-openagent-cli 0.1.6__tar.gz → 0.1.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.
- {deepflow_openagent_cli-0.1.6 → deepflow_openagent_cli-0.1.8}/PKG-INFO +118 -3
- {deepflow_openagent_cli-0.1.6 → deepflow_openagent_cli-0.1.8}/README.md +117 -2
- {deepflow_openagent_cli-0.1.6 → deepflow_openagent_cli-0.1.8}/pyproject.toml +1 -1
- {deepflow_openagent_cli-0.1.6 → deepflow_openagent_cli-0.1.8}/src/deepflow_openagent_cli/__init__.py +1 -1
- {deepflow_openagent_cli-0.1.6 → deepflow_openagent_cli-0.1.8}/src/deepflow_openagent_cli/cli.py +160 -0
- {deepflow_openagent_cli-0.1.6 → deepflow_openagent_cli-0.1.8}/src/deepflow_openagent_cli.egg-info/PKG-INFO +118 -3
- {deepflow_openagent_cli-0.1.6 → deepflow_openagent_cli-0.1.8}/tests/test_cloudpc_cli.py +180 -0
- {deepflow_openagent_cli-0.1.6 → deepflow_openagent_cli-0.1.8}/setup.cfg +0 -0
- {deepflow_openagent_cli-0.1.6 → deepflow_openagent_cli-0.1.8}/src/deepflow_openagent_cli/__main__.py +0 -0
- {deepflow_openagent_cli-0.1.6 → deepflow_openagent_cli-0.1.8}/src/deepflow_openagent_cli.egg-info/SOURCES.txt +0 -0
- {deepflow_openagent_cli-0.1.6 → deepflow_openagent_cli-0.1.8}/src/deepflow_openagent_cli.egg-info/dependency_links.txt +0 -0
- {deepflow_openagent_cli-0.1.6 → deepflow_openagent_cli-0.1.8}/src/deepflow_openagent_cli.egg-info/entry_points.txt +0 -0
- {deepflow_openagent_cli-0.1.6 → deepflow_openagent_cli-0.1.8}/src/deepflow_openagent_cli.egg-info/requires.txt +0 -0
- {deepflow_openagent_cli-0.1.6 → deepflow_openagent_cli-0.1.8}/src/deepflow_openagent_cli.egg-info/top_level.txt +0 -0
- {deepflow_openagent_cli-0.1.6 → deepflow_openagent_cli-0.1.8}/tests/test_cli.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: deepflow-openagent-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.8
|
|
4
4
|
Summary: Command-line client for OpenAgent OpenAPI (agents, conversations, SSE chat, tools, channels, knowledge bases).
|
|
5
5
|
Author: DeepFlow AI
|
|
6
6
|
Project-URL: Repository, https://github.com/DeepFlowAI/OpenAgent
|
|
@@ -158,6 +158,30 @@ selected version does not exist; use `--from base` for a fresh environment.
|
|
|
158
158
|
Version ID is different from the `x.y.z` version label. First save automatically
|
|
159
159
|
publishes; later saves require a separate `versions publish` command.
|
|
160
160
|
|
|
161
|
+
Binary uploads (including PDF) use the same sandbox upload writer as the workbench:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
deepflow_openagent_cli cloudpc files upload 123 --file ./report.pdf
|
|
165
|
+
# After retry exhaustion, reuse BOTH printed IDs and the same local file:
|
|
166
|
+
deepflow_openagent_cli cloudpc files upload 123 --file ./report.pdf \
|
|
167
|
+
--upload-id UPLOAD_ID --sandbox-id SANDBOX_ID
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Uploads accept 1 byte–50 MiB and create `/workspace/files/uploads` automatically.
|
|
171
|
+
The returned path is `/workspace/files/uploads/<upload_id>-<original_filename>`;
|
|
172
|
+
use that path in subsequent OCR or chat commands. The ID prefix prevents duplicate
|
|
173
|
+
files after a lost response. Reusing the same ID/name with different bytes is
|
|
174
|
+
rejected. The sandbox ID pins retries to the original development environment.
|
|
175
|
+
This admin/config API accepts binary content; channel visitor format restrictions
|
|
176
|
+
still apply only to visitor uploads. Uploading does not automatically start OCR.
|
|
177
|
+
|
|
178
|
+
Uploads and `dev exec` submission/status/stop retry connection failures and HTTP
|
|
179
|
+
408/429/502/503/504 up to four times (1/2/4/8 second delays). Execution retries keep
|
|
180
|
+
the same execution ID and output cursor. Upload retries resend the whole binary
|
|
181
|
+
body; chunk resume is not implemented. Other mutating commands are not retried.
|
|
182
|
+
Requires updated CLI, API server and Cloud PC control plane; no VM image rebuild
|
|
183
|
+
is needed for the upload writer.
|
|
184
|
+
|
|
161
185
|
Additional operations:
|
|
162
186
|
|
|
163
187
|
```bash
|
|
@@ -176,8 +200,9 @@ Config update replaces configuration: provide `model_name`, `thinking_enabled`
|
|
|
176
200
|
and the complete `env_vars` list; omitted thinking/env fields reset to defaults.
|
|
177
201
|
Config queries include environment variable values. File mutation and snapshot
|
|
178
202
|
operations share the per-agent management lock; direct writes by the sandbox's
|
|
179
|
-
Agent or terminal are outside that lock.
|
|
180
|
-
timeout and
|
|
203
|
+
Agent or terminal are outside that lock. Upload and execution requests have a
|
|
204
|
+
120-second per-attempt timeout and the retries described above. Other Cloud PC
|
|
205
|
+
HTTP requests have a 900-second timeout and are not automatically retried. After a timeout, inspect sandbox or
|
|
181
206
|
version state before retrying a mutation. Conversation logs and chat-control
|
|
182
207
|
routes still require an administrator session.
|
|
183
208
|
|
|
@@ -255,3 +280,93 @@ After installation or Agent edits, use `cloudpc versions save` and optionally
|
|
|
255
280
|
`versions publish` to preserve and publish the environment. Deploy the updated
|
|
256
281
|
OpenAgent backend, cloudpc VM service, and bridge bundle together; update the CLI
|
|
257
282
|
package as well. No database migration is required.
|
|
283
|
+
|
|
284
|
+
### Cloud PC channels
|
|
285
|
+
|
|
286
|
+
Cloud PC channel settings use `cloudpc channels`, separately from Agent settings
|
|
287
|
+
(`cloudpc config`) and Web SDK channels (`channels`). These commands require an
|
|
288
|
+
API key with `config` scope and Cloud PC enabled for the tenant. The backend must
|
|
289
|
+
also include support for API key authentication on `/api/v1/cloudpc/channels`;
|
|
290
|
+
older backends accept administrator sessions only.
|
|
291
|
+
|
|
292
|
+
```bash
|
|
293
|
+
deepflow_openagent_cli cloudpc channels list --page 1 --per-page 20
|
|
294
|
+
deepflow_openagent_cli cloudpc channels create --name "Cloud workspace"
|
|
295
|
+
deepflow_openagent_cli cloudpc channels agent-options
|
|
296
|
+
deepflow_openagent_cli cloudpc channels get 7
|
|
297
|
+
deepflow_openagent_cli docs show cloudpc.channels.update --json
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Update uses the existing PUT replacement contract. Read the current channel
|
|
301
|
+
first and prepare `channel.json` with the complete name, description, ordered
|
|
302
|
+
`agent_ids` and `appearance`. Convert the returned `bindings` to their `id`
|
|
303
|
+
values; the GET response is not itself an update payload.
|
|
304
|
+
|
|
305
|
+
```json
|
|
306
|
+
{
|
|
307
|
+
"name": "Cloud workspace",
|
|
308
|
+
"description": "Team assistant",
|
|
309
|
+
"agent_ids": [123, 456],
|
|
310
|
+
"appearance": {
|
|
311
|
+
"pageTitle": "Workspace",
|
|
312
|
+
"title": "Assistant",
|
|
313
|
+
"embedButtonBgColor": "#18181B",
|
|
314
|
+
"embedButtonIconColor": "#FFFFFF",
|
|
315
|
+
"userMessageStyle": {
|
|
316
|
+
"backgroundColor": "#1A1A1A",
|
|
317
|
+
"borderColor": "#CCCCCC",
|
|
318
|
+
"textColor": "#FFFFFF",
|
|
319
|
+
"borderRadius": {
|
|
320
|
+
"topLeft": 14,
|
|
321
|
+
"topRight": 14,
|
|
322
|
+
"bottomLeft": 14,
|
|
323
|
+
"bottomRight": 4
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
"require_auth": false,
|
|
328
|
+
"file_upload": {
|
|
329
|
+
"enabled": true,
|
|
330
|
+
"allowed_extensions": [".pdf", ".txt"]
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
```bash
|
|
336
|
+
deepflow_openagent_cli cloudpc channels update 7 --json-file ./channel.json
|
|
337
|
+
deepflow_openagent_cli cloudpc channels get 7
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
`--json` also accepts an inline JSON object. `name` is required; omitted
|
|
341
|
+
`description`, `agent_ids` and `appearance` reset to null, an empty list and
|
|
342
|
+
empty appearance respectively. Omitted/null `require_auth` and `file_upload`
|
|
343
|
+
preserve the current values. Enabling uploads requires at least one extension.
|
|
344
|
+
Only Cloud PCs belonging to the current tenant can be bound (up to 20).
|
|
345
|
+
|
|
346
|
+
User message styles are configured under `appearance.userMessageStyle` in the
|
|
347
|
+
same update payload. They apply to both main and follow-up messages, including
|
|
348
|
+
history and SDK embeds, after opening or refreshing the channel. They do not
|
|
349
|
+
change Agent replies or the admin workbench.
|
|
350
|
+
|
|
351
|
+
| Field | Value | Reset behavior |
|
|
352
|
+
| --- | --- | --- |
|
|
353
|
+
| `backgroundColor` | `#RRGGBB` | Empty string, null or omission restores each bubble's default |
|
|
354
|
+
| `borderColor` | `#RRGGBB`, rendered as a 1px solid border | Empty string, null or omission removes the border |
|
|
355
|
+
| `textColor` | `#RRGGBB`, also applied to the follow-up label | Empty string, null or omission restores the default |
|
|
356
|
+
| `borderRadius` | Object containing all four integer corners, 0–100 px | Null or omission restores each bubble's default |
|
|
357
|
+
|
|
358
|
+
The server trims and uppercases colors. Radius keys are `topLeft`, `topRight`,
|
|
359
|
+
`bottomLeft`, and `bottomRight`; partial groups, negative/fractional/out-of-range
|
|
360
|
+
values, strings and booleans are rejected with HTTP 422 (CLI exits nonzero).
|
|
361
|
+
To clear only one override, set that field to null while retaining the other
|
|
362
|
+
settings. To reset all user message styles, set `"userMessageStyle": null` in
|
|
363
|
+
the complete `channel.json`, then run the same update command. The outer PUT
|
|
364
|
+
still replaces `appearance` and bindings: keep all unrelated settings from the
|
|
365
|
+
GET response in the payload. Reading the channel returns the saved style under
|
|
366
|
+
`appearance.userMessageStyle`.
|
|
367
|
+
|
|
368
|
+
`deepflow_openagent_cli cloudpc channels delete 7` deletes the channel and
|
|
369
|
+
destroys its instances. The server entry point
|
|
370
|
+
`python -m app.openapi_cli cloudpc channels ...` supports the same commands.
|
|
371
|
+
This feature requires updating the backend and CLI; no database migration or
|
|
372
|
+
Cloud PC sandbox image update is needed.
|
|
@@ -137,6 +137,30 @@ selected version does not exist; use `--from base` for a fresh environment.
|
|
|
137
137
|
Version ID is different from the `x.y.z` version label. First save automatically
|
|
138
138
|
publishes; later saves require a separate `versions publish` command.
|
|
139
139
|
|
|
140
|
+
Binary uploads (including PDF) use the same sandbox upload writer as the workbench:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
deepflow_openagent_cli cloudpc files upload 123 --file ./report.pdf
|
|
144
|
+
# After retry exhaustion, reuse BOTH printed IDs and the same local file:
|
|
145
|
+
deepflow_openagent_cli cloudpc files upload 123 --file ./report.pdf \
|
|
146
|
+
--upload-id UPLOAD_ID --sandbox-id SANDBOX_ID
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Uploads accept 1 byte–50 MiB and create `/workspace/files/uploads` automatically.
|
|
150
|
+
The returned path is `/workspace/files/uploads/<upload_id>-<original_filename>`;
|
|
151
|
+
use that path in subsequent OCR or chat commands. The ID prefix prevents duplicate
|
|
152
|
+
files after a lost response. Reusing the same ID/name with different bytes is
|
|
153
|
+
rejected. The sandbox ID pins retries to the original development environment.
|
|
154
|
+
This admin/config API accepts binary content; channel visitor format restrictions
|
|
155
|
+
still apply only to visitor uploads. Uploading does not automatically start OCR.
|
|
156
|
+
|
|
157
|
+
Uploads and `dev exec` submission/status/stop retry connection failures and HTTP
|
|
158
|
+
408/429/502/503/504 up to four times (1/2/4/8 second delays). Execution retries keep
|
|
159
|
+
the same execution ID and output cursor. Upload retries resend the whole binary
|
|
160
|
+
body; chunk resume is not implemented. Other mutating commands are not retried.
|
|
161
|
+
Requires updated CLI, API server and Cloud PC control plane; no VM image rebuild
|
|
162
|
+
is needed for the upload writer.
|
|
163
|
+
|
|
140
164
|
Additional operations:
|
|
141
165
|
|
|
142
166
|
```bash
|
|
@@ -155,8 +179,9 @@ Config update replaces configuration: provide `model_name`, `thinking_enabled`
|
|
|
155
179
|
and the complete `env_vars` list; omitted thinking/env fields reset to defaults.
|
|
156
180
|
Config queries include environment variable values. File mutation and snapshot
|
|
157
181
|
operations share the per-agent management lock; direct writes by the sandbox's
|
|
158
|
-
Agent or terminal are outside that lock.
|
|
159
|
-
timeout and
|
|
182
|
+
Agent or terminal are outside that lock. Upload and execution requests have a
|
|
183
|
+
120-second per-attempt timeout and the retries described above. Other Cloud PC
|
|
184
|
+
HTTP requests have a 900-second timeout and are not automatically retried. After a timeout, inspect sandbox or
|
|
160
185
|
version state before retrying a mutation. Conversation logs and chat-control
|
|
161
186
|
routes still require an administrator session.
|
|
162
187
|
|
|
@@ -234,3 +259,93 @@ After installation or Agent edits, use `cloudpc versions save` and optionally
|
|
|
234
259
|
`versions publish` to preserve and publish the environment. Deploy the updated
|
|
235
260
|
OpenAgent backend, cloudpc VM service, and bridge bundle together; update the CLI
|
|
236
261
|
package as well. No database migration is required.
|
|
262
|
+
|
|
263
|
+
### Cloud PC channels
|
|
264
|
+
|
|
265
|
+
Cloud PC channel settings use `cloudpc channels`, separately from Agent settings
|
|
266
|
+
(`cloudpc config`) and Web SDK channels (`channels`). These commands require an
|
|
267
|
+
API key with `config` scope and Cloud PC enabled for the tenant. The backend must
|
|
268
|
+
also include support for API key authentication on `/api/v1/cloudpc/channels`;
|
|
269
|
+
older backends accept administrator sessions only.
|
|
270
|
+
|
|
271
|
+
```bash
|
|
272
|
+
deepflow_openagent_cli cloudpc channels list --page 1 --per-page 20
|
|
273
|
+
deepflow_openagent_cli cloudpc channels create --name "Cloud workspace"
|
|
274
|
+
deepflow_openagent_cli cloudpc channels agent-options
|
|
275
|
+
deepflow_openagent_cli cloudpc channels get 7
|
|
276
|
+
deepflow_openagent_cli docs show cloudpc.channels.update --json
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Update uses the existing PUT replacement contract. Read the current channel
|
|
280
|
+
first and prepare `channel.json` with the complete name, description, ordered
|
|
281
|
+
`agent_ids` and `appearance`. Convert the returned `bindings` to their `id`
|
|
282
|
+
values; the GET response is not itself an update payload.
|
|
283
|
+
|
|
284
|
+
```json
|
|
285
|
+
{
|
|
286
|
+
"name": "Cloud workspace",
|
|
287
|
+
"description": "Team assistant",
|
|
288
|
+
"agent_ids": [123, 456],
|
|
289
|
+
"appearance": {
|
|
290
|
+
"pageTitle": "Workspace",
|
|
291
|
+
"title": "Assistant",
|
|
292
|
+
"embedButtonBgColor": "#18181B",
|
|
293
|
+
"embedButtonIconColor": "#FFFFFF",
|
|
294
|
+
"userMessageStyle": {
|
|
295
|
+
"backgroundColor": "#1A1A1A",
|
|
296
|
+
"borderColor": "#CCCCCC",
|
|
297
|
+
"textColor": "#FFFFFF",
|
|
298
|
+
"borderRadius": {
|
|
299
|
+
"topLeft": 14,
|
|
300
|
+
"topRight": 14,
|
|
301
|
+
"bottomLeft": 14,
|
|
302
|
+
"bottomRight": 4
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
"require_auth": false,
|
|
307
|
+
"file_upload": {
|
|
308
|
+
"enabled": true,
|
|
309
|
+
"allowed_extensions": [".pdf", ".txt"]
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
```bash
|
|
315
|
+
deepflow_openagent_cli cloudpc channels update 7 --json-file ./channel.json
|
|
316
|
+
deepflow_openagent_cli cloudpc channels get 7
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
`--json` also accepts an inline JSON object. `name` is required; omitted
|
|
320
|
+
`description`, `agent_ids` and `appearance` reset to null, an empty list and
|
|
321
|
+
empty appearance respectively. Omitted/null `require_auth` and `file_upload`
|
|
322
|
+
preserve the current values. Enabling uploads requires at least one extension.
|
|
323
|
+
Only Cloud PCs belonging to the current tenant can be bound (up to 20).
|
|
324
|
+
|
|
325
|
+
User message styles are configured under `appearance.userMessageStyle` in the
|
|
326
|
+
same update payload. They apply to both main and follow-up messages, including
|
|
327
|
+
history and SDK embeds, after opening or refreshing the channel. They do not
|
|
328
|
+
change Agent replies or the admin workbench.
|
|
329
|
+
|
|
330
|
+
| Field | Value | Reset behavior |
|
|
331
|
+
| --- | --- | --- |
|
|
332
|
+
| `backgroundColor` | `#RRGGBB` | Empty string, null or omission restores each bubble's default |
|
|
333
|
+
| `borderColor` | `#RRGGBB`, rendered as a 1px solid border | Empty string, null or omission removes the border |
|
|
334
|
+
| `textColor` | `#RRGGBB`, also applied to the follow-up label | Empty string, null or omission restores the default |
|
|
335
|
+
| `borderRadius` | Object containing all four integer corners, 0–100 px | Null or omission restores each bubble's default |
|
|
336
|
+
|
|
337
|
+
The server trims and uppercases colors. Radius keys are `topLeft`, `topRight`,
|
|
338
|
+
`bottomLeft`, and `bottomRight`; partial groups, negative/fractional/out-of-range
|
|
339
|
+
values, strings and booleans are rejected with HTTP 422 (CLI exits nonzero).
|
|
340
|
+
To clear only one override, set that field to null while retaining the other
|
|
341
|
+
settings. To reset all user message styles, set `"userMessageStyle": null` in
|
|
342
|
+
the complete `channel.json`, then run the same update command. The outer PUT
|
|
343
|
+
still replaces `appearance` and bindings: keep all unrelated settings from the
|
|
344
|
+
GET response in the payload. Reading the channel returns the saved style under
|
|
345
|
+
`appearance.userMessageStyle`.
|
|
346
|
+
|
|
347
|
+
`deepflow_openagent_cli cloudpc channels delete 7` deletes the channel and
|
|
348
|
+
destroys its instances. The server entry point
|
|
349
|
+
`python -m app.openapi_cli cloudpc channels ...` supports the same commands.
|
|
350
|
+
This feature requires updating the backend and CLI; no database migration or
|
|
351
|
+
Cloud PC sandbox image update is needed.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "deepflow-openagent-cli"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.8"
|
|
8
8
|
description = "Command-line client for OpenAgent OpenAPI (agents, conversations, SSE chat, tools, channels, knowledge bases)."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
{deepflow_openagent_cli-0.1.6 → deepflow_openagent_cli-0.1.8}/src/deepflow_openagent_cli/cli.py
RENAMED
|
@@ -8,6 +8,7 @@ Run with:
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
10
|
import argparse
|
|
11
|
+
from http.client import HTTPException
|
|
11
12
|
import json
|
|
12
13
|
import mimetypes
|
|
13
14
|
import os
|
|
@@ -129,6 +130,73 @@ def _param(name: str, type_: str, required: bool, description: str) -> ParamDoc:
|
|
|
129
130
|
|
|
130
131
|
|
|
131
132
|
ENDPOINTS: tuple[EndpointDoc, ...] = (
|
|
133
|
+
EndpointDoc(
|
|
134
|
+
key="cloudpc.channels.list", method="GET", path="/api/v1/cloudpc/channels", scope="config",
|
|
135
|
+
summary="List Cloud PC channels for the current tenant.",
|
|
136
|
+
query_params=[_param("page", "integer", False, "Page number (default 1)."), _param("per_page", "integer", False, "Page size, 1-100 (default 20).")],
|
|
137
|
+
response="CloudPcChannelListResponse.",
|
|
138
|
+
example="deepflow_openagent_cli cloudpc channels list --page 1 --per-page 20",
|
|
139
|
+
),
|
|
140
|
+
EndpointDoc(
|
|
141
|
+
key="cloudpc.channels.create", method="POST", path="/api/v1/cloudpc/channels", scope="config",
|
|
142
|
+
summary="Create a Cloud PC channel; use update to configure bindings and appearance.",
|
|
143
|
+
body_fields=[_param("name", "string", True, "Channel name, 1-64 characters."), _param("description", "string", False, "Description, max 500 characters.")],
|
|
144
|
+
response="CloudPcChannelDetail, HTTP 201.",
|
|
145
|
+
example='deepflow_openagent_cli cloudpc channels create --name "Cloud workspace"',
|
|
146
|
+
),
|
|
147
|
+
EndpointDoc(
|
|
148
|
+
key="cloudpc.channels.agent_options", method="GET", path="/api/v1/cloudpc/channels/agent-options", scope="config",
|
|
149
|
+
summary="List enabled Cloud PCs available for binding, including unpublished ones.",
|
|
150
|
+
response="CloudPcAgentOptionList: items with id, name, description, version and status.",
|
|
151
|
+
example="deepflow_openagent_cli cloudpc channels agent-options",
|
|
152
|
+
),
|
|
153
|
+
EndpointDoc(
|
|
154
|
+
key="cloudpc.channels.get", method="GET", path="/api/v1/cloudpc/channels/{channel_id}", scope="config",
|
|
155
|
+
summary="Get Cloud PC channel settings and ordered bindings.",
|
|
156
|
+
path_params=[_param("channel_id", "integer", True, "Cloud PC channel ID.")],
|
|
157
|
+
response="CloudPcChannelDetail: id, name, description, token, appearance, bindings, require_auth, has_secret_key, file_upload and timestamps.",
|
|
158
|
+
example="deepflow_openagent_cli cloudpc channels get 1",
|
|
159
|
+
),
|
|
160
|
+
EndpointDoc(
|
|
161
|
+
key="cloudpc.channels.update", method="PUT", path="/api/v1/cloudpc/channels/{channel_id}", scope="config",
|
|
162
|
+
summary="Replace channel name, description, bindings and appearance; omitted require_auth/file_upload are preserved.",
|
|
163
|
+
path_params=[_param("channel_id", "integer", True, "Cloud PC channel ID.")],
|
|
164
|
+
body_fields=[
|
|
165
|
+
_param("name", "string", True, "Channel name, 1-64 characters."),
|
|
166
|
+
_param("description", "string", False, "Description, max 500 characters; omitted resets to null."),
|
|
167
|
+
_param("agent_ids", "array", False, "Complete ordered list of Cloud PC Agent IDs, max 20 unique IDs; omitted clears bindings. Convert GET bindings to their id values."),
|
|
168
|
+
_param("appearance", "object", False, "Complete appearance: favicon, pageTitle, logo, title, embedButtonBgColor, embedButtonIconColor, userMessageStyle; omitted clears appearance. Titles max 32 characters; colors #RRGGBB."),
|
|
169
|
+
_param("appearance.userMessageStyle", "object|null", False, "Overrides main and follow-up user bubbles in this channel, including history and embeds. Omitted/null restores defaults; other appearance fields must still be supplied."),
|
|
170
|
+
_param("appearance.userMessageStyle.backgroundColor", "string|null", False, "Background #RRGGBB. Server trims and uppercases; empty string/null restores each bubble's default."),
|
|
171
|
+
_param("appearance.userMessageStyle.borderColor", "string|null", False, "Border #RRGGBB; 1px solid when set. Empty string/null removes the border."),
|
|
172
|
+
_param("appearance.userMessageStyle.textColor", "string|null", False, "Text #RRGGBB, including the follow-up label. Empty string/null restores the default."),
|
|
173
|
+
_param("appearance.userMessageStyle.borderRadius", "object|null", False, "All four corners are required when set: topLeft, topRight, bottomLeft, bottomRight, each an integer 0-100 px. Null restores each bubble's default; partial groups, strings, floats and booleans are rejected."),
|
|
174
|
+
_param("require_auth", "boolean", False, "Require signed visitor authentication; omitted/null preserves current value."),
|
|
175
|
+
_param("file_upload", "object", False, "enabled and allowed_extensions; enabled uploads require at least one extension. Omitted/null preserves current value."),
|
|
176
|
+
],
|
|
177
|
+
response="CloudPcChannelDetail.",
|
|
178
|
+
errors=["400 invalid bindings or duplicate name", "401 invalid API key", "403 config scope missing, non-admin session or Cloud PC disabled", "404 channel not found in current tenant", "422 validation error"],
|
|
179
|
+
example="deepflow_openagent_cli cloudpc channels update 1 --json-file channel.json",
|
|
180
|
+
),
|
|
181
|
+
EndpointDoc(
|
|
182
|
+
key="cloudpc.channels.delete", method="DELETE", path="/api/v1/cloudpc/channels/{channel_id}", scope="config",
|
|
183
|
+
summary="Delete a Cloud PC channel and destroy its instances.",
|
|
184
|
+
path_params=[_param("channel_id", "integer", True, "Cloud PC channel ID.")],
|
|
185
|
+
response="Deletion message.",
|
|
186
|
+
example="deepflow_openagent_cli cloudpc channels delete 1",
|
|
187
|
+
),
|
|
188
|
+
EndpointDoc(
|
|
189
|
+
key="cloudpc.files.upload", method="POST",
|
|
190
|
+
path="/api/v1/cloudpc/agents/{agent_id}/dev-sandbox/files/upload", scope="config",
|
|
191
|
+
summary="Upload binary content (1 byte–50 MiB) into /workspace/files/uploads; safe to retry with the same upload ID and sandbox ID.",
|
|
192
|
+
path_params=[_param("agent_id", "integer", True, "Cloud PC Agent ID.")],
|
|
193
|
+
query_params=[_param("filename", "string", True, "Original basename."),
|
|
194
|
+
_param("upload_id", "string", True, "32 lowercase hex characters; prefixes the saved filename."),
|
|
195
|
+
_param("sandbox_id", "integer", True, "Pinned development sandbox ID from dev get.")],
|
|
196
|
+
response="Saved filename, absolute path and size.",
|
|
197
|
+
example="deepflow_openagent_cli cloudpc files upload 1 --file ./report.pdf",
|
|
198
|
+
),
|
|
199
|
+
|
|
132
200
|
EndpointDoc(
|
|
133
201
|
key="cloudpc.dev.exec", method="POST", path="/api/v1/cloudpc/agents/{agent_id}/dev-sandbox/executions", scope="config",
|
|
134
202
|
summary='Start an idempotent shell execution. Output remains in the sandbox runtime until restart or eviction (32 retained).',
|
|
@@ -1254,6 +1322,38 @@ class OpenApiClient:
|
|
|
1254
1322
|
except (URLError, OSError, UnicodeError, ValueError) as exc:
|
|
1255
1323
|
raise CliError(f"Chat stream interrupted: {exc}") from exc
|
|
1256
1324
|
|
|
1325
|
+
def _read_retryable(self, request: Request) -> bytes:
|
|
1326
|
+
"""Only for operations whose server contract makes replay safe."""
|
|
1327
|
+
for attempt in range(5):
|
|
1328
|
+
try:
|
|
1329
|
+
with urlopen(request, timeout=120) as response:
|
|
1330
|
+
return response.read()
|
|
1331
|
+
except HTTPError as exc:
|
|
1332
|
+
if exc.code not in {408, 429, 502, 503, 504} or attempt == 4:
|
|
1333
|
+
raise CliError(f"HTTP {exc.code}: {exc.read().decode('utf-8', errors='replace')}") from exc
|
|
1334
|
+
except (URLError, OSError, HTTPException) as exc:
|
|
1335
|
+
if attempt == 4:
|
|
1336
|
+
raise CliError(f"Connection failed after 5 attempts: {exc}") from exc
|
|
1337
|
+
print(f"Connection interrupted; retry {attempt + 1}/4…", file=sys.stderr)
|
|
1338
|
+
time.sleep(2 ** attempt)
|
|
1339
|
+
|
|
1340
|
+
def upload(self, agent_id: int, file: Path, upload_id: str, sandbox_id: int) -> bytes:
|
|
1341
|
+
if not self.api_key:
|
|
1342
|
+
raise CliError(f"Missing API key. Set {ENV_API_KEY} or pass --api-key.")
|
|
1343
|
+
try:
|
|
1344
|
+
with file.open("rb") as source:
|
|
1345
|
+
content = source.read(50 * 1024 * 1024 + 1)
|
|
1346
|
+
except OSError as exc:
|
|
1347
|
+
raise CliError(f"Cannot read upload: {exc}") from exc
|
|
1348
|
+
if not 0 < len(content) <= 50 * 1024 * 1024:
|
|
1349
|
+
raise CliError("Upload must be between 1 byte and 50 MiB.")
|
|
1350
|
+
url = self._build_url(get_endpoint("cloudpc.files.upload"), {"agent_id": agent_id},
|
|
1351
|
+
{"filename": file.name, "upload_id": upload_id, "sandbox_id": sandbox_id})
|
|
1352
|
+
request = Request(url, data=content, method="POST", headers={
|
|
1353
|
+
"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/octet-stream",
|
|
1354
|
+
})
|
|
1355
|
+
return self._read_retryable(request)
|
|
1356
|
+
|
|
1257
1357
|
def request(
|
|
1258
1358
|
self,
|
|
1259
1359
|
endpoint_key: str,
|
|
@@ -1262,6 +1362,7 @@ class OpenApiClient:
|
|
|
1262
1362
|
body: dict[str, Any] | None = None,
|
|
1263
1363
|
*,
|
|
1264
1364
|
stream: bool = False,
|
|
1365
|
+
retry: bool = False,
|
|
1265
1366
|
) -> bytes | None:
|
|
1266
1367
|
endpoint = get_endpoint(endpoint_key)
|
|
1267
1368
|
if endpoint.scope and not self.api_key:
|
|
@@ -1277,6 +1378,8 @@ class OpenApiClient:
|
|
|
1277
1378
|
headers["Content-Type"] = "application/json"
|
|
1278
1379
|
|
|
1279
1380
|
request = Request(url, data=data, headers=headers, method=endpoint.method)
|
|
1381
|
+
if not stream and (retry or endpoint_key in {"cloudpc.dev.exec", "cloudpc.dev.exec_status", "cloudpc.dev.exec_cancel"}):
|
|
1382
|
+
return self._read_retryable(request)
|
|
1280
1383
|
try:
|
|
1281
1384
|
options = {"timeout": 900} if endpoint_key.startswith("cloudpc.") else {}
|
|
1282
1385
|
with urlopen(request, **options) as response: # noqa: S310 - CLI calls user-provided API host.
|
|
@@ -1877,9 +1980,32 @@ def handle_cloudpc_chat(args: argparse.Namespace) -> int:
|
|
|
1877
1980
|
raise
|
|
1878
1981
|
|
|
1879
1982
|
|
|
1983
|
+
def handle_cloudpc_channels(args: argparse.Namespace) -> int:
|
|
1984
|
+
"""Manage Cloud PC channels through their dedicated API."""
|
|
1985
|
+
action = args.cloudpc_action
|
|
1986
|
+
endpoint = f"cloudpc.channels.{action.replace('-', '_')}"
|
|
1987
|
+
if action == "list":
|
|
1988
|
+
return run_request(args, endpoint, query={"page": args.page, "per_page": args.per_page})
|
|
1989
|
+
if action == "agent-options":
|
|
1990
|
+
return run_request(args, endpoint)
|
|
1991
|
+
if action == "create":
|
|
1992
|
+
return run_request(args, endpoint, body={"name": args.name, "description": args.description})
|
|
1993
|
+
body = None
|
|
1994
|
+
if action == "update":
|
|
1995
|
+
try:
|
|
1996
|
+
body = merge_payload(args)
|
|
1997
|
+
except (OSError, UnicodeError) as exc:
|
|
1998
|
+
raise CliError(f"Cannot read channel configuration file: {exc}") from exc
|
|
1999
|
+
if not body.get("name"):
|
|
2000
|
+
raise CliError("Channel update requires name in --json or --json-file.")
|
|
2001
|
+
return run_request(args, endpoint, {"channel_id": args.channel_id}, body=body)
|
|
2002
|
+
|
|
2003
|
+
|
|
1880
2004
|
def handle_cloudpc(args: argparse.Namespace) -> int:
|
|
1881
2005
|
"""Manage Cloud PC configuration, development files and saved versions."""
|
|
1882
2006
|
group = args.cloudpc_group
|
|
2007
|
+
if group == "channels":
|
|
2008
|
+
return handle_cloudpc_channels(args)
|
|
1883
2009
|
if group == "capability":
|
|
1884
2010
|
return run_request(args, "cloudpc.capability")
|
|
1885
2011
|
if group == "create":
|
|
@@ -1919,6 +2045,21 @@ def handle_cloudpc(args: argparse.Namespace) -> int:
|
|
|
1919
2045
|
raise CliError("Version ID must be positive.")
|
|
1920
2046
|
body = {"from_version_id": version_id}
|
|
1921
2047
|
elif group == "files":
|
|
2048
|
+
if action == "upload":
|
|
2049
|
+
if bool(args.upload_id) != (args.sandbox_id is not None):
|
|
2050
|
+
raise CliError("Use --upload-id and --sandbox-id together when resuming an upload.")
|
|
2051
|
+
client = client_from_args(args)
|
|
2052
|
+
upload_id = args.upload_id or uuid4().hex
|
|
2053
|
+
sandbox_id = args.sandbox_id
|
|
2054
|
+
if sandbox_id is None:
|
|
2055
|
+
dev = json.loads(client.request("cloudpc.dev.get", path_params, retry=True))
|
|
2056
|
+
sandbox_id = dev.get("sandbox_id")
|
|
2057
|
+
if sandbox_id is None:
|
|
2058
|
+
raise CliError("No upload-capable development sandbox; create/resume it and update the server.")
|
|
2059
|
+
print(f"upload_id={upload_id} sandbox_id={sandbox_id}; reuse both IDs to retry this upload.", file=sys.stderr)
|
|
2060
|
+
data = client.upload(args.agent_id, Path(args.file).expanduser(), upload_id, sandbox_id)
|
|
2061
|
+
print_output(data, pretty=args.pretty, raw=args.raw)
|
|
2062
|
+
return 0
|
|
1922
2063
|
if action in {"read", "delete"}:
|
|
1923
2064
|
query = {"path": args.path}
|
|
1924
2065
|
elif action == "write":
|
|
@@ -2076,6 +2217,20 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
2076
2217
|
|
|
2077
2218
|
cloudpc = subparsers.add_parser("cloudpc", help="Cloud PC management (requires config scope).")
|
|
2078
2219
|
cloudpc_sub = cloudpc.add_subparsers(dest="cloudpc_group", required=True)
|
|
2220
|
+
cloudpc_channels = cloudpc_sub.add_parser("channels", help="Cloud PC channel configuration.").add_subparsers(dest="cloudpc_action", required=True)
|
|
2221
|
+
channel_list = cloudpc_channels.add_parser("list")
|
|
2222
|
+
channel_list.add_argument("--page", type=int)
|
|
2223
|
+
channel_list.add_argument("--per-page", type=int)
|
|
2224
|
+
cloudpc_channels.add_parser("agent-options")
|
|
2225
|
+
channel_create = cloudpc_channels.add_parser("create")
|
|
2226
|
+
channel_create.add_argument("--name", required=True)
|
|
2227
|
+
channel_create.add_argument("--description")
|
|
2228
|
+
for action in ("get", "delete"):
|
|
2229
|
+
cloudpc_channels.add_parser(action).add_argument("channel_id", type=int)
|
|
2230
|
+
channel_update_help = "Replace name/description/bindings/appearance; omitted bindings/appearance are cleared. Omitted require_auth/file_upload are preserved. Set appearance.userMessageStyle in --json or --json-file for user bubble colors and four-corner radius; null restores defaults. See docs show cloudpc.channels.update."
|
|
2231
|
+
channel_update = cloudpc_channels.add_parser("update", help=channel_update_help, description=channel_update_help)
|
|
2232
|
+
channel_update.add_argument("channel_id", type=int)
|
|
2233
|
+
add_json_args(channel_update)
|
|
2079
2234
|
cloudpc_sub.add_parser("capability")
|
|
2080
2235
|
cloudpc_create = cloudpc_sub.add_parser("create")
|
|
2081
2236
|
cloudpc_create.add_argument("--name", required=True)
|
|
@@ -2129,6 +2284,11 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
2129
2284
|
elif action == "rename":
|
|
2130
2285
|
command.add_argument("--src", required=True)
|
|
2131
2286
|
command.add_argument("--dst", required=True)
|
|
2287
|
+
upload = cloudpc_files.add_parser("upload", help="Upload binary files to /workspace/files/uploads (max 50 MiB).")
|
|
2288
|
+
upload.add_argument("agent_id", type=int)
|
|
2289
|
+
upload.add_argument("--file", required=True)
|
|
2290
|
+
upload.add_argument("--upload-id", help="Reuse the printed ID when retrying an interrupted upload.")
|
|
2291
|
+
upload.add_argument("--sandbox-id", type=int, help="Reuse the printed sandbox ID on manual retry.")
|
|
2132
2292
|
cloudpc_versions = cloudpc_sub.add_parser("versions").add_subparsers(dest="cloudpc_action", required=True)
|
|
2133
2293
|
for action in ("list", "save", "publish"):
|
|
2134
2294
|
command = cloudpc_versions.add_parser(action)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: deepflow-openagent-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.8
|
|
4
4
|
Summary: Command-line client for OpenAgent OpenAPI (agents, conversations, SSE chat, tools, channels, knowledge bases).
|
|
5
5
|
Author: DeepFlow AI
|
|
6
6
|
Project-URL: Repository, https://github.com/DeepFlowAI/OpenAgent
|
|
@@ -158,6 +158,30 @@ selected version does not exist; use `--from base` for a fresh environment.
|
|
|
158
158
|
Version ID is different from the `x.y.z` version label. First save automatically
|
|
159
159
|
publishes; later saves require a separate `versions publish` command.
|
|
160
160
|
|
|
161
|
+
Binary uploads (including PDF) use the same sandbox upload writer as the workbench:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
deepflow_openagent_cli cloudpc files upload 123 --file ./report.pdf
|
|
165
|
+
# After retry exhaustion, reuse BOTH printed IDs and the same local file:
|
|
166
|
+
deepflow_openagent_cli cloudpc files upload 123 --file ./report.pdf \
|
|
167
|
+
--upload-id UPLOAD_ID --sandbox-id SANDBOX_ID
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Uploads accept 1 byte–50 MiB and create `/workspace/files/uploads` automatically.
|
|
171
|
+
The returned path is `/workspace/files/uploads/<upload_id>-<original_filename>`;
|
|
172
|
+
use that path in subsequent OCR or chat commands. The ID prefix prevents duplicate
|
|
173
|
+
files after a lost response. Reusing the same ID/name with different bytes is
|
|
174
|
+
rejected. The sandbox ID pins retries to the original development environment.
|
|
175
|
+
This admin/config API accepts binary content; channel visitor format restrictions
|
|
176
|
+
still apply only to visitor uploads. Uploading does not automatically start OCR.
|
|
177
|
+
|
|
178
|
+
Uploads and `dev exec` submission/status/stop retry connection failures and HTTP
|
|
179
|
+
408/429/502/503/504 up to four times (1/2/4/8 second delays). Execution retries keep
|
|
180
|
+
the same execution ID and output cursor. Upload retries resend the whole binary
|
|
181
|
+
body; chunk resume is not implemented. Other mutating commands are not retried.
|
|
182
|
+
Requires updated CLI, API server and Cloud PC control plane; no VM image rebuild
|
|
183
|
+
is needed for the upload writer.
|
|
184
|
+
|
|
161
185
|
Additional operations:
|
|
162
186
|
|
|
163
187
|
```bash
|
|
@@ -176,8 +200,9 @@ Config update replaces configuration: provide `model_name`, `thinking_enabled`
|
|
|
176
200
|
and the complete `env_vars` list; omitted thinking/env fields reset to defaults.
|
|
177
201
|
Config queries include environment variable values. File mutation and snapshot
|
|
178
202
|
operations share the per-agent management lock; direct writes by the sandbox's
|
|
179
|
-
Agent or terminal are outside that lock.
|
|
180
|
-
timeout and
|
|
203
|
+
Agent or terminal are outside that lock. Upload and execution requests have a
|
|
204
|
+
120-second per-attempt timeout and the retries described above. Other Cloud PC
|
|
205
|
+
HTTP requests have a 900-second timeout and are not automatically retried. After a timeout, inspect sandbox or
|
|
181
206
|
version state before retrying a mutation. Conversation logs and chat-control
|
|
182
207
|
routes still require an administrator session.
|
|
183
208
|
|
|
@@ -255,3 +280,93 @@ After installation or Agent edits, use `cloudpc versions save` and optionally
|
|
|
255
280
|
`versions publish` to preserve and publish the environment. Deploy the updated
|
|
256
281
|
OpenAgent backend, cloudpc VM service, and bridge bundle together; update the CLI
|
|
257
282
|
package as well. No database migration is required.
|
|
283
|
+
|
|
284
|
+
### Cloud PC channels
|
|
285
|
+
|
|
286
|
+
Cloud PC channel settings use `cloudpc channels`, separately from Agent settings
|
|
287
|
+
(`cloudpc config`) and Web SDK channels (`channels`). These commands require an
|
|
288
|
+
API key with `config` scope and Cloud PC enabled for the tenant. The backend must
|
|
289
|
+
also include support for API key authentication on `/api/v1/cloudpc/channels`;
|
|
290
|
+
older backends accept administrator sessions only.
|
|
291
|
+
|
|
292
|
+
```bash
|
|
293
|
+
deepflow_openagent_cli cloudpc channels list --page 1 --per-page 20
|
|
294
|
+
deepflow_openagent_cli cloudpc channels create --name "Cloud workspace"
|
|
295
|
+
deepflow_openagent_cli cloudpc channels agent-options
|
|
296
|
+
deepflow_openagent_cli cloudpc channels get 7
|
|
297
|
+
deepflow_openagent_cli docs show cloudpc.channels.update --json
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Update uses the existing PUT replacement contract. Read the current channel
|
|
301
|
+
first and prepare `channel.json` with the complete name, description, ordered
|
|
302
|
+
`agent_ids` and `appearance`. Convert the returned `bindings` to their `id`
|
|
303
|
+
values; the GET response is not itself an update payload.
|
|
304
|
+
|
|
305
|
+
```json
|
|
306
|
+
{
|
|
307
|
+
"name": "Cloud workspace",
|
|
308
|
+
"description": "Team assistant",
|
|
309
|
+
"agent_ids": [123, 456],
|
|
310
|
+
"appearance": {
|
|
311
|
+
"pageTitle": "Workspace",
|
|
312
|
+
"title": "Assistant",
|
|
313
|
+
"embedButtonBgColor": "#18181B",
|
|
314
|
+
"embedButtonIconColor": "#FFFFFF",
|
|
315
|
+
"userMessageStyle": {
|
|
316
|
+
"backgroundColor": "#1A1A1A",
|
|
317
|
+
"borderColor": "#CCCCCC",
|
|
318
|
+
"textColor": "#FFFFFF",
|
|
319
|
+
"borderRadius": {
|
|
320
|
+
"topLeft": 14,
|
|
321
|
+
"topRight": 14,
|
|
322
|
+
"bottomLeft": 14,
|
|
323
|
+
"bottomRight": 4
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
"require_auth": false,
|
|
328
|
+
"file_upload": {
|
|
329
|
+
"enabled": true,
|
|
330
|
+
"allowed_extensions": [".pdf", ".txt"]
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
```bash
|
|
336
|
+
deepflow_openagent_cli cloudpc channels update 7 --json-file ./channel.json
|
|
337
|
+
deepflow_openagent_cli cloudpc channels get 7
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
`--json` also accepts an inline JSON object. `name` is required; omitted
|
|
341
|
+
`description`, `agent_ids` and `appearance` reset to null, an empty list and
|
|
342
|
+
empty appearance respectively. Omitted/null `require_auth` and `file_upload`
|
|
343
|
+
preserve the current values. Enabling uploads requires at least one extension.
|
|
344
|
+
Only Cloud PCs belonging to the current tenant can be bound (up to 20).
|
|
345
|
+
|
|
346
|
+
User message styles are configured under `appearance.userMessageStyle` in the
|
|
347
|
+
same update payload. They apply to both main and follow-up messages, including
|
|
348
|
+
history and SDK embeds, after opening or refreshing the channel. They do not
|
|
349
|
+
change Agent replies or the admin workbench.
|
|
350
|
+
|
|
351
|
+
| Field | Value | Reset behavior |
|
|
352
|
+
| --- | --- | --- |
|
|
353
|
+
| `backgroundColor` | `#RRGGBB` | Empty string, null or omission restores each bubble's default |
|
|
354
|
+
| `borderColor` | `#RRGGBB`, rendered as a 1px solid border | Empty string, null or omission removes the border |
|
|
355
|
+
| `textColor` | `#RRGGBB`, also applied to the follow-up label | Empty string, null or omission restores the default |
|
|
356
|
+
| `borderRadius` | Object containing all four integer corners, 0–100 px | Null or omission restores each bubble's default |
|
|
357
|
+
|
|
358
|
+
The server trims and uppercases colors. Radius keys are `topLeft`, `topRight`,
|
|
359
|
+
`bottomLeft`, and `bottomRight`; partial groups, negative/fractional/out-of-range
|
|
360
|
+
values, strings and booleans are rejected with HTTP 422 (CLI exits nonzero).
|
|
361
|
+
To clear only one override, set that field to null while retaining the other
|
|
362
|
+
settings. To reset all user message styles, set `"userMessageStyle": null` in
|
|
363
|
+
the complete `channel.json`, then run the same update command. The outer PUT
|
|
364
|
+
still replaces `appearance` and bindings: keep all unrelated settings from the
|
|
365
|
+
GET response in the payload. Reading the channel returns the saved style under
|
|
366
|
+
`appearance.userMessageStyle`.
|
|
367
|
+
|
|
368
|
+
`deepflow_openagent_cli cloudpc channels delete 7` deletes the channel and
|
|
369
|
+
destroys its instances. The server entry point
|
|
370
|
+
`python -m app.openapi_cli cloudpc channels ...` supports the same commands.
|
|
371
|
+
This feature requires updating the backend and CLI; no database migration or
|
|
372
|
+
Cloud PC sandbox image update is needed.
|
|
@@ -44,6 +44,12 @@ def run(cli, *args):
|
|
|
44
44
|
|
|
45
45
|
|
|
46
46
|
@pytest.mark.parametrize(("arguments", "method", "suffix", "body"), [
|
|
47
|
+
(["channels", "list", "--page", "2", "--per-page", "5"], "GET", "/channels?page=2&per_page=5", None),
|
|
48
|
+
(["channels", "create", "--name", "渠道", "--description", "Demo"], "POST", "/channels", {"name": "渠道", "description": "Demo"}),
|
|
49
|
+
(["channels", "agent-options"], "GET", "/channels/agent-options", None),
|
|
50
|
+
(["channels", "get", "7"], "GET", "/channels/7", None),
|
|
51
|
+
(["channels", "update", "7", "--json", '{"name":"渠道","agent_ids":[3,2],"appearance":{"title":"助手"},"require_auth":false,"file_upload":{"enabled":true,"allowed_extensions":[".pdf"]}}'], "PUT", "/channels/7", {"name": "渠道", "agent_ids": [3, 2], "appearance": {"title": "助手"}, "require_auth": False, "file_upload": {"enabled": True, "allowed_extensions": [".pdf"]}}),
|
|
52
|
+
(["channels", "delete", "7"], "DELETE", "/channels/7", None),
|
|
47
53
|
(["capability"], "GET", "/capability", None),
|
|
48
54
|
(["create", "--name", "Test"], "POST", "/agents", {"name": "Test", "description": None}),
|
|
49
55
|
(["config", "get", "7"], "GET", "/agents/7/config", None),
|
|
@@ -79,6 +85,100 @@ def test_agents_kind_filter(cli, transport):
|
|
|
79
85
|
assert "kind=cloudpc" in calls[0][0].full_url
|
|
80
86
|
|
|
81
87
|
|
|
88
|
+
def test_channel_update_from_file(cli, transport, tmp_path):
|
|
89
|
+
payload = {"name": "渠道", "description": None, "agent_ids": [], "appearance": {}}
|
|
90
|
+
source = tmp_path / "channel.json"
|
|
91
|
+
source.write_text(json.dumps(payload), encoding="utf-8")
|
|
92
|
+
assert run(cli, "cloudpc", "channels", "update", "7", "--json-file", str(source)) == 0
|
|
93
|
+
calls, _ = transport
|
|
94
|
+
assert len(calls) == 1
|
|
95
|
+
assert json.loads(calls[0][0].data) == payload
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@pytest.mark.parametrize("payload", ["{}", "[]", "{invalid"])
|
|
99
|
+
def test_invalid_channel_update_does_not_send_request(cli, transport, payload):
|
|
100
|
+
assert run(cli, "cloudpc", "channels", "update", "7", "--json", payload) == 1
|
|
101
|
+
assert transport[0] == []
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_missing_channel_file_has_clean_error(cli, transport, tmp_path, capsys):
|
|
105
|
+
assert run(cli, "cloudpc", "channels", "update", "7", "--json-file", str(tmp_path / "missing.json")) == 1
|
|
106
|
+
assert transport[0] == []
|
|
107
|
+
assert "Cannot read channel configuration file" in capsys.readouterr().err
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_channel_docs_describe_replacement(cli, capsys):
|
|
111
|
+
assert cli.main(["docs", "show", "cloudpc.channels.update", "--json"]) == 0
|
|
112
|
+
doc = json.loads(capsys.readouterr().out)
|
|
113
|
+
assert doc["scope"] == "config"
|
|
114
|
+
assert doc["path"] == "/api/v1/cloudpc/channels/{channel_id}"
|
|
115
|
+
fields = {field["name"]: field for field in doc["body_fields"]}
|
|
116
|
+
assert fields["name"]["required"]
|
|
117
|
+
assert "omitted clears" in fields["agent_ids"]["description"]
|
|
118
|
+
assert "preserves" in fields["file_upload"]["description"]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@pytest.mark.parametrize("source", ["--json", "--json-file"])
|
|
122
|
+
@pytest.mark.parametrize("style", [
|
|
123
|
+
{"backgroundColor": "#123456", "borderColor": "#ABCDEF", "textColor": "#FFFFFF",
|
|
124
|
+
"borderRadius": {"topLeft": 0, "topRight": 100, "bottomLeft": 12, "bottomRight": 4}},
|
|
125
|
+
{"textColor": " #aabbcc "},
|
|
126
|
+
{"backgroundColor": "#123456", "borderColor": None, "textColor": "", "borderRadius": None},
|
|
127
|
+
None,
|
|
128
|
+
])
|
|
129
|
+
def test_channel_message_style_update_payload(cli, transport, tmp_path, source, style):
|
|
130
|
+
payload = {
|
|
131
|
+
"name": "Workspace", "description": "Keep", "agent_ids": [3, 2],
|
|
132
|
+
"appearance": {"title": "Assistant", "embedButtonBgColor": "#1A1A1A", "userMessageStyle": style},
|
|
133
|
+
"require_auth": True, "file_upload": {"enabled": True, "allowed_extensions": [".pdf"]},
|
|
134
|
+
}
|
|
135
|
+
value = json.dumps(payload)
|
|
136
|
+
if source == "--json-file":
|
|
137
|
+
path = tmp_path / "channel.json"
|
|
138
|
+
path.write_text(value, encoding="utf-8")
|
|
139
|
+
value = str(path)
|
|
140
|
+
assert run(cli, "cloudpc", "channels", "update", "7", source, value) == 0
|
|
141
|
+
calls, _ = transport
|
|
142
|
+
assert len(calls) == 1
|
|
143
|
+
assert calls[0][0].method == "PUT"
|
|
144
|
+
assert json.loads(calls[0][0].data) == payload
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def test_channel_message_style_read(cli, transport, capsys):
|
|
148
|
+
detail = {"id": 7, "appearance": {"userMessageStyle": {"textColor": "#AABBCC", "borderRadius": None}}}
|
|
149
|
+
transport[1]["http://test/api/v1/cloudpc/channels/7"] = json.dumps(detail).encode()
|
|
150
|
+
assert run(cli, "cloudpc", "channels", "get", "7") == 0
|
|
151
|
+
assert json.loads(capsys.readouterr().out) == detail
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def test_channel_message_style_validation_failure_is_reported_without_retry(cli, transport, capsys):
|
|
155
|
+
url = "http://test/api/v1/cloudpc/channels/7"
|
|
156
|
+
body = {"detail": [{"loc": ["body", "appearance", "userMessageStyle", "borderRadius", "topRight"], "msg": "Field required"}]}
|
|
157
|
+
transport[1][url] = HTTPError(url, 422, "Unprocessable Entity", {}, io.BytesIO(json.dumps(body).encode()))
|
|
158
|
+
payload = {"name": "Workspace", "appearance": {"userMessageStyle": {"borderRadius": {"topLeft": 1}}}}
|
|
159
|
+
assert run(cli, "cloudpc", "channels", "update", "7", "--json", json.dumps(payload)) == 1
|
|
160
|
+
assert len(transport[0]) == 1
|
|
161
|
+
error = capsys.readouterr().err
|
|
162
|
+
assert "HTTP 422" in error and "userMessageStyle" in error and "topRight" in error
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def test_channel_message_style_docs_and_help(cli, capsys):
|
|
166
|
+
assert cli.main(["docs", "show", "cloudpc.channels.update", "--json"]) == 0
|
|
167
|
+
fields = {field["name"]: field for field in json.loads(capsys.readouterr().out)["body_fields"]}
|
|
168
|
+
assert "userMessageStyle" in fields["appearance"]["description"]
|
|
169
|
+
prefix = "appearance.userMessageStyle"
|
|
170
|
+
for key in ["backgroundColor", "borderColor", "textColor", "borderRadius"]:
|
|
171
|
+
assert not fields[f"{prefix}.{key}"]["required"]
|
|
172
|
+
assert "null" in fields[f"{prefix}.{key}"]["description"].lower()
|
|
173
|
+
radius = fields[f"{prefix}.borderRadius"]["description"]
|
|
174
|
+
for key in ["topLeft", "topRight", "bottomLeft", "bottomRight", "0-100"]:
|
|
175
|
+
assert key in radius
|
|
176
|
+
with pytest.raises(SystemExit) as exc:
|
|
177
|
+
cli.main(["cloudpc", "channels", "update", "--help"])
|
|
178
|
+
assert exc.value.code == 0
|
|
179
|
+
assert "appearance.userMessageStyle" in capsys.readouterr().out
|
|
180
|
+
|
|
181
|
+
|
|
82
182
|
@pytest.mark.parametrize(("source", "expected"), [("latest", 23), ("published", 19)])
|
|
83
183
|
def test_resolve_version_and_report_actual_source(cli, transport, capsys, source, expected):
|
|
84
184
|
calls, responses = transport
|
|
@@ -254,3 +354,83 @@ def test_dev_chat_requires_message_or_existing_round(cli, transport):
|
|
|
254
354
|
assert run(cli, 'cloudpc', 'dev', 'chat', '7') == 1
|
|
255
355
|
assert run(cli, 'cloudpc', 'dev', 'chat', '7', '--attach') == 1
|
|
256
356
|
assert transport[0] == []
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def test_binary_upload_replays_exact_body_after_ssl_eof(cli, monkeypatch, tmp_path, capsys):
|
|
360
|
+
import ssl
|
|
361
|
+
source = tmp_path / '报告.pdf'
|
|
362
|
+
content = b'%PDF-1.7\n\x00\xff' + b'x' * (2 * 1024 * 1024)
|
|
363
|
+
source.write_bytes(content)
|
|
364
|
+
calls = []
|
|
365
|
+
def send(request, **kwargs):
|
|
366
|
+
calls.append(request)
|
|
367
|
+
if request.method == 'GET':
|
|
368
|
+
return io.BytesIO(b'{"sandbox_id":42,"exists":true}')
|
|
369
|
+
if len(calls) == 2:
|
|
370
|
+
raise ssl.SSLEOFError('UNEXPECTED_EOF_WHILE_READING')
|
|
371
|
+
return io.BytesIO(b'{"path":"/workspace/files/uploads/saved.pdf"}')
|
|
372
|
+
monkeypatch.setattr(cli, 'urlopen', send)
|
|
373
|
+
monkeypatch.setattr(cli.time, 'sleep', lambda _: None)
|
|
374
|
+
assert run(cli, 'cloudpc', 'files', 'upload', '7', '--file', str(source)) == 0
|
|
375
|
+
assert calls[1].data == calls[2].data == content
|
|
376
|
+
assert calls[1].full_url == calls[2].full_url
|
|
377
|
+
assert 'sandbox_id=42' in calls[1].full_url
|
|
378
|
+
assert 'upload_id=' in capsys.readouterr().err
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def test_exec_retries_same_submission_and_cursor(cli, monkeypatch, capsys):
|
|
382
|
+
from http.client import IncompleteRead
|
|
383
|
+
calls = []
|
|
384
|
+
def send(request, **kwargs):
|
|
385
|
+
calls.append(request)
|
|
386
|
+
if len(calls) in {1, 3}:
|
|
387
|
+
raise IncompleteRead(b'partial')
|
|
388
|
+
if request.method == 'POST':
|
|
389
|
+
return io.BytesIO(b'{}')
|
|
390
|
+
return io.BytesIO(b'{"events":[{"event":{"type":"stdout","text":"once"}}],"cursor":1,"state":"exited","exit_code":0}')
|
|
391
|
+
monkeypatch.setattr(cli, 'urlopen', send)
|
|
392
|
+
monkeypatch.setattr(cli.time, 'sleep', lambda _: None)
|
|
393
|
+
assert run(cli, 'cloudpc', 'dev', 'exec', '7', '--command', 'echo once', '--execution-id', 'job') == 0
|
|
394
|
+
assert calls[0].data == calls[1].data
|
|
395
|
+
assert calls[2].full_url == calls[3].full_url
|
|
396
|
+
assert capsys.readouterr().out == 'once'
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
@pytest.mark.parametrize('code', [400, 401, 403, 409, 413])
|
|
400
|
+
def test_upload_permanent_errors_not_retried(cli, monkeypatch, tmp_path, code):
|
|
401
|
+
file = tmp_path / 'x.pdf'
|
|
402
|
+
file.write_bytes(b'\xff\x00')
|
|
403
|
+
calls = []
|
|
404
|
+
def send(request, **kwargs):
|
|
405
|
+
calls.append(request)
|
|
406
|
+
raise HTTPError(request.full_url, code, 'rejected', {}, io.BytesIO(b'rejected'))
|
|
407
|
+
monkeypatch.setattr(cli, 'urlopen', send)
|
|
408
|
+
assert run(cli, 'cloudpc', 'files', 'upload', '7', '--file', str(file), '--sandbox-id', '42', '--upload-id', 'a' * 32) == 1
|
|
409
|
+
assert len(calls) == 1
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def test_exec_retry_exhaustion_preserves_attach_hint(cli, monkeypatch, capsys):
|
|
413
|
+
calls = []
|
|
414
|
+
def send(request, **kwargs):
|
|
415
|
+
calls.append(request)
|
|
416
|
+
raise ConnectionResetError('reset')
|
|
417
|
+
monkeypatch.setattr(cli, 'urlopen', send)
|
|
418
|
+
monkeypatch.setattr(cli.time, 'sleep', lambda _: None)
|
|
419
|
+
assert run(cli, 'cloudpc', 'dev', 'exec-attach', '7', 'job', '--after', '8') == 1
|
|
420
|
+
assert len(calls) == 5
|
|
421
|
+
assert all('after=8' in request.full_url for request in calls)
|
|
422
|
+
assert 'exec-attach 7 job --after 8' in capsys.readouterr().err
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
@pytest.mark.parametrize('size', [0, 50 * 1024 * 1024 + 1])
|
|
426
|
+
def test_upload_size_rejected_before_send(cli, transport, tmp_path, size):
|
|
427
|
+
source = tmp_path / 'x.pdf'
|
|
428
|
+
with source.open('wb') as file:
|
|
429
|
+
file.truncate(size)
|
|
430
|
+
assert run(cli, 'cloudpc', 'files', 'upload', '7', '--file', str(source), '--upload-id', 'a' * 32, '--sandbox-id', '42') == 1
|
|
431
|
+
assert transport[0] == []
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def test_upload_manual_retry_requires_both_ids(cli, transport):
|
|
435
|
+
assert run(cli, 'cloudpc', 'files', 'upload', '7', '--file', 'x.pdf', '--upload-id', 'a' * 32) == 1
|
|
436
|
+
assert transport[0] == []
|
|
File without changes
|
{deepflow_openagent_cli-0.1.6 → deepflow_openagent_cli-0.1.8}/src/deepflow_openagent_cli/__main__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|