170 lines
5.6 KiB
Python
170 lines
5.6 KiB
Python
"""P4 种子:默认平台 + 脚本模板 + 一个示例 Skill(LOBSTER 模式)。
|
||
|
||
执行:python -m app.scripts.seed_p4
|
||
可重复运行。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import asyncio
|
||
|
||
from sqlalchemy import delete, insert, select
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
||
from app.db.session import AsyncSessionLocal
|
||
from app.models import (
|
||
ChannelCode,
|
||
Platform,
|
||
Product,
|
||
ScriptTemplate,
|
||
Skill,
|
||
product_channel_code,
|
||
skill_product_scope,
|
||
)
|
||
|
||
|
||
DEFAULT_PLATFORMS = [
|
||
{"code": "facebook", "name": "Facebook", "url": "https://facebook.com"},
|
||
{"code": "tiktok", "name": "TikTok", "url": "https://tiktok.com"},
|
||
{"code": "google", "name": "Google Ads", "url": "https://ads.google.com"},
|
||
]
|
||
|
||
DEFAULT_CHANNELS = [
|
||
{"code": "FB_INT_01", "name": "FB 内导 01", "type": "INTERNAL"},
|
||
{"code": "FB_NAT_01", "name": "FB 自然 01", "type": "NATURAL"},
|
||
{"code": "TT_INT_01", "name": "TT 内导 01", "type": "INTERNAL"},
|
||
]
|
||
|
||
DEFAULT_SCRIPTS = [
|
||
{
|
||
"code": "py_hello",
|
||
"name": "Python Hello",
|
||
"language": "python",
|
||
"description": "示例脚本,输出欢迎信息",
|
||
"content": 'print("hello from loom script")\n',
|
||
"timeout_seconds": 30,
|
||
},
|
||
{
|
||
"code": "sh_curl_demo",
|
||
"name": "Shell curl 演示",
|
||
"language": "shell",
|
||
"description": "示例 curl 调用",
|
||
"content": 'echo "fetching..."\ncurl -s https://httpbin.org/ip\n',
|
||
"timeout_seconds": 60,
|
||
},
|
||
]
|
||
|
||
DEFAULT_SKILL = {
|
||
"code": "fb_lobster_post",
|
||
"name": "Facebook 发帖(龙虾)",
|
||
"category": "social",
|
||
"executor_type": "LOBSTER",
|
||
"description": "调用龙虾模板完成 Facebook 发帖",
|
||
"input_schema": {
|
||
"type": "object",
|
||
"required": ["title", "content"],
|
||
"properties": {
|
||
"title": {"type": "string", "title": "标题", "maxLength": 80},
|
||
"content": {"type": "string", "title": "正文", "format": "textarea"},
|
||
"image_url": {"type": "string", "title": "图片链接"},
|
||
"schedule_at": {"type": "string", "title": "定时时间", "format": "date-time"},
|
||
},
|
||
},
|
||
"param_mapping": {
|
||
"post_title": "{{input.title}}",
|
||
"post_body": "{{input.content}}",
|
||
"image_url": "{{input.image_url}}",
|
||
"scheduled": "{{input.schedule_at}}",
|
||
},
|
||
"lobster_template_id": "lobster-demo-001",
|
||
"doc": "演示 Skill:LOBSTER 执行器 + 产品范围 + JSON Schema 参数。.env 配置 LOBSTER_API_KEY 后自动走真实龙虾 API,否则走客户端内置 mock。",
|
||
}
|
||
|
||
|
||
async def upsert(db: AsyncSession, model, key_field: str, value, **defaults):
|
||
stmt = select(model).where(getattr(model, key_field) == value)
|
||
obj = (await db.execute(stmt)).scalar_one_or_none()
|
||
if obj is None:
|
||
obj = model(**{key_field: value}, **defaults)
|
||
db.add(obj)
|
||
await db.flush()
|
||
else:
|
||
for k, v in defaults.items():
|
||
setattr(obj, k, v)
|
||
return obj
|
||
|
||
|
||
async def seed() -> None:
|
||
async with AsyncSessionLocal() as db:
|
||
platforms = {}
|
||
for p in DEFAULT_PLATFORMS:
|
||
obj = await upsert(db, Platform, "code", p["code"],
|
||
name=p["name"], url=p["url"])
|
||
platforms[p["code"]] = obj
|
||
|
||
channels = {}
|
||
for c in DEFAULT_CHANNELS:
|
||
obj = await upsert(db, ChannelCode, "code", c["code"],
|
||
name=c["name"], type=c["type"])
|
||
channels[c["code"]] = obj
|
||
|
||
# 一个示例产品:FB / 产品代号 fb_demo
|
||
fb_product = await upsert(
|
||
db, Product, "code", "fb_demo",
|
||
name="FB 示例产品",
|
||
platform_id=platforms["facebook"].id,
|
||
description="P4 演示用产品",
|
||
)
|
||
# 绑定 2 个渠道码
|
||
await db.execute(
|
||
delete(product_channel_code).where(
|
||
product_channel_code.c.product_id == fb_product.id
|
||
)
|
||
)
|
||
await db.execute(
|
||
insert(product_channel_code),
|
||
[
|
||
{"product_id": fb_product.id, "channel_code_id": channels["FB_INT_01"].id},
|
||
{"product_id": fb_product.id, "channel_code_id": channels["FB_NAT_01"].id},
|
||
],
|
||
)
|
||
|
||
for s in DEFAULT_SCRIPTS:
|
||
await upsert(db, ScriptTemplate, "code", s["code"], **{
|
||
"name": s["name"],
|
||
"language": s["language"],
|
||
"description": s["description"],
|
||
"content": s["content"],
|
||
"timeout_seconds": s["timeout_seconds"],
|
||
})
|
||
|
||
# 示例 Skill
|
||
stmt = select(Skill).where(Skill.code == DEFAULT_SKILL["code"])
|
||
skill = (await db.execute(stmt)).scalar_one_or_none()
|
||
if skill is None:
|
||
skill = Skill(**DEFAULT_SKILL)
|
||
db.add(skill)
|
||
await db.flush()
|
||
else:
|
||
for k, v in DEFAULT_SKILL.items():
|
||
setattr(skill, k, v)
|
||
|
||
await db.execute(
|
||
delete(skill_product_scope).where(skill_product_scope.c.skill_id == skill.id)
|
||
)
|
||
await db.execute(
|
||
insert(skill_product_scope),
|
||
[{"skill_id": skill.id, "product_id": fb_product.id}],
|
||
)
|
||
|
||
await db.commit()
|
||
print("[seed_p4] done.")
|
||
print(f" platforms: {[p['code'] for p in DEFAULT_PLATFORMS]}")
|
||
print(f" channels: {[c['code'] for c in DEFAULT_CHANNELS]}")
|
||
print(f" scripts: {[s['code'] for s in DEFAULT_SCRIPTS]}")
|
||
print(f" product: fb_demo (2 channels)")
|
||
print(f" skill: {DEFAULT_SKILL['code']} (LOBSTER, scope=[fb_demo])")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
asyncio.run(seed())
|