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:
2026-03-11 01:44:29 +08:00
parent 027cbecab6
commit 66a19139b7
22 changed files with 5190 additions and 676 deletions

View File

@@ -41,14 +41,14 @@
], ],
"license": "UNLICENSED", "license": "UNLICENSED",
"dependencies": { "dependencies": {
"@kevisual/ai": "^0.0.26", "@kevisual/ai": "^0.0.27",
"@kevisual/auth": "^2.0.3", "@kevisual/auth": "^2.0.3",
"@kevisual/js-filter": "^0.0.5", "@kevisual/js-filter": "^0.0.6",
"@kevisual/query": "^0.0.52", "@kevisual/query": "^0.0.53",
"@types/busboy": "^1.5.4", "@types/busboy": "^1.5.4",
"@types/send": "^1.2.1", "@types/send": "^1.2.1",
"@types/ws": "^8.18.1", "@types/ws": "^8.18.1",
"bullmq": "^5.70.2", "bullmq": "^5.70.4",
"busboy": "^1.6.0", "busboy": "^1.6.0",
"drizzle-kit": "^0.31.9", "drizzle-kit": "^0.31.9",
"drizzle-orm": "^0.45.1", "drizzle-orm": "^0.45.1",
@@ -58,26 +58,28 @@
"xml2js": "^0.6.2" "xml2js": "^0.6.2"
}, },
"devDependencies": { "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/api": "^0.0.62",
"@kevisual/cnb": "^0.0.33", "@kevisual/cnb": "^0.0.42",
"@kevisual/context": "^0.0.8", "@kevisual/context": "^0.0.8",
"@kevisual/convex": "^0.0.4", "@kevisual/convex": "^0.0.6",
"@kevisual/local-app-manager": "0.1.32", "@kevisual/local-app-manager": "0.1.32",
"@kevisual/logger": "^0.0.4", "@kevisual/logger": "^0.0.4",
"@kevisual/oss": "0.0.20", "@kevisual/oss": "0.0.20",
"@kevisual/permission": "^0.0.4", "@kevisual/permission": "^0.0.4",
"@kevisual/router": "0.0.85", "@kevisual/router": "0.1.0",
"@kevisual/types": "^0.0.12", "@kevisual/types": "^0.0.12",
"@kevisual/use-config": "^1.0.30", "@kevisual/use-config": "^1.0.30",
"@types/archiver": "^7.0.0", "@types/archiver": "^7.0.0",
"@types/bun": "^1.3.10", "@types/bun": "^1.3.10",
"@types/crypto-js": "^4.2.2", "@types/crypto-js": "^4.2.2",
"@types/jsonwebtoken": "^9.0.10", "@types/jsonwebtoken": "^9.0.10",
"@types/node": "^25.3.3", "@types/node": "^25.4.0",
"@types/pg": "^8.18.0", "@types/pg": "^8.18.0",
"@types/semver": "^7.7.1", "@types/semver": "^7.7.1",
"@types/xml2js": "^0.4.14", "@types/xml2js": "^0.4.14",
"ai": "^6.0.116",
"archiver": "^7.0.1", "archiver": "^7.0.1",
"convex": "^1.32.0", "convex": "^1.32.0",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
@@ -87,6 +89,7 @@
"es-toolkit": "^1.45.1", "es-toolkit": "^1.45.1",
"ioredis": "^5.10.0", "ioredis": "^5.10.0",
"jsonwebtoken": "^9.0.3", "jsonwebtoken": "^9.0.3",
"lunar": "^2.0.0",
"nanoid": "^5.1.6", "nanoid": "^5.1.6",
"p-queue": "^9.1.0", "p-queue": "^9.1.0",
"pg": "^8.20.0", "pg": "^8.20.0",
@@ -99,7 +102,7 @@
"picomatch": "^4.0.2", "picomatch": "^4.0.2",
"ioredis": "^5.9.3" "ioredis": "^5.9.3"
}, },
"packageManager": "pnpm@10.30.3", "packageManager": "pnpm@10.32.0",
"workspaces": [ "workspaces": [
"wxmsg" "wxmsg"
] ]

1291
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,9 @@ import { BailianProvider } from '@kevisual/ai';
import * as schema from './db/schema.ts'; import * as schema from './db/schema.ts';
import { config } from './modules/config.ts' import { config } from './modules/config.ts'
import { db } from './modules/db.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 router = useContextKey('router', () => new SimpleRouter());
export const runtime = useContextKey('runtime', () => { export const runtime = useContextKey('runtime', () => {
return { return {
@@ -44,6 +46,24 @@ export const ai = useContextKey('ai', () => {
export { schema }; 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 const cnb = createOpenAICompatible({
export { convexApi }; 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',
}

View 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");

File diff suppressed because it is too large Load Diff

View File

@@ -15,6 +15,13 @@
"when": 1767070768620, "when": 1767070768620,
"tag": "0001_solid_nocturne", "tag": "0001_solid_nocturne",
"breakpoints": true "breakpoints": true
},
{
"idx": 2,
"version": "7",
"when": 1773148571509,
"tag": "0002_loving_lyja",
"breakpoints": true
} }
] ]
} }

View File

@@ -12,7 +12,13 @@ export const life = pgTable("flowme_life", {
link: text('link').default(''), link: text('link').default(''),
data: jsonb().default({}), data: jsonb().default({}),
effectiveAt: text('effectiveAt').default(''), effectiveAt: timestamp('effectiveAt', { withTimezone: true }),
/**
* 智能,
* 每年农历
* 备忘
* 归档
*/
type: text('type').default(''), type: text('type').default(''),
prompt: text('prompt').default(''), prompt: text('prompt').default(''),
taskType: text('taskType').default(''), taskType: text('taskType').default(''),

View 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 });
}

View File

@@ -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;

View File

@@ -109,7 +109,7 @@ export const pipeProxyReq = async (req: http.IncomingMessage, proxyReq: http.Cli
const bunRequest = req.bun.request; const bunRequest = req.bun.request;
const contentType = req.headers['content-type'] || ''; const contentType = req.headers['content-type'] || '';
if (contentType.includes('multipart/form-data')) { if (contentType.includes('multipart/form-data')) {
console.log('Processing multipart/form-data'); // console.log('Processing multipart/form-data');
const arrayBuffer = await bunRequest.arrayBuffer(); const arrayBuffer = await bunRequest.arrayBuffer();
// 设置请求头(在写入数据之前) // 设置请求头(在写入数据之前)
@@ -123,7 +123,6 @@ export const pipeProxyReq = async (req: http.IncomingMessage, proxyReq: http.Cli
proxyReq.end(); proxyReq.end();
return; return;
} }
console.log('Bun pipeProxyReq content-type', contentType);
// @ts-ignore // @ts-ignore
const bodyString = req.body; const bodyString = req.body;
bodyString && proxyReq.write(bodyString); bodyString && proxyReq.write(bodyString);

View File

@@ -1,4 +1,3 @@
import { convex, convexApi } from '@/app.ts';
import { User } from '@/models/user.ts'; import { User } from '@/models/user.ts';
import { omit } from 'es-toolkit'; import { omit } from 'es-toolkit';
import { IncomingMessage, ServerResponse } from 'http'; import { IncomingMessage, ServerResponse } from 'http';

View File

@@ -3,10 +3,12 @@ import { router } from './router.ts';
import { handleRequest as PageProxy } from './page-proxy.ts'; import { handleRequest as PageProxy } from './page-proxy.ts';
import './routes/jwks.ts' import './routes/jwks.ts'
import './routes/ai/openai.ts'
const simpleAppsPrefixs = [ const simpleAppsPrefixs = [
"/api/wxmsg", "/api/wxmsg",
"/api/convex/", "/api/convex/",
"/api/chat/completions"
]; ];

View 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);
});

View 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);
});

View 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或messagesquestion是用户的提问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);

View File

@@ -1 +1,3 @@
import './list.ts' import './list.ts'
import './today.ts'
import './chat.ts'

View 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}` };
}
}

View File

@@ -1,6 +1,7 @@
import { desc, eq, count, or, like, and } from 'drizzle-orm'; import { desc, eq, count, or, like, and } from 'drizzle-orm';
import { schema, app, db } from '@/app.ts' import { schema, app, db } from '@/app.ts'
import z from 'zod'; import z from 'zod';
import dayjs from 'dayjs';
app.route({ app.route({
path: 'flowme-life', path: 'flowme-life',
key: 'list', key: 'list',
@@ -72,7 +73,7 @@ app.route({
link: z.string().describe('链接').optional(), link: z.string().describe('链接').optional(),
data: z.record(z.string(), z.any()).describe('数据').optional(), data: z.record(z.string(), z.any()).describe('数据').optional(),
effectiveAt: z.string().describe('生效日期').optional(), effectiveAt: z.string().describe('生效日期').optional(),
type: z.string().describe('类型').optional(), type: z.string().describe('类型: 智能, 每年农历, 备忘, 归档等.默认智能').optional(),
prompt: z.string().describe('提示词').optional(), prompt: z.string().describe('提示词').optional(),
taskType: z.string().describe('任务类型').optional(), taskType: z.string().describe('任务类型').optional(),
taskResult: z.record(z.string(), z.any()).describe('任务结果').optional(), taskResult: z.record(z.string(), z.any()).describe('任务结果').optional(),
@@ -82,6 +83,11 @@ app.route({
}).define(async (ctx) => { }).define(async (ctx) => {
const { uid, updatedAt, createdAt, ...rest } = ctx.query.data || {}; const { uid, updatedAt, createdAt, ...rest } = ctx.query.data || {};
const tokenUser = ctx.state.tokenUser; 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({ const lifeItem = await db.insert(schema.life).values({
title: rest.title || '', title: rest.title || '',
summary: rest.summary || '', summary: rest.summary || '',
@@ -90,7 +96,7 @@ app.route({
link: rest.link || '', link: rest.link || '',
data: rest.data || {}, data: rest.data || {},
effectiveAt: rest.effectiveAt || '', effectiveAt: rest.effectiveAt || '',
type: rest.type || '', type: rest.type || '智能',
prompt: rest.prompt || '', prompt: rest.prompt || '',
taskType: rest.taskType || '', taskType: rest.taskType || '',
taskResult: rest.taskResult || {}, taskResult: rest.taskResult || {},
@@ -103,7 +109,7 @@ app.route({
path: 'flowme-life', path: 'flowme-life',
key: 'update', key: 'update',
middleware: ['auth'], middleware: ['auth'],
description: '更新一个 flowme-life', description: '更新一个 flowme-life 的数据',
metadata: { metadata: {
args: { args: {
data: z.object({ data: z.object({
@@ -135,6 +141,11 @@ app.route({
if (existing[0].uid !== tokenUser.id) { if (existing[0].uid !== tokenUser.id) {
ctx.throw(403, '没有权限更新该 flowme-life'); 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({ const lifeItem = await db.update(schema.life).set({
title: rest.title, title: rest.title,
summary: rest.summary, summary: rest.summary,
@@ -155,17 +166,15 @@ app.route({
path: 'flowme-life', path: 'flowme-life',
key: 'delete', key: 'delete',
middleware: ['auth'], middleware: ['auth'],
description: '删除 flowme-life', description: '删除单个 flowme-life, 参数: id 必填',
metadata: { metadata: {
args: { args: {
data: z.object({
id: z.string().describe('ID'), id: z.string().describe('ID'),
})
} }
} }
}).define(async (ctx) => { }).define(async (ctx) => {
const tokenUser = ctx.state.tokenUser; const tokenUser = ctx.state.tokenUser;
const { id } = ctx.query.data || {}; const { id } = ctx.query || {};
if (!id) { if (!id) {
ctx.throw(400, 'id 参数缺失'); ctx.throw(400, 'id 参数缺失');
} }
@@ -184,10 +193,15 @@ app.route({
path: 'flowme-life', path: 'flowme-life',
key: 'get', key: 'get',
middleware: ['auth'], middleware: ['auth'],
description: '获取单个 flowme-life, 参数: data.id 必填', description: '获取单个 flowme-life, 参数: id 必填',
metadata: {
args: {
id: z.string().describe('ID'),
}
}
}).define(async (ctx) => { }).define(async (ctx) => {
const tokenUser = ctx.state.tokenUser; const tokenUser = ctx.state.tokenUser;
const { id } = ctx.query.data || {}; const { id } = ctx.query || {};
if (!id) { if (!id) {
ctx.throw(400, 'id 参数缺失'); ctx.throw(400, 'id 参数缺失');
} }

View 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: `完成某件事情然后判断下一次运行时间。参数是idstring数据类型是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);

View File

@@ -1,4 +1,4 @@
import { CNB } from '@kevisual/cnb' import { CNB } from '@kevisual/cnb/src/index.ts'
import { UserModel } from '../../../auth/index.ts'; import { UserModel } from '../../../auth/index.ts';
import { CustomError } from '@kevisual/router'; import { CustomError } from '@kevisual/router';

View File

@@ -4,7 +4,7 @@ import { useConfig, useContextKey } from '@kevisual/context';
import { Query } from '@kevisual/query'; import { Query } from '@kevisual/query';
import util from 'node:util'; import util from 'node:util';
import dotenv from 'dotenv'; import dotenv from 'dotenv';
import { QueryLoginNode } from '@kevisual/api/login-node'
dotenv.config(); dotenv.config();
export { export {
app, app,
@@ -12,7 +12,6 @@ export {
} }
export const config = useConfig(); export const config = useConfig();
export const token = config.KEVISUAL_TOKEN || '';
export const cnbToken = config.CNB_TOKEN || ''; export const cnbToken = config.CNB_TOKEN || '';
export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); 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({ export const query = new Query({
url: 'https://kevisual.cn/api/router' 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
View 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))