接入ai
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
defaultProvider: deepseek
|
||||
defaultModel: deepseek:deepseek-chat
|
||||
|
||||
providers:
|
||||
openai:
|
||||
type: openai
|
||||
apiKeyEnv: OPENAI_API_KEY
|
||||
baseURL: https://api.openai.com/v1
|
||||
models:
|
||||
default: gpt-5
|
||||
|
||||
deepseek:
|
||||
type: openai-compatible
|
||||
apiKeyEnv: DEEPSEEK_API_KEY
|
||||
baseURL: https://api.deepseek.com
|
||||
models:
|
||||
default: deepseek-chat
|
||||
reasoner: deepseek-reasoner
|
||||
|
||||
uploads:
|
||||
maxImageMB: 20
|
||||
maxStepMB: 200
|
||||
maxOtherMB: 50
|
||||
allowed:
|
||||
- image/png
|
||||
- image/jpeg
|
||||
- image/webp
|
||||
- .step
|
||||
- .stp
|
||||
|
||||
cad:
|
||||
taskRoot: ./data/tasks
|
||||
viewer:
|
||||
mode: iframe
|
||||
allowedOrigins:
|
||||
- http://127.0.0.1
|
||||
- http://localhost
|
||||
Generated
+730
@@ -0,0 +1,730 @@
|
||||
{
|
||||
"name": "orbit-agent-server",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "orbit-agent-server",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@ai-sdk/openai": "4.0.20",
|
||||
"@ai-sdk/openai-compatible": "3.0.14",
|
||||
"ai": "7.0.37",
|
||||
"yaml": "^2.8.1",
|
||||
"zod": "^4.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
"tsx": "^4.20.6",
|
||||
"typescript": "^5"
|
||||
}
|
||||
},
|
||||
"node_modules/@ai-sdk/gateway": {
|
||||
"version": "4.0.28",
|
||||
"resolved": "https://registry.npmmirror.com/@ai-sdk/gateway/-/gateway-4.0.28.tgz",
|
||||
"integrity": "sha512-ee9TsNO3mkgHDWmTmJ8Fvltr6PFh52zhrV2+FaKJY3F3iu7kWZi5tCRJCR1HVhhlpj6lHniUb28nLQdPHkA/1Q==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider": "4.0.3",
|
||||
"@ai-sdk/provider-utils": "5.0.12",
|
||||
"@vercel/oidc": "3.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zod": "^3.25.76 || ^4.1.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@ai-sdk/openai": {
|
||||
"version": "4.0.20",
|
||||
"resolved": "https://registry.npmmirror.com/@ai-sdk/openai/-/openai-4.0.20.tgz",
|
||||
"integrity": "sha512-/Hu+btLIO2zuYqCp//vBBAGdM/BeN4gds+NWrlv7Y9WrcTXwnEEwh6P3QZIWh2Tt1I1lswuDIpAJ4t/c/rws3Q==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider": "4.0.3",
|
||||
"@ai-sdk/provider-utils": "5.0.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zod": "^3.25.76 || ^4.1.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@ai-sdk/openai-compatible": {
|
||||
"version": "3.0.14",
|
||||
"resolved": "https://registry.npmmirror.com/@ai-sdk/openai-compatible/-/openai-compatible-3.0.14.tgz",
|
||||
"integrity": "sha512-nUl7dBHUDTqcnuWGnEbKMwDDqL94BxRfibLwYv8uWp+kZn8Zqdxy7tUBt4sv2hpHQi+S2P6bUyqW18xmbykErw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider": "4.0.3",
|
||||
"@ai-sdk/provider-utils": "5.0.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zod": "^3.25.76 || ^4.1.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@ai-sdk/provider": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/@ai-sdk/provider/-/provider-4.0.3.tgz",
|
||||
"integrity": "sha512-e0CpNWJUY7OxAFAnCZkw+ri9QOHWwTs1tXP42782KFGCU07qt8NiXCrCVowyCB5dP2r5/Uls+g2oPd8kOJn9dw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"json-schema": "^0.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@ai-sdk/provider-utils": {
|
||||
"version": "5.0.12",
|
||||
"resolved": "https://registry.npmmirror.com/@ai-sdk/provider-utils/-/provider-utils-5.0.12.tgz",
|
||||
"integrity": "sha512-bbhlOgHeYwrIGheLkM6fhS8hVger8uFPmcOLg+kxc9EFh7y30XYorWhthlYAgpadO3SJhFZrIcEknN7qEqEVvA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider": "4.0.3",
|
||||
"@standard-schema/spec": "^1.1.0",
|
||||
"@workflow/serde": "4.1.0",
|
||||
"eventsource-parser": "^3.0.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zod": "^3.25.76 || ^4.1.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz",
|
||||
"integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.28.2.tgz",
|
||||
"integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz",
|
||||
"integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.28.2.tgz",
|
||||
"integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz",
|
||||
"integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz",
|
||||
"integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz",
|
||||
"integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz",
|
||||
"integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz",
|
||||
"integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz",
|
||||
"integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz",
|
||||
"integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz",
|
||||
"integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz",
|
||||
"integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz",
|
||||
"integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz",
|
||||
"integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz",
|
||||
"integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz",
|
||||
"integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz",
|
||||
"integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz",
|
||||
"integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz",
|
||||
"integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz",
|
||||
"integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz",
|
||||
"integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz",
|
||||
"integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz",
|
||||
"integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz",
|
||||
"integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz",
|
||||
"integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@standard-schema/spec": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.19.43",
|
||||
"resolved": "https://registry.npmmirror.com/@types/node/-/node-20.19.43.tgz",
|
||||
"integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vercel/oidc": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/@vercel/oidc/-/oidc-3.2.0.tgz",
|
||||
"integrity": "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/@workflow/serde": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/@workflow/serde/-/serde-4.1.0.tgz",
|
||||
"integrity": "sha512-pav4F2BoirECWR7Nf1TKt+2eETcBj7jj4cBefQ8VXQCA6NPkaKeLfj/zMgi+3zYV5ZIBT4GuUiphsj0/b9hPQQ==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/ai": {
|
||||
"version": "7.0.37",
|
||||
"resolved": "https://registry.npmmirror.com/ai/-/ai-7.0.37.tgz",
|
||||
"integrity": "sha512-stF+SEQJgKY3Qfe3FwNzqUrehHviOp2l7LemoI8YMOa0Zk5PxKsRNgUk3cHXz0RAue9RRyCAis2fz6LSCP6EKw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@ai-sdk/gateway": "4.0.28",
|
||||
"@ai-sdk/provider": "4.0.3",
|
||||
"@ai-sdk/provider-utils": "5.0.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zod": "^3.25.76 || ^4.1.8"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.28.2.tgz",
|
||||
"integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.28.2",
|
||||
"@esbuild/android-arm": "0.28.2",
|
||||
"@esbuild/android-arm64": "0.28.2",
|
||||
"@esbuild/android-x64": "0.28.2",
|
||||
"@esbuild/darwin-arm64": "0.28.2",
|
||||
"@esbuild/darwin-x64": "0.28.2",
|
||||
"@esbuild/freebsd-arm64": "0.28.2",
|
||||
"@esbuild/freebsd-x64": "0.28.2",
|
||||
"@esbuild/linux-arm": "0.28.2",
|
||||
"@esbuild/linux-arm64": "0.28.2",
|
||||
"@esbuild/linux-ia32": "0.28.2",
|
||||
"@esbuild/linux-loong64": "0.28.2",
|
||||
"@esbuild/linux-mips64el": "0.28.2",
|
||||
"@esbuild/linux-ppc64": "0.28.2",
|
||||
"@esbuild/linux-riscv64": "0.28.2",
|
||||
"@esbuild/linux-s390x": "0.28.2",
|
||||
"@esbuild/linux-x64": "0.28.2",
|
||||
"@esbuild/netbsd-arm64": "0.28.2",
|
||||
"@esbuild/netbsd-x64": "0.28.2",
|
||||
"@esbuild/openbsd-arm64": "0.28.2",
|
||||
"@esbuild/openbsd-x64": "0.28.2",
|
||||
"@esbuild/openharmony-arm64": "0.28.2",
|
||||
"@esbuild/sunos-x64": "0.28.2",
|
||||
"@esbuild/win32-arm64": "0.28.2",
|
||||
"@esbuild/win32-ia32": "0.28.2",
|
||||
"@esbuild/win32-x64": "0.28.2"
|
||||
}
|
||||
},
|
||||
"node_modules/eventsource-parser": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/eventsource-parser/-/eventsource-parser-3.1.1.tgz",
|
||||
"integrity": "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmmirror.com/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/json-schema": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmmirror.com/json-schema/-/json-schema-0.4.0.tgz",
|
||||
"integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==",
|
||||
"license": "(AFL-2.1 OR BSD-3-Clause)"
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.23.12",
|
||||
"resolved": "https://registry.npmmirror.com/tsx/-/tsx-4.23.12.tgz",
|
||||
"integrity": "sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "~0.28.0"
|
||||
},
|
||||
"bin": {
|
||||
"tsx": "dist/cli.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmmirror.com/yaml/-/yaml-2.9.0.tgz",
|
||||
"integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"yaml": "bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/eemeli"
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmmirror.com/zod/-/zod-4.4.3.tgz",
|
||||
"integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "orbit-agent-server",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "tsx src/server.ts",
|
||||
"test": "node --test --import tsx src/**/*.test.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/openai": "4.0.20",
|
||||
"@ai-sdk/openai-compatible": "3.0.14",
|
||||
"ai": "7.0.37",
|
||||
"yaml": "^2.8.1",
|
||||
"zod": "^4.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
"tsx": "^4.20.6",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { parseRequirementOverrides } from "./chat";
|
||||
|
||||
test("parses planetary reducer requirement overrides", () => {
|
||||
assert.deepEqual(
|
||||
parseRequirementOverrides("生成双级斜齿行星减速器,传动比 9,模数 0.5,3 个行星轮,外径不超过 90 mm"),
|
||||
{
|
||||
topology_family: "simple_2k_h_cascade",
|
||||
tooth_form: "helical",
|
||||
target_ratio: 9,
|
||||
module_mm: 0.5,
|
||||
planet_count: 3,
|
||||
max_outer_diameter_mm: 90,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("does not invent requirement overrides for ordinary text", () => {
|
||||
assert.deepEqual(parseRequirementOverrides("解释一下行星减速器的工作原理"), {});
|
||||
});
|
||||
@@ -0,0 +1,372 @@
|
||||
import { createOpenAI } from "@ai-sdk/openai";
|
||||
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
|
||||
import { streamText, type ModelMessage } from "ai";
|
||||
import { loadLlmConfig, resolveProviderApiKey, selectedModelId, type LlmConfig } from "./config";
|
||||
|
||||
type ChatMessage = {
|
||||
role: "user" | "assistant" | "system";
|
||||
content: string;
|
||||
};
|
||||
|
||||
function messageContent(value: unknown) {
|
||||
if (!value || typeof value !== "object") return "";
|
||||
const record = value as Record<string, unknown>;
|
||||
if (typeof record.content === "string") return record.content;
|
||||
if (typeof record.text === "string") return record.text;
|
||||
if (Array.isArray(record.parts)) {
|
||||
return record.parts.map((part) => {
|
||||
if (!part || typeof part !== "object") return "";
|
||||
const partRecord = part as Record<string, unknown>;
|
||||
if (partRecord.type === "text") return String(partRecord.text || "");
|
||||
if (typeof partRecord.text === "string") return partRecord.text;
|
||||
return "";
|
||||
}).join("");
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function normalizedMessages(messages: unknown[]): ChatMessage[] {
|
||||
return messages.map((message) => {
|
||||
const record = (message && typeof message === "object" ? message : {}) as Record<string, unknown>;
|
||||
const role: ChatMessage["role"] = record.role === "assistant" || record.role === "system" ? record.role : "user";
|
||||
return {
|
||||
role,
|
||||
content: messageContent(record),
|
||||
};
|
||||
}).filter((message) => message.content.trim() || message.role !== "user");
|
||||
}
|
||||
|
||||
function hostnameForUrl(value: string) {
|
||||
try {
|
||||
return new URL(value).hostname.toLowerCase();
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function isOfficialOpenAIBaseURL(value: string) {
|
||||
const hostname = hostnameForUrl(value);
|
||||
return hostname === "api.openai.com";
|
||||
}
|
||||
|
||||
function buildLanguageModel({
|
||||
apiKey,
|
||||
provider,
|
||||
providerConfig,
|
||||
model,
|
||||
}: {
|
||||
apiKey: string;
|
||||
provider: string;
|
||||
providerConfig: LlmConfig["providers"][string];
|
||||
model: string;
|
||||
}) {
|
||||
if (providerConfig.type === "openai" && isOfficialOpenAIBaseURL(providerConfig.baseURL)) {
|
||||
return createOpenAI({ apiKey, baseURL: providerConfig.baseURL })(model);
|
||||
}
|
||||
return createOpenAICompatible({
|
||||
name: provider,
|
||||
apiKey,
|
||||
baseURL: providerConfig.baseURL,
|
||||
})(model);
|
||||
}
|
||||
|
||||
function errorDetail(error: unknown) {
|
||||
if (!error || typeof error !== "object") {
|
||||
return "";
|
||||
}
|
||||
const record = error as Record<string, unknown>;
|
||||
const statusCode = record.statusCode || record.status || record.responseStatus;
|
||||
const data = record.data || record.responseBody || record.body;
|
||||
const parts = [];
|
||||
if (statusCode) {
|
||||
parts.push(`status=${String(statusCode)}`);
|
||||
}
|
||||
if (typeof data === "string") {
|
||||
parts.push(data.slice(0, 500));
|
||||
} else if (data && typeof data === "object") {
|
||||
const maybeError = (data as { error?: { message?: unknown } }).error;
|
||||
const message = maybeError?.message || (data as { message?: unknown }).message;
|
||||
if (message) {
|
||||
parts.push(String(message));
|
||||
}
|
||||
}
|
||||
return parts.join("; ");
|
||||
}
|
||||
|
||||
function formatModelError({
|
||||
error,
|
||||
provider,
|
||||
model,
|
||||
providerConfig,
|
||||
}: {
|
||||
error: unknown;
|
||||
provider: string;
|
||||
model: string;
|
||||
providerConfig: LlmConfig["providers"][string];
|
||||
}) {
|
||||
const baseHost = hostnameForUrl(providerConfig.baseURL) || "unknown-host";
|
||||
const message = error instanceof Error ? error.message : "unknown error";
|
||||
const detail = errorDetail(error);
|
||||
return [
|
||||
`provider: ${provider}`,
|
||||
`model: ${model}`,
|
||||
`baseURL host: ${baseHost}`,
|
||||
detail ? `${message} (${detail})` : message,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function compactJson(value: unknown, maxLength: number) {
|
||||
const source = JSON.stringify(value, null, 2);
|
||||
return source.length > maxLength ? `${source.slice(0, maxLength)}\n...<truncated>` : source;
|
||||
}
|
||||
|
||||
function buildSystemPrompt(viewerContext: unknown[], attachments: unknown[]) {
|
||||
return [
|
||||
"You are the CAD Agent Studio assistant inside Orbit Joint Studio.",
|
||||
"Follow the CAD Agent Studio Agent Rules from cadSet.",
|
||||
"You may answer ordinary questions, clarify requirements, and explain engineering tradeoffs.",
|
||||
"Strict honesty rule: never claim that a CAD file, model, STEP, URDF, or preview artifact was generated unless a real server-side CAD generation tool has succeeded and returned artifact URLs.",
|
||||
"This Orbit integration exposes real server-side CAD generation, task storage, artifact download, and upload endpoints. The UI has a separate explicit Generate action; ordinary chat must remain ordinary chat.",
|
||||
"If the user asks you to generate or modify CAD inside chat, clarify the requirements and tell them to use the explicit Generate action when they are ready. Do not invent files, task ids, generated variants, or viewer state.",
|
||||
"Do not rely on hardcoded templates, canned examples, mock data, fake filenames, or imaginary viewer state.",
|
||||
"普通聊天、确认、说明、提问不得自动生成 CAD。只有真实 CAD 工具接通并成功写出 artifact 后,才允许说已经生成。",
|
||||
`Viewer context count: ${viewerContext.length}`,
|
||||
viewerContext.length ? compactJson(viewerContext, 10000) : "",
|
||||
`Attachment count: ${attachments.length}`,
|
||||
attachments.length ? compactJson(attachments, 10000) : "",
|
||||
].filter(Boolean).join("\n");
|
||||
}
|
||||
|
||||
function lastUserText(messages: unknown[]) {
|
||||
return [...messages].reverse().find((message) => {
|
||||
if (!message || typeof message !== "object") return false;
|
||||
return (message as Record<string, unknown>).role === "user";
|
||||
}) as Record<string, unknown> | undefined;
|
||||
}
|
||||
|
||||
function messageText(message: Record<string, unknown> | undefined) {
|
||||
if (!message) return "";
|
||||
if (typeof message.content === "string") return message.content;
|
||||
if (typeof message.text === "string") return message.text;
|
||||
return "";
|
||||
}
|
||||
|
||||
function hasCadIntent(text: string) {
|
||||
return /(生成|创建|设计|建模|重建|导出|修改|调整|改成).*(减速器|行星|关节|模组|cad|step|urdf|mjcf|模数|传动比|行星轮|齿宽|压力角)|\b(generate|create|design|reconstruct|export|modify|edit)\b.*\b(cad|step|urdf|mjcf|reducer|joint|planetary|module|ratio)\b/i.test(text);
|
||||
}
|
||||
|
||||
export type RequirementOverrides = {
|
||||
topology_family?: "simple_2k_h" | "simple_2k_h_cascade" | "ferguson_wolfrom";
|
||||
target_ratio?: number;
|
||||
tooth_form?: "spur" | "helical";
|
||||
module_mm?: number;
|
||||
planet_count?: number;
|
||||
helix_angle_deg?: number;
|
||||
pressure_angle_deg?: number;
|
||||
face_width_mm?: number;
|
||||
backlash_mm?: number;
|
||||
ring_rim_thickness_mm?: number;
|
||||
max_outer_diameter_mm?: number;
|
||||
};
|
||||
|
||||
function firstNumber(text: string, patterns: RegExp[], integer = false) {
|
||||
for (const pattern of patterns) {
|
||||
const match = text.match(pattern);
|
||||
if (!match) continue;
|
||||
const value = Number(match[1]);
|
||||
if (Number.isFinite(value)) return integer ? Math.trunc(value) : value;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function parseRequirementOverrides(text: string): RequirementOverrides {
|
||||
const overrides: RequirementOverrides = {};
|
||||
if (/(双级|两级|二级|串联|cascade|two[- ]stage)/i.test(text)) {
|
||||
overrides.topology_family = "simple_2k_h_cascade";
|
||||
} else if (/(ferguson|wolfrom|沃尔夫罗姆|弗格森)/i.test(text)) {
|
||||
overrides.topology_family = "ferguson_wolfrom";
|
||||
} else if (/(单级|simple[_ -]?2k[_ -]?h)/i.test(text)) {
|
||||
overrides.topology_family = "simple_2k_h";
|
||||
}
|
||||
|
||||
const toothForm = text.match(/(斜齿|helical|直齿|spur)/i);
|
||||
if (toothForm) {
|
||||
overrides.tooth_form = /(斜齿|helical)/i.test(toothForm[1]) ? "helical" : "spur";
|
||||
}
|
||||
|
||||
const numericPatterns: Array<[
|
||||
keyof Omit<RequirementOverrides, "topology_family" | "tooth_form">,
|
||||
RegExp[],
|
||||
boolean?,
|
||||
]> = [
|
||||
["target_ratio", [
|
||||
/(?:传动比|减速比|速比|ratio)\s*(?:为|是|设为|=|:|:|to)?\s*([0-9]+(?:\.[0-9]+)?)/i,
|
||||
/([0-9]+(?:\.[0-9]+)?)\s*(?::|:)\s*1\s*(?:传动比|减速比)?/i,
|
||||
]],
|
||||
["module_mm", [
|
||||
/(?:齿轮)?模数\s*(?:为|是|设为|=|:|:|to)?\s*([0-9]+(?:\.[0-9]+)?)/i,
|
||||
/module(?:_mm)?\s*(?:=|:|to)?\s*([0-9]+(?:\.[0-9]+)?)/i,
|
||||
]],
|
||||
["planet_count", [
|
||||
/(?:行星轮数量|行星轮数|行星轮)\s*(?:为|是|设为|=|:|:|to)?\s*([0-9]+)/i,
|
||||
/([0-9]+)\s*(?:个|颗|只)?\s*行星轮/i,
|
||||
/planet_count\s*(?:=|:|to)?\s*([0-9]+)/i,
|
||||
], true],
|
||||
["helix_angle_deg", [/(?:螺旋角|helix_angle(?:_deg)?)\s*(?:为|是|设为|=|:|:|to)?\s*([0-9]+(?:\.[0-9]+)?)/i]],
|
||||
["pressure_angle_deg", [/(?:压力角|pressure_angle(?:_deg)?)\s*(?:为|是|设为|=|:|:|to)?\s*([0-9]+(?:\.[0-9]+)?)/i]],
|
||||
["face_width_mm", [/(?:齿宽|face_width(?:_mm)?)\s*(?:为|是|设为|=|:|:|to)?\s*([0-9]+(?:\.[0-9]+)?)/i]],
|
||||
["backlash_mm", [/(?:齿侧间隙|侧隙|backlash(?:_mm)?)\s*(?:为|是|设为|=|:|:|to)?\s*([0-9]+(?:\.[0-9]+)?)/i]],
|
||||
["ring_rim_thickness_mm", [/(?:齿圈轮缘厚度|轮缘厚度|ring_rim_thickness(?:_mm)?)\s*(?:为|是|设为|=|:|:|to)?\s*([0-9]+(?:\.[0-9]+)?)/i]],
|
||||
["max_outer_diameter_mm", [
|
||||
/(?:最大外径|外径)\s*(?:小于|不超过|限制为|为|是|=|:|:|to|≤|<=)?\s*([0-9]+(?:\.[0-9]+)?)\s*(?:mm|毫米)?/i,
|
||||
/max_outer_diameter(?:_mm)?\s*(?:=|:|to|<=)?\s*([0-9]+(?:\.[0-9]+)?)/i,
|
||||
]],
|
||||
];
|
||||
for (const [name, patterns, integer] of numericPatterns) {
|
||||
const value = firstNumber(text, patterns, integer);
|
||||
if (value !== undefined) overrides[name] = value;
|
||||
}
|
||||
return overrides;
|
||||
}
|
||||
|
||||
function backendOrigin() {
|
||||
return String(process.env.TOOLCHAIN_URL || "http://127.0.0.1:8000").replace(/\/+$/, "");
|
||||
}
|
||||
|
||||
async function backendJson(pathname: string, init: RequestInit = {}) {
|
||||
const response = await fetch(`${backendOrigin()}${pathname}`, init);
|
||||
const payload = await response.json().catch(() => null);
|
||||
if (!response.ok) {
|
||||
throw new Error(String(payload?.error || `CAD backend request failed (${response.status})`));
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
async function runExplicitCadAction({
|
||||
text,
|
||||
selectedTaskId,
|
||||
}: {
|
||||
text: string;
|
||||
selectedTaskId?: string;
|
||||
}) {
|
||||
if (/(重建|reconstruct).*(step|stp|上传)/i.test(text) || /(step|stp|上传).*(重建|reconstruct)/i.test(text)) {
|
||||
if (!selectedTaskId) throw new Error("请先选择包含 STEP 上传文件的任务。");
|
||||
return backendJson(`/api/tasks/${encodeURIComponent(selectedTaskId)}/reconstruct`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ request: text }),
|
||||
});
|
||||
}
|
||||
const parameterPatterns: Array<[string, RegExp]> = [
|
||||
["module_mm", /(?:模数|module(?:_mm)?)\s*(?:改成|调整为|设为|=|to)?\s*([0-9]+(?:\.[0-9]+)?)/i],
|
||||
["target_ratio", /(?:传动比|减速比|ratio)\s*(?:改成|调整为|设为|=|to)?\s*([0-9]+(?:\.[0-9]+)?)/i],
|
||||
["planet_count", /(?:行星轮数量|行星轮数|planet_count)\s*(?:改成|调整为|设为|=|to)?\s*([0-9]+)/i],
|
||||
["face_width_mm", /(?:齿宽|face_width(?:_mm)?)\s*(?:改成|调整为|设为|=|to)?\s*([0-9]+(?:\.[0-9]+)?)/i],
|
||||
["pressure_angle_deg", /(?:压力角|pressure_angle(?:_deg)?)\s*(?:改成|调整为|设为|=|to)?\s*([0-9]+(?:\.[0-9]+)?)/i],
|
||||
];
|
||||
for (const [parameter, pattern] of parameterPatterns) {
|
||||
const match = text.match(pattern);
|
||||
if (!match) continue;
|
||||
if (!selectedTaskId) throw new Error("请先选择一个已生成的 CAD 任务,再修改参数。");
|
||||
return backendJson(`/api/tasks/${encodeURIComponent(selectedTaskId)}/parameters`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ parameter, value: Number(match[1]) }),
|
||||
});
|
||||
}
|
||||
if (/(导出|export).*(mjcf|mujoco)/i.test(text)) {
|
||||
if (!selectedTaskId) throw new Error("请先在右侧选择一个已生成任务,再导出 MJCF。");
|
||||
return backendJson(`/api/tasks/${encodeURIComponent(selectedTaskId)}/exports/mjcf`, { method: "POST" });
|
||||
}
|
||||
if (/(导出|export).*(urdf|机器人描述|机器人模型)/i.test(text)) {
|
||||
if (!selectedTaskId) throw new Error("请先在右侧选择一个已生成任务,再导出 URDF。");
|
||||
return backendJson(`/api/tasks/${encodeURIComponent(selectedTaskId)}/exports/urdf`, { method: "POST" });
|
||||
}
|
||||
const kind = /(关节|模组|joint|actuator)/i.test(text) ? "joint_module" : "reducer";
|
||||
return backendJson("/api/generate", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
kind,
|
||||
request: text,
|
||||
mode: "auto",
|
||||
requirementOverrides: parseRequirementOverrides(text),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
function textResponse(text: string) {
|
||||
return new Response(text, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "text/plain; charset=utf-8",
|
||||
"Cache-Control": "no-store",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function streamAgentText({
|
||||
messages,
|
||||
attachments,
|
||||
viewerContext,
|
||||
provider,
|
||||
model,
|
||||
selectedTaskId,
|
||||
}: {
|
||||
messages: unknown[];
|
||||
attachments: unknown[];
|
||||
viewerContext: unknown[];
|
||||
provider?: string;
|
||||
model?: string;
|
||||
selectedTaskId?: string;
|
||||
}) {
|
||||
const userText = messageText(lastUserText(messages));
|
||||
if (hasCadIntent(userText)) {
|
||||
const result = await runExplicitCadAction({ text: userText, selectedTaskId });
|
||||
const artifact = result?.robotExport?.packageUrl || result?.artifacts?.find((item: Record<string, unknown>) => item.role === "primary")?.url;
|
||||
const suffix = artifact ? `\n产物:${artifact}` : "";
|
||||
return textResponse(`${result?.summary || "CAD 工具链执行完成。"}\n任务 ID:${result?.taskId || selectedTaskId || "-"}${suffix}`);
|
||||
}
|
||||
const config = loadLlmConfig();
|
||||
const selected = selectedModelId(provider, model, config);
|
||||
const apiKey = resolveProviderApiKey(selected.providerConfig);
|
||||
if (!apiKey) {
|
||||
throw new Error(`Provider ${selected.provider} is not configured with an API key.`);
|
||||
}
|
||||
|
||||
const languageModel = buildLanguageModel({
|
||||
apiKey,
|
||||
provider: selected.provider,
|
||||
providerConfig: selected.providerConfig,
|
||||
model: selected.model,
|
||||
});
|
||||
|
||||
const modelMessages = normalizedMessages(messages).map((message) => ({
|
||||
role: message.role,
|
||||
content: message.content,
|
||||
})) as ModelMessage[];
|
||||
|
||||
if (!modelMessages.some((message) => message.role === "user")) {
|
||||
throw new Error("A user message is required.");
|
||||
}
|
||||
|
||||
const result = streamText({
|
||||
model: languageModel,
|
||||
system: buildSystemPrompt(viewerContext, attachments),
|
||||
messages: modelMessages,
|
||||
onError: (error) => {
|
||||
throw new Error(formatModelError({
|
||||
error,
|
||||
provider: selected.provider,
|
||||
model: selected.model,
|
||||
providerConfig: selected.providerConfig,
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
||||
return result.toTextStreamResponse({
|
||||
headers: {
|
||||
"Cache-Control": "no-store",
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
import { loadLlmConfig, publicConfig } from "./config";
|
||||
|
||||
test("loads llm config and redacts credentials in public config", async () => {
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "orbit-agent-config-"));
|
||||
const configPath = path.join(dir, "llm.config.yaml");
|
||||
await fs.writeFile(configPath, [
|
||||
"defaultProvider: openai",
|
||||
"defaultModel: openai:gpt-5",
|
||||
"providers:",
|
||||
" openai:",
|
||||
" type: openai",
|
||||
" apiKey: secret-key",
|
||||
" baseURL: https://api.openai.com/v1",
|
||||
" models:",
|
||||
" default: gpt-5",
|
||||
"uploads:",
|
||||
" maxImageMB: 20",
|
||||
" maxStepMB: 200",
|
||||
" maxOtherMB: 50",
|
||||
" allowed:",
|
||||
" - image/png",
|
||||
"cad:",
|
||||
" taskRoot: ./data/tasks",
|
||||
" viewer:",
|
||||
" mode: iframe",
|
||||
" allowedOrigins:",
|
||||
" - http://127.0.0.1",
|
||||
].join("\n"));
|
||||
|
||||
const previous = process.env.CAD_AGENT_STUDIO_CONFIG;
|
||||
process.env.CAD_AGENT_STUDIO_CONFIG = configPath;
|
||||
try {
|
||||
const config = loadLlmConfig();
|
||||
assert.equal(config.defaultProvider, "openai");
|
||||
assert.equal(config.providers.openai.apiKey, "secret-key");
|
||||
|
||||
const exposed = publicConfig(config);
|
||||
assert.equal(exposed.providers.openai.configured, true);
|
||||
assert.equal(Object.hasOwn(exposed.providers.openai, "apiKey"), false);
|
||||
} finally {
|
||||
if (previous === undefined) {
|
||||
delete process.env.CAD_AGENT_STUDIO_CONFIG;
|
||||
} else {
|
||||
process.env.CAD_AGENT_STUDIO_CONFIG = previous;
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,124 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import YAML from "yaml";
|
||||
import { z } from "zod";
|
||||
|
||||
const modelMapSchema = z.record(z.string(), z.string());
|
||||
|
||||
const providerSchema = z.object({
|
||||
type: z.enum(["openai", "openai-compatible"]),
|
||||
apiKey: z.string().min(1).optional(),
|
||||
apiKeyEnv: z.string().min(1).optional(),
|
||||
baseURL: z.string().min(1),
|
||||
models: modelMapSchema,
|
||||
}).refine((provider) => Boolean(provider.apiKey || provider.apiKeyEnv), {
|
||||
message: "provider requires apiKey or apiKeyEnv",
|
||||
});
|
||||
|
||||
const llmConfigSchema = z.object({
|
||||
defaultProvider: z.string().min(1),
|
||||
defaultModel: z.string().min(1),
|
||||
providers: z.record(z.string(), providerSchema),
|
||||
uploads: z.object({
|
||||
maxImageMB: z.number().positive(),
|
||||
maxStepMB: z.number().positive(),
|
||||
maxOtherMB: z.number().positive().default(50),
|
||||
allowed: z.array(z.string().min(1)),
|
||||
}),
|
||||
cad: z.object({
|
||||
taskRoot: z.string().min(1),
|
||||
viewer: z.object({
|
||||
mode: z.literal("iframe"),
|
||||
allowedOrigins: z.array(z.string().min(1)).default([]),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export type LlmConfig = z.infer<typeof llmConfigSchema>;
|
||||
|
||||
export function repoRoot() {
|
||||
return path.resolve(process.env.ORBIT_PROJECT_ROOT || path.join(import.meta.dirname, "..", ".."));
|
||||
}
|
||||
|
||||
function appPath(...segments: string[]) {
|
||||
return path.join(path.resolve(import.meta.dirname, ".."), ...segments);
|
||||
}
|
||||
|
||||
function expandEnvTemplate(value: string) {
|
||||
return value.replace(/\$\{([A-Z0-9_]+)(?::-([^}]*))?\}/gi, (_match, name: string, fallback = "") => {
|
||||
const envValue = process.env[name];
|
||||
return envValue === undefined || envValue === "" ? fallback : envValue;
|
||||
});
|
||||
}
|
||||
|
||||
function expandConfig(value: unknown): unknown {
|
||||
if (typeof value === "string") {
|
||||
return expandEnvTemplate(value);
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(expandConfig);
|
||||
}
|
||||
if (value && typeof value === "object") {
|
||||
return Object.fromEntries(Object.entries(value).map(([key, nextValue]) => [key, expandConfig(nextValue)]));
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function loadLlmConfig(): LlmConfig {
|
||||
const candidates = [
|
||||
process.env.CAD_AGENT_STUDIO_CONFIG
|
||||
? path.resolve(process.env.CAD_AGENT_STUDIO_CONFIG)
|
||||
: null,
|
||||
path.join(repoRoot(), "llm.config.yaml"),
|
||||
appPath("config", "llm.config.yaml"),
|
||||
appPath("config", "llm.config.yaml.example"),
|
||||
].filter((candidate): candidate is string => Boolean(candidate));
|
||||
|
||||
const configPath = candidates.find((candidate) => fs.existsSync(candidate));
|
||||
if (!configPath) {
|
||||
throw new Error("No LLM configuration file was found.");
|
||||
}
|
||||
|
||||
const source = fs.readFileSync(configPath, "utf8");
|
||||
return llmConfigSchema.parse(expandConfig(YAML.parse(source)));
|
||||
}
|
||||
|
||||
export function resolveProviderApiKey(provider: LlmConfig["providers"][string]) {
|
||||
const directKey = String(provider.apiKey || "").trim();
|
||||
if (directKey) {
|
||||
return directKey;
|
||||
}
|
||||
const envName = String(provider.apiKeyEnv || "").trim();
|
||||
if (!envName) {
|
||||
return "";
|
||||
}
|
||||
return process.env[envName] || "";
|
||||
}
|
||||
|
||||
export function selectedModelId(providerId: string | undefined, modelId: string | undefined, config = loadLlmConfig()) {
|
||||
const provider = providerId && config.providers[providerId] ? providerId : config.defaultProvider;
|
||||
const providerConfig = config.providers[provider];
|
||||
const candidate = String(modelId || "").trim();
|
||||
const model = candidate || providerConfig.models.default || config.defaultModel.split(":").pop() || "";
|
||||
return { provider, model, providerConfig };
|
||||
}
|
||||
|
||||
export function publicConfig(config = loadLlmConfig()) {
|
||||
return {
|
||||
defaultProvider: config.defaultProvider,
|
||||
defaultModel: config.defaultModel,
|
||||
providers: Object.fromEntries(Object.entries(config.providers).map(([id, provider]) => [
|
||||
id,
|
||||
{
|
||||
type: provider.type,
|
||||
baseURL: provider.baseURL,
|
||||
models: provider.models,
|
||||
configured: Boolean(resolveProviderApiKey(provider)),
|
||||
},
|
||||
])),
|
||||
uploads: config.uploads,
|
||||
cad: {
|
||||
viewer: config.cad.viewer,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import http from "node:http";
|
||||
import { publicConfig } from "./config";
|
||||
import { streamAgentText } from "./chat";
|
||||
|
||||
const host = process.env.AGENT_HOST || "127.0.0.1";
|
||||
const port = Number(process.env.AGENT_PORT || 8010);
|
||||
|
||||
function corsHeaders() {
|
||||
return {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "GET,POST,OPTIONS",
|
||||
"Access-Control-Allow-Headers": "Content-Type",
|
||||
};
|
||||
}
|
||||
|
||||
function sendJson(response: http.ServerResponse, status: number, payload: unknown) {
|
||||
const body = Buffer.from(JSON.stringify(payload), "utf8");
|
||||
response.writeHead(status, {
|
||||
...corsHeaders(),
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
"Content-Length": body.byteLength,
|
||||
});
|
||||
response.end(body);
|
||||
}
|
||||
|
||||
function readJson(request: http.IncomingMessage) {
|
||||
return new Promise<unknown>((resolve, reject) => {
|
||||
const chunks: Buffer[] = [];
|
||||
request.on("data", (chunk) => {
|
||||
chunks.push(Buffer.from(chunk));
|
||||
if (Buffer.concat(chunks).byteLength > 2 * 1024 * 1024) {
|
||||
reject(new Error("Request body is too large."));
|
||||
request.destroy();
|
||||
}
|
||||
});
|
||||
request.on("end", () => {
|
||||
const text = Buffer.concat(chunks).toString("utf8");
|
||||
if (!text.trim()) {
|
||||
resolve({});
|
||||
return;
|
||||
}
|
||||
try {
|
||||
resolve(JSON.parse(text));
|
||||
} catch {
|
||||
reject(new Error("Request body must be valid JSON."));
|
||||
}
|
||||
});
|
||||
request.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
async function writeWebResponse(nodeResponse: http.ServerResponse, webResponse: Response) {
|
||||
const headers: Record<string, string> = {
|
||||
...corsHeaders(),
|
||||
};
|
||||
webResponse.headers.forEach((value, key) => {
|
||||
headers[key] = value;
|
||||
});
|
||||
nodeResponse.writeHead(webResponse.status, headers);
|
||||
if (!webResponse.body) {
|
||||
nodeResponse.end();
|
||||
return;
|
||||
}
|
||||
const reader = webResponse.body.getReader();
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
nodeResponse.write(Buffer.from(value));
|
||||
}
|
||||
nodeResponse.end();
|
||||
} catch (error) {
|
||||
nodeResponse.destroy(error instanceof Error ? error : undefined);
|
||||
}
|
||||
}
|
||||
|
||||
const server = http.createServer(async (request, response) => {
|
||||
const url = new URL(request.url || "/", `http://${request.headers.host || `${host}:${port}`}`);
|
||||
|
||||
if (request.method === "OPTIONS") {
|
||||
response.writeHead(204, corsHeaders());
|
||||
response.end();
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.method === "GET" && url.pathname === "/api/health") {
|
||||
sendJson(response, 200, { ok: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.method === "GET" && url.pathname === "/api/config") {
|
||||
try {
|
||||
sendJson(response, 200, publicConfig());
|
||||
} catch (error) {
|
||||
sendJson(response, 500, {
|
||||
error: error instanceof Error ? error.message : "Failed to load agent config.",
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.method === "POST" && url.pathname === "/api/chat") {
|
||||
try {
|
||||
const body = await readJson(request);
|
||||
const record = body && typeof body === "object" ? body as Record<string, unknown> : {};
|
||||
const streamResponse = await streamAgentText({
|
||||
messages: Array.isArray(record.messages) ? record.messages : [],
|
||||
attachments: Array.isArray(record.attachments) ? record.attachments : [],
|
||||
viewerContext: Array.isArray(record.viewerContext) ? record.viewerContext : [],
|
||||
provider: typeof record.provider === "string" ? record.provider : undefined,
|
||||
model: typeof record.model === "string" ? record.model : undefined,
|
||||
selectedTaskId: typeof record.selectedTaskId === "string" ? record.selectedTaskId : undefined,
|
||||
});
|
||||
await writeWebResponse(response, streamResponse);
|
||||
} catch (error) {
|
||||
sendJson(response, 500, {
|
||||
error: error instanceof Error ? error.message : "Failed to stream chat response.",
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
sendJson(response, 404, { error: "not_found" });
|
||||
});
|
||||
|
||||
server.listen(port, host, () => {
|
||||
console.log(`agent server: http://${host}:${port}`);
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2022", "DOM"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user