spec-agent 2.0.4 → 2.0.6
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.
Potentially problematic release.
This version of spec-agent might be problematic. Click here for more details.
- package/.cursor/rules/spec-agent-assistant.mdc +12 -0
- package/.cursor/skills/spec-agent-execution-orchestrator/SKILL.md +20 -0
- package/.cursor/skills/spec-agent-onboarding-agent/SKILL.md +15 -0
- package/.cursor/skills/spec-agent-product-dev-agent/SKILL.md +30 -1
- package/CURSOR_AGENT_PACK.md +30 -3
- package/README.md +9 -9
- package/USAGE_FROM_NPM.md +1 -4
- package/dist/commands/handoff.d.ts.map +1 -1
- package/dist/commands/handoff.js +73 -2
- package/dist/commands/handoff.js.map +1 -1
- package/dist/commands/scan.d.ts.map +1 -1
- package/dist/commands/scan.js +133 -7
- package/dist/commands/scan.js.map +1 -1
- package/dist/services/llm.d.ts +1 -1
- package/dist/services/llm.d.ts.map +1 -1
- package/dist/services/llm.js +5 -14
- package/dist/services/llm.js.map +1 -1
- package/dist/types.d.ts +11 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/commands/handoff.ts +106 -2
- package/src/commands/scan.ts +163 -10
- package/src/services/llm.ts +5 -14
- package/src/types.ts +12 -0
package/src/commands/handoff.ts
CHANGED
|
@@ -23,6 +23,28 @@ interface HandoffTask {
|
|
|
23
23
|
assignedType: string;
|
|
24
24
|
estimatedHours?: number;
|
|
25
25
|
sourceChunks?: number[];
|
|
26
|
+
prototypeRefs?: PrototypeMatch[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface PrototypeIndexFile {
|
|
30
|
+
assets: PrototypeAssetEntry[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface PrototypeAssetEntry {
|
|
34
|
+
id: string;
|
|
35
|
+
path: string;
|
|
36
|
+
sourceFile: string;
|
|
37
|
+
alt?: string;
|
|
38
|
+
summary?: string;
|
|
39
|
+
sourceChunks?: number[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface PrototypeMatch {
|
|
43
|
+
id: string;
|
|
44
|
+
path: string;
|
|
45
|
+
reason: string;
|
|
46
|
+
score: number;
|
|
47
|
+
summary?: string;
|
|
26
48
|
}
|
|
27
49
|
|
|
28
50
|
export async function handoffCommand(options: HandoffOptions, command: Command): Promise<void> {
|
|
@@ -33,6 +55,7 @@ export async function handoffCommand(options: HandoffOptions, command: Command):
|
|
|
33
55
|
const dispatchPath = path.join(workspacePath, 'dispatch_plan.json');
|
|
34
56
|
const taskPlanPath = path.join(workspacePath, 'task_plan.json');
|
|
35
57
|
const specPath = path.join(workspacePath, 'spec_summary.json');
|
|
58
|
+
const prototypeIndexPath = path.join(workspacePath, 'prototype_index.json');
|
|
36
59
|
|
|
37
60
|
if (!(await fileExists(dispatchPath))) {
|
|
38
61
|
logger.error(`dispatch_plan.json not found in workspace: ${workspacePath}`);
|
|
@@ -52,6 +75,10 @@ export async function handoffCommand(options: HandoffOptions, command: Command):
|
|
|
52
75
|
const dispatch = await readJson<DispatchPlan>(dispatchPath);
|
|
53
76
|
const taskPlan = await readJson<TaskPlan>(taskPlanPath);
|
|
54
77
|
const spec = (await fileExists(specPath)) ? await readJson<SpecSummary>(specPath) : null;
|
|
78
|
+
const prototypeIndex = (await fileExists(prototypeIndexPath))
|
|
79
|
+
? await readJson<PrototypeIndexFile>(prototypeIndexPath)
|
|
80
|
+
: null;
|
|
81
|
+
const prototypeAssets = prototypeIndex?.assets || [];
|
|
55
82
|
|
|
56
83
|
const taskById = new Map<string, Task>();
|
|
57
84
|
for (const group of taskPlan.parallelGroups) {
|
|
@@ -70,6 +97,7 @@ export async function handoffCommand(options: HandoffOptions, command: Command):
|
|
|
70
97
|
for (const taskId of agent.assignedTasks) {
|
|
71
98
|
const task = taskById.get(taskId);
|
|
72
99
|
if (!task) continue;
|
|
100
|
+
const sourceChunks = guessSourceChunks(task, sourceChunkMap);
|
|
73
101
|
const mapped: HandoffTask = {
|
|
74
102
|
id: task.id,
|
|
75
103
|
name: task.name,
|
|
@@ -79,7 +107,8 @@ export async function handoffCommand(options: HandoffOptions, command: Command):
|
|
|
79
107
|
assignedAgent: agent.agentId,
|
|
80
108
|
assignedType: agentType,
|
|
81
109
|
estimatedHours: task.estimatedHours,
|
|
82
|
-
sourceChunks
|
|
110
|
+
sourceChunks,
|
|
111
|
+
prototypeRefs: matchTaskPrototypes(task, sourceChunks, prototypeAssets),
|
|
83
112
|
};
|
|
84
113
|
rows.push(mapped);
|
|
85
114
|
handoffTasks.push(mapped);
|
|
@@ -136,10 +165,12 @@ export async function handoffCommand(options: HandoffOptions, command: Command):
|
|
|
136
165
|
taskPlan: 'task_plan.json',
|
|
137
166
|
dispatchPlan: 'dispatch_plan.json',
|
|
138
167
|
summariesDir: options.includeSummaries ? 'summaries/' : null,
|
|
168
|
+
prototypeIndex: prototypeAssets.length > 0 ? 'prototype_index.json' : null,
|
|
139
169
|
},
|
|
140
170
|
totals: {
|
|
141
171
|
agents: agentTaskMap.size,
|
|
142
172
|
tasks: handoffTasks.length,
|
|
173
|
+
prototypeAssets: prototypeAssets.length,
|
|
143
174
|
},
|
|
144
175
|
tasks: handoffTasks,
|
|
145
176
|
unassigned: dispatch.unassigned,
|
|
@@ -152,6 +183,7 @@ export async function handoffCommand(options: HandoffOptions, command: Command):
|
|
|
152
183
|
target: normalizeTarget(options.target),
|
|
153
184
|
totalAgents: agentTaskMap.size,
|
|
154
185
|
totalTasks: handoffTasks.length,
|
|
186
|
+
totalPrototypeAssets: prototypeAssets.length,
|
|
155
187
|
includeSummaries: Boolean(options.includeSummaries),
|
|
156
188
|
}),
|
|
157
189
|
'utf-8'
|
|
@@ -164,6 +196,7 @@ export async function handoffCommand(options: HandoffOptions, command: Command):
|
|
|
164
196
|
target: normalizeTarget(options.target),
|
|
165
197
|
totalAgents: agentTaskMap.size,
|
|
166
198
|
totalTasks: handoffTasks.length,
|
|
199
|
+
totalPrototypeAssets: prototypeAssets.length,
|
|
167
200
|
outputPath,
|
|
168
201
|
bundlePath,
|
|
169
202
|
indexPath,
|
|
@@ -235,6 +268,8 @@ function renderTaskPrompt(input: {
|
|
|
235
268
|
const sourceChunkText = row.sourceChunks && row.sourceChunks.length > 0
|
|
236
269
|
? row.sourceChunks.map(i => `chunk_${i}_summary.json`).join(', ')
|
|
237
270
|
: '未定位到明确 source chunk,请从 spec_summary.json 补充判断';
|
|
271
|
+
const prototypeLines = (row.prototypeRefs || [])
|
|
272
|
+
.map(ref => `- ${ref.id} | \`${ref.path}\` | score=${ref.score} | ${ref.reason}${ref.summary ? ` | ${ref.summary}` : ''}`);
|
|
238
273
|
|
|
239
274
|
return [
|
|
240
275
|
`# Task Handoff - ${row.id} ${row.name}`,
|
|
@@ -259,11 +294,16 @@ function renderTaskPrompt(input: {
|
|
|
259
294
|
'- `task_plan.json`',
|
|
260
295
|
'- `dispatch_plan.json`',
|
|
261
296
|
includeSummaries ? '- `summaries/`' : '- (可选)`summaries/`',
|
|
297
|
+
'- (可选)`prototype_index.json` / `prototypes/*`',
|
|
262
298
|
'',
|
|
263
299
|
'## Suggested Evidence',
|
|
264
300
|
'',
|
|
265
301
|
`- ${sourceChunkText}`,
|
|
266
302
|
'',
|
|
303
|
+
'## Prototype References',
|
|
304
|
+
'',
|
|
305
|
+
...(prototypeLines.length > 0 ? prototypeLines : ['- 无直接匹配原型,必要时在 prototype_index.json 中人工筛选']),
|
|
306
|
+
'',
|
|
267
307
|
'## Execution Constraints',
|
|
268
308
|
'',
|
|
269
309
|
'- 只改与该任务直接相关文件;避免大范围重构',
|
|
@@ -309,20 +349,23 @@ function renderIndexMarkdown(input: {
|
|
|
309
349
|
target: string;
|
|
310
350
|
totalAgents: number;
|
|
311
351
|
totalTasks: number;
|
|
352
|
+
totalPrototypeAssets: number;
|
|
312
353
|
includeSummaries: boolean;
|
|
313
354
|
}): string {
|
|
314
|
-
const { target, totalAgents, totalTasks, includeSummaries } = input;
|
|
355
|
+
const { target, totalAgents, totalTasks, totalPrototypeAssets, includeSummaries } = input;
|
|
315
356
|
return [
|
|
316
357
|
'# Handoff Bundle Index',
|
|
317
358
|
'',
|
|
318
359
|
`- Target: ${target}`,
|
|
319
360
|
`- Total Agents: ${totalAgents}`,
|
|
320
361
|
`- Total Tasks: ${totalTasks}`,
|
|
362
|
+
`- Total Prototype Assets: ${totalPrototypeAssets}`,
|
|
321
363
|
`- Include Summaries: ${includeSummaries ? 'yes' : 'no'}`,
|
|
322
364
|
'',
|
|
323
365
|
'## Files',
|
|
324
366
|
'',
|
|
325
367
|
'- `handoff_bundle.json`: 机器可读任务总览',
|
|
368
|
+
'- `prototype_index.json`: 原型图索引(如存在)',
|
|
326
369
|
'- `agents/*.md`: 每个 agent 的任务队列',
|
|
327
370
|
'- `tasks/*.md`: 每个任务可直接投喂编码 Agent 的提示词模板',
|
|
328
371
|
'',
|
|
@@ -334,6 +377,67 @@ function renderIndexMarkdown(input: {
|
|
|
334
377
|
].join('\n');
|
|
335
378
|
}
|
|
336
379
|
|
|
380
|
+
function matchTaskPrototypes(
|
|
381
|
+
task: Task,
|
|
382
|
+
sourceChunks: number[] | undefined,
|
|
383
|
+
assets: PrototypeAssetEntry[]
|
|
384
|
+
): PrototypeMatch[] {
|
|
385
|
+
if (assets.length === 0) {
|
|
386
|
+
return [];
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const taskName = normalizeName(task.name);
|
|
390
|
+
const taskTokens = new Set(taskName.split(' ').filter(token => token.length >= 2));
|
|
391
|
+
const matches: PrototypeMatch[] = [];
|
|
392
|
+
|
|
393
|
+
for (const asset of assets) {
|
|
394
|
+
let score = 0;
|
|
395
|
+
const reasons: string[] = [];
|
|
396
|
+
|
|
397
|
+
const chunkHits = intersectCount(sourceChunks || [], asset.sourceChunks || []);
|
|
398
|
+
if (chunkHits > 0) {
|
|
399
|
+
score += 70 + Math.min(20, chunkHits * 5);
|
|
400
|
+
reasons.push(`chunk重合(${chunkHits})`);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const text = normalizeName(`${asset.alt || ''} ${asset.summary || ''} ${path.basename(asset.sourceFile || '')}`);
|
|
404
|
+
let tokenHits = 0;
|
|
405
|
+
for (const token of taskTokens) {
|
|
406
|
+
if (token.length >= 2 && text.includes(token)) {
|
|
407
|
+
tokenHits++;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
if (tokenHits > 0) {
|
|
411
|
+
score += Math.min(30, tokenHits * 8);
|
|
412
|
+
reasons.push(`关键词命中(${tokenHits})`);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (score >= 35) {
|
|
416
|
+
matches.push({
|
|
417
|
+
id: asset.id,
|
|
418
|
+
path: asset.path,
|
|
419
|
+
score,
|
|
420
|
+
reason: reasons.join(' + ') || '弱匹配',
|
|
421
|
+
summary: asset.summary,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return matches
|
|
427
|
+
.sort((a, b) => b.score - a.score)
|
|
428
|
+
.slice(0, 5);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function intersectCount(a: number[], b: number[]): number {
|
|
432
|
+
if (a.length === 0 || b.length === 0) return 0;
|
|
433
|
+
const set = new Set(a);
|
|
434
|
+
let count = 0;
|
|
435
|
+
for (const value of b) {
|
|
436
|
+
if (set.has(value)) count++;
|
|
437
|
+
}
|
|
438
|
+
return count;
|
|
439
|
+
}
|
|
440
|
+
|
|
337
441
|
function normalizeName(input: string): string {
|
|
338
442
|
return (input || '')
|
|
339
443
|
.toLowerCase()
|
package/src/commands/scan.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as path from 'path';
|
|
2
|
+
import * as fs from 'fs-extra';
|
|
2
3
|
import { Command } from 'commander';
|
|
3
4
|
import { Logger } from '../utils/logger';
|
|
4
5
|
import {
|
|
@@ -8,8 +9,7 @@ import {
|
|
|
8
9
|
formatSize,
|
|
9
10
|
parseSize,
|
|
10
11
|
findFiles,
|
|
11
|
-
writeJson
|
|
12
|
-
readFileContent
|
|
12
|
+
writeJson
|
|
13
13
|
} from '../utils/file';
|
|
14
14
|
import { parseDocument, analyzeBase64Images } from '../services/document-parser';
|
|
15
15
|
import {
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
getLLMConfigForPurpose,
|
|
20
20
|
validateLLMConfig
|
|
21
21
|
} from '../services/llm';
|
|
22
|
-
import { Manifest, Chunk } from '../types';
|
|
22
|
+
import { Manifest, Chunk, PrototypeAsset } from '../types';
|
|
23
23
|
|
|
24
24
|
interface ScanOptions {
|
|
25
25
|
input?: string;
|
|
@@ -53,6 +53,16 @@ const SCAN_EXIT_CODE = {
|
|
|
53
53
|
RUNTIME_ERROR: 10,
|
|
54
54
|
} as const;
|
|
55
55
|
|
|
56
|
+
interface PrototypeAssetPending {
|
|
57
|
+
id: string;
|
|
58
|
+
sourceFile: string;
|
|
59
|
+
alt: string;
|
|
60
|
+
mimeType: string;
|
|
61
|
+
estimatedSize: number;
|
|
62
|
+
dataUri: string;
|
|
63
|
+
summary?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
56
66
|
export async function scanCommand(options: ScanOptions, command: Command): Promise<void> {
|
|
57
67
|
const logger = new Logger();
|
|
58
68
|
|
|
@@ -118,6 +128,7 @@ export async function scanCommand(options: ScanOptions, command: Command): Promi
|
|
|
118
128
|
let llmChunkingFallbackFiles = 0;
|
|
119
129
|
let imageAssetsDetected = 0;
|
|
120
130
|
let imageAssetsDescribed = 0;
|
|
131
|
+
const pendingPrototypeAssets: PrototypeAssetPending[] = [];
|
|
121
132
|
if (useLLMChunking && llmConfig) {
|
|
122
133
|
// LLM chunking is enabled by default; fail fast so we don't silently degrade
|
|
123
134
|
// to regex-only behavior on complex documents.
|
|
@@ -129,23 +140,44 @@ export async function scanCommand(options: ScanOptions, command: Command): Promi
|
|
|
129
140
|
}
|
|
130
141
|
}
|
|
131
142
|
|
|
132
|
-
for (
|
|
143
|
+
for (let fileIdx = 0; fileIdx < inputFiles.length; fileIdx++) {
|
|
144
|
+
const filePath = inputFiles[fileIdx];
|
|
133
145
|
logger.info(`Parsing ${path.basename(filePath)}...`);
|
|
134
146
|
|
|
135
147
|
try {
|
|
136
148
|
const parsed = await parseDocument(filePath);
|
|
137
149
|
let contentForChunking = parsed.content;
|
|
138
|
-
|
|
150
|
+
const parsedImages = (parsed.images || []).map(image => ({
|
|
151
|
+
...image,
|
|
152
|
+
id: scopeImageId(fileIdx, image.id),
|
|
153
|
+
}));
|
|
154
|
+
const idMapping = new Map<string, string>();
|
|
155
|
+
for (let i = 0; i < (parsed.images || []).length; i++) {
|
|
156
|
+
idMapping.set(parsed.images![i].id, parsedImages[i].id);
|
|
157
|
+
}
|
|
158
|
+
contentForChunking = replaceImageIds(contentForChunking, idMapping);
|
|
159
|
+
imageAssetsDetected += parsedImages.length;
|
|
139
160
|
let imageSummaries: Record<string, string> = {};
|
|
140
161
|
|
|
141
|
-
if (
|
|
142
|
-
logger.info(` 检测到 ${
|
|
162
|
+
if (parsedImages.length > 0) {
|
|
163
|
+
logger.info(` 检测到 ${parsedImages.length} 张嵌入图片`);
|
|
143
164
|
if (llmConfig && llmConfig.apiKey) {
|
|
144
|
-
imageSummaries = await describeEmbeddedImages(
|
|
165
|
+
imageSummaries = await describeEmbeddedImages(parsedImages, llmConfig, logger);
|
|
145
166
|
imageAssetsDescribed += Object.keys(imageSummaries).length;
|
|
146
167
|
} else {
|
|
147
168
|
logger.warn(' 未配置 LLM,图片仅保留占位信息,不含语义摘要');
|
|
148
169
|
}
|
|
170
|
+
for (const image of parsedImages) {
|
|
171
|
+
pendingPrototypeAssets.push({
|
|
172
|
+
id: image.id,
|
|
173
|
+
sourceFile: filePath,
|
|
174
|
+
alt: image.alt,
|
|
175
|
+
mimeType: image.mimeType,
|
|
176
|
+
estimatedSize: image.estimatedSize,
|
|
177
|
+
dataUri: image.dataUri,
|
|
178
|
+
summary: imageSummaries[image.id],
|
|
179
|
+
});
|
|
180
|
+
}
|
|
149
181
|
}
|
|
150
182
|
|
|
151
183
|
// Log if base64 images were found and removed
|
|
@@ -235,7 +267,9 @@ export async function scanCommand(options: ScanOptions, command: Command): Promi
|
|
|
235
267
|
// Prepare chunks directory
|
|
236
268
|
const outputDir = path.dirname(path.resolve(options.output));
|
|
237
269
|
const chunksDir = path.join(outputDir, 'chunks');
|
|
270
|
+
const prototypesDir = path.join(outputDir, 'prototypes');
|
|
238
271
|
await ensureDir(chunksDir);
|
|
272
|
+
await ensureDir(prototypesDir);
|
|
239
273
|
|
|
240
274
|
// Write chunks to files
|
|
241
275
|
const chunks: Chunk[] = [];
|
|
@@ -244,7 +278,7 @@ export async function scanCommand(options: ScanOptions, command: Command): Promi
|
|
|
244
278
|
const chunkFileName = `chunk_${i}.txt`;
|
|
245
279
|
const chunkFilePath = path.join(chunksDir, chunkFileName);
|
|
246
280
|
|
|
247
|
-
await
|
|
281
|
+
await fs.writeFile(chunkFilePath, rawChunk.content, 'utf-8');
|
|
248
282
|
|
|
249
283
|
chunks.push({
|
|
250
284
|
id: i,
|
|
@@ -254,6 +288,13 @@ export async function scanCommand(options: ScanOptions, command: Command): Promi
|
|
|
254
288
|
});
|
|
255
289
|
}
|
|
256
290
|
|
|
291
|
+
const prototypeAssets = await writePrototypeAssets({
|
|
292
|
+
outputDir,
|
|
293
|
+
prototypesDir,
|
|
294
|
+
rawChunks,
|
|
295
|
+
pendingAssets: pendingPrototypeAssets,
|
|
296
|
+
});
|
|
297
|
+
|
|
257
298
|
// Create manifest
|
|
258
299
|
const manifest: Manifest = {
|
|
259
300
|
version: '1.0.0',
|
|
@@ -262,6 +303,7 @@ export async function scanCommand(options: ScanOptions, command: Command): Promi
|
|
|
262
303
|
totalSize: totalSize,
|
|
263
304
|
chunkSize: chunkSizeBytes,
|
|
264
305
|
chunks: chunks,
|
|
306
|
+
prototypeAssets,
|
|
265
307
|
};
|
|
266
308
|
|
|
267
309
|
// Write manifest
|
|
@@ -270,6 +312,9 @@ export async function scanCommand(options: ScanOptions, command: Command): Promi
|
|
|
270
312
|
await writeJson(outputPath, manifest);
|
|
271
313
|
|
|
272
314
|
logger.success(`Manifest created: ${outputPath}`);
|
|
315
|
+
if (prototypeAssets.length > 0) {
|
|
316
|
+
logger.info(`Prototype index: ${path.join(outputDir, 'prototype_index.json')}`);
|
|
317
|
+
}
|
|
273
318
|
logger.json({
|
|
274
319
|
status: 'success',
|
|
275
320
|
totalFiles: inputFiles.length,
|
|
@@ -281,6 +326,7 @@ export async function scanCommand(options: ScanOptions, command: Command): Promi
|
|
|
281
326
|
llmChunkingFallbackFiles,
|
|
282
327
|
imageAssetsDetected,
|
|
283
328
|
imageAssetsDescribed,
|
|
329
|
+
prototypeAssets: prototypeAssets.length,
|
|
284
330
|
manifestPath: outputPath,
|
|
285
331
|
});
|
|
286
332
|
|
|
@@ -527,7 +573,7 @@ function appendImageSummariesToChunk(content: string, imageSummaries: Record<str
|
|
|
527
573
|
return content;
|
|
528
574
|
}
|
|
529
575
|
|
|
530
|
-
const idMatches = content.match(/图片引用\s+(
|
|
576
|
+
const idMatches = content.match(/图片引用\s+([A-Z0-9_]+)/g) || [];
|
|
531
577
|
const imageIds = Array.from(new Set(idMatches.map(item => item.replace(/.*\s+/, '').trim())));
|
|
532
578
|
if (imageIds.length === 0) {
|
|
533
579
|
return content;
|
|
@@ -548,6 +594,113 @@ function appendImageSummariesToChunk(content: string, imageSummaries: Record<str
|
|
|
548
594
|
return `${content}\n\n### 图片语义补充\n${summaryLines.join('\n')}`;
|
|
549
595
|
}
|
|
550
596
|
|
|
597
|
+
async function writePrototypeAssets(input: {
|
|
598
|
+
outputDir: string;
|
|
599
|
+
prototypesDir: string;
|
|
600
|
+
rawChunks: Array<{ content: string; sourceFiles: string[]; title?: string }>;
|
|
601
|
+
pendingAssets: PrototypeAssetPending[];
|
|
602
|
+
}): Promise<PrototypeAsset[]> {
|
|
603
|
+
const { outputDir, prototypesDir, rawChunks, pendingAssets } = input;
|
|
604
|
+
if (pendingAssets.length === 0) {
|
|
605
|
+
return [];
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const prototypeAssets: PrototypeAsset[] = [];
|
|
609
|
+
for (const asset of pendingAssets) {
|
|
610
|
+
const ext = extFromMimeType(asset.mimeType);
|
|
611
|
+
const fileName = `${asset.id}.${ext}`;
|
|
612
|
+
const absolutePath = path.join(prototypesDir, fileName);
|
|
613
|
+
const written = await writeDataUriToFile(asset.dataUri, absolutePath);
|
|
614
|
+
if (!written) {
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
const sourceChunks = collectSourceChunks(rawChunks, asset.id);
|
|
618
|
+
prototypeAssets.push({
|
|
619
|
+
id: asset.id,
|
|
620
|
+
sourceFile: asset.sourceFile,
|
|
621
|
+
alt: asset.alt,
|
|
622
|
+
mimeType: asset.mimeType,
|
|
623
|
+
estimatedSize: asset.estimatedSize,
|
|
624
|
+
path: path.join('prototypes', fileName).replace(/\\/g, '/'),
|
|
625
|
+
summary: asset.summary,
|
|
626
|
+
sourceChunks,
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const indexJsonPath = path.join(outputDir, 'prototype_index.json');
|
|
631
|
+
await writeJson(indexJsonPath, {
|
|
632
|
+
version: '1.0.0',
|
|
633
|
+
createdAt: new Date().toISOString(),
|
|
634
|
+
total: prototypeAssets.length,
|
|
635
|
+
assets: prototypeAssets,
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
const indexMarkdownPath = path.join(outputDir, 'prototype_index.md');
|
|
639
|
+
const lines: string[] = [
|
|
640
|
+
'# Prototype Index',
|
|
641
|
+
'',
|
|
642
|
+
`- Total: ${prototypeAssets.length}`,
|
|
643
|
+
'',
|
|
644
|
+
'| ID | Path | Source File | Chunks | Summary |',
|
|
645
|
+
'| --- | --- | --- | --- | --- |',
|
|
646
|
+
];
|
|
647
|
+
for (const asset of prototypeAssets) {
|
|
648
|
+
lines.push(
|
|
649
|
+
`| ${asset.id} | \`${asset.path}\` | \`${path.basename(asset.sourceFile)}\` | ${asset.sourceChunks.join(', ') || '-'} | ${(asset.summary || '').replace(/\|/g, '\\|')} |`
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
await fs.writeFile(indexMarkdownPath, `${lines.join('\n')}\n`, 'utf-8');
|
|
653
|
+
|
|
654
|
+
return prototypeAssets;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function collectSourceChunks(
|
|
658
|
+
rawChunks: Array<{ content: string; sourceFiles: string[]; title?: string }>,
|
|
659
|
+
imageId: string
|
|
660
|
+
): number[] {
|
|
661
|
+
const results: number[] = [];
|
|
662
|
+
for (let i = 0; i < rawChunks.length; i++) {
|
|
663
|
+
if (rawChunks[i].content.includes(imageId)) {
|
|
664
|
+
results.push(i);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
return results;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function scopeImageId(fileIdx: number, imageId: string): string {
|
|
671
|
+
return `F${String(fileIdx + 1).padStart(3, '0')}_${imageId}`;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function replaceImageIds(content: string, idMapping: Map<string, string>): string {
|
|
675
|
+
let next = content;
|
|
676
|
+
for (const [fromId, toId] of idMapping.entries()) {
|
|
677
|
+
next = next.replace(new RegExp(`\\b${escapeRegExp(fromId)}\\b`, 'g'), toId);
|
|
678
|
+
}
|
|
679
|
+
return next;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function escapeRegExp(input: string): string {
|
|
683
|
+
return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function extFromMimeType(mimeType: string): string {
|
|
687
|
+
if (mimeType.includes('png')) return 'png';
|
|
688
|
+
if (mimeType.includes('jpeg') || mimeType.includes('jpg')) return 'jpg';
|
|
689
|
+
if (mimeType.includes('webp')) return 'webp';
|
|
690
|
+
if (mimeType.includes('gif')) return 'gif';
|
|
691
|
+
return 'img';
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
async function writeDataUriToFile(dataUri: string, filePath: string): Promise<boolean> {
|
|
695
|
+
const match = dataUri.match(/^data:image\/[a-zA-Z0-9.+-]+;base64,([A-Za-z0-9+/=]+)$/);
|
|
696
|
+
if (!match) {
|
|
697
|
+
return false;
|
|
698
|
+
}
|
|
699
|
+
const buffer = Buffer.from(match[1], 'base64');
|
|
700
|
+
await fs.writeFile(filePath, buffer);
|
|
701
|
+
return true;
|
|
702
|
+
}
|
|
703
|
+
|
|
551
704
|
function readStdin(): Promise<string> {
|
|
552
705
|
return new Promise((resolve) => {
|
|
553
706
|
let data = '';
|
package/src/services/llm.ts
CHANGED
|
@@ -36,19 +36,10 @@ export function getLLMConfig(): LLMConfig {
|
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
export function getLLMConfigForPurpose(
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
analyze: process.env.LLM_MODEL_ANALYZE,
|
|
44
|
-
vision: process.env.LLM_MODEL_VISION,
|
|
45
|
-
default: process.env.LLM_MODEL,
|
|
46
|
-
};
|
|
47
|
-
const selectedModel = purposeModelMap[purpose] || base.model;
|
|
48
|
-
return {
|
|
49
|
-
...base,
|
|
50
|
-
model: selectedModel,
|
|
51
|
-
};
|
|
39
|
+
export function getLLMConfigForPurpose(_purpose: 'scan' | 'analyze' | 'vision' | 'default'): LLMConfig {
|
|
40
|
+
// Unified model strategy: always use LLM_MODEL.
|
|
41
|
+
// Keep this function for backward-compatible call sites.
|
|
42
|
+
return getLLMConfig();
|
|
52
43
|
}
|
|
53
44
|
|
|
54
45
|
export function validateLLMConfig(config: LLMConfig): void {
|
|
@@ -348,7 +339,7 @@ export async function describeEmbeddedImages(
|
|
|
348
339
|
|
|
349
340
|
const visionConfig = {
|
|
350
341
|
...config,
|
|
351
|
-
model:
|
|
342
|
+
model: getLLMConfig().model || config.model,
|
|
352
343
|
};
|
|
353
344
|
validateLLMConfig(visionConfig);
|
|
354
345
|
const maxImages = Math.max(0, parseEnvInt('LLM_IMAGE_MAX_PER_DOC', 12));
|
package/src/types.ts
CHANGED
|
@@ -10,6 +10,17 @@ export interface Chunk {
|
|
|
10
10
|
endLine?: number;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
export interface PrototypeAsset {
|
|
14
|
+
id: string;
|
|
15
|
+
sourceFile: string;
|
|
16
|
+
alt: string;
|
|
17
|
+
mimeType: string;
|
|
18
|
+
estimatedSize: number;
|
|
19
|
+
path: string;
|
|
20
|
+
summary?: string;
|
|
21
|
+
sourceChunks: number[];
|
|
22
|
+
}
|
|
23
|
+
|
|
13
24
|
export interface Manifest {
|
|
14
25
|
version: string;
|
|
15
26
|
createdAt: string;
|
|
@@ -17,6 +28,7 @@ export interface Manifest {
|
|
|
17
28
|
totalSize: number;
|
|
18
29
|
chunkSize: number;
|
|
19
30
|
chunks: Chunk[];
|
|
31
|
+
prototypeAssets?: PrototypeAsset[];
|
|
20
32
|
}
|
|
21
33
|
|
|
22
34
|
export interface Feature {
|