diff --git a/01_卡资(金)/金仓_存储备份/服务器管理/scripts/腾讯云_TAT_kr宝塔_Node全量启动修复.py b/01_卡资(金)/金仓_存储备份/服务器管理/scripts/腾讯云_TAT_kr宝塔_Node全量启动修复.py new file mode 100644 index 00000000..41b38ccb --- /dev/null +++ b/01_卡资(金)/金仓_存储备份/服务器管理/scripts/腾讯云_TAT_kr宝塔_Node全量启动修复.py @@ -0,0 +1,220 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +腾讯云 TAT + 宝塔 API:kr宝塔 Node 全量启动修复 +1. 修复 site.db 中所有 Node 项目的 project_script(MODULE_NOT_FOUND 根因:node /path 改为 cd /path && npm run start) +2. 停止全部 Node、清端口、批量启动 +3. 验证直到全部运行或超时 +凭证:00_账号与API索引.md 腾讯云 SecretId/SecretKey,宝塔 API 密钥 qcWubCdlfFjS2b2DMT1lzPFaDfmv1cBT +""" +import base64 +import json +import os +import re +import sys +import time + +KR_INSTANCE_ID = "ins-aw0tnqjo" +REGION = "ap-guangzhou" +BT_API_KEY = "qcWubCdlfFjS2b2DMT1lzPFaDfmv1cBT" + +SHELL_SCRIPT = r'''#!/bin/bash +echo "=== kr宝塔 Node 全量启动修复 ===" + +python3 - << 'PYMAIN' +import hashlib, json, os, re, sqlite3, subprocess, time, urllib.request, urllib.parse, ssl + +ssl._create_default_https_context = ssl._create_unverified_context +PANEL, K = "https://127.0.0.1:9988", "qcWubCdlfFjS2b2DMT1lzPFaDfmv1cBT" + +def sign(): + t = int(time.time()) + s = str(t) + hashlib.md5(K.encode()).hexdigest() + return {"request_time": t, "request_token": hashlib.md5(s.encode()).hexdigest()} + +def post(p, d=None): + pl = sign() + if d: pl.update(d) + r = urllib.request.Request(PANEL + p, data=urllib.parse.urlencode(pl).encode()) + with urllib.request.urlopen(r, timeout=25) as resp: + return json.loads(resp.read().decode()) + +def pids(port): + try: + o = subprocess.check_output("ss -tlnp 2>/dev/null | grep ':%s ' || true" % port, shell=True, universal_newlines=True) + return {int(x) for x in re.findall(r"pid=(\d+)", o)} + except: return set() + +def ports(it): + cfg = it.get("project_config") or {} + if isinstance(cfg, str): + try: cfg = json.loads(cfg) + except: cfg = {} + ps = [] + if cfg.get("port"): ps.append(int(cfg["port"])) + for m in re.findall(r"-p\s*(\d+)", str(cfg.get("project_script", ""))): ps.append(int(m)) + return sorted(set(ps)) + +# 【1】修复 site.db 启动命令 +print("\n【1】修复 site.db 启动命令") +db = "/www/server/panel/data/db/site.db" +fixed = 0 +if os.path.isfile(db): + conn = sqlite3.connect(db) + c = conn.cursor() + c.execute("SELECT id, name, path, project_config FROM sites WHERE project_type='Node'") + for row in c.fetchall(): + sid, name, path, cfg_str = row[0], row[1], row[2], row[3] or "{}" + path = (path or "").strip() + try: cfg = json.loads(cfg_str) if cfg_str else {} + except: cfg = {} + # 从 project_config 取 path 作为项目根目录 + proj_path = cfg.get("path") or cfg.get("project_path") or path + if not proj_path or not os.path.isdir(proj_path): + print(" 跳过 %s (路径不存在: %s)" % (name, proj_path)) + continue + old_script = str(cfg.get("project_script") or cfg.get("run_cmd") or "").strip() + cmd = "cd %s && (pnpm start 2>/dev/null || npm run start)" % proj_path + if "cd " not in old_script or proj_path not in old_script: + cfg["project_script"] = cmd + cfg["run_cmd"] = cmd + cfg["path"] = proj_path + c.execute("UPDATE sites SET path=?, project_config=? WHERE id=?", (proj_path, json.dumps(cfg, ensure_ascii=False), sid)) + fixed += 1 + print(" 已修复: %s -> %s" % (name, proj_path)) + conn.commit() + conn.close() +print(" 共修复 %d 个项目" % fixed) + +# 【2】停止全部 Node +print("\n【2】停止 Node 项目") +r0 = post("/project/nodejs/get_project_list") +items = r0.get("data") or r0.get("list") or [] +for it in items: + name = it.get("name") + if name: + try: + for port in ports(it): + for pid in pids(port): subprocess.call("kill -9 %s 2>/dev/null" % pid, shell=True) + pf = "/www/server/nodejs/vhost/pids/%s.pid" % name + if os.path.exists(pf): open(pf, "w").write("0") + post("/project/nodejs/stop_project", {"project_name": name}) + print(" 停: %s" % name) + except Exception as e: print(" 停 %s: %s" % (name, str(e)[:40])) + time.sleep(0.5) +time.sleep(3) + +# 【3】批量启动(最多3轮) +print("\n【3】批量启动 Node 项目") +for round_num in range(3): + r1 = post("/project/nodejs/get_project_list") + items = r1.get("data") or r1.get("list") or [] + to_start = [it for it in items if it.get("name") and not it.get("run")] + if not to_start: + print(" 全部已运行") + break + print(" 第%d轮: 待启动 %d 个" % (round_num + 1, len(to_start))) + for it in to_start: + name = it.get("name") + if not name: continue + try: + for port in ports(it): + for pid in pids(port): subprocess.call("kill -9 %s 2>/dev/null" % pid, shell=True) + pf = "/www/server/nodejs/vhost/pids/%s.pid" % name + if os.path.exists(pf): open(pf, "w").write("0") + post("/project/nodejs/stop_project", {"project_name": name}) + time.sleep(0.5) + r = post("/project/nodejs/start_project", {"project_name": name}) + ok = r.get("status") is True or "成功" in str(r.get("msg", "")) + print(" %s: %s" % (name, "OK" if ok else "FAIL")) + except Exception as e: print(" %s: ERR %s" % (name, str(e)[:30])) + time.sleep(1.5) + time.sleep(8) + +# 【4】最终状态 +print("\n【4】最终状态") +r2 = post("/project/nodejs/get_project_list") +items2 = r2.get("data") or r2.get("list") or [] +run_count = sum(1 for x in items2 if x.get("run")) +total = len(items2) +print(" 运行 %d / %d" % (run_count, total)) +for it in items2: + print(" %s: %s" % (it.get("name"), "运行中" if it.get("run") else "未启动")) +PYMAIN + +echo "" +echo "=== 完成 ===" +''' + +def _read_creds(): + d = os.path.dirname(os.path.abspath(__file__)) + for _ in range(6): + root = d + if os.path.isfile(os.path.join(root, "运营中枢", "工作台", "00_账号与API索引.md")): + with open(os.path.join(root, "运营中枢", "工作台", "00_账号与API索引.md"), "r", encoding="utf-8") as f: + text = f.read() + sid = skey = None + in_tx = False + for line in text.splitlines(): + if "### 腾讯云" in line or "腾讯云" in line and "Secret" in line: in_tx = True + if in_tx and "### " in line and "腾讯云" not in line: break + m = re.search(r"SecretId[^|]*\|\s*`([^`]+)`", line, re.I) + if m and "AKID" in str(m.group(1)): sid = m.group(1).strip() + m = re.search(r"SecretKey\s*\|\s*`([^`]+)`", line, re.I) + if m: skey = m.group(1).strip() + return sid or os.environ.get("TENCENTCLOUD_SECRET_ID"), skey or os.environ.get("TENCENTCLOUD_SECRET_KEY") + d = os.path.dirname(d) + return None, None + + +def main(): + sid, skey = _read_creds() + if not sid or not skey: + print("❌ 未配置腾讯云 SecretId/SecretKey"); return 1 + try: + from tencentcloud.common import credential + from tencentcloud.tat.v20201028 import tat_client, models + except ImportError: + print("请安装: pip install tencentcloud-sdk-python-tat"); return 1 + + cred = credential.Credential(sid, skey) + client = tat_client.TatClient(cred, REGION) + req = models.RunCommandRequest() + req.Content = base64.b64encode(SHELL_SCRIPT.encode("utf-8")).decode() + req.InstanceIds = [KR_INSTANCE_ID] + req.CommandType = "SHELL" + req.Timeout = 300 + req.CommandName = "kr宝塔_Node全量启动修复" + resp = client.RunCommand(req) + inv_id = resp.InvocationId + print("✅ TAT 已下发 InvocationId:", inv_id) + print(" 步骤: 修复 site.db 启动命令 → 停止 Node → 批量启动(3轮) → 验证") + print(" 等待 150s 后查询结果...") + time.sleep(150) + + try: + req2 = models.DescribeInvocationTasksRequest() + f = models.Filter() + f.Name, f.Values = "invocation-id", [inv_id] + req2.Filters = [f] + r2 = client.DescribeInvocationTasks(req2) + for t in (r2.InvocationTaskSet or []): + st = getattr(t, "TaskStatus", "") + print("\n状态:", st) + tr = getattr(t, "TaskResult", None) + if tr: + j = json.loads(tr) if isinstance(tr, str) else {} + out = j.get("Output", "") + if out: + try: + out = base64.b64decode(out).decode("utf-8", errors="replace") + except: + pass + print(out[:7000]) + except Exception as e: + print("查询失败:", e) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/03_卡木(木)/木叶_视频内容/视频切片/脚本/identify_highlights.py b/03_卡木(木)/木叶_视频内容/视频切片/脚本/identify_highlights.py index 6f3f379d..8f6429e7 100644 --- a/03_卡木(木)/木叶_视频内容/视频切片/脚本/identify_highlights.py +++ b/03_卡木(木)/木叶_视频内容/视频切片/脚本/identify_highlights.py @@ -14,9 +14,9 @@ from pathlib import Path OLLAMA_URL = "http://localhost:11434" DEFAULT_CTA = "关注我,每天学一招私域干货" -CLIP_COUNT = 8 -MIN_DURATION = 60 # 1 分钟起 -MAX_DURATION = 180 # 3 分钟 +CLIP_COUNT = 15 +MIN_DURATION = 60 # 最少 1 分钟 +MAX_DURATION = 300 # 最多 5 分钟 def parse_srt_segments(srt_path: str) -> list: @@ -42,22 +42,23 @@ def parse_srt_segments(srt_path: str) -> list: def fallback_highlights(transcript_path: str, clip_count: int) -> list: - """规则备用:按时长均匀切分,每段 60-180 秒""" + """规则备用:每段 60-300 秒(1-5 分钟)""" segments = parse_srt_segments(transcript_path) if not segments: return [] total = segments[-1]["end_sec"] if segments else 0 - interval = max(120, total / clip_count) # 每段约 2 分钟 + seg_dur = min(300, max(60, total / clip_count)) # 每段 1-5 分钟 result = [] for i in range(clip_count): - start_sec = int(interval * i + 30) - end_sec = min(int(start_sec + 120), int(total - 5)) # 约 2 分钟 - if end_sec <= start_sec + 30: + start_sec = int(i * seg_dur) + end_sec = min(int(start_sec + seg_dur), int(total - 5)) + if end_sec <= start_sec + 59: # 不足 1 分钟跳过 continue # 找该时间段内的字幕 texts = [s["text"] for s in segments if s["end_sec"] >= start_sec and s["start_sec"] <= end_sec] - excerpt = (texts[0][:50] + "..." if texts and len(texts[0]) > 50 else (texts[0] if texts else "")) - hook = (excerpt[:15] + "..." if len(excerpt) > 15 else excerpt) or f"精彩片段{i+1}" + joined = " ".join(texts)[:80] if texts else "" + excerpt = (joined + "..." if len(joined) > 50 else joined) or f"精彩片段{i+1}" + hook = (excerpt[:18] + "..." if len(excerpt) > 18 else excerpt) or f"精彩片段{i+1}" h, m, s = start_sec // 3600, (start_sec % 3600) // 60, start_sec % 60 eh, em, es = end_sec // 3600, (end_sec % 3600) // 60, end_sec % 60 result.append({ @@ -85,27 +86,66 @@ def srt_to_timestamped_text(srt_path: str) -> str: return "\n".join(lines) +def _sec_to_hhmmss(sec: float) -> str: + """秒数转为 HH:MM:SS""" + s = int(sec) + h, m = s // 3600, (s % 3600) // 60 + ss = s % 60 + return f"{h:02d}:{m:02d}:{ss:02d}" + + +def _parse_time_to_sec(t: str) -> float: + """解析 HH:MM:SS、HH:MM:SS.mmm、HH:MM:SS,mmm 为秒""" + t = str(t).strip().replace(",", ".") + parts = re.split(r"[:.]", t) + if len(parts) >= 3: + try: + return int(parts[0]) * 3600 + int(parts[1]) * 60 + float(parts[2]) + except (ValueError, TypeError): + pass + # 纯数字视为秒 + m = re.match(r"^(\d+(?:\.\d+)?)\s*$", t) + if m: + return float(m.group(1)) + return 0 + + +def _filter_short_clips(data: list) -> list: + """过滤掉时长 < 60 秒的切片""" + result = [] + for item in data: + if not isinstance(item, dict): + continue + st = item.get("start_time") or item.get("start") or "00:00:00" + et = item.get("end_time") or item.get("end") or "00:01:00" + dur = _parse_time_to_sec(et) - _parse_time_to_sec(st) + if dur >= 60: + result.append(item) + else: + print(f" 过滤短片段: {item.get('title','?')} (仅{dur:.0f}秒)", file=sys.stderr) + return result + + def _build_prompt(transcript: str, clip_count: int) -> str: - """构建高光识别 prompt(完整观点+干货,1-3分钟,全中文)""" - txt = transcript[:18000] if len(transcript) > 18000 else transcript - return f"""你是资深短视频策划师。请从视频文字稿中识别 {clip_count} 个**完整的核心观点/干货片段**。 + """构建高光识别 prompt(1-5分钟,主题与内容必须一致,全中文)""" + txt = transcript[:25000] if len(transcript) > 25000 else transcript + return f"""你是资深短视频策划师。请从视频文字稿中识别尽可能多的**完整干货片段**,目标 {clip_count} 个以上。 + +【时长要求】每个片段 **60-300 秒(1-5 分钟)**,少于 60 秒的不要输出。 +【主题一致】title、hook_3sec、transcript_excerpt 必须**完全对应**该时间段内的实际内容,禁止泛泛而谈。 + - title:从该段时间内的核心观点提炼,15字内 + - hook_3sec:从该段第一句或核心金句提炼,15字内 + - transcript_excerpt:该段内容的中文摘要,50字内 【切片原则】 -- 每个片段必须是**完整的一个话题/观点**,有头有尾,逻辑闭环,不能截断 -- 时长 **60-180 秒(1-3 分钟)**,尽量接近 2 分钟,确保内容完整 -- 优先选:金句、完整故事、可操作方法论、反常识观点、情绪高点、成体系讲解 +- 每段必须是完整话题,有头有尾 +- 优先:金句、故事、方法论、反常识、情绪高点 - 相邻片段间隔至少 60 秒 +- 从文字稿时间戳精确提取 start_time、end_time -【输出字段】所有内容**必须使用简体中文**,若原文是英文请翻译后填写: -- title: 核心观点标题(15字内,用于文件名) -- start_time: "HH:MM:SS"(从文字稿时间戳精确提取) -- end_time: "HH:MM:SS" -- hook_3sec: 封面 Hook 文案(15字内,吸引点击) -- cta_ending: "{DEFAULT_CTA}" -- transcript_excerpt: 本片段核心内容摘要(50字内,中文) -- reason: 推荐理由(中文) +【输出字段】全部**简体中文**,英文原文需翻译: +- title、start_time、end_time、hook_3sec、cta_ending(用"{DEFAULT_CTA}")、transcript_excerpt、reason -【强制】title、hook_3sec、transcript_excerpt、reason 必须全部简体中文,禁止英文。 只输出 JSON 数组,不要 ``` 或其他文字。 视频文字稿: @@ -233,8 +273,24 @@ def main(): data = fallback_highlights(str(transcript_path), args.clips) if not isinstance(data, list): data = [data] - # 强制中文:若 title/hook 含英文,翻译为中文 - print(" 确保导出名与封面为中文...") + # 过滤短于 1 分钟的切片 + data = _filter_short_clips(data) + # 统一 start_time/end_time 为 HH:MM:SS(兼容 Ollama 返回秒数) + for item in data: + if not isinstance(item, dict): + continue + st = item.get("start_time") or item.get("start") + if isinstance(st, (int, float)): + item["start_time"] = _sec_to_hhmmss(st) + et = item.get("end_time") or item.get("end") + if isinstance(et, (int, float)): + item["end_time"] = _sec_to_hhmmss(et) + # 若 AI 返回的片段全被过滤,用规则备用 + if not data and transcript_path.exists(): + print(" AI 片段时长无效,改用规则切分(1-5 分钟)", file=sys.stderr) + data = fallback_highlights(str(transcript_path), args.clips) + # 强制中文 + print(" 确保导出名与封面为简体中文...") data = _ensure_chinese_highlights(data) out_path = Path(args.output) out_path.parent.mkdir(parents=True, exist_ok=True) diff --git a/03_卡木(木)/木叶_视频内容/视频切片/脚本/soul_enhance.py b/03_卡木(木)/木叶_视频内容/视频切片/脚本/soul_enhance.py index 92b2729d..d2c352f1 100644 --- a/03_卡木(木)/木叶_视频内容/视频切片/脚本/soul_enhance.py +++ b/03_卡木(木)/木叶_视频内容/视频切片/脚本/soul_enhance.py @@ -594,8 +594,14 @@ def enhance_clip(clip_path, output_path, highlight_info, temp_dir, transcript_pa print(f" ⊘ 跳过字幕烧录(检测到原片已有字幕/图片)") else: start_time = highlight_info.get('start_time', '00:00:00') - parts = start_time.split(':') - start_sec = int(parts[0]) * 3600 + int(parts[1]) * 60 + float(parts[2]) + try: + if isinstance(start_time, (int, float)): + start_sec = float(start_time) + else: + parts = str(start_time).strip().split(':') + start_sec = int(parts[0]) * 3600 + int(parts[1]) * 60 + float(parts[2]) + except (IndexError, ValueError): + start_sec = 0 end_sec = start_sec + duration subtitles = parse_srt_for_clip(transcript_path, start_sec, end_sec) for sub in subtitles: @@ -606,7 +612,8 @@ def enhance_clip(clip_path, output_path, highlight_info, temp_dir, transcript_pa img_path = os.path.join(temp_dir, f'sub_{i:04d}.png') create_subtitle_image(sub['text'], width, height, img_path) sub_images.append({'path': img_path, 'start': sub['start'], 'end': sub['end']}) - print(f" ✓ 字幕图片 ({len(sub_images)}张)") + if sub_images: + print(f" ✓ 字幕图片 ({len(sub_images)}张)") # 4. 检测静音 silences = detect_silence(clip_path, SILENCE_THRESHOLD, SILENCE_MIN_DURATION) diff --git a/03_卡木(木)/木叶_视频内容/视频切片/脚本/soul_slice_pipeline.py b/03_卡木(木)/木叶_视频内容/视频切片/脚本/soul_slice_pipeline.py index e2cb2fd6..e3315861 100644 --- a/03_卡木(木)/木叶_视频内容/视频切片/脚本/soul_slice_pipeline.py +++ b/03_卡木(木)/木叶_视频内容/视频切片/脚本/soul_slice_pipeline.py @@ -93,6 +93,8 @@ def main(): parser.add_argument("--skip-highlights", action="store_true", help="跳过高光识别(已有 highlights.json)") parser.add_argument("--skip-clips", action="store_true", help="跳过切片(已有 clips/,仅重新增强)") parser.add_argument("--language", "-l", default="zh", choices=["zh", "en"], help="转录语言(纳瓦尔访谈等英文内容用 en)") + parser.add_argument("--skip-subs", action="store_true", help="跳过字幕烧录(原片已有字幕时用)") + parser.add_argument("--force-burn-subs", action="store_true", help="强制烧录字幕(忽略检测)") args = parser.parse_args() video_path = Path(args.video).resolve() @@ -205,19 +207,19 @@ def main(): # 4. 增强(封面+字幕+加速):soul_enhance(Pillow,无需 drawtext) enhanced_dir.mkdir(parents=True, exist_ok=True) - ok = run( - [ - sys.executable, - str(SCRIPT_DIR / "soul_enhance.py"), - "--clips", str(clips_dir), - "--highlights", str(highlights_path), - "--transcript", str(transcript_path), - "--output", str(enhanced_dir), - ], - "增强处理(封面+字幕+加速)", - timeout=900, - check=False, - ) + enhance_cmd = [ + sys.executable, + str(SCRIPT_DIR / "soul_enhance.py"), + "--clips", str(clips_dir), + "--highlights", str(highlights_path), + "--transcript", str(transcript_path), + "--output", str(enhanced_dir), + ] + if getattr(args, "skip_subs", False): + enhance_cmd.append("--skip-subs") + if getattr(args, "force_burn_subs", False): + enhance_cmd.append("--force-burn-subs") + ok = run(enhance_cmd, "增强处理(封面+字幕+加速)", timeout=900, check=False) import shutil enhanced_count = len(list(enhanced_dir.glob("*.mp4"))) if enhanced_count == 0 and clips_list: diff --git a/_执行日志/2026-02_Soul视频切片_复盘.md b/_执行日志/2026-02_Soul视频切片_复盘.md index 0d35146e..0d07bd3b 100644 --- a/_执行日志/2026-02_Soul视频切片_复盘.md +++ b/_执行日志/2026-02_Soul视频切片_复盘.md @@ -62,6 +62,22 @@ bash "/Users/karuo/Movies/soul视频/soul 派对 106场 20260221_output/热点 --- +## [卡若复盘] Soul 106 单目录输出+竖屏居中(2026-02-22) + +**🎯 目标·结果** +目标:成片统一输出到单目录、竖屏完全居中、挤掉右侧空白、有封面+字幕、同名覆盖。结果:已调整脚本与增强逻辑。 + +**📌 修改内容** +1. **单目录输出**:临时目录 `.tmp_slice` 只做中间处理,最终成片全部落在 `output` 根目录,不再分子目录。 +2. **同名覆盖**:soul_enhance 不再预先清空 mp4,直接写入覆盖;ffmpeg 使用 `-y` 覆盖。 +3. **竖屏裁剪**:`crop=1080:1920:0:(ih-1920)/2`,取左侧 1080 列并垂直居中,挤掉右侧空白,内容偏左显示(符合「往右挤掉右侧空白」)。 +4. **封面与字幕**:沿用 soul_enhance 的 Pillow 封面 + 烧录字幕逻辑。 + +**📁 输出路径** +`/Users/karuo/Movies/soul视频/soul 派对 106场 20260221_output/*.mp4`(如 soul106_01_xxx_enhanced.mp4) + +--- + ## [卡若复盘] 文档与字幕简体中文优化(2026-02) **🎯 目标·结果·达成率** diff --git a/运营中枢/工作台/gitea_push_log.md b/运营中枢/工作台/gitea_push_log.md index 9d101230..738fb7f6 100644 --- a/运营中枢/工作台/gitea_push_log.md +++ b/运营中枢/工作台/gitea_push_log.md @@ -89,3 +89,4 @@ | 2026-02-22 12:42:56 | 🔄 卡若AI 同步 2026-02-22 12:42 | 更新:金仓、卡木、运营中枢工作台 | 排除 >20MB: 8 个 | | 2026-02-22 13:08:21 | 🔄 卡若AI 同步 2026-02-22 13:08 | 更新:卡木、总索引与入口、运营中枢工作台 | 排除 >20MB: 8 个 | | 2026-02-22 13:45:50 | 🔄 卡若AI 同步 2026-02-22 13:45 | 更新:金仓、卡木、运营中枢工作台 | 排除 >20MB: 8 个 | +| 2026-02-22 13:57:34 | 🔄 卡若AI 同步 2026-02-22 13:57 | 更新:金仓、水溪整理归档、卡木、运营中枢工作台 | 排除 >20MB: 8 个 | diff --git a/运营中枢/工作台/代码管理.md b/运营中枢/工作台/代码管理.md index 01050453..e24c4b80 100644 --- a/运营中枢/工作台/代码管理.md +++ b/运营中枢/工作台/代码管理.md @@ -92,3 +92,4 @@ | 2026-02-22 12:42:56 | 成功 | 成功 | 🔄 卡若AI 同步 2026-02-22 12:42 | 更新:金仓、卡木、运营中枢工作台 | 排除 >20MB: 8 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) | | 2026-02-22 13:08:21 | 成功 | 成功 | 🔄 卡若AI 同步 2026-02-22 13:08 | 更新:卡木、总索引与入口、运营中枢工作台 | 排除 >20MB: 8 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) | | 2026-02-22 13:45:50 | 成功 | 成功 | 🔄 卡若AI 同步 2026-02-22 13:45 | 更新:金仓、卡木、运营中枢工作台 | 排除 >20MB: 8 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) | +| 2026-02-22 13:57:34 | 成功 | 成功 | 🔄 卡若AI 同步 2026-02-22 13:57 | 更新:金仓、水溪整理归档、卡木、运营中枢工作台 | 排除 >20MB: 8 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |