arkclaw-webchat-cli 0.5.3__tar.gz → 0.6.1__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.
- {arkclaw_webchat_cli-0.5.3 → arkclaw_webchat_cli-0.6.1}/PKG-INFO +1 -1
- {arkclaw_webchat_cli-0.5.3 → arkclaw_webchat_cli-0.6.1}/pyproject.toml +1 -1
- {arkclaw_webchat_cli-0.5.3 → arkclaw_webchat_cli-0.6.1}/src/ee_claw/cli.py +25 -7
- {arkclaw_webchat_cli-0.5.3 → arkclaw_webchat_cli-0.6.1}/src/ee_claw/flows.py +246 -68
- {arkclaw_webchat_cli-0.5.3 → arkclaw_webchat_cli-0.6.1}/src/ee_claw/identity.py +14 -0
- {arkclaw_webchat_cli-0.5.3 → arkclaw_webchat_cli-0.6.1}/src/ee_claw/transport/openclaw.py +32 -3
- {arkclaw_webchat_cli-0.5.3 → arkclaw_webchat_cli-0.6.1}/.gitignore +0 -0
- {arkclaw_webchat_cli-0.5.3 → arkclaw_webchat_cli-0.6.1}/README.md +0 -0
- {arkclaw_webchat_cli-0.5.3 → arkclaw_webchat_cli-0.6.1}/src/ee_claw/__init__.py +0 -0
- {arkclaw_webchat_cli-0.5.3 → arkclaw_webchat_cli-0.6.1}/src/ee_claw/attachments.py +0 -0
- {arkclaw_webchat_cli-0.5.3 → arkclaw_webchat_cli-0.6.1}/src/ee_claw/config.py +0 -0
- {arkclaw_webchat_cli-0.5.3 → arkclaw_webchat_cli-0.6.1}/src/ee_claw/control.py +0 -0
- {arkclaw_webchat_cli-0.5.3 → arkclaw_webchat_cli-0.6.1}/src/ee_claw/core.py +0 -0
- {arkclaw_webchat_cli-0.5.3 → arkclaw_webchat_cli-0.6.1}/src/ee_claw/doctor.py +0 -0
- {arkclaw_webchat_cli-0.5.3 → arkclaw_webchat_cli-0.6.1}/src/ee_claw/errors.py +0 -0
- {arkclaw_webchat_cli-0.5.3 → arkclaw_webchat_cli-0.6.1}/src/ee_claw/oauth.py +0 -0
- {arkclaw_webchat_cli-0.5.3 → arkclaw_webchat_cli-0.6.1}/src/ee_claw/output.py +0 -0
- {arkclaw_webchat_cli-0.5.3 → arkclaw_webchat_cli-0.6.1}/src/ee_claw/policy.py +0 -0
- {arkclaw_webchat_cli-0.5.3 → arkclaw_webchat_cli-0.6.1}/src/ee_claw/providers.py +0 -0
- {arkclaw_webchat_cli-0.5.3 → arkclaw_webchat_cli-0.6.1}/src/ee_claw/secrets_store.py +0 -0
- {arkclaw_webchat_cli-0.5.3 → arkclaw_webchat_cli-0.6.1}/src/ee_claw/sts.py +0 -0
- {arkclaw_webchat_cli-0.5.3 → arkclaw_webchat_cli-0.6.1}/src/ee_claw/transport/__init__.py +0 -0
- {arkclaw_webchat_cli-0.5.3 → arkclaw_webchat_cli-0.6.1}/src/ee_claw/transport/a2a.py +0 -0
- {arkclaw_webchat_cli-0.5.3 → arkclaw_webchat_cli-0.6.1}/src/ee_claw/transport/base.py +0 -0
- {arkclaw_webchat_cli-0.5.3 → arkclaw_webchat_cli-0.6.1}/src/ee_claw/update.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "arkclaw-webchat-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.6.1"
|
|
8
8
|
description = "CLI to chat with an ArkClaw EE space's Claw over enterprise SSO — zero permanent AK/SK."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -161,11 +161,20 @@ def chat(
|
|
|
161
161
|
message_opt: str | None = typer.Option(
|
|
162
162
|
None, "-m", "--message", help="One-shot message (same as the positional argument)."
|
|
163
163
|
),
|
|
164
|
+
agent: str = typer.Option(
|
|
165
|
+
"main",
|
|
166
|
+
"--agent",
|
|
167
|
+
help="Which agent in the claw to talk to (its agentId; see `arkclaw agents`). "
|
|
168
|
+
"Defaults to the claw's main agent.",
|
|
169
|
+
),
|
|
164
170
|
session: str | None = typer.Option(
|
|
165
171
|
None,
|
|
166
172
|
"--session",
|
|
167
|
-
help="
|
|
168
|
-
"
|
|
173
|
+
help="Resume a specific conversation by its 会话ID (see `arkclaw sessions`). "
|
|
174
|
+
"Omit to continue the agent's main session.",
|
|
175
|
+
),
|
|
176
|
+
new: bool = typer.Option(
|
|
177
|
+
False, "--new", help="Start a fresh conversation instead of continuing the last one."
|
|
169
178
|
),
|
|
170
179
|
approve_all: bool = typer.Option(
|
|
171
180
|
False,
|
|
@@ -192,6 +201,8 @@ def chat(
|
|
|
192
201
|
target=target,
|
|
193
202
|
message=message_opt or message,
|
|
194
203
|
session=session,
|
|
204
|
+
agent=agent,
|
|
205
|
+
new=new,
|
|
195
206
|
approve_all=approve_all,
|
|
196
207
|
files=list(file) or None,
|
|
197
208
|
output=output,
|
|
@@ -200,10 +211,17 @@ def chat(
|
|
|
200
211
|
|
|
201
212
|
|
|
202
213
|
@app.command()
|
|
203
|
-
def sessions(
|
|
204
|
-
|
|
214
|
+
def sessions(
|
|
215
|
+
claw: str | None = typer.Argument(
|
|
216
|
+
None, metavar="[CLAW]", help="Claw id (ci-...). Omit to use your default claw."
|
|
217
|
+
),
|
|
218
|
+
agent: str = typer.Option("main", "--agent", help="Which agent's conversations to list."),
|
|
219
|
+
json_mode: bool = _json_opt(),
|
|
220
|
+
) -> None:
|
|
221
|
+
"""List an agent's conversations from the server (newest first). Resume one
|
|
222
|
+
with `arkclaw chat <claw> --session <会话ID>`, or start fresh with `--new`."""
|
|
205
223
|
emitter = Emitter(json_mode=json_mode)
|
|
206
|
-
_run(emitter, lambda: flows.do_sessions(emitter))
|
|
224
|
+
_run(emitter, lambda: flows.do_sessions(emitter, clawid=claw, agent=agent))
|
|
207
225
|
|
|
208
226
|
|
|
209
227
|
@app.command(name="ls")
|
|
@@ -243,8 +261,8 @@ def push(
|
|
|
243
261
|
|
|
244
262
|
@app.command()
|
|
245
263
|
def agents(json_mode: bool = _json_opt()) -> None:
|
|
246
|
-
"""List the agents
|
|
247
|
-
|
|
264
|
+
"""List the agents you can chat with (openclaw: the agents in your accessible
|
|
265
|
+
claws; a2a: the agent at the endpoint). Then `chat <claw-or-name>`."""
|
|
248
266
|
emitter = Emitter(json_mode=json_mode)
|
|
249
267
|
_run(emitter, lambda: flows.do_agents(emitter))
|
|
250
268
|
|
|
@@ -20,6 +20,7 @@ import signal
|
|
|
20
20
|
import sys
|
|
21
21
|
import time
|
|
22
22
|
import urllib.parse
|
|
23
|
+
import uuid
|
|
23
24
|
from typing import Any
|
|
24
25
|
|
|
25
26
|
from ee_claw import config as config_mod
|
|
@@ -116,6 +117,79 @@ def _user_identity(id_token: str | None) -> dict[str, str]:
|
|
|
116
117
|
return ident
|
|
117
118
|
|
|
118
119
|
|
|
120
|
+
def _agent_display_name(agent: dict[str, Any]) -> str:
|
|
121
|
+
"""Friendly name for an agent. The main agent often comes back named "main";
|
|
122
|
+
show a friendly label instead of leaking the internal id (mirrors the web)."""
|
|
123
|
+
name = str(agent.get("name") or "").strip()
|
|
124
|
+
agent_id = str(agent.get("agentId") or "")
|
|
125
|
+
is_main = bool(agent.get("default")) or agent_id == "main"
|
|
126
|
+
if is_main and (not name or name == "main"):
|
|
127
|
+
return "ArkClaw 智能助手"
|
|
128
|
+
return name or agent_id or "(未命名)"
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _list_agents_for_claws(
|
|
132
|
+
cfg: SessionConfig, creds: dict[str, str], identity: dict[str, str], claw_ids: list[str]
|
|
133
|
+
) -> dict[str, tuple[str, list[dict[str, Any]]]]:
|
|
134
|
+
"""For each claw, list its agents over the ws (``arkclaw.team.agent.list``) —
|
|
135
|
+
which also proves access (the ChatToken mint denies non-owners). Returns
|
|
136
|
+
``clawid → (status, agents)``, status ∈ {"ok","denied","error"}. Parallel."""
|
|
137
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
138
|
+
|
|
139
|
+
region = str(cfg.region)
|
|
140
|
+
|
|
141
|
+
def one(clawid: str) -> tuple[str, str, list[dict[str, Any]]]:
|
|
142
|
+
t = OpenClawTransport(clawid=clawid, region=region, creds=creds, identity=identity)
|
|
143
|
+
try:
|
|
144
|
+
return clawid, "ok", asyncio.run(t.list_agents())
|
|
145
|
+
except ClawAccessDeniedError:
|
|
146
|
+
return clawid, "denied", []
|
|
147
|
+
except ArkclawError:
|
|
148
|
+
return clawid, "error", [] # transient — keep the claw, mark unknown
|
|
149
|
+
|
|
150
|
+
out: dict[str, tuple[str, list[dict[str, Any]]]] = {}
|
|
151
|
+
with ThreadPoolExecutor(max_workers=min(8, max(1, len(claw_ids)))) as ex:
|
|
152
|
+
for clawid, status, agents in ex.map(one, claw_ids):
|
|
153
|
+
out[clawid] = (status, agents)
|
|
154
|
+
return out
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _resolve_session_key(
|
|
158
|
+
cfg: SessionConfig, id_token: str, clawid: str, agent: str, session: str | None, new: bool
|
|
159
|
+
) -> str | None:
|
|
160
|
+
"""Compute the OpenClaw sessionKey ``agent:<agentId>:<label>`` for a chat:
|
|
161
|
+
|
|
162
|
+
* ``--new`` → a fresh ``agent:<agent>:cli-<uuid>`` (new conversation)
|
|
163
|
+
* ``--session <id>`` → resume: a full ``agent:…`` key passes through; a short
|
|
164
|
+
sessionId is resolved to its key via ``sessions.list``
|
|
165
|
+
* neither, agent=main → ``None`` (the default ``agent:main:main`` session)
|
|
166
|
+
* neither, other agent→ ``agent:<agent>:main``
|
|
167
|
+
"""
|
|
168
|
+
agent = agent or "main"
|
|
169
|
+
if new:
|
|
170
|
+
return f"agent:{agent}:cli-{uuid.uuid4()}"
|
|
171
|
+
if not session:
|
|
172
|
+
return None if agent == "main" else f"agent:{agent}:main"
|
|
173
|
+
if session.startswith("agent:"):
|
|
174
|
+
return session # already a full sessionKey
|
|
175
|
+
# A hex token is a 会话ID copied from `arkclaw sessions` → resolve to its key.
|
|
176
|
+
if re.fullmatch(r"[0-9a-f]{6,}", session):
|
|
177
|
+
creds = _openclaw_creds(cfg, id_token)
|
|
178
|
+
t = OpenClawTransport(
|
|
179
|
+
clawid=clawid, region=str(cfg.region), creds=creds, identity=_user_identity(id_token)
|
|
180
|
+
)
|
|
181
|
+
for s in asyncio.run(t.list_sessions(agent)):
|
|
182
|
+
sid = str(s.get("sessionId") or "")
|
|
183
|
+
if sid and (sid == session or sid.startswith(session)):
|
|
184
|
+
return str(s.get("key") or f"agent:{agent}:{session}")
|
|
185
|
+
raise ValidationError(
|
|
186
|
+
f"会话 {session!r} 不存在。",
|
|
187
|
+
hint="用 `arkclaw sessions` 查看现有会话,或加 `--new` 开新会话。",
|
|
188
|
+
)
|
|
189
|
+
# Otherwise a human-named session label (created/resumed by this key).
|
|
190
|
+
return f"agent:{agent}:{session}"
|
|
191
|
+
|
|
192
|
+
|
|
119
193
|
def _normalize(url: str) -> tuple[str, str]:
|
|
120
194
|
base = (url if "//" in url else "https://" + url).rstrip("/")
|
|
121
195
|
host = urllib.parse.urlparse(base).hostname or ""
|
|
@@ -131,55 +205,72 @@ def _is_userpool_address(host: str) -> bool:
|
|
|
131
205
|
|
|
132
206
|
|
|
133
207
|
def do_init(emitter: Emitter, address: str | None = None) -> dict[str, Any]:
|
|
134
|
-
"""``arkclaw init``: one-time
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
208
|
+
"""``arkclaw init``: one-time setup. The GOAL is **address-only** — when the
|
|
209
|
+
space publishes its CLI config (issuer/region/client_id/role…) via
|
|
210
|
+
``/.well-known/arkclaw-cli`` or GetAuthConfig, the user gives ONLY the
|
|
211
|
+
address and nothing is asked (works non-interactively too). Until then, it
|
|
212
|
+
prompts for the few bits discovery can't provide (the CLI's OAuth client,
|
|
213
|
+
the STS role)."""
|
|
214
|
+
interactive = not emitter.json and sys.stdin.isatty()
|
|
215
|
+
|
|
216
|
+
def need_tty() -> None:
|
|
217
|
+
if not interactive:
|
|
218
|
+
raise ValidationError(
|
|
219
|
+
"该空间尚未发布 CLI 配置,需要补充 client_id/role,而当前非交互终端。",
|
|
220
|
+
hint="在终端运行 `arkclaw init <地址>`;或脚本里用 env(ARKCLAW_CLIENT_ID/ROLE_TRN)+ flags。",
|
|
221
|
+
)
|
|
144
222
|
|
|
145
|
-
def ask(label: str, default: str | None = None, *, required: bool = False) -> str
|
|
223
|
+
def ask(label: str, default: str | None = None, *, required: bool = False) -> str:
|
|
146
224
|
suffix = f" [{default}]" if default else ""
|
|
147
225
|
while True:
|
|
148
226
|
ans = input(f" {label}{suffix}: ").strip() or (default or "")
|
|
149
227
|
if ans or not required:
|
|
150
|
-
return ans
|
|
228
|
+
return ans
|
|
151
229
|
emitter.line(" (必填)")
|
|
152
230
|
|
|
153
|
-
|
|
154
|
-
|
|
231
|
+
if not address:
|
|
232
|
+
need_tty()
|
|
233
|
+
emitter.line("arkclaw init —— 配置一次(能自动发现的不问你)\n")
|
|
234
|
+
address = ask("空间登录地址 (https://…)", required=True)
|
|
155
235
|
base, host = _normalize(str(address))
|
|
156
236
|
disc = discover_cli_config(base) or {}
|
|
157
237
|
issuer = disc.get("issuer") or (f"https://{host}" if _is_userpool_address(host) else None)
|
|
158
238
|
region = disc.get("region") or derive_region(base)
|
|
159
|
-
if
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
"CLI
|
|
171
|
-
)
|
|
172
|
-
role_trn: object = None
|
|
173
|
-
endpoint: object = None
|
|
174
|
-
if transport == "openclaw":
|
|
175
|
-
role_trn = disc.get("role_trn") or ask("STS role TRN (trn:iam::<账号>:role/<名字>)", required=True)
|
|
239
|
+
transport = "a2a" if str(disc.get("transport") or "").lower() == "a2a" else "openclaw"
|
|
240
|
+
client_id = disc.get("client_id")
|
|
241
|
+
role_trn = disc.get("role_trn")
|
|
242
|
+
provider_trn = disc.get("provider_trn")
|
|
243
|
+
endpoint = disc.get("endpoint")
|
|
244
|
+
clawid = disc.get("clawid")
|
|
245
|
+
|
|
246
|
+
# Fully discovered = the space published this CLI's client + target plane →
|
|
247
|
+
# nothing to ask (address-only). Otherwise prompt ONLY for what's missing.
|
|
248
|
+
complete = bool(client_id) and (bool(role_trn) if transport == "openclaw" else bool(endpoint))
|
|
249
|
+
if complete:
|
|
250
|
+
emitter.line("✓ 该空间已发布 CLI 配置 —— 全部自动解析,无需手动输入。")
|
|
176
251
|
else:
|
|
177
|
-
|
|
178
|
-
|
|
252
|
+
need_tty()
|
|
253
|
+
if issuer:
|
|
254
|
+
emitter.line(" ✓ 自动发现身份池 issuer")
|
|
255
|
+
if region:
|
|
256
|
+
emitter.line(f" ✓ 区域: {region}")
|
|
257
|
+
# "webchat" = the ChatToken/wss path (internal name "openclaw"); "a2a" unchanged.
|
|
258
|
+
t_in = ask("传输方式 (webchat/a2a)", default=("a2a" if transport == "a2a" else "webchat"))
|
|
259
|
+
transport = "a2a" if t_in.lower() == "a2a" else "openclaw"
|
|
260
|
+
if not client_id:
|
|
261
|
+
client_id = ask(
|
|
262
|
+
"CLI client_id(public OAuth 客户端,问管理员;平台发布发现后免此项)", required=True
|
|
263
|
+
)
|
|
264
|
+
if transport == "openclaw" and not role_trn:
|
|
265
|
+
role_trn = ask("STS role TRN (trn:iam::<账号>:role/<名字>)", required=True)
|
|
266
|
+
elif transport == "a2a" and not endpoint:
|
|
267
|
+
endpoint = ask("A2A endpoint (https://…)", required=True)
|
|
268
|
+
if not clawid:
|
|
269
|
+
clawid = ask("默认 claw id (ci-…,可留空,登录后用 arkclaw agents 选)") or None
|
|
179
270
|
|
|
180
271
|
saved = {
|
|
181
272
|
"address": base, "issuer": issuer, "client_id": client_id, "transport": transport,
|
|
182
|
-
"region": region, "role_trn": role_trn, "provider_trn":
|
|
273
|
+
"region": region, "role_trn": role_trn, "provider_trn": provider_trn,
|
|
183
274
|
"endpoint": endpoint, "space_id": disc.get("space_id"), "clawid": clawid,
|
|
184
275
|
}
|
|
185
276
|
config_mod.save_defaults(saved)
|
|
@@ -735,6 +826,8 @@ def do_chat(
|
|
|
735
826
|
target: str | None = None,
|
|
736
827
|
message: str | None = None,
|
|
737
828
|
session: str | None = None,
|
|
829
|
+
agent: str = "main",
|
|
830
|
+
new: bool = False,
|
|
738
831
|
approve_all: bool = False,
|
|
739
832
|
files: list[str] | None = None,
|
|
740
833
|
output: str | None = None,
|
|
@@ -790,6 +883,12 @@ def do_chat(
|
|
|
790
883
|
)
|
|
791
884
|
|
|
792
885
|
approver = _build_approver(emitter, approve_all=approve_all, interactive=interactive)
|
|
886
|
+
# Resolve agent + session → a concrete sessionKey (openclaw only; a2a uses
|
|
887
|
+
# the session name as the context label).
|
|
888
|
+
if cfg.transport == "openclaw":
|
|
889
|
+
effective_claw = clawid or cfg.claw
|
|
890
|
+
if effective_claw:
|
|
891
|
+
session = _resolve_session_key(cfg, id_token, effective_claw, agent, session, new)
|
|
793
892
|
target, transport = _target_transport(
|
|
794
893
|
cfg, clawid, id_token, session=session, approver=approver
|
|
795
894
|
)
|
|
@@ -831,8 +930,9 @@ def do_chat(
|
|
|
831
930
|
pass
|
|
832
931
|
emitter.line(
|
|
833
932
|
f"ArkClaw chat · {target}"
|
|
933
|
+
+ (f" · agent {agent}" if agent and agent != "main" else "")
|
|
934
|
+
+ (f" · 会话 {session.split(':')[-1][:16]}" if session else "")
|
|
834
935
|
+ (f" · 空间 {cfg.space}" if cfg.space and cfg.space != target else "")
|
|
835
|
-
+ (f" · 会话 {session}" if session else "")
|
|
836
936
|
+ " (Ctrl+C 退出)"
|
|
837
937
|
)
|
|
838
938
|
while True:
|
|
@@ -863,11 +963,54 @@ def do_chat(
|
|
|
863
963
|
emitter.line("")
|
|
864
964
|
|
|
865
965
|
|
|
866
|
-
def do_sessions(
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
966
|
+
def do_sessions(
|
|
967
|
+
emitter: Emitter, clawid: str | None = None, agent: str = "main"
|
|
968
|
+
) -> dict[str, Any]:
|
|
969
|
+
"""``arkclaw sessions [<claw>] [--agent <id>]``: the conversations of an agent
|
|
970
|
+
in a claw, from the server (ws ``sessions.list``) — newest first. Resume one
|
|
971
|
+
with ``arkclaw chat <claw> --session <会话ID>``. Falls back to the local
|
|
972
|
+
record for a2a / when the claw can't be reached."""
|
|
973
|
+
cfg = config_mod.load_config()
|
|
974
|
+
target = (clawid or (cfg.claw if cfg else None)) if cfg else None
|
|
975
|
+
if cfg and cfg.space and cfg.transport == "openclaw" and target:
|
|
976
|
+
try:
|
|
977
|
+
_, id_token = _load_session(cfg)
|
|
978
|
+
creds = _openclaw_creds(cfg, id_token)
|
|
979
|
+
t = OpenClawTransport(
|
|
980
|
+
clawid=target, region=str(cfg.region), creds=creds, identity=_user_identity(id_token)
|
|
981
|
+
)
|
|
982
|
+
sessions = asyncio.run(t.list_sessions(agent))
|
|
983
|
+
sessions.sort(key=lambda s: float(s.get("updatedAt") or 0), reverse=True)
|
|
984
|
+
emitter.line(f"★ claw {target} · agent {agent} · 空间 {cfg.space}")
|
|
985
|
+
if sessions:
|
|
986
|
+
emitter.table(
|
|
987
|
+
["标题", "上次活动", "会话ID", "状态"],
|
|
988
|
+
[
|
|
989
|
+
[
|
|
990
|
+
(str(s.get("derivedTitle") or "(无标题)"))[:48],
|
|
991
|
+
_ago_ms(s.get("updatedAt")),
|
|
992
|
+
str(s.get("sessionId") or "")[:8],
|
|
993
|
+
str(s.get("status") or "—"),
|
|
994
|
+
]
|
|
995
|
+
for s in sessions
|
|
996
|
+
],
|
|
997
|
+
)
|
|
998
|
+
emitter.line(" 续聊: arkclaw chat <claw> --session <会话ID>;新开: --new")
|
|
999
|
+
else:
|
|
1000
|
+
emitter.line(" 该 agent 还没有会话。`arkclaw chat` 开始第一段对话。")
|
|
1001
|
+
return {
|
|
1002
|
+
"claw": target, "agent": agent,
|
|
1003
|
+
"sessions": [
|
|
1004
|
+
{"sessionId": s.get("sessionId"), "key": s.get("key"),
|
|
1005
|
+
"title": s.get("derivedTitle"), "updatedAt": s.get("updatedAt"),
|
|
1006
|
+
"status": s.get("status")}
|
|
1007
|
+
for s in sessions
|
|
1008
|
+
],
|
|
1009
|
+
}
|
|
1010
|
+
except ArkclawError as e:
|
|
1011
|
+
emitter.line(f" ⚠ 无法从服务端列会话({e.code}),回退本地记录:")
|
|
1012
|
+
|
|
1013
|
+
rows = config_mod.list_sessions() # a2a / not-logged-in / no-claw / server-error fallback
|
|
871
1014
|
if rows:
|
|
872
1015
|
emitter.table(
|
|
873
1016
|
["会话", "目标", "空间", "最近使用"],
|
|
@@ -916,13 +1059,15 @@ def _ago(ts: object) -> str:
|
|
|
916
1059
|
|
|
917
1060
|
|
|
918
1061
|
def do_agents(emitter: Emitter) -> dict[str, Any]:
|
|
919
|
-
"""``arkclaw agents``: the agents
|
|
1062
|
+
"""``arkclaw agents``: the agents you can chat with.
|
|
920
1063
|
|
|
921
|
-
openclaw → the
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
(
|
|
925
|
-
|
|
1064
|
+
openclaw → the AGENTS hosted in your accessible claws. The candidate claws
|
|
1065
|
+
are the ones YOU have used from this machine (kept locally, newest first)
|
|
1066
|
+
plus your default; for each we list its agents over the ws
|
|
1067
|
+
(``arkclaw.team.agent.list``), which also verifies access — inaccessible
|
|
1068
|
+
claws are pruned. This is NOT a space-wide server enumeration (not per-user
|
|
1069
|
+
scoped for the CLI's role). You add a claw by chatting it once: ``arkclaw
|
|
1070
|
+
chat ci-...``.
|
|
926
1071
|
|
|
927
1072
|
a2a → the agent at the endpoint (its card). Profiles live under ``arkclaw
|
|
928
1073
|
profile``, full conversation history under ``arkclaw sessions``."""
|
|
@@ -982,12 +1127,11 @@ def do_agents(emitter: Emitter) -> dict[str, Any]:
|
|
|
982
1127
|
)
|
|
983
1128
|
elif cfg.transport == "openclaw":
|
|
984
1129
|
emitter.line(f"★ 当前登录(openclaw)· 空间 {cfg.space}")
|
|
985
|
-
# The
|
|
986
|
-
#
|
|
987
|
-
#
|
|
988
|
-
#
|
|
989
|
-
#
|
|
990
|
-
# (record_session) and shows up here next time.
|
|
1130
|
+
# The chat-able units are AGENTS, and one claw hosts several. Candidates
|
|
1131
|
+
# are the claws YOU have used from this machine (local history) + your
|
|
1132
|
+
# default; for each we list its agents over the ws (which also proves
|
|
1133
|
+
# access). Inaccessible claws are pruned from history (+ cleared as
|
|
1134
|
+
# default). NOT a space-wide server enumeration (not per-user scoped).
|
|
991
1135
|
seen: dict[str, float] = {}
|
|
992
1136
|
for s in config_mod.list_sessions():
|
|
993
1137
|
cid = s.get("claw")
|
|
@@ -996,23 +1140,57 @@ def do_agents(emitter: Emitter) -> dict[str, Any]:
|
|
|
996
1140
|
seen[str(cid)] = max(seen.get(str(cid), 0.0), float(ts) if isinstance(ts, (int, float)) else 0.0)
|
|
997
1141
|
if cfg.claw and str(cfg.claw) not in seen:
|
|
998
1142
|
seen[str(cfg.claw)] = 0.0 # the default claw, even if not chatted yet
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
]
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1143
|
+
|
|
1144
|
+
claw_ids = sorted(seen, key=lambda c: seen[c], reverse=True)
|
|
1145
|
+
rows: list[list[str]] = []
|
|
1146
|
+
agents_data: list[dict[str, Any]] = []
|
|
1147
|
+
checked = False
|
|
1148
|
+
if claw_ids:
|
|
1149
|
+
if not emitter.json:
|
|
1150
|
+
emitter.line(" 列出 agents…")
|
|
1151
|
+
try:
|
|
1152
|
+
_, id_token = _load_session(cfg)
|
|
1153
|
+
creds = _openclaw_creds(cfg, id_token)
|
|
1154
|
+
identity = _user_identity(id_token)
|
|
1155
|
+
results = _list_agents_for_claws(cfg, creds, identity, claw_ids)
|
|
1156
|
+
checked = True
|
|
1157
|
+
for clawid in claw_ids:
|
|
1158
|
+
status, agents = results[clawid]
|
|
1159
|
+
if status == "denied":
|
|
1160
|
+
config_mod.forget_claw(str(cfg.space), clawid) # stop listing it
|
|
1161
|
+
if cfg.claw == clawid:
|
|
1162
|
+
config_mod.save_config(dataclasses.replace(cfg, claw=None))
|
|
1163
|
+
cfg = config_mod.load_config() or cfg
|
|
1164
|
+
current["default_claw"] = cfg.claw
|
|
1165
|
+
continue
|
|
1166
|
+
if status == "error" or not agents:
|
|
1167
|
+
rows.append(["(无法列出 agent)", "—", clawid, "★" if clawid == cfg.claw else ""])
|
|
1168
|
+
continue
|
|
1169
|
+
for a in agents:
|
|
1170
|
+
aid = str(a.get("agentId") or "")
|
|
1171
|
+
is_main = bool(a.get("default")) or aid == "main"
|
|
1172
|
+
rows.append([
|
|
1173
|
+
_agent_display_name(a), aid or "—", clawid,
|
|
1174
|
+
"★" if (clawid == cfg.claw and is_main) else "",
|
|
1175
|
+
])
|
|
1176
|
+
agents_data.append({
|
|
1177
|
+
"clawId": clawid, "agentId": aid, "name": _agent_display_name(a),
|
|
1178
|
+
"description": a.get("description"), "default": is_main,
|
|
1179
|
+
})
|
|
1180
|
+
except ArkclawError:
|
|
1181
|
+
checked = False # offline / expired / no creds → can't list agents
|
|
1182
|
+
|
|
1183
|
+
current["agents"] = agents_data
|
|
1184
|
+
current["source"] = "verified" if checked else "local-history-unchecked"
|
|
1185
|
+
if checked and rows:
|
|
1186
|
+
emitter.table(["Agent", "agentId", "所属 Claw", "默认"], rows)
|
|
1187
|
+
elif checked:
|
|
1188
|
+
emitter.line(" 本空间没有你可访问的 claw / agent(历史里的都已无权或不存在)。")
|
|
1189
|
+
elif claw_ids:
|
|
1190
|
+
emitter.line(" ⚠ 未能连接核对(离线/会话过期),无法列出 agent;以下为本地历史的 claw:")
|
|
1006
1191
|
emitter.table(
|
|
1007
|
-
["Claw
|
|
1008
|
-
[
|
|
1009
|
-
[
|
|
1010
|
-
str(c["ClawInstanceId"]),
|
|
1011
|
-
_ago(c["last_used"]) if c["last_used"] else "—",
|
|
1012
|
-
"★" if c["ClawInstanceId"] == cfg.claw else "",
|
|
1013
|
-
]
|
|
1014
|
-
for c in claws
|
|
1015
|
-
],
|
|
1192
|
+
["Claw", "上次使用"],
|
|
1193
|
+
[[c, _ago(seen[c]) if seen[c] else "—"] for c in claw_ids],
|
|
1016
1194
|
)
|
|
1017
1195
|
else:
|
|
1018
1196
|
emitter.line(" 还没用过任何 claw。用 `arkclaw chat ci-xxxx` 开始,之后这里会记住它。")
|
|
@@ -118,6 +118,20 @@ def discover_cli_config(url: str) -> dict[str, object] | None:
|
|
|
118
118
|
if result.get(k):
|
|
119
119
|
out["client_id"] = result[k]
|
|
120
120
|
break
|
|
121
|
+
# The remaining CLI-plane fields (platform-added). Once the BFF surfaces
|
|
122
|
+
# these on GetAuthConfig, the user types ONLY the address — login/init
|
|
123
|
+
# resolve everything. (camelCase from the BFF → snake_case the CLI uses.)
|
|
124
|
+
for src, dst in (
|
|
125
|
+
("roleTrn", "role_trn"),
|
|
126
|
+
("providerTrn", "provider_trn"),
|
|
127
|
+
("region", "region"),
|
|
128
|
+
("spaceId", "space_id"),
|
|
129
|
+
("transport", "transport"),
|
|
130
|
+
("endpoint", "endpoint"),
|
|
131
|
+
("defaultClawId", "clawid"),
|
|
132
|
+
):
|
|
133
|
+
if result.get(src):
|
|
134
|
+
out[dst] = result[src]
|
|
121
135
|
return out or None
|
|
122
136
|
|
|
123
137
|
|
|
@@ -451,9 +451,10 @@ class OpenClawTransport:
|
|
|
451
451
|
# agents.files.get {agentId, name} → {file:{name,path,size,content}}
|
|
452
452
|
# agents.files.set {agentId, name, content} → write
|
|
453
453
|
|
|
454
|
-
async def _request(self, method: str, params: dict[str, Any]) ->
|
|
455
|
-
"""connect (hello-ok) → one control request → its res payload
|
|
456
|
-
are redacted; the token-bearing
|
|
454
|
+
async def _request(self, method: str, params: dict[str, Any]) -> Any:
|
|
455
|
+
"""connect (hello-ok) → one control request → its res payload (dict or
|
|
456
|
+
list, depending on the method). Errors are redacted; the token-bearing
|
|
457
|
+
URL is never echoed."""
|
|
457
458
|
chat_token, endpoint = get_chat_token(self.clawid, self.region, self.creds, identity=self.identity)
|
|
458
459
|
url = (
|
|
459
460
|
f"wss://{endpoint}/?chatToken={urllib.parse.quote(chat_token)}"
|
|
@@ -510,3 +511,31 @@ class OpenClawTransport:
|
|
|
510
511
|
return await self._request(
|
|
511
512
|
"agents.files.set", {"agentId": agent_id, "name": name, "content": content}
|
|
512
513
|
)
|
|
514
|
+
|
|
515
|
+
async def list_agents(self) -> list[dict[str, Any]]:
|
|
516
|
+
"""The agents living in this claw (ws RPC ``arkclaw.team.agent.list``).
|
|
517
|
+
Each item: ``{agentId, name, description?, default?}``. Raises
|
|
518
|
+
ClawAccessDeniedError (via the ChatToken mint) if the user can't access
|
|
519
|
+
this claw."""
|
|
520
|
+
payload = await self._request("arkclaw.team.agent.list", {})
|
|
521
|
+
if isinstance(payload, list):
|
|
522
|
+
return [a for a in payload if isinstance(a, dict)]
|
|
523
|
+
if isinstance(payload, dict): # some gateways wrap the array
|
|
524
|
+
for k in ("data", "agents", "list", "items"):
|
|
525
|
+
v = payload.get(k)
|
|
526
|
+
if isinstance(v, list):
|
|
527
|
+
return [a for a in v if isinstance(a, dict)]
|
|
528
|
+
return []
|
|
529
|
+
|
|
530
|
+
async def list_sessions(self, agent_id: str = "main") -> list[dict[str, Any]]:
|
|
531
|
+
"""An agent's conversations (ws RPC ``sessions.list``). Each item:
|
|
532
|
+
``{key, sessionId, derivedTitle?, updatedAt, status, ...}`` — ``key`` is
|
|
533
|
+
the sessionKey to resume with."""
|
|
534
|
+
payload = await self._request(
|
|
535
|
+
"sessions.list", {"agentId": agent_id, "includeDerivedTitles": True}
|
|
536
|
+
)
|
|
537
|
+
if isinstance(payload, dict):
|
|
538
|
+
sessions = payload.get("sessions")
|
|
539
|
+
if isinstance(sessions, list):
|
|
540
|
+
return [s for s in sessions if isinstance(s, dict)]
|
|
541
|
+
return []
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|