refactor(beaver): 移除Hermes相关引用和迁移代码,完善Beaver后端主线实现
移除了所有Hermes相关的命名引用,包括: - 从.gitignore中清理相关构建缓存文件 - 将README中的beaver-home路径配置更新 - 完善backend/README.md文档说明Beaver后端主线实现 - 移除Hermes风格的相关注释和兼容性代码 - 清理nanobot环境变量兼容性处理 - 删除技能迁移和服务迁移相关功能代码 - 更新测试用例中相关命名和函数名 BREAKING CHANGE: 移除了Hermes迁移相关API和CLI命令,不再支持nanobot环境变量兼容性
This commit is contained in:
23
.gitignore
vendored
23
.gitignore
vendored
@ -1,12 +1,35 @@
|
||||
# Runtime data generated by local Docker deployment
|
||||
authz-service/runtime/data/
|
||||
authz-service/src/data/
|
||||
app-instance/runtime/instances/
|
||||
app-instance/runtime/registry/
|
||||
router-proxy/runtime/conf.d/
|
||||
runtime/
|
||||
sessions/
|
||||
**/sessions/state.db
|
||||
**/runtime/**/*.lock
|
||||
|
||||
# Local build / cache artifacts
|
||||
**/__pycache__/
|
||||
**/.pytest_cache/
|
||||
**/node_modules/
|
||||
**/.next/
|
||||
**/.next-dev/
|
||||
**/.turbo/
|
||||
**/.ruff_cache/
|
||||
**/.mypy_cache/
|
||||
**/.cache/
|
||||
**/.venv/
|
||||
**/dist/
|
||||
**/build/
|
||||
**/*.egg-info/
|
||||
**/tsconfig.tsbuildinfo
|
||||
*.log
|
||||
*.tmp
|
||||
*.py[cod]
|
||||
|
||||
# Local secrets / env files
|
||||
.env
|
||||
*.env
|
||||
*.pem
|
||||
app-instance/frontend/.env_prod
|
||||
|
||||
@ -377,7 +377,7 @@ http://alice.203.0.113.10.nip.io:8088
|
||||
写到每个实例自己的:
|
||||
|
||||
```text
|
||||
app-instance/runtime/instances/<slug>/nanobot-home/config.json
|
||||
app-instance/runtime/instances/<slug>/beaver-home/config.json
|
||||
```
|
||||
|
||||
不是写在 AuthZ 的某个 setting 里。
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
# Beaver Backend
|
||||
|
||||
这是新的 `Beaver` 后端。
|
||||
|
||||
旧实现已保留在 [backend-old](/home/ivan/xuan/nano_project/app-instance/backend-old),新目录用于按 [change.md](/home/ivan/xuan/nano_project/app-instance/backend/change.md) 的蓝图逐步重建后端。
|
||||
这是 `Beaver` 后端。
|
||||
|
||||
当前已经落地的主线:
|
||||
|
||||
@ -28,11 +26,4 @@
|
||||
|
||||
## 说明
|
||||
|
||||
这个目录已经不是空骨架,但仍不等于完成迁移。
|
||||
|
||||
后续迁移原则:
|
||||
|
||||
1. 不再新增 `nanobot` 命名。
|
||||
2. 不在新目录中保留 `third_party/`。
|
||||
3. 所有 agent 最终都复用 `beaver.engine`。
|
||||
4. 高级 team 策略先编译成 Beaver 自有 `ExecutionGraph`,不直接暴露 swarms runtime。
|
||||
后端已切到 Beaver 主线,不再保留旧实现、vendored 第三方 runtime 或迁移期旧命名兼容入口。所有 agent 运行都复用 `beaver.engine`,多 agent 协调通过 Beaver 自有 coordinator 和 `ExecutionGraph` 表达。
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
|
||||
1. 先服务单 agent 主链
|
||||
2. 先支持 frozen curated memory,而不是 live memory
|
||||
3. skills 按 Hermes 风格支持“显式激活消息注入”,不在这里做磁盘扫描
|
||||
3. skills 通过显式激活消息注入,不在这里做磁盘扫描
|
||||
4. 为后续 channel / gateway / team metadata 预留注入位,但不提前做复杂逻辑
|
||||
"""
|
||||
|
||||
@ -45,7 +45,7 @@ class SkillContext:
|
||||
- `name`:用于生成激活提示
|
||||
- `content`:skill 的完整正文
|
||||
|
||||
注意:按当前 Hermes 风格实现,skill 正文不再塞进 system prompt,而是转成显式消息注入。
|
||||
注意:skill 正文不再塞进 system prompt,而是转成显式消息注入。
|
||||
"""
|
||||
|
||||
name: str
|
||||
@ -151,7 +151,7 @@ class ContextBuilder:
|
||||
- 身份与总规则要最靠前
|
||||
- session/execution 是本轮运行语境,优先级高于长期记忆
|
||||
- memory 必须是 frozen snapshot,避免中途写 memory 后 prompt 失真
|
||||
- activated skill 正文按 Hermes 风格放到显式消息里,避免 system prompt 持续膨胀
|
||||
- activated skill 正文放到显式消息里,避免 system prompt 持续膨胀
|
||||
"""
|
||||
|
||||
sections: list[str] = [BEAVER_USER_ASSISTANT_IDENTITY_PROMPT]
|
||||
@ -190,7 +190,7 @@ class ContextBuilder:
|
||||
|
||||
这里做三件事:
|
||||
1. 先生成最终 system prompt
|
||||
2. 按 Hermes 风格,把已激活 skill 的完整正文作为显式消息注入
|
||||
2. 把已激活 skill 的完整正文作为显式消息注入
|
||||
3. 把历史消息按原顺序接到后面
|
||||
4. 如果存在当前用户输入,则把本轮输入追加为最后一条 user message
|
||||
|
||||
@ -348,7 +348,7 @@ class ContextBuilder:
|
||||
return "# Current Session\n\n" + "\n".join(rows)
|
||||
|
||||
def build_skill_activation_messages(self, activated_skills: list[SkillContext]) -> list[dict[str, str]]:
|
||||
"""按 Hermes 风格把已激活 skill 转成显式消息。
|
||||
"""把已激活 skill 转成显式消息。
|
||||
|
||||
关键区别:
|
||||
- system prompt 只保留轻量 skills index
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
"""Hermes 风格的 provider runtime resolution。"""
|
||||
"""Provider runtime resolution for Beaver."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@ -165,7 +165,7 @@ def resolve_fallback_runtime(
|
||||
) -> ProviderRuntime | None:
|
||||
"""把 fallback 配置解析成独立 runtime。
|
||||
|
||||
Hermes 的 fallback 是“主 provider 失败后切换到另一个 provider:model”。
|
||||
fallback 的语义是“主 provider 失败后切换到另一个 provider:model”。
|
||||
这里先把 fallback 解析独立出来,具体何时激活交给上层 chain/factory。
|
||||
"""
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
"""Beaver session 子系统的 SQLite 存储实现。
|
||||
|
||||
设计来源主要参考 Hermes-agent:
|
||||
设计目标:
|
||||
1. SQLite 作为统一 session/transcript backend
|
||||
2. WAL 模式支持多读单写
|
||||
3. FTS5 支持跨 session 文本检索
|
||||
|
||||
@ -35,14 +35,12 @@ def default_config_path(*, workspace: str | Path | None = None) -> Path:
|
||||
|
||||
Priority:
|
||||
1. `BEAVER_CONFIG_PATH`
|
||||
2. `NANOBOT_CONFIG_PATH` for compatibility during migration
|
||||
3. `BEAVER_HOME/config.json`
|
||||
4. `NANOBOT_HOME/config.json` for migration compatibility
|
||||
5. `<workspace>/.beaver/config.json`
|
||||
6. `./.beaver/config.json`
|
||||
2. `BEAVER_HOME/config.json`
|
||||
3. `<workspace>/.beaver/config.json`
|
||||
4. `./.beaver/config.json`
|
||||
"""
|
||||
|
||||
explicit = os.getenv("BEAVER_CONFIG_PATH") or os.getenv("NANOBOT_CONFIG_PATH")
|
||||
explicit = os.getenv("BEAVER_CONFIG_PATH")
|
||||
if explicit:
|
||||
return Path(explicit).expanduser()
|
||||
|
||||
@ -50,10 +48,6 @@ def default_config_path(*, workspace: str | Path | None = None) -> Path:
|
||||
if beaver_home:
|
||||
return Path(beaver_home).expanduser() / "config.json"
|
||||
|
||||
nanobot_home = os.getenv("NANOBOT_HOME")
|
||||
if nanobot_home:
|
||||
return Path(nanobot_home).expanduser() / "config.json"
|
||||
|
||||
root = Path(workspace).expanduser() if workspace is not None else Path.cwd()
|
||||
return root / ".beaver" / "config.json"
|
||||
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
"""Scheduled task models for Beaver cron.
|
||||
|
||||
The scheduler borrows Hermes' durable JSON + explicit schedule parsing shape,
|
||||
but the execution target is Beaver Task mode: every trigger creates a normal
|
||||
Task run instead of a detached agent turn.
|
||||
Every trigger targets Beaver Task mode so scheduled work remains visible as a
|
||||
normal Task instead of a detached agent turn.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@ -19,7 +19,7 @@ from beaver.foundation.config import BeaverConfig
|
||||
from beaver.integrations.authz import AuthzClient
|
||||
|
||||
|
||||
OUTLOOK_SERVER_ID = os.getenv("BEAVER_OUTLOOK_MCP_SERVER_ID") or os.getenv("NANOBOT_OUTLOOK_MCP_SERVER_ID", "outlook_mcp")
|
||||
OUTLOOK_SERVER_ID = os.getenv("BEAVER_OUTLOOK_MCP_SERVER_ID", "outlook_mcp")
|
||||
OUTLOOK_OVERVIEW_MESSAGE_LIMIT = 8
|
||||
OUTLOOK_OVERVIEW_EVENT_LIMIT = 20
|
||||
OUTLOOK_MAX_PAGE_SIZE = 100
|
||||
@ -31,11 +31,11 @@ class OutlookIntegrationError(RuntimeError):
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class OutlookDefaults:
|
||||
domain: str = os.getenv("NANOBOT_OUTLOOK_DEFAULT_DOMAIN", "")
|
||||
service_endpoint: str = os.getenv("NANOBOT_OUTLOOK_DEFAULT_EWS_URL", "")
|
||||
server: str = os.getenv("NANOBOT_OUTLOOK_DEFAULT_EWS_SERVER", "")
|
||||
default_timezone: str = os.getenv("NANOBOT_OUTLOOK_DEFAULT_TIMEZONE", "Asia/Shanghai")
|
||||
autodiscover: bool = os.getenv("NANOBOT_OUTLOOK_DEFAULT_AUTODISCOVER", "0") == "1"
|
||||
domain: str = os.getenv("BEAVER_OUTLOOK_DEFAULT_DOMAIN", "")
|
||||
service_endpoint: str = os.getenv("BEAVER_OUTLOOK_DEFAULT_EWS_URL", "")
|
||||
server: str = os.getenv("BEAVER_OUTLOOK_DEFAULT_EWS_SERVER", "")
|
||||
default_timezone: str = os.getenv("BEAVER_OUTLOOK_DEFAULT_TIMEZONE", "Asia/Shanghai")
|
||||
autodiscover: bool = os.getenv("BEAVER_OUTLOOK_DEFAULT_AUTODISCOVER", "0") == "1"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@ -71,7 +71,7 @@ OUTLOOK_TOOL_NAMES = [
|
||||
|
||||
|
||||
def _call_timeout_seconds() -> float:
|
||||
raw = os.getenv("NANOBOT_OUTLOOK_MCP_CALL_TIMEOUT_SECONDS", "").strip()
|
||||
raw = os.getenv("BEAVER_OUTLOOK_MCP_CALL_TIMEOUT_SECONDS", "").strip()
|
||||
try:
|
||||
return max(1.0, float(raw)) if raw else 10.0
|
||||
except ValueError:
|
||||
@ -108,8 +108,8 @@ def outlook_defaults() -> dict[str, Any]:
|
||||
return {
|
||||
"provider": "ews",
|
||||
"server_id": OUTLOOK_SERVER_ID,
|
||||
"mcp_command": os.getenv("NANOBOT_OUTLOOK_MCP_COMMAND", "bw-outlook-mcp"),
|
||||
"mcp_extra_args": shlex.split(os.getenv("NANOBOT_OUTLOOK_MCP_EXTRA_ARGS", "").strip()),
|
||||
"mcp_command": os.getenv("BEAVER_OUTLOOK_MCP_COMMAND", "bw-outlook-mcp"),
|
||||
"mcp_extra_args": shlex.split(os.getenv("BEAVER_OUTLOOK_MCP_EXTRA_ARGS", "").strip()),
|
||||
"fields": asdict(OutlookDefaults()),
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
"""CLI entry for Beaver."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import typer
|
||||
except ModuleNotFoundError: # pragma: no cover - fallback for skeleton-only environments
|
||||
@ -29,8 +27,6 @@ except ModuleNotFoundError: # pragma: no cover - fallback for skeleton-only env
|
||||
typer = _FallbackTyper() # type: ignore[assignment]
|
||||
|
||||
from beaver.services.agent_service import AgentService
|
||||
from beaver.services.hermes_migration import HermesMigrationService
|
||||
from beaver.skills.specs import SkillSpecStore
|
||||
|
||||
app = typer.Typer(help="Beaver backend CLI") if hasattr(typer, "Typer") else typer
|
||||
|
||||
@ -59,26 +55,6 @@ def run(
|
||||
typer.echo(result.output_text)
|
||||
|
||||
|
||||
@app.command("migrate-hermes")
|
||||
def migrate_hermes(
|
||||
repo: str = typer.Option(..., "--repo", help="Local checkout of https://github.com/NousResearch/hermes-agent."),
|
||||
workspace: str | None = typer.Option(None, "--workspace", help="Workspace root to import skills into."),
|
||||
manifest: str | None = typer.Option(None, "--manifest", help="Path for hermes_migration_manifest.json."),
|
||||
dry_run: bool = typer.Option(False, "--dry-run", help="Only write the manifest without importing skills."),
|
||||
) -> None:
|
||||
"""Import no-credential Hermes Agent skills and write a manifest."""
|
||||
|
||||
service = AgentService(workspace=workspace)
|
||||
loaded = service.create_loop().boot()
|
||||
store = loaded.skill_spec_store or SkillSpecStore(loaded.workspace)
|
||||
migration = HermesMigrationService(store, manifest_path=Path(manifest) if manifest else None)
|
||||
result = migration.migrate(repo, dry_run=dry_run)
|
||||
typer.echo(
|
||||
f"Hermes migration complete: {len(result['included'])} included, "
|
||||
f"{len(result['skipped'])} skipped."
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Project script entrypoint."""
|
||||
app()
|
||||
|
||||
@ -58,7 +58,7 @@ LOCAL_TOOL_CATEGORIES = {
|
||||
|
||||
|
||||
def _workspace_path(value: str | None = None) -> Path:
|
||||
raw = value or os.getenv("BEAVER_WORKSPACE") or os.getenv("NANOBOT_WORKSPACE")
|
||||
raw = value or os.getenv("BEAVER_WORKSPACE")
|
||||
if raw:
|
||||
return Path(raw).expanduser().resolve()
|
||||
return Path.cwd()
|
||||
|
||||
@ -23,7 +23,6 @@ from beaver.foundation.models import CronExecutionResult, CronRunRecord
|
||||
from beaver.integrations.mcp import MCPConnectionManager
|
||||
from beaver.services.agent_service import NOTIFICATION_SESSION_ID, AgentService
|
||||
from beaver.services.cron_service import CronService, schedule_from_api
|
||||
from beaver.services.skill_migration import SkillMigrationService
|
||||
from beaver.services.skillhub_service import SkillHubService
|
||||
from beaver.skills.learning import SkillLearningWorker, SkillLearningWorkerConfig
|
||||
from beaver.skills.catalog.utils import parse_frontmatter
|
||||
@ -305,7 +304,7 @@ def create_app(
|
||||
)
|
||||
app.state.auth_tokens = {}
|
||||
app.state.handoff_codes = {}
|
||||
app.state.auth_file = Path(os.getenv("BEAVER_AUTH_FILE") or os.getenv("NANOBOT_AUTH_FILE") or "")
|
||||
app.state.auth_file = Path(os.getenv("BEAVER_AUTH_FILE") or "")
|
||||
max_file_size = 50 * 1024 * 1024
|
||||
|
||||
@app.get("/api/ping", response_model=WebStatusResponse)
|
||||
@ -427,7 +426,6 @@ def create_app(
|
||||
_clean_text(payload.get("base_url"))
|
||||
or config.backend_identity.public_base_url
|
||||
or os.getenv("BEAVER_FRONTEND_PUBLIC_BASE_URL")
|
||||
or os.getenv("NANOBOT_FRONTEND_PUBLIC_BASE_URL")
|
||||
or str(request.base_url).rstrip("/")
|
||||
)
|
||||
frontend_base_url = _clean_text(payload.get("frontend_base_url")) or public_base_url
|
||||
@ -526,7 +524,7 @@ def create_app(
|
||||
return {
|
||||
"id": username,
|
||||
"username": username,
|
||||
"email": os.getenv("BEAVER_BACKEND_IDENTITY__EMAIL") or os.getenv("NANOBOT_BACKEND_IDENTITY__EMAIL", ""),
|
||||
"email": os.getenv("BEAVER_BACKEND_IDENTITY__EMAIL", ""),
|
||||
"role": "owner",
|
||||
"quota_tier": "single-user",
|
||||
}
|
||||
@ -1184,30 +1182,6 @@ def create_app(
|
||||
)
|
||||
return result
|
||||
|
||||
@app.get("/api/tools/servers")
|
||||
async def list_tool_servers(request: Request) -> list[dict[str, Any]]:
|
||||
return await list_mcp_servers(request)
|
||||
|
||||
@app.get("/api/tools")
|
||||
async def list_tools(request: Request) -> dict[str, Any]:
|
||||
servers = await list_mcp_servers(request)
|
||||
tool_groups = await list_mcp_tools(request)
|
||||
server_map = {server["id"]: server for server in servers}
|
||||
grouped = {"local": [], "online": []}
|
||||
for group in tool_groups:
|
||||
server = server_map.get(group["server_id"], {})
|
||||
kind = str(server.get("kind") or "online")
|
||||
item = {
|
||||
**group,
|
||||
"server_name": server.get("name") or group["server_id"],
|
||||
"transport": server.get("transport"),
|
||||
"kind": kind,
|
||||
"category": server.get("category") or kind,
|
||||
"status": server.get("status"),
|
||||
}
|
||||
grouped["local" if kind == "local" else "online"].append(item)
|
||||
return {"servers": servers, "groups": grouped}
|
||||
|
||||
@app.get("/api/skills")
|
||||
async def list_skills(request: Request) -> list[dict[str, Any]]:
|
||||
loaded = get_agent_service(request).create_loop().boot()
|
||||
@ -1301,19 +1275,6 @@ def create_app(
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
return draft
|
||||
|
||||
@app.post("/api/skills/migrate")
|
||||
async def migrate_skills(request: Request) -> dict[str, Any]:
|
||||
loaded = get_agent_service(request).create_loop().boot()
|
||||
return SkillMigrationService(loaded.skill_spec_store).migrate_all() # type: ignore[arg-type]
|
||||
|
||||
@app.get("/api/skills/migration-manifest")
|
||||
async def get_skill_migration_manifest(request: Request) -> dict[str, Any]:
|
||||
loaded = get_agent_service(request).create_loop().boot()
|
||||
path = loaded.workspace / "skill_migration_manifest.json"
|
||||
if not path.exists():
|
||||
return {"included": [], "skipped": []}
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
@app.get("/api/marketplaces/skills/search")
|
||||
async def search_skillhub(
|
||||
request: Request,
|
||||
@ -2482,7 +2443,7 @@ def _provider_enabled(provider_name: str, provider_cfg: Any) -> bool:
|
||||
|
||||
|
||||
def _auth_file_path() -> Path:
|
||||
raw = os.getenv("BEAVER_AUTH_FILE") or os.getenv("NANOBOT_AUTH_FILE")
|
||||
raw = os.getenv("BEAVER_AUTH_FILE")
|
||||
if raw:
|
||||
return Path(raw)
|
||||
return Path.home() / ".beaver" / "web_auth_users.json"
|
||||
@ -2542,7 +2503,7 @@ def _issue_web_token(app: FastAPI, username: str) -> str:
|
||||
|
||||
|
||||
def _handoff_ttl_seconds() -> int:
|
||||
raw = os.getenv("NANOBOT_HANDOFF_CODE_TTL_SECONDS", "90").strip()
|
||||
raw = os.getenv("BEAVER_HANDOFF_CODE_TTL_SECONDS", "90").strip()
|
||||
try:
|
||||
return max(15, int(raw))
|
||||
except ValueError:
|
||||
@ -2550,7 +2511,7 @@ def _handoff_ttl_seconds() -> int:
|
||||
|
||||
|
||||
def _handoff_replay_window_seconds() -> int:
|
||||
raw = os.getenv("NANOBOT_HANDOFF_REPLAY_WINDOW_SECONDS", "15").strip()
|
||||
raw = os.getenv("BEAVER_HANDOFF_REPLAY_WINDOW_SECONDS", "15").strip()
|
||||
try:
|
||||
return max(1, int(raw))
|
||||
except ValueError:
|
||||
@ -2637,22 +2598,18 @@ def _require_web_user(app: FastAPI, authorization: str | None) -> str:
|
||||
def _backend_connection_view(request: Request) -> dict[str, Any]:
|
||||
public_base_url = (
|
||||
os.getenv("BEAVER_BACKEND_IDENTITY__PUBLIC_BASE_URL")
|
||||
or os.getenv("NANOBOT_BACKEND_IDENTITY__PUBLIC_BASE_URL")
|
||||
or os.getenv("BEAVER_FRONTEND_PUBLIC_BASE_URL")
|
||||
or os.getenv("NANOBOT_FRONTEND_PUBLIC_BASE_URL")
|
||||
or str(request.base_url).rstrip("/")
|
||||
)
|
||||
backend_id = (
|
||||
os.getenv("BEAVER_BACKEND_IDENTITY__BACKEND_ID")
|
||||
or os.getenv("NANOBOT_BACKEND_IDENTITY__BACKEND_ID")
|
||||
or os.getenv("BEAVER_BACKEND_IDENTITY__CLIENT_ID")
|
||||
or os.getenv("NANOBOT_BACKEND_IDENTITY__CLIENT_ID")
|
||||
)
|
||||
client_id = os.getenv("BEAVER_BACKEND_IDENTITY__CLIENT_ID") or os.getenv("NANOBOT_BACKEND_IDENTITY__CLIENT_ID") or backend_id
|
||||
client_id = os.getenv("BEAVER_BACKEND_IDENTITY__CLIENT_ID") or backend_id
|
||||
return {
|
||||
"backend_id": backend_id,
|
||||
"client_id": client_id,
|
||||
"name": os.getenv("BEAVER_BACKEND_IDENTITY__NAME") or os.getenv("NANOBOT_BACKEND_IDENTITY__NAME") or backend_id,
|
||||
"name": os.getenv("BEAVER_BACKEND_IDENTITY__NAME") or backend_id,
|
||||
"public_base_url": public_base_url,
|
||||
"api_base_url": public_base_url,
|
||||
"frontend_base_url": public_base_url,
|
||||
@ -2663,16 +2620,14 @@ def _backend_connection_view(request: Request) -> dict[str, Any]:
|
||||
|
||||
def _local_backend_view() -> dict[str, Any]:
|
||||
return {
|
||||
"backend_id": os.getenv("BEAVER_BACKEND_IDENTITY__BACKEND_ID") or os.getenv("NANOBOT_BACKEND_IDENTITY__BACKEND_ID"),
|
||||
"client_id": os.getenv("BEAVER_BACKEND_IDENTITY__CLIENT_ID") or os.getenv("NANOBOT_BACKEND_IDENTITY__CLIENT_ID"),
|
||||
"name": os.getenv("BEAVER_BACKEND_IDENTITY__NAME") or os.getenv("NANOBOT_BACKEND_IDENTITY__NAME"),
|
||||
"backend_id": os.getenv("BEAVER_BACKEND_IDENTITY__BACKEND_ID"),
|
||||
"client_id": os.getenv("BEAVER_BACKEND_IDENTITY__CLIENT_ID"),
|
||||
"name": os.getenv("BEAVER_BACKEND_IDENTITY__NAME"),
|
||||
"public_base_url": os.getenv("BEAVER_BACKEND_IDENTITY__PUBLIC_BASE_URL")
|
||||
or os.getenv("NANOBOT_BACKEND_IDENTITY__PUBLIC_BASE_URL")
|
||||
or os.getenv("BEAVER_FRONTEND_PUBLIC_BASE_URL")
|
||||
or os.getenv("NANOBOT_FRONTEND_PUBLIC_BASE_URL"),
|
||||
or os.getenv("BEAVER_FRONTEND_PUBLIC_BASE_URL"),
|
||||
"authz": {
|
||||
"enabled": (os.getenv("BEAVER_AUTHZ__ENABLED") or os.getenv("NANOBOT_AUTHZ__ENABLED", "")).strip() in {"1", "true", "True"},
|
||||
"base_url": os.getenv("BEAVER_AUTHZ__BASE_URL") or os.getenv("NANOBOT_AUTHZ__BASE_URL"),
|
||||
"enabled": os.getenv("BEAVER_AUTHZ__ENABLED", "").strip() in {"1", "true", "True"},
|
||||
"base_url": os.getenv("BEAVER_AUTHZ__BASE_URL"),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
"""Beaver 的精炼长期记忆存储层。
|
||||
|
||||
这个文件实现的是以 Hermes-agent 为基线的 curated memory 模型,目标不是
|
||||
这个文件实现的是 Beaver curated memory 模型,目标不是
|
||||
“把所有历史都存下来”,而是只保存跨会话仍然值得保留的稳定事实。
|
||||
|
||||
核心设计:
|
||||
|
||||
@ -38,10 +38,9 @@ _MAX_HISTORY = 20
|
||||
class CronService:
|
||||
"""Persistent single-timer scheduler.
|
||||
|
||||
Hermes' cron implementation stores jobs as JSON and ticks safely in the
|
||||
background. Beaver keeps that shape, but the callback is required to route
|
||||
agent work through Task mode so every scheduled trigger is visible as a
|
||||
normal Task.
|
||||
Jobs are stored as JSON and ticked safely in the background. The callback
|
||||
routes agent work through Task mode so every scheduled trigger is visible as
|
||||
a normal Task.
|
||||
"""
|
||||
|
||||
def __init__(self, store_path: str | Path, *, on_job: CronCallback | None = None) -> None:
|
||||
|
||||
@ -43,7 +43,7 @@ class MemoryService:
|
||||
def reload_for_new_run(self) -> None:
|
||||
"""每次新 run 开始前刷新 live state。
|
||||
|
||||
这是 Hermes 风格 memory policy 的关键点:
|
||||
这是 Beaver memory policy 的关键点:
|
||||
- 上一次会话中通过 tool 写入的持久记忆,下一次运行应该能看到
|
||||
- 但同一次 run 中途写入的新记忆,不应反向修改当前 frozen snapshot
|
||||
"""
|
||||
|
||||
@ -237,7 +237,7 @@ class SkillsLoader:
|
||||
def build_skills_summary(self) -> str:
|
||||
"""构建可注入 system prompt 的 skills index。
|
||||
|
||||
虽然函数名还沿用 `summary`,但当前语义已经更接近 Hermes 的 skills index:
|
||||
虽然函数名还沿用 `summary`,但当前语义是轻量 skills index:
|
||||
- 这里只告诉模型“系统里有哪些 skill 可用”
|
||||
- 不负责把 skill 正文塞进 system prompt
|
||||
- 真正激活的 skill 正文由 resolver/builder 走显式消息注入
|
||||
|
||||
@ -87,9 +87,7 @@ def strip_frontmatter(content: str) -> str:
|
||||
def parse_skill_metadata_blob(raw: str) -> dict[str, Any]:
|
||||
"""解析 metadata 字段里的 JSON 扩展配置。
|
||||
|
||||
为了兼容旧 nanobot 习惯,这里同时支持:
|
||||
- `nanobot`
|
||||
- `openclaw`
|
||||
Supports plain metadata objects and the current `openclaw` namespace.
|
||||
|
||||
第一版主要关心的字段有:
|
||||
- `always`
|
||||
@ -103,7 +101,7 @@ def parse_skill_metadata_blob(raw: str) -> dict[str, Any]:
|
||||
|
||||
if not isinstance(data, dict):
|
||||
return {}
|
||||
nested = data.get("nanobot", data.get("openclaw", data))
|
||||
nested = data.get("openclaw", data)
|
||||
return nested if isinstance(nested, dict) else {}
|
||||
|
||||
|
||||
|
||||
@ -20,6 +20,7 @@ from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass, field
|
||||
import inspect
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
@ -181,9 +182,21 @@ class ObjectBackedTool(BaseTool):
|
||||
arguments["current_session_id"] = context.session_id
|
||||
if "workspace" not in arguments and hasattr(self.backend, "workspace"):
|
||||
arguments["workspace"] = context.workspace
|
||||
if "metadata" not in arguments:
|
||||
if "metadata" not in arguments and self._backend_accepts_argument("metadata"):
|
||||
arguments["metadata"] = context.metadata
|
||||
|
||||
def _backend_accepts_argument(self, name: str) -> bool:
|
||||
try:
|
||||
signature = inspect.signature(self.backend.execute)
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
for parameter in signature.parameters.values():
|
||||
if parameter.kind == inspect.Parameter.VAR_KEYWORD:
|
||||
return True
|
||||
if parameter.name == name:
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _normalize_output(content: Any) -> dict[str, Any]:
|
||||
"""把后端工具返回值转成统一 success/content/error 语义。
|
||||
|
||||
@ -33,7 +33,7 @@ CRON_TOOL_PARAMETERS: dict[str, Any] = {
|
||||
},
|
||||
"schedule": {
|
||||
"type": "string",
|
||||
"description": "Hermes-style schedule, for example 'every 15m', '0 9 * * *', or an ISO datetime.",
|
||||
"description": "Schedule expression, for example 'every 15m', '0 9 * * *', or an ISO datetime.",
|
||||
},
|
||||
"every_seconds": {
|
||||
"type": "integer",
|
||||
|
||||
@ -59,7 +59,7 @@ def memory_tool(
|
||||
old_text: str | None = None,
|
||||
store: MemoryStore | None = None,
|
||||
) -> str:
|
||||
"""分发 Hermes 风格的 CRUD memory API,并返回 JSON 字符串。
|
||||
"""分发 CRUD memory API,并返回 JSON 字符串。
|
||||
|
||||
这里统一采用 JSON 返回,是为了兼容常见 tool-calling 场景:
|
||||
- LLM 更容易消费结构化结果
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
"""Beaver 内置 session_search tool。
|
||||
|
||||
这个工具对应 Hermes-agent 的跨会话检索能力,目标不是把所有历史内容塞回主上下文,
|
||||
这个工具提供跨会话检索能力,目标不是把所有历史内容塞回主上下文,
|
||||
而是按需从过去的 session 中找回“之前发生过什么”。
|
||||
|
||||
当前实现保留了几个关键行为:
|
||||
@ -28,7 +28,7 @@ class SessionSearchDB(Protocol):
|
||||
"""session_search 依赖的最小数据库契约。
|
||||
|
||||
这里没有直接绑定某个具体 SQLite 实现,而是先定义行为接口。
|
||||
这样后面无论你接的是 Hermes 风格 state DB、还是 Beaver 自己的 transcript store,
|
||||
这样后面无论你接的是当前 SQLite state DB、还是其他 transcript store,
|
||||
只要满足这些方法就能工作。
|
||||
"""
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
"""Beaver 内置 skill_view tool。
|
||||
|
||||
这个工具对应 Hermes 风格的显式 skill loading path:
|
||||
这个工具对应显式 skill loading path:
|
||||
1. skill 正文默认不会长期塞进 system prompt
|
||||
2. 模型若想查看某个 skill 的完整正文或支持文件,必须显式调用 `skill_view`
|
||||
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from beaver.engine import AgentLoop, EngineLoader
|
||||
from beaver.engine.providers import make_provider_bundle
|
||||
@ -139,9 +138,36 @@ def test_openai_compatible_qwen_config_keeps_openai_provider() -> None:
|
||||
assert bundle.main_provider._resolve_model("qwen-plus") == "openai/qwen-plus"
|
||||
|
||||
|
||||
def test_load_config_reads_stevenli_mcp_authz_identity() -> None:
|
||||
repo_root = Path(__file__).resolve().parents[4]
|
||||
config_path = repo_root / "app-instance" / "runtime" / "instances" / "stevenli" / "nanobot-home" / "config.json"
|
||||
def test_load_config_reads_mcp_authz_identity(tmp_path) -> None:
|
||||
config_path = tmp_path / "beaver-home" / "config.json"
|
||||
config_path.parent.mkdir()
|
||||
config_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"tools": {
|
||||
"mcpServers": {
|
||||
"outlook_mcp": {
|
||||
"url": "http://10.6.80.29:8000/mcp",
|
||||
"authMode": "oauth_backend_token",
|
||||
"authAudience": "mcp:outlook_mcp",
|
||||
"authScopes": ["list_tools", "tool:mail_list_messages"],
|
||||
"toolTimeout": 60,
|
||||
"sensitive": True,
|
||||
}
|
||||
}
|
||||
},
|
||||
"authz": {
|
||||
"enabled": True,
|
||||
"baseUrl": "http://nano-authz-service:19090",
|
||||
},
|
||||
"backend_identity": {
|
||||
"backend_id": "stevenli",
|
||||
"client_id": "stevenli",
|
||||
},
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
config = load_config(config_path=config_path)
|
||||
|
||||
server = config.tools.mcp_servers["outlook_mcp"]
|
||||
|
||||
@ -6,7 +6,7 @@ from beaver.tools.builtins import CronTool
|
||||
from beaver.services.cron_service import CronService, compute_next_run, parse_schedule, schedule_from_api
|
||||
|
||||
|
||||
def test_parse_hermes_style_schedules() -> None:
|
||||
def test_parse_schedule_expressions() -> None:
|
||||
interval = parse_schedule("every 15m")
|
||||
assert interval.kind == "every"
|
||||
assert interval.every_ms == 15 * 60 * 1000
|
||||
|
||||
@ -1,13 +1,11 @@
|
||||
import asyncio
|
||||
import io
|
||||
import json
|
||||
import zipfile
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from beaver.interfaces.web.app import _create_skill_upload_draft
|
||||
from beaver.services.hermes_migration import HermesMigrationService
|
||||
from beaver.services.skillhub_service import SkillHubService
|
||||
from beaver.skills.drafts import DraftService
|
||||
from beaver.skills.specs import SkillSpecStore
|
||||
@ -101,27 +99,6 @@ def test_upload_skill_zip_keeps_supporting_files_on_draft(tmp_path):
|
||||
assert upload_dir.endswith(draft["draft_id"])
|
||||
|
||||
|
||||
def test_hermes_migration_manifest_includes_no_credential_skill_and_skips_api_skill(tmp_path):
|
||||
repo = tmp_path / "hermes"
|
||||
safe = repo / "skills" / "safe"
|
||||
unsafe = repo / "skills" / "unsafe"
|
||||
safe.mkdir(parents=True)
|
||||
unsafe.mkdir(parents=True)
|
||||
safe.joinpath("SKILL.md").write_text("---\nname: safe\n---\nUse local files only.\n", encoding="utf-8")
|
||||
unsafe.joinpath("SKILL.md").write_text("---\nname: unsafe\n---\nRequires API_KEY.\n", encoding="utf-8")
|
||||
|
||||
store = SkillSpecStore(tmp_path / "workspace")
|
||||
manifest = HermesMigrationService(store).migrate(repo)
|
||||
included = {item["skill_name"] for item in manifest["included"]}
|
||||
skipped = {item.get("skill_name"): item["reason"] for item in manifest["skipped"]}
|
||||
|
||||
assert "safe" in included
|
||||
assert skipped["unsafe"] == "requires_external_credentials"
|
||||
assert store.get_skill_spec("safe") is not None
|
||||
manifest_path = tmp_path / "workspace" / "hermes_migration_manifest.json"
|
||||
assert json.loads(manifest_path.read_text(encoding="utf-8"))["source"] == "hermes-agent"
|
||||
|
||||
|
||||
def test_mcp_wrapper_metadata_preserves_server_id_with_underscores():
|
||||
tool_def = SimpleNamespace(name="auth_status", description="Auth", inputSchema={"type": "object", "properties": {}})
|
||||
|
||||
|
||||
@ -53,10 +53,9 @@
|
||||
| `/status` | 系统状态 |
|
||||
| `/cron` | 定时任务 |
|
||||
| `/skills` | 技能管理 |
|
||||
| `/plugins` | 插件管理 |
|
||||
| `/agents` | 智能体管理 |
|
||||
| `/mcp` | MCP 服务管理 |
|
||||
| `/marketplace` | 插件市场 |
|
||||
| `/marketplace` | 技能市场 |
|
||||
| `/files` | 工作区文件管理 |
|
||||
| `/help` | 帮助说明 |
|
||||
|
||||
@ -240,15 +239,9 @@ docker build \
|
||||
- 状态接口
|
||||
- WebSocket 连接
|
||||
|
||||
### 2. 命令名和目录名未做品牌迁移
|
||||
### 2. 技术标识
|
||||
|
||||
当前仓库的部分技术标识仍沿用旧命名,例如:
|
||||
|
||||
- `nanobot web`
|
||||
- `~/.beaver/plugins/`
|
||||
- 本地存储中的旧 token key
|
||||
|
||||
这些属于兼容性和后端约定的一部分,前端展示品牌已替换为 `Boardware Genius`,但技术标识没有在这个仓库里强制迁移。
|
||||
当前前端使用 Beaver 技术命名,本地 token、语言和 handoff 状态都使用 `beaver_*` key。
|
||||
|
||||
### 3. 动态内容可能仍包含英文
|
||||
|
||||
|
||||
@ -149,7 +149,6 @@ export default function NotificationDetailPage() {
|
||||
processArtifacts={[]}
|
||||
selectedRunId={null}
|
||||
onSelectRun={() => {}}
|
||||
onCancelRun={() => {}}
|
||||
onFeedback={() => {}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -7,7 +7,6 @@ import { Brain, Plus, Send, Trash2, X } from 'lucide-react';
|
||||
import { ChatWorkbench } from '@/components/chat-workbench/ChatWorkbench';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import {
|
||||
cancelDelegation,
|
||||
archiveSession,
|
||||
createSession,
|
||||
getActiveTask,
|
||||
@ -464,18 +463,6 @@ export default function ChatPage() {
|
||||
setSessionId(key);
|
||||
};
|
||||
|
||||
const handleCancelRun = useCallback(async (runId: string) => {
|
||||
try {
|
||||
await cancelDelegation(runId);
|
||||
} catch (err: any) {
|
||||
addMessage({
|
||||
role: 'assistant',
|
||||
content: pickAppText(locale, `取消任务 ${runId} 失败:${err.message || '未知错误'}`, `Failed to cancel task ${runId}: ${err.message || 'Unknown error'}`),
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
}, [addMessage, locale]);
|
||||
|
||||
const removePendingFile = useCallback((file: File) => {
|
||||
setPendingFiles((prev) => prev.filter((item) => item.file !== file));
|
||||
}, []);
|
||||
@ -566,7 +553,6 @@ export default function ChatPage() {
|
||||
processArtifacts={sessionProcessArtifacts}
|
||||
selectedRunId={selectedSessionRunId}
|
||||
onSelectRun={(runId) => setSelectedRunId(selectedSessionRunId === runId ? null : runId)}
|
||||
onCancelRun={handleCancelRun}
|
||||
onFeedback={handleFeedback}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -40,7 +40,6 @@ import {
|
||||
listSkillCandidates,
|
||||
listSkillDrafts,
|
||||
listSkills,
|
||||
migrateSkills,
|
||||
publishSkillDraft,
|
||||
regenerateSkillDraft,
|
||||
rejectSkillDraft,
|
||||
@ -207,15 +206,6 @@ export default function SkillsPage() {
|
||||
<Upload className="mr-2 h-4 w-4" />
|
||||
{t('上传技能', 'Upload skill')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => void runAction('migrate-skills', () => migrateSkills())}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={Boolean(actionId)}
|
||||
>
|
||||
{actionId === 'migrate-skills' ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Rocket className="mr-2 h-4 w-4" />}
|
||||
{t('迁移旧技能', 'Migrate legacy skills')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -15,17 +15,7 @@ import {
|
||||
Settings2,
|
||||
ScrollText,
|
||||
} from 'lucide-react';
|
||||
import { getStatus, restartSystem, updateProviderConfig } from '@/lib/api';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import { getStatus, updateProviderConfig } from '@/lib/api';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
@ -57,9 +47,6 @@ export default function StatusPage() {
|
||||
const [status, setStatus] = useState<SystemStatus | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [restartDialogOpen, setRestartDialogOpen] = useState(false);
|
||||
const [restarting, setRestarting] = useState(false);
|
||||
const [restartError, setRestartError] = useState<string | null>(null);
|
||||
const [selectedProvider, setSelectedProvider] = useState<ProviderStatus | null>(null);
|
||||
const [providerForm, setProviderForm] = useState<ProviderFormState>(() => ({
|
||||
enabled: false,
|
||||
@ -88,36 +75,6 @@ export default function StatusPage() {
|
||||
loadStatus();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!restarting) {
|
||||
return;
|
||||
}
|
||||
|
||||
const intervalId = window.setInterval(async () => {
|
||||
try {
|
||||
await getStatus();
|
||||
window.location.reload();
|
||||
} catch {
|
||||
// Ignore failures until the container is back.
|
||||
}
|
||||
}, 3000);
|
||||
|
||||
return () => {
|
||||
window.clearInterval(intervalId);
|
||||
};
|
||||
}, [restarting]);
|
||||
|
||||
const handleRestart = async () => {
|
||||
setRestartError(null);
|
||||
try {
|
||||
await restartSystem();
|
||||
setRestartDialogOpen(false);
|
||||
setRestarting(true);
|
||||
} catch (err: any) {
|
||||
setRestartError(err.message || pickAppText(locale, '重启失败', 'Restart failed'));
|
||||
}
|
||||
};
|
||||
|
||||
const openProviderDialog = (provider: ProviderStatus) => {
|
||||
setSelectedProvider(provider);
|
||||
setProviderError(null);
|
||||
@ -204,7 +161,7 @@ export default function StatusPage() {
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<Button onClick={loadStatus} variant="outline" size="sm" disabled={restarting}>
|
||||
<Button onClick={loadStatus} variant="outline" size="sm">
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
{pickAppText(locale, '刷新', 'Refresh')}
|
||||
</Button>
|
||||
@ -223,13 +180,8 @@ export default function StatusPage() {
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium">{pickAppText(locale, '运行与调试', 'Runtime and debugging')}</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{restarting
|
||||
? pickAppText(locale, '正在重启当前 docker,服务恢复后页面会自动刷新。', 'Restarting the current Docker container. The page will refresh automatically once the service is back.')
|
||||
: pickAppText(locale, '查看每次对话的运行日志,或重启当前 docker 容器。重启完成后需要重新登录。', 'Inspect per-chat runtime logs or restart the current Docker container. You will need to sign in again afterwards.')}
|
||||
{pickAppText(locale, '查看每次对话的运行日志和当前实例运行状态。', 'Inspect per-chat runtime logs and current instance status.')}
|
||||
</p>
|
||||
{restartError ? (
|
||||
<p className="text-sm text-destructive">{restartError}</p>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex flex-wrap justify-start gap-2 lg:justify-end">
|
||||
<Button asChild variant="outline">
|
||||
@ -238,34 +190,6 @@ export default function StatusPage() {
|
||||
{pickAppText(locale, '运行日志', 'Runtime Logs')}
|
||||
</Link>
|
||||
</Button>
|
||||
<AlertDialog open={restartDialogOpen} onOpenChange={setRestartDialogOpen}>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => setRestartDialogOpen(true)}
|
||||
disabled={restarting}
|
||||
>
|
||||
{restarting ? (
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
)}
|
||||
Restart
|
||||
</Button>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{pickAppText(locale, '确认重启当前实例?', 'Restart the current instance?')}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{pickAppText(locale, '这会重启当前 docker 容器,页面会短暂不可用。由于当前登录态保存在内存里,重启完成后需要重新登录。', 'This restarts the current Docker container and the page will be temporarily unavailable. Because the current sign-in state is stored in memory, you will need to sign in again after the restart.')}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={restarting}>{pickAppText(locale, '取消', 'Cancel')}</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleRestart} disabled={restarting}>
|
||||
{restarting ? pickAppText(locale, '重启中...', 'Restarting...') : pickAppText(locale, '确认重启', 'Confirm restart')}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5 grid gap-3 border-t pt-5 md:grid-cols-2">
|
||||
|
||||
@ -3,21 +3,21 @@
|
||||
import Link from 'next/link';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { AlertCircle, ArrowLeft, Bot, CheckCircle2, Download, FileText, HelpCircle, MessageSquare, RefreshCw, RotateCcw, Trash2, User, XCircle } from 'lucide-react';
|
||||
import { AlertCircle, ArrowLeft, Bot, CheckCircle2, Download, FileText, HelpCircle, MessageSquare, RefreshCw, Trash2, User, XCircle } from 'lucide-react';
|
||||
|
||||
import { OfficeStatusBadge, formatOfficeDuration, formatOfficeTime, progressPercent } from '@/components/office/OfficeShared';
|
||||
import { TaskRuntimeStatusBadge, formatTaskRuntimeDuration, formatTaskRuntimeTime, progressPercent } from '@/components/task-runtime/TaskRuntimeShared';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { cancelDelegation, deleteBackendTask, getBackendTask, getFileUrl, retryDelegation, submitChatFeedback } from '@/lib/api';
|
||||
import { deleteBackendTask, getBackendTask, getFileUrl, submitChatFeedback } from '@/lib/api';
|
||||
import { pickAppText } from '@/lib/i18n/core';
|
||||
import { useAppI18n } from '@/lib/i18n/provider';
|
||||
import { buildOfficeView, isOfficeTaskTerminal, type OfficeTaskView } from '@/lib/office';
|
||||
import { buildTaskRuntimeView, type TaskRuntimeNodeView } from '@/lib/task-runtime';
|
||||
import { useChatStore } from '@/lib/store';
|
||||
import type { BackendTask, BackendTaskRun, ProcessArtifact, ProcessEvent } from '@/types';
|
||||
|
||||
function taskVisibleStatus(task: OfficeTaskView, locale: 'zh-CN' | 'en-US') {
|
||||
function taskVisibleStatus(task: TaskRuntimeNodeView, locale: 'zh-CN' | 'en-US') {
|
||||
if (task.status === 'error') return pickAppText(locale, '任务失败', 'Task failed');
|
||||
if (task.status === 'cancelled') return pickAppText(locale, '已取消', 'Cancelled');
|
||||
return task.stageLabel || task.status;
|
||||
@ -46,7 +46,7 @@ export default function TaskDetailPage() {
|
||||
const updateMessageFeedback = useChatStore((state) => state.updateMessageFeedback);
|
||||
|
||||
const task = useMemo(
|
||||
() => buildOfficeView(taskId, { sessions, processRuns, processEvents, processArtifacts }, locale),
|
||||
() => buildTaskRuntimeView(taskId, { sessions, processRuns, processEvents, processArtifacts }, locale),
|
||||
[locale, processArtifacts, processEvents, processRuns, sessions, taskId]
|
||||
);
|
||||
const [backendTask, setBackendTask] = useState<BackendTask | null>(null);
|
||||
@ -105,7 +105,7 @@ export default function TaskDetailPage() {
|
||||
return map;
|
||||
}, [artifacts]);
|
||||
const phaseGroups = useMemo(() => {
|
||||
const groups = new Map<string, OfficeTaskView[]>();
|
||||
const groups = new Map<string, TaskRuntimeNodeView[]>();
|
||||
for (const item of task?.tasks ?? []) {
|
||||
const label = item.stageLabel || taskVisibleStatus(item, locale);
|
||||
groups.set(label, [...(groups.get(label) ?? []), item]);
|
||||
@ -180,7 +180,7 @@ export default function TaskDetailPage() {
|
||||
<div className="mt-3 flex flex-wrap gap-x-4 gap-y-1 text-xs text-muted-foreground">
|
||||
<span>{pickAppText(locale, '来源会话', 'Session')}: {backendTask.session_id}</span>
|
||||
<span>{pickAppText(locale, '创建者', 'Creator')}: {backendTask.creator}</span>
|
||||
<span>{pickAppText(locale, '更新', 'Updated')}: {formatOfficeTime(backendTask.updated_at, locale)}</span>
|
||||
<span>{pickAppText(locale, '更新', 'Updated')}: {formatTaskRuntimeTime(backendTask.updated_at, locale)}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@ -242,7 +242,7 @@ export default function TaskDetailPage() {
|
||||
<div key={index} className="rounded-md border border-border p-3">
|
||||
<div className="font-medium">{humanFeedback(String(item.feedback_type || ''), locale)}</div>
|
||||
{item.comment ? <p className="mt-1 text-muted-foreground">{String(item.comment)}</p> : null}
|
||||
{item.created_at ? <p className="mt-1 text-xs text-muted-foreground">{formatOfficeTime(String(item.created_at), locale)}</p> : null}
|
||||
{item.created_at ? <p className="mt-1 text-xs text-muted-foreground">{formatTaskRuntimeTime(String(item.created_at), locale)}</p> : null}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
@ -295,25 +295,6 @@ export default function TaskDetailPage() {
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={Boolean(actionBusy) || isOfficeTaskTerminal(task.status)}
|
||||
onClick={() => void runAction('cancel', () => cancelDelegation(task.rootRunId))}
|
||||
>
|
||||
<XCircle className="mr-2 h-4 w-4" />
|
||||
{pickAppText(locale, '取消任务', 'Cancel task')}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
disabled={Boolean(actionBusy) || !isOfficeTaskTerminal(task.status)}
|
||||
onClick={() => void runAction('retry', () => retryDelegation(task.rootRunId))}
|
||||
>
|
||||
<RotateCcw className="mr-2 h-4 w-4" />
|
||||
{pickAppText(locale, '重试任务', 'Retry task')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
@ -322,20 +303,20 @@ export default function TaskDetailPage() {
|
||||
<div className="min-w-0">
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<h1 className="truncate text-2xl font-semibold">{task.title}</h1>
|
||||
<OfficeStatusBadge status={task.status} />
|
||||
<TaskRuntimeStatusBadge status={task.status} />
|
||||
</div>
|
||||
<div className="mt-3 flex flex-wrap gap-x-4 gap-y-1 text-xs text-muted-foreground">
|
||||
<span>{pickAppText(locale, '来源会话', 'Session')}: {task.sourceSessionLabel}</span>
|
||||
<span>{pickAppText(locale, '主 Agent', 'Lead agent')}: {task.rootActorName}</span>
|
||||
<span>{pickAppText(locale, '开始', 'Started')}: {formatOfficeTime(task.createdAt, locale)}</span>
|
||||
<span>{pickAppText(locale, '耗时', 'Duration')}: {formatOfficeDuration(task.durationMs, locale)}</span>
|
||||
<span>{pickAppText(locale, '开始', 'Started')}: {formatTaskRuntimeTime(task.createdAt, locale)}</span>
|
||||
<span>{pickAppText(locale, '耗时', 'Duration')}: {formatTaskRuntimeDuration(task.durationMs, locale)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid w-full gap-3 sm:grid-cols-4 lg:w-[520px]">
|
||||
<Metric label={pickAppText(locale, '节点', 'Nodes')} value={String(task.stats.totalRuns)} />
|
||||
<Metric label={pickAppText(locale, '活跃', 'Active')} value={String(task.stats.activeRuns)} />
|
||||
<Metric label={pickAppText(locale, '产物', 'Artifacts')} value={String(task.stats.artifactCount)} />
|
||||
<Metric label={pickAppText(locale, '异常', 'Alerts')} value={String(task.alerts.length)} />
|
||||
<Metric label={pickAppText(locale, '异常', 'Alerts')} value={String(task.stats.alertCount)} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5 space-y-2">
|
||||
@ -398,7 +379,7 @@ export default function TaskDetailPage() {
|
||||
<div className="truncate font-medium">{node.title}</div>
|
||||
<div className="mt-1 text-xs text-muted-foreground">{node.actorName}</div>
|
||||
</div>
|
||||
<OfficeStatusBadge status={node.status} />
|
||||
<TaskRuntimeStatusBadge status={node.status} />
|
||||
</div>
|
||||
<div className="mt-3 text-sm text-muted-foreground">
|
||||
{node.summary || taskVisibleStatus(node, locale)}
|
||||
@ -426,13 +407,13 @@ export default function TaskDetailPage() {
|
||||
<div className="font-medium">{selectedNode.title}</div>
|
||||
<div className="mt-1 text-xs text-muted-foreground">{selectedNode.runId}</div>
|
||||
</div>
|
||||
<OfficeStatusBadge status={selectedNode.status} />
|
||||
<TaskRuntimeStatusBadge status={selectedNode.status} />
|
||||
<p className="text-sm text-muted-foreground">{selectedNode.summary || '-'}</p>
|
||||
<div className="space-y-2">
|
||||
{(eventsByRun.get(selectedNode.runId) ?? []).slice(-5).map((event) => (
|
||||
<div key={event.event_id} className="rounded-md border border-border bg-muted/30 p-2 text-xs">
|
||||
<div className="font-medium">{event.kind}</div>
|
||||
<div className="mt-1 text-muted-foreground">{event.text || formatOfficeTime(event.created_at, locale)}</div>
|
||||
<div className="mt-1 text-muted-foreground">{event.text || formatTaskRuntimeTime(event.created_at, locale)}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@ -548,7 +529,7 @@ function BackendRunConversation({ run, index }: { run: BackendTaskRun; index: nu
|
||||
<div>
|
||||
<div className="font-medium">{run.title || pickAppText(locale, `Agent ${index + 1}`, `Agent ${index + 1}`)}</div>
|
||||
<div className="mt-1 text-xs text-muted-foreground">
|
||||
{run.started_at ? formatOfficeTime(run.started_at, locale) : pickAppText(locale, '时间未知', 'Unknown time')}
|
||||
{run.started_at ? formatTaskRuntimeTime(run.started_at, locale) : pickAppText(locale, '时间未知', 'Unknown time')}
|
||||
{run.finish_reason ? ` · ${humanFinishReason(run.finish_reason, locale)}` : ''}
|
||||
</div>
|
||||
</div>
|
||||
@ -573,7 +554,7 @@ function BackendRunConversation({ run, index }: { run: BackendTaskRun; index: nu
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="mb-1 flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<span>{isAssistant ? run.title || pickAppText(locale, 'Agent 回复', 'Agent reply') : isTool ? message.tool_name || pickAppText(locale, '工具结果', 'Tool result') : pickAppText(locale, '用户要求', 'User request')}</span>
|
||||
{message.created_at ? <span>{formatOfficeTime(message.created_at, locale)}</span> : null}
|
||||
{message.created_at ? <span>{formatTaskRuntimeTime(message.created_at, locale)}</span> : null}
|
||||
</div>
|
||||
<div className="whitespace-pre-wrap rounded-md border border-border bg-muted/20 px-3 py-2 text-sm leading-6">
|
||||
{message.content}
|
||||
|
||||
@ -5,7 +5,7 @@ import { useSearchParams } from 'next/navigation';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { AlertCircle, ArrowRight, Clock3, FolderDown, ListTodo, Loader2, Play, Plus, RefreshCw, Trash2, X } from 'lucide-react';
|
||||
|
||||
import { formatOfficeTime } from '@/components/office/OfficeShared';
|
||||
import { formatTaskRuntimeTime } from '@/components/task-runtime/TaskRuntimeShared';
|
||||
import { TaskManagementTabs } from '@/components/task-management/TaskManagementTabs';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@ -151,7 +151,7 @@ function OrdinaryTasks() {
|
||||
</TableCell>
|
||||
<TableCell className="text-sm text-muted-foreground">{task.run_ids.length}</TableCell>
|
||||
<TableCell className="text-sm text-muted-foreground">{task.skill_names.length}</TableCell>
|
||||
<TableCell className="text-xs text-muted-foreground">{formatOfficeTime(task.updated_at, locale)}</TableCell>
|
||||
<TableCell className="text-xs text-muted-foreground">{formatTaskRuntimeTime(task.updated_at, locale)}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button asChild size="sm" variant="outline">
|
||||
|
||||
@ -8,7 +8,7 @@ import { pickAppText } from '@/lib/i18n/core';
|
||||
import { useAppI18n } from '@/lib/i18n/provider';
|
||||
import { useChatStore } from '@/lib/store';
|
||||
|
||||
const HANDOFF_STATE_KEY = 'nanobot_handoff_state';
|
||||
const HANDOFF_STATE_KEY = 'beaver_handoff_state';
|
||||
|
||||
type HandoffState = {
|
||||
code?: string;
|
||||
|
||||
@ -40,7 +40,7 @@ export function AppRuntimeBridge() {
|
||||
const sessionId = useChatStore((state) => state.sessionId);
|
||||
const setSessions = useChatStore((state) => state.setSessions);
|
||||
const setWsStatus = useChatStore((state) => state.setWsStatus);
|
||||
const setNanobotReady = useChatStore((state) => state.setNanobotReady);
|
||||
const setBeaverReady = useChatStore((state) => state.setBeaverReady);
|
||||
const resetProcessState = useChatStore((state) => state.resetProcessState);
|
||||
const ingestProcessEvent = useChatStore((state) => state.ingestProcessEvent);
|
||||
const statusCheckCleanupRef = React.useRef<(() => void) | null>(null);
|
||||
@ -63,14 +63,14 @@ export function AppRuntimeBridge() {
|
||||
statusCheckInFlightRef.current = true;
|
||||
try {
|
||||
await getStatus();
|
||||
setNanobotReady(true);
|
||||
setBeaverReady(true);
|
||||
} catch {
|
||||
setNanobotReady(false);
|
||||
setBeaverReady(false);
|
||||
} finally {
|
||||
statusCheckInFlightRef.current = false;
|
||||
}
|
||||
});
|
||||
}, [setNanobotReady]);
|
||||
}, [setBeaverReady]);
|
||||
|
||||
React.useEffect(() => {
|
||||
void loadSessions();
|
||||
@ -89,7 +89,7 @@ export function AppRuntimeBridge() {
|
||||
} else {
|
||||
statusCheckCleanupRef.current?.();
|
||||
statusCheckCleanupRef.current = null;
|
||||
setNanobotReady(null);
|
||||
setBeaverReady(null);
|
||||
}
|
||||
});
|
||||
|
||||
@ -98,7 +98,7 @@ export function AppRuntimeBridge() {
|
||||
statusCheckCleanupRef.current = null;
|
||||
unsubStatus();
|
||||
};
|
||||
}, [scheduleStatusCheck, setNanobotReady, setWsStatus]);
|
||||
}, [scheduleStatusCheck, setBeaverReady, setWsStatus]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const unsubMessage = wsManager.onMessage((data) => {
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import { Bell, Bot, ChevronDown, FolderOpen, ListTodo, LogOut, Mail, MessageSquare, PackageOpen, Puzzle, Settings, Store, Wrench } from 'lucide-react';
|
||||
import { Bell, Bot, ChevronDown, FolderOpen, ListTodo, LogOut, Mail, MessageSquare, Puzzle, Settings, Store, Wrench } from 'lucide-react';
|
||||
import { logout } from '@/lib/api';
|
||||
import { LanguageSwitcher } from '@/components/LanguageSwitcher';
|
||||
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
|
||||
@ -15,7 +15,7 @@ import { useAppI18n } from '@/lib/i18n/provider';
|
||||
import { useChatStore } from '@/lib/store';
|
||||
|
||||
type NavItem = {
|
||||
key: 'chat' | 'tasks' | 'notifications' | 'skills' | 'files' | 'tools' | 'agents' | 'outlook' | 'marketplace' | 'plugins' | 'settings';
|
||||
key: 'chat' | 'tasks' | 'notifications' | 'skills' | 'files' | 'tools' | 'agents' | 'outlook' | 'marketplace' | 'settings';
|
||||
href: string;
|
||||
icon: React.ComponentType<{ className?: string }>;
|
||||
matchPrefixes?: string[];
|
||||
@ -23,7 +23,7 @@ type NavItem = {
|
||||
|
||||
const NAV_ITEMS: NavItem[] = [
|
||||
{ key: 'chat', href: '/', icon: MessageSquare },
|
||||
{ key: 'tasks', href: '/tasks', icon: ListTodo, matchPrefixes: ['/tasks', '/office', '/cron'] },
|
||||
{ key: 'tasks', href: '/tasks', icon: ListTodo, matchPrefixes: ['/tasks', '/cron'] },
|
||||
{ key: 'notifications', href: '/notifications', icon: Bell, matchPrefixes: ['/notifications'] },
|
||||
{ key: 'skills', href: '/skills', icon: Puzzle },
|
||||
{ key: 'files', href: '/files', icon: FolderOpen, matchPrefixes: ['/files'] },
|
||||
@ -31,7 +31,6 @@ const NAV_ITEMS: NavItem[] = [
|
||||
{ key: 'agents', href: '/agents', icon: Bot, matchPrefixes: ['/agents'] },
|
||||
{ key: 'outlook', href: '/outlook', icon: Mail, matchPrefixes: ['/outlook'] },
|
||||
{ key: 'marketplace', href: '/marketplace', icon: Store, matchPrefixes: ['/marketplace'] },
|
||||
{ key: 'plugins', href: '/plugins', icon: PackageOpen, matchPrefixes: ['/plugins'] },
|
||||
{
|
||||
key: 'settings',
|
||||
href: '/settings',
|
||||
@ -43,12 +42,12 @@ const NAV_ITEMS: NavItem[] = [
|
||||
function ConnectionDot() {
|
||||
const { locale } = useAppI18n();
|
||||
const wsStatus = useChatStore((s) => s.wsStatus);
|
||||
const nanobotReady = useChatStore((s) => s.nanobotReady);
|
||||
const beaverReady = useChatStore((s) => s.beaverReady);
|
||||
|
||||
const isOnline = wsStatus === 'connected' && nanobotReady === true;
|
||||
const isChecking = wsStatus === 'connected' && nanobotReady === null;
|
||||
const isOnline = wsStatus === 'connected' && beaverReady === true;
|
||||
const isChecking = wsStatus === 'connected' && beaverReady === null;
|
||||
const isConnecting = wsStatus === 'connecting' || isChecking;
|
||||
const isOffline = wsStatus === 'disconnected' || (wsStatus === 'connected' && nanobotReady === false);
|
||||
const isOffline = wsStatus === 'disconnected' || (wsStatus === 'connected' && beaverReady === false);
|
||||
|
||||
const color = isOnline
|
||||
? 'bg-[#869683]'
|
||||
@ -56,7 +55,7 @@ function ConnectionDot() {
|
||||
? 'bg-[#8B7E77]'
|
||||
: 'bg-[#5F5550]';
|
||||
|
||||
const label = appConnectionStatusLabel(wsStatus, nanobotReady, locale);
|
||||
const label = appConnectionStatusLabel(wsStatus, beaverReady, locale);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
@ -84,7 +83,6 @@ const Header = () => {
|
||||
if (key === 'agents') return pickAppText(locale, '智能体', 'Agents');
|
||||
if (key === 'outlook') return 'Outlook';
|
||||
if (key === 'marketplace') return pickAppText(locale, '市场', 'Marketplace');
|
||||
if (key === 'plugins') return pickAppText(locale, '插件', 'Plugins');
|
||||
return pickAppText(locale, '配置', 'Settings');
|
||||
}, [locale]);
|
||||
|
||||
|
||||
@ -1,11 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { CheckCircle2, Loader2, Sparkles, Square } from 'lucide-react';
|
||||
import { CheckCircle2, Loader2, Sparkles } from 'lucide-react';
|
||||
|
||||
import type { ProcessArtifact, ProcessEvent, ProcessRun } from '@/types';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { appArtifactPreview, appFeedRoleLabel, appStatusLabel } from '@/lib/i18n/common';
|
||||
import { pickAppText } from '@/lib/i18n/core';
|
||||
import { useAppI18n } from '@/lib/i18n/provider';
|
||||
@ -410,7 +409,6 @@ export function AgentTeamBlock({
|
||||
artifacts,
|
||||
selectedRunId,
|
||||
onSelectRun,
|
||||
onCancelRun,
|
||||
}: {
|
||||
rootRun: ProcessRun;
|
||||
memberRuns: ProcessRun[];
|
||||
@ -418,7 +416,6 @@ export function AgentTeamBlock({
|
||||
artifacts: ProcessArtifact[];
|
||||
selectedRunId: string | null;
|
||||
onSelectRun: (runId: string) => void;
|
||||
onCancelRun: (runId: string) => void;
|
||||
}) {
|
||||
const { locale } = useAppI18n();
|
||||
const phases = useRunCardPhases(memberRuns);
|
||||
@ -435,10 +432,6 @@ export function AgentTeamBlock({
|
||||
const terminalRuns = sortedRuns.filter((run) => TERMINAL_STATUSES.has(run.status));
|
||||
const collapsedRuns = sortedRuns.filter((run) => phases[run.run_id] === 'collapsed');
|
||||
const liveCount = liveRuns.filter((run) => !TERMINAL_STATUSES.has(run.status)).length;
|
||||
const canCancelRoot =
|
||||
!rootRun.parent_run_id &&
|
||||
(rootRun.status === 'running' || rootRun.status === 'waiting');
|
||||
|
||||
if (liveRuns.length === 0 && terminalRuns.length > 0) {
|
||||
return (
|
||||
<div className="inline-flex max-w-full flex-wrap items-start gap-2 rounded-2xl border border-border/60 bg-card/35 px-3 py-3 backdrop-blur-sm">
|
||||
@ -486,12 +479,6 @@ export function AgentTeamBlock({
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{canCancelRoot && (
|
||||
<Button variant="outline" size="sm" className="bg-background/60" onClick={() => onCancelRun(rootRun.run_id)}>
|
||||
<Square className="mr-1.5 h-3.5 w-3.5" />
|
||||
{pickAppText(locale, '取消', 'Cancel')}
|
||||
</Button>
|
||||
)}
|
||||
<Badge variant="outline" className="border-border/70 bg-background/55 text-foreground/85">
|
||||
{pickAppText(locale, `${memberRuns.length} 个子任务`, `${memberRuns.length} subtasks`)}
|
||||
</Badge>
|
||||
|
||||
@ -15,7 +15,6 @@ export function ChatWorkbench({
|
||||
processArtifacts,
|
||||
selectedRunId,
|
||||
onSelectRun,
|
||||
onCancelRun,
|
||||
onFeedback,
|
||||
}: {
|
||||
messages: ChatMessage[];
|
||||
@ -27,7 +26,6 @@ export function ChatWorkbench({
|
||||
processArtifacts: ProcessArtifact[];
|
||||
selectedRunId: string | null;
|
||||
onSelectRun: (runId: string) => void;
|
||||
onCancelRun: (runId: string) => void;
|
||||
onFeedback: (runId: string, feedbackType: 'satisfied' | 'revise' | 'abandon', comment?: string) => void;
|
||||
}) {
|
||||
return (
|
||||
@ -42,7 +40,6 @@ export function ChatWorkbench({
|
||||
processArtifacts={processArtifacts}
|
||||
selectedRunId={selectedRunId}
|
||||
onSelectRun={onSelectRun}
|
||||
onCancelRun={onCancelRun}
|
||||
onFeedback={onFeedback}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -329,7 +329,6 @@ export function MessageList({
|
||||
processArtifacts,
|
||||
selectedRunId,
|
||||
onSelectRun,
|
||||
onCancelRun,
|
||||
onFeedback,
|
||||
}: {
|
||||
messages: ChatMessage[];
|
||||
@ -341,7 +340,6 @@ export function MessageList({
|
||||
processArtifacts: ProcessArtifact[];
|
||||
selectedRunId: string | null;
|
||||
onSelectRun: (runId: string) => void;
|
||||
onCancelRun: (runId: string) => void;
|
||||
onFeedback: (runId: string, feedbackType: 'satisfied' | 'revise' | 'abandon', comment?: string) => void;
|
||||
}) {
|
||||
const { locale } = useAppI18n();
|
||||
@ -411,7 +409,6 @@ export function MessageList({
|
||||
artifacts={processArtifacts}
|
||||
selectedRunId={selectedRunId}
|
||||
onSelectRun={onSelectRun}
|
||||
onCancelRun={onCancelRun}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
|
||||
@ -3,14 +3,14 @@
|
||||
import { useAppI18n } from '@/lib/i18n/provider';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { OfficeTaskStatus, OfficeZoneView } from '@/lib/office';
|
||||
import { officeTaskStatusLabel } from '@/lib/office';
|
||||
import type { TaskRuntimeStatus } from '@/lib/task-runtime';
|
||||
import { taskRuntimeStatusLabel } from '@/lib/task-runtime';
|
||||
|
||||
export function OfficeStatusBadge({
|
||||
export function TaskRuntimeStatusBadge({
|
||||
status,
|
||||
className,
|
||||
}: {
|
||||
status: OfficeTaskStatus;
|
||||
status: TaskRuntimeStatus;
|
||||
className?: string;
|
||||
}) {
|
||||
const { locale } = useAppI18n();
|
||||
@ -30,12 +30,12 @@ export function OfficeStatusBadge({
|
||||
className
|
||||
)}
|
||||
>
|
||||
{officeTaskStatusLabel(status, locale)}
|
||||
{taskRuntimeStatusLabel(status, locale)}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
export function formatOfficeTime(value?: string | null, locale: 'zh-CN' | 'en-US' = 'zh-CN'): string {
|
||||
export function formatTaskRuntimeTime(value?: string | null, locale: 'zh-CN' | 'en-US' = 'zh-CN'): string {
|
||||
if (!value) return '-';
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return value;
|
||||
@ -47,7 +47,7 @@ export function formatOfficeTime(value?: string | null, locale: 'zh-CN' | 'en-US
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
export function formatOfficeDuration(durationMs: number | null, locale: 'zh-CN' | 'en-US' = 'zh-CN'): string {
|
||||
export function formatTaskRuntimeDuration(durationMs: number | null, locale: 'zh-CN' | 'en-US' = 'zh-CN'): string {
|
||||
if (durationMs === null || durationMs < 0) return '-';
|
||||
if (durationMs < 1000) return locale === 'en-US' ? '<1s' : '<1秒';
|
||||
|
||||
@ -65,15 +65,3 @@ export function progressPercent(value: number | null, max: number | null): numbe
|
||||
if (value === null || max === null || max <= 0) return 0;
|
||||
return Math.max(0, Math.min(100, Math.round((value / max) * 100)));
|
||||
}
|
||||
|
||||
export function zonePanelClassName(zone: OfficeZoneView): string {
|
||||
return cn(
|
||||
'relative min-h-[220px] overflow-hidden rounded-2xl border p-4 shadow-sm',
|
||||
'before:pointer-events-none before:absolute before:inset-0 before:bg-[radial-gradient(circle_at_top_left,rgba(255,255,255,0.9),transparent_40%)]',
|
||||
zone.tone === 'info' && 'border-[#BCC4CE] bg-[#E4E7EB]/70',
|
||||
zone.tone === 'warn' && 'border-[#B8AEA8] bg-[#E7E2DE]/70',
|
||||
zone.tone === 'danger' && 'border-[#B8AEA8] bg-[#E7E2DE]/80',
|
||||
zone.tone === 'success' && 'border-[#B7C2B5] bg-[#E3E8E2]/75',
|
||||
zone.tone === 'neutral' && 'border-border bg-card'
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,9 +1,6 @@
|
||||
// Nanobot API client — single-user direct mode.
|
||||
// Beaver API client - single-user direct mode.
|
||||
|
||||
import type {
|
||||
AuthzBackendRecord,
|
||||
AuthzChannelSettings,
|
||||
AuthzRegisterBackendResponse,
|
||||
AuthzStatus,
|
||||
AuthUser,
|
||||
ActiveTask,
|
||||
@ -12,11 +9,8 @@ import type {
|
||||
ChatMessage,
|
||||
CronJob,
|
||||
FileAttachment,
|
||||
Marketplace,
|
||||
MarketplacePlugin,
|
||||
NotificationDetail,
|
||||
NotificationRun,
|
||||
PluginInfo,
|
||||
ProviderConfigPayload,
|
||||
Session,
|
||||
SessionDetail,
|
||||
@ -33,7 +27,6 @@ import type {
|
||||
SkillHubVersionsResponse,
|
||||
SkillLearningCandidate,
|
||||
SkillReviewRecord,
|
||||
SlashCommand,
|
||||
SessionProcessProjection,
|
||||
SystemStatus,
|
||||
TokenResponse,
|
||||
@ -55,8 +48,8 @@ import { getCurrentAppLocale, pickAppText } from '@/lib/i18n/core';
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL?.trim();
|
||||
const WS_URL = process.env.NEXT_PUBLIC_WS_URL?.trim();
|
||||
const DEFAULT_API_URL = 'http://127.0.0.1:18080';
|
||||
const ACCESS_TOKEN_KEY = 'nanobot_access_token';
|
||||
const REFRESH_TOKEN_KEY = 'nanobot_refresh_token';
|
||||
const ACCESS_TOKEN_KEY = 'beaver_access_token';
|
||||
const REFRESH_TOKEN_KEY = 'beaver_refresh_token';
|
||||
const REQUEST_TIMEOUT_MS = 8000;
|
||||
const OUTLOOK_REQUEST_TIMEOUT_MS = 45000;
|
||||
const SKILL_LEARNING_REQUEST_TIMEOUT_MS = 120000;
|
||||
@ -636,16 +629,6 @@ export async function updateProviderConfig(
|
||||
});
|
||||
}
|
||||
|
||||
export async function restartSystem(): Promise<{
|
||||
ok: boolean;
|
||||
restarting: boolean;
|
||||
detail: string;
|
||||
}> {
|
||||
return fetchJSON('/api/system/restart', {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Cron (proxied)
|
||||
// ---------------------------------------------------------------------------
|
||||
@ -861,14 +844,6 @@ export async function rollbackPublishedSkill(
|
||||
});
|
||||
}
|
||||
|
||||
export async function listCommands(): Promise<SlashCommand[]> {
|
||||
return fetchJSON('/api/commands');
|
||||
}
|
||||
|
||||
export async function listPlugins(): Promise<PluginInfo[]> {
|
||||
return fetchJSON('/api/plugins');
|
||||
}
|
||||
|
||||
export async function listAgents(): Promise<UiAgentDescriptor[]> {
|
||||
return fetchJSON('/api/agents');
|
||||
}
|
||||
@ -957,18 +932,6 @@ export async function deleteSubagent(subagentId: string): Promise<void> {
|
||||
});
|
||||
}
|
||||
|
||||
export async function cancelDelegation(runId: string): Promise<{ ok: boolean; run_id: string }> {
|
||||
return fetchJSON(`/api/delegations/${encodeURIComponent(runId)}/cancel`, {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
export async function retryDelegation(runId: string): Promise<{ ok: boolean; run_id: string }> {
|
||||
return fetchJSON(`/api/delegations/${encodeURIComponent(runId)}/retry`, {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
export async function listMcpServers(): Promise<UiMcpServerDescriptor[]> {
|
||||
return fetchJSON('/api/mcp/servers');
|
||||
}
|
||||
@ -1022,122 +985,6 @@ export async function getAuthzStatus(): Promise<AuthzStatus> {
|
||||
return fetchJSON('/api/authz/status');
|
||||
}
|
||||
|
||||
export async function bindLocalBackendIdentity(payload: {
|
||||
backend_id: string;
|
||||
client_id: string;
|
||||
client_secret: string;
|
||||
name?: string;
|
||||
public_base_url?: string;
|
||||
authz_base_url?: string;
|
||||
authz_enabled?: boolean;
|
||||
}): Promise<Record<string, unknown>> {
|
||||
return fetchJSON('/api/authz/local-backend/bind', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
}
|
||||
|
||||
export async function listAuthzBackends(): Promise<AuthzBackendRecord[]> {
|
||||
return fetchJSON('/api/authz/backends');
|
||||
}
|
||||
|
||||
export async function registerAuthzBackend(payload: {
|
||||
name?: string;
|
||||
backend_id?: string;
|
||||
base_url?: string;
|
||||
save_to_backend?: boolean;
|
||||
authz_base_url?: string;
|
||||
}): Promise<AuthzRegisterBackendResponse> {
|
||||
return fetchJSON('/api/authz/backends/register', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
}
|
||||
|
||||
export async function getAuthzBackend(backendId: string): Promise<AuthzBackendRecord> {
|
||||
return fetchJSON(`/api/authz/backends/${encodeURIComponent(backendId)}`);
|
||||
}
|
||||
|
||||
export async function enableAuthzBackend(backendId: string): Promise<AuthzBackendRecord> {
|
||||
return fetchJSON(`/api/authz/backends/${encodeURIComponent(backendId)}/enable`, {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
export async function disableAuthzBackend(backendId: string): Promise<AuthzBackendRecord> {
|
||||
return fetchJSON(`/api/authz/backends/${encodeURIComponent(backendId)}/disable`, {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
export async function rotateAuthzBackendSecret(backendId: string): Promise<Record<string, unknown>> {
|
||||
return fetchJSON(`/api/authz/backends/${encodeURIComponent(backendId)}/rotate-secret`, {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
export async function getAuthzBackendPermissions(backendId: string): Promise<Record<string, unknown>> {
|
||||
return fetchJSON(`/api/authz/backends/${encodeURIComponent(backendId)}/permissions`);
|
||||
}
|
||||
|
||||
export async function setAuthzBackendPermissions(
|
||||
backendId: string,
|
||||
payload: Record<string, unknown>
|
||||
): Promise<Record<string, unknown>> {
|
||||
return fetchJSON(`/api/authz/backends/${encodeURIComponent(backendId)}/permissions`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
}
|
||||
|
||||
export async function getAuthzBackendOutlookSettings(backendId: string): Promise<Record<string, unknown>> {
|
||||
return fetchJSON(`/api/authz/backends/${encodeURIComponent(backendId)}/settings/outlook`);
|
||||
}
|
||||
|
||||
export async function setAuthzBackendOutlookSettings(
|
||||
backendId: string,
|
||||
payload: Record<string, unknown>
|
||||
): Promise<Record<string, unknown>> {
|
||||
return fetchJSON(`/api/authz/backends/${encodeURIComponent(backendId)}/settings/outlook`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteAuthzBackendOutlookSettings(backendId: string): Promise<Record<string, unknown>> {
|
||||
return fetchJSON(`/api/authz/backends/${encodeURIComponent(backendId)}/settings/outlook`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
||||
export async function listAuthzChannelSettings(): Promise<Record<string, AuthzChannelSettings>> {
|
||||
return fetchJSON('/api/authz/channel-settings');
|
||||
}
|
||||
|
||||
export async function getAuthzChannelSettings(channelId: string): Promise<AuthzChannelSettings> {
|
||||
return fetchJSON(`/api/authz/channel-settings/${encodeURIComponent(channelId)}`);
|
||||
}
|
||||
|
||||
export async function setAuthzChannelSettings(
|
||||
channelId: string,
|
||||
payload: {
|
||||
configured?: boolean;
|
||||
config?: Record<string, unknown>;
|
||||
secrets?: Record<string, unknown>;
|
||||
}
|
||||
): Promise<AuthzChannelSettings> {
|
||||
return fetchJSON(`/api/authz/channel-settings/${encodeURIComponent(channelId)}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteAuthzChannelSettings(channelId: string): Promise<Record<string, unknown>> {
|
||||
return fetchJSON(`/api/authz/channel-settings/${encodeURIComponent(channelId)}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
||||
export async function testMcpServer(serverId: string): Promise<Record<string, unknown>> {
|
||||
return fetchJSON(`/api/mcp/servers/${encodeURIComponent(serverId)}/test`, {
|
||||
method: 'POST',
|
||||
@ -1282,10 +1129,6 @@ export async function uploadSkill(file: File): Promise<Skill> {
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function migrateSkills(): Promise<{ included: Array<Record<string, unknown>>; skipped: Array<Record<string, unknown>> }> {
|
||||
return fetchJSON('/api/skills/migrate', { method: 'POST', timeoutMs: 45000 });
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SkillHub marketplace
|
||||
// ---------------------------------------------------------------------------
|
||||
@ -1356,50 +1199,6 @@ export async function installSkillHubSkill(
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Marketplace (proxied)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function listMarketplaces(): Promise<Marketplace[]> {
|
||||
return fetchJSON('/api/marketplaces');
|
||||
}
|
||||
|
||||
export async function addMarketplace(source: string): Promise<Marketplace> {
|
||||
return fetchJSON('/api/marketplaces', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ source }),
|
||||
});
|
||||
}
|
||||
|
||||
export async function removeMarketplace(name: string): Promise<void> {
|
||||
await fetchJSON(`/api/marketplaces/${encodeURIComponent(name)}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateMarketplace(name: string): Promise<Marketplace> {
|
||||
return fetchJSON(`/api/marketplaces/${encodeURIComponent(name)}/update`, {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
export async function listMarketplacePlugins(name: string): Promise<MarketplacePlugin[]> {
|
||||
return fetchJSON(`/api/marketplaces/${encodeURIComponent(name)}/plugins`);
|
||||
}
|
||||
|
||||
export async function installMarketplacePlugin(marketplaceName: string, pluginName: string): Promise<void> {
|
||||
await fetchJSON(
|
||||
`/api/marketplaces/${encodeURIComponent(marketplaceName)}/plugins/${encodeURIComponent(pluginName)}/install`,
|
||||
{ method: 'POST' }
|
||||
);
|
||||
}
|
||||
|
||||
export async function uninstallPlugin(pluginName: string): Promise<void> {
|
||||
await fetchJSON(`/api/plugins/${encodeURIComponent(pluginName)}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Files (proxied)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import type { OfficeTaskStatus } from '@/lib/office';
|
||||
import type { TaskRuntimeStatus } from '@/lib/task-runtime';
|
||||
import type { ProcessArtifact, ProcessRun } from '@/types';
|
||||
import { getCurrentAppLocale, pickAppText, type AppLocale } from '@/lib/i18n/core';
|
||||
import type { WsStatus } from '@/lib/api';
|
||||
|
||||
export function appStatusLabel(
|
||||
status: ProcessRun['status'] | OfficeTaskStatus | string,
|
||||
status: ProcessRun['status'] | TaskRuntimeStatus | string,
|
||||
locale: AppLocale = getCurrentAppLocale()
|
||||
): string {
|
||||
if (status === 'queued') return pickAppText(locale, '排队中', 'Queued');
|
||||
@ -63,12 +63,12 @@ export function appArtifactPreview(artifact: ProcessArtifact, locale: AppLocale
|
||||
|
||||
export function appConnectionStatusLabel(
|
||||
wsStatus: WsStatus,
|
||||
nanobotReady: boolean | null,
|
||||
beaverReady: boolean | null,
|
||||
locale: AppLocale = getCurrentAppLocale()
|
||||
): string {
|
||||
const isOnline = wsStatus === 'connected' && nanobotReady === true;
|
||||
const isChecking = wsStatus === 'connected' && nanobotReady === null;
|
||||
const isOffline = wsStatus === 'disconnected' || (wsStatus === 'connected' && nanobotReady === false);
|
||||
const isOnline = wsStatus === 'connected' && beaverReady === true;
|
||||
const isChecking = wsStatus === 'connected' && beaverReady === null;
|
||||
const isOffline = wsStatus === 'disconnected' || (wsStatus === 'connected' && beaverReady === false);
|
||||
|
||||
if (isOnline) return pickAppText(locale, '已连接', 'Connected');
|
||||
if (isChecking) return pickAppText(locale, '检查中', 'Checking');
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
export const APP_LOCALE_COOKIE = 'nanobot_locale';
|
||||
export const APP_LOCALE_STORAGE_KEY = 'nanobot_locale';
|
||||
export const APP_LOCALE_COOKIE = 'beaver_locale';
|
||||
export const APP_LOCALE_STORAGE_KEY = 'beaver_locale';
|
||||
|
||||
export const APP_LOCALES = ['zh-CN', 'en-US'] as const;
|
||||
|
||||
|
||||
@ -14,7 +14,7 @@ import type {
|
||||
} from '@/types';
|
||||
import type { WsStatus } from '@/lib/api';
|
||||
|
||||
const ACTIVE_SESSION_STORAGE_KEY = 'nanobot_active_session_id';
|
||||
const ACTIVE_SESSION_STORAGE_KEY = 'beaver_active_session_id';
|
||||
|
||||
function getInitialSessionId(): string {
|
||||
if (typeof window === 'undefined') {
|
||||
@ -40,7 +40,7 @@ interface ChatStore {
|
||||
streamingContent: string;
|
||||
wsStatus: WsStatus;
|
||||
isThinking: boolean;
|
||||
nanobotReady: boolean | null;
|
||||
beaverReady: boolean | null;
|
||||
sessions: Session[];
|
||||
processRuns: ProcessRun[];
|
||||
processEvents: ProcessEvent[];
|
||||
@ -68,7 +68,7 @@ interface ChatStore {
|
||||
clearMessages: () => void;
|
||||
setWsStatus: (status: WsStatus) => void;
|
||||
setIsThinking: (thinking: boolean) => void;
|
||||
setNanobotReady: (ready: boolean | null) => void;
|
||||
setBeaverReady: (ready: boolean | null) => void;
|
||||
resetProcessState: () => void;
|
||||
ingestProcessEvent: (event: ProcessWsEvent) => void;
|
||||
setSessionProcess: (sessionId: string, projection: SessionProcessProjection) => void;
|
||||
@ -135,7 +135,7 @@ export const useChatStore = create<ChatStore>((set) => ({
|
||||
streamingContent: '',
|
||||
wsStatus: 'disconnected',
|
||||
isThinking: false,
|
||||
nanobotReady: null,
|
||||
beaverReady: null,
|
||||
sessions: [],
|
||||
processRuns: [],
|
||||
processEvents: [],
|
||||
@ -175,7 +175,7 @@ export const useChatStore = create<ChatStore>((set) => ({
|
||||
clearMessages: () => set({ messages: [], streamingContent: '' }),
|
||||
setWsStatus: (status) => set({ wsStatus: status }),
|
||||
setIsThinking: (thinking) => set({ isThinking: thinking }),
|
||||
setNanobotReady: (ready) => set({ nanobotReady: ready }),
|
||||
setBeaverReady: (ready) => set({ beaverReady: ready }),
|
||||
resetProcessState: () =>
|
||||
set({
|
||||
processRuns: [],
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { buildOfficeTaskList, buildOfficeView } from '@/lib/office';
|
||||
import { buildTaskRuntimeView } from '@/lib/task-runtime';
|
||||
import type { ProcessArtifact, ProcessEvent, ProcessRun, Session } from '@/types';
|
||||
|
||||
describe('office view builders', () => {
|
||||
describe('runtime view builders', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date('2026-03-24T12:00:00.000Z'));
|
||||
@ -13,7 +13,7 @@ describe('office view builders', () => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('builds an office view from a root run tree', () => {
|
||||
it('builds a runtime view from a root run tree', () => {
|
||||
const sessions: Session[] = [
|
||||
{
|
||||
key: 'web:default',
|
||||
@ -118,26 +118,21 @@ describe('office view builders', () => {
|
||||
},
|
||||
];
|
||||
|
||||
const office = buildOfficeView('run-root', {
|
||||
const runtime = buildTaskRuntimeView('run-root', {
|
||||
sessions,
|
||||
processRuns,
|
||||
processEvents,
|
||||
processArtifacts,
|
||||
});
|
||||
|
||||
expect(office).not.toBeNull();
|
||||
expect(office?.taskId).toBe('run-root');
|
||||
expect(office?.title).toBe('整理竞品研究并给出结论');
|
||||
expect(office?.sourceSessionLabel).toBe('需求讨论');
|
||||
expect(office?.members).toHaveLength(3);
|
||||
expect(office?.tasks).toHaveLength(3);
|
||||
expect(office?.assignments).toHaveLength(1);
|
||||
expect(office?.progress.label).toBe('已完成子任务 1 / 3');
|
||||
expect(office?.currentStageLabel).toBe('分析结果');
|
||||
expect(office?.stats.artifactCount).toBe(1);
|
||||
expect(office?.zones.find((zone) => zone.id === 'workspace')?.memberIds).toContain('main-agent');
|
||||
expect(office?.zones.find((zone) => zone.id === 'collab')?.memberIds).toContain('research-agent');
|
||||
expect(office?.zones.find((zone) => zone.id === 'research')?.memberIds).toContain('search-mcp');
|
||||
expect(runtime).not.toBeNull();
|
||||
expect(runtime?.taskId).toBe('run-root');
|
||||
expect(runtime?.title).toBe('整理竞品研究并给出结论');
|
||||
expect(runtime?.sourceSessionLabel).toBe('需求讨论');
|
||||
expect(runtime?.tasks).toHaveLength(3);
|
||||
expect(runtime?.progress.label).toBe('已完成子任务 1 / 3');
|
||||
expect(runtime?.stats.artifactCount).toBe(1);
|
||||
expect(runtime?.stats.alertCount).toBe(0);
|
||||
});
|
||||
|
||||
it('marks stale waiting tasks as blocked and emits alerts', () => {
|
||||
@ -155,109 +150,14 @@ describe('office view builders', () => {
|
||||
},
|
||||
];
|
||||
|
||||
const office = buildOfficeView('run-blocked', {
|
||||
const runtime = buildTaskRuntimeView('run-blocked', {
|
||||
sessions: [],
|
||||
processRuns,
|
||||
processEvents: [],
|
||||
processArtifacts: [],
|
||||
});
|
||||
|
||||
expect(office?.status).toBe('blocked');
|
||||
expect(office?.alerts).toHaveLength(1);
|
||||
expect(office?.alerts[0].level).toBe('warn');
|
||||
expect(office?.members[0].zoneId).toBe('collab');
|
||||
});
|
||||
|
||||
it('builds a filtered task list and sorts active tasks ahead of finished ones', () => {
|
||||
const sessions: Session[] = [
|
||||
{ key: 'web:alpha', path: 'Alpha Session' },
|
||||
{ key: 'web:beta', path: 'Beta Session' },
|
||||
];
|
||||
|
||||
const processRuns: ProcessRun[] = [
|
||||
{
|
||||
run_id: 'run-active',
|
||||
parent_run_id: null,
|
||||
session_id: 'web:alpha',
|
||||
actor_type: 'agent',
|
||||
actor_id: 'agent-a',
|
||||
actor_name: 'Agent A',
|
||||
title: '执行活跃任务',
|
||||
status: 'running',
|
||||
started_at: '2026-03-24T11:20:00.000Z',
|
||||
},
|
||||
{
|
||||
run_id: 'run-done',
|
||||
parent_run_id: null,
|
||||
session_id: 'web:alpha',
|
||||
actor_type: 'agent',
|
||||
actor_id: 'agent-b',
|
||||
actor_name: 'Agent B',
|
||||
title: '已结束任务',
|
||||
status: 'done',
|
||||
started_at: '2026-03-24T10:00:00.000Z',
|
||||
finished_at: '2026-03-24T10:08:00.000Z',
|
||||
},
|
||||
{
|
||||
run_id: 'run-other-session',
|
||||
parent_run_id: null,
|
||||
session_id: 'web:beta',
|
||||
actor_type: 'agent',
|
||||
actor_id: 'agent-c',
|
||||
actor_name: 'Agent C',
|
||||
title: '其他会话任务',
|
||||
status: 'running',
|
||||
started_at: '2026-03-24T11:00:00.000Z',
|
||||
},
|
||||
];
|
||||
|
||||
const tasks = buildOfficeTaskList({
|
||||
sessionId: 'web:alpha',
|
||||
sessions,
|
||||
processRuns,
|
||||
processEvents: [],
|
||||
processArtifacts: [],
|
||||
});
|
||||
|
||||
expect(tasks).toHaveLength(2);
|
||||
expect(tasks[0].taskId).toBe('run-active');
|
||||
expect(tasks[1].taskId).toBe('run-done');
|
||||
expect(tasks[0].sessionLabel).toBe('Alpha Session');
|
||||
});
|
||||
|
||||
it('keeps office tasks visible when the root run inherits session from descendants', () => {
|
||||
const tasks = buildOfficeTaskList({
|
||||
sessionId: 'web:alpha',
|
||||
sessions: [{ key: 'web:alpha', path: 'Alpha Session' }],
|
||||
processRuns: [
|
||||
{
|
||||
run_id: 'run-root-no-session',
|
||||
parent_run_id: null,
|
||||
actor_type: 'agent',
|
||||
actor_id: 'agent-a',
|
||||
actor_name: 'Agent A',
|
||||
title: '根任务缺少会话字段',
|
||||
status: 'running',
|
||||
started_at: '2026-03-24T11:20:00.000Z',
|
||||
},
|
||||
{
|
||||
run_id: 'run-child-with-session',
|
||||
parent_run_id: 'run-root-no-session',
|
||||
session_id: 'web:alpha',
|
||||
actor_type: 'agent',
|
||||
actor_id: 'agent-b',
|
||||
actor_name: 'Agent B',
|
||||
title: '子任务仍带着会话字段',
|
||||
status: 'running',
|
||||
started_at: '2026-03-24T11:21:00.000Z',
|
||||
},
|
||||
],
|
||||
processEvents: [],
|
||||
processArtifacts: [],
|
||||
});
|
||||
|
||||
expect(tasks).toHaveLength(1);
|
||||
expect(tasks[0].taskId).toBe('run-root-no-session');
|
||||
expect(tasks[0].sessionId).toBe('web:alpha');
|
||||
expect(runtime?.status).toBe('blocked');
|
||||
expect(runtime?.stats.alertCount).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import type {
|
||||
ProcessActorType,
|
||||
ProcessArtifact,
|
||||
ProcessEvent,
|
||||
ProcessRun,
|
||||
@ -8,108 +7,42 @@ import type {
|
||||
} from '@/types';
|
||||
import { getCurrentAppLocale, pickAppText, type AppLocale } from '@/lib/i18n/core';
|
||||
|
||||
const TERMINAL_STATUSES = new Set<OfficeTaskStatus>(['done', 'error', 'cancelled']);
|
||||
const TERMINAL_STATUSES = new Set<TaskRuntimeStatus>(['done', 'error', 'cancelled']);
|
||||
const STALE_WAITING_MS = 2 * 60 * 1000;
|
||||
|
||||
export type OfficeTaskStatus = ProcessRunStatus | 'blocked';
|
||||
export type TaskRuntimeStatus = ProcessRunStatus | 'blocked';
|
||||
|
||||
export type OfficeZoneId =
|
||||
| 'reception'
|
||||
| 'workspace'
|
||||
| 'collab'
|
||||
| 'research'
|
||||
| 'alert'
|
||||
| 'done';
|
||||
|
||||
export interface OfficeProgressView {
|
||||
mode: 'stage' | 'ratio' | 'status';
|
||||
export interface TaskRuntimeProgressView {
|
||||
label: string;
|
||||
value: number | null;
|
||||
max: number | null;
|
||||
stageLabel: string | null;
|
||||
}
|
||||
|
||||
export interface OfficeStatsView {
|
||||
export interface TaskRuntimeStatsView {
|
||||
totalRuns: number;
|
||||
activeRuns: number;
|
||||
doneRuns: number;
|
||||
errorRuns: number;
|
||||
cancelledRuns: number;
|
||||
memberCount: number;
|
||||
artifactCount: number;
|
||||
alertCount: number;
|
||||
}
|
||||
|
||||
export interface OfficeZoneView {
|
||||
id: OfficeZoneId;
|
||||
label: string;
|
||||
memberIds: string[];
|
||||
taskIds: string[];
|
||||
tone: 'neutral' | 'info' | 'warn' | 'danger' | 'success';
|
||||
}
|
||||
|
||||
export interface OfficeMemberView {
|
||||
memberId: string;
|
||||
actorId: string;
|
||||
actorName: string;
|
||||
actorType: ProcessActorType;
|
||||
status: OfficeTaskStatus;
|
||||
zoneId: OfficeZoneId;
|
||||
currentRunId: string;
|
||||
currentTitle: string;
|
||||
stageLabel: string | null;
|
||||
summary: string | null;
|
||||
startedAt: string | null;
|
||||
updatedAt: string | null;
|
||||
finishedAt: string | null;
|
||||
childRunIds: string[];
|
||||
artifactCount: number;
|
||||
isPrimary: boolean;
|
||||
}
|
||||
|
||||
export interface OfficeTaskView {
|
||||
export interface TaskRuntimeNodeView {
|
||||
taskId: string;
|
||||
runId: string;
|
||||
parentRunId: string | null;
|
||||
actorId: string;
|
||||
actorName: string;
|
||||
actorType: ProcessActorType;
|
||||
title: string;
|
||||
status: OfficeTaskStatus;
|
||||
status: TaskRuntimeStatus;
|
||||
stageLabel: string | null;
|
||||
summary: string | null;
|
||||
startedAt: string;
|
||||
updatedAt: string;
|
||||
finishedAt: string | null;
|
||||
childTaskIds: string[];
|
||||
artifactCount: number;
|
||||
errorText: string | null;
|
||||
isRoot: boolean;
|
||||
}
|
||||
|
||||
export interface OfficeAssignmentView {
|
||||
ownerRunId: string;
|
||||
ownerActorName: string;
|
||||
assigneeRunIds: string[];
|
||||
assigneeActorNames: string[];
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface OfficeAlertView {
|
||||
id: string;
|
||||
level: 'info' | 'warn' | 'error';
|
||||
title: string;
|
||||
description: string | null;
|
||||
runId: string | null;
|
||||
actorId: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface OfficeView {
|
||||
officeId: string;
|
||||
export interface TaskRuntimeView {
|
||||
taskId: string;
|
||||
sessionId: string | null;
|
||||
title: string;
|
||||
status: OfficeTaskStatus;
|
||||
status: TaskRuntimeStatus;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
finishedAt: string | null;
|
||||
@ -117,38 +50,12 @@ export interface OfficeView {
|
||||
sourceSessionLabel: string;
|
||||
rootRunId: string;
|
||||
rootActorName: string;
|
||||
currentStageLabel: string | null;
|
||||
progress: OfficeProgressView;
|
||||
stats: OfficeStatsView;
|
||||
alerts: OfficeAlertView[];
|
||||
zones: OfficeZoneView[];
|
||||
members: OfficeMemberView[];
|
||||
tasks: OfficeTaskView[];
|
||||
assignments: OfficeAssignmentView[];
|
||||
detailRunIds: string[];
|
||||
progress: TaskRuntimeProgressView;
|
||||
stats: TaskRuntimeStatsView;
|
||||
tasks: TaskRuntimeNodeView[];
|
||||
}
|
||||
|
||||
export interface OfficeTaskListItem {
|
||||
officeId: string;
|
||||
taskId: string;
|
||||
sessionId: string | null;
|
||||
sessionLabel: string;
|
||||
title: string;
|
||||
status: OfficeTaskStatus;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
finishedAt: string | null;
|
||||
rootRunId: string;
|
||||
rootActorName: string;
|
||||
memberCount: number;
|
||||
activeRuns: number;
|
||||
errorCount: number;
|
||||
artifactCount: number;
|
||||
currentStageLabel: string | null;
|
||||
progress: OfficeProgressView;
|
||||
}
|
||||
|
||||
type BuildOfficeInput = {
|
||||
type BuildTaskRuntimeInput = {
|
||||
sessions: Session[];
|
||||
processRuns: ProcessRun[];
|
||||
processEvents: ProcessEvent[];
|
||||
@ -235,11 +142,6 @@ function buildChildrenMap(processRuns: ProcessRun[]): Map<string, ProcessRun[]>
|
||||
return map;
|
||||
}
|
||||
|
||||
function findRootRuns(processRuns: ProcessRun[]): ProcessRun[] {
|
||||
const runIds = new Set(processRuns.map((run) => run.run_id));
|
||||
return processRuns.filter((run) => !run.parent_run_id || !runIds.has(run.parent_run_id));
|
||||
}
|
||||
|
||||
function collectRunTree(rootRun: ProcessRun, childrenMap: Map<string, ProcessRun[]>): ProcessRun[] {
|
||||
const collected: ProcessRun[] = [];
|
||||
const stack = [rootRun];
|
||||
@ -279,7 +181,7 @@ function getRunUpdatedAt(
|
||||
function deriveStageLabel(
|
||||
run: ProcessRun,
|
||||
runEvents: ProcessEvent[],
|
||||
fallbackStatus: OfficeTaskStatus,
|
||||
fallbackStatus: TaskRuntimeStatus,
|
||||
locale: AppLocale,
|
||||
): string | null {
|
||||
const runMetadataLabel = readMetadataString(run.metadata, [
|
||||
@ -315,7 +217,7 @@ function deriveRunStatus(
|
||||
run: ProcessRun,
|
||||
updatedAt: string,
|
||||
now: number,
|
||||
): OfficeTaskStatus {
|
||||
): TaskRuntimeStatus {
|
||||
if (run.status !== 'waiting') return run.status;
|
||||
const updatedTime = toTime(updatedAt);
|
||||
if (updatedTime !== null && now - updatedTime > STALE_WAITING_MS) {
|
||||
@ -324,88 +226,16 @@ function deriveRunStatus(
|
||||
return 'waiting';
|
||||
}
|
||||
|
||||
function mapZoneId(status: OfficeTaskStatus, actorType: ProcessActorType): OfficeZoneId {
|
||||
if (status === 'queued') return 'reception';
|
||||
if (status === 'waiting' || status === 'blocked') return actorType === 'mcp' ? 'research' : 'collab';
|
||||
if (status === 'running') return actorType === 'mcp' ? 'research' : 'workspace';
|
||||
if (status === 'done') return 'collab';
|
||||
return 'alert';
|
||||
}
|
||||
|
||||
function zoneLabel(zoneId: OfficeZoneId, locale: AppLocale): string {
|
||||
if (zoneId === 'reception') return pickAppText(locale, '接待区', 'Reception');
|
||||
if (zoneId === 'workspace') return pickAppText(locale, '工位区', 'Workspace');
|
||||
if (zoneId === 'collab') return pickAppText(locale, '协作区', 'Collaboration');
|
||||
if (zoneId === 'research') return pickAppText(locale, '研究区', 'Research');
|
||||
if (zoneId === 'alert') return pickAppText(locale, '异常区', 'Alerts');
|
||||
return pickAppText(locale, '完成区', 'Completed');
|
||||
}
|
||||
|
||||
function zoneTone(zoneId: OfficeZoneId): OfficeZoneView['tone'] {
|
||||
if (zoneId === 'workspace' || zoneId === 'research') return 'info';
|
||||
if (zoneId === 'collab' || zoneId === 'reception') return 'warn';
|
||||
if (zoneId === 'alert') return 'danger';
|
||||
if (zoneId === 'done') return 'success';
|
||||
return 'neutral';
|
||||
}
|
||||
|
||||
function taskStatusPriority(status: OfficeTaskStatus): number {
|
||||
if (status === 'running') return 6;
|
||||
if (status === 'blocked') return 5;
|
||||
if (status === 'waiting') return 4;
|
||||
if (status === 'queued') return 3;
|
||||
if (status === 'error') return 2;
|
||||
if (status === 'cancelled') return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
function selectDisplayRun(
|
||||
runs: ProcessRun[],
|
||||
eventsByRun: Map<string, ProcessEvent[]>,
|
||||
artifactsByRun: Map<string, ProcessArtifact[]>,
|
||||
now: number,
|
||||
): { run: ProcessRun; status: OfficeTaskStatus; updatedAt: string } {
|
||||
const sorted = [...runs]
|
||||
.map((run) => {
|
||||
const updatedAt = getRunUpdatedAt(run, eventsByRun, artifactsByRun);
|
||||
const status = deriveRunStatus(run, updatedAt, now);
|
||||
return { run, status, updatedAt };
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const byStatus = taskStatusPriority(b.status) - taskStatusPriority(a.status);
|
||||
if (byStatus !== 0) return byStatus;
|
||||
return compareIsoDesc(a.updatedAt, b.updatedAt);
|
||||
});
|
||||
|
||||
return sorted[0];
|
||||
}
|
||||
|
||||
function deriveErrorText(run: ProcessRun, runEvents: ProcessEvent[], locale: AppLocale): string | null {
|
||||
if (run.status !== 'error') return null;
|
||||
const direct = firstString(run.summary);
|
||||
if (direct) return direct;
|
||||
const sortedEvents = [...runEvents].sort((a, b) => compareIsoDesc(a.created_at, b.created_at));
|
||||
for (const event of sortedEvents) {
|
||||
if (event.status === 'error' && firstString(event.text)) {
|
||||
return event.text!.trim();
|
||||
}
|
||||
}
|
||||
return pickAppText(locale, '任务执行失败', 'Task execution failed');
|
||||
}
|
||||
|
||||
function deriveProgress(
|
||||
rootRun: ProcessRun,
|
||||
taskRuns: ProcessRun[],
|
||||
taskViews: OfficeTaskView[],
|
||||
locale: AppLocale,
|
||||
): OfficeProgressView {
|
||||
): TaskRuntimeProgressView {
|
||||
const stageValue = readMetadataNumber(rootRun.metadata, ['stage_index', 'step_index', 'phase_index']);
|
||||
const stageMax = readMetadataNumber(rootRun.metadata, ['stage_total', 'step_total', 'phase_total']);
|
||||
const stageLabel = readMetadataString(rootRun.metadata, ['stage_label', 'stage', 'phase_label', 'step_label']);
|
||||
|
||||
if (stageValue !== null && stageMax !== null && stageMax > 0) {
|
||||
return {
|
||||
mode: 'ratio',
|
||||
label: pickAppText(
|
||||
locale,
|
||||
`阶段 ${Math.min(stageValue, stageMax)} / ${stageMax}`,
|
||||
@ -413,14 +243,12 @@ function deriveProgress(
|
||||
),
|
||||
value: stageValue,
|
||||
max: stageMax,
|
||||
stageLabel,
|
||||
};
|
||||
}
|
||||
|
||||
const doneRuns = taskRuns.filter((run) => run.status === 'done').length;
|
||||
if (taskRuns.length > 0) {
|
||||
return {
|
||||
mode: 'ratio',
|
||||
label: pickAppText(
|
||||
locale,
|
||||
`已完成子任务 ${doneRuns} / ${taskRuns.length}`,
|
||||
@ -428,97 +256,25 @@ function deriveProgress(
|
||||
),
|
||||
value: doneRuns,
|
||||
max: taskRuns.length,
|
||||
stageLabel: stageLabel ?? taskViews.find((item) => item.isRoot)?.stageLabel ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
mode: 'status',
|
||||
label: pickAppText(locale, '等待任务数据', 'Waiting for task data'),
|
||||
value: null,
|
||||
max: null,
|
||||
stageLabel,
|
||||
};
|
||||
}
|
||||
|
||||
function buildAlerts(
|
||||
taskViews: OfficeTaskView[],
|
||||
now: number,
|
||||
locale: AppLocale,
|
||||
): OfficeAlertView[] {
|
||||
const alerts: OfficeAlertView[] = [];
|
||||
|
||||
for (const task of taskViews) {
|
||||
if (task.status === 'error') {
|
||||
alerts.push({
|
||||
id: `error:${task.runId}`,
|
||||
level: 'error',
|
||||
title: pickAppText(locale, `${task.actorName} 执行失败`, `${task.actorName} failed`),
|
||||
description: task.errorText,
|
||||
runId: task.runId,
|
||||
actorId: task.actorId,
|
||||
createdAt: task.updatedAt,
|
||||
});
|
||||
} else if (task.status === 'blocked') {
|
||||
alerts.push({
|
||||
id: `blocked:${task.runId}`,
|
||||
level: 'warn',
|
||||
title: pickAppText(locale, `${task.actorName} 长时间等待`, `${task.actorName} has been waiting for a while`),
|
||||
description: pickAppText(locale, '该任务长时间无更新,可能存在阻塞。', 'This task has not updated for a while and may be blocked.'),
|
||||
runId: task.runId,
|
||||
actorId: task.actorId,
|
||||
createdAt: task.updatedAt,
|
||||
});
|
||||
} else if (task.status === 'waiting') {
|
||||
const updatedTime = toTime(task.updatedAt);
|
||||
if (updatedTime !== null && now - updatedTime > STALE_WAITING_MS) {
|
||||
alerts.push({
|
||||
id: `stale:${task.runId}`,
|
||||
level: 'warn',
|
||||
title: pickAppText(locale, `${task.actorName} 等待时间偏长`, `${task.actorName} has been waiting longer than expected`),
|
||||
description: pickAppText(locale, '该任务仍处于等待态,建议查看详情确认依赖是否卡住。', 'This task is still waiting. Check the details to confirm whether a dependency is stuck.'),
|
||||
runId: task.runId,
|
||||
actorId: task.actorId,
|
||||
createdAt: task.updatedAt,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return alerts.sort((a, b) => compareIsoDesc(a.createdAt, b.createdAt));
|
||||
function countAlerts(taskViews: TaskRuntimeNodeView[]): number {
|
||||
return taskViews.filter((task) => task.status === 'error' || task.status === 'blocked').length;
|
||||
}
|
||||
|
||||
function buildZones(members: OfficeMemberView[], tasks: OfficeTaskView[], locale: AppLocale): OfficeZoneView[] {
|
||||
const ids: OfficeZoneId[] = ['reception', 'workspace', 'collab', 'research', 'alert', 'done'];
|
||||
return ids.map((id) => ({
|
||||
id,
|
||||
label: zoneLabel(id, locale),
|
||||
memberIds: members.filter((member) => member.zoneId === id).map((member) => member.memberId),
|
||||
taskIds: tasks.filter((task) => mapZoneId(task.status, task.actorType) === id).map((task) => task.taskId),
|
||||
tone: zoneTone(id),
|
||||
}));
|
||||
}
|
||||
|
||||
function buildAssignments(taskRuns: ProcessRun[], childrenMap: Map<string, ProcessRun[]>, locale: AppLocale): OfficeAssignmentView[] {
|
||||
return taskRuns
|
||||
.filter((run) => (childrenMap.get(run.run_id) ?? []).length > 0)
|
||||
.map((run) => {
|
||||
const children = childrenMap.get(run.run_id) ?? [];
|
||||
return {
|
||||
ownerRunId: run.run_id,
|
||||
ownerActorName: run.actor_name,
|
||||
assigneeRunIds: children.map((item) => item.run_id),
|
||||
assigneeActorNames: children.map((item) => item.actor_name),
|
||||
label: pickAppText(locale, `${run.actor_name} 分派了 ${children.length} 个子任务`, `${run.actor_name} assigned ${children.length} subtasks`),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function isOfficeTaskTerminal(status: OfficeTaskStatus): boolean {
|
||||
export function isTaskRuntimeTerminal(status: TaskRuntimeStatus): boolean {
|
||||
return TERMINAL_STATUSES.has(status);
|
||||
}
|
||||
|
||||
export function officeTaskStatusLabel(status: OfficeTaskStatus, locale: AppLocale = getCurrentAppLocale()): string {
|
||||
export function taskRuntimeStatusLabel(status: TaskRuntimeStatus, locale: AppLocale = getCurrentAppLocale()): string {
|
||||
if (status === 'queued') return pickAppText(locale, '排队中', 'Queued');
|
||||
if (status === 'running') return pickAppText(locale, '进行中', 'In Progress');
|
||||
if (status === 'waiting') return pickAppText(locale, '等待中', 'Waiting');
|
||||
@ -528,11 +284,11 @@ export function officeTaskStatusLabel(status: OfficeTaskStatus, locale: AppLocal
|
||||
return pickAppText(locale, '已取消', 'Cancelled');
|
||||
}
|
||||
|
||||
export function buildOfficeView(
|
||||
export function buildTaskRuntimeView(
|
||||
taskId: string,
|
||||
input: BuildOfficeInput,
|
||||
input: BuildTaskRuntimeInput,
|
||||
locale: AppLocale = getCurrentAppLocale(),
|
||||
): OfficeView | null {
|
||||
): TaskRuntimeView | null {
|
||||
const { sessions, processRuns, processEvents, processArtifacts } = input;
|
||||
const runById = new Map(processRuns.map((run) => [run.run_id, run]));
|
||||
const rootRun = runById.get(taskId);
|
||||
@ -547,7 +303,7 @@ export function buildOfficeView(
|
||||
const artifactsByRun = groupByRunId(taskArtifacts);
|
||||
const now = Date.now();
|
||||
|
||||
const taskViews: OfficeTaskView[] = taskRuns
|
||||
const taskViews: TaskRuntimeNodeView[] = taskRuns
|
||||
.map((run) => {
|
||||
const runEvents = eventsByRun.get(run.run_id) ?? [];
|
||||
const updatedAt = getRunUpdatedAt(run, eventsByRun, artifactsByRun);
|
||||
@ -560,72 +316,24 @@ export function buildOfficeView(
|
||||
return {
|
||||
taskId: run.run_id,
|
||||
runId: run.run_id,
|
||||
parentRunId: run.parent_run_id ?? null,
|
||||
actorId: run.actor_id,
|
||||
actorName: run.actor_name,
|
||||
actorType: run.actor_type,
|
||||
title: run.title,
|
||||
status,
|
||||
stageLabel,
|
||||
summary: firstString(run.summary),
|
||||
startedAt: run.started_at,
|
||||
updatedAt,
|
||||
finishedAt: run.finished_at ?? null,
|
||||
childTaskIds,
|
||||
artifactCount: (artifactsByRun.get(run.run_id) ?? []).length,
|
||||
errorText: deriveErrorText(run, runEvents, locale),
|
||||
isRoot: run.run_id === rootRun.run_id,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (a.isRoot !== b.isRoot) return a.isRoot ? -1 : 1;
|
||||
if (isOfficeTaskTerminal(a.status) !== isOfficeTaskTerminal(b.status)) {
|
||||
return isOfficeTaskTerminal(a.status) ? 1 : -1;
|
||||
if (isTaskRuntimeTerminal(a.status) !== isTaskRuntimeTerminal(b.status)) {
|
||||
return isTaskRuntimeTerminal(a.status) ? 1 : -1;
|
||||
}
|
||||
return compareIsoDesc(a.updatedAt, b.updatedAt);
|
||||
});
|
||||
|
||||
const actorRuns = new Map<string, ProcessRun[]>();
|
||||
for (const run of taskRuns) {
|
||||
const collection = actorRuns.get(run.actor_id);
|
||||
if (collection) {
|
||||
collection.push(run);
|
||||
continue;
|
||||
}
|
||||
actorRuns.set(run.actor_id, [run]);
|
||||
}
|
||||
|
||||
const members: OfficeMemberView[] = Array.from(actorRuns.entries())
|
||||
.map(([actorId, runs]) => {
|
||||
const display = selectDisplayRun(runs, eventsByRun, artifactsByRun, now);
|
||||
const currentRun = display.run;
|
||||
const currentTask = taskViews.find((task) => task.runId === currentRun.run_id);
|
||||
return {
|
||||
memberId: actorId,
|
||||
actorId,
|
||||
actorName: currentRun.actor_name,
|
||||
actorType: currentRun.actor_type,
|
||||
status: display.status,
|
||||
zoneId: mapZoneId(display.status, currentRun.actor_type),
|
||||
currentRunId: currentRun.run_id,
|
||||
currentTitle: currentRun.title,
|
||||
stageLabel: currentTask?.stageLabel ?? null,
|
||||
summary: currentTask?.summary ?? null,
|
||||
startedAt: currentRun.started_at ?? null,
|
||||
updatedAt: display.updatedAt,
|
||||
finishedAt: currentRun.finished_at ?? null,
|
||||
childRunIds: (childrenMap.get(currentRun.run_id) ?? []).map((child) => child.run_id),
|
||||
artifactCount: runs.reduce((count, run) => count + (artifactsByRun.get(run.run_id) ?? []).length, 0),
|
||||
isPrimary: currentRun.run_id === rootRun.run_id,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (a.isPrimary !== b.isPrimary) return a.isPrimary ? -1 : 1;
|
||||
const byStatus = taskStatusPriority(b.status) - taskStatusPriority(a.status);
|
||||
if (byStatus !== 0) return byStatus;
|
||||
return compareIsoDesc(a.updatedAt, b.updatedAt);
|
||||
});
|
||||
|
||||
const sessionId = rootRun.session_id ?? taskRuns.find((run) => run.session_id)?.session_id ?? null;
|
||||
const updatedAt = latestTimestamp([
|
||||
...taskViews.map((task) => task.updatedAt),
|
||||
@ -633,8 +341,8 @@ export function buildOfficeView(
|
||||
rootRun.started_at,
|
||||
]) ?? rootRun.started_at;
|
||||
const derivedRootStatus = deriveRunStatus(rootRun, updatedAt, now);
|
||||
const alerts = buildAlerts(taskViews, now, locale);
|
||||
const progress = deriveProgress(rootRun, taskRuns, taskViews, locale);
|
||||
const alertCount = countAlerts(taskViews);
|
||||
const progress = deriveProgress(rootRun, taskRuns, locale);
|
||||
const sourceSessionLabel = getSessionLabel(sessions, sessionId, locale);
|
||||
const createdAt = rootRun.started_at;
|
||||
const finishedAt = rootRun.finished_at ?? null;
|
||||
@ -646,7 +354,6 @@ export function buildOfficeView(
|
||||
: null;
|
||||
|
||||
return {
|
||||
officeId: rootRun.run_id,
|
||||
taskId: rootRun.run_id,
|
||||
sessionId,
|
||||
title: rootRun.title || pickAppText(locale, `任务 ${rootRun.run_id.slice(0, 8)}`, `Task ${rootRun.run_id.slice(0, 8)}`),
|
||||
@ -658,60 +365,13 @@ export function buildOfficeView(
|
||||
sourceSessionLabel,
|
||||
rootRunId: rootRun.run_id,
|
||||
rootActorName: rootRun.actor_name,
|
||||
currentStageLabel: deriveStageLabel(rootRun, eventsByRun.get(rootRun.run_id) ?? [], derivedRootStatus, locale),
|
||||
progress,
|
||||
stats: {
|
||||
totalRuns: taskRuns.length,
|
||||
activeRuns: taskViews.filter((task) => !isOfficeTaskTerminal(task.status)).length,
|
||||
doneRuns: taskViews.filter((task) => task.status === 'done').length,
|
||||
errorRuns: taskViews.filter((task) => task.status === 'error').length,
|
||||
cancelledRuns: taskViews.filter((task) => task.status === 'cancelled').length,
|
||||
memberCount: members.length,
|
||||
activeRuns: taskViews.filter((task) => !isTaskRuntimeTerminal(task.status)).length,
|
||||
artifactCount: taskArtifacts.length,
|
||||
alertCount,
|
||||
},
|
||||
alerts,
|
||||
zones: buildZones(members, taskViews, locale),
|
||||
members,
|
||||
tasks: taskViews,
|
||||
assignments: buildAssignments(taskRuns, childrenMap, locale),
|
||||
detailRunIds: taskViews.map((task) => task.runId),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildOfficeTaskList(
|
||||
input: BuildOfficeInput & { sessionId?: string | null },
|
||||
locale: AppLocale = getCurrentAppLocale(),
|
||||
): OfficeTaskListItem[] {
|
||||
const rootRuns = findRootRuns(input.processRuns);
|
||||
const offices = rootRuns
|
||||
.map((rootRun) => buildOfficeView(rootRun.run_id, input, locale))
|
||||
.filter((office): office is OfficeView => office !== null)
|
||||
.filter((office) => !input.sessionId || office.sessionId === input.sessionId);
|
||||
|
||||
return offices
|
||||
.map((office) => ({
|
||||
officeId: office.officeId,
|
||||
taskId: office.taskId,
|
||||
sessionId: office.sessionId,
|
||||
sessionLabel: office.sourceSessionLabel,
|
||||
title: office.title,
|
||||
status: office.status,
|
||||
createdAt: office.createdAt,
|
||||
updatedAt: office.updatedAt,
|
||||
finishedAt: office.finishedAt,
|
||||
rootRunId: office.rootRunId,
|
||||
rootActorName: office.rootActorName,
|
||||
memberCount: office.members.length,
|
||||
activeRuns: office.stats.activeRuns,
|
||||
errorCount: office.stats.errorRuns,
|
||||
artifactCount: office.stats.artifactCount,
|
||||
currentStageLabel: office.currentStageLabel,
|
||||
progress: office.progress,
|
||||
}))
|
||||
.sort((a, b) => {
|
||||
if (isOfficeTaskTerminal(a.status) !== isOfficeTaskTerminal(b.status)) {
|
||||
return isOfficeTaskTerminal(a.status) ? 1 : -1;
|
||||
}
|
||||
return compareIsoDesc(a.updatedAt, b.updatedAt);
|
||||
});
|
||||
}
|
||||
|
||||
@ -6,14 +6,6 @@ const nextConfig = {
|
||||
ignoreDuringBuilds: true,
|
||||
},
|
||||
images: { unoptimized: true },
|
||||
webpack: (config) => {
|
||||
config.resolve = config.resolve || {};
|
||||
config.resolve.alias = {
|
||||
...(config.resolve.alias || {}),
|
||||
phaser3spectorjs: false,
|
||||
};
|
||||
return config;
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
||||
|
||||
2888
app-instance/frontend/package-lock.json
generated
2888
app-instance/frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "nextjs",
|
||||
"name": "beaver-app-instance-frontend",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
@ -11,86 +11,41 @@
|
||||
"test": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.9.0",
|
||||
"@next/swc-wasm-nodejs": "13.5.1",
|
||||
"@radix-ui/react-accordion": "^1.2.0",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.1",
|
||||
"@radix-ui/react-aspect-ratio": "^1.1.0",
|
||||
"@radix-ui/react-avatar": "^1.1.0",
|
||||
"@radix-ui/react-checkbox": "^1.1.1",
|
||||
"@radix-ui/react-collapsible": "^1.1.0",
|
||||
"@radix-ui/react-context-menu": "^2.2.1",
|
||||
"@radix-ui/react-dialog": "^1.1.1",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
||||
"@radix-ui/react-hover-card": "^1.1.1",
|
||||
"@radix-ui/react-label": "^2.1.0",
|
||||
"@radix-ui/react-menubar": "^1.1.1",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.0",
|
||||
"@radix-ui/react-popover": "^1.1.1",
|
||||
"@radix-ui/react-progress": "^1.1.0",
|
||||
"@radix-ui/react-radio-group": "^1.2.0",
|
||||
"@radix-ui/react-scroll-area": "^1.1.0",
|
||||
"@radix-ui/react-select": "^2.1.1",
|
||||
"@radix-ui/react-separator": "^1.1.0",
|
||||
"@radix-ui/react-slider": "^1.2.0",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"@radix-ui/react-switch": "^1.1.0",
|
||||
"@radix-ui/react-tabs": "^1.1.0",
|
||||
"@radix-ui/react-toast": "^1.2.15",
|
||||
"@radix-ui/react-toggle": "^1.1.0",
|
||||
"@radix-ui/react-toggle-group": "^1.1.0",
|
||||
"@radix-ui/react-tooltip": "^1.1.2",
|
||||
"@supabase/supabase-js": "^2.58.0",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/jspdf": "^1.3.3",
|
||||
"@types/node": "20.6.2",
|
||||
"@types/react": "18.2.22",
|
||||
"@types/react-dom": "18.2.7",
|
||||
"autoprefixer": "10.4.15",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.0.0",
|
||||
"date-fns": "^3.6.0",
|
||||
"docx": "^9.5.1",
|
||||
"echarts": "^6.0.0",
|
||||
"echarts-for-react": "^3.0.5",
|
||||
"embla-carousel-react": "^8.3.0",
|
||||
"eslint": "8.49.0",
|
||||
"eslint-config-next": "13.5.1",
|
||||
"file-saver": "^2.0.5",
|
||||
"input-otp": "^1.2.4",
|
||||
"jspdf": "^3.0.3",
|
||||
"jspdf-autotable": "^5.0.2",
|
||||
"lucide-react": "^0.446.0",
|
||||
"next": "13.5.1",
|
||||
"next-themes": "^0.3.0",
|
||||
"pdfmake": "^0.2.20",
|
||||
"pdfmake-with-chinese-fonts": "^1.0.16",
|
||||
"phaser": "^3.90.0",
|
||||
"postcss": "8.4.30",
|
||||
"react": "18.2.0",
|
||||
"react-day-picker": "^8.10.1",
|
||||
"react-dom": "18.2.0",
|
||||
"react-force-graph-2d": "^1.29.0",
|
||||
"react-hook-form": "^7.53.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-resizable-panels": "^2.1.3",
|
||||
"react-resize-detector": "^12.3.0",
|
||||
"recharts": "^2.12.7",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"sonner": "^1.5.0",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"tailwindcss": "3.3.3",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"typescript": "5.2.2",
|
||||
"vaul": "^0.9.9",
|
||||
"xlsx": "^0.18.5",
|
||||
"zod": "^3.23.8",
|
||||
"zustand": "^5.0.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/pdfmake": "^0.2.12",
|
||||
"vite-tsconfig-paths": "^4.3.2",
|
||||
"vitest": "^1.6.1"
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Nanobot frontend types
|
||||
// Beaver frontend types
|
||||
|
||||
export interface AuthUser {
|
||||
id: string;
|
||||
@ -222,34 +222,6 @@ export interface SkillDetailResponse {
|
||||
frontmatter?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface SlashCommand {
|
||||
name: string;
|
||||
description: string;
|
||||
argument_hint: string | null;
|
||||
plugin_name: string;
|
||||
}
|
||||
|
||||
export interface PluginAgent {
|
||||
name: string;
|
||||
description: string;
|
||||
model: string | null;
|
||||
}
|
||||
|
||||
export interface PluginCommand {
|
||||
name: string;
|
||||
description: string;
|
||||
argument_hint: string | null;
|
||||
}
|
||||
|
||||
export interface PluginInfo {
|
||||
name: string;
|
||||
description: string;
|
||||
source: 'global' | 'workspace';
|
||||
agents: PluginAgent[];
|
||||
commands: PluginCommand[];
|
||||
skills: string[];
|
||||
}
|
||||
|
||||
export interface CronJob {
|
||||
id: string;
|
||||
name: string;
|
||||
@ -380,19 +352,6 @@ export interface ActiveTask {
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface Marketplace {
|
||||
name: string;
|
||||
source: string;
|
||||
type: 'local' | 'git';
|
||||
}
|
||||
|
||||
export interface MarketplacePlugin {
|
||||
name: string;
|
||||
description: string;
|
||||
marketplace_name: string;
|
||||
installed: boolean;
|
||||
}
|
||||
|
||||
export interface SkillHubVersionRef {
|
||||
id?: number;
|
||||
version: string;
|
||||
@ -557,14 +516,6 @@ export interface AuthzLocalBackendStatus {
|
||||
registered: boolean;
|
||||
}
|
||||
|
||||
export interface AuthzChannelSettings {
|
||||
configured: boolean;
|
||||
config?: Record<string, unknown>;
|
||||
secrets_masked?: boolean;
|
||||
secret_keys?: string[];
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface AuthzStatus {
|
||||
enabled: boolean;
|
||||
base_url: string;
|
||||
@ -573,35 +524,10 @@ export interface AuthzStatus {
|
||||
backend?: Record<string, unknown>;
|
||||
permissions?: Record<string, unknown>;
|
||||
outlook?: Record<string, unknown>;
|
||||
channel_settings?: Record<string, AuthzChannelSettings>;
|
||||
channel_settings?: Record<string, unknown>;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface AuthzBackendRecord {
|
||||
backend_id: string;
|
||||
name: string;
|
||||
base_url: string;
|
||||
frontend_base_url?: string | null;
|
||||
status: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
is_local_backend?: boolean;
|
||||
}
|
||||
|
||||
export interface AuthzRegisterBackendResponse {
|
||||
backend_id: string;
|
||||
client_id: string;
|
||||
client_secret: string;
|
||||
created_at: string;
|
||||
saved_to_backend: boolean;
|
||||
local_backend?: AuthzLocalBackendStatus & {
|
||||
authz?: {
|
||||
enabled: boolean;
|
||||
base_url: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface OutlookMailboxAddress {
|
||||
emailAddress?: {
|
||||
name?: string | null;
|
||||
|
||||
@ -43,8 +43,6 @@ def _normalize_record(record: dict[str, Any]) -> dict[str, Any]:
|
||||
"api_base_url",
|
||||
):
|
||||
normalized[key] = str(record.get(key, "") or "")
|
||||
if not normalized["beaver_home"]:
|
||||
normalized["beaver_home"] = str(record.get("nanobot_home", "") or "")
|
||||
return normalized
|
||||
|
||||
|
||||
|
||||
@ -20,7 +20,7 @@ function errorDetail(error: unknown): string {
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const locale = normalizePortalLocale(
|
||||
request.cookies.get('nanobot_locale')?.value ||
|
||||
request.cookies.get('beaver_locale')?.value ||
|
||||
request.headers.get('accept-language')
|
||||
);
|
||||
|
||||
|
||||
@ -20,7 +20,7 @@ function errorDetail(error: unknown): string {
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const locale = normalizePortalLocale(
|
||||
request.cookies.get('nanobot_locale')?.value ||
|
||||
request.cookies.get('beaver_locale')?.value ||
|
||||
request.headers.get('accept-language')
|
||||
);
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
export const PORTAL_LOCALE_COOKIE = 'nanobot_locale';
|
||||
export const PORTAL_LOCALE_STORAGE_KEY = 'nanobot_locale';
|
||||
export const PORTAL_LOCALE_COOKIE = 'beaver_locale';
|
||||
export const PORTAL_LOCALE_STORAGE_KEY = 'beaver_locale';
|
||||
|
||||
export const PORTAL_LOCALES = ['zh-CN', 'en-US'] as const;
|
||||
|
||||
|
||||
4
auth-portal/src/package-lock.json
generated
4
auth-portal/src/package-lock.json
generated
@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "nanobot-auth-portal",
|
||||
"name": "beaver-auth-portal",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "nanobot-auth-portal",
|
||||
"name": "beaver-auth-portal",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"next": "13.5.1",
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "nanobot-auth-portal",
|
||||
"name": "beaver-auth-portal",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
@ -20,4 +20,3 @@
|
||||
"typescript": "5.2.2"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -51,7 +51,7 @@ curl http://127.0.0.1:19090/.well-known/jwks.json
|
||||
- 例如 `https://authz.example.com`
|
||||
- 如果要让 AuthZ 负责编排实例注册,还要设置 `DEPLOY_API_BASE_URL`
|
||||
- 如果 deploy-control 开了鉴权,还要设置 `DEPLOY_API_TOKEN`
|
||||
- 不要把 `src/data/` 里的本地示例或真实数据直接拿去打镜像
|
||||
- 不要提交本地运行产生的 `runtime/data/` 内容
|
||||
|
||||
## API 说明
|
||||
|
||||
|
||||
@ -307,7 +307,7 @@ def _upsert_registry_record(record: dict[str, Any]) -> dict[str, Any]:
|
||||
"--instance-root",
|
||||
str(record.get("instance_root", "") or "").strip(),
|
||||
"--beaver-home",
|
||||
str(record.get("beaver_home", "") or record.get("nanobot_home", "") or "").strip(),
|
||||
str(record.get("beaver_home", "") or "").strip(),
|
||||
"--config-path",
|
||||
str(record.get("config_path", "") or "").strip(),
|
||||
"--auth-users-path",
|
||||
|
||||
Reference in New Issue
Block a user