已合并
fix:manager和runtime的表字段对齐 #534
fix:manager和runtime的表字段对齐 #534
已合并
王欣创建于 24 天前
共 65 个文件变更+1510-1519
@@ -76,7 +76,7 @@ uv run --no-sync python scripts/load_test.py --base-url http://127.0.0.1:30091/a
76## 环境76## 环境
77 77 
78- Python 3.11–3.13;Redis 须开 AOF/RDB(单实例/哨兵 `redis://`,**Redis Cluster 用 `redis+cluster://`**,键前缀已 hash tag 同槽化,见 `docs/feature/2026-08-redis-cluster.md`;cluster 无库号,URL 禁带 `/N`);DB 用 MySQL/PostgreSQL(禁 SQLite 回退——server 模式;local 模式调试可用)。78- Python 3.11–3.13;Redis 须开 AOF/RDB(单实例/哨兵 `redis://`,**Redis Cluster 用 `redis+cluster://`**,键前缀已 hash tag 同槽化,见 `docs/feature/2026-08-redis-cluster.md`;cluster 无库号,URL 禁带 `/N`);DB 用 MySQL/PostgreSQL(禁 SQLite 回退——server 模式;local 模式调试可用)。
79-- **存量库升级:历次 schema 变更须先按 `docs/feature/` 对应篇目手工 ALTER 再发版**(框架建表只 create_all 不补列)——容器表拆分三列(`2026-08-container-table-split.md`)、routing_scope `enabled`/`expires_at`(`2026-09-routing-scope-enabled-expires.md`)、模板表策略四列 RENAME(`2026-09-template-table-runtime-terms.md`);具体 SQL 见各篇。79+- **存量库升级:历次 schema 变更须先按 `docs/feature/` 对应篇目手工 ALTER 再发版**(框架建表只 create_all 不补列/不 DROP)——容器表拆分三列(`2026-08-container-table-split.md`)、routing_scope `enabled`/`expires_at`(`2026-09-routing-scope-enabled-expires.md`)、模板表策略四列 RENAME(`2026-09-template-table-runtime-terms.md`)、**legacy 内联容器列 DROP**(`2026-09-drop-legacy-inline-template-columns.md`)、**data 三元组对齐**(`2026-09-data-triad-alignment.md`);具体 SQL 见各篇。
80- 框架:`service/openjiuwen_runtime/service`(App/Envelope/SystemContext);App 范式参考 `applications/echo/echo_server.py`(**不是** a2a_service)。80- 框架:`service/openjiuwen_runtime/service`(App/Envelope/SystemContext);App 范式参考 `applications/echo/echo_server.py`(**不是** a2a_service)。
81- 部署:`applications/agent_runtime/scripts/deploy.sh local|server`(server 读 `.env.production.local`)。81- 部署:`applications/agent_runtime/scripts/deploy.sh local|server`(server 读 `.env.production.local`)。
82- 本仓库是嵌套 git 仓库(独立于外层 jiuwenclaw),提交分开做。82- 本仓库是嵌套 git 仓库(独立于外层 jiuwenclaw),提交分开做。
Aapplications/agent_runtime/docs/feature/2026-09-data-triad-alignment.md+53-0文件内容审核中,请稍后刷新重试
Mapplications/agent_runtime/docs/feature/README.md+2-0文件内容审核中,请稍后刷新重试
Mapplications/agent_runtime/docs/spec/session-manager.md+2-2文件内容审核中,请稍后刷新重试
Mapplications/agent_runtime/src/agent_runtime/session_manager/config_store.py+7-35文件内容审核中,请稍后刷新重试
@@ -76,6 +76,7 @@ SERVICE_CONFIG_CONTAINER_TABLE_DEF = TableDefinition(
76 ColumnDefinition("command", "json", nullable=True),76 ColumnDefinition("command", "json", nullable=True),
77 ColumnDefinition("args", "json", nullable=True),77 ColumnDefinition("args", "json", nullable=True),
78 ColumnDefinition("readiness_probe", "json", nullable=True),78 ColumnDefinition("readiness_probe", "json", nullable=True),
79+ ColumnDefinition("data", "json", nullable=True),
79 ColumnDefinition("created_at", "datetime", nullable=False),80 ColumnDefinition("created_at", "datetime", nullable=False),
80 ColumnDefinition("updated_at", "datetime", nullable=False),81 ColumnDefinition("updated_at", "datetime", nullable=False),
81 ],82 ],
@@ -783,6 +784,8 @@ def container_row_from_spec(spec: dict[str, Any]) -> dict[str, Any]:
783 }784 }
784 for column in _CONTAINER_SECTION_COLUMNS:785 for column in _CONTAINER_SECTION_COLUMNS:
785 row[column] = spec.get(column)786 row[column] = spec.get(column)
787+ data = spec.get("data")
788+ row["data"] = data if isinstance(data, dict) else None
786 return row789 return row
787 790 
788 791 
Mapplications/agent_runtime/src/agent_runtime/session_manager/routing.py+15-0文件内容审核中,请稍后刷新重试
Mapplications/agent_runtime/tests/session_manager/test_config_store.py+6-11文件内容审核中,请稍后刷新重试
@@ -292,7 +292,7 @@ MANAGER_WEB_GATEWAY_WS_TARGET=http://127.0.0.1:19000
292MANAGER_WEB_RESOLVER=kube-dns.kube-system.svc.cluster.local292MANAGER_WEB_RESOLVER=kube-dns.kube-system.svc.cluster.local
293```293```
294 294 
295-生产 nginx 将 `/chat`、`/ws`、`/gateway-api`、`/file-api`、`/share-api` 按 Cookie `jiuwenclaw_id` 动态反代到各实例 `instance_info.data` 中的 `user_web_host` / `gateway_web_*_host`(空则回退上述 `MANAGER_WEB_*_TARGET`)。`MANAGER_WEB_RESOLVER` 供变量 `proxy_pass` 做 DNS 解析(集群DNS的ClusterIP或完整服务名)。295+生产 nginx 将 `/chat`、`/ws`、`/gateway-api`、`/file-api`、`/share-api` 按 Cookie `jiuwenclaw_id` 动态反代到各实例 `instance_info.user_web_host` 与 `instance_info.data` 中的 `gateway_web_*_host`(空则回退上述 `MANAGER_WEB_*_TARGET`)。`MANAGER_WEB_RESOLVER` 供变量 `proxy_pass` 做 DNS 解析(集群DNS的ClusterIP或完整服务名)。
296 296 
297完整变量列表见 `applications/manager/.env.example`。297完整变量列表见 `applications/manager/.env.example`。
298---298---
@@ -97,15 +97,15 @@ async def check_config_host_alive(
97 97 
98async def require_config_hosts_reachable(98async def require_config_hosts_reachable(
99 *,99 *,
100- gateway_config_host: str | None = None,100+ gateway_host: str | None = None,
101- runtime_config_host: str | None = None,101+ runtime_host: str | None = None,
102 user_web_host: str | None = None,102 user_web_host: str | None = None,
103 timeout: float = _DEFAULT_TIMEOUT,103 timeout: float = _DEFAULT_TIMEOUT,
104) -> None:104) -> None:
105 """对非空的 Gateway / Runtime / User Web host 依次探活;任一失败即抛错。"""105 """对非空的 Gateway / Runtime / User Web host 依次探活;任一失败即抛错。"""
106- if gateway_config_host:106+ if gateway_host:
107- await probe_config_host(gateway_config_host, side="gateway", timeout=timeout)107+ await probe_config_host(gateway_host, side="gateway", timeout=timeout)
108- if runtime_config_host:108+ if runtime_host:
109- await probe_config_host(runtime_config_host, side="runtime", timeout=timeout)109+ await probe_config_host(runtime_host, side="runtime", timeout=timeout)
110 if user_web_host:110 if user_web_host:
111 await probe_config_host(user_web_host, side="user_web", timeout=timeout)111 await probe_config_host(user_web_host, side="user_web", timeout=timeout)
@@ -218,7 +218,7 @@ async def purge_manager_instance_data(
218 218 
219 219 
220async def _purge_gateway_via_http(jiuwenclaw_id: str) -> bool:220async def _purge_gateway_via_http(jiuwenclaw_id: str) -> bool:
221- """有 gateway_config_host 则 HTTP purge;否则返回 False(不抛错)。"""221+ """有 gateway_host 则 HTTP purge;否则返回 False(不抛错)。"""
222 jid = str(jiuwenclaw_id or "").strip()222 jid = str(jiuwenclaw_id or "").strip()
223 if not jid:223 if not jid:
224 return False224 return False
@@ -263,7 +263,7 @@ async def purge_runtime_instance_data(
263 263 
264 Runtime 只有全量快照替换,无按实例 purge API;空投影264 Runtime 只有全量快照替换,无按实例 purge API;空投影
265 ``{containers:[], templates:[], scopes:[]}`` 即删除该实例此前下发的配置。265 ``{containers:[], templates:[], scopes:[]}`` 即删除该实例此前下发的配置。
266- 无 ``runtime_config_host``(且无全局回退)或推送失败时跳过(不抛错)。266+ 无 ``runtime_host``(且无全局回退)或推送失败时跳过(不抛错)。
267 """267 """
268 from manager_server.core.instance_resource.runtime_config_sync import (268 from manager_server.core.instance_resource.runtime_config_sync import (
269 sync_runtime_config,269 sync_runtime_config,
@@ -26,14 +26,16 @@ logger = logging.getLogger(__name__)
26 26 
27_INSTANCE_TABLE = INSTANCE_INFO_TABLE_DEF.table_name27_INSTANCE_TABLE = INSTANCE_INFO_TABLE_DEF.table_name
28_LOG_MASKING_SEEDED_KEY = "log_masking_seeded"28_LOG_MASKING_SEEDED_KEY = "log_masking_seeded"
29-# 用户面反代上游存在 instance_info.data 中(非独立列)29+# 仍留在 instance_info.data 中的用户面反代上游(user_web_* 已独立成列)
30_USER_FACE_HOST_KEYS = (30_USER_FACE_HOST_KEYS = (
31- "user_web_host",
32 "gateway_web_http_host",31 "gateway_web_http_host",
33 "gateway_web_ws_host",32 "gateway_web_ws_host",
34)33)
35-_USER_WEB_STATUS_KEY = "user_web_status"34+_PROMOTED_USER_WEB_KEYS = (
36-_USER_WEB_LAST_ALIVE_KEY = "user_web_last_alive"35+ "user_web_host",
36+ "user_web_status",
37+ "user_web_last_alive",
38+)
37 39 
38ServiceSide = Literal["gateway", "runtime", "user_web"]40ServiceSide = Literal["gateway", "runtime", "user_web"]
39 41 
@@ -44,13 +46,10 @@ _DB_INSTANCE_SORT_FIELDS = frozenset({
44 "gateway_last_alive",46 "gateway_last_alive",
45 "runtime_status",47 "runtime_status",
46 "runtime_last_alive",48 "runtime_last_alive",
47- "updated_at",
48-})
49-_MEMORY_INSTANCE_SORT_FIELDS = frozenset({
50 "user_web_status",49 "user_web_status",
51 "user_web_last_alive",50 "user_web_last_alive",
51+ "updated_at",
52})52})
53-_ALLOWED_INSTANCE_SORT_FIELDS = _DB_INSTANCE_SORT_FIELDS | _MEMORY_INSTANCE_SORT_FIELDS
54_DEFAULT_INSTANCE_ORDER_BY: list[tuple[str, bool]] = [("updated_at", True)]53_DEFAULT_INSTANCE_ORDER_BY: list[tuple[str, bool]] = [("updated_at", True)]
55 54 
56_MAX_JIUWENCLAW_ID_ATTEMPTS = 1055_MAX_JIUWENCLAW_ID_ATTEMPTS = 10
@@ -200,16 +199,12 @@ def _matches_instance_search(row: Any, query: str) -> bool:
200 str(getattr(row, "jiuwenclaw_name", "") or ""),199 str(getattr(row, "jiuwenclaw_name", "") or ""),
201 str(getattr(row, "gateway_status", "") or ""),200 str(getattr(row, "gateway_status", "") or ""),
202 str(getattr(row, "runtime_status", "") or ""),201 str(getattr(row, "runtime_status", "") or ""),
203- str(_user_web_status_from_data(_instance_data_dict(row)) or ""),202+ str(getattr(row, "user_web_status", "") or ""),
204 str(getattr(row, "namespace", "") or ""),203 str(getattr(row, "namespace", "") or ""),
205 ]204 ]
206 return any(needle in field.lower() for field in fields)205 return any(needle in field.lower() for field in fields)
207 206 
208 207 
209-def _matches_user_web_status(row: Any, status: str) -> bool:
210- return _user_web_status_from_data(_instance_data_dict(row)) == status
211- 
212- 
213def _instance_row_to_summary(row: Any) -> dict:208def _instance_row_to_summary(row: Any) -> dict:
214 data = _instance_data_dict(row)209 data = _instance_data_dict(row)
215 face = _user_face_hosts_from_data(data)210 face = _user_face_hosts_from_data(data)
@@ -218,19 +213,22 @@ def _instance_row_to_summary(row: Any) -> dict:
218 jiuwenclaw_name=row.jiuwenclaw_name,213 jiuwenclaw_name=row.jiuwenclaw_name,
219 namespace=getattr(row, "namespace", None) or "default",214 namespace=getattr(row, "namespace", None) or "default",
220 space_id=row.space_id,215 space_id=row.space_id,
221- gateway_config_host=str(getattr(row, "gateway_config_host", "") or ""),216+ gateway_host=str(getattr(row, "gateway_host", "") or ""),
222 gateway_status=row.gateway_status,217 gateway_status=row.gateway_status,
223 gateway_last_alive=iso_datetime(218 gateway_last_alive=iso_datetime(
224 getattr(row, "gateway_last_alive", None)219 getattr(row, "gateway_last_alive", None)
225 ),220 ),
226- runtime_config_host=str(getattr(row, "runtime_config_host", "") or ""),221+ runtime_host=str(getattr(row, "runtime_host", "") or ""),
227 runtime_status=row.runtime_status,222 runtime_status=row.runtime_status,
228 runtime_last_alive=iso_datetime(223 runtime_last_alive=iso_datetime(
229 getattr(row, "runtime_last_alive", None)224 getattr(row, "runtime_last_alive", None)
230 ),225 ),
231- user_web_status=_user_web_status_from_data(data),226+ user_web_host=_norm_host(getattr(row, "user_web_host", None)),
232- user_web_last_alive=_user_web_last_alive_from_data(data),227+ user_web_status=str(getattr(row, "user_web_status", None) or "pending").strip()
233- user_web_host=face["user_web_host"],228+ or "pending",
229+ user_web_last_alive=iso_datetime(
230+ getattr(row, "user_web_last_alive", None)
231+ ),
234 gateway_web_http_host=face["gateway_web_http_host"],232 gateway_web_http_host=face["gateway_web_http_host"],
235 gateway_web_ws_host=face["gateway_web_ws_host"],233 gateway_web_ws_host=face["gateway_web_ws_host"],
236 created_at=iso_datetime(row.created_at),234 created_at=iso_datetime(row.created_at),
@@ -254,19 +252,12 @@ def _instance_data_dict(row: Any | None) -> dict[str, Any]:
254 return dict(data) if isinstance(data, dict) else {}252 return dict(data) if isinstance(data, dict) else {}
255 253 
256 254 
257-def _user_web_status_from_data(data: dict[str, Any] | None) -> str:255+def _strip_promoted_user_web_keys(data: dict[str, Any]) -> dict[str, Any]:
258- src = data if isinstance(data, dict) else {}256+ """避免已提列字段继续滞留在 data JSON。"""
259- return str(src.get(_USER_WEB_STATUS_KEY) or "pending").strip() or "pending"257+ cleaned = dict(data)
260- 258+ for key in _PROMOTED_USER_WEB_KEYS:
261- 259+ cleaned.pop(key, None)
262-def _user_web_last_alive_from_data(data: dict[str, Any] | None) -> str | None:260+ return cleaned
263- src = data if isinstance(data, dict) else {}
264- raw = src.get(_USER_WEB_LAST_ALIVE_KEY)
265- if raw is None or raw == "":
266- return None
267- if isinstance(raw, str):
268- return raw.strip() or None
269- return iso_datetime(raw)
270 261 
271 262 
272def _user_face_hosts_from_data(data: dict[str, Any] | None) -> dict[str, str | None]:263def _user_face_hosts_from_data(data: dict[str, Any] | None) -> dict[str, str | None]:
@@ -278,8 +269,8 @@ def _merge_user_face_hosts_into_data(
278 data: dict[str, Any] | None,269 data: dict[str, Any] | None,
279 hosts: dict[str, Any],270 hosts: dict[str, Any],
280) -> dict[str, Any]:271) -> dict[str, Any]:
281- """把用户面 host 写入 data;空字符串删除对应键。"""272+ """把 gateway_web_* host 写入 data;空字符串删除对应键。"""
282- merged = dict(data or {})273+ merged = _strip_promoted_user_web_keys(dict(data or {}))
283 for key in _USER_FACE_HOST_KEYS:274 for key in _USER_FACE_HOST_KEYS:
284 if key not in hosts:275 if key not in hosts:
285 continue276 continue
@@ -291,24 +282,6 @@ def _merge_user_face_hosts_into_data(
291 return merged282 return merged
292 283 
293 284 
294-def _sort_rows_by_user_web(
295- rows: list[Any],
296- *,
297- sort_by: str,
298- sort_order: str | None,
299-) -> list[Any]:
300- reverse = str(sort_order or "asc").strip().lower() == "desc"
301- 
302- def key(row: Any) -> tuple:
303- data = _instance_data_dict(row)
304- if sort_by == "user_web_status":
305- return (_user_web_status_from_data(data).lower(),)
306- alive = _user_web_last_alive_from_data(data) or ""
307- return (alive,)
308- 
309- return sorted(rows, key=key, reverse=reverse)
310- 
311- 
312async def is_log_masking_seeded(handler: DBHandler, jiuwenclaw_id: str) -> bool:285async def is_log_masking_seeded(handler: DBHandler, jiuwenclaw_id: str) -> bool:
313 """``instance_info.data.log_masking_seeded`` 为真时表示 builtin 种子已执行过。"""286 """``instance_info.data.log_masking_seeded`` 为真时表示 builtin 种子已执行过。"""
314 jid = str(jiuwenclaw_id or "").strip()287 jid = str(jiuwenclaw_id or "").strip()
@@ -328,6 +301,9 @@ async def create_instance_row(handler: DBHandler, row_data: dict[str, Any]) -> A
328 payload.setdefault("gateway_last_alive", None)301 payload.setdefault("gateway_last_alive", None)
329 payload.setdefault("runtime_status", "pending")302 payload.setdefault("runtime_status", "pending")
330 payload.setdefault("runtime_last_alive", None)303 payload.setdefault("runtime_last_alive", None)
304+ payload.setdefault("user_web_host", "")
305+ payload.setdefault("user_web_status", "pending")
306+ payload.setdefault("user_web_last_alive", None)
331 payload.setdefault("space_id", "default")307 payload.setdefault("space_id", "default")
332 return await handler.create(_INSTANCE_TABLE, payload)308 return await handler.create(_INSTANCE_TABLE, payload)
333 309 
@@ -425,7 +401,7 @@ async def maybe_full_sync_gateway_on_online(
425 await require_gateway_endpoint(jid)401 await require_gateway_endpoint(jid)
426 except ValueError:402 except ValueError:
427 logger.info(403 logger.info(
428- "[Instance] skip full sync on online: no gateway_config_host "404+ "[Instance] skip full sync on online: no gateway_host "
429 "jiuwenclaw_id=%s prev_status=%s",405 "jiuwenclaw_id=%s prev_status=%s",
430 jid,406 jid,
431 prev,407 prev,
@@ -511,7 +487,6 @@ async def apply_health_probe_result(
511 - alive:置 online,刷新 last_alive;Gateway / Runtime 从 pending/offline → online487 - alive:置 online,刷新 last_alive;Gateway / Runtime 从 pending/offline → online
512 时各自全量下发(Gateway → WS bootstrap;Runtime → config_sync)488 时各自全量下发(Gateway → WS bootstrap;Runtime → config_sync)
513 - 失败:仅当当前为 online 时置 offline;pending 保持不变489 - 失败:仅当当前为 online 时置 offline;pending 保持不变
514- - user_web:状态写入 ``instance_info.data``(非独立列)
515 """490 """
516 jid = str(jiuwenclaw_id or "").strip()491 jid = str(jiuwenclaw_id or "").strip()
517 if not jid:492 if not jid:
@@ -522,36 +497,6 @@ async def apply_health_probe_result(
522 side = _resolve_service_side(service_type)497 side = _resolve_service_side(service_type)
523 now = utc_now()498 now = utc_now()
524 499 
525- if side == "user_web":
526- data = _instance_data_dict(row)
527- prev_status = _user_web_status_from_data(data)
528- if alive:
529- data[_USER_WEB_STATUS_KEY] = "online"
530- data[_USER_WEB_LAST_ALIVE_KEY] = iso_datetime(now)
531- await handler.update(
532- _INSTANCE_TABLE,
533- {"jiuwenclaw_id": jid},
534- {
535- "data": data,
536- "updated_at": now,
537- "updated_by": "health-probe",
538- },
539- )
540- return True
541- if prev_status == "online":
542- data[_USER_WEB_STATUS_KEY] = "offline"
543- await handler.update(
544- _INSTANCE_TABLE,
545- {"jiuwenclaw_id": jid},
546- {
547- "data": data,
548- "updated_at": now,
549- "updated_by": "health-probe",
550- },
551- )
552- return True
553- return False
554- 
555 status_key = f"{side}_status"500 status_key = f"{side}_status"
556 prev_status = str(getattr(row, status_key, "") or "")501 prev_status = str(getattr(row, status_key, "") or "")
557 502 
@@ -611,6 +556,7 @@ async def list_instance_rows(
611 *,556 *,
612 gateway_status: str | None = None,557 gateway_status: str | None = None,
613 runtime_status: str | None = None,558 runtime_status: str | None = None,
559+ user_web_status: str | None = None,
614 offset: int,560 offset: int,
615 limit: int,561 limit: int,
616 sort_by: str | None = None,562 sort_by: str | None = None,
@@ -621,6 +567,8 @@ async def list_instance_rows(
621 filters["gateway_status"] = gateway_status567 filters["gateway_status"] = gateway_status
622 if runtime_status:568 if runtime_status:
623 filters["runtime_status"] = runtime_status569 filters["runtime_status"] = runtime_status
570+ if user_web_status:
571+ filters["user_web_status"] = user_web_status
624 total = await handler.count_records(_INSTANCE_TABLE, filters)572 total = await handler.count_records(_INSTANCE_TABLE, filters)
625 db_sort = sort_by if sort_by in _DB_INSTANCE_SORT_FIELDS else None573 db_sort = sort_by if sort_by in _DB_INSTANCE_SORT_FIELDS else None
626 order_by = resolve_order_by(574 order_by = resolve_order_by(
@@ -653,8 +601,9 @@ async def merge_instance_data(
653 row = await get_instance_row(handler, jiuwenclaw_id)601 row = await get_instance_row(handler, jiuwenclaw_id)
654 if row is None:602 if row is None:
655 return None603 return None
656- merged = dict(getattr(row, "data", None) or {})604+ merged = _strip_promoted_user_web_keys(dict(getattr(row, "data", None) or {}))
657 merged.update(patch)605 merged.update(patch)
606+ merged = _strip_promoted_user_web_keys(merged)
658 now = utc_now()607 now = utc_now()
659 return await handler.update(608 return await handler.update(
660 _INSTANCE_TABLE,609 _INSTANCE_TABLE,
@@ -669,12 +618,12 @@ class InstanceService:
669 618 
670 async def create(self, body: CreateInstanceBody) -> dict:619 async def create(self, body: CreateInstanceBody) -> dict:
671 jiuwenclaw_id = await generate_unique_jiuwenclaw_id(self._handler)620 jiuwenclaw_id = await generate_unique_jiuwenclaw_id(self._handler)
672- gateway_host = _norm_host(body.gateway_config_host)621+ gateway_host = _norm_host(body.gateway_host)
673- runtime_host = _norm_host(body.runtime_config_host)622+ runtime_host = _norm_host(body.runtime_host)
674 if not gateway_host:623 if not gateway_host:
675- raise ValueError("gateway_config_host is required")624+ raise ValueError("gateway_host is required")
676 if not runtime_host:625 if not runtime_host:
677- raise ValueError("runtime_config_host is required")626+ raise ValueError("runtime_host is required")
678 user_web_host = _norm_host(body.user_web_host)627 user_web_host = _norm_host(body.user_web_host)
679 gateway_web_http_host = _norm_host(body.gateway_web_http_host)628 gateway_web_http_host = _norm_host(body.gateway_web_http_host)
680 if not user_web_host:629 if not user_web_host:
@@ -684,15 +633,19 @@ class InstanceService:
684 await _assert_config_host_available(633 await _assert_config_host_available(
685 self._handler,634 self._handler,
686 gateway_host,635 gateway_host,
687- column="gateway_config_host",636+ column="gateway_host",
688 )637 )
689 await _assert_config_host_available(638 await _assert_config_host_available(
690 self._handler,639 self._handler,
691 runtime_host,640 runtime_host,
692- column="runtime_config_host",641+ column="runtime_host",
642+ )
643+ await _assert_config_host_available(
644+ self._handler,
645+ user_web_host,
646+ column="user_web_host",
693 )647 )
694 face_hosts = {648 face_hosts = {
695- "user_web_host": user_web_host,
696 "gateway_web_http_host": gateway_web_http_host,649 "gateway_web_http_host": gateway_web_http_host,
697 "gateway_web_ws_host": body.gateway_web_ws_host,650 "gateway_web_ws_host": body.gateway_web_ws_host,
698 }651 }
@@ -701,10 +654,9 @@ class InstanceService:
701 dict(body.data) if isinstance(body.data, dict) else {},654 dict(body.data) if isinstance(body.data, dict) else {},
702 face_hosts,655 face_hosts,
703 )656 )
704- data.setdefault(_USER_WEB_STATUS_KEY, "pending")
705 await require_config_hosts_reachable(657 await require_config_hosts_reachable(
706- gateway_config_host=gateway_host,658+ gateway_host=gateway_host,
707- runtime_config_host=runtime_host,659+ runtime_host=runtime_host,
708 user_web_host=user_web_host,660 user_web_host=user_web_host,
709 )661 )
710 namespace = _norm_namespace(body.namespace)662 namespace = _norm_namespace(body.namespace)
@@ -713,10 +665,12 @@ class InstanceService:
713 "jiuwenclaw_name": body.jiuwenclaw_name.strip(),665 "jiuwenclaw_name": body.jiuwenclaw_name.strip(),
714 "description": body.description,666 "description": body.description,
715 "namespace": namespace,667 "namespace": namespace,
716- "gateway_config_host": gateway_host,668+ "gateway_host": gateway_host,
717 "gateway_status": "pending",669 "gateway_status": "pending",
718- "runtime_config_host": runtime_host,670+ "runtime_host": runtime_host,
719 "runtime_status": "pending",671 "runtime_status": "pending",
672+ "user_web_host": user_web_host,
673+ "user_web_status": "pending",
720 "data": data or None,674 "data": data or None,
721 "space_id": (body.space_id or "default").strip() or "default",675 "space_id": (body.space_id or "default").strip() or "default",
722 "created_by": (body.created_by or "system").strip() or "system",676 "created_by": (body.created_by or "system").strip() or "system",
@@ -765,35 +719,33 @@ class InstanceService:
765 return {719 return {
766 "jiuwenclaw_id": jiuwenclaw_id,720 "jiuwenclaw_id": jiuwenclaw_id,
767 "namespace": getattr(row, "namespace", namespace),721 "namespace": getattr(row, "namespace", namespace),
768- "gateway_config_host": getattr(row, "gateway_config_host", gateway_host),722+ "gateway_host": getattr(row, "gateway_host", gateway_host),
769 "gateway_status": getattr(row, "gateway_status", "pending"),723 "gateway_status": getattr(row, "gateway_status", "pending"),
770- "runtime_config_host": getattr(row, "runtime_config_host", runtime_host),724+ "runtime_host": getattr(row, "runtime_host", runtime_host),
771 "runtime_status": getattr(row, "runtime_status", "pending"),725 "runtime_status": getattr(row, "runtime_status", "pending"),
772- "user_web_status": _user_web_status_from_data(_instance_data_dict(row)),726+ "user_web_host": getattr(row, "user_web_host", user_web_host),
727+ "user_web_status": getattr(row, "user_web_status", "pending"),
773 }728 }
774 729 
775 async def list_instances(self, query: InstanceListQuery) -> dict:730 async def list_instances(self, query: InstanceListQuery) -> dict:
776 page = max(query.page, 1)731 page = max(query.page, 1)
777 page_size = min(max(query.page_size, 1), 200)732 page_size = min(max(query.page_size, 1), 200)
778 search_query = (query.search or "").strip()733 search_query = (query.search or "").strip()
779- user_web_filter = str(query.user_web_status or "").strip()734+ user_web_filter = str(query.user_web_status or "").strip() or None
780 sort_by = str(query.sort_by or "").strip() or None735 sort_by = str(query.sort_by or "").strip() or None
781- needs_memory = bool(736+ needs_memory = bool(search_query)
782- search_query
783- or user_web_filter
784- or (sort_by in _MEMORY_INSTANCE_SORT_FIELDS)
785- )
786 737 
787 filters: dict[str, Any] = {}738 filters: dict[str, Any] = {}
788 if query.gateway_status:739 if query.gateway_status:
789 filters["gateway_status"] = query.gateway_status740 filters["gateway_status"] = query.gateway_status
790 if query.runtime_status:741 if query.runtime_status:
791 filters["runtime_status"] = query.runtime_status742 filters["runtime_status"] = query.runtime_status
743+ if user_web_filter:
744+ filters["user_web_status"] = user_web_filter
792 745 
793 if needs_memory:746 if needs_memory:
794- db_sort = sort_by if sort_by in _DB_INSTANCE_SORT_FIELDS else None
795 order_by = resolve_order_by(747 order_by = resolve_order_by(
796- db_sort,748+ sort_by if sort_by in _DB_INSTANCE_SORT_FIELDS else None,
797 query.sort_order,749 query.sort_order,
798 allowed_sort_fields=_DB_INSTANCE_SORT_FIELDS,750 allowed_sort_fields=_DB_INSTANCE_SORT_FIELDS,
799 default_order_by=_DEFAULT_INSTANCE_ORDER_BY,751 default_order_by=_DEFAULT_INSTANCE_ORDER_BY,
@@ -809,12 +761,6 @@ class InstanceService:
809 )761 )
810 if search_query:762 if search_query:
811 rows = [r for r in rows if _matches_instance_search(r, search_query)]763 rows = [r for r in rows if _matches_instance_search(r, search_query)]
812- if user_web_filter:
813- rows = [r for r in rows if _matches_user_web_status(r, user_web_filter)]
814- if sort_by in _MEMORY_INSTANCE_SORT_FIELDS:
815- rows = _sort_rows_by_user_web(
816- rows, sort_by=sort_by, sort_order=query.sort_order
817- )
818 total = len(rows)764 total = len(rows)
819 offset = (page - 1) * page_size765 offset = (page - 1) * page_size
820 page_rows = rows[offset: offset + page_size]766 page_rows = rows[offset: offset + page_size]
@@ -830,6 +776,7 @@ class InstanceService:
830 self._handler,776 self._handler,
831 gateway_status=query.gateway_status,777 gateway_status=query.gateway_status,
832 runtime_status=query.runtime_status,778 runtime_status=query.runtime_status,
779+ user_web_status=user_web_filter,
833 offset=offset,780 offset=offset,
834 limit=page_size,781 limit=page_size,
835 sort_by=sort_by,782 sort_by=sort_by,
@@ -892,18 +839,18 @@ class InstanceService:
892 updates[field] = str(updates[field]).strip()839 updates[field] = str(updates[field]).strip()
893 if "namespace" in updates and updates["namespace"] is not None:840 if "namespace" in updates and updates["namespace"] is not None:
894 updates["namespace"] = _norm_namespace(updates["namespace"])841 updates["namespace"] = _norm_namespace(updates["namespace"])
895- if "gateway_config_host" in updates:842+ if "gateway_host" in updates:
896- updates["gateway_config_host"] = _norm_host(updates["gateway_config_host"])843+ updates["gateway_host"] = _norm_host(updates["gateway_host"])
897- if "runtime_config_host" in updates:844+ if "runtime_host" in updates:
898- updates["runtime_config_host"] = _norm_host(updates["runtime_config_host"])845+ updates["runtime_host"] = _norm_host(updates["runtime_host"])
846+ if "user_web_host" in updates:
847+ updates["user_web_host"] = _norm_host(updates["user_web_host"])
848+ if not updates["user_web_host"]:
849+ raise ValueError("user_web_host is required")
899 850 
900 face_host_updates = {851 face_host_updates = {
901 key: updates.pop(key) for key in _USER_FACE_HOST_KEYS if key in updates852 key: updates.pop(key) for key in _USER_FACE_HOST_KEYS if key in updates
902 }853 }
903- if "user_web_host" in face_host_updates and not _norm_host(
904- face_host_updates.get("user_web_host")
905- ):
906- raise ValueError("user_web_host is required")
907 if "gateway_web_http_host" in face_host_updates and not _norm_host(854 if "gateway_web_http_host" in face_host_updates and not _norm_host(
908 face_host_updates.get("gateway_web_http_host")855 face_host_updates.get("gateway_web_http_host")
909 ):856 ):
@@ -931,49 +878,54 @@ class InstanceService:
931 exclude_jiuwenclaw_id=jid,878 exclude_jiuwenclaw_id=jid,
932 )879 )
933 880 
934- if "gateway_config_host" in updates:881+ if "gateway_host" in updates:
935- gateway_host = updates["gateway_config_host"]882+ gateway_host = updates["gateway_host"]
936 if gateway_host and _norm_host(883 if gateway_host and _norm_host(
937- getattr(row, "gateway_config_host", None)884+ getattr(row, "gateway_host", None)
938 ) != gateway_host:885 ) != gateway_host:
939 await _assert_config_host_available(886 await _assert_config_host_available(
940 self._handler,887 self._handler,
941 gateway_host,888 gateway_host,
942- column="gateway_config_host",889+ column="gateway_host",
943 exclude_jiuwenclaw_id=jid,890 exclude_jiuwenclaw_id=jid,
944 )891 )
945- if "runtime_config_host" in updates:892+ if "runtime_host" in updates:
946- runtime_host = updates["runtime_config_host"]893+ runtime_host = updates["runtime_host"]
947 if runtime_host and _norm_host(894 if runtime_host and _norm_host(
948- getattr(row, "runtime_config_host", None)895+ getattr(row, "runtime_host", None)
949 ) != runtime_host:896 ) != runtime_host:
950 await _assert_config_host_available(897 await _assert_config_host_available(
951 self._handler,898 self._handler,
952 runtime_host,899 runtime_host,
953- column="runtime_config_host",900+ column="runtime_host",
901+ exclude_jiuwenclaw_id=jid,
902+ )
903+ if "user_web_host" in updates:
904+ user_web_host = updates["user_web_host"]
905+ if user_web_host and _norm_host(
906+ getattr(row, "user_web_host", None)
907+ ) != user_web_host:
908+ await _assert_config_host_available(
909+ self._handler,
910+ user_web_host,
911+ column="user_web_host",
954 exclude_jiuwenclaw_id=jid,912 exclude_jiuwenclaw_id=jid,
955 )913 )
956 914 
957- user_web_probe: str | None = None
958- if "user_web_host" in face_host_updates:
959- merged_data = updates.get("data")
960- if isinstance(merged_data, dict):
961- user_web_probe = _norm_host(merged_data.get("user_web_host"))
962- else:
963- user_web_probe = _norm_host(face_host_updates.get("user_web_host"))
964- 
965 await require_config_hosts_reachable(915 await require_config_hosts_reachable(
966- gateway_config_host=(916+ gateway_host=(
967- updates["gateway_config_host"]917+ updates["gateway_host"]
968- if "gateway_config_host" in updates918+ if "gateway_host" in updates
969 else None919 else None
970 ),920 ),
971- runtime_config_host=(921+ runtime_host=(
972- updates["runtime_config_host"]922+ updates["runtime_host"]
973- if "runtime_config_host" in updates923+ if "runtime_host" in updates
974 else None924 else None
975 ),925 ),
976- user_web_host=user_web_probe,926+ user_web_host=(
927+ updates["user_web_host"] if "user_web_host" in updates else None
928+ ),
977 )929 )
978 930 
979 updates["updated_at"] = utc_now()931 updates["updated_at"] = utc_now()
@@ -14,7 +14,7 @@ _HEALTH_PATH = "/healthz"
14 14 
15 15 
16async def fetch_runtime_identity_from_health(16async def fetch_runtime_identity_from_health(
17- runtime_config_host: str,17+ runtime_host: str,
18 *,18 *,
19 timeout: float = _DEFAULT_TIMEOUT,19 timeout: float = _DEFAULT_TIMEOUT,
20) -> dict[str, Any]:20) -> dict[str, Any]:
@@ -22,13 +22,13 @@ async def fetch_runtime_identity_from_health(
22 22 
23 返回 ``{"namespace"}``(若响应体含该字段,否则 ``default``);探活失败抛 ``ValueError``。23 返回 ``{"namespace"}``(若响应体含该字段,否则 ``default``);探活失败抛 ``ValueError``。
24 """24 """
25- base = str(runtime_config_host or "").strip().rstrip("/")25+ base = str(runtime_host or "").strip().rstrip("/")
26 if not base:26 if not base:
27- raise ValueError("runtime_config_host is empty")27+ raise ValueError("runtime_host is empty")
28 link_mtls = ManagerLinkMTLSConfig.from_env()28 link_mtls = ManagerLinkMTLSConfig.from_env()
29 base = link_mtls.resolve_endpoint(base, role="runtime")29 base = link_mtls.resolve_endpoint(base, role="runtime")
30 if not base.startswith(("http://", "https://")):30 if not base.startswith(("http://", "https://")):
31- raise ValueError(f"runtime_config_host must be an http(s) URL, got {runtime_config_host!r}")31+ raise ValueError(f"runtime_host must be an http(s) URL, got {runtime_host!r}")
32 32 
33 url = f"{base}{_HEALTH_PATH}"33 url = f"{base}{_HEALTH_PATH}"
34 try:34 try:
@@ -1,19 +1,30 @@
1# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.1# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.
2-"""Gateway Config Receiver HTTP 客户端。"""2+"""Gateway / Runtime Config 下发 HTTP 客户端。
3+ 
4+``http_request`` 为公用传输层(endpoint + mTLS + httpx);
5+``gateway_request`` / ``runtime_request`` 在其上封装各自响应约定。
6+"""
3 7 
4from __future__ import annotations8from __future__ import annotations
5 9 
6-from typing import Any10+from typing import Any, Literal
7 11 
8import httpx12import httpx
13+from openjiuwen_runtime.foundation.db.handler import DBHandler
9 14 
15+from manager_server.infrastructure.config import settings
10from manager_server.infrastructure.db import get_db_handler16from manager_server.infrastructure.db import get_db_handler
11from manager_server.infrastructure.logger import get_logger17from manager_server.infrastructure.logger import get_logger
12-from manager_server.manager_config_push.endpoint import require_gateway_endpoint18+from manager_server.manager_config_push.endpoint import (
19+ require_gateway_endpoint,
20+ require_runtime_endpoint,
21+)
13from manager_server.security.link_mtls import ManagerLinkMTLSConfig22from manager_server.security.link_mtls import ManagerLinkMTLSConfig
14 23 
15logger = get_logger(__name__)24logger = get_logger(__name__)
16 25 
26+LinkRole = Literal["gateway", "runtime"]
27+ 
17 28 
18def _contains_credential_replace(value: Any) -> bool:29def _contains_credential_replace(value: Any) -> bool:
19 if isinstance(value, dict):30 if isinstance(value, dict):
@@ -25,6 +36,90 @@ def _contains_credential_replace(value: Any) -> bool:
25 return False36 return False
26 37 
27 38 
39+async def http_request(
40+ jiuwenclaw_id: str,
41+ role: LinkRole,
42+ method: str,
43+ path: str,
44+ json_body: dict[str, Any] | None = None,
45+ *,
46+ endpoint: str,
47+ timeout: float = 30.0,
48+ handler: DBHandler | None = None,
49+ network_target: Any | None = None,
50+) -> httpx.Response:
51+ """Manager → Gateway/Runtime 的公用 HTTP 请求。
52+ 
53+ ``endpoint`` 由调用方解析后传入(``gateway_host`` / ``runtime_host``);
54+ 本函数挂 mTLS、发 ``method path``。业务响应解析由
55+ ``gateway_request`` / ``runtime_request`` 各自处理。
56+ """
57+ jid = str(jiuwenclaw_id or "").strip()
58+ if not jid:
59+ raise ValueError("jiuwenclaw_id is required")
60+ if not path.startswith("/"):
61+ raise ValueError(f"path must start with /: {path!r}")
62+ base = str(endpoint or "").strip().rstrip("/")
63+ if not base:
64+ raise ValueError(f"{role} endpoint is required")
65+ 
66+ link_mtls = ManagerLinkMTLSConfig.from_env()
67+ db = handler
68+ if db is None and link_mtls.enforced:
69+ db = get_db_handler()
70+ link_target = await link_mtls.target(
71+ db,
72+ jid,
73+ role=role,
74+ endpoint=base,
75+ )
76+ endpoint = link_target.endpoint
77+ headers = link_target.headers
78+ url = f"{endpoint}{path}"
79+ payload = dict(json_body or {})
80+ 
81+ try:
82+ async with httpx.AsyncClient(
83+ timeout=timeout,
84+ trust_env=False,
85+ **link_target.client_kwargs,
86+ ) as client:
87+ if network_target is None:
88+ resp = await client.request(
89+ method.upper(),
90+ url,
91+ json=payload,
92+ **({"headers": headers} if headers else {}),
93+ )
94+ else:
95+ from manager_server.core.template.a2a_discovery import _pinned_request
96+ 
97+ pinned = _pinned_request(client, url, *network_target)
98+ request = client.build_request(
99+ method.upper(),
100+ pinned.url,
101+ json=payload,
102+ headers={**headers, "Host": pinned.headers["Host"]},
103+ extensions=pinned.extensions,
104+ )
105+ resp = await client.send(request)
106+ except Exception as exc:
107+ raise ValueError(
108+ f"{role} HTTP push failed jiuwenclaw_id={jid!r} url={url}: {exc}"
109+ ) from exc
110+ 
111+ if resp.status_code >= 400:
112+ detail = resp.text[:500]
113+ try:
114+ detail = resp.json().get("detail") or detail
115+ except Exception: # noqa: BLE001, S110
116+ pass
117+ raise ValueError(
118+ f"{role} HTTP push rejected status={resp.status_code} detail={detail!r}"
119+ )
120+ return resp
121+ 
122+ 
28async def gateway_request(123async def gateway_request(
29 jiuwenclaw_id: str,124 jiuwenclaw_id: str,
30 method: str,125 method: str,
@@ -38,23 +133,12 @@ async def gateway_request(
38 ``path`` 为绝对路径,如 ``/api/v1/logging``;``business`` 为业务字段 JSON。133 ``path`` 为绝对路径,如 ``/api/v1/logging``;``business`` 为业务字段 JSON。
39 """134 """
40 jid = str(jiuwenclaw_id or "").strip()135 jid = str(jiuwenclaw_id or "").strip()
41- if not jid:
42- raise ValueError("jiuwenclaw_id is required")
43- if not path.startswith("/"):
44- raise ValueError(f"path must start with /: {path!r}")
45- 
46 endpoint = await require_gateway_endpoint(jid)136 endpoint = await require_gateway_endpoint(jid)
47- link_mtls = ManagerLinkMTLSConfig.from_env()
48- link_target = await link_mtls.target(
49- get_db_handler() if link_mtls.enforced else None,
50- jid,
51- role="gateway",
52- endpoint=endpoint,
53- )
54- endpoint = link_target.endpoint
55 payload = dict(business or {})137 payload = dict(business or {})
56 network_target = None138 network_target = None
57- if path.startswith("/api/v1/a2a-outbound-templates") and _contains_credential_replace(payload):139+ if path.startswith("/api/v1/a2a-outbound-templates") and _contains_credential_replace(
140+ payload
141+ ):
58 from manager_server.core.template.a2a_discovery import _normalize_url, _validate_target142 from manager_server.core.template.a2a_discovery import _normalize_url, _validate_target
59 143 
60 data = payload.get("data") or {}144 data = payload.get("data") or {}
@@ -72,43 +156,16 @@ async def gateway_request(
72 f"A2A credential sync blocked by network access settings: {exc}"156 f"A2A credential sync blocked by network access settings: {exc}"
73 ) from exc157 ) from exc
74 158 
75- headers = link_target.headers159+ resp = await http_request(
76- 160+ jid,
77- url = f"{endpoint}{path}"161+ "gateway",
78- try:162+ method,
79- async with httpx.AsyncClient(163+ path,
80- timeout=timeout,164+ payload,
81- trust_env=False,165+ endpoint=endpoint,
82- **link_target.client_kwargs,166+ timeout=timeout,
83- ) as client:167+ network_target=network_target,
84- if network_target is None:168+ )
85- resp = await client.request(
86- method.upper(), url, json=payload, **({"headers": headers} if headers else {})
87- )
88- else:
89- from manager_server.core.template.a2a_discovery import _pinned_request
90- 
91- pinned = _pinned_request(client, url, *network_target)
92- request = client.build_request(
93- method.upper(),
94- pinned.url,
95- json=payload,
96- headers={**headers, "Host": pinned.headers["Host"]},
97- extensions=pinned.extensions,
98- )
99- resp = await client.send(request)
100- except Exception as exc:
101- raise ValueError(
102- f"gateway HTTP push failed jiuwenclaw_id={jid!r} url={url}: {exc}"
103- ) from exc
104- 
105- if resp.status_code >= 400:
106- detail = resp.text[:500]
107- try:
108- detail = resp.json().get("detail") or detail
109- except Exception: # noqa: BLE001, S110
110- pass
111- raise ValueError(f"gateway HTTP push rejected status={resp.status_code} detail={detail!r}")
112 169 
113 result = None170 result = None
114 try:171 try:
@@ -128,3 +185,47 @@ async def gateway_request(
128 "result": result,185 "result": result,
129 "transport": "http",186 "transport": "http",
130 }187 }
188+ 
189+ 
190+async def runtime_request(
191+ jiuwenclaw_id: str,
192+ method: str,
193+ path: str,
194+ payload: dict[str, Any] | None = None,
195+ *,
196+ timeout: float | None = None,
197+ handler: DBHandler | None = None,
198+) -> dict[str, Any]:
199+ """向 Agent Runtime 发一次同步写请求。
200+ 
201+ ``path`` 为绝对路径,如 ``/api/session/config_sync``。
202+ 无 ``runtime_host`` 时抛 ``ValueError``(与 gateway_request 对称)。
203+ """
204+ jid = str(jiuwenclaw_id or "").strip()
205+ endpoint = await require_runtime_endpoint(jid)
206+ resp = await http_request(
207+ jid,
208+ "runtime",
209+ method,
210+ path,
211+ payload,
212+ endpoint=endpoint,
213+ timeout=(
214+ settings.agent_runtime_sync_timeout if timeout is None else float(timeout)
215+ ),
216+ handler=handler,
217+ )
218+ body: Any = None
219+ try:
220+ body = resp.json()
221+ except Exception: # noqa: BLE001, S110
222+ body = None
223+ if isinstance(body, dict) and body.get("ok") is False:
224+ raise ValueError(body.get("error_message") or "agent runtime request failed")
225+ logger.info(
226+ "[ManagerConfigPush] runtime ok jiuwenclaw_id=%s %s %s",
227+ jid,
228+ method.upper(),
229+ path,
230+ )
231+ return body if isinstance(body, dict) else {"ok": True}
@@ -11,14 +11,14 @@ from manager_server.core.instance.instance_service import get_instance_row, list
11 11 
12 12 
13def resolve_gateway_endpoint(row: Any) -> str | None:13def resolve_gateway_endpoint(row: Any) -> str | None:
14- """解析 Gateway 配置下发基址(``gateway_config_host``)。"""14+ """解析 Gateway 配置下发基址(``gateway_host``)。"""
15- host = str(getattr(row, "gateway_config_host", None) or "").strip().rstrip("/")15+ host = str(getattr(row, "gateway_host", None) or "").strip().rstrip("/")
16 return host or None16 return host or None
17 17 
18 18 
19def resolve_runtime_endpoint(row: Any) -> str | None:19def resolve_runtime_endpoint(row: Any) -> str | None:
20- """解析 Runtime ``config_sync`` 基址(``runtime_config_host``)。"""20+ """解析 Runtime ``config_sync`` 基址(``runtime_host``)。"""
21- host = str(getattr(row, "runtime_config_host", None) or "").strip().rstrip("/")21+ host = str(getattr(row, "runtime_host", None) or "").strip().rstrip("/")
22 return host or None22 return host or None
23 23 
24 24 
@@ -31,14 +31,14 @@ async def require_gateway_endpoint(jiuwenclaw_id: str) -> str:
31 endpoint = resolve_gateway_endpoint(row)31 endpoint = resolve_gateway_endpoint(row)
32 if not endpoint:32 if not endpoint:
33 raise ValueError(33 raise ValueError(
34- f"no gateway_config_host for jiuwenclaw_id={jiuwenclaw_id!r}; "34+ f"no gateway_host for jiuwenclaw_id={jiuwenclaw_id!r}; "
35- "set gateway_config_host on the instance"35+ "set gateway_host on the instance"
36 )36 )
37 return endpoint37 return endpoint
38 38 
39 39 
40async def require_runtime_endpoint(jiuwenclaw_id: str) -> str:40async def require_runtime_endpoint(jiuwenclaw_id: str) -> str:
41- """按实例解析 Runtime 基址(``runtime_config_host``)。"""41+ """按实例解析 Runtime 基址(``runtime_host``)。"""
42 from manager_server.infrastructure.db import get_db_handler42 from manager_server.infrastructure.db import get_db_handler
43 43 
44 row = await get_instance_row(get_db_handler(), jiuwenclaw_id)44 row = await get_instance_row(get_db_handler(), jiuwenclaw_id)
@@ -47,8 +47,8 @@ async def require_runtime_endpoint(jiuwenclaw_id: str) -> str:
47 endpoint = resolve_runtime_endpoint(row)47 endpoint = resolve_runtime_endpoint(row)
48 if not endpoint:48 if not endpoint:
49 raise ValueError(49 raise ValueError(
50- f"no runtime_config_host for jiuwenclaw_id={jiuwenclaw_id!r}; "50+ f"no runtime_host for jiuwenclaw_id={jiuwenclaw_id!r}; "
51- "set runtime_config_host on the instance"51+ "set runtime_host on the instance"
52 )52 )
53 return endpoint53 return endpoint
54 54 
@@ -18,21 +18,28 @@ INSTANCE_INFO_TABLE_DEF = TableDefinition(
18 ColumnDefinition(18 ColumnDefinition(
19 "namespace", "string", length=64, nullable=False, default="default"19 "namespace", "string", length=64, nullable=False, default="default"
20 ),20 ),
21- # Gateway 侧(config_host → status → last_alive)21+ # Gateway 侧(host → status → last_alive)
22- ColumnDefinition("gateway_config_host", "string", length=512, nullable=False),22+ ColumnDefinition("gateway_host", "string", length=512, nullable=False),
23 ColumnDefinition(23 ColumnDefinition(
24 "gateway_status", "string", length=32, nullable=False, default="pending"24 "gateway_status", "string", length=32, nullable=False, default="pending"
25 ),25 ),
26 ColumnDefinition("gateway_last_alive", "datetime", nullable=True),26 ColumnDefinition("gateway_last_alive", "datetime", nullable=True),
27 # Runtime 侧27 # Runtime 侧
28- ColumnDefinition("runtime_config_host", "string", length=512, nullable=False),28+ ColumnDefinition("runtime_host", "string", length=512, nullable=False),
29 ColumnDefinition(29 ColumnDefinition(
30 "runtime_status", "string", length=32, nullable=False, default="pending"30 "runtime_status", "string", length=32, nullable=False, default="pending"
31 ),31 ),
32 ColumnDefinition("runtime_last_alive", "datetime", nullable=True),32 ColumnDefinition("runtime_last_alive", "datetime", nullable=True),
33+ # User Web 侧(探活地址 / 状态 / 上次存活;存量库由 init 补列 + 回填)
34+ ColumnDefinition(
35+ "user_web_host", "string", length=512, nullable=False, default=""
36+ ),
37+ ColumnDefinition(
38+ "user_web_status", "string", length=32, nullable=False, default="pending"
39+ ),
40+ ColumnDefinition("user_web_last_alive", "datetime", nullable=True),
33 ColumnDefinition("space_id", "string", length=64, nullable=False, default="default"),41 ColumnDefinition("space_id", "string", length=64, nullable=False, default="default"),
34- # 扩展 JSON:用户面反代上游 user_web_host / gateway_web_http_host /gateway_web_ws_host42+ # 扩展 JSON:gateway_web_http_host / gateway_web_ws_host 等用户面反代上游
35- # 以及 User Web 探活 user_web_status / user_web_last_alive 等
36 ColumnDefinition("data", "json", nullable=True),43 ColumnDefinition("data", "json", nullable=True),
37 ColumnDefinition("created_at", "datetime", nullable=False),44 ColumnDefinition("created_at", "datetime", nullable=False),
38 ColumnDefinition("created_by", "string", length=64, nullable=False),45 ColumnDefinition("created_by", "string", length=64, nullable=False),
@@ -40,7 +47,6 @@ INSTANCE_INFO_TABLE_DEF = TableDefinition(
40 ColumnDefinition("updated_by", "string", length=64, nullable=True),47 ColumnDefinition("updated_by", "string", length=64, nullable=True),
41 ],48 ],
42 indexes=[49 indexes=[
43- IndexDefinition(["gateway_status"], unique=False),
44 IndexDefinition(["updated_at"], unique=False),50 IndexDefinition(["updated_at"], unique=False),
45 ],51 ],
46)52)
Mapplications/manager/manager_server/src/manager_server/models/key_models.py+9-0文件内容审核中,请稍后刷新重试
@@ -200,6 +200,7 @@ A2A_ACCESS_POLICY_TEMPLATE_TABLE_DEF = TableDefinition(
200 ColumnDefinition("member_template_ids", "json", nullable=False, default=list),200 ColumnDefinition("member_template_ids", "json", nullable=False, default=list),
201 ColumnDefinition("enabled", "boolean", nullable=False, default=True),201 ColumnDefinition("enabled", "boolean", nullable=False, default=True),
202 ColumnDefinition("revision", "integer", nullable=False, default=1),202 ColumnDefinition("revision", "integer", nullable=False, default=1),
203+ ColumnDefinition("data", "json", nullable=True),
203 ColumnDefinition("created_at", "datetime", nullable=False),204 ColumnDefinition("created_at", "datetime", nullable=False),
204 ColumnDefinition("updated_at", "datetime", nullable=False),205 ColumnDefinition("updated_at", "datetime", nullable=False),
205 ],206 ],
@@ -231,6 +232,8 @@ PERMISSIONS_TEMPLATE_TABLE_DEF = TableDefinition(
231)232)
232 233 
233# 容器规格目录(与 Runtime service_config_container 对齐;平台全局,不带 jiuwenclaw_id)。234# 容器规格目录(与 Runtime service_config_container 对齐;平台全局,不带 jiuwenclaw_id)。
235+# 模板经 main_container_id / sidecar_container_ids 引用;create/update 时 UPSERT 本表。
236+# command/args 与 Runtime 对齐(存量库须手工 ALTER 补列)。
234# 段落 JSON 列为内部规范形(snake 键);模板经 main_container_id / sidecar_container_ids 引用。237# 段落 JSON 列为内部规范形(snake 键);模板经 main_container_id / sidecar_container_ids 引用。
235 238 
236MCP_TEMPLATE_TABLE_DEF = TableDefinition(239MCP_TEMPLATE_TABLE_DEF = TableDefinition(
@@ -283,7 +286,10 @@ SERVICE_CONFIG_CONTAINER_TABLE_DEF = TableDefinition(
283 ColumnDefinition("resources", "json", nullable=True),286 ColumnDefinition("resources", "json", nullable=True),
284 ColumnDefinition("volume_mounts", "json", nullable=True),287 ColumnDefinition("volume_mounts", "json", nullable=True),
285 ColumnDefinition("security_context", "json", nullable=True),288 ColumnDefinition("security_context", "json", nullable=True),
289+ ColumnDefinition("command", "json", nullable=True),
290+ ColumnDefinition("args", "json", nullable=True),
286 ColumnDefinition("readiness_probe", "json", nullable=True),291 ColumnDefinition("readiness_probe", "json", nullable=True),
292+ ColumnDefinition("data", "json", nullable=True),
287 ColumnDefinition("created_at", "datetime", nullable=False),293 ColumnDefinition("created_at", "datetime", nullable=False),
288 ColumnDefinition("updated_at", "datetime", nullable=False),294 ColumnDefinition("updated_at", "datetime", nullable=False),
289 ],295 ],
@@ -305,51 +311,26 @@ SERVICE_CONFIG_TEMPLATE_TABLE_DEF = TableDefinition(
305 ColumnDefinition("template_id", "string", length=100, nullable=False),311 ColumnDefinition("template_id", "string", length=100, nullable=False),
306 ColumnDefinition("template_name", "string", length=128, nullable=False, default=""),312 ColumnDefinition("template_name", "string", length=128, nullable=False, default=""),
307 ColumnDefinition("description", "string", length=512, nullable=True),313 ColumnDefinition("description", "string", length=512, nullable=True),
308- # 以下容器级列与 Runtime 对齐:三段式引用落地前仍保留内联形态(读兼容);
309- # 新写入可同时填 main_container_id / sidecar_container_ids / volumes。
310- ColumnDefinition("agent_image", "string", length=512, nullable=False, default=""),
311 ColumnDefinition("namespace", "string", length=128, nullable=False, default="default"),314 ColumnDefinition("namespace", "string", length=128, nullable=False, default="default"),
312 ColumnDefinition("node_name", "string", length=128, nullable=True),315 ColumnDefinition("node_name", "string", length=128, nullable=True),
313- ColumnDefinition("run_as_user", "integer", nullable=True),316+ # Pod 级 securityContext.fsGroup(与 Runtime 对齐;存量库须手工 ALTER)
314- ColumnDefinition("run_as_group", "integer", nullable=True),317+ ColumnDefinition("fs_group", "integer", nullable=True),
315 ColumnDefinition("pod_name", "string", length=128, nullable=False, default="agentserver"),318 ColumnDefinition("pod_name", "string", length=128, nullable=False, default="agentserver"),
316- ColumnDefinition("container_name", "string", length=128, nullable=False, default="agent"),
317- ColumnDefinition("container_port", "integer", nullable=False, default=8080),
318- ColumnDefinition("port_name", "string", length=64, nullable=False, default="http"),
319- ColumnDefinition("sse_port", "integer", nullable=False, default=8080),
320 ColumnDefinition("sse_path", "string", length=128, nullable=False, default="/sse"),319 ColumnDefinition("sse_path", "string", length=128, nullable=False, default="/sse"),
321- ColumnDefinition("health_path", "string", length=128, nullable=False, default="/health"),
322- ColumnDefinition("agent_env", "json", nullable=True),
323- ColumnDefinition(
324- "image_pull_policy",
325- "string",
326- length=64,
327- nullable=False,
328- default="IfNotPresent",
329- ),
330 ColumnDefinition("kubeconfig", "string", length=512, nullable=True),320 ColumnDefinition("kubeconfig", "string", length=512, nullable=True),
331- ColumnDefinition("readiness_initial_delay", "integer", nullable=False, default=5),
332- ColumnDefinition("readiness_period", "integer", nullable=False, default=5),
333 ColumnDefinition("ready_timeout", "integer", nullable=False, default=300),321 ColumnDefinition("ready_timeout", "integer", nullable=False, default=300),
334 ColumnDefinition("ready_poll_interval", "integer", nullable=False, default=2),322 ColumnDefinition("ready_poll_interval", "integer", nullable=False, default=2),
335- ColumnDefinition("nfs_server", "string", length=256, nullable=True),323+ # 容器规格经 main_container_id / sidecar_container_ids 引用目录;
336- ColumnDefinition("nfs_path", "string", length=256, nullable=True),324+ # volumes 为 Pod 级卷定义(与 Runtime 三段式对齐)。
337- ColumnDefinition("nfs_mount_path", "string", length=256, nullable=True),
338- ColumnDefinition("agent_cpu_request", "string", length=32, nullable=True),
339- ColumnDefinition("agent_memory_request", "string", length=32, nullable=True),
340- ColumnDefinition("agent_cpu_limit", "string", length=32, nullable=True),
341- ColumnDefinition("agent_memory_limit", "string", length=32, nullable=True),
342- ColumnDefinition("sidecars", "json", nullable=True),
343- ColumnDefinition("agent_host_path_mounts", "json", nullable=True),
344- ColumnDefinition("agent_configmap_mounts", "json", nullable=True),
345- ColumnDefinition("agent_pvc_mounts", "json", nullable=True),
346 ColumnDefinition("main_container_id", "string", length=100, nullable=True),325 ColumnDefinition("main_container_id", "string", length=100, nullable=True),
347 ColumnDefinition("sidecar_container_ids", "json", nullable=True),326 ColumnDefinition("sidecar_container_ids", "json", nullable=True),
348 ColumnDefinition("volumes", "json", nullable=True),327 ColumnDefinition("volumes", "json", nullable=True),
349- ColumnDefinition("min_idle_services", "integer", nullable=False, default=0),328+ # 策略四列与 Runtime/wire 术语同名(曾用 EE 名 min_idle_services/
350- ColumnDefinition("service_concurrency", "integer", nullable=False, default=2),329+ # service_concurrency/service_ttl/session_concurrency;存量库须手工 RENAME)。
351- ColumnDefinition("service_ttl", "integer", nullable=False, default=300),330+ ColumnDefinition("min_idle_pods", "integer", nullable=False, default=0),
352- ColumnDefinition("session_concurrency", "integer", nullable=False, default=3),331+ ColumnDefinition("pod_concurrency", "integer", nullable=False, default=2),
332+ ColumnDefinition("pod_ttl", "integer", nullable=False, default=300),
333+ ColumnDefinition("scope_concurrency", "integer", nullable=False, default=3),
353 ColumnDefinition("session_ttl", "integer", nullable=False, default=60),334 ColumnDefinition("session_ttl", "integer", nullable=False, default=60),
354 ColumnDefinition("message_timeout", "integer", nullable=False, default=600),335 ColumnDefinition("message_timeout", "integer", nullable=False, default=600),
355 ColumnDefinition("enabled", "boolean", nullable=False, default=True),336 ColumnDefinition("enabled", "boolean", nullable=False, default=True),
@@ -20,13 +20,13 @@ class CreateInstanceBody(BaseModel):
20 namespace: str = Field(default="default", max_length=64)20 namespace: str = Field(default="default", max_length=64)
21 space_id: str = Field(default="default", max_length=64)21 space_id: str = Field(default="default", max_length=64)
22 created_by: str = Field(default="system", max_length=64)22 created_by: str = Field(default="system", max_length=64)
23- gateway_config_host: str = Field(..., min_length=1, max_length=512)23+ gateway_host: str = Field(..., min_length=1, max_length=512)
24- runtime_config_host: str = Field(..., min_length=1, max_length=512)24+ runtime_host: str = Field(..., min_length=1, max_length=512)
25 user_web_host: str = Field(25 user_web_host: str = Field(
26 ...,26 ...,
27 min_length=1,27 min_length=1,
28 max_length=512,28 max_length=512,
29- description="写入 instance_info.data.user_web_host",29+ description="写入 instance_info.user_web_host",
30 )30 )
31 gateway_web_http_host: str = Field(31 gateway_web_http_host: str = Field(
32 ...,32 ...,
@@ -45,20 +45,20 @@ class CreateInstanceBody(BaseModel):
45class InstanceUpdateBody(BaseModel):45class InstanceUpdateBody(BaseModel):
46 """更新 instance_info(未传字段不修改)。46 """更新 instance_info(未传字段不修改)。
47 47 
48- Gateway/Runtime 的 status 与 last_alive 由 Manager 探活维护,不可通过本接口修改。48+ Gateway / Runtime / User Web 的 status 与 last_alive 由 Manager 探活维护,
49- 用户面反代 host 字段写入 ``data`` JSON,不落独立列。49+ 不可通过本接口修改。gateway_web_*_host 仍写入 ``data`` JSON。
50 """50 """
51 51 
52 jiuwenclaw_name: str | None = Field(default=None, max_length=128)52 jiuwenclaw_name: str | None = Field(default=None, max_length=128)
53 description: str | None = Field(default=None, max_length=4096)53 description: str | None = Field(default=None, max_length=4096)
54 namespace: str | None = Field(default=None, max_length=64)54 namespace: str | None = Field(default=None, max_length=64)
55 space_id: str | None = Field(default=None, max_length=64)55 space_id: str | None = Field(default=None, max_length=64)
56- gateway_config_host: str | None = Field(default=None, max_length=512)56+ gateway_host: str | None = Field(default=None, max_length=512)
57- runtime_config_host: str | None = Field(default=None, max_length=512)57+ runtime_host: str | None = Field(default=None, max_length=512)
58 user_web_host: str | None = Field(58 user_web_host: str | None = Field(
59 default=None,59 default=None,
60 max_length=512,60 max_length=512,
61- description="写入 instance_info.data.user_web_host",61+ description="写入 instance_info.user_web_host",
62 )62 )
63 gateway_web_http_host: str | None = Field(63 gateway_web_http_host: str | None = Field(
64 default=None,64 default=None,
@@ -79,22 +79,22 @@ class InstanceSummary(BaseModel):
79 jiuwenclaw_name: str79 jiuwenclaw_name: str
80 namespace: str80 namespace: str
81 space_id: str81 space_id: str
82- gateway_config_host: str82+ gateway_host: str
83 gateway_status: str83 gateway_status: str
84 gateway_last_alive: str | None = None84 gateway_last_alive: str | None = None
85- runtime_config_host: str85+ runtime_host: str
86 runtime_status: str86 runtime_status: str
87 runtime_last_alive: str | None = None87 runtime_last_alive: str | None = None
88+ user_web_host: str | None = None
88 user_web_status: str = Field(89 user_web_status: str = Field(
89 default="pending",90 default="pending",
90- description="来自 instance_info.data.user_web_status",91+ description="instance_info.user_web_status",
91 )92 )
92 user_web_last_alive: str | None = Field(93 user_web_last_alive: str | None = Field(
93 default=None,94 default=None,
94- description="来自 instance_info.data.user_web_last_alive",95+ description="instance_info.user_web_last_alive",
95 )96 )
96- # 以下三项来自 data JSON,响应中展开便于前端展示97+ # 以下两项来自 data JSON,响应中展开便于前端展示
97- user_web_host: str | None = None
98 gateway_web_http_host: str | None = None98 gateway_web_http_host: str | None = None
99 gateway_web_ws_host: str | None = None99 gateway_web_ws_host: str | None = None
100 created_at: str | None = None100 created_at: str | None = None
@@ -108,7 +108,7 @@ class InstanceListQuery(BaseModel):
108 runtime_status: str | None = None108 runtime_status: str | None = None
109 user_web_status: str | None = Field(109 user_web_status: str | None = Field(
110 default=None,110 default=None,
111- description="按 data.user_web_status 内存过滤",111+ description="按 instance_info.user_web_status 过滤",
112 )112 )
113 search: str | None = Field(113 search: str | None = Field(
114 default=None,114 default=None,
@@ -120,7 +120,7 @@ class InstanceListQuery(BaseModel):
120 description=(120 description=(
121 "排序字段:jiuwenclaw_name、gateway_status、gateway_last_alive、"121 "排序字段:jiuwenclaw_name、gateway_status、gateway_last_alive、"
122 "runtime_status、runtime_last_alive、user_web_status、user_web_last_alive、"122 "runtime_status、runtime_last_alive、user_web_status、user_web_last_alive、"
123- "namespace、updated_at(user_web_* 为内存排序)"123+ "namespace、updated_at"
124 ),124 ),
125 )125 )
126 sort_order: str | None = Field(default=None, description="排序方向:asc、desc")126 sort_order: str | None = Field(default=None, description="排序方向:asc、desc")
Mapplications/manager/manager_server/src/manager_server/schemas/safe_text.py+0-3文件内容审核中,请稍后刷新重试
@@ -8,7 +8,6 @@ from __future__ import annotations
8 8 
9from typing import Annotated, Any, Literal9from typing import Annotated, Any, Literal
10from urllib.parse import unquote, urlparse, urlsplit10from urllib.parse import unquote, urlparse, urlsplit
11-import re
12 11 
13from croniter import croniter12from croniter import croniter
14from pydantic import (13from pydantic import (
@@ -31,19 +30,6 @@ from manager_server.infrastructure.template_ref import (
31ModelTypeLiteral = Literal["default", "video", "audio", "vision", "image_gen"]30ModelTypeLiteral = Literal["default", "video", "audio", "vision", "image_gen"]
32ExtensionComponentLiteral = Literal["gateway", "agent_server"]31ExtensionComponentLiteral = Literal["gateway", "agent_server"]
33ExtensionHookTypeLiteral = Literal["pre_request", "post_request", "error", "schedule"]32ExtensionHookTypeLiteral = Literal["pre_request", "post_request", "error", "schedule"]
34-ImagePullPolicyLiteral = Literal["Always", "IfNotPresent", "Never"]
35-TemplateIdPath = Annotated[str, Field(min_length=1, max_length=100)]
36-TemplateRefField = Annotated[dict[str, list[str]], BeforeValidator(normalize_template_ref)]
37-OptionalTemplateRefField = Annotated[
38- dict[str, list[str]] | None,
39- BeforeValidator(normalize_template_ref_optional),
40-]
41- 
42- 
43-ModelTypeLiteral = Literal["default", "video", "audio", "vision", "image_gen"]
44-ExtensionComponentLiteral = Literal["gateway", "agent_server"]
45-ExtensionHookTypeLiteral = Literal["pre_request", "post_request", "error", "schedule"]
46-ImagePullPolicyLiteral = Literal["Always", "IfNotPresent", "Never"]
47TemplateIdPath = Annotated[str, Field(min_length=1, max_length=100)]33TemplateIdPath = Annotated[str, Field(min_length=1, max_length=100)]
48TemplateRefField = Annotated[dict[str, list[str]], BeforeValidator(normalize_template_ref)]34TemplateRefField = Annotated[dict[str, list[str]], BeforeValidator(normalize_template_ref)]
49OptionalTemplateRefField = Annotated[35OptionalTemplateRefField = Annotated[
@@ -614,6 +600,7 @@ class A2AAccessPolicyTemplateCreateBody(SafeTextMixin):
614 mode: A2AAccessPolicyMode600 mode: A2AAccessPolicyMode
615 member_template_ids: list[TemplateIdPath] = Field(default_factory=list)601 member_template_ids: list[TemplateIdPath] = Field(default_factory=list)
616 enabled: bool = True602 enabled: bool = True
603+ data: dict[str, Any] | None = None
617 604 
618 @field_validator("member_template_ids")605 @field_validator("member_template_ids")
619 @classmethod606 @classmethod
@@ -629,6 +616,7 @@ class A2AAccessPolicyTemplateUpdateBody(SafeTextMixin):
629 mode: A2AAccessPolicyMode | None = None616 mode: A2AAccessPolicyMode | None = None
630 member_template_ids: list[TemplateIdPath] | None = None617 member_template_ids: list[TemplateIdPath] | None = None
631 enabled: bool | None = None618 enabled: bool | None = None
619+ data: dict[str, Any] | None = None
632 620 
633 @field_validator("member_template_ids")621 @field_validator("member_template_ids")
634 @classmethod622 @classmethod
@@ -656,6 +644,7 @@ class A2AAccessPolicyTemplateOut(BaseModel):
656 enabled: bool644 enabled: bool
657 revision: int645 revision: int
658 reference_count: int = 0646 reference_count: int = 0
647+ data: dict[str, Any] | None = None
659 created_at: str | None648 created_at: str | None
660 updated_at: str | None649 updated_at: str | None
661 650 
@@ -810,159 +799,32 @@ class McpTemplateOut(BaseModel):
810 updated_at: str | None799 updated_at: str | None
811 800 
812 801 
813-# 与库表类型上限一致:integer → 有符号 32 位;autoscale_interval → DECIMAL(10,3)802+# 与库表类型上限一致:integer → 有符号 32 位
814 803 
815_SERVICE_INT_MAX = 2_147_483_647804_SERVICE_INT_MAX = 2_147_483_647
816 805 
817-# K8s resource quantity:CPU 如 500m / 2 / 0.5;内存须带单位 Ki/Mi/Gi/K/M/G
818-_K8S_CPU_RE = re.compile(r"^(?:(?:0|[1-9]\d*)(?:\.\d+)?|\.\d+)m?$")
819-_K8S_MEMORY_RE = re.compile(r"^(?:(?:0|[1-9]\d*)(?:\.\d+)?|\.\d+)(?:Ki|Mi|Gi|K|M|G)$")
820- 
821- 
822-def _normalize_resource_quantity(value: Any) -> str | None:
823- if value is None:
824- return None
825- text = str(value).strip()
826- return text or None
827- 
828- 
829-def _validate_k8s_cpu(value: str | None) -> str | None:
830- if value is None:
831- return None
832- if len(value) > 32:
833- raise ValueError("at most 32 characters")
834- if not _K8S_CPU_RE.fullmatch(value):
835- raise ValueError("must be a valid Kubernetes CPU quantity (e.g. '500m', '2', '0.5')")
836- return value
837- 
838- 
839-def _validate_k8s_memory(value: str | None) -> str | None:
840- if value is None:
841- return None
842- if len(value) > 32:
843- raise ValueError("at most 32 characters")
844- if not _K8S_MEMORY_RE.fullmatch(value):
845- raise ValueError(
846- "must be a Kubernetes memory quantity with unit "
847- "Ki/Mi/Gi/K/M/G (e.g. '512Mi', '2Gi', '128M')"
848- )
849- return value
850- 
851- 
852-K8sCpuQuantity = Annotated[
853- str | None,
854- BeforeValidator(_normalize_resource_quantity),
855- AfterValidator(_validate_k8s_cpu),
856-]
857-K8sMemoryQuantity = Annotated[
858- str | None,
859- BeforeValidator(_normalize_resource_quantity),
860- AfterValidator(_validate_k8s_memory),
861-]
862- 
863- 
864-def is_valid_unix_abs_path(value: str) -> bool:
865- """校验绝对 Unix 路径:以 / 开头,禁止 \\、空段、. 与 ..。"""
866- if not value or len(value) > 512:
867- return False
868- if "\0" in value or "\\" in value:
869- return False
870- if not value.startswith("/"):
871- return False
872- if value == "/":
873- return True
874- core = value.rstrip("/")
875- if not core.startswith("/"):
876- return False
877- for segment in core[1:].split("/"):
878- if not segment or segment in (".", ".."):
879- return False
880- return True
881- 
882- 
883-def _normalize_required_nfs_path(value: Any) -> str:
884- if value is None:
885- return "/"
886- text = str(value).strip()
887- return text or "/"
888- 
889- 
890-def _validate_required_nfs_path(value: str) -> str:
891- if not is_valid_unix_abs_path(value):
892- raise ValueError("must be an absolute Unix path (e.g. '/', '/data/nfs')")
893- return value
894- 
895- 
896-def _normalize_optional_unix_path(value: Any) -> str | None:
897- if value is None:
898- return None
899- text = str(value).strip()
900- return text or None
901- 
902- 
903-def _validate_optional_unix_path(value: str | None) -> str | None:
904- if value is None:
905- return None
906- if not is_valid_unix_abs_path(value):
907- raise ValueError("must be an absolute Unix path (e.g. '/mnt/nfs')")
908- return value
909- 
910- 
911-NfsExportPath = Annotated[
912- str,
913- BeforeValidator(_normalize_required_nfs_path),
914- AfterValidator(_validate_required_nfs_path),
915-]
916-OptionalUnixAbsPath = Annotated[
917- str | None,
918- BeforeValidator(_normalize_optional_unix_path),
919- AfterValidator(_validate_optional_unix_path),
920-]
921- 
922 806 
923class ServiceConfigTemplateCreateBody(SafeTextMixin):807class ServiceConfigTemplateCreateBody(SafeTextMixin):
924 model_config = ConfigDict(str_strip_whitespace=True)808 model_config = ConfigDict(str_strip_whitespace=True)
925 809 
926 template_name: str = Field(..., min_length=1, max_length=128)810 template_name: str = Field(..., min_length=1, max_length=128)
927 description: str | None = Field(default=None, max_length=512)811 description: str | None = Field(default=None, max_length=512)
928- agent_image: str = Field(default="", max_length=512)
929 namespace: str = Field(default="default", max_length=128)812 namespace: str = Field(default="default", max_length=128)
930 node_name: str | None = Field(default=None, max_length=128)813 node_name: str | None = Field(default=None, max_length=128)
931- run_as_user: int | None = Field(default=None, ge=0, le=_SERVICE_INT_MAX)814+ fs_group: int | None = Field(default=None, ge=0, le=_SERVICE_INT_MAX)
932- run_as_group: int | None = Field(default=None, ge=0, le=_SERVICE_INT_MAX)
933 pod_name: str = Field(default="agentserver", max_length=128)815 pod_name: str = Field(default="agentserver", max_length=128)
934- container_name: str = Field(default="agent", min_length=1, max_length=128)
935- container_port: int = Field(default=8080, ge=1, le=65535)
936- port_name: str = Field(default="http", max_length=64)
937- sse_port: int = Field(default=8080, ge=1, le=65535)
938 sse_path: str = Field(default="/sse", max_length=128)816 sse_path: str = Field(default="/sse", max_length=128)
939- health_path: str = Field(default="/health", max_length=128)
940- agent_env: dict[str, str] | None = None
941- image_pull_policy: ImagePullPolicyLiteral = Field(default="IfNotPresent")
942 kubeconfig: str | None = Field(default=None, max_length=512)817 kubeconfig: str | None = Field(default=None, max_length=512)
943- readiness_initial_delay: int = Field(default=5, ge=0, le=_SERVICE_INT_MAX)
944- readiness_period: int = Field(default=5, ge=1, le=_SERVICE_INT_MAX)
945 ready_timeout: int = Field(default=300, ge=1, le=_SERVICE_INT_MAX)818 ready_timeout: int = Field(default=300, ge=1, le=_SERVICE_INT_MAX)
946 ready_poll_interval: int = Field(default=2, ge=1, le=_SERVICE_INT_MAX)819 ready_poll_interval: int = Field(default=2, ge=1, le=_SERVICE_INT_MAX)
947- nfs_server: str | None = Field(default=None, max_length=256)
948- nfs_path: OptionalUnixAbsPath = None
949- nfs_mount_path: OptionalUnixAbsPath = None
950- agent_cpu_request: K8sCpuQuantity = None
951- agent_memory_request: K8sMemoryQuantity = None
952- agent_cpu_limit: K8sCpuQuantity = None
953- agent_memory_limit: K8sMemoryQuantity = None
954- sidecars: list[dict[str, Any]] | None = None
955- agent_host_path_mounts: list[dict[str, Any]] | None = None
956- agent_configmap_mounts: list[dict[str, Any]] | None = None
957- agent_pvc_mounts: list[dict[str, Any]] | None = None
958 main_container_id: str | None = Field(default=None, max_length=100)820 main_container_id: str | None = Field(default=None, max_length=100)
959 sidecar_container_ids: list[str] | None = None821 sidecar_container_ids: list[str] | None = None
960 volumes: list[dict[str, Any]] | None = None822 volumes: list[dict[str, Any]] | None = None
961- min_idle_services: int = Field(default=0, ge=0, le=_SERVICE_INT_MAX)823+ min_idle_pods: int = Field(default=0, ge=0, le=_SERVICE_INT_MAX)
962- service_concurrency: int = Field(default=2, ge=1, le=_SERVICE_INT_MAX)824+ pod_concurrency: int = Field(default=2, ge=1, le=_SERVICE_INT_MAX)
963- service_ttl: int = Field(default=300, ge=1, le=_SERVICE_INT_MAX)825+ pod_ttl: int = Field(default=300, ge=1, le=_SERVICE_INT_MAX)
964 message_timeout: int = Field(default=600, ge=1, le=_SERVICE_INT_MAX)826 message_timeout: int = Field(default=600, ge=1, le=_SERVICE_INT_MAX)
965- session_concurrency: int = Field(default=3, ge=1, le=_SERVICE_INT_MAX)827+ scope_concurrency: int = Field(default=3, ge=1, le=_SERVICE_INT_MAX)
966 session_ttl: int = Field(default=60, ge=1, le=_SERVICE_INT_MAX)828 session_ttl: int = Field(default=60, ge=1, le=_SERVICE_INT_MAX)
967 enabled: bool = True829 enabled: bool = True
968 data: dict[str, Any] | None = None830 data: dict[str, Any] | None = None
@@ -973,44 +835,22 @@ class ServiceConfigTemplateUpdateBody(SafeTextMixin):
973 835 
974 template_name: str | None = Field(default=None, min_length=1, max_length=128)836 template_name: str | None = Field(default=None, min_length=1, max_length=128)
975 description: str | None = Field(default=None, max_length=512)837 description: str | None = Field(default=None, max_length=512)
976- agent_image: str | None = Field(default=None, max_length=512)
977 namespace: str | None = Field(default=None, max_length=128)838 namespace: str | None = Field(default=None, max_length=128)
978 node_name: str | None = Field(default=None, max_length=128)839 node_name: str | None = Field(default=None, max_length=128)
979- run_as_user: int | None = Field(default=None, ge=0, le=_SERVICE_INT_MAX)840+ fs_group: int | None = Field(default=None, ge=0, le=_SERVICE_INT_MAX)
980- run_as_group: int | None = Field(default=None, ge=0, le=_SERVICE_INT_MAX)
981 pod_name: str | None = Field(default=None, max_length=128)841 pod_name: str | None = Field(default=None, max_length=128)
982- container_name: str | None = Field(default=None, min_length=1, max_length=128)
983- container_port: int | None = Field(default=None, ge=1, le=65535)
984- port_name: str | None = Field(default=None, max_length=64)
985- sse_port: int | None = Field(default=None, ge=1, le=65535)
986 sse_path: str | None = Field(default=None, max_length=128)842 sse_path: str | None = Field(default=None, max_length=128)
987- health_path: str | None = Field(default=None, max_length=128)
988- agent_env: dict[str, str] | None = None
989- image_pull_policy: ImagePullPolicyLiteral | None = None
990 kubeconfig: str | None = Field(default=None, max_length=512)843 kubeconfig: str | None = Field(default=None, max_length=512)
991- readiness_initial_delay: int | None = Field(default=None, ge=0, le=_SERVICE_INT_MAX)
992- readiness_period: int | None = Field(default=None, ge=1, le=_SERVICE_INT_MAX)
993 ready_timeout: int | None = Field(default=None, ge=1, le=_SERVICE_INT_MAX)844 ready_timeout: int | None = Field(default=None, ge=1, le=_SERVICE_INT_MAX)
994 ready_poll_interval: int | None = Field(default=None, ge=1, le=_SERVICE_INT_MAX)845 ready_poll_interval: int | None = Field(default=None, ge=1, le=_SERVICE_INT_MAX)
995- nfs_server: str | None = Field(default=None, max_length=256)
996- nfs_path: OptionalUnixAbsPath = None
997- nfs_mount_path: OptionalUnixAbsPath = None
998- agent_cpu_request: K8sCpuQuantity = None
999- agent_memory_request: K8sMemoryQuantity = None
1000- agent_cpu_limit: K8sCpuQuantity = None
1001- agent_memory_limit: K8sMemoryQuantity = None
1002- sidecars: list[dict[str, Any]] | None = None
1003- agent_host_path_mounts: list[dict[str, Any]] | None = None
1004- agent_configmap_mounts: list[dict[str, Any]] | None = None
1005- agent_pvc_mounts: list[dict[str, Any]] | None = None
1006 main_container_id: str | None = Field(default=None, max_length=100)846 main_container_id: str | None = Field(default=None, max_length=100)
1007 sidecar_container_ids: list[str] | None = None847 sidecar_container_ids: list[str] | None = None
1008 volumes: list[dict[str, Any]] | None = None848 volumes: list[dict[str, Any]] | None = None
1009- min_idle_services: int | None = Field(default=None, ge=0, le=_SERVICE_INT_MAX)849+ min_idle_pods: int | None = Field(default=None, ge=0, le=_SERVICE_INT_MAX)
1010- service_concurrency: int | None = Field(default=None, ge=1, le=_SERVICE_INT_MAX)850+ pod_concurrency: int | None = Field(default=None, ge=1, le=_SERVICE_INT_MAX)
1011- service_ttl: int | None = Field(default=None, ge=1, le=_SERVICE_INT_MAX)851+ pod_ttl: int | None = Field(default=None, ge=1, le=_SERVICE_INT_MAX)
1012 message_timeout: int | None = Field(default=None, ge=1, le=_SERVICE_INT_MAX)852 message_timeout: int | None = Field(default=None, ge=1, le=_SERVICE_INT_MAX)
1013- session_concurrency: int | None = Field(default=None, ge=1, le=_SERVICE_INT_MAX)853+ scope_concurrency: int | None = Field(default=None, ge=1, le=_SERVICE_INT_MAX)
1014 session_ttl: int | None = Field(default=None, ge=1, le=_SERVICE_INT_MAX)854 session_ttl: int | None = Field(default=None, ge=1, le=_SERVICE_INT_MAX)
1015 enabled: bool | None = None855 enabled: bool | None = None
1016 data: dict[str, Any] | None = None856 data: dict[str, Any] | None = None
@@ -1024,7 +864,7 @@ class ServiceConfigTemplateListQuery(BaseModel):
1024 search: str | None = Field(default=None, max_length=256)864 search: str | None = Field(default=None, max_length=256)
1025 sort_by: str | None = Field(865 sort_by: str | None = Field(
1026 default=None,866 default=None,
1027- description="排序字段:template_name、description、agent_image、updated_at",867+ description="排序字段:template_name、description、updated_at",
1028 )868 )
1029 sort_order: str | None = Field(default=None, description="排序方向:asc、desc")869 sort_order: str | None = Field(default=None, description="排序方向:asc、desc")
1030 870 
@@ -1034,44 +874,23 @@ class ServiceConfigTemplateOut(BaseModel):
1034 template_id: str874 template_id: str
1035 template_name: str875 template_name: str
1036 description: str | None876 description: str | None
1037- agent_image: str
1038 namespace: str877 namespace: str
1039 node_name: str | None878 node_name: str | None
1040- run_as_user: int | None879+ fs_group: int | None
1041- run_as_group: int | None
1042 pod_name: str880 pod_name: str
1043- container_name: str
1044- container_port: int
1045- port_name: str
1046- sse_port: int
1047 sse_path: str881 sse_path: str
1048- health_path: str
1049- agent_env: dict[str, Any] | None
1050- image_pull_policy: str
1051 kubeconfig: str | None882 kubeconfig: str | None
1052- readiness_initial_delay: int
1053- readiness_period: int
1054 ready_timeout: int883 ready_timeout: int
1055 ready_poll_interval: int884 ready_poll_interval: int
1056- nfs_server: str | None
1057- nfs_path: str | None
1058- nfs_mount_path: str | None
1059- agent_cpu_request: str | None
1060- agent_memory_request: str | None
1061- agent_cpu_limit: str | None
1062- agent_memory_limit: str | None
1063- sidecars: list[dict[str, Any]] | None
1064- agent_host_path_mounts: list[dict[str, Any]] | None
1065- agent_configmap_mounts: list[dict[str, Any]] | None
1066- agent_pvc_mounts: list[dict[str, Any]] | None
1067 main_container_id: str | None885 main_container_id: str | None
1068 sidecar_container_ids: list[str] | None886 sidecar_container_ids: list[str] | None
1069 volumes: list[dict[str, Any]] | None887 volumes: list[dict[str, Any]] | None
1070- min_idle_services: int888+ main_image: str | None = None
1071- service_concurrency: int889+ min_idle_pods: int
1072- service_ttl: int890+ pod_concurrency: int
891+ pod_ttl: int
1073 message_timeout: int892 message_timeout: int
1074- session_concurrency: int893+ scope_concurrency: int
1075 session_ttl: int894 session_ttl: int
1076 enabled: bool895 enabled: bool
1077 data: dict[str, Any] | None896 data: dict[str, Any] | None
Mapplications/manager/manager_server/src/manager_server/security/keys.py+8-1文件内容审核中,请稍后刷新重试
@@ -104,9 +104,9 @@ async def _create_instance_row(
104 "jiuwenclaw_name": "integration-test",104 "jiuwenclaw_name": "integration-test",
105 "description": None,105 "description": None,
106 "namespace": "default",106 "namespace": "default",
107- "gateway_config_host": "http://127.0.0.1:18080",107+ "gateway_host": "http://127.0.0.1:18080",
108 "gateway_status": "online",108 "gateway_status": "online",
109- "runtime_config_host": "http://127.0.0.1:18081",109+ "runtime_host": "http://127.0.0.1:18081",
110 "runtime_status": "pending",110 "runtime_status": "pending",
111 "data": data,111 "data": data,
112 "space_id": "default",112 "space_id": "default",
Mapplications/manager/manager_server/tests/unit_tests/manager/demo_payloads.py+47-23文件内容审核中,请稍后刷新重试
@@ -33,7 +33,7 @@ cd ../../..
33 33 
345273 是唯一外部入口:`/auth`、`/user`、`/manager` 由 Manager SPA 提供。345273 是唯一外部入口:`/auth`、`/user`、`/manager` 由 Manager SPA 提供。
35普通用户登录后由 `/user` 先调用 `POST /api/v1/user-console/active-cluster` 写入 Cookie `jiuwenclaw_id`,再跳转 `/chat/`。35普通用户登录后由 `/user` 先调用 `POST /api/v1/user-console/active-cluster` 写入 Cookie `jiuwenclaw_id`,再跳转 `/chat/`。
36-生产 nginx 按该 Cookie 动态反代到实例 `data.user_web_host` / `data.gateway_web_*_host`(空则回退 `MANAGER_WEB_*_TARGET`)。36+生产 nginx 按该 Cookie 动态反代到实例 `user_web_host` / `data.gateway_web_*_host`(空则回退 `MANAGER_WEB_*_TARGET`)。
37`/api` 转发 Manager Server,`/idp` 转发 Identity Center;`/ws`、`/gateway-api`、`/file-api`、`/share-api` 同理动态选 Gateway。37`/api` 转发 Manager Server,`/idp` 转发 Identity Center;`/ws`、`/gateway-api`、`/file-api`、`/share-api` 同理动态选 Gateway。
38独立的 `/gateway-api` 前缀用于避免与 Manager `/api/v1/*` 冲突。Vite 本地仍为静态 `MANAGER_WEB_*_TARGET` 代理。38独立的 `/gateway-api` 前缀用于避免与 Manager `/api/v1/*` 冲突。Vite 本地仍为静态 `MANAGER_WEB_*_TARGET` 代理。
39 39 
Mapplications/manager/manager_web/src/i18n/locales/en.json+10-7文件内容审核中,请稍后刷新重试
Mapplications/manager/manager_web/src/i18n/locales/zh.json+10-7文件内容审核中,请稍后刷新重试
Mapplications/manager/manager_web/src/pages/instance/InstanceDetailPage.tsx+22-22文件内容审核中,请稍后刷新重试
@@ -34,8 +34,8 @@ const HOST_PORT = {
34 34 
35/** 手动模式默认短名(同命名空间,不含 ns 后缀) */35/** 手动模式默认短名(同命名空间,不含 ns 后缀) */
36const DEFAULT_MANUAL_HOSTS = {36const DEFAULT_MANUAL_HOSTS = {
37- gatewayConfigHost: `http://${HOST_SVC.gateway}:${HOST_PORT.gatewayConfig}`,37+ gatewayHost: `http://${HOST_SVC.gateway}:${HOST_PORT.gatewayConfig}`,
38- runtimeConfigHost: `http://${HOST_SVC.runtime}:${HOST_PORT.runtimeConfig}`,38+ runtimeHost: `http://${HOST_SVC.runtime}:${HOST_PORT.runtimeConfig}`,
39 userWebHost: `http://${HOST_SVC.web}:${HOST_PORT.userWeb}`,39 userWebHost: `http://${HOST_SVC.web}:${HOST_PORT.userWeb}`,
40 gatewayWebHttpHost: `http://${HOST_SVC.gateway}:${HOST_PORT.gatewayWebHttp}`,40 gatewayWebHttpHost: `http://${HOST_SVC.gateway}:${HOST_PORT.gatewayWebHttp}`,
41 gatewayWebWsHost: `http://${HOST_SVC.gateway}:${HOST_PORT.gatewayWebWs}`,41 gatewayWebWsHost: `http://${HOST_SVC.gateway}:${HOST_PORT.gatewayWebWs}`,
@@ -49,16 +49,16 @@ const FIELD_MAX_LENGTH = {
49 jiuwenclaw_name: 128,49 jiuwenclaw_name: 128,
50 description: 4096,50 description: 4096,
51 namespace: 63,51 namespace: 63,
52- gateway_config_host: 512,52+ gateway_host: 512,
53- runtime_config_host: 512,53+ runtime_host: 512,
54 user_web_host: 512,54 user_web_host: 512,
55 gateway_web_http_host: 512,55 gateway_web_http_host: 512,
56 gateway_web_ws_host: 512,56 gateway_web_ws_host: 512,
57} as const;57} as const;
58 58 
59type HostBundle = {59type HostBundle = {
60- gatewayConfigHost: string;60+ gatewayHost: string;
61- runtimeConfigHost: string;61+ runtimeHost: string;
62 userWebHost: string;62 userWebHost: string;
63 gatewayWebHttpHost: string;63 gatewayWebHttpHost: string;
64 gatewayWebWsHost: string;64 gatewayWebWsHost: string;
@@ -67,8 +67,8 @@ type HostBundle = {
67function buildHostsFromNamespace(ns: string): HostBundle {67function buildHostsFromNamespace(ns: string): HostBundle {
68 const namespace = ns.trim();68 const namespace = ns.trim();
69 return {69 return {
70- gatewayConfigHost: `http://${HOST_SVC.gateway}.${namespace}:${HOST_PORT.gatewayConfig}`,70+ gatewayHost: `http://${HOST_SVC.gateway}.${namespace}:${HOST_PORT.gatewayConfig}`,
71- runtimeConfigHost: `http://${HOST_SVC.runtime}.${namespace}:${HOST_PORT.runtimeConfig}`,71+ runtimeHost: `http://${HOST_SVC.runtime}.${namespace}:${HOST_PORT.runtimeConfig}`,
72 userWebHost: `http://${HOST_SVC.web}.${namespace}:${HOST_PORT.userWeb}`,72 userWebHost: `http://${HOST_SVC.web}.${namespace}:${HOST_PORT.userWeb}`,
73 gatewayWebHttpHost: `http://${HOST_SVC.gateway}.${namespace}:${HOST_PORT.gatewayWebHttp}`,73 gatewayWebHttpHost: `http://${HOST_SVC.gateway}.${namespace}:${HOST_PORT.gatewayWebHttp}`,
74 gatewayWebWsHost: `http://${HOST_SVC.gateway}.${namespace}:${HOST_PORT.gatewayWebWs}`,74 gatewayWebWsHost: `http://${HOST_SVC.gateway}.${namespace}:${HOST_PORT.gatewayWebWs}`,
@@ -110,7 +110,7 @@ export function CreateInstanceModal({ open, onClose, onCreated }: Props) {
110 110 
111 const [name, setName] = useState('');111 const [name, setName] = useState('');
112 const [description, setDescription] = useState('');112 const [description, setDescription] = useState('');
113- const [hostMode, setHostMode] = useState<HostMode>('auto');113+ const [hostMode, setHostMode] = useState<HostMode>('manual');
114 /** 仅自动页签使用;与手动页签互不影响 */114 /** 仅自动页签使用;与手动页签互不影响 */
115 const [autoNamespace, setAutoNamespace] = useState(DEFAULT_NAMESPACE);115 const [autoNamespace, setAutoNamespace] = useState(DEFAULT_NAMESPACE);
116 /** 仅手动页签使用;与自动页签互不影响 */116 /** 仅手动页签使用;与自动页签互不影响 */
@@ -131,7 +131,7 @@ export function CreateInstanceModal({ open, onClose, onCreated }: Props) {
131 const next = {131 const next = {
132 name: '',132 name: '',
133 description: '',133 description: '',
134- hostMode: 'auto' as HostMode,134+ hostMode: 'manual' as HostMode,
135 autoNamespace: DEFAULT_NAMESPACE,135 autoNamespace: DEFAULT_NAMESPACE,
136 manualHosts: { ...DEFAULT_MANUAL_HOSTS },136 manualHosts: { ...DEFAULT_MANUAL_HOSTS },
137 };137 };
@@ -177,8 +177,8 @@ export function CreateInstanceModal({ open, onClose, onCreated }: Props) {
177 177 
178 const requiredChecks: { label: string; invalid: boolean }[] = [178 const requiredChecks: { label: string; invalid: boolean }[] = [
179 { label: t('instanceForm.name'), invalid: !name.trim() },179 { label: t('instanceForm.name'), invalid: !name.trim() },
180- { label: t('instanceForm.gatewayConfigHost'), invalid: !hosts.gatewayConfigHost.trim() },180+ { label: t('instanceForm.gatewayHost'), invalid: !hosts.gatewayHost.trim() },
181- { label: t('instanceForm.runtimeConfigHost'), invalid: !hosts.runtimeConfigHost.trim() },181+ { label: t('instanceForm.runtimeHost'), invalid: !hosts.runtimeHost.trim() },
182 { label: t('instanceForm.userWebHost'), invalid: !hosts.userWebHost.trim() },182 { label: t('instanceForm.userWebHost'), invalid: !hosts.userWebHost.trim() },
183 { label: t('instanceForm.gatewayWebHttpHost'), invalid: !hosts.gatewayWebHttpHost.trim() },183 { label: t('instanceForm.gatewayWebHttpHost'), invalid: !hosts.gatewayWebHttpHost.trim() },
184 ];184 ];
@@ -188,8 +188,8 @@ export function CreateInstanceModal({ open, onClose, onCreated }: Props) {
188 return;188 return;
189 }189 }
190 const requiredHosts: { label: string; value: string }[] = [190 const requiredHosts: { label: string; value: string }[] = [
191- { label: t('instanceForm.gatewayConfigHost'), value: hosts.gatewayConfigHost },191+ { label: t('instanceForm.gatewayHost'), value: hosts.gatewayHost },
192- { label: t('instanceForm.runtimeConfigHost'), value: hosts.runtimeConfigHost },192+ { label: t('instanceForm.runtimeHost'), value: hosts.runtimeHost },
193 { label: t('instanceForm.userWebHost'), value: hosts.userWebHost },193 { label: t('instanceForm.userWebHost'), value: hosts.userWebHost },
194 { label: t('instanceForm.gatewayWebHttpHost'), value: hosts.gatewayWebHttpHost },194 { label: t('instanceForm.gatewayWebHttpHost'), value: hosts.gatewayWebHttpHost },
195 ];195 ];
@@ -220,8 +220,8 @@ export function CreateInstanceModal({ open, onClose, onCreated }: Props) {
220 ...(namespace !== undefined ? { namespace } : {}),220 ...(namespace !== undefined ? { namespace } : {}),
221 space_id: 'default',221 space_id: 'default',
222 created_by: 'system',222 created_by: 'system',
223- gateway_config_host: hosts.gatewayConfigHost.trim().slice(0, FIELD_MAX_LENGTH.gateway_config_host),223+ gateway_host: hosts.gatewayHost.trim().slice(0, FIELD_MAX_LENGTH.gateway_host),
224- runtime_config_host: hosts.runtimeConfigHost.trim().slice(0, FIELD_MAX_LENGTH.runtime_config_host),224+ runtime_host: hosts.runtimeHost.trim().slice(0, FIELD_MAX_LENGTH.runtime_host),
225 user_web_host: hosts.userWebHost.trim().slice(0, FIELD_MAX_LENGTH.user_web_host),225 user_web_host: hosts.userWebHost.trim().slice(0, FIELD_MAX_LENGTH.user_web_host),
226 gateway_web_http_host: hosts.gatewayWebHttpHost.trim().slice(226 gateway_web_http_host: hosts.gatewayWebHttpHost.trim().slice(
227 0,227 0,
@@ -276,15 +276,6 @@ export function CreateInstanceModal({ open, onClose, onCreated }: Props) {
276 276 
277 <div className="flex flex-col items-center">277 <div className="flex flex-col items-center">
278 <div className="tabs-bar" role="tablist" aria-label={t('instanceForm.hostMode')}>278 <div className="tabs-bar" role="tablist" aria-label={t('instanceForm.hostMode')}>
279- <button
280- type="button"
281- role="tab"
282- aria-selected={hostMode === 'auto'}
283- className={`tab ${hostMode === 'auto' ? 'active' : ''}`}
284- onClick={() => setHostMode('auto')}
285- >
286- {t('instanceForm.hostModeAuto')}
287- </button>
288 <button279 <button
289 type="button"280 type="button"
290 role="tab"281 role="tab"
@@ -294,6 +285,15 @@ export function CreateInstanceModal({ open, onClose, onCreated }: Props) {
294 >285 >
295 {t('instanceForm.hostModeManual')}286 {t('instanceForm.hostModeManual')}
296 </button>287 </button>
288+ <button
289+ type="button"
290+ role="tab"
291+ aria-selected={hostMode === 'auto'}
292+ className={`tab ${hostMode === 'auto' ? 'active' : ''}`}
293+ onClick={() => setHostMode('auto')}
294+ >
295+ {t('instanceForm.hostModeAuto')}
296+ </button>
297 </div>297 </div>
298 <p className="text-xs text-muted mt-1 text-center">298 <p className="text-xs text-muted mt-1 text-center">
299 {hostMode === 'auto' ? t('instanceForm.hostModeAutoHint') : t('instanceForm.hostModeManualHint')}299 {hostMode === 'auto' ? t('instanceForm.hostModeAutoHint') : t('instanceForm.hostModeManualHint')}
@@ -311,8 +311,8 @@ export function CreateInstanceModal({ open, onClose, onCreated }: Props) {
311 placeholder={DEFAULT_NAMESPACE}311 placeholder={DEFAULT_NAMESPACE}
312 />312 />
313 </div>313 </div>
314- <HostPreview label={t('instanceForm.gatewayConfigHost')} value={autoHosts.gatewayConfigHost} />314+ <HostPreview label={t('instanceForm.gatewayHost')} value={autoHosts.gatewayHost} />
315- <HostPreview label={t('instanceForm.runtimeConfigHost')} value={autoHosts.runtimeConfigHost} />315+ <HostPreview label={t('instanceForm.runtimeHost')} value={autoHosts.runtimeHost} />
316 <HostPreview label={t('instanceForm.userWebHost')} value={autoHosts.userWebHost} />316 <HostPreview label={t('instanceForm.userWebHost')} value={autoHosts.userWebHost} />
317 <HostPreview317 <HostPreview
318 label={t('instanceForm.gatewayWebHttpHost')}318 label={t('instanceForm.gatewayWebHttpHost')}
@@ -323,21 +323,21 @@ export function CreateInstanceModal({ open, onClose, onCreated }: Props) {
323 ) : (323 ) : (
324 <>324 <>
325 <div>325 <div>
326- <FieldLabel required>{t('instanceForm.gatewayConfigHost')}</FieldLabel>326+ <FieldLabel required>{t('instanceForm.gatewayHost')}</FieldLabel>
327 <LimitedTextInput327 <LimitedTextInput
328- value={manualHosts.gatewayConfigHost}328+ value={manualHosts.gatewayHost}
329- maxLength={FIELD_MAX_LENGTH.gateway_config_host}329+ maxLength={FIELD_MAX_LENGTH.gateway_host}
330- onChange={(gatewayConfigHost) => patchManual({ gatewayConfigHost })}330+ onChange={(gatewayHost) => patchManual({ gatewayHost })}
331- placeholder={DEFAULT_MANUAL_HOSTS.gatewayConfigHost}331+ placeholder={DEFAULT_MANUAL_HOSTS.gatewayHost}
332 />332 />
333 </div>333 </div>
334 <div>334 <div>
335- <FieldLabel required>{t('instanceForm.runtimeConfigHost')}</FieldLabel>335+ <FieldLabel required>{t('instanceForm.runtimeHost')}</FieldLabel>
336 <LimitedTextInput336 <LimitedTextInput
337- value={manualHosts.runtimeConfigHost}337+ value={manualHosts.runtimeHost}
338- maxLength={FIELD_MAX_LENGTH.runtime_config_host}338+ maxLength={FIELD_MAX_LENGTH.runtime_host}
339- onChange={(runtimeConfigHost) => patchManual({ runtimeConfigHost })}339+ onChange={(runtimeHost) => patchManual({ runtimeHost })}
340- placeholder={DEFAULT_MANUAL_HOSTS.runtimeConfigHost}340+ placeholder={DEFAULT_MANUAL_HOSTS.runtimeHost}
341 />341 />
342 </div>342 </div>
343 <div>343 <div>
Mapplications/manager/manager_web/src/services/api.ts+1-1文件内容审核中,请稍后刷新重试
@@ -3,10 +3,10 @@ export interface InstanceSummary {
3 jiuwenclaw_name: string;3 jiuwenclaw_name: string;
4 namespace: string;4 namespace: string;
5 space_id: string;5 space_id: string;
6- gateway_config_host: string;6+ gateway_host: string;
7 gateway_status: string;7 gateway_status: string;
8 gateway_last_alive?: string | null;8 gateway_last_alive?: string | null;
9- runtime_config_host: string;9+ runtime_host: string;
10 runtime_status: string;10 runtime_status: string;
11 runtime_last_alive?: string | null;11 runtime_last_alive?: string | null;
12 user_web_status?: string;12 user_web_status?: string;
@@ -31,8 +31,8 @@ export interface CreateInstanceBody {
31 namespace?: string;31 namespace?: string;
32 space_id?: string;32 space_id?: string;
33 created_by?: string;33 created_by?: string;
34- gateway_config_host: string;34+ gateway_host: string;
35- runtime_config_host: string;35+ runtime_host: string;
36 user_web_host?: string;36 user_web_host?: string;
37 gateway_web_http_host?: string;37 gateway_web_http_host?: string;
38 gateway_web_ws_host?: string;38 gateway_web_ws_host?: string;
@@ -44,8 +44,8 @@ export interface UpdateInstanceBody {
44 description?: string;44 description?: string;
45 namespace?: string;45 namespace?: string;
46 space_id?: string;46 space_id?: string;
47- gateway_config_host?: string;47+ gateway_host?: string;
48- runtime_config_host?: string;48+ runtime_host?: string;
49 user_web_host?: string;49 user_web_host?: string;
50 gateway_web_http_host?: string;50 gateway_web_http_host?: string;
51 gateway_web_ws_host?: string;51 gateway_web_ws_host?: string;
Mapplications/manager/manager_web/src/types/template.ts+13-54文件内容审核中,请稍后刷新重试
Mapplications/manager/manager_web/src/utils/serviceConfigExport.ts+116-213文件内容审核中,请稍后刷新重试