fix: avoid embedded opencode port conflicts
This commit is contained in:
+1
-1
@@ -5,7 +5,7 @@ LOG_LEVEL=info
|
|||||||
OPENCODE_MODE=embedded
|
OPENCODE_MODE=embedded
|
||||||
OPENCODE_MODEL=deepseek/deepseek-v4-flash
|
OPENCODE_MODEL=deepseek/deepseek-v4-flash
|
||||||
OPENCODE_HOSTNAME=127.0.0.1
|
OPENCODE_HOSTNAME=127.0.0.1
|
||||||
OPENCODE_PORT=4096
|
OPENCODE_PORT=0
|
||||||
|
|
||||||
AGENT_LOCAL_USER_ID=local-user
|
AGENT_LOCAL_USER_ID=local-user
|
||||||
# 必须填写 Server /api/v1/projects 返回的项目 UUID。
|
# 必须填写 Server /api/v1/projects 返回的项目 UUID。
|
||||||
|
|||||||
+2
-2
@@ -58,8 +58,8 @@ const envSchema = z
|
|||||||
OPENCODE_MODE: z.enum(["embedded", "client"]).default("embedded"),
|
OPENCODE_MODE: z.enum(["embedded", "client"]).default("embedded"),
|
||||||
// embedded opencode server 的监听地址。
|
// embedded opencode server 的监听地址。
|
||||||
OPENCODE_HOSTNAME: z.string().default("127.0.0.1"),
|
OPENCODE_HOSTNAME: z.string().default("127.0.0.1"),
|
||||||
// embedded opencode server 的监听端口。
|
// embedded opencode server 的监听端口;0 表示由系统动态分配。
|
||||||
OPENCODE_PORT: z.coerce.number().int().positive().default(4096),
|
OPENCODE_PORT: z.coerce.number().int().nonnegative().default(0),
|
||||||
// opencode SDK 启动或连接运行时时的超时时间(毫秒)。
|
// opencode SDK 启动或连接运行时时的超时时间(毫秒)。
|
||||||
OPENCODE_TIMEOUT_MS: z.coerce.number().int().positive().default(5000),
|
OPENCODE_TIMEOUT_MS: z.coerce.number().int().positive().default(5000),
|
||||||
// 默认使用的 opencode 模型标识。
|
// 默认使用的 opencode 模型标识。
|
||||||
|
|||||||
+46
-3
@@ -4,6 +4,7 @@ import {
|
|||||||
type OpencodeClient,
|
type OpencodeClient,
|
||||||
} from "@opencode-ai/sdk/v2";
|
} from "@opencode-ai/sdk/v2";
|
||||||
import { existsSync, readFileSync } from "node:fs";
|
import { existsSync, readFileSync } from "node:fs";
|
||||||
|
import { createServer } from "node:net";
|
||||||
import { resolve } from "node:path";
|
import { resolve } from "node:path";
|
||||||
|
|
||||||
import { config } from "../config.js";
|
import { config } from "../config.js";
|
||||||
@@ -44,6 +45,34 @@ type RuntimeMessage = {
|
|||||||
const getRuntimeMessageRole = (message: RuntimeMessage) => message.info.role;
|
const getRuntimeMessageRole = (message: RuntimeMessage) => message.info.role;
|
||||||
const getRuntimeMessageId = (message: RuntimeMessage) => message.info.id;
|
const getRuntimeMessageId = (message: RuntimeMessage) => message.info.id;
|
||||||
|
|
||||||
|
const isTcpPortAvailable = (hostname: string, port: number) =>
|
||||||
|
new Promise<boolean>((resolve, reject) => {
|
||||||
|
const server = createServer();
|
||||||
|
server.unref();
|
||||||
|
server.once("error", (error: NodeJS.ErrnoException) => {
|
||||||
|
if (error.code === "EADDRINUSE") {
|
||||||
|
resolve(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
server.listen({ host: hostname, port, exclusive: true }, () => {
|
||||||
|
server.close((error) => (error ? reject(error) : resolve(true)));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export const resolveEmbeddedOpencodePort = async (
|
||||||
|
hostname: string,
|
||||||
|
configuredPort: number,
|
||||||
|
) => {
|
||||||
|
if (configuredPort === 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return (await isTcpPortAvailable(hostname, configuredPort))
|
||||||
|
? configuredPort
|
||||||
|
: 0;
|
||||||
|
};
|
||||||
|
|
||||||
export class OpencodeRuntimeAdapter {
|
export class OpencodeRuntimeAdapter {
|
||||||
private clientPromise: Promise<OpencodeClient> | null = null;
|
private clientPromise: Promise<OpencodeClient> | null = null;
|
||||||
private closeServer: (() => void) | null = null;
|
private closeServer: (() => void) | null = null;
|
||||||
@@ -408,10 +437,24 @@ export class OpencodeRuntimeAdapter {
|
|||||||
process.env.TJWATER_AGENT_INTERNAL_TOKEN ??
|
process.env.TJWATER_AGENT_INTERNAL_TOKEN ??
|
||||||
"";
|
"";
|
||||||
|
|
||||||
|
const port = await resolveEmbeddedOpencodePort(
|
||||||
|
config.OPENCODE_HOSTNAME,
|
||||||
|
config.OPENCODE_PORT,
|
||||||
|
);
|
||||||
|
if (port !== config.OPENCODE_PORT) {
|
||||||
|
logger.warn(
|
||||||
|
{
|
||||||
|
hostname: config.OPENCODE_HOSTNAME,
|
||||||
|
configuredPort: config.OPENCODE_PORT,
|
||||||
|
},
|
||||||
|
"configured opencode port is occupied; falling back to a dynamic port",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
{
|
{
|
||||||
hostname: config.OPENCODE_HOSTNAME,
|
hostname: config.OPENCODE_HOSTNAME,
|
||||||
port: config.OPENCODE_PORT,
|
port,
|
||||||
model: config.OPENCODE_MODEL,
|
model: config.OPENCODE_MODEL,
|
||||||
mode: config.OPENCODE_MODE,
|
mode: config.OPENCODE_MODE,
|
||||||
},
|
},
|
||||||
@@ -423,7 +466,7 @@ export class OpencodeRuntimeAdapter {
|
|||||||
try {
|
try {
|
||||||
runtime = await createOpencode({
|
runtime = await createOpencode({
|
||||||
hostname: config.OPENCODE_HOSTNAME,
|
hostname: config.OPENCODE_HOSTNAME,
|
||||||
port: config.OPENCODE_PORT,
|
port,
|
||||||
timeout: config.OPENCODE_TIMEOUT_MS,
|
timeout: config.OPENCODE_TIMEOUT_MS,
|
||||||
config: buildOpencodeConfig(),
|
config: buildOpencodeConfig(),
|
||||||
});
|
});
|
||||||
@@ -440,7 +483,7 @@ export class OpencodeRuntimeAdapter {
|
|||||||
{
|
{
|
||||||
elapsedMs: Math.max(0, Date.now() - startedAt),
|
elapsedMs: Math.max(0, Date.now() - startedAt),
|
||||||
hostname: config.OPENCODE_HOSTNAME,
|
hostname: config.OPENCODE_HOSTNAME,
|
||||||
port: config.OPENCODE_PORT,
|
baseUrl: runtime.server.url,
|
||||||
mode: config.OPENCODE_MODE,
|
mode: config.OPENCODE_MODE,
|
||||||
},
|
},
|
||||||
"opencode server started in embedded mode",
|
"opencode server started in embedded mode",
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import { describe, expect, it } from "bun:test";
|
import { describe, expect, it } from "bun:test";
|
||||||
import { type OpencodeClient } from "@opencode-ai/sdk/v2";
|
import { type OpencodeClient } from "@opencode-ai/sdk/v2";
|
||||||
|
import { createServer } from "node:net";
|
||||||
|
|
||||||
import { config } from "../../src/config.js";
|
import { config } from "../../src/config.js";
|
||||||
import { OpencodeRuntimeAdapter } from "../../src/runtime/opencode.js";
|
import {
|
||||||
|
OpencodeRuntimeAdapter,
|
||||||
|
resolveEmbeddedOpencodePort,
|
||||||
|
} from "../../src/runtime/opencode.js";
|
||||||
|
|
||||||
const createRuntimeAdapter = (
|
const createRuntimeAdapter = (
|
||||||
messages: unknown[],
|
messages: unknown[],
|
||||||
@@ -87,6 +91,32 @@ describe("OpencodeRuntimeAdapter.ensureClient", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("resolveEmbeddedOpencodePort", () => {
|
||||||
|
it("falls back to a dynamic port when the configured port is occupied", async () => {
|
||||||
|
const server = createServer();
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
server.once("error", reject);
|
||||||
|
server.listen(0, "127.0.0.1", resolve);
|
||||||
|
});
|
||||||
|
|
||||||
|
const address = server.address();
|
||||||
|
if (!address || typeof address === "string") {
|
||||||
|
server.close();
|
||||||
|
throw new Error("expected a TCP server address");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await expect(
|
||||||
|
resolveEmbeddedOpencodePort("127.0.0.1", address.port),
|
||||||
|
).resolves.toBe(0);
|
||||||
|
} finally {
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
server.close((error) => (error ? reject(error) : resolve()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("OpencodeRuntimeAdapter.warmup", () => {
|
describe("OpencodeRuntimeAdapter.warmup", () => {
|
||||||
it("initializes the project session and model tools before reporting ready", async () => {
|
it("initializes the project session and model tools before reporting ready", async () => {
|
||||||
const calls: string[] = [];
|
const calls: string[] = [];
|
||||||
|
|||||||
Reference in New Issue
Block a user