@honlnk/picsense 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.
@@ -0,0 +1,231 @@
1
+ /**
2
+ * Session 管理器:多轮迭代的核心。
3
+ *
4
+ * 设计依据:docs/picsense-design.md §4.2、docs/plan/03-session-manager.md。
5
+ *
6
+ * 关键设计:
7
+ * - Session.messages 只存「文字层历史」(不含 base64),图片在每次调 provider 时由
8
+ * image-loader 重新加载传入。避免大 base64 字符串长期驻留内存。
9
+ * - 图片只在首轮 user message 挂载(provider 内部处理),后续轮次 messages 纯文字,
10
+ * 拼成视觉模型的原生多轮 messages 数组。
11
+ * - 同一 session 串行(session 级 Mutex),不同 session 并行。
12
+ * - 过期 24h,每小时清理一次;进程退出即清空(纯内存态,不持久化)。
13
+ * - appendTurn 事务性:provider 调用失败时不污染 messages 历史。
14
+ */
15
+ import { randomUUID } from 'node:crypto';
16
+ import { loadImage } from './image-loader.js';
17
+ export class SessionNotFoundError extends Error {
18
+ constructor(sessionId) {
19
+ super(`Session not found: ${sessionId}`);
20
+ this.name = 'SessionNotFoundError';
21
+ }
22
+ }
23
+ /**
24
+ * 极简 Mutex:基于 Promise 链串行化同一 session 的请求。
25
+ * 参考 docs/plan/03-session-manager.md §3.2。
26
+ */
27
+ class Mutex {
28
+ chain = Promise.resolve();
29
+ /** 是否正在被持有。清理任务据此跳过正在使用的 session。 */
30
+ busy = false;
31
+ async run(fn) {
32
+ const run = this.chain.then(async () => {
33
+ this.busy = true;
34
+ try {
35
+ return await fn();
36
+ }
37
+ finally {
38
+ this.busy = false;
39
+ }
40
+ });
41
+ this.chain = run.then(() => undefined, () => undefined);
42
+ return run;
43
+ }
44
+ }
45
+ /**
46
+ * Session 管理器。
47
+ *
48
+ * @param provider 视觉模型 provider。
49
+ * @param maxImageBytes 单张图片字节上限(传给 image-loader)。
50
+ * @param ttlMs session 过期时长(默认 24h)。
51
+ * @param cleanupIntervalMs 清理任务执行间隔(默认 1h)。
52
+ */
53
+ export class SessionManager {
54
+ provider;
55
+ maxImageBytes;
56
+ ttlMs;
57
+ cleanupIntervalMs;
58
+ sessions = new Map();
59
+ cleanupTimer = null;
60
+ constructor(provider, maxImageBytes, ttlMs = 24 * 60 * 60 * 1000, cleanupIntervalMs = 60 * 60 * 1000) {
61
+ this.provider = provider;
62
+ this.maxImageBytes = maxImageBytes;
63
+ this.ttlMs = ttlMs;
64
+ this.cleanupIntervalMs = cleanupIntervalMs;
65
+ }
66
+ /** 启动定时清理任务。在 MCP server 启动时调用。 */
67
+ startCleanup() {
68
+ if (this.cleanupTimer)
69
+ return;
70
+ this.cleanupTimer = setInterval(() => this.cleanup(), this.cleanupIntervalMs);
71
+ // 不阻止进程退出。
72
+ if (this.cleanupTimer && typeof this.cleanupTimer.unref === 'function') {
73
+ this.cleanupTimer.unref();
74
+ }
75
+ }
76
+ /** 停止清理任务。在 server 关闭时调用。 */
77
+ stopCleanup() {
78
+ if (this.cleanupTimer) {
79
+ clearInterval(this.cleanupTimer);
80
+ this.cleanupTimer = null;
81
+ }
82
+ }
83
+ /** 扫描并删除过期 session(跳过正在使用的)。 */
84
+ cleanup(now = Date.now()) {
85
+ let removed = 0;
86
+ for (const [id, entry] of this.sessions) {
87
+ if (entry.mutex.busy)
88
+ continue; // 设计文档 §4.2.4:跳过正在使用的
89
+ if (now - entry.session.lastAccessAt > this.ttlMs) {
90
+ this.sessions.delete(id);
91
+ removed++;
92
+ }
93
+ }
94
+ return removed;
95
+ }
96
+ /**
97
+ * 创建 session 并执行首轮识别。
98
+ * 返回 session_id + summary + description。
99
+ */
100
+ async createSession(imageSources, firstPrompt) {
101
+ const id = randomUUID();
102
+ const now = Date.now();
103
+ const session = {
104
+ id,
105
+ summary: '',
106
+ imageSources: [...imageSources],
107
+ messages: [],
108
+ createdAt: now,
109
+ lastAccessAt: now,
110
+ };
111
+ const mutex = new Mutex();
112
+ this.sessions.set(id, { session, mutex });
113
+ // 串行执行首轮(即便首轮无并发,也走锁以保持一致语义)。
114
+ return mutex.run(() => this.executeFirstTurn(session, firstPrompt));
115
+ }
116
+ /**
117
+ * 创建 session(传入已加载的图片),用于视频抽帧等场景——
118
+ * 调用方已自行完成图片加载(如 ffmpeg 抽帧),无需走 image-loader 重新加载。
119
+ *
120
+ * @param images 已加载的图片帧(如视频抽出的帧序列)。
121
+ * @param displaySources 用于 list_sessions 展示的来源引用(如视频 URL/路径,非完整 base64)。
122
+ * @param firstPrompt 首轮 prompt。
123
+ */
124
+ async createSessionWithImages(images, displaySources, firstPrompt) {
125
+ const id = randomUUID();
126
+ const now = Date.now();
127
+ const session = {
128
+ id,
129
+ summary: '',
130
+ imageSources: [...displaySources],
131
+ messages: [],
132
+ createdAt: now,
133
+ lastAccessAt: now,
134
+ };
135
+ const mutex = new Mutex();
136
+ this.sessions.set(id, { session, mutex });
137
+ return mutex.run(() => this.executeFirstTurnWithImages(session, images, firstPrompt));
138
+ }
139
+ /**
140
+ * 在已有 session 上追加一轮提问。
141
+ * session 不存在抛 SessionNotFoundError;provider 失败不污染 messages。
142
+ */
143
+ async appendTurn(sessionId, prompt) {
144
+ const entry = this.sessions.get(sessionId);
145
+ if (!entry)
146
+ throw new SessionNotFoundError(sessionId);
147
+ return entry.mutex.run(() => this.executeAppendTurn(entry.session, prompt));
148
+ }
149
+ /** 返回所有 session 的快照(浅拷贝,不阻塞写)。 */
150
+ listSessions() {
151
+ const list = [];
152
+ for (const { session } of this.sessions.values()) {
153
+ list.push(this.toSnapshot(session));
154
+ }
155
+ // 按 lastAccessAt 倒序,最近使用的在前。
156
+ return list.sort((a, b) => b.last_access_at - a.last_access_at);
157
+ }
158
+ /** 当前 session 数量(调试/测试用)。 */
159
+ get size() {
160
+ return this.sessions.size;
161
+ }
162
+ toSnapshot(session) {
163
+ return {
164
+ session_id: session.id,
165
+ summary: session.summary,
166
+ image_sources: session.imageSources.map((s) => truncate(s, 80)),
167
+ last_access_at: session.lastAccessAt,
168
+ iteration: Math.floor(session.messages.length / 2),
169
+ };
170
+ }
171
+ /** 首轮:加载图片 → 调 provider(带 summary)→ 写入 messages。 */
172
+ async executeFirstTurn(session, firstPrompt) {
173
+ const images = [];
174
+ for (const src of session.imageSources) {
175
+ images.push(await loadImage(src, this.maxImageBytes));
176
+ }
177
+ return this.executeFirstTurnWithImages(session, images, firstPrompt);
178
+ }
179
+ /**
180
+ * 首轮(图片已加载):调 provider(带 summary)→ 写入 messages。
181
+ * 抽取自 executeFirstTurn,供 createSessionWithImages 复用(视频抽帧等场景)。
182
+ */
183
+ async executeFirstTurnWithImages(session, images, firstPrompt) {
184
+ // 调 provider:传入本轮 user prompt(图片由 provider 挂在首轮)。
185
+ const reqMessages = [{ role: 'user', content: firstPrompt }];
186
+ const res = await this.provider.analyze({
187
+ images,
188
+ messages: reqMessages,
189
+ generateSummary: true,
190
+ });
191
+ // 写入历史(事务:调用成功才落库)。
192
+ session.messages.push({ role: 'user', content: firstPrompt }, { role: 'assistant', content: res.description });
193
+ session.summary = res.summary ?? fallbackSummary(firstPrompt);
194
+ session.lastAccessAt = Date.now();
195
+ return {
196
+ session_id: session.id,
197
+ summary: session.summary,
198
+ description: res.description,
199
+ };
200
+ }
201
+ /** 后续轮:追加 user prompt → 调 provider(带完整历史,不带图)→ 追加 assistant。 */
202
+ async executeAppendTurn(session, prompt) {
203
+ // 构造发给 provider 的完整 messages:历史 + 本轮 prompt。
204
+ const reqMessages = [...session.messages, { role: 'user', content: prompt }];
205
+ const res = await this.provider.analyze({
206
+ images: [], // 后续轮不带图(图已在首轮)
207
+ messages: reqMessages,
208
+ generateSummary: false,
209
+ });
210
+ // 事务:成功才落库。
211
+ session.messages.push({ role: 'user', content: prompt }, { role: 'assistant', content: res.description });
212
+ session.lastAccessAt = Date.now();
213
+ return {
214
+ session_id: session.id,
215
+ summary: session.summary,
216
+ description: res.description,
217
+ };
218
+ }
219
+ }
220
+ function truncate(s, max) {
221
+ if (s.length <= max)
222
+ return s;
223
+ const head = Math.ceil((max - 3) / 2);
224
+ const tail = Math.floor((max - 3) / 2);
225
+ return `${s.slice(0, head)}...${s.slice(-tail)}`;
226
+ }
227
+ function fallbackSummary(firstPrompt) {
228
+ const trimmed = firstPrompt.trim().replace(/\s+/g, ' ');
229
+ return trimmed.length > 80 ? `${trimmed.slice(0, 80)}...` : trimmed || 'Vision session';
230
+ }
231
+ //# sourceMappingURL=session-manager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-manager.js","sourceRoot":"","sources":["../../src/core/session-manager.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,260 @@
1
+ /**
2
+ * 视频加载:把视频解码成帧序列,复用图片管线发送给视觉模型。
3
+ *
4
+ * 设计依据:docs/picsense-design.md §4.3。
5
+ *
6
+ * 为什么抽帧而非直传视频:
7
+ * 项目默认 provider 是 OpenAI Responses API,该 API **原生不支持视频**
8
+ * (只有 input_text / input_image / input_file,无 input_video)。
9
+ * OpenAI 官方视频理解方案就是抽帧——用 ffmpeg 把视频解码成 N 张 JPEG,
10
+ * 作为多个 input_image 发送(参考 OpenAI Cookbook)。
11
+ *
12
+ * 抽帧使用 ffmpeg-static(npm 包,自带跨平台 ffmpeg 二进制,无需系统安装),
13
+ * 契合本项目「零外部依赖」理念。URL 视频先下载到临时文件再抽帧。
14
+ *
15
+ * 抽出的帧作为 LoadedImage 返回(jpeg),复用现有 image-loader 的校验逻辑,
16
+ * 下游 provider / session-manager 完全不感知「视频」概念——它们看到的就是多张图。
17
+ */
18
+ import { spawn } from 'node:child_process';
19
+ import { mkdir, readdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
20
+ import os from 'node:os';
21
+ import path from 'node:path';
22
+ import { randomUUID } from 'node:crypto';
23
+ // ffmpeg-static 默认导出 ffmpeg 二进制的绝对路径(跨平台自带)。
24
+ import ffmpegStatic from 'ffmpeg-static';
25
+ export class VideoLoadError extends Error {
26
+ constructor(message) {
27
+ super(message);
28
+ this.name = 'VideoLoadError';
29
+ }
30
+ }
31
+ /** 支持的视频扩展名白名单(URL 不做强校验,交 ffmpeg 判断)。 */
32
+ const VIDEO_EXTS = new Set(['mp4', 'mov', 'm4v', 'avi', 'wmv', 'webm', 'mkv', 'flv', 'mpeg', 'mpg']);
33
+ /**
34
+ * 解析 ffmpeg 可执行文件路径。
35
+ *
36
+ * 解析顺序:
37
+ * 1. ffmpeg-static 自带二进制(npm 安装时由其 install 脚本下载)—— 检查文件存在。
38
+ * 2. 系统 ffmpeg(用户自行安装的 brew/apt/官网版本)—— 通过 which/where 查找。
39
+ *
40
+ * 解决场景:--ignore-scripts / 企业内网代理导致 ffmpeg-static 的 install 脚本
41
+ * 未执行、二进制缺失时,只要用户系统装了 ffmpeg 仍可正常工作。
42
+ *
43
+ * 两者都不可用时抛 VideoLoadError,提示用户安装。
44
+ */
45
+ async function resolveFfmpeg() {
46
+ // 1. ffmpeg-static 自带二进制(install 脚本执行过才存在)。
47
+ if (ffmpegStatic) {
48
+ try {
49
+ const stats = await stat(ffmpegStatic);
50
+ if (stats.isFile())
51
+ return ffmpegStatic;
52
+ }
53
+ catch {
54
+ // 文件不存在,继续 fallback。
55
+ }
56
+ }
57
+ // 2. 系统 ffmpeg。
58
+ const systemFfmpeg = await whichSystem('ffmpeg');
59
+ if (systemFfmpeg)
60
+ return systemFfmpeg;
61
+ throw new VideoLoadError('ffmpeg is required for video recognition but was not found. ' +
62
+ 'The bundled ffmpeg-static binary is missing (its install script may have been skipped, ' +
63
+ 'e.g. under --ignore-scripts or a restricted network), and no system ffmpeg was detected. ' +
64
+ 'Please install ffmpeg (e.g. `brew install ffmpeg` on macOS, `apt install ffmpeg` on Debian/Ubuntu) ' +
65
+ 'or reinstall this package without --ignore-scripts.');
66
+ }
67
+ /**
68
+ * 在 PATH 中查找可执行文件(跨平台)。
69
+ * Unix 用 which,Windows 用 where。找不到返回 undefined(不抛错)。
70
+ */
71
+ async function whichSystem(bin) {
72
+ const cmd = process.platform === 'win32' ? 'where' : 'which';
73
+ return new Promise((resolve) => {
74
+ const proc = spawn(cmd, [bin], { stdio: ['ignore', 'pipe', 'ignore'] });
75
+ let out = '';
76
+ proc.stdout.on('data', (chunk) => (out += chunk.toString()));
77
+ proc.on('error', () => resolve(undefined));
78
+ proc.on('close', (code) => {
79
+ if (code !== 0) {
80
+ resolve(undefined);
81
+ return;
82
+ }
83
+ // which/where 可能返回多行(多个匹配),取第一个。
84
+ const first = out.split(/\r?\n/).map((l) => l.trim()).find((l) => l.length > 0);
85
+ resolve(first);
86
+ });
87
+ });
88
+ }
89
+ /**
90
+ * 判断来源形态并加载视频。
91
+ *
92
+ * @param source 原始视频引用(URL / 本地路径)。
93
+ * @param maxBytes 单个视频字节上限。
94
+ * @param opts 抽帧选项。
95
+ */
96
+ export async function loadVideo(source, maxBytes, opts) {
97
+ const trimmed = source.trim();
98
+ if (!trimmed) {
99
+ throw new VideoLoadError('Empty video source.');
100
+ }
101
+ // 解析 ffmpeg 路径:优先 ffmpeg-static 自带二进制,失败则 fallback 系统 ffmpeg。
102
+ // 解决场景:--ignore-scripts / 企业内网导致 ffmpeg-static install 脚本未执行、
103
+ // 二进制缺失时,只要用户系统装了 ffmpeg(brew/apt/官网)仍可工作。
104
+ const ffmpegBin = await resolveFfmpeg();
105
+ // 临时工作目录:下载 / 抽帧都在这里,用完即删。
106
+ const workDir = path.join(os.tmpdir(), `picsense-video-${randomUUID()}`);
107
+ await mkdir(workDir, { recursive: true });
108
+ try {
109
+ let videoPath;
110
+ if (/^https?:\/\//i.test(trimmed)) {
111
+ // URL:下载到临时文件(OpenAI 无法消费视频 URL,必须本地抽帧)。
112
+ videoPath = await downloadVideo(trimmed, maxBytes, workDir);
113
+ }
114
+ else {
115
+ // 本地路径:校验存在性、类型、大小。
116
+ videoPath = await validateLocalVideo(trimmed, maxBytes);
117
+ }
118
+ // 抽帧。
119
+ const frames = await extractFrames(ffmpegBin, videoPath, workDir, opts);
120
+ if (frames.length === 0) {
121
+ throw new VideoLoadError(`Failed to extract any frames from "${trimmed}". The file may not be a valid video or is corrupted.`);
122
+ }
123
+ return {
124
+ frames,
125
+ frameCount: frames.length,
126
+ source: trimmed,
127
+ };
128
+ }
129
+ finally {
130
+ // 无论成功失败都清理临时目录。
131
+ await rm(workDir, { recursive: true, force: true }).catch(() => undefined);
132
+ }
133
+ }
134
+ /** 校验本地视频文件:存在性、扩展名、大小。 */
135
+ async function validateLocalVideo(filePath, maxBytes) {
136
+ let stats;
137
+ try {
138
+ stats = await stat(filePath);
139
+ }
140
+ catch {
141
+ throw new VideoLoadError(`Video file not found or unreadable: "${filePath}".`);
142
+ }
143
+ if (!stats.isFile()) {
144
+ throw new VideoLoadError(`Video path is not a file: "${filePath}".`);
145
+ }
146
+ if (stats.size > maxBytes) {
147
+ const mb = (stats.size / 1024 / 1024).toFixed(1);
148
+ const maxMb = (maxBytes / 1024 / 1024).toFixed(0);
149
+ throw new VideoLoadError(`Video "${filePath}" is ${mb}MB, exceeds the ${maxMb}MB limit.`);
150
+ }
151
+ const ext = path.extname(filePath).slice(1).toLowerCase();
152
+ if (ext && !VIDEO_EXTS.has(ext)) {
153
+ throw new VideoLoadError(`Unsupported video type ".${ext}" from "${filePath}". Supported: ${[...VIDEO_EXTS].join(', ')}.`);
154
+ }
155
+ return filePath;
156
+ }
157
+ /** 下载 URL 视频到临时文件,带大小上限校验(流式,超限即中止)。 */
158
+ async function downloadVideo(url, maxBytes, workDir) {
159
+ const controller = new AbortController();
160
+ // 下载单独留一个较短超时(视频可能较大)。
161
+ const timer = setTimeout(() => controller.abort(), 120000);
162
+ const outPath = path.join(workDir, `source${path.extname(new URL(url).pathname) || '.mp4'}`);
163
+ try {
164
+ const res = await fetch(url, {
165
+ signal: controller.signal,
166
+ headers: { Accept: 'video/*, */*' },
167
+ });
168
+ if (!res.ok || !res.body) {
169
+ throw new VideoLoadError(`Failed to download video: HTTP ${res.status} from "${url}".`);
170
+ }
171
+ // 流式写入:边写边计大小,超限即中止。
172
+ const file = await writeFileStream(res.body, outPath, maxBytes, url);
173
+ return file;
174
+ }
175
+ finally {
176
+ clearTimeout(timer);
177
+ }
178
+ }
179
+ /** 把 ReadableStream 流式写入文件,超 maxBytes 即中止抛错。 */
180
+ async function writeFileStream(stream, outPath, maxBytes, sourceUrl) {
181
+ const reader = stream.getReader();
182
+ const chunks = [];
183
+ let total = 0;
184
+ for (;;) {
185
+ const { done, value } = await reader.read();
186
+ if (done)
187
+ break;
188
+ total += value.byteLength;
189
+ if (total > maxBytes) {
190
+ const mb = (total / 1024 / 1024).toFixed(1);
191
+ const maxMb = (maxBytes / 1024 / 1024).toFixed(0);
192
+ throw new VideoLoadError(`Video from "${sourceUrl}" exceeds ${maxMb}MB limit (reached ${mb}MB during download).`);
193
+ }
194
+ chunks.push(Buffer.from(value));
195
+ }
196
+ await writeFile(outPath, Buffer.concat(chunks));
197
+ return outPath;
198
+ }
199
+ /**
200
+ * 调用 ffmpeg 抽帧。
201
+ *
202
+ * 参数:-i <input> -vf fps=<fps> -frames:v <max> -q:v 2 <out>/frame_%05d.jpg
203
+ * - fps=N:每秒抽 N 帧
204
+ * - -frames:v=N:最多抽 N 帧
205
+ * - -q:v 2:JPEG 质量(2 为高质量)
206
+ */
207
+ async function extractFrames(ffmpegBin, videoPath, workDir, opts) {
208
+ const frameDir = path.join(workDir, 'frames');
209
+ await mkdir(frameDir, { recursive: true });
210
+ const pattern = path.join(frameDir, 'frame_%05d.jpg');
211
+ const args = [
212
+ '-i', videoPath,
213
+ '-vf', `fps=${opts.fps}`,
214
+ '-frames:v', String(opts.maxFrames),
215
+ '-q:v', '2',
216
+ pattern,
217
+ '-y', // 覆盖输出
218
+ '-loglevel', 'error',
219
+ ];
220
+ await runFfmpeg(ffmpegBin, args);
221
+ // 读取抽出的帧(按文件名排序保证时间顺序)。
222
+ const files = (await readdir(frameDir))
223
+ .filter((f) => f.endsWith('.jpg'))
224
+ .sort();
225
+ const frames = [];
226
+ for (const f of files) {
227
+ const buf = await readFile(path.join(frameDir, f));
228
+ const b64 = buf.toString('base64');
229
+ frames.push({
230
+ url: `data:image/jpeg;base64,${b64}`,
231
+ mimeType: 'image/jpeg',
232
+ // sourceType 标记为 file,语义上是「来自本地视频文件的帧」。
233
+ sourceType: 'file',
234
+ });
235
+ }
236
+ return frames;
237
+ }
238
+ /** spawn ffmpeg,等待退出;非零退出码抛 VideoLoadError。 */
239
+ function runFfmpeg(bin, args) {
240
+ return new Promise((resolve, reject) => {
241
+ const proc = spawn(bin, args, { stdio: ['ignore', 'pipe', 'pipe'] });
242
+ let stderr = '';
243
+ proc.stderr.on('data', (chunk) => {
244
+ stderr += chunk.toString();
245
+ });
246
+ proc.on('error', (err) => {
247
+ reject(new VideoLoadError(`Failed to start ffmpeg: ${err.message}`));
248
+ });
249
+ proc.on('close', (code) => {
250
+ if (code === 0) {
251
+ resolve();
252
+ }
253
+ else {
254
+ const detail = stderr.trim().slice(0, 500);
255
+ reject(new VideoLoadError(`ffmpeg exited with code ${code}${detail ? `: ${detail}` : '.'}`));
256
+ }
257
+ });
258
+ });
259
+ }
260
+ //# sourceMappingURL=video-loader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"video-loader.js","sourceRoot":"","sources":["../../src/core/video-loader.ts"],"names":[],"mappings":"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"}