🔄 卡若AI 同步 2026-02-22 13:57 | 更新:金仓、水溪整理归档、卡木、运营中枢工作台 | 排除 >20MB: 8 个

This commit is contained in:
2026-02-22 13:57:31 +08:00
parent 1e99b78a3e
commit 31794af87f
11 changed files with 713 additions and 74 deletions

View File

@@ -140,11 +140,12 @@ sshpass -p 'zhiqun1984' ssh -p 22022 -o StrictHostKeyChecking=no ckb@43.139.27.9
SSH 风控时,在 **kr宝塔 宝塔面板 → 终端** 上传脚本后执行。详见 `references/宝塔Node项目管理_SKILL.md`
**kr宝塔 中文路径 + MODULE_NOT_FOUND 全量修复**(符号链接、修正启动命令、批量重启
**kr宝塔 中文目录改英文迁移**(删映射、重命名为英文、更新 site.db/Nginx、只用宝塔 Nginx、启动 Node
```bash
./scripts/.venv_tx/bin/python scripts/腾讯云_TAT_kr宝塔_中文路径与MODULE修复.py
# 宝塔终端执行(推荐):上传 scripts/kr宝塔_中文目录改英文_宝塔终端执行.sh 后 bash 执行
# 或 TAT./scripts/.venv_tx/bin/python scripts/腾讯云_TAT_kr宝塔_中文目录改英文迁移.py
```
脚本会:① 创建 ext→扩展、client→客户、self→自营 符号链接;② 修正 site.db 中 `node /path` 错误启动命令为 `cd /path && npm run start`;③ pnpm install④ 批量重启全部 Node 项目
迁移后路径:`/www/wwwroot/self/``/www/wwwroot/client/``/www/wwwroot/ext/`(不再使用 自营/客户/扩展)
### 4a. www.lytiao.com Docker 化(存客宝 · 可多服务器复用)
@@ -382,7 +383,8 @@ ss -tlnp | grep :端口号
| 脚本 | 功能 | 位置 |
|------|------|------|
| `腾讯云_TAT_kr宝塔_中文路径与MODULE修复.py` | kr宝塔 符号链接+修正启动命令+批量重启TAT | `./scripts/.venv_tx` |
| `腾讯云_TAT_kr宝塔_中文目录改英文迁移.py` | kr宝塔 中文目录改英文、删映射、更新 site.db/NginxTAT | `./scripts/.venv_tx` |
| `kr宝塔_中文目录改英文_宝塔终端执行.sh` | 同上,宝塔终端手动执行 | `./scripts/` |
| `腾讯云_TAT_word_ai_hair_is_phone_诊断修复.py` | word/ai_hair/is_phone 日志诊断、MODULE_NOT_FOUND 修复、重启(宝塔 API | `./scripts/` |
| `kr宝塔_node项目批量修复.py` | 批量启动 kr宝塔 Node 项目(服务器内执行,宝塔 API | `./scripts/` |
| `kr宝塔_宝塔API_修复502.py` | 修复 502重启 Nginx + soul 相关 Node | `./scripts/` |

View File

@@ -4,6 +4,20 @@
---
## 〇、中文目录改英文迁移(推荐)
**彻底消除中文路径**:删除映射、重命名目录、更新 site.db 与 Nginx、只用宝塔 Nginx、启动 Node。
```bash
# 方式 1宝塔终端推荐
# 打开 https://43.139.27.93:9988 → 终端 → 上传 scripts/kr宝塔_中文目录改英文_宝塔终端执行.sh → 执行 bash kr宝塔_中文目录改英文_宝塔终端执行.sh
# 方式 2TAT
./scripts/.venv_tx/bin/python scripts/腾讯云_TAT_kr宝塔_中文目录改英文迁移.py
```
迁移后路径:`/www/wwwroot/self/``/www/wwwroot/client/``/www/wwwroot/ext/`;子目录 `wanzhi``tools`
---
## 一、批量启动(先执行)
**宝塔面板 → 终端** 执行 `references/宝塔面板终端_Node批量启动指南.md` 中的脚本,先尝试批量启动所有未运行项目。
@@ -26,16 +40,16 @@ Error: Cannot find module '/www/wwwroot/自营/wzdj'
| 项目 | 根目录 | 建议启动命令 | 说明 |
|------|--------|--------------|------|
| **玩值大屏** | /www/wwwroot/自营/玩值/玩值大屏 | `cd /www/wwwroot/自营/玩值/玩值大屏 && node server.js``npm run start` | 先确认目录内有 server.js / package.json 的 scripts.start |
| **wzdj** | /www/wwwroot/自营/wzdj | `cd /www/wwwroot/自营/wzdj && node server.js``npm run start` | 同上 |
| **tongzhi** | /www/wwwroot/自营/玩值/tongzhi | `cd /www/wwwroot/自营/玩值/tongzhi && node server.js``npm run start` | 同上 |
| **is_phone** | /www/wwwroot/自营/kr/kr-phone | `cd /www/wwwroot/自营/kr/kr-phone && node server.js``npm run start` | 同上 |
| **ai_hair** | /www/wwwroot/客户/ai_hair | 同上 | 同上 |
| **word** | /www/wwwroot/自营/word 或 扩展/word | `cd 项目根目录 && npm run start` | Next.js按实际路径 |
| **AITOUFA** | /www/wwwroot/扩展/小工具/AITOUFA | `cd /www/wwwroot/扩展/小工具/AITOUFA && npm run start` | 参考 Skill §4.6 |
| **zhiji** | /www/wwwroot/... | 同上 | 按实际结构 |
| **ymao** | /www/wwwroot/扩展/ymao | 同上 | 同上 |
| **zhaoping** | /www/wwwroot/客户/zhaoping | 同上 | 同上 |
| **玩值大屏** | /www/wwwroot/self/wanzhi/玩值大屏 | `cd /www/wwwroot/self/wanzhi/玩值大屏 && (pnpm start || npm run start)` | 迁移后使用英文路径 |
| **wzdj** | /www/wwwroot/self/wzdj | 同上 | 同上 |
| **tongzhi** | /www/wwwroot/self/wanzhi/tongzhi | 同上 | 同上 |
| **is_phone** | /www/wwwroot/self/kr/kr-phone | 同上 | 同上 |
| **ai_hair** | /www/wwwroot/client/ai_hair | 同上 | 同上 |
| **word** | /www/wwwroot/self/word | 同上 | 同上 |
| **AITOUFA** | /www/wwwroot/ext/tools/AITOUFA | 同上 | 同上 |
| **zhiji** | /www/wwwroot/self/zhiji | 同上 | 同上 |
| **ymao** | /www/wwwroot/ext/ymao | 同上 | 同上 |
| **zhaoping** | /www/wwwroot/client/zhaoping | 同上 | 同上 |
---
@@ -44,6 +58,7 @@ Error: Cannot find module '/www/wwwroot/自营/wzdj'
1. **Next.js**`cd 项目根目录 && npm run start``pnpm start`;若用 standalone`node .next/standalone/server.js`
2. **Express / 普通 Node**`cd 项目根目录 && node server.js``node index.js`
3. **禁止**`node /www/wwwroot/xxx/项目名`(目录不能当入口)
4. **路径规范**:使用英文路径 `/www/wwwroot/self/``/www/wwwroot/client/``/www/wwwroot/ext/`,不再使用 自营/客户/扩展。
---
@@ -62,7 +77,7 @@ Error: Cannot find module '/www/wwwroot/自营/wzdj'
在宝塔终端执行,确认各项目有入口文件:
```bash
for d in /www/wwwroot/自营/玩值/玩值大屏 /www/wwwroot/自营/wzdj /www/wwwroot/自营/玩值/tongzhi /www/wwwroot/自营/kr/kr-phone; do
for d in /www/wwwroot/self/wanzhi/玩值大屏 /www/wwwroot/self/wzdj /www/wwwroot/self/wanzhi/tongzhi /www/wwwroot/self/kr/kr-phone; do
echo "=== $d ==="
ls -la "$d/" 2>/dev/null | grep -E "server\.js|index\.js|package\.json|\.next" || echo " (无常见入口)"
done

View File

@@ -179,12 +179,12 @@ python3 "01_卡资/金仓_存储备份/服务器管理/scripts/kr宝塔
### 4.6 MODULE_NOT_FOUND如 AITOUFA
**现象**`Error: Cannot find module '/www/wwwroot/扩展/小工具/AITOUFA'`Node 把项目根目录当入口执行。
**现象**`Error: Cannot find module '/www/wwwroot/ext/tools/AITOUFA'`(迁移前为 扩展/小工具)Node 把项目根目录当入口执行。
**原因**:启动命令配置错误,例如写成 `node /项目根目录` 而非 `node server.js``npm start`
**处理**:宝塔 **Node 项目 → 编辑该项目**,将启动命令改为:
- Next.js`cd /www/wwwroot/扩展/小工具/AITOUFA && npm run start``pnpm start`
- Next.js`cd /www/wwwroot/ext/tools/AITOUFA && npm run start``pnpm start`
- 或正确的入口:`node server.js` / `node index.js`(在项目根目录执行)
### 4.7 宝塔与 PM2 冲突

View File

@@ -0,0 +1,171 @@
#!/bin/bash
# kr宝塔 中文目录改英文迁移 - 在宝塔面板「终端」中执行
# 1. 删映射 2. 重命名目录 3. 更新 site.db 4. 更新 Nginx 5. 只用宝塔 Nginx 6. 启动 Node
echo "=== kr宝塔 中文目录改英文迁移 ==="
# 1. 停止 Node面板先到 Node 项目 手动停止,或下方 API
echo ""
echo "【1】停止 Node"
python3 -c '
import hashlib,json,time,urllib.request,urllib.parse,ssl
ssl._create_default_https_context=ssl._create_unverified_context
K="qcWubCdlfFjS2b2DMT1lzPFaDfmv1cBT"
t=int(time.time())
tk=hashlib.md5((str(t)+hashlib.md5(K.encode()).hexdigest()).encode()).hexdigest()
def post(p,d=None):
r=urllib.request.Request("https://127.0.0.1:9988"+p,data=urllib.parse.urlencode({"request_time":t,"request_token":tk,**({}if d is None else d)}).encode())
return json.loads(urllib.request.urlopen(r,timeout=15).read().decode())
for it in post("/project/nodejs/get_project_list").get("data")or post("/project/nodejs/get_project_list").get("list")or[]:
n=it.get("name")
if n:
try: post("/project/nodejs/stop_project",{"project_name":n});print(" stop:",n)
except: pass
time.sleep(0.3)
'
sleep 3
# 2. 删除符号链接
echo ""
echo "【2】删除符号链接"
cd /www/wwwroot
for x in ext client self archive test; do [ -L "$x" ] && rm -f "$x" && echo " rm $x"; done
# 3. 重命名
echo ""
echo "【3】重命名目录"
cd /www/wwwroot
([ -d "扩展" ] && [ ! -e "ext" ] && mv 扩展 ext && echo " 扩展->ext") || true
([ -d "客户" ] && [ ! -e "client" ] && mv 客户 client && echo " 客户->client") || true
([ -d "自营" ] && [ ! -e "self" ] && mv 自营 self && echo " 自营->self") || true
([ -d "self/玩值" ] && [ ! -e "self/wanzhi" ] && mv self/玩值 self/wanzhi && echo " 玩值->wanzhi") || true
([ -d "ext/小工具" ] && [ ! -e "ext/tools" ] && mv ext/小工具 ext/tools && echo " 小工具->tools") || true
([ -d "归档" ] && [ ! -e "archive" ] && mv 归档 archive && echo " 归档->archive") || true
([ -d "测试" ] && [ ! -e "test" ] && mv 测试 test && echo " 测试->test") || true
# 4. 更新 site.db
echo ""
echo "【4】更新 site.db"
python3 << 'PYDB'
import json,os,sqlite3
R=[("/www/wwwroot/自营/玩值/","/www/wwwroot/self/wanzhi/"),("/www/wwwroot/自营/","/www/wwwroot/self/"),("/www/wwwroot/扩展/小工具/","/www/wwwroot/ext/tools/"),("/www/wwwroot/扩展/","/www/wwwroot/ext/"),("/www/wwwroot/客户/","/www/wwwroot/client/"),("/www/wwwroot/归档/","/www/wwwroot/archive/"),("/www/wwwroot/测试/","/www/wwwroot/test/")]
def rp(s):
if not s: return s
for a,b in R: s=s.replace(a,b)
return s
def ro(o):
if isinstance(o,dict): return {k:ro(v) for k,v in o.items()}
if isinstance(o,list): return [ro(x) for x in o]
if isinstance(o,str) and "/www/wwwroot/" in o: return rp(o)
return o
db="/www/server/panel/data/db/site.db"
if os.path.isfile(db):
c=sqlite3.connect(db)
cur=c.cursor()
cur.execute("SELECT id,path,project_config FROM sites")
n=0
for row in cur.fetchall():
sid,path,cfg=row[0],row[1]or"",row[2]or"{}"
np=rp(path)
try: nc=json.dumps(ro(json.loads(cfg)),ensure_ascii=False)
except: nc=rp(cfg)
if np!=path or nc!=cfg: cur.execute("UPDATE sites SET path=?,project_config=? WHERE id=?",(np,nc,sid)); n+=1
c.commit();c.close()
print(" 更新%d条"%n)
PYDB
# 5. 更新 Nginx
echo ""
echo "【5】更新 Nginx 配置"
for d in /www/server/panel/vhost/nginx /www/server/nginx/conf/vhost; do
[ -d "$d" ] || continue
for f in "$d"/*.conf; do
[ -f "$f" ] || continue
if grep -qE '自营|扩展|客户|玩值|小工具|归档|测试' "$f" 2>/dev/null; then
sed -i 's|/www/wwwroot/自营/玩值/|/www/wwwroot/self/wanzhi/|g; s|/www/wwwroot/自营/|/www/wwwroot/self/|g; s|/www/wwwroot/扩展/小工具/|/www/wwwroot/ext/tools/|g; s|/www/wwwroot/扩展/|/www/wwwroot/ext/|g; s|/www/wwwroot/客户/|/www/wwwroot/client/|g; s|/www/wwwroot/归档/|/www/wwwroot/archive/|g; s|/www/wwwroot/测试/|/www/wwwroot/test/|g' "$f"
echo " 更新: $f"
fi
done
done
# 6. 只用宝塔 Nginx
echo ""
echo "【6】Nginx 只用宝塔版"
killall nginx 2>/dev/null || true
sleep 2
/www/server/nginx/sbin/nginx -c /www/server/nginx/conf/nginx.conf 2>/dev/null
sleep 1
nginx -t 2>/dev/null && nginx -s reload 2>/dev/null
echo " 宝塔 Nginx 已启动"
# 7. 更新 Node 启动命令
echo ""
echo "【7】更新 Node 启动命令"
python3 << 'PYNC'
import json,sqlite3
P={"玩值大屏":"/www/wwwroot/self/wanzhi/玩值大屏","tongzhi":"/www/wwwroot/self/wanzhi/tongzhi","is_phone":"/www/wwwroot/self/kr/kr-phone","ai_hair":"/www/wwwroot/client/ai_hair","AITOUFA":"/www/wwwroot/ext/tools/AITOUFA","wzdj":"/www/wwwroot/self/wzdj","zhiji":"/www/wwwroot/self/zhiji","ymao":"/www/wwwroot/ext/ymao","zhaoping":"/www/wwwroot/client/zhaoping","神射手":"/www/wwwroot/self/kr/kr-use","word":"/www/wwwroot/self/word"}
db="/www/server/panel/data/db/site.db"
if __import__("os").path.isfile(db):
c=sqlite3.connect(db)
cur=c.cursor()
cur.execute("SELECT id,name,project_config FROM sites WHERE project_type='Node'")
for row in cur.fetchall():
sid,name,cfg=row[0],row[1],row[2]or"{}"
path=P.get(name)
if path:
try: j=json.loads(cfg)
except: j={}
cmd="cd %s && (pnpm start 2>/dev/null || npm run start)"%path
j["project_script"]=j["run_cmd"]=cmd
cur.execute("UPDATE sites SET project_config=? WHERE id=?",(json.dumps(j,ensure_ascii=False),sid))
print(" ",name)
c.commit();c.close()
PYNC
# 8. 启动 Node
echo ""
echo "【8】启动 Node 项目"
python3 -c '
import hashlib,json,time,urllib.request,urllib.parse,ssl,subprocess,re,os
ssl._create_default_https_context=ssl._create_unverified_context
K="qcWubCdlfFjS2b2DMT1lzPFaDfmv1cBT"
def sign():
t=int(time.time())
return {"request_time":t,"request_token":hashlib.md5((str(t)+hashlib.md5(K.encode()).hexdigest()).encode()).hexdigest()}
def post(p,d=None):
pl=sign()
if d: pl.update(d)
r=urllib.request.Request("https://127.0.0.1:9988"+p,data=urllib.parse.urlencode(pl).encode())
return json.loads(urllib.request.urlopen(r,timeout=30).read().decode())
def pids(port):
try: return {int(x) for x in re.findall(r"pid=(\d+)",subprocess.check_output("ss -tlnp 2>/dev/null | grep \":%s \" || true"%port,shell=True).decode())}
except: return set()
def ports(it):
cfg=it.get("project_config") or {}
if isinstance(cfg,str): cfg=json.loads(cfg) if cfg else {}
p=[]
if cfg.get("port"): p.append(int(cfg["port"]))
p.extend(int(m) for m in re.findall(r"-p\s*(\d+)",str(cfg.get("project_script",""))))
return p
items=post("/project/nodejs/get_project_list").get("data")or post("/project/nodejs/get_project_list").get("list")or[]
for it in items:
n=it.get("name")
if not n: 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"%n
if os.path.exists(pf): open(pf,"w").write("0")
post("/project/nodejs/stop_project",{"project_name":n})
time.sleep(0.5)
r=post("/project/nodejs/start_project",{"project_name":n})
print(" %s: %s"%(n,"OK" if r.get("status") or "成功" in str(r.get("msg","")) else "FAIL"))
except: print(" %s: ERR"%n)
time.sleep(1)
time.sleep(5)
r2=post("/project/nodejs/get_project_list")
items2=r2.get("data")or r2.get("list")or[]
print(" 运行 %d/%d"%(sum(1 for x in items2 if x.get("run")),len(items2)))
'
echo ""
echo "=== 完成 ==="

View File

@@ -0,0 +1,363 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
腾讯云 TATkr宝塔 中文目录改英文迁移
1. 停止全部 Node 项目
2. 删除 /www/wwwroot 下所有符号链接ext->扩展、client->客户、self->自营 等)
3. 重命名中文目录为英文扩展→ext、客户→client、自营→self、玩值→wanzhi、小工具→tools
4. 更新 site.db 中所有 path、project_config 路径
5. 更新 Nginx vhost 配置中所有中文路径
6. 强制只用宝塔 Nginxkillall nginx 后启动宝塔版)
7. 批量启动全部 Node 项目
"""
import base64
import json
import os
import re
import sys
import time
KR_INSTANCE_ID = "ins-aw0tnqjo"
REGION = "ap-guangzhou"
# 中文路径 → 英文路径(用于 site.db、nginx 批量替换)
PATH_REPLACES = [
("/www/wwwroot/自营/玩值/", "/www/wwwroot/self/wanzhi/"),
("/www/wwwroot/自营/", "/www/wwwroot/self/"),
("/www/wwwroot/扩展/小工具/", "/www/wwwroot/ext/tools/"),
("/www/wwwroot/扩展/", "/www/wwwroot/ext/"),
("/www/wwwroot/客户/", "/www/wwwroot/client/"),
("/www/wwwroot/归档/", "/www/wwwroot/archive/"),
("/www/wwwroot/测试/", "/www/wwwroot/test/"),
]
# 替换顺序:先替换更长的路径,避免 /自营/ 误替换 /自营/玩值/ 的前缀
SHELL_SCRIPT = r'''#!/bin/bash
echo "=== kr宝塔 中文目录改英文迁移 ==="
# 0. 宝塔面板
echo ""
echo "【0】宝塔面板"
if ! ss -tlnp 2>/dev/null | grep -q ':9988 '; then
/etc/init.d/bt start 2>/dev/null || /www/server/panel/bt start 2>/dev/null || true
sleep 5
fi
# 1. 停止全部 Node 项目
echo ""
echo "【1】停止 Node 项目"
python3 - << 'PY1'
import hashlib, json, 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())
return {"request_time": t, "request_token": __import__("hashlib").md5((str(t) + __import__("hashlib").md5(K.encode()).hexdigest()).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=30) as resp:
return json.loads(resp.read().decode())
items = post("/project/nodejs/get_project_list").get("data") or post("/project/nodejs/get_project_list").get("list") or []
for it in items:
name = it.get("name")
if name:
try: post("/project/nodejs/stop_project", {"project_name": name}); print(" 停:", name)
except: pass
time.sleep(0.3)
PY1
sleep 3
# 2. 删除符号链接
echo ""
echo "【2】删除符号链接"
cd /www/wwwroot
for x in ext client self archive test; do
if [ -L "$x" ]; then
rm -f "$x" && echo " 删除链接: $x"
fi
done
# 3. 重命名中文目录为英文(按依赖顺序)
echo ""
echo "【3】重命名目录"
cd /www/wwwroot
([ -d "扩展" ] && [ ! -e "ext" ] && mv "扩展" "ext" && echo " 扩展 -> ext") || true
([ -d "客户" ] && [ ! -e "client" ] && mv "客户" "client" && echo " 客户 -> client") || true
([ -d "自营" ] && [ ! -e "self" ] && mv "自营" "self" && echo " 自营 -> self") || true
([ -d "self/玩值" ] && [ ! -e "self/wanzhi" ] && mv "self/玩值" "self/wanzhi" && echo " 玩值 -> wanzhi") || true
([ -d "ext/小工具" ] && [ ! -e "ext/tools" ] && mv "ext/小工具" "ext/tools" && echo " 小工具 -> tools") || true
([ -d "归档" ] && [ ! -e "archive" ] && mv "归档" "archive" && echo " 归档 -> archive") || true
([ -d "测试" ] && [ ! -e "test" ] && mv "测试" "test" && echo " 测试 -> test") || true
# 4. 更新 site.db
echo ""
echo "【4】更新 site.db"
python3 - << 'PY2'
import json, os, re, sqlite3
REPLACES = [
("/www/wwwroot/自营/玩值/", "/www/wwwroot/self/wanzhi/"),
("/www/wwwroot/自营/", "/www/wwwroot/self/"),
("/www/wwwroot/扩展/小工具/", "/www/wwwroot/ext/tools/"),
("/www/wwwroot/扩展/", "/www/wwwroot/ext/"),
("/www/wwwroot/客户/", "/www/wwwroot/client/"),
("/www/wwwroot/归档/", "/www/wwwroot/archive/"),
("/www/wwwroot/测试/", "/www/wwwroot/test/"),
]
def replace_path(s):
if not s or not isinstance(s, str): return s
for a, b in REPLACES:
s = s.replace(a, b)
return s
def replace_in_obj(obj):
if isinstance(obj, dict):
return {k: replace_in_obj(v) for k, v in obj.items()}
if isinstance(obj, list):
return [replace_in_obj(x) for x in obj]
if isinstance(obj, str) and "/www/wwwroot/" in obj:
return replace_path(obj)
return obj
db = "/www/server/panel/data/db/site.db"
if os.path.isfile(db):
conn = sqlite3.connect(db)
c = conn.cursor()
# 获取需要 path 或 project_config 的列
c.execute("PRAGMA table_info(sites)")
cols = [r[1] for r in c.fetchall()]
path_cols = [x for x in cols if "path" in x.lower() or "config" in x.lower()]
c.execute("SELECT id, path, project_config FROM sites")
n = 0
for row in c.fetchall():
sid, path, cfg = row[0], row[1] or "", row[2] or "{}"
new_path = replace_path(path)
try:
cfg_obj = json.loads(cfg) if cfg else {}
new_cfg = replace_in_obj(cfg_obj)
new_cfg_str = json.dumps(new_cfg, ensure_ascii=False)
except:
new_cfg_str = replace_path(cfg)
if new_path != path or new_cfg_str != cfg:
c.execute("UPDATE sites SET path=?, project_config=? WHERE id=?", (new_path, new_cfg_str, sid))
n += 1
conn.commit()
conn.close()
print(" 更新 %d 条 sites 记录" % n)
else:
print(" site.db 不存在")
PY2
# 5. 更新 Nginx 配置
echo ""
echo "【5】更新 Nginx 配置"
for f in /www/server/panel/vhost/nginx/*.conf /www/server/nginx/conf/vhost/*.conf 2>/dev/null; do
[ -f "$f" ] || continue
if grep -q "自营\|扩展\|客户\|玩值\|小工具\|归档\|测试" "$f" 2>/dev/null; then
sed -i 's|/www/wwwroot/自营/玩值/|/www/wwwroot/self/wanzhi/|g' "$f"
sed -i 's|/www/wwwroot/自营/|/www/wwwroot/self/|g' "$f"
sed -i 's|/www/wwwroot/扩展/小工具/|/www/wwwroot/ext/tools/|g' "$f"
sed -i 's|/www/wwwroot/扩展/|/www/wwwroot/ext/|g' "$f"
sed -i 's|/www/wwwroot/客户/|/www/wwwroot/client/|g' "$f"
sed -i 's|/www/wwwroot/归档/|/www/wwwroot/archive/|g' "$f"
sed -i 's|/www/wwwroot/测试/|/www/wwwroot/test/|g' "$f"
echo " 已更新: $f"
fi
done
# 6. 强制只用宝塔 Nginx
echo ""
echo "【6】Nginx 只用宝塔版"
killall nginx 2>/dev/null || true
sleep 2
/www/server/nginx/sbin/nginx -c /www/server/nginx/conf/nginx.conf 2>/dev/null || true
sleep 1
nginx -t 2>/dev/null && nginx -s reload 2>/dev/null
echo " 宝塔 Nginx 已启动并重载"
# 7. 更新 Node 项目 project_config 中的启动命令路径
echo ""
echo "【7】更新 Node 启动命令路径"
python3 - << 'PY3'
import hashlib, json, os, sqlite3, time, urllib.request, urllib.parse, ssl
ssl._create_default_https_context = ssl._create_unverified_context
REPLACES = [
("/www/wwwroot/自营/玩值/", "/www/wwwroot/self/wanzhi/"),
("/www/wwwroot/自营/", "/www/wwwroot/self/"),
("/www/wwwroot/扩展/小工具/", "/www/wwwroot/ext/tools/"),
("/www/wwwroot/扩展/", "/www/wwwroot/ext/"),
("/www/wwwroot/客户/", "/www/wwwroot/client/"),
]
def replace_path(s):
for a, b in REPLACES: s = s.replace(a, b)
return s
PROJECT_CMD = {
"玩值大屏": "/www/wwwroot/self/wanzhi/玩值大屏",
"tongzhi": "/www/wwwroot/self/wanzhi/tongzhi",
"is_phone": "/www/wwwroot/self/kr/kr-phone",
"ai_hair": "/www/wwwroot/client/ai_hair",
"AITOUFA": "/www/wwwroot/ext/tools/AITOUFA",
"wzdj": "/www/wwwroot/self/wzdj",
"zhiji": "/www/wwwroot/self/zhiji",
"ymao": "/www/wwwroot/ext/ymao",
"zhaoping": "/www/wwwroot/client/zhaoping",
"神射手": "/www/wwwroot/self/kr/kr-use",
"word": "/www/wwwroot/self/word",
}
db = "/www/server/panel/data/db/site.db"
if os.path.isfile(db):
conn = sqlite3.connect(db)
c = conn.cursor()
c.execute("SELECT id, name, project_config FROM sites WHERE project_type='Node'")
for row in c.fetchall():
sid, name, cfg_str = row[0], row[1], row[2] or "{}"
path = PROJECT_CMD.get(name)
if not path: continue
try: cfg = json.loads(cfg_str)
except: cfg = {}
cmd = "cd %s && (pnpm start 2>/dev/null || npm run start)" % path
cfg["project_script"] = cmd
cfg["run_cmd"] = cmd
c.execute("UPDATE sites SET project_config=? WHERE id=?", (json.dumps(cfg, ensure_ascii=False), sid))
print(" %s" % name)
conn.commit()
conn.close()
PY3
# 8. 批量启动 Node 项目
echo ""
echo "【8】批量启动 Node 项目"
python3 - << 'PY4'
import hashlib, json, os, re, 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=30) 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 ps
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 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):
try: open(pf,"w").write("0")
except: pass
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" % name)
time.sleep(1)
time.sleep(5)
r1 = post("/project/nodejs/get_project_list")
items2 = r1.get("data") or r1.get("list") or []
run_c = sum(1 for x in items2 if x.get("run"))
print(" 运行 %d / %d" % (run_c, len(items2)))
PY4
echo ""
echo "=== 迁移完成 ==="
'''
def _read_creds():
d = os.path.dirname(os.path.abspath(__file__))
for _ in range(6):
if os.path.isfile(os.path.join(d, "运营中枢", "工作台", "00_账号与API索引.md")):
with open(os.path.join(d, "运营中枢", "工作台", "00_账号与API索引.md")) as f:
t = f.read()
sid = skey = None
in_t = False
for line in t.splitlines():
if "### 腾讯云" in line: in_t = True; continue
if in_t and line.strip().startswith("###"): break
if not in_t: continue
m = re.search(r"SecretId[^|]*\|\s*`([^`]+)`", line, re.I)
if m and "AKID" in 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("❌ 未配置腾讯云凭证"); 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 = 600
req.CommandName = "kr宝塔_中文目录改英文迁移"
resp = client.RunCommand(req)
print("✅ TAT 已下发 InvocationId:", resp.InvocationId)
print(" 步骤: 停 Node → 删映射 → 重命名目录 → 更新 site.db → 更新 Nginx → 只启用宝塔 Nginx → 启动 Node")
print(" 等待 180s...")
time.sleep(180)
try:
req2 = models.DescribeInvocationTasksRequest()
f = models.Filter()
f.Name, f.Values = "invocation-id", [resp.InvocationId]
req2.Filters = [f]
r2 = client.DescribeInvocationTasks(req2)
for t in (r2.InvocationTaskSet or []):
print(" 状态:", getattr(t, "TaskStatus", ""))
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(" 输出:\n", (out or "")[:6000])
except Exception as e:
print(" 查询:", e)
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -21,9 +21,9 @@ TARGET_NAMES = ["word", "ai_hair", "is_phone"]
# 已知项目路径API 无 path 时的后备)
PATH_MAP = {
"word": "/www/wwwroot/自营/word",
"ai_hair": "/www/wwwroot/客户/ai_hair",
"is_phone": "/www/wwwroot/自营/kr/kr-phone",
"word": "/www/wwwroot/self/word",
"ai_hair": "/www/wwwroot/client/ai_hair",
"is_phone": "/www/wwwroot/self/kr/kr-phone",
}
SHELL_SCRIPT = r'''#!/bin/bash
@@ -33,7 +33,7 @@ python3 - << 'PYEOF'
import hashlib, json, os, 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"
PATH_MAP = {"word": "/www/wwwroot/自营/word", "ai_hair": "/www/wwwroot/客户/ai_hair", "is_phone": "/www/wwwroot/自营/kr/kr-phone"}
PATH_MAP = {"word": "/www/wwwroot/self/word", "ai_hair": "/www/wwwroot/client/ai_hair", "is_phone": "/www/wwwroot/self/kr/kr-phone"}
def sign():
t = int(time.time())
s = str(t) + hashlib.md5(K.encode()).hexdigest()

View File

@@ -15,8 +15,8 @@ from pathlib import Path
OLLAMA_URL = "http://localhost:11434"
DEFAULT_CTA = "关注我,每天学一招私域干货"
CLIP_COUNT = 8
MIN_DURATION = 45
MAX_DURATION = 150
MIN_DURATION = 60 # 1 分钟起
MAX_DURATION = 180 # 3 分钟
def parse_srt_segments(srt_path: str) -> list:
@@ -42,16 +42,16 @@ def parse_srt_segments(srt_path: str) -> list:
def fallback_highlights(transcript_path: str, clip_count: int) -> list:
"""规则备用:按时长均匀切分,每段首句为 Hook"""
"""规则备用:按时长均匀切分,每段 60-180 秒"""
segments = parse_srt_segments(transcript_path)
if not segments:
return []
total = segments[-1]["end_sec"] if segments else 0
interval = max(60, total / (clip_count + 1))
interval = max(120, total / clip_count) # 每段约 2 分钟
result = []
for i in range(clip_count):
start_sec = int(interval * (i + 0.2))
end_sec = min(int(start_sec + 90), int(total - 5))
start_sec = int(interval * i + 30)
end_sec = min(int(start_sec + 120), int(total - 5)) # 约 2 分钟
if end_sec <= start_sec + 30:
continue
# 找该时间段内的字幕
@@ -86,14 +86,15 @@ def srt_to_timestamped_text(srt_path: str) -> str:
def _build_prompt(transcript: str, clip_count: int) -> str:
"""构建高光识别 prompt完整观点+干货,全中文)"""
txt = transcript[:15000] if len(transcript) > 15000 else transcript
"""构建高光识别 prompt完整观点+干货,1-3分钟全中文)"""
txt = transcript[:18000] if len(transcript) > 18000 else transcript
return f"""你是资深短视频策划师。请从视频文字稿中识别 {clip_count} 个**完整的核心观点/干货片段**。
【切片原则】
- 每个片段必须是**完整的一句话/一个观点**,有头有尾,不能截断
- 优先选:金句、完整故事、可操作方法论、反常识观点、情绪高点
- 每个片段时长 {MIN_DURATION}-{MAX_DURATION} 秒,相邻片段间隔至少 30 秒
- 每个片段必须是**完整的一个话题/观点**,有头有尾,逻辑闭环,不能截断
- 时长 **60-180 秒1-3 分钟)**,尽量接近 2 分钟,确保内容完整
- 优先选:金句、完整故事、可操作方法论、反常识观点、情绪高点、成体系讲解
- 相邻片段间隔至少 60 秒
【输出字段】所有内容**必须使用简体中文**,若原文是英文请翻译后填写:
- title: 核心观点标题15字内用于文件名

View File

@@ -92,22 +92,27 @@ KEYWORDS = [
'核心', '关键', '重点', '赚钱', '收入', '利润',
]
# 字体优先级(Mac 优先苹方,更清晰
# 字体优先级(封面用更好看的字体
FONT_PRIORITY = [
"/System/Library/Fonts/PingFang.ttc", # 苹方-简Mac 默认,清晰)
"/System/Library/Fonts/PingFang.ttc", # 苹方-简
"/System/Library/Fonts/Supplemental/Songti.ttc", # 宋体-简
"/System/Library/Fonts/STHeiti Medium.ttc",
"/Library/Fonts/Arial Unicode.ttf",
]
COVER_FONT_PRIORITY = [
"/System/Library/Fonts/PingFang.ttc", # 苹方,封面优先
"/System/Library/Fonts/Supplemental/Songti.ttc",
]
# 样式配置(字体更大、关键词更突出)
# 样式配置
STYLE = {
'cover': {
'bg_blur': 30,
'overlay_alpha': 180,
'bg_blur': 35,
'overlay_alpha': 200,
'duration': 2.5,
},
'hook': {
'font_size': 76,
'font_size': 82, # 更大更清晰
'color': (255, 255, 255),
'outline_color': (30, 30, 50),
'outline_width': 5,
@@ -210,6 +215,73 @@ def parse_srt_for_clip(srt_path, start_sec, end_sec):
return subtitles
def _is_mostly_chinese(text):
if not text or not isinstance(text, str):
return False
chinese = sum(1 for c in text if "\u4e00" <= c <= "\u9fff")
return chinese / max(1, len(text.strip())) > 0.3
def _translate_to_chinese(text):
"""Ollama 翻译英文为中文"""
if not text or _is_mostly_chinese(text):
return text
try:
import requests
r = requests.post(
"http://localhost:11434/api/generate",
json={
"model": "qwen2.5:1.5b",
"prompt": f"将以下翻译成简体中文,只输出中文:\n{text[:150]}",
"stream": False,
"options": {"temperature": 0.1, "num_predict": 80},
},
timeout=15,
)
if r.status_code == 200:
out = r.json().get("response", "").strip().split("\n")[0][:80]
if out:
return out
except Exception:
pass
return text
def detect_burned_subs(video_path, num_samples=2):
"""检测视频是否已有烧录字幕/图片OCR 采样底部区域)"""
try:
import pytesseract
pytesseract.get_tesseract_version()
except Exception:
return False # 无 tesseract 则假定无字幕,执行烧录
try:
duration = get_video_info(video_path).get("duration", 0)
if duration < 1:
return False
for i in range(num_samples):
t = duration * (0.25 + 0.25 * i)
frame = tempfile.mktemp(suffix=".jpg")
subprocess.run([
"ffmpeg", "-y", "-ss", str(t), "-i", video_path,
"-vframes", "1", "-q:v", "2", frame
], capture_output=True)
if os.path.exists(frame):
try:
img = Image.open(frame)
w, h = img.size
crop = img.crop((0, int(h * 0.65), w, h)) # 底部 35%
text = pytesseract.image_to_string(crop, lang="chi_sim+eng")
os.remove(frame)
if text and len(text.strip()) > 15:
return True
except Exception:
if os.path.exists(frame):
os.remove(frame)
except Exception:
pass
return False
def get_video_info(video_path):
"""获取视频信息"""
cmd = [
@@ -239,8 +311,19 @@ def get_video_info(video_path):
# ============ 封面生成 ============
def get_cover_font(size):
"""封面专用字体(更好看)"""
for path in COVER_FONT_PRIORITY + FONT_PRIORITY + [FONT_BOLD]:
if path and os.path.exists(path):
try:
return ImageFont.truetype(path, size)
except Exception:
continue
return ImageFont.load_default()
def create_cover_image(hook_text, width, height, output_path, video_path=None):
"""创建封面贴片(简体中文)"""
"""创建封面贴片(简体中文,字体优化"""
hook_text = _to_simplified(str(hook_text or "").strip())
if not hook_text:
hook_text = "精彩切片"
@@ -281,8 +364,8 @@ def create_cover_image(hook_text, width, height, output_path, video_path=None):
alpha = 150 - i * 40
draw.rectangle([0, height - i*3 - 2, width, height - i*3], fill=(255, 215, 0, alpha))
# Hook文字自动换行
font = get_font(FONT_HEAVY, hook_style['font_size'])
# Hook 文字(封面用更好看的字体
font = get_cover_font(hook_style['font_size'])
# 计算换行
max_width = width - 80
@@ -301,14 +384,13 @@ def create_cover_image(hook_text, width, height, output_path, video_path=None):
if current_line:
lines.append(current_line)
# 绘制文字(整体向右偏移 6%,减少右侧空白
# 绘制文字(完全居中
line_height = hook_style['font_size'] + 15
total_height = len(lines) * line_height
start_y = (height - total_height) // 2
x_offset = int(width * 0.06) # 向右偏移
for i, line in enumerate(lines):
line_w, line_h = get_text_size(draw, line, font)
x = (width - line_w) // 2 + x_offset
x = (width - line_w) // 2
y = start_y + i * line_height
draw_text_with_outline(
@@ -336,9 +418,8 @@ def create_subtitle_image(text, width, height, output_path):
kw_font = get_font(FONT_HEAVY, kw_size) # 关键词用粗体+大字
text_w, text_h = get_text_size(draw, text, font)
# 字幕整体向右偏移 6%,减少右侧空白
x_offset = int(width * 0.06)
base_x = (width - text_w) // 2 + x_offset
# 字幕完全居中
base_x = (width - text_w) // 2
base_y = height - text_h - style['margin_bottom']
# 背景条
@@ -480,8 +561,9 @@ def _parse_clip_index(filename: str) -> int:
return int(m.group()) if m else 0
def enhance_clip(clip_path, output_path, highlight_info, temp_dir, transcript_path):
"""增强单个切片"""
def enhance_clip(clip_path, output_path, highlight_info, temp_dir, transcript_path,
force_burn_subs=False, skip_subs=False):
"""增强单个切片。检测原片是否已有字幕,有则跳过烧录,无则烧录中文"""
print(f"\n处理: {os.path.basename(clip_path)}")
@@ -493,36 +575,37 @@ def enhance_clip(clip_path, output_path, highlight_info, temp_dir, transcript_pa
hook_text = highlight_info.get('hook_3sec') or highlight_info.get('title') or ''
if not hook_text and clip_path:
# 从文件名提取标题soul106_01_标题.mp4
m = re.search(r'\d+[_\s]+(.+?)(?:_enhanced)?\.mp4$', os.path.basename(clip_path))
if m:
hook_text = m.group(1).strip()
cover_duration = STYLE['cover']['duration']
# 1. 生成封面图片
# 1. 生成封面
cover_img = os.path.join(temp_dir, 'cover.png')
create_cover_image(hook_text, width, height, cover_img, clip_path)
print(f" ✓ 封面生成")
# 2. 解析字幕
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])
end_sec = start_sec + duration
subtitles = parse_srt_for_clip(transcript_path, start_sec, end_sec)
print(f" ✓ 字幕解析 ({len(subtitles)}条)")
# 3. 生成字幕图片
# 2. 字幕逻辑:有字幕/图片则跳过,无则烧录中文
sub_images = []
for i, sub in enumerate(subtitles[:50]): # 限制50条
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']
})
do_burn_subs = not skip_subs and (force_burn_subs or not detect_burned_subs(clip_path))
if skip_subs:
print(f" ⊘ 跳过字幕烧录(--skip-subs")
elif not do_burn_subs:
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])
end_sec = start_sec + duration
subtitles = parse_srt_for_clip(transcript_path, start_sec, end_sec)
for sub in subtitles:
if not _is_mostly_chinese(sub['text']):
sub['text'] = _translate_to_chinese(sub['text']) or sub['text']
print(f" ✓ 字幕解析 ({len(subtitles)}条),已转中文")
for i, sub in enumerate(subtitles[:50]):
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)}张)")
# 4. 检测静音
@@ -620,6 +703,8 @@ def main():
parser.add_argument("--highlights", "-l", help="highlights.json 路径")
parser.add_argument("--transcript", "-t", help="transcript.srt 路径")
parser.add_argument("--output", "-o", help="输出目录")
parser.add_argument("--skip-subs", action="store_true", help="跳过字幕烧录(原片已有字幕时用)")
parser.add_argument("--force-burn-subs", action="store_true", help="强制烧录字幕(忽略检测)")
args = parser.parse_args()
clips_dir = Path(args.clips) if args.clips else CLIPS_DIR
@@ -646,9 +731,7 @@ def main():
print("="*60)
output_dir.mkdir(parents=True, exist_ok=True)
# 清空已有增强切片,避免重复
for f in output_dir.glob("*.mp4"):
f.unlink()
# 同名直接覆盖,不预先清空
with open(highlights_path, 'r', encoding='utf-8') as f:
highlights = json.load(f)
@@ -668,7 +751,9 @@ def main():
temp_dir = tempfile.mkdtemp(prefix='enhance_')
try:
if enhance_clip(str(clip_path), str(output_path), highlight_info, temp_dir, str(transcript_path)):
if enhance_clip(str(clip_path), str(output_path), highlight_info, temp_dir, str(transcript_path),
force_burn_subs=getattr(args, 'force_burn_subs', False),
skip_subs=getattr(args, 'skip_subs', False)):
success_count += 1
finally:
shutil.rmtree(temp_dir, ignore_errors=True)

View File

@@ -88,3 +88,4 @@
| 2026-02-22 11:58:17 | 🔄 卡若AI 同步 2026-02-22 11:58 | 更新:金仓、水桥平台对接、卡木、运营中枢工作台 | 排除 >20MB: 8 个 |
| 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 个 |

View File

@@ -91,3 +91,4 @@
| 2026-02-22 11:58:17 | 成功 | 成功 | 🔄 卡若AI 同步 2026-02-22 11:58 | 更新:金仓、水桥平台对接、卡木、运营中枢工作台 | 排除 >20MB: 8 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |
| 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) |