怎么使用 Downcity

从安装到日常使用,理解 Downcity 如何作为 Agent 基础设施支撑多个产品和工作流

怎么使用 Downcity

Downcity 的核心用法可以理解成一句话:用一套可复用 Agent 基础设施,承载多个本机 Agent、产品表层和自动化工作流。

最常用的是本机 Town 路线:安装 downcity,启动 town,在你的代码仓库里创建 agent,然后通过 CLI 或 Console 与它对话。需要嵌入到应用时,再使用 @downcity/agent SDK。需要多个产品或线上服务复用模型、账号、usage、支付和 Service 能力时,再接入 City runtime。

你会用到什么

入口什么时候用
town本机启动和管理 Agent、Console、City 连接、插件和任务。
city管理已部署的 City runtime、模型、token 和服务资源。
@downcity/agent在 Node 应用里嵌入本地 Agent,或调用远程 Agent。
@downcity/city构建可部署的 City runtime,让多个产品或 Town 复用后端能力。
Console在浏览器里查看和操作本机 Agent。
Chrome Extension把网页内容、选区和浏览器上下文发送给 Agent。

1. 安装 CLI

npm install -g downcity
# 或
pnpm add -g downcity

安装后会得到两个命令:

town --version
city --version

日常本机使用主要靠 towncity 主要用于部署或管理 City runtime。

2. 首次初始化 Town

town init

这一步会初始化全局配置和本机存储,默认位于 ~/.downcity/。Console token、运行状态和全局环境都会放在这里。

3. 连接 City

Agent 启动前需要连接到 City。模型目录、provider、key 和服务资源由 city 管理;town 只把当前 City 会话导入本机 Agent runtime:

city
town city use
town city status

如果已经在某个 agent 项目里,可以用 City 里的模型 ID 更新当前项目绑定:

town config set execution.modelId <modelId>

项目里的 downcity.json 会记录这份绑定:

{
  "name": "my-agent",
  "version": "1.0.0",
  "execution": {
    "type": "api",
    "modelId": "quality"
  }
}

4. 把一个项目变成 Agent

进入你的项目根目录:

cd /path/to/your-repo
town agent create .

创建后通常会生成:

  • PROFILE.md:agent 的角色、边界和工作方式
  • SOUL.md:更偏长期人格和协作风格的描述
  • downcity.json:项目运行配置
  • .downcity/:运行时数据、日志、会话、缓存和任务目录
  • .agents/skills/:项目内技能目录

创建完成后,优先检查三件事:

  1. downcity.json.execution.modelId 是否指向可用模型
  2. PROFILE.md 是否写清楚这个 Agent 应该怎么帮助你
  3. 当前项目是否真的需要启用聊天渠道或其他插件

5. 启动 Town 和 Agent

先启动本机 Town runtime:

town start

如果想同时打开浏览器 Console:

town start --console

然后在 agent 项目里启动 Agent:

town agent start
town agent status
town agent list --running

默认是后台 daemon,适合长期运行。如果想在当前终端看日志:

town agent start --foreground

停止和重启:

town agent stop
town agent restart

6. 和 Agent 对话

一次性发送消息:

town agent chat --message "总结一下这个仓库的结构"

指定目标 agent:

town agent chat --to <agentId> --message "检查最近一次任务状态"

进入持续对话模式:

town agent chat

在持续对话里输入 /exit/quit 退出。

如果你启动了 Console,可以在浏览器里打开本机控制台。默认 Console 端口是:

http://127.0.0.1:5315

7. 使用内建能力

Downcity 的日常能力大多通过 plugin 命令暴露。常见命令包括:

town skill
town task list
town memory
town shell
town web
town asr
town tts

典型场景:

  • town skill 管理 Agent 技能
  • town task 创建长期存在的自动化任务
  • town memory 管理可复用记忆
  • town shell 执行项目相关命令
  • town web 处理网页访问能力
  • town asr / town tts 接入语音能力

如果某个能力需要延迟执行或定时执行,优先查看 town task 和 plugin schedule 文档。

8. 接入 Chrome Extension

Chrome Extension 适合把网页内容交给 Agent:

  1. 在网页中选中文本,使用页内输入框把选区发送给 Agent。
  2. 没有选区时,可以按页面全文模式发送。
  3. 通过 Extension Popup 选择 Agent、查看本页发送历史,并快速回填内容。

它适合做网页总结、网页问答、资料整理、页面上下文写作和浏览器内工作流。

9. 用 SDK 嵌入应用

如果你要在自己的 Node 应用里直接创建 Agent,可以用 @downcity/agent

import { Agent } from "@downcity/agent";
import { createOpenAI } from "@ai-sdk/openai";

const openai = createOpenAI({
  apiKey: process.env.OPENAI_API_KEY!,
});

const agent = new Agent({
  id: "repo-helper",
  path: "/path/to/project",
  tools: {},
});

const session = await agent.createSession();
await session.set({
  model: openai.responses("gpt-5"),
});

const turn = await session.prompt({
  query: "总结当前项目结构",
});
const result = await turn.finished;

console.log(result.text);

SDK 模式下,模型由你的宿主应用创建并注入。@downcity/agent 负责 Agent runtime,不负责解析 City 模型目录里的 modelId

如果 Agent 已经作为 HTTP 服务运行,也可以使用远程 Agent:

import { RemoteAgent } from "@downcity/agent";

const agent = new RemoteAgent({
  url: "http://127.0.0.1:15314",
});

const session = await agent.createSession();
const turn = await session.prompt({
  query: "检查最近一次任务执行状态",
});
const result = await turn.finished;

console.log(result.text);

9.1 不使用 OpenAI SDK 传入模型

@downcity/agent 不要求你一定使用 OpenAI SDK。model 支持两种输入:

  • AI SDK LanguageModel
  • City runtime 返回的 CityModel

如果你使用 City,City catalog 返回的 model 可以直接传给 Agent:

import { Agent } from "@downcity/agent";
import { City } from "@downcity/city";

const city = new City({
  role: "user",
  city_url: "http://127.0.0.1:43127",
  town_id: "town_xxx",
  user_token: "ub_xxx",
});

const catalog = await city.ai.listModels();
const model = catalog.default();

if (!model) {
  throw new Error("No available City model");
}

const agent = new Agent({
  id: "city-backed-agent",
  path: "/path/to/project",
  model,
});

如果你不使用 City,也可以自己提供一个 AI SDK LanguageModel。这时 model 不能只是一个 (prompt) => string 函数;它需要实现模型协议。最小需要提供:

  • specificationVersion
  • provider
  • modelId
  • supportedUrls
  • doGenerate()
  • doStream()

一个最小 echo model 可以这样写:

import type { LanguageModel } from "ai";

const echo_model = {
  specificationVersion: "v3",
  provider: "local",
  modelId: "echo",
  supportedUrls: {},

  async doGenerate(options) {
    const prompt = options.prompt
      .flatMap((message) => message.content)
      .filter((part) => part.type === "text")
      .map((part) => part.text)
      .join("\n");

    return {
      content: [{ type: "text", text: `Echo: ${prompt}` }],
      finishReason: { unified: "stop", raw: "stop" },
      usage: {
        inputTokens: {
          total: undefined,
          noCache: undefined,
          cacheRead: undefined,
          cacheWrite: undefined,
        },
        outputTokens: {
          total: undefined,
          text: undefined,
          reasoning: undefined,
        },
      },
      warnings: [],
    };
  },

  async doStream(options) {
    const prompt = options.prompt
      .flatMap((message) => message.content)
      .filter((part) => part.type === "text")
      .map((part) => part.text)
      .join("\n");

    const text = `Echo: ${prompt}`;

    return {
      stream: new ReadableStream({
        start(controller) {
          controller.enqueue({ type: "stream-start", warnings: [] });
          controller.enqueue({ type: "text-start", id: "text-1" });
          controller.enqueue({
            type: "text-delta",
            id: "text-1",
            delta: text,
          });
          controller.enqueue({ type: "text-end", id: "text-1" });
          controller.enqueue({
            type: "finish",
            finishReason: { unified: "stop", raw: "stop" },
            usage: {
              inputTokens: {
                total: undefined,
                noCache: undefined,
                cacheRead: undefined,
                cacheWrite: undefined,
              },
              outputTokens: {
                total: undefined,
                text: undefined,
                reasoning: undefined,
              },
            },
          });
          controller.close();
        },
      }),
    };
  },
} satisfies LanguageModel;

const agent = new Agent({
  id: "local-echo-agent",
  path: "/path/to/project",
  model: echo_model,
});

如果你要接自己的网关或私有模型,也是在 doGenerate() / doStream() 里调用你的 HTTP API,然后把返回结果转换成 AI SDK 的模型协议。

10. City SDK 怎么使用

只在本机使用 Agent 时,通常不需要先部署 City runtime。

当你需要下面这些能力时,再考虑 City SDK:

  • 多个 Town 或多个产品复用同一套 City 模型目录
  • 给终端用户签发 user_token
  • 统一处理模型调用、用量、余额、支付和账号
  • 把 Downcity 能力接入正式产品后端
  • 在 Node 或 Edge 环境里部署 City runtime

City SDK 当前由 @downcity/city 提供,分成三侧:

  • 服务端用 CityAIServiceProvider 创建可部署 runtime
  • 可信后端用 Admin City 创建 town、签发 user_token
  • 产品侧用 User City 调用 AIService 或自定义 Service

10.1 安装

pnpm add @downcity/city @hono/node-server drizzle-orm better-sqlite3 ai

10.2 服务端创建 City runtime

下面是一个最小可运行的本地 City runtime。它使用 SQLite,并注册一个 echo 模型,方便你先把调用链跑通:

import { serve } from "@hono/node-server";
import { AIService, City, Provider } from "@downcity/city";
import { createUIMessageStreamResponse } from "ai";
import Database from "better-sqlite3";
import { drizzle } from "drizzle-orm/better-sqlite3";

const sqlite = new Database("./data.sqlite");
sqlite.pragma("journal_mode = WAL");

const db = Object.assign(drizzle(sqlite), {
  $client: { exec: (sql: string) => sqlite.exec(sql) },
});

const base = new CityBase({ db, dialect: "sqlite", raw: sqlite });

const echo = new Provider("echo", {
  text: async (ctx) => ({
    id: crypto.randomUUID(),
    role: "assistant",
    parts: [
      {
        type: "text",
        text: `Echo: ${String(ctx.input.prompt ?? "")}`,
        state: "done",
      },
    ],
  }),
  stream: async (ctx) =>
    createUIMessageStreamResponse({
      execute: async ({ writer }) => {
        writer.write({
          type: "text-delta",
          textDelta: `Echo: ${String(ctx.input.prompt ?? "")}`,
        });
      },
    }),
});

const ai = new AIService();
ai.use(
  echo.model({
    id: "local-echo",
    name: "Local Echo",
    default: true,
  }),
);

base.use(ai);

await base.health();

serve({
  fetch: base.router().fetch,
  port: 43127,
  hostname: "127.0.0.1",
});

启动后,City 会暴露这些核心路由:

GET  /v1/ai/models
POST /v1/ai/text
POST /v1/ai/stream
POST /v1/ai/chat/completions

10.3 可信后端签发 user_token

user_token 只能在可信环境签发,不要把 admin_secret_key 放到浏览器前端。

import { City } from "@downcity/city";

const admin = new City({
  role: "admin",
  city_url: "http://127.0.0.1:43127",
  admin_secret_key: process.env.DOWNCITY_CITY_ADMIN_SECRET_KEY,
});

const town = await admin.towns.create({
  name: "Demo Town",
});

const user = await admin.towns.tokens.apply({
  town_id: town.town_id,
  user_id: "user_123",
  metadata: {
    plan: "pro",
  },
  ttl: "7d",
});

console.log({
  town_id: town.town_id,
  user_token: user.user_token,
});

你的产品后端把 town_iduser_token 返回给产品侧 client。后续所有用户侧请求都带这两个值。

10.4 产品侧调用 City

产品侧使用 User City:

import { City } from "@downcity/city";

const client = new City({
  role: "user",
  city_url: "http://127.0.0.1:43127",
  town_id: "town_xxx",
  user_token: "ub_xxx",
});

const catalog = await client.ai.listModels();
const model = catalog.default();

const result = await client.ai.text({
  model,
  prompt: "你好 Downcity",
});

console.log(result.parts);

同一个 model 也可以直接传给 Agent:

import { Agent } from "@downcity/agent";

const agent = new Agent({
  id: "city-backed-agent",
  path: "/path/to/project",
  model,
});

如果你想把某个模型固定成操作句柄:

const local_echo = client.ai.model("local-echo");

const result = await local_echo.text({
  prompt: "解释一下 City SDK 的调用链",
});

10.5 调用自定义 Service

服务端可以挂载自己的业务 Service:

import { Service } from "@downcity/city";

const notes = new Service({
  id: "notes",
  name: "Notes",
});

notes.action("create", async (ctx) => ({
  ok: true,
  title: String(ctx.input.title ?? ""),
}));

base.use(notes);

产品侧调用:

const result = await client
  .service("notes")
  .action("create")
  .invoke({ title: "First note" });

10.6 用 OpenAI 兼容通路

如果你已经有 OpenAI SDK,也可以直接调用 City runtime 的 OpenAI 兼容端点:

import OpenAI from "openai";

const openai = new OpenAI({
  baseURL: "http://127.0.0.1:43127/v1/ai",
  apiKey: "ub_xxx",
});

const completion = await openai.chat.completions.create({
  model: "local-echo",
  messages: [{ role: "user", content: "hello" }],
});

City SDK 的核心调用链是:

产品前端 / 产品后端
  -> User City 携带 town_id + user_token
  -> City runtime 校验 token
  -> AIService / 自定义 Service
  -> Provider 或业务 action

更完整的 City 路线:

city --help

然后按City 快速开始文档完成:

  1. 创建 City runtime
  2. 添加模型
  3. 签发 user token
  4. 在 client 侧通过 User City 调用 City 里的 Service

对应文档:

推荐日常工作流

# 1. 启动本机 runtime 和 Console
town start --console

# 2. 进入项目并启动 agent
cd /path/to/your-repo
town agent start

# 3. 检查状态
town status
town agent status

# 4. 直接在终端对话
town agent chat --message "今天先帮我读一下项目结构"

# 5. 创建长期任务
town task create --title "Daily check" --description "Run tests" --when "@manual"

排障优先看什么

如果 Agent 没有正常工作,按这个顺序查:

  1. town status:确认 Town runtime 是否运行
  2. town agent status:确认当前 Agent 是否运行
  3. town city status:确认 Town 已导入当前 City 连接
  4. 实际发送一次请求:确认模型真实可调用
  5. .downcity/logs/:查看当前项目运行日志
  6. town agent doctor:诊断僵尸进程或损坏状态

下一步