mirror of
https://fastgit.cc/https://github.com/anomalyco/opencode
synced 2026-05-03 15:21:31 +08:00
Compare commits
4 Commits
v1.14.32
...
kit/httpap
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b60294c696 | ||
|
|
5a4763f560 | ||
|
|
2a600797fb | ||
|
|
9175c68a77 |
@@ -181,7 +181,7 @@ Use raw Effect HTTP routes where `HttpApi` does not fit. The goal is deleting Ho
|
||||
| `mcp` | `bridged` | status, add, OAuth, connect/disconnect |
|
||||
| `workspace` | `bridged` partial | adaptor/list/status; create/remove/session-restore remain |
|
||||
| top-level instance routes | `bridged` | path, vcs, command, agent, skill, lsp, formatter, dispose |
|
||||
| experimental JSON routes | `bridged` partial | console reads, tool ids, worktree list/mutations, resource list; global session list remains later |
|
||||
| experimental JSON routes | `bridged` partial | console, tool, worktree list/mutations, resource list; global session list remains later |
|
||||
| `session` | `later/special` | large stateful surface plus streaming |
|
||||
| `sync` | `later` | process/control side effects |
|
||||
| `event` | `special` | SSE |
|
||||
@@ -259,9 +259,9 @@ This checklist tracks bridge parity only. Checked routes are available through t
|
||||
|
||||
- [x] `GET /experimental/console` - active Console provider metadata.
|
||||
- [x] `GET /experimental/console/orgs` - switchable Console orgs.
|
||||
- [ ] `POST /experimental/console/switch` - switch active Console org.
|
||||
- [x] `POST /experimental/console/switch` - switch active Console org.
|
||||
- [x] `GET /experimental/tool/ids` - tool IDs.
|
||||
- [ ] `GET /experimental/tool` - tools for provider/model.
|
||||
- [x] `GET /experimental/tool` - tools for provider/model.
|
||||
- [x] `GET /experimental/worktree` - list worktrees.
|
||||
- [x] `POST /experimental/worktree` - create worktree.
|
||||
- [x] `DELETE /experimental/worktree` - remove worktree.
|
||||
@@ -350,7 +350,7 @@ Prefer smaller PRs from here so route behavior and SDK/OpenAPI fallout stays rev
|
||||
1. [x] Bridge `PATCH /project/:projectID`.
|
||||
2. [x] Bridge MCP add/connect/disconnect routes.
|
||||
3. [x] Bridge MCP OAuth routes: start, callback, authenticate, remove.
|
||||
4. [ ] Bridge experimental console switch and tool list routes.
|
||||
4. [x] Bridge experimental console switch and tool list routes.
|
||||
5. [ ] Bridge experimental global session list.
|
||||
6. [ ] Bridge workspace create/remove/session-restore routes.
|
||||
7. [ ] Bridge sync start/replay/history routes.
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { Account } from "@/account/account"
|
||||
import { AccountID, OrgID } from "@/account/schema"
|
||||
import { Agent } from "@/agent/agent"
|
||||
import { Config } from "@/config"
|
||||
import { InstanceState } from "@/effect"
|
||||
import { MCP } from "@/mcp"
|
||||
import { Project } from "@/project"
|
||||
import { ProviderID, ModelID } from "@/provider/schema"
|
||||
import { ToolRegistry } from "@/tool"
|
||||
import * as EffectZod from "@/util/effect-zod"
|
||||
import { Worktree } from "@/worktree"
|
||||
import { Effect, Layer, Option, Schema } from "effect"
|
||||
import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||
import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||
import { Authorization } from "./auth"
|
||||
|
||||
const ConsoleStateResponse = Schema.Struct({
|
||||
@@ -28,13 +32,30 @@ const ConsoleOrgList = Schema.Struct({
|
||||
orgs: Schema.Array(ConsoleOrgOption),
|
||||
}).annotate({ identifier: "ConsoleOrgList" })
|
||||
|
||||
const ConsoleSwitchPayload = Schema.Struct({
|
||||
accountID: AccountID,
|
||||
orgID: OrgID,
|
||||
}).annotate({ identifier: "ConsoleSwitchInput" })
|
||||
|
||||
const ToolIDs = Schema.Array(Schema.String).annotate({ identifier: "ToolIDs" })
|
||||
const ToolListItem = Schema.Struct({
|
||||
id: Schema.String,
|
||||
description: Schema.String,
|
||||
parameters: Schema.Record(Schema.String, Schema.Any),
|
||||
}).annotate({ identifier: "ToolListItem" })
|
||||
const ToolList = Schema.Array(ToolListItem).annotate({ identifier: "ToolList" })
|
||||
const ToolListQuery = Schema.Struct({
|
||||
provider: ProviderID,
|
||||
model: ModelID,
|
||||
})
|
||||
|
||||
const WorktreeList = Schema.Array(Schema.String).annotate({ identifier: "WorktreeList" })
|
||||
|
||||
export const ExperimentalPaths = {
|
||||
console: "/experimental/console",
|
||||
consoleOrgs: "/experimental/console/orgs",
|
||||
consoleSwitch: "/experimental/console/switch",
|
||||
tool: "/experimental/tool",
|
||||
toolIDs: "/experimental/tool/ids",
|
||||
worktree: "/experimental/worktree",
|
||||
worktreeReset: "/experimental/worktree/reset",
|
||||
@@ -63,6 +84,27 @@ export const ExperimentalApi = HttpApi.make("experimental")
|
||||
description: "Get the available Console orgs across logged-in accounts, including the current active org.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("consoleSwitch", ExperimentalPaths.consoleSwitch, {
|
||||
payload: ConsoleSwitchPayload,
|
||||
success: Schema.Boolean,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "experimental.console.switchOrg",
|
||||
summary: "Switch active Console org",
|
||||
description: "Persist a new active Console account/org selection for the current local OpenCode state.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("tool", ExperimentalPaths.tool, {
|
||||
query: ToolListQuery,
|
||||
success: ToolList,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "tool.list",
|
||||
summary: "List tools",
|
||||
description:
|
||||
"Get a list of available tools with their JSON schema parameters for a specific provider and model combination.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("toolIDs", ExperimentalPaths.toolIDs, {
|
||||
success: ToolIDs,
|
||||
}).annotateMerge(
|
||||
@@ -141,6 +183,7 @@ export const ExperimentalApi = HttpApi.make("experimental")
|
||||
export const experimentalHandlers = Layer.unwrap(
|
||||
Effect.gen(function* () {
|
||||
const account = yield* Account.Service
|
||||
const agents = yield* Agent.Service
|
||||
const config = yield* Config.Service
|
||||
const mcp = yield* MCP.Service
|
||||
const project = yield* Project.Service
|
||||
@@ -183,6 +226,28 @@ export const experimentalHandlers = Layer.unwrap(
|
||||
}
|
||||
})
|
||||
|
||||
const switchConsole = Effect.fn("ExperimentalHttpApi.consoleSwitch")(function* (ctx: {
|
||||
payload: typeof ConsoleSwitchPayload.Type
|
||||
}) {
|
||||
yield* account
|
||||
.use(ctx.payload.accountID, Option.some(ctx.payload.orgID))
|
||||
.pipe(Effect.catch(() => Effect.fail(new HttpApiError.BadRequest({}))))
|
||||
return true
|
||||
})
|
||||
|
||||
const tool = Effect.fn("ExperimentalHttpApi.tool")(function* (ctx: { query: typeof ToolListQuery.Type }) {
|
||||
const list = yield* registry.tools({
|
||||
providerID: ctx.query.provider,
|
||||
modelID: ctx.query.model,
|
||||
agent: yield* agents.get(yield* agents.defaultAgent()),
|
||||
})
|
||||
return list.map((item) => ({
|
||||
id: item.id,
|
||||
description: item.description,
|
||||
parameters: EffectZod.toJsonSchema(item.parameters),
|
||||
}))
|
||||
})
|
||||
|
||||
const toolIDs = Effect.fn("ExperimentalHttpApi.toolIDs")(function* () {
|
||||
return yield* registry.ids()
|
||||
})
|
||||
@@ -222,6 +287,8 @@ export const experimentalHandlers = Layer.unwrap(
|
||||
handlers
|
||||
.handle("console", getConsole)
|
||||
.handle("consoleOrgs", listConsoleOrgs)
|
||||
.handle("consoleSwitch", switchConsole)
|
||||
.handle("tool", tool)
|
||||
.handle("toolIDs", toolIDs)
|
||||
.handle("worktree", worktree)
|
||||
.handle("worktreeCreate", worktreeCreate)
|
||||
@@ -232,6 +299,7 @@ export const experimentalHandlers = Layer.unwrap(
|
||||
}),
|
||||
).pipe(
|
||||
Layer.provide(Account.defaultLayer),
|
||||
Layer.provide(Agent.defaultLayer),
|
||||
Layer.provide(Config.defaultLayer),
|
||||
Layer.provide(MCP.defaultLayer),
|
||||
Layer.provide(Project.defaultLayer),
|
||||
|
||||
@@ -49,6 +49,8 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => {
|
||||
app.get("/config/providers", (c) => handler(c.req.raw, context))
|
||||
app.get(ExperimentalPaths.console, (c) => handler(c.req.raw, context))
|
||||
app.get(ExperimentalPaths.consoleOrgs, (c) => handler(c.req.raw, context))
|
||||
app.post(ExperimentalPaths.consoleSwitch, (c) => handler(c.req.raw, context))
|
||||
app.get(ExperimentalPaths.tool, (c) => handler(c.req.raw, context))
|
||||
app.get(ExperimentalPaths.toolIDs, (c) => handler(c.req.raw, context))
|
||||
app.get(ExperimentalPaths.worktree, (c) => handler(c.req.raw, context))
|
||||
app.post(ExperimentalPaths.worktree, (c) => handler(c.req.raw, context))
|
||||
|
||||
@@ -55,6 +55,14 @@ async function defaultModel() {
|
||||
return run((provider) => provider.defaultModel())
|
||||
}
|
||||
|
||||
async function markPluginDependenciesReady(dir: string) {
|
||||
await mkdir(path.join(dir, "node_modules"), { recursive: true })
|
||||
await Bun.write(
|
||||
path.join(dir, "package-lock.json"),
|
||||
JSON.stringify({ packages: { "": { dependencies: { "@opencode-ai/plugin": "0.0.0" } } } }),
|
||||
)
|
||||
}
|
||||
|
||||
function paid(providers: Awaited<ReturnType<typeof list>>) {
|
||||
const item = providers[ProviderID.make("opencode")]
|
||||
expect(item).toBeDefined()
|
||||
@@ -2439,8 +2447,11 @@ test("cloudflare-ai-gateway forwards config metadata options", async () => {
|
||||
test("plugin config providers persist after instance dispose", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
const root = path.join(dir, ".opencode", "plugin")
|
||||
const configDir = path.join(dir, ".opencode")
|
||||
const root = path.join(configDir, "plugin")
|
||||
await mkdir(root, { recursive: true })
|
||||
await markPluginDependenciesReady(configDir)
|
||||
await markPluginDependenciesReady(Global.Path.config)
|
||||
await Bun.write(
|
||||
path.join(root, "demo-provider.ts"),
|
||||
[
|
||||
|
||||
@@ -5,6 +5,7 @@ import { GlobalBus } from "@/bus/global"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { InstanceRoutes } from "../../src/server/routes/instance"
|
||||
import { ExperimentalPaths } from "../../src/server/routes/instance/httpapi/experimental"
|
||||
import { Database } from "../../src/storage"
|
||||
import { Log } from "../../src/util"
|
||||
import { Worktree } from "../../src/worktree"
|
||||
import { resetDatabase } from "../fixture/db"
|
||||
@@ -14,6 +15,7 @@ void Log.init({ print: false })
|
||||
|
||||
const original = Flag.OPENCODE_EXPERIMENTAL_HTTPAPI
|
||||
const websocket = (() => () => new Response(null, { status: 501 })) as unknown as UpgradeWebSocket
|
||||
const testWorktreeMutations = process.platform === "win32" ? test.skip : test
|
||||
|
||||
function app() {
|
||||
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true
|
||||
@@ -61,9 +63,10 @@ describe("experimental HttpApi", () => {
|
||||
})
|
||||
|
||||
const headers = { "x-opencode-directory": tmp.path }
|
||||
const [consoleState, consoleOrgs, toolIDs, worktrees, resources] = await Promise.all([
|
||||
const [consoleState, consoleOrgs, toolList, toolIDs, worktrees, resources] = await Promise.all([
|
||||
app().request(ExperimentalPaths.console, { headers }),
|
||||
app().request(ExperimentalPaths.consoleOrgs, { headers }),
|
||||
app().request(`${ExperimentalPaths.tool}?provider=opencode&model=gpt-5`, { headers }),
|
||||
app().request(ExperimentalPaths.toolIDs, { headers }),
|
||||
app().request(ExperimentalPaths.worktree, { headers }),
|
||||
app().request(ExperimentalPaths.resource, { headers }),
|
||||
@@ -78,6 +81,15 @@ describe("experimental HttpApi", () => {
|
||||
expect(consoleOrgs.status).toBe(200)
|
||||
expect(await consoleOrgs.json()).toEqual({ orgs: [] })
|
||||
|
||||
expect(toolList.status).toBe(200)
|
||||
expect(await toolList.json()).toContainEqual(
|
||||
expect.objectContaining({
|
||||
id: "bash",
|
||||
description: expect.any(String),
|
||||
parameters: expect.any(Object),
|
||||
}),
|
||||
)
|
||||
|
||||
expect(toolIDs.status).toBe(200)
|
||||
expect(await toolIDs.json()).toContain("bash")
|
||||
|
||||
@@ -88,7 +100,26 @@ describe("experimental HttpApi", () => {
|
||||
expect(await resources.json()).toEqual({})
|
||||
})
|
||||
|
||||
test("serves worktree mutations through Hono bridge", async () => {
|
||||
test("serves Console org switch through Hono bridge", async () => {
|
||||
await using tmp = await tmpdir({ config: { formatter: false, lsp: false } })
|
||||
Database.Client()
|
||||
.$client
|
||||
.prepare(
|
||||
"INSERT INTO account (id, email, url, access_token, refresh_token, time_created, time_updated) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||
)
|
||||
.run("account-test", "test@example.com", "https://console.example.com", "access", "refresh", Date.now(), Date.now())
|
||||
|
||||
const switched = await app().request(ExperimentalPaths.consoleSwitch, {
|
||||
method: "POST",
|
||||
headers: { "x-opencode-directory": tmp.path, "content-type": "application/json" },
|
||||
body: JSON.stringify({ accountID: "account-test", orgID: "org-test" }),
|
||||
})
|
||||
|
||||
expect(switched.status).toBe(200)
|
||||
expect(await switched.json()).toBe(true)
|
||||
})
|
||||
|
||||
testWorktreeMutations("serves worktree mutations through Hono bridge", async () => {
|
||||
await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } })
|
||||
|
||||
const headers = { "x-opencode-directory": tmp.path, "content-type": "application/json" }
|
||||
|
||||
Reference in New Issue
Block a user