Przeglądaj źródła

Update core structure

Toby Chui 1 tydzień temu
rodzic
commit
55557728c6
5 zmienionych plików z 81 dodań i 993 usunięć
  1. 2 986
      src/agi-doc.md
  2. 2 2
      src/flags.go
  3. 21 2
      src/main.go
  4. 12 3
      src/main.router.go
  5. 44 0
      src/startup.go

+ 2 - 986
src/agi-doc.md

@@ -1,986 +1,2 @@
-# AJGI Documentation
-
-## What is AJGI?
-
-AJGI is the shortform of ArOZ Javascript Gateway Interface.
-In simple words, you can add function to your system with JavaScript :)
-
-## Usages
-
-1. Put your js / agi file inside web/* (e.g. ./web/Dummy/backend/test.js)
-2. Load your script by calling / ajax request to ```/system/ajgi/interface?script={yourfile}.js```, (e.g. /system/ajgi/interface?script=Dummy/backend/test.js)
-3. Wait for the reponse from the script by calling sendResp in the script
-
-## Module Init Script
-
-To initialize a module without a main.go function call, you can create a "init.agi" script in your module root under ./web/myModule where "myModule" is your module name.
-
-To register the module, you can call to the "registerModule" function with JSON stringify module launch info following the following example JavaScript Object.
-
-```
-//Define the launchInfo for the module
-var moduleLaunchInfo = {
-    Name: "NotepadA",
-    Desc: "The best code editor on ArOZ Online",
-    Group: "Office",
-    IconPath: "NotepadA/img/module_icon.png",
-    Version: "1.2",
-    StartDir: "NotepadA/index.html",
-    SupportFW: true,
-    LaunchFWDir: "NotepadA/index.html",
-    SupportEmb: true,
-    LaunchEmb: "NotepadA/embedded.html",
-    InitFWSize: [1024, 768],
-    InitEmbSize: [360, 200],
-    SupportedExt: [".bat",".coffee",".cpp",".cs",".csp",".csv",".fs",".dockerfile",".go",".html",".ini",".java",".js",".lua",".mips",".md", ".sql",".txt",".php",".py",".ts",".xml",".yaml"]
-}
-
-//Register the module
-registerModule(JSON.stringify(moduleLaunchInfo));
-```
-
-You might also create the database table in this section of the code. For example:
-
-```
-//Create database for this module
-newDBTableIfNotExists("myModule")
-```
-
-## Application Examples
-
-See web/UnitTest/backend/*.js for more information on how to use AGI in webapps.
-
-For subservice, see subservice/demo/agi/ for more examples.
-
-### Access From Frontend
-
-To access server functions from front-end (e.g. You are building a serverless webapp on top of arozos), you can call to the ao_module.js function for running an agi script located under ```./web``` directory. You can find the ao_module.js wrapper under ```./web/script/```
-
-Here is an example extracted from Music module for listing files nearby the openeing music file.
-
-./web/Music/embedded.html
-
-```
-ao_module_agirun("Music/functions/getMeta.js", {
-    file: encodeURIComponent(playingFileInfo.filepath)
-}, function(data){
-    songList = data;
-    for (var i = 0; i < data.length; i++){
-        //Do something here
-    }
-});
-```
-
-./web/Music/functions/getMeta.js
-
-```
-//Define helper functions
-function bytesToSize(bytes) {
-    var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
-    if (bytes == 0) return '0 Byte';
-    var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)));
-    return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + sizes[i];
- }
-
-//Main Logic
-if (requirelib("filelib") == true){
-    //Get the filename from paramters
-    var openingFilePath = decodeURIComponent(file);
-    var dirname = openingFilePath.split("/")
-    dirname.pop()
-    dirname = dirname.join("/");
-
-    //Scan nearby files
-    var nearbyFiles = filelib.aglob(dirname + "/*") //aglob must be used here to prevent errors for non-unicode filename
-    var audioFiles = [];
-    var supportedFormats = [".mp3",".flac",".wav",".ogg",".aac",".webm",".mp4"];
-    //For each nearby files
-    for (var i =0; i < nearbyFiles.length; i++){
-        var thisFile = nearbyFiles[i];
-        var ext = thisFile.split(".").pop();
-        ext = "." + ext;
-        //Check if the file extension is in the supported extension list
-        for (var k = 0; k < supportedFormats.length; k++){
-            if (filelib.isDir(nearbyFiles[i]) == false && supportedFormats[k] == ext){
-                var fileExt = ext.substr(1);
-                var fileName = thisFile.split("/").pop();
-                var fileSize = filelib.filesize(thisFile);
-                var humanReadableFileSize = bytesToSize(fileSize);
-
-                var thisFileInfo = [];
-                thisFileInfo.push(fileName);
-                thisFileInfo.push(thisFile);
-                thisFileInfo.push(fileExt);
-                thisFileInfo.push(humanReadableFileSize);
-
-                audioFiles.push(thisFileInfo);
-                break;
-            }
-        }
-    }
-    sendJSONResp(JSON.stringify(audioFiles));
-}
-```
-
-### Access from Subservice Backend
-
-It is also possible to access the AGI gateway from subservice backend.
-You can include aroz library from ```./subservice/demo/aroz``` . The following is an example extracted from demo subservice that request access to your desktop filelist.
-
-```
-package main
-import (
-    aroz "your/package/name/aroz"
-)
-
-var handler *aroz.ArozHandler
-
-//...
-
-func main(){
-    //Put other flags here
-
-    //Start subservice pipeline and flag parsing (This function call will also do flag.parse())
-    handler = aroz.HandleFlagParse(aroz.ServiceInfo{
-        Name: "Demo Subservice",
-        Desc: "A simple subservice code for showing how subservice works in ArOZ Online",            
-        Group: "Development",
-        IconPath: "demo/icon.png",
-        Version: "0.0.1",
-        //You can define any path before the actualy html file. This directory (in this case demo/ ) will be the reverse proxy endpoint for this module
-        StartDir: "demo/home.html",            
-        SupportFW: true, 
-        LaunchFWDir: "demo/home.html",
-        SupportEmb: true,
-        LaunchEmb: "demo/embedded.html",
-        InitFWSize: []int{720, 480},
-        InitEmbSize: []int{720, 480},
-        SupportedExt: []string{".txt",".md"},
-    });
-
-    //Start Web server with handler.Port
-    http.ListenAndServe(handler.Port, nil)
-}
-
-
-//Access AGI Gateway from Golang
-func agiGatewayTest(w http.ResponseWriter, r *http.Request){
-    //Get username and token from request
-    username, token := handler.GetUserInfoFromRequest(w,r)
-    log.Println("Received request from: ", username, " with token: ", token)
-
-    //Create an AGI Call that get the user desktop files
-    script := `
-        if (requirelib("filelib")){
-            var filelist = filelib.glob("user:/Desktop/*")
-            sendJSONResp(JSON.stringify(filelist));
-        }else{
-            sendJSONResp(JSON.stringify({
-                error: "Filelib require failed"
-            }));
-        }
-    `
-
-    //Execute the AGI request on server side
-    resp,err := handler.RequestGatewayInterface(token, script)
-    if err != nil{
-        //Something went wrong when performing POST request
-        log.Println(err)
-    }else{
-        //Try to read the resp body
-        bodyBytes, err := os.ReadAll(resp.Body)
-        if err != nil{
-            log.Println(err)
-            w.Write([]byte(err.Error()))
-            return
-        }
-        resp.Body.Close()
-
-        //Relay the information to the request using json header
-        //Or you can process the information within the go program
-        w.Header().Set("Content-Type", "application/json")
-        w.Write(bodyBytes)
-
-    }
-}
-```
-
-## APIs
-
-### Basics
-
-#### Response to request
-
-In order for the script to return something to the screen / caller as JSON / TEXT response, 
-one of these functions has to be called.
-
-```
-sendResp(string)    => Response header with text/plain header
-sendJSONResp(json_string) => Response request with JSON header
-
-//Since v1.119
-sendJSONResp(object) => Overload function, allow the same API to send Javascript object directly without the need for manual stringify using JSON.stringify
-
-//Since AGI v2.0
-sendOK(); => Reply OK in plain text
-```
-
-Customize header:
-
-You can also use customized header in return string as follow.
-
-```
-//Set Response header to html
-HTTP_HEADER = "text/html; charset=utf-8";
-
-//Send Response
-sendResp("<p>你好世界!</p>");
-```
-
-#### Register Module to module list
-
-You can call to the following function to register your module to the system module list. It is recommended that you register your module during the startup process (in the init.agi script located in your module root)
-
-Example Usage: 
-
-```
-registerModule(JSON.stringify(moduleInfo));
-```
-
-Module Info defination
-
-```
-//DO NOT USE THIS IN CODE. THIS IS A DATATYPE REPRESENTATION ONLY
-//PLEASE SEE THE INIT SECTION FOR A REAL OBJECT EXAMPLE
-moduleInfo = {
-    Name string                    //Name of this module. e.g. "Audio"
-    Desc string                    //Description for this module
-    Group string                //Group of the module, e.g. "system" / "media" etc
-    IconPath string                //Module icon image path e.g. "Audio/img/function_icon.png"
-    Version string                //Version of the module. Format: [0-9]*.[0-9][0-9].[0-9]
-    StartDir string             //Default starting dir, e.g. "Audio/index.html"
-    SupportFW bool                 //Support floatWindow. If yes, floatWindow dir will be loaded
-    LaunchFWDir string             //This link will be launched instead of 'StartDir' if fw mode
-    SupportEmb bool                //Support embedded mode
-    LaunchEmb string             //This link will be launched instead of StartDir / Fw if a file is opened with this module
-    InitFWSize [int, int]         //Floatwindow init size. [0] => Width, [1] => Height
-    InitEmbSize [int, int]        //Embedded mode init size. [0] => Width, [1] => Height
-    SupportedExt string_array     //Supported File Extensions. e.g. ".mp3", ".flac", ".wav"
-}
-```
-
-#### Print to STDOUT (console)
-
-To print something for debug, you can print text directly to ArOZ Online Core terminal using
-
-```
-console.log("text");
-```
-
-It has the same effect as using fmt.Println in golang.
-
-#### Delayed operations
-
-Synchronized delay (or blocking delay) can be used with the delay function (ms)
-
-```
-delay(5000);
-```
-
-For async delayed / timer ticking operations like setTimeout or setInterval is currently not supported.
-
-### System Functions
-
-System Functions are AGI functions that can be called anytime (system startup / scheduled task and user request tasks)
-The following variables and functions are categorized as system functions.
-
-#### CONST
-
-```
-BUILD_VERSION
-INTERNVAL_VERSION
-LOADED_MODULES
-LOADED_STORAGES
-```
-
-#### VAR
-
-```
-HTTP_RESP
-HTTP_HEADER (Default: "text/plain")
-```
-
-You can set HTTP_RESP with HTTP_HEADER to create custom response headers.
-For example, you can serve an HTML file using agi gateway
-
-```
-HTTP_RESP = "<html><body>Hello World</body></html>";
-HTTP_HEADER = "text/html";
-```
-
-#### Response Handlers
-
-```
-sendResp("Any string");
-sendJSONResp(JSON.stringify({text: "Hello World"));    //aka send Resp with JSON header
-```
-
-#### Database Related
-
-```
-newDBTableIfNotExists("tablename");
-dropDBTable("tablename");
-writeDBItem("tablename", "key", "value");
-readDBItem("tablename", "key");
-listDBTable("tablename"); //Return key value array
-deleteDBItem("tablename", "key");
-```
-
-#### Register and Packages
-
-```
-registerModule(JSON.stringify(moduleLaunchInfo)); //See moduleLaunchInfo in the sections above
-[deprecated] requirepkg("ffmpeg");
-[deprecated] execpkg("ffmpeg",'-i "files/users/TC/Desktop/群青.mp3" "files/users/TC/Desktop/群青.flac'); //ffmpeg must be required() before use
-```
-
-#### Structure & OOP
-
-```
-includes("hello world.js"); //Include another js / agi file within the current running one, return false if failed
-```
-
-### User Functions
-
-Users function are function group that only be usable when the interface is started from a user request.
-
-#### CONST
-
-```
-USERNAME
-USERICON
-USERQUOTA_TOTAL
-USERQUOTA_USED
-
-//Since AGI 1.3
-USER_VROOTS
-USER_MODULES //Might return ["*"] for admin permission
-```
-
-#### Filepath Virtualization
-
-```
-[deprecated] decodeVirtualPath("user:/Desktop"); //Convert virtual path (e.g. user:/Desktop) to real path (e.g. ./files/user/username/Desktop)
-[deprecated] decodeAbsoluteVirtualPath("user:/Desktop"); //Same as decodeVirtualPath but return in absolute path instead of relative path from the arozos binary root
-[deprecated] encodeRealPath("files/users/User/Desktop"); //Convert realpath into virtual path
-```
-
-#### Permission Related
-
-```
-getUserPermissionGroup();
-userIsAdmin(); => Return true / false
-```
-
-#### User Creation, Edit and Removal
-
-All the command in this section require administrator permission. To check if user is admin, use ``` userIsAdmin() ```.
-
-```
-userExists(username);
-createUser(username, password, defaultGroup);    //defaultGroup must be one of the permission group that exists in the system
-removeUser(username); //Return true if success, false if failed
-```
-
-### Serverless Function (agi v2.1)
-
-Since agi v2.1, developers can access agi and do dynamic programming via external agi call using ARZ Serverless panel. The following APIs allow devs to access GET, POST parameters and request Body on demand.
-
-Notes: These functions are only usable when called via the ARZ Serverless endpoints (URI start from ```/api/remote/```)
-
-#### CONST
-
-```
-REQ_METHOD //Get the method of request, e.g. GET / HEAD / POST
-```
-
-#### FUNCTIONS
-
-```
-getPara(key) //Return the GET value of given key
-postPara(key) //Return the POST value of given key
-readBody() //Get the whole request body as string
-```
-
-When there is an invalid operations or undefined keys, these functions always return null as default.
-
-#### Library requirement
-
-You can request other library to be loaded and have extra functions to work with files / images.
-
-```
-requirelib("filelib");
-```
-
-#### Include other script files
-
-You can also include another js file to load shared code between scripts
-
-```
-includes("hello world.js")
-```
-
-### Execute tasks in another routine
-
-You can use the execd function to execute something that is long pulling after the main thread returned the results of the current calculation.
-
-```
- execd("execd.js", "Payload to child")
-```
-
-To check if the current script is being executed as routine process, check for the following variable.
-
-```
-if (typeof PARENT_DETACHED == 'undefined'){
-    //This is parent
-}else if (PARENT_DETACHED == true){
-    //This is child
-}
-```
-
-To get the payload in child routine, get the following variable (Default: empty string)
-
-```
-PARENT_PAYLOAD
-```
-
-### filelib
-
-filelib is the core library for users to interact with the local filesystem.
-
-To use any of the library, the agi script must call the requirelib before calling any filelib functions. Example as follows.
-
-```
-if (!requirelib("filelib")){
-    console.log("Filelib import failed");
-}else{
-    console.log(filelib.fileExists("user:/Desktop/"));
-}
-```
-
-#### Filelib functions
-
-```
-    filelib.writeFile("user:/Desktop/test.txt", "Hello World");         //Write to file
-    filelib.readFile("user:/Desktop/test.txt");                            //Read from file
-    filelib.deleteFile("user:/Desktop/test.txt");                         //Delete a file by given path
-    filelib.readdir("user:/Desktop/");                                     //List all subdirectories within this directory
-    filelib.walk("user:/Desktop/");                                     //Recursive scan dir and return all files and folder in subdirs
-    filelib.glob("user:/Desktop/*.jpg", "smallToLarge");
-    filelib.aglob("user:/Desktop/*.jpg", "user");
-    filelib.filesize("user:/Desktop/test.jpg");
-    filelib.fileExists("user:/Desktop/test.jpg");
-    filelib.isDir("user:/Desktop/NewFolder/");
-    filelib.md5("user:/Desktop/test.jpg");
-    filelib.mkdir("user/Desktop/NewFolder");    
-    filelib.mtime("user:/Desktop/test.jpg", true);                             //Get modification time, return unix timestamp. Set the 2nd paramter to false for human readble format
-    filelib.rname("user:/Deskop");                                         //Get Rootname, return "User"
-```
-
-##### Special sorting mode for glob and aglob
-
-For glob and aglob, developer can pass in the following sorting modes (case sensitive)
-
-- default
-- reverse
-- smallToLarge
-- largeToSmall
-- mostRecent
-- leastRecent
-- smart (Added in v1.119, AGI only, for sorting filename containing digits with no zero pads)
-
-```
-//Example for sorting the desktop files to largeToSmall
-filelib.aglob("user:/Desktop/*", "largeToSmall");
-```
-
-To use the user default option which user has set in File Manager WebApp, pass in "user". Default sorting method is "default"
-
-```
-//Example of using user's selected mode
-filelib.aglob("user:/Desktop/*.jpg", "user");
-```
-
-##### Return type of filelib.readdir (Since ArozOS v2.002 / AGI 2.1)
-
-For filelib.readdir, it will return an array with the following object structure
-
-```go
-type fileInfo struct {
-    Filename string
-    Filepath string
-    Ext      string
-    Filesize int64
-    Modtime  int64
-    IsDir    bool
-}
-```
-
-Example return value (in JSON object, not stringify JSON string)
-
-```json
-[
-  {
-    "Ext": ".mp3",
-    "Filename": "MyMusic.mp3",
-    "Filepath": "user:/Desktop/MyMusic.mp3",
-    "Filesize": 12841616,
-    "IsDir": false,
-    "Modtime": 1653400359
-  },
-  {
-    "Ext": ".shortcut",
-    "Filename": "Code Studio.shortcut",
-    "Filepath": "user:/Desktop/Code Studio.shortcut",
-    "Filesize": 63,
-    "IsDir": false,
-    "Modtime": 1644421891
-  },
-  {
-    "Ext": ".txt",
-    "Filename": "END USER LICENSE.txt",
-    "Filepath": "files/users/TC/Desktop/END USER LICENSE.txt",
-    "Filesize": 11698,
-    "IsDir": false,
-    "Modtime": 1653318436
-  }
-]
-```
-
-### Relative path support when execute in personal page (Since ArozOS v2.005 / AGI 2.2)
-
-AGI 2.2 filelib support execution of file operations as relative path when the file is executed via personal page link. For example:
-
-```javascript
-var content = filelib.readFile("./untitled.md");
-```
-
-**In other library or use case, please use the full path of resources instead.**
-
-### appdata
-
-An API for access files inside the web folder. This API only provide read only functions. Include the appdata lib as follows.
-
-```javascript
-requirelib("appdata");
-```
-
-#### appdata functions
-
-```
-appdata.readFile("UnitTest/appdata.txt"); //Return false (boolean) if read failed
-appdata.listDir("UnitTest/backend/"); //Return a list of files in JSON string
-```
-
-### imagelib
-
-A basic image handling library to process images. Allowing basic image resize,
-get image dimension and others (to be expanded)
-
-```
-//Include the library
-requirelib("imagelib");
-```
-
-#### ImageLib functions
-
-```
-imagelib.getImageDimension("user:/Desktop/test.jpg");                                     //return [width, height]
-imagelib.resizeImage("user:/Desktop/input.png", "user:/Desktop/output.png", 500, 300);     //Resize input.png to 500 x 300 pixal and write to output.png
-imagelib.loadThumbString("user:/Desktop/test.jpg"); //Load the given file's thumbnail as base64 string, return false if failed
-imagelib.cropImage("user:/Desktop/test.jpg", "user:/Desktop/out.jpg",100,100,200,200)); 
-//Convert a RAW photo to a normal JPEG, return true on success and false on failure
-imagelib.rawToJPEG("user:/Desktop/photo.ARW", "user:/Desktop/photo.jpg");
-//Classify an image using neural network, since v1.119
-imagelib.classify("tmp:/classify.jpg", "yolo3"); 
-```
-
-#### Convert RAW to JPEG
-
-```
-Convert a camera RAW photo into a standard JPEG by extracting its embedded
-full-size preview. Supported RAW formats: .arw, .cr2, .dng, .nef, .raf, .orf
-
-1) Input file (virtual path to the RAW photo)
-2) Output file (virtual path, must end in .jpg or .jpeg, overwritten if exists)
-
-return true if success, false if failed (e.g. input is not a supported RAW
-format, or no embedded preview could be extracted)
-```
-
-#### Crop Image Options
-
-```
-Crop the given image with the following arguemnts: 
-
-1) Input file (virtual path)
-2) Output file (virtual path, will be overwritten if exists)
-3) Position X
-4) Position Y
-5) Crop With
-6) Crop Height
-
-return true if success, false if failed
-```
-
-#### AI Classifier Options (since v1.119)
-
-**ImageLib AI Classifier has been removed in this version to reduce binary size for embedded devices. The imagelib.classify() function is no longer available.**
-
-The output of the classifier will output the followings
-
-```
-Name (string, the name of object detected)
-Percentage (float, the confidence of detection)
-Positions (integer array, the pixel location of the detected object in left, top, width, height sequence)
-```
-
-Here is an example code for parsing the output, or you can also directly throw it into the JSON stringify and process it on frontend
-
-```javascript
- var results = imagelib.classify("tmp:/classify.jpg"); 
-    var responses = [];
-    for (var i = 0; i < results.length; i++){
-        responses.push({
-            "object": results[i].Name,
-            "confidence": results[i].Percentage,
-            "position_x": results[i].Positions[0],
-            "position_y": results[i].Positions[1],
-            "width": results[i].Positions[2],
-            "height": results[i].Positions[3]
-        });
-    }
-```
-
-### http
-
-A basic http function group that allow GET / POST / HEAD / Download request to other web resources
-
-```
-//Include the library
-requirelib("http");
-```
-
-#### http functions
-
-```
-http.get("http://example.com/api/"); //Create a get request, return the respond body
-http.post("http://localhost:8080/system/file_system/listDir", JSON.stringify({
-    dir: "user:/Desktop",
-    sort: "default"
-}));    //Create a POST request with JSON payload
-http.head("http://localhost:8080/", "Content-Type"); //Get the header field "Content-Type" from the requested url, leave 2nd paramter empty to return the whole header in JSON string
-http.download("http://example.com/music.mp3", "user:/Desktop", "(Optional) My Music.mp3")
-
-
-//Since agi v2.0
-http.getb64("http://example.com/photo.png"); //Get target resources as base64 bytes, return null if error
-http.getCode("http://redirect.example.com"); //Get response code for the target endpoint,if the code is redirection related (e.g. 302), a new varaible "_location" will be created to store the redirection address in String.
-
-//Since agi 2.3
-http.redirect("https://example.com", 307); //Redirect to target with status code
-```
-
-### aimodel
-
-The aimodel library lets AGI scripts call an OpenAI-compatible chat completion
-endpoint (OpenAI, Azure OpenAI, OpenRouter, Ollama, LM Studio, llama.cpp server,
-vLLM, etc.) **or** the Anthropic (Claude) Messages API. It supports both plain
-text prompts and file based prompts (images for vision models, or text documents
-inlined into the conversation).
-
-The API format (`openai` / `anthropic`), endpoint, global API key and default
-model are configured by an administrator in
-`System Settings > AI Integration > AI Model`. Per-model pricing and an optional
-usage quota are also set there, and every call's token usage and computed cost
-are accumulated into a metrics board on the same page. When the quota is reached,
-new requests fail with a quota error until the window resets or the cap is raised.
-
-```
-//Include the library
-requirelib("aimodel");
-```
-
-#### aimodel functions
-
-```
-//Plain text prompt. Returns the assistant reply as a string.
-var answer = aimodel.chat("Summarize the theory of relativity in one sentence.");
-
-//With options. All option fields are optional and fall back to the global config.
-var reply = aimodel.chat("Translate to French: good morning", {
-    model: "gpt-4o-mini",            //override the default model
-    system: "You are a translator.", //optional system prompt
-    temperature: 0.2,
-    max_tokens: 256,
-    endpoint: "http://localhost:11434/v1", //override the global endpoint
-    apikey: "sk-..."                 //override the global API key
-});
-
-//File based prompt. Pass a single vpath or an array of vpaths.
-//Images are sent as vision image_url parts; text documents are inlined.
-var caption = aimodel.chatWithFile("Describe this image", "user:/Photo/cat.png");
-var review  = aimodel.chatWithFile("Review this code", ["user:/Desktop/main.go"], {model:"gpt-4o"});
-
-//Low-level call. Pass a full messages array, returns the parsed response object
-//(including usage info and finish reason).
-var res = aimodel.request([
-    {role: "system", content: "You are a helpful assistant."},
-    {role: "user", content: "Hello!"}
-], {model: "gpt-4o-mini"});
-console.log(res.choices[0].message.content);
-console.log(res.usage.total_tokens);
-console.log(res.usage.tokens_per_second); //generation speed (completion tokens / sec)
-
-//Aggregated usage metrics (tokens + cost) accumulated by the system.
-var usage = aimodel.usage();
-console.log(usage.totalTokens, usage.totalCost);
-
-//Configured models (the models that have pricing defined) and the default.
-var models = aimodel.models(); // { default: "gpt-4o-mini", models: ["...", ...] }
-
-//Models advertised by the live endpoint (best effort, hits /models).
-var live = aimodel.listModels(); // { models: ["...", ...] } or { error: "..." }
-
-//Build OpenAI-style content parts from virtual file path(s). Useful for adding
-//attachments to a specific message in a aimodel.request() messages array.
-var parts = aimodel.fileParts(["user:/Photo/cat.png"]);
-var res2 = aimodel.request([
-    {role: "user", content: [{type:"text", text:"What is this?"}].concat(parts)}
-]);
-```
-
-The same calls work regardless of the configured API format — when Anthropic is
-selected, system prompts, image attachments and token usage are translated to and
-from the Anthropic Messages API automatically. Per-call you can override the format
-with the `apiFormat` option (`"openai"` or `"anthropic"`).
-
-On error (missing configuration, network failure, an error returned by the
-endpoint, or an exceeded usage quota), the call throws a JavaScript exception that
-can be caught with `try { ... } catch(e) { ... }`.
-
-### websocket
-
-websocket library provide request upgrade from normal HTTP request to WebSocket connections. 
-
-```
-//Include the library
-requirelib("websocket");
-```
-
-#### websocket functions
-
-```
-websocket.upgrade(10); //Timeout value in integer, return false if failed
-var recv = websocket.read(); //Blocking websocket listen
-websocket.send("Hello World"); //Send websocket to client (web UI)
-websocket.close(); //Close websocket connection
-```
-
-#### Usage Example
-
-Font-end
-
-```
-function getWSEndpoint(){
-    //Open opeartion in websocket
-    let protocol = "wss://";
-    if (location.protocol !== 'https:') {
-    protocol = "ws://";
-    }
-    wsControlEndpoint = (protocol + window.location.hostname + ":" + window.location.port);
-    return wsControlEndpoint;
-}
-
-let socket = new WebSocket(getWSEndpoint() + "/system/ajgi/interface?script=UnitTest/special/websocket.js");
-
-socket.onopen = function(e) {
-    log("✔️ Socket Opened");
-};
-
-socket.onmessage = function(event) {
-    log(`✔️ Received: ${event.data}`);
-};
-
-socket.onclose = function(event) {
-    if (event.wasClean) {
-    log(`📪 Connection Closed Cleanly code=${event.code} reason=${event.reason}`);
-    } else {
-    // e.g. server process killed or network down
-    // event.code is usually 1006 in this case
-    log(`❌ Connection Closed Unexpectedly`);
-    }
-};
-
-socket.onerror = function(error) {
-    log(`❌ ERROR! ${error.message}`);
-};
-```
-
-Backend example (without error handling). See the UnitTest/special/websocket.js for example with error handling.
-
-```
-function setup(){
-    //Require the WebSocket Library
-    requirelib("websocket");
-    websocket.upgrade(10);
-    console.log("WebSocket Opened!")
-    return true;
-}
-
-function waitForStart(){
-    websocket.send("Type something to start test");
-    var recv = websocket.read();
-    console.log(recv);
-}
-
-function loop(i){
-    websocket.send("Hello World: " + i);
-
-    //Wait for 1 second before next send
-    delay(1000);
-}
-
-function closing(){
-    //Try to close the WebSocket connection
-    websocket.close();
-}
-
-//Start executing the script
-if (setup()){
-    waitForStart();
-    for (var i = 0; i < 10; i++){
-        loop(i);
-    }
-    closing();
-}else{
-    console.log("WebSocket Setup Failed.")
-}
-```
-
-### iot
-
-The iot library provide access to the iot hardware control endpoints (or endpoints for short) in a much easier to use abstraction. 
-
-```
-//Include the library
-requirelib("iot");
-```
-
-#### iot functions
-
-```
-iot.ready() //Return the iot manager status. Return true if ready, false otherwise.
-iot.scan() //Force the iot manager to scan nearby iot devices
-iot.list() //List nearby iot device, might be cached. 
-iot.connect(devid) //Connect to a given device using device id
-iot.disconnect(devid) //Disconnect a given device using device id
-iot.status(devid) //Get the status of an iot device given its device ID, ID can be accessed using DeviceUUID key form an iot device object.
-iot.exec(devid, epname, payload); //Execute iot command using device id, endpoint name and payload (object).
-iot.iconTag(devid) //Get the device icon name from the device id
-```
-
-#### Example Return from iot.list() or iot.scan()
-
-```
-[
-   {
-      "ControlEndpoints":[
-         {
-            "Desc":"Toggle the power of the smart switch",
-            "Max":0,
-            "Min":0,
-            "Name":"Toggle Power",
-            "Regex":"",
-            "RelPath":"ay?o=1",
-            "Steps":0,
-            "Type":"none"
-         }
-      ],
-      "DeviceUUID":"84:F3:EB:3C:C7:F9",
-      "Handler":{ 
-          //hidden fields 
-      },
-      "IPAddr":"192.168.0.177",
-      "Manufacturer":"Sonoff",
-      "Model":"Sonoff S2X Smart Switch",
-      "Name":"Lights",
-      "Port":80,
-      "RequireAuth":false,
-      "RequireConnect":false,
-      "Status":{
-         "Power":"ON"
-      },
-      "Version":""
-   }
-]
-```
-
-#### Usage Example
-
-The following code do not handle errors. Please see iot.exec.js for a full example.
-
-```
-if (iot.ready() == true){
-    //Get device list from the iot manager
-    var deviceList = iot.list();
-
-    //Assume the first device is the one we want to control
-    var thisDevice = deviceList[0];
-
-    //Assume the first endpoint is the one we want to execute
-    var targetEndpoint = thisDevice.ControlEndpoints[0];
-
-    //Connect to the iot device
-    iot.connect(thisDevice.DeviceUUID);
-
-    //Execute the endpoint and get response from iot device
-    var results = iot.exec(thisDevice.DeviceUUID, targetEndpoint.Name, {});
-
-    //Disconnect the iot device after use
-    iot.disconnect(thisDevice.DeviceUUID);
-
-    if (results == false){
-        console.log("Something went wrong");
-    }else{
-        console.log("It works!" + JSON.stringify(results))
-    }
-}
-```
-
-For detailed example for other functions, see the js file located at ```UnitTest/backend/iot.*.js```
-
-### Share
-
-The share API allow access to the ArozOS share interface and generate UUID based on the shared file.
-
-```
-requirelib("share");
-```
-
-#### share functions
-
-```
-share.shareFile("user:/Desktop/test.pptx", 300); //File virtual path and timeout in seconds, return UUID of share
-share.getFileShareUUID("user:/Desktop/test.pptx"); //Get the share UUID of a given file, return null if not shared
-share.fileIsShared("user:/Desktop/test.pptx"); //Return true / false
-share.checkShareExists(shareUUID); //Return true / false
-share.checkSharePermission(shareUUID); //Return the share permission of shares (anyone / signedin / samegroup), return null if shareUUID invalid.
-share.removeShare(shareUUID);
-```
-
-#### Share Timeout
-
-For ```shareFile``` timeout value, **if set to 0 or unset, it will default to "forever"**. Hence, the share will not be automatically removed after timeout 
-
-Please also note that the share timeout is done by the AGI gateway system runtime. Hence, if you have shutdown / reset your ArozOS within the set period of time, your share **will not get automatically removed after the system startup again**.
+# AGI Doc
+Moved to mod/agi/README.md

+ 2 - 2
src/main.flags.go → src/flags.go

@@ -33,12 +33,13 @@ var subserviceBasePort = 12810            //Next subservice port
 
 // =========== SYSTEM BUILD INFORMATION ==============
 var build_version = "development"                      //System build flag, this can be either {development / production / stable}
-var internal_version = "0.2.026"                       //Internal build version, [fork_id].[major_release_no].[minor_release_no]
+var internal_version = "3.0.0"                         //Internal build version, [fork_id].[major_release_no].[minor_release_no]
 var deviceUUID string                                  //The device uuid of this host
 var deviceVendor = "IMUSLAB.INC"                       //Vendor of the system
 var deviceVendorURL = "http://imuslab.com"             //Vendor contact information
 var deviceModel = "AR100"                              //Hardware Model of the system
 var deviceModelDesc = "General Purpose Cloud Platform" //Device Model Description
+var vendorWebExists = false                            //Flag to indicate if the vendor web directory exists, this is used to determine if the system should serve vendor customized web resources or not
 var vendorResRoot = "./vendor-res/"                    //Root folder for vendor overwrite resources
 
 // =========== RUNTTIME RELATED ================
@@ -111,5 +112,4 @@ var nightlyTaskRunTime = flag.Int("ntt", 3, "Nightly tasks execution time. Defau
 var maxTempFileKeepTime = flag.Int("tmp_time", 86400, "Time before tmp file will be deleted in seconds. Default 86400 seconds = 24 hours")
 
 // Flags related to ArozOS Cluster services
-var allow_clustering = flag.Bool("allow_cluster", true, "Enable cluster operations within LAN. Require allow_mdns=true flag")
 var allow_iot = flag.Bool("allow_iot", true, "Enable IoT related APIs and scanner. Require MDNS enabled")

+ 21 - 2
src/main.go

@@ -12,6 +12,7 @@ import (
 	"time"
 
 	console "imuslab.com/arozos/mod/console"
+	fs "imuslab.com/arozos/mod/filesystem"
 	"imuslab.com/arozos/mod/info/logger"
 )
 
@@ -110,9 +111,27 @@ func main() {
 	Run_Test()
 
 	//Initiate all the static files transfer
-	fs := http.FileServer(http.Dir("./web"))
+	baseFileServer := http.FileServer(http.Dir("./web"))
+	vendorWebDir := filepath.Join(vendorResRoot, "web")
+	// Wrap the base file server: serve from vendorResRoot/web/ when a matching
+	// file exists there, falling through to ./web/ otherwise.
+	fileServer := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		vendorPath := filepath.Join(vendorWebDir, filepath.FromSlash(r.URL.Path))
+		if !vendorWebExists {
+			// Early exit if the vendor web directory doesn't exist, to avoid unnecessary file existence checks on every request.
+			baseFileServer.ServeHTTP(w, r)
+			return
+		}
+		if info, err := os.Stat(vendorPath); err == nil && !info.IsDir() {
+			http.ServeFile(w, r, vendorPath)
+			return
+		}
+		baseFileServer.ServeHTTP(w, r)
+	})
+	vendorWebExists = fs.FileExists(vendorWebDir)
+
 	//Updates 2022-09-06: Gzip handler moved inside the master router
-	http.Handle("/", mrouter(fs))
+	http.Handle("/", mrouter(fileServer))
 
 	//Setup handler for Ctrl +C
 	SetupCloseHandler()

+ 12 - 3
src/main.router.go

@@ -154,7 +154,7 @@ func mrouter(h http.Handler) http.Handler {
 			if !*enable_dir_listing {
 				if strings.HasSuffix(r.URL.Path, "/") {
 					//User trying to access a directory. Send NOT FOUND.
-					if fs.FileExists("web" + r.URL.Path + "index.html") {
+					if fileExistsInWebOrVendor(r.URL.Path + "index.html") {
 						//Index exists. Allow passthrough
 
 					} else {
@@ -163,7 +163,7 @@ func mrouter(h http.Handler) http.Handler {
 					}
 				}
 			}
-			if !fs.FileExists("web" + r.URL.Path) {
+			if !fileExistsInWebOrVendor(r.URL.Path) {
 				//File not found
 				errorHandleNotFound(w, r)
 				return
@@ -171,7 +171,7 @@ func mrouter(h http.Handler) http.Handler {
 			routerStaticContentServer(h, w, r)
 		} else {
 			//User not logged in. Check if the path end with public/. If yes, allow public access
-			if !fs.FileExists(filepath.Join("./web", r.URL.Path)) {
+			if !fileExistsInWebOrVendor(r.URL.Path) {
 				//Requested file not exists on the server. Return not found
 				errorHandleNotFound(w, r)
 			} else if r.URL.Path[len(r.URL.Path)-1:] != "/" && filepath.Base(filepath.Dir(r.URL.Path)) == "public" {
@@ -192,6 +192,15 @@ func mrouter(h http.Handler) http.Handler {
 	})
 }
 
+// fileExistsInWebOrVendor reports whether a URL path resolves to a file in
+// either the standard ./web directory or the vendorResRoot/web/ overlay.
+func fileExistsInWebOrVendor(urlPath string) bool {
+	if fs.FileExists("web" + urlPath) {
+		return true
+	}
+	return fs.FileExists(filepath.Join(vendorResRoot, "web", urlPath))
+}
+
 func routerStaticContentServer(h http.Handler, w http.ResponseWriter, r *http.Request) {
 	if *enable_gzip {
 		gzipmiddleware.Compress(h).ServeHTTP(w, r)

+ 44 - 0
src/startup.go

@@ -6,8 +6,10 @@ package main
 */
 
 import (
+	"encoding/json"
 	"fmt"
 	"os"
+	"path/filepath"
 
 	db "imuslab.com/arozos/mod/database"
 	"imuslab.com/arozos/mod/filesystem"
@@ -15,9 +17,51 @@ import (
 	"imuslab.com/arozos/mod/info/logger"
 )
 
+// vendorInfo holds optional vendor overrides loaded from vendor_info.json.
+type vendorInfo struct {
+	Vendor    string `json:"vendor"`
+	VendorURL string `json:"url"`
+	Model     string `json:"model"`
+	ModelDesc string `json:"desc"`
+}
+
+// loadVendorInfo reads vendor_info.json from vendorResRoot and overrides the
+// deviceVendor / deviceVendorURL / deviceModel / deviceModelDesc globals when
+// the file is present and valid.  Missing fields are left at their defaults.
+func loadVendorInfo() {
+	vendorInfoPath := filepath.Join(vendorResRoot, "vendor_info.json")
+	if !fs.FileExists(vendorInfoPath) {
+		return
+	}
+	content, err := os.ReadFile(vendorInfoPath)
+	if err != nil {
+		logger.PrintAndLog("Vendor", "Failed to read vendor_info.json", err)
+		return
+	}
+	var info vendorInfo
+	if err := json.Unmarshal(content, &info); err != nil {
+		logger.PrintAndLog("Vendor", "Failed to parse vendor_info.json", err)
+		return
+	}
+	if info.Vendor != "" {
+		deviceVendor = info.Vendor
+	}
+	if info.VendorURL != "" {
+		deviceVendorURL = info.VendorURL
+	}
+	if info.Model != "" {
+		deviceModel = info.Model
+	}
+	if info.ModelDesc != "" {
+		deviceModelDesc = info.ModelDesc
+	}
+	logger.PrintAndLog("Vendor", "Vendor info loaded from "+vendorInfoPath, nil)
+}
+
 func RunStartup() {
 	systemWideLogger, _ = logger.NewLogger("system", "system/logs/system/", true)
 	logger.SetDefaultLogger(systemWideLogger)
+	loadVendorInfo()
 	//1. Initiate the main system database
 
 	//Check if system or web both not exists and web.tar.gz exists. Unzip it for the user