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.
Files changed (245) hide show
  1. package/README.md +234 -0
  2. package/app/(admin)/approvals/page.tsx +16 -0
  3. package/app/(admin)/audit/page.tsx +18 -0
  4. package/app/(admin)/layout.tsx +47 -0
  5. package/app/(admin)/scheduled-tasks/page.tsx +17 -0
  6. package/app/(admin)/settings/page.tsx +46 -0
  7. package/app/(admin)/skills/[name]/page.tsx +378 -0
  8. package/app/(admin)/skills/page.tsx +406 -0
  9. package/app/(admin)/statistics/page.tsx +416 -0
  10. package/app/(admin)/tickets/[id]/page.tsx +348 -0
  11. package/app/(admin)/tickets/new/page.tsx +309 -0
  12. package/app/(admin)/tickets/page.tsx +27 -0
  13. package/app/api/audit/route.ts +30 -0
  14. package/app/api/auth/feishu/callback/route.ts +72 -0
  15. package/app/api/auth/feishu/login/route.ts +17 -0
  16. package/app/api/auth/feishu/sso/route.ts +78 -0
  17. package/app/api/auth/login/route.ts +85 -0
  18. package/app/api/auth/oauth/route.ts +168 -0
  19. package/app/api/config/providers/route.ts +105 -0
  20. package/app/api/config/route.ts +115 -0
  21. package/app/api/config/status/route.ts +56 -0
  22. package/app/api/config/test/route.ts +212 -0
  23. package/app/api/documents/[id]/route.ts +88 -0
  24. package/app/api/documents/route.ts +53 -0
  25. package/app/api/health/route.ts +32 -0
  26. package/app/api/knowledge/[id]/route.ts +152 -0
  27. package/app/api/knowledge/from-session/route.ts +27 -0
  28. package/app/api/knowledge/route.ts +100 -0
  29. package/app/api/market/knowledge/[id]/route.ts +92 -0
  30. package/app/api/market/knowledge/route.ts +130 -0
  31. package/app/api/marketplace/skills/[id]/approve/route.ts +68 -0
  32. package/app/api/marketplace/skills/[id]/certify/route.ts +54 -0
  33. package/app/api/marketplace/skills/[id]/install/route.ts +180 -0
  34. package/app/api/marketplace/skills/[id]/promote-to-system/route.ts +219 -0
  35. package/app/api/marketplace/skills/[id]/rate/route.ts +90 -0
  36. package/app/api/marketplace/skills/[id]/ratings/route.ts +55 -0
  37. package/app/api/marketplace/skills/[id]/reject/route.ts +68 -0
  38. package/app/api/marketplace/skills/[id]/route.ts +177 -0
  39. package/app/api/marketplace/skills/route.ts +235 -0
  40. package/app/api/memory/route.ts +40 -0
  41. package/app/api/my/files/[id]/route.ts +52 -0
  42. package/app/api/my/files/route.ts +230 -0
  43. package/app/api/my/knowledge/route.ts +36 -0
  44. package/app/api/pi-chat/route.ts +443 -0
  45. package/app/api/recommend/route.ts +38 -0
  46. package/app/api/scheduled-tasks/[id]/execute/route.ts +132 -0
  47. package/app/api/scheduled-tasks/[id]/route.ts +165 -0
  48. package/app/api/scheduled-tasks/[id]/toggle/route.ts +53 -0
  49. package/app/api/scheduled-tasks/route.ts +101 -0
  50. package/app/api/sessions/[id]/messages/route.ts +212 -0
  51. package/app/api/sessions/route.ts +101 -0
  52. package/app/api/share/file/[id]/route.ts +37 -0
  53. package/app/api/skills/[name]/execute/route.ts +121 -0
  54. package/app/api/skills/[name]/route.ts +167 -0
  55. package/app/api/skills/create/route.ts +65 -0
  56. package/app/api/skills/generate/route.ts +405 -0
  57. package/app/api/skills/installed/route.ts +151 -0
  58. package/app/api/skills/route.ts +174 -0
  59. package/app/api/skills/translate/route.ts +40 -0
  60. package/app/api/skills/user/[name]/route.ts +159 -0
  61. package/app/api/skills/user/route.ts +90 -0
  62. package/app/api/statistics/route.ts +94 -0
  63. package/app/api/task-executions/[id]/route.ts +34 -0
  64. package/app/api/task-executions/route.ts +29 -0
  65. package/app/api/tickets/[id]/approve/route.ts +129 -0
  66. package/app/api/tickets/[id]/execute/route.ts +201 -0
  67. package/app/api/tickets/[id]/route.ts +127 -0
  68. package/app/api/tickets/route.ts +103 -0
  69. package/app/api/user/skills/route.ts +175 -0
  70. package/app/api/users/route.ts +80 -0
  71. package/app/chat/page.tsx +5 -0
  72. package/app/globals.css +84 -0
  73. package/app/h5/layout.tsx +5 -0
  74. package/app/h5/mobile-approvals-page.tsx +167 -0
  75. package/app/h5/mobile-chat-page.tsx +951 -0
  76. package/app/h5/mobile-profile-page.tsx +147 -0
  77. package/app/h5/mobile-tickets-page.tsx +121 -0
  78. package/app/h5/page.tsx +23 -0
  79. package/app/h5/ticket-action-buttons.tsx +80 -0
  80. package/app/layout.tsx +26 -0
  81. package/app/login/page.tsx +318 -0
  82. package/app/market/knowledge/[id]/page.tsx +77 -0
  83. package/app/market/knowledge/page.tsx +358 -0
  84. package/app/market/layout.tsx +29 -0
  85. package/app/market/page.tsx +18 -0
  86. package/app/market/skills/page.tsx +397 -0
  87. package/app/my/files/page.tsx +511 -0
  88. package/app/my/knowledge/[id]/page.tsx +271 -0
  89. package/app/my/knowledge/new/page.tsx +234 -0
  90. package/app/my/knowledge/page.tsx +248 -0
  91. package/app/my/layout.tsx +32 -0
  92. package/app/my/memory/page.tsx +164 -0
  93. package/app/my/page.tsx +18 -0
  94. package/app/my/scheduled-tasks/[id]/edit/page.tsx +290 -0
  95. package/app/my/scheduled-tasks/[id]/executions/page.tsx +275 -0
  96. package/app/my/scheduled-tasks/[id]/page.tsx +284 -0
  97. package/app/my/scheduled-tasks/new/page.tsx +230 -0
  98. package/app/my/scheduled-tasks/page.tsx +27 -0
  99. package/app/my/skills/[name]/page.tsx +320 -0
  100. package/app/my/skills/new/page.tsx +394 -0
  101. package/app/my/skills/page.tsx +303 -0
  102. package/app/page.tsx +2288 -0
  103. package/app/share/[sessionId]/page.tsx +226 -0
  104. package/app/share/file/[id]/page.tsx +140 -0
  105. package/bin/README.md +63 -0
  106. package/bin/generate-api-system +300 -0
  107. package/bin/postinstall.js +95 -0
  108. package/bin/work-agent.js +173 -0
  109. package/components/ai-elements/agent.tsx +142 -0
  110. package/components/ai-elements/artifact.tsx +149 -0
  111. package/components/ai-elements/attachments.tsx +427 -0
  112. package/components/ai-elements/audio-player.tsx +232 -0
  113. package/components/ai-elements/canvas.tsx +26 -0
  114. package/components/ai-elements/chain-of-thought.tsx +223 -0
  115. package/components/ai-elements/checkpoint.tsx +72 -0
  116. package/components/ai-elements/code-block.tsx +555 -0
  117. package/components/ai-elements/commit.tsx +449 -0
  118. package/components/ai-elements/confirmation.tsx +173 -0
  119. package/components/ai-elements/connection.tsx +28 -0
  120. package/components/ai-elements/context.tsx +410 -0
  121. package/components/ai-elements/controls.tsx +19 -0
  122. package/components/ai-elements/conversation.tsx +167 -0
  123. package/components/ai-elements/edge.tsx +144 -0
  124. package/components/ai-elements/environment-variables.tsx +325 -0
  125. package/components/ai-elements/file-tree.tsx +298 -0
  126. package/components/ai-elements/image.tsx +25 -0
  127. package/components/ai-elements/inline-citation.tsx +294 -0
  128. package/components/ai-elements/jsx-preview.tsx +250 -0
  129. package/components/ai-elements/message.tsx +367 -0
  130. package/components/ai-elements/mic-selector.tsx +372 -0
  131. package/components/ai-elements/model-selector.tsx +214 -0
  132. package/components/ai-elements/node.tsx +72 -0
  133. package/components/ai-elements/open-in-chat.tsx +367 -0
  134. package/components/ai-elements/package-info.tsx +235 -0
  135. package/components/ai-elements/panel.tsx +16 -0
  136. package/components/ai-elements/persona.tsx +280 -0
  137. package/components/ai-elements/plan.tsx +144 -0
  138. package/components/ai-elements/prompt-input.tsx +1341 -0
  139. package/components/ai-elements/queue.tsx +275 -0
  140. package/components/ai-elements/reasoning.tsx +355 -0
  141. package/components/ai-elements/sandbox.tsx +133 -0
  142. package/components/ai-elements/schema-display.tsx +473 -0
  143. package/components/ai-elements/shimmer.tsx +78 -0
  144. package/components/ai-elements/snippet.tsx +141 -0
  145. package/components/ai-elements/sources.tsx +78 -0
  146. package/components/ai-elements/speech-input.tsx +324 -0
  147. package/components/ai-elements/stack-trace.tsx +531 -0
  148. package/components/ai-elements/suggestion.tsx +58 -0
  149. package/components/ai-elements/task.tsx +88 -0
  150. package/components/ai-elements/terminal.tsx +277 -0
  151. package/components/ai-elements/test-results.tsx +497 -0
  152. package/components/ai-elements/tool.tsx +174 -0
  153. package/components/ai-elements/toolbar.tsx +17 -0
  154. package/components/ai-elements/transcription.tsx +126 -0
  155. package/components/ai-elements/voice-selector.tsx +525 -0
  156. package/components/ai-elements/web-preview.tsx +282 -0
  157. package/components/audit-log-list.tsx +114 -0
  158. package/components/chat/EmptyPreviewState.tsx +12 -0
  159. package/components/chat/KnowledgePickerDialog.tsx +464 -0
  160. package/components/chat/KnowledgePreview.tsx +70 -0
  161. package/components/chat/KnowledgePreviewPanel.tsx +86 -0
  162. package/components/chat/MentionInput.tsx +309 -0
  163. package/components/chat/OrganizeDialog.tsx +258 -0
  164. package/components/chat/RecommendationBanner.tsx +94 -0
  165. package/components/chat/SaveToKnowledgeDialog.tsx +193 -0
  166. package/components/chat/SkillSelector.tsx +305 -0
  167. package/components/chat/SkillSwitcher.tsx +163 -0
  168. package/components/client-layout.tsx +15 -0
  169. package/components/knowledge/KnowledgeMetadataPanel.tsx +293 -0
  170. package/components/layout-wrapper.tsx +18 -0
  171. package/components/mobile-layout.tsx +62 -0
  172. package/components/scheduled-task-list.tsx +356 -0
  173. package/components/setup-guide.tsx +484 -0
  174. package/components/sub-nav.tsx +54 -0
  175. package/components/ticket-detail-content.tsx +383 -0
  176. package/components/ticket-list.tsx +366 -0
  177. package/components/top-nav.tsx +132 -0
  178. package/components/ui/accordion.tsx +58 -0
  179. package/components/ui/alert.tsx +59 -0
  180. package/components/ui/avatar.tsx +50 -0
  181. package/components/ui/badge.tsx +36 -0
  182. package/components/ui/button-group.tsx +83 -0
  183. package/components/ui/button.tsx +57 -0
  184. package/components/ui/card.tsx +91 -0
  185. package/components/ui/carousel.tsx +262 -0
  186. package/components/ui/collapsible.tsx +11 -0
  187. package/components/ui/command.tsx +153 -0
  188. package/components/ui/dialog.tsx +122 -0
  189. package/components/ui/dropdown-menu.tsx +200 -0
  190. package/components/ui/hover-card.tsx +29 -0
  191. package/components/ui/input-group.tsx +170 -0
  192. package/components/ui/input.tsx +22 -0
  193. package/components/ui/label.tsx +26 -0
  194. package/components/ui/popover.tsx +31 -0
  195. package/components/ui/progress.tsx +28 -0
  196. package/components/ui/scroll-area.tsx +48 -0
  197. package/components/ui/select.tsx +174 -0
  198. package/components/ui/separator.tsx +31 -0
  199. package/components/ui/spinner.tsx +16 -0
  200. package/components/ui/switch.tsx +29 -0
  201. package/components/ui/table.tsx +120 -0
  202. package/components/ui/tabs.tsx +55 -0
  203. package/components/ui/textarea.tsx +22 -0
  204. package/components/ui/tooltip.tsx +30 -0
  205. package/components/welcome-guide.tsx +182 -0
  206. package/components.json +24 -0
  207. package/lib/command-parser.ts +331 -0
  208. package/lib/dangerous-commands.ts +672 -0
  209. package/lib/db.ts +2250 -0
  210. package/lib/feishu-auth.ts +135 -0
  211. package/lib/file-storage.ts +306 -0
  212. package/lib/file-tool.ts +583 -0
  213. package/lib/knowledge-tool.ts +152 -0
  214. package/lib/knowledge-types.ts +66 -0
  215. package/lib/market-client.ts +313 -0
  216. package/lib/market-db.ts +736 -0
  217. package/lib/market-types.ts +51 -0
  218. package/lib/memory-tool.ts +211 -0
  219. package/lib/memory.ts +197 -0
  220. package/lib/pi-config.ts +436 -0
  221. package/lib/pi-session.ts +799 -0
  222. package/lib/pinyin.ts +13 -0
  223. package/lib/recommendation.ts +227 -0
  224. package/lib/risk-estimator.ts +350 -0
  225. package/lib/scheduled-task-tool.ts +184 -0
  226. package/lib/scheduler-init.ts +43 -0
  227. package/lib/scheduler.ts +416 -0
  228. package/lib/secure-bash-tool.ts +413 -0
  229. package/lib/skill-engine.ts +396 -0
  230. package/lib/skill-generator.ts +269 -0
  231. package/lib/skill-loader.ts +234 -0
  232. package/lib/skill-tool.ts +188 -0
  233. package/lib/skill-types.ts +82 -0
  234. package/lib/skills-init.ts +58 -0
  235. package/lib/ticket-tool.ts +246 -0
  236. package/lib/user-skill-types.ts +30 -0
  237. package/lib/user-skills.ts +362 -0
  238. package/lib/utils.ts +6 -0
  239. package/lib/workflow.ts +154 -0
  240. package/lib/zip-tool.ts +191 -0
  241. package/next.config.js +8 -0
  242. package/package.json +106 -0
  243. package/public/.gitkeep +1 -0
  244. package/public/icon.svg +1 -0
  245. 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
+ }