LobeChat deep customization: welcome tagline / example prompts / default agent / model-admin lock / enterprise identity bridge
E2E CI / Check Duplicate Run (push) Failing after 3s
🔄 Branch Synchronization / sync-branches (push) Failing after 3s
Test CI / Check Duplicate Run (push) Failing after 3s
E2E CI / Test Web App (push) Has been skipped
Test CI / Test App (shard 1/3) (push) Has been skipped
Test CI / Test Packages (push) Has been skipped
Test CI / Test App (shard 3/3) (push) Has been skipped
Test CI / Test App (shard 2/3) (push) Has been skipped
Test CI / Test Database (push) Has been skipped
Test CI / Test Desktop App (push) Has been skipped
Test CI / Merge and Upload App Coverage (push) Has been skipped
E2E CI / Check Duplicate Run (push) Failing after 3s
🔄 Branch Synchronization / sync-branches (push) Failing after 3s
Test CI / Check Duplicate Run (push) Failing after 3s
E2E CI / Test Web App (push) Has been skipped
Test CI / Test App (shard 1/3) (push) Has been skipped
Test CI / Test Packages (push) Has been skipped
Test CI / Test App (shard 3/3) (push) Has been skipped
Test CI / Test App (shard 2/3) (push) Has been skipped
Test CI / Test Database (push) Has been skipped
Test CI / Test Desktop App (push) Has been skipped
Test CI / Merge and Upload App Coverage (push) Has been skipped
This commit is contained in:
+87
-87
@@ -51,50 +51,56 @@
|
||||
"hotPaths": [
|
||||
{
|
||||
"path": "gateway/scripts/acceptance.sh",
|
||||
"accessCount": 35,
|
||||
"lastAccessed": 1776746745293,
|
||||
"accessCount": 37,
|
||||
"lastAccessed": 1776762981224,
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"path": "gateway/src/server.ts",
|
||||
"accessCount": 24,
|
||||
"lastAccessed": 1776746523169,
|
||||
"accessCount": 29,
|
||||
"lastAccessed": 1776762902077,
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"path": "src",
|
||||
"accessCount": 20,
|
||||
"lastAccessed": 1776746567860,
|
||||
"accessCount": 25,
|
||||
"lastAccessed": 1776772483106,
|
||||
"type": "directory"
|
||||
},
|
||||
{
|
||||
"path": "gateway/prisma/seed.ts",
|
||||
"accessCount": 20,
|
||||
"lastAccessed": 1776763220682,
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"path": "gateway/src/core/gateway.ts",
|
||||
"accessCount": 19,
|
||||
"lastAccessed": 1776690611538,
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"path": "docker-compose.yml",
|
||||
"accessCount": 19,
|
||||
"lastAccessed": 1776773523884,
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"path": "gateway/src/env.ts",
|
||||
"accessCount": 17,
|
||||
"lastAccessed": 1776746557069,
|
||||
"accessCount": 18,
|
||||
"lastAccessed": 1776762725387,
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"path": "",
|
||||
"accessCount": 15,
|
||||
"lastAccessed": 1776746558851,
|
||||
"accessCount": 18,
|
||||
"lastAccessed": 1776772696282,
|
||||
"type": "directory"
|
||||
},
|
||||
{
|
||||
"path": "docker-compose.yml",
|
||||
"accessCount": 14,
|
||||
"lastAccessed": 1776747021059,
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"path": "gateway/src/auth/middleware.ts",
|
||||
"accessCount": 13,
|
||||
"lastAccessed": 1776746524853,
|
||||
"accessCount": 14,
|
||||
"lastAccessed": 1776762725289,
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
@@ -105,8 +111,32 @@
|
||||
},
|
||||
{
|
||||
"path": "gateway/src/core/capabilities.ts",
|
||||
"accessCount": 9,
|
||||
"lastAccessed": 1776690611653,
|
||||
"accessCount": 12,
|
||||
"lastAccessed": 1776762816556,
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"path": "gateway/src/routes/admin/tools.ts",
|
||||
"accessCount": 12,
|
||||
"lastAccessed": 1776762925506,
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"path": ".env",
|
||||
"accessCount": 11,
|
||||
"lastAccessed": 1776772939687,
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"path": "README.md",
|
||||
"accessCount": 10,
|
||||
"lastAccessed": 1776748224192,
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"path": "gateway/src/core/filter.ts",
|
||||
"accessCount": 10,
|
||||
"lastAccessed": 1776762756452,
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
@@ -116,45 +146,15 @@
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"path": "gateway/src/core/filter.ts",
|
||||
"path": "gateway/package.json",
|
||||
"accessCount": 9,
|
||||
"lastAccessed": 1776746584958,
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"path": "gateway/prisma/seed.ts",
|
||||
"accessCount": 9,
|
||||
"lastAccessed": 1776746639419,
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"path": "gateway/src/routes/admin/tools.ts",
|
||||
"accessCount": 9,
|
||||
"lastAccessed": 1776746645871,
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"path": "README.md",
|
||||
"accessCount": 9,
|
||||
"lastAccessed": 1776746769787,
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"path": ".env",
|
||||
"accessCount": 9,
|
||||
"lastAccessed": 1776747010786,
|
||||
"lastAccessed": 1776762710635,
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"path": "gateway/src/routes/admin/identityMap.ts",
|
||||
"accessCount": 8,
|
||||
"lastAccessed": 1776687700868,
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"path": "gateway/package.json",
|
||||
"accessCount": 8,
|
||||
"lastAccessed": 1776690396624,
|
||||
"accessCount": 9,
|
||||
"lastAccessed": 1776762742659,
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
@@ -177,14 +177,20 @@
|
||||
},
|
||||
{
|
||||
"path": "gateway/src/routes/lobechatPlugin.ts",
|
||||
"accessCount": 7,
|
||||
"lastAccessed": 1776687652467,
|
||||
"accessCount": 8,
|
||||
"lastAccessed": 1776772711425,
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"path": "gateway/src/auth/devAuth.ts",
|
||||
"accessCount": 8,
|
||||
"lastAccessed": 1776773039528,
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"path": "gateway/prisma/schema.prisma",
|
||||
"accessCount": 7,
|
||||
"lastAccessed": 1776746816210,
|
||||
"lastAccessed": 1776773457701,
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
@@ -205,6 +211,12 @@
|
||||
"lastAccessed": 1776746550016,
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"path": "gateway/src/routes/capabilities.ts",
|
||||
"accessCount": 5,
|
||||
"lastAccessed": 1776762824807,
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"path": "lobechat-plugin-manifest/README.md",
|
||||
"accessCount": 4,
|
||||
@@ -241,12 +253,6 @@
|
||||
"lastAccessed": 1776683085106,
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"path": "gateway/src/routes/capabilities.ts",
|
||||
"accessCount": 3,
|
||||
"lastAccessed": 1776684617954,
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"path": "gateway/src/auth/casdoor.ts",
|
||||
"accessCount": 3,
|
||||
@@ -265,36 +271,42 @@
|
||||
"lastAccessed": 1776687022651,
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"path": "gateway/prisma/schema.prisma",
|
||||
"accessCount": 3,
|
||||
"lastAccessed": 1776746530468,
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"path": "gateway/src/tools/index.ts",
|
||||
"accessCount": 3,
|
||||
"lastAccessed": 1776746541218,
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"path": "gateway/Dockerfile",
|
||||
"accessCount": 3,
|
||||
"lastAccessed": 1776763028735,
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"path": "docker-compose/production/grafana/docker-compose.yml",
|
||||
"accessCount": 2,
|
||||
"lastAccessed": 1776681245580,
|
||||
"type": "directory"
|
||||
},
|
||||
{
|
||||
"path": "gateway/Dockerfile",
|
||||
"accessCount": 2,
|
||||
"lastAccessed": 1776684619369,
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"path": "docker-compose.gateway.yml",
|
||||
"accessCount": 2,
|
||||
"lastAccessed": 1776690209005,
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"path": "db-init/01-create-dbs.sql",
|
||||
"accessCount": 2,
|
||||
"lastAccessed": 1776772673608,
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"path": "src/config/featureFlags/schema.ts",
|
||||
"accessCount": 2,
|
||||
"lastAccessed": 1776773481021,
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"path": ".env.example.gateway",
|
||||
"accessCount": 1,
|
||||
@@ -319,18 +331,6 @@
|
||||
"lastAccessed": 1776681185251,
|
||||
"type": "directory"
|
||||
},
|
||||
{
|
||||
"path": "db-init/01-create-dbs.sql",
|
||||
"accessCount": 1,
|
||||
"lastAccessed": 1776681258116,
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"path": "src/config/featureFlags/schema.ts",
|
||||
"accessCount": 1,
|
||||
"lastAccessed": 1776681889494,
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"path": "src/config/featureFlags/index.ts",
|
||||
"accessCount": 1,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"session_id": "427b0f1b-876b-4ce1-be27-adbaf9f7bd35",
|
||||
"ended_at": "2026-04-21T03:33:58.985Z",
|
||||
"ended_at": "2026-04-21T05:28:52.414Z",
|
||||
"reason": "other",
|
||||
"agents_spawned": 1,
|
||||
"agents_completed": 1,
|
||||
"agents_spawned": 5,
|
||||
"agents_completed": 5,
|
||||
"modes_used": []
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{"t":0,"agent":"a113e02","agent_type":"executor","event":"agent_start","parent_mode":"none"}
|
||||
{"t":0,"agent":"a89bbb4","agent_type":"executor","event":"agent_start","parent_mode":"none"}
|
||||
{"t":0,"agent":"a89bbb4","agent_type":"executor","event":"agent_stop","success":true,"duration_ms":310029}
|
||||
{"t":0,"agent":"a113e02","agent_type":"executor","event":"agent_stop","success":true,"duration_ms":376428}
|
||||
{"t":0,"agent":"a9905af","agent_type":"executor","event":"agent_start","parent_mode":"none"}
|
||||
{"t":0,"agent":"a9905af","agent_type":"executor","event":"agent_stop","success":true,"duration_ms":467901}
|
||||
{"t":0,"agent":"a689e0e","agent_type":"executor","event":"agent_start","parent_mode":"none"}
|
||||
{"t":0,"agent":"a689e0e","agent_type":"executor","event":"agent_stop","success":true,"duration_ms":186848}
|
||||
{"t":0,"agent":"a7c24ef","agent_type":"executor","event":"agent_start","parent_mode":"none"}
|
||||
{"t":0,"agent":"a7c24ef","agent_type":"executor","event":"agent_stop","success":true,"duration_ms":318391}
|
||||
@@ -1,10 +0,0 @@
|
||||
{"t":0,"agent":"a7228f4","agent_type":"executor","event":"agent_start","parent_mode":"none"}
|
||||
{"t":0,"agent":"a7228f4","agent_type":"executor","event":"agent_stop","success":true,"duration_ms":439201}
|
||||
{"t":0,"agent":"a8b5b0a","agent_type":"executor","event":"agent_start","parent_mode":"none"}
|
||||
{"t":0,"agent":"a9bdbd9","agent_type":"executor","event":"agent_start","parent_mode":"none"}
|
||||
{"t":0,"agent":"a7a194c","agent_type":"executor","event":"agent_start","parent_mode":"none"}
|
||||
{"t":0,"agent":"a68a052","agent_type":"executor","event":"agent_start","parent_mode":"none"}
|
||||
{"t":0,"agent":"a9bdbd9","agent_type":"executor","event":"agent_stop","success":true,"duration_ms":192255}
|
||||
{"t":0,"agent":"a8b5b0a","agent_type":"executor","event":"agent_stop","success":true,"duration_ms":298745}
|
||||
{"t":0,"agent":"a7a194c","agent_type":"executor","event":"agent_stop","success":true,"duration_ms":405255}
|
||||
{"t":0,"agent":"a68a052","agent_type":"executor","event":"agent_stop","success":true,"duration_ms":386757}
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"lastSentAt": "2026-04-21T04:52:16.780Z"
|
||||
"lastSentAt": "2026-04-21T12:33:30.508Z"
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"tool_name": "Bash",
|
||||
"tool_input_preview": "{\"command\":\"grep -q \\\"^\\\\.env$\\\" .gitignore && echo \\\".env ignored\\\" || echo \\\"WARN .env NOT ignored\\\"\\ngrep -c \\\"^CASDOOR_ADMIN_SECRET=\\\" .env 2>/dev/null\\n# append (or update) without echoing the va...",
|
||||
"error": "Exit code 1\n.env ignored\n0\nTraceback (most recent call last):\n File \"<stdin>\", line 2, in <module>\n File \"<frozen os>\", line 709, in __getitem__\nKeyError: 'S'",
|
||||
"timestamp": "2026-04-21T04:52:00.827Z",
|
||||
"tool_input_preview": "{\"command\":\"date\\necho \\\"=== current build step ===\\\"; tail -3 /tmp/lobechat-build.log 2>&1\\necho \\\"=== build age ===\\\"; pgrep -fl \\\"com.docker.build\\\" | head -3\\nps -o etimes=,comm= -p $(pgrep -f \\\"c...",
|
||||
"error": "Exit code 1\nTue Apr 21 20:23:36 CST 2026\n=== current build step ===\n#36 resolving provenance for metadata file\n#36 DONE 0.0s\n Image lobechat-lobechat Built \n=== build age ===\n10856 /Applications/Docker.app/Contents/MacOS/com.docker.build --log-no-timestamp\n48702 bash -c \ncd /Users/gongzhiyong/go/lobechat\nps: etimes: keyword not found\n/Applications/Docker.app/Contents/MacOS/com.docker.build",
|
||||
"timestamp": "2026-04-21T12:23:36.620Z",
|
||||
"retry_count": 1
|
||||
}
|
||||
@@ -1,141 +1,107 @@
|
||||
{
|
||||
"updatedAt": "2026-04-21T04:49:00.597Z",
|
||||
"updatedAt": "2026-04-21T12:15:55.025Z",
|
||||
"missions": [
|
||||
{
|
||||
"id": "session:427b0f1b-876b-4ce1-be27-adbaf9f7bd35:none",
|
||||
"id": "session:17a7f260-0c2b-4628-b576-00051879584a:none",
|
||||
"source": "session",
|
||||
"name": "none",
|
||||
"objective": "Session mission",
|
||||
"createdAt": "2026-04-21T03:48:43.130Z",
|
||||
"updatedAt": "2026-04-21T04:49:00.597Z",
|
||||
"createdAt": "2026-04-21T09:11:40.540Z",
|
||||
"updatedAt": "2026-04-21T12:15:55.025Z",
|
||||
"status": "done",
|
||||
"workerCount": 6,
|
||||
"workerCount": 5,
|
||||
"taskCounts": {
|
||||
"total": 6,
|
||||
"total": 5,
|
||||
"pending": 0,
|
||||
"blocked": 0,
|
||||
"inProgress": 0,
|
||||
"completed": 6,
|
||||
"completed": 5,
|
||||
"failed": 0
|
||||
},
|
||||
"agents": [
|
||||
{
|
||||
"name": "executor:a02dceb",
|
||||
"name": "executor:a113e02",
|
||||
"role": "executor",
|
||||
"ownership": "a02dceb4096c12fed",
|
||||
"ownership": "a113e029b71f81ee2",
|
||||
"status": "done",
|
||||
"currentStep": null,
|
||||
"latestUpdate": "completed",
|
||||
"completedSummary": null,
|
||||
"updatedAt": "2026-04-21T03:52:14.768Z"
|
||||
"updatedAt": "2026-04-21T09:17:56.968Z"
|
||||
},
|
||||
{
|
||||
"name": "executor:a7228f4",
|
||||
"name": "executor:a89bbb4",
|
||||
"role": "executor",
|
||||
"ownership": "a7228f431ca80c4c2",
|
||||
"ownership": "a89bbb4ebd56ed319",
|
||||
"status": "done",
|
||||
"currentStep": null,
|
||||
"latestUpdate": "completed",
|
||||
"completedSummary": null,
|
||||
"updatedAt": "2026-04-21T04:28:00.414Z"
|
||||
"updatedAt": "2026-04-21T09:17:15.300Z"
|
||||
},
|
||||
{
|
||||
"name": "executor:a8b5b0a",
|
||||
"name": "executor:a9905af",
|
||||
"role": "executor",
|
||||
"ownership": "a8b5b0a64a894f1c8",
|
||||
"ownership": "a9905affb95123dd1",
|
||||
"status": "done",
|
||||
"currentStep": null,
|
||||
"latestUpdate": "completed",
|
||||
"completedSummary": null,
|
||||
"updatedAt": "2026-04-21T04:46:09.197Z"
|
||||
"updatedAt": "2026-04-21T12:05:32.432Z"
|
||||
},
|
||||
{
|
||||
"name": "executor:a9bdbd9",
|
||||
"name": "executor:a689e0e",
|
||||
"role": "executor",
|
||||
"ownership": "a9bdbd9f83692ba51",
|
||||
"ownership": "a689e0edc18c030c7",
|
||||
"status": "done",
|
||||
"currentStep": null,
|
||||
"latestUpdate": "completed",
|
||||
"completedSummary": null,
|
||||
"updatedAt": "2026-04-21T04:44:47.329Z"
|
||||
"updatedAt": "2026-04-21T12:09:43.444Z"
|
||||
},
|
||||
{
|
||||
"name": "executor:a7a194c",
|
||||
"name": "executor:a7c24ef",
|
||||
"role": "executor",
|
||||
"ownership": "a7a194c1e64a029df",
|
||||
"ownership": "a7c24efc187b1b149",
|
||||
"status": "done",
|
||||
"currentStep": null,
|
||||
"latestUpdate": "completed",
|
||||
"completedSummary": null,
|
||||
"updatedAt": "2026-04-21T04:48:54.196Z"
|
||||
},
|
||||
{
|
||||
"name": "executor:a68a052",
|
||||
"role": "executor",
|
||||
"ownership": "a68a052707f8c76ad",
|
||||
"status": "done",
|
||||
"currentStep": null,
|
||||
"latestUpdate": "completed",
|
||||
"completedSummary": null,
|
||||
"updatedAt": "2026-04-21T04:49:00.597Z"
|
||||
"updatedAt": "2026-04-21T12:15:55.025Z"
|
||||
}
|
||||
],
|
||||
"timeline": [
|
||||
{
|
||||
"id": "session-start:a9bdbd9f83692ba51:2026-04-21T04:41:35.074Z",
|
||||
"at": "2026-04-21T04:41:35.074Z",
|
||||
"id": "session-start:a689e0edc18c030c7:2026-04-21T12:06:36.596Z",
|
||||
"at": "2026-04-21T12:06:36.596Z",
|
||||
"kind": "update",
|
||||
"agent": "executor:a9bdbd9",
|
||||
"detail": "started executor:a9bdbd9",
|
||||
"sourceKey": "session-start:a9bdbd9f83692ba51"
|
||||
"agent": "executor:a689e0e",
|
||||
"detail": "started executor:a689e0e",
|
||||
"sourceKey": "session-start:a689e0edc18c030c7"
|
||||
},
|
||||
{
|
||||
"id": "session-start:a7a194c1e64a029df:2026-04-21T04:42:08.941Z",
|
||||
"at": "2026-04-21T04:42:08.941Z",
|
||||
"id": "session-stop:a689e0edc18c030c7:2026-04-21T12:09:43.444Z",
|
||||
"at": "2026-04-21T12:09:43.444Z",
|
||||
"kind": "completion",
|
||||
"agent": "executor:a689e0e",
|
||||
"detail": "completed",
|
||||
"sourceKey": "session-stop:a689e0edc18c030c7"
|
||||
},
|
||||
{
|
||||
"id": "session-start:a7c24efc187b1b149:2026-04-21T12:10:36.634Z",
|
||||
"at": "2026-04-21T12:10:36.634Z",
|
||||
"kind": "update",
|
||||
"agent": "executor:a7a194c",
|
||||
"detail": "started executor:a7a194c",
|
||||
"sourceKey": "session-start:a7a194c1e64a029df"
|
||||
"agent": "executor:a7c24ef",
|
||||
"detail": "started executor:a7c24ef",
|
||||
"sourceKey": "session-start:a7c24efc187b1b149"
|
||||
},
|
||||
{
|
||||
"id": "session-start:a68a052707f8c76ad:2026-04-21T04:42:33.840Z",
|
||||
"at": "2026-04-21T04:42:33.840Z",
|
||||
"kind": "update",
|
||||
"agent": "executor:a68a052",
|
||||
"detail": "started executor:a68a052",
|
||||
"sourceKey": "session-start:a68a052707f8c76ad"
|
||||
},
|
||||
{
|
||||
"id": "session-stop:a9bdbd9f83692ba51:2026-04-21T04:44:47.329Z",
|
||||
"at": "2026-04-21T04:44:47.329Z",
|
||||
"id": "session-stop:a7c24efc187b1b149:2026-04-21T12:15:55.025Z",
|
||||
"at": "2026-04-21T12:15:55.025Z",
|
||||
"kind": "completion",
|
||||
"agent": "executor:a9bdbd9",
|
||||
"agent": "executor:a7c24ef",
|
||||
"detail": "completed",
|
||||
"sourceKey": "session-stop:a9bdbd9f83692ba51"
|
||||
},
|
||||
{
|
||||
"id": "session-stop:a8b5b0a64a894f1c8:2026-04-21T04:46:09.197Z",
|
||||
"at": "2026-04-21T04:46:09.197Z",
|
||||
"kind": "completion",
|
||||
"agent": "executor:a8b5b0a",
|
||||
"detail": "completed",
|
||||
"sourceKey": "session-stop:a8b5b0a64a894f1c8"
|
||||
},
|
||||
{
|
||||
"id": "session-stop:a7a194c1e64a029df:2026-04-21T04:48:54.196Z",
|
||||
"at": "2026-04-21T04:48:54.196Z",
|
||||
"kind": "completion",
|
||||
"agent": "executor:a7a194c",
|
||||
"detail": "completed",
|
||||
"sourceKey": "session-stop:a7a194c1e64a029df"
|
||||
},
|
||||
{
|
||||
"id": "session-stop:a68a052707f8c76ad:2026-04-21T04:49:00.597Z",
|
||||
"at": "2026-04-21T04:49:00.597Z",
|
||||
"kind": "completion",
|
||||
"agent": "executor:a68a052",
|
||||
"detail": "completed",
|
||||
"sourceKey": "session-stop:a68a052707f8c76ad"
|
||||
"sourceKey": "session-stop:a7c24efc187b1b149"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,53 +1,53 @@
|
||||
{
|
||||
"agents": [
|
||||
{
|
||||
"agent_id": "a7228f431ca80c4c2",
|
||||
"agent_id": "a113e029b71f81ee2",
|
||||
"agent_type": "oh-my-claudecode:executor",
|
||||
"started_at": "2026-04-21T04:20:41.213Z",
|
||||
"started_at": "2026-04-21T09:11:40.540Z",
|
||||
"parent_mode": "none",
|
||||
"status": "completed",
|
||||
"completed_at": "2026-04-21T04:28:00.414Z",
|
||||
"duration_ms": 439201
|
||||
"completed_at": "2026-04-21T09:17:56.968Z",
|
||||
"duration_ms": 376428
|
||||
},
|
||||
{
|
||||
"agent_id": "a8b5b0a64a894f1c8",
|
||||
"agent_id": "a89bbb4ebd56ed319",
|
||||
"agent_type": "oh-my-claudecode:executor",
|
||||
"started_at": "2026-04-21T04:41:10.452Z",
|
||||
"started_at": "2026-04-21T09:12:05.271Z",
|
||||
"parent_mode": "none",
|
||||
"status": "completed",
|
||||
"completed_at": "2026-04-21T04:46:09.197Z",
|
||||
"duration_ms": 298745
|
||||
"completed_at": "2026-04-21T09:17:15.300Z",
|
||||
"duration_ms": 310029
|
||||
},
|
||||
{
|
||||
"agent_id": "a9bdbd9f83692ba51",
|
||||
"agent_id": "a9905affb95123dd1",
|
||||
"agent_type": "oh-my-claudecode:executor",
|
||||
"started_at": "2026-04-21T04:41:35.074Z",
|
||||
"started_at": "2026-04-21T11:57:44.531Z",
|
||||
"parent_mode": "none",
|
||||
"status": "completed",
|
||||
"completed_at": "2026-04-21T04:44:47.329Z",
|
||||
"duration_ms": 192255
|
||||
"completed_at": "2026-04-21T12:05:32.432Z",
|
||||
"duration_ms": 467901
|
||||
},
|
||||
{
|
||||
"agent_id": "a7a194c1e64a029df",
|
||||
"agent_id": "a689e0edc18c030c7",
|
||||
"agent_type": "oh-my-claudecode:executor",
|
||||
"started_at": "2026-04-21T04:42:08.941Z",
|
||||
"started_at": "2026-04-21T12:06:36.596Z",
|
||||
"parent_mode": "none",
|
||||
"status": "completed",
|
||||
"completed_at": "2026-04-21T04:48:54.196Z",
|
||||
"duration_ms": 405255
|
||||
"completed_at": "2026-04-21T12:09:43.444Z",
|
||||
"duration_ms": 186848
|
||||
},
|
||||
{
|
||||
"agent_id": "a68a052707f8c76ad",
|
||||
"agent_id": "a7c24efc187b1b149",
|
||||
"agent_type": "oh-my-claudecode:executor",
|
||||
"started_at": "2026-04-21T04:42:33.840Z",
|
||||
"started_at": "2026-04-21T12:10:36.634Z",
|
||||
"parent_mode": "none",
|
||||
"status": "completed",
|
||||
"completed_at": "2026-04-21T04:49:00.597Z",
|
||||
"duration_ms": 386757
|
||||
"completed_at": "2026-04-21T12:15:55.025Z",
|
||||
"duration_ms": 318391
|
||||
}
|
||||
],
|
||||
"total_spawned": 5,
|
||||
"total_completed": 5,
|
||||
"total_failed": 0,
|
||||
"last_updated": "2026-04-21T04:49:00.700Z"
|
||||
"last_updated": "2026-04-21T12:15:55.129Z"
|
||||
}
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
#!/usr/bin/env node
|
||||
/* eslint-disable no-console */
|
||||
/**
|
||||
* Bootstrap every existing LobeChat user so the UI is immediately usable:
|
||||
* 1. Enables the built-in "azure" (Azure OpenAI) provider with encrypted keyVaults.
|
||||
* 2. Inserts one "azure/<deploymentName>" ai_models row enabled=true.
|
||||
* 3. Installs one customPlugin "enterprise-gateway" pointing at the gateway
|
||||
* manifest URL (identity-aware; each user sees only their own allowed tools).
|
||||
*
|
||||
* Runs inside the lobechat container so it can use the bundled `pg` module and
|
||||
* reach `db:5432` / `gateway:3001` on the compose network. Idempotent:
|
||||
* re-running does not duplicate or error.
|
||||
*
|
||||
* Required env (passed through by the wrapper script):
|
||||
* LOBECHAT_DATABASE_URL postgres URL for the lobechat DB
|
||||
* KEY_VAULTS_SECRET same secret LobeChat uses to decrypt key_vaults
|
||||
* AZURE_OPENAI_API_KEY Azure OpenAI key
|
||||
* AZURE_OPENAI_ENDPOINT https://<resource>.openai.azure.com (or .cognitiveservices.azure.com)
|
||||
* AZURE_OPENAI_API_VERSION e.g. 2025-04-01-preview
|
||||
* AZURE_OPENAI_DEPLOYMENT deployment name (used as the model id)
|
||||
* GATEWAY_MANIFEST_URL defaults to http://gateway:3001/api/lobechat/manifest
|
||||
*/
|
||||
|
||||
const crypto = require('crypto');
|
||||
const { Client } = require('pg');
|
||||
|
||||
const DB_URL = process.env.LOBECHAT_DATABASE_URL;
|
||||
const KEY_VAULTS_SECRET = process.env.KEY_VAULTS_SECRET;
|
||||
const AZ_KEY = process.env.AZURE_OPENAI_API_KEY;
|
||||
const AZ_ENDPOINT = process.env.AZURE_OPENAI_ENDPOINT;
|
||||
const AZ_VERSION = process.env.AZURE_OPENAI_API_VERSION || '2025-04-01-preview';
|
||||
const AZ_DEPLOY = process.env.AZURE_OPENAI_DEPLOYMENT || 'gpt-5.4';
|
||||
const MANIFEST_URL =
|
||||
process.env.GATEWAY_MANIFEST_URL || 'http://gateway:3001/api/lobechat/manifest';
|
||||
|
||||
function die(msg) {
|
||||
console.error('[bootstrap] FATAL:', msg);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!DB_URL) die('LOBECHAT_DATABASE_URL is required');
|
||||
if (!KEY_VAULTS_SECRET) die('KEY_VAULTS_SECRET is required');
|
||||
if (!AZ_KEY || !AZ_ENDPOINT) die('AZURE_OPENAI_API_KEY and AZURE_OPENAI_ENDPOINT are required');
|
||||
|
||||
/**
|
||||
* Reproduces src/server/modules/KeyVaultsEncrypt. LobeChat stores
|
||||
* `${ivHex}:${authTagHex}:${cipherHex}` using AES-GCM with the raw bytes of
|
||||
* KEY_VAULTS_SECRET base64-decoded (must be 16/24/32 bytes).
|
||||
*/
|
||||
function encryptKeyVaults(plaintext) {
|
||||
const rawKey = Buffer.from(KEY_VAULTS_SECRET, 'base64');
|
||||
if (![16, 24, 32].includes(rawKey.length)) {
|
||||
die(
|
||||
`KEY_VAULTS_SECRET must decode to 16/24/32 bytes; got ${rawKey.length}. ` +
|
||||
`Regenerate with: openssl rand -base64 32 (and update .env + restart lobechat).`,
|
||||
);
|
||||
}
|
||||
const iv = crypto.randomBytes(12);
|
||||
const cipher = crypto.createCipheriv('aes-256-gcm', rawKey, iv);
|
||||
const enc = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
|
||||
const authTag = cipher.getAuthTag();
|
||||
return `${iv.toString('hex')}:${authTag.toString('hex')}:${enc.toString('hex')}`;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const client = new Client({ connectionString: DB_URL });
|
||||
await client.connect();
|
||||
console.log('[bootstrap] connected to', DB_URL.replace(/:[^:@]*@/, ':***@'));
|
||||
|
||||
const { rows: users } = await client.query('SELECT id, email FROM users ORDER BY created_at');
|
||||
console.log(`[bootstrap] found ${users.length} user(s)`);
|
||||
|
||||
// keyVaults shape for Azure OpenAI — see packages/types/src/user/settings/keyVaults.ts
|
||||
const keyVaultsJson = JSON.stringify({
|
||||
apiKey: AZ_KEY,
|
||||
apiVersion: AZ_VERSION,
|
||||
baseURL: AZ_ENDPOINT,
|
||||
endpoint: AZ_ENDPOINT, // deprecated alias; included for older runtime code paths
|
||||
});
|
||||
|
||||
// CustomPlugin manifest + params. LobeChat fetches `manifestUrl` on first use
|
||||
// and caches into the `manifest` column; we pre-populate nothing so the
|
||||
// identity-aware manifest is always re-fetched per user.
|
||||
const pluginIdentifier = 'enterprise-gateway';
|
||||
const customParams = {
|
||||
apiMode: 'simple',
|
||||
avatar: '🏢',
|
||||
description:
|
||||
'Identity-aware Enterprise Gateway tools. Exposed tool list filtered by caller RBAC.',
|
||||
manifestMode: 'url',
|
||||
manifestUrl: MANIFEST_URL,
|
||||
};
|
||||
|
||||
let providersInserted = 0;
|
||||
let modelsInserted = 0;
|
||||
let pluginsInserted = 0;
|
||||
|
||||
for (const u of users) {
|
||||
// Fresh ciphertext per user so two users sharing a leaked row hash nothing.
|
||||
const encKV = encryptKeyVaults(keyVaultsJson);
|
||||
|
||||
// --- ai_providers (builtin "azure") -----------------------------------
|
||||
const prov = await client.query(
|
||||
`INSERT INTO ai_providers (id, user_id, enabled, key_vaults, source, name, logo, settings, config)
|
||||
VALUES ($1, $2, TRUE, $3, 'builtin', 'Azure OpenAI', NULL, '{}'::jsonb, '{}'::jsonb)
|
||||
ON CONFLICT (id, user_id) DO UPDATE
|
||||
SET enabled = TRUE,
|
||||
key_vaults = EXCLUDED.key_vaults
|
||||
WHERE ai_providers.key_vaults IS NULL OR ai_providers.key_vaults = ''
|
||||
RETURNING id`,
|
||||
['azure', u.id, encKV],
|
||||
);
|
||||
if (prov.rowCount) providersInserted++;
|
||||
|
||||
// --- ai_models (one row for the deployment) ---------------------------
|
||||
const mdl = await client.query(
|
||||
`INSERT INTO ai_models
|
||||
(id, provider_id, user_id, enabled, display_name, type, source, config, abilities, parameters, settings)
|
||||
VALUES ($1, 'azure', $2, TRUE, $3, 'chat', 'custom',
|
||||
$4::jsonb,
|
||||
'{"functionCall":true,"vision":true}'::jsonb,
|
||||
'{}'::jsonb, '{}'::jsonb)
|
||||
ON CONFLICT (id, provider_id, user_id) DO UPDATE SET enabled = TRUE
|
||||
RETURNING id`,
|
||||
[
|
||||
AZ_DEPLOY,
|
||||
u.id,
|
||||
`${AZ_DEPLOY} (Azure)`,
|
||||
JSON.stringify({ deploymentName: AZ_DEPLOY }),
|
||||
],
|
||||
);
|
||||
if (mdl.rowCount) modelsInserted++;
|
||||
|
||||
// --- user_installed_plugins (customPlugin) ----------------------------
|
||||
const plg = await client.query(
|
||||
`INSERT INTO user_installed_plugins
|
||||
(user_id, identifier, type, manifest, settings, custom_params, source)
|
||||
VALUES ($1, $2, 'customPlugin', NULL, '{}'::jsonb, $3::jsonb, 'custom')
|
||||
ON CONFLICT (user_id, identifier) DO UPDATE
|
||||
SET custom_params = EXCLUDED.custom_params
|
||||
RETURNING identifier`,
|
||||
[u.id, pluginIdentifier, JSON.stringify(customParams)],
|
||||
);
|
||||
if (plg.rowCount) pluginsInserted++;
|
||||
|
||||
console.log(`[bootstrap] ✓ ${u.email || u.id}`);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[bootstrap] done. providers=${providersInserted} models=${modelsInserted} plugins=${pluginsInserted}`,
|
||||
);
|
||||
await client.end();
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error('[bootstrap] error:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
Executable
+73
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env bash
|
||||
# Bootstrap every existing LobeChat user with:
|
||||
# - Enabled Azure OpenAI provider (encrypted keyVaults)
|
||||
# - One Azure chat model matching $AZURE_OPENAI_DEPLOYMENT
|
||||
# - "enterprise-gateway" customPlugin pointing at the gateway manifest
|
||||
#
|
||||
# Usage: ./gateway/scripts/bootstrap-lobechat-users.sh
|
||||
# Pre-req: docker compose stack is up; .env populated with Azure + KEY_VAULTS_SECRET.
|
||||
# Idempotent: safe to re-run.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
SCRIPT_PATH="$ROOT_DIR/gateway/scripts/bootstrap-lobechat-users.js"
|
||||
|
||||
# Load .env so we get KEY_VAULTS_SECRET, Azure creds, etc.
|
||||
# Write a stripped copy to a tempfile (removes inline `# ...` comments), then
|
||||
# source it with `set -a` so every assignment becomes exported.
|
||||
if [[ -f "$ROOT_DIR/.env" ]]; then
|
||||
TMP_ENV="$(mktemp)"
|
||||
trap 'rm -f "$TMP_ENV"' EXIT
|
||||
# keep only KEY=VALUE lines; strip trailing " # comment"; preserve quotes/slashes in VALUE
|
||||
awk '
|
||||
/^[[:space:]]*#/ { next }
|
||||
/^[[:space:]]*$/ { next }
|
||||
{
|
||||
# split off trailing comment only if preceded by whitespace
|
||||
sub(/[[:space:]]+#.*$/, "")
|
||||
if ($0 ~ /^[A-Za-z_][A-Za-z0-9_]*=/) print
|
||||
}
|
||||
' "$ROOT_DIR/.env" > "$TMP_ENV"
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
source "$TMP_ENV"
|
||||
set +a
|
||||
fi
|
||||
|
||||
: "${KEY_VAULTS_SECRET:?KEY_VAULTS_SECRET must be set (see .env). Must decode to 16/24/32 bytes.}"
|
||||
: "${AZURE_OPENAI_API_KEY:?AZURE_OPENAI_API_KEY must be set in .env}"
|
||||
: "${AZURE_OPENAI_ENDPOINT:?AZURE_OPENAI_ENDPOINT must be set in .env}"
|
||||
AZURE_OPENAI_API_VERSION="${AZURE_OPENAI_API_VERSION:-2025-04-01-preview}"
|
||||
AZURE_OPENAI_DEPLOYMENT="${AZURE_OPENAI_DEPLOYMENT:-gpt-5.4}"
|
||||
|
||||
POSTGRES_USER="${POSTGRES_USER:-eg}"
|
||||
POSTGRES_PASSWORD="${POSTGRES_PASSWORD:-eg_pw}"
|
||||
LOBE_DB_NAME="${LOBE_DB_NAME:-lobechat}"
|
||||
LOBECHAT_DATABASE_URL="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${LOBE_DB_NAME}"
|
||||
|
||||
CONTAINER="${LOBECHAT_CONTAINER:-lobechat-lobechat-1}"
|
||||
|
||||
echo "[bootstrap] using container: $CONTAINER"
|
||||
if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER}\$"; then
|
||||
echo "[bootstrap] ERROR: container '$CONTAINER' not running." >&2
|
||||
echo " set LOBECHAT_CONTAINER=<name> or start the stack." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Copy script into the container (it has pg + node baked in).
|
||||
docker cp "$SCRIPT_PATH" "$CONTAINER:/tmp/bootstrap-lobechat-users.js"
|
||||
|
||||
# Run via node, pointing NODE_PATH at the app's node_modules so `require('pg')` resolves.
|
||||
docker exec \
|
||||
-e NODE_PATH=/app/node_modules \
|
||||
-e LOBECHAT_DATABASE_URL="$LOBECHAT_DATABASE_URL" \
|
||||
-e KEY_VAULTS_SECRET="$KEY_VAULTS_SECRET" \
|
||||
-e AZURE_OPENAI_API_KEY="$AZURE_OPENAI_API_KEY" \
|
||||
-e AZURE_OPENAI_ENDPOINT="$AZURE_OPENAI_ENDPOINT" \
|
||||
-e AZURE_OPENAI_API_VERSION="$AZURE_OPENAI_API_VERSION" \
|
||||
-e AZURE_OPENAI_DEPLOYMENT="$AZURE_OPENAI_DEPLOYMENT" \
|
||||
-e GATEWAY_MANIFEST_URL="${GATEWAY_MANIFEST_URL:-http://gateway:3001/api/lobechat/manifest}" \
|
||||
"$CONTAINER" node /tmp/bootstrap-lobechat-users.js
|
||||
|
||||
echo "[bootstrap] done. Sign in as admin@eg.local or user1@eg.local and open the model + plugin pickers."
|
||||
@@ -122,7 +122,9 @@
|
||||
{
|
||||
"avatar": "📊",
|
||||
"backgroundColor": "#E8F8F5",
|
||||
"plugins": ["steam"],
|
||||
"plugins": [
|
||||
"steam"
|
||||
],
|
||||
"systemRole": "你擅长处理和解释数据,通过图表和统计分析揭示数据背后的规律和趋势。",
|
||||
"title": "数据分析师"
|
||||
},
|
||||
@@ -294,7 +296,7 @@
|
||||
"23": "已进入待命状态",
|
||||
"24": "准备迎接挑战",
|
||||
"25": "新思路正在生成中",
|
||||
"26": "前路清晰,启程",
|
||||
"26": "清晰前路,启程",
|
||||
"27": "系统在线,随时帮你",
|
||||
"28": "努力加载好心情",
|
||||
"29": "掌控节奏,从现在开始",
|
||||
|
||||
@@ -122,7 +122,9 @@
|
||||
{
|
||||
"avatar": "📊",
|
||||
"backgroundColor": "#E8F8F5",
|
||||
"plugins": ["steam"],
|
||||
"plugins": [
|
||||
"steam"
|
||||
],
|
||||
"systemRole": "你擅長處理與解讀數據,透過圖表與統計分析揭示數據背後的規律與趨勢。",
|
||||
"title": "數據分析師"
|
||||
},
|
||||
@@ -294,7 +296,7 @@
|
||||
"23": "已進入待命狀態",
|
||||
"24": "準備迎接挑戰",
|
||||
"25": "新思路正在生成中",
|
||||
"26": "前路清晰,啟程!",
|
||||
"26": "Enterprise AI Workspace",
|
||||
"27": "系統在線,隨時為你服務 💡",
|
||||
"28": "努力加載好心情",
|
||||
"29": "掌握節奏,從現在開始 🎵",
|
||||
|
||||
@@ -94,7 +94,7 @@ export class AgentModel {
|
||||
return rows.map(({ slug, ...row }) => ({
|
||||
...row,
|
||||
avatar: row.avatar || (slug === INBOX_SESSION_ID ? DEFAULT_INBOX_AVATAR : null),
|
||||
title: row.title || (slug === INBOX_SESSION_ID ? 'LobeAI' : null),
|
||||
title: row.title || (slug === INBOX_SESSION_ID ? 'Enterprise Assistant' : null),
|
||||
}));
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { auth } from '@/auth';
|
||||
import {
|
||||
isModelAdmin,
|
||||
resolveEnterpriseRoles,
|
||||
} from '@/server/services/enterpriseIdentity';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export const GET = async (req: NextRequest) => {
|
||||
try {
|
||||
const session = await auth.api.getSession({ headers: req.headers });
|
||||
const email = session?.user?.email ?? null;
|
||||
|
||||
if (!email) {
|
||||
return NextResponse.json(
|
||||
{ email: null, enterprise_roles: [], is_admin: false },
|
||||
{ status: 200 },
|
||||
);
|
||||
}
|
||||
|
||||
const roles = await resolveEnterpriseRoles(email);
|
||||
return NextResponse.json(
|
||||
{
|
||||
email,
|
||||
enterprise_roles: roles,
|
||||
is_admin: isModelAdmin(roles),
|
||||
},
|
||||
{ status: 200 },
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('[enterprise/me] error', err);
|
||||
return NextResponse.json(
|
||||
{ email: null, enterprise_roles: [], is_admin: false },
|
||||
{ status: 200 },
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
import useSWR from 'swr';
|
||||
|
||||
export interface EnterpriseMe {
|
||||
email: string | null;
|
||||
enterprise_roles: string[];
|
||||
is_admin: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT: EnterpriseMe = {
|
||||
email: null,
|
||||
enterprise_roles: [],
|
||||
is_admin: false,
|
||||
};
|
||||
|
||||
const fetcher = async (url: string): Promise<EnterpriseMe> => {
|
||||
const res = await fetch(url, { credentials: 'include' });
|
||||
if (!res.ok) return DEFAULT;
|
||||
try {
|
||||
return (await res.json()) as EnterpriseMe;
|
||||
} catch {
|
||||
return DEFAULT;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches the current user's Enterprise Gateway role membership.
|
||||
* Returns `is_admin=true` for super_admin / permission_admin.
|
||||
* Safe to call on the server (returns defaults) — SWR will revalidate
|
||||
* on the client.
|
||||
*/
|
||||
export const useEnterpriseRoles = (): EnterpriseMe => {
|
||||
const { data } = useSWR<EnterpriseMe>('/webapi/enterprise/me', fetcher, {
|
||||
dedupingInterval: 60_000,
|
||||
revalidateOnFocus: false,
|
||||
});
|
||||
return data ?? DEFAULT;
|
||||
};
|
||||
@@ -157,8 +157,8 @@ export default {
|
||||
'historyRange': 'History Range',
|
||||
'historySummary': 'Historical Message Summary',
|
||||
'inactive': 'Inactive',
|
||||
'inbox.desc': 'Collaborate in one Workspace and move ideas into outcomes.',
|
||||
'inbox.title': 'Lobe AI',
|
||||
'inbox.desc': 'Ask anything about your enterprise — tools, tickets, customers, knowledge.',
|
||||
'inbox.title': 'Enterprise Assistant',
|
||||
'input.addAi': 'Add an AI message',
|
||||
'input.addUser': 'Add a user message',
|
||||
'input.disclaimer': 'Agents can make mistakes. Use your judgment for critical info.',
|
||||
|
||||
@@ -12,6 +12,6 @@ export default {
|
||||
'starter.developing': 'Coming soon',
|
||||
'starter.image': 'Image',
|
||||
'starter.imageGeneration': 'Image Generation',
|
||||
'starter.videoGeneration': 'Seedance 2.0',
|
||||
'starter.videoGeneration': 'Video',
|
||||
'starter.write': 'Write',
|
||||
};
|
||||
|
||||
@@ -49,7 +49,7 @@ const InboxEntry = memo(() => {
|
||||
inboxAgentId ? operationSelectors.isAgentRunning(inboxAgentId) : () => false,
|
||||
);
|
||||
|
||||
const title = inboxMeta.title || 'Lobe AI';
|
||||
const title = inboxMeta.title || 'Enterprise Assistant';
|
||||
const avatar = inboxMeta.avatar || DEFAULT_INBOX_AVATAR;
|
||||
const url = SESSION_CHAT_URL(inboxAgentId, false);
|
||||
|
||||
|
||||
@@ -79,13 +79,14 @@ const Examples = memo<ExamplesProps>(({ suggestMode, onExampleClick }) => {
|
||||
</Flexbox>
|
||||
<Flexbox gap={12} style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)' }}>
|
||||
{questions.map((item) => {
|
||||
const prompt = tSuggest(item.promptKey as any);
|
||||
// promptKey / titleKey carry literal enterprise prompt strings, not i18n keys.
|
||||
const prompt = item.promptKey;
|
||||
return (
|
||||
<ExampleItem
|
||||
description={prompt}
|
||||
key={item.id}
|
||||
prompt={prompt}
|
||||
title={tSuggest(item.titleKey as any)}
|
||||
title={item.titleKey}
|
||||
onClick={onExampleClick}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -82,7 +82,6 @@ const StarterList = memo(() => {
|
||||
titleKey: 'starter.imageGeneration',
|
||||
},
|
||||
{
|
||||
hot: true,
|
||||
icon: VideoIcon,
|
||||
key: 'video',
|
||||
titleKey: 'starter.videoGeneration',
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import { Flexbox } from '@lobehub/ui';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import Item from './Item';
|
||||
import { type QuestionItem } from './useRandomQuestions';
|
||||
@@ -12,8 +11,6 @@ interface ListProps {
|
||||
}
|
||||
|
||||
const List = memo<ListProps>(({ questions }) => {
|
||||
const { t } = useTranslation('suggestQuestions');
|
||||
|
||||
if (questions.length === 0) {
|
||||
return null;
|
||||
}
|
||||
@@ -21,14 +18,11 @@ const List = memo<ListProps>(({ questions }) => {
|
||||
return (
|
||||
<Flexbox gap={12} style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)' }}>
|
||||
{questions.map((item) => {
|
||||
const prompt = t(item.promptKey as any);
|
||||
// promptKey / titleKey now carry literal display strings (enterprise prompts),
|
||||
// not i18n lookup keys.
|
||||
const prompt = item.promptKey;
|
||||
return (
|
||||
<Item
|
||||
description={prompt}
|
||||
key={item.id}
|
||||
prompt={prompt}
|
||||
title={t(item.titleKey as any)}
|
||||
/>
|
||||
<Item description={prompt} key={item.id} prompt={prompt} title={item.titleKey} />
|
||||
);
|
||||
})}
|
||||
</Flexbox>
|
||||
|
||||
@@ -2,9 +2,42 @@ import { useCallback, useState } from 'react';
|
||||
|
||||
import { type StarterMode } from '@/store/home';
|
||||
|
||||
const QUESTION_COUNT = 40;
|
||||
const DISPLAY_COUNT = 6;
|
||||
|
||||
// Enterprise AI Workspace example prompts. Literal strings (not i18n keys)
|
||||
// so they bypass the upstream LobeChat suggestQuestions namespace entirely.
|
||||
interface EnterprisePrompt {
|
||||
prompt: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
const ENTERPRISE_PROMPTS: EnterprisePrompt[] = [
|
||||
{
|
||||
prompt: '显示我今天提交的所有工单状态',
|
||||
title: '查询我的工单',
|
||||
},
|
||||
{
|
||||
prompt: '在知识库里查一下我们关于 Azure 成本优化的最新规范',
|
||||
title: '搜索知识库',
|
||||
},
|
||||
{
|
||||
prompt: '为我生成本周的项目进展周报 Word 版本',
|
||||
title: '生成周报文档',
|
||||
},
|
||||
{
|
||||
prompt: '拉一下本月云成本总览',
|
||||
title: '云成本总览',
|
||||
},
|
||||
{
|
||||
prompt: '帮我分析客户 XX 最近的活动和风险点',
|
||||
title: '销售客户洞察',
|
||||
},
|
||||
{
|
||||
prompt: '在沙盒里跑一段 Python 计算斐波那契',
|
||||
title: '代码沙盒',
|
||||
},
|
||||
];
|
||||
|
||||
const shuffleArray = <T>(array: T[]): T[] => {
|
||||
const shuffled = [...array];
|
||||
for (let i = shuffled.length - 1; i > 0; i--) {
|
||||
@@ -14,24 +47,26 @@ const shuffleArray = <T>(array: T[]): T[] => {
|
||||
return shuffled;
|
||||
};
|
||||
|
||||
const generateQuestions = (mode: StarterMode) => {
|
||||
const generateQuestions = (mode: StarterMode): QuestionItem[] => {
|
||||
const modeKey = mode ?? 'chat';
|
||||
|
||||
if (!['agent', 'group', 'write', 'chat'].includes(modeKey)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const ids = Array.from({ length: QUESTION_COUNT }, (_, i) => i + 1);
|
||||
const shuffled = shuffleArray(ids);
|
||||
return shuffled.slice(0, DISPLAY_COUNT).map((id) => ({
|
||||
id,
|
||||
promptKey: `${modeKey}.${String(id).padStart(2, '0')}.prompt`,
|
||||
titleKey: `${modeKey}.${String(id).padStart(2, '0')}.title`,
|
||||
return shuffleArray(ENTERPRISE_PROMPTS)
|
||||
.slice(0, DISPLAY_COUNT)
|
||||
.map((item, idx) => ({
|
||||
id: idx + 1,
|
||||
promptKey: item.prompt,
|
||||
titleKey: item.title,
|
||||
}));
|
||||
};
|
||||
|
||||
export interface QuestionItem {
|
||||
id: number;
|
||||
// These fields retain the *Key suffix for backward compatibility with
|
||||
// the List component, but carry literal display strings rather than i18n keys.
|
||||
promptKey: string;
|
||||
titleKey: string;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,7 @@
|
||||
import { Center } from '@lobehub/ui';
|
||||
import { sample } from 'es-toolkit/compat';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { memo } from 'react';
|
||||
|
||||
const WelcomeText = memo(() => {
|
||||
const { t } = useTranslation('welcome');
|
||||
|
||||
const sentence = useMemo(() => {
|
||||
const messages = t('welcomeMessages', { returnObjects: true }) as Record<string, string>;
|
||||
return sample(Object.values(messages));
|
||||
}, [t]);
|
||||
|
||||
return (
|
||||
<Center
|
||||
style={{
|
||||
@@ -19,7 +10,7 @@ const WelcomeText = memo(() => {
|
||||
marginBlock: '36px 24px',
|
||||
}}
|
||||
>
|
||||
{sentence}
|
||||
{'Enterprise AI Workspace'}
|
||||
</Center>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useEnterpriseRoles } from '@/hooks/useEnterpriseRoles';
|
||||
import { useElectronStore } from '@/store/electron';
|
||||
import { electronSyncSelectors } from '@/store/electron/selectors';
|
||||
import { SettingsTabs } from '@/store/global/initialState';
|
||||
@@ -67,6 +68,7 @@ export const useCategory = () => {
|
||||
]);
|
||||
const remoteServerUrl = useElectronStore(electronSyncSelectors.remoteServerUrl);
|
||||
const isDevMode = useUserStore((s) => userGeneralSettingsSelectors.config(s).isDevMode);
|
||||
const { is_admin: isEnterpriseModelAdmin } = useEnterpriseRoles();
|
||||
|
||||
const avatarUrl = useMemo(() => {
|
||||
if (!avatar) return undefined;
|
||||
@@ -134,12 +136,13 @@ export const useCategory = () => {
|
||||
|
||||
// Agent group
|
||||
const agentItems: CategoryItem[] = [
|
||||
(!enableBusinessFeatures || isDevMode) && {
|
||||
(!enableBusinessFeatures || isDevMode) &&
|
||||
isEnterpriseModelAdmin && {
|
||||
icon: Brain,
|
||||
key: SettingsTabs.Provider,
|
||||
label: t('tab.provider'),
|
||||
},
|
||||
{
|
||||
isEnterpriseModelAdmin && {
|
||||
icon: Sparkles,
|
||||
key: SettingsTabs.ServiceModel,
|
||||
label: t('tab.serviceModel'),
|
||||
@@ -222,6 +225,7 @@ export const useCategory = () => {
|
||||
mobile,
|
||||
showApiKeyManage,
|
||||
isDevMode,
|
||||
isEnterpriseModelAdmin,
|
||||
avatarUrl,
|
||||
username,
|
||||
]);
|
||||
|
||||
@@ -14,6 +14,7 @@ import { authedProcedure, router } from '@/libs/trpc/lambda';
|
||||
import { serverDatabase } from '@/libs/trpc/lambda/middleware';
|
||||
import { getServerGlobalConfig } from '@/server/globalConfig';
|
||||
import { KeyVaultsGateKeeper } from '@/server/modules/KeyVaultsEncrypt';
|
||||
import { requireModelAdmin } from '@/server/services/enterpriseIdentity/guard';
|
||||
import { type ProviderConfig } from '@/types/user/settings';
|
||||
|
||||
const aiModelProcedure = authedProcedure.use(serverDatabase).use(async (opts) => {
|
||||
@@ -36,8 +37,14 @@ const aiModelProcedure = authedProcedure.use(serverDatabase).use(async (opts) =>
|
||||
});
|
||||
});
|
||||
|
||||
// Enterprise admin gate for model mutations.
|
||||
const aiModelAdminProcedure = aiModelProcedure.use(async (opts) => {
|
||||
await requireModelAdmin(opts.ctx.serverDB, opts.ctx.userId);
|
||||
return opts.next();
|
||||
});
|
||||
|
||||
export const aiModelRouter = router({
|
||||
batchToggleAiModels: aiModelProcedure
|
||||
batchToggleAiModels: aiModelAdminProcedure
|
||||
.input(
|
||||
z.object({
|
||||
enabled: z.boolean(),
|
||||
@@ -48,7 +55,7 @@ export const aiModelRouter = router({
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
return ctx.aiModelModel.batchToggleAiModels(input.id, input.models, input.enabled);
|
||||
}),
|
||||
batchUpdateAiModels: aiModelProcedure
|
||||
batchUpdateAiModels: aiModelAdminProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
@@ -60,18 +67,18 @@ export const aiModelRouter = router({
|
||||
return ctx.aiModelModel.batchUpdateAiModels(input.id, input.models);
|
||||
}),
|
||||
|
||||
clearModelsByProvider: aiModelProcedure
|
||||
clearModelsByProvider: aiModelAdminProcedure
|
||||
.input(z.object({ providerId: z.string() }))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
return ctx.aiModelModel.clearModelsByProvider(input.providerId);
|
||||
}),
|
||||
clearRemoteModels: aiModelProcedure
|
||||
clearRemoteModels: aiModelAdminProcedure
|
||||
.input(z.object({ providerId: z.string() }))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
return ctx.aiModelModel.clearRemoteModels(input.providerId);
|
||||
}),
|
||||
|
||||
createAiModel: aiModelProcedure.input(CreateAiModelSchema).mutation(async ({ input, ctx }) => {
|
||||
createAiModel: aiModelAdminProcedure.input(CreateAiModelSchema).mutation(async ({ input, ctx }) => {
|
||||
const data = await ctx.aiModelModel.create(input);
|
||||
|
||||
return data?.id;
|
||||
@@ -103,19 +110,19 @@ export const aiModelRouter = router({
|
||||
});
|
||||
}),
|
||||
|
||||
removeAiModel: aiModelProcedure
|
||||
removeAiModel: aiModelAdminProcedure
|
||||
.input(z.object({ id: z.string(), providerId: z.string() }))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
return ctx.aiModelModel.delete(input.id, input.providerId);
|
||||
}),
|
||||
|
||||
toggleModelEnabled: aiModelProcedure
|
||||
toggleModelEnabled: aiModelAdminProcedure
|
||||
.input(ToggleAiModelEnableSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
return ctx.aiModelModel.toggleModelEnabled(input);
|
||||
}),
|
||||
|
||||
updateAiModel: aiModelProcedure
|
||||
updateAiModel: aiModelAdminProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
@@ -127,7 +134,7 @@ export const aiModelRouter = router({
|
||||
return ctx.aiModelModel.update(input.id, input.providerId, input.value);
|
||||
}),
|
||||
|
||||
updateAiModelOrder: aiModelProcedure
|
||||
updateAiModelOrder: aiModelAdminProcedure
|
||||
.input(
|
||||
z.object({
|
||||
providerId: z.string(),
|
||||
|
||||
@@ -9,6 +9,7 @@ import { serverDatabase } from '@/libs/trpc/lambda/middleware';
|
||||
import { getServerGlobalConfig } from '@/server/globalConfig';
|
||||
import { KeyVaultsGateKeeper } from '@/server/modules/KeyVaultsEncrypt';
|
||||
import { initModelRuntimeFromDB } from '@/server/modules/ModelRuntime';
|
||||
import { requireModelAdmin } from '@/server/services/enterpriseIdentity/guard';
|
||||
import { type AiProviderDetailItem, type AiProviderRuntimeState } from '@/types/aiProvider';
|
||||
import {
|
||||
CreateAiProviderSchema,
|
||||
@@ -37,6 +38,13 @@ const aiProviderProcedure = authedProcedure.use(serverDatabase).use(async (opts)
|
||||
});
|
||||
});
|
||||
|
||||
// Mutations that change provider/model configuration must come from an
|
||||
// Enterprise Gateway admin. Reads use `aiProviderProcedure` directly.
|
||||
const aiProviderAdminProcedure = aiProviderProcedure.use(async (opts) => {
|
||||
await requireModelAdmin(opts.ctx.serverDB, opts.ctx.userId);
|
||||
return opts.next();
|
||||
});
|
||||
|
||||
export const aiProviderRouter = router({
|
||||
checkProviderConnectivity: aiProviderProcedure
|
||||
.input(
|
||||
@@ -85,7 +93,7 @@ export const aiProviderRouter = router({
|
||||
}
|
||||
}),
|
||||
|
||||
createAiProvider: aiProviderProcedure
|
||||
createAiProvider: aiProviderAdminProcedure
|
||||
.input(CreateAiProviderSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -120,13 +128,13 @@ export const aiProviderRouter = router({
|
||||
return ctx.aiInfraRepos.getAiProviderRuntimeState(KeyVaultsGateKeeper.getUserKeyVaults);
|
||||
}),
|
||||
|
||||
removeAiProvider: aiProviderProcedure
|
||||
removeAiProvider: aiProviderAdminProcedure
|
||||
.input(z.object({ id: z.string() }))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
return ctx.aiProviderModel.delete(input.id);
|
||||
}),
|
||||
|
||||
toggleProviderEnabled: aiProviderProcedure
|
||||
toggleProviderEnabled: aiProviderAdminProcedure
|
||||
.input(
|
||||
z.object({
|
||||
enabled: z.boolean(),
|
||||
@@ -137,7 +145,7 @@ export const aiProviderRouter = router({
|
||||
return ctx.aiProviderModel.toggleProviderEnabled(input.id, input.enabled);
|
||||
}),
|
||||
|
||||
updateAiProvider: aiProviderProcedure
|
||||
updateAiProvider: aiProviderAdminProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
@@ -148,7 +156,7 @@ export const aiProviderRouter = router({
|
||||
return ctx.aiProviderModel.update(input.id, input.value);
|
||||
}),
|
||||
|
||||
updateAiProviderConfig: aiProviderProcedure
|
||||
updateAiProviderConfig: aiProviderAdminProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
@@ -164,7 +172,7 @@ export const aiProviderRouter = router({
|
||||
);
|
||||
}),
|
||||
|
||||
updateAiProviderOrder: aiProviderProcedure
|
||||
updateAiProviderOrder: aiProviderAdminProcedure
|
||||
.input(
|
||||
z.object({
|
||||
sortMap: z.array(
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { TRPCError } from '@trpc/server';
|
||||
|
||||
import { UserModel } from '@/database/models/user';
|
||||
import { type LobeChatDatabase } from '@/database/type';
|
||||
|
||||
import { isModelAdmin, resolveEnterpriseRoles } from './index';
|
||||
|
||||
const ADMIN_BACKEND_URL =
|
||||
process.env.ENTERPRISE_ADMIN_URL || 'http://localhost:3001/admin/models';
|
||||
|
||||
const BYPASS = process.env.DISABLE_ENTERPRISE_MODEL_GUARD === '1';
|
||||
|
||||
/**
|
||||
* Throws TRPC FORBIDDEN unless the current user is an Enterprise Gateway
|
||||
* super_admin or permission_admin. Model/provider mutations are funneled
|
||||
* through this guard.
|
||||
*/
|
||||
export const requireModelAdmin = async (
|
||||
db: LobeChatDatabase,
|
||||
userId: string,
|
||||
): Promise<void> => {
|
||||
if (BYPASS) return;
|
||||
|
||||
const user = await UserModel.findById(db, userId);
|
||||
const roles = await resolveEnterpriseRoles(user?.email);
|
||||
|
||||
if (!isModelAdmin(roles)) {
|
||||
throw new TRPCError({
|
||||
code: 'FORBIDDEN',
|
||||
message: `Model configuration is managed in the Enterprise Admin backend at ${ADMIN_BACKEND_URL}`,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,86 @@
|
||||
import { Pool } from 'pg';
|
||||
|
||||
/**
|
||||
* Enterprise Identity bridge.
|
||||
*
|
||||
* Maps a LobeChat user (by email) to Enterprise Gateway roles by querying
|
||||
* the shared postgres instance (different logical DB: `enterprise_gateway`).
|
||||
*
|
||||
* - `resolveEnterpriseRoles(email)` returns an array of role keys (e.g.
|
||||
* `['super_admin']`). If no enterprise user matches, returns `[]`.
|
||||
* - Results are cached in-memory for 60 seconds per email.
|
||||
* - The admin roles that can manage model/provider configuration are
|
||||
* `super_admin` and `permission_admin`.
|
||||
*/
|
||||
|
||||
export const MODEL_ADMIN_ROLES = ['super_admin', 'permission_admin'] as const;
|
||||
|
||||
const CACHE_TTL_MS = 60_000;
|
||||
const cache = new Map<string, { expiresAt: number; roles: string[] }>();
|
||||
|
||||
let pool: Pool | null = null;
|
||||
|
||||
const deriveEnterpriseDbUrl = (): string | undefined => {
|
||||
const direct = process.env.ENTERPRISE_GATEWAY_DATABASE_URL;
|
||||
if (direct) return direct;
|
||||
|
||||
const lobeUrl = process.env.DATABASE_URL;
|
||||
if (!lobeUrl) return undefined;
|
||||
|
||||
// Swap the database name in the URL path (e.g. `/lobechat` -> `/enterprise_gateway`).
|
||||
try {
|
||||
const url = new URL(lobeUrl);
|
||||
url.pathname = '/enterprise_gateway';
|
||||
return url.toString();
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const getPool = (): Pool | null => {
|
||||
if (pool) return pool;
|
||||
const connectionString = deriveEnterpriseDbUrl();
|
||||
if (!connectionString) return null;
|
||||
pool = new Pool({ connectionString, max: 2 });
|
||||
// Swallow pool errors to avoid crashing the process on transient connection
|
||||
// issues; the caller treats any query failure as "no roles".
|
||||
pool.on('error', (err) => {
|
||||
console.warn('[enterpriseIdentity] pg pool error:', err.message);
|
||||
});
|
||||
return pool;
|
||||
};
|
||||
|
||||
export const resolveEnterpriseRoles = async (email?: string | null): Promise<string[]> => {
|
||||
if (!email) return [];
|
||||
const key = email.toLowerCase();
|
||||
|
||||
const now = Date.now();
|
||||
const hit = cache.get(key);
|
||||
if (hit && hit.expiresAt > now) return hit.roles;
|
||||
|
||||
const p = getPool();
|
||||
if (!p) return [];
|
||||
|
||||
try {
|
||||
const { rows } = await p.query<{ key: string }>(
|
||||
`SELECT r.key
|
||||
FROM enterprise_users u
|
||||
JOIN enterprise_user_roles ur ON ur.user_id = u.id
|
||||
JOIN enterprise_roles r ON r.id = ur.role_id
|
||||
WHERE lower(u.email) = $1 AND u.is_active = true`,
|
||||
[key],
|
||||
);
|
||||
const roles = rows.map((r) => r.key);
|
||||
cache.set(key, { expiresAt: now + CACHE_TTL_MS, roles });
|
||||
return roles;
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
'[enterpriseIdentity] resolveEnterpriseRoles failed:',
|
||||
(err as Error).message,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const isModelAdmin = (roles: string[]): boolean =>
|
||||
roles.some((r) => (MODEL_ADMIN_ROLES as readonly string[]).includes(r));
|
||||
Reference in New Issue
Block a user