🔄 卡若AI 同步 2026-03-02 02:35 | 更新:水桥平台对接、卡木、运营中枢工作台 | 排除 >20MB: 14 个
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
---
|
||||
name: 飞书管理
|
||||
description: 飞书日志/文档自动写入与知识库管理
|
||||
triggers: 飞书日志、写入飞书、飞书知识库、飞书运营报表、派对效果数据、104场写入、运营报表填写、派对截图填表发群、Excel写飞书、批量写飞书表格、表格日报、卡若的飞书日志、卡若飞书日志
|
||||
triggers: 飞书日志、写入飞书、飞书知识库、飞书运营报表、派对效果数据、104场写入、运营报表填写、派对截图填表发群、Excel写飞书、批量写飞书表格、表格日报、卡若的飞书日志、卡若飞书日志、上传json飞书、json上传飞书文档、按原格式上传飞书
|
||||
owner: 水桥
|
||||
group: 水
|
||||
version: "1.2"
|
||||
updated: "2026-02-25"
|
||||
version: "1.3"
|
||||
updated: "2026-03-02"
|
||||
---
|
||||
|
||||
# 飞书日志写入 Skill
|
||||
@@ -349,6 +349,29 @@ JSON 格式:与 `团队入职流程与新人登记表_feishu_blocks.json` 相
|
||||
|
||||
---
|
||||
|
||||
## 飞书导出 JSON 按原格式上传
|
||||
|
||||
将飞书导出的 JSON 文件(含 `content` + `blocks`)**按原有类型**上传为 Wiki 子文档:文档保持文档、多维表格会新建多维表格并嵌入、问卷/思维笔记等按 JSON 内类型还原。
|
||||
|
||||
**规则**:原 JSON 里是什么格式就生成什么格式;不把多维表格/看板换成链接,直接生成对应文档或多维表格块。
|
||||
|
||||
```bash
|
||||
# 上传单个导出 JSON(默认父节点:日记/新研究)
|
||||
python3 脚本/upload_json_to_feishu_doc.py /path/to/xxx.json
|
||||
|
||||
# 指定父节点与标题
|
||||
python3 脚本/upload_json_to_feishu_doc.py /path/to/xxx.json --parent <wiki_node_token> --title "文档标题"
|
||||
```
|
||||
|
||||
- **block_type 2**:正文 → 正文块
|
||||
- **block_type 3/4/6**:标题 → 对应标题块
|
||||
- **block_type 43(board/bitable)**:多维表格 → 在云空间新建多维表格(bitable),并将该块嵌入文档
|
||||
- 其他类型(todo、callout 等)按导出结构透传
|
||||
|
||||
应用需具备「创建多维表格」权限;若无权限,多维表格块会退化为一段说明文字。
|
||||
|
||||
---
|
||||
|
||||
## 统一文章上传(强制入口)
|
||||
|
||||
用于“本地 Markdown → 飞书 Wiki 文档”的统一发布。
|
||||
@@ -424,6 +447,7 @@ python3 script.py --arg value
|
||||
├── feishu_video_clip_README.md
|
||||
├── wanzhi_feishu_project_sync.py # 玩值电竞→飞书项目同步
|
||||
├── feishu_wiki_create_doc.py # Wiki 子文档创建(日记/研究)
|
||||
├── upload_json_to_feishu_doc.py # 飞书导出 JSON 按原格式上传(文档/多维表格/问卷等)
|
||||
└── .feishu_tokens.json # Token 存储
|
||||
```
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
| 18 | 画廊 | `gallery` | `gallery.imageList[].fileToken`,单图也可用 |
|
||||
| 19 | 高亮块 | `callout` | `callout.background_color`、`border_color`、`emoji_id`(如 sunrise) |
|
||||
| 27 | 图片(导出) | `image` | 导出用;**写入用 12 file**,先 `drive/v1/medias/upload_all` 上传 |
|
||||
| 43 | 多维表格 | `board`/`bitable` | 导出为 `board.token`;写入用 `bitable.token`。按原格式上传时由 `upload_json_to_feishu_doc.py` 新建多维表格并嵌入 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
将飞书导出的 JSON 文件(含 content + blocks)上传为飞书 Wiki 子文档。
|
||||
用法: python3 upload_json_to_feishu_doc.py /path/to/水流程规划.json
|
||||
将飞书导出的 JSON 文件(含 content + blocks)按**原有格式**上传为飞书 Wiki 子文档。
|
||||
- 文档→文档,多维表格→多维表格(block_type 43 会新建多维表格并嵌入),问卷/思维笔记等按类型还原。
|
||||
用法: python3 upload_json_to_feishu_doc.py /path/to/xxx.json
|
||||
可选: --parent <wiki_node_token> --title "文档标题"
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
import argparse
|
||||
import requests
|
||||
from pathlib import Path
|
||||
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
@@ -18,37 +21,65 @@ from feishu_wiki_create_doc import create_wiki_doc, get_token, CONFIG
|
||||
DEFAULT_PARENT = "KNf7wA8Rki1NSdkkSIqcdFtTnWb"
|
||||
|
||||
|
||||
def _text_block(content: str):
|
||||
return {
|
||||
"block_type": 2,
|
||||
"text": {
|
||||
"elements": [{"text_run": {"content": content, "text_element_style": {}}}],
|
||||
"style": {},
|
||||
},
|
||||
}
|
||||
def _to_api_block(b: dict) -> dict | None:
|
||||
"""将导出块转为 API 可用的块(去掉 block_id、parent_id,保留 block_type 与类型字段)。"""
|
||||
bt = b.get("block_type")
|
||||
out = {"block_type": bt}
|
||||
if bt == 2 and b.get("text"):
|
||||
out["text"] = b["text"]
|
||||
elif bt == 3 and b.get("heading1"):
|
||||
out["heading1"] = b["heading1"]
|
||||
elif bt == 4 and b.get("heading2"):
|
||||
out["heading2"] = b["heading2"]
|
||||
elif bt == 6 and b.get("heading4"):
|
||||
out["heading4"] = b["heading4"]
|
||||
elif bt == 17 and b.get("todo"):
|
||||
out["todo"] = b["todo"]
|
||||
elif bt == 19 and b.get("callout"):
|
||||
out["callout"] = b["callout"]
|
||||
elif bt == 43:
|
||||
# 多维表格:导出为 board.token,API 为 bitable.token;占位,后续用新建的 app_token 替换
|
||||
token = (b.get("board") or b.get("bitable") or {}).get("token", "")
|
||||
out["_bitable_placeholder"] = True
|
||||
out["_bitable_token"] = token # 可能为原文档 token(同租户可尝试直接嵌)
|
||||
out["bitable"] = {"token": token or "PLACEHOLDER"}
|
||||
else:
|
||||
# 其他类型尽量透传类型字段
|
||||
for key in ("page", "board", "bitable", "sheet", "mindnote", "poll"):
|
||||
if key in b and not key.startswith("_"):
|
||||
out[key] = b[key]
|
||||
break
|
||||
if "_bitable_placeholder" not in out and "bitable" not in out and "board" in b:
|
||||
out["_bitable_placeholder"] = True
|
||||
out["bitable"] = {"token": (b.get("board") or {}).get("token", "PLACEHOLDER")}
|
||||
return out
|
||||
|
||||
|
||||
def _heading1_block(content: str):
|
||||
return {
|
||||
"block_type": 3,
|
||||
"heading1": {
|
||||
"elements": [{"text_run": {"content": content, "text_element_style": {}}}],
|
||||
"style": {},
|
||||
},
|
||||
}
|
||||
def create_bitable_app(access_token: str, name: str, folder_token: str | None = None) -> str | None:
|
||||
"""在飞书云空间创建多维表格,返回 app_token。"""
|
||||
url = "https://open.feishu.cn/open-apis/bitable/v1/apps"
|
||||
headers = {"Authorization": f"Bearer {access_token}", "Content-Type": "application/json"}
|
||||
payload = {"name": name or "多维表格"}
|
||||
if folder_token:
|
||||
payload["folder_token"] = folder_token
|
||||
r = requests.post(url, headers=headers, json=payload, timeout=30)
|
||||
data = r.json()
|
||||
if data.get("code") == 0:
|
||||
return data.get("data", {}).get("app_token")
|
||||
return None
|
||||
|
||||
|
||||
def blocks_from_export_json(data: dict) -> tuple[str, list]:
|
||||
"""
|
||||
从飞书导出格式(content + blocks)解析出标题和可写入的 children 列表。
|
||||
多维表格(block_type 43/board)会产出占位块,需在获得 token 后调用 resolve_bitable_placeholders 替换。
|
||||
返回 (title, children)。
|
||||
"""
|
||||
blocks = data.get("blocks") or []
|
||||
if not blocks:
|
||||
title = (data.get("content") or "未命名").split("\n")[0].strip() or "未命名"
|
||||
return title, [_heading1_block(title)]
|
||||
return title, [{"block_type": 3, "heading1": {"elements": [{"text_run": {"content": title, "text_element_style": {}}}], "style": {}}}]
|
||||
|
||||
# 找根块(block_type 1 page 或 parent_id 为空)
|
||||
root = None
|
||||
by_id = {}
|
||||
for b in blocks:
|
||||
@@ -58,9 +89,8 @@ def blocks_from_export_json(data: dict) -> tuple[str, list]:
|
||||
|
||||
if not root:
|
||||
title = (data.get("content") or "未命名").split("\n")[0].strip() or "未命名"
|
||||
return title, [_heading1_block(title)]
|
||||
return title, [{"block_type": 3, "heading1": {"elements": [{"text_run": {"content": title, "text_element_style": {}}}], "style": {}}}]
|
||||
|
||||
# 标题:从 page 或 content 第一行取
|
||||
title = "未命名"
|
||||
if root.get("page") and root["page"].get("elements"):
|
||||
for el in root["page"]["elements"]:
|
||||
@@ -73,7 +103,7 @@ def blocks_from_export_json(data: dict) -> tuple[str, list]:
|
||||
|
||||
child_ids = root.get("children") or []
|
||||
children = []
|
||||
children.append(_heading1_block(title))
|
||||
children.append({"block_type": 3, "heading1": {"elements": [{"text_run": {"content": title, "text_element_style": {}}}], "style": {}}})
|
||||
|
||||
for bid in child_ids:
|
||||
b = by_id.get(bid)
|
||||
@@ -82,24 +112,44 @@ def blocks_from_export_json(data: dict) -> tuple[str, list]:
|
||||
bt = b.get("block_type")
|
||||
if bt == 2 and b.get("text"):
|
||||
els = b["text"].get("elements") or []
|
||||
content = ""
|
||||
for el in els:
|
||||
content += el.get("text_run", {}).get("content", "")
|
||||
content = content.strip()
|
||||
if content:
|
||||
children.append(_text_block(content))
|
||||
# 空段落可跳过,也可保留一行空
|
||||
elif bt == 43 and b.get("board"):
|
||||
# 多维表格/看板:API 可能不支持直接插入,用说明占位
|
||||
token = b["board"].get("token", "")
|
||||
children.append(_text_block("(原文档含多维表格/看板,可在原链接中查看)"))
|
||||
# 其他类型可后续扩展
|
||||
content = "".join(el.get("text_run", {}).get("content", "") for el in els).strip()
|
||||
if not content:
|
||||
continue
|
||||
style = b["text"].get("style", {})
|
||||
el_style = (b["text"].get("elements") or [{}])[0].get("text_element_style", {})
|
||||
children.append({"block_type": 2, "text": {"elements": [{"text_run": {"content": content, "text_element_style": el_style}}], "style": style}})
|
||||
elif bt == 43 and (b.get("board") or b.get("bitable")):
|
||||
token = (b.get("board") or b.get("bitable") or {}).get("token", "")
|
||||
children.append({"_bitable_placeholder": True, "block_type": 43, "bitable": {"token": token}, "name": "流量来源"})
|
||||
elif bt not in (2, 43):
|
||||
api_block = _to_api_block(b)
|
||||
if api_block and not api_block.get("_bitable_placeholder"):
|
||||
children.append(api_block)
|
||||
|
||||
return title, children
|
||||
|
||||
|
||||
def resolve_bitable_placeholders(children: list, access_token: str, default_name: str = "多维表格") -> list:
|
||||
"""将 children 中的 _bitable_placeholder 替换为新建的多维表格块(block_type 43 + bitable.token)。"""
|
||||
out = []
|
||||
for i, c in enumerate(children):
|
||||
if isinstance(c, dict) and c.get("_bitable_placeholder") and c.get("block_type") == 43:
|
||||
name = c.get("name") or default_name
|
||||
app_token = create_bitable_app(access_token, name)
|
||||
if app_token:
|
||||
out.append({"block_type": 43, "bitable": {"token": app_token}})
|
||||
time.sleep(0.4)
|
||||
else:
|
||||
out.append({"block_type": 2, "text": {"elements": [{"text_run": {"content": "(多维表格创建失败,请手动插入)", "text_element_style": {}}}], "style": {}}})
|
||||
else:
|
||||
c_copy = {k: v for k, v in (c or {}).items() if not k.startswith("_")}
|
||||
if c_copy:
|
||||
out.append(c_copy)
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description="将飞书导出 JSON 上传为飞书 Wiki 文档")
|
||||
ap = argparse.ArgumentParser(description="将飞书导出 JSON 按原格式上传为飞书 Wiki 文档")
|
||||
ap.add_argument("json_path", help="JSON 文件路径(含 content + blocks)")
|
||||
ap.add_argument("--parent", default=DEFAULT_PARENT, help="Wiki 父节点 token")
|
||||
ap.add_argument("--title", default=None, help="覆盖文档标题(默认从 JSON 解析)")
|
||||
@@ -117,8 +167,15 @@ def main():
|
||||
if args.title:
|
||||
title = args.title
|
||||
|
||||
token = get_token(args.parent)
|
||||
if not token:
|
||||
print("❌ 无法获取飞书 Token")
|
||||
sys.exit(1)
|
||||
|
||||
children = resolve_bitable_placeholders(children, token, default_name="流量来源")
|
||||
|
||||
print("=" * 50)
|
||||
print(f"📤 上传为飞书文档:{title}")
|
||||
print(f"📤 上传为飞书文档(按原格式):{title}")
|
||||
print(f" 父节点: {args.parent}")
|
||||
print(f" 块数: {len(children)}")
|
||||
print("=" * 50)
|
||||
|
||||
@@ -93,34 +93,182 @@ def _font(size: int):
|
||||
return ImageFont.load_default()
|
||||
|
||||
|
||||
def _radial_gradient(img, cx, cy, r_inner, r_outer, c_center, c_edge):
|
||||
"""从中心到边缘的径向渐变"""
|
||||
from PIL import ImageDraw
|
||||
W, H = img.size
|
||||
pix = img.load()
|
||||
for y in range(H):
|
||||
for x in range(W):
|
||||
d = ((x - cx) ** 2 + (y - cy) ** 2) ** 0.5
|
||||
t = min(1.0, max(0.0, (d - r_inner) / (r_outer - r_inner))) if r_outer > r_inner else 0
|
||||
r = int(c_center[0] + (c_edge[0] - c_center[0]) * t)
|
||||
g = int(c_center[1] + (c_edge[1] - c_center[1]) * t)
|
||||
b = int(c_center[2] + (c_edge[2] - c_center[2]) * t)
|
||||
pix[x, y] = (max(0, min(255, r)), max(0, min(255, g)), max(0, min(255, b)))
|
||||
|
||||
|
||||
def _draw_speed_lines(draw, W, H, n=40, color=(255, 220, 230, 80)):
|
||||
"""漫画速度线(从一角放射)"""
|
||||
import random
|
||||
random.seed(42)
|
||||
for _ in range(n):
|
||||
x0, y0 = random.randint(0, W // 3), random.randint(0, H // 3)
|
||||
dx, dy = random.randint(W // 2, W), random.randint(H // 2, H)
|
||||
draw.line([(x0, y0), (x0 + dx, y0 + dy)], fill=(color[0], color[1], color[2]), width=2)
|
||||
|
||||
|
||||
def _draw_vignette(img, strength=0.4):
|
||||
"""四角暗角"""
|
||||
from PIL import ImageDraw
|
||||
W, H = img.size
|
||||
overlay = Image.new("RGBA", (W, H), (0, 0, 0, 0))
|
||||
draw = ImageDraw.Draw(overlay)
|
||||
for y in range(H):
|
||||
for x in range(W):
|
||||
nx, ny = 2 * x / W - 1, 2 * y / H - 1
|
||||
d = (nx * nx + ny * ny) ** 0.5
|
||||
a = int(255 * strength * min(1, d * 1.2))
|
||||
if a > 0:
|
||||
draw.point((x, y), fill=(0, 0, 0, a))
|
||||
out = Image.new("RGB", (W, H))
|
||||
out.paste(img, (0, 0))
|
||||
from PIL import Image
|
||||
out.paste(overlay, (0, 0), overlay)
|
||||
return out
|
||||
|
||||
|
||||
def generate_with_pil():
|
||||
"""用 PIL 绘制 5 张漫画风格占位图(渐变+标题+简单构图)"""
|
||||
"""用 PIL 绘制 5 张漫画风格插画(双人剪影+光效+速度线+层次)"""
|
||||
from PIL import Image, ImageDraw
|
||||
W, H = 1024, 1024
|
||||
OUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
titles = ["场景1 · 蒙眼拥抱", "场景2 · 侧抱", "场景3 · 额相抵", "后续1 · 温柔对视", "后续2 · 并肩"]
|
||||
colors = [
|
||||
((180, 100, 120), (80, 40, 60)),
|
||||
((120, 80, 140), (50, 30, 70)),
|
||||
((100, 120, 160), (40, 50, 80)),
|
||||
((140, 160, 180), (60, 70, 90)),
|
||||
((160, 140, 120), (70, 60, 50)),
|
||||
|
||||
# 场景配置:背景中心色、边缘色、剪影色、标题、副标题
|
||||
scenes = [
|
||||
{
|
||||
"center": (220, 160, 200),
|
||||
"edge": (60, 35, 55),
|
||||
"silhouette": (45, 30, 45),
|
||||
"title": "蒙眼拥抱",
|
||||
"sub": "—— 场景一",
|
||||
"speed_lines": True,
|
||||
},
|
||||
{
|
||||
"center": (200, 150, 220),
|
||||
"edge": (50, 40, 70),
|
||||
"silhouette": (40, 32, 55),
|
||||
"title": "侧抱",
|
||||
"sub": "—— 场景二",
|
||||
"speed_lines": True,
|
||||
},
|
||||
{
|
||||
"center": (210, 170, 230),
|
||||
"edge": (55, 45, 75),
|
||||
"silhouette": (42, 35, 58),
|
||||
"title": "额相抵",
|
||||
"sub": "—— 场景三",
|
||||
"speed_lines": True,
|
||||
},
|
||||
{
|
||||
"center": (230, 200, 220),
|
||||
"edge": (70, 60, 85),
|
||||
"silhouette": (50, 42, 62),
|
||||
"title": "温柔对视",
|
||||
"sub": "—— 后续一",
|
||||
"speed_lines": False,
|
||||
},
|
||||
{
|
||||
"center": (240, 210, 200),
|
||||
"edge": (75, 65, 80),
|
||||
"silhouette": (52, 45, 65),
|
||||
"title": "并肩",
|
||||
"sub": "—— 后续二",
|
||||
"speed_lines": False,
|
||||
},
|
||||
]
|
||||
for (filename, _), title, (c1, c2) in zip(PROMPTS, titles, colors):
|
||||
|
||||
for idx, ((filename, _), sc) in enumerate(zip(PROMPTS, scenes)):
|
||||
out_path = OUT_DIR / filename
|
||||
img = Image.new("RGB", (W, H))
|
||||
cx, cy = W // 2, H // 2
|
||||
_radial_gradient(img, cx, cy, 0, int((W * W + H * H) ** 0.5 * 0.6), sc["center"], sc["edge"])
|
||||
|
||||
# 中心柔光
|
||||
glow = Image.new("RGBA", (W, H), (0, 0, 0, 0))
|
||||
gdraw = ImageDraw.Draw(glow)
|
||||
for r in range(350, 100, -15):
|
||||
alpha = 25 if r > 200 else 15
|
||||
gdraw.ellipse([cx - r, cy - r, cx + r, cy + r], outline=(255, 245, 255, alpha), width=4)
|
||||
img.paste(glow, (0, 0), glow)
|
||||
|
||||
draw = ImageDraw.Draw(img)
|
||||
for y in range(H):
|
||||
t = y / (H - 1)
|
||||
r = int(c1[0] + (c2[0] - c1[0]) * t)
|
||||
g = int(c1[1] + (c2[1] - c1[1]) * t)
|
||||
b = int(c1[2] + (c2[2] - c1[2]) * t)
|
||||
draw.line([(0, y), (W, y)], fill=(r, g, b))
|
||||
font = _font(52)
|
||||
bbox = draw.textbbox((0, 0), title, font=font)
|
||||
tw = bbox[2] - bbox[0]
|
||||
draw.text(((W - tw) // 2, H // 2 - 40), title, font=font, fill=(255, 255, 255, 230))
|
||||
draw.rounded_rectangle([W//4, H//3, 3*W//4, 2*H//3], radius=24, outline=(255, 255, 255, 120), width=3)
|
||||
s = sc["silhouette"]
|
||||
|
||||
if idx == 0:
|
||||
# 场景1:正面拥抱(女前男后,女有长发+蒙眼带,男环抱)
|
||||
# 女性剪影:椭圆头+长发轮廓+身体
|
||||
draw.ellipse([cx - 85, cy - 220, cx + 85, cy - 70], fill=s, outline=(60, 45, 60))
|
||||
draw.ellipse([cx - 75, cy - 200, cx + 75, cy - 90], fill=(30, 22, 30)) # 蒙眼带区域
|
||||
draw.polygon([(cx - 95, cy - 80), (cx - 110, cy + 180), (cx - 60, cy + 200), (cx - 50, cy - 60)], fill=s)
|
||||
draw.polygon([(cx + 95, cy - 80), (cx + 110, cy + 180), (cx + 60, cy + 200), (cx + 50, cy - 60)], fill=s)
|
||||
draw.ellipse([cx - 70, cy - 30, cx + 70, cy + 120], fill=s)
|
||||
# 男性剪影:从后环抱
|
||||
draw.ellipse([cx - 100, cy - 180, cx + 100, cy - 30], fill=(s[0] + 15, s[1] + 12, s[2] + 15))
|
||||
draw.polygon([(cx - 120, cy - 50), (cx - 130, cy + 220), (cx + 130, cy + 220), (cx + 120, cy - 50)], fill=(s[0] + 15, s[1] + 12, s[2] + 15))
|
||||
elif idx == 1:
|
||||
# 场景2:侧抱(男在侧,一手环腰)
|
||||
draw.ellipse([cx - 90, cy - 200, cx + 90, cy - 50], fill=s)
|
||||
draw.ellipse([cx - 70, cy - 180, cx + 70, cy - 70], fill=(28, 20, 28))
|
||||
draw.polygon([(cx - 100, cy - 40), (cx - 115, cy + 200), (cx - 55, cy + 210), (cx - 45, cy - 50)], fill=s)
|
||||
draw.polygon([(cx + 100, cy - 40), (cx + 115, cy + 200), (cx + 55, cy + 210), (cx + 45, cy - 50)], fill=s)
|
||||
draw.ellipse([cx - 75, cy + 10, cx + 75, cy + 140], fill=s)
|
||||
# 男性侧影
|
||||
draw.ellipse([cx + 60, cy - 160, cx + 220, cy + 20], fill=(s[0] + 12, s[1] + 10, s[2] + 12))
|
||||
draw.polygon([(cx + 80, cy - 20), (cx + 100, cy + 220), (cx + 230, cy + 200), (cx + 200, cy - 40)], fill=(s[0] + 12, s[1] + 10, s[2] + 12))
|
||||
draw.ellipse([cx + 140, cy - 50, cx + 220, cy + 50], fill=(s[0] + 12, s[1] + 10, s[2] + 12))
|
||||
elif idx == 2:
|
||||
# 场景3:额相抵、抚发
|
||||
draw.ellipse([cx - 88, cy - 210, cx + 88, cy - 65], fill=s)
|
||||
draw.ellipse([cx - 68, cy - 190, cx + 68, cy - 85], fill=(28, 22, 30))
|
||||
draw.polygon([(cx - 98, cy - 55), (cx - 112, cy + 190), (cx - 52, cy + 200), (cx - 48, cy - 55)], fill=s)
|
||||
draw.polygon([(cx + 98, cy - 55), (cx + 112, cy + 190), (cx + 52, cy + 200), (cx + 48, cy - 55)], fill=s)
|
||||
draw.ellipse([cx - 72, cy - 10, cx + 72, cy + 115], fill=s)
|
||||
draw.ellipse([cx - 95, cy - 175, cx + 95, cy - 25], fill=(s[0] + 10, s[1] + 8, s[2] + 10))
|
||||
draw.polygon([(cx - 105, cy - 35), (cx - 118, cy + 210), (cx + 118, cy + 210), (cx + 105, cy - 35)], fill=(s[0] + 10, s[1] + 8, s[2] + 10))
|
||||
elif idx == 3:
|
||||
# 后续1:面对面温柔对视(无蒙眼,两人相对)
|
||||
draw.ellipse([cx - 200, cy - 180, cx - 20, cy + 20], fill=s) # 女左
|
||||
draw.polygon([(cx - 210, cy + 10), (cx - 230, cy + 220), (cx - 50, cy + 230), (cx - 30, cy + 30)], fill=s)
|
||||
draw.ellipse([cx - 180, cy - 30, cx - 40, cy + 100], fill=s)
|
||||
draw.ellipse([cx + 20, cy - 180, cx + 200, cy + 20], fill=(s[0] + 12, s[1] + 10, s[2] + 12)) # 男右
|
||||
draw.polygon([(cx + 30, cy + 10), (cx + 50, cy + 230), (cx + 230, cy + 220), (cx + 210, cy + 30)], fill=(s[0] + 12, s[1] + 10, s[2] + 12))
|
||||
draw.ellipse([cx + 40, cy - 30, cx + 180, cy + 100], fill=(s[0] + 12, s[1] + 10, s[2] + 12))
|
||||
else:
|
||||
# 后续2:并肩坐(两人并排,女靠肩)
|
||||
draw.ellipse([cx - 220, cy - 80, cx - 80, cy + 80], fill=s)
|
||||
draw.polygon([(cx - 230, cy + 50), (cx - 250, cy + 230), (cx - 120, cy + 240), (cx - 100, cy + 60)], fill=s)
|
||||
draw.ellipse([cx - 210, cy + 30, cx - 100, cy + 180], fill=s)
|
||||
draw.ellipse([cx + 60, cy - 100, cx + 200, cy + 60], fill=(s[0] + 10, s[1] + 8, s[2] + 12))
|
||||
draw.polygon([(cx + 90, cy + 40), (cx + 70, cy + 230), (cx + 220, cy + 220), (cx + 210, cy + 50)], fill=(s[0] + 10, s[1] + 8, s[2] + 12))
|
||||
draw.ellipse([cx + 100, cy + 50, cx + 200, cy + 170], fill=(s[0] + 10, s[1] + 8, s[2] + 12))
|
||||
|
||||
if sc.get("speed_lines"):
|
||||
_draw_speed_lines(draw, W, H, n=55, color=(255, 235, 245))
|
||||
|
||||
# 暗角
|
||||
img = _draw_vignette(img, strength=0.35)
|
||||
|
||||
draw = ImageDraw.Draw(img)
|
||||
font_t = _font(56)
|
||||
font_s = _font(28)
|
||||
bbox_t = draw.textbbox((0, 0), sc["title"], font=font_t)
|
||||
tw = bbox_t[2] - bbox_t[0]
|
||||
draw.text(((W - tw) // 2, H - 140), sc["title"], font=font_t, fill=(255, 252, 255))
|
||||
bbox_s = draw.textbbox((0, 0), sc["sub"], font=font_s)
|
||||
sw = bbox_s[2] - bbox_s[0]
|
||||
draw.text(((W - sw) // 2, H - 78), sc["sub"], font=font_s, fill=(220, 210, 225))
|
||||
|
||||
img.save(out_path, "PNG")
|
||||
print("OK:", out_path)
|
||||
return True
|
||||
|
||||
@@ -192,3 +192,4 @@
|
||||
| 2026-03-01 23:41:32 | 🔄 卡若AI 同步 2026-03-01 23:41 | 更新:水桥平台对接、卡木、运营中枢工作台 | 排除 >20MB: 14 个 |
|
||||
| 2026-03-02 00:14:40 | 🔄 卡若AI 同步 2026-03-02 00:14 | 更新:卡木、运营中枢工作台 | 排除 >20MB: 14 个 |
|
||||
| 2026-03-02 00:25:52 | 🔄 卡若AI 同步 2026-03-02 00:25 | 更新:水桥平台对接、运营中枢工作台 | 排除 >20MB: 14 个 |
|
||||
| 2026-03-02 02:30:31 | 🔄 卡若AI 同步 2026-03-02 02:30 | 更新:金仓、水桥平台对接、运营中枢工作台 | 排除 >20MB: 14 个 |
|
||||
|
||||
@@ -195,3 +195,4 @@
|
||||
| 2026-03-01 23:41:32 | 成功 | 成功 | 🔄 卡若AI 同步 2026-03-01 23:41 | 更新:水桥平台对接、卡木、运营中枢工作台 | 排除 >20MB: 14 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |
|
||||
| 2026-03-02 00:14:40 | 成功 | 成功 | 🔄 卡若AI 同步 2026-03-02 00:14 | 更新:卡木、运营中枢工作台 | 排除 >20MB: 14 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |
|
||||
| 2026-03-02 00:25:52 | 成功 | 成功 | 🔄 卡若AI 同步 2026-03-02 00:25 | 更新:水桥平台对接、运营中枢工作台 | 排除 >20MB: 14 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |
|
||||
| 2026-03-02 02:30:31 | 成功 | 成功 | 🔄 卡若AI 同步 2026-03-02 02:30 | 更新:金仓、水桥平台对接、运营中枢工作台 | 排除 >20MB: 14 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |
|
||||
|
||||
Reference in New Issue
Block a user