work-agent 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +234 -0
- package/app/(admin)/approvals/page.tsx +16 -0
- package/app/(admin)/audit/page.tsx +18 -0
- package/app/(admin)/layout.tsx +47 -0
- package/app/(admin)/scheduled-tasks/page.tsx +17 -0
- package/app/(admin)/settings/page.tsx +46 -0
- package/app/(admin)/skills/[name]/page.tsx +378 -0
- package/app/(admin)/skills/page.tsx +406 -0
- package/app/(admin)/statistics/page.tsx +416 -0
- package/app/(admin)/tickets/[id]/page.tsx +348 -0
- package/app/(admin)/tickets/new/page.tsx +309 -0
- package/app/(admin)/tickets/page.tsx +27 -0
- package/app/api/audit/route.ts +30 -0
- package/app/api/auth/feishu/callback/route.ts +72 -0
- package/app/api/auth/feishu/login/route.ts +17 -0
- package/app/api/auth/feishu/sso/route.ts +78 -0
- package/app/api/auth/login/route.ts +85 -0
- package/app/api/auth/oauth/route.ts +168 -0
- package/app/api/config/providers/route.ts +105 -0
- package/app/api/config/route.ts +115 -0
- package/app/api/config/status/route.ts +56 -0
- package/app/api/config/test/route.ts +212 -0
- package/app/api/documents/[id]/route.ts +88 -0
- package/app/api/documents/route.ts +53 -0
- package/app/api/health/route.ts +32 -0
- package/app/api/knowledge/[id]/route.ts +152 -0
- package/app/api/knowledge/from-session/route.ts +27 -0
- package/app/api/knowledge/route.ts +100 -0
- package/app/api/market/knowledge/[id]/route.ts +92 -0
- package/app/api/market/knowledge/route.ts +130 -0
- package/app/api/marketplace/skills/[id]/approve/route.ts +68 -0
- package/app/api/marketplace/skills/[id]/certify/route.ts +54 -0
- package/app/api/marketplace/skills/[id]/install/route.ts +180 -0
- package/app/api/marketplace/skills/[id]/promote-to-system/route.ts +219 -0
- package/app/api/marketplace/skills/[id]/rate/route.ts +90 -0
- package/app/api/marketplace/skills/[id]/ratings/route.ts +55 -0
- package/app/api/marketplace/skills/[id]/reject/route.ts +68 -0
- package/app/api/marketplace/skills/[id]/route.ts +177 -0
- package/app/api/marketplace/skills/route.ts +235 -0
- package/app/api/memory/route.ts +40 -0
- package/app/api/my/files/[id]/route.ts +52 -0
- package/app/api/my/files/route.ts +230 -0
- package/app/api/my/knowledge/route.ts +36 -0
- package/app/api/pi-chat/route.ts +443 -0
- package/app/api/recommend/route.ts +38 -0
- package/app/api/scheduled-tasks/[id]/execute/route.ts +132 -0
- package/app/api/scheduled-tasks/[id]/route.ts +165 -0
- package/app/api/scheduled-tasks/[id]/toggle/route.ts +53 -0
- package/app/api/scheduled-tasks/route.ts +101 -0
- package/app/api/sessions/[id]/messages/route.ts +212 -0
- package/app/api/sessions/route.ts +101 -0
- package/app/api/share/file/[id]/route.ts +37 -0
- package/app/api/skills/[name]/execute/route.ts +121 -0
- package/app/api/skills/[name]/route.ts +167 -0
- package/app/api/skills/create/route.ts +65 -0
- package/app/api/skills/generate/route.ts +405 -0
- package/app/api/skills/installed/route.ts +151 -0
- package/app/api/skills/route.ts +174 -0
- package/app/api/skills/translate/route.ts +40 -0
- package/app/api/skills/user/[name]/route.ts +159 -0
- package/app/api/skills/user/route.ts +90 -0
- package/app/api/statistics/route.ts +94 -0
- package/app/api/task-executions/[id]/route.ts +34 -0
- package/app/api/task-executions/route.ts +29 -0
- package/app/api/tickets/[id]/approve/route.ts +129 -0
- package/app/api/tickets/[id]/execute/route.ts +201 -0
- package/app/api/tickets/[id]/route.ts +127 -0
- package/app/api/tickets/route.ts +103 -0
- package/app/api/user/skills/route.ts +175 -0
- package/app/api/users/route.ts +80 -0
- package/app/chat/page.tsx +5 -0
- package/app/globals.css +84 -0
- package/app/h5/layout.tsx +5 -0
- package/app/h5/mobile-approvals-page.tsx +167 -0
- package/app/h5/mobile-chat-page.tsx +951 -0
- package/app/h5/mobile-profile-page.tsx +147 -0
- package/app/h5/mobile-tickets-page.tsx +121 -0
- package/app/h5/page.tsx +23 -0
- package/app/h5/ticket-action-buttons.tsx +80 -0
- package/app/layout.tsx +26 -0
- package/app/login/page.tsx +318 -0
- package/app/market/knowledge/[id]/page.tsx +77 -0
- package/app/market/knowledge/page.tsx +358 -0
- package/app/market/layout.tsx +29 -0
- package/app/market/page.tsx +18 -0
- package/app/market/skills/page.tsx +397 -0
- package/app/my/files/page.tsx +511 -0
- package/app/my/knowledge/[id]/page.tsx +271 -0
- package/app/my/knowledge/new/page.tsx +234 -0
- package/app/my/knowledge/page.tsx +248 -0
- package/app/my/layout.tsx +32 -0
- package/app/my/memory/page.tsx +164 -0
- package/app/my/page.tsx +18 -0
- package/app/my/scheduled-tasks/[id]/edit/page.tsx +290 -0
- package/app/my/scheduled-tasks/[id]/executions/page.tsx +275 -0
- package/app/my/scheduled-tasks/[id]/page.tsx +284 -0
- package/app/my/scheduled-tasks/new/page.tsx +230 -0
- package/app/my/scheduled-tasks/page.tsx +27 -0
- package/app/my/skills/[name]/page.tsx +320 -0
- package/app/my/skills/new/page.tsx +394 -0
- package/app/my/skills/page.tsx +303 -0
- package/app/page.tsx +2288 -0
- package/app/share/[sessionId]/page.tsx +226 -0
- package/app/share/file/[id]/page.tsx +140 -0
- package/bin/README.md +63 -0
- package/bin/generate-api-system +300 -0
- package/bin/postinstall.js +95 -0
- package/bin/work-agent.js +173 -0
- package/components/ai-elements/agent.tsx +142 -0
- package/components/ai-elements/artifact.tsx +149 -0
- package/components/ai-elements/attachments.tsx +427 -0
- package/components/ai-elements/audio-player.tsx +232 -0
- package/components/ai-elements/canvas.tsx +26 -0
- package/components/ai-elements/chain-of-thought.tsx +223 -0
- package/components/ai-elements/checkpoint.tsx +72 -0
- package/components/ai-elements/code-block.tsx +555 -0
- package/components/ai-elements/commit.tsx +449 -0
- package/components/ai-elements/confirmation.tsx +173 -0
- package/components/ai-elements/connection.tsx +28 -0
- package/components/ai-elements/context.tsx +410 -0
- package/components/ai-elements/controls.tsx +19 -0
- package/components/ai-elements/conversation.tsx +167 -0
- package/components/ai-elements/edge.tsx +144 -0
- package/components/ai-elements/environment-variables.tsx +325 -0
- package/components/ai-elements/file-tree.tsx +298 -0
- package/components/ai-elements/image.tsx +25 -0
- package/components/ai-elements/inline-citation.tsx +294 -0
- package/components/ai-elements/jsx-preview.tsx +250 -0
- package/components/ai-elements/message.tsx +367 -0
- package/components/ai-elements/mic-selector.tsx +372 -0
- package/components/ai-elements/model-selector.tsx +214 -0
- package/components/ai-elements/node.tsx +72 -0
- package/components/ai-elements/open-in-chat.tsx +367 -0
- package/components/ai-elements/package-info.tsx +235 -0
- package/components/ai-elements/panel.tsx +16 -0
- package/components/ai-elements/persona.tsx +280 -0
- package/components/ai-elements/plan.tsx +144 -0
- package/components/ai-elements/prompt-input.tsx +1341 -0
- package/components/ai-elements/queue.tsx +275 -0
- package/components/ai-elements/reasoning.tsx +355 -0
- package/components/ai-elements/sandbox.tsx +133 -0
- package/components/ai-elements/schema-display.tsx +473 -0
- package/components/ai-elements/shimmer.tsx +78 -0
- package/components/ai-elements/snippet.tsx +141 -0
- package/components/ai-elements/sources.tsx +78 -0
- package/components/ai-elements/speech-input.tsx +324 -0
- package/components/ai-elements/stack-trace.tsx +531 -0
- package/components/ai-elements/suggestion.tsx +58 -0
- package/components/ai-elements/task.tsx +88 -0
- package/components/ai-elements/terminal.tsx +277 -0
- package/components/ai-elements/test-results.tsx +497 -0
- package/components/ai-elements/tool.tsx +174 -0
- package/components/ai-elements/toolbar.tsx +17 -0
- package/components/ai-elements/transcription.tsx +126 -0
- package/components/ai-elements/voice-selector.tsx +525 -0
- package/components/ai-elements/web-preview.tsx +282 -0
- package/components/audit-log-list.tsx +114 -0
- package/components/chat/EmptyPreviewState.tsx +12 -0
- package/components/chat/KnowledgePickerDialog.tsx +464 -0
- package/components/chat/KnowledgePreview.tsx +70 -0
- package/components/chat/KnowledgePreviewPanel.tsx +86 -0
- package/components/chat/MentionInput.tsx +309 -0
- package/components/chat/OrganizeDialog.tsx +258 -0
- package/components/chat/RecommendationBanner.tsx +94 -0
- package/components/chat/SaveToKnowledgeDialog.tsx +193 -0
- package/components/chat/SkillSelector.tsx +305 -0
- package/components/chat/SkillSwitcher.tsx +163 -0
- package/components/client-layout.tsx +15 -0
- package/components/knowledge/KnowledgeMetadataPanel.tsx +293 -0
- package/components/layout-wrapper.tsx +18 -0
- package/components/mobile-layout.tsx +62 -0
- package/components/scheduled-task-list.tsx +356 -0
- package/components/setup-guide.tsx +484 -0
- package/components/sub-nav.tsx +54 -0
- package/components/ticket-detail-content.tsx +383 -0
- package/components/ticket-list.tsx +366 -0
- package/components/top-nav.tsx +132 -0
- package/components/ui/accordion.tsx +58 -0
- package/components/ui/alert.tsx +59 -0
- package/components/ui/avatar.tsx +50 -0
- package/components/ui/badge.tsx +36 -0
- package/components/ui/button-group.tsx +83 -0
- package/components/ui/button.tsx +57 -0
- package/components/ui/card.tsx +91 -0
- package/components/ui/carousel.tsx +262 -0
- package/components/ui/collapsible.tsx +11 -0
- package/components/ui/command.tsx +153 -0
- package/components/ui/dialog.tsx +122 -0
- package/components/ui/dropdown-menu.tsx +200 -0
- package/components/ui/hover-card.tsx +29 -0
- package/components/ui/input-group.tsx +170 -0
- package/components/ui/input.tsx +22 -0
- package/components/ui/label.tsx +26 -0
- package/components/ui/popover.tsx +31 -0
- package/components/ui/progress.tsx +28 -0
- package/components/ui/scroll-area.tsx +48 -0
- package/components/ui/select.tsx +174 -0
- package/components/ui/separator.tsx +31 -0
- package/components/ui/spinner.tsx +16 -0
- package/components/ui/switch.tsx +29 -0
- package/components/ui/table.tsx +120 -0
- package/components/ui/tabs.tsx +55 -0
- package/components/ui/textarea.tsx +22 -0
- package/components/ui/tooltip.tsx +30 -0
- package/components/welcome-guide.tsx +182 -0
- package/components.json +24 -0
- package/lib/command-parser.ts +331 -0
- package/lib/dangerous-commands.ts +672 -0
- package/lib/db.ts +2250 -0
- package/lib/feishu-auth.ts +135 -0
- package/lib/file-storage.ts +306 -0
- package/lib/file-tool.ts +583 -0
- package/lib/knowledge-tool.ts +152 -0
- package/lib/knowledge-types.ts +66 -0
- package/lib/market-client.ts +313 -0
- package/lib/market-db.ts +736 -0
- package/lib/market-types.ts +51 -0
- package/lib/memory-tool.ts +211 -0
- package/lib/memory.ts +197 -0
- package/lib/pi-config.ts +436 -0
- package/lib/pi-session.ts +799 -0
- package/lib/pinyin.ts +13 -0
- package/lib/recommendation.ts +227 -0
- package/lib/risk-estimator.ts +350 -0
- package/lib/scheduled-task-tool.ts +184 -0
- package/lib/scheduler-init.ts +43 -0
- package/lib/scheduler.ts +416 -0
- package/lib/secure-bash-tool.ts +413 -0
- package/lib/skill-engine.ts +396 -0
- package/lib/skill-generator.ts +269 -0
- package/lib/skill-loader.ts +234 -0
- package/lib/skill-tool.ts +188 -0
- package/lib/skill-types.ts +82 -0
- package/lib/skills-init.ts +58 -0
- package/lib/ticket-tool.ts +246 -0
- package/lib/user-skill-types.ts +30 -0
- package/lib/user-skills.ts +362 -0
- package/lib/utils.ts +6 -0
- package/lib/workflow.ts +154 -0
- package/lib/zip-tool.ts +191 -0
- package/next.config.js +8 -0
- package/package.json +106 -0
- package/public/.gitkeep +1 -0
- package/public/icon.svg +1 -0
- package/tsconfig.json +42 -0
|
@@ -0,0 +1,799 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PI Agent Session Manager
|
|
3
|
+
*
|
|
4
|
+
* Manages pi-coding-agent sessions for intelligent operations.
|
|
5
|
+
* Handles session creation, configuration, and event streaming.
|
|
6
|
+
*
|
|
7
|
+
* Sessions are persisted to disk to survive server restarts and hot reloads.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// 全局变量,用于在工具中获取当前会话ID
|
|
11
|
+
let currentSessionId: string | null = null;
|
|
12
|
+
|
|
13
|
+
export function getCurrentSessionId(): string | null {
|
|
14
|
+
return currentSessionId;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function setCurrentSessionId(id: string | null): void {
|
|
18
|
+
currentSessionId = id;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
AuthStorage,
|
|
23
|
+
createAgentSession,
|
|
24
|
+
DefaultResourceLoader,
|
|
25
|
+
SessionManager,
|
|
26
|
+
SettingsManager,
|
|
27
|
+
createReadTool,
|
|
28
|
+
createEditTool,
|
|
29
|
+
createWriteTool,
|
|
30
|
+
createGrepTool,
|
|
31
|
+
createFindTool,
|
|
32
|
+
createLsTool,
|
|
33
|
+
type AgentSession,
|
|
34
|
+
type AgentSessionEvent,
|
|
35
|
+
type ToolDefinition,
|
|
36
|
+
} from "@mariozechner/pi-coding-agent";
|
|
37
|
+
|
|
38
|
+
export { AgentSessionEvent };
|
|
39
|
+
import { writeFileSync, readFileSync, mkdirSync, existsSync } from "fs";
|
|
40
|
+
import { join } from "path";
|
|
41
|
+
import {
|
|
42
|
+
DEFAULT_PROVIDERS,
|
|
43
|
+
getAuthConfig,
|
|
44
|
+
getAvailableProviders,
|
|
45
|
+
getDefaultProvider,
|
|
46
|
+
loadModelsConfig,
|
|
47
|
+
PI_CONFIG_DIR,
|
|
48
|
+
type ProviderConfig,
|
|
49
|
+
} from "./pi-config";
|
|
50
|
+
import { createSecureBashTool } from "./secure-bash-tool";
|
|
51
|
+
import { createTicketTool } from "./ticket-tool";
|
|
52
|
+
import { createScheduledTaskTool } from "./scheduled-task-tool";
|
|
53
|
+
import { createSkillTool } from "./skill-tool";
|
|
54
|
+
import { saveKnowledgeTool } from "./knowledge-tool";
|
|
55
|
+
import { createMemoryTool } from "./memory-tool";
|
|
56
|
+
import { fileTools } from "./file-tool";
|
|
57
|
+
import { loadUserMemories } from "./memory";
|
|
58
|
+
import { getActiveKnowledge, searchKnowledge, saveSession as dbSaveSession, getSession as dbGetSession, getAllSessions as dbGetAllSessions, deleteSession as dbDeleteSession, getDb } from "./db";
|
|
59
|
+
import {
|
|
60
|
+
getUserAvailableSkills,
|
|
61
|
+
loadSkillFromPath,
|
|
62
|
+
loadSkillMetaFromPath,
|
|
63
|
+
type SkillLocation,
|
|
64
|
+
} from './skill-loader';
|
|
65
|
+
|
|
66
|
+
// Session directory for persistence (our metadata)
|
|
67
|
+
const SESSION_DIR = join(process.cwd(), "data", "sessions");
|
|
68
|
+
|
|
69
|
+
// Session directory for pi-coding-agent (JSONL files)
|
|
70
|
+
const PI_SESSIONS_DIR = join(process.cwd(), "data", "pi-sessions");
|
|
71
|
+
|
|
72
|
+
// Ensure session directory exists
|
|
73
|
+
if (!existsSync(SESSION_DIR)) {
|
|
74
|
+
try {
|
|
75
|
+
mkdirSync(SESSION_DIR, { recursive: true });
|
|
76
|
+
} catch (error) {
|
|
77
|
+
// Directory may already exist, ignore
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Ensure pi-sessions directory exists
|
|
82
|
+
if (!existsSync(PI_SESSIONS_DIR)) {
|
|
83
|
+
try {
|
|
84
|
+
mkdirSync(PI_SESSIONS_DIR, { recursive: true });
|
|
85
|
+
} catch (error) {
|
|
86
|
+
// Directory may already exist, ignore
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Session storage (in-memory for quick access, persisted to disk)
|
|
91
|
+
const activeSessions = new Map<string, AgentSession>();
|
|
92
|
+
|
|
93
|
+
// Session metadata storage (for tracking sessions across restarts)
|
|
94
|
+
interface SessionMetadata {
|
|
95
|
+
sessionId: string;
|
|
96
|
+
userId?: string;
|
|
97
|
+
source?: 'web' | 'feishu_bot' | 'api';
|
|
98
|
+
sourceUserId?: string;
|
|
99
|
+
createdAt: string;
|
|
100
|
+
lastAccessedAt: string;
|
|
101
|
+
messageCount: number;
|
|
102
|
+
recentSummary?: string;
|
|
103
|
+
title?: string;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const sessionMetadata = new Map<string, SessionMetadata>();
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Configuration for PI agent session
|
|
110
|
+
*/
|
|
111
|
+
interface PiSessionConfig {
|
|
112
|
+
userId?: string;
|
|
113
|
+
source?: 'web' | 'feishu_bot' | 'api';
|
|
114
|
+
sourceUserId?: string;
|
|
115
|
+
systemPrompt?: string;
|
|
116
|
+
model?: string;
|
|
117
|
+
providerId?: string;
|
|
118
|
+
skills?: string[];
|
|
119
|
+
sessionId?: string;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Get session metadata file path
|
|
124
|
+
*/
|
|
125
|
+
function getSessionMetadataPath(sessionId: string): string {
|
|
126
|
+
return join(SESSION_DIR, `${sessionId}.json`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Save session metadata to database
|
|
131
|
+
*/
|
|
132
|
+
function saveSessionMetadata(metadata: SessionMetadata): void {
|
|
133
|
+
try {
|
|
134
|
+
dbSaveSession({
|
|
135
|
+
sessionId: metadata.sessionId,
|
|
136
|
+
userId: metadata.userId,
|
|
137
|
+
createdAt: metadata.createdAt,
|
|
138
|
+
lastAccessedAt: metadata.lastAccessedAt,
|
|
139
|
+
messageCount: metadata.messageCount,
|
|
140
|
+
recentSummary: metadata.recentSummary,
|
|
141
|
+
title: metadata.title,
|
|
142
|
+
});
|
|
143
|
+
} catch (error) {
|
|
144
|
+
console.error('Failed to save session metadata:', error);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Load session metadata from database
|
|
150
|
+
*/
|
|
151
|
+
function loadSessionMetadata(sessionId: string): SessionMetadata | null {
|
|
152
|
+
try {
|
|
153
|
+
const session = dbGetSession(sessionId);
|
|
154
|
+
if (!session) return null;
|
|
155
|
+
return {
|
|
156
|
+
sessionId: session.sessionId,
|
|
157
|
+
userId: session.userId,
|
|
158
|
+
source: session.source,
|
|
159
|
+
sourceUserId: session.sourceUserId,
|
|
160
|
+
createdAt: session.createdAt.toISOString(),
|
|
161
|
+
lastAccessedAt: session.lastAccessedAt.toISOString(),
|
|
162
|
+
messageCount: session.messageCount,
|
|
163
|
+
recentSummary: session.recentSummary,
|
|
164
|
+
title: session.title,
|
|
165
|
+
};
|
|
166
|
+
} catch (error) {
|
|
167
|
+
console.error('Failed to load session metadata:', error);
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Get or create a PI agent session
|
|
174
|
+
*/
|
|
175
|
+
export async function getOrCreateSession(
|
|
176
|
+
sessionId: string,
|
|
177
|
+
config: PiSessionConfig = {},
|
|
178
|
+
): Promise<{ session: AgentSession; isNew: boolean; restored?: boolean }> {
|
|
179
|
+
// Check if session exists in memory
|
|
180
|
+
const existing = activeSessions.get(sessionId);
|
|
181
|
+
if (existing) {
|
|
182
|
+
// Update last accessed time
|
|
183
|
+
const metadata = sessionMetadata.get(sessionId);
|
|
184
|
+
if (metadata) {
|
|
185
|
+
metadata.lastAccessedAt = new Date().toISOString();
|
|
186
|
+
saveSessionMetadata(metadata);
|
|
187
|
+
}
|
|
188
|
+
return { session: existing, isNew: false };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Check if session exists on disk (restoring after restart)
|
|
192
|
+
const existingMetadata = loadSessionMetadata(sessionId);
|
|
193
|
+
if (existingMetadata) {
|
|
194
|
+
// Restore session with previous context
|
|
195
|
+
// Pass sessionId to enable persistence - pi-coding-agent will load history from file
|
|
196
|
+
const session = await createPiSession({
|
|
197
|
+
...config,
|
|
198
|
+
sessionId, // Important: pass sessionId for persistence
|
|
199
|
+
userId: existingMetadata.userId || config.userId,
|
|
200
|
+
});
|
|
201
|
+
activeSessions.set(sessionId, session);
|
|
202
|
+
|
|
203
|
+
// Update metadata
|
|
204
|
+
existingMetadata.lastAccessedAt = new Date().toISOString();
|
|
205
|
+
sessionMetadata.set(sessionId, existingMetadata);
|
|
206
|
+
saveSessionMetadata(existingMetadata);
|
|
207
|
+
|
|
208
|
+
return { session, isNew: false, restored: true };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Create new session
|
|
212
|
+
// Pass sessionId to enable persistence
|
|
213
|
+
const session = await createPiSession({
|
|
214
|
+
...config,
|
|
215
|
+
sessionId, // Important: pass sessionId for persistence
|
|
216
|
+
});
|
|
217
|
+
activeSessions.set(sessionId, session);
|
|
218
|
+
|
|
219
|
+
// Save metadata
|
|
220
|
+
const metadata: SessionMetadata = {
|
|
221
|
+
sessionId,
|
|
222
|
+
userId: config.userId,
|
|
223
|
+
source: config.source,
|
|
224
|
+
sourceUserId: config.sourceUserId,
|
|
225
|
+
createdAt: new Date().toISOString(),
|
|
226
|
+
lastAccessedAt: new Date().toISOString(),
|
|
227
|
+
messageCount: 0,
|
|
228
|
+
};
|
|
229
|
+
sessionMetadata.set(sessionId, metadata);
|
|
230
|
+
saveSessionMetadata(metadata);
|
|
231
|
+
|
|
232
|
+
return { session, isNew: true };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Update session summary (call after each message to maintain context)
|
|
237
|
+
*/
|
|
238
|
+
export function updateSessionSummary(sessionId: string, summary: string): void {
|
|
239
|
+
const metadata = sessionMetadata.get(sessionId);
|
|
240
|
+
if (metadata) {
|
|
241
|
+
metadata.recentSummary = summary;
|
|
242
|
+
saveSessionMetadata(metadata);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Get the session file path for pi-coding-agent (JSONL format)
|
|
248
|
+
*/
|
|
249
|
+
function getPiSessionFilePath(sessionId: string): string {
|
|
250
|
+
return join(PI_SESSIONS_DIR, `${sessionId}.jsonl`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* 检查用户是否是管理员
|
|
255
|
+
*/
|
|
256
|
+
function checkIsAdmin(userId: string): boolean {
|
|
257
|
+
try {
|
|
258
|
+
const db = getDb();
|
|
259
|
+
const user = db.prepare('SELECT role FROM users WHERE id = ?').get(userId) as { role: string } | undefined;
|
|
260
|
+
return user?.role === 'admin';
|
|
261
|
+
} catch (error) {
|
|
262
|
+
console.error('[pi-session] Failed to check admin status:', error);
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Create a new PI agent session with tools
|
|
269
|
+
*/
|
|
270
|
+
async function createPiSession(config: PiSessionConfig): Promise<AgentSession> {
|
|
271
|
+
const { userId, systemPrompt, model, providerId, skills = [], sessionId } = config;
|
|
272
|
+
|
|
273
|
+
// 检查用户是否是管理员
|
|
274
|
+
const isAdmin = userId ? checkIsAdmin(userId) : false;
|
|
275
|
+
console.log('[pi-session] User:', userId, 'isAdmin:', isAdmin);
|
|
276
|
+
|
|
277
|
+
// Load skill content
|
|
278
|
+
let skillsContent = '';
|
|
279
|
+
if (skills.length > 0) {
|
|
280
|
+
const skillContents: string[] = [];
|
|
281
|
+
|
|
282
|
+
// 使用新的技能加载器
|
|
283
|
+
const availableSkills = getUserAvailableSkills(userId);
|
|
284
|
+
|
|
285
|
+
for (const skillName of skills) {
|
|
286
|
+
const location = availableSkills.get(skillName);
|
|
287
|
+
if (location) {
|
|
288
|
+
try {
|
|
289
|
+
const content = loadSkillFromPath(location.path);
|
|
290
|
+
if (content) {
|
|
291
|
+
skillContents.push(content);
|
|
292
|
+
const sourceLabel = location.source === 'system' ? '系统' :
|
|
293
|
+
location.source === 'user' ? '用户' : '市场';
|
|
294
|
+
console.log(`[pi-session] Loaded ${sourceLabel} skill: ${skillName} from ${location.path}`);
|
|
295
|
+
}
|
|
296
|
+
} catch (error) {
|
|
297
|
+
console.error(`[pi-session] Failed to load skill ${skillName}:`, error);
|
|
298
|
+
}
|
|
299
|
+
} else {
|
|
300
|
+
console.warn(`[pi-session] Skill not found: ${skillName}`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (skillContents.length > 0) {
|
|
305
|
+
skillsContent = skillContents.join('\n\n---\n\n');
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Load configuration from models.json
|
|
310
|
+
const providers = getAvailableProviders();
|
|
311
|
+
const defaultProvider = getDefaultProvider();
|
|
312
|
+
|
|
313
|
+
// Use selected provider or default
|
|
314
|
+
const selectedProvider = providerId
|
|
315
|
+
? providers.find(p => p.id === providerId) || defaultProvider
|
|
316
|
+
: defaultProvider;
|
|
317
|
+
|
|
318
|
+
// Set up auth storage
|
|
319
|
+
const authStorage = AuthStorage.inMemory();
|
|
320
|
+
|
|
321
|
+
// Support AI_ prefix environment variables (works with or without models.json)
|
|
322
|
+
const aiProvider = process.env.AI_PROVIDER;
|
|
323
|
+
const aiApiKey = process.env.AI_API_KEY;
|
|
324
|
+
const aiModel = process.env.AI_MODEL;
|
|
325
|
+
const aiBaseUrl = process.env.AI_BASE_URL;
|
|
326
|
+
|
|
327
|
+
// If AI_ environment variables are set, use them directly
|
|
328
|
+
if (aiProvider && aiApiKey) {
|
|
329
|
+
const providerId = aiProvider.toLowerCase();
|
|
330
|
+
const defaultProvider = DEFAULT_PROVIDERS[providerId];
|
|
331
|
+
|
|
332
|
+
authStorage.setRuntimeApiKey(providerId, aiApiKey);
|
|
333
|
+
|
|
334
|
+
console.log(`[pi-session] Using AI_ environment variables:`);
|
|
335
|
+
console.log(`[pi-session] AI_PROVIDER: ${providerId}`);
|
|
336
|
+
console.log(`[pi-session] AI_MODEL: ${aiModel || defaultProvider?.models?.[0]?.id || 'default'}`);
|
|
337
|
+
if (aiBaseUrl) {
|
|
338
|
+
console.log(`[pi-session] AI_BASE_URL: ${aiBaseUrl}`);
|
|
339
|
+
}
|
|
340
|
+
console.log(`[pi-session] AI_API_KEY: ${aiApiKey.substring(0, 10)}...`);
|
|
341
|
+
} else if (selectedProvider) {
|
|
342
|
+
// Set API key from models.json
|
|
343
|
+
authStorage.setRuntimeApiKey(selectedProvider.id, selectedProvider.apiKey);
|
|
344
|
+
|
|
345
|
+
console.log(`[pi-session] Using provider from models.json: ${selectedProvider.name} (${selectedProvider.id})`);
|
|
346
|
+
console.log(`[pi-session] API type: ${selectedProvider.api}`);
|
|
347
|
+
if (selectedProvider.baseUrl) {
|
|
348
|
+
console.log(`[pi-session] Base URL: ${selectedProvider.baseUrl}`);
|
|
349
|
+
}
|
|
350
|
+
console.log(`[pi-session] Config dir: ${PI_CONFIG_DIR}`);
|
|
351
|
+
console.log(`[pi-session] HOME env: ${process.env.HOME || 'not set'}`);
|
|
352
|
+
console.log(`[pi-session] CWD: ${process.cwd()}`);
|
|
353
|
+
// Debug: show API key (first 10 and last 10 chars)
|
|
354
|
+
const key = selectedProvider.apiKey;
|
|
355
|
+
const maskedKey = key.length > 20 ? `${key.substring(0, 10)}...${key.substring(key.length - 10)}` : key;
|
|
356
|
+
console.log(`[pi-session] API Key (debug): ${maskedKey}`);
|
|
357
|
+
console.log(`[pi-session] API Key length: ${key.length}`);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Check if we have any auth configured
|
|
361
|
+
if (!aiProvider && !aiApiKey && !selectedProvider) {
|
|
362
|
+
throw new Error(
|
|
363
|
+
'No AI provider configured. Please either:\n' +
|
|
364
|
+
'1. Set PI_CONFIG_PATH to your models.json location\n' +
|
|
365
|
+
'2. Mount ~/.pi/agent to the container\n' +
|
|
366
|
+
'3. Set AI_PROVIDER and AI_API_KEY environment variables\n' +
|
|
367
|
+
`Config directory: ${PI_CONFIG_DIR}`
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// In-memory settings with overrides
|
|
372
|
+
const settingsManager = SettingsManager.inMemory({
|
|
373
|
+
compaction: { enabled: false }, // Disable auto-compaction for web use
|
|
374
|
+
retry: { enabled: true, maxRetries: 2 },
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// Load user memories
|
|
378
|
+
const userMemories = userId ? loadUserMemories(userId) : '';
|
|
379
|
+
|
|
380
|
+
// Custom system prompt for general-purpose AI assistant
|
|
381
|
+
const defaultSystemPrompt = systemPrompt || `You are a helpful AI Assistant with command execution capabilities.
|
|
382
|
+
|
|
383
|
+
${skillsContent ? `
|
|
384
|
+
## User Activated Skills
|
|
385
|
+
|
|
386
|
+
The user has explicitly activated the following skill(s). Follow their instructions when processing user requests related to these skills:
|
|
387
|
+
|
|
388
|
+
---
|
|
389
|
+
${skillsContent}
|
|
390
|
+
---
|
|
391
|
+
` : ''}
|
|
392
|
+
|
|
393
|
+
${userMemories ? `
|
|
394
|
+
## User Memories
|
|
395
|
+
|
|
396
|
+
The following are the user's saved memories from previous conversations. Please reference this information when helping the user:
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
${userMemories}
|
|
400
|
+
---
|
|
401
|
+
|
|
402
|
+
**IMPORTANT:** When the user asks you to "remember" or "note" something, use the \`save_memory\` tool to save it to their memories.
|
|
403
|
+
` : `
|
|
404
|
+
**Memory:** If the user asks you to "remember" something important, use the \`save_memory\` tool to save it for future reference.`}
|
|
405
|
+
|
|
406
|
+
**Your Capabilities:**
|
|
407
|
+
- Execute shell commands using the bash tool
|
|
408
|
+
- Read, write, and edit files
|
|
409
|
+
- Search for files and content
|
|
410
|
+
- Create operation tickets for dangerous commands
|
|
411
|
+
- Save memories for the user using the save_memory tool
|
|
412
|
+
|
|
413
|
+
**CRITICAL SECURITY RULES:**
|
|
414
|
+
1. Dangerous commands (file deletions, system modifications, etc.) are BLOCKED and will NOT execute
|
|
415
|
+
2. When you need to perform a dangerous operation, you MUST use the \`create_ticket\` tool to create a ticket
|
|
416
|
+
3. Tickets require human approval before execution - always inform users about this
|
|
417
|
+
4. Safe commands (ls, cat, grep, etc.) can be executed directly
|
|
418
|
+
|
|
419
|
+
**How to Create Tickets:**
|
|
420
|
+
Use the \`create_ticket\` tool with the command you want to execute:
|
|
421
|
+
\`\`\`
|
|
422
|
+
create_ticket(command="your-command-here", title="操作标题", description="...")
|
|
423
|
+
\`\`\`
|
|
424
|
+
|
|
425
|
+
**Available Tools:**
|
|
426
|
+
- bash: Execute shell commands (dangerous commands are BLOCKED)
|
|
427
|
+
- read: Read file contents
|
|
428
|
+
- edit: Edit existing files
|
|
429
|
+
- write: Create new files
|
|
430
|
+
- grep: Search file contents
|
|
431
|
+
- find: Find files by pattern
|
|
432
|
+
- ls: List directory contents
|
|
433
|
+
- create_ticket: Create an operation ticket for approval workflow (USE THIS for dangerous commands!)
|
|
434
|
+
|
|
435
|
+
${userId ? `**Current User:** ${userId}` : ""}
|
|
436
|
+
|
|
437
|
+
Start by greeting the user and asking how you can help them.`;
|
|
438
|
+
|
|
439
|
+
// Create resource loader with custom system prompt
|
|
440
|
+
const loader = new DefaultResourceLoader({
|
|
441
|
+
cwd: process.cwd(),
|
|
442
|
+
systemPromptOverride: () => defaultSystemPrompt,
|
|
443
|
+
settingsManager,
|
|
444
|
+
});
|
|
445
|
+
await loader.reload();
|
|
446
|
+
|
|
447
|
+
// Create custom tools
|
|
448
|
+
// 1. Secure bash tool - intercepts dangerous commands and creates tickets
|
|
449
|
+
// 传入 userId 和 isAdmin,用于权限控制(非管理员只能执行远程命令)
|
|
450
|
+
const secureBashTool = createSecureBashTool(process.cwd(), userId, isAdmin);
|
|
451
|
+
|
|
452
|
+
// 2. Standard coding tools (read, edit, write, grep, find, ls)
|
|
453
|
+
const customToolSet = [
|
|
454
|
+
createReadTool(process.cwd()),
|
|
455
|
+
secureBashTool, // Our secure bash tool
|
|
456
|
+
createEditTool(process.cwd()),
|
|
457
|
+
createWriteTool(process.cwd()),
|
|
458
|
+
createGrepTool(process.cwd()),
|
|
459
|
+
createFindTool(process.cwd()),
|
|
460
|
+
createLsTool(process.cwd()),
|
|
461
|
+
];
|
|
462
|
+
|
|
463
|
+
// Create or open the SessionManager for persistence
|
|
464
|
+
let sessionManager: SessionManager;
|
|
465
|
+
if (sessionId) {
|
|
466
|
+
const sessionFile = getPiSessionFilePath(sessionId);
|
|
467
|
+
if (existsSync(sessionFile)) {
|
|
468
|
+
// Open existing session to restore history
|
|
469
|
+
sessionManager = SessionManager.open(sessionFile, PI_SESSIONS_DIR);
|
|
470
|
+
console.log(`[pi-session] Opened existing session: ${sessionId}`);
|
|
471
|
+
} else {
|
|
472
|
+
// Create new session with persistence
|
|
473
|
+
sessionManager = SessionManager.create(process.cwd(), PI_SESSIONS_DIR);
|
|
474
|
+
// Set the session file path to use our custom naming
|
|
475
|
+
sessionManager.setSessionFile(sessionFile);
|
|
476
|
+
console.log(`[pi-session] Created new persistent session: ${sessionId}`);
|
|
477
|
+
}
|
|
478
|
+
} else {
|
|
479
|
+
// Fallback to in-memory if no sessionId provided
|
|
480
|
+
sessionManager = SessionManager.inMemory();
|
|
481
|
+
console.log('[pi-session] Using in-memory session (no sessionId provided)');
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// 设置当前会话ID,供工具使用
|
|
485
|
+
setCurrentSessionId(sessionId || null);
|
|
486
|
+
|
|
487
|
+
// Create the session
|
|
488
|
+
// Pass agentDir to ensure ModelRegistry loads from the correct location
|
|
489
|
+
console.log('[pi-session] Creating agent session...');
|
|
490
|
+
console.log('[pi-session] cwd:', process.cwd());
|
|
491
|
+
console.log('[pi-session] agentDir:', PI_CONFIG_DIR);
|
|
492
|
+
console.log('[pi-session] selectedProvider.id:', selectedProvider?.id);
|
|
493
|
+
console.log('[pi-session] selectedProvider.baseUrl:', selectedProvider?.baseUrl);
|
|
494
|
+
console.log('[pi-session] selectedProvider.api:', selectedProvider?.api);
|
|
495
|
+
|
|
496
|
+
const result = await createAgentSession({
|
|
497
|
+
cwd: process.cwd(),
|
|
498
|
+
agentDir: PI_CONFIG_DIR, // Use our config directory for models.json
|
|
499
|
+
authStorage,
|
|
500
|
+
sessionManager,
|
|
501
|
+
settingsManager,
|
|
502
|
+
resourceLoader: loader,
|
|
503
|
+
customTools: [
|
|
504
|
+
createTicketTool as unknown as ToolDefinition,
|
|
505
|
+
createScheduledTaskTool as unknown as ToolDefinition,
|
|
506
|
+
createSkillTool(userId) as unknown as ToolDefinition,
|
|
507
|
+
saveKnowledgeTool as unknown as ToolDefinition,
|
|
508
|
+
createMemoryTool(userId) as unknown as ToolDefinition,
|
|
509
|
+
...fileTools.map(t => t as unknown as ToolDefinition),
|
|
510
|
+
], // Add custom tools
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
// IMPORTANT: Override the default tools with our custom tools
|
|
514
|
+
// The SDK's `tools` parameter only extracts tool names and uses default tools,
|
|
515
|
+
// so we need to manually set our secure tools after session creation.
|
|
516
|
+
// We must include ALL tools here, including the create_ticket from customTools.
|
|
517
|
+
console.log('[pi-session] Setting custom tools with secure bash and create_ticket...');
|
|
518
|
+
|
|
519
|
+
// Get the tools that were registered by customTools
|
|
520
|
+
const existingTools = result.session.agent.state.tools;
|
|
521
|
+
const createTicketExistingTool = existingTools.find((t: any) => t.name === 'create_ticket');
|
|
522
|
+
const createScheduledTaskExistingTool = existingTools.find((t: any) => t.name === 'create_scheduled_task');
|
|
523
|
+
const createSkillExistingTool = existingTools.find((t: any) => t.name === 'create_skill');
|
|
524
|
+
const saveKnowledgeExistingTool = existingTools.find((t: any) => t.name === 'save_knowledge');
|
|
525
|
+
const saveMemoryExistingTool = existingTools.find((t: any) => t.name === 'save_memory');
|
|
526
|
+
const listFilesTool = existingTools.find((t: any) => t.name === 'list_user_files');
|
|
527
|
+
const getFileInfoTool = existingTools.find((t: any) => t.name === 'get_file_info');
|
|
528
|
+
const downloadFileTool = existingTools.find((t: any) => t.name === 'download_file');
|
|
529
|
+
const uploadFileTool = existingTools.find((t: any) => t.name === 'upload_file');
|
|
530
|
+
const deleteFileTool = existingTools.find((t: any) => t.name === 'delete_file');
|
|
531
|
+
const updateFileInfoTool = existingTools.find((t: any) => t.name === 'update_file_info');
|
|
532
|
+
|
|
533
|
+
// Combine our custom bash tool with other tools and custom tools
|
|
534
|
+
const allTools = [
|
|
535
|
+
...customToolSet,
|
|
536
|
+
...(createTicketExistingTool ? [createTicketExistingTool] : []),
|
|
537
|
+
...(createScheduledTaskExistingTool ? [createScheduledTaskExistingTool] : []),
|
|
538
|
+
...(createSkillExistingTool ? [createSkillExistingTool] : []),
|
|
539
|
+
...(saveKnowledgeExistingTool ? [saveKnowledgeExistingTool] : []),
|
|
540
|
+
...(saveMemoryExistingTool ? [saveMemoryExistingTool] : []),
|
|
541
|
+
...(listFilesTool ? [listFilesTool] : []),
|
|
542
|
+
...(getFileInfoTool ? [getFileInfoTool] : []),
|
|
543
|
+
...(downloadFileTool ? [downloadFileTool] : []),
|
|
544
|
+
...(uploadFileTool ? [uploadFileTool] : []),
|
|
545
|
+
...(deleteFileTool ? [deleteFileTool] : []),
|
|
546
|
+
...(updateFileInfoTool ? [updateFileInfoTool] : []),
|
|
547
|
+
];
|
|
548
|
+
|
|
549
|
+
result.session.agent.setTools(allTools);
|
|
550
|
+
|
|
551
|
+
return result.session;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Convert PI session events to SSE format for web streaming
|
|
556
|
+
*/
|
|
557
|
+
export function eventToSSE(event: AgentSessionEvent): string | null {
|
|
558
|
+
switch (event.type) {
|
|
559
|
+
case "message_update":
|
|
560
|
+
if (event.assistantMessageEvent.type === "text_delta") {
|
|
561
|
+
return `data: ${JSON.stringify({ type: "text", content: event.assistantMessageEvent.delta })}\n\n`;
|
|
562
|
+
}
|
|
563
|
+
if (event.assistantMessageEvent.type === "thinking_delta") {
|
|
564
|
+
return `data: ${JSON.stringify({ type: "thinking", content: event.assistantMessageEvent.delta })}\n\n`;
|
|
565
|
+
}
|
|
566
|
+
break;
|
|
567
|
+
|
|
568
|
+
case "tool_execution_start":
|
|
569
|
+
return `data: ${JSON.stringify({ type: "tool_start", toolName: event.toolName, args: event.args })}\n\n`;
|
|
570
|
+
|
|
571
|
+
case "tool_execution_update":
|
|
572
|
+
// partialResult contains the tool output
|
|
573
|
+
return `data: ${JSON.stringify({ type: "tool_output", toolName: event.toolName, content: JSON.stringify(event.partialResult) })}\n\n`;
|
|
574
|
+
|
|
575
|
+
case "tool_execution_end":
|
|
576
|
+
return `data: ${JSON.stringify({ type: "tool_end", toolName: event.toolName, isError: event.isError })}\n\n`;
|
|
577
|
+
|
|
578
|
+
case "message_end":
|
|
579
|
+
return `data: ${JSON.stringify({ type: "message_end", message: event.message })}\n\n`;
|
|
580
|
+
|
|
581
|
+
case "agent_end":
|
|
582
|
+
return `data: ${JSON.stringify({ type: "agent_end", messages: event.messages })}\n\n`;
|
|
583
|
+
|
|
584
|
+
case "turn_start":
|
|
585
|
+
return `data: ${JSON.stringify({ type: "turn_start" })}\n\n`;
|
|
586
|
+
|
|
587
|
+
case "turn_end":
|
|
588
|
+
return `data: ${JSON.stringify({ type: "turn_end", message: event.message, toolResults: event.toolResults })}\n\n`;
|
|
589
|
+
|
|
590
|
+
default:
|
|
591
|
+
// Log unhandled events for debugging
|
|
592
|
+
console.log('[pi-session] Unhandled event type:', event.type, event);
|
|
593
|
+
return null;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
return null;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Abort current operation in a session
|
|
601
|
+
*/
|
|
602
|
+
export async function abortSession(sessionId: string): Promise<void> {
|
|
603
|
+
const session = activeSessions.get(sessionId);
|
|
604
|
+
if (session) {
|
|
605
|
+
try {
|
|
606
|
+
await session.abort();
|
|
607
|
+
console.log(`[pi-session] Aborted session: ${sessionId}`);
|
|
608
|
+
} catch (error) {
|
|
609
|
+
console.error(`[pi-session] Abort error for ${sessionId}:`, error);
|
|
610
|
+
throw error;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Clean up a session (call when user leaves or session expires)
|
|
617
|
+
*/
|
|
618
|
+
export function cleanupSession(sessionId: string): void {
|
|
619
|
+
const session = activeSessions.get(sessionId);
|
|
620
|
+
if (session) {
|
|
621
|
+
session.dispose();
|
|
622
|
+
activeSessions.delete(sessionId);
|
|
623
|
+
}
|
|
624
|
+
// Also remove metadata
|
|
625
|
+
sessionMetadata.delete(sessionId);
|
|
626
|
+
// Delete from database
|
|
627
|
+
try {
|
|
628
|
+
dbDeleteSession(sessionId);
|
|
629
|
+
} catch (error) {
|
|
630
|
+
console.error('Failed to delete session from database:', error);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Get all active sessions (for debugging/monitoring)
|
|
636
|
+
*/
|
|
637
|
+
export function getActiveSessions(): string[] {
|
|
638
|
+
return Array.from(activeSessions.keys());
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Check if a session exists
|
|
643
|
+
*/
|
|
644
|
+
export function hasSession(sessionId: string): boolean {
|
|
645
|
+
return activeSessions.has(sessionId);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Get all persisted session IDs (including inactive ones)
|
|
650
|
+
*/
|
|
651
|
+
export function getAllSessionIds(): string[] {
|
|
652
|
+
try {
|
|
653
|
+
const sessions = dbGetAllSessions();
|
|
654
|
+
return sessions.map(s => s.sessionId);
|
|
655
|
+
} catch (error) {
|
|
656
|
+
return [];
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* Get all session metadata
|
|
662
|
+
*/
|
|
663
|
+
export function getAllSessionMetadata(): SessionMetadata[] {
|
|
664
|
+
try {
|
|
665
|
+
const sessions = dbGetAllSessions();
|
|
666
|
+
return sessions.map(s => ({
|
|
667
|
+
sessionId: s.sessionId,
|
|
668
|
+
userId: s.userId,
|
|
669
|
+
createdAt: s.createdAt.toISOString(),
|
|
670
|
+
lastAccessedAt: s.lastAccessedAt.toISOString(),
|
|
671
|
+
messageCount: s.messageCount,
|
|
672
|
+
recentSummary: s.recentSummary,
|
|
673
|
+
title: s.title,
|
|
674
|
+
}));
|
|
675
|
+
} catch (error) {
|
|
676
|
+
console.error('Failed to get all session metadata:', error);
|
|
677
|
+
return [];
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Get session metadata by ID
|
|
683
|
+
*/
|
|
684
|
+
export function getSessionMetadata(sessionId: string): SessionMetadata | null {
|
|
685
|
+
return loadSessionMetadata(sessionId);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// ==================== 动态加载函数 ====================
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* 动态加载单个 Skill 的内容
|
|
692
|
+
*/
|
|
693
|
+
export function loadSkillContent(skillName: string): string | null {
|
|
694
|
+
const skillPath = join(process.cwd(), '.pi', 'skills', skillName, 'SKILL.md');
|
|
695
|
+
|
|
696
|
+
if (!existsSync(skillPath)) {
|
|
697
|
+
console.warn(`[pi-session] Skill not found: ${skillName}`);
|
|
698
|
+
return null;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
try {
|
|
702
|
+
const content = readFileSync(skillPath, 'utf-8');
|
|
703
|
+
console.log(`[pi-session] Dynamically loaded skill: ${skillName}`);
|
|
704
|
+
return content;
|
|
705
|
+
} catch (error) {
|
|
706
|
+
console.error(`[pi-session] Failed to load skill ${skillName}:`, error);
|
|
707
|
+
return null;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* 搜索知识库并返回匹配的知识内容
|
|
713
|
+
*/
|
|
714
|
+
export function searchKnowledgeContent(keywords: string, limit: number = 5): string {
|
|
715
|
+
try {
|
|
716
|
+
const results = searchKnowledge(keywords, limit);
|
|
717
|
+
if (results.length === 0) {
|
|
718
|
+
return '';
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
const knowledgeContent = results.map(k =>
|
|
722
|
+
`### ${k.title}\n\n${k.content}`
|
|
723
|
+
).join('\n\n---\n\n');
|
|
724
|
+
|
|
725
|
+
console.log(`[pi-session] Searched knowledge with "${keywords}", found ${results.length} items`);
|
|
726
|
+
return knowledgeContent;
|
|
727
|
+
} catch (error) {
|
|
728
|
+
console.error('[pi-session] Failed to search knowledge:', error);
|
|
729
|
+
return '';
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// ==================== 迁移函数 ====================
|
|
734
|
+
|
|
735
|
+
let sessionsMigrated = false;
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* 从文件迁移会话列表到数据库(一次性)
|
|
739
|
+
*/
|
|
740
|
+
export function migrateSessionsFromFiles(): void {
|
|
741
|
+
if (sessionsMigrated) {
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
try {
|
|
746
|
+
const { readdirSync, existsSync, readFileSync } = require('fs');
|
|
747
|
+
|
|
748
|
+
if (!existsSync(SESSION_DIR)) {
|
|
749
|
+
console.log('[pi-session] No sessions directory found, skipping migration');
|
|
750
|
+
sessionsMigrated = true;
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
const files = readdirSync(SESSION_DIR);
|
|
755
|
+
const sessionFiles = files.filter((f: string) => f.endsWith('.json'));
|
|
756
|
+
|
|
757
|
+
if (sessionFiles.length === 0) {
|
|
758
|
+
console.log('[pi-session] No session files found, skipping migration');
|
|
759
|
+
sessionsMigrated = true;
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
console.log(`[pi-session] Migrating ${sessionFiles.length} sessions from files to database...`);
|
|
764
|
+
|
|
765
|
+
let migrated = 0;
|
|
766
|
+
for (const file of sessionFiles) {
|
|
767
|
+
const sessionId = file.replace('.json', '');
|
|
768
|
+
|
|
769
|
+
// Skip if already in database
|
|
770
|
+
const existing = dbGetSession(sessionId);
|
|
771
|
+
if (existing) {
|
|
772
|
+
continue;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
try {
|
|
776
|
+
const path = join(SESSION_DIR, file);
|
|
777
|
+
const data = readFileSync(path, 'utf-8');
|
|
778
|
+
const metadata = JSON.parse(data) as SessionMetadata;
|
|
779
|
+
|
|
780
|
+
dbSaveSession({
|
|
781
|
+
sessionId: metadata.sessionId,
|
|
782
|
+
userId: metadata.userId,
|
|
783
|
+
createdAt: metadata.createdAt,
|
|
784
|
+
lastAccessedAt: metadata.lastAccessedAt,
|
|
785
|
+
messageCount: metadata.messageCount,
|
|
786
|
+
recentSummary: metadata.recentSummary,
|
|
787
|
+
});
|
|
788
|
+
migrated++;
|
|
789
|
+
} catch (error) {
|
|
790
|
+
console.warn(`[pi-session] Failed to migrate session ${sessionId}:`, error);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
console.log(`[pi-session] Migrated ${migrated} sessions to database`);
|
|
795
|
+
sessionsMigrated = true;
|
|
796
|
+
} catch (error) {
|
|
797
|
+
console.error('[pi-session] Migration failed:', error);
|
|
798
|
+
}
|
|
799
|
+
}
|