feat: add master agent chat controls
This commit is contained in:
520
package-lock.json
generated
520
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
108
src/app/api/v1/projects/[projectId]/agent-controls/route.ts
Normal file
108
src/app/api/v1/projects/[projectId]/agent-controls/route.ts
Normal 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 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
827
tests/master-agent-chat-controls.test.ts
Normal file
827
tests/master-agent-chat-controls.test.ts
Normal 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);
|
||||
});
|
||||
Reference in New Issue
Block a user