This commit is contained in:
2026-01-10 16:58:15 +08:00
commit 48e3033639
53 changed files with 41267 additions and 0 deletions

40
prompts/src/app.ts Normal file
View File

@@ -0,0 +1,40 @@
import { JimengService } from './services/jimeng.service.ts';
import { OSSService } from './services/oss.service.ts';
import { PBService } from './services/pb.service.ts';
import { useConfig } from '@kevisual/use-config';
import { App } from '@kevisual/router'
import { useContextKey } from '@kevisual/context';
import { getRedisConnection } from './module/redis.ts';
import { Kevisual } from '@kevisual/ai';
export const config = useConfig();
export const redis = useContextKey('redis', () => getRedisConnection());
export const jimengService = useContextKey('jimeng', new JimengService({
apiKey: config.JIMENG_API_KEY,
baseUrl: config.JIMENG_API_URL,
timeout: parseInt(config.JIMENG_TIMEOUT || '300000'),
}));
export {
getRedisConnection
}
export const ossService = useContextKey('oss', new OSSService({
accessKeyId: config.S3_ACCESS_KEY_ID,
accessKeySecret: config.S3_ACCESS_KEY_SECRET,
bucketName: config.S3_BUCKET_NAME,
region: config.S3_REGION,
endpoint: config.S3_ENDPOINT,
prefix: 'projects/horse/',
}));
export const pbService = useContextKey('pb', new PBService({
url: config.POCKETBASE_URL,
token: config.POCKETBASE_TOKEN,
}));
export const app = useContextKey('app', new App());
export const ai = useContextKey('ai', new Kevisual({
apiKey: config.KEVISUAL_NEW_API_KEY,
model: 'qwen-plus',
}));
export const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));

20
prompts/src/index.ts Normal file
View File

@@ -0,0 +1,20 @@
import { PromptGenerator, type PromptGeneratorOptions } from "./module/prompt-geneator.ts";
import { writeFile } from "node:fs/promises";
import { Prompt } from "./module/prompt-perfect.ts";
import { customAlphabet } from "nanoid";
const letter = 'abcdefghijklmnopqrstuvwxyz'
const randomString = customAlphabet(letter, 16);
async function saveToFile(data: Map<string, string>, outputPath: string): Promise<void> {
const arrayData = Array.from(data.entries()).map(([key, value]) => ({ key, value, id: randomString() }));
await writeFile(outputPath, JSON.stringify(arrayData, null, 2), "utf-8");
console.log(`Generated ${arrayData.length} prompts and saved to ${outputPath}`);
}
import './routes/index.ts';
export * from './app.ts';
// list all routes and import
export { PromptGenerator, PromptGeneratorOptions, saveToFile, Prompt };

View File

@@ -0,0 +1,9 @@
import { useConfig } from '@kevisual/use-config';
export const config = useConfig();
export const redisConfig = {
host: config.REDIS_HOST || 'localhost',
port: parseInt(config.REDIS_PORT || '6379'),
password: config.REDIS_PASSWORD || undefined,
db: parseInt(config.REDIS_DB || '0'),
};

View File

@@ -0,0 +1,12 @@
import { Logger } from "@kevisual/logger";
import { config } from '@/app.ts'
import { FeishuNotifier } from "@kevisual/notifier";
export const logger = new Logger({
level: config.LOG_LEVEL || 'info',
});
export const feishuNotifier = new FeishuNotifier({
webhook: config.FEISHU_NOTIFY_WEBHOOK_URL || '',
});
export const notify = feishuNotifier;

View File

@@ -0,0 +1,91 @@
import { randomInt as random } from "es-toolkit";
import { max } from "es-toolkit/compat";
export interface PromptGeneratorOptions {
count?: number;
outputPath?: string;
sourceKeys?: string[];
additionalValues?: string[];
source?: { [key: string]: string[] };
endString?: string;
}
export class PromptGenerator {
sourceKeys: string[];
additionalValues: string[];
count: number;
source: { [key: string]: string[] } = {};
endString: string = "";
map: Map<string, string> = new Map();
constructor(options: PromptGeneratorOptions = {}) {
this.additionalValues = options.additionalValues || [];
this.count = options.count ?? 1000;
this.source = options.source || {};
if (options?.sourceKeys) {
this.sourceKeys = options.sourceKeys || [];
} else {
this.sourceKeys = Object.keys(this.source);
}
this.endString = options.endString || "";
}
generateMap(opts?: { max?: number }): Map<string, string> {
const map = new Map<string, string>();
const source = this.source
let _max = max([opts?.max || 1200, this.count + 1000])!;
let k = 0;
while (map.size < this.count) {
const selectedValues: string[] = [];
this.sourceKeys.forEach((key: string) => {
const values = source[key];
if (!values) {
return;
}
const randomIndex = random(0, values.length - 1);
const randomValue = values[randomIndex];
selectedValues.push(randomValue);
});
const randomSlect = random(1, 3);
const additionalRandomValue = this.pickRandomValue(randomSlect, this.additionalValues);
selectedValues.push(...additionalRandomValue);
if (this.endString) {
selectedValues.push(this.endString);
}
const key = selectedValues.join(",");
k++;
if (k > _max) {
console.warn(`Reached maximum attempts (${_max}) to generate unique prompts. Generated ${map.size} unique prompts.`);
break;
}
if (!map.has(key)) {
map.set(key, selectedValues.join(" "));
}
}
this.map = map;
return map;
}
/**
* 获取随机附加值
* @param num
* @param arr
* @returns
*/
pickRandomValue(num: number, arr: string[]): string[] {
const randomAdditionalValues: string[] = [];
const usedIndices: Set<number> = new Set();
if (num > arr.length) {
num = Math.min(num, arr.length - 1);
}
while (randomAdditionalValues.length < num && usedIndices.size < arr.length) {
const randomIndex = random(0, arr.length - 1);
if (!usedIndices.has(randomIndex)) {
usedIndices.add(randomIndex);
randomAdditionalValues.push(arr[randomIndex]);
}
}
return randomAdditionalValues;
}
}

View File

@@ -0,0 +1,18 @@
export interface PromptOptions {
perfectPrompt?: string;
}
const DefaultPerfectPrompt = "我需要你帮我完善提示词,使其更加详细和具体。\n";
export class Prompt {
perfectPrompt: string = "";
constructor(options: PromptOptions = {}) {
this.perfectPrompt = options.perfectPrompt || DefaultPerfectPrompt;
}
perfect(info: string): string {
return `${info}\n当前的提示词是在<perfect/>标签中:\n<perfect>${this.perfectPrompt}</perfect>`;
}
clearPerfectTags(text: string): string {
return text.replace('<perfect>', '').replace('</perfect>', '').trim();
}
}

View File

@@ -0,0 +1,29 @@
import { Redis } from 'ioredis';
import { redisConfig } from './config.ts'
export interface RedisConfig {
host: string;
port: number;
password?: string;
db: number;
}
let redisConnection: Redis | null = null;
export const getRedisConnection = () => {
if (!redisConnection) {
redisConnection = new Redis({
...redisConfig,
maxRetriesPerRequest: null
});
redisConnection.on('connect', () => {
console.log('Redis connected');
});
redisConnection.on('error', (err) => {
console.error('Redis connection error:', err);
});
}
return redisConnection as any;
};

View File

@@ -0,0 +1,343 @@
import { randomInt as random } from 'es-toolkit';
// ============================================
// 关键词库定义
// ============================================
// 主主题关键词(带子主题)
export const MAIN_THEMES: Record<string, string[]> = {
: ['旅程', '剧本', '角色', '舞台', '起点', '终点', '过程', '意义', '价值', '方向'],
: ['过去', '现在', '未来', '瞬间', '永恒', '季节', '昼夜', '钟表', '沙漏', '流水'],
: ['伤痕', '蜕变', '破茧', '跌倒', '镜子', '旧我', '觉醒', '代价', '忍耐', '成熟'],
: ['独处', '夜晚', '人群疏离', '无人理解', '自我对话', '清醒者', '边缘人', '静默', '影子'],
: ['路口', '岔路', '方向', '代价', '后悔', '坚定', '承担', '割舍', '决定', '权衡'],
: ['翅膀', '天空', '边界', '束缚', '解脱', '随心所欲', '主宰', '释放', '翱翔', '远方'],
: ['伤疤', '火焰', '锤炼', '熔炉', '泪水', '黑夜', '黎明', '破碎', '重建', '结晶'],
: ['温暖', '刺痛', '放手', '成全', '燃烧', '熄灭', '靠近', '远离', '心跳', '温柔'],
: ['终结', '新生', '虚无', '告别', '离开', '遗忘', '铭记', '永恒', '轮回', '起点'],
: ['本质', '表象', '谎言', '虚假', '真实', '假象', '核心', '事实', '表面', '深层'],
: ['克制', '追求', '放下', '贪念', '满足', '渴望', '执念', '放手', '贪婪', '节制'],
: ['执着', '韧劲', '突破', '恒心', '毅力', '不放弃', '继续', '前行', '攀登', '坚守'],
: ['释然', '接纳', '告别', '轻松', '释怀', '松手', '转身', '遗忘', '宽恕', '和解'],
: ['开悟', '看见', '醒来', '明白', '领悟', '理解', '懂得', '发现', '觉悟', '洞悉'],
: ['普通', '真实', '踏实', '日常', '简单', '朴素', '寻常', '平淡', '自然', '安宁'],
: ['价值', '方向', '目的', '追求', '使命', '理由', '答案', '方向感', '存在感', '理由'],
: ['安静', '力量', '倾听', '静默', '无言', '无声', '寂静', '宁静', '平和', '镇定'],
: ['注定', '改变', '巧合', '安排', '转折', '际遇', '天意', '偶然', '必然', '定数'],
: ['善恶', '复杂', '真实', '面具', '伪装', '本质', '脆弱', '坚强', '自私', '无私'],
: ['规则', '真相', '边界', '运转', '秩序', '法则', '规律', '现实', '规则', '边界']
};
// 通用意象关键词
export const IMAGES = [
'光', '暗', '路', '门', '风', '火', '水', '树', '种子', '根', '山', '海', '雾', '雨',
'雪', '霜', '雷', '电', '云', '星', '月', '日', '花', '草', '叶', '果实', '飞鸟',
'游鱼', '蝴蝶', '灯塔', '镜子', '书本', '信', '钟', '钥匙', '锁', '刀', '剑', '帆',
'船', '桥', '窗', '塔', '影', '回声', '脚步', '呼吸', '尘埃', '烟雾', '碎片'
];
// 动词关键词
export const VERBS = [
'燃烧', '沉淀', '穿越', '凝视', '听见', '醒来', '破碎', '重建', '等待', '放手',
'坠落', '升起', '沉默', '挣扎', '回归', '追随', '停留', '遗忘', '铭记', '看清',
'承认', '接受', '抵抗', '屈服', '生长', '凋零', '绽放', '枯萎', '流动', '凝固',
'感受', '体会', '领悟', '明白', '理解', '懂得', '发现', '寻找', '追求', '失去',
'获得', '前行', '停留', '奔跑', '行走', '停留', '守望', '等待', '出发', '到达'
];
// 句式模板(带占位符)
export const TEMPLATES = [
// 对比型
{ type: '对比', text: '真正的{主},不是{子},而是{子}。' },
{ type: '对比', text: '越{子},越{子}。' },
{ type: '对比', text: '{主}与{子},从来不是对立的。' },
{ type: '对比', text: '不是{主}太远,而是{子}太近。' },
{ type: '对比', text: '{主}从不说谎,{子}却常常欺骗。' },
{ type: '对比', text: '不怕{主},只怕{动}。' },
{ type: '对比', text: '{主}从不是{子},而是{意义}。' },
{ type: '对比', text: '最高级的{主},是{动}{意义}。' },
{ type: '对比', text: '最深的{主},往往{动}。' },
{ type: '对比', text: '{主}的尽头,是{动}{意义}。' },
// 因果型
{ type: '因果', text: '当你{动}{主},你才懂得{意义}。' },
{ type: '因果', text: '只有{动}过{主}的人,才明白{意义}。' },
{ type: '因果', text: '{主}教会我,{动}才是{意义}。' },
{ type: '因果', text: '经历过{主},才知道{意义}的可贵。' },
{ type: '因果', text: '{主}的代价,是{动}{意义}。' },
{ type: '因果', text: '因为{动}{主},所以{动}{意义}。' },
{ type: '因果', text: '{主}让我明白,{动}才是真正的{意义}。' },
// 隐喻型
{ type: '隐喻', text: '{主}像{意象},终将{动}。' },
{ type: '隐喻', text: '{意象}不会说话,却教会了我{动}{意义}。' },
{ type: '隐喻', text: '{主}是{意象}的镜子,照见{意义}。' },
{ type: '隐喻', text: '在{主}中,我看见了{意义}。' },
{ type: '隐喻', text: '{主}如同{意象},{动}才是{意义}。' },
{ type: '隐喻', text: '{意象}见证了{主}的{子}。' },
{ type: '隐喻', text: '{主}是{意象},需要{动}才能明白{意义}。' },
{ type: '隐喻', text: '当{意象}遇见{主},{动}才是唯一的{意义}。' },
];
// ============================================
// 生成器类
// ============================================
export interface SentenceGeneratorOptions {
/** 生成数量 */
count?: number;
/** 使用的模板类型 */
templateTypes?: ('对比' | '因果' | '隐喻' | '判断')[];
/** 使用的主主题 */
mainThemes?: string[];
/** 输出格式 */
outputFormat?: 'array' | 'text' | 'json';
/** 是否包含标签 */
withTags?: boolean;
}
export interface GeneratedSentence {
text: string;
theme: string;
template: string;
templateType: string;
tags: string[];
index: number;
}
export class SentenceGenerator {
private options: Required<SentenceGeneratorOptions>;
private themeUsage: Map<string, number> = new Map();
private generatedCount: number = 0;
constructor(options: SentenceGeneratorOptions = {}) {
this.options = {
count: options.count ?? 100,
templateTypes: options.templateTypes ?? ['对比', '因果', '隐喻'],
mainThemes: options.mainThemes ?? Object.keys(MAIN_THEMES),
outputFormat: options.outputFormat ?? 'array',
withTags: options.withTags ?? true
};
}
/**
* 安全获取随机数组元素
*/
private pickRandom<T>(arr: T[]): T | undefined {
if (arr.length === 0) return undefined;
return arr[random(0, arr.length - 1)];
}
/**
* 获取可用主题(考虑均衡使用)
*/
private pickTheme(): string {
const { mainThemes, count } = this.options;
const threshold = count / mainThemes.length * 1.5;
const availableThemes = mainThemes.filter(theme => {
const usage = this.themeUsage.get(theme) || 0;
return usage < threshold;
});
const themes = availableThemes.length > 0 ? availableThemes : mainThemes;
const theme = this.pickRandom(themes) || mainThemes[0];
this.themeUsage.set(theme, (this.themeUsage.get(theme) || 0) + 1);
return theme;
}
/**
* 填充模板
*/
private fillTemplate(template: string, theme: string, subTheme: string, image: string, verb: string, meaning: string): string {
return template
.replace(/\{主\}/g, theme)
.replace(/\{子\}/g, subTheme)
.replace(/\{意象\}/g, image)
.replace(/\{动\}/g, verb)
.replace(/\{意义\}/g, meaning);
}
/**
* 生成单条句子
*/
private generateOne(): GeneratedSentence {
// 选择主主题
const theme = this.pickTheme();
// 选择子主题
const subTheme = this.pickRandom(MAIN_THEMES[theme]) || theme;
// 选择其他关键词
const image = this.pickRandom(IMAGES) || '';
const verb = this.pickRandom(VERBS) || '';
const meaning = this.pickRandom(MAIN_THEMES['意义']) || '意义';
// 选择模板
const availableTemplates = TEMPLATES.filter(t =>
this.options.templateTypes.includes(t.type as any)
);
const templateObj = this.pickRandom(availableTemplates);
if (!templateObj) {
throw new Error('没有可用的模板');
}
// 填充模板
let text = this.fillTemplate(templateObj.text, theme, subTheme, image, verb, meaning);
// 清理多余空格
text = text.replace(/\s+/g, ' ').trim();
this.generatedCount++;
return {
text,
theme,
template: templateObj.text,
templateType: templateObj.type,
tags: [theme, templateObj.type, subTheme, image, verb],
index: this.generatedCount
};
}
/**
* 生成句子
*/
generate(): GeneratedSentence[] {
const results: GeneratedSentence[] = [];
const maxAttempts = this.options.count * 10;
let attempts = 0;
while (results.length < this.options.count && attempts < maxAttempts) {
attempts++;
const sentence = this.generateOne();
// 去重
const isDuplicate = results.some(s => s.text === sentence.text);
if (!isDuplicate) {
results.push(sentence);
}
}
if (results.length < this.options.count) {
console.warn(`只生成了 ${results.length} 条句子,达到最大尝试次数`);
}
return results;
}
/**
* 生成并输出
*/
generateAndOutput(): string | GeneratedSentence[] {
return this.generate();
}
/**
* 导出为CSV
*/
exportToCSV(): string {
const results = this.generate();
const headers = ['序号', '句子', '主题', '模板类型', '标签'];
const rows = results.map(s => [
s.index.toString(),
`"${s.text}"`,
s.theme,
s.templateType,
s.tags.join(';')
]);
return [headers.join(','), ...rows.map(r => r.join(','))].join('\n');
}
/**
* 导出为JSON
*/
exportToJSON(): string {
const results = this.generate();
return JSON.stringify({
meta: {
total: results.length,
generatedAt: new Date().toISOString(),
generator: 'sentence-generator.ts',
version: '1.0'
},
sentences: results
}, null, 2);
}
/**
* 获取统计信息
*/
getStats(): Record<string, number> {
return Object.fromEntries(this.themeUsage);
}
/**
* 重置
*/
reset(): void {
this.generatedCount = 0;
this.themeUsage.clear();
}
}
// ============================================
// 便捷函数
// ============================================
export function generatePhilosophicalSentences(
count: number = 100,
options: Partial<SentenceGeneratorOptions> = {}
): GeneratedSentence[] {
const generator = new SentenceGenerator({ count, ...options });
return generator.generate();
}
export function generateByTheme(theme: string, count: number = 20): GeneratedSentence[] {
const generator = new SentenceGenerator({ count, mainThemes: [theme] });
return generator.generate();
}
export function generateByTemplate(
templateType: '对比' | '因果' | '隐喻' | '判断',
count: number = 50
): GeneratedSentence[] {
const generator = new SentenceGenerator({ count, templateTypes: [templateType] });
return generator.generate();
}
// ============================================
// CLI入口
// ============================================
const isMainModule = process.argv[1]?.includes('sentence-generator');
if (isMainModule) {
const args = process.argv.slice(2);
const count = parseInt(args[0]) || 100;
const format = (args[1] as 'text' | 'json' | 'csv') || 'text';
console.log(`生成 ${count} 条哲理句...\n`);
const generator = new SentenceGenerator({
count,
templateTypes: ['对比', '因果', '隐喻']
});
if (format === 'csv') {
console.log(generator.exportToCSV());
} else if (format === 'json') {
console.log(generator.exportToJSON());
} else {
const sentences = generator.generate();
sentences.forEach(s => {
console.log(`${s.index}. ${s.text} [${s.theme}]`);
});
}
console.log('\n统计信息');
console.log(generator.getStats());
}
export default SentenceGenerator;

View File

@@ -0,0 +1,56 @@
import { Prompt } from "./prompt-perfect.ts"
const exampleData = {
"text": "选择像锁,终将奔跑。",
"theme": "选择",
"template": "{主}像{意象},终将{动}。",
"templateType": "隐喻",
"tags": [
"选择",
"隐喻",
"后悔",
"锁",
"奔跑"
],
"index": 7,
"optimized": "选择如秤,终需掂量。", // 要展示的句子
}
const perfect2 = `你是一个专业的海报设计师和文案策划。请根据以下句子数据,设计一张富有启发性的海报:
**句子信息**
- 主题:{theme}
- 模板:{template}
- 模板类型:{templateType}
- 标签:{tags}
- 待优化句子:{text}
- 推荐展示文案:{optimized}
**设计要求**
1. **文案优化**
- 以 {optimized} 为基础进一步精炼成适合海报展示的短句8-15字
- 语言要有力量感,能引起共鸣
- 可运用比喻、排比等修辞手法增强表现力
2. **意象选择**
- 根据 {tags} 和主题选择1-2个视觉意象
- 意象要能准确传达句子的情感和哲理
- 推荐风格:温暖励志/清新自然/深沉思考/极简有力
3. **视觉风格**
- 现代简约,有质感
- 文字与画面和谐统一
- 适合社交媒体传播
**输出格式**
海报短句:[精炼后的短句]
视觉意象:[1-2个核心意象]
设计风格:[推荐风格]
设计说明:[50字以内的设计思路]
请直接输出以上内容,不要有其他解释。`
export class SentenceImage extends Prompt {
constructor() {
super({ perfectPrompt: perfect2 });
}
}

View File

@@ -0,0 +1,396 @@
import { config } from './config.ts';
import { readFileSync, writeFileSync } from 'node:fs';
import { Kevisual } from '@kevisual/ai';
// ============================================
// AI优化提示词配置
// ============================================
// 通用哲理句优化提示词
export const PERFECT_PROMPT = `你是一个深谙人生哲理的作家。请将以下不完整的哲理句补充完整,使其成为一句发人深省的哲理名言。
要求:
1. 保持原句的核心主题和结构
2. 语言要简洁有力,富有哲理深度
3. 让人读后有所思考和共鸣
4. 符合中文表达习惯
5. 字数控制在20-50字之间
原句:{{sentence}}
请直接输出优化后的句子,不需要任何解释。`;
// 不同主题的专项优化提示词
export const THEME_PROMPTS: Record<string, string> = {
: `你是一个智慧的导师。请将以下关于人生的句子优化成一句深刻的人生感悟:
要求:
- 语言平实却蕴含智慧
- 让人读后有所触动
- 适合作为人生格言
原句:{{sentence}}
直接输出优化后的句子。`,
: `你是一个洞察时光的智者。请将以下关于时间的句子优化:
要求:
- 体现时间的珍贵与无情
- 让人珍惜当下
- 富有诗意但不晦涩
原句:{{sentence}}
直接输出优化后的句子。`,
: `你是一个经历过蜕变的智者。请将以下关于成长的句子优化:
要求:
- 体现成长的代价与收获
- 让人对成长有新的理解
- 有力量感但不鸡汤
原句:{{sentence}}
直接输出优化后的句子。`,
: `你是一个深刻理解孤独的人。请将以下关于孤独的句子优化:
要求:
- 准确描述孤独的本质
- 不是消极,而是清醒
- 让人学会与孤独和解
原句:{{sentence}}
直接输出优化后的句子。`,
: `你是一个懂得爱的人。请将以下关于爱的句子优化:
要求:
- 体现爱的真谛
- 不是空洞的情话
- 让人对爱有新的认识
原句:{{sentence}}
直接输出优化后的句子。`,
: `你是一个善于抉择的人。请将以下关于选择的句子优化:
要求:
- 体现选择的重量
- 不是教条,而是智慧
- 让人更慎重地面对选择
原句:{{sentence}}
直接输出优化后的句子。`,
: `你是一个追求自由的人。请将以下关于自由的句子优化:
要求:
- 准确描述自由的真谛
- 让人理解自由的代价
- 有深度但不晦涩
原句:{{sentence}}
直接输出优化后的句子。`,
: `你是一个从痛苦中走过来的人。请将以下关于痛苦的句子优化:
要求:
- 不是宣泄负面情绪
- 而是让人从痛苦中获得力量
- 转化痛苦为成长
原句:{{sentence}}
直接输出优化后的句子。`
};
// ============================================
// AI客户端接口
// ============================================
export interface PerfectOptions {
/** Kevisual AI 实例 */
ai?: Kevisual;
/** 使用的提示词模板 */
promptTemplate?: string;
/** 并发请求数 */
concurrency?: number;
/** 重试次数 */
retryTimes?: number;
/** 成功后回调 */
onSuccess?: (item: PerfectItem) => void;
/** 失败后回调 */
onError?: (item: PerfectError) => void;
/** 进度回调 */
onProgress?: (progress: ProgressInfo) => void;
}
export interface PerfectItem {
index: number;
text: string;
template: string;
templateType: string;
optimized: string;
prompt?: string;
theme: string;
tags: string[];
}
export interface PerfectError {
index: number;
original: string;
error: string;
theme: string;
}
export interface ProgressInfo {
current: number;
total: number;
percentage: number;
successCount: number;
errorCount: number;
}
// ============================================
// 默认AI实例
// ============================================
function createDefaultAI(): Kevisual {
return new Kevisual({
apiKey: config.KEVISUAL_NEW_API_KEY || '',
});
}
// ============================================
// 句子优化器类
// ============================================
export class SentencePerfect {
private options: Required<PerfectOptions>;
private ai: Kevisual;
constructor(options: PerfectOptions = {}) {
this.ai = options.ai || createDefaultAI();
this.options = {
ai: this.ai,
promptTemplate: options.promptTemplate || PERFECT_PROMPT,
concurrency: options.concurrency ?? 5,
retryTimes: options.retryTimes ?? 3,
onSuccess: options.onSuccess || (() => { }),
onError: options.onError || (() => { }),
onProgress: options.onProgress || (() => { })
};
}
/**
* 获取主题对应的提示词
*/
private getThemePrompt(theme: string, sentence: string): string {
const themePrompt = THEME_PROMPTS[theme];
if (themePrompt) {
return themePrompt.replace('{{sentence}}', sentence);
}
return this.options.promptTemplate.replace('{{sentence}}', sentence);
}
/**
* 优化单条句子(带重试)
*/
async perfectOne(item: PerfectItem): Promise<PerfectItem> {
const prompt = this.getThemePrompt(item.theme, item.text);
let lastError: Error | null = null;
for (let i = 0; i < this.options.retryTimes; i++) {
try {
await this.ai.chat([{ role: 'user', content: prompt }]);
let optimized = this.ai.responseText || '';
optimized = optimized.trim().replace(/^["']|["']$/g, '');
const result: PerfectItem = {
...item,
prompt: prompt,
optimized: optimized || item.text
};
this.options.onSuccess(result);
return result;
} catch (error) {
lastError = error as Error;
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
}
}
const error: PerfectError = {
index: item.index,
original: item.text,
error: lastError?.message || '未知错误',
theme: item.theme
};
this.options.onError(error);
return {
...item,
optimized: item.text
};
}
/**
* 批量优化句子
*/
async perfectBatch(
items: PerfectItem[]
): Promise<PerfectItem[]> {
const total = items.length;
let successCount = 0;
let errorCount = 0;
const results: PerfectItem[] = [];
const batchSize = this.options.concurrency;
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
const promises = batch.map(async (item) => {
try {
const result = await this.perfectOne(item);
results.push(result);
successCount++;
} catch {
errorCount++;
}
const current = Math.min(i + batchSize, total);
const percentage = Math.round((current / total) * 100);
this.options.onProgress({
current,
total,
percentage,
successCount,
errorCount
});
});
await Promise.all(promises);
if (i + batchSize < items.length) {
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
return results;
}
/**
* 从文件加载句子并优化
*/
async perfectFromFile(
inputPath: string,
outputPath: string
): Promise<{ success: number; failed: number; results: any[] }> {
const data = JSON.parse(readFileSync(inputPath, 'utf-8'));
const sentences = data.sentences || data;
const items: PerfectItem[] = sentences.map((s: any, i: number) => ({
index: s.index || i + 1,
text: s.text || s,
template: s.template || '',
templateType: s.templateType || '',
theme: s.theme || '人生',
tags: s.tags || [],
optimized: ''
}));
const results = await this.perfectBatch(items);
// 合并原始数据与优化结果
const mergedResults = results.map((r, i) => {
return {
index: r.index,
text: r.text,
template: r.template,
templateType: r.templateType,
theme: r.theme,
tags: r.tags,
optimized: r.optimized
};
});
const successResults = results.filter(r => r.optimized !== r.text);
const failedResults = results.filter(r => r.optimized === r.text);
const output = {
meta: {
total: results.length,
success: successResults.length,
failed: failedResults.length,
optimizedAt: new Date().toISOString()
},
sentences: mergedResults
};
writeFileSync(outputPath, JSON.stringify(output, null, 2), 'utf-8');
return {
success: successResults.length,
failed: failedResults.length,
results: mergedResults
};
}
/**
* 设置AI实例
*/
setAI(ai: Kevisual): void {
this.ai = ai;
this.options.ai = ai;
}
}
// ============================================
// 便捷函数
// ============================================
/**
* 快速优化单条句子
*/
export async function quickPerfect(
sentence: string,
theme: string = '人生'
): Promise<string> {
const perfect = new SentencePerfect();
const item: PerfectItem = {
index: 0,
text: sentence,
template: '',
templateType: '',
theme,
tags: [],
optimized: ''
};
const result = await perfect.perfectOne(item);
return result.optimized;
}
/**
* 从JSON文件优化句子
*/
export async function perfectFromJSON(
inputPath: string,
outputPath: string,
options: Partial<PerfectOptions> = {}
): Promise<{ success: number; failed: number }> {
const perfect = new SentencePerfect(options);
const result = await perfect.perfectFromFile(inputPath, outputPath);
return { success: result.success, failed: result.failed };
}
export default SentencePerfect;

View File

@@ -0,0 +1,74 @@
import { app, ossService, pbService, redis } from '@/app.ts'
import { addImageGenerateJob } from '@/task/image-creator.job.ts';
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
type SentenceItem = {
text: string;
theme: string;
template: string;
templateType: string;
tags: string[];
index: number;
prompt?: string;
optimized?: string;
}
app.route({
path: 'image-creator',
key: 'create-sentence-list',
description: '导入句子列表',
middleware: ['auth']
}).define(async (ctx) => {
const data: SentenceItem[] = ctx.query?.data || [];
if (!Array.isArray(data) || data.length === 0) {
ctx.throw(400, 'Invalid input data');
}
const BATCH_SIZE = 50;
for (let i = 0; i < data.length; i += BATCH_SIZE) {
const chunk = data.slice(i, i + BATCH_SIZE);
const batch = pbService.client.createBatch();
for (const item of chunk) {
batch.collection(pbService.collectionName).create({
text: '句子生成海报',
summary: item.optimized,
description: '',
status: '创建',
tags: ['sentence', ...item.tags],
data: {
sentence: item
}
});
}
await batch.send();
console.log(`Processed batch ${Math.floor(i / BATCH_SIZE) + 1}: ${chunk.length} items.`);
await sleep(200); // 避免短时间内添加过多请求
}
ctx.body = { message: `Imported ${data.length} sentences.` };
}).addTo(app);
app.route({
path: 'image-creator',
key: 'fix-sentences',
middleware: ['auth'],
description: '修正句子条目的 summary 字段'
}).define(async () => {
const list = await pbService.collection.getFullList({
filter: `tags~"sentence"`,
})
console.log(`Fetched ${list.length} sentence items from PocketBase.`);
const BATCH_SIZE = 50;
for (let i = 0; i < list.length; i += BATCH_SIZE) {
const chunk = list.slice(i, i + BATCH_SIZE);
const batch = pbService.client.createBatch();
for (const item of chunk) {
const sentenceData = item.data?.sentence || {};
if (!sentenceData.optimized) continue;
batch.collection(pbService.collectionName).update(item.id, {
summary: sentenceData.optimized,
});
}
await batch.send();
console.log(`Processed batch ${Math.floor(i / BATCH_SIZE) + 1}: ${chunk.length} items.`);
}
}).addTo(app);

View File

@@ -0,0 +1,43 @@
import { app, ossService, pbService, redis } from '@/app.ts'
import { addImageGenerateJob } from '@/task/image-creator.job.ts';
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
app.route({
path: 'image-creator',
key: 'create-task',
description: '创建图片生成任务,将所有计划中的任务加入图片生成队列',
middleware: ['auth']
}).define(async (ctx) => {
const status = ctx.status ?? '计划中';
const list = await pbService.collection.getFullList({
filter: `status="${status}"`,
})
for (const item of list) {
await addImageGenerateJob(item);
await sleep(100); // 避免短时间内添加过多任务
}
console.log(`Added ${list.length} image generate jobs to the queue.`);
}).addTo(app);
app.route({
path: 'image-creator',
key: 'batch-update-tags',
description: '测试 Redis 连接',
isDebug: true
}).define(async (ctx) => {
const list = await pbService.collection.getFullList({
filter: `tags="[]"`,
})
console.log(`Fetched ${list.length} items from PocketBase.`, list[0]);
const BATCH_SIZE = 50;
for (let i = 0; i < list.length; i += BATCH_SIZE) {
const chunk = list.slice(i, i + BATCH_SIZE);
const batch = pbService.client.createBatch();
for (const item of chunk) {
batch.collection(pbService.collectionName).update(item.id, { tags: ["horse"] });
}
await batch.send();
console.log(`Processed batch ${Math.floor(i / BATCH_SIZE) + 1}: ${chunk.length} items.`);
}
ctx.body = { message: `Processed ${list.length} items.` };
}).addTo(app);

View File

@@ -0,0 +1,69 @@
import { pbService, jimengService, ossService, app } from '../index.ts';
import type { ImageCollection } from '../services/pb.service.ts';
// 更新 PB 状态
export async function updateItemStatus(
itemId: string,
status: string,
extraData?: Partial<ImageCollection>
): Promise<void> {
const collection = pbService.getCollection<ImageCollection>(pbService.collectionName);
if (extraData) {
const existingItem = await pbService.collection.getOne(itemId);
const data = existingItem.data;
const existingImages = data?.images || [];
const newImages = extraData.data?.images || [];
await collection.update(itemId, {
status,
...extraData,
data: {
...extraData?.data,
...data,
images: [...existingImages, ...newImages],
},
});
} else {
await collection.update(itemId, {
status,
});
}
}
app.route({
path: 'image-creator',
key: 'image-update',
description: '更新所有图片生成任务的状态',
middleware: ['auth']
}).define(async (ctx) => {
const { itemId, status, extraData } = ctx.query;
if (!itemId || !status) {
ctx.throw(400, 'Missing itemId or status');
}
await updateItemStatus(itemId as string, status as string, extraData as Partial<ImageCollection> | undefined);
ctx.body = { message: 'Item status updated successfully' };
}).addTo(app);
app.route({
path: 'image-creator',
key: 'image-download',
description: '下载所有图片生成任务的图片,并上传到 OSS, 返回图片链接。参数: itemId, imageUrl, index',
middleware: ['auth']
}).define(async (ctx) => {
const { itemId, imageUrl, index = 0 } = ctx.query;
if (!itemId || !imageUrl || index === undefined) {
ctx.throw(400, 'Missing itemId, imageUrl or index');
}
const imageBuffer = await jimengService.downloadImage(imageUrl);
const filename = `generated_${itemId}_${index}_${Date.now()}.png`;
await ossService.putObject(filename, imageBuffer);
const ossUrl = ossService.getLink(filename)
console.log(`[ImageDownload] Image uploaded to OSS: ${ossUrl}`);
const imageData = { type: 'tos' as const, url: ossUrl };
await updateItemStatus(itemId as string, 'completed', {
data: {
images: [imageData],
},
});
ctx.body = { message: 'Image downloaded and uploaded to OSS successfully', ossUrl };
}).addTo(app);

View File

@@ -0,0 +1,15 @@
import { app } from '../index.ts';
import './create-task.ts'
import './image-update.ts'
import './create-sentence.ts'
import './perfect.ts'
app.route({
path: 'auth',
id: 'auth'
}).define(async (ctx) => {
// ctx.body = { message: 'Auth route' };
}).addTo(app);

View File

@@ -0,0 +1,46 @@
import { app, ossService, pbService, redis } from '@/app.ts'
import { addPerfectPromptJob } from '@/task/perfect-prompt.job.ts';
import { addPerfectSentencePromptJob } from '@/task/perfect-sentence.job.ts';
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
app.route({
path: 'image-creator',
key: 'perfect-horse',
description: '优化关于像素马的关键字',
middleware: ['auth']
}).define(async (ctx) => {
const id = ctx.query.id as string;
if (!id) {
ctx.throw(400, '缺少 id 参数');
}
const item = await pbService.collection.getOne(id);
if (!item) {
ctx.throw(404, '未找到对应的条目');
}
addPerfectPromptJob(item);
ctx.body = { message: '已添加优化任务到队列' };
}).addTo(app);
app.route({
path: 'image-creator',
key: 'perfect-sentence',
description: '优化关于句子的关键字',
middleware: ['auth']
}).define(async (ctx) => {
const list = await pbService.collection.getFullList({
filter: `status="创建"`,
limit: 1
});
console.log(`Fetched ${list.length} items for sentence perfection.`);
const hasSentences = list.filter(item => {
return item.data && item.data.sentence;
});
for (const item of hasSentences) {
addPerfectSentencePromptJob(item);
await sleep(100); // 避免短时间内添加过多任务
}
console.log(`Added ${list.length} perfect prompt jobs to the queue.`);
ctx.body = { message: '已添加优化任务到队列' };
}).addTo(app);

View File

@@ -0,0 +1,121 @@
import { Result } from '@kevisual/query'
export interface JimengOptions {
/** API密钥用于认证请求 */
apiKey: string;
/** API基础URL */
baseUrl: string;
/** 请求超时时间(毫秒) */
timeout: number;
}
export interface JimengGenerateOptions {
/** 图片生成提示词 */
prompt: string;
/** 使用的模型版本,默认 jimeng-4.0 */
model?: string;
/** 图片比例,默认 1:1 */
ratio?: string;
/** 图片分辨率,默认 2k */
resolution?: string;
}
interface JimengResponse {
/** 请求创建时间戳 */
created: number;
/** 生成的图片列表 */
data: Array<{
/** 图片URL */
url: string;
}>;
}
export class JimengService {
private apiKey: string;
private baseUrl: string;
private timeout: number;
constructor(options: JimengOptions) {
this.apiKey = options.apiKey;
this.baseUrl = options.baseUrl || 'https://jimeng-api.kevisual.cn/v1';
this.timeout = options.timeout;
}
async generateImage(options: JimengGenerateOptions): Promise<Result<JimengResponse>> {
const {
prompt,
model = 'jimeng-4.6',
ratio = '1:1',
resolution = '2k'
} = options;
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
const response = await fetch(`${this.baseUrl}/images/generations`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`,
},
body: JSON.stringify({
model,
prompt,
ratio,
resolution,
}),
signal: controller.signal,
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`jimeng API error: ${response.status} ${response.statusText}`);
}
const result = await response.json() as JimengResponse;
return { code: 200, data: result };
} catch (error: any) {
return { code: 500, message: error.message || 'Unknown error' };
}
}
async downloadImage(url: string): Promise<Uint8Array> {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
try {
const response = await fetch(url, {
signal: controller.signal,
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`Failed to download image: ${response.statusText}`);
}
const arrayBuffer = await response.arrayBuffer();
return new Uint8Array(arrayBuffer);
} catch (error: any) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new Error('Image download timeout');
}
throw error;
}
}
/** 获取图片过期时间 */
async getExpiredTime(url: string): Promise<{ expiredAt: number, expired: boolean }> {
// https://p3-dreamina-sign.byteimg.com/tos-cn-i-tb4s082cfz/c018e06ee6654dd78ccacb29eff4744e~tplv-tb4s082cfz-aigc_resize:0:0.png?lk3s=43402efa&x-expires=1767852000&x-signature=34yf37N955BP37eLaYEzKeLQn0Q%3D&format=.png
const urlObj = new URL(url);
let expires = urlObj.searchParams.get('x-expires');
if (!expires) {
expires = '0';
}
const expiredAt = parseInt(expires) * 1000;
const expired = Date.now() > expiredAt;
return { expiredAt, expired };
}
}

View File

@@ -0,0 +1,36 @@
import { OssBase } from '@kevisual/oss/s3.ts';
import { S3Client } from '@aws-sdk/client-s3'
export type OSSOptions = {
accessKeyId: string;
accessKeySecret: string;
region: string;
bucketName: string;
endpoint: string;
prefix?: string;
}
export class OSSService extends OssBase {
declare client: S3Client;
endpoint: string;
constructor(options: OSSOptions) {
const client = new S3Client({
region: options.region,
endpoint: `${options.endpoint}`,
credentials: {
accessKeyId: options.accessKeyId,
secretAccessKey: options.accessKeySecret,
},
});
super({
client,
bucketName: options.bucketName,
prefix: options.prefix || '',
});
this.endpoint = options.endpoint;
}
getLink(objectName: string): string {
const endpoint = this.endpoint;
return `${endpoint}/${this.bucketName}/${this.prefix}${objectName}`;
}
}

View File

@@ -0,0 +1,174 @@
import PocketBase from 'pocketbase';
import { EventEmitter } from 'eventemitter3'
type PBOptions = {
url: string;
token?: string;
}
export class PBCore {
declare client: PocketBase;
emitter = new EventEmitter();
token?: string;
constructor(options: PBOptions) {
this.client = new PocketBase(options.url);
this.token = options.token || '';
if (this.token) {
this.client.authStore.save(this.token);
}
}
async loginByPassword(email: string, password: string, admin: boolean = true) {
const collectionName = admin ? '_superusers' : 'users';
const authData = await this.client.collection(collectionName).authWithPassword(email, password);
this.emitter.emit('login', authData);
return authData;
}
async login({ username, password, admin = true }: { username: string; password: string, admin?: boolean }) {
if (this.client.authStore.isValid) {
return true;
}
try {
await this.loginByPassword(username, password, admin);
return true;
} catch (error) {
console.error('PocketBase login error:', error);
return false;
}
}
/**
*
* https://pocketbase.io/docs/api-collections/#create-collection
*
* */
async createCollection({ name, type, fields }: CreateCollection) {
const collections = await this.client.collections.getFullList();
const existing = collections.find(c => c.name === name);
if (existing) {
const collection = this.client.collection(name);
const schema = await this.client.collections.getOne(name);
return {
schema: schema,
existing: true,
collection
};
}
const schema = await this.client.collections.create({
name,
type: type ?? 'base',
fields: fields ?? defaultFields,
});
const collection = this.client.collection(name);
return {
collection,
schema
}
}
}
export const defaultFields: CollectionFields[] = [
{
name: 'title',
type: 'text',
},
{
name: 'summary',
type: 'text',
},
{
name: 'description',
type: 'text',
},
{
name: 'tags',
type: 'json',
},
{
name: 'data',
type: 'json',
},
{
name: 'status',
type: 'text',
},
{
name: 'created',
type: 'autodate',
onCreate: true,
},
{
name: 'updated',
type: 'autodate',
onCreate: true,
onUpdate: true,
}
]
export type CollectionFields = {
name: string;
type: string;
required?: boolean;
options?: any;
onCreate?: boolean;
onUpdate?: boolean;
}
export type CreateCollectioRule = {
listRule?: string;
viewRule?: string;
/**
* createRule: 'id = @request.auth.id',
*/
createRule?: string;
updateRule?: string;
deleteRule?: string;
}
export type CreateCollection = {
name: string;
type?: 'base' | 'viwer' | 'auth';
fields?: CollectionFields[];
/**
* viewer:
* viewQuery: 'SELECT id, name from posts',
*/
viewQuery?: string;
} & CreateCollectioRule;
export class PBService extends PBCore {
collectionName = 'images_generation_tasks';
constructor(options: PBOptions) {
super(options);
this.client.autoCancellation(false)
}
getCollection<T>(name: string) {
return this.client.collection<T>(name);
}
async initPbService() {
const isLogin = this.client.authStore.isValid;
console.log('PocketBase is logged in:', isLogin);
}
async importData(data: any[]) {
const collection = this.getCollection(this.collectionName);
for (const item of data) {
await collection.create(item);
}
}
get collection() {
return this.client.collection<ImageCollection>(this.collectionName);
}
}
const ImageTaskStatus = ['提示词优化中', '计划中', '生成图片中', '图片下载中', '暂停中', '已完成', '失败'] as const;
type Data<T = {}> = {
images: { type: 'jimeng' | 'tos', url: string }[];
} & T
export type ImageCollection<T = any> = {
id: string;
created: string;
updated: string;
title: string;
tags: any;
summary: string;
description: string;
data: Data<T>;
status: typeof ImageTaskStatus[number];
}

View File

@@ -0,0 +1,58 @@
import { createStorage } from 'unstorage';
import fsDriver from 'unstorage/drivers/fs';
export interface PromptData {
value: string;
id: string;
perfect: string;
imageUrl?: string;
generatedAt?: number;
}
export class StorageService {
private storage: ReturnType<typeof createStorage>;
constructor() {
this.storage = createStorage({
driver: fsDriver({ base: 'storage' }),
});
}
async get(id: string): Promise<PromptData | null> {
const filename = id.endsWith('.json') ? id : `${id}.json`;
const data = await this.storage.getItem<PromptData>(filename);
return data || null;
}
async getPendingPrompts(): Promise<PromptData[]> {
const keys = await this.storage.getKeys();
const pending: PromptData[] = [];
for (const key of keys) {
if (key === 'usage.json') continue;
const data = await this.storage.getItem<PromptData>(key);
if (data && !data.imageUrl) {
pending.push(data);
}
}
return pending;
}
async update(id: string, data: Partial<PromptData>): Promise<void> {
const filename = id.endsWith('.json') ? id : `${id}.json`;
const existing = await this.storage.getItem<PromptData>(filename);
if (existing) {
await this.storage.setItem(filename, { ...existing, ...data });
}
}
async hasImage(id: string): Promise<boolean> {
const data = await this.get(id);
return !!data?.imageUrl;
}
}
export const storageService = new StorageService();

View File

@@ -0,0 +1,232 @@
import { Worker, Queue, Job } from 'bullmq';
import { getRedisConnection } from '../module/redis.ts';
import { pbService, jimengService, ossService, app } from '../index.ts';
import type { ImageCollection } from '../services/pb.service.ts';
import { updateItemStatus } from '../routes/image-update.ts';
import { notify } from '@/module/logger.ts';
export const IMAGE_GENERATE_JOB = 'image-generate';
export const IMAGE_DOWNLOAD_JOB = 'image-download';
// 状态常量
export const ImageTaskStatus = {
PENDING: '提示词优化中' as const,
PLANNING: '计划中' as const,
GENERATING: '生成图片中' as const,
DOWNLOADING: '图片下载中' as const,
PAUSED: '暂停中' as const,
COMPLETED: '已完成' as const,
FAILED: '失败' as const,
};
// 生成图片任务的节流时间(毫秒)
const JIMENG_THROTTLE_DELAY = 60 * 1000;
// 下载任务最大重试次数
const DOWNLOAD_MAX_RETRIES = 3;
// 图片生成任务最大重试次数
const GENERATE_MAX_RETRIES = 3;
export interface ImageCreatorJobData {
itemId: string;
prompt: string;
collectionName?: string;
}
export interface ImageGenerateJobData {
itemId: string;
prompt: string;
ratio?: string;
collectionName?: string;
}
export interface ImageDownloadJobData {
itemId: string;
imageUrl: string;
collectionName?: string;
index: number;
}
/**
* 单独添加生成图片任务
*/
export async function addImageGenerateJob(item: ImageCollection): Promise<void> {
const connection = getRedisConnection();
const queue = new Queue(IMAGE_GENERATE_JOB, { connection });
const jobData: ImageGenerateJobData = {
itemId: item.id,
prompt: item.description || item.summary || item.title,
collectionName: pbService.collectionName,
};
await queue.add(IMAGE_GENERATE_JOB, jobData, {
removeOnComplete: 100,
removeOnFail: 100,
delay: JIMENG_THROTTLE_DELAY, // 任务间隔 30 秒
});
await updateItemStatus(item.id, ImageTaskStatus.GENERATING);
// console.log(`[ImageGenerate] Job created for item: ${item.id}`);
await queue.close();
}
/**
* 单独添加下载图片任务
*/
export async function addImageDownloadJob(
itemId: string,
imageUrl: string,
index?: number
): Promise<void> {
const connection = getRedisConnection();
const queue = new Queue(IMAGE_DOWNLOAD_JOB, { connection });
const jobData: ImageDownloadJobData = {
itemId,
imageUrl,
collectionName: pbService.collectionName,
index: index ?? 0
};
// 使用 bullmq 内置重试,指数退避
await queue.add(IMAGE_DOWNLOAD_JOB, jobData, {
attempts: DOWNLOAD_MAX_RETRIES,
backoff: {
type: 'exponential',
delay: 200, // 初始 200 毫秒
},
removeOnComplete: 100,
removeOnFail: 100,
});
await updateItemStatus(itemId, ImageTaskStatus.DOWNLOADING);
// console.log(`[ImageDownload] Job created for item: ${itemId}`);
await queue.close();
}
/**
* 运行独立的下载 worker
*/
export async function runImageDownloadWorker(): Promise<void> {
const connection = getRedisConnection();
const worker = new Worker(
IMAGE_DOWNLOAD_JOB,
async (job: Job<ImageDownloadJobData>) => {
const { itemId, imageUrl, index } = job.data;
const attemptsMade = job.attemptsMade;
console.log(`[ImageDownload] Processing item: ${itemId}, attempt: ${attemptsMade + 1}/${DOWNLOAD_MAX_RETRIES}`);
try {
const imageBuffer = await jimengService.downloadImage(imageUrl);
const filename = `generated_${itemId}_${index}_${Date.now()}.png`;
await ossService.putObject(filename, imageBuffer);
const ossUrl = ossService.getLink(filename)
console.log(`[ImageDownload] Image uploaded to OSS: ${ossUrl}`);
const imageData = { type: 'tos' as const, url: ossUrl };
await updateItemStatus(itemId, ImageTaskStatus.COMPLETED, {
data: {
images: [imageData],
},
});
return { success: true, ossUrl };
} catch (error: any) {
console.error(`[ImageDownload] Error: ${error.message}`);
// 重试次数用尽,暂停任务
if (job.attemptsMade >= DOWNLOAD_MAX_RETRIES - 1) {
await updateItemStatus(itemId, ImageTaskStatus.PAUSED);
}
throw error;
}
},
{
connection,
concurrency: 1, // 避免更新冲突
lockDuration: 60000 * 5, // 锁持续时间 5分钟
stalledInterval: 30000, // 每30秒检查一次 stalled
} as any
);
worker.on('completed', (job) => {
console.log(`[ImageDownload] Job completed: ${job.id}`);
});
worker.on('failed', (job, err) => {
console.error(`[ImageDownload] Job failed: ${job?.id}, error: ${err.message}`);
notify.notify(`[ImageDownload] \nJob failed: ${job?.id}, error: ${err.message}\n Job data: ${JSON.stringify(job?.data)}`);
});
console.log('[ImageDownload] Worker 开始运行');
}
/**
* 运行图片生成 worker使用 jimeng API
*/
export async function runImageGenerateWorker(): Promise<void> {
const connection = getRedisConnection();
const worker = new Worker(
IMAGE_GENERATE_JOB,
async (job: Job<ImageGenerateJobData>) => {
const { itemId, prompt, ratio = '9:16' } = job.data;
const attemptsMade = job.attemptsMade;
console.log(`[ImageGenerate] Processing item: ${itemId}, attempt: ${attemptsMade + 1}/${GENERATE_MAX_RETRIES}`);
try {
// 调用 jimeng API 生成图片
const result = await jimengService.generateImage({
prompt,
ratio: ratio,
});
if (result.code !== 200 || !result.data?.data?.length) {
throw new Error(result.message || 'Failed to generate image');
}
const images = result.data.data;
for (const [index, img] of images.entries()) {
console.log(`[ImageGenerate] Image generated: ${img.url}`);
// 生成成功后,添加下载任务
await addImageDownloadJob(itemId, img.url, index);
}
// 更新状态为下载中
await updateItemStatus(itemId, ImageTaskStatus.DOWNLOADING, {
data: { images: images.map(img => ({ type: 'jimeng' as const, url: img.url })) },
});
return { success: true, images };
} catch (error: any) {
console.error(`[ImageGenerate] Error: ${error.message}`);
throw error;
}
},
{
connection,
concurrency: 1, // jimeng API 有节流限制,设置为 1
lockDuration: 60000 * 5, // 锁持续时间 5分钟
stalledInterval: 30000, // 每30秒检查一次 stalled
}
);
worker.on('completed', (job) => {
console.log(`[ImageGenerate] Job completed: ${job.id}`);
});
worker.on('failed', (job, err) => {
console.error(`[ImageGenerate] Job failed: ${job?.id}, error: ${err.message}`);
notify.notify(`[ImageGenerate] \nJob failed: ${job?.id}, error: ${err.message}\n Job data: ${JSON.stringify(job?.data)}`);
if (job && job.attemptsMade >= GENERATE_MAX_RETRIES - 1) {
worker.close();
notify.notify(`[ImageGenerate] Worker stopped after reaching max retries for item ${job.data.itemId}`);
}
});
console.log('[ImageGenerate] Worker 开始运行');
}
export { updateItemStatus };

View File

@@ -0,0 +1,143 @@
import { Worker, Job } from 'bullmq';
import { getRedisConnection } from '../module/redis.ts';
import { Prompt, pbService, ai } from '../index.ts';
import type { ImageCollection } from '../services/pb.service.ts';
import { updateItemStatus } from '../routes/image-update.ts';
import { Queue } from 'bullmq';
import { notify } from '@/module/logger.ts';
import { addImageGenerateJob } from './image-creator.job.ts';
export const PERFECT_PROMPT_JOB = 'perfect-prompt';
// 状态常量
export const PerfectPromptStatus = {
PENDING: '提示词优化中' as const,
PLANNING: '计划中' as const,
FAILED: '失败' as const,
};
// 最大重试次数
const MAX_RETRIES = 3;
export interface PerfectPromptJobData {
itemId: string;
prompt: string;
collectionName?: string;
data?: Record<string, any>;
}
// 优化提示词的模板
const DEFAULT_PERFECT_PROMPT = `请你将以下提示词进行完善,使其更加详细和具体,适合用于生成高质量的像素艺术图像。要求如下:
1. 只返回完善后的提示词,不要包含任何多余的内容或解释。
2. 确保提示词专注于像素艺术风格,包括但不限于像素化角色、场景和物体的描述。
3. 使用具体的细节来增强提示词的表现力,例如颜色、构图、光影效果等。
4. 避免使用与像素艺术无关的术语或描述。
5. 保持提示词的简洁性,避免过于冗长,但要确保信息量充足。
6. 如果需要颜色,需要整个图像的颜色更少的描述,而不是复杂的颜色细节, 背景默认纯蓝色。
7. 使用中文进行描述。
`;
/**
* 单独添加优化提示词任务
*/
export async function addPerfectPromptJob(item: ImageCollection): Promise<void> {
const connection = getRedisConnection();
const queue = new Queue(PERFECT_PROMPT_JOB, { connection });
const jobData: PerfectPromptJobData = {
itemId: item.id,
prompt: item.description || item.summary || item.title || '',
collectionName: pbService.collectionName,
};
await queue.add(PERFECT_PROMPT_JOB, jobData, {
attempts: MAX_RETRIES,
backoff: {
type: 'exponential',
delay: 2000,
},
removeOnComplete: 100,
removeOnFail: 100,
});
await updateItemStatus(item.id, PerfectPromptStatus.PENDING);
await queue.close();
}
/**
* 运行优化提示词 worker
*/
export async function runPerfectPromptWorker(): Promise<void> {
const connection = getRedisConnection();
// 获取环境变量中的 API key
const worker = new Worker(
PERFECT_PROMPT_JOB,
async (job: Job<PerfectPromptJobData>) => {
const { itemId, prompt } = job.data;
const attemptsMade = job.attemptsMade;
console.log(`[PerfectPrompt] Processing item: ${itemId}, attempt: ${attemptsMade + 1}/${MAX_RETRIES}`);
try {
if (!prompt) {
throw new Error('Prompt is empty');
}
const promptTool = new Prompt({ perfectPrompt: DEFAULT_PERFECT_PROMPT });
await ai.chat([
{
role: 'user',
content: promptTool.perfect(prompt),
},
]);
const perfectText = promptTool.clearPerfectTags(ai.responseText);
if (!perfectText) {
throw new Error('Generated perfect prompt is empty');
}
console.log(`[PerfectPrompt] Perfect prompt generated for item: ${itemId}`);
// 更新状态为已完成,并保存优化后的提示词
await updateItemStatus(itemId, PerfectPromptStatus.PLANNING, {
description: perfectText,
});
// 任务完成,把任务抛给下一个图片生成队列
const item = await pbService.collection.getOne(itemId);
if (item) {
addImageGenerateJob(item)
}
return { success: true, perfectPrompt: perfectText };
} catch (error: any) {
console.error(`[PerfectPrompt] Error: ${error.message}`);
// 重试次数用尽,标记为失败
if (job.attemptsMade >= MAX_RETRIES - 1) {
await updateItemStatus(itemId, PerfectPromptStatus.FAILED);
}
throw error;
}
},
{
connection,
concurrency: 2,
lockDuration: 60000 * 5, // 锁持续时间 5分钟
stalledInterval: 30000, // 每30秒检查一次 stalled
}
);
worker.on('completed', (job) => {
console.log(`[PerfectPrompt] Job completed: ${job.id}`);
});
worker.on('failed', (job, err) => {
console.error(`[PerfectPrompt] Job failed: ${job?.id}, error: ${err.message}`);
notify.notify(`[PerfectPrompt] \nJob failed: ${job?.id}, error: ${err.message}\n Job data: ${JSON.stringify(job?.data)}`);
if (job && job.attemptsMade >= MAX_RETRIES - 1) {
worker.close();
notify.notify(`[PerfectPrompt] Worker stopped after reaching max retries for item ${job.data.itemId}`);
}
});
console.log('[PerfectPrompt] Worker started');
}

View File

@@ -0,0 +1,121 @@
import { Worker, Job } from 'bullmq';
import { getRedisConnection } from '../module/redis.ts';
import { Prompt, pbService, ai, app } from '../index.ts';
import type { ImageCollection } from '../services/pb.service.ts';
import { updateItemStatus } from '../routes/image-update.ts';
import { Queue } from 'bullmq';
import { notify } from '@/module/logger.ts';
import { addImageGenerateJob } from './image-creator.job.ts';
import { PerfectPromptJobData } from './perfect-prompt.job.ts';
import { SentenceImage } from '@/module/sentence-image.ts';
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
export const PERFECT_SENTENCE_PROMPT_JOB = 'perfect-sentence-prompt';
// 最大重试次数
const MAX_RETRIES = 3;
// 状态常量
export const PerfectPromptStatus = {
PENDING: '提示词优化中' as const,
PLANNING: '计划中' as const,
FAILED: '失败' as const,
};
export async function addPerfectSentencePromptJob(item: ImageCollection): Promise<void> {
const connection = getRedisConnection();
const queue = new Queue(PERFECT_SENTENCE_PROMPT_JOB, { connection });
const jobData: PerfectPromptJobData = {
itemId: item.id,
prompt: item.description || item.summary || item.title || '',
collectionName: pbService.collectionName,
data: item.data
};
await queue.add(PERFECT_SENTENCE_PROMPT_JOB, jobData, {
attempts: MAX_RETRIES,
backoff: {
type: 'exponential',
delay: 2000,
},
removeOnComplete: 100,
removeOnFail: 100,
});
await updateItemStatus(item.id, PerfectPromptStatus.PENDING);
await queue.close();
}
export const runPerfectSentencePromptWorker = () => {
const connection = getRedisConnection();
const worker = new Worker<PerfectPromptJobData>(
PERFECT_SENTENCE_PROMPT_JOB,
async (job: Job<PerfectPromptJobData>) => {
const { itemId, prompt, data } = job.data;
const perfect = new SentenceImage();
try {
let content = ''
if (data && data.sentence) {
const { prompt, ...rest } = data.sentence || {};
if (rest) {
content = JSON.stringify(rest);
}
}
if (!content) {
content = prompt;
}
if (!content) {
notify.notify(`[Sentence] 提示词优化任务跳过,条目 ${itemId}: 无内容可优化。`);
await updateItemStatus(itemId, PerfectPromptStatus.FAILED);
return;
}
const perfectText = perfect.perfect(content);
const result = await ai.chat([], {
messages: [{ role: 'user', content: perfectText }],
// model: 'qwen-plus',
// enable_thinking: true
model: 'qwen-turbo',
});
const perfectPrompt = perfect.clearPerfectTags(ai.responseText!) || '';
console.log(`[Sentence] 提示词优化 ${itemId}:\n`, perfectPrompt);
await updateItemStatus(itemId, '提示词优化完成', {
description: perfectPrompt
});
await sleep(500); // 确保数据已保存
// 任务完成,把任务抛给下一个图片生成队列
const item = await pbService.collection.getOne(itemId);
if (item) {
addImageGenerateJob(item)
}
return { success: true, perfectPrompt: perfectText };
} catch (error: any) {
notify.notify(`[Sentence] 提示词优化任务失败,条目 ${itemId}: ${error.message}`);
await updateItemStatus(itemId, PerfectPromptStatus.FAILED);
throw error;
}
},
{
connection,
lockDuration: 60000 * 5, // 锁持续时间 5分钟
stalledInterval: 30000, // 每30秒检查一次 stalled
}
);
worker.on('completed', (job) => {
// notify.notify(`[Sentence] Perfect Prompt Job Completed for item ${job.data.itemId}`);
console.log(`[Sentence] Perfect Prompt Job Completed for item ${job.data.itemId}`);
});
worker.on('failed', (job, err) => {
notify.notify(`[Sentence] 提示词优化任务失败,条目${job?.data.itemId}: ${err.message}`);
// 如果是第三次失败停止worker;
if (job && job.attemptsMade >= MAX_RETRIES - 1) {
worker.close();
notify.notify(`[Sentence] 提示词优化工作者在条目 ${job.data.itemId} 达到最大重试次数后停止`);
}
});
console.log('[Sentence] Worker 开始运行');
return worker;
};

View File

@@ -0,0 +1,12 @@
import { runImageDownloadWorker, runImageGenerateWorker } from '../task/image-creator.job.ts';
import { runPerfectSentencePromptWorker } from '../task/perfect-sentence.job.ts';
runImageDownloadWorker();
runImageGenerateWorker();
runPerfectSentencePromptWorker();
// 运行半小时后停止
setTimeout(() => {
console.log('Stop timeed', new Date().toISOString());
process.exit(0);
}, 10 * 60 * 60 * 1000); // 10 hours in milliseconds