feat: implement AI agent for flowme-life interactions
- Add agent-run module to handle AI interactions with tools and messages. - Create routes for proxying requests to OpenAI and Anthropic APIs. - Implement flowme-life chat route for user queries and task management. - Add services for retrieving and updating life records in the database. - Implement logic for fetching today's tasks and marking tasks as done with next execution time calculation. - Introduce tests for flowme-life functionalities.
This commit is contained in:
23
package.json
23
package.json
@@ -41,14 +41,14 @@
|
||||
],
|
||||
"license": "UNLICENSED",
|
||||
"dependencies": {
|
||||
"@kevisual/ai": "^0.0.26",
|
||||
"@kevisual/ai": "^0.0.27",
|
||||
"@kevisual/auth": "^2.0.3",
|
||||
"@kevisual/js-filter": "^0.0.5",
|
||||
"@kevisual/query": "^0.0.52",
|
||||
"@kevisual/js-filter": "^0.0.6",
|
||||
"@kevisual/query": "^0.0.53",
|
||||
"@types/busboy": "^1.5.4",
|
||||
"@types/send": "^1.2.1",
|
||||
"@types/ws": "^8.18.1",
|
||||
"bullmq": "^5.70.2",
|
||||
"bullmq": "^5.70.4",
|
||||
"busboy": "^1.6.0",
|
||||
"drizzle-kit": "^0.31.9",
|
||||
"drizzle-orm": "^0.45.1",
|
||||
@@ -58,26 +58,28 @@
|
||||
"xml2js": "^0.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aws-sdk/client-s3": "^3.1002.0",
|
||||
"@ai-sdk/openai-compatible": "^2.0.35",
|
||||
"@aws-sdk/client-s3": "^3.1005.0",
|
||||
"@kevisual/api": "^0.0.62",
|
||||
"@kevisual/cnb": "^0.0.33",
|
||||
"@kevisual/cnb": "^0.0.42",
|
||||
"@kevisual/context": "^0.0.8",
|
||||
"@kevisual/convex": "^0.0.4",
|
||||
"@kevisual/convex": "^0.0.6",
|
||||
"@kevisual/local-app-manager": "0.1.32",
|
||||
"@kevisual/logger": "^0.0.4",
|
||||
"@kevisual/oss": "0.0.20",
|
||||
"@kevisual/permission": "^0.0.4",
|
||||
"@kevisual/router": "0.0.85",
|
||||
"@kevisual/router": "0.1.0",
|
||||
"@kevisual/types": "^0.0.12",
|
||||
"@kevisual/use-config": "^1.0.30",
|
||||
"@types/archiver": "^7.0.0",
|
||||
"@types/bun": "^1.3.10",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/node": "^25.3.3",
|
||||
"@types/node": "^25.4.0",
|
||||
"@types/pg": "^8.18.0",
|
||||
"@types/semver": "^7.7.1",
|
||||
"@types/xml2js": "^0.4.14",
|
||||
"ai": "^6.0.116",
|
||||
"archiver": "^7.0.1",
|
||||
"convex": "^1.32.0",
|
||||
"crypto-js": "^4.2.0",
|
||||
@@ -87,6 +89,7 @@
|
||||
"es-toolkit": "^1.45.1",
|
||||
"ioredis": "^5.10.0",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"lunar": "^2.0.0",
|
||||
"nanoid": "^5.1.6",
|
||||
"p-queue": "^9.1.0",
|
||||
"pg": "^8.20.0",
|
||||
@@ -99,7 +102,7 @@
|
||||
"picomatch": "^4.0.2",
|
||||
"ioredis": "^5.9.3"
|
||||
},
|
||||
"packageManager": "pnpm@10.30.3",
|
||||
"packageManager": "pnpm@10.32.0",
|
||||
"workspaces": [
|
||||
"wxmsg"
|
||||
]
|
||||
|
||||
1291
pnpm-lock.yaml
generated
1291
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
26
src/app.ts
26
src/app.ts
@@ -7,7 +7,9 @@ import { BailianProvider } from '@kevisual/ai';
|
||||
import * as schema from './db/schema.ts';
|
||||
import { config } from './modules/config.ts'
|
||||
import { db } from './modules/db.ts'
|
||||
import { convexClient, convexApi } from './modules/convex.ts'
|
||||
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
|
||||
|
||||
|
||||
export const router = useContextKey('router', () => new SimpleRouter());
|
||||
export const runtime = useContextKey('runtime', () => {
|
||||
return {
|
||||
@@ -44,6 +46,24 @@ export const ai = useContextKey('ai', () => {
|
||||
|
||||
export { schema };
|
||||
|
||||
export const bailian = createOpenAICompatible({
|
||||
baseURL: 'https://coding.dashscope.aliyuncs.com/v1',
|
||||
name: 'custom-bailian',
|
||||
apiKey: process.env.BAILIAN_CODE_API_KEY!,
|
||||
});
|
||||
|
||||
export const convex = useContextKey('convex', () => convexClient);
|
||||
export { convexApi };
|
||||
export const cnb = createOpenAICompatible({
|
||||
baseURL: 'https://api.cnb.cool/kevisual/kevisual/-/ai/',
|
||||
name: 'custom-cnb',
|
||||
apiKey: process.env.CNB_API_KEY!,
|
||||
});
|
||||
|
||||
export const models = {
|
||||
'doubao-ark-code-latest': 'doubao-ark-code-latest',
|
||||
'GLM-4.7': 'GLM-4.7',
|
||||
'MiniMax-M2.1': 'MiniMax-M2.1',
|
||||
'qwen3-coder-plus': 'qwen3-coder-plus',
|
||||
'hunyuan-a13b': 'hunyuan-a13b',
|
||||
'qwen-plus': 'qwen-plus',
|
||||
'auto': 'auto',
|
||||
}
|
||||
505
src/db/drizzle/0002_loving_lyja.sql
Normal file
505
src/db/drizzle/0002_loving_lyja.sql
Normal file
@@ -0,0 +1,505 @@
|
||||
CREATE TYPE "public"."enum_cf_router_code_type" AS ENUM('route', 'middleware');--> statement-breakpoint
|
||||
CREATE TABLE "ai_agent" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"type" varchar(255) NOT NULL,
|
||||
"baseUrl" varchar(255) NOT NULL,
|
||||
"apiKey" varchar(255) NOT NULL,
|
||||
"temperature" double precision,
|
||||
"cache" varchar(255),
|
||||
"cacheName" varchar(255),
|
||||
"createdAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updatedAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"model" varchar(255) NOT NULL,
|
||||
"data" json DEFAULT '{}'::json,
|
||||
"status" varchar(255) DEFAULT 'open',
|
||||
"key" varchar(255) NOT NULL,
|
||||
"description" text,
|
||||
"deletedAt" timestamp with time zone,
|
||||
CONSTRAINT "ai_agent_key_key" UNIQUE("key")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "apps_trades" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"out_trade_no" varchar(255) NOT NULL,
|
||||
"money" integer NOT NULL,
|
||||
"subject" text NOT NULL,
|
||||
"status" varchar(255) DEFAULT 'WAIT_BUYER_PAY' NOT NULL,
|
||||
"type" varchar(255) DEFAULT 'alipay' NOT NULL,
|
||||
"data" jsonb DEFAULT '{"list":[]}'::jsonb,
|
||||
"uid" uuid,
|
||||
"createdAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updatedAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"deletedAt" timestamp with time zone,
|
||||
CONSTRAINT "apps_trades_out_trade_no_key" UNIQUE("out_trade_no")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "cf_orgs" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"username" varchar(255) NOT NULL,
|
||||
"users" jsonb DEFAULT '[]'::jsonb,
|
||||
"createdAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updatedAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"deletedAt" timestamp with time zone,
|
||||
"description" varchar(255),
|
||||
CONSTRAINT "cf_orgs_username_key" UNIQUE("username")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "cf_router_code" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"path" varchar(255) NOT NULL,
|
||||
"key" varchar(255) NOT NULL,
|
||||
"active" boolean DEFAULT false,
|
||||
"project" varchar(255) DEFAULT 'default',
|
||||
"code" text DEFAULT '',
|
||||
"type" "enum_cf_router_code_type" DEFAULT 'route',
|
||||
"createdAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updatedAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"middleware" varchar(255)[] DEFAULT '{"RRAY[]::character varying[])::character varying(25"}',
|
||||
"next" varchar(255) DEFAULT '',
|
||||
"exec" text DEFAULT '',
|
||||
"data" json DEFAULT '{}'::json,
|
||||
"validator" json DEFAULT '{}'::json,
|
||||
"deletedAt" timestamp with time zone
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "cf_user" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"username" varchar(255) NOT NULL,
|
||||
"password" varchar(255),
|
||||
"salt" varchar(255),
|
||||
"needChangePassword" boolean DEFAULT false,
|
||||
"createdAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updatedAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"description" text,
|
||||
"data" jsonb DEFAULT '{}'::jsonb,
|
||||
"deletedAt" timestamp with time zone,
|
||||
"type" varchar(255) DEFAULT 'user',
|
||||
"owner" uuid,
|
||||
"orgId" uuid,
|
||||
"email" varchar(255),
|
||||
"avatar" text,
|
||||
"nickname" text,
|
||||
CONSTRAINT "cf_user_username_key" UNIQUE("username")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "cf_user_secrets" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"description" text,
|
||||
"status" varchar(255) DEFAULT 'active',
|
||||
"title" text,
|
||||
"expiredTime" timestamp with time zone,
|
||||
"token" varchar(255) DEFAULT '' NOT NULL,
|
||||
"userId" uuid,
|
||||
"data" jsonb DEFAULT '{}'::jsonb,
|
||||
"orgId" uuid,
|
||||
"createdAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updatedAt" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "chat_histories" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"data" json,
|
||||
"chatId" uuid,
|
||||
"chatPromptId" uuid,
|
||||
"root" boolean DEFAULT false,
|
||||
"show" boolean DEFAULT true,
|
||||
"uid" varchar(255),
|
||||
"createdAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updatedAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"role" varchar(255) DEFAULT 'user'
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "chat_prompts" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"title" varchar(255) NOT NULL,
|
||||
"description" text,
|
||||
"data" json,
|
||||
"key" varchar(255) DEFAULT '' NOT NULL,
|
||||
"uid" varchar(255),
|
||||
"createdAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updatedAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"deletedAt" timestamp with time zone
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "chat_sessions" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"data" json DEFAULT '{}'::json,
|
||||
"chatPromptId" uuid,
|
||||
"type" varchar(255) DEFAULT 'production',
|
||||
"uid" varchar(255),
|
||||
"createdAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updatedAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"title" varchar(255) DEFAULT '',
|
||||
"key" varchar(255)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "file_sync" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"name" varchar(255),
|
||||
"hash" varchar(255),
|
||||
"stat" jsonb DEFAULT '{}'::jsonb,
|
||||
"data" jsonb DEFAULT '{}'::jsonb,
|
||||
"checkedAt" timestamp with time zone,
|
||||
"createdAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updatedAt" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "kv_ai_chat_history" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"username" varchar(255) DEFAULT '' NOT NULL,
|
||||
"model" varchar(255) DEFAULT '' NOT NULL,
|
||||
"group" varchar(255) DEFAULT '' NOT NULL,
|
||||
"title" varchar(255) DEFAULT '' NOT NULL,
|
||||
"messages" jsonb DEFAULT '[]'::jsonb NOT NULL,
|
||||
"prompt_tokens" integer DEFAULT 0,
|
||||
"total_tokens" integer DEFAULT 0,
|
||||
"completion_tokens" integer DEFAULT 0,
|
||||
"data" jsonb DEFAULT '{}'::jsonb,
|
||||
"uid" uuid,
|
||||
"createdAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updatedAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"version" integer DEFAULT 0,
|
||||
"type" varchar(255) DEFAULT 'keep' NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "kv_app" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"data" jsonb DEFAULT '{}'::jsonb,
|
||||
"version" varchar(255) DEFAULT '',
|
||||
"key" varchar(255),
|
||||
"uid" uuid,
|
||||
"createdAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updatedAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"deletedAt" timestamp with time zone,
|
||||
"title" varchar(255) DEFAULT '',
|
||||
"description" varchar(255) DEFAULT '',
|
||||
"user" varchar(255),
|
||||
"status" varchar(255) DEFAULT 'running',
|
||||
"pid" uuid,
|
||||
"proxy" boolean DEFAULT false,
|
||||
CONSTRAINT "key_uid_unique" UNIQUE("key","uid")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "kv_app_domain" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"domain" varchar(255) NOT NULL,
|
||||
"appId" varchar(255),
|
||||
"uid" varchar(255),
|
||||
"createdAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updatedAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"deletedAt" timestamp with time zone,
|
||||
"data" jsonb,
|
||||
"status" varchar(255) DEFAULT 'running' NOT NULL,
|
||||
CONSTRAINT "kv_app_domain_domain_key" UNIQUE("domain")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "kv_app_list" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"data" json DEFAULT '{}'::json,
|
||||
"version" varchar(255) DEFAULT '',
|
||||
"uid" uuid,
|
||||
"createdAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updatedAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"deletedAt" timestamp with time zone,
|
||||
"key" varchar(255),
|
||||
"status" varchar(255) DEFAULT 'running'
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "kv_config" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"title" text DEFAULT '',
|
||||
"key" text DEFAULT '',
|
||||
"description" text DEFAULT '',
|
||||
"tags" jsonb DEFAULT '[]'::jsonb,
|
||||
"data" jsonb DEFAULT '{}'::jsonb,
|
||||
"uid" uuid,
|
||||
"createdAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updatedAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"deletedAt" timestamp with time zone,
|
||||
"hash" text DEFAULT ''
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "kv_light_code" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"title" text DEFAULT '',
|
||||
"description" text DEFAULT '',
|
||||
"type" text DEFAULT 'render-js',
|
||||
"code" text DEFAULT '',
|
||||
"data" jsonb DEFAULT '{}'::jsonb,
|
||||
"createdAt" timestamp DEFAULT now() NOT NULL,
|
||||
"updatedAt" timestamp DEFAULT now() NOT NULL,
|
||||
"uid" uuid,
|
||||
"tags" jsonb DEFAULT '[]'::jsonb,
|
||||
"hash" text DEFAULT ''
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "kv_github" (
|
||||
"id" uuid PRIMARY KEY NOT NULL,
|
||||
"title" varchar(255) DEFAULT '',
|
||||
"githubToken" varchar(255) DEFAULT '',
|
||||
"uid" uuid,
|
||||
"createdAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updatedAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"deletedAt" timestamp with time zone
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "kv_packages" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"title" text DEFAULT '',
|
||||
"description" text DEFAULT '',
|
||||
"tags" jsonb DEFAULT '[]'::jsonb,
|
||||
"data" jsonb DEFAULT '{}'::jsonb,
|
||||
"publish" jsonb DEFAULT '{}'::jsonb,
|
||||
"expand" jsonb DEFAULT '{}'::jsonb,
|
||||
"uid" uuid,
|
||||
"createdAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updatedAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"deletedAt" timestamp with time zone
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "kv_page" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"title" varchar(255) DEFAULT '',
|
||||
"description" text DEFAULT '',
|
||||
"type" varchar(255) DEFAULT '',
|
||||
"data" json DEFAULT '{}'::json,
|
||||
"uid" uuid,
|
||||
"createdAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updatedAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"deletedAt" timestamp with time zone,
|
||||
"publish" json DEFAULT '{}'::json
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "kv_resource" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"name" varchar(255) DEFAULT '',
|
||||
"description" text DEFAULT '',
|
||||
"source" varchar(255) DEFAULT '',
|
||||
"sourceId" varchar(255) DEFAULT '',
|
||||
"version" varchar(255) DEFAULT '0.0.0',
|
||||
"data" json DEFAULT '{}'::json,
|
||||
"uid" uuid,
|
||||
"createdAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updatedAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"deletedAt" timestamp with time zone
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "kv_vip" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"userId" uuid NOT NULL,
|
||||
"level" varchar(255) DEFAULT 'free',
|
||||
"category" varchar(255) NOT NULL,
|
||||
"startDate" timestamp with time zone,
|
||||
"endDate" timestamp with time zone,
|
||||
"data" jsonb DEFAULT '{}'::jsonb,
|
||||
"createdAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updatedAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"deletedAt" timestamp with time zone,
|
||||
"title" text DEFAULT '' NOT NULL,
|
||||
"description" text DEFAULT '' NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "micro_apps_upload" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"title" varchar(255) DEFAULT '',
|
||||
"description" varchar(255) DEFAULT '',
|
||||
"tags" jsonb DEFAULT '[]'::jsonb,
|
||||
"type" varchar(255) DEFAULT '',
|
||||
"source" varchar(255) DEFAULT '',
|
||||
"data" jsonb DEFAULT '{}'::jsonb,
|
||||
"share" boolean DEFAULT false,
|
||||
"uname" varchar(255) DEFAULT '',
|
||||
"uid" uuid,
|
||||
"createdAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updatedAt" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "micro_mark" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"title" text DEFAULT '',
|
||||
"description" text DEFAULT '',
|
||||
"tags" jsonb DEFAULT '[]'::jsonb,
|
||||
"data" jsonb DEFAULT '{}'::jsonb,
|
||||
"uname" varchar(255) DEFAULT '',
|
||||
"uid" uuid,
|
||||
"createdAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updatedAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"cover" text DEFAULT '',
|
||||
"thumbnail" text DEFAULT '',
|
||||
"link" text DEFAULT '',
|
||||
"summary" text DEFAULT '',
|
||||
"markType" text DEFAULT 'md',
|
||||
"config" jsonb DEFAULT '{}'::jsonb,
|
||||
"puid" uuid,
|
||||
"deletedAt" timestamp with time zone,
|
||||
"version" integer DEFAULT 1,
|
||||
"fileList" jsonb DEFAULT '[]'::jsonb,
|
||||
"key" text DEFAULT ''
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "query_views" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"uid" uuid,
|
||||
"title" text DEFAULT '',
|
||||
"summary" text DEFAULT '',
|
||||
"description" text DEFAULT '',
|
||||
"tags" jsonb DEFAULT '[]'::jsonb,
|
||||
"link" text DEFAULT '',
|
||||
"data" jsonb DEFAULT '{}'::jsonb,
|
||||
"createdAt" timestamp DEFAULT now() NOT NULL,
|
||||
"updatedAt" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "router_views" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"uid" uuid,
|
||||
"title" text DEFAULT '',
|
||||
"summary" text DEFAULT '',
|
||||
"description" text DEFAULT '',
|
||||
"tags" jsonb DEFAULT '[]'::jsonb,
|
||||
"link" text DEFAULT '',
|
||||
"data" jsonb DEFAULT '{}'::jsonb,
|
||||
"views" jsonb DEFAULT '[]'::jsonb,
|
||||
"createdAt" timestamp DEFAULT now() NOT NULL,
|
||||
"updatedAt" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "work_share_mark" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"title" text DEFAULT '',
|
||||
"key" text DEFAULT '',
|
||||
"markType" text DEFAULT 'md',
|
||||
"description" text DEFAULT '',
|
||||
"cover" text DEFAULT '',
|
||||
"link" text DEFAULT '',
|
||||
"tags" jsonb DEFAULT '[]'::jsonb,
|
||||
"summary" text DEFAULT '',
|
||||
"config" jsonb DEFAULT '{}'::jsonb,
|
||||
"data" jsonb DEFAULT '{}'::jsonb,
|
||||
"fileList" jsonb DEFAULT '[]'::jsonb,
|
||||
"uname" varchar(255) DEFAULT '',
|
||||
"version" integer DEFAULT 1,
|
||||
"markedAt" timestamp with time zone,
|
||||
"uid" uuid,
|
||||
"puid" uuid,
|
||||
"createdAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updatedAt" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"deletedAt" timestamp with time zone
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "n_code_make" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"slug" text NOT NULL,
|
||||
"resources" jsonb DEFAULT '[]'::jsonb,
|
||||
"userId" uuid,
|
||||
"createdAt" timestamp DEFAULT now() NOT NULL,
|
||||
"updatedAt" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "n_code_shop" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"slug" text NOT NULL,
|
||||
"title" text NOT NULL,
|
||||
"tags" jsonb,
|
||||
"link" text,
|
||||
"description" text DEFAULT '' NOT NULL,
|
||||
"data" jsonb,
|
||||
"platform" text NOT NULL,
|
||||
"userinfo" text,
|
||||
"orderLink" text NOT NULL,
|
||||
"userId" uuid,
|
||||
"createdAt" timestamp DEFAULT now() NOT NULL,
|
||||
"updatedAt" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "n_code_short_link" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"slug" text NOT NULL,
|
||||
"code" text DEFAULT '' NOT NULL,
|
||||
"type" text DEFAULT 'link' NOT NULL,
|
||||
"version" text DEFAULT '1.0.0' NOT NULL,
|
||||
"title" text DEFAULT '' NOT NULL,
|
||||
"description" text DEFAULT '' NOT NULL,
|
||||
"tags" jsonb DEFAULT '[]'::jsonb,
|
||||
"data" jsonb DEFAULT '{}'::jsonb,
|
||||
"userId" uuid,
|
||||
"createdAt" timestamp DEFAULT now() NOT NULL,
|
||||
"updatedAt" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "flowme" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"uid" uuid,
|
||||
"title" text DEFAULT '',
|
||||
"tags" jsonb DEFAULT '[]'::jsonb,
|
||||
"summary" text DEFAULT '',
|
||||
"description" text DEFAULT '',
|
||||
"link" text DEFAULT '',
|
||||
"data" jsonb DEFAULT '{}'::jsonb,
|
||||
"channelId" uuid,
|
||||
"type" text DEFAULT '',
|
||||
"source" text DEFAULT '',
|
||||
"importance" integer DEFAULT 0,
|
||||
"isArchived" boolean DEFAULT false,
|
||||
"createdAt" timestamp DEFAULT now() NOT NULL,
|
||||
"updatedAt" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "flowme_channels" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"uid" uuid,
|
||||
"title" text DEFAULT '',
|
||||
"tags" jsonb DEFAULT '[]'::jsonb,
|
||||
"summary" text DEFAULT '',
|
||||
"description" text DEFAULT '',
|
||||
"link" text DEFAULT '',
|
||||
"data" jsonb DEFAULT '{}'::jsonb,
|
||||
"key" text DEFAULT '',
|
||||
"color" text DEFAULT '#007bff',
|
||||
"createdAt" timestamp DEFAULT now() NOT NULL,
|
||||
"updatedAt" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "flowme_life" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"uid" uuid,
|
||||
"title" text DEFAULT '',
|
||||
"tags" jsonb DEFAULT '[]'::jsonb,
|
||||
"summary" text DEFAULT '',
|
||||
"description" text DEFAULT '',
|
||||
"link" text DEFAULT '',
|
||||
"data" jsonb DEFAULT '{}'::jsonb,
|
||||
"effectiveAt" timestamp with time zone,
|
||||
"type" text DEFAULT '',
|
||||
"prompt" text DEFAULT '',
|
||||
"taskType" text DEFAULT '',
|
||||
"taskResult" jsonb DEFAULT '{}'::jsonb,
|
||||
"createdAt" timestamp DEFAULT now() NOT NULL,
|
||||
"updatedAt" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "cf_prompts" ALTER COLUMN "parents" SET DATA TYPE text[];--> statement-breakpoint
|
||||
ALTER TABLE "cf_prompts" ALTER COLUMN "parents" SET DEFAULT '{}';--> statement-breakpoint
|
||||
ALTER TABLE "flowme" ADD CONSTRAINT "flowme_channelId_flowme_channels_id_fk" FOREIGN KEY ("channelId") REFERENCES "public"."flowme_channels"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
||||
CREATE INDEX "file_sync_name_idx" ON "file_sync" USING btree ("name");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "kv_app_key_uid" ON "kv_app" USING btree ("key","uid");--> statement-breakpoint
|
||||
CREATE INDEX "query_views_uid_idx" ON "query_views" USING btree ("uid");--> statement-breakpoint
|
||||
CREATE INDEX "query_title_idx" ON "query_views" USING btree ("title");--> statement-breakpoint
|
||||
CREATE INDEX "router_views_uid_idx" ON "router_views" USING btree ("uid");--> statement-breakpoint
|
||||
CREATE INDEX "router_title_idx" ON "router_views" USING btree ("title");--> statement-breakpoint
|
||||
CREATE INDEX "router_views_views_idx" ON "router_views" USING gin ("views");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "n_code_make_idx_slug" ON "n_code_make" USING btree ("slug");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "n_code_shop_idx_slug" ON "n_code_shop" USING btree ("slug");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "n_code_short_idx_slug" ON "n_code_short_link" USING btree ("slug");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "n_code_short_idx_code" ON "n_code_short_link" USING btree ("code");--> statement-breakpoint
|
||||
CREATE INDEX "flowme_uid_idx" ON "flowme" USING btree ("uid");--> statement-breakpoint
|
||||
CREATE INDEX "flowme_title_idx" ON "flowme" USING btree ("title");--> statement-breakpoint
|
||||
CREATE INDEX "flowme_channel_id_idx" ON "flowme" USING btree ("channelId");--> statement-breakpoint
|
||||
CREATE INDEX "flowme_channels_uid_idx" ON "flowme_channels" USING btree ("uid");--> statement-breakpoint
|
||||
CREATE INDEX "flowme_channels_key_idx" ON "flowme_channels" USING btree ("key");--> statement-breakpoint
|
||||
CREATE INDEX "flowme_channels_title_idx" ON "flowme_channels" USING btree ("title");--> statement-breakpoint
|
||||
CREATE INDEX "life_uid_idx" ON "flowme_life" USING btree ("uid");--> statement-breakpoint
|
||||
CREATE INDEX "life_title_idx" ON "flowme_life" USING btree ("title");--> statement-breakpoint
|
||||
CREATE INDEX "life_effective_at_idx" ON "flowme_life" USING btree ("effectiveAt");--> statement-breakpoint
|
||||
CREATE INDEX "life_summary_idx" ON "flowme_life" USING btree ("summary");--> statement-breakpoint
|
||||
CREATE INDEX "prompts_parents_idx" ON "cf_prompts" USING gin ("parents");
|
||||
3479
src/db/drizzle/meta/0002_snapshot.json
Normal file
3479
src/db/drizzle/meta/0002_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -15,6 +15,13 @@
|
||||
"when": 1767070768620,
|
||||
"tag": "0001_solid_nocturne",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 2,
|
||||
"version": "7",
|
||||
"when": 1773148571509,
|
||||
"tag": "0002_loving_lyja",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -12,7 +12,13 @@ export const life = pgTable("flowme_life", {
|
||||
link: text('link').default(''),
|
||||
data: jsonb().default({}),
|
||||
|
||||
effectiveAt: text('effectiveAt').default(''),
|
||||
effectiveAt: timestamp('effectiveAt', { withTimezone: true }),
|
||||
/**
|
||||
* 智能,
|
||||
* 每年农历
|
||||
* 备忘
|
||||
* 归档
|
||||
*/
|
||||
type: text('type').default(''),
|
||||
prompt: text('prompt').default(''),
|
||||
taskType: text('taskType').default(''),
|
||||
|
||||
65
src/modules/ai/agent-run.ts
Normal file
65
src/modules/ai/agent-run.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { type QueryRouterServer, type App, type RouteInfo } from '@kevisual/router'
|
||||
import { generateText, tool, type ModelMessage, type LanguageModel, type GenerateTextResult } from 'ai';
|
||||
import z from 'zod';
|
||||
import { filter } from '@kevisual/js-filter'
|
||||
export const createTool = async (app: QueryRouterServer | App, message: { path: string, key: string, token?: string }) => {
|
||||
const route = app.findRoute({ path: message.path, key: message.key });
|
||||
if (!route) {
|
||||
console.error(`未找到路径 ${message.path} 和 key ${message.key} 的路由`);
|
||||
return null;
|
||||
}
|
||||
const _tool = tool({
|
||||
description: route?.metadata?.summary || route?.description || '无描述',
|
||||
inputSchema: z.object({
|
||||
...route.metadata?.args
|
||||
}), // 这里可以根据实际需要定义输入参数的 schema
|
||||
execute: async (args: any) => {
|
||||
const res = await app.run({ path: message.path, key: message.key, payload: args, token: message.token });
|
||||
return res;
|
||||
}
|
||||
});
|
||||
return _tool;
|
||||
}
|
||||
|
||||
export const createTools = async (opts: { app: QueryRouterServer | App, token?: string }) => {
|
||||
const { app, token } = opts;
|
||||
const tools: Record<string, any> = {};
|
||||
for (const route of app.routes) {
|
||||
const id = route.id!;
|
||||
const _tool = await createTool(app, { path: route.path!, key: route.key!, token });
|
||||
if (_tool && id) {
|
||||
tools[id] = _tool;
|
||||
}
|
||||
}
|
||||
return tools;
|
||||
}
|
||||
type Route = Partial<RouteInfo>
|
||||
type AgentResult = {
|
||||
result: GenerateTextResult<Record<string, any>, any>,
|
||||
messages: ModelMessage[],
|
||||
}
|
||||
export const reCallAgent = async (opts: { messages?: ModelMessage[], tools?: Record<string, any>, languageModel: LanguageModel }): Promise<AgentResult> => {
|
||||
const { messages = [], tools = {}, languageModel } = opts;
|
||||
const result = await generateText({
|
||||
model: languageModel,
|
||||
messages,
|
||||
tools,
|
||||
});
|
||||
const step = result.steps[0]!;
|
||||
if (step.finishReason === 'tool-calls') {
|
||||
messages.push(...result.response.messages);
|
||||
return reCallAgent({ messages, tools, languageModel });
|
||||
}
|
||||
return { result, messages };
|
||||
}
|
||||
export const runAgent = async (opts: { app: QueryRouterServer | App, messages?: ModelMessage[], routes?: Route[], query?: string, languageModel: LanguageModel, token: string }) => {
|
||||
const { app, languageModel } = opts;
|
||||
let messages = opts.messages || [];
|
||||
|
||||
let routes = opts?.routes || app.routes;
|
||||
if (opts.query) {
|
||||
routes = filter(routes, opts.query);
|
||||
};
|
||||
const tools = await createTools({ app, token: opts.token });
|
||||
return await reCallAgent({ messages, tools, languageModel });
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { api } from '@kevisual/convex';
|
||||
import { ConvexClient, AuthTokenFetcher } from "convex/browser";
|
||||
const url = process.env["CONVEX_URL"]
|
||||
const convexClient = new ConvexClient(url!);
|
||||
|
||||
const token = process.env["KEVISUAL_CONVEX_TOKEN"]
|
||||
const authTokenFetcher: AuthTokenFetcher = async ({ forceRefreshToken }: { forceRefreshToken: boolean }) => {
|
||||
console.log("AuthTokenFetcher called, forceRefreshToken:", forceRefreshToken);
|
||||
return token;
|
||||
}
|
||||
convexClient.setAuth(authTokenFetcher, (isAuthenticated) => {
|
||||
console.log("Auth isAuthenticated:", isAuthenticated);
|
||||
});
|
||||
|
||||
export { convexClient }
|
||||
export const convexApi = api;
|
||||
@@ -109,7 +109,7 @@ export const pipeProxyReq = async (req: http.IncomingMessage, proxyReq: http.Cli
|
||||
const bunRequest = req.bun.request;
|
||||
const contentType = req.headers['content-type'] || '';
|
||||
if (contentType.includes('multipart/form-data')) {
|
||||
console.log('Processing multipart/form-data');
|
||||
// console.log('Processing multipart/form-data');
|
||||
const arrayBuffer = await bunRequest.arrayBuffer();
|
||||
|
||||
// 设置请求头(在写入数据之前)
|
||||
@@ -123,7 +123,6 @@ export const pipeProxyReq = async (req: http.IncomingMessage, proxyReq: http.Cli
|
||||
proxyReq.end();
|
||||
return;
|
||||
}
|
||||
console.log('Bun pipeProxyReq content-type', contentType);
|
||||
// @ts-ignore
|
||||
const bodyString = req.body;
|
||||
bodyString && proxyReq.write(bodyString);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { convex, convexApi } from '@/app.ts';
|
||||
import { User } from '@/models/user.ts';
|
||||
import { omit } from 'es-toolkit';
|
||||
import { IncomingMessage, ServerResponse } from 'http';
|
||||
|
||||
@@ -3,10 +3,12 @@ import { router } from './router.ts';
|
||||
import { handleRequest as PageProxy } from './page-proxy.ts';
|
||||
|
||||
import './routes/jwks.ts'
|
||||
import './routes/ai/openai.ts'
|
||||
|
||||
const simpleAppsPrefixs = [
|
||||
"/api/wxmsg",
|
||||
"/api/convex/",
|
||||
"/api/chat/completions"
|
||||
];
|
||||
|
||||
|
||||
|
||||
51
src/routes-simple/routes/ai/anthropic.ts
Normal file
51
src/routes-simple/routes/ai/anthropic.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { router } from '@/app.ts';
|
||||
import http from 'node:http';
|
||||
import https from 'node:https';
|
||||
import { pipeProxyReq, pipeProxyRes } from '@/modules/fm-manager/index.ts';
|
||||
import { useKey } from '@kevisual/context';
|
||||
|
||||
/**
|
||||
* TODO: 由于目前没有找到合适的开源 Anthropic 兼容实现,暂时先把 /v1/messages 请求代理到配置的 OpenAI 兼容目标地址,等后续有了合适的 Anthropic 兼容实现再改回来
|
||||
* 代理 /v1/messages 请求到配置的 OpenAI 兼容目标地址
|
||||
* 配置项: config.OPENAI_BASE_URL,例如 http://localhost:11434/v1
|
||||
*
|
||||
*/
|
||||
router.all("/v1/messages", async (req, res) => {
|
||||
const targetUrl = new URL('https://api.cnb.cool/kevisual/kevisual/-/ai/chat/messages');
|
||||
const token = useKey('CNB_API_KEY');
|
||||
// 收集并转发请求头(排除 host 和 authorization,用自己的 token 替换)
|
||||
const headers: Record<string, any> = {};
|
||||
for (const [key, value] of Object.entries(req.headers)) {
|
||||
if (key.toLowerCase() !== 'host' && key.toLowerCase() !== 'authorization') {
|
||||
headers[key] = value;
|
||||
}
|
||||
}
|
||||
if (token) {
|
||||
headers['authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
const isHttps = targetUrl.protocol === 'https:';
|
||||
const protocol = isHttps ? https : http;
|
||||
|
||||
const options: http.RequestOptions = {
|
||||
hostname: targetUrl.hostname,
|
||||
port: targetUrl.port || (isHttps ? 443 : 80),
|
||||
path: targetUrl.pathname + (targetUrl.search || ''),
|
||||
method: req.method,
|
||||
headers,
|
||||
...(isHttps ? { rejectUnauthorized: false } : {}),
|
||||
};
|
||||
|
||||
const proxyReq = protocol.request(options, (proxyRes) => {
|
||||
res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
|
||||
pipeProxyRes(proxyRes, res);
|
||||
});
|
||||
|
||||
proxyReq.on('error', (err) => {
|
||||
console.error('[anthropic proxy] error:', err.message);
|
||||
res.writeHead(502, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: err.message }));
|
||||
});
|
||||
|
||||
pipeProxyReq(req, proxyReq, res);
|
||||
});
|
||||
50
src/routes-simple/routes/ai/openai.ts
Normal file
50
src/routes-simple/routes/ai/openai.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { router } from '@/app.ts';
|
||||
import http from 'node:http';
|
||||
import https from 'node:https';
|
||||
import { pipeProxyReq, pipeProxyRes } from '@/modules/fm-manager/index.ts';
|
||||
import { useKey } from '@kevisual/context';
|
||||
|
||||
/**
|
||||
* 代理 /api/chat/completions 请求到配置的 OpenAI 兼容目标地址
|
||||
* 配置项: config.OPENAI_BASE_URL,例如 http://localhost:11434/v1
|
||||
*/
|
||||
router.all("/api/chat/completions", async (req, res) => {
|
||||
const targetUrl = new URL('https://api.cnb.cool/kevisual/kevisual/-/ai/chat/completions');
|
||||
|
||||
const token = useKey('CNB_API_KEY');
|
||||
// 收集并转发请求头(排除 host 和 authorization,用自己的 token 替换)
|
||||
const headers: Record<string, any> = {};
|
||||
for (const [key, value] of Object.entries(req.headers)) {
|
||||
if (key.toLowerCase() !== 'host' && key.toLowerCase() !== 'authorization') {
|
||||
headers[key] = value;
|
||||
}
|
||||
}
|
||||
if (token) {
|
||||
headers['authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
const isHttps = targetUrl.protocol === 'https:';
|
||||
const protocol = isHttps ? https : http;
|
||||
|
||||
const options: http.RequestOptions = {
|
||||
hostname: targetUrl.hostname,
|
||||
port: targetUrl.port || (isHttps ? 443 : 80),
|
||||
path: targetUrl.pathname + (targetUrl.search || ''),
|
||||
method: req.method,
|
||||
headers,
|
||||
...(isHttps ? { rejectUnauthorized: false } : {}),
|
||||
};
|
||||
|
||||
const proxyReq = protocol.request(options, (proxyRes) => {
|
||||
res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
|
||||
pipeProxyRes(proxyRes, res);
|
||||
});
|
||||
|
||||
proxyReq.on('error', (err) => {
|
||||
console.error('[openai proxy] error:', err.message);
|
||||
res.writeHead(502, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: err.message }));
|
||||
});
|
||||
|
||||
pipeProxyReq(req, proxyReq, res);
|
||||
});
|
||||
44
src/routes/flowme-life/chat.ts
Normal file
44
src/routes/flowme-life/chat.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
|
||||
import { schema, app, cnb, models } from '@/app.ts'
|
||||
import z from 'zod';
|
||||
import { runAgent } from '@kevisual/ai/agent'
|
||||
|
||||
app.route({
|
||||
path: 'flowme-life',
|
||||
key: 'chat',
|
||||
description: `聊天接口, 对自己的数据进行操作,参数是 question或messages,question是用户的提问,messages是对话消息列表,优先级高于 question`,
|
||||
middleware: ['auth']
|
||||
, metadata: {
|
||||
args: {
|
||||
question: z.string().describe('用户的提问'),
|
||||
messages: z.any().optional().describe('对话消息列表,优先级高于 question'),
|
||||
}
|
||||
}
|
||||
}).define(async (ctx) => {
|
||||
const question = ctx.query.question || '';
|
||||
const _messages = ctx.query.messages;
|
||||
const token = ctx.query.token || '';
|
||||
if (!question && !_messages) {
|
||||
ctx.throw(400, '缺少参数 question 或 messages');
|
||||
}
|
||||
const routes = ctx.app.getList().filter(r => r.path.startsWith('flowme-life') && r.key !== 'chat');
|
||||
const messages = _messages || [
|
||||
{
|
||||
"role": "system" as const,
|
||||
"content": `你是我的智能助手,协助我操作我的数据, 请根据我的提问选择合适的接口进行调用。`
|
||||
},
|
||||
{
|
||||
"role": "user" as const,
|
||||
"content": question
|
||||
}
|
||||
]
|
||||
const res = await runAgent({
|
||||
app: app,
|
||||
messages: messages,
|
||||
languageModel: cnb(models['auto']),
|
||||
// query: 'WHERE path LIKE 'flowme-life%',
|
||||
routes,
|
||||
token,
|
||||
});
|
||||
ctx.body = res
|
||||
}).addTo(app);
|
||||
@@ -1 +1,3 @@
|
||||
import './list.ts'
|
||||
import './today.ts'
|
||||
import './chat.ts'
|
||||
38
src/routes/flowme-life/life.services.ts
Normal file
38
src/routes/flowme-life/life.services.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { schema, db } from '@/app.ts';
|
||||
|
||||
export type LifeItem = typeof schema.life.$inferSelect;
|
||||
|
||||
/**
|
||||
* 根据 id 获取 life 记录
|
||||
*/
|
||||
export async function getLifeItem(id: string): Promise<{ code: number; data?: LifeItem; message?: string }> {
|
||||
try {
|
||||
const result = await db.select().from(schema.life).where(eq(schema.life.id, id)).limit(1);
|
||||
if (result.length === 0) {
|
||||
return { code: 404, message: `记录 ${id} 不存在` };
|
||||
}
|
||||
return { code: 200, data: result[0] };
|
||||
} catch (e) {
|
||||
return { code: 500, message: `获取记录 ${id} 失败: ${e?.message || e}` };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 life 记录的 effectiveAt(下次执行时间)
|
||||
*/
|
||||
export async function updateLifeEffectiveAt(id: string, effectiveAt: string | Date): Promise<{ code: number; data?: LifeItem; message?: string }> {
|
||||
try {
|
||||
const result = await db
|
||||
.update(schema.life)
|
||||
.set({ effectiveAt: new Date(effectiveAt) })
|
||||
.where(eq(schema.life.id, id))
|
||||
.returning();
|
||||
if (result.length === 0) {
|
||||
return { code: 404, message: `记录 ${id} 不存在` };
|
||||
}
|
||||
return { code: 200, data: result[0] };
|
||||
} catch (e) {
|
||||
return { code: 500, message: `更新记录 ${id} 失败: ${e?.message || e}` };
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { desc, eq, count, or, like, and } from 'drizzle-orm';
|
||||
import { schema, app, db } from '@/app.ts'
|
||||
import z from 'zod';
|
||||
import dayjs from 'dayjs';
|
||||
app.route({
|
||||
path: 'flowme-life',
|
||||
key: 'list',
|
||||
@@ -72,7 +73,7 @@ app.route({
|
||||
link: z.string().describe('链接').optional(),
|
||||
data: z.record(z.string(), z.any()).describe('数据').optional(),
|
||||
effectiveAt: z.string().describe('生效日期').optional(),
|
||||
type: z.string().describe('类型').optional(),
|
||||
type: z.string().describe('类型: 智能, 每年农历, 备忘, 归档等.默认智能').optional(),
|
||||
prompt: z.string().describe('提示词').optional(),
|
||||
taskType: z.string().describe('任务类型').optional(),
|
||||
taskResult: z.record(z.string(), z.any()).describe('任务结果').optional(),
|
||||
@@ -82,6 +83,11 @@ app.route({
|
||||
}).define(async (ctx) => {
|
||||
const { uid, updatedAt, createdAt, ...rest } = ctx.query.data || {};
|
||||
const tokenUser = ctx.state.tokenUser;
|
||||
if (rest.effectiveAt && isNaN(Date.parse(rest.effectiveAt))) {
|
||||
rest.effectiveAt = null;
|
||||
} else if (rest.effectiveAt) {
|
||||
rest.effectiveAt = dayjs(rest.effectiveAt).toISOString();
|
||||
}
|
||||
const lifeItem = await db.insert(schema.life).values({
|
||||
title: rest.title || '',
|
||||
summary: rest.summary || '',
|
||||
@@ -90,7 +96,7 @@ app.route({
|
||||
link: rest.link || '',
|
||||
data: rest.data || {},
|
||||
effectiveAt: rest.effectiveAt || '',
|
||||
type: rest.type || '',
|
||||
type: rest.type || '智能',
|
||||
prompt: rest.prompt || '',
|
||||
taskType: rest.taskType || '',
|
||||
taskResult: rest.taskResult || {},
|
||||
@@ -103,7 +109,7 @@ app.route({
|
||||
path: 'flowme-life',
|
||||
key: 'update',
|
||||
middleware: ['auth'],
|
||||
description: '更新一个 flowme-life',
|
||||
description: '更新一个 flowme-life 的数据',
|
||||
metadata: {
|
||||
args: {
|
||||
data: z.object({
|
||||
@@ -135,6 +141,11 @@ app.route({
|
||||
if (existing[0].uid !== tokenUser.id) {
|
||||
ctx.throw(403, '没有权限更新该 flowme-life');
|
||||
}
|
||||
if (rest.effectiveAt && isNaN(Date.parse(rest.effectiveAt))) {
|
||||
rest.effectiveAt = null;
|
||||
} else if (rest.effectiveAt) {
|
||||
rest.effectiveAt = dayjs(rest.effectiveAt).toISOString();
|
||||
}
|
||||
const lifeItem = await db.update(schema.life).set({
|
||||
title: rest.title,
|
||||
summary: rest.summary,
|
||||
@@ -155,17 +166,15 @@ app.route({
|
||||
path: 'flowme-life',
|
||||
key: 'delete',
|
||||
middleware: ['auth'],
|
||||
description: '删除 flowme-life',
|
||||
description: '删除单个 flowme-life, 参数: id 必填',
|
||||
metadata: {
|
||||
args: {
|
||||
data: z.object({
|
||||
id: z.string().describe('ID'),
|
||||
})
|
||||
id: z.string().describe('ID'),
|
||||
}
|
||||
}
|
||||
}).define(async (ctx) => {
|
||||
const tokenUser = ctx.state.tokenUser;
|
||||
const { id } = ctx.query.data || {};
|
||||
const { id } = ctx.query || {};
|
||||
if (!id) {
|
||||
ctx.throw(400, 'id 参数缺失');
|
||||
}
|
||||
@@ -184,10 +193,15 @@ app.route({
|
||||
path: 'flowme-life',
|
||||
key: 'get',
|
||||
middleware: ['auth'],
|
||||
description: '获取单个 flowme-life, 参数: data.id 必填',
|
||||
description: '获取单个 flowme-life, 参数: id 必填',
|
||||
metadata: {
|
||||
args: {
|
||||
id: z.string().describe('ID'),
|
||||
}
|
||||
}
|
||||
}).define(async (ctx) => {
|
||||
const tokenUser = ctx.state.tokenUser;
|
||||
const { id } = ctx.query.data || {};
|
||||
const { id } = ctx.query || {};
|
||||
if (!id) {
|
||||
ctx.throw(400, 'id 参数缺失');
|
||||
}
|
||||
|
||||
174
src/routes/flowme-life/today.ts
Normal file
174
src/routes/flowme-life/today.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
import { desc, eq, count, like, and, lt } from 'drizzle-orm';
|
||||
import { schema, app, db } from '@/app.ts'
|
||||
import z from 'zod';
|
||||
import dayjs from 'dayjs';
|
||||
import { useContextKey } from '@kevisual/context';
|
||||
import { createLunarDate, toGregorian } from 'lunar';
|
||||
import { getLifeItem, updateLifeEffectiveAt } from './life.services.ts';
|
||||
app.route({
|
||||
path: 'flowme-life',
|
||||
key: 'today',
|
||||
description: `获取今天需要做的事情列表`,
|
||||
middleware: ['auth'],
|
||||
}).define(async (ctx) => {
|
||||
const tokenUser = ctx.state.tokenUser;
|
||||
const uid = tokenUser.id;
|
||||
|
||||
const tomorrow = dayjs().add(1, 'day').startOf('day').toDate();
|
||||
|
||||
let whereCondition = eq(schema.life.uid, uid);
|
||||
whereCondition = and(
|
||||
eq(schema.life.uid, uid),
|
||||
eq(schema.life.taskType, '运行中'),
|
||||
lt(schema.life.effectiveAt, tomorrow)
|
||||
);
|
||||
|
||||
const list = await db.select()
|
||||
.from(schema.life)
|
||||
.where(whereCondition)
|
||||
.orderBy(desc(schema.life.effectiveAt));
|
||||
|
||||
console.log('today res', list.map(i => i['title']));
|
||||
if (list.length > 0) {
|
||||
ctx.body = {
|
||||
list,
|
||||
content: list.map(item => {
|
||||
return `任务id:[${item['id']}]\n标题: ${item['title']}。\n启动时间: ${dayjs(item['effectiveAt']).format('YYYY-MM-DD HH:mm:ss')}。标签: ${item['tags'] || '无'} \n总结: ${item['summary'] || '无'}`;
|
||||
}).join('\n')
|
||||
};
|
||||
} else {
|
||||
ctx.body = {
|
||||
list,
|
||||
content: '今天没有需要做的事情了,休息一下吧'
|
||||
}
|
||||
}
|
||||
}).addTo(app);
|
||||
|
||||
|
||||
app.route({
|
||||
path: 'flowme-life',
|
||||
key: 'done',
|
||||
description: `完成某件事情,然后判断下一次运行时间。参数是id(string),数据类型是string。如果多个存在,则是ids的string数组`,
|
||||
middleware: ['auth'],
|
||||
metadata: {
|
||||
args: {
|
||||
id: z.string().optional().describe('记录id'),
|
||||
ids: z.array(z.string()).optional().describe('记录id数组'),
|
||||
}
|
||||
}
|
||||
}).define(async (ctx) => {
|
||||
const id = ctx.query.id;
|
||||
const ids: string[] = ctx.query.ids || [];
|
||||
if (!id && ids.length === 0) {
|
||||
ctx.throw(400, '缺少参数 id');
|
||||
}
|
||||
if (ids.length === 0 && id) {
|
||||
ids.push(String(id));
|
||||
}
|
||||
console.log('id', id, ids);
|
||||
const messages = [];
|
||||
const changeItem = async (id: string) => {
|
||||
// 获取记录详情
|
||||
const recordRes = await getLifeItem(id);
|
||||
if (recordRes.code !== 200) {
|
||||
messages.push({
|
||||
id,
|
||||
content: `获取记录 ${id} 详情失败`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const record = recordRes.data;
|
||||
|
||||
// 检查启动时间是否大于今天
|
||||
const startTime = record.effectiveAt;
|
||||
const today = dayjs().startOf('day');
|
||||
const startDate = dayjs(startTime).startOf('day');
|
||||
|
||||
if (startDate.isAfter(today)) {
|
||||
messages.push({
|
||||
id,
|
||||
content: `记录 ${id} 的启动时间是 ${dayjs(startTime).format('YYYY-MM-DD HH:mm:ss')},还没到今天呢,到时候再做吧`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
// 计算下一次运行时间
|
||||
// 1. 知道当前时间
|
||||
// 2. 知道任务类型,如果是每日,则加一天;如果是每周,则加七天;如果是每月,则加一个月,如果是每年农历,需要转为新的,如果是其他,需要智能判断
|
||||
// 3. 更新记录
|
||||
const strTime = (time: string | Date) => {
|
||||
return dayjs(time).format('YYYY-MM-DD HH:mm:ss');
|
||||
}
|
||||
const currentTime = strTime(new Date().toISOString());
|
||||
const title = record.title || '无标题';
|
||||
const isLuar = record.type?.includes?.('农历') || title.includes('农历');
|
||||
let summay = record.summary || '无';
|
||||
if (summay.length > 200) {
|
||||
summay = summay.substring(0, 200) + '...';
|
||||
}
|
||||
const prompt = record.prompt || '';
|
||||
const type = record.type || '';
|
||||
const content = `上一次执行的时间是${strTime(startTime)},当前时间是${currentTime},请帮我计算下一次的运行时间,如果时间不存在,默认在8点启动。
|
||||
${prompt ? `这是我给你的提示词,帮你更好地理解我的需求:${prompt}` : ''}
|
||||
|
||||
相关资料是
|
||||
任务:${record.title}
|
||||
总结:${summay}
|
||||
类型: ${type}
|
||||
`
|
||||
const ai = useContextKey('ai');
|
||||
await ai.chat([
|
||||
{ role: 'system', content: `你是一个时间计算专家,擅长根据任务类型和时间计算下一次运行时间。只返回我对应的日期的结果,格式是:YYYY-MM-DD HH:mm:ss。如果类型是每日,则加一天;如果是每周,则加七天;如果是每月,则加一个月,如果是每年农历,需要转为新的,如果是其他,需要智能判断` },
|
||||
{ role: 'user', content }
|
||||
])
|
||||
let nextTime = ai.responseText?.trim();
|
||||
try {
|
||||
// 判断返回的时间是否可以格式化
|
||||
if (nextTime && dayjs(nextTime).isValid()) {
|
||||
const time = dayjs(nextTime);
|
||||
if (isLuar) {
|
||||
const festival = createLunarDate({ year: time.year(), month: time.month() + 1, day: time.date() });
|
||||
const { date } = toGregorian(festival);
|
||||
nextTime = dayjs(date).toISOString();
|
||||
} else {
|
||||
nextTime = time.toISOString();
|
||||
}
|
||||
} else {
|
||||
messages.push({
|
||||
id,
|
||||
content: `记录 ${id} 的任务 "${record.title}",AI 返回的时间格式无效,无法格式化,返回内容是:${ai.responseText}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
messages.push({
|
||||
id,
|
||||
content: `记录 ${id} 的任务 "${record.title}",AI 返回结果解析失败,返回内容是:${ai.responseText}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const update = await updateLifeEffectiveAt(id, nextTime);
|
||||
if (update.code !== 200) {
|
||||
messages.push({
|
||||
id,
|
||||
content: `记录 ${id} 的任务 "${record.title}",更新记录失败`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const msg = {
|
||||
id,
|
||||
nextTime,
|
||||
showCNTime: dayjs(nextTime).format('YYYY-MM-DD HH:mm:ss'),
|
||||
content: `任务 "${record.title}" 已标记为完成。下一次运行时间是 ${dayjs(nextTime).format('YYYY-MM-DD HH:mm:ss')}`
|
||||
};
|
||||
messages.push(msg);
|
||||
}
|
||||
|
||||
for (const _id of ids) {
|
||||
await changeItem(String(_id));
|
||||
}
|
||||
ctx.body = {
|
||||
content: messages.map(m => m.content).join('\n'),
|
||||
list: messages
|
||||
};
|
||||
|
||||
}).addTo(app);
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CNB } from '@kevisual/cnb'
|
||||
import { CNB } from '@kevisual/cnb/src/index.ts'
|
||||
import { UserModel } from '../../../auth/index.ts';
|
||||
import { CustomError } from '@kevisual/router';
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useConfig, useContextKey } from '@kevisual/context';
|
||||
import { Query } from '@kevisual/query';
|
||||
import util from 'node:util';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
import { QueryLoginNode } from '@kevisual/api/login-node'
|
||||
dotenv.config();
|
||||
export {
|
||||
app,
|
||||
@@ -12,7 +12,6 @@ export {
|
||||
}
|
||||
export const config = useConfig();
|
||||
|
||||
export const token = config.KEVISUAL_TOKEN || '';
|
||||
export const cnbToken = config.CNB_TOKEN || '';
|
||||
|
||||
export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
@@ -37,4 +36,10 @@ export const exit = (code = 0) => {
|
||||
|
||||
export const query = new Query({
|
||||
url: 'https://kevisual.cn/api/router'
|
||||
// url: 'https://kevisual.xiongxiao.me/api/router'
|
||||
})
|
||||
|
||||
export const queryLogin = new QueryLoginNode({ query });
|
||||
|
||||
export const token = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImtpZC1rZXktMSJ9.eyJzdWIiOiJ1c2VyOjBlNzAwZGM4LTkwZGQtNDFiNy05MWRkLTMzNmVhNTFkZTNkMiIsIm5hbWUiOiJyb290IiwiZXhwIjoxNzczMjM1NzM0LCJpc3MiOiJodHRwczovL2NvbnZleC5rZXZpc3VhbC5jbiIsImlhdCI6MTc3MzE0OTMzNCwiYXVkIjoiY29udmV4LWFwcCJ9.Zj3bepCCKnVGgXoOnmmdkM-2u0qiT2V-bLhI-0C1a-YX9-ZlcQP2W_1rYN_D2kaaL5BPduvKhoY1hJzM5UwxRYLc-tYr2oBU4fwEyHc3bn-M8p0spX2-Tbie7CN_WbBszZ9KGePNKCveWmx5rCc14YhfUiIvczviU7WP728yFsaHJ29sVu3FJqd3ezMSkdwwPtlwCBtOhuE3nyqPdWP6nRZHkSSbAZDu5jUb_-3TqGjI2cHVZwChfcIVNwdjTeQrj2KMMQ2NdXBim01PZcolr3wqNwpSsm4bN4IVyB5RmwCw7gzHyYSOSZ1bnE8kc53M0KANDSLBFynKUXzNQJ-Wmg'
|
||||
// console.log('test config', token);
|
||||
36
src/test/flowme.ts
Normal file
36
src/test/flowme.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { queryLogin, app, token, showMore } from './common.ts'
|
||||
|
||||
|
||||
// const rest = await app.run({
|
||||
// path: 'flowme-life',
|
||||
// key: 'today',
|
||||
// // @ts-ignore
|
||||
// token: token,
|
||||
// })
|
||||
|
||||
// console.log('flowme-life today', rest)
|
||||
|
||||
const updateId = '8c63cb7a-ff6d-463b-b210-6311ee12ed46'
|
||||
|
||||
// const updateRest = await app.run({
|
||||
// path: 'flowme-life',
|
||||
// key: 'done',
|
||||
// // @ts-ignore
|
||||
// token: token,
|
||||
// payload: {
|
||||
// id: updateId,
|
||||
// }
|
||||
// })
|
||||
// console.log('flowme-life done', updateRest)
|
||||
|
||||
const chatRes = await app.run({
|
||||
path: 'flowme-life',
|
||||
key: 'chat',
|
||||
// @ts-ignore
|
||||
token: token,
|
||||
payload: {
|
||||
// question: '帮我查询一下今天的待办事项'
|
||||
// question: '帮我查询一下今天的待办事项, 然后帮我把键盘充电的待办标记为完成',
|
||||
}
|
||||
})
|
||||
console.log('flowme-life chat', showMore(chatRes))
|
||||
Reference in New Issue
Block a user