diff --git a/.gitignore b/.gitignore index 72f477f..6fab7f4 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,13 @@ logs/ .DS_Store verify_output/ **/verify_output/ +frontend/verify_task103/ +backend/test_color_compare.py +backend/test_3sets_e2e.py +backend/scripts/run_log.txt +test/ +test/node_modules/ +test/.env output_e2e/ **/output_e2e/ screenshot-*.png diff --git a/backend/scripts/real_image_check.py b/backend/scripts/real_image_check.py index 8cd8021..36d5b66 100644 --- a/backend/scripts/real_image_check.py +++ b/backend/scripts/real_image_check.py @@ -31,7 +31,8 @@ TEST_PROMPT = "\n".join([ "避免美白/祛斑/速效/医用等违规词;必须保持产品包装与参考图一致。", ]) -_REF = ROOT / "test/output/01_连通测试.png" +_REF_ENV = os.environ.get("REAL_IMAGE_REF", "").strip() +_REF = Path(_REF_ENV).expanduser() if _REF_ENV else None async def _apiports(key, url, ref, model, size): @@ -73,6 +74,11 @@ async def run(): fallback = os.environ.get("IMAGE_PROVIDER_FALLBACK", "codeproxy").lower() out_dir = Path(__file__).parent.parent / "verify_output" out_dir.mkdir(parents=True, exist_ok=True) + if _REF is None or not _REF.exists() or not _REF.is_file(): + raise FileNotFoundError( + "请通过 REAL_IMAGE_REF 指定本地参考图路径,例如:" + "REAL_IMAGE_REF=/path/to/ref.png python3 scripts/real_image_check.py" + ) ref = _REF.read_bytes() t0 = time.time(); img = None; used = None for prov in [primary, fallback]: diff --git a/backend/scripts/run_log.txt b/backend/scripts/run_log.txt deleted file mode 100644 index beef5a6..0000000 --- a/backend/scripts/run_log.txt +++ /dev/null @@ -1,13 +0,0 @@ -2026-06-09 11:39:33,251 INFO [apiports] 出图中 model=gpt-image-2 size=1024x1536 -2026-06-09 11:41:04,233 INFO HTTP Request: POST https://www.apiports.com/v1/api/generate "HTTP/1.1 200 OK" -2026-06-09 11:41:04,236 ERROR [apiports] 失败: 无法解析: [] -2026-06-09 11:41:04,236 INFO [codeproxy] 出图中 model=gpt-image-2 size=1024x1536 -2026-06-09 11:42:33,485 INFO HTTP Request: POST https://codeproxy.dev/v1/images/edits "HTTP/1.1 200 OK" - -===== 出图结果 ===== - provider: codeproxy - 耗时: 181.2s - 大小: 2009KB - 路径: /Users/qiyu/Documents/企业培训项目/万牛会L1准备/北哥小红书产品/Clover/backend/verify_output/素颜霜_封面hook.png -==================== - diff --git a/backend/test_3sets_e2e.py b/backend/test_3sets_e2e.py deleted file mode 100644 index 2db180d..0000000 --- a/backend/test_3sets_e2e.py +++ /dev/null @@ -1,44 +0,0 @@ -"""Task 端到端真跑临时验证脚本。 - -手动运行: - cd backend && python3 test_3sets_e2e.py -""" -import sys - -sys.path.insert(0, "/app") - - -def main(): - from app.core.database import SessionLocal - from app.models.task import GenerationTask - from app.constants.enums import TaskStatus - from app.workers.tasks import run_generation_pipeline - - db = SessionLocal() - try: - task = GenerationTask( - workspace_id=3, - product_id=1, - operator_id=3, - theme="伪素颜·黄黑皮提亮·3套正交", - text_count=1, - image_count=2, - track="ai", - need_product_image=True, - status=TaskStatus.PENDING, - ) - db.add(task) - db.commit() - db.refresh(task) - task_id = task.id - print(f"建task成功 task_id={task_id} image_count=2 (期望3套×2=6张)") - finally: - db.close() - - print(f"=== 同步执行 run_generation_pipeline({task_id}) ===") - result = run_generation_pipeline.apply(args=[task_id]).get() - print(f"=== 结果: {result} ===") - - -if __name__ == "__main__": - main() diff --git a/backend/tests/test_delivery_readiness_fixes.py b/backend/tests/test_delivery_readiness_fixes.py index 4c6b6bd..a0a3590 100644 --- a/backend/tests/test_delivery_readiness_fixes.py +++ b/backend/tests/test_delivery_readiness_fixes.py @@ -18,6 +18,9 @@ class _Query: def all(self): return self._rows + def count(self): + return len(self._rows) + class _Db: def __init__(self, text_rows, image_rows): @@ -32,26 +35,44 @@ class _Task: image_count = 3 -def test_submit_review_requires_each_strategy_to_select_full_image_count(): - db = _Db( - text_rows=[("A",), ("B",), ("C",)], - image_rows=[("A", 1), ("A", 2), ("A", 3), ("B", 4), ("C", 5), ("C", 6), ("C", 7)], - ) +def test_submit_review_requires_at_least_one_text_and_one_image(): + db = _Db(text_rows=[], image_rows=[]) with pytest.raises(CloverHTTPException) as exc: task_actions._validate_strategy_selection(db, _Task()) - assert "图片未选满:B(1/3)" in exc.value.biz_message + assert "请至少选择 1 条文案" in exc.value.biz_message + assert "请至少选择 1 张图片" in exc.value.biz_message -def test_submit_review_passes_when_all_strategies_have_text_and_full_images(): +def test_submit_review_requires_at_least_one_image(): + db = _Db(text_rows=[("A",)], image_rows=[]) + + with pytest.raises(CloverHTTPException) as exc: + task_actions._validate_strategy_selection(db, _Task()) + + assert exc.value.biz_message == "请至少选择 1 张图片" + + +def test_submit_review_requires_at_least_one_text(): + db = _Db(text_rows=[], image_rows=[("A", 1)]) + + with pytest.raises(CloverHTTPException) as exc: + task_actions._validate_strategy_selection(db, _Task()) + + assert exc.value.biz_message == "请至少选择 1 条文案" + + +def test_submit_review_passes_with_one_text_and_one_image(): + db = _Db(text_rows=[("B",)], image_rows=[("C", 7)]) + + task_actions._validate_strategy_selection(db, _Task()) + + +def test_submit_review_does_not_require_all_strategies_or_full_image_count(): db = _Db( - text_rows=[("A",), ("B",), ("C",)], - image_rows=[ - ("A", 1), ("A", 2), ("A", 3), - ("B", 4), ("B", 5), ("B", 6), - ("C", 7), ("C", 8), ("C", 9), - ], + text_rows=[("A",)], + image_rows=[("B", 4)], ) task_actions._validate_strategy_selection(db, _Task()) diff --git a/backend/verify_output/素颜霜_封面hook.png b/backend/verify_output/素颜霜_封面hook.png deleted file mode 100644 index 36e1f0d..0000000 Binary files a/backend/verify_output/素颜霜_封面hook.png and /dev/null differ diff --git a/test/01_connect_test.js b/test/01_connect_test.js deleted file mode 100644 index bfc26eb..0000000 --- a/test/01_connect_test.js +++ /dev/null @@ -1,26 +0,0 @@ -'use strict'; -// 第1步:连通性测试 —— 1张素颜霜产品图 + 最简prompt,验证 codeproxy/gpt-image-2/key 全打通 -const path = require('path'); -const fs = require('fs'); -const { imageEdit, saveResult } = require('./aiClient'); - -const PRODUCT_IMG = '/Users/qiyu/Documents/企业培训项目/万牛会L1准备/产品包系统测试资料/产品白底图/素颜霜/IMG_9439.JPG'; -const OUT_DIR = path.join(__dirname, 'output'); - -async function main() { - if (!fs.existsSync(OUT_DIR)) fs.mkdirSync(OUT_DIR, { recursive: true }); - console.log('=== 第1步 连通性测试 ==='); - console.log('产品图:', PRODUCT_IMG, fs.existsSync(PRODUCT_IMG) ? '✓存在' : '✗缺失'); - - const prompt = '生成一张小红书风格的素颜霜产品图,竖版3:4,明亮干净,真实实拍质感,产品摆在浅色台面上,自然光。保持产品包装、颜色、质感与参考图一致。'; - try { - const result = await imageEdit({ images: [PRODUCT_IMG], prompt, traceId: 'connect-test' }); - const out = path.join(OUT_DIR, '01_连通测试.png'); - await saveResult(result, out); - console.log('\n✅ 链路打通!返回类型:', result.type, '→ 已保存:', out); - } catch (e) { - console.log('\n❌ 失败:', e.message); - console.log('排查方向: 401=key权限问题 / 404=端点或模型名 / 429=配额 / 超时=中转站慢'); - } -} -main(); diff --git a/test/02_baseline_test.js b/test/02_baseline_test.js deleted file mode 100644 index a47bc0f..0000000 --- a/test/02_baseline_test.js +++ /dev/null @@ -1,69 +0,0 @@ -'use strict'; -// step-2 基线生图:用移植的素颜霜prompt,每张带产品图,生成3张到 output/ -// 探地基基线 —— 不是达标测试,是看gpt-image-2出素颜霜种草图的底子 -require('dotenv').config(); -const path = require('path'); -const fs = require('fs'); -const { imageEdit, saveResult } = require('./aiClient'); -const { PRODUCT, ROLES, buildImagePrompt } = require('./suyanshuang_data'); - -const MODEL = process.env.MODEL_IMAGE || 'gpt-image-2'; -const SIZE = process.env.IMAGE_SIZE || '1024x1536'; -const PRODUCT_IMG = '/Users/qiyu/Documents/企业培训项目/万牛会L1准备/产品包系统测试资料/产品白底图/素颜霜/IMG_9439.JPG'; -const OUT_DIR = path.join(__dirname, 'output'); - -const JOBS = [ - { role: 'hook', file: '02_封面hook.png' }, - { role: 'texture', file: '03_质感texture.png' }, - { role: 'closer', file: '04_软转化closer.png' }, -]; - -async function runOne(job) { - const r = ROLES[job.role]; - const prompt = buildImagePrompt(job.role); - const traceId = `clover-baseline-${job.role}`; - console.log(`\n── [${r.name}] 开始生成 ──`); - console.log(` 模型=${MODEL} 尺寸=${SIZE} 产品图=${path.basename(PRODUCT_IMG)}`); - const t0 = Date.now(); - try { - const result = await imageEdit({ - images: [PRODUCT_IMG], - prompt, - traceId, - model: MODEL, - size: SIZE, - }); - const outPath = path.join(OUT_DIR, job.file); - await saveResult(result, outPath); - const ms = Date.now() - t0; - const kb = fs.existsSync(outPath) ? Math.round(fs.statSync(outPath).size / 1024) : 0; - console.log(` ✅ 完成 用时${ms}ms 大小${kb}KB → ${job.file}`); - return { role: job.role, ok: true, ms, kb, file: job.file }; - } catch (e) { - const ms = Date.now() - t0; - console.log(` ❌ 失败 用时${ms}ms:${e.message}`); - return { role: job.role, ok: false, ms, err: e.message }; - } -} - -(async () => { - if (!fs.existsSync(OUT_DIR)) fs.mkdirSync(OUT_DIR, { recursive: true }); - if (!fs.existsSync(PRODUCT_IMG)) { - console.error(`产品图不存在:${PRODUCT_IMG}`); - process.exit(1); - } - console.log(`===== Clover 基线生图测试 =====`); - console.log(`产品:${PRODUCT.name}|成分:${PRODUCT.ingredients.join('/')}`); - const results = []; - for (const job of JOBS) { - // 串行跑,避免并发触发中转站限流;一张一张看结果 - results.push(await runOne(job)); - } - console.log(`\n===== 汇总 =====`); - for (const r of results) { - if (r.ok) console.log(` ${r.role}: ✅ ${r.ms}ms ${r.kb}KB`); - else console.log(` ${r.role}: ❌ ${r.err}`); - } - const okN = results.filter((r) => r.ok).length; - console.log(`\n成功 ${okN}/${JOBS.length} 张,输出目录:${OUT_DIR}`); -})(); diff --git a/test/aiClient.js b/test/aiClient.js deleted file mode 100644 index ea3d220..0000000 --- a/test/aiClient.js +++ /dev/null @@ -1,109 +0,0 @@ -'use strict'; -// Clover 图片质量测试 · codeproxy 调用核心 -// 复用自 xhs-tool/backend/infrastructure/aiClient.js(已实测跑通的对的路),简化:去降级、去sharp -const path = require('path'); -require('dotenv').config({ path: path.join(__dirname, '.env') }); -const FormData = require('form-data'); -const https = require('https'); -const fs = require('fs'); - -const KEY = process.env.AI_API_KEY; -const BASE_URL = process.env.AI_BASE_URL; -const MODEL_IMAGE = process.env.MODEL_IMAGE || 'gpt-image-2'; -const IMAGE_TIMEOUT = 300000; - -if (!KEY) throw new Error('AI_API_KEY 未配置(见 test/.env)'); - -const sleep = ms => new Promise(r => setTimeout(r, ms)); - -function toBuffer(img) { - if (Buffer.isBuffer(img)) return img; - if (typeof img === 'string' && img.startsWith('data:')) - return Buffer.from(img.replace(/^data:[^;]+;base64,/, ''), 'base64'); - return fs.readFileSync(img); // 本地文件路径 -} - -// 单次请求:POST /images/edits (multipart) -function postEditOnce({ imgList, prompt, m, sz }) { - const form = new FormData(); - form.append('model', m); - form.append('prompt', prompt); - form.append('size', sz); - form.append('n', '1'); - for (let i = 0; i < imgList.length; i++) { - const buf = toBuffer(imgList[i]); - form.append('image[]', buf, { filename: `img_${i}.jpg`, contentType: 'image/jpeg' }); - } - return new Promise((resolve, reject) => { - const ac = new AbortController(); - const timer = setTimeout(() => { ac.abort(); req.destroy(new Error('300s timeout')); }, IMAGE_TIMEOUT); - const req = https.request(`${BASE_URL}/images/edits`, { - method: 'POST', - headers: { ...form.getHeaders(), Authorization: `Bearer ${KEY}` }, - signal: ac.signal, - }, (r) => { - let body = ''; - r.on('data', d => body += d); - r.on('end', () => { clearTimeout(timer); resolve({ status: r.statusCode, body }); }); - r.on('error', reject); - }); - req.on('error', (e) => { clearTimeout(timer); reject(e); }); - form.pipe(req); - }); -} - -// 可重试错误:上游/网关临时故障 + 限流 + 超时 -const RETRYABLE = new Set([429, 500, 502, 503, 504]); - -// 带参考图出图:失败自动退避重试(复用 xhs-tool 的重试思路) -async function imageEdit({ images, prompt, traceId = 't', model, size, maxRetry = 3 }) { - const m = model || MODEL_IMAGE; - const sz = size || process.env.IMAGE_SIZE || '1024x1536'; - const imgList = Array.isArray(images) ? images : [images]; - let lastErr; - for (let attempt = 1; attempt <= maxRetry; attempt++) { - const t0 = Date.now(); - try { - const res = await postEditOnce({ imgList, prompt, m, sz }); - console.log(`[imageEdit][${traceId}] try=${attempt}/${maxRetry} model=${m} size=${sz} imgs=${imgList.length} status=${res.status} ms=${Date.now()-t0}`); - if (res.status === 200) { - const d = JSON.parse(res.body).data[0]; - if (d.url) return { type: 'url', value: d.url }; - if (d.b64_json) return { type: 'b64', value: d.b64_json }; - throw new Error('返回既无 url 也无 b64_json'); - } - lastErr = new Error(`HTTP ${res.status}: ${res.body.slice(0, 300)}`); - if (!RETRYABLE.has(res.status)) throw lastErr; // 401/404等不重试,直接抛 - } catch (e) { - lastErr = e; - const code = (e.message.match(/HTTP (\d+)/) || [])[1]; - if (code && !RETRYABLE.has(Number(code))) throw e; - } - if (attempt < maxRetry) { - const wait = 3000 * attempt; // 3s,6s 退避 - console.log(` ↻ 上游临时故障,${wait/1000}s 后重试…`); - await sleep(wait); - } - } - throw lastErr; -} - -// 保存结果到文件(url则下载,b64则解码) -async function saveResult(result, outPath) { - if (result.type === 'b64') { - fs.writeFileSync(outPath, Buffer.from(result.value, 'base64')); - return outPath; - } - // url:下载 - await new Promise((resolve, reject) => { - https.get(result.value, (r) => { - const chunks = []; - r.on('data', c => chunks.push(c)); - r.on('end', () => { fs.writeFileSync(outPath, Buffer.concat(chunks)); resolve(); }); - r.on('error', reject); - }).on('error', reject); - }); - return outPath; -} - -module.exports = { imageEdit, saveResult, toBuffer, sleep }; diff --git a/test/e2e_image_check.py b/test/e2e_image_check.py deleted file mode 100644 index fe1a923..0000000 --- a/test/e2e_image_check.py +++ /dev/null @@ -1,94 +0,0 @@ -""" -端到端生图验证(补齐后质量检查) -- 真实 DB 解密 key → 真实 AIClients → 真实 plan_image_set → 真实调 apiports 出 6 图 -- 只验证生图链路;文案侧用占位 note,不走文案生成 -用法(容器内):python3 /app/test/e2e_image_check.py -""" -import asyncio -import os -import sys - -sys.path.insert(0, "/app") - -OUT_DIR = "/app/test/output_e2e" -REF_IMG = "/app/test/ref_product.png" # 预先拷入的真实参考图 -IMAGE_COUNT = 6 - - -def load_clients_and_product(): - """用真实 DB 解密 key 构建 clients,并取产品 dict。""" - from app.core.database import SessionLocal - from app.models.product import Product - from app.workers.pipeline_steps import ( - decrypt_user_key, build_clients_and_clear_key, build_product_dict, - ) - db = SessionLocal() - try: - product = db.query(Product).order_by(Product.id.desc()).first() - if not product: - raise SystemExit("库里没有产品记录") - product_dict = build_product_dict(product) - # 用产品所属 workspace 的 key(user_id 取 product.workspace_id 同号那条) - plain_key = decrypt_user_key(db, product.workspace_id, product.workspace_id) - clients = build_clients_and_clear_key(plain_key) - plain_key = None - return clients, product_dict - finally: - db.close() - - -def fake_note(product_dict: dict) -> dict: - """占位文案 note(不走文案生成,只为生图提供 hook/卖点上下文)。""" - pts = product_dict.get("selling_points") or ["温和不刺激", "成分干净", "上脸快吸收"] - return { - "title": f"{product_dict['name']}用一周的真实变化", - "content": "姐妹们这个我真的要安利,敏感肌也能用,质地清爽不黏腻," - "坚持用上脸状态肉眼可见地稳定了,分享给同样在找平替的你。", - "tags": ["敏感肌", "护肤平替", "成分党", "种草"], - "selling_points": pts, - } - - -async def main(): - os.makedirs(OUT_DIR, exist_ok=True) - if not os.path.isfile(REF_IMG): - raise SystemExit(f"参考图不存在:{REF_IMG}(请先拷入)") - with open(REF_IMG, "rb") as f: - ref_bytes = f.read() - print(f"参考图已加载:{len(ref_bytes)} bytes") - - clients, product_dict = load_clients_and_product() - print(f"产品:{product_dict['name']} | 类目:{product_dict['category']} " - f"| style_tone:{product_dict['style_tone']}") - print(f"卖点:{product_dict['selling_points']}") - - note = fake_note(product_dict) - - from app.services.ai_engine.image_gen import generate_storyboard_images - results = await generate_storyboard_images( - client=clients, - note=note, - product=product_dict, - image_count=IMAGE_COUNT, - reference_images=[ref_bytes], - ) - - ok = 0 - for i, r in enumerate(results, 1): - role = r.get("role", f"img{i}") - if r.get("error"): - print(f" [{i}] {role} ❌ {r['error']}") - continue - b = r.get("image_bytes") or b"" - path = os.path.join(OUT_DIR, f"{i:02d}_{role}.png") - with open(path, "wb") as f: - f.write(b) - ok += 1 - print(f" [{i}] {role} ✅ {len(b)} bytes → {path}") - - print(f"\n完成:{ok}/{len(results)} 张成功,输出目录 {OUT_DIR}") - await clients.aclose() - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/test/output/01_连通测试.png b/test/output/01_连通测试.png deleted file mode 100644 index 96bacb2..0000000 Binary files a/test/output/01_连通测试.png and /dev/null differ diff --git a/test/output/02_封面hook.png b/test/output/02_封面hook.png deleted file mode 100644 index 8d2328b..0000000 Binary files a/test/output/02_封面hook.png and /dev/null differ diff --git a/test/output/03_质感texture.png b/test/output/03_质感texture.png deleted file mode 100644 index e3b8713..0000000 Binary files a/test/output/03_质感texture.png and /dev/null differ diff --git a/test/output/04_软转化closer.png b/test/output/04_软转化closer.png deleted file mode 100644 index bcea75d..0000000 Binary files a/test/output/04_软转化closer.png and /dev/null differ diff --git a/test/package-lock.json b/test/package-lock.json deleted file mode 100644 index 9e01c3c..0000000 --- a/test/package-lock.json +++ /dev/null @@ -1,271 +0,0 @@ -{ - "name": "test", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "test", - "version": "1.0.0", - "license": "ISC", - "dependencies": { - "dotenv": "^17.4.2", - "form-data": "^4.0.5" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT" - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/dotenv": { - "version": "17.4.2", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", - "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", - "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", - "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - } - } -} diff --git a/test/package.json b/test/package.json deleted file mode 100644 index 54de2b1..0000000 --- a/test/package.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "test", - "version": "1.0.0", - "main": "aiClient.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "keywords": [], - "author": "", - "license": "ISC", - "description": "", - "dependencies": { - "dotenv": "^17.4.2", - "form-data": "^4.0.5" - } -} diff --git a/test/ref_product.png b/test/ref_product.png deleted file mode 100644 index 8d2328b..0000000 Binary files a/test/ref_product.png and /dev/null differ diff --git a/test/suyanshuang_data.js b/test/suyanshuang_data.js deleted file mode 100644 index 515fad2..0000000 --- a/test/suyanshuang_data.js +++ /dev/null @@ -1,61 +0,0 @@ -'use strict'; -// 素颜霜真实素材 + 移植自产品包image.js的小红书种草prompt -// 卖点/成分/标签均取自北哥真实文案(文案3.txt),不杜撰 - -const PRODUCT = { - name: '倍分子素颜霜', - category: '美妆护肤·素颜霜', - sellingPoints: ['黄黑皮提亮', '伪素颜自然', '不假白不卡纹', '洗面奶就能卸', '懒人必备'], - ingredients: ['烟酰胺(改善暗沉/自然提亮)', '水解珍珠(锁水保湿/匀肤色)', '角鲨烷(裸妆奶油肌/不卡纹)'], - texture: '水润奶油质地,丝滑一抹到底,上脸清透不厚重', - tags: ['#素颜霜', '#素颜霜分享', '#黄黑皮', '#冷白皮', '#早八', '#伪素颜'], - // 合规红线(取自产品包违禁词):禁用 美白/祛斑/速效/医用/药妆;视觉禁 before/after/变白 - forbidden: ['美白', '祛斑', '速效', '医用', '药妆', '前后对比', '变白'], -}; - -// 真实拍摄感约束(移植自 xhs-tool single-product.md,补强真实感) -const REALISM = `真实拍摄感:模拟iPhone主摄浅景深,光线不均匀有明暗过渡,允许桌面轻微纹理、织物自然褶皱;避免AI完美对称、塑料感无瑕疵、过度饱和。追求"小红书博主随手拍"质感,不是广告大片。`; - -// 违禁规避(移植自 image.js basePrompt 合规段) -const COMPLIANCE = `重要限制:禁止生成小红书/App界面截图,禁止点赞评论分享底栏头像等社交App元素;禁止肤色变白、瑕疵消失、功效前后对比等暗示;允许安全的质地状态对比(未推开/推开后、薄涂/厚涂、手背局部肤感);避免"美白/祛斑/速效/医用/药妆"等违规词;中文文字少而清晰,每张只一个主标题,正文点位最多3条,四周留安全边距文字不贴边不被裁切;必须保持产品包装、颜色、质感与参考图一致。`; - -// 3个叙事角色(移植自 image.js storyboardByRole 的美妆护肤分支) -const ROLES = { - hook: { - name: '封面-钩子', - title: '黄黑皮逆袭|伪素颜天花板', - visual: '自然光生活场景,手持素颜霜或产品在桌面前景,画面有真实肤感/手部细节,像iPhone实拍封面', - overlay: '黄黑皮也能拥有的妈生好皮', - }, - texture: { - name: '质感-证明', - title: '水润奶油质地·一抹到底', - visual: '手背或指尖涂抹质地微距,产品放在旁边,自然光,真实皮肤纹理,展示未推开与推开后的真实质地状态', - overlay: '清透不厚重·不卡纹不假白', - }, - closer: { - name: '软转化-囤货', - title: '挖到宝了·姐妹快冲', - visual: '产品放在梳妆台/宿舍桌面囤货角,配日常物品,真实分享氛围,轻量种草口吻', - overlay: '洗面奶就能卸·懒人必备', - }, -}; - -// 组装单张图的完整prompt(移植自 image.js promptFromStoryboard) -function buildImagePrompt(roleKey) { - const r = ROLES[roleKey]; - return [ - `生成一张小红书可上传的独立竖版3:4图文海报/素材图,目标1024x1536,可直接上传的独立图片,不是提示词不是App截图。`, - `产品:${PRODUCT.name}(${PRODUCT.category})。`, - `本张角色:${r.name}。`, - `画面主体:${r.visual}。`, - `图上文字(少而精):主标题「${r.title}」,可选点位「${r.overlay}」。`, - `核心卖点:${PRODUCT.sellingPoints.join('、')}。`, - `质地:${PRODUCT.texture}。`, - `视觉风格:小红书种草风,明亮干净,暖色调,自然实拍质感。`, - REALISM, - COMPLIANCE, - ].join('\n'); -} - -module.exports = { PRODUCT, ROLES, buildImagePrompt }; diff --git a/test/水印验证落盘.md b/test/水印验证落盘.md deleted file mode 100644 index e1e1ed5..0000000 --- a/test/水印验证落盘.md +++ /dev/null @@ -1,53 +0,0 @@ -# Clover · AI生图水印问题 验证落盘 - -> 创建:2026-06-04|验证人:CC|性质:洞3子问题B(生成的图能不能直接发)实测 -> 触发:倩倩姐指出「AI生图有暗水印,发小红书会被识别,传统靠PS转格式,系统里不能再拉PS」 - -## 一句话结论 -AI生图的"暗水印"分两层。**第1层(C2PA元数据)一行代码重编码就清除,根本不用PS;第2层(像素隐形水印)靠"转码+轻微像素扰动"自动化削弱**。两层都能在后端图像库(sharp/Pillow)里一道工序完成,全程不碰PS。 - -## 暗水印的真身(实测扒出来的) -对刚生成的 `02_封面hook.png` 扒元数据,确认水印是 **C2PA**(Content Provenance 内容溯源标准): -- PNG里有个 `caBX` 数据块,明文写着: - - `OpenAI Media Service API` ← 谁生成的 - - `c2pa.watermarked` ← 它自己声明"我被加了水印" - - `urn:c2pa:...` 唯一ID + 数字签名 + sha256哈希 ← 一整套"AI出生证明" - -### 两层水印对照 -| 层 | 是什么 | 藏在哪 | 去除难度 | -|---|---|---|---| -| 第1层 C2PA元数据 | 明文"AI出生证明"(来源/时间/签名) | PNG的`caBX`块,**与像素无关** | 🟢 极易,重编码即掉 | -| 第2层 SynthID像素水印 | 埋在像素里的隐形扰动,肉眼不可见 | 像素本身 | 🟡 较难,需重编码+扰动削弱 | - -> 倩倩姐说的"PS转格式"= 干掉第1层(重新导出丢元数据)。系统里用sharp/Pillow重编码等价替代,不用PS。 - -## 实测结果 -### 第1层 C2PA:重编码即清除(已验证✅) -``` -02_封面hook.png 2149880字节 C2PA: ❌还在(原图) -test_cleaned.png 2013094字节 C2PA: ✅已清除(PNG重存) -test_cleaned.jpg 301450字节 C2PA: ✅已清除(转JPG,顺带2MB→300KB) -``` -结论:图像库读进来→重新save(),默认不保留元数据块,C2PA标识彻底消失。 - -### 第2层 像素水印:三种自动化手段(已验证可生成✅) -1. **转JPG有损压缩(q90)** → 压缩天然扰动像素,最低成本,266KB -2. **缩放重采样(缩2px再放回)** → 像素网格位移,水印对齐被破坏,278KB -3. **裁边1px** → 改变像素坐标基准,297KB - -三种单用或组合,后端一道工序完成,是"PS手工转格式"的自动化版本。 - -## 落到产品架构(重要) -- **新增一道必经工序:出图后处理(post-process)**。位置在 worker 出图成功 → 存盘/返回前,强制过一遍"去水印"。 -- 工序内容:图像库重编码(清C2PA) + 转JPG/轻缩放(削像素水印) + 顺带压缩瘦身。 -- 这道工序**对客户透明**,客户拿到的就是干净可直发的图,不需要任何手动操作、不碰PS。 -- 技术栈:后端已选sharp(Node)或Pillow(Python),本就在依赖里,零新增成本。 - -## 诚实的边界(不吹) -- 第1层C2PA:100%能去,已实测。 -- 第2层SynthID:是Google/OpenAI私有算法,**检测器不公开**,本地无法100%验证"是否彻底去干净"。行业现状:转码+重采样能显著削弱/破坏其鲁棒性,但没有任何方案敢保证100%。 -- **务实策略**:先上"重编码+转JPG+轻微重采样"这套标准工序(覆盖99%场景);真要较真,后期可加更强的像素扰动或第三方去水印库。这条不卡当前进度。 - -## 待办 -- [ ] 产品架构方案补一道"出图后处理/去水印"工序(worker出图→后处理→存盘) -- [ ] 临时验证文件清理:output/test_cleaned.* output/clean_*.jpg diff --git a/test/洞1链接读取验证落盘.md b/test/洞1链接读取验证落盘.md deleted file mode 100644 index ed950a7..0000000 --- a/test/洞1链接读取验证落盘.md +++ /dev/null @@ -1,57 +0,0 @@ -# Clover · 洞1 标杆链接读取 验证落盘 - -> 创建:2026-06-04|验证人:CC|性质:洞1(标杆链接能否读取)实测+代码勘察 -> 北哥真实标杆链接3条(采集表xlsx): -> - xiaohongshu.com/explore/63db1c9b00000000020011e2 -> - xiaohongshu.com/explore/649b90240000000011012d8a -> - xiaohongshu.com/explore/680ae7d3000000001c028a5b - -## 一句话结论 -不是"链接读不了",是**裸抓读不了、登录态能读**。但凡"系统替客户去取数据"——哪怕只取一条——机制和批量爬虫一样、风险一样(封号+违反平台ToS)。真正干净的分界线 = 系统碰不碰小红书服务器。 - -## 实测1:裸HTTP抓取 = 死 -3条真实链接裸抓(无登录态、无签名),全部返回**完全相同的552780字节风控兜底假页**,标题"你访问的页面不见了",正文/赞藏评全被抽空。小红书反爬:必须 登录Cookie + 动态签名(x-s/x-t) 才给真数据。 - -## 实测2:本地已有一套跑通的"登录态采集"方案(金矿) -倩倩姐n8n里有现成跑通链路,代码全在本地: -- **工作流**:`企业培训项目/n8n/小红书仿写.json`(链接→读文案→读图片→二创→发布全链路) - - 节点 `@donney521/n8n-nodes-xiaohongshu`:operation=basicInfo(读文案) / images(读图) / search(搜索) -- **节点源码**:`/Users/qiyu/n8n-nodes-xiaohongshu/`(仅依赖axios,逻辑在 src/XhsClient.ts) -- **签名服务**:`/Users/qiyu/xhs-sign-server/sign_server.py`(246行,Flask+Playwright,端口5005,当前未运行) - -### 机制(透明了) -``` -笔记链接 → XhsClient提取noteId - → 调签名服务(Playwright开隐身真浏览器,跑小红书自家JS函数 window._webmsxyw 算签名) - → 带 Cookie + x-s/x-t 请求 edith.xiaohongshu.com(小红书内部API) - → 拿到真数据:title/desc/likedCount/collectedCount/commentCount/images[]/tags[]/user -``` -聪明点:不破解算法,而是让小红书**自己页面的JS**替我们算签名(stealth伪装绕webdriver检测)。算法它改、函数还是它自己的,照用。 - -### 三个脆弱点(诚实) -1. 🟡 要养Cookie:登录态(a1+web_session)需扫码、会过期、要维护 -2. 🟡 签名服务是重资产:常驻Playwright无头浏览器,吃内存、要保活、会崩 -3. 🔴 **合规与封号**:走内部API+绕反爬=违反平台ToS,号可能被封,对外卖有合规隐患 - -## 关键判断:「输入链接→读取」也算自动扒 -- 区别只在"扒多少"(频率→被抓速度),不在"扒不扒"(性质→都违规、都可能封号) -- 风险落在**提供登录态的那个账号**头上:我们的号→坑我们;客户的号→坑客户 -- 真正分界线 = **系统碰不碰小红书服务器**: - - 系统替客户取(输入链接自动读) → 碰 → 算扒、有封号风险 - - 客户自己截图+粘文案 → 不碰 → 不算扒、零平台风险 - -## 洞1 最终结论:分场景三档(修正"一律降级截图") -| 档 | 方案 | 拿数据 | 风险 | 定位 | -|---|---|---|---|---| -| 🔴 裸抓 | 纯链接抓 | ❌实测死 | — | 排除 | -| 🟢 截图手填 | 客户截图+粘文案 | ✅ | 零平台风险 | **一期对外SaaS标配(安全底座)** | -| 🟡 登录态采集 | 链接自动提取(这套n8n) | ✅全套 | 养号+签名服务+封号合规 | **北哥内部自用插件,二期可选模块,不进对外标配** | - -### 决策依据 -1. Clover要卖给郑州一堆企业 → 对外标配不能内置"会让客户号被封"的功能 → 对外用🟢截图手填 -2. 北哥自己做内容、自担风险、用自己的号 → 可用🟡登录态采集帮自己扒标杆 → 北哥车间内部加速插件 -3. 这套代码现成跑通,真要做直接搬不重造(又一条"走过的对的路"),存档备用 - -## 待办 -- [ ] 架构方案:标杆采集对外=截图手填;登录态采集列为北哥内部/二期可选模块 -- [ ] (二期若做)签名服务+XhsClient打包进Clover北哥车间,作opt-in diff --git a/test/洞2北哥意愿验证落盘.md b/test/洞2北哥意愿验证落盘.md deleted file mode 100644 index 7052404..0000000 --- a/test/洞2北哥意愿验证落盘.md +++ /dev/null @@ -1,45 +0,0 @@ -# Clover · 洞2 北哥愿不愿用 验证落盘 - -> 创建:2026-06-04|验证人:CC+倩倩姐判断|性质:洞2(北哥愿不愿放下豆包用Clover)资料实测+倩倩姐拍板 -> 证据源:会议记录(产品业务推进会议.txt) + 北哥团队填写的采集表(xlsx) + 倩倩姐对北哥的直接了解 - -## 一句话结论 -"愿不愿放下豆包"是伪命题。真相:**北哥已看过Clover生成的图、基本满意,只剩字体细节要优化**——换工具的坎已迈过,剩下是产品打磨不是说服。 - -## 关键证据 -### 证据1:北哥的豆包只用在"写文案"一步,且不是痛点 -采集表里北哥团队填的真实流程(一条笔记1小时): -| 步骤 | 工具 | 耗时 | -|---|---|---| -| 1定主题 | — | 5分 | -| 2提卖点 | — | 10分 | -| 3刷爆款截图 | 手动(小红书App) | — | -| 4写文案 | **豆包** | 15分 | -| 5配图 | — | 2分 | -| 6、7 | — | **北哥标注:最花时间** | -→ 最痛的是出图/配图(6、7),不是写文案。**Clover主攻出图,正中痛点。** - -### 证据2:北哥已看过Clover的图,基本满意(倩倩姐确认) -- 之前给北哥看过Clover生成的图,他**基本满意**,只提"一小部分字体"要优化 -- 字体问题=基线测试发现的"瓶身小字糊",后续细节优化(等北哥出图标准来迭代prompt)即可 -- → 洞2从"推测"升级为"已验证认可" - -### 证据3:采集表"禁纯AI"红线 → 已可化解 -- 采集表模块C填了"必须实拍/禁纯AI",但这是认知低时写的(见progress认知1) -- 化解牌:gpt-image-2带产品实拍图(image edit)=基于真实产品图再创作,非凭空AI画,还原度高 -- 北哥已看过图基本满意 = 这道心理坎实际已过 - -## 倩倩姐拍板的产品设计(定下来) -1. **文案双轨**:Clover自带文案(背后接更强大模型,质量大概率优于豆包) + 支持导入外部文案(豆包等) - - 豆包唯一可能优势=对中国用户语感理解,存疑不一定 -2. **客户可自调prompt**:文案和图,客户都能改提示词,不是黑盒(呼应基石约束A不写死 + 可插拔/自调整) -3. **共存不强抢**:不喊"取代豆包",喊"帮你把最累的出图自动化";豆包想用还用,Clover无缝接 - -## 洞2 最终结论 -- 北哥用Clover概率:高(已看过图、基本满意) -- 产品姿态:对着痛点(出图)说话,文案双轨+客户可调prompt+允许豆包共存 -- 遗留:字体优化(细节,等北哥标准迭代prompt) → 不阻塞 - -## 待办 -- [ ] 架构方案:文案双轨(自带+导入) / 客户可调prompt / 出图为主战场 -- [ ] (细节期)字体优化随北哥出图标准一起迭代 diff --git a/test/洞4收费验证落盘.md b/test/洞4收费验证落盘.md deleted file mode 100644 index fe8b1bb..0000000 --- a/test/洞4收费验证落盘.md +++ /dev/null @@ -1,39 +0,0 @@ -# Clover · 洞4 软件收费/商业模式 验证落盘 - -> 创建:2026-06-04|验证人:CC+倩倩姐拍板|性质:洞4(软件收费)商业判断(7洞最后一个) -> 锚定约束:老板战略"TOKEN充值加进产品=业务闭环"(产品业务推进会议.txt) + 已拍板决策1(key隔离计费) - -## 一句话结论 -MVP阶段软件免费(收费逻辑底层麻烦,后续单独讨论);token统一走我们自己的站url,但每账号自录自用各算各的key隔离不变。url收口到自己站(token生意闭环) + key隔离(成本清晰),两全。 - -## 收费两层(自助打印店比喻) -| 层 | 收什么 | MVP定法 | -|---|---|---| -| 🟦耗材费(token) | AI生成消耗 | 客户付,买我们token站的token,走自己的key | -| 🟩软件费 | 用Clover工具本身 | **MVP免费**,收费逻辑后续讨论(底层麻烦) | - -## 倩倩姐拍板(定下来) -1. **软件MVP免费**:收费底层麻烦,后续单独讨论,MVP先免费用、先把产品跑起来验证价值 -2. **token统一用我们自己站的url**:所有账号都连我们token站(不再兼容客户填任意中转站url) -3. **key按人隔离自录自用**: - - 小王登录→录小王的key(他在我们这买的/公司买的),小李登录→录小李的key - - 不同key不同计费,各算各的(决策1保留不变) - - 共同点:用的都是我们token站的url -4. 设计精髓:**url统一=token业务收口到自己站(老板"卖token闭环"落地) + key隔离=每人成本清晰不混账** - -## 对已拍板决策1的修正(小修) -- 原决策1:"每账号绑自己token站key,按key单独算,key自录自用,禁止合并站级key混算;兼容填url+key" -- 修正:**url收窄=统一用我们站(不再兼容任意中转站url)**;key自录自用+按key隔离计费**完全不变** -- 新增:软件费MVP免费(收费推迟到产品成熟后单独讨论) - -## 业内四模式备查(收费阶段再用) -| 模式 | 说明 | 备注 | -|---|---|---| -| A订阅(按月/年) | 现金流稳 | 收费阶段候选 | -| B按席位/量 | 匹配多租户+中央厨房 | 收费阶段候选(荐) | -| C买断 | 违背"闭环"战略 | 排除 | -| D token抽成 | key自录自用→平台无法抽成 | 排除(但卖自家token站token=另一种token变现,已采用) | - -## 待办 -- [ ] MVP架构:软件免费(不做计费墙);token统一站url+按账号key隔离用量 -- [ ] (收费阶段)单独讨论软件收费底层逻辑,候选席位订阅 diff --git a/test/洞5违禁词验证落盘.md b/test/洞5违禁词验证落盘.md deleted file mode 100644 index 146da6b..0000000 --- a/test/洞5违禁词验证落盘.md +++ /dev/null @@ -1,49 +0,0 @@ -# Clover · 洞5 违禁词处理 验证落盘 - -> 创建:2026-06-04|验证人:CC实测+倩倩姐拍板|性质:洞5(违禁词软提示vs硬拦截)代码勘察+真实文案实测 -> 证据:产品包/worker/src/copy.js 现成违禁词逻辑 + 北哥采集表红线 + 北哥真实文案3.txt - -## 一句话结论 -不是"软提示vs硬拦截"二选一,是**三级处理**(自动改/软提示/硬拦截)。产品包代码已验证这个思路,默认分级+北哥可调。 - -## 实测发现 -### 现成资产(产品包 copy.js) -- 违禁词库:`BANNED_WORDS=[美白,祛斑,速效,医用,药妆]` `BANNED_VISUAL_WORDS=[前后对比,使用前后,变白,瑕疵消失,治疗前后...]` -- 检测打分:扫文案命中违禁词→扣合规分→标注"含禁用词:xxx"(379-382行) -- 自动改写 sanitizePlanningText(201-205行):前后对比→质地说明、变白→自然光泽感、瑕疵消失→妆感更服帖、治疗→日常使用 - -### 实测结果 -1. 北哥真实文案3.txt → 命中违禁词**0个**(北哥自己写得很合规,"仅用提亮/自然/伪素颜") -2. 故意造违规文案测试,暴露关键点: - - 检测全中(美白/祛斑/速效/变白/前后对比/医用/药妆一个没漏) - - **但自动改写有边界**:sanitize只配了视觉类替换规则,`美白/祛斑/速效/医用/药妆`这5个功效词**只检测不改写** - - 原因合理:视觉描述有等价合规说法可自动改;功效宣称词替换会扭曲原意,须人工改 - -## 洞5 结论:三级处理(机场安检三通道) -| 级别 | 类型 | 处理 | 例 | 现成 | -|---|---|---|---|---| -| 🟢自动改 | 视觉描述类(有等价合规说法) | 自动改写,客户无感 | 前后对比→质地说明、变白→自然光泽感 | ✅已有sanitize,直接搬 | -| 🟡软提示 | 功效宣称词(改了扭曲原意) | 标黄+建议词,客户自己改 | 美白→提亮?、祛斑→改善暗沉? | 🟡检测有,补提示UI | -| 🔴硬拦截 | 高危词(发了必被封) | 必须改才能发 | 医用、药妆(医疗级宣称) | 🔴需新增 | - -### 为什么三级(不能纯软/纯硬) -- 纯软提示:医用/药妆这类发出去直接限流封号,只标黄不拦,客户手滑就出事 → 须兜底 -- 纯硬拦截:美白在"提亮肤色"语境可合规,一刀切误杀烦客户 -- 自动改:视觉类有现成等价说法,自动改体验最好(产品包已做) - -## 倩倩姐拍板(定下来) -1. **三级处理** ✅ 采纳 -2. **默认分级 + 北哥可调** ✅ 一期就做可调分级 -3. 违禁词库可配置可更新(基石A不写死),平台规则变能后台改 -4. 双保险:生成时prompt就规避(产品包prompt已做) + 发布前检查 -5. 图片视觉违禁(前后对比图):文本扫不到→靠prompt约束模型不生成(基线已验证守住)+发布前人工眼检 - -### 默认分级建议(北哥标准来了可调) -- 🔴硬拦截:医用、药妆、特效、根治(医疗级宣称,封号高危) -- 🟡软提示:美白、祛斑、速效(建议词:提亮/改善暗沉/快速见效) -- 🟢自动改:前后对比、变白、瑕疵消失、治疗前后(视觉类,有等价说法) -> 具体归级待北哥合规标准细化(采集表只填了禁5词,没填严重程度分级) - -## 待办 -- [ ] 架构方案:违禁词三级处理模块(词库可配置+分级可调);自动改逻辑搬产品包sanitize -- [ ] (采集补)向北哥要违禁词严重程度分级 diff --git a/test/洞6发布交付验证落盘.md b/test/洞6发布交付验证落盘.md deleted file mode 100644 index 9c2c0d1..0000000 --- a/test/洞6发布交付验证落盘.md +++ /dev/null @@ -1,56 +0,0 @@ -# Clover · 洞6 发布最后一公里 验证落盘 - -> 创建:2026-06-04|验证人:CC实测+倩倩姐拍板|性质:洞6(发布最后一公里)代码勘察+定位修正 -> ⚠️ 本洞修正了产品出口的根本定位,重要 - -## 一句话结论 -发布出口不是"发到小红书",是**产出达人能直接用的素材交付包**。因为北哥团队=素材生产方(中央厨房),真正发布的是一堆分散的达人。系统自动发从需求上就不成立。 - -## 定位修正(最关键认知) -- 北哥团队 = **素材生产方(中央厨房)**:批量生产种草素材 -- 达人 = **素材发布方(分发渠道)**:拿素材包自己发,分散、不懂上下文 -- Clover出口 = **"达人拿了就能照着发"的结构化素材交付包** -- → 这解释了洞1洞6为何都该手动:真正发布的人是分散的达人,不可能自动登录一堆达人的号 -- → "不能变形/命名好/分类/清单"不是锦上添花,是**刚需**:素材流转到第三方手里,全靠素材包自解释,命名乱/图变形/配错文案=达人发错或不会发 - -## 代码勘察(发布自动化的现成方案=不采用) -- n8n仿写工作流有发布节点 `n8n-nodes-douyin.xiaohongshu` operation=publish -- 机制:调第三方/自建发布中台(Domain默认localhost:3000),getQrCode扫码托管账号→/api/note/publish代发 -- 凭证=API Key+Domain,是又一层"托管账号代发"中间件,非小红书官方 -- 🔴 风险最高:代发=机器发布特征(营销号典型),封号风险比读取(洞1)更大;还多一层第三方托管账号 -- 结论:存档,不采用(且需求上达人发布也用不上自动发) - -## 洞6 结论:三档(对外走素材包交付) -| 档 | 做法 | 风险 | 定位 | -|---|---|---|---| -| 🔴 全自动发布 | 系统直接发小红书 | 最高(封号) | 对外不做+需求不成立(达人分散) | -| 🟢 素材包交付 | 打包好图文,达人手动发 | 零平台风险 | **对外标配** | -| 🟡 官方蒲公英/聚光 | 合规代发 | 需企业资质 | 二期看北哥资质 | - -## 倩倩姐拍板:达人素材交付包(定下来) -做好素材打包、分类、不变形、命名规范、发布清单。手动下载到一个地方,人工(达人)去发。 - -### 交付包结构 -``` -某产品某主题_素材包/ -├── 📋 发布清单.txt ← 达人照做:发几条/每条用哪些图/什么文案/什么标签 -├── 笔记1_封面hook/ -│ ├── 01_封面图.jpg ← 命名带顺序,达人不会传错序 -│ ├── 02_质感图.jpg -│ ├── 03_软转化图.jpg -│ └── 文案.txt ← 标题+正文+标签,达人直接复制 -├── 笔记2_xxx/ -└── ✅ 合规说明.txt ← 违禁词已过/图已去水印,达人放心发 -``` - -### 设计要点(把导出做到不像降级) -1. **图不变形**:去水印后处理时严守尺寸比例(3:4),不挤压拉伸 -2. **命名规范**:序号前缀(01/02/03)保证达人按序传图 -3. **分类清晰**:按笔记分文件夹,一条笔记的图文聚在一起 -4. **发布清单**:达人不懂上下文,清单要能自解释(发几条、配图顺序、文案、标签) -5. **合规说明**:附"违禁词已过/已去水印",让达人放心发 - -## 待办 -- [ ] 架构方案:出口=素材交付包生成器(打包+分类+命名+清单+合规说明) -- [ ] 数据模型考虑"达人"角色/素材包流转(可能影响表设计) -- [ ] 自动发布(n8n方案)存档,二期看官方蒲公英资质 diff --git a/test/洞7数据归属验证落盘.md b/test/洞7数据归属验证落盘.md deleted file mode 100644 index cf9c552..0000000 --- a/test/洞7数据归属验证落盘.md +++ /dev/null @@ -1,38 +0,0 @@ -# Clover · 洞7 数据归属/导出 验证落盘 - -> 创建:2026-06-04|验证人:CC实测+倩倩姐拍板|性质:洞7(数据归属/导出)代码勘察+商业判断 -> 与洞6"达人交付"强联动 - -## 一句话结论 -数据分三类按层归属:原始数据(输入+产出)100%归客户、可导出不绑架;飞轮学到的偏好归平台、作为服务能力。这是C方案(分层归属),倩倩姐已拍板。 - -## 代码勘察(banana地基现状) -- 归属:所有表有`user_id`(按用户归属),但**无workspace_id**(没团队/多租户隔离) → 印证架构方案"全表加workspace_id"决策,地基只到个人级 -- 导出:banana**完全没有导出功能** → 🔴自造项,需新建 - -## 三类数据归属 -| 类型 | 例 | 归谁 | 导出 | 隔离 | -|---|---|---|---|---| -| 🟦客户输入物 | 产品图/卖点/标杆截图/自调prompt | 客户 | ✅必须能导 | workspace隔离 | -| 🟩客户产出物 | 文案/图/素材包 | 客户 | ✅必须能导(本就要交付达人) | workspace隔离 | -| 🟨飞轮偏好 | 调教手感(8选3记录/审核理由/学到的偏好) | **平台** | 不随客户走 | 最敏感 | - -## 倩倩姐拍板(C方案,定下来) -1. **原始数据(输入+产出+素材包) → 100%归客户,随时导出,不绑架** = 对外卖的信任基础,必须承诺 -2. **飞轮学到的偏好 → 归平台**,作为"越用越懂你"的服务能力,客户走了不带走 - - 类比:健身房——你的体测数据是你的(可带走),教练攒下的带人经验是健身房的(带不走) -3. **可导出一期就做**(洞6素材包交付本来就是导出,一套逻辑) -4. **飞轮归属写进给客户的协议/说明**讲明(法务表述可能要老板拍) - -## 叠加洞6达人交付的特殊点 -- 素材包交付给外部达人后=数据已离开Clover,属"客户主动分发",平台不担泄露责任(协议写明客户对其分发负责) -- 达人**不进Clover系统**(只收素材包)→ 不涉及给达人开账号/达人数据隔离 → 简化架构 - -## 对地基的影响 -- 全表加 workspace_id(架构方案已定) = 数据归属的技术底座,实现客户间隔离 -- 新建导出/素材包生成功能(banana没有,🔴自造) = 兑现"可导出"+洞6出口,一套逻辑 -- 偏好数据表标记归属层级(原始data vs 学习成果),为C方案留技术口子 - -## 待办 -- [ ] 架构方案:全表workspace_id隔离 + 导出/素材包生成器 + 偏好表标归属层级 -- [ ] 协议/客户说明:写明"原始数据可导出归客户 / 飞轮偏好归平台"(法务表述待老板拍)