Migrate AI runtime to SDK and hardcode Zhipu coding endpoint
This commit is contained in:
21
.env.example
21
.env.example
@@ -12,21 +12,12 @@ BETTER_AUTH_SECRET=replace-with-a-long-random-secret
|
|||||||
BETTER_AUTH_BASE_URL=https://fiscal.b11studio.xyz
|
BETTER_AUTH_BASE_URL=https://fiscal.b11studio.xyz
|
||||||
BETTER_AUTH_TRUSTED_ORIGINS=https://fiscal.b11studio.xyz
|
BETTER_AUTH_TRUSTED_ORIGINS=https://fiscal.b11studio.xyz
|
||||||
|
|
||||||
# OpenClaw / ZeroClaw (OpenAI-compatible)
|
# AI SDK (Vercel) + Zhipu provider
|
||||||
# Leave empty to use internal `openclaw` Compose service (`http://openclaw:4000`).
|
# Legacy OPENCLAW_* variables are removed and no longer read by the app.
|
||||||
# Set this only when targeting an external OpenClaw endpoint.
|
# Coding endpoint is hardcoded in runtime: https://api.z.ai/api/coding/paas/v4
|
||||||
OPENCLAW_BASE_URL=
|
ZHIPU_API_KEY=
|
||||||
OPENCLAW_API_KEY=
|
ZHIPU_MODEL=glm-4.7-flashx
|
||||||
OPENCLAW_MODEL=zeroclaw
|
AI_TEMPERATURE=0.2
|
||||||
OPENCLAW_AUTH_MODE=none
|
|
||||||
OPENCLAW_PORT=4000
|
|
||||||
|
|
||||||
# OpenClaw container source for Docker Compose
|
|
||||||
OPENCLAW_IMAGE=coolify-zeroclaw:local
|
|
||||||
# If this repo is private, include credentials in the URL:
|
|
||||||
# OPENCLAW_BUILD_CONTEXT=https://<username>:<token>@gitea-hs848cs8kgs840o8c8s8cwkk.b11studio.xyz/Francy51/coolify_ZeroClaw.git
|
|
||||||
OPENCLAW_BUILD_CONTEXT=https://gitea-hs848cs8kgs840o8c8s8cwkk.b11studio.xyz/Francy51/coolify_ZeroClaw.git
|
|
||||||
OPENCLAW_DOCKERFILE=Dockerfile
|
|
||||||
|
|
||||||
# SEC API etiquette
|
# SEC API etiquette
|
||||||
SEC_USER_AGENT=Fiscal Clone <support@fiscal.local>
|
SEC_USER_AGENT=Fiscal Clone <support@fiscal.local>
|
||||||
|
|||||||
57
README.md
57
README.md
@@ -1,6 +1,6 @@
|
|||||||
# Fiscal Clone 3.0
|
# Fiscal Clone 3.0
|
||||||
|
|
||||||
Turbopack-first rebuild of a fiscal.ai-style terminal with OpenClaw integration.
|
Turbopack-first rebuild of a fiscal.ai-style terminal with Vercel AI SDK integration.
|
||||||
|
|
||||||
## Stack
|
## Stack
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ Turbopack-first rebuild of a fiscal.ai-style terminal with OpenClaw integration.
|
|||||||
- Eden Treaty for type-safe frontend API calls
|
- Eden Treaty for type-safe frontend API calls
|
||||||
- Workflow DevKit Local World for background task execution
|
- Workflow DevKit Local World for background task execution
|
||||||
- SQLite-backed domain storage (watchlist, holdings, filings, tasks, insights)
|
- SQLite-backed domain storage (watchlist, holdings, filings, tasks, insights)
|
||||||
- OpenClaw/ZeroClaw analysis via OpenAI-compatible chat endpoint
|
- Vercel AI SDK (`ai`) + Zhipu community provider (`zhipu-ai-provider`) for analysis tasks (hardcoded to `https://api.z.ai/api/coding/paas/v4`)
|
||||||
|
|
||||||
## Run locally
|
## Run locally
|
||||||
|
|
||||||
@@ -44,10 +44,8 @@ cp .env.example .env
|
|||||||
docker compose up --build -d
|
docker compose up --build -d
|
||||||
```
|
```
|
||||||
|
|
||||||
For local Docker, host port mapping comes from `docker-compose.override.yml` (default `http://localhost:3000` via `APP_PORT`, and `http://localhost:4000` for OpenClaw via `OPENCLAW_PORT`).
|
For local Docker, host port mapping comes from `docker-compose.override.yml` (default `http://localhost:3000` via `APP_PORT`).
|
||||||
OpenClaw is included as a Compose service (`openclaw`) and is built by default from `OPENCLAW_BUILD_CONTEXT` (set to `Francy51/coolify_ZeroClaw` in `.env.example`).
|
The app calls Zhipu directly via AI SDK and always targets the Coding API endpoint (`https://api.z.ai/api/coding/paas/v4`), so no extra AI gateway container is required.
|
||||||
If that Gitea repo is private, set `OPENCLAW_BUILD_CONTEXT` with embedded credentials (`https://<username>:<token>@.../coolify_ZeroClaw.git`) or point `OPENCLAW_IMAGE` to a prebuilt image you can pull.
|
|
||||||
The app container defaults to `OPENCLAW_BASE_URL=http://openclaw:4000` unless you explicitly set a different `OPENCLAW_BASE_URL`.
|
|
||||||
On container startup, the app applies Drizzle migrations automatically before launching Next.js.
|
On container startup, the app applies Drizzle migrations automatically before launching Next.js.
|
||||||
The app stores SQLite data in Docker volume `fiscal_sqlite_data` (mounted to `/app/data`) and workflow local data in `fiscal_workflow_data` (mounted to `/app/.workflow-data`).
|
The app stores SQLite data in Docker volume `fiscal_sqlite_data` (mounted to `/app/data`) and workflow local data in `fiscal_workflow_data` (mounted to `/app/.workflow-data`).
|
||||||
|
|
||||||
@@ -88,19 +86,10 @@ BETTER_AUTH_SECRET=replace-with-a-long-random-secret
|
|||||||
BETTER_AUTH_BASE_URL=https://fiscal.b11studio.xyz
|
BETTER_AUTH_BASE_URL=https://fiscal.b11studio.xyz
|
||||||
BETTER_AUTH_TRUSTED_ORIGINS=https://fiscal.b11studio.xyz
|
BETTER_AUTH_TRUSTED_ORIGINS=https://fiscal.b11studio.xyz
|
||||||
|
|
||||||
OPENCLAW_BASE_URL=
|
ZHIPU_API_KEY=
|
||||||
OPENCLAW_API_KEY=
|
ZHIPU_MODEL=glm-4.7-flashx
|
||||||
OPENCLAW_MODEL=zeroclaw
|
# optional generation tuning
|
||||||
OPENCLAW_AUTH_MODE=none
|
AI_TEMPERATURE=0.2
|
||||||
OPENCLAW_PORT=4000
|
|
||||||
OPENCLAW_IMAGE=coolify-zeroclaw:local
|
|
||||||
OPENCLAW_BUILD_CONTEXT=https://gitea-hs848cs8kgs840o8c8s8cwkk.b11studio.xyz/Francy51/coolify_ZeroClaw.git
|
|
||||||
OPENCLAW_DOCKERFILE=Dockerfile
|
|
||||||
# for OPENCLAW_AUTH_MODE=basic
|
|
||||||
# OPENCLAW_BASIC_AUTH_USERNAME=your_nginx_user
|
|
||||||
# OPENCLAW_BASIC_AUTH_PASSWORD=your_nginx_password
|
|
||||||
# optional: forward API key in a custom header when using basic/none auth
|
|
||||||
# OPENCLAW_API_KEY_HEADER=X-OpenClaw-API-Key
|
|
||||||
SEC_USER_AGENT=Fiscal Clone <support@fiscal.local>
|
SEC_USER_AGENT=Fiscal Clone <support@fiscal.local>
|
||||||
|
|
||||||
WORKFLOW_TARGET_WORLD=local
|
WORKFLOW_TARGET_WORLD=local
|
||||||
@@ -108,19 +97,27 @@ WORKFLOW_LOCAL_DATA_DIR=.workflow-data
|
|||||||
WORKFLOW_LOCAL_QUEUE_CONCURRENCY=100
|
WORKFLOW_LOCAL_QUEUE_CONCURRENCY=100
|
||||||
```
|
```
|
||||||
|
|
||||||
If OpenClaw is unset or invalidly configured, the app uses local fallback analysis so task workflows still run.
|
If `ZHIPU_API_KEY` is unset, the app uses local fallback analysis so task workflows still run.
|
||||||
|
`ZHIPU_BASE_URL` is deprecated and ignored; runtime always uses `https://api.z.ai/api/coding/paas/v4`.
|
||||||
|
|
||||||
For OpenClaw behind Nginx Basic Auth, use:
|
## Migration from OPENCLAW_* to ZHIPU_*
|
||||||
|
|
||||||
```env
|
This repository now uses direct provider calls through AI SDK. The legacy gateway container path is removed.
|
||||||
OPENCLAW_BASE_URL=https://your-nginx-host
|
The AI runtime endpoint is fixed to `https://api.z.ai/api/coding/paas/v4`.
|
||||||
OPENCLAW_AUTH_MODE=basic
|
|
||||||
OPENCLAW_BASIC_AUTH_USERNAME=your_nginx_user
|
| Legacy variable | Replacement |
|
||||||
OPENCLAW_BASIC_AUTH_PASSWORD=your_nginx_password
|
| --- | --- |
|
||||||
# optional if upstream still needs an API key in a non-Authorization header
|
| `OPENCLAW_API_KEY` | `ZHIPU_API_KEY` |
|
||||||
OPENCLAW_API_KEY=your_key
|
| `OPENCLAW_MODEL` | `ZHIPU_MODEL` |
|
||||||
OPENCLAW_API_KEY_HEADER=X-OpenClaw-API-Key
|
| `OPENCLAW_BASE_URL` | Removed (runtime endpoint is hardcoded to `https://api.z.ai/api/coding/paas/v4`) |
|
||||||
```
|
| `OPENCLAW_AUTH_MODE` | Removed (not needed with direct provider calls) |
|
||||||
|
| `OPENCLAW_BASIC_AUTH_USERNAME` | Removed |
|
||||||
|
| `OPENCLAW_BASIC_AUTH_PASSWORD` | Removed |
|
||||||
|
| `OPENCLAW_API_KEY_HEADER` | Removed |
|
||||||
|
| `OPENCLAW_PORT` | Removed (no gateway service) |
|
||||||
|
| `OPENCLAW_IMAGE` | Removed |
|
||||||
|
| `OPENCLAW_BUILD_CONTEXT` | Removed |
|
||||||
|
| `OPENCLAW_DOCKERFILE` | Removed |
|
||||||
|
|
||||||
## API surface
|
## API surface
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { Metadata } from 'next';
|
|||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'Fiscal Clone',
|
title: 'Fiscal Clone',
|
||||||
description: 'Futuristic fiscal intelligence terminal with durable tasks and OpenClaw integration.'
|
description: 'Futuristic fiscal intelligence terminal with durable tasks and AI SDK integration.'
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
|||||||
@@ -184,7 +184,7 @@ export default function CommandCenterPage() {
|
|||||||
)}
|
)}
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
<Panel title="AI Brief" subtitle="Latest portfolio insight from OpenClaw/ZeroClaw" className="xl:col-span-2">
|
<Panel title="AI Brief" subtitle="Latest portfolio insight from AI SDK (Zhipu)" className="xl:col-span-2">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<p className="text-sm text-[color:var(--terminal-muted)]">Loading intelligence output...</p>
|
<p className="text-sm text-[color:var(--terminal-muted)]">Loading intelligence output...</p>
|
||||||
) : state.latestInsight ? (
|
) : state.latestInsight ? (
|
||||||
|
|||||||
26
bun.lock
26
bun.lock
@@ -8,6 +8,7 @@
|
|||||||
"@elysiajs/eden": "^1.4.8",
|
"@elysiajs/eden": "^1.4.8",
|
||||||
"@libsql/client": "^0.17.0",
|
"@libsql/client": "^0.17.0",
|
||||||
"@tailwindcss/postcss": "^4.2.1",
|
"@tailwindcss/postcss": "^4.2.1",
|
||||||
|
"ai": "^6.0.104",
|
||||||
"better-auth": "^1.4.19",
|
"better-auth": "^1.4.19",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
@@ -19,6 +20,7 @@
|
|||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4",
|
||||||
"recharts": "^3.7.0",
|
"recharts": "^3.7.0",
|
||||||
"workflow": "^4.1.0-beta.60",
|
"workflow": "^4.1.0-beta.60",
|
||||||
|
"zhipu-ai-provider": "^0.2.2",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^25.3.0",
|
"@types/node": "^25.3.0",
|
||||||
@@ -34,6 +36,12 @@
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages": {
|
"packages": {
|
||||||
|
"@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.58", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.15", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-2e1hBCKsd+7m0hELwrakR1QDfZfFhz9PF2d4qb8TxQueEyApo7ydlEWRpXeKC+KdA2FRV21dMb1G6FxdeNDa2w=="],
|
||||||
|
|
||||||
|
"@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="],
|
||||||
|
|
||||||
|
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.15", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8XiKWbemmCbvNN0CLR9u3PQiet4gtEVIrX4zzLxnCj06AwsEDJwJVBbKrEI4t6qE8XRSIvU2irka0dcpziKW6w=="],
|
||||||
|
|
||||||
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
|
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
|
||||||
|
|
||||||
"@aws-crypto/sha256-browser": ["@aws-crypto/sha256-browser@5.2.0", "", { "dependencies": { "@aws-crypto/sha256-js": "^5.2.0", "@aws-crypto/supports-web-crypto": "^5.2.0", "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-locate-window": "^3.0.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw=="],
|
"@aws-crypto/sha256-browser": ["@aws-crypto/sha256-browser@5.2.0", "", { "dependencies": { "@aws-crypto/sha256-js": "^5.2.0", "@aws-crypto/supports-web-crypto": "^5.2.0", "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-locate-window": "^3.0.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw=="],
|
||||||
@@ -348,6 +356,8 @@
|
|||||||
|
|
||||||
"@oclif/plugin-help": ["@oclif/plugin-help@6.2.31", "", { "dependencies": { "@oclif/core": "^4" } }, "sha512-o4xR98DEFf+VqY+M9B3ZooTm2T/mlGvyBHwHcnsPJCEnvzHqEA9xUlCUK4jm7FBXHhkppziMgCC2snsueLoIpQ=="],
|
"@oclif/plugin-help": ["@oclif/plugin-help@6.2.31", "", { "dependencies": { "@oclif/core": "^4" } }, "sha512-o4xR98DEFf+VqY+M9B3ZooTm2T/mlGvyBHwHcnsPJCEnvzHqEA9xUlCUK4jm7FBXHhkppziMgCC2snsueLoIpQ=="],
|
||||||
|
|
||||||
|
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
||||||
|
|
||||||
"@react-router/node": ["@react-router/node@7.13.0", "", { "dependencies": { "@mjackson/node-fetch-server": "^0.2.0" }, "peerDependencies": { "react-router": "7.13.0", "typescript": "^5.1.0" }, "optionalPeers": ["typescript"] }, "sha512-Mhr3fAou19oc/S93tKMIBHwCPfqLpWyWM/m0NWd3pJh/wZin8/9KhAdjwxhYbXw1TrTBZBLDENa35uZ+Y7oh3A=="],
|
"@react-router/node": ["@react-router/node@7.13.0", "", { "dependencies": { "@mjackson/node-fetch-server": "^0.2.0" }, "peerDependencies": { "react-router": "7.13.0", "typescript": "^5.1.0" }, "optionalPeers": ["typescript"] }, "sha512-Mhr3fAou19oc/S93tKMIBHwCPfqLpWyWM/m0NWd3pJh/wZin8/9KhAdjwxhYbXw1TrTBZBLDENa35uZ+Y7oh3A=="],
|
||||||
|
|
||||||
"@reduxjs/toolkit": ["@reduxjs/toolkit@2.11.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^11.0.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" } }, "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ=="],
|
"@reduxjs/toolkit": ["@reduxjs/toolkit@2.11.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^11.0.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" } }, "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ=="],
|
||||||
@@ -546,7 +556,7 @@
|
|||||||
|
|
||||||
"@vercel/functions": ["@vercel/functions@3.4.3", "", { "dependencies": { "@vercel/oidc": "3.2.0" }, "peerDependencies": { "@aws-sdk/credential-provider-web-identity": "*" }, "optionalPeers": ["@aws-sdk/credential-provider-web-identity"] }, "sha512-kA14KIUVgAY6VXbhZ5jjY+s0883cV3cZqIU3WhrSRxuJ9KvxatMjtmzl0K23HK59oOUjYl7HaE/eYMmhmqpZzw=="],
|
"@vercel/functions": ["@vercel/functions@3.4.3", "", { "dependencies": { "@vercel/oidc": "3.2.0" }, "peerDependencies": { "@aws-sdk/credential-provider-web-identity": "*" }, "optionalPeers": ["@aws-sdk/credential-provider-web-identity"] }, "sha512-kA14KIUVgAY6VXbhZ5jjY+s0883cV3cZqIU3WhrSRxuJ9KvxatMjtmzl0K23HK59oOUjYl7HaE/eYMmhmqpZzw=="],
|
||||||
|
|
||||||
"@vercel/oidc": ["@vercel/oidc@3.0.5", "", {}, "sha512-fnYhv671l+eTTp48gB4zEsTW/YtRgRPnkI2nT7x6qw5rkI1Lq2hTmQIpHPgyThI0znLK+vX2n9XxKdXZ7BUbbw=="],
|
"@vercel/oidc": ["@vercel/oidc@3.1.0", "", {}, "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w=="],
|
||||||
|
|
||||||
"@vercel/queue": ["@vercel/queue@0.0.0-alpha.38", "", { "dependencies": { "@vercel/oidc": "^3.0.5", "mixpart": "0.0.5-alpha.1" } }, "sha512-gSYpZrYy1LpzfXqDMUZX7xEIQxyhelvMDgthxijs495kHIxWC65S0C3vaAw5+3c1ujbPeJgLz8fn3SDTJspssw=="],
|
"@vercel/queue": ["@vercel/queue@0.0.0-alpha.38", "", { "dependencies": { "@vercel/oidc": "^3.0.5", "mixpart": "0.0.5-alpha.1" } }, "sha512-gSYpZrYy1LpzfXqDMUZX7xEIQxyhelvMDgthxijs495kHIxWC65S0C3vaAw5+3c1ujbPeJgLz8fn3SDTJspssw=="],
|
||||||
|
|
||||||
@@ -614,6 +624,8 @@
|
|||||||
|
|
||||||
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
|
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
|
||||||
|
|
||||||
|
"ai": ["ai@6.0.104", "", { "dependencies": { "@ai-sdk/gateway": "3.0.58", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.15", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-boYGxbtdsa1YX3uuN7BV0FvAL3sGq7p/RLAMonK94jyt5C7sKj6jfib3/wD12koqX53htLTI/l4tX0HqNFRMZQ=="],
|
||||||
|
|
||||||
"ansi-align": ["ansi-align@3.0.1", "", { "dependencies": { "string-width": "^4.1.0" } }, "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w=="],
|
"ansi-align": ["ansi-align@3.0.1", "", { "dependencies": { "string-width": "^4.1.0" } }, "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w=="],
|
||||||
|
|
||||||
"ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="],
|
"ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="],
|
||||||
@@ -864,6 +876,8 @@
|
|||||||
|
|
||||||
"events-universal": ["events-universal@1.0.1", "", { "dependencies": { "bare-events": "^2.7.0" } }, "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw=="],
|
"events-universal": ["events-universal@1.0.1", "", { "dependencies": { "bare-events": "^2.7.0" } }, "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw=="],
|
||||||
|
|
||||||
|
"eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="],
|
||||||
|
|
||||||
"exact-mirror": ["exact-mirror@0.2.7", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-+MeEmDcLA4o/vjK2zujgk+1VTxPR4hdp23qLqkWfStbECtAq9gmsvQa3LW6z/0GXZyHJobrCnmy1cdeE7BjsYg=="],
|
"exact-mirror": ["exact-mirror@0.2.7", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-+MeEmDcLA4o/vjK2zujgk+1VTxPR4hdp23qLqkWfStbECtAq9gmsvQa3LW6z/0GXZyHJobrCnmy1cdeE7BjsYg=="],
|
||||||
|
|
||||||
"execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="],
|
"execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="],
|
||||||
@@ -1030,6 +1044,8 @@
|
|||||||
|
|
||||||
"json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="],
|
"json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="],
|
||||||
|
|
||||||
|
"json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],
|
||||||
|
|
||||||
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
||||||
|
|
||||||
"jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="],
|
"jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="],
|
||||||
@@ -1468,6 +1484,8 @@
|
|||||||
|
|
||||||
"yocto-queue": ["yocto-queue@1.2.2", "", {}, "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ=="],
|
"yocto-queue": ["yocto-queue@1.2.2", "", {}, "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ=="],
|
||||||
|
|
||||||
|
"zhipu-ai-provider": ["zhipu-ai-provider@0.2.2", "", { "dependencies": { "@ai-sdk/provider": "^2.0.0", "@ai-sdk/provider-utils": "^3.0.0" } }, "sha512-UjX1ho4DI9ICUv/mrpAnzmrRe5/LXrGkS5hF6h4WDY2aup5GketWWopFzWYCqsbArXAM5wbzzdH9QzZusgGiBg=="],
|
||||||
|
|
||||||
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
|
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
|
||||||
|
|
||||||
"@aws-crypto/sha256-browser/@aws-sdk/types": ["@aws-sdk/types@3.973.3", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-tma6D8/xHZHJEUqmr6ksZjZ0onyIUqKDQLyp50ttZJmS0IwFYzxBgp5CxFvpYAnah52V3UtgrqGA6E83gtT7NQ=="],
|
"@aws-crypto/sha256-browser/@aws-sdk/types": ["@aws-sdk/types@3.973.3", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-tma6D8/xHZHJEUqmr6ksZjZ0onyIUqKDQLyp50ttZJmS0IwFYzxBgp5CxFvpYAnah52V3UtgrqGA6E83gtT7NQ=="],
|
||||||
@@ -1680,6 +1698,8 @@
|
|||||||
|
|
||||||
"@workflow/world-local/zod": ["zod@4.1.11", "", {}, "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg=="],
|
"@workflow/world-local/zod": ["zod@4.1.11", "", {}, "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg=="],
|
||||||
|
|
||||||
|
"@workflow/world-vercel/@vercel/oidc": ["@vercel/oidc@3.0.5", "", {}, "sha512-fnYhv671l+eTTp48gB4zEsTW/YtRgRPnkI2nT7x6qw5rkI1Lq2hTmQIpHPgyThI0znLK+vX2n9XxKdXZ7BUbbw=="],
|
||||||
|
|
||||||
"@workflow/world-vercel/zod": ["zod@4.1.11", "", {}, "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg=="],
|
"@workflow/world-vercel/zod": ["zod@4.1.11", "", {}, "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg=="],
|
||||||
|
|
||||||
"@xhmikosr/archive-type/file-type": ["file-type@20.5.0", "", { "dependencies": { "@tokenizer/inflate": "^0.2.6", "strtok3": "^10.2.0", "token-types": "^6.0.0", "uint8array-extras": "^1.4.0" } }, "sha512-BfHZtG/l9iMm4Ecianu7P8HRD2tBHLtjXinm4X62XBOYzi7CYA7jyqfJzOvXHqzVrVPYqBo2/GvbARMaaJkKVg=="],
|
"@xhmikosr/archive-type/file-type": ["file-type@20.5.0", "", { "dependencies": { "@tokenizer/inflate": "^0.2.6", "strtok3": "^10.2.0", "token-types": "^6.0.0", "uint8array-extras": "^1.4.0" } }, "sha512-BfHZtG/l9iMm4Ecianu7P8HRD2tBHLtjXinm4X62XBOYzi7CYA7jyqfJzOvXHqzVrVPYqBo2/GvbARMaaJkKVg=="],
|
||||||
@@ -1772,6 +1792,10 @@
|
|||||||
|
|
||||||
"wsl-utils/is-wsl": ["is-wsl@3.1.1", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="],
|
"wsl-utils/is-wsl": ["is-wsl@3.1.1", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="],
|
||||||
|
|
||||||
|
"zhipu-ai-provider/@ai-sdk/provider": ["@ai-sdk/provider@2.0.1", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-KCUwswvsC5VsW2PWFqF8eJgSCu5Ysj7m1TxiHTVA6g7k360bk0RNQENT8KTMAYEs+8fWPD3Uu4dEmzGHc+jGng=="],
|
||||||
|
|
||||||
|
"zhipu-ai-provider/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.21", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-veuMwTLxsgh31Jjn0SnBABnM1f7ebHhRWcV2ZuY3hP3iJDCZ8VXBaYqcHXoOQDqUXTCas08sKQcHyWK+zl882Q=="],
|
||||||
|
|
||||||
"@aws-crypto/sha256-browser/@aws-sdk/types/@smithy/types": ["@smithy/types@4.13.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-COuLsZILbbQsdrwKQpkkpyep7lCsByxwj7m0Mg5v66/ZTyenlfBc40/QFQ5chO0YN/PNEH1Bi3fGtfXPnYNeDw=="],
|
"@aws-crypto/sha256-browser/@aws-sdk/types/@smithy/types": ["@smithy/types@4.13.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-COuLsZILbbQsdrwKQpkkpyep7lCsByxwj7m0Mg5v66/ZTyenlfBc40/QFQ5chO0YN/PNEH1Bi3fGtfXPnYNeDw=="],
|
||||||
|
|
||||||
"@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
|
"@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export function AuthShell({ title, subtitle, children, footer }: AuthShellProps)
|
|||||||
<p className="terminal-caption text-xs uppercase tracking-[0.3em] text-[color:var(--terminal-muted)]">Fiscal Clone</p>
|
<p className="terminal-caption text-xs uppercase tracking-[0.3em] text-[color:var(--terminal-muted)]">Fiscal Clone</p>
|
||||||
<h1 className="mt-3 text-3xl font-semibold text-[color:var(--terminal-bright)]">Autonomous Analyst Desk</h1>
|
<h1 className="mt-3 text-3xl font-semibold text-[color:var(--terminal-bright)]">Autonomous Analyst Desk</h1>
|
||||||
<p className="mt-3 text-sm leading-6 text-[color:var(--terminal-muted)]">
|
<p className="mt-3 text-sm leading-6 text-[color:var(--terminal-muted)]">
|
||||||
Secure entry into filings intelligence, portfolio diagnostics, and async AI workflows connected to OpenClaw/ZeroClaw.
|
Secure entry into filings intelligence, portfolio diagnostics, and async AI workflows powered by AI SDK.
|
||||||
</p>
|
</p>
|
||||||
<Link
|
<Link
|
||||||
href="https://www.sec.gov/"
|
href="https://www.sec.gov/"
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ export function AppShell({ title, subtitle, actions, children }: AppShellProps)
|
|||||||
<p className="mt-1 truncate text-sm text-[color:var(--terminal-bright)]">{displayName}</p>
|
<p className="mt-1 truncate text-sm text-[color:var(--terminal-bright)]">{displayName}</p>
|
||||||
{role ? <p className="mt-1 text-xs uppercase tracking-[0.16em] text-[color:var(--terminal-muted)]">Role: {role}</p> : null}
|
{role ? <p className="mt-1 text-xs uppercase tracking-[0.16em] text-[color:var(--terminal-muted)]">Role: {role}</p> : null}
|
||||||
<p className="mt-2 text-xs text-[color:var(--terminal-muted)]">
|
<p className="mt-2 text-xs text-[color:var(--terminal-muted)]">
|
||||||
OpenClaw and market data are driven by environment configuration and live API tasks.
|
AI and market data are driven by environment configuration and live API tasks.
|
||||||
</p>
|
</p>
|
||||||
<Button className="mt-3 w-full" variant="ghost" onClick={() => void signOut()} disabled={isSigningOut}>
|
<Button className="mt-3 w-full" variant="ghost" onClick={() => void signOut()} disabled={isSigningOut}>
|
||||||
<LogOut className="size-4" />
|
<LogOut className="size-4" />
|
||||||
|
|||||||
@@ -5,6 +5,3 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
BETTER_AUTH_BASE_URL: ${BETTER_AUTH_BASE_URL:-http://localhost:3000}
|
BETTER_AUTH_BASE_URL: ${BETTER_AUTH_BASE_URL:-http://localhost:3000}
|
||||||
BETTER_AUTH_TRUSTED_ORIGINS: ${BETTER_AUTH_TRUSTED_ORIGINS:-http://localhost:3000}
|
BETTER_AUTH_TRUSTED_ORIGINS: ${BETTER_AUTH_TRUSTED_ORIGINS:-http://localhost:3000}
|
||||||
openclaw:
|
|
||||||
ports:
|
|
||||||
- '${OPENCLAW_PORT:-4000}:4000'
|
|
||||||
|
|||||||
@@ -18,13 +18,9 @@ services:
|
|||||||
BETTER_AUTH_BASE_URL: ${BETTER_AUTH_BASE_URL:-https://fiscal.b11studio.xyz}
|
BETTER_AUTH_BASE_URL: ${BETTER_AUTH_BASE_URL:-https://fiscal.b11studio.xyz}
|
||||||
BETTER_AUTH_ADMIN_USER_IDS: ${BETTER_AUTH_ADMIN_USER_IDS:-}
|
BETTER_AUTH_ADMIN_USER_IDS: ${BETTER_AUTH_ADMIN_USER_IDS:-}
|
||||||
BETTER_AUTH_TRUSTED_ORIGINS: ${BETTER_AUTH_TRUSTED_ORIGINS:-https://fiscal.b11studio.xyz}
|
BETTER_AUTH_TRUSTED_ORIGINS: ${BETTER_AUTH_TRUSTED_ORIGINS:-https://fiscal.b11studio.xyz}
|
||||||
OPENCLAW_BASE_URL: ${OPENCLAW_BASE_URL:-http://openclaw:4000}
|
ZHIPU_API_KEY: ${ZHIPU_API_KEY:-}
|
||||||
OPENCLAW_API_KEY: ${OPENCLAW_API_KEY:-}
|
ZHIPU_MODEL: ${ZHIPU_MODEL:-glm-4.7-flashx}
|
||||||
OPENCLAW_MODEL: ${OPENCLAW_MODEL:-zeroclaw}
|
AI_TEMPERATURE: ${AI_TEMPERATURE:-0.2}
|
||||||
OPENCLAW_AUTH_MODE: ${OPENCLAW_AUTH_MODE:-none}
|
|
||||||
OPENCLAW_BASIC_AUTH_USERNAME: ${OPENCLAW_BASIC_AUTH_USERNAME:-}
|
|
||||||
OPENCLAW_BASIC_AUTH_PASSWORD: ${OPENCLAW_BASIC_AUTH_PASSWORD:-}
|
|
||||||
OPENCLAW_API_KEY_HEADER: ${OPENCLAW_API_KEY_HEADER:-}
|
|
||||||
SEC_USER_AGENT: ${SEC_USER_AGENT:-Fiscal Clone <support@fiscal.local>}
|
SEC_USER_AGENT: ${SEC_USER_AGENT:-Fiscal Clone <support@fiscal.local>}
|
||||||
WORKFLOW_TARGET_WORLD: local
|
WORKFLOW_TARGET_WORLD: local
|
||||||
WORKFLOW_LOCAL_DATA_DIR: ${WORKFLOW_LOCAL_DATA_DIR:-/app/.workflow-data}
|
WORKFLOW_LOCAL_DATA_DIR: ${WORKFLOW_LOCAL_DATA_DIR:-/app/.workflow-data}
|
||||||
@@ -32,9 +28,6 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- fiscal_sqlite_data:/app/data
|
- fiscal_sqlite_data:/app/data
|
||||||
- fiscal_workflow_data:/app/.workflow-data
|
- fiscal_workflow_data:/app/.workflow-data
|
||||||
depends_on:
|
|
||||||
openclaw:
|
|
||||||
condition: service_started
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "wget -q --spider http://127.0.0.1:3000/api/health || exit 1"]
|
test: ["CMD-SHELL", "wget -q --spider http://127.0.0.1:3000/api/health || exit 1"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
@@ -43,15 +36,6 @@ services:
|
|||||||
expose:
|
expose:
|
||||||
- "3000"
|
- "3000"
|
||||||
|
|
||||||
openclaw:
|
|
||||||
image: ${OPENCLAW_IMAGE:-coolify-zeroclaw:local}
|
|
||||||
build:
|
|
||||||
context: ${OPENCLAW_BUILD_CONTEXT:-https://gitea-hs848cs8kgs840o8c8s8cwkk.b11studio.xyz/Francy51/coolify_ZeroClaw.git}
|
|
||||||
dockerfile: ${OPENCLAW_DOCKERFILE:-Dockerfile}
|
|
||||||
restart: unless-stopped
|
|
||||||
expose:
|
|
||||||
- "4000"
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
fiscal_sqlite_data:
|
fiscal_sqlite_data:
|
||||||
fiscal_workflow_data:
|
fiscal_workflow_data:
|
||||||
|
|||||||
192
lib/server/ai.test.ts
Normal file
192
lib/server/ai.test.ts
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
import { beforeEach, describe, expect, it, mock } from 'bun:test';
|
||||||
|
import {
|
||||||
|
__resetAiWarningsForTests,
|
||||||
|
getAiConfig,
|
||||||
|
runAiAnalysis
|
||||||
|
} from './ai';
|
||||||
|
|
||||||
|
type EnvSource = Record<string, string | undefined>;
|
||||||
|
const CODING_API_BASE_URL = 'https://api.z.ai/api/coding/paas/v4';
|
||||||
|
|
||||||
|
describe('ai config and runtime', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
__resetAiWarningsForTests();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses coding endpoint defaults when optional env values are missing', () => {
|
||||||
|
const config = getAiConfig({
|
||||||
|
env: {
|
||||||
|
ZHIPU_API_KEY: 'key'
|
||||||
|
},
|
||||||
|
warn: () => {}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(config.apiKey).toBe('key');
|
||||||
|
expect(config.baseUrl).toBe(CODING_API_BASE_URL);
|
||||||
|
expect(config.model).toBe('glm-4.7-flashx');
|
||||||
|
expect(config.temperature).toBe(0.2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores ZHIPU_BASE_URL and keeps the hardcoded coding endpoint', () => {
|
||||||
|
const config = getAiConfig({
|
||||||
|
env: {
|
||||||
|
ZHIPU_API_KEY: 'key',
|
||||||
|
ZHIPU_BASE_URL: 'https://api.z.ai/api/paas/v4'
|
||||||
|
},
|
||||||
|
warn: () => {}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(config.baseUrl).toBe(CODING_API_BASE_URL);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clamps temperature into [0, 2]', () => {
|
||||||
|
const negative = getAiConfig({
|
||||||
|
env: {
|
||||||
|
ZHIPU_API_KEY: 'key',
|
||||||
|
AI_TEMPERATURE: '-2'
|
||||||
|
},
|
||||||
|
warn: () => {}
|
||||||
|
});
|
||||||
|
expect(negative.temperature).toBe(0);
|
||||||
|
|
||||||
|
const high = getAiConfig({
|
||||||
|
env: {
|
||||||
|
ZHIPU_API_KEY: 'key',
|
||||||
|
AI_TEMPERATURE: '9'
|
||||||
|
},
|
||||||
|
warn: () => {}
|
||||||
|
});
|
||||||
|
expect(high.temperature).toBe(2);
|
||||||
|
|
||||||
|
const invalid = getAiConfig({
|
||||||
|
env: {
|
||||||
|
ZHIPU_API_KEY: 'key',
|
||||||
|
AI_TEMPERATURE: 'not-a-number'
|
||||||
|
},
|
||||||
|
warn: () => {}
|
||||||
|
});
|
||||||
|
expect(invalid.temperature).toBe(0.2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns fallback output when ZHIPU_API_KEY is missing', async () => {
|
||||||
|
const generate = mock(async () => ({ text: 'should-not-be-used' }));
|
||||||
|
|
||||||
|
const result = await runAiAnalysis(
|
||||||
|
'Prompt line one\nPrompt line two',
|
||||||
|
'System prompt',
|
||||||
|
{
|
||||||
|
env: {},
|
||||||
|
warn: () => {},
|
||||||
|
generate
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.provider).toBe('local-fallback');
|
||||||
|
expect(result.model).toBe('glm-4.7-flashx');
|
||||||
|
expect(result.text).toContain('AI SDK fallback mode is active');
|
||||||
|
expect(generate).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('warns once when deprecated OPENCLAW_* env vars are present', () => {
|
||||||
|
const warn = mock((_message: string) => {});
|
||||||
|
|
||||||
|
const env: EnvSource = {
|
||||||
|
OPENCLAW_API_KEY: 'legacy-key',
|
||||||
|
OPENCLAW_BASE_URL: 'http://legacy.local',
|
||||||
|
ZHIPU_API_KEY: 'new-key'
|
||||||
|
};
|
||||||
|
|
||||||
|
getAiConfig({ env, warn });
|
||||||
|
getAiConfig({ env, warn });
|
||||||
|
|
||||||
|
expect(warn).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('warns once when ZHIPU_BASE_URL is set because coding endpoint is hardcoded', () => {
|
||||||
|
const warn = mock((_message: string) => {});
|
||||||
|
|
||||||
|
const env: EnvSource = {
|
||||||
|
ZHIPU_API_KEY: 'new-key',
|
||||||
|
ZHIPU_BASE_URL: 'https://api.z.ai/api/paas/v4'
|
||||||
|
};
|
||||||
|
|
||||||
|
getAiConfig({ env, warn });
|
||||||
|
getAiConfig({ env, warn });
|
||||||
|
|
||||||
|
expect(warn).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not consume OPENCLAW_* values for live generation', async () => {
|
||||||
|
const generate = mock(async () => ({ text: 'should-not-be-used' }));
|
||||||
|
const warn = mock((_message: string) => {});
|
||||||
|
|
||||||
|
const result = await runAiAnalysis('Legacy-only env prompt', undefined, {
|
||||||
|
env: {
|
||||||
|
OPENCLAW_API_KEY: 'legacy-key',
|
||||||
|
OPENCLAW_MODEL: 'legacy-model'
|
||||||
|
},
|
||||||
|
warn,
|
||||||
|
generate
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.provider).toBe('local-fallback');
|
||||||
|
expect(result.model).toBe('glm-4.7-flashx');
|
||||||
|
expect(generate).not.toHaveBeenCalled();
|
||||||
|
expect(warn).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses configured ZHIPU values and injected generator when API key exists', async () => {
|
||||||
|
const createModel = mock((config: {
|
||||||
|
apiKey?: string;
|
||||||
|
model: string;
|
||||||
|
baseUrl: string;
|
||||||
|
temperature: number;
|
||||||
|
}) => {
|
||||||
|
expect(config.apiKey).toBe('new-key');
|
||||||
|
expect(config.baseUrl).toBe(CODING_API_BASE_URL);
|
||||||
|
expect(config.model).toBe('glm-4-plus');
|
||||||
|
expect(config.temperature).toBe(0.4);
|
||||||
|
return { modelId: config.model };
|
||||||
|
});
|
||||||
|
const generate = mock(async (input: {
|
||||||
|
model: unknown;
|
||||||
|
system?: string;
|
||||||
|
prompt: string;
|
||||||
|
temperature: number;
|
||||||
|
}) => {
|
||||||
|
expect(input.system).toBe('Use concise style');
|
||||||
|
expect(input.prompt).toBe('Analyze this filing');
|
||||||
|
expect(input.temperature).toBe(0.4);
|
||||||
|
return { text: ' Generated insight ' };
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await runAiAnalysis('Analyze this filing', 'Use concise style', {
|
||||||
|
env: {
|
||||||
|
ZHIPU_API_KEY: 'new-key',
|
||||||
|
ZHIPU_MODEL: 'glm-4-plus',
|
||||||
|
ZHIPU_BASE_URL: 'https://api.z.ai/api/paas/v4',
|
||||||
|
AI_TEMPERATURE: '0.4'
|
||||||
|
},
|
||||||
|
warn: () => {},
|
||||||
|
createModel,
|
||||||
|
generate
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createModel).toHaveBeenCalledTimes(1);
|
||||||
|
expect(generate).toHaveBeenCalledTimes(1);
|
||||||
|
expect(result.provider).toBe('zhipu');
|
||||||
|
expect(result.model).toBe('glm-4-plus');
|
||||||
|
expect(result.text).toBe('Generated insight');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when AI generation returns an empty response', async () => {
|
||||||
|
await expect(
|
||||||
|
runAiAnalysis('Analyze this filing', undefined, {
|
||||||
|
env: { ZHIPU_API_KEY: 'new-key' },
|
||||||
|
warn: () => {},
|
||||||
|
createModel: () => ({}),
|
||||||
|
generate: async () => ({ text: ' ' })
|
||||||
|
})
|
||||||
|
).rejects.toThrow('AI SDK returned an empty response');
|
||||||
|
});
|
||||||
|
});
|
||||||
190
lib/server/ai.ts
Normal file
190
lib/server/ai.ts
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
import { generateText } from 'ai';
|
||||||
|
import { createZhipu } from 'zhipu-ai-provider';
|
||||||
|
|
||||||
|
type AiConfig = {
|
||||||
|
apiKey?: string;
|
||||||
|
baseUrl: string;
|
||||||
|
model: string;
|
||||||
|
temperature: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type EnvSource = Record<string, string | undefined>;
|
||||||
|
|
||||||
|
type GetAiConfigOptions = {
|
||||||
|
env?: EnvSource;
|
||||||
|
warn?: (message: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type AiGenerateInput = {
|
||||||
|
model: unknown;
|
||||||
|
system?: string;
|
||||||
|
prompt: string;
|
||||||
|
temperature: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type AiGenerateOutput = {
|
||||||
|
text: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type RunAiAnalysisOptions = GetAiConfigOptions & {
|
||||||
|
createModel?: (config: AiConfig) => unknown;
|
||||||
|
generate?: (input: AiGenerateInput) => Promise<AiGenerateOutput>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEPRECATED_LEGACY_GATEWAY_ENV_KEYS = [
|
||||||
|
'OPENCLAW_BASE_URL',
|
||||||
|
'OPENCLAW_API_KEY',
|
||||||
|
'OPENCLAW_MODEL',
|
||||||
|
'OPENCLAW_AUTH_MODE',
|
||||||
|
'OPENCLAW_BASIC_AUTH_USERNAME',
|
||||||
|
'OPENCLAW_BASIC_AUTH_PASSWORD',
|
||||||
|
'OPENCLAW_API_KEY_HEADER',
|
||||||
|
'OPENCLAW_PORT',
|
||||||
|
'OPENCLAW_IMAGE',
|
||||||
|
'OPENCLAW_BUILD_CONTEXT',
|
||||||
|
'OPENCLAW_DOCKERFILE'
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const CODING_API_BASE_URL = 'https://api.z.ai/api/coding/paas/v4';
|
||||||
|
|
||||||
|
let warnedDeprecatedGatewayEnv = false;
|
||||||
|
let warnedIgnoredZhipuBaseUrl = false;
|
||||||
|
|
||||||
|
function envValue(name: string, env: EnvSource = process.env) {
|
||||||
|
const value = env[name];
|
||||||
|
if (!value) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const trimmed = value.trim();
|
||||||
|
return trimmed.length > 0 ? trimmed : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseTemperature(value: string | undefined) {
|
||||||
|
const parsed = Number(value);
|
||||||
|
if (!Number.isFinite(parsed)) {
|
||||||
|
return 0.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.min(Math.max(parsed, 0), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function warnDeprecatedGatewayEnv(env: EnvSource, warn: (message: string) => void) {
|
||||||
|
if (warnedDeprecatedGatewayEnv) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const presentKeys = DEPRECATED_LEGACY_GATEWAY_ENV_KEYS.filter((key) => Boolean(envValue(key, env)));
|
||||||
|
if (presentKeys.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
warnedDeprecatedGatewayEnv = true;
|
||||||
|
warn(
|
||||||
|
`[AI SDK] Deprecated OPENCLAW_* variables are ignored after migration: ${presentKeys.join(', ')}. Use ZHIPU_API_KEY, ZHIPU_MODEL, and AI_TEMPERATURE.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function warnIgnoredZhipuBaseUrl(env: EnvSource, warn: (message: string) => void) {
|
||||||
|
if (warnedIgnoredZhipuBaseUrl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const configuredBaseUrl = envValue('ZHIPU_BASE_URL', env);
|
||||||
|
if (!configuredBaseUrl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
warnedIgnoredZhipuBaseUrl = true;
|
||||||
|
warn(
|
||||||
|
`[AI SDK] ZHIPU_BASE_URL is ignored. The Coding API endpoint is hardcoded to ${CODING_API_BASE_URL}.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fallbackResponse(prompt: string) {
|
||||||
|
const clipped = prompt.split('\n').slice(0, 6).join(' ').slice(0, 260);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'AI SDK fallback mode is active (Zhipu configuration is missing).',
|
||||||
|
'Thesis: Portfolio remains analyzable with local heuristics until live model access is configured.',
|
||||||
|
'Risk scan: Concentration and filing sentiment should be monitored after each sync cycle.',
|
||||||
|
`Context digest: ${clipped}`
|
||||||
|
].join('\n\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function defaultCreateModel(config: AiConfig) {
|
||||||
|
const zhipu = createZhipu({
|
||||||
|
apiKey: config.apiKey,
|
||||||
|
baseURL: config.baseUrl
|
||||||
|
});
|
||||||
|
|
||||||
|
return zhipu(config.model);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function defaultGenerate(input: AiGenerateInput): Promise<AiGenerateOutput> {
|
||||||
|
const result = await generateText({
|
||||||
|
model: input.model as never,
|
||||||
|
system: input.system,
|
||||||
|
prompt: input.prompt,
|
||||||
|
temperature: input.temperature
|
||||||
|
});
|
||||||
|
|
||||||
|
return { text: result.text };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAiConfig(options?: GetAiConfigOptions) {
|
||||||
|
const env = options?.env ?? process.env;
|
||||||
|
warnDeprecatedGatewayEnv(env, options?.warn ?? console.warn);
|
||||||
|
warnIgnoredZhipuBaseUrl(env, options?.warn ?? console.warn);
|
||||||
|
|
||||||
|
return {
|
||||||
|
apiKey: envValue('ZHIPU_API_KEY', env),
|
||||||
|
baseUrl: CODING_API_BASE_URL,
|
||||||
|
model: envValue('ZHIPU_MODEL', env) ?? 'glm-4.7-flashx',
|
||||||
|
temperature: parseTemperature(envValue('AI_TEMPERATURE', env))
|
||||||
|
} satisfies AiConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isAiConfigured(options?: GetAiConfigOptions) {
|
||||||
|
const config = getAiConfig(options);
|
||||||
|
return Boolean(config.apiKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runAiAnalysis(prompt: string, systemPrompt?: string, options?: RunAiAnalysisOptions) {
|
||||||
|
const config = getAiConfig(options);
|
||||||
|
|
||||||
|
if (!config.apiKey) {
|
||||||
|
return {
|
||||||
|
provider: 'local-fallback',
|
||||||
|
model: config.model,
|
||||||
|
text: fallbackResponse(prompt)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const createModel = options?.createModel ?? defaultCreateModel;
|
||||||
|
const generate = options?.generate ?? defaultGenerate;
|
||||||
|
const model = createModel(config);
|
||||||
|
|
||||||
|
const result = await generate({
|
||||||
|
model,
|
||||||
|
system: systemPrompt,
|
||||||
|
prompt,
|
||||||
|
temperature: config.temperature
|
||||||
|
});
|
||||||
|
|
||||||
|
const text = result.text.trim();
|
||||||
|
if (!text) {
|
||||||
|
throw new Error('AI SDK returned an empty response');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
provider: 'zhipu',
|
||||||
|
model: config.model,
|
||||||
|
text
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function __resetAiWarningsForTests() {
|
||||||
|
warnedDeprecatedGatewayEnv = false;
|
||||||
|
warnedIgnoredZhipuBaseUrl = false;
|
||||||
|
}
|
||||||
@@ -1,167 +0,0 @@
|
|||||||
type ChatCompletionResponse = {
|
|
||||||
choices?: Array<{
|
|
||||||
message?: {
|
|
||||||
content?: string;
|
|
||||||
};
|
|
||||||
}>;
|
|
||||||
};
|
|
||||||
|
|
||||||
type OpenClawAuthMode = 'bearer' | 'basic' | 'none';
|
|
||||||
|
|
||||||
type OpenClawConfig = {
|
|
||||||
baseUrl?: string;
|
|
||||||
apiKey?: string;
|
|
||||||
model: string;
|
|
||||||
authMode: OpenClawAuthMode;
|
|
||||||
basicAuthUsername?: string;
|
|
||||||
basicAuthPassword?: string;
|
|
||||||
apiKeyHeader?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
function envValue(name: string) {
|
|
||||||
const value = process.env[name];
|
|
||||||
if (!value) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const trimmed = value.trim();
|
|
||||||
return trimmed.length > 0 ? trimmed : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DEFAULT_MODEL = 'zeroclaw';
|
|
||||||
const DEFAULT_AUTH_MODE: OpenClawAuthMode = 'bearer';
|
|
||||||
|
|
||||||
function parseAuthMode(value: string | undefined): OpenClawAuthMode {
|
|
||||||
const normalized = value?.trim().toLowerCase();
|
|
||||||
if (normalized === 'basic' || normalized === 'none') {
|
|
||||||
return normalized;
|
|
||||||
}
|
|
||||||
|
|
||||||
return DEFAULT_AUTH_MODE;
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasSupportedProtocol(url: string) {
|
|
||||||
try {
|
|
||||||
const parsed = new URL(url);
|
|
||||||
return parsed.protocol === 'http:' || parsed.protocol === 'https:';
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildCompletionsUrl(baseUrl: string) {
|
|
||||||
if (!hasSupportedProtocol(baseUrl)) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const withSlash = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`;
|
|
||||||
return new URL('v1/chat/completions', withSlash).toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasRequiredAuth(config: OpenClawConfig) {
|
|
||||||
if (config.authMode === 'none') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.authMode === 'basic') {
|
|
||||||
return Boolean(config.basicAuthUsername && config.basicAuthPassword);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Boolean(config.apiKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildAuthHeaders(config: OpenClawConfig) {
|
|
||||||
const headers: Record<string, string> = {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
};
|
|
||||||
|
|
||||||
if (config.authMode === 'basic' && config.basicAuthUsername && config.basicAuthPassword) {
|
|
||||||
const credentials = Buffer
|
|
||||||
.from(`${config.basicAuthUsername}:${config.basicAuthPassword}`, 'utf8')
|
|
||||||
.toString('base64');
|
|
||||||
headers.Authorization = `Basic ${credentials}`;
|
|
||||||
} else if (config.authMode === 'bearer' && config.apiKey) {
|
|
||||||
headers.Authorization = `Bearer ${config.apiKey}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.apiKey && config.apiKeyHeader) {
|
|
||||||
headers[config.apiKeyHeader] = config.apiKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
return headers;
|
|
||||||
}
|
|
||||||
|
|
||||||
function fallbackResponse(prompt: string) {
|
|
||||||
const clipped = prompt.split('\n').slice(0, 6).join(' ').slice(0, 260);
|
|
||||||
|
|
||||||
return [
|
|
||||||
'OpenClaw fallback mode is active (configuration is missing or invalid).',
|
|
||||||
'Thesis: Portfolio remains analyzable with local heuristics until live model access is configured.',
|
|
||||||
'Risk scan: Concentration and filing sentiment should be monitored after each sync cycle.',
|
|
||||||
`Context digest: ${clipped}`
|
|
||||||
].join('\n\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getOpenClawConfig() {
|
|
||||||
return {
|
|
||||||
baseUrl: envValue('OPENCLAW_BASE_URL'),
|
|
||||||
apiKey: envValue('OPENCLAW_API_KEY'),
|
|
||||||
model: envValue('OPENCLAW_MODEL') ?? DEFAULT_MODEL,
|
|
||||||
authMode: parseAuthMode(envValue('OPENCLAW_AUTH_MODE')),
|
|
||||||
basicAuthUsername: envValue('OPENCLAW_BASIC_AUTH_USERNAME'),
|
|
||||||
basicAuthPassword: envValue('OPENCLAW_BASIC_AUTH_PASSWORD'),
|
|
||||||
apiKeyHeader: envValue('OPENCLAW_API_KEY_HEADER')
|
|
||||||
} satisfies OpenClawConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isOpenClawConfigured() {
|
|
||||||
const config = getOpenClawConfig();
|
|
||||||
return Boolean(config.baseUrl && hasSupportedProtocol(config.baseUrl) && hasRequiredAuth(config));
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function runOpenClawAnalysis(prompt: string, systemPrompt?: string) {
|
|
||||||
const config = getOpenClawConfig();
|
|
||||||
const endpoint = config.baseUrl ? buildCompletionsUrl(config.baseUrl) : undefined;
|
|
||||||
|
|
||||||
if (!endpoint || !hasRequiredAuth(config)) {
|
|
||||||
return {
|
|
||||||
provider: 'local-fallback',
|
|
||||||
model: config.model,
|
|
||||||
text: fallbackResponse(prompt)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(endpoint, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: buildAuthHeaders(config),
|
|
||||||
body: JSON.stringify({
|
|
||||||
model: config.model,
|
|
||||||
temperature: 0.2,
|
|
||||||
messages: [
|
|
||||||
systemPrompt
|
|
||||||
? { role: 'system', content: systemPrompt }
|
|
||||||
: null,
|
|
||||||
{ role: 'user', content: prompt }
|
|
||||||
].filter(Boolean)
|
|
||||||
}),
|
|
||||||
cache: 'no-store'
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const body = await response.text();
|
|
||||||
throw new Error(`OpenClaw request failed (${response.status}): ${body.slice(0, 220)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload = await response.json() as ChatCompletionResponse;
|
|
||||||
const text = payload.choices?.[0]?.message?.content?.trim();
|
|
||||||
|
|
||||||
if (!text) {
|
|
||||||
throw new Error('OpenClaw returned an empty response');
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
provider: 'openclaw',
|
|
||||||
model: config.model,
|
|
||||||
text
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { Filing, Holding, Task } from '@/lib/types';
|
import type { Filing, Holding, Task } from '@/lib/types';
|
||||||
import { runOpenClawAnalysis } from '@/lib/server/openclaw';
|
import { runAiAnalysis } from '@/lib/server/ai';
|
||||||
import { buildPortfolioSummary } from '@/lib/server/portfolio';
|
import { buildPortfolioSummary } from '@/lib/server/portfolio';
|
||||||
import { getQuote } from '@/lib/server/prices';
|
import { getQuote } from '@/lib/server/prices';
|
||||||
import {
|
import {
|
||||||
@@ -143,7 +143,7 @@ async function processAnalyzeFiling(task: Task) {
|
|||||||
'Return concise sections: Thesis, Red Flags, Follow-up Questions, Portfolio Impact.'
|
'Return concise sections: Thesis, Red Flags, Follow-up Questions, Portfolio Impact.'
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
||||||
const analysis = await runOpenClawAnalysis(prompt, 'Use concise institutional analyst language.');
|
const analysis = await runAiAnalysis(prompt, 'Use concise institutional analyst language.');
|
||||||
|
|
||||||
await saveFilingAnalysis(accessionNumber, {
|
await saveFilingAnalysis(accessionNumber, {
|
||||||
provider: analysis.provider,
|
provider: analysis.provider,
|
||||||
@@ -186,7 +186,7 @@ async function processPortfolioInsights(task: Task) {
|
|||||||
'Respond with: 1) health score (0-100), 2) top 3 risks, 3) top 3 opportunities, 4) next actions in 7 days.'
|
'Respond with: 1) health score (0-100), 2) top 3 risks, 3) top 3 opportunities, 4) next actions in 7 days.'
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
||||||
const analysis = await runOpenClawAnalysis(prompt, 'Act as a risk-aware buy-side analyst.');
|
const analysis = await runAiAnalysis(prompt, 'Act as a risk-aware buy-side analyst.');
|
||||||
|
|
||||||
await createPortfolioInsight({
|
await createPortfolioInsight({
|
||||||
userId,
|
userId,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
"@elysiajs/eden": "^1.4.8",
|
"@elysiajs/eden": "^1.4.8",
|
||||||
"@libsql/client": "^0.17.0",
|
"@libsql/client": "^0.17.0",
|
||||||
"@tailwindcss/postcss": "^4.2.1",
|
"@tailwindcss/postcss": "^4.2.1",
|
||||||
|
"ai": "^6.0.104",
|
||||||
"better-auth": "^1.4.19",
|
"better-auth": "^1.4.19",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
@@ -25,7 +26,8 @@
|
|||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4",
|
||||||
"recharts": "^3.7.0",
|
"recharts": "^3.7.0",
|
||||||
"workflow": "^4.1.0-beta.60"
|
"workflow": "^4.1.0-beta.60",
|
||||||
|
"zhipu-ai-provider": "^0.2.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^25.3.0",
|
"@types/node": "^25.3.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user