diff --git a/packages/cli/src/commands/playground.ts b/packages/cli/src/commands/playground.ts index 327eed6..cca6dab 100644 --- a/packages/cli/src/commands/playground.ts +++ b/packages/cli/src/commands/playground.ts @@ -133,6 +133,54 @@ function openBrowser(url: string): void { } } +interface ExistingPlayground { + version: string; + pid: number; +} + +/** + * Probe the target port for an already-running playground instance. + * Returns version + pid when the enriched `/health` response is present, + * `null` when the port is free or occupied by a non-playground process. + */ +async function probeExistingPlayground(port: number): Promise { + try { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 2000); + const res = await fetch(`http://localhost:${port}/health`, { signal: controller.signal }); + clearTimeout(timeout); + if (!res.ok) return null; + const body = (await res.json()) as { playground?: { version?: string; pid?: number } }; + if (body.playground?.pid) { + return { version: body.playground.version ?? "unknown", pid: body.playground.pid }; + } + } catch { + // port not listening or not a playground — ignore + } + return null; +} + +/** + * Kill an existing playground process (best-effort, SIGTERM) and wait briefly for the port to free. + * Returns true when the port appears released; false on timeout. + */ +async function replaceExistingPlayground(existing: ExistingPlayground, port: number): Promise { + log.info(`Replacing playground v${existing.version} (pid ${existing.pid}) on port ${port}...`); + try { + process.kill(existing.pid, "SIGTERM"); + } catch { + // already gone — that's fine + return true; + } + // Wait up to 3 s for the port to free (100 ms poll). + for (let i = 0; i < 30; i++) { + await new Promise((r) => setTimeout(r, 100)); + const still = await probeExistingPlayground(port); + if (!still) return true; + } + return false; +} + export async function playgroundCommand(options: PlaygroundOptions): Promise { const port = options.port ? Number(options.port) : DEFAULT_PORT; if (!Number.isInteger(port) || port <= 0) { @@ -143,6 +191,28 @@ export async function playgroundCommand(options: PlaygroundOptions): Promise/web. This file ships to dist/bin/playground.js, // so the package root is two levels up from the emitted bundle. -const webRoot = join(dirname(fileURLToPath(import.meta.url)), "../../web"); +const pkgRoot = join(dirname(fileURLToPath(import.meta.url)), "../.."); +const webRoot = join(pkgRoot, "web"); const indexHtml = injectPlaygroundRuntimeMarker(readFileSync(join(webRoot, "index.html"), "utf8")); +/** Read the playground package version once at startup (works for both source and published dist). */ +function readPlaygroundVersion(): string { + try { + const manifest = JSON.parse(readFileSync(join(pkgRoot, "package.json"), "utf8")) as { version?: string }; + return manifest.version ?? "unknown"; + } catch { + return "unknown"; + } +} + +const playgroundVersion = readPlaygroundVersion(); + function injectPlaygroundRuntimeMarker(html: string): string { if (html.includes('name="agents-runtime"')) return html; return html.replace("", '\n '); @@ -38,12 +51,23 @@ export async function startServer(): Promise { } const root = new Hono(); - // Server routes: /api/*, /health, /openapi.json (merged into the router). + // Playground-enriched health check — registered before the server routes so it takes + // precedence over the plain { status: "ok" } from apps/server. The `playground` field + // lets the CLI detect a stale instance on the target port and replace it automatically. + root.get("/health", (c) => c.json({ status: "ok", playground: { version: playgroundVersion, pid: process.pid } })); + // Server routes: /api/*, /openapi.json (merged into the router). root.route("/", app); // Always serve the injected shell for document routes — static middleware would otherwise // return web/index.html without the playground runtime marker. root.get("/", (c) => c.html(indexHtml)); root.get("/index.html", (c) => c.html(indexHtml)); + // Static assets — bust browser caches across playground upgrades. The filenames are + // stable (`assets/index.js`, not hashed) because the console embed requires predictable + // paths, so we instruct browsers to revalidate on every navigation. + root.use("/assets/*", async (c, next) => { + await next(); + c.header("Cache-Control", "no-cache"); + }); root.use("/assets/*", serveStatic({ root: webRoot })); // SPA fallback: any unmatched GET renders the shell so client routing works on reload. root.get("*", (c) => c.html(indexHtml)); diff --git a/scenario/git-repo.yaml b/scenario/git-repo.yaml new file mode 100644 index 0000000..3770c0d --- /dev/null +++ b/scenario/git-repo.yaml @@ -0,0 +1,79 @@ +# OpenCMA BYOC 端到端测试配置 +# +# 用途:验证 Qoder BYOC 内网接入(工具调用 + MCP)是否打通。 +# 本文件含真实 environment_id / tunnel_id / 内网域名,已被 .gitignore 排除,请勿提交。 +# +# 使用前请确保 .env 中已设置: +# - QODER_PAT Qoder API 个人访问令牌(https://qoder.com/account/integrations) +# - MCP_COOP_TOKEN 内网 coop MCP 的 access token(apply 时需要) +# - GITHUB_REPOSITORY_URL GitHub 仓库 URL +# - GITHUB_TOKEN GitHub 细粒度 token(仅需仓库 Contents 权限) +# +# 关键约束: +# - environment_id 指向的 BYOC 环境由 QCA 侧开通,OpenCMA 只引用不修改。 +# - tunnel 只是 agents.yaml 内的命名别名,直接引用 Qoder 下发的 tunnel_id。 + +version: "1" + +providers: + qoder: + api_key: ${QODER_PAT} + +defaults: + provider: qoder + +# 已存在的 BYOC 环境,通过 environment_id 直接引用,不需要 state import。 +environments: + alibaba-byoc: + description: "alibaba 内网byoc环境" + environment_id: env_00j8jl58nepkwyartwag + config: + type: self_hosted + metadata: + byoc: alibaba + +# 已存在的 BYOC tunnel。直接写死 tunnel_id,OpenCMA 不会尝试创建或修改它。 +tunnels: + alibaba-internal: + description: "Tunnel to Alibaba internal MCP services" + tunnel_id: tnl_00j8jl58if01s00dqm8k + +# 内网 MCP(Aone 协作 MCP)vault 配置。 +# 执行 apply 前请在 shell 中设置环境变量:export MCP_COOP_TOKEN= +# 该 token 通过 ${MCP_COOP_TOKEN} 注入,不会落盘到本文件。 +vaults: + alibaba-mcp-oauth: + display_name: "Alibaba MCP OAuth" + credentials: + - name: coop-mcp + mcp_server_url: "https://mcp.alibaba-inc.com/coop/mcp" + type: static_bearer + access_token: ${MCP_COOP_TOKEN} + protocol: streamable_http + + +agents: + byoc-agent: + description: "Assistant running inside Alibaba BYOC" + model: + qoder: auto + instructions: | + You are a BYOC end-to-end test assistant running in a self-hosted environment. + Your goal is to verify that Qoder BYOC tool calling and MCP connectivity are working. + Answer user questions concisely. Use the available builtin tools and configured MCP servers when helpful. + If a tool or MCP call fails, report the error clearly so connectivity issues can be diagnosed. + environment: alibaba-byoc + tunnel: alibaba-internal + resources: + - type: github_repository + url: ${GITHUB_REPOSITORY_URL} + authorization_token: ${GITHUB_TOKEN} + mount_path: /data/workspace/repository + tools: + builtin: [Bash, Read, Write, Edit, Glob, Grep, WebFetch, WebSearch] + default_permission: allow + # 内网 MCP 服务器配置 + mcp_servers: + - name: coop + url: https://mcp.alibaba-inc.com/coop/mcp + vault: alibaba-mcp-oauth diff --git a/scenario/pr.md b/scenario/pr.md new file mode 100644 index 0000000..b0428ee --- /dev/null +++ b/scenario/pr.md @@ -0,0 +1,65 @@ +# OpenAgentPack 正式开源:让 CMA 成为团队里的远程同事 + +想象一下:每天早上 8 点,一个 Agent 已经在云端读完行业动态、内部资料和项目进展,整理好日报;新同事加入项目时,不必从头学习 Prompt 和工具配置,直接复用团队验证过的 Agent,就能按同一套方法工作。 + +这就是 CMA(Cloud Managed Agent,云端托管的远程 Agent)的价值:它不依赖某个人的电脑,可以持续工作;它沉淀的也不只是一次对话,而是整个团队可复用的知识与工作方法。 + +今天,我们正式开源 **OpenAgentPack**。它用 Git 和 YAML 管理 CMA,让远程 Agent 从“个人配置”变成可共享、可审查、可运行的团队资产。 + +项目地址:[GitHub - OpenAgentPack](https://github.com/modelstudioai/OpenAgentPack) + +## 场景一:把一个人的经验,变成团队能力 + +假设一名研究员搭建了“行业研究 Agent”:它知道去哪里找资料、如何判断来源、按什么框架分析,以及怎样输出报告。 + +过去,这些经验散落在 Prompt、文件、Skill 和控制台配置里。换个人使用,往往要重新搭建;原作者离开后,Agent 甚至可能无人敢改。 + +OpenAgentPack 把模型、指令、工具、Skill、MCP、知识文件和运行环境收敛到一份 `agents.yaml`,并交给 Git 管理: + +```mermaid +flowchart LR + A[个人经验] --> B[Prompt / Skill / 知识 / 工具] + B --> C[agents.yaml] + C --> D[Git 审查与版本管理] + D --> E[团队复用的 CMA] +``` + +同事可以直接复用这套 Agent;专家更新分析方法时,通过 Pull Request 说明变化;效果不理想时,可以回到上一个稳定版本。知识不再只存在于人的脑中,而是成为能运行、能迭代、能交接的组织资产。 + +## 场景二:电脑关机,任务继续 + +很多工作并不需要人一直盯着:每天汇总舆情、每周生成项目周报、定期检查系统异常、收到事件后整理材料。 + +本地 Agent 一旦断网、休眠或关闭终端,任务就会中断。CMA 运行在远端环境中,可以通过定时或事件触发持续执行: + +```mermaid +flowchart LR + A[定时 / 事件触发] --> B[远程 CMA] + B --> C[读取资料与调用工具] + C --> D[分析并生成结果] + D --> E[报告 / 文件 / 消息] +``` + +例如,团队可以声明一个“每日项目管家”:早上自动读取仓库变更和任务记录,归纳风险与待办,产出简报。成员上班后看到的是结果,而不是等待 Agent 在个人电脑上跑完。 + +OpenAgentPack 用 `deployment` 描述这类可重复任务,并把 Agent、运行环境、初始事件和调度统一管理。Qoder 与 Claude 支持服务端定时运行;其他平台也可通过外部 cron 或 CI 触发。 + +## 让远程 Agent 可控,才能放心交给它工作 + +无人值守不等于不可控。OpenAgentPack 提供一套类似 Terraform 的流程: + +```text +同步已有 Agent → 修改声明 → validate → plan → apply → 远端运行 +``` + +`plan` 会在变更发生前展示将创建、更新或删除什么;依赖按正确顺序执行,失败时跳过下游资源;远端被手动修改后,还能发现配置漂移。团队既能提高自动化程度,也保留审查、追溯和回滚能力。 + +目前,OpenAgentPack 已支持百炼、Qoder、Claude 和火山方舟。你可以先把控制台中已有的 Agent 同步回 Git,再逐步建立团队共享、自动运行和多平台迁移流程。 + +```bash +npm install -g @openagentpack/cli +agents init +agents validate && agents plan +``` + +CMA 的意义,不只是把 Agent 搬到云端,而是让它成为团队真正拥有的远程生产力:**知识可以传承,任务可以持续,变化始终可控。**