feat: add master agent chat controls

This commit is contained in:
kris
2026-03-31 19:30:26 +08:00
parent bc464905a5
commit e741952295
7 changed files with 1632 additions and 1 deletions

520
package-lock.json generated
View File

@@ -29,6 +29,7 @@
"eslint": "^9",
"eslint-config-next": "16.2.1",
"tailwindcss": "^4",
"tsx": "^4.21.0",
"typescript": "^5"
}
},
@@ -398,6 +399,448 @@
"tslib": "^2.4.0"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz",
"integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz",
"integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz",
"integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz",
"integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz",
"integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz",
"integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz",
"integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz",
"integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz",
"integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz",
"integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz",
"integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz",
"integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz",
"integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz",
"integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz",
"integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz",
"integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz",
"integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz",
"integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz",
"integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz",
"integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz",
"integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz",
"integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz",
"integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz",
"integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz",
"integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz",
"integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@eslint-community/eslint-utils": {
"version": "4.9.1",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
@@ -3665,6 +4108,48 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/esbuild": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz",
"integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.27.4",
"@esbuild/android-arm": "0.27.4",
"@esbuild/android-arm64": "0.27.4",
"@esbuild/android-x64": "0.27.4",
"@esbuild/darwin-arm64": "0.27.4",
"@esbuild/darwin-x64": "0.27.4",
"@esbuild/freebsd-arm64": "0.27.4",
"@esbuild/freebsd-x64": "0.27.4",
"@esbuild/linux-arm": "0.27.4",
"@esbuild/linux-arm64": "0.27.4",
"@esbuild/linux-ia32": "0.27.4",
"@esbuild/linux-loong64": "0.27.4",
"@esbuild/linux-mips64el": "0.27.4",
"@esbuild/linux-ppc64": "0.27.4",
"@esbuild/linux-riscv64": "0.27.4",
"@esbuild/linux-s390x": "0.27.4",
"@esbuild/linux-x64": "0.27.4",
"@esbuild/netbsd-arm64": "0.27.4",
"@esbuild/netbsd-x64": "0.27.4",
"@esbuild/openbsd-arm64": "0.27.4",
"@esbuild/openbsd-x64": "0.27.4",
"@esbuild/openharmony-arm64": "0.27.4",
"@esbuild/sunos-x64": "0.27.4",
"@esbuild/win32-arm64": "0.27.4",
"@esbuild/win32-ia32": "0.27.4",
"@esbuild/win32-x64": "0.27.4"
}
},
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
@@ -4413,6 +4898,21 @@
"node": ">=14.14"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/ftp": {
"version": "0.3.10",
"resolved": "https://registry.npmjs.org/ftp/-/ftp-0.3.10.tgz",
@@ -8061,6 +8561,26 @@
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/tsx": {
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "~0.27.0",
"get-tsconfig": "^4.7.5"
},
"bin": {
"tsx": "dist/cli.mjs"
},
"engines": {
"node": ">=18.0.0"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
}
},
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",

View File

@@ -9,6 +9,7 @@
"postbuild": "mkdir -p .next/standalone/.next && rm -rf .next/standalone/.next/static .next/standalone/public && cp -R .next/static .next/standalone/.next/static && cp -R public .next/standalone/public",
"start": "BOSS_RUNTIME_ROOT=\"$PWD\" BOSS_STATE_FILE=\"$PWD/data/boss-state.json\" node .next/standalone/server.js",
"lint": "eslint",
"test:master-agent-controls": "tsx --test tests/master-agent-chat-controls.test.ts",
"apk:debug": "cd android && ./gradlew assembleDebug && cd .. && zsh ./scripts/publish-apk-to-public.sh",
"apk:release": "zsh ./scripts/build-release-apk.sh",
"aab:release": "zsh ./scripts/build-release-aab.sh"
@@ -35,6 +36,7 @@
"eslint": "^9",
"eslint-config-next": "16.2.1",
"tailwindcss": "^4",
"tsx": "^4.21.0",
"typescript": "^5"
}
}

View File

@@ -0,0 +1,108 @@
import { NextRequest, NextResponse } from "next/server";
import { requireRequestSession } from "@/lib/boss-auth";
import {
getProjectAgentControls,
hasPersistedProject,
updateProjectAgentControls,
} from "@/lib/boss-data";
const reasoningEffortValues = new Set(["low", "medium", "high"]);
export async function GET(
request: NextRequest,
context: { params: Promise<{ projectId: string }> },
) {
const { projectId } = await context.params;
if (projectId !== "master-agent") {
return NextResponse.json({ ok: false, message: "PROJECT_NOT_FOUND" }, { status: 404 });
}
const projectExists = await hasPersistedProject(projectId);
if (!projectExists) {
return NextResponse.json({ ok: false, message: "PROJECT_NOT_FOUND" }, { status: 404 });
}
const session = await requireRequestSession(request);
if (!session) {
return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
}
const controls = await getProjectAgentControls(projectId);
return NextResponse.json({ ok: true, controls });
}
export async function POST(
request: NextRequest,
context: { params: Promise<{ projectId: string }> },
) {
const session = await requireRequestSession(request);
if (!session) {
return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
}
const { projectId } = await context.params;
if (projectId !== "master-agent") {
return NextResponse.json({ ok: false, message: "PROJECT_NOT_FOUND" }, { status: 404 });
}
if (session.role !== "highest_admin") {
return NextResponse.json({ ok: false, message: "FORBIDDEN" }, { status: 403 });
}
const rawBody = await request.text().catch(() => "");
let body: unknown;
try {
body = JSON.parse(rawBody);
} catch {
return NextResponse.json({ ok: false, message: "INVALID_JSON_PAYLOAD" }, { status: 400 });
}
if (!body || typeof body !== "object" || Array.isArray(body)) {
return NextResponse.json({ ok: false, message: "INVALID_AGENT_CONTROLS_PAYLOAD" }, { status: 400 });
}
const payload = body as {
modelOverride?: unknown;
reasoningEffortOverride?: unknown;
};
const hasModelOverride = Object.prototype.hasOwnProperty.call(payload, "modelOverride");
const hasReasoningEffortOverride = Object.prototype.hasOwnProperty.call(
payload,
"reasoningEffortOverride",
);
const allowedKeys = new Set(["modelOverride", "reasoningEffortOverride"]);
const hasUnsupportedKeys = Object.keys(payload).some((key) => !allowedKeys.has(key));
if ((!hasModelOverride && !hasReasoningEffortOverride) || hasUnsupportedKeys) {
return NextResponse.json({ ok: false, message: "INVALID_AGENT_CONTROLS_PAYLOAD" }, { status: 400 });
}
if (hasModelOverride && payload.modelOverride !== undefined && payload.modelOverride !== null && typeof payload.modelOverride !== "string") {
return NextResponse.json({ ok: false, message: "INVALID_MODEL_OVERRIDE" }, { status: 400 });
}
if (hasReasoningEffortOverride && payload.reasoningEffortOverride !== undefined && payload.reasoningEffortOverride !== null && typeof payload.reasoningEffortOverride !== "string") {
return NextResponse.json(
{ ok: false, message: "INVALID_REASONING_EFFORT_OVERRIDE" },
{ status: 400 },
);
}
if (hasReasoningEffortOverride && typeof payload.reasoningEffortOverride === "string" && !reasoningEffortValues.has(payload.reasoningEffortOverride)) {
return NextResponse.json(
{ ok: false, message: "INVALID_REASONING_EFFORT_OVERRIDE" },
{ status: 400 },
);
}
try {
const controls = await updateProjectAgentControls(
projectId,
{
...(hasModelOverride ? { modelOverride: payload.modelOverride } : {}),
...(hasReasoningEffortOverride ? { reasoningEffortOverride: payload.reasoningEffortOverride } : {}),
},
);
return NextResponse.json({ ok: true, controls: controls ?? null });
} catch (error) {
return NextResponse.json(
{ ok: false, message: error instanceof Error ? error.message : "UNKNOWN_ERROR" },
{ status: error instanceof Error && error.message === "PROJECT_NOT_FOUND" ? 404 : 400 },
);
}
}

View File

@@ -19,5 +19,8 @@ export async function GET(
return NextResponse.json({ ok: false, message: "PROJECT_NOT_FOUND" }, { status: 404 });
}
return NextResponse.json({ ok: true, ...detail });
return NextResponse.json({
ok: true,
...detail,
});
}

View File

@@ -143,6 +143,7 @@ export type DispatchPlanStatus =
| "rejected"
| "dispatched";
export type DispatchExecutionStatus = "queued" | "running" | "completed" | "failed";
export type ReasoningEffort = "low" | "medium" | "high";
export interface UserSettings {
liveUpdates: boolean;
@@ -266,6 +267,7 @@ export interface Project {
createdByAgent: boolean;
collaborationMode: "development" | "approval_required";
approvalState: "not_required" | "pending_agent" | "pending_user" | "approved" | "rejected";
agentControls?: ProjectAgentControls;
unreadCount: number;
riskLevel: RiskLevel;
contextBudgetPct?: number;
@@ -314,6 +316,12 @@ export interface DispatchExecution {
completedByDeviceId?: string;
}
export interface ProjectAgentControls {
modelOverride?: string;
reasoningEffortOverride?: ReasoningEffort;
updatedAt: string;
}
export interface DeviceImportCandidate {
candidateId: string;
deviceId: string;
@@ -1613,6 +1621,27 @@ function trimToDefined(value?: string) {
return trimmed ? trimmed : undefined;
}
function parseControlTextOverride(value: unknown) {
if (value === undefined || value === null) {
return { kind: "clear" as const };
}
if (typeof value !== "string") {
return { kind: "invalid" as const };
}
const trimmed = value.trim();
return trimmed ? { kind: "set" as const, value: trimmed } : { kind: "clear" as const };
}
function parseReasoningEffortOverride(value: unknown) {
if (value === undefined || value === null) {
return { kind: "clear" as const };
}
if (!isReasoningEffort(value)) {
return { kind: "invalid" as const };
}
return { kind: "set" as const, value };
}
function normalizeStringSet(values: string[]) {
return dedupeStrings(values.map((value) => value.trim()).filter(Boolean)).sort((a, b) => a.localeCompare(b));
}
@@ -2034,6 +2063,29 @@ function normalizeProjectConversationShape(
return project;
}
function normalizeProjectAgentControls(
raw: Partial<ProjectAgentControls> | undefined,
): ProjectAgentControls | undefined {
const modelOverride = trimToDefined(raw?.modelOverride);
const reasoningEffortOverride = isReasoningEffort(raw?.reasoningEffortOverride)
? raw.reasoningEffortOverride
: undefined;
if (!modelOverride && !reasoningEffortOverride) {
return undefined;
}
return {
modelOverride,
reasoningEffortOverride,
updatedAt: raw?.updatedAt ?? nowIso(),
};
}
function isReasoningEffort(value: unknown): value is ReasoningEffort {
return value === "low" || value === "medium" || value === "high";
}
function resolveProjectUpdatedAt(project: Pick<Project, "updatedAt" | "lastMessageAt" | "threadMeta">, latestActivityAt?: string) {
return latestIsoTimestamp(
project.updatedAt,
@@ -2490,6 +2542,7 @@ function normalizeProject(raw: Partial<Project>, fallback?: Project): Project {
createdByAgent: raw.createdByAgent ?? false,
collaborationMode: raw.collaborationMode ?? "development",
approvalState: raw.approvalState ?? "not_required",
agentControls: normalizeProjectAgentControls(raw.agentControls),
};
project.groupMembers = ensureArray(raw.groupMembers, []).map((member) =>
normalizeGroupMember(member, projectId, project.threadMeta),
@@ -3284,11 +3337,126 @@ async function mutateState<T>(mutator: (state: BossState) => Promise<T> | T) {
return result;
}
async function mutateStateIfChanged<T>(
mutator: (state: BossState) => Promise<{ result: T; changed: boolean }> | { result: T; changed: boolean },
) {
let result!: T;
const run = async () => {
const state = await readState();
const outcome = await mutator(state);
result = outcome.result;
if (outcome.changed) {
await writeState(state);
}
};
stateMutationQueue = stateMutationQueue.then(run, run);
await stateMutationQueue;
return result;
}
async function loadPersistedStateRaw() {
await ensureStateFile();
const parseStateText = (text: string) => JSON.parse(text) as Partial<BossState>;
const tryRead = async (filePath: string) => {
const text = await fs.readFile(filePath, "utf8");
return parseStateText(text);
};
try {
return await tryRead(dataFile);
} catch {
try {
return await tryRead(backupFile);
} catch {
if (lastPersistedStateText) {
return parseStateText(lastPersistedStateText);
}
return JSON.parse(JSON.stringify(syncDerivedState(cloneInitialState()))) as Partial<BossState>;
}
}
}
export async function getProject(projectId: string) {
const state = await readState();
return state.projects.find((project) => project.id === projectId) ?? null;
}
export async function hasPersistedProject(projectId: string) {
const rawState = await loadPersistedStateRaw();
return Array.isArray(rawState.projects) && rawState.projects.some((project) => project?.id === projectId);
}
export async function getProjectAgentControls(projectId: string) {
if (projectId !== "master-agent") {
return null;
}
const state = await readState();
return state.projects.find((project) => project.id === projectId)?.agentControls ?? null;
}
export async function updateProjectAgentControls(
projectId: string,
payload: {
modelOverride?: unknown;
reasoningEffortOverride?: unknown;
},
) {
if (projectId !== "master-agent") {
throw new Error("MASTER_AGENT_CONTROLS_SCOPE_RESTRICTED");
}
const modelOverrideInput = Object.prototype.hasOwnProperty.call(payload, "modelOverride")
? parseControlTextOverride(payload.modelOverride)
: { kind: "preserve" as const };
const reasoningEffortInput = Object.prototype.hasOwnProperty.call(payload, "reasoningEffortOverride")
? parseReasoningEffortOverride(payload.reasoningEffortOverride)
: { kind: "preserve" as const };
if (modelOverrideInput.kind === "invalid") {
throw new Error("INVALID_MODEL_OVERRIDE");
}
if (reasoningEffortInput.kind === "invalid") {
throw new Error("INVALID_REASONING_EFFORT_OVERRIDE");
}
return mutateStateIfChanged((state) => {
const project = state.projects.find((item) => item.id === projectId);
if (!project) throw new Error("PROJECT_NOT_FOUND");
const currentControls = project.agentControls;
const modelOverride =
modelOverrideInput.kind === "set"
? modelOverrideInput.value
: modelOverrideInput.kind === "clear"
? undefined
: currentControls?.modelOverride;
const reasoningEffortOverride =
reasoningEffortInput.kind === "set"
? reasoningEffortInput.value
: reasoningEffortInput.kind === "clear"
? undefined
: currentControls?.reasoningEffortOverride;
const currentModelOverride = currentControls?.modelOverride;
const currentReasoningEffortOverride = currentControls?.reasoningEffortOverride;
if (currentModelOverride === modelOverride && currentReasoningEffortOverride === reasoningEffortOverride) {
return { result: currentControls, changed: false };
}
const nextControls = {
modelOverride,
reasoningEffortOverride,
updatedAt: nowIso(),
} satisfies ProjectAgentControls;
project.agentControls = normalizeProjectAgentControls(nextControls);
project.threadMeta.updatedAt = nextControls.updatedAt;
project.updatedAt = nextControls.updatedAt;
return { result: project.agentControls, changed: true };
});
}
export async function getDevice(deviceId: string) {
const state = await readState();
return state.devices.find((device) => device.id === deviceId) ?? null;

View File

@@ -18,6 +18,7 @@ import type {
OpsRepairTicket,
OpsRepairVerification,
Project,
ProjectAgentControls,
RiskLevel,
ThreadContextAlert,
ThreadContextSnapshot,
@@ -75,6 +76,7 @@ export interface ThreadContextView {
export interface ProjectDetailView {
project: Project;
agentControls?: ProjectAgentControls | null;
devices: Device[];
masterIdentity?: MasterIdentitySummary;
activeThreadContexts: ThreadContextView[];
@@ -542,6 +544,7 @@ export function getProjectDetailView(state: BossState, projectId: string): Proje
return {
project,
agentControls: project.id === "master-agent" ? project.agentControls ?? null : undefined,
devices: state.devices.filter((device) => project.deviceIds.includes(device.id)),
masterIdentity: projectId === "master-agent" ? getProjectMasterIdentity(state) : undefined,
activeThreadContexts,

View File

@@ -0,0 +1,827 @@
import test from "node:test";
import assert from "node:assert/strict";
import os from "node:os";
import path from "node:path";
import { mkdtemp, rm } from "node:fs/promises";
import { execFile as execFileCallback } from "node:child_process";
import { promisify } from "node:util";
import { NextRequest } from "next/server";
let runtimeRoot = "";
let readState: (typeof import("../src/lib/boss-data"))["readState"];
let writeState: (typeof import("../src/lib/boss-data"))["writeState"];
let updateProjectAgentControls: (typeof import("../src/lib/boss-data"))["updateProjectAgentControls"];
let getProjectAgentControls: (typeof import("../src/lib/boss-data"))["getProjectAgentControls"];
let getProjectDetailView: (typeof import("../src/lib/boss-projections"))["getProjectDetailView"];
let getProjectRoute: (typeof import("../src/app/api/v1/projects/[projectId]/route"))["GET"];
let getAgentControlsRoute: (typeof import("../src/app/api/v1/projects/[projectId]/agent-controls/route"))["GET"];
let postAgentControlsRoute: (typeof import("../src/app/api/v1/projects/[projectId]/agent-controls/route"))["POST"];
let createAuthSession: (typeof import("../src/lib/boss-data"))["createAuthSession"];
let AUTH_SESSION_COOKIE = "";
const execFile = promisify(execFileCallback);
async function setup() {
if (runtimeRoot) return;
runtimeRoot = await mkdtemp(path.join(os.tmpdir(), "boss-master-agent-controls-"));
process.env.BOSS_RUNTIME_ROOT = runtimeRoot;
process.env.BOSS_STATE_FILE = path.join(runtimeRoot, "boss-state.json");
const [data, projections, projectRouteModule, agentControlsRouteModule, auth] = await Promise.all([
import("../src/lib/boss-data.ts"),
import("../src/lib/boss-projections.ts"),
import("../src/app/api/v1/projects/[projectId]/route.ts"),
import("../src/app/api/v1/projects/[projectId]/agent-controls/route.ts"),
import("../src/lib/boss-auth.ts"),
]);
readState = data.readState;
writeState = data.writeState;
updateProjectAgentControls = data.updateProjectAgentControls;
getProjectAgentControls = data.getProjectAgentControls;
getProjectDetailView = projections.getProjectDetailView;
getProjectRoute = projectRouteModule.GET;
getAgentControlsRoute = agentControlsRouteModule.GET;
postAgentControlsRoute = agentControlsRouteModule.POST;
createAuthSession = data.createAuthSession;
AUTH_SESSION_COOKIE = auth.AUTH_SESSION_COOKIE;
}
async function resetMasterAgentControls() {
await setup();
const state = await readState();
const project = state.projects.find((item) => item.id === "master-agent");
assert.ok(project, "expected seeded master-agent project");
delete project.agentControls;
await writeState(state);
}
test.beforeEach(async () => {
await resetMasterAgentControls();
});
test.after(async () => {
if (runtimeRoot) {
await rm(runtimeRoot, { recursive: true, force: true });
}
});
test("master-agent 会话可保存并读取模型与推理强度覆盖", async () => {
await setup();
await updateProjectAgentControls("master-agent", {
modelOverride: "gpt-5.4",
reasoningEffortOverride: "high",
});
const controls = await getProjectAgentControls("master-agent");
assert.equal(controls?.modelOverride, "gpt-5.4");
assert.equal(controls?.reasoningEffortOverride, "high");
const state = await readState();
const project = state.projects.find((item) => item.id === "master-agent");
assert.equal(project?.agentControls?.modelOverride, "gpt-5.4");
assert.equal(project?.agentControls?.reasoningEffortOverride, "high");
const detail = getProjectDetailView(state, "master-agent");
assert.equal(detail?.agentControls?.modelOverride, "gpt-5.4");
assert.equal(detail?.agentControls?.reasoningEffortOverride, "high");
});
test("master-agent 对话控制路由可读写并回显到项目详情", async () => {
await setup();
const session = await createAuthSession({
account: "17600003315",
role: "highest_admin",
displayName: "Boss 超级管理员",
loginMethod: "password",
});
const headers = {
"content-type": "application/json",
cookie: `${AUTH_SESSION_COOKIE}=${session.sessionToken}`,
};
const postResponse = await postAgentControlsRoute(
new NextRequest("http://127.0.0.1:3000/api/v1/projects/master-agent/agent-controls", {
method: "POST",
headers,
body: JSON.stringify({
modelOverride: "gpt-5.4",
reasoningEffortOverride: "medium",
}),
}),
{ params: Promise.resolve({ projectId: "master-agent" }) },
);
assert.equal(postResponse.status, 200);
const postPayload = (await postResponse.json()) as {
ok: boolean;
controls: {
modelOverride?: string;
reasoningEffortOverride?: string;
updatedAt: string;
} | null;
};
assert.equal(postPayload.ok, true);
assert.equal(postPayload.controls?.modelOverride, "gpt-5.4");
assert.equal(postPayload.controls?.reasoningEffortOverride, "medium");
const getResponse = await getAgentControlsRoute(
new NextRequest("http://127.0.0.1:3000/api/v1/projects/master-agent/agent-controls", {
method: "GET",
headers,
}),
{ params: Promise.resolve({ projectId: "master-agent" }) },
);
assert.equal(getResponse.status, 200);
const getPayload = (await getResponse.json()) as {
ok: boolean;
controls: {
modelOverride?: string;
reasoningEffortOverride?: string;
updatedAt: string;
} | null;
};
assert.equal(getPayload.ok, true);
assert.equal(getPayload.controls?.modelOverride, "gpt-5.4");
assert.equal(getPayload.controls?.reasoningEffortOverride, "medium");
const projectResponse = await getProjectRoute(
new NextRequest("http://127.0.0.1:3000/api/v1/projects/master-agent", {
method: "GET",
headers,
}),
{ params: Promise.resolve({ projectId: "master-agent" }) },
);
assert.equal(projectResponse.status, 200);
const projectPayload = (await projectResponse.json()) as {
ok: boolean;
agentControls: {
modelOverride?: string;
reasoningEffortOverride?: string;
updatedAt: string;
} | null;
};
assert.equal(projectPayload.ok, true);
assert.equal(projectPayload.agentControls?.modelOverride, "gpt-5.4");
assert.equal(projectPayload.agentControls?.reasoningEffortOverride, "medium");
});
test("master-agent 对话控制路由单字段更新不会清掉另一字段", async () => {
await setup();
const session = await createAuthSession({
account: "17600003315",
role: "highest_admin",
displayName: "Boss 超级管理员",
loginMethod: "password",
});
const headers = {
"content-type": "application/json",
cookie: `${AUTH_SESSION_COOKIE}=${session.sessionToken}`,
};
await postAgentControlsRoute(
new NextRequest("http://127.0.0.1:3000/api/v1/projects/master-agent/agent-controls", {
method: "POST",
headers,
body: JSON.stringify({
modelOverride: "gpt-5.4",
reasoningEffortOverride: "high",
}),
}),
{ params: Promise.resolve({ projectId: "master-agent" }) },
);
const response = await postAgentControlsRoute(
new NextRequest("http://127.0.0.1:3000/api/v1/projects/master-agent/agent-controls", {
method: "POST",
headers,
body: JSON.stringify({
reasoningEffortOverride: "low",
}),
}),
{ params: Promise.resolve({ projectId: "master-agent" }) },
);
assert.equal(response.status, 200);
const payload = (await response.json()) as {
ok: boolean;
controls: {
modelOverride?: string;
reasoningEffortOverride?: string;
} | null;
};
assert.equal(payload.ok, true);
assert.equal(payload.controls?.modelOverride, "gpt-5.4");
assert.equal(payload.controls?.reasoningEffortOverride, "low");
});
test("master-agent 对话控制 POST 清空后仍稳定回传 controls null", async () => {
await setup();
const session = await createAuthSession({
account: "17600003315",
role: "highest_admin",
displayName: "Boss 超级管理员",
loginMethod: "password",
});
const headers = {
"content-type": "application/json",
cookie: `${AUTH_SESSION_COOKIE}=${session.sessionToken}`,
};
await postAgentControlsRoute(
new NextRequest("http://127.0.0.1:3000/api/v1/projects/master-agent/agent-controls", {
method: "POST",
headers,
body: JSON.stringify({
modelOverride: "gpt-5.4",
reasoningEffortOverride: "high",
}),
}),
{ params: Promise.resolve({ projectId: "master-agent" }) },
);
const clearResponse = await postAgentControlsRoute(
new NextRequest("http://127.0.0.1:3000/api/v1/projects/master-agent/agent-controls", {
method: "POST",
headers,
body: JSON.stringify({
modelOverride: null,
reasoningEffortOverride: null,
}),
}),
{ params: Promise.resolve({ projectId: "master-agent" }) },
);
assert.equal(clearResponse.status, 200);
const clearPayload = (await clearResponse.json()) as {
ok: boolean;
controls: unknown;
};
assert.equal(clearPayload.ok, true);
assert.equal(Object.prototype.hasOwnProperty.call(clearPayload, "controls"), true);
assert.equal(clearPayload.controls, null);
});
test("非 master-agent 项目详情不应回传 agentControls 字段", async () => {
await setup();
const session = await createAuthSession({
account: "17600003315",
role: "highest_admin",
displayName: "Boss 超级管理员",
loginMethod: "password",
});
const response = await getProjectRoute(
new NextRequest("http://127.0.0.1:3000/api/v1/projects/boss-console", {
method: "GET",
headers: {
cookie: `${AUTH_SESSION_COOKIE}=${session.sessionToken}`,
},
}),
{ params: Promise.resolve({ projectId: "boss-console" }) },
);
assert.equal(response.status, 200);
const payload = (await response.json()) as Record<string, unknown>;
assert.equal(payload.ok, true);
assert.equal(Object.prototype.hasOwnProperty.call(payload, "agentControls"), false);
});
test("master-agent 对话控制 POST 仅允许 highest_admin 修改", async () => {
await setup();
const session = await createAuthSession({
account: "viewer-0001",
role: "member",
displayName: "普通成员",
loginMethod: "password",
});
const response = await postAgentControlsRoute(
new NextRequest("http://127.0.0.1:3000/api/v1/projects/master-agent/agent-controls", {
method: "POST",
headers: {
"content-type": "application/json",
cookie: `${AUTH_SESSION_COOKIE}=${session.sessionToken}`,
},
body: JSON.stringify({
modelOverride: "gpt-5.4",
reasoningEffortOverride: "low",
}),
}),
{ params: Promise.resolve({ projectId: "master-agent" }) },
);
assert.equal(response.status, 403);
const payload = (await response.json()) as { ok: boolean; message: string };
assert.equal(payload.ok, false);
assert.equal(payload.message, "FORBIDDEN");
const controls = await getProjectAgentControls("master-agent");
assert.equal(controls, null);
});
test("master-agent 对话控制 POST 会稳定拒绝非法 modelOverride", async () => {
await setup();
const session = await createAuthSession({
account: "17600003315",
role: "highest_admin",
displayName: "Boss 超级管理员",
loginMethod: "password",
});
const response = await postAgentControlsRoute(
new NextRequest("http://127.0.0.1:3000/api/v1/projects/master-agent/agent-controls", {
method: "POST",
headers: {
"content-type": "application/json",
cookie: `${AUTH_SESSION_COOKIE}=${session.sessionToken}`,
},
body: JSON.stringify({
modelOverride: 123,
}),
}),
{ params: Promise.resolve({ projectId: "master-agent" }) },
);
assert.equal(response.status, 400);
const payload = (await response.json()) as { ok: boolean; message: string };
assert.equal(payload.ok, false);
assert.equal(payload.message, "INVALID_MODEL_OVERRIDE");
const controls = await getProjectAgentControls("master-agent");
assert.equal(controls, null);
});
test("master-agent 对话控制 POST 会稳定拒绝 malformed JSON 和空对象", async () => {
await setup();
const session = await createAuthSession({
account: "17600003315",
role: "highest_admin",
displayName: "Boss 超级管理员",
loginMethod: "password",
});
const headers = {
"content-type": "application/json",
cookie: `${AUTH_SESSION_COOKIE}=${session.sessionToken}`,
};
const beforeState = await readState();
const beforeProject = beforeState.projects.find((item) => item.id === "master-agent");
assert.ok(beforeProject, "expected seeded master-agent project");
const beforeUpdatedAt = beforeProject.updatedAt;
const beforeThreadMetaUpdatedAt = beforeProject.threadMeta.updatedAt;
const malformedResponse = await postAgentControlsRoute(
new NextRequest("http://127.0.0.1:3000/api/v1/projects/master-agent/agent-controls", {
method: "POST",
headers,
body: "{",
}),
{ params: Promise.resolve({ projectId: "master-agent" }) },
);
assert.equal(malformedResponse.status, 400);
assert.equal((await malformedResponse.json()).message, "INVALID_JSON_PAYLOAD");
const emptyObjectResponse = await postAgentControlsRoute(
new NextRequest("http://127.0.0.1:3000/api/v1/projects/master-agent/agent-controls", {
method: "POST",
headers,
body: JSON.stringify({}),
}),
{ params: Promise.resolve({ projectId: "master-agent" }) },
);
assert.equal(emptyObjectResponse.status, 400);
assert.equal((await emptyObjectResponse.json()).message, "INVALID_AGENT_CONTROLS_PAYLOAD");
const nullResponse = await postAgentControlsRoute(
new NextRequest("http://127.0.0.1:3000/api/v1/projects/master-agent/agent-controls", {
method: "POST",
headers,
body: "null",
}),
{ params: Promise.resolve({ projectId: "master-agent" }) },
);
assert.equal(nullResponse.status, 400);
assert.equal((await nullResponse.json()).message, "INVALID_AGENT_CONTROLS_PAYLOAD");
const arrayResponse = await postAgentControlsRoute(
new NextRequest("http://127.0.0.1:3000/api/v1/projects/master-agent/agent-controls", {
method: "POST",
headers,
body: "[]",
}),
{ params: Promise.resolve({ projectId: "master-agent" }) },
);
assert.equal(arrayResponse.status, 400);
assert.equal((await arrayResponse.json()).message, "INVALID_AGENT_CONTROLS_PAYLOAD");
const primitiveResponse = await postAgentControlsRoute(
new NextRequest("http://127.0.0.1:3000/api/v1/projects/master-agent/agent-controls", {
method: "POST",
headers,
body: "1",
}),
{ params: Promise.resolve({ projectId: "master-agent" }) },
);
assert.equal(primitiveResponse.status, 400);
assert.equal((await primitiveResponse.json()).message, "INVALID_AGENT_CONTROLS_PAYLOAD");
const controls = await getProjectAgentControls("master-agent");
assert.equal(controls, null);
const state = await readState();
const project = state.projects.find((item) => item.id === "master-agent");
assert.equal(project?.agentControls, undefined);
assert.equal(project?.updatedAt, beforeUpdatedAt);
assert.equal(project?.threadMeta.updatedAt, beforeThreadMetaUpdatedAt);
});
test("master-agent 对话控制 helper 会安全忽略非法 modelOverride 输入", async () => {
await setup();
await updateProjectAgentControls("master-agent", {
modelOverride: "gpt-5.4",
reasoningEffortOverride: "high",
});
const beforeState = await readState();
const beforeProject = beforeState.projects.find((item) => item.id === "master-agent");
assert.ok(beforeProject, "expected seeded master-agent project");
const beforeUpdatedAt = beforeProject.updatedAt;
const beforeThreadMetaUpdatedAt = beforeProject.threadMeta.updatedAt;
await assert.rejects(
() =>
updateProjectAgentControls("master-agent", {
modelOverride: { bad: true } as never,
reasoningEffortOverride: "medium",
}),
/INVALID_MODEL_OVERRIDE/,
);
const controls = await getProjectAgentControls("master-agent");
assert.equal(controls?.modelOverride, "gpt-5.4");
assert.equal(controls?.reasoningEffortOverride, "high");
const afterState = await readState();
const afterProject = afterState.projects.find((item) => item.id === "master-agent");
assert.equal(afterProject?.updatedAt, beforeUpdatedAt);
assert.equal(afterProject?.threadMeta.updatedAt, beforeThreadMetaUpdatedAt);
});
test("master-agent 对话控制 helper 重复提交或重复清空不应刷新更新时间", async () => {
await setup();
await updateProjectAgentControls("master-agent", {
modelOverride: "gpt-5.4",
reasoningEffortOverride: "high",
});
const beforeRepeatState = await readState();
const beforeRepeatProject = beforeRepeatState.projects.find((item) => item.id === "master-agent");
assert.ok(beforeRepeatProject, "expected seeded master-agent project");
const beforeRepeatUpdatedAt = beforeRepeatProject.updatedAt;
const beforeRepeatThreadMetaUpdatedAt = beforeRepeatProject.threadMeta.updatedAt;
await updateProjectAgentControls("master-agent", {
modelOverride: "gpt-5.4",
reasoningEffortOverride: "high",
});
const afterRepeatState = await readState();
const afterRepeatProject = afterRepeatState.projects.find((item) => item.id === "master-agent");
assert.equal(afterRepeatProject?.updatedAt, beforeRepeatUpdatedAt);
assert.equal(afterRepeatProject?.threadMeta.updatedAt, beforeRepeatThreadMetaUpdatedAt);
await updateProjectAgentControls("master-agent", {
modelOverride: undefined,
reasoningEffortOverride: undefined,
});
const afterClearState = await readState();
const afterClearProject = afterClearState.projects.find((item) => item.id === "master-agent");
const clearedUpdatedAt = afterClearProject?.updatedAt;
const clearedThreadMetaUpdatedAt = afterClearProject?.threadMeta.updatedAt;
await updateProjectAgentControls("master-agent", {
modelOverride: undefined,
reasoningEffortOverride: undefined,
});
const afterRepeatClearState = await readState();
const afterRepeatClearProject = afterRepeatClearState.projects.find((item) => item.id === "master-agent");
assert.equal(afterRepeatClearProject?.updatedAt, clearedUpdatedAt);
assert.equal(afterRepeatClearProject?.threadMeta.updatedAt, clearedThreadMetaUpdatedAt);
assert.equal(await getProjectAgentControls("master-agent"), null);
});
test("master-agent 对话控制 helper 单字段更新不会清掉另一字段", async () => {
await setup();
await updateProjectAgentControls("master-agent", {
modelOverride: "gpt-5.4",
reasoningEffortOverride: "high",
});
await updateProjectAgentControls("master-agent", {
reasoningEffortOverride: "low",
});
const controls = await getProjectAgentControls("master-agent");
assert.equal(controls?.modelOverride, "gpt-5.4");
assert.equal(controls?.reasoningEffortOverride, "low");
});
test("master-agent 对话控制可清空为未覆盖状态", async () => {
await setup();
await updateProjectAgentControls("master-agent", {
modelOverride: "gpt-5.4",
reasoningEffortOverride: "high",
});
await updateProjectAgentControls("master-agent", {
modelOverride: undefined,
reasoningEffortOverride: undefined,
});
const controls = await getProjectAgentControls("master-agent");
assert.equal(controls, null);
const state = await readState();
const project = state.projects.find((item) => item.id === "master-agent");
assert.equal(project?.agentControls, undefined);
const detail = getProjectDetailView(state, "master-agent");
assert.equal(detail?.agentControls, null);
});
test("GET /agent-controls returns 404 for missing project", async () => {
await setup();
const session = await createAuthSession({
account: "17600003315",
role: "highest_admin",
displayName: "Boss 超级管理员",
loginMethod: "password",
});
const response = await getAgentControlsRoute(
new NextRequest("http://127.0.0.1:3000/api/v1/projects/missing-project/agent-controls", {
method: "GET",
headers: {
cookie: `${AUTH_SESSION_COOKIE}=${session.sessionToken}`,
},
}),
{ params: Promise.resolve({ projectId: "missing-project" }) },
);
assert.equal(response.status, 404);
assert.equal((await response.json()).message, "PROJECT_NOT_FOUND");
});
test(
"GET /agent-controls returns 404 when master-agent record is missing from state",
{ concurrency: false },
async () => {
await setup();
const script = String.raw`
import { mkdtemp, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { NextRequest } from "next/server";
(async () => {
const runtimeRoot = await mkdtemp(path.join(os.tmpdir(), "boss-master-agent-missing-"));
process.env.BOSS_RUNTIME_ROOT = runtimeRoot;
process.env.BOSS_STATE_FILE = path.join(runtimeRoot, "boss-state.json");
const dataModule = await import("./src/lib/boss-data.ts");
const authModule = await import("./src/lib/boss-auth.ts");
const routeModule = await import("./src/app/api/v1/projects/[projectId]/agent-controls/route.ts");
const data = dataModule.default ?? dataModule;
const auth = authModule.default ?? authModule;
const route = routeModule.default ?? routeModule;
const session = await data.createAuthSession({
account: "17600003315",
role: "highest_admin",
displayName: "Boss 超级管理员",
loginMethod: "password",
});
const state = await data.readState();
state.projects = state.projects.filter((item) => item.id !== "master-agent");
await writeFile(process.env.BOSS_STATE_FILE!, JSON.stringify(state, null, 2), "utf8");
if (await data.hasPersistedProject("master-agent")) {
throw new Error("expected raw state to miss master-agent");
}
const response = await route.GET(
new NextRequest("http://127.0.0.1:3000/api/v1/projects/master-agent/agent-controls", {
method: "GET",
headers: {
cookie: auth.AUTH_SESSION_COOKIE + "=" + session.sessionToken,
},
}),
{ params: Promise.resolve({ projectId: "master-agent" }) },
);
const text = await response.text();
if (response.status !== 404) {
throw new Error("expected 404, got " + response.status + ": " + text);
}
if (!text.includes("PROJECT_NOT_FOUND")) {
throw new Error("expected PROJECT_NOT_FOUND, got " + text);
}
console.log(text);
})().catch((error) => {
console.error(error);
process.exit(1);
});
`;
const tsxBin = path.resolve("node_modules/.bin/tsx");
const { stdout } = await execFile(tsxBin, ["--eval", script], {
cwd: process.cwd(),
env: { ...process.env },
encoding: "utf8",
maxBuffer: 1024 * 1024,
});
assert.match(stdout, /PROJECT_NOT_FOUND/);
},
);
test("GET /agent-controls 在未显式设置 BOSS_STATE_FILE 时仍可正常读取 controls", async () => {
const runtimeRoot = await mkdtemp(path.join(os.tmpdir(), "boss-master-agent-default-state-"));
const childEnv = { ...process.env };
delete childEnv.BOSS_STATE_FILE;
const script = String.raw`
import { NextRequest } from "next/server";
(async () => {
const runtimeRoot = process.env.BOSS_RUNTIME_ROOT;
if (!runtimeRoot) {
throw new Error("missing BOSS_RUNTIME_ROOT");
}
delete process.env.BOSS_STATE_FILE;
const dataModule = await import("./src/lib/boss-data.ts");
const authModule = await import("./src/lib/boss-auth.ts");
const routeModule = await import("./src/app/api/v1/projects/[projectId]/agent-controls/route.ts");
const data = dataModule.default ?? dataModule;
const auth = authModule.default ?? authModule;
const route = routeModule.default ?? routeModule;
await data.updateProjectAgentControls("master-agent", {
modelOverride: "gpt-5.4",
reasoningEffortOverride: "medium",
});
const session = await data.createAuthSession({
account: "17600003315",
role: "highest_admin",
displayName: "Boss 超级管理员",
loginMethod: "password",
});
const response = await route.GET(
new NextRequest("http://127.0.0.1:3000/api/v1/projects/master-agent/agent-controls", {
method: "GET",
headers: {
cookie: auth.AUTH_SESSION_COOKIE + "=" + session.sessionToken,
},
}),
{ params: Promise.resolve({ projectId: "master-agent" }) },
);
const payload = await response.json();
if (response.status !== 200) {
throw new Error("expected 200, got " + response.status + ": " + JSON.stringify(payload));
}
if (!payload.ok || payload.controls?.modelOverride !== "gpt-5.4" || payload.controls?.reasoningEffortOverride !== "medium") {
throw new Error("unexpected payload: " + JSON.stringify(payload));
}
console.log("OK");
})().catch((error) => {
console.error(error);
process.exit(1);
});
`;
const tsxBin = path.resolve("node_modules/.bin/tsx");
const { stdout } = await execFile(tsxBin, ["--eval", script], {
cwd: process.cwd(),
env: { ...childEnv, BOSS_RUNTIME_ROOT: runtimeRoot },
encoding: "utf8",
maxBuffer: 1024 * 1024,
});
assert.match(stdout, /OK/);
});
test("GET /agent-controls rejects ordinary projects", async () => {
await setup();
const session = await createAuthSession({
account: "17600003315",
role: "highest_admin",
displayName: "Boss 超级管理员",
loginMethod: "password",
});
const response = await getAgentControlsRoute(
new NextRequest("http://127.0.0.1:3000/api/v1/projects/boss-console/agent-controls", {
method: "GET",
headers: {
cookie: `${AUTH_SESSION_COOKIE}=${session.sessionToken}`,
},
}),
{ params: Promise.resolve({ projectId: "boss-console" }) },
);
assert.equal(response.status, 404);
assert.equal((await response.json()).message, "PROJECT_NOT_FOUND");
});
test("POST /agent-controls rejects unknown-key payload and preserves controls", async () => {
await setup();
const session = await createAuthSession({
account: "17600003315",
role: "highest_admin",
displayName: "Boss 超级管理员",
loginMethod: "password",
});
await updateProjectAgentControls("master-agent", {
modelOverride: "gpt-5.4",
reasoningEffortOverride: "high",
});
const beforeState = await readState();
const beforeProject = beforeState.projects.find((item) => item.id === "master-agent");
assert.ok(beforeProject, "expected seeded master-agent project");
const beforeUpdatedAt = beforeProject.updatedAt;
const response = await postAgentControlsRoute(
new NextRequest("http://127.0.0.1:3000/api/v1/projects/master-agent/agent-controls", {
method: "POST",
headers: {
"content-type": "application/json",
cookie: `${AUTH_SESSION_COOKIE}=${session.sessionToken}`,
},
body: JSON.stringify({
unexpectedKey: "value",
}),
}),
{ params: Promise.resolve({ projectId: "master-agent" }) },
);
assert.equal(response.status, 400);
assert.equal((await response.json()).message, "INVALID_AGENT_CONTROLS_PAYLOAD");
const controls = await getProjectAgentControls("master-agent");
assert.equal(controls?.modelOverride, "gpt-5.4");
assert.equal(controls?.reasoningEffortOverride, "high");
const afterState = await readState();
const afterProject = afterState.projects.find((item) => item.id === "master-agent");
assert.equal(afterProject?.updatedAt, beforeUpdatedAt);
});
test("master-agent controls helper 不会写入普通项目", async () => {
await setup();
await assert.rejects(
() =>
updateProjectAgentControls("boss-console", {
modelOverride: "gpt-5.4",
reasoningEffortOverride: "low",
}),
/MASTER_AGENT_CONTROLS_SCOPE_RESTRICTED/,
);
const state = await readState();
const project = state.projects.find((item) => item.id === "boss-console");
assert.equal(project?.agentControls, undefined);
assert.equal(await getProjectAgentControls("boss-console"), null);
});