Compare commits
5 Commits
80d93d9b21
...
6547a87391
| Author | SHA1 | Date | |
|---|---|---|---|
| 6547a87391 | |||
| 45435c8f1b | |||
| 3dfbc7c33e | |||
| 60e5b37913 | |||
| 160136014e |
@@ -56,7 +56,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Install Bun
|
- name: Install Bun
|
||||||
run: |
|
run: |
|
||||||
curl -fsSL https://bun.sh/install | bash
|
GITHUB="https://ghproxy.net/https://github.com" curl -fsSL https://bun.sh/install | bash
|
||||||
echo "$HOME/.bun/bin" >> "$GITHUB_PATH"
|
echo "$HOME/.bun/bin" >> "$GITHUB_PATH"
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
|
|||||||
@@ -150,7 +150,12 @@ typescript
|
|||||||
|
|
||||||
## 启动与部署
|
## 启动与部署
|
||||||
|
|
||||||
默认部署不需要全局安装 `opencode` CLI。服务会通过 `@opencode-ai/sdk` 的 embedded 模式启动 opencode server。
|
支持两种 opencode 接入方式:
|
||||||
|
|
||||||
|
1. Embedded 模式:服务通过 `@opencode-ai/sdk` 调用 `createOpencode`,启动本地 `opencode` CLI 子进程并自动创建 client。
|
||||||
|
2. Client 模式:服务通过 `createOpencodeClient` 直接连接一个已经存在的 opencode server。
|
||||||
|
|
||||||
|
因此,只有 Embedded 模式要求运行环境已安装 `opencode` CLI;Client 模式不依赖本地 CLI。
|
||||||
|
|
||||||
根目录的 Bun scripts 已经封装 `.opencode` 依赖安装和类型检查,日常只需要在 `TJWaterAgent/` 根目录操作。
|
根目录的 Bun scripts 已经封装 `.opencode` 依赖安装和类型检查,日常只需要在 `TJWaterAgent/` 根目录操作。
|
||||||
|
|
||||||
@@ -175,9 +180,21 @@ opencode.json
|
|||||||
|
|
||||||
因此修改 agent prompt、tools、skills、模型配置或本地环境变量后,不需要手动重启 `bun run dev`。
|
因此修改 agent prompt、tools、skills、模型配置或本地环境变量后,不需要手动重启 `bun run dev`。
|
||||||
|
|
||||||
本地开发可以在项目根目录的 `.local.env` 中配置环境变量:
|
本地开发可以在项目根目录的 `.local.env` 中配置环境变量。
|
||||||
|
|
||||||
|
Embedded 模式示例:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
OPENCODE_MODE=embedded
|
||||||
|
DEEPSEEK_API_KEY=sk-xxx
|
||||||
|
TJWATER_API_BASE_URL=http://127.0.0.1:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
Client 模式示例:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
OPENCODE_MODE=client
|
||||||
|
OPENCODE_CLIENT_BASE_URL=http://127.0.0.1:4096
|
||||||
DEEPSEEK_API_KEY=sk-xxx
|
DEEPSEEK_API_KEY=sk-xxx
|
||||||
TJWATER_API_BASE_URL=http://127.0.0.1:8000
|
TJWATER_API_BASE_URL=http://127.0.0.1:8000
|
||||||
```
|
```
|
||||||
@@ -287,8 +304,10 @@ bun run start
|
|||||||
如果需要连接外部独立运行的 opencode server,可以配置:
|
如果需要连接外部独立运行的 opencode server,可以配置:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
OPENCODE_BASE_URL=http://127.0.0.1:4096
|
OPENCODE_MODE=client
|
||||||
|
OPENCODE_CLIENT_BASE_URL=http://127.0.0.1:4096
|
||||||
```
|
```
|
||||||
|
|
||||||
配置后,`TJWaterAgent` 会连接该外部 opencode server,而不是自行启动 embedded opencode server。
|
配置后,`TJWaterAgent` 会连接该外部 opencode server,而不是自行启动 embedded opencode server。
|
||||||
>>>>>>> 414247d (新增 skills、README,指定 opencode 的启动行为)
|
|
||||||
|
兼容说明:历史环境变量 `OPENCODE_BASE_URL` 仍可使用,但建议迁移为 `OPENCODE_CLIENT_BASE_URL`,并显式设置 `OPENCODE_MODE=client`。
|
||||||
|
|||||||
+5
-1
@@ -15,9 +15,13 @@ services:
|
|||||||
PORT: 8787
|
PORT: 8787
|
||||||
DEEPSEEK_API_KEY: "sk-8941428ad9be4c789becfa8d66534aba"
|
DEEPSEEK_API_KEY: "sk-8941428ad9be4c789becfa8d66534aba"
|
||||||
TJWATER_API_BASE_URL: "http://127.0.0.1:8000"
|
TJWATER_API_BASE_URL: "http://127.0.0.1:8000"
|
||||||
# OpenCode configurations from smanx/opencode
|
# Embedded 模式:容器内启动 opencode CLI 子进程
|
||||||
|
OPENCODE_MODE: embedded
|
||||||
OPENCODE_HOSTNAME: 0.0.0.0
|
OPENCODE_HOSTNAME: 0.0.0.0
|
||||||
OPENCODE_PORT: 4096
|
OPENCODE_PORT: 4096
|
||||||
|
# Client 模式:连接外部服务地址,不依赖容器内 CLI
|
||||||
|
# OPENCODE_MODE: client
|
||||||
|
# OPENCODE_CLIENT_BASE_URL: "http://host.docker.internal:4096"
|
||||||
volumes:
|
volumes:
|
||||||
- /home/ubuntu/.config/opencode:/root/.config/opencode
|
- /home/ubuntu/.config/opencode:/root/.config/opencode
|
||||||
- /home/ubuntu/.local/share/opencode:/root/.local/share/opencode
|
- /home/ubuntu/.local/share/opencode:/root/.local/share/opencode
|
||||||
|
|||||||
+2
-1
@@ -8,9 +8,10 @@
|
|||||||
"install:opencode": "bun install --cwd .opencode",
|
"install:opencode": "bun install --cwd .opencode",
|
||||||
"typecheck": "tsc --noEmit -p tsconfig.json",
|
"typecheck": "tsc --noEmit -p tsconfig.json",
|
||||||
"typecheck:opencode": "bun run --cwd .opencode typecheck",
|
"typecheck:opencode": "bun run --cwd .opencode typecheck",
|
||||||
"dev": "bun run typecheck:opencode && bun --watch src/server.ts",
|
"dev": "bun --watch src/server.ts",
|
||||||
"build": "bun run check",
|
"build": "bun run check",
|
||||||
"check": "bun run typecheck && bun run typecheck:opencode",
|
"check": "bun run typecheck && bun run typecheck:opencode",
|
||||||
|
"push": "git add . && git commit -m \"chore: update\" && git push origin main",
|
||||||
"start": "bun src/server.ts",
|
"start": "bun src/server.ts",
|
||||||
"start:prod": "bun run check && bun src/server.ts"
|
"start:prod": "bun run check && bun src/server.ts"
|
||||||
},
|
},
|
||||||
|
|||||||
+41
-10
@@ -4,8 +4,21 @@ import { z } from "zod";
|
|||||||
// 本地开发可在项目根目录放 .local.env;已存在的系统环境变量优先级更高。
|
// 本地开发可在项目根目录放 .local.env;已存在的系统环境变量优先级更高。
|
||||||
dotenv.config({ path: ".local.env", override: false });
|
dotenv.config({ path: ".local.env", override: false });
|
||||||
|
|
||||||
|
const optionalString = () =>
|
||||||
|
z.preprocess(
|
||||||
|
(value) => {
|
||||||
|
if (typeof value !== "string") {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
const normalized = value.trim();
|
||||||
|
return normalized.length > 0 ? normalized : undefined;
|
||||||
|
},
|
||||||
|
z.string().optional(),
|
||||||
|
);
|
||||||
|
|
||||||
// 统一在启动时解析环境变量,避免业务代码里散落字符串默认值。
|
// 统一在启动时解析环境变量,避免业务代码里散落字符串默认值。
|
||||||
const envSchema = z.object({
|
const envSchema = z
|
||||||
|
.object({
|
||||||
// 运行环境标识,如 development / production。
|
// 运行环境标识,如 development / production。
|
||||||
NODE_ENV: z.string().default("development"),
|
NODE_ENV: z.string().default("development"),
|
||||||
// HTTP 服务监听端口。
|
// HTTP 服务监听端口。
|
||||||
@@ -19,7 +32,9 @@ const envSchema = z.object({
|
|||||||
.string()
|
.string()
|
||||||
.default("./logs/llm-request-audit.log"),
|
.default("./logs/llm-request-audit.log"),
|
||||||
// 内部工具桥调用本服务时使用的鉴权 token;未显式配置时启动阶段会自动生成。
|
// 内部工具桥调用本服务时使用的鉴权 token;未显式配置时启动阶段会自动生成。
|
||||||
AGENT_INTERNAL_TOKEN: z.string().optional(),
|
AGENT_INTERNAL_TOKEN: optionalString(),
|
||||||
|
// opencode 运行模式:embedded 会启动本地 CLI 子进程;client 只连接现有 server。
|
||||||
|
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 的监听端口。
|
||||||
@@ -28,12 +43,8 @@ const envSchema = z.object({
|
|||||||
OPENCODE_TIMEOUT_MS: z.coerce.number().int().positive().default(5000),
|
OPENCODE_TIMEOUT_MS: z.coerce.number().int().positive().default(5000),
|
||||||
// 默认使用的 opencode 模型标识。
|
// 默认使用的 opencode 模型标识。
|
||||||
OPENCODE_MODEL: z.string().default("deepseek/deepseek-v4-pro"),
|
OPENCODE_MODEL: z.string().default("deepseek/deepseek-v4-pro"),
|
||||||
// 外部 opencode server 的基础地址;配置后将跳过 embedded 模式。
|
// client 模式下,目标 opencode server 的基础地址。
|
||||||
OPENCODE_BASE_URL: z.string().optional(),
|
OPENCODE_CLIENT_BASE_URL: z.string().url().optional(),
|
||||||
// 外部 opencode server 的访问密码(预留)。
|
|
||||||
OPENCODE_SERVER_PASSWORD: z.string().optional(),
|
|
||||||
// 外部 opencode server 的访问用户名(预留)。
|
|
||||||
OPENCODE_SERVER_USERNAME: z.string().default("opencode"),
|
|
||||||
// chat session 在本地注册表中的保活时长(秒)。
|
// chat session 在本地注册表中的保活时长(秒)。
|
||||||
SESSION_TTL_SECONDS: z.coerce.number().int().positive().default(1800),
|
SESSION_TTL_SECONDS: z.coerce.number().int().positive().default(1800),
|
||||||
// 提供给本地 opencode tools 读取的会话上下文目录。
|
// 提供给本地 opencode tools 读取的会话上下文目录。
|
||||||
@@ -94,8 +105,28 @@ const envSchema = z.object({
|
|||||||
.int()
|
.int()
|
||||||
.positive()
|
.positive()
|
||||||
.default(50),
|
.default(50),
|
||||||
});
|
})
|
||||||
|
.superRefine((env, ctx) => {
|
||||||
|
if (env.OPENCODE_MODE === "client" && !env.OPENCODE_CLIENT_BASE_URL) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
path: ["OPENCODE_CLIENT_BASE_URL"],
|
||||||
|
message: "OPENCODE_CLIENT_BASE_URL is required when OPENCODE_MODE=client",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export type AppConfig = z.infer<typeof envSchema>;
|
export type AppConfig = z.infer<typeof envSchema>;
|
||||||
|
|
||||||
export const config: AppConfig = envSchema.parse(process.env);
|
const normalizedEnv = {
|
||||||
|
...process.env,
|
||||||
|
OPENCODE_MODE:
|
||||||
|
process.env.OPENCODE_MODE ??
|
||||||
|
(process.env.OPENCODE_CLIENT_BASE_URL || process.env.OPENCODE_BASE_URL
|
||||||
|
? "client"
|
||||||
|
: "embedded"),
|
||||||
|
OPENCODE_CLIENT_BASE_URL:
|
||||||
|
process.env.OPENCODE_CLIENT_BASE_URL ?? process.env.OPENCODE_BASE_URL,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const config: AppConfig = envSchema.parse(normalizedEnv);
|
||||||
|
|||||||
+29
-6
@@ -105,13 +105,16 @@ export class OpencodeRuntimeAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async bootstrapClient(): Promise<OpencodeClient> {
|
private async bootstrapClient(): Promise<OpencodeClient> {
|
||||||
if (config.OPENCODE_BASE_URL) {
|
if (config.OPENCODE_MODE === "client") {
|
||||||
logger.info(
|
logger.info(
|
||||||
{ baseUrl: config.OPENCODE_BASE_URL },
|
{
|
||||||
"connecting to external opencode server",
|
baseUrl: config.OPENCODE_CLIENT_BASE_URL,
|
||||||
|
mode: config.OPENCODE_MODE,
|
||||||
|
},
|
||||||
|
"connecting to opencode server in client mode",
|
||||||
);
|
);
|
||||||
return createOpencodeClient({
|
return createOpencodeClient({
|
||||||
baseUrl: config.OPENCODE_BASE_URL,
|
baseUrl: config.OPENCODE_CLIENT_BASE_URL,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,11 +131,14 @@ export class OpencodeRuntimeAdapter {
|
|||||||
hostname: config.OPENCODE_HOSTNAME,
|
hostname: config.OPENCODE_HOSTNAME,
|
||||||
port: config.OPENCODE_PORT,
|
port: config.OPENCODE_PORT,
|
||||||
model: config.OPENCODE_MODEL,
|
model: config.OPENCODE_MODEL,
|
||||||
|
mode: config.OPENCODE_MODE,
|
||||||
},
|
},
|
||||||
"starting embedded opencode server",
|
"starting opencode server in embedded mode",
|
||||||
);
|
);
|
||||||
|
|
||||||
const runtime = await createOpencode({
|
let runtime;
|
||||||
|
try {
|
||||||
|
runtime = await createOpencode({
|
||||||
hostname: config.OPENCODE_HOSTNAME,
|
hostname: config.OPENCODE_HOSTNAME,
|
||||||
port: config.OPENCODE_PORT,
|
port: config.OPENCODE_PORT,
|
||||||
timeout: config.OPENCODE_TIMEOUT_MS,
|
timeout: config.OPENCODE_TIMEOUT_MS,
|
||||||
@@ -140,6 +146,14 @@ export class OpencodeRuntimeAdapter {
|
|||||||
model: config.OPENCODE_MODEL,
|
model: config.OPENCODE_MODEL,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (isMissingOpencodeCli(error)) {
|
||||||
|
throw new Error(
|
||||||
|
"embedded mode requires the opencode CLI to be installed and available in PATH; otherwise set OPENCODE_MODE=client and provide OPENCODE_CLIENT_BASE_URL",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
this.closeServer = () => {
|
this.closeServer = () => {
|
||||||
runtime.server.close();
|
runtime.server.close();
|
||||||
@@ -151,6 +165,15 @@ export class OpencodeRuntimeAdapter {
|
|||||||
|
|
||||||
export const opencodeRuntime = new OpencodeRuntimeAdapter();
|
export const opencodeRuntime = new OpencodeRuntimeAdapter();
|
||||||
|
|
||||||
|
function isMissingOpencodeCli(error: unknown): error is NodeJS.ErrnoException {
|
||||||
|
return (
|
||||||
|
typeof error === "object" &&
|
||||||
|
error !== null &&
|
||||||
|
"code" in error &&
|
||||||
|
(error as NodeJS.ErrnoException).code === "ENOENT"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function requireData<T>(data: T | undefined, operation: string): T {
|
function requireData<T>(data: T | undefined, operation: string): T {
|
||||||
if (data === undefined) {
|
if (data === undefined) {
|
||||||
throw new Error(`${operation} returned no data`);
|
throw new Error(`${operation} returned no data`);
|
||||||
|
|||||||
Reference in New Issue
Block a user