Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion example/parsar/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Open http://127.0.0.1:18180. `OPENAI_BASE_URL` ends in `/v1`; remote endpoints r
| Object | What it is |
| --- | --- |
| **资源库** | Building blocks, grouped by what uses them: models for Agent engines; Skills, MCP servers, Plugins, knowledge and instruction fragments for Agents; credentials for MCP. Resources are shared: editing one affects everything that uses it, and an item in use cannot be removed. |
| **环境模板** | The prepared workspace: npm and Python packages, a Bash setup script, environment variables, Feishu bot variables and initial files. |
| **环境模板** | The prepared workspace: npm and Python packages, a Bash setup script, network access (allowed, blocked or limited to listed domains), environment variables, Feishu bot variables and initial files. |
| **Agent** | What people talk to: name, description, instructions, how it runs, one required environment template, capabilities and behavior switches. How it runs mirrors a local Codex configuration: Harness, model, reasoning effort, web search and, under **高级**, reasoning summary, verbosity and service tier. **复制** creates an independent copy. |
| **飞书机器人** | One bot is bound to at most one Agent. It routes its chats to that Agent and is the Agent’s Feishu identity in every conversation, including Web chats. |
| **对话** | One public Core Session with its own Environment. It freezes the Agent and environment configuration when it starts. |
Expand Down
2 changes: 1 addition & 1 deletion example/parsar/README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ pnpm --filter @oac/parsar-example dev
| 对象 | 含义 |
| --- | --- |
| **资源库** | 组装用的零件,按使用方分组:模型供 Agent 的执行引擎使用;Skills、MCP 服务、Plugins、知识和指令片段供 Agent 使用;凭据供 MCP 使用。资源是共享的:修改一项会影响所有使用它的对象,正在使用的条目不能移除。 |
| **环境模板** | 准备好的工作区:npm 与 Python 软件包、Bash 初始化脚本、环境变量、飞书机器人变量和初始文件。 |
| **环境模板** | 准备好的工作区:npm 与 Python 软件包、Bash 初始化脚本、联网权限(允许、禁止或仅允许指定域名)、环境变量、飞书机器人变量和初始文件。 |
| **Agent** | 用户对话的对象:名称、描述、指令、运行方式、一个必选的环境模板、能力和行为开关。运行方式对应本地 Codex 的配置:Harness、模型、推理强度、网页搜索,以及 **高级** 中的推理摘要、输出详细度和服务层级。**复制**会生成一份独立副本。 |
| **飞书机器人** | 一个机器人最多绑定一个 Agent。它把自己的聊天路由给该 Agent,并在所有对话中(包括网页对话)作为该 Agent 的飞书身份。 |
| **对话** | 一个公开 Core Session,拥有独立的 Environment,在开始时冻结 Agent 和环境配置。 |
Expand Down
16 changes: 16 additions & 0 deletions example/parsar/server/runtime-profile.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,20 @@ export function runtimeProfile(body, previous) {
Object.keys(body.packages).some((k) => !["npm", "python"].includes(k))
)
throw new AppError(400, "环境模板支持 npm 和 Python 软件包。");
// Core validates the domains; Parsar only maps the choice.
const access = body.network?.access;
let network;
if (access === "disabled") network = { access };
else if (access === "restricted") {
const domains = body.network.allowed_domains;
if (!Array.isArray(domains) || domains.some((d) => typeof d !== "string"))
throw new AppError(400, "请填写允许访问的域名,每行一个。");
network = {
access,
allowed_domains: domains.map((d) => d.trim()).filter(Boolean),
};
} else if (access !== undefined && access !== "enabled")
throw new AppError(400, "网络设置无效。");
const env = Object.create(null);
const input = body.env === undefined ? previous?.env || {} : body.env;
if (
Expand Down Expand Up @@ -94,6 +108,7 @@ export function runtimeProfile(body, previous) {
description: text(body.description || "", "说明", 1000),
packages,
setup_script: text(body.setup_script || "", "初始化脚本", 100000),
...(network && { network }),
env,
bot_env,
files: normalized,
Expand All @@ -114,6 +129,7 @@ export function runtimeEnvironment(runtime) {
return {
type: "openai_hosted",
packages: runtime.packages || { npm: [], python: [] },
...(runtime.network && { network: runtime.network }),
env: { ...runtime.env },
setup_commands: runtime.setup_script
? [{ command: runtime.setup_script, cwd: "/workspace" }]
Expand Down
49 changes: 49 additions & 0 deletions example/parsar/src/EnvironmentFields.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,54 @@ export function EnvironmentFields({
)}
/>
</Field>
<Field label={tr("网络")} id="environment-network">
<Select
id="environment-network"
value={value.network?.access ?? "enabled"}
onValueChange={(access) =>
onChange({
...value,
network:
access === "enabled"
? undefined
: access === "disabled"
? { access }
: { access: "restricted", allowed_domains: [] },
})
}
>
<SelectOption value="enabled">{tr("允许联网")}</SelectOption>
<SelectOption value="disabled">{tr("禁止联网")}</SelectOption>
<SelectOption value="restricted">{tr("仅允许指定域名")}</SelectOption>
</Select>
</Field>
{value.network?.access === "restricted" && (
<Field label={tr("允许的域名")} id="environment-domains">
<Textarea
id="environment-domains"
rows={4}
required
value={(value.network.allowed_domains || []).join("\n")}
placeholder={tr("每行一个,例如 {{value0}}", {
value0: "api.example.com",
})}
onChange={(e) => {
const domains = e.target.value.split("\n");
e.target.setCustomValidity(
domains.some((d) => d.includes("*"))
? tr("域名需完整填写,不支持通配符")
: domains.filter((d) => d.trim()).length > 100
? tr("最多 100 个域名")
: "",
);
onChange({
...value,
network: { access: "restricted", allowed_domains: domains },
});
}}
/>
</Field>
)}
<Group
title={tr("环境变量")}
action={
Expand Down Expand Up @@ -319,6 +367,7 @@ export function environmentSummary(value: RuntimeProfile) {
[
packages ? tr("{{count}} 个软件包", { count: packages }) : "",
value.setup_script ? tr("初始化脚本") : "",
value.network ? tr("限制联网") : "",
Object.keys(value.env || {}).length ? tr("环境变量") : "",
value.files?.length ? tr("初始文件") : "",
Object.keys(value.bot_env || {}).length ? tr("飞书机器人身份") : "",
Expand Down
1 change: 1 addition & 0 deletions example/parsar/src/lib/product.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export interface RuntimeProfile extends NamedResource {
description?: string;
packages?: { npm?: string[]; python?: string[] };
setup_script?: string;
network?: { access: "disabled" | "restricted"; allowed_domains?: string[] };
bot_env?: Record<string, string>;
env?: Record<string, string | null>;
prepare?: boolean;
Expand Down
26 changes: 26 additions & 0 deletions example/parsar/tests/product.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,32 @@ import { join } from "node:path";
import { homedir } from "node:os";
import { openStore, dataPath, AppError } from "../server/store.mjs";
import { productAPI } from "../server/product.mjs";
import { runtimeEnvironment } from "../server/runtime-profile.mjs";

test("environment template network maps to the official field and is omitted when enabled", async (t) => {
const store = openStore(":memory:");
t.after(() => store.close());
const { put } = await setup(store, async () => ({ id: randomUUID() }));
const request = async (network) =>
runtimeEnvironment(await put("runtimes", { name: "Net", network }));
assert.ok(!("network" in (await request(undefined))));
assert.ok(!("network" in (await request({ access: "enabled" }))));
assert.deepEqual((await request({ access: "disabled" })).network, {
access: "disabled",
});
assert.deepEqual(
(
await request({
access: "restricted",
allowed_domains: [" api.example.com ", ""],
})
).network,
{ access: "restricted", allowed_domains: ["api.example.com"] },
);
await assert.rejects(put("runtimes", { name: "Bad", network: { access: "x" } }), {
status: 400,
});
});

test("environment templates prepare hosted Sessions, protect variables and freeze retries", async (t) => {
const store = openStore(":memory:");
Expand Down
Loading