capability-runtime 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. capability_runtime/__init__.py +90 -0
  2. capability_runtime/adapters/__init__.py +13 -0
  3. capability_runtime/adapters/agent_adapter.py +439 -0
  4. capability_runtime/adapters/agently_backend.py +423 -0
  5. capability_runtime/adapters/triggerflow_workflow_engine.py +865 -0
  6. capability_runtime/adapters/workflow_engine.py +43 -0
  7. capability_runtime/config.py +172 -0
  8. capability_runtime/errors.py +20 -0
  9. capability_runtime/guards.py +150 -0
  10. capability_runtime/host_protocol.py +400 -0
  11. capability_runtime/host_toolkit/__init__.py +55 -0
  12. capability_runtime/host_toolkit/approvals_profiles.py +94 -0
  13. capability_runtime/host_toolkit/evidence_hooks.py +65 -0
  14. capability_runtime/host_toolkit/history.py +74 -0
  15. capability_runtime/host_toolkit/invoke_capability.py +409 -0
  16. capability_runtime/host_toolkit/resume.py +317 -0
  17. capability_runtime/host_toolkit/system_prompt.py +132 -0
  18. capability_runtime/host_toolkit/turn_delta.py +128 -0
  19. capability_runtime/logging_utils.py +94 -0
  20. capability_runtime/manifest.py +173 -0
  21. capability_runtime/output_validator.py +139 -0
  22. capability_runtime/protocol/__init__.py +43 -0
  23. capability_runtime/protocol/agent.py +62 -0
  24. capability_runtime/protocol/capability.py +98 -0
  25. capability_runtime/protocol/chat_backend.py +38 -0
  26. capability_runtime/protocol/context.py +244 -0
  27. capability_runtime/protocol/workflow.py +119 -0
  28. capability_runtime/registry.py +287 -0
  29. capability_runtime/reporting/__init__.py +2 -0
  30. capability_runtime/reporting/node_report.py +497 -0
  31. capability_runtime/runtime.py +930 -0
  32. capability_runtime/runtime_ui_events_mixin.py +310 -0
  33. capability_runtime/sdk_lifecycle.py +982 -0
  34. capability_runtime/service_facade.py +418 -0
  35. capability_runtime/services.py +181 -0
  36. capability_runtime/structured_output.py +208 -0
  37. capability_runtime/structured_stream.py +38 -0
  38. capability_runtime/types.py +103 -0
  39. capability_runtime/ui_events/__init__.py +19 -0
  40. capability_runtime/ui_events/projector.py +617 -0
  41. capability_runtime/ui_events/session.py +292 -0
  42. capability_runtime/ui_events/store.py +127 -0
  43. capability_runtime/ui_events/transport.py +33 -0
  44. capability_runtime/ui_events/v1.py +76 -0
  45. capability_runtime/upstream_compat.py +182 -0
  46. capability_runtime/utils/__init__.py +1 -0
  47. capability_runtime/utils/usage.py +65 -0
  48. capability_runtime/workflow_runtime.py +218 -0
  49. capability_runtime-0.1.0.dist-info/METADATA +232 -0
  50. capability_runtime-0.1.0.dist-info/RECORD +52 -0
  51. capability_runtime-0.1.0.dist-info/WHEEL +5 -0
  52. capability_runtime-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,90 @@
1
+ """capability-runtime:统一 Runtime 入口(能力协议 + 执行 + 报告)。"""
2
+ from __future__ import annotations
3
+
4
+ __version__ = "0.1.0"
5
+
6
+ # === 统一入口 ===
7
+ from .config import CustomTool, RuntimeConfig
8
+ from .runtime import Runtime
9
+ from .service_facade import RuntimeServiceFacade, RuntimeServiceHandle, RuntimeServiceRequest, RuntimeSession
10
+ from .structured_stream import StructuredStreamEvent
11
+
12
+ # === 报告类型 ===
13
+ from .types import NodeReport, NodeResult
14
+
15
+ # === Host toolkit(精选公共导出)===
16
+ from .host_toolkit import InvokeCapabilityAllowlist, make_invoke_capability_tool
17
+ from .host_protocol import ApprovalTicket, HostRunSnapshot, HostRunStatus, ResumeIntent
18
+ from .manifest import CapabilityDescriptor, CapabilityManifestEntry, CapabilityVisibility
19
+
20
+ # === Protocol 导出 ===
21
+ from .protocol.agent import AgentIOSchema, AgentSpec
22
+ from .protocol.capability import (
23
+ CapabilityKind,
24
+ CapabilityRef,
25
+ CapabilityResult,
26
+ CapabilitySpec,
27
+ CapabilityStatus,
28
+ )
29
+ from .protocol.context import ExecutionContext
30
+ from .protocol.workflow import (
31
+ ConditionalStep,
32
+ InputMapping,
33
+ LoopStep,
34
+ ParallelStep,
35
+ Step,
36
+ WorkflowSpec,
37
+ )
38
+ from .services import RuntimeServices
39
+ from .workflow_runtime import WorkflowReplayRequest, WorkflowRunSnapshot, WorkflowRunStatus, WorkflowStepSnapshot
40
+
41
+ # === 错误导出 ===
42
+ from .errors import CapabilityNotFoundError, RuntimeFrameworkError
43
+
44
+ __all__ = [
45
+ # Runtime
46
+ "Runtime",
47
+ "RuntimeConfig",
48
+ "CustomTool",
49
+ "StructuredStreamEvent",
50
+ # Reports
51
+ "NodeReport",
52
+ "NodeResult",
53
+ # Host toolkit (selected)
54
+ "InvokeCapabilityAllowlist",
55
+ "make_invoke_capability_tool",
56
+ # Protocol
57
+ "CapabilityKind",
58
+ "CapabilityRef",
59
+ "CapabilitySpec",
60
+ "CapabilityStatus",
61
+ "CapabilityResult",
62
+ "CapabilityManifestEntry",
63
+ "CapabilityDescriptor",
64
+ "CapabilityVisibility",
65
+ "HostRunStatus",
66
+ "ApprovalTicket",
67
+ "ResumeIntent",
68
+ "HostRunSnapshot",
69
+ "WorkflowRunStatus",
70
+ "WorkflowStepSnapshot",
71
+ "WorkflowRunSnapshot",
72
+ "WorkflowReplayRequest",
73
+ "RuntimeSession",
74
+ "RuntimeServiceRequest",
75
+ "RuntimeServiceHandle",
76
+ "RuntimeServiceFacade",
77
+ "AgentSpec",
78
+ "AgentIOSchema",
79
+ "WorkflowSpec",
80
+ "Step",
81
+ "LoopStep",
82
+ "ParallelStep",
83
+ "ConditionalStep",
84
+ "InputMapping",
85
+ "ExecutionContext",
86
+ "RuntimeServices",
87
+ # Errors
88
+ "RuntimeFrameworkError",
89
+ "CapabilityNotFoundError",
90
+ ]
@@ -0,0 +1,13 @@
1
+ """Adapters:桥接上游与能力组织层。"""
2
+ from __future__ import annotations
3
+
4
+ # 已有桥接适配器(不修改)
5
+ # from .agently_backend import AgentlyChatBackend
6
+ # from .triggerflow_tool import ...
7
+
8
+ # 新增能力适配器
9
+ from .agent_adapter import AgentAdapter
10
+
11
+ __all__ = [
12
+ "AgentAdapter",
13
+ ]
@@ -0,0 +1,439 @@
1
+ """
2
+ AgentAdapter:AgentSpec 的执行适配器(统一 mock/bridge/sdk_native)。
3
+
4
+ 说明:
5
+ - 本仓不实现 skills 引擎;skills 的注入与执行由上游 `skills_runtime` 完成;
6
+ - 本适配器负责把 AgentSpec + input 翻译成 SDK Agent 的 task 文本,并驱动事件流执行;
7
+ - `Runtime.run_stream()` 的事件转发语义依赖本适配器的流式执行能力。
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import asyncio
13
+ import inspect
14
+ import json
15
+ from typing import Any, AsyncIterator, Dict, List, Optional, Union
16
+
17
+ from skills_runtime.core.contracts import AgentEvent
18
+ from skills_runtime.core.errors import FrameworkIssue
19
+
20
+ from ..logging_utils import log_suppressed_exception
21
+ from ..protocol.agent import AgentSpec
22
+ from ..protocol.capability import CapabilityResult, CapabilityStatus
23
+ from ..protocol.context import ExecutionContext
24
+ from ..reporting.node_report import NodeReportBuilder
25
+ from ..services import RuntimeServices, map_node_status
26
+
27
+ # _build_task 使用的 prompt section 常量
28
+ _SECTION_SYSTEM = "## 系统指令"
29
+ _SECTION_TASK = "## 任务"
30
+ _SECTION_INPUT = "## 输入"
31
+ _SECTION_OUTPUT_PREFIX = "## 输出要求"
32
+ _SECTION_SKILLS = "## 使用以下 Skills"
33
+
34
+
35
+ class AgentAdapter:
36
+ """
37
+ Agent 适配器(Runtime 内部组件)。
38
+
39
+ 参数:
40
+ - services:RuntimeServices(供 adapter 调用的内部服务协议)
41
+ """
42
+
43
+ def __init__(self, *, services: RuntimeServices) -> None:
44
+ self._services = services
45
+
46
+ async def execute_stream(
47
+ self,
48
+ *,
49
+ spec: AgentSpec,
50
+ input: Dict[str, Any],
51
+ context: ExecutionContext,
52
+ ) -> AsyncIterator[Union[AgentEvent, CapabilityResult]]:
53
+ """
54
+ 流式执行 AgentSpec:先 yield AgentEvent(若为真实执行),最后 yield CapabilityResult。
55
+
56
+ mock 模式:
57
+ - 不产出中间事件(只产出最终 CapabilityResult)
58
+
59
+ bridge/sdk_native 模式:
60
+ - 转发上游 `skills_runtime` 的 `AgentEvent`(事实事件流);
61
+ - 基于事件流聚合 `NodeReport` 并写入 `CapabilityResult.node_report`(其中 `events_path` 等字段为证据指针,真相源仍为 WAL/events)。
62
+
63
+ 备注:
64
+ - 每条 `AgentEvent` 也会通过 Runtime 的内部 tap 旁路分发(用于 UI events 投影;不改变对外 `AgentEvent` 转发语义)。
65
+ """
66
+
67
+ mode = str(getattr(self._services.config, "mode", "mock"))
68
+ if mode == "mock":
69
+ yield await self._mock_execute(spec=spec, input=input, context=context)
70
+ return
71
+
72
+ async for item in self._bridge_execute_stream(spec=spec, input=input, context=context):
73
+ yield item
74
+
75
+ async def _mock_execute(self, *, spec: AgentSpec, input: Dict[str, Any], context: ExecutionContext) -> CapabilityResult:
76
+ """
77
+ mock 执行(离线回归)。
78
+
79
+ 约束:
80
+ - handler 可返回 Any 或 CapabilityResult;
81
+ - handler 支持同步或 async;
82
+ - 异常将转为 FAILED(避免 silent success)。
83
+ """
84
+
85
+ handler = getattr(self._services.config, "mock_handler", None)
86
+ if handler is None:
87
+ return CapabilityResult(
88
+ status=CapabilityStatus.SUCCESS,
89
+ output={"mock": True, "id": spec.base.id, "input_keys": list(input.keys())},
90
+ )
91
+
92
+ try:
93
+ # 用 inspect.signature 探测参数数量,避免 try/except TypeError 吞掉 handler 内部的 TypeError
94
+ sig = inspect.signature(handler)
95
+ params = sig.parameters
96
+ has_var_positional = any(p.kind == inspect.Parameter.VAR_POSITIONAL for p in params.values())
97
+ positional_params = [
98
+ p for p in params.values()
99
+ if p.kind in (inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD)
100
+ ]
101
+
102
+ # 决定传入参数数量:
103
+ # - VAR_POSITIONAL:可接受任意数量,传入全部 3 个
104
+ # - 否则:按位置参数数量切片
105
+ all_args = (spec, input, context)
106
+ if has_var_positional:
107
+ call_args = all_args
108
+ elif len(positional_params) >= 3:
109
+ call_args = all_args
110
+ elif len(positional_params) == 2:
111
+ call_args = (spec, input)
112
+ elif len(positional_params) == 1:
113
+ call_args = (spec,)
114
+ else:
115
+ call_args = ()
116
+
117
+ is_async_handler = inspect.iscoroutinefunction(handler) or inspect.iscoroutinefunction(
118
+ getattr(handler, "__call__", None)
119
+ )
120
+ if is_async_handler:
121
+ out = handler(*call_args)
122
+ else:
123
+ # 同步 mock_handler 放到 worker thread 执行,避免阻塞当前事件循环,
124
+ # 使 workflow step/loop timeout 等上层护栏仍然有效。
125
+ out = await asyncio.to_thread(handler, *call_args)
126
+
127
+ if inspect.isawaitable(out):
128
+ out = await out
129
+
130
+ if isinstance(out, CapabilityResult):
131
+ return out
132
+ return CapabilityResult(status=CapabilityStatus.SUCCESS, output=out)
133
+ except asyncio.CancelledError:
134
+ report = self._services.build_fail_closed_report(
135
+ run_id=context.run_id,
136
+ status="incomplete",
137
+ reason="cancelled",
138
+ completion_reason="run_cancelled",
139
+ meta={"capability_id": spec.base.id, "source": "mock_handler"},
140
+ )
141
+ return CapabilityResult(
142
+ status=CapabilityStatus.CANCELLED,
143
+ error="execution cancelled",
144
+ error_code="RUN_CANCELLED",
145
+ report=report,
146
+ node_report=report,
147
+ )
148
+ except Exception as exc:
149
+ report = self._services.build_fail_closed_report(
150
+ run_id=context.run_id,
151
+ status="failed",
152
+ reason="mock_handler_error",
153
+ completion_reason="mock_handler_error",
154
+ meta={"capability_id": spec.base.id, "exception_type": type(exc).__name__},
155
+ )
156
+ return CapabilityResult(
157
+ status=CapabilityStatus.FAILED,
158
+ error=f"mock_handler error: {exc}",
159
+ error_code="MOCK_HANDLER_ERROR",
160
+ report=report,
161
+ node_report=report,
162
+ )
163
+
164
+ async def _bridge_execute_stream(
165
+ self, *, spec: AgentSpec, input: Dict[str, Any], context: ExecutionContext
166
+ ) -> AsyncIterator[Union[AgentEvent, CapabilityResult]]:
167
+ """
168
+ 真实执行(bridge/sdk_native):驱动 SDK Agent.run_stream_async 并聚合 NodeReport。
169
+ """
170
+
171
+ # preflight gate(生产默认 fail-closed)
172
+ issues: List[FrameworkIssue] = []
173
+ if getattr(self._services.config, "preflight_mode", "error") != "off":
174
+ issues = self._services.preflight()
175
+ if issues and getattr(self._services.config, "preflight_mode", "error") == "error":
176
+ report = self._services.build_fail_closed_report(
177
+ run_id=context.run_id,
178
+ status="failed",
179
+ reason="skill_config_error",
180
+ completion_reason="preflight_failed",
181
+ meta={
182
+ "preflight_mode": "error",
183
+ "skill_issue": {
184
+ "code": "SKILL_PREFLIGHT_FAILED",
185
+ "details": {"issues": [self._services.redact_issue(i) for i in issues]},
186
+ },
187
+ },
188
+ )
189
+ yield CapabilityResult(
190
+ status=CapabilityStatus.FAILED,
191
+ error="Skills preflight failed",
192
+ error_code="PREFLIGHT_FAILED",
193
+ report=report,
194
+ node_report=report,
195
+ metadata={"skill_issues": [self._services.redact_issue(i) for i in issues]},
196
+ )
197
+ return
198
+
199
+ task = self._build_task(spec=spec, input=input)
200
+ agent = self._services.create_sdk_agent(llm_config=spec.llm_config)
201
+
202
+ events: List[AgentEvent] = []
203
+ host_meta = self._services.get_host_meta(context=context)
204
+ initial_history = host_meta.get("initial_history") if isinstance(host_meta.get("initial_history"), list) else None
205
+
206
+ try:
207
+ async for ev in agent.run_stream_async(task, run_id=context.run_id, initial_history=initial_history):
208
+ events.append(ev)
209
+ # 内部旁路:UI events v1 投影(不改变对外 AgentEvent 语义)
210
+ try:
211
+ self._services.emit_agent_event_taps(ev=ev, context=context, capability_id=spec.base.id)
212
+ except Exception as tap_exc:
213
+ log_suppressed_exception(
214
+ context="emit_agent_event_taps",
215
+ exc=tap_exc,
216
+ run_id=context.run_id,
217
+ capability_id=spec.base.id,
218
+ extra={"event_type": getattr(ev, "type", None)},
219
+ )
220
+ if getattr(self._services.config, "on_event", None) is not None:
221
+ try:
222
+ self._services.call_callback(
223
+ self._services.config.on_event,
224
+ ev,
225
+ {"run_id": context.run_id, "capability_id": spec.base.id},
226
+ )
227
+ except Exception as cb_exc:
228
+ log_suppressed_exception(
229
+ context="on_event_callback",
230
+ exc=cb_exc,
231
+ run_id=context.run_id,
232
+ capability_id=spec.base.id,
233
+ extra={"event_type": getattr(ev, "type", None)},
234
+ )
235
+ yield ev
236
+ except asyncio.CancelledError:
237
+ report = self._services.build_fail_closed_report(
238
+ run_id=context.run_id,
239
+ status="incomplete",
240
+ reason="cancelled",
241
+ completion_reason="run_cancelled",
242
+ meta={"capability_id": spec.base.id, "source": "sdk_agent"},
243
+ )
244
+ yield CapabilityResult(
245
+ status=CapabilityStatus.CANCELLED,
246
+ error="execution cancelled",
247
+ error_code="RUN_CANCELLED",
248
+ report=report,
249
+ node_report=report,
250
+ )
251
+ return
252
+ except Exception as exc:
253
+ report = self._services.build_fail_closed_report(
254
+ run_id=context.run_id,
255
+ status="failed",
256
+ reason="engine_error",
257
+ completion_reason="engine_exception",
258
+ meta={"engine_exception": type(exc).__name__},
259
+ )
260
+ yield CapabilityResult(
261
+ status=CapabilityStatus.FAILED,
262
+ error=str(exc),
263
+ error_code="ENGINE_ERROR",
264
+ report=report,
265
+ node_report=report,
266
+ )
267
+ return
268
+
269
+ report = NodeReportBuilder().build(events=events)
270
+ if issues and getattr(self._services.config, "preflight_mode", "error") == "warn":
271
+ report.meta["preflight_mode"] = "warn"
272
+ report.meta["preflight_issues"] = [self._services.redact_issue(i) for i in issues]
273
+
274
+ if initial_history is not None:
275
+ report.meta["initial_history_injected"] = True
276
+ session_id = host_meta.get("session_id")
277
+ if isinstance(session_id, str) and session_id:
278
+ report.meta["session_id"] = session_id
279
+ host_turn_id = host_meta.get("host_turn_id")
280
+ if isinstance(host_turn_id, str) and host_turn_id:
281
+ report.meta["host_turn_id"] = host_turn_id
282
+
283
+ final_output = ""
284
+ for ev in events:
285
+ if ev.type == "run_completed":
286
+ final_output = str(ev.payload.get("final_output") or "")
287
+ if ev.type in ("run_failed", "run_cancelled"):
288
+ final_output = str(ev.payload.get("message") or "")
289
+ if ev.type == "run_waiting_human":
290
+ final_output = str(ev.payload.get("message") or "")
291
+
292
+ self._services.apply_output_validation(
293
+ final_output=final_output,
294
+ report=report,
295
+ context={"run_id": context.run_id, "capability_id": spec.base.id, "bag": dict(context.bag)},
296
+ output_schema=spec.output_schema,
297
+ )
298
+
299
+ status = map_node_status(report)
300
+ yield CapabilityResult(
301
+ status=status,
302
+ output=final_output,
303
+ error=report.reason if status == CapabilityStatus.FAILED else None,
304
+ report=report,
305
+ node_report=report,
306
+ artifacts=list(report.artifacts),
307
+ )
308
+
309
+ def _build_task(self, *, spec: AgentSpec, input: Dict[str, Any]) -> str:
310
+ """
311
+ 将 AgentSpec + input 转换为 SDK Agent 的 task 文本(结构化拼接)。
312
+
313
+ 约束:
314
+ - 不做 prompt engineering;
315
+ - 仅做结构化拼接,保证可回归与可诊断。
316
+ """
317
+
318
+ parts: List[str] = []
319
+
320
+ if spec.system_prompt and str(spec.system_prompt).strip():
321
+ parts.append(f"{_SECTION_SYSTEM}\n{str(spec.system_prompt).strip()}")
322
+
323
+ if spec.base.description:
324
+ parts.append(f"{_SECTION_TASK}\n{spec.base.description}")
325
+
326
+ if input:
327
+ lines: List[str] = []
328
+ for k, v in input.items():
329
+ if isinstance(v, str):
330
+ lines.append(f"- {k}: {v}")
331
+ else:
332
+ lines.append(f"- {k}: {json.dumps(v, ensure_ascii=False)}")
333
+ parts.append(f"{_SECTION_INPUT}\n" + "\n".join(lines))
334
+
335
+ if spec.output_schema and spec.output_schema.fields:
336
+ parts.append(self._build_output_contract(spec=spec))
337
+
338
+ # skills mention(可选)
339
+ mentions = self._build_skill_mentions(spec=spec)
340
+ if mentions:
341
+ parts.append(f"{_SECTION_SKILLS}\n" + "\n".join(mentions))
342
+
343
+ if spec.prompt_template:
344
+ parts.append(str(spec.prompt_template))
345
+
346
+ return "\n\n".join(parts)
347
+
348
+ def _build_output_contract(self, *, spec: AgentSpec) -> str:
349
+ """
350
+ 构造结构化输出约束段。
351
+
352
+ 说明:
353
+ - 这里仍属于提示层约束,但要明确“只返回 JSON object”;
354
+ - 真正的收口由 Runtime 的结构化输出校验负责。
355
+ """
356
+
357
+ if spec.output_schema is None:
358
+ return ""
359
+
360
+ lines = [
361
+ _SECTION_OUTPUT_PREFIX,
362
+ "请只返回 JSON object,不要 Markdown code fence,不要解释性前后缀。",
363
+ "字段要求:",
364
+ ]
365
+ lines.extend([f"- {name}: {typ}" for name, typ in spec.output_schema.fields.items()])
366
+
367
+ required = [str(name) for name in list(spec.output_schema.required or []) if str(name)]
368
+ if required:
369
+ lines.append("必填字段:")
370
+ lines.extend([f"- {name}" for name in required])
371
+
372
+ return "\n".join(lines)
373
+
374
+ def _build_skill_mentions(self, *, spec: AgentSpec) -> List[str]:
375
+ """
376
+ 将 spec.skills 转为 SDK 识别的 mention 文本。
377
+
378
+ 优先级:
379
+ 1) skills_mention_map 显式映射(最稳定)
380
+ 2) 从 Runtime bridge 初始化的 skills_config 推断默认 space(尽力而为)
381
+ """
382
+
383
+ skills = list(getattr(spec, "skills", []) or [])
384
+ if not skills:
385
+ return []
386
+
387
+ mention_map: Dict[str, str] = dict(getattr(spec, "skills_mention_map", {}) or {})
388
+
389
+ inferred_prefix: Optional[str] = None
390
+ skills_cfg = getattr(self._services.config, "skills_config", None)
391
+ inferred_prefix = self._infer_space_prefix(skills_cfg)
392
+
393
+ out: List[str] = []
394
+ for name in skills:
395
+ if name in mention_map and str(mention_map[name]).strip():
396
+ out.append(str(mention_map[name]).strip())
397
+ continue
398
+ if inferred_prefix:
399
+ out.append(f"${inferred_prefix}.{name}")
400
+ return out
401
+
402
+ def _infer_space_prefix(self, skills_cfg: Any) -> Optional[str]:
403
+ """
404
+ 从 skills_config 推断 strict mention 的 space 前缀(版本感知)。
405
+
406
+ 说明:
407
+ - 该逻辑是 best-effort:skills_config 的具体形态由上游决定(dict / pydantic model / dataclass)。
408
+ - 无法推断时返回 None(调用方将不输出 mention)。
409
+
410
+ 返回值形态:
411
+ - legacy:`[account:domain]`
412
+ - v0.1.5+:`[namespace]`(namespace 允许 `a:b:c` 多段)
413
+ """
414
+
415
+ spaces = None
416
+ if isinstance(skills_cfg, dict):
417
+ spaces = skills_cfg.get("spaces")
418
+ else:
419
+ spaces = getattr(skills_cfg, "spaces", None)
420
+
421
+ if not isinstance(spaces, list):
422
+ return None
423
+
424
+ def _get(obj: Any, key: str) -> Optional[str]:
425
+ if isinstance(obj, dict):
426
+ v = obj.get(key)
427
+ else:
428
+ v = getattr(obj, key, None)
429
+ return str(v).strip() if isinstance(v, str) and str(v).strip() else None
430
+
431
+ for sp in spaces:
432
+ namespace = _get(sp, "namespace")
433
+ if namespace:
434
+ return f"[{namespace}]"
435
+ account = _get(sp, "account")
436
+ domain = _get(sp, "domain")
437
+ if account and domain:
438
+ return f"[{account}:{domain}]"
439
+ return None