chore: finalize delivery cleanup

This commit is contained in:
yangqianqian
2026-06-30 18:41:16 +08:00
parent df1856d793
commit 341f3d2b0d
25 changed files with 48 additions and 1054 deletions

7
.gitignore vendored
View File

@@ -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

View File

@@ -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]:

View File

@@ -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
====================

View File

@@ -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()

View File

@@ -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())

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 MiB

View File

@@ -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();

View File

@@ -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}`);
})();

View File

@@ -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 };

View File

@@ -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 的 keyuser_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())

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

271
test/package-lock.json generated
View File

@@ -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"
}
}
}
}

View File

@@ -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"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

View File

@@ -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 };

View File

@@ -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层C2PA100%能去,已实测。
- 第2层SynthID是Google/OpenAI私有算法**检测器不公开**本地无法100%验证"是否彻底去干净"。行业现状:转码+重采样能显著削弱/破坏其鲁棒性但没有任何方案敢保证100%。
- **务实策略**:先上"重编码+转JPG+轻微重采样"这套标准工序(覆盖99%场景);真要较真,后期可加更强的像素扰动或第三方去水印库。这条不卡当前进度。
## 待办
- [ ] 产品架构方案补一道"出图后处理/去水印"工序(worker出图→后处理→存盘)
- [ ] 临时验证文件清理output/test_cleaned.* output/clean_*.jpg

View File

@@ -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

View File

@@ -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 / 出图为主战场
- [ ] (细节期)字体优化随北哥出图标准一起迭代

View File

@@ -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隔离用量
- [ ] (收费阶段)单独讨论软件收费底层逻辑,候选席位订阅

View File

@@ -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
- [ ] (采集补)向北哥要违禁词严重程度分级

View File

@@ -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方案)存档,二期看官方蒲公英资质

View File

@@ -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隔离 + 导出/素材包生成器 + 偏好表标归属层级
- [ ] 协议/客户说明:写明"原始数据可导出归客户 / 飞轮偏好归平台"(法务表述待老板拍)